Download scripts/score_tts_preprocessor.py from Syncre/arabic-audio-reader-worker: direct link, hf CLI and curl.
- Browser
- Download file 7.52 kB
-
https://huggingface.co/spaces/Syncre/arabic-audio-reader-worker/resolve/main/scripts/score_tts_preprocessor.py
- Command line
-
hf download hf://spaces/Syncre/arabic-audio-reader-worker/scripts/score_tts_preprocessor.py
-
curl -L -o score_tts_preprocessor.py https://huggingface.co/spaces/Syncre/arabic-audio-reader-worker/resolve/main/scripts/score_tts_preprocessor.py
7.52 kB
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| CRITERIA = ["pronunciation", "meaning_preserved", "harakat_quality", "pacing", "comfort"] | |
| WEIGHTS = { | |
| "pronunciation": 0.3, | |
| "meaning_preserved": 0.3, | |
| "harakat_quality": 0.15, | |
| "pacing": 0.1, | |
| "comfort": 0.15, | |
| } | |
| def parse_rating(value: str) -> dict[str, Any]: | |
| if "=" not in value: | |
| raise ValueError("Rating must look like mishkala=5,5,4,5,5") | |
| label, scores_text = value.split("=", 1) | |
| label = label.strip() | |
| if not label: | |
| raise ValueError("Preprocessor label cannot be empty.") | |
| pieces = [piece.strip() for piece in scores_text.split(",")] | |
| if len(pieces) != len(CRITERIA): | |
| raise ValueError(f"Expected {len(CRITERIA)} scores for {label}: {', '.join(CRITERIA)}") | |
| scores: dict[str, int] = {} | |
| for criterion, piece in zip(CRITERIA, pieces): | |
| try: | |
| score = int(piece) | |
| except ValueError as exc: | |
| raise ValueError(f"{criterion} for {label} must be a number from 1 to 5") from exc | |
| if not 1 <= score <= 5: | |
| raise ValueError(f"{criterion} for {label} must be from 1 to 5") | |
| scores[criterion] = score | |
| weighted = round(sum(scores[key] * WEIGHTS[key] for key in CRITERIA), 2) | |
| return { | |
| "label": label, | |
| "scores": scores, | |
| "weightedScore": weighted, | |
| "minimumScore": min(scores.values()), | |
| "promotionReady": weighted >= 4.0 and scores["meaning_preserved"] >= 4 and min(scores.values()) >= 3, | |
| } | |
| def choose_best(ratings: list[dict[str, Any]]) -> dict[str, Any] | None: | |
| if not ratings: | |
| return None | |
| return max( | |
| ratings, | |
| key=lambda item: ( | |
| float(item.get("weightedScore") or 0), | |
| int((item.get("scores") or {}).get("meaning_preserved") or 0), | |
| int(item.get("minimumScore") or 0), | |
| ), | |
| ) | |
| def score_tts_preprocessor( | |
| rating_values: list[str], | |
| baseline_label: str = "plain", | |
| report_path: Path | None = None, | |
| json_path: Path | None = None, | |
| ) -> dict[str, Any]: | |
| if not rating_values: | |
| raise ValueError("At least one --rating is required.") | |
| ratings = [parse_rating(value) for value in rating_values] | |
| best = choose_best(ratings) | |
| baseline = next((item for item in ratings if item["label"] == baseline_label), None) | |
| improvement = None | |
| if best and baseline: | |
| improvement = round(float(best["weightedScore"]) - float(baseline["weightedScore"]), 2) | |
| payload = { | |
| "ready": bool(best and best.get("promotionReady") and best.get("label") != baseline_label), | |
| "best": best, | |
| "baseline": baseline, | |
| "baselineLabel": baseline_label, | |
| "weightedImprovementOverBaseline": improvement, | |
| "ratings": ratings, | |
| "criteria": CRITERIA, | |
| "weights": WEIGHTS, | |
| } | |
| if report_path: | |
| write_report(report_path, payload) | |
| payload["reportPath"] = str(report_path) | |
| if json_path: | |
| json_path.parent.mkdir(parents=True, exist_ok=True) | |
| json_path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") | |
| payload["jsonPath"] = str(json_path) | |
| return payload | |
| def markdown_value(value: Any) -> str: | |
| if value is None or value == "": | |
| return "-" | |
| return str(value) | |
| def write_report(path: Path, payload: dict[str, Any]) -> None: | |
| best = payload.get("best") or {} | |
| baseline = payload.get("baseline") or {} | |
| lines = [ | |
| "# Arabic TTS Preprocessor Listening Score", | |
| "", | |
| "Scores compare the same cleaned Arabic text before and after a pronunciation preprocessor such as Mishkala Tashkeel.", | |
| "", | |
| f"Best preprocessor: {markdown_value(best.get('label'))}", | |
| f"Baseline: {markdown_value(baseline.get('label') or payload.get('baselineLabel'))}", | |
| f"Weighted improvement over baseline: {markdown_value(payload.get('weightedImprovementOverBaseline'))}", | |
| f"Promotion ready: {markdown_value(payload.get('ready'))}", | |
| "", | |
| "| Preprocessor | Pronunciation | Meaning preserved | Harakat quality | Pacing | Long-listen comfort | Weighted | Min | Ready |", | |
| "| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | --- |", | |
| ] | |
| for item in payload.get("ratings", []): | |
| scores = item.get("scores") or {} | |
| lines.append( | |
| "| " | |
| + " | ".join( | |
| [ | |
| markdown_value(item.get("label")), | |
| markdown_value(scores.get("pronunciation")), | |
| markdown_value(scores.get("meaning_preserved")), | |
| markdown_value(scores.get("harakat_quality")), | |
| markdown_value(scores.get("pacing")), | |
| markdown_value(scores.get("comfort")), | |
| markdown_value(item.get("weightedScore")), | |
| markdown_value(item.get("minimumScore")), | |
| markdown_value(item.get("promotionReady")), | |
| ] | |
| ) | |
| + " |" | |
| ) | |
| lines.extend( | |
| [ | |
| "", | |
| "## Promotion Rule", | |
| "", | |
| "Promote a preprocessor only when it beats the plain sample, its weighted score is at least 4.0, meaning preservation is at least 4, and no criterion is below 3.", | |
| "If the diacritized sample sounds more formal but less comfortable for long listening, keep the plain cleaned text.", | |
| ] | |
| ) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| path.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8") | |
| def print_table(payload: dict[str, Any]) -> None: | |
| print("preprocessor weighted meaning min ready") | |
| print("---------------- -------- ------- --- -----") | |
| for item in payload["ratings"]: | |
| scores = item.get("scores") or {} | |
| print( | |
| f"{item.get('label', '-'):<16} " | |
| f"{item.get('weightedScore', 0):>8} " | |
| f"{scores.get('meaning_preserved', 0):>7} " | |
| f"{item.get('minimumScore', 0):>3} " | |
| f"{str(item.get('promotionReady')):<5}" | |
| ) | |
| best = payload.get("best") or {} | |
| if best: | |
| print() | |
| print(f"Best preprocessor: {best.get('label')} weighted={best.get('weightedScore')} ready={payload.get('ready')}") | |
| def main_cli() -> None: | |
| parser = argparse.ArgumentParser(description="Score plain vs diacritized Arabic TTS samples after listening.") | |
| parser.add_argument( | |
| "--rating", | |
| action="append", | |
| default=[], | |
| help="label=pronunciation,meaning_preserved,harakat_quality,pacing,comfort using 1-5 scores.", | |
| ) | |
| parser.add_argument("--baseline-label", default="plain", help="Label used for the non-preprocessed sample.") | |
| parser.add_argument("--write-report", type=Path, help="Write a Markdown listening score report.") | |
| parser.add_argument("--write-json", type=Path, help="Write a JSON score report for model_promotion_gate.py.") | |
| parser.add_argument("--json", action="store_true", help="Print JSON instead of a compact table.") | |
| args = parser.parse_args() | |
| payload = score_tts_preprocessor( | |
| args.rating, | |
| baseline_label=args.baseline_label, | |
| report_path=args.write_report, | |
| json_path=args.write_json, | |
| ) | |
| if args.json: | |
| print(json.dumps(payload, ensure_ascii=False, indent=2)) | |
| else: | |
| print_table(payload) | |
| if __name__ == "__main__": | |
| main_cli() | |