@optima-chat/gen-cli 1.0.9 → 1.2.0

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 (40) hide show
  1. package/.claude/skills/gen/SKILL.md +345 -0
  2. package/.claude/skills/multigrid-poster/SKILL.md +194 -0
  3. package/.claude/skills/multigrid-poster/layouts/2x2.json +34 -0
  4. package/.claude/skills/multigrid-poster/layouts/3x3.json +43 -0
  5. package/.claude/skills/multigrid-poster/scripts/compose.py +116 -0
  6. package/.claude/skills/multigrid-poster/scripts/placeholder.png +0 -0
  7. package/.claude/skills/multigrid-poster/shared/fonts/MaShanZheng-Regular.ttf +0 -0
  8. package/.claude/skills/video-edit/SKILL.md +189 -0
  9. package/.claude/skills/video-gen/SKILL.md +718 -0
  10. package/.claude/skills/video-gen/templates/INDEX.md +78 -0
  11. package/.claude/skills/video-gen/templates/before-after-beauty.md +183 -0
  12. package/.claude/skills/video-gen/templates/drama-fmcg.md +183 -0
  13. package/.claude/skills/video-gen/templates/kol-reaction-food.md +193 -0
  14. package/.claude/skills/video-gen/templates/multi-point-apparel.md +185 -0
  15. package/.claude/skills/video-gen/templates/pain-solution-home.md +184 -0
  16. package/.claude/skills/video-gen/templates/pdp-360-showcase.md +189 -0
  17. package/.claude/skills/video-gen/templates/pdp-feature-highlight.md +182 -0
  18. package/.claude/skills/video-gen/templates/scene-digital.md +183 -0
  19. package/dist/commands/avatar.d.ts +9 -0
  20. package/dist/commands/avatar.d.ts.map +1 -0
  21. package/dist/commands/avatar.js +155 -0
  22. package/dist/commands/avatar.js.map +1 -0
  23. package/dist/commands/doctor.d.ts +9 -0
  24. package/dist/commands/doctor.d.ts.map +1 -0
  25. package/dist/commands/doctor.js +293 -0
  26. package/dist/commands/doctor.js.map +1 -0
  27. package/dist/commands/video.d.ts.map +1 -1
  28. package/dist/commands/video.js +21 -25
  29. package/dist/commands/video.js.map +1 -1
  30. package/dist/commands/voice.d.ts +9 -0
  31. package/dist/commands/voice.d.ts.map +1 -0
  32. package/dist/commands/voice.js +123 -0
  33. package/dist/commands/voice.js.map +1 -0
  34. package/dist/index.js +7 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/services/generation-api.d.ts +69 -4
  37. package/dist/services/generation-api.d.ts.map +1 -1
  38. package/dist/services/generation-api.js +114 -8
  39. package/dist/services/generation-api.js.map +1 -1
  40. package/package.json +4 -3
