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

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 (201) hide show
  1. package/.angular-mcp-cache/package.json +1 -0
  2. package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
  3. package/.cursor/mcp.json +15 -0
  4. package/.github/workflows/docker-community-push-latest.yml +23 -13
  5. package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
  6. package/.github/workflows/playwright.yml +27 -0
  7. package/CHANGELOG.md +130 -6
  8. package/Dockerfile +4 -5
  9. package/angular.json +24 -4
  10. package/docs/changelog/this-branch.md +36 -0
  11. package/env.sample +3 -2
  12. package/mocks/voice-websocket-mock/server.cjs +245 -0
  13. package/nginx.conf +22 -2
  14. package/package.json +10 -3
  15. package/playwright.config.ts +41 -0
  16. package/src/app/app.component.html +2 -2
  17. package/src/app/app.component.scss +25 -14
  18. package/src/app/app.component.spec.ts +21 -6
  19. package/src/app/app.component.ts +10 -9
  20. package/src/app/app.module.ts +15 -0
  21. package/src/app/component/conversation-detail/conversation/conversation.component.html +25 -11
  22. package/src/app/component/conversation-detail/conversation/conversation.component.scss +40 -2
  23. package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
  24. package/src/app/component/conversation-detail/conversation/conversation.component.ts +100 -14
  25. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
  26. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
  27. package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
  28. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +23 -10
  29. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +33 -2
  30. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
  31. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +8 -6
  32. package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
  33. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +200 -96
  34. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +211 -6
  35. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
  36. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +291 -76
  37. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
  38. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
  39. package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
  40. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
  41. package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
  42. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
  43. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
  44. package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
  45. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -0
  46. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +83 -0
  47. package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
  48. package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
  49. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
  50. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
  51. package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
  52. package/src/app/component/form/form-builder/form-builder.component.html +1 -1
  53. package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
  54. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
  55. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
  56. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
  57. package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
  58. package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
  59. package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
  60. package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
  61. package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
  62. package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
  63. package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
  64. package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
  65. package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
  66. package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
  67. package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
  68. package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
  69. package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
  70. package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
  71. package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
  72. package/src/app/component/form/prechat-form-test-mock.ts +35 -0
  73. package/src/app/component/home/home.component.html +38 -31
  74. package/src/app/component/home/home.component.scss +4 -2
  75. package/src/app/component/home/home.component.spec.ts +226 -11
  76. package/src/app/component/home-conversations/home-conversations.component.html +30 -26
  77. package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
  78. package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
  79. package/src/app/component/last-message/last-message.component.html +15 -9
  80. package/src/app/component/last-message/last-message.component.scss +16 -2
  81. package/src/app/component/last-message/last-message.component.spec.ts +204 -23
  82. package/src/app/component/last-message/last-message.component.ts +4 -1
  83. package/src/app/component/launcher-button/launcher-button.component.html +8 -13
  84. package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
  85. package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
  86. package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
  87. package/src/app/component/list-conversations/list-conversations.component.html +22 -22
  88. package/src/app/component/menu-options/menu-options.component.html +30 -20
  89. package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
  90. package/src/app/component/message/audio/audio.component.html +13 -15
  91. package/src/app/component/message/audio/audio.component.spec.ts +140 -5
  92. package/src/app/component/message/audio/audio.component.ts +1 -5
  93. package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
  94. package/src/app/component/message/audio-sync/audio-sync.component.scss +65 -0
  95. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +103 -0
  96. package/src/app/component/message/audio-sync/audio-sync.component.ts +643 -0
  97. package/src/app/component/message/avatar/avatar.component.html +2 -2
  98. package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
  99. package/src/app/component/message/bubble-message/bubble-message.component.html +43 -51
  100. package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
  101. package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
  102. package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -109
  103. package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
  104. package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
  105. package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
  106. package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
  107. package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
  108. package/src/app/component/message/carousel/carousel.component.html +29 -16
  109. package/src/app/component/message/carousel/carousel.component.scss +20 -8
  110. package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
  111. package/src/app/component/message/carousel/carousel.component.ts +16 -0
  112. package/src/app/component/message/frame/frame.component.html +9 -4
  113. package/src/app/component/message/frame/frame.component.spec.ts +34 -15
  114. package/src/app/component/message/frame/frame.component.ts +7 -2
  115. package/src/app/component/message/html/html.component.html +1 -1
  116. package/src/app/component/message/html/html.component.scss +1 -1
  117. package/src/app/component/message/html/html.component.spec.ts +24 -7
  118. package/src/app/component/message/image/image.component.html +12 -10
  119. package/src/app/component/message/image/image.component.scss +16 -0
  120. package/src/app/component/message/image/image.component.spec.ts +101 -15
  121. package/src/app/component/message/image/image.component.ts +90 -51
  122. package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
  123. package/src/app/component/message/json-sources/json-sources.component.html +38 -0
  124. package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
  125. package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
  126. package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
  127. package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
  128. package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
  129. package/src/app/component/message/text/text.component.html +3 -3
  130. package/src/app/component/message/text/text.component.scss +80 -86
  131. package/src/app/component/message/text/text.component.spec.ts +106 -13
  132. package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
  133. package/src/app/component/selection-department/selection-department.component.html +21 -23
  134. package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
  135. package/src/app/component/selection-department/selection-department.component.ts +8 -1
  136. package/src/app/component/send-button/send-button.component.html +5 -13
  137. package/src/app/component/send-button/send-button.component.spec.ts +2 -2
  138. package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
  139. package/src/app/directives/tooltip.directive.spec.ts +8 -4
  140. package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
  141. package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
  142. package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
  143. package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
  144. package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
  145. package/src/app/pipe/marked.pipe.spec.ts +38 -2
  146. package/src/app/pipe/marked.pipe.ts +51 -41
  147. package/src/app/providers/app-config.service.ts +4 -2
  148. package/src/app/providers/brand.service.spec.ts +23 -2
  149. package/src/app/providers/brand.service.ts +1 -1
  150. package/src/app/providers/global-settings.service.spec.ts +1009 -14
  151. package/src/app/providers/global-settings.service.ts +82 -2
  152. package/src/app/providers/json-sources-parser.service.ts +175 -0
  153. package/src/app/providers/translator.service.ts +26 -6
  154. package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
  155. package/src/app/providers/tts-audio-playback-coordinator.service.ts +109 -0
  156. package/src/app/providers/url-preview.service.ts +82 -0
  157. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
  158. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
  159. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
  160. package/src/app/providers/voice/audio.types.ts +40 -0
  161. package/src/app/providers/voice/vad.service.spec.ts +28 -0
  162. package/src/app/providers/voice/vad.service.ts +70 -0
  163. package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
  164. package/src/app/providers/voice/voice-streaming.service.ts +702 -0
  165. package/src/app/providers/voice/voice-streaming.types.ts +112 -0
  166. package/src/app/providers/voice/voice.service.spec.ts +227 -0
  167. package/src/app/providers/voice/voice.service.ts +973 -0
  168. package/src/app/sass/_variables.scss +3 -0
  169. package/src/app/sass/animations.scss +19 -1
  170. package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
  171. package/src/app/utils/globals.ts +21 -1
  172. package/src/app/utils/json-sources-utils.ts +27 -0
  173. package/src/app/utils/url-utils.ts +98 -0
  174. package/src/app/utils/utils-resources.ts +1 -1
  175. package/src/assets/i18n/en.json +106 -99
  176. package/src/assets/i18n/es.json +107 -100
  177. package/src/assets/i18n/fr.json +107 -100
  178. package/src/assets/i18n/it.json +107 -98
  179. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
  180. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  181. package/src/assets/sounds/keyboard.mp3 +0 -0
  182. package/src/assets/twp/chatbot-panel.html +3 -1
  183. package/src/assets/twp/index-dev.html +18 -0
  184. package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
  185. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  186. package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
  187. package/src/chat21-core/models/message.ts +2 -1
  188. package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
  189. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
  190. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
  191. package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
  192. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  193. package/src/chat21-core/utils/constants.ts +4 -0
  194. package/src/chat21-core/utils/utils-message.ts +45 -6
  195. package/src/chat21-core/utils/utils.ts +5 -2
  196. package/src/widget-config-template.json +4 -1
  197. package/src/widget-config.json +4 -1
  198. package/tests/widget-form-rich.spec.ts +67 -0
  199. package/tests/widget-index-dev-settings.spec.ts +52 -0
  200. package/tests/widget-twp-iframe.spec.ts +39 -0
  201. package/tsconfig.json +5 -0
