chainlesschain 0.47.6 → 0.47.7
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/package.json +2 -2
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/Analytics-BFI7jbwM.css +1 -0
- package/src/assets/web-panel/assets/Analytics-DQ135mAd.js +3 -0
- package/src/assets/web-panel/assets/AppLayout-6SPt_8Y_.js +1 -0
- package/src/assets/web-panel/assets/AppLayout-BFJ-Fofn.css +1 -0
- package/src/assets/web-panel/assets/{Backup-Ba9UybpT.js → Backup-DbVRG5vE.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-BwXskT21.js → Chat-wVhrFK9C.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-UmOe7qvE.js → Cowork-lOC25IW2.js} +1 -1
- package/src/assets/web-panel/assets/{Cron-JHS-rc-4.js → Cron-3P0eVLTV.js} +1 -1
- package/src/assets/web-panel/assets/{Dashboard-B95cMCO7.js → Dashboard-Br7kCwKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Git-CSYO0_zk.js → Git-CrDCcBig.js} +2 -2
- package/src/assets/web-panel/assets/{Logs-Hxw_K0km.js → Logs-BfTE8urP.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-DIE75TrB.js → McpTools-CsGIijNe.js} +1 -1
- package/src/assets/web-panel/assets/{Memory-C4KVnLlp.js → Memory-BXX_yMKJ.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-DuzrHMAk.js → Notes-DU6Vf2cL.js} +1 -1
- package/src/assets/web-panel/assets/{Organization-DTq6uF82.js → Organization-Bny6yOPV.js} +4 -4
- package/src/assets/web-panel/assets/{P2P-C0hjlhsR.js → P2P-BxFZ1Bit.js} +2 -2
- package/src/assets/web-panel/assets/{Permissions-Ec0NH-xC.js → Permissions-B1j3Mtms.js} +3 -3
- package/src/assets/web-panel/assets/{Projects-U8D0asCS.js → Projects-D-CGscDu.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-BngtTLvJ.js → Providers-r6NaBYMf.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B9NbwCKM.js → RssFeed-D7b68C5q.js} +1 -1
- package/src/assets/web-panel/assets/{Security-BL5Rkr1T.js → Security-MJfKv0EJ.js} +3 -3
- package/src/assets/web-panel/assets/{Services-D4MJzLld.js → Services-Yb_Q1V3d.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-CQTOMDwF.js → Skills-DLTHcH5T.js} +1 -1
- package/src/assets/web-panel/assets/{Tasks-DepbJMnL.js → Tasks-CqycpPjS.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-C24PVZPu.js → Templates-y01u2Zis.js} +1 -1
- package/src/assets/web-panel/assets/VideoEditing-BA1N-5kq.css +1 -0
- package/src/assets/web-panel/assets/VideoEditing-B_nPKw6B.js +1 -0
- package/src/assets/web-panel/assets/{Wallet-PQoSpN_P.js → Wallet-CsRgnjJY.js} +1 -1
- package/src/assets/web-panel/assets/{WebAuthn-BcuyQ4Lr.js → WebAuthn-DWoR5ADp.js} +1 -1
- package/src/assets/web-panel/assets/{WorkflowEditor-C-SvXbHW.js → WorkflowEditor-DBJhFPMN.js} +1 -1
- package/src/assets/web-panel/assets/{antd-DEjZPGMj.js → antd-Dh2t0vGq.js} +84 -84
- package/src/assets/web-panel/assets/index-tN-8TosE.js +2 -0
- package/src/assets/web-panel/assets/{markdown-CusdXFxb.js → markdown-CBnGGMzE.js} +1 -1
- package/src/assets/web-panel/index.html +2 -2
- package/src/commands/agent.js +20 -0
- package/src/commands/mcp.js +86 -4
- package/src/commands/memory.js +85 -4
- package/src/commands/sandbox.js +80 -6
- package/src/commands/serve.js +10 -0
- package/src/commands/session.js +250 -0
- package/src/commands/stream.js +75 -0
- package/src/commands/video.js +363 -0
- package/src/gateways/http/envelope-http-server.js +194 -0
- package/src/gateways/ws/message-dispatcher.js +123 -0
- package/src/gateways/ws/session-core-protocol.js +427 -0
- package/src/gateways/ws/session-protocol.js +42 -1
- package/src/gateways/ws/video-protocol.js +230 -0
- package/src/gateways/ws/ws-server.js +72 -0
- package/src/gateways/ws/ws-session-gateway.js +7 -3
- package/src/harness/jsonl-session-store.js +17 -9
- package/src/index.js +8 -0
- package/src/lib/agent-stream.js +63 -0
- package/src/lib/chat-core.js +183 -6
- package/src/lib/cowork/ab-comparator-cli.js +44 -23
- package/src/lib/cowork/agent-group-runner.js +145 -0
- package/src/lib/cowork/debate-review-cli.js +47 -25
- package/src/lib/cowork/project-style-analyzer-cli.js +34 -7
- package/src/lib/interaction-adapter.js +59 -1
- package/src/lib/jsonl-session-store.js +2 -0
- package/src/lib/memory-injection.js +90 -0
- package/src/lib/provider-stream.js +120 -0
- package/src/lib/sandbox-v2.js +198 -3
- package/src/lib/session-consolidator.js +125 -0
- package/src/lib/session-core-singletons.js +56 -0
- package/src/lib/session-tail.js +128 -0
- package/src/lib/session-usage.js +166 -0
- package/src/lib/shell-approval.js +96 -0
- package/src/lib/ws-chat-handler.js +3 -0
- package/src/repl/agent-repl.js +271 -6
- package/src/repl/chat-repl.js +87 -100
- package/src/runtime/agent-core.js +98 -15
- package/src/runtime/agent-runtime.js +105 -3
- package/src/runtime/policies/agent-policy.js +10 -0
- package/src/skills/video-editing/SKILL.md +46 -0
- package/src/skills/video-editing/beat-snap.js +127 -0
- package/src/skills/video-editing/extractors/audio-extractor.js +212 -0
- package/src/skills/video-editing/extractors/subtitle-extractor.js +90 -0
- package/src/skills/video-editing/extractors/video-extractor.js +137 -0
- package/src/skills/video-editing/parallel-orchestrator.js +212 -0
- package/src/skills/video-editing/pipeline.js +480 -0
- package/src/skills/video-editing/prompts/aesthetic-analysis.md +21 -0
- package/src/skills/video-editing/prompts/audio-segment.md +15 -0
- package/src/skills/video-editing/prompts/character-identify.md +19 -0
- package/src/skills/video-editing/prompts/dense-caption.md +20 -0
- package/src/skills/video-editing/prompts/editor-system.md +29 -0
- package/src/skills/video-editing/prompts/hook-dialogue.md +17 -0
- package/src/skills/video-editing/prompts/protagonist-detect.md +20 -0
- package/src/skills/video-editing/prompts/scene-caption.md +16 -0
- package/src/skills/video-editing/prompts/shot-caption.md +25 -0
- package/src/skills/video-editing/prompts/shot-plan.md +28 -0
- package/src/skills/video-editing/prompts/structure-proposal.md +16 -0
- package/src/skills/video-editing/prompts/vlog-scene-caption.md +18 -0
- package/src/skills/video-editing/render/audio-mix.js +128 -0
- package/src/skills/video-editing/render/ffmpeg-concat.js +45 -0
- package/src/skills/video-editing/render/ffmpeg-extract.js +67 -0
- package/src/skills/video-editing/reviewer.js +161 -0
- package/src/skills/video-editing/tools/commit.js +108 -0
- package/src/skills/video-editing/tools/review-clip.js +46 -0
- package/src/skills/video-editing/tools/semantic-retrieval.js +56 -0
- package/src/skills/video-editing/tools/shot-trimming.js +73 -0
- package/src/assets/web-panel/assets/Analytics-B4OM8S8X.css +0 -1
- package/src/assets/web-panel/assets/Analytics-DgypYeUB.js +0 -3
- package/src/assets/web-panel/assets/AppLayout-Bzf3mSZI.js +0 -1
- package/src/assets/web-panel/assets/AppLayout-DQyDwGut.css +0 -1
- package/src/assets/web-panel/assets/index-CwvzTTw_.js +0 -2
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Structure Proposal
|
|
2
|
+
|
|
3
|
+
从全部场景中选 8-15 个匹配用户创意的场景。
|
|
4
|
+
|
|
5
|
+
**输入**: 用户指令 + 音乐段落总结 + 全部场景列表
|
|
6
|
+
**输出**: 严格 JSON
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"theme": "<the picked angle>",
|
|
11
|
+
"narrative_logic": "<why these scenes in this order>",
|
|
12
|
+
"scene_indices": [3, 7, 12, 18, 24]
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
**铁律**: 选中的每个场景都必须把 `{{MAIN_CHARACTER_NAME}}` 作为主要视觉主体。只输出 JSON。
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Vlog Scene Caption
|
|
2
|
+
|
|
3
|
+
分析旅行 vlog 场景的剪辑潜力。
|
|
4
|
+
|
|
5
|
+
**输入**: 帧序列 + 地点上下文
|
|
6
|
+
**输出**: 严格 JSON
|
|
7
|
+
|
|
8
|
+
```json
|
|
9
|
+
{
|
|
10
|
+
"classification": "landscape | activity | talking | transit | food",
|
|
11
|
+
"visual_analysis": "<风光/构图/光线>",
|
|
12
|
+
"journey_context": "<这个场景在旅程中的位置>",
|
|
13
|
+
"creator_presence": "primary | background | absent",
|
|
14
|
+
"score": { "landscape_beauty": 0, "authenticity": 0, "expression": 0 }
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
每项 0-5。只输出 JSON。
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audio-mix.js — 混音:视频 + 背景音乐
|
|
3
|
+
*
|
|
4
|
+
* MVP: 简单叠加 + loudnorm
|
|
5
|
+
* Phase 4 扩展: 对话区 ducking + 结尾 fade
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { spawn } from "child_process";
|
|
9
|
+
|
|
10
|
+
export async function mixAudio(videoPath, audioPath, outputPath, options = {}) {
|
|
11
|
+
const bgmVolume = options.bgmVolume ?? 0.3;
|
|
12
|
+
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
const filter = [
|
|
15
|
+
`[1:a]volume=${bgmVolume},loudnorm=I=-16:LRA=11:TP=-1.5[bgm]`,
|
|
16
|
+
`[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout]`,
|
|
17
|
+
].join(";");
|
|
18
|
+
|
|
19
|
+
const args = [
|
|
20
|
+
"-y",
|
|
21
|
+
"-i",
|
|
22
|
+
videoPath,
|
|
23
|
+
"-i",
|
|
24
|
+
audioPath,
|
|
25
|
+
"-filter_complex",
|
|
26
|
+
filter,
|
|
27
|
+
"-map",
|
|
28
|
+
"0:v",
|
|
29
|
+
"-map",
|
|
30
|
+
"[aout]",
|
|
31
|
+
"-c:v",
|
|
32
|
+
"copy",
|
|
33
|
+
"-c:a",
|
|
34
|
+
"aac",
|
|
35
|
+
"-b:a",
|
|
36
|
+
"192k",
|
|
37
|
+
"-shortest",
|
|
38
|
+
outputPath,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
42
|
+
let stderr = "";
|
|
43
|
+
proc.stderr.on("data", (d) => (stderr += d));
|
|
44
|
+
proc.on("close", (code) => {
|
|
45
|
+
if (code !== 0)
|
|
46
|
+
return reject(
|
|
47
|
+
new Error(`ffmpeg mix exit ${code}: ${stderr.slice(-300)}`),
|
|
48
|
+
);
|
|
49
|
+
resolve(outputPath);
|
|
50
|
+
});
|
|
51
|
+
proc.on("error", reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function mixAudioWithDucking(
|
|
56
|
+
videoPath,
|
|
57
|
+
audioPath,
|
|
58
|
+
outputPath,
|
|
59
|
+
options = {},
|
|
60
|
+
) {
|
|
61
|
+
const bgmVolume = options.bgmVolume ?? 0.3;
|
|
62
|
+
const fadeOutDuration = options.fadeOutDuration ?? 3;
|
|
63
|
+
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const filter = [
|
|
66
|
+
`[0:a]aresample=async=1[speech]`,
|
|
67
|
+
`[1:a]volume=${bgmVolume},loudnorm=I=-16:LRA=11:TP=-1.5[bgm_norm]`,
|
|
68
|
+
`[bgm_norm][speech]sidechaincompress=threshold=0.02:ratio=6:attack=200:release=1000[bgm_duck]`,
|
|
69
|
+
`[speech][bgm_duck]amix=inputs=2:duration=first:dropout_transition=2,afade=t=out:d=${fadeOutDuration}:curve=tri[aout]`,
|
|
70
|
+
].join(";");
|
|
71
|
+
|
|
72
|
+
const args = buildFfmpegArgs(videoPath, audioPath, outputPath, filter);
|
|
73
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
74
|
+
let stderr = "";
|
|
75
|
+
proc.stderr.on("data", (d) => (stderr += d.toString("utf8")));
|
|
76
|
+
proc.on("close", (code) => {
|
|
77
|
+
if (code !== 0)
|
|
78
|
+
return reject(
|
|
79
|
+
new Error(`ffmpeg ducking mix exit ${code}: ${stderr.slice(-300)}`),
|
|
80
|
+
);
|
|
81
|
+
resolve(outputPath);
|
|
82
|
+
});
|
|
83
|
+
proc.on("error", reject);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function buildDuckingFilter(options = {}) {
|
|
88
|
+
const bgmVolume = options.bgmVolume ?? 0.3;
|
|
89
|
+
const fadeOutDuration = options.fadeOutDuration ?? 3;
|
|
90
|
+
return [
|
|
91
|
+
`[0:a]aresample=async=1[speech]`,
|
|
92
|
+
`[1:a]volume=${bgmVolume},loudnorm=I=-16:LRA=11:TP=-1.5[bgm_norm]`,
|
|
93
|
+
`[bgm_norm][speech]sidechaincompress=threshold=0.02:ratio=6:attack=200:release=1000[bgm_duck]`,
|
|
94
|
+
`[speech][bgm_duck]amix=inputs=2:duration=first:dropout_transition=2,afade=t=out:d=${fadeOutDuration}:curve=tri[aout]`,
|
|
95
|
+
].join(";");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function buildSimpleFilter(options = {}) {
|
|
99
|
+
const bgmVolume = options.bgmVolume ?? 0.3;
|
|
100
|
+
return [
|
|
101
|
+
`[1:a]volume=${bgmVolume},loudnorm=I=-16:LRA=11:TP=-1.5[bgm]`,
|
|
102
|
+
`[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[aout]`,
|
|
103
|
+
].join(";");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildFfmpegArgs(videoPath, audioPath, outputPath, filter) {
|
|
107
|
+
return [
|
|
108
|
+
"-y",
|
|
109
|
+
"-i",
|
|
110
|
+
videoPath,
|
|
111
|
+
"-i",
|
|
112
|
+
audioPath,
|
|
113
|
+
"-filter_complex",
|
|
114
|
+
filter,
|
|
115
|
+
"-map",
|
|
116
|
+
"0:v",
|
|
117
|
+
"-map",
|
|
118
|
+
"[aout]",
|
|
119
|
+
"-c:v",
|
|
120
|
+
"copy",
|
|
121
|
+
"-c:a",
|
|
122
|
+
"aac",
|
|
123
|
+
"-b:a",
|
|
124
|
+
"192k",
|
|
125
|
+
"-shortest",
|
|
126
|
+
outputPath,
|
|
127
|
+
];
|
|
128
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ffmpeg-concat.js — concat demuxer 拼接多个片段
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
export async function concatClips(clipPaths, workDir) {
|
|
10
|
+
if (clipPaths.length === 0) throw new Error("No clips to concatenate");
|
|
11
|
+
|
|
12
|
+
if (clipPaths.length === 1) return clipPaths[0];
|
|
13
|
+
|
|
14
|
+
const listPath = path.join(workDir, "concat_list.txt");
|
|
15
|
+
const lines = clipPaths.map((p) => `file '${p.replace(/'/g, "'\\''")}'`);
|
|
16
|
+
await fs.writeFile(listPath, lines.join("\n"));
|
|
17
|
+
|
|
18
|
+
const outPath = path.join(workDir, "concat_output.mp4");
|
|
19
|
+
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const args = [
|
|
22
|
+
"-y",
|
|
23
|
+
"-f",
|
|
24
|
+
"concat",
|
|
25
|
+
"-safe",
|
|
26
|
+
"0",
|
|
27
|
+
"-i",
|
|
28
|
+
listPath,
|
|
29
|
+
"-c",
|
|
30
|
+
"copy",
|
|
31
|
+
outPath,
|
|
32
|
+
];
|
|
33
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
34
|
+
let stderr = "";
|
|
35
|
+
proc.stderr.on("data", (d) => (stderr += d));
|
|
36
|
+
proc.on("close", (code) => {
|
|
37
|
+
if (code !== 0)
|
|
38
|
+
return reject(
|
|
39
|
+
new Error(`ffmpeg concat exit ${code}: ${stderr.slice(-300)}`),
|
|
40
|
+
);
|
|
41
|
+
resolve(outPath);
|
|
42
|
+
});
|
|
43
|
+
proc.on("error", reject);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ffmpeg-extract.js — 按 shot_point 从视频中抽取片段
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { spawn } from "child_process";
|
|
6
|
+
import { promises as fs } from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
|
|
9
|
+
export async function extractClips(videoPath, shotPoints, workDir) {
|
|
10
|
+
const clipsDir = path.join(workDir, "clips");
|
|
11
|
+
await fs.mkdir(clipsDir, { recursive: true });
|
|
12
|
+
|
|
13
|
+
const clipPaths = [];
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < shotPoints.length; i++) {
|
|
16
|
+
const entry = shotPoints[i];
|
|
17
|
+
const clips = entry.clips || [];
|
|
18
|
+
|
|
19
|
+
for (let j = 0; j < clips.length; j++) {
|
|
20
|
+
const clip = clips[j];
|
|
21
|
+
const outFile = path.join(clipsDir, `clip_${i}_${j}.mp4`);
|
|
22
|
+
await extractSingle(
|
|
23
|
+
videoPath,
|
|
24
|
+
clip.start,
|
|
25
|
+
clip.duration || clip.end - clip.start,
|
|
26
|
+
outFile,
|
|
27
|
+
);
|
|
28
|
+
clipPaths.push(outFile);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return clipPaths;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function extractSingle(videoPath, startSec, duration, outFile) {
|
|
36
|
+
return new Promise((resolve, reject) => {
|
|
37
|
+
const args = [
|
|
38
|
+
"-y",
|
|
39
|
+
"-ss",
|
|
40
|
+
String(startSec),
|
|
41
|
+
"-i",
|
|
42
|
+
videoPath,
|
|
43
|
+
"-t",
|
|
44
|
+
String(duration),
|
|
45
|
+
"-c",
|
|
46
|
+
"copy",
|
|
47
|
+
"-avoid_negative_ts",
|
|
48
|
+
"make_zero",
|
|
49
|
+
outFile,
|
|
50
|
+
];
|
|
51
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
52
|
+
let stderr = "";
|
|
53
|
+
proc.stderr.on("data", (d) => (stderr += d));
|
|
54
|
+
proc.on("close", (code) => {
|
|
55
|
+
if (code !== 0)
|
|
56
|
+
return reject(
|
|
57
|
+
new Error(`ffmpeg extract exit ${code}: ${stderr.slice(-300)}`),
|
|
58
|
+
);
|
|
59
|
+
resolve(outFile);
|
|
60
|
+
});
|
|
61
|
+
proc.on("error", reject);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function extractSingleClip(videoPath, start, end, outFile) {
|
|
66
|
+
return extractSingle(videoPath, start, end - start, outFile);
|
|
67
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* reviewer.js — Reviewer Gate: VLM 检查 commit 质量
|
|
3
|
+
*
|
|
4
|
+
* 在 commit 前调用 VLM 检查主角占比/美学分:
|
|
5
|
+
* - protagonist_ratio >= threshold → pass
|
|
6
|
+
* - 不达标 → reject (触发 rerun)
|
|
7
|
+
*
|
|
8
|
+
* 扩展 ApprovalGate 为 quality-check policy 类型。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_THRESHOLDS = {
|
|
12
|
+
protagonist_ratio: 0.5,
|
|
13
|
+
aesthetic_score: 2.5,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Quality checker registry.
|
|
18
|
+
* Each checker: { name, check(entry, context) => { pass, score, reason } }
|
|
19
|
+
*/
|
|
20
|
+
const checkerRegistry = new Map();
|
|
21
|
+
|
|
22
|
+
export function registerChecker(name, checkFn) {
|
|
23
|
+
checkerRegistry.set(name, { name, check: checkFn });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getChecker(name) {
|
|
27
|
+
return checkerRegistry.get(name);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function listCheckers() {
|
|
31
|
+
return Array.from(checkerRegistry.keys());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ── Built-in checkers ─────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
registerChecker("vision-protagonist", async (entry, context) => {
|
|
37
|
+
if (!context.llmCall) {
|
|
38
|
+
return { pass: true, score: 0.5, reason: "No VLM available, skipped" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const result = await context.llmCall({
|
|
42
|
+
type: "protagonist-detect",
|
|
43
|
+
clips: entry.clips,
|
|
44
|
+
mainCharacter: context.mainCharacter,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const ratio = result?.protagonist_ratio ?? result?.ratio ?? 0;
|
|
48
|
+
const threshold =
|
|
49
|
+
context.thresholds?.protagonist_ratio ??
|
|
50
|
+
DEFAULT_THRESHOLDS.protagonist_ratio;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
pass: ratio >= threshold,
|
|
54
|
+
score: ratio,
|
|
55
|
+
reason:
|
|
56
|
+
ratio >= threshold
|
|
57
|
+
? `Protagonist ratio ${(ratio * 100).toFixed(0)}% >= ${(threshold * 100).toFixed(0)}%`
|
|
58
|
+
: `Protagonist ratio ${(ratio * 100).toFixed(0)}% < ${(threshold * 100).toFixed(0)}% threshold`,
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
registerChecker("aesthetic-score", async (entry, context) => {
|
|
63
|
+
if (!context.llmCall) {
|
|
64
|
+
return { pass: true, score: 3.0, reason: "No VLM available, skipped" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result = await context.llmCall({
|
|
68
|
+
type: "aesthetic-analysis",
|
|
69
|
+
clips: entry.clips,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const score = result?.overall_aesthetic_score ?? 3.0;
|
|
73
|
+
const threshold =
|
|
74
|
+
context.thresholds?.aesthetic_score ?? DEFAULT_THRESHOLDS.aesthetic_score;
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
pass: score >= threshold,
|
|
78
|
+
score,
|
|
79
|
+
reason:
|
|
80
|
+
score >= threshold
|
|
81
|
+
? `Aesthetic ${score.toFixed(1)} >= ${threshold}`
|
|
82
|
+
: `Aesthetic ${score.toFixed(1)} < ${threshold} threshold`,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Review an entry against specified checkers.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} entry - shot_point entry with clips
|
|
90
|
+
* @param {string[]} checkerNames - names of checkers to run
|
|
91
|
+
* @param {object} context - { llmCall, mainCharacter, thresholds }
|
|
92
|
+
* @returns {{ pass: boolean, checks: object[], aggregateScore: number }}
|
|
93
|
+
*/
|
|
94
|
+
export async function reviewEntry(entry, checkerNames, context) {
|
|
95
|
+
const checks = [];
|
|
96
|
+
|
|
97
|
+
for (const name of checkerNames) {
|
|
98
|
+
const checker = checkerRegistry.get(name);
|
|
99
|
+
if (!checker) {
|
|
100
|
+
checks.push({
|
|
101
|
+
name,
|
|
102
|
+
pass: true,
|
|
103
|
+
score: 0,
|
|
104
|
+
reason: `Unknown checker: ${name}`,
|
|
105
|
+
});
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const result = await checker.check(entry, context);
|
|
111
|
+
checks.push({ name, ...result });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
checks.push({
|
|
114
|
+
name,
|
|
115
|
+
pass: false,
|
|
116
|
+
score: 0,
|
|
117
|
+
reason: `Checker error: ${err.message}`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const allPass = checks.every((c) => c.pass);
|
|
123
|
+
const aggregateScore =
|
|
124
|
+
checks.length > 0
|
|
125
|
+
? checks.reduce((s, c) => s + c.score, 0) / checks.length
|
|
126
|
+
: 0;
|
|
127
|
+
|
|
128
|
+
return { pass: allPass, checks, aggregateScore };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Quality-check policy for ApprovalGate integration.
|
|
133
|
+
*
|
|
134
|
+
* Usage:
|
|
135
|
+
* const policy = createQualityCheckPolicy(["vision-protagonist"], { thresholds, onFail: "rerun" });
|
|
136
|
+
* const result = await policy.evaluate(entry, context);
|
|
137
|
+
*/
|
|
138
|
+
export function createQualityCheckPolicy(checkerNames, options = {}) {
|
|
139
|
+
return {
|
|
140
|
+
type: "quality-check",
|
|
141
|
+
checkers: checkerNames,
|
|
142
|
+
onFail: options.onFail || "rerun",
|
|
143
|
+
thresholds: options.thresholds || DEFAULT_THRESHOLDS,
|
|
144
|
+
|
|
145
|
+
async evaluate(entry, context) {
|
|
146
|
+
const mergedCtx = {
|
|
147
|
+
...context,
|
|
148
|
+
thresholds: {
|
|
149
|
+
...DEFAULT_THRESHOLDS,
|
|
150
|
+
...this.thresholds,
|
|
151
|
+
...context.thresholds,
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const review = await reviewEntry(entry, this.checkers, mergedCtx);
|
|
155
|
+
return {
|
|
156
|
+
...review,
|
|
157
|
+
action: review.pass ? "approve" : this.onFail,
|
|
158
|
+
};
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video_commit_clip — 提交选定片段(最多 3 段拼接成一个镜头)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
export const TOOL_DEF = {
|
|
9
|
+
name: "video_commit_clip",
|
|
10
|
+
description:
|
|
11
|
+
"Finalize shot selection with multi-clip stitching (up to 3 clips per output).",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
section_idx: {
|
|
16
|
+
type: "number",
|
|
17
|
+
description: "Section index in shot plan",
|
|
18
|
+
},
|
|
19
|
+
shot_idx: { type: "number", description: "Shot index within section" },
|
|
20
|
+
clips: {
|
|
21
|
+
type: "array",
|
|
22
|
+
items: {
|
|
23
|
+
type: "object",
|
|
24
|
+
properties: {
|
|
25
|
+
start: { type: "number" },
|
|
26
|
+
end: { type: "number" },
|
|
27
|
+
},
|
|
28
|
+
required: ["start", "end"],
|
|
29
|
+
},
|
|
30
|
+
maxItems: 3,
|
|
31
|
+
description: "1-3 clips to stitch",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["section_idx", "shot_idx", "clips"],
|
|
35
|
+
},
|
|
36
|
+
isReadOnly: false,
|
|
37
|
+
riskLevel: "MEDIUM",
|
|
38
|
+
availableInPlanMode: false,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export async function execute({ section_idx, shot_idx, clips }, context) {
|
|
42
|
+
if (!clips || clips.length === 0) {
|
|
43
|
+
return { status: "error", message: "No clips provided." };
|
|
44
|
+
}
|
|
45
|
+
if (clips.length > 3) {
|
|
46
|
+
return { status: "error", message: "Max 3 clips per commit." };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const clip of clips) {
|
|
50
|
+
if (clip.end <= clip.start) {
|
|
51
|
+
return {
|
|
52
|
+
status: "error",
|
|
53
|
+
message: `Invalid clip: start=${clip.start} >= end=${clip.end}`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const committed = context.committedClips || [];
|
|
59
|
+
for (const clip of clips) {
|
|
60
|
+
for (const existing of committed) {
|
|
61
|
+
if (clip.start < existing.end && clip.end > existing.start) {
|
|
62
|
+
return {
|
|
63
|
+
status: "conflict",
|
|
64
|
+
message: `Clip [${clip.start}-${clip.end}] overlaps with committed [${existing.start}-${existing.end}]`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const totalDuration = clips.reduce((s, c) => s + (c.end - c.start), 0);
|
|
71
|
+
const entry = {
|
|
72
|
+
section_idx,
|
|
73
|
+
shot_idx,
|
|
74
|
+
clips: clips.map((c, i) => ({
|
|
75
|
+
idx: i,
|
|
76
|
+
start: c.start,
|
|
77
|
+
end: c.end,
|
|
78
|
+
duration: parseFloat((c.end - c.start).toFixed(3)),
|
|
79
|
+
})),
|
|
80
|
+
total_duration: parseFloat(totalDuration.toFixed(3)),
|
|
81
|
+
committed_at: new Date().toISOString(),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
committed.push(
|
|
85
|
+
...clips.map((c) => ({
|
|
86
|
+
start: c.start,
|
|
87
|
+
end: c.end,
|
|
88
|
+
section_idx,
|
|
89
|
+
shot_idx,
|
|
90
|
+
})),
|
|
91
|
+
);
|
|
92
|
+
context.committedClips = committed;
|
|
93
|
+
|
|
94
|
+
if (context.shotPointPath) {
|
|
95
|
+
let existing = [];
|
|
96
|
+
try {
|
|
97
|
+
const raw = await fs.readFile(context.shotPointPath, "utf-8");
|
|
98
|
+
existing = JSON.parse(raw);
|
|
99
|
+
} catch {}
|
|
100
|
+
existing.push(entry);
|
|
101
|
+
await fs.writeFile(
|
|
102
|
+
context.shotPointPath,
|
|
103
|
+
JSON.stringify(existing, null, 2),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { status: "success", ...entry };
|
|
108
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video_review_clip — 检查候选片段与已 commit 片段的时间区间冲突
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const TOOL_DEF = {
|
|
6
|
+
name: "video_review_clip",
|
|
7
|
+
description:
|
|
8
|
+
"Check a candidate clip for overlap against previously committed clips.",
|
|
9
|
+
inputSchema: {
|
|
10
|
+
type: "object",
|
|
11
|
+
properties: {
|
|
12
|
+
start: { type: "number", description: "Candidate start time (seconds)" },
|
|
13
|
+
end: { type: "number", description: "Candidate end time (seconds)" },
|
|
14
|
+
},
|
|
15
|
+
required: ["start", "end"],
|
|
16
|
+
},
|
|
17
|
+
isReadOnly: true,
|
|
18
|
+
riskLevel: "LOW",
|
|
19
|
+
availableInPlanMode: true,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function execute({ start, end }, context) {
|
|
23
|
+
const committed = context.committedClips || [];
|
|
24
|
+
const overlaps = [];
|
|
25
|
+
|
|
26
|
+
for (const clip of committed) {
|
|
27
|
+
if (start < clip.end && end > clip.start) {
|
|
28
|
+
overlaps.push({
|
|
29
|
+
conflict_with: clip,
|
|
30
|
+
overlap_start: Math.max(start, clip.start),
|
|
31
|
+
overlap_end: Math.min(end, clip.end),
|
|
32
|
+
overlap_duration: Math.min(end, clip.end) - Math.max(start, clip.start),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
candidate: { start, end, duration: end - start },
|
|
39
|
+
has_conflict: overlaps.length > 0,
|
|
40
|
+
overlaps,
|
|
41
|
+
suggestion:
|
|
42
|
+
overlaps.length > 0
|
|
43
|
+
? `Avoid time ranges: ${overlaps.map((o) => `[${o.overlap_start.toFixed(1)}-${o.overlap_end.toFixed(1)}]`).join(", ")}`
|
|
44
|
+
: "No conflicts, safe to commit.",
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* video_semantic_retrieval — 在 scene 索引中按范围拉候选镜头
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { promises as fs } from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
|
|
8
|
+
export const TOOL_DEF = {
|
|
9
|
+
name: "video_semantic_retrieval",
|
|
10
|
+
description:
|
|
11
|
+
"Explore scene metadata within a bounded range to discover candidate shots.",
|
|
12
|
+
inputSchema: {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
scene_start: {
|
|
16
|
+
type: "number",
|
|
17
|
+
description: "Start scene index (inclusive)",
|
|
18
|
+
},
|
|
19
|
+
scene_end: { type: "number", description: "End scene index (inclusive)" },
|
|
20
|
+
query: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Optional semantic query to filter by",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
required: ["scene_start", "scene_end"],
|
|
26
|
+
},
|
|
27
|
+
isReadOnly: true,
|
|
28
|
+
riskLevel: "LOW",
|
|
29
|
+
availableInPlanMode: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export async function execute({ scene_start, scene_end, query }, context) {
|
|
33
|
+
const scenePath = path.join(context.assetDir, "scene.json");
|
|
34
|
+
const raw = await fs.readFile(scenePath, "utf-8");
|
|
35
|
+
const { scenes } = JSON.parse(raw);
|
|
36
|
+
|
|
37
|
+
const start = Math.max(0, scene_start);
|
|
38
|
+
const end = Math.min(scenes.length - 1, scene_end);
|
|
39
|
+
let candidates = scenes.slice(start, end + 1).map((s, i) => ({
|
|
40
|
+
scene_idx: start + i,
|
|
41
|
+
...s,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
if (query) {
|
|
45
|
+
const q = query.toLowerCase();
|
|
46
|
+
candidates = candidates.filter((c) =>
|
|
47
|
+
JSON.stringify(c).toLowerCase().includes(q),
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
total_scenes: scenes.length,
|
|
53
|
+
range: [start, end],
|
|
54
|
+
candidates,
|
|
55
|
+
};
|
|
56
|
+
}
|