subtitle-forge
v0.1.0
Published
Generate and translate timestamped subtitles from timed transcripts while preserving cue timing.
Maintainers
Readme
subtitle-forge
面向 AI Agent、云函数和后台 Worker 的无副作用字幕生成与翻译库。
subtitle-forge 实现的是这条可复用链路:
带时间戳逐字稿 -> 字幕 cue -> SRT/WebVTT -> 保留时间轴的翻译字幕它不提交 ASR 任务、不读写文件、不运行 FFmpeg、不封装视频、不管理本地输出目录。这些事情应该由你的应用、CLI 或 Worker 负责。这个包只接收 JSON/string 输入,并返回 JSON/string 输出。
英文文档见 README.md。
安装
npm install subtitle-forge要求:
- Node.js 22 或更新版本。
- 使用 ESM import。
- 默认使用全局
fetch;也可以给LlmTranslator传入自定义fetch。
什么时候使用这个包
当你已经有以下任意输入时,使用这个包:
- provider-neutral 的
TimedTranscript,包含词级时间戳。 - Speechmatics
json-v2transcript。 - 已有 SRT 文本,需要翻译字幕文本并保留原始编号和时间轴。
不要把它当完整视频流水线使用。它故意不做抽音频、调用 ASR provider、烧录字幕或上传媒体文件。
给 AI Agent 的使用约定
如果你是 AI 编码 Agent,请遵循这个约定:
- 先把 ASR 输出转换为
TimedTranscript:words必须包含{ text, start, end },时间单位是秒。start和end必须是有限数字。- 如果 ASR 有说话人信息,保留
speaker。 - 如果 ASR 输出已经处理好空格,可以在句子/片段开头设置
delimiterBefore: ""。 - 词尾是句末时设置
eos: true,这会帮助字幕切分。
- 只需要源字幕时,调用
buildSourceSubtitles()。 - 需要从带时间戳逐字稿生成源字幕和翻译字幕时,调用
translateTimedTranscript()。 - 已经有 SRT、只需要翻译时,调用
translateSrtText()。 - 把
translation.items存到外部系统以支持断点续跑,下次调用时作为existingItems传回。 - 不要让 LLM 改字幕编号或时间戳。这个库通过 cue
index替换文本来保留时间轴。 - 如果
TimedTranscript.words为空,应该提前失败或先做 ASR/forced alignment。只有纯文本、没有时间戳时,无法可靠生成 SRT 时间轴。
快速开始:带时间戳逐字稿到翻译 SRT
import { translateTimedTranscript } from "subtitle-forge";
const result = await translateTimedTranscript({
transcript: {
provider: "your-asr",
language: "en",
words: [
{ text: "Hello", start: 0.0, end: 0.2, delimiterBefore: "" },
{ text: "world.", start: 0.25, end: 0.7, eos: true },
{ text: "Let's", start: 1.4, end: 1.7, delimiterBefore: "" },
{ text: "begin.", start: 1.75, end: 2.1, eos: true },
],
},
llm: {
apiKey: process.env.LLM_API_KEY,
baseUrl: process.env.LLM_BASE_URL ?? "https://api.openai.com/v1",
model: process.env.LLM_MODEL,
},
sourceLanguage: "English",
targetLanguage: "Simplified Chinese",
subtitleFormats: ["srt", "vtt"],
});
console.log(result.source.srt);
console.log(result.translation.srt);
console.log(result.translation.vtt);
console.log(result.translation.items);快速开始:已有 SRT 到翻译 SRT
import { translateSrtText } from "subtitle-forge";
const result = await translateSrtText({
srt: `1
00:00:00,000 --> 00:00:01,000
Hello, world.
`,
llm: {
apiKey: process.env.LLM_API_KEY,
model: process.env.LLM_MODEL,
},
sourceLanguage: "English",
targetLanguage: "Simplified Chinese",
});
console.log(result.srt);断点续跑
translateTimedTranscript() 和 translateSrtText() 都支持 existingItems 和 onProgress。
import { translateSrtText, type TranslationItem } from "subtitle-forge";
const existingItems: TranslationItem[] = await loadCheckpointFromDatabase(jobId);
const result = await translateSrtText({
srt,
llm,
targetLanguage: "Japanese",
existingItems,
async onProgress(items, progress) {
await saveCheckpointToDatabase(jobId, {
completed: progress.completed,
total: progress.total,
items,
});
},
});
await saveFinalSubtitle(jobId, result.srt);checkpoint 的核心格式只是:
type TranslationItem = {
index: number;
text: string;
};库会忽略与当前 cue indexes 不匹配的 checkpoint item。
自定义翻译器
你可以不使用内置 OpenAI-compatible client,而是注入自己的 translator。这适合测试、内部网关、队列任务或非 OpenAI provider。
import {
translateSrtText,
type SubtitleCueTranslator,
} from "subtitle-forge";
const translator: SubtitleCueTranslator = {
async translateCues({ cues }) {
return cues.map((cue) => ({
index: cue.index,
text: `translated: ${cue.text}`,
}));
},
};
const result = await translateSrtText({
srt,
translator,
});自定义 translator 必须为每个尚未被 existingItems 覆盖的输入 cue index 返回一个非空 TranslationItem。
云函数模式
import { translateTimedTranscript } from "subtitle-forge";
export async function handleSubtitleJob(request: {
jobId: string;
transcript: unknown;
existingItems?: Array<{ index: number; text: string }>;
}) {
const result = await translateTimedTranscript({
transcript: request.transcript as any,
llm: {
apiKey: process.env.LLM_API_KEY,
baseUrl: process.env.LLM_BASE_URL,
model: process.env.LLM_MODEL,
},
sourceLanguage: "auto",
targetLanguage: "Simplified Chinese",
existingItems: request.existingItems,
subtitleFormats: ["srt", "vtt"],
async onProgress(items, progress) {
await saveJobState(request.jobId, { ...progress, items });
},
});
await saveObject(`${request.jobId}/source.srt`, result.source.srt);
await saveObject(`${request.jobId}/translation.zh.srt`, result.translation.srt);
return {
sourceCueCount: result.source.cues.length,
translatedCueCount: result.translation.items.length,
};
}公共 API
translateTimedTranscript(options)
从带时间戳逐字稿生成源字幕,然后翻译生成的源 SRT,并同时返回源字幕和翻译字幕。
function translateTimedTranscript(
options: TranslateTimedTranscriptOptions,
): Promise<TranslateTimedTranscriptResult>;关键参数:
| 参数 | 类型 | 默认值 | 说明 |
| --- | --- | --- | --- |
| transcript | TimedTranscript | 必填 | provider-neutral 的带时间戳词数组。 |
| translator | SubtitleCueTranslator | 可选 | 自定义翻译 provider。 |
| llm | LlmTranslatorOptions | 可选 | 内置 OpenAI-compatible translator 配置。没有 translator 时必填。 |
| sourceLanguage | string | "auto" | 源语言名称或代码,用于翻译 prompt。 |
| targetLanguage | string | "Simplified Chinese" | 目标语言名称或代码。 |
| cueOptions | CueOptions | 见下文 | 控制词级时间戳如何切成字幕 cue。 |
| subtitleOptions | SubtitleTextOptions | { maxLineLength: 37, maxLines: 2 } | 控制 SRT/VTT 渲染时的换行。 |
| subtitleFormats | SubtitleFormat[] | ["srt"] | 包含 "vtt" 时额外返回 WebVTT。 |
| batchSize | number | 30 | 每次 LLM 请求提交多少条字幕 cue。 |
| contextWindow | number | 8 | 每批前后额外发送多少条 cue 作为上下文。 |
| existingItems | TranslationItem[] | [] | 已完成翻译 checkpoint,用于跳过已翻译 cue。 |
| onProgress | callback | 可选 | 每批翻译完成后调用。 |
返回结构:
type TranslateTimedTranscriptResult = {
source: {
transcriptText: string;
cues: SubtitleSegment[];
srt: string;
vtt?: string;
};
translation: {
sourceCues: SrtCue[];
items: TranslationItem[];
cues: SrtCue[];
srt: string;
vtt?: string;
};
};buildSourceSubtitles(options)
只生成源语言字幕,不调用 LLM。
function buildSourceSubtitles(options: {
transcript: TimedTranscript;
cueOptions?: CueOptions;
subtitleOptions?: SubtitleTextOptions;
subtitleFormats?: SubtitleFormat[];
}): SourceSubtitleResult;translateSrtText(options)
翻译已有 SRT 文本,同时保留 cue 编号和时间戳。
function translateSrtText(
options: TranslateSrtTextOptions,
): Promise<TranslatedSubtitleResult>;当 ASR 或字幕编辑器已经生成 SRT 时,使用这个函数。
LlmTranslator
OpenAI-compatible Chat Completions 翻译器。
const translator = new LlmTranslator({
apiKey: process.env.LLM_API_KEY,
baseUrl: "https://api.openai.com/v1",
model: "your-model",
temperature: 0.2,
thinking: "disabled",
reasoningEffort: "low",
fetch: customFetch,
});它调用:
POST {baseUrl}/chat/completions期望响应结构:
{
"choices": [
{
"message": {
"content": "{\"items\":[{\"index\":1,\"text\":\"...\"}]}"
}
}
]
}内置 translator 会:
- 只发送 cue
index和text,不发送时间戳。 - 发送
context_before和context_after来保持上下文连续。 - 校验返回的 index 完整性。
- 修复一些常见的 LLM JSON 格式问题。
- 当整批失败时拆成更小 batch 重试。
数据类型
TimedTranscript
type TimedTranscript = {
provider?: string;
language?: string;
text?: string;
words: TimedWord[];
raw?: unknown;
};TimedWord
type TimedWord = {
text: string;
start: number;
end: number;
speaker?: string;
delimiterBefore?: string;
eos?: boolean;
};规则:
start和end的单位是秒。text可以包含标点。- 拼接词文本时,
delimiterBefore默认是空格。 - 使用
delimiterBefore: ""可以避免在某个词前插入空格。 eos表示句末,有助于字幕切分。- 相邻词都存在
speaker且 speaker 不同时,会强制切分 cue。
CueOptions
type CueOptions = {
maxDuration?: number; // 默认 4.2 秒
targetDuration?: number; // 默认 2.8 秒
maxChars?: number; // 默认 54
maxWords?: number; // 默认 12
pauseThreshold?: number; // 默认 0.55 秒
minDuration?: number; // 默认 0.45 秒
startPadding?: number; // 默认 0.08 秒
endPadding?: number; // 默认 0.16 秒
nextCueGap?: number; // 默认 0.05 秒
};SubtitleSegment
type SubtitleSegment = {
index?: number;
start_time: number;
end_time: number;
content: string;
speaker?: string;
};SrtCue
type SrtCue = {
index: number;
start: string; // HH:MM:SS,mmm
end: string; // HH:MM:SS,mmm
text: string;
};TranslationItem
type TranslationItem = {
index: number;
text: string;
};底层工具函数
import {
cleanSubtitleText,
cuesToSrt,
cuesToVtt,
formatSrtTimestamp,
formatVttTimestamp,
parseSrt,
parseSubtitleFormats,
replaceCueText,
segmentsToSrt,
segmentsToVtt,
timedTranscriptToPlainText,
timedTranscriptToWordCues,
timedWordsToCues,
wrapSubtitleText,
} from "subtitle-forge";常见用途:
timedWordsToCues(words, cueOptions)把词级时间戳切成字幕片段。segmentsToSrt(segments, subtitleOptions)把本地字幕片段渲染成 SRT。parseSrt(srt)把 SRT 解析成 cues。replaceCueText(cues, items)保留时间轴,只替换字幕文本。cuesToVtt(cues, subtitleOptions)渲染 WebVTT。
Speechmatics 辅助函数
如果输入是 Speechmatics json-v2,可以使用:
import {
speechmaticsTranscriptToTimedTranscript,
transcriptJsonToPlainText,
transcriptJsonToTimedWords,
} from "subtitle-forge";
const timedTranscript = speechmaticsTranscriptToTimedTranscript(jsonV2);其他 ASR provider 应由你的应用映射为 TimedTranscript。
错误行为
以下情况会抛错:
- 没有 timed words,无法生成字幕。
- SRT 输入中没有可解析的 cue。
- 翻译时既没有提供
translator,也没有提供llm。 - LLM 响应缺少一个或多个请求的 cue index。
- LLM 响应在基础修复后仍无法解析为 JSON。
- OpenAI-compatible endpoint 返回非 2xx 响应。
Import 路径
import { translateTimedTranscript } from "subtitle-forge";
import { parseSrt } from "subtitle-forge/srt";
import { LlmTranslator } from "subtitle-forge/llm";
import { timedWordsToCues } from "subtitle-forge/transcript";除非你明确需要更小的子模块入口,否则建议使用根 import。
发布检查清单
维护者发布前运行:
npm test
npm pack --dry-run -w subtitle-forge
npm publish -w subtitle-forge --access public这个包是 scoped package,并且已设置 publishConfig.access = public。
