File size: 7,516 Bytes
2e1a095
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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()