@chat21/chat21-web-widget 5.1.32-rc4 → 5.1.33

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 (59) 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 +7 -50
  4. package/Dockerfile +5 -4
  5. package/angular.json +2 -5
  6. package/deploy_amazon_beta.sh +7 -17
  7. package/deploy_amazon_prod.sh +41 -0
  8. package/docs/changelog/this-branch.md +0 -36
  9. package/nginx.conf +2 -22
  10. package/package.json +1 -4
  11. package/src/app/app.component.ts +9 -10
  12. package/src/app/app.module.ts +0 -9
  13. package/src/app/component/conversation-detail/conversation/conversation.component.html +2 -8
  14. package/src/app/component/conversation-detail/conversation/conversation.component.scss +2 -12
  15. package/src/app/component/conversation-detail/conversation/conversation.component.ts +5 -45
  16. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +1 -1
  17. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +1 -10
  18. package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +0 -1
  19. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +79 -146
  20. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +13 -140
  21. package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +7 -124
  22. package/src/app/component/last-message/last-message.component.ts +1 -4
  23. package/src/app/component/message/audio/audio.component.ts +5 -0
  24. package/src/app/component/message/bubble-message/bubble-message.component.html +1 -6
  25. package/src/app/component/message/bubble-message/bubble-message.component.ts +1 -2
  26. package/src/app/providers/global-settings.service.ts +0 -21
  27. package/src/app/providers/translator.service.ts +0 -2
  28. package/src/app/sass/_variables.scss +0 -3
  29. package/src/app/utils/globals.ts +1 -7
  30. package/src/assets/i18n/en.json +0 -1
  31. package/src/assets/i18n/es.json +0 -1
  32. package/src/assets/i18n/fr.json +0 -1
  33. package/src/assets/i18n/it.json +0 -1
  34. package/src/chat21-core/models/message.ts +1 -2
  35. package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +2 -3
  36. package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +0 -12
  37. package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
  38. package/src/chat21-core/utils/utils-message.ts +0 -7
  39. package/src/chat21-core/utils/utils.ts +2 -5
  40. package/src/launch.js +41 -32
  41. package/src/launch_template.js +41 -32
  42. package/tsconfig.json +0 -5
  43. package/src/app/component/message/audio-sync/audio-sync.component.html +0 -19
  44. package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -65
  45. package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +0 -23
  46. package/src/app/component/message/audio-sync/audio-sync.component.ts +0 -197
  47. package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +0 -12
  48. package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +0 -171
  49. package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +0 -39
  50. package/src/app/providers/voice/audio.types.ts +0 -34
  51. package/src/app/providers/voice/vad.service.spec.ts +0 -28
  52. package/src/app/providers/voice/vad.service.ts +0 -70
  53. package/src/app/providers/voice/voice.service.spec.ts +0 -60
  54. package/src/app/providers/voice/voice.service.ts +0 -294
  55. package/src/app/shims/onnxruntime-web-wasm.ts +0 -4
  56. package/src/assets/onnx/ort-wasm-simd-threaded.mjs +0 -59
  57. package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
  58. package/src/assets/vad/silero_vad_legacy.onnx +0 -0
  59. package/src/assets/vad/vad.worklet.bundle.min.js +0 -1
