@optima-chat/optima-agent 0.9.49 → 0.9.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/.claude/skills/doctor-avatar/SKILL.md +281 -0
  2. package/.claude/skills/video-edit/SKILL.md +53 -0
  3. package/dist/bin/banner-tool.d.ts +3 -0
  4. package/dist/bin/banner-tool.d.ts.map +1 -0
  5. package/dist/bin/banner-tool.js +139 -0
  6. package/dist/bin/banner-tool.js.map +1 -0
  7. package/dist/bin/bi-cli.js +0 -0
  8. package/dist/bin/browser-cli.js +0 -0
  9. package/dist/bin/channels.js +0 -0
  10. package/dist/bin/commerce.js +0 -0
  11. package/dist/bin/gen.js +0 -0
  12. package/dist/bin/google-ads.js +0 -0
  13. package/dist/bin/kb-skills.js +0 -0
  14. package/dist/bin/logistics.js +0 -0
  15. package/dist/bin/optima.js +0 -0
  16. package/dist/bin/scout.js +0 -0
  17. package/dist/bin/sentinel.js +0 -0
  18. package/dist/bin/shopify.js +0 -0
  19. package/package.json +14 -12
  20. package/.claude/settings.local.json +0 -166
  21. package/dist/bin/comfy.d.ts +0 -3
  22. package/dist/bin/comfy.d.ts.map +0 -1
  23. package/dist/bin/comfy.js +0 -3
  24. package/dist/bin/comfy.js.map +0 -1
  25. package/dist/bin/growth.d.ts +0 -3
  26. package/dist/bin/growth.d.ts.map +0 -1
  27. package/dist/bin/growth.js +0 -3
  28. package/dist/bin/growth.js.map +0 -1
  29. package/dist/src/hooks-loader.d.ts +0 -6
  30. package/dist/src/hooks-loader.d.ts.map +0 -1
  31. package/dist/src/hooks-loader.js +0 -215
  32. package/dist/src/hooks-loader.js.map +0 -1
  33. package/dist/src/ui/App.d.ts +0 -6
  34. package/dist/src/ui/App.d.ts.map +0 -1
  35. package/dist/src/ui/App.js +0 -164
  36. package/dist/src/ui/App.js.map +0 -1
  37. package/dist/src/ui/components/Composer.d.ts +0 -10
  38. package/dist/src/ui/components/Composer.d.ts.map +0 -1
  39. package/dist/src/ui/components/Composer.js +0 -13
  40. package/dist/src/ui/components/Composer.js.map +0 -1
  41. package/dist/src/ui/components/Header.d.ts +0 -7
  42. package/dist/src/ui/components/Header.d.ts.map +0 -1
  43. package/dist/src/ui/components/Header.js +0 -7
  44. package/dist/src/ui/components/Header.js.map +0 -1
  45. package/dist/src/ui/components/Message.d.ts +0 -12
  46. package/dist/src/ui/components/Message.d.ts.map +0 -1
  47. package/dist/src/ui/components/Message.js +0 -21
  48. package/dist/src/ui/components/Message.js.map +0 -1
  49. package/dist/src/ui/components/MessageList.d.ts +0 -9
  50. package/dist/src/ui/components/MessageList.d.ts.map +0 -1
  51. package/dist/src/ui/components/MessageList.js +0 -18
  52. package/dist/src/ui/components/MessageList.js.map +0 -1
  53. package/dist/src/ui/components/Spinner.d.ts +0 -6
  54. package/dist/src/ui/components/Spinner.d.ts.map +0 -1
  55. package/dist/src/ui/components/Spinner.js +0 -7
  56. package/dist/src/ui/components/Spinner.js.map +0 -1
  57. package/dist/src/ui/components/StatusBar.d.ts +0 -11
  58. package/dist/src/ui/components/StatusBar.d.ts.map +0 -1
  59. package/dist/src/ui/components/StatusBar.js +0 -7
  60. package/dist/src/ui/components/StatusBar.js.map +0 -1
  61. package/dist/src/ui/components/index.d.ts +0 -7
  62. package/dist/src/ui/components/index.d.ts.map +0 -1
  63. package/dist/src/ui/components/index.js +0 -7
  64. package/dist/src/ui/components/index.js.map +0 -1
  65. package/dist/src/validation/error-formatter.d.ts +0 -21
  66. package/dist/src/validation/error-formatter.d.ts.map +0 -1
  67. package/dist/src/validation/error-formatter.js +0 -98
  68. package/dist/src/validation/error-formatter.js.map +0 -1
  69. package/dist/src/validation/index.d.ts +0 -10
  70. package/dist/src/validation/index.d.ts.map +0 -1
  71. package/dist/src/validation/index.js +0 -10
  72. package/dist/src/validation/index.js.map +0 -1
  73. package/dist/src/validation/json-validator.d.ts +0 -25
  74. package/dist/src/validation/json-validator.d.ts.map +0 -1
  75. package/dist/src/validation/json-validator.js +0 -173
  76. package/dist/src/validation/json-validator.js.map +0 -1
  77. package/dist/src/validation/schema.d.ts +0 -353
  78. package/dist/src/validation/schema.d.ts.map +0 -1
  79. package/dist/src/validation/schema.js +0 -57
  80. package/dist/src/validation/schema.js.map +0 -1
  81. package/dist/src/validation/suggestions.d.ts +0 -25
  82. package/dist/src/validation/suggestions.d.ts.map +0 -1
  83. package/dist/src/validation/suggestions.js +0 -144
  84. package/dist/src/validation/suggestions.js.map +0 -1
  85. package/dist/src/validation/types.d.ts +0 -40
  86. package/dist/src/validation/types.d.ts.map +0 -1
  87. package/dist/src/validation/types.js +0 -5
  88. package/dist/src/validation/types.js.map +0 -1
  89. package/dist/src/validation/yaml-validator.d.ts +0 -25
  90. package/dist/src/validation/yaml-validator.d.ts.map +0 -1
  91. package/dist/src/validation/yaml-validator.js +0 -177
  92. package/dist/src/validation/yaml-validator.js.map +0 -1
