yuanflow-cli 0.1.39 → 0.1.41

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 (38) hide show
  1. package/README.md +149 -12
  2. package/generated/registry.json +985 -984
  3. package/package.json +1 -1
  4. package/scripts/generate-registry.js +1 -1
  5. package/skills/yuanflow-skill/IP/350/277/220/350/220/245/SKILL.md +1 -0
  6. package/skills/yuanflow-skill/README.md +18 -10
  7. package/skills/yuanflow-skill/SKILL.md +46 -11
  8. package/skills/yuanflow-skill/{OSS → YuanFlow}/346/226/207/344/273/266/344/270/255/350/275/254/345/267/245/345/205/267/SKILL.md +8 -8
  9. package/skills/yuanflow-skill/yuanflow-cli/SKILL.md +3 -3
  10. package/skills/yuanflow-skill//344/275/234/345/223/201/344/270/213/350/275/275/347/273/274/345/220/210/345/267/245/345/205/267/SKILL.md +1 -1
  11. package/skills/yuanflow-skill//344/275/234/345/223/201/350/257/204/350/256/272/351/207/207/351/233/206/SKILL.md +1 -1
  12. package/skills/yuanflow-skill//345/205/254/344/274/227/345/217/267/347/224/237/346/210/220/344/270/216/345/217/221/345/270/203/SKILL.md +1 -1
  13. package/skills/yuanflow-skill//345/210/233/344/275/234/346/200/273/347/233/221/SKILL.md +94 -0
  14. package/skills/yuanflow-skill//345/260/217/347/272/242/344/271/246/350/277/220/350/220/245/{344/270/216/345/217/221/345/270/203/SKILL.md → SKILL.md} +10 -5
  15. package/skills/yuanflow-skill//345/270/220/345/217/267/345/256/232/344/275/215/SKILL.md +1 -0
  16. package/skills/yuanflow-skill//346/226/207/346/241/210/345/210/233/344/275/234/SKILL.md +1 -0
  17. package/skills/yuanflow-skill//347/203/255/351/227/250/345/206/205/345/256/271/346/225/264/347/220/206/SKILL.md +83 -0
  18. package/skills/yuanflow-skill//347/233/264/346/222/255/346/212/225/346/265/201/347/255/226/347/225/245/SKILL.md +1 -0
  19. package/skills/yuanflow-skill//347/233/264/346/222/255/350/257/235/346/234/257/347/224/237/346/210/220/SKILL.md +1 -0
  20. package/skills/yuanflow-skill//350/207/252/345/252/222/344/275/223/347/237/245/350/257/206/345/272/223/SKILL.md +2 -0
  21. package/skills/yuanflow-skill//350/247/206/350/247/211/347/220/206/350/247/243/SKILL.md +174 -0
  22. package/skills/yuanflow-skill//350/247/206/351/242/221/346/212/225/346/265/201/347/255/226/347/225/245/SKILL.md +1 -0
  23. package/skills/yuanflow-skill//350/247/206/351/242/221/346/213/206/350/247/243/SKILL.md +245 -0
  24. package/skills/yuanflow-skill//350/247/206/351/242/221/346/231/272/350/203/275/345/211/252/350/276/221/SKILL.md +60 -12
  25. package/skills/yuanflow-skill//351/200/211/351/242/230/347/255/226/345/210/222/SKILL.md +1 -0
  26. package/skills/yuanflow-skill//351/237/263/350/247/206/351/242/221/345/234/250/347/272/277/350/275/254/346/226/207/345/255/227/SKILL.md +10 -10
  27. package/src/agent-protocol.js +11 -5
  28. package/src/ai-tools.js +835 -0
  29. package/src/cli.js +58 -2
  30. package/src/comment-collector.js +1 -1
  31. package/src/oss-tools.js +11 -11
  32. package/src/shortcuts.js +3 -3
  33. package/src/trending-tools.js +117 -0
  34. package/src/video-tools.js +182 -3
  35. package/src/work-tools.js +4 -4
  36. /package/skills/yuanflow-skill//345/260/217/347/272/242/344/271/246/350/277/220/350/220/245/{344/270/216/345/217/221/345/270/203/references → references}/commands.md" +0 -0
  37. /package/skills/yuanflow-skill//345/260/217/347/272/242/344/271/246/350/277/220/350/220/245/{344/270/216/345/217/221/345/270/203/references → references}/interaction-policy.md" +0 -0
  38. /package/skills/yuanflow-skill//345/260/217/347/272/242/344/271/246/350/277/220/350/220/245/{344/270/216/345/217/221/345/270/203/references → references}/publishing-policy.md" +0 -0
package/src/cli.js CHANGED
@@ -27,6 +27,8 @@ import {
27
27
  searchContent,
28
28
  searchUsers,
29
29
  } from './work-tools.js';
30
+ import { fetchVideoHotList } from './trending-tools.js';
31
+ import { formatAiHelp, runAiCommand } from './ai-tools.js';
30
32
 
