Skip to content

Instantly share code, notes, and snippets.

@ttscoff
Last active March 23, 2026 15:25
Show Gist options
  • Select an option

  • Save ttscoff/61bb59ce78e0ad85c8f0433aaf9992ba to your computer and use it in GitHub Desktop.

Select an option

Save ttscoff/61bb59ce78e0ad85c8f0433aaf9992ba to your computer and use it in GitHub Desktop.
Search audio files for text using whisper.cpp and ffmpeg
#!/usr/bin/env ruby
# A simple script to index and search audio files using whisper.cpp
# Saves a text file for each audio file with the transcript, and allows searching through them
# Usage:
# ruby audio-search.rb index [file.mp3] # Index a single file or all .mp3 files if no file is specified
# ruby audio-search.rb search [query] # Search for a query in the indexed transcripts, whole words in search order but allowing characters between
# Search can be fuzzy (words in any order) by adding --fuzzy or -f flag:
# ruby audio-search.rb search --fuzzy [query]
# Requires ffmpeg and whisper-cli to be installed and in the system PATH
require 'fileutils'
# location of whisper.cpp model file
model = "/Users/ttscoff/Desktop/Code/github/whisper.cpp/models/ggml-base.en.bin"
def index_all(model)
Dir.glob("**/*.mp3").each do |file|
txt_target = "#{File.basename(file, ".mp3")}.txt"
unless File.exist?(txt_target)
index_file(file, model)
end
end
end
def index_file(file, model)
wav_target = "#{File.basename(file, ".mp3")}.wav"
txt_target = "#{File.basename(file, ".mp3")}.txt"
# Always overwrite: remove existing wav/txt if present
FileUtils.rm_f(wav_target) if File.exist?(wav_target)
FileUtils.rm_f(txt_target) if File.exist?(txt_target)
`ffmpeg -i '#{file}' -ar 16000 -ac 1 -c:a pcm_s16le '#{wav_target}'`
transcript = `whisper-cli -m #{model} -f "#{wav_target}"`
File.write(txt_target, transcript)
FileUtils.rm_rf(wav_target)
end
def search(query, fuzzy: false)
results = []
Dir.glob("**/*.txt").each do |file|
content = File.read(file)
if fuzzy
# Fuzzy search: check if all query words are present in the content, regardless of order
query.split.each do |word|
unless content =~ /#{Regexp.escape(word)}/i
next
end
end
results << "#{File.basename(file, ".txt")}.mp3"
else
# Whole word search: split query into words, escape them for regex, and join with ".*" to allow for any characters in between
query_rx = query.split.map { |word| Regexp.escape(word) }.join(".*")
if content =~ /#{query_rx}/i
results << "#{File.basename(file, ".txt")}.mp3"
end
end
end
results
end
if ARGV.length == 0
puts "Usage: #{__FILE__} [index|search] [query]"
exit
end
command = ARGV[0]
case command
when "index"
if ARGV.length < 2
index_all(model)
else
file = ARGV[1]
unless File.exist?(file)
puts "File not found: #{file}"
exit
end
index_file(file, model)
end
when "search"
if ARGV.include?("--fuzzy") || ARGV.include?("-f")
fuzzy = true
ARGV.delete("--fuzzy")
ARGV.delete("-f")
else
fuzzy = false
end
if ARGV.length < 2
puts "Usage: #{__FILE__} search [query]"
exit
end
query = ARGV[1]
results = search(query, fuzzy: fuzzy)
if results.empty?
puts "No matches found for '#{query}'"
else
results.each { |file| puts "#{file}" }
end
else
puts "Unknown command: #{command}"
puts "Usage: #{__FILE__} [index|search] [query]"
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment