BlueMagpie-TTS-Demo / production.py
voidful's picture
Use Breeze25 for efficient semantic verification
86b8987 verified
Raw History Blame Contribute Delete
231 kB
"""Production inference helpers for the BlueMagpie-TTS Space."""
from __future__ import annotations
import math
import operator
import random
import re
import unicodedata
from dataclasses import dataclass
from datetime import date
from decimal import Decimal, InvalidOperation, localcontext
from typing import Sequence
from urllib.parse import urlsplit
import numpy as np
import torch
from opencc import OpenCC
from pypinyin import Style, lazy_pinyin
from torch import nn
_SPACE_RE = re.compile(r"\s+")
_PUNCT_NO_LEFT_SPACE_RE = re.compile(r"\s+([,。!?;:、,.!?;:])")
_CJK_PUNCT_RIGHT_SPACE_RE = re.compile(r"([,。!?;:、])\s+")
_BOPOMOFO_TONES = {"ˊ", "ˇ", "ˋ", "˙"}
_BOPOMOFO_ASR_POPO_RE = re.compile(
r"(?i)(?<![a-z0-9])(?:po|bo)\s*(?:po|bo)\s*(?:mo|摸)(?![a-z0-9])"
)
_ASR_HOMOPHONE_TRANSLATION = str.maketrans(
{
"她": "他",
"它": "他",
"牠": "他",
"祂": "他",
"妳": "你",
}
)
_TERMINAL_PUNCTUATION = frozenset("。!?.!?")
_NONTERMINAL_TRAILING_PUNCTUATION = frozenset(",,、;;::")
_TRAILING_CLOSERS = frozenset("\"'”’」』】))]}")
_BOPOMOFO_READINGS = {
"ㄅ": "波", "ㄆ": "坡", "ㄇ": "摸", "ㄈ": "佛", "ㄉ": "得",
"ㄊ": "特", "ㄋ": "呢", "ㄌ": "了", "ㄍ": "哥", "ㄎ": "科",
"ㄏ": "喝", "ㄐ": "基", "ㄑ": "欺", "ㄒ": "希", "ㄓ": "知",
"ㄔ": "吃", "ㄕ": "師", "ㄖ": "日", "ㄗ": "資", "ㄘ": "雌",
"ㄙ": "思", "ㄚ": "啊", "ㄛ": "喔", "ㄜ": "鵝", "ㄝ": "欸",
"ㄞ": "哀", "ㄟ": "欸", "ㄠ": "凹", "ㄡ": "歐", "ㄢ": "安",
"ㄣ": "恩", "ㄤ": "昂", "ㄥ": "鞥", "ㄦ": "兒", "ㄧ": "衣",
"ㄨ": "烏", "ㄩ": "迂",
}
_ZH_DIGITS = "零一二三四五六七八九"
_ZH_SMALL_UNITS = ("", "十", "百", "千")
_ZH_LARGE_UNITS = ("", "萬", "億", "兆")
_SPOKEN_DATE_RE = re.compile(
r"(?<![A-Za-z0-9/])(\d{4})([/-])(\d{1,2})\2(\d{1,2})(?![A-Za-z0-9/])"
)
_SPOKEN_ZH_DATE_RE = re.compile(
r"(?<![A-Za-z0-9])(\d{4})\s*年\s*(\d{1,2})\s*月\s*"
r"(\d{1,2})\s*(?:日|號)(?![A-Za-z0-9])"
)
_SPOKEN_TIME_RE = re.compile(
r"(?<![A-Za-z0-9:])([01]?\d|2[0-3]):([0-5]\d)(?![A-Za-z0-9:])"
)
_ASR_EXPLICIT_ZH_TIME_RE = re.compile(
r"(?<![A-Za-z0-9])([01]?\d|2[0-3])[\u9ede\u70b9\u6642\u65f6]"
r"([0-5]?\d)\u5206(?![A-Za-z0-9])"
)
_ASR_BARE_ZH_TIME_RE = re.compile(
r"(?<![A-Za-z0-9])([01]?\d|2[0-3])[\u9ede\u70b9\u6642\u65f6]"
r"([0-5]?\d)(?![A-Za-z0-9]|\u5206|\u6beb?\u79d2|\u5fae\u79d2|"
r"\u516c\u65a4|\u5343\u514b|\u516c\u514b|\u6beb\u514b|\u5ea6)"
)
_ASR_FORMATTED_CLOCK_RE = re.compile(
r"(?<![A-Za-z0-9])([01]?\d|2[0-3])\s*"
r"[.:\u9ede\u70b9\u6642\u65f6]\s*([0-5]?\d)(?:\s*\u5206)?"
r"(?![A-Za-z0-9])"
)
_ASR_ARABIC_DIGIT_RUN_RE = re.compile(
r"(?<![A-Za-z0-9])\d+(?![A-Za-z0-9])"
)
_ASR_XIAOLAOSHU_ALIAS_RE = re.compile(r"(?i)xiaolaoshu")
_ASR_STRUCTURED_NUMERIC_NEIGHBORS = frozenset(
".,:/\\-+_@#年月日時时分秒點点%%"
)
_ASR_FREQUENCY_MEASUREMENT_RE = re.compile(
r"(?i)(?<![A-Za-z0-9./:\\+_@#-])"
r"([+]?\d+(?:\.\d+)?)\s*"
r"(GHz|MHz|kHz|Hz|吉赫茲|兆赫茲|千赫茲|赫茲|吉赫兹|兆赫兹|千赫兹|赫兹)"
r"(?![A-Za-z0-9])"
)
_REJECT_ONLY_FREQUENCY_MEASUREMENT_RE = re.compile(
r"(?i)([+]?\d+(?:\.\d+)?)\s*"
r"(GHz|MHz|kHz|Hz|吉赫茲|兆赫茲|千赫茲|赫茲|吉赫兹|兆赫兹|千赫兹|赫兹)"
r"(?![A-Za-z0-9])"
)
_ZH_FREQUENCY_DIGITS = "零一二三四五六七八九"
_ZH_FREQUENCY_INTEGER_CHARS = _ZH_FREQUENCY_DIGITS + "十百千萬億兆"
_ZH_FREQUENCY_NUMBER_BOUNDARY_CLASS = (
"A-Za-z0-9"
+ re.escape(
_ZH_FREQUENCY_INTEGER_CHARS + "點点正負负./:\\+_@#-"
)
)
_CANONICAL_ZH_FREQUENCY_PATTERNS = tuple(
(
re.compile(
rf"(?<![{_ZH_FREQUENCY_NUMBER_BOUNDARY_CLASS}])"
rf"(正?[{_ZH_FREQUENCY_INTEGER_CHARS}]+"
rf"(?:點[{_ZH_FREQUENCY_DIGITS}]+)?)\s*{unit}"
r"(?![A-Za-z0-9])"
),
unit,
)
# Every valid unit decomposition is retained below. Some frontend forms,
# notably ``一兆赫茲``, are lexically ambiguous between a large cardinal
# followed by Hz and a smaller cardinal followed by MHz. Such targets
# must fail closed instead of relying on unit-order preference.
for unit in (
"吉赫茲",
"吉赫兹",
"兆赫茲",
"兆赫兹",
"千赫茲",
"千赫兹",
"赫茲",
"赫兹",
)
)
_FREQUENCY_SCALE_HZ = {
"hz": Decimal("1"),
"khz": Decimal("1000"),
"mhz": Decimal("1000000"),
"ghz": Decimal("1000000000"),
"赫茲": Decimal("1"),
"千赫茲": Decimal("1000"),
"兆赫茲": Decimal("1000000"),
"吉赫茲": Decimal("1000000000"),
"赫兹": Decimal("1"),
"千赫兹": Decimal("1000"),
"兆赫兹": Decimal("1000000"),
"吉赫兹": Decimal("1000000000"),
}
_SPOKEN_PERCENT_RE = re.compile(
r"(?<![A-Za-z0-9.])([+-]?\d+(?:\.\d+)?)\s*[%%](?![A-Za-z0-9])"
)
_SPOKEN_UNIT_RE = re.compile(
r"(?<![A-Za-z0-9.])([+-]?\d+(?:\.\d+)?)\s*"
r"(km/h|m/s|ms|kHz|MHz|GHz|Hz|km|cm|mm|kg|mg|mL|ml|kW|°\s*[Cc]|℃|m|g|L|l|W|V)"
r"(?![A-Za-z])"
)
_SPOKEN_ZH_MEASURE_RE = re.compile(
r"(?<![A-Za-z0-9.\u9ede\u70b9])([+-]?\d+(?:\.\d+)?)\s*"
r"(公里|公尺|公分|公厘|厘米|毫米|公斤|千克|公克|毫克|公升|毫升|"
r"攝氏度|千赫茲|兆赫茲|吉赫茲|赫茲|小時|分鐘|毫秒|米|克|升|度|秒|天|"
r"個|人|次|張|台|件|份|元)"
r"(?![A-Za-z0-9])"
)
_NETWORK_NONASCII_BOUNDARY_PUNCTUATION = (
",。!?;、:()【】「」『』《》〈〉“”‘’[]{}"
"…⋯—–.。、〔〕〖〗〘〙〚〛﹁﹂"'︰﹐﹑﹒﹔﹕﹖﹗"
)
_SPOKEN_URL_RE = re.compile(
rf"(?i)(?<![A-Za-z0-9])(?:https?://|ftp://|www\.)"
rf"[^\s<>\"{re.escape(_NETWORK_NONASCII_BOUNDARY_PUNCTUATION)}]+"
)
_SPOKEN_EMAIL_RE = re.compile(
r"(?i)(?<![A-Za-z0-9.!#$%&'*+/=?^_`{|}~-])"
r"[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@"
r"[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+"
r"(?![A-Za-z0-9-])"
)
_UNSUPPORTED_QUOTED_EMAIL_RE = re.compile(
r'(?i)(?<![A-Za-z0-9])"(?:[^"\\]|\\.)*"@(?:'
r"[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+|\[[^\]\s]+\])"
)
_SPOKEN_SEMVER_RE = re.compile(
r"(?i)(?<![A-Za-z0-9.])(?:v|version\s+|版本\s*(?:v\s*)?)?"
r"(\d+)\.(\d+)\.(\d+)"
r"(?:-([0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*))?"
r"(?:\+([0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*))?"
r"(?![A-Za-z0-9.])"
)
_CURRENCY_NUMBER_PATTERN = r"[+-]?(?:\d{1,3}(?:,\d{3})+|\d+)(?:\.\d+)?"
_CURRENCY_CODE_PATTERN = (
r"NT\$|TWD|NTD|US\$|USD|HK\$|HKD|CN¥|CNY|RMB|JPY|EUR|GBP|KRW|"
r"\$|€|¥|¥|£|₩"
)
_SPOKEN_CURRENCY_PREFIX_RE = re.compile(
rf"(?i)(?<![A-Za-z0-9])({_CURRENCY_CODE_PATTERN})\s*"
rf"({_CURRENCY_NUMBER_PATTERN})(?![\d,.])"
)
_SPOKEN_CURRENCY_SUFFIX_RE = re.compile(
rf"(?i)(?<![A-Za-z0-9.])({_CURRENCY_NUMBER_PATTERN})\s*"
rf"(TWD|NTD|USD|HKD|CNY|RMB|JPY|EUR|GBP|KRW)(?![A-Za-z])"
)
_MODEL_CODE_RE = re.compile(
r"(?<![A-Za-z0-9])([A-Z]{2,8})[ -]*(\d{1,8})(?![A-Za-z0-9.])"
)
_ASR_MODEL_CODE_RE = re.compile(
r"(?<![A-Za-z0-9])([A-Za-z]{2,8})[ _-]*(\d{1,8})"
r"(?![A-Za-z0-9.])"
)
_UPPERCASE_ACRONYM_RE = re.compile(r"(?<![A-Za-z0-9])([A-Z]{2,8})(?![A-Za-z0-9])")
_ZH_UNIT_READINGS = {
"km/h": "公里每小時",
"m/s": "公尺每秒",
"ms": "毫秒",
"khz": "千赫茲",
"mhz": "兆赫茲",
"ghz": "吉赫茲",
"hz": "赫茲",
"km": "公里",
"cm": "公分",
"mm": "毫米",
"kg": "公斤",
"mg": "毫克",
"ml": "毫升",
"kw": "千瓦",
"°c": "攝氏度",
"℃": "攝氏度",
"m": "公尺",
"g": "公克",
"l": "公升",
"w": "瓦",
"v": "伏特",
}
_ZH_CURRENCY_READINGS = {
"NT$": "新台幣",
"TWD": "新台幣",
"NTD": "新台幣",
"US$": "美元",
"USD": "美元",
"$": "美元",
"HK$": "港幣",
"HKD": "港幣",
"CN¥": "人民幣",
"CNY": "人民幣",
"RMB": "人民幣",
"JPY": "日圓",
"¥": "日圓",
"¥": "日圓",
"EUR": "歐元",
"€": "歐元",
"GBP": "英鎊",
"£": "英鎊",
"KRW": "韓元",
"₩": "韓元",
}
_ASR_JPY_ZH_AMOUNT_PATTERN = (
r"[正負负]?["
+ _ZH_DIGITS
+ r"十百千萬万億亿兆]+(?:[點点]["
+ _ZH_DIGITS
+ r"]+)?"
)
_ASR_JPY_CURRENCY_AMOUNT_RE = re.compile(
rf"(?P<currency>日[元圓圆])\s*"
rf"(?P<amount>{_CURRENCY_NUMBER_PATTERN}|{_ASR_JPY_ZH_AMOUNT_PATTERN})"
r"(?P<redundant_unit>元)?"
rf"(?![\d,.{_ZH_DIGITS}十百千萬万億亿兆點点元])"
)
_ASR_JPY_AMOUNT_TRANSLATION = str.maketrans(
{"负": "負", "点": "點", "万": "萬", "亿": "億"}
)
_ZH_NETWORK_SYMBOL_READINGS = {
".": "點",
"/": "斜線",
":": "冒號",
"?": "問號",
"=": "等於",
"&": "和",
"#": "井號",
"@": "小老鼠",
"-": "橫線",
"_": "底線",
"%": "百分號",
"+": "加號",
"~": "波浪號",
"!": "驚嘆號",
"$": "錢字號",
"'": "單引號",
"(": "左括號",
")": "右括號",
"*": "星號",
",": "逗號",
";": "分號",
"[": "左方括號",
"]": "右方括號",
"^": "插入號",
"`": "反引號",
"{": "左大括號",
"|": "豎線",
"}": "右大括號",
}
_ZH_NETWORK_LETTER_READINGS = {
"A": "欸",
"B": "比",
"C": "西",
"D": "迪",
"E": "伊",
"F": "艾夫",
"G": "吉",
"H": "艾取",
"I": "愛",
"J": "傑",
"K": "凱",
"L": "艾爾",
"M": "艾姆",
"N": "恩",
"O": "歐",
"P": "批",
"Q": "丘",
"R": "阿爾",
"S": "艾斯",
"T": "踢",
"U": "優",
"V": "維",
"W": "達不溜",
"X": "艾克斯",
"Y": "歪",
"Z": "茲",
}
_ZH_NETWORK_READING_TO_LETTER = {
reading: letter.casefold()
for letter, reading in _ZH_NETWORK_LETTER_READINGS.items()
}
_ZH_NETWORK_READING_TO_LETTER.update(
{
"爱": "i",
"杰": "j",
"凯": "k",
"艾尔": "l",
"欧": "o",
"阿尔": "r",
"优": "u",
"维": "v",
"达不溜": "w",
"兹": "z",
}
)
_T2S_CONVERTER = OpenCC("t2s")
class StopHysteresisController(nn.Module):
"""Apply probability-threshold hysteresis around a legacy stop head.
The pinned public model predates native ``stop_threshold`` and
``stop_consecutive`` arguments. This adapter preserves the learned logits
and only changes the final stop decision used by legacy generation.
"""
def __init__(
self,
stop_head: nn.Module,
threshold: float = 0.65,
late_threshold: float = 0.50,
consecutive: int = 2,
late_start_ratio: float = 0.80,
late_full_ratio: float = 1.00,
) -> None:
super().__init__()
self.stop_head = stop_head
self.threshold = float(threshold)
self.late_threshold = float(late_threshold)
self.consecutive = max(1, int(consecutive))
self.late_start_ratio = max(0.0, float(late_start_ratio))
self.late_full_ratio = max(
self.late_start_ratio + 1.0e-6,
float(late_full_ratio),
)
self._active = False
self._min_len = 2
self._expected_steps = 0
self._hard_stop_steps = 0
self._step = 0
self._hits = 0
self.last_probabilities: list[float] = []
self.last_generated_steps = 0
self.last_stop_reason = "inactive"
def begin(
self,
min_len: int,
*,
expected_steps: int = 0,
hard_stop_steps: int = 0,
) -> None:
self._active = True
self._min_len = max(0, int(min_len))
self._expected_steps = max(0, int(expected_steps))
self._hard_stop_steps = max(0, int(hard_stop_steps))
self._step = 0
self._hits = 0
self.last_probabilities = []
self.last_generated_steps = 0
self.last_stop_reason = "running"
def end(self) -> None:
self._active = False
def forward(self, hidden: torch.Tensor) -> torch.Tensor:
logits = self.stop_head(hidden)
if not self._active:
return logits
probability = float(torch.softmax(logits.float(), dim=-1)[0, 1].detach().cpu())
self.last_probabilities.append(probability)
generated_steps = self._step + 1
threshold = self.threshold
if self._expected_steps > 0:
progress = generated_steps / float(self._expected_steps)
if progress >= self.late_start_ratio:
blend = min(
1.0,
max(
0.0,
(progress - self.late_start_ratio)
/ (self.late_full_ratio - self.late_start_ratio),
),
)
threshold = self.threshold + blend * (self.late_threshold - self.threshold)
eligible = self._step > self._min_len
if eligible and probability >= threshold:
self._hits += 1
else:
self._hits = 0
should_stop = eligible and self._hits >= self.consecutive
stop_reason = "stop_threshold" if should_stop else "running"
if self._hard_stop_steps > 0 and generated_steps >= self._hard_stop_steps:
if not should_stop:
should_stop = True
stop_reason = "hard_stop"
self._step += 1
self.last_generated_steps = generated_steps
self.last_stop_reason = stop_reason
decision = torch.zeros_like(logits)
decision[..., 1 if should_stop else 0] = 1.0
return decision
def set_generation_seed(seed: int) -> None:
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
if torch.cuda.is_available():
torch.cuda.manual_seed_all(seed)
def _is_cjk(char: str) -> bool:
codepoint = ord(char)
return (
0x3400 <= codepoint <= 0x4DBF
or 0x4E00 <= codepoint <= 0x9FFF
or 0xF900 <= codepoint <= 0xFAFF
or 0x3040 <= codepoint <= 0x30FF
or 0xAC00 <= codepoint <= 0xD7AF
)
def _is_bopomofo(char: str) -> bool:
codepoint = ord(char)
return 0x3100 <= codepoint <= 0x312F or 0x31A0 <= codepoint <= 0x31BF or char in _BOPOMOFO_TONES
def _zh_four_digit_section(value: int, *, suppress_leading_one: bool = True) -> str:
"""Read one non-negative, at-most-four-digit integer in Mandarin."""
output: list[str] = []
pending_zero = False
for position in range(3, -1, -1):
divisor = 10**position
digit = value // divisor
value %= divisor
if digit:
if pending_zero and output:
output.append(_ZH_DIGITS[0])
if not (digit == 1 and position == 1 and not output and suppress_leading_one):
output.append(_ZH_DIGITS[digit])
output.append(_ZH_SMALL_UNITS[position])
pending_zero = False
elif output and value:
pending_zero = True
return "".join(output)
def _zh_integer(value: int) -> str:
if value == 0:
return _ZH_DIGITS[0]
if value < 0 or value >= 10**16:
raise ValueError("Mandarin integer normalizer supports values from 0 to 10^16 - 1")
sections: list[int] = []
while value:
sections.append(value % 10000)
value //= 10000
output: list[str] = []
pending_zero = False
for section_index in range(len(sections) - 1, -1, -1):
section = sections[section_index]
if section == 0:
if output and any(sections[:section_index]):
pending_zero = True
continue
if output and (pending_zero or section < 1000):
output.append(_ZH_DIGITS[0])
output.append(_zh_four_digit_section(section, suppress_leading_one=not output))
output.append(_ZH_LARGE_UNITS[section_index])
pending_zero = False
return "".join(output)
def _zh_number(number: str) -> str:
"""Read a validated decimal string as a Mandarin cardinal number."""
sign = ""
if number.startswith(("+", "-")):
sign = "正" if number[0] == "+" else "負"
number = number[1:]
integer, separator, fraction = number.partition(".")
if not integer or not integer.isdigit() or separator and (not fraction or not fraction.isdigit()):
raise ValueError("invalid decimal number")
if len(integer) > 16:
raise ValueError("number is too large for conservative normalization")
spoken = _zh_integer(int(integer))
if separator:
spoken += "點" + "".join(_ZH_DIGITS[int(digit)] for digit in fraction)
return sign + spoken
def _zh_clock_time(hour_text: str, minute_text: str) -> str:
"""Return the canonical Mandarin reading for a validated clock time."""
hour = int(hour_text)
minute = int(minute_text)
minute_spoken = "整" if minute == 0 else f"{_zh_integer(minute)}分"
return f"{_zh_integer(hour)}點{minute_spoken}"
def _zh_digit_sequence(digits: str) -> str:
if not digits or not digits.isdigit():
raise ValueError("expected a non-empty digit sequence")
return "".join(_ZH_DIGITS[int(digit)] for digit in digits)
def _nonspace_neighbor(text: str, index: int, step: int) -> str | None:
"""Return the nearest non-space character on one side of ``index``."""
position = int(index)
while 0 <= position < len(text):
character = text[position]
if not character.isspace():
return character
position += int(step)
return None
def _semantic_neighbor(text: str, index: int, step: int) -> str | None:
"""Return a simplified alphanumeric/CJK context anchor."""
position = int(index)
while 0 <= position < len(text):
character = text[position]
if character.isalnum() or _is_cjk(character):
return _T2S_CONVERTER.convert(character).casefold()
position += int(step)
return None
def _structured_numeric_neighbor(character: str | None) -> bool:
if character is None:
return False
return bool(
character in _ASR_STRUCTURED_NUMERIC_NEIGHBORS
or character.isascii() and character.isalnum()
)
def _target_numeric_proofs(
normalized_target: str,
reading: str,
) -> list[tuple[int, int]]:
"""Find unstructured, exact target occurrences of one derived reading."""
proofs: list[tuple[int, int]] = []
start = 0
while True:
index = normalized_target.find(reading, start)
if index < 0:
return proofs
end = index + len(reading)
left = _nonspace_neighbor(normalized_target, index - 1, -1)
right = _nonspace_neighbor(normalized_target, end, 1)
if not (
_structured_numeric_neighbor(left)
or _structured_numeric_neighbor(right)
):
proofs.append((index, end))
start = index + 1
def _canonicalize_target_proven_arabic_digits(
normalized_text: str,
normalized_target: str,
) -> str:
"""Replace a bare ASR digit run only with its exact target-proven reading.
Both possible Mandarin readings are derived from the run itself: a
cardinal value (``1250`` -> ``一千二百五十``) and a digit sequence
(``1250`` -> ``一二五零``). A derived reading must occur in the target at
matching semantic left/right anchors. Structured numeric domains are
deliberately excluded because dates, clocks, versions, network values,
and model codes have their own stricter normalizers.
"""
matches = tuple(_ASR_ARABIC_DIGIT_RUN_RE.finditer(normalized_text))
if not matches:
return normalized_text
used_proofs: set[tuple[int, int, str]] = set()
output: list[str] = []
cursor = 0
for match in matches:
output.append(normalized_text[cursor : match.start()])
run = match.group(0)
left = _nonspace_neighbor(normalized_text, match.start() - 1, -1)
right = _nonspace_neighbor(normalized_text, match.end(), 1)
replacement: str | None = None
if not (
_structured_numeric_neighbor(left)
or _structured_numeric_neighbor(right)
):
readings: list[str] = []
try:
readings.append(_zh_integer(int(run)))
except ValueError:
pass
digit_reading = _zh_digit_sequence(run)
if digit_reading not in readings:
readings.append(digit_reading)
text_context = (
_semantic_neighbor(normalized_text, match.start() - 1, -1),
_semantic_neighbor(normalized_text, match.end(), 1),
)
for reading in readings:
for proof_start, proof_end in _target_numeric_proofs(
normalized_target,
reading,
):
proof_key = (proof_start, proof_end, reading)
if proof_key in used_proofs:
continue
target_context = (
_semantic_neighbor(
normalized_target,
proof_start - 1,
-1,
),
_semantic_neighbor(normalized_target, proof_end, 1),
)
if target_context == text_context:
replacement = reading
used_proofs.add(proof_key)
break
if replacement is not None:
break
output.append(replacement if replacement is not None else run)
cursor = match.end()
output.append(normalized_text[cursor:])
return "".join(output)
def _canonicalize_target_proven_structured_asr_forms(
text: str,
normalized_target: str,
) -> str:
"""Canonicalize Breeze-style clock/model renderings under exact proof.
Breeze ASR 25 commonly writes a spoken clock as ``7.45 分`` and emits
model identifiers in lowercase without their separator (``ax418``).
Those are legitimate written renderings, but globally interpreting every
decimal or lowercase alphanumeric run would hide real content errors.
Each replacement is therefore derived from the candidate text, required
verbatim in the already-normalized target, matched at the same semantic
left/right anchors, and consumed at most once.
"""
used_proofs: set[tuple[int, int, str]] = set()
def replace_matches(
value: str,
pattern: re.Pattern[str],
canonicalizer: Callable[[re.Match[str]], str | None],
) -> str:
matches = tuple(pattern.finditer(value))
if not matches:
return value
output: list[str] = []
cursor = 0
for match in matches:
output.append(value[cursor : match.start()])
canonical = canonicalizer(match)
replacement: str | None = None
if canonical:
text_context = (
_semantic_neighbor(value, match.start() - 1, -1),
_semantic_neighbor(value, match.end(), 1),
)
proof_start = 0
while True:
proof_start = normalized_target.find(canonical, proof_start)
if proof_start < 0:
break
proof_end = proof_start + len(canonical)
proof_key = (proof_start, proof_end, canonical)
target_context = (
_semantic_neighbor(
normalized_target,
proof_start - 1,
-1,
),
_semantic_neighbor(normalized_target, proof_end, 1),
)
if proof_key not in used_proofs and target_context == text_context:
replacement = canonical
used_proofs.add(proof_key)
break
proof_start += 1
output.append(replacement if replacement is not None else match.group(0))
cursor = match.end()
output.append(value[cursor:])
return "".join(output)
def canonical_clock(match: re.Match[str]) -> str | None:
try:
return _zh_clock_time(match.group(1), match.group(2))
except (TypeError, ValueError):
return None
def canonical_model_code(match: re.Match[str]) -> str | None:
letters, digits = match.groups()
try:
return f"{' '.join(letters.upper())} {_zh_digit_sequence(digits)}"
except ValueError:
return None
canonicalized = replace_matches(
text,
_ASR_FORMATTED_CLOCK_RE,
canonical_clock,
)
return replace_matches(
canonicalized,
_ASR_MODEL_CODE_RE,
canonical_model_code,
)
def _canonical_jpy_amount_reading(amount: str) -> str | None:
"""Return one validated Mandarin amount reading for a JPY proof span."""
compact = re.sub(r"\s+", "", str(amount or ""))
if re.fullmatch(_CURRENCY_NUMBER_PATTERN, compact):
try:
return _zh_number(compact.replace(",", ""))
except ValueError:
return None
if re.fullmatch(_ASR_JPY_ZH_AMOUNT_PATTERN, compact):
return compact.translate(_ASR_JPY_AMOUNT_TRANSLATION)
return None
def _canonicalize_target_proven_jpy_amounts(
normalized_text: str,
normalized_target: str,
) -> str:
"""Canonicalize a bounded JPY alias and amount only under an exact proof.
Whisper commonly renders spoken ``日圓四千五百`` as ``日元4500`` or
``日元4500元``. A global ``圓``/``元`` homophone or trailing-unit rule
would hide unrelated lexical errors, while the generic bare-number
normalizer correctly refuses to borrow a proof across unequal currency
anchors. This structured-domain path therefore requires a target span
without the redundant unit, the same exact amount, and the same semantic
characters immediately outside the complete candidate span. Every
target range is consumable once and ambiguous matches stay unchanged.
"""
target_proofs: list[
tuple[int, int, str, tuple[str | None, str | None], str]
] = []
for match in _ASR_JPY_CURRENCY_AMOUNT_RE.finditer(normalized_target):
if match.group("redundant_unit") is not None:
continue
reading = _canonical_jpy_amount_reading(match.group("amount"))
if reading is None:
continue
target_proofs.append(
(
match.start(),
match.end(),
reading,
(
_semantic_neighbor(
normalized_target,
match.start() - 1,
-1,
),
_semantic_neighbor(normalized_target, match.end(), 1),
),
match.group(0),
)
)
if not target_proofs:
return normalized_text
used_proofs: set[int] = set()
output: list[str] = []
cursor = 0
for match in _ASR_JPY_CURRENCY_AMOUNT_RE.finditer(normalized_text):
output.append(normalized_text[cursor : match.start()])
reading = _canonical_jpy_amount_reading(match.group("amount"))
text_context = (
_semantic_neighbor(normalized_text, match.start() - 1, -1),
_semantic_neighbor(normalized_text, match.end(), 1),
)
matching = [
index
for index, (_, _, proof_reading, proof_context, _) in enumerate(
target_proofs
)
if (
index not in used_proofs
and reading is not None
and proof_reading == reading
and proof_context == text_context
)
]
replacement = match.group(0)
if len(matching) == 1:
proof_index = matching[0]
used_proofs.add(proof_index)
replacement = target_proofs[proof_index][4]
output.append(replacement)
cursor = match.end()
output.append(normalized_text[cursor:])
return "".join(output)
@dataclass(frozen=True)
class _FrequencyMeasurement:
"""One explicitly unit-bearing frequency measurement."""
start: int
end: int
raw_text: str
dimension: str
base_unit_value: Decimal
context_key: tuple[str, int, int]
def _frequency_structured_spans(text: str) -> tuple[tuple[int, int], ...]:
"""Return numeric domains that must never authorize frequency scaling."""
patterns = (
_SPOKEN_URL_RE,
_SPOKEN_EMAIL_RE,
_SPOKEN_DATE_RE,
_SPOKEN_ZH_DATE_RE,
_SPOKEN_TIME_RE,
_SPOKEN_PERCENT_RE,
_SPOKEN_SEMVER_RE,
_SPOKEN_CURRENCY_PREFIX_RE,
_SPOKEN_CURRENCY_SUFFIX_RE,
_MODEL_CODE_RE,
)
return tuple(
(match.start(), match.end())
for pattern in patterns
for match in pattern.finditer(text)
)
def _frequency_model_prefix(text: str, start: int) -> bool:
"""Reject a measurement whose number is serving as a model identifier."""
prefix = text[:start]
clause_prefix = re.split(r"[,。!?;、,!?;\n]", prefix)[-1]
return bool(
re.search(
r"(?:型號|型号|版本|\bmodel\b|\bversion\b)",
clause_prefix,
flags=re.IGNORECASE,
)
or re.search(r"(?:\b(?:model|version)\s+|[A-Z]{2,8}[ -]*)$", prefix)
or re.search(r"(?:[A-Z](?:\s+[A-Z]){1,7})\s*$", prefix)
or re.search(
r"(?:型號|型号|版本)\s*"
r"(?:(?:[A-Za-z]\s*){2,8})?$",
prefix,
)
or re.search(
r"\b(?:model|version)\s+"
r"(?:(?:[A-Za-z]\s*){2,8})?$",
prefix,
flags=re.IGNORECASE,
)
)
def _parse_canonical_zh_integer(spoken: str) -> int | None:
"""Invert only the exact integer language emitted by ``_zh_integer``."""
if spoken == "零":
return 0
if not spoken:
return None
digit_values = {character: value for value, character in enumerate(_ZH_DIGITS)}
small_units = {"十": 10, "百": 100, "千": 1000}
large_units = {"萬": 10**4, "億": 10**8, "兆": 10**12}
total = 0
section = 0
pending_digit: int | None = None
for character in spoken:
if character in digit_values:
digit = digit_values[character]
if pending_digit not in {None, 0}:
return None
pending_digit = digit
continue
if character in small_units:
multiplier = 1 if pending_digit is None else pending_digit
section += multiplier * small_units[character]
pending_digit = None
continue
if character in large_units:
if pending_digit is not None:
section += pending_digit
if section <= 0:
return None
total += section * large_units[character]
section = 0
pending_digit = None
continue
return None
if pending_digit is not None:
section += pending_digit
value = total + section
if value >= 10**16 or _zh_integer(value) != spoken:
return None
return value
def _parse_canonical_zh_number(spoken: str) -> Decimal | None:
"""Invert a non-negative canonical number and verify it by round trip."""
explicit_positive = spoken.startswith("正")
unsigned = spoken[1:] if explicit_positive else spoken
if unsigned.count("點") > 1:
return None
integer_spoken, separator, fraction_spoken = unsigned.partition("點")
integer = _parse_canonical_zh_integer(integer_spoken)
if integer is None or separator and not fraction_spoken:
return None
digit_values = {character: value for value, character in enumerate(_ZH_DIGITS)}
if any(character not in digit_values for character in fraction_spoken):
return None
fraction = "".join(str(digit_values[character]) for character in fraction_spoken)
number_text = ("+" if explicit_positive else "") + str(integer)
if separator:
number_text += "." + fraction
try:
if _zh_number(number_text) != spoken:
return None
value = Decimal(number_text)
except (InvalidOperation, ValueError):
return None
return value if value.is_finite() and value >= 0 else None
def _frequency_context_skeleton(
text: str,
measurement_spans: Sequence[tuple[int, int]],
) -> str:
"""Return a punctuation-insensitive, spoken-form sentence skeleton.
Frequency spellings must be hidden before normalizing the rest of the
sentence. Otherwise the very scale difference being proved (for example
``48 kHz`` versus ``48000Hz``) leaks into the context key. Normalizing the
remaining text also lets equivalent raw/ASR unit readings such as
``10 km/h`` and ``10公里每小時`` provide the same conservative
full-sentence context.
"""
output: list[str] = []
cursor = 0
for start, end in measurement_spans:
output.append(text[cursor:start])
output.append("¤")
cursor = end
output.append(text[cursor:])
normalized = normalize_spoken_forms("".join(output))
simplified = _T2S_CONVERTER.convert(normalized).casefold()
return "".join(
character
for character in simplified
if character == "¤" or character.isalnum() or _is_cjk(character)
)
def _extract_frequency_measurements(
text: str,
) -> tuple[
tuple[_FrequencyMeasurement, ...],
bool,
dict[tuple[str, int, int], frozenset[Decimal]],
]:
"""Extract measurements, ambiguity, and exact base-Hz evidence sets."""
candidates_by_span: dict[
tuple[int, int],
list[tuple[int, int, str, Decimal, int]],
] = {}
structured_spans = _frequency_structured_spans(text)
def is_blocked(start: int, end: int) -> bool:
return bool(
any(
start < structured_end and end > structured_start
for structured_start, structured_end in structured_spans
)
or _frequency_model_prefix(text, start)
)
def add_candidate(
start: int,
end: int,
raw_text: str,
number: Decimal,
scale: Decimal,
priority: int,
) -> None:
if is_blocked(start, end):
return
with localcontext() as context:
# Decimal arithmetic still observes a context precision. Size it
# from the exact coefficient so multiplying by a power-of-ten
# frequency scale cannot round a long fractional measurement.
context.prec = max(64, len(number.as_tuple().digits) + 16)
base_value = number * scale
span = (start, end)
candidates_by_span.setdefault(span, []).append(
(
start,
end,
raw_text,
base_value,
priority,
)
)
for match in _ASR_FREQUENCY_MEASUREMENT_RE.finditer(text):
number_text = match.group(1)
try:
# Keep this proof path inside the same numeric domain supported by
# the unchanged frontend normalizer.
_zh_number(number_text)
number = Decimal(number_text)
except (InvalidOperation, ValueError):
continue
scale = _FREQUENCY_SCALE_HZ.get(match.group(2).casefold())
if scale is None or not number.is_finite() or number < 0:
continue
add_candidate(
match.start(),
match.end(),
match.group(0),
number,
scale,
-1,
)
for priority, (pattern, unit) in enumerate(_CANONICAL_ZH_FREQUENCY_PATTERNS):
scale = _FREQUENCY_SCALE_HZ[unit]
for match in pattern.finditer(text):
number = _parse_canonical_zh_number(match.group(1))
if number is None:
continue
add_candidate(
match.start(),
match.end(),
match.group(0),
number,
scale,
priority,
)
if not candidates_by_span:
return (), False, {}
ordered_spans = sorted(candidates_by_span)
measurement_spans = tuple(ordered_spans)
skeleton = _frequency_context_skeleton(text, measurement_spans)
measurement_count = len(ordered_spans)
raw_matches: list[tuple[int, int, str, Decimal, int]] = []
evidence_by_context: dict[
tuple[str, int, int],
frozenset[Decimal],
] = {}
ambiguous = False
context_by_span: dict[tuple[int, int], tuple[str, int, int]] = {}
for ordinal, span in enumerate(ordered_spans):
candidates = candidates_by_span[span]
base_values = frozenset(candidate[3] for candidate in candidates)
context_key = (skeleton, ordinal, measurement_count)
context_by_span[span] = context_key
evidence_by_context[context_key] = base_values
if len(base_values) != 1:
ambiguous = True
continue
raw_matches.append(min(candidates, key=lambda candidate: candidate[4]))
if not raw_matches:
return (), ambiguous, evidence_by_context
raw_matches.sort(key=lambda item: item[0])
measurements: list[_FrequencyMeasurement] = []
for start, end, raw_text, base_value, _ in raw_matches:
measurements.append(
_FrequencyMeasurement(
start=start,
end=end,
raw_text=raw_text,
dimension="frequency",
base_unit_value=base_value,
context_key=context_by_span[(start, end)],
)
)
return tuple(measurements), ambiguous, evidence_by_context
def _raw_frequency_measurements(text: str) -> tuple[_FrequencyMeasurement, ...]:
"""Extract only unambiguous, context-bound frequency measurements."""
measurements, _, _ = _extract_frequency_measurements(text)
return measurements
def _has_ambiguous_canonical_frequency(text: str) -> bool:
"""Return whether frontend-normalized text lost frequency provenance."""
_, ambiguous, _ = _extract_frequency_measurements(text)
return ambiguous
def _frequency_measurements_contradict(text: str, target_text: str) -> bool:
"""Reject explicit frequency values whose exact base-Hz sets disagree."""
def reject_only_evidence(
value: str,
) -> dict[tuple[str, int, int], frozenset[Decimal]]:
raw_matches: list[tuple[int, int, Decimal]] = []
for match in _REJECT_ONLY_FREQUENCY_MEASUREMENT_RE.finditer(value):
try:
number = Decimal(match.group(1))
except InvalidOperation:
continue
scale = _FREQUENCY_SCALE_HZ.get(match.group(2).casefold())
if scale is None or not number.is_finite() or number < 0:
continue
with localcontext() as context:
context.prec = max(64, len(number.as_tuple().digits) + 16)
base_value = number * scale
raw_matches.append((match.start(), match.end(), base_value))
if not raw_matches:
return {}
spans = tuple((start, end) for start, end, _ in raw_matches)
skeleton = _frequency_context_skeleton(value, spans)
count = len(raw_matches)
return {
(skeleton, ordinal, count): frozenset((base_value,))
for ordinal, (_, _, base_value) in enumerate(raw_matches)
}
target_reject_evidence = reject_only_evidence(target_text)
text_reject_evidence = reject_only_evidence(text)
for context_key, target_values in target_reject_evidence.items():
text_values = text_reject_evidence.get(context_key)
if text_values is not None and target_values.isdisjoint(text_values):
return True
_, _, target_evidence = _extract_frequency_measurements(target_text)
_, _, text_evidence = _extract_frequency_measurements(text)
for context_key, target_values in target_evidence.items():
text_values = text_evidence.get(context_key)
if text_values is not None and target_values.isdisjoint(text_values):
return True
return False
def _canonicalize_target_proven_frequency_scales(
text: str,
target_text: str,
) -> str:
"""Rewrite ASR frequency scale only under an exact raw target proof."""
target_measurements, target_ambiguous, _ = _extract_frequency_measurements(
target_text
)
text_measurements, _, _ = _extract_frequency_measurements(text)
if target_ambiguous or not target_measurements or not text_measurements:
return text
target_by_context = {
measurement.context_key: measurement
for measurement in target_measurements
}
output: list[str] = []
cursor = 0
for measurement in text_measurements:
output.append(text[cursor : measurement.start])
proof = target_by_context.get(measurement.context_key)
replacement = measurement.raw_text
if (
proof is not None
and proof.dimension == measurement.dimension
and proof.base_unit_value == measurement.base_unit_value
):
replacement = proof.raw_text
output.append(replacement)
cursor = measurement.end
output.append(text[cursor:])
return "".join(output)
def _zh_network_text(value: str) -> str:
"""Read lexical labels naturally while keeping typed atoms explicit."""
output: list[str] = []
buffer: list[str] = []
def flush() -> None:
if not buffer:
return
token = "".join(buffer)
if token.isdigit():
output.append(_zh_digit_sequence(token))
elif token.isascii() and token.isalpha():
if token.casefold() == "www" or (len(token) > 1 and token.isupper()):
output.append(_zh_network_letters(token))
else:
output.append(token)
else:
output.append(token)
buffer.clear()
for character in value:
if character.isascii() and character.isalnum():
if buffer and character.isdigit() != buffer[-1].isdigit():
flush()
buffer.append(character)
continue
flush()
reading = _ZH_NETWORK_SYMBOL_READINGS.get(character)
if reading:
output.append(reading)
elif not character.isspace() and not character.isalnum():
# Never let an accepted-but-unmapped ASCII symbol disappear when
# comparison normalization removes punctuation. The explicit
# Unicode codepoint reading is conservative and keeps exact
# omission detection fail-closed for unusual URI/IRI characters.
output.append(f"符號{_zh_digit_sequence(str(ord(character)))}")
elif not character.isspace():
output.append(character)
flush()
return " ".join(output)
def _zh_network_letters(value: str) -> str:
"""Return model-native spaced uppercase letters for typed ASCII atoms."""
return " ".join(
character.upper()
for character in value
if not character.isspace()
)
def _zh_version_qualifier(value: str) -> str:
"""Spell opaque prerelease/build labels without lexical network rules."""
output: list[str] = []
for character in value:
if character.isascii() and character.isalpha():
output.append(character.upper())
elif character.isascii() and character.isdigit():
output.append(_ZH_DIGITS[int(character)])
else:
reading = _ZH_NETWORK_SYMBOL_READINGS.get(character)
if reading:
output.append(reading)
elif not character.isspace():
output.append(character)
return " ".join(output)
def _zh_network_domain(value: str) -> str:
"""Render an explicit URL-fallback domain without lexical guessing."""
head, separator, suffix = value.rpartition(".")
if separator and head and suffix.isascii() and suffix.isalpha():
return f"{_zh_network_text(head)} 點 {_zh_network_letters(suffix)}"
return _zh_network_text(value)
def _zh_email_domain(value: str) -> str:
"""Render uniquely segmented email labels and explicit separators.
Compound labels such as ``forestmail`` are substantially clearer to the
acoustic model as ``forest mail``. Segmentation is allowed only when the
fixed lexical dictionary proves one unique full-label decomposition; all
opaque, ambiguous and typed labels retain the existing literal reading.
The email verifier reconstructs adjacent lexical tokens before applying
the exact raw-address proof, so this changes pronunciation without
weakening identifier equality.
"""
head, separator, suffix = value.rpartition(".")
if separator and head and suffix.isascii() and suffix.isalpha():
spoken_labels: list[str] = []
for label in head.split("."):
segments = _unique_email_domain_label_segments(label)
spoken_labels.append(
" ".join(segments)
if len(segments) > 1
else _zh_network_text(label)
)
return (
f"{' 點 '.join(spoken_labels)} 點 "
f"{_zh_network_letters(suffix)}"
)
return _zh_network_text(value)
@dataclass(frozen=True)
class NetworkUrlRenderAtomProof:
"""One raw URL slice and its exact emitted spoken range."""
component: str
rule: str
raw_start: int
raw_end: int
spoken_start: int
spoken_end: int
raw_text: str
spoken_text: str
@dataclass(frozen=True)
class NetworkUrlRenderingProof:
"""Complete reversible proof for one safely naturalized HTTP(S) URL."""
raw_identifier: str
spoken_text: str
atoms: tuple[NetworkUrlRenderAtomProof, ...]
_NATURALIZED_URL_LEXICAL_WORDS = frozenset(
{
"audio",
"coast",
"data",
"forest",
"help",
"island",
"mail",
"museum",
"news",
"radio",
"search",
"sound",
"tour",
"trail",
"voice",
"watch",
"weather",
}
)
_EMAIL_DOMAIN_LEXICAL_WORDS = (
_NATURALIZED_URL_LEXICAL_WORDS
| frozenset({"arts", "harbor"})
)
_NATURALIZED_URL_ATOM_COMPONENTS = frozenset(
{"scheme", "host_label", "host_dot", "path_slash", "path_atom", "terminator"}
)
_NATURALIZED_URL_ATOM_RULES = frozenset(
{
"https_scheme_cue",
"http_scheme_cue",
"lexical_host_label",
"segmented_host_label",
"host_dot",
"spelled_host_suffix",
"path_cue",
"spelled_short_path",
"url_terminator",
}
)
_NATURALIZED_URL_MAX_SPOKEN_UNITS = 36
def _unique_lexical_label_segments(
label: str,
lexical_words: frozenset[str],
) -> tuple[str, ...]:
"""Return one lexicon-proven split, otherwise retain the whole label."""
if (
not label.isascii()
or not label.isalpha()
or not label.islower()
or len(label) < 6
):
return (label,)
solutions: list[tuple[str, ...]] = []
def search(offset: int, path: tuple[str, ...]) -> None:
if len(solutions) > 1:
return
if offset == len(label):
if len(path) >= 2:
solutions.append(path)
return
for end in range(offset + 3, len(label) + 1):
word = label[offset:end]
if word in lexical_words:
search(end, (*path, word))
search(0, ())
return solutions[0] if len(solutions) == 1 else (label,)
def _unique_naturalized_url_label_segments(label: str) -> tuple[str, ...]:
"""Return one URL-dictionary-proven split or retain the whole label."""
return _unique_lexical_label_segments(
label,
_NATURALIZED_URL_LEXICAL_WORDS,
)
def _unique_email_domain_label_segments(label: str) -> tuple[str, ...]:
"""Return one email-dictionary-proven split or retain the whole label."""
return _unique_lexical_label_segments(
label,
_EMAIL_DOMAIN_LEXICAL_WORDS,
)
def _build_naturalized_url_rendering_proof(
value: str,
) -> NetworkUrlRenderingProof | None:
"""Build the one canonical rendering for the safe short-path grammar.
Unsupported ports, credentials, queries, fragments, non-DNS hosts, long
paths or non-canonical casing fall back to the existing fully explicit URL
spelling. This unchecked builder is also the independent source of truth
used by :func:`inverse_network_url_rendering`.
"""
raw = unicodedata.normalize("NFC", str(value or ""))
if not raw or not raw.isascii():
return None
try:
parsed = urlsplit(raw)
port = parsed.port
except ValueError:
return None
del port
scheme = parsed.scheme
host = parsed.hostname or ""
path = parsed.path
if (
scheme not in {"http", "https"}
or parsed.username is not None
or parsed.password is not None
or parsed.netloc != host
or parsed.query
or parsed.fragment
or not re.fullmatch(r"[a-z]+(?:\.[a-z]+)+", host)
# URL paths are case-sensitive. Accept one canonical casing only so
# acoustically identical upper/lower paths can never share a proof.
or not re.fullmatch(r"(?:/[a-z]{1,4})?", path)
or raw != f"{scheme}://{host}{path}"
):
return None
pieces: list[str] = []
atoms: list[NetworkUrlRenderAtomProof] = []
spoken_cursor = 0
def emit(
*,
component: str,
rule: str,
raw_start: int,
raw_end: int,
spoken: str,
) -> None:
nonlocal spoken_cursor
pieces.append(spoken)
spoken_end = spoken_cursor + len(spoken)
atoms.append(
NetworkUrlRenderAtomProof(
component=component,
rule=rule,
raw_start=raw_start,
raw_end=raw_end,
spoken_start=spoken_cursor,
spoken_end=spoken_end,
raw_text=raw[raw_start:raw_end],
spoken_text=spoken,
)
)
spoken_cursor = spoken_end
scheme_end = len(scheme) + 3
emit(
component="scheme",
rule=(
"https_scheme_cue"
if scheme == "https"
else "http_scheme_cue"
),
raw_start=0,
raw_end=scheme_end,
spoken=("安全網址是 " if scheme == "https" else "網址是 "),
)
host_start = scheme_end
raw_cursor = host_start
labels = host.split(".")
segmented_label_seen = False
for label_index, label in enumerate(labels):
if label_index:
emit(
component="host_dot",
rule="host_dot",
raw_start=raw_cursor,
raw_end=raw_cursor + 1,
spoken=" 點 ",
)
raw_cursor += 1
if label_index + 1 == len(labels) and len(label) <= 3:
for character_index, character in enumerate(label):
emit(
component="host_label",
rule="spelled_host_suffix",
raw_start=raw_cursor + character_index,
raw_end=raw_cursor + character_index + 1,
spoken=(
character.upper()
if character_index == 0
else f" {character.upper()}"
),
)
else:
segments = _unique_naturalized_url_label_segments(label)
segmented_label_seen = segmented_label_seen or len(segments) > 1
segment_cursor = raw_cursor
for segment_index, segment in enumerate(segments):
emit(
component="host_label",
rule=(
"segmented_host_label"
if len(segments) > 1
else "lexical_host_label"
),
raw_start=segment_cursor,
raw_end=segment_cursor + len(segment),
spoken=(
segment
if segment_index == 0
else f" {segment}"
),
)
segment_cursor += len(segment)
raw_cursor += len(label)
if path:
slash_start = host_start + len(host)
emit(
component="path_slash",
rule="path_cue",
raw_start=slash_start,
raw_end=slash_start + 1,
spoken=",路徑是 ",
)
for character_index, character in enumerate(path[1:]):
emit(
component="path_atom",
rule="spelled_short_path",
raw_start=slash_start + 1 + character_index,
raw_end=slash_start + 2 + character_index,
spoken=(
character.upper()
if character_index == 0
else f" {character.upper()}"
),
)
emit(
component="terminator",
rule="url_terminator",
raw_start=len(raw),
raw_end=len(raw),
spoken=",網址輸入完畢",
)
proof = NetworkUrlRenderingProof(
raw_identifier=raw,
spoken_text="".join(pieces),
atoms=tuple(atoms),
)
if not segmented_label_seen:
return None
# The natural phrase is intentionally indivisible. Long generic URLs keep
# the legacy explicit spelling so the existing component planner can split
# them rather than turning a valid request into a hard planning failure.
if count_speech_units(proof.spoken_text) > _NATURALIZED_URL_MAX_SPOKEN_UNITS:
return None
return proof
def _naturalized_url_rendering_proof(
value: str,
) -> NetworkUrlRenderingProof | None:
"""Render and independently validate the safe short-path URL subset."""
proof = _build_naturalized_url_rendering_proof(value)
if proof is not None and inverse_network_url_rendering(proof) != proof.raw_identifier:
raise ValueError("naturalized URL rendering is not reversible")
return proof
def inverse_network_url_rendering(proof: NetworkUrlRenderingProof) -> str:
"""Validate every atom range and reconstruct the exact raw identifier."""
if not isinstance(proof, NetworkUrlRenderingProof):
raise ValueError("URL rendering proof has an invalid type")
raw = proof.raw_identifier
spoken = proof.spoken_text
if (
not raw
or not raw.isascii()
or not spoken
or not isinstance(proof.atoms, tuple)
or not proof.atoms
):
raise ValueError("URL rendering proof is incomplete")
raw_cursor = 0
spoken_cursor = 0
reconstructed: list[str] = []
terminator_seen = False
for atom_index, atom in enumerate(proof.atoms):
if (
not isinstance(atom, NetworkUrlRenderAtomProof)
or atom.component not in _NATURALIZED_URL_ATOM_COMPONENTS
or atom.rule not in _NATURALIZED_URL_ATOM_RULES
or type(atom.raw_start) is not int
or type(atom.raw_end) is not int
or type(atom.spoken_start) is not int
or type(atom.spoken_end) is not int
or not 0 <= atom.raw_start <= atom.raw_end <= len(raw)
or not 0 <= atom.spoken_start < atom.spoken_end <= len(spoken)
or atom.spoken_start != spoken_cursor
or raw[atom.raw_start : atom.raw_end] != atom.raw_text
or spoken[atom.spoken_start : atom.spoken_end] != atom.spoken_text
):
raise ValueError("URL rendering atom proof is inconsistent")
if atom.component == "terminator":
if (
terminator_seen
or atom_index + 1 != len(proof.atoms)
or atom.raw_start != len(raw)
or atom.raw_end != len(raw)
or atom.spoken_text != ",網址輸入完畢"
):
raise ValueError("URL terminator proof is inconsistent")
terminator_seen = True
else:
if terminator_seen or atom.raw_start != raw_cursor:
raise ValueError("URL rendering raw ranges are not contiguous")
reconstructed.append(atom.raw_text)
raw_cursor = atom.raw_end
spoken_cursor = atom.spoken_end
if (
not terminator_seen
or raw_cursor != len(raw)
or spoken_cursor != len(spoken)
or "".join(reconstructed) != raw
):
raise ValueError("URL rendering proof does not cover its identifier")
scheme = raw.partition("://")[0]
first = proof.atoms[0]
expected_scheme_rule = (
"https_scheme_cue"
if scheme == "https"
else "http_scheme_cue"
)
expected_scheme_spoken = (
"安全網址是 " if scheme == "https" else "網址是 "
)
if (
scheme not in {"http", "https"}
or first.component != "scheme"
or first.rule != expected_scheme_rule
or first.spoken_text != expected_scheme_spoken
):
raise ValueError("URL scheme cue does not match its raw proof")
expected = _build_naturalized_url_rendering_proof(raw)
if expected is None or proof != expected:
raise ValueError("URL rendering proof is not the canonical raw mapping")
return raw
def naturalized_url_rendering_proof(
value: str,
) -> NetworkUrlRenderingProof | None:
"""Return the validated reversible proof for the safe URL subset."""
return _naturalized_url_rendering_proof(value)
_NATURALIZED_URL_SPOKEN_LABEL_PATTERN = (
r"(?:[a-z]+(?: [a-z]+)*|[A-Z](?: [A-Z]){0,2})"
)
_NATURALIZED_URL_SPOKEN_RE = re.compile(
rf"(?<!\w)(?P<cue>安全網址是|網址是) "
rf"(?P<host>{_NATURALIZED_URL_SPOKEN_LABEL_PATTERN}"
rf"(?: 點 {_NATURALIZED_URL_SPOKEN_LABEL_PATTERN})+)"
r"(?:,路徑是 (?P<path>[A-Z](?: [A-Z]){0,3}))?"
r",網址輸入完畢(?!\w)"
)
def _naturalized_url_proof_from_spoken(
spoken: str,
) -> NetworkUrlRenderingProof | None:
"""Recover a proof only when ``spoken`` is one canonical renderer image."""
normalized = normalize_tts_text(spoken)
match = _NATURALIZED_URL_SPOKEN_RE.fullmatch(normalized)
if match is None:
return None
scheme = "https" if match.group("cue") == "安全網址是" else "http"
labels: list[str] = []
for label in match.group("host").split(" 點 "):
tokens = label.split()
if tokens and all(
token.isascii() and token.isalpha() and token.islower()
for token in tokens
):
labels.append("".join(tokens))
continue
if (
tokens
and len(tokens) <= 3
and all(
len(token) == 1
and token.isascii()
and token.isalpha()
and token.isupper()
for token in tokens
)
):
labels.append("".join(tokens).lower())
continue
return None
path = match.group("path")
path_tokens = path.split() if path else ()
raw = f"{scheme}://{'.'.join(labels)}"
if path_tokens:
raw += f"/{''.join(path_tokens).lower()}"
proof = _build_naturalized_url_rendering_proof(raw)
return proof if proof is not None and proof.spoken_text == normalized else None
def _naturalized_url_spoken_records(
text: str,
) -> tuple[tuple[int, int, str], ...]:
"""Locate canonical naturalized URLs without accepting free-form cue prose."""
source = normalize_tts_text(text)
output: list[tuple[int, int, str]] = []
for match in _NATURALIZED_URL_SPOKEN_RE.finditer(source):
spoken = match.group(0)
if _naturalized_url_proof_from_spoken(spoken) is not None:
output.append((match.start(), match.end(), spoken))
return tuple(output)
def contains_naturalized_url_spoken_form(text: str) -> bool:
"""Return whether text contains one complete canonical naturalized URL."""
return bool(_naturalized_url_spoken_records(text))
def _strip_naturalized_url_prosodic_commas(text: str) -> str:
"""Drop only renderer-authored, non-lexical URL pause marks."""
def replace(match: re.Match[str]) -> str:
pieces = [match.group(1)]
if match.group(2):
pieces.append(match.group(2))
pieces.append(match.group(3))
return " ".join(pieces)
return re.sub(
r"((?:安全網址是|網址是)\s+.*?)"
r"(?:\s*[,,]?\s*(路徑是\s+.*?))?"
r"\s*[,,]?\s*(網址輸入完畢)",
replace,
str(text or ""),
)
def _zh_url(value: str) -> str:
naturalized = _naturalized_url_rendering_proof(value)
if naturalized is not None:
return naturalized.spoken_text
scheme, separator, remainder = value.partition("://")
if separator:
protocol = _zh_network_letters(scheme)
prefix = f"{protocol} 冒號 斜線 斜線 "
else:
remainder = value
prefix = ""
boundary = re.search(r"[/?#]", remainder)
split_at = boundary.start() if boundary is not None else len(remainder)
host = remainder[:split_at]
tail = remainder[split_at:]
spoken_host = _zh_network_domain(host)
spoken_tail = f" {_zh_network_text(tail)}" if tail else ""
return f"{prefix}{spoken_host}{spoken_tail}"
def _zh_email(value: str) -> str:
local, domain = value.rsplit("@", 1)
return f"{_zh_network_text(local)} 小老鼠 {_zh_email_domain(domain)}"
def contains_network_identifier(text: str) -> bool:
"""Return whether raw input contains an explicit URL or email address."""
raw = unicodedata.normalize("NFC", str(text or ""))
return bool(_SPOKEN_URL_RE.search(raw) or _SPOKEN_EMAIL_RE.search(raw))
def network_identifier_has_ambiguous_iri(text: str) -> bool:
"""Reject non-ASCII URL identifiers whose speech collides with ASCII."""
raw = unicodedata.normalize("NFC", str(text or ""))
return any(
not match.group(0).isascii()
for match in _SPOKEN_URL_RE.finditer(raw)
)
_NETWORK_BOUNDARY_PUNCTUATION = (
_NETWORK_NONASCII_BOUNDARY_PUNCTUATION + ",.!?;:()[]{}<>\"'"
)
_NETWORK_ALIGNMENT_BOUNDARY_SENTINEL = "\ue200"
_NETWORK_SPOKEN_SYMBOL_TOKENS = frozenset(
(*_ZH_NETWORK_SYMBOL_READINGS.values(), *_ZH_NETWORK_LETTER_READINGS.values())
)
def _raw_network_spoken_spans(text: str) -> tuple[str, ...]:
"""Expand explicit raw URL/email spans in source order without overlap."""
raw = unicodedata.normalize("NFC", str(text or ""))
matches: list[tuple[int, int, str]] = []
for match in _SPOKEN_URL_RE.finditer(raw):
matched = match.group(0)
if matched:
matches.append((match.start(), match.end(), _zh_url(matched)))
for match in _SPOKEN_EMAIL_RE.finditer(raw):
matches.append((match.start(), match.end(), _zh_email(match.group(0))))
selected: list[tuple[int, int, str]] = []
for start, end, spoken in sorted(matches, key=lambda item: (item[0], -item[1])):
if any(start < kept_end and end > kept_start for kept_start, kept_end, _ in selected):
continue
selected.append((start, end, spoken))
return tuple(spoken for _, _, spoken in sorted(selected))
def _is_expanded_network_token(token: str) -> bool:
if token.isalnum():
return True
if token.isascii() and token and all(
character.isalnum() or character in _ZH_NETWORK_SYMBOL_READINGS
for character in token
):
return True
if token and all(character in _ZH_DIGITS for character in token):
return True
if token.startswith("符號") and token[2:] and all(
character in _ZH_DIGITS for character in token[2:]
):
return True
return (
token in _NETWORK_SPOKEN_SYMBOL_TOKENS
or token in _SIMPLIFIED_NETWORK_SYMBOL_READINGS
)
def _network_letter_token(token: str) -> str | None:
if len(token) == 1 and token.isascii() and token.isalpha():
return token.casefold()
return _ZH_NETWORK_READING_TO_LETTER.get(token)
_SPOKEN_EMAIL_SYMBOL_TOKENS = {
reading: symbol
for symbol, reading in _ZH_NETWORK_SYMBOL_READINGS.items()
if symbol in ".!#$%&'*+/=?^_`{|}~-@"
}
def _spoken_email_token_value(token: str) -> str | None:
letter = _network_letter_token(token)
if letter is not None:
return letter
if token.isascii() and token.isalnum():
return token.casefold()
if token and all(character in _ZH_DIGITS for character in token):
return "".join(str(_ZH_DIGITS.index(character)) for character in token)
canonical = _SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
return _SPOKEN_EMAIL_SYMBOL_TOKENS.get(canonical)
def _spoken_email_subspan_bounds(tokens: Sequence[str]) -> tuple[tuple[int, int], ...]:
"""Locate maximal syntactically valid spoken email token ranges."""
tokens = tuple(_SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token) for token in tokens)
output: list[tuple[int, int]] = []
for at, token in enumerate(tokens):
if token != "小老鼠":
continue
if any(
tokens[index : index + 3] == ["冒號", "斜線", "斜線"]
for index in range(max(0, at - 2))
):
continue
www_end = None
for start in range(at):
if (
start + 1 < at
and tokens[start].casefold() == "www"
and tokens[start + 1] == "點"
):
www_end = start + 2
break
if (
start + 3 < at
and [
_network_letter_token(part)
for part in tokens[start : start + 3]
]
== ["w", "w", "w"]
and tokens[start + 3] == "點"
):
www_end = start + 4
break
if www_end is not None and any(
part in {"斜線", "問號", "井號", "冒號"}
for part in tokens[www_end:]
):
continue
left_floor = 0
for separator in range(at):
if tokens[separator] == "和" and "小老鼠" in tokens[:separator]:
left_floor = separator + 1
left = at
while left > left_floor:
value = _spoken_email_token_value(tokens[left - 1])
if value is None or value == "@":
break
left -= 1
right = at + 1
while right < len(tokens):
value = _spoken_email_token_value(tokens[right])
if value is None or value == "@":
break
right += 1
matches: list[tuple[int, int, int, int]] = []
for start in range(left, at):
for end in range(at + 2, right + 1):
rendered = "".join(
value
for part in tokens[start:end]
if (value := _spoken_email_token_value(part)) is not None
)
if _SPOKEN_EMAIL_RE.fullmatch(rendered):
matches.append((len(rendered), end - start, -start, end))
if matches:
match = max(matches)
output.append((-match[2], match[3]))
return tuple(output)
def _spoken_email_subspans(span: str) -> tuple[str, ...]:
tokens = span.split()
return tuple(
" ".join(tokens[start:end])
for start, end in _spoken_email_subspan_bounds(tokens)
)
_SIMPLIFIED_NETWORK_SYMBOL_READINGS = {
"点": "點",
"冒号": "冒號",
"斜线": "斜線",
"问号": "問號",
"等于": "等於",
"井号": "井號",
"横线": "橫線",
"底线": "底線",
"百分号": "百分號",
"加号": "加號",
"波浪号": "波浪號",
"惊叹号": "驚嘆號",
"钱字号": "錢字號",
"单引号": "單引號",
"左括号": "左括號",
"右括号": "右括號",
"星号": "星號",
"逗号": "逗號",
"分号": "分號",
"左方括号": "左方括號",
"右方括号": "右方括號",
"插入号": "插入號",
"反引号": "反引號",
"左大括号": "左大括號",
"竖线": "豎線",
"右大括号": "右大括號",
}
def _canonicalize_network_spoken_span(span: str) -> str:
"""Normalize opaque network tokens without applying prose unit rules."""
symbol_readings = frozenset(_ZH_NETWORK_SYMBOL_READINGS.values())
output: list[str] = []
for token in span.split():
letter = _network_letter_token(token)
if letter is not None:
output.append(_ZH_NETWORK_LETTER_READINGS[letter.upper()])
continue
if token in symbol_readings:
output.append(token)
continue
if token in _SIMPLIFIED_NETWORK_SYMBOL_READINGS:
output.append(_SIMPLIFIED_NETWORK_SYMBOL_READINGS[token])
continue
if token and token.isascii() and all(
character.isalnum() or character in _ZH_NETWORK_SYMBOL_READINGS
for character in token
):
for character in token:
if character.isdigit():
output.append(_ZH_DIGITS[int(character)])
elif character.isalpha():
output.append(_ZH_NETWORK_LETTER_READINGS[character.upper()])
else:
output.append(_ZH_NETWORK_SYMBOL_READINGS[character])
continue
output.append(token)
return " ".join(output)
def _expanded_network_spoken_span_records(
text: str,
) -> tuple[tuple[int, int, str], ...]:
"""Recover protected spans after the request frontend has expanded them.
The Space normalizes the whole request before chunking, so the quality
path receives spoken forms rather than the original URL/email spelling.
A maximal run is considered network data only when it contains an
unambiguous scheme, email, or ``www`` anchor.
"""
source = normalize_tts_text(str(text or ""))
records: list[tuple[str, int, int]] = []
boundary_pattern = f"([{re.escape(_NETWORK_BOUNDARY_PUNCTUATION)}])"
source_cursor = 0
for segment in re.split(boundary_pattern, source):
if not segment:
continue
if len(segment) == 1 and segment in _NETWORK_BOUNDARY_PUNCTUATION:
position = source.find(segment, source_cursor)
if position < 0:
raise ValueError("network span tokenizer lost a boundary")
records.append(("", position, position + 1))
source_cursor = position + 1
continue
segment_start = source.find(segment, source_cursor)
if segment_start < 0:
raise ValueError("network span tokenizer lost a segment")
records.extend(
(
match.group(0),
segment_start + match.start(),
segment_start + match.end(),
)
for match in re.finditer(r"\S+", segment)
)
source_cursor = segment_start + len(segment)
spans: list[tuple[int, int, str]] = []
index = 0
while index < len(records):
if not _is_expanded_network_token(records[index][0]):
index += 1
continue
end = index
while end < len(records) and _is_expanded_network_token(records[end][0]):
end += 1
tokens = [token for token, _, _ in records[index:end]]
comparison_tokens = [
_SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
for token in tokens
]
protected_start: int | None = None
for anchor in range(1, len(comparison_tokens) - 2):
if comparison_tokens[anchor : anchor + 3] != ["冒號", "斜線", "斜線"]:
continue
for scheme in ("https", "http", "ftp"):
length = len(scheme)
start = anchor - length
letters = (
[
_network_letter_token(token)
for token in comparison_tokens[start:anchor]
]
if start >= 0
else []
)
if letters and None not in letters and "".join(letters) == scheme:
protected_start = start
break
if protected_start is not None:
break
email_bounds = (
_spoken_email_subspan_bounds(comparison_tokens)
if protected_start is None
else ()
)
if protected_start is None and not email_bounds:
for start in range(max(0, len(comparison_tokens) - 3)):
prefix = comparison_tokens[start]
split_prefix = [
_network_letter_token(token)
for token in comparison_tokens[start : start + 3]
]
if (
(
prefix.casefold() == "www"
and comparison_tokens[start + 1] == "點"
)
or (
split_prefix == ["w", "w", "w"]
and comparison_tokens[start + 3] == "點"
)
):
protected_start = start
break
if protected_start is not None:
span_start = records[index + protected_start][1]
span_end = records[end - 1][2]
spans.append((span_start, span_end, source[span_start:span_end]))
elif email_bounds:
for token_start, token_end in email_bounds:
span_start = records[index + token_start][1]
span_end = records[index + token_end - 1][2]
spans.append((span_start, span_end, source[span_start:span_end]))
index = max(end, index + 1)
return tuple(spans)
def _expanded_network_spoken_spans(text: str) -> tuple[str, ...]:
return tuple(
spoken
for _, _, spoken in _expanded_network_spoken_span_records(text)
)
def network_protected_spoken_spans(text: str) -> tuple[str, ...]:
"""Return exact spoken URL/email spans from raw or frontend-normalized text."""
raw_spans = _raw_network_spoken_spans(text)
if raw_spans:
return raw_spans
records = (
*_naturalized_url_spoken_records(text),
*_expanded_network_spoken_span_records(text),
)
selected: list[tuple[int, int, str]] = []
for start, end, spoken in sorted(records, key=lambda item: (item[0], -item[1])):
if any(
start < kept_end and end > kept_start
for kept_start, kept_end, _ in selected
):
continue
selected.append((start, end, spoken))
return tuple(spoken for _, _, spoken in selected)
def _zh_semantic_version(match: re.Match[str]) -> str:
major, minor, patch, prerelease, build = match.groups()
try:
core = "點".join(_zh_integer(int(part)) for part in (major, minor, patch))
except ValueError:
return match.group(0)
output = f"版本{core}"
if prerelease:
output += f" 預發布 {_zh_version_qualifier(prerelease)}"
if build:
output += f" 建置 {_zh_version_qualifier(build)}"
return output
def _zh_currency(code: str, number: str, original: str) -> str:
reading = _ZH_CURRENCY_READINGS.get(code.upper(), _ZH_CURRENCY_READINGS.get(code))
if reading is None:
return original
try:
spoken_number = _zh_number(number.replace(",", ""))
except ValueError:
return original
return reading + spoken_number
def normalize_spoken_forms(text: str, *, locale: str = "zh-TW") -> str:
"""Conservatively expand common written forms for TTS.
Chinese locales expand validated dates, network addresses, semantic
versions, currencies, percentages, numeric units, and standalone
all-uppercase acronyms/model identifiers. Ordinary English words are
deliberately left untouched. English locales only receive the existing
Unicode/punctuation normalization. Unknown locales raise instead of
silently applying the wrong pronunciation rules.
"""
raw = unicodedata.normalize("NFC", str(text or ""))
locale_key = str(locale or "").replace("_", "-").lower()
if locale_key in {"en", "en-us", "en-gb"}:
return normalize_tts_text(raw)
if locale_key not in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}:
raise ValueError(f"unsupported spoken-form locale: {locale!r}")
if _UNSUPPORTED_QUOTED_EMAIL_RE.search(raw):
raise ValueError("quoted email local parts are not supported")
if any(
match.group(0).endswith(tuple(".,!?;:'"))
for match in _SPOKEN_URL_RE.finditer(raw)
):
raise ValueError(
"URLs ending in ambiguous punctuation are not supported; "
"percent-encode the final symbol"
)
protected: list[str] = []
def protect(spoken: str) -> str:
marker = f"\uf000{len(protected)}\uf001"
protected.append(spoken)
return marker
def network_prefix_boundary(match: re.Match[str]) -> str:
preceding = match.string[: match.start()].rstrip()[-1:]
if not preceding or preceding in _NETWORK_BOUNDARY_PUNCTUATION:
return ""
return ","
def network_suffix_boundary(match: re.Match[str], *, trailing: str = "") -> str:
"""Keep expanded network data separate from following prose.
Once a URL/email is expanded, whitespace alone cannot distinguish its
final path/domain token from ordinary following English. A semantic
comma is inaudible as lexical content but preserves that provenance
for chunking and the exact protected-range verifier.
"""
if trailing:
return ""
remainder = match.string[match.end() :]
following = remainder.lstrip()[:1]
if not following or following in _NETWORK_BOUNDARY_PUNCTUATION:
return ""
return ","
def replace_url(match: re.Match[str]) -> str:
matched = match.group(0)
return (
network_prefix_boundary(match)
+ protect(_zh_url(matched))
+ network_suffix_boundary(match)
)
def replace_email(match: re.Match[str]) -> str:
return (
network_prefix_boundary(match)
+ protect(_zh_email(match.group(0)))
+ network_suffix_boundary(match)
)
def replace_date(match: re.Match[str]) -> str:
year_text, _, month_text, day_text = match.groups()
try:
date(int(year_text), int(month_text), int(day_text))
except ValueError:
return match.group(0)
return (
f"{_zh_digit_sequence(year_text)}年"
f"{_zh_integer(int(month_text))}月{_zh_integer(int(day_text))}日"
)
def replace_zh_date(match: re.Match[str]) -> str:
year_text, month_text, day_text = match.groups()
try:
date(int(year_text), int(month_text), int(day_text))
except ValueError:
return match.group(0)
return (
f"{_zh_digit_sequence(year_text)}年"
f"{_zh_integer(int(month_text))}月{_zh_integer(int(day_text))}日"
)
def replace_number(match: re.Match[str], suffix: str) -> str:
try:
return _zh_number(match.group(1)) + suffix
except ValueError:
return match.group(0)
def replace_time(match: re.Match[str]) -> str:
return _zh_clock_time(match.group(1), match.group(2))
def replace_percent(match: re.Match[str]) -> str:
try:
return "百分之" + _zh_number(match.group(1))
except ValueError:
return match.group(0)
def replace_unit(match: re.Match[str]) -> str:
unit_key = re.sub(r"\s+", "", match.group(2)).lower()
reading = _ZH_UNIT_READINGS.get(unit_key)
if reading is None:
return match.group(0)
return replace_number(match, reading)
def replace_model_code(match: re.Match[str]) -> str:
letters, digits = match.groups()
return f"{' '.join(letters)} {_zh_digit_sequence(digits)}"
output = _SPOKEN_URL_RE.sub(replace_url, raw)
output = _SPOKEN_EMAIL_RE.sub(replace_email, output)
output = _SPOKEN_ZH_DATE_RE.sub(replace_zh_date, output)
output = _SPOKEN_DATE_RE.sub(replace_date, output)
output = _SPOKEN_TIME_RE.sub(replace_time, output)
output = _SPOKEN_PERCENT_RE.sub(replace_percent, output)
output = _SPOKEN_UNIT_RE.sub(replace_unit, output)
output = _SPOKEN_ZH_MEASURE_RE.sub(
lambda match: replace_number(match, match.group(2)),
output,
)
output = _SPOKEN_SEMVER_RE.sub(_zh_semantic_version, output)
output = _SPOKEN_CURRENCY_PREFIX_RE.sub(
lambda match: _zh_currency(match.group(1), match.group(2), match.group(0)),
output,
)
output = _SPOKEN_CURRENCY_SUFFIX_RE.sub(
lambda match: _zh_currency(match.group(2), match.group(1), match.group(0)),
output,
)
output = _MODEL_CODE_RE.sub(replace_model_code, output)
output = _UPPERCASE_ACRONYM_RE.sub(lambda match: " ".join(match.group(1)), output)
for index, spoken in enumerate(protected):
output = output.replace(f"\uf000{index}\uf001", spoken)
return normalize_tts_text(output)
def normalize_asr_spoken_forms(
text: str,
target_text: str,
*,
locale: str = "zh-TW",
) -> str:
"""Canonicalize only target-proven ASR numeric and frequency readings."""
raw_target = unicodedata.normalize("NFC", str(target_text or ""))
raw_text = unicodedata.normalize("NFC", str(text or ""))
normalized_target = normalize_spoken_forms(raw_target, locale=locale)
locale_key = str(locale or "").replace("_", "-").lower()
if locale_key not in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}:
return normalize_spoken_forms(raw_text, locale=locale)
# Preserve complete literal URL/email spans before fragment punctuation
# canonicalization. Otherwise rewriting a dot first destroys the raw URL
# regex and makes a correct path/query fail the protected exact-range gate.
protected_network: list[str] = []
def network_comparison_key(value: str) -> str:
canonical = normalize_tts_eval_text(value).casefold()
for reading, letter in sorted(
_ZH_NETWORK_READING_TO_LETTER.items(),
key=lambda item: len(item[0]),
reverse=True,
):
canonical = canonical.replace(reading.casefold(), letter)
canonical = _T2S_CONVERTER.convert(canonical)
output: list[str] = []
for character in canonical:
if character in _ZH_DIGITS:
output.append(str(_ZH_DIGITS.index(character)))
continue
try:
output.append(str(unicodedata.decimal(character)))
except (TypeError, ValueError):
if character.isalnum():
output.append(character)
return "".join(output)
target_network_spans = network_protected_spoken_spans(normalized_target)
target_network_keys = {
network_comparison_key(span)
for span in target_network_spans
}
email_symbol_tokens = {
reading: symbol
for symbol, reading in _ZH_NETWORK_SYMBOL_READINGS.items()
if symbol in ".!#$%&'*+/=?^_`{|}~-@"
}
def spoken_email_token_value(token: str) -> str | None:
token = _SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
letter = _network_letter_token(token)
if letter is not None:
return letter
if token.isascii() and token.isalnum():
return token.casefold()
if token and all(character in _ZH_DIGITS for character in token):
return "".join(str(_ZH_DIGITS.index(character)) for character in token)
return email_symbol_tokens.get(token)
def spoken_email_subspans(span: str) -> tuple[str, ...]:
"""Extract maximal syntactically valid emails around spoken ``@``."""
tokens = [
_SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
for token in span.split()
]
output: list[str] = []
for at, token in enumerate(tokens):
if token != "小老鼠":
continue
if any(
tokens[index : index + 3] == ["冒號", "斜線", "斜線"]
for index in range(max(0, at - 2))
):
continue
www_end = None
for start in range(at):
if (
start + 1 < at
and tokens[start].casefold() == "www"
and tokens[start + 1] == "點"
):
www_end = start + 2
break
if (
start + 3 < at
and [
_network_letter_token(part)
for part in tokens[start : start + 3]
]
== ["w", "w", "w"]
and tokens[start + 3] == "點"
):
www_end = start + 4
break
if www_end is not None and any(
part in {"斜線", "問號", "井號", "冒號"}
for part in tokens[www_end:]
):
continue
left_floor = 0
for separator in range(at):
if tokens[separator] == "和" and "小老鼠" in tokens[:separator]:
left_floor = separator + 1
left = at
while left > left_floor:
value = spoken_email_token_value(tokens[left - 1])
if value is None or value == "@":
break
left -= 1
right = at + 1
while right < len(tokens):
value = spoken_email_token_value(tokens[right])
if value is None or value == "@":
break
right += 1
matches: list[tuple[int, int, int, str]] = []
for start in range(left, at):
for end in range(at + 2, right + 1):
rendered = "".join(
value
for part in tokens[start:end]
if (value := spoken_email_token_value(part)) is not None
)
if _SPOKEN_EMAIL_RE.fullmatch(rendered):
matches.append(
(
len(rendered),
end - start,
-start,
" ".join(tokens[start:end]),
)
)
if matches:
output.append(max(matches)[3])
return tuple(output)
target_email_key_limits: dict[str, int] = {}
target_email_value_limits: dict[str, int] = {}
for span in target_network_spans:
for email_span in spoken_email_subspans(span):
key = network_comparison_key(email_span)
if key:
target_email_key_limits[key] = target_email_key_limits.get(key, 0) + 1
rendered = "".join(
value
for token in email_span.split()
if (value := spoken_email_token_value(token)) is not None
)
if _SPOKEN_EMAIL_RE.fullmatch(rendered):
rendered = rendered.casefold()
target_email_value_limits[rendered] = (
target_email_value_limits.get(rendered, 0) + 1
)
def protect_network(spoken: str) -> str:
marker = f"\uf100{len(protected_network)}\uf101"
protected_network.append(_canonicalize_network_spoken_span(spoken))
return marker
# Pinned Whisper decoders sometimes romanize the spoken email separator
# and remove the whitespace at the preceding CJK/ASCII boundary. Recover
# only a complete email that is already proven by the normalized target;
# every local-part, domain and TLD atom must match exactly and identifier
# characters are forbidden on both outer edges.
email_identifier_boundary = r"A-Za-z0-9.!#$%&'*+/=?^_`{|}~@-"
for target_email, occurrence_limit in target_email_value_limits.items():
local, domain = target_email.rsplit("@", 1)
alias_pattern = re.compile(
rf"(?i)(?<![{email_identifier_boundary}])"
rf"{re.escape(local)}\s*"
rf"(?:xiaolaoshui|xiaolaoshu|xlouse)\s*"
rf"{re.escape(domain)}"
rf"(?![{email_identifier_boundary}])"
)
raw_text = alias_pattern.sub(
lambda _match, email=target_email: protect_network(_zh_email(email)),
raw_text,
count=occurrence_limit,
)
def protect_url(match: re.Match[str]) -> str:
matched = match.group(0)
alias_in_or_after_match = (
_ASR_XIAOLAOSHU_ALIAS_RE.search(matched) is not None
or re.match(
r"\s*xiaolaoshu",
match.string[match.end() :],
flags=re.IGNORECASE,
)
is not None
)
if target_email_key_limits and alias_in_or_after_match:
# A no-space ``www.fooXiaolaoshuexample.tw`` rendering is first
# recognized by the broad schemeless-URL regex. Leave it visible
# to the exact target-email alias proof below; wrong content still
# remains literal and fails the protected-range comparison.
return matched
core = matched
# Whisper commonly emits ASCII sentence punctuation directly after a
# URL. Keep a trailing symbol inside the protected range only when the
# normalized target proves that exact URL spelling; otherwise preserve
# it outside the marker as ordinary punctuation.
while core and core[-1] in ".,!?;:":
if network_comparison_key(_zh_url(core)) in target_network_keys:
break
core = core[:-1]
trailing = matched[len(core) :]
return protect_network(_zh_url(core)) + trailing
raw_text = _SPOKEN_URL_RE.sub(protect_url, raw_text)
raw_text = _SPOKEN_EMAIL_RE.sub(
lambda match: protect_network(_zh_email(match.group(0))),
raw_text,
)
target_network_tokens = {
token
for span in target_network_spans
for token in span.split()
}
if {"冒號", "斜線"}.issubset(target_network_tokens):
raw_text = re.sub(
r"\s*:\s*/\s*/\s*",
" 冒號 斜線 斜線 ",
raw_text,
)
# Whisper often formats a correctly spoken URL/email fragment back into
# literal punctuation. Expand only separators proven by the target and
# bounded by identifier characters. Missing/reordered letters still fail,
# while sentence punctuation is deliberately outside the bounded pattern.
for symbol, reading in _ZH_NETWORK_SYMBOL_READINGS.items():
alpha_side = r"(?:[A-Za-z]\s*)+"
fragment_proven = re.search(
rf"{alpha_side}{re.escape(reading)}\s*{alpha_side}",
normalized_target,
) is not None
if reading not in target_network_tokens and not fragment_proven:
continue
raw_text = re.sub(
rf"(?:"
rf"(?<=[A-Za-z0-9])\s*{re.escape(symbol)}\s*(?=[^\W_])"
rf"|(?<=[^\W_])\s*{re.escape(symbol)}\s*(?=[A-Za-z0-9])"
rf")",
f" {reading} ",
raw_text,
)
# Whisper can render the spoken email separator ``小老鼠`` as its
# Mandarin pinyin ``Xiaolaoshu``, including without spaces between opaque
# labels. Try each occurrence independently and accept it only when the
# replacement adds exact coverage of a complete target email span. This
# keeps URL paths containing ``@``, wrong domains, missing labels, literal
# labels, prose aliases, and already-covered targets fail-closed.
def covered_target_email_counts(value: str) -> dict[str, int]:
unprotected = value
for index in range(len(protected_network)):
unprotected = unprotected.replace(f"\uf100{index}\uf101", ",")
counts = {key: 0 for key in target_email_key_limits}
for span in protected_network:
for email_span in spoken_email_subspans(span):
key = network_comparison_key(email_span)
limit = target_email_key_limits.get(key, 0)
if limit and counts[key] < limit:
counts[key] += 1
for span in network_protected_spoken_spans(unprotected):
for email_span in spoken_email_subspans(span):
key = network_comparison_key(email_span)
limit = target_email_key_limits.get(key, 0)
if limit and counts[key] < limit:
counts[key] += 1
return counts
alias_search_start = 0
while target_email_key_limits:
alias_match = _ASR_XIAOLAOSHU_ALIAS_RE.search(raw_text, alias_search_start)
if alias_match is None:
break
candidate = (
raw_text[: alias_match.start()]
+ " 小老鼠 "
+ raw_text[alias_match.end() :]
)
before_counts = covered_target_email_counts(raw_text)
after_counts = covered_target_email_counts(candidate)
if any(
after_counts[key] > before_counts[key]
for key in target_email_key_limits
):
raw_text = candidate
alias_search_start = alias_match.start() + len(" 小老鼠 ")
else:
alias_search_start = alias_match.end()
for spoken in network_protected_spoken_spans(raw_text):
pattern = r"\s+".join(re.escape(token) for token in spoken.split())
match = re.search(pattern, raw_text)
if match is None:
continue
raw_text = (
raw_text[: match.start()]
+ protect_network(spoken)
+ raw_text[match.end() :]
)
placeholder_prefix = "藍鵲網路保護佔位符"
while placeholder_prefix in raw_text or placeholder_prefix in raw_target:
placeholder_prefix += "號"
network_placeholders: list[tuple[str, str]] = []
for index, spoken in enumerate(protected_network):
marker = f"\uf100{index}\uf101"
placeholder = f"{placeholder_prefix}{_zh_integer(index + 1)}結束"
raw_text = raw_text.replace(marker, placeholder)
network_placeholders.append((placeholder, spoken))
def restore_network_placeholders(value: str) -> str:
for placeholder, spoken in network_placeholders:
value = value.replace(placeholder, spoken)
return value
raw_text = _canonicalize_target_proven_structured_asr_forms(
raw_text,
normalized_target,
)
target_proven_text = _canonicalize_target_proven_frequency_scales(
raw_text,
raw_target,
)
# Preserve numeric grouping long enough to prove ASR forms such as
# ``日圓4,500元``. General punctuation normalization would otherwise
# turn the thousands separator into a sentence comma before the bounded
# JPY matcher sees it. The post-normalization pass remains necessary for
# already-spoken Chinese amount forms.
target_proven_text = _canonicalize_target_proven_jpy_amounts(
target_proven_text,
normalized_target,
)
normalized_text = normalize_spoken_forms(target_proven_text, locale=locale)
normalized_text = _canonicalize_target_proven_jpy_amounts(
normalized_text,
normalized_target,
)
normalized_text = _canonicalize_target_proven_arabic_digits(
normalized_text,
normalized_target,
)
# Only an unambiguous target reading may prove that Whisper's numeric
# ``15點30(分)`` rendering means a clock time. In particular, do not
# rewrite the target's own numeric form first: that would let ambiguous
# decimals such as ``15點05分貝`` or scores self-authorize a clock
# canonicalization. Colon times have already been expanded by
# ``normalize_spoken_forms`` and fully-spoken clock targets already contain
# the canonical phrase, so both remain valid proof sources.
target_clock_text = normalized_target.translate(
str.maketrans({"点": "點", "時": "點", "时": "點"})
)
if (
_ASR_EXPLICIT_ZH_TIME_RE.search(normalized_target) is not None
or _ASR_BARE_ZH_TIME_RE.search(normalized_target) is not None
):
# A numeric ``X點Y`` construction in the target is itself ambiguous.
# Do not let a separate, fully-spoken clock elsewhere in the sentence
# globally authorize rewriting that decimal, duration, or score.
return restore_network_placeholders(normalized_text)
def replace_target_proven_time(match: re.Match[str]) -> str:
canonical = _zh_clock_time(match.group(1), match.group(2))
return canonical if canonical in target_clock_text else match.group(0)
normalized_text = _ASR_EXPLICIT_ZH_TIME_RE.sub(
replace_target_proven_time,
normalized_text,
)
normalized_text = _ASR_BARE_ZH_TIME_RE.sub(
replace_target_proven_time,
normalized_text,
)
return restore_network_placeholders(normalize_tts_text(normalized_text))
def normalize_tts_text(text: str) -> str:
"""Normalize common punctuation and pronounce standalone Bopomofo symbols."""
raw = unicodedata.normalize("NFC", str(text or ""))
has_cjk = any(_is_cjk(char) for char in raw)
output: list[str] = []
index = 0
while index < len(raw):
char = raw[index]
if char.isspace() or char in {"\r", "\n", "\t", "\u3000"}:
output.append(" ")
index += 1
continue
if unicodedata.category(char)[0] == "C":
index += 1
continue
if raw.startswith("...", index):
output.append("。" if has_cjk else ".")
index += 3
while index < len(raw) and raw[index] == ".":
index += 1
continue
if char in {"…", "⋯"}:
output.append("。" if has_cjk else ".")
index += 1
while index < len(raw) and raw[index] in {"…", "⋯"}:
index += 1
continue
if _is_bopomofo(char):
while index < len(raw) and _is_bopomofo(raw[index]):
reading = _BOPOMOFO_READINGS.get(raw[index])
if reading:
output.append(reading)
index += 1
continue
previous = raw[index - 1] if index else ""
following = raw[index + 1] if index + 1 < len(raw) else ""
folded = unicodedata.normalize("NFKC", char)
if folded.isascii() and folded.isalnum():
char = folded
if char in {",", ",", "﹐", "、"}:
output.append("," if previous.isdigit() and following.isdigit() else ("," if has_cjk else ","))
elif char in {".", "。", "。", "."}:
inside_ascii = previous.isascii() and previous.isalnum() and following.isascii() and following.isalnum()
output.append("." if inside_ascii or not has_cjk else "。")
elif char in {"?", "?", "﹖"}:
output.append("?" if has_cjk else "?")
elif char in {"!", "!", "﹗"}:
output.append("!" if has_cjk else "!")
elif char in {";", ";"}:
output.append(";" if has_cjk else ";")
elif char in {":", ":"}:
ascii_context = (
previous.isascii()
and previous.isalnum()
and following.isascii()
and following.isalnum()
)
output.append(":" if following == "/" or ascii_context else (":" if has_cjk else ":"))
elif char in {"“", "”", "„", """}:
output.append('"')
elif char in {"‘", "’", "'"}:
output.append("'")
elif char == "(":
output.append("(")
elif char == ")":
output.append(")")
else:
output.append(char)
index += 1
normalized = _SPACE_RE.sub(" ", "".join(output))
normalized = _PUNCT_NO_LEFT_SPACE_RE.sub(r"\1", normalized)
normalized = _CJK_PUNCT_RIGHT_SPACE_RE.sub(r"\1", normalized)
return normalized.strip()
_UNSPOKEN_CJK_QUOTE_MARKS = frozenset("「」『』")
def strip_unspoken_cjk_quotes_for_generation(text: str) -> str:
"""Remove layout-only CJK quote marks from ordinary generation text.
This helper is intentionally generation-only and must not run on network
requests: quotation marks remain useful raw boundaries around a URL or
email. Lexical verification continues to use the original request, so no
spoken unit is waived by this acoustic-model input cleanup.
"""
normalized = normalize_tts_text(text)
if not any(mark in normalized for mark in _UNSPOKEN_CJK_QUOTE_MARKS):
return normalized
without_quotes = "".join(
" " if character in _UNSPOKEN_CJK_QUOTE_MARKS else character
for character in normalized
)
return normalize_tts_text(without_quotes)
def normalize_tts_eval_text(text: str) -> str:
"""Normalize only verified ASR-equivalent forms for semantic scoring.
This remains separate from model-input normalization: Mandarin pronoun
homophones are acoustically indistinguishable, but their written forms must
stay untouched in the text sent to the TTS model.
"""
normalized = normalize_tts_text(text)
normalized = normalized.translate(_ASR_HOMOPHONE_TRANSLATION)
if "注音" in normalized or "符號" in normalized:
normalized = _BOPOMOFO_ASR_POPO_RE.sub("波坡摸", normalized)
return normalized
def ensure_terminal_punctuation(text: str) -> str:
"""Give the acoustic model an explicit endpoint cue without changing words."""
normalized = normalize_tts_text(text)
if not normalized:
return ""
split_at = len(normalized)
while split_at > 0 and normalized[split_at - 1] in _TRAILING_CLOSERS:
split_at -= 1
core = normalized[:split_at].rstrip()
suffix = normalized[split_at:]
if core and core[-1] in _TERMINAL_PUNCTUATION:
return normalized
while core and core[-1] in _NONTERMINAL_TRAILING_PUNCTUATION:
core = core[:-1].rstrip()
if not core:
return normalized
punctuation = "。" if any(_is_cjk(char) for char in core) else "."
return f"{core}{punctuation}{suffix}"
def count_speech_units(text: str) -> int:
"""Count CJK characters and compressed ASCII runs for pace control."""
units = 0
ascii_buffer: list[str] = []
def flush_ascii() -> None:
nonlocal units
if not ascii_buffer:
return
token = "".join(ascii_buffer)
divisor = 2 if token.isdigit() else 4
units += max(1, math.ceil(len(token) / divisor))
ascii_buffer.clear()
for char in normalize_tts_text(text).lower():
if char.isascii() and char.isalnum():
ascii_buffer.append(char)
elif _is_cjk(char):
flush_ascii()
units += 1
else:
flush_ascii()
flush_ascii()
return units
def count_network_endpoint_duration_units(text: str) -> int:
"""Count conservative duration units for network-conditioned generation.
Public pace, chunk planning and generation-work budgets intentionally keep
using :func:`count_speech_units`. The acoustic model needs more native
endpoint headroom for opaque URL/email labels, though, so this separate
counter compresses every contiguous ASCII alphanumeric run by two instead
of compressing alphabetic runs by four. It is valid only for the
network-conditioned endpoint estimate and never imposes a minimum length.
"""
units = 0
ascii_run_length = 0
def flush_ascii() -> None:
nonlocal units, ascii_run_length
if ascii_run_length <= 0:
return
units += max(1, math.ceil(ascii_run_length / 2))
ascii_run_length = 0
for char in normalize_tts_text(text).lower():
if char.isascii() and char.isalnum():
ascii_run_length += 1
elif _is_cjk(char):
flush_ascii()
units += 1
else:
flush_ascii()
flush_ascii()
return units
def effective_generation_cfg(
text: str,
requested_cfg: float,
*,
short_text_unit_threshold: int = 6,
short_text_min_cfg: float = 3.0,
) -> float:
"""Use stronger acoustic guidance only for empirically unstable short text."""
cfg = float(requested_cfg)
units = count_speech_units(text)
if 0 < units <= max(1, int(short_text_unit_threshold)):
return max(cfg, float(short_text_min_cfg))
return cfg
def estimate_step_seconds(model, sample_rate: int) -> float | None:
patch_size = int(
getattr(model, "patch_size", 0)
or getattr(getattr(model, "config", None), "patch_size", 0)
or 0
)
audio_vae = getattr(model, "audio_vae", None)
decode_chunk = int(
getattr(model, "_decode_chunk_size", 0)
or getattr(audio_vae, "decode_chunk_size", 0)
or getattr(audio_vae, "chunk_size", 0)
or 0
)
if patch_size <= 0 or decode_chunk <= 0 or sample_rate <= 0:
return None
return float(patch_size * decode_chunk / sample_rate)
def target_cps_min_len(
text: str,
target_cps: float,
step_seconds: float | None,
base_min_len: int = 2,
stop_consecutive: int = 1,
) -> int:
if target_cps <= 0.0 or not step_seconds or step_seconds <= 0.0:
return int(base_min_len)
units = count_speech_units(text)
if units <= 0:
return int(base_min_len)
target_steps = math.ceil((units / target_cps) / step_seconds)
earliest_stop_offset = max(1, int(stop_consecutive)) + 1
return max(int(base_min_len), max(0, target_steps - earliest_stop_offset))
def target_cps_steps(text: str, target_cps: float, step_seconds: float | None) -> int:
if target_cps <= 0.0 or not step_seconds or step_seconds <= 0.0:
return 0
units = count_speech_units(text)
if units <= 0:
return 0
return max(1, math.ceil((units / target_cps) / step_seconds))
def target_pace_speed(
audio_samples: int,
sample_rate: int,
text: str,
*,
target_cps: float,
min_speed: float = 0.80,
) -> float:
"""Return a pitch-preserving stretch rate without extending generation."""
units = count_speech_units(text)
if audio_samples <= 0 or sample_rate <= 0 or units <= 0 or target_cps <= 0.0:
return 1.0
actual_seconds = float(audio_samples) / float(sample_rate)
target_seconds = float(units) / float(target_cps)
if actual_seconds >= target_seconds:
return 1.0
return min(1.0, max(float(min_speed), actual_seconds / target_seconds))
def active_pace_correction_speed(
active_duration_seconds: float,
text: str,
*,
target_cps: float,
prior_speed: float = 1.0,
min_total_speed: float = 0.80,
) -> float:
"""Return a second stretch rate from active-voice duration.
``prior_speed`` is the rate already applied by ``target_pace_speed``.
Successive pitch-preserving stretch rates multiply, so the second rate is
floored at ``min_total_speed / prior_speed``. Invalid evidence and audio
that is already at or below the target active CPS fail safely to ``1.0``;
this helper never speeds audio up.
"""
units = count_speech_units(text)
try:
active_seconds = float(active_duration_seconds)
target = float(target_cps)
previous_rate = float(prior_speed)
total_floor = float(min_total_speed)
except (TypeError, ValueError, OverflowError):
return 1.0
if (
units <= 0
or not math.isfinite(active_seconds)
or active_seconds <= 0.0
or not math.isfinite(target)
or target <= 0.0
or not math.isfinite(previous_rate)
or not 0.0 < previous_rate <= 1.0
or not math.isfinite(total_floor)
or not 0.0 < total_floor <= 1.0
):
return 1.0
desired_rate = active_seconds * target / float(units)
if not math.isfinite(desired_rate) or desired_rate >= 1.0:
return 1.0
remaining_floor = total_floor / previous_rate
if remaining_floor >= 1.0:
return 1.0
return min(1.0, max(remaining_floor, desired_rate))
def duration_hard_stop_steps(
expected_steps: int,
*,
ratio: float = 1.08,
margin_steps: int = 3,
fallback: int = 2000,
) -> int:
expected_steps = max(0, int(expected_steps))
if expected_steps <= 0:
return max(1, int(fallback))
return max(
expected_steps + max(0, int(margin_steps)),
math.ceil(expected_steps * max(1.0, float(ratio))),
)
def endpoint_generation_plan(
text: str,
*,
generation_cps: float,
step_seconds: float | None,
margin_steps: int = 1,
add_terminal_punctuation: bool = True,
duration_units: int | None = None,
) -> tuple[str, int, int]:
"""Plan a native-pace generation cap independently of output playback pace."""
model_text = (
ensure_terminal_punctuation(text)
if add_terminal_punctuation
else normalize_tts_text(text)
)
if duration_units is None:
expected_steps = target_cps_steps(
model_text,
generation_cps,
step_seconds,
)
else:
if isinstance(duration_units, bool):
raise ValueError("duration_units must be a non-negative integer")
try:
planned_units = operator.index(duration_units)
except (TypeError, ValueError, OverflowError) as error:
raise ValueError(
"duration_units must be a non-negative integer"
) from error
if planned_units < 0:
raise ValueError("duration_units must be a non-negative integer")
expected_steps = (
0
if (
planned_units <= 0
or generation_cps <= 0.0
or not step_seconds
or step_seconds <= 0.0
)
else max(
1,
math.ceil(
(planned_units / generation_cps) / step_seconds
),
)
)
hard_stop_steps = duration_hard_stop_steps(
expected_steps,
ratio=1.0,
margin_steps=margin_steps,
)
return model_text, expected_steps, hard_stop_steps
def select_generation_cps(
text: str,
*,
cjk_cps: float = 5.2,
ascii_cps: float = 4.6,
) -> float:
"""Reserve more generation time for ASCII words than compact CJK units."""
normalized = normalize_tts_text(text)
if (
any(char.isascii() and char.isalnum() for char in normalized)
or network_protected_spoken_spans(normalized)
):
return float(ascii_cps)
return float(cjk_cps)
def finish_audio(
audio: np.ndarray,
sample_rate: int,
*,
fade_ms: float = 60.0,
trailing_silence_ms: float = 180.0,
) -> np.ndarray:
"""Fade a forced endpoint and leave a short, unambiguous final pause."""
signal = np.asarray(audio, dtype=np.float32).reshape(-1).copy()
fade_samples = min(
signal.size,
max(0, int(round(float(fade_ms) * int(sample_rate) / 1000.0))),
)
if fade_samples > 0:
phase = np.linspace(
0.0,
np.pi / 2.0,
fade_samples,
dtype=np.float32,
)
envelope = np.square(np.cos(phase))
envelope[-1] = 0.0
signal[-fade_samples:] *= envelope
silence_samples = max(
0,
int(round(float(trailing_silence_ms) * int(sample_rate) / 1000.0)),
)
if silence_samples > 0:
signal = np.pad(signal, (0, silence_samples))
return signal
_SEMANTIC_STRONG_BREAKS = frozenset("。!?!?;;")
_SEMANTIC_SOFT_BREAKS = frozenset(",,、:: ")
_QUALITY_MEDIUM_BREAKS = frozenset("。!?!?;;,,、::")
_NETWORK_CONTEXTUAL_EMAIL_MIN_PROSE_UNITS = 7
_SEMANTIC_TRAILING_CLOSERS = frozenset("\"'”’」』】))]}")
_PROTECTED_ASCII_SPAN_RE = re.compile(
r"(?i)[a-z0-9]+(?:[._+/@:#~%-]+[a-z0-9]+)+"
r"|(?<![a-z0-9])(?:[a-z]\s+){2,}[a-z](?![a-z0-9])"
)
_STRUCTURED_CLAUSE_BREAKS = frozenset(",,、::")
_STRUCTURED_CLAUSE_TARGET_UNITS = 32
_NETWORK_ATOMIC_MAX_UNITS = 36
_NETWORK_GENERATION_COMPONENT_AFTER = frozenset()
_NETWORK_GENERATION_COMPONENT_BEFORE = frozenset(
{"點", "小老鼠", "斜線", "問號", "井號", "冒號", "和"}
)
_NORMALIZED_ZH_NUMBER_PATTERN = (
r"[正負]?[" + _ZH_DIGITS + r"十百千萬億兆]+"
r"(?:點[" + _ZH_DIGITS + r"]+)?"
)
_NORMALIZED_STRUCTURED_NUMERIC_CUE_RE = re.compile(
rf"(?:"
rf"百分之{_NORMALIZED_ZH_NUMBER_PATTERN}"
rf"|(?:新台幣|美元|港幣|人民幣|日圓|歐元|英鎊|韓元)"
rf"{_NORMALIZED_ZH_NUMBER_PATTERN}"
rf"|{_NORMALIZED_ZH_NUMBER_PATTERN}(?:"
rf"公里每小時|公尺每秒|攝氏度|千赫茲|兆赫茲|"
rf"吉赫茲|赫茲|毫秒|公里|公尺|公分|公厘|"
rf"公斤|公克|毫克|公升|毫升|千瓦|瓦|伏特"
rf")"
rf"|[" + _ZH_DIGITS + rf"]{{4}}年{_NORMALIZED_ZH_NUMBER_PATTERN}月"
rf"{_NORMALIZED_ZH_NUMBER_PATTERN}日"
rf"|{_NORMALIZED_ZH_NUMBER_PATTERN}點(?:整|{_NORMALIZED_ZH_NUMBER_PATTERN}分)"
rf"|版本{_NORMALIZED_ZH_NUMBER_PATTERN}點{_NORMALIZED_ZH_NUMBER_PATTERN}"
rf"點{_NORMALIZED_ZH_NUMBER_PATTERN}"
rf")"
)
def _semantic_sentence_units(text: str) -> list[str]:
"""Return strong-punctuation units, keeping closing quotes with the sentence."""
units: list[str] = []
start = 0
index = 0
while index < len(text):
if text[index] not in _SEMANTIC_STRONG_BREAKS:
index += 1
continue
end = index + 1
while end < len(text) and text[end] in _SEMANTIC_TRAILING_CLOSERS:
end += 1
unit = text[start:end].strip()
if unit:
units.append(unit)
start = end
index = end
tail = text[start:].strip()
if tail:
units.append(tail)
return units
def _ends_at_strong_boundary(text: str) -> bool:
core = str(text).rstrip("".join(_SEMANTIC_TRAILING_CLOSERS))
return bool(core and core[-1] in _SEMANTIC_STRONG_BREAKS)
def _expanded_network_split_ranges(text: str) -> tuple[tuple[int, int], ...]:
"""Locate frontend-expanded network spans in normalized model text."""
normalized = normalize_tts_text(text)
records = (
*_naturalized_url_spoken_records(normalized),
*_expanded_network_spoken_span_records(normalized),
)
ranges: list[tuple[int, int]] = []
for start, end, _ in sorted(records, key=lambda item: (item[0], -item[1])):
if any(start < kept_end and end > kept_start for kept_start, kept_end in ranges):
continue
ranges.append((start, end))
return tuple(ranges)
def _protected_split_offsets(text: str, max_units: int) -> set[int]:
"""Return offsets that would bisect one bounded ASCII/network identifier."""
forbidden: set[int] = set()
for match in _PROTECTED_ASCII_SPAN_RE.finditer(text):
if count_speech_units(match.group(0)) > max_units:
raise ValueError("an indivisible ASCII identifier exceeds the chunk limit")
forbidden.update(range(match.start() + 1, match.end()))
for start, end in _expanded_network_split_ranges(text):
if count_speech_units(text[start:end]) > max_units:
raise ValueError("an indivisible network identifier exceeds the chunk limit")
forbidden.update(range(start + 1, end))
return forbidden
def _structured_numeric_cue_count(text: str) -> int:
"""Count bounded, already-expanded numeric frontend constructions."""
return sum(1 for _ in _NORMALIZED_STRUCTURED_NUMERIC_CUE_RE.finditer(text))
def _should_split_structured_clauses(text: str) -> bool:
return bool(network_protected_spoken_spans(text)) or (
_structured_numeric_cue_count(text) >= 2
)
def _structured_clause_units(text: str, max_units: int) -> list[str]:
"""Split only on real clause punctuation outside protected identifiers."""
forbidden = _protected_split_offsets(text, max_units)
clauses: list[str] = []
start = 0
for index, character in enumerate(text):
end = index + 1
if character not in _STRUCTURED_CLAUSE_BREAKS or end in forbidden:
continue
clause = text[start:end].strip()
if clause:
clauses.append(clause)
start = end
tail = text[start:].strip()
if tail:
clauses.append(tail)
return clauses
def _pack_structured_clauses(
clauses: Sequence[str],
*,
max_units: int,
min_units: int,
target_units: int = _STRUCTURED_CLAUSE_TARGET_UNITS,
) -> list[str]:
"""Pack natural clauses near a soft target while retaining hard bounds."""
maximum = max(1, int(max_units))
minimum = max(1, min(int(min_units), maximum))
target = max(minimum, min(int(target_units), maximum))
atoms: list[str] = []
for clause in clauses:
normalized = normalize_tts_text(clause)
if not normalized:
continue
if count_speech_units(normalized) > maximum:
atoms.extend(_hard_split_text(normalized, maximum, minimum))
else:
atoms.append(normalized)
if not atoms:
return []
# A short opening clause is an onset risk on its own. Merge it forward
# before optimizing the remaining natural boundaries.
while len(atoms) > 1 and count_speech_units(atoms[0]) < minimum:
merged = f"{atoms[0]}{atoms[1]}"
if count_speech_units(merged) > maximum:
return _hard_split_text("".join(atoms), maximum, minimum)
atoms[:2] = [merged]
def build_plan(multi_limit: int) -> tuple[int, int, tuple[int, ...]] | None:
best: list[tuple[int, int, tuple[int, ...]] | None] = [None] * (
len(atoms) + 1
)
best[len(atoms)] = (0, 0, ())
for start in range(len(atoms) - 1, -1, -1):
for end in range(start + 1, len(atoms) + 1):
chunk = "".join(atoms[start:end])
units = count_speech_units(chunk)
if units > maximum:
break
if units < minimum:
continue
if units > multi_limit and end - start > 1:
continue
remainder = best[end]
if remainder is None:
continue
candidate = (
1 + remainder[0],
abs(target - units) + remainder[1],
(end,) + remainder[2],
)
if best[start] is None or candidate[:2] < best[start][:2]:
best[start] = candidate
return best[0]
# First hold multi-clause chunks to the 32-unit target. If an interior or
# trailing short clause makes that impossible, permit at most one
# minimum-sized unit of slack; protected single clauses remain indivisible.
plan = build_plan(target)
if plan is None:
plan = build_plan(min(maximum, target + minimum))
if plan is None:
return _hard_split_text("".join(atoms), maximum, minimum)
output: list[str] = []
start = 0
for end in plan[2]:
output.append("".join(atoms[start:end]))
start = end
return output
def _hard_split_text(text: str, max_chars: int, min_chunk_chars: int) -> list[str]:
"""Deterministically split one overlong unit by speech units, not codepoints."""
remaining = text.strip()
maximum = max(1, int(max_chars))
minimum = max(1, min(int(min_chunk_chars), maximum))
chunks: list[str] = []
while count_speech_units(remaining) > maximum:
forbidden = _protected_split_offsets(remaining, maximum)
candidates: list[tuple[int, int, int]] = []
for offset in range(1, len(remaining)):
if offset in forbidden:
continue
head = remaining[:offset].rstrip()
tail = remaining[offset:].lstrip()
head_units = count_speech_units(head)
tail_units = count_speech_units(tail)
if not minimum <= head_units <= maximum:
continue
if 0 < tail_units < minimum:
continue
semantic = int(
remaining[offset - 1] in _SEMANTIC_SOFT_BREAKS
or remaining[offset].isspace()
)
# Prefer a real soft boundary, then fill the chunk, then choose the
# earliest raw offset for a deterministic tie break.
candidates.append((semantic, head_units, -offset))
if not candidates:
raise ValueError("text cannot be split within the speech-unit limits")
_, _, negative_offset = max(candidates)
cut = -negative_offset
chunk = remaining[:cut].strip()
if not chunk:
raise ValueError("text splitter produced an empty chunk")
chunks.append(chunk)
remaining = remaining[cut:].lstrip()
if remaining:
chunks.append(remaining)
return chunks
def split_text_for_tts(text: str, max_chars: int = 80, min_chunk_chars: int = 12) -> list[str]:
"""Split at semantic boundaries under speech-unit limits.
``max_chars`` and ``min_chunk_chars`` retain their public names for API
compatibility, but both limits are measured with ``count_speech_units``.
This prevents expanded URLs and spaced acronyms from forcing a raw-string
cut through an otherwise short spoken sentence.
"""
text = normalize_tts_text(text)
if not text:
return []
if max_chars <= 0:
return [text]
maximum = max(1, int(max_chars))
minimum = max(1, min(int(min_chunk_chars), maximum))
total_units = count_speech_units(text)
sentence_units = _semantic_sentence_units(text)
# Long multi-sentence requests benefit from keeping their natural endpoint
# cues even when the combined text narrowly fits under the hard maximum.
proactive_sentence_split = (
len(sentence_units) > 1 and total_units > maximum * 0.60
)
proactive_clause_split = any(
_should_split_structured_clauses(unit)
and len(_structured_clause_units(unit, maximum)) > 1
and count_speech_units(unit) > min(
maximum,
_STRUCTURED_CLAUSE_TARGET_UNITS,
)
for unit in sentence_units
)
if (
total_units <= maximum
and not proactive_sentence_split
and not proactive_clause_split
):
return [text] if text else []
chunks: list[str] = []
for unit in sentence_units:
clause_units = _structured_clause_units(unit, maximum)
if (
_should_split_structured_clauses(unit)
and len(clause_units) > 1
and count_speech_units(unit)
> min(maximum, _STRUCTURED_CLAUSE_TARGET_UNITS)
):
pieces = _pack_structured_clauses(
clause_units,
max_units=maximum,
min_units=minimum,
)
else:
pieces = (
_hard_split_text(unit, maximum, minimum)
if count_speech_units(unit) > maximum
else [unit]
)
for piece in pieces:
piece_units = count_speech_units(piece)
if piece_units < minimum and chunks:
merged = f"{chunks[-1]}{piece}"
if count_speech_units(merged) <= maximum:
chunks[-1] = merged
continue
chunks.append(piece)
if len(chunks) > 1 and count_speech_units(chunks[0]) < minimum:
merged = f"{chunks[0]}{chunks[1]}"
if count_speech_units(merged) <= maximum:
chunks[:2] = [merged]
if total_units >= minimum and any(
not minimum <= count_speech_units(chunk) <= maximum
for chunk in chunks
):
if all(
count_speech_units(chunk) <= maximum
and _ends_at_strong_boundary(chunk)
for chunk in chunks
):
return chunks
# Repartition the offending neighborhood without silently publishing a
# sub-minimum fragment. The fallback itself remains semantic-first.
return _hard_split_text(text, maximum, minimum)
return chunks or [text]
def split_quality_text_for_tts(
text: str,
*,
long_max_units: int = 80,
min_chunk_units: int = 12,
medium_max_units: int = 48,
medium_chunk_units: int = 24,
) -> list[str]:
"""Use smaller semantic chunks only for bounded punctuated medium text.
This keeps short single-clause requests and established long-form planning
unchanged. Medium text is repartitioned only when it has an internal
punctuation boundary, so the smaller limit cannot introduce an arbitrary
pause into an otherwise continuous phrase.
"""
normalized = normalize_tts_text(text)
if not normalized:
return []
long_maximum = max(1, int(long_max_units))
minimum = max(1, min(int(min_chunk_units), long_maximum))
medium_maximum = max(minimum, min(int(medium_max_units), long_maximum))
medium_chunk_maximum = max(
minimum,
min(int(medium_chunk_units), medium_maximum),
)
total_units = count_speech_units(normalized)
has_internal_boundary = any(
character in _QUALITY_MEDIUM_BREAKS
for character in normalized[:-1]
)
if not (
medium_chunk_maximum < total_units <= medium_maximum
and has_internal_boundary
):
return split_text_for_tts(
normalized,
max_chars=long_maximum,
min_chunk_chars=minimum,
)
# A smaller medium-text target is useful only when the source already has
# a legal semantic seam. Calling the generic hard splitter with the
# smaller bound can cut a structured number (for example, between
# ``十七`` and ``點二五``) merely to satisfy the size target. Build a
# bounded partition from real punctuation offsets instead and fall back to
# the established long bound when no all-minimum partition exists.
boundaries: set[int] = {len(normalized)}
for index, character in enumerate(normalized):
if character not in _QUALITY_MEDIUM_BREAKS:
continue
end = index + 1
if character in _SEMANTIC_STRONG_BREAKS:
while (
end < len(normalized)
and normalized[end] in _SEMANTIC_TRAILING_CLOSERS
):
end += 1
boundaries.add(end)
ordered = (0, *sorted(boundaries))
# ``best[i]`` stores (chunk_count, target-distance, raw cut offsets) for
# the best punctuation-only partition beginning at ``ordered[i]``.
best: list[tuple[int, int, tuple[int, ...]] | None] = [None] * len(ordered)
best[-1] = (0, 0, ())
for start_index in range(len(ordered) - 2, -1, -1):
start = ordered[start_index]
for end_index in range(start_index + 1, len(ordered)):
end = ordered[end_index]
chunk = normalized[start:end].strip()
units = count_speech_units(chunk)
if units > medium_chunk_maximum:
break
if units < minimum:
continue
remainder = best[end_index]
if remainder is None:
continue
candidate = (
1 + remainder[0],
abs(medium_chunk_maximum - units) + remainder[1],
(end,) + remainder[2],
)
if best[start_index] is None or candidate[:2] < best[start_index][:2]:
best[start_index] = candidate
plan = best[0]
if plan is None or len(plan[2]) < 2:
return split_text_for_tts(
normalized,
max_chars=long_maximum,
min_chunk_chars=minimum,
)
chunks: list[str] = []
start = 0
for end in plan[2]:
chunks.append(normalized[start:end].strip())
start = end
return chunks
@dataclass(frozen=True)
class NetworkFragmentProof:
"""Range-bound local slice of one planner-proven network identifier.
``chunk_start``/``chunk_end`` are offsets in the exact generation chunk.
``parent_start``/``parent_end`` are offsets in ``full_spoken_proof``. The
two slices must be byte-for-byte equal after NFC normalization. Keeping
both coordinate systems prevents a repeated label elsewhere in the chunk
from borrowing a parent URL/email proof.
"""
span_index: int
chunk_start: int
chunk_end: int
parent_start: int
parent_end: int
full_spoken_proof: str
raw_identifier: str = ""
url_rendering_proof: NetworkUrlRenderingProof | None = None
identifier_kind: str = ""
@dataclass(frozen=True)
class GenerationChunkSpec:
"""One generation-only slice with immutable full-identifier provenance."""
text: str
source_start: int
source_end: int
network_span_indices: tuple[int, ...] = ()
network_component_indices: tuple[tuple[int, int], ...] = ()
network_full_spoken_proofs: tuple[str, ...] = ()
network_fragment_proofs: tuple[NetworkFragmentProof, ...] = ()
boundary_after: str = "none"
@property
def network_conditioned(self) -> bool:
return bool(self.network_span_indices)
_EMAIL_DOMAIN_MAIL_TOKEN_RE = re.compile(
r"(?<![A-Za-z])mail(?![A-Za-z])",
flags=re.IGNORECASE,
)
def email_domain_mail_generation_variant(
text: str,
fragment_proofs: Sequence[NetworkFragmentProof],
) -> str:
"""Spell a range-proven email-domain ``mail`` token for generation only.
The returned text is an acoustic candidate target, not a new semantic
target. Replacements are permitted only inside a planner-authenticated
email-domain fragment whose parent proof exactly equals the canonical
renderer image of its raw address. Callers must continue to verify audio
against the unmodified canonical text.
"""
source = str(text)
replacements: set[tuple[int, int]] = set()
for proof in tuple(fragment_proofs):
if not isinstance(proof, NetworkFragmentProof):
raise ValueError("email generation variant requires fragment proofs")
if not (
0 <= proof.chunk_start <= proof.chunk_end <= len(source)
and 0
<= proof.parent_start
<= proof.parent_end
<= len(proof.full_spoken_proof)
):
raise ValueError("email generation fragment proof is out of range")
chunk_fragment = source[proof.chunk_start : proof.chunk_end]
parent_fragment = proof.full_spoken_proof[
proof.parent_start : proof.parent_end
]
if chunk_fragment != parent_fragment:
raise ValueError("email generation fragment proof does not bind to text")
if proof.identifier_kind != "email":
continue
canonical_email = proof.full_spoken_proof
if _spoken_email_subspans(canonical_email) != (canonical_email,):
raise ValueError("email generation parent proof is not canonical")
separator = canonical_email.find(" 小老鼠 ")
if separator < 0:
raise ValueError("email generation parent proof lacks a separator")
domain_start = separator + len(" 小老鼠 ")
for match in _EMAIL_DOMAIN_MAIL_TOKEN_RE.finditer(parent_fragment):
parent_match_start = proof.parent_start + match.start()
if parent_match_start < domain_start:
continue
replacements.add(
(
proof.chunk_start + match.start(),
proof.chunk_start + match.end(),
)
)
output = source
for start, end in sorted(replacements, reverse=True):
output = f"{output[:start]}M A I L{output[end:]}"
return output
@dataclass(frozen=True)
class _NetworkGenerationSpan:
start: int
end: int
spoken_proof: str
kind: str
component_ranges: tuple[tuple[int, int], ...]
mandatory_cut_offsets: tuple[int, ...]
raw_identifier: str = ""
url_rendering_proof: NetworkUrlRenderingProof | None = None
def _raw_network_generation_matches(
text: str,
) -> tuple[
tuple[
int,
int,
str,
str,
str,
NetworkUrlRenderingProof | None,
],
...,
]:
"""Return non-overlapping raw identifiers and their exact spoken proofs."""
raw = unicodedata.normalize("NFC", str(text or ""))
matches: list[
tuple[
int,
int,
str,
str,
str,
NetworkUrlRenderingProof | None,
]
] = []
for match in _SPOKEN_URL_RE.finditer(raw):
value = match.group(0)
if not value.isascii():
raise ValueError("network generation does not support non-ASCII IRI")
rendering_proof = _naturalized_url_rendering_proof(value)
matches.append(
(
match.start(),
match.end(),
_zh_url(value),
"url",
value if rendering_proof is not None else "",
rendering_proof,
)
)
for match in _SPOKEN_EMAIL_RE.finditer(raw):
value = match.group(0)
if not value.isascii():
raise ValueError("network generation does not support non-ASCII email")
matches.append(
(match.start(), match.end(), _zh_email(value), "email", "", None)
)
selected: list[
tuple[
int,
int,
str,
str,
str,
NetworkUrlRenderingProof | None,
]
] = []
for start, end, spoken, kind, raw_identifier, rendering_proof in sorted(
matches,
key=lambda item: (item[0], -item[1]),
):
if any(
start < kept_end and end > kept_start
for kept_start, kept_end, *_ in selected
):
continue
selected.append(
(
start,
end,
normalize_tts_text(spoken),
kind,
raw_identifier,
rendering_proof,
)
)
return tuple(sorted(selected))
def _network_component_ranges(
spoken: str,
*,
absolute_start: int,
hard_max_units: int,
url_rendering_proof: NetworkUrlRenderingProof | None = None,
) -> tuple[tuple[int, int], ...]:
"""Split one proven identifier only at audible grammar delimiters."""
if url_rendering_proof is not None:
if (
inverse_network_url_rendering(url_rendering_proof)
!= url_rendering_proof.raw_identifier
or url_rendering_proof.spoken_text != spoken
):
raise ValueError("naturalized URL component proof is inconsistent")
if count_speech_units(spoken) > hard_max_units:
raise ValueError(
"an indivisible naturalized URL exceeds the generation limit"
)
return ((absolute_start, absolute_start + len(spoken)),)
token_matches = tuple(re.finditer(r"\S+", spoken))
tokens = tuple(
_SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(match.group(0), match.group(0))
for match in token_matches
)
cuts: set[int] = {0, len(spoken)}
scheme_slashes: set[int] = set()
for index in range(2, len(tokens)):
if tokens[index - 2 : index + 1] == ("冒號", "斜線", "斜線"):
scheme_slashes.update({index - 1, index})
cuts.add(token_matches[index].end())
for index, (token, match) in enumerate(zip(tokens, token_matches, strict=True)):
if token in _NETWORK_GENERATION_COMPONENT_AFTER:
cuts.add(match.end())
if (
token in _NETWORK_GENERATION_COMPONENT_BEFORE
and index not in scheme_slashes
and not (token == "冒號" and index + 2 < len(tokens) and tokens[index + 1 : index + 3] == ("斜線", "斜線"))
):
cuts.add(match.start())
ordered = sorted(cuts)
ranges: list[tuple[int, int]] = []
for left, right in zip(ordered, ordered[1:]):
component = normalize_tts_text(spoken[left:right])
if not component:
continue
if count_speech_units(component) > hard_max_units:
raise ValueError("an indivisible network component exceeds the generation limit")
ranges.append((absolute_start + left, absolute_start + right))
if not ranges:
raise ValueError("network generation proof has no components")
if ranges[0][0] != absolute_start or ranges[-1][1] != absolute_start + len(spoken):
raise ValueError("network generation components do not cover their proof")
return tuple(ranges)
def _network_mandatory_cut_offsets(
spoken: str,
*,
kind: str,
absolute_start: int,
url_rendering_proof: NetworkUrlRenderingProof | None = None,
) -> tuple[int, ...]:
"""Return grammar boundaries that no generation chunk may cross.
Hybrid lexical labels are substantially easier for the model than spelling
every letter, but a whole email/URL still asks one trajectory to preserve a
long opaque identifier. Split email local/domain parts and URL
scheme/remainder parts independently so coverage selection can combine
exact fragments without relaxing whole-output verification.
"""
if url_rendering_proof is not None:
if kind != "url" or url_rendering_proof.spoken_text != spoken:
raise ValueError("naturalized URL mandatory-cut proof is inconsistent")
return ()
token_matches = tuple(re.finditer(r"\S+", spoken))
tokens = tuple(
_SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(match.group(0), match.group(0))
for match in token_matches
)
offsets: list[int] = []
if kind == "email":
separators = [
match.start()
for token, match in zip(tokens, token_matches, strict=True)
if token == "小老鼠"
]
if len(separators) != 1:
raise ValueError("email generation proof lacks one separator")
offsets.append(separators[0])
elif kind == "url":
scheme_ends = [
token_matches[index].end()
for index in range(2, len(tokens))
if tokens[index - 2 : index + 1] == ("冒號", "斜線", "斜線")
]
if len(scheme_ends) > 1:
raise ValueError("URL generation proof has ambiguous scheme separators")
offsets.extend(scheme_ends)
else:
raise ValueError("network generation proof has an invalid kind")
return tuple(absolute_start + offset for offset in offsets)
def _network_generation_spans(
raw_text: str,
normalized_text: str,
*,
hard_max_units: int,
) -> tuple[_NetworkGenerationSpan, ...]:
"""Bind raw ASCII grammar to exact offsets in the normalized model text."""
normalized = normalize_tts_text(normalized_text)
expected = normalize_spoken_forms(raw_text)
if normalized != expected:
raise ValueError("normalized text does not match the raw network request")
raw = unicodedata.normalize("NFC", str(raw_text or ""))
raw_matches = _raw_network_generation_matches(raw)
spans: list[_NetworkGenerationSpan] = []
previous_raw_end: int | None = None
previous_kind: str | None = None
cursor = 0
for (
raw_start,
raw_end,
spoken,
kind,
raw_identifier,
url_rendering_proof,
) in raw_matches:
# Derive the offset from the exact raw prefix rather than searching for
# a possibly duplicated spoken phrase. The frontend inserts a comma
# on each unpunctuated side of a network identifier; a preceding
# identifier at the end of the prefix needs its deferred suffix comma
# accounted for as well.
normalized_prefix = normalize_spoken_forms(raw[:raw_start])
boundary_count = 0
preceding = raw[:raw_start].rstrip()[-1:]
if preceding and preceding not in _NETWORK_BOUNDARY_PUNCTUATION:
boundary_count += 1
if (
previous_raw_end is not None
and previous_kind == kind
and not raw[previous_raw_end:raw_start].strip()
):
following = raw[previous_raw_end:].lstrip()[:1]
if following and following not in _NETWORK_BOUNDARY_PUNCTUATION:
boundary_count += 1
start = len(normalized_prefix) + boundary_count
if start < cursor or normalized[start : start + len(spoken)] != spoken:
raise ValueError(
"network spoken proof offset does not match normalized text"
)
end = start + len(spoken)
components = _network_component_ranges(
spoken,
absolute_start=start,
hard_max_units=hard_max_units,
url_rendering_proof=url_rendering_proof,
)
mandatory_cuts = _network_mandatory_cut_offsets(
spoken,
kind=kind,
absolute_start=start,
url_rendering_proof=url_rendering_proof,
)
component_boundaries = {
boundary
for component in components
for boundary in component
}
if any(
cut <= start or cut >= end or cut not in component_boundaries
for cut in mandatory_cuts
):
raise ValueError("mandatory network cut lacks component provenance")
spans.append(
_NetworkGenerationSpan(
start=start,
end=end,
spoken_proof=spoken,
kind=kind,
component_ranges=components,
mandatory_cut_offsets=mandatory_cuts,
raw_identifier=raw_identifier,
url_rendering_proof=url_rendering_proof,
)
)
cursor = end
previous_raw_end = raw_end
previous_kind = kind
return tuple(spans)
def _network_generation_group_plan(
text: str,
*,
group_start: int,
group_end: int,
original_boundaries: Sequence[int],
spans: Sequence[_NetworkGenerationSpan],
minimum: int,
network_minimum: int,
target: int,
network_maximum: int,
ordinary_maximum: int,
split_atomic_email_at_separator: bool,
) -> tuple[tuple[int, int], ...]:
"""Deterministically pack one network-bearing sentence near the target."""
group_spans = tuple(
span for span in spans if span.start < group_end and span.end > group_start
)
if not group_spans:
return tuple(
zip(
(group_start, *original_boundaries),
(*original_boundaries, group_end),
strict=True,
)
)
preferred_cuts: set[int] = {
group_start,
group_end,
*original_boundaries,
}
component_cuts = {
offset
for span in group_spans
for component in span.component_ranges
for offset in component
}
atomic_maximum = min(network_maximum, _NETWORK_ATOMIC_MAX_UNITS)
atomic_spans = tuple(
span
for span in group_spans
if count_speech_units(span.spoken_proof) <= atomic_maximum
)
splittable_spans = tuple(
span
for span in group_spans
if count_speech_units(span.spoken_proof) > atomic_maximum
)
splittable_component_cuts = {
offset
for span in splittable_spans
for component in span.component_ranges
for offset in component
}
atomic_email_separator_cuts: set[int] = set()
if split_atomic_email_at_separator and len(group_spans) == 1:
span = group_spans[0]
if (
span in atomic_spans
and span.kind == "email"
and len(span.mandatory_cut_offsets) == 1
):
separator = span.mandatory_cut_offsets[0]
left_units = count_speech_units(text[group_start:separator])
right_units = count_speech_units(text[separator:group_end])
prose_units = count_speech_units(text[group_start:span.start])
domain_units = count_speech_units(text[separator:span.end])
if (
separator in component_cuts
and prose_units >= _NETWORK_CONTEXTUAL_EMAIL_MIN_PROSE_UNITS
and domain_units >= network_minimum
and network_minimum <= left_units <= network_maximum
and network_minimum <= right_units <= network_maximum
):
atomic_email_separator_cuts.add(separator)
legal_network_component_cuts = (
splittable_component_cuts | atomic_email_separator_cuts
)
preferred_cuts.update(legal_network_component_cuts)
mandatory_cuts = {
offset
for span in splittable_spans
for offset in span.mandatory_cut_offsets
} | atomic_email_separator_cuts
if not mandatory_cuts.issubset(component_cuts):
raise ValueError("mandatory network cuts lack component provenance")
atomic_url_precomma_cuts = {
span.end
for span in atomic_spans
if (
span.kind == "url"
and span.end < group_end
and text[span.end] in {",", ","}
)
}
preferred_cuts.difference_update(atomic_url_precomma_cuts)
network_interiors = {
offset
for span in group_spans
for offset in range(span.start + 1, span.end)
}
for index in range(group_start, group_end):
end = index + 1
if text[index] in _STRUCTURED_CLAUSE_BREAKS and end not in network_interiors:
preferred_cuts.add(end)
# Every source offset outside an identifier remains an emergency prose
# split. Inside an identifier, only exact component boundaries are legal.
cuts = {
offset
for offset in range(group_start, group_end + 1)
if (
offset not in atomic_url_precomma_cuts
and (
offset not in network_interiors
or offset in legal_network_component_cuts
)
)
}
ordered = sorted(offset for offset in cuts if group_start <= offset <= group_end)
network_span_starts = {span.start for span in group_spans}
def solve(
active_mandatory_cuts: set[int],
) -> tuple[int, int, int, int, int, tuple[int, ...]] | None:
best: dict[
int,
tuple[int, int, int, int, int, tuple[int, ...]],
] = {
group_end: (0, 0, 0, 0, 0, ())
}
for start in reversed(ordered[:-1]):
selected: (
tuple[int, int, int, int, int, tuple[int, ...]] | None
) = None
for end in ordered:
if end <= start:
continue
if any(start < cut < end for cut in active_mandatory_cuts):
continue
chunk = text[start:end]
units = count_speech_units(chunk)
network_conditioned = any(
span.start < end and span.end > start for span in group_spans
)
maximum = (
network_maximum if network_conditioned else ordinary_maximum
)
effective_minimum = (
network_minimum if network_conditioned else minimum
)
if units > maximum:
continue
if units < effective_minimum and not (
start == group_start and end == group_end
):
continue
remainder = best.get(end)
if remainder is None:
continue
candidate = (
1 + remainder[0],
int(end != group_end and end not in preferred_cuts)
+ remainder[1],
int(start in network_span_starts) + remainder[2],
max(units, remainder[3]),
abs(target - units) + remainder[4],
(end,) + remainder[5],
)
if selected is None or candidate < selected:
selected = candidate
if selected is not None:
best[start] = selected
return best.get(group_start)
plan = solve(mandatory_cuts)
if plan is None:
# A tiny common identifier such as ``https://a.tw`` cannot place both
# sides of the preferred scheme cut above the network minimum. Retry
# only after proving which identifier-local side is intrinsically short;
# all other mandatory cuts and every grammar component boundary remain.
short_identifier_cuts = {
cut
for span in group_spans
for cut in span.mandatory_cut_offsets
if count_speech_units(text[span.start:cut]) < network_minimum
or count_speech_units(text[cut:span.end]) < network_minimum
}
if short_identifier_cuts:
plan = solve(mandatory_cuts - short_identifier_cuts)
if plan is None:
raise ValueError("network-bearing sentence cannot satisfy generation limits")
output: list[tuple[int, int]] = []
start = group_start
for end in plan[5]:
output.append((start, end))
start = end
return tuple(output)
def _semantic_sentence_ranges(text: str) -> tuple[tuple[int, int], ...]:
"""Return exact, contiguous strong-sentence source ranges."""
if not text:
return ()
ranges: list[tuple[int, int]] = []
start = 0
index = 0
while index < len(text):
if text[index] not in _SEMANTIC_STRONG_BREAKS:
index += 1
continue
end = index + 1
while end < len(text) and text[end] in _SEMANTIC_TRAILING_CLOSERS:
end += 1
while end < len(text) and text[end].isspace():
end += 1
if count_speech_units(text[start:end]) > 0:
ranges.append((start, end))
start = end
index = end
if start < len(text) and count_speech_units(text[start:]) > 0:
ranges.append((start, len(text)))
if not ranges or ranges[0][0] != 0 or ranges[-1][1] != len(text):
raise ValueError("semantic source ranges do not cover normalized text")
if any(
left_end != right_start
for (_, left_end), (right_start, _) in zip(ranges, ranges[1:])
):
raise ValueError("semantic source ranges are not contiguous")
return tuple(ranges)
def _ordinary_generation_ranges(
text: str,
*,
start: int,
end: int,
minimum: int,
maximum: int,
) -> tuple[tuple[int, int], ...]:
"""Hard-split one exact non-network source range without re-normalizing it."""
source = text[start:end]
semantic_chunks = split_text_for_tts(
source,
max_chars=maximum,
min_chunk_chars=minimum,
)
if semantic_chunks and "".join(semantic_chunks) == source:
ranges: list[tuple[int, int]] = []
cursor = start
for chunk in semantic_chunks:
next_cursor = cursor + len(chunk)
ranges.append((cursor, next_cursor))
cursor = next_cursor
if cursor == end:
return tuple(ranges)
if count_speech_units(source) <= maximum:
return ((start, end),)
output: list[tuple[int, int]] = []
cursor = start
while count_speech_units(text[cursor:end]) > maximum:
local = text[cursor:end]
forbidden = _protected_split_offsets(local, maximum)
candidates: list[tuple[int, int, int]] = []
for local_offset in range(1, len(local)):
if local_offset in forbidden:
continue
head_units = count_speech_units(local[:local_offset])
tail_units = count_speech_units(local[local_offset:])
if not minimum <= head_units <= maximum:
continue
if 0 < tail_units < minimum:
continue
semantic = int(
local[local_offset - 1] in _SEMANTIC_SOFT_BREAKS
or local[local_offset].isspace()
)
candidates.append((semantic, head_units, -local_offset))
if not candidates:
raise ValueError("ordinary generation range cannot satisfy chunk limits")
_, _, negative_offset = max(candidates)
next_cursor = cursor - negative_offset
output.append((cursor, next_cursor))
cursor = next_cursor
output.append((cursor, end))
return tuple(output)
def plan_generation_chunks(
raw_text: str,
normalized_text: str,
*,
min_units: int = 12,
network_min_units: int = 8,
target_units: int = 32,
network_max_units: int = 36,
ordinary_max_units: int = 80,
split_atomic_email_at_separator: bool = False,
) -> tuple[GenerationChunkSpec, ...]:
"""Plan model chunks while retaining exact whole-network verification proof.
Ordinary semantic chunking is unchanged. Only sentences that contain a raw
ASCII URL/email are repacked at proven audible component delimiters. When
``split_atomic_email_at_separator`` is enabled, an otherwise bounded email
is split only at its range-proven ``@``/``小老鼠`` grammar boundary when
both generation rows can satisfy the network minimum. The caller must
continue to verify the untouched ``normalized_text`` as one whole target
before returning audio.
"""
if network_identifier_has_ambiguous_iri(raw_text):
raise ValueError("network generation does not support non-ASCII IRI")
minimum = max(1, int(min_units))
network_minimum = max(1, int(network_min_units))
target = max(minimum, int(target_units))
network_maximum = max(minimum, int(network_max_units))
ordinary_maximum = max(minimum, int(ordinary_max_units))
if not isinstance(split_atomic_email_at_separator, bool):
raise ValueError("atomic email split policy must be boolean")
if not (
network_minimum
<= minimum
<= target
<= network_maximum
<= ordinary_maximum
):
raise ValueError("generation chunk limits are inconsistent")
normalized = normalize_tts_text(normalized_text)
if not contains_network_identifier(raw_text):
ordinary = split_text_for_tts(
normalized,
max_chars=ordinary_maximum,
min_chunk_chars=minimum,
)
specs: list[GenerationChunkSpec] = []
cursor = 0
for index, chunk in enumerate(ordinary):
start = normalized.find(chunk, cursor)
if start < 0:
raise ValueError("ordinary chunk is absent from normalized text")
end = start + len(chunk)
specs.append(
GenerationChunkSpec(
text=chunk,
source_start=start,
source_end=end,
boundary_after=("semantic" if index + 1 < len(ordinary) else "none"),
)
)
cursor = end
return tuple(specs)
spans = _network_generation_spans(
raw_text,
normalized,
hard_max_units=network_maximum,
)
if not spans:
raise ValueError("network request has no bound spoken proof")
planned_ranges: list[tuple[int, int]] = []
for sentence_start, sentence_end in _semantic_sentence_ranges(normalized):
sentence_has_network = any(
span.start < sentence_end and span.end > sentence_start
for span in spans
)
if sentence_has_network:
planned_ranges.extend(
_network_generation_group_plan(
normalized,
group_start=sentence_start,
group_end=sentence_end,
original_boundaries=(),
spans=spans,
minimum=minimum,
network_minimum=network_minimum,
target=target,
network_maximum=network_maximum,
ordinary_maximum=ordinary_maximum,
split_atomic_email_at_separator=(
split_atomic_email_at_separator
),
)
)
else:
planned_ranges.extend(
_ordinary_generation_ranges(
normalized,
start=sentence_start,
end=sentence_end,
minimum=minimum,
maximum=ordinary_maximum,
)
)
if (
not planned_ranges
or planned_ranges[0][0] != 0
or planned_ranges[-1][1] != len(normalized)
or any(
left_end != right_start
for (_, left_end), (right_start, _) in zip(
planned_ranges,
planned_ranges[1:],
)
)
):
raise ValueError("generation source ranges do not exactly cover the target")
specs = []
internal_boundaries = {
right
for span in spans
for _, right in span.component_ranges[:-1]
}
for start, end in planned_ranges:
chunk = normalized[start:end]
if not chunk or count_speech_units(chunk) <= 0:
raise ValueError("generation planner produced an empty chunk")
overlapping = tuple(
span_index
for span_index, span in enumerate(spans)
if span.start < end and span.end > start
)
component_indices = tuple(
(span_index, component_index)
for span_index in overlapping
for component_index, (left, right) in enumerate(
spans[span_index].component_ranges
)
if left < end and right > start
)
fragment_proofs = tuple(
NetworkFragmentProof(
span_index=span_index,
chunk_start=max(start, spans[span_index].start) - start,
chunk_end=min(end, spans[span_index].end) - start,
parent_start=max(start, spans[span_index].start)
- spans[span_index].start,
parent_end=min(end, spans[span_index].end)
- spans[span_index].start,
full_spoken_proof=spans[span_index].spoken_proof,
raw_identifier=spans[span_index].raw_identifier,
url_rendering_proof=spans[span_index].url_rendering_proof,
identifier_kind=spans[span_index].kind,
)
for span_index in overlapping
)
specs.append(
GenerationChunkSpec(
text=chunk,
source_start=start,
source_end=end,
network_span_indices=overlapping,
network_component_indices=component_indices,
network_full_spoken_proofs=tuple(
spans[span_index].spoken_proof for span_index in overlapping
),
network_fragment_proofs=fragment_proofs,
boundary_after=(
"network_internal"
if end in internal_boundaries
else ("semantic" if end < len(normalized) else "none")
),
)
)
if "".join(spec.text for spec in specs) != normalized:
raise ValueError("generation chunks do not reconstruct the normalized target")
if any(
count_speech_units(spec.text)
> (network_maximum if spec.network_conditioned else ordinary_maximum)
for spec in specs
):
raise ValueError("generation chunk exceeds its provenance-specific limit")
for spec in specs:
proofs = spec.network_fragment_proofs
if spec.network_conditioned:
if not (
len(proofs)
== len(spec.network_span_indices)
== len(spec.network_full_spoken_proofs)
):
raise ValueError("network fragment provenance is incomplete")
elif proofs or spec.network_full_spoken_proofs:
raise ValueError("ordinary chunk carries network fragment provenance")
previous_chunk_end = 0
for span_index, full_proof, proof in zip(
spec.network_span_indices,
spec.network_full_spoken_proofs,
proofs,
strict=True,
):
if (
proof.span_index != span_index
or proof.full_spoken_proof != full_proof
or not 0 <= proof.chunk_start < proof.chunk_end <= len(spec.text)
or not 0 <= proof.parent_start < proof.parent_end <= len(full_proof)
or proof.chunk_start < previous_chunk_end
or spec.text[proof.chunk_start : proof.chunk_end]
!= full_proof[proof.parent_start : proof.parent_end]
):
raise ValueError("network fragment provenance is inconsistent")
spoken_rendering = _naturalized_url_proof_from_spoken(full_proof)
if spoken_rendering is not None:
fresh_rendering = _naturalized_url_rendering_proof(
proof.raw_identifier
)
if (
not proof.raw_identifier
or fresh_rendering is None
or spoken_rendering != fresh_rendering
or proof.url_rendering_proof != fresh_rendering
or inverse_network_url_rendering(proof.url_rendering_proof)
!= proof.raw_identifier
or proof.url_rendering_proof.spoken_text != full_proof
or proof.parent_start != 0
or proof.parent_end != len(full_proof)
):
raise ValueError(
"naturalized URL fragment provenance is inconsistent"
)
elif proof.raw_identifier or proof.url_rendering_proof is not None:
raise ValueError(
"non-naturalized network proof carries URL rendering provenance"
)
previous_chunk_end = proof.chunk_end
# The local fragments are not merely hints: taken across the plan they must
# form an exact, non-overlapping partition of every parent identifier.
for span_index, span in enumerate(spans):
partition = sorted(
(
proof.parent_start,
proof.parent_end,
proof.full_spoken_proof,
)
for spec in specs
for proof in spec.network_fragment_proofs
if proof.span_index == span_index
)
if (
not partition
or partition[0][0] != 0
or partition[-1][1] != len(span.spoken_proof)
or any(
left_end != right_start
for (_, left_end, _), (right_start, _, _) in zip(
partition,
partition[1:],
)
)
or any(full_proof != span.spoken_proof for _, _, full_proof in partition)
):
raise ValueError("network fragment provenance does not partition its parent")
return tuple(specs)
def coalesce_text_chunks(
chunks: Sequence[str],
*,
max_chunks: int,
max_units: int,
) -> list[str]:
"""Merge the smallest adjacent pairs until a runtime chunk cap is met."""
if isinstance(max_chunks, bool) or isinstance(max_units, bool):
raise ValueError("chunk limits must be positive integers")
limit = int(max_chunks)
maximum = int(max_units)
if limit <= 0 or maximum <= 0:
raise ValueError("chunk limits must be positive integers")
output = [normalize_tts_text(chunk) for chunk in chunks]
if not output or any(not chunk for chunk in output):
raise ValueError("chunks must be non-empty")
while len(output) > limit:
candidates: list[tuple[int, int]] = []
for index in range(len(output) - 1):
merged = f"{output[index]}{output[index + 1]}"
units = count_speech_units(merged)
if units <= maximum:
candidates.append((units, index))
if not candidates:
raise ValueError("text cannot fit within the generated-chunk budget")
_, index = min(candidates)
output[index : index + 2] = [f"{output[index]}{output[index + 1]}"]
return output
def split_leading_clause(
text: str,
*,
search_chars: int = 40,
min_chunk_chars: int = 12,
) -> list[str]:
"""Split the onset only at a real punctuation boundary.
A hard onset split gives the stop head a text fragment with no endpoint
cue. That can turn the artificial chunk tail into extra speech. Leave the
utterance intact when no suitable punctuation exists.
"""
normalized = normalize_tts_text(text)
if not normalized:
return []
minimum = max(1, int(min_chunk_chars))
upper = min(max(0, int(search_chars)), len(normalized) - minimum)
if upper < minimum:
return [normalized]
for index, char in enumerate(normalized[:upper], start=1):
if index >= minimum and char in ",,、;;::。!?!?":
return [normalized[:index], normalized[index:]]
return [normalized]
def punctuation_pause_seconds(text: str, fallback: float = 0.25) -> float:
stripped = text.rstrip()
if not stripped:
return max(0.0, float(fallback))
if stripped[-1] in ",,、":
return 0.15
if stripped[-1] in ";;::":
return 0.23
if stripped[-1] in "。!?.!?":
return 0.35
return max(0.0, float(fallback))
def _peak_rms(audio: np.ndarray) -> tuple[float, float]:
audio = np.asarray(audio, dtype=np.float32).reshape(-1)
peak = float(np.max(np.abs(audio))) if audio.size else 0.0
rms = float(np.sqrt(np.mean(np.square(audio, dtype=np.float64)))) if audio.size else 0.0
return peak, rms
def encode_pcm16(audio: np.ndarray) -> np.ndarray:
"""Encode finite mono audio with the standard signed PCM16 scale."""
values = np.asarray(audio)
if values.ndim != 1 or values.size <= 0 or not np.issubdtype(
values.dtype,
np.floating,
):
raise ValueError("audio must be a non-empty mono floating-point array")
finite = values.astype(np.float64, copy=False)
if not np.isfinite(finite).all():
raise ValueError("audio must contain only finite samples")
if float(np.max(np.abs(finite))) > 1.0:
raise ValueError("audio samples must be bounded to [-1, 1]")
scaled = np.rint(finite * 32768.0)
return np.clip(scaled, -32768.0, 32767.0).astype(np.int16)
def decode_pcm16(pcm: np.ndarray) -> np.ndarray:
"""Decode one mono PCM16 buffer exactly as a WAV reader does."""
values = np.asarray(pcm)
if values.ndim != 1 or values.size <= 0 or values.dtype != np.int16:
raise ValueError("pcm must be a non-empty mono int16 array")
return values.astype(np.float32) / np.float32(32768.0)
def pcm16_verification_waveform(audio: np.ndarray) -> np.ndarray:
"""Return the decoder-equivalent waveform for the exact public PCM codes."""
return decode_pcm16(encode_pcm16(audio))
def pcm16_audio_output(
sample_rate: int,
audio: np.ndarray,
) -> tuple[int, np.ndarray]:
"""Serialize verified mono float audio as PCM16 without peak normalization.
Gradio peak-normalizes floating-point ndarray outputs before writing them.
Returning explicit PCM16 preserves the level already selected by the
production loudness/peak limiter, including its intended noise headroom.
Callers should verify ``pcm16_verification_waveform(audio)``; encoding that
canonical waveform here is sample-for-sample idempotent.
"""
if isinstance(sample_rate, (bool, np.bool_)):
raise ValueError("sample_rate must be a positive integer")
try:
rate_hz = operator.index(sample_rate)
except (TypeError, ValueError, OverflowError) as error:
raise ValueError("sample_rate must be a positive integer") from error
if rate_hz <= 0:
raise ValueError("sample_rate must be a positive integer")
return int(rate_hz), encode_pcm16(audio)
def match_chunk_rms(reference: np.ndarray, chunk: np.ndarray, max_adjust_db: float = 4.0) -> np.ndarray:
output = np.asarray(chunk, dtype=np.float32).reshape(-1).copy()
reference_peak, reference_rms = _peak_rms(reference)
chunk_peak, chunk_rms = _peak_rms(output)
del reference_peak
if output.size == 0 or reference_rms <= 0.0 or chunk_rms <= 0.0:
return output
bound = 10.0 ** (max(0.0, float(max_adjust_db)) / 20.0)
gain = float(np.clip(reference_rms / chunk_rms, 1.0 / bound, bound))
if chunk_peak > 0.0:
gain = min(gain, 0.95 / chunk_peak)
output *= gain
return output
def fade_internal_edges(chunks: list[np.ndarray], sample_rate: int, fade_ms: float = 80.0) -> list[np.ndarray]:
outputs = [np.asarray(chunk, dtype=np.float32).reshape(-1).copy() for chunk in chunks]
requested = max(0, int(round(float(fade_ms) * sample_rate / 1000.0)))
if requested <= 0 or len(outputs) < 2:
return outputs
for index, output in enumerate(outputs):
count = min(requested, output.size)
if index > 0:
output[:count] *= np.linspace(0.0, 1.0, count, endpoint=True, dtype=np.float32)
if index + 1 < len(outputs):
output[-count:] *= np.linspace(1.0, 0.0, count, endpoint=True, dtype=np.float32)
return outputs
def fade_variable_internal_edges(
chunks: Sequence[np.ndarray],
sample_rate: int,
fade_ms_by_boundary: Sequence[float],
) -> list[np.ndarray]:
"""Apply independently bounded fades at each adjacent chunk boundary."""
outputs = [np.asarray(chunk, dtype=np.float32).reshape(-1).copy() for chunk in chunks]
if len(fade_ms_by_boundary) != max(0, len(outputs) - 1):
raise ValueError("fade boundary count must equal chunk count minus one")
if sample_rate <= 0:
raise ValueError("sample_rate must be positive")
if any(output.size <= 0 for output in outputs):
raise ValueError("audio chunks must be non-empty")
widths: list[int] = []
for index, raw_fade_ms in enumerate(fade_ms_by_boundary):
try:
fade_ms = float(raw_fade_ms)
except (TypeError, ValueError, OverflowError) as error:
raise ValueError("fade duration must be finite and non-negative") from error
if not math.isfinite(fade_ms) or fade_ms < 0.0:
raise ValueError("fade duration must be finite and non-negative")
requested = int(round(fade_ms * sample_rate / 1000.0))
widths.append(
min(requested, outputs[index].size, outputs[index + 1].size)
)
if any(
widths[index - 1] + widths[index] > outputs[index].size
for index in range(1, len(outputs) - 1)
):
raise ValueError("adjacent fades overlap inside an audio chunk")
for index, count in enumerate(widths):
if count <= 0:
continue
outputs[index][-count:] *= np.linspace(
1.0,
0.0,
count,
endpoint=True,
dtype=np.float32,
)
outputs[index + 1][:count] *= np.linspace(
0.0,
1.0,
count,
endpoint=True,
dtype=np.float32,
)
return outputs
def join_audio_chunks(
chunks: list[np.ndarray],
pauses: list[int],
crossfade_samples: int = 0,
*,
pre_faded_edges: bool = False,
) -> np.ndarray:
if not chunks:
return np.zeros(0, dtype=np.float32)
output = np.asarray(chunks[0], dtype=np.float32).copy()
for index, chunk in enumerate(chunks[1:]):
next_chunk = np.asarray(chunk, dtype=np.float32).copy()
pause = max(0, int(pauses[index] if index < len(pauses) else 0))
crossfade = min(max(0, int(crossfade_samples)), output.size, next_chunk.size)
if pause > 0:
if crossfade > 0 and not pre_faded_edges:
output[-crossfade:] *= np.linspace(1.0, 0.0, crossfade, dtype=np.float32)
next_chunk[:crossfade] *= np.linspace(0.0, 1.0, crossfade, dtype=np.float32)
output = np.concatenate((output, np.zeros(pause, dtype=np.float32), next_chunk))
elif crossfade > 0:
if pre_faded_edges:
overlap = output[-crossfade:] + next_chunk[:crossfade]
else:
fade_out = np.linspace(1.0, 0.0, crossfade, endpoint=False, dtype=np.float32)
overlap = output[-crossfade:] * fade_out + next_chunk[:crossfade] * (1.0 - fade_out)
output = np.concatenate((output[:-crossfade], overlap, next_chunk[crossfade:]))
else:
output = np.concatenate((output, next_chunk))
return output.astype(np.float32, copy=False)
def join_audio_chunks_variable(
chunks: Sequence[np.ndarray],
pauses: Sequence[int],
crossfade_samples_by_boundary: Sequence[int],
*,
pre_faded_edges: bool = False,
) -> np.ndarray:
"""Join chunks with one validated crossfade width per boundary."""
if not chunks:
if pauses or crossfade_samples_by_boundary:
raise ValueError("empty chunks cannot carry boundary metadata")
return np.zeros(0, dtype=np.float32)
boundary_count = len(chunks) - 1
if len(pauses) != boundary_count or len(crossfade_samples_by_boundary) != boundary_count:
raise ValueError("pause/crossfade boundary counts must equal chunk count minus one")
prepared = [
np.asarray(chunk, dtype=np.float32).reshape(-1).copy()
for chunk in chunks
]
if any(chunk.size <= 0 for chunk in prepared):
raise ValueError("audio chunks must be non-empty")
validated_pauses: list[int] = []
requested_crossfades: list[int] = []
for raw_pause, raw_crossfade in zip(
pauses,
crossfade_samples_by_boundary,
strict=True,
):
if isinstance(raw_pause, (bool, np.bool_)) or isinstance(
raw_crossfade,
(bool, np.bool_),
):
raise ValueError("pause and crossfade widths must be non-negative integers")
try:
pause = operator.index(raw_pause)
crossfade = operator.index(raw_crossfade)
except (TypeError, ValueError, OverflowError) as error:
raise ValueError(
"pause and crossfade widths must be non-negative integers"
) from error
if pause < 0 or crossfade < 0:
raise ValueError("pause and crossfade widths must be non-negative integers")
validated_pauses.append(int(pause))
requested_crossfades.append(int(crossfade))
widths = [
min(requested, prepared[index].size, prepared[index + 1].size)
for index, requested in enumerate(requested_crossfades)
]
if any(
widths[index - 1] + widths[index] > prepared[index].size
for index in range(1, len(prepared) - 1)
):
raise ValueError("adjacent crossfades overlap inside an audio chunk")
output = prepared[0]
for index, next_chunk in enumerate(prepared[1:]):
pause = validated_pauses[index]
crossfade = widths[index]
if pause > 0:
if crossfade > 0 and not pre_faded_edges:
output[-crossfade:] *= np.linspace(
1.0,
0.0,
crossfade,
dtype=np.float32,
)
next_chunk[:crossfade] *= np.linspace(
0.0,
1.0,
crossfade,
dtype=np.float32,
)
output = np.concatenate(
(output, np.zeros(pause, dtype=np.float32), next_chunk)
)
elif crossfade > 0:
if pre_faded_edges:
overlap = output[-crossfade:] + next_chunk[:crossfade]
else:
fade_out = np.linspace(
1.0,
0.0,
crossfade,
endpoint=False,
dtype=np.float32,
)
overlap = (
output[-crossfade:] * fade_out
+ next_chunk[:crossfade] * (1.0 - fade_out)
)
output = np.concatenate(
(output[:-crossfade], overlap, next_chunk[crossfade:])
)
else:
output = np.concatenate((output, next_chunk))
return output.astype(np.float32, copy=False)
def apply_loudness_floor(
audio: np.ndarray,
min_rms: float = 0.07,
peak_limit: float = 0.95,
max_gain: float = 3.0,
) -> np.ndarray:
output = np.asarray(audio, dtype=np.float32).reshape(-1).copy()
peak, rms = _peak_rms(output)
if min_rms > 0.0 and 0.0 < rms < min_rms:
gain = min(float(max_gain), float(min_rms) / rms)
output *= gain
peak *= gain
if peak_limit > 0.0 and peak > peak_limit:
output *= float(peak_limit) / peak
return output
def _comparison_text(
text: str,
*,
locale: str,
target_text: str | None = None,
preserve_network_boundaries: bool = False,
) -> str:
spoken = (
normalize_asr_spoken_forms(text, target_text, locale=locale)
if target_text is not None
else normalize_spoken_forms(text, locale=locale)
)
proof_text = target_text if target_text is not None else text
proof_network_spans = network_protected_spoken_spans(proof_text)
if proof_network_spans:
# The two commas emitted by the naturalized URL renderer are prosodic,
# not lexical URL atoms. Remove only those two expected cue separators
# before boundary provenance is encoded, so Whisper may omit them while
# an extra punctuation mark inside the host/path remains a visible edit.
if preserve_network_boundaries and any(
_naturalized_url_proof_from_spoken(span) is not None
for span in proof_network_spans
):
spoken = _strip_naturalized_url_prosodic_commas(spoken)
def network_symbol_reading(character: str) -> str:
compatibility_character = unicodedata.normalize("NFKC", character)
reading = (
_ZH_NETWORK_SYMBOL_READINGS.get(character)
or (
_ZH_NETWORK_SYMBOL_READINGS.get(compatibility_character)
if len(compatibility_character) == 1
else None
)
)
return reading or f"符號{_zh_digit_sequence(str(ord(character)))}"
def network_digit_key(character: str) -> str | None:
if character in _ZH_DIGITS:
return str(_ZH_DIGITS.index(character))
try:
return str(unicodedata.decimal(character))
except (TypeError, ValueError):
return None
def network_boundary_key(value: str) -> str:
canonical = normalize_tts_eval_text(value).casefold()
for reading, letter in sorted(
_ZH_NETWORK_READING_TO_LETTER.items(),
key=lambda item: len(item[0]),
reverse=True,
):
canonical = canonical.replace(reading, letter)
canonical = _T2S_CONVERTER.convert(canonical)
output: list[str] = []
for character in canonical:
digit_key = network_digit_key(character)
if digit_key is not None:
output.append(digit_key)
elif character.isalnum():
output.append(character)
return "".join(output)
proof_network_keys = tuple(
network_boundary_key(span) for span in proof_network_spans
)
def replace_bounded_symbol_run(match: re.Match[str]) -> str:
left = spoken[: match.start()].rstrip()[-1:]
right = spoken[match.end() :].lstrip()[:1]
symbol_run = "".join(
character
for character in match.group(1)
if not character.isspace()
)
# The protected-range alignment canonicalizes every delimiter to
# one opaque sentinel later. Keep it symbolic here even when an
# adjacent ASR insertion makes both sides look identifier-like.
if preserve_network_boundaries and symbol_run and all(
character in _NETWORK_BOUNDARY_PUNCTUATION
for character in symbol_run
):
return match.group(0)
# A frontend delimiter remains outside a protected span even when
# ASR inserts an ordinary letter immediately beside it. Preserve
# that provenance when either side still matches a target network
# span. If the extra letter is between the delimiter and the
# scheme/path, neither side matches and the insertion stays inside
# the protected range below.
if symbol_run and all(
character in _NETWORK_BOUNDARY_PUNCTUATION
for character in symbol_run
):
left_key = network_boundary_key(spoken[: match.start()])
right_key = network_boundary_key(spoken[match.end() :])
if any(
(left_key.endswith(key) or right_key.startswith(key))
for key in proof_network_keys
if key
):
return match.group(0)
def is_identifier_side(character: str) -> bool:
if not character:
return False
if character.isascii() and character.isalnum():
return True
if character.isdecimal() or character in _ZH_DIGITS:
return True
# Preserve non-Han IRI labels while leaving punctuation at a
# Chinese-prose/network boundary as punctuation. The latter
# is inserted deliberately by the frontend and must not turn
# into an audible network token during comparison.
unicode_name = unicodedata.name(character, "")
return character.isalnum() and "CJK UNIFIED IDEOGRAPH" not in unicode_name
if not (is_identifier_side(left) and is_identifier_side(right)):
return match.group(0)
readings = [
network_symbol_reading(character)
for character in match.group(1)
if not character.isspace()
]
return f" {' '.join(readings)} "
# Mixed Whisper renderings may put spaces around literal separators,
# use Chinese digits, or insert a symbol not present in the target.
# Canonicalize every punctuation run bounded by Unicode alphanumerics;
# this makes equivalent separators match while keeping extra symbols
# as visible alignment insertions instead of dropping them later.
spoken = re.sub(
r"(?<=[^\W_])\s*((?:[^\w\s]|_)+)\s*(?=[^\W_])",
replace_bounded_symbol_run,
spoken,
)
output: list[str] = []
for character in spoken:
digit_key = network_digit_key(character)
if digit_key is not None:
output.append(digit_key)
continue
if (
character.isspace()
or character.isalnum()
or character in _NETWORK_BOUNDARY_PUNCTUATION
):
output.append(character)
else:
output.append(f" {network_symbol_reading(character)} ")
spoken = "".join(output)
for reading, letter in sorted(
_ZH_NETWORK_READING_TO_LETTER.items(),
key=lambda item: len(item[0]),
reverse=True,
):
spoken = spoken.replace(reading, f" {letter} ")
normalized = normalize_tts_eval_text(spoken).casefold()
locale_key = str(locale or "").replace("_", "-").lower()
if locale_key in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}:
normalized = _T2S_CONVERTER.convert(normalized)
if not preserve_network_boundaries:
return "".join(char for char in normalized if char.isalnum())
output: list[str] = []
for character in normalized:
if character.isalnum():
output.append(character)
elif character in _NETWORK_BOUNDARY_PUNCTUATION and (
not output or output[-1] != _NETWORK_ALIGNMENT_BOUNDARY_SENTINEL
):
output.append(_NETWORK_ALIGNMENT_BOUNDARY_SENTINEL)
return "".join(output)
def mandarin_acoustic_units(normalized_text: str) -> tuple[str, ...]:
"""Map normalized Mandarin/ASCII text to deterministic tone-aware units.
Whisper may render an acoustically identical Mandarin syllable with a
different Han character. Endpoint validation should not reject that
spelling choice, but it must still distinguish lexical tone (``藝`` yi4
from ``一`` yi1) and non-homophones. ASCII identifiers remain character
units so email/URL omissions cannot disappear through phonetic scoring.
"""
readings = lazy_pinyin(
str(normalized_text or ""),
style=Style.TONE3,
errors=lambda fragment: list(fragment),
strict=True,
v_to_u=False,
neutral_tone_with_five=True,
tone_sandhi=False,
)
return tuple(
reading.casefold()
for reading in readings
if reading and any(character.isalnum() for character in reading)
)
def _levenshtein_alignment(source: str, hypothesis: str) -> list[tuple[str, int, int]]:
"""Return deterministic edit operations as ``(op, source_index, hyp_index)``."""
rows = len(source) + 1
columns = len(hypothesis) + 1
distance = [[0] * columns for _ in range(rows)]
for source_index in range(rows):
distance[source_index][0] = source_index
for hyp_index in range(columns):
distance[0][hyp_index] = hyp_index
for source_index in range(1, rows):
for hyp_index in range(1, columns):
substitution = distance[source_index - 1][hyp_index - 1] + (
source[source_index - 1] != hypothesis[hyp_index - 1]
)
deletion = distance[source_index - 1][hyp_index] + 1
insertion = distance[source_index][hyp_index - 1] + 1
distance[source_index][hyp_index] = min(substitution, deletion, insertion)
operations: list[tuple[str, int, int]] = []
source_index = len(source)
hyp_index = len(hypothesis)
while source_index or hyp_index:
if source_index and hyp_index:
cost = source[source_index - 1] != hypothesis[hyp_index - 1]
if distance[source_index][hyp_index] == distance[source_index - 1][hyp_index - 1] + cost:
operations.append(
(
"substitute" if cost else "equal",
source_index - 1,
hyp_index - 1,
)
)
source_index -= 1
hyp_index -= 1
continue
if source_index and distance[source_index][hyp_index] == distance[source_index - 1][hyp_index] + 1:
operations.append(("delete", source_index - 1, hyp_index))
source_index -= 1
continue
operations.append(("insert", source_index, hyp_index - 1))
hyp_index -= 1
operations.reverse()
return operations
def _protected_ranges_are_exact_in_all_optimal_alignments(
source: str,
hypothesis: str,
protected_ranges: Sequence[tuple[int, int]],
) -> bool:
"""Require protected source ranges to survive every optimal edit path.
A deterministic Levenshtein backtrace is insufficient when ordinary text
duplicates a protected fragment: one optimal path can match the protected
occurrence while another deletes it and matches the ordinary occurrence.
Prefix/suffix distances let us inspect every edge that belongs to at least
one globally optimal path. Protected characters may only traverse equal
diagonal edges, and insertions may not be anchored inside the closed range
(including immediately before its first or after its last character).
"""
ranges = tuple(protected_ranges)
if not ranges:
return True
if any(
isinstance(start, bool)
or isinstance(end, bool)
or not isinstance(start, int)
or not isinstance(end, int)
or not 0 <= start < end <= len(source)
for start, end in ranges
):
raise ValueError("protected alignment range is invalid")
source_protected = [False] * len(source)
insertion_protected = [False] * (len(source) + 1)
for start, end in ranges:
for source_index in range(start, end):
source_protected[source_index] = True
for source_offset in range(start, end + 1):
insertion_protected[source_offset] = True
source_length = len(source)
hypothesis_length = len(hypothesis)
forward = [
[0] * (hypothesis_length + 1) for _ in range(source_length + 1)
]
for source_index in range(source_length + 1):
forward[source_index][0] = source_index
for hypothesis_index in range(hypothesis_length + 1):
forward[0][hypothesis_index] = hypothesis_index
for source_index in range(1, source_length + 1):
for hypothesis_index in range(1, hypothesis_length + 1):
forward[source_index][hypothesis_index] = min(
forward[source_index - 1][hypothesis_index] + 1,
forward[source_index][hypothesis_index - 1] + 1,
forward[source_index - 1][hypothesis_index - 1]
+ (
source[source_index - 1]
!= hypothesis[hypothesis_index - 1]
),
)
backward = [
[0] * (hypothesis_length + 1) for _ in range(source_length + 1)
]
for source_index in range(source_length + 1):
backward[source_index][hypothesis_length] = source_length - source_index
for hypothesis_index in range(hypothesis_length + 1):
backward[source_length][hypothesis_index] = (
hypothesis_length - hypothesis_index
)
for source_index in range(source_length - 1, -1, -1):
for hypothesis_index in range(hypothesis_length - 1, -1, -1):
backward[source_index][hypothesis_index] = min(
backward[source_index + 1][hypothesis_index] + 1,
backward[source_index][hypothesis_index + 1] + 1,
backward[source_index + 1][hypothesis_index + 1]
+ (source[source_index] != hypothesis[hypothesis_index]),
)
optimal_distance = forward[source_length][hypothesis_length]
for source_index in range(source_length + 1):
for hypothesis_index in range(hypothesis_length + 1):
prefix_distance = forward[source_index][hypothesis_index]
if (
source_index < source_length
and source_protected[source_index]
and prefix_distance
+ 1
+ backward[source_index + 1][hypothesis_index]
== optimal_distance
):
return False
if (
source_index < source_length
and hypothesis_index < hypothesis_length
and source_protected[source_index]
and source[source_index] != hypothesis[hypothesis_index]
and prefix_distance
+ 1
+ backward[source_index + 1][hypothesis_index + 1]
== optimal_distance
):
return False
if (
hypothesis_index < hypothesis_length
and insertion_protected[source_index]
and prefix_distance
+ 1
+ backward[source_index][hypothesis_index + 1]
== optimal_distance
):
return False
return True
@dataclass(frozen=True)
class NetworkFragmentAsrEvidence:
"""Result of exact, range-bound local network-fragment verification."""
transcript_text: str
protected_range_count: int
passed: bool
_NETWORK_FRAGMENT_IDENTIFIER_CHARACTERS = (
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+ "".join(_ZH_NETWORK_SYMBOL_READINGS)
)
_NETWORK_FRAGMENT_READING_TO_ASCII = {
reading: symbol for symbol, reading in _ZH_NETWORK_SYMBOL_READINGS.items()
}
def _network_fragment_token_ascii(token: str) -> str | None:
"""Invert one typed frontend network atom without prose heuristics."""
canonical = _SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
letter = _network_letter_token(canonical)
if letter is not None:
return letter
if canonical.isascii() and canonical.isalnum():
# A hybrid frontend may intentionally retain a planner-bound opaque
# label (``tour``/``islandmuseum``) while spelling only its separators
# and short suffix. It is still an exact typed atom, never prose: the
# range object below binds its precise occurrence to the parent URL.
return canonical.casefold()
if canonical and all(character in _ZH_DIGITS for character in canonical):
return "".join(str(_ZH_DIGITS.index(character)) for character in canonical)
return _NETWORK_FRAGMENT_READING_TO_ASCII.get(canonical)
def _network_fragment_ascii(spoken_fragment: str) -> str | None:
"""Return the exact ASCII atom sequence for one planner-bound fragment."""
output: list[str] = []
for token in normalize_tts_text(spoken_fragment).split():
value = _network_fragment_token_ascii(token)
if value is None:
return None
output.append(value)
rendered = "".join(output)
return rendered if rendered and any(character.isalnum() for character in rendered) else None
def _network_fragment_alignment_text(value: str) -> str:
"""Composable strict key used only by the exact local fragment gate.
Punctuation and symbols must remain visible here. Otherwise an unmatched
mutation inside a protected identifier (``islandmu/seum`` or an emoji,
for example) could disappear before the all-optimal alignment check.
"""
normalized = _T2S_CONVERTER.convert(
unicodedata.normalize("NFC", str(value or "")).casefold()
)
return "".join(character for character in normalized if not character.isspace())
def _network_fragment_mixed_render_pattern(spoken_fragment: str) -> str | None:
"""Build an exact ordered lattice for one planner-bound fragment.
Whisper commonly mixes the frontend's spoken symbol readings with literal
ASCII in one identifier, for example ``小老鼠islandmuseum.tw``. The
fragment proof already fixes the exact parent slice, so each frontend atom
may safely use either its own spoken spelling or its own exact ASCII value.
No atom may be dropped, reordered, substituted, or inferred from prose.
"""
atom_patterns: list[str] = []
for token in normalize_tts_text(spoken_fragment).split():
canonical = _SIMPLIFIED_NETWORK_SYMBOL_READINGS.get(token, token)
ascii_atom = _network_fragment_token_ascii(canonical)
if ascii_atom is None:
return None
spoken_variants = {token, canonical}
spoken_variants.update(
alias
for alias, canonical_reading in _SIMPLIFIED_NETWORK_SYMBOL_READINGS.items()
if canonical_reading == canonical
)
if canonical == "小老鼠":
# Pinned Whisper decoders can romanize the audibly correct email
# separator. These aliases remain one atom in this exact,
# planner-range-bound lattice; the surrounding local part, domain,
# TLD, ordering, and conservative identifier boundaries are still
# mandatory and cannot be inferred from prose.
spoken_variants.update({"xiaolaoshu", "xiaolaoshui"})
spoken_variants.add(ascii_atom)
variants = sorted(
{
r"\s*".join(re.escape(character) for character in value)
for value in spoken_variants
if value
},
key=lambda value: (-len(value), value),
)
atom_patterns.append(f"(?:{'|'.join(variants)})")
return r"\s*".join(atom_patterns) if atom_patterns else None
def _naturalized_url_mixed_render_pattern(
proof: NetworkUrlRenderingProof,
) -> str:
"""Return the exact ASR lattice proven by one natural URL rendering.
Whisper commonly preserves the renderer's lexical cues while formatting
host separators as literal dots and collapsing spelled ASCII atoms, for
example ``安全網址是coastwatch.example.tw路徑是tide網址輸入完畢``.
Every accepted alternative below is bound to one atom in the reversible
raw-to-spoken proof. No atom may be omitted, reordered, substituted, or
inferred from surrounding prose. The fully literal raw URL remains a
separate case-sensitive alternative because URL paths are case-sensitive.
"""
if not isinstance(proof, NetworkUrlRenderingProof):
raise ValueError("naturalized URL mixed pattern requires a rendering proof")
inverse_network_url_rendering(proof)
def spoken_pattern(value: str) -> str:
variants = {
unicodedata.normalize("NFC", value),
_T2S_CONVERTER.convert(unicodedata.normalize("NFC", value)),
}
rendered: list[str] = []
for variant in sorted(variants, key=lambda item: (-len(item), item)):
pieces: list[str] = []
whitespace_pending = False
for character in variant:
if character.isspace():
whitespace_pending = True
continue
if whitespace_pending:
pieces.append(r"\s*")
whitespace_pending = False
if character in {",", ","}:
# These two renderer-authored commas are prosodic. Their
# owning atom remains mandatory even when ASR omits only
# the pause mark itself.
pieces.append(r"\s*[,,]?\s*")
else:
pieces.append(re.escape(character))
if whitespace_pending:
pieces.append(r"\s*")
rendered.append("".join(pieces))
return f"(?:{'|'.join(dict.fromkeys(rendered))})"
atom_patterns: list[str] = []
for atom in proof.atoms:
spoken = spoken_pattern(atom.spoken_text)
if atom.component == "terminator":
# The raw terminator range is empty, so only its complete spoken
# cue proves semantic completion of a mixed rendering.
atom_patterns.append(spoken)
continue
raw = re.escape(atom.raw_text)
atom_patterns.append(f"(?:{spoken}|{raw})")
mixed = r"\s*".join(atom_patterns)
exact_raw = f"(?-i:{re.escape(proof.raw_identifier)})"
return f"(?:{exact_raw}|{mixed})"
def canonicalize_asr_network_fragments(
text: str,
target_text: str,
fragment_proofs: Sequence[NetworkFragmentProof],
) -> NetworkFragmentAsrEvidence:
"""Canonicalize and exact-gate local ASCII URL/email fragments.
This helper is deliberately separate from :func:`compare_asr_text`. It is
valid only for generation chunks carrying the range objects emitted by
:func:`plan_generation_chunks`; whole/joined/final verification must not
call it. Each literal replacement must equal the *entire* chunk-local
parent slice. Substrings of a full URL cannot borrow its proof.
"""
raw_text = unicodedata.normalize("NFC", str(text or ""))
target = unicodedata.normalize("NFC", str(target_text or ""))
if isinstance(fragment_proofs, (str, bytes)):
raise ValueError("network fragment proofs must be range objects")
try:
proofs = tuple(fragment_proofs)
except TypeError as error:
raise ValueError("network fragment proofs must be iterable") from error
if not proofs:
return NetworkFragmentAsrEvidence(raw_text, 0, True)
if not target:
raise ValueError("network fragment target must be non-empty")
previous_chunk_end = 0
typed_fragments: dict[str, tuple[str, str]] = {}
has_naturalized_url_proof = False
for proof in proofs:
if not isinstance(proof, NetworkFragmentProof):
raise ValueError("network fragment proof has an invalid type")
integer_fields = (
proof.span_index,
proof.chunk_start,
proof.chunk_end,
proof.parent_start,
proof.parent_end,
)
if any(isinstance(value, bool) or not isinstance(value, int) for value in integer_fields):
raise ValueError("network fragment proof offsets must be integers")
full_proof = unicodedata.normalize("NFC", str(proof.full_spoken_proof or ""))
if (
proof.span_index < 0
or not 0 <= proof.chunk_start < proof.chunk_end <= len(target)
or not 0 <= proof.parent_start < proof.parent_end <= len(full_proof)
or proof.chunk_start < previous_chunk_end
or target[proof.chunk_start : proof.chunk_end]
!= full_proof[proof.parent_start : proof.parent_end]
or full_proof not in network_protected_spoken_spans(full_proof)
):
raise ValueError("network fragment proof does not bind its target range")
previous_chunk_end = proof.chunk_end
fragment = target[proof.chunk_start : proof.chunk_end]
spoken_rendering = _naturalized_url_proof_from_spoken(full_proof)
if spoken_rendering is not None:
has_naturalized_url_proof = True
fresh_rendering = _naturalized_url_rendering_proof(
proof.raw_identifier
)
if (
not proof.raw_identifier
or fresh_rendering is None
or spoken_rendering != fresh_rendering
or proof.url_rendering_proof != fresh_rendering
or inverse_network_url_rendering(proof.url_rendering_proof)
!= proof.raw_identifier
or proof.url_rendering_proof.spoken_text != full_proof
or proof.parent_start != 0
or proof.parent_end != len(full_proof)
or fragment != full_proof
):
raise ValueError(
"naturalized URL fragment proof is inconsistent"
)
ascii_fragment = proof.raw_identifier
mixed_pattern = _naturalized_url_mixed_render_pattern(
fresh_rendering
)
elif proof.raw_identifier or proof.url_rendering_proof is not None:
raise ValueError(
"non-naturalized network proof carries URL rendering provenance"
)
else:
ascii_fragment = _network_fragment_ascii(fragment)
mixed_pattern = _network_fragment_mixed_render_pattern(fragment)
if ascii_fragment is None or mixed_pattern is None:
# Unmapped atoms remain eligible only in their canonical spoken
# form. A literal approximation is never inferred.
continue
old_fragment = typed_fragments.get(ascii_fragment.casefold())
if old_fragment is not None and old_fragment[0] != fragment:
raise ValueError("network fragment ASCII proof is ambiguous")
typed_fragments[ascii_fragment.casefold()] = (fragment, mixed_pattern)
# Protect longer exact fragments first, normalize surrounding transcript
# punctuation, then restore their planner-proven spoken forms. A mixed
# render is eligible only at a conservative prose boundary. In particular,
# an unknown adjacent symbol (backslash, angle bracket, emoji, and so on)
# must remain visible rather than letting a global replacement erase its
# provenance before the exact range alignment.
canonicalized = raw_text
unsafe_invisible_character = any(
unicodedata.category(character).startswith("C")
and not character.isspace()
for character in raw_text
)
placeholder_prefix = "藍鵲片段保護佔位符"
while placeholder_prefix in raw_text or placeholder_prefix in target:
placeholder_prefix += "號"
fragment_placeholders: list[tuple[str, str]] = []
unsafe_typed_literal = False
ascii_sentence_boundaries = frozenset(",.;:!?\"'()[]{}")
def literal_boundary_kind(value: str, offset: int, direction: int) -> str:
while 0 <= offset < len(value) and value[offset].isspace():
offset += direction
if offset < 0 or offset >= len(value):
return "safe"
character = value[offset]
if character.isascii() and character.isalnum():
return "identifier"
if (
character.isspace()
or _is_cjk(character)
or character in _NETWORK_NONASCII_BOUNDARY_PUNCTUATION
or character in ascii_sentence_boundaries
):
return "safe"
return "unsafe"
for ascii_fragment, (spoken_fragment, mixed_pattern) in sorted(
typed_fragments.items(),
key=lambda item: (-len(item[0]), item[0]),
):
pattern = re.compile(
mixed_pattern,
flags=re.IGNORECASE | re.ASCII,
)
def protect_literal(match: re.Match[str]) -> str:
nonlocal unsafe_typed_literal
boundary_kinds = (
literal_boundary_kind(match.string, match.start() - 1, -1),
literal_boundary_kind(match.string, match.end(), 1),
)
if "identifier" in boundary_kinds:
return match.group(0)
if "unsafe" in boundary_kinds:
unsafe_typed_literal = True
return match.group(0)
placeholder = (
f"{placeholder_prefix}"
f"{_zh_integer(len(fragment_placeholders) + 1)}結束"
)
fragment_placeholders.append((placeholder, spoken_fragment))
return placeholder
canonicalized = pattern.sub(protect_literal, canonicalized)
canonicalized = normalize_tts_text(canonicalized)
for placeholder, spoken_fragment in fragment_placeholders:
canonicalized = canonicalized.replace(placeholder, spoken_fragment)
def alignment_key(value: str) -> str:
source = (
_strip_naturalized_url_prosodic_commas(value)
if has_naturalized_url_proof
else value
)
return _network_fragment_alignment_text(source)
target_key = alignment_key(target)
transcript_key = alignment_key(canonicalized)
protected_ranges: list[tuple[int, int]] = []
rebuilt_target_key: list[str] = []
cursor = 0
key_cursor = 0
for proof in proofs:
prefix_key = alignment_key(target[cursor : proof.chunk_start])
fragment_key = alignment_key(
target[proof.chunk_start : proof.chunk_end]
)
rebuilt_target_key.extend((prefix_key, fragment_key))
key_cursor += len(prefix_key)
if not fragment_key:
raise ValueError("network fragment proof has no comparable content")
protected_ranges.append((key_cursor, key_cursor + len(fragment_key)))
key_cursor += len(fragment_key)
cursor = proof.chunk_end
rebuilt_target_key.append(alignment_key(target[cursor:]))
if "".join(rebuilt_target_key) != target_key:
raise ValueError("network fragment comparison ranges are not composable")
passed = (
not unsafe_typed_literal
and not unsafe_invisible_character
and _protected_ranges_are_exact_in_all_optimal_alignments(
target_key,
transcript_key,
protected_ranges,
)
)
return NetworkFragmentAsrEvidence(
transcript_text=canonicalized,
protected_range_count=len(protected_ranges),
passed=bool(passed),
)
@dataclass(frozen=True)
class AsrComparison:
"""Orthographic whole-text and acoustic endpoint evidence for one candidate."""
target_text: str
transcript_text: str
edit_distance: int
cer: float
prefix_cer: float
suffix_cer: float
prefix_deletions: int
suffix_deletions: int
extra_tail: str
extra_tail_units: int
network_protected_spans: int
network_protected_spans_passed: bool
passed: bool
def _network_protected_alignment_evidence(
raw_target: str,
raw_transcript: str,
*,
locale: str,
) -> tuple[int, bool]:
"""Require exact edits inside every normalized URL/email source range.
The ordinary CER representation intentionally removes punctuation. That
makes an insertion immediately outside a URL indistinguishable from an
insertion at the first/last URL character. For this protected-range gate
only, retain frontend punctuation as alignment provenance. An insertion
before the leading delimiter or after the trailing delimiter remains
ordinary prose, while an insertion between a delimiter and the protected
span is still rejected as URL/email content.
"""
normalized_frontend_target = normalize_spoken_forms(raw_target, locale=locale)
spoken_spans = network_protected_spoken_spans(normalized_frontend_target)
if not spoken_spans:
return 0, True
if network_identifier_has_ambiguous_iri(raw_target):
return len(spoken_spans), False
target_alignment_text = _comparison_text(
normalized_frontend_target,
locale=locale,
target_text=normalized_frontend_target,
preserve_network_boundaries=True,
)
transcript_alignment_text = _comparison_text(
raw_transcript,
locale=locale,
target_text=normalized_frontend_target,
preserve_network_boundaries=True,
)
operations = _levenshtein_alignment(
target_alignment_text,
transcript_alignment_text,
)
ranges: list[tuple[int, int]] = []
cursor = 0
for spoken in spoken_spans:
protected_text = _comparison_text(
spoken,
locale=locale,
target_text=normalized_frontend_target,
preserve_network_boundaries=True,
)
if not protected_text:
return len(spoken_spans), False
start = target_alignment_text.find(protected_text, cursor)
if start < 0:
return len(spoken_spans), False
end = start + len(protected_text)
ranges.append((start, end))
cursor = end
equal_source_indices = {
source_index
for operation, source_index, _ in operations
if operation == "equal"
}
insertions = [
source_index
for operation, source_index, _ in operations
if operation == "insert"
]
boundary_indices = {
boundary_index
for start, end in ranges
for boundary_index in (start - 1, end)
if 0 <= boundary_index < len(target_alignment_text)
and target_alignment_text[boundary_index]
== _NETWORK_ALIGNMENT_BOUNDARY_SENTINEL
}
ambiguous_boundary_substitution = any(
operation == "substitute" and source_index in boundary_indices
for operation, source_index, _ in operations
)
deleted_boundary_indices = {
source_index
for operation, source_index, _ in operations
if operation == "delete" and source_index in boundary_indices
}
ambiguous_deleted_boundary_insertion = any(
insertion_index in {boundary_index, boundary_index + 1}
for boundary_index in deleted_boundary_indices
for insertion_index in insertions
)
passed = all(
all(source_index in equal_source_indices for source_index in range(start, end))
and not any(start <= source_index <= end for source_index in insertions)
for start, end in ranges
) and not (
ambiguous_boundary_substitution
or ambiguous_deleted_boundary_insertion
)
return len(ranges), bool(passed)
def compare_asr_text(
target: str,
transcript: str,
*,
locale: str = "zh-TW",
prefix_units: int = 6,
suffix_units: int = 6,
max_cer: float = 0.10,
max_prefix_cer: float = 0.0,
max_suffix_cer: float = 0.0,
max_extra_tail_units: int = 0,
) -> AsrComparison:
"""Compare an ASR transcript with strict onset, completion, and tail gates.
Punctuation and whitespace are ignored, while written dates/numbers are
normalized before alignment. Empty targets, unsupported locales, invalid
limits, and non-finite limits all fail closed via ``passed=False``.
"""
try:
locale_key = str(locale or "").replace("_", "-").lower()
ambiguous_frequency_target = bool(
locale_key in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}
and _has_ambiguous_canonical_frequency(str(target or ""))
)
frequency_contradiction = bool(
locale_key in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}
and _frequency_measurements_contradict(
str(transcript or ""),
str(target or ""),
)
)
target_text = _comparison_text(
target,
locale=locale,
target_text=target,
)
transcript_text = _comparison_text(
transcript,
locale=locale,
target_text=target,
)
locale_key = str(locale or "").replace("_", "-").lower()
if locale_key in {"zh", "zh-tw", "zh-hant", "zh-cn", "zh-hans"}:
target_endpoint_units = mandarin_acoustic_units(target_text)
transcript_endpoint_units = mandarin_acoustic_units(transcript_text)
else:
target_endpoint_units = tuple(target_text)
transcript_endpoint_units = tuple(transcript_text)
except (TypeError, ValueError):
ambiguous_frequency_target = True
frequency_contradiction = True
target_text = ""
transcript_text = ""
target_endpoint_units = ()
transcript_endpoint_units = ()
try:
prefix_count = int(prefix_units)
suffix_count = int(suffix_units)
except (TypeError, ValueError, OverflowError):
prefix_count = 0
suffix_count = 0
prefix_limit = min(max(0, prefix_count), len(target_endpoint_units))
suffix_start = max(0, len(target_endpoint_units) - max(0, suffix_count))
orthographic_operations = _levenshtein_alignment(target_text, transcript_text)
try:
network_protected_spans, network_protected_spans_passed = (
_network_protected_alignment_evidence(
str(target or ""),
str(transcript or ""),
locale=locale,
)
)
except (TypeError, ValueError):
network_protected_spans = 0
network_protected_spans_passed = False
edit_distance = sum(
operation != "equal" for operation, _, _ in orthographic_operations
)
cer = edit_distance / len(target_text) if target_text else math.inf
orthographic_tail = "".join(
transcript_text[hypothesis_index]
for operation, source_index, hypothesis_index in orthographic_operations
if operation == "insert"
and source_index >= len(target_text)
and 0 <= hypothesis_index < len(transcript_text)
)
operations = _levenshtein_alignment(
target_endpoint_units,
transcript_endpoint_units,
)
prefix_errors = 0
suffix_errors = 0
prefix_deletions = 0
suffix_deletions = 0
tail_characters: list[str] = []
for operation, source_index, hyp_index in operations:
if operation == "equal":
continue
if operation == "insert":
if source_index < prefix_limit:
prefix_errors += 1
if source_index >= suffix_start:
suffix_errors += 1
if (
source_index >= len(target_endpoint_units)
and 0 <= hyp_index < len(transcript_endpoint_units)
):
tail_characters.append(transcript_endpoint_units[hyp_index])
continue
if source_index < prefix_limit:
prefix_errors += 1
prefix_deletions += operation == "delete"
if source_index >= suffix_start:
suffix_errors += 1
suffix_deletions += operation == "delete"
prefix_cer = prefix_errors / prefix_limit if prefix_limit else math.inf
suffix_length = len(target_endpoint_units) - suffix_start
suffix_cer = suffix_errors / suffix_length if suffix_length else math.inf
extra_tail = orthographic_tail
limits: list[float] = []
for value in (max_cer, max_prefix_cer, max_suffix_cer, max_extra_tail_units):
try:
limits.append(float(value))
except (TypeError, ValueError, OverflowError):
limits.append(math.nan)
valid_limits = all(math.isfinite(value) and value >= 0.0 for value in limits)
passed = bool(
target_text
and transcript_text
and not ambiguous_frequency_target
and not frequency_contradiction
and network_protected_spans_passed
and prefix_limit > 0
and suffix_length > 0
and valid_limits
and cer <= limits[0]
and prefix_cer <= limits[1]
and suffix_cer <= limits[2]
and len(tail_characters) <= limits[3]
)
return AsrComparison(
target_text=target_text,
transcript_text=transcript_text,
edit_distance=edit_distance,
cer=cer,
prefix_cer=prefix_cer,
suffix_cer=suffix_cer,
prefix_deletions=prefix_deletions,
suffix_deletions=suffix_deletions,
extra_tail=extra_tail,
extra_tail_units=len(tail_characters),
network_protected_spans=network_protected_spans,
network_protected_spans_passed=network_protected_spans_passed,
passed=passed,
)
def _finite_number(value, *, minimum: float | None = None, maximum: float | None = None) -> float | None:
if isinstance(value, (bool, np.bool_)):
return None
try:
number = float(value)
except (TypeError, ValueError, OverflowError):
return None
if not math.isfinite(number):
return None
if minimum is not None and number < minimum:
return None
if maximum is not None and number > maximum:
return None
return number
def candidate_local_score(
*,
cer: float,
speaker_similarity: float,
boundary_speaker_drop: float,
prefix_cer: float = 0.0,
suffix_cer: float = 0.0,
pace_penalty: float = 0.0,
style_penalty: float = 0.0,
asr_passed: bool = True,
truncated: bool = False,
extra_tail: bool = False,
speaker_weight: float = 0.05,
boundary_weight: float = 0.10,
prefix_weight: float = 0.50,
suffix_weight: float = 0.50,
pace_weight: float = 1.0,
style_weight: float = 1.0,
max_cer: float | None = None,
min_speaker_similarity: float | None = None,
max_boundary_speaker_drop: float | None = None,
) -> float:
"""Return a finite local candidate cost or ``inf`` for unsafe evidence."""
if asr_passed is not True or truncated is not False or extra_tail is not False:
return math.inf
metric_values = [
_finite_number(cer, minimum=0.0),
_finite_number(speaker_similarity, minimum=-1.0, maximum=1.0),
_finite_number(boundary_speaker_drop, minimum=0.0),
_finite_number(prefix_cer, minimum=0.0),
_finite_number(suffix_cer, minimum=0.0),
_finite_number(pace_penalty, minimum=0.0),
_finite_number(style_penalty, minimum=0.0),
]
weights = [
_finite_number(speaker_weight, minimum=0.0),
_finite_number(boundary_weight, minimum=0.0),
_finite_number(prefix_weight, minimum=0.0),
_finite_number(suffix_weight, minimum=0.0),
_finite_number(pace_weight, minimum=0.0),
_finite_number(style_weight, minimum=0.0),
]
if any(value is None for value in metric_values + weights):
return math.inf
candidate_cer, similarity, boundary_drop, onset_cer, ending_cer, pace, style = metric_values
gate_max_cer = None if max_cer is None else _finite_number(max_cer, minimum=0.0)
gate_min_similarity = (
None
if min_speaker_similarity is None
else _finite_number(min_speaker_similarity, minimum=-1.0, maximum=1.0)
)
gate_max_boundary = (
None
if max_boundary_speaker_drop is None
else _finite_number(max_boundary_speaker_drop, minimum=0.0)
)
if (
max_cer is not None and gate_max_cer is None
or min_speaker_similarity is not None and gate_min_similarity is None
or max_boundary_speaker_drop is not None and gate_max_boundary is None
):
return math.inf
if gate_max_cer is not None and candidate_cer > gate_max_cer:
return math.inf
if gate_min_similarity is not None and similarity < gate_min_similarity:
return math.inf
if gate_max_boundary is not None and boundary_drop > gate_max_boundary:
return math.inf
speaker_w, boundary_w, prefix_w, suffix_w, pace_w, style_w = weights
score = (
candidate_cer
+ speaker_w * (1.0 - similarity)
+ boundary_w * boundary_drop
+ prefix_w * onset_cer
+ suffix_w * ending_cer
+ pace_w * pace
+ style_w * style
)
return score if math.isfinite(score) and score >= 0.0 else math.inf
def candidate_transition_score(
*,
speaker_similarity: float,
f0_delta: float,
rms_delta: float,
speaker_weight: float = 1.0,
f0_weight: float = 1.0,
rms_weight: float = 1.0,
) -> float:
"""Score continuity between adjacent candidates with finite-only inputs."""
values = [
_finite_number(speaker_similarity, minimum=-1.0, maximum=1.0),
_finite_number(f0_delta, minimum=0.0),
_finite_number(rms_delta, minimum=0.0),
_finite_number(speaker_weight, minimum=0.0),
_finite_number(f0_weight, minimum=0.0),
_finite_number(rms_weight, minimum=0.0),
]
if any(value is None for value in values):
return math.inf
similarity, pitch_delta, loudness_delta, speaker_w, pitch_w, loudness_w = values
score = (
speaker_w * (1.0 - similarity)
+ pitch_w * pitch_delta
+ loudness_w * loudness_delta
)
return score if math.isfinite(score) and score >= 0.0 else math.inf
@dataclass(frozen=True)
class CandidateSequenceSelection:
candidate_indices: tuple[int, ...]
total_score: float
def select_candidate_sequence(
local_scores: Sequence[Sequence[float]],
transition_scores: Sequence[Sequence[Sequence[float]]] = (),
) -> CandidateSequenceSelection | None:
"""Select the minimum-cost candidate path in ``O(N K^2)``.
Non-finite/negative scores remove only their candidate or edge. Malformed
matrices and graphs with no complete finite path return ``None`` so callers
cannot accidentally fall back to an unverified candidate.
"""
try:
local_rows = [list(row) for row in local_scores]
except TypeError:
return None
if not local_rows or any(not row for row in local_rows):
return None
safe_local = [
[
score if score is not None else math.inf
for score in (_finite_number(value, minimum=0.0) for value in row)
]
for row in local_rows
]
if len(safe_local) == 1:
try:
if len(transition_scores) != 0:
return None
except TypeError:
return None
best_index = min(range(len(safe_local[0])), key=safe_local[0].__getitem__)
best_score = safe_local[0][best_index]
if not math.isfinite(best_score):
return None
return CandidateSequenceSelection((best_index,), best_score)
try:
transitions = [[list(row) for row in matrix] for matrix in transition_scores]
except TypeError:
return None
if len(transitions) != len(safe_local) - 1:
return None
safe_transitions: list[list[list[float]]] = []
for index, matrix in enumerate(transitions):
previous_count = len(safe_local[index])
current_count = len(safe_local[index + 1])
if len(matrix) != previous_count or any(len(row) != current_count for row in matrix):
return None
safe_transitions.append(
[
[
score if score is not None else math.inf
for score in (_finite_number(value, minimum=0.0) for value in row)
]
for row in matrix
]
)
previous_costs = safe_local[0]
backpointers: list[list[int]] = []
for step in range(1, len(safe_local)):
current_costs = [math.inf] * len(safe_local[step])
current_backpointers = [-1] * len(safe_local[step])
for current_index, local_score in enumerate(safe_local[step]):
if not math.isfinite(local_score):
continue
for previous_index, previous_score in enumerate(previous_costs):
edge_score = safe_transitions[step - 1][previous_index][current_index]
if not math.isfinite(previous_score) or not math.isfinite(edge_score):
continue
total = previous_score + edge_score + local_score
if math.isfinite(total) and total < current_costs[current_index]:
current_costs[current_index] = total
current_backpointers[current_index] = previous_index
previous_costs = current_costs
backpointers.append(current_backpointers)
final_index = min(range(len(previous_costs)), key=previous_costs.__getitem__)
total_score = previous_costs[final_index]
if not math.isfinite(total_score):
return None
indices = [final_index]
for pointers in reversed(backpointers):
final_index = pointers[final_index]
if final_index < 0:
return None
indices.append(final_index)
indices.reverse()
return CandidateSequenceSelection(tuple(indices), total_score)
@torch.no_grad()
def extract_windowed_speaker_embedding(
wav_path: str,
encoder,
*,
device: str = "cpu",
min_duration_seconds: float = 3.0,
window_seconds: float = 3.0,
hop_seconds: float = 1.5,
max_windows: int = 12,
full_clip_max_seconds: float = 12.0,
) -> torch.Tensor:
"""Extract one denoised ECAPA embedding from overlapping reference windows."""
import librosa
waveform, _ = librosa.load(wav_path, sr=16000, mono=True)
waveform = np.asarray(waveform, dtype=np.float32)
duration = waveform.size / 16000.0
if duration < min_duration_seconds:
raise ValueError(
f"reference audio must be at least {min_duration_seconds:.1f} seconds; got {duration:.2f}"
)
segments: list[np.ndarray] = []
if duration <= full_clip_max_seconds:
segments.append(waveform)
window = max(1, int(round(window_seconds * 16000)))
hop = max(1, int(round(hop_seconds * 16000)))
starts = list(range(0, max(0, waveform.size - window) + 1, hop))
if max_windows > 0 and len(starts) > max_windows:
indices = np.linspace(0, len(starts) - 1, max_windows).round().astype(int)
starts = [starts[index] for index in dict.fromkeys(indices.tolist())]
segments.extend(waveform[start : start + window] for start in starts)
embeddings: list[torch.Tensor] = []
for segment in segments:
tensor = torch.from_numpy(np.ascontiguousarray(segment)).float().unsqueeze(0).to(device)
embedding = encoder.encode_batch(tensor).reshape(-1)
embeddings.append(torch.nn.functional.normalize(embedding, dim=0).cpu())
if not embeddings:
raise ValueError("reference audio did not contain a usable speech window")
return torch.nn.functional.normalize(torch.stack(embeddings).mean(dim=0), dim=0)