@gobing-ai/knowledge-kit 0.0.13 → 0.0.14

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 (109) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/dist/index.js +20 -0
  3. package/package.json +1 -1
  4. package/plugins/generations/content-gen/dist/index.js +20 -0
  5. package/plugins/generations/core-facts-gen/dist/index.js +20 -0
  6. package/plugins/generations/daily-article-gen/dist/index.js +29 -1
  7. package/plugins/generations/daily-article-gen/src/index.ts +13 -1
  8. package/plugins/generations/dailynews-gen/dist/index.js +20 -0
  9. package/plugins/generations/episode-plan-gen/dist/index.js +24 -0
  10. package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
  11. package/plugins/generations/image-gen/dist/index.js +2296 -53
  12. package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
  13. package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
  14. package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
  15. package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
  16. package/plugins/generations/image-gen/src/providers/google.ts +268 -0
  17. package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
  18. package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
  19. package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
  20. package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
  21. package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
  22. package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
  23. package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
  24. package/plugins/generations/image-gen/src/providers/seedream.ts +1 -1
  25. package/plugins/generations/image-gen/src/providers/types.ts +52 -51
  26. package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
  27. package/plugins/generations/news-report-gen/dist/index.js +22193 -0
  28. package/plugins/generations/news-report-gen/package.json +17 -0
  29. package/plugins/generations/news-report-gen/plugin.json +7 -0
  30. package/plugins/generations/news-report-gen/src/index.ts +308 -0
  31. package/plugins/generations/news-report-gen/tsconfig.json +4 -0
  32. package/plugins/generations/omni-voice-gen/Makefile +14 -0
  33. package/plugins/generations/omni-voice-gen/README.md +112 -0
  34. package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
  35. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
  36. package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
  37. package/plugins/generations/omni-voice-gen/plugin.json +6 -0
  38. package/plugins/generations/omni-voice-gen/profiles.json +12 -0
  39. package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
  40. package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
  41. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
  42. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
  43. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
  44. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
  45. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
  46. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
  47. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
  48. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
  49. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
  50. package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
  51. package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
  52. package/plugins/generations/voice-gen/dist/index.js +30 -2
  53. package/plugins/generations/voice-gen/src/index.ts +16 -1
  54. package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
  55. package/plugins/ingestions/aihot-ingest/dist/index.js +20 -0
  56. package/plugins/ingestions/horizon-ingest/dist/index.js +20 -0
  57. package/plugins/ingestions/last30days-ingest/dist/index.js +20 -0
  58. package/plugins/ingestions/web-search/dist/index.js +20 -0
  59. package/plugins/kk/commands/image-extract.md +40 -0
  60. package/plugins/kk/commands/image-generate.md +18 -1
  61. package/plugins/kk/plugin.json +1 -1
  62. package/plugins/kk/skills/image-authoring/SKILL.md +10 -3
  63. package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
  64. package/plugins/kk/skills/image-authoring/references/style-extraction.md +14 -9
  65. package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
  66. package/plugins/publishings/emdash-pub/dist/index.js +20 -0
  67. package/plugins/publishings/podcast-pub/dist/index.js +57 -3
  68. package/plugins/publishings/podcast-pub/src/index.ts +18 -2
  69. package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
  70. package/plugins/publishings/qiita-pub/dist/index.js +20 -0
  71. package/plugins/publishings/surfdash-pub/dist/index.js +96 -6
  72. package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
  73. package/plugins/publishings/zenn-pub/dist/index.js +20 -0
  74. package/plugins/sp/scripts/batch-preflight.mjs +346 -0
  75. package/plugins/sp/scripts/batch-preflight.ts +459 -0
  76. package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
  77. package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
  78. package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
  79. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
  80. package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
  81. package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
  82. package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
  83. package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
  84. package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
  85. package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
  86. package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
  87. package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
  88. package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
  89. package/plugins/sp/scripts/idea-handoff.mjs +22 -0
  90. package/plugins/sp/scripts/idea-handoff.ts +44 -0
  91. package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
  92. package/plugins/sp/scripts/inline-run-setup.ts +198 -0
  93. package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
  94. package/plugins/sp/scripts/pr-reviewing.ts +925 -0
  95. package/plugins/sp/scripts/quality-gate.mjs +179 -0
  96. package/plugins/sp/scripts/quality-gate.ts +217 -0
  97. package/plugins/sp/scripts/script-contract-check.ts +319 -0
  98. package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
  99. package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
  100. package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
  101. package/plugins/sp/scripts/task-size-precheck.ts +175 -0
  102. package/plugins/sp/scripts/transition-shim-check.ts +238 -0
  103. package/plugins/sp/scripts/validate-commands.ts +689 -0
  104. package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
  105. package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
  106. package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
  107. package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
  108. package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
  109. package/plugins/sp/scripts/wrapup-steps.ts +466 -0
