@lightcone-ai/daemon 0.15.69 → 0.15.71
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 +1 -1
- package/src/_vendor/video/composer-v2/index.js +8 -1
- package/src/mcp-config.js +0 -1
- package/src/record-url-narration-tool.js +17 -9
- package/src/tools/compose-video-v2.js +21 -4
- package/mcp-servers/official/video-narration-planner/index.js +0 -37
- package/mcp-servers/official/video-narration-planner/manifest.json +0 -44
package/package.json
CHANGED
|
@@ -292,7 +292,14 @@ export async function composeVideoV2({
|
|
|
292
292
|
finalClip = await silentClip({ videoPath: visualClip.path, duration: visualClip.duration, tmpDir });
|
|
293
293
|
}
|
|
294
294
|
|
|
295
|
-
|
|
295
|
+
// Accept `text` as an alias for `subtitle_text`: plan_video_segments takes
|
|
296
|
+
// segment narration as `text` on input, compose_video_v2's canonical name is
|
|
297
|
+
// `subtitle_text`. Either reaches the burn pass so subtitles aren't silently dropped.
|
|
298
|
+
const subtitleText = (
|
|
299
|
+
typeof seg.subtitle_text === 'string' ? seg.subtitle_text
|
|
300
|
+
: typeof seg.text === 'string' ? seg.text
|
|
301
|
+
: ''
|
|
302
|
+
).trim();
|
|
296
303
|
readyClips.push({ path: finalClip, duration: visualClip.duration, transition, subtitleText });
|
|
297
304
|
}
|
|
298
305
|
|
package/src/mcp-config.js
CHANGED
|
@@ -83,7 +83,6 @@ const SERVER_BACKED_MCP_SERVERS = new Set([
|
|
|
83
83
|
'portfolio-analysis',
|
|
84
84
|
// thin-proxy MCP services migrated to the server (roadmap §4) — every one of
|
|
85
85
|
// these uses startThinProxy() and requires the SERVER_URL/MACHINE_API_KEY/AGENT_ID triple.
|
|
86
|
-
'video-narration-planner',
|
|
87
86
|
'page-understanding',
|
|
88
87
|
'platform-policy-db',
|
|
89
88
|
'keyword-research',
|
|
@@ -39,24 +39,32 @@ function deriveDurationMs(recorderOutput) {
|
|
|
39
39
|
return lastTms > 0 ? lastTms : null;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
function planSegments(plan) {
|
|
43
|
+
if (!isPlainObject(plan)) return null;
|
|
44
|
+
for (const key of ['phases', 'sections', 'segments']) {
|
|
45
|
+
if (Array.isArray(plan[key]) && plan[key].length > 0) return plan[key];
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
function derivePhaseCount({ plan, recorderOutput }) {
|
|
43
51
|
const explicit = normalizeNumberOrNull(recorderOutput?.phases);
|
|
44
52
|
if (explicit != null) return explicit;
|
|
45
53
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
return null;
|
|
54
|
+
const segments = planSegments(plan);
|
|
55
|
+
return segments ? segments.length : null;
|
|
49
56
|
}
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
58
|
+
// record_url_narration is an atomic tool, not the tail of a fixed pipeline.
|
|
59
|
+
// The plan may be hand-written by the scripter or produced by plan_video_segments;
|
|
60
|
+
// it just needs a non-empty list of segments with per-segment visual action + duration
|
|
61
|
+
// so the recording stays in sync with the narration audio.
|
|
53
62
|
function assertPipelineCompliance(plan) {
|
|
54
63
|
if (!isPlainObject(plan)) return;
|
|
55
|
-
if (!plan
|
|
64
|
+
if (!planSegments(plan)) {
|
|
56
65
|
throw new Error(
|
|
57
|
-
'
|
|
58
|
-
+ '
|
|
59
|
-
+ 'Do not hand-write phases or bypass detail_sections.'
|
|
66
|
+
'record_url_narration: `plan` must contain a non-empty `phases` (or `sections` / `segments`) array — '
|
|
67
|
+
+ 'either hand-written or from plan_video_segments. Each entry should carry a visual action and a duration.'
|
|
60
68
|
);
|
|
61
69
|
}
|
|
62
70
|
}
|
|
@@ -59,13 +59,30 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
59
59
|
);
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
|
+
const warnings = [];
|
|
62
63
|
// Heuristic warning: a multi-segment image video that reuses one single image
|
|
63
64
|
// will look near-static — usually a sign the source page didn't render and the
|
|
64
65
|
// agent fell back to one blank screenshot.
|
|
65
|
-
let warning = null;
|
|
66
66
|
if (imagePaths.length >= 2 && new Set(imagePaths).size === 1) {
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
warnings.push(
|
|
68
|
+
`WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
|
|
69
|
+
+ 'The output will be near-static — verify the source page actually rendered before submitting this video.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
// Warn when narration is present but no subtitle text is — compose_video_v2 burns
|
|
73
|
+
// subtitles only from `subtitle_text` (or its `text` alias); without it the video
|
|
74
|
+
// ships with no captions. Simplest fix: pass plan_video_segments' output verbatim.
|
|
75
|
+
{
|
|
76
|
+
const hasSubText = s => (typeof s?.subtitle_text === 'string' && s.subtitle_text.trim())
|
|
77
|
+
|| (typeof s?.text === 'string' && s.text.trim());
|
|
78
|
+
const narratedNoSub = segments.filter(s =>
|
|
79
|
+
(typeof s?.audio_path === 'string' && s.audio_path.trim()) && !hasSubText(s)).length;
|
|
80
|
+
if (narratedNoSub > 0) {
|
|
81
|
+
warnings.push(
|
|
82
|
+
`WARNING: ${narratedNoSub} segment(s) have narration audio but no subtitle text — the output will have NO subtitles. `
|
|
83
|
+
+ 'If subtitles are wanted, set subtitle_text per segment (or pass the plan_video_segments output array verbatim).'
|
|
84
|
+
);
|
|
85
|
+
}
|
|
69
86
|
}
|
|
70
87
|
|
|
71
88
|
const outDir = workspaceDir
|
|
@@ -89,7 +106,7 @@ export async function runComposeVideoV2Tool({ segments, outro_paths, format, res
|
|
|
89
106
|
`segments=${segments.length}`,
|
|
90
107
|
`outro_clips=${(outro_paths ?? []).length}`,
|
|
91
108
|
];
|
|
92
|
-
|
|
109
|
+
for (const w of warnings) lines.push(w);
|
|
93
110
|
return toolText(lines.join('\n'));
|
|
94
111
|
} catch (error) {
|
|
95
112
|
return toolError(`compose_video_v2 failed: ${error.message}`);
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Thin-proxy. Real impl on lightcone server.
|
|
3
|
-
// Source of truth: src/mcp-services/video-narration-planner/
|
|
4
|
-
import { z } from 'zod';
|
|
5
|
-
import { startThinProxy } from '../../_thin-proxy/forward.js';
|
|
6
|
-
|
|
7
|
-
await startThinProxy({
|
|
8
|
-
serverId: 'video-narration-planner',
|
|
9
|
-
serverName: 'official-video-narration-planner',
|
|
10
|
-
tools: [
|
|
11
|
-
{
|
|
12
|
-
name: 'plan_video',
|
|
13
|
-
description: 'Stage 2: plan narrative arc + phase plan for URL narration video. Enforces highlights<=3 and phases<=5.',
|
|
14
|
-
inputSchema: {
|
|
15
|
-
understanding: z.record(z.any()).describe('Stage 1 output page_understanding object.'),
|
|
16
|
-
persona: z.string().optional(),
|
|
17
|
-
target_platform: z.string(),
|
|
18
|
-
total_duration_s: z.number().int().min(20).max(90).optional(),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
name: 'detail_sections',
|
|
23
|
-
description: 'Stage 3: take agent-written sentences, call TTS voiceover for each phase, and fill duration/dwell.',
|
|
24
|
-
inputSchema: {
|
|
25
|
-
strategy: z.record(z.any()),
|
|
26
|
-
sentences: z.array(z.object({
|
|
27
|
-
phase_id: z.string(),
|
|
28
|
-
text: z.string(),
|
|
29
|
-
})),
|
|
30
|
-
workspace_id: z.string().optional(),
|
|
31
|
-
credential_id: z.string().optional(),
|
|
32
|
-
format: z.enum(['mp3', 'wav', 'flac']).optional(),
|
|
33
|
-
strict_tts: z.boolean().optional(),
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"id": "video-narration-planner",
|
|
3
|
-
"name": "Official Video Narration Planner MCP",
|
|
4
|
-
"version": "0.1.0",
|
|
5
|
-
"runtime": "node",
|
|
6
|
-
"entrypoint": "index.js",
|
|
7
|
-
"tool_declarations": [
|
|
8
|
-
{ "name": "plan_video", "classification": "cacheable" },
|
|
9
|
-
{ "name": "detail_sections", "classification": "mandatory" }
|
|
10
|
-
],
|
|
11
|
-
"tool_block_rules": [
|
|
12
|
-
{
|
|
13
|
-
"workspace_id": "ae63cc9e-feff-4d7e-a62e-a7a7c5fd69d9",
|
|
14
|
-
"agent_id": "91a45fd7-ce5f-4da6-9b27-e34bf7b7c0e2",
|
|
15
|
-
"tools": ["plan_video"],
|
|
16
|
-
"message": "plan_video blocked for editor_in_chief in CvMax. In this workspace, @short_video_scripter owns video planning."
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
"workspace_id": "ae63cc9e-feff-4d7e-a62e-a7a7c5fd69d9",
|
|
20
|
-
"agent_id": "91a45fd7-ce5f-4da6-9b27-e34bf7b7c0e2",
|
|
21
|
-
"tools": ["detail_sections"],
|
|
22
|
-
"message": "detail_sections blocked for editor_in_chief in CvMax. In this workspace, @short_video_scripter owns video planning."
|
|
23
|
-
}
|
|
24
|
-
],
|
|
25
|
-
"smoke_test": {
|
|
26
|
-
"tool": "plan_video",
|
|
27
|
-
"arguments": {
|
|
28
|
-
"understanding": {
|
|
29
|
-
"url": "https://example.com/job-detail",
|
|
30
|
-
"core_message": "岗位职责与薪资透明,适合应届生快速判断是否投递",
|
|
31
|
-
"visual_hotspots": [
|
|
32
|
-
{ "id": "hero", "y_range": [120, 320], "weight": 10, "text_excerpt": "岗位标题与薪资" },
|
|
33
|
-
{ "id": "requirements", "y_range": [780, 1080], "weight": 8, "text_excerpt": "学历与经验要求" },
|
|
34
|
-
{ "id": "apply", "y_range": [1420, 1660], "weight": 7, "text_excerpt": "投递入口" }
|
|
35
|
-
],
|
|
36
|
-
"skip_zones": [
|
|
37
|
-
{ "y_range": [2300, 3200], "reason": "广告与推荐位" }
|
|
38
|
-
]
|
|
39
|
-
},
|
|
40
|
-
"persona": "校招求职学生",
|
|
41
|
-
"target_platform": "douyin"
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
}
|