Spaces:
Running
Running
Download taurus/tools/align.mjs from Ethscriptions/POLS: direct link, hf CLI and curl.
- Browser
- Download file 2.65 kB
-
https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/tools/align.mjs
- Command line
-
hf download hf://spaces/Ethscriptions/POLS/taurus/tools/align.mjs
-
curl -L -o align.mjs https://huggingface.co/spaces/Ethscriptions/POLS/resolve/main/taurus/tools/align.mjs
2.65 kB
| // 字幕与旁白波形对齐:把每条字幕起点吸附到真实发声起点(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)); | |