feat: OpenWhispr-inspired transcription polish pass
Major quality pass on top of Phase 2. Five substantive changes plus
cross-cutting touches across audio, hotkey, transcription, and Tauri
command layers.
Transcription quality
- Long-audio chunking in commands/transcription.rs: Parakeet and large
file transcription now chunk-and-recompose with overlap trimming, so
the live-path chunking advantage extends to file-based workflows.
- Stateful live speech gate in commands/live.rs on top of the earlier
duplicate-boundary filtering — distinguishes start-of-speech from
mid-speech and holds state across chunks.
Auto-learning corrections
- New crates/ai-formatting/src/correction_learning.rs: extracts user
text corrections from viewer edits and proposes additions to the
active profile's vocabulary.
- src-tauri/src/commands/profiles.rs bridge for frontend-driven
confirmation of learned terms.
- src/routes/viewer/+page.svelte hooks the learning path into the
segment-edit flow so corrections feed profile_terms without a
separate 'train this profile' UX.
Transcript profile provenance
- Migration v8 (crates/storage/src/migrations.rs) adds profile_id to
transcripts, defaulting to DEFAULT_PROFILE_ID so existing rows stay
valid.
- crates/storage/src/database.rs: TranscriptRow + CRUD carry profile_id.
- src-tauri/src/commands/transcripts.rs: add_transcript accepts and
persists profile_id.
- DictationPage.svelte + FilesPage.svelte send activeProfileId on
capture so learned corrections are attributed to the right profile.
Cleanup prompt contract
- crates/ai-formatting/src/llm_client.rs hardened: the CLEANUP_PROMPT
now specifies concrete do/do-not rules, ready for a real model-backed
cleanup pass. The llm_client is still a stub — kon-llm remains unwired
— but the prompt shape is final.
Cross-cutting polish
- Minor touches in audio (capture/decode/resample), hotkey (lib/linux/stub),
core, transcription (concurrency/model_manager/local_engine/whisper_rs),
and the rest of src-tauri/src/commands/*: error-path tightening, log
clarity, TS-migration follow-ups (@ts-nocheck additions for incremental
typing).
Verified locally: npm run check, cargo test -p kon-ai-formatting,
cargo test -p kon-storage, cargo test -p kon --lib commands::live::tests,
cargo check — all green.
Scope boundary: kon-llm crate is still a stub; task extraction remains
rule-based. Bundled local-LLM runtime is the next clean step and is not
in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,7 @@
|
||||
// produced by the padding leaks into the saved audio file.
|
||||
|
||||
use rubato::{
|
||||
Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType,
|
||||
WindowFunction,
|
||||
Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction,
|
||||
};
|
||||
|
||||
use kon_core::constants::WHISPER_SAMPLE_RATE;
|
||||
@@ -78,11 +77,7 @@ impl StreamingResampler {
|
||||
INPUT_CHUNK,
|
||||
1, // mono
|
||||
)
|
||||
.map_err(|e| {
|
||||
KonError::AudioDecodeFailed(format!(
|
||||
"StreamingResampler init failed: {e}"
|
||||
))
|
||||
})?;
|
||||
.map_err(|e| KonError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")))?;
|
||||
|
||||
Ok(Self::Sinc {
|
||||
resampler,
|
||||
@@ -98,7 +93,11 @@ impl StreamingResampler {
|
||||
pub fn push_samples(&mut self, mono: &[f32]) -> Result<Vec<f32>> {
|
||||
match self {
|
||||
Self::Passthrough => Ok(mono.to_vec()),
|
||||
Self::Sinc { resampler, residual, .. } => {
|
||||
Self::Sinc {
|
||||
resampler,
|
||||
residual,
|
||||
..
|
||||
} => {
|
||||
if mono.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -128,7 +127,11 @@ impl StreamingResampler {
|
||||
pub fn flush(&mut self) -> Result<Vec<f32>> {
|
||||
match self {
|
||||
Self::Passthrough => Ok(Vec::new()),
|
||||
Self::Sinc { resampler, residual, ratio } => {
|
||||
Self::Sinc {
|
||||
resampler,
|
||||
residual,
|
||||
ratio,
|
||||
} => {
|
||||
if residual.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -139,9 +142,7 @@ impl StreamingResampler {
|
||||
|
||||
let input = vec![chunk];
|
||||
let result = resampler.process(&input, None).map_err(|e| {
|
||||
KonError::AudioDecodeFailed(format!(
|
||||
"StreamingResampler flush failed: {e}"
|
||||
))
|
||||
KonError::AudioDecodeFailed(format!("StreamingResampler flush failed: {e}"))
|
||||
})?;
|
||||
|
||||
let Some(mut out) = result.into_iter().next() else {
|
||||
@@ -183,8 +184,7 @@ mod tests {
|
||||
let from_rate = 48_000u32;
|
||||
let secs = 1.0;
|
||||
let n = (from_rate as f64 * secs) as usize;
|
||||
let samples: Vec<f32> =
|
||||
(0..n).map(|i| (i as f32 * 0.001).sin()).collect();
|
||||
let samples: Vec<f32> = (0..n).map(|i| (i as f32 * 0.001).sin()).collect();
|
||||
|
||||
let mut r = StreamingResampler::new(from_rate).unwrap();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user