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

374 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](bundled-speech.json) and
[`bundled-speech-assets.json`](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.
```sh
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](https://github.com/ValveSoftware/gamescope/issues/1935).
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:
```json
{
"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](https://docs.cloud.google.com/text-to-speech/docs/chirp3-hd),
[Google Gemini-TTS and permissions](https://docs.cloud.google.com/text-to-speech/docs/gemini-tts),
[Google authentication](https://docs.cloud.google.com/text-to-speech/docs/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](evidence/tts-lag-2026-10-13/README.md).
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](https://docs.cloud.google.com/text-to-speech/docs/reference/rest/v1/text/synthesize),
[Google authentication](https://docs.cloud.google.com/text-to-speech/docs/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`.