@hoanghong/speaking-angular 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 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
@@ -5,11 +5,46 @@ Component nhận dạng lời nói online dùng ngay. Chỉ cài package, chọn
5
5
  ## 1. Cài đặt
6
6
 
7
7
  ```bash
8
- npm install @hoanghong/speaking-angular@^0.3.0
8
+ npm install ./vendor/hoanghong-speaking-core-0.4.0.tgz ./vendor/hoanghong-speaking-angular-0.4.0.tgz
9
9
  ```
10
10
 
11
11
  Core được cài tự động. Với ứng dụng mới: `npx @angular/cli@18 new speaking-demo --standalone --routing=false --style=css`, rồi `cd speaking-demo` và chạy lệnh trên.
12
12
 
13
+ Bản 0.4.0 là bộ cài test local: giải nén ZIP và copy hai tarball vào `vendor/` trước.
14
+ Chưa publish 0.4.0 lên registry. Các tính năng nhận dạng cũ vẫn dùng được.
15
+
16
+ ## Đánh giá phát âm tiếng Anh Mỹ (0.4.0)
17
+
18
+ Thêm `SpeakingService` vào `providers` của component dùng để test, import từ
19
+ `@hoanghong/speaking-angular`, rồi gọi trong class component:
20
+
21
+ ```ts
22
+ constructor(private readonly speaking: SpeakingService) {}
23
+
24
+ async assessPronunciation(file: File): Promise<void> {
25
+ const result = await this.speaking.pronunciation.assessAudio(file, {
26
+ referenceText: 'cat',
27
+ expectedPhonemes: ['k', 'æ', 't'],
28
+ filename: file.name,
29
+ });
30
+ console.log(result.phoneme_similarity, result.comparisons, result.warnings);
31
+ }
32
+ ```
33
+
34
+ Gọi bằng file vừa chọn hoặc `result.audio` trong callback `completed` của component hiện có.
35
+ Có thể dùng câu/đoạn đọc: thay referenceText bằng đúng nội dung cần đọc và bỏ
36
+ expectedPhonemes nếu server đã cài eSpeak; không dùng chuỗi âm của `cat` cho đoạn khác.
37
+ Hỗ trợ audio tối đa 120 giây/25 MiB, reference 4096 ký tự, client chờ tối đa 11 phút.
38
+ Thêm `signal: controller.signal` để hủy request. Hàm reject khi lỗi; UI cần catch và hiển thị.
39
+ `calibrated=false`: similarity chỉ là đối chiếu âm vị, chưa phải điểm phát âm chính thức.
40
+ Kết quả có timestamp, âm thiếu/thừa/thay thế, formant/pitch/intensity/PSD và warnings.
41
+
42
+ Dùng `provideSpeakingApp` hiện có nếu có appKey. SDK tự cấp phiên riêng cho mỗi lần đánh giá,
43
+ không nhập service token. Endpoint public `/sdk/v1/pronunciation-assessments` và model phải
44
+ được triển khai trước; 404/503 nghĩa là server chưa sẵn sàng. Bộ cài không triển khai server.
45
+ Test server riêng qua `provideSpeakingGateway({baseUrl: 'https://YOUR_TEST_GATEWAY'})`;
46
+ gateway phải chuyển tiếp endpoint và giữ service credential ở backend, không đưa vào app.
47
+
13
48
  ## 2. Thêm component — ví dụ đầy đủ
14
49
 
15
50
  Thay `src/app/app.component.ts` bằng:
@@ -1,6 +1,6 @@
1
1
  import { Inject, Injectable, InjectionToken } from '@angular/core';
2
2
  import { Subject } from 'rxjs';
3
- import { BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway, createSpeakingService, } from '@hoanghong/speaking-core';
3
+ import { BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway, createSpeakingService, PronunciationClient, } from '@hoanghong/speaking-core';
4
4
  import * as i0 from "@angular/core";
5
5
  export const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG', {
6
6
  providedIn: 'root', factory: () => createSpeakingService(),
@@ -9,6 +9,7 @@ export const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG', {
9
9
  export class SpeakingService {
10
10
  zone;
11
11
  batch;
12
+ pronunciation;
12
13
  realtime;
13
14
  eventSubject = new Subject();
14
15
  events$ = this.eventSubject.asObservable();
@@ -17,6 +18,7 @@ export class SpeakingService {
17
18
  constructor(config, zone) {
18
19
  this.zone = zone;
19
20
  this.batch = new BatchSpeakingClient(config.batch);
21
+ this.pronunciation = new PronunciationClient(config.pronunciation ?? config.batch);
20
22
  this.realtime = new RealtimeSpeakingClient(config.realtime);
21
23
  this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);
22
24
  this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));
@@ -52,4 +54,4 @@ export function provideSpeakingApp(options) {
52
54
  return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingService(options) }];
53
55
  }
54
56
  export * from '@hoanghong/speaking-core';
