File size: 27,617 Bytes
2219798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a2313f5
2219798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
<title>Humaneness Voice Small</title>
<link rel="stylesheet" href="/legacy.css">
<style>
header{flex-wrap:wrap}.sub a{color:var(--accent)}
#chat{padding-bottom:32px}.msg{width:min(100%,820px)}
footer{max-height:46vh;overflow-y:auto}.hint{align-items:center;margin-top:10px}
.hint input[type=number]{width:64px}.hint input[type=number],.hint input[type=text]{background:var(--panel2);color:var(--fg);border:1px solid var(--line);padding:4px 6px;border-radius:6px}
.hint input[type=file]{max-width:220px;font-size:12px}.hint button{padding:5px 9px;font-size:12px;border-radius:6px}
#q{min-height:58px}#refs{max-width:1100px;margin:8px auto}.refs-row{display:flex;gap:12px;align-items:center;flex-wrap:wrap}
#refs audio{height:28px;width:230px}.ref-note{color:var(--dim);font-size:11.5px;margin:6px 0 0}
.ref-name{color:var(--fg);max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.ref-slot{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0}.ref-slot button{background:var(--panel2);color:var(--dim);border:1px solid var(--line);padding:4px 8px;font-size:12px}
audio{height:36px;max-width:100%}.take{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:9px 12px;display:flex;flex-direction:column;gap:6px}.take.best{border-color:#426e55}
.take a{color:var(--accent);font-size:12px}.take .meta{line-height:1.5}#lat{position:static;align-self:flex-start;margin:18px 16px 0 0;flex:0 0 248px;width:248px}.empty{max-width:720px;margin:auto;color:var(--dim)}.empty h2{color:var(--fg);font-size:22px;font-weight:500}.empty p{margin:12px 0}.examples{display:flex;gap:8px;flex-wrap:wrap}.examples button{padding:6px 10px;background:var(--panel2);color:var(--fg);border:1px solid var(--line);font-size:12px}
.error{color:var(--warn)}.notice{color:var(--accent2);font-size:12px}#busy{color:var(--accent);font-size:12px;max-width:1100px;margin:8px auto 0;min-height:18px}#manualhelp{display:none;color:var(--dim);font-size:12px;max-width:1100px;margin:8px auto}
@media(max-width:700px){header{padding:10px 12px}header .sub{font-size:11px}#chat{padding:16px 12px}footer{padding:12px}.hint{gap:10px}.ref-slot{gap:5px}.msg{max-width:100%}}
</style>
</head>
<body>
<header><span class="dot" id="dot"></span><h1>Humaneness Voice Small</h1><span class="sub" id="status">Modelle laden…</span><a class="reportlink" href="https://laion-humaneness-voice-small-studies.static.hf.space/performance/index.html" target="_blank" rel="noopener">GPU benchmark ↗</a><a class="reportlink" href="https://laion-humaneness-voice-small-studies.static.hf.space/study/index.html" target="_blank" rel="noopener">CFG / temperature study ↗</a><a class="reportlink" href="https://huggingface.co/laion/Humaneness-Voice-Small" target="_blank" rel="noopener">Modell & Prompt-Guide ↗</a><button class="icon" id="reset" title="Neuer Chat">↺</button></header>
<main><div id="chat"><div class="empty" id="welcome"><h2>Eine Stimme. Viele Möglichkeiten.</h2><p>Schreib eine Nachricht oder lass einen Text spielen. Luna schreibt die Regie, Small erzeugt die Stimme. Lade für eine beständige Stimme ein eigenes Referenzaudio hoch; ohne Referenz ist die Ausgabe mit starker CFG weniger zuverlässig.</p><div class="examples"><button data-example="Sag mir etwas Tröstendes. Ich habe heute einen schwierigen Tag gehabt.">Sanft & nah</button><button data-example="Erzähl mir begeistert, dass wir es endlich geschafft haben.">Helle Freude</button><button data-example="Perform this line with restrained anger: You knew what this meant to me, and you did it anyway.">Zurückgehaltene Wut</button></div><p class="ref-note">GENERAL/SCRIPT bietet Regie, Pausen und Geräusche. CAPTION/TRANSCRIPT ist laut Modell-README meist verständlicher. Timing und Vocal Bursts sind experimentell.</p></div></div></main>
<div id="lat"><h2><span>Letzter Take</span><span id="lstate">bereit</span></h2><div class="l"><span>Luna</span><span id="m_llm">—</span></div><div class="l"><span>Generierung</span><span id="m_gen">—</span></div><div class="l"><span>Bewertung</span><span id="m_score">—</span></div><div class="l hi"><span>Gesamt</span><span id="m_tot">—</span></div><hr><div class="l"><span>Audio</span><span id="m_dur">—</span></div><div class="l"><span>Wortfehlerrate</span><span id="m_wer">—</span></div><div class="l"><span>CFG</span><span id="m_cfg">1.50</span></div></div>
<footer>
<div class="row"><textarea id="q" rows="2" placeholder="Sag etwas zu deinem Voice Actor… (Enter senden, Shift+Enter neue Zeile)"></textarea><button class="icon" id="mic" title="Aufnahme starten / stoppen und transkribieren">●</button><button id="send" disabled>Senden</button></div>
<div class="hint">
<label>Eingabe <select id="inputmode"><option value="chat">Chat mit Luna</option><option value="perform">Text wortgetreu spielen</option><option value="manual">Manueller Modell-Prompt</option></select></label>
<label>Format <select id="format"><option value="caption">CAPTION / TRANSCRIPT · empfohlen</option><option value="structured">GENERAL / SCRIPT</option><option value="tags">Tags + Caption / Transcript</option><option value="transcript">Nur TRANSCRIPT</option></select></label>
<label>Sprache <select id="language"><option value="auto">Automatisch</option><option value="de">Deutsch</option><option value="en">English</option></select></label>
<label>Generierung <select id="generation"><option value="single">Ein Take</option><option value="best">Best-of-N</option><option value="all">Alle N Takes vergleichen</option><option value="search_winner">Chunk-Suche · Gewinner aus N · live</option><option value="search_beam">Chunk-Suche · Top-2 Beam · gemeinsamer Präfix</option></select></label>
<label>N <input id="n" type="number" min="1" max="12" value="8"></label>
<label title="Streaming dekodiert PCM inkrementell auf der GPU. Die Browser-Wiedergabe wartet weiterhin auf Alignment und Bewertung aller Takes.">GPU-Pfad <select id="streaming"><option value="offline">Offline · schneller Gesamtdurchsatz</option><option value="streaming">Streaming PCM · Wiedergabe nach Bewertung</option></select></label>
<label title="Nur für den Streaming-Pfad: kleinere Chunks bringen früher PCM, kosten aber Codec-Durchsatz.">PCM-Chunk <select id="chunkframes"><option value="2">160 ms</option><option value="5">400 ms</option><option value="10" selected>800 ms</option><option value="20">1600 ms</option></select></label>
</div>
<div class="hint">
<label><input id="cfg" type="checkbox" checked> CFG</label><label>Stärke <input id="g" type="range" min="1" max="5" step="0.25" value="1.5"><b id="gv">1.50</b></label>
<label title="Studien-Ausgangspunkt: Temperatur 1,0 und CFG 1,5–2 mit CAPTION/TRANSCRIPT. Temperatur 0 bleibt für deterministisches Sampling verfügbar, war im Studienrezept aber nicht die beste Wahl.">Temperatur <input id="temperature" type="range" min="0" max="1.5" step="0.05" value="1"><b id="tv">1.00</b></label>
<label>Seed <input id="seed" type="number" min="0" max="2147483647" value="777" style="width:95px"></label>
<label>Dauer <input id="seconds" type="number" min="1" max="60" step="0.1" placeholder="auto"> s</label>
<label><input id="autoplay" type="checkbox" checked> Autoplay</label>
<label title="Qwen Forced Alignment: unerwünschten Vorlauf unterdrücken, nach dem letzten Wort ausblenden. Gewünschte Pausen und Geräusche bleiben geschützt."><input id="align" type="checkbox" checked> Sprachgrenzen & Fades <span id="alignstatus"></span></label>
<label>Referenz <select id="reference"><option value="">Ohne Referenz</option></select></label>
<button id="ref-toggle" style="background:var(--panel2);color:var(--fg)">Referenzaudio ▾</button>
</div>
<p class="ref-note" id="studyrecommendation">Studienempfehlung: CAPTION/TRANSCRIPT · Temperatur 1,00 · CFG 1,50 (Alternative: 2,00). Mit sauberem Sprach-Referenzaudio starten. Pilotstudie mit Modellbewertungen, kein universelles Optimum. <button id="recommended" type="button" style="font-size:12px;padding:4px 8px">Empfohlene Settings</button></p>
<div id="searchoptions" hidden><div class="hint"><label>Batchgröße / BS <select id="searchn"><option value="1">1</option><option value="2">2</option><option value="3">3</option><option value="4">4</option><option value="5">5</option><option value="6">6</option><option value="7">7</option><option value="8" selected>8</option><option value="9">9</option><option value="10">10</option><option value="11">11</option><option value="12">12</option><option value="13">13</option><option value="14">14</option><option value="15">15</option><option value="16">16</option></select></label><label>Such-Chunk <select id="searchchunk"><option value="10">800 ms</option><option value="20">1600 ms</option></select></label><label>Such-Limit <input id="searchseconds" type="number" min="0.8" max="60" step="0.1" value="6.6"> s</label></div><p class="ref-note">Experimentell: Gewinner-aus-N streamt bestätigte Chunks sofort. Top-2 expandiert beide Präfixe (bei ungerader BS z. B. 4+3; BS 1 = ein Pfad). Nur der gemeinsame Audiopräfix darf live ausgegeben werden, daher kann die Wiedergabe bis zum Ende warten. Das Limit ist eine Obergrenze, keine garantierte Dauer. Live-Audio ist roh; Fades gelten erst für den fertigen Take. BS 16 benötigt im gemessenen Ein-GPU-Test 1600 ms für RTF &lt;1. <a href="https://laion-humaneness-voice-small-studies.static.hf.space/search/index.html" target="_blank" rel="noopener">Search study & code ↗</a></p></div>
<div id="refs" hidden>
<div class="ref-slot"><span class="chip">A</span><input id="fileA" type="file" accept="audio/*,.wav,.mp3,.flac,.ogg,.m4a,.webm"><span id="nameA" class="ref-name">Keine Referenz</span><audio id="previewA" controls hidden></audio><button id="deleteA" hidden>Entfernen</button></div>
<div class="ref-slot"><span class="chip">B</span><input id="fileB" type="file" accept="audio/*,.wav,.mp3,.flac,.ogg,.m4a,.webm"><span id="nameB" class="ref-name">Keine Referenz</span><audio id="previewB" controls hidden></audio><button id="deleteB" hidden>Entfernen</button></div>
<p class="ref-note">Pro Take wird die ausgewählte Referenz verwendet. Uploads werden auf bis zu 2,88 Sekunden gekürzt. Referenzen und erzeugte Audios werden nach einer Stunde ohne Nutzung gelöscht. Nur deine Browser-Session hat Zugriff.</p>
</div>
<div id="manualhelp">Der manuelle Prompt wird unverändert verwendet und umgeht Luna. Das Transkript wird daraus abgeleitet. <button id="template" style="font-size:12px;padding:4px 8px">Vorlage einsetzen</button></div>
<div id="busy" role="status" aria-live="polite"></div>
</footer>
<script>
'use strict';
const $=id=>document.getElementById(id);let history=[],refs=[],running=false,ready=false,recorder=null,stream=null,chunks=[];
let livePlayback=null;
class LivePlayer{constructor(box,autoplay){this.ctx=new AudioContext();this.buffers=[];this.next=0;this.active=autoplay;this.played=false;this.sources=[];this.ctx.resume().catch(()=>{});this.panel=element('div','notice');this.label=element('span','','Live-Suche: noch kein irreversibler Audiopräfix. ');this.panel.append(this.label);const play=element('button','','Stream abspielen');play.onclick=()=>{this.ctx.resume();this.active=true;this.next=this.ctx.currentTime+.08;for(const b of this.buffers)this.schedule(b);};const stop=element('button','','Stream stoppen');stop.onclick=()=>{this.active=false;for(const s of this.sources){try{s.stop();}catch{}}this.sources=[];};this.panel.append(play,stop);box.append(this.panel);}schedule(buffer){const source=this.ctx.createBufferSource();source.buffer=buffer;source.connect(this.ctx.destination);this.next=Math.max(this.next,this.ctx.currentTime+.03);source.start(this.next);this.next+=buffer.duration;this.sources.push(source);this.played=true;}async chunk(event){const response=await checked(await fetch(event.url));const buffer=await this.ctx.decodeAudioData(await response.arrayBuffer());this.buffers.push(buffer);this.label.textContent='Bestätigtes Audio: '+event.committed_until_seconds.toFixed(2)+' s · '+this.buffers.length+' MP3-Chunks. ';if(this.active)this.schedule(buffer);}}
document.querySelector('main').append($('lat'));
const sec=ms=>(ms/1000).toFixed(2)+' s';
const alignMetric=element('div','l');alignMetric.append(element('span','','Sprachgrenzen'));const alignTime=element('span','','—');alignTime.id='m_align';alignMetric.append(alignTime);$('m_score').parentElement.before(alignMetric);
for(const [id,label] of [['m_gpu','GPU Modellzeit'],['m_rtf','On-device RTF'],['m_throughput','Audio s / GPU s']]){const row=element('div','l');row.title='TTS + PCM-Codec, CUDA-Event-Zeitspanne inklusive Launch-Pausen; ohne Luna, Referenz-Encoding, Alignment, Scoring, MP3 und Netzwerk. RTF über alle erzeugten Takes, vor dem Kürzen.';row.append(element('span','',label));const value=element('span','','—');value.id=id;row.append(value);$('m_dur').parentElement.before(row);}
function performanceDetails(box,result){const p=result.performance;if(!p){for(const id of ['m_gpu','m_rtf','m_throughput'])$(id).textContent='—';return;}$('m_gpu').textContent=p.device_seconds.toFixed(2)+' s';$('m_rtf').textContent=p.aggregate_rtf.toFixed(3)+(result.candidates.length>1?' · aggregiert':'');$('m_throughput').textContent=p.audio_seconds_per_second.toFixed(2);const d=element('details');d.append(element('summary','','On-device performance · '+(p.streaming?'incremental PCM':'offline PCM')));let lines=[p.backend,'GPU time: '+p.device_seconds.toFixed(3)+' s; raw audio across all takes: '+p.raw_audio_seconds.toFixed(2)+' s','Aggregate RTF = GPU time / sum of raw audio durations: '+p.aggregate_rtf.toFixed(4),'Included: '+p.includes,'Excluded: '+p.excludes,'Includes first-use compilation / graph capture if applicable. Playback waits for alignment and judging.'];for(const b of p.batches){const mean=b.audio_seconds.reduce((a,v)=>a+v,0)/b.batch_size;lines.push('Batch '+b.batch_size+': '+b.device_seconds.toFixed(3)+' s; per-stream RTF '+(b.device_seconds/mean).toFixed(3));}d.append(element('pre','',lines.join('\n')));box.append(d);}
function element(tag,cls,text){const e=document.createElement(tag);if(cls)e.className=cls;if(text!==undefined)e.textContent=text;return e;}
function scroll(){ $('chat').scrollTop=$('chat').scrollHeight; }
function message(role,text){$('welcome')?.remove();const box=element('div','msg '+role);box.append(element('div','bubble',text));$('chat').append(box);scroll();return box;}
async function checked(response){if(!response.ok){let d;try{d=await response.json();}catch{d={detail:'HTTP '+response.status};}throw Error(typeof d.detail==='string'?d.detail:JSON.stringify(d.detail));}return response;}
function renderRefs(list,selectId){refs=list;const selected=selectId??$('reference').value;$('reference').replaceChildren(new Option('Ohne Referenz',''));for(const r of refs){$('reference').add(new Option(r.slot+' · '+r.name,r.id));}if(refs.some(r=>r.id===selected))$('reference').value=selected;for(const slot of ['A','B']){const r=refs.find(x=>x.slot===slot);$('name'+slot).textContent=r?r.name:'Keine Referenz';$('preview'+slot).hidden=!r;$('delete'+slot).hidden=!r;if(r){if(!$('preview'+slot).src.endsWith(r.url))$('preview'+slot).src=r.url;}else $('preview'+slot).removeAttribute('src');}}
async function state(){try{const s=await(await checked(await fetch('/api/state'))).json();ready=s.ready&&!s.busy;$('dot').classList.toggle('ok',s.ready);$('status').textContent=s.error?'Fehler: '+s.error:s.ready?s.stage+' · '+s.director+' · 48 kHz':s.phase;$('alignstatus').textContent=s.ready&&!s.alignment_available?'(nicht verfügbar)':'';$('send').disabled=!ready||running;$('mic').disabled=!ready||running;renderRefs(s.references);if(s.busy&&!running){$('busy').textContent=`Server belegt: ${s.job_phase||'Generierung'} (${s.job_seconds??0} s). Bitte kurz warten.`;$('busy').dataset.external='yes';}else if($('busy').dataset.external==='yes'){delete $('busy').dataset.external;$('busy').textContent='';}if(s.ready&&!s.director_ready)$('busy').textContent='Luna API-Key fehlt; manuelle Prompts sind verfügbar.';return s;}catch(e){ready=false;$('send').disabled=true;$('status').textContent='Server wird neu gestartet / nicht erreichbar';}}
for(const slot of ['A','B']){$('file'+slot).onchange=async()=>{const file=$('file'+slot).files[0];if(!file)return;$('busy').textContent='Referenz '+slot+' hochladen…';try{const data=new FormData();data.append('file',file);const r=await(await checked(await fetch('/api/reference?slot='+slot,{method:'POST',body:data}))).json();renderRefs(r.references,r.id);$('busy').textContent='Referenz '+slot+' bereit · '+r.seconds+' s';}catch(e){$('busy').textContent=e.message;}finally{$('file'+slot).value='';}};$('delete'+slot).onclick=async()=>{const r=refs.find(x=>x.slot===slot);if(!r)return;try{const d=await(await checked(await fetch('/api/reference/'+r.id,{method:'DELETE'}))).json();renderRefs(d.references);}catch(e){$('busy').textContent=e.message;}};}
$('ref-toggle').onclick=()=>{$('refs').hidden=!$('refs').hidden;};
$('g').oninput=()=>$('gv').textContent=Number($('g').value).toFixed(2);$('temperature').oninput=()=>$('tv').textContent=Number($('temperature').value).toFixed(2);
$('cfg').onchange=()=>{$('g').disabled=!$('cfg').checked;};
$('generation').onchange=()=>{$('searchoptions').hidden=!$('generation').value.startsWith('search_');};
$('recommended').onclick=()=>{$('format').value='caption';$('temperature').value='1';$('temperature').oninput();$('cfg').checked=true;$('cfg').onchange();$('g').value='1.5';$('g').oninput();};
$('inputmode').onchange=()=>{$('manualhelp').style.display=$('inputmode').value==='manual'?'block':'none';$('send').textContent=$('inputmode').value==='manual'?'Generieren':'Senden';$('q').placeholder=$('inputmode').value==='manual'?'Modell-Prompt hier einfügen…':'Sag etwas zu deinem Voice Actor…';};
const templates={structured:'GENERAL: warm, natural, softly amused, conversational\nSCRIPT:\n(clearly amused, easy and unforced) [4.0 seconds duration] Ich hätte [0.4 seconds pause] wirklich nicht gedacht, dass wir es schaffen.',caption:'CAPTION: Warm, natural, softly amused conversation.\nTRANSCRIPT: "Ich hätte wirklich nicht gedacht, dass wir es schaffen."',tags:'CAPTION: Tags: warm, softly_amused, conversational. Caption: An easy, genuine moment of quiet relief.\nTRANSCRIPT: "Ich hätte wirklich nicht gedacht, dass wir es schaffen."',transcript:'TRANSCRIPT: "Ich hätte wirklich nicht gedacht, dass wir es schaffen."'};
$('template').onclick=()=>{$('q').value=templates[$('format').value];};
document.querySelectorAll('[data-example]').forEach(b=>b.onclick=()=>{$('q').value=b.dataset.example;$('q').focus();});
$('reset').onclick=()=>{if(running)return;history=[];$('chat').replaceChildren();$('busy').textContent='Neuer Chat. Deine Referenzen bleiben auswählbar.';};
function alignmentDetails(node,a){if(!a)return;const d=element('details','alignment');d.append(element('summary','',a.applied?'Sprachgrenzen & Fades angewendet':a.enabled?'Sprachgrenzen geprüft':'Sprachgrenzen ausgeschaltet'));let lines=[`Audio: ${a.raw_seconds.toFixed(3)} → ${a.seconds.toFixed(3)} s`];if(a.lead_s!=null)lines.push(`Fade-in: ${(a.lead_ramp_s*1000).toFixed(0)} ms bis zum ersten Wort bei ${a.lead_s.toFixed(3)} s`);if(a.cut_s!=null)lines.push(`Fade-out: ${(a.tail_ramp_s*1000).toFixed(0)} ms ab ${a.cut_s.toFixed(3)} s; ${a.removed_s.toFixed(3)} s Nachlauf entfernt`);if(a.note)lines.push(a.note);if(a.enabled&&!a.available)lines.push('Aligner nicht verfügbar; Audio unverändert.');if(a.spans?.length){lines.push('\nWortgrenzen im ursprünglichen Audio (Strukturprüfung, keine Modell-Konfidenz):');for(const [word,start,end] of a.spans)lines.push(`${start??'?'}–${end??'?'} s  ${word}`);}d.append(element('pre','',lines.join('\n')));node.append(d);}
function takes(box,result){let list=[...result.candidates];if(result.generation!=='all')list.sort((a,b)=>a.rank-b.rank);for(const c of list){const node=element('div','take'+(c.index===result.winner?' best':''));const title=result.generation==='single'?'Take':`Take ${c.index+1} · Rang ${c.rank+1}${c.index===result.winner?' · beste Bewertung':''}`;node.append(element('div','meta',title+` · ${c.sec.toFixed(2)} s · WER ${c.wer.toFixed(3)} · Reward ${c.reward.toFixed(3)}`));node.append(element('div','meta',`Genuineness ${c.genuineness.toFixed(2)} · Blend ${c.blend.toFixed(2)} · CLAP ${c.clap.toFixed(3)}${c.burst!==undefined?' · Burst '+c.burst.toFixed(3):''}`));const audio=element('audio');audio.controls=true;audio.preload='none';audio.src=c.url;node.append(audio);const a=element('a','', 'MP3 herunterladen · Mono · 96 kbit/s');a.href=c.url;a.download='humaneness-take-'+(c.index+1)+'.mp3';node.append(a);const detail=element('details');detail.append(element('summary','', 'Erkanntes Transkript'));detail.append(element('pre','',c.hyp));node.append(detail);alignmentDetails(node,c.alignment);box.append(node);if($('autoplay').checked&&(result.generation==='all'?c.index===0:c.index===result.winner))audio.play().catch(()=>{$('busy').textContent='Audio bereit. Zum Abspielen auf Play drücken.';});}
if(result.search_performance){const p=result.search_performance;$('m_gpu').textContent=p.tts_codec_seconds.toFixed(2)+' s · TTS/Codec';$('m_rtf').textContent=p.search_rtf.toFixed(3)+' · Suche + Scoring';$('m_throughput').textContent=(1/p.search_rtf).toFixed(2)+' · Audio s / Such-s';const d=element('details');d.append(element('summary','','Chunk search · '+p.n+' Stränge · Beam '+p.beam_width+' · '+p.chunk_ms+' ms'));d.append(element('pre','',JSON.stringify(p,null,2)));box.append(d);}else performanceDetails(box,result);
const best=result.candidates.find(c=>c.index===result.winner);$('m_align').textContent=sec(result.alignment_ms??0);$('m_llm').textContent=sec(result.llm_ms);$('m_gen').textContent=sec(result.generation_ms);$('m_score').textContent=sec(result.scoring_ms);$('m_tot').textContent=sec(result.total_ms);$('m_dur').textContent=best.sec.toFixed(2)+' s';$('m_wer').textContent=best.wer.toFixed(3);$('m_cfg').textContent=result.cfg_effective?result.guidance.toFixed(2):'aus / gleiche Prompts';if(best.wer>0.3)box.append(element('div','notice','Die Spracherkennung zeigt viele Abweichungen. Hör den Take an; versuche ein sauberes Referenzaudio, CAPTION/TRANSCRIPT oder eine niedrigere CFG-Stärke.'));if(!result.cfg_effective&&$('cfg').checked&&$('format').value==='transcript')box.append(element('div','notice','Ohne Regie sind beide CFG-Prompts identisch; CFG verändert diesen Take nicht.'));scroll();}
async function send(){
  const input=$('q').value.trim();if(!input||running||!ready)return;
  running=true;$('send').disabled=true;$('mic').disabled=true;$('lstate').textContent='arbeitet';
  const body={message:input,format:$('format').value,language:$('language').value,input_mode:$('inputmode').value,generation:$('generation').value,n:Number($('n').value),guidance:$('cfg').checked?Number($('g').value):1,align:$('align').checked,temperature:Number($('temperature').value),streaming:$('streaming').value==='streaming',chunk_frames:Number($('chunkframes').value),seed:Number($('seed').value),seconds:$('seconds').value?Number($('seconds').value):null,reference_id:$('reference').value||null,history,search_n:Number($('searchn').value),search_chunk_frames:Number($('searchchunk').value),search_seconds:Number($('searchseconds').value)};
  message('user',input);const box=message('bot','Regie wird vorbereitet…');const bubble=box.querySelector('.bubble');let spoken='',succeeded=false;
  if(livePlayback)livePlayback.ctx.close().catch(()=>{});
  livePlayback=body.generation.startsWith('search_')?new LivePlayer(box,$('autoplay').checked):null;
  try{
    const response=await checked(await fetch('/api/turn',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}));
    const reader=response.body.getReader(),decoder=new TextDecoder();let buffer='';
    while(true){
      const {value,done}=await reader.read();if(done)break;buffer+=decoder.decode(value,{stream:true});let end;
      while((end=buffer.indexOf('\n\n'))>=0){
        const frame=buffer.slice(0,end);buffer=buffer.slice(end+2);const line=frame.split('\n').find(x=>x.startsWith('data: '));if(!line)continue;
        const event=JSON.parse(line.slice(6));
        if(event.event==='status'){$('busy').textContent=event.phase+'…';if(!spoken)bubble.textContent=event.phase+'…';}
        if(event.event==='progress')$('busy').textContent=`${event.done} von ${event.total} Takes erzeugt…`;
        if(event.event==='search_chunk'&&livePlayback){await livePlayback.chunk(event);$('busy').textContent='Such-Chunk '+event.step+' · '+event.committed_until_seconds.toFixed(2)+' s bestätigt';}
        if(event.event==='direction'){
          spoken=event.text;bubble.textContent=spoken;box.append(element('div','meta',`${event.lang==='de'?'Deutsch':'English'} · ${event.format} · ${event.reference||'ohne Referenz'} · ${event.frames} Frames`));
          const d=element('details');d.append(element('summary','','Modell-Prompt & CFG ansehen / bearbeiten'));d.append(element('pre','',event.prompt));d.append(element('pre','','CFG-Vergleich:\n'+event.unconditional));
          const b=element('button','','Als manuellen Prompt bearbeiten');b.style.cssText='padding:6px 10px;font-size:12px;margin:8px 0';b.onclick=()=>{$('inputmode').value='manual';$('inputmode').onchange();$('format').value=event.format;$('q').value=event.prompt;$('q').focus();};d.append(b);box.append(d);scroll();
        }
        if(event.event==='result'){
          const autoplay=$('autoplay').checked;if(livePlayback?.played)$('autoplay').checked=false;
          takes(box,event);$('autoplay').checked=autoplay;succeeded=true;$('busy').textContent='Fertig · '+sec(event.total_ms);
        }
        if(event.event==='error')throw Error(event.message);
      }
    }
    if(!succeeded)throw Error('Verbindung beendet, bevor ein Ergebnis angekommen ist.');
    if(body.input_mode!=='manual'){history.push({role:'user',content:input},{role:'assistant',content:spoken});history=history.slice(-32);}
    $('q').value='';
  }catch(e){if(!spoken)bubble.textContent='Anfrage nicht abgeschlossen.';box.append(element('div','error',e.message));$('busy').textContent=e.message;scroll();}
  finally{running=false;$('lstate').textContent='bereit';await state();}
}
$('send').onclick=send;$('q').onkeydown=e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send();}};
$('mic').onclick=async()=>{if(recorder&&recorder.state==='recording'){recorder.stop();return;}try{stream=await navigator.mediaDevices.getUserMedia({audio:true});chunks=[];recorder=new MediaRecorder(stream);recorder.ondataavailable=e=>{if(e.data.size)chunks.push(e.data);};recorder.onstop=async()=>{stream.getTracks().forEach(t=>t.stop());$('mic').textContent='●';$('busy').textContent='Aufnahme transkribieren…';try{const blob=new Blob(chunks,{type:recorder.mimeType});const r=await(await checked(await fetch('/api/asr',{method:'POST',body:blob}))).json();$('q').value=r.text;$('busy').textContent='Transkript bereit; du kannst es bearbeiten und senden.';}catch(e){$('busy').textContent=e.message;}};recorder.start();$('mic').textContent='■';$('busy').textContent='Aufnahme läuft; erneut drücken zum Stoppen.';}catch(e){$('busy').textContent='Mikrofon: '+e.message;}};
state();setInterval(()=>{if(!running)state();},15000);
</script>
</body></html>