Spaces:
Running
Running
File size: 2,652 Bytes
87b5b90 | 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 | // 字幕与旁白波形对齐:把每条字幕起点吸附到真实发声起点(node tools/align.mjs,需要 FF)
import fs from 'fs'; import path from 'path'; import { execFileSync } from 'child_process'; import { fileURLToPath } from 'url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'), SP = path.join(ROOT, 'js/script.js');
const raw = fs.readFileSync(SP, 'utf8'), FILM = JSON.parse(raw.slice(raw.indexOf('{'), raw.lastIndexOf('}') + 1));
const R = 8000, HOP = 80, PRE = .06, report = [];
for (const A of FILM.acts) {
const b = execFileSync(process.env.FF || 'ffmpeg', ['-v', 'error', '-i', path.join(ROOT, A.voice.file), '-ac', '1', '-ar', '' + R, '-f', 'f32le', '-'], { maxBuffer: 1 << 26 });
const y = new Float32Array(b.buffer, b.byteOffset, b.length / 4), env = [];
for (let i = 0; i < y.length; i += HOP) { let s = 0; for (let j = i; j < i + HOP && j < y.length; j++) s += y[j] * y[j]; env.push(Math.sqrt(s / HOP)); }
const pk = Math.max(...env), th = Math.max(.012, pk * .06);
const onset = rel => { const f = Math.round(rel * 100); let best = null;
for (let d = -30; d <= 50; d++) { const i = f + d; if (i > 4 && env[i] > th && env.slice(i - 5, i).every(e => e < th) && (best === null || Math.abs(d) < Math.abs(best))) best = d; } return best; };
A.subs.forEach(s => { const d = onset(s.t0 - A.voice.at); if (d === null) { report.push(['?', s.text]); return; }
const t0 = A.voice.at + (Math.round((s.t0 - A.voice.at) * 100) + d) / 100 - PRE; report.push([Math.round((t0 + PRE - s.t0) * 1000), s.text]);
s.t0 = +t0.toFixed(3); });
// 结束点:本句起点到下一句起点之间最后一个有声帧之后 0.15 秒
A.subs.forEach((s, i) => { const lim = i < A.subs.length - 1 ? A.subs[i + 1].t0 + PRE : A.voice.at + env.length / 100;
let last = Math.round((s.t0 + PRE - A.voice.at) * 100); for (let f = last; f < Math.round((lim - A.voice.at) * 100) && f < env.length; f++) if (env[f] > th) last = f;
s.t1 = +(A.voice.at + last / 100 + .15).toFixed(3); });
for (let i = 0; i < A.subs.length - 1; i++) { const a = A.subs[i], n = A.subs[i + 1]; if (a.t1 > n.t0 - .02 || n.t0 - a.t1 < .35) a.t1 = +(n.t0 - .02).toFixed(3); }
A.subs.forEach(s => { if (s.t1 < s.t0 + .6) s.t1 = +(s.t0 + .6).toFixed(3); });
}
fs.writeFileSync(SP, raw.slice(0, raw.indexOf('{')) + JSON.stringify(FILM, null, 1) + raw.slice(raw.lastIndexOf('}') + 1));
const k = report.filter(r => r[0] !== '?').map(r => r[0]); console.log('aligned', k.length, '/', report.length, 'shift mean', (k.reduce((a, b) => a + b, 0) / k.length).toFixed(0), 'ms; min', Math.min(...k), 'max', Math.max(...k));
|