Spaces:
Sleeping
Sleeping
| """Core data model for EchoScript. | |
| A `Transcript` is the canonical representation of "what was said" in an | |
| audio file. It is produced exactly once per upload (optionally restricted | |
| to a start/end time window). Every other artifact -- translations, | |
| subtitle files, future summaries/keywords -- is derived from a Transcript | |
| and must never reach back into the original audio. | |
| Audio -> Transcript -> Outputs (allowed) | |
| Audio -> Translation (never) | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field | |
| from typing import Optional | |
| class Segment: | |
| """A single timed chunk of text (transcribed or translated).""" | |
| index: int | |
| start: float # seconds, relative to the processed audio window | |
| end: float # seconds | |
| text: str | |
| def duration(self) -> float: | |
| return max(0.0, self.end - self.start) | |
| class Transcript: | |
| """The canonical transcript of an audio file. | |
| This is the single source of truth for everything downstream. If a user | |
| edits text in the UI (see v1.1: Transcript Editing), that edit happens | |
| on this object, and every translation/subtitle export regenerated after | |
| the edit will reflect it automatically. | |
| """ | |
| source_filename: str | |
| language: str # ISO 639-1 code detected/forced, e.g. "fr" | |
| language_probability: float # 0..1, Whisper's detection confidence | |
| duration: float # seconds, of the processed window | |
| segments: list[Segment] = field(default_factory=list) | |
| # Optional processing window applied to the source audio, in seconds, | |
| # relative to the original file. None means "from the very start" / | |
| # "to the very end" for that side of the window. | |
| window_start: Optional[float] = None | |
| window_end: Optional[float] = None | |
| def text(self) -> str: | |
| """Full plain-text transcript, segments joined with newlines.""" | |
| return "\n".join(s.text for s in self.segments) | |
| def word_count(self) -> int: | |
| return len(self.text.split()) | |
| def replace_text(self, new_text: str) -> None: | |
| """Used by the (future) transcript-editing feature. | |
| Re-flows freeform edited text back across the existing segment | |
| timings as evenly as possible, so timing-dependent outputs (SRT/VTT) | |
| keep working after a manual correction. Intentionally simple for | |
| v1.0; a smarter alignment can replace this later without touching | |
| any other service. | |
| """ | |
| lines = new_text.split("\n") | |
| if len(lines) != len(self.segments): | |
| # Fallback: dump everything into the first segment rather than | |
| # silently losing edited text. | |
| if self.segments: | |
| self.segments = [ | |
| Segment( | |
| index=1, | |
| start=self.segments[0].start, | |
| end=self.segments[-1].end, | |
| text=new_text.strip(), | |
| ) | |
| ] | |
| return | |
| self.segments = [ | |
| Segment(index=seg.index, start=seg.start, end=seg.end, text=line.strip()) | |
| for seg, line in zip(self.segments, lines) | |
| ] | |
| def to_dict(self) -> dict: | |
| return { | |
| "source_filename": self.source_filename, | |
| "language": self.language, | |
| "language_probability": self.language_probability, | |
| "duration": self.duration, | |
| "window_start": self.window_start, | |
| "window_end": self.window_end, | |
| "word_count": self.word_count, | |
| "segments": [ | |
| {"index": s.index, "start": s.start, "end": s.end, "text": s.text} | |
| for s in self.segments | |
| ], | |
| } | |
| class Translation: | |
| """A translation of a Transcript into a target language. | |
| Always derived from `Transcript.text` / per-segment text, never from | |
| the original audio. Segment timings are copied 1:1 from the source | |
| transcript so subtitle generation keeps working on translated output. | |
| """ | |
| source_language: str | |
| target_language: str | |
| segments: list[Segment] = field(default_factory=list) | |
| def text(self) -> str: | |
| return "\n".join(s.text for s in self.segments) | |
| def word_count(self) -> int: | |
| return len(self.text.split()) | |
| def to_dict(self) -> dict: | |
| return { | |
| "source_language": self.source_language, | |
| "target_language": self.target_language, | |
| "word_count": self.word_count, | |
| "segments": [ | |
| {"index": s.index, "start": s.start, "end": s.end, "text": s.text} | |
| for s in self.segments | |
| ], | |
| } | |