piper.download_voices pastes the voice name straight into the HTTP request line, and http.client encodes that as ASCII — so it dies with UnicodeEncodeError on the ã in pt_PT-tugão-medium before a byte leaves the container. Every pt_BR voice downloads fine. The failure lands precisely on the one voice the pt-PT pair needs, and it is the *only* European voice in the catalogue. The entrypoint now falls back to fetching the model and its config itself with the path percent-encoded, which is all the downloader was missing. Same host, same files, same destination names, so the cached-voice check and the server invocation are unchanged.
58 lines
2.5 KiB
Bash
Executable File
58 lines
2.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Fetch the configured voice if the shared volume doesn't have it yet, then
|
|
# serve it. The download is the only step that needs the internet, and it runs
|
|
# once per voice for the life of the volume — Petal itself stays offline-first.
|
|
set -euo pipefail
|
|
|
|
voice="${PIPER_VOICE:?PIPER_VOICE must be set}"
|
|
data_dir="${PIPER_DATA_DIR:-/voices}"
|
|
port="${PIPER_PORT:-5000}"
|
|
|
|
if [ ! -f "${data_dir}/${voice}.onnx" ]; then
|
|
echo ">> downloading voice ${voice} into ${data_dir}"
|
|
# piper.download_voices cannot fetch a voice whose name isn't ASCII, and the
|
|
# only European Portuguese voice in the catalogue is pt_PT-tugão-medium:
|
|
# the downloader pastes the name straight into the request line, and
|
|
# http.client encodes that as ASCII, so it dies with UnicodeEncodeError on
|
|
# the ã before a byte leaves the container. Every pt_BR voice downloads
|
|
# fine — the failure lands precisely on the voice the pt-PT pair needs.
|
|
#
|
|
# So: try the supported path, and fall back to fetching the two files
|
|
# ourselves with the URL percent-encoded, which is all the downloader was
|
|
# missing. Same host, same files, same destination names.
|
|
python -m piper.download_voices "${voice}" --data-dir "${data_dir}" || {
|
|
echo ">> download_voices failed for ${voice}; fetching directly (non-ASCII voice name)"
|
|
python - "${voice}" "${data_dir}" <<'PY'
|
|
import json, sys, urllib.parse, urllib.request
|
|
|
|
voice, data_dir = sys.argv[1], sys.argv[2]
|
|
BASE = "https://huggingface.co/rhasspy/piper-voices/resolve/main/"
|
|
|
|
catalogue = json.load(urllib.request.urlopen(BASE + "voices.json", timeout=120))
|
|
entry = catalogue.get(voice)
|
|
if entry is None:
|
|
sys.exit(f"no voice named {voice!r} in the catalogue")
|
|
|
|
# The catalogue keys the files by repo path; only the model and its config are
|
|
# needed to serve (MODEL_CARD is licence text).
|
|
for path in entry["files"]:
|
|
if not path.endswith((".onnx", ".onnx.json")):
|
|
continue
|
|
url = BASE + urllib.parse.quote(path)
|
|
dest = f"{data_dir}/{path.rsplit('/', 1)[-1]}"
|
|
print(f">> {url} -> {dest}", flush=True)
|
|
with urllib.request.urlopen(url, timeout=600) as r, open(dest, "wb") as out:
|
|
while chunk := r.read(1 << 20):
|
|
out.write(chunk)
|
|
PY
|
|
}
|
|
fi
|
|
|
|
echo ">> serving ${voice} on :${port}"
|
|
# 0.0.0.0 is safe here: the container sits on Petal's internal compose network
|
|
# with no published ports, so only Petal can reach it.
|
|
exec python -m piper.http_server \
|
|
-m "${voice}" \
|
|
--data-dir "${data_dir}" \
|
|
--host 0.0.0.0 --port "${port}"
|