@gobing-ai/knowledge-kit 0.0.16 → 0.0.18
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/README.md +74 -0
- package/dist/index.js +46 -100
- package/package.json +19 -1
- package/plugins/generations/content-gen/dist/index.js +42 -94
- package/plugins/generations/core-facts-gen/dist/index.js +51 -100
- package/plugins/generations/daily-article-gen/dist/index.js +98 -100
- package/plugins/generations/daily-article-gen/src/index.ts +80 -6
- package/plugins/generations/dailynews-gen/dist/index.js +144 -130
- package/plugins/generations/dailynews-gen/src/script-builder.ts +141 -33
- package/plugins/generations/episode-plan-gen/dist/index.js +124 -115
- package/plugins/generations/episode-plan-gen/src/index.ts +111 -24
- package/plugins/generations/image-gen/dist/index.js +63 -115
- package/plugins/generations/news-report-gen/dist/index.js +164 -100
- package/plugins/generations/news-report-gen/src/index.ts +199 -7
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +17 -1
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +4 -22
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +8 -0
- package/plugins/generations/voice-gen/dist/index.js +136 -117
- package/plugins/generations/voice-gen/src/concat.ts +34 -0
- package/plugins/generations/voice-gen/src/index.ts +6 -4
- package/plugins/generations/voice-gen/src/voicescript.ts +61 -0
- package/plugins/ingestions/aihot-ingest/dist/index.js +47 -97
- package/plugins/ingestions/horizon-ingest/dist/index.js +47 -96
- package/plugins/ingestions/karakeep-local/dist/index.js +217 -266
- package/plugins/ingestions/last30days-ingest/dist/index.js +42 -91
- package/plugins/ingestions/web-search/dist/index.js +68 -117
- package/plugins/kk/commands/workflow-run.md +25 -2
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +522 -163
- package/plugins/kk/scripts/validate-voicescript.ts +55 -0
- package/plugins/kk/skills/publish/SKILL.md +80 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +125 -120
- package/plugins/kk/workflows/leftover-publish-fanout-example.yaml +53 -0
- package/plugins/publishings/emdash-pub/dist/index.js +42 -91
- package/plugins/publishings/infoq-pub/dist/index.js +21994 -0
- package/plugins/publishings/infoq-pub/package.json +17 -0
- package/plugins/publishings/infoq-pub/plugin.json +7 -0
- package/plugins/publishings/infoq-pub/src/index.ts +123 -0
- package/plugins/publishings/infoq-pub/tsconfig.json +4 -0
- package/plugins/publishings/juejin-pub/dist/index.js +21994 -0
- package/plugins/publishings/juejin-pub/package.json +17 -0
- package/plugins/publishings/juejin-pub/plugin.json +7 -0
- package/plugins/publishings/juejin-pub/src/index.ts +123 -0
- package/plugins/publishings/juejin-pub/tsconfig.json +4 -0
- package/plugins/publishings/medium-pub/dist/index.js +22041 -0
- package/plugins/publishings/medium-pub/package.json +17 -0
- package/plugins/publishings/medium-pub/plugin.json +7 -0
- package/plugins/publishings/medium-pub/src/index.ts +215 -0
- package/plugins/publishings/medium-pub/tsconfig.json +4 -0
- package/plugins/publishings/podcast-pub/dist/index.js +183 -123
- package/plugins/publishings/podcast-pub/src/index.ts +47 -3
- package/plugins/publishings/podcast-pub/src/show-notes.ts +153 -34
- package/plugins/publishings/qiita-pub/dist/index.js +42 -91
- package/plugins/publishings/substack-pub/dist/index.js +21994 -0
- package/plugins/publishings/substack-pub/package.json +17 -0
- package/plugins/publishings/substack-pub/plugin.json +7 -0
- package/plugins/publishings/substack-pub/src/index.ts +123 -0
- package/plugins/publishings/substack-pub/tsconfig.json +4 -0
- package/plugins/publishings/surfdash-pub/dist/index.js +172 -103
- package/plugins/publishings/surfdash-pub/src/index.ts +32 -2
- package/plugins/publishings/wechatmp-pub/dist/index.js +22119 -0
- package/plugins/publishings/wechatmp-pub/package.json +17 -0
- package/plugins/publishings/wechatmp-pub/plugin.json +7 -0
- package/plugins/publishings/wechatmp-pub/src/index.ts +304 -0
- package/plugins/publishings/wechatmp-pub/tsconfig.json +4 -0
- package/plugins/publishings/x-pub/dist/index.js +22110 -0
- package/plugins/publishings/x-pub/package.json +17 -0
- package/plugins/publishings/x-pub/plugin.json +7 -0
- package/plugins/publishings/x-pub/src/index.ts +222 -0
- package/plugins/publishings/x-pub/tsconfig.json +4 -0
- package/plugins/publishings/xhs-pub/dist/index.js +21994 -0
- package/plugins/publishings/xhs-pub/package.json +17 -0
- package/plugins/publishings/xhs-pub/plugin.json +7 -0
- package/plugins/publishings/xhs-pub/src/index.ts +123 -0
- package/plugins/publishings/xhs-pub/tsconfig.json +4 -0
- package/plugins/publishings/zenn-pub/dist/index.js +42 -91
|
@@ -50,6 +50,10 @@ const VALIDATE_SIDECAR = 'validate-voicescript.ts';
|
|
|
50
50
|
const WRAP_SIDECAR = 'wrap-voicescript-doc.ts';
|
|
51
51
|
const RENDER_SIDECAR = 'render-md.ts';
|
|
52
52
|
const REVISE_COUNT = '.revise-count';
|
|
53
|
+
// Post-translation title-dedup threshold (task 0138 R3). ponytail: the 2026-09-16 Gemini 3.8 Live
|
|
54
|
+
// pair measures 0.417 zh title-only Jaccard, the nearest distinct pair 0.1 (4x margin). Character
|
|
55
|
+
// bigrams need same-language input — pre-translate dedup over the mixed-language blend cannot work.
|
|
56
|
+
const DEDUP_TITLE_THRESHOLD = 0.4;
|
|
53
57
|
|
|
54
58
|
function fail(message: string): never {
|
|
55
59
|
console.error(message);
|
|
@@ -496,6 +500,16 @@ function metadataOf(value: Record<string, unknown> | null): Record<string, unkno
|
|
|
496
500
|
return metadata !== null && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : {};
|
|
497
501
|
}
|
|
498
502
|
|
|
503
|
+
/**
|
|
504
|
+
* Required stage input: a missing or unparsable file fails the stage naming the path, so a
|
|
505
|
+
* run-date/path mismatch surfaces as its real cause instead of a downstream "field missing" error.
|
|
506
|
+
*/
|
|
507
|
+
function requireJson(path: string, label: string): Record<string, unknown> {
|
|
508
|
+
const parsed = readJson(path);
|
|
509
|
+
if (parsed === null) failWith(1, `${label}: required input not found or unparsable: ${path}`);
|
|
510
|
+
return parsed;
|
|
511
|
+
}
|
|
512
|
+
|
|
499
513
|
/** `jq -e '.targets[]? | select(.target == t and .ok == true and .status == "published")'`. */
|
|
500
514
|
function targetPublished(path: string, target: string): boolean {
|
|
501
515
|
const parsed = readJson(path);
|
|
@@ -643,6 +657,72 @@ function dailyQcContent(args: string[]): void {
|
|
|
643
657
|
});
|
|
644
658
|
}
|
|
645
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Character-bigram Jaccard over whitespace-normalized, lowercased titles — the twin of
|
|
662
|
+
* episode-plan-gen's `titleSimilarity`. Duplicated on purpose: this file is node-builtins-only
|
|
663
|
+
* (no workspace imports), so it cannot reach that module.
|
|
664
|
+
*/
|
|
665
|
+
function titleSimilarity(a: string, b: string): number {
|
|
666
|
+
const bigrams = (text: string): Set<string> => {
|
|
667
|
+
const normalized = text.replace(/\s+/g, ' ').trim().toLowerCase();
|
|
668
|
+
const out = new Set<string>();
|
|
669
|
+
for (let i = 0; i < normalized.length - 1; i++) out.add(normalized.slice(i, i + 2));
|
|
670
|
+
return out;
|
|
671
|
+
};
|
|
672
|
+
const aGrams = bigrams(a);
|
|
673
|
+
const bGrams = bigrams(b);
|
|
674
|
+
if (aGrams.size === 0 || bGrams.size === 0) return 0;
|
|
675
|
+
let intersection = 0;
|
|
676
|
+
for (const gram of aGrams) if (bGrams.has(gram)) intersection++;
|
|
677
|
+
return intersection / (aGrams.size + bGrams.size - intersection);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Post-translation near-duplicate dedup (task 0138 R3): drop any plan item whose title is a
|
|
682
|
+
* >=DEDUP_TITLE_THRESHOLD bigram match for an EARLIER item, so curated plan order decides the
|
|
683
|
+
* survivor. It runs on the translated plan because cross-language near-dups are invisible before
|
|
684
|
+
* plan-translate (the 2026-09-16 Gemini pair measures 0.208 mixed-language, 0.417 all-zh).
|
|
685
|
+
*/
|
|
686
|
+
export function dedupPlanByTitle<T extends { id?: unknown; title?: unknown }>(
|
|
687
|
+
docs: T[],
|
|
688
|
+
): { kept: T[]; dropped: string[] } {
|
|
689
|
+
const kept: T[] = [];
|
|
690
|
+
const dropped: string[] = [];
|
|
691
|
+
for (const doc of docs) {
|
|
692
|
+
const title = typeof doc.title === 'string' ? doc.title : '';
|
|
693
|
+
const duplicate =
|
|
694
|
+
title !== '' &&
|
|
695
|
+
kept.some((earlier) => {
|
|
696
|
+
const earlierTitle = typeof earlier.title === 'string' ? earlier.title : '';
|
|
697
|
+
return titleSimilarity(earlierTitle, title) >= DEDUP_TITLE_THRESHOLD;
|
|
698
|
+
});
|
|
699
|
+
if (duplicate) {
|
|
700
|
+
dropped.push(typeof doc.id === 'string' && doc.id !== '' ? doc.id : title);
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
kept.push(doc);
|
|
704
|
+
}
|
|
705
|
+
return { kept, dropped };
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Run the VoiceScript content validator over a written script and fail the stage when it rejects
|
|
710
|
+
* (task 0139 R3). `spawnSync` rather than `delegate` on purpose: `delegate` exits the process
|
|
711
|
+
* directly, which would skip `timed`'s `finally` and lose the step-timing entry the run report
|
|
712
|
+
* reads. The sidecar's own stderr carries `content check failed: segment <idx> …`; this adds the
|
|
713
|
+
* stage-level naming so the operator sees which artifact was rejected.
|
|
714
|
+
*/
|
|
715
|
+
function assertScriptContent(scriptPath: string): void {
|
|
716
|
+
// `spawnSync` + `failWith`, deliberately NOT `delegate`: delegate is `never`-returning (it calls
|
|
717
|
+
// process.exit), so using it mid-function would abort the calling stage on SUCCESS — dailyWrapDocs
|
|
718
|
+
// would exit before packaging its docs. A clean exit here falls through to the rest of the stage.
|
|
719
|
+
const sidecar = requireSidecar('', VALIDATE_SIDECAR, 'validate_script');
|
|
720
|
+
const runner = spawnSync(process.execPath, [sidecar, '--in', scriptPath], { stdio: 'inherit' });
|
|
721
|
+
if (runner.status !== 0) {
|
|
722
|
+
failWith(runner.status ?? 1, `script content check failed: ${scriptPath} was rejected (see above)`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
646
726
|
function dailyArticle(args: string[]): void {
|
|
647
727
|
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
648
728
|
const articleMd = join(workDir, '2-article', `${runDate}_06_article_article.md`);
|
|
@@ -650,10 +730,6 @@ function dailyArticle(args: string[]): void {
|
|
|
650
730
|
const planIn = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
651
731
|
const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
652
732
|
timed(workDir, 'article', () => {
|
|
653
|
-
if (isFile(articleMd)) {
|
|
654
|
-
console.log(`article cached: ${articleMd}`);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
733
|
let planUsable = true;
|
|
658
734
|
const raw = isFile(planIn) ? readFileSync(planIn, 'utf-8') : '';
|
|
659
735
|
try {
|
|
@@ -671,6 +747,25 @@ function dailyArticle(args: string[]): void {
|
|
|
671
747
|
}
|
|
672
748
|
}
|
|
673
749
|
if (!planUsable) failWith(1, 'article: no usable plan input');
|
|
750
|
+
// R3 (task 0138): dedup runs BEFORE the cache early-return — the script stage reads this same
|
|
751
|
+
// plan.json and must see one survivor even when the article is already cached. Idempotent:
|
|
752
|
+
// a no-op once the plan is deduped.
|
|
753
|
+
const planParsed: unknown = readJson(planIn);
|
|
754
|
+
if (Array.isArray(planParsed)) {
|
|
755
|
+
const { kept, dropped } = dedupPlanByTitle(planParsed as Record<string, unknown>[]);
|
|
756
|
+
if (dropped.length > 0) {
|
|
757
|
+
sted('write', planIn, () => writeJsonText(planIn, kept));
|
|
758
|
+
console.error(`article: dropped ${dropped.length} near-duplicate plan item(s): ${dropped.join(', ')}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (isFile(articleMd)) {
|
|
762
|
+
console.log(`article cached: ${articleMd}`);
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
// Agent-written prose body (workflow `article-write` state) replaces the mechanical
|
|
766
|
+
// per-item render when present — the 3-format split: surfdash gets an article,
|
|
767
|
+
// not a news list (2026-09-16 feedback).
|
|
768
|
+
const proseBody = join(workDir, '2-article', `${runDate}_06_article-write_body.md`);
|
|
674
769
|
kk(
|
|
675
770
|
[
|
|
676
771
|
'executor',
|
|
@@ -683,7 +778,10 @@ function dailyArticle(args: string[]): void {
|
|
|
683
778
|
...pluginsArgs(pluginsPath),
|
|
684
779
|
],
|
|
685
780
|
{
|
|
686
|
-
env: {
|
|
781
|
+
env: {
|
|
782
|
+
ARTICLE_DATE: runDate,
|
|
783
|
+
...(isFile(proseBody) ? { ARTICLE_BODY_FILE: proseBody } : {}),
|
|
784
|
+
},
|
|
687
785
|
},
|
|
688
786
|
);
|
|
689
787
|
const body = readJson(articleContent)?.body;
|
|
@@ -711,7 +809,10 @@ function dailyCoverNormalize(args: string[]): void {
|
|
|
711
809
|
function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
712
810
|
const png = join(workDir, '2-cover', 'content-cover-01.png');
|
|
713
811
|
const jpg = join(workDir, '2-cover', 'content-cover-01.jpg');
|
|
714
|
-
|
|
812
|
+
// Must match the name dailyPublishPrep and the article->cover workflow guard read:
|
|
813
|
+
// `${runDate}_07_cover_cover.png` (a plain `cover-${runDate}.png` left options.coverPath
|
|
814
|
+
// undefined and silently dropped the cover from both podcast and surfdash — 2026-09-16 run).
|
|
815
|
+
const out = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
715
816
|
const source = isFile(png) ? png : isFile(jpg) ? jpg : '';
|
|
716
817
|
if (source === '') failWith(1, 'cover normalize: no content-cover-01.{png,jpg} artifact');
|
|
717
818
|
if (source === jpg) {
|
|
@@ -734,7 +835,7 @@ function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
|
734
835
|
// `rm -f` is silent when the file is already gone.
|
|
735
836
|
}
|
|
736
837
|
}
|
|
737
|
-
console.log(`cover normalized:
|
|
838
|
+
console.log(`cover normalized: ${runDate}_07_cover_cover.png`);
|
|
738
839
|
}
|
|
739
840
|
|
|
740
841
|
function dailyScript(args: string[]): void {
|
|
@@ -753,6 +854,9 @@ function dailyScript(args: string[]): void {
|
|
|
753
854
|
const body = readJson(contentOut)?.body;
|
|
754
855
|
if (typeof body !== 'string') failWith(1, `script: ${contentOut} has no string body`);
|
|
755
856
|
sted('write', yamlOut, () => writeRaw(yamlOut, body));
|
|
857
|
+
// R3 (task 0139): fresh writes are validated here; the script cache path is covered by
|
|
858
|
+
// wrap-docs, which runs on every path that reaches audio.
|
|
859
|
+
assertScriptContent(yamlOut);
|
|
756
860
|
});
|
|
757
861
|
}
|
|
758
862
|
|
|
@@ -761,6 +865,9 @@ function dailyWrapDocs(args: string[]): void {
|
|
|
761
865
|
const scriptPath = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
|
|
762
866
|
const docsPath = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
|
|
763
867
|
timed(workDir, 'wrap-docs', () => {
|
|
868
|
+
// R3 (task 0139): the second validator placement — covers a HITL-edited YAML and the
|
|
869
|
+
// script-stage cache path, neither of which re-enters dailyScript's fresh-write branch.
|
|
870
|
+
assertScriptContent(scriptPath);
|
|
764
871
|
const url = new URL(scriptPath, `file://${process.cwd()}/`);
|
|
765
872
|
const body = readFileSync(decodeURIComponent(url.pathname), 'utf-8');
|
|
766
873
|
const profile = voiceProfile || process.env.VOICEBOX_DEFAULT_PROFILE || 'robin-news';
|
|
@@ -777,7 +884,17 @@ function dailyWrapDocs(args: string[]): void {
|
|
|
777
884
|
}
|
|
778
885
|
|
|
779
886
|
function dailyGenerate(args: string[]): void {
|
|
780
|
-
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '',
|
|
887
|
+
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '', vdriver = ''] = args;
|
|
888
|
+
// 20260917 review: the workflow picks a voice driver by short name; both drivers' envs are
|
|
889
|
+
// passed unconditionally so switching `vdriver` needs no shell changes. Unknown -> fail loud.
|
|
890
|
+
const VOICE_DRIVERS: Record<string, string> = { ominivoice: 'omni-voice-gen', voicebox: 'voice-gen' };
|
|
891
|
+
const voiceGenerator = VOICE_DRIVERS[vdriver];
|
|
892
|
+
if (!voiceGenerator) {
|
|
893
|
+
failWith(
|
|
894
|
+
1,
|
|
895
|
+
`generate: unknown vdriver '${vdriver}' (expected one of: ${Object.keys(VOICE_DRIVERS).join(', ')})`,
|
|
896
|
+
);
|
|
897
|
+
}
|
|
781
898
|
const docs = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
|
|
782
899
|
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
783
900
|
timed(workDir, 'generate', () => {
|
|
@@ -794,6 +911,111 @@ function dailyGenerate(args: string[]): void {
|
|
|
794
911
|
});
|
|
795
912
|
}
|
|
796
913
|
|
|
914
|
+
/**
|
|
915
|
+
* 20260917 review #7: prepend the show's 8s intro sting to the generated audio.
|
|
916
|
+
* `intro_music` empty -> skip. Writes `<stem>.intro.wav` (original render kept), points
|
|
917
|
+
* `audioPath` at the merged file, extends `duration`, marks `introMerged` (idempotent).
|
|
918
|
+
*/
|
|
919
|
+
function dailyIntro(args: string[]): void {
|
|
920
|
+
const [workDir = '', runDate = '', introSrc = ''] = args;
|
|
921
|
+
if (!introSrc || introSrc.trim() === '') {
|
|
922
|
+
console.log('intro-music: intro_music var empty — skip');
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
const contentPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
926
|
+
timed(workDir, 'intro-music', () => {
|
|
927
|
+
const content = requireJson(contentPath, 'intro-music');
|
|
928
|
+
const meta = metadataOf(content);
|
|
929
|
+
if (meta.introMerged === true) {
|
|
930
|
+
console.log(`intro-music cached: ${contentPath}`);
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
const rawFile = meta.mp3Path ?? meta.audioPath;
|
|
934
|
+
if (typeof rawFile !== 'string' || rawFile === '') {
|
|
935
|
+
failWith(1, 'intro-music: voice content carries no mp3Path/audioPath');
|
|
936
|
+
}
|
|
937
|
+
const introPath = resolve(introSrc);
|
|
938
|
+
if (!existsSync(introPath)) failWith(1, `intro-music: intro file not found: ${introPath}`);
|
|
939
|
+
const mainPath = resolve(rawFile);
|
|
940
|
+
if (!existsSync(mainPath)) failWith(1, `intro-music: generated audio not found: ${mainPath}`);
|
|
941
|
+
const mergedPath = mainPath.replace(/\.wav$/, '.intro.wav');
|
|
942
|
+
const probe = (file: string, entries: string): string =>
|
|
943
|
+
spawnSync('ffprobe', [
|
|
944
|
+
'-v',
|
|
945
|
+
'error',
|
|
946
|
+
'-select_streams',
|
|
947
|
+
'a:0',
|
|
948
|
+
'-show_entries',
|
|
949
|
+
entries,
|
|
950
|
+
'-of',
|
|
951
|
+
'csv=p=0',
|
|
952
|
+
file,
|
|
953
|
+
])
|
|
954
|
+
.stdout?.toString()
|
|
955
|
+
.trim() ?? '';
|
|
956
|
+
const mainDur = Number.parseFloat(probe(mainPath, 'format=duration'));
|
|
957
|
+
const introDur = Number.parseFloat(probe(introPath, 'format=duration'));
|
|
958
|
+
if (!Number.isFinite(mainDur) || !Number.isFinite(introDur)) {
|
|
959
|
+
failWith(1, `intro-music: ffprobe could not read durations (main=${mainDur} intro=${introDur})`);
|
|
960
|
+
}
|
|
961
|
+
const rate = probe(mainPath, 'stream=sample_rate') || '24000'; // match the generated audio's rate; mono s16 both sides
|
|
962
|
+
const norm = (label: string): string =>
|
|
963
|
+
`[${label}:a]aresample=${rate},aformat=sample_fmts=s16:channel_layouts=mono[${label}]`;
|
|
964
|
+
const fc = `${norm('0')};${norm('1')};[0][1]concat=n=2:v=0:a=1[out]`;
|
|
965
|
+
const conv = spawnSync('ffmpeg', [
|
|
966
|
+
'-y',
|
|
967
|
+
'-hide_banner',
|
|
968
|
+
'-loglevel',
|
|
969
|
+
'error',
|
|
970
|
+
'-i',
|
|
971
|
+
introPath,
|
|
972
|
+
'-i',
|
|
973
|
+
mainPath,
|
|
974
|
+
'-filter_complex',
|
|
975
|
+
fc,
|
|
976
|
+
'-map',
|
|
977
|
+
'[out]',
|
|
978
|
+
'-c:a',
|
|
979
|
+
'pcm_s16le',
|
|
980
|
+
mergedPath,
|
|
981
|
+
]);
|
|
982
|
+
if (conv.status !== 0 || !existsSync(mergedPath)) {
|
|
983
|
+
failWith(1, `intro-music: ffmpeg concat failed: ${conv.stderr?.toString().trim()}`);
|
|
984
|
+
}
|
|
985
|
+
if (meta.mp3Path && typeof meta.mp3Path === 'string') {
|
|
986
|
+
// transcode_mp3=true: keep the mp3 path honest by re-transcoding the merged wav.
|
|
987
|
+
const mp3 = spawnSync('ffmpeg', [
|
|
988
|
+
'-y',
|
|
989
|
+
'-hide_banner',
|
|
990
|
+
'-loglevel',
|
|
991
|
+
'error',
|
|
992
|
+
'-i',
|
|
993
|
+
mergedPath,
|
|
994
|
+
'-codec:a',
|
|
995
|
+
'libmp3lame',
|
|
996
|
+
'-qscale:a',
|
|
997
|
+
'2',
|
|
998
|
+
resolve(meta.mp3Path),
|
|
999
|
+
]);
|
|
1000
|
+
if (mp3.status !== 0) failWith(1, `intro-music: mp3 re-transcode failed: ${mp3.stderr?.toString().trim()}`);
|
|
1001
|
+
}
|
|
1002
|
+
const mergedDur = Number.parseFloat(probe(mergedPath, 'format=duration')) || mainDur + introDur;
|
|
1003
|
+
const updated = {
|
|
1004
|
+
...content,
|
|
1005
|
+
metadata: {
|
|
1006
|
+
...meta,
|
|
1007
|
+
audioPath: mergedPath,
|
|
1008
|
+
duration: mergedDur,
|
|
1009
|
+
introMerged: true,
|
|
1010
|
+
},
|
|
1011
|
+
};
|
|
1012
|
+
sted('write', contentPath, () => writeJsonText(contentPath, updated));
|
|
1013
|
+
console.log(
|
|
1014
|
+
`intro-music: merged ${introDur.toFixed(1)}s intro -> ${mergedPath} (${mergedDur.toFixed(1)}s total)`,
|
|
1015
|
+
);
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
797
1019
|
function dailyQualityReport(args: string[]): void {
|
|
798
1020
|
const [workDir = '', runDate = ''] = args;
|
|
799
1021
|
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
@@ -817,7 +1039,7 @@ function dailyPublishPrep(args: string[]): void {
|
|
|
817
1039
|
const prepPath = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
818
1040
|
const coverPath = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
819
1041
|
timed(workDir, 'publish-prep', () => {
|
|
820
|
-
const article =
|
|
1042
|
+
const article = requireJson(articlePath, 'publish-prep');
|
|
821
1043
|
const audioMeta = metadataOf(readJson(audioPath));
|
|
822
1044
|
const rawFile = audioMeta.mp3Path ?? audioMeta.audioPath;
|
|
823
1045
|
if (typeof rawFile !== 'string' || rawFile === '') {
|
|
@@ -825,13 +1047,21 @@ function dailyPublishPrep(args: string[]): void {
|
|
|
825
1047
|
}
|
|
826
1048
|
const audioFile = resolve(rawFile);
|
|
827
1049
|
const resolvedCover = resolve(coverPath);
|
|
1050
|
+
const hasCover = existsSync(resolvedCover);
|
|
1051
|
+
// podcast-pub's documented contract reads metadata.coverImage (map.ts); options.coverPath
|
|
1052
|
+
// alone was silently dropped by the mapper — episodes published without a cover.
|
|
828
1053
|
const merged = {
|
|
829
1054
|
...article,
|
|
830
|
-
metadata: {
|
|
1055
|
+
metadata: {
|
|
1056
|
+
...metadataOf(article),
|
|
1057
|
+
audioFile,
|
|
1058
|
+
runDate,
|
|
1059
|
+
...(hasCover ? { coverImage: resolvedCover } : {}),
|
|
1060
|
+
},
|
|
831
1061
|
options: {
|
|
832
1062
|
audioFile,
|
|
833
1063
|
durationSec: audioMeta.duration,
|
|
834
|
-
coverPath:
|
|
1064
|
+
coverPath: hasCover ? resolvedCover : undefined,
|
|
835
1065
|
},
|
|
836
1066
|
};
|
|
837
1067
|
sted('write', prepPath, () => writeJsonText(prepPath, merged));
|
|
@@ -877,6 +1107,9 @@ function dailyShowNotes(args: string[]): void {
|
|
|
877
1107
|
const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
878
1108
|
const out = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
879
1109
|
const plan = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
1110
|
+
// Generated-episode content carries `metadata.segments[]` — the source of real
|
|
1111
|
+
// audio offsets for the show-notes timeline (jump-to-item timestamps).
|
|
1112
|
+
const audioContent = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
880
1113
|
timed(workDir, 'show-notes', () => {
|
|
881
1114
|
const effective = readJson(article)?.title;
|
|
882
1115
|
const titleArg = title !== '' ? title : typeof effective === 'string' ? effective : '';
|
|
@@ -887,6 +1120,7 @@ function dailyShowNotes(args: string[]): void {
|
|
|
887
1120
|
PODCAST_PUB_RUN_DATE: runDate,
|
|
888
1121
|
PODCAST_PUB_BRIEF_MODE: 'true',
|
|
889
1122
|
PODCAST_PUB_ARTICLE_POST_PATH: postPath,
|
|
1123
|
+
...(isFile(audioContent) ? { PODCAST_PUB_AUDIO_CONTENT: audioContent } : {}),
|
|
890
1124
|
},
|
|
891
1125
|
});
|
|
892
1126
|
});
|
|
@@ -917,7 +1151,7 @@ function dailyPublishPodcast(args: string[]): void {
|
|
|
917
1151
|
return;
|
|
918
1152
|
}
|
|
919
1153
|
try {
|
|
920
|
-
const content =
|
|
1154
|
+
const content = requireJson(prep, 'publish-podcast');
|
|
921
1155
|
const showNotes = readFileSync(notes, 'utf-8');
|
|
922
1156
|
const merged = { ...content, metadata: { ...metadataOf(content), showNotes } };
|
|
923
1157
|
sted('write', prep, () => writeJsonText(prep, merged));
|
|
@@ -997,6 +1231,95 @@ function dailyPublishPartial(args: string[]): void {
|
|
|
997
1231
|
});
|
|
998
1232
|
}
|
|
999
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* 20260917 review #4/#6: after both publishes land, patch the surfdash disk posts —
|
|
1236
|
+
* reuse the podcast-hosted cover URL in frontmatter (image/og_image; no re-upload; fallback:
|
|
1237
|
+
* copy the local cover into assets/) and append a podcast cross-link at the end of zh/en/ja.
|
|
1238
|
+
* Idempotent per post; commits + pushes the surfdash checkout (SURFDASH_AUTO_PUSH=0 keeps commit-only).
|
|
1239
|
+
*/
|
|
1240
|
+
function dailyPatchSurfdash(args: string[]): void {
|
|
1241
|
+
const [workDir = '', runDate = '', surfdashRoot = ''] = args;
|
|
1242
|
+
if (!surfdashRoot || !existsSync(surfdashRoot)) {
|
|
1243
|
+
console.log(`patch-surfdash: surfdash root '${surfdashRoot}' not found — skip`);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
timed(workDir, 'patch-surfdash', () => {
|
|
1247
|
+
const sdResult = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
1248
|
+
const podcastResult = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
|
|
1249
|
+
if (!isFile(sdResult) || !isFile(podcastResult)) {
|
|
1250
|
+
failWith(1, 'patch-surfdash: missing surfdash/podcast publish results — run the publish states first');
|
|
1251
|
+
}
|
|
1252
|
+
const targetOf = (path: string): Record<string, unknown> => {
|
|
1253
|
+
const parsed = readJson(path) as { targets?: Array<Record<string, unknown>> };
|
|
1254
|
+
return parsed.targets?.[0] ?? {};
|
|
1255
|
+
};
|
|
1256
|
+
const sd = targetOf(sdResult);
|
|
1257
|
+
const pod = targetOf(podcastResult);
|
|
1258
|
+
const postPath = (sd.metadata as Record<string, unknown> | undefined)?.postPath;
|
|
1259
|
+
if (typeof postPath !== 'string' || postPath === '')
|
|
1260
|
+
failWith(1, 'patch-surfdash: surfdash result carries no postPath');
|
|
1261
|
+
const slugDir = dirname(postPath);
|
|
1262
|
+
const showNotesUrl = typeof pod.url === 'string' ? pod.url : '';
|
|
1263
|
+
const coverUrl = ((pod.metadata as Record<string, unknown> | undefined)?.imageUrl as string | undefined) ?? '';
|
|
1264
|
+
const localCover = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
1265
|
+
const dateIso = `${runDate.slice(0, 4)}-${runDate.slice(4, 6)}-${runDate.slice(6, 8)}`;
|
|
1266
|
+
const crossLink: Record<string, string> = {
|
|
1267
|
+
zh: `🎧 本期已同步制作为播客节目,[点击收听《每日 AI 资讯 · ${dateIso}》](${showNotesUrl})。`,
|
|
1268
|
+
en: `🎧 This episode is also available as a podcast: [listen to the Daily AI Briefing · ${dateIso}](${showNotesUrl}).`,
|
|
1269
|
+
ja: `🎧 本エピソードはポッドキャストでも配信中:[Daily AI Briefing · ${dateIso} を聴く](${showNotesUrl})。`,
|
|
1270
|
+
};
|
|
1271
|
+
let changed = 0;
|
|
1272
|
+
for (const [locale, dir] of [
|
|
1273
|
+
['zh', slugDir],
|
|
1274
|
+
['en', join(dirname(slugDir), 'en', basename(slugDir))],
|
|
1275
|
+
['ja', join(dirname(slugDir), 'ja', basename(slugDir))],
|
|
1276
|
+
] as const) {
|
|
1277
|
+
const indexMd = join(dir, 'index.md');
|
|
1278
|
+
if (!existsSync(indexMd)) {
|
|
1279
|
+
console.log(`patch-surfdash: ${locale} post not published (${indexMd}) — skip`);
|
|
1280
|
+
continue;
|
|
1281
|
+
}
|
|
1282
|
+
let md = readFileSync(indexMd, 'utf-8');
|
|
1283
|
+
const fmEnd = md.indexOf('---', 3); // frontmatter is `---\n ... \n---\n`
|
|
1284
|
+
if (fmEnd < 0) failWith(1, `patch-surfdash: no frontmatter in ${indexMd}`);
|
|
1285
|
+
if (!/^image:/m.test(md.slice(0, fmEnd))) {
|
|
1286
|
+
let image = coverUrl;
|
|
1287
|
+
if (image === '' && existsSync(localCover)) {
|
|
1288
|
+
// Fallback for pre-#6 runs: copy the local cover instead of a hosted URL.
|
|
1289
|
+
const assets = join(dir, 'assets');
|
|
1290
|
+
mkdirSync(assets, { recursive: true });
|
|
1291
|
+
copyFileSync(localCover, join(assets, 'cover.png'));
|
|
1292
|
+
image = './assets/cover.png';
|
|
1293
|
+
}
|
|
1294
|
+
if (image !== '') {
|
|
1295
|
+
md = md.replace(/^publishDate: .*$/m, (m0) => `${m0}\nimage: '${image}'\nog_image: '${image}'`);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (showNotesUrl !== '' && !md.includes(showNotesUrl)) {
|
|
1299
|
+
md = `${md.replace(/\s*$/, '')}\n\n${crossLink[locale]}\n`;
|
|
1300
|
+
}
|
|
1301
|
+
if (md !== readFileSync(indexMd, 'utf-8')) {
|
|
1302
|
+
sted('write', indexMd, () => writeFileSync(indexMd, md));
|
|
1303
|
+
changed++;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
if (changed === 0) {
|
|
1307
|
+
console.log('patch-surfdash: nothing to change');
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
const git = (...a: string[]): void => {
|
|
1311
|
+
const r = spawnSync('git', ['-C', surfdashRoot, ...a]);
|
|
1312
|
+
if (r.status !== 0) failWith(1, `patch-surfdash: git ${a[0]} failed: ${r.stderr?.toString().trim()}`);
|
|
1313
|
+
};
|
|
1314
|
+
git('add', 'content/posts/articles');
|
|
1315
|
+
git('commit', '-m', `feat: cover + podcast cross-link for ${basename(slugDir)} (kk daily patch)`);
|
|
1316
|
+
if (process.env.SURFDASH_AUTO_PUSH !== '0') git('push');
|
|
1317
|
+
console.log(
|
|
1318
|
+
`patch-surfdash: patched ${changed} post(s), committed${process.env.SURFDASH_AUTO_PUSH !== '0' ? ' + pushed' : ' (push skipped: SURFDASH_AUTO_PUSH=0)'}`,
|
|
1319
|
+
);
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1000
1323
|
function dailyRunReport(args: string[]): void {
|
|
1001
1324
|
const [
|
|
1002
1325
|
workDir = '',
|
|
@@ -1021,6 +1344,9 @@ function dailyRunReport(args: string[]): void {
|
|
|
1021
1344
|
return;
|
|
1022
1345
|
}
|
|
1023
1346
|
sted('write', cands, () => writeJqText(cands, metadataOf(readJson(candidates)).docs ?? []));
|
|
1347
|
+
// Plugin env paths must be absolute: spawnPluginEntry runs plugins with cwd = the
|
|
1348
|
+
// plugin package dir, so a relative workDir silently misses every best-effort read.
|
|
1349
|
+
const absWorkDir = resolve(workDir);
|
|
1024
1350
|
kk(['executor', 'run', 'news-report-gen', '--in', cands, '--out', reportJson, ...pluginsArgs(pluginsPath)], {
|
|
1025
1351
|
env: {
|
|
1026
1352
|
QC_MIN_QUALITY: minQuality,
|
|
@@ -1028,11 +1354,19 @@ function dailyRunReport(args: string[]): void {
|
|
|
1028
1354
|
QC_MIN_URGENCY: minUrgency,
|
|
1029
1355
|
QC_MIN_IMPACT: minImpact,
|
|
1030
1356
|
NEWS_REPORT_REJECTED_FILE: join(
|
|
1031
|
-
|
|
1357
|
+
absWorkDir,
|
|
1032
1358
|
'2-plan',
|
|
1033
1359
|
`${runDate}_04_quality-control-content_candidates.rejected.json`,
|
|
1034
1360
|
),
|
|
1035
|
-
NEWS_REPORT_TIMING_FILE: join(
|
|
1361
|
+
NEWS_REPORT_TIMING_FILE: join(absWorkDir, 'step-timing.json'),
|
|
1362
|
+
// 0139 R5b: the generate stage's QC audit, read best-effort (absent -> section omitted).
|
|
1363
|
+
NEWS_REPORT_QC_FILE: join(absWorkDir, '3-audio', `${runDate}_11_generate_content.json`),
|
|
1364
|
+
// 20260917 funnel: the plan stage's full selection trail (every fetched title +
|
|
1365
|
+
// scores + disposition), rendered as the 选稿漏斗 section (absent -> omitted).
|
|
1366
|
+
NEWS_REPORT_PLAN_AUDIT_FILE: join(absWorkDir, '2-plan', `${runDate}_03_plan_plan.json.audit.json`),
|
|
1367
|
+
// 20260917 funnel: the blended fan-in input, rendered as the 语料构成
|
|
1368
|
+
// source/category composition (absent -> omitted).
|
|
1369
|
+
NEWS_REPORT_BLENDED_FILE: join(absWorkDir, '1-ingest', `${runDate}_02_collect_blended.json`),
|
|
1036
1370
|
NEWS_REPORT_DATE: runDate,
|
|
1037
1371
|
},
|
|
1038
1372
|
});
|
|
@@ -1043,68 +1377,78 @@ function dailyRunReport(args: string[]): void {
|
|
|
1043
1377
|
});
|
|
1044
1378
|
}
|
|
1045
1379
|
|
|
1046
|
-
|
|
1380
|
+
/**
|
|
1381
|
+
* Collapsed translate scaffolding (2026-09-16 feedback): surfdash-pub owns the
|
|
1382
|
+
* deterministic mechanics — `scaffold-locale` for both target locales — while the
|
|
1383
|
+
* workflow `translate` state's agent step owns the LLM translation of the drafts.
|
|
1384
|
+
* Writes one marker file per locale (zh post path + en/ja draft paths); empty
|
|
1385
|
+
* marker = locale absent (callers skip). Reuses an existing on-disk draft as the
|
|
1386
|
+
* cache across run retries — re-scaffolding would erase the agent's translation.
|
|
1387
|
+
*/
|
|
1388
|
+
function dailyTranslateLocalize(args: string[]): void {
|
|
1047
1389
|
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
1048
1390
|
const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
|
|
1049
|
-
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-
|
|
1050
|
-
|
|
1051
|
-
const scaffold = join(workDir, '3-publish', `${runDate}_20_translate-en_scaffold-en.json`);
|
|
1052
|
-
const scaffoldResult = join(workDir, '3-publish', `${runDate}_20_translate-en_scaffold-en-result.json`);
|
|
1053
|
-
timed(workDir, 'translate-en', () => {
|
|
1391
|
+
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_zh-post-path.txt`);
|
|
1392
|
+
timed(workDir, 'translate-localize', () => {
|
|
1054
1393
|
const postPath = firstPostPath(readJson(result), 'surfdash-pub');
|
|
1055
1394
|
const zh = postPath.endsWith('index.md') ? postPath : '';
|
|
1056
1395
|
sted('write', zhFile, () => writeRaw(zhFile, zh));
|
|
1057
|
-
const en = zh.replace('/zh/', '/en/');
|
|
1058
|
-
sted('write', enFile, () => writeRaw(enFile, en));
|
|
1059
|
-
if (zh === '') {
|
|
1060
|
-
console.log('no zh post path — skip translate-en');
|
|
1061
|
-
return;
|
|
1062
|
-
}
|
|
1063
|
-
if (isFile(en)) {
|
|
1064
|
-
console.log(`en draft cached: ${en}`);
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
sted('write', scaffold, () =>
|
|
1068
|
-
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target: 'en' } }),
|
|
1069
|
-
);
|
|
1070
|
-
kk(['executor', 'run', 'surfdash-pub', '--in', scaffold, '--out', scaffoldResult, ...pluginsArgs(pluginsPath)]);
|
|
1071
|
-
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1072
|
-
sted('write', enFile, () => writeRaw(enFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function dailyTranslateJa(args: string[]): void {
|
|
1077
|
-
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
1078
|
-
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-en_zh-post-path.txt`);
|
|
1079
|
-
const jaFile = join(workDir, '3-publish', `${runDate}_22_translate-ja_ja-post-path.txt`);
|
|
1080
|
-
const scaffold = join(workDir, '3-publish', `${runDate}_22_translate-ja_scaffold-ja.json`);
|
|
1081
|
-
const scaffoldResult = join(workDir, '3-publish', `${runDate}_22_translate-ja_scaffold-ja-result.json`);
|
|
1082
|
-
timed(workDir, 'translate-ja', () => {
|
|
1083
|
-
const zh = isFile(zhFile) ? readFileSync(zhFile, 'utf-8') : '';
|
|
1084
|
-
const ja = zh.replace('/zh/', '/ja/');
|
|
1085
|
-
sted('write', jaFile, () => writeRaw(jaFile, ja));
|
|
1086
1396
|
if (zh === '') {
|
|
1087
|
-
|
|
1397
|
+
for (const target of ['en', 'ja'] as const) {
|
|
1398
|
+
sted(
|
|
1399
|
+
'write',
|
|
1400
|
+
join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
|
|
1401
|
+
() =>
|
|
1402
|
+
writeRaw(
|
|
1403
|
+
join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`),
|
|
1404
|
+
'',
|
|
1405
|
+
),
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
console.log('no zh post path — skip localize');
|
|
1088
1409
|
return;
|
|
1089
1410
|
}
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1411
|
+
for (const target of ['en', 'ja'] as const) {
|
|
1412
|
+
const markerFile = join(workDir, '3-publish', `${runDate}_20_translate-localize_${target}-post-path.txt`);
|
|
1413
|
+
const existing = zh.replace('/zh/', `/${target}/`);
|
|
1414
|
+
if (isFile(existing)) {
|
|
1415
|
+
sted('write', markerFile, () => writeRaw(markerFile, existing));
|
|
1416
|
+
console.log(`${target} draft cached: ${existing}`);
|
|
1417
|
+
continue;
|
|
1418
|
+
}
|
|
1419
|
+
const scaffold = join(workDir, '3-publish', `${runDate}_20_translate-localize_scaffold-${target}.json`);
|
|
1420
|
+
const scaffoldResult = join(
|
|
1421
|
+
workDir,
|
|
1422
|
+
'3-publish',
|
|
1423
|
+
`${runDate}_20_translate-localize_scaffold-${target}-result.json`,
|
|
1424
|
+
);
|
|
1425
|
+
sted('write', scaffold, () =>
|
|
1426
|
+
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target } }),
|
|
1427
|
+
);
|
|
1428
|
+
kk([
|
|
1429
|
+
'executor',
|
|
1430
|
+
'run',
|
|
1431
|
+
'surfdash-pub',
|
|
1432
|
+
'--in',
|
|
1433
|
+
scaffold,
|
|
1434
|
+
'--out',
|
|
1435
|
+
scaffoldResult,
|
|
1436
|
+
...pluginsArgs(pluginsPath),
|
|
1437
|
+
]);
|
|
1438
|
+
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1439
|
+
sted('write', markerFile, () => writeRaw(markerFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1093
1440
|
}
|
|
1094
|
-
sted('write', scaffold, () =>
|
|
1095
|
-
writeJqText(scaffold, { body: '', options: { operation: 'scaffold-locale', postPath: zh, target: 'ja' } }),
|
|
1096
|
-
);
|
|
1097
|
-
kk(['executor', 'run', 'surfdash-pub', '--in', scaffold, '--out', scaffoldResult, ...pluginsArgs(pluginsPath)]);
|
|
1098
|
-
const draftPath = metadataOf(readJson(scaffoldResult)).draftPath;
|
|
1099
|
-
sted('write', jaFile, () => writeRaw(jaFile, typeof draftPath === 'string' ? draftPath : ''));
|
|
1100
1441
|
});
|
|
1101
1442
|
}
|
|
1102
1443
|
|
|
1103
1444
|
function dailyTranslateSync(args: string[]): void {
|
|
1104
|
-
const [workDir = '', runDate = '', pluginsPath = '',
|
|
1445
|
+
const [workDir = '', runDate = '', pluginsPath = '', zhPathFile = ''] = args;
|
|
1105
1446
|
const sync = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync.json`);
|
|
1106
1447
|
const syncResult = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync-result.json`);
|
|
1107
1448
|
timed(workDir, 'translate-sync', () => {
|
|
1449
|
+
// Reads the marker file itself — the collapsed `translate` state has no
|
|
1450
|
+
// cross-state var to hand the path through.
|
|
1451
|
+
const zhPostPath = isFile(zhPathFile) ? readFileSync(zhPathFile, 'utf-8') : '';
|
|
1108
1452
|
if (zhPostPath === '') {
|
|
1109
1453
|
console.log('no zh post path — skip translate-sync');
|
|
1110
1454
|
return;
|
|
@@ -1118,109 +1462,124 @@ function dailyTranslateSync(args: string[]): void {
|
|
|
1118
1462
|
|
|
1119
1463
|
const [stage, ...rest] = process.argv.slice(2);
|
|
1120
1464
|
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
in: { type: 'string' },
|
|
1143
|
-
out: { type: 'string' },
|
|
1144
|
-
profile: { type: 'string' },
|
|
1145
|
-
});
|
|
1146
|
-
if (!values.in || !values.out) {
|
|
1147
|
-
fail(
|
|
1148
|
-
'usage: kk-workflow-stages.ts wrap [<override>] --in <voicescript.yaml> --out <docs.json> [--profile <name>]',
|
|
1149
|
-
);
|
|
1465
|
+
// Imported as a module (unit tests reach `dedupPlanByTitle`) → exports only, never a stage run;
|
|
1466
|
+
// executed as the entrypoint → dispatch.
|
|
1467
|
+
if (import.meta.main)
|
|
1468
|
+
switch (stage) {
|
|
1469
|
+
case 'prepare-itc':
|
|
1470
|
+
prepareItc(rest);
|
|
1471
|
+
break;
|
|
1472
|
+
case 'prepare-solo':
|
|
1473
|
+
prepareSolo(rest);
|
|
1474
|
+
break;
|
|
1475
|
+
case 'prepare-storm':
|
|
1476
|
+
prepareStorm(rest);
|
|
1477
|
+
break;
|
|
1478
|
+
case 'duration':
|
|
1479
|
+
duration(rest);
|
|
1480
|
+
break;
|
|
1481
|
+
case 'validate': {
|
|
1482
|
+
const { override, values } = parseStage(rest, { in: { type: 'string' } });
|
|
1483
|
+
if (!values.in) fail('usage: kk-workflow-stages.ts validate [<override>] --in <voicescript.yaml>');
|
|
1484
|
+
delegate(requireSidecar(override, VALIDATE_SIDECAR, 'validate_script'), ['--in', values.in]);
|
|
1485
|
+
break;
|
|
1150
1486
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1487
|
+
case 'wrap': {
|
|
1488
|
+
const { override, values } = parseStage(rest, {
|
|
1489
|
+
in: { type: 'string' },
|
|
1490
|
+
out: { type: 'string' },
|
|
1491
|
+
profile: { type: 'string' },
|
|
1492
|
+
});
|
|
1493
|
+
if (!values.in || !values.out) {
|
|
1494
|
+
fail(
|
|
1495
|
+
'usage: kk-workflow-stages.ts wrap [<override>] --in <voicescript.yaml> --out <docs.json> [--profile <name>]',
|
|
1496
|
+
);
|
|
1497
|
+
}
|
|
1498
|
+
delegate(requireSidecar(override, WRAP_SIDECAR, 'wrap_script'), [
|
|
1499
|
+
values.in,
|
|
1500
|
+
values.out,
|
|
1501
|
+
values.profile ?? '',
|
|
1502
|
+
]);
|
|
1503
|
+
break;
|
|
1504
|
+
}
|
|
1505
|
+
case 'render-storm': {
|
|
1506
|
+
const { override, values } = parseStage(rest, { in: { type: 'string' }, out: { type: 'string' } });
|
|
1507
|
+
if (!values.in || !values.out) {
|
|
1508
|
+
fail('usage: kk-workflow-stages.ts render-storm [<override>] --in <content.json> --out <content.md>');
|
|
1509
|
+
}
|
|
1510
|
+
delegate(requireSidecar(override, RENDER_SIDECAR, 'render_script'), [
|
|
1511
|
+
'--in',
|
|
1512
|
+
values.in,
|
|
1513
|
+
'--out',
|
|
1514
|
+
values.out,
|
|
1515
|
+
]);
|
|
1516
|
+
break;
|
|
1158
1517
|
}
|
|
1159
|
-
|
|
1160
|
-
|
|
1518
|
+
case 'daily-prepare':
|
|
1519
|
+
dailyPrepare(rest);
|
|
1520
|
+
break;
|
|
1521
|
+
case 'daily-collect-facts':
|
|
1522
|
+
dailyCollectFacts(rest);
|
|
1523
|
+
break;
|
|
1524
|
+
case 'daily-plan':
|
|
1525
|
+
dailyPlan(rest);
|
|
1526
|
+
break;
|
|
1527
|
+
case 'daily-qc-content':
|
|
1528
|
+
dailyQcContent(rest);
|
|
1529
|
+
break;
|
|
1530
|
+
case 'daily-article':
|
|
1531
|
+
dailyArticle(rest);
|
|
1532
|
+
break;
|
|
1533
|
+
case 'daily-cover-normalize':
|
|
1534
|
+
dailyCoverNormalize(rest);
|
|
1535
|
+
break;
|
|
1536
|
+
case 'daily-script':
|
|
1537
|
+
dailyScript(rest);
|
|
1538
|
+
break;
|
|
1539
|
+
case 'daily-wrap-docs':
|
|
1540
|
+
dailyWrapDocs(rest);
|
|
1541
|
+
break;
|
|
1542
|
+
case 'daily-generate':
|
|
1543
|
+
dailyGenerate(rest);
|
|
1544
|
+
break;
|
|
1545
|
+
case 'daily-intro':
|
|
1546
|
+
dailyIntro(rest);
|
|
1547
|
+
break;
|
|
1548
|
+
case 'daily-quality-report':
|
|
1549
|
+
dailyQualityReport(rest);
|
|
1550
|
+
break;
|
|
1551
|
+
case 'daily-publish-prep':
|
|
1552
|
+
dailyPublishPrep(rest);
|
|
1553
|
+
break;
|
|
1554
|
+
case 'daily-publish-surfdash':
|
|
1555
|
+
dailyPublishSurfdash(rest);
|
|
1556
|
+
break;
|
|
1557
|
+
case 'daily-show-notes':
|
|
1558
|
+
dailyShowNotes(rest);
|
|
1559
|
+
break;
|
|
1560
|
+
case 'daily-publish-podcast':
|
|
1561
|
+
dailyPublishPodcast(rest);
|
|
1562
|
+
break;
|
|
1563
|
+
case 'daily-publish-classify':
|
|
1564
|
+
dailyPublishClassify(rest);
|
|
1565
|
+
break;
|
|
1566
|
+
case 'daily-patch-surfdash':
|
|
1567
|
+
dailyPatchSurfdash(rest);
|
|
1568
|
+
break;
|
|
1569
|
+
case 'daily-publish-partial':
|
|
1570
|
+
dailyPublishPartial(rest);
|
|
1571
|
+
break;
|
|
1572
|
+
case 'daily-run-report':
|
|
1573
|
+
dailyRunReport(rest);
|
|
1574
|
+
break;
|
|
1575
|
+
case 'daily-translate-localize':
|
|
1576
|
+
dailyTranslateLocalize(rest);
|
|
1577
|
+
break;
|
|
1578
|
+
case 'daily-translate-sync':
|
|
1579
|
+
dailyTranslateSync(rest);
|
|
1580
|
+
break;
|
|
1581
|
+
default:
|
|
1582
|
+
fail(
|
|
1583
|
+
`unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|daily-*)`,
|
|
1584
|
+
);
|
|
1161
1585
|
}
|
|
1162
|
-
case 'daily-prepare':
|
|
1163
|
-
dailyPrepare(rest);
|
|
1164
|
-
break;
|
|
1165
|
-
case 'daily-collect-facts':
|
|
1166
|
-
dailyCollectFacts(rest);
|
|
1167
|
-
break;
|
|
1168
|
-
case 'daily-plan':
|
|
1169
|
-
dailyPlan(rest);
|
|
1170
|
-
break;
|
|
1171
|
-
case 'daily-qc-content':
|
|
1172
|
-
dailyQcContent(rest);
|
|
1173
|
-
break;
|
|
1174
|
-
case 'daily-article':
|
|
1175
|
-
dailyArticle(rest);
|
|
1176
|
-
break;
|
|
1177
|
-
case 'daily-cover-normalize':
|
|
1178
|
-
dailyCoverNormalize(rest);
|
|
1179
|
-
break;
|
|
1180
|
-
case 'daily-script':
|
|
1181
|
-
dailyScript(rest);
|
|
1182
|
-
break;
|
|
1183
|
-
case 'daily-wrap-docs':
|
|
1184
|
-
dailyWrapDocs(rest);
|
|
1185
|
-
break;
|
|
1186
|
-
case 'daily-generate':
|
|
1187
|
-
dailyGenerate(rest);
|
|
1188
|
-
break;
|
|
1189
|
-
case 'daily-quality-report':
|
|
1190
|
-
dailyQualityReport(rest);
|
|
1191
|
-
break;
|
|
1192
|
-
case 'daily-publish-prep':
|
|
1193
|
-
dailyPublishPrep(rest);
|
|
1194
|
-
break;
|
|
1195
|
-
case 'daily-publish-surfdash':
|
|
1196
|
-
dailyPublishSurfdash(rest);
|
|
1197
|
-
break;
|
|
1198
|
-
case 'daily-show-notes':
|
|
1199
|
-
dailyShowNotes(rest);
|
|
1200
|
-
break;
|
|
1201
|
-
case 'daily-publish-podcast':
|
|
1202
|
-
dailyPublishPodcast(rest);
|
|
1203
|
-
break;
|
|
1204
|
-
case 'daily-publish-classify':
|
|
1205
|
-
dailyPublishClassify(rest);
|
|
1206
|
-
break;
|
|
1207
|
-
case 'daily-publish-partial':
|
|
1208
|
-
dailyPublishPartial(rest);
|
|
1209
|
-
break;
|
|
1210
|
-
case 'daily-run-report':
|
|
1211
|
-
dailyRunReport(rest);
|
|
1212
|
-
break;
|
|
1213
|
-
case 'daily-translate-en':
|
|
1214
|
-
dailyTranslateEn(rest);
|
|
1215
|
-
break;
|
|
1216
|
-
case 'daily-translate-ja':
|
|
1217
|
-
dailyTranslateJa(rest);
|
|
1218
|
-
break;
|
|
1219
|
-
case 'daily-translate-sync':
|
|
1220
|
-
dailyTranslateSync(rest);
|
|
1221
|
-
break;
|
|
1222
|
-
default:
|
|
1223
|
-
fail(
|
|
1224
|
-
`unknown stage: ${stage ?? '(none)'} (expected prepare-itc|prepare-solo|prepare-storm|validate|wrap|duration|render-storm|daily-*)`,
|
|
1225
|
-
);
|
|
1226
|
-
}
|