@@ -0,0 +1,82 @@
1
+ import { HttpClient, HttpHeaders } from '@angular/common/http';
2
+ import { Injectable } from '@angular/core';
3
+ import { firstValueFrom } from 'rxjs';
4
+ import { AppConfigService } from './app-config.service';
5
+ import { Globals } from '../utils/globals';
6
+
7
+ export type UrlPreviewItem = {
8
+ url: string;
9
+ title?: string;
10
+ description?: string;
11
+ image?: string;
12
+ siteName?: string;
13
+ favicon?: string;
14
+ favicon_hd?: string;
15
+ };
16
+
17
+ @Injectable({
18
+ providedIn: 'root'
19
+ })
20
+ export class UrlPreviewService {
21
+ constructor(
22
+ private http: HttpClient,
23
+ private appConfigService: AppConfigService,
24
+ private g: Globals
25
+ ) {}
26
+
27
+ async previewUrls(urls: string[]): Promise<UrlPreviewItem[]> {
28
+ const apiUrl = this.appConfigService.getConfig()?.apiUrl;
29
+ const projectId = this.g.projectid;
30
+
31
+ const cleaned = (urls || []).map((u) => (u || '').trim()).filter(Boolean).slice(0, 10);
32
+ if (!apiUrl || !projectId || cleaned.length === 0) return [];
33
+
34
+ const base = apiUrl.endsWith('/') ? apiUrl : apiUrl + '/';
35
+ const url = `${base}${projectId}/url-preview`;
36
+
37
+ const token = this.g.tiledeskToken;
38
+ const headers = new HttpHeaders({
39
+ Accept: 'application/json',
40
+ 'Content-Type': 'application/json',
41
+ Authorization: token || ''
42
+ });
43
+
44
+ const body = { urls: cleaned };
45
+
46
+ try {
47
+ const res = await firstValueFrom(
48
+ this.http.post<any>(url, body, { headers })
49
+ );
50
+
51
+ // Expected response:
52
+ // [
53
+ // { url: "...", success: true, data: { url,title,description,image,siteName,... } },
54
+ // ...
55
+ // ]
56
+ // But we stay liberal and accept some alternative wrappers.
57
+ const items: any[] = Array.isArray(res)
58
+ ? res
59
+ : (Array.isArray(res?.items) ? res.items : (Array.isArray(res?.data) ? res.data : []));
60
+
61
+ return (items || [])
62
+ .filter((x) => x && typeof x === 'object')
63
+ .filter((x) => x.success !== false) // keep true/undefined, drop explicit failures
64
+ .map((x) => {
65
+ const d = x.data && typeof x.data === 'object' ? x.data : x;
66
+ return {
67
+ url: (d.url || x.url || x.link || '').toString(),
68
+ title: typeof d.title === 'string' ? d.title : (typeof x.title === 'string' ? x.title : undefined),
69
+ description: typeof d.description === 'string' ? d.description : (typeof x.description === 'string' ? x.description : undefined),
70
+ image: typeof d.image === 'string' ? d.image : (typeof x.image === 'string' ? x.image : undefined),
71
+ siteName: typeof d.siteName === 'string' ? d.siteName : undefined,
72
+ favicon: typeof d.favicon === 'string' ? d.favicon : (typeof x.favicon === 'string' ? x.favicon : undefined),
73
+ favicon_hd: typeof d.favicon_hd === 'string' ? d.favicon_hd : (typeof x.favicon_hd === 'string' ? x.favicon_hd : undefined)
74
+ } as UrlPreviewItem;
75
+ })
76
+ .filter((x) => !!x.url);
77
+ } catch {
78
+ return [];
79
+ }
80
+ }
81
+ }
82
+
@@ -0,0 +1,12 @@
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
+ }
@@ -0,0 +1,171 @@
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
+ }
@@ -0,0 +1,39 @@
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
+ }
@@ -0,0 +1,40 @@
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
+ }
@@ -0,0 +1,28 @@
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
+ });
@@ -0,0 +1,70 @@
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
+ }
@@ -0,0 +1,23 @@
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
+ });