#70-Debug: Diar Weak-Fold (7->4 Sprecher), LLM-Robustheit + Whole-Pass-Modellgrenze, Thermal-Cap fuer analyzeRecording

- diar.js: Stufe-3 Weak-Fold (DIAR_MIN_TOTAL_SEC 20s) gegen Phantom-Sprecher
- llm.js: End-Instruktion, JSON-Repair, Sanity+Fallback, Chunk-Retry, Quote-Sanitizer, gelabelter/gewichteter Reduce; WHOLE_MAX 20000 (Gemma-4B-Grenze)
- rkllm_server.cpp: max_context_len 8192->12288
- server.js: analyzeRecording unter A76-Takt-Cap (95-97 Grad ungedeckelt)
- Doku Kap. 03/04/05 + Changelog

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0113vDKdSNtD3Dwcw4QMu9yV
This commit is contained in:
schakal00783
2026-07-16 14:37:07 +02:00
co-authored by Claude Fable 5
parent f9e1eb82c4
commit 2f00ddeec6
8 changed files with 191 additions and 27 deletions
+29
View File
@@ -22,6 +22,15 @@ const MIN_REP_SEC = 0.4; // skip ultra-short cuts for embedding when a longer
// less than this much total speech is folded into the temporally-nearest real
// speaker. A genuine join/leave participant speaks well over this.
const MIN_SPEAKER_SEC = +(process.env.DIAR_MIN_SPEAKER_SEC || 3);
// Stage 3: a single >=MIN_SPEAKER_SEC stray cluster can still OPEN an anchor
// when its (short, unreliable) embedding lands below MERGE_THRESHOLD to every
// real speaker (#70: 3 phantom speakers with 2.5-4.8s total talk next to real
// ones with 53-736s). Total talk time over the whole call separates real
// participants from such noise: any FINAL speaker below this is folded into
// the acoustically nearest strong speaker. NOTE: measured on SEGMENT times
// (incl. intra-segment pauses), which run ~20-40% above summed word times —
// 10s let a 9.2s-word-time phantom through on #70, hence 20.
const MIN_TOTAL_SEC = +(process.env.DIAR_MIN_TOTAL_SEC || 20);
// Diarization on the capped A76 cores runs at ~1x wall-clock (a 46-min wav ≈
// 40 min). The old fixed 10-min timeout killed every recording over ~11 min and
@@ -142,6 +151,26 @@ export async function consolidateSpeakers(wavPath, segs) {
const merged = segs.map(s => ({ ...s, speaker: speakerOf[s.speaker] || nearestRep(s.speaker) }));
// Stage 3: fold weak FINAL speakers (< MIN_TOTAL_SEC total talk over the
// whole call) into the acoustically nearest strong one — see MIN_TOTAL_SEC.
const total = {};
for (const s of merged) total[s.speaker] = (total[s.speaker] || 0) + (s.end - s.start);
const strong = reps.filter(r => (total[r] || 0) >= MIN_TOTAL_SEC);
if (strong.length && strong.length < reps.length) {
const fold = {};
for (const r of Object.keys(total)) {
if (strong.includes(r)) continue;
let best = strong[0], bs = -Infinity;
for (const t of strong) {
const c = (cents[r] && cents[t]) ? spk.cosine(cents[r], cents[t]) : -1;
if (c > bs) { bs = c; best = t; }
}
fold[r] = best;
}
for (const s of merged) if (fold[s.speaker]) s.speaker = fold[s.speaker];
console.log(`diar: folded ${Object.keys(fold).length} weak speaker(s) <${MIN_TOTAL_SEC}s total talk`);
}
// Renumber surviving speakers by first appearance.
const rootMap = {}; let n = 0;
for (const s of merged) if (!(s.speaker in rootMap)) rootMap[s.speaker] = `speaker_${String(n++).padStart(2, '0')}`;