@@ -0,0 +1,116 @@
1
+ #!/usr/bin/env python3
2
+ """multigrid-poster compose — Pillow 版渲染器
3
+
4
+ 输入:layout.json + N 张 cell 图片 + 标题文字 + caption 文字
5
+ 输出:1242×1660 PNG(小红书封面标准)
6
+
7
+ 用法:
8
+ python compose.py \
9
+ --layout <SKILL_DIR>/layouts/2x2.json \
10
+ --cells cell_0.png cell_1.png cell_2.png cell_3.png \
11
+ --title-line "26岁一个人创业" \
12
+ --title-line "跨境电商月入10w+" \
13
+ --caption-line "只需要一部手机就可以完成!" \
14
+ --caption-line "跨境人的必备app推荐" \
15
+ --output cover.png
16
+
17
+ 依赖:Pillow (容器自带,无需额外安装)
18
+ 字体:从 SKILL_DIR/shared/fonts/ 加载(layout.json 里指定)
19
+ """
20
+ import argparse
21
+ import json
22
+ import sys
23
+ from pathlib import Path
24
+ from PIL import Image, ImageDraw, ImageFont
25
+
26
+
27
+ def compose(layout_path: Path, cell_paths: list[Path],
28
+ title_lines: list[str], caption_lines: list[str],
29
+ output_path: Path) -> Path:
30
+ """根据 layout.json 渲染海报。
31
+
32
+ layout.json 字段:
33
+ canvas_size [w, h]
34
+ cells.positions [[x,y], ...] cell 左上角
35
+ cells.sizes [[w,h], ...] cell 尺寸
36
+ text_zones.title {lines: [...], size, color, stroke_w, stroke_color, font}
37
+ text_zones.caption {同上}
38
+ """
39
+ layout = json.loads(layout_path.read_text(encoding="utf-8"))
40
+ # 约定:layout 必须放在 <skill>/layouts/ 下,字体路径相对 <skill>/ 解析
41
+ skill_dir = layout_path.parent.parent
42
+
43
+ # 文本行数 vs layout 配置行数:行多了 zip 会静默截断,显式 fail-fast
44
+ # 错误信息用英文 — sys.exit 走 stderr,Windows GBK locale 下中文会乱码
45
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
46
+ zone = layout.get("text_zones", {}).get(zone_key)
47
+ if not zone:
48
+ continue
49
+ max_lines = len(zone["lines"])
50
+ if len(lines) > max_lines:
51
+ sys.exit(f"too many {zone_key} lines: got {len(lines)}, layout supports {max_lines}")
52
+
53
+ # Canvas
54
+ canvas = Image.new("RGB", tuple(layout["canvas_size"]), "white")
55
+
56
+ # Cells
57
+ cell_count = len(layout["cells"]["positions"])
58
+ if len(cell_paths) != cell_count:
59
+ sys.exit(f"cell count mismatch: layout expects {cell_count}, got {len(cell_paths)}")
60
+ for cp, pos, size in zip(cell_paths,
61
+ layout["cells"]["positions"],
62
+ layout["cells"]["sizes"]):
63
+ img = Image.open(cp).convert("RGB").resize(tuple(size), Image.LANCZOS)
64
+ canvas.paste(img, tuple(pos))
65
+
66
+ draw = ImageDraw.Draw(canvas)
67
+
68
+ # Text zones (title + caption 通用绘制)
69
+ for zone_key, lines in [("title", title_lines), ("caption", caption_lines)]:
70
+ if zone_key not in layout["text_zones"]:
71
+ continue
72
+ zone = layout["text_zones"][zone_key]
73
+ font_path = skill_dir / zone["font"]
74
+ font = ImageFont.truetype(str(font_path), zone["size"])
75
+ for text, line_cfg in zip(lines, zone["lines"]):
76
+ if not text:
77
+ continue
78
+ draw.text(
79
+ tuple(line_cfg["position"]), text,
80
+ fill=zone["color"], font=font,
81
+ stroke_width=zone.get("stroke_w", 0),
82
+ stroke_fill=zone.get("stroke_color", zone["color"]),
83
+ anchor=line_cfg.get("anchor", "la"),
84
+ )
85
+
86
+ canvas.save(output_path, optimize=True)
87
+ return output_path
88
+
89
+
90
+ def main():
91
+ ap = argparse.ArgumentParser(description=__doc__,
92
+ formatter_class=argparse.RawDescriptionHelpFormatter)
93
+ ap.add_argument("--layout", required=True, type=Path,
94
+ help="layout.json 路径(如 SKILL_DIR/layouts/2x2.json)")
95
+ ap.add_argument("--cells", required=True, nargs="+", type=Path,
96
+ help="N 张 cell 图片路径,顺序对应 layout.cells.positions")
97
+ ap.add_argument("--title-line", action="append", default=[],
98
+ help="标题行(可重复)")
99
+ ap.add_argument("--caption-line", action="append", default=[],
100
+ help="底部 caption 行(可重复)")
101
+ ap.add_argument("--output", required=True, type=Path,
102
+ help="输出 PNG 路径")
103
+ args = ap.parse_args()
104
+
105
+ out = compose(
106
+ layout_path=args.layout,
107
+ cell_paths=args.cells,
108
+ title_lines=args.title_line,
109
+ caption_lines=args.caption_line,
110
+ output_path=args.output,
111
+ )
112
+ print(f"saved {out} ({out.stat().st_size // 1024} KB)")
113
+
114
+
115
+ if __name__ == "__main__":
116
+ main()
@@ -0,0 +1,189 @@
1
+ ---
2
+ name: video-edit
3
+ description: "剪辑用户【已有的】口播视频——你直接重写成片应该说什么的脚本,系统用文本对齐回原始音频时间戳。
4
+
5
+ 必备前提:用户已经有视频文件(拍好的、上传的、或给出文件路径)。
6
+
7
+ 触发:用户上传/给出视频文件 + 说'剪一下'/'去卡顿'/'变流畅'/
8
+ '剪辑'/'剪短点'/'加字幕'/'cut'/'edit'/'trim'/'让视频更紧凑'。"
9
+ version: 1.0.0
10
+ owner_repo: Optima-Chat/optima-gen
11
+ ---
12
+
13
+ # Video-Edit Skill — Descript 模式:你写脚本,系统对齐
14
+
15
+ 用户给原始视频,你交付剪好的成片。
16
+
17
+ ## 用户怎么说,你交付什么
18
+
19
+ **默认所有剪辑都带字幕**——中文短视频 99% 需要字幕,"剪一下"的潜台词就是"给我能直接发的成片"。
20
+
21
+ | 用户说 | 你交付 |
22
+ |---|---|
23
+ | "剪一下" / "去卡顿" / "变流畅" | `<video>_subbed.mp4`(剪 + 字幕) |
24
+ | "剪成 X 秒短视频" / "30 秒" | `<video>_subbed.mp4`(压到目标时长 + 字幕) |
25
+ | "加字幕"(不需要剪) | `<video>_subbed.mp4`(仅字幕) |
26
+ | "剪一下不要字幕" / "无字幕版" | `<video>_edited.mp4` |
27
+
28
+ 只有当用户提到平台但**没说目标时长**时(如"剪成 TikTok"无时长),才追问"目标多长?"。
29
+ 其他情况**直接动手**,不要废话。
30
+
31
+ ## 核心原则:你直接写最终脚本
32
+
33
+ 剪辑工具有两种模式:
34
+ - **旧模式(`cut`)**:在 proposal 里给短语标 `#` 删除——只能删整段
35
+ - **新模式(`smart-cut`)✅ 默认用这个**:你直接写"成片应该说什么",系统找原始音频里对应的位置剪出来
36
+
37
+ 新模式的杀手锏是:你**不受短语边界限制**,可以:
38
+ - 跳过填充词("嗯"/"那个")
39
+ - NG 重拍中只保留你认为最好那遍
40
+ - 把同一句话的不同片段拼起来
41
+ - 保留某句的前半 + 另一句的后半
42
+
43
+ **约束**:
44
+ - 不能凭空造内容——脚本里的字必须出自原始转写,否则匹配不上
45
+ - 不能调换顺序——内容按原片时间顺序保留
46
+ - 字面错别字会"匹不上"被丢——必须用转写文本里的原字(whisper 怎么识的就怎么写)
47
+
48
+ ## 你的内部流程
49
+
50
+ ### 默认剪辑(剪 + 字幕,最常见)
51
+
52
+ ```bash
53
+ video-edit analyze <video>
54
+ ```
55
+
56
+ 读 `<video>.work/cut_proposal.md` 了解原片内容(每行是个短语单元,带时间戳和文本)。
57
+
58
+ 然后基于这些短语,**自己写一份最终脚本**到 `<video>.work/final_script.txt`:
59
+
60
+ **格式**:每个短语段两行——中文一行(带 `**关键词**` 标记,1-2 个),英文翻译一行;段与段之间空行。
61
+
62
+ ```
63
+ 做**跨境电商**是有**捷径**的
64
+ Cross-border e-commerce has shortcuts
65
+
66
+ 我最不缺的
67
+ What I lack the least
68
+
69
+ 就是**囤货**
70
+ It is just hoarding goods
71
+ ```
72
+
73
+ 写脚本时的判断:
74
+ - 原片每个 phrase,问自己"这句话进成片吗"——进就把内容(用 proposal 里的原文)写进脚本
75
+ - 同一意思被讲多遍(NG 重拍)——只写一遍,挑最流畅那遍的措辞
76
+ - 填充词/单字口头禅/跑题——直接不写
77
+ - 整段废话/犹豫——直接不写
78
+ - **中文必须用原文措辞**——不要润色或改写,否则匹配不上
79
+ - **每行 ≤ 10 个汉字**——超过会显示挤、可能溢出视频边界
80
+ - **`**关键词**` 标记 1-2 个**:挑能传达这句"重点信息"的实词(名词/动词),不要标助词、口头禅;短语里没明显重点就 0 个标记
81
+ - **英文翻译要地道、口语化**——给海外用户看的,不是直译。短句即可,可省略主语。
82
+
83
+ **写完后必做的自查(保存前)**:把 final_script 当成一段**连续口播稿**通读一遍——不是一行行检查,是**整体读**。问自己:
84
+
85
+ 1. **同一件事说了两遍吗?**(NG 重拍最常见的坑:上一段和下一段措辞不同但说的是同一件事,比如"我做这个挺久了" + "做了三四年了"——属于同一意思,只留一段)
86
+ 2. **逻辑有跳跃/断点吗?**(如果有,缺哪句话补哪句,前提是原片说过)
87
+ 3. **任何一段是"上一段的翻版"吗?**(删一段)
88
+
89
+ **这一步不可省略**——`smart-cut` 现在会硬阻断字面 4+ 字重复,但语义级重复(同义不同字)只能靠你这一遍通读发现。
90
+
91
+ 写完自查通过后:
92
+
93
+ ```bash
94
+ video-edit smart-cut <video>
95
+ ```
96
+
97
+ `smart-cut` 用 difflib 把脚本对齐回原始 word 时间戳。**如果输出 `[FAIL] 检测到字面重复`**:
98
+
99
+ 1. 看报错指出的"第 X 段 vs 第 Y 段"
100
+ 2. 打开 final_script.txt 删掉重复的那段(一般留措辞更流畅的那遍)
101
+ 3. 重跑 `smart-cut`
102
+ 4. 直到不再 FAIL 才能继续
103
+
104
+ **剪完之后必跑 review**(语义级 NG 兜底检测):
105
+
106
+ ```bash
107
+ video-edit review <video>
108
+ ```
109
+
110
+ 如果报告里有 `HARD: repeated-content`:删 `final_script.txt` 里对应的重复段,重跑 `smart-cut` + `review`,直到 HARD = 0。
111
+
112
+ review 通过后再烧字幕:
113
+
114
+ ```bash
115
+ video-edit subtitle <video>
116
+ ```
117
+
118
+ ### 用户要求调整时
119
+
120
+ 用户看完成片说某段不对:
121
+ 1. 编辑 `<video>.work/final_script.txt`(加/删内容)
122
+ 2. 删 `<video>.work/subs.ass`
123
+ 3. 重跑 `smart-cut` + `subtitle`
124
+
125
+ ### 限时长精剪(用户给了目标时长)
126
+
127
+ 写 final_script 时**额外做这些**:
128
+ - 估算字数:中文口播 ~3-4 字/秒,30 秒约 90-120 字
129
+ - 找钩子句放最前面(脚本开头那段)
130
+ - 压到目标时长(更激进地删)
131
+
132
+ ### 仅字幕(用户明确说不剪)
133
+
134
+ ```bash
135
+ video-edit subtitle <video>
136
+ ```
137
+
138
+ 没有 `_edited.mp4` 时,命令直接对原片烧字幕。
139
+
140
+ ### 无字幕版(用户明确说不要字幕)
141
+
142
+ ```bash
143
+ video-edit analyze <video>
144
+ # 写 final_script.txt
145
+ video-edit smart-cut <video>
146
+ ```
147
+
148
+ 不跑 subtitle,交付 `<原片>_edited.mp4`。
149
+
150
+ ## 写脚本的心法
151
+
152
+ 读 proposal 时**逐 phrase 判断**:
153
+ 1. 这句进成片吗?进 → 把 phrase 文本贴到 final_script
154
+ 2. 这句和前后哪句重复?只留一遍
155
+ 3. 这句是填充词/口头禅?跳过
156
+ 4. 这句跑题?跳过
157
+
158
+ **关键纪律**:脚本里的字**必须**来自原始转写——直接复制 proposal 里的 phrase 文本最稳。
159
+ 你想怎么润色都不行(系统按字符匹配,改了字就匹不上)。
160
+
161
+ **追求"看着不卡",宁可激进**。删多了用户能要求加回来,删少了用户体验差。
162
+
163
+ ## 交付怎么说
164
+
165
+ 简洁,**只说结果**:
166
+
167
+ - ✅ "剪好了,xxx_subbed.mp4,时长 23 秒"
168
+ - ❌ "我用了 video-edit smart-cut,写了 final_script.txt..."
169
+
170
+ ## 不要做的事
171
+
172
+ - ❌ 跳过 final_script 直接 smart-cut——会失败(找不到脚本文件)
173
+ - ❌ 在脚本里写转写以外的字——匹配不上,那部分会被丢
174
+ - ❌ 用 `both` 或 `cut`——已弃用,质量差
175
+ - ❌ 把命令名/路径暴露给用户——内部细节
176
+ - ❌ 用户说"剪掉前 X 秒"/"剪掉中间一段"——定点裁剪不是去卡顿,告诉用户"我擅长去卡顿停顿,定点裁剪请用别的方式"
177
+ - ❌ **跳过通读自查直接 smart-cut**——重复内容是用户最反感的问题,省这一步省不出去
178
+ - ❌ **跳过 review 直接 subtitle**——subtitle 烧完字幕的成片重复不可逆,必须先通过 review
179
+
180
+ ## 命令参考(你内部用)
181
+
182
+ | 命令 | 用途 |
183
+ |---|---|
184
+ | `video-edit analyze <video>` | 转写 + 静音检测 + 生成 proposal(**默认入口**)|
185
+ | `video-edit smart-cut <video>` | 按 final_script.txt 对齐剪辑(**默认主力**)|
186
+ | `video-edit subtitle <video>` | 对成片重新转写 + 烧字幕,输出 `<原片>_subbed.mp4` |
187
+ | `video-edit cut <video>` | 旧模式:按 proposal 的 `#` 标记剪辑(弃用,质量差)|
188
+ | `video-edit both <video>` | 旧模式:analyze + cut 一步到位(弃用,跳过审片)|
189
+ | `video-edit review <video>` | 诊断用:检查 `_edited.mp4` |