@chat21/chat21-web-widget 5.1.34-rc1 → 5.1.34

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.
Files changed (193) hide show
  1. package/.github/workflows/docker-community-push-latest.yml +13 -23
  2. package/.github/workflows/docker-image-tag-community-tag-push.yml +12 -22
  3. package/CHANGELOG.md +8 -129
  4. package/Dockerfile +5 -4
  5. package/angular.json +3 -21
  6. package/docs/changelog/this-branch.md +0 -36
  7. package/env.sample +2 -3
  8. package/nginx.conf +2 -22
  9. package/package.json +3 -10
  10. package/src/app/app.component.html +2 -2
  11. package/src/app/app.component.scss +14 -25
  12. package/src/app/app.component.spec.ts +6 -21
  13. package/src/app/app.component.ts +9 -10
  14. package/src/app/app.module.ts +0 -13
  15. package/src/app/component/conversation-detail/conversation/conversation.component.html +11 -25
  16. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -40
  17. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +75 -644
  18. package/src/app/component/conversation-detail/conversation/conversation.component.ts +14 -100
  19. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +13 -25
  20. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +5 -123
  21. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +0 -1
  22. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +10 -23
  23. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -19
  24. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +149 -242
  25. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +5 -8
  26. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +3 -53
  27. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +96 -200
  28. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +6 -211
  29. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +78 -452
  30. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +76 -291
  31. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +53 -113
  32. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +4 -12
  33. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +29 -274
  34. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +9 -23
  35. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +8 -80
  36. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +23 -29
  37. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +16 -185
  38. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +14 -34
  39. package/src/app/component/error-alert/error-alert.component.spec.ts +5 -65
  40. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +7 -16
  41. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +0 -21
  42. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +7 -89
  43. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  44. package/src/app/component/form/form-builder/form-builder.component.spec.ts +21 -163
  45. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +4 -8
  46. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +5 -10
  47. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +16 -90
  48. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +0 -26
  49. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +11 -45
  50. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +6 -24
  51. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +5 -14
  52. package/src/app/component/form/inputs/form-text/form-text.component.html +12 -14
  53. package/src/app/component/form/inputs/form-text/form-text.component.scss +1 -11
  54. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +17 -113
  55. package/src/app/component/form/inputs/form-text/form-text.component.ts +3 -35
  56. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +11 -13
  57. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +5 -6
  58. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +13 -149
  59. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +0 -26
  60. package/src/app/component/form/prechat-form/prechat-form.component.html +11 -14
  61. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +10 -102
  62. package/src/app/component/form/prechat-form/prechat-form.component.ts +1 -8
  63. package/src/app/component/home/home.component.html +31 -38
  64. package/src/app/component/home/home.component.scss +2 -4
  65. package/src/app/component/home/home.component.spec.ts +11 -226
  66. package/src/app/component/home-conversations/home-conversations.component.html +26 -30
  67. package/src/app/component/home-conversations/home-conversations.component.scss +0 -3
  68. package/src/app/component/home-conversations/home-conversations.component.spec.ts +36 -212
  69. package/src/app/component/last-message/last-message.component.html +9 -15
  70. package/src/app/component/last-message/last-message.component.scss +2 -16
  71. package/src/app/component/last-message/last-message.component.spec.ts +23 -204
  72. package/src/app/component/last-message/last-message.component.ts +1 -4
  73. package/src/app/component/launcher-button/launcher-button.component.html +13 -8
  74. package/src/app/component/launcher-button/launcher-button.component.spec.ts +8 -104
  75. package/src/app/component/list-all-conversations/list-all-conversations.component.html +17 -12
  76. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +0 -2
  77. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  78. package/src/app/component/menu-options/menu-options.component.html +20 -30
  79. package/src/app/component/menu-options/menu-options.component.spec.ts +9 -125
  80. package/src/app/component/message/audio/audio.component.html +15 -13
  81. package/src/app/component/message/audio/audio.component.spec.ts +5 -140
  82. package/src/app/component/message/audio/audio.component.ts +5 -1
  83. package/src/app/component/message/avatar/avatar.component.html +2 -2
  84. package/src/app/component/message/avatar/avatar.component.spec.ts +7 -99
  85. package/src/app/component/message/bubble-message/bubble-message.component.html +51 -38
  86. package/src/app/component/message/bubble-message/bubble-message.component.scss +1 -54
  87. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +57 -154
  88. package/src/app/component/message/bubble-message/bubble-message.component.ts +11 -89
  89. package/src/app/component/message/buttons/action-button/action-button.component.html +4 -3
  90. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +5 -49
  91. package/src/app/component/message/buttons/link-button/link-button.component.scss +8 -5
  92. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +5 -50
  93. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +5 -44
  94. package/src/app/component/message/carousel/carousel.component.html +16 -29
  95. package/src/app/component/message/carousel/carousel.component.scss +8 -20
  96. package/src/app/component/message/carousel/carousel.component.spec.ts +3 -80
  97. package/src/app/component/message/carousel/carousel.component.ts +0 -16
  98. package/src/app/component/message/frame/frame.component.html +4 -9
  99. package/src/app/component/message/frame/frame.component.spec.ts +15 -34
  100. package/src/app/component/message/frame/frame.component.ts +2 -7
  101. package/src/app/component/message/html/html.component.html +1 -1
  102. package/src/app/component/message/html/html.component.scss +1 -1
  103. package/src/app/component/message/html/html.component.spec.ts +7 -24
  104. package/src/app/component/message/image/image.component.html +10 -12
  105. package/src/app/component/message/image/image.component.scss +0 -16
  106. package/src/app/component/message/image/image.component.spec.ts +15 -101
  107. package/src/app/component/message/image/image.component.ts +51 -90
  108. package/src/app/component/message/info-message/info-message.component.spec.ts +14 -26
  109. package/src/app/component/message/like-unlike/like-unlike.component.html +9 -7
  110. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +3 -31
  111. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +17 -38
  112. package/src/app/component/message/text/text.component.html +3 -3
  113. package/src/app/component/message/text/text.component.scss +86 -80
  114. package/src/app/component/message/text/text.component.spec.ts +13 -106
  115. package/src/app/component/message-attachment/message-attachment.component.spec.ts +13 -134
  116. package/src/app/component/selection-department/selection-department.component.html +23 -21
  117. package/src/app/component/selection-department/selection-department.component.spec.ts +14 -159
  118. package/src/app/component/selection-department/selection-department.component.ts +1 -8
  119. package/src/app/component/send-button/send-button.component.html +13 -5
  120. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  121. package/src/app/component/star-rating-widget/star-rating-widget.component.html +81 -51
  122. package/src/app/directives/tooltip.directive.spec.ts +4 -8
  123. package/src/app/modals/confirm-close/confirm-close.component.html +8 -20
  124. package/src/app/modals/confirm-close/confirm-close.component.scss +0 -3
  125. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +4 -13
  126. package/src/app/modals/confirm-close/confirm-close.component.ts +1 -8
  127. package/src/app/pipe/html-entites-encode.pipe.spec.ts +2 -35
  128. package/src/app/pipe/marked.pipe.spec.ts +2 -38
  129. package/src/app/pipe/marked.pipe.ts +41 -51
  130. package/src/app/providers/app-config.service.ts +2 -4
  131. package/src/app/providers/brand.service.spec.ts +2 -23
  132. package/src/app/providers/brand.service.ts +1 -1
  133. package/src/app/providers/global-settings.service.spec.ts +14 -1009
  134. package/src/app/providers/global-settings.service.ts +2 -82
  135. package/src/app/providers/translator.service.ts +6 -26
  136. package/src/app/sass/_variables.scss +0 -3
  137. package/src/app/sass/animations.scss +1 -19
  138. package/src/app/utils/globals.ts +1 -21
  139. package/src/app/utils/utils-resources.ts +1 -1
  140. package/src/assets/i18n/en.json +99 -106
  141. package/src/assets/i18n/es.json +100 -107
  142. package/src/assets/i18n/fr.json +100 -107
  143. package/src/assets/i18n/it.json +98 -107
  144. package/src/assets/twp/index-dev.html +0 -18
  145. package/src/chat21-core/models/message.ts +1 -2
  146. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +2 -3
  147. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +0 -12
  148. package/src/chat21-core/providers/scripts/script.service.spec.ts +2 -12
  149. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  150. package/src/chat21-core/utils/utils-message.ts +0 -7
  151. package/src/chat21-core/utils/utils.ts +2 -5
  152. package/src/widget-config-template.json +1 -4
  153. package/src/widget-config.json +1 -4
  154. package/tsconfig.json +0 -5
  155. package/.angular-mcp-cache/package.json +0 -1
  156. package/.cursor/angular18-accessibility-auditor-skill.md +0 -442
  157. package/.cursor/mcp.json +0 -15
  158. package/.github/workflows/build.yml +0 -22
  159. package/.github/workflows/playwright.yml +0 -27
  160. package/mocks/voice-websocket-mock/server.cjs +0 -245
  161. package/playwright.config.ts +0 -41
  162. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +0 -46
  163. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +0 -83
  164. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +0 -192
  165. package/src/app/component/form/prechat-form-test-mock.ts +0 -35
  166. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -18
  167. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -65
  168. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +0 -103
  169. package/src/app/component/message/audio-sync/audio-sync.component.ts +0 -643
  170. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +0 -117
  171. package/src/app/providers/tts-audio-playback-coordinator.service.ts +0 -109
  172. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +0 -12
  173. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +0 -171
  174. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +0 -39
  175. package/src/app/providers/voice/audio.types.ts +0 -40
  176. package/src/app/providers/voice/vad.service.spec.ts +0 -28
  177. package/src/app/providers/voice/vad.service.ts +0 -70
  178. package/src/app/providers/voice/voice-streaming.service.spec.ts +0 -23
  179. package/src/app/providers/voice/voice-streaming.service.ts +0 -702
  180. package/src/app/providers/voice/voice-streaming.types.ts +0 -112
  181. package/src/app/providers/voice/voice.service.spec.ts +0 -227
  182. package/src/app/providers/voice/voice.service.ts +0 -973
  183. package/src/app/shims/onnxruntime-web-wasm.ts +0 -4
  184. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +0 -59
  185. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  186. package/src/assets/sounds/keyboard.mp3 +0 -0
  187. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +0 -14
  188. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  189. package/src/assets/vad/vad.worklet.bundle.min.js +0 -1
  190. package/src/chat21-core/providers/chat-manager.spec.ts +0 -72
  191. package/tests/widget-form-rich.spec.ts +0 -67
  192. package/tests/widget-index-dev-settings.spec.ts +0 -52
  193. package/tests/widget-twp-iframe.spec.ts +0 -39
