@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.
@@ -6,7 +6,7 @@
6
6
  },
7
7
  "plugins": [
8
8
  {
9
- "version": "0.1.5",
9
+ "version": "0.1.7",
10
10
  "name": "kk",
11
11
  "source": "./plugins/kk",
12
12
  "description": "knowledge-kit — a set of toolkit for knowledge management"
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gobing-ai/knowledge-kit",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "An ingest → create → publish content pipeline CLI (Bun).",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
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 tag: stamped on every Content this generator emits (task 0119). */
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` tag, first position. */
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
- return [AI_NEWS_TAG, ...existing.filter((t) => t !== AI_NEWS_TAG)];
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())
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "kk",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "knowledge-kit Claude Code plugin: skills, commands, subagents, hooks, and rules"
5
5
  }