@gobing-ai/knowledge-kit 0.0.37 → 0.1.1

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.
Files changed (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/board/collect/index.css +654 -16
  3. package/dist/board/collect/index.js +191 -64
  4. package/dist/board/daily-voice/index.css +679 -12
  5. package/dist/board/daily-voice/index.js +185 -66
  6. package/dist/board/itc/index.css +679 -13
  7. package/dist/board/itc/index.js +188 -66
  8. package/dist/index.js +2864 -139
  9. package/package.json +3 -2
  10. package/plugins/generations/content-gen/dist/index.js +158 -12
  11. package/plugins/generations/core-facts-gen/dist/index.js +158 -12
  12. package/plugins/generations/daily-article-gen/dist/index.js +158 -12
  13. package/plugins/generations/dailynews-gen/dist/index.js +158 -12
  14. package/plugins/generations/episode-plan-gen/dist/index.js +158 -12
  15. package/plugins/generations/gemini-tts/README.md +4 -3
  16. package/plugins/generations/gemini-tts/dist/index.js +158 -12
  17. package/plugins/generations/image-gen/dist/index.js +158 -12
  18. package/plugins/generations/news-report-gen/dist/index.js +158 -12
  19. package/plugins/generations/omni-voice-gen/README.md +15 -4
  20. package/plugins/generations/omni-voice-gen/profiles.json +1 -2
  21. package/plugins/generations/voice-gen/dist/index.js +158 -12
  22. package/plugins/generations/voxcpm-tts/README.md +21 -1
  23. package/plugins/generations/voxcpm-tts/dist/index.js +173 -13
  24. package/plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py +109 -0
  25. package/plugins/generations/voxcpm-tts/src/voxcpm-client.ts +28 -1
  26. package/plugins/ingestions/aihot-ingest/dist/index.js +158 -12
  27. package/plugins/ingestions/horizon-ingest/dist/index.js +158 -12
  28. package/plugins/ingestions/last30days-ingest/dist/index.js +158 -12
  29. package/plugins/ingestions/web-search/dist/index.js +158 -12
  30. package/plugins/kk/commands/workflow-run.md +56 -18
  31. package/plugins/kk/decisions/kk-itc.yaml +46 -0
  32. package/plugins/kk/decisions/kk-seo-keyword.yaml +57 -0
  33. package/plugins/kk/decisions/kk-seo-page.yaml +230 -0
  34. package/plugins/kk/decisions/kk-seo-pair.yaml +21 -0
  35. package/plugins/kk/decisions/kk-seo-site.yaml +90 -0
  36. package/plugins/kk/plugin.json +1 -1
  37. package/plugins/kk/publish-presets/chinese-article.json +14 -0
  38. package/plugins/kk/publish-presets/technical-article.json +14 -0
  39. package/plugins/kk/references/smart-seo-attribution.md +85 -0
  40. package/plugins/kk/scripts/daily-stages.ts +42 -2
  41. package/plugins/kk/scripts/itc-stages.ts +25 -118
  42. package/plugins/kk/scripts/kk-workflow-stages.ts +38 -1
  43. package/plugins/kk/scripts/seo-decisions.ts +491 -0
  44. package/plugins/kk/scripts/seo-enrich.ts +728 -0
  45. package/plugins/kk/scripts/seo-facts.ts +610 -0
  46. package/plugins/kk/scripts/seo-fetch.ts +766 -0
  47. package/plugins/kk/scripts/seo-report.ts +460 -0
  48. package/plugins/kk/scripts/seo-rules.ts +1882 -0
  49. package/plugins/kk/scripts/seo-stages.ts +1044 -0
  50. package/plugins/kk/skills/article-adapt/SKILL.md +2 -2
  51. package/plugins/kk/skills/smart-seo/SKILL.md +183 -0
  52. package/plugins/kk/skills/smart-seo/references/method.md +126 -0
  53. package/plugins/kk/skills/smart-seo/references/narrative.md +125 -0
  54. package/plugins/kk/skills/smart-seo/references/operations.md +209 -0
  55. package/plugins/kk/skills/smart-seo/references/seo-geo-sources.md +90 -0
  56. package/plugins/kk/workflows/feature-verification.yaml +28 -24
  57. package/plugins/kk/workflows/kk-daily-ai-voice2.yaml +85 -17
  58. package/plugins/kk/workflows/kk-itc-author.yaml +406 -0
  59. package/plugins/kk/workflows/kk-itc.yaml +208 -682
  60. package/plugins/kk/workflows/kk-smart-seo.yaml +251 -0
  61. package/plugins/publishings/bilibili-pub/dist/index.js +21782 -0
  62. package/plugins/publishings/bilibili-pub/dist/profile-check.js +20410 -0
  63. package/plugins/publishings/bilibili-pub/package.json +20 -0
  64. package/plugins/publishings/bilibili-pub/plugin.json +12 -0
  65. package/plugins/publishings/bilibili-pub/src/flow.ts +1296 -0
  66. package/plugins/publishings/bilibili-pub/src/index.ts +139 -0
  67. package/plugins/publishings/bilibili-pub/src/profile-check.ts +19 -0
  68. package/plugins/publishings/bilibili-pub/src/selectors.ts +222 -0
  69. package/plugins/publishings/bilibili-pub/tsconfig.json +4 -0
  70. package/plugins/publishings/cnblogs-pub/dist/index.js +21383 -0
  71. package/plugins/publishings/cnblogs-pub/dist/profile-check.js +20412 -0
  72. package/plugins/publishings/cnblogs-pub/package.json +19 -0
  73. package/plugins/publishings/cnblogs-pub/plugin.json +12 -0
  74. package/plugins/publishings/cnblogs-pub/src/flow.ts +708 -0
  75. package/plugins/publishings/cnblogs-pub/src/index.ts +172 -0
  76. package/plugins/publishings/cnblogs-pub/src/profile-check.ts +24 -0
  77. package/plugins/publishings/cnblogs-pub/src/selectors.ts +104 -0
  78. package/plugins/publishings/cnblogs-pub/tsconfig.json +4 -0
  79. package/plugins/publishings/csdn-pub/dist/index.js +21552 -0
  80. package/plugins/publishings/csdn-pub/dist/profile-check.js +20963 -0
  81. package/plugins/publishings/csdn-pub/package.json +19 -0
  82. package/plugins/publishings/csdn-pub/plugin.json +12 -0
  83. package/plugins/publishings/csdn-pub/src/index.ts +1178 -0
  84. package/plugins/publishings/csdn-pub/src/profile-check.ts +23 -0
  85. package/plugins/publishings/csdn-pub/tsconfig.json +4 -0
  86. package/plugins/publishings/emdash-pub/dist/index.js +158 -12
  87. package/plugins/publishings/emdash-pub/plugin.json +22 -1
  88. package/plugins/publishings/infoq-pub/dist/index.js +160 -12
  89. package/plugins/publishings/infoq-pub/dist/profile-check.js +160 -12
  90. package/plugins/publishings/infoq-pub/plugin.json +21 -0
  91. package/plugins/publishings/jike-pub/dist/index.js +21379 -0
  92. package/plugins/publishings/jike-pub/dist/profile-check.js +20951 -0
  93. package/plugins/publishings/jike-pub/package.json +19 -0
  94. package/plugins/publishings/jike-pub/plugin.json +12 -0
  95. package/plugins/publishings/jike-pub/src/index.ts +800 -0
  96. package/plugins/publishings/jike-pub/src/profile-check.ts +21 -0
  97. package/plugins/publishings/jike-pub/tsconfig.json +4 -0
  98. package/plugins/publishings/juejin-pub/dist/index.js +160 -12
  99. package/plugins/publishings/juejin-pub/dist/profile-check.js +160 -12
  100. package/plugins/publishings/juejin-pub/plugin.json +21 -0
  101. package/plugins/publishings/medium-pub/dist/index.js +158 -12
  102. package/plugins/publishings/medium-pub/plugin.json +22 -1
  103. package/plugins/publishings/podcast-pub/dist/index.js +158 -12
  104. package/plugins/publishings/qiita-pub/dist/index.js +158 -12
  105. package/plugins/publishings/qiita-pub/plugin.json +24 -1
  106. package/plugins/publishings/shipinhao-pub/dist/index.js +21715 -0
  107. package/plugins/publishings/shipinhao-pub/dist/profile-check.js +20431 -0
  108. package/plugins/publishings/shipinhao-pub/package.json +19 -0
  109. package/plugins/publishings/shipinhao-pub/plugin.json +12 -0
  110. package/plugins/publishings/shipinhao-pub/src/flow.ts +1323 -0
  111. package/plugins/publishings/shipinhao-pub/src/index.ts +173 -0
  112. package/plugins/publishings/shipinhao-pub/src/profile-check.ts +25 -0
  113. package/plugins/publishings/shipinhao-pub/src/selectors.ts +241 -0
  114. package/plugins/publishings/shipinhao-pub/tsconfig.json +4 -0
  115. package/plugins/publishings/sspai-pub/dist/index.js +21371 -0
  116. package/plugins/publishings/sspai-pub/dist/profile-check.js +20414 -0
  117. package/plugins/publishings/sspai-pub/package.json +19 -0
  118. package/plugins/publishings/sspai-pub/plugin.json +12 -0
  119. package/plugins/publishings/sspai-pub/src/flow.ts +624 -0
  120. package/plugins/publishings/sspai-pub/src/index.ts +167 -0
  121. package/plugins/publishings/sspai-pub/src/profile-check.ts +25 -0
  122. package/plugins/publishings/sspai-pub/src/selectors.ts +122 -0
  123. package/plugins/publishings/sspai-pub/tsconfig.json +4 -0
  124. package/plugins/publishings/substack-pub/dist/index.js +160 -12
  125. package/plugins/publishings/substack-pub/dist/profile-check.js +160 -12
  126. package/plugins/publishings/substack-pub/plugin.json +21 -0
  127. package/plugins/publishings/surfdash-pub/dist/index.js +158 -12
  128. package/plugins/publishings/surfdash-pub/plugin.json +22 -1
  129. package/plugins/publishings/threads-pub/dist/index.js +158 -12
  130. package/plugins/publishings/wechatmp-pub/dist/index.js +1669 -502
  131. package/plugins/publishings/wechatmp-pub/dist/profile-check.js +204 -21
  132. package/plugins/publishings/wechatmp-pub/plugin.json +32 -0
  133. package/plugins/publishings/wechatmp-pub/src/cgi-draft.ts +540 -0
  134. package/plugins/publishings/wechatmp-pub/src/cgi-media.ts +380 -0
  135. package/plugins/publishings/wechatmp-pub/src/cgi.ts +485 -0
  136. package/plugins/publishings/wechatmp-pub/src/flow.ts +104 -0
  137. package/plugins/publishings/weibo-pub/dist/index.js +160 -12
  138. package/plugins/publishings/weibo-pub/dist/profile-check.js +160 -12
  139. package/plugins/publishings/weibo-pub/plugin.json +35 -0
  140. package/plugins/publishings/x-pub/dist/index.js +160 -12
  141. package/plugins/publishings/x-pub/dist/profile-check.js +160 -12
  142. package/plugins/publishings/x-pub/plugin.json +40 -0
  143. package/plugins/publishings/xhs-pub/dist/index.js +160 -12
  144. package/plugins/publishings/xhs-pub/dist/profile-check.js +160 -12
  145. package/plugins/publishings/xhs-pub/plugin.json +37 -0
  146. package/plugins/publishings/xiaoyuzhou-pub/dist/index.js +21491 -0
  147. package/plugins/publishings/xiaoyuzhou-pub/dist/profile-check.js +20417 -0
  148. package/plugins/publishings/xiaoyuzhou-pub/package.json +19 -0
  149. package/plugins/publishings/xiaoyuzhou-pub/plugin.json +12 -0
  150. package/plugins/publishings/xiaoyuzhou-pub/src/flow.ts +826 -0
  151. package/plugins/publishings/xiaoyuzhou-pub/src/index.ts +174 -0
  152. package/plugins/publishings/xiaoyuzhou-pub/src/profile-check.ts +26 -0
  153. package/plugins/publishings/xiaoyuzhou-pub/src/selectors.ts +125 -0
  154. package/plugins/publishings/xiaoyuzhou-pub/tsconfig.json +4 -0
  155. package/plugins/publishings/zenn-pub/dist/index.js +158 -12
  156. package/plugins/publishings/zenn-pub/plugin.json +22 -1
  157. package/plugins/generations/omni-voice-gen/voices/robin-news.wav +0 -0
@@ -11987,6 +11987,83 @@ import { parseArgs } from "util";
11987
11987
  // ../../plugins/generations/voice-gen/src/index.ts
11988
11988
  import { basename, dirname as dirname2, extname, join as join2, resolve } from "path";
11989
11989
 
11990
+ // ../../packages/kk-core/src/content-limits.ts
11991
+ var PLATFORM_CONTENT_LIMITS = Object.freeze({
11992
+ schemaVersion: 5,
11993
+ tagLimits: Object.freeze({
11994
+ default: 7,
11995
+ imageText: Object.freeze({
11996
+ instagram: 5,
11997
+ douyin: 5,
11998
+ kuaishou: 4,
11999
+ weixin: 1,
12000
+ xiaohongshu: 10
12001
+ }),
12002
+ shortVideo: Object.freeze({
12003
+ douyin: 5,
12004
+ kuaishou: 4,
12005
+ xiaohongshu: 10,
12006
+ bilibili: 10,
12007
+ zhihu: 5,
12008
+ tiktok: 20,
12009
+ youtube: 60
12010
+ })
12011
+ }),
12012
+ titleCapabilities: Object.freeze({
12013
+ dynamic: Object.freeze({
12014
+ bilibili: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12015
+ zhihu: Object.freeze({ max: 50, unit: "utf16_code_units" }),
12016
+ douban: Object.freeze({ max: 70, unit: "utf16_code_units" }),
12017
+ x: null,
12018
+ threads: null,
12019
+ weibo: null,
12020
+ jike: null,
12021
+ zsxq: null,
12022
+ toutiao: null,
12023
+ juejin: null
12024
+ }),
12025
+ imageText: Object.freeze({
12026
+ xiaohongshu: Object.freeze({ max: 20, unit: "xiaohongshu_title_weighted" }),
12027
+ weixin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12028
+ douyin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12029
+ shipinhao: Object.freeze({ max: 22, unit: "utf16_code_units" }),
12030
+ reddit: Object.freeze({ max: 300, unit: "utf16_code_units" })
12031
+ }),
12032
+ article: Object.freeze({
12033
+ juejin: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12034
+ csdn: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12035
+ cnblogs: Object.freeze({ max: 128, unit: "utf16_code_units" }),
12036
+ weibo: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12037
+ sspai: Object.freeze({ max: 80, unit: "utf16_code_units" })
12038
+ }),
12039
+ shortVideo: Object.freeze({
12040
+ shipinhao: Object.freeze({ max: 16, unit: "utf16_code_units" }),
12041
+ douyin: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12042
+ bilibili: Object.freeze({ max: 80, unit: "utf16_code_units" })
12043
+ })
12044
+ }),
12045
+ bodyCapabilities: Object.freeze({
12046
+ dynamic: Object.freeze({
12047
+ threads: Object.freeze({ max: 500, unit: "unicode_code_points", behavior: "split_thread" }),
12048
+ x: Object.freeze({ max: 280, unit: "weighted", behavior: "split_thread" }),
12049
+ weibo: Object.freeze({ max: 2000, unit: "unicode_code_points" }),
12050
+ jike: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12051
+ }),
12052
+ imageText: Object.freeze({
12053
+ xiaohongshu: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12054
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12055
+ }),
12056
+ article: Object.freeze({
12057
+ wechatmp: Object.freeze({ max: 50000, unit: "unicode_code_points" }),
12058
+ juejin: Object.freeze({ max: 50000, unit: "unicode_code_points" })
12059
+ }),
12060
+ shortVideo: Object.freeze({
12061
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12062
+ kuaishou: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12063
+ shipinhao: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12064
+ })
12065
+ })
12066
+ });
11990
12067
  // ../../packages/kk-core/src/kinds.ts
11991
12068
  init_zod();
11992
12069
 
@@ -12077,6 +12154,67 @@ var BrowserSessionDescriptorSchema = object2({
12077
12154
  }
12078
12155
  }).optional()
