@gobing-ai/knowledge-kit 0.1.8 → 0.1.9
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/dist/board/collect/index.js +725 -1101
- package/dist/board/daily-voice/index.js +722 -101
- package/dist/board/itc/index.js +13 -0
- package/dist/index.js +1792 -102
- package/package.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +24 -1
- package/plugins/ingestions/horizon-ingest/src/index.ts +34 -0
- package/plugins/kk/config.example.yaml +8 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/publish-presets/daily-ai-voice.json +123 -0
- package/plugins/kk/schedules/kk-daily-publish3-cn-peak.cron.tmpl +20 -0
- package/plugins/kk/schedules/kk-daily-publish3-overseas.cron.tmpl +17 -0
- package/plugins/kk/scripts/daily-stages.ts +580 -78
- package/plugins/kk/scripts/kk-workflow-stages.ts +11 -1
- package/plugins/kk/scripts/topic-boundary.json +18 -83
- package/plugins/kk/workflows/kk-collect.yaml +2 -0
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +75 -6
- package/plugins/kk/workflows/kk-daily-ai-voice3.yaml +842 -0
- package/plugins/kk/workflows/kk-daily-publish3.yaml +282 -0
- package/plugins/publishings/podcast-pub/README.md +65 -0
- package/plugins/publishings/podcast-pub/dist/index.js +1709 -274
- package/plugins/publishings/podcast-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/src/backfill.ts +465 -0
- package/plugins/publishings/podcast-pub/src/index.ts +211 -34
- package/plugins/publishings/podcast-pub/src/microfeed-client.ts +32 -3
- package/plugins/publishings/podcast-pub/src/seo.ts +1081 -0
- package/plugins/publishings/podcast-pub/src/show-notes.ts +71 -6
- package/plugins/publishings/surfdash-pub/dist/index.js +66 -5
- package/plugins/publishings/surfdash-pub/src/index.ts +94 -10
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kk-daily-ai-voice3
|
|
3
|
+
kind: dag
|
|
4
|
+
description: >-
|
|
5
|
+
The generate half of voice3 (design docs/design/kk-daily-ai-voice3.md §4, task 0324): topic →
|
|
6
|
+
QC/translate → drafts → content safety → operator review → audio ∥ images → quality, then the
|
|
7
|
+
generate-side channel payloads (`social-write` + `daily-bundle-variants`) and the frozen
|
|
8
|
+
`bundle.json` that `kk-daily-publish3` consumes. It publishes nothing: every channel decision and
|
|
9
|
+
every send lives in the publish preset and the publish workflow, which is why this graph carries
|
|
10
|
+
no `xhs_*` / `wechat_*` / `publish_enabled` switch. voice2 stays the production path until the
|
|
11
|
+
operator swaps the 04:30 schedule entry (§8).
|
|
12
|
+
|
|
13
|
+
vars:
|
|
14
|
+
|
|
15
|
+
# ---- voice3 additions (task 0324 R4) ----
|
|
16
|
+
# `auto_publish` launches publish3 nested when true; off by default exactly like voice2's
|
|
17
|
+
# publish_enabled, and the operator turns it on in the schedule entry after the shadow period.
|
|
18
|
+
# Card-series authoring/rendering (billed image generation). The cards ROW in the publish preset
|
|
19
|
+
# is `off` by default, so this stays false until an operator turns both on.
|
|
20
|
+
cards_enabled: "false"
|
|
21
|
+
# Card count (voice2's `xhs_cards`, minus the channel name): the card plan targets this many.
|
|
22
|
+
cards: "6"
|
|
23
|
+
auto_publish: "false"
|
|
24
|
+
publish_window: "after-generate"
|
|
25
|
+
publish_preset: ""
|
|
26
|
+
publish_dummy: "false"
|
|
27
|
+
# Composed by the `prepare` node (`kk daily prepare`), then read back into vars through the
|
|
28
|
+
# single-node read chain below — shell actions cannot set vars, and a concurrent `setVars` merge
|
|
29
|
+
# is last-writer-wins, so the DAG exchanges data by fixed paths under work_dir, never by vars.
|
|
30
|
+
work_dir: ""
|
|
31
|
+
# Digits-only run-date stamp the artifact names carry; projected from <work_dir>/run-date.txt.
|
|
32
|
+
run_date: ""
|
|
33
|
+
# Store extract window (kk store extract --window). The old workflow's ingestion window was
|
|
34
|
+
# intrinsic to the fan-in; here it is the pool's freshness bound (kk-collect refreshes every 6h).
|
|
35
|
+
window: "24h"
|
|
36
|
+
language: "zh"
|
|
37
|
+
voice_profile: "robin-news"
|
|
38
|
+
# Voice driver switch — voxcpm (voxcpm-tts) | gemini (gemini-tts) | voicebox (voice-gen)
|
|
39
|
+
# | ominivoice (omni-voice-gen). The generate tier below mirrors the fallback order, and the
|
|
40
|
+
# fit/duration gates take their spoken-char rate from this var — switching drivers here is enough.
|
|
41
|
+
# 2026-10-20 (operator decision): voxcpm STAYS the primary. It is audibly echoey - a 40 ms periodic
|
|
42
|
+
# reflection on renders longer than ~30 s plus a pause floor only 16-22 dB below speech, measured by
|
|
43
|
+
# `kk stage audio-quality` on every episode's raw model output since 10-15 (cepstrum 40 ms @ 26-31 dB
|
|
44
|
+
# / pauseFloor -16..-22 dB, versus gemini's 20-25 ms @ 19-20 dB / -34..-37 dB) - and no exposed knob
|
|
45
|
+
# (cfg_value, inference_timesteps, normalize, denoise) or usable checkpoint fixes it (VoxCPM1.5:
|
|
46
|
+
# 80 ms @ 31 dB; VoxCPM-0.5B loses the lattice but measures -15.5 dB at 16 kHz). The operator keeps
|
|
47
|
+
# the local driver for its speed and cost and will monitor it episode by episode until a proper fix
|
|
48
|
+
# exists; flipping this one var to "gemini" is the escape hatch (measured dry: 20-25 ms / -34..-37 dB)
|
|
49
|
+
# at the price of a 1h47m-2h21m remote render.
|
|
50
|
+
vdriver: "voxcpm"
|
|
51
|
+
intro_music: "~/.config/kk/voices/AI每日资讯_8秒播客片头.mp3"
|
|
52
|
+
surfdash_root: "/Users/robin/xprojects/surfdash"
|
|
53
|
+
title: ""
|
|
54
|
+
skip_review: "false"
|
|
55
|
+
transcode_mp3: "true"
|
|
56
|
+
podcast_artist: "Robin Min"
|
|
57
|
+
podcast_album: "咸话咸说"
|
|
58
|
+
cover_enabled: "true"
|
|
59
|
+
cover_provider: "minimax"
|
|
60
|
+
cover_model: "image-01"
|
|
61
|
+
cover_fallback: ""
|
|
62
|
+
# 0219 R3: image-gen vendor fallback for the XHS render; "" restores strict fail-fast.
|
|
63
|
+
xhs_fallback: "true"
|
|
64
|
+
# 2026-10-21 operator decision: OFF for now (was on since 2026-09-29). The plugin reported today's
|
|
65
|
+
# draft as saved (`草稿箱` 12 -> 13 plus a scoped `保存成功` toast) while the console showed the
|
|
66
|
+
# operator no draft, so the channel's success evidence is not trustworthy until that is resolved.
|
|
67
|
+
# Card-image fallback to the aspect-only request when the exact canvas is refused.
|
|
68
|
+
# launch-time switch and nothing else in the run depends on this target.
|
|
69
|
+
xhs_style: "xhs-knowledge"
|
|
70
|
+
xhs_provider: "minimax"
|
|
71
|
+
xhs_model: "image-01"
|
|
72
|
+
# The 合集 tag a channel payload carries is that channel's preset `options` (the shipped preset
|
|
73
|
+
# pins 「每日AI资讯」), not a generate-side var: voice3 does not know which channels exist.
|
|
74
|
+
plan_max_items: "20"
|
|
75
|
+
plan_translate_enabled: "true"
|
|
76
|
+
article_write_enabled: "true"
|
|
77
|
+
script_write_enabled: "true"
|
|
78
|
+
plugins_path: ""
|
|
79
|
+
# Content-safety budget and platforms, forwarded to the nested kk-daily-safety state machine.
|
|
80
|
+
safety_max_revise: "2"
|
|
81
|
+
safety_platforms: "xiaoyuzhou,apple-podcasts,wechatmp"
|
|
82
|
+
agent: "inline"
|
|
83
|
+
# 0238 R2: executor for the `-retry` siblings of the two hard-fail agent.run producers. The
|
|
84
|
+
# siblings declare `role: reviewer` (capable-1), two rungs above the primary's `scribe`, so the
|
|
85
|
+
# default `inline` resolves the retry above any rung the primary reaches by climbing past an
|
|
86
|
+
# unusable cheap executor (ac5-live-1). The operator may still pin a known-good executor here.
|
|
87
|
+
agent_retry: "inline"
|
|
88
|
+
# QC pins — lockstep with episode-plan-gen's DEFAULT_FILTER_CONFIG (env wins), same as voice1.
|
|
89
|
+
qc_categories: "ai-news,ai-technical,ai-research,ai-industry"
|
|
90
|
+
qc_min_quality: "2"
|
|
91
|
+
qc_min_importance: "3"
|
|
92
|
+
qc_min_urgency: "2"
|
|
93
|
+
qc_min_impact: "1"
|
|
94
|
+
qc_min_episode_items: "8"
|
|
95
|
+
episode_min_minutes: "15"
|
|
96
|
+
# 2026-10-09 operator decision: ingest bodies are spoken in full and today's pools render 22-46
|
|
97
|
+
# minutes at the current item counts, so the 20-minute ceiling refused real episodes before any
|
|
98
|
+
# TTS request. The pair drives the pre-spend estimates (`daily-qc-content`'s pool gate, and
|
|
99
|
+
# `daily-generate`'s script gate, which measures the authored VoiceScript) and the measured gate
|
|
100
|
+
# in `daily-episode-duration`.
|
|
101
|
+
episode_max_minutes: "25"
|
|
102
|
+
# Run id, injected by spur at launch (0674 R5/R8) — the selections / ledger / mark-published
|
|
103
|
+
# identity. A shell command may only reference declared vars.
|
|
104
|
+
__runId: ""
|
|
105
|
+
# Executed definition digest, injected by spur at launch (G8 §9). The Board capture compares it
|
|
106
|
+
# with the named definition and records the topology only on exact agreement.
|
|
107
|
+
__definitionDigest: ""
|
|
108
|
+
|
|
109
|
+
# One action per node (engine: DagNodeDef.action), no back-edges (ADR-031). A node whose deps are
|
|
110
|
+
# ALL skipped is skipped; a done+skipped mix proceeds — which is how the empty-pool path still
|
|
111
|
+
# reaches run-report and ledger (they list `extract` in dependsOn, G11 R3).
|
|
112
|
+
#
|
|
113
|
+
# NODES ARE DECLARED IN TOPOLOGICAL ORDER (every node after all of its `dependsOn`). The engine
|
|
114
|
+
# settles skips in one sweeping pass over this list, in declaration order, and stops as soon as a
|
|
115
|
+
# pass finds no ready node — a node declared before a dependency that becomes `skipped` later in
|
|
116
|
+
# that same pass stays `pending` and the run ends `dag-unreachable-nodes` instead of `done`.
|
|
117
|
+
#
|
|
118
|
+
# Fail-fast is the other half: any failed node fails the run, so every non-blocking channel (xhs,
|
|
119
|
+
# wechat, locales) keeps its exit-0-on-soft-failure kk stage. `onError: continue` sits where voice1 had
|
|
120
|
+
# it (cover-brief, script-write) and, since 0238 R2, on the two hard-fail agent.run producers
|
|
121
|
+
# (plan-translate, article-write), whose `-retry` siblings below carry the loud second attempt.
|
|
122
|
+
# Wave-level waiting is the accepted ceiling (design §6): a node starts when its own deps settle,
|
|
123
|
+
# but its whole wave is awaited before the next wave is dispatched.
|
|
124
|
+
nodes:
|
|
125
|
+
- id: prepare
|
|
126
|
+
description: >-
|
|
127
|
+
Sync the workflow ledger and compose today's workspace: `kk daily prepare` writes the effective
|
|
128
|
+
work_dir to .spur/runs/kk-daily-ai-voice2/work-dir.txt, the run date to <work_dir>/run-date.txt,
|
|
129
|
+
the artifact directories and the safety revise budget. `kk ledger sync` records runs that
|
|
130
|
+
already finished without a ledger row (failed ones included) — a fail-fast DAG never reaches
|
|
131
|
+
its own `ledger` node, so the next run's prepare is the backfill (design §7).
|
|
132
|
+
The `daily-preflight` stage runs FIRST: the scheduler executes this checkout's YAML while
|
|
133
|
+
`kk stage` resolves its stage runner from the installed package, so a run whose YAML names a
|
|
134
|
+
stage the installed runner lacks must fail here, naming the stage, instead of 29 minutes later
|
|
135
|
+
inside the nested safety machine (2026-10-20: `unknown stage: daily-safety-verdict`, nothing
|
|
136
|
+
published).
|
|
137
|
+
action:
|
|
138
|
+
kind: shell
|
|
139
|
+
options:
|
|
140
|
+
command: >-
|
|
141
|
+
kk stage daily-preflight plugins/kk/workflows/kk-daily-ai-voice2.yaml
|
|
142
|
+
plugins/kk/workflows/kk-daily-safety.yaml
|
|
143
|
+
&& kk daily prepare --workflow kk-daily-ai-voice2 --run-date "${vars.run_date}" --ledger-sync
|
|
144
|
+
--run "${vars.__runId}" --definition-digest "${vars.__definitionDigest}"
|
|
145
|
+
--setting window="${vars.window}" --setting language="${vars.language}" --setting vdriver="${vars.vdriver}"
|
|
146
|
+
--setting skip_review="${vars.skip_review}" --setting transcode_mp3="${vars.transcode_mp3}"
|
|
147
|
+
--setting cover_enabled="${vars.cover_enabled}" --setting plan_translate_enabled="${vars.plan_translate_enabled}"
|
|
148
|
+
--setting article_write_enabled="${vars.article_write_enabled}"
|
|
149
|
+
--setting script_write_enabled="${vars.script_write_enabled}" --setting plan_max_items="${vars.plan_max_items}"
|
|
150
|
+
--setting safety_max_revise="${vars.safety_max_revise}" --setting qc_min_quality="${vars.qc_min_quality}"
|
|
151
|
+
--setting qc_min_importance="${vars.qc_min_importance}" --setting qc_min_urgency="${vars.qc_min_urgency}"
|
|
152
|
+
--setting qc_min_impact="${vars.qc_min_impact}" --setting qc_min_episode_items="${vars.qc_min_episode_items}"
|
|
153
|
+
--setting episode_min_minutes="${vars.episode_min_minutes}" --setting episode_max_minutes="${vars.episode_max_minutes}"
|
|
154
|
+
|
|
155
|
+
- id: read-work-dir
|
|
156
|
+
description: "Project the effective work_dir into vars (single-node wave: nothing has fanned out yet)"
|
|
157
|
+
dependsOn: [prepare]
|
|
158
|
+
action:
|
|
159
|
+
kind: file.read.into-var
|
|
160
|
+
options:
|
|
161
|
+
path: .spur/runs/kk-daily-ai-voice2/work-dir.txt
|
|
162
|
+
var: work_dir
|
|
163
|
+
|
|
164
|
+
- id: read-run-date
|
|
165
|
+
description: "Project the digits-only run-date stamp into vars (single-node wave, after work_dir is known)"
|
|
166
|
+
dependsOn: [read-work-dir]
|
|
167
|
+
action:
|
|
168
|
+
kind: file.read.into-var
|
|
169
|
+
options:
|
|
170
|
+
path: ${vars.work_dir}/run-date.txt
|
|
171
|
+
var: run_date
|
|
172
|
+
|
|
173
|
+
# 20261012: a dead browser session must be named BEFORE the run's ~40 minutes of generation, not
|
|
174
|
+
# after. On 2026-10-12 the saved MP session was refused server-side (unexpired cookies, but the
|
|
175
|
+
# console served its QR login and /cgi-bin/home carried no token), so `wechat-publish` failed soft
|
|
176
|
+
# at the very end and the operator only learned it by noticing the missing draft. A leaf off the
|
|
177
|
+
# head chain on purpose: `extract` keeps `read-run-date` as its only dependency, so the head chain
|
|
178
|
+
# stays single-node waves, and nothing depends on this node — it warns, it never routes.
|
|
179
|
+
|
|
180
|
+
- id: session-preflight
|
|
181
|
+
description: >-
|
|
182
|
+
Probe each enabled publish channel's saved browser session up front and WARN when one is
|
|
183
|
+
dead, naming the operator action. The probe set is derived from the publish PRESET (0325), not
|
|
184
|
+
from workflow vars: this workflow no longer knows which channels exist. Warning-only by
|
|
185
|
+
construction (onError: continue) — the channel's own publish receipt stays the authority.
|
|
186
|
+
dependsOn: [read-run-date]
|
|
187
|
+
action:
|
|
188
|
+
kind: shell
|
|
189
|
+
options:
|
|
190
|
+
onError: continue
|
|
191
|
+
command: >-
|
|
192
|
+
kk stage run publish-matrix-check "${vars.work_dir}" --all-windows
|
|
193
|
+
|| echo "session-preflight: a publish channel's saved session was refused — fix it with: kk bot login <channel>"
|
|
194
|
+
|
|
195
|
+
- id: extract
|
|
196
|
+
description: >-
|
|
197
|
+
Select today's episode pool from the kk store: the ai-news topic profile over the labels
|
|
198
|
+
kk-collect wrote, minus every story already selected or published for the ai-news topic (the
|
|
199
|
+
store's `selections` ledger, written by the `select` node below). Writes
|
|
200
|
+
<work_dir>/10-collect/pool.json (the fan-in blended.json shape) and prints {count}. An empty
|
|
201
|
+
pool writes [] and exits 0 — the run's no-content path, not a failure. The `daily-extract`
|
|
202
|
+
stage widens the window (×2, ×3, capped at 72h) only when the configured window's pool cannot
|
|
203
|
+
reach `episode_min_minutes` (2026-10-19): a published episode consumes more stories than a day
|
|
204
|
+
of collection supplies, so the tight window alone closed 2026-10-18/19 as no-content while
|
|
205
|
+
older unselected items sat in the store.
|
|
206
|
+
dependsOn: [read-run-date]
|
|
207
|
+
action:
|
|
208
|
+
kind: shell
|
|
209
|
+
options:
|
|
210
|
+
command: >-
|
|
211
|
+
kk stage daily-extract "${vars.work_dir}" ai-news "${vars.window}" "${vars.episode_min_minutes}" "${vars.vdriver}"
|
|
212
|
+
|
|
213
|
+
- id: plan
|
|
214
|
+
description: >-
|
|
215
|
+
Score/dedup/select + annotate the pool into the episode plan (episode-plan-gen; annotate rides
|
|
216
|
+
inside planEpisode). The labels extract inlined into metadata replace its keyword re-scoring.
|
|
217
|
+
Guarded on the pool: an empty pool skips the whole content subtree, and run-report/ledger
|
|
218
|
+
still run.
|
|
219
|
+
dependsOn: [extract]
|
|
220
|
+
condition:
|
|
221
|
+
kind: shell
|
|
222
|
+
options:
|
|
223
|
+
command: 'kk store pool-count "${vars.work_dir}/10-collect/pool.json" --min 1'
|
|
224
|
+
action:
|
|
225
|
+
kind: shell
|
|
226
|
+
options:
|
|
227
|
+
command: >-
|
|
228
|
+
kk stage daily-plan "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.plan_max_items}" "${vars.plan_translate_enabled}"
|
|
229
|
+
|
|
230
|
+
- id: qc-content
|
|
231
|
+
description: >-
|
|
232
|
+
Content QC: filter the plan's annotated candidates by category allowlist + score thresholds
|
|
233
|
+
(episode-plan-gen --mode filter). Writes 21-plan/<date>_04_quality-control-content_candidates.json,
|
|
234
|
+
whose length guards the whole content fan-out below — a pool that filtered to zero items ends
|
|
235
|
+
the run as no-content instead of building an empty episode. The stage also applies the
|
|
236
|
+
pre-spend floor gate (2026-10-12): a kept pool whose estimated spoken length cannot reach
|
|
237
|
+
`episode_min_minutes` is emptied here, so a thin day closes as no-content instead of paying
|
|
238
|
+
for a synthesis pass that `daily-episode-duration` then refuses.
|
|
239
|
+
dependsOn: [plan]
|
|
240
|
+
action:
|
|
241
|
+
kind: shell
|
|
242
|
+
options:
|
|
243
|
+
command: >-
|
|
244
|
+
kk stage daily-qc-content "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.qc_categories}" "${vars.qc_min_quality}" "${vars.qc_min_importance}" "${vars.qc_min_urgency}" "${vars.qc_min_impact}" "${vars.qc_min_episode_items}" "${vars.episode_min_minutes}" "${vars.vdriver}"
|
|
245
|
+
|
|
246
|
+
- id: select
|
|
247
|
+
description: >-
|
|
248
|
+
Record today's planned set in the store's topic-scoped `selections` ledger (0228). It runs at
|
|
249
|
+
QC, the moment the run has committed to its episode set, so the cross-day exclusion no longer
|
|
250
|
+
depends on the DAG reaching its tail: a run that fails, pauses at review or skips publish still
|
|
251
|
+
excludes what it planned. Guarded on the QC candidates — a pool that filtered to zero items
|
|
252
|
+
selects nothing.
|
|
253
|
+
dependsOn: [qc-content]
|
|
254
|
+
condition:
|
|
255
|
+
kind: shell
|
|
256
|
+
options:
|
|
257
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
258
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
259
|
+
action:
|
|
260
|
+
kind: shell
|
|
261
|
+
options:
|
|
262
|
+
command: >-
|
|
263
|
+
kk store select --topic ai-news --workflow kk-daily-ai-voice2 --run "${vars.__runId}"
|
|
264
|
+
--in "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json"
|
|
265
|
+
|
|
266
|
+
# ---- content branches: translate / prose article / cover / script author (concurrent) ----
|
|
267
|
+
#
|
|
268
|
+
# 0238 R2: `plan-translate` and `article-write` are the two HARD-FAIL agent.run producers — a
|
|
269
|
+
# missing expectFile fails the run (R4 property, kept). The resolved executor is stochastic: a run
|
|
270
|
+
# answered a greeting with zero tool calls and exited 0 (0238 evidence). The engine has no node
|
|
271
|
+
# retry, so each producer carries a `-retry` sibling on a DIFFERENT tier (`agent_retry`, role
|
|
272
|
+
# reviewer) with the SAME input/expectFile; the primary becomes `onError: continue` and the sibling
|
|
273
|
+
# (no onError) is the loud gate. Downstream readers depend on BOTH, so the done+skipped mix of a
|
|
274
|
+
# successful primary still proceeds. Delete the siblings when spur grows an agent.run retry.
|
|
275
|
+
|
|
276
|
+
- id: plan-translate
|
|
277
|
+
description: "Translate the needsTranslation plan items into the output plan.json via agent.run; onError: continue — plan-translate-retry below is the loud gate (0238 R2)"
|
|
278
|
+
dependsOn: [qc-content]
|
|
279
|
+
condition:
|
|
280
|
+
kind: shell
|
|
281
|
+
options:
|
|
282
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
283
|
+
command: >-
|
|
284
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
285
|
+
&& test "${vars.plan_translate_enabled}" = "true"
|
|
286
|
+
&& ! jq -e --argjson n "$(jq '.metadata.docs|length' "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json")" 'type=="array" and length==$n and all(.[];type=="object")' "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
|
|
287
|
+
action:
|
|
288
|
+
kind: agent.run
|
|
289
|
+
# 0238 R2: a stochastic executor no-op must not end the run here — the retry sibling decides.
|
|
290
|
+
onError: continue
|
|
291
|
+
options:
|
|
292
|
+
role: scribe
|
|
293
|
+
agent: ${vars.agent}
|
|
294
|
+
# ADR-115 error tier: keep this prompt <= 1000 chars (0133 R3; 0138 R4a copy discipline).
|
|
295
|
+
# The anchor is shared with plan-translate-retry so the two attempts cannot drift apart.
|
|
296
|
+
# 20261015 forensics: headless agy's terminal sandbox denies workspace writes for EVERY
|
|
297
|
+
# shell command (command() rules are promptless-but-sandboxed; unsandboxed() is dead), so
|
|
298
|
+
# the producer must deliver via the file-write tool; and the condition gate checks JSON
|
|
299
|
+
# validity (array, length == candidates docs, all objects) because a pi token-cap staged
|
|
300
|
+
# write once flowed 4/12 items downstream. Keep both when editing prompt or condition.
|
|
301
|
+
input: &plan-translate-input |
|
|
302
|
+
Read ${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json; metadata.docs (Doc[]) is the plan.
|
|
303
|
+
For needsTranslation items adapt "title", "body" and "metadata.brief" into spoken simplified Chinese — keep every fact, number, name, quote, hedge (口播全长改编,不摘要不省略), body ≈0.6–0.7× source.
|
|
304
|
+
Titles read as news; keep product/model names and body URLs verbatim, use 「」 not ASCII quotes.
|
|
305
|
+
Copy discipline: reader-facing news copy — no scaffold labels (「可做角度」「为什么现在值得注意」「社区讨论」「背景」「潜在影响」「影响」), no hedging labels (「读者宜另行核实」「仍待验证」) — merge or drop.
|
|
306
|
+
Keep id/sourceUri/mediaType/metadata as-is; no comments/trailing commas.
|
|
307
|
+
Deliver with your file-write tool (headless agy sandbox blocks shell writes); the file must end as ONE valid JSON array — no split markers/placeholders.
|
|
308
|
+
Output the JSON array — same length and order — to ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
|
|
309
|
+
expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
|
|
310
|
+
allowAppend: false
|
|
311
|
+
|
|
312
|
+
- id: plan-translate-retry
|
|
313
|
+
description: >-
|
|
314
|
+
Bounded recovery for plan-translate (0238 R2): runs only when the primary settled without a
|
|
315
|
+
VALID expectFile (missing or contract-violating JSON), retries the same input on a different
|
|
316
|
+
tier, and carries NO onError — a second miss fails the run loudly instead of letting an
|
|
317
|
+
untranslated or truncated plan flow downstream.
|
|
318
|
+
dependsOn: [plan-translate]
|
|
319
|
+
condition:
|
|
320
|
+
kind: shell
|
|
321
|
+
options:
|
|
322
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
323
|
+
command: >-
|
|
324
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
325
|
+
&& test "${vars.plan_translate_enabled}" = "true"
|
|
326
|
+
&& ! jq -e --argjson n "$(jq '.metadata.docs|length' "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json")" 'type=="array" and length==$n and all(.[];type=="object")' "${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json"
|
|
327
|
+
action:
|
|
328
|
+
kind: agent.run
|
|
329
|
+
options:
|
|
330
|
+
role: reviewer
|
|
331
|
+
agent: ${vars.agent_retry}
|
|
332
|
+
input: *plan-translate-input
|
|
333
|
+
expectFile: ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json
|
|
334
|
+
allowAppend: false
|
|
335
|
+
|
|
336
|
+
- id: article-write
|
|
337
|
+
description: "Write the tech-sharing prose article from the translated plan via agent.run (3-format split: surfdash gets prose, not a news list); onError: continue — article-write-retry below is the loud gate (0238 R2)"
|
|
338
|
+
dependsOn: [qc-content, plan-translate, plan-translate-retry]
|
|
339
|
+
condition:
|
|
340
|
+
kind: shell
|
|
341
|
+
options:
|
|
342
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
343
|
+
command: >-
|
|
344
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
345
|
+
&& test "${vars.article_write_enabled}" = "true"
|
|
346
|
+
&& ! test -s "${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md"
|
|
347
|
+
action:
|
|
348
|
+
kind: agent.run
|
|
349
|
+
# 0238 R2: a stochastic executor no-op must not end the run here — the retry sibling decides.
|
|
350
|
+
onError: continue
|
|
351
|
+
options:
|
|
352
|
+
role: scribe
|
|
353
|
+
agent: ${vars.agent}
|
|
354
|
+
# ADR-115 error tier: keep this prompt <= 1000 chars. No title line: the canonical episode
|
|
355
|
+
# title is computed by daily-article-gen (0189 R4).
|
|
356
|
+
# The anchor is shared with article-write-retry so the two attempts cannot drift apart.
|
|
357
|
+
input: &article-write-input |
|
|
358
|
+
Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — metadata.docs (Doc[]) is the translated plan; don't edit it.
|
|
359
|
+
Write vivid simplified-Chinese tech news for 咸话咸说之每日AI乱炖 (column 每日AI乱炖, host Robin). No title line. Open with exactly「这里是由Robin为你带来的咸话咸说之每日AI乱炖」。Never「我是咸话咸说」or an AI-production footer. Each ## section: what happened, why it matters, one concrete detail — a human reporter, not a telegram. Inline [来源:<label>](<sourceUri>). Close by tying the items together.
|
|
360
|
+
选题边界:只写技术内容;某条主线若属政治/外交或贸易管制/军事/公共政策争议/社会公共事件,跳过该条并在文末加一行「> 注:<N> 条条目因选题边界未写入:<id>(<原因>)、<id>(<原因>)」。
|
|
361
|
+
事实纪律:每个事实取自该条计划内容,不虚构、不引他处;数字与主体不得放大;引语只能归属其原句主语;保留「据报道/称/承诺」等限定词;导语/收束不加计划外程度词;材料不足就少写或不写,不凑数。
|
|
362
|
+
Narrative register — smooth transitions, no list-like bulleting, no scaffold labels (「可做角度」「背景」「潜在影响」), no hedging (「读者宜另行核实」「仍待验证」). 「」 quotes.
|
|
363
|
+
Output the markdown to ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md.
|
|
364
|
+
expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
|
|
365
|
+
allowAppend: false
|
|
366
|
+
|
|
367
|
+
- id: article-write-retry
|
|
368
|
+
description: >-
|
|
369
|
+
Bounded recovery for article-write (0238 R2): runs only when the primary settled without its
|
|
370
|
+
expectFile, retries the same input on a different tier, and carries NO onError — a second miss
|
|
371
|
+
fails the run loudly instead of publishing an article nobody wrote.
|
|
372
|
+
dependsOn: [article-write]
|
|
373
|
+
condition:
|
|
374
|
+
kind: shell
|
|
375
|
+
options:
|
|
376
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
377
|
+
command: >-
|
|
378
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
379
|
+
&& test "${vars.article_write_enabled}" = "true"
|
|
380
|
+
&& ! test -s "${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md"
|
|
381
|
+
action:
|
|
382
|
+
kind: agent.run
|
|
383
|
+
options:
|
|
384
|
+
role: reviewer
|
|
385
|
+
agent: ${vars.agent_retry}
|
|
386
|
+
input: *article-write-input
|
|
387
|
+
expectFile: ${vars.work_dir}/22-article/${vars.run_date}_06_article-write_body.md
|
|
388
|
+
allowAppend: false
|
|
389
|
+
|
|
390
|
+
- id: article
|
|
391
|
+
description: >-
|
|
392
|
+
Generate the dated daily article Content from the episode plan (daily-article-gen template).
|
|
393
|
+
Depends on the QC candidates (so the prose/translate nodes can be disabled without skipping
|
|
394
|
+
this node: a done dependency in the mix keeps it runnable) as well as on both authoring nodes
|
|
395
|
+
AND their retry siblings, and it carries the same candidates guard — a pool that filtered to
|
|
396
|
+
zero items must not build an empty article.
|
|
397
|
+
dependsOn: [qc-content, plan-translate, plan-translate-retry, article-write, article-write-retry]
|
|
398
|
+
condition:
|
|
399
|
+
kind: shell
|
|
400
|
+
options:
|
|
401
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
402
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
403
|
+
action:
|
|
404
|
+
kind: shell
|
|
405
|
+
options:
|
|
406
|
+
command: >-
|
|
407
|
+
kk stage daily-article "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
|
|
408
|
+
|
|
409
|
+
- id: cover-brief
|
|
410
|
+
description: "Author the cover subject from the lead plan item via agent.run (F614; onError: continue — a bad brief falls back to the format default subject in the render stage)"
|
|
411
|
+
# 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
|
|
412
|
+
# producers settle. `qc-content` stays in the list: a done dependency in the mix keeps this node
|
|
413
|
+
# runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
|
|
414
|
+
dependsOn: [qc-content, plan-translate, plan-translate-retry]
|
|
415
|
+
condition:
|
|
416
|
+
kind: shell
|
|
417
|
+
options:
|
|
418
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
419
|
+
command: >-
|
|
420
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
421
|
+
&& test "${vars.cover_enabled}" = "true"
|
|
422
|
+
&& test ! -f "${vars.work_dir}/23-cover/${vars.run_date}_07_cover_cover.png"
|
|
423
|
+
action:
|
|
424
|
+
kind: agent.run
|
|
425
|
+
# A bad or failed brief must never halt the run: the stage falls back to the format's default
|
|
426
|
+
# subject and keeps the same pinned format/style/provider/model.
|
|
427
|
+
onError: continue
|
|
428
|
+
options:
|
|
429
|
+
role: scribe
|
|
430
|
+
agent: ${vars.agent}
|
|
431
|
+
# ADR-115 error tier: keep this prompt <= 1000 chars.
|
|
432
|
+
input: |
|
|
433
|
+
Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of plan docs; don't edit it.
|
|
434
|
+
Take item 1 only (its "title" and "metadata.brief") — the episode's lead story. If item 1's mainline is public affairs (politics/diplomacy, trade controls, military, policy disputes, social events), take the first in-scope item instead.
|
|
435
|
+
Write one concrete visual scene in English, ≤240 characters, that a viewer would connect to that story: objects, settings, symbols, metaphor.
|
|
436
|
+
No text, letters, numbers, logos, brand marks, flags, or recognizable real people. No style, color, lighting, or composition words — the preset owns those.
|
|
437
|
+
Output strictly valid JSON {"subject": "..."} to ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json.
|
|
438
|
+
expectFile: ${vars.work_dir}/23-cover/${vars.run_date}_07_cover-brief.json
|
|
439
|
+
allowAppend: false
|
|
440
|
+
|
|
441
|
+
- id: cover
|
|
442
|
+
description: "Render the daily cover through the pinned daily-news presets (F614)"
|
|
443
|
+
dependsOn: [cover-brief]
|
|
444
|
+
action:
|
|
445
|
+
kind: shell
|
|
446
|
+
options:
|
|
447
|
+
command: >-
|
|
448
|
+
kk stage daily-cover "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.cover_provider}" "${vars.cover_model}" "${vars.cover_fallback}"
|
|
449
|
+
|
|
450
|
+
- id: cover-normalize
|
|
451
|
+
description: "Normalize the rendered cover (dimensions/format pins) so every consumer sees the same asset"
|
|
452
|
+
dependsOn: [cover]
|
|
453
|
+
action:
|
|
454
|
+
kind: shell
|
|
455
|
+
options:
|
|
456
|
+
command: >-
|
|
457
|
+
kk stage daily-cover-normalize "${vars.work_dir}" "${vars.run_date}"
|
|
458
|
+
|
|
459
|
+
- id: script-style
|
|
460
|
+
description: "Snapshot the shipped zh style contract into the work dir (separate node: it is also an audit input for the revision loop)"
|
|
461
|
+
dependsOn: [qc-content]
|
|
462
|
+
condition:
|
|
463
|
+
kind: shell
|
|
464
|
+
options:
|
|
465
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
466
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
467
|
+
action:
|
|
468
|
+
kind: shell
|
|
469
|
+
options:
|
|
470
|
+
command: >-
|
|
471
|
+
kk stage daily-script-style "${vars.work_dir}" "${vars.run_date}"
|
|
472
|
+
|
|
473
|
+
- id: script-write
|
|
474
|
+
description: "Author the per-item script plan (intro/outro + lead/insight per doc id) from the plan via agent.run; onError: continue — a failed plan falls back to templates (F613)"
|
|
475
|
+
# 0238 R3: reads 21-plan/<date>_03_plan_plan.json, so it must not start before the plan
|
|
476
|
+
# producers settle. `script-style` stays first in the list: a done dependency in the mix keeps
|
|
477
|
+
# this node runnable when the plan-authoring nodes are disabled (all-skipped deps skip a node).
|
|
478
|
+
dependsOn: [script-style, plan-translate, plan-translate-retry]
|
|
479
|
+
condition:
|
|
480
|
+
kind: shell
|
|
481
|
+
options:
|
|
482
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
483
|
+
command: >-
|
|
484
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
485
|
+
&& test "${vars.script_write_enabled}" = "true"
|
|
486
|
+
&& test "${vars.language}" = "zh"
|
|
487
|
+
&& ! test -s "${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json"
|
|
488
|
+
action:
|
|
489
|
+
kind: agent.run
|
|
490
|
+
onError: continue
|
|
491
|
+
options:
|
|
492
|
+
role: scribe
|
|
493
|
+
agent: ${vars.agent}
|
|
494
|
+
# ADR-115 error tier: keep this prompt <= 1000 chars.
|
|
495
|
+
input: |
|
|
496
|
+
Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of docs; don't edit it. Also read the style contract ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_style.md and obey its char caps, duration budget and banned phrases.
|
|
497
|
+
For each doc write a `lead` (a spoken bridge, not containing the title) and an `insight` (a take, not a recap: the narration just read that doc's facts — never restate its facts, numbers or wording; give only your angle, judgment or what-to-watch, no new facts, keep hedges(据报道), no new degree words).
|
|
498
|
+
intro/outro hook only in-scope items; no greeting or sign-off — the builder adds both. 选题边界:政治/外交、贸易管制、军事、公共政策争议、社会公共事件条目绝不复述;读文章 22-article/*_06_article-write_body.md,其文末注记点名的 id 一并进 excluded,各配中文原因。
|
|
499
|
+
Output only JSON {"version":1,"intro":"…","outro":"…","items":[{"id":"…","lead":"…","insight":"…"}],"excluded":[{"id":"…","reason":"…"}]} to ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json. Exactly one `items` entry per doc not in `excluded`.
|
|
500
|
+
expectFile: ${vars.work_dir}/24-script/${vars.run_date}_08_script-write_plan.json
|
|
501
|
+
allowAppend: false
|
|
502
|
+
|
|
503
|
+
- id: script
|
|
504
|
+
description: >-
|
|
505
|
+
Compile the episode plan Doc[] into structured broadcast VoiceScript YAML Content. Depends on
|
|
506
|
+
`article` so the node still runs when the plan-authoring nodes were disabled or skipped (the
|
|
507
|
+
builder falls back to templates) — but not when the whole content branch is skipped.
|
|
508
|
+
dependsOn: [article, script-style, script-write]
|
|
509
|
+
condition:
|
|
510
|
+
kind: shell
|
|
511
|
+
options:
|
|
512
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
513
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
514
|
+
action:
|
|
515
|
+
kind: shell
|
|
516
|
+
options:
|
|
517
|
+
command: >-
|
|
518
|
+
kk stage daily-script "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.language}" "${vars.title}" "${vars.voice_profile}" "${vars.episode_max_minutes}" "${vars.vdriver}"
|
|
519
|
+
|
|
520
|
+
# ---- content safety: a nested state machine (no sub-workflow action exists; ADR-031) ----
|
|
521
|
+
|
|
522
|
+
- id: safety
|
|
523
|
+
description: >-
|
|
524
|
+
Content safety as a nested state machine (kk-daily-safety.yaml): review → judge → bounded
|
|
525
|
+
revise loop → clean|block. It blocks here (and only here), so a non-zero exit means the drafts
|
|
526
|
+
must not publish — `block` or an integrity failure. The launcher vars the parent run exports
|
|
527
|
+
are stripped at the nested-spur boundary (the same two `NESTED_SPUR_UNSET` vars the stage
|
|
528
|
+
runner strips, kk-workflow-stages.ts): without it `spur workflow run` refuses to start inside
|
|
529
|
+
an active run. voice3 always runs the gate: it is the only check between an LLM draft and a
|
|
530
|
+
published post.
|
|
531
|
+
dependsOn: [script]
|
|
532
|
+
action:
|
|
533
|
+
kind: shell
|
|
534
|
+
options:
|
|
535
|
+
command: >-
|
|
536
|
+
env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
|
|
537
|
+
spur workflow run plugins/kk/workflows/kk-daily-safety.yaml
|
|
538
|
+
--vars "{\"work_dir\":\"${vars.work_dir}\",\"run_date\":\"${vars.run_date}\",\"safety_max_revise\":\"${vars.safety_max_revise}\",\"safety_platforms\":\"${vars.safety_platforms}\",\"agent\":\"${vars.agent}\"}"
|
|
539
|
+
--quiet --no-plan
|
|
540
|
+
|
|
541
|
+
- id: review
|
|
542
|
+
description: >-
|
|
543
|
+
Operator review of the safety-gated drafts: `pause: true` stops the run here and RESUME is the
|
|
544
|
+
approval (the engine marks a paused node done on resume, so this node carries no action);
|
|
545
|
+
cancelling the run is the rejection. Same contract as voice1's HITL confirmation, now a node
|
|
546
|
+
instead of a state. Skipped when skip_review=true or when there is nothing to publish.
|
|
547
|
+
dependsOn: [safety]
|
|
548
|
+
pause: true
|
|
549
|
+
condition:
|
|
550
|
+
kind: shell
|
|
551
|
+
options:
|
|
552
|
+
command: 'test "${vars.skip_review}" != "true"'
|
|
553
|
+
|
|
554
|
+
# ---- audio branch ----
|
|
555
|
+
# wrap-docs must be DECLARED BEFORE generate: the DAG requires nodes in topological order, and
|
|
556
|
+
# generate consumes wrap-docs's _10_wrap-docs_docs.json as its TTS input (0212 dogfood finding —
|
|
557
|
+
# an edge to a later-declared node wedges the dispatcher with generate never dispatched).
|
|
558
|
+
|
|
559
|
+
- id: wrap-docs
|
|
560
|
+
description: "Wrap VoiceScript into Doc[] input for the voice generator plugin (the generate-side variant input)"
|
|
561
|
+
dependsOn: [script, review]
|
|
562
|
+
condition:
|
|
563
|
+
kind: shell
|
|
564
|
+
options:
|
|
565
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
566
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
567
|
+
action:
|
|
568
|
+
kind: shell
|
|
569
|
+
options:
|
|
570
|
+
command: >-
|
|
571
|
+
kk stage daily-wrap-docs "${vars.work_dir}" "${vars.run_date}" "${vars.voice_profile}"
|
|
572
|
+
|
|
573
|
+
- id: generate
|
|
574
|
+
description: "Voice tier, primary driver (vars.vdriver — voxcpm; its 40 ms lattice is known and monitored, see the vdriver comment). onError: continue — the fallback siblings below are the loud chain (same pattern as 0238 R2)."
|
|
575
|
+
# wrap-docs is a data dependency, not a sibling: daily-generate reads its _10_wrap-docs_docs.json
|
|
576
|
+
# as the TTS input. Without this edge the two nodes race in the same wave (0212 dogfood finding).
|
|
577
|
+
dependsOn: [script, safety, review, wrap-docs]
|
|
578
|
+
condition:
|
|
579
|
+
kind: shell
|
|
580
|
+
options:
|
|
581
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
582
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
583
|
+
action:
|
|
584
|
+
kind: shell
|
|
585
|
+
options:
|
|
586
|
+
onError: continue
|
|
587
|
+
command: >-
|
|
588
|
+
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "${vars.vdriver}" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
589
|
+
|
|
590
|
+
# Driver fallback chain (2026-10-15, operator decision): voxcpm -> gemini -> omni. Each sibling
|
|
591
|
+
# runs the same stage with a pinned driver; its condition re-checks the pool AND that NO upstream
|
|
592
|
+
# driver left a valid, passing whole-document Content (the stage stamps metadata.generator with
|
|
593
|
+
# its plugin id and the cache ignores foreign-driver sections, so a failed attempt can neither be
|
|
594
|
+
# relabeled nor mix voices). Non-final siblings continue on error; the last one is loud.
|
|
595
|
+
|
|
596
|
+
- id: generate-fallback
|
|
597
|
+
description: "Voice tier fallback 1: gemini (gemini-tts) when the primary driver left no valid Content — the measured-dry escape hatch from voxcpm's lattice"
|
|
598
|
+
dependsOn: [generate]
|
|
599
|
+
condition:
|
|
600
|
+
kind: shell
|
|
601
|
+
options:
|
|
602
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
603
|
+
command: >-
|
|
604
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
605
|
+
&& ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
|
|
606
|
+
action:
|
|
607
|
+
kind: shell
|
|
608
|
+
options:
|
|
609
|
+
onError: continue
|
|
610
|
+
command: >-
|
|
611
|
+
kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "gemini" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
612
|
+
|
|
613
|
+
- id: generate-fallback2
|
|
614
|
+
description: "Voice tier fallback 2 (last, loud): omni-voice-gen when neither voxcpm nor gemini delivered"
|
|
615
|
+
dependsOn: [generate-fallback]
|
|
616
|
+
condition:
|
|
617
|
+
kind: shell
|
|
618
|
+
options:
|
|
619
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
620
|
+
command: >-
|
|
621
|
+
test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS
|
|
622
|
+
&& ! jq -e '(.metadata.generator == "kk:voxcpm-tts" or .metadata.generator == "kk:gemini-tts" or .metadata.generator == "kk:omni-voice-gen") and .metadata.qc.passed == true' "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
|
|
623
|
+
action:
|
|
624
|
+
kind: shell
|
|
625
|
+
options:
|
|
626
|
+
command: >-
|
|
627
|
+
OMNIVOICE_SPEED=1.06 kk stage daily-generate "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.transcode_mp3}" "ominivoice" "1" "${vars.episode_max_minutes}" "${vars.episode_min_minutes}"
|
|
628
|
+
|
|
629
|
+
- id: intro
|
|
630
|
+
description: "Prepend the 8s intro sting (intro_music var; empty = skip), update duration/audioPath"
|
|
631
|
+
dependsOn: [generate, generate-fallback, generate-fallback2]
|
|
632
|
+
action:
|
|
633
|
+
kind: shell
|
|
634
|
+
options:
|
|
635
|
+
command: >-
|
|
636
|
+
kk stage daily-intro "${vars.work_dir}" "${vars.run_date}" "${vars.intro_music}"
|
|
637
|
+
|
|
638
|
+
# ---- advisory voice audit (2026-10-16): never fails the run ----
|
|
639
|
+
# The 20261015 episode shipped 59 different speakers because an instruct-mode profile pins none,
|
|
640
|
+
# and nothing but a human listening caught it. This grades the final render by per-segment median
|
|
641
|
+
# F0 (a speaker change swings it tens of Hz) and prints the table plus a PASS/FAIL line. It is
|
|
642
|
+
# deliberately advisory: `|| true` keeps its non-zero exit out of the run, and onError covers a
|
|
643
|
+
# missing venv (the check needs librosa from the voxcpm plugin's environment).
|
|
644
|
+
|
|
645
|
+
- id: voice-audit
|
|
646
|
+
description: "Advisory speaker-consistency audit of the joined episode (warn-only, never fails the run)"
|
|
647
|
+
dependsOn: [intro]
|
|
648
|
+
condition:
|
|
649
|
+
kind: shell
|
|
650
|
+
options:
|
|
651
|
+
command: >-
|
|
652
|
+
test -f "${vars.work_dir}/30-audio/${vars.run_date}_11_generate_content.json"
|
|
653
|
+
action:
|
|
654
|
+
kind: shell
|
|
655
|
+
options:
|
|
656
|
+
command: >-
|
|
657
|
+
kk stage daily-voice-audit "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
|
|
658
|
+
onError: continue
|
|
659
|
+
|
|
660
|
+
- id: duration
|
|
661
|
+
description: "Measured duration gate: under episode_min_minutes the run refuses and closes as no-content (refusal artifact + emptied candidates, exit 0 — 0266); above episode_max_minutes it still fails loud. Duration is authoritative."
|
|
662
|
+
dependsOn: [intro]
|
|
663
|
+
action:
|
|
664
|
+
kind: shell
|
|
665
|
+
options:
|
|
666
|
+
command: >-
|
|
667
|
+
kk stage daily-episode-duration "${vars.work_dir}" "${vars.run_date}" "${vars.episode_min_minutes}" "${vars.episode_max_minutes}" "${vars.vdriver}"
|
|
668
|
+
|
|
669
|
+
- id: quality
|
|
670
|
+
description: >-
|
|
671
|
+
Verify audio quality, speech rate, and silence/repetition QC metrics over the assembled
|
|
672
|
+
episode. Depends on the audio tail and wrap-docs: voice3 no longer packages a publish-prep
|
|
673
|
+
Content, so the audited bytes are the generated episode itself.
|
|
674
|
+
dependsOn: [duration, wrap-docs]
|
|
675
|
+
action:
|
|
676
|
+
kind: shell
|
|
677
|
+
options:
|
|
678
|
+
command: >-
|
|
679
|
+
kk stage daily-quality-report "${vars.work_dir}" "${vars.run_date}"
|
|
680
|
+
|
|
681
|
+
- id: show-notes
|
|
682
|
+
description: >-
|
|
683
|
+
Render timeline show notes from the episode plan — content, not publishing: the publish half
|
|
684
|
+
ships them as the episode variant's `metadata.showNotes`, and the link placeholders stay
|
|
685
|
+
unresolved until a send resolves them."
|
|
686
|
+
dependsOn: [quality]
|
|
687
|
+
condition:
|
|
688
|
+
kind: shell
|
|
689
|
+
options:
|
|
690
|
+
# 0324 R3: the pool verdict is `pool-gate`'s token, never a repeated `kk store pool-count`.
|
|
691
|
+
command: 'test "$(cat ".spur/run/${vars.__runId}-pool-gate.status" 2>/dev/null)" = PASS'
|
|
692
|
+
action:
|
|
693
|
+
kind: shell
|
|
694
|
+
options:
|
|
695
|
+
command: >-
|
|
696
|
+
kk stage daily-show-notes "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.title}"
|
|
697
|
+
- id: cards-write
|
|
698
|
+
description: "Plan the card series across the episode's plan items via agent.run; onError: continue — a failed plan skips the card series, never the run"
|
|
699
|
+
dependsOn: [script, review, pool-gate]
|
|
700
|
+
condition:
|
|
701
|
+
kind: shell
|
|
702
|
+
options:
|
|
703
|
+
command: 'test "${vars.cards_enabled}" = "true"'
|
|
704
|
+
action:
|
|
705
|
+
kind: agent.run
|
|
706
|
+
onError: continue
|
|
707
|
+
options:
|
|
708
|
+
role: scribe
|
|
709
|
+
agent: ${vars.agent}
|
|
710
|
+
# ADR-115 error tier: keep this prompt <= 1000 chars.
|
|
711
|
+
input: |
|
|
712
|
+
Read ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json — a JSON array of news docs; don't edit it.
|
|
713
|
+
Plan the whole episode, not the lead item alone. Card 1 is the episode hook.
|
|
714
|
+
Write a card note: "title" (≤20 chars) and "body" (Chinese lead paragraph + 3-5 hashtags) — the stage appends one numbered line per image below it. 选题边界:公共事务主线不写入,只写技术内容。
|
|
715
|
+
Write exactly ${vars.cards} cards in plan order: card 1 "cover"; one "content" card per plan item, up to ${vars.cards} - 2 of them; the last "closing". Each card carries "role", "copy" (Chinese headline ≤12 chars), "points" (2-4 short Chinese bullets), "layout", "subject" (one concrete English visual scene), "sourcePoint" (that item's fact) and "itemId" (its "id").
|
|
716
|
+
Ground every field in that item — no invented facts. No writing, letters, numbers, logos or people; screens stay blank.
|
|
717
|
+
Output strictly valid JSON {"title": "...", "body": "...", "cards": [...]} to ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json.
|
|
718
|
+
expectFile: ${vars.work_dir}/25-xhs/${vars.run_date}_18_xhs_plan.json
|
|
719
|
+
allowAppend: false
|
|
720
|
+
|
|
721
|
+
- id: cards-render
|
|
722
|
+
description: "Render the card series through the pinned xhs-card/xhs-knowledge presets, attach the episode cover last and pair every image with its numbered line (0235 note_scope=episode); onError: continue — the publish node names the missing series and stays soft"
|
|
723
|
+
dependsOn: [cards-write]
|
|
724
|
+
# A skipped cards-write still admits its dependents, so each card node keeps its own switch:
|
|
725
|
+
# without it a run with cards_enabled=false would re-package a stale same-day 25-xhs/ series.
|
|
726
|
+
# The pool guard is `pool-gate`'s alone in voice3 (0324 R3).
|
|
727
|
+
condition:
|
|
728
|
+
kind: shell
|
|
729
|
+
options:
|
|
730
|
+
command: 'test "${vars.cards_enabled}" = "true"'
|
|
731
|
+
action:
|
|
732
|
+
kind: shell
|
|
733
|
+
onError: continue
|
|
734
|
+
options:
|
|
735
|
+
command: >-
|
|
736
|
+
kk stage daily-xhs-render "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}" "${vars.cards}" "${vars.xhs_style}" "${vars.xhs_provider}" "${vars.xhs_model}" "${vars.xhs_fallback}" "episode"
|
|
737
|
+
|
|
738
|
+
# ---- generate-side payloads and the frozen bundle (0324 R2/R3) ----
|
|
739
|
+
|
|
740
|
+
# One guard for the whole pool (0324 R3): every node that used to repeat `kk store pool-count`
|
|
741
|
+
# depends on this one, so a refused pool empties the candidates file once and the rest of the
|
|
742
|
+
# graph skips through the engine's all-deps-skipped rule.
|
|
743
|
+
- id: pool-gate
|
|
744
|
+
description: >-
|
|
745
|
+
The single pool guard (0324 R3): `kk store pool-count --min 1` over the candidates file that
|
|
746
|
+
`qc-content` wrote, recorded once as a token at `.spur/run/<runId>-pool-gate.status`. voice2
|
|
747
|
+
repeated the same guard on twenty-one conditions. A token, not a bare dependency, because the
|
|
748
|
+
engine skips a node only when ALL its dependencies were skipped — so a failed gate beside a
|
|
749
|
+
done dependency would still admit the node. Everything downstream reads the token.
|
|
750
|
+
dependsOn: [qc-content]
|
|
751
|
+
action:
|
|
752
|
+
kind: shell
|
|
753
|
+
options:
|
|
754
|
+
command: >-
|
|
755
|
+
OUT=".spur/run/${vars.__runId}-pool-gate.status";
|
|
756
|
+
if kk store pool-count "${vars.work_dir}/21-plan/${vars.run_date}_04_quality-control-content_candidates.json" --min 1;
|
|
757
|
+
then echo PASS > "$OUT";
|
|
758
|
+
else echo EMPTY > "$OUT"; exit 1; fi
|
|
759
|
+
|
|
760
|
+
- id: social-write
|
|
761
|
+
description: >-
|
|
762
|
+
Author one short post per social platform (x, weibo, threads) from the approved plan, in zh,
|
|
763
|
+
inside each platform's character cap. `daily-bundle-variants` truncates as a backstop, so a
|
|
764
|
+
missed cap degrades rather than fails; a missing file simply skips that platform's variant.
|
|
765
|
+
dependsOn: [quality]
|
|
766
|
+
action:
|
|
767
|
+
kind: agent.run
|
|
768
|
+
onError: continue
|
|
769
|
+
options:
|
|
770
|
+
role: scribe
|
|
771
|
+
agent: ${vars.agent}
|
|
772
|
+
input: |
|
|
773
|
+
Read the approved episode plan at ${vars.work_dir}/21-plan/${vars.run_date}_03_plan_plan.json.
|
|
774
|
+
Write three short zh posts, one per file:
|
|
775
|
+
- ${vars.work_dir}/26-social/x.md — at most 280 weighted characters (this feed counts CJK as 2, so ~140 汉字 or 280 Latin letters)
|
|
776
|
+
- ${vars.work_dir}/26-social/weibo.md — at most 2000 characters
|
|
777
|
+
- ${vars.work_dir}/26-social/threads.md — at most 500 characters
|
|
778
|
+
Each post: the day's single most interesting item, one concrete number or name from the
|
|
779
|
+
plan, no hashtag spam, no emoji, no "点击链接", no invented facts. Plain text, no markdown
|
|
780
|
+
headings. Deliver with your file-write tool; each file must be non-empty.
|
|
781
|
+
expectFile: ${vars.work_dir}/26-social/x.md
|
|
782
|
+
|
|
783
|
+
- id: bundle-variants
|
|
784
|
+
description: >-
|
|
785
|
+
Build the channel payloads from the run's own artifacts (0327 builders): `30-variants/<key>`
|
|
786
|
+
for article / episode / cards plus the channel-specific keys and the social posts, all with
|
|
787
|
+
`{{article_url}}` / `{{podcast_url}}` placeholders because nothing is published yet.
|
|
788
|
+
dependsOn: [quality, social-write]
|
|
789
|
+
action:
|
|
790
|
+
kind: shell
|
|
791
|
+
options:
|
|
792
|
+
command: >-
|
|
793
|
+
kk stage run daily-bundle-variants "${vars.work_dir}" "${vars.run_date}" "${vars.plugins_path}"
|
|
794
|
+
|
|
795
|
+
- id: bundle-freeze
|
|
796
|
+
description: >-
|
|
797
|
+
Freeze `bundle.json` (0321 `BundleSchema`): every `30-variants/*` file with its sha256 plus the
|
|
798
|
+
episode title, so the publish half can run later, or three times a day, against exactly these
|
|
799
|
+
bytes. Fails when the article or episode variant is missing — a bundle never advertises what
|
|
800
|
+
generate did not produce.
|
|
801
|
+
dependsOn: [bundle-variants]
|
|
802
|
+
action:
|
|
803
|
+
kind: shell
|
|
804
|
+
options:
|
|
805
|
+
command: >-
|
|
806
|
+
kk stage run publish-matrix-bundle "${vars.work_dir}" --run-date "${vars.run_date}" --run-id "${vars.__runId}"
|
|
807
|
+
|
|
808
|
+
- id: generation-report
|
|
809
|
+
description: "The generate half's report (the publish half writes its own, per window)."
|
|
810
|
+
dependsOn: [bundle-freeze]
|
|
811
|
+
action:
|
|
812
|
+
kind: shell
|
|
813
|
+
options:
|
|
814
|
+
command: >-
|
|
815
|
+
kk daily run-report --work-dir "${vars.work_dir}" --run-date "${vars.run_date}" --skip-store
|
|
816
|
+
|
|
817
|
+
- id: publish
|
|
818
|
+
description: >-
|
|
819
|
+
Nested `kk-daily-publish3` launch when `auto_publish=true` (0324 R4): the after-generate
|
|
820
|
+
window against the bundle this run just froze. The launcher vars are stripped at the nested
|
|
821
|
+
boundary (the same two voice2 strips for the safety machine), and the nested run's exit code is
|
|
822
|
+
surfaced — a failed publish window is a failed generate run's last step, never a silent one.
|
|
823
|
+
dependsOn: [generation-report]
|
|
824
|
+
condition:
|
|
825
|
+
kind: shell
|
|
826
|
+
options:
|
|
827
|
+
command: 'test "${vars.auto_publish}" = "true"'
|
|
828
|
+
action:
|
|
829
|
+
kind: shell
|
|
830
|
+
options:
|
|
831
|
+
command: >-
|
|
832
|
+
env -u SPUR_WORKFLOW_RUN_ACTIVE -u SPUR_EXPECTED_DEFINITION_DIGEST
|
|
833
|
+
spur workflow run plugins/kk/workflows/kk-daily-publish3.yaml
|
|
834
|
+
--vars "{\"work_dir\":\"${vars.work_dir}\",\"run_date\":\"${vars.run_date}\",\"window\":\"${vars.publish_window}\",\"preset\":\"${vars.publish_preset}\",\"dummy\":\"${vars.publish_dummy}\"}"
|
|
835
|
+
|
|
836
|
+
- id: ledger
|
|
837
|
+
description: "Append this run to the workflow ledger; bookkeeping never fails a generation."
|
|
838
|
+
dependsOn: [generation-report, publish]
|
|
839
|
+
action:
|
|
840
|
+
kind: shell
|
|
841
|
+
options:
|
|
842
|
+
command: 'kk ledger append --workflow kk-daily-ai-voice3 --work-dir "${vars.work_dir}" --run "${vars.__runId}" || true'
|