@lmk1010/speech-streaming-aliyun 0.0.0-stage → 0.2.2-rc.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DeepSeek
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/lib/index.js ADDED
@@ -0,0 +1,289 @@
1
+ import { execFile } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import z from "@deepseek-ai/schemastery";
4
+ import { Channel } from "@lmk1010/speech-streaming";
5
+ //#region lib/types/index.js
6
+ /**
7
+ * Aliyun Model Studio (DashScope) realtime recognition. One WebSocket is opened ahead of use and reused for every
8
+ * turn (`run-task` … `finish-task`), so a press only pays for the task start, not the TLS and WebSocket handshake.
9
+ */
10
+ const name = "speech-streaming-aliyun";
11
+ const inject = ["speechStreaming"];
12
+ const Config = z.object({
13
+ providerId: z.string().min(1).default("aliyun-realtime"),
14
+ model: z.string().min(1).default("paraformer-realtime-v2"),
15
+ host: z.string().default(""),
16
+ apiKey: z.string().default(""),
17
+ idleCloseMs: z.natural().default(12e4),
18
+ startTimeoutMs: z.natural().min(1e3).default(8e3)
19
+ });
20
+ function keychain(service) {
21
+ if (process.platform !== "darwin") return Promise.resolve("");
22
+ return new Promise((resolve) => {
23
+ execFile("security", [
24
+ "find-generic-password",
25
+ "-a",
26
+ "dshub",
27
+ "-s",
28
+ service,
29
+ "-w"
30
+ ], (error, stdout) => {
31
+ resolve(error ? "" : stdout.trim());
32
+ });
33
+ });
34
+ }
35
+ const SocketWithHeaders = WebSocket;
36
+ /** Reused WebSocket with one task at a time. */
37
+ var Connection = class {
38
+ config;
39
+ credentials;
40
+ socket;
41
+ opening;
42
+ idle;
43
+ handler;
44
+ constructor(config, credentials) {
45
+ this.config = config;
46
+ this.credentials = credentials;
47
+ }
48
+ async get() {
49
+ clearTimeout(this.idle);
50
+ if (this.socket?.readyState === WebSocket.OPEN) return this.socket;
51
+ this.opening ??= (async () => {
52
+ const { host, key } = await this.credentials();
53
+ if (key === "") throw new Error("Aliyun API key is not configured");
54
+ const socket = new SocketWithHeaders(`wss://${host}/api-ws/v1/inference`, { headers: { Authorization: `bearer ${key}` } });
55
+ socket.binaryType = "arraybuffer";
56
+ await new Promise((resolve, reject) => {
57
+ const timer = setTimeout(() => {
58
+ reject(/* @__PURE__ */ new Error("Aliyun connection timed out"));
59
+ socket.close();
60
+ }, this.config.startTimeoutMs);
61
+ socket.onopen = () => {
62
+ clearTimeout(timer);
63
+ resolve();
64
+ };
65
+ socket.onerror = () => {
66
+ clearTimeout(timer);
67
+ reject(/* @__PURE__ */ new Error("Aliyun connection failed"));
68
+ };
69
+ });
70
+ socket.onmessage = (event) => {
71
+ const text = typeof event.data === "string" ? event.data : Buffer.from(event.data).toString();
72
+ try {
73
+ this.handler?.(JSON.parse(text));
74
+ } catch (error) {
75
+ this.handler?.({ header: {
76
+ event: "task-failed",
77
+ error_message: String(error)
78
+ } });
79
+ }
80
+ };
81
+ socket.onclose = () => {
82
+ if (this.socket === socket) this.socket = void 0;
83
+ this.handler?.({ header: {
84
+ event: "task-failed",
85
+ error_message: "connection closed"
86
+ } });
87
+ };
88
+ this.socket = socket;
89
+ return socket;
90
+ })().finally(() => {
91
+ this.opening = void 0;
92
+ });
93
+ return await this.opening;
94
+ }
95
+ listen(handler) {
96
+ this.handler = handler;
97
+ }
98
+ release() {
99
+ clearTimeout(this.idle);
100
+ this.idle = setTimeout(() => {
101
+ this.close();
102
+ }, this.config.idleCloseMs);
103
+ }
104
+ close() {
105
+ clearTimeout(this.idle);
106
+ this.socket?.close();
107
+ this.socket = void 0;
108
+ }
109
+ };
110
+ var AliyunRealtime = class {
111
+ config;
112
+ info;
113
+ connection;
114
+ busy = Promise.resolve();
115
+ constructor(config) {
116
+ this.config = config;
117
+ this.info = {
118
+ id: config.providerId,
119
+ name: `Aliyun ${config.model}`,
120
+ location: "cloud",
121
+ languages: [
122
+ "auto",
123
+ "zh",
124
+ "en"
125
+ ]
126
+ };
127
+ this.connection = new Connection(config, async () => {
128
+ return {
129
+ host: config.host || await keychain("dshub.aliyun.host") || "dashscope.aliyuncs.com",
130
+ key: config.apiKey || process.env.DASHSCOPE_API_KEY || await keychain("dshub.aliyun.dashscope")
131
+ };
132
+ });
133
+ }
134
+ warm() {
135
+ this.connection.get().then(() => {
136
+ this.connection.release();
137
+ }, () => {});
138
+ }
139
+ async open(_options, signal) {
140
+ const previous = this.busy;
141
+ let done;
142
+ this.busy = new Promise((resolve) => {
143
+ done = resolve;
144
+ });
145
+ await previous;
146
+ const events = new Channel();
147
+ const taskId = randomUUID().replaceAll("-", "");
148
+ const finals = [];
149
+ let segment = 0, ended = false;
150
+ const finish = (error) => {
151
+ if (ended) return;
152
+ ended = true;
153
+ this.connection.listen(void 0);
154
+ this.connection.release();
155
+ if (error) events.fail(error);
156
+ else {
157
+ events.push({
158
+ type: "end",
159
+ text: finals.join(""),
160
+ audioSeconds: 0
161
+ });
162
+ events.close();
163
+ }
164
+ done();
165
+ };
166
+ let socket;
167
+ try {
168
+ socket = await this.connection.get();
169
+ } catch (error) {
170
+ finish(error);
171
+ throw error;
172
+ }
173
+ const started = Promise.withResolvers();
174
+ const timer = setTimeout(() => {
175
+ started.reject(/* @__PURE__ */ new Error("Aliyun task did not start"));
176
+ }, this.config.startTimeoutMs);
177
+ this.connection.listen((message) => {
178
+ switch (message.header?.event) {
179
+ case "task-started":
180
+ clearTimeout(timer);
181
+ started.resolve();
182
+ break;
183
+ case "result-generated": {
184
+ const sentence = message.payload?.output?.sentence;
185
+ if (!sentence?.text) break;
186
+ if (sentence.sentence_end) {
187
+ finals.push(sentence.text);
188
+ events.push({
189
+ type: "final",
190
+ segment,
191
+ text: sentence.text
192
+ });
193
+ segment++;
194
+ } else events.push({
195
+ type: "partial",
196
+ segment,
197
+ text: sentence.text
198
+ });
199
+ break;
200
+ }
201
+ case "task-finished":
202
+ finish();
203
+ break;
204
+ case "task-failed":
205
+ clearTimeout(timer);
206
+ started.reject(new Error(message.header.error_message ?? "Aliyun task failed"));
207
+ finish(new Error(message.header.error_message ?? "Aliyun task failed"));
208
+ break;
209
+ default: break;
210
+ }
211
+ });
212
+ socket.send(JSON.stringify({
213
+ header: {
214
+ action: "run-task",
215
+ task_id: taskId,
216
+ streaming: "duplex"
217
+ },
218
+ payload: {
219
+ task_group: "audio",
220
+ task: "asr",
221
+ function: "recognition",
222
+ model: this.config.model,
223
+ parameters: {
224
+ format: "pcm",
225
+ sample_rate: 16e3
226
+ },
227
+ input: {}
228
+ }
229
+ }));
230
+ const bytes = (pcm) => Uint8Array.from(new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength));
231
+ const pending = [];
232
+ let ready = false, finishing = false;
233
+ const sendFinish = () => {
234
+ socket.send(JSON.stringify({
235
+ header: {
236
+ action: "finish-task",
237
+ task_id: taskId,
238
+ streaming: "duplex"
239
+ },
240
+ payload: { input: {} }
241
+ }));
242
+ };
243
+ started.promise.then(() => {
244
+ ready = true;
245
+ for (const chunk of pending) socket.send(chunk);
246
+ pending.length = 0;
247
+ if (finishing) sendFinish();
248
+ }, (error) => {
249
+ finish(error);
250
+ });
251
+ signal.addEventListener("abort", () => {
252
+ if (!ended) {
253
+ if (ready) sendFinish();
254
+ finish(/* @__PURE__ */ new Error("cancelled"));
255
+ }
256
+ }, { once: true });
257
+ return {
258
+ write: (pcm) => {
259
+ if (ready) socket.send(bytes(pcm));
260
+ else pending.push(bytes(pcm));
261
+ },
262
+ finish: () => {
263
+ finishing = true;
264
+ if (ready) sendFinish();
265
+ },
266
+ events
267
+ };
268
+ }
269
+ dispose() {
270
+ this.connection.close();
271
+ }
272
+ };
273
+ /**
274
+ * Register the Aliyun realtime provider.
275
+ * @param ctx - streaming registry owner.
276
+ * @param config - model, host and credentials.
277
+ */
278
+ function apply(ctx, config) {
279
+ const provider = new AliyunRealtime(config);
280
+ ctx.effect(() => {
281
+ const unregister = ctx.speechStreaming.register(provider);
282
+ return async () => {
283
+ provider.dispose();
284
+ await unregister();
285
+ };
286
+ });
287
+ }
288
+ //#endregion
289
+ export { AliyunRealtime, Config, apply, inject, name };
@@ -0,0 +1,41 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ import { type SpeechProviderId, type StreamingProvider, type StreamingSession, type StreamOptions } from '@lmk1010/speech-streaming';
4
+ export declare const name = "speech-streaming-aliyun";
5
+ export declare const inject: string[];
6
+ export interface Config {
7
+ providerId: string;
8
+ /** Realtime model: `paraformer-realtime-v2` or `fun-asr-realtime`. */
9
+ model: string;
10
+ /** API host; empty reads the Keychain item `dshub.aliyun.host`, then falls back to `dashscope.aliyuncs.com`. */
11
+ host: string;
12
+ /** API key; empty reads `DASHSCOPE_API_KEY`, then the macOS Keychain item `dshub.aliyun.dashscope`. */
13
+ apiKey: string;
14
+ /** Close the idle connection after this long. */
15
+ idleCloseMs: number;
16
+ /** Deadline for the connection and the task start. */
17
+ startTimeoutMs: number;
18
+ }
19
+ export declare const Config: z<Partial<Config>, Config>;
20
+ export declare class AliyunRealtime implements StreamingProvider {
21
+ private readonly config;
22
+ readonly info: {
23
+ id: SpeechProviderId;
24
+ name: string;
25
+ location: "cloud";
26
+ languages: string[];
27
+ };
28
+ private readonly connection;
29
+ private busy;
30
+ constructor(config: Config);
31
+ warm(): void;
32
+ open(_options: StreamOptions, signal: AbortSignal): Promise<StreamingSession>;
33
+ dispose(): void;
34
+ }
35
+ /**
36
+ * Register the Aliyun realtime provider.
37
+ * @param ctx - streaming registry owner.
38
+ * @param config - model, host and credentials.
39
+ */
40
+ export declare function apply(ctx: Context, config: Config): void;
41
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Aliyun Model Studio (DashScope) realtime recognition. One WebSocket is opened ahead of use and reused for every
3
+ * turn (`run-task` … `finish-task`), so a press only pays for the task start, not the TLS and WebSocket handshake.
4
+ */
5
+ import { execFile } from 'node:child_process';
6
+ import { randomUUID } from 'node:crypto';
7
+ import z from '@deepseek-ai/schemastery';
8
+ import { Channel } from '@lmk1010/speech-streaming';
9
+ export const name = 'speech-streaming-aliyun';
10
+ export const inject = ['speechStreaming'];
11
+ export const Config = z.object({
12
+ providerId: z.string().min(1).default('aliyun-realtime'),
13
+ model: z.string().min(1).default('paraformer-realtime-v2'),
14
+ host: z.string().default(''),
15
+ apiKey: z.string().default(''),
16
+ idleCloseMs: z.natural().default(120_000),
17
+ startTimeoutMs: z.natural().min(1000).default(8000),
18
+ });
19
+ function keychain(service) {
20
+ if (process.platform !== 'darwin')
21
+ return Promise.resolve('');
22
+ return new Promise((resolve) => {
23
+ execFile('security', ['find-generic-password', '-a', 'dshub', '-s', service, '-w'], (error, stdout) => { resolve(error ? '' : stdout.trim()); });
24
+ });
25
+ }
26
+ const SocketWithHeaders = WebSocket;
27
+ /** Reused WebSocket with one task at a time. */
28
+ class Connection {
29
+ config;
30
+ credentials;
31
+ socket;
32
+ opening;
33
+ idle;
34
+ handler;
35
+ constructor(config, credentials) {
36
+ this.config = config;
37
+ this.credentials = credentials;
38
+ }
39
+ async get() {
40
+ clearTimeout(this.idle);
41
+ if (this.socket?.readyState === WebSocket.OPEN)
42
+ return this.socket;
43
+ this.opening ??= (async () => {
44
+ const { host, key } = await this.credentials();
45
+ if (key === '')
46
+ throw new Error('Aliyun API key is not configured');
47
+ const socket = new SocketWithHeaders(`wss://${host}/api-ws/v1/inference`, { headers: { Authorization: `bearer ${key}` } });
48
+ socket.binaryType = 'arraybuffer';
49
+ await new Promise((resolve, reject) => {
50
+ const timer = setTimeout(() => { reject(new Error('Aliyun connection timed out')); socket.close(); }, this.config.startTimeoutMs);
51
+ socket.onopen = () => { clearTimeout(timer); resolve(); };
52
+ socket.onerror = () => { clearTimeout(timer); reject(new Error('Aliyun connection failed')); };
53
+ });
54
+ socket.onmessage = (event) => {
55
+ const text = typeof event.data === 'string' ? event.data : Buffer.from(event.data).toString();
56
+ try {
57
+ this.handler?.(JSON.parse(text));
58
+ }
59
+ catch (error) {
60
+ this.handler?.({ header: { event: 'task-failed', error_message: String(error) } });
61
+ }
62
+ };
63
+ socket.onclose = () => { if (this.socket === socket)
64
+ this.socket = undefined; this.handler?.({ header: { event: 'task-failed', error_message: 'connection closed' } }); };
65
+ this.socket = socket;
66
+ return socket;
67
+ })().finally(() => { this.opening = undefined; });
68
+ return await this.opening;
69
+ }
70
+ listen(handler) { this.handler = handler; }
71
+ release() {
72
+ clearTimeout(this.idle);
73
+ this.idle = setTimeout(() => { this.close(); }, this.config.idleCloseMs);
74
+ }
75
+ close() { clearTimeout(this.idle); this.socket?.close(); this.socket = undefined; }
76
+ }
77
+ export class AliyunRealtime {
78
+ config;
79
+ info;
80
+ connection;
81
+ busy = Promise.resolve();
82
+ constructor(config) {
83
+ this.config = config;
84
+ this.info = { id: config.providerId, name: `Aliyun ${config.model}`, location: 'cloud', languages: ['auto', 'zh', 'en'] };
85
+ this.connection = new Connection(config, async () => {
86
+ // Read on every new connection (a few milliseconds), so a key saved, replaced or deleted in settings applies at once.
87
+ return {
88
+ host: config.host || await keychain('dshub.aliyun.host') || 'dashscope.aliyuncs.com',
89
+ key: config.apiKey || process.env.DASHSCOPE_API_KEY || await keychain('dshub.aliyun.dashscope'),
90
+ };
91
+ });
92
+ }
93
+ warm() { void this.connection.get().then(() => { this.connection.release(); }, () => { }); }
94
+ async open(_options, signal) {
95
+ // One task at a time on the shared connection.
96
+ const previous = this.busy;
97
+ let done;
98
+ this.busy = new Promise((resolve) => { done = resolve; });
99
+ await previous;
100
+ const events = new Channel();
101
+ const taskId = randomUUID().replaceAll('-', '');
102
+ const finals = [];
103
+ let segment = 0, ended = false;
104
+ const finish = (error) => {
105
+ if (ended)
106
+ return;
107
+ ended = true;
108
+ this.connection.listen(undefined);
109
+ this.connection.release();
110
+ if (error)
111
+ events.fail(error);
112
+ else {
113
+ events.push({ type: 'end', text: finals.join(''), audioSeconds: 0 });
114
+ events.close();
115
+ }
116
+ done();
117
+ };
118
+ let socket;
119
+ try {
120
+ socket = await this.connection.get();
121
+ }
122
+ catch (error) {
123
+ finish(error);
124
+ throw error;
125
+ }
126
+ const started = Promise.withResolvers();
127
+ const timer = setTimeout(() => { started.reject(new Error('Aliyun task did not start')); }, this.config.startTimeoutMs);
128
+ this.connection.listen((message) => {
129
+ switch (message.header?.event) {
130
+ case 'task-started':
131
+ clearTimeout(timer);
132
+ started.resolve();
133
+ break;
134
+ case 'result-generated': {
135
+ const sentence = message.payload?.output?.sentence;
136
+ if (!sentence?.text)
137
+ break;
138
+ if (sentence.sentence_end) {
139
+ finals.push(sentence.text);
140
+ events.push({ type: 'final', segment, text: sentence.text });
141
+ segment++;
142
+ }
143
+ else
144
+ events.push({ type: 'partial', segment, text: sentence.text });
145
+ break;
146
+ }
147
+ case 'task-finished':
148
+ finish();
149
+ break;
150
+ case 'task-failed':
151
+ clearTimeout(timer);
152
+ started.reject(new Error(message.header.error_message ?? 'Aliyun task failed'));
153
+ finish(new Error(message.header.error_message ?? 'Aliyun task failed'));
154
+ break;
155
+ default: break;
156
+ }
157
+ });
158
+ socket.send(JSON.stringify({ header: { action: 'run-task', task_id: taskId, streaming: 'duplex' },
159
+ payload: { task_group: 'audio', task: 'asr', function: 'recognition', model: this.config.model,
160
+ parameters: { format: 'pcm', sample_rate: 16000 }, input: {} } }));
161
+ // WebSocket.send needs bytes backed by an ordinary ArrayBuffer.
162
+ const bytes = (pcm) => Uint8Array.from(new Uint8Array(pcm.buffer, pcm.byteOffset, pcm.byteLength));
163
+ const pending = [];
164
+ let ready = false, finishing = false;
165
+ const sendFinish = () => {
166
+ socket.send(JSON.stringify({ header: { action: 'finish-task', task_id: taskId, streaming: 'duplex' }, payload: { input: {} } }));
167
+ };
168
+ void started.promise.then(() => {
169
+ ready = true;
170
+ for (const chunk of pending)
171
+ socket.send(chunk);
172
+ pending.length = 0;
173
+ if (finishing)
174
+ sendFinish();
175
+ }, (error) => { finish(error); });
176
+ signal.addEventListener('abort', () => { if (!ended) {
177
+ if (ready)
178
+ sendFinish();
179
+ finish(new Error('cancelled'));
180
+ } }, { once: true });
181
+ return {
182
+ write: (pcm) => { if (ready)
183
+ socket.send(bytes(pcm));
184
+ else
185
+ pending.push(bytes(pcm)); },
186
+ finish: () => { finishing = true; if (ready)
187
+ sendFinish(); },
188
+ events,
189
+ };
190
+ }
191
+ dispose() { this.connection.close(); }
192
+ }
193
+ /**
194
+ * Register the Aliyun realtime provider.
195
+ * @param ctx - streaming registry owner.
196
+ * @param config - model, host and credentials.
197
+ */
198
+ export function apply(ctx, config) {
199
+ const provider = new AliyunRealtime(config);
200
+ ctx.effect(() => {
201
+ const unregister = ctx.speechStreaming.register(provider);
202
+ return async () => { provider.dispose(); await unregister(); };
203
+ });
204
+ }
205
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,42 @@
1
1
  {
2
2
  "name": "@lmk1010/speech-streaming-aliyun",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "description": "Aliyun Model Studio realtime speech recognition (Paraformer / Fun-ASR) over a reused WebSocket",
4
+ "version": "0.2.2-rc.1",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "types": "lib/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./lib/types/index.d.ts",
11
+ "default": "./lib/index.js"
12
+ },
13
+ "./src/*": "./src/*",
14
+ "./package.json": "./package.json"
15
+ },
16
+ "files": [
17
+ "lib/index.js",
18
+ "lib/types/**/*.js",
19
+ "lib/types/**/*.d.ts"
20
+ ],
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@deepseek-ai/schemastery": "~3.19.0"
24
+ },
25
+ "peerDependencies": {
26
+ "@deepseek-ai/cordis": "~4.1.0",
27
+ "@lmk1010/speech-streaming": "0.2.2-rc.1"
28
+ },
29
+ "devDependencies": {
30
+ "@deepseek-ai/cordis": "~4.1.0",
31
+ "@lmk1010/speech-streaming": "0.2.2-rc.1"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/lmk1010/dsh-airpods-space.git",
39
+ "directory": "packages/airpods/speech-streaming-aliyun"
40
+ },
41
+ "homepage": "https://github.com/lmk1010/dsh-airpods-space#readme"
6
42
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.