yuanflow-cli 0.1.47 → 0.1.48

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/README.md CHANGED
@@ -50,15 +50,15 @@ yuanflow-cli browser profile-path --platform douyin --account main --format agen
50
50
  yuanflow-cli browser task-plan --platform xiaohongshu --task publish --account main --format agent-json
51
51
  yuanflow-cli ai qwen3-vl-plus --prompt "描述这张图" --image-url "https://example.com/image.png" --format agent-json
52
52
  yuanflow-cli ai qwen3-vl-plus --prompt "总结这个视频画面" --video-url "https://example.com/video.mp4" --format agent-json
53
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\素材\demo.mp4" --format agent-json
54
- yuanflow-cli voice clone --file-transfer "D:\voice\sample.wav" --name "我的声音" --activate --format agent-json
53
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "<本地视频路径>" --format agent-json
54
+ yuanflow-cli voice clone --file-transfer "<本地音频路径>" --name "我的声音" --activate --format agent-json
55
55
  yuanflow-cli voice list --format agent-json
56
56
  yuanflow-cli voice activate --voice voice_xxx --format agent-json
57
- yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voice voice_xxx --output "D:\voice\replicate.mp3" --format agent-json
57
+ yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voice voice_xxx --output "<输出音频路径>" --format agent-json
58
58
  yuanflow-cli ai doubao-tts voices --format agent-json
59
59
  yuanflow-cli ai doubao-tts voice --voice zh_female_xiaohe_uranus_bigtts --format agent-json
60
- yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\preview.mp3" --format agent-json
61
- yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\doubao.mp3" --format agent-json
60
+ yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
61
+ yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
62
62
  yuanflow-cli ai fun-asr --audio-url "https://example.com/audio.wav" --response-format verbose_json --format agent-json
63
63
  yuanflow-cli list douyin
64
64
  ```
@@ -77,7 +77,10 @@ token 优先级:`--token` > `YUANCHUANG_API_TOKEN` > 本地 `config.token`。
77
77
 
78
78
  ```bash
79
79
  # 创建声音克隆:本地音频先走 YuanFlow 文件中转
80
- yuanflow-cli voice clone --file-transfer "D:\voice\sample.wav" --name "我的声音" --activate --format agent-json
80
+ yuanflow-cli voice clone --file-transfer "<本地音频路径>" --name "我的声音" --activate --format agent-json
81
+
82
+ # 创建声音克隆:本地音频 multipart 直接提交给 YuanFlow API
83
+ yuanflow-cli voice clone --file "<本地音频路径>" --name "我的声音" --activate --format agent-json
81
84
 
82
85
  # 查询已有声音克隆 ID
83
86
  yuanflow-cli voice list --format agent-json
@@ -86,17 +89,17 @@ yuanflow-cli voice list --format agent-json
86
89
  yuanflow-cli voice activate --voice voice_xxx --format agent-json
87
90
 
88
91
  # 使用克隆音色生成复刻音频
89
- yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voice voice_xxx --output "D:\voice\replicate.mp3" --format agent-json
92
+ yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voice voice_xxx --output "<输出音频路径>" --format agent-json
90
93
  ```
91
94
 
92
95
  命令清单:
93
96
 
94
- - `voice clone`:`POST /v1/audio/voices`,创建声音克隆,常用参数 `--file`、`--file-transfer`、`--audio-url`、`--name`、`--activate`。
97
+ - `voice clone`:`POST /v1/audio/voices`,创建声音克隆,默认使用 `voice-enrollment` 并绑定后续合成模型 `cosyvoice-v3-flash`,常用参数 `--file`、`--file-transfer`、`--audio-url`、`--name`、`--language-hints`、`--activate`。
95
98
  - `voice list`:`GET /v1/audio/voices`,查询当前用户已有声音克隆音色 ID。
96
99
  - `voice activate`:`POST /v1/audio/voices/{voice_xxx}/activate`,把已有声音克隆设为默认音色。
97
100
  - `voice replicate`:`POST /v1/audio/speech`,使用 `voice_xxx` 或 `default` 生成复刻音频,真实调用时必须传 `--output`。
98
101
 
99
- `--file-transfer` 会先通过 YuanFlow 文件中转上传本地音频,再把临时访问链接提交给 YuanFlow API。`--file` 保留为直接提交本地音频的兼容方式。`--file`、`--file-transfer` 和 `--audio-url` 只能选择一个。
102
+ `--file-transfer` 会先通过 YuanFlow 文件中转上传本地音频,再把临时访问链接作为 `audio_url` 提交给 YuanFlow API。`--file` 会通过 multipart 直接提交本地音频,由 YuanFlow API 处理临时访问链接。`--file`、`--file-transfer` 和 `--audio-url` 只能选择一个。
100
103
 
101
104
  ### AI 模型命令
102
105
 
@@ -106,13 +109,15 @@ yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voi
106
109
  yuanflow-cli ai help
107
110
  yuanflow-cli ai qwen3-vl-plus --prompt "描述这张图" --image-url "https://example.com/image.png" --format agent-json
108
111
  yuanflow-cli ai qwen3-vl-plus --prompt "总结这个视频画面" --video-url "https://example.com/video.mp4" --format agent-json
109
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "D:\素材\cover.png" --format agent-json
110
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\素材\demo.mp4" --format agent-json
112
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "<本地图片路径>" --format agent-json
113
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "<本地视频路径>" --format agent-json
114
+ yuanflow-cli ai voice-enrollment --file "<本地音频路径>" --name "我的声音" --activate --format agent-json
115
+ yuanflow-cli ai cosyvoice-v3-flash --text "你好,这是声音复刻测试。" --voice voice_xxx --output "<输出音频路径>" --format agent-json
111
116
  yuanflow-cli ai fun-asr --audio-url "https://example.com/audio.wav" --response-format verbose_json --format agent-json
112
117
  yuanflow-cli ai doubao-tts voices --format agent-json
113
118
  yuanflow-cli ai doubao-tts voice --voice zh_female_xiaohe_uranus_bigtts --format agent-json
114
- yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\preview.mp3" --format agent-json
115
- yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\doubao.mp3" --format agent-json
119
+ yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
120
+ yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
116
121
  ```
117
122
 
118
123
  #### qwen3-vl-plus 视觉理解
@@ -127,10 +132,10 @@ yuanflow-cli ai qwen3-vl-plus --prompt "描述这张图" --image-url "https://ex
127
132
  yuanflow-cli ai qwen3-vl-plus --prompt "总结这个视频画面" --video-url "https://example.com/video.mp4" --format agent-json
128
133
 
129
134
  # 本地图片:先上传到 YuanFlow 文件中转,再把临时访问链接传给 YuanFlow API
130
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "D:\素材\cover.png" --format agent-json
135
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "<本地图片路径>" --format agent-json
131
136
 
132
137
  # 本地视频:先上传到 YuanFlow 文件中转,再把临时访问链接传给 YuanFlow API
133
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\素材\demo.mp4" --format agent-json
138
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "<本地视频路径>" --format agent-json
134
139
  ```
135
140
 
136
141
  本地文件上传要求:
@@ -149,6 +154,8 @@ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\素
149
154
  命令清单:
150
155
 
151
156
  - `ai qwen3-vl-plus`:`POST /v1/chat/completions`,文本/图片/视频理解,常用参数 `--prompt`、`--image-url`、`--video-url`、`--image-file`、`--video-file`。
157
+ - `ai voice-enrollment`:`POST /v1/audio/voices`,创建声音克隆音色,返回 `voice_xxx`。
158
+ - `ai cosyvoice-v3-flash`:`POST /v1/audio/speech`,使用 `voice_xxx` 或 `default` 合成复刻音频。
152
159
  - `ai fun-asr`:`POST /v1/audio/transcriptions`,语音识别,`--audio-url` 适合远程音频,`--file` 适合本地音频直传。
153
160
  - `ai doubao-tts`:`POST /v1/audio/speech`,豆包语音合成,`--voice` 直接传豆包官方音色 ID。
154
161
  - `ai doubao-tts voices`:`GET /api/voice-assets/doubao/voices`,查询 doubao-tts 可用音色列表。
@@ -171,10 +178,10 @@ yuanflow-cli ai doubao-tts voices --format agent-json
171
178
  yuanflow-cli ai doubao-tts voice --voice zh_female_xiaohe_uranus_bigtts --format agent-json
172
179
 
173
180
  # 3. 下载该音色的试听音频
174
- yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\preview-xiaohe.mp3" --format agent-json
181
+ yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
175
182
 
176
183
  # 4. 使用同一个 voice_type 进行真实语音合成
177
- yuanflow-cli ai doubao-tts --text "你好,这是豆包语音合成测试。" --voice zh_female_xiaohe_uranus_bigtts --output "D:\voice\doubao-xiaohe.mp3" --format agent-json
184
+ yuanflow-cli ai doubao-tts --text "你好,这是豆包语音合成测试。" --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --format agent-json
178
185
  ```
