Some checks failed
standards workspaces build + test / jouleclaw-rs (push) Successful in 3m1s
standards workspaces build + test / aac-rs (push) Successful in 1m24s
standards workspaces build + test / joule-code-rs (push) Successful in 54s
standards workspaces build + test / joulecontract-rs (push) Successful in 59s
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Successful in 4s
standards workspaces build + test / map-rs (push) Successful in 1m8s
standards workspaces build + test / mesh-rs (push) Successful in 40s
standards workspaces build + test / eoc-rs (push) Successful in 1m49s
standards workspaces build + test / jcp-rs (push) Successful in 1m25s
standards workspaces build + test / openpay (push) Successful in 3m24s
standards workspaces build + test / proof-rs (push) Successful in 1m16s
standards workspaces build + test / sandbox-rs (push) Successful in 2m9s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m36s
standards workspaces build + test / arl-rs (push) Has been cancelled
standards workspaces build + test / joule-ui-rs (push) Has been cancelled
standards workspaces build + test / jouledesk-rs (push) Has been cancelled
standards workspaces build + test / joulehook-rs (push) Has been cancelled
standards workspaces build + test / sense-rs (push) Has been cancelled
standards workspaces build + test / macOS energy backend (jcp-energy) (push) Has been cancelled
A better voice is a 100 MB download, and the person could not hear one before deciding. Making the samples showed something worse: some voices did not say the words at all. - Samples: each voice says a greeting and a request in its language (two or three seconds, MP3, about 10 KB). Settings > Better voices plays it with the sentence written beside it, before any download; checked by size and SHA-256 and played through the voices' own output. render_samples.mjs renders them with LAME; they are committed (Piper's synthesis is not repeatable), and build_voices.py publishes each beside the voices while it says the current text. - Intelligibility: intelligibility.py has every voice say seven everyday phrases, a speech recognizer (Whisper large-v3-turbo on MLX) writes down what it hears, and the scores are committed. Ratings now come from that evidence (RATING_RULE): under 0.80 the voice is withheld; under 0.90, or any phrase under 0.60, experimental; 0.90 and over clear; 0.95 and over with an upstream grade of A or B, natural. - Withheld, because they say other words: German (the only German voice, 0.14), Dutch MLS (0.27), Ukrainian (0.18) and both Japanese voices (0.63-0.69: they misread 外, 好き and 疲れた). Their languages say so plainly under "Why". Portuguese and Hindi are experimental; Spanish, Chinese, Italian, Flemish and Cori are clear; Heart, Emma and Siwis natural. 16 voices in 8 languages, 452 MB of packs. Co-Authored-By: Claude Opus 5.5 <[email protected]>
95 lines
3.3 KiB
Python
95 lines
3.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Check a voice-pack directory before a site build ships it.
|
|
|
|
python3 tools/voices/check_packs.py DIR
|
|
|
|
DIR must hold what tools/voices/build_voices.py built for the committed
|
|
catalogue (data/voices/index.json): its index.json points at
|
|
index.<sha12>.json, whose bytes are the committed catalogue's, and every file
|
|
that catalogue names is there with its exact size and SHA-256. Otherwise this
|
|
exits non-zero and says what is wrong, so a build never ships packs that
|
|
disagree with the catalogue the site describes, or that are incomplete.
|
|
|
|
Standard library only; Python 3.9 or later (macOS's own python3).
|
|
"""
|
|
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
COMMITTED = HERE.parents[1] / "data" / "voices" / "index.json"
|
|
SHOW = 10
|
|
|
|
|
|
def sha256(path):
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(1 << 20), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def file_refs(cat):
|
|
"""Every FileRef the catalogue names: runtimes, components and packs,
|
|
each with its licence text, and each pack's sample."""
|
|
refs = []
|
|
owners = list(cat.get("runtimes", {}).values()) + list(cat.get("components", {}).values())
|
|
for lang in cat.get("languages", {}).values():
|
|
owners += lang.get("packs", [])
|
|
for owner in owners:
|
|
refs += owner.get("files", [])
|
|
for one in ("licence_text", "sample"):
|
|
if one in owner:
|
|
refs.append(owner[one])
|
|
return refs
|
|
|
|
|
|
def check(out):
|
|
committed = COMMITTED.read_bytes()
|
|
want = "index.%s.json" % hashlib.sha256(committed).hexdigest()[:12]
|
|
try:
|
|
pointer = json.loads((out / "index.json").read_text(encoding="utf-8"))
|
|
except (OSError, ValueError) as e:
|
|
return ["%s: no readable index.json (%s)" % (out, e)]
|
|
problems = []
|
|
if pointer.get("current") != want:
|
|
problems.append(
|
|
"index.json points at %s, but the committed catalogue is %s: rebuild the packs "
|
|
"with tools/voices/build_voices.py" % (pointer.get("current"), want)
|
|
)
|
|
catalogue = out / want
|
|
if not catalogue.is_file() or catalogue.read_bytes() != committed:
|
|
problems.append("%s is missing, or differs from data/voices/index.json" % want)
|
|
return problems
|
|
for ref in file_refs(json.loads(committed)):
|
|
path = out / ref["url"]
|
|
if not path.is_file():
|
|
problems.append("missing: %s" % ref["url"])
|
|
elif path.stat().st_size != ref["bytes"]:
|
|
problems.append("%s is %d bytes, not %d" % (ref["url"], path.stat().st_size, ref["bytes"]))
|
|
elif sha256(path) != ref["sha256"]:
|
|
problems.append("%s does not match its SHA-256" % ref["url"])
|
|
return problems
|
|
|
|
|
|
def main(argv):
|
|
if len(argv) != 2:
|
|
print(__doc__.strip(), file=sys.stderr)
|
|
return 2
|
|
out = Path(argv[1])
|
|
problems = check(out)
|
|
if problems:
|
|
print("✗ voice packs in %s do not match the committed catalogue:" % out, file=sys.stderr)
|
|
for p in problems[:SHOW]:
|
|
print(" - " + p, file=sys.stderr)
|
|
if len(problems) > SHOW:
|
|
print(" ... and %d more" % (len(problems) - SHOW), file=sys.stderr)
|
|
return 1
|
|
print("✓ voice packs in %s match the committed catalogue" % out)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|