@hoanghong/speaking-core 0.3.0 → 0.4.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 +6 -0
- package/README.md +23 -1
- package/dist/gateway.d.ts +2 -0
- package/dist/gateway.js +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/pronunciation.d.ts +58 -0
- package/dist/pronunciation.js +52 -0
- package/dist/public-service.d.ts +1 -0
- package/dist/public-service.js +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.4.0
|
|
2
|
+
|
|
3
|
+
- Add US English pronunciation assessment client: 120-second audio, reference passage, optional IPA tokens and uncalibrated phoneme diagnostics.
|
|
4
|
+
- Share existing gateway/public application sessions; no service credentials in client apps.
|
|
5
|
+
- Local test release; requires server pronunciation endpoint and model provisioning.
|
|
6
|
+
|
|
1
7
|
# Changelog
|
|
2
8
|
|
|
3
9
|
## 0.3.0
|
package/README.md
CHANGED
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
# @hoanghong/speaking-core
|
|
2
2
|
|
|
3
|
-
SDK online cho người tự làm UI.
|
|
3
|
+
SDK online cho người tự làm UI. Bản test 0.4.0 cài bằng `npm install ./vendor/hoanghong-speaking-core-0.4.0.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
|
+
|
|
5
|
+
## Đánh giá phát âm tiếng Anh Mỹ
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { PronunciationClient, createSpeakingService } from '@hoanghong/speaking-core';
|
|
9
|
+
const pronunciation = new PronunciationClient(createSpeakingService().pronunciation);
|
|
10
|
+
export async function assess(file: File) {
|
|
11
|
+
return pronunciation.assessAudio(file, {
|
|
12
|
+
referenceText: 'cat', expectedPhonemes: ['k', 'æ', 't'], filename: file.name,
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Audio tối đa 120 giây/25 MiB, reference tối đa 4096 ký tự; timeout client 11 phút.
|
|
18
|
+
Đọc cả đoạn: thay referenceText bằng đoạn mẫu; bỏ expectedPhonemes khi server có eSpeak.
|
|
19
|
+
Các âm tùy chọn là token vocabulary, không tách IPA theo ký tự. Mỗi lần gọi cấp phiên riêng,
|
|
20
|
+
không tự retry. AbortSignal được hỗ trợ. Server phải triển khai endpoint/model phát âm;
|
|
21
|
+
bộ cài này chưa xác nhận hosted endpoint đã sẵn sàng. Gateway riêng dùng cấu hình
|
|
22
|
+
`createSpeakingGateway(...).pronunciation`, giữ credential của service ở backend.
|
|
23
|
+
Response typed PronunciationAssessment: âm mẫu/nhận dạng, comparisons, timestamp ước lượng,
|
|
24
|
+
formant/pitch/intensity/PSD và warnings. `calibrated=false`, không dùng similarity làm điểm
|
|
25
|
+
phát âm chính thức. Các API realtime/upload dưới đây giữ nguyên.
|
|
4
26
|
|
|
5
27
|
## Kết nối tự động, không cấu hình URL/token
|
|
6
28
|
|
package/dist/gateway.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BatchOptions } from './batch.js';
|
|
2
2
|
import { MicrophoneOptions } from './microphone.js';
|
|
3
3
|
import { RealtimeOptions } from './realtime.js';
|
|
4
|
+
import { PronunciationOptions } from './pronunciation.js';
|
|
4
5
|
export interface SpeakingGatewayOptions {
|
|
5
6
|
/** Application gateway, e.g. /api/speaking. It must relay /v1 HTTP + WebSocket. */
|
|
6
7
|
baseUrl?: string;
|
|
@@ -13,4 +14,5 @@ export declare function createSpeakingGateway(options?: SpeakingGatewayOptions):
|
|
|
13
14
|
batch: BatchOptions;
|
|
14
15
|
realtime: RealtimeOptions;
|
|
15
16
|
microphone: MicrophoneOptions;
|
|
17
|
+
pronunciation: PronunciationOptions;
|
|
16
18
|
};
|
package/dist/gateway.js
CHANGED
|
@@ -12,6 +12,7 @@ export function createSpeakingGateway(options = {}) {
|
|
|
12
12
|
socket.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
13
13
|
return {
|
|
14
14
|
batch: { baseUrl, headers, fetch: request },
|
|
15
|
+
pronunciation: { baseUrl, headers, fetch: request },
|
|
15
16
|
realtime: {
|
|
16
17
|
websocketUrl: socket.href,
|
|
17
18
|
ticketProvider: async (start) => {
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface PronunciationOptions {
|
|
2
|
+
baseUrl: string;
|
|
3
|
+
/** Short-lived app/gateway authentication; never embed a service credential. */
|
|
4
|
+
headers: () => Promise<HeadersInit>;
|
|
5
|
+
fetch?: typeof globalThis.fetch;
|
|
6
|
+
timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PronunciationPhone {
|
|
9
|
+
phoneme: string;
|
|
10
|
+
start: number;
|
|
11
|
+
end: number;
|
|
12
|
+
}
|
|
13
|
+
export interface PronunciationComparison {
|
|
14
|
+
operation: 'match' | 'substitution' | 'deletion' | 'insertion';
|
|
15
|
+
expected: string | null;
|
|
16
|
+
observed: string | null;
|
|
17
|
+
start: number | null;
|
|
18
|
+
end: number | null;
|
|
19
|
+
}
|
|
20
|
+
export interface PronunciationAcousticFrame {
|
|
21
|
+
time: number;
|
|
22
|
+
formants_hz: Array<number | null>;
|
|
23
|
+
delta_formants_hz: Array<number | null>;
|
|
24
|
+
f2_minus_f1_hz: number | null;
|
|
25
|
+
pitch_hz: number | null;
|
|
26
|
+
intensity_db: number | null;
|
|
27
|
+
psd_power_per_hz: number[];
|
|
28
|
+
}
|
|
29
|
+
export interface PronunciationAssessment {
|
|
30
|
+
reference_text: string;
|
|
31
|
+
language: 'en-US';
|
|
32
|
+
expected_phonemes: string[];
|
|
33
|
+
observed_phonemes: PronunciationPhone[];
|
|
34
|
+
comparisons: PronunciationComparison[];
|
|
35
|
+
phoneme_error_rate: number;
|
|
36
|
+
phoneme_similarity: number;
|
|
37
|
+
calibrated: false;
|
|
38
|
+
timing_method: 'ctc_greedy_approximate';
|
|
39
|
+
duration: number;
|
|
40
|
+
acoustic_frames: PronunciationAcousticFrame[];
|
|
41
|
+
psd_frequencies_hz: number[];
|
|
42
|
+
model: string;
|
|
43
|
+
warnings: string[];
|
|
44
|
+
}
|
|
45
|
+
export interface AssessPronunciationOptions {
|
|
46
|
+
referenceText: string;
|
|
47
|
+
/** Vocabulary tokens, not individual IPA characters. Optional when server has eSpeak. */
|
|
48
|
+
expectedPhonemes?: string[];
|
|
49
|
+
filename?: string;
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}
|
|
52
|
+
export declare class PronunciationClient {
|
|
53
|
+
private readonly options;
|
|
54
|
+
private readonly request;
|
|
55
|
+
constructor(options: PronunciationOptions);
|
|
56
|
+
/** US English, <=120 seconds and <=25 MiB. No automatic retries or duplicate quota charges. */
|
|
57
|
+
assessAudio(audio: Blob, options: AssessPronunciationOptions): Promise<PronunciationAssessment>;
|
|
58
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SpeakingError } from './contracts.js';
|
|
2
|
+
export class PronunciationClient {
|
|
3
|
+
options;
|
|
4
|
+
request;
|
|
5
|
+
constructor(options) {
|
|
6
|
+
this.options = options;
|
|
7
|
+
this.request = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
8
|
+
}
|
|
9
|
+
/** US English, <=120 seconds and <=25 MiB. No automatic retries or duplicate quota charges. */
|
|
10
|
+
async assessAudio(audio, options) {
|
|
11
|
+
if (!options.referenceText.trim() || options.referenceText.length > 4096) {
|
|
12
|
+
throw new SpeakingError('INVALID_REFERENCE', 'Provide reference text of 1 to 4096 characters');
|
|
13
|
+
}
|
|
14
|
+
if (!audio.size || audio.size > 25 * 1024 * 1024) {
|
|
15
|
+
throw new SpeakingError('INVALID_AUDIO', 'Audio must be nonempty and at most 25 MiB');
|
|
16
|
+
}
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const abort = () => controller.abort(options.signal?.reason);
|
|
19
|
+
if (options.signal?.aborted)
|
|
20
|
+
abort();
|
|
21
|
+
else
|
|
22
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), this.options.timeoutMs ?? 660_000);
|
|
24
|
+
try {
|
|
25
|
+
if (controller.signal.aborted)
|
|
26
|
+
throw new SpeakingError('CANCELLED', 'Assessment was cancelled');
|
|
27
|
+
const headers = new Headers(await this.options.headers());
|
|
28
|
+
headers.delete('Content-Type');
|
|
29
|
+
const form = new FormData();
|
|
30
|
+
form.append('file', audio, options.filename ?? 'recording.wav');
|
|
31
|
+
form.append('reference_text', options.referenceText);
|
|
32
|
+
form.append('language', 'en-US');
|
|
33
|
+
if (options.expectedPhonemes !== undefined) {
|
|
34
|
+
form.append('expected_phonemes', JSON.stringify(options.expectedPhonemes));
|
|
35
|
+
}
|
|
36
|
+
const response = await this.request(this.options.baseUrl.replace(/\/$/, '') + '/v1/pronunciation-assessments', { method: 'POST', credentials: 'same-origin', headers, body: form, signal: controller.signal });
|
|
37
|
+
const body = await response.json().catch(() => null);
|
|
38
|
+
if (!response.ok) {
|
|
39
|
+
throw new SpeakingError(body?.code ?? 'HTTP_ERROR', body?.message ?? 'Assessment failed', response.status);
|
|
40
|
+
}
|
|
41
|
+
if (!body || body.language !== 'en-US' || body.calibrated !== false ||
|
|
42
|
+
typeof body.phoneme_similarity !== 'number' || !Array.isArray(body.comparisons)) {
|
|
43
|
+
throw new SpeakingError('INVALID_RESPONSE', 'Invalid pronunciation assessment response');
|
|
44
|
+
}
|
|
45
|
+
return body;
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
options.signal?.removeEventListener('abort', abort);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
package/dist/public-service.d.ts
CHANGED
|
@@ -5,4 +5,5 @@ export declare function createSpeakingService(options?: {
|
|
|
5
5
|
batch: import("./batch.js").BatchOptions;
|
|
6
6
|
realtime: import("./realtime.js").RealtimeOptions;
|
|
7
7
|
microphone: import("./microphone.js").MicrophoneOptions;
|
|
8
|
+
pronunciation: import("./pronunciation.js").PronunciationOptions;
|
|
8
9
|
};
|
package/dist/public-service.js
CHANGED
|
@@ -37,5 +37,7 @@ export function createSpeakingService(options = {}) {
|
|
|
37
37
|
};
|
|
38
38
|
const realtime = createSpeakingGateway({ baseUrl: SERVICE, headers: async () => ({ Authorization: 'Bearer ' + await session() }) });
|
|
39
39
|
config.realtime = realtime.realtime;
|
|
40
|
+
// Every synchronous assessment consumes its own short-lived, single-use session.
|
|
41
|
+
config.pronunciation.headers = async () => ({ Authorization: 'Bearer ' + await session() });
|
|
40
42
|
return config;
|
|
41
43
|
}
|