CLAUDE.md 11 KB

CLAUDE.md

本文件为 Claude Code (claude.ai/code) 在本仓库中工作时提供指引。

项目概述

文本转视频生成流水线。接收描述视频结构(封面、场景、结束语)的 JSON 输入,输出带 TTS 旁白、字幕和模板视觉样式的 MP4 视频。

常用命令

pnpm build          # 构建所有包 (turbo)
pnpm typecheck      # 类型检查所有包
pnpm lint           # Lint 所有包

# 运行视频渲染
node apps/cli/dist/index.js render test/fixtures/sample-knowledge.json -t knowledge -p bilibili

# Web UI 开发服务器
cd apps/web && pnpm dev

部署(Docker Compose / HTTP API / 配置 / OSS·飞书发布)详见 docs/DEPLOYMENT.md,环境变量模板见 .env.example

架构

pnpm workspaces + Turborepo 的 monorepo 结构。

apps/cli/          CLI 入口 (Commander)
apps/web/          Next.js 15 WebUI + API 路由
packages/shared/   Zod schema、类型定义、常量、LLM 客户端
packages/core/     流水线编排与 6 个阶段
packages/tts/      TTS provider 注册表 + 4 个 provider
packages/templates/ 5 个模板的 Remotion React 组件

流水线数据流

输入始终是结构化 JSON (VideoInputSchema)。流水线在 packages/core/src/stages/ 中依次执行 6 个阶段:

  1. parse — 通过 VideoInputSchema 验证 JSON,转换为内部 ParsedContent(cover → scene-0,outro → 最后一个 scene)。github-trending 在这里:①内容场景按 github.repo.todayStars 降序取 top 6(视频只介绍今日涨星最多的 6 个,顺序与首屏一致);②确定性写入 cover 开场口播(一句承接语 下面进入项目详解。不含日期/数量——日期与卡片由首屏视觉呈现);③承载 LLM 产出的 trendSummary(一句话趋势)透传到首屏渲染。不再注入 summary 场景,也已停用 coverTags
  2. tts — 按场景调用 TTS provider,生成逐场景音频文件 + word timestamps
  3. assets — 解析图片资源(本地 path > 远程 url > 关键词 query),复制背景图/字体
  4. compose — 转换为带帧时间轴的 ComposedProject,word timestamps 转为场景内相对时间。逐字段拷贝场景数据——新增场景字段时必须在此阶段显式透传buildInputProps...scene 自动透传,但 compose 是手写字段映射)
  5. render — 打包 Remotion bundle,将资源复制到 publicDir,通过 renderMedia 渲染
  6. export — 输出最终 MP4 到统一输出目录,按 {模板名称}/{ISO日期(YYYY-MM-DD)}/{原文件名}.mp4 自动建子目录
  7. publish(可选,按需开启)— 渲染成功后上传到阿里云 OSS 并推送飞书 webhook:上传成功推送 OSS 资源链接,生成/上传失败推送失败信息。生成失败(未产出文件)也会推送失败信息。未配置 OSS/飞书时跳过

核心类型链:VideoInputParsedContentComposedProject → Remotion props。

Remotion 约束

Remotion 通过其 webpack dev server 提供所有静态资源。组件中绝不能使用文件系统绝对路径 — 必须使用 staticFile(filename)。render 阶段会将所有资源(音频、图片、背景图)复制到统一的 publicDir,通过 props 传递文件名。

音频按场景独立播放:每个 <Sequence> 包含自己的 <Audio> 组件,不使用全局音频轨道。

TTS Provider 体系

Provider 通过 registerProvider()packages/tts/src/providers/ 中注册。每个 provider 实现 TTSProvider 接口(synthesizelistVoicesalign)。新增 provider 的方式:在 providers/ 下创建文件并在 index.ts 中 import。

模板系统

5 个模板(news、knowledge、opinion、marketing、github-trending),每个在 packages/templates/src/<name>/index.tsx 中有对应的场景组件。共享基础组件位于 packages/templates/src/base/Root.tsx 按以下规则路由:

  • sceneType === "cover" → 内置 CoverScenegithub-trending cover 渲染:深色标题卡居中靠上 + top6 仓库卡片网格[横屏 3×2 / 竖屏 2×3,每卡:名称/语言/今日 +N 涨星/highlights 一句话] + trendSummary 副标题;另在进度条上方渲染 ChapterToc 章节目录,当前章节高亮)
  • sceneType === "outro" → 内置 OutroScene
  • 其他 content 类型 → SCENE_MAP[template] 对应模板组件

github-trending 是当前唯一带 isPortrait 分支的模板,竖屏布局采用 flex column(头部取自然高度,内容 flex 填充),横屏保持绝对定位。Section 子组件通过 size: "default" | "large" 区分横竖屏字号档位,并用 -webkit-line-clamp 做安全截断(body 用 flex: "0 0 auto" 防止 shrink 导致的单行底部裁剪)。

SubtitleBarpackages/templates/src/base/components/subtitle-bar.tsx)接受可选 fontSizestyle prop——竖屏场景可传 { bottom: 380 } 提高字幕位置、fontSize: 56 放大字号;横屏不传则保持默认(bottom: 60fontSize: 40)。

输出目录与发布

统一输出目录

CLI / HTTP / WebUI 的视频都写入同一个输出目录,由 packages/sharedresolveOutputDir() 统一解析(相对路径相对 monorepo 根,CLI 与 apps/web 因此落地到同一物理目录)。解析优先级:OUTPUT_DIR 环境变量 > config.output.dir > ./output

