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.
- package/README.md +149 -12
- package/generated/registry.json +985 -984
- package/package.json +1 -1
- package/scripts/generate-registry.js +1 -1
- package/skills/yuanflow-skill/IP/350/277/220/350/220/245/SKILL.md +1 -0
- package/skills/yuanflow-skill/README.md +18 -10
- package/skills/yuanflow-skill/SKILL.md +46 -11
- 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
- package/skills/yuanflow-skill/yuanflow-cli/SKILL.md +3 -3
- 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
- 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
- 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
- package/skills/yuanflow-skill//345/210/233/344/275/234/346/200/273/347/233/221/SKILL.md +94 -0
- 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
- package/skills/yuanflow-skill//345/270/220/345/217/267/345/256/232/344/275/215/SKILL.md +1 -0
- package/skills/yuanflow-skill//346/226/207/346/241/210/345/210/233/344/275/234/SKILL.md +1 -0
- 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
- 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
- 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
- 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
- package/skills/yuanflow-skill//350/247/206/350/247/211/347/220/206/350/247/243/SKILL.md +174 -0
- 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
- package/skills/yuanflow-skill//350/247/206/351/242/221/346/213/206/350/247/243/SKILL.md +245 -0
- 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
- package/skills/yuanflow-skill//351/200/211/351/242/230/347/255/226/345/210/222/SKILL.md +1 -0
- 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
- package/src/agent-protocol.js +11 -5
- package/src/ai-tools.js +835 -0
- package/src/cli.js +58 -2
- package/src/comment-collector.js +1 -1
- package/src/oss-tools.js +11 -11
- package/src/shortcuts.js +3 -3
- package/src/trending-tools.js +117 -0
- package/src/video-tools.js +182 -3
- package/src/work-tools.js +4 -4
- /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
- /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
- /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
|
-
社媒请求调用
|
|
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
|
|
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 由主程序认证系统注入,不需要手动配置。
|
package/src/comment-collector.js
CHANGED
|
@@ -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: '上传本地文件到
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
67
|
-
{ flag: '--target-key', name: 'targetKey', required: true, label: '
|
|
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
|
+
}
|
package/src/video-tools.js
CHANGED
|
@@ -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
|
|
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
|
|
|
File without changes
|
|
File without changes
|