@gobing-ai/knowledge-kit 0.0.15 → 0.0.17
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/.claude-plugin/marketplace.json +1 -1
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +41 -1
- package/plugins/generations/daily-article-gen/src/index.ts +68 -2
- package/plugins/generations/dailynews-gen/dist/index.js +90 -31
- package/plugins/generations/dailynews-gen/src/script-builder.ts +133 -33
- package/plugins/generations/episode-plan-gen/dist/index.js +4 -2
- package/plugins/generations/episode-plan-gen/src/index.ts +9 -2
- package/plugins/generations/news-report-gen/dist/index.js +48 -2
- package/plugins/generations/news-report-gen/src/index.ts +83 -7
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +16 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +4 -22
- package/plugins/generations/voice-gen/dist/index.js +35 -0
- package/plugins/generations/voice-gen/src/voicescript.ts +53 -0
- package/plugins/kk/commands/workflow-run.md +25 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +208 -107
- package/plugins/kk/scripts/validate-voicescript.ts +55 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +20 -15
package/package.json
CHANGED
|
@@ -21967,6 +21967,45 @@ function sourceLabelOf(doc2) {
|
|
|
21967
21967
|
return "\u6765\u6E90";
|
|
21968
21968
|
}
|
|
21969
21969
|
}
|
|
21970
|
+
var SCAFFOLD_LABELS = ["\u53EF\u505A\u89D2\u5EA6", "\u4E3A\u4EC0\u4E48\u73B0\u5728\u503C\u5F97\u6CE8\u610F", "\u793E\u533A\u8BA8\u8BBA", "\u80CC\u666F", "\u6F5C\u5728\u5F71\u54CD", "\u5F71\u54CD"];
|
|
21971
|
+
var HEDGING_MARKERS = ["\u8BFB\u8005\u5B9C\u53E6\u884C\u6838\u5B9E", "\u8BFB\u8005\u6700\u597D", "\u4ECD\u5F85\u9A8C\u8BC1", "\u5C1A\u5F85\u9A8C\u8BC1", "\u9700\u53E6\u884C\u6838\u5B9E"];
|
|
21972
|
+
function scaffoldPrefixLength(text) {
|
|
21973
|
+
const match = text.match(/^(?:#{1,6}\s*)?(?:\*\*)?\s*[\u300C\u300E]?\s*([^\u300D\u300F*\uFF1A:\n]{2,})\s*[\u300D\u300F]?\s*(?:\*\*)?\s*[\uFF1A:]?\s*/);
|
|
21974
|
+
if (match === null || !SCAFFOLD_LABELS.includes((match[1] ?? "").trim()))
|
|
21975
|
+
return 0;
|
|
21976
|
+
return match[0].length;
|
|
21977
|
+
}
|
|
21978
|
+
function collapseScaffoldLabels(text) {
|
|
21979
|
+
let rest = text;
|
|
21980
|
+
for (;; ) {
|
|
21981
|
+
const next = rest.slice(scaffoldPrefixLength(rest));
|
|
21982
|
+
if (next === rest)
|
|
21983
|
+
return rest;
|
|
21984
|
+
rest = next;
|
|
21985
|
+
}
|
|
21986
|
+
}
|
|
21987
|
+
function stripHedging(text) {
|
|
21988
|
+
if (!HEDGING_MARKERS.some((marker) => text.includes(marker)))
|
|
21989
|
+
return text;
|
|
21990
|
+
return text.split(/(?<=\u3002)/).filter((sentence) => !HEDGING_MARKERS.some((marker) => sentence.includes(marker))).join("").trim();
|
|
21991
|
+
}
|
|
21992
|
+
function stripScaffold(body) {
|
|
21993
|
+
const kept = [];
|
|
21994
|
+
for (const line of body.split(`
|
|
21995
|
+
`)) {
|
|
21996
|
+
const trimmed = line.trim();
|
|
21997
|
+
if (trimmed === "") {
|
|
21998
|
+
kept.push(line);
|
|
21999
|
+
continue;
|
|
22000
|
+
}
|
|
22001
|
+
const collapsed = stripHedging(collapseScaffoldLabels(trimmed));
|
|
22002
|
+
if (collapsed === "")
|
|
22003
|
+
continue;
|
|
22004
|
+
kept.push(collapsed === trimmed ? line : collapsed);
|
|
22005
|
+
}
|
|
22006
|
+
return kept.join(`
|
|
22007
|
+
`);
|
|
22008
|
+
}
|
|
21970
22009
|
function renderDoc(doc2, index) {
|
|
21971
22010
|
const heading = doc2.title?.trim() || `\u65B0\u95FB ${index + 1}`;
|
|
21972
22011
|
const link = doc2.sourceUri ? `[\u6765\u6E90\uFF1A${sourceLabelOf(doc2)}](${doc2.sourceUri})` : "";
|
|
@@ -21977,7 +22016,7 @@ function renderDoc(doc2, index) {
|
|
|
21977
22016
|
|
|
21978
22017
|
${metaLine ? `${metaLine}
|
|
21979
22018
|
|
|
21980
|
-
` : ""}${(doc2.body ?? "").trim()}`;
|
|
22019
|
+
` : ""}${stripScaffold(doc2.body ?? "").trim()}`;
|
|
21981
22020
|
}
|
|
21982
22021
|
function renderDailyArticle(docs, date6) {
|
|
21983
22022
|
const items = docs.map(renderDoc).join(`
|
|
@@ -22043,6 +22082,7 @@ if (import.meta.main) {
|
|
|
22043
22082
|
process.exit(await main2());
|
|
22044
22083
|
}
|
|
22045
22084
|
export {
|
|
22085
|
+
stripScaffold,
|
|
22046
22086
|
renderDailyArticle,
|
|
22047
22087
|
processGeneratorIO,
|
|
22048
22088
|
main2 as main,
|
|
@@ -8,7 +8,8 @@ import { echoError } from '@gobing-ai/ts-utils';
|
|
|
8
8
|
* daily-article-gen: render a merged `Doc[]` into one readable daily AI-news
|
|
9
9
|
* article. Mechanical template fill — no LLM, no STORM. Every news item keeps
|
|
10
10
|
* a human-clickable source link (`[来源](sourceUri)`) so readers can trace
|
|
11
|
-
* claims to their origin.
|
|
11
|
+
* claims to their origin. Research scaffolding is stripped before render
|
|
12
|
+
* (task 0138 R4b — see `stripScaffold`).
|
|
12
13
|
*
|
|
13
14
|
* Run date: `ARTICLE_DATE` env var (YYYYMMDD or ISO), else today (UTC).
|
|
14
15
|
* Language-neutral: the Doc bodies already carry the summary language.
|
|
@@ -26,6 +27,71 @@ function sourceLabelOf(doc: Doc): string {
|
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Research-scaffold labels that must never reach reader copy (task 0138 R4b). The last30days
|
|
32
|
+
* research style emits them as `**「label」**` prefixes; the plan-translate prompt forbids them,
|
|
33
|
+
* but this deterministic pass catches what the agent misses. Grows with sources.
|
|
34
|
+
*/
|
|
35
|
+
const SCAFFOLD_LABELS = ['可做角度', '为什么现在值得注意', '社区讨论', '背景', '潜在影响', '影响'];
|
|
36
|
+
|
|
37
|
+
/** Self-referential hedging markers — research voice, not news copy (task 0138 R4b). */
|
|
38
|
+
const HEDGING_MARKERS = ['读者宜另行核实', '读者最好', '仍待验证', '尚待验证', '需另行核实'];
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Length of a leading scaffold label in any shape the research bodies use — `**「背景」**`,
|
|
42
|
+
* `「背景」`, `**背景**`, `### 背景`, `背景:` — or 0 when the line does not open with a known label.
|
|
43
|
+
* The capture is greedy and unbounded on purpose: it must reach the whole label (so `可做` cannot
|
|
44
|
+
* satisfy it where `可做角度` is the real one) and must not impose a length the label list outgrows.
|
|
45
|
+
* The `SCAFFOLD_LABELS` membership check, not the regex, decides.
|
|
46
|
+
*/
|
|
47
|
+
function scaffoldPrefixLength(text: string): number {
|
|
48
|
+
const match = text.match(
|
|
49
|
+
/^(?:#{1,6}\s*)?(?:\*\*)?\s*[「『]?\s*([^」』*::\n]{2,})\s*[」』]?\s*(?:\*\*)?\s*[::]?\s*/,
|
|
50
|
+
);
|
|
51
|
+
if (match === null || !SCAFFOLD_LABELS.includes((match[1] ?? '').trim())) return 0;
|
|
52
|
+
return match[0].length;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Drop every leading scaffold label, keeping the prose after it (`**「可做角度」** 可做角度:X` -> `X`). */
|
|
56
|
+
function collapseScaffoldLabels(text: string): string {
|
|
57
|
+
let rest = text;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const next = rest.slice(scaffoldPrefixLength(rest));
|
|
60
|
+
if (next === rest) return rest;
|
|
61
|
+
rest = next;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Drop the sentences carrying a hedging marker; a line that is only hedging collapses to ''. */
|
|
66
|
+
function stripHedging(text: string): string {
|
|
67
|
+
if (!HEDGING_MARKERS.some((marker) => text.includes(marker))) return text;
|
|
68
|
+
return text
|
|
69
|
+
.split(/(?<=。)/)
|
|
70
|
+
.filter((sentence) => !HEDGING_MARKERS.some((marker) => sentence.includes(marker)))
|
|
71
|
+
.join('')
|
|
72
|
+
.trim();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Strip research scaffolding from a Doc body before it reaches reader copy (task 0138 R4b):
|
|
77
|
+
* standalone label lines are dropped, inline label prefixes collapse to their prose, and hedging
|
|
78
|
+
* sentences are removed. A clean body passes through byte-identical.
|
|
79
|
+
*/
|
|
80
|
+
export function stripScaffold(body: string): string {
|
|
81
|
+
const kept: string[] = [];
|
|
82
|
+
for (const line of body.split('\n')) {
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
if (trimmed === '') {
|
|
85
|
+
kept.push(line); // paragraph breaks survive untouched
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const collapsed = stripHedging(collapseScaffoldLabels(trimmed));
|
|
89
|
+
if (collapsed === '') continue;
|
|
90
|
+
kept.push(collapsed === trimmed ? line : collapsed);
|
|
91
|
+
}
|
|
92
|
+
return kept.join('\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
29
95
|
/** Render one news item section: heading, source link, and summary body. */
|
|
30
96
|
function renderDoc(doc: Doc, index: number): string {
|
|
31
97
|
const heading = doc.title?.trim() || `新闻 ${index + 1}`;
|
|
@@ -33,7 +99,7 @@ function renderDoc(doc: Doc, index: number): string {
|
|
|
33
99
|
const meta = doc.metadata as Record<string, unknown> | undefined;
|
|
34
100
|
const publishedAt = typeof meta?.publishedAt === 'string' ? meta.publishedAt.slice(0, 10) : '';
|
|
35
101
|
const metaLine = [link, publishedAt].filter(Boolean).join(' · ');
|
|
36
|
-
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${(doc.body ?? '').trim()}`;
|
|
102
|
+
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${stripScaffold(doc.body ?? '').trim()}`;
|
|
37
103
|
}
|
|
38
104
|
|
|
39
105
|
/** Render the article markdown for a merged Doc[] (pure — exported for tests). */
|
|
@@ -21999,34 +21999,85 @@ function normalizeBroadcastText(raw, isZh) {
|
|
|
21999
21999
|
}
|
|
22000
22000
|
return text;
|
|
22001
22001
|
}
|
|
22002
|
-
function synthesizePersonalUnderstanding(title, body, isZh) {
|
|
22002
|
+
function synthesizePersonalUnderstanding(title, body, isZh, used) {
|
|
22003
22003
|
const combined = `${title} ${body}`.toLowerCase();
|
|
22004
|
-
|
|
22005
|
-
|
|
22006
|
-
|
|
22007
|
-
|
|
22008
|
-
|
|
22009
|
-
|
|
22010
|
-
|
|
22011
|
-
|
|
22012
|
-
|
|
22013
|
-
|
|
22014
|
-
|
|
22015
|
-
|
|
22016
|
-
|
|
22017
|
-
|
|
22004
|
+
const line = isZh ? zhInsightFor(combined) : enInsightFor(combined);
|
|
22005
|
+
if (used.has(line))
|
|
22006
|
+
return null;
|
|
22007
|
+
used.add(line);
|
|
22008
|
+
return line;
|
|
22009
|
+
}
|
|
22010
|
+
function zhInsightFor(combined) {
|
|
22011
|
+
if (/jakub|szymon|\u9996\u5E2D\u79D1\u5B66\u5BB6|\u521B\u59CB\u56E2\u961F|\u6280\u672F\u9886\u8896|\u9EC4\u91D1\u642D\u6863/i.test(combined)) {
|
|
22012
|
+
return "\u5728\u5404\u79CD\u6280\u672F\u5149\u73AF\u80CC\u540E\uFF0C\u6B63\u662F\u8FD9\u6837\u4E13\u6CE8\u800C\u4E0D\u77E5\u75B2\u5026\u7684\u5E95\u5C42\u653B\u575A\u8005\uFF0C\u4E3A\u4ECA\u5929\u6574\u4E2A\u5927\u6A21\u578B\u751F\u6001\u5960\u5B9A\u4E86\u6700\u575A\u5B9E\u7684\u6280\u672F\u57FA\u77F3\u3002";
|
|
22013
|
+
}
|
|
22014
|
+
if (/\u7B97\u529B|\u5409\u74E6|\u7535\u529B|\u80FD\u6E90|\u6570\u636E\u4E2D\u5FC3|\u57FA\u7840\u8BBE\u65BD|\u57FA\u5EFA/.test(combined)) {
|
|
22015
|
+
return "\u8FD9\u5176\u5B9E\u91CA\u653E\u4E86\u4E00\u4E2A\u975E\u5E38\u660E\u786E\u7684\u4FE1\u53F7\uFF1AAI\u7ADE\u4E89\u7684\u4E0B\u534A\u573A\uFF0C\u5DF2\u7ECF\u4E0D\u518D\u53EA\u662F\u5355\u7EAF\u62FC\u6A21\u578B\u53C2\u6570\uFF0C\u66F4\u662F\u7535\u529B\u3001\u80FD\u6E90\u548C\u5168\u6808\u5DE5\u4E1A\u57FA\u7840\u8BBE\u65BD\u7684\u786C\u6838\u8F83\u91CF\u3002";
|
|
22016
|
+
}
|
|
22017
|
+
if (/\u82AF\u7247|\u786C\u4EF6|\u5904\u7406\u5668|enclave/i.test(combined)) {
|
|
22018
|
+
return "\u771F\u6B63\u88AB\u91CD\u65B0\u5B9A\u4E49\u7684\uFF0C\u662F\u786C\u4EF6\u5C42\u627F\u62C5\u7684\u5B89\u5168\u4E0E\u7B97\u529B\u8D23\u4EFB\uFF1A\u628A\u80FD\u529B\u505A\u8FDB\u7845\u7247\u5C42\u66F4\u53EF\u9760\u3001\u4E5F\u66F4\u96BE\u7ED5\u8FC7\uFF0C\u4F46\u751F\u6001\u9501\u5B9A\u7684\u4EE3\u4EF7\u9700\u8981\u4E00\u8D77\u7B97\u8FDB\u53BB\u3002";
|
|
22018
22019
|
}
|
|
22020
|
+
if (/sora|\u89C6\u9891|\u751F\u6210\u89C6\u9891|\u89C6\u9891\u751F\u6210|\u6587\u751F\u89C6\u9891|\u5185\u5BB9\u521B\u4F5C|cameo/i.test(combined)) {
|
|
22021
|
+
return "\u6211\u4E2A\u4EBA\u89C9\u5F97\uFF0C\u8FD9\u6B21\u6700\u5927\u7684\u770B\u70B9\u662F\u5185\u5BB9\u521B\u4F5C\u95E8\u69DB\u7684\u518D\u6B21\u964D\u4F4E\u3002\u4E0D\u8FC7\u6B63\u5982\u56E2\u961F\u63D0\u5230\u7684\uFF0C\u5982\u4F55\u5E73\u8861\u521B\u4F5C\u81EA\u7531\u4E0E\u9632\u6B62\u4F4E\u8D28\u5185\u5BB9\u6CDB\u6EE5\uFF0C\u4F1A\u662F\u6574\u4E2A\u884C\u4E1A\u63A5\u4E0B\u6765\u6700\u503C\u5F97\u89C2\u5BDF\u7684\u5173\u952E\u8003\u9898\u3002";
|
|
22022
|
+
}
|
|
22023
|
+
if (/\u878D\u8D44|\u6295\u8D44|\u5E02\u573A|\u5546\u4E1A|\u4F30\u503C|\u5408\u4F5C/.test(combined)) {
|
|
22024
|
+
return "\u8FD9\u9879\u8FDB\u5C55\u53CD\u6620\u51FA\u8D44\u672C\u4E0E\u4EA7\u4E1A\u754C\u6B63\u5728\u52A0\u901F\u5F62\u6210\u5408\u529B\uFF0C\u540E\u7EED\u5B9E\u9645\u4E1A\u52A1\u573A\u666F\u7684\u843D\u5730\u6548\u679C\u975E\u5E38\u503C\u5F97\u6211\u4EEC\u6301\u7EED\u8DDF\u8FDB\u3002";
|
|
22025
|
+
}
|
|
22026
|
+
return "\u8FD9\u9879\u65B0\u52A8\u6001\u53CD\u6620\u51FA\u6280\u672F\u6F14\u8FDB\u6B63\u5728\u8FDB\u4E00\u6B65\u63D0\u901F\uFF0C\u5BF9\u76F8\u5173\u9886\u57DF\u7684\u5B9E\u9645\u5F71\u54CD\u4E5F\u975E\u5E38\u503C\u5F97\u6211\u4EEC\u6301\u7EED\u5173\u6CE8\u3002";
|
|
22027
|
+
}
|
|
22028
|
+
function enInsightFor(combined) {
|
|
22019
22029
|
if (/jakub|szymon|chief scientist|founding team|technical leader|partnership/i.test(combined)) {
|
|
22020
22030
|
return "Behind the headlines, it is relentless technical leadership and deep engineering rigor that continue to drive these foundational milestones.";
|
|
22021
22031
|
}
|
|
22022
|
-
if (/compute|gigawatt|
|
|
22032
|
+
if (/compute|gigawatt|power|energy|datacenter|infrastructure/.test(combined)) {
|
|
22023
22033
|
return "This signals clearly that the AI frontier is no longer just about algorithms, but a massive contest of power, energy, and full-stack industrial engineering.";
|
|
22024
22034
|
}
|
|
22025
|
-
if (/
|
|
22035
|
+
if (/chip|hardware|processor|enclave/i.test(combined)) {
|
|
22036
|
+
return "What stands out is the hardware layer quietly taking on the security and compute burden: putting the guarantee in silicon is harder to bypass, and it carries its own lock-in cost.";
|
|
22037
|
+
}
|
|
22038
|
+
if (/sora|video|video generation|content creation|cameo/i.test(combined)) {
|
|
22026
22039
|
return "In my view, the biggest takeaway here is the dramatic lowering of creation barriers, while content quality governance will be the key test ahead.";
|
|
22027
22040
|
}
|
|
22028
22041
|
return "This development highlights how rapidly the ecosystem is moving from experimental proofs of concept into large-scale execution.";
|
|
22029
22042
|
}
|
|
22043
|
+
var MAX_FACT_SEGMENT_CHARS = 300;
|
|
22044
|
+
var TITLE_ECHO_NOISE_RE = /[\uFF0C\u3002\u3001\uFF1A:\uFF1B;!\uFF01?\uFF1F\u2026\s\u300C\u300D\u300E\u300F\u3010\u3011]/g;
|
|
22045
|
+
function stripLeadingTitleEcho(body, title) {
|
|
22046
|
+
const wanted = title.replace(TITLE_ECHO_NOISE_RE, "");
|
|
22047
|
+
if (wanted.length < 4)
|
|
22048
|
+
return body;
|
|
22049
|
+
let matched = "";
|
|
22050
|
+
let end = -1;
|
|
22051
|
+
for (let i = 0;i < body.length && matched.length < wanted.length; i++) {
|
|
22052
|
+
const kept = (body[i] ?? "").replace(TITLE_ECHO_NOISE_RE, "");
|
|
22053
|
+
if (kept === "")
|
|
22054
|
+
continue;
|
|
22055
|
+
matched += kept;
|
|
22056
|
+
if (matched.length >= wanted.length)
|
|
22057
|
+
end = i + 1;
|
|
22058
|
+
}
|
|
22059
|
+
if (end === -1 || matched !== wanted)
|
|
22060
|
+
return body;
|
|
22061
|
+
return body.slice(end).replace(/^[\uFF0C\u3002\u3001\uFF1A:\uFF1B;!\uFF01?\uFF1F\u2026\s\u300C\u300D\u300E\u300F\u3010\u3011]+/, "");
|
|
22062
|
+
}
|
|
22063
|
+
function splitFactText(text, maxChars = MAX_FACT_SEGMENT_CHARS) {
|
|
22064
|
+
if (text.length <= maxChars)
|
|
22065
|
+
return [text];
|
|
22066
|
+
const clauses = text.match(/[^\u3002\uFF01\uFF1F\u2026\uFF1B]+[\u3002\uFF01\uFF1F\u2026\uFF1B]*|[\s\S]/g) ?? [text];
|
|
22067
|
+
const chunks = [];
|
|
22068
|
+
let current = "";
|
|
22069
|
+
for (const clause of clauses) {
|
|
22070
|
+
if (current !== "" && current.length + clause.length > maxChars) {
|
|
22071
|
+
chunks.push(current);
|
|
22072
|
+
current = clause;
|
|
22073
|
+
continue;
|
|
22074
|
+
}
|
|
22075
|
+
current += clause;
|
|
22076
|
+
}
|
|
22077
|
+
if (current !== "")
|
|
22078
|
+
chunks.push(current);
|
|
22079
|
+
return chunks.length > 0 ? chunks : [text];
|
|
22080
|
+
}
|
|
22030
22081
|
function buildNewsVoiceScript(docs, options) {
|
|
22031
22082
|
const language = exports_external.enum(["zh", "en"]).parse((options?.language || "zh").toLowerCase());
|
|
22032
22083
|
const isZh = language === "zh";
|
|
@@ -22086,26 +22137,31 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22086
22137
|
gap_ms: 700,
|
|
22087
22138
|
emotion: "confident, warm"
|
|
22088
22139
|
});
|
|
22140
|
+
const usedInsights = new Set;
|
|
22089
22141
|
docs.forEach((doc2, idx) => {
|
|
22090
22142
|
const isLast = idx === docs.length - 1 && docs.length > 1;
|
|
22091
22143
|
const cleanTitle = (doc2.title ?? "").trim();
|
|
22092
|
-
const cleanBody = (doc2.body ?? "").trim();
|
|
22144
|
+
const cleanBody = stripLeadingTitleEcho((doc2.body ?? "").trim(), cleanTitle);
|
|
22093
22145
|
const lead = isZh ? idx === 0 ? `\u9996\u5148\u6765\u804A\u804A\u5927\u5BB6\u975E\u5E38\u5173\u6CE8\u7684\u3010${cleanTitle}\u3011\u3002` : isLast ? `\u6700\u540E\uFF0C\u6765\u770B\u770B\u4ECA\u5929\u7684\u6700\u540E\u4E00\u6761\u8D44\u8BAF\uFF0C\u3010${cleanTitle}\u3011\u3002` : `\u63A5\u7740\u6211\u4EEC\u628A\u76EE\u5149\u8F6C\u5411\u53E6\u4E00\u6761\u91CD\u8981\u8FDB\u5C55\uFF0C\u3010${cleanTitle}\u3011\u3002` : idx === 0 ? `First up today, let's look at ${cleanTitle}.` : isLast ? `And finally today, let's wrap up with ${cleanTitle}.` : `Next, turning our attention to ${cleanTitle}.`;
|
|
22094
22146
|
const normalizedLead = normalizeBroadcastText(lead, isZh);
|
|
22095
|
-
const normalizedBody = normalizeBroadcastText(cleanBody, isZh);
|
|
22096
|
-
const insight = synthesizePersonalUnderstanding(cleanTitle, cleanBody, isZh);
|
|
22097
|
-
const normalizedInsight = normalizeBroadcastText(insight, isZh);
|
|
22147
|
+
const normalizedBody = cleanBody === "" ? "" : normalizeBroadcastText(cleanBody, isZh);
|
|
22148
|
+
const insight = synthesizePersonalUnderstanding(cleanTitle, cleanBody, isZh, usedInsights);
|
|
22149
|
+
const normalizedInsight = insight === null ? null : normalizeBroadcastText(insight, isZh);
|
|
22098
22150
|
const factText = isZh ? `${normalizedLead}${normalizedBody}` : `${normalizedLead} ${normalizedBody}`;
|
|
22099
|
-
|
|
22100
|
-
|
|
22101
|
-
|
|
22102
|
-
|
|
22103
|
-
|
|
22104
|
-
|
|
22105
|
-
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
|
|
22151
|
+
for (const chunk of splitFactText(factText)) {
|
|
22152
|
+
segments.push({
|
|
22153
|
+
text: chunk,
|
|
22154
|
+
gap_ms: 500,
|
|
22155
|
+
emotion: "professional, clear"
|
|
22156
|
+
});
|
|
22157
|
+
}
|
|
22158
|
+
if (normalizedInsight !== null) {
|
|
22159
|
+
segments.push({
|
|
22160
|
+
text: normalizedInsight,
|
|
22161
|
+
gap_ms: 800,
|
|
22162
|
+
emotion: "thoughtful, personal"
|
|
22163
|
+
});
|
|
22164
|
+
}
|
|
22109
22165
|
});
|
|
22110
22166
|
const outroText = isZh ? `\u597D\u4E86\uFF0C\u4EE5\u4E0A\u5C31\u662F\u4ECA\u5929AI\u79D1\u6280\u6F2B\u8C08\u7684\u5168\u90E8\u5185\u5BB9\u3002\u4E0D\u77E5\u9053\u4ECA\u5929\u8FD9\u51E0\u6761\u6D88\u606F\u6709\u6CA1\u6709\u7ED9\u60A8\u5E26\u6765\u4E00\u4E9B\u65B0\u7684\u542F\u53D1\uFF1F\u611F\u8C22\u60A8\u7684\u6536\u542C\uFF0C\u6211\u662F${hostName}\uFF0C\u795D\u60A8\u62E5\u6709\u5145\u5B9E\u9AD8\u6548\u7684\u4E00\u5929\uFF0C\u6211\u4EEC\u4E0B\u671F\u8282\u76EE\u4E0D\u89C1\u4E0D\u6563\uFF01` : `That wraps up today's AI Daily Briefing. I hope today's highlights gave you some useful takeaways. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
22111
22167
|
segments.push({
|
|
@@ -22330,6 +22386,8 @@ if (import.meta.main) {
|
|
|
22330
22386
|
}
|
|
22331
22387
|
export {
|
|
22332
22388
|
synthesizePersonalUnderstanding,
|
|
22389
|
+
stripLeadingTitleEcho,
|
|
22390
|
+
splitFactText,
|
|
22333
22391
|
processGeneratorIO,
|
|
22334
22392
|
normalizeBroadcastText,
|
|
22335
22393
|
main2 as main,
|
|
@@ -22340,5 +22398,6 @@ export {
|
|
|
22340
22398
|
VoiceSpeakerSchema,
|
|
22341
22399
|
VoiceScriptSegmentSchema,
|
|
22342
22400
|
VoiceScriptSchema,
|
|
22401
|
+
MAX_FACT_SEGMENT_CHARS,
|
|
22343
22402
|
EffectConfigSchema
|
|
22344
22403
|
};
|
|
@@ -144,39 +144,131 @@ export function normalizeBroadcastText(raw: string, isZh: boolean): string {
|
|
|
144
144
|
/**
|
|
145
145
|
* Synthesizes a natural, concise spoken perspective / takeaway based on the story facts.
|
|
146
146
|
* Gives the monologue a demonstrative tech podcast talk feel rather than mechanical news reading.
|
|
147
|
+
*
|
|
148
|
+
* `used` is the per-episode set of lines already spoken (task 0139 R2). A colliding item gets
|
|
149
|
+
* `null` — no commentary segment at all — because a repeated line is worse than none and a
|
|
150
|
+
* second-choice line would be a different topic misfire (Q&A item 5).
|
|
147
151
|
*/
|
|
148
|
-
export function synthesizePersonalUnderstanding(
|
|
152
|
+
export function synthesizePersonalUnderstanding(
|
|
153
|
+
title: string,
|
|
154
|
+
body: string,
|
|
155
|
+
isZh: boolean,
|
|
156
|
+
used: Set<string>,
|
|
157
|
+
): string | null {
|
|
149
158
|
const combined = `${title} ${body}`.toLowerCase();
|
|
159
|
+
const line = isZh ? zhInsightFor(combined) : enInsightFor(combined);
|
|
160
|
+
if (used.has(line)) return null;
|
|
161
|
+
used.add(line);
|
|
162
|
+
return line;
|
|
163
|
+
}
|
|
150
164
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
return '这项新动态反映出技术演进正在进一步提速,对相关领域的实际影响也非常值得我们持续关注。';
|
|
165
|
+
/**
|
|
166
|
+
* Bucket order is first-match-wins, and the buckets are deliberately disjoint (task 0139 R2).
|
|
167
|
+
* The energy line requires energy-side keywords: the pre-fix bucket matched bare
|
|
168
|
+
* `芯片|硬件|工厂`, so it fired after the Nvidia-regulation and Apple Secure-Enclave items on the
|
|
169
|
+
* 2026-09-16 run. Hardware/chip content now has its own line instead.
|
|
170
|
+
*/
|
|
171
|
+
function zhInsightFor(combined: string): string {
|
|
172
|
+
if (/jakub|szymon|首席科学家|创始团队|技术领袖|黄金搭档/i.test(combined)) {
|
|
173
|
+
return '在各种技术光环背后,正是这样专注而不知疲倦的底层攻坚者,为今天整个大模型生态奠定了最坚实的技术基石。';
|
|
174
|
+
}
|
|
175
|
+
if (/算力|吉瓦|电力|能源|数据中心|基础设施|基建/.test(combined)) {
|
|
176
|
+
return '这其实释放了一个非常明确的信号:AI竞争的下半场,已经不再只是单纯拼模型参数,更是电力、能源和全栈工业基础设施的硬核较量。';
|
|
165
177
|
}
|
|
178
|
+
if (/芯片|硬件|处理器|enclave/i.test(combined)) {
|
|
179
|
+
return '真正被重新定义的,是硬件层承担的安全与算力责任:把能力做进硅片层更可靠、也更难绕过,但生态锁定的代价需要一起算进去。';
|
|
180
|
+
}
|
|
181
|
+
// Video/creation only — bare `生成` matched every generative-AI body (Meta and both Gemini
|
|
182
|
+
// items on the 2026-09-16 run) and bare `chatgpt` matched the Meta item, which is what made
|
|
183
|
+
// this line a topic misfire. A product name is not a creation-barrier story.
|
|
184
|
+
if (/sora|视频|生成视频|视频生成|文生视频|内容创作|cameo/i.test(combined)) {
|
|
185
|
+
return '我个人觉得,这次最大的看点是内容创作门槛的再次降低。不过正如团队提到的,如何平衡创作自由与防止低质内容泛滥,会是整个行业接下来最值得观察的关键考题。';
|
|
186
|
+
}
|
|
187
|
+
if (/融资|投资|市场|商业|估值|合作/.test(combined)) {
|
|
188
|
+
return '这项进展反映出资本与产业界正在加速形成合力,后续实际业务场景的落地效果非常值得我们持续跟进。';
|
|
189
|
+
}
|
|
190
|
+
return '这项新动态反映出技术演进正在进一步提速,对相关领域的实际影响也非常值得我们持续关注。';
|
|
191
|
+
}
|
|
166
192
|
|
|
167
|
-
|
|
193
|
+
/** English mirror of {@link zhInsightFor} — same bucket order, same disjointness rule. */
|
|
194
|
+
function enInsightFor(combined: string): string {
|
|
168
195
|
if (/jakub|szymon|chief scientist|founding team|technical leader|partnership/i.test(combined)) {
|
|
169
196
|
return 'Behind the headlines, it is relentless technical leadership and deep engineering rigor that continue to drive these foundational milestones.';
|
|
170
197
|
}
|
|
171
|
-
if (/compute|gigawatt|
|
|
198
|
+
if (/compute|gigawatt|power|energy|datacenter|infrastructure/.test(combined)) {
|
|
172
199
|
return 'This signals clearly that the AI frontier is no longer just about algorithms, but a massive contest of power, energy, and full-stack industrial engineering.';
|
|
173
200
|
}
|
|
174
|
-
if (/
|
|
201
|
+
if (/chip|hardware|processor|enclave/i.test(combined)) {
|
|
202
|
+
return 'What stands out is the hardware layer quietly taking on the security and compute burden: putting the guarantee in silicon is harder to bypass, and it carries its own lock-in cost.';
|
|
203
|
+
}
|
|
204
|
+
if (/sora|video|video generation|content creation|cameo/i.test(combined)) {
|
|
175
205
|
return 'In my view, the biggest takeaway here is the dramatic lowering of creation barriers, while content quality governance will be the key test ahead.';
|
|
176
206
|
}
|
|
177
207
|
return 'This development highlights how rapidly the ecosystem is moving from experimental proofs of concept into large-scale execution.';
|
|
178
208
|
}
|
|
179
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Longest fact segment the script emits (task 0139 R4).
|
|
212
|
+
* ponytail: observed zh speech rates 3.4–6.1 chars/s put 300 chars at 49–88s per chunk; raise only
|
|
213
|
+
* with measured evidence. 400 would reach ~117s at the slowest observed rate, where whisper
|
|
214
|
+
* transcription fidelity collapsed to 0.25–0.41 on the 2026-09-16 run.
|
|
215
|
+
*/
|
|
216
|
+
export const MAX_FACT_SEGMENT_CHARS = 300;
|
|
217
|
+
|
|
218
|
+
/** Punctuation and space the translator drifts between a title and its body restatement. */
|
|
219
|
+
const TITLE_ECHO_NOISE_RE = /[,。、::;;!!??…\s「」『』【】]/g;
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Drop a leading restatement of the item title from its body (task 0139 R1).
|
|
223
|
+
*
|
|
224
|
+
* `plan-translate` derives each title from the body's first sentence, so the echo is always a
|
|
225
|
+
* prefix with at most punctuation drift (`:` → `,`) — the rule is a deterministic significant-
|
|
226
|
+
* character prefix compare, never a fuzzy/LLM similarity match (anti-pattern list). The body is
|
|
227
|
+
* returned untouched whenever the normalized prefix does not equal the normalized title, and a
|
|
228
|
+
* body that is *only* the restatement collapses to the empty string (the lead alone carries it).
|
|
229
|
+
*/
|
|
230
|
+
export function stripLeadingTitleEcho(body: string, title: string): string {
|
|
231
|
+
const wanted = title.replace(TITLE_ECHO_NOISE_RE, '');
|
|
232
|
+
if (wanted.length < 4) return body; // a short title would match too much of the body
|
|
233
|
+
let matched = '';
|
|
234
|
+
let end = -1;
|
|
235
|
+
for (let i = 0; i < body.length && matched.length < wanted.length; i++) {
|
|
236
|
+
const kept = (body[i] ?? '').replace(TITLE_ECHO_NOISE_RE, '');
|
|
237
|
+
if (kept === '') continue; // drift punctuation between title and echo
|
|
238
|
+
matched += kept;
|
|
239
|
+
if (matched.length >= wanted.length) end = i + 1;
|
|
240
|
+
}
|
|
241
|
+
if (end === -1 || matched !== wanted) return body;
|
|
242
|
+
return body.slice(end).replace(/^[,。、::;;!!??…\s「」『』【】]+/, '');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Split a fact text into chunks of at most `maxChars` (task 0139 R4).
|
|
247
|
+
*
|
|
248
|
+
* Greedy packing at clause boundaries: a chunk closes when the next clause would push it past the
|
|
249
|
+
* cap, so `chunks.join('') === text` always holds — the `[\s\S]` alternative is what keeps leading
|
|
250
|
+
* or stray punctuation from being dropped by the match. `;` is a boundary too: a clause-joined
|
|
251
|
+
* sentence would otherwise stay whole and blow past the ~90s budget the cap exists to protect. A
|
|
252
|
+
* single clause longer than the cap is emitted whole — splitting mid-thought is worse than a long
|
|
253
|
+
* segment — and is left to the QC duration audit.
|
|
254
|
+
*/
|
|
255
|
+
export function splitFactText(text: string, maxChars: number = MAX_FACT_SEGMENT_CHARS): string[] {
|
|
256
|
+
if (text.length <= maxChars) return [text];
|
|
257
|
+
const clauses = text.match(/[^。!?…;]+[。!?…;]*|[\s\S]/g) ?? [text];
|
|
258
|
+
const chunks: string[] = [];
|
|
259
|
+
let current = '';
|
|
260
|
+
for (const clause of clauses) {
|
|
261
|
+
if (current !== '' && current.length + clause.length > maxChars) {
|
|
262
|
+
chunks.push(current);
|
|
263
|
+
current = clause;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
current += clause;
|
|
267
|
+
}
|
|
268
|
+
if (current !== '') chunks.push(current);
|
|
269
|
+
return chunks.length > 0 ? chunks : [text];
|
|
270
|
+
}
|
|
271
|
+
|
|
180
272
|
export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions): VoiceScript {
|
|
181
273
|
const language = z.enum(['zh', 'en']).parse((options?.language || 'zh').toLowerCase());
|
|
182
274
|
const isZh = language === 'zh';
|
|
@@ -259,10 +351,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
259
351
|
});
|
|
260
352
|
|
|
261
353
|
// 2. Story segments: demonstrative talk with facts + conversational insight
|
|
354
|
+
const usedInsights = new Set<string>(); // one per episode (task 0139 R2)
|
|
262
355
|
docs.forEach((doc, idx) => {
|
|
263
356
|
const isLast = idx === docs.length - 1 && docs.length > 1;
|
|
264
357
|
const cleanTitle = (doc.title ?? '').trim();
|
|
265
|
-
|
|
358
|
+
// R1: drop the title restatement plan-translate prepends to the body, so the lead's
|
|
359
|
+
// 【title】 is the only place the title is spoken for this item.
|
|
360
|
+
const cleanBody = stripLeadingTitleEcho((doc.body ?? '').trim(), cleanTitle);
|
|
266
361
|
|
|
267
362
|
// Conversational lead
|
|
268
363
|
const lead = isZh
|
|
@@ -278,23 +373,28 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
278
373
|
: `Next, turning our attention to ${cleanTitle}.`;
|
|
279
374
|
|
|
280
375
|
const normalizedLead = normalizeBroadcastText(lead, isZh);
|
|
281
|
-
const normalizedBody = normalizeBroadcastText(cleanBody, isZh);
|
|
282
|
-
const insight = synthesizePersonalUnderstanding(cleanTitle, cleanBody, isZh);
|
|
283
|
-
const normalizedInsight = normalizeBroadcastText(insight, isZh);
|
|
376
|
+
const normalizedBody = cleanBody === '' ? '' : normalizeBroadcastText(cleanBody, isZh);
|
|
377
|
+
const insight = synthesizePersonalUnderstanding(cleanTitle, cleanBody, isZh, usedInsights);
|
|
378
|
+
const normalizedInsight = insight === null ? null : normalizeBroadcastText(insight, isZh);
|
|
284
379
|
|
|
285
380
|
const factText = isZh ? `${normalizedLead}${normalizedBody}` : `${normalizedLead} ${normalizedBody}`;
|
|
286
381
|
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
382
|
+
// R4: facts first, chunked so no single segment runs past ~90s of speech; then the insight
|
|
383
|
+
// only when this episode has not already spoken that line (R2 collision ⇒ no segment).
|
|
384
|
+
for (const chunk of splitFactText(factText)) {
|
|
385
|
+
segments.push({
|
|
386
|
+
text: chunk,
|
|
387
|
+
gap_ms: 500, // Natural breathing pause before giving personal perspective
|
|
388
|
+
emotion: 'professional, clear',
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
if (normalizedInsight !== null) {
|
|
392
|
+
segments.push({
|
|
393
|
+
text: normalizedInsight,
|
|
394
|
+
gap_ms: 800, // Distinct 800ms pause before the next story
|
|
395
|
+
emotion: 'thoughtful, personal',
|
|
396
|
+
});
|
|
397
|
+
}
|
|
298
398
|
});
|
|
299
399
|
|
|
300
400
|
// 3. Outro segment: Warm, conversational wrap-up
|
|
@@ -21968,6 +21968,7 @@ var SOURCE_BASE = {
|
|
|
21968
21968
|
};
|
|
21969
21969
|
var DEFAULT_SOURCE_BASE = 5;
|
|
21970
21970
|
var TITLE_SIMILARITY_THRESHOLD = 0.6;
|
|
21971
|
+
var STUB_BODY_FLOOR = 300;
|
|
21971
21972
|
var FRESHNESS_DAYS = 7;
|
|
21972
21973
|
var PER_SOURCE_CAP = 5;
|
|
21973
21974
|
var NON_NEWS_TAGS = new Set(["geopolitics", "military-conflict", "war"]);
|
|
@@ -22166,8 +22167,9 @@ function classifyCategory(doc2) {
|
|
|
22166
22167
|
}
|
|
22167
22168
|
function scoreDimensions(doc2, planDate) {
|
|
22168
22169
|
const meta3 = asRecord(doc2.metadata);
|
|
22169
|
-
|
|
22170
|
-
|
|
22170
|
+
const bodyLen = (doc2.body ?? "").length;
|
|
22171
|
+
let quality = bodyLen < STUB_BODY_FLOOR ? 1 : 1 + Math.min(4, Math.round(bodyLen / 1500));
|
|
22172
|
+
if (doc2.sourceUri && bodyLen >= STUB_BODY_FLOOR)
|
|
22171
22173
|
quality = Math.min(5, quality + 1);
|
|
22172
22174
|
const source = typeof meta3.source === "string" ? meta3.source : "";
|
|
22173
22175
|
let importance = IMPORTANCE_BASE[source] ?? DEFAULT_IMPORTANCE;
|
|
@@ -35,6 +35,11 @@ const SOURCE_BASE: Record<string, number> = {
|
|
|
35
35
|
};
|
|
36
36
|
const DEFAULT_SOURCE_BASE = 5;
|
|
37
37
|
const TITLE_SIMILARITY_THRESHOLD = 0.6; // character-bigram Jaccard
|
|
38
|
+
// Minimum body length for any substance credit (task 0138 D2). Scoring runs on blend-time bodies:
|
|
39
|
+
// the 2026-09-16 aihot stubs are 234-255 EN chars, substantive items are >=348 — a headline-echo
|
|
40
|
+
// stub must not reach `qc_min_quality=2` on its sourceUri bonus alone.
|
|
41
|
+
// ponytail: re-calibrate with evidence if a source ships legitimate 256-347-char items.
|
|
42
|
+
const STUB_BODY_FLOOR = 300;
|
|
38
43
|
const FRESHNESS_DAYS = 7; // a daily episode drops items published before this window (dogfood 20260906: aihot served a 2020-2025 blog archive as "selected")
|
|
39
44
|
const PER_SOURCE_CAP = 5; // one feed can't flood the episode even when all its items score high
|
|
40
45
|
const NON_NEWS_TAGS = new Set(['geopolitics', 'military-conflict', 'war']); // horizon finance-news tags (dogfood 20260906: Iran oil tankers in an AI episode)
|
|
@@ -308,8 +313,10 @@ export function classifyCategory(doc: Doc): Category {
|
|
|
308
313
|
*/
|
|
309
314
|
export function scoreDimensions(doc: Doc, planDate: string): DimensionScores {
|
|
310
315
|
const meta = asRecord(doc.metadata);
|
|
311
|
-
|
|
312
|
-
|
|
316
|
+
// The floor short-circuits the sourcedness bonus: "quality 1 regardless of sourceUri" (R2).
|
|
317
|
+
const bodyLen = (doc.body ?? '').length;
|
|
318
|
+
let quality = bodyLen < STUB_BODY_FLOOR ? 1 : 1 + Math.min(4, Math.round(bodyLen / 1500));
|
|
319
|
+
if (doc.sourceUri && bodyLen >= STUB_BODY_FLOOR) quality = Math.min(5, quality + 1);
|
|
313
320
|
|
|
314
321
|
const source = typeof meta.source === 'string' ? meta.source : '';
|
|
315
322
|
let importance = IMPORTANCE_BASE[source] ?? DEFAULT_IMPORTANCE;
|