@lightcone-ai/daemon 0.15.33 → 0.15.34
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
|
|
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 =
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
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
|
-
|
|
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:
|
|
1321
|
+
ttsErrors.push({ phase_id: phaseId, error: message });
|
|
1419
1322
|
if (strict_tts) {
|
|
1420
|
-
throw new Error(`detail_sections_tts_failed:${
|
|
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-${
|
|
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:
|
|
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:
|
|
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.'),
|