diff --git a/.github/scripts/assert_intent_roundtrip.py b/.github/scripts/assert_intent_roundtrip.py index fe87b9ca..407ab274 100755 --- a/.github/scripts/assert_intent_roundtrip.py +++ b/.github/scripts/assert_intent_roundtrip.py @@ -118,7 +118,12 @@ def send(self, msg_type, data=None, context=None): self._dropped(error) def wait_for(self, reply_type, timeout, match=None): - """Read until `reply_type` arrives, or the deadline passes. None on timeout.""" + """Read until `reply_type` arrives, or the deadline passes. None on timeout. + + `reply_type` may be a tuple of types, for an answer that arrives under a spec + topic on newer services and a legacy one on older ones. + """ + reply_types = reply_type if isinstance(reply_type, tuple) else (reply_type,) deadline = time.monotonic() + timeout while True: remaining = deadline - time.monotonic() @@ -140,7 +145,7 @@ def wait_for(self, reply_type, timeout, match=None): message = json.loads(raw) except (TypeError, ValueError): continue - if message.get("type") != reply_type: + if message.get("type") not in reply_types: continue if match and not match(message): continue diff --git a/.github/scripts/assert_local_speech.sh b/.github/scripts/assert_local_speech.sh new file mode 100755 index 00000000..47d374f0 --- /dev/null +++ b/.github/scripts/assert_local_speech.sh @@ -0,0 +1,133 @@ +#!/usr/bin/env bash +# Assert that a local speech install really runs speech on the device. +# +# 1. mycroft.conf names the onnx-asr and phoonnx plugins, with the public server +# only as the STT fallback, and the STT model is on disk already: the installer +# fetches it rather than leaving it to the first utterance; +# 2. the running listener loaded the onnx-asr recognizer - its own log says so; +# 3. the running audio service speaks with phoonnx (assert_local_speech_bus.py +# asks it over the bus, and it names the plugin it used); +# 4. what that voice said, the running listener hears back. A listener with no +# microphone never starts answering the bus, which is every CI runner, and then +# the recognizer hears it in a fresh process instead. +# +# 1 alone was all this used to check, and it passes with services that never loaded +# what the file names - they only reload a mycroft.conf rewritten in place. +# +# assert_local_speech.sh [venv-python] a virtualenv install +# assert_local_speech.sh --containers a containers install (ovos_listener and +# ovos_audio) +# +# Never run this under sudo. HOME decides which mycroft.conf is read and where the +# models and logs are, and all of them belong to the install user. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +stt_config=$( + cat <<'PY' +import os +from ovos_config import Configuration + +stt = Configuration()["stt"] +if stt.get("module") != "ovos-stt-plugin-onnx-asr": + raise SystemExit(f"STT is {stt.get('module')!r}, not the local onnx-asr plugin") +if stt.get("fallback_module") != "ovos-stt-plugin-server": + raise SystemExit(f"STT fallback is {stt.get('fallback_module')!r}, not the public server") +models = os.path.expanduser("~/.local/share/ovos_stt_plugin_onnxasr") +if not os.path.isdir(models) or not os.listdir(models): + raise SystemExit(f"no STT model in {models}: the install did not fetch it") +print(f"config : STT {(stt.get(stt['module']) or {}).get('model')}, public fallback") +PY +) + +tts_config=$( + cat <<'PY' +from ovos_config import Configuration + +tts = Configuration()["tts"] +if tts.get("module") != "ovos-tts-plugin-phoonnx": + raise SystemExit(f"TTS is {tts.get('module')!r}, not the local phoonnx plugin") +print(f"config : TTS {(tts.get(tts['module']) or {}).get('voice')}") +PY +) + +# What the voice said, heard in a process of its own. The path is "-" to read the +# audio from stdin, which is how it reaches a container. +hear=$( + cat <<'PY' +import sys +import tempfile +from ovos_config import Configuration +from ovos_plugin_manager.stt import load_stt_plugin +from ovos_plugin_manager.utils.audio import AudioData + +path = sys.argv[1] +if path == "-": + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as wav: + wav.write(sys.stdin.buffer.read()) + path = wav.name +config = Configuration() +stt, lang = config["stt"], config["lang"] +recognizer = dict(stt.get(stt["module"]) or {}, lang=lang) +heard = load_stt_plugin(stt["module"])(config=recognizer).execute( + AudioData.from_file(path), language=lang) or "" +if "time" not in heard.lower(): + raise SystemExit(f"the voice said 'what time is it' and the recognizer heard {heard!r}") +print(f"recognizer: heard {heard!r} in a fresh process") +PY +) + +wav="$(mktemp --suffix=.wav)" +websocket_dir="" +cleanup() { + rm -f "$wav" + if [ -n "$websocket_dir" ]; then + rm -rf "$websocket_dir" + fi +} +trap cleanup EXIT + +if [ "${1:-}" = "--containers" ]; then + docker exec ovos_listener python3 -c "$stt_config" + docker exec ovos_audio python3 -c "$tts_config" + listener_log() { docker logs ovos_listener 2>&1; } + hear_fresh() { docker exec -i ovos_listener python3 -c "$hear" - <"$wav"; } + # No host venv: the bus check needs websocket-client and nothing else, so it goes + # into a throwaway directory rather than ~/.local (see assert_intent_roundtrip.sh). + bus_python=python3 + if ! python3 -c "import websocket" 2>/dev/null; then + websocket_dir="$(mktemp -d)" + python3 -m pip install --quiet --disable-pip-version-check --no-cache-dir \ + --target "$websocket_dir" websocket-client + export PYTHONPATH="${websocket_dir}${PYTHONPATH:+:${PYTHONPATH}}" + fi +else + venv_python="${1:-${HOME}/.venvs/ovos/bin/python}" + if [ ! -x "${venv_python}" ]; then + echo "no virtualenv python at ${venv_python}" >&2 + exit 1 + fi + "${venv_python}" -c "$stt_config" + "${venv_python}" -c "$tts_config" + listener_log() { cat "${XDG_STATE_HOME:-${HOME}/.local/state}/mycroft/voice.log"; } + hear_fresh() { "${venv_python}" -c "$hear" "$wav"; } + bus_python="${venv_python}" +fi + +# The plugin logs as it loads its model, and every line names its module. Not grep +# -q: it stops reading at the first match, the log writer dies of SIGPIPE, and +# pipefail turns a found line into a failure. +if ! listener_log | grep -F "ovos_stt_plugin_onnxasr" >/dev/null; then + echo "the running listener never loaded the onnx-asr recognizer" >&2 + exit 1 +fi +echo "listener : loaded the onnx-asr recognizer" + +status=0 +"${bus_python}" "${here}/assert_local_speech_bus.py" --wav "$wav" || status=$? +case "$status" in +0) ;; +3) hear_fresh ;; +*) exit "$status" ;; +esac diff --git a/.github/scripts/assert_local_speech_bus.py b/.github/scripts/assert_local_speech_bus.py new file mode 100755 index 00000000..f90423dd --- /dev/null +++ b/.github/scripts/assert_local_speech_bus.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +"""Have the running services speak and listen, over the messagebus. + +assert_local_speech.sh checks mycroft.conf and the plugins it names. That is not the +same as the services using them: they load their plugins when they start and reload +only a configuration file rewritten in place, so a mycroft.conf they never picked up +passes every check made on the file while the device keeps talking to the public +servers. This asks the services themselves: + + 1. the audio service synthesises a sentence for the bus (speak:b64_audio) and names + the plugin that did it, which must be the local one; + 2. the listener transcribes that audio with its own recognizer + (recognizer_loop:b64_transcribe), never its fallback, and must hear the sentence. + +The listener only starts answering the bus once its microphone has started, and a CI +runner has no microphone. When it never reports ready, the audio is still written to +--wav and this exits 3, so the caller can have the recognizer hear it in a fresh +process instead. + +Like assert_intent_roundtrip.py, which this borrows its bus client from, it speaks the +bus protocol over a bare websocket, so it runs the same on a host with no venv. +""" +import argparse +import base64 +import io +import secrets +import sys +import time +import wave + +from assert_intent_roundtrip import Bus, Failure, wait_ready + +LISTENER_UNAVAILABLE = 3 +LOCAL_TTS = "ovos-tts-plugin-phoonnx" + + +def speak(bus, utterance, timeout): + """The audio service's own TTS, as bytes of a WAV file, and the plugin it used.""" + marker = secrets.token_hex(8) + bus.send("speak:b64_audio", {"utterance": utterance}, + {"source": ["ci"], "ci_probe": marker}) + # Newer services answer on the spec topic, older ones on the legacy one, and the + # bus client may mirror one onto the other: whichever comes first will do. + answer = bus.wait_for(("ovos.audio.speech", "speak:b64_audio.response"), timeout, + match=lambda m: (m.get("context") or {}).get("ci_probe") == marker) + if answer is None: + raise Failure( + f"the audio service did not synthesise {utterance!r} for the bus within " + f"{timeout:.0f}s, having reported itself ready." + ) + data = answer.get("data") or {} + try: + return base64.b64decode(data["audio"]), data.get("tts_id") + except (KeyError, TypeError, ValueError) as error: + raise Failure(f"the audio service answered without usable audio: {error!r}") + + +def pcm(wav_bytes): + """The frames of a mono WAV, with the rate and width the listener needs to read them.""" + try: + with wave.open(io.BytesIO(wav_bytes)) as wav: + if wav.getnchannels() != 1: + raise Failure(f"the voice produced {wav.getnchannels()} channels, not one") + return wav.readframes(wav.getnframes()), wav.getframerate(), wav.getsampwidth() + except (wave.Error, EOFError) as error: + raise Failure(f"the audio service's answer is not a WAV file: {error}") + + +def transcribe(bus, wav_bytes, timeout): + frames, rate, width = pcm(wav_bytes) + marker = secrets.token_hex(8) + bus.send("recognizer_loop:b64_transcribe", + {"audio": base64.b64encode(frames).decode("ascii"), + "sample_rate": rate, "sample_width": width}, + {"source": ["ci"], "ci_probe": marker}) + answer = bus.wait_for("recognizer_loop:b64_transcribe.response", timeout, + match=lambda m: (m.get("context") or {}).get("ci_probe") == marker) + if answer is None: + raise Failure( + f"the listener did not transcribe the audio within {timeout:.0f}s, having " + f"reported itself ready." + ) + heard = [] + for transcription in (answer.get("data") or {}).get("transcriptions") or []: + # (text, confidence) pairs, which JSON turns into lists; plain strings from + # older listeners. + heard.append(transcription[0] if isinstance(transcription, (list, tuple)) + else str(transcription)) + return heard[0] if heard else "" + + +def main(argv=None): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--url", default="ws://127.0.0.1:8181/core") + parser.add_argument("--utterance", default="what time is it") + parser.add_argument("--expect-word", default="time", + help="a word the recognizer has to hear in --utterance") + parser.add_argument("--expect-tts", default=LOCAL_TTS, + help="the plugin the audio service has to name") + parser.add_argument("--wav", required=True, + help="where to write what the voice said") + parser.add_argument("--connect-timeout", type=float, default=30) + parser.add_argument("--ready-timeout", type=float, default=300, + help="the services restart onto local speech, and the " + "listener loads its model before it answers") + parser.add_argument("--listener-ready-timeout", type=float, default=60, + help="how long to give a listener that may have no " + "microphone to start with") + parser.add_argument("--reply-timeout", type=float, default=120) + args = parser.parse_args(argv) + + bus = None + try: + bus = Bus(args.url, args.connect_timeout) + wait_ready(bus, "audio", args.ready_timeout) + + started = time.monotonic() + wav_bytes, tts_id = speak(bus, args.utterance, args.reply_timeout) + print(f"voice : the audio service said {args.utterance!r} with {tts_id} " + f"in {time.monotonic() - started:.1f}s") + with open(args.wav, "wb") as out: + out.write(wav_bytes) + if tts_id != args.expect_tts: + raise Failure( + f"the running audio service speaks with {tts_id!r}, not {args.expect_tts!r}. " + f"mycroft.conf may name the local voice, but the service never loaded it." + ) + + try: + wait_ready(bus, "voice", args.listener_ready_timeout) + except Failure: + print("listener : not ready, so it is not answering the bus here (no " + "microphone to start with?)") + return LISTENER_UNAVAILABLE + + started = time.monotonic() + heard = transcribe(bus, wav_bytes, args.reply_timeout) + print(f"listener : heard {heard!r} in {time.monotonic() - started:.1f}s") + if args.expect_word not in heard.lower(): + raise Failure( + f"the voice said {args.utterance!r} and the running listener heard " + f"{heard!r}." + ) + print("the running services speak and hear on this device") + return 0 + except Failure as failure: + print(f"\nlocal speech check failed: {failure}", file=sys.stderr) + return 1 + finally: + if bus is not None: + bus.close() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/scenarios-ubuntu2404.yml b/.github/workflows/scenarios-ubuntu2404.yml index fe86ab2f..c9bd860f 100644 --- a/.github/workflows/scenarios-ubuntu2404.yml +++ b/.github/workflows/scenarios-ubuntu2404.yml @@ -245,6 +245,32 @@ jobs: feature_set: - minimal - all + speech_engine: + - public + # Local speech is alpha only. Both runners have the 16 GB and the AVX2/NEON the + # installer's hardware check asks for, so a run that falls back to public here + # is a regression, not a runner limit. + include: + - runner: ubuntu-24.04 + method: virtualenv + channel: alpha + feature_set: all + speech_engine: local + - runner: ubuntu-24.04-arm + method: virtualenv + channel: alpha + feature_set: all + speech_engine: local + - runner: ubuntu-24.04 + method: containers + channel: alpha + feature_set: all + speech_engine: local + - runner: ubuntu-24.04-arm + method: containers + channel: alpha + feature_set: all + speech_engine: local runs-on: ${{ matrix.runner }} timeout-minutes: 120 @@ -345,6 +371,7 @@ jobs: raspberry_pi_tuning: false share_telemetry: false share_usage_telemetry: false + speech_engine: ${{ matrix.speech_engine }} EOF - name: Run installer scenario exhaustive @@ -410,6 +437,15 @@ jobs: --utterance "what time is it" --expect-speak \ --ready-timeout 420 --reply-timeout 120 --speak-timeout 120 + - name: Validate speech runs on the device + if: matrix.speech_engine == 'local' + run: | + if [ "${{ matrix.method }}" = "containers" ]; then + .github/scripts/assert_local_speech.sh --containers + else + .github/scripts/assert_local_speech.sh "$HOME/.venvs/ovos/bin/python" + fi + - name: Create uninstall scenario file shell: bash run: | diff --git a/AUDIT.md b/AUDIT.md new file mode 100644 index 00000000..e1b569cb --- /dev/null +++ b/AUDIT.md @@ -0,0 +1,28 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Record PR 648 findings, regression evidence and remaining validation limits. + +# Audit + +## 2026-10-08 — PR 648 follow-up + +- **Fixed: rejected STT downloads survived failed probes.** Timeout and unsuccessful + process exits now call `forget_model()` in + [`first_working()`](ansible/roles/ovos_config/files/speech_setup.py). + [`SpeechSetupTest.test_failed_probes_remove_only_new_downloads()`](scripts/test_speech_setup.py) + reproduces both failures, checks that existing caches survive and verifies the + successful fallback's files remain. The new assertions failed before the fix. +- **Fixed: local-speech privacy wording.** The selection and summary disclose + public fallback. The speech screen and [automation guide](docs/automation.md) + describe failed/empty recognition uploads and public speech when a local model + cannot run; they make no fully offline promise. +- **Verification:** `pytest -q scripts/test_speech_setup.py scripts/test_local_speech_bus.py` + passes **28 tests and 4 subtests**. **99 BATS tests pass with no skips**, including + all locales, navigation, real whiptail and speech Ansible tasks. Ruff, ShellCheck, + the offline contract checker and `git diff --check` pass. Display verification + is recorded in [the maintenance report](MAINTENANCE_REPORT.md). +- **Outstanding integration evidence:** the full matrix for reviewed head + `f10c2d5f` failed two container scenarios with skill-ID errors before completing + all speech checks. See [run 37778575537](https://github.com/OpenVoiceOS/ovos-installer/actions/runs/37778575537). + The affected launch IDs are not changed by this patch. A new matrix run and + Raspberry Pi 5 timing are required before claiming complete device coverage. + +This is a focused review of the changed paths, not a whole-repository security audit. diff --git a/FAQ.md b/FAQ.md new file mode 100644 index 00000000..e8e12d10 --- /dev/null +++ b/FAQ.md @@ -0,0 +1,24 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Explain public fallback and safe rejected-model cleanup. + +# Frequently asked questions + +## Does local speech keep every recording offline? + +No. Failed or empty recognition sends that recording to public servers. If a +local recognizer or voice cannot run during setup, that component uses public +speech instead; the installer reports this. See [scenario settings](docs/automation.md#scenario-settings) +and [`with_public_fallback()`](ansible/roles/ovos_config/files/speech_setup.py). + +## What happens to a failed speech-model download? + +`first_working()` removes a rejected recognizer cache if the current run created +it, including after a crash or timeout. `forget_model()` preserves directories +that existed before the run. A successful retry keeps its working download. +See [implementation](ansible/roles/ovos_config/files/speech_setup.py) and +[`SpeechSetupTest.test_failed_probes_remove_only_new_downloads()`](scripts/test_speech_setup.py). + +## Do the automated tests install OVOS or download models? + +The speech setup unit tests use stub plugins and real child processes. They +exercise failure, timeout, retention and fallback behavior without an actual +installation. Real-device timing remains a separate validation step. diff --git a/MAINTENANCE_REPORT.md b/MAINTENANCE_REPORT.md new file mode 100644 index 00000000..9702fd45 --- /dev/null +++ b/MAINTENANCE_REPORT.md @@ -0,0 +1,36 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Document cleanup and speech-fallback disclosure corrections for PR 648. + +# Maintenance report + +## 2026-10-08 — PR 648 review fixes + +- `first_working()` in [speech_setup.py](ansible/roles/ovos_config/files/speech_setup.py) + now cleans newly created recognizer caches after timeout and unsuccessful + probes, using the existing protection for caches present before the run. +- [Speech setup regressions](scripts/test_speech_setup.py) cover new/existing + caches under both failure paths, public fallback and a same-model retry that + succeeds. The new failure assertions were run against the old behavior first. +- Speech selection, summary and documentation now state that local speech has a + public fallback. All 14 locale catalogs and their generated screens are updated. +- Added a [documentation index](docs/index.md) and the workspace-required + maintenance references without changing runtime dependencies or installer pins. + +### Verification + +- Pytest: **28 passed, 4 subtests passed** for speech setup and bus checks. +- BATS: **99 passed, none skipped**, covering speech/Ansible integration, + navigation, all 14 locales, real whiptail and dialog sizing. +- Real English and French speech screens checked at 80×24 and 90×32; disclosures, + choices and navigation buttons remain visible. +- Repository-wide Ruff, ShellCheck on changed shell screens, offline contract + checks, Markdown local links and diff whitespace checks pass. +- No real-device installer or model downloads were run locally. + +### Transparency Report + +- **AI Model:** GPT-6 (Codex). +- **Actions Taken:** reproduced review findings, changed cleanup and localized + disclosure, added regression assertions, reviewed the diff, ran tests and wrote + these maintenance notes. +- **Oversight:** user requested the fixes; automated and AI review were performed. + No human translation sign-off or physical Raspberry Pi test is claimed. diff --git a/QUICK_FACTS.md b/QUICK_FACTS.md new file mode 100644 index 00000000..0e31c811 --- /dev/null +++ b/QUICK_FACTS.md @@ -0,0 +1,18 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Record installer entry points and local-speech fallback behavior. + +# Quick facts + +| Field | Value | +| --- | --- | +| Package/repository | `OpenVoiceOS/ovos-installer`; shell/Ansible application, not a Python distribution | +| Version | Checked-out Git commit; `setup.sh` exports `INSTALLER_VERSION` from `git rev-parse --short=8 HEAD` | +| Entry points | `installer.sh` bootstrap; `setup.sh` orchestration; no Python package/plugin entry points | +| Speech selection | `speech_engine: public` or `local`; local includes public fallback | +| Local eligibility | Alpha, supported audio profile, capable 64-bit hardware and at least 7680 MiB RAM; see `utils/speech.sh` | +| Failed recognition | Failed or empty local recognition may send the recording to public servers | +| Failed model setup | The affected recognition or synthesis component uses public speech | +| Rejected STT downloads | New caches are removed after timeout, unsuccessful probe or excessive measured memory; existing caches are preserved | +| Test command | `pytest -q scripts/test_speech_setup.py scripts/test_local_speech_bus.py` | + +Source: [`first_working()`, `forget_model()` and `with_public_fallback()`](ansible/roles/ovos_config/files/speech_setup.py), +[`SpeechSetupTest`](scripts/test_speech_setup.py), [documentation](docs/index.md). diff --git a/README.md b/README.md index 3a55a616..735b1037 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Clarify that on-device speech includes a public-server fallback. + # Open Voice OS Installer **Your own voice assistant, on your own hardware.** Open source, @@ -24,6 +26,11 @@ speech, skills, services, the lot. - **Give it a brain.** An optional LLM fallback answers what the skills do not, pointed at whichever OpenAI-compatible endpoint you like — including one you host yourself. +- **Run speech on your device.** On a Raspberry Pi 5 with 8 GB, or anything at + least as capable, recognition and the assistant's voice can run locally on + the alpha channel. Failed or empty recognition sends the recording to public + servers. If a local model cannot run, that part uses public speech instead. + This is not a fully offline mode. See [speech settings](docs/automation.md#scenario-settings). - **Put it in every room.** HiveMind satellites share a single assistant across several devices, so the Pi in the hallway and the one in the kitchen are the same assistant. diff --git a/SUGGESTIONS.md b/SUGGESTIONS.md new file mode 100644 index 00000000..4de38806 --- /dev/null +++ b/SUGGESTIONS.md @@ -0,0 +1,13 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Record remaining local-speech validation opportunities. + +# Suggestions + +| Problem or opportunity | Proposed action | Expected impact | +| --- | --- | --- | +| Container end-to-end validation was interrupted by skill-ID errors | Resolve the image/package mismatch and rerun the exhaustive matrix, including ARM64 local speech | Confirms the installed services actually answer | +| CI runners do not establish Pi 5 latency | Install on a Pi 5 with 8 GB and time several real utterances, including an update | Confirms the advertised hardware floor and user experience | +| Localized fallback wording needs human review | Ask native speakers to review the 14 speech screens, especially Kabyle, Basque and Hindi | Makes recording uploads and public fallback understandable | + +Relevant evidence: [audit](AUDIT.md), +[`SpeechSetupTest`](scripts/test_speech_setup.py) and +[`first_working()`](ansible/roles/ovos_config/files/speech_setup.py). diff --git a/ansible/roles/ovos_config/defaults/main.yml b/ansible/roles/ovos_config/defaults/main.yml index 9d4a36a8..aa874316 100644 --- a/ansible/roles/ovos_config/defaults/main.yml +++ b/ansible/roles/ovos_config/defaults/main.yml @@ -83,6 +83,16 @@ ovos_config_homescreen_probe: >- from importlib.metadata import entry_points; print(next((e.name for g in ('opm.skill', 'ovos.plugin.skill') for e in entry_points(group=g) if 'homescreen' in e.name), '')) +# Local speech (ovos_installer_speech_engine: local) - see tasks/speech.yml. +# The public STT server stays configured as the fallback for an utterance the +# local plugin fails on or hears nothing in. +ovos_config_speech_stt_fallback: ovos-stt-plugin-server +# A recognizer that needs more than this is skipped for the next candidate: the +# machines local speech targets have 8 GB, which the rest of OVOS shares. +ovos_config_speech_stt_memory_budget_mb: "{{ ovos_installer_speech_stt_memory_budget_mb | default(3072) }}" +# The stt and tts sections the last run wrote, carried through the template so a +# re-run does not drop them only for speech.yml to put them back. +ovos_config_speech_current: {} ovos_config_mycroft_conf_owner: >- {{ ovos_installer_user diff --git a/ansible/roles/ovos_config/files/speech_setup.py b/ansible/roles/ovos_config/files/speech_setup.py new file mode 100644 index 00000000..0a64ea2c --- /dev/null +++ b/ansible/roles/ovos_config/files/speech_setup.py @@ -0,0 +1,277 @@ +"""Pick, fetch and prove the on-device speech plugins for a locale. + +Usage: speech_setup.py [STT memory budget in MB] + +For each half asked for, it tries the configuration the installed ovos-config +recommends for the locale - the files `ovos-config autoconfigure --offline` +merges - and then the plugin's own choice for the language, and keeps the first +one that works on this machine: the recognizer has to load and run within the +memory budget, the voice has to actually speak. A recommendation alone is not +enough: one names a voice the installed phoonnx does not know, and another a +model that needs 5 GB of memory. Loading is also what downloads the model, so a +kept choice is ready for the first utterance. + +Prints {"stt": section or null, "tts": section or null, "notes": [...]}. null +means nothing worked here, and that half stays on the public servers. A kept STT +section also carries the public servers ovos-config recommends for the locale, +for the fallback the installer adds to it. + +The installer parses what this prints as JSON, and ovos_utils logs to stdout, so +stdout is pointed at stderr for the whole run and the result alone goes to the +real one. +""" +import json +import os +import re +import shutil +import subprocess +import sys +from collections.abc import Iterable + +# Each candidate gets this long to download its model, load it and run once. That +# is far more than any of them needs - it is there so that a download that stalls +# without failing cannot hold the whole installation forever. +CANDIDATE_TIMEOUT = int(os.environ.get("OVOS_SPEECH_CANDIDATE_TIMEOUT", "1800")) + +kinds, lang, gender = sys.argv[1].split(","), sys.argv[2], sys.argv[3] +budget_mb = int(sys.argv[4]) if len(sys.argv) > 4 else 3072 +notes = [] + +# Each candidate is tried in a process of its own: a rejected 5 GB model then +# gives its memory back before the next one loads, instead of stacking up on an +# 8 GB board, and the peak this reports is that candidate's alone. +TRY = r''' +import json, os, resource, sys, tempfile +kind, lang, section = sys.argv[1], sys.argv[2], json.loads(sys.argv[3]) +module = section["module"] +config = dict(section.get(module) or {}, lang=lang) +if kind == "stt": + from ovos_plugin_manager.stt import load_stt_plugin + from ovos_plugin_manager.utils.audio import AudioData + plugin = load_stt_plugin(module) + if plugin is None: + sys.exit(f"{module} is not installed") + plugin(config=config).execute(AudioData(b"\0\0" * 16000, 16000, 2), language=lang) +else: + from ovos_plugin_manager.tts import load_tts_plugin + plugin = load_tts_plugin(module) + if plugin is None: + sys.exit(f"{module} is not installed") + with tempfile.TemporaryDirectory() as tmp: + wav = os.path.join(tmp, "probe.wav") + plugin(config=config).get_tts("Open Voice OS. 1, 2, 3.", wav) + if os.path.getsize(wav) <= 44: + sys.exit("the voice produced no audio") +peak = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss +print(json.dumps({"peak_mb": peak // (1048576 if sys.platform == "darwin" else 1024)})) +''' + + +def find(folder): + import ovos_config + + path = os.path.join(os.path.dirname(ovos_config.__file__), "recommends", folder) + if not os.path.isdir(path): + return None + try: + from ovos_config.utils import find_recommends_file + except ImportError: + # ovos-config 2.x: the exact tag, then a file sharing its primary subtag. + exact = os.path.join(path, f"{lang.lower()}.conf") + if os.path.isfile(exact): + return exact + primary = lang.lower().split("-")[0] + for name in sorted(os.listdir(path)): + if name.endswith(".conf") and name.lower().startswith(primary): + return os.path.join(path, name) + return None + return find_recommends_file(path, lang) + + +def recommended(folder, key): + from ovos_config.config import LocalConf + + path = find(folder) + return LocalConf(path).get(key) if path else None + + +def prefer_int8(stt): + """Ask for int8 weights where the model's repository ships them. + + A recommendation names a model, not its size: Spanish names a 1.1B model and + no quantization, which loads 4.4 GB of fp32 weights next to an int8 copy of + 1.1 GB. Without the hub (offline, older images) the section is kept as is. + """ + module = (stt or {}).get("module") + options = dict((stt or {}).get(module) or {}) + model = options.get("model") or "" + if "/" not in model or options.get("quantization"): + return stt + try: + from huggingface_hub import HfApi + files = HfApi().list_repo_files(model) + except Exception: + return stt + if any(re.search(r"[._]int8\.onnx$", name) for name in files): + options["quantization"] = "int8" + stt = dict(stt, **{module: options}) + return stt + + +def stt_candidates(): + # A recommendation that cannot be read or used costs only itself: the plugin's + # own model for the language is still tried. + try: + rec = recommended("offline_stt", "stt") + rec = prefer_int8(rec) if rec else None + except Exception as error: + notes.append(f"STT recommendation could not be loaded: {type(error).__name__}: {error}") + rec = None + if rec: + yield "ovos-config's recommendation", rec + try: + from ovos_stt_plugin_onnxasr.defaults import resolve_model + model = resolve_model(lang, {}) + except Exception: + model = None + if model: + module = "ovos-stt-plugin-onnx-asr" + yield "the plugin's own model for the language", prefer_int8( + {"module": module, "fallback_module": "", module: {"model": model}}) + + +def tts_candidates(): + # As for STT: phoonnx's own voices are still tried. + try: + rec = recommended(f"offline_{gender}", "tts") + except Exception as error: + notes.append(f"TTS recommendation could not be loaded: {type(error).__name__}: {error}") + rec = None + if rec: + yield "ovos-config's recommendation", rec + try: + from phoonnx.model_manager import TTSModelManager + manager = TTSModelManager() + manager.load() + manager.merge_default_voices() + voices = [voice.voice_id for voice in manager.get_lang_voices(lang)] + except Exception: + voices = [] + module = "ovos-tts-plugin-phoonnx" + # phoonnx lists a language's voices best first; a few tries are enough to + # get past one that a phoonnx release cannot run. + for voice in voices[:4]: + yield "phoonnx's own voice for the language", {"module": module, module: {"voice": voice}} + + +def model_of(section): + options = section.get(section["module"]) or {} + return options.get("model") or options.get("voice") or section["module"] + + +def model_cache(model): + """Where the recognizer plugin keeps `model`, or None when that cannot be told.""" + try: + from ovos_utils.xdg_utils import xdg_data_home + except ImportError: + return None + return os.path.join(xdg_data_home(), "ovos_stt_plugin_onnxasr", model.replace("/", "--")) + + +def forget_model(model, existed_before): + """Delete a rejected recognizer model this run downloaded, so it does not hold the + disk until uninstall. One that was there before this run is not this run's to + delete: an earlier install's local section can name the very model tried here.""" + path = model_cache(model) + if path and not existed_before: + shutil.rmtree(path, ignore_errors=True) + + +# Whether each recognizer model's directory was there when this run first looked. +# The same model can be tried twice, as int8 and as fp32, and the second try must not +# take the first try's download for one that was there before. +there_at_first_sight = {} + + +def first_working(kind: str, candidates: Iterable[tuple[str, dict]]) -> dict | None: + """Keep the first working candidate and remove only this run's rejected STT downloads.""" + tried = set() + for origin, section in candidates: + model = model_of(section) + # The same model with other settings is another candidate: a recommendation + # can ask for int8 weights a repository does not have, and the plugin's own + # choice of that model without them then still gets its try. + key = json.dumps(section, sort_keys=True) + if key in tried: + continue + tried.add(key) + cache = model_cache(model) if kind == "stt" else None + if cache and cache not in there_at_first_sight: + there_at_first_sight[cache] = os.path.exists(cache) + existed_before = bool(cache) and there_at_first_sight[cache] + try: + run = subprocess.run([sys.executable, "-c", TRY, kind, lang, json.dumps(section)], + capture_output=True, text=True, timeout=CANDIDATE_TIMEOUT) + except subprocess.TimeoutExpired: + notes.append(f"{kind.upper()} {model} ({origin}) was given up on after " + f"{CANDIDATE_TIMEOUT} seconds") + if kind == "stt": + forget_model(model, existed_before) + continue + lines = [line for line in run.stdout.splitlines() if line.startswith("{")] + if run.returncode != 0 or not lines: + reason = (run.stderr.strip().splitlines() or ["it did not start"])[-1] + notes.append(f"{kind.upper()} {model} ({origin}) does not work here: {reason[:200]}") + if kind == "stt": + forget_model(model, existed_before) + continue + peak = json.loads(lines[-1])["peak_mb"] + if kind == "stt" and peak > budget_mb: + notes.append(f"STT {model} ({origin}) needs {peak} MB, above the {budget_mb} MB budget") + forget_model(model, existed_before) + continue + if origin != "ovos-config's recommendation": + notes.append(f"{kind.upper()} uses {model}, {origin}") + return section + return None + + +def with_public_fallback(stt): + """Keep the locale's public STT servers in the section, for its fallback. + + The section replaces the whole of mycroft.conf's stt, and with it the servers + `ovos-config autoconfigure --online` picked for the language - Spanish gets one + with a Spanish model. Without them the fallback would use the plugin's default + servers instead. + """ + online = recommended("online_stt", "stt") or {} + module = online.get("module") + if stt is None or not module or not online.get(module): + return stt + return dict(stt, **{module: online[module]}) + + +def set_up(kind, candidates): + """first_working(), except that a fault in this script costs the half, not the run.""" + try: + return first_working(kind, candidates()) + except Exception as error: + notes.append(f"{kind.upper()} could not be set up: {type(error).__name__}: {error}") + return None + + +result_stream = os.fdopen(os.dup(1), "w") +os.dup2(2, 1) + +result = {"stt": None, "tts": None} +if "stt" in kinds: + result["stt"] = set_up("stt", stt_candidates) + try: + result["stt"] = with_public_fallback(result["stt"]) + except Exception as error: + notes.append(f"STT fallback keeps the default public servers: {error}") +if "tts" in kinds: + result["tts"] = set_up("tts", tts_candidates) +result["notes"] = notes +result_stream.write(json.dumps(result) + "\n") +result_stream.flush() diff --git a/ansible/roles/ovos_config/tasks/install.yml b/ansible/roles/ovos_config/tasks/install.yml index 0d972493..f8f298c7 100644 --- a/ansible/roles/ovos_config/tasks/install.yml +++ b/ansible/roles/ovos_config/tasks/install.yml @@ -71,10 +71,11 @@ ovos_config_spoken_time_format: "half" when: ovos_config_geoip_country | default('') in ovos_config_geoip_half_time_countries -# Read the configured homescreen back before the template overwrites the file, so -# a re-run keeps the id the last run resolved instead of dropping it and waiting -# for the post-install step to put it back (which would make every run changed). -- name: Read the configured homescreen skill id +# Read the configured homescreen and local speech back before the template +# overwrites the file, so a re-run keeps what the last run resolved instead of +# dropping it and waiting for the post-install steps to put it back (which would +# make every run changed). +- name: Read the configuration the last run wrote ansible.builtin.slurp: src: "{{ ovos_config_mycroft_conf_path }}" register: ovos_config_existing_conf @@ -103,6 +104,23 @@ ovos_config_idle_display_skill: "" changed_when: false +- name: Keep the local speech configuration this host already resolved + when: + - ovos_config_existing_conf.content is defined + - (ovos_installer_speech_engine | default('public')) == 'local' + block: + - name: Read the speech sections out of the current configuration + vars: + _existing: "{{ ovos_config_existing_conf.content | b64decode | from_json }}" + ansible.builtin.set_fact: + ovos_config_speech_current: "{{ _existing | dict2items | selectattr('key', 'in', ['stt', 'tts']) | items2dict }}" + changed_when: false + rescue: + - name: Start over when the current configuration cannot be read + ansible.builtin.set_fact: + ovos_config_speech_current: {} + changed_when: false + - name: Generate mycroft.conf ansible.builtin.template: src: mycroft.conf.j2 diff --git a/ansible/roles/ovos_config/tasks/speech.yml b/ansible/roles/ovos_config/tasks/speech.yml new file mode 100644 index 00000000..bd5c6016 --- /dev/null +++ b/ansible/roles/ovos_config/tasks/speech.yml @@ -0,0 +1,191 @@ +--- +# Local speech: STT and TTS run on this host. speech_setup.py picks the plugins and +# models - ovos-config's recommendation for the locale, or the plugin's own choice +# for the language when that does not work here - downloads them, and proves they +# run before anything is written: a voice the installed phoonnx cannot speak, or a +# model too big for the machine, would otherwise leave a device mute or deaf. Only +# the stt and tts sections are taken: `ovos-config autoconfigure` also merges +# per-language defaults for units, wake words and intents, which this installer +# sets itself. +# +# A half that does not work here stays on the public servers: whatever the caller +# had there before this ran - the public defaults, or the per-language public +# servers `ovos-config autoconfigure --online` wrote - is left alone, and a local +# section an earlier run wrote is removed rather than kept failing. A setup that +# cannot run at all ends the same way, with the reason printed, rather than ending +# the installation over an optional feature. +# +# The caller says where OVOS runs: ovos_config_speech_python, the virtualenv's +# Python, or ovos_config_speech_containers, the names of the listener and audio +# containers. The write is registered as ovos_config_speech_configuration: the +# services only notice a mycroft.conf that is rewritten in place, and this one is +# replaced, so the caller restarts them when it changed. +- name: Pick, download and check the local speech plugins + become: true + become_user: "{{ ovos_installer_user }}" + ansible.builtin.command: + argv: + - "{{ ovos_config_speech_python }}" + - -c + - "{{ lookup('ansible.builtin.file', 'speech_setup.py') }}" + - stt,tts + - "{{ ovos_installer_locale }}" + - "{{ 'female' if ovos_installer_ovos_config_tts_gender == '--female' else 'male' }}" + - "{{ ovos_config_speech_stt_memory_budget_mb }}" + register: ovos_config_speech_setup + changed_when: false + failed_when: false + when: ovos_config_speech_containers is not defined + +# Each half is set up where it will run - the recognizer in the listener, the +# voice in the audio service - so each model lands in that container's volume +# (ovos_stt_models, ovos_tts_models). +- name: Pick, download and check the local speech plugins in their containers + become: true + become_user: "{{ ovos_installer_user }}" + community.docker.docker_container_exec: + container: "{{ ovos_config_speech_containers[item.container] }}" + argv: + - python3 + - -c + - "{{ lookup('ansible.builtin.file', 'speech_setup.py') }}" + - "{{ item.kind }}" + - "{{ ovos_installer_locale }}" + - "{{ 'female' if ovos_installer_ovos_config_tts_gender == '--female' else 'male' }}" + - "{{ ovos_config_speech_stt_memory_budget_mb }}" + loop: + - { kind: stt, container: listener } + - { kind: tts, container: audio } + loop_control: + label: "{{ item.kind }}" + register: ovos_config_speech_setup_containers + changed_when: false + failed_when: false + # The listener and audio containers only exist in the desktop profiles. + when: + - ovos_config_speech_containers is defined + - ovos_containers_is_desktop_profile | bool + +# A failed local recognition, or one that hears nothing, goes to the public +# server for that utterance instead of leaving the user unanswered. +- name: Keep what works on this machine + vars: + _runs: >- + {{ + ovos_config_speech_setup_containers.results + if ovos_config_speech_containers is defined + else [ovos_config_speech_setup] + }} + # The result is the last line the script prints. A run that failed, or printed + # no result, keeps nothing and says why. + _results: >- + {%- set results = [] -%} + {%- for run in _runs -%} + {%- set last = ((run.stdout | default('', true) | trim).split('\n') | last) -%} + {%- if (run.rc | default(1)) == 0 and last.startswith('{') -%} + {%- set _ = results.append(last | from_json) -%} + {%- else -%} + {%- set reason = (run.stderr | default(run.msg | default(''), true) | trim).split('\n') | last -%} + {%- set _ = results.append({'stt': none, 'tts': none, 'notes': [ + 'The local speech setup did not finish: ' ~ (reason | default('no output', true))[:300]]}) -%} + {%- endif -%} + {%- endfor -%} + {{ results }} + _stt: "{{ _results | map(attribute='stt') | select | list | first | default(none) }}" + _tts: "{{ _results | map(attribute='tts') | select | list | first | default(none) }}" + ansible.builtin.set_fact: + ovos_config_speech_wanted: >- + {{ + ({'stt': _stt | combine({'fallback_module': ovos_config_speech_stt_fallback})} + if _stt else {}) + | combine({'tts': _tts} if _tts else {}) + }} + ovos_config_speech_notes: "{{ _results | map(attribute='notes') | flatten }}" + +- name: Report what the local speech setup had to work around + ansible.builtin.debug: + msg: "{{ item }}" + loop: "{{ ovos_config_speech_notes }}" + +- name: Report what stays on the public servers + ansible.builtin.debug: + msg: >- + No on-device {{ item | upper }} works for {{ ovos_installer_locale }} on this + machine, so {{ item | upper }} stays on the public servers. + loop: "{{ ['stt', 'tts'] | difference(ovos_config_speech_wanted.keys() | list) }}" + +# Read even when nothing works here: a local section an earlier run wrote may +# have to go. +- name: Read mycroft.conf for the speech configuration + ansible.builtin.slurp: + src: "{{ ovos_config_mycroft_conf_path }}" + register: ovos_config_speech_conf + failed_when: false + changed_when: false + +# Same reasoning as homescreen.yml: a hand-edited mycroft.conf that is not +# plain JSON is not a reason to end the install. +- name: Parse mycroft.conf + when: ovos_config_speech_conf.content is defined + block: + - name: Read the current configuration + ansible.builtin.set_fact: + ovos_config_speech_conf_current: "{{ ovos_config_speech_conf.content | b64decode | from_json }}" + ovos_config_speech_conf_readable: true + changed_when: false + rescue: + - name: Note that mycroft.conf could not be read as JSON + ansible.builtin.set_fact: + ovos_config_speech_conf_readable: false + changed_when: false + + - name: Report the speech configuration that has to be set by hand + ansible.builtin.debug: + msg: >- + {{ ovos_config_mycroft_conf_path }} is not plain JSON, so local speech + was not configured and the public servers stay in use. To switch, add + these sections to it by hand: {{ ovos_config_speech_wanted | to_json }} + +# A half with nothing that works here keeps a public server section, and loses +# any other: in the virtualenv that leaves OVOS's own defaults, the public +# servers, and in containers autoconfigure --online has just written the public +# section for the locale, which stays. +- name: Write the local speech configuration + vars: + _conf: "{{ ovos_config_speech_conf_current | default({}, true) }}" + _public_modules: + - ovos-stt-plugin-server + - ovos-tts-plugin-server + _stale: >- + {{ + ['stt', 'tts'] + | reject('in', ovos_config_speech_wanted) + | select('in', _conf) + | reject('in', _conf | dict2items + | selectattr('value', 'mapping') + | selectattr('value.module', 'defined') + | selectattr('value.module', 'in', _public_modules) + | map(attribute='key') | list) + | list + }} + _final: >- + {{ _conf | dict2items | rejectattr('key', 'in', _stale) | items2dict + | combine(ovos_config_speech_wanted) }} + ansible.builtin.copy: + content: "{{ _final | to_nice_json }}\n" + dest: "{{ ovos_config_mycroft_conf_path }}" + owner: "{{ ovos_config_mycroft_conf_owner }}" + group: "{{ ovos_config_mycroft_conf_group }}" + mode: "{{ ovos_config_mycroft_conf_mode }}" + register: ovos_config_speech_configuration + when: + - ovos_config_speech_conf_readable | default(false) | bool + - _conf is mapping + - _final != _conf + +- name: Record where speech runs + vars: + _applied: "{{ ovos_config_speech_conf_readable | default(false) | bool }}" + ansible.builtin.set_fact: + ovos_config_speech_stt_engine: "{{ 'local' if _applied and 'stt' in ovos_config_speech_wanted else 'public' }}" + ovos_config_speech_tts_engine: "{{ 'local' if _applied and 'tts' in ovos_config_speech_wanted else 'public' }}" diff --git a/ansible/roles/ovos_config/templates/mycroft.conf.j2 b/ansible/roles/ovos_config/templates/mycroft.conf.j2 index b5cf47ae..5c98a602 100644 --- a/ansible/roles/ovos_config/templates/mycroft.conf.j2 +++ b/ansible/roles/ovos_config/templates/mycroft.conf.j2 @@ -146,6 +146,13 @@ } {% endif %} }, +{% endif %} +{# Local speech is written by tasks/speech.yml once the virtualenv can say what + ovos-config recommends; a re-run carries the last result through here. #} +{% if (ovos_installer_speech_engine | default('public')) == 'local' %} +{% for _speech_section, _speech_value in (ovos_config_speech_current | default({}, true)).items() %} + "{{ _speech_section }}": {{ _speech_value | to_json }}, +{% endfor %} {% endif %} "websocket": { "host": "{{ '0.0.0.0' if ovos_installer_method == "containers" else '127.0.0.1' }}", diff --git a/ansible/roles/ovos_containers/defaults/main.yml b/ansible/roles/ovos_containers/defaults/main.yml index 5d87ae25..01600025 100644 --- a/ansible/roles/ovos_containers/defaults/main.yml +++ b/ansible/roles/ovos_containers/defaults/main.yml @@ -161,6 +161,15 @@ ovos_containers_uninstall_name_prefixes: # use an "ovos_" name prefix. ovos_containers_uninstall_name_excludes: >- {{ ovos_installer_uninstall_name_excludes | default(['ovos_stt', 'ovos_tts']) }} +# Volumes ovos-docker's own compose files declare whose names happen to match an +# exclude pattern: the on-device speech models (gigabytes) and the TTS cache. They +# belong to the stack, so the sweep removes them like its other volumes. Without +# this, they only went while the composition directory existed - an uninstall +# after a reboot left them behind. +ovos_containers_uninstall_volume_exceptions: + - ovos_stt_models + - ovos_tts_models + - ovos_tts_cache # Remove the images backing the removed containers. Follows the docker-compose # "remove_images" setting so both removal paths behave the same way. ovos_containers_uninstall_remove_images: >- @@ -181,6 +190,13 @@ ovos_containers_hivemind_client_ids: >- {{ ovos_containers_hivemind_clients.stdout_lines | default([]) | select('match', '^[0-9]+,') | map('split', ',') | map('first') | list }} ovos_containers_container_ovos_cli: ovos_cli +ovos_containers_container_ovos_listener: ovos_listener +ovos_containers_container_ovos_audio: ovos_audio +# Local speech (ovos_installer_speech_engine: local) raises the limits ovos-docker +# gives the listener and the audio service: onnx-asr's recognition model alone +# peaks around 1.2 GB, and some languages get larger models than that. +ovos_containers_speech_listener_memory_limit: "{{ ovos_installer_speech_listener_memory_limit | default('4G') }}" +ovos_containers_speech_audio_memory_limit: "{{ ovos_installer_speech_audio_memory_limit | default('1G') }}" # Service and container names from ovos-docker's docker-compose.skills-extra.yml. # Keep them overridable in case upstream changes. ovos_containers_service_skill_homeassistant: ovos_skill_homeassistant diff --git a/ansible/roles/ovos_containers/tasks/composer.yml b/ansible/roles/ovos_containers/tasks/composer.yml index 16768c3c..c82b93ee 100644 --- a/ansible/roles/ovos_containers/tasks/composer.yml +++ b/ansible/roles/ovos_containers/tasks/composer.yml @@ -60,13 +60,91 @@ - not ansible_check_mode - ovos_installer_profile == "satellite" +# Local speech needs two things from ovos-docker, and both are checked rather than +# assumed. The compose files of the pinned release have to give the listener room +# for the recognition model and a volume to keep it in - with less, the model gets +# the listener OOM-killed - and the images this stack just pulled have to carry the +# onnx-asr and phoonnx plugins, or the listener cannot load its recognizer. Short +# of either, speech stays on the public servers. +- name: Read the compose files this stack runs + ansible.builtin.slurp: + src: "{{ ovos_containers_composition_directory }}/{{ item }}" + loop: "{{ ovos_containers_composition_files }}" + register: ovos_containers_composition_contents + when: + - (ovos_installer_speech_engine | default('public')) == 'local' + - ovos_containers_is_desktop_profile | bool + +- name: Check the listener and audio images carry the local speech plugins + become: true + become_user: "{{ ovos_installer_user }}" + community.docker.docker_container_exec: + container: "{{ item.container }}" + argv: + - python3 + - -c + - "{{ item.probe }}" + loop: + - container: "{{ ovos_containers_container_ovos_listener }}" + probe: >- + from ovos_plugin_manager.stt import find_stt_plugins; + raise SystemExit(0 if 'ovos-stt-plugin-onnx-asr' in find_stt_plugins() else 1) + - container: "{{ ovos_containers_container_ovos_audio }}" + probe: >- + from ovos_plugin_manager.tts import find_tts_plugins; + raise SystemExit(0 if 'ovos-tts-plugin-phoonnx' in find_tts_plugins() else 1) + loop_control: + label: "{{ item.container }}" + register: ovos_containers_speech_plugins + failed_when: false + changed_when: false + when: + - not ansible_check_mode + - (ovos_installer_speech_engine | default('public')) == 'local' + - ovos_containers_is_desktop_profile | bool + +- name: Decide where speech runs + vars: + _compose: >- + {{ ovos_containers_composition_contents.results | default([]) + | map(attribute='content', default='') | map('b64decode') | join('\n') }} + _compose_ready: "{{ 'LISTENER_MEMORY_LIMIT' in _compose and 'ovos_stt_models' in _compose }}" + _plugins_ready: >- + {{ (ovos_containers_speech_plugins.results | default([]) | length) == 2 + and (ovos_containers_speech_plugins.results | map(attribute='rc', default=1) | max) == 0 }} + ansible.builtin.set_fact: + ovos_containers_speech_engine: >- + {{ + 'local' + if (ovos_installer_speech_engine | default('public')) == 'local' + and _compose_ready | bool and _plugins_ready | bool + else 'public' + }} + +- name: Say why speech stays on the public servers + ansible.builtin.debug: + msg: >- + The ovos-docker release this installer runs, or the images it pulled, cannot + run speech on the device yet, so speech stays on the public servers. Re-run + the installer once they can. + when: + - (ovos_installer_speech_engine | default('public')) == 'local' + - ovos_containers_speech_engine == 'public' + +# Always --online, for local speech too. The public servers are the baseline for +# both halves, and local speech (below) replaces each half it proves works here. +# --offline would leave a half that fails that proof on the offline +# recommendation that just failed it - a Spanish voice the installed phoonnx +# cannot speak, which leaves the device mute. And never neither: without a mode +# autoconfigure picks its hybrid one (voice on the device, recognition on the +# public servers), which is not one of the two choices the installer offers. - name: Merge ovos-config language recommendations into mounted config become: true become_user: "{{ ovos_installer_user }}" community.docker.docker_container_exec: container: "{{ ovos_containers_container_ovos_cli }}" command: >- - ovos-config autoconfigure --lang {{ ovos_installer_locale }} + ovos-config autoconfigure --lang {{ ovos_installer_locale }} --online {{ ovos_installer_ovos_config_tts_gender }} when: - not ansible_check_mode @@ -77,6 +155,44 @@ # container ovos_cli" after the stack is already up. - ovos_containers_is_desktop_profile | bool +- name: Configure local speech + ansible.builtin.include_role: + name: ovos_config + tasks_from: speech.yml + vars: + ovos_config_speech_containers: + cli: "{{ ovos_containers_container_ovos_cli }}" + listener: "{{ ovos_containers_container_ovos_listener }}" + audio: "{{ ovos_containers_container_ovos_audio }}" + when: + - not ansible_check_mode + - ovos_containers_speech_engine == 'local' + - ovos_containers_is_desktop_profile | bool + +# The listener and the audio service are already running on what autoconfigure +# wrote, and they only reload a mycroft.conf rewritten in place. Local speech +# replaces the file, which their watcher never sees, so without a restart they +# would stay on the public servers until the next boot. +- name: Restart the listener and audio services on their local speech + become: true + become_user: "{{ ovos_installer_user }}" + community.docker.docker_compose_v2: + project_src: "{{ ovos_containers_composition_directory }}" + project_name: "{{ ovos_containers_compose_project_name }}" + files: "{{ ovos_containers_composition_files }}" + services: + - "{{ ovos_containers_container_ovos_listener }}" + - "{{ ovos_containers_container_ovos_audio }}" + state: restarted + environment: + DOCKER_TIMEOUT: "{{ ovos_containers_docker_timeout }}" + when: + - not ansible_check_mode + - ovos_containers_speech_engine == 'local' + - ovos_containers_is_desktop_profile | bool + - ovos_config_speech_configuration is defined + - ovos_config_speech_configuration is changed + - name: Compute optional skills flags vars: _ovos_homeassistant_url: >- diff --git a/ansible/roles/ovos_containers/tasks/uninstall.yml b/ansible/roles/ovos_containers/tasks/uninstall.yml index d05c4ebb..1464dc4d 100644 --- a/ansible/roles/ovos_containers/tasks/uninstall.yml +++ b/ansible/roles/ovos_containers/tasks/uninstall.yml @@ -107,10 +107,11 @@ _ovos_containers_uninstall_volumes_raw: >- {% set projects = ovos_containers_uninstall_project_names | default([]) %} {% set exclude_re = _ovos_containers_uninstall_exclude_re | trim %} + {% set exceptions = ovos_containers_uninstall_volume_exceptions | default([]) %} {% set ns = namespace(names=[]) %} {% for v in (ovos_containers_host_info.volumes | default([])) %} {% set labels = v.Labels | default({}) or {} %} - {% set excluded = exclude_re | length > 0 and v.Name is match(exclude_re) %} + {% set excluded = exclude_re | length > 0 and v.Name is match(exclude_re) and v.Name not in exceptions %} {% if labels.get(ovos_containers_uninstall_compose_project_label, '') in projects and not excluded %} {% set _ = ns.names.append(v.Name) %} {% endif %} diff --git a/ansible/roles/ovos_containers/templates/docker/env.j2 b/ansible/roles/ovos_containers/templates/docker/env.j2 index 972ae65e..cbcc6473 100644 --- a/ansible/roles/ovos_containers/templates/docker/env.j2 +++ b/ansible/roles/ovos_containers/templates/docker/env.j2 @@ -12,6 +12,9 @@ '{{ text }}' {%- endif -%} {%- endmacro -%} +{% if (ovos_installer_speech_engine | default('public')) == 'local' %} +AUDIO_MEMORY_LIMIT={{ env(ovos_containers_speech_audio_memory_limit) }} +{% endif %} {% if ovos_installer_display_server != 'N/A' %} DISPLAY={{ env(ovos_installer_display_server_number if ovos_installer_display_server_number is defined else ovos_containers_display_default) }} {% endif %} @@ -30,6 +33,9 @@ I2C_GID={{ env(getent_group['i2c'][1]) }} {% if 'input' in getent_group %} INPUT_GID={{ env(getent_group['input'][1]) }} {% endif %} +{% if (ovos_installer_speech_engine | default('public')) == 'local' %} +LISTENER_MEMORY_LIMIT={{ env(ovos_containers_speech_listener_memory_limit) }} +{% endif %} OVOS_CONFIG_FOLDER={{ env(ovos_containers_ovos_config_dir) }} OVOS_CONFIG_PHAL_FOLDER={{ env(ovos_containers_ovos_config_phal_dir) }} OVOS_PERSONA_FOLDER={{ env(ovos_containers_ovos_persona_dir) }} diff --git a/ansible/roles/ovos_installer/defaults/main.yml b/ansible/roles/ovos_installer/defaults/main.yml index 63edfa98..da2f93e3 100644 --- a/ansible/roles/ovos_installer/defaults/main.yml +++ b/ansible/roles/ovos_installer/defaults/main.yml @@ -43,7 +43,10 @@ ovos_installer_footprint_shared_dirs: owned: >- {{ ovos_installer_footprint_hf_models | map('regex_replace', '^', 'hub/') | list + ovos_installer_footprint_hf_models | map('regex_replace', '^', 'hub/.locks/') | list }} - bookkeeping: [".*", "xet", "hub/.locks", "hub/CACHEDIR.TAG", "hub/version.txt"] + # A repository's refs and its record of files that do not exist hold no model: + # huggingface_hub writes them even for a download into a directory of its own, + # which is how the local recognizer fetches its model. + bookkeeping: [".*", "xet", "hub/.locks", "hub/CACHEDIR.TAG", "hub/version.txt", "hub/*--*/refs", "hub/*--*/.no_exist"] store: hub/blobs owned_even_if_there_before: true - path: "{{ ovos_installer_user_home }}/.local/share/uv" @@ -225,6 +228,11 @@ ovos_installer_mark2_vad_pre_wake_enabled: false ovos_installer_listener_multiplier: 1.5 ovos_installer_tuning_source_volume: 1.0 ovos_installer_cpu_is_capable: false +# Where STT and TTS run: "public" (the Open Voice OS servers, the default) or +# "local" (on this host, see ovos_config/tasks/speech.yml). setup.sh only passes +# local for alpha installs on hardware that can run it. +ovos_installer_speech_engine: public +ovos_installer_local_speech_capable: false ovos_installer_locale: "en-us" ovos_installer_feature_skills: true ovos_installer_feature_extra_skills: false diff --git a/ansible/roles/ovos_installer/tasks/assert.yml b/ansible/roles/ovos_installer/tasks/assert.yml index 51eaeddc..3171518a 100644 --- a/ansible/roles/ovos_installer/tasks/assert.yml +++ b/ansible/roles/ovos_installer/tasks/assert.yml @@ -31,6 +31,25 @@ - not (ovos_installer_is_cleaning | bool) - ovos_installer_feature_gui | bool +- name: Assert the speech engine is one the installer knows + ansible.builtin.assert: + that: + - ovos_installer_speech_engine in ['local', 'public'] + fail_msg: "ovos_installer_speech_engine must be local or public." + +- name: Assert local speech constraints + ansible.builtin.assert: + that: + - ovos_installer_channel == 'alpha' + - ovos_installer_profile != 'server' + - ovos_installer_method == 'virtualenv' or ovos_installer_profile in ['ovos', 'listener'] + fail_msg: >- + Local speech is only available on alpha installs whose profile has audio, + and with containers only for the ovos and listener profiles. + when: + - not (ovos_installer_is_cleaning | bool) + - ovos_installer_speech_engine == 'local' + - name: Assert containers required variables ansible.builtin.assert: that: diff --git a/ansible/roles/ovos_services/defaults/main.yml b/ansible/roles/ovos_services/defaults/main.yml index d063986d..307c1a4c 100644 --- a/ansible/roles/ovos_services/defaults/main.yml +++ b/ansible/roles/ovos_services/defaults/main.yml @@ -243,6 +243,7 @@ ovos_services_remove_directories: - "{{ ovos_installer_user_home }}/.cache/mycroft" - "{{ ovos_installer_user_home }}/.cache/ovos_gui" - "{{ ovos_installer_user_home }}/.cache/ovos-installer" + - "{{ ovos_installer_user_home }}/.cache/phoonnx" - "{{ ovos_installer_user_home }}/.config/mycroft" - "{{ ovos_installer_user_home }}/.config/hivemind" - "{{ ovos_installer_user_home }}/.config/hivemind-core" @@ -256,6 +257,8 @@ ovos_services_remove_directories: - "{{ ovos_installer_user_home }}/.local/share/mycroft" - "{{ ovos_installer_user_home }}/.local/share/hivemind-core" - "{{ ovos_installer_user_home }}/.local/share/OpenVoiceOS" + - "{{ ovos_installer_user_home }}/.local/share/ovos_stt_plugin_onnxasr" + - "{{ ovos_installer_user_home }}/.local/share/phoonnx" - "{{ ovos_installer_user_home }}/.local/share/precise-onnx" - "{{ ovos_installer_user_home }}/.local/share/precise-lite" - "{{ ovos_installer_user_home }}/.local/share/wallpapers" diff --git a/ansible/roles/ovos_services/tasks/systemd.yml b/ansible/roles/ovos_services/tasks/systemd.yml index c240f424..4af18b8b 100644 --- a/ansible/roles/ovos_services/tasks/systemd.yml +++ b/ansible/roles/ovos_services/tasks/systemd.yml @@ -82,8 +82,16 @@ - name: Enable and start OVOS and/or HiveMind systemd units vars: + # Local speech rewrites mycroft.conf after the template, and by replacing it, + # which running services do not notice: a run that switches to it, or changes + # what it picked, restarts them too. ovos_installer_systemd_state: >- - {{ 'restarted' if (ovos_config_configuration.changed | default(false)) | bool else 'started' }} + {{ + 'restarted' + if (ovos_config_configuration.changed | default(false)) | bool + or (ovos_config_speech_configuration.changed | default(false)) | bool + else 'started' + }} ovos_installer_ovos_scope: "{{ ovos_installer_systemd_scope | default('user') }}" ovos_installer_ovos_unit_user: "{{ 'root' if ovos_installer_ovos_scope == 'system' else ovos_installer_user }}" ovos_services_unit_user: "{{ 'root' if item.scope == 'system' else ovos_installer_ovos_unit_user }}" diff --git a/ansible/roles/ovos_telemetry/defaults/main.yml b/ansible/roles/ovos_telemetry/defaults/main.yml index 4cad0ba5..c23a3e1d 100644 --- a/ansible/roles/ovos_telemetry/defaults/main.yml +++ b/ansible/roles/ovos_telemetry/defaults/main.yml @@ -2,6 +2,11 @@ ovos_telemetry_feature_gui: "{{ ovos_installer_feature_gui | default(false) }}" ovos_telemetry_feature_homeassistant: "{{ ovos_installer_feature_homeassistant | default(false) }}" ovos_telemetry_feature_llm: "{{ ovos_installer_feature_llm | default(false) }}" +# Where speech ended up, not what was asked for: a locale the installed +# ovos-config recommends no on-device model for stays public when local was +# picked. ovos_config/tasks/speech.yml sets these; every other run is public. +ovos_telemetry_stt_engine: "{{ ovos_config_speech_stt_engine | default('public') }}" +ovos_telemetry_tts_engine: "{{ ovos_config_speech_tts_engine | default('public') }}" ovos_telemetry_geoip_url: http://ip-api.com/json ovos_telemetry_url: "https://telemetry.smartgic.io/ovos-installer/metrics/" diff --git a/ansible/roles/ovos_telemetry/tasks/main.yml b/ansible/roles/ovos_telemetry/tasks/main.yml index 8233f31b..cc265964 100644 --- a/ansible/roles/ovos_telemetry/tasks/main.yml +++ b/ansible/roles/ovos_telemetry/tasks/main.yml @@ -76,6 +76,9 @@ gui_feature: "{{ ovos_telemetry_feature_gui | default(false) | bool }}" llm_feature: "{{ ovos_telemetry_feature_llm | default(false) | bool }}" cpu_capable: "{{ ovos_installer_cpu_is_capable | default(false) | bool }}" + local_speech_capable: "{{ ovos_installer_local_speech_capable | default(false) | bool }}" + stt_engine: "{{ ovos_telemetry_stt_engine }}" + tts_engine: "{{ ovos_telemetry_tts_engine }}" tuning_enabled: "{{ (ovos_installer_tuning | default('no')) == 'yes' }}" country: "{{ _telemetry_country }}" hardware: >- diff --git a/ansible/roles/ovos_virtualenv/tasks/venv.yml b/ansible/roles/ovos_virtualenv/tasks/venv.yml index 400ee345..6a0a2ebf 100644 --- a/ansible/roles/ovos_virtualenv/tasks/venv.yml +++ b/ansible/roles/ovos_virtualenv/tasks/venv.yml @@ -502,6 +502,11 @@ - autoconfigure - --lang - "{{ ovos_installer_locale }}" + # Always --online: local speech replaces each half it proves works here + # afterwards, and a half that does not stays public instead of on an offline + # recommendation that just failed. Never neither: without a mode autoconfigure + # picks its hybrid one, which is not one of the two choices the installer offers. + - --online - "{{ ovos_installer_ovos_config_tts_gender }}" environment: PATH: "{{ ovos_virtualenv_path }}/bin" @@ -509,3 +514,15 @@ changed_when: false when: - ovos_installer_ovos_config_autoconfigure_enabled | default(false) | bool + +# The plugins and models come from the ovos-config this virtualenv now holds, +# so local speech can only be configured once it is installed. +- name: Configure local speech + ansible.builtin.include_role: + name: ovos_config + tasks_from: speech.yml + vars: + ovos_config_speech_python: "{{ ovos_virtualenv_path }}/bin/python" + when: + - not (ovos_virtualenv_is_cleaning | bool) + - (ovos_installer_speech_engine | default('public')) == 'local' diff --git a/ansible/roles/ovos_virtualenv/templates/virtualenv/core-requirements.txt.j2 b/ansible/roles/ovos_virtualenv/templates/virtualenv/core-requirements.txt.j2 index f95e13c9..649a2fb3 100644 --- a/ansible/roles/ovos_virtualenv/templates/virtualenv/core-requirements.txt.j2 +++ b/ansible/roles/ovos_virtualenv/templates/virtualenv/core-requirements.txt.j2 @@ -54,6 +54,14 @@ ovos-phal-plugin-ipgeo ovos-tts-plugin-piper {% endif %} ovos-tts-plugin-polly +{% if (ovos_installer_speech_engine | default('public')) == 'local' %} +# Local speech: the STT and TTS plugins ovos-config recommends for running on +# this host; ovos_config/tasks/speech.yml picks their models. onnx-asr fetches +# models through huggingface_hub, which only its hub extra brings in. +ovos-stt-plugin-onnx-asr +onnx-asr[cpu,hub] +phoonnx +{% endif %} {% if ovos_installer_locale == "ca-es" %} ovos-tts-plugin-matxa-multispeaker-cat diff --git a/ansible/roles/ovos_virtualenv/templates/virtualenv/satellite-requirements.txt.j2 b/ansible/roles/ovos_virtualenv/templates/virtualenv/satellite-requirements.txt.j2 index afef28d3..78effe82 100644 --- a/ansible/roles/ovos_virtualenv/templates/virtualenv/satellite-requirements.txt.j2 +++ b/ansible/roles/ovos_virtualenv/templates/virtualenv/satellite-requirements.txt.j2 @@ -46,6 +46,14 @@ ovos-phal-plugin-ipgeo ovos-tts-plugin-piper {% endif %} ovos-tts-plugin-polly +{% if (ovos_installer_speech_engine | default('public')) == 'local' %} +# Local speech: the STT and TTS plugins ovos-config recommends for running on +# this host; ovos_config/tasks/speech.yml picks their models. onnx-asr fetches +# models through huggingface_hub, which only its hub extra brings in. +ovos-stt-plugin-onnx-asr +onnx-asr[cpu,hub] +phoonnx +{% endif %} {% if ovos_installer_locale == "ca-es" %} ovos-tts-plugin-matxa-multispeaker-cat diff --git a/docs/automation.md b/docs/automation.md index 4c44734c..6b2d3b56 100644 --- a/docs/automation.md +++ b/docs/automation.md @@ -1,3 +1,5 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Document public fallbacks for local speech scenarios. + # Automation Installing without answering questions, for scripted installs or several @@ -27,7 +29,9 @@ EOF ``` Then run the installer as usual. More examples live in -[scenarios/](https://github.com/OpenVoiceOS/ovos-installer/tree/main/scenarios). +[scenarios/](https://github.com/OpenVoiceOS/ovos-installer/tree/main/scenarios), +including one that runs speech recognition and the voice on a Raspberry Pi 5 +([scenario-local-speech.yml](https://github.com/OpenVoiceOS/ovos-installer/blob/main/scenarios/scenario-local-speech.yml)). ## Scenario settings @@ -44,10 +48,18 @@ Then run the installer as usual. More examples live in | `llm.key` | API key for that endpoint, required with `features.llm` | | `llm.model` | Model name to use, required with `features.llm` | | `llm.persona` | System prompt for `ovos-persona`, required with `features.llm` | +| `speech_engine` | `public` (the default) uses community servers, which can go offline at any time. `local` runs speech on the device **with public fallback**: failed or empty recognition sends the recording to public servers, and recognition or voice synthesis uses public servers entirely if its local model cannot run. The installer reports any setup fallback. Local needs the `alpha` channel, a profile with audio (with `containers`, `ovos` or `listener`) and a Raspberry Pi 5 with 8 GB or an equivalent machine; anywhere else the installer uses `public`. This is not a fully offline mode. | | `raspberry_pi_tuning` | Maximum-performance tuning for a Pi, including an overclocking prompt | | `share_telemetry` | Share anonymous usage statistics — see [Telemetry](telemetry.md) | | `share_usage_telemetry` | Share detailed usage data — see [Telemetry](telemetry.md) | +Speech candidates are checked by `first_working()` in +[speech_setup.py](../ansible/roles/ovos_config/files/speech_setup.py). +Its `with_public_fallback()` preserves the locale's public server settings; +[speech.yml](../ansible/roles/ovos_config/tasks/speech.yml) applies the working +models and reports which parts remain public. Regression coverage lives in +[`SpeechSetupTest`](../scripts/test_speech_setup.py). + ## Environment variables For the settings that are not worth a screen of their own. With the `curl` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..8ba369cf --- /dev/null +++ b/docs/index.md @@ -0,0 +1,28 @@ +Last Edit: Codex (GPT-6) - 2026-10-08 - Motive: Index installer guides and local-speech maintenance evidence. + +# Installer documentation + +The installer selects a supported installation method, writes configuration and +manages OVOS services. Start with the [README](../README.md). + +| Guide | Scope | +| --- | --- | +| [Supported systems](supported-systems.md) | Hardware, operating systems and installation methods | +| [Automation](automation.md) | Scenarios, environment variables and public speech fallback | +| [macOS](macos.md) | Native macOS prerequisites and operation | +| [Services](services.md) | Starting, stopping and checking OVOS | +| [Terminal client](terminal-client.md) | Using OVOS without a microphone | +| [Troubleshooting](troubleshooting.md) | Diagnosing installation and runtime problems | +| [Telemetry](telemetry.md) | Optional data sharing | +| [Architecture](architecture.md) | Installer orchestration and roles | + +Local speech is selected by `first_working()` in +[speech_setup.py](../ansible/roles/ovos_config/files/speech_setup.py). +`forget_model()` removes rejected recognizer downloads only when they did not +exist before this run. `with_public_fallback()` retains the public server +settings; [speech.yml](../ansible/roles/ovos_config/tasks/speech.yml) applies them. +[`SpeechSetupTest`](../scripts/test_speech_setup.py) exercises these paths using +stub plugins and real child processes, without downloading models. + +Maintenance evidence: [audit](../AUDIT.md), [change log](../MAINTENANCE_REPORT.md), +[quick facts](../QUICK_FACTS.md), [FAQ](../FAQ.md) and [suggestions](../SUGGESTIONS.md). diff --git a/docs/telemetry.md b/docs/telemetry.md index 552ee6eb..f6a6efdb 100644 --- a/docs/telemetry.md +++ b/docs/telemetry.md @@ -26,6 +26,7 @@ The table below lists the collected data. See the [Ansible task](https://github. | `container` | OVOS installed into containers | | `country` | Country where OVOS was installed | | `cpu_capable` | Whether the CPU supports AVX2 or SIMD instructions | +| `local_speech_capable` | Whether the hardware could run speech on the device | | `display_server` | Whether X or Wayland is used as the display server | | `extra_skills_feature` | Extra OVOS skills enabled during the installation | | `gui_feature` | GUI enabled during the installation | @@ -42,6 +43,8 @@ The table below lists the collected data. See the [Ansible task](https://github. | `raspberry_pi` | Whether OVOS was installed on a Raspberry Pi | | `skills_feature` | Default OVOS skills enabled during the installation | | `sound_server` | Whether PulseAudio or PipeWire is used | +| `stt_engine` | `local` or `public`: where speech recognition runs | +| `tts_engine` | `local` or `public`: where the assistant's voice is made | | `tuning_enabled` | Whether the Raspberry Pi tuning feature was used | | `venv` | OVOS installed into a Python virtual environment | diff --git a/scenarios/scenario-local-speech.yml b/scenarios/scenario-local-speech.yml new file mode 100644 index 00000000..4ee93817 --- /dev/null +++ b/scenarios/scenario-local-speech.yml @@ -0,0 +1,12 @@ +--- +uninstall: false +method: virtualenv +channel: alpha +profile: ovos +features: + skills: true + extra_skills: false +raspberry_pi_tuning: true +share_telemetry: true +share_usage_telemetry: true +speech_engine: local diff --git a/scripts/sync_translations.py b/scripts/sync_translations.py index 5b63780c..6f933e6c 100644 --- a/scripts/sync_translations.py +++ b/scripts/sync_translations.py @@ -77,8 +77,13 @@ SUMMARY_STATE_UNSUPPORTED_PROFILE="{state_unsupported_profile}" SUMMARY_STATE_MISSING_URL="{state_missing_url}" SUMMARY_STATE_MISSING_CONFIGURATION="{state_missing_configuration}" +SUMMARY_SPEECH_LOCAL="{speech_local}" +SUMMARY_SPEECH_PUBLIC="{speech_public}" +SUMMARY_SPEECH_PUBLIC_HARDWARE="{speech_public_hardware}" +SUMMARY_SPEECH_PUBLIC_SETUP="{speech_public_setup}" +SUMMARY_SPEECH_UNUSED="{speech_unused}" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED """ FEATURES_TEMPLATE = """ @@ -96,6 +101,18 @@ export CONTENT TITLE SKILL_DESCRIPTION EXTRA_SKILL_DESCRIPTION GUI_DESCRIPTION HOMEASSISTANT_DESCRIPTION LLM_DESCRIPTION """ +SPEECH_TEMPLATE = """ +#!/usr/bin/env bash +CONTENT=" +{content} +" +TITLE="{title}" +LOCAL_DESCRIPTION="{local_description}" +PUBLIC_DESCRIPTION="{public_description}" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION +""" + SAT_TEMPLATE = """ #!/usr/bin/env bash # Global message @@ -179,6 +196,7 @@ "overclock.sh": OVERCLOCK_TEMPLATE, "profiles.sh": TEMPLATE, "satellite.sh": SAT_TEMPLATE, + "speech.sh": SPEECH_TEMPLATE, "summary.sh": SUMMARY_TEMPLATE, "telemetry.sh": TEMPLATE, "tuning.sh": TEMPLATE, diff --git a/scripts/test_local_speech_bus.py b/scripts/test_local_speech_bus.py new file mode 100644 index 00000000..e08d1547 --- /dev/null +++ b/scripts/test_local_speech_bus.py @@ -0,0 +1,175 @@ +"""The local speech check that asks the running services, against a fake messagebus. + +.github/scripts/assert_local_speech_bus.py is what tells a local speech install whose +services really loaded the local plugins from one whose mycroft.conf merely names them. +Each way that can go wrong is played here by a bus that answers in that one wrong way. +""" +import io +import subprocess +import sys +import tempfile +import time +import unittest +import wave +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +HARNESS = ROOT / ".github" / "scripts" / "assert_local_speech_bus.py" + +try: + import websockets # noqa: F401 (used by the subprocess server below) + HAVE_SERVER = True +except ImportError: + HAVE_SERVER = False + +try: + import websocket # noqa: F401 + HAVE_CLIENT = True +except ImportError: + HAVE_CLIENT = False + +SERVER = r""" +import asyncio, base64, io, json, sys, wave, websockets +MODE, PORT = sys.argv[1], int(sys.argv[2]) + +def voice(): + # What phoonnx produces: a mono 16-bit WAV at 22050 Hz. + out = io.BytesIO() + with wave.open(out, "wb") as wav: + wav.setnchannels(1) + wav.setsampwidth(2) + wav.setframerate(22050) + wav.writeframes(b"\x01\x00" * 2205) + return out.getvalue() + +async def handler(ws): + async for raw in ws: + m = json.loads(raw) + kind, data, ctx = m.get("type"), m.get("data") or {}, m.get("context") or {} + async def send(rt, payload, context=ctx): + await ws.send(json.dumps({"type": rt, "data": payload, "context": context})) + if kind.endswith(".is_ready"): + if kind == "mycroft.voice.is_ready" and MODE == "no_microphone": + continue + await send(kind + ".response", {"status": True}) + elif kind == "speak:b64_audio": + tts_id = "ovos-tts-plugin-server" if MODE == "never_reloaded" else "ovos-tts-plugin-phoonnx" + payload = {"audio": base64.b64encode(voice()).decode(), "listen": False, + "tts_id": tts_id, "utterance": data.get("utterance")} + # Someone else's speech first: it must not be taken for the answer. + await send("ovos.audio.speech", dict(payload, tts_id="other"), {"source": ["skills"]}) + topic = "speak:b64_audio.response" if MODE == "legacy_audio" else "ovos.audio.speech" + await send(topic, payload) + elif kind == "recognizer_loop:b64_transcribe": + audio = base64.b64decode(data.get("audio", "")) + # The listener reads raw frames at the rate it is told. A WAV header, or the + # wrong rate, is noise to it. + if audio.startswith(b"RIFF") or data.get("sample_rate") != 22050 or data.get("sample_width") != 2: + heard = "brr" + else: + heard = "" if MODE == "deaf_listener" else "what time is it" + await send("recognizer_loop:b64_transcribe.response", + {"transcriptions": [[heard, 0.9]], "lang": "en-US"}) + +async def main(): + async with websockets.serve(handler, "127.0.0.1", PORT): + print("ready", flush=True) + await asyncio.Future() + +asyncio.run(main()) +""" + + +class FakeBus: + """A messagebus whose audio service and listener answer correctly, or in one wrong way.""" + + _next_port = [8371] + + def __init__(self, mode="healthy"): + self.mode = mode + self.port = FakeBus._next_port[0] + FakeBus._next_port[0] += 1 + self.proc = None + + def __enter__(self): + self.proc = subprocess.Popen( + [sys.executable, "-c", SERVER, self.mode, str(self.port)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + line = self.proc.stdout.readline() + if line.strip() == "ready": + return self + if self.proc.poll() is not None: + raise RuntimeError(f"fake bus died: {self.proc.stderr.read()}") + raise RuntimeError("fake bus never became ready") + + def __exit__(self, *exc): + if self.proc and self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=5) + except subprocess.TimeoutExpired: + self.proc.kill() + self.proc.wait() + if self.proc: + self.proc.stdout.close() + self.proc.stderr.close() + + +@unittest.skipUnless(HAVE_SERVER and HAVE_CLIENT, + "needs websockets (server) and websocket-client (harness)") +class RunningServicesCheck(unittest.TestCase): + + def check(self, mode): + with FakeBus(mode) as bus, tempfile.TemporaryDirectory() as tmp: + wav = Path(tmp) / "said.wav" + result = subprocess.run( + [sys.executable, str(HARNESS), "--url", f"ws://127.0.0.1:{bus.port}/core", + "--wav", str(wav), "--connect-timeout", "4", "--ready-timeout", "6", + "--listener-ready-timeout", "3", "--reply-timeout", "4"], + capture_output=True, text=True, timeout=90) + written = wav.read_bytes() if wav.exists() else b"" + return result.returncode, result.stdout + result.stderr, written + + def test_services_that_speak_and_hear_locally_pass(self): + code, output, _ = self.check("healthy") + self.assertEqual(code, 0, output) + self.assertIn("ovos-tts-plugin-phoonnx", output) + self.assertIn("heard 'what time is it'", output) + + def test_an_answer_on_the_legacy_topic_counts(self): + code, output, _ = self.check("legacy_audio") + self.assertEqual(code, 0, output) + + def test_an_audio_service_still_on_the_public_voice_fails(self): + # mycroft.conf names phoonnx, the service never reloaded it: the case that every + # check on the file passes. + code, output, _ = self.check("never_reloaded") + self.assertEqual(code, 1, output) + self.assertIn("ovos-tts-plugin-server", output) + self.assertIn("never loaded it", output) + + def test_a_listener_with_no_microphone_hands_the_audio_back(self): + # Exit 3 is the caller's cue to have the recognizer hear the file in a process of + # its own, so the file has to be the voice's own WAV. + code, output, written = self.check("no_microphone") + self.assertEqual(code, 3, output) + with wave.open(io.BytesIO(written)) as wav: + self.assertEqual(wav.getframerate(), 22050) + + def test_a_listener_that_hears_nothing_fails(self): + code, output, _ = self.check("deaf_listener") + self.assertEqual(code, 1, output) + self.assertIn("heard ''", output) + + def test_it_is_executable_and_documents_its_own_flags(self): + self.assertTrue(HARNESS.stat().st_mode & 0o111, "the harness must be executable") + result = subprocess.run([sys.executable, str(HARNESS), "--help"], + capture_output=True, text=True, timeout=30) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertIn("--expect-tts", result.stdout) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_speech_setup.py b/scripts/test_speech_setup.py new file mode 100644 index 00000000..9cb68fac --- /dev/null +++ b/scripts/test_speech_setup.py @@ -0,0 +1,383 @@ +"""What the local speech setup keeps, and what it refuses. + +ovos_config/files/speech_setup.py runs inside the OVOS virtualenv, or the listener and +audio containers, and decides what the device will use for speech. It is driven here +against stand-ins for ovos-config (both the 2.x and the 3.x way of finding a language), +the Hugging Face hub, the plugin manager and the two plugins' own per-language choices, +so each fallback can be made to happen on purpose. The candidates are still tried in real +child processes, which is where the memory budget is measured. +""" +import json +import os +import subprocess +import sys +import tempfile +import textwrap +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +SETUP = ROOT / "ansible" / "roles" / "ovos_config" / "files" / "speech_setup.py" +STT = "ovos-stt-plugin-onnx-asr" +TTS = "ovos-tts-plugin-phoonnx" + +STUBS = { + # ovos_utils logs to stdout; STUB_NOISY makes the stand-in do the same, at the + # Python and at the file descriptor level, the way an import really can. + "ovos_config/config.py": """ + import json, os, sys + + if os.environ.get("STUB_NOISY"): + print("2026-10-07 - OVOS - ovos_config:load:1 - INFO - loading configuration") + sys.stdout.flush() + os.write(1, b"raw write from an extension\\n") + + + class LocalConf(dict): + def __init__(self, path): + super().__init__(json.load(open(path))) + """, + "ovos_plugin_manager/__init__.py": "", + "ovos_plugin_manager/utils/__init__.py": "", + "ovos_plugin_manager/utils/audio.py": """ + class AudioData: + def __init__(self, frames, rate, width): + self.frames = frames + """, + # Loading downloads the model where the plugin keeps it, as the real one does. A + # model whose name starts with "broken" then fails to load, one starting with + # "huge" holds 200 MB once loaded, one starting with "stalled" never finishes + # loading; anything else loads and hears nothing. + "ovos_plugin_manager/stt.py": """ + import os, time + + + class Recognizer: + def __init__(self, config): + model = config.get("model", "") + cache = os.path.join(os.environ["STUB_XDG_DATA_HOME"], "ovos_stt_plugin_onnxasr", + model.replace("/", "--")) + os.makedirs(cache, exist_ok=True) + open(os.path.join(cache, "model.onnx"), "a").close() + # A repository without int8 weights: asking for them fails. + if "fp32only" in model and config.get("quantization") == "int8": + raise RuntimeError("no int8 weights in " + model) + if model.startswith("broken"): + raise RuntimeError("cannot load " + model) + if model.startswith("stalled"): + time.sleep(60) + self.weights = b"x" * (200 * 1024 * 1024) if model.startswith("huge") else b"" + + def execute(self, audio, language=None): + return "" + + + def load_stt_plugin(module): + return Recognizer if module == "ovos-stt-plugin-onnx-asr" else None + """, + # A voice whose name starts with "mute" loads but cannot speak, like the Spanish + # voices in phoonnx 1.3.4a1. + "ovos_plugin_manager/tts.py": """ + import wave + + + class Voice: + def __init__(self, config): + self.voice = config.get("voice", "") + + def get_tts(self, text, path): + if self.voice.startswith("mute"): + raise wave.Error("# channels not specified") + with wave.open(path, "wb") as out: + out.setnchannels(1) + out.setsampwidth(2) + out.setframerate(16000) + out.writeframes(b"\\0\\0" * 1600) + + + def load_tts_plugin(module): + return Voice if module == "ovos-tts-plugin-phoonnx" else None + """, + "ovos_stt_plugin_onnxasr/__init__.py": "", + "ovos_stt_plugin_onnxasr/defaults.py": """ + import json, os + + REGISTRY = json.loads(os.environ.get("STUB_STT_REGISTRY", "{}")) + + + def resolve_model(lang, lang2model): + return REGISTRY.get(lang.lower().split("-")[0]) + """, + "phoonnx/__init__.py": "", + "phoonnx/model_manager.py": """ + import json, os + + VOICES = json.loads(os.environ.get("STUB_VOICES", "{}")) + + + class Voice: + def __init__(self, voice_id): + self.voice_id = voice_id + + + class TTSModelManager: + def load(self): + pass + + def merge_default_voices(self): + pass + + def get_lang_voices(self, lang): + return [Voice(v) for v in VOICES.get(lang.lower().split("-")[0], [])] + """, + "ovos_utils/__init__.py": "", + "ovos_utils/xdg_utils.py": """ + import os + + + def xdg_data_home(): + return os.environ["STUB_XDG_DATA_HOME"] + """, + "huggingface_hub/__init__.py": """ + import json, os + + + class HfApi: + def list_repo_files(self, repo): + return json.loads(os.environ.get("STUB_HUB", "{}")).get(repo, []) + """, +} + + +def stt(model, **options): + return {"stt": {"module": STT, "fallback_module": "", STT: dict(model=model, **options)}} + + +def tts(voice): + return {"tts": {"module": TTS, TTS: {"voice": voice}}} + + +class SpeechSetupTest(unittest.TestCase): + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.root = Path(self.tmp.name) + self.data = self.root / "data" + self.data.mkdir() + for path, content in STUBS.items(): + target = self.root / "stubs" / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(textwrap.dedent(content)) + self.env = dict(os.environ, PYTHONPATH=str(self.root / "stubs"), STUB_XDG_DATA_HOME=str(self.data)) + + def tearDown(self): + self.tmp.cleanup() + + def recommends(self, files, find_by_distance=False): + package = self.root / "stubs" / "ovos_config" + (package / "__init__.py").write_text("") + if find_by_distance: + (package / "utils.py").write_text(textwrap.dedent(""" + import os + + + def find_recommends_file(folder, lang): + primary = lang.lower().split("-")[0] + for name in sorted(os.listdir(folder)): + if name.lower().startswith(primary): + return os.path.join(folder, name) + return None + """)) + for path, content in files.items(): + target = package / "recommends" / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(json.dumps(content)) + + def setup(self, lang, kinds="stt,tts", gender="male", budget=100, registry=None, voices=None, hub=None, + **env_extra): + env = dict(self.env, STUB_STT_REGISTRY=json.dumps(registry or {}), + STUB_VOICES=json.dumps(voices or {}), STUB_HUB=json.dumps(hub or {}), **env_extra) + out = subprocess.run([sys.executable, str(SETUP), kinds, lang, gender, str(budget)], + capture_output=True, text=True, env=env, check=True).stdout + # The installer parses the whole of stdout, so nothing else may be on it. + return json.loads(out) + + def test_working_recommendations_are_kept_as_they_are(self): + self.recommends({"offline_stt/en-us.conf": stt("parakeet", quantization="int8"), + "offline_male/en-us.conf": tts("OpenVoiceOS/pipertts_en-US_miro")}) + result = self.setup("en-us") + self.assertEqual(result["stt"][STT], {"model": "parakeet", "quantization": "int8"}) + self.assertEqual(result["tts"][TTS]["voice"], "OpenVoiceOS/pipertts_en-US_miro") + self.assertEqual(result["notes"], []) + + def test_2x_finds_a_language_by_its_primary_subtag(self): + self.recommends({"offline_male/da-dk.conf": tts("OpenVoiceOS/phoonnx_da-DK_miro_espeak")}) + self.assertEqual(self.setup("da", kinds="tts")["tts"][TTS]["voice"], "OpenVoiceOS/phoonnx_da-DK_miro_espeak") + + def test_3x_asks_ovos_config_for_the_closest_tag(self): + self.recommends({"offline_stt/kab.conf": stt("OpenVoiceOS/nvidia-kab-conformer")}, find_by_distance=True) + self.assertEqual(self.setup("kab-dz", kinds="stt")["stt"][STT]["model"], "OpenVoiceOS/nvidia-kab-conformer") + + def test_a_recognizer_that_does_not_load_gives_way_to_the_plugins_own_model(self): + self.recommends({"offline_stt/ca-es.conf": stt("broken/ca-model")}) + result = self.setup("ca-es", kinds="stt", registry={"ca": "OpenVoiceOS/nvidia-ca-conformer"}) + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/nvidia-ca-conformer") + self.assertTrue(any("broken/ca-model" in note for note in result["notes"])) + + def test_failed_probes_remove_only_new_downloads(self) -> None: + """Crashes and timeouts free new downloads but preserve earlier installs.""" + for failure in ("broken", "stalled"): + for preexisting in (False, True): + with self.subTest(failure=failure, preexisting=preexisting): + model = f"{failure}/{'existing' if preexisting else 'new'}" + self.recommends({"offline_stt/en-us.conf": stt(model)}) + rejected = self.data / "ovos_stt_plugin_onnxasr" / model.replace("/", "--") + if preexisting: + rejected.mkdir(parents=True) + (rejected / "weights.onnx").write_text("earlier installation") + + result = self.setup("en-us", kinds="stt", registry={"en": "working/fallback"}, + OVOS_SPEECH_CANDIDATE_TIMEOUT="1") + + self.assertEqual(result["stt"][STT]["model"], "working/fallback") + self.assertTrue((self.data / "ovos_stt_plugin_onnxasr" / + "working--fallback" / "model.onnx").is_file()) + self.assertEqual(rejected.exists(), preexisting) + if preexisting: + self.assertEqual((rejected / "weights.onnx").read_text(), "earlier installation") + + def test_a_recognizer_over_the_memory_budget_gives_way_and_is_deleted(self): + self.recommends({"offline_stt/pt-pt.conf": stt("huge/whisper-medium-pt")}) + downloaded = self.data / "ovos_stt_plugin_onnxasr" / "huge--whisper-medium-pt" + result = self.setup("pt-pt", kinds="stt", budget=100, registry={"pt": "OpenVoiceOS/parakeet-pt"}) + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/parakeet-pt") + self.assertTrue(any("above the 100 MB budget" in note for note in result["notes"])) + self.assertFalse(downloaded.exists()) + + def test_a_recognizer_over_the_budget_that_was_there_before_is_kept(self): + # An earlier install's local section can name the very model tried here. + self.recommends({"offline_stt/pt-pt.conf": stt("huge/whisper-medium-pt")}) + earlier = self.data / "ovos_stt_plugin_onnxasr" / "huge--whisper-medium-pt" + earlier.mkdir(parents=True) + (earlier / "weights.onnx").write_text("an earlier download") + result = self.setup("pt-pt", kinds="stt", budget=100, registry={"pt": "OpenVoiceOS/parakeet-pt"}) + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/parakeet-pt") + self.assertEqual((earlier / "weights.onnx").read_text(), "an earlier download") + + def test_a_voice_that_cannot_speak_gives_way_to_the_next_phoonnx_voice(self): + self.recommends({"offline_male/es-es.conf": tts("mute/pipertts_es-ES_miro")}) + result = self.setup("es-es", kinds="tts", voices={"es": ["mute/pipertts_es-ES_miro", "piper/es_ES-davefx-medium"]}) + self.assertEqual(result["tts"][TTS]["voice"], "piper/es_ES-davefx-medium") + + def test_a_language_without_a_recommendation_uses_the_plugins_own_choice(self): + self.recommends({}) + result = self.setup("pl-pl", registry={"pl": "OpenVoiceOS/parakeet-pl"}, voices={"pl": ["piper/pl_PL-gosia-medium"]}) + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/parakeet-pl") + self.assertEqual(result["stt"]["fallback_module"], "") + self.assertEqual(result["tts"][TTS]["voice"], "piper/pl_PL-gosia-medium") + + def test_when_nothing_works_both_halves_stay_public(self): + self.recommends({"offline_stt/es-es.conf": stt("broken/es"), "offline_male/es-es.conf": tts("mute/es")}) + result = self.setup("es-es", voices={"es": ["mute/es", "mute/other"]}) + self.assertIsNone(result["stt"]) + self.assertIsNone(result["tts"]) + self.assertFalse((self.data / "ovos_stt_plugin_onnxasr" / "broken--es").exists()) + + def test_int8_is_asked_for_where_the_repository_ships_it(self): + model = "OpenVoiceOS/parakeet-rnnt-1.1b-es" + self.recommends({"offline_stt/es-es.conf": stt(model)}) + result = self.setup("es-es", kinds="stt", hub={model: ["encoder-model.onnx_data", "encoder-model.int8.onnx"]}) + self.assertEqual(result["stt"][STT]["quantization"], "int8") + + def test_a_repository_without_int8_weights_is_left_alone(self): + model = "OpenVoiceOS/whisper-medium-pt" + self.recommends({"offline_stt/pt-pt.conf": stt(model)}) + result = self.setup("pt-pt", kinds="stt", hub={model: ["encoder_model.onnx_data"]}) + self.assertNotIn("quantization", result["stt"][STT]) + + def test_only_the_result_reaches_stdout(self): + # ovos_utils logs to stdout, and an import that logs or writes would put a line + # in front of the JSON the installer parses - the whole install would stop on it. + self.recommends({"offline_stt/en-us.conf": stt("parakeet"), "offline_male/en-us.conf": tts("miro")}) + result = self.setup("en-us", STUB_NOISY="1") + self.assertIsNotNone(result["stt"]) + self.assertIsNotNone(result["tts"]) + + def test_the_fallback_keeps_the_public_servers_ovos_config_picks_for_the_locale(self): + # The local section replaces the whole stt section, and with it the servers + # autoconfigure --online chose for the language; the fallback needs them back. + servers = {"urls": ["https://zuazo-es.tigregotico.pt/stt", "https://stt.smartgic.io/fasterwhisper/stt"]} + self.recommends({"offline_stt/es-es.conf": stt("parakeet-es"), + "online_stt/es-es.conf": {"stt": {"module": "ovos-stt-plugin-server", + "ovos-stt-plugin-server": servers}}}) + result = self.setup("es-es", kinds="stt") + self.assertEqual(result["stt"][STT]["model"], "parakeet-es") + self.assertEqual(result["stt"]["ovos-stt-plugin-server"], servers) + + def test_without_public_servers_for_the_locale_the_fallback_keeps_its_defaults(self): + self.recommends({"offline_stt/kab-dz.conf": stt("kab-conformer")}) + result = self.setup("kab-dz", kinds="stt") + self.assertNotIn("ovos-stt-plugin-server", result["stt"]) + + def test_a_fault_while_choosing_costs_that_half_and_not_the_run(self): + # A section that names no plugin: the script must still answer, with the other + # half set up and the reason in the notes, rather than exit without a result. + self.recommends({"offline_stt/en-us.conf": {"stt": {"model": "parakeet"}}, + "offline_male/en-us.conf": tts("miro")}) + result = self.setup("en-us") + self.assertIsNone(result["stt"]) + self.assertEqual(result["tts"][TTS]["voice"], "miro") + self.assertTrue(any(note.startswith("STT could not be set up") for note in result["notes"])) + + def broken_recommendation(self, folder): + broken = self.root / "stubs" / "ovos_config" / "recommends" / folder / "en-us.conf" + broken.parent.mkdir(parents=True, exist_ok=True) + broken.write_text("{ not json") + + def test_a_recommendation_that_cannot_be_read_gives_way_to_the_plugins_own_model(self): + self.recommends({"offline_male/en-us.conf": tts("miro")}) + self.broken_recommendation("offline_stt") + result = self.setup("en-us", registry={"en": "OpenVoiceOS/parakeet-en"}) + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/parakeet-en") + self.assertTrue(any(note.startswith("STT recommendation could not be loaded") for note in result["notes"])) + + def test_a_voice_recommendation_that_cannot_be_read_gives_way_to_phoonnx_voices(self): + self.recommends({}) + self.broken_recommendation("offline_male") + result = self.setup("en-us", kinds="tts", voices={"en": ["miro"]}) + self.assertEqual(result["tts"][TTS]["voice"], "miro") + self.assertTrue(any(note.startswith("TTS recommendation could not be loaded") for note in result["notes"])) + + def test_the_same_model_without_int8_is_tried_when_its_int8_weights_do_not_load(self): + self.recommends({"offline_stt/en-us.conf": stt("org/fp32only-en", quantization="int8")}) + result = self.setup("en-us", kinds="stt", registry={"en": "org/fp32only-en"}) + self.assertEqual(result["stt"][STT], {"model": "org/fp32only-en"}) + self.assertTrue(any("no int8 weights" in note for note in result["notes"])) + self.assertTrue((self.data / "ovos_stt_plugin_onnxasr" / "org--fp32only-en" / "model.onnx").is_file()) + + def test_a_download_from_an_earlier_try_this_run_is_still_this_runs_to_delete(self): + # The int8 try downloads, then fails; the fp32 try of the same model is over the + # budget. What the int8 try downloaded is this run's, and goes. + self.recommends({"offline_stt/en-us.conf": stt("huge/fp32only-en", quantization="int8")}) + downloaded = self.data / "ovos_stt_plugin_onnxasr" / "huge--fp32only-en" + result = self.setup("en-us", kinds="stt", budget=100, registry={"en": "huge/fp32only-en"}) + self.assertIsNone(result["stt"]) + self.assertTrue(any("above the 100 MB budget" in note for note in result["notes"])) + self.assertFalse(downloaded.exists()) + + def test_a_candidate_that_never_finishes_is_given_up_on(self): + self.recommends({"offline_stt/en-us.conf": stt("stalled/parakeet")}) + result = self.setup("en-us", kinds="stt", registry={"en": "OpenVoiceOS/parakeet-en"}, + OVOS_SPEECH_CANDIDATE_TIMEOUT="3") + self.assertEqual(result["stt"][STT]["model"], "OpenVoiceOS/parakeet-en") + self.assertTrue(any("stalled/parakeet" in note and "given up on" in note for note in result["notes"])) + + def test_only_the_halves_asked_for_are_set_up(self): + self.recommends({"offline_stt/en-us.conf": stt("parakeet"), "offline_male/en-us.conf": tts("miro")}) + result = self.setup("en-us", kinds="stt") + self.assertIsNotNone(result["stt"]) + self.assertIsNone(result["tts"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/setup.sh b/setup.sh index 8b3e0f4b..b76b350a 100644 --- a/setup.sh +++ b/setup.sh @@ -53,6 +53,9 @@ source utils/banner.sh # shellcheck source=utils/common.sh source utils/common.sh +# shellcheck source=utils/speech.sh +source utils/speech.sh + # # shellcheck source=utils/argparse.sh source utils/argparse.sh @@ -96,6 +99,7 @@ required_packages check_python_compatibility detect_sound detect_display +detect_local_speech_support create_python_venv install_ansible detect_scenario @@ -141,6 +145,7 @@ if [ "$EXISTING_INSTANCE" == "true" ]; then fi normalize_feature_gui_support +normalize_speech_engine log_info "➤ Starting Ansible playbook... ☕🍵🧋" @@ -261,6 +266,8 @@ ansible_command=( -e "ovos_installer_satellite_key=${SATELLITE_KEY}" \ -e "ovos_installer_satellite_password=${SATELLITE_PASSWORD}" \ -e "ovos_installer_cpu_is_capable=${CPU_IS_CAPABLE}" \ + -e "ovos_installer_local_speech_capable=${LOCAL_SPEECH_CAPABLE:-false}" \ + -e "ovos_installer_speech_engine=${SPEECH_ENGINE:-public}" \ -e "ovos_installer_cleaning=${ansible_cleaning}" \ -e "ovos_installer_display_server=${DISPLAY_SERVER}" \ -e "ovos_installer_telemetry=${SHARE_TELEMETRY}" \ diff --git a/tests/bats/code_quality.bats b/tests/bats/code_quality.bats index 3c74c5bd..0a74caee 100644 --- a/tests/bats/code_quality.bats +++ b/tests/bats/code_quality.bats @@ -2967,7 +2967,17 @@ YAML run grep -F -q "{{ ovos_installer_ovos_config_tts_gender }}" "$container_tasks" assert_success - run bash -c "awk '/ovos-config autoconfigure --lang/ { in_command=1 } in_command { print } in_command && /ovos_installer_ovos_config_tts_gender/ { exit }' \"$container_tasks\" | grep -F -q -- '--online'" + # Always --online. Never neither: autoconfigure then goes hybrid (the voice on the + # device, recognition on the public servers), which is not one of the two speech + # choices the installer offers. And never --offline, not even for local speech: + # local speech replaces each half it proves works afterwards, and --offline would + # leave a half that fails on the offline recommendation that just failed - a + # Spanish voice phoonnx cannot speak, and a mute device. + run grep -F -q "ovos-config autoconfigure --lang {{ ovos_installer_locale }} --online" "$container_tasks" + assert_success + run bash -c "awk '/Merge ovos-config language recommendations into mycroft.conf/,/changed_when/' '$virtualenv_tasks' | grep -q -- '- --online$'" + assert_success + run bash -c "grep -h -v '^[[:space:]]*#' '$container_tasks' '$virtualenv_tasks' | grep -q -- '--offline'" assert_failure run grep -F -q "ovos_installer_ovos_config_autoconfigure_enabled | default(false) | bool" "$container_tasks" @@ -3019,7 +3029,7 @@ YAML run grep -q "SCENARIO_ALLOWED_FEATURES=(skills extra_skills gui homeassistant llm)" utils/constants.sh assert_success - run grep -q "SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm)" utils/constants.sh + run grep -q "SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm speech_engine)" utils/constants.sh assert_success run grep -q "SCENARIO_ALLOWED_LLM_OPTIONS=(api_url key model persona max_tokens temperature top_p)" utils/constants.sh @@ -3603,7 +3613,7 @@ function anchors_of() { run grep -q 'export HARDWARE_CONFIRMATION="\${HARDWARE_CONFIRMATION:-}"' utils/argparse.sh assert_success - run grep -q 'declare -ra SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm)' utils/constants.sh + run grep -q 'declare -ra SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm speech_engine)' utils/constants.sh assert_success run grep -q 'hardware)' utils/scenario.sh @@ -5429,3 +5439,131 @@ for template in ('core-requirements.txt.j2', 'satellite-requirements.txt.j2'): run bash -c "grep -A14 -F '\"module\": \"ovos-ww-plugin-precise-onnx\",' '$conf_file' | grep -q '\"model\": \"https://'" assert_success } + +@test "local_speech_is_wired_from_requirements_to_telemetry" { + # The plugins are only installed when local speech was chosen, and onnx-asr + # needs its hub extra: without huggingface_hub it cannot fetch a model at all. + local requirements + for requirements in core-requirements satellite-requirements; do + local file="ansible/roles/ovos_virtualenv/templates/virtualenv/${requirements}.txt.j2" + run bash -c "awk \"/ovos_installer_speech_engine/,/endif/\" '$file'" + assert_output --partial "ovos-stt-plugin-onnx-asr" + assert_output --partial "onnx-asr[cpu,hub]" + assert_output --partial "phoonnx" + done + + # The models come from the ovos-config installed in the virtualenv, so the + # speech configuration runs after the install and only for local speech. + local venv_tasks="ansible/roles/ovos_virtualenv/tasks/venv.yml" + run grep -q "tasks_from: speech.yml" "$venv_tasks" + assert_success + run bash -c "awk '/Configure local speech/,0' '$venv_tasks' | grep -q \"ovos_installer_speech_engine | default('public')) == 'local'\"" + assert_success + + # Only what speech_setup.py proved works here is written - it also downloads the + # models, within a memory budget - and the public STT server stays as the fallback. + local speech_tasks="ansible/roles/ovos_config/tasks/speech.yml" + run grep -q "speech_setup.py" "$speech_tasks" + assert_success + run grep -q "ovos_config_speech_stt_memory_budget_mb" "$speech_tasks" + assert_success + run grep -q "'fallback_module': ovos_config_speech_stt_fallback" "$speech_tasks" + assert_success + run grep -q "^ovos_config_speech_stt_fallback: ovos-stt-plugin-server$" ansible/roles/ovos_config/defaults/main.yml + assert_success + + # A re-run keeps the sections the last run wrote instead of dropping them. + run grep -q "ovos_config_speech_current" ansible/roles/ovos_config/templates/mycroft.conf.j2 + assert_success + + # Running services only reload a mycroft.conf rewritten in place, and the speech + # write replaces it, so a run that changes it restarts the systemd units too. + run bash -c "grep -A8 'ovos_installer_systemd_state:' ansible/roles/ovos_services/tasks/systemd.yml | grep -q 'ovos_config_speech_configuration.changed'" + assert_success + run grep -q "register: ovos_config_speech_configuration" "$speech_tasks" + assert_success + + # Telemetry reports where speech ended up, and uninstall removes the models. + local telemetry="ansible/roles/ovos_telemetry/tasks/main.yml" + run grep -q 'stt_engine: "{{ ovos_telemetry_stt_engine }}"' "$telemetry" + assert_success + run grep -q 'tts_engine: "{{ ovos_telemetry_tts_engine }}"' "$telemetry" + assert_success + run grep -q "local_speech_capable:" "$telemetry" + assert_success + run grep -q "/.local/share/ovos_stt_plugin_onnxasr" ansible/roles/ovos_services/defaults/main.yml + assert_success +} + +@test "local_speech_is_exercised_by_scenarios" { + # The example scenario asks for local speech where it is available. + local scenario="scenarios/scenario-local-speech.yml" + run grep -qE '^speech_engine: local$' "$scenario" + assert_success + run grep -qE '^channel: alpha$' "$scenario" + assert_success + run grep -qE '^method: virtualenv$' "$scenario" + assert_success + + # The nightly matrix installs it through a scenario file, then checks that the + # device speaks and hears without the public servers. That the uninstall takes + # the models away, their directories or their volumes, is proven by the machine + # comparison after every uninstall + # (every_ci_job_that_uninstalls_proves_the_machine_was_given_back). + local workflow=".github/workflows/scenarios-ubuntu2404.yml" + run grep -qF 'speech_engine: ${{ matrix.speech_engine }}' "$workflow" + assert_success + run grep -qF '.github/scripts/assert_local_speech.sh' "$workflow" + assert_success + [ -x .github/scripts/assert_local_speech.sh ] + + # Containers too: the voice speaks in ovos_audio and the listener hears it in + # ovos_listener. + run grep -qF '.github/scripts/assert_local_speech.sh --containers' "$workflow" + assert_success + + # The running services are asked too, not only the file: a mycroft.conf that + # names the local plugins passes every check on the file while services that + # never reloaded it stay on the public servers. + [ -x .github/scripts/assert_local_speech_bus.py ] + run grep -qF 'assert_local_speech_bus.py' .github/scripts/assert_local_speech.sh + assert_success +} + +@test "local_speech_reaches_containers_only_when_ovos_docker_can_run_it" { + local composer="ansible/roles/ovos_containers/tasks/composer.yml" + + # The compose of the pinned release must give the listener room for the model and a + # volume to keep it in, and the pulled images must carry both plugins; short of + # either, containers stay public instead of OOM-killing or crash-looping the listener. + run grep -F -q "'LISTENER_MEMORY_LIMIT' in _compose and 'ovos_stt_models' in _compose" "$composer" + assert_success + run grep -F -q "'ovos-stt-plugin-onnx-asr' in find_stt_plugins()" "$composer" + assert_success + run grep -F -q "'ovos-tts-plugin-phoonnx' in find_tts_plugins()" "$composer" + assert_success + + # Local speech is configured through the same tasks as the virtualenv, run in the + # cli, listener and audio containers. + run bash -c "awk '/- name: Configure local speech/,/- name: Compute optional skills flags/' '$composer' | grep -q 'ovos_config_speech_containers:'" + assert_success + run grep -q "^ovos_containers_container_ovos_listener: ovos_listener$" ansible/roles/ovos_containers/defaults/main.yml + assert_success + run grep -q "^ovos_containers_container_ovos_audio: ovos_audio$" ansible/roles/ovos_containers/defaults/main.yml + assert_success + + # The listener and audio service are up before local speech replaces + # mycroft.conf, and they do not see a replaced file: they are restarted when it + # changed, or they would stay on the public servers until the next boot. + run bash -c "awk '/- name: Restart the listener and audio services on their local speech/,/- name: Compute optional skills flags/' '$composer' | grep -q 'state: restarted'" + assert_success + run bash -c "awk '/- name: Restart the listener and audio services on their local speech/,/- name: Compute optional skills flags/' '$composer' | grep -q 'ovos_config_speech_configuration is changed'" + assert_success + + # The raised limits only go into .env when local speech was asked for. + local env_template="ansible/roles/ovos_containers/templates/docker/env.j2" + run bash -c "grep -B1 '^LISTENER_MEMORY_LIMIT=' '$env_template' | grep -q \"ovos_installer_speech_engine | default('public')) == 'local'\"" + assert_success + run bash -c "grep -B1 '^AUDIO_MEMORY_LIMIT=' '$env_template' | grep -q \"ovos_installer_speech_engine | default('public')) == 'local'\"" + assert_success +} diff --git a/tests/bats/locales.bats b/tests/bats/locales.bats index 1e09a774..e869f8fa 100644 --- a/tests/bats/locales.bats +++ b/tests/bats/locales.bats @@ -129,6 +129,52 @@ function setup() { done } +@test "locales_speech_scripts_are_complete_and_sourceable" { + for locale_dir in tui/locales/*; do + local locale_file="$locale_dir/speech.sh" + + if [ ! -f "$locale_file" ]; then + echo "Missing speech locale: $locale_file" >&2 + return 1 + fi + + run bash -euc " + source '$locale_file' + test -n \"\$TITLE\" + test -n \"\$CONTENT\" + test -n \"\$LOCAL_DESCRIPTION\" + test -n \"\$PUBLIC_DESCRIPTION\" + " + assert_success + done +} + +@test "locales_summary_scripts_show_where_speech_runs" { + for f in tui/locales/*/summary.sh; do + run bash -euc " + METHOD='virtualenv' + CHANNEL='alpha' + PROFILE='ovos' + SPEECH_SUMMARY_STATE='speech-state-marker' + BACK_BUTTON='Back' + source '$f' + test -n \"\$SUMMARY_SPEECH_LOCAL\" + test -n \"\$SUMMARY_SPEECH_PUBLIC\" + test -n \"\$SUMMARY_SPEECH_PUBLIC_HARDWARE\" + test -n \"\$SUMMARY_SPEECH_PUBLIC_SETUP\" + test -n \"\$SUMMARY_SPEECH_UNUSED\" + printf '%s\\n' \"\$CONTENT\" + " + + if [ "$status" -ne 0 ]; then + echo "Failed to source $f" >&2 + echo "$output" >&2 + return 1 + fi + assert_output --partial "speech-state-marker" + done +} + @test "locales_usage_telemetry_scripts_are_complete_and_sourceable" { for locale_dir in tui/locales/*; do local locale_file="$locale_dir/usage_telemetry.sh" @@ -315,6 +361,26 @@ function setup() { done } +@test "locales_speech_strings_are_localized_outside_en_us" { + for f in tui/locales/*/speech.sh; do + if [ "$f" = "tui/locales/en-us/speech.sh" ]; then + continue + fi + + run bash -euc " + source tui/locales/en-us/speech.sh + en_content=\$CONTENT + en_local_description=\$LOCAL_DESCRIPTION + en_public_description=\$PUBLIC_DESCRIPTION + source '$f' + [ \"\$CONTENT\" != \"\$en_content\" ] + [ \"\$LOCAL_DESCRIPTION\" != \"\$en_local_description\" ] + [ \"\$PUBLIC_DESCRIPTION\" != \"\$en_public_description\" ] + " + assert_success + done +} + @test "english_llm_locale_explains reply tuning in plain language" { local file="tui/locales/en-us/llm.sh" @@ -349,3 +415,134 @@ function setup() { run grep -Eq '\b(default|provider|summary|voice-friendly|tuning)\b' "$file" assert_failure } + +@test "locales_speech_disclose_public_fallback_in_choices_and_summaries" { + run python3 - <<'PY' +import json +import subprocess +from pathlib import Path + +# Check what users see after sourcing the generated locales, including both +# fallback triggers. A translated heading alone must not satisfy disclosure. +expected = { + "en-us": ( + "with public fallback", "Downloads speech models", + "fails or returns no text, that recording is sent to public servers", + "recognition or synthesis cannot be set up, that part uses public servers", + "the installer reports it", "go offline at any time", + ), + "ca-es": ( + "amb servidors públics de reserva", "Baixa models de veu", + "falla o no retorna text, aquella gravació s'envia als servidors públics", + "reconeixement o la síntesi local, aquella part fa servir servidors públics", + "l'instal·lador ho indica", "deixar de funcionar en qualsevol moment", + ), + "da": ( + "med offentlige servere som reserve", "Henter talemodeller", + "fejler eller ikke giver tekst, sendes optagelsen til offentlige servere", + "genkendelse eller syntese ikke kan sættes op, bruger den del offentlige servere", + "installationsprogrammet oplyser det", "gå ned når som helst", + ), + "de-de": ( + "mit öffentlichen Servern als Ersatz", "Lädt Sprachmodelle herunter", + "fehlschlägt oder keinen Text liefert, wird die Aufnahme an öffentliche Server gesendet", + "Erkennung oder Synthese nicht einrichten, nutzt dieser Teil öffentliche Server", + "der Installer weist darauf hin", "jederzeit langsamer werden oder ausfallen", + ), + "es-es": ( + "con servidores públicos de respaldo", "Descarga modelos de voz", + "falla o no devuelve texto, esa grabación se envía a servidores públicos", + "reconocimiento o la síntesis local, esa parte usa servidores públicos", + "el instalador lo indica", "dejar de funcionar en cualquier momento", + ), + "eu-es": ( + "zerbitzari publikoak ordezko gisa", "Ahots-ereduak deskargatzen ditu", + "huts egiten badu edo testurik itzultzen ez badu, grabazioa zerbitzari publikoetara bidaltzen da", + "Ezagutza edo sintesi lokala ezin bada konfiguratu, zati horrek zerbitzari publikoak erabiltzen ditu", + "instalatzaileak horren berri ematen du", "edozein unetan moteldu edo gelditu daitezke", + ), + "fr-fr": ( + "avec des serveurs publics en secours", "Télécharge des modèles vocaux", + "échoue ou ne renvoie aucun texte, cet enregistrement est envoyé aux serveurs publics", + "reconnaissance ou la synthèse locale ne peut pas être configurée, cette partie utilise les serveurs publics", + "l'installateur le signale", "s'arrêter à tout moment", + ), + "gl-es": ( + "con servidores públicos de respaldo", "Descarga modelos de voz", + "falla ou non devolve texto, esa gravación envíase a servidores públicos", + "recoñecemento ou a síntese local, esa parte usa servidores públicos", + "o instalador indícao", "deixar de funcionar en calquera momento", + ), + "hi-in": ( + "सार्वजनिक सर्वरों की मदद से", "वाक् मॉडल डाउनलोड होते हैं", + "विफल हो या कोई टेक्स्ट न लौटाए, तो वह रिकॉर्डिंग सार्वजनिक सर्वरों पर भेजी जाती है", + "पहचान या संश्लेषण स्थापित नहीं हो पाता, तो उस हिस्से के लिए सार्वजनिक सर्वर इस्तेमाल होते हैं", + "इंस्टॉलर इसकी जानकारी देता है", "कभी भी बंद हो सकते हैं", + ), + "it-it": ( + "con server pubblici di riserva", "Scarica modelli vocali", + "fallisce o non restituisce testo, quella registrazione viene inviata ai server pubblici", + "riconoscimento o la sintesi locale, quella parte usa i server pubblici", + "l'installer lo segnala", "andare offline in qualsiasi momento", + ), + "kab-dz": ( + "s yiqeddacen izayazen d tallalt", "Ad d-yessader timudmin n taɣect", + "yecceḍ uɛqal adigan neɣ ur d-yerri ara aḍris, asekles-nni ad yettwazen ɣer yiqeddacen izayazen", + "aɛqal neɣ asuddes adigan, aḥric-nni ad yesseqdec iqeddacen izayazen", + "amesbeddi ad d-yefk talɣut ɣef waya", "ad ḥbesen melmi tebɣu tili", + ), + "nl-nl": ( + "met openbare servers als reserve", "Downloadt spraakmodellen", + "mislukt of geen tekst oplevert, gaat die opname naar openbare servers", + "herkenning of synthese niet kan worden ingesteld, gebruikt dat onderdeel openbare servers", + "het installatieprogramma meldt dit", "op elk moment traag worden of uitvallen", + ), + "pl-pl": ( + "z serwerami publicznymi w rezerwie", "Pobiera modele mowy", + "zawiedzie lub nie zwróci tekstu, nagranie jest wysyłane do serwerów publicznych", + "rozpoznawania lub syntezy, ta część używa serwerów publicznych", + "instalator o tym informuje", "przestać działać w każdej chwili", + ), + "pt-pt": ( + "com servidores públicos de reserva", "Descarrega modelos de voz", + "falhar ou não devolver texto, essa gravação é enviada para servidores públicos", + "reconhecimento ou a síntese local, essa parte usa servidores públicos", + "o instalador informa-o", "indisponíveis a qualquer momento", + ), +} +locale_paths = list(Path("translations").glob("*/strings.json")) +assert {path.parent.name for path in locale_paths} == set(expected) +for path in locale_paths: + locale = path.parent.name + data = json.loads(path.read_text()) + result = subprocess.run( + ["bash", "-euc", 'source "$1"; printf "%s\\0%s\\0" "$CONTENT" "$LOCAL_DESCRIPTION"; ' + 'source "$2"; printf "%s" "$SUMMARY_SPEECH_LOCAL"', "bash", + f"tui/locales/{locale}/speech.sh", f"tui/locales/{locale}/summary.sh"], + check=True, capture_output=True, text=True, + ) + content, choice, summary = result.stdout.split("\0") + fallback, *disclosures = expected[locale] + for field, value in (("content", content), ("choice", choice), ("summary", summary)): + assert fallback in value, f"{locale}: missing public fallback in {field}" + for disclosure in disclosures: + assert disclosure in content, f"{locale}: missing speech disclosure: {disclosure}" + assert choice == data["speech.sh"]["local_description"] + assert summary == data["summary.sh"]["speech_local"] +PY + assert_success +} + +@test "english_speech_locale_does_not_promise_private_or_offline_processing" { + run bash -euc ' + source tui/locales/en-us/speech.sh + printf "%s\n" "$CONTENT" "$LOCAL_DESCRIPTION" + source tui/locales/en-us/summary.sh + printf "%s\n" "$SUMMARY_SPEECH_LOCAL" + ' + assert_success + refute_output --partial "everything runs on this machine" + refute_output --partial "Your voice stays here" + refute_output --partial "nothing depends on the internet" + assert_output --partial "not a production service" +} diff --git a/tests/bats/speech.bats b/tests/bats/speech.bats new file mode 100644 index 00000000..8e4f1146 --- /dev/null +++ b/tests/bats/speech.bats @@ -0,0 +1,477 @@ +#!/usr/bin/env bats +# +# Local speech is offered on a Raspberry Pi 5 with 8 GB or a machine at least as +# capable, on alpha installs only: the virtualenv, or containers with the ovos or +# listener profile. Everything else stays on the public servers, whatever was +# asked for. + +function setup() { + load "$HOME/shell-testing/test_helper/bats-support/load" + load "$HOME/shell-testing/test_helper/bats-assert/load" + load ../../utils/constants.sh + load ../../utils/speech.sh + + LOG_FILE="$(mktemp)" + + # A 64-bit Linux host with AVX2 and 16 GB, which can run local speech. + CPU_IS_CAPABLE="true" + RASPBERRYPI_MODEL="N/A" + TOTAL_MEMORY_MB=16000 + MOCK_UNAME_S="Linux" + MOCK_UNAME_M="x86_64" + MOCK_LONG_BIT="64" + + function uname() { + case "$1" in + -s) printf '%s\n' "$MOCK_UNAME_S" ;; + -m) printf '%s\n' "$MOCK_UNAME_M" ;; + esac + } + function getconf() { + printf '%s\n' "$MOCK_LONG_BIT" + } +} + +function teardown() { + rm -f "$LOG_FILE" + unset -f uname getconf sysctl +} + +@test "speech: a capable 64-bit Linux machine with 16 GB can run local speech" { + run local_speech_hardware_supported + assert_success +} + +@test "speech: a Raspberry Pi 5 with 8 GB can run local speech" { + MOCK_UNAME_M="aarch64" + RASPBERRYPI_MODEL="Raspberry Pi 5 Model B Rev 1.0" + # What an 8 GB board reports once firmware and kernel have taken their share. + TOTAL_MEMORY_MB=8052 + + run local_speech_hardware_supported + assert_success +} + +@test "speech: the Pi 500 and the Compute Module 5 count as a Pi 5" { + MOCK_UNAME_M="aarch64" + TOTAL_MEMORY_MB=8052 + + RASPBERRYPI_MODEL="Raspberry Pi 500 Rev 1.0" + run local_speech_hardware_supported + assert_success + + RASPBERRYPI_MODEL="Raspberry Pi Compute Module 5 Rev 1.0" + run local_speech_hardware_supported + assert_success +} + +@test "speech: a Raspberry Pi 5 with 4 GB stays public" { + MOCK_UNAME_M="aarch64" + RASPBERRYPI_MODEL="Raspberry Pi 5 Model B Rev 1.0" + TOTAL_MEMORY_MB=4040 + + run local_speech_hardware_supported + assert_failure +} + +@test "speech: a Raspberry Pi 4 stays public even with 8 GB" { + MOCK_UNAME_M="aarch64" + RASPBERRYPI_MODEL="Raspberry Pi 4 Model B Rev 1.5" + TOTAL_MEMORY_MB=7810 + + run local_speech_hardware_supported + assert_failure +} + +@test "speech: the memory floor is 7.5 GiB" { + TOTAL_MEMORY_MB=7680 + run local_speech_hardware_supported + assert_success + + TOTAL_MEMORY_MB=7679 + run local_speech_hardware_supported + assert_failure +} + +@test "speech: a CPU without AVX2 or NEON stays public" { + CPU_IS_CAPABLE="false" + + run local_speech_hardware_supported + assert_failure +} + +@test "speech: a 32-bit userland stays public" { + MOCK_UNAME_M="aarch64" + MOCK_LONG_BIT="32" + RASPBERRYPI_MODEL="Raspberry Pi 5 Model B Rev 1.0" + TOTAL_MEMORY_MB=8052 + + run local_speech_hardware_supported + assert_failure +} + +@test "speech: an Intel Mac stays public and an Apple silicon Mac does not" { + MOCK_UNAME_S="Darwin" + + MOCK_UNAME_M="x86_64" + run local_speech_hardware_supported + assert_failure + + MOCK_UNAME_M="arm64" + run local_speech_hardware_supported + assert_success +} + +@test "speech: memory on a Mac is read from hw.memsize" { + MOCK_UNAME_S="Darwin" + function sysctl() { + printf '%s\n' "17179869184" + } + + run total_memory_mb + assert_success + assert_output "16384" +} + +@test "speech: detection exports whether this host is capable" { + MOCK_UNAME_S="Darwin" + MOCK_UNAME_M="arm64" + function sysctl() { + printf '%s\n' "8589934592" + } + + detect_local_speech_support >/dev/null + assert_equal "$TOTAL_MEMORY_MB" "8192" + assert_equal "$LOCAL_SPEECH_CAPABLE" "true" + + function sysctl() { + printf '%s\n' "4294967296" + } + detect_local_speech_support >/dev/null + assert_equal "$LOCAL_SPEECH_CAPABLE" "false" +} + +@test "speech: local is only available on alpha installs with audio" { + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="alpha" + METHOD="virtualenv" + PROFILE="ovos" + run local_speech_available + assert_success + + PROFILE="satellite" + run local_speech_available + assert_success + + PROFILE="server" + run local_speech_available + assert_failure + + PROFILE="ovos" + CHANNEL="testing" + run local_speech_available + assert_failure + + CHANNEL="alpha" + METHOD="virtualenv" + LOCAL_SPEECH_CAPABLE="false" + run local_speech_available + assert_failure +} + +@test "speech: an available local choice is kept" { + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="alpha" + METHOD="virtualenv" + PROFILE="ovos" + SPEECH_ENGINE="local" + + normalize_speech_engine + assert_equal "$SPEECH_ENGINE" "local" +} + +@test "speech: local where it is not available falls back to public and says so" { + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="testing" + METHOD="virtualenv" + PROFILE="ovos" + SPEECH_ENGINE="local" + + run normalize_speech_engine + assert_success + assert_output --partial "Using the public servers instead" + + normalize_speech_engine >/dev/null + assert_equal "$SPEECH_ENGINE" "public" +} + +@test "speech: nothing chosen means public" { + unset SPEECH_ENGINE + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="alpha" + METHOD="virtualenv" + PROFILE="ovos" + + run normalize_speech_engine + assert_output "" + + normalize_speech_engine + assert_equal "$SPEECH_ENGINE" "public" +} + +function run_scenario_with_speech_engine() { + local value="$1" + local scenario_file yq_mock + scenario_file="$(mktemp)" + yq_mock="$(mktemp)" + + printf 'speech_engine: %s\n' "$value" >"$scenario_file" + { + printf '%s\n' '#!/usr/bin/env bash' + printf '%s\n' 'if [ "$1" = '"'"'to_entries | map([.key, .value] | join("=")) | .[]'"'"' ]; then' + printf '%s\n' ' printf "%s\n" uninstall=false method=virtualenv channel=alpha profile=ovos features=enabled raspberry_pi_tuning=false share_telemetry=false share_usage_telemetry=false' + printf ' printf "%%s\\n" speech_engine=%s\n' "$value" + printf '%s\n' 'elif [ "$1" = '"'"'.features | to_entries | map([.key, .value] | join("=")) | .[]'"'"' ]; then' + printf '%s\n' ' printf "%s\n" skills=true' + printf '%s\n' 'fi' + } >"$yq_mock" + chmod +x "$yq_mock" + + run bash -c ' + cd "$1" + source utils/constants.sh + source utils/common.sh + export SCENARIO_PATH="$2" + export YQ_BINARY_PATH="$3" + source utils/scenario.sh + printf "%s|%s|%s\n" "${SPEECH_ENGINE:-unset}" "${SCENARIO_NOT_SUPPORTED:-false}" "${SCENARIO_ERROR:-}" + ' bash "$BATS_TEST_DIRNAME/../.." "$scenario_file" "$yq_mock" + + rm -f "$scenario_file" "$yq_mock" +} + +@test "speech: a scenario can ask for local or public speech" { + run_scenario_with_speech_engine "local" + assert_success + assert_output "local|false|" + + run_scenario_with_speech_engine "public" + assert_success + assert_output "public|false|" +} + +@test "speech: a scenario with an unknown speech engine is rejected" { + run_scenario_with_speech_engine "offline" + assert_success + assert_output "unset|true|speech_engine: offline (expected: local, public)" +} + +@test "speech: containers get local speech for the ovos and listener profiles only" { + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="alpha" + METHOD="containers" + + PROFILE="ovos" + run local_speech_available + assert_success + + PROFILE="listener" + run local_speech_available + assert_success + + # A containers satellite runs hivemind-docker, whose images carry no speech plugins. + PROFILE="satellite" + run local_speech_available + assert_failure + + PROFILE="server" + run local_speech_available + assert_failure + + PROFILE="ovos" + CHANNEL="testing" + run local_speech_available + assert_failure +} + +# The tasks that write local speech into mycroft.conf, run for real against a stand-in +# for the virtualenv's python: it prints a log line, the way ovos_utils does on stdout, +# then the result speech_setup.py would have printed. +function speech_tasks_setup() { + if ! command -v ansible-playbook >/dev/null 2>&1; then + skip "ansible-playbook is not available" + fi + + SPEECH_DIR="$(mktemp -d "${BATS_TEST_TMPDIR:-/tmp}/speech.XXXXXX")" + SPEECH_CONF="$SPEECH_DIR/mycroft.conf" + SPEECH_RESULT="$SPEECH_DIR/result" + SPEECH_RC="$SPEECH_DIR/rc" + printf '0\n' >"$SPEECH_RC" + + cat >"$SPEECH_DIR/python" <&2 +exit "\$rc" +STUB + chmod +x "$SPEECH_DIR/python" + + cat >"$SPEECH_DIR/play.yml" <<'YAML' +- hosts: localhost + gather_facts: false + tasks: + - name: Run the local speech tasks + ansible.builtin.include_role: + name: ovos_config + tasks_from: speech.yml + - name: Say what the run decided + ansible.builtin.debug: + msg: >- + ENGINES={{ ovos_config_speech_stt_engine }},{{ ovos_config_speech_tts_engine }} + CHANGED={{ ovos_config_speech_configuration.changed | default(false) }} +YAML +} + +function run_speech_tasks() { + printf '%s\n' "$1" >"$SPEECH_RESULT" + run ansible-playbook -i localhost, -c local "$SPEECH_DIR/play.yml" -e "{ + \"ansible_become\": false, + \"ovos_config_speech_python\": \"$SPEECH_DIR/python\", + \"ovos_config_mycroft_conf_path\": \"$SPEECH_CONF\", + \"ovos_config_mycroft_conf_owner\": \"$(id -un)\", + \"ovos_config_mycroft_conf_group\": \"$(id -gn)\", + \"ovos_config_mycroft_conf_mode\": \"0644\", + \"ovos_installer_user\": \"$(id -un)\", + \"ovos_installer_locale\": \"es-es\", + \"ovos_installer_ovos_config_tts_gender\": \"--male\" + }" +} + +LOCAL_STT='{"module": "ovos-stt-plugin-onnx-asr", "fallback_module": "", "ovos-stt-plugin-onnx-asr": {"model": "parakeet-es", "quantization": "int8"}, "ovos-stt-plugin-server": {"urls": ["https://zuazo-es.tigregotico.pt/stt"]}}' +LOCAL_TTS='{"module": "ovos-tts-plugin-phoonnx", "ovos-tts-plugin-phoonnx": {"voice": "OpenVoiceOS/pipertts_es-ES_miro"}}' +PUBLIC_TTS='{"module": "ovos-tts-plugin-server", "ovos-tts-plugin-server": {"voice": "sharvard-medium#M"}}' + +@test "speech: what works here is written, with the public STT server as its fallback" { + speech_tasks_setup + printf '{"lang": "es-es", "websocket": {"port": 8181}}\n' >"$SPEECH_CONF" + + run_speech_tasks "{\"stt\": $LOCAL_STT, \"tts\": $LOCAL_TTS, \"notes\": []}" + assert_success + assert_output --partial "ENGINES=local,local CHANGED=True" + + run jq -c '[.stt.module, .stt.fallback_module, .stt["ovos-stt-plugin-server"].urls[0], .tts["ovos-tts-plugin-phoonnx"].voice, .websocket.port]' "$SPEECH_CONF" + assert_output '["ovos-stt-plugin-onnx-asr","ovos-stt-plugin-server","https://zuazo-es.tigregotico.pt/stt","OpenVoiceOS/pipertts_es-ES_miro",8181]' + + # A second run with the same result leaves the file alone, so it restarts nothing. + run_speech_tasks "{\"stt\": $LOCAL_STT, \"tts\": $LOCAL_TTS, \"notes\": []}" + assert_success + assert_output --partial "ENGINES=local,local CHANGED=False" +} + +@test "speech: a half that no longer works here does not keep the local section an earlier run wrote" { + speech_tasks_setup + # The virtualenv: the template carried both of the last run's sections through. + printf '{"lang": "es-es", "stt": %s, "tts": %s}\n' "$LOCAL_STT" "$LOCAL_TTS" >"$SPEECH_CONF" + + run_speech_tasks "{\"stt\": $LOCAL_STT, \"tts\": null, \"notes\": [\"TTS OpenVoiceOS/pipertts_es-ES_miro does not work here\"]}" + assert_success + assert_output --partial "ENGINES=local,public CHANGED=True" + assert_output --partial "No on-device TTS works for es-es" + + # No tts section: OVOS falls back to its own default, the public server. + run jq -c '[.stt.module, has("tts")]' "$SPEECH_CONF" + assert_output '["ovos-stt-plugin-onnx-asr",false]' +} + +@test "speech: a half that does not work here keeps the public section autoconfigure wrote" { + speech_tasks_setup + # Containers: autoconfigure --online has just written the public voice for the locale. + printf '{"lang": "es-es", "tts": %s}\n' "$PUBLIC_TTS" >"$SPEECH_CONF" + + run_speech_tasks "{\"stt\": $LOCAL_STT, \"tts\": null, \"notes\": []}" + assert_success + assert_output --partial "ENGINES=local,public CHANGED=True" + + run jq -c '[.stt.module, .tts.module, .tts["ovos-tts-plugin-server"].voice]' "$SPEECH_CONF" + assert_output '["ovos-stt-plugin-onnx-asr","ovos-tts-plugin-server","sharvard-medium#M"]' +} + +@test "speech: a setup that cannot run leaves speech public instead of ending the install" { + speech_tasks_setup + printf '{"lang": "es-es", "stt": %s, "tts": %s}\n' "$LOCAL_STT" "$PUBLIC_TTS" >"$SPEECH_CONF" + printf '1\n' >"$SPEECH_RC" + + run_speech_tasks "" + assert_success + assert_output --partial "The local speech setup did not finish: ModuleNotFoundError" + assert_output --partial "ENGINES=public,public CHANGED=True" + + run jq -c '[has("stt"), .tts.module]' "$SPEECH_CONF" + assert_output '[false,"ovos-tts-plugin-server"]' +} + +@test "speech: a mycroft.conf that is not plain JSON is left as it is" { + speech_tasks_setup + printf '// hand-edited\n{"lang": "es-es"}\n' >"$SPEECH_CONF" + cp "$SPEECH_CONF" "$SPEECH_DIR/before" + + run_speech_tasks "{\"stt\": $LOCAL_STT, \"tts\": $LOCAL_TTS, \"notes\": []}" + assert_success + assert_output --partial "is not plain JSON" + assert_output --partial "ENGINES=public,public" + + run cmp "$SPEECH_DIR/before" "$SPEECH_CONF" + assert_success +} + +@test "speech: a containers uninstall removes the model volumes, and still spares STT and TTS servers" { + if ! command -v ansible-playbook >/dev/null 2>&1; then + skip "ansible-playbook is not available" + fi + if ! ansible-doc -t module community.docker.docker_volume >/dev/null 2>&1; then + skip "the community.docker collection is not installed" + fi + + # The real uninstall tasks, with Docker out of reach: the sweep's selection runs + # against what docker_host_info would have listed, and nothing is removed. + local play + play="$(mktemp "${BATS_TEST_TMPDIR:-/tmp}/sweep.XXXXXX.yml")" + cat >"$play" <<'YAML' +- hosts: localhost + gather_facts: false + tasks: + - name: Run the containers uninstall + ansible.builtin.include_role: + name: ovos_containers + tasks_from: uninstall.yml + - name: Say what the sweep picked + ansible.builtin.debug: + msg: "VOLUMES={{ ovos_containers_uninstall_target_volumes | sort | join(',') }}" +YAML + + # The model volumes start "ovos_stt" and "ovos_tts", the prefixes that protect + # STT and TTS servers people run themselves. Removed only while the composition + # directory still existed, they outlived any uninstall after a reboot. + run ansible-playbook -i localhost, -c local "$play" -e '{ + "ansible_become": false, + "ovos_containers_docker_socket_path": "/nonexistent/docker.sock", + "ovos_containers_cmdline_path": "/nonexistent/cmdline.txt", + "ovos_installer_docker_compose_remove_volumes": false, + "ovos_containers_remove_directories": [], + "ovos_containers_repo_directories": [], + "ovos_containers_host_info": {"containers": [], "networks": [], "volumes": [ + {"Name": "ovos_stt_models", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "ovos_tts_models", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "ovos_tts_cache", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "ovos_models", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "ovos_tts_server_data", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "ovos_stt_whisper", "Labels": {"com.docker.compose.project": "ovos"}}, + {"Name": "somebody_else", "Labels": {"com.docker.compose.project": "other"}} + ]} + }' + rm -f "$play" + assert_success + assert_output --partial "VOLUMES=ovos_models,ovos_stt_models,ovos_tts_cache,ovos_tts_models" +} diff --git a/tests/bats/tui_navigation.bats b/tests/bats/tui_navigation.bats index e27bc787..2683207f 100644 --- a/tests/bats/tui_navigation.bats +++ b/tests/bats/tui_navigation.bats @@ -702,3 +702,119 @@ function rendered_titles() { run sort -u "$WHIPTAIL_BACKTITLE_LOG" assert_output "Back: previous screen - keep going back to leave" } + +@test "navigation: the speech question is only asked where local speech is available" { + source tui/navigation.sh + + LOCAL_SPEECH_CAPABLE="true" + CHANNEL="alpha" + METHOD="virtualenv" + PROFILE="ovos" + run tui_flow_step_enabled speech + assert_success + + CHANNEL="testing" + run tui_flow_step_enabled speech + assert_failure + + CHANNEL="alpha" + LOCAL_SPEECH_CAPABLE="false" + run tui_flow_step_enabled speech + assert_failure +} + +@test "tui: capable hardware on alpha is asked where speech runs" { + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="alpha" + + # shellcheck source=tui/main.sh + source tui/main.sh + + run rendered_titles + assert_line --index 5 "Open Voice OS Installation - Features" + assert_line --index 6 "Open Voice OS Installation - Speech" + assert_line --index 7 "Open Voice OS Installation - Summary" + + # Local is what the hardware was checked for, so it is preselected. + assert_equal "$SPEECH_ENGINE" "local" + assert_equal "$(jq -r '.speech_engine' "$INSTALLER_STATE_FILE")" "local" +} + +@test "tui: back on the summary screen returns to the speech question" { + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="alpha" + # 6 features, 7 speech, 8 summary. + cancel_on_call 8 + + # shellcheck source=tui/main.sh + source tui/main.sh + + run rendered_titles + assert_line --index 7 "Open Voice OS Installation - Summary" + assert_line --index 8 "Open Voice OS Installation - Speech" + assert_line --index 9 "Open Voice OS Installation - Summary" +} + +@test "tui: choosing the public servers is kept" { + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="alpha public" + + # shellcheck source=tui/main.sh + source tui/main.sh + + assert_equal "$SPEECH_ENGINE" "public" + assert_equal "$(jq -r '.speech_engine' "$INSTALLER_STATE_FILE")" "public" +} + +@test "tui: updating an install from before the speech question keeps it public" { + # It ran on the public servers, and pressing Enter through an update must not + # switch it over and download gigabytes of models. + EXISTING_INSTANCE="true" + INSTANCE_TYPE="virtualenv" + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="alpha" + + # shellcheck source=tui/main.sh + source tui/main.sh + + run rendered_titles + assert_line "Open Voice OS Installation - Speech" + assert_equal "$SPEECH_ENGINE" "public" +} + +@test "tui: updating an install keeps the speech it chose" { + EXISTING_INSTANCE="true" + INSTANCE_TYPE="virtualenv" + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="alpha" + printf '{"speech_engine": "local"}\n' >"$INSTALLER_STATE_FILE" + + # shellcheck source=tui/main.sh + source tui/main.sh + + assert_equal "$SPEECH_ENGINE" "local" +} + +@test "tui: the testing channel is never asked about speech" { + export LOCAL_SPEECH_CAPABLE="true" + + # shellcheck source=tui/main.sh + source tui/main.sh + + assert_equal "$CHANNEL" "testing" + run rendered_titles + refute_line "Open Voice OS Installation - Speech" +} + +@test "tui: a containers install on alpha is asked where speech runs" { + export LOCAL_SPEECH_CAPABLE="true" + WHIPTAIL_PREFERRED_TAGS="containers alpha" + + # shellcheck source=tui/main.sh + source tui/main.sh + + assert_equal "$METHOD" "containers" + run rendered_titles + assert_line --index 6 "Open Voice OS Installation - Speech" + assert_equal "$SPEECH_ENGINE" "local" +} diff --git a/tests/bats/uninstall_footprint.bats b/tests/bats/uninstall_footprint.bats index 4c9f3db2..3bbb82ce 100644 --- a/tests/bats/uninstall_footprint.bats +++ b/tests/bats/uninstall_footprint.bats @@ -377,6 +377,20 @@ YAML assert_output "hf_their_token" } +@test "footprint: what huggingface_hub records for a download elsewhere is its own bookkeeping" { + play record + install_side_effects + # The local recognizer downloads its model into a directory of its own, and + # huggingface_hub still writes the revision it looked up into the cache. + mkdir -p "$H/.cache/huggingface/hub/models--istupakov--parakeet-tdt-0.6b-v3-onnx/refs" + echo 0123456789abcdef0123456789abcdef01234567 \ + >"$H/.cache/huggingface/hub/models--istupakov--parakeet-tdt-0.6b-v3-onnx/refs/main" + + play restore + run find "$H" -mindepth 1 + assert_output "" +} + @test "footprint: a file in the cache's store stays while one of the user's models links to it" { play record install_side_effects diff --git a/translations/ca-es/strings.json b/translations/ca-es/strings.json index ebe70a69..db7fc813 100644 --- a/translations/ca-es/strings.json +++ b/translations/ca-es/strings.json @@ -56,9 +56,20 @@ "content_key": "Introduïu la clau d'escolta de HiveMind relacionada amb el satèl·lit:", "content_password": "Introduïu la contrasenya d'escolta de HiveMind relacionada amb el satèl·lit:" }, + "speech.sh": { + "title": "Instal·lació de l'Open Voice OS - Veu", + "content": "L'Open Voice OS converteix la veu en text (reconeixement) i llegeix les respostes en veu alta (síntesi).\n\n - local: aquest equip amb servidors públics de reserva. Baixa models de veu (de centenars de megabytes a uns quants gigabytes). Si el reconeixement local falla o no retorna text, aquella gravació s'envia als servidors públics. Si no es pot configurar el reconeixement o la síntesi local, aquella part fa servir servidors públics; l'instal·lador ho indica.\n - public: els servidors de la comunitat processen les teves gravacions i les respostes de veu per internet. Aquests servidors de reserva, mantinguts per voluntaris, no són un servei de producció: poden anar lents o deixar de funcionar en qualsevol moment.\n\nSelecciona on es processa la veu:", + "local_description": "Aquest equip amb servidors públics de reserva", + "public_description": "Servidors públics (sé que poden caure)" + }, "summary.sh": { "title": "Instal·lació de l'Open VoiceOS - Resum", - "content": "Gairebé heu acabat, aquí teniu un resum de les opcions que heu triat en instal·lar l'Open Voice OS:\n\n - Mètode: ${METHOD:-}\n - Versió: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Habilitats: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Habilitats extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Afinació: ${TUNING_SUMMARY_STATE:-}\n\nLes opcions seleccionades durant el procés d'instal·lació de l'Open Voice OS s'han considerat acuradament per a adaptar el nostre sistema a les vostres necessitats i preferències úniques.\n\nUs sembla correcte aquest resum? Si no, selecciona ${BACK_BUTTON:-} per tornar enrere i fer canvis." + "content": "Gairebé heu acabat, aquí teniu un resum de les opcions que heu triat en instal·lar l'Open Voice OS:\n\n - Mètode: ${METHOD:-}\n - Versió: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Habilitats: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Habilitats extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Veu: ${SPEECH_SUMMARY_STATE:-}\n - Afinació: ${TUNING_SUMMARY_STATE:-}\n\nLes opcions seleccionades durant el procés d'instal·lació de l'Open Voice OS s'han considerat acuradament per a adaptar el nostre sistema a les vostres necessitats i preferències úniques.\n\nUs sembla correcte aquest resum? Si no, selecciona ${BACK_BUTTON:-} per tornar enrere i fer canvis.", + "speech_local": "en aquest equip amb servidors públics de reserva", + "speech_public": "servidors públics", + "speech_public_hardware": "servidors públics (local: Pi 5 de 8 GB o més)", + "speech_public_setup": "servidors públics (local no disponible aquí)", + "speech_unused": "aquest perfil no la fa servir" }, "telemetry.sh": { "title": "Instal·lació de l'Open Voice OS - Telemetria", diff --git a/translations/da/strings.json b/translations/da/strings.json index 14f83dce..f11a62a0 100644 --- a/translations/da/strings.json +++ b/translations/da/strings.json @@ -56,9 +56,20 @@ "content_key": "Indtast venligst HiveMind-lytternøglen relateret til satellitten:", "content_password": "Indtast venligst HiveMind-lytteradgangskoden relateret til satellitten:" }, + "speech.sh": { + "title": "Open Voice OS Installation - Tale", + "content": "Open Voice OS omsætter tale til tekst (genkendelse) og læser svar højt (syntese).\n\n - local: denne maskine med offentlige servere som reserve. Henter talemodeller (hundredvis af megabyte til nogle få gigabyte). Hvis lokal genkendelse fejler eller ikke giver tekst, sendes optagelsen til offentlige servere. Hvis lokal genkendelse eller syntese ikke kan sættes op, bruger den del offentlige servere; installationsprogrammet oplyser det.\n - public: fællesskabets servere behandler dine optagelser og talte svar over internettet. Disse frivilligt drevne reserveservere er ikke en produktionstjeneste: de kan blive langsomme eller gå ned når som helst.\n\nVælg, hvor tale skal behandles:", + "local_description": "Denne maskine med offentlige servere som reserve", + "public_description": "Offentlige servere (jeg ved, de kan gå ned)" + }, "summary.sh": { "title": "Open Voice OS Installation - Resume", - "content": "Du er næsten færdig, her er en oversigt over de valg, du har truffet for at installere Open Voice OS:\n\n - Metode: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profil: ${PROFILE:-}\n - Færdigheder: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Ekstra færdigheder: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDe valg, der blev truffet under installationen af ​​Open Voice OS, er blevet nøje overvejet for at skræddersy vores system til dine unikke behov og præferencer.\n\nSer denne oversigt korrekt ud for dig? Hvis ikke, vælg ${BACK_BUTTON:-} for at gå tilbage og foretage ændringer." + "content": "Du er næsten færdig, her er en oversigt over de valg, du har truffet for at installere Open Voice OS:\n\n - Metode: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profil: ${PROFILE:-}\n - Færdigheder: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Ekstra færdigheder: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tale: ${SPEECH_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDe valg, der blev truffet under installationen af ​​Open Voice OS, er blevet nøje overvejet for at skræddersy vores system til dine unikke behov og præferencer.\n\nSer denne oversigt korrekt ud for dig? Hvis ikke, vælg ${BACK_BUTTON:-} for at gå tilbage og foretage ændringer.", + "speech_local": "på denne maskine med offentlige servere som reserve", + "speech_public": "offentlige servere", + "speech_public_hardware": "offentlige servere (lokal: Pi 5 med 8 GB eller mere)", + "speech_public_setup": "offentlige servere (lokal er ikke tilgængelig her)", + "speech_unused": "bruges ikke af denne profil" }, "telemetry.sh": { "title": "Open Voice OS Installation - Telemetri", diff --git a/translations/de-de/strings.json b/translations/de-de/strings.json index 24e18956..45877bce 100644 --- a/translations/de-de/strings.json +++ b/translations/de-de/strings.json @@ -56,9 +56,20 @@ "content_key": "Geben Sie den Zugangsschlüssel des Satelliten für den HiveMind Server ein:", "content_password": "Geben Sie das Passwort des Satelliten für den HiveMind Server ein:" }, + "speech.sh": { + "title": "Open Voice OS-Installation - Sprachverarbeitung", + "content": "Open Voice OS wandelt Sprache in Text um (Erkennung) und liest Antworten vor (Synthese).\n\n - local: dieser Rechner mit öffentlichen Servern als Ersatz. Lädt Sprachmodelle herunter (hunderte Megabyte bis einige Gigabyte). Wenn die lokale Erkennung fehlschlägt oder keinen Text liefert, wird die Aufnahme an öffentliche Server gesendet. Lässt sich die lokale Erkennung oder Synthese nicht einrichten, nutzt dieser Teil öffentliche Server; der Installer weist darauf hin.\n - public: Community-Server verarbeiten Ihre Aufnahmen und gesprochenen Antworten über das Internet. Diese ehrenamtlich betriebenen Ersatzserver sind kein Produktionsdienst: Sie können jederzeit langsamer werden oder ausfallen.\n\nBitte wählen Sie, wo Sprache verarbeitet wird:", + "local_description": "Dieser Rechner mit öffentlichen Servern als Ersatz", + "public_description": "Öffentliche Server (ich weiß, sie können ausfallen)" + }, "summary.sh": { "title": "Open Voice OS Installation - Zusammenfassung", - "content": "Fast fertig. Eine kurze Zusammenfassung:\n\n - Umgebung: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profil: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra-Skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDie Entscheidungen, die während des Installationsprozesses von Open Voice OS getroffen werden, wurden sorgfältig abgewogen, um unser System an Ihre individuellen Bedürfnisse und Vorlieben anzupassen.\n\nStimmen die Einstellungen? Falls nicht, wählen Sie ${BACK_BUTTON:-}, um zurückzugehen und Änderungen vorzunehmen." + "content": "Fast fertig. Eine kurze Zusammenfassung:\n\n - Umgebung: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profil: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra-Skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Sprache: ${SPEECH_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDie Entscheidungen, die während des Installationsprozesses von Open Voice OS getroffen werden, wurden sorgfältig abgewogen, um unser System an Ihre individuellen Bedürfnisse und Vorlieben anzupassen.\n\nStimmen die Einstellungen? Falls nicht, wählen Sie ${BACK_BUTTON:-}, um zurückzugehen und Änderungen vorzunehmen.", + "speech_local": "auf diesem Rechner mit öffentlichen Servern als Ersatz", + "speech_public": "öffentliche Server", + "speech_public_hardware": "öffentliche Server (lokal: Pi 5 mit 8 GB oder besser)", + "speech_public_setup": "öffentliche Server (lokal hier nicht verfügbar)", + "speech_unused": "von diesem Profil nicht genutzt" }, "telemetry.sh": { "title": "Open Voice OS Installation - Telemetrie", diff --git a/translations/en-us/strings.json b/translations/en-us/strings.json index eea5f091..14f93d8f 100644 --- a/translations/en-us/strings.json +++ b/translations/en-us/strings.json @@ -56,14 +56,25 @@ "content_key": "Please enter the HiveMind listener key related to the satellite:", "content_password": "Please enter the HiveMind listener password related to the satellite:" }, + "speech.sh": { + "title": "Open Voice OS Installation - Speech", + "content": "Open Voice OS turns speech into text (recognition) and reads replies aloud (synthesis).\n\n - local: use this machine with public fallback. Downloads speech models (hundreds of megabytes to a few gigabytes). If local recognition fails or returns no text, that recording is sent to public servers. If local recognition or synthesis cannot be set up, that part uses public servers; the installer reports it.\n - public: community servers process your recordings and spoken replies over the internet. These volunteer-run backup servers are not a production service: they can slow down or go offline at any time.\n\nPlease select where speech is processed:", + "local_description": "This machine with public fallback", + "public_description": "Public servers (I accept they can go offline)" + }, "summary.sh": { "title": "Open Voice OS Installation - Summary", - "content": "You are almost done, here is a summary of choices you made to install Open Voice OS:\n\n - Method: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profile: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - HA: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nThe choices made during the Open Voice OS installation process have been carefully considered to tailor our system to your unique needs and preferences.\n\nDoes this summary look correct to you? If not, select ${BACK_BUTTON:-} to go back and make changes.", + "content": "You are almost done, here is a summary of choices you made to install Open Voice OS:\n\n - Method: ${METHOD:-}\n - Version: ${CHANNEL:-}\n - Profile: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - HA: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Speech: ${SPEECH_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nThe choices made during the Open Voice OS installation process have been carefully considered to tailor our system to your unique needs and preferences.\n\nDoes this summary look correct to you? If not, select ${BACK_BUTTON:-} to go back and make changes.", "state_enabled": "enabled", "state_disabled": "disabled", "state_unsupported_profile": "selected (not supported for this profile)", "state_missing_url": "selected (missing URL; will be skipped)", - "state_missing_configuration": "selected (missing configuration; will be skipped)" + "state_missing_configuration": "selected (missing configuration; will be skipped)", + "speech_local": "on this machine with public fallback", + "speech_public": "public servers", + "speech_public_hardware": "public servers (local needs a Pi 5 with 8 GB or better)", + "speech_public_setup": "public servers (local is not available here)", + "speech_unused": "not used by this profile" }, "telemetry.sh": { "title": "Open Voice OS Installation - Telemetry", diff --git a/translations/es-es/strings.json b/translations/es-es/strings.json index dfe89e0b..56ad4694 100644 --- a/translations/es-es/strings.json +++ b/translations/es-es/strings.json @@ -56,9 +56,20 @@ "content_key": "Introduce la clave de escucha HiveMind del satélite:", "content_password": "Introduce la contraseña de escucha HiveMind del satélite:" }, + "speech.sh": { + "title": "Instalación de Open Voice OS - Voz", + "content": "Open Voice OS convierte la voz en texto (reconocimiento) y lee las respuestas en voz alta (síntesis).\n\n - local: este equipo con servidores públicos de respaldo. Descarga modelos de voz (de cientos de megabytes a unos pocos gigabytes). Si el reconocimiento local falla o no devuelve texto, esa grabación se envía a servidores públicos. Si no se puede configurar el reconocimiento o la síntesis local, esa parte usa servidores públicos; el instalador lo indica.\n - public: los servidores de la comunidad procesan sus grabaciones y las respuestas de voz por internet. Estos servidores de respaldo, mantenidos por voluntarios, no son un servicio de producción: pueden ralentizarse o dejar de funcionar en cualquier momento.\n\nSeleccione dónde se procesa la voz:", + "local_description": "Este equipo con servidores públicos de respaldo", + "public_description": "Servidores públicos (sé que pueden caerse)" + }, "summary.sh": { "title": "Instalación de Open Voice OS - Resumen", - "content": "¡Ya casi has terminado! Aquí tienes un resumen de las opciones que has elegido para instalar Open Voice OS:\n\n- Método: ${METHOD:-}\n- Versión: ${CHANNEL:-}\n- Perfil: ${PROFILE:-}\n- Habilidades: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n- Extras: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n- Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n- LLM: ${LLM_SUMMARY_STATE:-}\n- Ajustes: ${TUNING_SUMMARY_STATE:-}\n\nLas decisiones que has tomado durante el proceso de instalación de Open Voice OS han sido cuidadosamente consideradas para adaptar el sistema a tus necesidades y preferencias.\n\n¿Este resumen es correcto? Si no, selecciona ${BACK_BUTTON:-} para volver atrás y hacer cambios." + "content": "¡Ya casi has terminado! Aquí tienes un resumen de las opciones que has elegido para instalar Open Voice OS:\n\n- Método: ${METHOD:-}\n- Versión: ${CHANNEL:-}\n- Perfil: ${PROFILE:-}\n- Habilidades: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n- Extras: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n- Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n- LLM: ${LLM_SUMMARY_STATE:-}\n- Voz: ${SPEECH_SUMMARY_STATE:-}\n- Ajustes: ${TUNING_SUMMARY_STATE:-}\n\nLas decisiones que has tomado durante el proceso de instalación de Open Voice OS han sido cuidadosamente consideradas para adaptar el sistema a tus necesidades y preferencias.\n\n¿Este resumen es correcto? Si no, selecciona ${BACK_BUTTON:-} para volver atrás y hacer cambios.", + "speech_local": "en este equipo con servidores públicos de respaldo", + "speech_public": "servidores públicos", + "speech_public_hardware": "servidores públicos (local: Pi 5 con 8 GB o más)", + "speech_public_setup": "servidores públicos (local no disponible aquí)", + "speech_unused": "este perfil no la usa" }, "telemetry.sh": { "title": "Instalación de Open Voice OS - Telemetría", diff --git a/translations/eu-es/strings.json b/translations/eu-es/strings.json index 72aab672..2a332a17 100644 --- a/translations/eu-es/strings.json +++ b/translations/eu-es/strings.json @@ -55,9 +55,20 @@ "content_key": "Mesedez, sartu satelitearekin erlazionatutako HiveMind entzule-gakoa:", "content_password": "Mesedez, idatzi satelitearekin erlazionatutako HiveMind entzulearen pasahitza:" }, + "speech.sh": { + "title": "Open Voice OS instalazioa - Ahotsa", + "content": "Open Voice OS-k ahotsa testu bihurtzen du (ezagutza) eta erantzunak ozen irakurtzen ditu (sintesia).\n\n - local: makina hau, zerbitzari publikoak ordezko gisa erabiliz. Ahots-ereduak deskargatzen ditu (ehunka megabytetik gigabyte gutxi batzuetara). Ezagutza lokalak huts egiten badu edo testurik itzultzen ez badu, grabazioa zerbitzari publikoetara bidaltzen da. Ezagutza edo sintesi lokala ezin bada konfiguratu, zati horrek zerbitzari publikoak erabiltzen ditu; instalatzaileak horren berri ematen du.\n - public: komunitatearen zerbitzariek zure grabazioak eta ahots-erantzunak Internet bidez prozesatzen dituzte. Boluntarioek mantendutako ordezko zerbitzari hauek ez dira produkzio-zerbitzu bat: edozein unetan moteldu edo gelditu daitezke.\n\nAukeratu non prozesatzen den ahotsa:", + "local_description": "Makina hau, zerbitzari publikoak ordezko gisa", + "public_description": "Zerbitzari publikoak (badakit eror daitezkeela)" + }, "summary.sh": { "title": "Ireki Voice OS instalazioa - Laburpena", - "content": "Ia amaitu duzu, hona hemen Open Voice OS instalatzeko egin dituzun aukeren laburpena:\n\n - Metodoa: ${METHOD:-}\n - Bertsioa: ${CHANNEL:-}\n - Profila: ${PROFILE:-}\n - Trebetasunak: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Trebetasun gehigarriak: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Afinazioa: ${TUNING_SUMMARY_STATE:-}\n\nOpen Voice OS instalazio-prozesuan egindako aukerak arretaz aztertu dira gure sistema zure behar eta lehentasun berezietara egokitzeko.\n\nLaburpen hau zuzena iruditzen zaizu? Hala ez bada, hautatu ${BACK_BUTTON:-} atzera egiteko eta aldaketak egiteko." + "content": "Ia amaitu duzu, hona hemen Open Voice OS instalatzeko egin dituzun aukeren laburpena:\n\n - Metodoa: ${METHOD:-}\n - Bertsioa: ${CHANNEL:-}\n - Profila: ${PROFILE:-}\n - Trebetasunak: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Trebetasun gehigarriak: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Ahotsa: ${SPEECH_SUMMARY_STATE:-}\n - Afinazioa: ${TUNING_SUMMARY_STATE:-}\n\nOpen Voice OS instalazio-prozesuan egindako aukerak arretaz aztertu dira gure sistema zure behar eta lehentasun berezietara egokitzeko.\n\nLaburpen hau zuzena iruditzen zaizu? Hala ez bada, hautatu ${BACK_BUTTON:-} atzera egiteko eta aldaketak egiteko.", + "speech_local": "makina honetan, zerbitzari publikoak ordezko gisa", + "speech_public": "zerbitzari publikoak", + "speech_public_hardware": "zerbitzari publikoak (lokala: 8 GB-ko Pi 5 edo hobea)", + "speech_public_setup": "zerbitzari publikoak (lokala ez dago hemen)", + "speech_unused": "profil honek ez du erabiltzen" }, "telemetry.sh": { "title": "Ireki Voice OS instalazioa - Telemetria", diff --git a/translations/fr-fr/strings.json b/translations/fr-fr/strings.json index 2dce6d1e..480bca3c 100644 --- a/translations/fr-fr/strings.json +++ b/translations/fr-fr/strings.json @@ -56,9 +56,20 @@ "content_key": "Veuillez saisir la clé d'HiveMind Listener liée au satellite:", "content_password": "Veuillez saisir le mot de passe d'HiveMind Listener lié au satellite:" }, + "speech.sh": { + "title": "Open Voice OS Installation - Traitement de la parole", + "content": "Open Voice OS transforme la parole en texte (reconnaissance) et lit les réponses à voix haute (synthèse).\n\n - local : cette machine avec des serveurs publics en secours. Télécharge des modèles vocaux (de quelques centaines de mégaoctets à quelques gigaoctets). Si la reconnaissance locale échoue ou ne renvoie aucun texte, cet enregistrement est envoyé aux serveurs publics. Si la reconnaissance ou la synthèse locale ne peut pas être configurée, cette partie utilise les serveurs publics ; l'installateur le signale.\n - public : les serveurs communautaires traitent vos enregistrements et les réponses vocales via Internet. Ces serveurs de secours, gérés par des bénévoles, ne sont pas un service de production : ils peuvent ralentir ou s'arrêter à tout moment.\n\nVeuillez sélectionner où la parole est traitée :", + "local_description": "Cette machine avec des serveurs publics en secours", + "public_description": "Serveurs publics (je sais qu'ils peuvent tomber)" + }, "summary.sh": { "title": "Open Voice OS Installation - Résumé", - "content": "Vous y êtes presque ! Voici un résumé des choix que vous avez effectués pour l'installation d'Open Voice OS :\n\n - Méthode d'installation : ${METHOD:-}\n - Canal de déploiement : ${CHANNEL:-}\n - Profil d'installation : ${PROFILE:-}\n - Compétences par défaut : ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Compétences supplémentaires : ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant : ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM : ${LLM_SUMMARY_STATE:-}\n - Réglages Raspberry Pi : ${TUNING_SUMMARY_STATE:-}\n\nLes choix effectués lors du processus d'installation d'Open Voice OS ont été soigneusement étudiés pour adapter notre système à vos besoins et préférences.\n\nEst-ce que tout cela vous semble correct ? Sinon, sélectionnez ${BACK_BUTTON:-} pour revenir en arrière et modifier vos choix." + "content": "Vous y êtes presque ! Voici un résumé des choix que vous avez effectués pour l'installation d'Open Voice OS :\n\n - Méthode d'installation : ${METHOD:-}\n - Canal de déploiement : ${CHANNEL:-}\n - Profil d'installation : ${PROFILE:-}\n - Compétences par défaut : ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Compétences supplémentaires : ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant : ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM : ${LLM_SUMMARY_STATE:-}\n - Traitement de la parole : ${SPEECH_SUMMARY_STATE:-}\n - Réglages Raspberry Pi : ${TUNING_SUMMARY_STATE:-}\n\nLes choix effectués lors du processus d'installation d'Open Voice OS ont été soigneusement étudiés pour adapter notre système à vos besoins et préférences.\n\nEst-ce que tout cela vous semble correct ? Sinon, sélectionnez ${BACK_BUTTON:-} pour revenir en arrière et modifier vos choix.", + "speech_local": "sur cette machine avec des serveurs publics en secours", + "speech_public": "serveurs publics", + "speech_public_hardware": "serveurs publics (local : Pi 5 8 Go minimum)", + "speech_public_setup": "serveurs publics (local indisponible ici)", + "speech_unused": "non utilisé par ce profil" }, "telemetry.sh": { "title": "Open Voice OS Installation - Télémétrie", diff --git a/translations/gl-es/strings.json b/translations/gl-es/strings.json index 299440b0..199aba99 100644 --- a/translations/gl-es/strings.json +++ b/translations/gl-es/strings.json @@ -56,9 +56,20 @@ "content_key": "Introduce a clave do HiveMind Listener asociada ao satélite:", "content_password": "Introduce o contrasinal do HiveMind Listener asociado ao satélite:" }, + "speech.sh": { + "title": "Instalación de Open Voice OS - Voz", + "content": "Open Voice OS converte a voz en texto (recoñecemento) e le as respostas en voz alta (síntese).\n\n - local: este equipo con servidores públicos de respaldo. Descarga modelos de voz (de centos de megabytes a uns poucos gigabytes). Se o recoñecemento local falla ou non devolve texto, esa gravación envíase a servidores públicos. Se non se pode configurar o recoñecemento ou a síntese local, esa parte usa servidores públicos; o instalador indícao.\n - public: os servidores da comunidade procesan as túas gravacións e as respostas de voz por internet. Estes servidores de respaldo, mantidos por voluntarios, non son un servizo de produción: poden ir máis lentos ou deixar de funcionar en calquera momento.\n\nSelecciona onde se procesa a voz:", + "local_description": "Este equipo con servidores públicos de respaldo", + "public_description": "Servidores públicos (sei que poden caer)" + }, "summary.sh": { "title": "Instalación de Open Voice OS - Recapitulación", - "content": "Estás a piques de rematar. Aquí tes un resumo das opcións que escolliches para instalar Open Voice OS:\n\n - Método: ${METHOD:-}\n - Versión: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Habilidades: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Habilidades adicionais: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Axustes: ${TUNING_SUMMARY_STATE:-}\n\nAs decisións tomadas durante a instalación de Open Voice OS foron coidadosamente pensadas para adaptar o noso sistema ás túas necesidades e preferencias.\n\nÉ correcto este resumo? Se non, selecciona ${BACK_BUTTON:-} para volver atrás e facer cambios." + "content": "Estás a piques de rematar. Aquí tes un resumo das opcións que escolliches para instalar Open Voice OS:\n\n - Método: ${METHOD:-}\n - Versión: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Habilidades: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Habilidades adicionais: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Voz: ${SPEECH_SUMMARY_STATE:-}\n - Axustes: ${TUNING_SUMMARY_STATE:-}\n\nAs decisións tomadas durante a instalación de Open Voice OS foron coidadosamente pensadas para adaptar o noso sistema ás túas necesidades e preferencias.\n\nÉ correcto este resumo? Se non, selecciona ${BACK_BUTTON:-} para volver atrás e facer cambios.", + "speech_local": "neste equipo con servidores públicos de respaldo", + "speech_public": "servidores públicos", + "speech_public_hardware": "servidores públicos (local: Pi 5 de 8 GB ou mellor)", + "speech_public_setup": "servidores públicos (local non dispoñible aquí)", + "speech_unused": "este perfil non a usa" }, "telemetry.sh": { "title": "Instalación de Open Voice OS - Telemetría", diff --git a/translations/hi-in/strings.json b/translations/hi-in/strings.json index 74c8f951..64f330a2 100644 --- a/translations/hi-in/strings.json +++ b/translations/hi-in/strings.json @@ -55,9 +55,20 @@ "content_key": "हाइवमाइंड सुनने वाले उपग्रह से संबंधित कुंजी दर्ज करें:", "content_password": "हाइवमाइंड सुनने वाले उपग्रह से संबंधित पासवर्ड दर्ज करें:" }, + "speech.sh": { + "title": "Open Voice OS Installation - आवाज़", + "content": "Open Voice OS आवाज़ को टेक्स्ट में बदलता है (वाक् पहचान) और जवाब बोलकर सुनाता है (वाक् संश्लेषण)।\n\n - local: यह मशीन, ज़रूरत पड़ने पर सार्वजनिक सर्वरों की मदद से। वाक् मॉडल डाउनलोड होते हैं (कुछ सौ मेगाबाइट से कुछ गीगाबाइट तक)। अगर स्थानीय पहचान विफल हो या कोई टेक्स्ट न लौटाए, तो वह रिकॉर्डिंग सार्वजनिक सर्वरों पर भेजी जाती है। अगर स्थानीय पहचान या संश्लेषण स्थापित नहीं हो पाता, तो उस हिस्से के लिए सार्वजनिक सर्वर इस्तेमाल होते हैं; इंस्टॉलर इसकी जानकारी देता है।\n - public: समुदाय के सर्वर आपकी रिकॉर्डिंग और बोले जाने वाले जवाब इंटरनेट पर प्रोसेस करते हैं। स्वयंसेवकों द्वारा चलाए जा रहे ये वैकल्पिक सर्वर नियमित सेवा नहीं हैं: ये धीमे पड़ सकते हैं या कभी भी बंद हो सकते हैं।\n\nकृपया चुनें कि आवाज़ कहाँ प्रोसेस हो:", + "local_description": "यह मशीन, ज़रूरत पर सार्वजनिक सर्वरों की मदद से", + "public_description": "सार्वजनिक सर्वर (ये कभी भी बंद हो सकते हैं)" + }, "summary.sh": { "title": "Open Voice OS Installation - सारांश", - "content": "आपका काम लगभग पूरा हो चुका है, ओपन वॉयस ओएस (Open Voice OS) स्थापित करने के लिए आपके द्वारा चुने गए विकल्पों का सारांश यहां दिया गया है:\n - विधि: ${METHOD:-}\n - संस्करण: ${CHANNEL:-}\n - प्रोफाइल: ${PROFILE:-}\n - कौशल: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - अतिरिक्त कौशल: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - ट्यूनिंग: ${TUNING_SUMMARY_STATE:-}\n\nओपन वॉयस ओएस (Open Voice OS) इंस्टॉलेशन प्रक्रिया के दौरान चुने गए विकल्पों पर हमारे सिस्टम (System) को आपकी विशिष्ट आवश्यकताओं और प्राथमिकताओं के अनुरूप बनाने के लिए सावधानीपूर्वक विचार किया गया है।\nक्या आपको यह सही लगता है? यदि नहीं, तो वापस जाने और बदलाव करने के लिए ${BACK_BUTTON:-} चुनें।" + "content": "आपका काम लगभग पूरा हो चुका है, ओपन वॉयस ओएस (Open Voice OS) स्थापित करने के लिए आपके द्वारा चुने गए विकल्पों का सारांश यहां दिया गया है:\n - विधि: ${METHOD:-}\n - संस्करण: ${CHANNEL:-}\n - प्रोफाइल: ${PROFILE:-}\n - कौशल: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - अतिरिक्त कौशल: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - आवाज़: ${SPEECH_SUMMARY_STATE:-}\n - ट्यूनिंग: ${TUNING_SUMMARY_STATE:-}\n\nओपन वॉयस ओएस (Open Voice OS) इंस्टॉलेशन प्रक्रिया के दौरान चुने गए विकल्पों पर हमारे सिस्टम (System) को आपकी विशिष्ट आवश्यकताओं और प्राथमिकताओं के अनुरूप बनाने के लिए सावधानीपूर्वक विचार किया गया है।\nक्या आपको यह सही लगता है? यदि नहीं, तो वापस जाने और बदलाव करने के लिए ${BACK_BUTTON:-} चुनें।", + "speech_local": "इस मशीन पर, ज़रूरत पर सार्वजनिक सर्वरों की मदद से", + "speech_public": "सार्वजनिक सर्वर", + "speech_public_hardware": "सार्वजनिक सर्वर (लोकल: 8 GB वाला Pi 5 या बेहतर)", + "speech_public_setup": "सार्वजनिक सर्वर (यहाँ लोकल उपलब्ध नहीं)", + "speech_unused": "यह प्रोफ़ाइल इसका उपयोग नहीं करती" }, "telemetry.sh": { "title": "Open Voice OS Installation - टेलीमेटरी", diff --git a/translations/it-it/strings.json b/translations/it-it/strings.json index 5b03927d..f85dd236 100644 --- a/translations/it-it/strings.json +++ b/translations/it-it/strings.json @@ -56,9 +56,20 @@ "content_key": "Immettere la chiave di ascolto HiveMind del satellite:", "content_password": "Inserire la password dell'ascoltatore di HiveMind del satellite:" }, + "speech.sh": { + "title": "Installazione di Open Voice OS - Voce", + "content": "Open Voice OS trasforma la voce in testo (riconoscimento) e legge le risposte ad alta voce (sintesi).\n\n - local: questa macchina con server pubblici di riserva. Scarica modelli vocali (da centinaia di megabyte a pochi gigabyte). Se il riconoscimento locale fallisce o non restituisce testo, quella registrazione viene inviata ai server pubblici. Se non è possibile configurare il riconoscimento o la sintesi locale, quella parte usa i server pubblici; l'installer lo segnala.\n - public: i server della comunità elaborano le tue registrazioni e le risposte vocali tramite internet. Questi server di riserva, gestiti da volontari, non sono un servizio di produzione: possono rallentare o andare offline in qualsiasi momento.\n\nSeleziona dove elaborare la voce:", + "local_description": "Questa macchina con server pubblici di riserva", + "public_description": "Server pubblici (so che possono andare offline)" + }, "summary.sh": { "title": "Installazione di Open Voice OS - Riassunto", - "content": "Abbiamo quasi finito. Qui c'è un riassunto delle opzioni scelte per installare Open Voice OS:\n\n - Distribuzione: ${METHOD:-}\n - Versione: ${CHANNEL:-}\n - Profilo: ${PROFILE:-}\n - Competenze: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Competenze extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Ottimizzazione: ${TUNING_SUMMARY_STATE:-}\n\nLe decisioni prese durante il processo di installazione di Open Voice OS sono state attentamente valutate per personalizzare il nostro sistema in base alle tue esigenze e preferenze individuali.\n\nLe impostazioni sono corrette? In caso contrario, seleziona ${BACK_BUTTON:-} per tornare indietro e apportare modifiche." + "content": "Abbiamo quasi finito. Qui c'è un riassunto delle opzioni scelte per installare Open Voice OS:\n\n - Distribuzione: ${METHOD:-}\n - Versione: ${CHANNEL:-}\n - Profilo: ${PROFILE:-}\n - Competenze: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Competenze extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Voce: ${SPEECH_SUMMARY_STATE:-}\n - Ottimizzazione: ${TUNING_SUMMARY_STATE:-}\n\nLe decisioni prese durante il processo di installazione di Open Voice OS sono state attentamente valutate per personalizzare il nostro sistema in base alle tue esigenze e preferenze individuali.\n\nLe impostazioni sono corrette? In caso contrario, seleziona ${BACK_BUTTON:-} per tornare indietro e apportare modifiche.", + "speech_local": "su questa macchina con server pubblici di riserva", + "speech_public": "server pubblici", + "speech_public_hardware": "server pubblici (locale: Pi 5 con 8 GB o superiore)", + "speech_public_setup": "server pubblici (locale non disponibile qui)", + "speech_unused": "non usata da questo profilo" }, "telemetry.sh": { "title": "Installazione di Open Voice OS - Telemetria", diff --git a/translations/kab-dz/strings.json b/translations/kab-dz/strings.json index c35184bb..49359968 100644 --- a/translations/kab-dz/strings.json +++ b/translations/kab-dz/strings.json @@ -54,14 +54,25 @@ "content_key": "Ttxil, sekcem-d tsarut n umesseflid n HiveMind yeqqnen ɣer ugensa-nni:", "content_password": "Ttxil, sekcem-d awal n uɛeddi n umesseflid HiveMind yeqqnen ɣer ugensa-nni:" }, + "speech.sh": { + "title": "Asbeddi n Open Voice OS - Taɣect", + "content": "Open Voice OS yettbeddil taɣect ɣer uḍris (aɛqal) yerna yeqqar-d tiririyin s taɣect (asuddes).\n\n - local: tamacint-a s yiqeddacen izayazen d tallalt. Ad d-yessader timudmin n taɣect (seg yimiḍ n MB alamma d kra n GB). Ma yecceḍ uɛqal adigan neɣ ur d-yerri ara aḍris, asekles-nni ad yettwazen ɣer yiqeddacen izayazen. Ma ur yezmir ara ad yesbedd aɛqal neɣ asuddes adigan, aḥric-nni ad yesseqdec iqeddacen izayazen; amesbeddi ad d-yefk talɣut ɣef waya.\n - public: iqeddacen n temɣiwant senfaren iseklasen-ik d tririyin n taɣect s Internet. Iqeddacen-a n tallalt ttwasefraken s lebɣi, mačči d ameẓlu n tfalit: zemren ad ẓẓayen neɣ ad ḥbesen melmi tebɣu tili.\n\nTtxil-k fren anida ara tettwasenfar taɣect:", + "local_description": "Tamacint-a s yiqeddacen izayazen d tallalt", + "public_description": "Iqeddacen izayazen (ẓriɣ zemren ad ḥbesen)" + }, "summary.sh": { "title": "Asbeddi n Open Voice OS - Agzul", - "content": "Qrib ad tekfuḍ, ha-t-an ugzul n textiṛiyin i tgiḍ akken ad tesbeddeḍ anagraw n Open Voice OS\n\n - Tarrayt: ${METHOD:-}\n - Lqem: ${CHANNEL:-}\n - Amaɣnu: ${PROFILE:-}\n - Tussniwin: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Tussniwin timernanin: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tamellit: ${TUNING_SUMMARY_STATE:-}\n\nIfranen i d-yettwaxedmen deg ukala n usbeddi n Open Voice OS ttwafernen s ttawil akked ufeṣṣel i wakken ad yeddu unagraw-nneɣ akken i teḥwaǧeḍ.\n\nYettban-ak-d iṣeḥḥa ugzul-a? Ma ulac, fren ${BACK_BUTTON:-} akken ad tuɣaleḍ ɣer deffir ad tbeddleḍ.", + "content": "Qrib ad tekfuḍ, ha-t-an ugzul n textiṛiyin i tgiḍ akken ad tesbeddeḍ anagraw n Open Voice OS\n\n - Tarrayt: ${METHOD:-}\n - Lqem: ${CHANNEL:-}\n - Amaɣnu: ${PROFILE:-}\n - Tussniwin: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Tussniwin timernanin: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Taɣect: ${SPEECH_SUMMARY_STATE:-}\n - Tamellit: ${TUNING_SUMMARY_STATE:-}\n\nIfranen i d-yettwaxedmen deg ukala n usbeddi n Open Voice OS ttwafernen s ttawil akked ufeṣṣel i wakken ad yeddu unagraw-nneɣ akken i teḥwaǧeḍ.\n\nYettban-ak-d iṣeḥḥa ugzul-a? Ma ulac, fren ${BACK_BUTTON:-} akken ad tuɣaleḍ ɣer deffir ad tbeddleḍ.", "state_enabled": "d urmid", "state_disabled": "d arurmid", "state_unsupported_profile": "yettawfran (ur yettwasefrek ara deg umaɣnu-a)", "state_missing_url": "yettwafran (txuṣṣ URL; as yettwazgal)", - "state_missing_configuration": "yettwafran (txuṣṣ tawila; as yettwazgal)" + "state_missing_configuration": "yettwafran (txuṣṣ tawila; as yettwazgal)", + "speech_local": "ɣef tmacint-a s yiqeddacen izayazen d tallalt", + "speech_public": "iqeddacen izayazen", + "speech_public_hardware": "iqeddacen izayazen (adigan: Pi 5 s 8 GB neɣ ugar)", + "speech_public_setup": "iqeddacen izayazen (adigan ulac-it dagi)", + "speech_unused": "ur yettwaseqdac ara deg umaɣnu-a" }, "telemetry.sh": { "title": "Asbeddi n Open Voice OS - Aktili", diff --git a/translations/nl-nl/strings.json b/translations/nl-nl/strings.json index 9c12a84f..82d466e5 100644 --- a/translations/nl-nl/strings.json +++ b/translations/nl-nl/strings.json @@ -56,9 +56,20 @@ "content_key": "Voer de HiveMind listener sleutel in, behorende bij de satelliet:", "content_password": "Voer het HiveMind listener wachtwoord in, behorende bij de satelliet:" }, + "speech.sh": { + "title": "Open Voice OS Installatie - Spraak", + "content": "Open Voice OS zet spraak om in tekst (herkenning) en leest antwoorden voor (synthese).\n\n - local: deze machine met openbare servers als reserve. Downloadt spraakmodellen (honderden megabytes tot enkele gigabytes). Als lokale herkenning mislukt of geen tekst oplevert, gaat die opname naar openbare servers. Als lokale herkenning of synthese niet kan worden ingesteld, gebruikt dat onderdeel openbare servers; het installatieprogramma meldt dit.\n - public: servers van de community verwerken je opnames en gesproken antwoorden via internet. Deze reserveservers draaien dankzij vrijwilligers en zijn geen productiedienst: ze kunnen op elk moment traag worden of uitvallen.\n\nSelecteer waar spraak wordt verwerkt:", + "local_description": "Deze machine met openbare servers als reserve", + "public_description": "Openbare servers (ik weet dat ze kunnen uitvallen)" + }, "summary.sh": { "title": "OpenVoice OS Installatie - Samenvatting", - "content": "Bijna klaar. Een korte samenvatting:\n\n - Omgeving: ${METHOD:-}\n - Versie: ${CHANNEL:-}\n - Profiel: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDe beslissingen die zijn genomen tijdens het installatieproces van OpenVoice OS zijn zorgvuldig gemaakt om ons systeem aan te passen aan je individuele behoeften en voorkeuren.\n\nZijn de instellingen zo in orde? Zo niet, kies ${BACK_BUTTON:-} om terug te gaan en wijzigingen te maken." + "content": "Bijna klaar. Een korte samenvatting:\n\n - Omgeving: ${METHOD:-}\n - Versie: ${CHANNEL:-}\n - Profiel: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Extra skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Spraak: ${SPEECH_SUMMARY_STATE:-}\n - Tuning: ${TUNING_SUMMARY_STATE:-}\n\nDe beslissingen die zijn genomen tijdens het installatieproces van OpenVoice OS zijn zorgvuldig gemaakt om ons systeem aan te passen aan je individuele behoeften en voorkeuren.\n\nZijn de instellingen zo in orde? Zo niet, kies ${BACK_BUTTON:-} om terug te gaan en wijzigingen te maken.", + "speech_local": "op deze machine met openbare servers als reserve", + "speech_public": "openbare servers", + "speech_public_hardware": "openbare servers (lokaal: Pi 5 met 8 GB of beter)", + "speech_public_setup": "openbare servers (lokaal hier niet beschikbaar)", + "speech_unused": "niet gebruikt door dit profiel" }, "telemetry.sh": { "title": "OpenVoice OS Installatie - Telemetrie", diff --git a/translations/pl-pl/strings.json b/translations/pl-pl/strings.json index 298d74dc..528f270f 100644 --- a/translations/pl-pl/strings.json +++ b/translations/pl-pl/strings.json @@ -56,9 +56,20 @@ "content_key": "Proszę wprowadzić klucz nasłuchu HiveMind związany z satelitą:", "content_password": "Proszę wprowadzić hasło nasłuchujące HiveMind powiązane z satelitą:" }, + "speech.sh": { + "title": "Instalacja Open Voice OS - Mowa", + "content": "Open Voice OS zamienia mowę na tekst (rozpoznawanie) i czyta odpowiedzi na głos (synteza).\n\n - local: ten komputer z serwerami publicznymi w rezerwie. Pobiera modele mowy (od setek megabajtów do kilku gigabajtów). Jeśli lokalne rozpoznawanie zawiedzie lub nie zwróci tekstu, nagranie jest wysyłane do serwerów publicznych. Jeśli nie da się skonfigurować lokalnego rozpoznawania lub syntezy, ta część używa serwerów publicznych; instalator o tym informuje.\n - public: serwery społeczności przetwarzają Twoje nagrania i odpowiedzi głosowe przez internet. Te zapasowe serwery prowadzone przez wolontariuszy nie są usługą produkcyjną: mogą zwolnić lub przestać działać w każdej chwili.\n\nWybierz, gdzie ma być przetwarzana mowa:", + "local_description": "Ten komputer z serwerami publicznymi w rezerwie", + "public_description": "Serwery publiczne (wiem, że mogą przestać działać)" + }, "summary.sh": { "title": "Instalacja Open Voice OS – Podsumowanie", - "content": "Już prawie skończyłeś, oto podsumowanie wyborów dokonanych podczas instalacji Open Voice OS:\n\n- Metoda: ${METHOD:-}\n- Wersja: ${CHANNEL:-}\n- Profil: ${PROFILE:-}\n- Umiejętności: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n- Dodatkowe umiejętności: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n- Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n- LLM: ${LLM_SUMMARY_STATE:-}\n- Strojenie: ${TUNING_SUMMARY_STATE:-}\n\nWybory dokonane podczas instalacji Open Voice OS zostały starannie rozważone, aby dostosować nasz system do Twoich unikalnych potrzeb i preferencji.\n\nCzy to podsumowanie wydaje Ci się poprawne? Jeśli nie, wybierz ${BACK_BUTTON:-}, aby wrócić i wprowadzić zmiany." + "content": "Już prawie skończyłeś, oto podsumowanie wyborów dokonanych podczas instalacji Open Voice OS:\n\n- Metoda: ${METHOD:-}\n- Wersja: ${CHANNEL:-}\n- Profil: ${PROFILE:-}\n- Umiejętności: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n- Dodatkowe umiejętności: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n- Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n- LLM: ${LLM_SUMMARY_STATE:-}\n- Mowa: ${SPEECH_SUMMARY_STATE:-}\n- Strojenie: ${TUNING_SUMMARY_STATE:-}\n\nWybory dokonane podczas instalacji Open Voice OS zostały starannie rozważone, aby dostosować nasz system do Twoich unikalnych potrzeb i preferencji.\n\nCzy to podsumowanie wydaje Ci się poprawne? Jeśli nie, wybierz ${BACK_BUTTON:-}, aby wrócić i wprowadzić zmiany.", + "speech_local": "na tym komputerze z serwerami publicznymi w rezerwie", + "speech_public": "serwery publiczne", + "speech_public_hardware": "serwery publiczne (lokalnie: Pi 5 z 8 GB lub lepszy)", + "speech_public_setup": "serwery publiczne (tryb lokalny tu niedostępny)", + "speech_unused": "nieużywana w tym profilu" }, "telemetry.sh": { "title": "Instalacja Open Voice OS - Telemetria", diff --git a/translations/pt-pt/strings.json b/translations/pt-pt/strings.json index ed3bbf08..3f147349 100644 --- a/translations/pt-pt/strings.json +++ b/translations/pt-pt/strings.json @@ -56,9 +56,20 @@ "content_key": "Introduza a chave de acesso do servidor HiveMind para o satélite:", "content_password": "Introduza a palavra-passe do servidor HiveMind para o satélite:" }, + "speech.sh": { + "title": "Open Voice OS Instalação - Voz", + "content": "O Open Voice OS converte a voz em texto (reconhecimento) e lê as respostas em voz alta (síntese).\n\n - local: esta máquina com servidores públicos de reserva. Descarrega modelos de voz (de centenas de megabytes a alguns gigabytes). Se o reconhecimento local falhar ou não devolver texto, essa gravação é enviada para servidores públicos. Se não for possível configurar o reconhecimento ou a síntese local, essa parte usa servidores públicos; o instalador informa-o.\n - public: os servidores da comunidade processam as suas gravações e as respostas de voz pela internet. Estes servidores de reserva, mantidos por voluntários, não são um serviço de produção: podem ficar lentos ou indisponíveis a qualquer momento.\n\nSeleccione onde a voz é processada:", + "local_description": "Esta máquina com servidores públicos de reserva", + "public_description": "Servidores públicos (sei que podem falhar)" + }, "summary.sh": { "title": "Open Voice OS Instalação - Resumo", - "content": "Quase a terminar. Um breve resumo:\n\n - Implementação: ${METHOD:-}\n - Versão: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Skills extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Otimização: ${TUNING_SUMMARY_STATE:-}\n\nAs decisões tomadas durante o processo de instalação do Open Voice OS foram cuidadosamente consideradas para personalizar o nosso sistema de acordo com as suas necessidades e preferências individuais.\n\nAs definições estão correctas? Se não, seleccione ${BACK_BUTTON:-} para voltar atrás e fazer alterações." + "content": "Quase a terminar. Um breve resumo:\n\n - Implementação: ${METHOD:-}\n - Versão: ${CHANNEL:-}\n - Perfil: ${PROFILE:-}\n - Skills: ${FEATURE_SKILLS_SUMMARY_STATE:-}\n - Skills extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-}\n - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-}\n - LLM: ${LLM_SUMMARY_STATE:-}\n - Voz: ${SPEECH_SUMMARY_STATE:-}\n - Otimização: ${TUNING_SUMMARY_STATE:-}\n\nAs decisões tomadas durante o processo de instalação do Open Voice OS foram cuidadosamente consideradas para personalizar o nosso sistema de acordo com as suas necessidades e preferências individuais.\n\nAs definições estão correctas? Se não, seleccione ${BACK_BUTTON:-} para voltar atrás e fazer alterações.", + "speech_local": "nesta máquina com servidores públicos de reserva", + "speech_public": "servidores públicos", + "speech_public_hardware": "servidores públicos (local: Pi 5 com 8 GB ou melhor)", + "speech_public_setup": "servidores públicos (local indisponível aqui)", + "speech_unused": "não utilizado por este perfil" }, "telemetry.sh": { "title": "Open Voice OS Instalação - Telemetria", diff --git a/tui/locales/ca-es/speech.sh b/tui/locales/ca-es/speech.sh new file mode 100644 index 00000000..cc5c01b6 --- /dev/null +++ b/tui/locales/ca-es/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +L'Open Voice OS converteix la veu en text (reconeixement) i llegeix les respostes en veu alta (síntesi). + + - local: aquest equip amb servidors públics de reserva. Baixa models de veu (de centenars de megabytes a uns quants gigabytes). Si el reconeixement local falla o no retorna text, aquella gravació s'envia als servidors públics. Si no es pot configurar el reconeixement o la síntesi local, aquella part fa servir servidors públics; l'instal·lador ho indica. + - public: els servidors de la comunitat processen les teves gravacions i les respostes de veu per internet. Aquests servidors de reserva, mantinguts per voluntaris, no són un servei de producció: poden anar lents o deixar de funcionar en qualsevol moment. + +Selecciona on es processa la veu: +" +TITLE="Instal·lació de l'Open Voice OS - Veu" +LOCAL_DESCRIPTION="Aquest equip amb servidors públics de reserva" +PUBLIC_DESCRIPTION="Servidors públics (sé que poden caure)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/ca-es/summary.sh b/tui/locales/ca-es/summary.sh index e7f0c5b2..17d00208 100644 --- a/tui/locales/ca-es/summary.sh +++ b/tui/locales/ca-es/summary.sh @@ -9,6 +9,7 @@ Gairebé heu acabat, aquí teniu un resum de les opcions que heu triat en instal - Habilitats extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Veu: ${SPEECH_SUMMARY_STATE:-} - Afinació: ${TUNING_SUMMARY_STATE:-} Les opcions seleccionades durant el procés d'instal·lació de l'Open Voice OS s'han considerat acuradament per a adaptar el nostre sistema a les vostres necessitats i preferències úniques. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="en aquest equip amb servidors públics de reserva" +SUMMARY_SPEECH_PUBLIC="servidors públics" +SUMMARY_SPEECH_PUBLIC_HARDWARE="servidors públics (local: Pi 5 de 8 GB o més)" +SUMMARY_SPEECH_PUBLIC_SETUP="servidors públics (local no disponible aquí)" +SUMMARY_SPEECH_UNUSED="aquest perfil no la fa servir" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/da/speech.sh b/tui/locales/da/speech.sh new file mode 100644 index 00000000..e1b4909b --- /dev/null +++ b/tui/locales/da/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS omsætter tale til tekst (genkendelse) og læser svar højt (syntese). + + - local: denne maskine med offentlige servere som reserve. Henter talemodeller (hundredvis af megabyte til nogle få gigabyte). Hvis lokal genkendelse fejler eller ikke giver tekst, sendes optagelsen til offentlige servere. Hvis lokal genkendelse eller syntese ikke kan sættes op, bruger den del offentlige servere; installationsprogrammet oplyser det. + - public: fællesskabets servere behandler dine optagelser og talte svar over internettet. Disse frivilligt drevne reserveservere er ikke en produktionstjeneste: de kan blive langsomme eller gå ned når som helst. + +Vælg, hvor tale skal behandles: +" +TITLE="Open Voice OS Installation - Tale" +LOCAL_DESCRIPTION="Denne maskine med offentlige servere som reserve" +PUBLIC_DESCRIPTION="Offentlige servere (jeg ved, de kan gå ned)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/da/summary.sh b/tui/locales/da/summary.sh index 901bb1a6..c3e3f904 100644 --- a/tui/locales/da/summary.sh +++ b/tui/locales/da/summary.sh @@ -9,6 +9,7 @@ Du er næsten færdig, her er en oversigt over de valg, du har truffet for at in - Ekstra færdigheder: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Tale: ${SPEECH_SUMMARY_STATE:-} - Tuning: ${TUNING_SUMMARY_STATE:-} De valg, der blev truffet under installationen af ​​Open Voice OS, er blevet nøje overvejet for at skræddersy vores system til dine unikke behov og præferencer. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="på denne maskine med offentlige servere som reserve" +SUMMARY_SPEECH_PUBLIC="offentlige servere" +SUMMARY_SPEECH_PUBLIC_HARDWARE="offentlige servere (lokal: Pi 5 med 8 GB eller mere)" +SUMMARY_SPEECH_PUBLIC_SETUP="offentlige servere (lokal er ikke tilgængelig her)" +SUMMARY_SPEECH_UNUSED="bruges ikke af denne profil" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/de-de/speech.sh b/tui/locales/de-de/speech.sh new file mode 100644 index 00000000..1dfd5fc0 --- /dev/null +++ b/tui/locales/de-de/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS wandelt Sprache in Text um (Erkennung) und liest Antworten vor (Synthese). + + - local: dieser Rechner mit öffentlichen Servern als Ersatz. Lädt Sprachmodelle herunter (hunderte Megabyte bis einige Gigabyte). Wenn die lokale Erkennung fehlschlägt oder keinen Text liefert, wird die Aufnahme an öffentliche Server gesendet. Lässt sich die lokale Erkennung oder Synthese nicht einrichten, nutzt dieser Teil öffentliche Server; der Installer weist darauf hin. + - public: Community-Server verarbeiten Ihre Aufnahmen und gesprochenen Antworten über das Internet. Diese ehrenamtlich betriebenen Ersatzserver sind kein Produktionsdienst: Sie können jederzeit langsamer werden oder ausfallen. + +Bitte wählen Sie, wo Sprache verarbeitet wird: +" +TITLE="Open Voice OS-Installation - Sprachverarbeitung" +LOCAL_DESCRIPTION="Dieser Rechner mit öffentlichen Servern als Ersatz" +PUBLIC_DESCRIPTION="Öffentliche Server (ich weiß, sie können ausfallen)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/de-de/summary.sh b/tui/locales/de-de/summary.sh index 715ad0c9..4c521941 100644 --- a/tui/locales/de-de/summary.sh +++ b/tui/locales/de-de/summary.sh @@ -9,6 +9,7 @@ Fast fertig. Eine kurze Zusammenfassung: - Extra-Skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Sprache: ${SPEECH_SUMMARY_STATE:-} - Tuning: ${TUNING_SUMMARY_STATE:-} Die Entscheidungen, die während des Installationsprozesses von Open Voice OS getroffen werden, wurden sorgfältig abgewogen, um unser System an Ihre individuellen Bedürfnisse und Vorlieben anzupassen. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="auf diesem Rechner mit öffentlichen Servern als Ersatz" +SUMMARY_SPEECH_PUBLIC="öffentliche Server" +SUMMARY_SPEECH_PUBLIC_HARDWARE="öffentliche Server (lokal: Pi 5 mit 8 GB oder besser)" +SUMMARY_SPEECH_PUBLIC_SETUP="öffentliche Server (lokal hier nicht verfügbar)" +SUMMARY_SPEECH_UNUSED="von diesem Profil nicht genutzt" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/en-us/speech.sh b/tui/locales/en-us/speech.sh new file mode 100644 index 00000000..9e01c18c --- /dev/null +++ b/tui/locales/en-us/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS turns speech into text (recognition) and reads replies aloud (synthesis). + + - local: use this machine with public fallback. Downloads speech models (hundreds of megabytes to a few gigabytes). If local recognition fails or returns no text, that recording is sent to public servers. If local recognition or synthesis cannot be set up, that part uses public servers; the installer reports it. + - public: community servers process your recordings and spoken replies over the internet. These volunteer-run backup servers are not a production service: they can slow down or go offline at any time. + +Please select where speech is processed: +" +TITLE="Open Voice OS Installation - Speech" +LOCAL_DESCRIPTION="This machine with public fallback" +PUBLIC_DESCRIPTION="Public servers (I accept they can go offline)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/en-us/summary.sh b/tui/locales/en-us/summary.sh index b3d173b3..4b9d56de 100644 --- a/tui/locales/en-us/summary.sh +++ b/tui/locales/en-us/summary.sh @@ -9,6 +9,7 @@ You are almost done, here is a summary of choices you made to install Open Voice - Extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - HA: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Speech: ${SPEECH_SUMMARY_STATE:-} - Tuning: ${TUNING_SUMMARY_STATE:-} The choices made during the Open Voice OS installation process have been carefully considered to tailor our system to your unique needs and preferences. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="on this machine with public fallback" +SUMMARY_SPEECH_PUBLIC="public servers" +SUMMARY_SPEECH_PUBLIC_HARDWARE="public servers (local needs a Pi 5 with 8 GB or better)" +SUMMARY_SPEECH_PUBLIC_SETUP="public servers (local is not available here)" +SUMMARY_SPEECH_UNUSED="not used by this profile" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/es-es/speech.sh b/tui/locales/es-es/speech.sh new file mode 100644 index 00000000..79f23de3 --- /dev/null +++ b/tui/locales/es-es/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS convierte la voz en texto (reconocimiento) y lee las respuestas en voz alta (síntesis). + + - local: este equipo con servidores públicos de respaldo. Descarga modelos de voz (de cientos de megabytes a unos pocos gigabytes). Si el reconocimiento local falla o no devuelve texto, esa grabación se envía a servidores públicos. Si no se puede configurar el reconocimiento o la síntesis local, esa parte usa servidores públicos; el instalador lo indica. + - public: los servidores de la comunidad procesan sus grabaciones y las respuestas de voz por internet. Estos servidores de respaldo, mantenidos por voluntarios, no son un servicio de producción: pueden ralentizarse o dejar de funcionar en cualquier momento. + +Seleccione dónde se procesa la voz: +" +TITLE="Instalación de Open Voice OS - Voz" +LOCAL_DESCRIPTION="Este equipo con servidores públicos de respaldo" +PUBLIC_DESCRIPTION="Servidores públicos (sé que pueden caerse)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/es-es/summary.sh b/tui/locales/es-es/summary.sh index 1d579545..91c6cc7f 100644 --- a/tui/locales/es-es/summary.sh +++ b/tui/locales/es-es/summary.sh @@ -9,6 +9,7 @@ CONTENT=" - Extras: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} +- Voz: ${SPEECH_SUMMARY_STATE:-} - Ajustes: ${TUNING_SUMMARY_STATE:-} Las decisiones que has tomado durante el proceso de instalación de Open Voice OS han sido cuidadosamente consideradas para adaptar el sistema a tus necesidades y preferencias. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="en este equipo con servidores públicos de respaldo" +SUMMARY_SPEECH_PUBLIC="servidores públicos" +SUMMARY_SPEECH_PUBLIC_HARDWARE="servidores públicos (local: Pi 5 con 8 GB o más)" +SUMMARY_SPEECH_PUBLIC_SETUP="servidores públicos (local no disponible aquí)" +SUMMARY_SPEECH_UNUSED="este perfil no la usa" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/eu-es/speech.sh b/tui/locales/eu-es/speech.sh new file mode 100644 index 00000000..cab46128 --- /dev/null +++ b/tui/locales/eu-es/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS-k ahotsa testu bihurtzen du (ezagutza) eta erantzunak ozen irakurtzen ditu (sintesia). + + - local: makina hau, zerbitzari publikoak ordezko gisa erabiliz. Ahots-ereduak deskargatzen ditu (ehunka megabytetik gigabyte gutxi batzuetara). Ezagutza lokalak huts egiten badu edo testurik itzultzen ez badu, grabazioa zerbitzari publikoetara bidaltzen da. Ezagutza edo sintesi lokala ezin bada konfiguratu, zati horrek zerbitzari publikoak erabiltzen ditu; instalatzaileak horren berri ematen du. + - public: komunitatearen zerbitzariek zure grabazioak eta ahots-erantzunak Internet bidez prozesatzen dituzte. Boluntarioek mantendutako ordezko zerbitzari hauek ez dira produkzio-zerbitzu bat: edozein unetan moteldu edo gelditu daitezke. + +Aukeratu non prozesatzen den ahotsa: +" +TITLE="Open Voice OS instalazioa - Ahotsa" +LOCAL_DESCRIPTION="Makina hau, zerbitzari publikoak ordezko gisa" +PUBLIC_DESCRIPTION="Zerbitzari publikoak (badakit eror daitezkeela)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/eu-es/summary.sh b/tui/locales/eu-es/summary.sh index 9e791544..158f1759 100644 --- a/tui/locales/eu-es/summary.sh +++ b/tui/locales/eu-es/summary.sh @@ -9,6 +9,7 @@ Ia amaitu duzu, hona hemen Open Voice OS instalatzeko egin dituzun aukeren labur - Trebetasun gehigarriak: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Ahotsa: ${SPEECH_SUMMARY_STATE:-} - Afinazioa: ${TUNING_SUMMARY_STATE:-} Open Voice OS instalazio-prozesuan egindako aukerak arretaz aztertu dira gure sistema zure behar eta lehentasun berezietara egokitzeko. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="makina honetan, zerbitzari publikoak ordezko gisa" +SUMMARY_SPEECH_PUBLIC="zerbitzari publikoak" +SUMMARY_SPEECH_PUBLIC_HARDWARE="zerbitzari publikoak (lokala: 8 GB-ko Pi 5 edo hobea)" +SUMMARY_SPEECH_PUBLIC_SETUP="zerbitzari publikoak (lokala ez dago hemen)" +SUMMARY_SPEECH_UNUSED="profil honek ez du erabiltzen" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/fr-fr/speech.sh b/tui/locales/fr-fr/speech.sh new file mode 100644 index 00000000..0ee2edf8 --- /dev/null +++ b/tui/locales/fr-fr/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS transforme la parole en texte (reconnaissance) et lit les réponses à voix haute (synthèse). + + - local : cette machine avec des serveurs publics en secours. Télécharge des modèles vocaux (de quelques centaines de mégaoctets à quelques gigaoctets). Si la reconnaissance locale échoue ou ne renvoie aucun texte, cet enregistrement est envoyé aux serveurs publics. Si la reconnaissance ou la synthèse locale ne peut pas être configurée, cette partie utilise les serveurs publics ; l'installateur le signale. + - public : les serveurs communautaires traitent vos enregistrements et les réponses vocales via Internet. Ces serveurs de secours, gérés par des bénévoles, ne sont pas un service de production : ils peuvent ralentir ou s'arrêter à tout moment. + +Veuillez sélectionner où la parole est traitée : +" +TITLE="Open Voice OS Installation - Traitement de la parole" +LOCAL_DESCRIPTION="Cette machine avec des serveurs publics en secours" +PUBLIC_DESCRIPTION="Serveurs publics (je sais qu'ils peuvent tomber)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/fr-fr/summary.sh b/tui/locales/fr-fr/summary.sh index 04845d61..d924e619 100644 --- a/tui/locales/fr-fr/summary.sh +++ b/tui/locales/fr-fr/summary.sh @@ -9,6 +9,7 @@ Vous y êtes presque ! Voici un résumé des choix que vous avez effectués pour - Compétences supplémentaires : ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant : ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM : ${LLM_SUMMARY_STATE:-} + - Traitement de la parole : ${SPEECH_SUMMARY_STATE:-} - Réglages Raspberry Pi : ${TUNING_SUMMARY_STATE:-} Les choix effectués lors du processus d'installation d'Open Voice OS ont été soigneusement étudiés pour adapter notre système à vos besoins et préférences. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="sur cette machine avec des serveurs publics en secours" +SUMMARY_SPEECH_PUBLIC="serveurs publics" +SUMMARY_SPEECH_PUBLIC_HARDWARE="serveurs publics (local : Pi 5 8 Go minimum)" +SUMMARY_SPEECH_PUBLIC_SETUP="serveurs publics (local indisponible ici)" +SUMMARY_SPEECH_UNUSED="non utilisé par ce profil" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/gl-es/speech.sh b/tui/locales/gl-es/speech.sh new file mode 100644 index 00000000..e32855b5 --- /dev/null +++ b/tui/locales/gl-es/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS converte a voz en texto (recoñecemento) e le as respostas en voz alta (síntese). + + - local: este equipo con servidores públicos de respaldo. Descarga modelos de voz (de centos de megabytes a uns poucos gigabytes). Se o recoñecemento local falla ou non devolve texto, esa gravación envíase a servidores públicos. Se non se pode configurar o recoñecemento ou a síntese local, esa parte usa servidores públicos; o instalador indícao. + - public: os servidores da comunidade procesan as túas gravacións e as respostas de voz por internet. Estes servidores de respaldo, mantidos por voluntarios, non son un servizo de produción: poden ir máis lentos ou deixar de funcionar en calquera momento. + +Selecciona onde se procesa a voz: +" +TITLE="Instalación de Open Voice OS - Voz" +LOCAL_DESCRIPTION="Este equipo con servidores públicos de respaldo" +PUBLIC_DESCRIPTION="Servidores públicos (sei que poden caer)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/gl-es/summary.sh b/tui/locales/gl-es/summary.sh index d742d6cc..06a6e85f 100644 --- a/tui/locales/gl-es/summary.sh +++ b/tui/locales/gl-es/summary.sh @@ -9,6 +9,7 @@ Estás a piques de rematar. Aquí tes un resumo das opcións que escolliches par - Habilidades adicionais: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Voz: ${SPEECH_SUMMARY_STATE:-} - Axustes: ${TUNING_SUMMARY_STATE:-} As decisións tomadas durante a instalación de Open Voice OS foron coidadosamente pensadas para adaptar o noso sistema ás túas necesidades e preferencias. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="neste equipo con servidores públicos de respaldo" +SUMMARY_SPEECH_PUBLIC="servidores públicos" +SUMMARY_SPEECH_PUBLIC_HARDWARE="servidores públicos (local: Pi 5 de 8 GB ou mellor)" +SUMMARY_SPEECH_PUBLIC_SETUP="servidores públicos (local non dispoñible aquí)" +SUMMARY_SPEECH_UNUSED="este perfil non a usa" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/hi-in/speech.sh b/tui/locales/hi-in/speech.sh new file mode 100644 index 00000000..3c0eb459 --- /dev/null +++ b/tui/locales/hi-in/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS आवाज़ को टेक्स्ट में बदलता है (वाक् पहचान) और जवाब बोलकर सुनाता है (वाक् संश्लेषण)। + + - local: यह मशीन, ज़रूरत पड़ने पर सार्वजनिक सर्वरों की मदद से। वाक् मॉडल डाउनलोड होते हैं (कुछ सौ मेगाबाइट से कुछ गीगाबाइट तक)। अगर स्थानीय पहचान विफल हो या कोई टेक्स्ट न लौटाए, तो वह रिकॉर्डिंग सार्वजनिक सर्वरों पर भेजी जाती है। अगर स्थानीय पहचान या संश्लेषण स्थापित नहीं हो पाता, तो उस हिस्से के लिए सार्वजनिक सर्वर इस्तेमाल होते हैं; इंस्टॉलर इसकी जानकारी देता है। + - public: समुदाय के सर्वर आपकी रिकॉर्डिंग और बोले जाने वाले जवाब इंटरनेट पर प्रोसेस करते हैं। स्वयंसेवकों द्वारा चलाए जा रहे ये वैकल्पिक सर्वर नियमित सेवा नहीं हैं: ये धीमे पड़ सकते हैं या कभी भी बंद हो सकते हैं। + +कृपया चुनें कि आवाज़ कहाँ प्रोसेस हो: +" +TITLE="Open Voice OS Installation - आवाज़" +LOCAL_DESCRIPTION="यह मशीन, ज़रूरत पर सार्वजनिक सर्वरों की मदद से" +PUBLIC_DESCRIPTION="सार्वजनिक सर्वर (ये कभी भी बंद हो सकते हैं)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/hi-in/summary.sh b/tui/locales/hi-in/summary.sh index a9fbeadc..328911aa 100644 --- a/tui/locales/hi-in/summary.sh +++ b/tui/locales/hi-in/summary.sh @@ -8,6 +8,7 @@ CONTENT=" - अतिरिक्त कौशल: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - आवाज़: ${SPEECH_SUMMARY_STATE:-} - ट्यूनिंग: ${TUNING_SUMMARY_STATE:-} ओपन वॉयस ओएस (Open Voice OS) इंस्टॉलेशन प्रक्रिया के दौरान चुने गए विकल्पों पर हमारे सिस्टम (System) को आपकी विशिष्ट आवश्यकताओं और प्राथमिकताओं के अनुरूप बनाने के लिए सावधानीपूर्वक विचार किया गया है। @@ -20,5 +21,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="इस मशीन पर, ज़रूरत पर सार्वजनिक सर्वरों की मदद से" +SUMMARY_SPEECH_PUBLIC="सार्वजनिक सर्वर" +SUMMARY_SPEECH_PUBLIC_HARDWARE="सार्वजनिक सर्वर (लोकल: 8 GB वाला Pi 5 या बेहतर)" +SUMMARY_SPEECH_PUBLIC_SETUP="सार्वजनिक सर्वर (यहाँ लोकल उपलब्ध नहीं)" +SUMMARY_SPEECH_UNUSED="यह प्रोफ़ाइल इसका उपयोग नहीं करती" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/it-it/speech.sh b/tui/locales/it-it/speech.sh new file mode 100644 index 00000000..b9254a50 --- /dev/null +++ b/tui/locales/it-it/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS trasforma la voce in testo (riconoscimento) e legge le risposte ad alta voce (sintesi). + + - local: questa macchina con server pubblici di riserva. Scarica modelli vocali (da centinaia di megabyte a pochi gigabyte). Se il riconoscimento locale fallisce o non restituisce testo, quella registrazione viene inviata ai server pubblici. Se non è possibile configurare il riconoscimento o la sintesi locale, quella parte usa i server pubblici; l'installer lo segnala. + - public: i server della comunità elaborano le tue registrazioni e le risposte vocali tramite internet. Questi server di riserva, gestiti da volontari, non sono un servizio di produzione: possono rallentare o andare offline in qualsiasi momento. + +Seleziona dove elaborare la voce: +" +TITLE="Installazione di Open Voice OS - Voce" +LOCAL_DESCRIPTION="Questa macchina con server pubblici di riserva" +PUBLIC_DESCRIPTION="Server pubblici (so che possono andare offline)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/it-it/summary.sh b/tui/locales/it-it/summary.sh index 14cd999a..63d752e9 100644 --- a/tui/locales/it-it/summary.sh +++ b/tui/locales/it-it/summary.sh @@ -9,6 +9,7 @@ Abbiamo quasi finito. Qui c'è un riassunto delle opzioni scelte per installare - Competenze extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Voce: ${SPEECH_SUMMARY_STATE:-} - Ottimizzazione: ${TUNING_SUMMARY_STATE:-} Le decisioni prese durante il processo di installazione di Open Voice OS sono state attentamente valutate per personalizzare il nostro sistema in base alle tue esigenze e preferenze individuali. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="su questa macchina con server pubblici di riserva" +SUMMARY_SPEECH_PUBLIC="server pubblici" +SUMMARY_SPEECH_PUBLIC_HARDWARE="server pubblici (locale: Pi 5 con 8 GB o superiore)" +SUMMARY_SPEECH_PUBLIC_SETUP="server pubblici (locale non disponibile qui)" +SUMMARY_SPEECH_UNUSED="non usata da questo profilo" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/kab-dz/speech.sh b/tui/locales/kab-dz/speech.sh new file mode 100644 index 00000000..bbed9f9d --- /dev/null +++ b/tui/locales/kab-dz/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS yettbeddil taɣect ɣer uḍris (aɛqal) yerna yeqqar-d tiririyin s taɣect (asuddes). + + - local: tamacint-a s yiqeddacen izayazen d tallalt. Ad d-yessader timudmin n taɣect (seg yimiḍ n MB alamma d kra n GB). Ma yecceḍ uɛqal adigan neɣ ur d-yerri ara aḍris, asekles-nni ad yettwazen ɣer yiqeddacen izayazen. Ma ur yezmir ara ad yesbedd aɛqal neɣ asuddes adigan, aḥric-nni ad yesseqdec iqeddacen izayazen; amesbeddi ad d-yefk talɣut ɣef waya. + - public: iqeddacen n temɣiwant senfaren iseklasen-ik d tririyin n taɣect s Internet. Iqeddacen-a n tallalt ttwasefraken s lebɣi, mačči d ameẓlu n tfalit: zemren ad ẓẓayen neɣ ad ḥbesen melmi tebɣu tili. + +Ttxil-k fren anida ara tettwasenfar taɣect: +" +TITLE="Asbeddi n Open Voice OS - Taɣect" +LOCAL_DESCRIPTION="Tamacint-a s yiqeddacen izayazen d tallalt" +PUBLIC_DESCRIPTION="Iqeddacen izayazen (ẓriɣ zemren ad ḥbesen)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/kab-dz/summary.sh b/tui/locales/kab-dz/summary.sh index 61cc493f..9d98fedc 100644 --- a/tui/locales/kab-dz/summary.sh +++ b/tui/locales/kab-dz/summary.sh @@ -9,6 +9,7 @@ Qrib ad tekfuḍ, ha-t-an ugzul n textiṛiyin i tgiḍ akken ad tesbeddeḍ ana - Tussniwin timernanin: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Taɣect: ${SPEECH_SUMMARY_STATE:-} - Tamellit: ${TUNING_SUMMARY_STATE:-} Ifranen i d-yettwaxedmen deg ukala n usbeddi n Open Voice OS ttwafernen s ttawil akked ufeṣṣel i wakken ad yeddu unagraw-nneɣ akken i teḥwaǧeḍ. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="d arurmid" SUMMARY_STATE_UNSUPPORTED_PROFILE="yettawfran (ur yettwasefrek ara deg umaɣnu-a)" SUMMARY_STATE_MISSING_URL="yettwafran (txuṣṣ URL; as yettwazgal)" SUMMARY_STATE_MISSING_CONFIGURATION="yettwafran (txuṣṣ tawila; as yettwazgal)" +SUMMARY_SPEECH_LOCAL="ɣef tmacint-a s yiqeddacen izayazen d tallalt" +SUMMARY_SPEECH_PUBLIC="iqeddacen izayazen" +SUMMARY_SPEECH_PUBLIC_HARDWARE="iqeddacen izayazen (adigan: Pi 5 s 8 GB neɣ ugar)" +SUMMARY_SPEECH_PUBLIC_SETUP="iqeddacen izayazen (adigan ulac-it dagi)" +SUMMARY_SPEECH_UNUSED="ur yettwaseqdac ara deg umaɣnu-a" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/nl-nl/speech.sh b/tui/locales/nl-nl/speech.sh new file mode 100644 index 00000000..7f54b86b --- /dev/null +++ b/tui/locales/nl-nl/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS zet spraak om in tekst (herkenning) en leest antwoorden voor (synthese). + + - local: deze machine met openbare servers als reserve. Downloadt spraakmodellen (honderden megabytes tot enkele gigabytes). Als lokale herkenning mislukt of geen tekst oplevert, gaat die opname naar openbare servers. Als lokale herkenning of synthese niet kan worden ingesteld, gebruikt dat onderdeel openbare servers; het installatieprogramma meldt dit. + - public: servers van de community verwerken je opnames en gesproken antwoorden via internet. Deze reserveservers draaien dankzij vrijwilligers en zijn geen productiedienst: ze kunnen op elk moment traag worden of uitvallen. + +Selecteer waar spraak wordt verwerkt: +" +TITLE="Open Voice OS Installatie - Spraak" +LOCAL_DESCRIPTION="Deze machine met openbare servers als reserve" +PUBLIC_DESCRIPTION="Openbare servers (ik weet dat ze kunnen uitvallen)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/nl-nl/summary.sh b/tui/locales/nl-nl/summary.sh index fe962e12..4bc3456b 100644 --- a/tui/locales/nl-nl/summary.sh +++ b/tui/locales/nl-nl/summary.sh @@ -9,6 +9,7 @@ Bijna klaar. Een korte samenvatting: - Extra skills: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Spraak: ${SPEECH_SUMMARY_STATE:-} - Tuning: ${TUNING_SUMMARY_STATE:-} De beslissingen die zijn genomen tijdens het installatieproces van OpenVoice OS zijn zorgvuldig gemaakt om ons systeem aan te passen aan je individuele behoeften en voorkeuren. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="op deze machine met openbare servers als reserve" +SUMMARY_SPEECH_PUBLIC="openbare servers" +SUMMARY_SPEECH_PUBLIC_HARDWARE="openbare servers (lokaal: Pi 5 met 8 GB of beter)" +SUMMARY_SPEECH_PUBLIC_SETUP="openbare servers (lokaal hier niet beschikbaar)" +SUMMARY_SPEECH_UNUSED="niet gebruikt door dit profiel" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/pl-pl/speech.sh b/tui/locales/pl-pl/speech.sh new file mode 100644 index 00000000..c8f6b033 --- /dev/null +++ b/tui/locales/pl-pl/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +Open Voice OS zamienia mowę na tekst (rozpoznawanie) i czyta odpowiedzi na głos (synteza). + + - local: ten komputer z serwerami publicznymi w rezerwie. Pobiera modele mowy (od setek megabajtów do kilku gigabajtów). Jeśli lokalne rozpoznawanie zawiedzie lub nie zwróci tekstu, nagranie jest wysyłane do serwerów publicznych. Jeśli nie da się skonfigurować lokalnego rozpoznawania lub syntezy, ta część używa serwerów publicznych; instalator o tym informuje. + - public: serwery społeczności przetwarzają Twoje nagrania i odpowiedzi głosowe przez internet. Te zapasowe serwery prowadzone przez wolontariuszy nie są usługą produkcyjną: mogą zwolnić lub przestać działać w każdej chwili. + +Wybierz, gdzie ma być przetwarzana mowa: +" +TITLE="Instalacja Open Voice OS - Mowa" +LOCAL_DESCRIPTION="Ten komputer z serwerami publicznymi w rezerwie" +PUBLIC_DESCRIPTION="Serwery publiczne (wiem, że mogą przestać działać)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/pl-pl/summary.sh b/tui/locales/pl-pl/summary.sh index 2a0987f8..30d2b001 100644 --- a/tui/locales/pl-pl/summary.sh +++ b/tui/locales/pl-pl/summary.sh @@ -9,6 +9,7 @@ Już prawie skończyłeś, oto podsumowanie wyborów dokonanych podczas instalac - Dodatkowe umiejętności: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} +- Mowa: ${SPEECH_SUMMARY_STATE:-} - Strojenie: ${TUNING_SUMMARY_STATE:-} Wybory dokonane podczas instalacji Open Voice OS zostały starannie rozważone, aby dostosować nasz system do Twoich unikalnych potrzeb i preferencji. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="na tym komputerze z serwerami publicznymi w rezerwie" +SUMMARY_SPEECH_PUBLIC="serwery publiczne" +SUMMARY_SPEECH_PUBLIC_HARDWARE="serwery publiczne (lokalnie: Pi 5 z 8 GB lub lepszy)" +SUMMARY_SPEECH_PUBLIC_SETUP="serwery publiczne (tryb lokalny tu niedostępny)" +SUMMARY_SPEECH_UNUSED="nieużywana w tym profilu" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/locales/pt-pt/speech.sh b/tui/locales/pt-pt/speech.sh new file mode 100644 index 00000000..523ee691 --- /dev/null +++ b/tui/locales/pt-pt/speech.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +CONTENT=" +O Open Voice OS converte a voz em texto (reconhecimento) e lê as respostas em voz alta (síntese). + + - local: esta máquina com servidores públicos de reserva. Descarrega modelos de voz (de centenas de megabytes a alguns gigabytes). Se o reconhecimento local falhar ou não devolver texto, essa gravação é enviada para servidores públicos. Se não for possível configurar o reconhecimento ou a síntese local, essa parte usa servidores públicos; o instalador informa-o. + - public: os servidores da comunidade processam as suas gravações e as respostas de voz pela internet. Estes servidores de reserva, mantidos por voluntários, não são um serviço de produção: podem ficar lentos ou indisponíveis a qualquer momento. + +Seleccione onde a voz é processada: +" +TITLE="Open Voice OS Instalação - Voz" +LOCAL_DESCRIPTION="Esta máquina com servidores públicos de reserva" +PUBLIC_DESCRIPTION="Servidores públicos (sei que podem falhar)" + +export CONTENT TITLE LOCAL_DESCRIPTION PUBLIC_DESCRIPTION diff --git a/tui/locales/pt-pt/summary.sh b/tui/locales/pt-pt/summary.sh index dc5bc9f7..dfec05bd 100644 --- a/tui/locales/pt-pt/summary.sh +++ b/tui/locales/pt-pt/summary.sh @@ -9,6 +9,7 @@ Quase a terminar. Um breve resumo: - Skills extra: ${FEATURE_EXTRA_SKILLS_SUMMARY_STATE:-} - Home Assistant: ${HOMEASSISTANT_SUMMARY_STATE:-} - LLM: ${LLM_SUMMARY_STATE:-} + - Voz: ${SPEECH_SUMMARY_STATE:-} - Otimização: ${TUNING_SUMMARY_STATE:-} As decisões tomadas durante o processo de instalação do Open Voice OS foram cuidadosamente consideradas para personalizar o nosso sistema de acordo com as suas necessidades e preferências individuais. @@ -22,5 +23,10 @@ SUMMARY_STATE_DISABLED="disabled" SUMMARY_STATE_UNSUPPORTED_PROFILE="selected (not supported for this profile)" SUMMARY_STATE_MISSING_URL="selected (missing URL; will be skipped)" SUMMARY_STATE_MISSING_CONFIGURATION="selected (missing configuration; will be skipped)" +SUMMARY_SPEECH_LOCAL="nesta máquina com servidores públicos de reserva" +SUMMARY_SPEECH_PUBLIC="servidores públicos" +SUMMARY_SPEECH_PUBLIC_HARDWARE="servidores públicos (local: Pi 5 com 8 GB ou melhor)" +SUMMARY_SPEECH_PUBLIC_SETUP="servidores públicos (local indisponível aqui)" +SUMMARY_SPEECH_UNUSED="não utilizado por este perfil" -export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION +export CONTENT TITLE SUMMARY_STATE_ENABLED SUMMARY_STATE_DISABLED SUMMARY_STATE_UNSUPPORTED_PROFILE SUMMARY_STATE_MISSING_URL SUMMARY_STATE_MISSING_CONFIGURATION SUMMARY_SPEECH_LOCAL SUMMARY_SPEECH_PUBLIC SUMMARY_SPEECH_PUBLIC_HARDWARE SUMMARY_SPEECH_PUBLIC_SETUP SUMMARY_SPEECH_UNUSED diff --git a/tui/main.sh b/tui/main.sh index cde9abeb..b93e830f 100644 --- a/tui/main.sh +++ b/tui/main.sh @@ -81,6 +81,10 @@ while [ "$tui_step_index" -lt "${#TUI_FLOW[@]}" ]; do # shellcheck source=tui/satellite/main.sh source tui/satellite/main.sh ;; + speech) + # shellcheck source=tui/speech.sh + source tui/speech.sh + ;; tuning) # shellcheck source=tui/tuning.sh source tui/tuning.sh diff --git a/tui/navigation.sh b/tui/navigation.sh index 778ddac3..b86e5b77 100644 --- a/tui/navigation.sh +++ b/tui/navigation.sh @@ -16,6 +16,8 @@ # shellcheck source=tui/dialogs.sh source tui/dialogs.sh +# shellcheck source=utils/speech.sh +source utils/speech.sh TUI_NAV="${TUI_NAV:-next}" export TUI_NAV @@ -34,6 +36,7 @@ declare -a TUI_FLOW=( profiles features satellite + speech tuning summary telemetry @@ -153,6 +156,9 @@ function tui_flow_step_enabled() { satellite) [[ "${PROFILE:-}" == "satellite" ]] ;; + speech) + local_speech_available + ;; tuning) [[ "${RASPBERRYPI_MODEL:-N/A}" != "N/A" ]] ;; diff --git a/tui/speech.sh b/tui/speech.sh new file mode 100644 index 00000000..44a76fed --- /dev/null +++ b/tui/speech.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash +# shellcheck source=tui/navigation.sh +source tui/navigation.sh +# shellcheck source=tui/locales/en-us/speech.sh +source "tui/locales/$LOCALE/speech.sh" + +# The flow only shows this screen when local_speech_available, so both choices +# can actually be installed. Local is what the hardware was checked for, so a new +# install starts there. An existing install that has never answered predates this +# question and runs on the public servers: an update keeps it there unless the +# user picks local, rather than switching the device over, and downloading +# gigabytes of models, for someone pressing Enter through the screens. +active_engine="local" +if [ "${EXISTING_INSTANCE:-false}" == "true" ]; then + active_engine="public" +fi +if [ -f "$INSTALLER_STATE_FILE" ]; then + saved_engine="$(jq -r '.speech_engine // ""' "$INSTALLER_STATE_FILE" 2>>"$LOG_FILE")" + case "$saved_engine" in + local|public) + active_engine="$saved_engine" + ;; + esac +fi + +whiptail_args=( + --title "$TITLE" + --radiolist "$CONTENT" + --cancel-button "$BACK_BUTTON" + --ok-button "$OK_BUTTON" + "$TUI_WINDOW_HEIGHT" "$TUI_WINDOW_WIDTH" 4 +) + +for engine in local public; do + if [ "$engine" == "local" ]; then + whiptail_args+=("$engine" "$LOCAL_DESCRIPTION") + else + whiptail_args+=("$engine" "$PUBLIC_DESCRIPTION") + fi + if [ "$engine" == "$active_engine" ]; then + whiptail_args+=("ON") + else + whiptail_args+=("OFF") + fi +done + +if ! tui_nav_capture SPEECH_ENGINE "${whiptail_args[@]}"; then + SPEECH_ENGINE="$active_engine" + export SPEECH_ENGINE + return 0 +fi +export SPEECH_ENGINE + +# Persist selection (used for defaults when navigating back or re-running). +state_tmp="$(mktemp)" +if [ -f "$INSTALLER_STATE_FILE" ] && \ + jq --arg speech_engine "$SPEECH_ENGINE" \ + 'if type=="object" then . else {} end | .speech_engine = $speech_engine' \ + "$INSTALLER_STATE_FILE" >"$state_tmp" 2>>"$LOG_FILE"; then + mv -f "$state_tmp" "$INSTALLER_STATE_FILE" +else + jq -n --arg speech_engine "$SPEECH_ENGINE" '{speech_engine: $speech_engine}' >"$state_tmp" 2>>"$LOG_FILE" && \ + mv -f "$state_tmp" "$INSTALLER_STATE_FILE" +fi + +# Keep state writable by the target user when running under sudo/root. +if [ -n "${RUN_AS:-}" ] && [ -f "$INSTALLER_STATE_FILE" ]; then + chown "$RUN_AS":"$(id -ng "$RUN_AS" 2>>"$LOG_FILE")" "$INSTALLER_STATE_FILE" &>>"$LOG_FILE" || true +fi diff --git a/tui/summary.sh b/tui/summary.sh index 5489f9bd..dacb3a26 100644 --- a/tui/summary.sh +++ b/tui/summary.sh @@ -43,12 +43,31 @@ function summary_feature_state() { printf '%s\n' "$incomplete_state" } +# What the playbook will do with speech, which is not always what was picked: +# normalize_speech_engine keeps a run that cannot use local speech on public. +function summary_speech_state() { + if [ "${PROFILE:-}" == "server" ]; then + printf '%s\n' "${SUMMARY_SPEECH_UNUSED:-not used by this profile}" + elif local_speech_available; then + if [ "${SPEECH_ENGINE:-}" == "local" ]; then + printf '%s\n' "${SUMMARY_SPEECH_LOCAL:-on this machine with public fallback}" + else + printf '%s\n' "${SUMMARY_SPEECH_PUBLIC:-public servers}" + fi + elif [ "${LOCAL_SPEECH_CAPABLE:-false}" != "true" ]; then + printf '%s\n' "${SUMMARY_SPEECH_PUBLIC_HARDWARE:-public servers}" + else + printf '%s\n' "${SUMMARY_SPEECH_PUBLIC_SETUP:-public servers}" + fi +} + # Recomputed on every pass: the user can go back, change a choice, and come # straight back to this screen. function summary_refresh_states() { FEATURE_SKILLS_SUMMARY_STATE="$(summary_toggle_state "${FEATURE_SKILLS:-false}")" FEATURE_EXTRA_SKILLS_SUMMARY_STATE="$(summary_toggle_state "${FEATURE_EXTRA_SKILLS:-false}")" TUNING_SUMMARY_STATE="$(summary_toggle_state "${TUNING:-no}")" + SPEECH_SUMMARY_STATE="$(summary_speech_state)" local homeassistant_configured="false" if [ -n "${HOMEASSISTANT_URL:-}" ]; then @@ -78,6 +97,7 @@ function summary_refresh_states() { export FEATURE_SKILLS_SUMMARY_STATE="" export FEATURE_EXTRA_SKILLS_SUMMARY_STATE="" export TUNING_SUMMARY_STATE="" +export SPEECH_SUMMARY_STATE="" export HOMEASSISTANT_SUMMARY_STATE="" export LLM_SUMMARY_STATE="" diff --git a/utils/constants.sh b/utils/constants.sh index 4de3b592..6c79213d 100644 --- a/utils/constants.sh +++ b/utils/constants.sh @@ -20,6 +20,7 @@ export EXIT_PERMISSION_DENIED=2 export EXIT_SUCCESS=0 export I2C_BUS="1" export INSTALLER_VENV_NAME="ovos-installer" +export LOCAL_SPEECH_MIN_MEMORY_MB=7680 export LOG_FILE=/var/log/ovos-installer.log export NEWT_COLORS=" root=white,black @@ -54,6 +55,8 @@ else fi export PASTE_URL="https://paste.uoi.io" export PULSE_SOCKET_WSL2=/mnt/wslg/PulseServer +# Raspberry Pi 5, Pi 500 and Compute Module 5, as the device tree names them. +export RASPBERRY_PI_5_FAMILY_REGEX='(^|[[:space:]])Raspberry[[:space:]]Pi[[:space:]](5|500|Compute[[:space:]]Module[[:space:]]5)([^0-9]|$)' export REBOOT_FILE_PATH=/tmp/ovos.reboot declare -ra SCENARIO_ALLOWED_FEATURES=(skills extra_skills gui homeassistant llm) export SCENARIO_ALLOWED_FEATURES @@ -61,7 +64,7 @@ declare -ra SCENARIO_ALLOWED_HIVEMIND_OPTIONS=(host port key password) export SCENARIO_ALLOWED_HIVEMIND_OPTIONS declare -ra SCENARIO_ALLOWED_LLM_OPTIONS=(api_url key model persona max_tokens temperature top_p) export SCENARIO_ALLOWED_LLM_OPTIONS -declare -ra SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm) +declare -ra SCENARIO_ALLOWED_OPTIONS=(features channel hardware share_telemetry share_usage_telemetry profile method uninstall raspberry_pi_tuning hivemind llm speech_engine) export SCENARIO_ALLOWED_OPTIONS export SCENARIO_NAME="scenario.yaml" export SCENARIO_PATH="" diff --git a/utils/scenario.sh b/utils/scenario.sh index aa6168f7..f085a294 100644 --- a/utils/scenario.sh +++ b/utils/scenario.sh @@ -164,6 +164,17 @@ if [ -f "$SCENARIO_PATH" ]; then fi export TUNING ;; + speech_engine) + case "${options[$option]}" in + local | public) + export SPEECH_ENGINE="${options[$option]}" + ;; + *) + scenario_reject "speech_engine" "${options[$option]}" "local, public" + break + ;; + esac + ;; features) for feature in "${!features[@]}"; do # Ensure the feature is supported by the installer diff --git a/utils/speech.sh b/utils/speech.sh new file mode 100644 index 00000000..e8f882d5 --- /dev/null +++ b/utils/speech.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Where speech recognition (STT) and speech synthesis (TTS) run. +# +# public the Open Voice OS defaults: both go to the community servers. +# local the onnx-asr and phoonnx plugins ovos-config recommends for the +# locale run on this host, with the public STT server as fallback. +# +# Local speech only gives a usable assistant on a Raspberry Pi 5 with 8 GB or a +# machine at least as capable, so everything below that is kept on public. + +# Total memory in MiB, or 0 when it cannot be read. +function total_memory_mb() { + local memory_bytes="" + + case "$(uname -s 2>/dev/null || true)" in + Darwin) + memory_bytes="$(sysctl -n hw.memsize 2>/dev/null || true)" + if [[ "$memory_bytes" =~ ^[0-9]+$ ]]; then + printf '%s\n' "$((memory_bytes / 1048576))" + return 0 + fi + ;; + *) + if [ -r /proc/meminfo ]; then + awk '/^MemTotal:/ { printf "%d\n", $2 / 1024; found = 1 } END { if (!found) print 0 }' /proc/meminfo + return 0 + fi + ;; + esac + + printf '%s\n' "0" +} + +# Whether this host can run local speech at all, whatever the user picks later. +# +# No Raspberry Pi other than the Pi 5 family qualifies, whatever its memory: a +# Pi 4 with 8 GB passes the memory test and still cannot keep up. Any other +# machine needs the SIMD instructions onnxruntime is built for and a 64-bit +# userland, since onnxruntime publishes no 32-bit wheels and none for Intel +# Macs. An 8 GB board reports less than 8192 MiB once firmware and kernel have +# taken their share, which is why the floor sits at 7.5 GiB. +function local_speech_hardware_supported() { + [ "${CPU_IS_CAPABLE:-false}" == "true" ] || return 1 + [ "$(getconf LONG_BIT 2>/dev/null || echo 0)" == "64" ] || return 1 + + case "$(uname -s 2>/dev/null || true)-$(uname -m 2>/dev/null || true)" in + Linux-x86_64 | Linux-aarch64 | Darwin-arm64) ;; + *) return 1 ;; + esac + + if [ "${RASPBERRYPI_MODEL:-N/A}" != "N/A" ] && + ! [[ "$RASPBERRYPI_MODEL" =~ $RASPBERRY_PI_5_FAMILY_REGEX ]]; then + return 1 + fi + + [ "${TOTAL_MEMORY_MB:-0}" -ge "$LOCAL_SPEECH_MIN_MEMORY_MB" ] +} + +# Requires detect_cpu_instructions and is_raspberrypi_soc to have run. +function detect_local_speech_support() { + printf '%s' "➤ Checking local speech support... " + TOTAL_MEMORY_MB="$(total_memory_mb)" + export TOTAL_MEMORY_MB + + if local_speech_hardware_supported; then + export LOCAL_SPEECH_CAPABLE="true" + else + export LOCAL_SPEECH_CAPABLE="false" + fi + echo -e "[${done_format:-done}]" +} + +# Capable hardware is not enough. Local speech is only offered on the alpha +# channel, and a server has no audio to process. A containers satellite runs the +# hivemind-docker images, which do not carry the speech plugins. +function local_speech_available() { + [ "${LOCAL_SPEECH_CAPABLE:-false}" == "true" ] && [ "${CHANNEL:-}" == "alpha" ] || return 1 + + case "${METHOD:-}" in + virtualenv) + [ "${PROFILE:-}" != "server" ] + ;; + containers) + [ "${PROFILE:-}" == "ovos" ] || [ "${PROFILE:-}" == "listener" ] + ;; + *) + return 1 + ;; + esac +} + +# Settle SPEECH_ENGINE before the playbook runs. A scenario file, an earlier +# run's state or a choice made before going back and changing the channel can +# all ask for local where it is not available; those runs get public. +function normalize_speech_engine() { + if [ "${SPEECH_ENGINE:-}" == "local" ] && ! local_speech_available; then + echo "Local speech needs the alpha channel, a profile with audio (ovos or listener with containers) and a Raspberry Pi 5 with 8 GB or an equivalent machine. Using the public servers instead." | tee -a "$LOG_FILE" + fi + + if [ "${SPEECH_ENGINE:-}" == "local" ] && local_speech_available; then + export SPEECH_ENGINE="local" + else + export SPEECH_ENGINE="public" + fi +}