@gobing-ai/knowledge-kit 0.0.12 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/dist/index.js +120 -26
- package/package.json +1 -1
- package/plugins/generations/content-gen/dist/index.js +22187 -0
- package/plugins/generations/content-gen/plugin.json +1 -1
- package/plugins/generations/core-facts-gen/dist/index.js +22068 -0
- package/plugins/generations/core-facts-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/dist/index.js +22050 -0
- package/plugins/generations/daily-article-gen/plugin.json +1 -1
- package/plugins/generations/daily-article-gen/src/index.ts +13 -1
- package/plugins/generations/dailynews-gen/dist/index.js +22344 -0
- package/plugins/generations/dailynews-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/dist/index.js +22503 -0
- package/plugins/generations/episode-plan-gen/plugin.json +1 -1
- package/plugins/generations/episode-plan-gen/src/index.ts +11 -0
- package/plugins/generations/image-gen/config.example.yaml +75 -0
- package/plugins/generations/image-gen/dist/index.js +24862 -0
- package/plugins/generations/image-gen/package.json +17 -0
- package/plugins/generations/image-gen/plugin.json +7 -0
- package/plugins/generations/image-gen/presets/formats/cover.yaml +57 -0
- package/plugins/generations/image-gen/presets/formats/free.yaml +46 -0
- package/plugins/generations/image-gen/presets/formats/illustration.yaml +48 -0
- package/plugins/generations/image-gen/presets/styles/clean-webapp-ui.yaml +28 -0
- package/plugins/generations/image-gen/presets/styles/cute.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/editorial.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/fresh.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/minimalist.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/photorealistic.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/sketch.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/technical-diagram.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/vibrant.yaml +3 -0
- package/plugins/generations/image-gen/presets/styles/warm.yaml +3 -0
- package/plugins/generations/image-gen/src/bytes.ts +19 -0
- package/plugins/generations/image-gen/src/index.ts +319 -0
- package/plugins/generations/image-gen/src/job.ts +143 -0
- package/plugins/generations/image-gen/src/paths.ts +31 -0
- package/plugins/generations/image-gen/src/presets.ts +344 -0
- package/plugins/generations/image-gen/src/providers/agnes.ts +110 -0
- package/plugins/generations/image-gen/src/providers/azure.ts +153 -0
- package/plugins/generations/image-gen/src/providers/codex-cli.ts +170 -0
- package/plugins/generations/image-gen/src/providers/dashscope.ts +485 -0
- package/plugins/generations/image-gen/src/providers/google.ts +268 -0
- package/plugins/generations/image-gen/src/providers/huggingface.ts +59 -0
- package/plugins/generations/image-gen/src/providers/jimeng.ts +259 -0
- package/plugins/generations/image-gen/src/providers/minimax.ts +171 -0
- package/plugins/generations/image-gen/src/providers/openai.ts +319 -0
- package/plugins/generations/image-gen/src/providers/openrouter.ts +257 -0
- package/plugins/generations/image-gen/src/providers/refs.ts +24 -0
- package/plugins/generations/image-gen/src/providers/replicate.ts +279 -0
- package/plugins/generations/image-gen/src/providers/seedream.ts +128 -0
- package/plugins/generations/image-gen/src/providers/types.ts +286 -0
- package/plugins/generations/image-gen/src/providers/zai.ts +237 -0
- package/plugins/generations/image-gen/tsconfig.json +8 -0
- package/plugins/generations/news-report-gen/dist/index.js +22193 -0
- package/plugins/generations/news-report-gen/package.json +17 -0
- package/plugins/generations/news-report-gen/plugin.json +7 -0
- package/plugins/generations/news-report-gen/src/index.ts +308 -0
- package/plugins/generations/news-report-gen/tsconfig.json +4 -0
- package/plugins/generations/omni-voice-gen/Makefile +14 -0
- package/plugins/generations/omni-voice-gen/README.md +112 -0
- package/plugins/generations/omni-voice-gen/bin/omni-voice-gen +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen-prr8skpb. +2 -0
- package/plugins/generations/omni-voice-gen/dist/omni-voice-gen.js +6 -0
- package/plugins/generations/omni-voice-gen/plugin.json +6 -0
- package/plugins/generations/omni-voice-gen/profiles.json +12 -0
- package/plugins/generations/omni-voice-gen/pyproject.toml +25 -0
- package/plugins/generations/omni-voice-gen/scripts/coverage_gate.py +74 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__init__.py +1 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/__main__.py +39 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/audio.py +190 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/backend.py +150 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/contract.py +76 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/mp3.py +60 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/pipeline.py +289 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/profiles.py +100 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/qc.py +234 -0
- package/plugins/generations/omni-voice-gen/src/omni_voice_gen/voicescript.py +352 -0
- package/plugins/generations/omni-voice-gen/uv.lock +3510 -0
- package/plugins/generations/voice-gen/dist/index.js +23055 -0
- package/plugins/generations/voice-gen/plugin.json +1 -1
- package/plugins/generations/voice-gen/src/index.ts +16 -1
- package/plugins/generations/voice-gen/src/voicebox-client.ts +3 -1
- package/plugins/ingestions/aihot-ingest/dist/index.js +22378 -0
- package/plugins/ingestions/aihot-ingest/plugin.json +1 -1
- package/plugins/ingestions/horizon-ingest/dist/index.js +22125 -0
- package/plugins/ingestions/horizon-ingest/plugin.json +1 -1
- package/plugins/ingestions/karakeep-local/dist/index.js +24204 -0
- package/plugins/ingestions/karakeep-local/plugin.json +1 -1
- package/plugins/ingestions/last30days-ingest/dist/index.js +22070 -0
- package/plugins/ingestions/last30days-ingest/plugin.json +1 -1
- package/plugins/ingestions/web-search/dist/index.js +24399 -0
- package/plugins/ingestions/web-search/plugin.json +1 -1
- package/plugins/kk/commands/image-extract.md +40 -0
- package/plugins/kk/commands/image-generate.md +32 -0
- package/plugins/kk/config.example.yaml +80 -0
- package/plugins/kk/plugin.json +1 -1
- package/plugins/kk/skills/image-authoring/SKILL.md +257 -0
- package/plugins/kk/skills/image-authoring/references/format-drafting.md +57 -0
- package/plugins/kk/skills/image-authoring/references/illustration-positions.md +87 -0
- package/plugins/kk/skills/image-authoring/references/migrating-from-wt.md +31 -0
- package/plugins/kk/skills/image-authoring/references/providers.md +52 -0
- package/plugins/kk/skills/image-authoring/references/style-extraction.md +139 -0
- package/plugins/kk/workflows/kk-daily-ai-voice.yaml +130 -30
- package/plugins/publishings/emdash-pub/dist/index.js +22263 -0
- package/plugins/publishings/emdash-pub/plugin.json +1 -1
- package/plugins/publishings/podcast-pub/dist/index.js +22650 -0
- package/plugins/publishings/podcast-pub/plugin.json +8 -2
- package/plugins/publishings/podcast-pub/src/index.ts +18 -2
- package/plugins/publishings/podcast-pub/src/show-notes.ts +56 -9
- package/plugins/publishings/qiita-pub/dist/index.js +22101 -0
- package/plugins/publishings/qiita-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/dist/index.js +22323 -0
- package/plugins/publishings/surfdash-pub/plugin.json +1 -1
- package/plugins/publishings/surfdash-pub/src/index.ts +109 -9
- package/plugins/publishings/zenn-pub/dist/index.js +22142 -0
- package/plugins/publishings/zenn-pub/plugin.json +1 -1
- package/plugins/sp/scripts/batch-preflight.mjs +346 -0
- package/plugins/sp/scripts/batch-preflight.ts +459 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.mjs +615 -0
- package/plugins/sp/scripts/daily-summary/daily-summary.ts +846 -0
- package/plugins/sp/scripts/daily-summary/logger.ts +28 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.mjs +223 -0
- package/plugins/sp/scripts/dogfood-testing/detect-pipeline-driving.ts +367 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.mjs +132 -0
- package/plugins/sp/scripts/dogfood-testing/validate-report.ts +169 -0
- package/plugins/sp/scripts/feature-dev-precheck.mjs +171 -0
- package/plugins/sp/scripts/feature-dev-precheck.ts +238 -0
- package/plugins/sp/scripts/feature-sync-bounded.mjs +285 -0
- package/plugins/sp/scripts/feature-sync-bounded.ts +478 -0
- package/plugins/sp/scripts/history-anatomy-cache.mjs +902 -0
- package/plugins/sp/scripts/history-anatomy-cache.ts +1028 -0
- package/plugins/sp/scripts/idea-handoff.mjs +22 -0
- package/plugins/sp/scripts/idea-handoff.ts +44 -0
- package/plugins/sp/scripts/inline-pipeline-parity-check.ts +185 -0
- package/plugins/sp/scripts/inline-run-setup.ts +198 -0
- package/plugins/sp/scripts/pr-reviewing.mjs +769 -0
- package/plugins/sp/scripts/pr-reviewing.ts +925 -0
- package/plugins/sp/scripts/quality-gate.mjs +179 -0
- package/plugins/sp/scripts/quality-gate.ts +217 -0
- package/plugins/sp/scripts/script-contract-check.ts +319 -0
- package/plugins/sp/scripts/stage-registry-adapter.ts +1533 -0
- package/plugins/sp/scripts/surface-drift-inventory.ts +929 -0
- package/plugins/sp/scripts/task-evidence-precheck.ts +181 -0
- package/plugins/sp/scripts/task-size-precheck.ts +175 -0
- package/plugins/sp/scripts/transition-shim-check.ts +238 -0
- package/plugins/sp/scripts/validate-commands.ts +689 -0
- package/plugins/sp/scripts/validate-flag-contracts.ts +878 -0
- package/plugins/sp/scripts/verify-answer-lint.ts +530 -0
- package/plugins/sp/scripts/workflow-step-profile.mjs +316 -0
- package/plugins/sp/scripts/workflow-step-profile.ts +456 -0
- package/plugins/sp/scripts/wrapup-steps.mjs +373 -0
- 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
|