31
33
  export async function main(argv) {
32
34
  const args = argv.slice(2);
@@ -72,6 +74,11 @@ export async function main(argv) {
72
74
  return;
73
75
  }
74
76
 
77
+ if (command === 'trending') {
78
+ await handleTrending(rest);
79
+ return;
80
+ }
81
+
75
82
  if (command === 'knowledge') {
76
83
  await handleKnowledge(rest);
77
84
  return;
@@ -92,6 +99,11 @@ export async function main(argv) {
92
99
  return;
93
100
  }
94
101
 
102
+ if (command === 'ai') {
103
+ await handleAi(rest);
104
+ return;
105
+ }
106
+
95
107
  if (command === 'schema') {
96
108
  await handleSchema(rest);
97
109
  return;
@@ -351,6 +363,19 @@ async function handleSearch(args) {
351
363
  });
352
364
  }
353
365
 
366
+ async function handleTrending(args) {
367
+ const { positionals, options } = parseOptions(args);
368
+ const [action = 'video-hot-list'] = positionals;
369
+ if (action !== 'video-hot-list') {
370
+ throw new Error('未知 trending 命令。用法:yuanflow-cli trending video-hot-list');
371
+ }
372
+ const result = await fetchVideoHotList({ options });
373
+ await outputResult(result, options, {
374
+ command: 'trending video-hot-list',
375
+ meta: { endpoint: result.endpoint.path, kind: result.endpoint.kind },
376
+ });
377
+ }
378
+
354
379
  async function handleKnowledge(args) {
355
380
  const { positionals, options } = parseOptions(args);
356
381
  const [action = 'docs'] = positionals;
@@ -412,6 +437,20 @@ async function handleVideo(args) {
412
437
  });
413
438
  }
414
439
 
