@lightcone-ai/daemon 0.15.32 → 0.15.33

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.
@@ -1120,10 +1120,9 @@ function buildPhaseSentence({ phase, strategy, phaseIndex }) {
1120
1120
  const slotText = resolveSummaryValue(strategy, slotKey);
1121
1121
  const slotTextOrFallback = slotText || describeHighlightFocus(phase);
1122
1122
  const company = resolveSummaryValue(strategy, 'company') || '该公司';
1123
- const publishedAt = resolveSummaryValue(strategy, 'published_at') || '近期';
1124
- const recruitmentType = resolveSummaryValue(strategy, 'recruitment_type') || '招聘信息';
1123
+ const publishedAt = resolveSummaryValue(strategy, 'published_at');
1124
+ const recruitmentType = resolveSummaryValue(strategy, 'recruitment_type') || '招聘';
1125
1125
  const cohort = resolveSummaryValue(strategy, 'cohort');
1126
- const entry = resolveSummaryValue(strategy, 'entry_or_cta') || '官方岗位入口';
1127
1126
  const locations = resolveSummaryValue(strategy, 'locations');
1128
1127
  const jobDirections = resolveSummaryValue(strategy, 'job_directions');
1129
1128
  const process = resolveSummaryValue(strategy, 'process');
@@ -1131,74 +1130,92 @@ function buildPhaseSentence({ phase, strategy, phaseIndex }) {
1131
1130
 
1132
1131
  if (role === 'hook') {
1133
1132
  if (mode === 'job_intel_broadcast') {
1134
- const cohortPart = cohort ? `${cohort} ` : '';
1133
+ const cohortPart = cohort ? cohort : recruitmentType;
1135
1134
  return ensureSentenceLength(
1136
- `岗位情报先划重点:${company}在${publishedAt}发布了${cohortPart}${recruitmentType}。接下来我会按岗位方向、城市和流程三块快速过一遍,只保留能直接支持投递判断的信息。`
1135
+ `${company}${cohortPart}信息来了!岗位方向、城市和流程帮你快速过一遍。`
1137
1136
  );
1138
1137
  }
1139
1138
 
1140
1139
  if (mode === 'job_alert') {
1140
+ const cohortPart = cohort ? cohort : recruitmentType;
1141
+ const timePart = publishedAt ? `,${publishedAt}发布` : '';
1141
1142
  return ensureSentenceLength(
1142
- `这是一条岗位提醒:${company}在${publishedAt}发布了${recruitmentType}。当前页面细节不完整,我只说已经能确认的事实,帮你先判断要不要继续花时间深挖。`
1143
+ `注意!${company}${cohortPart}${timePart},感兴趣的先来看看能确认的信息。`
1143
1144
  );
1144
1145
  }
1145
1146
 
1146
1147
  if (mode === 'refuse_auto_broadcast') {
1147
1148
  return ensureSentenceLength(
1148
- '当前页面缺少稳定的公司、岗位或发布时间线索,我不能直接生成岗位情报式结论。为了避免误导,下面仅给你保守处理建议。'
1149
+ '这个页面的招聘信息不太完整,我只说能确认的部分,详情去原文核实。'
1149
1150
  );
1150
1151
  }
1151
1152
 
1152
- return ensureSentenceLength(
1153
- '这条链接和招聘信息相关,但更像资讯或公告汇总。下面我只提可核验线索,帮助你快速决定是否跳转到官方岗位页继续确认。'
1154
- );
1153
+ // info_summary
1154
+ const cohortPart = cohort ? cohort : recruitmentType;
1155
+ return ensureSentenceLength(`${company}${cohortPart}出来了,感兴趣的来看看!`);
1155
1156
  }
1156
1157
 
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') {
1158
1170
  return null;
1159
1171
  }
1160
1172
 
1161
1173
  if (mode === 'job_intel_broadcast') {
1162
1174
  if (slotKey === 'job_directions') {
1163
1175
  return ensureSentenceLength(
1164
- `岗位方向目前可确认为${slotTextOrFallback}。这直接决定你是否值得继续投入,建议先对照自己的专业或技能栈做第一轮筛选,再看后续细节。`
1176
+ `开放方向有${slotTextOrFallback},先对照自己的专业和技能筛一遍。`
1165
1177
  );
1166
1178
  }
1167
1179
  if (slotKey === 'locations') {
1168
1180
  return ensureSentenceLength(
1169
- `工作城市线索是${slotTextOrFallback}。这一步主要用于判断通勤与生活成本是否可接受,先把城市匹配度过一遍,能明显减少后续无效投递。`
1181
+ `工作城市${slotTextOrFallback},城市不合适可以直接跳过。`
1170
1182
  );
1171
1183
  }
1172
1184
  if (slotKey === 'process') {
1173
1185
  return ensureSentenceLength(
1174
- `招聘流程可读到${slotTextOrFallback}。你可以按这个顺序预留时间准备简历、测评和面试节点,避免临近截止才发现准备节奏不够。`
1186
+ `招聘流程:${slotTextOrFallback},提前把简历和面试节点安排好。`
1175
1187
  );
1176
1188
  }
1177
1189
  if (slotKey === 'target_or_requirements') {
1178
1190
  return ensureSentenceLength(
1179
- `任职对象与要求显示为${slotTextOrFallback}。这一段最适合做硬性条件核对,先看是否满足门槛,再决定要不要继续投入申请材料。`
1191
+ `对象和要求是${slotTextOrFallback},先核对门槛,合适再继续准备材料。`
1180
1192
  );
1181
1193
  }
1182
1194
  return ensureSentenceLength(
1183
- `补充一个关键信息:${slotLabel(slotKey) || '重点线索'}是${slotTextOrFallback}。这条信息能帮助你更快判断岗位匹配度,避免被页面里次要内容分散注意力。`
1195
+ `${slotTextOrFallback},这条信息帮你判断岗位是否值得继续看。`
1184
1196
  );
1185
1197
  }
1186
1198
 
1187
1199
  if (mode === 'job_alert') {
1188
1200
  return ensureSentenceLength(
1189
- `目前页面能稳定识别的${slotLabel(slotKey) || '线索'}是${slotTextOrFallback}。其余细节暂不充足,建议你把它当作提醒信号,再回到原文逐条核验。`
1201
+ `目前能确认的是${slotTextOrFallback},其余细节去原文逐条核对。`
1190
1202
  );
1191
1203
  }
1192
1204
 
1193
1205
  if (mode === 'refuse_auto_broadcast') {
1194
1206
  return ensureSentenceLength(
1195
- `这一段的${slotLabel(slotKey) || '线索'}置信度仍然偏低,我不会把“${slotTextOrFallback}”当成可直接投递依据。先补充权威来源,再做下一步判断更安全。`
1207
+ `${slotTextOrFallback},置信度偏低,建议补充权威来源后再做判断。`
1196
1208
  );
1197
1209
  }
1198
1210
 
1199
- const summaryPieces = [jobDirections, locations, process, targetOrRequirements].filter(Boolean).slice(0, 2);
1211
+ // info_summary
1212
+ if (slotKey === 'target_or_requirements') {
1213
+ return ensureSentenceLength(
1214
+ `对象和要求看原文,${targetOrRequirements || '具体条件以原文为准'}。`
1215
+ );
1216
+ }
1200
1217
  return ensureSentenceLength(
1201
- `第${phaseIndex + 1}个可核验线索是${slotTextOrFallback}。目前它更适合作为招聘资讯摘要,${summaryPieces.length > 0 ? `可结合${summaryPieces.join('、')}交叉判断` : `建议与官方岗位页交叉核对`},不要直接当成完整岗位说明。`
1218
+ `${slotTextOrFallback},感兴趣进原文看完整信息。`
1202
1219
  );
1203
1220
  }
