@voicethere/agent 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,10 +17,10 @@ import {
17
17
 
18
18
  ## Product vs e2e
19
19
 
20
- | Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
21
- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
20
+ | Kind | Dashboard create | Prebuilt seed bundle | Typical consumer |
21
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | ----------------------- |
22
22
  | **product** | Yes (`echo`, `echo-dc`, `voice-starter`, `language-switch`, `world-sync`, `world-sync-binary`, `game-sync`, `voice-showcase`, `recording-consent`, `positional-tts`, `spatial-showcase`, `webhooks`, `webhooks-redis`) | Yes — `dist/templates/<id>/agent.js` | Platform project create |
23
- | **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
23
+ | **e2e** | No | No — build from sources at test time | `voicethere/e2e` smokes |
24
24
 
25
25
  Product templates always set `seedOnCreate: true`. CI fails if a product template is missing its prebuilt bundle after `npm run build`.
26
26
 
@@ -47,11 +47,11 @@ npm run build:templates
47
47
 
48
48
  Three product templates cover positional / world sync, from JSON to Redis:
49
49
 
50
- | Id | Channel | State |
51
- | -------------------- | ------------------------------- | ----------------------------- |
52
- | `world-sync` | `onDataChannelMessage` (JSON) | One agent, in-memory, no Redis |
53
- | `world-sync-binary` | `onDataChannelBinary` + `broadCastBinaryToClients` (`ArrayBuffer`) | One agent, in-memory, no Redis |
54
- | `game-sync` | JSON control + binary world snapshots | Redis when `AGENT_REDIS_URL` is set |
50
+ | Id | Channel | State |
51
+ | ------------------- | ------------------------------------------------------------------ | ----------------------------------- |
52
+ | `world-sync` | `onDataChannelMessage` (JSON) | One agent, in-memory, no Redis |
53
+ | `world-sync-binary` | `onDataChannelBinary` + `broadCastBinaryToClients` (`ArrayBuffer`) | One agent, in-memory, no Redis |
54
+ | `game-sync` | JSON control + binary world snapshots | Redis when `AGENT_REDIS_URL` is set |
55
55
 
56
56
  See each folder README for the wire format.
57
57
 
@@ -59,33 +59,33 @@ See each folder README for the wire format.
59
59
 
60
60
  Each folder has its own README. Summary:
61
61
 
62
- | Id | Folder | Summary |
63
- | -- | ------ | ------- |
64
- | `echo` | `echo/` | Voice + chat echo |
65
- | `echo-dc` | `echo-dc/` | Data-channel echo, no TTS |
66
- | `voice-starter` | `voice-starter/` | Every speech event |
67
- | `language-switch` | `language-switch/` | Separate `setVoiceLanguage` calls for STT, TTS, and vendor |
68
- | `world-sync` | `world-sync/` | JSON pose broadcast |
69
- | `world-sync-binary` | `world-sync-binary/` | Binary pose `ArrayBuffer` |
70
- | `game-sync` | `game-sync/` | Authoritative sim, Redis + binary snapshots |
71
- | `voice-showcase` | `voice-showcase/` | Conversational landing demo |
72
- | `recording-consent` | `recording-consent/` | Recording consent flow |
73
- | `positional-tts` | `positional-tts/` | Orbiting TTS |
74
- | `spatial-showcase` | `spatial-showcase/` | Orbit / soundboard / proximity |
75
- | `webhooks` | `webhooks/` | Inbound HMAC webhooks |
76
- | `webhooks-redis` | `webhooks-redis/` | Webhooks plus Redis counter |
62
+ | Id | Folder | Summary |
63
+ | ------------------- | -------------------- | ----------------------------------------------------------------- |
64
+ | `echo` | `echo/` | Voice + chat echo |
65
+ | `echo-dc` | `echo-dc/` | Data-channel echo, no TTS |
66
+ | `voice-starter` | `voice-starter/` | Every speech event |
67
+ | `language-switch` | `language-switch/` | Manual `setVoiceLanguage` or runner auto-switch + prompt handlers |
68
+ | `world-sync` | `world-sync/` | JSON pose broadcast |
69
+ | `world-sync-binary` | `world-sync-binary/` | Binary pose `ArrayBuffer` |
70
+ | `game-sync` | `game-sync/` | Authoritative sim, Redis + binary snapshots |
71
+ | `voice-showcase` | `voice-showcase/` | Conversational landing demo |
72
+ | `recording-consent` | `recording-consent/` | Recording consent flow |
73
+ | `positional-tts` | `positional-tts/` | Orbiting TTS |
74
+ | `spatial-showcase` | `spatial-showcase/` | Orbit / soundboard / proximity |
75
+ | `webhooks` | `webhooks/` | Inbound HMAC webhooks |
76
+ | `webhooks-redis` | `webhooks-redis/` | Webhooks plus Redis counter |
77
77
 
78
78
  ## E2e templates
79
79
 
80
80
  These mirror former `e2e/fixtures/*` sources. E2E resolves entries from the package, builds into ephemeral workdirs, and uploads `dist/agent.js`.
81
81
 
82
- | Id | Source | Purpose |
83
- | ----------------- | ---------------------------------------------- | --------------------------------------------- |
84
- | `echo-smoke` | `echo-smoke/agent.ts` | voice-smoke, agent-smoke, cli-smoke |
85
- | `crash` | `crash/agent.ts` | session-errors-smoke, crash-policy smokes |
86
- | `game-sync-smoke` | `game-sync-smoke/agent.ts` | deploy-smoke, shared-child, idle smokes |
87
- | `redis-sync` | `redis-sync/agent.ts` + `world-layout.ts` | redis-sync-smoke (binary positions + Redis world blob) |
88
- | `mix-smoke` | `mix-smoke/agent.ts` | voice-data-mix-smoke |
82
+ | Id | Source | Purpose |
83
+ | ----------------- | ----------------------------------------- | ------------------------------------------------------ |
84
+ | `echo-smoke` | `echo-smoke/agent.ts` | voice-smoke, agent-smoke, cli-smoke |
85
+ | `crash` | `crash/agent.ts` | session-errors-smoke, crash-policy smokes |
86
+ | `game-sync-smoke` | `game-sync-smoke/agent.ts` | deploy-smoke, shared-child, idle smokes |
87
+ | `redis-sync` | `redis-sync/agent.ts` + `world-layout.ts` | redis-sync-smoke (binary positions + Redis world blob) |
88
+ | `mix-smoke` | `mix-smoke/agent.ts` | voice-data-mix-smoke |
89
89
 
90
90
  **Note:** Product `echo` is not the same as e2e `echo-smoke` — keep both ids.
91
91
 
@@ -1,12 +1,22 @@
1
1
  # language-switch
2
2
 
3
- Spoken-language detection tells the agent which ISO 639-1 code it heard. It does **not** change the TTS voice or the STT model. This template changes the two sides separately with `setVoiceLanguage`:
3
+ Spoken-language detection tells the agent which ISO 639-1 code it heard. It does **not** change the TTS voice or the STT model by itself. This template shows how to react in two ways:
4
+
5
+ ## Mode A — Manual switch (default)
6
+
7
+ Leave the project **spoken-language auto-switch** setting off. When `onUserLanguage` fires, this agent calls `setVoiceLanguage` separately for each side:
4
8
 
5
9
  - `scope: "tts"` switches only the speaking voice
6
10
  - `scope: "stt"` switches only the listening model
7
11
 
8
12
  A Sherpa language with no STT id (`it`, `pt`, `nl`, `pl`, `hi`) fails the STT call and still switches TTS.
9
13
 
14
+ ## Mode B — Runner auto-switch
15
+
16
+ Enable auto-switch in the project voice settings (runner applies STT/TTS when LID detects a new language). When `session_start.env` includes a truthy `SHERPA_LID_AUTO_SWITCH`, this template **does not** call `setVoiceLanguage` from `onUserLanguage` — it logs that the runner owns the switch and updates prompts only. Use `onVoiceLanguageChanged` when you need the committed language after the runner applies the change.
17
+
18
+ Detection and chat commands are unchanged: `/tts` and `/stt` still call `setVoiceLanguage` for one vendor at a time.
19
+
10
20
  Chat commands change one vendor while the session stays connected. API keys are project secrets on the running deploy, not arguments:
11
21
 
12
22
  - `/tts sherpa de` — Sherpa TTS only
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * Change STT and TTS separately, including the vendor, while the call stays up.
3
3
  *
4
- * The runner reports `user_language` and does not change either side. This
5
- * agent switches the Sherpa voice and the Sherpa STT model as two calls, so
6
- * one side can fail without blocking the other. Chat commands change a single
7
- * vendor mid-conversation:
4
+ * Two deployment modes (see README):
5
+ *
6
+ * (A) Project auto-switch off — this agent calls `setVoiceLanguage` from
7
+ * `onUserLanguage` when LID detects a new language.
8
+ * (B) Project enables runner auto-switch — STT/TTS are runner-owned; use
9
+ * `onUserLanguage` / `onVoiceLanguageChanged` for prompts only.
10
+ *
11
+ * Chat commands change a single vendor mid-conversation:
8
12
  *
9
13
  * - `/tts sherpa de` — Sherpa TTS only
10
14
  * - `/stt sherpa de` — Sherpa STT only
@@ -17,6 +21,7 @@
17
21
  import {
18
22
  agentLog,
19
23
  defineAgent,
24
+ isRunnerLidAutoSwitchEnabled,
20
25
  parseChatText,
21
26
  setVoiceLanguage,
22
27
  speak,
@@ -39,17 +44,22 @@ const REPLIES: Record<string, string> = {
39
44
 
40
45
  type SessionLanguage = {
41
46
  language: string;
47
+ runnerAutoSwitch: boolean;
42
48
  echoTimer: ReturnType<typeof setTimeout> | undefined;
43
49
  suppressNextFinal: boolean;
44
50
  };
45
51
 
46
52
  const sessions = new Map<string, SessionLanguage>();
47
53
 
48
- function stateFor(sessionId: string): SessionLanguage {
54
+ function stateFor(
55
+ sessionId: string,
56
+ env?: Record<string, string>,
57
+ ): SessionLanguage {
49
58
  const existing = sessions.get(sessionId);
50
59
  if (existing) return existing;
51
60
  const created: SessionLanguage = {
52
61
  language: "en",
62
+ runnerAutoSwitch: env ? isRunnerLidAutoSwitchEnabled(env) : false,
53
63
  echoTimer: undefined,
54
64
  suppressNextFinal: false,
55
65
  };
@@ -61,9 +71,16 @@ function replyFor(language: string): string {
61
71
  return REPLIES[language] ?? `Continuing in ${language}.`;
62
72
  }
63
73
 
64
- function logSwitch(sessionId: string, side: string, result: VoiceLanguageResult): void {
74
+ function logSwitch(
75
+ sessionId: string,
76
+ side: string,
77
+ result: VoiceLanguageResult,
78
+ ): void {
65
79
  if (!result.ok) {
66
- agentLog("warn", `setVoiceLanguage ${side} failed: ${result.reason ?? "unknown"}`);
80
+ agentLog(
81
+ "warn",
82
+ `setVoiceLanguage ${side} failed: ${result.reason ?? "unknown"}`,
83
+ );
67
84
  return;
68
85
  }
69
86
  const provider = side === "tts" ? result.ttsProvider : result.sttProvider;
@@ -74,21 +91,45 @@ function logSwitch(sessionId: string, side: string, result: VoiceLanguageResult)
74
91
  );
75
92
  }
76
93
 
94
+ function prepareLanguageTransition(state: SessionLanguage): void {
95
+ if (state.echoTimer) {
96
+ clearTimeout(state.echoTimer);
97
+ state.echoTimer = undefined;
98
+ } else {
99
+ state.suppressNextFinal = true;
100
+ }
101
+ }
102
+
77
103
  defineAgent({
78
- onSessionStart({ sessionId }) {
79
- stateFor(sessionId);
80
- agentLog("info", `language-switch session_start ${sessionId}`);
104
+ onSessionStart({ sessionId, env }) {
105
+ const state = stateFor(sessionId, env);
106
+ if (state.runnerAutoSwitch) {
107
+ agentLog(
108
+ "info",
109
+ `language-switch session_start ${sessionId} runner LID auto-switch owns STT/TTS`,
110
+ );
111
+ } else {
112
+ agentLog(
113
+ "info",
114
+ `language-switch session_start ${sessionId} manual setVoiceLanguage`,
115
+ );
116
+ }
81
117
  },
82
118
 
83
119
  async onUserLanguage({ sessionId, language }) {
84
120
  const state = stateFor(sessionId);
85
121
  if (!language || language === state.language) return;
86
122
 
87
- if (state.echoTimer) {
88
- clearTimeout(state.echoTimer);
89
- state.echoTimer = undefined;
90
- } else {
91
- state.suppressNextFinal = true;
123
+ prepareLanguageTransition(state);
124
+
125
+ if (state.runnerAutoSwitch) {
126
+ agentLog(
127
+ "info",
128
+ `LID detected ${language}; runner auto-switch applies STT/TTS — agent updates prompts only`,
129
+ );
130
+ state.language = language;
131
+ speak(sessionId, replyFor(language));
132
+ return;
92
133
  }
93
134
 
94
135
  const tts = await setVoiceLanguage(sessionId, {
@@ -114,6 +155,18 @@ defineAgent({
114
155
  speak(sessionId, replyFor(language));
115
156
  },
116
157
 
158
+ async onVoiceLanguageChanged({ sessionId, language }) {
159
+ const state = stateFor(sessionId);
160
+ if (!state.runnerAutoSwitch || !language || language === state.language) {
161
+ return;
162
+ }
163
+ agentLog(
164
+ "info",
165
+ `runner committed voice language ${language} for ${sessionId}`,
166
+ );
167
+ state.language = language;
168
+ },
169
+
117
170
  async onDataChannelMessage(ctx) {
118
171
  const text = parseChatText(ctx.message);
119
172
  if (!text) return;