@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.69",
3
+ "version": "0.15.71",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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
- const subtitleText = typeof seg.subtitle_text === 'string' ? seg.subtitle_text.trim() : '';
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
- if (Array.isArray(plan?.phases)) return plan.phases.length;
47
- if (Array.isArray(plan?.sections)) return plan.sections.length;
48
- return null;
54
+ const segments = planSegments(plan);
55
+ return segments ? segments.length : null;
49
56
  }
50
57
 
51
- const PIPELINE_SENTINEL_KEY = 'detail_sections_version';
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[PIPELINE_SENTINEL_KEY]) {
64
+ if (!planSegments(plan)) {
56
65
  throw new Error(
57
- 'pipeline_violation: plan must come from detail_sections output. '
58
- + 'Required pipeline: analyze_page plan_video detail_sections record_url_narration compose_video submit_to_library. '
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
- warning = `WARNING: all ${imagePaths.length} image segments reuse the same file (${imagePaths[0]}). `
68
- + 'The output will be near-static — verify the source page actually rendered before submitting this video.';
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
- if (warning) lines.push(warning);
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
- }