@@ -0,0 +1,281 @@
1
+ ---
2
+ name: doctor-avatar
3
+ description: "训练数字分身——从用户上传的真人视频提取人物形象 + 声音(avatar + voice clone),后续用此分身合成口播视频。触发场景:用户上传了视频(附件 / URL / 文件路径),并表达想要这个人的形象 / 声音 / 数字分身的意图——『要这个人物的形象』『要这个人的声音』『要训练这个人』『做这个人的数字分身』『克隆这个人』『把这个人 IP 化』『做成 AI 老师 / AI 主播 / AI 医生 / AI 客服』。**触发条件二者必备**:有视频 + 有训练分身的明确意图。普通视频生成 / 视频复刻请走 video-gen skill。"
4
+ ---
5
+
6
+ # Doctor Avatar Skill
7
+
8
+ 把用户上传的真人视频训成数字分身(avatar + voice clone),后续可用此分身合成口播视频。**核心价值**:用户给一段真人视频 + "要这个人的形象跟声音",我自动跑通 voice 训练 + avatar 训练 + 验证 + preview,产出可复用的 `voice_id` + `avatar_id`。
9
+
10
+ > **用户向输出原则**:对话 / status / 报告里**不出现具体上游品牌 / 服务名**,统一用"训练服务" / "数字分身训练" / "上游" 等中性词。skill 内部 reasoning 引用(spec / probe / source code)可保留具体名称用于 LLM 思考。
11
+
12
+ ## Global Rules
13
+
14
+ 优先级高于任何 pipeline 步骤。违反会导致泄漏、超支或数据错误。
15
+
16
+ 1. **触发严格**:必须有视频 + 训练意图二者。**只有视频没意图 → 不触发**(可能用户只想让你看视频)。**只有意图没视频 → 反问**"要训练谁的分身,把视频发我"。
17
+ 2. **背景必须显式决策**:训练系统会把视频背景 baked 进 avatar look,**video gen 时无法替换**。`--clean-bg` 或 `--accept-baked-bg` 二选一,不能 silent 默认。
18
+ 3. **每次任务必须有合法 external_id**:这是分身的 unique key,后续查 / 用都靠它。LLM 主动 normalize 用户输入(见 §1)。
19
+ 4. **Anti-fabrication**:命令 / flag / 端点必须用本 skill 列出的精确版本。CLI sub-command / API 端点不允许凭印象拼。
20
+ 5. **限制硬约束**(来自 spec §13):
21
+ - doctor-video text ≤ 50 字 / 输出 ≤ 10 秒(spec §13.3 实测)
22
+ - 视频 ≥ 30 秒(voice 训练用,Phase 1 probe 经验值),建议 2-5 分钟(avatar 质量,spec §13.5)
23
+ - 单声道音频质量好(噪声 / 多人混杂会 fail)
24
+ 6. **用户向不暴露上游品牌**:见上面"用户向输出原则"。
25
+
26
+ ## Workflow
27
+
28
+ 按顺序执行,每步必走。
29
+
30
+ ### 1. 收集 + normalize external_id
31
+
32
+ `external_id` 是医生 / IP 主体的 unique key,后续 `gen doctor profile <external_id>` 查、`gen doctor video --avatar <avatar_id> --voice <voice_id>` 合成视频时绑定。
33
+
34
+ **主动问用户**:
35
+ > "训完后用什么标识找到这个分身?给一个英文 / 拼音 slug(例 `dr-wang`、`kol-li-001`、`teacher-zhang`)。"
36
+
37
+ **Normalize 规则**(用户给的不规范时由 LLM 处理,不让 backend 兜底):
38
+
39
+ | 用户给 | LLM 处理 |
40
+ |---|---|
41
+ | `Dr Wang ` | lowercase + 空格转 `-` → `dr-wang` |
42
+ | `dr.wang!` | 移除 `[^a-z0-9_-]` → `drwang` |
43
+ | `张医生`(纯中文) | **拒绝**,回:"slug 需要 ASCII(英文/拼音/数字/`-`),例 `dr-zhang`,你想用什么?" |
44
+ | 长度 > 128 | 截断 + 警告 |
45
+ | 最终 | `[a-z0-9_-]+` 长度 ≤ 128 |
46
+
47
+ **不要**:
48
+ - ❌ 自动从中文姓名转拼音(脆弱,工具不一定有 pinyin lib)
49
+ - ❌ 用 timestamp 编(用户后续记不住)
50
+ - ❌ 凭印象 invent
51
+
52
+ **先查重**:
53
+ ```bash
54
+ gen doctor profile <slug> # 已存在 → 跳到 §Edge cases 1
55
+ ```
56
+
57
+ ### 2. 视频规格检查
58
+
59
+ ```bash
60
+ ffprobe -v quiet -print_format json -show_format -show_streams <video-path>
61
+ ```
62
+
63
+ 记录:
64
+ - `duration` 总时长
65
+ - `width` × `height` 分辨率
66
+ - `audio` 流是否存在 + sample_rate / channels
67
+
68
+ 判定(阈值来自 spec §13.5 + Phase 1 probe 经验):
69
+
70
+ | 项 | 阈值 | 不达标动作 |
71
+ |---|---|---|
72
+ | 视频时长 | < 30s | 中止;要求重传 ≥ 30s 视频 |
73
+ | 视频时长 | 30-119s | 警告"voice OK,但 avatar 质量可能受影响,推荐 ≥ 2min" |
74
+ | 分辨率 | < 720p | 警告但继续 |
75
+ | 音频流 | 缺失 | 中止;视频里没声音怎么训 voice |
76
+
77
+ **不前置检测多人 vs 单人** — ffprobe 拿不到 speaker count(`channels=1` 不等于单人)。如果实际多人混杂,等 voice clone fail 时按 `error_message` 反馈用户重传。
78
+
79
+ ### 3. 背景判断 + 决策
80
+
81
+ spec §13.4 关键约束:训练系统会把整段视频画面(包含背景)整体作为 avatar look 内容,video gen 时**无法用 `background` 参数替换**。
82
+
83
+ **直接问用户三选一**(不要抽帧 + Read 图判断 — 视觉判断不稳定且贵):
84
+
85
+ > "训练前确认:这段视频的背景是?
86
+ > A. **白墙 / 棚拍 / 绿幕 / 单色背景**(干净)
87
+ > B. **标准固定场景**(诊室 / 办公桌 / 医院前台 — 这个背景将永远 baked 进分身,后续无法换)
88
+ > C. **户外 / 商场 / 多人 / 杂乱**(脏)"
89
+
90
+ | 用户选 | 决策 |
91
+ |---|---|
92
+ | A | `--clean-bg` |
93
+ | B | 二次确认:"分身的所有未来视频背景都会是这个场景,可接受吗?" → 接受用 `--accept-baked-bg`;不接受 → 让用户重拍 A 类背景 |
94
+ | C | 强烈劝退,推荐重拍干净背景。如用户坚持产出 → `--accept-baked-bg` + 明确告诉产出视频背景永远是录制时场景 |
95
+
96
+ **禁止**:
97
+ - ❌ silent 默认 `--clean-bg`(错判脏背景为干净 → 产出脏数据)
98
+ - ❌ 自己抽帧用图像识别判断(视觉判断不稳定 + 浪费 token + 用户答的更准)
99
+
100
+ ### 4. 拆音频(如需)
101
+
102
+ `gen doctor onboard` 接受 `<audio-file> <video-file>` 两个文件参数。如用户只给一个视频文件,先 ffmpeg 拆出音频:
103
+
104
+ ```bash
105
+ AUDIO_TMP=$(mktemp -t doctor-audio-XXXXXX.wav)
106
+ ffmpeg -i <video-path> -vn -acodec pcm_s16le -ar 44100 -ac 1 -t 90 "$AUDIO_TMP"
107
+ ```
108
+
109
+ (取前 90 秒单声道 WAV,够 voice clone)
110
+
111
+ > 用 `mktemp` 而非硬编码 `/tmp/audio.wav`,防止多 doctor 并发训时 race 覆盖。
112
+
113
+ ### 5. 触发训练(onboard facade)
114
+
115
+ ```bash
116
+ gen doctor onboard "$AUDIO_TMP" <video-path> \
117
+ --external-id <id> \
118
+ --clean-bg # 或 --accept-baked-bg(二选一,不能省)
119
+ ```
120
+
121
+ 返回 JSON:
122
+ ```json
123
+ { "external_id": "dr-zhang", "voice_task_id": "<uuid>", "avatar_task_id": "<uuid>", "status": "pending" }
124
+ ```
125
+
126
+ ### 6. Poll 直到完成
127
+
128
+ ```bash
129
+ # 30 min 上限(avatar train 慢边界);5s 间隔
130
+ TIMEOUT=1800
131
+ INTERVAL=5
132
+ ELAPSED=0
133
+ while [ $ELAPSED -lt $TIMEOUT ]; do
134
+ V_STATUS=$(gen task get $VOICE_TASK | jq -r '.status')
135
+ A_STATUS=$(gen task get $AVATAR_TASK | jq -r '.status')
136
+ echo "[t+${ELAPSED}s] voice=$V_STATUS avatar=$A_STATUS"
137
+
138
+ V_DONE=false; A_DONE=false
139
+ [[ "$V_STATUS" =~ ^(completed|failed)$ ]] && V_DONE=true
140
+ [[ "$A_STATUS" =~ ^(completed|failed)$ ]] && A_DONE=true
141
+ $V_DONE && $A_DONE && break
142
+
143
+ sleep $INTERVAL
144
+ ELAPSED=$((ELAPSED + INTERVAL))
145
+ done
146
+ ```
147
+
148
+ > **Fallback**:如果 `gen task get` 不可用(CLI 版本不对 / 找不到命令),直接打后端 endpoint(route 永远存在 — backend `registerTaskRoutes`):
149
+ > ```bash
150
+ > curl -s -H "Authorization: Bearer $OPTIMA_TOKEN" "$GENERATION_API/api/task/$TASK_ID" | jq
151
+ > ```
152
+
153
+ 期望耗时:
154
+ - voice clone:30s - 2 min
155
+ - avatar train:5 - 30 min
156
+ - spec §13.2:trained look 在训练完成后还有短期 race(几分钟到 1 小时);**onboard 阶段不阻塞**(race 影响的是后续 doctor-video gen,adapter 已 retry-on-still-processing)
157
+
158
+ 任一 fail → 看 `error_message`:
159
+ - voice clone fail → 大概率音频质量(噪声 / 多人混杂)→ 提醒用户重传清晰音频
160
+ - avatar train fail → 视频问题(时长 / 分辨率 / 内容)→ 给具体 error 让用户判断
161
+
162
+ ### 7. 拉 profile 给用户
163
+
164
+ 两个 task 都 completed 后:
165
+
166
+ ```bash
167
+ gen doctor profile <external_id>
168
+ ```
169
+
170
+ 返回(关键字段;略缩为示意):
171
+ ```json
172
+ {
173
+ "external_id": "dr-zhang",
174
+ "heygen_voice_id": "f7a8ea9f...", // DB 历史命名,内部字段
175
+ "heygen_avatar_group_id": "174fa88e...",
176
+ "heygen_avatar_look_ids": ["04182555..."],
177
+ "voice_status": "complete",
178
+ "avatar_status": "completed",
179
+ "preview_image_url": "https://...preview.jpg"
180
+ }
181
+ ```
182
+
183
+ > DB 字段名 `heygen_*` 是历史命名(spec §7),代码层不动。**LLM 给用户报告时改用中性词**:`voice_id` / `avatar_id` / `preview` 等。
184
+
185
+ 给用户报告(用中性词):
186
+ - ✅ 训练完成
187
+ - preview 图片(从 `preview_image_url` 下载或直接给 URL)
188
+ - `voice_id`(取自 `heygen_voice_id`)+ `avatar_id`(取自 `heygen_avatar_look_ids[0]`)
189
+ - 告诉用户后续可以"用 X 的分身做视频说 Y"
190
+
191
+ > **内部备忘(不主动告诉用户)**:单视频实际训出 4 个 looks 但当前只暴露第一个(spec §13.5 limitation,[v1.1 issue #54](https://github.com/Optima-Chat/optima-gen/issues/54) 跟踪)。用户问起 "为什么只有一种表情" 再说。
192
+
193
+ ### 8. (可选)立即合成 demo
194
+
195
+ 如果用户在原始请求里说了想"看效果 / 做一段试试 / 给我个 demo":
196
+
197
+ ```bash
198
+ DEMO_OUT=$(mktemp -t doctor-demo-XXXXXX.mp4)
199
+ gen doctor video \
200
+ --avatar <avatar_id> \
201
+ --voice <voice_id> \
202
+ --text "用户给的文字 ≤ 50 字,如果用户没给就用 '大家好,我是 X'" \
203
+ -o "$DEMO_OUT"
204
+ ```
205
+
206
+ 合成完给用户看 mp4(几十秒到 2-3 分钟)。
207
+
208
+ 如返回 "still processing" → adapter 自动 retry-on-still-processing(初始 1min,30min 上限);如最终 fail → **降级 demo**(用通用公共 avatar + trained voice 合成,具体公共 avatar ID 见 video-gen skill 的"数字人口播"段)。降级时告诉用户:
209
+ > "分身的视觉部分还在最后定型(几十分钟内会好),先用通用形象 + 你的声音演示。**这条 demo 仅证明 voice 训成,不展示真分身效果** — 几小时后用 `gen doctor video` 重试就能用真分身了。"
210
+
211
+ > 不在 skill 内硬编码具体公共 avatar ID — backend / video-gen skill 维护清单,这里只走"降级 demo"概念。
212
+
213
+ ## Edge cases
214
+
215
+ ### 1. external_id 已存在
216
+
217
+ `gen doctor profile <slug>` 返回非 null → 该分身已训过。**不要 silent 重训**(会在训练后台占新配额)。
218
+
219
+ | 状态 | 决策 |
220
+ |---|---|
221
+ | voice + avatar 都 `complete` | 问用户:"X 已训过,要用现有还是重训?(重训会占新配额且原 IDs 失效)"。默认推荐用现有 |
222
+ | 有一项 `failed` | 问用户:"X 的 [voice / avatar] 上次失败,要补训吗?" 同意 → 单独调 `gen voice clone` / `gen avatar train`(不走 onboard,因 onboard 总是触发两个) |
223
+ | 都 `pending` / `training` | 提示用户上次训练还在跑,跑 §6 polling loop 等完 |
224
+
225
+ ### 2. 用户上传多个视频
226
+
227
+ 主动列出最近上传的视频文件(文件名 + 时长),**让用户选哪一个用于训练**。不要假设最后一个 = 训练用。
228
+
229
+ ### 3. 用户中途改主意
230
+
231
+ - 想换视频 / 换 external_id → 如果 onboard 已 submit,旧 task 让它跑完(训练资源已扣,不阻塞);用户拿新参数重新走 §1-§6
232
+ - 想取消 → `gen task cancel <task_id>` 各自 cancel(尽力而为,上游可能已 in-flight 不可中止)
233
+ - 取消 fallback:`curl -X POST -H "Authorization: Bearer $OPTIMA_TOKEN" "$GENERATION_API/api/task/$TASK_ID/cancel"`
234
+
235
+ ### 4. 用户没给视频但说"训练 X 医生"
236
+
237
+ 反问:"X 医生的视频附件没看到,把视频发我(2-5 min,正面拍摄,干净背景)。"
238
+ **不要** 凭印象搜索 X 医生的公开视频去训(法律 + 同意书风险)。
239
+
240
+ ### 5. 用户给了视频但意图模糊
241
+
242
+ 视频可能是"复刻这条视频" / "把这条视频翻译" / "训分身"。如果不明确是后者,**不要触发本 skill**。问一句:
243
+ > "你想要 (a) 用这条视频训练 X 的数字分身(以后用分身做新视频),还是 (b) 复刻这条视频做产品宣传 / (c) 把视频翻译成其他语言?"
244
+
245
+ 分别走 doctor-avatar / video-gen / video-gen 视频翻译。
246
+
247
+ ---
248
+
249
+ ## 后续会话(用户已经训练过分身)
250
+
251
+ 如果用户后续说"用 X 医生的分身做个视频说 Y"或类似:
252
+
253
+ 1. `gen doctor profile <external_id>` 或 slug 化的 name 拿 voice_id + avatar_id
254
+ 2. `gen doctor video --avatar <avatar_id> --voice <voice_id> --text Y` 合成
255
+
256
+ 如用户说的不是同一医生(没 train 过),反问"X 医生的分身没训过,要先上传视频训练吗"。
257
+
258
+ ## 限制 + 失败模式(来自 spec §13)
259
+
260
+ | 限制 | 说明 |
261
+ |---|---|
262
+ | text ≤ 50 字 / 输出 ≤ 10 秒 | 长 prompt 在 trained avatar 上挂死,client 端拆段(spec §13.3) |
263
+ | 单视频 4 looks 只用第一个 | 上游服务未暴露 list-looks endpoint(spec §13.5,v1.1 issue #54) |
264
+ | trained look "still processing" 短期 race | adapter 自动 retry,30min 失败降级公共 avatar(spec §13.2) |
265
+ | 背景 baked | video gen `background` 参数无效,训练时必须干净背景(spec §13.4) |
266
+ | consent | API 不强制,产品上自行做合规(医院授权书 / 法律协议)(spec §13.6) |
267
+
268
+ ## 不在范围
269
+
270
+ - ❌ Multi-doctor 一次训(一个 external_id 对应一次 onboard)
271
+ - ❌ Background remove / matting(让用户重拍干净背景或自己 preprocess)
272
+ - ❌ Long video gen(text > 50 字让用户拆段)
273
+ - ❌ License / consent 流程(产品层做,不在 skill 内)
274
+
275
+ ## 相关 / 参考
276
+
277
+ - **Backend**:`optima-gen/packages/generation`(adapters / routes / db)
278
+ - **CLI**:`@optima-chat/gen-cli ≥ 1.1.1`(`gen voice / avatar / doctor` + 现有 `gen task get/cancel/list` 系列命令)
279
+ - **Spec**:[optima-doctor-avatar §13](https://github.com/Optima-Chat/optima-doctor-avatar/blob/main/docs/superpowers/specs/2026-05-07-doctor-avatar-poc-design.md#13-phase-1-probe-findings-2026-05-08)
280
+ - **v1.1 follow-up**:[optima-gen issue #54](https://github.com/Optima-Chat/optima-gen/issues/54)
281
+ - **相关 skill**:`video-gen`(普通视频生成 / 复刻 / PDP / 数字人口播降级 demo 时引用),不要混
@@ -138,6 +138,59 @@ review 通过后再烧字幕:
138
138
  video-edit subtitle <video>
139
139
  ```
140
140
 
141
+ ### Banner(顶部持续标语横幅,可选)
142
+
143
+ 复刻开拍 app 的"标语横幅"效果——视频顶部持续显示 4-12 字大字标题(深酒红 96pt + 上下 ━ 装饰线)。
144
+
145
+ `subtitle` 完成后做以下**4 步**:
146
+
147
+ #### Step 1:判断是否适合加 banner
148
+
149
+ 抽视频首帧,目测主播脸位置:
150
+
151
+ ```bash
152
+ ffmpeg -y -ss 0 -i <video> -vframes 1 -loglevel error <video>.work/_first_frame.jpg
153
+ # 然后用视觉能力看 _first_frame.jpg
154
+ ```
155
+
156
+ - **脸顶在画面下半 2/3 之内**(脸顶 y > 30% 画面高度)→ 适合加 banner,进 Step 2
157
+ - **脸贴顶 / 大特写**(脸顶 y < 30%)→ 不加 banner,告诉用户"原片脸贴顶,加 banner 会挡脸,跳过",**流程结束**
158
+
159
+ #### Step 2:自动提炼 banner 候选
160
+
161
+ 读 `final_script.txt`,挑核心卖点:
162
+ - 找 `**...**` 关键词标记的实体名词
163
+ - 优先选数字 + 主体类(如 `日更100条 + 实操训练营`)
164
+ - 拼 4-12 字短语(**用空格分隔**让 banner-tool 自动换行成两行)
165
+
166
+ 举例(loyally1 视频关键词:`跨境电商 / 单量 / 100条 / 实操训练营 / Temu TK / 矩阵 / 实操资料包`):
167
+ - 候选:`日更100条 实操训练营`(数字+服务名)
168
+ - 备选:`跨境电商 矩阵打法` / `Temu TK 实操营`
169
+
170
+ #### Step 3:询问用户确认
171
+
172
+ 在 chat 里告诉用户:
173
+
174
+ > 建议 banner 写 **"日更100条 实操训练营"**(核心卖点是日更频率 + 训练营服务)。
175
+ > 回 `ok` 用建议,告诉我你想要的标题,或说`不要 banner`。
176
+
177
+ 按用户回应:
178
+ - `ok` / `可以` / `好` / 继续往下说话不提 banner → 用 agent 建议
179
+ - 用户给新 text → 用用户的
180
+ - `不要 banner` / `跳过` → 不加,**流程结束**
181
+
182
+ #### Step 4:注入 banner + 重烧字幕
183
+
184
+ ```bash
185
+ TITLE="<上一步确定的标题>"
186
+ DURATION=$(ffprobe -v quiet -show_format -of default=nk=1:nw=1 -show_entries format=duration <video>_subbed.mp4)
187
+ banner-tool inject <video>.work/subs.ass --text "$TITLE" --duration "$DURATION"
188
+ rm <video>_subbed.mp4
189
+ video-edit subtitle <video> # subs.ass 已存在 → 只重烧不重写
190
+ ```
191
+
192
+ 最终成片 `<video>_subbed.mp4` 顶部出现持续标语横幅。
193
+
141
194
  ### 用户要求调整时
142
195
 
143
196
  用户看完成片说某段不对:
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=banner-tool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner-tool.d.ts","sourceRoot":"","sources":["../../bin/banner-tool.ts"],"names":[],"mappings":""}
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * banner-tool inject <ass-path> --text <文本> --duration <秒> [options]
4
+ *
5
+ * 在 ASS 字幕文件里注入"顶部持续 banner Style + Dialogue"(开拍 app 风格)。
6
+ *
7
+ * 视觉:深酒红 #75170D 96pt 加粗 + 上下 ━ 装饰横线 + Layer 1 在主字幕之上 + 0s 持续到结束。
8
+ * 幂等:重跑 Style 不重复添加,Dialogue 替换。
9
+ */
10
+ import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
11
+ import { tmpdir } from 'os';
12
+ import { join } from 'path';
13
+ const KAIPAI_BANNER_STYLE = 'Style: Banner,Source Han Sans SC,96,&H000D1775,&H000D1775,&H00141414,&H80000000,1,0,0,0,100,100,4,0,1,8,3,8,40,40,30,1';
14
+ function parseArgs(argv) {
15
+ // 全局 --help / -h
16
+ if (argv.includes('--help') || argv.includes('-h')) {
17
+ console.log('Usage: banner-tool inject <ass-path> --text <文本> --duration <秒> [--start N] [--style preset] [--dry-run]');
18
+ process.exit(0);
19
+ }
20
+ const command = argv[0];
21
+ if (command !== 'inject') {
22
+ throw new Error(`Unknown command: ${command}. Only 'inject' supported.`);
23
+ }
24
+ const assPath = argv[1];
25
+ if (!assPath || assPath.startsWith('--')) {
26
+ throw new Error('Missing <ass-path> positional argument');
27
+ }
28
+ let text = '';
29
+ let duration = NaN;
30
+ let start = 0;
31
+ let style = 'kaipai';
32
+ let dryRun = false;
33
+ for (let i = 2; i < argv.length; i++) {
34
+ const a = argv[i];
35
+ if (a === '--text')
36
+ text = argv[++i] ?? '';
37
+ else if (a === '--duration')
38
+ duration = parseFloat(argv[++i] ?? '');
39
+ else if (a === '--start')
40
+ start = parseFloat(argv[++i] ?? '');
41
+ else if (a === '--style')
42
+ style = argv[++i] ?? 'kaipai';
43
+ else if (a === '--dry-run')
44
+ dryRun = true;
45
+ else if (a === '--help' || a === '-h') {
46
+ console.log('Usage: banner-tool inject <ass-path> --text <文本> --duration <秒> [--start N] [--style preset] [--dry-run]');
47
+ process.exit(0);
48
+ }
49
+ }
50
+ if (!text)
51
+ throw new Error('Missing --text');
52
+ if (!Number.isFinite(duration) || duration <= 0) {
53
+ throw new Error('Missing or invalid --duration');
54
+ }
55
+ if (start < 0 || start >= duration) {
56
+ throw new Error('--start must be in [0, duration)');
57
+ }
58
+ if (style !== 'kaipai') {
59
+ throw new Error(`Unknown style preset: ${style}. Only 'kaipai' supported.`);
60
+ }
61
+ return { command, assPath, text, duration, start, style, dryRun };
62
+ }
63
+ /**
64
+ * 文本换行规则:
65
+ * - ≤ 6 codepoints:单行
66
+ * - 7-14 + 含空格:中间最近空格 → \N
67
+ * - > 14:warn + 截到 14 + ⋯
68
+ */
69
+ function wrapText(text) {
70
+ const chars = [...text];
71
+ if (chars.length > 14) {
72
+ process.stderr.write(`[WARN] text length ${chars.length} > 14, truncating to 14 + ⋯\n`);
73
+ return chars.slice(0, 14).join('') + '⋯';
74
+ }
75
+ if (chars.length <= 6)
76
+ return text;
77
+ const spaceIdx = text.indexOf(' ');
78
+ if (spaceIdx > 0 && spaceIdx < text.length - 1) {
79
+ return text.slice(0, spaceIdx) + '\\N' + text.slice(spaceIdx + 1);
80
+ }
81
+ return text;
82
+ }
83
+ function fmtAssTime(t) {
84
+ const h = Math.floor(t / 3600);
85
+ const m = Math.floor((t % 3600) / 60);
86
+ const s = (t % 60).toFixed(2);
87
+ return `${h}:${m.toString().padStart(2, '0')}:${s.padStart(5, '0')}`;
88
+ }
89
+ function injectBanner(args) {
90
+ if (!existsSync(args.assPath))
91
+ throw new Error(`file not found: ${args.assPath}`);
92
+ let content = readFileSync(args.assPath, 'utf-8');
93
+ if (!content.trim())
94
+ throw new Error(`empty file: ${args.assPath}`);
95
+ if (!content.includes('[V4+ Styles]')) {
96
+ throw new Error('not a valid ASS file: missing [V4+ Styles]');
97
+ }
98
+ if (!content.includes('[Events]')) {
99
+ throw new Error('not a valid ASS file: missing [Events]');
100
+ }
101
+ // 1. Style 幂等添加 — 在 [V4+ Styles] 段最后一个 Style 行后插入
102
+ let styleAdded = false;
103
+ if (!/^Style: Banner,/m.test(content)) {
104
+ content = content.replace(/(\[V4\+ Styles\][\s\S]*?)(\n\n\[Events\])/, `$1\n${KAIPAI_BANNER_STYLE}$2`);
105
+ styleAdded = true;
106
+ }
107
+ // 2. Dialogue 替换或附加
108
+ const wrapped = wrapText(args.text);
109
+ const startTime = fmtAssTime(args.start);
110
+ const endTime = fmtAssTime(args.start + args.duration);
111
+ const newDialogue = `Dialogue: 1,${startTime},${endTime},Banner,,40,40,30,,━━━━━━━━━━━━\\N${wrapped}\\N━━━━━━━━━━━━`;
112
+ if (/^Dialogue: \d+,[^,]+,[^,]+,Banner,/m.test(content)) {
113
+ content = content.replace(/^Dialogue: \d+,[^,]+,[^,]+,Banner,[^\n]+/m, newDialogue);
114
+ }
115
+ else {
116
+ content = content.trimEnd() + '\n' + newDialogue + '\n';
117
+ }
118
+ if (args.dryRun) {
119
+ console.log('Would write to', args.assPath);
120
+ if (styleAdded)
121
+ console.log(' + Style: Banner,...');
122
+ console.log(' + Dialogue:', newDialogue.slice(0, 120));
123
+ return;
124
+ }
125
+ // 原子写
126
+ const tmp = join(tmpdir(), `banner-tool-${Date.now()}-${Math.random().toString(36).slice(2)}.ass`);
127
+ writeFileSync(tmp, content, 'utf-8');
128
+ renameSync(tmp, args.assPath);
129
+ console.log(`[OK] Banner injected: "${args.text}" ${startTime}-${endTime} → ${args.assPath}`);
130
+ }
131
+ try {
132
+ const args = parseArgs(process.argv.slice(2));
133
+ injectBanner(args);
134
+ }
135
+ catch (err) {
136
+ process.stderr.write(`[ERR] ${err.message}\n`);
137
+ process.exit(1);
138
+ }
139
+ //# sourceMappingURL=banner-tool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"banner-tool.js","sourceRoot":"","sources":["../../bin/banner-tool.ts"],"names":[],"mappings":"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"}
File without changes
File without changes
File without changes
File without changes
package/dist/bin/gen.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/dist/bin/scout.js CHANGED
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@optima-chat/optima-agent",
3
- "version": "0.9.49",
3
+ "version": "0.9.50",
4
4
  "description": "基于 Claude Agent SDK 的电商运营 AI 助手",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -17,7 +17,8 @@
17
17
  "logistics": "./dist/bin/logistics.js",
18
18
  "browser-cli": "./dist/bin/browser-cli.js",
19
19
  "kb-skills": "./dist/bin/kb-skills.js",
20
- "channels": "./dist/bin/channels.js"
20
+ "channels": "./dist/bin/channels.js",
21
+ "banner-tool": "./dist/bin/banner-tool.js"
21
22
  },
22
23
  "files": [
23
24
  "dist",
@@ -36,6 +37,17 @@
36
37
  ],
37
38
  "author": "Optima Chat",
38
39
  "license": "MIT",
40
+ "scripts": {
41
+ "build": "npm run sync:kb-skills && tsc",
42
+ "dev": "tsx watch src/index.ts",
43
+ "start": "node dist/src/index.js",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "optima": "tsx bin/optima.ts",
48
+ "prepublishOnly": "npm run build",
49
+ "sync:kb-skills": "tsx bin/sync-kb-skills.ts"
50
+ },
39
51
  "dependencies": {
40
52
  "@anthropic-ai/claude-agent-sdk": "^0.2.63",
41
53
  "@optima-chat/ads-cli": "latest",
@@ -64,15 +76,5 @@
64
76
  },
65
77
  "engines": {
66
78
  "node": ">=18.0.0"
67
- },
68
- "scripts": {
69
- "build": "npm run sync:kb-skills && tsc",
70
- "dev": "tsx watch src/index.ts",
71
- "start": "node dist/src/index.js",
72
- "typecheck": "tsc --noEmit",
73
- "test": "vitest run",
74
- "test:watch": "vitest",
75
- "optima": "tsx bin/optima.ts",
76
- "sync:kb-skills": "tsx bin/sync-kb-skills.ts"
77
79
  }
78
80
  }