# Edit Segment(出片后改某段)

改**已出片**视频的某一句:重生那段音频 → 重拼整条 → 整条重渲染。

> ⚠️ **沙箱 bash 须知**(同 [generate.md §0](generate.md)):agent-runtime 的 bash 是安全沙箱,**软拒绝** `$( )` 命令替换、`for`/`while` 循环、命令内换行、`;`/`&&`/`||` 串多条、`> file` 重定向、`| jq`/`| sed`/`| grep` 管道、`nohup &`。所以本文档的所有"取值 / 判断 / 写文件"都靠 **LLM tool_use**(Read / Write / Edit tool + 单条命令读 JSON 输出),**不靠 shell 拼**。下面 bash 块都是**单条命令**;凡需要变量 / 文件内容,LLM 先用 Read tool 读、把值**内联**进命令,不存 `$VAR`。

> **音频先行下的成本模型**(跟旧版不同,务必理解):
> - 改音频(重生那段 `seg-NN.wav`)= **免费 / 极廉**(voice preview 不烧视频额度)。
> - 但成片是**单条连续渲染**的整条视频,**没法只换一段画面**——改任意一句都要**整条重渲一次**(烧一次视频额度)。
> - 所以**绝大多数修改应该在出片前的音频试听阶段**([generate.md §6.5](generate.md))就改掉(那时改音频免费、还没渲染)。走到 edit.md 说明**成片后才发现要改**——能做,但要重渲一次,提醒用户。

## 触发

用户说类似:
- "把刚才那条视频的第 2 段改成 X"
- "重做第 2 段" / "seg-3 重新生成"
- "刚才那条改一句话"

不在范围(走别的流程):
- "换 avatar"(换分身)→ 等于新视频,走 [generate.md](generate.md)
- "改总文本"(全部重写)→ 反问"要全新生成还是只改某句?"
- "video 比例 / 时长 / 字幕等元参数"→ trained avatar 不支持,反问

## 流程

### 1. 找 video-id

- 用户说"刚才那条" / "刚做的" / 不指定 → `ls -t ~/digital-human/videos/ | head -1` 取最新
- 用户给了 video-id(例 `20260515-1430`)→ 直接定位
- 用户给文本片段或主题 → 遍历 `videos/*/meta.md` "总文本"段模糊匹配
- 0 / 多个匹配 → 反问消歧

如果 video 是 SPEC 落地前(无 workspace 目录)生成的 → 反问 "这条视频没有 workspace 记录,只能重新生成整条。要走 [generate.md](generate.md) 全新创建吗?"

### 1.5 State check(找到 video-id 后必走)

**Step 0: 旧流程 / 无分段视频检测(必须在 state 路由之前跑)**

