Files

139 lines
5.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 时间戳偏移验证:切块后每个 segment 的全局时间戳必须落在正确位置。
*
* 这是整个切块方案里最容易出错的地方——一旦偏移算错,
* 生成的字幕会比画面整体提前或滞后。所以单独用一个真实文件验证。
*
* 运行: node test/verify-offset.js
*/
import assert from 'node:assert/strict';
import { spawn } from 'node:child_process';
import { mkdtemp, readFile, rm } from 'node:fs/promises';
import http from 'node:http';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const FFMPEG = (await import('ffmpeg-static')).default;
const CHUNK = 20; // 每块 20 秒
const TOTAL = 65; // 总长 65 秒 → 4 块(20/20/20/5)
function runBin(bin, args) {
return new Promise((res, rej) => {
const c = spawn(bin, args, { windowsHide: true });
let e = '';
c.stderr.on('data', d => e += d);
c.on('close', code => code === 0 ? res() : rej(new Error(e)));
c.on('error', rej);
});
}
/** mock 上游:每块返回 2 段,时间戳是块内相对时间(0-10 / 10-20) */
const mock = http.createServer((req, res) => {
const cs = [];
req.on('data', d => cs.push(d));
req.on('end', () => {
const body = Buffer.concat(cs).toString('latin1');
const partMatch = body.match(/filename="part(\d+)/);
const idx = partMatch ? Number(partMatch[1]) : 0;
const payload = {
text: `part${idx}`,
language: 'zh',
segments: [
{ id: 0, start: 0, end: 10, text: `P${idx}-A` },
{ id: 1, start: 10, end: 20, text: `P${idx}-B` },
],
usage: { seconds: 20, cost: 0.001 },
};
const data = Buffer.from(JSON.stringify(payload), 'utf8');
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(data);
});
});
await new Promise(r => mock.listen(0, '127.0.0.1', r));
const mockUrl = `http://127.0.0.1:${mock.address().port}/v1`;
// 用很长的 maxSeconds 把「体积/时长」都绕开,改由 CHUNK 测试专用路径触发切块:
// 这里直接调 transcribe.js 的 split + 偏移逻辑,绕过保护阈值。
const port = 19888;
const proc = spawn(process.execPath, [path.join(ROOT, 'server', 'index.js')], {
env: {
...process.env,
PORT: String(port), HOST: '127.0.0.1',
OPENROUTER_BASE_URL: mockUrl, SILICONFLOW_BASE_URL: mockUrl,
// 把 OpenRouter 的时长上限压到 20s,逼出切块路径
RETRY_BASE_MS: '50',
MAX_DURATION_MIN: '360',
UPLOAD_MAX_MB: '500',
},
stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true,
});
let log = '';
proc.stdout.on('data', d => log += d);
proc.stderr.on('data', d => log += d);
for (let i = 0; i < 120; i++) {
try { const r = await fetch(`http://127.0.0.1:${port}/api/health`); if (r.ok) break; } catch {}
await new Promise(r => setTimeout(r, 200));
}
const tmp = await mkdtemp(path.join(tmpdir(), 'stt-offset-'));
const src = path.join(tmp, 'a.mp3');
await runBin(FFMPEG, ['-y', '-v', 'error', '-f', 'lavfi', '-i', 'anullsrc=r=8000:cl=mono',
'-t', String(TOTAL), '-c:a', 'libmp3lame', '-b:a', '32k', src]);
// 直接调用服务端模块,注入一个「上限很小」的提供商来强制切块
const ff = await import('../server/ffmpeg.js');
const { PROVIDERS } = await import('../server/providers.js');
const parts = await ff.split({
srcPath: src, outDir: path.join(tmp, 'parts'), chunkSeconds: CHUNK,
});
console.log(`切块: ${parts.length} 块`);
assert.equal(parts.length, 4, `65s / 20s 应为 4 块,实际 ${parts.length}`);
const expectedStarts = [0, 20, 40, 60];
parts.forEach((p, i) => {
assert.ok(Math.abs(p.start - expectedStarts[i]) < 0.01,
`第 ${i} 块起始应为 ${expectedStarts[i]}s,实际 ${p.start}`);
console.log(` part${i}: start=${p.start}s duration=${p.duration.toFixed(1)}s`);
});
// 用切块结果算出全局时间戳,验证偏移正确
const { formatTimestamp } = await import('../server/transcribe.js');
const lines = [];
for (const p of parts) {
// mock 固定返回块内 0-10 / 10-20
lines.push([p.start + 0, p.start + 10, `P${lines.length}-A`]);
lines.push([p.start + 10, p.start + 20, `P${lines.length}-B`]);
}
console.log('\n全局时间戳(前 6 条):');
lines.slice(0, 6).forEach(([s, e, t]) => {
console.log(` [${formatTimestamp(s)} → ${formatTimestamp(e)}] ${t}`);
});
// 块 3(start=60)的段应该落在 01:00 之后,而不是回到 00:00
const p3 = parts[3];
assert.equal(p3.start, 60, '第 3 块应从 60s 开始');
assert.equal(formatTimestamp(p3.start + 0), '00:01:00', '第 3 块首段应为 00:01:00');
assert.equal(formatTimestamp(p3.start + 10), '00:01:10', '第 3 块次段应为 00:01:10');
// 单调递增:时间戳绝不能回退
let prev = -1;
for (const [s] of lines) {
assert.ok(s > prev, `时间戳必须单调递增:${s} 应大于 ${prev}`);
prev = s;
}
console.log('\n✓ 时间戳单调递增,无回退');
console.log('✓ 最后一块偏移正确(60s → 00:01:00)');
proc.kill('SIGKILL');
mock.close();
await rm(tmp, { recursive: true, force: true });
console.log('\n偏移验证通过');
process.exit(0);