feat(persons): eine Person, mehrere Namen und mehrere Stimmproben
Die Identitaet war der NAME. Derselbe Mensch, einmal "Sascha" und einmal "Sascha Wolf" benannt, existierte zweimal und konnte zwei Cluster benennen -- Aufnahme #101 (34 s, zwei Menschen) zeigte Michael/Sascha/Sprecher 3/ Sascha Wolf. Der Vornamens-Trick in diar.js hat das ueberdeckt, setzte aber auch "Markus Ufer" und "Markus Witsch" gleich (Kosinus 0,241). Neu: persons + person_names (Name = PK, gehoert genau einer Person), voiceprints.person_id. Mehrere Proben pro Person (max. 8) statt eines laufenden Mittels; bewertet wird die beste Probe. Score UND Abstand rechnen zwischen Personen -- vorher rissen zwei Abdruecke desselben Menschen (0,928) die Abstandspruefung, womit die Zwangsbedingung faktisch aus war. diar.identityKey bildet Marker-Namen und Abdruecke auf p<person_id> ab. GUI "Stimmen" zeigt Personen mit Namens-Chips: Namen hinzufuegen/entfernen, Personen zusammenfuehren, Proben verwerfen. Altbestand geleert (Dump in backup/), weil lab/vpcos.mjs neben echten Dubletten (Jens ~ Jens Kuppler = 1,000) auch kontaminierte Abdruecke zeigte: Johannes lag mit 0,912 an Sascha Wolf, aber 0,115 an Johannes Jeckel. Verifiziert an #101: Sascha Wolf als zweiter Name derselben Person -> Re-Diarisierung benennt nur noch Michael + Sascha statt drei Namen fuer zwei Menschen. GUI mit Playwright geprueft, keine JS-Fehler.
This commit is contained in:
+54
-8
@@ -213,17 +213,63 @@ async function viewVoices() {
|
||||
</div>
|
||||
<div id="vmsg" class="muted"></div>
|
||||
</div>
|
||||
<div class="card"><h3>Angelernte Stimmen</h3><div id="vplist" class="list">Lade…</div></div>`;
|
||||
<div class="card"><h3>Personen</h3>
|
||||
<div class="hint">Eine Person darf mehrere Namen haben („Sascha“ und „Sascha Wolf“ = ein Mensch) und
|
||||
mehrere Stimmproben aus verschiedenen Aufnahmen. Erkannt wird über die beste Probe.</div>
|
||||
<div id="vplist" class="list">Lade…</div></div>`;
|
||||
const listEl = v.querySelector('#vplist');
|
||||
async function load() {
|
||||
try {
|
||||
const vps = await api.get('/api/voiceprints');
|
||||
listEl.innerHTML = vps.length ? vps.map(p => `
|
||||
<div class="item"><div class="grow"><b>👤 ${escapeHtml(p.name)}</b>
|
||||
<div class="muted">${new Date(p.created_at).toLocaleString('de-DE')} · ${p.dim}-dim</div></div>
|
||||
<button class="btn danger" data-del="${p.id}">🗑</button></div>`).join('')
|
||||
: '<div class="muted">Noch keine Stimmen. Oben anlernen.</div>';
|
||||
listEl.querySelectorAll('[data-del]').forEach(b => b.onclick = async () => { await api.del('/api/voiceprints/' + b.dataset.del); load(); });
|
||||
const persons = await api.get('/api/persons');
|
||||
const opts = persons.map(p => `<option value="${p.id}">${escapeHtml(p.display_name)}</option>`).join('');
|
||||
listEl.innerHTML = persons.length ? persons.map(p => `
|
||||
<div class="item person" data-pid="${p.id}">
|
||||
<div class="grow">
|
||||
<b>👤 ${escapeHtml(p.display_name)}</b>
|
||||
<div class="chips">${p.names.map(n => `<span class="chip">${escapeHtml(n)}${p.names.length > 1
|
||||
? `<button class="chipx" data-delname="${encodeURIComponent(n)}" title="Namen entfernen">✕</button>` : ''}</span>`).join('')}
|
||||
<button class="btn tiny" data-addname="${p.id}">+ Name</button></div>
|
||||
<div class="muted">${p.voiceprints.length} Stimmprobe${p.voiceprints.length === 1 ? '' : 'n'}${p.last_print
|
||||
? ' · letzte ' + new Date(p.last_print).toLocaleDateString('de-DE') : ''}${p.voiceprints.some(x => x.stale)
|
||||
? ' · ⚠ andere Modellversion' : ''}</div>
|
||||
</div>
|
||||
${persons.length > 1 ? `<select class="input tiny" data-merge="${p.id}">
|
||||
<option value="">↳ zusammenführen mit…</option>${opts}</select>` : ''}
|
||||
${p.voiceprints.length ? `<button class="btn" data-clear="${p.id}" title="Stimmproben löschen, Person behalten">🎙✕</button>` : ''}
|
||||
<button class="btn danger" data-delperson="${p.id}" title="Person samt Proben löschen">🗑</button>
|
||||
</div>`).join('')
|
||||
: '<div class="muted">Noch keine Personen. Oben eine Stimme anlernen oder in einer Aufnahme einen Sprecher benennen.</div>';
|
||||
|
||||
listEl.querySelectorAll('[data-addname]').forEach(b => b.onclick = async () => {
|
||||
const name = prompt('Weiterer Name für diese Person:');
|
||||
if (!name || !name.trim()) return;
|
||||
try {
|
||||
const r = await api.post(`/api/persons/${b.dataset.addname}/names`, { name: name.trim() });
|
||||
toast(r.merged ? 'Name gehörte einer anderen Person → zusammengeführt ✓' : 'Name hinzugefügt ✓');
|
||||
load();
|
||||
} catch (e) { toast(e.message, true); }
|
||||
});
|
||||
listEl.querySelectorAll('[data-delname]').forEach(b => b.onclick = async () => {
|
||||
try { await api.del('/api/persons/names/' + b.dataset.delname); load(); }
|
||||
catch (e) { toast(e.message, true); }
|
||||
});
|
||||
listEl.querySelectorAll('[data-merge]').forEach(sel => sel.onchange = async () => {
|
||||
const fromId = sel.value; if (!fromId) return;
|
||||
if (fromId === sel.dataset.merge) { sel.value = ''; return; }
|
||||
if (!confirm('Beide Personen als EIN Mensch führen? Namen und Stimmproben wandern zusammen.')) { sel.value = ''; return; }
|
||||
try { await api.post(`/api/persons/${sel.dataset.merge}/merge`, { fromId: +fromId }); toast('Zusammengeführt ✓'); load(); }
|
||||
catch (e) { toast(e.message, true); }
|
||||
});
|
||||
listEl.querySelectorAll('[data-clear]').forEach(b => b.onclick = async () => {
|
||||
if (!confirm('Alle Stimmproben dieser Person löschen? Die Person und ihre Namen bleiben.')) return;
|
||||
const p = persons.find(x => x.id === +b.dataset.clear);
|
||||
for (const vp of p.voiceprints) await api.del('/api/voiceprints/' + vp.id);
|
||||
load();
|
||||
});
|
||||
listEl.querySelectorAll('[data-delperson]').forEach(b => b.onclick = async () => {
|
||||
if (!confirm('Person samt allen Namen und Stimmproben löschen?')) return;
|
||||
await api.del('/api/persons/' + b.dataset.delperson); load();
|
||||
});
|
||||
} catch (e) { listEl.innerHTML = '<div class="err">' + e.message + '</div>'; }
|
||||
}
|
||||
await load();
|
||||
|
||||
+577
-560
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user