Files
EscapistsReintroduced/docs/welcome-voice-test.md
T

22 KiB
Raw Blame History

Welcome briefing

The bundled welcome plays once per authenticated account in the running server game session, after a controlled character exists, loading releases audio and the player closes the menus. The server records permission before the client downloads the greeting, so a crash, reconnect or return through the main menu does not replay it. The ledger also survives world cleanup in the same game instance. Restarting the server game instance clears it. A new account can still hear its own greeting even if it receives a reused connection ID.

Death, respawn and terrain redirects additionally retain the client-side consumed flag. Empty/unavailable audio does not claim a greeting. Explicit admin Test & enable previews remain repeatable and do not consume normal greetings. There is one download attempt; failures never block playing or cause a retry loop. Requests are skipped while the engine HTTP service is disabled (including native -noBackend diagnostics). An empty URL disables downloading instead of requesting an empty address.

The default welcome is a bundled generic recording and contains no terrain or weather claims. The addon includes 37 prerecorded messages: the welcome, ten extraction-radio lines, and 26 soldier lines. They need no speech service, credentials, HTTP downloads, or PCM reconstruction during play. Recordings use the existing Algenib voice, with their scripts and hashes in bundled-speech.json and bundled-speech-assets.json. Their combined PCM source size is about 6.9 MB. Event-specific wording still requires the appropriate gameplay event; the bank does not manufacture contact, rescue, or weather facts.

An administrator can explicitly opt into a live welcome using TTS → Test & enable. Disable live TTS restores the bundled welcome source. TTS → Prerecorded speech → On / Off controls all bundled welcome, soldier, and extraction recordings for everyone. It defaults to On, saves immediately as voice.recordingsEnabled, survives restarts, and stops bundled audio already playing when switched Off. Text announcements remain visible. The optional live welcome has its own setting. The voice.bundledSpeech preference defaults to true, including for existing configurations without that field. Live bridge credentials alone do not disable the bundled default.

The optional dynamic briefing names the loaded terrain, not a saved next-round choice, and says the escape was generated for it. It reports the game's current weather and its next known weather state when different. It gives no invented time or forecast for an unknown custom weather state. The server checks for changes once a minute, so late joiners receive a recent briefing; connected players do not hear updates again. It is ordinary local audio, not positional soldier speech or an enemy radio transmission. A matching hint labels it as synthetic speech.

Example: “Listen up. Welcome to Escape Reintroduced. This is Nogova. Your escape was generated for this terrain. Current weather: clear skies. Expected next: rain. Timing is uncertain. Stay together, keep your heads down. Good luck.”

Speech service

Use a Python virtual environment and install tools/welcome_voice_requirements.txt. The helper uses Google Application Default Credentials on the speech host. Google credentials never enter the mod, the game server configuration or clients. Enable Cloud Text-to-Speech and billing in the Google project, then configure ADC (for development: gcloud auth application-default login and gcloud auth application-default set-quota-project PROJECT_ID).

The default voice is en-US-Chirp3-HD-Algenib, a gravelly male voice chosen for the veteran-style briefing. Google synthesis with this voice was tested. Age is an artistic choice, not a guaranteed voice attribute. Optional --model gemini-2.5-flash-tts --voice Algenib adds explicit direction for a calm, weathered, sixty-five-year-old veteran. That model returned HTTP 403 with the current credentials and is not the default. Google documents additional aiplatform.endpoints.predict permission for Gemini-TTS.

Create a private random bridge token file, at least 32 characters long, readable only by the service account. Put the same token in the game server's voice configuration. This is a separate shared secret, not a Google token.

python tools/welcome_voice.py \
  --project YOUR_PROJECT_ID --output /var/lib/escape-voice/welcome.wav \
  --serve --bind 127.0.0.1 --port 18947 \
  --briefing-cache /var/lib/escape-voice/briefings \
  --bridge-token-file /etc/escape-voice/bridge-token

Expose the helper through an HTTPS reverse proxy reachable by the game server and players. The default loopback bind is for a local proxy. --bind and --port can be changed for other hosting. This is independent of AMP.

The game server sends an authenticated JSON POST to the configured bridge base URL. Only the public cached audio URL and transcript replicate to players. The helper accepts both Content-Length and HTTP/1.1 chunked request bodies, with an 8 KiB decoded-body limit and bounded chunk framing. Older helpers required Content-Length and rejected chunked uploads with HTTP 400 before authentication or synthesis. Update tools/welcome_voice.py on the speech host and restart that helper to pick up the fix; no game restart is needed. Failure logs identify request parsing, authentication, synthesis limits or provider failures without recording request text or credentials. GET requests only read existing clips; they cannot trigger paid synthesis. Identical text and voice settings reuse the disk cache across joins and service restarts. New synthesis is capped at one attempt per minute, with at most 256 cached clips. Failure also consumes that interval; a full cache refuses new synthesis until the operator removes unused clips. Audio remains limited to mono 16-bit PCM, 8–48 kHz, 30 seconds and 2 MiB.

