@gobing-ai/knowledge-kit 0.0.14 → 0.0.15
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/omni-voice-gen/README.md +9 -3
- package/plugins/generations/omni-voice-gen/profiles.json +5 -1
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +12 -1
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +18 -2
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +8 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +0 -12
- package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
- package/plugins/kk/commands/workflow-run.md +54 -22
- package/plugins/kk/config.example.yaml +5 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +1226 -0
- package/plugins/kk/{workflows → scripts}/validate-voicescript.ts +4 -4
- package/plugins/kk/{workflows → scripts}/wrap-voicescript-doc.ts +7 -6
- package/plugins/kk/skills/audio-authoring/SKILL.md +11 -12
- package/plugins/kk/skills/itc-generating/SKILL.md +2 -2
- package/plugins/kk/skills/itc-generating/references/generic-craft.md +1 -1
- package/plugins/kk/skills/itc-generating/references/platform-english.md +1 -1
- package/plugins/kk/skills/itc-generating/references/platform-wechat.md +1 -1
- package/plugins/kk/skills/storm-research/SKILL.md +5 -5
- package/plugins/kk/skills/topic/SKILL.md +7 -7
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +149 -163
- package/plugins/kk/workflows/kk-itc.yaml +12 -29
- package/plugins/kk/workflows/kk-solo-podcast.yaml +72 -131
- package/plugins/kk/workflows/kk-storm-research.yaml +12 -62
|
@@ -0,0 +1,1226 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Deterministic stage runner shared by the kk product workflows (ADR-115 composition owner).
|
|
4
|
+
*
|
|
5
|
+
* Each YAML action keeps one short package-root resolution block and delegates its multi-command
|
|
6
|
+
* body here, so a shell action stays a single logical call:
|
|
7
|
+
*
|
|
8
|
+
* prepare-itc <work_dir> <topic> <input_file> <force>
|
|
9
|
+
* prepare-solo <work_dir> <topic> <input_file> <voice_profile> <force>
|
|
10
|
+
* prepare-storm <work_dir> <topic> <input_file> <max_results> <fixture>
|
|
11
|
+
* validate [<override>] --in <voicescript.yaml>
|
|
12
|
+
* wrap [<override>] --in <voicescript.yaml> --out <docs.json> [--profile <name>]
|
|
13
|
+
* duration <work_dir> <target_duration_min>
|
|
14
|
+
* render-storm [<override>] --in <content.json> --out <content.md>
|
|
15
|
+
*
|
|
16
|
+
* `validate` / `wrap` / `render-storm` resolve the shipped sibling sidecars and delegate to them;
|
|
17
|
+
* nothing about those sidecars changed. The `prepare-*` verbs and `duration` keep the logic that
|
|
18
|
+
* used to live inline in `plugins/kk/workflows/*.yaml`, message-for-message.
|
|
19
|
+
*
|
|
20
|
+
* Node/Bun builtins only — no @gobing-ai/* and no relative imports (the file is resolved from the
|
|
21
|
+
* installed package or from a checkout).
|
|
22
|
+
*
|
|
23
|
+
* The YAML prelude resolves this file as `$pkg/plugins/kk/scripts/kk-workflow-stages.ts` where `pkg`
|
|
24
|
+
* comes from `realpath "$(command -v kk)"` (the installed package root), and falls back to the
|
|
25
|
+
* cwd-relative `plugins/kk/scripts/kk-workflow-stages.ts` when that copy is absent — the checkout
|
|
26
|
+
* tier kk-storm-research's render has always relied on, preserved on purpose.
|
|
27
|
+
*
|
|
28
|
+
* SYNC: plugins/kk/workflows/{kk-solo-podcast,kk-itc,kk-storm-research,kk-daily-ai-voice}.yaml shell
|
|
29
|
+
* this script. Keep message substrings stable — apps/cli/tests/kk-workflow-stages.test.ts and the
|
|
30
|
+
* four workflow static tests match them.
|
|
31
|
+
*/
|
|
32
|
+
import { spawnSync } from 'node:child_process';
|
|
33
|
+
import { createHash } from 'node:crypto';
|
|
34
|
+
import {
|
|
35
|
+
appendFileSync,
|
|
36
|
+
copyFileSync,
|
|
37
|
+
existsSync,
|
|
38
|
+
mkdirSync,
|
|
39
|
+
readFileSync,
|
|
40
|
+
realpathSync,
|
|
41
|
+
renameSync,
|
|
42
|
+
statSync,
|
|
43
|
+
unlinkSync,
|
|
44
|
+
writeFileSync,
|
|
45
|
+
} from 'node:fs';
|
|
46
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
47
|
+
|
|
48
|
+
const SCRIPTS_DIR = import.meta.dir;
|
|
49
|
+
const VALIDATE_SIDECAR = 'validate-voicescript.ts';
|
|
50
|
+
const WRAP_SIDECAR = 'wrap-voicescript-doc.ts';
|
|
51
|
+
const RENDER_SIDECAR = 'render-md.ts';
|
|
52
|
+
const REVISE_COUNT = '.revise-count';
|
|
53
|
+
|
|
54
|
+
function fail(message: string): never {
|
|
55
|
+
console.error(message);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** coreutils-style reason for the errno codes the staged file operations can hit. */
|
|
60
|
+
const ERRNO_REASONS: Record<string, string> = {
|
|
61
|
+
EACCES: 'Permission denied',
|
|
62
|
+
EROFS: 'Read-only file system',
|
|
63
|
+
ENOSPC: 'No space left on device',
|
|
64
|
+
ENOENT: 'No such file or directory',
|
|
65
|
+
ENOTDIR: 'Not a directory',
|
|
66
|
+
EISDIR: 'Is a directory',
|
|
67
|
+
EEXIST: 'File exists',
|
|
68
|
+
EPERM: 'Operation not permitted',
|
|
69
|
+
EFAULT: 'Bad address',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run a filesystem operation, turning a failure into the named diagnostic the shell reported
|
|
74
|
+
* (`mkdir: <path>: Permission denied`) instead of an uncaught Bun stack trace.
|
|
75
|
+
*/
|
|
76
|
+
function sted(op: string, path: string, run: () => void): void {
|
|
77
|
+
try {
|
|
78
|
+
run();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const failure = error as { code?: string; message?: string };
|
|
81
|
+
fail(`${op}: ${path}: ${(failure.code !== undefined && ERRNO_REASONS[failure.code]) || failure.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** `[ -f ]` semantics: exists AND is a regular file; an unreadable parent answers false, never throws. */
|
|
86
|
+
function isFile(path: string): boolean {
|
|
87
|
+
try {
|
|
88
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() === true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Runtime override chain, in the order the workflows resolved it inline: explicit var →
|
|
96
|
+
* `KK_WORKFLOWS_DIR` → the shipped package copy → `~/.config/kk/workflows`. The package copy sits
|
|
97
|
+
* before the config-dir copy on purpose (F8 R1) — the shipped bytes win unless the operator elected
|
|
98
|
+
* that directory as an override.
|
|
99
|
+
*/
|
|
100
|
+
function resolveSidecar(override: string, sidecar: string): string {
|
|
101
|
+
if (override && isFile(override)) return override;
|
|
102
|
+
const kkWorkflowsDir = process.env.KK_WORKFLOWS_DIR;
|
|
103
|
+
if (kkWorkflowsDir) {
|
|
104
|
+
const fromEnv = join(kkWorkflowsDir, sidecar);
|
|
105
|
+
if (isFile(fromEnv)) return fromEnv;
|
|
106
|
+
}
|
|
107
|
+
const shipped = join(SCRIPTS_DIR, sidecar);
|
|
108
|
+
if (isFile(shipped)) return shipped;
|
|
109
|
+
return join(process.env.HOME ?? '', '.config/kk/workflows', sidecar);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Fail loud with the remediation path when no copy of a sidecar exists. `label` is the workflow var
|
|
114
|
+
* that carries the override (`validate_script` / `wrap_script` / `render_script`) so the diagnostic
|
|
115
|
+
* names the same thing the operator sees in the YAML.
|
|
116
|
+
*/
|
|
117
|
+
function requireSidecar(override: string, sidecar: string, label: string): string {
|
|
118
|
+
const resolved = resolveSidecar(override, sidecar);
|
|
119
|
+
if (!isFile(resolved)) {
|
|
120
|
+
fail(
|
|
121
|
+
`${label} not found — no ${sidecar} under: the explicit var, $KK_WORKFLOWS_DIR, the package scripts dir (also ./plugins/kk/scripts when the prelude's pkg degrades to a dot), $HOME/.config/kk/workflows — install or refresh the kk package (bun link in a checkout)`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
return resolved;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Run a shipped sibling under the same bun runtime, passing stdout/stderr and the exit code through. */
|
|
128
|
+
function delegate(sidecar: string, args: string[]): never {
|
|
129
|
+
const runner = spawnSync(process.execPath, [sidecar, ...args], { stdio: 'inherit' });
|
|
130
|
+
process.exit(runner.status ?? 1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* The directory a relative `input_file` resolves against. bash used its logical `$PWD`; the runner
|
|
135
|
+
* has only `process.cwd()` (physical) plus the inherited `$PWD`, so trust `$PWD` exactly when it
|
|
136
|
+
* realpaths to the same directory and fall back to the physical cwd otherwise.
|
|
137
|
+
*/
|
|
138
|
+
function logicalCwd(): string {
|
|
139
|
+
const pwd = process.env.PWD;
|
|
140
|
+
if (!pwd) return process.cwd();
|
|
141
|
+
try {
|
|
142
|
+
return realpathSync(pwd) === realpathSync(process.cwd()) ? pwd : process.cwd();
|
|
143
|
+
} catch {
|
|
144
|
+
return process.cwd();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** jq's `-n '<literal>' > file`: pretty 2-space JSON with a trailing newline, keys in literal order. */
|
|
149
|
+
function writeJson(path: string, value: Record<string, unknown>): void {
|
|
150
|
+
sted('write', path, () => writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** `.revise-count` lives at the work_dir root in every workflow that uses it. */
|
|
154
|
+
|
|
155
|
+
function bumpReviseCount(workDir: string): void {
|
|
156
|
+
const path = join(workDir, '.revise-count');
|
|
157
|
+
let previous = Number.NaN;
|
|
158
|
+
if (isFile(path))
|
|
159
|
+
sted('read', path, () => {
|
|
160
|
+
previous = Number.parseInt(readFileSync(path, 'utf-8'), 10);
|
|
161
|
+
});
|
|
162
|
+
sted('write', path, () => writeFileSync(path, `${(Number.isNaN(previous) ? 0 : previous) + 1}\n`));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface PrepareOptions {
|
|
166
|
+
readonly workDirArg: string;
|
|
167
|
+
readonly topic: string;
|
|
168
|
+
readonly inputFile: string;
|
|
169
|
+
readonly force: string;
|
|
170
|
+
/** Stage directories created under work_dir, in the workflow's declared order. */
|
|
171
|
+
readonly dirs: readonly string[];
|
|
172
|
+
/** solo-podcast requires a profile (argv or $VOICEBOX_DEFAULT_PROFILE); itc does not. */
|
|
173
|
+
readonly requireVoiceProfile: boolean;
|
|
174
|
+
readonly voiceProfile: string;
|
|
175
|
+
/** itc zeroes .revise-count before the XOR checks; solo-podcast zeroes it after brief.md. */
|
|
176
|
+
readonly reviseCountEarly: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** XOR + brief.md workspace staging shared by the itc and solo-podcast prepares. */
|
|
180
|
+
function prepareWorkspace(options: PrepareOptions): void {
|
|
181
|
+
const { workDirArg, topic, inputFile, force, dirs, voiceProfile, requireVoiceProfile, reviseCountEarly } = options;
|
|
182
|
+
if (!workDirArg) fail('missing work_dir');
|
|
183
|
+
const workDir = resolve(workDirArg);
|
|
184
|
+
for (const dir of dirs) sted('mkdir', join(workDir, dir), () => mkdirSync(join(workDir, dir), { recursive: true }));
|
|
185
|
+
if (reviseCountEarly)
|
|
186
|
+
sted('write', join(workDir, REVISE_COUNT), () => writeFileSync(join(workDir, REVISE_COUNT), '0\n'));
|
|
187
|
+
if (topic && inputFile) fail('XOR violation: both topic and input_file set');
|
|
188
|
+
if (!topic && !inputFile) fail('XOR violation: neither topic nor input_file set');
|
|
189
|
+
if (requireVoiceProfile && !voiceProfile && !process.env.VOICEBOX_DEFAULT_PROFILE) {
|
|
190
|
+
fail('missing voice_profile (set vars.voice_profile or VOICEBOX_DEFAULT_PROFILE)');
|
|
191
|
+
}
|
|
192
|
+
const briefPath = join(workDir, 'brief.md');
|
|
193
|
+
if (isFile(briefPath) && force !== 'true') {
|
|
194
|
+
fail(`initialization target already exists at ${workDirArg}/brief.md; pass force=true to replace`);
|
|
195
|
+
}
|
|
196
|
+
if (topic) {
|
|
197
|
+
sted('write', briefPath, () => writeFileSync(briefPath, `${topic}\n`));
|
|
198
|
+
} else {
|
|
199
|
+
if (!isFile(inputFile)) fail(`input file not found: ${inputFile}`);
|
|
200
|
+
sted('cp', inputFile, () => copyFileSync(inputFile, briefPath));
|
|
201
|
+
}
|
|
202
|
+
if (!reviseCountEarly)
|
|
203
|
+
sted('write', join(workDir, REVISE_COUNT), () => writeFileSync(join(workDir, REVISE_COUNT), '0\n'));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function prepareItc(args: string[]): void {
|
|
207
|
+
const [workDirArg = '', topic = '', inputFile = '', force = 'false'] = args;
|
|
208
|
+
prepareWorkspace({
|
|
209
|
+
workDirArg,
|
|
210
|
+
topic,
|
|
211
|
+
inputFile,
|
|
212
|
+
force,
|
|
213
|
+
dirs: ['2-outline'],
|
|
214
|
+
voiceProfile: '',
|
|
215
|
+
requireVoiceProfile: false,
|
|
216
|
+
reviseCountEarly: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function prepareSolo(args: string[]): void {
|
|
221
|
+
const [workDirArg = '', topic = '', inputFile = '', voiceProfile = '', force = 'false'] = args;
|
|
222
|
+
prepareWorkspace({
|
|
223
|
+
workDirArg,
|
|
224
|
+
topic,
|
|
225
|
+
inputFile,
|
|
226
|
+
force,
|
|
227
|
+
dirs: ['2-outline', '4-audio'],
|
|
228
|
+
voiceProfile,
|
|
229
|
+
requireVoiceProfile: true,
|
|
230
|
+
reviseCountEarly: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* STORM prepare: sentence mode writes topic.json; file mode copies input.md and derives the
|
|
236
|
+
* local-doc entry + topic.json query from the file's H1 (0056 works layout).
|
|
237
|
+
*/
|
|
238
|
+
function prepareStorm(args: string[]): void {
|
|
239
|
+
const [workDirArg = '', topic = '', inputFile = '', maxResultsRaw = '8', fixture = 'false'] = args;
|
|
240
|
+
if (!workDirArg) fail('missing work_dir');
|
|
241
|
+
const workDir = resolve(workDirArg);
|
|
242
|
+
sted('mkdir', workDir, () => mkdirSync(workDir, { recursive: true }));
|
|
243
|
+
if (topic && inputFile) fail('XOR violation: both topic and input_file set');
|
|
244
|
+
if (!topic && !inputFile) fail('XOR violation: neither topic nor input_file set');
|
|
245
|
+
// `jq`'s `tonumber` grammar (`+8`, `8.`, `.5`, `1e3` yes; `0x10`, surrounding whitespace, empty no),
|
|
246
|
+
// with three deliberate divergences: jq echoed the lexeme it parsed (`8.0`, `1E+3`) where this
|
|
247
|
+
// writes the equivalent JS number; jq accepted the C-float specials `inf`/`Infinity`/`nan` (writing
|
|
248
|
+
// ±1.797e308 and `null`) where this rejects them; and jq accepted `1e400` (writing `1E+400`) where
|
|
249
|
+
// this fails loud rather than emitting a non-finite value.
|
|
250
|
+
if (!/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/.test(maxResultsRaw)) {
|
|
251
|
+
fail(`invalid maxResults: ${maxResultsRaw}`);
|
|
252
|
+
}
|
|
253
|
+
const maxResults = Number(maxResultsRaw);
|
|
254
|
+
if (!Number.isFinite(maxResults)) fail(`invalid maxResults: ${maxResultsRaw}`);
|
|
255
|
+
const topicFile = (query: string): Record<string, unknown> => ({
|
|
256
|
+
topic: query,
|
|
257
|
+
maxResults,
|
|
258
|
+
fixture: fixture === 'true',
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (topic) {
|
|
262
|
+
writeJson(join(workDir, 'topic.json'), topicFile(topic));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (!isFile(inputFile)) fail(`input_file not readable: ${inputFile}`);
|
|
266
|
+
sted('cp', inputFile, () => copyFileSync(inputFile, join(workDir, 'input.md')));
|
|
267
|
+
const body = readFileSync(inputFile, 'utf-8');
|
|
268
|
+
const heading = body.split('\n').find((line) => line.startsWith('# '));
|
|
269
|
+
let query = heading === undefined ? '' : heading.replace(/^# */, '');
|
|
270
|
+
if (!query) query = basename(inputFile).replace(/\.[^.]*$/, '');
|
|
271
|
+
if (!query) fail('no query derivable (no H1 and empty basename)');
|
|
272
|
+
// A relative input_file must hash the same file:// URI the shell's logical `$PWD` produced.
|
|
273
|
+
const absolute = join(resolve(logicalCwd(), dirname(inputFile)), basename(inputFile));
|
|
274
|
+
const sourceUri = `file://${absolute}`;
|
|
275
|
+
writeJson(join(workDir, 'local-doc.json'), {
|
|
276
|
+
id: createHash('sha256').update(sourceUri).digest('hex').slice(0, 16),
|
|
277
|
+
// `$(cat file)` drops every trailing newline before jq receives the body.
|
|
278
|
+
body: body.replace(/\n+$/, ''),
|
|
279
|
+
title: query,
|
|
280
|
+
sourceUri,
|
|
281
|
+
mediaType: 'text/markdown',
|
|
282
|
+
metadata: { query, rank: -1, position: 0 },
|
|
283
|
+
});
|
|
284
|
+
writeJson(join(workDir, 'topic.json'), topicFile(query));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function duration(args: string[]): void {
|
|
288
|
+
const [workDirArg = '', targetDurationMin = ''] = args;
|
|
289
|
+
if (!workDirArg) fail('missing work_dir');
|
|
290
|
+
const targetMinutes = Number(targetDurationMin);
|
|
291
|
+
if (!Number.isFinite(targetMinutes) || targetMinutes <= 0) {
|
|
292
|
+
fail(`invalid target_duration_min: ${targetDurationMin || '(empty)'}`);
|
|
293
|
+
}
|
|
294
|
+
const workDir = resolve(workDirArg);
|
|
295
|
+
const okPath = join(workDir, '4-audio', '.duration-ok');
|
|
296
|
+
const contentPath = join(workDir, '4-audio', 'content.json');
|
|
297
|
+
// `rm -f <path>`: silent when the file is already gone, loud with the errno otherwise.
|
|
298
|
+
try {
|
|
299
|
+
unlinkSync(okPath);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
if ((error as { code?: string }).code !== 'ENOENT') sted('rm', okPath, () => unlinkSync(okPath));
|
|
302
|
+
}
|
|
303
|
+
if (!isFile(contentPath)) return;
|
|
304
|
+
|
|
305
|
+
let raw: unknown;
|
|
306
|
+
try {
|
|
307
|
+
const parsed = JSON.parse(readFileSync(contentPath, 'utf-8')) as { metadata?: { duration?: unknown } };
|
|
308
|
+
raw = parsed.metadata?.duration;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
// A parse failure is "not a number"; an unreadable file is a real error — re-read inside the
|
|
311
|
+
// wrapper so the errno (EACCES/EIO) is reported instead of being swallowed as missing data.
|
|
312
|
+
if ((error as { code?: string }).code !== undefined) {
|
|
313
|
+
sted('read', contentPath, () => readFileSync(contentPath, 'utf-8'));
|
|
314
|
+
}
|
|
315
|
+
raw = undefined;
|
|
316
|
+
}
|
|
317
|
+
// A numeric string is a number for this gate (`jq` + `awk` read it that way); anything else that
|
|
318
|
+
// is not a finite number takes the "missing or not a number" branch its own message promises.
|
|
319
|
+
const numeric = typeof raw === 'number' ? raw : typeof raw === 'string' && raw.trim() !== '' ? Number(raw) : NaN;
|
|
320
|
+
if (!Number.isFinite(numeric)) {
|
|
321
|
+
sted('write', join(workDir, '.duration-feedback'), () =>
|
|
322
|
+
writeFileSync(
|
|
323
|
+
join(workDir, '.duration-feedback'),
|
|
324
|
+
'Regenerate: audio metadata.duration missing or not a number.\n',
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
bumpReviseCount(workDir);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const targetSeconds = targetMinutes * 60;
|
|
331
|
+
if (numeric >= targetSeconds * 0.5 && numeric <= targetSeconds * 2) {
|
|
332
|
+
sted('touch', okPath, () => writeFileSync(okPath, ''));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
// `printf '%s' "$dur"` echoed the raw token; keep it (a numeric string like "1200.0" stays so).
|
|
336
|
+
const shown = typeof raw === 'string' ? raw : String(numeric);
|
|
337
|
+
sted('write', join(workDir, '.duration-feedback'), () =>
|
|
338
|
+
writeFileSync(
|
|
339
|
+
join(workDir, '.duration-feedback'),
|
|
340
|
+
`Regenerate: last audio duration ${shown}s outside 0.5x–2x of ${targetSeconds}s; adjust segment lengths toward the target.\n`,
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
bumpReviseCount(workDir);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Positional override (arg 0) + `--flag value` options, for the delegating stages. Scanned by hand
|
|
348
|
+
* rather than with `node:util`'s `parseArgs`: a workflow var may legitimately hold a path that starts
|
|
349
|
+
* with `-` (e.g. `-x`), which `parseArgs` would treat as an option and throw on — the pre-0132 shell
|
|
350
|
+
* chain treated it as a path, fell through the `[ -f ]` test, and kept working.
|
|
351
|
+
*/
|
|
352
|
+
function parseStage(
|
|
353
|
+
rest: string[],
|
|
354
|
+
options: Record<string, { type: 'string' }>,
|
|
355
|
+
): { override: string; values: Record<string, string | undefined> } {
|
|
356
|
+
const values: Record<string, string | undefined> = {};
|
|
357
|
+
let override = '';
|
|
358
|
+
for (let i = 0; i < rest.length; i++) {
|
|
359
|
+
const token = rest[i] ?? '';
|
|
360
|
+
const flag = token.startsWith('--') ? token.slice(2) : '';
|
|
361
|
+
if (flag !== '') {
|
|
362
|
+
// Split on the FIRST `=`: a value may contain one (`--in=/tmp/a=b.yaml`).
|
|
363
|
+
const eq = flag.indexOf('=');
|
|
364
|
+
const name = eq < 0 ? flag : flag.slice(0, eq);
|
|
365
|
+
if (!Object.hasOwn(options, name)) fail(`unknown option: ${token}`);
|
|
366
|
+
values[name] = eq < 0 ? (rest[++i] ?? '') : flag.slice(eq + 1);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (override === '') override = token;
|
|
370
|
+
}
|
|
371
|
+
return { override, values };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── kk-daily-ai-voice stages (0133) ─────────────────────────────────────────────────────────────
|
|
375
|
+
// Ported one-for-one from plugins/kk/workflows/kk-daily-ai-voice.yaml. Every ported stage keeps its
|
|
376
|
+
// step-timing append (success AND failure), its cache predicate and message, the plugins-path argv,
|
|
377
|
+
// and the plugin argv/env it used. `kk` is resolved from PATH, exactly as the shell did. Stages whose
|
|
378
|
+
// inline body already fits the ADR-115 budget (`collect:onEnter:0`'s fan-in at 7 commands, the HITL/prompt states)
|
|
379
|
+
// deliberately stay in the YAML.
|
|
380
|
+
|
|
381
|
+
const DAILY_DIRS = [
|
|
382
|
+
'1-ingest',
|
|
383
|
+
'2-facts',
|
|
384
|
+
'2-plan',
|
|
385
|
+
'2-article',
|
|
386
|
+
'2-cover',
|
|
387
|
+
'2-script',
|
|
388
|
+
'3-audio',
|
|
389
|
+
'3-publish',
|
|
390
|
+
] as const;
|
|
391
|
+
|
|
392
|
+
/** `date -u +%FT%TZ` — the timestamp grammar the step-timing lines have always used. */
|
|
393
|
+
function utcStamp(): string {
|
|
394
|
+
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* The pre-0133 shell wrapped every daily step in `trap … EXIT`, so the timing line lands on failure
|
|
399
|
+
* too. The body signals failure by throwing `failWith`, never by calling `process.exit` directly.
|
|
400
|
+
*/
|
|
401
|
+
class StageFailure extends Error {
|
|
402
|
+
constructor(
|
|
403
|
+
readonly status: number,
|
|
404
|
+
readonly named: boolean,
|
|
405
|
+
message: string,
|
|
406
|
+
) {
|
|
407
|
+
super(message);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function failWith(status: number, message?: string): never {
|
|
412
|
+
throw new StageFailure(status, message !== undefined, message ?? `stage failed (status ${status})`);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function appendTiming(workDir: string, step: string, startedAt: string): void {
|
|
416
|
+
if (!workDir) return;
|
|
417
|
+
try {
|
|
418
|
+
appendFileSync(
|
|
419
|
+
join(workDir, 'step-timing.json'),
|
|
420
|
+
`${JSON.stringify({ step, startedAt, endedAt: utcStamp() })}\n`,
|
|
421
|
+
);
|
|
422
|
+
} catch {
|
|
423
|
+
// The shell trap was best-effort too: a missing/unwritable work_dir never failed the step.
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function timed(workDir: string, step: string, body: () => void): void {
|
|
428
|
+
const startedAt = utcStamp();
|
|
429
|
+
let status = 0;
|
|
430
|
+
try {
|
|
431
|
+
body();
|
|
432
|
+
} catch (error) {
|
|
433
|
+
if (error instanceof StageFailure) {
|
|
434
|
+
if (error.named) console.error(error.message);
|
|
435
|
+
status = error.status;
|
|
436
|
+
} else {
|
|
437
|
+
console.error((error as Error).message);
|
|
438
|
+
status = 1;
|
|
439
|
+
}
|
|
440
|
+
} finally {
|
|
441
|
+
appendTiming(workDir, step, startedAt);
|
|
442
|
+
}
|
|
443
|
+
if (status !== 0) process.exit(status);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function pluginsArgs(pluginsPath: string): string[] {
|
|
447
|
+
return pluginsPath === '' ? [] : ['--plugins-path', pluginsPath];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** `kk <args>` with the caller's env overrides; `soft` mirrors the shell's `|| true`. */
|
|
451
|
+
function kk(
|
|
452
|
+
args: string[],
|
|
453
|
+
options: { env?: Record<string, string>; unset?: readonly string[]; soft?: boolean } = {},
|
|
454
|
+
): number {
|
|
455
|
+
const env = { ...process.env, ...(options.env ?? {}) };
|
|
456
|
+
for (const name of options.unset ?? []) delete env[name];
|
|
457
|
+
const runner = spawnSync('kk', args, { stdio: 'inherit', env });
|
|
458
|
+
if (runner.error !== undefined) {
|
|
459
|
+
// A missing/un-launchable `kk` is not a plugin failure: the pre-0133 shell reported
|
|
460
|
+
// `kk: command not found` and exited 127. `soft` callers tolerate a *plugin* failure, not
|
|
461
|
+
// an unusable environment — the operator must still see why nothing ran.
|
|
462
|
+
console.error(`kk: ${runner.error.message}`);
|
|
463
|
+
failWith(127);
|
|
464
|
+
}
|
|
465
|
+
const status = runner.status ?? 1;
|
|
466
|
+
if (status !== 0 && options.soft !== true) failWith(status);
|
|
467
|
+
return status;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function writeJsonText(path: string, value: unknown): void {
|
|
471
|
+
// `Bun.write(path, JSON.stringify(value, null, 2))` — no trailing newline.
|
|
472
|
+
writeFileSync(path, JSON.stringify(value, null, 2));
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function writeJqText(path: string, value: unknown): void {
|
|
476
|
+
// `jq … > path` — pretty output WITH the trailing newline jq always prints.
|
|
477
|
+
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function writeRaw(path: string, value: string): void {
|
|
481
|
+
// `printf '%s' … > path` / `bun -e 'Bun.write'` — no trailing newline.
|
|
482
|
+
writeFileSync(path, value);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function readJson(path: string): Record<string, unknown> | null {
|
|
486
|
+
if (!isFile(path)) return null;
|
|
487
|
+
try {
|
|
488
|
+
return JSON.parse(readFileSync(path, 'utf-8')) as Record<string, unknown>;
|
|
489
|
+
} catch {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function metadataOf(value: Record<string, unknown> | null): Record<string, unknown> {
|
|
495
|
+
const metadata = value?.metadata;
|
|
496
|
+
return metadata !== null && typeof metadata === 'object' ? (metadata as Record<string, unknown>) : {};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/** `jq -e '.targets[]? | select(.target == t and .ok == true and .status == "published")'`. */
|
|
500
|
+
function targetPublished(path: string, target: string): boolean {
|
|
501
|
+
const parsed = readJson(path);
|
|
502
|
+
const targets = parsed?.targets;
|
|
503
|
+
if (!Array.isArray(targets)) return false;
|
|
504
|
+
return targets.some(
|
|
505
|
+
(entry) =>
|
|
506
|
+
entry !== null &&
|
|
507
|
+
typeof entry === 'object' &&
|
|
508
|
+
(entry as Record<string, unknown>).target === target &&
|
|
509
|
+
(entry as Record<string, unknown>).ok === true &&
|
|
510
|
+
(entry as Record<string, unknown>).status === 'published',
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function dailyPrepare(args: string[]): void {
|
|
515
|
+
const [
|
|
516
|
+
workDirArg = '',
|
|
517
|
+
limit = '',
|
|
518
|
+
cursor = '',
|
|
519
|
+
stateFile = '',
|
|
520
|
+
last30daysEnabled = '',
|
|
521
|
+
topic = '',
|
|
522
|
+
horizonHours = '',
|
|
523
|
+
] = args;
|
|
524
|
+
const workDir = resolve(workDirArg);
|
|
525
|
+
timed(workDirArg, 'prepare', () => {
|
|
526
|
+
if (!workDirArg) failWith(1, 'missing required variable: work_dir');
|
|
527
|
+
for (const dir of DAILY_DIRS)
|
|
528
|
+
sted('mkdir', join(workDir, dir), () => mkdirSync(join(workDir, dir), { recursive: true }));
|
|
529
|
+
sted('write', join(workDir, 'run-date.txt'), () =>
|
|
530
|
+
writeFileSync(join(workDir, 'run-date.txt'), `${formatRunDate()}\n`),
|
|
531
|
+
);
|
|
532
|
+
sted('write', join(workDir, '1-ingest/source.json'), () =>
|
|
533
|
+
writeJsonText(join(workDir, '1-ingest/source.json'), {
|
|
534
|
+
limit: Number(limit),
|
|
535
|
+
cursor: cursor === '' ? undefined : cursor,
|
|
536
|
+
stateFile: stateFile === '' ? undefined : stateFile,
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
if (last30daysEnabled !== 'true') {
|
|
540
|
+
sted('write', join(workDir, '1-ingest/last30days-docs.json'), () =>
|
|
541
|
+
writeFileSync(join(workDir, '1-ingest/last30days-docs.json'), '[]'),
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
sted('write', join(workDir, '1-ingest/last30days.json'), () =>
|
|
545
|
+
writeJsonText(join(workDir, '1-ingest/last30days.json'), { topic }),
|
|
546
|
+
);
|
|
547
|
+
sted('write', join(workDir, '1-ingest/horizon.json'), () =>
|
|
548
|
+
writeJsonText(join(workDir, '1-ingest/horizon.json'), { hours: Number(horizonHours) }),
|
|
549
|
+
);
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/** `date +%Y%m%d` — the run-date stamp the artifact names carry. */
|
|
554
|
+
function formatRunDate(): string {
|
|
555
|
+
const now = new Date();
|
|
556
|
+
const month = `${now.getMonth() + 1}`.padStart(2, '0');
|
|
557
|
+
const day = `${now.getDate()}`.padStart(2, '0');
|
|
558
|
+
return `${now.getFullYear()}${month}${day}`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function dailyCollectFacts(args: string[]): void {
|
|
562
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
563
|
+
const content = join(workDir, '2-facts', `${runDate}_02_collect_content.json`);
|
|
564
|
+
const facts = join(workDir, '2-facts', `${runDate}_02_collect_core-facts.md`);
|
|
565
|
+
const blended = join(workDir, '1-ingest', `${runDate}_02_collect_blended.json`);
|
|
566
|
+
timed(workDir, 'collect', () => {
|
|
567
|
+
if (isFile(content) && isFile(facts)) {
|
|
568
|
+
console.log(`core facts cached: ${facts}`);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
kk(['executor', 'run', 'core-facts-gen', '--in', blended, '--out', content, ...pluginsArgs(pluginsPath)]);
|
|
572
|
+
const body = readJson(content)?.body;
|
|
573
|
+
if (typeof body !== 'string') failWith(1, `collect: ${content} has no string body`);
|
|
574
|
+
sted('write', facts, () => writeRaw(facts, body));
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function dailyPlan(args: string[]): void {
|
|
579
|
+
const [workDir = '', runDate = '', pluginsPath = '', planMaxItems = ''] = args;
|
|
580
|
+
const planContent = join(workDir, '2-plan', `${runDate}_03_plan_content.json`);
|
|
581
|
+
const planDocs = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
582
|
+
const blended = join(workDir, '1-ingest', `${runDate}_02_collect_blended.json`);
|
|
583
|
+
timed(workDir, 'plan', () => {
|
|
584
|
+
if (isFile(planDocs)) {
|
|
585
|
+
console.log(`plan cached: ${planDocs}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
kk(
|
|
589
|
+
['executor', 'run', 'episode-plan-gen', '--in', blended, '--out', planContent, ...pluginsArgs(pluginsPath)],
|
|
590
|
+
{
|
|
591
|
+
env: { EPISODE_PLAN_MAX_ITEMS: planMaxItems, EPISODE_PLAN_DATE: runDate },
|
|
592
|
+
},
|
|
593
|
+
);
|
|
594
|
+
const docs = metadataOf(readJson(planContent)).docs;
|
|
595
|
+
if (!Array.isArray(docs) || docs.length === 0) failWith(1, 'plan: content.metadata.docs missing or empty');
|
|
596
|
+
sted('write', planDocs, () => writeJsonText(planDocs, docs));
|
|
597
|
+
if (!isFile(planDocs)) failWith(1, `plan: ${planDocs} not written after episode-plan-gen`);
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function dailyQcContent(args: string[]): void {
|
|
602
|
+
const [
|
|
603
|
+
workDir = '',
|
|
604
|
+
runDate = '',
|
|
605
|
+
pluginsPath = '',
|
|
606
|
+
qcCategories = '',
|
|
607
|
+
minQuality = '',
|
|
608
|
+
minImportance = '',
|
|
609
|
+
minUrgency = '',
|
|
610
|
+
minImpact = '',
|
|
611
|
+
] = args;
|
|
612
|
+
const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
613
|
+
const annotated = join(workDir, '2-plan', `${runDate}_04_quality-control-content_annotated.json`);
|
|
614
|
+
const planContent = join(workDir, '2-plan', `${runDate}_03_plan_content.json`);
|
|
615
|
+
timed(workDir, 'quality-control-content', () => {
|
|
616
|
+
if (isFile(candidates)) {
|
|
617
|
+
console.log(`candidates cached: ${candidates}`);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
sted('write', annotated, () => writeJqText(annotated, metadataOf(readJson(planContent)).docs ?? null));
|
|
621
|
+
kk(
|
|
622
|
+
[
|
|
623
|
+
'executor',
|
|
624
|
+
'run',
|
|
625
|
+
'episode-plan-gen',
|
|
626
|
+
'--in',
|
|
627
|
+
annotated,
|
|
628
|
+
'--out',
|
|
629
|
+
candidates,
|
|
630
|
+
...pluginsArgs(pluginsPath),
|
|
631
|
+
],
|
|
632
|
+
{
|
|
633
|
+
env: {
|
|
634
|
+
EPISODE_PLAN_MODE: 'filter',
|
|
635
|
+
QC_CATEGORIES: qcCategories,
|
|
636
|
+
QC_MIN_QUALITY: minQuality,
|
|
637
|
+
QC_MIN_IMPORTANCE: minImportance,
|
|
638
|
+
QC_MIN_URGENCY: minUrgency,
|
|
639
|
+
QC_MIN_IMPACT: minImpact,
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function dailyArticle(args: string[]): void {
|
|
647
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
648
|
+
const articleMd = join(workDir, '2-article', `${runDate}_06_article_article.md`);
|
|
649
|
+
const articleContent = join(workDir, '2-article', `${runDate}_06_article_content.json`);
|
|
650
|
+
const planIn = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
651
|
+
const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
652
|
+
timed(workDir, 'article', () => {
|
|
653
|
+
if (isFile(articleMd)) {
|
|
654
|
+
console.log(`article cached: ${articleMd}`);
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
let planUsable = true;
|
|
658
|
+
const raw = isFile(planIn) ? readFileSync(planIn, 'utf-8') : '';
|
|
659
|
+
try {
|
|
660
|
+
JSON.parse(raw);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
console.error(
|
|
663
|
+
`article: plan.json invalid JSON — replacing with untranslated plan (downstream states read this file): ${(error as Error).message}`,
|
|
664
|
+
);
|
|
665
|
+
const docs = metadataOf(readJson(candidates)).docs;
|
|
666
|
+
if (!Array.isArray(docs) || docs.length === 0) {
|
|
667
|
+
console.error('article: candidates.json metadata.docs missing');
|
|
668
|
+
planUsable = false;
|
|
669
|
+
} else {
|
|
670
|
+
sted('write', planIn, () => writeJsonText(planIn, docs));
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (!planUsable) failWith(1, 'article: no usable plan input');
|
|
674
|
+
kk(
|
|
675
|
+
[
|
|
676
|
+
'executor',
|
|
677
|
+
'run',
|
|
678
|
+
'daily-article-gen',
|
|
679
|
+
'--in',
|
|
680
|
+
planIn,
|
|
681
|
+
'--out',
|
|
682
|
+
articleContent,
|
|
683
|
+
...pluginsArgs(pluginsPath),
|
|
684
|
+
],
|
|
685
|
+
{
|
|
686
|
+
env: { ARTICLE_DATE: runDate },
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
const body = readJson(articleContent)?.body;
|
|
690
|
+
if (typeof body !== 'string') failWith(1, `article: ${articleContent} has no string body`);
|
|
691
|
+
sted('write', articleMd, () => writeRaw(articleMd, body));
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function dailyCoverNormalize(args: string[]): void {
|
|
696
|
+
const [workDir = '', runDate = ''] = args;
|
|
697
|
+
// The pre-0133 shell had no timing trap, so this stage is not `timed`; it still fails through
|
|
698
|
+
// `failWith`, which needs the named-failure handling `timed` gives the other stages.
|
|
699
|
+
try {
|
|
700
|
+
coverNormalizeBody(workDir, runDate);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (error instanceof StageFailure) {
|
|
703
|
+
if (error.named) console.error(error.message);
|
|
704
|
+
process.exit(error.status);
|
|
705
|
+
}
|
|
706
|
+
console.error((error as Error).message);
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function coverNormalizeBody(workDir: string, runDate: string): void {
|
|
712
|
+
const png = join(workDir, '2-cover', 'content-cover-01.png');
|
|
713
|
+
const jpg = join(workDir, '2-cover', 'content-cover-01.jpg');
|
|
714
|
+
const out = join(workDir, '2-cover', `cover-${runDate}.png`);
|
|
715
|
+
const source = isFile(png) ? png : isFile(jpg) ? jpg : '';
|
|
716
|
+
if (source === '') failWith(1, 'cover normalize: no content-cover-01.{png,jpg} artifact');
|
|
717
|
+
if (source === jpg) {
|
|
718
|
+
const sips = spawnSync('sips', ['-s', 'format', 'png', source, '--out', out], { stdio: 'ignore' });
|
|
719
|
+
// Bun reports a missing binary as `status: undefined` (older versions: `null`) plus an
|
|
720
|
+
// `error` object — the pre-0133 shell's `command -v sips` failed the same way.
|
|
721
|
+
if (sips.error !== undefined || typeof sips.status !== 'number') {
|
|
722
|
+
failWith(1, 'cover normalize: sips not found; cannot transcode jpg -> png');
|
|
723
|
+
}
|
|
724
|
+
if (sips.status !== 0) {
|
|
725
|
+
failWith(1, `cover normalize: sips failed (status ${sips.status}); jpg -> png did not run`);
|
|
726
|
+
}
|
|
727
|
+
} else {
|
|
728
|
+
sted('mv', source, () => renameSync(source, out));
|
|
729
|
+
}
|
|
730
|
+
for (const stale of [jpg, png]) {
|
|
731
|
+
try {
|
|
732
|
+
unlinkSync(stale);
|
|
733
|
+
} catch {
|
|
734
|
+
// `rm -f` is silent when the file is already gone.
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
console.log(`cover normalized: cover-${runDate}.png`);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function dailyScript(args: string[]): void {
|
|
741
|
+
const [workDir = '', runDate = '', pluginsPath = '', language = '', title = '', voiceProfile = ''] = args;
|
|
742
|
+
const yamlOut = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
|
|
743
|
+
const contentOut = join(workDir, '2-script', `${runDate}_08_script_content.json`);
|
|
744
|
+
const planIn = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
745
|
+
timed(workDir, 'script', () => {
|
|
746
|
+
if (isFile(yamlOut)) {
|
|
747
|
+
console.log(`voicescript capped by plan: ${yamlOut}`);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
kk(['executor', 'run', 'dailynews-gen', '--in', planIn, '--out', contentOut, ...pluginsArgs(pluginsPath)], {
|
|
751
|
+
env: { DAILYNEWS_LANGUAGE: language, DAILYNEWS_TITLE: title, VOICEBOX_DEFAULT_PROFILE: voiceProfile },
|
|
752
|
+
});
|
|
753
|
+
const body = readJson(contentOut)?.body;
|
|
754
|
+
if (typeof body !== 'string') failWith(1, `script: ${contentOut} has no string body`);
|
|
755
|
+
sted('write', yamlOut, () => writeRaw(yamlOut, body));
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function dailyWrapDocs(args: string[]): void {
|
|
760
|
+
const [workDir = '', runDate = '', voiceProfile = ''] = args;
|
|
761
|
+
const scriptPath = join(workDir, '2-script', `${runDate}_08_script_voicescript.yaml`);
|
|
762
|
+
const docsPath = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
|
|
763
|
+
timed(workDir, 'wrap-docs', () => {
|
|
764
|
+
const url = new URL(scriptPath, `file://${process.cwd()}/`);
|
|
765
|
+
const body = readFileSync(decodeURIComponent(url.pathname), 'utf-8');
|
|
766
|
+
const profile = voiceProfile || process.env.VOICEBOX_DEFAULT_PROFILE || 'robin-news';
|
|
767
|
+
const doc = {
|
|
768
|
+
id: createHash('sha256').update(url.href).digest('hex').slice(0, 16),
|
|
769
|
+
title: 'Daily AI News VoiceScript',
|
|
770
|
+
sourceUri: url.href,
|
|
771
|
+
body,
|
|
772
|
+
mediaType: 'application/yaml',
|
|
773
|
+
metadata: { voiceProfile: profile },
|
|
774
|
+
};
|
|
775
|
+
sted('write', docsPath, () => writeJsonText(docsPath, [doc]));
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function dailyGenerate(args: string[]): void {
|
|
780
|
+
const [workDir = '', runDate = '', pluginsPath = '', transcodeMp3 = '', voiceGenerator = ''] = args;
|
|
781
|
+
const docs = join(workDir, '3-audio', `${runDate}_10_wrap-docs_docs.json`);
|
|
782
|
+
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
783
|
+
timed(workDir, 'generate', () => {
|
|
784
|
+
if (isFile(content) && metadataOf(readJson(content)).qc !== undefined) {
|
|
785
|
+
const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
|
|
786
|
+
if (qc?.passed === true) {
|
|
787
|
+
console.log(`audio cached: ${content} (QC passed)`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
kk(['executor', 'run', voiceGenerator, '--in', docs, '--out', content, ...pluginsArgs(pluginsPath)], {
|
|
792
|
+
env: { VOICE_GEN_MP3: transcodeMp3 },
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function dailyQualityReport(args: string[]): void {
|
|
798
|
+
const [workDir = '', runDate = ''] = args;
|
|
799
|
+
const content = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
800
|
+
timed(workDir, 'quality-control', () => {
|
|
801
|
+
const qc = metadataOf(readJson(content)).qc as Record<string, unknown> | undefined;
|
|
802
|
+
console.log('=== Voice Quality Control Report ===');
|
|
803
|
+
console.log(`Overall Score: ${qc ? qc.overallScore : 100}/100 | Passed: ${qc ? qc.passed : true}`);
|
|
804
|
+
const issues = qc?.criticalIssues;
|
|
805
|
+
if (Array.isArray(issues) && issues.length > 0) {
|
|
806
|
+
console.warn(`QC Issues:\n${issues.map((issue) => ` - ${issue}`).join('\n')}`);
|
|
807
|
+
} else {
|
|
808
|
+
console.log('All audio segments passed quality control checks cleanly.');
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function dailyPublishPrep(args: string[]): void {
|
|
814
|
+
const [workDir = '', runDate = ''] = args;
|
|
815
|
+
const articlePath = join(workDir, '2-article', `${runDate}_06_article_content.json`);
|
|
816
|
+
const audioPath = join(workDir, '3-audio', `${runDate}_11_generate_content.json`);
|
|
817
|
+
const prepPath = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
818
|
+
const coverPath = join(workDir, '2-cover', `${runDate}_07_cover_cover.png`);
|
|
819
|
+
timed(workDir, 'publish-prep', () => {
|
|
820
|
+
const article = readJson(articlePath) ?? {};
|
|
821
|
+
const audioMeta = metadataOf(readJson(audioPath));
|
|
822
|
+
const rawFile = audioMeta.mp3Path ?? audioMeta.audioPath;
|
|
823
|
+
if (typeof rawFile !== 'string' || rawFile === '') {
|
|
824
|
+
failWith(1, 'publish-prep: voice content carries no mp3Path/audioPath');
|
|
825
|
+
}
|
|
826
|
+
const audioFile = resolve(rawFile);
|
|
827
|
+
const resolvedCover = resolve(coverPath);
|
|
828
|
+
const merged = {
|
|
829
|
+
...article,
|
|
830
|
+
metadata: { ...metadataOf(article), audioFile, runDate },
|
|
831
|
+
options: {
|
|
832
|
+
audioFile,
|
|
833
|
+
durationSec: audioMeta.duration,
|
|
834
|
+
coverPath: existsSync(resolvedCover) ? resolvedCover : undefined,
|
|
835
|
+
},
|
|
836
|
+
};
|
|
837
|
+
sted('write', prepPath, () => writeJsonText(prepPath, merged));
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function dailyPublishSurfdash(args: string[]): void {
|
|
842
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
843
|
+
const prep = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
844
|
+
const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
845
|
+
timed(workDir, 'publish-surfdash', () => {
|
|
846
|
+
if (targetPublished(result, 'surfdash-pub')) {
|
|
847
|
+
console.log(`publish-surfdash cached: surfdash target ok in ${result}`);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
kk(
|
|
851
|
+
[
|
|
852
|
+
'executor',
|
|
853
|
+
'fan-out',
|
|
854
|
+
'--content',
|
|
855
|
+
prep,
|
|
856
|
+
'--target',
|
|
857
|
+
'surfdash-pub',
|
|
858
|
+
'--out',
|
|
859
|
+
result,
|
|
860
|
+
...pluginsArgs(pluginsPath),
|
|
861
|
+
],
|
|
862
|
+
{
|
|
863
|
+
unset: ['SPUR_WORKFLOW_RUN_ACTIVE'],
|
|
864
|
+
soft: true,
|
|
865
|
+
},
|
|
866
|
+
);
|
|
867
|
+
console.log('publish-surfdash: surfdash target dispatched; classification happens after podcast publish');
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function dailyShowNotes(args: string[]): void {
|
|
872
|
+
const [workDir = '', runDate = '', pluginsPath = '', title = ''] = args;
|
|
873
|
+
const article = join(workDir, '2-article', `${runDate}_06_article_content.json`);
|
|
874
|
+
// The preceding `publish-surfdash` state writes this artifact; `_17_publish_result.json` does
|
|
875
|
+
// not exist yet at `show-notes` time (the later `publish` state merges it), so reading it left
|
|
876
|
+
// PODCAST_PUB_ARTICLE_POST_PATH permanently empty (task 0137).
|
|
877
|
+
const result = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
878
|
+
const out = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
879
|
+
const plan = join(workDir, '2-plan', `${runDate}_03_plan_plan.json`);
|
|
880
|
+
timed(workDir, 'show-notes', () => {
|
|
881
|
+
const effective = readJson(article)?.title;
|
|
882
|
+
const titleArg = title !== '' ? title : typeof effective === 'string' ? effective : '';
|
|
883
|
+
const postPath = firstPostPath(readJson(result), 'surfdash-pub');
|
|
884
|
+
kk(['executor', 'run', 'podcast-pub', '--in', plan, '--out', out, ...pluginsArgs(pluginsPath)], {
|
|
885
|
+
env: {
|
|
886
|
+
PODCAST_PUB_TITLE: titleArg,
|
|
887
|
+
PODCAST_PUB_RUN_DATE: runDate,
|
|
888
|
+
PODCAST_PUB_BRIEF_MODE: 'true',
|
|
889
|
+
PODCAST_PUB_ARTICLE_POST_PATH: postPath,
|
|
890
|
+
},
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** `jq -r '[…select(.target == t and .ok == true) | .metadata.postPath // empty] | first // ""'`. */
|
|
896
|
+
function firstPostPath(parsed: Record<string, unknown> | null, target: string): string {
|
|
897
|
+
const targets = parsed?.targets;
|
|
898
|
+
if (!Array.isArray(targets)) return '';
|
|
899
|
+
for (const entry of targets) {
|
|
900
|
+
if (entry === null || typeof entry !== 'object') continue;
|
|
901
|
+
const record = entry as Record<string, unknown>;
|
|
902
|
+
if (record.target !== target || record.ok !== true) continue;
|
|
903
|
+
const postPath = metadataOf(record).postPath;
|
|
904
|
+
if (typeof postPath === 'string' && postPath !== '') return postPath;
|
|
905
|
+
}
|
|
906
|
+
return '';
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function dailyPublishPodcast(args: string[]): void {
|
|
910
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
911
|
+
const prep = join(workDir, '3-publish', `${runDate}_13_publish-prep_content.json`);
|
|
912
|
+
const notes = join(workDir, '3-publish', `${runDate}_15_show-notes_show-notes.md`);
|
|
913
|
+
const result = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
|
|
914
|
+
timed(workDir, 'publish-podcast', () => {
|
|
915
|
+
if (targetPublished(result, 'podcast-pub')) {
|
|
916
|
+
console.log(`publish-podcast cached: podcast target ok in ${result}`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const content = readJson(prep) ?? {};
|
|
921
|
+
const showNotes = readFileSync(notes, 'utf-8');
|
|
922
|
+
const merged = { ...content, metadata: { ...metadataOf(content), showNotes } };
|
|
923
|
+
sted('write', prep, () => writeJsonText(prep, merged));
|
|
924
|
+
kk(
|
|
925
|
+
[
|
|
926
|
+
'executor',
|
|
927
|
+
'fan-out',
|
|
928
|
+
'--content',
|
|
929
|
+
prep,
|
|
930
|
+
'--target',
|
|
931
|
+
'podcast-pub',
|
|
932
|
+
'--out',
|
|
933
|
+
result,
|
|
934
|
+
...pluginsArgs(pluginsPath),
|
|
935
|
+
],
|
|
936
|
+
{
|
|
937
|
+
unset: ['SPUR_WORKFLOW_RUN_ACTIVE'],
|
|
938
|
+
soft: true,
|
|
939
|
+
},
|
|
940
|
+
);
|
|
941
|
+
} catch (error) {
|
|
942
|
+
// The shell's `… && … || true` swallowed both steps' failures — but an unusable `kk`
|
|
943
|
+
// (exit 127) is an environment failure, not a tolerated plugin failure.
|
|
944
|
+
if (error instanceof StageFailure && error.status === 127) throw error;
|
|
945
|
+
console.error((error as Error).message);
|
|
946
|
+
}
|
|
947
|
+
console.log('publish-podcast: podcast target dispatched');
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function dailyPublishClassify(args: string[]): void {
|
|
952
|
+
const [workDir = '', runDate = ''] = args;
|
|
953
|
+
const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
|
|
954
|
+
const statusFile = join(workDir, '3-publish', `${runDate}_17_publish_publish-status.txt`);
|
|
955
|
+
const surfdash = join(workDir, '3-publish', `${runDate}_14_publish-surfdash_result.json`);
|
|
956
|
+
const podcast = join(workDir, '3-publish', `${runDate}_16_publish-podcast_result.json`);
|
|
957
|
+
timed(workDir, 'publish', () => {
|
|
958
|
+
// Both per-target files must exist: the pre-0133 `jq -s … || echo '[]'` chain could not read
|
|
959
|
+
// a missing one (jq exits 2 after printing the slurped partial), which drove status to `none`.
|
|
960
|
+
// Classifying over whichever file happens to exist would report `full` for a publish that
|
|
961
|
+
// never dispatched — the failure masking this rule exists to prevent.
|
|
962
|
+
const bothWritten = isFile(surfdash) && isFile(podcast);
|
|
963
|
+
const targets = bothWritten ? [...targetsOf(surfdash), ...targetsOf(podcast)] : [];
|
|
964
|
+
const okCount = targets.filter((entry) => entry.ok === true && entry.status === 'published').length;
|
|
965
|
+
const total = targets.length;
|
|
966
|
+
const status = okCount > 0 && okCount === total ? 'full' : okCount > 0 ? 'partial' : 'none';
|
|
967
|
+
sted('write', result, () => writeJqText(result, { ok: okCount === total && total > 0, targets }));
|
|
968
|
+
sted('write', statusFile, () => writeRaw(statusFile, status));
|
|
969
|
+
console.log(`publish: status=${status} (${okCount}/${total} targets published); routing is transition-owned`);
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function targetsOf(path: string): Record<string, unknown>[] {
|
|
974
|
+
const targets = readJson(path)?.targets;
|
|
975
|
+
return Array.isArray(targets)
|
|
976
|
+
? (targets.filter((entry) => entry !== null && typeof entry === 'object') as Record<string, unknown>[])
|
|
977
|
+
: [];
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function dailyPublishPartial(args: string[]): void {
|
|
981
|
+
const [workDir = '', runDate = ''] = args;
|
|
982
|
+
const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
|
|
983
|
+
timed(workDir, 'publish-partial', () => {
|
|
984
|
+
const failed = targetsOf(result)
|
|
985
|
+
.filter((entry) => entry.ok !== true || entry.status !== 'published')
|
|
986
|
+
.map((entry) => `${entry.target ?? ''}`)
|
|
987
|
+
.join(',');
|
|
988
|
+
const list = failed === '' ? '(unparsed)' : failed;
|
|
989
|
+
console.error(`WARNING: partial publish — failed targets: ${list}`);
|
|
990
|
+
sted('mkdir', '.spur/run', () => mkdirSync('.spur/run', { recursive: true }));
|
|
991
|
+
sted('write', `.spur/run/publish-partial-${runDate}.txt`, () =>
|
|
992
|
+
writeFileSync(
|
|
993
|
+
`.spur/run/publish-partial-${runDate}.txt`,
|
|
994
|
+
`${runDate} partial publish: failed targets: ${list} (result.json preserved for kk executor fan-out --retry-from)\n`,
|
|
995
|
+
),
|
|
996
|
+
);
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function dailyRunReport(args: string[]): void {
|
|
1001
|
+
const [
|
|
1002
|
+
workDir = '',
|
|
1003
|
+
runDate = '',
|
|
1004
|
+
pluginsPath = '',
|
|
1005
|
+
minQuality = '',
|
|
1006
|
+
minImportance = '',
|
|
1007
|
+
minUrgency = '',
|
|
1008
|
+
minImpact = '',
|
|
1009
|
+
] = args;
|
|
1010
|
+
const reportMd = join(workDir, `${runDate}_19_run-report_report.md`);
|
|
1011
|
+
const reportJson = join(workDir, `${runDate}_19_run-report_report.json`);
|
|
1012
|
+
const cands = join(workDir, '2-plan', `${runDate}_19_run-report_episode-plan.json`);
|
|
1013
|
+
const candidates = join(workDir, '2-plan', `${runDate}_04_quality-control-content_candidates.json`);
|
|
1014
|
+
timed(workDir, 'run-report', () => {
|
|
1015
|
+
if (isFile(reportMd)) {
|
|
1016
|
+
console.log(`run report cached: ${reportMd}`);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
if (!isFile(candidates)) {
|
|
1020
|
+
console.log('run-report: no candidates.json (run ended below plan step) — skip');
|
|
1021
|
+
return;
|
|
1022
|
+
}
|
|
1023
|
+
sted('write', cands, () => writeJqText(cands, metadataOf(readJson(candidates)).docs ?? []));
|
|
1024
|
+
kk(['executor', 'run', 'news-report-gen', '--in', cands, '--out', reportJson, ...pluginsArgs(pluginsPath)], {
|
|
1025
|
+
env: {
|
|
1026
|
+
QC_MIN_QUALITY: minQuality,
|
|
1027
|
+
QC_MIN_IMPORTANCE: minImportance,
|
|
1028
|
+
QC_MIN_URGENCY: minUrgency,
|
|
1029
|
+
QC_MIN_IMPACT: minImpact,
|
|
1030
|
+
NEWS_REPORT_REJECTED_FILE: join(
|
|
1031
|
+
workDir,
|
|
1032
|
+
'2-plan',
|
|
1033
|
+
`${runDate}_04_quality-control-content_candidates.rejected.json`,
|
|
1034
|
+
),
|
|
1035
|
+
NEWS_REPORT_TIMING_FILE: join(workDir, 'step-timing.json'),
|
|
1036
|
+
NEWS_REPORT_DATE: runDate,
|
|
1037
|
+
},
|
|
1038
|
+
});
|
|
1039
|
+
const body = readJson(reportJson)?.body;
|
|
1040
|
+
if (typeof body !== 'string') failWith(1, `run-report: ${reportJson} has no string body`);
|
|
1041
|
+
sted('write', reportMd, () => writeRaw(reportMd, body));
|
|
1042
|
+
console.log(`run report written: ${reportMd}`);
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function dailyTranslateEn(args: string[]): void {
|
|
1047
|
+
const [workDir = '', runDate = '', pluginsPath = ''] = args;
|
|
1048
|
+
const result = join(workDir, '3-publish', `${runDate}_17_publish_result.json`);
|
|
1049
|
+
const zhFile = join(workDir, '3-publish', `${runDate}_20_translate-en_zh-post-path.txt`);
|
|
1050
|
+
const enFile = join(workDir, '3-publish', `${runDate}_20_translate-en_en-post-path.txt`);
|
|
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', () => {
|
|
1054
|
+
const postPath = firstPostPath(readJson(result), 'surfdash-pub');
|
|
1055
|
+
const zh = postPath.endsWith('index.md') ? postPath : '';
|
|
1056
|
+
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
|
+
if (zh === '') {
|
|
1087
|
+
console.log('no zh post path — skip translate-ja');
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
if (isFile(ja)) {
|
|
1091
|
+
console.log(`ja draft cached: ${ja}`);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
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
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function dailyTranslateSync(args: string[]): void {
|
|
1104
|
+
const [workDir = '', runDate = '', pluginsPath = '', zhPostPath = ''] = args;
|
|
1105
|
+
const sync = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync.json`);
|
|
1106
|
+
const syncResult = join(workDir, '3-publish', `${runDate}_24_translate-sync_sync-result.json`);
|
|
1107
|
+
timed(workDir, 'translate-sync', () => {
|
|
1108
|
+
if (zhPostPath === '') {
|
|
1109
|
+
console.log('no zh post path — skip translate-sync');
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
sted('write', sync, () =>
|
|
1113
|
+
writeJqText(sync, { body: '', options: { operation: 'sync', postPath: zhPostPath } }),
|
|
1114
|
+
);
|
|
1115
|
+
kk(['executor', 'run', 'surfdash-pub', '--in', sync, '--out', syncResult, ...pluginsArgs(pluginsPath)]);
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const [stage, ...rest] = process.argv.slice(2);
|
|
1120
|
+
|
|
1121
|
+
switch (stage) {
|
|
1122
|
+
case 'prepare-itc':
|
|
1123
|
+
prepareItc(rest);
|
|
1124
|
+
break;
|
|
1125
|
+
case 'prepare-solo':
|
|
1126
|
+
prepareSolo(rest);
|
|
1127
|
+
break;
|
|
1128
|
+
case 'prepare-storm':
|
|
1129
|
+
prepareStorm(rest);
|
|
1130
|
+
break;
|
|
1131
|
+
case 'duration':
|
|
1132
|
+
duration(rest);
|
|
1133
|
+
break;
|
|
1134
|
+
case 'validate': {
|
|
1135
|
+
const { override, values } = parseStage(rest, { in: { type: 'string' } });
|
|
1136
|
+
if (!values.in) fail('usage: kk-workflow-stages.ts validate [<override>] --in <voicescript.yaml>');
|
|
1137
|
+
delegate(requireSidecar(override, VALIDATE_SIDECAR, 'validate_script'), ['--in', values.in]);
|
|
1138
|
+
break;
|
|
1139
|
+
}
|
|
1140
|
+
case 'wrap': {
|
|
1141
|
+
const { override, values } = parseStage(rest, {
|
|
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
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
delegate(requireSidecar(override, WRAP_SIDECAR, 'wrap_script'), [values.in, values.out, values.profile ?? '']);
|
|
1152
|
+
break;
|
|
1153
|
+
}
|
|
1154
|
+
case 'render-storm': {
|
|
1155
|
+
const { override, values } = parseStage(rest, { in: { type: 'string' }, out: { type: 'string' } });
|
|
1156
|
+
if (!values.in || !values.out) {
|
|
1157
|
+
fail('usage: kk-workflow-stages.ts render-storm [<override>] --in <content.json> --out <content.md>');
|
|
1158
|
+
}
|
|
1159
|
+
delegate(requireSidecar(override, RENDER_SIDECAR, 'render_script'), ['--in', values.in, '--out', values.out]);
|
|
1160
|
+
break;
|
|
1161
|
+
}
|
|
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
|
+
}
|