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
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