The existing /welcome.b64 and /welcome.wav endpoints remain supported. --input-wav supplies the standalone static clip without calling Google; --regenerate explicitly replaces that static clip. To upgrade an older static recording to the new voice, regenerate it once. Dynamic cache entries include the text, voice, model, prompt and endpoint in their identity.

Speech downloads use sequential ?part=N requests on the .b64 URL. Each response contains at most 256 KiB of encoded audio plus a small JSON envelope (offset, total, data), below the engine's REST response limit. The client checks part order, exact sizes and a consistent total, then runs the existing bounded WAV preparation. Audio bytes, quality and duration are unchanged; old cached clips need no regeneration. At most eleven parts can form a 2 MiB WAV. An invalid part or failed request cancels the transfer without playing partial audio. The original full-download URLs remain available for older clients.

Deploy the updated helper before the addon containing the chunked downloader. Restart only the speech helper to activate its new endpoint. The addon update is still required on the mission and clients; updating the helper alone does not fix an older client's oversized downloads. Existing external speech helpers must implement the same part response protocol.

The opt-in native speech-download fixture uses an explicit loopback --speech-probe-url, real REST callbacks, queued decoding and a native playback handle. Compare its speech-downloaded.wav with the served clip for exact byte integrity. It does not prove audible multiplayer playback.

Enable the dynamic briefing

Log in as administrator, open the mission menu from Escape, and select TTS. Enter the speech helper's base URL in Speech bridge URL and its token in Bridge token, then choose Test & enable. Each field has a Paste button that reads the administrator’s local clipboard, without needing to enter text editing mode. Pasting replaces that field only; it does not submit or save anything. Empty, multiline or oversized clipboard input leaves the field unchanged. The token field is masked. A successful generation saves the settings, enables TTS immediately and plays a preview only for that administrator. Failed tests retain the previous settings and audio. The green status means generation was verified; Disable is red and restores the original welcome behavior.

The URL is the helper's address, not Google's text:synthesize endpoint. The token is the helper's --bridge-token-file value, not a Google access token. Google credentials remain on the speech host. Saved tokens are never returned in menu snapshots. Leave the token field blank to keep it when testing the same saved URL; changing the URL requires entering its token. Disable retains the saved address/token for later reuse and cancels any pending test.

On Linux, a nested Gamescope session can prevent the game from reading text copied on the desktop. The Paste button uses the engine's clipboard API and cannot recover text that the compositor has not passed through. If it reports that the game cannot read clipboard text, try launching Reforger without the gamescope ... -- wrapper in Steam's launch options (retain %command% and any other required options), then copy the value again and click Paste. This restarts the client only; it requires no server changes. See the Gamescope clipboard issue. An empty clipboard result does not establish that the desktop clipboard is empty.

Alternatively, edit only the voice section of the dedicated server's $profile:Escapists.json, preserving the rest of the document:

{
  "voice": {
    "welcomeUrl": "",
    "briefingUrl": "https://YOUR_AUDIO_HOST/",
    "briefingToken": "YOUR_PRIVATE_BRIDGE_TOKEN_AT_LEAST_32_CHARACTERS",
    "ttsEnabled": true
  }
}

Bundled playback starts immediately once the normal welcome readiness checks pass. A saved explicit live preference (bundledSpeech: false) verifies its service at startup; unavailable live speech falls back to the generic bundled welcome. The admin test saves the live preference only after verification. Disable keeps the bridge address/token but saves the bundled preference.

Installing the addon supplies the recordings to every player. The external helper is needed only for optional live speech and its admin preview. Legacy welcomeUrl settings are preserved in the profile; the generic bundled welcome is now the default and failure fallback.

Mission code can use the server-only mode.ERR_Speech().Request(text, receiver) entry point after verification. It returns false when unavailable, busy, or rate-limited. The receiver gets a public clip URL/text on success or Failed(); the caller chooses recipients and fallback behavior. Only one request runs at a time, with a five-second minimum interval. The supplied helper additionally limits new synthesis to one per minute; cached text does not call Google again. Soldier dialogue and radio routing are not added by this settings page.

Reforger 1.8's GET callback truncates binary responses at zero bytes, so the helper serves base64 and the client reconstructs and validates the WAV. World cleanup stops playback, removes the temporary file and ignores stale callbacks, while preserving the connection's consumed welcome flag.

