Skip to main content

qualia_client_core/
audio_pipeline.rs

1//! Client-facing auditory Ears MVP + later swarm helpers.
2
3use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
4use qualia_audio::capture::{CapturePurpose, CaptureSession};
5use qualia_audio::cross_modal::{
6    frames_to_media_ms, propose_temporal_correlations, TimeIntervalMs,
7};
8use qualia_audio::generation::{synthesize_reference_tone, VoiceConsent};
9use qualia_audio::pipeline::{
10    run_ears_demo, run_ears_on_wav_file, run_ears_weighted, section18_smoke, sonify_demo_to_wav,
11    speech_phone_demo, EarsDemoResult,
12};
13use qualia_audio::semantic::{human_correct_quin, human_reject_quin};
14use serde::Serialize;
15
16#[derive(Debug, Clone, Serialize)]
17pub struct EarsDemoDto {
18    pub sample_rate: u32,
19    pub frames: u32,
20    pub n_events: usize,
21    pub n_quins: usize,
22    pub model_hash: String,
23    pub media_hash: String,
24    pub is_reference: bool,
25    pub mel_frames: usize,
26    pub cqt_peak: f32,
27    pub event_instance_hashes: Vec<String>,
28    pub note: String,
29}
30
31impl From<EarsDemoResult> for EarsDemoDto {
32    fn from(r: EarsDemoResult) -> Self {
33        let instances: Vec<String> = r
34            .events
35            .iter()
36            .map(|e| {
37                format!(
38                    "0x{:016x}",
39                    e.source_hash ^ e.start_frame ^ e.end_frame.wrapping_mul(0x9e37_79b9)
40                )
41            })
42            .collect();
43        Self {
44            sample_rate: r.sample_rate,
45            frames: r.frames,
46            n_events: r.n_events,
47            n_quins: r.n_quins,
48            model_hash: format!("0x{:016x}", r.model_hash),
49            media_hash: format!("0x{:016x}", r.media_hash),
50            is_reference: r.is_reference,
51            mel_frames: r.mel_frames,
52            cqt_peak: r.cqt_peak,
53            event_instance_hashes: instances,
54            note: r.note,
55        }
56    }
57}
58
59pub fn ears_demo(storage_root: Option<&std::path::Path>) -> Result<EarsDemoDto, String> {
60    run_ears_demo(storage_root, 440.0, 16000, 300).map(Into::into)
61}
62
63/// Honest, machine-readable status of one audio capability (for Listen UI honesty chips).
64#[derive(Debug, Clone, Serialize)]
65pub struct AudioCapabilityDto {
66    pub id: String,
67    pub domain: String,
68    pub status: String,
69    pub zero_heap_hot: bool,
70    pub streaming: bool,
71    pub test_name: String,
72    pub note: String,
73}
74
75/// Snapshot the audio capability registry as serializable DTOs.
76pub fn audio_capabilities() -> Vec<AudioCapabilityDto> {
77    qualia_audio::capability_registry::CAPABILITIES
78        .iter()
79        .map(|c| AudioCapabilityDto {
80            id: c.id.to_string(),
81            domain: c.domain.as_str().to_string(),
82            status: c.status.as_str().to_string(),
83            zero_heap_hot: c.zero_heap_hot,
84            streaming: c.streaming,
85            test_name: c.test_name.to_string(),
86            note: c.note.to_string(),
87        })
88        .collect()
89}
90
91#[derive(Debug, Clone, Serialize)]
92pub struct CrossModalDemoDto {
93    pub n_correlations: usize,
94    pub asserts_causality_any: bool,
95    pub note: String,
96}
97
98pub fn cross_modal_demo() -> CrossModalDemoDto {
99    let v = [TimeIntervalMs {
100        start_ms: 0,
101        end_ms: 2000,
102        instance: 0x15,
103    }];
104    let a_start = frames_to_media_ms(8000, 16000, 0);
105    let a = [TimeIntervalMs {
106        start_ms: a_start,
107        end_ms: a_start + 1000,
108        instance: 0xA0,
109    }];
110    let mut out = [qualia_audio::AvCorrelationProposal {
111        media_hash: 1,
112        visual_instance: 0,
113        auditory_instance: 0,
114        overlap_start_ms: 0,
115        overlap_end_ms: 0,
116        confidence_u16: 0,
117        asserts_causality: true,
118    }; 8];
119    let n = propose_temporal_correlations(1, &v, &a, &mut out);
120    CrossModalDemoDto {
121        n_correlations: n,
122        asserts_causality_any: out[..n].iter().any(|c| c.asserts_causality),
123        note: "Temporal overlap only — asserts_causality always false.".into(),
124    }
125}
126
127pub fn synth_with_consent(allow: bool) -> Result<u32, String> {
128    let c = if allow {
129        VoiceConsent::synthesis_only("demo-voice")
130    } else {
131        VoiceConsent::denied("demo-voice")
132    };
133    let mut o = [0.0f32; 512];
134    let r =
135        synthesize_reference_tone(c, 440.0, 16000, 512, 1, &mut o).map_err(|e| format!("{e:?}"))?;
136    Ok(r.frames)
137}
138
139pub fn ears_from_wav(
140    storage_root: Option<&std::path::Path>,
141    path: &std::path::Path,
142) -> Result<EarsDemoDto, String> {
143    run_ears_on_wav_file(storage_root, path).map(Into::into)
144}
145
146pub fn section18_smoke_dto() -> Result<String, String> {
147    section18_smoke()
148}
149
150pub fn audio_reject_instance(instance_hex: &str) -> Result<String, String> {
151    let inst = parse_hex(instance_hex)?;
152    let q = human_reject_quin(qualia_audio::q_hash("did:webizen:local-principal"), inst, 0);
153    Ok(format!(
154        "reject_quin parity=0x{:016x} instance=0x{:016x} (machine claim retained)",
155        q.parity, inst
156    ))
157}
158
159pub fn audio_correct_instance(instance_hex: &str, new_class_hex: &str) -> Result<String, String> {
160    let inst = parse_hex(instance_hex)?;
161    let cls = parse_hex(new_class_hex)?;
162    let q = human_correct_quin(
163        qualia_audio::q_hash("did:webizen:local-principal"),
164        inst,
165        cls,
166    );
167    Ok(format!(
168        "correct_quin parity=0x{:016x} new_class=0x{:016x}",
169        q.parity, cls
170    ))
171}
172
173fn parse_hex(s: &str) -> Result<u64, String> {
174    let t = s.trim().trim_start_matches("0x").trim_start_matches("0X");
175    u64::from_str_radix(t, 16).map_err(|e| e.to_string())
176}
177
178pub fn ears_weighted_demo(storage_root: Option<&std::path::Path>) -> Result<EarsDemoDto, String> {
179    run_ears_weighted(storage_root, 440.0, 16000, 300).map(Into::into)
180}
181
182/// U3-style hear: demo events → WAV data URL + optional file under storage.
183pub fn sonify_ears_demo(
184    storage_root: Option<&std::path::Path>,
185) -> Result<serde_json::Value, String> {
186    let r = run_ears_demo(None, 440.0, 16000, 400)?;
187    let wav = sonify_demo_to_wav(r.sample_rate, &r.events, r.frames as usize)?;
188    let mut path_out = None;
189    if let Some(root) = storage_root {
190        let dir = root.join("audio_hear");
191        std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
192        let p = dir.join(format!("sonify_{:016x}.wav", r.media_hash));
193        std::fs::write(&p, &wav).map_err(|e| e.to_string())?;
194        path_out = Some(p.display().to_string());
195    }
196    Ok(serde_json::json!({
197        "wav_data_url": format!("data:audio/wav;base64,{}", B64.encode(&wav)),
198        "path": path_out,
199        "n_events": r.n_events,
200        "frames": r.frames,
201        "sample_rate": r.sample_rate,
202        "note": "Parametric sonification of event intervals (U3-style navigate/hear). Not original PCM playback."
203    }))
204}
205
206pub fn speech_demo(supported: bool) -> Result<serde_json::Value, String> {
207    let (n, model) = speech_phone_demo(supported)?;
208    Ok(serde_json::json!({
209        "tokens": n,
210        "model_hash": format!("0x{:016x}", model),
211        "language_supported": supported,
212        "note": if supported {
213            "Greedy phone decode over seed speech weights (not full ASR)."
214        } else {
215            "Unknown language: empty transcript (no silent map)."
216        }
217    }))
218}
219
220/// Capture policy demo: intent required before live ring accepts PCM.
221pub fn capture_policy_demo() -> Result<serde_json::Value, String> {
222    let mut s = CaptureSession::new(CapturePurpose::Analysis, 16000, 1);
223    let denied = s.start().is_err();
224    s.grant_intent();
225    s.start().map_err(|e| format!("{e:?}"))?;
226    let pushed = s.push_mono(&[0.1, 0.2, 0.3, 0.0]);
227    let mut out = [0.0f32; 8];
228    let pulled = s.pull_mono(&mut out);
229    Ok(serde_json::json!({
230        "denied_without_intent": denied,
231        "pushed": pushed,
232        "pulled": pulled,
233        "note": "Shell must call grant_intent before device/file stream. Hardware mic via shell push_mono."
234    }))
235}
236
237/// Seed and persist AED weights under storage_root/models/aed_seed.qaed
238pub fn ensure_aed_weights(storage_root: &std::path::Path) -> Result<String, String> {
239    let path = storage_root.join("models").join("aed_seed.qaed");
240    if path.is_file() {
241        let b = qualia_audio::AedWeightBundle::load_path(&path)?;
242        return Ok(format!(
243            "loaded AED weights hash=0x{:016x} path={}",
244            b.model_hash,
245            path.display()
246        ));
247    }
248    let b = qualia_audio::AedWeightBundle::from_seed(0xAED1);
249    b.save_path(&path)?;
250    Ok(format!(
251        "wrote AED seed weights hash=0x{:016x} path={}",
252        b.model_hash,
253        path.display()
254    ))
255}
256
257pub fn ensure_speech_weights(storage_root: &std::path::Path) -> Result<String, String> {
258    let path = storage_root.join("models").join("speech_seed.qspk");
259    if path.is_file() {
260        let w = qualia_audio::SpeechEncoderWeights::load_path(&path)?;
261        return Ok(format!(
262            "loaded speech weights hash=0x{:016x} path={}",
263            w.model_hash,
264            path.display()
265        ));
266    }
267    let w = qualia_audio::SpeechEncoderWeights::from_seed(7, 16);
268    w.save_path(&path)?;
269    Ok(format!(
270        "wrote speech seed weights hash=0x{:016x} path={}",
271        w.model_hash,
272        path.display()
273    ))
274}
275
276pub fn daw_history_demo() -> Result<serde_json::Value, String> {
277    use qualia_audio::{OpKind, ProcessPlan, SessionHistory, SessionOp, TrackState};
278    let mut plan = ProcessPlan::new(48000, 64);
279    plan.add_track(TrackState::default());
280    let mut hist = SessionHistory::new();
281    hist.apply_and_record(
282        &mut plan,
283        SessionOp {
284            kind: OpKind::SetGain,
285            track: 0,
286            value_f32: 0.4,
287            value_bool: false,
288            prev_f32: 1.0,
289            prev_bool: false,
290        },
291    );
292    let g1 = plan.tracks[0].gain;
293    hist.undo(&mut plan);
294    let g0 = plan.tracks[0].gain;
295    hist.redo(&mut plan);
296    let g2 = plan.tracks[0].gain;
297    let mut lane = qualia_audio::AutomationLane::new(0);
298    lane.add(0, 0.0);
299    lane.add(1000, 1.0);
300    let mid = lane.value_at(500);
301    Ok(serde_json::json!({
302        "gain_after_set": g1,
303        "gain_after_undo": g0,
304        "gain_after_redo": g2,
305        "automation_mid": mid,
306        "note": "SessionHistory undo/redo + AutomationLane interp (cold path)."
307    }))
308}
309
310/// Run weighted AED on mono PCM (from mic pull or file).
311pub fn analyze_mono_pcm(
312    mono: &[f32],
313    sample_rate: u32,
314    storage_root: Option<&std::path::Path>,
315) -> Result<EarsDemoDto, String> {
316    use qualia_audio::types::{AuditoryEvent, AuditoryModel, TranscriptToken};
317    use qualia_audio::wav::encode_wav_i16_mono;
318    use qualia_audio::{AedWeightBundle, WeightedAedModel};
319
320    if mono.is_empty() {
321        return Err("no PCM (arm mic or import WAV first)".into());
322    }
323    let mut i16s = vec![0i16; mono.len()];
324    for (i, &s) in mono.iter().enumerate() {
325        i16s[i] = (s.clamp(-1.0, 1.0) * 32767.0) as i16;
326    }
327    let mut wav = vec![0u8; 44 + mono.len() * 2];
328    let wn = encode_wav_i16_mono(&i16s, sample_rate.max(8000), &mut wav)
329        .map_err(|e| format!("{e:?}"))?;
330    let decoded = qualia_audio::decode_wav(&wav[..wn]).map_err(|e| format!("{e:?}"))?;
331
332    let bundle = if let Some(root) = storage_root {
333        let path = root.join("models").join("aed_seed.qaed");
334        if path.is_file() {
335            AedWeightBundle::load_path(&path)?
336        } else {
337            let b = AedWeightBundle::from_seed(0xAED1);
338            let _ = b.save_path(&path);
339            b
340        }
341    } else {
342        AedWeightBundle::from_seed(0xAED1)
343    };
344
345    let mut model = WeightedAedModel::from_bundle(bundle);
346
347    let mut events = [AuditoryEvent::empty(); 32];
348    let mut tokens = [TranscriptToken::empty(); 8];
349    let mut emb = [0.0f32; 4];
350    let mut ws = [0u8; 8];
351    let counts = model
352        .infer_chunk(decoded.view(), &mut events, &mut tokens, &mut emb, &mut ws)
353        .map_err(|e| format!("{e:?}"))?;
354
355    let media_hash = qualia_audio::media_digest(&wav[..wn]).hash;
356    let mut quins = [qualia_audio::AudioQuin::with_parity(0, 0, 0, 0, 0); 64];
357    let n_q = qualia_audio::compile_auditory_quins(
358        qualia_audio::MediaDigest {
359            hash: media_hash,
360            byte_len: wn as u64,
361        },
362        &events[..counts.events],
363        model.model_hash(),
364        &mut quins,
365    );
366
367    Ok(EarsDemoDto {
368        sample_rate: decoded.sample_rate,
369        frames: mono.len() as u32,
370        n_events: counts.events,
371        n_quins: n_q,
372        model_hash: format!("0x{:016x}", model.model_hash()),
373        media_hash: format!("0x{:016x}", media_hash),
374        is_reference: false,
375        mel_frames: 0,
376        cqt_peak: 0.0,
377        event_instance_hashes: events[..counts.events]
378            .iter()
379            .map(|e| format!("0x{:016x}", e.source_hash ^ e.start_frame))
380            .collect(),
381        note: "Live/imported mono analyzed with disk AED weights (seed-shaped, not certified foundation)."
382            .into(),
383    })
384}
385
386/// One mixer track strip (UI ↔ process plan).
387#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
388pub struct MixerTrackDto {
389    pub name: String,
390    pub gain: f32,
391    pub pan: f32,
392    pub mute: bool,
393    pub solo: bool,
394    pub lowpass: f32,
395    pub eq_gain_db: f32,
396    pub eq_freq_hz: f32,
397    pub comp_threshold: f32,
398    pub comp_ratio: f32,
399    pub delay_samples: u32,
400    pub delay_mix: f32,
401}
402
403impl Default for MixerTrackDto {
404    fn default() -> Self {
405        Self {
406            name: "Track".into(),
407            gain: 0.85,
408            pan: 0.0,
409            mute: false,
410            solo: false,
411            lowpass: 0.0,
412            eq_gain_db: 0.0,
413            eq_freq_hz: 1000.0,
414            comp_threshold: 1.0,
415            comp_ratio: 1.0,
416            delay_samples: 0,
417            delay_mix: 0.0,
418        }
419    }
420}
421
422/// Default 3-strip mixer session for Listen UI.
423pub fn mixer_default_session() -> serde_json::Value {
424    let tracks = vec![
425        MixerTrackDto {
426            name: "Tone A".into(),
427            pan: -0.4,
428            ..Default::default()
429        },
430        MixerTrackDto {
431            name: "Tone B".into(),
432            pan: 0.4,
433            gain: 0.7,
434            ..Default::default()
435        },
436        MixerTrackDto {
437            name: "Pad".into(),
438            gain: 0.5,
439            lowpass: 0.25,
440            ..Default::default()
441        },
442    ];
443    serde_json::json!({
444        "sample_rate": 48000,
445        "block_frames": 64,
446        "tracks": tracks,
447        "note": "Reference mixer — not a commercial DAW. EQ/comp/delay are deterministic primitives."
448    })
449}
450
451/// Bounce a mixer session (synthetic tones per track) through ProcessPlan FX chain.
452pub fn mixer_bounce(tracks: &[MixerTrackDto]) -> Result<serde_json::Value, String> {
453    use qualia_audio::{ProcessPlan, TrackState};
454    let n = tracks.len().min(16).max(1);
455    let frames = 2048usize;
456    let sr = 48000u32;
457    let mut plan = ProcessPlan::new(sr, 64);
458    let mut mono_bufs: Vec<Vec<f32>> = Vec::with_capacity(n);
459    for (i, t) in tracks.iter().take(n).enumerate() {
460        plan.add_track(TrackState {
461            gain: t.gain.clamp(0.0, 2.0),
462            pan: t.pan.clamp(-1.0, 1.0),
463            mute: t.mute,
464            solo: t.solo,
465            lowpass: t.lowpass.clamp(0.0, 0.99),
466            eq_gain_db: t.eq_gain_db,
467            eq_freq_hz: t.eq_freq_hz.max(20.0),
468            comp_threshold: t.comp_threshold.clamp(0.05, 1.0),
469            comp_ratio: t.comp_ratio.max(1.0),
470            delay_samples: t.delay_samples.min(512),
471            delay_mix: t.delay_mix.clamp(0.0, 1.0),
472        });
473        let freq = 220.0 * (i as f32 + 1.0);
474        let mut buf = vec![0.0f32; frames];
475        for (s, sample) in buf.iter_mut().enumerate() {
476            *sample = (2.0 * core::f32::consts::PI * freq * s as f32 / sr as f32).sin() * 0.4;
477        }
478        mono_bufs.push(buf);
479    }
480    let refs: Vec<&[f32]> = mono_bufs.iter().map(|b| b.as_slice()).collect();
481    let mut out = vec![0.0f32; frames * 2];
482    let written = plan
483        .bounce_interleaved(&refs, &mut out)
484        .map_err(|e| e.to_string())?;
485    let peak = out
486        .iter()
487        .take(written * 2)
488        .map(|x| x.abs())
489        .fold(0.0f32, f32::max);
490    let energy: f32 = out.iter().take(written * 2).map(|x| x * x).sum();
491    Ok(serde_json::json!({
492        "frames_written": written,
493        "peak": peak,
494        "energy": energy,
495        "n_tracks": n,
496        "note": "Offline bounce of synthetic tones through mixer FX — reference quality only."
497    }))
498}
499
500/// Music analysis demo: onsets + tempo + structure (reference quality).
501pub fn music_analysis_demo() -> Result<serde_json::Value, String> {
502    use qualia_audio::{
503        detect_onsets, estimate_tempo_from_onsets, propose_structure_segments, MusicAssumptions,
504        OnsetEvent, StructureSegment,
505    };
506    let sr = 16000u32;
507    let mut mono = vec![0.0f32; 8000];
508    // Impulses ~2 Hz → ~120 BPM when folded
509    for k in 0..8 {
510        let i = (k * 4000 / 2) as usize;
511        if i < mono.len() {
512            mono[i] = 1.0;
513        }
514    }
515    let mut onsets = [OnsetEvent {
516        frame: 0,
517        strength: 0.0,
518    }; 32];
519    let n_on = detect_onsets(&mono, 256, 128, 0.01, &mut onsets);
520    let tempo = estimate_tempo_from_onsets(
521        &onsets[..n_on],
522        sr,
523        MusicAssumptions {
524            assumes_4_4: true,
525            assumes_12tet: false,
526            tuning_a4_hz: 440.0,
527        },
528    );
529    let mut segs = [StructureSegment {
530        start_frame: 0,
531        end_frame: 0,
532        mean_energy: 0.0,
533        label_hash: 0,
534    }; 8];
535    let n_seg = propose_structure_segments(&mono, 256, 128, &mut segs);
536    Ok(serde_json::json!({
537        "n_onsets": n_on,
538        "bpm": tempo.bpm,
539        "tempo_confidence": tempo.confidence,
540        "n_segments": n_seg,
541        "note": "Reference music analysis — not a production beat tracker."
542    }))
543}
544
545/// DAW FX chain demo (EQ + comp + delay on mono).
546pub fn daw_fx_demo() -> Result<serde_json::Value, String> {
547    use qualia_audio::{ProcessPlan, TrackState};
548    let mut tr = TrackState::default();
549    tr.eq_gain_db = 3.0;
550    tr.comp_threshold = 0.3;
551    tr.comp_ratio = 3.0;
552    tr.delay_samples = 48;
553    tr.delay_mix = 0.2;
554    let mono: Vec<f32> = (0..512)
555        .map(|i| (2.0 * core::f32::consts::PI * 440.0 * i as f32 / 48000.0).sin() * 0.5)
556        .collect();
557    let mut out = vec![0.0f32; 512];
558    let n = ProcessPlan::process_mono_fx(&tr, 48000, &mono, &mut out);
559    let energy: f32 = out.iter().map(|x| x * x).sum();
560    Ok(serde_json::json!({
561        "frames": n,
562        "energy": energy,
563        "note": "EQ+comp+delay offline path (deterministic FX primitives)."
564    }))
565}
566
567/// TTS consent + stem separation demo.
568pub fn gen_audio_demo() -> Result<serde_json::Value, String> {
569    use qualia_audio::{separate_two_stems_reference, synthesize_reference_tone, VoiceConsent};
570    let mut consent = VoiceConsent::synthesis_only("demo-voice");
571    let mut tone = [0.0f32; 512];
572    let rec = synthesize_reference_tone(consent, 440.0, 16000, 512, 7, &mut tone)
573        .map_err(|e| format!("{e:?}"))?;
574    consent.revoke(1);
575    let denied = synthesize_reference_tone(consent, 440.0, 16000, 512, 7, &mut tone).is_err();
576    let mut body = [0.0f32; 512];
577    let mut detail = [0.0f32; 512];
578    let (s0, s1) = separate_two_stems_reference(&tone, 0x00E0_D1A0_u64, &mut body, &mut detail)
579        .map_err(|e| format!("{e:?}"))?;
580    Ok(serde_json::json!({
581        "synth_frames": rec.frames,
582        "is_reference_synth": rec.is_reference_synth,
583        "revoke_denies": denied,
584        "stem_body": format!("0x{:016x}", s0.stem_class),
585        "stem_detail": format!("0x{:016x}", s1.stem_class),
586        "note": "Reference synth + sep; licensed TTS/demucs COMPLETE-WITH-GATE."
587    }))
588}
589
590/// Shared media clock + joint window demo.
591pub fn shared_clock_demo() -> Result<serde_json::Value, String> {
592    use qualia_audio::{events_overlapping_window, SharedMediaClock, TimeIntervalMs};
593    let clock = SharedMediaClock::new(0xC10C, 16000, 25.0);
594    let v_ms = clock.video_frame_to_ms(25);
595    let a_ms = clock.audio_frame_to_ms(16000);
596    let intervals = [
597        TimeIntervalMs {
598            start_ms: 0,
599            end_ms: 500,
600            instance: 1,
601        },
602        TimeIntervalMs {
603            start_ms: 800,
604            end_ms: 1200,
605            instance: 2,
606        },
607    ];
608    let win = TimeIntervalMs {
609        start_ms: 400,
610        end_ms: 900,
611        instance: 0,
612    };
613    let mut out = [0u64; 4];
614    let n = events_overlapping_window(&intervals, win, &mut out);
615    Ok(serde_json::json!({
616        "video_25_frames_ms": v_ms,
617        "audio_1s_ms": a_ms,
618        "drift_at_1s": clock.drift_ms(25, 16000),
619        "window_hits": n,
620        "asserts_causality": false,
621        "note": "Shared clock + joint interval query; overlap ≠ causality."
622    }))
623}
624
625/// Speech using disk weights if present.
626pub fn speech_from_disk(
627    storage_root: &std::path::Path,
628    supported: bool,
629) -> Result<serde_json::Value, String> {
630    use qualia_audio::{decode_for_language, SpeechEncoderWeights, TranscriptToken};
631    let path = storage_root.join("models").join("speech_seed.qspk");
632    let w = if path.is_file() {
633        SpeechEncoderWeights::load_path(&path)?
634    } else {
635        let w = SpeechEncoderWeights::from_seed(7, 16);
636        w.save_path(&path)?;
637        w
638    };
639    let mut mono = vec![0.0f32; 4096];
640    for i in 0..mono.len() {
641        mono[i] = (2.0 * core::f32::consts::PI * 180.0 * i as f32 / 16000.0).sin() * 0.25;
642    }
643    let mut tok = [TranscriptToken::empty(); 32];
644    let n =
645        decode_for_language(&w, &mono, 16000, supported, &mut tok).map_err(|e| format!("{e:?}"))?;
646    Ok(serde_json::json!({
647        "tokens": n,
648        "model_hash": format!("0x{:016x}", w.model_hash),
649        "weights_path": path.display().to_string(),
650        "language_supported": supported,
651        "note": "Speech weights loaded from disk when present."
652    }))
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn ears_dto() {
661        let d = ears_demo(None).unwrap();
662        assert!(d.n_events >= 1);
663        assert!(d.is_reference);
664        assert!(d.cqt_peak > 0.0);
665    }
666
667    #[test]
668    fn cross_modal_not_causal() {
669        let d = cross_modal_demo();
670        assert!(!d.asserts_causality_any);
671    }
672
673    #[test]
674    fn section18_ok() {
675        assert!(section18_smoke_dto().unwrap().contains("OK"));
676    }
677
678    #[test]
679    fn analyze_mono_pcm_seed() {
680        let mut mono = vec![0.0f32; 4096];
681        for i in 0..mono.len() {
682            mono[i] = (2.0 * core::f32::consts::PI * 440.0 * i as f32 / 16000.0).sin() * 0.3;
683        }
684        let d = analyze_mono_pcm(&mono, 16000, None).unwrap();
685        assert_eq!(d.sample_rate, 16000);
686        assert!(d.frames > 0);
687        assert!(!d.model_hash.is_empty());
688    }
689}