64 lines
3.0 KiB
JavaScript
64 lines
3.0 KiB
JavaScript
/* Model the cut rules with the SHIPPED constants and drive them with synthetic
|
|||
|
|
speech patterns. The question is: how long does a segment get, per style? */
|
||
|
|
const fs=require('fs');
|
||
|
|
const src=fs.readFileSync('engine.js','utf8');
|
||
|
|
const g=(n)=>Number(src.match(new RegExp('const '+n+'\\s*=\\s*Math\\.round\\((\\d+) / FRAME_MS\\)'))[1]);
|
||
|
|
const FRAME=Number(src.match(/const FRAME\s*=\s*(\d+)/)[1]);
|
||
|
|
const SAMPLE_RATE=Number(src.match(/const SAMPLE_RATE\s*=\s*(\d+)/)[1]);
|
||
|
|
const FRAME_MS=(FRAME/SAMPLE_RATE)*1000;
|
||
|
|
const F=(n)=>Math.round(g(n)/FRAME_MS); // source stores ms; the loop counts frames
|
||
|
|
const C={SOFT_CUT:F('SOFT_CUT'),SOFT_DIP:F('SOFT_DIP'),HARD_CUT:F('HARD_CUT'),
|
||
|
|
HARD_CUT_MAX:F('HARD_CUT_MAX'),EARLY_CLOSE:F('EARLY_CLOSE'),
|
||
|
|
EARLY_MIN_SEG:F('EARLY_MIN_SEG'),SILENCE_TO_END:F('SILENCE_TO_END')};
|
||
|
|
console.log('FRAME_MS='+FRAME_MS+' constants (ms): '+
|
||
|
|
Object.entries(C).map(([k,v])=>k+'='+Math.round(v*FRAME_MS)).join(' ')+'\n');
|
||
|
|
|
||
|
|
/* pattern: fn(frameIndex) -> {speech:bool, dipish:bool, sentenceEnded:bool} */
|
||
|
|
function run(name, pattern, maxFrames){
|
||
|
|
let len=0,silence=0,dip=0,cuts=[];
|
||
|
|
for (let i=0;i<maxFrames;i++){
|
||
|
|
const p=pattern(i);
|
||
|
|
len++;
|
||
|
|
if (p.speech){ silence=0; dip=0; }
|
||
|
|
else { silence++; dip++; }
|
||
|
|
if (!p.speech && p.dipish) dip++;
|
||
|
|
let why=null;
|
||
|
|
if (len>=C.HARD_CUT_MAX) why='ceiling';
|
||
|
|
else if (silence>=C.SILENCE_TO_END) why='silence';
|
||
|
|
else if (len>=C.SOFT_CUT && dip>=C.SOFT_DIP) why=(len>=C.HARD_CUT?'hard':'soft');
|
||
|
|
else if (p.sentenceEnded && len>=C.EARLY_MIN_SEG && silence>=C.EARLY_CLOSE) why='early';
|
||
|
|
else if (p.sentenceEnded && len>=C.SOFT_CUT) why='sentence';
|
||
|
|
if (why){ cuts.push({ms:len*FRAME_MS,why}); len=0;silence=0;dip=0; }
|
||
|
|
}
|
||
|
|
const avg=cuts.length?Math.round(cuts.reduce((a,c)=>a+c.ms,0)/cuts.length):0;
|
||
|
|
const worst=cuts.length?Math.max(...cuts.map(c=>c.ms)):0;
|
||
|
|
console.log(name);
|
||
|
|
console.log(' segments: '+cuts.length+' avg '+avg+'ms worst '+worst+'ms');
|
||
|
|
console.log(' reasons : '+JSON.stringify(cuts.reduce((a,c)=>(a[c.why]=(a[c.why]||0)+1,a),{})));
|
||
|
|
return {avg,worst};
|
||
|
|
}
|
||
|
|
|
||
|
|
const S=(ms)=>Math.round(ms/FRAME_MS);
|
||
|
|
/* A fast preacher: continuous speech, a sentence boundary every ~4s, and no
|
||
|
|
pause anywhere long enough for the old rules to notice. */
|
||
|
|
run('FAST preacher — no pauses, sentence every 4s', (i)=>({
|
||
|
|
speech:true, dipish:false, sentenceEnded: i%S(4000)===S(3999)
|
||
|
|
}), S(60000));
|
||
|
|
|
||
|
|
/* Same speaker but Whisper never reports a sentence end (worst case). */
|
||
|
|
run('FAST preacher — no pauses, no punctuation at all', (i)=>({
|
||
|
|
speech:true, dipish:false, sentenceEnded:false
|
||
|
|
}), S(60000));
|
||
|
|
|
||
|
|
/* A normal speaker: 150ms micro-gaps between phrases every 2s. */
|
||
|
|
run('NORMAL speaker — 150ms gap every 2s', (i)=>{
|
||
|
|
const inGap = (i%S(2000)) >= S(1850);
|
||
|
|
return { speech:!inGap, dipish:inGap, sentenceEnded:false };
|
||
|
|
}, S(60000));
|
||
|
|
|
||
|
|
/* Someone who stops properly at the end of each sentence. */
|
||
|
|
run('DELIBERATE speaker — 600ms full stop every 5s', (i)=>{
|
||
|
|
const inGap = (i%S(5000)) >= S(4400);
|
||
|
|
return { speech:!inGap, dipish:inGap, sentenceEnded:false };
|
||
|
|
}, S(60000));
|