Clip reconstruction is queued on the client. Welcome messages, announcements and soldier speech share one preparation slice per game update, limited to 4,095 decoded bytes and an approximately two-millisecond decoding budget. File writes use the same small chunks. Playback waits for the complete WAV and the existing loading/menu readiness checks. This trades some preparation delay for keeping a whole clip's decoding out of a single frame. A completed clip logs its byte count, preparation frames and longest measured slice; neither audio text nor credentials are included. Cancellation removes partial files, and soldier preparation can finish without its network-wait timeout discarding it.

References: Google Chirp 3 voices, Google Gemini-TTS and permissions, Google authentication.

Verification

On 13 October, native profiling reproduced a client preparation stall: decoding the original seven-second synthetic WAV took 274 ms in one call, and a thirty-second WAV took 1,833 ms. The queued version passed 36 native behavioral checks with a maximum preparation slice of 3 ms across the same input sizes. All reconstructed WAVs matched their inputs byte for byte. The seven-second spatial graph took 1 ms to build/write, 2 ms to initialize and 1 ms to start. This was an isolated local test with injected downloaded payloads, not a live HTTP or multiplayer latency measurement. Preparing three clips concurrently took about 18.4 seconds overall; the budget reduces stalls by spreading the work over time. Other mission work still produced longer updates.

tests/test_speech_preparation.py exercises the actual decoder, shared queue, native array removal semantics, cancellation, file failures, readiness gates and soldier timeout handling. Existing lifecycle, speech service, soldier and admin-menu tests also pass. Strict native validation still reports 21 inherited engine/resource diagnostics, and the full repository check stops at the existing missing saved root ID in ERRM_AtlasLeaf.et. Timings, failed attempts and exact evidence.

The 4 October changes have production-method tests for connection claims, waiting for a usable URL/loading/menus, one playback across world replacement, reconnection and cancellation. Python tests cover authenticated HTTP handling, cache reuse, changed text/voice identity, failed-provider rate limits and exact Google request fields. Native Reforger compiled the new code and confirmed the loaded Nogova label and connection claim behavior. Google produced a 16.96-second mono 24 kHz sample with the default gravelly voice. Evidence and the sample are in docs/evidence/welcome-briefing-2026-10-04.

The private dedicated fixture with -noBackend could not execute REST requests; without that flag the isolated namespace could not initialize backend services. The complete live server-to-bridge-to-player path still needs an online playtest. The HTTP handler and client lifecycle have separate automated coverage; this is not a claim of completed live multiplayer audio validation for the new briefing.

Historical fixed-clip playback verification (3 October)

python3 tests/test_welcome_voice.py checks base64 decoding against generated binary data, malformed padding, Google request structure, credential URL guards and preservation of a cached WAV after invalid input. Native evidence is stored under docs/evidence/welcome-voice-2026-10-03.

The Reforger 1.8.0.13 client downloaded a byte-identical 337,258-byte test WAV; captured playback matched its audio envelope at 0.982 correlation. The final dedicated-server/client test made no request while spawn audio was paused, then played once after the loading placeholder was released. Its offline server could not authenticate a soldier, so the test fixture explicitly released that placeholder; the production welcome code does not do so. The clip was generated locally with espeak-ng, not Google.

Google authentication and synthesis were subsequently verified with one real Cloud Text-to-Speech request using en-GB-Neural2-B. The cached result is docs/evidence/welcome-voice-2026-10-03/google-welcome.wav (6.79 seconds, mono 24 kHz, 16-bit PCM). This file can be replayed through the helper with --input-wav without another provider call. See google-synthesis.json and google-review.json in the same evidence directory for the Google test results. Native soldier/radio speech and Google provider credentials remain outside this prototype.

References: Google synthesis REST method, Google authentication.

RKS #2 LAN test setup, 3 October

The cached Google clip is served by escapists-welcome-test.service from /opt/escapists-welcome-test on the AMP host. RKS #2 (RKSserver201) has the legacy EscapistsWelcomeVoice.json in its existing profile, pointing to http://192.168.52.4:18947/welcome.b64. The endpoint was fetched successfully from the developer's computer and its decoded WAV matched the Google clip exactly. The service requires no Google credentials to replay the cached clip. The unified-config build automatically imports this into Escapists.json on first start, together with the other mission preferences.

The installed Escape version at preparation was 0.0.49, and its package did not contain ERR_WelcomeVoice.c. The owner must publish the updated addons/core/addon.gproj, update Escape on RKS #2, and restart the mission before testing. The running game was not restarted during preparation.

