Add timestamps to sous_chef output and enforce register
Switch to timeIndexedTranscriptionWithAlternatives preset so
each segment carries its audio time range.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/sous_chef/Sources/SousChef.swift b/sous_chef/Sources/SousChef.swift
index 91b7b21..2484017 100644
--- a/sous_chef/Sources/SousChef.swift
+++ b/sous_chef/Sources/SousChef.swift
@@ -23,7 +23,7 @@ struct SousChef: AsyncParsableCommand {
guard let locale = await SpeechTranscriber.supportedLocale(equivalentTo: Locale(identifier: "en-US")) else {
throw TranscriptionError.unsupportedLocale
}
- let transcriber = SpeechTranscriber(locale: locale, preset: .transcription)
+ let transcriber = SpeechTranscriber(locale: locale, preset: .timeIndexedTranscriptionWithAlternatives)
// Install assets if needed.
if let request = try await AssetInventory.assetInstallationRequest(supporting: [transcriber]) {
@@ -39,13 +39,15 @@ struct SousChef: AsyncParsableCommand {
// Collect results in a separate task.
let resultsTask = Task {
- var segments: [String] = []
+ var lines: [String] = []
for try await result in transcriber.results {
let text = String(result.text.characters)
- segments.append(text)
- log(" \(text)")
+ let timestamp = Self.formatTimestamp(result.text)
+ let line = "[\(timestamp)] \(text)"
+ lines.append(line)
+ log(" \(line)")
}
- return segments
+ return lines
}
// Run the analysis.
@@ -59,13 +61,29 @@ struct SousChef: AsyncParsableCommand {
await analyzer.cancelAndFinishNow()
}
- let segments = try await resultsTask.value
+ let lines = try await resultsTask.value
- // Write plain text output.
- let text = segments.joined(separator: "\n")
+ // Write timestamped transcript.
+ let text = lines.joined(separator: "\n")
try text.write(to: outputURL, atomically: true, encoding: .utf8)
- log("Done. Wrote \(segments.count) segments to \(output).")
+ log("Done. Wrote \(lines.count) segments to \(output).")
+ }
+
+ /// Extract the start time from the first audioTimeRange attribute in the result text.
+ private static func formatTimestamp(_ text: AttributedString) -> String {
+ for run in text.runs {
+ if let timeRange = run[AttributeScopes.SpeechAttributes.TimeRangeAttribute.self] {
+ let seconds = CMTimeGetSeconds(timeRange.start)
+ let h = Int(seconds) / 3600
+ let m = (Int(seconds) % 3600) / 60
+ let s = Int(seconds) % 60
+ return h > 0
+ ? String(format: "%d:%02d:%02d", h, m, s)
+ : String(format: "%d:%02d", m, s)
+ }
+ }
+ return "?:??"
}
private func log(_ message: String) {
diff --git a/transcribers.rake b/transcribers.rake
index 916de2d..aca6a8a 100644
--- a/transcribers.rake
+++ b/transcribers.rake
@@ -22,11 +22,29 @@ module Transcribers
include Rake::DSL
def prereqs = []
+
+ def register
+ raise NotImplementedError, "#{self.class}#register not implemented"
+ end
+ end
+
+ class Whisperx < Base
+ def name = "whisperx"
def register; end
- private
+ def call(audio_path, transcript_path)
+ hf_token = ENV.fetch("HUGGING_FACE_TOKEN") { abort "Set HUGGING_FACE_TOKEN for diarization." }
+ sh "uvx", "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
- # Shared setup for whisper.cpp model downloads.
+ class WhisperCpp < Base
def register_model(name)
models_dir = MODELS_DIR
script = DOWNLOAD_SCRIPT
@@ -45,23 +63,9 @@ module Transcribers
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"
+ class WhisperCppLarge < WhisperCpp
+ # MODEL = "large-v3-turbo"
+ MODEL = "large-v3"
def name = "whisper-cpp-large"
def prereqs = [Transcribers.model_path(MODEL).to_s]
@@ -79,7 +83,7 @@ module Transcribers
end
end
- class WhisperCppTdrz < Base
+ class WhisperCppTdrz < WhisperCpp
MODEL = "small.en-tdrz"
def name = "whisper-cpp-tdrz"
@@ -106,7 +110,8 @@ module Transcribers
def prereqs = [BINARY.to_s]
def register
- file BINARY.to_s do
+ sources = FileList["sous_chef/**/*.swift"].exclude(%r{/\.build/})
+ file BINARY.to_s => sources do
sh "cd sous_chef && swift build -c release"
end
end