npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

subtitle-forge

v0.1.0

Published

Generate and translate timestamped subtitles from timed transcripts while preserving cue timing.

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-v2 transcript。
  • 已有 SRT 文本,需要翻译字幕文本并保留原始编号和时间轴。

不要把它当完整视频流水线使用。它故意不做抽音频、调用 ASR provider、烧录字幕或上传媒体文件。

给 AI Agent 的使用约定

如果你是 AI 编码 Agent,请遵循这个约定:

  1. 先把 ASR 输出转换为 TimedTranscript
    • words 必须包含 { text, start, end },时间单位是秒。
    • startend 必须是有限数字。
    • 如果 ASR 有说话人信息,保留 speaker
    • 如果 ASR 输出已经处理好空格,可以在句子/片段开头设置 delimiterBefore: ""
    • 词尾是句末时设置 eos: true,这会帮助字幕切分。
  2. 只需要源字幕时,调用 buildSourceSubtitles()
  3. 需要从带时间戳逐字稿生成源字幕和翻译字幕时,调用 translateTimedTranscript()
  4. 已经有 SRT、只需要翻译时,调用 translateSrtText()
  5. translation.items 存到外部系统以支持断点续跑,下次调用时作为 existingItems 传回。
  6. 不要让 LLM 改字幕编号或时间戳。这个库通过 cue index 替换文本来保留时间轴。
  7. 如果 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() 都支持 existingItemsonProgress

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 indextext,不发送时间戳。
  • 发送 context_beforecontext_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;
};

规则:

  • startend 的单位是秒。
  • 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