renderplan 0.1.0__py3-none-any.whl
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.
- renderplan/__init__.py +77 -0
- renderplan/__main__.py +3 -0
- renderplan/_toml.py +82 -0
- renderplan/_version.py +24 -0
- renderplan/cli.py +71 -0
- renderplan/codec.py +688 -0
- renderplan/errors.py +10 -0
- renderplan/identity.py +80 -0
- renderplan/model.py +937 -0
- renderplan/py.typed +0 -0
- renderplan/version.py +13 -0
- renderplan-0.1.0.dist-info/METADATA +160 -0
- renderplan-0.1.0.dist-info/RECORD +17 -0
- renderplan-0.1.0.dist-info/WHEEL +5 -0
- renderplan-0.1.0.dist-info/entry_points.txt +2 -0
- renderplan-0.1.0.dist-info/licenses/LICENSE +201 -0
- renderplan-0.1.0.dist-info/top_level.txt +1 -0
renderplan/codec.py
ADDED
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, cast
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
tomllib: Any = importlib.import_module("tomllib")
|
|
10
|
+
except ModuleNotFoundError: # pragma: no cover - Python 3.10
|
|
11
|
+
tomllib = importlib.import_module("tomli")
|
|
12
|
+
|
|
13
|
+
from ._toml import dumps as toml_dumps
|
|
14
|
+
from .errors import CodecError
|
|
15
|
+
from .model import (
|
|
16
|
+
MANIFEST_FORMAT,
|
|
17
|
+
PLAN_FORMAT,
|
|
18
|
+
PREFLIGHT_FORMAT,
|
|
19
|
+
PROFILE_FORMAT,
|
|
20
|
+
AudioItem,
|
|
21
|
+
CapacityConstraint,
|
|
22
|
+
PauseItem,
|
|
23
|
+
PausePolicy,
|
|
24
|
+
PlanningPolicy,
|
|
25
|
+
PreflightCheck,
|
|
26
|
+
PreflightReport,
|
|
27
|
+
Producer,
|
|
28
|
+
RenderManifest,
|
|
29
|
+
RenderPlan,
|
|
30
|
+
RenderProfile,
|
|
31
|
+
ReplanRequest,
|
|
32
|
+
RoleProfile,
|
|
33
|
+
RoleRequirements,
|
|
34
|
+
SourceSpan,
|
|
35
|
+
SpeechArtifact,
|
|
36
|
+
SpeechControl,
|
|
37
|
+
SpeechItem,
|
|
38
|
+
TokenBundle,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _mapping(value: Any, field: str) -> Mapping[str, Any]:
|
|
43
|
+
if not isinstance(value, Mapping):
|
|
44
|
+
raise CodecError(f"{field} must be a TOML table")
|
|
45
|
+
return cast(Mapping[str, Any], value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _check_keys(data: Mapping[str, Any], allowed: set[str], field: str) -> None:
|
|
49
|
+
unknown = set(data).difference(allowed)
|
|
50
|
+
if unknown:
|
|
51
|
+
raise CodecError(f"{field} contains unknown field(s): {', '.join(sorted(unknown))}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _boolean(value: Any, field: str) -> bool:
|
|
55
|
+
if not isinstance(value, bool):
|
|
56
|
+
raise CodecError(f"{field} must be a boolean")
|
|
57
|
+
return value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _integer(value: Any, field: str) -> int:
|
|
61
|
+
if isinstance(value, bool) or not isinstance(value, int):
|
|
62
|
+
raise CodecError(f"{field} must be an integer")
|
|
63
|
+
return value
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _strings(value: Any, field: str) -> tuple[str, ...]:
|
|
67
|
+
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
68
|
+
raise CodecError(f"{field} must be an array of strings")
|
|
69
|
+
return tuple(value)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _duration_ms(value: Any, field: str) -> int:
|
|
73
|
+
if isinstance(value, bool):
|
|
74
|
+
raise CodecError(f"{field} must be '<integer>ms' or integer ms")
|
|
75
|
+
if isinstance(value, int):
|
|
76
|
+
if value < 0:
|
|
77
|
+
raise CodecError(f"{field} must be non-negative")
|
|
78
|
+
return value
|
|
79
|
+
if isinstance(value, str) and value.endswith("ms"):
|
|
80
|
+
raw = value[:-2]
|
|
81
|
+
if raw.isdigit():
|
|
82
|
+
return int(raw)
|
|
83
|
+
raise CodecError(f"{field} must be '<integer>ms' or integer ms")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _producer(value: Any) -> Producer | None:
|
|
87
|
+
if value is None:
|
|
88
|
+
return None
|
|
89
|
+
data = _mapping(value, "producer")
|
|
90
|
+
_check_keys(data, {"name", "version"}, "producer")
|
|
91
|
+
return Producer(name=str(data.get("name", "")), version=str(data.get("version", "")))
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _constraint(value: Any, field: str) -> CapacityConstraint | None:
|
|
95
|
+
if value is None:
|
|
96
|
+
return None
|
|
97
|
+
data = _mapping(value, field)
|
|
98
|
+
_check_keys(data, {"metric", "maximum", "guarantee"}, field)
|
|
99
|
+
return CapacityConstraint(
|
|
100
|
+
metric=str(data.get("metric", "")),
|
|
101
|
+
maximum=_integer(data.get("maximum"), f"{field}.maximum"),
|
|
102
|
+
guarantee=data.get("guarantee", "advisory"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _source_spans(value: Any, field: str) -> tuple[SourceSpan, ...]:
|
|
107
|
+
if not isinstance(value, list):
|
|
108
|
+
raise CodecError(f"{field} must be an array of tables")
|
|
109
|
+
result: list[SourceSpan] = []
|
|
110
|
+
for index, raw in enumerate(value):
|
|
111
|
+
item = _mapping(raw, f"{field}[{index}]")
|
|
112
|
+
_check_keys(item, {"locator", "start", "end"}, f"{field}[{index}]")
|
|
113
|
+
result.append(
|
|
114
|
+
SourceSpan(
|
|
115
|
+
locator=str(item.get("locator", "")),
|
|
116
|
+
start=_integer(item.get("start"), f"{field}[{index}].start"),
|
|
117
|
+
end=_integer(item.get("end"), f"{field}[{index}].end"),
|
|
118
|
+
)
|
|
119
|
+
)
|
|
120
|
+
return tuple(result)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _token_bundle(value: Any) -> TokenBundle | None:
|
|
124
|
+
if value is None:
|
|
125
|
+
return None
|
|
126
|
+
data = _mapping(value, "token")
|
|
127
|
+
_check_keys(data, {"span", "pos", "tag", "lemma", "morph"}, "token")
|
|
128
|
+
raw_span = data.get("span")
|
|
129
|
+
if not isinstance(raw_span, list):
|
|
130
|
+
raise CodecError("token.span must be an array")
|
|
131
|
+
span: list[tuple[int, int]] = []
|
|
132
|
+
for index, pair in enumerate(raw_span):
|
|
133
|
+
if not isinstance(pair, list) or len(pair) != 2:
|
|
134
|
+
raise CodecError(f"token.span[{index}] must be [start, end]")
|
|
135
|
+
span.append(
|
|
136
|
+
(_integer(pair[0], "token.span start"), _integer(pair[1], "token.span end"))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def sparse(name: str) -> tuple[tuple[int, str], ...]:
|
|
140
|
+
raw = data.get(name, [])
|
|
141
|
+
if not isinstance(raw, list):
|
|
142
|
+
raise CodecError(f"token.{name} must be an array")
|
|
143
|
+
result: list[tuple[int, str]] = []
|
|
144
|
+
for pair in raw:
|
|
145
|
+
if not isinstance(pair, list) or len(pair) != 2 or not isinstance(pair[1], str):
|
|
146
|
+
raise CodecError(f"token.{name} entries must be [index, string]")
|
|
147
|
+
result.append((_integer(pair[0], f"token.{name} index"), pair[1]))
|
|
148
|
+
return tuple(result)
|
|
149
|
+
|
|
150
|
+
return TokenBundle(
|
|
151
|
+
span=tuple(span),
|
|
152
|
+
pos=_strings(data.get("pos", []), "token.pos"),
|
|
153
|
+
tag=_strings(data.get("tag", []), "token.tag"),
|
|
154
|
+
lemma=sparse("lemma"),
|
|
155
|
+
morph=sparse("morph"),
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _speech_controls(value: Any) -> tuple[SpeechControl, ...]:
|
|
160
|
+
if not isinstance(value, list):
|
|
161
|
+
raise CodecError("speech.control must be an array of tables")
|
|
162
|
+
result: list[SpeechControl] = []
|
|
163
|
+
for index, raw in enumerate(value):
|
|
164
|
+
field = f"speech.control[{index}]"
|
|
165
|
+
data = _mapping(raw, field)
|
|
166
|
+
_check_keys(data, {"start", "end", "name", "params"}, field)
|
|
167
|
+
params = _mapping(data.get("params", {}), f"{field}.params")
|
|
168
|
+
result.append(
|
|
169
|
+
SpeechControl(
|
|
170
|
+
start=_integer(data.get("start"), f"{field}.start"),
|
|
171
|
+
end=_integer(data.get("end"), f"{field}.end"),
|
|
172
|
+
name=str(data.get("name", "")),
|
|
173
|
+
params=dict(params),
|
|
174
|
+
)
|
|
175
|
+
)
|
|
176
|
+
return tuple(result)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def profile_from_dict(raw: Mapping[str, Any]) -> RenderProfile:
|
|
180
|
+
_check_keys(
|
|
181
|
+
raw,
|
|
182
|
+
{
|
|
183
|
+
"format",
|
|
184
|
+
"schema_version",
|
|
185
|
+
"profile_id",
|
|
186
|
+
"producer",
|
|
187
|
+
"default_role",
|
|
188
|
+
"planning",
|
|
189
|
+
"pauses",
|
|
190
|
+
"roles",
|
|
191
|
+
},
|
|
192
|
+
"renderprofile",
|
|
193
|
+
)
|
|
194
|
+
roles_raw = _mapping(raw.get("roles"), "roles")
|
|
195
|
+
roles: dict[str, RoleProfile] = {}
|
|
196
|
+
for name, role_value in roles_raw.items():
|
|
197
|
+
role_field = f"roles.{name}"
|
|
198
|
+
data = _mapping(role_value, role_field)
|
|
199
|
+
_check_keys(data, {"language", "binding", "requirements", "capacity"}, role_field)
|
|
200
|
+
req_data = _mapping(data.get("requirements", {}), f"{role_field}.requirements")
|
|
201
|
+
_check_keys(
|
|
202
|
+
req_data,
|
|
203
|
+
{"token_fields", "frontend", "pronunciation"},
|
|
204
|
+
f"{role_field}.requirements",
|
|
205
|
+
)
|
|
206
|
+
cap_data = _mapping(data.get("capacity", {}), f"{role_field}.capacity")
|
|
207
|
+
_check_keys(cap_data, {"planner", "renderer"}, f"{role_field}.capacity")
|
|
208
|
+
roles[name] = RoleProfile(
|
|
209
|
+
language=str(data.get("language", "")),
|
|
210
|
+
binding=dict(_mapping(data.get("binding"), f"{role_field}.binding")),
|
|
211
|
+
requirements=RoleRequirements(
|
|
212
|
+
token_fields=_strings(
|
|
213
|
+
req_data.get("token_fields", []), f"{role_field}.requirements.token_fields"
|
|
214
|
+
),
|
|
215
|
+
frontend=str(req_data.get("frontend", "renderer")),
|
|
216
|
+
pronunciation=str(req_data.get("pronunciation", "renderer")),
|
|
217
|
+
),
|
|
218
|
+
planner_capacity=_constraint(
|
|
219
|
+
cap_data.get("planner"), f"{role_field}.capacity.planner"
|
|
220
|
+
),
|
|
221
|
+
renderer_capacity=_constraint(
|
|
222
|
+
cap_data.get("renderer"), f"{role_field}.capacity.renderer"
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
planning_raw = _mapping(raw.get("planning", {}), "planning")
|
|
226
|
+
_check_keys(planning_raw, {"split", "capacity_fallback"}, "planning")
|
|
227
|
+
pauses_raw = _mapping(raw.get("pauses", {}), "pauses")
|
|
228
|
+
pause_fields = {
|
|
229
|
+
"mode",
|
|
230
|
+
"x_weak",
|
|
231
|
+
"weak",
|
|
232
|
+
"medium",
|
|
233
|
+
"strong",
|
|
234
|
+
"x_strong",
|
|
235
|
+
"clause",
|
|
236
|
+
"sentence",
|
|
237
|
+
"paragraph",
|
|
238
|
+
"voice_change",
|
|
239
|
+
}
|
|
240
|
+
_check_keys(pauses_raw, pause_fields, "pauses")
|
|
241
|
+
durations = {
|
|
242
|
+
key: _duration_ms(value, f"pauses.{key}")
|
|
243
|
+
for key, value in pauses_raw.items()
|
|
244
|
+
if key != "mode"
|
|
245
|
+
}
|
|
246
|
+
return RenderProfile(
|
|
247
|
+
roles=roles,
|
|
248
|
+
default_role=str(raw.get("default_role", "")),
|
|
249
|
+
planning=PlanningPolicy(
|
|
250
|
+
split=planning_raw.get("split", "sentence"),
|
|
251
|
+
capacity_fallback=cast(
|
|
252
|
+
Any,
|
|
253
|
+
_strings(
|
|
254
|
+
planning_raw.get("capacity_fallback", ["clause", "token"]),
|
|
255
|
+
"planning.capacity_fallback",
|
|
256
|
+
),
|
|
257
|
+
),
|
|
258
|
+
),
|
|
259
|
+
pauses=PausePolicy(mode=pauses_raw.get("mode", "manual"), durations_ms=durations),
|
|
260
|
+
profile_id=cast(str | None, raw.get("profile_id")),
|
|
261
|
+
producer=_producer(raw.get("producer")),
|
|
262
|
+
format=str(raw.get("format", "")),
|
|
263
|
+
schema_version=_integer(raw.get("schema_version"), "schema_version"),
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def profile_to_dict(profile: RenderProfile) -> dict[str, Any]:
|
|
268
|
+
data: dict[str, Any] = {
|
|
269
|
+
"format": profile.format,
|
|
270
|
+
"schema_version": profile.schema_version,
|
|
271
|
+
"profile_id": profile.profile_id or profile.computed_id(),
|
|
272
|
+
"default_role": profile.default_role,
|
|
273
|
+
}
|
|
274
|
+
if profile.producer is not None:
|
|
275
|
+
data["producer"] = profile.producer.to_dict()
|
|
276
|
+
data["planning"] = profile.planning.to_dict()
|
|
277
|
+
data["pauses"] = profile.pauses.to_dict()
|
|
278
|
+
data["roles"] = {name: role.to_dict() for name, role in sorted(profile.roles.items())}
|
|
279
|
+
return data
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def plan_from_dict(raw: Mapping[str, Any]) -> RenderPlan:
|
|
283
|
+
_check_keys(
|
|
284
|
+
raw,
|
|
285
|
+
{
|
|
286
|
+
"format",
|
|
287
|
+
"schema_version",
|
|
288
|
+
"plan_id",
|
|
289
|
+
"source_plan_id",
|
|
290
|
+
"render_profile_id",
|
|
291
|
+
"producer",
|
|
292
|
+
"flow",
|
|
293
|
+
},
|
|
294
|
+
"renderplan",
|
|
295
|
+
)
|
|
296
|
+
flow_raw = raw.get("flow")
|
|
297
|
+
if not isinstance(flow_raw, list):
|
|
298
|
+
raise CodecError("flow must be an array of tables")
|
|
299
|
+
flow: list[Any] = []
|
|
300
|
+
for index, item_value in enumerate(flow_raw):
|
|
301
|
+
field = f"flow[{index}]"
|
|
302
|
+
item = _mapping(item_value, field)
|
|
303
|
+
kind = item.get("kind")
|
|
304
|
+
if kind == "speech":
|
|
305
|
+
_check_keys(
|
|
306
|
+
item,
|
|
307
|
+
{
|
|
308
|
+
"kind",
|
|
309
|
+
"number",
|
|
310
|
+
"segment_id",
|
|
311
|
+
"wav",
|
|
312
|
+
"text",
|
|
313
|
+
"language",
|
|
314
|
+
"role",
|
|
315
|
+
"source",
|
|
316
|
+
"split_reason",
|
|
317
|
+
"planning_level",
|
|
318
|
+
"semantic_end",
|
|
319
|
+
"preflight_required",
|
|
320
|
+
"binding",
|
|
321
|
+
"token",
|
|
322
|
+
"control",
|
|
323
|
+
},
|
|
324
|
+
field,
|
|
325
|
+
)
|
|
326
|
+
number = _integer(item.get("number"), f"{field}.number")
|
|
327
|
+
expected_wav = f"{number:06d}.wav"
|
|
328
|
+
if item.get("wav") != expected_wav:
|
|
329
|
+
raise CodecError(f"speech.wav must be {expected_wav!r}")
|
|
330
|
+
flow.append(
|
|
331
|
+
SpeechItem(
|
|
332
|
+
number=number,
|
|
333
|
+
text=str(item.get("text", "")),
|
|
334
|
+
language=str(item.get("language", "")),
|
|
335
|
+
role=str(item.get("role", "")),
|
|
336
|
+
binding=dict(_mapping(item.get("binding"), f"{field}.binding")),
|
|
337
|
+
source=_source_spans(item.get("source", []), f"{field}.source"),
|
|
338
|
+
split_reason=str(item.get("split_reason", "policy")),
|
|
339
|
+
planning_level=str(item.get("planning_level", "sentence")),
|
|
340
|
+
semantic_end=str(item.get("semantic_end", "none")),
|
|
341
|
+
tokens=_token_bundle(item.get("token")),
|
|
342
|
+
controls=_speech_controls(item.get("control", [])),
|
|
343
|
+
preflight_required=_boolean(
|
|
344
|
+
item.get("preflight_required", True), f"{field}.preflight_required"
|
|
345
|
+
),
|
|
346
|
+
segment_id=cast(str | None, item.get("segment_id")),
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
elif kind == "pause":
|
|
350
|
+
_check_keys(item, {"kind", "pause_id", "duration", "reason", "source"}, field)
|
|
351
|
+
flow.append(
|
|
352
|
+
PauseItem(
|
|
353
|
+
duration_ms=_duration_ms(item.get("duration"), f"{field}.duration"),
|
|
354
|
+
reason=str(item.get("reason", "")),
|
|
355
|
+
source=_source_spans(item.get("source", []), f"{field}.source"),
|
|
356
|
+
pause_id=cast(str | None, item.get("pause_id")),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
elif kind == "audio":
|
|
360
|
+
_check_keys(item, {"kind", "audio_id", "src", "source", "options"}, field)
|
|
361
|
+
flow.append(
|
|
362
|
+
AudioItem(
|
|
363
|
+
src=str(item.get("src", "")),
|
|
364
|
+
source=_source_spans(item.get("source", []), f"{field}.source"),
|
|
365
|
+
options=dict(_mapping(item.get("options", {}), f"{field}.options")),
|
|
366
|
+
audio_id=cast(str | None, item.get("audio_id")),
|
|
367
|
+
)
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
raise CodecError(f"{field}.kind is unsupported: {kind!r}")
|
|
371
|
+
return RenderPlan(
|
|
372
|
+
source_plan_id=str(raw.get("source_plan_id", "")),
|
|
373
|
+
render_profile_id=str(raw.get("render_profile_id", "")),
|
|
374
|
+
flow=tuple(flow),
|
|
375
|
+
plan_id=cast(str | None, raw.get("plan_id")),
|
|
376
|
+
producer=_producer(raw.get("producer")),
|
|
377
|
+
format=str(raw.get("format", "")),
|
|
378
|
+
schema_version=_integer(raw.get("schema_version"), "schema_version"),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def plan_to_dict(plan: RenderPlan) -> dict[str, Any]:
|
|
383
|
+
data: dict[str, Any] = {
|
|
384
|
+
"format": plan.format,
|
|
385
|
+
"schema_version": plan.schema_version,
|
|
386
|
+
"plan_id": plan.plan_id or plan.computed_id(),
|
|
387
|
+
"source_plan_id": plan.source_plan_id,
|
|
388
|
+
"render_profile_id": plan.render_profile_id,
|
|
389
|
+
}
|
|
390
|
+
if plan.producer is not None:
|
|
391
|
+
data["producer"] = plan.producer.to_dict()
|
|
392
|
+
flow: list[dict[str, Any]] = []
|
|
393
|
+
for item in plan.flow:
|
|
394
|
+
if isinstance(item, SpeechItem):
|
|
395
|
+
entry: dict[str, Any] = {
|
|
396
|
+
"kind": "speech",
|
|
397
|
+
"number": item.number,
|
|
398
|
+
"segment_id": item.segment_id or item.computed_id(),
|
|
399
|
+
"wav": item.wav,
|
|
400
|
+
"text": item.text,
|
|
401
|
+
"language": item.language,
|
|
402
|
+
"role": item.role,
|
|
403
|
+
"source": [span.to_dict() for span in item.source],
|
|
404
|
+
"split_reason": item.split_reason,
|
|
405
|
+
"planning_level": item.planning_level,
|
|
406
|
+
"semantic_end": item.semantic_end,
|
|
407
|
+
"preflight_required": item.preflight_required,
|
|
408
|
+
"binding": dict(item.binding),
|
|
409
|
+
}
|
|
410
|
+
if item.tokens is not None:
|
|
411
|
+
entry["token"] = item.tokens.to_dict()
|
|
412
|
+
if item.controls:
|
|
413
|
+
entry["control"] = [control.to_dict() for control in item.controls]
|
|
414
|
+
flow.append(entry)
|
|
415
|
+
elif isinstance(item, PauseItem):
|
|
416
|
+
pause_entry: dict[str, Any] = {
|
|
417
|
+
"kind": "pause",
|
|
418
|
+
"pause_id": item.pause_id or item.computed_id(),
|
|
419
|
+
"duration": f"{item.duration_ms}ms",
|
|
420
|
+
"reason": item.reason,
|
|
421
|
+
}
|
|
422
|
+
if item.source:
|
|
423
|
+
pause_entry["source"] = [span.to_dict() for span in item.source]
|
|
424
|
+
flow.append(pause_entry)
|
|
425
|
+
else:
|
|
426
|
+
audio_entry: dict[str, Any] = {
|
|
427
|
+
"kind": "audio",
|
|
428
|
+
"audio_id": item.audio_id or item.computed_id(),
|
|
429
|
+
"src": item.src,
|
|
430
|
+
"source": [span.to_dict() for span in item.source],
|
|
431
|
+
}
|
|
432
|
+
if item.options:
|
|
433
|
+
audio_entry["options"] = dict(item.options)
|
|
434
|
+
flow.append(audio_entry)
|
|
435
|
+
data["flow"] = flow
|
|
436
|
+
return data
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def preflight_from_dict(raw: Mapping[str, Any]) -> PreflightReport:
|
|
440
|
+
_check_keys(
|
|
441
|
+
raw,
|
|
442
|
+
{
|
|
443
|
+
"format",
|
|
444
|
+
"schema_version",
|
|
445
|
+
"report_id",
|
|
446
|
+
"render_plan_id",
|
|
447
|
+
"source_plan_id",
|
|
448
|
+
"render_profile_id",
|
|
449
|
+
"status",
|
|
450
|
+
"checks",
|
|
451
|
+
"replan",
|
|
452
|
+
"producer",
|
|
453
|
+
},
|
|
454
|
+
"preflight",
|
|
455
|
+
)
|
|
456
|
+
checks_raw = raw.get("checks", [])
|
|
457
|
+
replan_raw = raw.get("replan", [])
|
|
458
|
+
if not isinstance(checks_raw, list) or not isinstance(replan_raw, list):
|
|
459
|
+
raise CodecError("checks and replan must be arrays of tables")
|
|
460
|
+
checks_list: list[PreflightCheck] = []
|
|
461
|
+
for index, raw_check in enumerate(checks_raw):
|
|
462
|
+
field = f"checks[{index}]"
|
|
463
|
+
data = _mapping(raw_check, field)
|
|
464
|
+
_check_keys(
|
|
465
|
+
data,
|
|
466
|
+
{
|
|
467
|
+
"segment_number",
|
|
468
|
+
"segment_id",
|
|
469
|
+
"status",
|
|
470
|
+
"metric",
|
|
471
|
+
"measured",
|
|
472
|
+
"maximum",
|
|
473
|
+
"source",
|
|
474
|
+
"detail",
|
|
475
|
+
},
|
|
476
|
+
field,
|
|
477
|
+
)
|
|
478
|
+
checks_list.append(
|
|
479
|
+
PreflightCheck(
|
|
480
|
+
segment_number=_integer(data.get("segment_number"), f"{field}.segment_number"),
|
|
481
|
+
segment_id=str(data.get("segment_id", "")),
|
|
482
|
+
status=cast(Any, data.get("status")),
|
|
483
|
+
metric=str(data.get("metric", "")),
|
|
484
|
+
measured=cast(int | None, data.get("measured")),
|
|
485
|
+
maximum=cast(int | None, data.get("maximum")),
|
|
486
|
+
source=str(data.get("source", "")),
|
|
487
|
+
detail=cast(str | None, data.get("detail")),
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
replan_list: list[ReplanRequest] = []
|
|
491
|
+
for index, raw_request in enumerate(replan_raw):
|
|
492
|
+
field = f"replan[{index}]"
|
|
493
|
+
data = _mapping(raw_request, field)
|
|
494
|
+
_check_keys(
|
|
495
|
+
data,
|
|
496
|
+
{
|
|
497
|
+
"segment_number",
|
|
498
|
+
"segment_id",
|
|
499
|
+
"source",
|
|
500
|
+
"current_level",
|
|
501
|
+
"action",
|
|
502
|
+
"reason",
|
|
503
|
+
"metric",
|
|
504
|
+
"measured",
|
|
505
|
+
"maximum",
|
|
506
|
+
},
|
|
507
|
+
field,
|
|
508
|
+
)
|
|
509
|
+
replan_list.append(
|
|
510
|
+
ReplanRequest(
|
|
511
|
+
segment_number=_integer(data.get("segment_number"), f"{field}.segment_number"),
|
|
512
|
+
segment_id=str(data.get("segment_id", "")),
|
|
513
|
+
source=_source_spans(data.get("source", []), f"{field}.source"),
|
|
514
|
+
current_level=str(data.get("current_level", "")),
|
|
515
|
+
action=str(data.get("action", "split")),
|
|
516
|
+
reason=str(data.get("reason", "capacity")),
|
|
517
|
+
metric=cast(str | None, data.get("metric")),
|
|
518
|
+
measured=cast(int | None, data.get("measured")),
|
|
519
|
+
maximum=cast(int | None, data.get("maximum")),
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
return PreflightReport(
|
|
523
|
+
render_plan_id=str(raw.get("render_plan_id", "")),
|
|
524
|
+
source_plan_id=str(raw.get("source_plan_id", "")),
|
|
525
|
+
render_profile_id=str(raw.get("render_profile_id", "")),
|
|
526
|
+
status=cast(Any, raw.get("status")),
|
|
527
|
+
checks=tuple(checks_list),
|
|
528
|
+
replan=tuple(replan_list),
|
|
529
|
+
producer=_producer(raw.get("producer")),
|
|
530
|
+
report_id=cast(str | None, raw.get("report_id")),
|
|
531
|
+
format=str(raw.get("format", "")),
|
|
532
|
+
schema_version=_integer(raw.get("schema_version"), "schema_version"),
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def preflight_to_dict(report: PreflightReport) -> dict[str, Any]:
|
|
537
|
+
data: dict[str, Any] = {
|
|
538
|
+
"format": report.format,
|
|
539
|
+
"schema_version": report.schema_version,
|
|
540
|
+
"report_id": report.report_id or report.computed_id(),
|
|
541
|
+
"render_plan_id": report.render_plan_id,
|
|
542
|
+
"source_plan_id": report.source_plan_id,
|
|
543
|
+
"render_profile_id": report.render_profile_id,
|
|
544
|
+
"status": report.status,
|
|
545
|
+
"checks": [item.to_dict() for item in report.checks],
|
|
546
|
+
"replan": [item.to_dict() for item in report.replan],
|
|
547
|
+
}
|
|
548
|
+
if report.producer is not None:
|
|
549
|
+
data["producer"] = report.producer.to_dict()
|
|
550
|
+
return data
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def manifest_from_dict(raw: Mapping[str, Any]) -> RenderManifest:
|
|
554
|
+
_check_keys(
|
|
555
|
+
raw,
|
|
556
|
+
{
|
|
557
|
+
"format",
|
|
558
|
+
"schema_version",
|
|
559
|
+
"manifest_id",
|
|
560
|
+
"render_plan_id",
|
|
561
|
+
"complete",
|
|
562
|
+
"producer",
|
|
563
|
+
"speech",
|
|
564
|
+
},
|
|
565
|
+
"render-manifest",
|
|
566
|
+
)
|
|
567
|
+
speech_raw = raw.get("speech", [])
|
|
568
|
+
if not isinstance(speech_raw, list):
|
|
569
|
+
raise CodecError("speech must be an array of tables")
|
|
570
|
+
speech_list: list[SpeechArtifact] = []
|
|
571
|
+
for index, raw_artifact in enumerate(speech_raw):
|
|
572
|
+
field = f"speech[{index}]"
|
|
573
|
+
data = _mapping(raw_artifact, field)
|
|
574
|
+
_check_keys(
|
|
575
|
+
data,
|
|
576
|
+
{
|
|
577
|
+
"segment_number",
|
|
578
|
+
"segment_id",
|
|
579
|
+
"wav",
|
|
580
|
+
"sha256",
|
|
581
|
+
"frames",
|
|
582
|
+
"sample_rate",
|
|
583
|
+
"channels",
|
|
584
|
+
},
|
|
585
|
+
field,
|
|
586
|
+
)
|
|
587
|
+
speech_list.append(
|
|
588
|
+
SpeechArtifact(
|
|
589
|
+
segment_number=_integer(data.get("segment_number"), f"{field}.segment_number"),
|
|
590
|
+
segment_id=str(data.get("segment_id", "")),
|
|
591
|
+
wav=str(data.get("wav", "")),
|
|
592
|
+
sha256=str(data.get("sha256", "")),
|
|
593
|
+
frames=_integer(data.get("frames"), f"{field}.frames"),
|
|
594
|
+
sample_rate=_integer(data.get("sample_rate"), f"{field}.sample_rate"),
|
|
595
|
+
channels=_integer(data.get("channels"), f"{field}.channels"),
|
|
596
|
+
)
|
|
597
|
+
)
|
|
598
|
+
return RenderManifest(
|
|
599
|
+
render_plan_id=str(raw.get("render_plan_id", "")),
|
|
600
|
+
speech=tuple(speech_list),
|
|
601
|
+
complete=_boolean(raw.get("complete", False), "complete"),
|
|
602
|
+
producer=_producer(raw.get("producer")),
|
|
603
|
+
manifest_id=cast(str | None, raw.get("manifest_id")),
|
|
604
|
+
format=str(raw.get("format", "")),
|
|
605
|
+
schema_version=_integer(raw.get("schema_version"), "schema_version"),
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def manifest_to_dict(manifest: RenderManifest) -> dict[str, Any]:
|
|
610
|
+
data: dict[str, Any] = {
|
|
611
|
+
"format": manifest.format,
|
|
612
|
+
"schema_version": manifest.schema_version,
|
|
613
|
+
"manifest_id": manifest.manifest_id or manifest.computed_id(),
|
|
614
|
+
"render_plan_id": manifest.render_plan_id,
|
|
615
|
+
"complete": manifest.complete,
|
|
616
|
+
"speech": [item.to_dict() for item in manifest.speech],
|
|
617
|
+
}
|
|
618
|
+
if manifest.producer is not None:
|
|
619
|
+
data["producer"] = manifest.producer.to_dict()
|
|
620
|
+
return data
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _read(path: str | Path) -> Mapping[str, Any]:
|
|
624
|
+
location = Path(path)
|
|
625
|
+
try:
|
|
626
|
+
with location.open("rb") as stream:
|
|
627
|
+
return _mapping(tomllib.load(stream), "document")
|
|
628
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
629
|
+
raise CodecError(f"cannot read {location}: {exc}") from exc
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
def _write(path: str | Path, data: Mapping[str, Any]) -> Path:
|
|
633
|
+
location = Path(path)
|
|
634
|
+
try:
|
|
635
|
+
text = toml_dumps(dict(data))
|
|
636
|
+
location.parent.mkdir(parents=True, exist_ok=True)
|
|
637
|
+
temporary = location.with_name(location.name + ".tmp")
|
|
638
|
+
temporary.write_text(text, encoding="utf-8")
|
|
639
|
+
temporary.replace(location)
|
|
640
|
+
except OSError as exc:
|
|
641
|
+
raise CodecError(f"cannot write {location}: {exc}") from exc
|
|
642
|
+
return location
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def load_profile(path: str | Path) -> RenderProfile:
|
|
646
|
+
return profile_from_dict(_read(path))
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def save_profile(profile: RenderProfile, path: str | Path) -> Path:
|
|
650
|
+
return _write(path, profile_to_dict(profile.with_identity()))
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def load_plan(path: str | Path) -> RenderPlan:
|
|
654
|
+
return plan_from_dict(_read(path))
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def save_plan(plan: RenderPlan, path: str | Path) -> Path:
|
|
658
|
+
return _write(path, plan_to_dict(plan.with_identity()))
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def load_preflight(path: str | Path) -> PreflightReport:
|
|
662
|
+
return preflight_from_dict(_read(path))
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def save_preflight(report: PreflightReport, path: str | Path) -> Path:
|
|
666
|
+
return _write(path, preflight_to_dict(report.with_identity()))
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def load_manifest(path: str | Path) -> RenderManifest:
|
|
670
|
+
return manifest_from_dict(_read(path))
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
def save_manifest(manifest: RenderManifest, path: str | Path) -> Path:
|
|
674
|
+
return _write(path, manifest_to_dict(manifest.with_identity()))
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def load_any(path: str | Path) -> RenderProfile | RenderPlan | PreflightReport | RenderManifest:
|
|
678
|
+
raw = _read(path)
|
|
679
|
+
format_name = raw.get("format")
|
|
680
|
+
if format_name == PROFILE_FORMAT:
|
|
681
|
+
return profile_from_dict(raw)
|
|
682
|
+
if format_name == PLAN_FORMAT:
|
|
683
|
+
return plan_from_dict(raw)
|
|
684
|
+
if format_name == PREFLIGHT_FORMAT:
|
|
685
|
+
return preflight_from_dict(raw)
|
|
686
|
+
if format_name == MANIFEST_FORMAT:
|
|
687
|
+
return manifest_from_dict(raw)
|
|
688
|
+
raise CodecError(f"unknown renderplan document format: {format_name!r}")
|
renderplan/errors.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class RenderPlanError(ValueError):
|
|
2
|
+
"""Base error for invalid renderplan contract values."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(RenderPlanError):
|
|
6
|
+
"""A model violates the renderplan contract."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CodecError(RenderPlanError):
|
|
10
|
+
"""A persisted TOML document cannot be decoded or encoded."""
|