@lightcone-ai/daemon 0.15.33 → 0.15.35

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.
@@ -1097,127 +1097,6 @@ function resolveVoiceForPhase({ profile, role, index = 0 }) {
1097
1097
  };
1098
1098
  }
1099
1099
 
1100
- function resolveSummaryValue(strategy = {}, slotKey) {
1101
- const summary = toSafeString(strategy?.semantic_summary?.[slotKey]);
1102
- if (summary) return summary;
1103
- const slot = strategy?.semantic_slots?.[slotKey];
1104
- if (!slot) return '';
1105
- if (slotKey === 'recruitment_type') return formatRecruitmentType(slot) || slotValueToText(slot);
1106
- return slotValueToText(slot);
1107
- }
1108
-
1109
- function describeHighlightFocus(phase) {
1110
- const semanticLabel = slotLabel(toSafeString(phase?.semantic_slot));
1111
- const text = toSafeString(phase?.highlight?.text || phase?.highlight?.id || semanticLabel || phase?.visual_action?.target_hotspot);
1112
- if (!text) return '页面核心信息';
1113
- return text;
1114
- }
1115
-
1116
- function buildPhaseSentence({ phase, strategy, phaseIndex }) {
1117
- const role = toSafeString(phase?.role || '').toLowerCase() || (phase?.phase_id === 'cta' ? 'cta' : 'highlight');
1118
- const mode = normalizeModeHint(strategy?.narration_mode) || 'info_summary';
1119
- const slotKey = toSafeString(phase?.semantic_slot);
1120
- const slotText = resolveSummaryValue(strategy, slotKey);
1121
- const slotTextOrFallback = slotText || describeHighlightFocus(phase);
1122
- const company = resolveSummaryValue(strategy, 'company') || '该公司';
1123
- const publishedAt = resolveSummaryValue(strategy, 'published_at');
1124
- const recruitmentType = resolveSummaryValue(strategy, 'recruitment_type') || '招聘';
1125
- const cohort = resolveSummaryValue(strategy, 'cohort');
1126
- const locations = resolveSummaryValue(strategy, 'locations');
1127
- const jobDirections = resolveSummaryValue(strategy, 'job_directions');
1128
- const process = resolveSummaryValue(strategy, 'process');
1129
- const targetOrRequirements = resolveSummaryValue(strategy, 'target_or_requirements');
1130
-
1131
- if (role === 'hook') {
1132
- if (mode === 'job_intel_broadcast') {
1133
- const cohortPart = cohort ? cohort : recruitmentType;
1134
- return ensureSentenceLength(
1135
- `${company}${cohortPart}信息来了!岗位方向、城市和流程帮你快速过一遍。`
1136
- );
1137
- }
1138
-
1139
- if (mode === 'job_alert') {
1140
- const cohortPart = cohort ? cohort : recruitmentType;
1141
- const timePart = publishedAt ? `,${publishedAt}发布` : '';
1142
- return ensureSentenceLength(
1143
- `注意!${company}${cohortPart}${timePart},感兴趣的先来看看能确认的信息。`
1144
- );
1145
- }
1146
-
1147
- if (mode === 'refuse_auto_broadcast') {
1148
- return ensureSentenceLength(
1149
- '这个页面的招聘信息不太完整,我只说能确认的部分,详情去原文核实。'
1150
- );
1151
- }
1152
-
1153
- // info_summary
1154
- const cohortPart = cohort ? cohort : recruitmentType;
1155
- return ensureSentenceLength(`${company}${cohortPart}出来了,感兴趣的来看看!`);
1156
- }
1157
-
1158
- if (role === 'cta') {
1159
- if (mode === 'job_intel_broadcast') {
1160
- return ensureSentenceLength('信息过了一遍,觉得合适直接去原文投,时间别拖。');
1161
- }
1162
- if (mode === 'refuse_auto_broadcast') {
1163
- return ensureSentenceLength('建议去官方渠道确认一下信息,再决定要不要投。');
1164
- }
1165
- return ensureSentenceLength('感兴趣就进原文看看,核实岗位和要求再投。');
1166
- }
1167
-
1168
- // published_at 是元数据,不适合单独作为一个旁白段落,跳过
1169
- if (slotKey === 'published_at') {
1170
- return null;
1171
- }
1172
-
1173
- if (mode === 'job_intel_broadcast') {
1174
- if (slotKey === 'job_directions') {
1175
- return ensureSentenceLength(
1176
- `开放方向有${slotTextOrFallback},先对照自己的专业和技能筛一遍。`
1177
- );
1178
- }
1179
- if (slotKey === 'locations') {
1180
- return ensureSentenceLength(
1181
- `工作城市${slotTextOrFallback},城市不合适可以直接跳过。`
1182
- );
1183
- }
1184
- if (slotKey === 'process') {
1185
- return ensureSentenceLength(
1186
- `招聘流程:${slotTextOrFallback},提前把简历和面试节点安排好。`
1187
- );
1188
- }
1189
- if (slotKey === 'target_or_requirements') {
1190
- return ensureSentenceLength(
1191
- `对象和要求是${slotTextOrFallback},先核对门槛,合适再继续准备材料。`
1192
- );
1193
- }
1194
- return ensureSentenceLength(
1195
- `${slotTextOrFallback},这条信息帮你判断岗位是否值得继续看。`
1196
- );
1197
- }
1198
-
1199
- if (mode === 'job_alert') {
1200
- return ensureSentenceLength(
1201
- `目前能确认的是${slotTextOrFallback},其余细节去原文逐条核对。`
1202
- );
1203
- }
1204
-
1205
- if (mode === 'refuse_auto_broadcast') {
1206
- return ensureSentenceLength(
1207
- `${slotTextOrFallback},置信度偏低,建议补充权威来源后再做判断。`
1208
- );
1209
- }
1210
-
1211
- // info_summary
1212
- if (slotKey === 'target_or_requirements') {
1213
- return ensureSentenceLength(
1214
- `对象和要求看原文,${targetOrRequirements || '具体条件以原文为准'}。`
1215
- );
1216
- }
1217
- return ensureSentenceLength(
1218
- `${slotTextOrFallback},感兴趣进原文看完整信息。`
1219
- );
1220
- }
1221
1100
 
