@hoanghong/speaking-core 0.1.0 → 0.2.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/CHANGELOG.md +12 -0
- package/README.md +112 -11
- package/dist/gateway.d.ts +16 -0
- package/dist/gateway.js +35 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +4 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.2.0
|
|
4
|
+
|
|
5
|
+
- Add createSpeakingGateway for shared HTTP/WebSocket/ticket configuration.
|
|
6
|
+
- Validate gateway URLs and malformed ticket responses.
|
|
7
|
+
- Add complete microphone/upload examples and gateway/asset setup instructions.
|
|
8
|
+
- Preserve existing low-level 0.1 APIs.
|
|
9
|
+
|
|
10
|
+
## 0.1.0
|
|
11
|
+
|
|
12
|
+
- Initial batch, realtime and browser microphone SDK.
|
package/README.md
CHANGED
|
@@ -1,19 +1,120 @@
|
|
|
1
1
|
# @hoanghong/speaking-core
|
|
2
2
|
|
|
3
|
-
TypeScript SDK cho
|
|
3
|
+
TypeScript SDK cho FSEL ASR online: batch upload, WebSocket realtime, microphone AudioWorklet. MIT. Ngôn ngữ `en`, `ja`, `ko`, `zh`, `vi`. Không chứa model hoặc service credential.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Muốn UI dùng ngay: cài [@hoanghong/speaking-angular](https://www.npmjs.com/package/@hoanghong/speaking-angular). Flutter dùng [fsel_speaking](https://pub.dev/packages/fsel_speaking). Core dành cho người tự thiết kế giao diện browser.
|
|
6
|
+
|
|
7
|
+
## 1. Cài đặt
|
|
7
8
|
|
|
8
9
|
```bash
|
|
9
|
-
npm
|
|
10
|
-
npm test
|
|
11
|
-
npm pack
|
|
10
|
+
npm install @hoanghong/speaking-core@^0.2.0
|
|
12
11
|
```
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
Cần browser có WebSocket, AudioWorklet, getUserMedia, HTTPS hoặc localhost. Node >=18 dùng cho tooling/batch; microphone chỉ chạy trong browser. Thư viện là ESM.
|
|
14
|
+
|
|
15
|
+
## 2. Chuẩn bị gateway và asset
|
|
16
|
+
|
|
17
|
+
Backend ứng dụng cung cấp prefix như `/api/speaking`, xác thực người dùng và relay giao thức FSEL ASR tới server. Chỉ backend giữ service token. Không nhúng khóa S3/service token vào bundle. Địa chỉ gateway quan sát được qua Network; không thể giấu URL mạng trong SDK. Package không kèm server ASR hoặc tài khoản miễn phí; server phải có weights cho ngôn ngữ được chọn.
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
Gateway forward `POST /v1/realtime-tickets`, `WS /v1/realtime`, `POST /v1/transcription-jobs`, `GET/DELETE /v1/transcription-jobs/{id}` sau khi bỏ prefix. Bảo vệ route bằng đăng nhập, quyền sở hữu job và quota; không mở relay công khai dùng service token.
|
|
21
|
+
|
|
22
|
+
Copy `node_modules/@hoanghong/speaking-core/assets/pcm-worklet.js` sang static asset `assets/speaking/pcm-worklet.js`. Với Angular thêm vào `angular.json` assets:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{"glob":"pcm-worklet.js","input":"node_modules/@hoanghong/speaking-core/assets","output":"assets/speaking"}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Với bundler khác, dùng chức năng copy static assets tương ứng; URL phải trả JavaScript. Không dùng một HTML fallback làm worklet.
|
|
29
|
+
|
|
30
|
+
## 3. Ví dụ microphone đầy đủ
|
|
31
|
+
|
|
32
|
+
HTML của trang:
|
|
33
|
+
|
|
34
|
+
```html
|
|
35
|
+
<button id="start">Bắt đầu</button>
|
|
36
|
+
<button id="stop" disabled>Dừng</button>
|
|
37
|
+
<button id="cancel" disabled>Hủy</button>
|
|
38
|
+
<pre id="transcript" aria-live="polite"></pre>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Module JavaScript/TypeScript được bundler xử lý sau khi DOM đã có:
|
|
42
|
+
|
|
43
|
+
```js
|
|
44
|
+
import {
|
|
45
|
+
createSpeakingGateway, RealtimeSpeakingClient, SpeakingMicrophone,
|
|
46
|
+
} from '@hoanghong/speaking-core';
|
|
47
|
+
|
|
48
|
+
const config = createSpeakingGateway({ baseUrl: '/api/speaking' });
|
|
49
|
+
const client = new RealtimeSpeakingClient(config.realtime);
|
|
50
|
+
const microphone = new SpeakingMicrophone(client, config.microphone);
|
|
51
|
+
const start = document.querySelector('#start');
|
|
52
|
+
const stop = document.querySelector('#stop');
|
|
53
|
+
const cancel = document.querySelector('#cancel');
|
|
54
|
+
const transcript = document.querySelector('#transcript');
|
|
55
|
+
function idle() { start.disabled = false; stop.disabled = true; cancel.disabled = true; }
|
|
56
|
+
const unsubscribe = client.onEvent(event => {
|
|
57
|
+
if (event.type === 'partial') transcript.textContent = event.text;
|
|
58
|
+
if (event.type === 'clientError') { transcript.textContent = event.error.message; idle(); }
|
|
59
|
+
});
|
|
60
|
+
start.onclick = async () => {
|
|
61
|
+
start.disabled = true;
|
|
62
|
+
transcript.textContent = 'Đang chuẩn bị…';
|
|
63
|
+
try {
|
|
64
|
+
await microphone.start({ language: 'vi' });
|
|
65
|
+
transcript.textContent = 'Đang nghe…';
|
|
66
|
+
stop.disabled = false; cancel.disabled = false;
|
|
67
|
+
} catch (error) { transcript.textContent = String(error); idle(); }
|
|
68
|
+
};
|
|
69
|
+
stop.onclick = async () => {
|
|
70
|
+
stop.disabled = true;
|
|
71
|
+
try {
|
|
72
|
+
const final = await microphone.stop();
|
|
73
|
+
transcript.textContent = final.result?.text ?? 'Không có lời nói.';
|
|
74
|
+
} catch (error) { transcript.textContent = String(error); }
|
|
75
|
+
finally { idle(); }
|
|
76
|
+
};
|
|
77
|
+
cancel.onclick = async () => {
|
|
78
|
+
try { await microphone.cancel(); transcript.textContent = 'Đã hủy.'; }
|
|
79
|
+
catch (error) { transcript.textContent = String(error); }
|
|
80
|
+
finally { idle(); }
|
|
81
|
+
};
|
|
82
|
+
window.addEventListener('pagehide', () => {
|
|
83
|
+
unsubscribe();
|
|
84
|
+
void microphone.dispose();
|
|
85
|
+
}, { once: true });
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Không tự bắt đầu ghi âm khi load trang. Chờ quyền microphone và `start()` hoàn tất trước khi nói. Khi dùng trong SPA, gọi unsubscribe/dispose ở lifecycle unmount thay vì chỉ pagehide. Không tự reconnect/replay audio sau mất mạng.
|
|
89
|
+
|
|
90
|
+
## 4. Ví dụ upload đầy đủ
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
import { BatchSpeakingClient, createSpeakingGateway } from '@hoanghong/speaking-core';
|
|
94
|
+
|
|
95
|
+
const batch = new BatchSpeakingClient(createSpeakingGateway().batch);
|
|
96
|
+
export async function transcribeFile(file, signal) {
|
|
97
|
+
if (file.size > 25 * 1024 * 1024) throw new Error('File tối đa 25 MiB');
|
|
98
|
+
let job = await batch.submitAudio(file, 'vi', crypto.randomUUID(), 'short_answer', file.name, signal);
|
|
99
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
100
|
+
while (!['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status)) {
|
|
101
|
+
if (Date.now() > deadline) throw new Error('Hết thời gian chờ; job có thể còn chạy');
|
|
102
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
104
|
+
job = await batch.getResult(job.job_id, signal);
|
|
105
|
+
}
|
|
106
|
+
if (job.error) throw new Error(job.error.message);
|
|
107
|
+
return job;
|
|
108
|
+
}
|
|
109
|
+
// Truyền File lấy từ input[type=file] vào transcribeFile(file).
|
|
110
|
+
// Chủ động hủy server job: await batch.cancelJob(jobId).
|
|
111
|
+
// AbortSignal chỉ dừng request phía client, không tự hủy job trên server.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Cấu hình và xử lý lỗi
|
|
115
|
+
|
|
116
|
+
`createSpeakingGateway({baseUrl, headers, workletUrl, fetch})` trả ba nhóm `batch`, `realtime`, `microphone`. Mặc định base `/api/speaking`, worklet `assets/speaking/pcm-worklet.js`. Relative base dùng origin trang hiện tại; ngoài browser truyền URL HTTP(S) tuyệt đối. HTTPS tự dùng WSS. Không truyền base có userinfo/query/hash. `headers` là callback Promise trả HeadersInit, dùng auth của người dùng ứng dụng nếu cần, không dùng service token dài hạn. Cùng origin sử dụng cookie của ứng dụng; auth khác origin/CORS cần backend cấu hình tương ứng.
|
|
117
|
+
|
|
118
|
+
Realtime ticket được xin tự động qua POST JSON `language`/`audio_profile`; ticket dùng để authenticate WebSocket. SDK xử lý queue audio có giới hạn, ACK, stop/final flush; microphone thu PCM mono 16 kHz. Realtime tối đa 300 giây theo server. Các constructor cấu hình thủ công 0.1 vẫn dùng được.
|
|
18
119
|
|
|
19
|
-
|
|
120
|
+
401/403: kiểm tra auth ứng dụng/gateway. 404: prefix/rewrite/asset. 503: model hoặc tài nguyên server. Worklet MIME/404: kiểm tra copy asset. Microphone denied: quyền và HTTPS. WS timeout: proxy Upgrade/WSS và timeout. `SpeakingError` có `code`, `message`, `status` khi có HTTP status. Không log credential hoặc ticket.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BatchOptions } from './batch.js';
|
|
2
|
+
import { MicrophoneOptions } from './microphone.js';
|
|
3
|
+
import { RealtimeOptions } from './realtime.js';
|
|
4
|
+
export interface SpeakingGatewayOptions {
|
|
5
|
+
/** Application gateway, e.g. /api/speaking. It must relay /v1 HTTP + WebSocket. */
|
|
6
|
+
baseUrl?: string;
|
|
7
|
+
/** Optional short-lived application auth. Never embed the ASR service token. */
|
|
8
|
+
headers?: () => Promise<HeadersInit>;
|
|
9
|
+
workletUrl?: string;
|
|
10
|
+
fetch?: typeof globalThis.fetch;
|
|
11
|
+
}
|
|
12
|
+
export declare function createSpeakingGateway(options?: SpeakingGatewayOptions): {
|
|
13
|
+
batch: BatchOptions;
|
|
14
|
+
realtime: RealtimeOptions;
|
|
15
|
+
microphone: MicrophoneOptions;
|
|
16
|
+
};
|
package/dist/gateway.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SpeakingError } from './contracts.js';
|
|
2
|
+
export function createSpeakingGateway(options = {}) {
|
|
3
|
+
const origin = typeof location === 'undefined' ? undefined : location.origin;
|
|
4
|
+
const url = new URL(options.baseUrl ?? '/api/speaking', origin);
|
|
5
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
|
|
6
|
+
throw new SpeakingError('INVALID_CONFIG', 'Use an HTTP(S) gateway URL without credentials, query or hash');
|
|
7
|
+
}
|
|
8
|
+
const baseUrl = url.href.replace(/\/$/, '');
|
|
9
|
+
const headers = options.headers ?? (async () => ({}));
|
|
10
|
+
const request = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
11
|
+
const socket = new URL(baseUrl + '/v1/realtime');
|
|
12
|
+
socket.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
13
|
+
return {
|
|
14
|
+
batch: { baseUrl, headers, fetch: request },
|
|
15
|
+
realtime: {
|
|
16
|
+
websocketUrl: socket.href,
|
|
17
|
+
ticketProvider: async (start) => {
|
|
18
|
+
const auth = new Headers(await headers());
|
|
19
|
+
auth.set('Content-Type', 'application/json');
|
|
20
|
+
const response = await request(baseUrl + '/v1/realtime-tickets', {
|
|
21
|
+
method: 'POST', headers: auth, credentials: 'same-origin',
|
|
22
|
+
body: JSON.stringify({ language: start.language, audio_profile: start.audioProfile ?? 'short_answer' }),
|
|
23
|
+
});
|
|
24
|
+
const decoded = await response.json().catch(() => ({}));
|
|
25
|
+
const body = decoded && typeof decoded === 'object' ? decoded : {};
|
|
26
|
+
if (!response.ok)
|
|
27
|
+
throw new SpeakingError(body.code ?? 'HTTP_ERROR', body.message ?? 'Cannot create speaking ticket', response.status);
|
|
28
|
+
if (typeof body.ticket !== 'string' || !body.ticket)
|
|
29
|
+
throw new SpeakingError('INVALID_RESPONSE', 'Gateway returned no ticket');
|
|
30
|
+
return body.ticket;
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
microphone: { workletUrl: options.workletUrl ?? 'assets/speaking/pcm-worklet.js' },
|
|
34
|
+
};
|
|
35
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoanghong/speaking-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Online speaking SDK: gateway configuration, realtime microphone and audio upload",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"assets",
|
|
18
|
-
"README.md"
|
|
18
|
+
"README.md",
|
|
19
|
+
"CHANGELOG.md"
|
|
19
20
|
],
|
|
20
21
|
"scripts": {
|
|
21
22
|
"build": "tsc -p tsconfig.json",
|