AI 短剧配音别硬塞进音符:SSE 旁白、BGM 与字幕时间线怎么接

发布时间:2026/10/25 17:53:54
AI 短剧配音别硬塞进音符:SSE 旁白、BGM 与字幕时间线怎么接

一段 30 秒短剧里,人物说完一句“门外是谁”,音乐才慢慢抬起。如果把旁白、字幕和伴奏各自按收到消息的时刻开播,网速就会变成剪辑师。先固定作品时间轴,再接生成服务,问题会清楚得多。

先写 cue sheet,再接接口

例子使用自编情节:角色听到门响、走近门口、决定不开门。声音交付分成旁白音频、背景音乐、字幕三项。每一项记录素材 ID、源文件采样率、作品内起点和结束点;作品时间统一用整数毫秒。旁白 400–1800ms、字幕同段显示,音乐可以贯穿,但旁白出现时应降低音乐音量。

10 月的 LakeAIService 更新记录收录了语音与视频工作流能力;规划阶段读到的语音文档更新日期为 10 月 16 日。此次复核只返回 JavaScript 空壳页,不能据此补造新的字段。下方使用自定义教学事件,不冒充厂商原始响应。接真实服务时应在服务端做一次明确的协议适配,并保存脱敏样本。

SSE 分帧和 JSON 解析是两件事

一次网络读取可能只有半行,也可能带多个事件。必须先保留残片、按空行分帧,再拼接多行 data。HTTP 字节还要先经过流式 TextDecoder,不能直接把每块字节当完整 UTF-8。原生 EventSource 面向 GET 事件流;需要 POST 请求体的语音流通常要用 fetch 加 reader,自行处理取消和异常。

以下核心接受已经解码的字符串,为待解析缓冲和 data 累积设置长度上限;特别大的单次读块也可能被拒绝。它识别 LF/CRLF 行,只处理本例需要的 data 字段;单独 CR 行结束还未实现,完整 SSE 客户端还涉及 id、event、retry 和重新连接语义。

function dataFrames() { let pending = '', data = [], size = 0; return chunk => { pending += chunk; if (pending.length > 65536) throw Error('line limit'); const out = []; for (;;) { const i = pending.indexOf('\n'); if (i < 0) break; let line = pending.slice(0,i); pending = pending.slice(i+1); if (line.endsWith('\r')) line = line.slice(0,-1); if (line === '') { if (data.length) out.push(data.join('\n')); data=[];size=0;continue; } if (line.startsWith(':')) continue; if (line === 'data' || line.startsWith('data:')) { let value=line.slice(5);if(value.startsWith(' '))value=value.slice(1); size += value.length;if(size>65536)throw Error('frame limit'); data.push(value); } } return out; }; } function cueSheet(cues, durationMs=30000) { const ids=new Set(); for(const c of cues){ if(!c || ids.has(c.id) || typeof c.id!=='string' || !['voice','music','caption'].includes(c.track) || !Number.isInteger(c.startMs) || !Number.isInteger(c.endMs) || c.startMs<0 || c.endMs<=c.startMs || c.endMs>durationMs) throw Error('invalid cue'); ids.add(c.id); } return [...cues].sort((a,b)=>a.startMs-b.startMs); } function framePayload(text) { const outer=JSON.parse(text); // 只有合同明确要求时才二次解析,不能对任意字符串反复 JSON.parse。 return typeof outer.payload==='string' ? JSON.parse(outer.payload) : outer.payload; }

到达时间不能充当音频时间

收到音频块后,作品内的时间戳仍按 cue sheet 解释。若解码后的素材长度与合同不符,先报出差异,不能悄悄把字幕拖到网络回包位置。采样率转换由明确的解码/重采样步骤承担;把 24kHz 样本按 48kHz 播放会同时改变时长和音高。

图中三轨对齐与音乐下压都是设计示意,400–1800ms 来自本文构造的剪辑合同。真正混音时,以 AudioContext.currentTime 或离线渲染时钟安排节点;字幕使用同一个作品起点换算。ducking 可先对旁白区间生成带 attack/release 的增益包络,再听审是否抽吸,不能仅凭峰值较低就说混音自然。

失败后恢复哪一段

断线重试若从头生成,应生成新的素材版本,不能直接把新音频追加到旧块后面。只有服务明确提供稳定分片标识和续传合同,才能去重续接。取消 fetch 也不等于服务端任务取消,更不等于已排程的声音停止。

本轮测试覆盖了半行、CRLF、多行 data、双层教学载荷和 cue 越界。没有调用 TTS、解码或渲染音频,没有交付虚构 WAV,也未验证字幕对口型。接入前还需检查素材授权、服务实际事件格式、结束帧和错误帧,并用一段授权旁白完成听审。

复现记录

本轮在 Node.js v26.3.0 中运行随文核心逻辑的构造样例,进程退出码为 0。测试输出:

PASS: fragmented CRLF, multi-line data, normalized nested payload, cue boundaries

将上方完整代码保存为 example.mjs,以 Node.js 26 执行 node example.mjs 可加载核心与教学输入;函数默认不访问网络、模型或硬件。浏览器适配入口需要明确传入对应对象,本轮未执行这些入口。上面的通过项来自独立构造样例,不能替代集成测试。

资料与时效

  • 功能发布记录(2026):LAS AI 1.6.0–1.6.2:2026-10-10 / 2026-10-15 / 2026-10-21。
  • 语音合成(LAS):首次发布日期未核实;更新 2026-10-16。
  • HyperFrames 视频生成(LAS 算子):首次发布日期未核实;更新 2026-10-21。
  • Web Audio API 1.1:2026-09-22。
  • HTML Living Standard: Server-sent events:首次发布日期未核实;更新 2026-10-22。

本稿对应原预排时段;实际发布前须人工复核来源更正、SDK/API 版本及可用性,不据此预言 10 月 24–31 日的新事件。