1222
1101
  function estimateDurationMs(text, speed = 1) {
1223
1102
  const chars = Math.max(1, toCharLength(text));
@@ -1359,6 +1238,7 @@ function shouldTryTts({ env, workspaceId }) {
1359
1238
 
1360
1239
  export async function detailSections({
1361
1240
  strategy = {},
1241
+ sentences = null,
1362
1242
  workspace_id = null,
1363
1243
  credential_id = null,
1364
1244
  format = 'mp3',
@@ -1375,6 +1255,21 @@ export async function detailSections({
1375
1255
  );
1376
1256
  }
1377
1257
 
1258
+ if (!Array.isArray(sentences) || sentences.length === 0) {
1259
+ throw new Error(
1260
+ 'sentences are required. Write one narration sentence per phase '
1261
+ + '(hook, highlight_1…N, cta) based on the semantic_slots and persona from plan_video. '
1262
+ + 'Use natural peer-voice tone for the target platform — not a news broadcast.'
1263
+ );
1264
+ }
1265
+
1266
+ const sentenceMap = {};
1267
+ for (const item of sentences) {
1268
+ const phaseId = toSafeString(item?.phase_id);
1269
+ const text = toSafeString(item?.text);
1270
+ if (phaseId && text) sentenceMap[phaseId] = text;
1271
+ }
1272
+
1378
1273
  const profile = getPlatformProfile(strategy?.target_platform);
1379
1274
  const phases = normalizePhaseList(strategy);
1380
1275
 
@@ -1384,15 +1279,23 @@ export async function detailSections({
1384
1279
 
1385
1280
  for (let i = 0; i < phases.length; i += 1) {
1386
1281
  const phase = phases[i];
1387
- const role = toSafeString(phase?.role).toLowerCase() || (phase?.phase_id === 'cta' ? 'cta' : (phase?.phase_id === 'hook' ? 'hook' : 'highlight'));
1282
+ const phaseId = toSafeString(phase.phase_id) || `phase_${i + 1}`;
1283
+ const role = toSafeString(phase?.role).toLowerCase() || (phaseId === 'cta' ? 'cta' : (phaseId === 'hook' ? 'hook' : 'highlight'));
1388
1284
 
1389
- const sentence = buildPhaseSentence({
1390
- phase,
1391
- strategy,
1392
- phaseIndex: i,
1393
- });
1285
+ const sentence = sentenceMap[phaseId];
1286
+ if (!sentence) {
1287
+ throw new Error(
1288
+ `missing sentence for phase "${phaseId}". Provide a text sentence for every phase in the plan.`
1289
+ );
1290
+ }
1394
1291
 
1395
- if (sentence == null) continue;
1292
+ const charLen = toCharLength(sentence);
1293
+ if (charLen < SENTENCE_MIN_CHARS) {
1294
+ throw new Error(`sentence for "${phaseId}" too short (${charLen} chars, min ${SENTENCE_MIN_CHARS}).`);
1295
+ }
1296
+ if (charLen > SENTENCE_MAX_CHARS) {
1297
+ throw new Error(`sentence for "${phaseId}" too long (${charLen} chars, max ${SENTENCE_MAX_CHARS}). Shorten to fit one TTS segment.`);
1298
+ }
1396
1299
 
1397
1300
  const voice = resolveVoiceForPhase({ profile, role, index: i });
1398
1301
  const phaseBudgetMs = clampInt((toFiniteNumber(phase?.duration_s) ?? 6) * 1000, 1000, 180000, 6000);
@@ -1415,9 +1318,9 @@ export async function detailSections({
1415
1318
  usedLiveTts = true;
1416
1319
  } catch (error) {
1417
1320
  const message = toSafeString(error?.message) || 'tts_call_failed';
1418
- ttsErrors.push({ phase_id: phase.phase_id, error: message });
1321
+ ttsErrors.push({ phase_id: phaseId, error: message });
1419
1322
  if (strict_tts) {
1420
- throw new Error(`detail_sections_tts_failed:${phase.phase_id}:${message}`);
1323
+ throw new Error(`detail_sections_tts_failed:${phaseId}:${message}`);
1421
1324
  }
1422
1325
  }
1423
1326
  }
@@ -1425,7 +1328,7 @@ export async function detailSections({
1425
1328
  if (!audio) {
1426
1329
  const estimatedDuration = estimateDurationMs(sentence, voice.speed);
1427
1330
  audio = {
1428
- audio_id: `estimated-${phase.phase_id || `p${i + 1}`}`,
1331
+ audio_id: `estimated-${phaseId}`,
1429
1332
  duration_ms: estimatedDuration,
1430
1333
  audio_url: null,
1431
1334
  audio_path: null,
@@ -1447,7 +1350,7 @@ export async function detailSections({
1447
1350
  );
1448
1351
 
1449
1352
  sections.push({
1450
- phase_id: toSafeString(phase.phase_id) || `phase_${i + 1}`,
1353
+ phase_id: phaseId,
1451
1354
  visual_action: visualAction,
1452
1355
  sentence,
1453
1356
  voice_preset: voice.voice_preset,
@@ -84,9 +84,19 @@ export function createVideoNarrationPlannerServer({
84
84
 
85
85
  server.tool(
86
86
  'detail_sections',
87
- 'Stage 3: expand each phase into sentence+voice settings, call TTS voiceover, and fill duration/dwell.',
87
+ 'Stage 3: take agent-written sentences, call TTS voiceover for each phase, and fill duration/dwell. You MUST write the sentences yourself before calling this tool.',
88
88
  {
89
89
  strategy: z.record(z.any()).describe('Output of plan_video (video strategy with phase_plan).'),
90
+ sentences: z.array(z.object({
91
+ phase_id: z.string().describe('Phase ID from plan_video phase_plan (e.g. hook, highlight_1, cta).'),
92
+ text: z.string().describe('Narration sentence you wrote for this phase.'),
93
+ })).describe(
94
+ 'Narration sentences YOU write, one per phase in the plan. '
95
+ + 'Use semantic_slots, persona, and platform tone from plan_video output. '
96
+ + 'Sound like a peer sharing useful info — not a news anchor. '
97
+ + 'Each sentence: 8–60 chars, spoken Chinese, ends with punctuation. '
98
+ + 'Do NOT use "信息来源为", "发布时间为", "凭截图判断" or similar metadata/warning phrasing.'
99
+ ),
90
100
  workspace_id: z.string().optional().describe('Workspace id for TTS generation. Defaults to WORKSPACE_ID env if provided.'),
91
101
  credential_id: z.string().optional().describe('Optional explicit tts_provider credential id.'),
92
102
  format: z.enum(['mp3', 'wav', 'flac']).optional().describe('Desired audio format for generated voiceover.'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.33",
3
+ "version": "0.15.35",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {