@umicat/platform-sdk 0.1.0
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.
- package/README.md +82 -0
- package/dist/ai/AiModule.d.ts +62 -0
- package/dist/ai/AiModule.js +107 -0
- package/dist/core/Transport.d.ts +42 -0
- package/dist/core/Transport.js +7 -0
- package/dist/core/UmicatCore.d.ts +80 -0
- package/dist/core/UmicatCore.js +89 -0
- package/dist/core/cacheBust.d.ts +25 -0
- package/dist/core/cacheBust.js +59 -0
- package/dist/core/transports/LocalStorageTransport.d.ts +24 -0
- package/dist/core/transports/LocalStorageTransport.js +83 -0
- package/dist/core/transports/PostMessageTransport.d.ts +32 -0
- package/dist/core/transports/PostMessageTransport.js +127 -0
- package/dist/dialogue/DialogueModule.d.ts +96 -0
- package/dist/dialogue/DialogueModule.js +156 -0
- package/dist/dialogue/runner.d.ts +108 -0
- package/dist/dialogue/runner.js +80 -0
- package/dist/gamedata/GameDataModule.d.ts +45 -0
- package/dist/gamedata/GameDataModule.js +59 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +20 -0
- package/dist/platform/PlatformModule.d.ts +47 -0
- package/dist/platform/PlatformModule.js +63 -0
- package/dist/protocol.d.ts +238 -0
- package/dist/protocol.js +11 -0
- package/dist/realtime/RealtimeModule.d.ts +93 -0
- package/dist/realtime/RealtimeModule.js +115 -0
- package/dist/realtime/UmicatRoom.d.ts +197 -0
- package/dist/realtime/UmicatRoom.js +353 -0
- package/dist/saves/SavesModule.d.ts +23 -0
- package/dist/saves/SavesModule.js +37 -0
- package/dist/voice/VoiceModule.d.ts +44 -0
- package/dist/voice/VoiceModule.js +234 -0
- package/package.json +48 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
const VOICE_EVENT = 'umicat:voice';
|
|
2
|
+
/**
|
|
3
|
+
* Voice input — device/platform speech-to-text with a live mic level for
|
|
4
|
+
* waveform UIs. Access via `umicat.voice`. Never throws from `start()`; it
|
|
5
|
+
* resolves `null` when unsupported or the mic is denied.
|
|
6
|
+
*/
|
|
7
|
+
export class VoiceModule {
|
|
8
|
+
constructor(transport) {
|
|
9
|
+
this.transport = transport;
|
|
10
|
+
}
|
|
11
|
+
/** Does this host declare the native voice capability (WebView bridge path)? */
|
|
12
|
+
hasHostVoice() {
|
|
13
|
+
return !!this.transport.capabilities?.includes('voice') && typeof this.transport.on === 'function';
|
|
14
|
+
}
|
|
15
|
+
/** True if voice input works here — either the native host bridge or the
|
|
16
|
+
* browser's own SpeechRecognition + mic. Check before showing a mic button. */
|
|
17
|
+
supported() {
|
|
18
|
+
if (this.hasHostVoice())
|
|
19
|
+
return true;
|
|
20
|
+
return webVoiceSupported();
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Start a voice session. Resolves to a {@link VoiceSession}, or `null` if
|
|
24
|
+
* unsupported / the mic was denied (callers fall back to typing).
|
|
25
|
+
*/
|
|
26
|
+
async start(lang, cb) {
|
|
27
|
+
if (this.hasHostVoice())
|
|
28
|
+
return this.startHostVoice(lang, cb);
|
|
29
|
+
return startWebVoice(lang, cb);
|
|
30
|
+
}
|
|
31
|
+
/** Native path: drive the platform recognizer through the host bridge. */
|
|
32
|
+
async startHostVoice(lang, cb) {
|
|
33
|
+
const transport = this.transport;
|
|
34
|
+
let level = 0;
|
|
35
|
+
let finalText = '';
|
|
36
|
+
let ended = false;
|
|
37
|
+
let cancelled = false;
|
|
38
|
+
const finish = () => {
|
|
39
|
+
if (ended)
|
|
40
|
+
return;
|
|
41
|
+
ended = true;
|
|
42
|
+
off();
|
|
43
|
+
if (!cancelled) {
|
|
44
|
+
const t = finalText.trim();
|
|
45
|
+
if (t)
|
|
46
|
+
cb.onFinal(t);
|
|
47
|
+
}
|
|
48
|
+
cb.onEnd();
|
|
49
|
+
};
|
|
50
|
+
// Subscribe BEFORE start so no early event is missed (single session, so
|
|
51
|
+
// no sessionId filtering needed).
|
|
52
|
+
const off = transport.on(VOICE_EVENT, (payload) => {
|
|
53
|
+
const e = payload;
|
|
54
|
+
switch (e.event) {
|
|
55
|
+
case 'partial':
|
|
56
|
+
cb.onPartial?.(e.text ?? '');
|
|
57
|
+
break;
|
|
58
|
+
case 'final':
|
|
59
|
+
finalText = e.text ?? finalText;
|
|
60
|
+
break;
|
|
61
|
+
case 'level':
|
|
62
|
+
if (typeof e.level === 'number')
|
|
63
|
+
level = e.level;
|
|
64
|
+
break;
|
|
65
|
+
case 'error':
|
|
66
|
+
if (!cancelled)
|
|
67
|
+
cb.onError?.(e.code ?? 'error');
|
|
68
|
+
finish();
|
|
69
|
+
break;
|
|
70
|
+
case 'end':
|
|
71
|
+
finish();
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
const params = { lang };
|
|
77
|
+
await transport.call('voice.start', params);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
off();
|
|
81
|
+
cb.onError?.('start');
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
level: () => level,
|
|
86
|
+
// Native emits 'final' then 'end' → finish() delivers onFinal.
|
|
87
|
+
stop: () => { transport.call('voice.stop').catch(() => { }); },
|
|
88
|
+
cancel: () => {
|
|
89
|
+
cancelled = true;
|
|
90
|
+
transport.call('voice.cancel').catch(() => { });
|
|
91
|
+
finish();
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/** True if the BROWSER can do speech-to-text (its own recognition) AND give us the mic. */
|
|
97
|
+
export function webVoiceSupported() {
|
|
98
|
+
if (typeof window === 'undefined')
|
|
99
|
+
return false;
|
|
100
|
+
const w = window;
|
|
101
|
+
const hasSR = !!(w.SpeechRecognition || w.webkitSpeechRecognition);
|
|
102
|
+
const hasMic = !!(navigator.mediaDevices && typeof navigator.mediaDevices.getUserMedia === 'function');
|
|
103
|
+
const secure = window.isSecureContext !== false; // getUserMedia needs a secure context (https)
|
|
104
|
+
return hasSR && hasMic && secure;
|
|
105
|
+
}
|
|
106
|
+
/** Browser path: the device's own SpeechRecognition + a Web Audio level meter. */
|
|
107
|
+
async function startWebVoice(lang, cb) {
|
|
108
|
+
const w = window;
|
|
109
|
+
const SRClass = w.SpeechRecognition || w.webkitSpeechRecognition;
|
|
110
|
+
if (!SRClass || !navigator.mediaDevices?.getUserMedia)
|
|
111
|
+
return null;
|
|
112
|
+
// 1) Mic stream + analyser for the live waveform.
|
|
113
|
+
let stream;
|
|
114
|
+
try {
|
|
115
|
+
stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
cb.onError?.('not-allowed'); // denied / no mic
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const AC = window.AudioContext || w.webkitAudioContext;
|
|
122
|
+
const ctx = new AC();
|
|
123
|
+
const source = ctx.createMediaStreamSource(stream);
|
|
124
|
+
const analyser = ctx.createAnalyser();
|
|
125
|
+
analyser.fftSize = 512;
|
|
126
|
+
source.connect(analyser);
|
|
127
|
+
const buf = new Uint8Array(analyser.fftSize);
|
|
128
|
+
// 2) Speech recognition (the browser's own).
|
|
129
|
+
const rec = new SRClass();
|
|
130
|
+
rec.lang = lang;
|
|
131
|
+
rec.interimResults = true;
|
|
132
|
+
rec.continuous = false;
|
|
133
|
+
rec.maxAlternatives = 1;
|
|
134
|
+
let finalText = '';
|
|
135
|
+
let cancelled = false;
|
|
136
|
+
let ended = false;
|
|
137
|
+
const cleanup = () => {
|
|
138
|
+
try {
|
|
139
|
+
rec.onresult = null;
|
|
140
|
+
rec.onerror = null;
|
|
141
|
+
}
|
|
142
|
+
catch { /* ignore */ }
|
|
143
|
+
try {
|
|
144
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
145
|
+
}
|
|
146
|
+
catch { /* ignore */ }
|
|
147
|
+
try {
|
|
148
|
+
source.disconnect();
|
|
149
|
+
analyser.disconnect();
|
|
150
|
+
}
|
|
151
|
+
catch { /* ignore */ }
|
|
152
|
+
try {
|
|
153
|
+
void ctx.close();
|
|
154
|
+
}
|
|
155
|
+
catch { /* ignore */ }
|
|
156
|
+
};
|
|
157
|
+
rec.onresult = (ev) => {
|
|
158
|
+
let interim = '';
|
|
159
|
+
for (let i = ev.resultIndex; i < ev.results.length; i++) {
|
|
160
|
+
const r = ev.results[i];
|
|
161
|
+
if (r.isFinal)
|
|
162
|
+
finalText += r[0].transcript;
|
|
163
|
+
else
|
|
164
|
+
interim += r[0].transcript;
|
|
165
|
+
}
|
|
166
|
+
if (interim)
|
|
167
|
+
cb.onPartial?.(interim);
|
|
168
|
+
};
|
|
169
|
+
rec.onerror = (ev) => { if (!cancelled)
|
|
170
|
+
cb.onError?.(ev.error || 'error'); };
|
|
171
|
+
rec.onend = () => {
|
|
172
|
+
if (ended)
|
|
173
|
+
return;
|
|
174
|
+
ended = true;
|
|
175
|
+
cleanup();
|
|
176
|
+
if (!cancelled) {
|
|
177
|
+
const t = finalText.trim();
|
|
178
|
+
if (t)
|
|
179
|
+
cb.onFinal(t);
|
|
180
|
+
}
|
|
181
|
+
cb.onEnd();
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
rec.start();
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
cleanup();
|
|
188
|
+
cb.onError?.('start');
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
level() {
|
|
193
|
+
try {
|
|
194
|
+
analyser.getByteTimeDomainData(buf);
|
|
195
|
+
let sum = 0;
|
|
196
|
+
for (let i = 0; i < buf.length; i++) {
|
|
197
|
+
const x = (buf[i] - 128) / 128;
|
|
198
|
+
sum += x * x;
|
|
199
|
+
}
|
|
200
|
+
return Math.min(1, Math.sqrt(sum / buf.length) * 3.2); // speech is quiet → scale up
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
stop() {
|
|
207
|
+
try {
|
|
208
|
+
rec.stop();
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
if (!ended) {
|
|
212
|
+
ended = true;
|
|
213
|
+
cleanup();
|
|
214
|
+
const t = finalText.trim();
|
|
215
|
+
if (t)
|
|
216
|
+
cb.onFinal(t);
|
|
217
|
+
cb.onEnd();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
cancel() {
|
|
222
|
+
cancelled = true;
|
|
223
|
+
try {
|
|
224
|
+
rec.abort();
|
|
225
|
+
}
|
|
226
|
+
catch { /* ignore */ }
|
|
227
|
+
if (!ended) {
|
|
228
|
+
ended = true;
|
|
229
|
+
cleanup();
|
|
230
|
+
cb.onEnd();
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@umicat/platform-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Umicat's platform layer, independent of any rendering engine: identity, cloud saves, shared game data, multiplayer, runtime AI, voice and scripted dialogue. Consumed by @umicat/phaser-sdk and @umicat/three-sdk alike.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./protocol": {
|
|
14
|
+
"types": "./dist/protocol.d.ts",
|
|
15
|
+
"default": "./dist/protocol.js"
|
|
16
|
+
},
|
|
17
|
+
"./*.js": {
|
|
18
|
+
"types": "./dist/*.d.ts",
|
|
19
|
+
"default": "./dist/*.js"
|
|
20
|
+
},
|
|
21
|
+
"./*": {
|
|
22
|
+
"types": "./dist/*.d.ts",
|
|
23
|
+
"default": "./dist/*.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc",
|
|
32
|
+
"test": "npm run build && node --test scripts/*.test.mjs"
|
|
33
|
+
},
|
|
34
|
+
"license": "UNLICENSED",
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"typescript": "^5.3.3"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"colyseus.js": "^0.16.0"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "umicat-platform-sdk"
|
|
47
|
+
}
|
|
48
|
+
}
|