nida-tts / src /nida /data /types.py
Bilal7Dev's picture
Nida v0.5 (research preview): weights, 16 voices, code, model card
c0bce59 verified
Raw History Blame Contribute Delete
4.41 kB
"""Records shared by the data-pipeline stages.
All times are integer milliseconds relative to the start of the *segment* (``*_ms``)
unless a name says otherwise (``file_*_ms`` = relative to the source file). Confidences
are probabilities in [0, 1].
"""
from __future__ import annotations
import hashlib
import math
from dataclasses import dataclass, field
from nida.text.turkish import nfc
__all__ = [
"EventDetection",
"QuarantineRecord",
"TimedWord",
"WordStyleScore",
"segment_id",
]
def segment_id(source_id: str, file_id: str, start_ms: int, end_ms: int) -> str:
"""Content-addressed segment id: ``sha1(source, file, start_ms, end_ms)[:16]``.
Fields are joined with U+001F (unit separator) so that no field value can make two
different tuples collide. Ids are NFC-normalised first: the same file name in NFD
(e.g. from macOS) must not get a second id (train/test leakage).
Raises:
ValueError: if a field contains U+001F, a time is not an int, or the times are not
``0 <= start < end``.
"""
source_id, file_id = nfc(source_id), nfc(file_id)
for name, value in (("source_id", source_id), ("file_id", file_id)):
if not value or "\x1f" in value:
raise ValueError(f"invalid {name} {value!r}")
_check_ms(start_ms, "start_ms")
_check_ms(end_ms, "end_ms")
if not 0 <= start_ms < end_ms:
raise ValueError(f"invalid segment times {start_ms}..{end_ms} ms")
key = "\x1f".join((source_id, file_id, str(start_ms), str(end_ms)))
return hashlib.sha1(key.encode("utf-8")).hexdigest()[:16]
def _check_ms(value: object, what: str) -> None:
"""Times are integer ms; a float (e.g. seconds from an aligner) is a caller bug."""
if type(value) is not int:
raise ValueError(f"{what}: time must be an int number of ms, got {value!r}")
def _check_interval(start_ms: int, end_ms: int, what: str) -> None:
_check_ms(start_ms, what)
_check_ms(end_ms, what)
if start_ms < 0 or end_ms <= start_ms:
raise ValueError(f"{what}: invalid interval {start_ms}..{end_ms} ms")
def _check_prob(value: float, what: str) -> None:
if not (math.isfinite(value) and 0.0 <= value <= 1.0):
raise ValueError(f"{what}: probability {value!r} not in [0, 1]")
@dataclass(frozen=True)
class TimedWord:
"""One word of the primary transcript with its forced-alignment interval (S5)."""
text: str
start_ms: int
end_ms: int
align_score: float | None = None
def __post_init__(self) -> None:
if not self.text or any(ch.isspace() for ch in self.text):
raise ValueError(f"word text must be non-empty without whitespace: {self.text!r}")
_check_interval(self.start_ms, self.end_ms, f"word {self.text!r}")
@property
def duration_ms(self) -> int:
return self.end_ms - self.start_ms
@dataclass(frozen=True)
class EventDetection:
"""A detected non-verbal event (S6). ``kind`` is a tag-spec event id (``laugh``, …)."""
kind: str
start_ms: int
end_ms: int
conf: float
source: str
def __post_init__(self) -> None:
_check_interval(self.start_ms, self.end_ms, f"event {self.kind!r}")
_check_prob(self.conf, f"event {self.kind!r}")
if not self.source:
raise ValueError("event source must be non-empty")
@property
def duration_ms(self) -> int:
return self.end_ms - self.start_ms
@dataclass(frozen=True)
class WordStyleScore:
"""Per-word probability that a word is spoken in span style ``style`` (S6).
``scores[i]`` belongs to word ``i`` of the segment; ``None`` = not measurable (e.g.
word too short for a pitch estimate), which counts as "not marked".
"""
style: str
scores: tuple[float | None, ...]
source: str
def __post_init__(self) -> None:
for score in self.scores:
if score is not None:
_check_prob(score, f"style {self.style!r}")
@dataclass(frozen=True)
class QuarantineRecord:
"""An item dropped by a stage, with the reason (never dropped silently)."""
item_id: str
stage: str
reason: str
details: dict[str, object] = field(default_factory=dict)
def __post_init__(self) -> None:
if not (self.item_id and self.stage and self.reason):
raise ValueError("quarantine record needs item_id, stage and reason")