@gitruck/cli 0.1.0 → 0.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.
- package/AGENT.md +40 -5
- package/README.md +20 -14
- package/dist/index.js +189 -110
- package/package.json +9 -1
- package/skills/gtrk-oralcut/SKILL.md +75 -17
package/AGENT.md
CHANGED
|
@@ -47,6 +47,12 @@ gtrk oralcut <毛片.mp4> [--script 文字稿.txt] # 剪一条;剪完自动
|
|
|
47
47
|
| `-o, --out <dir>` | 自定义产物目录 | `<毛片同目录>/<毛片名>-video-project-<YYMMDD-HHMMSS>` |
|
|
48
48
|
| `-f, --formats <list>` | 三方格式逗号分隔 | `gtrk,jianying,xml` |
|
|
49
49
|
| `--jianying-draft-dir <dir>` | 剪映草稿根目录;传路径或 `auto` | 读 `gtrk init` 配置 / 自动探测 |
|
|
50
|
+
| `--lang <code>` | 语言代码(英文 `en-US`、日文 `ja-JP`…) | `zh-CN` |
|
|
51
|
+
| `--visual-assist` | **视觉兜底**:ASR 漏字时用人脸/说话检测保护 + 重识别(剪不准时开) | 关 |
|
|
52
|
+
| `--no-adaptive-rhythm` | 关闭自适应节奏,改用固定标点停顿表 | 自适应开 |
|
|
53
|
+
| `--render` | 额外渲染成片视频(`outputs` 加 `video`) | 只出工程 |
|
|
54
|
+
| `--crf <n>` / `--codec <c>` | 视频质量 14-28(默认 18,越小越清晰)/ 编码(默认 h264),配 `--render` | — |
|
|
55
|
+
| `--param k=v` / `--params-json '{…}'` | **通用透传**:任意云端参数(标量可重复 / JSON 嵌套),优先级最高 | — |
|
|
50
56
|
| `--reupload` | 强制重新上传,忽略本地上传缓存 | 关 |
|
|
51
57
|
| `--no-open` | 完成后**不**自动打开产物目录 | **默认会自动打开** |
|
|
52
58
|
|
|
@@ -61,6 +67,27 @@ gtrk oralcut <毛片.mp4> [--script 文字稿.txt] # 剪一条;剪完自动
|
|
|
61
67
|
- **节奏预设**:`steady` 保留更多停顿(稳)、`concise` 默认精炼、`compact` 最紧凑(压停顿最狠)。
|
|
62
68
|
- **部分格式失败不致命**:CLI 如实回显云端 `errors`(某格式没出来不影响其余)。
|
|
63
69
|
|
|
70
|
+
**细节微调 —— 按用户诉求因势象形、自由组合**(上表是常用一等 flag;下面是节奏细调 + 完整取值)。你有云端全部参数,按需自由决定用哪些。**唯一要求:名字 / 取值 / 范围照文档用**(别记错拼错);传越界云端报 `6016` 附原因、照改即可(乱传不产错误成片、只明确报错)。没特别诉求就跑默认。
|
|
71
|
+
|
|
72
|
+
- **剪不准 / 剪掉真内容 / 有句话没剪进去** → `--visual-assist`:ASR 之外并行跑人脸 + 说话检测,画面在说话却没识别出字的地方**保护不剪**并重识别捞回,捞不回的进 `report.review_points` 复核;引擎挂了只降级不失败、不额外计费、绝不凭空生成。**「剪不准」的兜底。**
|
|
73
|
+
- **节奏散参数**(无一等 flag,走 `--param 键=值` / `--params-json`,单位秒、范围 0–5):
|
|
74
|
+
- `punctuation_breaks`:逐标点停顿,键 `,、;:。!?—……` + `paragraph`(段落)。例 `--params-json '{"punctuation_breaks":{"。":0.6,",":0.3}}'`。
|
|
75
|
+
- `intra_gap_max`(>此值算气口,默认 0.35)/ `intra_gap_target`(收到多长,默认 0.10)/ `pad_in`(0.05) / `pad_out`(0.08)。例「气口留白多点」`--param pad_out=0.15`。
|
|
76
|
+
- `render.audio_crossfade_ms`(切点淡化毫秒 0–50、默认 8)等也能透传。
|
|
77
|
+
- **节奏预设完整值**(选最贴内容的、再逐项覆盖;「默认」列 = 不选预设时各标点的默认停顿):
|
|
78
|
+
|
|
79
|
+
| 参数(秒) | steady 稳健 | concise 精练 | compact 紧凑 | 默认 |
|
|
80
|
+
|---|---|---|---|---|
|
|
81
|
+
| 适用 | 讲述/教学 | 自媒体中长 | 短视频/广告 | 通用 |
|
|
82
|
+
| `、`/`,`/`;` | 0.18/0.25/0.36 | 0.12/0.18/0.26 | 0.06/0.08/0.12 | 0.15/0.20/0.30 |
|
|
83
|
+
| `:`/`—` | 0.45/0.55 | 0.32/0.40 | 0.15/0.18 | 0.35/0.45 |
|
|
84
|
+
| `。``!`/`?` | 0.55/0.60 | 0.40/0.45 | 0.18/0.20 | 0.45/0.50 |
|
|
85
|
+
| `……`/`paragraph` | 0.75/1.20 | 0.55/0.90 | 0.25/0.40 | 0.60/1.00 |
|
|
86
|
+
| `intra_gap_max`/`intra_gap_target` | 0.45/0.15 | 0.35/0.10 | 0.25/0.05 | 0.35/0.10 |
|
|
87
|
+
| `pad_in`/`pad_out` | 0.05/0.10 | 0.05/0.08 | 0.03/0.05 | 0.05/0.08 |
|
|
88
|
+
|
|
89
|
+
- 优先级:`--preset` → 一等 flag → 透传(后者覆盖前者)。用户**自己点名**某参数 + 值 → 照他原样透传(CLI 底层支持任意云端参数)。**以上即 agent 需要的全部参数、本文档自足**(`gtrk oralcut --help` 也列全部 flag);官网的原始 HTTP API 文档是给人看的,agent 不必也无法访问。
|
|
90
|
+
|
|
64
91
|
---
|
|
65
92
|
|
|
66
93
|
## 3. 产物结构 + 三端打开
|
|
@@ -91,19 +118,21 @@ gtrk oralcut <毛片.mp4> [--script 文字稿.txt] # 剪一条;剪完自动
|
|
|
91
118
|
4. **要不要自动打开**:默认就开(用户常不知道文件去哪了,别让他找)。只有明确「别打开 / 批处理」才加 `--no-open`。
|
|
92
119
|
5. **剪映装在非标准位置 / 多版本**:若用户没跑过 `gtrk init` 或探测失败,问剪映草稿根目录、传 `--jianying-draft-dir`(或让他先 `gtrk init`)。
|
|
93
120
|
6. **只要某一两端**:用户只要客户端 → `--formats gtrk`;只要剪映 → `--formats jianying`;默认三端全给。
|
|
121
|
+
7. **有具体细节诉求**(剪不准 / 换语言 / 要成片 / 调某个停顿)→ 见上「细节微调」,按需自由取用(`--visual-assist` / `--lang` / `--render` / 散参数);没特别诉求跑默认即可。
|
|
94
122
|
|
|
95
|
-
|
|
123
|
+
**跑完读 `report`、验证、给用户交代**(`--json` 的 stdout 那行带 `files` / `errors` / **`report`**;别只信"成功"、别谎报三端都好):
|
|
124
|
+
- **读 `report`(因势象形的另一半)**:`duration_before`→`after`(剪了多少);`dropped[]`(剔了哪些、`reason` retake/misread);`coverage`(<0.6 附 `low_coverage` = 文稿与实拍严重不符);`uncovered_script[]`(**漏读**:文稿有、实拍没找到 → 如实说,疑似漏识别则建议 `--visual-assist` 重跑);`review_points[]`(建议复核处);开了 visual_assist 还有 `suspect_omissions` / `stt_recovered` / `visual_assist_degraded`。**据此因势象形**:覆盖率低 / 漏读多 → 开 `--visual-assist` 或核对文稿重跑;节奏不满意 → 调 `--preset` / 散参数重跑(同毛片可反复剪对比)。
|
|
96
125
|
- 确认产物目录在、`gtrk/project.gtrk` 非空(>0 字节);要剪映就确认剪映草稿根里有**同名工程目录** + `draft_meta_info.json`。
|
|
97
|
-
- 云端 `errors` 非空 →
|
|
98
|
-
- 然后**回给用户**:产物目录路径 + 三端各自怎么打开(客户端选 `gtrk/project.gtrk`、剪映已在项目列表、PR 导入 `xml/premiere.xml
|
|
126
|
+
- 云端 `errors` 非空 → 如实告知哪个格式失败、原因。
|
|
127
|
+
- 然后**回给用户**:产物目录路径 + 三端各自怎么打开(客户端选 `gtrk/project.gtrk`、剪映已在项目列表、PR 导入 `xml/premiere.xml`),并**据 `report` 给一句交代**(剪了多久 → 多久、去掉了什么、有无漏读需复核)。
|
|
99
128
|
|
|
100
129
|
---
|
|
101
130
|
|
|
102
131
|
## 5. 典型调用
|
|
103
132
|
|
|
104
133
|
```bash
|
|
105
|
-
# 有稿 +
|
|
106
|
-
gtrk oralcut "D:/素材/某选题-原始口播.mp4" --script "D:/素材/某选题-文字稿.txt" --
|
|
134
|
+
# 有稿 + 剪完就看(最常见;剪完默认自动打开产物目录,无需额外 flag)
|
|
135
|
+
gtrk oralcut "D:/素材/某选题-原始口播.mp4" --script "D:/素材/某选题-文字稿.txt" --json
|
|
107
136
|
|
|
108
137
|
# 无稿、要最紧凑节奏
|
|
109
138
|
gtrk oralcut "D:/素材/某条.mp4" --preset compact
|
|
@@ -113,6 +142,12 @@ gtrk oralcut "D:/素材/某条.mp4" --formats gtrk
|
|
|
113
142
|
|
|
114
143
|
# 剪映装在非标准盘符,手动指目录
|
|
115
144
|
gtrk oralcut "D:/素材/某条.mp4" --jianying-draft-dir "F:/JianyingPro/User Data/Projects/com.lveditor.draft"
|
|
145
|
+
|
|
146
|
+
# 用户抱怨"剪掉了真内容 / 剪不准" → 开视觉兜底重跑
|
|
147
|
+
gtrk oralcut "D:/素材/某条.mp4" --visual-assist
|
|
148
|
+
|
|
149
|
+
# 细到某标点停顿 + 顺手渲一个成片
|
|
150
|
+
gtrk oralcut "D:/素材/某条.mp4" --params-json '{"punctuation_breaks":{"。":0.6}}' --render --crf 20
|
|
116
151
|
```
|
|
117
152
|
|
|
118
153
|
---
|
package/README.md
CHANGED
|
@@ -25,31 +25,36 @@
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## 获取 API Key
|
|
29
|
+
|
|
30
|
+
CLI 要调用同合云云端能力,需先拿一个 API Key(形如 `gc_xxxxxxxx`):
|
|
31
|
+
|
|
32
|
+
1. 打开官网 **[cloud.ai-mcn.tv](https://cloud.ai-mcn.tv)** 并登录 —— **登录即开通**、自带免费测试额度、零门槛。
|
|
33
|
+
2. 进入 **[控制台](https://cloud.ai-mcn.tv/zh-CN/dashboard)**,在「API 密钥 / 密钥管理」处生成并复制你的 Key。
|
|
34
|
+
3. 下一步 `gtrk install` 会让你把它粘进去(一次配好、本地长期复用)。
|
|
35
|
+
|
|
36
|
+
> 快速开始文档:[cloud.ai-mcn.tv/zh-CN/docs/quick-start](https://cloud.ai-mcn.tv/zh-CN/docs/quick-start) · 对接咨询:business@migotimes.com
|
|
37
|
+
|
|
28
38
|
## 安装 & 快速上手
|
|
29
39
|
|
|
30
40
|
需要 Node.js ≥ 20.6(`node -v` 查看)。
|
|
31
41
|
|
|
32
42
|
```bash
|
|
33
|
-
# 1)
|
|
34
|
-
npm i -g @gitruck/cli
|
|
35
|
-
#
|
|
43
|
+
# 1) 一条命令装全:命令行 gtrk + /gtrk-oralcut skill + 配置(填 API Key、自动扫剪映目录)
|
|
44
|
+
npm i -g @gitruck/cli@latest && gtrk install
|
|
45
|
+
# 或免全局安装直接用:npx @gitruck/cli@latest install
|
|
36
46
|
|
|
37
|
-
# 2)
|
|
38
|
-
gtrk init
|
|
39
|
-
|
|
40
|
-
# 3) 剪一条(剪完自动打开产物目录)
|
|
47
|
+
# 2) 剪一条(剪完自动打开产物目录)
|
|
41
48
|
gtrk oralcut "D:/素材/某选题-原始口播.mp4" --script "D:/素材/某选题-文字稿.txt"
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
>
|
|
51
|
+
> 只想配置、不装 skill:用 `gtrk init`。本地开发:`cd gtrk-cli && bun install && bun run src/index.ts <命令>`。
|
|
45
52
|
|
|
46
53
|
产物目录形如 `<毛片名>-video-project-<YYMMDD-HHMMSS>/`,内含 `gtrk/`、`jianying/`、`xml/` 三端工程。
|
|
47
54
|
|
|
48
55
|
## 给 AI Agent 用
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
gtrk skills install # 把 /gtrk-oralcut 装进 ~/.claude/skills
|
|
52
|
-
```
|
|
57
|
+
`gtrk install` 已经把 `/gtrk-oralcut` skill 装进 `~/.claude/skills`(单独装用 `gtrk skills install`)。
|
|
53
58
|
|
|
54
59
|
然后在 Claude Code 里直接说「**帮我把这条口播剪一版**」或打 `/gtrk-oralcut`,agent 会问清毛片 / 文稿 / 节奏,调 `gtrk oralcut --json` 跑通闭环、验证产物、把三端打开方式回给你。完整可移植 playbook 见 [`AGENT.md`](./AGENT.md)。
|
|
55
60
|
|
|
@@ -102,13 +107,14 @@ gtrk init --api-key <KEY> --jianying-draft-dir auto -y
|
|
|
102
107
|
| `--no-open` | 完成后不自动打开产物目录 | **默认自动打开** |
|
|
103
108
|
| `--json` | 机读:stdout 只输出结果 JSON(给 agent / 脚本) | 关 |
|
|
104
109
|
|
|
105
|
-
`--json`
|
|
110
|
+
`--json` 输出(成功时 stdout 单行):`{ ok, outDir, files:{gtrk,jianying,xml}, jianyingDraftPath, report, errors, taskId, fileId }`;命令失败则进程非 0 退出、报错走 stderr、stdout 无 JSON。
|
|
106
111
|
|
|
107
112
|
### 其它
|
|
108
113
|
|
|
109
|
-
- `gtrk
|
|
114
|
+
- `gtrk install [--api-key … -y --skills-dir …]` — 一条命令装全(skill + 配置 + 体检),对标飞书 `lark-cli install`。
|
|
115
|
+
- `gtrk init [--api-key … --api-base … --jianying-draft-dir … -y]` — 仅配置(交互 / 非交互)。
|
|
110
116
|
- `gtrk doctor` — 体检。
|
|
111
|
-
- `gtrk skills install [--dir <skills 目录>]` —
|
|
117
|
+
- `gtrk skills install [--dir <skills 目录>]` — 单独安装 agent skill。
|
|
112
118
|
|
|
113
119
|
---
|
|
114
120
|
|
package/dist/index.js
CHANGED
|
@@ -1888,22 +1888,101 @@ var {
|
|
|
1888
1888
|
Help
|
|
1889
1889
|
} = import__.default;
|
|
1890
1890
|
|
|
1891
|
+
// src/index.ts
|
|
1892
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
1893
|
+
import { join as join8 } from "node:path";
|
|
1894
|
+
|
|
1895
|
+
// src/lib/paths.ts
|
|
1896
|
+
import { dirname, join } from "node:path";
|
|
1897
|
+
import { fileURLToPath } from "node:url";
|
|
1898
|
+
import { existsSync } from "node:fs";
|
|
1899
|
+
function packageRoot() {
|
|
1900
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1901
|
+
for (let i = 0;i < 8; i++) {
|
|
1902
|
+
if (existsSync(join(dir, "package.json")))
|
|
1903
|
+
return dir;
|
|
1904
|
+
const parent = dirname(dir);
|
|
1905
|
+
if (parent === dir)
|
|
1906
|
+
break;
|
|
1907
|
+
dir = parent;
|
|
1908
|
+
}
|
|
1909
|
+
return dir;
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1912
|
+
// src/commands/skills.ts
|
|
1913
|
+
import { homedir } from "node:os";
|
|
1914
|
+
import { join as join2 } from "node:path";
|
|
1915
|
+
import { existsSync as existsSync2, mkdirSync, copyFileSync } from "node:fs";
|
|
1916
|
+
|
|
1917
|
+
// src/lib/log.ts
|
|
1918
|
+
var c = {
|
|
1919
|
+
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
1920
|
+
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
1921
|
+
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
1922
|
+
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
1923
|
+
red: (s) => `\x1B[31m${s}\x1B[0m`
|
|
1924
|
+
};
|
|
1925
|
+
var humanOut = process.stdout;
|
|
1926
|
+
function routeLogsToStderr() {
|
|
1927
|
+
humanOut = process.stderr;
|
|
1928
|
+
}
|
|
1929
|
+
var line = (s) => humanOut.write(`${s}
|
|
1930
|
+
`);
|
|
1931
|
+
var log = {
|
|
1932
|
+
step: (msg) => line(c.cyan(msg)),
|
|
1933
|
+
info: (msg) => line(c.dim(` ${msg}`)),
|
|
1934
|
+
ok: (msg) => line(c.green(`✅ ${msg}`)),
|
|
1935
|
+
warn: (msg) => line(c.yellow(`⚠️ ${msg}`)),
|
|
1936
|
+
err: (msg) => process.stderr.write(`${c.red(`❌ ${msg}`)}
|
|
1937
|
+
`),
|
|
1938
|
+
tick: (msg) => humanOut.write(`\r ${msg}\x1B[K`),
|
|
1939
|
+
tickEnd: () => humanOut.write(`
|
|
1940
|
+
`)
|
|
1941
|
+
};
|
|
1942
|
+
|
|
1943
|
+
// src/commands/skills.ts
|
|
1944
|
+
var SKILL_NAME = "gtrk-oralcut";
|
|
1945
|
+
var SRC = join2(packageRoot(), "skills", SKILL_NAME, "SKILL.md");
|
|
1946
|
+
function installSkill(opts = {}) {
|
|
1947
|
+
if (!existsSync2(SRC)) {
|
|
1948
|
+
log.warn(`找不到打包的 skill 源:${SRC}(跳过 skill 安装,不影响命令行)`);
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
const dest = join2(opts.dir ?? join2(homedir(), ".claude", "skills"), SKILL_NAME);
|
|
1952
|
+
try {
|
|
1953
|
+
mkdirSync(dest, { recursive: true });
|
|
1954
|
+
copyFileSync(SRC, join2(dest, "SKILL.md"));
|
|
1955
|
+
log.ok(`已安装 /${SKILL_NAME} → ${join2(dest, "SKILL.md")}`);
|
|
1956
|
+
log.info("在 Claude Code 里打 /gtrk-oralcut,或直接说「帮我剪个口播」即可触发(可能需重载会话)。");
|
|
1957
|
+
return true;
|
|
1958
|
+
} catch (e) {
|
|
1959
|
+
log.warn(`skill 安装失败(不影响命令行使用):${e instanceof Error ? e.message : String(e)}`);
|
|
1960
|
+
return false;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function registerSkills(program2) {
|
|
1964
|
+
const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
|
|
1965
|
+
skills.command("install").description(`把 /${SKILL_NAME} 安装到 ~/.claude/skills(对标飞书 skills add)`).option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
|
|
1966
|
+
installSkill({ dir: opts.dir });
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1891
1970
|
// src/commands/init.ts
|
|
1892
|
-
import { join as
|
|
1893
|
-
import { existsSync as
|
|
1971
|
+
import { join as join5, resolve as resolve2 } from "node:path";
|
|
1972
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1894
1973
|
|
|
1895
1974
|
// src/lib/user-config.ts
|
|
1896
|
-
import { homedir } from "node:os";
|
|
1897
|
-
import { join } from "node:path";
|
|
1898
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
1975
|
+
import { homedir as homedir2 } from "node:os";
|
|
1976
|
+
import { join as join3 } from "node:path";
|
|
1977
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
1899
1978
|
var DEFAULT_API_BASE = "https://api.ai-mcn.tv:10000";
|
|
1900
|
-
var DIR =
|
|
1901
|
-
var FILE =
|
|
1979
|
+
var DIR = join3(homedir2(), ".gtrk-cli");
|
|
1980
|
+
var FILE = join3(DIR, "config.json");
|
|
1902
1981
|
function configPath() {
|
|
1903
1982
|
return FILE;
|
|
1904
1983
|
}
|
|
1905
1984
|
function readUserConfig() {
|
|
1906
|
-
if (!
|
|
1985
|
+
if (!existsSync3(FILE))
|
|
1907
1986
|
return {};
|
|
1908
1987
|
try {
|
|
1909
1988
|
return JSON.parse(readFileSync(FILE, "utf8"));
|
|
@@ -1912,29 +1991,29 @@ function readUserConfig() {
|
|
|
1912
1991
|
}
|
|
1913
1992
|
}
|
|
1914
1993
|
function writeUserConfig(patch) {
|
|
1915
|
-
|
|
1994
|
+
mkdirSync2(DIR, { recursive: true });
|
|
1916
1995
|
const merged = { ...readUserConfig(), ...patch };
|
|
1917
1996
|
writeFileSync(FILE, JSON.stringify(merged, null, 2));
|
|
1918
1997
|
}
|
|
1919
1998
|
|
|
1920
1999
|
// src/lib/jianying.ts
|
|
1921
|
-
import { join as
|
|
1922
|
-
import { existsSync as
|
|
2000
|
+
import { join as join4, resolve } from "node:path";
|
|
2001
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1923
2002
|
function probeJianyingDraftDir() {
|
|
1924
2003
|
const local = process.env.LOCALAPPDATA;
|
|
1925
2004
|
if (!local)
|
|
1926
2005
|
return;
|
|
1927
2006
|
const candidates = [
|
|
1928
|
-
|
|
1929
|
-
|
|
2007
|
+
join4(local, "JianyingPro", "User Data", "Projects", "com.lveditor.draft"),
|
|
2008
|
+
join4(local, "CapCut", "User Data", "Projects", "com.lveditor.draft")
|
|
1930
2009
|
];
|
|
1931
|
-
return candidates.find((p) =>
|
|
2010
|
+
return candidates.find((p) => existsSync4(p));
|
|
1932
2011
|
}
|
|
1933
2012
|
function resolveJianyingDraftDir(opt) {
|
|
1934
2013
|
if (opt && opt !== "auto")
|
|
1935
2014
|
return resolve(opt);
|
|
1936
2015
|
const saved = readUserConfig().jianyingDraftDir;
|
|
1937
|
-
if (saved &&
|
|
2016
|
+
if (saved && existsSync4(saved))
|
|
1938
2017
|
return saved;
|
|
1939
2018
|
return probeJianyingDraftDir();
|
|
1940
2019
|
}
|
|
@@ -1957,27 +2036,10 @@ function launch(cmd) {
|
|
|
1957
2036
|
} catch {}
|
|
1958
2037
|
}
|
|
1959
2038
|
|
|
1960
|
-
// src/lib/paths.ts
|
|
1961
|
-
import { dirname, join as join3 } from "node:path";
|
|
1962
|
-
import { fileURLToPath } from "node:url";
|
|
1963
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
1964
|
-
function packageRoot() {
|
|
1965
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
1966
|
-
for (let i = 0;i < 8; i++) {
|
|
1967
|
-
if (existsSync3(join3(dir, "package.json")))
|
|
1968
|
-
return dir;
|
|
1969
|
-
const parent = dirname(dir);
|
|
1970
|
-
if (parent === dir)
|
|
1971
|
-
break;
|
|
1972
|
-
dir = parent;
|
|
1973
|
-
}
|
|
1974
|
-
return dir;
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
2039
|
// src/lib/prompt.ts
|
|
1978
2040
|
import { stdin, stdout } from "node:process";
|
|
1979
2041
|
import { spawnSync } from "node:child_process";
|
|
1980
|
-
var
|
|
2042
|
+
var c2 = {
|
|
1981
2043
|
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
1982
2044
|
cyan: (s) => `\x1B[36m${s}\x1B[0m`
|
|
1983
2045
|
};
|
|
@@ -2002,8 +2064,8 @@ function readClipboard() {
|
|
|
2002
2064
|
}
|
|
2003
2065
|
function ask(message, opts = {}) {
|
|
2004
2066
|
return new Promise((resolve2) => {
|
|
2005
|
-
const hint = opts.defaultValue ?
|
|
2006
|
-
stdout.write(`${
|
|
2067
|
+
const hint = opts.defaultValue ? c2.dim(` (${opts.defaultValue})`) : "";
|
|
2068
|
+
stdout.write(`${c2.cyan("?")} ${message}${hint} `);
|
|
2007
2069
|
let value = "";
|
|
2008
2070
|
const echo = (s) => stdout.write(opts.mask ? "•".repeat([...s].length) : s);
|
|
2009
2071
|
const wasRaw = stdin.isRaw ?? false;
|
|
@@ -2060,14 +2122,14 @@ function promptSecret(message) {
|
|
|
2060
2122
|
return ask(message, { mask: true });
|
|
2061
2123
|
}
|
|
2062
2124
|
async function promptConfirm(message, defaultYes = true) {
|
|
2063
|
-
const a = (await ask(`${message} ${
|
|
2125
|
+
const a = (await ask(`${message} ${c2.dim(defaultYes ? "[Y/n]" : "[y/N]")}`)).trim().toLowerCase();
|
|
2064
2126
|
if (!a)
|
|
2065
2127
|
return defaultYes;
|
|
2066
2128
|
return a === "y" || a === "yes";
|
|
2067
2129
|
}
|
|
2068
2130
|
|
|
2069
2131
|
// src/commands/doctor.ts
|
|
2070
|
-
import { existsSync as
|
|
2132
|
+
import { existsSync as existsSync5 } from "node:fs";
|
|
2071
2133
|
function registerDoctor(program2) {
|
|
2072
2134
|
program2.command("doctor").description("体检:配置 / 云端连通 / 剪映目录 / 运行时是否就绪").action(async () => {
|
|
2073
2135
|
await runDoctor();
|
|
@@ -2116,7 +2178,7 @@ async function runDoctor() {
|
|
|
2116
2178
|
}
|
|
2117
2179
|
rows.push({ name: "云端连通 + 鉴权", status: apiStatus, detail: apiDetail });
|
|
2118
2180
|
const draftDir = resolveJianyingDraftDir(undefined);
|
|
2119
|
-
const draftOk = !!draftDir &&
|
|
2181
|
+
const draftOk = !!draftDir && existsSync5(draftDir);
|
|
2120
2182
|
rows.push({
|
|
2121
2183
|
name: "剪映草稿目录",
|
|
2122
2184
|
status: draftOk ? "ok" : "warn",
|
|
@@ -2124,8 +2186,8 @@ async function runDoctor() {
|
|
|
2124
2186
|
});
|
|
2125
2187
|
rows.push({
|
|
2126
2188
|
name: "配置文件",
|
|
2127
|
-
status:
|
|
2128
|
-
detail:
|
|
2189
|
+
status: existsSync5(configPath()) ? "ok" : "warn",
|
|
2190
|
+
detail: existsSync5(configPath()) ? configPath() : `未生成 —— 跑 gtrk init(${configPath()})`
|
|
2129
2191
|
});
|
|
2130
2192
|
console.log(`
|
|
2131
2193
|
gtrk 体检:
|
|
@@ -2143,34 +2205,8 @@ gtrk 体检:
|
|
|
2143
2205
|
return !failed;
|
|
2144
2206
|
}
|
|
2145
2207
|
|
|
2146
|
-
// src/lib/log.ts
|
|
2147
|
-
var c2 = {
|
|
2148
|
-
dim: (s) => `\x1B[2m${s}\x1B[0m`,
|
|
2149
|
-
cyan: (s) => `\x1B[36m${s}\x1B[0m`,
|
|
2150
|
-
green: (s) => `\x1B[32m${s}\x1B[0m`,
|
|
2151
|
-
yellow: (s) => `\x1B[33m${s}\x1B[0m`,
|
|
2152
|
-
red: (s) => `\x1B[31m${s}\x1B[0m`
|
|
2153
|
-
};
|
|
2154
|
-
var humanOut = process.stdout;
|
|
2155
|
-
function routeLogsToStderr() {
|
|
2156
|
-
humanOut = process.stderr;
|
|
2157
|
-
}
|
|
2158
|
-
var line = (s) => humanOut.write(`${s}
|
|
2159
|
-
`);
|
|
2160
|
-
var log = {
|
|
2161
|
-
step: (msg) => line(c2.cyan(msg)),
|
|
2162
|
-
info: (msg) => line(c2.dim(` ${msg}`)),
|
|
2163
|
-
ok: (msg) => line(c2.green(`✅ ${msg}`)),
|
|
2164
|
-
warn: (msg) => line(c2.yellow(`⚠️ ${msg}`)),
|
|
2165
|
-
err: (msg) => process.stderr.write(`${c2.red(`❌ ${msg}`)}
|
|
2166
|
-
`),
|
|
2167
|
-
tick: (msg) => humanOut.write(`\r ${msg}\x1B[K`),
|
|
2168
|
-
tickEnd: () => humanOut.write(`
|
|
2169
|
-
`)
|
|
2170
|
-
};
|
|
2171
|
-
|
|
2172
2208
|
// src/commands/init.ts
|
|
2173
|
-
var GUIDE_IMAGE =
|
|
2209
|
+
var GUIDE_IMAGE = join5(packageRoot(), "assets", "jianying-draft-path.png");
|
|
2174
2210
|
function registerInit(program2) {
|
|
2175
2211
|
program2.command("init").description("一次性配置:API Key + 剪映草稿目录(之后所有命令免重复配置)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址(缺省用默认生产地址)").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(runInit);
|
|
2176
2212
|
}
|
|
@@ -2205,7 +2241,7 @@ async function runInit(opts) {
|
|
|
2205
2241
|
openFile(GUIDE_IMAGE);
|
|
2206
2242
|
const manual = (await promptText("剪映草稿根目录(…\\com.lveditor.draft),留空跳过:")).trim();
|
|
2207
2243
|
if (manual) {
|
|
2208
|
-
if (
|
|
2244
|
+
if (existsSync6(manual))
|
|
2209
2245
|
jianyingDraftDir = resolve2(manual);
|
|
2210
2246
|
else
|
|
2211
2247
|
log.warn(`目录不存在,已跳过:${manual}`);
|
|
@@ -2254,10 +2290,20 @@ async function runInitNonInteractive(opts) {
|
|
|
2254
2290
|
await runDoctor();
|
|
2255
2291
|
}
|
|
2256
2292
|
|
|
2293
|
+
// src/commands/install.ts
|
|
2294
|
+
function registerInstall(program2) {
|
|
2295
|
+
program2.command("install").description("一条命令装全:安装 /gtrk-oralcut skill + 配置(对标飞书 lark-cli install)").option("--api-key <key>", "非交互:直接指定 API Key").option("--api-base <url>", "非交互:指定 API 根地址").option("--jianying-draft-dir <dir>", "非交互:剪映草稿目录(传 auto 则自动探测)").option("--skills-dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").option("-y, --yes", "非交互:用传入值 + 自动探测,不弹任何提示").action(async (opts) => {
|
|
2296
|
+
log.step("① 安装 agent skill…");
|
|
2297
|
+
installSkill({ dir: opts.skillsDir });
|
|
2298
|
+
log.step("② 配置 + 体检…");
|
|
2299
|
+
await runInit(opts);
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2257
2303
|
// src/commands/oralcut.ts
|
|
2258
|
-
import { resolve as resolve3, join as
|
|
2304
|
+
import { resolve as resolve3, join as join7, dirname as dirname2, basename as basename2, extname } from "node:path";
|
|
2259
2305
|
import { mkdir as mkdir2, cp, readFile as readFile2 } from "node:fs/promises";
|
|
2260
|
-
import { existsSync as
|
|
2306
|
+
import { existsSync as existsSync8 } from "node:fs";
|
|
2261
2307
|
|
|
2262
2308
|
// src/lib/config.ts
|
|
2263
2309
|
function loadConfig() {
|
|
@@ -2357,18 +2403,18 @@ async function download(url, dest) {
|
|
|
2357
2403
|
}
|
|
2358
2404
|
|
|
2359
2405
|
// src/lib/upload-cache.ts
|
|
2360
|
-
import { homedir as
|
|
2361
|
-
import { join as
|
|
2406
|
+
import { homedir as homedir3 } from "node:os";
|
|
2407
|
+
import { join as join6 } from "node:path";
|
|
2362
2408
|
import { stat, mkdir, readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
2363
|
-
import { existsSync as
|
|
2364
|
-
var CACHE_DIR =
|
|
2365
|
-
var CACHE_FILE =
|
|
2409
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2410
|
+
var CACHE_DIR = join6(homedir3(), ".gtrk-cli");
|
|
2411
|
+
var CACHE_FILE = join6(CACHE_DIR, "upload-cache.json");
|
|
2366
2412
|
async function fingerprint(path) {
|
|
2367
2413
|
const s = await stat(path);
|
|
2368
2414
|
return `${s.size}:${Math.round(s.mtimeMs)}`;
|
|
2369
2415
|
}
|
|
2370
2416
|
async function load() {
|
|
2371
|
-
if (!
|
|
2417
|
+
if (!existsSync7(CACHE_FILE))
|
|
2372
2418
|
return {};
|
|
2373
2419
|
try {
|
|
2374
2420
|
return JSON.parse(await readFile(CACHE_FILE, "utf8"));
|
|
@@ -2429,8 +2475,43 @@ var FORMAT_META = {
|
|
|
2429
2475
|
fcpxml: { label: "Final Cut (fcpxml)", openHint: (p) => `Final Cut Pro:导入 ${p}` },
|
|
2430
2476
|
otio: { label: "OpenTimelineIO", openHint: (p) => `用支持 OTIO 的工具打开 ${p}` }
|
|
2431
2477
|
};
|
|
2478
|
+
var collectParam = (v, acc) => {
|
|
2479
|
+
acc.push(v);
|
|
2480
|
+
return acc;
|
|
2481
|
+
};
|
|
2482
|
+
function coerceValue(v) {
|
|
2483
|
+
if (v === "true")
|
|
2484
|
+
return true;
|
|
2485
|
+
if (v === "false")
|
|
2486
|
+
return false;
|
|
2487
|
+
if (v.trim() !== "" && !Number.isNaN(Number(v)))
|
|
2488
|
+
return Number(v);
|
|
2489
|
+
return v;
|
|
2490
|
+
}
|
|
2491
|
+
function parseExtraParams(pairs, jsonStr) {
|
|
2492
|
+
const out = {};
|
|
2493
|
+
for (const pair of pairs) {
|
|
2494
|
+
const i = pair.indexOf("=");
|
|
2495
|
+
if (i < 0)
|
|
2496
|
+
throw new Error(`--param 需要 key=value 格式:「${pair}」`);
|
|
2497
|
+
out[pair.slice(0, i).trim()] = coerceValue(pair.slice(i + 1));
|
|
2498
|
+
}
|
|
2499
|
+
if (jsonStr) {
|
|
2500
|
+
let parsed;
|
|
2501
|
+
try {
|
|
2502
|
+
parsed = JSON.parse(jsonStr);
|
|
2503
|
+
} catch {
|
|
2504
|
+
throw new Error(`--params-json 不是合法 JSON:${jsonStr}`);
|
|
2505
|
+
}
|
|
2506
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2507
|
+
throw new Error("--params-json 必须是一个 JSON 对象");
|
|
2508
|
+
}
|
|
2509
|
+
Object.assign(out, parsed);
|
|
2510
|
+
}
|
|
2511
|
+
return out;
|
|
2512
|
+
}
|
|
2432
2513
|
function registerOralCut(program2) {
|
|
2433
|
-
program2.command("oralcut <input>").description("智能口播剪辑闭环:上传毛片 → 云端剪辑 → 拉回 gtrk/剪映/PR 工程文件 → 打开").option("-s, --script <file>", "文稿 txt 路径(缺省走无稿智能重建)").option("-p, --preset <preset>", "节奏预设 steady|concise|compact", "concise").option("-o, --out <dir>", "工程产物目录(缺省 = <毛片同目录>/<毛片名>-video-project-<YYMMDD-HHMMSS>)").option("-f, --formats <list>", "三方格式(逗号分隔)", "gtrk,jianying,xml").option("--jianying-draft-dir <dir>", "剪映草稿根目录;传路径或 auto(默认读 gtrk init 配置 / 自动探测)").option("--reupload", "强制重新上传,忽略本地上传缓存(毛片改了但指纹意外没变时用)").option("--no-open", "完成后不自动打开产物目录(默认会自动打开,省得你找文件去哪了)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON(给 agent/脚本解析)").action(async (input, opts) => {
|
|
2514
|
+
program2.command("oralcut <input>").description("智能口播剪辑闭环:上传毛片 → 云端剪辑 → 拉回 gtrk/剪映/PR 工程文件 → 打开").option("-s, --script <file>", "文稿 txt 路径(缺省走无稿智能重建)").option("-p, --preset <preset>", "节奏预设 steady|concise|compact", "concise").option("-o, --out <dir>", "工程产物目录(缺省 = <毛片同目录>/<毛片名>-video-project-<YYMMDD-HHMMSS>)").option("-f, --formats <list>", "三方格式(逗号分隔)", "gtrk,jianying,xml").option("--jianying-draft-dir <dir>", "剪映草稿根目录;传路径或 auto(默认读 gtrk init 配置 / 自动探测)").option("--lang <code>", "语言代码(默认 zh-CN;如 en-US / ja-JP)").option("--visual-assist", "视觉兜底:ASR 漏字时用人脸/说话检测保护并重识别(剪不准/怕剪掉真内容时开)").option("--no-adaptive-rhythm", "关闭自适应节奏(默认开;关了改用固定标点停顿表)").option("--render", "额外渲染成片视频(outputs 加 video)").option("--crf <n>", "视频质量 CRF 14-28(越小越清晰/文件越大,默认 18;需配 --render)").option("--codec <c>", "视频编码(默认 h264;需配 --render)").option("--param <k=v>", "透传任意云端参数(标量、可重复;如 --param intra_gap_max=0.4 --param visual_assist=true)", collectParam, []).option("--params-json <json>", `透传任意云端参数(JSON 对象、支持嵌套;如 '{"punctuation_breaks":{"。":0.3},"render":{"crf":20}}')`).option("--reupload", "强制重新上传,忽略本地上传缓存(毛片改了但指纹意外没变时用)").option("--no-open", "完成后不自动打开产物目录(默认会自动打开,省得你找文件去哪了)").option("--json", "机读模式:人读日志转 stderr,stdout 只输出结果 JSON(给 agent/脚本解析)").action(async (input, opts) => {
|
|
2434
2515
|
await runOralCut(input, opts);
|
|
2435
2516
|
});
|
|
2436
2517
|
}
|
|
@@ -2439,17 +2520,17 @@ async function runOralCut(input, opts) {
|
|
|
2439
2520
|
routeLogsToStderr();
|
|
2440
2521
|
const cfg = loadConfig();
|
|
2441
2522
|
const inputAbs = resolve3(input);
|
|
2442
|
-
if (!
|
|
2523
|
+
if (!existsSync8(inputAbs))
|
|
2443
2524
|
throw new Error(`毛片不存在:${inputAbs}`);
|
|
2444
2525
|
const projName = basename2(inputAbs, extname(inputAbs));
|
|
2445
2526
|
const formats = opts.formats.split(",").map((s) => s.trim()).filter(Boolean);
|
|
2446
2527
|
const wantJianying = formats.some((f) => f === "jianying" || f === "capcut");
|
|
2447
|
-
const outDir = resolve3(opts.out ??
|
|
2528
|
+
const outDir = resolve3(opts.out ?? join7(dirname2(inputAbs), `${projName}-video-project-${timestamp()}`));
|
|
2448
2529
|
await mkdir2(outDir, { recursive: true });
|
|
2449
2530
|
let scriptPath = opts.script ? resolve3(opts.script) : undefined;
|
|
2450
2531
|
if (!scriptPath) {
|
|
2451
|
-
const sibling =
|
|
2452
|
-
if (
|
|
2532
|
+
const sibling = join7(dirname2(inputAbs), `${projName}.txt`);
|
|
2533
|
+
if (existsSync8(sibling)) {
|
|
2453
2534
|
scriptPath = sibling;
|
|
2454
2535
|
log.info(`自动识别到同名文稿:${sibling}(按有稿剪辑;不想用就改名或显式 --script)`);
|
|
2455
2536
|
}
|
|
@@ -2463,15 +2544,16 @@ async function runOralCut(input, opts) {
|
|
|
2463
2544
|
else
|
|
2464
2545
|
log.warn("没找到剪映草稿目录(剪映/CapCut 未装在标准位置)→ 将只产 draft_content.json、缺 meta,剪映无法直接打开。可加 --jianying-draft-dir <你的草稿目录> 重跑。");
|
|
2465
2546
|
}
|
|
2466
|
-
log.step(`▶ 智能口播剪辑:${basename2(inputAbs)}(预设 ${opts.preset},格式 ${formats.join("/")})`);
|
|
2547
|
+
log.step(`▶ 智能口播剪辑:${basename2(inputAbs)}(预设 ${opts.preset}${opts.visualAssist ? " · 视觉兜底" : ""},格式 ${formats.join("/")})`);
|
|
2548
|
+
const extraParams = parseExtraParams(opts.param, opts.paramsJson);
|
|
2467
2549
|
log.step("① 上传毛片到云端…");
|
|
2468
2550
|
let up = await uploadCached(cfg, inputAbs, { force: opts.reupload });
|
|
2469
2551
|
log.info(up.cached ? `命中上传缓存,复用 file_id = ${up.fileId}(免二次上传)` : `file_id = ${up.fileId}`);
|
|
2470
2552
|
const buildPayload = (fid) => {
|
|
2471
2553
|
const p = {
|
|
2472
2554
|
file_id: fid,
|
|
2473
|
-
la: "zh-CN",
|
|
2474
|
-
outputs: ["project"],
|
|
2555
|
+
la: opts.lang ?? "zh-CN",
|
|
2556
|
+
outputs: opts.render ? ["project", "video"] : ["project"],
|
|
2475
2557
|
project_formats: formats,
|
|
2476
2558
|
source_path: inputAbs,
|
|
2477
2559
|
rhythm_preset: opts.preset
|
|
@@ -2480,6 +2562,20 @@ async function runOralCut(input, opts) {
|
|
|
2480
2562
|
p.script = script;
|
|
2481
2563
|
if (draftDir)
|
|
2482
2564
|
p.struct_meta = { nle_draft_dir: draftDir };
|
|
2565
|
+
if (opts.visualAssist)
|
|
2566
|
+
p.visual_assist = true;
|
|
2567
|
+
if (opts.adaptiveRhythm === false)
|
|
2568
|
+
p.adaptive_rhythm = false;
|
|
2569
|
+
if (opts.render) {
|
|
2570
|
+
const r = {};
|
|
2571
|
+
if (opts.crf != null)
|
|
2572
|
+
r.crf = Number(opts.crf);
|
|
2573
|
+
if (opts.codec)
|
|
2574
|
+
r.codec = opts.codec;
|
|
2575
|
+
if (Object.keys(r).length)
|
|
2576
|
+
p.render = r;
|
|
2577
|
+
}
|
|
2578
|
+
Object.assign(p, extraParams);
|
|
2483
2579
|
return p;
|
|
2484
2580
|
};
|
|
2485
2581
|
log.step("② 提交智能口播剪辑任务…");
|
|
@@ -2508,9 +2604,9 @@ async function runOralCut(input, opts) {
|
|
|
2508
2604
|
const byFormat = {};
|
|
2509
2605
|
for (const f of files) {
|
|
2510
2606
|
const base = baseFormat(f.format);
|
|
2511
|
-
const fmtDir =
|
|
2607
|
+
const fmtDir = join7(outDir, base);
|
|
2512
2608
|
await mkdir2(fmtDir, { recursive: true });
|
|
2513
|
-
const dest =
|
|
2609
|
+
const dest = join7(fmtDir, f.filename);
|
|
2514
2610
|
await download(f.download_url, dest);
|
|
2515
2611
|
(byFormat[base] ??= []).push(dest);
|
|
2516
2612
|
log.info(`${FORMAT_META[base]?.label ?? f.format} ← ${f.filename}`);
|
|
@@ -2520,16 +2616,16 @@ async function runOralCut(input, opts) {
|
|
|
2520
2616
|
}
|
|
2521
2617
|
let jianyingDraftPath;
|
|
2522
2618
|
if (byFormat.jianying && draftDir) {
|
|
2523
|
-
jianyingDraftPath =
|
|
2619
|
+
jianyingDraftPath = join7(draftDir, basename2(outDir));
|
|
2524
2620
|
await mkdir2(jianyingDraftPath, { recursive: true });
|
|
2525
|
-
await cp(
|
|
2621
|
+
await cp(join7(outDir, "jianying"), jianyingDraftPath, { recursive: true });
|
|
2526
2622
|
log.info(`剪映草稿已落到:${jianyingDraftPath}`);
|
|
2527
2623
|
}
|
|
2528
2624
|
if (!opts.json) {
|
|
2529
2625
|
log.step("⑤ 三方打开(产物已就位,按需自取):");
|
|
2530
2626
|
for (const base of Object.keys(byFormat)) {
|
|
2531
2627
|
const meta = FORMAT_META[base];
|
|
2532
|
-
const target = base === "jianying" ? jianyingDraftPath ??
|
|
2628
|
+
const target = base === "jianying" ? jianyingDraftPath ?? join7(outDir, "jianying") : byFormat[base][0];
|
|
2533
2629
|
console.log(` • ${meta?.label ?? base}:${meta?.openHint(target) ?? target}`);
|
|
2534
2630
|
}
|
|
2535
2631
|
}
|
|
@@ -2545,6 +2641,7 @@ async function runOralCut(input, opts) {
|
|
|
2545
2641
|
outDir,
|
|
2546
2642
|
files: byFormat,
|
|
2547
2643
|
jianyingDraftPath: jianyingDraftPath ?? null,
|
|
2644
|
+
report: result.report ?? null,
|
|
2548
2645
|
errors,
|
|
2549
2646
|
taskId,
|
|
2550
2647
|
fileId: up.fileId
|
|
@@ -2552,32 +2649,14 @@ async function runOralCut(input, opts) {
|
|
|
2552
2649
|
}
|
|
2553
2650
|
}
|
|
2554
2651
|
|
|
2555
|
-
// src/commands/skills.ts
|
|
2556
|
-
import { homedir as homedir3 } from "node:os";
|
|
2557
|
-
import { join as join7 } from "node:path";
|
|
2558
|
-
import { existsSync as existsSync8, mkdirSync as mkdirSync2, copyFileSync } from "node:fs";
|
|
2559
|
-
var SKILL_NAME = "gtrk-oralcut";
|
|
2560
|
-
var SRC = join7(packageRoot(), "skills", SKILL_NAME, "SKILL.md");
|
|
2561
|
-
function registerSkills(program2) {
|
|
2562
|
-
const skills = program2.command("skills").description("管理 agent skill(安装到 Claude Code)");
|
|
2563
|
-
skills.command("install").description(`把 /${SKILL_NAME} 安装到 ~/.claude/skills(对标飞书 skills add)`).option("--dir <dir>", "自定义 skills 目录(缺省 ~/.claude/skills)").action((opts) => {
|
|
2564
|
-
if (!existsSync8(SRC))
|
|
2565
|
-
throw new Error(`找不到打包的 skill 源:${SRC}`);
|
|
2566
|
-
const base = opts.dir ?? join7(homedir3(), ".claude", "skills");
|
|
2567
|
-
const dest = join7(base, SKILL_NAME);
|
|
2568
|
-
mkdirSync2(dest, { recursive: true });
|
|
2569
|
-
copyFileSync(SRC, join7(dest, "SKILL.md"));
|
|
2570
|
-
console.log(`✅ 已安装 /${SKILL_NAME} → ${join7(dest, "SKILL.md")}`);
|
|
2571
|
-
console.log(" 在 Claude Code 里打 /gtrk-oralcut,或直接说「帮我剪个口播」即可触发(可能需重载会话)。");
|
|
2572
|
-
});
|
|
2573
|
-
}
|
|
2574
|
-
|
|
2575
2652
|
// src/index.ts
|
|
2576
2653
|
try {
|
|
2577
2654
|
process.loadEnvFile?.();
|
|
2578
2655
|
} catch {}
|
|
2656
|
+
var { version } = JSON.parse(readFileSync2(join8(packageRoot(), "package.json"), "utf8"));
|
|
2579
2657
|
var program2 = new Command;
|
|
2580
|
-
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(
|
|
2658
|
+
program2.name("gtrk").description("同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通").version(version);
|
|
2659
|
+
registerInstall(program2);
|
|
2581
2660
|
registerInit(program2);
|
|
2582
2661
|
registerOralCut(program2);
|
|
2583
2662
|
registerDoctor(program2);
|
package/package.json
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitruck/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "同合云成片流水线 CLI —— agent 驱动云端任务、产物拉回本地、三方工程文件(客户端/剪映/PR)互通。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Gitruck (同合云)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Gitruck/cli.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Gitruck/cli#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/Gitruck/cli/issues"
|
|
14
|
+
},
|
|
7
15
|
"type": "module",
|
|
8
16
|
"bin": {
|
|
9
17
|
"gtrk": "dist/index.js"
|
|
@@ -7,44 +7,102 @@ description: 智能口播剪辑闭环——把一条口播原视频(毛片)通
|
|
|
7
7
|
|
|
8
8
|
把用户的口播毛片,用 `gtrk` CLI 跑通「上传 → 云端智能口播剪辑 → 拉回三方工程文件 → 打开」,再把产物目录 + 三端打开方式回给用户。**CLI 是手、你是脑。**
|
|
9
9
|
|
|
10
|
-
>
|
|
10
|
+
> **本 skill 已含你需要的全部信息**(参数、取值、执行、读报告、排错),照它做即可,不用也无法去查外部文档。(源仓库另有一份更全的工具无关版 `AGENT.md`,那是给在仓库里开发的人看的,与你无关。)
|
|
11
11
|
|
|
12
|
-
## 前置:CLI
|
|
12
|
+
## 前置:CLI 装没装 + 体检
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
先跑 `gtrk doctor`(它顺带验证 `gtrk` 命令本身在不在):
|
|
15
|
+
- **`gtrk` 找不到 / command not found** → CLI 没装 → 让用户装 `npm i -g @gitruck/cli@latest`(需先有 Node.js),再重试。
|
|
15
16
|
- 报「缺 API Key」→ 让用户先跑一次 `gtrk init`(一次性配 Key + 剪映目录,**交互式、需真终端**,你别替他跑)。
|
|
16
17
|
- 报云端连不上 / 剪映目录没配 → 照提示让用户处理。
|
|
17
18
|
- 全绿就往下走。
|
|
18
19
|
|
|
19
20
|
## 把用户的话变成一次调用
|
|
20
21
|
|
|
22
|
+
> **每次调用都必须带 `--json`**(成功时 stdout 只出一行结果 JSON、人读日志转 stderr),你才能解析结果 + 读报告。不是可选项。
|
|
23
|
+
|
|
21
24
|
从用户消息里取这几样,缺关键的就问:
|
|
22
25
|
|
|
23
26
|
1. **毛片路径**(必需):口播原视频的本地路径。没有就问。
|
|
24
|
-
2.
|
|
25
|
-
3. **节奏**:用户说「快/紧凑/卡点狠」→ `--preset compact`;「稳/别删太多停顿」→ `--preset steady
|
|
26
|
-
4. **只要某端**:只要客户端 → `--formats gtrk`;只要剪映 → `--formats jianying
|
|
27
|
-
|
|
28
|
-
##
|
|
27
|
+
2. **文稿**:用户给了逐字稿/文字稿**文件** → `--script <txt文件路径>`(接的是 .txt 文件路径,不是稿子正文);没给则不传(CLI 会探毛片同名 `.txt`,有就自动按有稿剪、更准;无则云端无稿智能重建)。
|
|
28
|
+
3. **节奏**:用户说「快/紧凑/卡点狠」→ `--preset compact`;「稳/别删太多停顿」→ `--preset steady`;没特别说就不传(CLI 默认 `concise` 精练)。
|
|
29
|
+
4. **只要某端**:只要客户端 → `--formats gtrk`;只要剪映 → `--formats jianying`;只要 PR → `--formats xml`(全部格式见下表、逗号可多选);默认三端全给(`gtrk,jianying,xml`)。
|
|
30
|
+
|
|
31
|
+
## 完整参数规格 —— 按用户需求因势象形、自由组合
|
|
32
|
+
|
|
33
|
+
**你手上有云端全部参数(下表)。按用户的具体诉求自由决定用哪些、怎么配——需要就调、没提就跑默认(默认已是给多数人调好的)。** 只有一条要求:**名字 / 取值 / 范围照下表用**(别记错拼错);万一传越界,云端会报 `6016` 并附具体原因,照着改即可——乱传不会产出错误成片,只会明确报错。
|
|
34
|
+
|
|
35
|
+
| 用户想要 | CLI 怎么传 | 取值 · 默认 | 说明 |
|
|
36
|
+
|---|---|---|---|
|
|
37
|
+
| 有稿对齐(更准) | `--script <txt>`(或毛片同名 `.txt` 自动) | 10–50000 字 · 缺省无稿重建 | 有稿逐字对齐质量最佳;无稿智能重建终稿 |
|
|
38
|
+
| 换语种 | `--lang <码>` | `zh-CN`/`en-US`/`ja-JP`… · 默认 `zh-CN` | 不传即 CLI 默认发 `zh-CN`;主推中文,余语种尽力而为 |
|
|
39
|
+
| 产哪些工程格式 | `--formats <逗号列表>` | `xml`/`fcpxml`/`otio`/`jianying`/`capcut`/`gtrk` · CLI 缺省 `gtrk,jianying,xml` | 可多选 |
|
|
40
|
+
| 顺带渲个成片视频 | `--render` | 开关 · 缺省不渲 | 加它则额外出 mp4 成片 |
|
|
41
|
+
| 成片画质 | `--crf <n>`(配 `--render`) | 整数 14–28 · `18` | 越小越清晰 / 越大体积越小 |
|
|
42
|
+
| 成片编码 | `--codec <c>`(配 `--render`) | `h264` · `h264` | 当前仅 h264 |
|
|
43
|
+
| 节奏松紧 | `--preset <名>` | `steady`/`concise`/`compact` · 默认 `concise` | 不传即 CLI 默认发 `concise`(精练);见下方预设表,铺底后可逐项覆盖 |
|
|
44
|
+
| **剪不准 / 剪掉真内容 / 有话没剪进去** | `--visual-assist` | 开关 · 关 | **视觉找补兜底**:画面说话检测保护疑似漏识别段不被误剪并重识别捞回;不额外计费、绝不凭空生成 |
|
|
45
|
+
| 别随说话人节奏自适应 | `--no-adaptive-rhythm` | 开关 · 自适应默认开 | 关了改用固定标点停顿表 |
|
|
46
|
+
| 剪映草稿目录(非标准位置) | `--jianying-draft-dir <目录>`(或 `auto`) | 路径 · 读 init 配置 / 自动探测 | 出剪映可直开草稿需要它 |
|
|
47
|
+
|
|
48
|
+
**更细的节奏散参数**(无一等 flag,用 `--param 键=值` 或嵌套 `--params-json`):
|
|
49
|
+
|
|
50
|
+
| 参数 | 传法 | 取值 · 默认 | 说明 |
|
|
51
|
+
|---|---|---|---|
|
|
52
|
+
| `punctuation_breaks` | `--params-json '{"punctuation_breaks":{"。":0.6,",":0.3}}'` | 每标点 0–5 秒 · 见预设表 | 逐标点停顿;键只能是 `,、;:。!?—……` 和 `paragraph`(段落) |
|
|
53
|
+
| `intra_gap_max` | `--param intra_gap_max=0.4` | 0–5 · `0.35` | 句内停顿超此值算气口、触发收紧 |
|
|
54
|
+
| `intra_gap_target` | `--param intra_gap_target=0.05` | 0–5 且 <max · `0.10` | 气口收到的目标秒数 |
|
|
55
|
+
| `pad_in` / `pad_out` | `--param pad_out=0.15` | 0–5 · `0.05` / `0.08` | 保留片段头 / 尾呼吸留白 |
|
|
56
|
+
| `render.audio_crossfade_ms` | `--params-json '{"render":{"audio_crossfade_ms":12}}'` | 0–50 · `8` | 切点淡化毫秒,`0`=硬切 |
|
|
57
|
+
|
|
58
|
+
**节奏预设表 · 完整值**(选最贴内容类型的,再逐项覆盖散参数;「默认」列 = 不选预设时的基线,也即各标点的默认停顿):
|
|
59
|
+
|
|
60
|
+
| 参数(秒) | `steady` 稳健 | `concise` 精练 | `compact` 紧凑 | 默认 |
|
|
61
|
+
|---|---|---|---|---|
|
|
62
|
+
| 适用 | 讲述 / 教学 | 自媒体中长视频 | 短视频 / 广告 | 通用 |
|
|
63
|
+
| `、` / `,` / `;` | 0.18 / 0.25 / 0.36 | 0.12 / 0.18 / 0.26 | 0.06 / 0.08 / 0.12 | 0.15 / 0.20 / 0.30 |
|
|
64
|
+
| `:` / `—` | 0.45 / 0.55 | 0.32 / 0.40 | 0.15 / 0.18 | 0.35 / 0.45 |
|
|
65
|
+
| `。` `!` / `?` | 0.55 / 0.60 | 0.40 / 0.45 | 0.18 / 0.20 | 0.45 / 0.50 |
|
|
66
|
+
| `……` / `paragraph`(段落) | 0.75 / 1.20 | 0.55 / 0.90 | 0.25 / 0.40 | 0.60 / 1.00 |
|
|
67
|
+
| `intra_gap_max` / `intra_gap_target` | 0.45 / 0.15 | 0.35 / 0.10 | 0.25 / 0.05 | 0.35 / 0.10 |
|
|
68
|
+
| `pad_in` / `pad_out` | 0.05 / 0.10 | 0.05 / 0.08 | 0.03 / 0.05 | 0.05 / 0.08 |
|
|
69
|
+
|
|
70
|
+
> **因势象形举例**:「教学口播、别删太狠、句号后多顿一下」→ `--preset steady --params-json '{"punctuation_breaks":{"。":0.7}}'`;「英文短视频卡点狠、顺便出成片」→ `--lang en-US --preset compact --render`;「上次有句话没剪进去」→ `--visual-assist`。用户**自己点名**某参数+值 → 照他原样 `--param`/`--params-json` 透传(CLI 底层支持任意云端参数)。
|
|
71
|
+
>
|
|
72
|
+
> **以上就是你需要的全部参数——本 skill 自足,别去别处查**(`gtrk oralcut --help` 也随时列全部 flag)。官网另有一份面向开发者的原始 HTTP API 文档,那是给人看的,你不必也无法访问。
|
|
73
|
+
|
|
74
|
+
## 执行(每次都带 `--json`)
|
|
29
75
|
|
|
30
76
|
```bash
|
|
31
|
-
gtrk oralcut "<毛片绝对路径>" [--script "
|
|
77
|
+
gtrk oralcut "<毛片绝对路径>" [--script "<txt路径>"] [--preset steady|concise|compact] [其它参数…] --json
|
|
32
78
|
```
|
|
33
79
|
|
|
34
|
-
- `--json`:人读日志走 stderr
|
|
35
|
-
`{ ok, outDir, files:{gtrk:[],jianying:[],xml:[]}, jianyingDraftPath, errors, taskId, fileId }`
|
|
36
|
-
-
|
|
80
|
+
- `--json`:人读日志走 stderr,**成功时 stdout 只有一行结果 JSON**:
|
|
81
|
+
`{ ok, outDir, files:{gtrk:[],jianying:[],xml:[]}, jianyingDraftPath, report, errors, taskId, fileId }`
|
|
82
|
+
- **命令失败**(毛片路径错 / 鉴权失败 / 参数越界 6016 等)→ **进程非 0 退出、报错打到 stderr、stdout 无 JSON**。先看退出码,非 0 就把 stderr 的报错如实回给用户、别当成功。
|
|
83
|
+
- 默认会自动打开产物目录(用户能直接看到文件);纯脚本/无头环境才加 `--no-open`。
|
|
37
84
|
- 云端处理是分钟级,耐心等命令返回。
|
|
38
85
|
|
|
39
|
-
##
|
|
86
|
+
## 跑完读报告、给用户交代、按需调整(因势象形的另一半)
|
|
87
|
+
|
|
88
|
+
`--json` 的 stdout 那行带 **`report`**(云端返回的原始剪辑报告对象,字段按需读、个别可能缺,读前判空)——读它,别只回一句"剪好了":
|
|
89
|
+
|
|
90
|
+
- `duration_before` → `after`:剪了多少(如 114.7s → 68.6s)。
|
|
91
|
+
- `dropped[]`:剔掉了哪些段(`reason`:`retake` 重读 / `misread` 口误)——可挑几条告诉用户「去掉了这些重录 / 口误」。
|
|
92
|
+
- `coverage`(0–1):终稿覆盖率。**低于 0.6(附 `low_coverage`)= 文稿与实拍严重不符** → 提醒用户核对文稿、或考虑无稿重跑。
|
|
93
|
+
- `uncovered_script[]`:**文稿里有、实拍没找到的段(漏读)** → 如实告诉用户「『xx』这段没在录像里找到」;疑似是漏识别而非真漏读时,建议加 `--visual-assist` 重跑。
|
|
94
|
+
- `review_points[]`:建议人工复核处(识别置信度偏低)。
|
|
95
|
+
- `adaptive_rhythm` / `analysis_degraded`:自适应是否生效 / 是否降级到机械规则。
|
|
96
|
+
- 开了 `--visual-assist`:`suspect_omissions` / `stt_recovered`(找回了哪些字)/ `visual_assist_degraded`。
|
|
97
|
+
- `ok=false` 或 `errors` 非空:某格式没出来 → 如实说哪个、为什么。
|
|
98
|
+
|
|
99
|
+
**据此因势象形调整**(同一毛片可反复剪对比):覆盖率低 / 漏读多 → 开 `--visual-assist` 或核对文稿重跑;节奏不满意 → 调 `--preset` / 散参数重跑;某格式失败 → 补 `--jianying-draft-dir` / `--reupload` 重跑。
|
|
40
100
|
|
|
41
|
-
|
|
42
|
-
- `ok=false` 或 `errors` 非空 → 如实告诉用户哪个格式没出来、为什么;必要时按用户意图微调重跑(换 `--preset`、补 `--jianying-draft-dir`、加 `--reupload`)。
|
|
43
|
-
- 确认 `files.gtrk[0]` 存在;要剪映就确认 `jianyingDraftPath` 非空。
|
|
101
|
+
跑完确认 `files.gtrk[0]` 在;要剪映确认 `jianyingDraftPath` 非空。
|
|
44
102
|
|
|
45
103
|
## 回给用户(三端打开方式)
|
|
46
104
|
|
|
47
|
-
-
|
|
105
|
+
- **客户端**:在同合云桌面客户端(OpenCut Gitruck Edition)里「打开工程」选 `files.gtrk[0]`(…/gtrk/project.gtrk)。
|
|
48
106
|
- **剪映**:已自动落到剪映草稿目录(`jianyingDraftPath`),开剪映在项目列表里找**同名工程**即可。
|
|
49
107
|
- **PR/FCP**:Premiere 里「文件 > 导入」`files.xml[0]`(…/xml/premiere.xml)。
|
|
50
108
|
|