From 9b23f69d9c4001ded05c797280103fec50bc5dbf Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Tue, 11 Aug 2026 11:29:14 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9E=E5=88=86=E9=95=9C?= =?UTF-8?q?=E2=86=92=E6=97=B6=E9=97=B4=E7=BA=BF=E2=86=92=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=20pipeline=20=E8=AE=BE=E8=AE=A1=E6=96=B9?= =?UTF-8?q?=E6=A1=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 独立纯函数包 video/pipeline/:重建时间线+回写对齐、prompt 实体名→token 替换、按可配置 schema 组装完整视频 API 请求体。参考 video-factory 实现,方案 B 从零设计。 Co-Authored-By: Claude Opus 4.7 --- ...26-08-11-shots-timeline-pipeline-design.md | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-11-shots-timeline-pipeline-design.md diff --git a/docs/superpowers/specs/2026-08-11-shots-timeline-pipeline-design.md b/docs/superpowers/specs/2026-08-11-shots-timeline-pipeline-design.md new file mode 100644 index 0000000..2d1a1a5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-11-shots-timeline-pipeline-design.md @@ -0,0 +1,226 @@ +# 分镜 → 时间线 → 模型语言 Pipeline 设计 + +日期:2026-08-11 +状态:已评审(用户确认) +范围:ai-agent 新增独立纯函数包 `video/pipeline/` + +## 1. 背景与动机 + +`script_transcribe` 节点产出 `[{"shots":[...]}]` 后,视频生成需要把分镜镜头转成视频模型能理解的输入。video-factory 项目已有完整链路:**时间线分段 → prompt(实体名→characterN 替换)→ 按 schema 嵌套的完整视频 API 请求体**。 + +ai-agent 现有 `video/plan/` 已覆盖"重建时间线 + 拆段 + 中文 prompt + 扁平 modelCall 参数",但与 video-factory 存在差异,且缺少三件事: + +1. **时间线口径**:现有 `NormalizeShots` 丢弃 AI 时间戳、按台词字数重建时长(保留此口径,用户选定"重建但回写对齐"); +2. **跨边界镜头不切分**:跨段边界的镜头原样保留、时间码不回写对齐(本方案改为按段边界切分 + 短残片并入); +3. **模型语言转换缺失**: + - prompt 内实体名未替换成 `characterN` 等 token(`reference_labels` 单独传,靠模型自行理解); + - 无按 schema 嵌套的完整视频 API 请求体(`video-factory` 的 `BuildSchemaRequest` 等价物)。 + +用户决策:**新流程从零设计**,做成**独立纯函数包**(不依赖 `plan`、不碰 workflow、零 I/O),**外部直接传 shots**,目标视频 API **先做通用抽象**(schema 可配),模型语言覆盖 **prompt 文本层 + 完整请求体层**。 + +## 2. 目标与非目标 + +### 目标 +- 提供一个纯函数入口:`shots → 时间线(重建+回写对齐)→ 分段 → prompt(token 替换)→ 完整视频 API 请求体` +- 通用抽象:模型 schema 可配置,未配置时用默认火山风格结构 +- 可独立单测,零 I/O、零 workflow 依赖 + +### 非目标(本阶段不做) +- 不接入 workflow 节点 / 前后置处理器(后续可写薄适配层) +- 不调用模型网关 / 媒体服务(`video/plan` 与 `video/` 根包负责) +- 不做视频合并(concat/merge)、不做首帧联动(serial) +- 不重构现有 `video/plan/` 或 `script_transcribe` + +## 3. 包结构与依赖 + +- 新包:`ai-agent/video/pipeline/` +- 依赖:仅 `ai-agent/video/domain`(复用 `domain.Shot` 作为输入契约)+ Go 标准库(`encoding/json`、`strings`、`fmt` 等) +- **不 import** `plan`、workflow、gateway、media 等 +- 内部自带:时间线重建、超长镜头切分、段时长计算、回写对齐、prompt 构建、schema 请求体构建 + +文件规划(建议): +``` +video/pipeline/ + pipeline.go // Input/Segment/Output 契约 + Run 编排入口 + timeline.go // ① 时间线构建:rebuildDurations / splitOversized / calcSegmentDurations / alignToSegments + prompt.go // ② prompt 构建:实体名→token 替换、截断 + request.go // ③ 请求体构建:schema 嵌套、默认 schema、media 组装 + *_test.go // 各阶段单测 + 用户样本 golden test +``` + +## 4. 数据契约 + +```go +// Input 外部直接传入的生成请求参数。 +type Input struct { + Shots []domain.Shot // 镜头脚本(中文键或英文键均可,调用方已归一为 domain.Shot) + TotalDuration int // 目标总时长(秒),<=0 按镜头时间码累加兜底,仍<=0 默认 60 + MaxSegmentDur int // 单段最大时长(秒),<=0 默认 15 + MinSegmentDur int // 单段最小时长(秒),<=0 默认 5 + Refs Refs // 参考素材(角色/场景/道具/产品,具名) + Seed int64 // 随机种子基数,各段 = Seed + 段序号 + NegativePrompt string + ModelName string // 写入请求体 body.model + Schema map[string]any // 视频模型 schema(可选),nil 用默认通用结构 + MaxRefs int // 参考素材上限,<=0 默认 5 + MaxPromptChars int // prompt 截断长度(rune),<=0 不截断 + TokenConfig TokenConfig // 实体名替换 token 的生成配置 +} + +// TokenConfig token 前缀策略:可配置,默认全用 characterN(video-factory 兼容)。 +type TokenConfig struct { + // 是否按类别区分前缀。false=一律 characterN(推荐,模型只按 reference_urls 顺序识别); + // true=character/scene/prop/product 分别编号。 + ByCategory bool + // 自定义前缀模板,如 "char%d"。为空时按 ByCategory 决定默认行为。 + Template string +} + +// Refs 参考素材,与 domain/plan 的 Refs 概念一致(自包含定义,不依赖 plan)。 +type Refs struct { + Characters []RefItem `json:"characters,omitempty"` + Scenes []RefItem `json:"scenes,omitempty"` + Props []RefItem `json:"props,omitempty"` + Products []RefItem `json:"products,omitempty"` +} + +type RefItem struct { + Name string `json:"name"` + URL string `json:"url"` +} + +// Segment 一个视频生成段:时间轴、段内镜头、prompt、参考素材、请求体。 +type Segment struct { + Index int // 段序号(从 0 起) + StartSec int // 段在全局时间轴上的起点(秒) + Duration int // 段时长(秒) + Shots []domain.Shot // 段内镜头(已回写对齐,不跨段) + Prompt string // 实体名→token 替换后的 prompt 文本 + Labels map[string]string // 实体名 → token(character1...) + RefURLs []string // 参考素材 URL,顺序与 token 对应 + Seed int64 + Request map[string]any // 完整视频 API 请求体(schema 嵌套) +} + +// Output Run 的产出。 +type Output struct { + Segments []Segment + TotalDuration int // 实际总时长(秒) +} +``` + +## 5. 阶段① 时间线构建(重建 + 回写对齐) + +入口:`BuildTimeline(in Input) (segShots [][]domain.Shot, segDurs []int, err error)` +(`segShots[i]` 为第 i 段的已对齐镜头列表,段数量与 `segDurs` 一致) + +### 5.1 rebuildDurations +丢弃 AI 的 `startTime/endTime`,按镜头文本量重建每段时长: +- 有文字(台词+旁白)的镜头:按语速(默认 4 字/秒,感叹/疑问多→6,低落→3)算最小时长 = `ceil(字数/语速) + 1`,低弹性; +- 纯视觉镜头:以 AI 原始时长意图为基准(`endTime-startTime`),高弹性; +- 盈余按弹性权重分配;超出目标则先压缩视觉镜头、再等比压缩有声镜头、最后截断尾部; +- 不变量:重建后各镜头时长之和 **精确等于** `TotalDuration`。 + +> 算法与 `video/domain/timeline.go` 的 `rebuildTimeline` 语义一致,但在 `pipeline` 内**自包含实现**(不 import plan)。 + +### 5.2 splitOversized +超 `MaxSegmentDur` 的镜头按句末断句(`。!?;\n!?.`)切子镜头,避免"说话说一半";最后一片段 `≤ minSeg` 时减少拆分段数。重排 `index`。 + +### 5.3 calcSegmentDurations +将 `TotalDuration` 拆为多段: +- `numSegments = ceil(Total / MaxSegmentDur)`,`base = Total / num`,前 `Total % num` 段各 `+1`; +- 每段 `≥ MinSegmentDur`(不足时从后一向前借位); +- 边界:`MinSegmentDur > MaxSegmentDur` 时收敛为相等。 + +### 5.4 alignToSegments(回写对齐,核心新增) +按段边界把镜头切分并对齐: +1. 以段边界 `segStart`/`segEnd`(秒)为切点; +2. 对每个与段窗口相交的镜头,裁剪到 `[max(shotStart, segStart), min(shotEnd, segEnd))`,时间码写回为段内绝对时间轴上的 `MM:SS`; +3. **跨边界镜头切成两个子镜头**,各留各段,剩余部分进入下一段的时间线; +4. **短残片并入**:切出的子镜头时长 `< 1s` 时并入相邻段(并入前一段末尾),避免产生超短镜头; +5. 不变量:每个镜头只属于一个段;段内镜头时间码落在该段窗口内;所有段首尾相接覆盖 `[0, TotalDuration)`。 + +输出:`segShots [][]domain.Shot`(每段一组已对齐镜头)+ `segDurs`,供阶段②逐段构建 prompt。 + +## 6. 阶段② Prompt 构建(模型语言·文本层) + +入口:`BuildSegmentPrompt(segShots []domain.Shot, refs Refs, tc TokenConfig, maxRefs int) (prompt string, labels map[string]string, refURLs []string)` + +1. **收集实体**:按 演员→场景→道具(→产品,若产品图固定携带)出现顺序收集段内具名实体,查 `refs` 拿 URL,上限 `MaxRefs`; +2. **生成 token**:按 `TokenConfig` 生成 `character1...`(默认,全用同一前缀、按引用顺序编号)或按类别前缀 `character1/scene1/prop1/product1`,或自定义模板; +3. **替换 prompt 文本**:把镜头文本中的实体名替换为 token;**按名字长度降序替换**,避免长名是短名前缀时的部分覆盖;无 URL 的实体保留原名; +4. **截断**:`MaxPromptChars > 0` 且超出时截断并追加 `...`(保留至少 50 字符下限); +5. 返回 `prompt`(替换后)、`labels`(实体名→token)、`refURLs`(顺序与 token 对应)。 + +## 7. 阶段③ 请求体构建(模型语言·API 层) + +入口:`BuildVideoRequest(seg Segment, in Input) map[string]any` + +1. 组装 flat input: + ```go + map[string]any{ + "prompt": seg.Prompt, + "seed": seg.Seed, + // 以下有值才写 + "negative_prompt": in.NegativePrompt, + "duration": seg.Duration, + "media": buildMedia(seg.RefURLs, mediaTypeValue), // 参考图 media 数组 + "reference_urls": seg.RefURLs, + } + ``` +2. **schema 嵌套**:仿 video-factory `BuildSchemaRequest`——遍历 schema,无 `type` 键的节点视为分组递归进入,有 `type` 的字段从 flat input 取值、缺失填 `default`、可校验 required/type;`in.Schema == nil` 时用默认通用结构; +3. `body["model"] = in.ModelName`; +4. media 组装:参考图 URL 包装为 media 项,type 值取 `reference_image`(schema_mapping 可配置,复用路径解析思路);媒体在请求中保持 URL 字符串(base64 转码属 I/O,不在本包做,由调用方处理)。 + +### 默认通用 schema(火山风格) +```json +{ + "body": { + "input": { + "prompt": { "type": "string", "required": true }, + "seed": { "type": "integer", "default": -1 }, + "negative_prompt": { "type": "string" }, + "duration": { "type": "integer" }, + "media": { "type": "array" }, + "reference_urls": { "type": "array" } + }, + "parameters": { + "model": { "type": "string" } + } + } +} +``` + +## 8. 入口 + +```go +// Run 编排①→②→③,纯函数,无 I/O。 +func Run(in Input) (*Output, error) +``` + +错误处理: +- `Shots` 为空 → 返回错误(不产出空请求); +- `TotalDuration <= 0` 且镜头无法累加时长 → 兜底 60; +- schema 字段校验失败 → 返回明确错误。 + +## 9. 测试计划 + +| 用例 | 断言 | +|------|------| +| 时间线重建恒等 | 重建后镜头时长之和 == TotalDuration | +| 跨边界切分回写 | 段边界处的镜头被切成子镜头,各镜头时间码落在所属段窗口内,不跨段 | +| 短残片并入 | 切出的 `<1s` 子镜头并入相邻段 | +| token 替换 | 实体名替换为 character1...;长名优先替换;无 URL 实体保留原名 | +| token 可配置 | ByCategory=true 时按类别前缀;Template 生效 | +| schema 嵌套/默认值 | flat input 正确嵌套进 `{body:{input,parameters}}`,缺失字段填 default | +| 用户样本 golden test | 用 2026-08-11 实测的 3 镜头中文样本(时间码 00:00-00:07 等)跑通 Run,断言段数量、prompt 含 characterN、请求体结构 | + +## 10. 与现有链路的关系(本阶段不实现) + +- 新包完全独立,不接 workflow; +- 将来接入:写薄适配层把 `Segment.Request` 交给模型网关 `modelCall`(对应 `video/plan` 的 `SegmentParamsToMap` 语义),媒体 URL 转 base64 在适配层做; +- 现有 `split_shots`/`generate_segments_serial` 保持不动,两者可并存。 + +## 11. 评审记录 + +- 2026-08-11:方案 B(完全独立从零写)获用户确认;token 前缀=可配置(默认 characterN);跨边界=切分+短残片并入;目标 API=通用抽象;挂载层=独立纯函数包;输入=外部直接传 shots。 \ No newline at end of file