12079
12156
  }).strict();
12157
+ var ContentCountUnitSchema = _enum([
12158
+ "utf16_code_units",
12159
+ "unicode_code_points",
12160
+ "weighted",
12161
+ "xiaohongshu_title_weighted"
12162
+ ]);
12163
+ var LimitEntrySchema = object2({
12164
+ max: number2().int().positive(),
12165
+ unit: ContentCountUnitSchema
12166
+ }).strict();
12167
+ var PublishingProfileLimitsSchema = object2({
12168
+ title: LimitEntrySchema.optional(),
12169
+ body: LimitEntrySchema.optional()
12170
+ }).strict();
12171
+ var PublishingProfileRequiresSchema = object2({
12172
+ cover: boolean2().optional(),
12173
+ orderedImages: boolean2().optional()
12174
+ }).strict();
12175
+ var PublishingProfileModeBindingSchema = object2({
12176
+ metadata: record(string2(), unknown())
12177
+ }).strict();
12178
+ var PublishingProfileModesSchema = object2({
12179
+ draft: PublishingProfileModeBindingSchema.optional(),
12180
+ live: PublishingProfileModeBindingSchema.optional()
12181
+ }).strict().superRefine((modes, ctx) => {
12182
+ if (modes.draft === undefined && modes.live === undefined) {
12183
+ ctx.addIssue({
12184
+ code: "custom",
12185
+ message: 'modes must define at least one of "draft" or "live"'
12186
+ });
12187
+ }
12188
+ });
12189
+ var PublishingProfileSchema = object2({
12190
+ format: string2().min(1),
12191
+ locale: string2().regex(/^(source|[a-z]{2,3}(-[A-Za-z0-9]+)*)$/, {
12192
+ message: 'locale must be "source" or a valid language tag'
12193
+ }),
12194
+ limits: PublishingProfileLimitsSchema.optional(),
12195
+ requires: PublishingProfileRequiresSchema.optional(),
12196
+ modes: PublishingProfileModesSchema
12197
+ }).strict();
12198
+ var PublishingCapabilitiesSchema = object2({
12199
+ defaultProfile: string2().min(1),
12200
+ profiles: record(string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), PublishingProfileSchema)
12201
+ }).strict().superRefine((data, ctx) => {
12202
+ const keys = Object.keys(data.profiles);
12203
+ if (keys.length === 0) {
12204
+ ctx.addIssue({
12205
+ code: "custom",
12206
+ path: ["profiles"],
12207
+ message: "profiles record must be nonempty"
12208
+ });
12209
+ }
12210
+ if (!(data.defaultProfile in data.profiles)) {
12211
+ ctx.addIssue({
12212
+ code: "custom",
12213
+ path: ["defaultProfile"],
12214
+ message: `defaultProfile "${data.defaultProfile}" must reference a declared profile`
12215
+ });
12216
+ }
12217
+ });
12080
12218
  var PluginManifestSchemaBase = looseObject({
12081
12219
  name: string2().min(1),
12082
12220
  kind: PluginKindSchema,
@@ -12085,23 +12223,31 @@ var PluginManifestSchemaBase = looseObject({
12085
12223
  version: string2().optional(),
12086
12224
  description: string2().optional(),
12087
12225
  operations: record(string2(), PluginOperationSchema).optional(),
12088
- browserSession: BrowserSessionDescriptorSchema.optional()
12226
+ browserSession: BrowserSessionDescriptorSchema.optional(),
12227
+ publishing: PublishingCapabilitiesSchema.optional()
12089
12228
  });
12090
12229
  var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
12091
- if (manifest.browserSession === undefined)
12092
- return;
12093
- if (manifest.kind !== "publish") {
12094
- ctx.addIssue({
12095
- code: "custom",
12096
- path: ["browserSession"],
12097
- message: `browserSession requires kind "publish", not "${manifest.kind}"`
12098
- });
12230
+ if (manifest.browserSession !== undefined) {
12231
+ if (manifest.kind !== "publish") {
12232
+ ctx.addIssue({
12233
+ code: "custom",
12234
+ path: ["browserSession"],
12235
+ message: `browserSession requires kind "publish", not "${manifest.kind}"`
12236
+ });
12237
+ }
12238
+ if (manifest.check === undefined) {
12239
+ ctx.addIssue({
12240
+ code: "custom",
12241
+ path: ["browserSession"],
12242
+ message: "browserSession requires a check probe entry that owns the session routines"
12243
+ });
12244
+ }
12099
12245
  }
12100
- if (manifest.check === undefined) {
12246
+ if (manifest.publishing !== undefined && manifest.kind !== "publish") {
12101
12247
  ctx.addIssue({
12102
12248
  code: "custom",
12103
- path: ["browserSession"],
12104
- message: "browserSession requires a check probe entry that owns the session routines"
12249
+ path: ["publishing"],
12250
+ message: `publishing requires kind "publish", not "${manifest.kind}"`
12105
12251
  });
12106
12252
  }
12107
12253
  });
@@ -13549,6 +13695,9 @@ function createVoxCpmClient(options = {}) {
13549
13695
  let renderInFlight;
13550
13696
  let warnedChunk = false;
13551
13697
  let warnedLanguage = false;
13698
+ let warnedInstruct = false;
13699
+ let warnedInstructIgnored = false;
13700
+ let instructSegments = 0;
13552
13701
  const pending = new Map;
13553
13702
  const renderResults = new Map;
13554
13703
  const signalHandlers = new Map;
@@ -13747,6 +13896,14 @@ function createVoxCpmClient(options = {}) {
13747
13896
  }
13748
13897
  const requestedInstruct = body.instruct?.trim();
13749
13898
  if (profile.mode === "instruct") {
13899
+ instructSegments += 1;
13900
+ if (instructSegments > 1) {
13901
+ throw new Error(`voxcpm-tts: profile "${profile.id}" is instruct-mode and pins no speaker \u2014 segment ${instructSegments} would sound like a different person; use a reference/continuation profile (VOXCPM_PROFILES_PATH)`);
13902
+ }
13903
+ if (!warnedInstruct) {
13904
+ warnedInstruct = true;
13905
+ echoError(`voxcpm-tts: profile "${profile.id}" is instruct-mode and pins no speaker \u2014 every request draws a new voice; use a reference/continuation profile for multi-segment episodes`);
13906
+ }
13750
13907
  const instruct = requestedInstruct || profile.instruct;
13751
13908
  if (instruct) {
13752
13909
  return { instruct, normalize: body.normalize ?? true };
@@ -13754,7 +13911,10 @@ function createVoxCpmClient(options = {}) {
13754
13911
  return { normalize: body.normalize ?? true };
13755
13912
  }
13756
13913
  if (requestedInstruct) {
13757
- throw new Error(`voxcpm-tts: per-request instruct is not supported for "${profile.mode}" mode`);
13914
+ if (!warnedInstructIgnored) {
13915
+ warnedInstructIgnored = true;
13916
+ echoError(`voxcpm-tts: ignoring the per-request instruct on "${profile.mode}" profile "${profile.id}" \u2014 the reference clip defines the voice`);
13917
+ }
13758
13918
  }
13759
13919
  return { normalize: body.normalize ?? true };
13760
13920
  }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env python3
2
+ """Grade one rendered episode for speaker consistency (音色/音质 uniformity).
3
+
4
+ Why this exists: a profile whose ``mode`` is ``instruct`` carries no reference audio, so VoxCPM
5
+ samples a speaker per request and a multi-segment episode changes voice between segments. The
6
+ 2026-10-15 episode drew 59 different speakers (within-item F0 swing up to 103 Hz) and nobody
7
+ noticed until a human listened. This script makes that measurable.
8
+
9
+ Usage (needs the plugin venv, which has librosa/numpy):
10
+
11
+ plugins/generations/voxcpm-tts/.venv/bin/python \
12
+ plugins/generations/voxcpm-tts/scripts/audit-speaker-consistency.py <content.json> [more.json ...]
13
+
14
+ Pass an episode content JSON (grouped per item when the sibling ``sections/<date>/`` dir exists)
15
+ or individual section JSONs. Exit code 1 means the thresholds below were missed.
16
+
17
+ Thresholds come from the two measured modes (2026-10-15, same three texts):
18
+ reference/continuation anchored -> F0 spread 22-26 Hz (one speaker)
19
+ instruct (random speaker) -> F0 spread 141 Hz (three texts, three speakers)
20
+ Grade "pass" needs every item's F0 spread <= 45 Hz and the per-item median <= 30 Hz. Median pitch
21
+ is the honest signal here: it tracks the speaker register, not the sentence's intonation.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import pathlib
28
+ import sys
29
+ import warnings
30
+
31
+ import librosa
32
+ import numpy as np
33
+
34
+ warnings.filterwarnings('ignore')
35
+
36
+ SR = 16000
37
+ MAX_ITEM_F0_SPREAD = 45.0
38
+ MAX_ITEM_MEDIAN_SPREAD = 30.0
39
+
40
+
41
+ def measure(content_path: pathlib.Path) -> list[float]:
42
+ """Per-segment median F0 (Hz) of one rendered Content."""
43
+ content = json.loads(content_path.read_text())
44
+ wav_path = content_path.parent / f'{content_path.stem}.wav'
45
+ audio, _ = librosa.load(str(wav_path), sr=SR, mono=True)
46
+ medians, offset = [], 0.0
47
+ for segment in content['metadata']['segments']:
48
+ start = int(offset * SR)
49
+ offset += segment['duration']
50
+ window = audio[start : int(offset * SR)]
51
+ if len(window) < SR * 0.4:
52
+ continue
53
+ f0 = librosa.yin(window, fmin=70, fmax=350, sr=SR)
54
+ voiced = f0[(f0 > 80) & (f0 < 340)]
55
+ if len(voiced):
56
+ medians.append(float(np.median(voiced)))
57
+ return medians
58
+
59
+
60
+ def grouping(content_path: pathlib.Path) -> list[tuple[str, pathlib.Path]]:
61
+ """Items to grade: the section JSONs when they sit next to the episode, else the file itself."""
62
+ sections = content_path.parent / 'sections' / content_path.stem.split('_')[0]
63
+ if content_path.name.endswith('_11_generate_content.json') and sections.is_dir():
64
+ return sorted((p.stem, p) for p in sections.glob('*.json'))
65
+ return [(content_path.stem, content_path)]
66
+
67
+
68
+ def main(argv: list[str]) -> int:
69
+ if not argv:
70
+ print(__doc__.strip().splitlines()[0], file=sys.stderr)
71
+ print('usage: audit-speaker-consistency.py <content.json> [...]', file=sys.stderr)
72
+ return 2
73
+
74
+ failures: list[str] = []
75
+ spreads: list[float] = []
76
+ print(f'{"item":14s} {"segs":>4s} {"medF0":>6s} {"spread":>7s} verdict')
77
+ for target in argv:
78
+ path = pathlib.Path(target)
79
+ for name, section_path in grouping(path):
80
+ medians = measure(section_path)
81
+ if len(medians) < 2:
82
+ continue
83
+ spread = max(medians) - min(medians)
84
+ spreads.append(spread)
85
+ verdict = 'ok'
86
+ if spread > MAX_ITEM_F0_SPREAD:
87
+ verdict = f'unstable (> {MAX_ITEM_F0_SPREAD:.0f} Hz)'
88
+ failures.append(f'{name}: F0 spread {spread:.0f} Hz across {len(medians)} segments')
89
+ print(f'{name:14s} {len(medians):4d} {np.median(medians):6.0f} {spread:7.0f} {verdict}')
90
+
91
+ if not spreads:
92
+ print('no item had enough segments to grade', file=sys.stderr)
93
+ return 2
94
+
95
+ median_spread = float(np.median(spreads))
96
+ print(f'\nper-item F0 spread: median {median_spread:.0f} Hz, worst {max(spreads):.0f} Hz over {len(spreads)} items')
97
+ if median_spread > MAX_ITEM_MEDIAN_SPREAD:
98
+ failures.append(f'median per-item F0 spread {median_spread:.0f} Hz > {MAX_ITEM_MEDIAN_SPREAD:.0f} Hz')
99
+ if failures:
100
+ print('FAIL — the episode changes speaker between segments (check the profile mode):')
101
+ for line in failures:
102
+ print(f' - {line}')
103
+ return 1
104
+ print('PASS — one speaker across every item')
105
+ return 0
106
+
107
+
108
+ if __name__ == '__main__':
109
+ sys.exit(main(sys.argv[1:]))
@@ -273,6 +273,9 @@ export function createVoxCpmClient(options: VoxCpmClientOptions = {}): VoxCpmCli
273
273
  let renderInFlight: { id: number; dest: string } | undefined;
274
274
  let warnedChunk = false;
275
275
  let warnedLanguage = false;
276
+ let warnedInstruct = false;
277
+ let warnedInstructIgnored = false;
278
+ let instructSegments = 0;
276
279
  const pending = new Map<number, PendingRequest>();
277
280
  const renderResults = new Map<number, Promise<Record<string, unknown>>>();
278
281
  const signalHandlers = new Map<NodeJS.Signals, () => void>();
@@ -509,6 +512,22 @@ export function createVoxCpmClient(options: VoxCpmClientOptions = {}): VoxCpmCli
509
512
  }
510
513
  const requestedInstruct = body.instruct?.trim();
511
514
  if (profile.mode === 'instruct') {
515
+ // Instruct profiles carry no reference audio, so the model samples a speaker per request:
516
+ // a multi-segment episode then changes voice from segment to segment (20261015: within-item
517
+ // F0 swing up to 103 Hz, 59 speakers). One segment is still legitimate; a second one is the
518
+ // defect, so refuse it instead of shipping a voice change.
519
+ instructSegments += 1;
520
+ if (instructSegments > 1) {
521
+ throw new Error(
522
+ `voxcpm-tts: profile "${profile.id}" is instruct-mode and pins no speaker — segment ${instructSegments} would sound like a different person; use a reference/continuation profile (VOXCPM_PROFILES_PATH)`,
523
+ );
524
+ }
525
+ if (!warnedInstruct) {
526
+ warnedInstruct = true;
527
+ echoError(
528
+ `voxcpm-tts: profile "${profile.id}" is instruct-mode and pins no speaker — every request draws a new voice; use a reference/continuation profile for multi-segment episodes`,
529
+ );
530
+ }
512
531
  const instruct = requestedInstruct || profile.instruct;
513
532
  if (instruct) {
514
533
  return { instruct, normalize: body.normalize ?? true };
@@ -516,7 +535,15 @@ export function createVoxCpmClient(options: VoxCpmClientOptions = {}): VoxCpmCli
516
535
  return { normalize: body.normalize ?? true };
517
536
  }
518
537
  if (requestedInstruct) {
519
- throw new Error(`voxcpm-tts: per-request instruct is not supported for "${profile.mode}" mode`);
538
+ // The shared pipeline turns a segment's `emotion` into a per-request instruct, so this is
539
+ // the normal case for every daily episode. A reference/continuation profile already fixes
540
+ // the voice: ignoring the instruction beats failing the tier (or re-labelling the clip).
541
+ if (!warnedInstructIgnored) {
542
+ warnedInstructIgnored = true;
543
+ echoError(
544
+ `voxcpm-tts: ignoring the per-request instruct on "${profile.mode}" profile "${profile.id}" — the reference clip defines the voice`,
545
+ );
546
+ }
520
547
  }
521
548
  return { normalize: body.normalize ?? true };
522
549
  }
@@ -12236,6 +12236,83 @@ var init_file_system_node = () => {};
12236
12236
  import { dirname as dirname2 } from "path";
12237
12237
  import { parseArgs } from "util";
12238
12238
 
12239
+ // ../../packages/kk-core/src/content-limits.ts
12240
+ var PLATFORM_CONTENT_LIMITS = Object.freeze({
12241
+ schemaVersion: 5,
12242
+ tagLimits: Object.freeze({
12243
+ default: 7,
12244
+ imageText: Object.freeze({
12245
+ instagram: 5,
12246
+ douyin: 5,
12247
+ kuaishou: 4,
12248
+ weixin: 1,
12249
+ xiaohongshu: 10
12250
+ }),
12251
+ shortVideo: Object.freeze({
12252
+ douyin: 5,
12253
+ kuaishou: 4,
12254
+ xiaohongshu: 10,
12255
+ bilibili: 10,
12256
+ zhihu: 5,
12257
+ tiktok: 20,
12258
+ youtube: 60
12259
+ })
12260
+ }),
12261
+ titleCapabilities: Object.freeze({
12262
+ dynamic: Object.freeze({
12263
+ bilibili: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12264
+ zhihu: Object.freeze({ max: 50, unit: "utf16_code_units" }),
12265
+ douban: Object.freeze({ max: 70, unit: "utf16_code_units" }),
12266
+ x: null,
12267
+ threads: null,
12268
+ weibo: null,
12269
+ jike: null,
12270
+ zsxq: null,
12271
+ toutiao: null,
12272
+ juejin: null
12273
+ }),
12274
+ imageText: Object.freeze({
12275
+ xiaohongshu: Object.freeze({ max: 20, unit: "xiaohongshu_title_weighted" }),
12276
+ weixin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12277
+ douyin: Object.freeze({ max: 20, unit: "utf16_code_units" }),
12278
+ shipinhao: Object.freeze({ max: 22, unit: "utf16_code_units" }),
12279
+ reddit: Object.freeze({ max: 300, unit: "utf16_code_units" })
12280
+ }),
12281
+ article: Object.freeze({
12282
+ juejin: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12283
+ csdn: Object.freeze({ max: 100, unit: "utf16_code_units" }),
12284
+ cnblogs: Object.freeze({ max: 128, unit: "utf16_code_units" }),
12285
+ weibo: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12286
+ sspai: Object.freeze({ max: 80, unit: "utf16_code_units" })
12287
+ }),
12288
+ shortVideo: Object.freeze({
12289
+ shipinhao: Object.freeze({ max: 16, unit: "utf16_code_units" }),
12290
+ douyin: Object.freeze({ max: 30, unit: "utf16_code_units" }),
12291
+ bilibili: Object.freeze({ max: 80, unit: "utf16_code_units" })
12292
+ })
12293
+ }),
12294
+ bodyCapabilities: Object.freeze({
12295
+ dynamic: Object.freeze({
12296
+ threads: Object.freeze({ max: 500, unit: "unicode_code_points", behavior: "split_thread" }),
12297
+ x: Object.freeze({ max: 280, unit: "weighted", behavior: "split_thread" }),
12298
+ weibo: Object.freeze({ max: 2000, unit: "unicode_code_points" }),
12299
+ jike: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12300
+ }),
12301
+ imageText: Object.freeze({
12302
+ xiaohongshu: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12303
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12304
+ }),
12305
+ article: Object.freeze({
12306
+ wechatmp: Object.freeze({ max: 50000, unit: "unicode_code_points" }),
12307
+ juejin: Object.freeze({ max: 50000, unit: "unicode_code_points" })
12308
+ }),
12309
+ shortVideo: Object.freeze({
12310
+ douyin: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12311
+ kuaishou: Object.freeze({ max: 1000, unit: "unicode_code_points" }),
12312
+ shipinhao: Object.freeze({ max: 1000, unit: "unicode_code_points" })
12313
+ })
12314
+ })
12315
+ });
12239
12316
  // ../../packages/kk-core/src/kinds.ts
12240
12317
  init_zod();
12241
12318
 
@@ -12326,6 +12403,67 @@ var BrowserSessionDescriptorSchema = object({
12326
12403
  }
12327
12404
  }).optional()
12328
12405
  }).strict();
12406
+ var ContentCountUnitSchema = _enum([
12407
+ "utf16_code_units",
12408
+ "unicode_code_points",
12409
+ "weighted",
12410
+ "xiaohongshu_title_weighted"
12411
+ ]);
12412
+ var LimitEntrySchema = object({
12413
+ max: number2().int().positive(),
12414
+ unit: ContentCountUnitSchema
12415
+ }).strict();
12416
+ var PublishingProfileLimitsSchema = object({
12417
+ title: LimitEntrySchema.optional(),
12418
+ body: LimitEntrySchema.optional()
12419
+ }).strict();
12420
+ var PublishingProfileRequiresSchema = object({
12421
+ cover: boolean2().optional(),
12422
+ orderedImages: boolean2().optional()
12423
+ }).strict();
12424
+ var PublishingProfileModeBindingSchema = object({
12425
+ metadata: record(string2(), unknown())
12426
+ }).strict();
12427
+ var PublishingProfileModesSchema = object({
12428
+ draft: PublishingProfileModeBindingSchema.optional(),
12429
+ live: PublishingProfileModeBindingSchema.optional()
12430
+ }).strict().superRefine((modes, ctx) => {
12431
+ if (modes.draft === undefined && modes.live === undefined) {
12432
+ ctx.addIssue({
12433
+ code: "custom",
12434
+ message: 'modes must define at least one of "draft" or "live"'
12435
+ });
12436
+ }
12437
+ });
12438
+ var PublishingProfileSchema = object({
12439
+ format: string2().min(1),
12440
+ locale: string2().regex(/^(source|[a-z]{2,3}(-[A-Za-z0-9]+)*)$/, {
12441
+ message: 'locale must be "source" or a valid language tag'
12442
+ }),
12443
+ limits: PublishingProfileLimitsSchema.optional(),
12444
+ requires: PublishingProfileRequiresSchema.optional(),
12445
+ modes: PublishingProfileModesSchema
12446
+ }).strict();
12447
+ var PublishingCapabilitiesSchema = object({
12448
+ defaultProfile: string2().min(1),
12449
+ profiles: record(string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/), PublishingProfileSchema)
12450
+ }).strict().superRefine((data, ctx) => {
12451
+ const keys = Object.keys(data.profiles);
12452
+ if (keys.length === 0) {
12453
+ ctx.addIssue({
12454
+ code: "custom",
12455
+ path: ["profiles"],
12456
+ message: "profiles record must be nonempty"
12457
+ });
12458
+ }
12459
+ if (!(data.defaultProfile in data.profiles)) {
12460
+ ctx.addIssue({
12461
+ code: "custom",
12462
+ path: ["defaultProfile"],
12463
+ message: `defaultProfile "${data.defaultProfile}" must reference a declared profile`
12464
+ });
12465
+ }
12466
+ });
12329
12467
  var PluginManifestSchemaBase = looseObject({
12330
12468
  name: string2().min(1),
12331
12469
  kind: PluginKindSchema,
@@ -12334,23 +12472,31 @@ var PluginManifestSchemaBase = looseObject({
12334
12472
  version: string2().optional(),
12335
12473
  description: string2().optional(),
12336
12474
  operations: record(string2(), PluginOperationSchema).optional(),
12337
- browserSession: BrowserSessionDescriptorSchema.optional()
12475
+ browserSession: BrowserSessionDescriptorSchema.optional(),
12476
+ publishing: PublishingCapabilitiesSchema.optional()
12338
12477
  });
12339
12478
  var PluginManifestSchema = PluginManifestSchemaBase.superRefine((manifest, ctx) => {
12340
- if (manifest.browserSession === undefined)
12341
- return;
12342
- if (manifest.kind !== "publish") {
12343
- ctx.addIssue({
12344
- code: "custom",
12345
- path: ["browserSession"],
12346
- message: `browserSession requires kind "publish", not "${manifest.kind}"`
12347
- });
12479
+ if (manifest.browserSession !== undefined) {
12480
+ if (manifest.kind !== "publish") {
12481
+ ctx.addIssue({
12482
+ code: "custom",
12483
+ path: ["browserSession"],
12484
+ message: `browserSession requires kind "publish", not "${manifest.kind}"`
12485
+ });
12486
+ }
12487
+ if (manifest.check === undefined) {
12488
+ ctx.addIssue({
12489
+ code: "custom",
12490
+ path: ["browserSession"],
12491
+ message: "browserSession requires a check probe entry that owns the session routines"
12492
+ });
12493
+ }
12348
12494
  }
12349
- if (manifest.check === undefined) {
12495
+ if (manifest.publishing !== undefined && manifest.kind !== "publish") {
12350
12496
  ctx.addIssue({
12351
12497
  code: "custom",
12352
- path: ["browserSession"],
12353
- message: "browserSession requires a check probe entry that owns the session routines"
12498
+ path: ["publishing"],
12499
+ message: `publishing requires kind "publish", not "${manifest.kind}"`
12354
12500
  });
12355
12501
  }
12356
12502
  });