@gobing-ai/knowledge-kit 0.0.28 → 0.0.29
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 +27 -4
- package/plugins/generations/daily-article-gen/src/index.ts +34 -3
- package/plugins/generations/dailynews-gen/dist/index.js +27 -7
- package/plugins/generations/dailynews-gen/src/index.ts +29 -1
- package/plugins/generations/dailynews-gen/src/script-builder.ts +15 -12
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Bold.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/NotoSansSC-Regular.otf +0 -0
- package/plugins/generations/image-gen/assets/fonts/OFL.txt +92 -0
- package/plugins/generations/image-gen/assets/fonts/SOURCE.md +15 -0
- package/plugins/generations/image-gen/dist/index.js +591 -15
- package/plugins/generations/image-gen/package.json +1 -0
- package/plugins/generations/image-gen/presets/formats/daily-news-cover.yaml +2 -2
- package/plugins/generations/image-gen/presets/formats/xhs-card.yaml +19 -11
- package/plugins/generations/image-gen/presets/styles/xhs-knowledge.yaml +2 -1
- package/plugins/generations/image-gen/scripts/compose-e2e-artifact.ts +135 -0
- package/plugins/generations/image-gen/src/bytes.ts +28 -0
- package/plugins/generations/image-gen/src/compose/card.ts +294 -0
- package/plugins/generations/image-gen/src/compose/chromium.ts +147 -0
- package/plugins/generations/image-gen/src/compose/cmap.ts +177 -0
- package/plugins/generations/image-gen/src/index.ts +167 -9
- package/plugins/generations/image-gen/src/job.ts +6 -0
- package/plugins/generations/image-gen/src/presets.ts +53 -13
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/daily-voice-style.zh.md +25 -12
- package/plugins/kk/scripts/kk-workflow-stages.ts +268 -9
- package/plugins/kk/scripts/topic-boundary.json +105 -0
- package/plugins/kk/scripts/validate-voicescript.ts +15 -0
- package/plugins/kk/skills/content-safety/SKILL.md +163 -0
- package/plugins/kk/skills/content-safety/references/platforms/apple-podcasts.md +36 -0
- package/plugins/kk/skills/content-safety/references/platforms/netease-music.md +31 -0
- package/plugins/kk/skills/content-safety/references/platforms/wechatmp.md +32 -0
- package/plugins/kk/skills/content-safety/references/platforms/xiaoyuzhou.md +35 -0
- package/plugins/kk/skills/xhs-images/SKILL.md +11 -7
- package/plugins/kk/skills/xhs-images/references/card-layouts.md +4 -4
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +103 -13
- package/plugins/publishings/podcast-pub/dist/index.js +4 -0
- package/plugins/publishings/podcast-pub/src/index.ts +2 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +6 -0
package/package.json
CHANGED
|
@@ -22035,13 +22035,19 @@ ${metaLine ? `${metaLine}
|
|
|
22035
22035
|
|
|
22036
22036
|
` : ""}${stripScaffold(doc2.body ?? "").trim()}`;
|
|
22037
22037
|
}
|
|
22038
|
+
function episodeTitle(docs, date6) {
|
|
22039
|
+
const lead = docs[0]?.title?.trim() ?? "";
|
|
22040
|
+
const theme = lead !== "" ? lead : "\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5";
|
|
22041
|
+
return `AI \u4E71\u7096\uFF5C${theme} \xB7 ${date6}`;
|
|
22042
|
+
}
|
|
22038
22043
|
function renderDailyArticle(docs, date6) {
|
|
22039
22044
|
const items = docs.map(renderDoc).join(`
|
|
22040
22045
|
|
|
22041
22046
|
`);
|
|
22042
|
-
|
|
22047
|
+
const intro = docs.length > 0 ? `\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u5171 ${docs.length} \u6761\u5185\u5BB9\uFF0C\u5747\u9644\u53EF\u70B9\u51FB\u6765\u6E90\u94FE\u63A5\u3002` : "\u300CAI \u4E71\u7096\u300D\u662F\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684 AI \u6280\u672F\u5206\u4EAB\u680F\u76EE\uFF1A\u5DE5\u5177\u3001\u8BBA\u6587\u4E0E\u5B9E\u8DF5\u968F\u804A\u3002\u672C\u671F\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002";
|
|
22048
|
+
return `# ${episodeTitle(docs, date6)}
|
|
22043
22049
|
|
|
22044
|
-
|
|
22050
|
+
${intro}
|
|
22045
22051
|
|
|
22046
22052
|
${items}
|
|
22047
22053
|
`;
|
|
@@ -22054,10 +22060,26 @@ function tagsFor(docs) {
|
|
|
22054
22060
|
});
|
|
22055
22061
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
22056
22062
|
}
|
|
22063
|
+
var LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
22057
22064
|
function docsToContent(docs, date6, bodyOverride) {
|
|
22065
|
+
const title = episodeTitle(docs, date6);
|
|
22066
|
+
let body = renderDailyArticle(docs, date6);
|
|
22067
|
+
if (bodyOverride) {
|
|
22068
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), "");
|
|
22069
|
+
body = `# ${title}
|
|
22070
|
+
|
|
22071
|
+
${stripped}
|
|
22072
|
+
`;
|
|
22073
|
+
}
|
|
22074
|
+
body = `${body.trimEnd()}
|
|
22075
|
+
|
|
22076
|
+
---
|
|
22077
|
+
|
|
22078
|
+
*\u6587\u5B57\u7531 AI \u8F85\u52A9\u6574\u7406\uFF0C\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002*
|
|
22079
|
+
`;
|
|
22058
22080
|
return ContentSchema.parse({
|
|
22059
|
-
title
|
|
22060
|
-
body
|
|
22081
|
+
title,
|
|
22082
|
+
body,
|
|
22061
22083
|
format: "markdown",
|
|
22062
22084
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
22063
22085
|
metadata: { generator: "kk:daily-article", date: date6, doc_count: docs.length, tags: tagsFor(docs) }
|
|
@@ -22110,5 +22132,6 @@ export {
|
|
|
22110
22132
|
renderDailyArticle,
|
|
22111
22133
|
processGeneratorIO,
|
|
22112
22134
|
main2 as main,
|
|
22135
|
+
episodeTitle,
|
|
22113
22136
|
docsToContent
|
|
22114
22137
|
};
|
|
@@ -102,10 +102,27 @@ function renderDoc(doc: Doc, index: number): string {
|
|
|
102
102
|
return `## ${index + 1}. ${heading}\n\n${metaLine ? `${metaLine}\n\n` : ''}${stripScaffold(doc.body ?? '').trim()}`;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Episode title for the 「AI 乱炖」 column (task 0189 R1/R4): `AI 乱炖|<本期实际技术主题> · YYYY-MM-DD`.
|
|
107
|
+
* The deterministic proxy for 本期实际技术主题 is the lead plan item's title — the same doc the
|
|
108
|
+
* cover brief and XHS cards already treat as the episode's lead story. Pure function so the
|
|
109
|
+
* agent-written and template article paths, ID3, RSS and show notes all derive one identical
|
|
110
|
+
* title; never re-introduces the retired news-bulletin positioning (task 0189 R4).
|
|
111
|
+
*/
|
|
112
|
+
export function episodeTitle(docs: Doc[], date: string): string {
|
|
113
|
+
const lead = docs[0]?.title?.trim() ?? '';
|
|
114
|
+
const theme = lead !== '' ? lead : '工具、论文与实践';
|
|
115
|
+
return `AI 乱炖|${theme} · ${date}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
105
118
|
/** Render the article markdown for a merged Doc[] (pure — exported for tests). */
|
|
106
119
|
export function renderDailyArticle(docs: Doc[], date: string): string {
|
|
107
120
|
const items = docs.map(renderDoc).join('\n\n');
|
|
108
|
-
|
|
121
|
+
const intro =
|
|
122
|
+
docs.length > 0
|
|
123
|
+
? `「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期共 ${docs.length} 条内容,均附可点击来源链接。`
|
|
124
|
+
: '「AI 乱炖」是「咸话咸说」的 AI 技术分享栏目:工具、论文与实践随聊。本期没有足够扎实的可聊内容。';
|
|
125
|
+
return `# ${episodeTitle(docs, date)}\n\n${intro}\n\n${items}\n`;
|
|
109
126
|
}
|
|
110
127
|
|
|
111
128
|
/** Single-source daily tag: stamped on every Content this generator emits (task 0119). */
|
|
@@ -120,11 +137,25 @@ function tagsFor(docs: Doc[]): string[] {
|
|
|
120
137
|
return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
|
|
121
138
|
}
|
|
122
139
|
|
|
140
|
+
/** A leading H1 line — agent bodies used to carry their own title; the canonical title is computed (task 0189 R4). */
|
|
141
|
+
const LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
142
|
+
|
|
123
143
|
/** Build the article Content from a merged Doc[]; `bodyOverride` replaces the mechanical render (agent-written prose), still scaffold-stripped as a safety net. */
|
|
124
144
|
export function docsToContent(docs: Doc[], date: string, bodyOverride?: string): Content {
|
|
145
|
+
const title = episodeTitle(docs, date);
|
|
146
|
+
let body = renderDailyArticle(docs, date);
|
|
147
|
+
if (bodyOverride) {
|
|
148
|
+
// The canonical episode title is computed from the lead item; a leading H1 the agent may
|
|
149
|
+
// still have written is replaced (not duplicated) so body and metadata agree on every path.
|
|
150
|
+
const stripped = stripScaffold(bodyOverride.trim()).trim().replace(new RegExp(LEADING_H1_RE), '');
|
|
151
|
+
body = `# ${title}\n\n${stripped}\n`;
|
|
152
|
+
}
|
|
153
|
+
// Task 0189 R1: the AI-production note rides on every article path (mechanical and agent
|
|
154
|
+
// prose). Podcast show notes are rendered separately by podcast-pub and do not inherit it.
|
|
155
|
+
body = `${body.trimEnd()}\n\n---\n\n*文字由 AI 辅助整理,音频为 AI 合成配音。*\n`;
|
|
125
156
|
return ContentSchema.parse({
|
|
126
|
-
title
|
|
127
|
-
body
|
|
157
|
+
title,
|
|
158
|
+
body,
|
|
128
159
|
format: 'markdown',
|
|
129
160
|
references: docs.filter((d) => d.sourceUri).map((d) => ({ url: d.sourceUri, title: d.title, cite: d.id })),
|
|
130
161
|
metadata: { generator: 'kk:daily-article', date, doc_count: docs.length, tags: tagsFor(docs) },
|
|
@@ -22116,10 +22116,10 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22116
22116
|
const broadcastDate = options?.date ?? new Date;
|
|
22117
22117
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
22118
22118
|
if (docs.length === 0) {
|
|
22119
|
-
const title2 = options?.title || (isZh ? "\
|
|
22119
|
+
const title2 = options?.title || (isZh ? "AI \u4E71\u7096\uFF08\u6682\u65E0\u66F4\u65B0\uFF09" : "AI \u4E71\u7096 (No Updates)");
|
|
22120
22120
|
const segments2 = isZh ? [
|
|
22121
22121
|
{
|
|
22122
|
-
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\
|
|
22122
|
+
text: `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\u3002\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${voiceProfile}\u3002\u4ECA\u5929\u6CA1\u6709\u8DB3\u591F\u624E\u5B9E\u7684\u53EF\u804A\u5185\u5BB9\u3002`,
|
|
22123
22123
|
gap_ms: 400,
|
|
22124
22124
|
emotion: "calm"
|
|
22125
22125
|
},
|
|
@@ -22130,7 +22130,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22130
22130
|
}
|
|
22131
22131
|
] : [
|
|
22132
22132
|
{
|
|
22133
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
22133
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI \u4E71\u7096, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
22134
22134
|
gap_ms: 400,
|
|
22135
22135
|
emotion: "calm"
|
|
22136
22136
|
},
|
|
@@ -22151,7 +22151,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22151
22151
|
segments: segments2
|
|
22152
22152
|
});
|
|
22153
22153
|
}
|
|
22154
|
-
const title = options?.title || (isZh ?
|
|
22154
|
+
const title = options?.title || (isZh ? `AI \u4E71\u7096\uFF08\u5171${docs.length}\u6761\uFF09` : `AI \u4E71\u7096 (${docs.length} stories)`);
|
|
22155
22155
|
const segments = [];
|
|
22156
22156
|
let hostName = options?.speakers?.host?.name || voiceProfile.replace(/[-_]?(news|voice|bot|profile)$/i, "") || voiceProfile;
|
|
22157
22157
|
if (hostName.length > 0) {
|
|
@@ -22161,7 +22161,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22161
22161
|
const planItems = new Map((scriptPlan?.items ?? []).map((item) => [item.id, item]));
|
|
22162
22162
|
const countText = isZh ? `${formatChineseCount(docs.length)}\u6761` : `${docs.length}`;
|
|
22163
22163
|
const titlesSummary = docs.slice(0, 3).map((d) => (d.title ?? "").trim()).filter(Boolean).join("\u3001");
|
|
22164
|
-
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\
|
|
22164
|
+
const introText = isZh ? scriptPlan ? `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002${scriptPlan.intro}` : `\u5927\u5BB6\u597D\uFF0C\u4ECA\u5929\u662F${dateStr}\uFF0C\u6B22\u8FCE\u6536\u542C\u300C\u54B8\u8BDD\u54B8\u8BF4\u300D\u7684\u300CAI \u4E71\u7096\u300D\u680F\u76EE\uFF0C\u6211\u662F${hostName}\u3002\u4ECA\u5929\u6211\u4ECE AI \u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u4E2D\u6574\u7406\u4E86${countText}\u503C\u5F97\u804A\u7684\u5185\u5BB9${titlesSummary ? `\uFF0C\u5305\u62EC${titlesSummary}` : ""}\uFF0C\u54B1\u4EEC\u4E00\u6761\u6761\u6765\u804A\u3002` : `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI \u4E71\u7096, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
22165
22165
|
segments.push({
|
|
22166
22166
|
text: normalizeBroadcastText(introText, isZh),
|
|
22167
22167
|
gap_ms: 700,
|
|
@@ -22196,7 +22196,7 @@ function buildNewsVoiceScript(docs, options) {
|
|
|
22196
22196
|
});
|
|
22197
22197
|
}
|
|
22198
22198
|
});
|
|
22199
|
-
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\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` : `\u597D\u4E86\uFF0C\
|
|
22199
|
+
const outroText = isZh ? scriptPlan ? `${scriptPlan.outro}\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\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` : `\u597D\u4E86\uFF0C\u672C\u671FAI\u4E71\u7096\u5C31\u804A\u5230\u8FD9\u91CC\u3002\u5E0C\u671B\u4ECA\u5929\u8FD9\u4E9B\u5DE5\u5177\u3001\u8BBA\u6587\u548C\u5B9E\u8DF5\u7684\u7ECF\u9A8C\u5BF9\u60A8\u6709\u542F\u53D1\u3002\u672C\u671F\u97F3\u9891\u4E3A AI \u5408\u6210\u914D\u97F3\u3002\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 this episode of AI \u4E71\u7096. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
22200
22200
|
segments.push({
|
|
22201
22201
|
text: normalizeBroadcastText(outroText, isZh),
|
|
22202
22202
|
gap_ms: 300,
|
|
@@ -22456,6 +22456,24 @@ async function resolveScriptPlan(fs, docs) {
|
|
|
22456
22456
|
}
|
|
22457
22457
|
return { scriptPlan: validated.plan, scriptSource: "plan", scriptPlanErrors: [] };
|
|
22458
22458
|
}
|
|
22459
|
+
function broadcastDateFromEnv(raw) {
|
|
22460
|
+
if (!raw)
|
|
22461
|
+
return;
|
|
22462
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
22463
|
+
if (match === null) {
|
|
22464
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
22465
|
+
return;
|
|
22466
|
+
}
|
|
22467
|
+
const year = Number(match[1]);
|
|
22468
|
+
const month = Number(match[2]) - 1;
|
|
22469
|
+
const day = Number(match[3]);
|
|
22470
|
+
const date6 = new Date(year, month, day);
|
|
22471
|
+
if (date6.getFullYear() !== year || date6.getMonth() !== month || date6.getDate() !== day) {
|
|
22472
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
22473
|
+
return;
|
|
22474
|
+
}
|
|
22475
|
+
return date6;
|
|
22476
|
+
}
|
|
22459
22477
|
async function processGeneratorIO(inputPath, outputPath, options) {
|
|
22460
22478
|
const fs = createNodeFileSystem();
|
|
22461
22479
|
let docs;
|
|
@@ -22475,7 +22493,8 @@ async function processGeneratorIO(inputPath, outputPath, options) {
|
|
|
22475
22493
|
}
|
|
22476
22494
|
const isZh = (options?.language || "zh").toLowerCase() === "zh";
|
|
22477
22495
|
const planOutcome = isZh ? await resolveScriptPlan(fs, docs) : { scriptSource: "template", scriptPlanErrors: [] };
|
|
22478
|
-
const
|
|
22496
|
+
const date6 = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
22497
|
+
const script = buildNewsVoiceScript(docs, { ...options, date: date6, scriptPlan: planOutcome.scriptPlan });
|
|
22479
22498
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
22480
22499
|
const content = {
|
|
22481
22500
|
title: script.title,
|
|
@@ -22556,6 +22575,7 @@ export {
|
|
|
22556
22575
|
formatChineseCount,
|
|
22557
22576
|
formatBroadcastDate,
|
|
22558
22577
|
buildNewsVoiceScript,
|
|
22578
|
+
broadcastDateFromEnv,
|
|
22559
22579
|
VoiceSpeakerSchema,
|
|
22560
22580
|
VoiceScriptSegmentSchema,
|
|
22561
22581
|
VoiceScriptSchema,
|
|
@@ -66,6 +66,32 @@ async function resolveScriptPlan(fs: FileSystem, docs: Doc[]): Promise<ScriptPla
|
|
|
66
66
|
return { scriptPlan: validated.plan, scriptSource: 'plan', scriptPlanErrors: [] };
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/**
|
|
70
|
+
* `DAILYNEWS_DATE` (YYYYMMDD or `YYYY-MM-DD`) → a local-midnight Date. Task 0189: the broadcast
|
|
71
|
+
* greeting must carry the episode's run date, not the render date — a 9/29 episode rebuilt on 9/30
|
|
72
|
+
* must not greet "今天是9月30日". Parsed as local calendar parts, never UTC (`new Date('2026-09-29')`
|
|
73
|
+
* is UTC midnight and renders the previous day west of UTC). Absent → undefined; garbled or
|
|
74
|
+
* out-of-range (the Date constructor rolls over, e.g. month 13) → undefined with a warn so a
|
|
75
|
+
* caller bug surfaces instead of silently broadcasting the wrong day.
|
|
76
|
+
*/
|
|
77
|
+
export function broadcastDateFromEnv(raw: string | undefined): Date | undefined {
|
|
78
|
+
if (!raw) return undefined;
|
|
79
|
+
const match = raw.match(/^(\d{4})-?(\d{2})-?(\d{2})$/);
|
|
80
|
+
if (match === null) {
|
|
81
|
+
echoError(`dailynews-gen: ignoring malformed DAILYNEWS_DATE "${raw}" (expected YYYYMMDD or YYYY-MM-DD)`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const year = Number(match[1]);
|
|
85
|
+
const month = Number(match[2]) - 1;
|
|
86
|
+
const day = Number(match[3]);
|
|
87
|
+
const date = new Date(year, month, day);
|
|
88
|
+
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
|
89
|
+
echoError(`dailynews-gen: ignoring out-of-range DAILYNEWS_DATE "${raw}"`);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
return date;
|
|
93
|
+
}
|
|
94
|
+
|
|
69
95
|
export async function processGeneratorIO(
|
|
70
96
|
inputPath: string,
|
|
71
97
|
outputPath: string,
|
|
@@ -101,7 +127,9 @@ export async function processGeneratorIO(
|
|
|
101
127
|
const planOutcome = isZh
|
|
102
128
|
? await resolveScriptPlan(fs, docs)
|
|
103
129
|
: { scriptSource: 'template' as const, scriptPlanErrors: [] as ScriptPlanError[] };
|
|
104
|
-
|
|
130
|
+
// Explicit option wins; else DAILYNEWS_DATE (workflow backfill/retry); else the builder's now.
|
|
131
|
+
const date = options?.date ?? broadcastDateFromEnv(process.env.DAILYNEWS_DATE);
|
|
132
|
+
const script = buildNewsVoiceScript(docs, { ...options, date, scriptPlan: planOutcome.scriptPlan });
|
|
105
133
|
const yamlBody = formatVoiceScriptToYaml(script);
|
|
106
134
|
|
|
107
135
|
const content: Content = {
|
|
@@ -313,11 +313,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
313
313
|
const dateStr = formatBroadcastDate(broadcastDate, language);
|
|
314
314
|
|
|
315
315
|
if (docs.length === 0) {
|
|
316
|
-
const title = options?.title || (isZh ? '
|
|
316
|
+
const title = options?.title || (isZh ? 'AI 乱炖(暂无更新)' : 'AI 乱炖 (No Updates)');
|
|
317
317
|
const segments: VoiceScriptSegment[] = isZh
|
|
318
318
|
? [
|
|
319
319
|
{
|
|
320
|
-
|
|
320
|
+
// Task 0189 R1/R4: honest no-content episode — the copy names the show
|
|
321
|
+
// (咸话咸说) and the column (AI 乱炖), and never claims a news digest happened.
|
|
322
|
+
text: `大家好,今天是${dateStr}。欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${voiceProfile}。今天没有足够扎实的可聊内容。`,
|
|
321
323
|
gap_ms: 400,
|
|
322
324
|
emotion: 'calm',
|
|
323
325
|
},
|
|
@@ -329,7 +331,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
329
331
|
]
|
|
330
332
|
: [
|
|
331
333
|
{
|
|
332
|
-
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with
|
|
334
|
+
text: `Hello and welcome. Today is ${dateStr}. This is ${voiceProfile} with AI 乱炖, our chat about AI tools, papers, and hands-on practice. There is nothing solid enough to share today.`,
|
|
333
335
|
gap_ms: 400,
|
|
334
336
|
emotion: 'calm',
|
|
335
337
|
},
|
|
@@ -352,9 +354,7 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
352
354
|
});
|
|
353
355
|
}
|
|
354
356
|
|
|
355
|
-
const title =
|
|
356
|
-
options?.title ||
|
|
357
|
-
(isZh ? `今日AI科技漫谈(共${docs.length}条)` : `AI Daily Briefing (${docs.length} stories)`);
|
|
357
|
+
const title = options?.title || (isZh ? `AI 乱炖(共${docs.length}条)` : `AI 乱炖 (${docs.length} stories)`);
|
|
358
358
|
const segments: VoiceScriptSegment[] = [];
|
|
359
359
|
|
|
360
360
|
let hostName =
|
|
@@ -382,9 +382,10 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
382
382
|
? scriptPlan
|
|
383
383
|
? // F613: fixed greeting stays builder-owned; the plan only fills the slot between
|
|
384
384
|
// greeting and first story (the template tail 从「今天科技圈有几条…正题。」 is replaced).
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
// Task 0189 R1: the greeting names the show (咸话咸说) and the column (AI 乱炖).
|
|
386
|
+
`大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。${scriptPlan.intro}`
|
|
387
|
+
: `大家好,今天是${dateStr},欢迎收听「咸话咸说」的「AI 乱炖」栏目,我是${hostName}。今天我从 AI 工具、论文和实践中整理了${countText}值得聊的内容${titlesSummary ? `,包括${titlesSummary}` : ''},咱们一条条来聊。`
|
|
388
|
+
: `Hello and welcome. Today is ${dateStr}. This is ${hostName} with AI 乱炖, where we talk about AI tools, papers, and hands-on practice. In today's episode, we have ${countText} stories. Let's dive straight in.`;
|
|
388
389
|
|
|
389
390
|
segments.push({
|
|
390
391
|
text: normalizeBroadcastText(introText, isZh),
|
|
@@ -449,11 +450,13 @@ export function buildNewsVoiceScript(docs: Doc[], options?: ScriptBuilderOptions
|
|
|
449
450
|
|
|
450
451
|
// 3. Outro segment: Warm, conversational wrap-up. F613: the plan outro replaces the template
|
|
451
452
|
// wrap-up head; the fixed sign-off stays builder-owned.
|
|
453
|
+
// Task 0189 R1: the AI-voice disclosure is part of the builder-owned sign-off (not the plan),
|
|
454
|
+
// so it is spoken in every episode — Apple Podcasts guideline 1.11 requires it in the content.
|
|
452
455
|
const outroText = isZh
|
|
453
456
|
? scriptPlan
|
|
454
|
-
? `${scriptPlan.outro}
|
|
455
|
-
:
|
|
456
|
-
: `That wraps up
|
|
457
|
+
? `${scriptPlan.outro}本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
458
|
+
: `好了,本期AI乱炖就聊到这里。希望今天这些工具、论文和实践的经验对您有启发。本期音频为 AI 合成配音。感谢您的收听,我是${hostName},祝您拥有充实高效的一天,我们下期节目不见不散!`
|
|
459
|
+
: `That wraps up this episode of AI 乱炖. I hope today's takeaways were useful. The audio in this episode is AI-synthesized. Thank you for listening. This is ${hostName}, wishing you a productive and wonderful day. Until next time!`;
|
|
457
460
|
|
|
458
461
|
segments.push({
|
|
459
462
|
text: normalizeBroadcastText(outroText, isZh),
|
|
Binary file
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
This Font Software is licensed under the SIL Open Font License,
|
|
2
|
+
Version 1.1.
|
|
3
|
+
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
-----------------------------------------------------------
|
|
8
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
9
|
+
-----------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
PREAMBLE
|
|
12
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
13
|
+
development of collaborative font projects, to support the font
|
|
14
|
+
creation efforts of academic and linguistic communities, and to
|
|
15
|
+
provide a free and open framework in which fonts may be shared and
|
|
16
|
+
improved in partnership with others.
|
|
17
|
+
|
|
18
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
19
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
20
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
21
|
+
redistributed and/or sold with any software provided that any reserved
|
|
22
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
23
|
+
however, cannot be released under any other type of license. The
|
|
24
|
+
requirement for fonts to remain under this license does not apply to
|
|
25
|
+
any document created using the fonts or their derivatives.
|
|
26
|
+
|
|
27
|
+
DEFINITIONS
|
|
28
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
29
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
30
|
+
include source files, build scripts and documentation.
|
|
31
|
+
|
|
32
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
33
|
+
copyright statement(s).
|
|
34
|
+
|
|
35
|
+
"Original Version" refers to the collection of Font Software
|
|
36
|
+
components as distributed by the Copyright Holder(s).
|
|
37
|
+
|
|
38
|
+
"Modified Version" refers to any derivative made by adding to,
|
|
39
|
+
deleting, or substituting -- in part or in whole -- any of the
|
|
40
|
+
components of the Original Version, by changing formats or by porting
|
|
41
|
+
the Font Software to a new environment.
|
|
42
|
+
|
|
43
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
44
|
+
writer or other person who contributed to the Font Software.
|
|
45
|
+
|
|
46
|
+
PERMISSION & CONDITIONS
|
|
47
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
48
|
+
a copy of the Font Software, to use, study, copy, merge, embed,
|
|
49
|
+
modify, redistribute, and sell modified and unmodified copies of the
|
|
50
|
+
Font Software, subject to the following conditions:
|
|
51
|
+
|
|
52
|
+
1) Neither the Font Software nor any of its individual components, in
|
|
53
|
+
Original or Modified Versions, may be sold by itself.
|
|
54
|
+
|
|
55
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
56
|
+
redistributed and/or sold with any software, provided that each copy
|
|
57
|
+
contains the above copyright notice and this license. These can be
|
|
58
|
+
included either as stand-alone text files, human-readable headers or
|
|
59
|
+
in the appropriate machine-readable metadata fields within text or
|
|
60
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
61
|
+
|
|
62
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
63
|
+
Name(s) unless explicit written permission is granted by the
|
|
64
|
+
corresponding Copyright Holder. This restriction only applies to the
|
|
65
|
+
primary font name as presented to the users.
|
|
66
|
+
|
|
67
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
68
|
+
Software shall not be used to promote, endorse or advertise any
|
|
69
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
70
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
71
|
+
permission.
|
|
72
|
+
|
|
73
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
74
|
+
must be distributed entirely under this license, and must not be
|
|
75
|
+
distributed under any other license. The requirement for fonts to
|
|
76
|
+
remain under this license does not apply to any document created using
|
|
77
|
+
the Font Software.
|
|
78
|
+
|
|
79
|
+
TERMINATION
|
|
80
|
+
This license becomes null and void if any of the above conditions are
|
|
81
|
+
not met.
|
|
82
|
+
|
|
83
|
+
DISCLAIMER
|
|
84
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
85
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
86
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
87
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
88
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
89
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
90
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
91
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
92
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Noto Sans SC — provenance
|
|
2
|
+
|
|
3
|
+
Shipped static SubsetOTF/SC fonts used by the `compose` text-card composer (task 0180, ADR-028).
|
|
4
|
+
Output is reproducible on every install because these bytes ship with the plugin — no runtime
|
|
5
|
+
download and no system-font lookup.
|
|
6
|
+
|
|
7
|
+
- **Upstream:** https://github.com/notofonts/noto-cjk (Noto Sans CJK SC, static SubsetOTF)
|
|
8
|
+
- **Version:** tag `Sans2.004` (https://github.com/notofonts/noto-cjk/releases/tag/Sans2.004)
|
|
9
|
+
- **Licence:** SIL Open Font License 1.1 (`OFL.txt`, from the repo root `LICENSE` at the same tag)
|
|
10
|
+
|
|
11
|
+
| File | Upstream path at `Sans2.004` | SHA-256 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| `NotoSansSC-Regular.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Regular.otf` | `faa6c9df652116dde789d351359f3d7e5d2285a2b2a1f04a2d7244df706d5ea9` |
|
|
14
|
+
| `NotoSansSC-Bold.otf` | `Sans/SubsetOTF/SC/NotoSansSC-Bold.otf` | `c6cb5a93abaa9edc8ee7463b7ebb7f42d618d40e6ed2f7a5371c97b0b64767c0` |
|
|
15
|
+
| `OFL.txt` | `LICENSE` | `6a73f9541c2de74158c0e7cf6b0a58ef774f5a780bf191f2d7ec9cc53efe2bf2` |
|