Fix: Voice-Auto-Naming tot (TitaNet bricht bei langen Schnitten ab + vergiftete Promise-Kette)
identifySpeakers() embettete den laengsten Lauf ungekuerzt; TitaNet-ONNX bricht ab 180 s ab (broadcast 12288 by 17998) und riss den spk_embed-Helfer mit. Da spk.js die Aufrufe ueber eine Promise-Kette serialisiert, schlugen danach ALLE weiteren Sprecher mit 'spk beendet' fehl. - ID_CUT_SEC=25s in identifySpeakers - Kette in spk.js und tdChain in recorder.js failure-isoliert - pro Name gewinnt nur der beste Kosinus (Fabio war auf 2 Clustern)
This commit is contained in:
+3
-2
@@ -147,11 +147,12 @@ function startTd() {
|
|||||||
}
|
}
|
||||||
// One request at a time (single warm process), chained like spk.embed.
|
// One request at a time (single warm process), chained like spk.embed.
|
||||||
function runDaemonTranscribe(wavPath) {
|
function runDaemonTranscribe(wavPath) {
|
||||||
tdChain = tdChain.then(async () => {
|
const run = tdChain.then(async () => {
|
||||||
await startTd();
|
await startTd();
|
||||||
return await new Promise((res, rej) => { tdPending = { resolve: res, reject: rej }; tdProc.stdin.write(wavPath + '\n'); });
|
return await new Promise((res, rej) => { tdPending = { resolve: res, reject: rej }; tdProc.stdin.write(wavPath + '\n'); });
|
||||||
});
|
});
|
||||||
return tdChain;
|
tdChain = run.then(() => {}, () => {}); // a failed call must not poison the queue
|
||||||
|
return run;
|
||||||
}
|
}
|
||||||
// Wav duration from file size (16 kHz mono s16 = 32000 B/s, 44 B header).
|
// Wav duration from file size (16 kHz mono s16 = 32000 B/s, 44 B header).
|
||||||
function wavDurationSec(wavPath) {
|
function wavDurationSec(wavPath) {
|
||||||
|
|||||||
+18
-3
@@ -65,18 +65,33 @@ async function identifySpeakers(recData) {
|
|||||||
const len = cur.end - cur.start;
|
const len = cur.end - cur.start;
|
||||||
if (!best[cur.speaker] || len > best[cur.speaker].len) best[cur.speaker] = { start: cur.start, end: cur.end, len };
|
if (!best[cur.speaker] || len > best[cur.speaker].len) best[cur.speaker] = { start: cur.start, end: cur.end, len };
|
||||||
}
|
}
|
||||||
const map = {};
|
// Cut at most ID_CUT_SEC: onnxruntime ABORTS the embedding helper on very long
|
||||||
|
// inputs (reproduced: 180 s -> "Non-zero status code returned while running
|
||||||
|
// Where"; 20/60 s fine). On #74 the longest run of a moderator was minutes
|
||||||
|
// long, which killed the helper on the first speaker and — through the then
|
||||||
|
// poisoned promise chain in spk.js — every speaker after it ("spk beendet"),
|
||||||
|
// i.e. voice auto-naming was dead for the whole recording.
|
||||||
|
const ID_CUT_SEC = 25;
|
||||||
|
const hits = [];
|
||||||
for (const sp of speakers) {
|
for (const sp of speakers) {
|
||||||
const r = best[sp]; if (!r) continue;
|
const r = best[sp]; if (!r) continue;
|
||||||
const seg = `/tmp/idseg_${recData.id}_${sp}.wav`;
|
const seg = `/tmp/idseg_${recData.id}_${sp}.wav`;
|
||||||
|
const from = r.start / 1000;
|
||||||
|
const to = Math.min(r.end / 1000, from + ID_CUT_SEC);
|
||||||
try {
|
try {
|
||||||
await pexec('ffmpeg', ['-y', '-i', wavPath, '-ss', String(r.start / 1000), '-to', String(r.end / 1000), '-ar', '16000', '-ac', '1', seg]);
|
await pexec('ffmpeg', ['-y', '-i', wavPath, '-ss', String(from), '-to', String(to), '-ar', '16000', '-ac', '1', seg]);
|
||||||
const emb = await spk.embed(seg);
|
const emb = await spk.embed(seg);
|
||||||
const m = spk.bestMatch(emb, vps);
|
const m = spk.bestMatch(emb, vps);
|
||||||
if (m) map[sp] = m.name;
|
if (m) hits.push({ speaker: sp, name: m.name, score: m.score });
|
||||||
} catch (e) { console.error('identify', sp, e.message); }
|
} catch (e) { console.error('identify', sp, e.message); }
|
||||||
finally { try { fs.unlinkSync(seg); } catch {} }
|
finally { try { fs.unlinkSync(seg); } catch {} }
|
||||||
}
|
}
|
||||||
|
// One enrolled person can only BE one cluster: keep the best-scoring match per
|
||||||
|
// name (#74 handed "Fabio" to two clusters at once).
|
||||||
|
const bestByName = {};
|
||||||
|
for (const h of hits) if (!bestByName[h.name] || h.score > bestByName[h.name].score) bestByName[h.name] = h;
|
||||||
|
const map = {};
|
||||||
|
for (const h of Object.values(bestByName)) map[h.speaker] = h.name;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-2
@@ -47,12 +47,21 @@ function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Extract the voiceprint for a 16 kHz mono wav file.
|
// Extract the voiceprint for a 16 kHz mono wav file.
|
||||||
|
// Calls are serialized through `chain`, but a FAILED call must not poison it:
|
||||||
|
// .then() on a rejected promise skips the callback, so ONE crash used to make
|
||||||
|
// every later embed() fail instantly with the same "spk beendet" — that is why
|
||||||
|
// speaker auto-naming was dead for a whole recording after the first bad cut
|
||||||
|
// (root-caused 2026-07-27). The queue is continued from a swallowed copy.
|
||||||
|
// NOTE for callers: keep cuts short. onnxruntime ABORTS the helper process on
|
||||||
|
// very long inputs — reproduced at 180 s ("Non-zero status code returned while
|
||||||
|
// running Where"), fine at 20/60 s.
|
||||||
export function embed(wavPath) {
|
export function embed(wavPath) {
|
||||||
chain = chain.then(async () => {
|
const run = chain.then(async () => {
|
||||||
await start();
|
await start();
|
||||||
return await new Promise((res, rej) => { pending = { resolve: res, reject: rej }; proc.stdin.write(wavPath + '\n'); });
|
return await new Promise((res, rej) => { pending = { resolve: res, reject: rej }; proc.stdin.write(wavPath + '\n'); });
|
||||||
});
|
});
|
||||||
return chain;
|
chain = run.then(() => {}, () => {});
|
||||||
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cosine(a, b) {
|
export function cosine(a, b) {
|
||||||
|
|||||||
+16
-2
@@ -119,8 +119,22 @@ flowchart LR
|
|||||||
VP --> AUTOID
|
VP --> AUTOID
|
||||||
```
|
```
|
||||||
|
|
||||||
- Beim Lernen wird der längste zusammenhängende Sprech-Abschnitt (≥ 1,5 s) des Sprechers als TitaNet-Embedding
|
- Beim Lernen wird der längste zusammenhängende Sprech-Abschnitt (≥ 1,5 s, **max. 20–25 s**) des Sprechers als
|
||||||
in den Zentroid gefaltet (`db.upsertVoiceprint`) → zukünftige Aufnahmen identifizieren die Person automatisch.
|
TitaNet-Embedding in den Zentroid gefaltet (`db.upsertVoiceprint`) → zukünftige Aufnahmen identifizieren die
|
||||||
|
Person automatisch.
|
||||||
|
- ⚠️ **Warum die Schnitte kurz sein MÜSSEN (2026-07-27 root-caused):** das TitaNet-ONNX bricht bei langen
|
||||||
|
Eingaben ab — reproduziert bei 180 s:
|
||||||
|
`Non-zero status code returned while running Where node … Attempting to broadcast an axis by a dimension
|
||||||
|
other than 1. 12288 by 17998` → der `spk_embed`-Helfer stirbt (20 s / 60 s laufen sauber). Auf #74 war der
|
||||||
|
längste Lauf eines Moderators minutenlang; damit starb der Helfer beim ERSTEN Sprecher und — weil
|
||||||
|
`spk.js` seine Aufrufe über eine Promise-Kette serialisiert und `.then()` auf einer *rejected* Promise den
|
||||||
|
Callback überspringt — jeder weitere Sprecher mit „spk beendet". Das war die Ursache des seit 2026-07-16
|
||||||
|
offenen Rätsels „Voice-Auto-Naming tot trotz freiem RAM". Zwei Fixes: `identifySpeakers()` schneidet auf
|
||||||
|
`ID_CUT_SEC = 25 s`, und die Kette in `spk.js` (ebenso `tdChain` in `recorder.js`) wird aus einer
|
||||||
|
geschluckten Kopie fortgeführt → ein Absturz kostet nur noch diesen einen Aufruf (verifiziert:
|
||||||
|
180-s-Schnitt scheitert, der nächste 20-s-Schnitt liefert wieder ein Embedding).
|
||||||
|
- Ein enrolltes Profil kann nur EINEN Cluster benennen: pro Name gewinnt der beste Kosinus
|
||||||
|
(#74 hatte „Fabio" gleichzeitig auf zwei Clustern).
|
||||||
- Teilnehmerliste der Analyse kommt seit 2026-07-15 **aus der Diarization** (Namen wo bekannt, sonst
|
- Teilnehmerliste der Analyse kommt seit 2026-07-15 **aus der Diarization** (Namen wo bekannt, sonst
|
||||||
„Sprecher N“) — nicht mehr vom LLM (das listete alle bloß *erwähnten* Namen, 11 statt 3). Siehe Kap. 05.
|
„Sprecher N“) — nicht mehr vom LLM (das listete alle bloß *erwähnten* Namen, 11 statt 3). Siehe Kap. 05.
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,14 @@ Was, Warum, betroffene Dateien/Backups).
|
|||||||
`style.css` (Kopfzeilen-Eingabe, Marker-Chips, Marker-Liste im Detail). Backups `.bak-v2clust`,
|
`style.css` (Kopfzeilen-Eingabe, Marker-Chips, Marker-Liste im Detail). Backups `.bak-v2clust`,
|
||||||
`.bak-markers`. Bundle `index-q6bHhDgg.js`. Labor-Skripte (Segment-Dump, Fenster-Embeddings, Cluster-Grid, Schwellen-Fit) liegen auf dem Board
|
`.bak-markers`. Bundle `index-q6bHhDgg.js`. Labor-Skripte (Segment-Dump, Fenster-Embeddings, Cluster-Grid, Schwellen-Fit) liegen auf dem Board
|
||||||
unter `/opt/callrec/lab/` und im Repo unter `app/lab/` — damit ist das Tuning reproduzierbar.
|
unter `/opt/callrec/lab/` und im Repo unter `app/lab/` — damit ist das Tuning reproduzierbar.
|
||||||
|
- **Nachtrag gleicher Tag — „Voice-Auto-Naming tot" (offen seit 2026-07-16) GELÖST:** TitaNet-ONNX bricht bei
|
||||||
|
langen Eingaben ab (reproduziert 180 s → `Non-zero status code … broadcast … 12288 by 17998`, 20/60 s ok).
|
||||||
|
`identifySpeakers()` embettete den LÄNGSTEN Lauf ungekürzt (auf #74 minutenlang) → Helfer stirbt beim
|
||||||
|
ersten Sprecher; da `spk.js` über eine Promise-Kette serialisiert und `.then()` auf einer rejected Promise
|
||||||
|
den Callback überspringt, schlugen ALLE weiteren Sprecher sofort mit „spk beendet" fehl. Fixes:
|
||||||
|
`ID_CUT_SEC = 25 s` in `identifySpeakers()`, Kette in `spk.js` + `tdChain` in `recorder.js` failure-isoliert,
|
||||||
|
und pro Name gewinnt nur der beste Kosinus (#74 hatte „Fabio" auf zwei Clustern). Verifiziert per
|
||||||
|
Testskript: 180-s-Schnitt scheitert, direkt danach liefert ein 20-s-Schnitt wieder ein Embedding.
|
||||||
- **GOTCHA (Betrieb):** zwei schwere Jobs parallel (Labor-sherpa + App-Analyse, deren `finally` den Takt
|
- **GOTCHA (Betrieb):** zwei schwere Jobs parallel (Labor-sherpa + App-Analyse, deren `finally` den Takt
|
||||||
auf 2,25 GHz zurückstellt) haben die A76 auf **101,9 °C** getrieben — lüfterloses Board, das ist
|
auf 2,25 GHz zurückstellt) haben die A76 auf **101,9 °C** getrieben — lüfterloses Board, das ist
|
||||||
Shutdown-Nähe. Schwere Jobs immer serialisieren, Cap 1,4–1,6 GHz.
|
Shutdown-Nähe. Schwere Jobs immer serialisieren, Cap 1,4–1,6 GHz.
|
||||||
|
|||||||
Reference in New Issue
Block a user