1204
1221
 
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "default_outro_video_id": "outro-default-zh",
3
3
  "sentence_char_range": {
4
- "min": 100,
5
- "max": 150
4
+ "min": 14,
5
+ "max": 60
6
6
  },
7
7
  "platform_profiles": {
8
8
  "xiaohongshu": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.32",
3
+ "version": "0.15.33",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -301,13 +301,30 @@ function msToAssTimestamp(ms) {
301
301
  return `${hr}:${String(min).padStart(2, '0')}:${String(sec).padStart(2, '0')}.${String(cs).padStart(2, '0')}`;
302
302
  }
303
303
 
304
+ // Hard-wrap CJK subtitle text so it never overflows the video frame.
305
+ // libass WrapStyle:0 doesn't handle Chinese text reliably (no word boundaries),
306
+ // so we insert explicit \N breaks every maxChars characters.
307
+ function wrapSubtitleText(text, maxChars = 14) {
308
+ const chars = Array.from(String(text ?? ''));
309
+ if (chars.length <= maxChars) return chars.join('');
310
+ const lines = [];
311
+ for (let i = 0; i < chars.length; i += maxChars) {
312
+ lines.push(chars.slice(i, i + maxChars).join(''));
313
+ }
314
+ return lines.join('\\N');
315
+ }
316
+
304
317
  export function buildAssContent(subtitles = [], { playResX = 1080, playResY = 1920 } = {}) {
318
+ // Max chars per line: (playResX - marginL - marginR) / fontSizePx
319
+ // 1080 - 30 - 30 = 1020px, fontsize 72 ≈ 72px/char → 14 chars
320
+ const maxCharsPerLine = Math.floor((playResX - 60) / SUBTITLE_FONT_SIZE);
321
+
305
322
  const header = [
306
323
  '[Script Info]',
307
324
  'ScriptType: v4.00+',
308
325
  `PlayResX: ${playResX}`,
309
326
  `PlayResY: ${playResY}`,
310
- 'WrapStyle: 0',
327
+ 'WrapStyle: 2',
311
328
  '',
312
329
  '[V4+ Styles]',
313
330
  'Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding',
@@ -318,7 +335,8 @@ export function buildAssContent(subtitles = [], { playResX = 1080, playResY = 19
318
335
  ].join('\n');
319
336
 
320
337
  const events = subtitles.map(({ text, start_ms, end_ms }) => {
321
- const safeText = String(text ?? '').replace(/\r?\n/g, '\\N').replace(/,/g, '{\\,}');
338
+ const wrapped = wrapSubtitleText(text, maxCharsPerLine);
339
+ const safeText = wrapped.replace(/\r?\n/g, '\\N').replace(/,/g, '{\\,}');
322
340
  return `Dialogue: 0,${msToAssTimestamp(start_ms)},${msToAssTimestamp(end_ms)},Default,,0,0,0,,${safeText}`;
323
341
  });
324
342