55
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BlYWtpbmcuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zcGVha2luZy5zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLGNBQWMsRUFBK0IsTUFBTSxlQUFlLENBQUM7QUFDaEcsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUMvQixPQUFPLEVBQ1MsbUJBQW1CLEVBQ2pDLHNCQUFzQixFQUFpQixrQkFBa0IsRUFDakMscUJBQXFCLEVBQzdDLHFCQUFxQixHQUN0QixNQUFNLDBCQUEwQixDQUFDOztBQVFsQyxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsSUFBSSxjQUFjLENBQWlCLHNCQUFzQixFQUFFO0lBQ3hGLFVBQVUsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLEdBQUcsRUFBRSxDQUFDLHFCQUFxQixFQUFFO0NBQzNELENBQUMsQ0FBQztBQUVILDRGQUE0RjtBQUU1RixNQUFNLE9BQU8sZUFBZTtJQVFvRDtJQVByRSxLQUFLLENBQXNCO0lBQzNCLFFBQVEsQ0FBeUI7SUFDekIsWUFBWSxHQUFHLElBQUksT0FBTyxFQUFpQixDQUFDO0lBQ3BELE9BQU8sR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRSxDQUFDO0lBQ25DLFVBQVUsQ0FBcUI7SUFDL0IsV0FBVyxDQUFhO0lBRXpDLFlBQXFDLE1BQXNCLEVBQW1CLElBQVk7UUFBWixTQUFJLEdBQUosSUFBSSxDQUFRO1FBQ3hGLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbkQsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLHNCQUFzQixDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUM1RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksa0JBQWtCLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0UsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN4RyxDQUFDO0lBRUQsS0FBSyxDQUFDLE9BQXFCO1FBQ3pCLE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFFRCxJQUFJLEtBQXFDLE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDekUsTUFBTSxLQUFvQixPQUFPLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBRTVELFdBQVc7UUFDVCxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDbkIsSUFBSSxDQUFDLFlBQVksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUM3QixLQUFLLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDakMsQ0FBQzt3R0ExQlUsZUFBZSxrQkFRTixlQUFlOzRHQVJ4QixlQUFlOzs0RkFBZixlQUFlO2tCQUQzQixVQUFVOzswQkFTSSxNQUFNOzJCQUFDLGVBQWU7O0FBcUJyQyxNQUFNLFVBQVUsZUFBZSxDQUFDLE1BQXNCO0lBQ3BELE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxFQUFFLGVBQWUsQ0FBQyxDQUFDO0FBQzNFLENBQUM7QUFFRCx1RkFBdUY7QUFDdkYsTUFBTSxVQUFVLHNCQUFzQixDQUFDLFVBQWtDLEVBQUU7SUFDekUsT0FBTyxDQUFDLEVBQUUsT0FBTyxFQUFFLGVBQWUsRUFBRSxVQUFVLEVBQUUsR0FBRyxFQUFFLENBQUMscUJBQXFCLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQzFGLENBQUM7QUFFRCx5RkFBeUY7QUFDekYsTUFBTSxVQUFVLGtCQUFrQixDQUFDLE9BQTJCO0lBQzVELE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FBQztBQUMxRixDQUFDO0FBSUQsY0FBYywwQkFBMEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEluamVjdCwgSW5qZWN0YWJsZSwgSW5qZWN0aW9uVG9rZW4sIE5nWm9uZSwgT25EZXN0cm95LCBQcm92aWRlciB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xyXG5pbXBvcnQgeyBTdWJqZWN0IH0gZnJvbSAncnhqcyc7XHJcbmltcG9ydCB7XHJcbiAgQmF0Y2hPcHRpb25zLCBCYXRjaFNwZWFraW5nQ2xpZW50LCBNaWNyb3Bob25lT3B0aW9ucywgUmVjb3JkZWRTcGVha2luZ0ZpbmFsLCBSZWFsdGltZU9wdGlvbnMsXHJcbiAgUmVhbHRpbWVTcGVha2luZ0NsaWVudCwgU3BlYWtpbmdFdmVudCwgU3BlYWtpbmdNaWNyb3Bob25lLCBTdGFydE9wdGlvbnMsXHJcbiAgU3BlYWtpbmdHYXRld2F5T3B0aW9ucywgY3JlYXRlU3BlYWtpbmdHYXRld2F5LFxyXG4gIGNyZWF0ZVNwZWFraW5nU2VydmljZSxcclxufSBmcm9tICdAaG9hbmdob25nL3NwZWFraW5nLWNvcmUnO1xyXG5cclxuZXhwb3J0IGludGVyZmFjZSBTcGVha2luZ0NvbmZpZyB7XHJcbiAgYmF0Y2g6IEJhdGNoT3B0aW9ucztcclxuICByZWFsdGltZTogUmVhbHRpbWVPcHRpb25zO1xyXG4gIG1pY3JvcGhvbmU6IE1pY3JvcGhvbmVPcHRpb25zO1xyXG59XHJcblxyXG5leHBvcnQgY29uc3QgU1BFQUtJTkdfQ09ORklHID0gbmV3IEluamVjdGlvblRva2VuPFNwZWFraW5nQ29uZmlnPignRlNFTF9TUEVBS0lOR19DT05GSUcnLCB7XHJcbiAgcHJvdmlkZWRJbjogJ3Jvb3QnLCBmYWN0b3J5OiAoKSA9PiBjcmVhdGVTcGVha2luZ1NlcnZpY2UoKSxcclxufSk7XHJcblxyXG4vKiogUHJvdmlkZSBhdCB0aGUgc2NyZWVuL2NvbXBvbmVudCBsZXZlbCB0byBnaXZlIGVhY2ggc3BlYWtpbmcgc2NyZWVuIGl0cyBvd24gbGlmZWN5Y2xlLiAqL1xyXG5ASW5qZWN0YWJsZSgpXHJcbmV4cG9ydCBjbGFzcyBTcGVha2luZ1NlcnZpY2UgaW1wbGVtZW50cyBPbkRlc3Ryb3kge1xyXG4gIHJlYWRvbmx5IGJhdGNoOiBCYXRjaFNwZWFraW5nQ2xpZW50O1xyXG4gIHJlYWRvbmx5IHJlYWx0aW1lOiBSZWFsdGltZVNwZWFraW5nQ2xpZW50O1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgZXZlbnRTdWJqZWN0ID0gbmV3IFN1YmplY3Q8U3BlYWtpbmdFdmVudD4oKTtcclxuICByZWFkb25seSBldmVudHMkID0gdGhpcy5ldmVudFN1YmplY3QuYXNPYnNlcnZhYmxlKCk7XHJcbiAgcHJpdmF0ZSByZWFkb25seSBtaWNyb3Bob25lOiBTcGVha2luZ01pY3JvcGhvbmU7XHJcbiAgcHJpdmF0ZSByZWFkb25seSB1bnN1YnNjcmliZTogKCkgPT4gdm9pZDtcclxuXHJcbiAgY29uc3RydWN0b3IoQEluamVjdChTUEVBS0lOR19DT05GSUcpIGNvbmZpZzogU3BlYWtpbmdDb25maWcsIHByaXZhdGUgcmVhZG9ubHkgem9uZTogTmdab25lKSB7XHJcbiAgICB0aGlzLmJhdGNoID0gbmV3IEJhdGNoU3BlYWtpbmdDbGllbnQoY29uZmlnLmJhdGNoKTtcclxuICAgIHRoaXMucmVhbHRpbWUgPSBuZXcgUmVhbHRpbWVTcGVha2luZ0NsaWVudChjb25maWcucmVhbHRpbWUpO1xyXG4gICAgdGhpcy5taWNyb3Bob25lID0gbmV3IFNwZWFraW5nTWljcm9waG9uZSh0aGlzLnJlYWx0aW1lLCBjb25maWcubWljcm9waG9uZSk7XHJcbiAgICB0aGlzLnVuc3Vic2NyaWJlID0gdGhpcy5yZWFsdGltZS5vbkV2ZW50KGV2ZW50ID0+IHRoaXMuem9uZS5ydW4oKCkgPT4gdGhpcy5ldmVudFN1YmplY3QubmV4dChldmVudCkpKTtcclxuICB9XHJcblxyXG4gIHN0YXJ0KG9wdGlvbnM6IFN0YXJ0T3B0aW9ucyk6IFByb21pc2U8dm9pZD4ge1xyXG4gICAgcmV0dXJuIHRoaXMuem9uZS5ydW5PdXRzaWRlQW5ndWxhcigoKSA9PiB0aGlzLm1pY3JvcGhvbmUuc3RhcnQob3B0aW9ucykpO1xyXG4gIH1cclxuXHJcbiAgc3RvcCgpOiBQcm9taXNlPFJlY29yZGVkU3BlYWtpbmdGaW5hbD4geyByZXR1cm4gdGhpcy5taWNyb3Bob25lLnN0b3AoKTsgfVxyXG4gIGNhbmNlbCgpOiBQcm9taXNlPHZvaWQ+IHsgcmV0dXJuIHRoaXMubWljcm9waG9uZS5jYW5jZWwoKTsgfVxyXG5cclxuICBuZ09uRGVzdHJveSgpOiB2b2lkIHtcclxuICAgIHRoaXMudW5zdWJzY3JpYmUoKTtcclxuICAgIHRoaXMuZXZlbnRTdWJqZWN0LmNvbXBsZXRlKCk7XHJcbiAgICB2b2lkIHRoaXMubWljcm9waG9uZS5kaXNwb3NlKCk7XHJcbiAgfVxyXG59XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gcHJvdmlkZVNwZWFraW5nKGNvbmZpZzogU3BlYWtpbmdDb25maWcpOiBQcm92aWRlcltdIHtcclxuICByZXR1cm4gW3sgcHJvdmlkZTogU1BFQUtJTkdfQ09ORklHLCB1c2VWYWx1ZTogY29uZmlnIH0sIFNwZWFraW5nU2VydmljZV07XHJcbn1cclxuXHJcbi8qKiBDb25maWd1cmUgb25jZSBhdCBhcHAgYm9vdHN0cmFwOyBldmVyeSBmc2VsLXNwZWFraW5nIGNvbXBvbmVudCBvd25zIGl0cyBzZXNzaW9uLiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gcHJvdmlkZVNwZWFraW5nR2F0ZXdheShvcHRpb25zOiBTcGVha2luZ0dhdGV3YXlPcHRpb25zID0ge30pOiBQcm92aWRlcltdIHtcclxuICByZXR1cm4gW3sgcHJvdmlkZTogU1BFQUtJTkdfQ09ORklHLCB1c2VGYWN0b3J5OiAoKSA9PiBjcmVhdGVTcGVha2luZ0dhdGV3YXkob3B0aW9ucykgfV07XHJcbn1cclxuXHJcbi8qKiBBcHBsaWNhdGlvbiBpZGVudGl0eSBvbmx5OyBob3N0ZWQgZW5kcG9pbnQgYW5kIHNob3J0LWxpdmVkIHNlc3Npb25zIGFyZSBhdXRvbWF0aWMuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBwcm92aWRlU3BlYWtpbmdBcHAob3B0aW9uczogeyBhcHBLZXk6IHN0cmluZyB9KTogUHJvdmlkZXJbXSB7XHJcbiAgcmV0dXJuIFt7IHByb3ZpZGU6IFNQRUFLSU5HX0NPTkZJRywgdXNlRmFjdG9yeTogKCkgPT4gY3JlYXRlU3BlYWtpbmdTZXJ2aWNlKG9wdGlvbnMpIH1dO1xyXG59XHJcblxyXG5cclxuXHJcbmV4cG9ydCAqIGZyb20gJ0Bob2FuZ2hvbmcvc3BlYWtpbmctY29yZSc7XHJcbiJdfQ==
57
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BlYWtpbmcuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zcGVha2luZy5zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLGNBQWMsRUFBK0IsTUFBTSxlQUFlLENBQUM7QUFDaEcsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUMvQixPQUFPLEVBQ1MsbUJBQW1CLEVBQ2pDLHNCQUFzQixFQUFpQixrQkFBa0IsRUFDakMscUJBQXFCLEVBQzdDLHFCQUFxQixFQUNyQixtQkFBbUIsR0FDcEIsTUFBTSwwQkFBMEIsQ0FBQzs7QUFTbEMsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLElBQUksY0FBYyxDQUFpQixzQkFBc0IsRUFBRTtJQUN4RixVQUFVLEVBQUUsTUFBTSxFQUFFLE9BQU8sRUFBRSxHQUFHLEVBQUUsQ0FBQyxxQkFBcUIsRUFBRTtDQUMzRCxDQUFDLENBQUM7QUFFSCw0RkFBNEY7QUFFNUYsTUFBTSxPQUFPLGVBQWU7SUFTb0Q7SUFSckUsS0FBSyxDQUFzQjtJQUMzQixhQUFhLENBQXNCO0lBQ25DLFFBQVEsQ0FBeUI7SUFDekIsWUFBWSxHQUFHLElBQUksT0FBTyxFQUFpQixDQUFDO0lBQ3BELE9BQU8sR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRSxDQUFDO0lBQ25DLFVBQVUsQ0FBcUI7SUFDL0IsV0FBVyxDQUFhO0lBRXpDLFlBQXFDLE1BQXNCLEVBQW1CLElBQVk7UUFBWixTQUFJLEdBQUosSUFBSSxDQUFRO1FBQ3hGLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbkQsSUFBSSxDQUFDLGFBQWEsR0FBRyxJQUFJLG1CQUFtQixDQUFDLE1BQU0sQ0FBQyxhQUFhLElBQUksTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQ25GLElBQUksQ0FBQyxRQUFRLEdBQUcsSUFBSSxzQkFBc0IsQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUM7UUFDNUQsSUFBSSxDQUFDLFVBQVUsR0FBRyxJQUFJLGtCQUFrQixDQUFDLElBQUksQ0FBQyxRQUFRLEVBQUUsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDO1FBQzNFLElBQUksQ0FBQyxXQUFXLEdBQUcsSUFBSSxDQUFDLFFBQVEsQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLEVBQUUsQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDeEcsQ0FBQztJQUVELEtBQUssQ0FBQyxPQUFxQjtRQUN6QixPQUFPLElBQUksQ0FBQyxJQUFJLENBQUMsaUJBQWlCLENBQUMsR0FBRyxFQUFFLENBQUMsSUFBSSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQztJQUMzRSxDQUFDO0lBRUQsSUFBSSxLQUFxQyxPQUFPLElBQUksQ0FBQyxVQUFVLENBQUMsSUFBSSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBQ3pFLE1BQU0sS0FBb0IsT0FBTyxJQUFJLENBQUMsVUFBVSxDQUFDLE1BQU0sRUFBRSxDQUFDLENBQUMsQ0FBQztJQUU1RCxXQUFXO1FBQ1QsSUFBSSxDQUFDLFdBQVcsRUFBRSxDQUFDO1FBQ25CLElBQUksQ0FBQyxZQUFZLENBQUMsUUFBUSxFQUFFLENBQUM7UUFDN0IsS0FBSyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sRUFBRSxDQUFDO0lBQ2pDLENBQUM7d0dBNUJVLGVBQWUsa0JBU04sZUFBZTs0R0FUeEIsZUFBZTs7NEZBQWYsZUFBZTtrQkFEM0IsVUFBVTs7MEJBVUksTUFBTTsyQkFBQyxlQUFlOztBQXNCckMsTUFBTSxVQUFVLGVBQWUsQ0FBQyxNQUFzQjtJQUNwRCxPQUFPLENBQUMsRUFBRSxPQUFPLEVBQUUsZUFBZSxFQUFFLFFBQVEsRUFBRSxNQUFNLEVBQUUsRUFBRSxlQUFlLENBQUMsQ0FBQztBQUMzRSxDQUFDO0FBRUQsdUZBQXVGO0FBQ3ZGLE1BQU0sVUFBVSxzQkFBc0IsQ0FBQyxVQUFrQyxFQUFFO0lBQ3pFLE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FBQztBQUMxRixDQUFDO0FBRUQseUZBQXlGO0FBQ3pGLE1BQU0sVUFBVSxrQkFBa0IsQ0FBQyxPQUEyQjtJQUM1RCxPQUFPLENBQUMsRUFBRSxPQUFPLEVBQUUsZUFBZSxFQUFFLFVBQVUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxxQkFBcUIsQ0FBQyxPQUFPLENBQUMsRUFBRSxDQUFDLENBQUM7QUFDMUYsQ0FBQztBQUlELGNBQWMsMEJBQTBCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgeyBJbmplY3QsIEluamVjdGFibGUsIEluamVjdGlvblRva2VuLCBOZ1pvbmUsIE9uRGVzdHJveSwgUHJvdmlkZXIgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcclxuaW1wb3J0IHsgU3ViamVjdCB9IGZyb20gJ3J4anMnO1xyXG5pbXBvcnQge1xyXG4gIEJhdGNoT3B0aW9ucywgQmF0Y2hTcGVha2luZ0NsaWVudCwgTWljcm9waG9uZU9wdGlvbnMsIFJlY29yZGVkU3BlYWtpbmdGaW5hbCwgUmVhbHRpbWVPcHRpb25zLFxyXG4gIFJlYWx0aW1lU3BlYWtpbmdDbGllbnQsIFNwZWFraW5nRXZlbnQsIFNwZWFraW5nTWljcm9waG9uZSwgU3RhcnRPcHRpb25zLFxyXG4gIFNwZWFraW5nR2F0ZXdheU9wdGlvbnMsIGNyZWF0ZVNwZWFraW5nR2F0ZXdheSxcclxuICBjcmVhdGVTcGVha2luZ1NlcnZpY2UsXHJcbiAgUHJvbnVuY2lhdGlvbkNsaWVudCwgUHJvbnVuY2lhdGlvbk9wdGlvbnMsXHJcbn0gZnJvbSAnQGhvYW5naG9uZy9zcGVha2luZy1jb3JlJztcclxuXHJcbmV4cG9ydCBpbnRlcmZhY2UgU3BlYWtpbmdDb25maWcge1xyXG4gIGJhdGNoOiBCYXRjaE9wdGlvbnM7XHJcbiAgcmVhbHRpbWU6IFJlYWx0aW1lT3B0aW9ucztcclxuICBtaWNyb3Bob25lOiBNaWNyb3Bob25lT3B0aW9ucztcclxuICBwcm9udW5jaWF0aW9uPzogUHJvbnVuY2lhdGlvbk9wdGlvbnM7XHJcbn1cclxuXHJcbmV4cG9ydCBjb25zdCBTUEVBS0lOR19DT05GSUcgPSBuZXcgSW5qZWN0aW9uVG9rZW48U3BlYWtpbmdDb25maWc+KCdGU0VMX1NQRUFLSU5HX0NPTkZJRycsIHtcclxuICBwcm92aWRlZEluOiAncm9vdCcsIGZhY3Rvcnk6ICgpID0+IGNyZWF0ZVNwZWFraW5nU2VydmljZSgpLFxyXG59KTtcclxuXHJcbi8qKiBQcm92aWRlIGF0IHRoZSBzY3JlZW4vY29tcG9uZW50IGxldmVsIHRvIGdpdmUgZWFjaCBzcGVha2luZyBzY3JlZW4gaXRzIG93biBsaWZlY3ljbGUuICovXHJcbkBJbmplY3RhYmxlKClcclxuZXhwb3J0IGNsYXNzIFNwZWFraW5nU2VydmljZSBpbXBsZW1lbnRzIE9uRGVzdHJveSB7XHJcbiAgcmVhZG9ubHkgYmF0Y2g6IEJhdGNoU3BlYWtpbmdDbGllbnQ7XHJcbiAgcmVhZG9ubHkgcHJvbnVuY2lhdGlvbjogUHJvbnVuY2lhdGlvbkNsaWVudDtcclxuICByZWFkb25seSByZWFsdGltZTogUmVhbHRpbWVTcGVha2luZ0NsaWVudDtcclxuICBwcml2YXRlIHJlYWRvbmx5IGV2ZW50U3ViamVjdCA9IG5ldyBTdWJqZWN0PFNwZWFraW5nRXZlbnQ+KCk7XHJcbiAgcmVhZG9ubHkgZXZlbnRzJCA9IHRoaXMuZXZlbnRTdWJqZWN0LmFzT2JzZXJ2YWJsZSgpO1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgbWljcm9waG9uZTogU3BlYWtpbmdNaWNyb3Bob25lO1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgdW5zdWJzY3JpYmU6ICgpID0+IHZvaWQ7XHJcblxyXG4gIGNvbnN0cnVjdG9yKEBJbmplY3QoU1BFQUtJTkdfQ09ORklHKSBjb25maWc6IFNwZWFraW5nQ29uZmlnLCBwcml2YXRlIHJlYWRvbmx5IHpvbmU6IE5nWm9uZSkge1xyXG4gICAgdGhpcy5iYXRjaCA9IG5ldyBCYXRjaFNwZWFraW5nQ2xpZW50KGNvbmZpZy5iYXRjaCk7XHJcbiAgICB0aGlzLnByb251bmNpYXRpb24gPSBuZXcgUHJvbnVuY2lhdGlvbkNsaWVudChjb25maWcucHJvbnVuY2lhdGlvbiA/PyBjb25maWcuYmF0Y2gpO1xyXG4gICAgdGhpcy5yZWFsdGltZSA9IG5ldyBSZWFsdGltZVNwZWFraW5nQ2xpZW50KGNvbmZpZy5yZWFsdGltZSk7XHJcbiAgICB0aGlzLm1pY3JvcGhvbmUgPSBuZXcgU3BlYWtpbmdNaWNyb3Bob25lKHRoaXMucmVhbHRpbWUsIGNvbmZpZy5taWNyb3Bob25lKTtcclxuICAgIHRoaXMudW5zdWJzY3JpYmUgPSB0aGlzLnJlYWx0aW1lLm9uRXZlbnQoZXZlbnQgPT4gdGhpcy56b25lLnJ1bigoKSA9PiB0aGlzLmV2ZW50U3ViamVjdC5uZXh0KGV2ZW50KSkpO1xyXG4gIH1cclxuXHJcbiAgc3RhcnQob3B0aW9uczogU3RhcnRPcHRpb25zKTogUHJvbWlzZTx2b2lkPiB7XHJcbiAgICByZXR1cm4gdGhpcy56b25lLnJ1bk91dHNpZGVBbmd1bGFyKCgpID0+IHRoaXMubWljcm9waG9uZS5zdGFydChvcHRpb25zKSk7XHJcbiAgfVxyXG5cclxuICBzdG9wKCk6IFByb21pc2U8UmVjb3JkZWRTcGVha2luZ0ZpbmFsPiB7IHJldHVybiB0aGlzLm1pY3JvcGhvbmUuc3RvcCgpOyB9XHJcbiAgY2FuY2VsKCk6IFByb21pc2U8dm9pZD4geyByZXR1cm4gdGhpcy5taWNyb3Bob25lLmNhbmNlbCgpOyB9XHJcblxyXG4gIG5nT25EZXN0cm95KCk6IHZvaWQge1xyXG4gICAgdGhpcy51bnN1YnNjcmliZSgpO1xyXG4gICAgdGhpcy5ldmVudFN1YmplY3QuY29tcGxldGUoKTtcclxuICAgIHZvaWQgdGhpcy5taWNyb3Bob25lLmRpc3Bvc2UoKTtcclxuICB9XHJcbn1cclxuXHJcbmV4cG9ydCBmdW5jdGlvbiBwcm92aWRlU3BlYWtpbmcoY29uZmlnOiBTcGVha2luZ0NvbmZpZyk6IFByb3ZpZGVyW10ge1xyXG4gIHJldHVybiBbeyBwcm92aWRlOiBTUEVBS0lOR19DT05GSUcsIHVzZVZhbHVlOiBjb25maWcgfSwgU3BlYWtpbmdTZXJ2aWNlXTtcclxufVxyXG5cclxuLyoqIENvbmZpZ3VyZSBvbmNlIGF0IGFwcCBib290c3RyYXA7IGV2ZXJ5IGZzZWwtc3BlYWtpbmcgY29tcG9uZW50IG93bnMgaXRzIHNlc3Npb24uICovXHJcbmV4cG9ydCBmdW5jdGlvbiBwcm92aWRlU3BlYWtpbmdHYXRld2F5KG9wdGlvbnM6IFNwZWFraW5nR2F0ZXdheU9wdGlvbnMgPSB7fSk6IFByb3ZpZGVyW10ge1xyXG4gIHJldHVybiBbeyBwcm92aWRlOiBTUEVBS0lOR19DT05GSUcsIHVzZUZhY3Rvcnk6ICgpID0+IGNyZWF0ZVNwZWFraW5nR2F0ZXdheShvcHRpb25zKSB9XTtcclxufVxyXG5cclxuLyoqIEFwcGxpY2F0aW9uIGlkZW50aXR5IG9ubHk7IGhvc3RlZCBlbmRwb2ludCBhbmQgc2hvcnQtbGl2ZWQgc2Vzc2lvbnMgYXJlIGF1dG9tYXRpYy4gKi9cclxuZXhwb3J0IGZ1bmN0aW9uIHByb3ZpZGVTcGVha2luZ0FwcChvcHRpb25zOiB7IGFwcEtleTogc3RyaW5nIH0pOiBQcm92aWRlcltdIHtcclxuICByZXR1cm4gW3sgcHJvdmlkZTogU1BFQUtJTkdfQ09ORklHLCB1c2VGYWN0b3J5OiAoKSA9PiBjcmVhdGVTcGVha2luZ1NlcnZpY2Uob3B0aW9ucykgfV07XHJcbn1cclxuXHJcblxyXG5cclxuZXhwb3J0ICogZnJvbSAnQGhvYW5naG9uZy9zcGVha2luZy1jb3JlJztcclxuIl19
@@ -1,7 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { InjectionToken, Inject, Injectable, EventEmitter, inject, DestroyRef, Output, Input, Component } from '@angular/core';
3
3
  import { Subject } from 'rxjs';
