ChristophSchuhmann's picture
Publish study artifacts 1-150 of 1487
a2313f5 verified
Raw History Blame Contribute Delete
27.6 kB
<!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>