179
186
 
180
187
  音色列表返回的关键字段:
@@ -298,11 +305,7 @@ Agent 应先查看 `knowledge docs`,再把用户需求整理成 `task_frame`
298
305
 
299
306
  ### 视频制作能力迁移
300
307
 
301
- 视频智能剪辑、主音频+B-roll、EDL 校验和基础渲染链路已迁移到独立项目维护,不再作为 `yuanflow-cli video` 命令提供。独立开发目录:
302
-
303
- ```text
304
- D:\AI_project\视频制作
305
- ```
308
+ 视频智能剪辑、主音频+B-roll、EDL 校验和基础渲染链路已迁移到独立项目维护,不再作为 `yuanflow-cli video` 命令提供。
306
309
 
307
310
  `YuanFlow-cli` 仍保留通用视频理解、视频拆解、音视频在线转文字、作品下载和知识库查询等通用能力;视频制作项目后续按需调用这些通用能力。
308
311
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yuanflow-cli",
3
- "version": "0.1.47",
3
+ "version": "0.1.48",
4
4
  "description": "YuanFlow 自媒体 API CLI 与 Skill 安装器。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -15,33 +15,54 @@ emoji: 🎙️
15
15
  1. 先确认本机可执行 `yuanflow-cli --help`。
16
16
  2. 外部 CLI 使用 `YUANCHUANG_API_TOKEN` 或 `yuanflow-cli config set-token <你的令牌>` 完成鉴权。
17
17
  3. 选择音频输入方式:
18
- - 本地音频直接提交:`--file`
19
- - 本地音频先走 YuanFlow 文件中转:`--file-transfer`
20
- - 已有公网音频链接:`--audio-url`
18
+ - 本地音频优先先调用 `YuanFlow文件中转工具`,用 `oss temp-upload` 上传并取得临时访问 URL,再通过 `--audio-url` 提交。
19
+ - 已有公网音频链接或上一步取得的临时访问 URL:`--audio-url`
20
+ - 兼容快捷方式:`--file-transfer` 会在声音克隆命令内部先走 YuanFlow 文件中转,再把临时访问链接作为 `audio_url` 提交。
21
+ - 只有用户明确要求直接传本地文件时,才使用 multipart:`--file`
21
22
  4. 创建成功后,保存返回的 `voice_xxx`。这是后续声音复刻必须使用的音色 ID。
22
23
 
23
24
  不要在回复、日志或文件中暴露 token。用户主流程统一称为 YuanFlow API 和 YuanFlow 文件中转,不要求用户配置第三方平台 Key。
24
25
 
25
26
  ## 创建声音克隆
26
27
 
27
- 推荐本地文件使用 YuanFlow 文件中转:
28
+ 本地文件优先先使用 `YuanFlow文件中转工具` 获取临时 URL,再创建声音克隆:
28
29
 
29
30
  ```powershell
30
- yuanflow-cli voice clone --file-transfer "<本地声音样本路径>" --name "我的声音" --activate --format agent-json
31
+ yuanflow-cli oss temp-upload --file "<本地声音样本路径>" --content-type audio/wav --format agent-json
31
32
  ```
32
33
 
33
- 如果用户明确希望直接提交本地音频:
34
+ 从返回结果中读取临时访问链接:
35
+
36
+ ```text
37
+ data.response.data.signed_url
38
+ ```
39
+
40
+ 再把临时访问链接作为 `audio_url` 提交:
34
41
 
35
42
  ```powershell
36
- yuanflow-cli voice clone --file "<本地声音样本路径>" --name "我的声音" --activate --format agent-json
43
+ yuanflow-cli voice clone --audio-url "<临时访问URL>" --name "我的声音" --language-hints zh --activate --format agent-json
44
+ ```
45
+
46
+ 如果当前执行环境需要一步完成,也可以使用兼容快捷方式:
47
+
48
+ ```powershell
49
+ yuanflow-cli voice clone --file-transfer "<本地声音样本路径>" --name "我的声音" --language-hints zh --activate --format agent-json
50
+ ```
51
+
52
+ 如果用户明确希望直接提交本地音频,使用 multipart:
53
+
54
+ ```powershell
55
+ yuanflow-cli voice clone --file "<本地声音样本路径>" --name "我的声音" --language-hints zh --activate --format agent-json
37
56
  ```
38
57
 
39
58
  如果已经有可访问音频 URL:
40
59
 
41
60
  ```powershell
42
- yuanflow-cli voice clone --audio-url "https://example.com/sample.wav" --name "我的声音" --activate --format agent-json
61
+ yuanflow-cli voice clone --audio-url "https://example.com/sample.wav" --name "我的声音" --language-hints zh --activate --format agent-json
43
62
  ```
44
63
 
64
+ 默认创建模型为 `voice-enrollment`,默认绑定后续合成模型 `cosyvoice-v3-flash`。除非用户明确要求其它 YuanFlow API 对外模型参数,一般不要改 `--target-model`。
65
+
45
66
  返回里重点读取:
46
67
 
47
68
  ```text
@@ -83,6 +104,7 @@ yuanflow-cli voice activate --voice voice_xxx --format agent-json
83
104
  - 优先使用清晰、无背景音乐、无明显混响的人声音频。
84
105
  - 建议使用 wav、mp3、m4a、flac 等常见格式。
85
106
  - 文件过大、噪音明显或多人混说时,先让用户换音频或裁剪。
107
+ - 样本语言建议用 `--language-hints zh` 这类提示传入;多语种用逗号分隔。
86
108
  - 不要上传身份证、合同、私密通话、未授权人物声音等敏感内容,除非用户明确确认且任务确实需要。
87
109
 
88
110
  ## YuanFlow-main 内置环境
@@ -91,15 +113,35 @@ yuanflow-cli voice activate --voice voice_xxx --format agent-json
91
113
 
92
114
  创建声音克隆:
93
115
 
116
+ ```json
117
+ {
118
+ "args": [
119
+ "oss",
120
+ "temp-upload",
121
+ "--file",
122
+ "<本地声音样本路径>",
123
+ "--content-type",
124
+ "audio/wav",
125
+ "--format",
126
+ "agent-json"
127
+ ],
128
+ "timeout": 300
129
+ }
130
+ ```
131
+
132
+ 拿到 `data.response.data.signed_url` 后,再提交声音克隆:
133
+
94
134
  ```json