@@ -219,67 +219,76 @@ function loadIframe(tiledeskScriptBaseLocation) {
219
219
  iDiv.appendChild(ifrm);
220
220
 
221
221
  // Funzione helper per caricare iframe con fallback per compatibilità CSP (Wix, etc.)
222
- // Usa Blob URL come metodo principale (più compatibile con CSP) con fallback a srcdoc e document.write
223
- function loadIframeContent(iframe, htmlContent, baseLocation) {
224
- var isLocalhost = baseLocation.includes('localhost');
222
+ // Priorità: document.write / srcdoc prima della Blob URL. Le Blob URL spesso danno origine opaca
223
+ // (blob:null): l'iframe non può leggere window.parent.tiledeskSettings → projectid mancante.
224
+ function loadIframeContent(iframe, htmlContent) {
225
225
  var blobUrl = null;
226
-
227
- // Metodo 1: Blob URL (più compatibile con CSP di Wix e altre piattaforme)
228
- // Usa Blob URL come metodo principale perché è meno spesso bloccato da CSP rispetto a srcdoc
226
+
227
+ // 1) document.write: iframe stessa origine della pagina host tiledeskSettings sul parent accessibile
228
+ try {
229
+ var cw = iframe.contentWindow;
230
+ if (cw && cw.document) {
231
+ cw.document.open();
232
+ cw.document.write(htmlContent);
233
+ cw.document.close();
234
+ return;
235
+ }
236
+ } catch (e) {
237
+ console.warn('[Tiledesk] iframe document.write failed, trying srcdoc/blob:', e);
238
+ }
239
+
240
+ // 2) srcdoc: stessa origine del parent (HTML5); utile se document.write è bloccato
241
+ if ('srcdoc' in iframe) {
242
+ try {
243
+ iframe.srcdoc = htmlContent;
244
+ return;
245
+ } catch (e) {
246
+ console.warn('[Tiledesk] iframe srcdoc failed, trying blob:', e);
247
+ }
248
+ }
249
+
250
+ // 3) Blob URL (spesso permesso da CSP dove srcdoc/write no; può rompere lettura parent.tiledeskSettings)
229
251
  if (typeof Blob !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL) {
230
252
  try {
231
253
  var blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' });
232
254
  blobUrl = URL.createObjectURL(blob);
233
255
  iframe.src = blobUrl;
234
-
235
- // Cleanup del blob URL dopo il caricamento per liberare memoria
256
+
236
257
  var originalOnload = iframe.onload;
237
258
  iframe.onload = function() {
238
- // Revoca il blob URL dopo un delay per assicurarsi che tutto sia caricato
239
259
  setTimeout(function() {
240
260
  if (blobUrl) {
241
261
  try {
242
262
  URL.revokeObjectURL(blobUrl);
243
263
  blobUrl = null;
244
- } catch(e) {
245
- console.warn('Error revoking blob URL:', e);
264
+ } catch (err) {
265
+ console.warn('Error revoking blob URL:', err);
246
266
  }
247
267
  }
248
268
  }, 1000);
249
269
  if (originalOnload) originalOnload.call(this);
250
270
  };
251
- return; // Blob URL impostato con successo
252
- } catch(e) {
253
- console.warn('Blob URL not available, trying srcdoc:', e);
254
- }
255
- }
256
-
257
- // Metodo 2: srcdoc (fallback se Blob URL non disponibile)
258
- // Skip per localhost (usa document.write per compatibilità sviluppo)
259
- if (!isLocalhost && 'srcdoc' in iframe) {
260
- try {
261
- iframe.srcdoc = htmlContent;
262
- return; // srcdoc impostato
263
- } catch(e) {
264
- console.warn('srcdoc not allowed, trying document.write:', e);
271
+ return;
272
+ } catch (e) {
273
+ console.warn('Blob URL not available:', e);
265
274
  }
266
275
  }
267
-
268
- // Metodo 3: document.write (fallback finale, funziona su localhost e browser vecchi)
269
- if (isLocalhost || (iframe.contentWindow && iframe.contentWindow.document)) {
276
+
277
+ // 4) Ultimo tentativo document.write (iframe magari non pronto al primo passo)
278
+ if (iframe.contentWindow && iframe.contentWindow.document) {
270
279
  try {
271
280
  iframe.contentWindow.document.open();
272
281
  iframe.contentWindow.document.write(htmlContent);
273
282
  iframe.contentWindow.document.close();
274
- return; // document.write completato
275
- } catch(e) {
276
- console.error('All iframe loading methods failed:', e);
283
+ return;
284
+ } catch (e) {
285
+ console.error('[Tiledesk] All iframe loading methods failed:', e);
277
286
  }
278
287
  }
279
288
  }
280
289
 
281
290
  // Carica il contenuto dell'iframe con fallback automatico
282
- loadIframeContent(ifrm, srcTileDesk, tiledeskScriptBaseLocation);
291
+ loadIframeContent(ifrm, srcTileDesk);
283
292
 
284
293
 
285
294
  }
package/tsconfig.json CHANGED
@@ -23,11 +23,6 @@
23
23
  "dom"
24
24
  ],
25
25
  "resolveJsonModule": true,
26
- "paths": {
27
- "onnxruntime-web/wasm": [
28
- "src/app/shims/onnxruntime-web-wasm.ts"
29
- ]
30
- }
31
26
  },
