@hoanghong/speaking-core 0.4.0 → 0.4.2
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/CHANGELOG.md +10 -0
- package/README.md +15 -1
- package/dist/microphone.d.ts +6 -0
- package/dist/microphone.js +7 -1
- package/dist/public-service.d.ts +1 -0
- package/dist/public-service.js +15 -4
- package/dist/realtime.d.ts +1 -0
- package/dist/realtime.js +18 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
## 0.4.2
|
|
2
|
+
|
|
3
|
+
- Simplify public integration documentation; no runtime changes.
|
|
4
|
+
|
|
5
|
+
# 0.4.1
|
|
6
|
+
|
|
7
|
+
- Application keys support quota updates, rotation and revocation.
|
|
8
|
+
- Optional public-service baseUrl for private deployments and local integration.
|
|
9
|
+
- Includes microphone selection and realtime audio backpressure fixes (web SDK).
|
|
10
|
+
|
|
1
11
|
## 0.4.0
|
|
2
12
|
|
|
3
13
|
- Add US English pronunciation assessment client: 120-second audio, reference passage, optional IPA tokens and uncalibrated phoneme diagnostics.
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @hoanghong/speaking-core
|
|
2
2
|
|
|
3
|
-
SDK online cho người tự làm UI. Bản test 0.4.
|
|
3
|
+
SDK online cho người tự làm UI. Bản test 0.4.2 cài bằng `npm install ./vendor/hoanghong-speaking-core-0.4.2.tgz` từ bộ ZIP, chưa publish registry. Angular dùng @hoanghong/speaking-angular với component có sẵn; Flutter dùng fsel_speaking. MIT.
|
|
4
4
|
|
|
5
5
|
## Đánh giá phát âm tiếng Anh Mỹ
|
|
6
6
|
|
|
@@ -76,3 +76,17 @@ createSpeakingGateway và các constructor cấu hình thủ công cũ vẫn đ
|
|
|
76
76
|
## Key ứng dụng
|
|
77
77
|
|
|
78
78
|
Dùng `createSpeakingService({ appKey: "KEY_DUOC_FSEL_CAP" })` thay cho lời gọi không tham số. SDK gửi key khi cấp phiên; quota quản lý phía server, không có URL cần cấu hình. Key này là định danh ứng dụng có thể quan sát ở frontend, không có quyền quản trị. INVALID_APP_KEY/401 nghĩa là sai hoặc đã bị khóa; không tự fallback ẩn danh.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
## Application key and service URL
|
|
82
|
+
|
|
83
|
+
Pass the application key provided by FSEL as `appKey`. Quotas and key status are
|
|
84
|
+
managed by the service. App keys identify an application and are visible in
|
|
85
|
+
browser/mobile applications.
|
|
86
|
+
|
|
87
|
+
The default service is https://ai-service.fsel.edu.vn/sdk. For a private deployment,
|
|
88
|
+
set `baseUrl` to the absolute /sdk endpoint supplied by your service operator.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
createSpeakingService({ appKey: 'YOUR_APP_KEY', baseUrl: 'https://your-service.example/sdk' });
|
|
92
|
+
```
|
package/dist/microphone.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { RecordedSpeakingFinal, StartOptions } from './contracts.js';
|
|
|
3
3
|
export interface MicrophoneOptions {
|
|
4
4
|
/** Host the packaged pcm-worklet.js with your app's normal CSP; no inline/blob worker. */
|
|
5
5
|
workletUrl: string;
|
|
6
|
+
/** Select the same device as the host application's microphone diagnostic. */
|
|
7
|
+
deviceId?: string;
|
|
8
|
+
/** Disable processing when a driver/browser suppresses speech unexpectedly. */
|
|
9
|
+
echoCancellation?: boolean;
|
|
10
|
+
noiseSuppression?: boolean;
|
|
11
|
+
autoGainControl?: boolean;
|
|
6
12
|
}
|
|
7
13
|
export declare class SpeakingMicrophone {
|
|
8
14
|
readonly client: RealtimeSpeakingClient;
|
package/dist/microphone.js
CHANGED
|
@@ -30,7 +30,13 @@ export class SpeakingMicrophone {
|
|
|
30
30
|
this.recording.clear();
|
|
31
31
|
try {
|
|
32
32
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
33
|
-
audio: {
|
|
33
|
+
audio: {
|
|
34
|
+
channelCount: 1,
|
|
35
|
+
...(this.options.deviceId ? { deviceId: { exact: this.options.deviceId } } : {}),
|
|
36
|
+
echoCancellation: this.options.echoCancellation ?? true,
|
|
37
|
+
noiseSuppression: this.options.noiseSuppression ?? true,
|
|
38
|
+
autoGainControl: this.options.autoGainControl ?? true,
|
|
39
|
+
},
|
|
34
40
|
});
|
|
35
41
|
if (generation !== this.generation) {
|
|
36
42
|
stream.getTracks().forEach(track => track.stop());
|
package/dist/public-service.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/** Hosted public service. The SDK handles short-lived capabilities internally. */
|
|
2
2
|
export declare function createSpeakingService(options?: {
|
|
3
3
|
appKey?: string;
|
|
4
|
+
baseUrl?: string;
|
|
4
5
|
}): {
|
|
5
6
|
batch: import("./batch.js").BatchOptions;
|
|
6
7
|
realtime: import("./realtime.js").RealtimeOptions;
|
package/dist/public-service.js
CHANGED
|
@@ -5,9 +5,20 @@ const SERVICE = 'https://ai-service.fsel.edu.vn/sdk';
|
|
|
5
5
|
export function createSpeakingService(options = {}) {
|
|
6
6
|
if (options.appKey !== undefined && !options.appKey.trim())
|
|
7
7
|
throw new SpeakingError('INVALID_CONFIG', 'Application key cannot be empty');
|
|
8
|
+
let endpoint;
|
|
9
|
+
try {
|
|
10
|
+
endpoint = new URL(options.baseUrl ?? SERVICE);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
throw new SpeakingError('INVALID_CONFIG', 'Use an absolute HTTP(S) service URL');
|
|
14
|
+
}
|
|
15
|
+
if (!['http:', 'https:'].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash) {
|
|
16
|
+
throw new SpeakingError('INVALID_CONFIG', 'Use an HTTP(S) service URL without credentials, query or fragment');
|
|
17
|
+
}
|
|
18
|
+
const service = endpoint.href.replace(/\/$/, '');
|
|
8
19
|
const request = globalThis.fetch.bind(globalThis);
|
|
9
20
|
const session = async () => {
|
|
10
|
-
const response = await request(
|
|
21
|
+
const response = await request(service + '/session', {
|
|
11
22
|
method: 'POST', credentials: 'omit',
|
|
12
23
|
headers: options.appKey ? { 'X-Speaking-Key': options.appKey } : undefined,
|
|
13
24
|
});
|
|
@@ -21,13 +32,13 @@ export function createSpeakingService(options = {}) {
|
|
|
21
32
|
let token;
|
|
22
33
|
let previousKey;
|
|
23
34
|
const config = createSpeakingGateway({
|
|
24
|
-
baseUrl:
|
|
35
|
+
baseUrl: service,
|
|
25
36
|
headers: async () => {
|
|
26
37
|
if (!token)
|
|
27
38
|
throw new SpeakingError('INVALID_STATE', 'Submit audio before reading a public job');
|
|
28
39
|
return { Authorization: 'Bearer ' + token };
|
|
29
40
|
},
|
|
30
|
-
workletUrl:
|
|
41
|
+
workletUrl: service + '/pcm-worklet.js',
|
|
31
42
|
});
|
|
32
43
|
config.batch.beforeSubmit = async (key) => {
|
|
33
44
|
if (!token || key !== previousKey) {
|
|
@@ -35,7 +46,7 @@ export function createSpeakingService(options = {}) {
|
|
|
35
46
|
previousKey = key;
|
|
36
47
|
}
|
|
37
48
|
};
|
|
38
|
-
const realtime = createSpeakingGateway({ baseUrl:
|
|
49
|
+
const realtime = createSpeakingGateway({ baseUrl: service, headers: async () => ({ Authorization: 'Bearer ' + await session() }) });
|
|
39
50
|
config.realtime = realtime.realtime;
|
|
40
51
|
// Every synchronous assessment consumes its own short-lived, single-use session.
|
|
41
52
|
config.pronunciation.headers = async () => ({ Authorization: 'Bearer ' + await session() });
|
package/dist/realtime.d.ts
CHANGED
package/dist/realtime.js
CHANGED
|
@@ -7,6 +7,7 @@ export class RealtimeSpeakingClient {
|
|
|
7
7
|
waiters = new Set();
|
|
8
8
|
queue = Promise.resolve();
|
|
9
9
|
queuedBytes = 0;
|
|
10
|
+
pendingAudio = [];
|
|
10
11
|
seq = 0;
|
|
11
12
|
bytes = 0;
|
|
12
13
|
started;
|
|
@@ -90,6 +91,7 @@ export class RealtimeSpeakingClient {
|
|
|
90
91
|
this.seq = 0;
|
|
91
92
|
this.bytes = 0;
|
|
92
93
|
this.queuedBytes = 0;
|
|
94
|
+
this.pendingAudio = [];
|
|
93
95
|
this.final = undefined;
|
|
94
96
|
this.failure = undefined;
|
|
95
97
|
this.stopPromise = undefined;
|
|
@@ -172,19 +174,31 @@ export class RealtimeSpeakingClient {
|
|
|
172
174
|
if (this.state !== 'recording' || !pcm.length || pcm.length % 2) {
|
|
173
175
|
return Promise.reject(new SpeakingError('INVALID_AUDIO', 'An active session and PCM16 samples are required'));
|
|
174
176
|
}
|
|
175
|
-
if (this.queuedBytes + pcm.length > (this.options.maxQueuedBytes ??
|
|
177
|
+
if (this.queuedBytes + pcm.length > (this.options.maxQueuedBytes ?? 320000)) {
|
|
176
178
|
const error = new SpeakingError('BACKPRESSURE', 'Speaking connection cannot keep up with audio');
|
|
177
179
|
this.fail(error);
|
|
178
180
|
return Promise.reject(error);
|
|
179
181
|
}
|
|
180
|
-
|
|
182
|
+
this.pendingAudio.push(pcm.slice());
|
|
181
183
|
const generation = this.generation;
|
|
182
|
-
this.queuedBytes +=
|
|
184
|
+
this.queuedBytes += pcm.length;
|
|
185
|
+
let consumed = 0;
|
|
183
186
|
const operation = this.queue.then(async () => {
|
|
184
187
|
if (generation !== this.generation)
|
|
185
188
|
throw new SpeakingError('CANCELLED', 'Session changed');
|
|
186
189
|
if (this.failure)
|
|
187
190
|
throw this.failure;
|
|
191
|
+
if (!this.pendingAudio.length)
|
|
192
|
+
return;
|
|
193
|
+
const chunks = this.pendingAudio;
|
|
194
|
+
this.pendingAudio = [];
|
|
195
|
+
consumed = chunks.reduce((size, chunk) => size + chunk.length, 0);
|
|
196
|
+
const owned = new Uint8Array(consumed);
|
|
197
|
+
let at = 0;
|
|
198
|
+
for (const chunk of chunks) {
|
|
199
|
+
owned.set(chunk, at);
|
|
200
|
+
at += chunk.length;
|
|
201
|
+
}
|
|
188
202
|
const max = this.started.max_chunk_bytes;
|
|
189
203
|
for (let offset = 0; offset < owned.length; offset += max) {
|
|
190
204
|
const chunk = owned.subarray(offset, offset + max);
|
|
@@ -215,7 +229,7 @@ export class RealtimeSpeakingClient {
|
|
|
215
229
|
this.fail(error instanceof SpeakingError ? error : new SpeakingError('CONNECTION_LOST', 'Speaking audio failed'));
|
|
216
230
|
throw error;
|
|
217
231
|
}).finally(() => { if (generation === this.generation)
|
|
218
|
-
this.queuedBytes -=
|
|
232
|
+
this.queuedBytes -= consumed; });
|
|
219
233
|
this.queue = operation.catch(() => { });
|
|
220
234
|
return operation;
|
|
221
235
|
}
|