Extract transcribers into self-registering Rake classes
Each transcriber owns its setup as Rake file tasks, so
prereqs flow through the dependency graph naturally.

Assisted-by: Claude Opus 4.6 via pi
change qsqltxztnllmvlqoxzokvyyosypuwmyt
commit 7eaf32328b87a1af35d92a5ac24992bddfc17349
author Alpha Chen <alpha@kejadlen.dev>
date
parent ruqlqqrs
diff --git a/Rakefile b/Rakefile
index 5264796..1c16d92 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,22 +1,18 @@
 require "net/http"
 require "pathname"
 require "uri"
+
 require_relative "lib/feed"
 require_relative "lib/download"
 
-TRANSCRIBER = ENV.fetch("TRANSCRIBER", "whisperx")
-
 CACHE_DIR = Pathname("cache")
 AUDIO_DIR = CACHE_DIR / "audio"
 TRANSCRIPTS_DIR = Pathname("transcripts")
-SOUS_CHEF = Pathname("sous_chef/.build/release/sous_chef")
 HRN_FEED = CACHE_DIR / "hrn_feed.xml"
 HRN_FEED_URL = "https://rss.art19.com/cooking-issues"
-MODELS_DIR = CACHE_DIR / "models"
 
 directory CACHE_DIR.to_s
 directory AUDIO_DIR.to_s
-directory MODELS_DIR.to_s
 
 file HRN_FEED.to_s => CACHE_DIR.to_s do
   puts "Downloading HRN feed..."
@@ -27,24 +23,10 @@ file HRN_FEED.to_s => CACHE_DIR.to_s do
   HRN_FEED.write(response.body)
 end
 
-file SOUS_CHEF.to_s do
-  sh "cd sous_chef && swift build -c release"
-end
-
-DOWNLOAD_SCRIPT = CACHE_DIR / "download-ggml-model.sh"
-DOWNLOAD_SCRIPT_URL = "https://raw.githubusercontent.com/ggml-org/whisper.cpp/master/models/download-ggml-model.sh"
+load File.expand_path("transcribers.rake", __dir__)
 
-file DOWNLOAD_SCRIPT.to_s => CACHE_DIR.to_s do
-  puts "Downloading whisper model script..."
-  CookingIssues::Download.fetch(DOWNLOAD_SCRIPT_URL, DOWNLOAD_SCRIPT.to_s)
-  chmod 0o755, DOWNLOAD_SCRIPT.to_s
-end
-
-desc "Download a whisper.cpp GGML model (e.g., rake model[large-v3-turbo])"
-task :model, [:name] => [DOWNLOAD_SCRIPT.to_s, MODELS_DIR.to_s] do |_t, args|
-  abort "Usage: rake model[NAME]" unless args[:name]
-  sh DOWNLOAD_SCRIPT.to_s, args[:name], MODELS_DIR.to_s
-end
+TRANSCRIBER = Transcribers.resolve(ENV.fetch("TRANSCRIBER", "whisperx"))
+TRANSCRIBER.register
 
 Rake::Task[HRN_FEED.to_s].invoke
 
@@ -55,38 +37,11 @@ def audio_path(ep)
 end
 
 def transcript_path(ep)
-  (TRANSCRIPTS_DIR / TRANSCRIBER / "#{ep.slug}.txt").to_s
-end
-
-def transcribe(ep, audio_path, transcript_path)
-  case TRANSCRIBER
-  when "whisperx"
-    hf_token = ENV.fetch("HUGGING_FACE_TOKEN") { abort "Set HUGGING_FACE_TOKEN for diarization." }
-    sh "whisperx", audio_path,
-      "--model", "large-v3",
-      "--compute_type", "int8",
-      "--device", "cpu",
-      "--diarize", "--hf_token", hf_token,
-      "--output_dir", File.dirname(transcript_path),
-      "--output_format", "txt"
-  when "whisper-cpp-large"
-    model_path = MODELS_DIR / "ggml-large-v3-turbo.bin"
-    Rake::Task[:model].invoke("large-v3-turbo") unless model_path.exist?
-    sh "whisper-cli", "--model", model_path.to_s, "--output-txt", "--output-file", transcript_path.delete_suffix(".txt"), audio_path
-  when "whisper-cpp-tdrz"
-    model_path = MODELS_DIR / "ggml-small.en-tdrz.bin"
-    Rake::Task[:model].invoke("small.en-tdrz") unless model_path.exist?
-    sh "whisper-cli", "--model", model_path.to_s, "-tdrz", "--output-txt", "--output-file", transcript_path.delete_suffix(".txt"), audio_path
-  when "sous_chef"
-    Rake::Task[SOUS_CHEF.to_s].invoke
-    sh SOUS_CHEF.to_s, audio_path, transcript_path
-  else
-    abort "Unknown transcriber: #{TRANSCRIBER}. Use 'whisperx', 'whisper-cpp-large', 'whisper-cpp-tdrz', or 'sous_chef'."
-  end
+  (TRANSCRIPTS_DIR / TRANSCRIBER.name / "#{ep.slug}.txt").to_s
 end
 
 EPISODES.values.each do |ep|