4
- import { createSpeakingService, BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway } from '@hoanghong/speaking-core';
4
+ import { createSpeakingService, BatchSpeakingClient, PronunciationClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway } from '@hoanghong/speaking-core';
5
5
  export * from '@hoanghong/speaking-core';
6
6
  import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
7
 
@@ -12,6 +12,7 @@ const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG', {
12
12
  class SpeakingService {
13
13
  zone;
14
14
  batch;
15
+ pronunciation;
15
16
  realtime;
16
17
  eventSubject = new Subject();
17
18
  events$ = this.eventSubject.asObservable();
@@ -20,6 +21,7 @@ class SpeakingService {
20
21
  constructor(config, zone) {
21
22
  this.zone = zone;
22
23
  this.batch = new BatchSpeakingClient(config.batch);
24
+ this.pronunciation = new PronunciationClient(config.pronunciation ?? config.batch);
23
25
  this.realtime = new RealtimeSpeakingClient(config.realtime);
24
26
  this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);
25
27
  this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));
@@ -1 +1 @@
1
- {"version":3,"file":"hoanghong-speaking-angular.mjs","sources":["../../src/speaking.service.ts","../../src/speaking.component.ts","../../src/hoanghong-speaking-angular.ts"],"sourcesContent":["import { Inject, Injectable, InjectionToken, NgZone, OnDestroy, Provider } from '@angular/core';\r\nimport { Subject } from 'rxjs';\r\nimport {\r\n BatchOptions, BatchSpeakingClient, MicrophoneOptions, RecordedSpeakingFinal, RealtimeOptions,\r\n RealtimeSpeakingClient, SpeakingEvent, SpeakingMicrophone, StartOptions,\r\n SpeakingGatewayOptions, createSpeakingGateway,\r\n createSpeakingService,\r\n} from '@hoanghong/speaking-core';\r\n\r\nexport interface SpeakingConfig {\r\n batch: BatchOptions;\r\n realtime: RealtimeOptions;\r\n microphone: MicrophoneOptions;\r\n}\r\n\r\nexport const SPEAKING_CONFIG = new InjectionToken<SpeakingConfig>('FSEL_SPEAKING_CONFIG', {\r\n providedIn: 'root', factory: () => createSpeakingService(),\r\n});\r\n\r\n/** Provide at the screen/component level to give each speaking screen its own lifecycle. */\r\n@Injectable()\r\nexport class SpeakingService implements OnDestroy {\r\n readonly batch: BatchSpeakingClient;\r\n readonly realtime: RealtimeSpeakingClient;\r\n private readonly eventSubject = new Subject<SpeakingEvent>();\r\n readonly events$ = this.eventSubject.asObservable();\r\n private readonly microphone: SpeakingMicrophone;\r\n private readonly unsubscribe: () => void;\r\n\r\n constructor(@Inject(SPEAKING_CONFIG) config: SpeakingConfig, private readonly zone: NgZone) {\r\n this.batch = new BatchSpeakingClient(config.batch);\r\n this.realtime = new RealtimeSpeakingClient(config.realtime);\r\n this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);\r\n this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));\r\n }\r\n\r\n start(options: StartOptions): Promise<void> {\r\n return this.zone.runOutsideAngular(() => this.microphone.start(options));\r\n }\r\n\r\n stop(): Promise<RecordedSpeakingFinal> { return this.microphone.stop(); }\r\n cancel(): Promise<void> { return this.microphone.cancel(); }\r\n\r\n ngOnDestroy(): void {\r\n this.unsubscribe();\r\n this.eventSubject.complete();\r\n void this.microphone.dispose();\r\n }\r\n}\r\n\r\nexport function provideSpeaking(config: SpeakingConfig): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useValue: config }, SpeakingService];\r\n}\r\n\r\n/** Configure once at app bootstrap; every fsel-speaking component owns its session. */\r\nexport function provideSpeakingGateway(options: SpeakingGatewayOptions = {}): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingGateway(options) }];\r\n}\r\n\r\n/** Application identity only; hosted endpoint and short-lived sessions are automatic. */\r\nexport function provideSpeakingApp(options: { appKey: string }): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingService(options) }];\r\n}\r\n\r\n\r\n\r\nexport * from '@hoanghong/speaking-core';\r\n","import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';\r\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\r\nimport { Job, Language, SpeakingResult, SpeakingServiceType, SpeakingEvent } from '@hoanghong/speaking-core';\r\nimport { SpeakingService } from './speaking.service';\r\n\r\n@Component({\r\n selector: 'fsel-speaking', standalone: true, providers: [SpeakingService],\r\n template: `\r\n <section aria-label=\"FSEL Speaking\">\r\n <label>Ngôn ngữ <select [value]=\"language\" [disabled]=\"busy\" (change)=\"selectLanguage($event)\">\r\n @for (item of languages; track item.code) { <option [value]=\"item.code\" [selected]=\"item.code === language\">{{ item.name }}</option> }\r\n </select></label>\r\n <div class=\"actions\">\r\n @if (serviceType !== 'upload') {\r\n <button type=\"button\" (click)=\"start()\" [disabled]=\"busy\">Bắt đầu nói</button>\r\n <button type=\"button\" (click)=\"stop()\" [disabled]=\"!recording\">Dừng và lấy kết quả</button>\r\n }\r\n <button type=\"button\" (click)=\"cancel()\" [disabled]=\"!busy || (uploading && !job)\">Hủy</button>\r\n </div>\r\n @if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)\r\n <input type=\"file\" accept=\"audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm\" [disabled]=\"busy\" (change)=\"upload($event)\">\r\n </label> }\r\n <p role=\"status\">{{ status }}</p>\r\n @if (error) { <p role=\"alert\" class=\"error\">{{ error }}</p> }\r\n <label>Văn bản nhận dạng <textarea rows=\"7\" readonly [value]=\"text\"></textarea></label>\r\n </section>`,\r\n styles: [`:host{display:block;font:inherit;color:inherit}section{border:1px solid #b9c1bc;padding:20px;border-radius:4px}label{display:grid;gap:8px;margin-bottom:16px}select,input,textarea,button{font:inherit;box-sizing:border-box;max-width:100%}select,input,textarea{padding:10px;border:1px solid #929c95;border-radius:3px;background:#fff;color:#202723}textarea{width:100%;resize:vertical}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}button{min-height:44px;padding:8px 14px;background:#365448;color:white;border:1px solid #365448;border-radius:4px;cursor:pointer}button:disabled{opacity:.5;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #a66a15;outline-offset:2px}.error{color:#a02020;overflow-wrap:anywhere}`],\r\n})\r\nexport class FselSpeakingComponent implements OnInit {\r\n @Input() initialLanguage: Language = 'vi';\r\n @Input() uploadEnabled = true;\r\n @Input() serviceType?: SpeakingServiceType;\r\n @Output() completed = new EventEmitter<SpeakingResult>();\r\n @Output() failed = new EventEmitter<Error>();\r\n readonly speaking = inject(SpeakingService);\r\n private readonly destroy = inject(DestroyRef);\r\n readonly languages = [{ code: 'en', name: 'Tiếng Anh' }, { code: 'ja', name: 'Tiếng Nhật' }, { code: 'ko', name: 'Tiếng Hàn' }, { code: 'zh', name: 'Tiếng Trung' }, { code: 'vi', name: 'Tiếng Việt' }];\r\n language: Language = 'vi'; busy = false; recording = false; uploading = false;\r\n text = ''; error = ''; status = 'Chọn ngôn ngữ, sau đó bấm nói hoặc chọn file.';\r\n job?: Job; private generation = 0; private abort?: AbortController;\r\n constructor() {\r\n this.speaking.events$.pipe(takeUntilDestroyed(this.destroy)).subscribe((event: SpeakingEvent) => {\r\n if (event.type === 'partial') this.text = event.text;\r\n if (event.type === 'state') this.recording = event.state === 'recording';\r\n if (event.type === 'clientError') { this.report(event.error); this.busy = false; this.recording = false; }\r\n });\r\n this.destroy.onDestroy(() => { ++this.generation; this.abort?.abort(); });\r\n }\r\n ngOnInit(): void { this.language = this.initialLanguage; }\r\n selectLanguage(event: Event): void { this.language = (event.target as HTMLSelectElement).value as Language; }\r\n private report(error: unknown): void {\r\n const value = error instanceof Error ? error : new Error('Speaking failed');\r\n this.error = value.message; this.failed.emit(value);\r\n }\r\n async start(): Promise<void> {\r\n if (this.busy) return;\r\n const generation = ++this.generation;\r\n this.busy = true; this.uploading = false; this.error = ''; this.text = ''; this.status = 'Đang mở microphone và chuẩn bị model…';\r\n try { await this.speaking.start({ language: this.language }); if (generation === this.generation) this.status = 'Đang nghe…'; }\r\n catch (error) { if (generation === this.generation) { this.report(error); this.busy = false; } }\r\n }\r\n async stop(): Promise<void> {\r\n if (!this.recording) return;\r\n const generation = this.generation;\r\n this.recording = false; this.status = 'Đang lấy kết quả…';\r\n try {\r\n const result = await this.speaking.stop();\r\n if (generation !== this.generation) return;\r\n this.text = result.result?.text ?? ''; this.status = result.status === 'no_speech' ? 'Không nhận thấy lời nói.' : 'Đã hoàn tất.';\r\n this.completed.emit({ ...result, language: this.language, text: this.text, audio: result.audio, serviceType: 'realtime' });\r\n } catch (error) { if (generation === this.generation) this.report(error); }\r\n finally { if (generation === this.generation) this.busy = false; }\r\n }\r\n async cancel(): Promise<void> {\r\n ++this.generation; this.abort?.abort();\r\n try { if (this.uploading && this.job) await this.speaking.batch.cancelJob(this.job.job_id); else await this.speaking.cancel(); this.status = 'Đã hủy.'; }\r\n catch (error) { this.report(error); }\r\n finally { this.busy = false; this.recording = false; this.uploading = false; }\r\n }\r\n async upload(event: Event): Promise<void> {\r\n const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = '';\r\n if (!file || this.busy) return;\r\n if (file.size > 25 * 1024 * 1024) { this.report(new Error('File tối đa 25 MB.')); return; }\r\n const generation = ++this.generation; this.abort = new AbortController();\r\n this.busy = true; this.uploading = true; this.job = undefined; this.text = ''; this.error = ''; this.status = 'Đang upload…';\r\n try {\r\n let job = await this.speaking.batch.submitAudio(file, this.language, crypto.randomUUID(), 'short_answer', file.name, this.abort.signal);\r\n const deadline = Date.now() + 10 * 60 * 1000;\r\n while (generation === this.generation) {\r\n this.job = job;\r\n if (['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status)) break;\r\n if (Date.now() > deadline) throw new Error('Hết thời gian chờ; job có thể vẫn đang chạy trên server.');\r\n this.status = 'Đang nhận dạng file…';\r\n await new Promise(resolve => setTimeout(resolve, 1500));\r\n if (generation !== this.generation) return;\r\n job = await this.speaking.batch.getResult(job.job_id, this.abort.signal);\r\n }\r\n if (generation !== this.generation) return;\r\n if (job.error) throw new Error(job.error.message);\r\n this.text = job.result?.text ?? ''; this.status = job.status === 'no_speech' ? 'Không nhận thấy lời nói.' : job.status === 'cancelled' ? 'Đã hủy.' : 'Đã hoàn tất.';\r\n if (job.status !== 'cancelled') this.completed.emit({ ...job, language: this.language, text: this.text, audio: file, serviceType: 'upload' });\r\n } catch (error) { if (generation === this.generation) this.report(error); }\r\n finally { if (generation === this.generation) { this.busy = false; this.uploading = false; } }\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"hoanghong-speaking-angular.mjs","sources":["../../src/speaking.service.ts","../../src/speaking.component.ts","../../src/hoanghong-speaking-angular.ts"],"sourcesContent":["import { Inject, Injectable, InjectionToken, NgZone, OnDestroy, Provider } from '@angular/core';\r\nimport { Subject } from 'rxjs';\r\nimport {\r\n BatchOptions, BatchSpeakingClient, MicrophoneOptions, RecordedSpeakingFinal, RealtimeOptions,\r\n RealtimeSpeakingClient, SpeakingEvent, SpeakingMicrophone, StartOptions,\r\n SpeakingGatewayOptions, createSpeakingGateway,\r\n createSpeakingService,\r\n PronunciationClient, PronunciationOptions,\r\n} from '@hoanghong/speaking-core';\r\n\r\nexport interface SpeakingConfig {\r\n batch: BatchOptions;\r\n realtime: RealtimeOptions;\r\n microphone: MicrophoneOptions;\r\n pronunciation?: PronunciationOptions;\r\n}\r\n\r\nexport const SPEAKING_CONFIG = new InjectionToken<SpeakingConfig>('FSEL_SPEAKING_CONFIG', {\r\n providedIn: 'root', factory: () => createSpeakingService(),\r\n});\r\n\r\n/** Provide at the screen/component level to give each speaking screen its own lifecycle. */\r\n@Injectable()\r\nexport class SpeakingService implements OnDestroy {\r\n readonly batch: BatchSpeakingClient;\r\n readonly pronunciation: PronunciationClient;\r\n readonly realtime: RealtimeSpeakingClient;\r\n private readonly eventSubject = new Subject<SpeakingEvent>();\r\n readonly events$ = this.eventSubject.asObservable();\r\n private readonly microphone: SpeakingMicrophone;\r\n private readonly unsubscribe: () => void;\r\n\r\n constructor(@Inject(SPEAKING_CONFIG) config: SpeakingConfig, private readonly zone: NgZone) {\r\n this.batch = new BatchSpeakingClient(config.batch);\r\n this.pronunciation = new PronunciationClient(config.pronunciation ?? config.batch);\r\n this.realtime = new RealtimeSpeakingClient(config.realtime);\r\n this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);\r\n this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));\r\n }\r\n\r\n start(options: StartOptions): Promise<void> {\r\n return this.zone.runOutsideAngular(() => this.microphone.start(options));\r\n }\r\n\r\n stop(): Promise<RecordedSpeakingFinal> { return this.microphone.stop(); }\r\n cancel(): Promise<void> { return this.microphone.cancel(); }\r\n\r\n ngOnDestroy(): void {\r\n this.unsubscribe();\r\n this.eventSubject.complete();\r\n void this.microphone.dispose();\r\n }\r\n}\r\n\r\nexport function provideSpeaking(config: SpeakingConfig): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useValue: config }, SpeakingService];\r\n}\r\n\r\n/** Configure once at app bootstrap; every fsel-speaking component owns its session. */\r\nexport function provideSpeakingGateway(options: SpeakingGatewayOptions = {}): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingGateway(options) }];\r\n}\r\n\r\n/** Application identity only; hosted endpoint and short-lived sessions are automatic. */\r\nexport function provideSpeakingApp(options: { appKey: string }): Provider[] {\r\n return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingService(options) }];\r\n}\r\n\r\n\r\n\r\nexport * from '@hoanghong/speaking-core';\r\n","import { Component, DestroyRef, EventEmitter, Input, OnInit, Output, inject } from '@angular/core';\r\nimport { takeUntilDestroyed } from '@angular/core/rxjs-interop';\r\nimport { Job, Language, SpeakingResult, SpeakingServiceType, SpeakingEvent } from '@hoanghong/speaking-core';\r\nimport { SpeakingService } from './speaking.service';\r\n\r\n@Component({\r\n selector: 'fsel-speaking', standalone: true, providers: [SpeakingService],\r\n template: `\r\n <section aria-label=\"FSEL Speaking\">\r\n <label>Ngôn ngữ <select [value]=\"language\" [disabled]=\"busy\" (change)=\"selectLanguage($event)\">\r\n @for (item of languages; track item.code) { <option [value]=\"item.code\" [selected]=\"item.code === language\">{{ item.name }}</option> }\r\n </select></label>\r\n <div class=\"actions\">\r\n @if (serviceType !== 'upload') {\r\n <button type=\"button\" (click)=\"start()\" [disabled]=\"busy\">Bắt đầu nói</button>\r\n <button type=\"button\" (click)=\"stop()\" [disabled]=\"!recording\">Dừng và lấy kết quả</button>\r\n }\r\n <button type=\"button\" (click)=\"cancel()\" [disabled]=\"!busy || (uploading && !job)\">Hủy</button>\r\n </div>\r\n @if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)\r\n <input type=\"file\" accept=\"audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm\" [disabled]=\"busy\" (change)=\"upload($event)\">\r\n </label> }\r\n <p role=\"status\">{{ status }}</p>\r\n @if (error) { <p role=\"alert\" class=\"error\">{{ error }}</p> }\r\n <label>Văn bản nhận dạng <textarea rows=\"7\" readonly [value]=\"text\"></textarea></label>\r\n </section>`,\r\n styles: [`:host{display:block;font:inherit;color:inherit}section{border:1px solid #b9c1bc;padding:20px;border-radius:4px}label{display:grid;gap:8px;margin-bottom:16px}select,input,textarea,button{font:inherit;box-sizing:border-box;max-width:100%}select,input,textarea{padding:10px;border:1px solid #929c95;border-radius:3px;background:#fff;color:#202723}textarea{width:100%;resize:vertical}.actions{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:16px}button{min-height:44px;padding:8px 14px;background:#365448;color:white;border:1px solid #365448;border-radius:4px;cursor:pointer}button:disabled{opacity:.5;cursor:default}button:focus-visible,input:focus-visible,select:focus-visible,textarea:focus-visible{outline:3px solid #a66a15;outline-offset:2px}.error{color:#a02020;overflow-wrap:anywhere}`],\r\n})\r\nexport class FselSpeakingComponent implements OnInit {\r\n @Input() initialLanguage: Language = 'vi';\r\n @Input() uploadEnabled = true;\r\n @Input() serviceType?: SpeakingServiceType;\r\n @Output() completed = new EventEmitter<SpeakingResult>();\r\n @Output() failed = new EventEmitter<Error>();\r\n readonly speaking = inject(SpeakingService);\r\n private readonly destroy = inject(DestroyRef);\r\n readonly languages = [{ code: 'en', name: 'Tiếng Anh' }, { code: 'ja', name: 'Tiếng Nhật' }, { code: 'ko', name: 'Tiếng Hàn' }, { code: 'zh', name: 'Tiếng Trung' }, { code: 'vi', name: 'Tiếng Việt' }];\r\n language: Language = 'vi'; busy = false; recording = false; uploading = false;\r\n text = ''; error = ''; status = 'Chọn ngôn ngữ, sau đó bấm nói hoặc chọn file.';\r\n job?: Job; private generation = 0; private abort?: AbortController;\r\n constructor() {\r\n this.speaking.events$.pipe(takeUntilDestroyed(this.destroy)).subscribe((event: SpeakingEvent) => {\r\n if (event.type === 'partial') this.text = event.text;\r\n if (event.type === 'state') this.recording = event.state === 'recording';\r\n if (event.type === 'clientError') { this.report(event.error); this.busy = false; this.recording = false; }\r\n });\r\n this.destroy.onDestroy(() => { ++this.generation; this.abort?.abort(); });\r\n }\r\n ngOnInit(): void { this.language = this.initialLanguage; }\r\n selectLanguage(event: Event): void { this.language = (event.target as HTMLSelectElement).value as Language; }\r\n private report(error: unknown): void {\r\n const value = error instanceof Error ? error : new Error('Speaking failed');\r\n this.error = value.message; this.failed.emit(value);\r\n }\r\n async start(): Promise<void> {\r\n if (this.busy) return;\r\n const generation = ++this.generation;\r\n this.busy = true; this.uploading = false; this.error = ''; this.text = ''; this.status = 'Đang mở microphone và chuẩn bị model…';\r\n try { await this.speaking.start({ language: this.language }); if (generation === this.generation) this.status = 'Đang nghe…'; }\r\n catch (error) { if (generation === this.generation) { this.report(error); this.busy = false; } }\r\n }\r\n async stop(): Promise<void> {\r\n if (!this.recording) return;\r\n const generation = this.generation;\r\n this.recording = false; this.status = 'Đang lấy kết quả…';\r\n try {\r\n const result = await this.speaking.stop();\r\n if (generation !== this.generation) return;\r\n this.text = result.result?.text ?? ''; this.status = result.status === 'no_speech' ? 'Không nhận thấy lời nói.' : 'Đã hoàn tất.';\r\n this.completed.emit({ ...result, language: this.language, text: this.text, audio: result.audio, serviceType: 'realtime' });\r\n } catch (error) { if (generation === this.generation) this.report(error); }\r\n finally { if (generation === this.generation) this.busy = false; }\r\n }\r\n async cancel(): Promise<void> {\r\n ++this.generation; this.abort?.abort();\r\n try { if (this.uploading && this.job) await this.speaking.batch.cancelJob(this.job.job_id); else await this.speaking.cancel(); this.status = 'Đã hủy.'; }\r\n catch (error) { this.report(error); }\r\n finally { this.busy = false; this.recording = false; this.uploading = false; }\r\n }\r\n async upload(event: Event): Promise<void> {\r\n const input = event.target as HTMLInputElement; const file = input.files?.[0]; input.value = '';\r\n if (!file || this.busy) return;\r\n if (file.size > 25 * 1024 * 1024) { this.report(new Error('File tối đa 25 MB.')); return; }\r\n const generation = ++this.generation; this.abort = new AbortController();\r\n this.busy = true; this.uploading = true; this.job = undefined; this.text = ''; this.error = ''; this.status = 'Đang upload…';\r\n try {\r\n let job = await this.speaking.batch.submitAudio(file, this.language, crypto.randomUUID(), 'short_answer', file.name, this.abort.signal);\r\n const deadline = Date.now() + 10 * 60 * 1000;\r\n while (generation === this.generation) {\r\n this.job = job;\r\n if (['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status)) break;\r\n if (Date.now() > deadline) throw new Error('Hết thời gian chờ; job có thể vẫn đang chạy trên server.');\r\n this.status = 'Đang nhận dạng file…';\r\n await new Promise(resolve => setTimeout(resolve, 1500));\r\n if (generation !== this.generation) return;\r\n job = await this.speaking.batch.getResult(job.job_id, this.abort.signal);\r\n }\r\n if (generation !== this.generation) return;\r\n if (job.error) throw new Error(job.error.message);\r\n this.text = job.result?.text ?? ''; this.status = job.status === 'no_speech' ? 'Không nhận thấy lời nói.' : job.status === 'cancelled' ? 'Đã hủy.' : 'Đã hoàn tất.';\r\n if (job.status !== 'cancelled') this.completed.emit({ ...job, language: this.language, text: this.text, audio: file, serviceType: 'upload' });\r\n } catch (error) { if (generation === this.generation) this.report(error); }\r\n finally { if (generation === this.generation) { this.busy = false; this.uploading = false; } }\r\n }\r\n}\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoanghong/speaking-angular",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Ready-to-use online speaking: five languages, microphone realtime and audio upload",
5
5
  "type": "module",
6
6
  "peerDependencies": {
@@ -11,7 +11,7 @@
11
11
  "sideEffects": false,
12
12
  "license": "MIT",
13
13
  "dependencies": {
14
- "@hoanghong/speaking-core": "^0.3.0",
14
+ "@hoanghong/speaking-core": "^0.4.0",
15
15
  "tslib": "^2.3.0"
16
16
  },
17
17
  "publishConfig": {
@@ -1,16 +1,18 @@
1
1
  import { InjectionToken, NgZone, OnDestroy, Provider } from '@angular/core';
2
- import { BatchOptions, BatchSpeakingClient, MicrophoneOptions, RecordedSpeakingFinal, RealtimeOptions, RealtimeSpeakingClient, SpeakingEvent, StartOptions, SpeakingGatewayOptions } from '@hoanghong/speaking-core';
2
+ import { BatchOptions, BatchSpeakingClient, MicrophoneOptions, RecordedSpeakingFinal, RealtimeOptions, RealtimeSpeakingClient, SpeakingEvent, StartOptions, SpeakingGatewayOptions, PronunciationClient, PronunciationOptions } from '@hoanghong/speaking-core';
3
3
  import * as i0 from "@angular/core";
4
4
  export interface SpeakingConfig {
5
5
  batch: BatchOptions;
6
6
  realtime: RealtimeOptions;
7
7
  microphone: MicrophoneOptions;
8
+ pronunciation?: PronunciationOptions;
8
9
  }
9
10
  export declare const SPEAKING_CONFIG: InjectionToken<SpeakingConfig>;
10
11
  /** Provide at the screen/component level to give each speaking screen its own lifecycle. */
11
12
  export declare class SpeakingService implements OnDestroy {
12
13
  private readonly zone;
13
14
  readonly batch: BatchSpeakingClient;
15
+ readonly pronunciation: PronunciationClient;
14
16
  readonly realtime: RealtimeSpeakingClient;
15
17
  private readonly eventSubject;
16
18
  readonly events$: import("rxjs").Observable<SpeakingEvent>;