440
+ async function handleAi(args) {
441
+ const { positionals, options } = parseOptions(args);
442
+ const [action = 'help', ...rest] = positionals;
443
+ if (action === 'help' && !isAgentJsonFormat(options)) {
444
+ console.log(formatAiHelp());
445
+ return;
446
+ }
447
+ const result = await runAiCommand({ action, rest, options });
448
+ await outputResult(result, { ...options, output: undefined }, {
449
+ command: `ai ${[action, ...rest].filter(Boolean).join(' ')}`,
450
+ meta: { endpoint: result.endpoint?.path, kind: result.endpoint?.kind || 'ai-model' },
451
+ });
452
+ }
453
+
415
454
  async function handleGeneratedCommand(platform, args) {
416
455
  if (!getPlatforms().includes(platform)) {
417
456
  throw new Error(`未知平台:${platform}。可用平台:${getPlatforms().join(', ')}`);
@@ -578,6 +617,7 @@ function printHelp() {
578
617
  yuanflow-cli works download --platform youtube --target "dQw4w9WgXcQ" --dry-run
579
618
  yuanflow-cli search content --platform xiaohongshu --keyword "美妆" --dry-run
580
619
  yuanflow-cli search users --platform instagram --keyword "nasa" --dry-run
620
+ yuanflow-cli trending video-hot-list --dry-run --format agent-json
581
621
  yuanflow-cli knowledge docs --dry-run
582
622
  yuanflow-cli knowledge entry --output-format short_video_script --domain 自媒体运营 --content-goal "写一个创业者短视频选题" --target-audience 创业者 --format agent-json
583
623
  yuanflow-cli oss signed-url --key path/to/file.png --ttl-seconds 1800 --format agent-json
@@ -585,14 +625,30 @@ function printHelp() {
585
625
  yuanflow-cli browser task-plan --platform xiaohongshu --task publish --account main --format agent-json
586
626
  yuanflow-cli video init --input "D:\\素材" --primary-audio "D:\\素材\\口播.mp3" --broll "D:\\素材\\画面.mp4" --format agent-json
587
627
  yuanflow-cli video strategy --project "D:\\素材\\yuanflow-video-edit" --template-type talking_head --rules-file "D:\\规则\\logic.json,D:\\规则\\template.json,D:\\规则\\cli.json" --format agent-json
628
+ yuanflow-cli video align --project "D:\\素材\\yuanflow-video-edit" --asr-file "D:\\素材\\asr.json" --format agent-json
588
629
  yuanflow-cli video timeline --project "D:\\素材\\yuanflow-video-edit" --fps 1 --format agent-json
630
+ yuanflow-cli video visual-review --project "D:\\素材\\yuanflow-video-edit" --review-file "D:\\素材\\yuanflow-video-edit\\visual_review.agent.json" --format agent-json
589
631
  yuanflow-cli video plan --project "D:\\素材\\yuanflow-video-edit" --timeline-plan "D:\\素材\\yuanflow-video-edit\\timeline_plan.agent.json" --format agent-json
632
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述这张图" --image-url "https://example.com/image.png" --dry-run
633
+ yuanflow-cli ai qwen3-vl-plus --prompt "总结这个视频画面" --video-url "https://example.com/video.mp4" --dry-run
634
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地图片" --image-file "D:\\素材\\cover.png" --dry-run
635
+ yuanflow-cli ai qwen3-vl-plus --prompt "描述本地视频" --video-file "D:\\素材\\demo.mp4" --dry-run
636
+ yuanflow-cli ai qwen-voice-enrollment --file "D:\\voice\\sample.wav" --name demo --activate --dry-run
637
+ yuanflow-cli ai qwen3-tts-vc-realtime-2026-01-15 --text "你好" --voice voice_xxx --output "D:\\voice\\qwen.mp3" --dry-run
638
+ yuanflow-cli ai fun-asr --audio-url "https://example.com/audio.wav" --response-format verbose_json --dry-run
639
+ yuanflow-cli ai doubao-tts voices --dry-run
640
+ yuanflow-cli ai doubao-tts voice --voice zh_female_xiaohe_uranus_bigtts --dry-run
641
+ yuanflow-cli ai doubao-tts voice-download --voice zh_female_xiaohe_uranus_bigtts --output "D:\\voice\\preview.mp3" --dry-run
642
+ yuanflow-cli ai doubao-tts --text "你好" --voice zh_female_xiaohe_uranus_bigtts --output "D:\\voice\\doubao.mp3" --dry-run
590
643
  yuanflow-cli list douyin
591
644
 
592
645
  说明:
593
- 社媒请求调用 Yuan API 的 /social/*path;知识库和 OSS 原子能力调用 /api/* 或 /atomic/*。
646
+ 社媒请求调用 YuanFlow API 的 /social/*path;知识库和 YuanFlow 文件中转调用 /api/* 或 /atomic/*。
647
+ ai 命令调用 YuanFlow API OpenAI 兼容端点,模型名使用 YuanFlow API 对外模型参数,不暴露底层供应商内部模型名。
648
+ qwen3-vl-plus 支持 --image-url、--video-url、--image-file、--video-file 四选一;视频建议最大 2GB、时长 2 秒到 1 小时。
649
+ qwen3-vl-plus 本地图片/视频会先走 YuanFlow 文件中转,内部调用 /atomic/oss/temp-upload 后把 signed_url 提交给模型。
594
650
  browser 命令是自媒体平台专用浏览器自动化协议,只返回受控 profile/cookie/任务路径与执行计划,不用于普通网页搜索。
595
- video 命令是视频智能剪辑基础链路:规则库策略快照、主音频+B-roll、1秒1帧抽帧、Agent 生成 timeline_plan/EDL、CLI 校验和渲染。
651
+ video 命令是视频智能剪辑基础链路:规则库策略快照、主音频+B-roll、ASR 时间戳对齐、1秒1帧抽帧、视觉理解回写、Agent 生成 timeline_plan/EDL、CLI 校验和渲染。
596
652
  需要鉴权的请求都会使用 Authorization: Bearer <token>。
597
653
  token 优先级:--token > YUANCHUANG_API_TOKEN > 本地 config.token。
598
654
  YuanFlow 主程序内使用时,token 由主程序认证系统注入,不需要手动配置。
@@ -454,7 +454,7 @@ export function listCommentCommands() {
454
454
  options: buildCommentCommandOptions(endpoint),
455
455
  queryParams: buildCommentCommandQueryParams(endpoint),
456
456
  requestBody: endpoint.bodyMode ? buildCommentCommandRequestBody(endpoint) : null,
457
- returns: '返回评论列表、翻页游标和平台原始响应字段,字段以上游接口实际响应为准。',
457
+ returns: '返回评论列表、翻页游标和平台原始响应字段,字段以 YuanFlow API 实际响应为准。',
458
458
  }));
459
459
  }
460
460
 
package/src/oss-tools.js CHANGED
@@ -12,14 +12,14 @@ export function listOssCommands() {
12
12
  key: 'oss.temp-upload',
13
13
  command: 'oss temp-upload',
14
14
  kind: 'oss-atomic',
15
- description: '上传本地文件到 Yuan API 临时 OSS bucket,并返回对象 key 和临时访问信息。',
15
+ description: '上传本地文件到 YuanFlow 文件中转,并返回对象 key 和临时访问信息。',
16
16
  method: 'POST',
17
17
  apiPath: OSS_TEMP_UPLOAD_PATH,
18
18
  positionals: [],
19
19
  options: [
20
20
  { flag: '--file', name: 'file', required: true, label: '本地文件路径。' },
21
21
  { flag: '--filename', name: 'filename', required: false, label: '上传时使用的文件名,默认取本地文件名。' },
22
- { flag: '--key', name: 'key', required: false, label: '可选 OSS 对象 key。' },
22
+ { flag: '--key', name: 'key', required: false, label: '可选文件对象 key。' },
23
23
  { flag: '--content-type', name: 'contentType', required: false, label: '文件 MIME 类型,默认 application/octet-stream。' },
24
24
  ...commonOptions(),
25
25
  ],
@@ -29,19 +29,19 @@ export function listOssCommands() {
29
29
  content_base64: '<base64>',
30
30
  content_type: '<content-type>',
31
31
  },
32
- returns: '返回 bucket、key、url、signed_url、expires_at 等字段,字段以后端实际响应为准。',
32
+ returns: '返回 bucket、key、url、signed_url、expires_at 等字段,字段以 YuanFlow API 实际响应为准。',
33
33
  },
34
34
  {
35
35
  key: 'oss.signed-url',
36
36
  command: 'oss signed-url',
37
37
  kind: 'oss-atomic',
38
- description: '为已有 OSS 对象生成临时签名访问链接。',
38
+ description: '为已有文件中转对象生成临时签名访问链接。',
39
39
  method: 'POST',
40
40
  apiPath: OSS_SIGNED_URL_PATH,
41
41
  positionals: [],
42
42
  options: [
43
- { flag: '--key', name: 'key', required: true, label: 'OSS 对象 key。' },
44
- { flag: '--bucket', name: 'bucket', required: false, label: '可选 bucket,不传则使用后端默认配置。' },
43
+ { flag: '--key', name: 'key', required: true, label: '文件对象 key。' },
44
+ { flag: '--bucket', name: 'bucket', required: false, label: '可选 bucket,不传则使用 YuanFlow 默认配置。' },
45
45
  { flag: '--ttl-seconds', name: 'ttlSeconds', required: false, label: '签名有效期,最大 86400 秒。' },
46
46
  { flag: '--method', name: 'method', required: false, label: '签名方法:GET、PUT、HEAD。' },
47
47
  ...commonOptions(),
@@ -52,19 +52,19 @@ export function listOssCommands() {
52
52
  ttl_seconds: 1800,
53
53
  method: 'GET',
54
54
  },
55
- returns: '返回签名 URL、过期时间和对象信息,字段以后端实际响应为准。',
55
+ returns: '返回签名 URL、过期时间和对象信息,字段以 YuanFlow API 实际响应为准。',
56
56
  },
57
57
  {
58
58
  key: 'oss.copy',
59
59
  command: 'oss copy',
60
60
  kind: 'oss-atomic',
61
- description: '复制 OSS 对象到目标 key。',
61
+ description: '复制文件中转对象到目标 key。',
62
62
  method: 'POST',
63
63
  apiPath: OSS_COPY_PATH,
64
64
  positionals: [],
65
65
  options: [
66
- { flag: '--source-key', name: 'sourceKey', required: true, label: ' OSS 对象 key。' },
67
- { flag: '--target-key', name: 'targetKey', required: true, label: '目标 OSS 对象 key。' },
66
+ { flag: '--source-key', name: 'sourceKey', required: true, label: '源文件对象 key。' },
67
+ { flag: '--target-key', name: 'targetKey', required: true, label: '目标文件对象 key。' },
68
68
  { flag: '--source-bucket', name: 'sourceBucket', required: false, label: '可选源 bucket。' },
69
69
  { flag: '--target-bucket', name: 'targetBucket', required: false, label: '可选目标 bucket。' },
70
70
  ...commonOptions(),
@@ -75,7 +75,7 @@ export function listOssCommands() {
75
75
  source_bucket: '<optional source bucket>',
76
76
  target_bucket: '<optional target bucket>',
77
77
  },
78
- returns: '返回复制后的对象信息,字段以后端实际响应为准。',
78
+ returns: '返回复制后的对象信息,字段以 YuanFlow API 实际响应为准。',
79
79
  },
80
80
  ];
81
81
  }
package/src/shortcuts.js CHANGED
@@ -7,7 +7,7 @@ export const shortcuts = [
7
7
  socialPath: '/douyin/app/v3/fetch_multi_video_v2',
8
8
  positionals: [{ name: 'url', label: '作品链接' }],
9
9
  options: [],
10
- returns: '返回作品基础信息、作者信息、互动统计、媒体信息等字段,字段结构以上游实际响应为准。',
10
+ returns: '返回作品基础信息、作者信息、互动统计、媒体信息等字段,字段结构以 YuanFlow API 实际响应为准。',
11
11
  alternatives: [
12
12
  'GET /social/douyin/app/v3/fetch_one_video_by_share_url?share_url=...',
13
13
  'GET /social/douyin/web/fetch_one_video?aweme_id=...',
@@ -21,7 +21,7 @@ export const shortcuts = [
21
21
  socialPath: '/douyin/app/v3/fetch_video_high_quality_play_url',
22
22
  positionals: [{ name: 'share_url', label: '作品链接或分享链接' }],
23
23
  options: [{ flag: 'region', name: 'region', label: '地区,可选,如 CN' }],
24
- returns: '返回视频播放地址、清晰度信息、媒体地址等;是否可直接下载取决于上游返回和目标平台限制。',
24
+ returns: '返回视频播放地址、清晰度信息、媒体地址等;是否可直接下载取决于 YuanFlow API 返回和目标平台限制。',
25
25
  alternatives: [
26
26
  'POST /social/douyin/app/v3/fetch_multi_video_high_quality_play_url',
27
27
  'GET /social/douyin/web/fetch_video_high_quality_play_url',
@@ -280,7 +280,7 @@ export const shortcuts = [
280
280
  {
281
281
  platform: 'wechat',
282
282
  command: 'channels-home',
283
- description: '微信视频号主页采集,参数结构通常需要用 --json 传入上游要求的完整请求体。',
283
+ description: '微信视频号主页采集,参数结构通常需要用 --json 传入接口要求的完整请求体。',
284
284
  method: 'POST',
285
285
  socialPath: '/wechat_channels/fetch_home_page',
286
286
  positionals: [],
@@ -0,0 +1,117 @@
1
+ import { callEndpoint } from './request.js';
2
+
3
+ const VIDEO_HOT_LIST_PATH = '/douyin/billboard/fetch_hot_total_video_list';
4
+
5
+ const VIDEO_HOT_BOARD_TYPES = [
6
+ { subType: 1001, label: '视频总榜' },
7
+ { subType: 1002, label: '低粉爆款' },
8
+ { subType: 1003, label: '高完播率' },
9
+ { subType: 1004, label: '高涨粉率' },
10
+ { subType: 1005, label: '高点赞率' },
11
+ ];
12
+
13
+ export function listTrendingCommands() {
14
+ return [
15
+ {
16
+ key: 'trending.video-hot-list',
17
+ command: 'trending video-hot-list',
18
+ kind: 'trending',
19
+ description: '一次性查询抖音视频热榜五类榜单:视频总榜、低粉爆款、高完播率、高涨粉率、高点赞率。',
20
+ method: 'POST',
21
+ socialPath: VIDEO_HOT_LIST_PATH,
22
+ positionals: [],
23
+ options: [
24
+ { flag: '--page', name: 'page', required: false, label: '页码,默认 1' },
25
+ { flag: '--page-size', name: 'page_size', required: false, label: '每页数量,默认 10' },
26
+ { flag: '--date-window', name: 'date_window', required: false, label: '时间窗口,默认 2 按天' },
27
+ { flag: '--tags-json', name: 'tags', required: false, label: '垂类标签 JSON 数组,空则全部垂类' },
28
+ ],
29
+ returns: '返回五类视频热榜的统一 JSON,字段结构以 YuanFlow API 实际响应为准。',
30
+ },
31
+ ];
32
+ }
33
+
34
+ export async function fetchVideoHotList({ options }) {
35
+ const query = buildVideoHotListQuery(options);
36
+ const boards = [];
37
+
38
+ for (const board of VIDEO_HOT_BOARD_TYPES) {
39
+ const requestBody = {
40
+ page: query.page,
41
+ page_size: query.page_size,
42
+ date_window: query.date_window,
43
+ sub_type: board.subType,
44
+ ...(query.tags !== undefined ? { tags: query.tags } : {}),
45
+ };
46
+ const response = await callEndpoint(VIDEO_HOT_LIST_PATH, {
47
+ ...options,
48
+ method: 'POST',
49
+ body: requestBody,
50
+ });
51
+ boards.push({
52
+ sub_type: board.subType,
53
+ label: board.label,
54
+ request: requestBody,
55
+ response,
56
+ });
57
+ }
58
+
59
+ return {
60
+ platform: 'douyin',
61
+ endpoint: {
62
+ method: 'POST',
63
+ path: VIDEO_HOT_LIST_PATH,
64
+ kind: 'video-hot-list',
65
+ },
66
+ query,
67
+ boards,
68
+ guidance: [
69
+ '结合用户个人创作信息、个人创作库历史记录、历史任务或对话记录输出建议。',
70
+ '不要只罗列榜单,必须区分五类榜单来源并提炼可执行选题和内容结构。',
71
+ '重点比较总榜、低粉爆款、高完播率、高涨粉率、高点赞率之间的差异。',
72
+ ],
73
+ };
74
+ }
75
+
76
+ function buildVideoHotListQuery(options = {}) {
77
+ return {
78
+ page: parsePositiveInteger(options.named?.page, 1, 'page'),
79
+ page_size: parsePositiveInteger(options.named?.['page-size'] ?? options.named?.page_size, 10, 'page_size'),
80
+ date_window: parsePositiveInteger(options.named?.['date-window'] ?? options.named?.date_window, 2, 'date_window'),
81
+ ...(parseTags(options.named?.['tags-json'] ?? options.named?.tags) !== undefined
82
+ ? { tags: parseTags(options.named?.['tags-json'] ?? options.named?.tags) }
83
+ : {}),
84
+ };
85
+ }
86
+
87
+ function parsePositiveInteger(value, fallback, name) {
88
+ if (value === undefined || value === null || value === '') {
89
+ return fallback;
90
+ }
91
+ const number = Number(value);
92
+ if (!Number.isInteger(number) || number <= 0) {
93
+ throw new Error(`${name} 必须是正整数。`);
94
+ }
95
+ return number;
96
+ }
97
+
98
+ function parseTags(value) {
99
+ if (value === undefined || value === null || value === '') {
100
+ return undefined;
101
+ }
102
+ if (Array.isArray(value)) {
103
+ return value;
104
+ }
105
+ try {
106
+ const parsed = JSON.parse(String(value));
107
+ if (!Array.isArray(parsed)) {
108
+ throw new Error('tags-json 必须是数组。');
109
+ }
110
+ return parsed;
111
+ } catch (error) {
112
+ if (error?.message === 'tags-json 必须是数组。') {
113
+ throw error;
114
+ }
115
+ throw new Error('tags-json 必须是合法 JSON 数组。');
116
+ }
117
+ }
@@ -9,7 +9,9 @@ const IMAGE_EXTS = new Set(['.jpg', '.jpeg', '.png', '.webp']);
9
9
  const PROJECT_FILE = 'project.json';
10
10
  const ASSETS_FILE = 'assets.json';
11
11
  const BEATS_FILE = 'beats.json';
12
+ const AUDIO_ALIGNMENT_FILE = 'audio_alignment.json';
12
13
  const VISUAL_SEGMENTS_FILE = 'visual_segments.json';
14
+ const VISUAL_UNDERSTANDING_FILE = 'visual_understanding.json';
13
15
  const EDL_FILE = 'edl.json';
14
16
  const STRATEGY_FILE = 'strategy_snapshot.json';
15
17
  const TIMELINE_PLAN_FILE = 'timeline_plan.json';
@@ -39,11 +41,19 @@ export function listVideoCommands() {
39
41
  option('--script-file', 'scriptFile', false, '读取文案文件。'),
40
42
  option('--transcript-file', 'transcriptFile', false, '读取转写 JSON 或文本文件。'),
41
43
  ]),
44
+ videoCommand('align', '导入 ASR 或 forced alignment 时间戳结果,生成 audio_alignment.json 和带时间戳 beats.json。', [
45
+ option('--project', 'project', true, '剪辑项目目录。'),
46
+ option('--asr-file', 'asrFile', true, '音视频在线转文字或 forced alignment 输出的 JSON 文件。'),
47
+ ]),
42
48
  videoCommand('timeline', '按 1 秒 1 帧抽帧并生成 visual_segments.json。', [
43
49
  option('--project', 'project', true, '剪辑项目目录。'),
44
50
  option('--fps', 'fps', false, '抽帧频率,基础版建议 1。'),
45
51
  option('--dry-run', 'dryRun', false, '不调用 ffmpeg,只生成占位帧清单。'),
46
52
  ]),
53
+ videoCommand('visual-review', '导入 Agent/人工对抽帧结果的视觉理解,回写 visual_segments.json。', [
54
+ option('--project', 'project', true, '剪辑项目目录。'),
55
+ option('--review-file', 'reviewFile', true, 'Agent 或人工生成的 visual_review.agent.json。'),
56
+ ]),
47
57
  videoCommand('plan', '校验 Agent 生成的 timeline_plan 和 EDL,写入标准 timeline_plan.json / edl.json。', [
48
58
  option('--project', 'project', true, '剪辑项目目录。'),
49
59
  option('--timeline-plan', 'timelinePlan', false, 'Agent 生成的 timeline_plan.agent.json。'),
@@ -72,12 +82,14 @@ export async function runVideoCommand({ action, options }) {
72
82
  if (action === 'inspect') return inspectProject(options);
73
83
  if (action === 'strategy') return saveStrategySnapshot(options);
74
84
  if (action === 'transcribe') return importTranscript(options);
85
+ if (action === 'align') return importAudioAlignment(options);
75
86
  if (action === 'timeline') return buildTimeline(options);
87
+ if (action === 'visual-review') return importVisualReview(options);
76
88
  if (action === 'plan') return planFromAgentEdl(options);
77
89
  if (action === 'render-preview') return renderVideo(options, { final: false });
78
90
  if (action === 'render-final') return renderVideo(options, { final: true });
79
91
  if (action === 'evaluate') return evaluateVideo(options);
80
- throw new Error('未知 video 命令。用法:yuanflow-cli video init|inspect|strategy|transcribe|timeline|plan|render-preview|render-final|evaluate');
92
+ throw new Error('未知 video 命令。用法:yuanflow-cli video init|inspect|strategy|transcribe|align|timeline|visual-review|plan|render-preview|render-final|evaluate');
81
93
  }
82
94
 
83
95
  function videoCommand(action, description, options) {
@@ -208,6 +220,58 @@ async function importTranscript(options) {
208
220
  return { ok: true, action: 'video.transcribe', project: projectSummary(project), beats };
209
221
  }
210
222
 
223
+ async function importAudioAlignment(options) {
224
+ const project = await readProject(options);
225
+ const asrPath = requiredPath(options.named?.['asr-file'], '缺少 --asr-file。');
226
+ const parsed = JSON.parse(await fs.readFile(asrPath, 'utf8'));
227
+ const segments = extractAlignmentSegments(parsed);
228
+ if (segments.length === 0) {
229
+ return {
230
+ ok: false,
231
+ action: 'video.align',
232
+ project: projectSummary(project),
233
+ alignment_validation: {
234
+ ok: false,
235
+ errors: ['ASR/对齐结果里没有可用的 segments,无法生成带时间戳 beats。'],
236
+ },
237
+ };
238
+ }
239
+
240
+ const alignment = {
241
+ version: 1,
242
+ source: path.resolve(asrPath),
243
+ imported_at: new Date().toISOString(),
244
+ text: stringOrDefault(parsed.text || parsed.data?.text || parsed.result?.text, segments.map((item) => item.text).join('')),
245
+ segments,
246
+ };
247
+ const beats = segments.map((segment, index) => ({
248
+ beat_id: `beat_${String(index + 1).padStart(3, '0')}`,
249
+ text: segment.text,
250
+ start_s: segment.start_s,
251
+ end_s: segment.end_s,
252
+ intent: '',
253
+ visual_need: '',
254
+ emotion: '',
255
+ alignment_source: 'audio_alignment',
256
+ }));
257
+ await writeJson(path.join(project.work_dir, AUDIO_ALIGNMENT_FILE), alignment);
258
+ await writeJson(path.join(project.work_dir, BEATS_FILE), {
259
+ source: path.resolve(asrPath),
260
+ imported_at: new Date().toISOString(),
261
+ alignment_file: path.join(project.work_dir, AUDIO_ALIGNMENT_FILE),
262
+ beats,
263
+ });
264
+ return {
265
+ ok: true,
266
+ action: 'video.align',
267
+ project: projectSummary(project),
268
+ alignment,
269
+ beats,
270
+ alignment_path: path.join(project.work_dir, AUDIO_ALIGNMENT_FILE),
271
+ beats_path: path.join(project.work_dir, BEATS_FILE),
272
+ };
273
+ }
274
+
211
275
  async function saveStrategySnapshot(options) {
212
276
  const project = await readProject(options);
213
277
  const templateType = stringOrDefault(options.named?.['template-type'], 'custom');
@@ -310,6 +374,61 @@ async function buildTimeline(options) {
310
374
  };
311
375
  }
312
376
 
377
+ async function importVisualReview(options) {
378
+ const project = await readProject(options);
379
+ const reviewPath = requiredPath(options.named?.['review-file'], '缺少 --review-file。');
380
+ const review = JSON.parse(await fs.readFile(reviewPath, 'utf8'));
381
+ const visualSegments = await readOptionalJson(path.join(project.work_dir, VISUAL_SEGMENTS_FILE));
382
+ if (!Array.isArray(visualSegments) || visualSegments.length === 0) {
383
+ return {
384
+ ok: false,
385
+ action: 'video.visual-review',
386
+ project: projectSummary(project),
387
+ review_validation: { ok: false, errors: ['请先执行 video timeline 生成 visual_segments.json。'], warnings: [] },
388
+ };
389
+ }
390
+
391
+ const reviewValidation = validateVisualReview(review, visualSegments);
392
+ if (!reviewValidation.ok) {
393
+ return { ok: false, action: 'video.visual-review', project: projectSummary(project), review_validation: reviewValidation };
394
+ }
395
+
396
+ const byId = new Map((review.reviews || []).map((item) => [item.segment_id, item]));
397
+ const reviewedSegments = visualSegments.map((segment) => {
398
+ const item = byId.get(segment.segment_id);
399
+ if (!item) return segment;
400
+ return {
401
+ ...segment,
402
+ description: item.description,
403
+ subjects: Array.isArray(item.subjects) ? item.subjects : [],
404
+ scene: stringOrDefault(item.scene, ''),
405
+ motion: stringOrDefault(item.motion, ''),
406
+ semantic_tags: Array.isArray(item.semantic_tags) ? item.semantic_tags : [],
407
+ quality_score: isFiniteNumber(item.quality_score) ? Number(item.quality_score) : null,
408
+ visual_review_source: path.resolve(reviewPath),
409
+ agent_review_required: false,
410
+ };
411
+ });
412
+ const payload = {
413
+ version: 1,
414
+ source: path.resolve(reviewPath),
415
+ imported_at: new Date().toISOString(),
416
+ reviewed_count: review.reviews.length,
417
+ reviews: review.reviews,
418
+ };
419
+ await writeJson(path.join(project.work_dir, VISUAL_UNDERSTANDING_FILE), payload);
420
+ await writeJson(path.join(project.work_dir, VISUAL_SEGMENTS_FILE), reviewedSegments);
421
+ return {
422
+ ok: true,
423
+ action: 'video.visual-review',
424
+ project: projectSummary(project),
425
+ review_validation: reviewValidation,
426
+ visual_understanding_path: path.join(project.work_dir, VISUAL_UNDERSTANDING_FILE),
427
+ visual_segments_path: path.join(project.work_dir, VISUAL_SEGMENTS_FILE),
428
+ reviewed_segments: reviewedSegments.filter((segment) => byId.has(segment.segment_id)),
429
+ };
430
+ }
431
+
313
432
  async function planFromAgentEdl(options) {
314
433
  const project = await readProject(options);
315
434
  const timelinePlanSource = normalizeOptionalPath(options.named?.['timeline-plan']);
@@ -334,9 +453,9 @@ async function planFromAgentEdl(options) {
334
453
  action: 'video.plan',
335
454
  project: projectSummary(project),
336
455
  agent_action_required: true,
337
- required_inputs: ['strategy_snapshot.json', 'beats.json', 'visual_segments.json', '用户剪辑 brief'],
456
+ required_inputs: ['strategy_snapshot.json', 'audio_alignment.json 或 beats.json', 'visual_segments.json', 'visual_understanding.json', '用户剪辑 brief'],
338
457
  expected_outputs: [path.join(project.work_dir, TIMELINE_PLAN_FILE), path.join(project.work_dir, EDL_FILE)],
339
- message: '由 Agent 读取规则库策略快照、beats 1秒1帧抽帧结果后生成 timeline_plan;timeline_plan 内可包含 edl,或再用 --edl 单独传入。',
458
+ message: '由 Agent 读取规则库策略快照、音频对齐结果、beats、视觉理解结果和 1秒1帧抽帧结果后生成 timeline_plan;timeline_plan 内可包含 edl,或再用 --edl 单独传入。',
340
459
  };
341
460
  }
342
461
  const edl = edlSource ? JSON.parse(await fs.readFile(edlSource, 'utf8')) : timelinePlan.edl;
@@ -480,6 +599,36 @@ function validateTimelinePlan(plan, beatsPayload, visualSegments) {
480
599
  return { ok: errors.length === 0, errors, warnings };
481
600
  }
482
601
 
602
+ function validateVisualReview(review, visualSegments) {
603
+ const errors = [];
604
+ const warnings = [];
605
+ if (!review || typeof review !== 'object' || Array.isArray(review)) {
606
+ return { ok: false, errors: ['visual_review 必须是 JSON 对象。'], warnings };
607
+ }
608
+ if (!Array.isArray(review.reviews) || review.reviews.length === 0) {
609
+ errors.push('visual_review.reviews 必须是非空数组。');
610
+ }
611
+ const knownSegmentIds = new Set((visualSegments || []).map((segment) => segment.segment_id));
612
+ const seen = new Set();
613
+ for (const [index, item] of (review.reviews || []).entries()) {
614
+ const label = `reviews[${index}]`;
615
+ if (!item.segment_id) errors.push(`${label}.segment_id 不能为空。`);
616
+ if (item.segment_id && !knownSegmentIds.has(item.segment_id)) {
617
+ errors.push(`${label}.segment_id 未出现在 visual_segments.json 中: ${item.segment_id}`);
618
+ }
619
+ if (item.segment_id && seen.has(item.segment_id)) {
620
+ errors.push(`${label}.segment_id 重复: ${item.segment_id}`);
621
+ }
622
+ if (item.segment_id) seen.add(item.segment_id);
623
+ if (!item.description) errors.push(`${label}.description 不能为空,必须说明画面内容。`);
624
+ if (!Array.isArray(item.subjects)) warnings.push(`${label}.subjects 建议使用数组。`);
625
+ if (item.quality_score !== undefined && (!isFiniteNumber(item.quality_score) || Number(item.quality_score) < 0 || Number(item.quality_score) > 1)) {
626
+ errors.push(`${label}.quality_score 必须是 0 到 1 之间的数字。`);
627
+ }
628
+ }
629
+ return { ok: errors.length === 0, errors, warnings };
630
+ }
631
+
483
632
  function buildRenderCommands(project, edl, assets, output, { final }) {
484
633
  const byId = new Map(assets.map((asset) => [asset.asset_id, asset]));
485
634
  const renderDir = path.join(project.work_dir, 'renders');
@@ -594,6 +743,29 @@ async function readScriptInput(options) {
594
743
  return '';
595
744
  }
596
745
 
746
+ function extractAlignmentSegments(payload) {
747
+ const candidates = [
748
+ payload?.segments,
749
+ payload?.data?.segments,
750
+ payload?.result?.segments,
751
+ payload?.data?.result?.segments,
752
+ payload?.response?.segments,
753
+ payload?.words,
754
+ payload?.data?.words,
755
+ payload?.result?.words,
756
+ ];
757
+ const found = candidates.find((item) => Array.isArray(item));
758
+ return (found || [])
759
+ .map((item) => {
760
+ const text = stringOrDefault(item.text || item.word || item.sentence, '').trim();
761
+ const start = firstFiniteNumber(item.start_s, item.start, item.begin_s, item.begin, item.from);
762
+ const end = firstFiniteNumber(item.end_s, item.end, item.stop_s, item.stop, item.to);
763
+ return { text, start_s: start, end_s: end };
764
+ })
765
+ .filter((item) => item.text && isFiniteNumber(item.start_s) && isFiniteNumber(item.end_s) && Number(item.end_s) > Number(item.start_s))
766
+ .map((item) => ({ text: item.text, start_s: Number(item.start_s), end_s: Number(item.end_s) }));
767
+ }
768
+
597
769
  function splitScriptToBeats(script) {
598
770
  return String(script)
599
771
  .replace(/\r/g, '\n')
@@ -755,6 +927,13 @@ function isFiniteNumber(value) {
755
927
  return Number.isFinite(Number(value));
756
928
  }
757
929
 
930
+ function firstFiniteNumber(...values) {
931
+ for (const value of values) {
932
+ if (isFiniteNumber(value)) return Number(value);
933
+ }
934
+ return null;
935
+ }
936
+
758
937
  function projectSummary(project) {
759
938
  return {
760
939
  project_id: project.project_id,
package/src/work-tools.js CHANGED
@@ -148,8 +148,8 @@ function workCommand(platform, action, endpoint, kind) {
148
148
  requestBody: endpoint.bodyMode ? requestBodyExample(endpoint, 'target') : null,
149
149
  returns:
150
150
  action === 'download'
151
- ? '返回作品可播放媒体信息、下载地址候选或视频流信息,字段以上游实际响应为准。'
152
- : '返回作品详情、作者信息、正文/标题、互动统计和媒体信息,字段以上游实际响应为准。',
151
+ ? '返回作品可播放媒体信息、下载地址候选或视频流信息,字段以 YuanFlow API 实际响应为准。'
152
+ : '返回作品详情、作者信息、正文/标题、互动统计和媒体信息,字段以 YuanFlow API 实际响应为准。',
153
153
  };
154
154
  }
155
155
 
@@ -167,8 +167,8 @@ function searchCommand(platform, action, endpoint, kind) {
167
167
  requestBody: endpoint.bodyMode ? requestBodyExample(endpoint, 'keyword') : null,
168
168
  returns:
169
169
  action === 'users'
170
- ? '返回用户/频道搜索结果、分页字段和平台原始摘要字段,字段以上游实际响应为准。'
171
- : '返回内容搜索结果、分页字段和平台原始摘要字段,字段以上游实际响应为准。',
170
+ ? '返回用户/频道搜索结果、分页字段和平台原始摘要字段,字段以 YuanFlow API 实际响应为准。'
171
+ : '返回内容搜索结果、分页字段和平台原始摘要字段,字段以 YuanFlow API 实际响应为准。',
172
172
  };
173
173
  }
174
174