@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
|
@@ -22030,6 +22030,37 @@ function minimumsFromEnv() {
|
|
|
22030
22030
|
}
|
|
22031
22031
|
return out;
|
|
22032
22032
|
}
|
|
22033
|
+
async function readQcSummary(path2) {
|
|
22034
|
+
if (!path2 || path2.trim() === "")
|
|
22035
|
+
return null;
|
|
22036
|
+
try {
|
|
22037
|
+
const parsed = JSON.parse(await Bun.file(path2).text());
|
|
22038
|
+
const qc = parsed?.metadata?.qc;
|
|
22039
|
+
if (qc === null || typeof qc !== "object")
|
|
22040
|
+
return null;
|
|
22041
|
+
const record2 = qc;
|
|
22042
|
+
const counts = new Map;
|
|
22043
|
+
const audits = Array.isArray(record2.segmentAudits) ? record2.segmentAudits : [];
|
|
22044
|
+
for (const audit of audits) {
|
|
22045
|
+
const issues = audit?.issues;
|
|
22046
|
+
if (!Array.isArray(issues))
|
|
22047
|
+
continue;
|
|
22048
|
+
for (const issue2 of issues) {
|
|
22049
|
+
if (typeof issue2 !== "string")
|
|
22050
|
+
continue;
|
|
22051
|
+
const type = issue2.split(":")[0]?.trim() || issue2;
|
|
22052
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
22053
|
+
}
|
|
22054
|
+
}
|
|
22055
|
+
return {
|
|
22056
|
+
passed: record2.passed === true,
|
|
22057
|
+
overallScore: typeof record2.overallScore === "number" ? record2.overallScore : 0,
|
|
22058
|
+
issues: [...counts.entries()].map(([type, count]) => ({ type, count })).sort((a, b) => b.count - a.count || a.type.localeCompare(b.type))
|
|
22059
|
+
};
|
|
22060
|
+
} catch {
|
|
22061
|
+
return null;
|
|
22062
|
+
}
|
|
22063
|
+
}
|
|
22033
22064
|
async function readTiming(path2) {
|
|
22034
22065
|
if (!path2 || path2.trim() === "")
|
|
22035
22066
|
return [];
|
|
@@ -22101,6 +22132,16 @@ function renderReportMarkdown(docs, date6, options = {}) {
|
|
|
22101
22132
|
}
|
|
22102
22133
|
lines.push(`| category allowlist | \u2014 | ${counts.category} |`);
|
|
22103
22134
|
lines.push("");
|
|
22135
|
+
const qc = options.qc ?? null;
|
|
22136
|
+
if (qc !== null) {
|
|
22137
|
+
lines.push("## \u97F3\u9891 QC", "");
|
|
22138
|
+
lines.push("| \u6307\u6807 | \u503C |", "| --- | --- |");
|
|
22139
|
+
lines.push(`| \u7EFC\u5408\u8BC4\u5206 | ${qc.overallScore} |`);
|
|
22140
|
+
lines.push(`| \u901A\u8FC7 | ${qc.passed ? "\u662F" : "\u5426"} |`);
|
|
22141
|
+
for (const issue2 of qc.issues)
|
|
22142
|
+
lines.push(`| ${issue2.type} | ${issue2.count} |`);
|
|
22143
|
+
lines.push("");
|
|
22144
|
+
}
|
|
22104
22145
|
const timing = options.timing ?? [];
|
|
22105
22146
|
if (timing.length > 0) {
|
|
22106
22147
|
lines.push("## \u6B65\u8017\u65F6", "");
|
|
@@ -22127,7 +22168,10 @@ async function readRejectedAudit(path2) {
|
|
|
22127
22168
|
return [];
|
|
22128
22169
|
try {
|
|
22129
22170
|
const parsed = JSON.parse(await Bun.file(path2).text());
|
|
22130
|
-
|
|
22171
|
+
if (Array.isArray(parsed))
|
|
22172
|
+
return parsed;
|
|
22173
|
+
const rejected = parsed?.rejected;
|
|
22174
|
+
return Array.isArray(rejected) ? rejected : [];
|
|
22131
22175
|
} catch {
|
|
22132
22176
|
return [];
|
|
22133
22177
|
}
|
|
@@ -22148,7 +22192,8 @@ async function processGeneratorIO(inputPath, outputPath) {
|
|
|
22148
22192
|
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
22149
22193
|
const rejected = await readRejectedAudit(process.env.NEWS_REPORT_REJECTED_FILE);
|
|
22150
22194
|
const timing = await readTiming(process.env.NEWS_REPORT_TIMING_FILE);
|
|
22151
|
-
const
|
|
22195
|
+
const qc = await readQcSummary(process.env.NEWS_REPORT_QC_FILE);
|
|
22196
|
+
const content = docsToContent(docs, dateFromEnv(), { rejected, timing, qc });
|
|
22152
22197
|
const fs = createNodeFileSystem();
|
|
22153
22198
|
const outDir = dirname2(outputPath);
|
|
22154
22199
|
if (outDir && outDir !== ".")
|
|
@@ -22186,6 +22231,7 @@ export {
|
|
|
22186
22231
|
rejectedCounts,
|
|
22187
22232
|
readTiming,
|
|
22188
22233
|
readRejectedAudit,
|
|
22234
|
+
readQcSummary,
|
|
22189
22235
|
processGeneratorIO,
|
|
22190
22236
|
minimumsFromEnv,
|
|
22191
22237
|
main2 as main,
|
|
@@ -17,7 +17,9 @@ import { echoError } from '@gobing-ai/ts-utils';
|
|
|
17
17
|
* workflow from the filter Content's metadata.docs). Scores live in
|
|
18
18
|
* `metadata.scores` (quality/importance/urgency/impact 0-5).
|
|
19
19
|
* - `NEWS_REPORT_REJECTED_FILE` — the filter's rejected audit path
|
|
20
|
-
* (`2-plan/candidates.rejected.json`, RejectedDoc[] of {doc, reasons})
|
|
20
|
+
* (`2-plan/candidates.rejected.json`, RejectedDoc[] of {doc, reasons})
|
|
21
|
+
* - `NEWS_REPORT_QC_FILE` — the generate stage's QC audit path
|
|
22
|
+
* (`3-audio/<date>_11_generate_content.json`); absent -> the 音频 QC block is omitted;
|
|
21
23
|
* absent or unreadable -> reject counts render as 0, never throw.
|
|
22
24
|
* - `NEWS_REPORT_TIMING_FILE` — `<work_dir>/step-timing.json` JSONL of
|
|
23
25
|
* {step, startedAt, endedAt} appended by the workflow shell steps; missing
|
|
@@ -144,6 +146,51 @@ interface RejectedAudit {
|
|
|
144
146
|
reasons: string[];
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/** Generate-stage audio QC summary rendered into the run report (task 0139 R5b). */
|
|
150
|
+
export interface QcSummary {
|
|
151
|
+
passed: boolean;
|
|
152
|
+
overallScore: number;
|
|
153
|
+
/** Per-issue-type counts, most frequent first; `duration_anomaly: <detail>` counts as its type. */
|
|
154
|
+
issues: { type: string; count: number }[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read the generate stage's QC audit (`<run_date>_11_generate_content.json`) (task 0139 R5b).
|
|
159
|
+
*
|
|
160
|
+
* Best-effort by contract: an absent, unreadable, or malformed file returns `null` and the report
|
|
161
|
+
* omits the section — a run report must never fail a run. Issue types are the text before the first
|
|
162
|
+
* `:` so a detailed `duration_anomaly: expected …` rolls up under one row.
|
|
163
|
+
*/
|
|
164
|
+
export async function readQcSummary(path: string | undefined): Promise<QcSummary | null> {
|
|
165
|
+
if (!path || path.trim() === '') return null;
|
|
166
|
+
try {
|
|
167
|
+
const parsed = JSON.parse(await Bun.file(path).text()) as { metadata?: { qc?: unknown } } | null;
|
|
168
|
+
const qc = parsed?.metadata?.qc;
|
|
169
|
+
if (qc === null || typeof qc !== 'object') return null;
|
|
170
|
+
const record = qc as { passed?: unknown; overallScore?: unknown; segmentAudits?: unknown };
|
|
171
|
+
const counts = new Map<string, number>();
|
|
172
|
+
const audits = Array.isArray(record.segmentAudits) ? record.segmentAudits : [];
|
|
173
|
+
for (const audit of audits) {
|
|
174
|
+
const issues = (audit as { issues?: unknown } | null)?.issues;
|
|
175
|
+
if (!Array.isArray(issues)) continue;
|
|
176
|
+
for (const issue of issues) {
|
|
177
|
+
if (typeof issue !== 'string') continue;
|
|
178
|
+
const type = issue.split(':')[0]?.trim() || issue;
|
|
179
|
+
counts.set(type, (counts.get(type) ?? 0) + 1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
passed: record.passed === true,
|
|
184
|
+
overallScore: typeof record.overallScore === 'number' ? record.overallScore : 0,
|
|
185
|
+
issues: [...counts.entries()]
|
|
186
|
+
.map(([type, count]) => ({ type, count }))
|
|
187
|
+
.sort((a, b) => b.count - a.count || a.type.localeCompare(b.type)),
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
147
194
|
/** Parse the timing JSONL; unreadable/missing -> [] (never throw). Last entry per step wins. */
|
|
148
195
|
export async function readTiming(path: string | undefined): Promise<TimingEntry[]> {
|
|
149
196
|
if (!path || path.trim() === '') return [];
|
|
@@ -180,7 +227,11 @@ function durationMs(entry: TimingEntry): number | null {
|
|
|
180
227
|
export function renderReportMarkdown(
|
|
181
228
|
docs: Doc[],
|
|
182
229
|
date: string,
|
|
183
|
-
options: {
|
|
230
|
+
options: {
|
|
231
|
+
rejected?: { doc: Doc; reasons: string[] }[];
|
|
232
|
+
timing?: TimingEntry[];
|
|
233
|
+
qc?: QcSummary | null;
|
|
234
|
+
} = {},
|
|
184
235
|
): string {
|
|
185
236
|
const lines: string[] = [`# 每日 AI 语音运行报告 — ${date}`, ''];
|
|
186
237
|
lines.push('## 选稿评分(保留候选)', '');
|
|
@@ -221,6 +272,18 @@ export function renderReportMarkdown(
|
|
|
221
272
|
lines.push(`| category allowlist | — | ${counts.category} |`);
|
|
222
273
|
lines.push('');
|
|
223
274
|
|
|
275
|
+
const qc = options.qc ?? null;
|
|
276
|
+
if (qc !== null) {
|
|
277
|
+
// 0139 R5b: the audio QC verdict is otherwise invisible in the report — this run published
|
|
278
|
+
// an episode whose audit said `passed:false`, and nothing but the artifact recorded it.
|
|
279
|
+
lines.push('## 音频 QC', '');
|
|
280
|
+
lines.push('| 指标 | 值 |', '| --- | --- |');
|
|
281
|
+
lines.push(`| 综合评分 | ${qc.overallScore} |`);
|
|
282
|
+
lines.push(`| 通过 | ${qc.passed ? '是' : '否'} |`);
|
|
283
|
+
for (const issue of qc.issues) lines.push(`| ${issue.type} | ${issue.count} |`);
|
|
284
|
+
lines.push('');
|
|
285
|
+
}
|
|
286
|
+
|
|
224
287
|
const timing = options.timing ?? [];
|
|
225
288
|
if (timing.length > 0) {
|
|
226
289
|
lines.push('## 步耗时', '');
|
|
@@ -240,12 +303,20 @@ export function renderReportMarkdown(
|
|
|
240
303
|
return lines.join('\n');
|
|
241
304
|
}
|
|
242
305
|
|
|
243
|
-
/**
|
|
306
|
+
/**
|
|
307
|
+
* Read the rejected audit; absent/unreadable/malformed -> [] (never throw).
|
|
308
|
+
*
|
|
309
|
+
* Two writer shapes: a bare `RejectedAudit[]` (older producers) and episode-plan-gen's
|
|
310
|
+
* `{ rejected, config, total }` envelope (`processFilterIO` — the live one). Reading only the
|
|
311
|
+
* array shape reported 0 rejections for every real run (task 0138 D1).
|
|
312
|
+
*/
|
|
244
313
|
export async function readRejectedAudit(path: string | undefined): Promise<RejectedAudit[]> {
|
|
245
314
|
if (!path || path.trim() === '') return [];
|
|
246
315
|
try {
|
|
247
|
-
const parsed = JSON.parse(await Bun.file(path).text());
|
|
248
|
-
|
|
316
|
+
const parsed: unknown = JSON.parse(await Bun.file(path).text());
|
|
317
|
+
if (Array.isArray(parsed)) return parsed as RejectedAudit[];
|
|
318
|
+
const rejected = (parsed as { rejected?: unknown } | null)?.rejected;
|
|
319
|
+
return Array.isArray(rejected) ? (rejected as RejectedAudit[]) : [];
|
|
249
320
|
} catch {
|
|
250
321
|
return [];
|
|
251
322
|
}
|
|
@@ -255,7 +326,11 @@ export async function readRejectedAudit(path: string | undefined): Promise<Rejec
|
|
|
255
326
|
export function docsToContent(
|
|
256
327
|
docs: Doc[],
|
|
257
328
|
date: string,
|
|
258
|
-
options: {
|
|
329
|
+
options: {
|
|
330
|
+
rejected?: { doc: Doc; reasons: string[] }[];
|
|
331
|
+
timing?: TimingEntry[];
|
|
332
|
+
qc?: QcSummary | null;
|
|
333
|
+
},
|
|
259
334
|
): Content {
|
|
260
335
|
return ContentSchema.parse({
|
|
261
336
|
title: `每日 AI 语音运行报告 — ${date}`,
|
|
@@ -274,7 +349,8 @@ export async function processGeneratorIO(inputPath: string, outputPath: string):
|
|
|
274
349
|
const docs = DocListSchema.parse(await readJsonFile(inputPath));
|
|
275
350
|
const rejected = await readRejectedAudit(process.env.NEWS_REPORT_REJECTED_FILE);
|
|
276
351
|
const timing = await readTiming(process.env.NEWS_REPORT_TIMING_FILE);
|
|
277
|
-
const
|
|
352
|
+
const qc = await readQcSummary(process.env.NEWS_REPORT_QC_FILE);
|
|
353
|
+
const content = docsToContent(docs, dateFromEnv(), { rejected, timing, qc });
|
|
278
354
|
const fs = createNodeFileSystem();
|
|
279
355
|
const outDir = dirname(outputPath);
|
|
280
356
|
if (outDir && outDir !== '.') await fs.ensureDir(outDir);
|
|
@@ -303,3 +303,19 @@ def run(
|
|
|
303
303
|
except BaseException:
|
|
304
304
|
_delete(out_path, audio_path, mp3_path)
|
|
305
305
|
raise
|
|
306
|
+
|
|
307
|
+
# R5a (task 0139): repetition is the one QC issue the verify loop cannot fix and the one that
|
|
308
|
+
# damages credibility on air, so it fails the run — but only AFTER content.json is on disk, and
|
|
309
|
+
# outside the delete-on-failure try above, so the operator can inspect the audit that condemned
|
|
310
|
+
# it. The audio and MP3 stay too: this is a review stop, not a corrupt-artifact cleanup. Recovery
|
|
311
|
+
# is HITL YAML inspection plus a re-run; Q&A item 3 ships this with no override knob.
|
|
312
|
+
repeated = [
|
|
313
|
+
audit.get("segmentIndex")
|
|
314
|
+
for audit in content.metadata.get("qc", {}).get("segmentAudits", [])
|
|
315
|
+
if "repetition_detected" in audit.get("issues", [])
|
|
316
|
+
]
|
|
317
|
+
if repeated:
|
|
318
|
+
indexes = ", ".join(str(index) for index in repeated)
|
|
319
|
+
raise RuntimeError(
|
|
320
|
+
f"segment(s) {indexes} carry repetition_detected — content.json written for inspection"
|
|
321
|
+
)
|
|
@@ -24,13 +24,6 @@ MAX_RATE_ZH = 15.0
|
|
|
24
24
|
MAX_RATE_EN = 30.0
|
|
25
25
|
MAX_RATE_OTHER = 25.0
|
|
26
26
|
|
|
27
|
-
MAX_DURATION_ZH = 15.0
|
|
28
|
-
MAX_DURATION_EN = 30.0
|
|
29
|
-
MAX_DURATION_OTHER = 20.0
|
|
30
|
-
|
|
31
|
-
MAX_DURATION_ZH_DIVISOR = 15.0
|
|
32
|
-
MAX_DURATION_EN_DIVISOR = 20.0
|
|
33
|
-
MAX_DURATION_OTHER_DIVISOR = 15.0
|
|
34
27
|
|
|
35
28
|
RE_CHAR_RUN = re.compile(r"(.)\1{2,}", re.DOTALL)
|
|
36
29
|
RE_WORD_RUN = re.compile(r"(.{2,3}?)\1{2,}", re.DOTALL)
|
|
@@ -185,21 +178,10 @@ def audit_voice_segments(
|
|
|
185
178
|
abs_anomaly = duration > max_sec + MAX_ABS_DIFF_SEC if max_sec > 0 else False
|
|
186
179
|
max_rate = MAX_RATE_ZH if language.startswith("zh") else MAX_RATE_EN if language.startswith("en") else MAX_RATE_OTHER
|
|
187
180
|
rate_anomaly = duration > 0 and (len(text) / duration) > max_rate
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if language.startswith("en")
|
|
193
|
-
else MAX_DURATION_OTHER
|
|
194
|
-
) + len(text) / (
|
|
195
|
-
MAX_DURATION_ZH_DIVISOR
|
|
196
|
-
if language.startswith("zh")
|
|
197
|
-
else MAX_DURATION_EN_DIVISOR
|
|
198
|
-
if language.startswith("en")
|
|
199
|
-
else MAX_DURATION_OTHER_DIVISOR
|
|
200
|
-
)
|
|
201
|
-
abs_ceiling_anomaly = duration > max_possible
|
|
202
|
-
duration_anomaly = ratio_anomaly or abs_anomaly or rate_anomaly or abs_ceiling_anomaly
|
|
181
|
+
# The former abs-ceiling term (duration > MAX_DURATION_<lang> + len(text)/<divisor>)
|
|
182
|
+
# compared against a fastest-speech floor, so any segment over ~39 chars flagged even
|
|
183
|
+
# when inside its expected range; genuinely overlong audio stays covered by ratio/abs/rate.
|
|
184
|
+
duration_anomaly = ratio_anomaly or abs_anomaly or rate_anomaly
|
|
203
185
|
if duration_anomaly:
|
|
204
186
|
issues.append(
|
|
205
187
|
f"duration_anomaly: expected {min_sec:.2f}s-{max_sec:.2f}s, got {duration:.2f}s"
|
|
@@ -22715,6 +22715,40 @@ function mergeVoiceScripts(scripts, docs, envDefaultProfile) {
|
|
|
22715
22715
|
}
|
|
22716
22716
|
return result;
|
|
22717
22717
|
}
|
|
22718
|
+
var CONTENT_ECHO_NOISE_RE = /[\uFF0C\u3002\u3001\uFF1A:\uFF1B;!\uFF01?\uFF1F\u2026\s\u300C\u300D\u300E\u300F\u3010\u3011]/g;
|
|
22719
|
+
var CONTENT_SCAFFOLD_LABEL_RE = /^(?:\*\*)?[\u300C\u300E]?(?:\u80CC\u666F|\u6F5C\u5728\u5F71\u54CD|\u53EF\u505A\u89D2\u5EA6|\u4E3A\u4EC0\u4E48\u73B0\u5728\u503C\u5F97\u6CE8\u610F|\u793E\u533A\u8BA8\u8BBA)[\u300D\u300F]?(?:\*\*)?(?:[\uFF1A:\uFF0C,]|\s|$)/;
|
|
22720
|
+
var CONTENT_TAG_TAIL_RE = /\u6807\u7B7E[\uFF1A:\uFF0C,]/;
|
|
22721
|
+
var CONTENT_URL_MANGLE_RE = /[\uFF0C\u3002]\/\//;
|
|
22722
|
+
function assertSegmentContent(script) {
|
|
22723
|
+
const seen = new Map;
|
|
22724
|
+
for (let idx = 0;idx < script.segments.length; idx++) {
|
|
22725
|
+
const text = (script.segments[idx]?.text ?? "").trim();
|
|
22726
|
+
if (text === "")
|
|
22727
|
+
continue;
|
|
22728
|
+
const previous = seen.get(text);
|
|
22729
|
+
if (previous !== undefined) {
|
|
22730
|
+
throw new Error(`content check failed: segment ${idx} duplicates segment ${previous} verbatim`);
|
|
22731
|
+
}
|
|
22732
|
+
seen.set(text, idx);
|
|
22733
|
+
const lead = text.match(/^[^\u3010]*\u3010([^\u3011]{4,80})\u3011[\uFF0C\u3002\u3001\s]*/);
|
|
22734
|
+
if (lead !== null) {
|
|
22735
|
+
const bracketed = (lead[1] ?? "").replace(CONTENT_ECHO_NOISE_RE, "");
|
|
22736
|
+
const rest = text.slice(lead[0].length).replace(CONTENT_ECHO_NOISE_RE, "");
|
|
22737
|
+
if (bracketed.length >= 4 && rest.startsWith(bracketed)) {
|
|
22738
|
+
throw new Error(`content check failed: segment ${idx} repeats its \u3010${lead[1]}\u3011 title in the body text`);
|
|
22739
|
+
}
|
|
22740
|
+
}
|
|
22741
|
+
if (CONTENT_SCAFFOLD_LABEL_RE.test(text)) {
|
|
22742
|
+
throw new Error(`content check failed: segment ${idx} opens with a research scaffold label`);
|
|
22743
|
+
}
|
|
22744
|
+
if (CONTENT_TAG_TAIL_RE.test(text)) {
|
|
22745
|
+
throw new Error(`content check failed: segment ${idx} carries a \u6807\u7B7E metadata tail`);
|
|
22746
|
+
}
|
|
22747
|
+
if (CONTENT_URL_MANGLE_RE.test(text)) {
|
|
22748
|
+
throw new Error(`content check failed: segment ${idx} has a colon-mangled URL`);
|
|
22749
|
+
}
|
|
22750
|
+
}
|
|
22751
|
+
}
|
|
22718
22752
|
function validateVoiceScript(script) {
|
|
22719
22753
|
if (!script || typeof script !== "object") {
|
|
22720
22754
|
throw new Error("VoiceScript must be an object");
|
|
@@ -22791,6 +22825,7 @@ function validateVoiceScript(script) {
|
|
|
22791
22825
|
assertEngine(segment.engine, `Segment at index ${idx}`);
|
|
22792
22826
|
assertLanguage(segment.language, `Segment at index ${idx}`);
|
|
22793
22827
|
}
|
|
22828
|
+
assertSegmentContent(script);
|
|
22794
22829
|
}
|
|
22795
22830
|
|
|
22796
22831
|
// ../../plugins/generations/voice-gen/src/index.ts
|
|
@@ -367,6 +367,57 @@ export function mergeVoiceScripts(scripts: VoiceScript[], docs: Doc[], envDefaul
|
|
|
367
367
|
return result;
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
+
/**
|
|
371
|
+
* Content checks mirrored from `plugins/kk/scripts/validate-voicescript.ts` (0139 R3 SYNC contract).
|
|
372
|
+
* Keep the messages byte-identical to the kk copy — the daily workflow runs whichever copy the
|
|
373
|
+
* package resolves, and both must reject the same spoken-copy defects with the same reason.
|
|
374
|
+
*/
|
|
375
|
+
|
|
376
|
+
/** Punctuation/space the translator drifts between a title and its body restatement. */
|
|
377
|
+
const CONTENT_ECHO_NOISE_RE = /[,。、::;;!!??…\s「」『』【】]/g;
|
|
378
|
+
/** Research scaffold labels the plan-translate stage must not emit as spoken copy. */
|
|
379
|
+
const CONTENT_SCAFFOLD_LABEL_RE =
|
|
380
|
+
/^(?:\*\*)?[「『]?(?:背景|潜在影响|可做角度|为什么现在值得注意|社区讨论)[」』]?(?:\*\*)?(?:[::,,]|\s|$)/;
|
|
381
|
+
/** `标签,` metadata tails — the plan item's tag line spoken aloud. */
|
|
382
|
+
const CONTENT_TAG_TAIL_RE = /标签[::,,]/;
|
|
383
|
+
/** `wss,//` / `https。//` — the translator replacing a URL colon with a Chinese comma. */
|
|
384
|
+
const CONTENT_URL_MANGLE_RE = /[,。]\/\//;
|
|
385
|
+
|
|
386
|
+
function assertSegmentContent(script: VoiceScript): void {
|
|
387
|
+
const seen = new Map<string, number>();
|
|
388
|
+
for (let idx = 0; idx < script.segments.length; idx++) {
|
|
389
|
+
const text = (script.segments[idx]?.text ?? '').trim();
|
|
390
|
+
if (text === '') continue;
|
|
391
|
+
|
|
392
|
+
const previous = seen.get(text);
|
|
393
|
+
if (previous !== undefined) {
|
|
394
|
+
throw new Error(`content check failed: segment ${idx} duplicates segment ${previous} verbatim`);
|
|
395
|
+
}
|
|
396
|
+
seen.set(text, idx);
|
|
397
|
+
|
|
398
|
+
const lead = text.match(/^[^【]*【([^】]{4,80})】[,。、\s]*/);
|
|
399
|
+
if (lead !== null) {
|
|
400
|
+
const bracketed = (lead[1] ?? '').replace(CONTENT_ECHO_NOISE_RE, '');
|
|
401
|
+
const rest = text.slice(lead[0].length).replace(CONTENT_ECHO_NOISE_RE, '');
|
|
402
|
+
if (bracketed.length >= 4 && rest.startsWith(bracketed)) {
|
|
403
|
+
throw new Error(
|
|
404
|
+
`content check failed: segment ${idx} repeats its 【${lead[1]}】 title in the body text`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (CONTENT_SCAFFOLD_LABEL_RE.test(text)) {
|
|
410
|
+
throw new Error(`content check failed: segment ${idx} opens with a research scaffold label`);
|
|
411
|
+
}
|
|
412
|
+
if (CONTENT_TAG_TAIL_RE.test(text)) {
|
|
413
|
+
throw new Error(`content check failed: segment ${idx} carries a 标签 metadata tail`);
|
|
414
|
+
}
|
|
415
|
+
if (CONTENT_URL_MANGLE_RE.test(text)) {
|
|
416
|
+
throw new Error(`content check failed: segment ${idx} has a colon-mangled URL`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
370
421
|
/**
|
|
371
422
|
* Validate that a VoiceScript satisfies all Voicebox and plugin constraints.
|
|
372
423
|
*/
|
|
@@ -499,4 +550,6 @@ export function validateVoiceScript(script: VoiceScript): void {
|
|
|
499
550
|
assertEngine(segment.engine, `Segment at index ${idx}`);
|
|
500
551
|
assertLanguage(segment.language, `Segment at index ${idx}`);
|
|
501
552
|
}
|
|
553
|
+
|
|
554
|
+
assertSegmentContent(script);
|
|
502
555
|
}
|
|
@@ -28,8 +28,8 @@ Parse per the frozen rule:
|
|
|
28
28
|
|
|
29
29
|
- First token is the workflow **name** when it matches `^[a-z0-9][a-z0-9-]*$` and is not `--in`;
|
|
30
30
|
otherwise `name=kk-storm-research` and that token is the topic. Supported profile names:
|
|
31
|
-
`kk-storm-research` (default), `kk-itc`, `kk-solo-podcast`. Any other name after successful copy → exit 1 listing
|
|
32
|
-
`kk-storm-research`, `kk-itc`, `kk-solo-podcast`.
|
|
31
|
+
`kk-storm-research` (default), `kk-itc`, `kk-solo-podcast`, `kk-daily-ai-voice`. Any other name after successful copy → exit 1 listing
|
|
32
|
+
`kk-storm-research`, `kk-itc`, `kk-solo-podcast`, `kk-daily-ai-voice`.
|
|
33
33
|
- `--in <file>` → file mode. `--force` → overwrite an existing dest YAML / existing `brief.md`.
|
|
34
34
|
- Profile `kk-storm-research`: `--dir <path>` (default `$works_dir/kk-storm-research/<topic-id>`),
|
|
35
35
|
`--fixture` → force `fixture: "true"`.
|
|
@@ -40,6 +40,9 @@ Parse per the frozen rule:
|
|
|
40
40
|
- Profile `kk-solo-podcast`: `--dir <path>` (default `$works_dir/kk-solo-podcast/<date>`), `--outline <a|b|c>`
|
|
41
41
|
(default empty), `--duration <min>` (default `8`), `--language <code>` (default `en`),
|
|
42
42
|
`--script-approved` (sets `script_approved=true`; default off). `--fixture` is storm-only.
|
|
43
|
+
- Profile `kk-daily-ai-voice`: `--dir <path>` (default `$works_dir/kk-daily-ai-voice/<date>`),
|
|
44
|
+
`--publish <true|false>` (default `false`), `--last30days <true|false>` (default `false`),
|
|
45
|
+
`--skip-review` (sets `skip_review=true`; default off). `--fixture` is storm-only.
|
|
43
46
|
- **XOR:** exactly one of `<topic>` / `--in <file>`. Both set or both empty → exit 1, stderr
|
|
44
47
|
states the XOR rule, no writes.
|
|
45
48
|
|
|
@@ -241,6 +244,19 @@ spur workflow run "$dest" --vars \
|
|
|
241
244
|
"{\"topic\":\"$TOPIC\",\"input_file\":\"$INPUT_FILE\",\"work_dir\":\"$work_dir\",\"outline\":\"$OUTLINE\",\"script_approved\":\"$SCRIPT_APPROVED\",\"force\":\"$FORCE\",\"target_duration_min\":\"$DURATION\",\"language\":\"$LANGUAGE\",\"voice_profile\":\"$VOICE_PROFILE\",\"validate_script\":\"$vs_arg\",\"wrap_script\":\"$ws_arg\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
|
|
242
245
|
```
|
|
243
246
|
|
|
247
|
+
### Profile `kk-daily-ai-voice`
|
|
248
|
+
|
|
249
|
+
Bind vars: `work_dir`, `publish_enabled`, `last30days_enabled`, `skip_review`, `plugins_path`
|
|
250
|
+
(empty → ADR-012 default discovery), `agent`. `$dest` resolves like solo-podcast. The run is
|
|
251
|
+
topicless — no `topic`/`input_file`/`fixture` vars. `run_date` is computed by the `prepare`
|
|
252
|
+
state and written to `$work_dir/run-date.txt`. The `review` state pauses the run for operator
|
|
253
|
+
approval of the VoiceScript; resume with `spur workflow continue [run-id]`.
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
spur workflow run "$dest" --vars \
|
|
257
|
+
"{\"work_dir\":\"$work_dir\",\"publish_enabled\":\"$PUBLISH\",\"last30days_enabled\":\"$L30D\",\"skip_review\":\"$SKIP_REVIEW\",\"plugins_path\":\"\",\"agent\":\"$AGENT\"}"
|
|
258
|
+
```
|
|
259
|
+
|
|
244
260
|
## 6. Report
|
|
245
261
|
|
|
246
262
|
- **`kk-storm-research`**: print resolved `work_dir`, then `$work_dir/content.md` (report sidecar)
|
|
@@ -251,6 +267,13 @@ spur workflow run "$dest" --vars \
|
|
|
251
267
|
- **`kk-solo-podcast`**: print resolved `work_dir`, then `$work_dir/1-briefing-briefing.md`,
|
|
252
268
|
`$work_dir/3-script-voicescript.yaml`, `$work_dir/4-audio/content.json`, and
|
|
253
269
|
`$work_dir/4-audio/content.wav`.
|
|
270
|
+
- **`kk-daily-ai-voice`**: print resolved `work_dir` (read the date from `$work_dir/run-date.txt`),
|
|
271
|
+
then `$work_dir/2-article/<date>_06_article_article.md`,
|
|
272
|
+
`$work_dir/2-script/<date>_08_script_voicescript.yaml`,
|
|
273
|
+
`$work_dir/3-audio/<date>_11_generate_content.json` (carries the audio path and QC metrics),
|
|
274
|
+
and `$work_dir/<date>_19_run-report_report.md`. With `publish_enabled=true` also print
|
|
275
|
+
`$work_dir/3-publish/<date>_17_publish_publish-status.txt` and the
|
|
276
|
+
`<date>_14_publish-surfdash_result.json` / `<date>_16_publish-podcast_result.json` outcomes.
|
|
254
277
|
|
|
255
278
|
If the run ended `failed`, show the failing state (`spur workflow trace <run-id> --json`) and the
|
|
256
279
|
fail-loud recovery line.
|
package/plugins/kk/plugin.json
CHANGED