Last active
March 23, 2026 15:25
-
-
Save ttscoff/61bb59ce78e0ad85c8f0433aaf9992ba to your computer and use it in GitHub Desktop.
Search audio files for text using whisper.cpp and ffmpeg
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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