voicerender 0.1.1__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.
Files changed (39) hide show
  1. voicerender/__init__.py +127 -0
  2. voicerender/__main__.py +3 -0
  3. voicerender/_version.py +24 -0
  4. voicerender/capacity.py +678 -0
  5. voicerender/cli.py +345 -0
  6. voicerender/engines/__init__.py +68 -0
  7. voicerender/engines/api_probe.py +495 -0
  8. voicerender/engines/base.py +425 -0
  9. voicerender/engines/catalog.py +83 -0
  10. voicerender/engines/inflectsynth.py +920 -0
  11. voicerender/engines/kittensynth.py +511 -0
  12. voicerender/engines/kokoro.py +725 -0
  13. voicerender/engines/metadata.py +58 -0
  14. voicerender/engines/piper.py +173 -0
  15. voicerender/engines/pipersynth.py +570 -0
  16. voicerender/engines/pocket.py +169 -0
  17. voicerender/engines/pocketsynth.py +1234 -0
  18. voicerender/engines/registry.py +320 -0
  19. voicerender/engines/selection.py +61 -0
  20. voicerender/engines/supertonicsynth.py +707 -0
  21. voicerender/errors.py +161 -0
  22. voicerender/events.py +43 -0
  23. voicerender/handoff.py +62 -0
  24. voicerender/inputs/__init__.py +21 -0
  25. voicerender/inputs/utterplan_v5.py +897 -0
  26. voicerender/lexicons.py +42 -0
  27. voicerender/manifest.py +750 -0
  28. voicerender/models.py +499 -0
  29. voicerender/py.typed +0 -0
  30. voicerender/render.py +365 -0
  31. voicerender/render_v5.py +1193 -0
  32. voicerender/schemas/render-manifest-v2.schema.json +415 -0
  33. voicerender/storage.py +113 -0
  34. voicerender-0.1.1.dist-info/METADATA +254 -0
  35. voicerender-0.1.1.dist-info/RECORD +39 -0
  36. voicerender-0.1.1.dist-info/WHEEL +5 -0
  37. voicerender-0.1.1.dist-info/entry_points.txt +2 -0
  38. voicerender-0.1.1.dist-info/licenses/LICENSE +201 -0
  39. voicerender-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,678 @@
