refactor: extract rehearser recording
This commit is contained in:
parent
ba7b3115b9
commit
0c7cc04643
@ -8,6 +8,10 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Production bundle refresh.** Rebuilt `static/dist/main.min.js` from the current manifest so the release path
|
||||
includes the validated frontend fixes and retry-history behavior.
|
||||
- **Rehearser recording extraction.** Microphone recording, level metering, overlay state, and recorded-line controls
|
||||
now live in `rehearser-recording.js`.
|
||||
- **Audiobook preview/export extraction.** Editable cast review, Rehearser handoff, chapter-aware synthesis/export,
|
||||
pause settings, and export browsing now live in `audiobook-preview-export.js`.
|
||||
- **Audiobook casting-pipeline extraction.** LLM casting, unknown-speaker recast, automatic repair, and cast-view
|
||||
|
||||
2
TODO.md
2
TODO.md
@ -121,7 +121,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
||||
## P1 — Modular frontend architecture
|
||||
|
||||
- [ ] Split `audiobook.js`, `rehearser.js`, and `style.css` by responsibility; Rehearser casting, playback, training,
|
||||
voice design, I/O, library, and stage blocks are now isolated, with import/event wiring and remaining Audiobook orchestration
|
||||
voice design, I/O, library, stage, and recording blocks are now isolated, with import/event wiring and remaining Audiobook orchestration
|
||||
still in the monolith; Audiobook LLM orchestration and preview/export are now isolated from core Reader wiring.
|
||||
- [x] Extract Rehearser stage display and cast-sidebar controls into `rehearser-stage.js`.
|
||||
- [ ] Replace broad globals with namespaces such as `window.TTVC.audiobook`; a compatibility namespace bridge now
|
||||
|
||||
@ -35,6 +35,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
|
||||
- ✅ `rehearser-stage.js` — stage font/cast sidebar controls and cast-strip rendering.
|
||||
- ✅ `rehearser-playback.js` — WebAudio playback, transport state, bounded decoded-audio prefetch, and persistent line-audio cache/synthesis controls.
|
||||
- ✅ `rehearser-io.js` — rehearsal serialization, library save/load, autosave, session/file export, and screenplay/PDF imports.
|
||||
- ✅ `rehearser-recording.js` — microphone recording overlay, level meter, and recorded-line controls.
|
||||
- ✅ `rehearser-voice-picker.js` — voice preview state, local matching, fish.audio search/import, and candidate assignment; DOM event registration remains in `rehearser-cast.js`.
|
||||
- ✅ `rehearser-cast.js` — cast view, full cast-card rendering, sort/filter state handlers, and delegated cast toolbar wiring.
|
||||
- ✅ `rehearser-voice-design.js` — character development, backend selection, and backend-refresh synchronization.
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.24.53">
|
||||
<meta name="app-version" content="1.24.54">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,12 +27,12 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.53">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.54">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.54">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.54">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.54">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.24.54">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.24.54">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.24.53"></script>
|
||||
<script src="/static/loader.js?v=1.24.54"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
151
static/js/rehearser-recording.js
Normal file
151
static/js/rehearser-recording.js
Normal file
@ -0,0 +1,151 @@
|
||||
// Rehearser microphone recording overlay and controls.
|
||||
// Loaded before rehearser.js; shared state and playback helpers resolve at call time.
|
||||
|
||||
// ── Recording overlay ───────────────────────────────────────────────────────
|
||||
|
||||
function showRecOverlay(line) {
|
||||
const overlay = $('reh-rec-overlay'); if (!overlay) return;
|
||||
overlay.hidden = false;
|
||||
const cue = $('reh-rec-cue'), c = rehState.cast[line.speaker] || { color: '#89b4fa' };
|
||||
if (cue) cue.innerHTML = `<span class="reh-rec-cue-name" style="color:${c.color}">${escHtml(line.speaker)}</span> — your line:<div class="reh-rec-cue-text">${escHtml(stripMarkdown(line.text))}</div>`;
|
||||
if ($('reh-rec-preview')) { $('reh-rec-preview').style.display='none'; $('reh-rec-preview').src=''; }
|
||||
if ($('reh-rec-confirm-row')) $('reh-rec-confirm-row').hidden = true;
|
||||
if ($('reh-rec-start')) $('reh-rec-start').disabled = false;
|
||||
if ($('reh-rec-stop')) $('reh-rec-stop').disabled = true;
|
||||
if ($('reh-rec-time')) $('reh-rec-time').textContent = '0:00';
|
||||
rehState.lastRecBlob = null;
|
||||
}
|
||||
|
||||
function hideRecOverlay() {
|
||||
const overlay = $('reh-rec-overlay'); if (overlay) overlay.hidden = true;
|
||||
stopRehMic();
|
||||
}
|
||||
|
||||
// ── Mic recording ───────────────────────────────────────────────────────────
|
||||
|
||||
function rehRenderMeter(level=0, db=-Infinity, clipped=false) {
|
||||
const meter = $('reh-mic-meter'); if (!meter) return;
|
||||
if (!meter.children.length) for (let i=0;i<18;i++){const b=document.createElement('div');b.className='bar';meter.appendChild(b);}
|
||||
const active = Math.round(Math.max(0,Math.min(1,level))*meter.children.length);
|
||||
[...meter.children].forEach((bar,i)=>{
|
||||
bar.className='bar'; bar.style.height=(7+Math.min(i,active)*1.55)+'px';
|
||||
if(i<active){bar.classList.add('on');if(db>-12&&i>11)bar.classList.add('hot');if(clipped&&i>14)bar.classList.add('clip');}
|
||||
});
|
||||
const el=$('reh-db-readout');if(el)el.textContent=Number.isFinite(db)?db.toFixed(1)+' dB':'-∞ dB';
|
||||
}
|
||||
|
||||
function rehStartMeter() {
|
||||
if (!rehState.recAnalyser) return;
|
||||
if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf);
|
||||
const data=new Float32Array(rehState.recAnalyser.fftSize), canvas=$('reh-live-wave'), RING=300, ADD=10;
|
||||
rehState.recWaveRing = new Float32Array(RING);
|
||||
const tick=()=>{
|
||||
rehState.recAnalyser.getFloatTimeDomainData(data);
|
||||
let sum=0,peak=0; for(const s of data){sum+=s*s;peak=Math.max(peak,Math.abs(s));}
|
||||
const rms=Math.sqrt(sum/data.length), db=rms>0?20*Math.log10(rms):-Infinity;
|
||||
rehRenderMeter((db+60)/60,db,peak>0.98);
|
||||
if(canvas&&rehState.recWaveRing){
|
||||
const ring=rehState.recWaveRing; ring.copyWithin(0,ADD);
|
||||
for(let i=0;i<ADD;i++) ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];
|
||||
const ctx=canvas.getContext('2d'),w=canvas.width,h=canvas.height;
|
||||
ctx.clearRect(0,0,w,h); ctx.beginPath();
|
||||
ctx.strokeStyle=peak>0.98?'#f38ba8':db>-12?'#f9e2af':'#a6e3a1'; ctx.lineWidth=1.5;
|
||||
const mid=h/2;
|
||||
for(let i=0;i<RING;i++){const x=(i/RING)*w,y=mid-ring[i]*mid*0.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
|
||||
ctx.stroke();
|
||||
}
|
||||
rehState.recMeterRaf=requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
async function startRehMic() {
|
||||
if (rehState.recDestStream) return;
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
rehState.recStream = await requestMicrophoneStream({ raw: true });
|
||||
if (AudioCtx) {
|
||||
rehState.recAudioCtx = new AudioCtx();
|
||||
rehState.recSourceNode = rehState.recAudioCtx.createMediaStreamSource(rehState.recStream);
|
||||
rehState.recGainNode = rehState.recAudioCtx.createGain();
|
||||
rehState.recAnalyser = rehState.recAudioCtx.createAnalyser();
|
||||
rehState.recAnalyser.fftSize = 1024;
|
||||
const dest = rehState.recAudioCtx.createMediaStreamDestination();
|
||||
rehState.recSourceNode.connect(rehState.recGainNode);
|
||||
rehState.recGainNode.connect(rehState.recAnalyser);
|
||||
rehState.recGainNode.connect(dest);
|
||||
rehState.recDestStream = dest.stream;
|
||||
rehStartMeter();
|
||||
} else { rehState.recDestStream = rehState.recStream; }
|
||||
}
|
||||
|
||||
function stopRehMic() {
|
||||
if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf);
|
||||
rehState.recMeterRaf = null;
|
||||
[rehState.recSourceNode,rehState.recGainNode,rehState.recAnalyser].forEach(n=>{try{if(n)n.disconnect();}catch(_){}});
|
||||
if (rehState.recStream) rehState.recStream.getTracks().forEach(t=>t.stop());
|
||||
if (rehState.recDestStream) rehState.recDestStream.getTracks().forEach(t=>t.stop());
|
||||
if (rehState.recAudioCtx) rehState.recAudioCtx.close().catch(()=>{});
|
||||
Object.assign(rehState,{recStream:null,recDestStream:null,recSourceNode:null,recGainNode:null,recAnalyser:null,recAudioCtx:null,recWaveRing:null});
|
||||
rehRenderMeter();
|
||||
const wc=$('reh-live-wave');if(wc)wc.getContext('2d').clearRect(0,0,wc.width,wc.height);
|
||||
}
|
||||
|
||||
$('reh-rec-start')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await startRehMic();
|
||||
rehState.recChunks=[]; rehState.recSecs=0;
|
||||
if($('reh-rec-time'))$('reh-rec-time').textContent='0:00';
|
||||
if($('reh-rec-start'))$('reh-rec-start').disabled=true;
|
||||
if($('reh-rec-stop'))$('reh-rec-stop').disabled=false;
|
||||
if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=true;
|
||||
rehState.recTimer=setInterval(()=>{
|
||||
rehState.recSecs++;
|
||||
if($('reh-rec-time'))$('reh-rec-time').textContent=Math.floor(rehState.recSecs/60)+':'+String(rehState.recSecs%60).padStart(2,'0');
|
||||
},1000);
|
||||
rehState.mediaRec=new MediaRecorder(rehState.recDestStream||rehState.recStream,{audioBitsPerSecond:256000});
|
||||
rehState.mediaRec.ondataavailable=e=>{if(e.data.size)rehState.recChunks.push(e.data);};
|
||||
rehState.mediaRec.onstop=()=>{
|
||||
clearInterval(rehState.recTimer);
|
||||
if($('reh-rec-start'))$('reh-rec-start').disabled=false;
|
||||
if($('reh-rec-stop'))$('reh-rec-stop').disabled=true;
|
||||
const blob=new Blob(rehState.recChunks,{type:rehState.mediaRec.mimeType||'audio/webm'});
|
||||
const url=URL.createObjectURL(blob);
|
||||
const p=$('reh-rec-preview');if(p){p.src=url;p.style.display='';}
|
||||
if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=false;
|
||||
rehState.lastRecBlob=blob;
|
||||
};
|
||||
rehState.mediaRec.start(100);
|
||||
} catch(e){stopRehMic();toast(await microphoneErrorMessage(e),'error');}
|
||||
});
|
||||
|
||||
$('reh-rec-stop')?.addEventListener('click', () => { if(rehState.mediaRec?.state!=='inactive')rehState.mediaRec.stop(); });
|
||||
|
||||
$('reh-rec-keep')?.addEventListener('click', () => {
|
||||
if (rehState.lastRecBlob)
|
||||
rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'me', blob:rehState.lastRecBlob});
|
||||
stopRehMic(); hideRecOverlay();
|
||||
rehState.lineIndex++;
|
||||
startPlay();
|
||||
});
|
||||
|
||||
$('reh-rec-redo')?.addEventListener('click', () => { stopRehMic(); const l=rehState.lines[rehState.lineIndex]; if(l)showRecOverlay(l); });
|
||||
|
||||
$('reh-skip-line')?.addEventListener('click', () => {
|
||||
rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'skip'});
|
||||
stopRehMic(); hideRecOverlay();
|
||||
rehState.lineIndex++;
|
||||
startPlay();
|
||||
});
|
||||
|
||||
|
||||
window.TTVC=window.TTVC||{};
|
||||
window.TTVC.rehearser=window.TTVC.rehearser||{};
|
||||
Object.assign(window.TTVC.rehearser,{
|
||||
recording:{
|
||||
showOverlay:showRecOverlay,
|
||||
hideOverlay:hideRecOverlay,
|
||||
start:startRehMic,
|
||||
stop:stopRehMic,
|
||||
},
|
||||
});
|
||||
|
||||
@ -2386,142 +2386,6 @@ $('reh-page-title')?.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape') { this.textContent = $('reh-script-title')?.value || 'Script'; this.blur(); }
|
||||
});
|
||||
|
||||
// ── Recording overlay ───────────────────────────────────────────────────────
|
||||
|
||||
function showRecOverlay(line) {
|
||||
const overlay = $('reh-rec-overlay'); if (!overlay) return;
|
||||
overlay.hidden = false;
|
||||
const cue = $('reh-rec-cue'), c = rehState.cast[line.speaker] || { color: '#89b4fa' };
|
||||
if (cue) cue.innerHTML = `<span class="reh-rec-cue-name" style="color:${c.color}">${escHtml(line.speaker)}</span> — your line:<div class="reh-rec-cue-text">${escHtml(stripMarkdown(line.text))}</div>`;
|
||||
if ($('reh-rec-preview')) { $('reh-rec-preview').style.display='none'; $('reh-rec-preview').src=''; }
|
||||
if ($('reh-rec-confirm-row')) $('reh-rec-confirm-row').hidden = true;
|
||||
if ($('reh-rec-start')) $('reh-rec-start').disabled = false;
|
||||
if ($('reh-rec-stop')) $('reh-rec-stop').disabled = true;
|
||||
if ($('reh-rec-time')) $('reh-rec-time').textContent = '0:00';
|
||||
rehState.lastRecBlob = null;
|
||||
}
|
||||
|
||||
function hideRecOverlay() {
|
||||
const overlay = $('reh-rec-overlay'); if (overlay) overlay.hidden = true;
|
||||
stopRehMic();
|
||||
}
|
||||
|
||||
// ── Mic recording ───────────────────────────────────────────────────────────
|
||||
|
||||
function rehRenderMeter(level=0, db=-Infinity, clipped=false) {
|
||||
const meter = $('reh-mic-meter'); if (!meter) return;
|
||||
if (!meter.children.length) for (let i=0;i<18;i++){const b=document.createElement('div');b.className='bar';meter.appendChild(b);}
|
||||
const active = Math.round(Math.max(0,Math.min(1,level))*meter.children.length);
|
||||
[...meter.children].forEach((bar,i)=>{
|
||||
bar.className='bar'; bar.style.height=(7+Math.min(i,active)*1.55)+'px';
|
||||
if(i<active){bar.classList.add('on');if(db>-12&&i>11)bar.classList.add('hot');if(clipped&&i>14)bar.classList.add('clip');}
|
||||
});
|
||||
const el=$('reh-db-readout');if(el)el.textContent=Number.isFinite(db)?db.toFixed(1)+' dB':'-∞ dB';
|
||||
}
|
||||
|
||||
function rehStartMeter() {
|
||||
if (!rehState.recAnalyser) return;
|
||||
if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf);
|
||||
const data=new Float32Array(rehState.recAnalyser.fftSize), canvas=$('reh-live-wave'), RING=300, ADD=10;
|
||||
rehState.recWaveRing = new Float32Array(RING);
|
||||
const tick=()=>{
|
||||
rehState.recAnalyser.getFloatTimeDomainData(data);
|
||||
let sum=0,peak=0; for(const s of data){sum+=s*s;peak=Math.max(peak,Math.abs(s));}
|
||||
const rms=Math.sqrt(sum/data.length), db=rms>0?20*Math.log10(rms):-Infinity;
|
||||
rehRenderMeter((db+60)/60,db,peak>0.98);
|
||||
if(canvas&&rehState.recWaveRing){
|
||||
const ring=rehState.recWaveRing; ring.copyWithin(0,ADD);
|
||||
for(let i=0;i<ADD;i++) ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];
|
||||
const ctx=canvas.getContext('2d'),w=canvas.width,h=canvas.height;
|
||||
ctx.clearRect(0,0,w,h); ctx.beginPath();
|
||||
ctx.strokeStyle=peak>0.98?'#f38ba8':db>-12?'#f9e2af':'#a6e3a1'; ctx.lineWidth=1.5;
|
||||
const mid=h/2;
|
||||
for(let i=0;i<RING;i++){const x=(i/RING)*w,y=mid-ring[i]*mid*0.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y);}
|
||||
ctx.stroke();
|
||||
}
|
||||
rehState.recMeterRaf=requestAnimationFrame(tick);
|
||||
};
|
||||
tick();
|
||||
}
|
||||
|
||||
async function startRehMic() {
|
||||
if (rehState.recDestStream) return;
|
||||
const AudioCtx = window.AudioContext || window.webkitAudioContext;
|
||||
rehState.recStream = await requestMicrophoneStream({ raw: true });
|
||||
if (AudioCtx) {
|
||||
rehState.recAudioCtx = new AudioCtx();
|
||||
rehState.recSourceNode = rehState.recAudioCtx.createMediaStreamSource(rehState.recStream);
|
||||
rehState.recGainNode = rehState.recAudioCtx.createGain();
|
||||
rehState.recAnalyser = rehState.recAudioCtx.createAnalyser();
|
||||
rehState.recAnalyser.fftSize = 1024;
|
||||
const dest = rehState.recAudioCtx.createMediaStreamDestination();
|
||||
rehState.recSourceNode.connect(rehState.recGainNode);
|
||||
rehState.recGainNode.connect(rehState.recAnalyser);
|
||||
rehState.recGainNode.connect(dest);
|
||||
rehState.recDestStream = dest.stream;
|
||||
rehStartMeter();
|
||||
} else { rehState.recDestStream = rehState.recStream; }
|
||||
}
|
||||
|
||||
function stopRehMic() {
|
||||
if (rehState.recMeterRaf) cancelAnimationFrame(rehState.recMeterRaf);
|
||||
rehState.recMeterRaf = null;
|
||||
[rehState.recSourceNode,rehState.recGainNode,rehState.recAnalyser].forEach(n=>{try{if(n)n.disconnect();}catch(_){}});
|
||||
if (rehState.recStream) rehState.recStream.getTracks().forEach(t=>t.stop());
|
||||
if (rehState.recDestStream) rehState.recDestStream.getTracks().forEach(t=>t.stop());
|
||||
if (rehState.recAudioCtx) rehState.recAudioCtx.close().catch(()=>{});
|
||||
Object.assign(rehState,{recStream:null,recDestStream:null,recSourceNode:null,recGainNode:null,recAnalyser:null,recAudioCtx:null,recWaveRing:null});
|
||||
rehRenderMeter();
|
||||
const wc=$('reh-live-wave');if(wc)wc.getContext('2d').clearRect(0,0,wc.width,wc.height);
|
||||
}
|
||||
|
||||
$('reh-rec-start')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await startRehMic();
|
||||
rehState.recChunks=[]; rehState.recSecs=0;
|
||||
if($('reh-rec-time'))$('reh-rec-time').textContent='0:00';
|
||||
if($('reh-rec-start'))$('reh-rec-start').disabled=true;
|
||||
if($('reh-rec-stop'))$('reh-rec-stop').disabled=false;
|
||||
if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=true;
|
||||
rehState.recTimer=setInterval(()=>{
|
||||
rehState.recSecs++;
|
||||
if($('reh-rec-time'))$('reh-rec-time').textContent=Math.floor(rehState.recSecs/60)+':'+String(rehState.recSecs%60).padStart(2,'0');
|
||||
},1000);
|
||||
rehState.mediaRec=new MediaRecorder(rehState.recDestStream||rehState.recStream,{audioBitsPerSecond:256000});
|
||||
rehState.mediaRec.ondataavailable=e=>{if(e.data.size)rehState.recChunks.push(e.data);};
|
||||
rehState.mediaRec.onstop=()=>{
|
||||
clearInterval(rehState.recTimer);
|
||||
if($('reh-rec-start'))$('reh-rec-start').disabled=false;
|
||||
if($('reh-rec-stop'))$('reh-rec-stop').disabled=true;
|
||||
const blob=new Blob(rehState.recChunks,{type:rehState.mediaRec.mimeType||'audio/webm'});
|
||||
const url=URL.createObjectURL(blob);
|
||||
const p=$('reh-rec-preview');if(p){p.src=url;p.style.display='';}
|
||||
if($('reh-rec-confirm-row'))$('reh-rec-confirm-row').hidden=false;
|
||||
rehState.lastRecBlob=blob;
|
||||
};
|
||||
rehState.mediaRec.start(100);
|
||||
} catch(e){stopRehMic();toast(await microphoneErrorMessage(e),'error');}
|
||||
});
|
||||
|
||||
$('reh-rec-stop')?.addEventListener('click', () => { if(rehState.mediaRec?.state!=='inactive')rehState.mediaRec.stop(); });
|
||||
|
||||
$('reh-rec-keep')?.addEventListener('click', () => {
|
||||
if (rehState.lastRecBlob)
|
||||
rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'me', blob:rehState.lastRecBlob});
|
||||
stopRehMic(); hideRecOverlay();
|
||||
rehState.lineIndex++;
|
||||
startPlay();
|
||||
});
|
||||
|
||||
$('reh-rec-redo')?.addEventListener('click', () => { stopRehMic(); const l=rehState.lines[rehState.lineIndex]; if(l)showRecOverlay(l); });
|
||||
|
||||
$('reh-skip-line')?.addEventListener('click', () => {
|
||||
rehState.clips.push({lineIndex:rehState.lineIndex, speaker:rehState.lines[rehState.lineIndex]?.speaker, type:'skip'});
|
||||
stopRehMic(); hideRecOverlay();
|
||||
rehState.lineIndex++;
|
||||
startPlay();
|
||||
});
|
||||
|
||||
// ── Phase 4: Summary ────────────────────────────────────────────────────────
|
||||
|
||||
function renderSummary() {
|
||||
|
||||
@ -4,9 +4,9 @@
|
||||
"integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace"
|
||||
],
|
||||
"lazy": {
|
||||
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
||||
"s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets"],
|
||||
"s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||
"s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser-recording", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
||||
"s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser-recording", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets"],
|
||||
"s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser-recording", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||
"s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser-io", "rehearser-playback", "rehearser-recording", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook-pipeline", "audiobook-preview-export", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,5 +52,6 @@ test('Rehearser playback module publishes transport APIs after loading', async (
|
||||
synthAll: typeof window.TTVC?.rehearser?.playback?.synthAll,
|
||||
loadRecord: typeof window.TTVC?.rehearser?.io?.loadRecord,
|
||||
exportSession: typeof window.TTVC?.rehearser?.io?.exportSession,
|
||||
}))).toEqual({ start: 'function', stop: 'function', synthAll: 'function', loadRecord: 'function', exportSession: 'function' });
|
||||
recording: typeof window.TTVC?.rehearser?.recording?.start,
|
||||
}))).toEqual({ start: 'function', stop: 'function', synthAll: 'function', loadRecord: 'function', exportSession: 'function', recording: 'function' });
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user