@hoanghong/speaking-angular 0.1.0 → 0.3.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 +22 -0
- package/README.md +115 -12
- package/esm2022/public-api.mjs +3 -45
- package/esm2022/speaking.component.mjs +214 -0
- package/esm2022/speaking.service.mjs +55 -0
- package/fesm2022/hoanghong-speaking-angular.mjs +225 -4
- package/fesm2022/hoanghong-speaking-angular.mjs.map +1 -1
- package/package.json +3 -3
- package/public-api.d.ts +2 -28
- package/speaking.component.d.ts +37 -0
- package/speaking.service.d.ts +34 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Per-application keys with server-managed quota; appKey setup without a service URL.
|
|
6
|
+
|
|
7
|
+
- Hosted public service configuration with automatic short-lived sessions and quota errors.
|
|
8
|
+
- Select realtime/upload using serviceType; no URL or token setup for hosted use.
|
|
9
|
+
- Completed callback includes text and audio: recorded PCM as WAV, or original uploaded file.
|
|
10
|
+
- Bounded five-minute recording buffer and preserved final residual audio.
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## 0.2.0
|
|
14
|
+
|
|
15
|
+
- Add standalone FselSpeakingComponent and provideSpeakingGateway.
|
|
16
|
+
- Include microphone, upload, five languages, cancellation and result/error outputs.
|
|
17
|
+
- Document installation, worklet assets, application bootstrap, gateway contract and troubleshooting with full code.
|
|
18
|
+
- Preserve existing low-level 0.1 APIs.
|
|
19
|
+
|
|
20
|
+
## 0.1.0
|
|
21
|
+
|
|
22
|
+
- Initial Angular service and core SDK integration.
|
package/README.md
CHANGED
|
@@ -1,20 +1,123 @@
|
|
|
1
1
|
# @hoanghong/speaking-angular
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
Provide `SPEAKING_CONFIG` trong app, `SpeakingService` ở screen/component. Theo dõi `events$` và dùng
|
|
5
|
-
`start()`, `stop()`, `cancel()`; `batch` cung cấp SDK upload/poll/cancel job.
|
|
6
|
-
`ngOnDestroy` dọn microphone và socket. Không chứa credential ASR, model hoặc logic chấm điểm.
|
|
3
|
+
Component nhận dạng lời nói online dùng ngay. Chỉ cài package, chọn `serviceType`, ngôn ngữ và nhận callback **text + file audio**. Không khai báo URL, không tạo gateway, không nhập token, không copy worklet. Model chạy tại dịch vụ FSEL, không tải xuống máy người dùng. MIT. Angular 18.x, RxJS 7.8.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
## 1. Cài đặt
|
|
9
6
|
|
|
10
7
|
```bash
|
|
11
|
-
npm
|
|
12
|
-
npm run build
|
|
13
|
-
cd dist
|
|
14
|
-
npm pack
|
|
8
|
+
npm install @hoanghong/speaking-angular@^0.3.0
|
|
15
9
|
```
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
Xem `docs/realtime-and-sdk.md` ở repository root cho cấu hình ticketProvider và ví dụ tích hợp.
|
|
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.
|
|
19
12
|
|
|
20
|
-
|
|
13
|
+
## 2. Thêm component — ví dụ đầy đủ
|
|
14
|
+
|
|
15
|
+
Thay `src/app/app.component.ts` bằng:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Component, OnDestroy } from '@angular/core';
|
|
19
|
+
import { FselSpeakingComponent, SpeakingResult } from '@hoanghong/speaking-angular';
|
|
20
|
+
|
|
21
|
+
@Component({
|
|
22
|
+
selector: 'app-root', standalone: true,
|
|
23
|
+
imports: [FselSpeakingComponent],
|
|
24
|
+
template: `
|
|
25
|
+
<main style="max-width:800px;margin:auto;padding:20px">
|
|
26
|
+
<h1>Luyện nói</h1>
|
|
27
|
+
<fsel-speaking serviceType="realtime" initialLanguage="vi"
|
|
28
|
+
(completed)="onCompleted($event)" (failed)="onFailed($event)" />
|
|
29
|
+
<p>{{ text }}</p>
|
|
30
|
+
@if (audioUrl) {
|
|
31
|
+
<audio controls [src]="audioUrl"></audio>
|
|
32
|
+
<p><a [href]="audioUrl" [download]="filename">Tải file bài nói</a></p>
|
|
33
|
+
}
|
|
34
|
+
<p role="alert">{{ error }}</p>
|
|
35
|
+
</main>
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
export class AppComponent implements OnDestroy {
|
|
39
|
+
text = ''; error = ''; filename = ''; audioUrl = '';
|
|
40
|
+
onCompleted(result: SpeakingResult): void {
|
|
41
|
+
if (this.audioUrl) URL.revokeObjectURL(this.audioUrl);
|
|
42
|
+
this.text = result.text;
|
|
43
|
+
this.filename = result.audio.name;
|
|
44
|
+
this.audioUrl = URL.createObjectURL(result.audio);
|
|
45
|
+
// result.audio là File: có thể đưa vào FormData để lưu ở hệ thống của bạn.
|
|
46
|
+
}
|
|
47
|
+
onFailed(error: Error): void { this.error = error.message; }
|
|
48
|
+
ngOnDestroy(): void { if (this.audioUrl) URL.revokeObjectURL(this.audioUrl); }
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Giữ `src/main.ts` chuẩn Angular CLI; nếu cần tạo lại:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
56
|
+
import { AppComponent } from './app/app.component';
|
|
57
|
+
bootstrapApplication(AppComponent).catch(console.error);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Không cần `provideSpeakingGateway` hoặc chỉnh assets trong `angular.json` khi dùng dịch vụ public. Nếu nâng cấp từ 0.2, bỏ provider gateway cũ để sử dụng dịch vụ public mặc định.
|
|
61
|
+
|
|
62
|
+
## Key riêng của ứng dụng (cấu hình một lần)
|
|
63
|
+
|
|
64
|
+
Nếu được FSEL cấp key riêng, thêm vào `src/app/app.config.ts`:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { ApplicationConfig } from '@angular/core';
|
|
68
|
+
import { provideSpeakingApp } from '@hoanghong/speaking-angular';
|
|
69
|
+
export const appConfig: ApplicationConfig = {
|
|
70
|
+
providers: [provideSpeakingApp({ appKey: 'KEY_DUOC_FSEL_CAP' })],
|
|
71
|
+
};
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Khi có appConfig, `main.ts` cần truyền nó cho bootstrap:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { bootstrapApplication } from '@angular/platform-browser';
|
|
78
|
+
import { AppComponent } from './app/app.component';
|
|
79
|
+
import { appConfig } from './app/app.config';
|
|
80
|
+
bootstrapApplication(AppComponent, appConfig).catch(console.error);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Component và callback text/audio không đổi; không cần URL. Đây là key định danh có thể quan sát trong frontend, không phải khóa quản trị. Hạn mức của key được FSEL quản lý phía server, thay đổi không cần build lại ứng dụng. Key sai/bị khóa trả INVALID_APP_KEY; không tự chuyển sang dùng ẩn danh. Bỏ cấu hình key thì dùng hạn mức public cơ bản.
|
|
84
|
+
|
|
85
|
+
## 3. Chạy
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
npx ng serve
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Mở localhost do CLI in ra → chọn ngôn ngữ → Bắt đầu nói → cho phép microphone → chờ “Đang nghe…” → nói → Dừng và lấy kết quả. Callback nhận text và WAV, ví dụ trên cho nghe lại/tải file. Production phải dùng HTTPS. Không ghi âm tự động.
|
|
92
|
+
|
|
93
|
+
## 4. Chọn dịch vụ
|
|
94
|
+
|
|
95
|
+
- `serviceType="realtime"`: microphone, text tạm khi nói, kết quả cuối kèm WAV PCM16 mono 16 kHz.
|
|
96
|
+
- `serviceType="upload"`: chọn file audio, trả transcript và chính File đầu vào, giữ tên/định dạng.
|
|
97
|
+
- Không đặt `serviceType`: hiện cả hai cách. `uploadEnabled=false` ẩn upload.
|
|
98
|
+
- `initialLanguage`: `en` (Anh), `ja` (Nhật), `ko` (Hàn), `zh` (Trung), `vi` (Việt, mặc định).
|
|
99
|
+
|
|
100
|
+
Ví dụ upload: thay `serviceType="realtime"` bằng `serviceType="upload"`. Callback không đổi.
|
|
101
|
+
|
|
102
|
+
## 5. Kết quả trả về
|
|
103
|
+
|
|
104
|
+
`completed` nhận `SpeakingResult`:
|
|
105
|
+
|
|
106
|
+
| Trường | Kiểu / ý nghĩa |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `text` | String nhận dạng cuối; rỗng khi không có lời nói |
|
|
109
|
+
| `audio` | File, dùng createObjectURL/FormData/arrayBuffer; realtime có tên speaking.wav |
|
|
110
|
+
| `language` | en/ja/ko/zh/vi |
|
|
111
|
+
| `serviceType` | realtime/upload |
|
|
112
|
+
| `status` | succeeded/no_speech; các metadata gốc vẫn được giữ |
|
|
113
|
+
| `result` | Transcript chi tiết từ server nếu có |
|
|
114
|
+
|
|
115
|
+
File realtime chứa âm thanh người dùng vừa nói, đã chuẩn hóa cho nhận dạng; không phải giọng AI đọc text. File không tự được lưu vĩnh viễn lên S3 hoặc cấp URL tải công khai. Ứng dụng sở hữu File sau callback, tự lưu nếu cần. Revoke object URL khi thay file/rời trang như ví dụ để tránh giữ RAM. Hủy/lỗi không phát callback thành công.
|
|
116
|
+
|
|
117
|
+
## 6. Giới hạn và lỗi
|
|
118
|
+
|
|
119
|
+
Dịch vụ public tự cấp phiên ngắn hạn. Mặc định server giới hạn 5 lượt/IP/ngày và 100 lượt/ngày cho mỗi instance, tính theo UTC; hạn mức có thể được FSEL điều chỉnh. Một phiên cấp mới tính một lượt kể cả khi nhận dạng thất bại. Người dùng cùng mạng có thể chung IP. Realtime tối đa 5 phút; buffer PCM tối đa khoảng 9,6 MB trước khi tạo WAV. Upload UI tối đa 25 MiB, server có thể giới hạn thấp hơn; chờ job tối đa 10 phút. Không tự reconnect/replay audio sau mất mạng. Rời màn ngừng thu/polling; job đã upload có thể còn chạy trên server.
|
|
120
|
+
|
|
121
|
+
`failed` nhận Error; lỗi dịch vụ có `code`/`status` qua SpeakingError. QUOTA_EXCEEDED/429: hết lượt, chờ ngày mới. 503: dịch vụ/model chưa sẵn sàng. 404: server chưa triển khai phiên bản dịch vụ public. Microphone denied: bật quyền site và dùng HTTPS/localhost. Mạng/CSP: cho phép kết nối, worklet tới dịch vụ FSEL theo chính sách ứng dụng. Server phải chạy bản hỗ trợ public SDK trước khi dùng 0.3.
|
|
122
|
+
|
|
123
|
+
API thấp của 0.2 vẫn có sẵn cho tích hợp tùy chỉnh; không cần dùng trong luồng trên. Microphone thiết bị và khả năng truy cập mạng cần kiểm tra trong ứng dụng triển khai thực tế.
|
package/esm2022/public-api.mjs
CHANGED
|
@@ -1,45 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import * as i0 from "@angular/core";
|
|
5
|
-
export const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG');
|
|
6
|
-
/** Provide at the screen/component level to give each speaking screen its own lifecycle. */
|
|
7
|
-
export class SpeakingService {
|
|
8
|
-
zone;
|
|
9
|
-
batch;
|
|
10
|
-
realtime;
|
|
11
|
-
eventSubject = new Subject();
|
|
12
|
-
events$ = this.eventSubject.asObservable();
|
|
13
|
-
microphone;
|
|
14
|
-
unsubscribe;
|
|
15
|
-
constructor(config, zone) {
|
|
16
|
-
this.zone = zone;
|
|
17
|
-
this.batch = new BatchSpeakingClient(config.batch);
|
|
18
|
-
this.realtime = new RealtimeSpeakingClient(config.realtime);
|
|
19
|
-
this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);
|
|
20
|
-
this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));
|
|
21
|
-
}
|
|
22
|
-
start(options) {
|
|
23
|
-
return this.zone.runOutsideAngular(() => this.microphone.start(options));
|
|
24
|
-
}
|
|
25
|
-
stop() { return this.microphone.stop(); }
|
|
26
|
-
cancel() { return this.microphone.cancel(); }
|
|
27
|
-
ngOnDestroy() {
|
|
28
|
-
this.unsubscribe();
|
|
29
|
-
this.eventSubject.complete();
|
|
30
|
-
void this.microphone.dispose();
|
|
31
|
-
}
|
|
32
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService, deps: [{ token: SPEAKING_CONFIG }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
33
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService });
|
|
34
|
-
}
|
|
35
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService, decorators: [{
|
|
36
|
-
type: Injectable
|
|
37
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
38
|
-
type: Inject,
|
|
39
|
-
args: [SPEAKING_CONFIG]
|
|
40
|
-
}] }, { type: i0.NgZone }] });
|
|
41
|
-
export function provideSpeaking(config) {
|
|
42
|
-
return [{ provide: SPEAKING_CONFIG, useValue: config }, SpeakingService];
|
|
43
|
-
}
|
|
44
|
-
export * from '@hoanghong/speaking-core';
|
|
45
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wdWJsaWMtYXBpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLGNBQWMsRUFBK0IsTUFBTSxlQUFlLENBQUM7QUFDaEcsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUMvQixPQUFPLEVBQ1MsbUJBQW1CLEVBQ2pDLHNCQUFzQixFQUFpQixrQkFBa0IsR0FDMUQsTUFBTSwwQkFBMEIsQ0FBQzs7QUFRbEMsTUFBTSxDQUFDLE1BQU0sZUFBZSxHQUFHLElBQUksY0FBYyxDQUFpQixzQkFBc0IsQ0FBQyxDQUFDO0FBRTFGLDRGQUE0RjtBQUU1RixNQUFNLE9BQU8sZUFBZTtJQVFvRDtJQVByRSxLQUFLLENBQXNCO0lBQzNCLFFBQVEsQ0FBeUI7SUFDekIsWUFBWSxHQUFHLElBQUksT0FBTyxFQUFpQixDQUFDO0lBQ3BELE9BQU8sR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRSxDQUFDO0lBQ25DLFVBQVUsQ0FBcUI7SUFDL0IsV0FBVyxDQUFhO0lBRXpDLFlBQXFDLE1BQXNCLEVBQW1CLElBQVk7UUFBWixTQUFJLEdBQUosSUFBSSxDQUFRO1FBQ3hGLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbkQsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLHNCQUFzQixDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUM1RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksa0JBQWtCLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0UsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN4RyxDQUFDO0lBRUQsS0FBSyxDQUFDLE9BQXFCO1FBQ3pCLE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFFRCxJQUFJLEtBQTZCLE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDakUsTUFBTSxLQUFvQixPQUFPLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBRTVELFdBQVc7UUFDVCxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDbkIsSUFBSSxDQUFDLFlBQVksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUM3QixLQUFLLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDakMsQ0FBQzt3R0ExQlUsZUFBZSxrQkFRTixlQUFlOzRHQVJ4QixlQUFlOzs0RkFBZixlQUFlO2tCQUQzQixVQUFVOzswQkFTSSxNQUFNOzJCQUFDLGVBQWU7O0FBcUJyQyxNQUFNLFVBQVUsZUFBZSxDQUFDLE1BQXNCO0lBQ3BELE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxFQUFFLGVBQWUsQ0FBQyxDQUFDO0FBQzNFLENBQUM7QUFFRCxjQUFjLDBCQUEwQixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgSW5qZWN0LCBJbmplY3RhYmxlLCBJbmplY3Rpb25Ub2tlbiwgTmdab25lLCBPbkRlc3Ryb3ksIFByb3ZpZGVyIH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IFN1YmplY3QgfSBmcm9tICdyeGpzJztcclxuaW1wb3J0IHtcclxuICBCYXRjaE9wdGlvbnMsIEJhdGNoU3BlYWtpbmdDbGllbnQsIE1pY3JvcGhvbmVPcHRpb25zLCBSZWFsdGltZUZpbmFsLCBSZWFsdGltZU9wdGlvbnMsXHJcbiAgUmVhbHRpbWVTcGVha2luZ0NsaWVudCwgU3BlYWtpbmdFdmVudCwgU3BlYWtpbmdNaWNyb3Bob25lLCBTdGFydE9wdGlvbnMsXHJcbn0gZnJvbSAnQGhvYW5naG9uZy9zcGVha2luZy1jb3JlJztcclxuXHJcbmV4cG9ydCBpbnRlcmZhY2UgU3BlYWtpbmdDb25maWcge1xyXG4gIGJhdGNoOiBCYXRjaE9wdGlvbnM7XHJcbiAgcmVhbHRpbWU6IFJlYWx0aW1lT3B0aW9ucztcclxuICBtaWNyb3Bob25lOiBNaWNyb3Bob25lT3B0aW9ucztcclxufVxyXG5cclxuZXhwb3J0IGNvbnN0IFNQRUFLSU5HX0NPTkZJRyA9IG5ldyBJbmplY3Rpb25Ub2tlbjxTcGVha2luZ0NvbmZpZz4oJ0ZTRUxfU1BFQUtJTkdfQ09ORklHJyk7XHJcblxyXG4vKiogUHJvdmlkZSBhdCB0aGUgc2NyZWVuL2NvbXBvbmVudCBsZXZlbCB0byBnaXZlIGVhY2ggc3BlYWtpbmcgc2NyZWVuIGl0cyBvd24gbGlmZWN5Y2xlLiAqL1xyXG5ASW5qZWN0YWJsZSgpXHJcbmV4cG9ydCBjbGFzcyBTcGVha2luZ1NlcnZpY2UgaW1wbGVtZW50cyBPbkRlc3Ryb3kge1xyXG4gIHJlYWRvbmx5IGJhdGNoOiBCYXRjaFNwZWFraW5nQ2xpZW50O1xyXG4gIHJlYWRvbmx5IHJlYWx0aW1lOiBSZWFsdGltZVNwZWFraW5nQ2xpZW50O1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgZXZlbnRTdWJqZWN0ID0gbmV3IFN1YmplY3Q8U3BlYWtpbmdFdmVudD4oKTtcclxuICByZWFkb25seSBldmVudHMkID0gdGhpcy5ldmVudFN1YmplY3QuYXNPYnNlcnZhYmxlKCk7XHJcbiAgcHJpdmF0ZSByZWFkb25seSBtaWNyb3Bob25lOiBTcGVha2luZ01pY3JvcGhvbmU7XHJcbiAgcHJpdmF0ZSByZWFkb25seSB1bnN1YnNjcmliZTogKCkgPT4gdm9pZDtcclxuXHJcbiAgY29uc3RydWN0b3IoQEluamVjdChTUEVBS0lOR19DT05GSUcpIGNvbmZpZzogU3BlYWtpbmdDb25maWcsIHByaXZhdGUgcmVhZG9ubHkgem9uZTogTmdab25lKSB7XHJcbiAgICB0aGlzLmJhdGNoID0gbmV3IEJhdGNoU3BlYWtpbmdDbGllbnQoY29uZmlnLmJhdGNoKTtcclxuICAgIHRoaXMucmVhbHRpbWUgPSBuZXcgUmVhbHRpbWVTcGVha2luZ0NsaWVudChjb25maWcucmVhbHRpbWUpO1xyXG4gICAgdGhpcy5taWNyb3Bob25lID0gbmV3IFNwZWFraW5nTWljcm9waG9uZSh0aGlzLnJlYWx0aW1lLCBjb25maWcubWljcm9waG9uZSk7XHJcbiAgICB0aGlzLnVuc3Vic2NyaWJlID0gdGhpcy5yZWFsdGltZS5vbkV2ZW50KGV2ZW50ID0+IHRoaXMuem9uZS5ydW4oKCkgPT4gdGhpcy5ldmVudFN1YmplY3QubmV4dChldmVudCkpKTtcclxuICB9XHJcblxyXG4gIHN0YXJ0KG9wdGlvbnM6IFN0YXJ0T3B0aW9ucyk6IFByb21pc2U8dm9pZD4ge1xyXG4gICAgcmV0dXJuIHRoaXMuem9uZS5ydW5PdXRzaWRlQW5ndWxhcigoKSA9PiB0aGlzLm1pY3JvcGhvbmUuc3RhcnQob3B0aW9ucykpO1xyXG4gIH1cclxuXHJcbiAgc3RvcCgpOiBQcm9taXNlPFJlYWx0aW1lRmluYWw+IHsgcmV0dXJuIHRoaXMubWljcm9waG9uZS5zdG9wKCk7IH1cclxuICBjYW5jZWwoKTogUHJvbWlzZTx2b2lkPiB7IHJldHVybiB0aGlzLm1pY3JvcGhvbmUuY2FuY2VsKCk7IH1cclxuXHJcbiAgbmdPbkRlc3Ryb3koKTogdm9pZCB7XHJcbiAgICB0aGlzLnVuc3Vic2NyaWJlKCk7XHJcbiAgICB0aGlzLmV2ZW50U3ViamVjdC5jb21wbGV0ZSgpO1xyXG4gICAgdm9pZCB0aGlzLm1pY3JvcGhvbmUuZGlzcG9zZSgpO1xyXG4gIH1cclxufVxyXG5cclxuZXhwb3J0IGZ1bmN0aW9uIHByb3ZpZGVTcGVha2luZyhjb25maWc6IFNwZWFraW5nQ29uZmlnKTogUHJvdmlkZXJbXSB7XHJcbiAgcmV0dXJuIFt7IHByb3ZpZGU6IFNQRUFLSU5HX0NPTkZJRywgdXNlVmFsdWU6IGNvbmZpZyB9LCBTcGVha2luZ1NlcnZpY2VdO1xyXG59XHJcblxyXG5leHBvcnQgKiBmcm9tICdAaG9hbmdob25nL3NwZWFraW5nLWNvcmUnO1xyXG4iXX0=
|
|
1
|
+
export * from './speaking.service';
|
|
2
|
+
export * from './speaking.component';
|
|
3
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicHVibGljLWFwaS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9wdWJsaWMtYXBpLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLGNBQWMsb0JBQW9CLENBQUM7QUFDbkMsY0FBYyxzQkFBc0IsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCAqIGZyb20gJy4vc3BlYWtpbmcuc2VydmljZSc7XHJcbmV4cG9ydCAqIGZyb20gJy4vc3BlYWtpbmcuY29tcG9uZW50JztcclxuIl19
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { Component, DestroyRef, EventEmitter, Input, Output, inject } from '@angular/core';
|
|
2
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
3
|
+
import { SpeakingService } from './speaking.service';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
export class FselSpeakingComponent {
|
|
6
|
+
initialLanguage = 'vi';
|
|
7
|
+
uploadEnabled = true;
|
|
8
|
+
serviceType;
|
|
9
|
+
completed = new EventEmitter();
|
|
10
|
+
failed = new EventEmitter();
|
|
11
|
+
speaking = inject(SpeakingService);
|
|
12
|
+
destroy = inject(DestroyRef);
|
|
13
|
+
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' }];
|
|
14
|
+
language = 'vi';
|
|
15
|
+
busy = false;
|
|
16
|
+
recording = false;
|
|
17
|
+
uploading = false;
|
|
18
|
+
text = '';
|
|
19
|
+
error = '';
|
|
20
|
+
status = 'Chọn ngôn ngữ, sau đó bấm nói hoặc chọn file.';
|
|
21
|
+
job;
|
|
22
|
+
generation = 0;
|
|
23
|
+
abort;
|
|
24
|
+
constructor() {
|
|
25
|
+
this.speaking.events$.pipe(takeUntilDestroyed(this.destroy)).subscribe((event) => {
|
|
26
|
+
if (event.type === 'partial')
|
|
27
|
+
this.text = event.text;
|
|
28
|
+
if (event.type === 'state')
|
|
29
|
+
this.recording = event.state === 'recording';
|
|
30
|
+
if (event.type === 'clientError') {
|
|
31
|
+
this.report(event.error);
|
|
32
|
+
this.busy = false;
|
|
33
|
+
this.recording = false;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
this.destroy.onDestroy(() => { ++this.generation; this.abort?.abort(); });
|
|
37
|
+
}
|
|
38
|
+
ngOnInit() { this.language = this.initialLanguage; }
|
|
39
|
+
selectLanguage(event) { this.language = event.target.value; }
|
|
40
|
+
report(error) {
|
|
41
|
+
const value = error instanceof Error ? error : new Error('Speaking failed');
|
|
42
|
+
this.error = value.message;
|
|
43
|
+
this.failed.emit(value);
|
|
44
|
+
}
|
|
45
|
+
async start() {
|
|
46
|
+
if (this.busy)
|
|
47
|
+
return;
|
|
48
|
+
const generation = ++this.generation;
|
|
49
|
+
this.busy = true;
|
|
50
|
+
this.uploading = false;
|
|
51
|
+
this.error = '';
|
|
52
|
+
this.text = '';
|
|
53
|
+
this.status = 'Đang mở microphone và chuẩn bị model…';
|
|
54
|
+
try {
|
|
55
|
+
await this.speaking.start({ language: this.language });
|
|
56
|
+
if (generation === this.generation)
|
|
57
|
+
this.status = 'Đang nghe…';
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
if (generation === this.generation) {
|
|
61
|
+
this.report(error);
|
|
62
|
+
this.busy = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async stop() {
|
|
67
|
+
if (!this.recording)
|
|
68
|
+
return;
|
|
69
|
+
const generation = this.generation;
|
|
70
|
+
this.recording = false;
|
|
71
|
+
this.status = 'Đang lấy kết quả…';
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.speaking.stop();
|
|
74
|
+
if (generation !== this.generation)
|
|
75
|
+
return;
|
|
76
|
+
this.text = result.result?.text ?? '';
|
|
77
|
+
this.status = result.status === 'no_speech' ? 'Không nhận thấy lời nói.' : 'Đã hoàn tất.';
|
|
78
|
+
this.completed.emit({ ...result, language: this.language, text: this.text, audio: result.audio, serviceType: 'realtime' });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (generation === this.generation)
|
|
82
|
+
this.report(error);
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
if (generation === this.generation)
|
|
86
|
+
this.busy = false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async cancel() {
|
|
90
|
+
++this.generation;
|
|
91
|
+
this.abort?.abort();
|
|
92
|
+
try {
|
|
93
|
+
if (this.uploading && this.job)
|
|
94
|
+
await this.speaking.batch.cancelJob(this.job.job_id);
|
|
95
|
+
else
|
|
96
|
+
await this.speaking.cancel();
|
|
97
|
+
this.status = 'Đã hủy.';
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
this.report(error);
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
this.busy = false;
|
|
104
|
+
this.recording = false;
|
|
105
|
+
this.uploading = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async upload(event) {
|
|
109
|
+
const input = event.target;
|
|
110
|
+
const file = input.files?.[0];
|
|
111
|
+
input.value = '';
|
|
112
|
+
if (!file || this.busy)
|
|
113
|
+
return;
|
|
114
|
+
if (file.size > 25 * 1024 * 1024) {
|
|
115
|
+
this.report(new Error('File tối đa 25 MB.'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const generation = ++this.generation;
|
|
119
|
+
this.abort = new AbortController();
|
|
120
|
+
this.busy = true;
|
|
121
|
+
this.uploading = true;
|
|
122
|
+
this.job = undefined;
|
|
123
|
+
this.text = '';
|
|
124
|
+
this.error = '';
|
|
125
|
+
this.status = 'Đang upload…';
|
|
126
|
+
try {
|
|
127
|
+
let job = await this.speaking.batch.submitAudio(file, this.language, crypto.randomUUID(), 'short_answer', file.name, this.abort.signal);
|
|
128
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
129
|
+
while (generation === this.generation) {
|
|
130
|
+
this.job = job;
|
|
131
|
+
if (['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status))
|
|
132
|
+
break;
|
|
133
|
+
if (Date.now() > deadline)
|
|
134
|
+
throw new Error('Hết thời gian chờ; job có thể vẫn đang chạy trên server.');
|
|
135
|
+
this.status = 'Đang nhận dạng file…';
|
|
136
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
137
|
+
if (generation !== this.generation)
|
|
138
|
+
return;
|
|
139
|
+
job = await this.speaking.batch.getResult(job.job_id, this.abort.signal);
|
|
140
|
+
}
|
|
141
|
+
if (generation !== this.generation)
|
|
142
|
+
return;
|
|
143
|
+
if (job.error)
|
|
144
|
+
throw new Error(job.error.message);
|
|
145
|
+
this.text = job.result?.text ?? '';
|
|
146
|
+
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.';
|
|
147
|
+
if (job.status !== 'cancelled')
|
|
148
|
+
this.completed.emit({ ...job, language: this.language, text: this.text, audio: file, serviceType: 'upload' });
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (generation === this.generation)
|
|
152
|
+
this.report(error);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
if (generation === this.generation) {
|
|
156
|
+
this.busy = false;
|
|
157
|
+
this.uploading = false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FselSpeakingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
162
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FselSpeakingComponent, isStandalone: true, selector: "fsel-speaking", inputs: { initialLanguage: "initialLanguage", uploadEnabled: "uploadEnabled", serviceType: "serviceType" }, outputs: { completed: "completed", failed: "failed" }, providers: [SpeakingService], ngImport: i0, template: `
|
|
163
|
+
<section aria-label="FSEL Speaking">
|
|
164
|
+
<label>Ngôn ngữ <select [value]="language" [disabled]="busy" (change)="selectLanguage($event)">
|
|
165
|
+
@for (item of languages; track item.code) { <option [value]="item.code" [selected]="item.code === language">{{ item.name }}</option> }
|
|
166
|
+
</select></label>
|
|
167
|
+
<div class="actions">
|
|
168
|
+
@if (serviceType !== 'upload') {
|
|
169
|
+
<button type="button" (click)="start()" [disabled]="busy">Bắt đầu nói</button>
|
|
170
|
+
<button type="button" (click)="stop()" [disabled]="!recording">Dừng và lấy kết quả</button>
|
|
171
|
+
}
|
|
172
|
+
<button type="button" (click)="cancel()" [disabled]="!busy || (uploading && !job)">Hủy</button>
|
|
173
|
+
</div>
|
|
174
|
+
@if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)
|
|
175
|
+
<input type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm" [disabled]="busy" (change)="upload($event)">
|
|
176
|
+
</label> }
|
|
177
|
+
<p role="status">{{ status }}</p>
|
|
178
|
+
@if (error) { <p role="alert" class="error">{{ error }}</p> }
|
|
179
|
+
<label>Văn bản nhận dạng <textarea rows="7" readonly [value]="text"></textarea></label>
|
|
180
|
+
</section>`, isInline: true, 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:#fff;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}\n"] });
|
|
181
|
+
}
|
|
182
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FselSpeakingComponent, decorators: [{
|
|
183
|
+
type: Component,
|
|
184
|
+
args: [{ selector: 'fsel-speaking', standalone: true, providers: [SpeakingService], template: `
|
|
185
|
+
<section aria-label="FSEL Speaking">
|
|
186
|
+
<label>Ngôn ngữ <select [value]="language" [disabled]="busy" (change)="selectLanguage($event)">
|
|
187
|
+
@for (item of languages; track item.code) { <option [value]="item.code" [selected]="item.code === language">{{ item.name }}</option> }
|
|
188
|
+
</select></label>
|
|
189
|
+
<div class="actions">
|
|
190
|
+
@if (serviceType !== 'upload') {
|
|
191
|
+
<button type="button" (click)="start()" [disabled]="busy">Bắt đầu nói</button>
|
|
192
|
+
<button type="button" (click)="stop()" [disabled]="!recording">Dừng và lấy kết quả</button>
|
|
193
|
+
}
|
|
194
|
+
<button type="button" (click)="cancel()" [disabled]="!busy || (uploading && !job)">Hủy</button>
|
|
195
|
+
</div>
|
|
196
|
+
@if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)
|
|
197
|
+
<input type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm" [disabled]="busy" (change)="upload($event)">
|
|
198
|
+
</label> }
|
|
199
|
+
<p role="status">{{ status }}</p>
|
|
200
|
+
@if (error) { <p role="alert" class="error">{{ error }}</p> }
|
|
201
|
+
<label>Văn bản nhận dạng <textarea rows="7" readonly [value]="text"></textarea></label>
|
|
202
|
+
</section>`, 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:#fff;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}\n"] }]
|
|
203
|
+
}], ctorParameters: () => [], propDecorators: { initialLanguage: [{
|
|
204
|
+
type: Input
|
|
205
|
+
}], uploadEnabled: [{
|
|
206
|
+
type: Input
|
|
207
|
+
}], serviceType: [{
|
|
208
|
+
type: Input
|
|
209
|
+
}], completed: [{
|
|
210
|
+
type: Output
|
|
211
|
+
}], failed: [{
|
|
212
|
+
type: Output
|
|
213
|
+
}] } });
|
|
214
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BlYWtpbmcuY29tcG9uZW50LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vc3JjL3NwZWFraW5nLmNvbXBvbmVudC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxPQUFPLEVBQUUsU0FBUyxFQUFFLFVBQVUsRUFBRSxZQUFZLEVBQUUsS0FBSyxFQUFVLE1BQU0sRUFBRSxNQUFNLEVBQUUsTUFBTSxlQUFlLENBQUM7QUFDbkcsT0FBTyxFQUFFLGtCQUFrQixFQUFFLE1BQU0sNEJBQTRCLENBQUM7QUFFaEUsT0FBTyxFQUFFLGVBQWUsRUFBRSxNQUFNLG9CQUFvQixDQUFDOztBQXlCckQsTUFBTSxPQUFPLHFCQUFxQjtJQUN2QixlQUFlLEdBQWEsSUFBSSxDQUFDO0lBQ2pDLGFBQWEsR0FBRyxJQUFJLENBQUM7SUFDckIsV0FBVyxDQUF1QjtJQUNqQyxTQUFTLEdBQUcsSUFBSSxZQUFZLEVBQWtCLENBQUM7SUFDL0MsTUFBTSxHQUFHLElBQUksWUFBWSxFQUFTLENBQUM7SUFDcEMsUUFBUSxHQUFHLE1BQU0sQ0FBQyxlQUFlLENBQUMsQ0FBQztJQUMzQixPQUFPLEdBQUcsTUFBTSxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ3JDLFNBQVMsR0FBRyxDQUFDLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsV0FBVyxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsRUFBRSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLFdBQVcsRUFBRSxFQUFFLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxJQUFJLEVBQUUsYUFBYSxFQUFFLEVBQUUsRUFBRSxJQUFJLEVBQUUsSUFBSSxFQUFFLElBQUksRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDO0lBQ3pNLFFBQVEsR0FBYSxJQUFJLENBQUM7SUFBQyxJQUFJLEdBQUcsS0FBSyxDQUFDO0lBQUMsU0FBUyxHQUFHLEtB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QUMsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDO1FBQUMsQ0FBQztnQkFDbkUsQ0FBQztZQUFDLElBQUksVUFBVSxLQUFLLElBQUksQ0FBQyxVQUFVLEVBQUUsQ0FBQztnQkFBQyxJQUFJLENBQUMsSUFBSSxHQUFHLEtBQUssQ0FBQztnQkFBQyxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQztZQUFDLENBQUM7UUFBQyxDQUFDO0lBQ2hHLENBQUM7d0dBM0VVLHFCQUFxQjs0RkFBckIscUJBQXFCLCtOQXRCd0IsQ0FBQyxlQUFlLENBQUMsMEJBQy9EOzs7Ozs7Ozs7Ozs7Ozs7Ozs7ZUFrQkc7OzRGQUdGLHFCQUFxQjtrQkF2QmpDLFNBQVM7K0JBQ0UsZUFBZSxjQUFjLElBQUksYUFBYSxDQUFDLGVBQWUsQ0FBQyxZQUMvRDs7Ozs7Ozs7Ozs7Ozs7Ozs7O2VBa0JHO3dEQUlKLGVBQWU7c0JBQXZCLEtBQUs7Z0JBQ0csYUFBYTtzQkFBckIsS0FBSztnQkFDRyxXQUFXO3NCQUFuQixLQUFLO2dCQUNJLFNBQVM7c0JBQWxCLE1BQU07Z0JBQ0csTUFBTTtzQkFBZixNQUFNIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgQ29tcG9uZW50LCBEZXN0cm95UmVmLCBFdmVudEVtaXR0ZXIsIElucHV0LCBPbkluaXQsIE91dHB1dCwgaW5qZWN0IH0gZnJvbSAnQGFuZ3VsYXIvY29yZSc7XHJcbmltcG9ydCB7IHRha2VVbnRpbERlc3Ryb3llZCB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUvcnhqcy1pbnRlcm9wJztcclxuaW1wb3J0IHsgSm9iLCBMYW5ndWFnZSwgU3BlYWtpbmdSZXN1bHQsIFNwZWFraW5nU2VydmljZVR5cGUsIFNwZWFraW5nRXZlbnQgfSBmcm9tICdAaG9hbmdob25nL3NwZWFraW5nLWNvcmUnO1xyXG5pbXBvcnQgeyBTcGVha2luZ1NlcnZpY2UgfSBmcm9tICcuL3NwZWFraW5nLnNlcnZpY2UnO1xyXG5cclxuQENvbXBvbmVudCh7XHJcbiAgc2VsZWN0b3I6ICdmc2VsLXNwZWFraW5nJywgc3RhbmRhbG9uZTogdHJ1ZSwgcHJvdmlkZXJzOiBbU3BlYWtpbmdTZXJ2aWNlXSxcclxuICB0ZW1wbGF0ZTogYFxyXG4gICAgPHNlY3Rpb24gYXJpYS1sYWJlbD1cIkZTRUwgU3BlYWtpbmdcIj5cclxuICAgICAgPGxhYmVsPk5nw7RuIG5n4buvIDxzZWxlY3QgW3ZhbHVlXT1cImxhbmd1YWdlXCIgW2Rpc2FibGVkXT1cImJ1c3lcIiAoY2hhbmdlKT1cInNlbGVjdExhbmd1YWdlKCRldmVudClcIj5cclxuICAgICAgICBAZm9yIChpdGVtIG9mIGxhbmd1YWdlczsgdHJhY2sgaXRlbS5jb2RlKSB7IDxvcHRpb24gW3ZhbHVlXT1cIml0ZW0uY29kZVwiIFtzZWxlY3RlZF09XCJpdGVtLmNvZGUgPT09IGxhbmd1YWdlXCI+e3sgaXRlbS5uYW1lIH19PC9vcHRpb24+IH1cclxuICAgICAgPC9zZWxlY3Q+PC9sYWJlbD5cclxuICAgICAgPGRpdiBjbGFzcz1cImFjdGlvbnNcIj5cclxuICAgICAgICBAaWYgKHNlcnZpY2VUeXBlICE9PSAndXBsb2FkJykge1xyXG4gICAgICAgIDxidXR0b24gdHlwZT1cImJ1dHRvblwiIChjbGljayk9XCJzdGFydCgpXCIgW2Rpc2FibGVkXT1cImJ1c3lcIj5C4bqvdCDEkeG6p3UgbsOzaTwvYnV0dG9uPlxyXG4gICAgICAgIDxidXR0b24gdHlwZT1cImJ1dHRvblwiIChjbGljayk9XCJzdG9wKClcIiBbZGlzYWJsZWRdPVwiIXJlY29yZGluZ1wiPkThu6tuZyB2w6AgbOG6pXkga+G6v3QgcXXhuqM8L2J1dHRvbj5cclxuICAgICAgICB9XHJcbiAgICAgICAgPGJ1dHRvbiB0eXBlPVwiYnV0dG9uXCIgKGNsaWNrKT1cImNhbmNlbCgpXCIgW2Rpc2FibGVkXT1cIiFidXN5IHx8ICh1cGxvYWRpbmcgJiYgIWpvYilcIj5I4buneTwvYnV0dG9uPlxyXG4gICAgICA8L2Rpdj5cclxuICAgICAgQGlmIChzZXJ2aWNlVHlwZSAhPT0gJ3JlYWx0aW1lJyAmJiB1cGxvYWRFbmFibGVkKSB7IDxsYWJlbD5DaOG7jW4gZmlsZSBhdWRpbyAodOG7kWkgxJFhIDI1IE1CKVxyXG4gICAgICAgIDxpbnB1dCB0eXBlPVwiZmlsZVwiIGFjY2VwdD1cImF1ZGlvLyosLndhdiwubXAzLC5tNGEsLm9nZywuZmxhYywud2VibVwiIFtkaXNhYmxlZF09XCJidXN5XCIgKGNoYW5nZSk9XCJ1cGxvYWQoJGV2ZW50KVwiPlxyXG4gICAgICA8L2xhYmVsPiB9XHJcbiAgICAgIDxwIHJvbGU9XCJzdGF0dXNcIj57eyBzdGF0dXMgfX08L3A+XHJcbiAgICAgIEBpZiAoZXJyb3IpIHsgPHAgcm9sZT1cImFsZXJ0XCIgY2xhc3M9XCJlcnJvclwiPnt7IGVycm9yIH19PC9wPiB9XHJcbiAgICAgIDxsYWJlbD5WxINuIGLhuqNuIG5o4bqtbiBk4bqhbmcgPHRleHRhcmVhIHJvd3M9XCI3XCIgcmVhZG9ubHkgW3ZhbHVlXT1cInRleHRcIj48L3RleHRhcmVhPjwvbGFiZWw+XHJcbiAgICA8L3NlY3Rpb24+YCxcclxuICBzdHlsZXM6IFtgOmhvc3R7ZGlzcGxheTpibG9jaztmb250OmluaGVyaXQ7Y29sb3I6aW5oZXJpdH1zZWN0aW9ue2JvcmRlcjoxcHggc29saWQgI2I5YzFiYztwYWRkaW5nOjIwcHg7Ym9yZGVyLXJhZGl1czo0cHh9bGFiZWx7ZGlzcGxheTpncmlkO2dhcDo4cHg7bWFyZ2luLWJvdHRvbToxNnB4fXNlbGVjdCxpbnB1dCx0ZXh0YXJlYSxidXR0b257Zm9udDppbmhlcml0O2JveC1zaXppbmc6Ym9yZGVyLWJveDttYXgtd2lkdGg6MTAwJX1zZWxlY3QsaW5wdXQsdGV4dGFyZWF7cGFkZGluZzoxMHB4O2JvcmRlcjoxcHggc29saWQgIzkyOWM5NTtib3JkZXItcmFkaXVzOjNweDtiYWNrZ3JvdW5kOiNmZmY7Y29sb3I6IzIwMjcyM310ZXh0YXJlYXt3aWR0aDoxMDAlO3Jlc2l6ZTp2ZXJ0aWNhbH0uYWN0aW9uc3tkaXNwbGF5OmZsZXg7Z2FwOjhweDtmbGV4LXdyYXA6d3JhcDttYXJnaW4tYm90dG9tOjE2cHh9YnV0dG9ue21pbi1oZWlnaHQ6NDRweDtwYWRkaW5nOjhweCAxNHB4O2JhY2tncm91bmQ6IzM2NTQ0ODtjb2xvcjp3aGl0ZTtib3JkZXI6MXB4IHNvbGlkICMzNjU0NDg7Ym9yZGVyLXJhZGl1czo0cHg7Y3Vyc29yOnBvaW50ZXJ9YnV0dG9uOmRpc2FibGVke29wYWNpdHk6LjU7Y3Vyc29yOmRlZmF1bHR9YnV0dG9uOmZvY3VzLXZpc2libGUsaW5wdXQ6Zm9jdXMtdmlzaWJsZSxzZWxlY3Q6Zm9jdXMtdmlzaWJsZSx0ZXh0YXJlYTpmb2N1cy12aXNpYmxle291dGxpbmU6M3B4IHNvbGlkICNhNjZhMTU7b3V0bGluZS1vZmZzZXQ6MnB4fS5lcnJvcntjb2xvcjojYTAyMDIwO292ZXJmbG93LXdyYXA6YW55d2hlcmV9YF0sXHJcbn0pXHJcbmV4cG9ydCBjbGFzcyBGc2VsU3BlYWtpbmdDb21wb25lbnQgaW1wbGVtZW50cyBPbkluaXQge1xyXG4gIEBJbnB1dCgpIGluaXRpYWxMYW5ndWFnZTogTGFuZ3VhZ2UgPSAndmknO1xyXG4gIEBJbnB1dCgpIHVwbG9hZEVuYWJsZWQgPSB0cnVlO1xyXG4gIEBJbnB1dCgpIHNlcnZpY2VUeXBlPzogU3BlYWtpbmdTZXJ2aWNlVHlwZTtcclxuICBAT3V0cHV0KCkgY29tcGxldGVkID0gbmV3IEV2ZW50RW1pdHRlcjxTcGVha2luZ1Jlc3VsdD4oKTtcclxuICBAT3V0cHV0KCkgZmFpbGVkID0gbmV3IEV2ZW50RW1pdHRlcjxFcnJvcj4oKTtcclxuICByZWFkb25seSBzcGVha2luZyA9IGluamVjdChTcGVha2luZ1NlcnZpY2UpO1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgZGVzdHJveSA9IGluamVjdChEZXN0cm95UmVmKTtcclxuICByZWFkb25seSBsYW5ndWFnZXMgPSBbeyBjb2RlOiAnZW4nLCBuYW1lOiAnVGnhur9uZyBBbmgnIH0sIHsgY29kZTogJ2phJywgbmFtZTogJ1Rp4bq/bmcgTmjhuq10JyB9LCB7IGNvZGU6ICdrbycsIG5hbWU6ICdUaeG6v25nIEjDoG4nIH0sIHsgY29kZTogJ3poJywgbmFtZTogJ1Rp4bq/bmcgVHJ1bmcnIH0sIHsgY29kZTogJ3ZpJywgbmFtZTogJ1Rp4bq/bmcgVmnhu4d0JyB9XTtcclxuICBsYW5ndWFnZTogTGFuZ3VhZ2UgPSAndmknOyBidXN5ID0gZmFsc2U7IHJlY29yZGluZyA9IGZhbHNlOyB1cGxvYWRpbmcgPSBmYWxzZTtcclxuICB0ZXh0ID0gJyc7IGVycm9yID0gJyc7IHN0YXR1cyA9ICdDaOG7jW4gbmfDtG4gbmfhu68sIHNhdSDEkcOzIGLhuqVtIG7Ds2kgaG/hurdjIGNo4buNbiBmaWxlLic7XHJcbiAgam9iPzogSm9iOyBwcml2YXRlIGdlbmVyYXRpb24gPSAwOyBwcml2YXRlIGFib3J0PzogQWJvcnRDb250cm9sbGVyO1xyXG4gIGNvbnN0cnVjdG9yKCkge1xyXG4gICAgdGhpcy5zcGVha2luZy5ldmVudHMkLnBpcGUodGFrZVVudGlsRGVzdHJveWVkKHRoaXMuZGVzdHJveSkpLnN1YnNjcmliZSgoZXZlbnQ6IFNwZWFraW5nRXZlbnQpID0+IHtcclxuICAgICAgaWYgKGV2ZW50LnR5cGUgPT09ICdwYXJ0aWFsJykgdGhpcy50ZXh0ID0gZXZlbnQudGV4dDtcclxuICAgICAgaWYgKGV2ZW50LnR5cGUgPT09ICdzdGF0ZScpIHRoaXMucmVjb3JkaW5nID0gZXZlbnQuc3RhdGUgPT09ICdyZWNvcmRpbmcnO1xyXG4gICAgICBpZiAoZXZlbnQudHlwZSA9PT0gJ2NsaWVudEVycm9yJykgeyB0aGlzLnJlcG9ydChldmVudC5lcnJvcik7IHRoaXMuYnVzeSA9IGZhbHNlOyB0aGlzLnJlY29yZGluZyA9IGZhbHNlOyB9XHJcbiAgICB9KTtcclxuICAgIHRoaXMuZGVzdHJveS5vbkRlc3Ryb3koKCkgPT4geyArK3RoaXMuZ2VuZXJhdGlvbjsgdGhpcy5hYm9ydD8uYWJvcnQoKTsgfSk7XHJcbiAgfVxyXG4gIG5nT25Jbml0KCk6IHZvaWQgeyB0aGlzLmxhbmd1YWdlID0gdGhpcy5pbml0aWFsTGFuZ3VhZ2U7IH1cclxuICBzZWxlY3RMYW5ndWFnZShldmVudDogRXZlbnQpOiB2b2lkIHsgdGhpcy5sYW5ndWFnZSA9IChldmVudC50YXJnZXQgYXMgSFRNTFNlbGVjdEVsZW1lbnQpLnZhbHVlIGFzIExhbmd1YWdlOyB9XHJcbiAgcHJpdmF0ZSByZXBvcnQoZXJyb3I6IHVua25vd24pOiB2b2lkIHtcclxuICAgIGNvbnN0IHZhbHVlID0gZXJyb3IgaW5zdGFuY2VvZiBFcnJvciA/IGVycm9yIDogbmV3IEVycm9yKCdTcGVha2luZyBmYWlsZWQnKTtcclxuICAgIHRoaXMuZXJyb3IgPSB2YWx1ZS5tZXNzYWdlOyB0aGlzLmZhaWxlZC5lbWl0KHZhbHVlKTtcclxuICB9XHJcbiAgYXN5bmMgc3RhcnQoKTogUHJvbWlzZTx2b2lkPiB7XHJcbiAgICBpZiAodGhpcy5idXN5KSByZXR1cm47XHJcbiAgICBjb25zdCBnZW5lcmF0aW9uID0gKyt0aGlzLmdlbmVyYXRpb247XHJcbiAgICB0aGlzLmJ1c3kgPSB0cnVlOyB0aGlzLnVwbG9hZGluZyA9IGZhbHNlOyB0aGlzLmVycm9yID0gJyc7IHRoaXMudGV4dCA9ICcnOyB0aGlzLnN0YXR1cyA9ICfEkGFuZyBt4bufIG1pY3JvcGhvbmUgdsOgIGNodeG6qW4gYuG7iyBtb2RlbOKApic7XHJcbiAgICB0cnkgeyBhd2FpdCB0aGlzLnNwZWFraW5nLnN0YXJ0KHsgbGFuZ3VhZ2U6IHRoaXMubGFuZ3VhZ2UgfSk7IGlmIChnZW5lcmF0aW9uID09PSB0aGlzLmdlbmVyYXRpb24pIHRoaXMuc3RhdHVzID0gJ8SQYW5nIG5naGXigKYnOyB9XHJcbiAgICBjYXRjaCAoZXJyb3IpIHsgaWYgKGdlbmVyYXRpb24gPT09IHRoaXMuZ2VuZXJhdGlvbikgeyB0aGlzLnJlcG9ydChlcnJvcik7IHRoaXMuYnVzeSA9IGZhbHNlOyB9IH1cclxuICB9XHJcbiAgYXN5bmMgc3RvcCgpOiBQcm9taXNlPHZvaWQ+IHtcclxuICAgIGlmICghdGhpcy5yZWNvcmRpbmcpIHJldHVybjtcclxuICAgIGNvbnN0IGdlbmVyYXRpb24gPSB0aGlzLmdlbmVyYXRpb247XHJcbiAgICB0aGlzLnJlY29yZGluZyA9IGZhbHNlOyB0aGlzLnN0YXR1cyA9ICfEkGFuZyBs4bqleSBr4bq/dCBxdeG6o+KApic7XHJcbiAgICB0cnkge1xyXG4gICAgICBjb25zdCByZXN1bHQgPSBhd2FpdCB0aGlzLnNwZWFraW5nLnN0b3AoKTtcclxuICAgICAgaWYgKGdlbmVyYXRpb24gIT09IHRoaXMuZ2VuZXJhdGlvbikgcmV0dXJuO1xyXG4gICAgICB0aGlzLnRleHQgPSByZXN1bHQucmVzdWx0Py50ZXh0ID8/ICcnOyB0aGlzLnN0YXR1cyA9IHJlc3VsdC5zdGF0dXMgPT09ICdub19zcGVlY2gnID8gJ0tow7RuZyBuaOG6rW4gdGjhuqV5IGzhu51pIG7Ds2kuJyA6ICfEkMOjIGhvw6BuIHThuqV0Lic7XHJcbiAgICAgIHRoaXMuY29tcGxldGVkLmVtaXQoeyAuLi5yZXN1bHQsIGxhbmd1YWdlOiB0aGlzLmxhbmd1YWdlLCB0ZXh0OiB0aGlzLnRleHQsIGF1ZGlvOiByZXN1bHQuYXVkaW8sIHNlcnZpY2VUeXBlOiAncmVhbHRpbWUnIH0pO1xyXG4gICAgfSBjYXRjaCAoZXJyb3IpIHsgaWYgKGdlbmVyYXRpb24gPT09IHRoaXMuZ2VuZXJhdGlvbikgdGhpcy5yZXBvcnQoZXJyb3IpOyB9XHJcbiAgICBmaW5hbGx5IHsgaWYgKGdlbmVyYXRpb24gPT09IHRoaXMuZ2VuZXJhdGlvbikgdGhpcy5idXN5ID0gZmFsc2U7IH1cclxuICB9XHJcbiAgYXN5bmMgY2FuY2VsKCk6IFByb21pc2U8dm9pZD4ge1xyXG4gICAgKyt0aGlzLmdlbmVyYXRpb247IHRoaXMuYWJvcnQ/LmFib3J0KCk7XHJcbiAgICB0cnkgeyBpZiAodGhpcy51cGxvYWRpbmcgJiYgdGhpcy5qb2IpIGF3YWl0IHRoaXMuc3BlYWtpbmcuYmF0Y2guY2FuY2VsSm9iKHRoaXMuam9iLmpvYl9pZCk7IGVsc2UgYXdhaXQgdGhpcy5zcGVha2luZy5jYW5jZWwoKTsgdGhpcy5zdGF0dXMgPSAnxJDDoyBo4buneS4nOyB9XHJcbiAgICBjYXRjaCAoZXJyb3IpIHsgdGhpcy5yZXBvcnQoZXJyb3IpOyB9XHJcbiAgICBmaW5hbGx5IHsgdGhpcy5idXN5ID0gZmFsc2U7IHRoaXMucmVjb3JkaW5nID0gZmFsc2U7IHRoaXMudXBsb2FkaW5nID0gZmFsc2U7IH1cclxuICB9XHJcbiAgYXN5bmMgdXBsb2FkKGV2ZW50OiBFdmVudCk6IFByb21pc2U8dm9pZD4ge1xyXG4gICAgY29uc3QgaW5wdXQgPSBldmVudC50YXJnZXQgYXMgSFRNTElucHV0RWxlbWVudDsgY29uc3QgZmlsZSA9IGlucHV0LmZpbGVzPy5bMF07IGlucHV0LnZhbHVlID0gJyc7XHJcbiAgICBpZiAoIWZpbGUgfHwgdGhpcy5idXN5KSByZXR1cm47XHJcbiAgICBpZiAoZmlsZS5zaXplID4gMjUgKiAxMDI0ICogMTAyNCkgeyB0aGlzLnJlcG9ydChuZXcgRXJyb3IoJ0ZpbGUgdOG7kWkgxJFhIDI1IE1CLicpKTsgcmV0dXJuOyB9XHJcbiAgICBjb25zdCBnZW5lcmF0aW9uID0gKyt0aGlzLmdlbmVyYXRpb247IHRoaXMuYWJvcnQgPSBuZXcgQWJvcnRDb250cm9sbGVyKCk7XHJcbiAgICB0aGlzLmJ1c3kgPSB0cnVlOyB0aGlzLnVwbG9hZGluZyA9IHRydWU7IHRoaXMuam9iID0gdW5kZWZpbmVkOyB0aGlzLnRleHQgPSAnJzsgdGhpcy5lcnJvciA9ICcnOyB0aGlzLnN0YXR1cyA9ICfEkGFuZyB1cGxvYWTigKYnO1xyXG4gICAgdHJ5IHtcclxuICAgICAgbGV0IGpvYiA9IGF3YWl0IHRoaXMuc3BlYWtpbmcuYmF0Y2guc3VibWl0QXVkaW8oZmlsZSwgdGhpcy5sYW5ndWFnZSwgY3J5cHRvLnJhbmRvbVVVSUQoKSwgJ3Nob3J0X2Fuc3dlcicsIGZpbGUubmFtZSwgdGhpcy5hYm9ydC5zaWduYWwpO1xyXG4gICAgICBjb25zdCBkZWFkbGluZSA9IERhdGUubm93KCkgKyAxMCAqIDYwICogMTAwMDtcclxuICAgICAgd2hpbGUgKGdlbmVyYXRpb24gPT09IHRoaXMuZ2VuZXJhdGlvbikge1xyXG4gICAgICAgIHRoaXMuam9iID0gam9iO1xyXG4gICAgICAgIGlmIChbJ3N1Y2NlZWRlZCcsICdub19zcGVlY2gnLCAnZmFpbGVkJywgJ2NhbmNlbGxlZCddLmluY2x1ZGVzKGpvYi5zdGF0dXMpKSBicmVhaztcclxuICAgICAgICBpZiAoRGF0ZS5ub3coKSA+IGRlYWRsaW5lKSB0aHJvdyBuZXcgRXJyb3IoJ0jhur90IHRo4budaSBnaWFuIGNo4budOyBqb2IgY8OzIHRo4buDIHbhuqtuIMSRYW5nIGNo4bqheSB0csOqbiBzZXJ2ZXIuJyk7XHJcbiAgICAgICAgdGhpcy5zdGF0dXMgPSAnxJBhbmcgbmjhuq1uIGThuqFuZyBmaWxl4oCmJztcclxuICAgICAgICBhd2FpdCBuZXcgUHJvbWlzZShyZXNvbHZlID0+IHNldFRpbWVvdXQocmVzb2x2ZSwgMTUwMCkpO1xyXG4gICAgICAgIGlmIChnZW5lcmF0aW9uICE9PSB0aGlzLmdlbmVyYXRpb24pIHJldHVybjtcclxuICAgICAgICBqb2IgPSBhd2FpdCB0aGlzLnNwZWFraW5nLmJhdGNoLmdldFJlc3VsdChqb2Iuam9iX2lkLCB0aGlzLmFib3J0LnNpZ25hbCk7XHJcbiAgICAgIH1cclxuICAgICAgaWYgKGdlbmVyYXRpb24gIT09IHRoaXMuZ2VuZXJhdGlvbikgcmV0dXJuO1xyXG4gICAgICBpZiAoam9iLmVycm9yKSB0aHJvdyBuZXcgRXJyb3Ioam9iLmVycm9yLm1lc3NhZ2UpO1xyXG4gICAgICB0aGlzLnRleHQgPSBqb2IucmVzdWx0Py50ZXh0ID8/ICcnOyB0aGlzLnN0YXR1cyA9IGpvYi5zdGF0dXMgPT09ICdub19zcGVlY2gnID8gJ0tow7RuZyBuaOG6rW4gdGjhuqV5IGzhu51pIG7Ds2kuJyA6IGpvYi5zdGF0dXMgPT09ICdjYW5jZWxsZWQnID8gJ8SQw6MgaOG7p3kuJyA6ICfEkMOjIGhvw6BuIHThuqV0Lic7XHJcbiAgICAgIGlmIChqb2Iuc3RhdHVzICE9PSAnY2FuY2VsbGVkJykgdGhpcy5jb21wbGV0ZWQuZW1pdCh7IC4uLmpvYiwgbGFuZ3VhZ2U6IHRoaXMubGFuZ3VhZ2UsIHRleHQ6IHRoaXMudGV4dCwgYXVkaW86IGZpbGUsIHNlcnZpY2VUeXBlOiAndXBsb2FkJyB9KTtcclxuICAgIH0gY2F0Y2ggKGVycm9yKSB7IGlmIChnZW5lcmF0aW9uID09PSB0aGlzLmdlbmVyYXRpb24pIHRoaXMucmVwb3J0KGVycm9yKTsgfVxyXG4gICAgZmluYWxseSB7IGlmIChnZW5lcmF0aW9uID09PSB0aGlzLmdlbmVyYXRpb24pIHsgdGhpcy5idXN5ID0gZmFsc2U7IHRoaXMudXBsb2FkaW5nID0gZmFsc2U7IH0gfVxyXG4gIH1cclxufVxyXG4iXX0=
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Inject, Injectable, InjectionToken } from '@angular/core';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
import { BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway, createSpeakingService, } from '@hoanghong/speaking-core';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
export const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG', {
|
|
6
|
+
providedIn: 'root', factory: () => createSpeakingService(),
|
|
7
|
+
});
|
|
8
|
+
/** Provide at the screen/component level to give each speaking screen its own lifecycle. */
|
|
9
|
+
export class SpeakingService {
|
|
10
|
+
zone;
|
|
11
|
+
batch;
|
|
12
|
+
realtime;
|
|
13
|
+
eventSubject = new Subject();
|
|
14
|
+
events$ = this.eventSubject.asObservable();
|
|
15
|
+
microphone;
|
|
16
|
+
unsubscribe;
|
|
17
|
+
constructor(config, zone) {
|
|
18
|
+
this.zone = zone;
|
|
19
|
+
this.batch = new BatchSpeakingClient(config.batch);
|
|
20
|
+
this.realtime = new RealtimeSpeakingClient(config.realtime);
|
|
21
|
+
this.microphone = new SpeakingMicrophone(this.realtime, config.microphone);
|
|
22
|
+
this.unsubscribe = this.realtime.onEvent(event => this.zone.run(() => this.eventSubject.next(event)));
|
|
23
|
+
}
|
|
24
|
+
start(options) {
|
|
25
|
+
return this.zone.runOutsideAngular(() => this.microphone.start(options));
|
|
26
|
+
}
|
|
27
|
+
stop() { return this.microphone.stop(); }
|
|
28
|
+
cancel() { return this.microphone.cancel(); }
|
|
29
|
+
ngOnDestroy() {
|
|
30
|
+
this.unsubscribe();
|
|
31
|
+
this.eventSubject.complete();
|
|
32
|
+
void this.microphone.dispose();
|
|
33
|
+
}
|
|
34
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService, deps: [{ token: SPEAKING_CONFIG }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
35
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService });
|
|
36
|
+
}
|
|
37
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: SpeakingService, decorators: [{
|
|
38
|
+
type: Injectable
|
|
39
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
40
|
+
type: Inject,
|
|
41
|
+
args: [SPEAKING_CONFIG]
|
|
42
|
+
}] }, { type: i0.NgZone }] });
|
|
43
|
+
export function provideSpeaking(config) {
|
|
44
|
+
return [{ provide: SPEAKING_CONFIG, useValue: config }, SpeakingService];
|
|
45
|
+
}
|
|
46
|
+
/** Configure once at app bootstrap; every fsel-speaking component owns its session. */
|
|
47
|
+
export function provideSpeakingGateway(options = {}) {
|
|
48
|
+
return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingGateway(options) }];
|
|
49
|
+
}
|
|
50
|
+
/** Application identity only; hosted endpoint and short-lived sessions are automatic. */
|
|
51
|
+
export function provideSpeakingApp(options) {
|
|
52
|
+
return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingService(options) }];
|
|
53
|
+
}
|
|
54
|
+
export * from '@hoanghong/speaking-core';
|
|
55
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic3BlYWtpbmcuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3NyYy9zcGVha2luZy5zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxNQUFNLEVBQUUsVUFBVSxFQUFFLGNBQWMsRUFBK0IsTUFBTSxlQUFlLENBQUM7QUFDaEcsT0FBTyxFQUFFLE9BQU8sRUFBRSxNQUFNLE1BQU0sQ0FBQztBQUMvQixPQUFPLEVBQ1MsbUJBQW1CLEVBQ2pDLHNCQUFzQixFQUFpQixrQkFBa0IsRUFDakMscUJBQXFCLEVBQzdDLHFCQUFxQixHQUN0QixNQUFNLDBCQUEwQixDQUFDOztBQVFsQyxNQUFNLENBQUMsTUFBTSxlQUFlLEdBQUcsSUFBSSxjQUFjLENBQWlCLHNCQUFzQixFQUFFO0lBQ3hGLFVBQVUsRUFBRSxNQUFNLEVBQUUsT0FBTyxFQUFFLEdBQUcsRUFBRSxDQUFDLHFCQUFxQixFQUFFO0NBQzNELENBQUMsQ0FBQztBQUVILDRGQUE0RjtBQUU1RixNQUFNLE9BQU8sZUFBZTtJQVFvRDtJQVByRSxLQUFLLENBQXNCO0lBQzNCLFFBQVEsQ0FBeUI7SUFDekIsWUFBWSxHQUFHLElBQUksT0FBTyxFQUFpQixDQUFDO0lBQ3BELE9BQU8sR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLFlBQVksRUFBRSxDQUFDO0lBQ25DLFVBQVUsQ0FBcUI7SUFDL0IsV0FBVyxDQUFhO0lBRXpDLFlBQXFDLE1BQXNCLEVBQW1CLElBQVk7UUFBWixTQUFJLEdBQUosSUFBSSxDQUFRO1FBQ3hGLElBQUksQ0FBQyxLQUFLLEdBQUcsSUFBSSxtQkFBbUIsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUM7UUFDbkQsSUFBSSxDQUFDLFFBQVEsR0FBRyxJQUFJLHNCQUFzQixDQUFDLE1BQU0sQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUM1RCxJQUFJLENBQUMsVUFBVSxHQUFHLElBQUksa0JBQWtCLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUM7UUFDM0UsSUFBSSxDQUFDLFdBQVcsR0FBRyxJQUFJLENBQUMsUUFBUSxDQUFDLE9BQU8sQ0FBQyxLQUFLLENBQUMsRUFBRSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEdBQUcsRUFBRSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUN4RyxDQUFDO0lBRUQsS0FBSyxDQUFDLE9BQXFCO1FBQ3pCLE9BQU8sSUFBSSxDQUFDLElBQUksQ0FBQyxpQkFBaUIsQ0FBQyxHQUFHLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDO0lBQzNFLENBQUM7SUFFRCxJQUFJLEtBQXFDLE9BQU8sSUFBSSxDQUFDLFVBQVUsQ0FBQyxJQUFJLEVBQUUsQ0FBQyxDQUFDLENBQUM7SUFDekUsTUFBTSxLQUFvQixPQUFPLElBQUksQ0FBQyxVQUFVLENBQUMsTUFBTSxFQUFFLENBQUMsQ0FBQyxDQUFDO0lBRTVELFdBQVc7UUFDVCxJQUFJLENBQUMsV0FBVyxFQUFFLENBQUM7UUFDbkIsSUFBSSxDQUFDLFlBQVksQ0FBQyxRQUFRLEVBQUUsQ0FBQztRQUM3QixLQUFLLElBQUksQ0FBQyxVQUFVLENBQUMsT0FBTyxFQUFFLENBQUM7SUFDakMsQ0FBQzt3R0ExQlUsZUFBZSxrQkFRTixlQUFlOzRHQVJ4QixlQUFlOzs0RkFBZixlQUFlO2tCQUQzQixVQUFVOzswQkFTSSxNQUFNOzJCQUFDLGVBQWU7O0FBcUJyQyxNQUFNLFVBQVUsZUFBZSxDQUFDLE1BQXNCO0lBQ3BELE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsUUFBUSxFQUFFLE1BQU0sRUFBRSxFQUFFLGVBQWUsQ0FBQyxDQUFDO0FBQzNFLENBQUM7QUFFRCx1RkFBdUY7QUFDdkYsTUFBTSxVQUFVLHNCQUFzQixDQUFDLFVBQWtDLEVBQUU7SUFDekUsT0FBTyxDQUFDLEVBQUUsT0FBTyxFQUFFLGVBQWUsRUFBRSxVQUFVLEVBQUUsR0FBRyxFQUFFLENBQUMscUJBQXFCLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0FBQzFGLENBQUM7QUFFRCx5RkFBeUY7QUFDekYsTUFBTSxVQUFVLGtCQUFrQixDQUFDLE9BQTJCO0lBQzVELE9BQU8sQ0FBQyxFQUFFLE9BQU8sRUFBRSxlQUFlLEVBQUUsVUFBVSxFQUFFLEdBQUcsRUFBRSxDQUFDLHFCQUFxQixDQUFDLE9BQU8sQ0FBQyxFQUFFLENBQUMsQ0FBQztBQUMxRixDQUFDO0FBSUQsY0FBYywwQkFBMEIsQ0FBQyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEluamVjdCwgSW5qZWN0YWJsZSwgSW5qZWN0aW9uVG9rZW4sIE5nWm9uZSwgT25EZXN0cm95LCBQcm92aWRlciB9IGZyb20gJ0Bhbmd1bGFyL2NvcmUnO1xyXG5pbXBvcnQgeyBTdWJqZWN0IH0gZnJvbSAncnhqcyc7XHJcbmltcG9ydCB7XHJcbiAgQmF0Y2hPcHRpb25zLCBCYXRjaFNwZWFraW5nQ2xpZW50LCBNaWNyb3Bob25lT3B0aW9ucywgUmVjb3JkZWRTcGVha2luZ0ZpbmFsLCBSZWFsdGltZU9wdGlvbnMsXHJcbiAgUmVhbHRpbWVTcGVha2luZ0NsaWVudCwgU3BlYWtpbmdFdmVudCwgU3BlYWtpbmdNaWNyb3Bob25lLCBTdGFydE9wdGlvbnMsXHJcbiAgU3BlYWtpbmdHYXRld2F5T3B0aW9ucywgY3JlYXRlU3BlYWtpbmdHYXRld2F5LFxyXG4gIGNyZWF0ZVNwZWFraW5nU2VydmljZSxcclxufSBmcm9tICdAaG9hbmdob25nL3NwZWFraW5nLWNvcmUnO1xyXG5cclxuZXhwb3J0IGludGVyZmFjZSBTcGVha2luZ0NvbmZpZyB7XHJcbiAgYmF0Y2g6IEJhdGNoT3B0aW9ucztcclxuICByZWFsdGltZTogUmVhbHRpbWVPcHRpb25zO1xyXG4gIG1pY3JvcGhvbmU6IE1pY3JvcGhvbmVPcHRpb25zO1xyXG59XHJcblxyXG5leHBvcnQgY29uc3QgU1BFQUtJTkdfQ09ORklHID0gbmV3IEluamVjdGlvblRva2VuPFNwZWFraW5nQ29uZmlnPignRlNFTF9TUEVBS0lOR19DT05GSUcnLCB7XHJcbiAgcHJvdmlkZWRJbjogJ3Jvb3QnLCBmYWN0b3J5OiAoKSA9PiBjcmVhdGVTcGVha2luZ1NlcnZpY2UoKSxcclxufSk7XHJcblxyXG4vKiogUHJvdmlkZSBhdCB0aGUgc2NyZWVuL2NvbXBvbmVudCBsZXZlbCB0byBnaXZlIGVhY2ggc3BlYWtpbmcgc2NyZWVuIGl0cyBvd24gbGlmZWN5Y2xlLiAqL1xyXG5ASW5qZWN0YWJsZSgpXHJcbmV4cG9ydCBjbGFzcyBTcGVha2luZ1NlcnZpY2UgaW1wbGVtZW50cyBPbkRlc3Ryb3kge1xyXG4gIHJlYWRvbmx5IGJhdGNoOiBCYXRjaFNwZWFraW5nQ2xpZW50O1xyXG4gIHJlYWRvbmx5IHJlYWx0aW1lOiBSZWFsdGltZVNwZWFraW5nQ2xpZW50O1xyXG4gIHByaXZhdGUgcmVhZG9ubHkgZXZlbnRTdWJqZWN0ID0gbmV3IFN1YmplY3Q8U3BlYWtpbmdFdmVudD4oKTtcclxuICByZWFkb25seSBldmVudHMkID0gdGhpcy5ldmVudFN1YmplY3QuYXNPYnNlcnZhYmxlKCk7XHJcbiAgcHJpdmF0ZSByZWFkb25seSBtaWNyb3Bob25lOiBTcGVha2luZ01pY3JvcGhvbmU7XHJcbiAgcHJpdmF0ZSByZWFkb25seSB1bnN1YnNjcmliZTogKCkgPT4gdm9pZDtcclxuXHJcbiAgY29uc3RydWN0b3IoQEluamVjdChTUEVBS0lOR19DT05GSUcpIGNvbmZpZzogU3BlYWtpbmdDb25maWcsIHByaXZhdGUgcmVhZG9ubHkgem9uZTogTmdab25lKSB7XHJcbiAgICB0aGlzLmJhdGNoID0gbmV3IEJhdGNoU3BlYWtpbmdDbGllbnQoY29uZmlnLmJhdGNoKTtcclxuICAgIHRoaXMucmVhbHRpbWUgPSBuZXcgUmVhbHRpbWVTcGVha2luZ0NsaWVudChjb25maWcucmVhbHRpbWUpO1xyXG4gICAgdGhpcy5taWNyb3Bob25lID0gbmV3IFNwZWFraW5nTWljcm9waG9uZSh0aGlzLnJlYWx0aW1lLCBjb25maWcubWljcm9waG9uZSk7XHJcbiAgICB0aGlzLnVuc3Vic2NyaWJlID0gdGhpcy5yZWFsdGltZS5vbkV2ZW50KGV2ZW50ID0+IHRoaXMuem9uZS5ydW4oKCkgPT4gdGhpcy5ldmVudFN1YmplY3QubmV4dChldmVudCkpKTtcclxuICB9XHJcblxyXG4gIHN0YXJ0KG9wdGlvbnM6IFN0YXJ0T3B0aW9ucyk6IFByb21pc2U8dm9pZD4ge1xyXG4gICAgcmV0dXJuIHRoaXMuem9uZS5ydW5PdXRzaWRlQW5ndWxhcigoKSA9PiB0aGlzLm1pY3JvcGhvbmUuc3RhcnQob3B0aW9ucykpO1xyXG4gIH1cclxuXHJcbiAgc3RvcCgpOiBQcm9taXNlPFJlY29yZGVkU3BlYWtpbmdGaW5hbD4geyByZXR1cm4gdGhpcy5taWNyb3Bob25lLnN0b3AoKTsgfVxyXG4gIGNhbmNlbCgpOiBQcm9taXNlPHZvaWQ+IHsgcmV0dXJuIHRoaXMubWljcm9waG9uZS5jYW5jZWwoKTsgfVxyXG5cclxuICBuZ09uRGVzdHJveSgpOiB2b2lkIHtcclxuICAgIHRoaXMudW5zdWJzY3JpYmUoKTtcclxuICAgIHRoaXMuZXZlbnRTdWJqZWN0LmNvbXBsZXRlKCk7XHJcbiAgICB2b2lkIHRoaXMubWljcm9waG9uZS5kaXNwb3NlKCk7XHJcbiAgfVxyXG59XHJcblxyXG5leHBvcnQgZnVuY3Rpb24gcHJvdmlkZVNwZWFraW5nKGNvbmZpZzogU3BlYWtpbmdDb25maWcpOiBQcm92aWRlcltdIHtcclxuICByZXR1cm4gW3sgcHJvdmlkZTogU1BFQUtJTkdfQ09ORklHLCB1c2VWYWx1ZTogY29uZmlnIH0sIFNwZWFraW5nU2VydmljZV07XHJcbn1cclxuXHJcbi8qKiBDb25maWd1cmUgb25jZSBhdCBhcHAgYm9vdHN0cmFwOyBldmVyeSBmc2VsLXNwZWFraW5nIGNvbXBvbmVudCBvd25zIGl0cyBzZXNzaW9uLiAqL1xyXG5leHBvcnQgZnVuY3Rpb24gcHJvdmlkZVNwZWFraW5nR2F0ZXdheShvcHRpb25zOiBTcGVha2luZ0dhdGV3YXlPcHRpb25zID0ge30pOiBQcm92aWRlcltdIHtcclxuICByZXR1cm4gW3sgcHJvdmlkZTogU1BFQUtJTkdfQ09ORklHLCB1c2VGYWN0b3J5OiAoKSA9PiBjcmVhdGVTcGVha2luZ0dhdGV3YXkob3B0aW9ucykgfV07XHJcbn1cclxuXHJcbi8qKiBBcHBsaWNhdGlvbiBpZGVudGl0eSBvbmx5OyBob3N0ZWQgZW5kcG9pbnQgYW5kIHNob3J0LWxpdmVkIHNlc3Npb25zIGFyZSBhdXRvbWF0aWMuICovXHJcbmV4cG9ydCBmdW5jdGlvbiBwcm92aWRlU3BlYWtpbmdBcHAob3B0aW9uczogeyBhcHBLZXk6IHN0cmluZyB9KTogUHJvdmlkZXJbXSB7XHJcbiAgcmV0dXJuIFt7IHByb3ZpZGU6IFNQRUFLSU5HX0NPTkZJRywgdXNlRmFjdG9yeTogKCkgPT4gY3JlYXRlU3BlYWtpbmdTZXJ2aWNlKG9wdGlvbnMpIH1dO1xyXG59XHJcblxyXG5cclxuXHJcbmV4cG9ydCAqIGZyb20gJ0Bob2FuZ2hvbmcvc3BlYWtpbmctY29yZSc7XHJcbiJdfQ==
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Inject, Injectable } from '@angular/core';
|
|
2
|
+
import { InjectionToken, Inject, Injectable, EventEmitter, inject, DestroyRef, Output, Input, Component } from '@angular/core';
|
|
3
3
|
import { Subject } from 'rxjs';
|
|
4
|
-
import { BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone } from '@hoanghong/speaking-core';
|
|
4
|
+
import { createSpeakingService, BatchSpeakingClient, RealtimeSpeakingClient, SpeakingMicrophone, createSpeakingGateway } from '@hoanghong/speaking-core';
|
|
5
5
|
export * from '@hoanghong/speaking-core';
|
|
6
|
+
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
|
6
7
|
|
|
7
|
-
const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG'
|
|
8
|
+
const SPEAKING_CONFIG = new InjectionToken('FSEL_SPEAKING_CONFIG', {
|
|
9
|
+
providedIn: 'root', factory: () => createSpeakingService(),
|
|
10
|
+
});
|
|
8
11
|
/** Provide at the screen/component level to give each speaking screen its own lifecycle. */
|
|
9
12
|
class SpeakingService {
|
|
10
13
|
zone;
|
|
@@ -43,10 +46,228 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImpo
|
|
|
43
46
|
function provideSpeaking(config) {
|
|
44
47
|
return [{ provide: SPEAKING_CONFIG, useValue: config }, SpeakingService];
|
|
45
48
|
}
|
|
49
|
+
/** Configure once at app bootstrap; every fsel-speaking component owns its session. */
|
|
50
|
+
function provideSpeakingGateway(options = {}) {
|
|
51
|
+
return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingGateway(options) }];
|
|
52
|
+
}
|
|
53
|
+
/** Application identity only; hosted endpoint and short-lived sessions are automatic. */
|
|
54
|
+
function provideSpeakingApp(options) {
|
|
55
|
+
return [{ provide: SPEAKING_CONFIG, useFactory: () => createSpeakingService(options) }];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class FselSpeakingComponent {
|
|
59
|
+
initialLanguage = 'vi';
|
|
60
|
+
uploadEnabled = true;
|
|
61
|
+
serviceType;
|
|
62
|
+
completed = new EventEmitter();
|
|
63
|
+
failed = new EventEmitter();
|
|
64
|
+
speaking = inject(SpeakingService);
|
|
65
|
+
destroy = inject(DestroyRef);
|
|
66
|
+
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' }];
|
|
67
|
+
language = 'vi';
|
|
68
|
+
busy = false;
|
|
69
|
+
recording = false;
|
|
70
|
+
uploading = false;
|
|
71
|
+
text = '';
|
|
72
|
+
error = '';
|
|
73
|
+
status = 'Chọn ngôn ngữ, sau đó bấm nói hoặc chọn file.';
|
|
74
|
+
job;
|
|
75
|
+
generation = 0;
|
|
76
|
+
abort;
|
|
77
|
+
constructor() {
|
|
78
|
+
this.speaking.events$.pipe(takeUntilDestroyed(this.destroy)).subscribe((event) => {
|
|
79
|
+
if (event.type === 'partial')
|
|
80
|
+
this.text = event.text;
|
|
81
|
+
if (event.type === 'state')
|
|
82
|
+
this.recording = event.state === 'recording';
|
|
83
|
+
if (event.type === 'clientError') {
|
|
84
|
+
this.report(event.error);
|
|
85
|
+
this.busy = false;
|
|
86
|
+
this.recording = false;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
this.destroy.onDestroy(() => { ++this.generation; this.abort?.abort(); });
|
|
90
|
+
}
|
|
91
|
+
ngOnInit() { this.language = this.initialLanguage; }
|
|
92
|
+
selectLanguage(event) { this.language = event.target.value; }
|
|
93
|
+
report(error) {
|
|
94
|
+
const value = error instanceof Error ? error : new Error('Speaking failed');
|
|
95
|
+
this.error = value.message;
|
|
96
|
+
this.failed.emit(value);
|
|
97
|
+
}
|
|
98
|
+
async start() {
|
|
99
|
+
if (this.busy)
|
|
100
|
+
return;
|
|
101
|
+
const generation = ++this.generation;
|
|
102
|
+
this.busy = true;
|
|
103
|
+
this.uploading = false;
|
|
104
|
+
this.error = '';
|
|
105
|
+
this.text = '';
|
|
106
|
+
this.status = 'Đang mở microphone và chuẩn bị model…';
|
|
107
|
+
try {
|
|
108
|
+
await this.speaking.start({ language: this.language });
|
|
109
|
+
if (generation === this.generation)
|
|
110
|
+
this.status = 'Đang nghe…';
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
if (generation === this.generation) {
|
|
114
|
+
this.report(error);
|
|
115
|
+
this.busy = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async stop() {
|
|
120
|
+
if (!this.recording)
|
|
121
|
+
return;
|
|
122
|
+
const generation = this.generation;
|
|
123
|
+
this.recording = false;
|
|
124
|
+
this.status = 'Đang lấy kết quả…';
|
|
125
|
+
try {
|
|
126
|
+
const result = await this.speaking.stop();
|
|
127
|
+
if (generation !== this.generation)
|
|
128
|
+
return;
|
|
129
|
+
this.text = result.result?.text ?? '';
|
|
130
|
+
this.status = result.status === 'no_speech' ? 'Không nhận thấy lời nói.' : 'Đã hoàn tất.';
|
|
131
|
+
this.completed.emit({ ...result, language: this.language, text: this.text, audio: result.audio, serviceType: 'realtime' });
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (generation === this.generation)
|
|
135
|
+
this.report(error);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
if (generation === this.generation)
|
|
139
|
+
this.busy = false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async cancel() {
|
|
143
|
+
++this.generation;
|
|
144
|
+
this.abort?.abort();
|
|
145
|
+
try {
|
|
146
|
+
if (this.uploading && this.job)
|
|
147
|
+
await this.speaking.batch.cancelJob(this.job.job_id);
|
|
148
|
+
else
|
|
149
|
+
await this.speaking.cancel();
|
|
150
|
+
this.status = 'Đã hủy.';
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
this.report(error);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
this.busy = false;
|
|
157
|
+
this.recording = false;
|
|
158
|
+
this.uploading = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async upload(event) {
|
|
162
|
+
const input = event.target;
|
|
163
|
+
const file = input.files?.[0];
|
|
164
|
+
input.value = '';
|
|
165
|
+
if (!file || this.busy)
|
|
166
|
+
return;
|
|
167
|
+
if (file.size > 25 * 1024 * 1024) {
|
|
168
|
+
this.report(new Error('File tối đa 25 MB.'));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const generation = ++this.generation;
|
|
172
|
+
this.abort = new AbortController();
|
|
173
|
+
this.busy = true;
|
|
174
|
+
this.uploading = true;
|
|
175
|
+
this.job = undefined;
|
|
176
|
+
this.text = '';
|
|
177
|
+
this.error = '';
|
|
178
|
+
this.status = 'Đang upload…';
|
|
179
|
+
try {
|
|
180
|
+
let job = await this.speaking.batch.submitAudio(file, this.language, crypto.randomUUID(), 'short_answer', file.name, this.abort.signal);
|
|
181
|
+
const deadline = Date.now() + 10 * 60 * 1000;
|
|
182
|
+
while (generation === this.generation) {
|
|
183
|
+
this.job = job;
|
|
184
|
+
if (['succeeded', 'no_speech', 'failed', 'cancelled'].includes(job.status))
|
|
185
|
+
break;
|
|
186
|
+
if (Date.now() > deadline)
|
|
187
|
+
throw new Error('Hết thời gian chờ; job có thể vẫn đang chạy trên server.');
|
|
188
|
+
this.status = 'Đang nhận dạng file…';
|
|
189
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
190
|
+
if (generation !== this.generation)
|
|
191
|
+
return;
|
|
192
|
+
job = await this.speaking.batch.getResult(job.job_id, this.abort.signal);
|
|
193
|
+
}
|
|
194
|
+
if (generation !== this.generation)
|
|
195
|
+
return;
|
|
196
|
+
if (job.error)
|
|
197
|
+
throw new Error(job.error.message);
|
|
198
|
+
this.text = job.result?.text ?? '';
|
|
199
|
+
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.';
|
|
200
|
+
if (job.status !== 'cancelled')
|
|
201
|
+
this.completed.emit({ ...job, language: this.language, text: this.text, audio: file, serviceType: 'upload' });
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (generation === this.generation)
|
|
205
|
+
this.report(error);
|
|
206
|
+
}
|
|
207
|
+
finally {
|
|
208
|
+
if (generation === this.generation) {
|
|
209
|
+
this.busy = false;
|
|
210
|
+
this.uploading = false;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FselSpeakingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
215
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.14", type: FselSpeakingComponent, isStandalone: true, selector: "fsel-speaking", inputs: { initialLanguage: "initialLanguage", uploadEnabled: "uploadEnabled", serviceType: "serviceType" }, outputs: { completed: "completed", failed: "failed" }, providers: [SpeakingService], ngImport: i0, template: `
|
|
216
|
+
<section aria-label="FSEL Speaking">
|
|
217
|
+
<label>Ngôn ngữ <select [value]="language" [disabled]="busy" (change)="selectLanguage($event)">
|
|
218
|
+
@for (item of languages; track item.code) { <option [value]="item.code" [selected]="item.code === language">{{ item.name }}</option> }
|
|
219
|
+
</select></label>
|
|
220
|
+
<div class="actions">
|
|
221
|
+
@if (serviceType !== 'upload') {
|
|
222
|
+
<button type="button" (click)="start()" [disabled]="busy">Bắt đầu nói</button>
|
|
223
|
+
<button type="button" (click)="stop()" [disabled]="!recording">Dừng và lấy kết quả</button>
|
|
224
|
+
}
|
|
225
|
+
<button type="button" (click)="cancel()" [disabled]="!busy || (uploading && !job)">Hủy</button>
|
|
226
|
+
</div>
|
|
227
|
+
@if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)
|
|
228
|
+
<input type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm" [disabled]="busy" (change)="upload($event)">
|
|
229
|
+
</label> }
|
|
230
|
+
<p role="status">{{ status }}</p>
|
|
231
|
+
@if (error) { <p role="alert" class="error">{{ error }}</p> }
|
|
232
|
+
<label>Văn bản nhận dạng <textarea rows="7" readonly [value]="text"></textarea></label>
|
|
233
|
+
</section>`, isInline: true, 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:#fff;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}\n"] });
|
|
234
|
+
}
|
|
235
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", ngImport: i0, type: FselSpeakingComponent, decorators: [{
|
|
236
|
+
type: Component,
|
|
237
|
+
args: [{ selector: 'fsel-speaking', standalone: true, providers: [SpeakingService], template: `
|
|
238
|
+
<section aria-label="FSEL Speaking">
|
|
239
|
+
<label>Ngôn ngữ <select [value]="language" [disabled]="busy" (change)="selectLanguage($event)">
|
|
240
|
+
@for (item of languages; track item.code) { <option [value]="item.code" [selected]="item.code === language">{{ item.name }}</option> }
|
|
241
|
+
</select></label>
|
|
242
|
+
<div class="actions">
|
|
243
|
+
@if (serviceType !== 'upload') {
|
|
244
|
+
<button type="button" (click)="start()" [disabled]="busy">Bắt đầu nói</button>
|
|
245
|
+
<button type="button" (click)="stop()" [disabled]="!recording">Dừng và lấy kết quả</button>
|
|
246
|
+
}
|
|
247
|
+
<button type="button" (click)="cancel()" [disabled]="!busy || (uploading && !job)">Hủy</button>
|
|
248
|
+
</div>
|
|
249
|
+
@if (serviceType !== 'realtime' && uploadEnabled) { <label>Chọn file audio (tối đa 25 MB)
|
|
250
|
+
<input type="file" accept="audio/*,.wav,.mp3,.m4a,.ogg,.flac,.webm" [disabled]="busy" (change)="upload($event)">
|
|
251
|
+
</label> }
|
|
252
|
+
<p role="status">{{ status }}</p>
|
|
253
|
+
@if (error) { <p role="alert" class="error">{{ error }}</p> }
|
|
254
|
+
<label>Văn bản nhận dạng <textarea rows="7" readonly [value]="text"></textarea></label>
|
|
255
|
+
</section>`, 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:#fff;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}\n"] }]
|
|
256
|
+
}], ctorParameters: () => [], propDecorators: { initialLanguage: [{
|
|
257
|
+
type: Input
|
|
258
|
+
}], uploadEnabled: [{
|
|
259
|
+
type: Input
|
|
260
|
+
}], serviceType: [{
|
|
261
|
+
type: Input
|
|
262
|
+
}], completed: [{
|
|
263
|
+
type: Output
|
|
264
|
+
}], failed: [{
|
|
265
|
+
type: Output
|
|
266
|
+
}] } });
|
|
46
267
|
|
|
47
268
|
/**
|
|
48
269
|
* Generated bundle index. Do not edit.
|
|
49
270
|
*/
|
|
50
271
|
|
|
51
|
-
export { SPEAKING_CONFIG, SpeakingService, provideSpeaking };
|
|
272
|
+
export { FselSpeakingComponent, SPEAKING_CONFIG, SpeakingService, provideSpeaking, provideSpeakingApp, provideSpeakingGateway };
|
|
52
273
|
//# sourceMappingURL=hoanghong-speaking-angular.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hoanghong-speaking-angular.mjs","sources":["../../src/public-api.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, RealtimeFinal, RealtimeOptions,\r\n RealtimeSpeakingClient, SpeakingEvent, SpeakingMicrophone, StartOptions,\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\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<RealtimeFinal> { 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\nexport * from '@hoanghong/speaking-core';\r\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":";;;;;;MAaa,eAAe,GAAG,IAAI,cAAc,CAAiB,sBAAsB;AAExF;MAEa,eAAe,CAAA;AAQoD,IAAA,IAAA;AAPrE,IAAA,KAAK;AACL,IAAA,QAAQ;AACA,IAAA,YAAY,GAAG,IAAI,OAAO,EAAiB;AACnD,IAAA,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,YAAY,EAAE;AAClC,IAAA,UAAU;AACV,IAAA,WAAW;IAE5B,WAAA,CAAqC,MAAsB,EAAmB,IAAY,EAAA;QAAZ,IAAA,CAAA,IAAI,GAAJ,IAAI;QAChF,IAAI,CAAC,KAAK,GAAG,IAAI,mBAAmB,CAAC,MAAM,CAAC,KAAK,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,IAAI,sBAAsB,CAAC,MAAM,CAAC,QAAQ,CAAC;AAC3D,QAAA,IAAI,CAAC,UAAU,GAAG,IAAI,kBAAkB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,CAAC;AAC1E,QAAA,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IACvG;AAEA,IAAA,KAAK,CAAC,OAAqB,EAAA;AACzB,QAAA,OAAO,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;IAC1E;IAEA,IAAI,GAAA,EAA6B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;IAChE,MAAM,GAAA,EAAoB,OAAO,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IAE3D,WAAW,GAAA;QACT,IAAI,CAAC,WAAW,EAAE;AAClB,QAAA,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE;AAC5B,QAAA,KAAK,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE;IAChC;AA1BW,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,kBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,SAAA,EAAA,QAAA,EAAA,EAAA,EAAA,IAAA,EAAA,eAAe,kBAQN,eAAe,EAAA,EAAA,EAAA,KAAA,EAAA,EAAA,CAAA,MAAA,EAAA,CAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,UAAA,EAAA,CAAA;4GARxB,eAAe,EAAA,CAAA;;4FAAf,eAAe,EAAA,UAAA,EAAA,CAAA;kBAD3B;;0BASc,MAAM;2BAAC,eAAe;;AAqB/B,SAAU,eAAe,CAAC,MAAsB,EAAA;AACpD,IAAA,OAAO,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,eAAe,CAAC;AAC1E;;AChDA;;AAEG;;"}
|
|
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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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hoanghong/speaking-angular",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Ready-to-use online speaking: five languages, microphone realtime and audio upload",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"peerDependencies": {
|
|
7
7
|
"@angular/common": ">=18 <19",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"sideEffects": false,
|
|
12
12
|
"license": "MIT",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@hoanghong/speaking-core": "^0.
|
|
14
|
+
"@hoanghong/speaking-core": "^0.3.0",
|
|
15
15
|
"tslib": "^2.3.0"
|
|
16
16
|
},
|
|
17
17
|
"publishConfig": {
|
package/public-api.d.ts
CHANGED
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as i0 from "@angular/core";
|
|
4
|
-
export interface SpeakingConfig {
|
|
5
|
-
batch: BatchOptions;
|
|
6
|
-
realtime: RealtimeOptions;
|
|
7
|
-
microphone: MicrophoneOptions;
|
|
8
|
-
}
|
|
9
|
-
export declare const SPEAKING_CONFIG: InjectionToken<SpeakingConfig>;
|
|
10
|
-
/** Provide at the screen/component level to give each speaking screen its own lifecycle. */
|
|
11
|
-
export declare class SpeakingService implements OnDestroy {
|
|
12
|
-
private readonly zone;
|
|
13
|
-
readonly batch: BatchSpeakingClient;
|
|
14
|
-
readonly realtime: RealtimeSpeakingClient;
|
|
15
|
-
private readonly eventSubject;
|
|
16
|
-
readonly events$: import("rxjs").Observable<SpeakingEvent>;
|
|
17
|
-
private readonly microphone;
|
|
18
|
-
private readonly unsubscribe;
|
|
19
|
-
constructor(config: SpeakingConfig, zone: NgZone);
|
|
20
|
-
start(options: StartOptions): Promise<void>;
|
|
21
|
-
stop(): Promise<RealtimeFinal>;
|
|
22
|
-
cancel(): Promise<void>;
|
|
23
|
-
ngOnDestroy(): void;
|
|
24
|
-
static ɵfac: i0.ɵɵFactoryDeclaration<SpeakingService, never>;
|
|
25
|
-
static ɵprov: i0.ɵɵInjectableDeclaration<SpeakingService>;
|
|
26
|
-
}
|
|
27
|
-
export declare function provideSpeaking(config: SpeakingConfig): Provider[];
|
|
28
|
-
export * from '@hoanghong/speaking-core';
|
|
1
|
+
export * from './speaking.service';
|
|
2
|
+
export * from './speaking.component';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { EventEmitter, OnInit } from '@angular/core';
|
|
2
|
+
import { Job, Language, SpeakingResult, SpeakingServiceType } from '@hoanghong/speaking-core';
|
|
3
|
+
import { SpeakingService } from './speaking.service';
|
|
4
|
+
import * as i0 from "@angular/core";
|
|
5
|
+
export declare class FselSpeakingComponent implements OnInit {
|
|
6
|
+
initialLanguage: Language;
|
|
7
|
+
uploadEnabled: boolean;
|
|
8
|
+
serviceType?: SpeakingServiceType;
|
|
9
|
+
completed: EventEmitter<SpeakingResult>;
|
|
10
|
+
failed: EventEmitter<Error>;
|
|
11
|
+
readonly speaking: SpeakingService;
|
|
12
|
+
private readonly destroy;
|
|
13
|
+
readonly languages: {
|
|
14
|
+
code: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}[];
|
|
17
|
+
language: Language;
|
|
18
|
+
busy: boolean;
|
|
19
|
+
recording: boolean;
|
|
20
|
+
uploading: boolean;
|
|
21
|
+
text: string;
|
|
22
|
+
error: string;
|
|
23
|
+
status: string;
|
|
24
|
+
job?: Job;
|
|
25
|
+
private generation;
|
|
26
|
+
private abort?;
|
|
27
|
+
constructor();
|
|
28
|
+
ngOnInit(): void;
|
|
29
|
+
selectLanguage(event: Event): void;
|
|
30
|
+
private report;
|
|
31
|
+
start(): Promise<void>;
|
|
32
|
+
stop(): Promise<void>;
|
|
33
|
+
cancel(): Promise<void>;
|
|
34
|
+
upload(event: Event): Promise<void>;
|
|
35
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FselSpeakingComponent, never>;
|
|
36
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<FselSpeakingComponent, "fsel-speaking", never, { "initialLanguage": { "alias": "initialLanguage"; "required": false; }; "uploadEnabled": { "alias": "uploadEnabled"; "required": false; }; "serviceType": { "alias": "serviceType"; "required": false; }; }, { "completed": "completed"; "failed": "failed"; }, never, never, true, never>;
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { InjectionToken, NgZone, OnDestroy, Provider } from '@angular/core';
|
|
2
|
+
import { BatchOptions, BatchSpeakingClient, MicrophoneOptions, RecordedSpeakingFinal, RealtimeOptions, RealtimeSpeakingClient, SpeakingEvent, StartOptions, SpeakingGatewayOptions } from '@hoanghong/speaking-core';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
export interface SpeakingConfig {
|
|
5
|
+
batch: BatchOptions;
|
|
6
|
+
realtime: RealtimeOptions;
|
|
7
|
+
microphone: MicrophoneOptions;
|
|
8
|
+
}
|
|
9
|
+
export declare const SPEAKING_CONFIG: InjectionToken<SpeakingConfig>;
|
|
10
|
+
/** Provide at the screen/component level to give each speaking screen its own lifecycle. */
|
|
11
|
+
export declare class SpeakingService implements OnDestroy {
|
|
12
|
+
private readonly zone;
|
|
13
|
+
readonly batch: BatchSpeakingClient;
|
|
14
|
+
readonly realtime: RealtimeSpeakingClient;
|
|
15
|
+
private readonly eventSubject;
|
|
16
|
+
readonly events$: import("rxjs").Observable<SpeakingEvent>;
|
|
17
|
+
private readonly microphone;
|
|
18
|
+
private readonly unsubscribe;
|
|
19
|
+
constructor(config: SpeakingConfig, zone: NgZone);
|
|
20
|
+
start(options: StartOptions): Promise<void>;
|
|
21
|
+
stop(): Promise<RecordedSpeakingFinal>;
|
|
22
|
+
cancel(): Promise<void>;
|
|
23
|
+
ngOnDestroy(): void;
|
|
24
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<SpeakingService, never>;
|
|
25
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<SpeakingService>;
|
|
26
|
+
}
|
|
27
|
+
export declare function provideSpeaking(config: SpeakingConfig): Provider[];
|
|
28
|
+
/** Configure once at app bootstrap; every fsel-speaking component owns its session. */
|
|
29
|
+
export declare function provideSpeakingGateway(options?: SpeakingGatewayOptions): Provider[];
|
|
30
|
+
/** Application identity only; hosted endpoint and short-lived sessions are automatic. */
|
|
31
|
+
export declare function provideSpeakingApp(options: {
|
|
32
|
+
appKey: string;
|
|
33
|
+
}): Provider[];
|
|
34
|
+
export * from '@hoanghong/speaking-core';
|