32
27
  "skipLibCheck": true,
33
28
  "angularCompilerOptions": {
@@ -1,19 +0,0 @@
1
- <div class="lyrics-container">
2
-
3
- <audio
4
- #audioPlayer
5
- [src]="message?.metadata?.src"
6
- (timeupdate)="onTimeUpdate()"
7
- style="display:none">
8
- </audio>
9
-
10
- <p class="lyrics message_innerhtml marked" #transcriptBox [style.color]="color">
11
- <span
12
- *ngFor="let w of words; let i = index; trackBy: trackByIndex"
13
- class="word"
14
- [ngClass]="w.state">
15
- {{ w.text }}
16
- </span>
17
- </p>
18
-
19
- </div>
@@ -1,65 +0,0 @@
1
- :host {
2
- display: block;
3
- font-size: var(--font-size-bubble-message, 14px);
4
- }
5
-
6
- /* Allineato a text.component.scss (.message_innerhtml, p) */
7
- .message_innerhtml {
8
- margin: 0;
9
-
10
- &.marked {
11
- padding: 12px 16px;
12
- margin-block-start: 0em !important;
13
- margin-block-end: 0em !important;
14
- }
15
- }
16
-
17
- .lyrics {
18
- font-size: inherit;
19
- margin: 0;
20
- line-height: 1.4em;
21
- font-style: normal;
22
- letter-spacing: normal;
23
- font-stretch: normal;
24
- font-variant: normal;
25
- font-weight: 300;
26
- overflow: hidden;
27
-
28
- display: flex;
29
- flex-wrap: wrap;
30
- gap: 6px;
31
- /* Colore bubble: da [style.color] / @Input() color — ereditato dalle .word */
32
- }
33
-
34
- /* base word */
35
- .word {
36
- transition:
37
- transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),
38
- color 0.3s ease,
39
- opacity 0.3s ease,
40
- filter 0.3s ease;
41
-
42
- will-change: transform;
43
- }
44
-
45
- /* FUTURE */
46
- .word.future {
47
- opacity: 0;
48
- transform: scale(0.98);
49
- }
50
-
51
- /* PAST: stesso colore del testo bubble (@Input color sul <p>) */
52
- .word.past {
53
- opacity: 1;
54
- color: inherit;
55
- transform: scale(1);
56
- }
57
-
58
- /* ACTIVE (solo momentaneo, tipo “karaoke flash”) */
59
- .word.active {
60
- opacity: 1;
61
- color: #00c3ff;
62
- font-weight: 700;
63
- transform: scale(1.18);
64
- text-shadow: 0 0 10px rgba(0, 195, 255, 0.35);
65
- }
@@ -1,23 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
-
3
- import { AudioSyncComponent } from './audio-sync.component';
4
-
5
- describe('AudioSyncComponent', () => {
6
- let component: AudioSyncComponent;
7
- let fixture: ComponentFixture<AudioSyncComponent>;
8
-
9
- beforeEach(async () => {
10
- await TestBed.configureTestingModule({
11
- imports: [AudioSyncComponent]
12
- })
13
- .compileComponents();
14
-
15
- fixture = TestBed.createComponent(AudioSyncComponent);
16
- component = fixture.componentInstance;
17
- fixture.detectChanges();
18
- });
19
-
20
- it('should create', () => {
21
- expect(component).toBeTruthy();
22
- });
23
- });
@@ -1,197 +0,0 @@
1
- import {
2
- AfterViewInit,
3
- ChangeDetectorRef,
4
- Component,
5
- ElementRef,
6
- Input,
7
- OnChanges,
8
- OnDestroy,
9
- SimpleChanges,
10
- ViewChild,
11
- } from '@angular/core';
12
- import { MessageModel } from 'src/chat21-core/models/message';
13
-
14
- /** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
15
- const HAVE_METADATA = 1;
16
-
17
- @Component({
18
- selector: 'chat-audio-sync',
19
- templateUrl: './audio-sync.component.html',
20
- styleUrl: './audio-sync.component.scss',
21
- })
22
- export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
23
- @Input() message: MessageModel | null = null;
24
- @Input() color?: string;
25
-
26
- @ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
27
- @ViewChild('transcriptBox') transcriptBox!: ElementRef<HTMLElement>;
28
-
29
- words: {
30
- text: string;
31
- start: number;
32
- end: number;
33
- state: 'future' | 'active' | 'past';
34
- }[] = [];
35
-
36
- currentTime = 0;
37
- duration = 1;
38
- activeIndex = -1;
39
-
40
- private timingReady = false;
41
- private onMetadataLoaded: () => void;
42
- private onPlaybackEnded: () => void;
43
-
44
- constructor(private readonly cdr: ChangeDetectorRef) {}
45
-
46
- /** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
47
- private get skipSyncAnimation(): boolean {
48
- return this.message?.isJustRecived === false;
49
- }
50
-
51
- ngOnChanges(changes: SimpleChanges): void {
52
- if (!changes['message']) {
53
- return;
54
- }
55
- if (this.audioRef?.nativeElement && this.timingReady) {
56
- this.duration = this.audioRef.nativeElement.duration || 1;
57
- this.buildFakeTiming();
58
- if (this.skipSyncAnimation) {
59
- this.markAllWordsPast();
60
- } else {
61
- this.syncStatesFromCurrentTime();
62
- }
63
- }
64
- }
65
-
66
- ngAfterViewInit(): void {
67
- const audio = this.audioRef.nativeElement;
68
-
69
- this.onPlaybackEnded = () => {
70
- if (this.skipSyncAnimation) {
71
- return;
72
- }
73
- this.markAllWordsPast();
74
- if (this.message) {
75
- this.message.isJustRecived = false;
76
- }
77
- this.cdr.detectChanges();
78
- };
79
-
80
- this.onMetadataLoaded = () => {
81
- if (this.timingReady) {
82
- return;
83
- }
84
- this.timingReady = true;
85
- this.duration = audio.duration || 1;
86
- this.buildFakeTiming();
87
- if (this.skipSyncAnimation) {
88
- this.markAllWordsPast();
89
- this.cdr.detectChanges();
90
- return;
91
- }
92
- this.syncStatesFromCurrentTime();
93
- this.cdr.detectChanges();
94
-
95
- setTimeout(() => {
96
- audio.play().catch(() => {
97
- this.syncStatesFromCurrentTime();
98
- this.cdr.detectChanges();
99
- });
100
- }, 200);
101
- };
102
-
103
- audio.addEventListener('loadedmetadata', this.onMetadataLoaded);
104
- audio.addEventListener('ended', this.onPlaybackEnded);
105
-
106
- if (audio.readyState >= HAVE_METADATA) {
107
- this.onMetadataLoaded();
108
- }
109
- }
110
-
111
- ngOnDestroy(): void {
112
- const audio = this.audioRef?.nativeElement;
113
- if (!audio) {
114
- return;
115
- }
116
- if (this.onMetadataLoaded) {
117
- audio.removeEventListener('loadedmetadata', this.onMetadataLoaded);
118
- }
119
- if (this.onPlaybackEnded) {
120
- audio.removeEventListener('ended', this.onPlaybackEnded);
121
- }
122
- }
123
-
124
- private markAllWordsPast(): void {
125
- this.words.forEach((w) => {
126
- w.state = 'past';
127
- });
128
- this.activeIndex = -1;
129
- }
130
-
131
- buildFakeTiming(): void {
132
- const rawWords = (this.message?.text || '')
133
- .trim()
134
- .split(/\s+/)
135
- .filter((w) => w.length > 0);
136
- if (rawWords.length === 0) {
137
- this.words = [];
138
- return;
139
- }
140
- const step = this.duration / rawWords.length;
141
-
142
- this.words = rawWords.map((w, i) => ({
143
- text: w,
144
- start: i * step,
145
- end: (i + 1) * step,
146
- state: 'future' as const,
147
- }));
148
- }
149
-
150
- syncStatesFromCurrentTime(): void {
151
- if (this.skipSyncAnimation) {
152
- return;
153
- }
154
- const audio = this.audioRef?.nativeElement;
155
- if (!audio || this.words.length === 0) {
156
- return;
157
- }
158
- this.currentTime = audio.currentTime;
159
- let newActiveIndex = -1;
160
-
161
- this.words.forEach((w, i) => {
162
- if (this.currentTime >= w.end) {
163
- w.state = 'past';
164
- } else if (this.currentTime >= w.start && this.currentTime < w.end) {
165
- w.state = 'active';
166
- newActiveIndex = i;
167
- } else {
168
- w.state = 'future';
169
- }
170
- });
171
-
172
- if (newActiveIndex !== this.activeIndex) {
173
- this.activeIndex = newActiveIndex;
174
- this.scrollToActive();
175
- }
176
- }
177
-
178
- onTimeUpdate(): void {
179
- this.syncStatesFromCurrentTime();
180
- }
181
-
182
- scrollToActive(): void {
183
- const container = this.transcriptBox?.nativeElement;
184
- const active = container?.querySelector('.active') as HTMLElement;
185
-
186
- if (active) {
187
- active.scrollIntoView({
188
- behavior: 'smooth',
189
- block: 'center',
190
- });
191
- }
192
- }
193
-
194
- trackByIndex(index: number): number {
195
- return index;
196
- }
197
- }
@@ -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,34 +0,0 @@
1
- /**
2
- * Tipi condivisi per cattura microfono, VAD e registrazione (WebM).
3
- */
4
-
5
- export const DEFAULT_VOICE_AUDIO_CONSTRAINTS: MediaTrackConstraints = {
6
- echoCancellation: true,
7
- noiseSuppression: true,
8
- autoGainControl: true,
9
- };
10
-
11
- export const DEFAULT_VOICE_MEDIA_STREAM_CONSTRAINTS: MediaStreamConstraints = {
12
- audio: DEFAULT_VOICE_AUDIO_CONSTRAINTS,
13
- };
14
-
15
- export interface VoiceRecordedBlob {
16
- blob: Blob;
17
- mimeType: string;
18
- }
19
-
20
- /**
21
- * Segmento audio dopo VAD; può includere `transcript` se STT è configurato e abilitato.
22
- */
23
- export interface VoiceSegmentPayload extends VoiceRecordedBlob {
24
- transcript?: string;
25
- transcriptionError?: string;
26
- }
27
-
28
- export interface VoiceSessionStartOptions {
29
- /** Opzionale se usi solo {@link VoiceService.audioSegment$}. */
30
- onRecordingComplete?: (result: VoiceSegmentPayload) => void;
31
- constraints?: MediaStreamConstraints;
32
- /** Default `true`. Se `false`, non viene chiamato lo STT sul segmento. */
33
- enableTranscription?: boolean;
34
- }