This URL is reachable only on the LAN; remote players need a publicly reachable clip URL. To undo the test after migration, clear voice.welcomeUrl in Escapists.json, restart the mission and run sudo systemctl disable --now escapists-welcome-test. The setup record is docs/evidence/welcome-voice-2026-10-03/rks2-lan-setup.json.

Soldier speech investigation (5 October)

The experimental automatic VirtualHQ reports have been removed. They sent periodic HQ report text to a radio listener and played a downloaded WAV through AudioSystem.PlaySound. They were not a replacement for soldiers talking to one another, and their non-spatial playback sounded detached from the soldier. The existing welcome, configured speech bridge, and native AI callouts remain.

The replacement hooks the native SCR_AICommsHandler.TransmitRequest event. Nine short direct-callout types are supported: clear, moving, covering, reloading, taking fire, no ammunition, engaging, target down and lost visual. The actual AI decides when to say them. Contextual direct reports can replace contact/engaging events with a confirmed visible count or observed approach. Other detailed orders and radio transmissions stay native. There are no generic HQ announcements. Calm groups can now have the short exchanges described below.

A supported callout near a living player selects its bundled recording. Nearby clients initialize its native spatial graph without synthesis or downloading. The server replaces a call only after every nearby living player has acknowledged readiness for that exact clip in the current mission. Otherwise the allowed call uses native audio. Covert calls remain silent, including their stock fallback; conversations still require both sides to be ready. Unknown callout types and radio transmissions remain native.

Direct speech uses AudioSystem.PlayEvent at the soldier's transform. Playback follows the soldier near head height, stops on death or loss of a nearby living listener, and keeps the existing 80 metre delivery limit, twelve second lifetime and eight simultaneous actor limit. These are local audio graphs, not native VoN transmission, and are not a full replacement for native radio filtering or all environmental occlusion. Disabling the optional live TTS service does not disable bundled speech.

Verification: production policy tests cover all-client readiness, fallback, actual-event mapping, radio exclusion, range, unavailable service, changed URL and preparation failure. Earlier live-download validation: a hidden client using a null audio sink prepared the production runtime graph and recorded the existing Google WAV with strong left and right separation at five metres. Playback at 500 metres returned an invalid handle and the recording was silent after the preceding clip finished. The production graph played once. The full mod compiled and started in the hidden Nogova dedicated fixture. Live multiplayer callout timing and readiness still need a playtest after publishing; these checks are not an end-to-end multiplayer voice test. Evidence is in docs/evidence/soldier-speech-2026-10-05.

Earlier radio experiment evidence in docs/evidence/hq-speech-2026-10-05 is historical. It proves radio recipient routing, not native soldier voice playback.

Context and conversations (5 October follow-up)

The comms handler suppresses direct and radio callouts while an alerted group has not opened fire or come under fire. This is a conservative proxy for being discovered, not a claim to know what a player has noticed. Own projectile events and incoming bullet impacts or flybys permit combat calls for 20 seconds; mere sightings, aiming and distant gunshots do not. Recent perceived enemies keep casual chatter suppressed for at least 60 seconds after the last threat observation. Disabling/unconfiguring Missing bundled clips preserve allowed native speech; this feature does not change combat decisions.

Reports count distinct currently visible, living enemy characters in the speaker's own native perception, without enumerating players or inspecting vehicle occupants. Counts are one/two/three/at least four. “Getting closer” requires successive fresh sightings of the same target, with at least three metres of observed target movement toward the observer's earlier position. Moving the observer toward a stationary target cannot trigger that report. These reports replace native contact/engaging requests, with a 20 second report interval. New generated lines have a seven second per-speaker interval.

A calm group leader may start one of three brief two-person exchanges, roughly every 90–180 seconds, if a living AI teammate is within eight metres. Every living group member must be calm. Both clips must be prepared for nearby listeners before the opening plays. A reply is queued seven seconds after a successfully delivered opening and is discarded if threats arise, either participant dies, the partner moves away, or its opportunity expires. Clips still warm lazily under the existing 65 second synthesis guard, so new lines can take several eligible encounters to become audible. This is contextual scripted dialogue voiced by Google TTS, not an LLM generating conversations.

Validation for this follow-up: production-method regression harnesses cover stealth fallback, offline bundled playback, count/motion sampling, two-clip readiness, reply cancellation and cooldowns. The complete mod compiled and loaded in the isolated Nogova dedicated fixture without script errors. No live server restart, Workshop publication or end-to-end multiplayer conversation test was performed. Evidence: docs/evidence/soldier-context-2026-10-05/review.json.