Add text rendering step for whisper-cpp transcripts
Groups segments into paragraphs by silence gaps (500ms
threshold). Other transcribers raise NotImplementedError
until their renderers are added.

Assisted-by: Claude Opus 4.6 via pi
change lpsnoxmnyqqppokkztmuzmsolsopksut
commit 6abc330e02f4ca97fff2087c053c2ff3cc66f4b8
author Alpha Chen <alpha@kejadlen.dev>
date
parent nttslpyz
diff --git a/Rakefile b/Rakefile
index 05cc1b6..fca577e 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,3 +1,4 @@
+require "json"
 require "net/http"
 require "pathname"
 require "uri"
@@ -40,6 +41,10 @@ def transcript_path(ep)
   (TRANSCRIPTS_DIR / TRANSCRIBER.name / "#{ep.slug}.json").to_s
 end
 
+def text_path(ep)
+  (TRANSCRIPTS_DIR / TRANSCRIBER.name / "#{ep.slug}.txt").to_s
+end
+
 EPISODES.values.each do |ep|
   transcript_dir = (TRANSCRIPTS_DIR / TRANSCRIBER.name).to_s
   directory transcript_dir
@@ -55,22 +60,28 @@ EPISODES.values.each do |ep|
   file transcript => [audio, transcript_dir, *TRANSCRIBER.prereqs] do
     TRANSCRIBER.call(audio, transcript)
   end
+
+  txt = text_path(ep)
+
+  file txt => transcript do
+    TRANSCRIBER.render(transcript, txt)
+  end
 end
 
 task default: :sync
 
-desc "Download and transcribe all episodes"
+desc "Download, transcribe, and render all episodes"
 task :sync do
   # The feed is reverse-chronological; process oldest episodes first.
   EPISODES.values.sort_by(&:number).each do |ep|
-    Rake::Task[transcript_path(ep)].invoke
+    Rake::Task[text_path(ep)].invoke
   end
 end
 
 desc "List all episodes from the feed"
 task :episodes do
   EPISODES.values.sort_by(&:number).each do |ep|
-    status = Pathname(transcript_path(ep)).exist? ? "✓" : " "
+    status = Pathname(text_path(ep)).exist? ? "✓" : " "
     puts "[#{status}] #{ep.slug}  #{ep.title}"
   end
 end
@@ -82,7 +93,7 @@ task :transcribe, [:number] do |_t, args|
   ep = EPISODES[args[:number].to_i]
   abort "Episode #{args[:number]} not found in feed." unless ep
 
-  Rake::Task[transcript_path(ep)].invoke
+  Rake::Task[text_path(ep)].invoke
 end
 
 desc "Re-transcribe an episode (e.g., rake retranscribe[42])"
@@ -92,8 +103,11 @@ task :retranscribe, [:number] do |_t, args|
   ep = EPISODES[args[:number].to_i]
   abort "Episode #{args[:number]} not found in feed." unless ep
 
-  path = Pathname(transcript_path(ep))
-  path.delete if path.exist?
+  json = Pathname(transcript_path(ep))
+  txt = Pathname(text_path(ep))
+  json.delete if json.exist?
+  txt.delete if txt.exist?
+  Rake::Task[text_path(ep)].reenable
   Rake::Task[transcript_path(ep)].reenable
-  Rake::Task[transcript_path(ep)].invoke
+  Rake::Task[text_path(ep)].invoke
 end
diff --git a/transcribers.rake b/transcribers.rake
index b48979d..d10e074 100644
--- a/transcribers.rake
+++ b/transcribers.rake
@@ -27,6 +27,10 @@ module Transcribers
     def register
       raise NotImplementedError, "#{self.class}#register not implemented"
     end
+
+    def render(json_path, txt_path)
+      raise NotImplementedError, "#{self.class}#render not implemented"
+    end
   end
 
   class Whisperx < Base
@@ -46,6 +50,39 @@ module Transcribers
   end
 
   class WhisperCpp < Base
+    # Milliseconds of silence between segments that triggers a paragraph break.
+    PARAGRAPH_GAP_MS = 500
+
+    def render(json_path, txt_path)
+      data = JSON.parse(File.read(json_path))
+      segments = data["transcription"]
+
+      paragraphs = []
+      current = []
+
+      segments.each_with_index do |seg, i|
+        if i > 0
+          gap = seg["offsets"]["from"] - segments[i - 1]["offsets"]["to"]
+          if gap >= PARAGRAPH_GAP_MS
+            paragraphs << flush_paragraph(current)
+            current = []
+          end
+        end
+        current << seg
+      end
+      paragraphs << flush_paragraph(current) unless current.empty?
+
+      File.write(txt_path, paragraphs.join("\n\n"))
+    end
+
+    private
+
+    def flush_paragraph(segments)
+      timestamp = segments.first["timestamps"]["from"].sub(/^00:/, "")
+      text = segments.map { |s| s["text"].strip }.join(" ")
+      "[#{timestamp}] #{text}"
+    end
+
     def register_model(name)
       models_dir = MODELS_DIR
       script = DOWNLOAD_SCRIPT