目录布局:{outputDir}/{模板}/{ISO日期 YYYY-MM-DD}/{模板}-{平台}-{jobId前8位}.mp4,在 export.ts 中由 isoDateString() 生成日期子目录。日期取配置时区(默认 Asia/Shanghai,可用 TIMEZONE 覆盖)而非 UTC,与首屏显示日期保持一致,避免 UTC 容器跨天错位。

缓存自动清理

每次 runPipeline 启动时按 config.output.retentionDays(默认 30,可用 OUTPUT_RETENTION_DAYS 覆盖,设 0 禁用)做一次机会性扫描,删除 mtime 超过 TTL 的文件及随之变空的目录(packages/core/src/cleanup.ts)。该清理是 best-effort,永不抛错、不会中断渲染。

发布(OSS + 飞书)

publish 阶段位于 packages/core/src/publish/oss.tsali-oss 分片上传、feishu.ts 带可选 HMAC 签名)。resolvePublishConfig()config/default.yamloss/feishu 与环境变量合并;只要 OSS 或飞书任一可用就启用。行为:渲染成功→上传成功推送 OSS 链接;上传失败推送"已生成但上传失败";渲染本身失败推送"生成失败"。本地调试可用 CLI --no-publish 跳过,或 pipelineConfig.skipPublish。OSS 上传的 URL 会回填到 ExportFile.ossUrl 并由 CLI 打印 oss: <url>(Web 解析后存入 JobData.ossUrls)。

publish 仅在 CLI 进程(runPipeline)中执行;WebUI 通过 spawn CLI 复用该流程,不直接依赖 @pipeline/core/ali-oss,故 ali-oss 只出现在 core 的依赖里。

配置

  • .env(monorepo 根目录)— 所有 provider 的 API Key(CLI 启动时加载,Next.js 通过 next.config.ts 加载)
  • config/default.yaml — TTS provider、模型、模板配色、输出设置、OSS/飞书非敏感配置
  • CLI 按以下顺序读取配置:--config 参数 → pipeline.config.yamlconfig/default.yaml

发布相关环境变量

  • OUTPUT_DIR / OUTPUT_RETENTION_DAYS — 统一输出目录与缓存保留天数
  • TIMEZONE — github-trending 首屏日期与输出日期子目录所用时区,默认 Asia/Shanghai(Docker 同时据此设 TZ
  • OSS_REGION OSS_BUCKET OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET OSS_ENDPOINT OSS_PATH OSS_PUBLIC_DOMAIN OSS_SECURE — 阿里云 OSS(密钥走 .env)
  • FEISHU_WEBHOOK_URL FEISHU_WEBHOOK_SECRET — 飞书自定义机器人 webhook
  • SCHEDULES — JSON 数组,整体覆盖 config/default.yamlschedules(部署期改定时任务无需重建镜像)
  • SCHEDULER_ENABLEDfalse 关闭容器内进程内调度器(默认开启)

定时生成(容器内调度)

容器内置进程内调度器:Next.js 服务启动时经 apps/web/src/instrumentation.tsregister() 钩子)启动 apps/web/src/lib/scheduler.ts,用 node-cronconfig/default.yamlschedules 段(cron + TZ,默认 Asia/Shanghai)到点 spawn CLI 渲染——复用 /api/render 同一链路(apps/web/src/lib/run-render.tsstartRenderJob),任务进 jobs 列表、走 OSS/飞书发布。单副本下进程内调度不重复执行;内置单飞(上一次未结束则跳过)。改配置后重启容器生效;SCHEDULES 环境变量可整体覆盖,SCHEDULER_ENABLED=false 关闭。详见 docs/DEPLOYMENT.md §4.3。

核心 Schema

全部在 packages/shared/src/types/ 中用 Zod 定义:

  • VideoInputSchema — 用户输入格式(title、subtitle、cover、scenes[]、outro、globalStyle;github-trending 另有可选 trendSummary,由 LLM 产出。coverTags 字段保留但已停用)
  • SceneSchema — 内部场景,含 sceneType: "cover" | "content" | "summary" | "outro"、可选 github(GithubSceneData,repotodayStars 今日涨星)、可选 trendSummary(string,仅 github-trending 的 cover 场景用。coverTags 保留但停用)
  • ComposedSceneSchema — compose 后的场景,含帧时间轴、audioPath、已解析的图片。字段与 SceneSchema 一一对应
  • PipelineInputSchema — 顶层流水线输入(text、template、platform、ttsProvider)

新增场景字段时三个 schema 都要同步: SceneSchema(scene.ts)、ComposedSceneSchema(pipeline.ts)、RemotionScene(Root.tsx 的 interface)。还要在 compose.ts 手动透传(不是 ...scene 展开),否则字段会在 compose 阶段丢失。

LLM 与 TTS 提示

  • LLMClientpackages/shared/src/llm/client.ts)设置 max_tokens: 16384chat() 返回 { content, finishReason }。github-trending 大 trending 列表可能撑爆 token 上限被截断 → JSON 不完整;parse 阶段对解析失败会重试一次(追加精简指令让响应在限额内闭合),错误信息带 finish_reason 便于诊断
  • github-trending 的 cover narration 是 parse.ts 硬编码的一句承接语(下面进入项目详解。),不交给 LLM,也不含日期/仓库数量。trendSummary(一句话趋势)由 LLM 产出并透传到首屏渲染。LLM 只产出 per-repo scenes[](数据源规则要求按今日涨星降序选 6 个),不生成 overview/summary 场景;parse 端会再次按 todayStars 排序取 top6 作兜底。coverTags 已停用
  • TTS 按场景独立合成,受 provider 配额限制;调试布局可用 --no-tts 跳过(生成静音视频)