Download src/speech_text_data_aligners/application/annotate.py from vertox-ai/speech-text-data-aligners: direct link, hf CLI and curl.
- Browser
- Download file 15.8 kB
-
https://huggingface.co/vertox-ai/speech-text-data-aligners/resolve/main/src/speech_text_data_aligners/application/annotate.py
- Command line
-
hf download hf://vertox-ai/speech-text-data-aligners/src/speech_text_data_aligners/application/annotate.py
-
curl -L -o annotate.py https://huggingface.co/vertox-ai/speech-text-data-aligners/resolve/main/src/speech_text_data_aligners/application/annotate.py
15.8 kB
| # License: Apache-2.0 License | |
| # Created by: Patrick Lumbantobing, VertoX-AI | |
| # Copyright (c) 2026 VertoX-AI. All rights reserved. | |
| # | |
| # This work is licensed under the Apache-2.0 License. | |
| # To view a copy of this license, visit | |
| # https://www.apache.org/licenses/LICENSE-2.0 | |
| """Orchestrate reusable metadata annotation with injected alignment backends.""" | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import os | |
| import tempfile | |
| import time | |
| from dataclasses import asdict, dataclass | |
| from datetime import UTC, datetime | |
| from pathlib import Path | |
| from typing import cast | |
| from speech_text_data_aligners.core.errors import ( | |
| BackendOutputError, | |
| ConfigurationError, | |
| SpeechTextAlignerError, | |
| failure_from_error, | |
| ) | |
| from speech_text_data_aligners.core.extraction import ( | |
| BOUNDARY_COLUMNS, | |
| extract_boundary_fields, | |
| ) | |
| from speech_text_data_aligners.core.models import ( | |
| AlignmentFailure, | |
| AlignmentOutcome, | |
| AlignmentRequest, | |
| ) | |
| from speech_text_data_aligners.core.normalization import lexicalize | |
| from speech_text_data_aligners.core.protocols import AlignmentBackend | |
| from speech_text_data_aligners.core.validation import ( | |
| find_path_aliases, | |
| validate_alignment_result, | |
| ) | |
| from speech_text_data_aligners.data.audio import ( | |
| make_alignment_request, | |
| resolve_audio_path, | |
| ) | |
| from speech_text_data_aligners.data.metadata import ( | |
| MetadataTable, | |
| read_metadata, | |
| write_annotated_metadata, | |
| ) | |
| from speech_text_data_aligners.runtime.receipts import ( | |
| RunReceipt, | |
| canonical_fingerprint, | |
| capture_host, | |
| sha256_file, | |
| write_receipt, | |
| ) | |
| _PROJECT_VERSION = "0.1.0" | |
| class AnnotationOptions: | |
| """Control failure policy and artifact publication for annotation. | |
| Parameters | |
| ---------- | |
| strict: Abort on the first row failure and publish no output CSV. | |
| overwrite: Explicitly permit replacement of output/diagnostic/receipt files. | |
| diagnostics_path: Optional failure JSON path; failures default beside output. | |
| receipt_path: Optional run-receipt JSON path. | |
| """ | |
| strict: bool = False | |
| overwrite: bool = False | |
| diagnostics_path: Path | None = None | |
| receipt_path: Path | None = None | |
| _DEFAULT_ANNOTATION_OPTIONS = AnnotationOptions() | |
| class AnnotationRun: | |
| """Describe one completed annotation publication and all ordered outcomes. | |
| Parameters | |
| ---------- | |
| output_csv: Published enriched metadata path. | |
| outcomes: Exactly one ordered outcome per source row. | |
| diagnostics_path: Published failure artifact, if failures occurred. | |
| receipt: Captured deterministic run receipt value. | |
| receipt_path: Published receipt path when requested. | |
| """ | |
| output_csv: Path | |
| outcomes: tuple[AlignmentOutcome, ...] | |
| diagnostics_path: Path | None | |
| receipt: RunReceipt | |
| receipt_path: Path | None | |
| def failures(self) -> tuple[AlignmentFailure, ...]: | |
| """Return structured failures in original row order. | |
| Returns: | |
| Tuple containing only failed variants. | |
| """ | |
| return tuple( | |
| outcome.failure for outcome in self.outcomes if outcome.failure is not None | |
| ) | |
| class _PreparedRows: | |
| """Hold one source table, valid requests, row positions, and preflight failures.""" | |
| table: MetadataTable | |
| requests: tuple[AlignmentRequest, ...] | |
| request_positions: tuple[int, ...] | |
| initial_outcomes: tuple[AlignmentOutcome | None, ...] | |
| def _raise_failure(failure: AlignmentFailure) -> None: | |
| """Abort strict orchestration for one already-structured row failure. | |
| Parameters | |
| ---------- | |
| failure: Failure emitted by preflight, a backend, or validation. | |
| Raises | |
| ------ | |
| SpeechTextAlignerError: Always, with the stable failure code in safe context. | |
| """ | |
| raise SpeechTextAlignerError( | |
| failure.message, | |
| context=(("failure_code", failure.code), ("request_id", failure.request_id)), | |
| retryable=failure.retryable, | |
| ) | |
| def _prepare_rows( | |
| metadata_csv: Path, | |
| audio_dir: Path, | |
| *, | |
| strict: bool, | |
| ) -> _PreparedRows: | |
| """Read metadata and form ordered requests while structuring row preflight errors. | |
| Parameters | |
| ---------- | |
| metadata_csv: Source metadata CSV. | |
| audio_dir: Directory containing ``<audio_id>.wav`` files. | |
| strict: Raise immediately instead of retaining a row failure. | |
| Returns | |
| ------- | |
| Source table plus valid requests and per-row placeholder outcomes. | |
| Raises | |
| ------ | |
| MetadataError: If source metadata is malformed. | |
| SpeechTextAlignerError: On the first row preflight failure in strict mode. | |
| """ | |
| table = read_metadata(metadata_csv) | |
| requests: list[AlignmentRequest] = [] | |
| positions: list[int] = [] | |
| outcomes: list[AlignmentOutcome | None] = [None] * len(table.rows) | |
| for ordinal in range(len(table.rows)): | |
| row = table.row_dict(ordinal) | |
| request_id = f"{ordinal:06d}" | |
| try: | |
| # Lexical preflight ensures empty text never reaches an expensive backend. | |
| lexicalize(row["en_transcript"]) | |
| request = make_alignment_request( | |
| request_id=request_id, | |
| audio_path=resolve_audio_path(audio_dir, row["audio_id"]), | |
| transcript=row["en_transcript"], | |
| context=(("line_id", row["line_id"]), ("audio_id", row["audio_id"])), | |
| ) | |
| except SpeechTextAlignerError as error: | |
| failure = failure_from_error(error, request_id=request_id) | |
| if strict: | |
| _raise_failure(failure) | |
| outcomes[ordinal] = AlignmentOutcome(failure=failure) | |
| else: | |
| requests.append(request) | |
| positions.append(ordinal) | |
| return _PreparedRows(table, tuple(requests), tuple(positions), tuple(outcomes)) | |
| def _execute_backend( | |
| prepared: _PreparedRows, | |
| backend: AlignmentBackend, | |
| *, | |
| strict: bool, | |
| ) -> tuple[AlignmentOutcome, ...]: | |
| """Run one injected backend and merge validated outcomes into source row order. | |
| Parameters | |
| ---------- | |
| prepared: Metadata rows and successfully prepared requests. | |
| backend: Injected protocol implementation. | |
| strict: Abort rather than retaining the first backend/validation failure. | |
| Returns | |
| ------- | |
| Exactly one outcome for every original source row. | |
| Raises | |
| ------ | |
| SpeechTextAlignerError: For strict row failures or whole-backend failure. | |
| BackendOutputError: In strict mode when batch count/order is malformed. | |
| """ | |
| merged = list(prepared.initial_outcomes) | |
| if not prepared.requests: | |
| return tuple(outcome for outcome in merged if outcome is not None) | |
| try: | |
| backend_outcomes = backend.align_batch(prepared.requests) | |
| if len(backend_outcomes) != len(prepared.requests): | |
| raise BackendOutputError( | |
| "backend returned an outcome count that does not match requests" | |
| ) | |
| except SpeechTextAlignerError as error: | |
| if strict: | |
| raise | |
| backend_outcomes = tuple( | |
| AlignmentOutcome( | |
| failure=failure_from_error(error, request_id=request.request_id) | |
| ) | |
| for request in prepared.requests | |
| ) | |
| for request, position, outcome in zip( | |
| prepared.requests, prepared.request_positions, backend_outcomes, strict=True | |
| ): | |
| try: | |
| if outcome.request_id != request.request_id: | |
| raise BackendOutputError( | |
| "backend outcomes are reordered or misidentified" | |
| ) | |
| if outcome.failure is not None: | |
| if strict: | |
| _raise_failure(outcome.failure) | |
| merged[position] = outcome | |
| continue | |
| assert outcome.result is not None | |
| validated = validate_alignment_result(request, outcome.result) | |
| merged[position] = AlignmentOutcome(result=validated) | |
| except SpeechTextAlignerError as error: | |
| failure = failure_from_error(error, request_id=request.request_id) | |
| if strict: | |
| _raise_failure(failure) | |
| merged[position] = AlignmentOutcome(failure=failure) | |
| if any(outcome is None for outcome in merged): | |
| raise BackendOutputError("internal outcome merge left an unpopulated row") | |
| return tuple(outcome for outcome in merged if outcome is not None) | |
| def _write_failures( | |
| failures: tuple[AlignmentFailure, ...], | |
| path: Path, | |
| *, | |
| overwrite: bool, | |
| ) -> None: | |
| """Atomically write structured failures as deterministic JSON. | |
| Parameters | |
| ---------- | |
| failures: Ordered row failures. | |
| path: Diagnostic artifact destination. | |
| overwrite: Explicit replacement authorization. | |
| Raises | |
| ------ | |
| BackendOutputError: If overwrite is unauthorized or publication fails. | |
| Side Effects: | |
| Creates and atomically publishes a JSON diagnostic artifact. | |
| """ | |
| if path.exists() and not overwrite: | |
| raise BackendOutputError( | |
| "diagnostic artifact exists without overwrite permission" | |
| ) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| temporary_path: Path | None = None | |
| try: | |
| with tempfile.NamedTemporaryFile( | |
| mode="w", | |
| encoding="utf-8", | |
| newline="\n", | |
| dir=path.parent, | |
| prefix=f".{path.name}.", | |
| suffix=".tmp", | |
| delete=False, | |
| ) as stream: | |
| temporary_path = Path(stream.name) | |
| json.dump( | |
| [asdict(failure) for failure in failures], | |
| stream, | |
| sort_keys=True, | |
| separators=(",", ":"), | |
| ensure_ascii=False, | |
| ) | |
| stream.write("\n") | |
| stream.flush() | |
| os.fsync(stream.fileno()) | |
| os.replace(temporary_path, path) | |
| except OSError as error: | |
| if temporary_path is not None: | |
| temporary_path.unlink(missing_ok=True) | |
| raise BackendOutputError( | |
| f"cannot publish diagnostic artifact: {path}" | |
| ) from error | |
| def _preflight_annotation_paths( | |
| metadata_csv: Path, | |
| output_csv: Path, | |
| options: AnnotationOptions, | |
| ) -> Path: | |
| """Validate the complete annotation publication plan before backend execution. | |
| Parameters | |
| ---------- | |
| metadata_csv: Immutable source metadata path. | |
| output_csv: Annotated CSV destination. | |
| options: Diagnostic, receipt, and overwrite publication controls. | |
| Returns | |
| ------- | |
| Effective diagnostics destination, including the deterministic default. | |
| Raises | |
| ------ | |
| ConfigurationError: If logical artifacts alias or an existing destination | |
| lacks overwrite authorization. | |
| Side Effects: | |
| Reads filesystem identity/existence metadata only; no artifact is published. | |
| """ | |
| diagnostics_path = options.diagnostics_path or output_csv.with_name( | |
| f"{output_csv.stem}.diagnostics.json" | |
| ) | |
| paths = { | |
| "source metadata CSV": metadata_csv, | |
| "annotated output CSV": output_csv, | |
| "diagnostics JSON": diagnostics_path, | |
| } | |
| if options.receipt_path is not None: | |
| paths["run receipt JSON"] = options.receipt_path | |
| aliases = find_path_aliases(paths) | |
| if aliases: | |
| left, right = aliases[0] | |
| raise ConfigurationError(f"annotation artifact paths alias: {left} and {right}") | |
| if not options.overwrite: | |
| existing = tuple( | |
| name | |
| for name, path in paths.items() | |
| if name != "source metadata CSV" and path.exists() | |
| ) | |
| if existing: | |
| raise ConfigurationError( | |
| f"annotation artifact exists without overwrite permission: {existing[0]}" | |
| ) | |
| return diagnostics_path | |
| def annotate_metadata( | |
| *, | |
| metadata_csv: Path, | |
| audio_dir: Path, | |
| output_csv: Path, | |
| backend: AlignmentBackend, | |
| options: AnnotationOptions = _DEFAULT_ANNOTATION_OPTIONS, | |
| ) -> AnnotationRun: | |
| """Annotate metadata through an injected backend and atomically publish results. | |
| In continue-on-error mode every source row remains ordered and failed rows receive | |
| six empty cells plus a separate structured diagnostic artifact. Strict mode aborts | |
| before publishing the output CSV when any row fails. | |
| Parameters | |
| ---------- | |
| metadata_csv: Immutable source metadata CSV. | |
| audio_dir: Directory containing WAVs resolved from ``audio_id``. | |
| output_csv: New annotated CSV destination. | |
| backend: Injected backend-neutral protocol implementation. | |
| options: Strict/continue and artifact publication controls. | |
| Returns | |
| ------- | |
| Completed annotation run with ordered outcomes and captured receipt. | |
| Raises | |
| ------ | |
| SpeechTextAlignerError: For malformed metadata, unsafe paths, strict failures, | |
| backend protocol violations, or artifact publication errors. | |
| Side Effects: | |
| Publishes the output CSV and, when applicable/requested, diagnostics and a run | |
| receipt. It never modifies the source CSV. | |
| """ | |
| diagnostics_path = _preflight_annotation_paths(metadata_csv, output_csv, options) | |
| started_wall = datetime.now(UTC) | |
| started_monotonic = time.monotonic() | |
| prepared = _prepare_rows(metadata_csv, audio_dir, strict=options.strict) | |
| outcomes = _execute_backend(prepared, backend, strict=options.strict) | |
| requests = {request.request_id: request for request in prepared.requests} | |
| annotations: list[dict[str, str | float | None]] = [] | |
| for outcome in outcomes: | |
| if outcome.result is None: | |
| annotations.append(dict.fromkeys(BOUNDARY_COLUMNS, None)) | |
| else: | |
| request = requests[outcome.request_id] | |
| annotations.append( | |
| cast( | |
| "dict[str, str | float | None]", | |
| extract_boundary_fields(request, outcome.result).as_dict(), | |
| ) | |
| ) | |
| failures = tuple( | |
| outcome.failure for outcome in outcomes if outcome.failure is not None | |
| ) | |
| write_annotated_metadata( | |
| prepared.table, | |
| output_csv, | |
| tuple(annotations), | |
| overwrite=options.overwrite, | |
| ) | |
| if failures: | |
| _write_failures(failures, diagnostics_path, overwrite=options.overwrite) | |
| finished_wall = datetime.now(UTC) | |
| elapsed = time.monotonic() - started_monotonic | |
| receipt = RunReceipt( | |
| schema_version="1", | |
| project_version=_PROJECT_VERSION, | |
| operation="annotate", | |
| backend_names=(backend.name,), | |
| input_path=str(metadata_csv), | |
| input_sha256=sha256_file(metadata_csv), | |
| request_ids=tuple(outcome.request_id for outcome in outcomes), | |
| host=capture_host(), | |
| config_sha256=canonical_fingerprint( | |
| {"backend": backend.name, "strict": options.strict} | |
| ), | |
| started_at_utc=started_wall.isoformat(), | |
| finished_at_utc=finished_wall.isoformat(), | |
| elapsed_s=elapsed if math.isfinite(elapsed) else 0.0, | |
| success_count=sum(outcome.succeeded for outcome in outcomes), | |
| failure_count=len(failures), | |
| artifacts=(str(output_csv),) + ((str(diagnostics_path),) if failures else ()), | |
| ) | |
| if options.receipt_path is not None: | |
| write_receipt(receipt, options.receipt_path, overwrite=options.overwrite) | |
| return AnnotationRun( | |
| output_csv=output_csv, | |
| outcomes=outcomes, | |
| diagnostics_path=diagnostics_path if failures else None, | |
| receipt=receipt, | |
| receipt_path=options.receipt_path, | |
| ) | |