1
+ """VoiceRender-owned request fitting, subdivision, and result merging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from collections.abc import Iterable, Mapping
7
+ from dataclasses import asdict, dataclass, field, is_dataclass
8
+ from enum import Enum
9
+ from pathlib import Path
10
+ from typing import Any, Literal, cast
11
+
12
+ import numpy as np
13
+
14
+ from .engines.base import (
15
+ PronunciationSpan,
16
+ RenderedSpeech,
17
+ RequestMeasure,
18
+ SpeechRequest,
19
+ SpeechToken,
20
+ SpeechWordTiming,
21
+ validate_rendered_speech,
22
+ )
23
+ from .errors import EngineBackendError, SpeechRequestTooLongError
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class SemanticBoundary:
28
+ """A planner-authorized request split position and its provenance."""
29
+
30
+ position: int
31
+ kind: str
32
+ semantic_boundary_id: str | None = None
33
+ origin: str | None = None
34
+
35
+
36
+ LOWERING_SCHEMA = "voicerender.atomic-lowering.v1"
37
+ CAPACITY_SCHEMA = "voicerender.capacity-fitting.v1"
38
+
39
+
40
+ def _json_value(value: Any) -> Any:
41
+ if is_dataclass(value) and not isinstance(value, type):
42
+ return _json_value(asdict(cast(Any, value)))
43
+ if isinstance(value, Enum):
44
+ return _json_value(value.value)
45
+ if isinstance(value, Path):
46
+ return str(value)
47
+ if isinstance(value, Mapping):
48
+ return {str(key): _json_value(item) for key, item in value.items()}
49
+ if isinstance(value, (tuple, list)):
50
+ return [_json_value(item) for item in value]
51
+ if isinstance(value, bool | int | str) or value is None:
52
+ return value
53
+ if isinstance(value, float):
54
+ if math.isfinite(value):
55
+ return value
56
+ raise ValueError("capacity metadata must contain finite numbers")
57
+ if hasattr(value, "tolist"):
58
+ return _json_value(value.tolist())
59
+ raise TypeError(f"capacity metadata is not JSON-compatible: {type(value).__name__}")
60
+
61
+
62
+ @dataclass(frozen=True, slots=True)
63
+ class CapacityContext:
64
+ """VoiceRender-owned planning hints for request-capacity subdivision."""
65
+
66
+ semantic_boundaries: tuple[SemanticBoundary, ...] = ()
67
+ linguistic_token_ranges: tuple[tuple[int, int], ...] = ()
68
+ protected_ranges: tuple[tuple[int, int], ...] = ()
69
+ planned_semantics: bool = False
70
+
71
+
72
+ @dataclass(frozen=True, slots=True)
73
+ class CapacityBoundary:
74
+ """One legal split candidate with priority and provenance."""
75
+
76
+ position: int
77
+ priority: int
78
+ kind: str
79
+ source: str
80
+ semantic_boundary_id: str | None = None
81
+ origin: str | None = None
82
+
83
+
84
+ @dataclass(frozen=True, slots=True)
85
+ class AtomicRequest:
86
+ """One exact child request rendered for a semantic parent request."""
87
+
88
+ request: SpeechRequest
89
+ parent_id: str
90
+ index: int
91
+ char_start: int
92
+ char_end: int
93
+ measure: RequestMeasure | None = None
94
+ render: Mapping[str, Any] = field(default_factory=dict)
95
+ split: Mapping[str, Any] = field(default_factory=dict)
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ measure = self.measure
99
+ request = self.request
100
+ return {
101
+ "id": request.id,
102
+ "parent_id": self.parent_id,
103
+ "index": self.index,
104
+ "char_start": self.char_start,
105
+ "char_end": self.char_end,
106
+ "text": request.text,
107
+ "request": _json_value(
108
+ {
109
+ "id": request.id,
110
+ "text": request.text,
111
+ "language": request.language,
112
+ "voice": request.voice,
113
+ "speaker": request.speaker,
114
+ "pronunciation_overrides": request.pronunciation_overrides,
115
+ "tokens": request.tokens,
116
+ "whole_request_phonemes": request.whole_request_phonemes,
117
+ "options": request.options,
118
+ }
119
+ ),
120
+ "measure": (
121
+ {
122
+ "fits": measure.fits,
123
+ "amount": measure.amount,
124
+ "maximum": measure.maximum,
125
+ "unit": measure.unit,
126
+ "source": measure.source,
127
+ "details": _json_value(measure.details),
128
+ }
129
+ if measure is not None
130
+ else None
131
+ ),
132
+ "render": _json_value(self.render),
133
+ "split": _json_value(self.split),
134
+ }
135
+
136
+
137
+ @dataclass(frozen=True, slots=True)
138
+ class AtomicRender:
139
+ """Merged result plus the exact child requests used to produce it."""
140
+
141
+ result: RenderedSpeech
142
+ requests: tuple[AtomicRequest, ...]
143
+
144
+ @property
145
+ def manifest(self) -> dict[str, Any]:
146
+ return {
147
+ "schema": LOWERING_SCHEMA,
148
+ "capacity_schema": CAPACITY_SCHEMA,
149
+ "requests": [item.to_dict() for item in self.requests],
150
+ }
151
+
152
+
153
+ @dataclass(frozen=True, slots=True)
154
+ class _RenderedChild:
155
+ request: SpeechRequest
156
+ start: int
157
+ end: int
158
+ measure: RequestMeasure
159
+ result: RenderedSpeech
160
+ split: Mapping[str, Any] = field(default_factory=dict)
161
+
162
+
163
+ def render_atomic_request(
164
+ session: Any,
165
+ request: SpeechRequest,
166
+ *,
167
+ capacity: CapacityContext | None = None,
168
+ protected_ranges: Iterable[tuple[int, int]] = (),
169
+ ) -> AtomicRender:
170
+ """Measure, fit, render, and merge one exact VoiceRender speech request."""
171
+ capacity_context = capacity if capacity is not None else CapacityContext()
172
+ protected = _collect_protected_ranges(
173
+ request,
174
+ (
175
+ *capacity_context.linguistic_token_ranges,
176
+ *capacity_context.protected_ranges,
177
+ *protected_ranges,
178
+ ),
179
+ )
180
+ measured: dict[tuple[int, int], RequestMeasure] = {}
181
+
182
+ def child_request(start: int, end: int) -> SpeechRequest:
183
+ return _slice_request(request, start, end)
184
+
185
+ def measure_range(start: int, end: int) -> RequestMeasure:
186
+ key = (start, end)
187
+ if key in measured:
188
+ return measured[key]
189
+ candidate = child_request(start, end)
190
+ measure_method = getattr(session, "measure", None)
191
+ if not callable(measure_method):
192
+ result = RequestMeasure(
193
+ fits=None,
194
+ amount=None,
195
+ maximum=None,
196
+ unit="unknown",
197
+ source="measurement.unavailable",
198
+ )
199
+ else:
200
+ try:
201
+ result = measure_method(candidate)
202
+ except SpeechRequestTooLongError as exc:
203
+ result = _measure_from_error(exc, candidate, "measurement.too_long")
204
+ if not isinstance(result, RequestMeasure):
205
+ result = RequestMeasure(
206
+ fits=None,
207
+ amount=None,
208
+ maximum=None,
209
+ unit="unknown",
210
+ source="measurement.indeterminate",
211
+ )
212
+ measured[key] = result
213
+ return result
214
+
215
+ def render_range(
216
+ start: int,
217
+ end: int,
218
+ split_context: Mapping[str, Any] | None = None,
219
+ ) -> list[_RenderedChild]:
220
+ candidate = child_request(start, end)
221
+ capacity_measure = measure_range(start, end)
222
+ if capacity_measure.fits is not False:
223
+ try:
224
+ result = session.synthesize(candidate)
225
+ except SpeechRequestTooLongError as exc:
226
+ capacity_measure = _measure_from_error(
227
+ exc, candidate, "synthesis.too_long"
228
+ )
229
+ measured[(start, end)] = capacity_measure
230
+ else:
231
+ validate_rendered_speech(candidate, result)
232
+ return [
233
+ _RenderedChild(
234
+ candidate,
235
+ start,
236
+ end,
237
+ capacity_measure,
238
+ result,
239
+ dict(split_context or {}),
240
+ )
241
+ ]
242
+ if request.whole_request_phonemes is not None:
243
+ raise _capacity_error(
244
+ candidate,
245
+ capacity_measure,
246
+ "whole-request phonemes cannot be subdivided safely",
247
+ )
248
+ boundaries = _candidate_boundaries(
249
+ request,
250
+ start,
251
+ end,
252
+ protected,
253
+ capacity_context,
254
+ )
255
+ if not boundaries:
256
+ raise _capacity_error(
257
+ candidate, capacity_measure, "no legal source-text split boundary"
258
+ )
259
+ split_boundary = _choose_boundary(
260
+ request,
261
+ start,
262
+ end,
263
+ boundaries,
264
+ capacity_measure,
265
+ measure_range,
266
+ )
267
+ split_position = split_boundary.position
268
+ if split_position <= start or split_position >= end:
269
+ raise _capacity_error(
270
+ candidate, capacity_measure, "no legal source-text split boundary"
271
+ )
272
+ split_provenance = _split_provenance(split_boundary)
273
+ return render_range(start, split_position, split_provenance) + render_range(
274
+ split_position,
275
+ end,
276
+ split_provenance,
277
+ )
278
+
279
+ children = render_range(0, len(request.text))
280
+ if "".join(child.request.text for child in children) != request.text:
281
+ raise AssertionError("capacity fitting did not preserve exact request text")
282
+
283
+ atomic_requests = tuple(
284
+ AtomicRequest(
285
+ request=child.request,
286
+ parent_id=request.id,
287
+ index=index,
288
+ char_start=child.start,
289
+ char_end=child.end,
290
+ measure=child.measure,
291
+ render={
292
+ "sample_rate": child.result.sample_rate,
293
+ "frames": int(np.asarray(child.result.audio).size),
294
+ "warnings": list(child.result.warnings),
295
+ "metadata": _json_value(child.result.metadata),
296
+ },
297
+ split=child.split,
298
+ )
299
+ for index, child in enumerate(children)
300
+ )
301
+ result = _merge_results(request, children, atomic_requests)
302
+ validate_rendered_speech(request, result)
303
+ return AtomicRender(result=result, requests=atomic_requests)
304
+
305
+
306
+ def _collect_protected_ranges(
307
+ request: SpeechRequest,
308
+ extra: Iterable[tuple[int, int]],
309
+ ) -> tuple[tuple[int, int], ...]:
310
+ ranges = [(token.start, token.end) for token in request.tokens]
311
+ ranges.extend((span.start, span.end) for span in request.pronunciation_overrides)
312
+ ranges.extend(extra)
313
+ for start, end in ranges:
314
+ if start < 0 or start >= end or end > len(request.text):
315
+ raise ValueError(
316
+ "protected request range is outside the exact request text"
317
+ )
318
+ return tuple(sorted(set(ranges)))
319
+
320
+
321
+ def _slice_request(request: SpeechRequest, start: int, end: int) -> SpeechRequest:
322
+ text = request.text[start:end]
323
+ if start == 0 and end == len(request.text):
324
+ child_id = request.id
325
+ else:
326
+ child_id = f"{request.id}.part-{start:06d}-{end:06d}"
327
+ tokens = tuple(
328
+ SpeechToken(
329
+ start=token.start - start,
330
+ end=token.end - start,
331
+ text=token.text,
332
+ pos=token.pos,
333
+ tag=token.tag,
334
+ lemma=token.lemma,
335
+ morph=token.morph,
336
+ language=token.language,
337
+ )
338
+ for token in request.tokens
339
+ if token.start >= start and token.end <= end
340
+ )
341
+ overrides = tuple(
342
+ PronunciationSpan(
343
+ start=span.start - start,
344
+ end=span.end - start,
345
+ phonemes=span.phonemes,
346
+ language=span.language,
347
+ alphabet=span.alphabet,
348
+ )
349
+ for span in request.pronunciation_overrides
350
+ if span.start >= start and span.end <= end
351
+ )
352
+ return SpeechRequest(
353
+ id=child_id,
354
+ text=text,
355
+ language=request.language,
356
+ voice=request.voice,
357
+ speaker=request.speaker,
358
+ pronunciation_overrides=overrides,
359
+ tokens=tokens,
360
+ whole_request_phonemes=request.whole_request_phonemes,
361
+ options=dict(request.options),
362
+ )
363
+
364
+
365
+ def _candidate_boundaries(
366
+ request: SpeechRequest,
367
+ start: int,
368
+ end: int,
369
+ protected: tuple[tuple[int, int], ...],
370
+ capacity: CapacityContext,
371
+ ) -> tuple[CapacityBoundary, ...]:
372
+ text = request.text
373
+ candidates: dict[int, CapacityBoundary] = {}
374
+ planned = capacity.planned_semantics
375
+
376
+ def add(
377
+ position: int,
378
+ priority: int,
379
+ kind: str,
380
+ source: str = "voicerender.heuristic",
381
+ *,
382
+ semantic_boundary_id: str | None = None,
383
+ origin: str | None = None,
384
+ ) -> None:
385
+ if not start < position < end:
386
+ return
387
+ if any(left < position < right for left, right in protected):
388
+ return
389
+ if not text[start:position].strip() or not text[position:end].strip():
390
+ return
391
+ candidate = CapacityBoundary(
392
+ position=position,
393
+ priority=priority,
394
+ kind=kind,
395
+ source=source,
396
+ semantic_boundary_id=semantic_boundary_id,
397
+ origin=origin,
398
+ )
399
+ existing = candidates.get(position)
400
+ if existing is None or _prefer_candidate(candidate, existing):
401
+ candidates[position] = candidate
402
+
403
+ if planned:
404
+ semantic_priorities = {"clause": 0, "parenthetical": 1}
405
+ for boundary in capacity.semantic_boundaries:
406
+ priority = semantic_priorities.get(boundary.kind)
407
+ if priority is None:
408
+ continue
409
+ add(
410
+ boundary.position,
411
+ priority,
412
+ f"semantic_{boundary.kind}",
413
+ "utterplan",
414
+ semantic_boundary_id=boundary.semantic_boundary_id,
415
+ origin=boundary.origin,
416
+ )
417
+
418
+ index = start
419
+ while index < end:
420
+ if text[index] in "\r\n":
421
+ position = index + 1
422
+ while position < end and text[position] in "\r\n":
423
+ position += 1
424
+ add(position, 2 if planned else 0, "newline")
425
+ index = position
426
+ continue
427
+ index += 1
428
+
429
+ if not planned:
430
+ sentence_marks = ".!?。!?"
431
+ closing = "\"'’”»)]}"
432
+ index = start
433
+ while index < end:
434
+ if text[index] in sentence_marks:
435
+ position = index + 1
436
+ while position < end and text[position] in sentence_marks + closing:
437
+ position += 1
438
+ while position < end and text[position] in " \t":
439
+ position += 1
440
+ add(position, 1, "sentence_punctuation")
441
+ index += 1
442
+
443
+ clause_marks = ";:,,;:、"
444
+ index = start
445
+ while index < end:
446
+ if text[index] in clause_marks:
447
+ position = index + 1
448
+ while position < end and text[position] in " \t":
449
+ position += 1
450
+ add(
451
+ position,
452
+ 3 if planned else 2,
453
+ "punctuation_clause",
454
+ )
455
+ index += 1
456
+
457
+ token_priority = 4 if planned else 3
458
+ for token_start, token_end in capacity.linguistic_token_ranges:
459
+ add(token_start, token_priority, "token_edge", "utterplan")
460
+ add(token_end, token_priority, "token_edge", "utterplan")
461
+ for token in request.tokens:
462
+ add(token.start, token_priority, "token_edge", "request.tokens")
463
+ add(token.end, token_priority, "token_edge", "request.tokens")
464
+
465
+ index = start
466
+ while index < end:
467
+ if text[index].isspace():
468
+ position = index + 1
469
+ while position < end and text[position].isspace():
470
+ position += 1
471
+ add(position, 5 if planned else 4, "whitespace")
472
+ index = position
473
+ continue
474
+ index += 1
475
+
476
+ if not planned:
477
+ for position in range(start + 1, end):
478
+ add(position, 5, "character")
479
+ return tuple(
480
+ sorted(
481
+ candidates.values(),
482
+ key=lambda candidate: (
483
+ candidate.priority,
484
+ candidate.position,
485
+ candidate.kind,
486
+ ),
487
+ )
488
+ )
489
+
490
+
491
+ def _prefer_candidate(candidate: CapacityBoundary, current: CapacityBoundary) -> bool:
492
+ if candidate.priority != current.priority:
493
+ return candidate.priority < current.priority
494
+ if candidate.source == "utterplan" and current.source != "utterplan":
495
+ return True
496
+ if current.source == "utterplan" and candidate.source != "utterplan":
497
+ return False
498
+ return (candidate.kind, candidate.semantic_boundary_id or "") < (
499
+ current.kind,
500
+ current.semantic_boundary_id or "",
501
+ )
502
+
503
+
504
+ def _choose_boundary(
505
+ request: SpeechRequest,
506
+ start: int,
507
+ end: int,
508
+ boundaries: tuple[CapacityBoundary, ...],
509
+ parent_measure: RequestMeasure,
510
+ measure_range: Any,
511
+ ) -> CapacityBoundary:
512
+ groups: dict[int, list[CapacityBoundary]] = {}
513
+ for boundary in boundaries:
514
+ groups.setdefault(boundary.priority, []).append(boundary)
515
+ ordered_groups = [
516
+ sorted(groups[priority], key=lambda boundary: boundary.position)
517
+ for priority in sorted(groups)
518
+ ]
519
+ fallback = min(
520
+ ordered_groups[0],
521
+ key=lambda boundary: (
522
+ abs(boundary.position - (start + end) / 2),
523
+ boundary.position,
524
+ ),
525
+ )
526
+ if parent_measure.fits is None:
527
+ return fallback
528
+
529
+ for boundaries_at_priority in ordered_groups:
530
+ low = 0
531
+ high = len(boundaries_at_priority) - 1
532
+ while low <= high:
533
+ middle = (low + high) // 2
534
+ boundary = boundaries_at_priority[middle]
535
+ left = measure_range(start, boundary.position)
536
+ right = measure_range(boundary.position, end)
537
+ if left.fits is True and right.fits is True:
538
+ return boundary
539
+ if left.fits is None or right.fits is None:
540
+ break
541
+ if left.fits is False and right.fits is True:
542
+ high = middle - 1
543
+ elif left.fits is True and right.fits is False:
544
+ low = middle + 1
545
+ else:
546
+ break
547
+ return fallback
548
+
549
+
550
+ def _split_provenance(boundary: CapacityBoundary) -> dict[str, Any]:
551
+ provenance: dict[str, Any] = {
552
+ "reason": "model_capacity",
553
+ "boundary_kind": boundary.kind,
554
+ "source": boundary.source,
555
+ }
556
+ if boundary.semantic_boundary_id is not None:
557
+ provenance["semantic_boundary_id"] = boundary.semantic_boundary_id
558
+ if boundary.origin is not None:
559
+ provenance["origin"] = boundary.origin
560
+ return provenance
561
+
562
+
563
+ def _measure_from_error(
564
+ error: SpeechRequestTooLongError,
565
+ request: SpeechRequest,
566
+ source: str,
567
+ ) -> RequestMeasure:
568
+ if error.unit == "model_tokens":
569
+ unit: Literal["model_tokens", "phoneme_ids", "unknown"] = "model_tokens"
570
+ elif error.unit == "phoneme_ids":
571
+ unit = "phoneme_ids"
572
+ else:
573
+ unit = "unknown"
574
+ return RequestMeasure(
575
+ fits=False,
576
+ amount=error.amount,
577
+ maximum=error.maximum,
578
+ unit=unit,
579
+ source=source,
580
+ details={
581
+ "native_error_type": getattr(error, "native_error_type", None),
582
+ "request_id": request.id,
583
+ "text_length": len(request.text),
584
+ },
585
+ )
586
+
587
+
588
+ def _capacity_error(
589
+ request: SpeechRequest,
590
+ measure: RequestMeasure,
591
+ reason: str,
592
+ ) -> SpeechRequestTooLongError:
593
+ return SpeechRequestTooLongError(
594
+ f"VoiceRender cannot fit request {request.id!r}: {reason}.",
595
+ language=request.language,
596
+ voice=request.voice,
597
+ speaker=request.speaker,
598
+ request_id=request.id,
599
+ amount=measure.amount,
600
+ maximum=measure.maximum,
601
+ unit=measure.unit,
602
+ text_length=len(request.text),
603
+ source="voicerender.capacity",
604
+ details={
605
+ "capacity_source": measure.source,
606
+ "capacity_reason": reason,
607
+ },
608
+ )
609
+
610
+
611
+ def _merge_results(
612
+ request: SpeechRequest,
613
+ children: list[_RenderedChild],
614
+ atomic_requests: tuple[AtomicRequest, ...],
615
+ ) -> RenderedSpeech:
616
+ sample_rate = children[0].result.sample_rate
617
+ arrays: list[np.ndarray] = []
618
+ timings: list[SpeechWordTiming] = []
619
+ warnings: list[str] = []
620
+ sample_offset = 0
621
+ for child in children:
622
+ result = child.result
623
+ if result.sample_rate != sample_rate:
624
+ raise EngineBackendError(
625
+ "Atomic request children returned different sample rates.",
626
+ language=request.language,
627
+ voice=request.voice,
628
+ speaker=request.speaker,
629
+ request_id=request.id,
630
+ details={
631
+ "expected_sample_rate": sample_rate,
632
+ "actual_sample_rate": result.sample_rate,
633
+ "child_request_id": child.request.id,
634
+ },
635
+ )
636
+ audio = np.asarray(result.audio, dtype=np.float32)
637
+ arrays.append(audio)
638
+ timings.extend(
639
+ SpeechWordTiming(
640
+ text=timing.text,
641
+ char_start=child.start + timing.char_start,
642
+ char_end=child.start + timing.char_end,
643
+ start_sample=sample_offset + timing.start_sample,
644
+ end_sample=sample_offset + timing.end_sample,
645
+ )
646
+ for timing in result.word_timings
647
+ )
648
+ for warning in result.warnings:
649
+ if warning not in warnings:
650
+ warnings.append(warning)
651
+ sample_offset += int(audio.size)
652
+
653
+ metadata = dict(children[0].result.metadata)
654
+ metadata["atomic_lowering"] = {
655
+ "schema": LOWERING_SCHEMA,
656
+ "capacity_schema": CAPACITY_SCHEMA,
657
+ "requests": [item.to_dict() for item in atomic_requests],
658
+ }
659
+ return RenderedSpeech(
660
+ id=request.id,
661
+ audio=np.concatenate(arrays).astype(np.float32, copy=False),
662
+ sample_rate=sample_rate,
663
+ warnings=tuple(warnings),
664
+ word_timings=tuple(timings),
665
+ metadata=metadata,
666
+ )
667
+
668
+
669
+ __all__ = [
670
+ "CAPACITY_SCHEMA",
671
+ "LOWERING_SCHEMA",
672
+ "AtomicRender",
673
+ "AtomicRequest",
674
+ "CapacityBoundary",
675
+ "CapacityContext",
676
+ "SemanticBoundary",
677
+ "render_atomic_request",
678
+ ]