@chat21/chat21-web-widget 5.1.30 → 5.1.32-rc13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/docker-community-push-latest.yml +23 -13
- package/.github/workflows/docker-image-tag-community-tag-push.yml +22 -12
- package/CHANGELOG.md +89 -2
- package/Dockerfile +4 -5
- package/angular.json +5 -2
- package/deploy_amazon_beta.sh +17 -7
- package/docs/changelog/this-branch.md +36 -0
- package/nginx.conf +22 -2
- package/package.json +4 -1
- package/src/app/app.component.ts +10 -9
- package/src/app/app.module.ts +11 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +9 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +12 -2
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +46 -5
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +9 -5
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +19 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +2 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +128 -80
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +117 -13
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +120 -8
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +43 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +79 -0
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +192 -0
- package/src/app/component/last-message/last-message.component.ts +4 -1
- package/src/app/component/message/audio/audio.component.ts +0 -5
- package/src/app/component/message/audio-sync/audio-sync.component.html +18 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +64 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +23 -0
- package/src/app/component/message/audio-sync/audio-sync.component.ts +558 -0
- package/src/app/component/message/bubble-message/bubble-message.component.html +6 -1
- package/src/app/component/message/bubble-message/bubble-message.component.ts +2 -1
- package/src/app/providers/global-settings.service.ts +21 -0
- package/src/app/providers/translator.service.ts +2 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +93 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.config.ts +12 -0
- package/src/app/providers/voice/STT&TTS/openai-voice.provider.ts +171 -0
- package/src/app/providers/voice/STT&TTS/speech-provider.abstract.ts +39 -0
- package/src/app/providers/voice/audio.types.ts +34 -0
- package/src/app/providers/voice/vad.service.spec.ts +28 -0
- package/src/app/providers/voice/vad.service.ts +70 -0
- package/src/app/providers/voice/voice.service.spec.ts +60 -0
- package/src/app/providers/voice/voice.service.ts +376 -0
- package/src/app/sass/_variables.scss +3 -0
- package/src/app/shims/onnxruntime-web-wasm.ts +4 -0
- package/src/app/utils/conversation-sender-classifier.ts +21 -0
- package/src/app/utils/globals.ts +7 -1
- package/src/assets/i18n/en.json +1 -0
- package/src/assets/i18n/es.json +1 -0
- package/src/assets/i18n/fr.json +1 -0
- package/src/assets/i18n/it.json +1 -0
- package/src/assets/onnx/ort-wasm-simd-threaded.mjs +59 -0
- package/src/assets/onnx/ort-wasm-simd-threaded.wasm +0 -0
- package/src/assets/vad/silero_vad_legacy.onnx +0 -0
- package/src/assets/vad/vad.worklet.bundle.min.js +1 -0
- package/src/chat21-core/models/message.ts +2 -1
- package/src/chat21-core/providers/firebase/firebase-conversation-handler.ts +3 -2
- package/src/chat21-core/providers/mqtt/mqtt-conversation-handler.ts +12 -0
- package/src/chat21-core/providers/tiledesk/tiledesk-requests.service.ts +1 -1
- package/src/chat21-core/utils/utils-message.ts +7 -0
- package/src/chat21-core/utils/utils.ts +5 -2
- package/src/launch.js +41 -32
- package/src/launch_template.js +41 -32
- package/tsconfig.json +5 -0
- package/deploy_amazon_prod.sh +0 -41
|
@@ -0,0 +1,558 @@
|
|
|
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 { Subscription } from 'rxjs';
|
|
13
|
+
import { MessageModel } from 'src/chat21-core/models/message';
|
|
14
|
+
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
15
|
+
import { Globals } from 'src/app/utils/globals';
|
|
16
|
+
|
|
17
|
+
/** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
|
|
18
|
+
const HAVE_METADATA = 1;
|
|
19
|
+
|
|
20
|
+
@Component({
|
|
21
|
+
selector: 'chat-audio-sync',
|
|
22
|
+
templateUrl: './audio-sync.component.html',
|
|
23
|
+
styleUrl: './audio-sync.component.scss',
|
|
24
|
+
})
|
|
25
|
+
export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
26
|
+
@Input() message: MessageModel | null = null;
|
|
27
|
+
@Input() color?: string;
|
|
28
|
+
|
|
29
|
+
@ViewChild('audioPlayer') audioRef!: ElementRef<HTMLAudioElement>;
|
|
30
|
+
@ViewChild('transcriptBox') transcriptBox!: ElementRef<HTMLElement>;
|
|
31
|
+
|
|
32
|
+
words: {
|
|
33
|
+
text: string;
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
state: 'future' | 'active' | 'past';
|
|
37
|
+
}[] = [];
|
|
38
|
+
|
|
39
|
+
currentTime = 0;
|
|
40
|
+
duration = 1;
|
|
41
|
+
activeIndex = -1;
|
|
42
|
+
|
|
43
|
+
private timingReady = false;
|
|
44
|
+
private onMetadataLoaded: () => void;
|
|
45
|
+
private onPlaybackEnded: () => void;
|
|
46
|
+
|
|
47
|
+
/** Id univoco per il coordinatore (di solito `message.uid`). */
|
|
48
|
+
private playbackOwnerId = '';
|
|
49
|
+
private destroyed = false;
|
|
50
|
+
private playbackRequested = false;
|
|
51
|
+
private playbackStarted = false;
|
|
52
|
+
private streamAbort?: AbortController;
|
|
53
|
+
private mediaSourceObjectUrl?: string;
|
|
54
|
+
private stopAllSub?: Subscription;
|
|
55
|
+
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly cdr: ChangeDetectorRef,
|
|
58
|
+
private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
|
|
59
|
+
private readonly globals: Globals,
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
/** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
|
|
63
|
+
private get skipSyncAnimation(): boolean {
|
|
64
|
+
return this.message?.isJustRecived === false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
ngOnChanges(changes: SimpleChanges): void {
|
|
68
|
+
if (!changes['message']) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (this.audioRef?.nativeElement && this.timingReady) {
|
|
72
|
+
const d = this.audioRef.nativeElement.duration;
|
|
73
|
+
if (Number.isFinite(d) && d > 0) {
|
|
74
|
+
this.duration = d;
|
|
75
|
+
}
|
|
76
|
+
this.buildFakeTiming();
|
|
77
|
+
if (this.skipSyncAnimation) {
|
|
78
|
+
this.markAllWordsPast();
|
|
79
|
+
} else if (this.playbackStarted) {
|
|
80
|
+
this.syncStatesFromCurrentTime();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
ngAfterViewInit(): void {
|
|
86
|
+
const audio = this.audioRef.nativeElement;
|
|
87
|
+
|
|
88
|
+
this.playbackOwnerId =
|
|
89
|
+
(this.message?.uid && String(this.message.uid).trim()) ||
|
|
90
|
+
`tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
91
|
+
|
|
92
|
+
this.onPlaybackEnded = () => {
|
|
93
|
+
this.playbackStarted = false;
|
|
94
|
+
this.cleanupStreaming();
|
|
95
|
+
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
96
|
+
if (this.skipSyncAnimation) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.markAllWordsPast();
|
|
100
|
+
if (this.message) {
|
|
101
|
+
this.message.isJustRecived = false;
|
|
102
|
+
}
|
|
103
|
+
this.cdr.detectChanges();
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
this.onMetadataLoaded = () => {
|
|
107
|
+
// La durata potrebbe arrivare tardi (specie con streaming).
|
|
108
|
+
const d = audio.duration;
|
|
109
|
+
if (Number.isFinite(d) && d > 0) {
|
|
110
|
+
this.duration = d;
|
|
111
|
+
} else if (!this.timingReady) {
|
|
112
|
+
this.duration = this.estimateDurationSecondsFromText();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
this.timingReady = true;
|
|
116
|
+
this.buildFakeTiming();
|
|
117
|
+
if (this.skipSyncAnimation) {
|
|
118
|
+
this.markAllWordsPast();
|
|
119
|
+
this.cdr.detectChanges();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (this.playbackStarted) {
|
|
123
|
+
this.syncStatesFromCurrentTime();
|
|
124
|
+
}
|
|
125
|
+
this.cdr.detectChanges();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
audio.addEventListener('loadedmetadata', this.onMetadataLoaded);
|
|
129
|
+
audio.addEventListener('ended', this.onPlaybackEnded);
|
|
130
|
+
|
|
131
|
+
// Prepara subito le parole (durata stimata) e poi aggiorna quando arriva la metadata reale.
|
|
132
|
+
this.duration = this.estimateDurationSecondsFromText();
|
|
133
|
+
this.timingReady = true;
|
|
134
|
+
this.buildFakeTiming();
|
|
135
|
+
if (this.skipSyncAnimation) {
|
|
136
|
+
this.markAllWordsPast();
|
|
137
|
+
this.cdr.detectChanges();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
this.cdr.detectChanges();
|
|
141
|
+
|
|
142
|
+
setTimeout(() => {
|
|
143
|
+
if (this.playbackRequested || this.destroyed) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
this.playbackRequested = true;
|
|
147
|
+
this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
|
|
148
|
+
if (this.destroyed) {
|
|
149
|
+
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
this.playbackStarted = true;
|
|
153
|
+
this.syncStatesFromCurrentTime();
|
|
154
|
+
this.cdr.detectChanges();
|
|
155
|
+
this.startPlayback(audio);
|
|
156
|
+
});
|
|
157
|
+
}, 200);
|
|
158
|
+
|
|
159
|
+
// Stop signal: user pressed X while this TTS was playing or queued.
|
|
160
|
+
this.stopAllSub = this.ttsPlayback.stopAllPlayback$.subscribe(() => {
|
|
161
|
+
if (!this.playbackRequested && !this.playbackStarted) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.destroyed = true;
|
|
165
|
+
this.playbackStarted = false;
|
|
166
|
+
this.cleanupStreaming();
|
|
167
|
+
try {
|
|
168
|
+
audio.pause();
|
|
169
|
+
audio.currentTime = 0;
|
|
170
|
+
} catch {
|
|
171
|
+
/* ignore */
|
|
172
|
+
}
|
|
173
|
+
this.markAllWordsPast();
|
|
174
|
+
if (this.message) {
|
|
175
|
+
this.message.isJustRecived = false;
|
|
176
|
+
}
|
|
177
|
+
this.cdr.detectChanges();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ngOnDestroy(): void {
|
|
182
|
+
this.destroyed = true;
|
|
183
|
+
this.playbackStarted = false;
|
|
184
|
+
this.cleanupStreaming();
|
|
185
|
+
this.stopAllSub?.unsubscribe();
|
|
186
|
+
this.stopAllSub = undefined;
|
|
187
|
+
|
|
188
|
+
const audio = this.audioRef?.nativeElement;
|
|
189
|
+
if (audio) {
|
|
190
|
+
try {
|
|
191
|
+
audio.pause();
|
|
192
|
+
audio.currentTime = 0;
|
|
193
|
+
} catch {
|
|
194
|
+
/* ignore */
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
this.ttsPlayback.release(this.playbackOwnerId);
|
|
198
|
+
|
|
199
|
+
if (!audio) {
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (this.onMetadataLoaded) {
|
|
203
|
+
audio.removeEventListener('loadedmetadata', this.onMetadataLoaded);
|
|
204
|
+
}
|
|
205
|
+
if (this.onPlaybackEnded) {
|
|
206
|
+
audio.removeEventListener('ended', this.onPlaybackEnded);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private startPlayback(audio: HTMLAudioElement): void {
|
|
211
|
+
const src = (this.message as any)?.metadata?.src as string | undefined;
|
|
212
|
+
if (!src) {
|
|
213
|
+
this.playbackStarted = false;
|
|
214
|
+
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
215
|
+
this.markAllWordsPast();
|
|
216
|
+
if (this.message) {
|
|
217
|
+
this.message.isJustRecived = false;
|
|
218
|
+
}
|
|
219
|
+
this.cdr.detectChanges();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (this.message?.type === 'tts') {
|
|
224
|
+
this.startStreamingFromEndpoint(audio, src);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
audio.src = src;
|
|
229
|
+
try {
|
|
230
|
+
audio.currentTime = 0;
|
|
231
|
+
} catch {
|
|
232
|
+
/* ignore */
|
|
233
|
+
}
|
|
234
|
+
audio.play().catch(() => this.handlePlaybackError());
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private startStreamingFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
|
|
238
|
+
this.cleanupStreaming();
|
|
239
|
+
|
|
240
|
+
const jwt = this.getJwtToken();
|
|
241
|
+
const voiceSettings = this.getVoiceSettingsBody();
|
|
242
|
+
const requestBody = this.buildTtsRequestBody(voiceSettings);
|
|
243
|
+
// <audio src="..."> non può inviare header/body: serve fetch().
|
|
244
|
+
const hasMse = typeof (window as any).MediaSource !== 'undefined';
|
|
245
|
+
if (!hasMse) {
|
|
246
|
+
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const MediaSourceCtor = (window as any).MediaSource as typeof MediaSource;
|
|
251
|
+
const mediaSource = new MediaSourceCtor();
|
|
252
|
+
const objectUrl = URL.createObjectURL(mediaSource);
|
|
253
|
+
this.mediaSourceObjectUrl = objectUrl;
|
|
254
|
+
audio.src = objectUrl;
|
|
255
|
+
|
|
256
|
+
const abort = new AbortController();
|
|
257
|
+
this.streamAbort = abort;
|
|
258
|
+
|
|
259
|
+
const onSourceOpen = async () => {
|
|
260
|
+
mediaSource.removeEventListener('sourceopen', onSourceOpen);
|
|
261
|
+
try {
|
|
262
|
+
const headers: Record<string, string> = {
|
|
263
|
+
'Content-Type': 'application/json',
|
|
264
|
+
'Authorization': `${jwt}`
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const response = await fetch(endpoint, {
|
|
268
|
+
method: 'POST',
|
|
269
|
+
headers,
|
|
270
|
+
body: JSON.stringify(requestBody),
|
|
271
|
+
signal: abort.signal,
|
|
272
|
+
});
|
|
273
|
+
if (!response.ok || !response.body) {
|
|
274
|
+
throw new Error(`TTS stream request failed (${response.status})`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
|
|
278
|
+
const mime = (headerType && MediaSourceCtor.isTypeSupported(headerType))
|
|
279
|
+
? headerType
|
|
280
|
+
: 'audio/mpeg';
|
|
281
|
+
|
|
282
|
+
if (!MediaSourceCtor.isTypeSupported(mime)) {
|
|
283
|
+
this.cleanupStreaming();
|
|
284
|
+
// Fallback: fetch completo e play via blob (no streaming).
|
|
285
|
+
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sourceBuffer = mediaSource.addSourceBuffer(mime);
|
|
290
|
+
sourceBuffer.mode = 'sequence';
|
|
291
|
+
|
|
292
|
+
const reader = response.body.getReader();
|
|
293
|
+
const queue: Uint8Array[] = [];
|
|
294
|
+
let doneReading = false;
|
|
295
|
+
let started = false;
|
|
296
|
+
|
|
297
|
+
const tryEndOfStream = () => {
|
|
298
|
+
if (doneReading && queue.length === 0 && !sourceBuffer.updating) {
|
|
299
|
+
try {
|
|
300
|
+
mediaSource.endOfStream();
|
|
301
|
+
} catch {
|
|
302
|
+
/* ignore */
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const pump = () => {
|
|
308
|
+
if (abort.signal.aborted) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (sourceBuffer.updating) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const chunk = queue.shift();
|
|
315
|
+
if (!chunk) {
|
|
316
|
+
tryEndOfStream();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
const ab = chunk.buffer.slice(
|
|
321
|
+
chunk.byteOffset,
|
|
322
|
+
chunk.byteOffset + chunk.byteLength,
|
|
323
|
+
) as ArrayBuffer;
|
|
324
|
+
sourceBuffer.appendBuffer(ab);
|
|
325
|
+
} catch {
|
|
326
|
+
this.cleanupStreaming();
|
|
327
|
+
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
sourceBuffer.addEventListener('updateend', () => {
|
|
332
|
+
if (!started && this.playbackStarted && !this.destroyed) {
|
|
333
|
+
started = true;
|
|
334
|
+
audio.play().catch(() => this.handlePlaybackError());
|
|
335
|
+
}
|
|
336
|
+
pump();
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// Primo pump (se arrivano subito chunk)
|
|
340
|
+
pump();
|
|
341
|
+
|
|
342
|
+
while (!abort.signal.aborted) {
|
|
343
|
+
const { value, done } = await reader.read();
|
|
344
|
+
if (done) {
|
|
345
|
+
doneReading = true;
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
if (value && value.byteLength > 0) {
|
|
349
|
+
queue.push(value);
|
|
350
|
+
pump();
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
doneReading = true;
|
|
355
|
+
tryEndOfStream();
|
|
356
|
+
} catch {
|
|
357
|
+
if (!abort.signal.aborted) {
|
|
358
|
+
this.handlePlaybackError();
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
mediaSource.addEventListener('sourceopen', onSourceOpen);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private handlePlaybackError(): void {
|
|
367
|
+
this.playbackStarted = false;
|
|
368
|
+
this.cleanupStreaming();
|
|
369
|
+
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
370
|
+
this.markAllWordsPast();
|
|
371
|
+
if (this.message) {
|
|
372
|
+
this.message.isJustRecived = false;
|
|
373
|
+
}
|
|
374
|
+
this.cdr.detectChanges();
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private cleanupStreaming(): void {
|
|
378
|
+
try {
|
|
379
|
+
this.streamAbort?.abort();
|
|
380
|
+
} catch {
|
|
381
|
+
/* ignore */
|
|
382
|
+
}
|
|
383
|
+
this.streamAbort = undefined;
|
|
384
|
+
|
|
385
|
+
if (this.mediaSourceObjectUrl) {
|
|
386
|
+
try {
|
|
387
|
+
URL.revokeObjectURL(this.mediaSourceObjectUrl);
|
|
388
|
+
} catch {
|
|
389
|
+
/* ignore */
|
|
390
|
+
}
|
|
391
|
+
this.mediaSourceObjectUrl = undefined;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
private getJwtToken(): string | null {
|
|
396
|
+
const token = (this.globals?.tiledeskToken || this.globals?.jwt || '').trim();
|
|
397
|
+
return token.length > 0 ? token : null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private getVoiceSettingsBody(): unknown {
|
|
401
|
+
const raw = (this.message as any)?.metadata?.voiceSettings;
|
|
402
|
+
if (raw === null || raw === undefined) {
|
|
403
|
+
return {};
|
|
404
|
+
}
|
|
405
|
+
if (typeof raw === 'string') {
|
|
406
|
+
const s = raw.trim();
|
|
407
|
+
if (!s) return {};
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(s);
|
|
410
|
+
} catch {
|
|
411
|
+
// se non è JSON valido, invialo come stringa (il backend può gestirlo)
|
|
412
|
+
return { voiceSettings: raw };
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return raw;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private async fetchAsBlobAndPlay(
|
|
419
|
+
audio: HTMLAudioElement,
|
|
420
|
+
endpoint: string,
|
|
421
|
+
jwt: string | null,
|
|
422
|
+
requestBody: unknown,
|
|
423
|
+
): Promise<void> {
|
|
424
|
+
try {
|
|
425
|
+
const headers: Record<string, string> = {
|
|
426
|
+
'Content-Type': 'application/json',
|
|
427
|
+
'Authorization': `${jwt}`
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
console.log('headers', headers);
|
|
431
|
+
console.log('requestBody', requestBody);
|
|
432
|
+
|
|
433
|
+
const response = await fetch(endpoint, {
|
|
434
|
+
method: 'POST',
|
|
435
|
+
headers,
|
|
436
|
+
body: JSON.stringify(requestBody ?? {}),
|
|
437
|
+
signal: this.streamAbort?.signal,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
if (!response.ok) {
|
|
441
|
+
throw new Error(`TTS request failed (${response.status})`);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const blob = await response.blob();
|
|
445
|
+
if (this.destroyed) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const objectUrl = URL.createObjectURL(blob);
|
|
450
|
+
this.mediaSourceObjectUrl = objectUrl;
|
|
451
|
+
audio.src = objectUrl;
|
|
452
|
+
audio.play().catch(() => this.handlePlaybackError());
|
|
453
|
+
} catch {
|
|
454
|
+
this.handlePlaybackError();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
private buildTtsRequestBody(voiceSettings: unknown): unknown {
|
|
459
|
+
const text = this.message?.text ?? '';
|
|
460
|
+
if (
|
|
461
|
+
voiceSettings &&
|
|
462
|
+
typeof voiceSettings === 'object' &&
|
|
463
|
+
!Array.isArray(voiceSettings)
|
|
464
|
+
) {
|
|
465
|
+
return { ...(voiceSettings as Record<string, unknown>), text, streaming: true };
|
|
466
|
+
}
|
|
467
|
+
return { voiceSettings, text, streaming: true };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private markAllWordsPast(): void {
|
|
471
|
+
this.words.forEach((w) => {
|
|
472
|
+
w.state = 'past';
|
|
473
|
+
});
|
|
474
|
+
this.activeIndex = -1;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private estimateDurationSecondsFromText(): number {
|
|
478
|
+
const rawWords = (this.message?.text || '')
|
|
479
|
+
.trim()
|
|
480
|
+
.split(/\s+/)
|
|
481
|
+
.filter((w) => w.length > 0);
|
|
482
|
+
if (rawWords.length === 0) {
|
|
483
|
+
return 1;
|
|
484
|
+
}
|
|
485
|
+
// ~140 WPM → ~0.43s/word
|
|
486
|
+
return Math.max(1, rawWords.length * 0.43);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
buildFakeTiming(): void {
|
|
490
|
+
const rawWords = (this.message?.text || '')
|
|
491
|
+
.trim()
|
|
492
|
+
.split(/\s+/)
|
|
493
|
+
.filter((w) => w.length > 0);
|
|
494
|
+
if (rawWords.length === 0) {
|
|
495
|
+
this.words = [];
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
const step = this.duration / rawWords.length;
|
|
499
|
+
|
|
500
|
+
this.words = rawWords.map((w, i) => ({
|
|
501
|
+
text: w,
|
|
502
|
+
start: i * step,
|
|
503
|
+
end: (i + 1) * step,
|
|
504
|
+
state: 'future' as const,
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
syncStatesFromCurrentTime(): void {
|
|
509
|
+
if (this.skipSyncAnimation) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
const audio = this.audioRef?.nativeElement;
|
|
513
|
+
if (!audio || this.words.length === 0) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
this.currentTime = audio.currentTime;
|
|
517
|
+
let newActiveIndex = -1;
|
|
518
|
+
|
|
519
|
+
this.words.forEach((w, i) => {
|
|
520
|
+
if (this.currentTime >= w.end) {
|
|
521
|
+
w.state = 'past';
|
|
522
|
+
} else if (this.currentTime >= w.start && this.currentTime < w.end) {
|
|
523
|
+
w.state = 'active';
|
|
524
|
+
newActiveIndex = i;
|
|
525
|
+
} else {
|
|
526
|
+
w.state = 'future';
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
if (newActiveIndex !== this.activeIndex) {
|
|
531
|
+
this.activeIndex = newActiveIndex;
|
|
532
|
+
this.scrollToActive();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
onTimeUpdate(): void {
|
|
537
|
+
if (!this.playbackStarted) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
this.syncStatesFromCurrentTime();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
scrollToActive(): void {
|
|
544
|
+
const container = this.transcriptBox?.nativeElement;
|
|
545
|
+
const active = container?.querySelector('.active') as HTMLElement;
|
|
546
|
+
|
|
547
|
+
if (active) {
|
|
548
|
+
active.scrollIntoView({
|
|
549
|
+
behavior: 'smooth',
|
|
550
|
+
block: 'center',
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
trackByIndex(index: number): number {
|
|
556
|
+
return index;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
@@ -64,6 +64,11 @@
|
|
|
64
64
|
[stylesMap]="stylesMap">
|
|
65
65
|
</chat-audio>
|
|
66
66
|
|
|
67
|
+
<chat-audio-sync *ngIf="isAudioTTS(message)"
|
|
68
|
+
[message]="message"
|
|
69
|
+
[color]="fontColor">
|
|
70
|
+
</chat-audio-sync>
|
|
71
|
+
|
|
67
72
|
|
|
68
73
|
<!-- <chat-frame *ngIf="message.metadata && message.metadata.type && message.metadata.type.includes('video')"
|
|
69
74
|
[metadata]="message.metadata"
|
|
@@ -75,7 +80,7 @@
|
|
|
75
80
|
<!-- <div *ngIf="message.type == 'text'"> -->
|
|
76
81
|
|
|
77
82
|
<!-- tooltip="{{message.timestamp | dateAgo}} ({{message.timestamp | date:'shortDate'}} {{message.timestamp | date:'HH:mm:ss'}})" placement="bottom" -->
|
|
78
|
-
<div *ngIf="message?.text && !isAudio(message)" >
|
|
83
|
+
<div *ngIf="message?.text && (!isAudio(message) && !isAudioTTS(message))" >
|
|
79
84
|
|
|
80
85
|
<!-- [htmlEnabled]="(message?.type==='html')? true : false" -->
|
|
81
86
|
<chat-text *ngIf="message?.type !=='html'"
|
|
@@ -5,7 +5,7 @@ import { LoggerService } from 'src/chat21-core/providers/abstract/logger.service
|
|
|
5
5
|
import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
|
|
6
6
|
import { MAX_WIDTH_IMAGES, MESSAGE_TYPE_MINE, MESSAGE_TYPE_OTHERS, MIN_WIDTH_IMAGES } from 'src/chat21-core/utils/constants';
|
|
7
7
|
import { convertColorToRGBA } from 'src/chat21-core/utils/utils';
|
|
8
|
-
import { isAudio, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
|
|
8
|
+
import { isAudio, isAudioTTS, isFile, isFrame, isImage, messageType } from 'src/chat21-core/utils/utils-message';
|
|
9
9
|
import { getColorBck } from 'src/chat21-core/utils/utils-user';
|
|
10
10
|
|
|
11
11
|
@Component({
|
|
@@ -26,6 +26,7 @@ export class BubbleMessageComponent implements OnInit {
|
|
|
26
26
|
isFile = isFile;
|
|
27
27
|
isFrame = isFrame;
|
|
28
28
|
isAudio = isAudio;
|
|
29
|
+
isAudioTTS=isAudioTTS;
|
|
29
30
|
convertColorToRGBA = convertColorToRGBA
|
|
30
31
|
|
|
31
32
|
// ========== begin:: check message type functions ======= //
|
|
@@ -1125,11 +1125,22 @@ export class GlobalSettingsService {
|
|
|
1125
1125
|
if (TEMP !== undefined) {
|
|
1126
1126
|
globals.showAudioRecorderFooterButton = (TEMP === true) ? true : false;
|
|
1127
1127
|
}
|
|
1128
|
+
TEMP = tiledeskSettings['showAudioStreamFooterButton'];
|
|
1129
|
+
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > showAudioStreamFooterButton:: ', TEMP]);
|
|
1130
|
+
if (TEMP !== undefined) {
|
|
1131
|
+
globals.showAudioStreamFooterButton = (TEMP === true) ? true : false;
|
|
1132
|
+
}
|
|
1128
1133
|
TEMP = tiledeskSettings['size'];
|
|
1129
1134
|
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > size:: ', TEMP]);
|
|
1130
1135
|
if (TEMP !== undefined) {
|
|
1131
1136
|
globals.size = TEMP;
|
|
1132
1137
|
}
|
|
1138
|
+
|
|
1139
|
+
TEMP = tiledeskSettings['closeChatInConversation'];
|
|
1140
|
+
// this.logger.debug('[GLOBAL-SET] setVariablesFromSettings > closeChatInConversation:: ', TEMP]);
|
|
1141
|
+
if (TEMP !== undefined) {
|
|
1142
|
+
globals.closeChatInConversation = (TEMP === true) ? true : false;
|
|
1143
|
+
}
|
|
1133
1144
|
}
|
|
1134
1145
|
|
|
1135
1146
|
/**
|
|
@@ -1867,6 +1878,11 @@ export class GlobalSettingsService {
|
|
|
1867
1878
|
globals.showAttachmentFooterButton = stringToBoolean(TEMP);
|
|
1868
1879
|
}
|
|
1869
1880
|
|
|
1881
|
+
TEMP = getParameterByName(windowContext, 'tiledesk_showAudioStreamFooterButton');
|
|
1882
|
+
if (TEMP) {
|
|
1883
|
+
globals.showAudioStreamFooterButton = stringToBoolean(TEMP);
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1870
1886
|
TEMP = getParameterByName(windowContext, 'tiledesk_showEmojiFooterButton');
|
|
1871
1887
|
if (TEMP) {
|
|
1872
1888
|
globals.showEmojiFooterButton = stringToBoolean(TEMP);
|
|
@@ -1876,6 +1892,11 @@ export class GlobalSettingsService {
|
|
|
1876
1892
|
if (TEMP) {
|
|
1877
1893
|
globals.size = TEMP;
|
|
1878
1894
|
}
|
|
1895
|
+
|
|
1896
|
+
TEMP = getParameterByName(windowContext, 'tiledesk_closeChatInConversation');
|
|
1897
|
+
if (TEMP) {
|
|
1898
|
+
globals.closeChatInConversation = stringToBoolean(TEMP);
|
|
1899
|
+
}
|
|
1879
1900
|
|
|
1880
1901
|
}
|
|
1881
1902
|
|
|
@@ -302,6 +302,7 @@ export class TranslatorService {
|
|
|
302
302
|
'CLOSED',
|
|
303
303
|
'LABEL_PREVIEW',
|
|
304
304
|
'MAX_ATTACHMENT',
|
|
305
|
+
'MAX_ATTACHMENT_ERROR',
|
|
305
306
|
'EMOJI'
|
|
306
307
|
];
|
|
307
308
|
|
|
@@ -358,6 +359,7 @@ export class TranslatorService {
|
|
|
358
359
|
globals.LABEL_PREVIEW = res['LABEL_PREVIEW']
|
|
359
360
|
globals.LABEL_ERROR_FIELD_REQUIRED= res['LABEL_ERROR_FIELD_REQUIRED']
|
|
360
361
|
globals.MAX_ATTACHMENT = res['MAX_ATTACHMENT']
|
|
362
|
+
globals.MAX_ATTACHMENT_ERROR = res['MAX_ATTACHMENT_ERROR']
|
|
361
363
|
globals.EMOJI = res['EMOJI']
|
|
362
364
|
|
|
363
365
|
|