@hoanghong/speaking-core 0.5.0 → 0.6.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 +131 -70
- package/dist/pronunciation.d.ts +10 -0
- package/dist/pronunciation.js +15 -0
- package/package.json +9 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.6.0
|
|
2
|
+
|
|
3
|
+
- Versioned overall local pronunciation score (harmonic mean of accuracy and completeness).
|
|
4
|
+
- Explicit score scale, availability, components, exclusions and calibration status.
|
|
5
|
+
- New clients accept older servers without inventing an overall score.
|
|
6
|
+
|
|
1
7
|
## 0.5.0
|
|
2
8
|
|
|
3
9
|
- Optional multi-criteria local pronunciation diagnostics with methods, limitations and unavailable scores.
|
package/README.md
CHANGED
|
@@ -1,113 +1,174 @@
|
|
|
1
1
|
# @hoanghong/speaking-core
|
|
2
2
|
|
|
3
|
-
SDK
|
|
3
|
+
TypeScript SDK for custom speaking interfaces: realtime transcription, audio upload and US English pronunciation diagnostics. Models run on the service. The SDK manages connections and short-lived sessions; no service URL or service token is needed. MIT license.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install from npm
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hoanghong/speaking-core@0.6.0
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Install the supplied version 0.6.0 archive
|
|
12
|
+
|
|
13
|
+
Copy the tarball from the release ZIP into your application's vendor directory:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install ./vendor/hoanghong-speaking-core-0.6.0.tgz
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
These instructions use the supplied archive and do not assume registry publication. Angular applications may use @hoanghong/speaking-angular; Flutter applications may use fsel_speaking.
|
|
20
|
+
|
|
21
|
+
## Assess pronunciation from a file
|
|
22
|
+
|
|
23
|
+
Ask the learner to read the exact reference passage. Pass a selected File or recorded Blob to assessAudio. Transcription alone does not assess pronunciation.
|
|
6
24
|
|
|
7
25
|
```ts
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
import {
|
|
27
|
+
createSpeakingService, PronunciationClient, PronunciationAssessment, SpeakingError,
|
|
28
|
+
} from '@hoanghong/speaking-core';
|
|
29
|
+
|
|
30
|
+
const config = createSpeakingService();
|
|
31
|
+
// If FSEL supplied an application key, use instead:
|
|
32
|
+
// const config = createSpeakingService({ appKey: 'YOUR_APP_KEY' });
|
|
33
|
+
const pronunciation = new PronunciationClient(config.pronunciation);
|
|
34
|
+
|
|
35
|
+
export async function assessPronunciation(
|
|
36
|
+
file: File,
|
|
37
|
+
referenceText: string,
|
|
38
|
+
signal?: AbortSignal,
|
|
39
|
+
): Promise<PronunciationAssessment> {
|
|
40
|
+
try {
|
|
41
|
+
return await pronunciation.assessAudio(file, {
|
|
42
|
+
referenceText,
|
|
43
|
+
filename: file.name,
|
|
44
|
+
signal,
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (error instanceof SpeakingError) {
|
|
48
|
+
console.error(error.code, error.status, error.message);
|
|
49
|
+
}
|
|
50
|
+
throw error; // Let the UI display the error and offer a deliberate retry.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function describeOverall(result: PronunciationAssessment): string {
|
|
55
|
+
const value = result.overall;
|
|
56
|
+
return value?.status === 'estimated' && value.score != null
|
|
57
|
+
? `${value.score.toFixed(1)} / ${value.scale} (local estimate)`
|
|
58
|
+
: value?.reason ?? 'Unavailable: upgrade the server if needed';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function describeAccuracy(result: PronunciationAssessment): string {
|
|
62
|
+
const accuracy = result.breakdown?.accuracy;
|
|
63
|
+
console.log(result.comparisons, result.warnings);
|
|
64
|
+
return accuracy?.status === 'estimated' && accuracy.score != null
|
|
65
|
+
? `${accuracy.score.toFixed(1)} / 100 (estimate)`
|
|
66
|
+
: 'Unavailable';
|
|
14
67
|
}
|
|
15
68
|
```
|
|
16
69
|
|
|
17
|
-
|
|
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.
|
|
70
|
+
In an async file-selection/button handler, call assessPronunciation(file, 'The cat is sitting on the mat.', controller.signal), then describeAccuracy(result). Create controller with new AbortController(); call controller.abort() when cancelling or leaving the screen. Keep a loading state, disable duplicate submissions and catch errors. Cancellation does not refund quota or guarantee that server inference stops.
|
|
26
71
|
|
|
27
|
-
|
|
72
|
+
For an explicit single-word pronunciation, pass expectedPhonemes: ['k', 'æ', 't'] with referenceText: 'cat'. Each entry must be a complete model vocabulary token. Do not split IPA into characters, include enclosing slashes or reuse these tokens for a different passage. Normally omit expectedPhonemes for sentences; server-side text-to-phoneme support must be installed.
|
|
73
|
+
|
|
74
|
+
## Pronunciation inputs and results
|
|
75
|
+
|
|
76
|
+
Only en-US pronunciation is supported, independently of transcription language. Audio must be nonempty, at most 120 seconds and 25 MiB. Reference text must contain 1–4096 characters. The service validates duration and supported formats; WAV PCM16 mono at 16 kHz is suitable. The default assessment request timeout is 11 minutes. Each assessment obtains its own session and consumes quota; there is no automatic retry.
|
|
77
|
+
|
|
78
|
+
| Result field | Meaning |
|
|
79
|
+
| --- | --- |
|
|
80
|
+
| overall | Versioned overall local score, status, scale, formula, components and availability reason |
|
|
81
|
+
| reference_text, expected_phonemes | Passage and expected model tokens |
|
|
82
|
+
| observed_phonemes | Recognized tokens with approximate start/end seconds |
|
|
83
|
+
| comparisons | match/substitution/deletion/insertion; missing sounds can have null times |
|
|
84
|
+
| phoneme_similarity, phoneme_error_rate | Edit-based similarity and error rate, not calibrated grades |
|
|
85
|
+
| breakdown | Optional criteria, phoneme error counts, acoustic pauses and limitations |
|
|
86
|
+
| acoustic_frames, psd_frequencies_hz | Formants, pitch, intensity and spectral measurements |
|
|
87
|
+
| warnings, calibrated, timing_method | Interpretation limits; calibration is false and timing is approximate |
|
|
88
|
+
|
|
89
|
+
Older services may omit overall and breakdown or return null. Show unavailable criteria as "Unavailable", never zero. Display warnings, criterion method/reason and breakdown.limitations. Error counts refer to phonemes, not words.
|
|
90
|
+
|
|
91
|
+
Accuracy = 100*M/(M+S+I); completeness = 100*(N-D)/N, where M/S/D/I mean matches/substitutions/omissions/insertions and N is reference token count. Substitutions count as covered for completeness. Similarity = 100*max(0, 1-(S+D+I)/N), so it differs from accuracy. Fluency is an energy-continuity proxy and can be inflated by noise. Prosody provides pitch measurements without a score. Overall pronunciation is the harmonic mean of Accuracy and Completeness. Vocabulary, grammar and topic scores are unavailable. These estimates are not calibrated educational grades, Azure scores or pass/fail decisions.
|
|
92
|
+
|
|
93
|
+
## Realtime transcription and recorded audio
|
|
28
94
|
|
|
29
95
|
```ts
|
|
30
|
-
import {
|
|
96
|
+
import {
|
|
97
|
+
createSpeakingService, RealtimeSpeakingClient, SpeakingMicrophone,
|
|
98
|
+
} from '@hoanghong/speaking-core';
|
|
99
|
+
|
|
31
100
|
const config = createSpeakingService();
|
|
32
101
|
const client = new RealtimeSpeakingClient(config.realtime);
|
|
33
|
-
const
|
|
102
|
+
const microphone = new SpeakingMicrophone(client, config.microphone);
|
|
34
103
|
const unsubscribe = client.onEvent(event => {
|
|
35
104
|
if (event.type === 'partial') console.log(event.text);
|
|
36
105
|
if (event.type === 'clientError') console.error(event.error);
|
|
37
106
|
});
|
|
38
|
-
|
|
39
|
-
|
|
107
|
+
|
|
108
|
+
// Call from explicit user actions; catch failures in the UI.
|
|
109
|
+
export async function start() { await microphone.start({ language: 'en' }); }
|
|
40
110
|
export async function stop() {
|
|
41
|
-
const result = await
|
|
111
|
+
const result = await microphone.stop();
|
|
42
112
|
return { text: result.result?.text ?? '', audio: result.audio };
|
|
43
113
|
}
|
|
44
|
-
export async function cancel() { await
|
|
45
|
-
export async function dispose() { unsubscribe(); await
|
|
114
|
+
export async function cancel() { await microphone.cancel(); }
|
|
115
|
+
export async function dispose() { unsubscribe(); await microphone.dispose(); }
|
|
46
116
|
```
|
|
47
117
|
|
|
48
|
-
|
|
118
|
+
The returned audio is a WAV File, PCM16 mono at 16 kHz, including final buffered samples. Pass it to the pronunciation helper with the passage for a separate assessment. Recording/transcription and pronunciation each create a session. Realtime supports 300 seconds, but assessed recordings must stay within the pronunciation limit of 120 seconds.
|
|
49
119
|
|
|
50
|
-
|
|
120
|
+
Microphone capture requires HTTPS or localhost, getUserMedia and AudioWorklet. The worklet loads automatically. Dispose when unmounting. For playback/download, create an object URL and revoke it when replacing audio or leaving. Use FormData for storage uploads; do not serialize audio into JSON.
|
|
121
|
+
|
|
122
|
+
## Upload for transcription
|
|
51
123
|
|
|
52
124
|
```ts
|
|
53
125
|
import { BatchSpeakingClient, createSpeakingService } from '@hoanghong/speaking-core';
|
|
54
|
-
|
|
55
|
-
export async function
|
|
56
|
-
if (file.size > 25 * 1024 * 1024)
|
|
57
|
-
|
|
58
|
-
|
|
126
|
+
|
|
127
|
+
export async function transcribe(file: File) {
|
|
128
|
+
if (!file.size || file.size > 25 * 1024 * 1024) {
|
|
129
|
+
throw new Error('Select nonempty audio of at most 25 MiB.');
|
|
130
|
+
}
|
|
131
|
+
const batch = new BatchSpeakingClient(createSpeakingService().batch);
|
|
132
|
+
let job = await batch.submitAudio(file, 'en', crypto.randomUUID(), 'short_answer', file.name);
|
|
133
|
+
const deadline = Date.now() + 600_000;
|
|
59
134
|
while (!['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status)) {
|
|
60
|
-
if (Date.now() > deadline) throw new Error('
|
|
135
|
+
if (Date.now() > deadline) throw new Error('Transcription timed out.');
|
|
61
136
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
62
137
|
job = await batch.getResult(job.job_id);
|
|
63
138
|
}
|
|
64
139
|
if (job.error) throw new Error(job.error.message);
|
|
65
|
-
if (job.status === 'cancelled') throw new Error('
|
|
66
|
-
return { text: job.result?.text ?? '', audio: file
|
|
140
|
+
if (job.status === 'cancelled') throw new Error('Transcription was cancelled.');
|
|
141
|
+
return { text: job.result?.text ?? '', audio: file };
|
|
67
142
|
}
|
|
68
143
|
```
|
|
69
144
|
|
|
70
|
-
|
|
145
|
+
Retain the batch client for polling/cancellation. Each public batch configuration owns one job capability at a time; use separate clients/configurations for concurrent jobs. Audio is not automatically stored permanently.
|
|
71
146
|
|
|
72
|
-
|
|
147
|
+
## Application keys and errors
|
|
73
148
|
|
|
74
|
-
|
|
149
|
+
Use createSpeakingService({ appKey: 'YOUR_APP_KEY' }) for an application key supplied by FSEL. It is a visible application identifier, not an administrative credential. Never embed service credentials. Without a key, the basic anonymous quota applies. Invalid/disabled keys do not fall back to anonymous access.
|
|
75
150
|
|
|
76
|
-
|
|
151
|
+
Transcription languages: en/ja/ko/zh/vi. Basic quota defaults to 5 sessions per IP/day and 100 per instance/day (UTC), subject to operator changes. Sessions count even when processing fails. Realtime PCM buffering is approximately 9.6 MB maximum. Network/CSP must permit SDK connections and worklet loading.
|
|
77
152
|
|
|
78
|
-
|
|
153
|
+
Handle SpeakingError.code/status/message: INVALID_REFERENCE, INVALID_AUDIO, INVALID_APP_KEY/401, QUOTA_EXCEEDED/429, unsupported audio/415, validation/422, missing functionality/404 and unavailable model/service/503. Cancellation and timeouts also reject. Do not automatically retry or replay audio. Installing the library does not deploy or verify service/model readiness.
|
|
79
154
|
|
|
80
155
|
|
|
81
|
-
##
|
|
156
|
+
## Overall pronunciation output (0.6.0)
|
|
82
157
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
createSpeakingService({ appKey: 'YOUR_APP_KEY' });
|
|
89
|
-
```
|
|
158
|
+
Use `result.overall?.score` for the overall chart.
|
|
159
|
+
It is a local estimate, the harmonic mean of unrounded Accuracy and Completeness:
|
|
160
|
+
`2*A*C/(A+C)`, or 0 if A+C=0. Fluency and Prosody are excluded. This is not an
|
|
161
|
+
Azure or calibrated educational grade. `phoneme_similarity` is a separate legacy
|
|
162
|
+
edit-distance diagnostic and can be zero while the overall score is positive.
|
|
90
163
|
|
|
164
|
+
The overall object includes `version=local-accuracy-completeness-v1`, `scale=100`,
|
|
165
|
+
`calibrated=false`, `status`, `method`, `reason`, `components` and
|
|
166
|
+
`excluded_components` (Flutter: `excludedComponents`).
|
|
167
|
+
Render null/unavailable scores as unavailable; preserve valid zero values.
|
|
168
|
+
An older server can omit `overall`; the SDK never computes a replacement score.
|
|
169
|
+
`breakdown.pronunciation` mirrors the overall criterion for compatibility.
|
|
170
|
+
The server must be upgraded as well as the SDK.
|
|
91
171
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
vocabulary, grammar and topic. Each has score (0–100 or null), status
|
|
96
|
-
(estimated/unavailable), method and reason. These are local diagnostics, not Azure
|
|
97
|
-
scores or calibrated grades. Older services may omit breakdown.
|
|
98
|
-
|
|
99
|
-
- Accuracy = 100 * matches / (matches + substitutions + insertions), at phoneme level.
|
|
100
|
-
- Completeness = 100 * (reference phonemes - omissions) / reference phonemes.
|
|
101
|
-
Substitutions count as covered; this is coverage, not correctness.
|
|
102
|
-
- Fluency is an energy continuity proxy = 100 * (1 - internal pause seconds / active span).
|
|
103
|
-
20 ms RMS windows, threshold=max(0.005, 0.1*peak RMS), internal gaps >=250 ms.
|
|
104
|
-
Excludes leading/trailing silence; requires >=2 s audio and >=1 s active span.
|
|
105
|
-
Noise may inflate the value and intentional pauses may lower it; this is not VAD/native fluency.
|
|
106
|
-
- Prosody has no score. Pitch P90-P10 and standard deviation (semitones) are reported
|
|
107
|
-
with >=20 voiced frames and >=2 s audio. Variation does not imply correct intonation.
|
|
108
|
-
- Overall pronunciation and content scores remain null: no calibrated composite,
|
|
109
|
-
stress model, transcript/grammar evaluator or topic evaluator is configured.
|
|
110
|
-
|
|
111
|
-
errors counts phoneme matches/substitutions/omissions/insertions. signal.pauses are
|
|
112
|
-
acoustic intervals, not linguistic unexpected/missing breaks. Consult limitations
|
|
113
|
-
and version=local-diagnostics-v1 before interpreting estimates.
|
|
172
|
+
```json
|
|
173
|
+
{"overall":{"score":50,"status":"estimated","method":"accuracy_completeness_hmean: 2*A*C/(A+C); zero if A+C=0","reason":null,"version":"local-accuracy-completeness-v1","scale":100,"calibrated":false,"components":["accuracy","completeness"],"excluded_components":["fluency","prosody"]}}
|
|
174
|
+
```
|
package/dist/pronunciation.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface PronunciationCriterion {
|
|
|
33
33
|
method: string;
|
|
34
34
|
reason: string | null;
|
|
35
35
|
}
|
|
36
|
+
/** Overall local accuracy/completeness harmonic mean. Use this for the overall chart, never phoneme_similarity. */
|
|
37
|
+
export interface PronunciationOverall extends PronunciationCriterion {
|
|
38
|
+
version: 'local-accuracy-completeness-v1';
|
|
39
|
+
scale: 100;
|
|
40
|
+
calibrated: false;
|
|
41
|
+
components: ['accuracy', 'completeness'];
|
|
42
|
+
excluded_components: ['fluency', 'prosody'];
|
|
43
|
+
}
|
|
36
44
|
export interface PronunciationBreakdown {
|
|
37
45
|
version: 'local-diagnostics-v1';
|
|
38
46
|
scope: 'scripted_phoneme_diagnostics';
|
|
@@ -84,6 +92,8 @@ export interface PronunciationAssessment {
|
|
|
84
92
|
warnings: string[];
|
|
85
93
|
/** Optional for compatibility with older service versions. */
|
|
86
94
|
breakdown?: PronunciationBreakdown | null;
|
|
95
|
+
/** Absent on older servers. Null score is unavailable; zero is a valid score. */
|
|
96
|
+
overall?: PronunciationOverall | null;
|
|
87
97
|
}
|
|
88
98
|
export interface AssessPronunciationOptions {
|
|
89
99
|
referenceText: string;
|
package/dist/pronunciation.js
CHANGED
|
@@ -57,6 +57,21 @@ export class PronunciationClient {
|
|
|
57
57
|
throw new SpeakingError('INVALID_RESPONSE', 'Invalid pronunciation diagnostic breakdown');
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
+
if (body.overall != null) {
|
|
61
|
+
const overall = body.overall;
|
|
62
|
+
if (overall.version !== 'local-accuracy-completeness-v1' || overall.scale !== 100 ||
|
|
63
|
+
overall.calibrated !== false || typeof overall.method !== 'string' ||
|
|
64
|
+
JSON.stringify(overall.components) !== JSON.stringify(['accuracy', 'completeness']) ||
|
|
65
|
+
JSON.stringify(overall.excluded_components) !== JSON.stringify(['fluency', 'prosody']) ||
|
|
66
|
+
(overall.reason !== null && typeof overall.reason !== 'string') ||
|
|
67
|
+
(overall.status !== 'estimated' && overall.status !== 'unavailable') ||
|
|
68
|
+
(overall.status === 'unavailable' ? overall.score !== null :
|
|
69
|
+
typeof overall.score !== 'number' || !Number.isFinite(overall.score) || overall.score < 0 || overall.score > 100) ||
|
|
70
|
+
(body.breakdown != null && (overall.score !== body.breakdown.pronunciation.score ||
|
|
71
|
+
overall.status !== body.breakdown.pronunciation.status || overall.method !== body.breakdown.pronunciation.method))) {
|
|
72
|
+
throw new SpeakingError('INVALID_RESPONSE', 'Invalid overall pronunciation score');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
60
75
|
return body;
|
|
61
76
|
}
|
|
62
77
|
finally {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoanghong/speaking-core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "TypeScript SDK for speech transcription, audio upload and versioned local pronunciation assessment with typed overall and component scores.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -32,5 +32,11 @@
|
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public",
|
|
34
34
|
"registry": "https://registry.npmjs.org/"
|
|
35
|
-
}
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"speech",
|
|
38
|
+
"pronunciation-assessment",
|
|
39
|
+
"speech-to-text",
|
|
40
|
+
"sdk"
|
|
41
|
+
]
|
|
36
42
|
}
|