95
135
  {
96
136
  "args": [
97
137
  "voice",
98
138
  "clone",
99
- "--file-transfer",
100
- "<本地声音样本路径>",
139
+ "--audio-url",
140
+ "<临时访问URL>",
101
141
  "--name",
102
142
  "我的声音",
143
+ "--language-hints",
144
+ "zh",
103
145
  "--activate",
104
146
  "--format",
105
147
  "agent-json"
@@ -143,4 +185,4 @@ yuanflow-cli voice activate --voice voice_xxx --format agent-json
143
185
  - token 缺失:说明需要配置 YuanFlow API token,或在 YuanFlow-main 受控环境中运行。
144
186
  - 没有返回 `voice_xxx`:说明声音克隆未完成,不要继续声音复刻。
145
187
  - 已存在克隆限制:先用 `voice list` 查询已有音色,必要时让用户选择已有 `voice_xxx` 继续。
146
- - 音频不可访问:如果使用 URL,让用户换成可访问链接;如果是本地文件,改用 `--file-transfer`。
188
+ - 音频不可访问:如果使用 URL,让用户换成可访问链接;如果是本地文件,优先调用 `YuanFlow文件中转工具` 获取新的临时 URL,再用 `--audio-url` 重试。
@@ -14,6 +14,8 @@ emoji: 🔊
14
14
 
15
15
  如果没有声音克隆 ID,先调用 `声音克隆`,不要直接复刻。
16
16
 
17
+ 当前声音复刻使用 YuanFlow API 对外模型参数 `cosyvoice-v3-flash`。Agent 日常优先使用聚合命令 `yuanflow-cli voice replicate`,不要在公共流程里要求用户理解底层接口细节。
18
+
17
19
  ## 外部 CLI 主流程
18
20
 
19
21
  外部 Agent 或用户直接使用时,优先使用 `yuanflow-cli voice replicate`。
@@ -22,6 +24,7 @@ emoji: 🔊
22
24
  2. 外部 CLI 使用 `YUANCHUANG_API_TOKEN` 或 `yuanflow-cli config set-token <你的令牌>` 完成鉴权。
23
25
  3. 确认 `--voice` 是 `voice_xxx` 或 `default`。
24
26
  4. 用 `--output` 指定音频保存路径。
27
+ 5. 如果链路里需要上传本地参考音频,不要直接交给复刻命令;先调用 `YuanFlow文件中转工具` 的 `oss temp-upload` 获取临时 URL,再转到 `声音克隆` 使用 `--audio-url` 创建或确认 `voice_xxx`。
25
28
 
26
29
  不要在回复、日志或文件中暴露 token。用户主流程统一称为 YuanFlow API,不要求用户配置第三方平台 Key。
27
30
 
@@ -35,6 +38,28 @@ yuanflow-cli voice list --format agent-json
35
38
 
36
39
  如果列表中没有可用音色,转到 `声音克隆` Skill,先创建声音克隆。
37
40
 
41
+ ## 本地文件处理
42
+
43
+ `voice replicate` 只接收文本和已有音色 ID,不接收本地参考音频。遇到本地音频文件时,优先按下面链路处理:
44
+
45
+ ```powershell
46
+ yuanflow-cli oss temp-upload --file "<本地声音样本路径>" --content-type audio/wav --format agent-json
47
+ ```
48
+
49
+ 从返回结果读取临时访问链接:
50
+
51
+ ```text
52
+ data.response.data.signed_url
53
+ ```
54
+
55
+ 再调用 `声音克隆` 创建或确认音色:
56
+
57
+ ```powershell
58
+ yuanflow-cli voice clone --audio-url "<临时访问URL>" --name "我的声音" --language-hints zh --activate --format agent-json
59
+ ```
60
+
61
+ 拿到 `voice_xxx` 后,再执行下面的声音复刻。
62
+
38
63
  ## 生成复刻音频
39
64
 
40
65
  ```powershell
@@ -53,6 +78,18 @@ yuanflow-cli voice replicate --text "你好,这是声音复刻测试。" --voi
53
78
  yuanflow-cli voice replicate --text "测试内容" --voice voice_xxx --response-format wav --output "<输出音频路径>" --format agent-json
54
79
  ```
55
80
 
81
+ 常用控制参数:
82
+
83
+ - `--instructions`:语气、情绪、角色等朗读要求。
84
+ - `--sample-rate`:采样率,默认按 YuanFlow API 处理,常用 `24000`。
85
+ - `--rate`:语速。
86
+ - `--volume`:音量。
87
+ - `--pitch`:音调。
88
+ - `--language`:目标合成语言,例如 `zh`。
89
+ - `--language-hints`:目标合成语言提示,例如 `zh`。
90
+ - `--enable-ssml`:需要 SSML 时开启。
91
+ - `--word-timestamp-enabled`:需要字级时间戳时开启。
92
+
56
93
  ## 输出要求
57
94
 
58
95
  最终回复给用户时说明:
@@ -84,6 +121,8 @@ data.response.content_type
84
121
  "你好,这是声音复刻测试。",
85
122
  "--voice",
86
123
  "voice_xxx",
124
+ "--language",
125
+ "zh",
87
126
  "--output",
88
127
  "replicate.mp3",
89
128
  "--format",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: 视频拆解
3
- description: 当用户提交对标视频、本地视频、视频 URL,要求做自媒体创作方向的视频拆解、爆款结构分析、内容复盘、脚本拆解、镜头节奏拆解、账号/竞品内容学习时使用。开始前必须先使用“自媒体知识库”查询与对标拆解相关的规则,再通过 yuanflow-cli qwen3-vl-plus 上传/理解视频,并把知识库规则放入提示词中指导拆解。
3
+ description: 当用户提交对标视频、本地视频、视频 URL,要求做自媒体创作方向的视频拆解、爆款结构分析、内容复盘、脚本拆解、镜头节奏拆解、视频风格解析、账号/竞品内容学习时使用。开始前必须先查自媒体知识库;平台 URL 还要尽量获取作品详情和视频文件,再用 qwen3-vl-plus 规则化拆解。
4
4
  emoji: 🎬
5
5
  ---
6
6
 
@@ -11,19 +11,20 @@ emoji: 🎬
11
11
  核心链路:
12
12
 
13
13
  1. 先查 `自媒体知识库`,获取与“对标拆解、视频拆解、内容创作、脚本结构、镜头节奏、爆款复盘”相关的规则摘要。
14
- 2. 如果用户给的是视频 URL,先用相关工具解析并保存到本地。
15
- 3. 使用 `yuanflow-cli ai qwen3-vl-plus --video-file` 提交本地视频。
16
- 4. 把知识库拆解规则写进 prompt,让模型按规则拆解视频。
17
- 5. 整理成适合自媒体创作复用的详细拆解报告。
14
+ 2. 如果用户给的是平台视频 URL,先获取作品详情,尽量拿到标题、文案、封面、作者、发布时间、点赞/评论/转发/收藏等互动数据。
15
+ 3. 如果需要分析视频画面,再用下载/播放相关工具解析并保存到本地。
16
+ 4. 使用 `yuanflow-cli ai qwen3-vl-plus --video-file` 提交本地视频。
17
+ 5. 把知识库规则、作品详情摘要和用户目标写进 prompt,让模型按规则拆解视频。
18
+ 6. 合并“平台元数据 + 视觉理解结果 + 知识库规则”,整理成适合自媒体创作复用的详细拆解报告。
18
19
 
19
20
  ## 外部 CLI 主流程
20
21
 
21
- 外部 Agent 或用户直接使用时,先用 `yuanflow-cli knowledge ...` 查询拆解规则,再用 `yuanflow-cli ai qwen3-vl-plus` 处理视频。
22
+ 外部 Agent 或用户直接使用时,先用 `yuanflow-cli knowledge ...` 查询拆解规则;如果输入是平台作品 URL,再用 `yuanflow-cli works detail` 获取作品详情,用 `yuanflow-cli works download` 获取可播放/下载地址;最后用 `yuanflow-cli ai qwen3-vl-plus` 处理本地视频。
22
23
 
23
24
  1. 先确认本机可执行 `yuanflow-cli --help`。
24
25
  2. 外部 CLI 使用 `YUANCHUANG_API_TOKEN` 或 `yuanflow-cli config set-token <你的令牌>` 完成鉴权。
25
26
  3. 本地视频会先经过 YuanFlow 文件中转,再提交给 YuanFlow API。
26
- 4. 如果是平台视频 URL,先解析并在用户授权后保存到本地文件。
27
+ 4. 如果是平台视频 URL,先获取作品详情;需要画面拆解时,再解析并在用户授权后保存到本地文件。
27
28
 
28
29
  本地视频上传依赖 qwen3-vl-plus 的 YuanFlow 文件中转能力。外部 Agent 只需要配置 YuanFlow API token;不要要求用户提供第三方平台 Key。
29
30
 
@@ -74,7 +75,42 @@ YuanFlow-main 内置环境调用见后文专属小节。
74
75
  - 账号定位
75
76
  - 带货/转化结构
76
77
 
77
- ### 3. 视频 URL 先解析并保存本地
78
+ ### 3. 平台视频 URL 先获取作品详情
79
+
80
+ 如果用户提交的是平台视频 URL,先用 `作品详情获取工具` 或 `yuanflow-cli works detail` 获取平台元数据。它负责标题、文案/描述、封面、作者、发布时间、互动统计和媒体信息;这些内容不要让视觉模型凭画面猜。
81
+
82
+ 先按链接判断平台:
83
+
84
+ - `douyin.com`、`v.douyin.com`:`--platform douyin`。
85
+ - `xiaohongshu.com`、`xhslink.com`:`--platform xiaohongshu`,如详情接口要求 `xsec_token`,按返回或用户提供信息补充 `--xsec-token`。
86
+ - `bilibili.com`、`b23.tv`、`BV`:`--platform bilibili`。
87
+ - `youtube.com`、`youtu.be`:`--platform youtube`。
88
+ - `tiktok.com`、`vm.tiktok.com`:`--platform tiktok`。
89
+ - `kuaishou.com`:`--platform kuaishou`。
90
+ - `xigua.com`:`--platform xigua`。
91
+
92
+ 外部 CLI 示例:
93
+
94
+ ```powershell
95
+ yuanflow-cli works detail --platform douyin --target "https://v.douyin.com/xxx/" --format agent-json
96
+ ```
97
+
98
+ 拿到详情后,先提炼成“作品详情摘要”,供后续 prompt 使用:
99
+
100
+ ```text
101
+ 【作品详情摘要】
102
+ - 标题:
103
+ - 文案/描述:
104
+ - 作者:
105
+ - 发布时间:
106
+ - 封面:
107
+ - 点赞/评论/转发/收藏/播放:
108
+ - 其它可确认媒体信息:
109
+ ```
110
+
111
+ 如果详情接口没有返回某些字段,最终报告里写“未返回/未确认”,不要编造。
112
+
113
+ ### 4. 视频 URL 再解析并保存本地
78
114
 
79
115
  如果用户提交的是平台视频 URL,先用 `作品下载综合工具` 或 `yuanflow-cli works download` 解析可播放/下载地址候选。
80
116
 
@@ -85,6 +121,8 @@ YuanFlow-main 内置环境调用见后文专属小节。
85
121
  - `youtube.com`、`youtu.be`:`--platform youtube`。
86
122
  - `ixigua.com`:`--platform xigua`。
87
123
 
124
+ 注意:`works detail` 支持的平台更多,`works download` 当前只覆盖可播放/下载地址已接入的平台。详情能拿到不代表一定能直接下载视频;下载失败时,让用户提供本地视频或可直接访问的视频文件。
125
+
88
126
  外部 CLI 示例:
89
127
 
90
128
  ```powershell
@@ -121,21 +159,66 @@ ffmpeg -y -i "<解析得到的 m3u8 地址>" -c copy "<视频拆解工作目录>
121
159
 
122
160
  如果平台解析只返回播放候选而没有稳定下载 URL,说明当前链接需要用户提供可下载文件,或先让用户使用其它授权方式保存视频。
123
161
 
124
- ### 4. 调用 qwen3-vl-plus 做规则化拆解
162
+ ### 5. 调用 qwen3-vl-plus 做规则化拆解
125
163
 
126
- 把知识库查询到的规则摘要压缩进 prompt。不要只让模型“分析视频”,必须告诉它按规则拆解。
164
+ 把知识库查询到的规则摘要、作品详情摘要和用户目标压缩进 prompt。不要只让模型“分析视频”,必须告诉它按规则拆解,也要明确哪些字段来自平台详情、哪些内容来自视频画面理解。
127
165
 
128
166
  外部 CLI 示例:
129
167
 
130
168
  ```powershell
131
- yuanflow-cli ai qwen3-vl-plus --prompt "你是自媒体视频拆解助手。请按知识库规则拆解这个视频:..." --video-file "<本地视频路径>" --format agent-json
169
+ yuanflow-cli ai qwen3-vl-plus --prompt "你是自媒体视频拆解助手。请按知识库规则和作品详情摘要拆解这个视频:..." --video-file "<本地视频路径>" --format agent-json
132
170
  ```
133
171
 
134
172
  ## YuanFlow-main 内置环境
135
173
 
136
174
  在 YuanFlow-main 内置环境,优先调用受控工具 `yuanflow_cli_call`。token、受管包路径和输出目录由 YuanFlow-main 管理,不要要求用户手动提供 YuanFlow token。
137
175
 
138
- YuanFlow-main 内置工具示例:
176
+ 内置环境应按阶段调用:
177
+
178
+ 1. `knowledge entry/packs/rules`:查询拆解规则。
179
+ 2. `works detail`:平台 URL 获取标题、文案、封面和互动数据。
180
+ 3. `works download`:需要画面拆解时获取可播放/下载地址并保存本地。
181
+ 4. `ai qwen3-vl-plus`:提交本地视频做视觉与风格拆解。
182
+
183
+ YuanFlow-main 作品详情调用示例:
184
+
185
+ ```json
186
+ {
187
+ "args": [
188
+ "works",
189
+ "detail",
190
+ "--platform",
191
+ "douyin",
192
+ "--target",
193
+ "https://v.douyin.com/xxx/",
194
+ "--format",
195
+ "agent-json"
196
+ ],
197
+ "timeout": 180
198
+ }
199
+ ```
200
+
201
+ YuanFlow-main 下载/播放地址调用示例:
202
+
203
+ ```json
204
+ {
205
+ "args": [
206
+ "works",
207
+ "download",
208
+ "--platform",
209
+ "douyin",
210
+ "--target",
211
+ "https://v.douyin.com/xxx/",
212
+ "--region",
213
+ "CN",
214
+ "--format",
215
+ "agent-json"
216
+ ],
217
+ "timeout": 300
218
+ }
219
+ ```
220
+
221
+ YuanFlow-main 视频拆解调用示例:
139
222
 
140
223
  ```json
141
224
  {
@@ -143,7 +226,7 @@ YuanFlow-main 内置工具示例:
143
226
  "ai",
144
227
  "qwen3-vl-plus",
145
228
  "--prompt",
146
- "你是自媒体视频拆解助手。请严格按以下知识库规则拆解这个对标视频:\\n【知识库规则摘要】...\\n【用户目标】学习该视频的选题、开头、脚本结构、镜头节奏和可复用创作方法。\\n请输出:1. 一句话总结;2. 视频定位;3. 开头钩子;4. 内容结构;5. 镜头/画面节奏;6. 情绪和转化设计;7. 可复用模板;8. 不建议照搬的风险。",
229
+ "你是自媒体视频拆解助手。请严格按以下知识库规则和作品详情摘要拆解这个对标视频:\\n【知识库规则摘要】...\\n【作品详情摘要】标题、文案、封面、作者、发布时间、互动数据...\\n【用户目标】学习该视频的选题、开头、脚本结构、镜头节奏、视频风格和可复用创作方法。\\n请区分平台详情可确认字段和视频画面分析结论,不要编造未返回的数据。",
147
230
  "--video-file",
148
231
  "<本地视频路径>",
149
232
  "--format",
@@ -169,43 +252,71 @@ YuanFlow-main 内置工具示例:
169
252
 
170
253
  ## 1. 一句话结论
171
254
 
172
- ## 2. 视频基础判断
255
+ ## 2. 视频基础信息与数据表现
256
+ - 标题:
257
+ - 文案/描述:
258
+ - 作者/账号:
259
+ - 发布时间:
260
+ - 封面:
261
+ - 点赞/评论/转发/收藏/播放:
262
+ - 数据初步判断:
263
+ - 未返回或不确定字段:
264
+
265
+ ## 3. 视频基础判断
173
266
  - 内容类型:
174
267
  - 目标受众:
175
268
  - 核心卖点/观点:
176
269
  - 适用平台:
177
270
 
178
- ## 3. 开头钩子拆解
271
+ ## 4. 封面、标题与文案拆解
272
+ - 封面吸引点:
273
+ - 标题钩子:
274
+ - 文案结构:
275
+ - 标题/文案/封面的匹配度:
276
+
277
+ ## 5. 开头钩子拆解
179
278
  - 前 3 秒:
180
279
  - 冲突/利益点:
181
280
  - 留人方式:
182
281
 
183
- ## 4. 内容结构拆解
282
+ ## 6. 内容结构拆解
184
283
  - 段落 1:
185
284
  - 段落 2:
186
285
  - 段落 3:
187
286
  - 结尾:
188
287
 
189
- ## 5. 画面与镜头节奏
288
+ ## 7. 画面与镜头节奏
190
289
  - 场景变化:
191
290
  - 人物/产品/字幕:
192
291
  - 节奏特点:
193
292
 
194
- ## 6. 情绪、信任和转化设计
293
+ ## 8. 视频风格解析拆解
294
+ - 整体风格标签:
295
+ - 镜头语言:
296
+ - 剪辑节奏:
297
+ - 字幕与包装:
298
+ - 音乐/音效:
299
+ - 叙事语气:
300
+ - 人设或账号气质:
301
+ - 平台适配特点:
302
+ - 可复用风格公式:
303
+
304
+ ## 9. 情绪、信任和转化设计
195
305
 
196
- ## 7. 可复用创作模板
306
+ ## 10. 可复用创作模板
197
307
 
198
- ## 8. 可借鉴点与风险
308
+ ## 11. 可借鉴点与风险
199
309
  - 可借鉴:
200
310
  - 不建议照搬:
201
311
  - 需要二次原创:
202
312
 
203
- ## 9. 给用户的下一步建议
313
+ ## 12. 给用户的下一步建议
204
314
  ```
205
315
 
206
316
  ## 失败处理
207
317
 
208
318
  - 知识库查询失败:说明无法取得拆解规则,不要跳过规则直接当普通视觉理解;可询问用户是否改为通用视觉理解。
319
+ - 作品详情获取失败:说明无法补齐标题、文案、封面和互动数据;可继续做视频画面拆解,但最终报告必须标注平台元数据缺失。
209
320
  - 视频 URL 解析失败:说明需要用户提供本地视频或可直接访问的下载链接。
210
321
  - YuanFlow token 缺失:说明需要在受管环境或 CLI 配置 YuanFlow API token,不要让用户在聊天里粘贴敏感 Key。
211
322
  - 视频超限:提示最大 2GB、2 秒到 1 小时,让用户裁剪或压缩。
@@ -25,7 +25,8 @@ const ERROR_MAP = [
25
25
  message.includes('未知平台') ||
26
26
  message.includes('未找到命令') ||
27
27
  message.includes('未知命令') ||
28
- message.includes('不支持'),
28
+ message.includes('不支持') ||
29
+ message.includes('已有未归档音色'),
29
30
  },
30
31
  {
31
32
  code: 'AUTH_INVALID',
@@ -78,7 +79,7 @@ export function createAgentSuccess(command, data, meta = {}) {
78
79
  }
79
80
 
80
81
  export function createAgentError(command, error) {
81
- const message = error?.message || String(error);
82
+ const message = sanitizeErrorMessage(error?.message || String(error));
82
83
  const mapped = mapError(message);
83
84
  return {
84
85
  payload: {
@@ -97,6 +98,18 @@ export function createAgentError(command, error) {
97
98
  };
98
99
  }
99
100
 
101
+ function sanitizeErrorMessage(message) {
102
+ const text = String(message || '');
103
+ if (text.includes('one username can only have one voice id')) {
104
+ return '请求失败:HTTP 409 当前用户已有未归档音色。请先使用 voice list 查询已有 voice_xxx,或使用已有音色复刻。';
105
+ }
106
+ const internalErrorType = ['new', 'api', 'error'].join('_');
107
+ const internalProductName = ['New', 'Api'].join('');
108
+ return text
109
+ .replace(new RegExp(internalErrorType, 'gi'), 'platform_service_error')
110
+ .replace(new RegExp(internalProductName, 'g'), 'YuanFlow API');
111
+ }
112
+
100
113
  export function getCommandName(platform, command) {
101
114
  return [platform, command].filter(Boolean).join(' ') || 'unknown';
102
115
  }
package/src/ai-tools.js CHANGED
@@ -11,8 +11,8 @@ const DOUBAO_TTS_VOICE_ASSETS_PATH = '/api/voice-assets/doubao/voices';
11
11
  const YUANFLOW_FILE_TRANSFER_PATH = '/atomic/oss/temp-upload';
12
12
 
13
13
  const MODEL_QWEN_VL = 'qwen3-vl-plus';
14
- const MODEL_QWEN_VOICE = 'qwen-voice-enrollment';
15
- const MODEL_QWEN_TTS_VC = 'qwen3-tts-vc-realtime-2026-01-15';
14
+ const MODEL_VOICE_ENROLLMENT = 'voice-enrollment';
15
+ const MODEL_COSYVOICE_FLASH = 'cosyvoice-v3-flash';
16
16
  const MODEL_FUN_ASR = 'fun-asr';
17
17
  const MODEL_DOUBAO_TTS = 'doubao-tts';
18
18
 
@@ -41,34 +41,36 @@ export function listAiCommands() {
41
41
  returns: '返回 OpenAI chat.completion 兼容 JSON。',
42
42
  }),
43
43
  aiCommand({
44
- key: 'ai.qwen-voice-enrollment',
45
- command: 'ai qwen-voice-enrollment',
46
- description: '调用 YuanFlow API 对外模型 qwen-voice-enrollment,创建音色复刻记录。',
44
+ key: 'ai.voice-enrollment',
45
+ command: 'ai voice-enrollment',
46
+ description: '调用 YuanFlow API 对外模型 voice-enrollment,创建音色复刻记录。',
47
47
  apiPath: AUDIO_VOICES_PATH,
48
48
  options: [
49
- option('--file', 'file', false, '本地音频文件;与 --audio-url 二选一。'),
49
+ option('--file', 'file', false, '本地音频文件;通过 multipart 直接提交给 YuanFlow API,与 --audio-url 二选一。'),
50
50
  option('--audio-url', 'audioUrl', false, '公网可访问音频 URL;与 --file 二选一。'),
51
51
  option('--name', 'name', false, '音色展示名。'),
52
52
  option('--preferred-name', 'preferredName', false, '偏好音色名,默认跟随 --name。'),
53
- option('--text', 'text', false, '参考音频对应文本,可选。'),
54
- option('--language', 'language', false, '语言代码,可选。'),
53
+ option('--target-model', 'targetModel', false, `后续合成模型,默认 ${MODEL_COSYVOICE_FLASH}。`),
54
+ option('--language-hints', 'languageHints', false, '逗号分隔的样本音频语种提示,例如 zh。'),
55
+ option('--language', 'language', false, '兼容别名;会映射为 language_hints。'),
55
56
  option('--activate', 'activate', false, '创建后设为当前默认音色。'),
56
57
  ...commonOptions(),
57
58
  ],
58
59
  requestBody: {
59
- model: MODEL_QWEN_VOICE,
60
- audio: '<本地文件 data URI 或 audio_url>',
60
+ model: MODEL_VOICE_ENROLLMENT,
61
+ target_model: MODEL_COSYVOICE_FLASH,
62
+ file: '<multipart 本地音频,或通过 audio_url 传入公网音频 URL>',
61
63
  },
62
- returns: '返回 voice_xxx 音色对象;后续 qwen3-tts-vc-realtime-2026-01-15 可用 --voice voice_xxx 调用。',
64
+ returns: '返回 voice_xxx 音色对象;后续 cosyvoice-v3-flash 可用 --voice voice_xxx 调用。',
63
65
  }),
64
66
  aiCommand({
65
- key: 'ai.qwen3-tts-vc-realtime-2026-01-15',
66
- command: 'ai qwen3-tts-vc-realtime-2026-01-15',
67
- description: '调用 YuanFlow API 对外模型 qwen3-tts-vc-realtime-2026-01-15,使用 voice_xxx 或 default 合成音频。',
67
+ key: 'ai.cosyvoice-v3-flash',
68
+ command: 'ai cosyvoice-v3-flash',
69
+ description: '调用 YuanFlow API 对外模型 cosyvoice-v3-flash,使用 voice_xxx 或 default 合成音频。',
68
70
  apiPath: AUDIO_SPEECH_PATH,
69
71
  options: speechOptions('音色 ID:voice_xxx 或 default。', false),
70
72
  requestBody: {
71
- model: MODEL_QWEN_TTS_VC,
73
+ model: MODEL_COSYVOICE_FLASH,
72
74
  input: '<text>',
73
75
  voice: '<voice_xxx|default>',
74
76
  },
@@ -166,10 +168,10 @@ export async function runAiCommand({ action = 'help', rest = [], options }) {
166
168
  return { ok: true, commands: listAiCommands() };
167
169
  case MODEL_QWEN_VL:
168
170
  return callJson(CHAT_COMPLETIONS_PATH, options, await buildQwenVLBody(options));
169
- case MODEL_QWEN_VOICE:
170
- return callJson(AUDIO_VOICES_PATH, options, await buildVoiceEnrollmentBody(options));
171
- case MODEL_QWEN_TTS_VC:
172
- return callSpeech(MODEL_QWEN_TTS_VC, options, false);
171
+ case MODEL_VOICE_ENROLLMENT:
172
+ return callVoiceEnrollment(options);
173
+ case MODEL_COSYVOICE_FLASH:
174
+ return callSpeech(MODEL_COSYVOICE_FLASH, options, false);
173
175
  case MODEL_FUN_ASR:
174
176
  return callFunASR(options);
175
177
  case MODEL_DOUBAO_TTS:
@@ -308,21 +310,38 @@ async function buildVoiceEnrollmentBody(options) {
308
310
  throw new Error('--file 和 --audio-url 不能同时使用。');
309
311
  }
310
312
  const body = {
311
- model: MODEL_QWEN_VOICE,
313
+ model: MODEL_VOICE_ENROLLMENT,
314
+ target_model: cleanOptional(options.named?.['target-model']) || MODEL_COSYVOICE_FLASH,
312
315
  ...optionalField('name', options.named?.name),
313
316
  ...optionalField('preferred_name', options.named?.['preferred-name']),
314
- ...optionalField('text', options.named?.text),
315
- ...optionalField('language', options.named?.language),
316
317
  ...optionalBooleanField('activate', options.named?.activate),
317
318
  };
319
+ const languageHints = splitList(options.named?.['language-hints'] || options.named?.language);
320
+ if (languageHints.length > 0) {
321
+ body.language_hints = languageHints;
322
+ }
318
323
  if (audioUrl) {
319
324
  body.audio_url = audioUrl;
320
325
  } else {
321
- body.audio = options.dryRun ? '<data URI omitted in dry-run>' : await fileToDataUri(filePath);
326
+ body.file = '<file omitted>';
322
327
  }
323
328
  return body;
324
329
  }
325
330
 
331
+ async function callVoiceEnrollment(options) {
332
+ const body = await buildVoiceEnrollmentBody(options);
333
+ const filePath = cleanOptional(options.file);
334
+ const audioUrl = cleanOptional(options.named?.['audio-url']);
335
+ if (filePath && !audioUrl && !options.json) {
336
+ const response = await callMultipartJson(AUDIO_VOICES_PATH, options, {
337
+ ...body,
338
+ filePath,
339
+ });
340
+ return result(MODEL_VOICE_ENROLLMENT, AUDIO_VOICES_PATH, body, response);
341
+ }
342
+ return callJson(AUDIO_VOICES_PATH, options, body);
343
+ }
344
+
326
345
  async function callSpeech(model, options, requiresVoice) {
327
346
  const body = buildSpeechBody(model, options, requiresVoice);
328
347
  const response = await callBinary(AUDIO_SPEECH_PATH, options, body);
@@ -352,10 +371,18 @@ function buildSpeechBody(model, options, requiresVoice) {
352
371
  const metadata = parseJsonObject(options.named?.metadata);
353
372
  addNumber(metadata, 'sample_rate', options.named?.['sample-rate']);
354
373
  addNumber(metadata, 'volume', options.named?.volume);
355
- addNumber(metadata, 'pitch_rate', options.named?.['pitch-rate']);
374
+ addNumber(metadata, 'rate', options.named?.rate || options.named?.speed);
375
+ addNumber(metadata, 'pitch', options.named?.pitch || options.named?.['pitch-rate']);
356
376
  addNumber(metadata, 'bit_rate', options.named?.['bit-rate']);
357
377
  addString(metadata, 'mode', options.named?.mode);
358
378
  addString(metadata, 'language', options.named?.language);
379
+ addNumber(metadata, 'seed', options.named?.seed);
380
+ addBoolean(metadata, 'enable_ssml', options.named?.['enable-ssml']);
381
+ addBoolean(metadata, 'word_timestamp_enabled', options.named?.['word-timestamp-enabled']);
382
+ const hints = splitList(options.named?.['language-hints']);
383
+ if (hints.length > 0) {
384
+ metadata.language_hints = hints;
385
+ }
359
386
  if (Object.keys(metadata).length > 0) {
360
387
  body.metadata = metadata;
361
388
  }
@@ -489,19 +516,25 @@ async function callGetJson(apiPath, options) {
489
516
  }
490
517
 
491
518
  async function callMultipartJson(apiPath, options, payload) {
492
- const request = await buildRequest(apiPath, options, 'POST', {
493
- model: payload.model,
494
- response_format: payload.response_format,
495
- metadata: payload.metadata,
496
- file: '<file omitted>',
497
- });
519
+ const requestBody = { ...payload, file: '<file omitted>' };
520
+ delete requestBody.filePath;
521
+ const request = await buildRequest(apiPath, options, 'POST', requestBody);
498
522
  if (request.dryRun) {
499
523
  return request;
500
524
  }
501
525
  const form = new FormData();
502
- form.set('model', payload.model);
503
- form.set('response_format', payload.response_format);
504
- form.set('metadata', JSON.stringify(payload.metadata || {}));
526
+ for (const [key, value] of Object.entries(payload || {})) {
527
+ if (key === 'filePath' || key === 'file' || value === undefined || value === null) {
528
+ continue;
529
+ }
530
+ if (Array.isArray(value)) {
531
+ form.set(key, value.join(','));
532
+ } else if (typeof value === 'object') {
533
+ form.set(key, JSON.stringify(value));
534
+ } else {
535
+ form.set(key, String(value));
536
+ }
537
+ }
505
538
  const file = new Blob([await readFile(payload.filePath)], { type: inferAudioMimeType(payload.filePath) });
506
539
  form.set('file', file, path.basename(payload.filePath));
507
540
  const response = await fetch(request.url, {
@@ -617,8 +650,16 @@ function speechOptions(voiceLabel, voiceRequired) {
617
650
  option('--voice', 'voice', voiceRequired, voiceLabel),
618
651
  option('--output', 'output', true, '音频保存路径;dry-run 时可不传。'),
619
652
  option('--response-format', 'responseFormat', false, 'mp3、wav、pcm 等,默认 mp3。'),
620
- option('--speed', 'speed', false, '语速控制。'),
653
+ option('--rate', 'rate', false, '语速控制,会写入 metadata.rate。'),
654
+ option('--speed', 'speed', false, '兼容别名;会写入 metadata.rate。'),
655
+ option('--volume', 'volume', false, '音量控制,会写入 metadata.volume。'),
656
+ option('--pitch', 'pitch', false, '音调控制,会写入 metadata.pitch。'),
621
657
  option('--sample-rate', 'sampleRate', false, '采样率。'),
658
+ option('--language', 'language', false, '目标合成语言,例如 zh。'),
659
+ option('--language-hints', 'languageHints', false, '逗号分隔的目标合成语种提示,例如 zh。'),
660
+ option('--enable-ssml', 'enableSsml', false, '是否开启 SSML。'),
661
+ option('--word-timestamp-enabled', 'wordTimestampEnabled', false, '是否开启字级时间戳。'),
662
+ option('--seed', 'seed', false, '随机种子。'),
622
663
  option('--metadata', 'metadata', false, '透传给 YuanFlow API 的 metadata JSON。'),
623
664
  ...commonOptions(),
624
665
  ];
@@ -638,11 +679,6 @@ function option(flag, name, required, label) {
638
679
  return { flag, name, required, label };
639
680
  }
640
681
 
641
- async function fileToDataUri(filePath) {
642
- const data = await readFile(filePath);
643
- return `data:${inferAudioMimeType(filePath)};base64,${data.toString('base64')}`;
644
- }
645
-
646
682
  function inferAudioMimeType(filePath) {
647
683
  switch (path.extname(filePath).toLowerCase()) {
648
684
  case '.mp3':
package/src/cli.js CHANGED
@@ -629,19 +629,19 @@ function printHelp() {
629
629
  yuanflow-cli browser task-plan --platform xiaohongshu --task publish --account main --format agent-json
630
630
  yuanflow-cli ai qwen3-vl-plus --prompt "描述这张图" --image-url "https://example.com/image.png" --dry-run
631
631
  yuanflow-cli ai qwen3-vl-plus --prompt "总结这个视频画面" --video-url "https://example.com/video.mp4" --dry-run
632
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "D:\\素材\\cover.png" --dry-run
633
- yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\\素材\\demo.mp4" --dry-run
634
- yuanflow-cli voice clone --file-transfer "D:\\voice\\sample.wav" --name demo --activate --dry-run
632
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "<本地图片路径>" --dry-run
633
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "<本地视频路径>" --dry-run
634
+ yuanflow-cli voice clone --file-transfer "<本地音频路径>" --name demo --activate --dry-run
635
635
  yuanflow-cli voice list --dry-run
636
636
  yuanflow-cli voice activate --voice voice_xxx --dry-run
637
- yuanflow-cli voice replicate --text "你好" --voice voice_xxx --output "D:\\voice\\replicate.mp3" --dry-run
638
- yuanflow-cli ai qwen-voice-enrollment --file "D:\\voice\\sample.wav" --name demo --activate --dry-run
639
- yuanflow-cli ai qwen3-tts-vc-realtime-2026-01-15 --text "你好" --voice voice_xxx --output "D:\\voice\\qwen.mp3" --dry-run
637
+ yuanflow-cli voice replicate --text "你好" --voice voice_xxx --output "<输出音频路径>" --dry-run
638
+ yuanflow-cli ai voice-enrollment --file "<本地音频路径>" --name demo --activate --dry-run
639
+ yuanflow-cli ai cosyvoice-v3-flash --text "你好" --voice voice_xxx --output "<输出音频路径>" --dry-run
640
640
  yuanflow-cli ai fun-asr --audio-url "https://example.com/audio.wav" --response-format verbose_json --dry-run
641
641
  yuanflow-cli ai doubao-tts voices --dry-run
642
642
  yuanflow-cli ai doubao-tts voice --voice zh_female_xiaohe_uranus_bigtts --dry-run
643
- yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "D:\\voice\\preview.mp3" --dry-run
644
- yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "D:\\voice\\doubao.mp3" --dry-run
643
+ yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --dry-run
644
+ yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "<输出音频路径>" --dry-run
645
645
  yuanflow-cli list douyin
646
646
 
647
647
  说明:
@@ -650,7 +650,7 @@ function printHelp() {
650
650
  qwen3-vl-plus 支持 --image-url、--video-url、--image-file、--video-file 四选一;视频建议最大 2GB、时长 2 秒到 1 小时。
651
651
  qwen3-vl-plus 本地图片/视频会先走 YuanFlow 文件中转,内部调用 /atomic/oss/temp-upload 后把 signed_url 提交给模型。
652
652
  browser 命令是自媒体平台专用浏览器自动化协议,只返回受控 profile/cookie/任务路径与执行计划,不用于普通网页搜索。
653
- 视频智能剪辑和视频制作执行链已迁移到独立项目 D:\\AI_project\\视频制作,不再由 yuanflow-cli video 命令提供;视频拆解仍由 yuanflow-skill 的“视频拆解”提供。
653
+ 视频智能剪辑和视频制作执行链已迁移到独立项目,不再由 yuanflow-cli video 命令提供;视频拆解仍由 yuanflow-skill 的“视频拆解”提供。
654
654
  需要鉴权的请求都会使用 Authorization: Bearer <token>。
655
655
  token 优先级:--token > YUANCHUANG_API_TOKEN > 本地 config.token。
656
656
  YuanFlow-main 内置环境使用时,token 由 YuanFlow-main 内置环境注入,不需要手动配置。
@@ -7,8 +7,8 @@ const AUDIO_SPEECH_PATH = '/v1/audio/speech';
7
7
  const AUDIO_VOICES_PATH = '/v1/audio/voices';
8
8
  const YUANFLOW_FILE_TRANSFER_PATH = '/atomic/oss/temp-upload';
9
9
 
10
- const MODEL_VOICE_CLONE = 'qwen-voice-enrollment';
11
- const MODEL_VOICE_REPLICATE = 'qwen3-tts-vc-realtime-2026-01-15';
10
+ const MODEL_VOICE_CLONE = 'voice-enrollment';
11
+ const MODEL_VOICE_REPLICATE = 'cosyvoice-v3-flash';
12
12
 
13
13
  export function listVoiceCommands() {
14
14
  return [
@@ -19,19 +19,21 @@ export function listVoiceCommands() {
19
19
  method: 'POST',
20
20
  apiPath: AUDIO_VOICES_PATH,
21
21
  options: [
22
- option('--file', 'file', false, '本地音频文件;与 --file-transfer、--audio-url 三选一。'),
22
+ option('--file', 'file', false, '本地音频文件;通过 multipart 直接提交给 YuanFlow API,与 --file-transfer、--audio-url 三选一。'),
23
23
  option('--file-transfer', 'fileTransfer', false, '本地音频文件;先通过 YuanFlow 文件中转生成临时 URL,再创建声音克隆。'),
24
24
  option('--audio-url', 'audioUrl', false, '公网可访问音频 URL;与 --file、--file-transfer 三选一。'),
25
25
  option('--name', 'name', false, '声音克隆展示名。'),
26
26
  option('--preferred-name', 'preferredName', false, '偏好音色名,默认跟随 --name。'),
27
- option('--text', 'text', false, '参考音频对应文本,可选。'),
28
- option('--language', 'language', false, '语言代码,可选。'),
27
+ option('--target-model', 'targetModel', false, `后续合成模型,默认 ${MODEL_VOICE_REPLICATE}。`),
28
+ option('--language-hints', 'languageHints', false, '逗号分隔的样本音频语种提示,例如 zh。'),
29
+ option('--language', 'language', false, '兼容别名;会映射为 language_hints。'),
29
30
  option('--activate', 'activate', false, '创建后设为当前默认音色。'),
30
31
  ...commonOptions(),
31
32
  ],
32
33
  requestBody: {
33
34
  model: MODEL_VOICE_CLONE,
34
- audio: '<本地音频 data URI,或通过 audio_url 传入 YuanFlow 文件中转 URL>',
35
+ target_model: MODEL_VOICE_REPLICATE,
36
+ file: '<multipart 本地音频,或通过 audio_url 传入公网音频 URL>',
35
37
  },
36
38
  returns: '返回 voice_xxx 音色对象;后续 voice replicate 可通过 --voice voice_xxx 复刻声音。',
37
39
  }),
@@ -69,8 +71,16 @@ export function listVoiceCommands() {
69
71
  option('--voice', 'voice', true, '声音克隆 ID:voice_xxx;也可传 default 使用已激活默认音色。'),
70
72
  option('--output', 'output', true, '音频保存路径;dry-run 时可不传。'),
71
73
  option('--response-format', 'responseFormat', false, 'mp3、wav、pcm 等,默认 mp3。'),
72
- option('--speed', 'speed', false, '语速控制。'),
74
+ option('--rate', 'rate', false, '语速控制,会写入 metadata.rate。'),
75
+ option('--speed', 'speed', false, '兼容别名;会写入 metadata.rate。'),
76
+ option('--volume', 'volume', false, '音量控制,会写入 metadata.volume。'),
77
+ option('--pitch', 'pitch', false, '音调控制,会写入 metadata.pitch。'),
73
78
  option('--sample-rate', 'sampleRate', false, '采样率。'),
79
+ option('--language', 'language', false, '目标合成语言,例如 zh。'),
80
+ option('--language-hints', 'languageHints', false, '逗号分隔的目标合成语种提示,例如 zh。'),
81
+ option('--enable-ssml', 'enableSsml', false, '是否开启 SSML。'),
82
+ option('--word-timestamp-enabled', 'wordTimestampEnabled', false, '是否开启字级时间戳。'),
83
+ option('--seed', 'seed', false, '随机种子。'),
74
84
  option('--metadata', 'metadata', false, '透传给 YuanFlow API 的 metadata JSON。'),
75
85
  ...commonOptions(),
76
86
  ],
@@ -112,8 +122,10 @@ export async function runVoiceCommand({ action = 'help', options }) {
112
122
  }
113
123
 
114
124
  async function cloneVoice(options) {
115
- const body = await buildVoiceCloneBody(options);
116
- const response = await callJson(AUDIO_VOICES_PATH, options, body);
125
+ const { body, filePath } = await buildVoiceClonePayload(options);
126
+ const response = filePath
127
+ ? await callMultipartJson(AUDIO_VOICES_PATH, options, body, filePath)
128
+ : await callJson(AUDIO_VOICES_PATH, options, body);
117
129
  return result('voice clone', AUDIO_VOICES_PATH, body, response, { kind: 'voice-clone' });
118
130
  }
119
131
 
@@ -138,9 +150,9 @@ async function replicateVoice(options) {
138
150
  return result('voice replicate', AUDIO_SPEECH_PATH, body, response, { kind: 'voice-replicate' });
139
151
  }
140
152
 
141
- async function buildVoiceCloneBody(options) {
153
+ async function buildVoiceClonePayload(options) {
142
154
  if (options.json) {
143
- return JSON.parse(options.json);
155
+ return { body: JSON.parse(options.json) };
144
156
  }
145
157
  const filePath = cleanOptional(options.file);
146
158
  const fileTransferPath = cleanOptional(options.named?.['file-transfer']);
@@ -155,20 +167,24 @@ async function buildVoiceCloneBody(options) {
155
167
 
156
168
  const body = {
157
169
  model: MODEL_VOICE_CLONE,
170
+ target_model: cleanOptional(options.named?.['target-model']) || MODEL_VOICE_REPLICATE,
158
171
  ...optionalField('name', options.named?.name),
159
172
  ...optionalField('preferred_name', options.named?.['preferred-name']),
160
- ...optionalField('text', options.named?.text),
161
- ...optionalField('language', options.named?.language),
162
173
  ...optionalBooleanField('activate', options.named?.activate),
163
174
  };
175
+ const languageHints = splitList(options.named?.['language-hints'] || options.named?.language);
176
+ if (languageHints.length > 0) {
177
+ body.language_hints = languageHints;
178
+ }
164
179
  if (audioUrl) {
165
180
  body.audio_url = audioUrl;
166
181
  } else if (fileTransferPath) {
167
182
  body.audio_url = await resolveYuanFlowAudioFile(fileTransferPath, options);
168
183
  } else {
169
- body.audio = options.dryRun ? '<data URI omitted in dry-run>' : await fileToDataUri(filePath);
184
+ body.file = '<file omitted>';
185
+ return { body, filePath };
170
186
  }
171
- return body;
187
+ return { body };
172
188
  }
173
189
 
174
190
  function buildVoiceReplicateBody(options) {
@@ -187,9 +203,19 @@ function buildVoiceReplicateBody(options) {
187
203
  response_format: cleanOptional(options.named?.['response-format']) || 'mp3',
188
204
  ...optionalField('instructions', options.named?.instructions),
189
205
  };
190
- addNumber(body, 'speed', options.named?.speed);
191
206
  const metadata = parseJsonObject(options.named?.metadata);
192
207
  addNumber(metadata, 'sample_rate', options.named?.['sample-rate']);
208
+ addNumber(metadata, 'volume', options.named?.volume);
209
+ addNumber(metadata, 'rate', options.named?.rate || options.named?.speed);
210
+ addNumber(metadata, 'pitch', options.named?.pitch);
211
+ addString(metadata, 'language', options.named?.language);
212
+ addNumber(metadata, 'seed', options.named?.seed);
213
+ addBoolean(metadata, 'enable_ssml', options.named?.['enable-ssml']);
214
+ addBoolean(metadata, 'word_timestamp_enabled', options.named?.['word-timestamp-enabled']);
215
+ const hints = splitList(options.named?.['language-hints']);
216
+ if (hints.length > 0) {
217
+ metadata.language_hints = hints;
218
+ }
193
219
  if (Object.keys(metadata).length > 0) {
194
220
  body.metadata = metadata;
195
221
  }
@@ -236,6 +262,28 @@ async function callJson(apiPath, options, body) {
236
262
  return readJsonResponse(response);
237
263
  }
238
264
 
265
+ async function callMultipartJson(apiPath, options, body, filePath) {
266
+ const request = await buildRequest(apiPath, options, 'POST', body);
267
+ if (request.dryRun) {
268
+ return request;
269
+ }
270
+ const form = new FormData();
271
+ for (const [key, value] of Object.entries(body || {})) {
272
+ if (key === 'file') {
273
+ continue;
274
+ }
275
+ form.set(key, Array.isArray(value) ? value.join(',') : String(value));
276
+ }
277
+ const file = new Blob([await readFile(filePath)], { type: inferAudioMimeType(filePath) });
278
+ form.set('file', file, path.basename(filePath));
279
+ const response = await fetch(request.url, {
280
+ method: 'POST',
281
+ headers: request.headers,
282
+ body: form,
283
+ });
284
+ return readJsonResponse(response);
285
+ }
286
+
239
287
  async function callGetJson(apiPath, options) {
240
288
  const request = await buildRequest(apiPath, options, 'GET');
241
289
  if (request.dryRun) {
@@ -364,11 +412,6 @@ function option(flag, name, required, label) {
364
412
  return { flag, name, required, label };
365
413
  }
366
414
 
367
- async function fileToDataUri(filePath) {
368
- const data = await readFile(filePath);
369
- return `data:${inferAudioMimeType(filePath)};base64,${data.toString('base64')}`;
370
- }
371
-
372
415
  function inferAudioMimeType(filePath) {
373
416
  switch (path.extname(filePath).toLowerCase()) {
374
417
  case '.mp3':
@@ -418,6 +461,20 @@ function addNumber(target, name, value) {
418
461
  }
419
462
  }
420
463
 
464
+ function addString(target, name, value) {
465
+ const cleaned = cleanOptional(value);
466
+ if (cleaned !== undefined) {
467
+ target[name] = cleaned;
468
+ }
469
+ }
470
+
471
+ function addBoolean(target, name, value) {
472
+ const parsed = parseBoolean(value);
473
+ if (parsed !== undefined) {
474
+ target[name] = parsed;
475
+ }
476
+ }
477
+
421
478
  function parseBoolean(value) {
422
479
  const cleaned = cleanOptional(value);
423
480
  if (cleaned === undefined) {
@@ -429,6 +486,20 @@ function parseBoolean(value) {
429
486
  return ['1', 'true', 'yes', 'on'].includes(String(cleaned).toLowerCase());
430
487
  }
431
488
 
489
+ function splitList(value) {
490
+ const cleaned = cleanOptional(value);
491
+ if (!cleaned) {
492
+ return [];
493
+ }
494
+ if (Array.isArray(cleaned)) {
495
+ return cleaned.map((item) => String(item).trim()).filter(Boolean);
496
+ }
497
+ return String(cleaned)
498
+ .split(',')
499
+ .map((item) => item.trim())
500
+ .filter(Boolean);
501
+ }
502
+
432
503
  function cleanOptional(value) {
433
504
  if (value === undefined || value === null) return undefined;
434
505
  if (typeof value === 'string') {