@@ -1,117 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { TtsAudioPlaybackCoordinator } from './tts-audio-playback-coordinator.service';
3
-
4
- describe('TtsAudioPlaybackCoordinator', () => {
5
- let coordinator: TtsAudioPlaybackCoordinator;
6
-
7
- beforeEach(() => {
8
- TestBed.configureTestingModule({ providers: [TtsAudioPlaybackCoordinator] });
9
- coordinator = TestBed.inject(TtsAudioPlaybackCoordinator);
10
- });
11
-
12
- // ── Basic lifecycle ───────────────────────────────────────────────────────
13
-
14
- it('should start playing immediately when nothing is active', () => {
15
- const start = jasmine.createSpy('start');
16
- coordinator.requestStart('msg-1', start);
17
- expect(start).toHaveBeenCalledTimes(1);
18
- });
19
-
20
- it('isTTSPlaying$ should be true while playing and false after release', () => {
21
- const states: boolean[] = [];
22
- coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
23
-
24
- coordinator.requestStart('msg-1', () => {});
25
- coordinator.releaseIfCurrent('msg-1');
26
-
27
- expect(states).toEqual([false, true, false]);
28
- });
29
-
30
- it('stopAll clears the queue, sets playing=false, and emits stopAllPlayback$', () => {
31
- const stopNextSpy = spyOn((coordinator as any)._stopAll$, 'next').and.callThrough();
32
-
33
- coordinator.requestStart('msg-1', () => {});
34
- coordinator.stopAll();
35
-
36
- expect(stopNextSpy).toHaveBeenCalledTimes(1);
37
- const states: boolean[] = [];
38
- coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
39
- expect(states).toEqual([false]);
40
- });
41
-
42
- // ── Preemption tests (SPEC-002) ───────────────────────────────────────────
43
-
44
- it('requestStart while playing preempts old owner: new start() is called immediately', () => {
45
- const start1 = jasmine.createSpy('start1');
46
- const start2 = jasmine.createSpy('start2');
47
-
48
- coordinator.requestStart('msg-1', start1);
49
- coordinator.requestStart('msg-2', start2);
50
-
51
- expect(start1).toHaveBeenCalledTimes(1);
52
- expect(start2).toHaveBeenCalledTimes(1); // started immediately, not queued
53
- });
54
-
55
- it('preemptPlayback$ emits evicted ownerId only (not the new owner)', () => {
56
- const preempted: string[] = [];
57
- coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
58
-
59
- coordinator.requestStart('msg-1', () => {});
60
- coordinator.requestStart('msg-2', () => {}); // preempts msg-1
61
-
62
- expect(preempted).toEqual(['msg-1']);
63
- });
64
-
65
- it('preemptPlayback$ does NOT emit the new owner id', () => {
66
- const preempted: string[] = [];
67
- coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
68
-
69
- coordinator.requestStart('msg-1', () => {});
70
- coordinator.requestStart('msg-2', () => {});
71
-
72
- expect(preempted).not.toContain('msg-2');
73
- });
74
-
75
- it('isTTSPlaying$ stays true after preemption until new owner releases', () => {
76
- const states: boolean[] = [];
77
- coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
78
-
79
- coordinator.requestStart('msg-1', () => {}); // true
80
- coordinator.requestStart('msg-2', () => {}); // still true (preemption, new owner active)
81
- coordinator.releaseIfCurrent('msg-2'); // false
82
-
83
- expect(states).toEqual([false, true, false]);
84
- });
85
-
86
- it('releaseIfCurrent for an evicted owner is a no-op', () => {
87
- const states: boolean[] = [];
88
- coordinator.isTTSPlaying$.subscribe((v) => states.push(v));
89
-
90
- coordinator.requestStart('msg-1', () => {});
91
- coordinator.requestStart('msg-2', () => {}); // msg-1 evicted
92
-
93
- // Old owner calls release after being preempted — should not affect playing state
94
- coordinator.releaseIfCurrent('msg-1');
95
-
96
- expect(states).toEqual([false, true]); // no extra false emission
97
- });
98
-
99
- it('chain of preemptions: each new requestStart immediately evicts the current owner', () => {
100
- const preempted: string[] = [];
101
- coordinator.preemptPlayback$.subscribe((id) => preempted.push(id));
102
-
103
- coordinator.requestStart('msg-1', () => {});
104
- coordinator.requestStart('msg-2', () => {});
105
- coordinator.requestStart('msg-3', () => {});
106
-
107
- expect(preempted).toEqual(['msg-1', 'msg-2']);
108
- });
109
-
110
- it('requestStart is idempotent for the current owner', () => {
111
- const start = jasmine.createSpy('start');
112
- coordinator.requestStart('msg-1', start);
113
- coordinator.requestStart('msg-1', start); // same owner — should be ignored
114
-
115
- expect(start).toHaveBeenCalledTimes(1);
116
- });
117
- });
@@ -1,109 +0,0 @@
1
- import { Injectable } from '@angular/core';
2
- import { BehaviorSubject, Observable, Subject } from 'rxjs';
3
-
4
- /**
5
- * Garantisce un solo messaggio TTS in riproduzione alla volta.
6
- * Quando arriva un nuovo messaggio TTS mentre un altro è in corso, quello vecchio viene
7
- * interrotto immediatamente (preemption) e il nuovo parte subito.
8
- */
9
- @Injectable({ providedIn: 'root' })
10
- export class TtsAudioPlaybackCoordinator {
11
- private currentOwnerId: string | null = null;
12
- private readonly queue: Array<{ ownerId: string; start: () => void }> = [];
13
-
14
- /** Emits true while any TTS is playing or queued; false when the queue is fully drained. */
15
- private readonly _isTTSPlaying$ = new BehaviorSubject<boolean>(false);
16
- readonly isTTSPlaying$ = this._isTTSPlaying$.asObservable();
17
-
18
- /** Emits once when stopAll() is called — signals every AudioSyncComponent to abort immediately. */
19
- private readonly _stopAll$ = new Subject<void>();
20
- readonly stopAllPlayback$: Observable<void> = this._stopAll$.asObservable();
21
-
22
- /**
23
- * Emits the ownerId of the component being preempted (stopped mid-playback by a newer message).
24
- * Only the component whose ownerId matches should react — unlike stopAll$ which targets everyone.
25
- */
26
- private readonly _preemptCurrent$ = new Subject<string>();
27
- readonly preemptPlayback$: Observable<string> = this._preemptCurrent$.asObservable();
28
-
29
- /**
30
- * Richiede l'avvio della riproduzione TTS per `ownerId`.
31
- * Se un altro TTS è già in corso, viene interrotto immediatamente (preemption) e
32
- * `ownerId` parte subito. Qualsiasi coda pendente viene svuotata.
33
- */
34
- requestStart(ownerId: string, start: () => void): void {
35
- const id = (ownerId || '').trim();
36
- if (!id) {
37
- return;
38
- }
39
- if (this.currentOwnerId === id) {
40
- return;
41
- }
42
-
43
- if (this.currentOwnerId) {
44
- // Preempt: signal only the evicted owner to stop (not a broadcast stopAll).
45
- // This avoids stopping the component that is about to start playing.
46
- const evicted = this.currentOwnerId;
47
- this.queue.length = 0;
48
- this.currentOwnerId = null;
49
- this._preemptCurrent$.next(evicted);
50
- } else {
51
- this.queue.length = 0;
52
- }
53
-
54
- this.currentOwnerId = id;
55
- if (!this._isTTSPlaying$.getValue()) {
56
- this._isTTSPlaying$.next(true);
57
- }
58
- try {
59
- start();
60
- } catch {
61
- this.releaseIfCurrent(id);
62
- }
63
- }
64
-
65
- /** Chiamare a fine riproduzione naturale (`ended`) se questo messaggio era ancora “attivo”. */
66
- releaseIfCurrent(ownerId: string): void {
67
- const id = (ownerId || '').trim();
68
- if (!id) {
69
- return;
70
- }
71
- if (this.currentOwnerId !== id) {
72
- // Se era in coda, rimuovilo.
73
- const idx = this.queue.findIndex((j) => j.ownerId === id);
74
- if (idx !== -1) {
75
- this.queue.splice(idx, 1);
76
- }
77
- return;
78
- }
79
-
80
- this.currentOwnerId = null;
81
- const next = this.queue.shift();
82
- if (!next) {
83
- this._isTTSPlaying$.next(false);
84
- return;
85
- }
86
- this.currentOwnerId = next.ownerId;
87
- try {
88
- next.start();
89
- } catch {
90
- this.releaseIfCurrent(next.ownerId);
91
- }
92
- }
93
-
94
- /** Distruzione componente o stop esplicito. */
95
- release(ownerId: string): void {
96
- this.releaseIfCurrent(ownerId);
97
- }
98
-
99
- /**
100
- * Stops all TTS playback immediately and clears the queue.
101
- * Broadcasts on stopAllPlayback$ so every AudioSyncComponent can abort its stream and reveal all text.
102
- */
103
- stopAll(): void {
104
- this.queue.length = 0;
105
- this.currentOwnerId = null;
106
- this._isTTSPlaying$.next(false);
107
- this._stopAll$.next();
108
- }
109
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * Configurazione opzionale per i servizi voce OpenAI (da `environment` o runtime).
3
- */
4
- export interface OpenAiVoiceEnvironmentConfig {
5
- /** Obbligatoria per chiamate API reali; se assente, STT/TTS non inviano richieste. */
6
- apiKey?: string;
7
- baseUrl?: string;
8
- transcriptionModel?: string;
9
- ttsModel?: string;
10
- /** Voce predefinita TTS (es. `alloy`). */
11
- ttsVoice?: string;
12
- }
@@ -1,171 +0,0 @@
1
- import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http';
2
- import { Injectable } from '@angular/core';
3
- import { firstValueFrom } from 'rxjs';
4
- import { environment } from 'src/environments/environment';
5
-
6
- import type { OpenAiVoiceEnvironmentConfig } from './openai-voice.config';
7
- import {
8
- SpeechToTextProvider,
9
- TextToSpeechProvider,
10
- type SpeechToTextRequest,
11
- type SpeechToTextResult,
12
- type TextToSpeechRequest,
13
- type TextToSpeechResult,
14
- } from './speech-provider.abstract';
15
- import { AppConfigService } from '../../app-config.service';
16
-
17
- const DEFAULT_BASE = 'https://api.openai.com/v1';
18
- const DEFAULT_TRANSCRIPTION_MODEL = 'whisper-1';
19
- const DEFAULT_TTS_MODEL = 'tts-1';
20
- const DEFAULT_VOICE = 'alloy';
21
- const DEFAULT_FORMAT = 'mp3';
22
-
23
- /**
24
- * Provider OpenAI unico: STT (Whisper) + TTS, entrambi via {@link HttpClient}.
25
- */
26
- @Injectable({ providedIn: 'root' })
27
- export class OpenAiVoiceProviderService extends SpeechToTextProvider implements TextToSpeechProvider {
28
- constructor(
29
- private readonly httpClient: HttpClient,
30
- private readonly appConfig: AppConfigService
31
- ) {
32
- super();
33
- }
34
-
35
- async transcribe(request: SpeechToTextRequest): Promise<SpeechToTextResult> {
36
- const cfg = this.getConfig();
37
- const apiKey = cfg.apiKey?.trim();
38
- if (!apiKey) {
39
- return { text: '' };
40
- }
41
-
42
- const base = (cfg.baseUrl ?? DEFAULT_BASE).replace(/\/$/, '');
43
- const model = cfg.transcriptionModel ?? DEFAULT_TRANSCRIPTION_MODEL;
44
- const url = `${base}/audio/transcriptions`;
45
-
46
- const ext = this.extensionForMime(request.mimeType);
47
- const file = new File([request.audio], `segment.${ext}`, { type: request.mimeType });
48
-
49
- const form = new FormData();
50
- form.append('file', file);
51
- form.append('model', model);
52
- if (request.language) {
53
- form.append('language', request.language);
54
- }
55
-
56
- const headers = new HttpHeaders({
57
- Authorization: `Bearer ${apiKey}`,
58
- });
59
-
60
- try {
61
- const data = await firstValueFrom(
62
- this.httpClient.post<{ text?: string }>(url, form, { headers }),
63
- );
64
- return { text: (data.text ?? '').trim() };
65
- } catch (e) {
66
- if (e instanceof HttpErrorResponse && e.error instanceof Blob) {
67
- const errText = await e.error.text();
68
- throw new Error(`OpenAI transcription ${e.status}: ${errText || e.statusText}`);
69
- }
70
- throw this.mapOpenAiHttpError(e);
71
- }
72
- }
73
-
74
- async synthesize(request: TextToSpeechRequest): Promise<TextToSpeechResult> {
75
- const cfg = this.getConfig();
76
- const apiKey = cfg.apiKey?.trim();
77
- if (!apiKey) {
78
- throw new Error('OpenAI API key not configured (environment.openAiVoice.apiKey)');
79
- }
80
-
81
- const base = (cfg.baseUrl ?? DEFAULT_BASE).replace(/\/$/, '');
82
- const model = cfg.ttsModel ?? DEFAULT_TTS_MODEL;
83
- const voice = request.voice ?? cfg.ttsVoice ?? DEFAULT_VOICE;
84
- const responseFormat =
85
- (request.responseFormat as 'mp3' | 'opus' | 'aac' | 'flac' | undefined) ?? DEFAULT_FORMAT;
86
- const url = `${base}/audio/speech`;
87
-
88
- const body = {
89
- model,
90
- voice,
91
- input: request.text,
92
- response_format: responseFormat,
93
- };
94
-
95
- const headers = new HttpHeaders({
96
- Authorization: `Bearer ${apiKey}`,
97
- 'Content-Type': 'application/json',
98
- });
99
-
100
- try {
101
- const blob = await firstValueFrom(
102
- this.httpClient.post(url, body, {
103
- headers,
104
- responseType: 'blob',
105
- }),
106
- );
107
- return { audio: blob, mimeType: this.mimeForFormat(responseFormat) };
108
- } catch (e) {
109
- if (e instanceof HttpErrorResponse && e.error instanceof Blob) {
110
- const errText = await e.error.text();
111
- throw new Error(`OpenAI TTS ${e.status}: ${errText || e.statusText}`);
112
- }
113
- if (e instanceof HttpErrorResponse) {
114
- throw new Error(`OpenAI TTS ${e.status}: ${e.message || e.statusText}`);
115
- }
116
- throw e;
117
- }
118
- }
119
-
120
- private getConfig(): OpenAiVoiceEnvironmentConfig {
121
- return this.appConfig.getConfig().openAiKey ?? {};
122
- }
123
-
124
- private mapOpenAiHttpError(e: unknown): Error {
125
- if (!(e instanceof HttpErrorResponse)) {
126
- return e instanceof Error ? e : new Error(String(e));
127
- }
128
- const label = 'OpenAI transcription';
129
- if (e.error instanceof Blob) {
130
- return new Error(`${label} ${e.status}: ${e.statusText}`);
131
- }
132
- if (typeof e.error === 'object' && e.error !== null && 'error' in e.error) {
133
- const err = (e.error as { error?: { message?: string } }).error;
134
- return new Error(`${label} ${e.status}: ${err?.message ?? JSON.stringify(e.error)}`);
135
- }
136
- if (typeof e.error === 'string') {
137
- return new Error(`${label} ${e.status}: ${e.error}`);
138
- }
139
- return new Error(`${label} ${e.status}: ${e.message || e.statusText}`);
140
- }
141
-
142
- private extensionForMime(mime: string): string {
143
- if (mime.includes('webm')) {
144
- return 'webm';
145
- }
146
- if (mime.includes('mp4') || mime.includes('m4a')) {
147
- return 'm4a';
148
- }
149
- if (mime.includes('wav')) {
150
- return 'wav';
151
- }
152
- if (mime.includes('mpeg') || mime.includes('mp3')) {
153
- return 'mp3';
154
- }
155
- return 'webm';
156
- }
157
-
158
- private mimeForFormat(fmt: string): string {
159
- switch (fmt) {
160
- case 'opus':
161
- return 'audio/opus';
162
- case 'aac':
163
- return 'audio/aac';
164
- case 'flac':
165
- return 'audio/flac';
166
- case 'mp3':
167
- default:
168
- return 'audio/mpeg';
169
- }
170
- }
171
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Contratti astratti per Speech-to-Text e Text-to-Speech.
3
- * Implementazione OpenAI unificata: `OpenAiVoiceProviderService` (`openai-voice.provider.ts`).
4
- */
5
-
6
- /** Input per la trascrizione di un segmento audio. */
7
- export interface SpeechToTextRequest {
8
- audio: Blob;
9
- mimeType: string;
10
- /** ISO 639-1 opzionale (es. `it`, `en`). */
11
- language?: string;
12
- }
13
-
14
- export interface SpeechToTextResult {
15
- text: string;
16
- }
17
-
18
- /** Input per la sintesi vocale. */
19
- export interface TextToSpeechRequest {
20
- text: string;
21
- /** Voce provider-specific (es. OpenAI: `alloy`, `echo`, …). */
22
- voice?: string;
23
- language?: string;
24
- /** Formato audio desiderato (dipende dal provider). */
25
- responseFormat?: string;
26
- }
27
-
28
- export interface TextToSpeechResult {
29
- audio: Blob;
30
- mimeType: string;
31
- }
32
-
33
- export abstract class SpeechToTextProvider {
34
- abstract transcribe(request: SpeechToTextRequest): Promise<SpeechToTextResult>;
35
- }
36
-
37
- export abstract class TextToSpeechProvider {
38
- abstract synthesize(request: TextToSpeechRequest): Promise<TextToSpeechResult>;
39
- }
@@ -1,40 +0,0 @@
1
- /**
2
- * Tipi condivisi per cattura microfono, VAD e registrazione (WebM).
3
- */
4
- import type { VoiceStreamingSessionConfig } from './voice-streaming.types';
5
-
6
- export const DEFAULT_VOICE_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
7
- echoCancellation: true,
8
- noiseSuppression: true,
9
- autoGainControl: true,
10
- };
11
-
12
- export const DEFAULT_VOICE_MEDIA_STREAM_CONSTRAINTS: MediaStreamConstraints = {
13
- audio: DEFAULT_VOICE_AUDIO_CONSTRAINTS,
14
- };
15
-
16
- export interface VoiceRecordedBlob {
17
- blob: Blob;
18
- mimeType: string;
19
- }
20
-
21
- /**
22
- * Segmento audio dopo VAD; può includere `transcript` se STT è configurato e abilitato.
23
- */
24
- export interface VoiceSegmentPayload extends VoiceRecordedBlob {
25
- transcript?: string;
26
- transcriptionError?: string;
27
- }
28
-
29
- export interface VoiceSessionStartOptions {
30
- /** Opzionale se usi solo {@link VoiceService.audioSegment$}. */
31
- onRecordingComplete?: (result: VoiceSegmentPayload) => void;
32
- constraints?: MediaStreamConstraints;
33
- /** Default `true`. Se `false`, non viene chiamato lo STT sul segmento. */
34
- enableTranscription?: boolean;
35
- /**
36
- * Con `voiceIngressStream`: solo streaming WSS — niente VAD locale; transcript e TTS dal server.
37
- * Senza: MicVAD + segmenti e upload/STT lato client.
38
- */
39
- voiceIngressStream?: VoiceStreamingSessionConfig | null;
40
- }
@@ -1,28 +0,0 @@
1
- import { Location } from '@angular/common';
2
- import { TestBed } from '@angular/core/testing';
3
-
4
- import { VadService } from './vad.service';
5
-
6
- describe('VadService', () => {
7
- let service: VadService;
8
-
9
- beforeEach(() => {
10
- TestBed.configureTestingModule({
11
- providers: [
12
- VadService,
13
- {
14
- provide: Location,
15
- useValue: {
16
- prepareExternalUrl: (url: string) => `/${url}`,
17
- },
18
- },
19
- ],
20
- });
21
- service = TestBed.inject(VadService);
22
- });
23
-
24
- it('should expose VAD and ONNX WASM base URLs with trailing slash', () => {
25
- expect(service.getVadAssetBaseUrl()).toBe('/assets/vad/');
26
- expect(service.getOnnxWasmBaseUrl()).toBe('/assets/onnx/');
27
- });
28
- });
@@ -1,70 +0,0 @@
1
- import { Location } from '@angular/common';
2
- import { Injectable } from '@angular/core';
3
- import { MicVAD, getDefaultRealTimeVADOptions } from '@ricky0123/vad-web';
4
- import type { RealTimeVADOptions } from '@ricky0123/vad-web';
5
-
6
- /**
7
- * MicVAD (@ricky0123/vad-web): modelli in assets/vad/, WASM ONNX in assets/onnx/
8
- * (allineato a ort.env.wasm.wasmPaths = "/assets/onnx/").
9
- */
10
- @Injectable({ providedIn: 'root' })
11
- export class VadService {
12
- private onnxRuntimeEnvPromise: Promise<void> | null = null;
13
-
14
- constructor(private readonly location: Location) {}
15
-
16
- /**
17
- * Base URL per silero_vad_legacy.onnx / vad.worklet.bundle.min.js
18
- * (MicVAD usa baseAssetPath + nome file interno, non modelURL singolo).
19
- */
20
- getVadAssetBaseUrl(): string {
21
- return this.ensureTrailingSlash(this.location.prepareExternalUrl('assets/vad/'));
22
- }
23
-
24
- /** Base URL per ort-wasm-*.mjs / .wasm (es. /assets/onnx/). */
25
- getOnnxWasmBaseUrl(): string {
26
- return this.ensureTrailingSlash(this.location.prepareExternalUrl('assets/onnx/'));
27
- }
28
-
29
- /**
30
- * Pre-configura il modulo onnxruntime-web/wasm (stesso usato da MicVAD):
31
- * wasmPaths + numThreads prima del primo MicVAD.new.
32
- */
33
- ensureOnnxRuntimeEnv(): Promise<void> {
34
- if (!this.onnxRuntimeEnvPromise) {
35
- this.onnxRuntimeEnvPromise = (async () => {
36
- const ort = await import('onnxruntime-web/wasm');
37
- const wasmBase = this.getOnnxWasmBaseUrl();
38
- ort.env.wasm.wasmPaths = wasmBase;
39
- ort.env.wasm.numThreads = 1;
40
- ort.env.logLevel = 'error';
41
- })();
42
- }
43
- return this.onnxRuntimeEnvPromise;
44
- }
45
-
46
- async createMicVad(overrides: Partial<RealTimeVADOptions>): Promise<MicVAD> {
47
- await this.ensureOnnxRuntimeEnv();
48
- const base = getDefaultRealTimeVADOptions('legacy');
49
- const vadBase = this.getVadAssetBaseUrl();
50
- const ortWasmBase = this.getOnnxWasmBaseUrl();
51
-
52
- return MicVAD.new({
53
- ...base,
54
- startOnLoad: false,
55
- baseAssetPath: vadBase,
56
- onnxWASMBasePath: ortWasmBase,
57
- ortConfig: (ort) => {
58
- base.ortConfig?.(ort);
59
- ort.env.wasm.wasmPaths = ortWasmBase;
60
- ort.env.wasm.numThreads = 1;
61
- ort.env.logLevel = 'error';
62
- },
63
- ...overrides,
64
- });
65
- }
66
-
67
- private ensureTrailingSlash(path: string): string {
68
- return path.endsWith('/') ? path : `${path}/`;
69
- }
70
- }
@@ -1,23 +0,0 @@
1
- import { TestBed } from '@angular/core/testing';
2
- import { AppConfigService } from 'src/app/providers/app-config.service';
3
- import { VoiceStreamingService } from './voice-streaming.service';
4
-
5
- describe('VoiceStreamingService', () => {
6
- let service: VoiceStreamingService;
7
-
8
- beforeEach(() => {
9
- TestBed.configureTestingModule({
10
- providers: [
11
- {
12
- provide: AppConfigService,
13
- useValue: { getConfig: () => ({ voiceProxyWsUrl: '' }) },
14
- },
15
- ],
16
- });
17
- service = TestBed.inject(VoiceStreamingService);
18
- });
19
-
20
- it('should be created', () => {
21
- expect(service).toBeTruthy();
22
- });
23
- });