@@ -0,0 +1,352 @@
1
+ """VoiceScript parse/merge/validate — Python port of voice-gen/src/voicescript.ts.
2
+
3
+ Deltas from the TS source (task 0122 Q&A):
4
+ - The Voicebox engine allowlist and the 23-language allowlist are NOT ported. `engine`,
5
+ `personality`, `effects_chain`, `effect_preset` are collected into a WarnSink (D5) and
6
+ language strings pass through unvalidated (OmniVoice's _resolve_language owns that check).
7
+ - Parsing is yaml.safe_load only (JSON ⊂ YAML 1.2); branch order and error text keep voice-gen
8
+ semantics.
9
+ - Warnings are never emitted here — pipeline.py (0125) prints `sink.emit_once()` to stderr so
10
+ the warning appears exactly once per run.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ import yaml
19
+
20
+ from .contract import Doc
21
+
22
+ VOICEBOX_TEXT_MAX = 50_000
23
+ VOICEBOX_INSTRUCT_MAX = 500
24
+ VOICEBOX_CHUNK_CHARS_MIN = 100
25
+ VOICEBOX_CHUNK_CHARS_MAX = 5_000
26
+ VOICEBOX_CHUNK_CHARS_DEFAULT = 800
27
+ VOICEBOX_CROSSFADE_MS_MIN = 0
28
+ VOICEBOX_CROSSFADE_MS_MAX = 500
29
+ VOICEBOX_CROSSFADE_MS_DEFAULT = 50
30
+
31
+ WARN_IGNORE_FIELDS = ("engine", "personality", "effects_chain", "effect_preset")
32
+
33
+ VOICESCRIPT_KEYS = frozenset(
34
+ {
35
+ "segments",
36
+ "speakers",
37
+ "default_profile",
38
+ "default_profile_id",
39
+ "default_engine",
40
+ "default_emotion",
41
+ "default_effects_chain",
42
+ "default_effect_preset",
43
+ }
44
+ )
45
+
46
+
47
+ @dataclass
48
+ class VoiceSpeaker:
49
+ profile: Any = None
50
+ profile_id: Any = None
51
+ name: Any = None
52
+ role: Any = None
53
+ engine: Any = None
54
+ language: Any = None
55
+ emotion: Any = None
56
+ instruct: Any = None
57
+ effects_chain: Any = None
58
+ effect_preset: Any = None
59
+ personality: Any = None
60
+
61
+
62
+ @dataclass
63
+ class VoiceSegment:
64
+ text: Any = ""
65
+ speaker: Any = None
66
+ profile: Any = None
67
+ profile_id: Any = None
68
+ emotion: Any = None
69
+ instruct: Any = None
70
+ engine: Any = None
71
+ language: Any = None
72
+ gap_ms: Any = None
73
+ max_chunk_chars: Any = None
74
+ crossfade_ms: Any = None
75
+ personality: Any = None
76
+ seed: Any = None
77
+ effects_chain: Any = None
78
+ effect_preset: Any = None
79
+
80
+
81
+ @dataclass
82
+ class VoiceScript:
83
+ segments: list[Any] = field(default_factory=list)
84
+ title: Any = None
85
+ language: Any = None
86
+ default_engine: Any = None
87
+ default_profile: Any = None
88
+ default_profile_id: Any = None
89
+ default_emotion: Any = None
90
+ default_effects_chain: Any = None
91
+ default_effect_preset: Any = None
92
+ max_chunk_chars: Any = None
93
+ crossfade_ms: Any = None
94
+ speakers: dict[str, Any] = field(default_factory=dict)
95
+
96
+
97
+ class WarnSink:
98
+ """Collects D5 warn-ignore fields during validation; emit_once() renders the one warning."""
99
+
100
+ def __init__(self) -> None:
101
+ self._seen: dict[str, set[str]] = {}
102
+
103
+ def add(self, field: str, where: str) -> None:
104
+ self._seen.setdefault(field, set()).add(where)
105
+
106
+ def emit_once(self) -> str | None:
107
+ if not self._seen:
108
+ return None
109
+ parts = [f"{name} (at {', '.join(sorted(wheres))})" for name, wheres in self._seen.items()]
110
+ return f"omni-voice-gen: ignoring VoiceScript fields OmniVoice cannot honor: {'; '.join(parts)}"
111
+
112
+
113
+ def _filter(cls: type, value: dict[str, Any]) -> Any:
114
+ """Build a dataclass from a mapping, dropping unknown keys (unknown YAML never fails)."""
115
+ return cls(**{k: v for k, v in value.items() if k in cls.__dataclass_fields__})
116
+
117
+
118
+ def _plain_text_script(text: str, doc: Doc, env_default_profile: str | None) -> VoiceScript:
119
+ profile = env_default_profile
120
+ if profile is None and doc.metadata and isinstance(doc.metadata.get("voiceProfile"), str):
121
+ profile = doc.metadata["voiceProfile"]
122
+ return VoiceScript(segments=[VoiceSegment(text=text)], default_profile=profile)
123
+
124
+
125
+ def parse_doc_to_voicescript(doc: Doc, env_default_profile: str | None = None) -> VoiceScript:
126
+ """Parse a Doc body into a VoiceScript; plain text falls back to a single-segment script."""
127
+ trimmed = (doc.body or "").strip()
128
+
129
+ if trimmed.startswith("{"):
130
+ try:
131
+ parsed = yaml.safe_load(trimmed)
132
+ except yaml.YAMLError as exc:
133
+ raise ValueError("Invalid VoiceScript JSON structure: failed to parse JSON object") from exc
134
+ if not isinstance(parsed, dict):
135
+ raise ValueError("Invalid VoiceScript JSON structure: root must be a JSON object")
136
+ return _script_from_mapping(parsed)
137
+
138
+ if (
139
+ trimmed.startswith("[")
140
+ and not trimmed.startswith("[-")
141
+ and "segments:" not in trimmed
142
+ and "speakers:" not in trimmed
143
+ ):
144
+ # Plain text starting with a paralinguistic tag e.g. [laugh] or [sigh]
145
+ return _plain_text_script(trimmed, doc, env_default_profile)
146
+
147
+ try:
148
+ parsed = yaml.safe_load(trimmed)
149
+ except yaml.YAMLError:
150
+ parsed = None
151
+ if isinstance(parsed, dict) and any(key in parsed for key in VOICESCRIPT_KEYS):
152
+ return _script_from_mapping(parsed)
153
+
154
+ return _plain_text_script(trimmed, doc, env_default_profile)
155
+
156
+
157
+ def _script_from_mapping(parsed: dict[str, Any]) -> VoiceScript:
158
+ script = _filter(VoiceScript, parsed)
159
+ speakers = parsed.get("speakers")
160
+ if isinstance(speakers, dict):
161
+ script.speakers = {k: _filter(VoiceSpeaker, v) if isinstance(v, dict) else v for k, v in speakers.items()}
162
+ segments = parsed.get("segments")
163
+ if isinstance(segments, list):
164
+ script.segments = [_filter(VoiceSegment, s) if isinstance(s, dict) else s for s in segments]
165
+ return script
166
+
167
+
168
+ def merge_voice_scripts(
169
+ scripts: list[VoiceScript], docs: list[Doc], env_default_profile: str | None = None
170
+ ) -> VoiceScript:
171
+ """Merge Doc-level VoiceScripts into one; first-wins for scalar defaults."""
172
+ if not scripts:
173
+ return VoiceScript(segments=[])
174
+ if len(scripts) == 1:
175
+ return scripts[0]
176
+
177
+ combined_speakers: dict[str, Any] = {}
178
+ combined_segments: list[Any] = []
179
+
180
+ title = None
181
+ default_profile = env_default_profile
182
+ default_profile_id = None
183
+ default_engine = None
184
+ default_emotion = None
185
+ default_effects_chain = None
186
+ default_effect_preset = None
187
+ language = None
188
+ max_chunk_chars = None
189
+ crossfade_ms = None
190
+
191
+ for i, script in enumerate(scripts):
192
+ if script is None:
193
+ continue
194
+ doc = docs[i] if i < len(docs) else None
195
+
196
+ if not title:
197
+ title = script.title or (doc.title if doc else None)
198
+ if script.default_profile and default_profile == env_default_profile:
199
+ default_profile = script.default_profile
200
+ if script.default_profile_id and not default_profile_id:
201
+ default_profile_id = script.default_profile_id
202
+ if not default_profile and doc and doc.metadata and isinstance(doc.metadata.get("voiceProfile"), str):
203
+ default_profile = doc.metadata["voiceProfile"]
204
+ if not default_engine and script.default_engine:
205
+ default_engine = script.default_engine
206
+ if not default_emotion and script.default_emotion:
207
+ default_emotion = script.default_emotion
208
+ if not default_effects_chain and script.default_effects_chain:
209
+ default_effects_chain = script.default_effects_chain
210
+ if not default_effect_preset and script.default_effect_preset:
211
+ default_effect_preset = script.default_effect_preset
212
+ if not language and script.language:
213
+ language = script.language
214
+ if max_chunk_chars is None and script.max_chunk_chars is not None:
215
+ max_chunk_chars = script.max_chunk_chars
216
+ if crossfade_ms is None and script.crossfade_ms is not None:
217
+ crossfade_ms = script.crossfade_ms
218
+
219
+ for key, speaker in (script.speakers or {}).items():
220
+ if key in combined_speakers:
221
+ raise ValueError(f'Duplicate speaker key across documents: "{key}"')
222
+ combined_speakers[key] = speaker
223
+
224
+ if isinstance(script.segments, list):
225
+ combined_segments.extend(script.segments)
226
+
227
+ result = VoiceScript(
228
+ title=title or (docs[0].title if docs else None) or "Generated voice",
229
+ segments=combined_segments,
230
+ )
231
+ if combined_speakers:
232
+ result.speakers = combined_speakers
233
+ if default_profile:
234
+ result.default_profile = default_profile
235
+ if default_profile_id:
236
+ result.default_profile_id = default_profile_id
237
+ if default_engine:
238
+ result.default_engine = default_engine
239
+ if default_emotion:
240
+ result.default_emotion = default_emotion
241
+ if default_effects_chain:
242
+ result.default_effects_chain = default_effects_chain
243
+ if default_effect_preset:
244
+ result.default_effect_preset = default_effect_preset
245
+ if language:
246
+ result.language = language
247
+ if max_chunk_chars is not None:
248
+ result.max_chunk_chars = max_chunk_chars
249
+ if crossfade_ms is not None:
250
+ result.crossfade_ms = crossfade_ms
251
+ return result
252
+
253
+
254
+ def _is_int(value: Any) -> bool:
255
+ return not isinstance(value, bool) and isinstance(value, (int, float)) and float(value).is_integer()
256
+
257
+
258
+ def _collect_ignored(obj: Any, where: str, sink: WarnSink) -> None:
259
+ for name in WARN_IGNORE_FIELDS:
260
+ if getattr(obj, name, None) is not None:
261
+ sink.add(name, where)
262
+
263
+
264
+ def validate_voice_script(script: VoiceScript, sink: WarnSink) -> None:
265
+ """Raise ValueError with voice-gen message text on any violation; collect D5 fields into sink."""
266
+ if not isinstance(script, VoiceScript):
267
+ # ValueError, not TypeError: every validation failure must carry voice-gen's error
268
+ # semantics (TS throws Error; __main__ maps ValueError onto the single fail-loud exit).
269
+ raise ValueError("VoiceScript must be an object") # noqa: TRY004
270
+
271
+ if not isinstance(script.segments, list) or len(script.segments) == 0:
272
+ raise ValueError("VoiceScript must have a non-empty segments array")
273
+
274
+ if script.max_chunk_chars is not None and (
275
+ not _is_int(script.max_chunk_chars)
276
+ or not VOICEBOX_CHUNK_CHARS_MIN <= script.max_chunk_chars <= VOICEBOX_CHUNK_CHARS_MAX
277
+ ):
278
+ raise ValueError(f"max_chunk_chars must be between {VOICEBOX_CHUNK_CHARS_MIN} and {VOICEBOX_CHUNK_CHARS_MAX}")
279
+
280
+ if script.crossfade_ms is not None and (
281
+ not _is_int(script.crossfade_ms)
282
+ or not VOICEBOX_CROSSFADE_MS_MIN <= script.crossfade_ms <= VOICEBOX_CROSSFADE_MS_MAX
283
+ ):
284
+ raise ValueError(f"crossfade_ms must be between {VOICEBOX_CROSSFADE_MS_MIN} and {VOICEBOX_CROSSFADE_MS_MAX}")
285
+
286
+ _collect_ignored(script, "script", sink)
287
+
288
+ for name, speaker in (script.speakers or {}).items():
289
+ where = f'speakers.{name}'
290
+ if not isinstance(speaker, VoiceSpeaker):
291
+ raise ValueError(f'Speaker "{name}" definition must be an object') # noqa: TRY004
292
+ if not speaker.profile and not speaker.profile_id:
293
+ raise ValueError(f'Speaker "{name}" must have a profile or profile_id specified')
294
+ if isinstance(speaker.instruct, str) and len(speaker.instruct) > VOICEBOX_INSTRUCT_MAX:
295
+ raise ValueError(f'Speaker "{name}" instruct exceeds 500 characters ({len(speaker.instruct)} chars)')
296
+ _collect_ignored(speaker, where, sink)
297
+
298
+ for idx, segment in enumerate(script.segments):
299
+ if not isinstance(segment, VoiceSegment):
300
+ raise ValueError(f"Segment at index {idx} must be an object") # noqa: TRY004
301
+
302
+ if not isinstance(segment.text, str) or len(segment.text) == 0:
303
+ raise ValueError(f"Segment at index {idx} text must not be empty")
304
+
305
+ if len(segment.text) > VOICEBOX_TEXT_MAX:
306
+ raise ValueError(
307
+ f"Segment text exceeds the 50,000-character Voicebox /generate cap ({len(segment.text)} chars)"
308
+ )
309
+
310
+ if isinstance(segment.instruct, str) and len(segment.instruct) > VOICEBOX_INSTRUCT_MAX:
311
+ raise ValueError(f"Segment instruct exceeds 500 characters ({len(segment.instruct)} chars)")
312
+
313
+ if segment.speaker and segment.speaker not in (script.speakers or {}):
314
+ raise ValueError(f'Unknown speaker: "{segment.speaker}"')
315
+
316
+ if segment.max_chunk_chars is not None and (
317
+ not _is_int(segment.max_chunk_chars)
318
+ or not VOICEBOX_CHUNK_CHARS_MIN <= segment.max_chunk_chars <= VOICEBOX_CHUNK_CHARS_MAX
319
+ ):
320
+ raise ValueError(
321
+ f"max_chunk_chars must be between {VOICEBOX_CHUNK_CHARS_MIN} and {VOICEBOX_CHUNK_CHARS_MAX}"
322
+ )
323
+
324
+ if segment.crossfade_ms is not None and (
325
+ not _is_int(segment.crossfade_ms)
326
+ or not VOICEBOX_CROSSFADE_MS_MIN <= segment.crossfade_ms <= VOICEBOX_CROSSFADE_MS_MAX
327
+ ):
328
+ raise ValueError(
329
+ f"crossfade_ms must be between {VOICEBOX_CROSSFADE_MS_MIN} and {VOICEBOX_CROSSFADE_MS_MAX}"
330
+ )
331
+
332
+ if segment.gap_ms is not None and (
333
+ isinstance(segment.gap_ms, bool) or not isinstance(segment.gap_ms, (int, float)) or segment.gap_ms < 0
334
+ ):
335
+ raise ValueError("gap_ms must be non-negative")
336
+
337
+ if segment.seed is not None and (not _is_int(segment.seed) or segment.seed < 0):
338
+ raise ValueError("seed must be a non-negative integer")
339
+
340
+ _collect_ignored(segment, f"segments[{idx}]", sink)
341
+
342
+
343
+ def resolve_instruct(instruct: str | None = None, emotion: str | None = None) -> str | None:
344
+ if instruct and emotion:
345
+ if emotion.lower() in instruct.lower():
346
+ return instruct
347
+ return f"{instruct}. Emotion: {emotion}"
348
+ if instruct:
349
+ return instruct
350
+ if emotion:
351
+ return f"Speak in a {emotion} tone."
352
+ return None