feat(kon): add full transcript provenance — enriched schema, metadata pipeline, error log

Schema enrichment:
- transcripts table: +engine, +model_id, +inference_ms, +sample_rate, +audio_channels,
  +format_mode, +remove_fillers, +british_english, +anti_hallucination
- New error_log table: context, error_code, message, metadata (JSON)
- 5 indices on frequently queried columns

Metadata pipeline:
- TranscriptMetadata struct in kon-core for full provenance capture
- LocalEngine tracks loaded ModelId, returns TimedTranscript with inference_ms
- std::time::Instant timing around transcribe-rs inference calls
- Tauri transcription commands emit inference_ms in event payloads
- InsertTranscriptParams accepts all provenance fields
- log_error() for structured error logging to SQLite

Every transcript is now fully reproducible from its stored metadata.
23 tests passing, clippy clean.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jake
2026-03-16 21:36:44 +00:00
parent 29ff91d3f6
commit 292f9716ff
9 changed files with 276 additions and 100 deletions

View File

@@ -84,10 +84,11 @@ pub async fn load_model(
}
let engine = state.whisper_engine.clone();
let mid = id.clone();
tokio::task::spawn_blocking(move || {
let model = kon_transcription::load_whisper(&model_file)
.map_err(|e| e.to_string())?;
engine.load(model);
engine.load(model, mid);
Ok::<_, String>(())
})
.await
@@ -152,10 +153,11 @@ pub async fn load_parakeet_model(
}
let engine = state.parakeet_engine.clone();
let mid = id.clone();
tokio::task::spawn_blocking(move || {
let model = kon_transcription::load_parakeet(&dir)
.map_err(|e| e.to_string())?;
engine.load(model);
engine.load(model, mid);
Ok::<_, String>(())
})
.await

View File

@@ -31,15 +31,13 @@ pub async fn transcribe_pcm(
initial_prompt: Some(initial_prompt),
};
let result = tokio::task::spawn_blocking(move || {
let transcript = engine.transcribe_sync(&audio, &options)
.map_err(|e| e.to_string())?;
Ok::<_, String>(transcript)
let timed = tokio::task::spawn_blocking(move || {
engine.transcribe_sync(&audio, &options).map_err(|e| e.to_string())
})
.await
.map_err(|e| e.to_string())??;
let mut segments: Vec<Segment> = result.segments().to_vec();
let mut segments: Vec<Segment> = timed.transcript.segments().to_vec();
post_process_segments(
&mut segments,
&PostProcessOptions {
@@ -55,9 +53,10 @@ pub async fn transcribe_pcm(
serde_json::json!({
"status": "transcription",
"segments": segments,
"language": result.language(),
"duration": result.duration(),
"language": timed.transcript.language(),
"duration": timed.transcript.duration(),
"chunk_id": chunk_id,
"inference_ms": timed.inference_ms,
}),
)
.map_err(|e| format!("Failed to emit result: {e}"))?;
@@ -83,20 +82,19 @@ pub async fn transcribe_file(
initial_prompt: Some(initial_prompt),
};
let result = tokio::task::spawn_blocking(move || {
let timed = tokio::task::spawn_blocking(move || {
let audio = kon_audio::decode_audio_file(Path::new(&path))
.map_err(|e| e.to_string())?;
let resampled = kon_audio::resample_to_16khz(&audio)
.map_err(|e| e.to_string())?;
let transcript = engine
let resampled =
kon_audio::resample_to_16khz(&audio).map_err(|e| e.to_string())?;
engine
.transcribe_sync(&resampled, &options)
.map_err(|e| e.to_string())?;
Ok::<_, String>(transcript)
.map_err(|e| e.to_string())
})
.await
.map_err(|e| e.to_string())??;
let mut segments: Vec<Segment> = result.segments().to_vec();
let mut segments: Vec<Segment> = timed.transcript.segments().to_vec();
post_process_segments(
&mut segments,
&PostProcessOptions {
@@ -109,8 +107,9 @@ pub async fn transcribe_file(
Ok(serde_json::json!({
"segments": segments,
"language": result.language(),
"duration": result.duration(),
"language": timed.transcript.language(),
"duration": timed.transcript.duration(),
"inference_ms": timed.inference_ms,
}))
}
@@ -129,16 +128,13 @@ pub async fn transcribe_pcm_parakeet(
let audio = kon_core::AudioSamples::mono_16khz(samples);
let options = TranscriptionOptions::default();
let result = tokio::task::spawn_blocking(move || {
let transcript = engine
.transcribe_sync(&audio, &options)
.map_err(|e| e.to_string())?;
Ok::<_, String>(transcript)
let timed = tokio::task::spawn_blocking(move || {
engine.transcribe_sync(&audio, &options).map_err(|e| e.to_string())
})
.await
.map_err(|e| e.to_string())??;
let mut segments: Vec<Segment> = result.segments().to_vec();
let mut segments: Vec<Segment> = timed.transcript.segments().to_vec();
post_process_segments(
&mut segments,
&PostProcessOptions {
@@ -154,9 +150,10 @@ pub async fn transcribe_pcm_parakeet(
serde_json::json!({
"status": "transcription",
"segments": segments,
"language": result.language(),
"duration": result.duration(),
"language": timed.transcript.language(),
"duration": timed.transcript.duration(),
"chunk_id": chunk_id,
"inference_ms": timed.inference_ms,
}),
)
.map_err(|e| format!("Failed to emit result: {e}"))?;