-  transcript_dir = (TRANSCRIPTS_DIR / TRANSCRIBER).to_s
+  transcript_dir = (TRANSCRIPTS_DIR / TRANSCRIBER.name).to_s
   directory transcript_dir
 
   audio = audio_path(ep)
@@ -97,13 +52,11 @@ EPISODES.values.each do |ep|
     CookingIssues::Download.fetch(ep.audio_url, audio)
   end
 
-  file transcript => [audio, transcript_dir] do
-    transcribe(ep, audio, transcript)
+  file transcript => [audio, transcript_dir, *TRANSCRIBER.prereqs] do
+    TRANSCRIBER.call(audio, transcript)
   end
 end
 
-# --- Tasks ---
-
 task default: :sync
 
 desc "Download and transcribe all episodes"
diff --git a/transcribers.rake b/transcribers.rake
new file mode 100644
index 0000000..916de2d
--- /dev/null
+++ b/transcribers.rake
@@ -0,0 +1,118 @@
+module Transcribers
+  MODELS_DIR = CACHE_DIR / "models"
+  DOWNLOAD_SCRIPT = CACHE_DIR / "download-ggml-model.sh"
+  DOWNLOAD_SCRIPT_URL = "https://raw.githubusercontent.com/ggml-org/whisper.cpp/master/models/download-ggml-model.sh"
+
+  def self.resolve(name)
+    case name
+    when "whisperx" then Whisperx.new
+    when "whisper-cpp-large" then WhisperCppLarge.new
+    when "whisper-cpp-tdrz" then WhisperCppTdrz.new
+    when "sous_chef" then SousChef.new
+    else
+      abort "Unknown transcriber: #{name}. Use 'whisperx', 'whisper-cpp-large', 'whisper-cpp-tdrz', or 'sous_chef'."
+    end
+  end
+
+  def self.model_path(name)
+    MODELS_DIR / "ggml-#{name}.bin"
+  end
+
+  class Base
+    include Rake::DSL
+
+    def prereqs = []
+    def register; end
+
+    private
+
+    # Shared setup for whisper.cpp model downloads.
+    def register_model(name)
+      models_dir = MODELS_DIR
+      script = DOWNLOAD_SCRIPT
+
+      directory models_dir.to_s
+
+      file script.to_s => CACHE_DIR.to_s do
+        puts "Downloading whisper model script..."
+        CookingIssues::Download.fetch(DOWNLOAD_SCRIPT_URL, script.to_s)
+        script.chmod(0o755)
+      end
+
+      file Transcribers.model_path(name).to_s => [script.to_s, models_dir.to_s] do
+        sh script.to_s, name, models_dir.to_s
+      end
+    end
+  end
+
+  class Whisperx < Base
+    def name = "whisperx"
+
+    def call(audio_path, transcript_path)
+      hf_token = ENV.fetch("HUGGING_FACE_TOKEN") { abort "Set HUGGING_FACE_TOKEN for diarization." }
+      sh "whisperx", audio_path,
+        "--model", "large-v3",
+        "--compute_type", "int8",
+        "--device", "cpu",
+        "--diarize", "--hf_token", hf_token,
+        "--output_dir", File.dirname(transcript_path),
+        "--output_format", "txt"
+    end
+  end
+
+  class WhisperCppLarge < Base
+    MODEL = "large-v3-turbo"
+
+    def name = "whisper-cpp-large"
+    def prereqs = [Transcribers.model_path(MODEL).to_s]
+
+    def register
+      register_model(MODEL)
+    end
+
+    def call(audio_path, transcript_path)
+      sh "whisper-cli",
+        "--model", Transcribers.model_path(MODEL).to_s,
+        "--output-txt",
+        "--output-file", transcript_path.delete_suffix(".txt"),
+        audio_path
+    end
+  end
+
+  class WhisperCppTdrz < Base
+    MODEL = "small.en-tdrz"
+
+    def name = "whisper-cpp-tdrz"
+    def prereqs = [Transcribers.model_path(MODEL).to_s]
+
+    def register
+      register_model(MODEL)
+    end
+
+    def call(audio_path, transcript_path)
+      sh "whisper-cli",
+        "--model", Transcribers.model_path(MODEL).to_s,
+        "-tdrz",
+        "--output-txt",
+        "--output-file", transcript_path.delete_suffix(".txt"),
+        audio_path
+    end
+  end
+
+  class SousChef < Base
+    BINARY = Pathname("sous_chef/.build/release/sous_chef")
+
+    def name = "sous_chef"
+    def prereqs = [BINARY.to_s]
+
+    def register
+      file BINARY.to_s do
+        sh "cd sous_chef && swift build -c release"
+      end
+    end
+
+    def call(audio_path, transcript_path)
+      sh BINARY.to_s, audio_path, transcript_path
+    end
+  end
+end