sa2kit 1.6.49 → 1.6.52
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/dist/{chunk-QROLPPXP.mjs → chunk-PKKIDPXE.mjs} +3 -3
- package/dist/{chunk-QROLPPXP.mjs.map → chunk-PKKIDPXE.mjs.map} +1 -1
- package/dist/{chunk-VLZ5N6XZ.js → chunk-TSTBLX6B.js} +3 -3
- package/dist/{chunk-VLZ5N6XZ.js.map → chunk-TSTBLX6B.js.map} +1 -1
- package/dist/iflytek/index.d.mts +109 -0
- package/dist/iflytek/index.d.ts +109 -0
- package/dist/iflytek/index.js +365 -0
- package/dist/iflytek/index.js.map +1 -0
- package/dist/iflytek/index.mjs +362 -0
- package/dist/iflytek/index.mjs.map +1 -0
- package/dist/iflytek/server.d.mts +59 -0
- package/dist/iflytek/server.d.ts +59 -0
- package/dist/iflytek/server.js +281 -0
- package/dist/iflytek/server.js.map +1 -0
- package/dist/iflytek/server.mjs +274 -0
- package/dist/iflytek/server.mjs.map +1 -0
- package/dist/showmasterpiece/index.js +45 -45
- package/dist/showmasterpiece/index.js.map +1 -1
- package/dist/showmasterpiece/index.mjs +45 -45
- package/dist/showmasterpiece/index.mjs.map +1 -1
- package/dist/showmasterpiece/migration/index.js +10 -10
- package/dist/showmasterpiece/migration/index.mjs +2 -2
- package/dist/types-C4bbgHHW.d.mts +100 -0
- package/dist/types-C4bbgHHW.d.ts +100 -0
- package/dist/universalFile/server/index.js +80 -80
- package/dist/universalFile/server/index.mjs +2 -2
- package/package.json +20 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { d as IflytekClientConfig, e as IflytekClientEvents, f as IflytekPhase, A as AudioRecorder } from '../types-C4bbgHHW.mjs';
|
|
2
|
+
export { a as IflytekAudioFrame, j as IflytekErrorPayload, h as IflytekReadyPayload, i as IflytekResultPayload, I as IflytekServerConfig, b as IflytekStartPayload, c as IflytekStopPayload, g as IflytekTransport } from '../types-C4bbgHHW.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 讯飞语音转文字 — 客户端核心类
|
|
6
|
+
*
|
|
7
|
+
* 纯逻辑、无 UI 依赖。管理:
|
|
8
|
+
* 本地 PCM 录音 → 通过 transport(Socket.IO)传输到服务端适配层 → 接收转写结果
|
|
9
|
+
*
|
|
10
|
+
* 状态机:idle → connecting → recording → stopping → idle
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare class IflytekSTT {
|
|
14
|
+
private transport;
|
|
15
|
+
private recorder;
|
|
16
|
+
private sampleRate;
|
|
17
|
+
private language;
|
|
18
|
+
private domain;
|
|
19
|
+
private accent;
|
|
20
|
+
private readyTimeout;
|
|
21
|
+
private stopWaitTimeout;
|
|
22
|
+
private debug;
|
|
23
|
+
private events;
|
|
24
|
+
private session;
|
|
25
|
+
private bound;
|
|
26
|
+
private lastPressTime;
|
|
27
|
+
private handleReady;
|
|
28
|
+
private handleResult;
|
|
29
|
+
private handleError;
|
|
30
|
+
constructor(config: IflytekClientConfig);
|
|
31
|
+
/** 注册事件回调 */
|
|
32
|
+
on(events: IflytekClientEvents): this;
|
|
33
|
+
/** 当前阶段 */
|
|
34
|
+
get phase(): IflytekPhase;
|
|
35
|
+
/** 当前会话 ID */
|
|
36
|
+
get sessionId(): string | null;
|
|
37
|
+
/** 是否正在录音(connecting / recording / stopping) */
|
|
38
|
+
get isActive(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 开始录音。
|
|
41
|
+
* 对应 onPressIn / 按钮按下。
|
|
42
|
+
* 返回 true 表示成功启动,false 表示被忽略(去抖 / 已有活跃会话)。
|
|
43
|
+
*/
|
|
44
|
+
start(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* 停止录音。
|
|
47
|
+
* 对应 onPressOut / 按钮松开。
|
|
48
|
+
*/
|
|
49
|
+
stop(): void;
|
|
50
|
+
/** 强制终止当前会话(页面卸载 / 引擎切换时调用) */
|
|
51
|
+
abort(): void;
|
|
52
|
+
/** 完全释放资源,解除 transport 监听 */
|
|
53
|
+
dispose(): void;
|
|
54
|
+
private _onReady;
|
|
55
|
+
private _onResult;
|
|
56
|
+
private _onError;
|
|
57
|
+
private _onAudioData;
|
|
58
|
+
private sendFinal;
|
|
59
|
+
private destroy;
|
|
60
|
+
private setPhase;
|
|
61
|
+
private bindTransport;
|
|
62
|
+
private unbindTransport;
|
|
63
|
+
private log;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 基于 Web Audio API 的 AudioRecorder 实现
|
|
68
|
+
*
|
|
69
|
+
* 适用于浏览器环境(Next.js 客户端 / Electron 渲染进程)。
|
|
70
|
+
* 输出与 React Native PCM 库一致的 base64 编码 16-bit PCM 数据。
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { IflytekSTT, WebAudioRecorder } from "sa2kit/iflytek";
|
|
75
|
+
*
|
|
76
|
+
* const recorder = new WebAudioRecorder();
|
|
77
|
+
* const stt = new IflytekSTT({ transport: socket, recorder });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
declare class WebAudioRecorder implements AudioRecorder {
|
|
82
|
+
private stream;
|
|
83
|
+
private audioCtx;
|
|
84
|
+
private sourceNode;
|
|
85
|
+
private workletNode;
|
|
86
|
+
private processorNode;
|
|
87
|
+
private callback;
|
|
88
|
+
private sampleRate;
|
|
89
|
+
private bufferSize;
|
|
90
|
+
private running;
|
|
91
|
+
init(options: {
|
|
92
|
+
sampleRate: number;
|
|
93
|
+
channels: number;
|
|
94
|
+
bitsPerSample: number;
|
|
95
|
+
audioSource: number;
|
|
96
|
+
bufferSize: number;
|
|
97
|
+
}): void;
|
|
98
|
+
on(event: "data", callback: (base64Chunk: string) => void): {
|
|
99
|
+
remove: () => void;
|
|
100
|
+
};
|
|
101
|
+
start(): void;
|
|
102
|
+
stop(): void;
|
|
103
|
+
private startAsync;
|
|
104
|
+
private cleanup;
|
|
105
|
+
private float32ToBase64PCM16;
|
|
106
|
+
private closestPow2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { AudioRecorder, IflytekClientConfig, IflytekClientEvents, IflytekPhase, IflytekSTT, WebAudioRecorder };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { d as IflytekClientConfig, e as IflytekClientEvents, f as IflytekPhase, A as AudioRecorder } from '../types-C4bbgHHW.js';
|
|
2
|
+
export { a as IflytekAudioFrame, j as IflytekErrorPayload, h as IflytekReadyPayload, i as IflytekResultPayload, I as IflytekServerConfig, b as IflytekStartPayload, c as IflytekStopPayload, g as IflytekTransport } from '../types-C4bbgHHW.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 讯飞语音转文字 — 客户端核心类
|
|
6
|
+
*
|
|
7
|
+
* 纯逻辑、无 UI 依赖。管理:
|
|
8
|
+
* 本地 PCM 录音 → 通过 transport(Socket.IO)传输到服务端适配层 → 接收转写结果
|
|
9
|
+
*
|
|
10
|
+
* 状态机:idle → connecting → recording → stopping → idle
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
declare class IflytekSTT {
|
|
14
|
+
private transport;
|
|
15
|
+
private recorder;
|
|
16
|
+
private sampleRate;
|
|
17
|
+
private language;
|
|
18
|
+
private domain;
|
|
19
|
+
private accent;
|
|
20
|
+
private readyTimeout;
|
|
21
|
+
private stopWaitTimeout;
|
|
22
|
+
private debug;
|
|
23
|
+
private events;
|
|
24
|
+
private session;
|
|
25
|
+
private bound;
|
|
26
|
+
private lastPressTime;
|
|
27
|
+
private handleReady;
|
|
28
|
+
private handleResult;
|
|
29
|
+
private handleError;
|
|
30
|
+
constructor(config: IflytekClientConfig);
|
|
31
|
+
/** 注册事件回调 */
|
|
32
|
+
on(events: IflytekClientEvents): this;
|
|
33
|
+
/** 当前阶段 */
|
|
34
|
+
get phase(): IflytekPhase;
|
|
35
|
+
/** 当前会话 ID */
|
|
36
|
+
get sessionId(): string | null;
|
|
37
|
+
/** 是否正在录音(connecting / recording / stopping) */
|
|
38
|
+
get isActive(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* 开始录音。
|
|
41
|
+
* 对应 onPressIn / 按钮按下。
|
|
42
|
+
* 返回 true 表示成功启动,false 表示被忽略(去抖 / 已有活跃会话)。
|
|
43
|
+
*/
|
|
44
|
+
start(): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* 停止录音。
|
|
47
|
+
* 对应 onPressOut / 按钮松开。
|
|
48
|
+
*/
|
|
49
|
+
stop(): void;
|
|
50
|
+
/** 强制终止当前会话(页面卸载 / 引擎切换时调用) */
|
|
51
|
+
abort(): void;
|
|
52
|
+
/** 完全释放资源,解除 transport 监听 */
|
|
53
|
+
dispose(): void;
|
|
54
|
+
private _onReady;
|
|
55
|
+
private _onResult;
|
|
56
|
+
private _onError;
|
|
57
|
+
private _onAudioData;
|
|
58
|
+
private sendFinal;
|
|
59
|
+
private destroy;
|
|
60
|
+
private setPhase;
|
|
61
|
+
private bindTransport;
|
|
62
|
+
private unbindTransport;
|
|
63
|
+
private log;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 基于 Web Audio API 的 AudioRecorder 实现
|
|
68
|
+
*
|
|
69
|
+
* 适用于浏览器环境(Next.js 客户端 / Electron 渲染进程)。
|
|
70
|
+
* 输出与 React Native PCM 库一致的 base64 编码 16-bit PCM 数据。
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* import { IflytekSTT, WebAudioRecorder } from "sa2kit/iflytek";
|
|
75
|
+
*
|
|
76
|
+
* const recorder = new WebAudioRecorder();
|
|
77
|
+
* const stt = new IflytekSTT({ transport: socket, recorder });
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
|
|
81
|
+
declare class WebAudioRecorder implements AudioRecorder {
|
|
82
|
+
private stream;
|
|
83
|
+
private audioCtx;
|
|
84
|
+
private sourceNode;
|
|
85
|
+
private workletNode;
|
|
86
|
+
private processorNode;
|
|
87
|
+
private callback;
|
|
88
|
+
private sampleRate;
|
|
89
|
+
private bufferSize;
|
|
90
|
+
private running;
|
|
91
|
+
init(options: {
|
|
92
|
+
sampleRate: number;
|
|
93
|
+
channels: number;
|
|
94
|
+
bitsPerSample: number;
|
|
95
|
+
audioSource: number;
|
|
96
|
+
bufferSize: number;
|
|
97
|
+
}): void;
|
|
98
|
+
on(event: "data", callback: (base64Chunk: string) => void): {
|
|
99
|
+
remove: () => void;
|
|
100
|
+
};
|
|
101
|
+
start(): void;
|
|
102
|
+
stop(): void;
|
|
103
|
+
private startAsync;
|
|
104
|
+
private cleanup;
|
|
105
|
+
private float32ToBase64PCM16;
|
|
106
|
+
private closestPow2;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { AudioRecorder, IflytekClientConfig, IflytekClientEvents, IflytekPhase, IflytekSTT, WebAudioRecorder };
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
require('../chunk-Z6ZWNWWR.js');
|
|
4
|
+
|
|
5
|
+
// src/iflytek/IflytekSTT.ts
|
|
6
|
+
var IflytekSTT = class {
|
|
7
|
+
constructor(config) {
|
|
8
|
+
this.events = {};
|
|
9
|
+
this.session = null;
|
|
10
|
+
this.bound = false;
|
|
11
|
+
this.lastPressTime = 0;
|
|
12
|
+
this.transport = config.transport;
|
|
13
|
+
this.recorder = config.recorder;
|
|
14
|
+
this.sampleRate = config.sampleRate ?? 16e3;
|
|
15
|
+
this.language = config.language ?? "zh_cn";
|
|
16
|
+
this.domain = config.domain ?? "iat";
|
|
17
|
+
this.accent = config.accent ?? "mandarin";
|
|
18
|
+
this.readyTimeout = config.readyTimeout ?? 5e3;
|
|
19
|
+
this.stopWaitTimeout = config.stopWaitTimeout ?? 1500;
|
|
20
|
+
this.debug = config.debug ?? false;
|
|
21
|
+
this.handleReady = this._onReady.bind(this);
|
|
22
|
+
this.handleResult = this._onResult.bind(this);
|
|
23
|
+
this.handleError = this._onError.bind(this);
|
|
24
|
+
}
|
|
25
|
+
// ─── 公共 API ───
|
|
26
|
+
/** 注册事件回调 */
|
|
27
|
+
on(events) {
|
|
28
|
+
this.events = { ...this.events, ...events };
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
/** 当前阶段 */
|
|
32
|
+
get phase() {
|
|
33
|
+
return this.session?.phase ?? "idle";
|
|
34
|
+
}
|
|
35
|
+
/** 当前会话 ID */
|
|
36
|
+
get sessionId() {
|
|
37
|
+
return this.session?.id ?? null;
|
|
38
|
+
}
|
|
39
|
+
/** 是否正在录音(connecting / recording / stopping) */
|
|
40
|
+
get isActive() {
|
|
41
|
+
return this.session !== null;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* 开始录音。
|
|
45
|
+
* 对应 onPressIn / 按钮按下。
|
|
46
|
+
* 返回 true 表示成功启动,false 表示被忽略(去抖 / 已有活跃会话)。
|
|
47
|
+
*/
|
|
48
|
+
start() {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (now - this.lastPressTime < 200) return false;
|
|
51
|
+
this.lastPressTime = now;
|
|
52
|
+
if (this.session) return false;
|
|
53
|
+
this.bindTransport();
|
|
54
|
+
const sid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
55
|
+
const session = {
|
|
56
|
+
id: sid,
|
|
57
|
+
phase: "connecting",
|
|
58
|
+
ready: false,
|
|
59
|
+
hasAudio: false,
|
|
60
|
+
finalSent: false,
|
|
61
|
+
frameStatus: 0,
|
|
62
|
+
bufferedChunk: null,
|
|
63
|
+
dataListener: null,
|
|
64
|
+
readyTimer: null,
|
|
65
|
+
stopWaitTimer: null
|
|
66
|
+
};
|
|
67
|
+
this.session = session;
|
|
68
|
+
this.setPhase("connecting", sid);
|
|
69
|
+
this.recorder.init({
|
|
70
|
+
sampleRate: this.sampleRate,
|
|
71
|
+
channels: 1,
|
|
72
|
+
bitsPerSample: 16,
|
|
73
|
+
audioSource: 1,
|
|
74
|
+
bufferSize: 4096
|
|
75
|
+
});
|
|
76
|
+
session.dataListener = this.recorder.on("data", (chunk) => {
|
|
77
|
+
this._onAudioData(chunk, sid);
|
|
78
|
+
});
|
|
79
|
+
this.log("start", { sid });
|
|
80
|
+
this.recorder.start();
|
|
81
|
+
this.transport.emit("iflytek:start", { sessionId: sid });
|
|
82
|
+
session.readyTimer = setTimeout(() => {
|
|
83
|
+
if (this.session?.id === sid && this.session.phase === "connecting") {
|
|
84
|
+
this.log("ready timeout");
|
|
85
|
+
this.transport.emit("iflytek:stop", { sessionId: sid });
|
|
86
|
+
this.destroy();
|
|
87
|
+
this.events.onError?.("\u8BAF\u98DE\u670D\u52A1\u672A\u5C31\u7EEA\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5", sid);
|
|
88
|
+
}
|
|
89
|
+
}, this.readyTimeout);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 停止录音。
|
|
94
|
+
* 对应 onPressOut / 按钮松开。
|
|
95
|
+
*/
|
|
96
|
+
stop() {
|
|
97
|
+
const s = this.session;
|
|
98
|
+
if (!s) return;
|
|
99
|
+
if (s.hasAudio) {
|
|
100
|
+
this.log("stop");
|
|
101
|
+
this.sendFinal(s);
|
|
102
|
+
} else {
|
|
103
|
+
this.log("wait for first frame");
|
|
104
|
+
s.stopWaitTimer = setTimeout(() => {
|
|
105
|
+
if (this.session?.id === s.id && !this.session.hasAudio) {
|
|
106
|
+
this.log("no audio captured");
|
|
107
|
+
this.transport.emit("iflytek:stop", { sessionId: s.id });
|
|
108
|
+
this.destroy();
|
|
109
|
+
this.events.onError?.("\u5F55\u97F3\u592A\u77ED\uFF0C\u8BF7\u7A0D\u5FAE\u591A\u6309\u4F4F\u4E00\u70B9\u518D\u8BF4\u8BDD", s.id);
|
|
110
|
+
}
|
|
111
|
+
}, this.stopWaitTimeout);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** 强制终止当前会话(页面卸载 / 引擎切换时调用) */
|
|
115
|
+
abort() {
|
|
116
|
+
const sid = this.session?.id;
|
|
117
|
+
if (sid) {
|
|
118
|
+
this.transport.emit("iflytek:stop", { sessionId: sid });
|
|
119
|
+
}
|
|
120
|
+
this.destroy();
|
|
121
|
+
}
|
|
122
|
+
/** 完全释放资源,解除 transport 监听 */
|
|
123
|
+
dispose() {
|
|
124
|
+
this.abort();
|
|
125
|
+
this.unbindTransport();
|
|
126
|
+
}
|
|
127
|
+
// ─── transport 事件处理 ───
|
|
128
|
+
_onReady(data) {
|
|
129
|
+
const s = this.session;
|
|
130
|
+
if (!s || s.phase !== "connecting") return;
|
|
131
|
+
if (data.sessionId && data.sessionId !== s.id) return;
|
|
132
|
+
this.log("ready");
|
|
133
|
+
if (s.readyTimer) {
|
|
134
|
+
clearTimeout(s.readyTimer);
|
|
135
|
+
s.readyTimer = null;
|
|
136
|
+
}
|
|
137
|
+
s.ready = true;
|
|
138
|
+
this.setPhase("recording", s.id);
|
|
139
|
+
const buf = s.bufferedChunk;
|
|
140
|
+
if (buf) {
|
|
141
|
+
this.log("flush buffered frame", { len: buf.length });
|
|
142
|
+
this.transport.emit("iflytek:audio", {
|
|
143
|
+
sessionId: s.id,
|
|
144
|
+
status: 0,
|
|
145
|
+
audio: buf,
|
|
146
|
+
language: this.language,
|
|
147
|
+
domain: this.domain,
|
|
148
|
+
accent: this.accent
|
|
149
|
+
});
|
|
150
|
+
s.hasAudio = true;
|
|
151
|
+
s.frameStatus = 1;
|
|
152
|
+
s.bufferedChunk = null;
|
|
153
|
+
}
|
|
154
|
+
if (s.stopWaitTimer && s.hasAudio) {
|
|
155
|
+
clearTimeout(s.stopWaitTimer);
|
|
156
|
+
s.stopWaitTimer = null;
|
|
157
|
+
this.sendFinal(s);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
_onResult(data) {
|
|
161
|
+
const s = this.session;
|
|
162
|
+
if (s && data.sessionId && data.sessionId !== s.id) return;
|
|
163
|
+
const text = data.text?.trim() ?? "";
|
|
164
|
+
const isFinal = Boolean(data.isFinal);
|
|
165
|
+
this.log("result", { text: text.slice(0, 30), isFinal });
|
|
166
|
+
if (isFinal) {
|
|
167
|
+
const sid = s?.id ?? data.sessionId;
|
|
168
|
+
this.destroy();
|
|
169
|
+
if (text) {
|
|
170
|
+
this.events.onFinalResult?.(text, sid);
|
|
171
|
+
}
|
|
172
|
+
} else if (text) {
|
|
173
|
+
this.events.onInterimResult?.(text, data.sessionId);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
_onError(data) {
|
|
177
|
+
const s = this.session;
|
|
178
|
+
if (s && data.sessionId && data.sessionId !== s.id) return;
|
|
179
|
+
this.log("error", { message: data.message, sessionId: data.sessionId });
|
|
180
|
+
const sid = s?.id ?? data.sessionId;
|
|
181
|
+
this.destroy();
|
|
182
|
+
this.events.onError?.(data.message || "\u8BAF\u98DE\u8BC6\u522B\u5931\u8D25", sid);
|
|
183
|
+
}
|
|
184
|
+
// ─── 音频数据 ───
|
|
185
|
+
_onAudioData(chunk, sid) {
|
|
186
|
+
const s = this.session;
|
|
187
|
+
if (!s || s.id !== sid) return;
|
|
188
|
+
if (!s.ready) {
|
|
189
|
+
s.bufferedChunk = chunk;
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
this.log("frame", { s: s.frameStatus, len: chunk.length });
|
|
193
|
+
this.transport.emit("iflytek:audio", {
|
|
194
|
+
sessionId: sid,
|
|
195
|
+
status: s.frameStatus,
|
|
196
|
+
audio: chunk,
|
|
197
|
+
language: this.language,
|
|
198
|
+
domain: this.domain,
|
|
199
|
+
accent: this.accent
|
|
200
|
+
});
|
|
201
|
+
s.hasAudio = true;
|
|
202
|
+
s.frameStatus = 1;
|
|
203
|
+
if (s.stopWaitTimer && s.hasAudio) {
|
|
204
|
+
clearTimeout(s.stopWaitTimer);
|
|
205
|
+
s.stopWaitTimer = null;
|
|
206
|
+
this.sendFinal(s);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ─── 内部工具 ───
|
|
210
|
+
sendFinal(s) {
|
|
211
|
+
if (s.finalSent) return;
|
|
212
|
+
s.finalSent = true;
|
|
213
|
+
this.setPhase("stopping", s.id);
|
|
214
|
+
s.dataListener?.remove();
|
|
215
|
+
s.dataListener = null;
|
|
216
|
+
try {
|
|
217
|
+
this.recorder.stop();
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
this.log("send final frame");
|
|
221
|
+
this.transport.emit("iflytek:audio", { sessionId: s.id, status: 2 });
|
|
222
|
+
}
|
|
223
|
+
destroy() {
|
|
224
|
+
const s = this.session;
|
|
225
|
+
if (!s) return;
|
|
226
|
+
s.dataListener?.remove();
|
|
227
|
+
if (s.readyTimer) clearTimeout(s.readyTimer);
|
|
228
|
+
if (s.stopWaitTimer) clearTimeout(s.stopWaitTimer);
|
|
229
|
+
try {
|
|
230
|
+
this.recorder.stop();
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
this.session = null;
|
|
234
|
+
this.setPhase("idle", null);
|
|
235
|
+
}
|
|
236
|
+
setPhase(phase, sid) {
|
|
237
|
+
this.events.onPhaseChange?.(phase, sid);
|
|
238
|
+
}
|
|
239
|
+
bindTransport() {
|
|
240
|
+
if (this.bound) return;
|
|
241
|
+
this.transport.on("iflytek:ready", this.handleReady);
|
|
242
|
+
this.transport.on("iflytek:result", this.handleResult);
|
|
243
|
+
this.transport.on("iflytek:error", this.handleError);
|
|
244
|
+
this.bound = true;
|
|
245
|
+
}
|
|
246
|
+
unbindTransport() {
|
|
247
|
+
if (!this.bound) return;
|
|
248
|
+
this.transport.off("iflytek:ready", this.handleReady);
|
|
249
|
+
this.transport.off("iflytek:result", this.handleResult);
|
|
250
|
+
this.transport.off("iflytek:error", this.handleError);
|
|
251
|
+
this.bound = false;
|
|
252
|
+
}
|
|
253
|
+
log(msg, extra) {
|
|
254
|
+
if (!this.debug) return;
|
|
255
|
+
if (extra) {
|
|
256
|
+
console.log(`[sa2kit/iflytek] ${msg}`, extra);
|
|
257
|
+
} else {
|
|
258
|
+
console.log(`[sa2kit/iflytek] ${msg}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// src/iflytek/WebAudioRecorder.ts
|
|
264
|
+
var WebAudioRecorder = class {
|
|
265
|
+
constructor() {
|
|
266
|
+
this.stream = null;
|
|
267
|
+
this.audioCtx = null;
|
|
268
|
+
this.sourceNode = null;
|
|
269
|
+
this.workletNode = null;
|
|
270
|
+
this.processorNode = null;
|
|
271
|
+
this.callback = null;
|
|
272
|
+
this.sampleRate = 16e3;
|
|
273
|
+
this.bufferSize = 4096;
|
|
274
|
+
this.running = false;
|
|
275
|
+
}
|
|
276
|
+
init(options) {
|
|
277
|
+
this.sampleRate = options.sampleRate;
|
|
278
|
+
this.bufferSize = options.bufferSize;
|
|
279
|
+
this.cleanup();
|
|
280
|
+
}
|
|
281
|
+
on(event, callback) {
|
|
282
|
+
this.callback = callback;
|
|
283
|
+
return {
|
|
284
|
+
remove: () => {
|
|
285
|
+
if (this.callback === callback) {
|
|
286
|
+
this.callback = null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
start() {
|
|
292
|
+
if (this.running) return;
|
|
293
|
+
this.running = true;
|
|
294
|
+
this.startAsync().catch((err) => {
|
|
295
|
+
console.error("[WebAudioRecorder] start failed:", err);
|
|
296
|
+
this.running = false;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
stop() {
|
|
300
|
+
this.running = false;
|
|
301
|
+
this.cleanup();
|
|
302
|
+
}
|
|
303
|
+
async startAsync() {
|
|
304
|
+
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
305
|
+
audio: {
|
|
306
|
+
sampleRate: { ideal: this.sampleRate },
|
|
307
|
+
channelCount: 1,
|
|
308
|
+
echoCancellation: false,
|
|
309
|
+
noiseSuppression: false,
|
|
310
|
+
autoGainControl: false
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
this.audioCtx = new AudioContext({ sampleRate: this.sampleRate });
|
|
314
|
+
this.sourceNode = this.audioCtx.createMediaStreamSource(this.stream);
|
|
315
|
+
const bufSize = this.closestPow2(this.bufferSize);
|
|
316
|
+
this.processorNode = this.audioCtx.createScriptProcessor(bufSize, 1, 1);
|
|
317
|
+
this.processorNode.onaudioprocess = (e) => {
|
|
318
|
+
if (!this.running || !this.callback) return;
|
|
319
|
+
const float32 = e.inputBuffer.getChannelData(0);
|
|
320
|
+
const base64 = this.float32ToBase64PCM16(float32);
|
|
321
|
+
this.callback(base64);
|
|
322
|
+
};
|
|
323
|
+
this.sourceNode.connect(this.processorNode);
|
|
324
|
+
this.processorNode.connect(this.audioCtx.destination);
|
|
325
|
+
}
|
|
326
|
+
cleanup() {
|
|
327
|
+
this.processorNode?.disconnect();
|
|
328
|
+
this.processorNode = null;
|
|
329
|
+
this.workletNode?.disconnect();
|
|
330
|
+
this.workletNode = null;
|
|
331
|
+
this.sourceNode?.disconnect();
|
|
332
|
+
this.sourceNode = null;
|
|
333
|
+
if (this.audioCtx && this.audioCtx.state !== "closed") {
|
|
334
|
+
this.audioCtx.close().catch(() => {
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
this.audioCtx = null;
|
|
338
|
+
this.stream?.getTracks().forEach((t) => t.stop());
|
|
339
|
+
this.stream = null;
|
|
340
|
+
}
|
|
341
|
+
float32ToBase64PCM16(float32) {
|
|
342
|
+
const buf = new ArrayBuffer(float32.length * 2);
|
|
343
|
+
const view = new DataView(buf);
|
|
344
|
+
for (let i = 0; i < float32.length; i++) {
|
|
345
|
+
const s = Math.max(-1, Math.min(1, float32[i]));
|
|
346
|
+
view.setInt16(i * 2, s < 0 ? s * 32768 : s * 32767, true);
|
|
347
|
+
}
|
|
348
|
+
const bytes = new Uint8Array(buf);
|
|
349
|
+
let binary = "";
|
|
350
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
351
|
+
binary += String.fromCharCode(bytes[i]);
|
|
352
|
+
}
|
|
353
|
+
return typeof btoa === "function" ? btoa(binary) : Buffer.from(buf).toString("base64");
|
|
354
|
+
}
|
|
355
|
+
closestPow2(n) {
|
|
356
|
+
let v = 256;
|
|
357
|
+
while (v < n && v < 16384) v *= 2;
|
|
358
|
+
return v;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
exports.IflytekSTT = IflytekSTT;
|
|
363
|
+
exports.WebAudioRecorder = WebAudioRecorder;
|
|
364
|
+
//# sourceMappingURL=index.js.map
|
|
365
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/iflytek/IflytekSTT.ts","../../src/iflytek/WebAudioRecorder.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * 讯飞语音转文字 — 客户端核心类\n *\n * 纯逻辑、无 UI 依赖。管理:\n * 本地 PCM 录音 → 通过 transport(Socket.IO)传输到服务端适配层 → 接收转写结果\n *\n * 状态机:idle → connecting → recording → stopping → idle\n */\n\nimport type {\n IflytekPhase,\n IflytekClientConfig,\n IflytekClientEvents,\n IflytekTransport,\n AudioRecorder,\n IflytekReadyPayload,\n IflytekResultPayload,\n IflytekErrorPayload,\n} from \"./types\";\n\ninterface Session {\n id: string;\n phase: IflytekPhase;\n ready: boolean;\n hasAudio: boolean;\n finalSent: boolean;\n frameStatus: 0 | 1;\n bufferedChunk: string | null;\n dataListener: { remove: () => void } | null;\n readyTimer: ReturnType<typeof setTimeout> | null;\n stopWaitTimer: ReturnType<typeof setTimeout> | null;\n}\n\nexport class IflytekSTT {\n private transport: IflytekTransport;\n private recorder: AudioRecorder;\n private sampleRate: number;\n private language: string;\n private domain: string;\n private accent: string;\n private readyTimeout: number;\n private stopWaitTimeout: number;\n private debug: boolean;\n\n private events: IflytekClientEvents = {};\n private session: Session | null = null;\n private bound = false;\n private lastPressTime = 0;\n\n // 保存绑定后的 handler 引用以便 off\n private handleReady: (data: IflytekReadyPayload) => void;\n private handleResult: (data: IflytekResultPayload) => void;\n private handleError: (data: IflytekErrorPayload) => void;\n\n constructor(config: IflytekClientConfig) {\n this.transport = config.transport;\n this.recorder = config.recorder;\n this.sampleRate = config.sampleRate ?? 16000;\n this.language = config.language ?? \"zh_cn\";\n this.domain = config.domain ?? \"iat\";\n this.accent = config.accent ?? \"mandarin\";\n this.readyTimeout = config.readyTimeout ?? 5000;\n this.stopWaitTimeout = config.stopWaitTimeout ?? 1500;\n this.debug = config.debug ?? false;\n\n this.handleReady = this._onReady.bind(this);\n this.handleResult = this._onResult.bind(this);\n this.handleError = this._onError.bind(this);\n }\n\n // ─── 公共 API ───\n\n /** 注册事件回调 */\n on(events: IflytekClientEvents): this {\n this.events = { ...this.events, ...events };\n return this;\n }\n\n /** 当前阶段 */\n get phase(): IflytekPhase {\n return this.session?.phase ?? \"idle\";\n }\n\n /** 当前会话 ID */\n get sessionId(): string | null {\n return this.session?.id ?? null;\n }\n\n /** 是否正在录音(connecting / recording / stopping) */\n get isActive(): boolean {\n return this.session !== null;\n }\n\n /**\n * 开始录音。\n * 对应 onPressIn / 按钮按下。\n * 返回 true 表示成功启动,false 表示被忽略(去抖 / 已有活跃会话)。\n */\n start(): boolean {\n const now = Date.now();\n if (now - this.lastPressTime < 200) return false;\n this.lastPressTime = now;\n if (this.session) return false;\n\n this.bindTransport();\n\n const sid = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;\n const session: Session = {\n id: sid,\n phase: \"connecting\",\n ready: false,\n hasAudio: false,\n finalSent: false,\n frameStatus: 0,\n bufferedChunk: null,\n dataListener: null,\n readyTimer: null,\n stopWaitTimer: null,\n };\n this.session = session;\n this.setPhase(\"connecting\", sid);\n\n this.recorder.init({\n sampleRate: this.sampleRate,\n channels: 1,\n bitsPerSample: 16,\n audioSource: 1,\n bufferSize: 4096,\n });\n\n session.dataListener = this.recorder.on(\"data\", (chunk: string) => {\n this._onAudioData(chunk, sid);\n });\n\n this.log(\"start\", { sid });\n this.recorder.start();\n\n this.transport.emit(\"iflytek:start\", { sessionId: sid });\n\n session.readyTimer = setTimeout(() => {\n if (this.session?.id === sid && this.session.phase === \"connecting\") {\n this.log(\"ready timeout\");\n this.transport.emit(\"iflytek:stop\", { sessionId: sid });\n this.destroy();\n this.events.onError?.(\"讯飞服务未就绪,请检查网络后重试\", sid);\n }\n }, this.readyTimeout);\n\n return true;\n }\n\n /**\n * 停止录音。\n * 对应 onPressOut / 按钮松开。\n */\n stop(): void {\n const s = this.session;\n if (!s) return;\n\n if (s.hasAudio) {\n this.log(\"stop\");\n this.sendFinal(s);\n } else {\n this.log(\"wait for first frame\");\n s.stopWaitTimer = setTimeout(() => {\n if (this.session?.id === s.id && !this.session.hasAudio) {\n this.log(\"no audio captured\");\n this.transport.emit(\"iflytek:stop\", { sessionId: s.id });\n this.destroy();\n this.events.onError?.(\"录音太短,请稍微多按住一点再说话\", s.id);\n }\n }, this.stopWaitTimeout);\n }\n }\n\n /** 强制终止当前会话(页面卸载 / 引擎切换时调用) */\n abort(): void {\n const sid = this.session?.id;\n if (sid) {\n this.transport.emit(\"iflytek:stop\", { sessionId: sid });\n }\n this.destroy();\n }\n\n /** 完全释放资源,解除 transport 监听 */\n dispose(): void {\n this.abort();\n this.unbindTransport();\n }\n\n // ─── transport 事件处理 ───\n\n private _onReady(data: IflytekReadyPayload) {\n const s = this.session;\n if (!s || s.phase !== \"connecting\") return;\n if (data.sessionId && data.sessionId !== s.id) return;\n\n this.log(\"ready\");\n if (s.readyTimer) {\n clearTimeout(s.readyTimer);\n s.readyTimer = null;\n }\n s.ready = true;\n this.setPhase(\"recording\", s.id);\n\n const buf = s.bufferedChunk;\n if (buf) {\n this.log(\"flush buffered frame\", { len: buf.length });\n this.transport.emit(\"iflytek:audio\", {\n sessionId: s.id,\n status: 0,\n audio: buf,\n language: this.language,\n domain: this.domain,\n accent: this.accent,\n });\n s.hasAudio = true;\n s.frameStatus = 1;\n s.bufferedChunk = null;\n }\n\n if (s.stopWaitTimer && s.hasAudio) {\n clearTimeout(s.stopWaitTimer);\n s.stopWaitTimer = null;\n this.sendFinal(s);\n }\n }\n\n private _onResult(data: IflytekResultPayload) {\n const s = this.session;\n if (s && data.sessionId && data.sessionId !== s.id) return;\n\n const text = data.text?.trim() ?? \"\";\n const isFinal = Boolean(data.isFinal);\n this.log(\"result\", { text: text.slice(0, 30), isFinal });\n\n if (isFinal) {\n const sid = s?.id ?? data.sessionId;\n this.destroy();\n if (text) {\n this.events.onFinalResult?.(text, sid);\n }\n } else if (text) {\n this.events.onInterimResult?.(text, data.sessionId);\n }\n }\n\n private _onError(data: IflytekErrorPayload) {\n const s = this.session;\n if (s && data.sessionId && data.sessionId !== s.id) return;\n this.log(\"error\", { message: data.message, sessionId: data.sessionId });\n const sid = s?.id ?? data.sessionId;\n this.destroy();\n this.events.onError?.(data.message || \"讯飞识别失败\", sid);\n }\n\n // ─── 音频数据 ───\n\n private _onAudioData(chunk: string, sid: string) {\n const s = this.session;\n if (!s || s.id !== sid) return;\n\n if (!s.ready) {\n s.bufferedChunk = chunk;\n return;\n }\n\n this.log(\"frame\", { s: s.frameStatus, len: chunk.length });\n this.transport.emit(\"iflytek:audio\", {\n sessionId: sid,\n status: s.frameStatus,\n audio: chunk,\n language: this.language,\n domain: this.domain,\n accent: this.accent,\n });\n s.hasAudio = true;\n s.frameStatus = 1;\n\n if (s.stopWaitTimer && s.hasAudio) {\n clearTimeout(s.stopWaitTimer);\n s.stopWaitTimer = null;\n this.sendFinal(s);\n }\n }\n\n // ─── 内部工具 ───\n\n private sendFinal(s: Session) {\n if (s.finalSent) return;\n s.finalSent = true;\n this.setPhase(\"stopping\", s.id);\n s.dataListener?.remove();\n s.dataListener = null;\n try {\n this.recorder.stop();\n } catch {}\n this.log(\"send final frame\");\n this.transport.emit(\"iflytek:audio\", { sessionId: s.id, status: 2 });\n }\n\n private destroy() {\n const s = this.session;\n if (!s) return;\n s.dataListener?.remove();\n if (s.readyTimer) clearTimeout(s.readyTimer);\n if (s.stopWaitTimer) clearTimeout(s.stopWaitTimer);\n try {\n this.recorder.stop();\n } catch {}\n this.session = null;\n this.setPhase(\"idle\", null);\n }\n\n private setPhase(phase: IflytekPhase, sid: string | null) {\n this.events.onPhaseChange?.(phase, sid);\n }\n\n private bindTransport() {\n if (this.bound) return;\n this.transport.on(\"iflytek:ready\", this.handleReady);\n this.transport.on(\"iflytek:result\", this.handleResult);\n this.transport.on(\"iflytek:error\", this.handleError);\n this.bound = true;\n }\n\n private unbindTransport() {\n if (!this.bound) return;\n this.transport.off(\"iflytek:ready\", this.handleReady);\n this.transport.off(\"iflytek:result\", this.handleResult);\n this.transport.off(\"iflytek:error\", this.handleError);\n this.bound = false;\n }\n\n private log(msg: string, extra?: Record<string, unknown>) {\n if (!this.debug) return;\n if (extra) {\n console.log(`[sa2kit/iflytek] ${msg}`, extra);\n } else {\n console.log(`[sa2kit/iflytek] ${msg}`);\n }\n }\n}\n","/**\n * 基于 Web Audio API 的 AudioRecorder 实现\n *\n * 适用于浏览器环境(Next.js 客户端 / Electron 渲染进程)。\n * 输出与 React Native PCM 库一致的 base64 编码 16-bit PCM 数据。\n *\n * @example\n * ```ts\n * import { IflytekSTT, WebAudioRecorder } from \"sa2kit/iflytek\";\n *\n * const recorder = new WebAudioRecorder();\n * const stt = new IflytekSTT({ transport: socket, recorder });\n * ```\n */\n\nimport type { AudioRecorder } from \"./types\";\n\nexport class WebAudioRecorder implements AudioRecorder {\n private stream: MediaStream | null = null;\n private audioCtx: AudioContext | null = null;\n private sourceNode: MediaStreamAudioSourceNode | null = null;\n private workletNode: AudioWorkletNode | null = null;\n private processorNode: ScriptProcessorNode | null = null;\n private callback: ((base64Chunk: string) => void) | null = null;\n private sampleRate = 16000;\n private bufferSize = 4096;\n private running = false;\n\n init(options: {\n sampleRate: number;\n channels: number;\n bitsPerSample: number;\n audioSource: number;\n bufferSize: number;\n }): void {\n this.sampleRate = options.sampleRate;\n this.bufferSize = options.bufferSize;\n this.cleanup();\n }\n\n on(\n event: \"data\",\n callback: (base64Chunk: string) => void,\n ): { remove: () => void } {\n this.callback = callback;\n return {\n remove: () => {\n if (this.callback === callback) {\n this.callback = null;\n }\n },\n };\n }\n\n start(): void {\n if (this.running) return;\n this.running = true;\n this.startAsync().catch((err) => {\n console.error(\"[WebAudioRecorder] start failed:\", err);\n this.running = false;\n });\n }\n\n stop(): void {\n this.running = false;\n this.cleanup();\n }\n\n private async startAsync(): Promise<void> {\n this.stream = await navigator.mediaDevices.getUserMedia({\n audio: {\n sampleRate: { ideal: this.sampleRate },\n channelCount: 1,\n echoCancellation: false,\n noiseSuppression: false,\n autoGainControl: false,\n },\n });\n\n this.audioCtx = new AudioContext({ sampleRate: this.sampleRate });\n this.sourceNode = this.audioCtx.createMediaStreamSource(this.stream);\n\n // 优先尝试 ScriptProcessorNode(兼容性最广,简单可靠)\n const bufSize = this.closestPow2(this.bufferSize);\n this.processorNode = this.audioCtx.createScriptProcessor(bufSize, 1, 1);\n\n this.processorNode.onaudioprocess = (e) => {\n if (!this.running || !this.callback) return;\n const float32 = e.inputBuffer.getChannelData(0);\n const base64 = this.float32ToBase64PCM16(float32);\n this.callback(base64);\n };\n\n this.sourceNode.connect(this.processorNode);\n this.processorNode.connect(this.audioCtx.destination);\n }\n\n private cleanup(): void {\n this.processorNode?.disconnect();\n this.processorNode = null;\n this.workletNode?.disconnect();\n this.workletNode = null;\n this.sourceNode?.disconnect();\n this.sourceNode = null;\n if (this.audioCtx && this.audioCtx.state !== \"closed\") {\n this.audioCtx.close().catch(() => {});\n }\n this.audioCtx = null;\n this.stream?.getTracks().forEach((t) => t.stop());\n this.stream = null;\n }\n\n private float32ToBase64PCM16(float32: Float32Array): string {\n const buf = new ArrayBuffer(float32.length * 2);\n const view = new DataView(buf);\n for (let i = 0; i < float32.length; i++) {\n const s = Math.max(-1, Math.min(1, float32[i]!));\n view.setInt16(i * 2, s < 0 ? s * 0x8000 : s * 0x7fff, true);\n }\n const bytes = new Uint8Array(buf);\n let binary = \"\";\n for (let i = 0; i < bytes.length; i++) {\n binary += String.fromCharCode(bytes[i]!);\n }\n return typeof btoa === \"function\"\n ? btoa(binary)\n : Buffer.from(buf).toString(\"base64\");\n }\n\n private closestPow2(n: number): number {\n let v = 256;\n while (v < n && v < 16384) v *= 2;\n return v;\n }\n}\n"]}
|