v2(音频先行)跟 v1(逐段视频 + stitch)**不兼容**。prod 上可能有 v1 时期生成的视频(#118 的卡顿就是 v1 在用)。镜像升到 2.0.0 后,这些旧视频若流进新的音频改段路径会**静默出错**:`gen voice concat` 只扫 `seg-*.{wav,mp3}`,旧的 `seg-*.mp4` 段被排除 → 只剩重生那一段 → 渲出**只含被改段**的残片。所以先挡。

**两条单命令查 + LLM 判断**(别写 `if`/`grep -q`/`$()`,沙箱拦):

```bash
ls ~/digital-human/videos/<VIDEO_ID>/segments/
```

再 Read `~/digital-human/videos/<VIDEO_ID>/meta.md` 看"当前版本"字段。然后 LLM **自己判断**(不靠 shell 条件):

- **`IS_OLD_FLOW`**(命中任一):`segments/` 里出现 `seg-*.mp4` 段文件,**或** meta.md "当前版本"是旧字面量 `(generating)` / `(review pending)`。
- **`NO_SEGMENTS`**:没有 `segments/` 目录,或目录里没有任何 `seg-*.txt`(公共形象单条渲染不产分段)。

| 命中 | 行为(user-facing,不报 §-编号) |
|---|---|
| `IS_OLD_FLOW` | 反问:"这条是旧流程(逐段视频)生成的,改某句得整条重新生成——新流程是音频驱动单条渲染,跟旧的逐段视频不兼容。要用 `<meta 里的分身>` + 原文重新生成吗?" 用户同意 → 走 [generate.md](generate.md)(复用 meta.md "总文本",不再问 text)。**绝不**让它流进下面的 `final-vN.mp4` 改段分支。 |
| `NO_SEGMENTS`(且非 old-flow) | 反问:"这条是公共形象单条生成的,没有分段,改内容要整条重新生成,要吗?" 同意 → [generate.md](generate.md) 公共分支。 |

两者都不命中 → 继续下面 state 路由。

Read `meta.md` 取"当前版本"字段,**按 state machine 分支**(state 即路由 source-of-truth,不靠用户 phrasing 猜):

| "当前版本" 值 | 行为 |
|---|---|
| `final-vN.mp4` | ✅ 已出片,走本文档 §2-§8 主流程(重渲产 final-v<N+1>.mp4) |
| `(audio-review pending)` | ⏳ **跳出 edit.md,跳转 [generate.md §6.5 checkpoint](generate.md)** —— 还没出片,用户说"改 seg-N"是 [§6.6 出片前改音频](generate.md)(免费、不渲染) |
| `(audio generating)` | 🚧 音频段还没生完。**反问**:"这条还在生成音频,要先把剩余段生完吗?(生成中不稳定,没法改)" 用户同意 → 跳 [generate.md §6.8](generate.md) 续生。**话术不出现内部 §-编号** |
| `(rendering)` | 🎬 视频正在渲染。反问"这条正在出片,等渲染完再改好吗?"——渲染中改段没意义 |

**为什么硬分支**:同一句"改 seg-N",在不同 state 下正确动作完全不同。出片前(audio-review pending)改音频免费、不渲染;出片后(final-vN)改要整条重渲。不 state-aware 会乱花额度或产出不存在的版本引用。

### 2. 读现状

用 **Read tool** 读 `~/digital-human/videos/<VIDEO_ID>/meta.md` 和 `~/digital-human/videos/<VIDEO_ID>/history.md`(别 `cat`),再单条 `ls` 看分段:

```bash
ls ~/digital-human/videos/<VIDEO_ID>/segments/
```

LLM 给用户回顾:
> "找到 `<VIDEO_ID>`(分身: 张医生,4 句,当前 final-v2.mp4)。想改哪一句?现有:
> - seg-01: '大家好,我是张医生。'
> - seg-02: '今天介绍一款 ...'
> - seg-03: '它能帮你 ...'
> - seg-04: '谢谢观看。'"

### 3. 判断改的是什么

| 改的是 | 处理 |
|---|---|
| 单段 text | 走 §4(主流程) |
| 多段 text | 逐段确认 + 逐段走 §4 的重生音频,最后**只重渲一次**(§6 合并重渲) |
| 换 avatar / voice | 反问 "这等于新视频,本段流程不支持。要走 [generate.md](generate.md) 用 <新分身> 全新生成吗?" |
| 段间顺序 | 反问 "重排需要重拼音频 + 重渲,本 skill v1 不支持。要手动 mv segments/* 改名后我帮你重拼重渲吗?" |
| 段时长 / 字幕 | trained avatar 不支持调时长 / 字幕参数,reply 不支持 |

### 4. text 改动(主流程)

获取新 text + **二次确认带阈值**(决策表 #14):

```python
# 伪代码
old_text = read("segments/seg-<N>.txt")
new_text = <用户给的新 text>
edit_ratio = levenshtein(old_text, new_text) / max(len(old_text), len(new_text))

if edit_ratio > 0.3:
    # 大改 → 反问确认(注意:这里要重渲整条视频,会烧一次额度)
    ask_user(f"确认改 seg-<N>?\n  旧: {old_text}\n  新: {new_text}\n  (改音频免费,但成片要整条重渲一次)")
    if not confirmed:
        return
# else (≤30%): typo/标点微调,静默继续
```

30% 阈值依据:错别字 / 标点 < 5% 静默;半句话 / 换语序 20-40% 触发确认;整段重写 > 50% 肯定确认。无 levenshtein 工具时用粗估 fallback。

### 5. 覆盖 segments/seg-NN.txt + 重生 seg-NN.wav(只重生音频,免费)

设改的是第 N 段(2 位零填充,例 `02`)。三步,全走 tool_use / 单条命令:

1. **覆盖 text**:用 **Write tool** 把新文本写进 `~/digital-human/videos/<VIDEO_ID>/segments/seg-02.txt`(别 `echo >`)。
2. **反查 voice_id**(meta.md 不存 UUID):Read `meta.md` 自己读出 external_id(形如 `dh-xxxxxxxx`,或老格式 `external_id: ...` 行),**不用 grep/sed**。再单条:
   ```bash
   gen avatar profile dh-xxxxxxxx
   ```
   从它 stdout 的 JSON 里读 `heygen_voice_id`(出片要用的 `heygen_avatar_look_ids[0]` 也一并记下,§6 要),**别 `$()`/`jq` 截**。
3. **只重生这一段音频**(免费 / 极廉,不烧视频额度;新文本**内联**进 `--text`,你手里就有):
   ```bash
   gen voice preview --voice <heygen_voice_id> --text "<新 text>" --output ~/digital-human/videos/<VIDEO_ID>/segments/seg-02.wav
   ```

**其他段音频不动**。多段改:对每个改的段各发**一条** `gen voice preview`(LLM 并行多条,不写 `for`/`nohup`),然后 §6 **只重渲一次**。

> 可选:让用户先**听**新的 seg-N 音频确认,再 §6 重渲(省得渲完又不满意)。强烈推荐多段改 / 大改时先听。

### 6. 重拼音频 → 整条重渲 → final-v<下一版本>.mp4

**版本号计算 + 上限清理**(LLM 看 `ls` 输出自己数,别 `sed`/`sort`/`wc`/`$(())`):

```bash
ls ~/digital-human/videos/<VIDEO_ID>/final-v*.mp4
```

- LLM 从输出里找最大版本号 `LAST`(例已有 final-v1/v2 → LAST=2),下一版 `NEXT = LAST + 1`(=3)。
- **决策表 #12 上限 10**:输出里 final-v*.mp4 已有 **≥ 10 个** → 删最老的那个(单条命令、路径写全):
  ```bash
  rm ~/digital-human/videos/<VIDEO_ID>/final-v1.mp4
  ```

**重拼音频 + 单条重渲**(两条单命令,值内联):

```bash
gen voice concat --workspace-dir ~/digital-human/videos/<VIDEO_ID>/ --out ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3
```

```bash
gen avatar video --avatar <heygen_avatar_look_ids[0]> --audio ~/digital-human/videos/<VIDEO_ID>/full-audio.mp3 -o ~/digital-human/videos/<VIDEO_ID>/final-v<NEXT>.mp4
```

- `gen voice concat` 重拼整条音频(各段已含新的 seg-NN.wav,自动扫 `segments/seg-*` 排序拼接)。**🔴 严禁手搓 `ffmpeg -f concat`**——即使撞 soft-deny 也绝不回退(同 [generate.md §7](generate.md))。
- `<heygen_avatar_look_ids[0]>` 用 §5 第 2 步 `gen avatar profile` 输出里读到的值内联;`<NEXT>` 用上面算出的下一版本号。

> 没有 `gen video stitch` 那一步了——音频先行下成片是单条渲染,不是拼接多段 mp4。

**失败处理**:`gen avatar video --audio` 返回 `音频驱动 doctor-video 暂未开放` → 后端音频驱动未启用(灰度状态,不是用户错误)→ 告知"稍后可用",不重试。其他失败看 `error_message`,音频已拼好可直接重跑渲染。

### 7. 更新 meta.md + history.md

都用 **Edit tool** 改(别 `echo >>` / `cat <<EOF`,沙箱拦;时间用你 LLM 知道的当前时间内联,别 `$(date)`)。

**meta.md**:表头"当前版本"→ `final-v<NEXT>.mp4`;Segments 表第 N 行"最后更新"→ `<当前时间> (v<NEXT>)`。

**history.md** 末尾用 Edit 追加一行(保留旧记录):
```markdown
- **<当前时间>** [v<NEXT>] 改了 seg-<N>: "<旧 text>" → "<新 text>" (<编辑距离比例 X%>,重渲整条)
```

### 8. 报告

> "✅ 视频更新完成: ~/digital-human/videos/<VIDEO_ID>/final-v<NEXT>.mp4
> 改动:seg-<N> 从 '<旧>' 改成 '<新>',重生了该段音频 + 整条重渲一次。
> 历史版本 final-v1 ~ final-v<LAST> 仍在,想回退直接用旧版本。"

如果触发了 final-vN.mp4 删最老逻辑(数量 ≥ 10),加一句"📦 历史版本已满 10,自动清理了最早的 final-v<被删版本>.mp4"。

---

## Edge cases

### 1. 用户说"改第 N 段"但 N 不存在
例:只有 4 段,用户说"改第 7 段"。反问:"这条只有 4 句(seg-01 ~ seg-04),没有第 7 句。要改哪一句?"

### 2. final-v* 数量已达上限 10 + 用户改段
按 §6 自动删最老 final-v1.mp4,保留 latest + 9 个历史。**不问用户**,在 §8 报告里告知。

### 3. 用户改的 text 跟旧 text 完全一样(edit_ratio = 0)
反问:"新旧 text 一样,确认要重渲吗?(音频会重生,韵律可能有细微差异,但要烧一次重渲额度)" 默认推荐"不改"。

### 4. 用户说"改最后一段"/"改第一段"(相对引用)
单条 `ls ~/digital-human/videos/<VIDEO_ID>/segments/` 看分段,LLM **数** `seg-*.txt` 的个数得到最后一段号(例 4 个 → 最后是 `seg-04`),"第一段"= `seg-01`。别用 `$(ls ... | wc -l)` 拼。

### 5. 用户改完后说"算了,用回旧版本"
- final-v<NEXT>.mp4 留着(磁盘占用接受)
- 告诉用户旧版本 `~/digital-human/videos/<VIDEO_ID>/final-v<LAST>.mp4` 还在
- 不做"标记 active 版本"功能(final-vN 并存)

---

## Common Mistakes

| ❌ Mistake | ✅ 正确做法 | 为什么 |
|---|---|---|
| 出片后才让用户发现要改 | 引导用户在出片前 [音频试听阶段](generate.md) 改(免费、不渲染) | edit.md 改一句要整条重渲烧额度;音频阶段改免费 |
| 改一句重生所有段音频 | 仅重生改的那段 `seg-NN.wav`,其他不动 | 其他段音频不变,重拼时复用 |
| 多段改时每段都重渲一次 | 逐段重生音频,最后**只重渲一次**整条 | 视频是单条渲染,改 N 段也只渲一次 |
| 用 `gen video stitch` / `gen avatar video --text` 出片 | 用 `gen voice concat` + `gen avatar video --audio` | 音频先行架构下成片是单条音频驱动渲染 |
| 改段时静默不二次确认 | edit_ratio > 30% 反问(且提示要重渲);≤30% 静默 | 重渲烧额度不可逆 |
| final 覆盖旧版本 | final-vN.mp4 并存,最多 10 个 | 用户对比 / 回退 |
| 反查 voice/avatar 从 meta.md 读 | 永远 `gen avatar profile <external_id>` 反查 | meta.md 不存 UUID(UX 原则) |
| 改段时把 video-id 改成新的 | 同一条视频改段复用原 video-id | video-id 是用户对这条视频的锚 |
| `--audio` 报"暂未开放"当 bug 反复重试 | 后端灰度状态,告知稍后可用 | 音频驱动有 feature flag |
