@gobing-ai/knowledge-kit 0.1.5 → 0.1.7
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/index.js +20 -1
- package/package.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +3 -1
- package/plugins/generations/daily-article-gen/src/index.ts +5 -3
- package/plugins/generations/voxcpm-tts/README.md +31 -0
- package/plugins/generations/voxcpm-tts/scripts/probe-long-render.py +134 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/scripts/audio-quality.ts +635 -0
- package/plugins/kk/scripts/daily-stages.ts +51 -1
- package/plugins/kk/scripts/kk-workflow-stages.ts +25 -1
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +3 -1
- package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +20 -7
- package/plugins/publishings/surfdash-pub/dist/index.js +30 -3
- package/plugins/publishings/surfdash-pub/src/index.ts +33 -6
- package/plugins/publishings/surfdash-pub/src/patch.ts +4 -0
- package/plugins/kk/workflows/feature-verification.yaml +0 -89
package/dist/index.js
CHANGED
|
@@ -51661,7 +51661,8 @@ async function createItemDecisionAsker(options = {}) {
|
|
|
51661
51661
|
const injected = options.hub ?? await loadInjectedHub();
|
|
51662
51662
|
const fallback = options.fallback ?? (injected === undefined ? (decisionId, input, maker) => runDecisionSubprocess(decisionId, input, maker, fs) : undefined);
|
|
51663
51663
|
const hub = injected ?? await (await import("@gobing-ai/ts-ai-decision")).createDecisionHub({
|
|
51664
|
-
catalogs: [catalogPath]
|
|
51664
|
+
catalogs: [catalogPath],
|
|
51665
|
+
builtins: { makerOptions: { fetch: bufferedDecisionFetch } }
|
|
51665
51666
|
});
|
|
51666
51667
|
const backendFailures = new Map;
|
|
51667
51668
|
return {
|
|
@@ -51696,6 +51697,24 @@ async function createItemDecisionAsker(options = {}) {
|
|
|
51696
51697
|
}
|
|
51697
51698
|
};
|
|
51698
51699
|
}
|
|
51700
|
+
function createBufferedDecisionFetch(inner = fetch) {
|
|
51701
|
+
return async (input, init) => {
|
|
51702
|
+
const response = await inner(input, init);
|
|
51703
|
+
const text = await response.text();
|
|
51704
|
+
const buffered = {
|
|
51705
|
+
ok: response.ok,
|
|
51706
|
+
status: response.status,
|
|
51707
|
+
statusText: response.statusText,
|
|
51708
|
+
headers: response.headers,
|
|
51709
|
+
body: null,
|
|
51710
|
+
text: async () => text,
|
|
51711
|
+
json: async () => JSON.parse(text),
|
|
51712
|
+
clone: () => buffered
|
|
51713
|
+
};
|
|
51714
|
+
return buffered;
|
|
51715
|
+
};
|
|
51716
|
+
}
|
|
51717
|
+
var bufferedDecisionFetch = createBufferedDecisionFetch();
|
|
51699
51718
|
function runDecisionSubprocess(decisionId, input, maker, fs = createNodeFileSystem()) {
|
|
51700
51719
|
const command = process.env.KK_SPUR_BIN ?? "spur";
|
|
51701
51720
|
const scratch = join21(tmpdir3(), `kk-decision-${randomUUID2()}`);
|
package/package.json
CHANGED
|
@@ -12399,12 +12399,14 @@ ${items}
|
|
|
12399
12399
|
`;
|
|
12400
12400
|
}
|
|
12401
12401
|
var AI_NEWS_TAG = "ai-news";
|
|
12402
|
+
var AI_DAILY_SUB_CATEGORY_TAG = "sub-category:ai-daily";
|
|
12402
12403
|
function tagsFor(docs) {
|
|
12403
12404
|
const existing = docs.flatMap((d) => {
|
|
12404
12405
|
const meta = d.metadata;
|
|
12405
12406
|
return Array.isArray(meta?.tags) ? meta.tags.filter((t) => typeof t === "string") : [];
|
|
12406
12407
|
});
|
|
12407
|
-
|
|
12408
|
+
const mandatory = [AI_NEWS_TAG, AI_DAILY_SUB_CATEGORY_TAG];
|
|
12409
|
+
return [...new Set([...mandatory, ...existing.filter((t) => !mandatory.includes(t))])];
|
|
12408
12410
|
}
|
|
12409
12411
|
var LEADING_H1_RE = /^#(?!#)\s*[^\n]+\n+/.source;
|
|
12410
12412
|
var NOTE_EXCLUDED_RE = /\u672A\u5199\u5165[\uFF1A:](.*)/g;
|
|
@@ -122,16 +122,18 @@ export function renderDailyArticle(docs: Doc[], date: string): string {
|
|
|
122
122
|
return `# ${episodeTitle(date)}\n\n${intro}\n\n${items}\n`;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
/** Single-source daily
|
|
125
|
+
/** Single-source daily tags: stamped on every Content this generator emits (task 0119 + sub-category series tag). */
|
|
126
126
|
const AI_NEWS_TAG = 'ai-news';
|
|
127
|
+
const AI_DAILY_SUB_CATEGORY_TAG = 'sub-category:ai-daily';
|
|
127
128
|
|
|
128
|
-
/** Merge existing item tags (deduped) with the mandatory `ai-news`
|
|
129
|
+
/** Merge existing item tags (deduped) with the mandatory `ai-news` and `sub-category:ai-daily` tags, leading positions. */
|
|
129
130
|
function tagsFor(docs: Doc[]): string[] {
|
|
130
131
|
const existing = docs.flatMap((d) => {
|
|
131
132
|
const meta = d.metadata as Record<string, unknown> | undefined;
|
|
132
133
|
return Array.isArray(meta?.tags) ? meta.tags.filter((t): t is string => typeof t === 'string') : [];
|
|
133
134
|
});
|
|
134
|
-
|
|
135
|
+
const mandatory = [AI_NEWS_TAG, AI_DAILY_SUB_CATEGORY_TAG];
|
|
136
|
+
return [...new Set([...mandatory, ...existing.filter((t) => !mandatory.includes(t))])];
|
|
135
137
|
}
|
|
136
138
|
|
|
137
139
|
/** A leading H1 line — agent bodies used to carry their own title; the canonical title is computed (task 0189 R4). */
|
|
@@ -79,6 +79,24 @@ that evidence belongs to task 0258.
|
|
|
79
79
|
> Grade any render with `scripts/audit-speaker-consistency.py` (needs this plugin's venv):
|
|
80
80
|
> a per-item F0 spread over 45 Hz means the profile is not anchoring a speaker.
|
|
81
81
|
|
|
82
|
+
### Known artifact: a 40 ms echo lattice on long renders (2026-10-20)
|
|
83
|
+
|
|
84
|
+
A `reference`-anchored profile renders short text clean and long text with a 40 ms (≈25 Hz) periodic
|
|
85
|
+
reflection plus a pause floor only 16–22 dB below speech — heard as 「回声感、瓮声瓮气」. Measured on the
|
|
86
|
+
raw model output (no ffmpeg join, no mp3): `kk stage audio-quality` reports `cepstrum 40 ms @ 27–31 dB`
|
|
87
|
+
for renders >30 s, `pause floor −16…−22 dB`, while a 10 s render of the same profile/text measures
|
|
88
|
+
`22 ms @ 19 dB / −22.5 dB` and the same days' `gemini-tts` episodes measure `20–25 ms @ 19–20 dB /
|
|
89
|
+
−34…−37 dB`. `inference_timesteps`, `cfg_value` and `normalize` do not move it (measured 2026-10-20).
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# reproducible in ~2.5 min, no episode needed:
|
|
93
|
+
plugins/generations/voxcpm-tts/.venv/bin/python \
|
|
94
|
+
plugins/generations/voxcpm-tts/scripts/probe-long-render.py --out /tmp/voxcpm-probe
|
|
95
|
+
kk stage audio-quality /tmp/voxcpm-probe/long-baseline.wav
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Treat a driver/profile change as unproven until a LONG render is measured, not a single sentence.
|
|
99
|
+
|
|
82
100
|
## Control policy
|
|
83
101
|
|
|
84
102
|
`normalize` maps directly; an explicit `seed` calls `torch.manual_seed` before synthesis (the
|
|
@@ -90,3 +108,16 @@ one for every segment, derived from its `emotion`, and the anchored clip already
|
|
|
90
108
|
A per-request `engine`,
|
|
91
109
|
`personality=true` or an active `effects_chain` is rejected. Speed stays in shared postprocessing.
|
|
92
110
|
Transcription is omitted, so ASR-dependent QC is reported unavailable (not passing evidence).
|
|
111
|
+
|
|
112
|
+
#### API/library claim sources and confidence
|
|
113
|
+
|
|
114
|
+
The externally changeable claims this file makes about the installed `voxcpm` package, each with the
|
|
115
|
+
artifact it was read from. This block is enforced by `structure/api-library-claim-citation` (the file
|
|
116
|
+
is in that rule's `--require-block` registry), so a HIGH claim without a citation fails the gate.
|
|
117
|
+
|
|
118
|
+
| Claim | Source | Local verification | Confidence |
|
|
119
|
+
| --- | --- | --- | --- |
|
|
120
|
+
| `reference_wav_path` is accepted only by VoxCPM2 checkpoints; any other model id refuses it, so a `reference` profile is VoxCPM2-only while older checkpoints clone through `prompt_wav_path` + `prompt_text` | installed `voxcpm` package | `plugins/generations/voxcpm-tts/.venv/lib/python3.12/site-packages/voxcpm/core.py:239` (`ValueError("reference_wav_path is only supported with VoxCPM2 models")`); reproduced with `scripts/probe-long-render.py --model openbmb/VoxCPM1.5` | HIGH |
|
|
121
|
+
| `generate()`/`_generate()` exposes `cfg_value` (default 2.0), `inference_timesteps` (10), `normalize`, `denoise` and `max_len`, and this plugin passes none of them — the profile and text are the only inputs it sets | installed `voxcpm` package plus this plugin's worker | `…/site-packages/voxcpm/core.py:180`–`:193` (signature and defaults) and `python/worker.py:252`–`:262` (the three `generate(...)` call shapes) | HIGH |
|
|
122
|
+
| `VOXCPM_MODEL` defaults to `openbmb/VoxCPM2`, the model self-reports its output rate, and that rate is what the WAV header carries (no resampling in this plugin) | this plugin's worker | `python/worker.py:181` (`environ.get("VOXCPM_MODEL", "openbmb/VoxCPM2")`), `:207` (`int(model.tts_model.sample_rate)`) and `:264` (`write_pcm16_wav(dest, pcm, sample_rate)`) | HIGH |
|
|
123
|
+
| A `reference` profile pins one speaker (median-pitch swing 22–26 Hz, sd 9–12 Hz) while `instruct` samples a new speaker per request (141 Hz swing), so multi-segment work needs a reference or continuation profile | measured on this machine, 2026-10-15, three texts | `scripts/audit-speaker-consistency.py` grades a render; the 2026-10-15 figures are the ones its docstring records | MEDIUM |
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Reproduce the long-form voxcpm "echo / hollow" artifact in ~2.5 minutes.
|
|
3
|
+
|
|
4
|
+
Why this exists: the 2026-10-20 operator report ("回声感、瓮声瓮气") is not reproducible on a short
|
|
5
|
+
sentence — a 10 s render comes out clean — but it IS reproducible on a paragraph of ~450 Chinese
|
|
6
|
+
characters, which is the length the daily pipeline actually renders per section. Measured 2026-10-20
|
|
7
|
+
with `kk stage audio-quality` (real-cepstrum echo probe + pause floor):
|
|
8
|
+
|
|
9
|
+
short render (~10 s) cepstrum 22 ms @ 19 dB pause floor -22.5 dB clean
|
|
10
|
+
long render (~72 s) cepstrum 40 ms @ 30 dB pause floor -16.1 dB reproduced
|
|
11
|
+
shipped 2026-10-20 §00 cepstrum 40 ms @ 23 dB pause floor -15.7 dB reproduced
|
|
12
|
+
|
|
13
|
+
So the artifact is duration-dependent, and a knob experiment has to use a LONG text or it measures
|
|
14
|
+
nothing. The same run measured `inference_timesteps=20` (29.6 dB) and `cfg_value=2.5` (31.5 dB) against
|
|
15
|
+
the default's 30 dB: none of the exposed sampler knobs remove the 40 ms periodicity.
|
|
16
|
+
|
|
17
|
+
Usage (needs the plugin venv):
|
|
18
|
+
|
|
19
|
+
plugins/generations/voxcpm-tts/.venv/bin/python \
|
|
20
|
+
plugins/generations/voxcpm-tts/scripts/probe-long-render.py [--out /tmp/voxcpm-probe]
|
|
21
|
+
|
|
22
|
+
kk stage audio-quality /tmp/voxcpm-probe/long-baseline.wav # then read cepstrum + pauseFloor
|
|
23
|
+
|
|
24
|
+
Options: `--text-file <path>` (default: the built-in paragraph below), `--ref <wav>` (default: the
|
|
25
|
+
operator's `robin-news` clip), `--steps N`, `--cfg F`, `--normalize`, `--name <label>`.
|
|
26
|
+
|
|
27
|
+
The verdict vocabulary lives in `plugins/kk/scripts/audio-quality.ts` (`kk stage audio-quality`);
|
|
28
|
+
this script only renders.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import argparse
|
|
34
|
+
import contextlib
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
import time
|
|
38
|
+
import wave
|
|
39
|
+
|
|
40
|
+
DEFAULT_TEXT = (
|
|
41
|
+
"大家好,今天是2026年10月20日,星期二。这里是由Robin为你带来的咸话咸说之每日AI乱炖。"
|
|
42
|
+
"今天聊两件跟动手有关的事,一位自称几乎不懂游戏开发的作者,靠编程智能体把一款手机端的即时战略游戏"
|
|
43
|
+
"一路做到能离线运行;另一件是 Google 开源了让 PyTorch 跑上 TPU 的项目。两条线索都指向同一件事:"
|
|
44
|
+
"工具链的边界在移动,真正决定成败的不是模型榜单,而是谁能把验收口径写清楚。"
|
|
45
|
+
"先说游戏那条,作者的说法很朴素,他不写引擎,只把需求拆成可以验证的小步,让智能体一步一步改,"
|
|
46
|
+
"每一步都跑一次构建,坏了自己回滚。这种做法听起来笨,但它把不确定性压到了单步之内,"
|
|
47
|
+
"出问题的时候能立刻定位到底层接口还是生成逻辑。我更在意的是这套流程对工程团队的启示:"
|
|
48
|
+
"当生成成本降到几乎为零,稀缺的就不是代码,而是判断哪一步值得交给机器、哪一步必须留给人验。"
|
|
49
|
+
)
|
|
50
|
+
DEFAULT_REF = os.path.expanduser("~/.config/kk/voices/robin-news.wav")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> int:
|
|
54
|
+
parser = argparse.ArgumentParser(description="Render one long-form voxcpm sample for echo triage.")
|
|
55
|
+
parser.add_argument("--text-file", default=None, help="UTF-8 text to render (default: built-in paragraph)")
|
|
56
|
+
parser.add_argument("--ref", default=DEFAULT_REF, help="reference clip (default: %(default)s)")
|
|
57
|
+
parser.add_argument("--out", default="/tmp/voxcpm-probe", help="output directory (default: %(default)s)")
|
|
58
|
+
parser.add_argument("--name", default="long-baseline", help="output file stem (default: %(default)s)")
|
|
59
|
+
parser.add_argument("--steps", type=int, default=10, help="inference_timesteps (default 10, the plugin's)")
|
|
60
|
+
parser.add_argument("--cfg", type=float, default=2.0, help="cfg_value (default 2.0, the plugin's)")
|
|
61
|
+
parser.add_argument("--normalize", action="store_true", help="pass normalize=True")
|
|
62
|
+
parser.add_argument(
|
|
63
|
+
"--mode",
|
|
64
|
+
choices=["reference", "continuation", "instruct"],
|
|
65
|
+
default="reference",
|
|
66
|
+
help="reference cloning (VoxCPM2 only), prompt continuation, or instruct-only (default reference)",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument("--prompt-text", default=None, help="transcript of --ref, required by --mode continuation")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--denoise",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="load the ZipEnhancer denoiser and pass denoise=True (the plugin ships load_denoiser=False)",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument("--model", default="openbmb/VoxCPM2", help="hub id or local snapshot")
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
text = open(args.text_file, encoding="utf-8").read().strip() if args.text_file else DEFAULT_TEXT
|
|
78
|
+
if not text:
|
|
79
|
+
print("probe: empty text", file=sys.stderr)
|
|
80
|
+
return 2
|
|
81
|
+
if not os.path.isfile(args.ref):
|
|
82
|
+
print(f"probe: reference clip not found: {args.ref}", file=sys.stderr)
|
|
83
|
+
return 2
|
|
84
|
+
os.makedirs(args.out, exist_ok=True)
|
|
85
|
+
|
|
86
|
+
# The model prints its own progress on stdout; keep this script's stdout for the summary.
|
|
87
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
88
|
+
import numpy as np
|
|
89
|
+
import voxcpm
|
|
90
|
+
|
|
91
|
+
model = voxcpm.VoxCPM.from_pretrained(
|
|
92
|
+
args.model, load_denoiser=args.denoise, optimize=False, device=None
|
|
93
|
+
)
|
|
94
|
+
sample_rate = int(model.tts_model.sample_rate)
|
|
95
|
+
|
|
96
|
+
if args.mode == "continuation" and not args.prompt_text:
|
|
97
|
+
print("probe: --mode continuation needs --prompt-text (the transcript of --ref)", file=sys.stderr)
|
|
98
|
+
return 2
|
|
99
|
+
# Only VoxCPM2 accepts `reference_wav_path`; every other checkpoint clones through a prompt pair.
|
|
100
|
+
anchoring = {
|
|
101
|
+
"reference": {"reference_wav_path": args.ref},
|
|
102
|
+
"continuation": {"prompt_wav_path": args.ref, "prompt_text": args.prompt_text or ""},
|
|
103
|
+
"instruct": {},
|
|
104
|
+
}[args.mode]
|
|
105
|
+
|
|
106
|
+
started = time.time()
|
|
107
|
+
with contextlib.redirect_stdout(sys.stderr):
|
|
108
|
+
waveform = model.generate(
|
|
109
|
+
text=text,
|
|
110
|
+
cfg_value=args.cfg,
|
|
111
|
+
inference_timesteps=args.steps,
|
|
112
|
+
normalize=args.normalize,
|
|
113
|
+
denoise=args.denoise,
|
|
114
|
+
**anchoring,
|
|
115
|
+
)
|
|
116
|
+
samples = np.clip(np.asarray(waveform).squeeze(), -1.0, 1.0)
|
|
117
|
+
path = os.path.join(args.out, f"{args.name}.wav")
|
|
118
|
+
with wave.open(path, "wb") as handle:
|
|
119
|
+
handle.setnchannels(1)
|
|
120
|
+
handle.setsampwidth(2)
|
|
121
|
+
handle.setframerate(sample_rate)
|
|
122
|
+
handle.writeframes((samples * 32767).astype(np.int16).tobytes())
|
|
123
|
+
|
|
124
|
+
seconds = len(samples) / sample_rate
|
|
125
|
+
print(
|
|
126
|
+
f"{path} {seconds:.1f}s sr={sample_rate} chars={len(text)} "
|
|
127
|
+
f"mode={args.mode} steps={args.steps} cfg={args.cfg} normalize={args.normalize} denoise={args.denoise} render={time.time() - started:.1f}s"
|
|
128
|
+
)
|
|
129
|
+
print("measure it with: kk stage audio-quality " + path, file=sys.stderr)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
sys.exit(main())
|
package/plugins/kk/plugin.json
CHANGED