@chat21/chat21-web-widget 5.1.32-rc8 → 5.1.32-rc9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/package.json +1 -1
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +5 -2
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +10 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +7 -1
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +3 -4
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +8 -9
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +7 -7
- package/src/app/component/message/audio-sync/audio-sync.component.scss +0 -1
- package/src/app/component/message/audio-sync/audio-sync.component.ts +70 -7
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +15 -0
- package/src/app/providers/voice/voice.service.ts +5 -0
- package/src/app/sass/_variables.scss +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@
|
|
|
6
6
|
### **Copyrigth**:
|
|
7
7
|
*Tiledesk SRL*
|
|
8
8
|
|
|
9
|
+
# 5.1.32-rc9
|
|
10
|
+
- **added**: mic-triggered TTS interruption — when VAD detects user speech, stop current TTS playback, clear the queue, and reveal the full message text
|
|
11
|
+
- **added**: global TTS cancel API (`TtsAudioPlaybackCoordinator.cancelAll()` + `cancelAll$`) to stop current + queued TTS playback from UI/events (e.g. close stream)
|
|
12
|
+
- **changed**: `chat-audio-sync` TTS playback now streams audio via authenticated POST to `message.metadata.src`, sending `voiceSettings` + `text` and `streaming: true`
|
|
13
|
+
- **changed**: stream UI spectrum — removed circular orb and stretched the spectrum line to fill the `#streamAudioAlert` width with 10px side padding
|
|
14
|
+
- **changed**: conversation content layout while streaming — adjusted received bubble left margin and loading spinner margins for full-size mode
|
|
15
|
+
|
|
16
|
+
|
|
9
17
|
# 5.1.32-rc8
|
|
10
18
|
- **changed**: updated the dev environment defaults to align with the stage setup (remote config URL, API endpoints, logging level, storage prefix, and related settings)
|
|
11
19
|
|
package/package.json
CHANGED
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
[ngClass]="{'slide-in-left': false}"
|
|
63
63
|
[class.no-background]="(isImage(message) || isFrame(message) || isCarousel(message)) && ((message?.text && message?.text.trim() === '') || !message?.text)"
|
|
64
64
|
[class.emoticon]="isEmojii(message?.text)"
|
|
65
|
-
[
|
|
65
|
+
[class.fullSizeMessage]="isStreamAudioActive"
|
|
66
|
+
[style.margin-left]="isSameSender(message?.sender, i) ? 'calc(var(--avatar-width) + 10px)' : null"
|
|
66
67
|
[ngStyle]="{'background': stylesMap.get('bubbleReceivedBackground'), 'color': stylesMap.get('bubbleReceivedTextColor'), 'width':isFrame(message) ?'100%' : null}"
|
|
67
68
|
[isSameSender]="isSameSender(message?.sender, i)"
|
|
68
69
|
[message]="message"
|
|
@@ -134,9 +135,10 @@
|
|
|
134
135
|
[senderFullname]="nameUserTypingNow"
|
|
135
136
|
[baseLocation]="baseLocation">
|
|
136
137
|
</chat-avatar-image>
|
|
138
|
+
|
|
137
139
|
<user-typing
|
|
138
|
-
[ngClass]="{'userTypingNowExist': !idUserTypingNow}"
|
|
139
140
|
[color]="stylesMap?.get('iconColor')"
|
|
141
|
+
[ngClass]="{'userTypingNowExist': !idUserTypingNow}"
|
|
140
142
|
[translationMap]="translationMap"
|
|
141
143
|
[idUserTypingNow]="idUserTypingNow"
|
|
142
144
|
[nameUserTypingNow]="nameUserTypingNow">
|
|
@@ -145,6 +147,7 @@
|
|
|
145
147
|
|
|
146
148
|
<div *ngIf="showThinkingMessage && lastServerSenderKind === 'bot'" class="msg_container base_receive thinking_receive">
|
|
147
149
|
<user-typing class="loading thinking-dots"
|
|
150
|
+
[class.fullSize]="isStreamAudioActive"
|
|
148
151
|
[color]="stylesMap?.get('iconColor')"
|
|
149
152
|
[translationMap]="translationMap"
|
|
150
153
|
[idUserTypingNow]="idUserTypingNow"
|
|
@@ -27,6 +27,11 @@
|
|
|
27
27
|
margin: 25px 50px
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
|
|
31
|
+
:host .loading.fullSize ::ng-deep > div.spinner{
|
|
32
|
+
margin: 50px 0px !important;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
// ============= CSS c21-body ================= //
|
|
31
36
|
.c21-body {
|
|
32
37
|
// -webkit-box-shadow: inset 0 10px 10px -10px rgba(0,0,0,0.4);
|
|
@@ -236,6 +241,11 @@
|
|
|
236
241
|
height: fit-content;
|
|
237
242
|
width: auto;
|
|
238
243
|
|
|
244
|
+
&.fullSizeMessage {
|
|
245
|
+
max-width: 100%;
|
|
246
|
+
margin: auto 0 auto 0 !important;
|
|
247
|
+
}
|
|
248
|
+
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
|
package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { findAndRemoveEmoji, isImage } from 'src/chat21-core/utils/utils-message
|
|
|
17
17
|
import { ProjectModel } from 'src/models/project';
|
|
18
18
|
import { Subscription } from 'rxjs';
|
|
19
19
|
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
20
|
+
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
20
21
|
|
|
21
22
|
@Component({
|
|
22
23
|
selector: 'chat-conversation-footer',
|
|
@@ -110,7 +111,8 @@ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy
|
|
|
110
111
|
constructor(private chatManager: ChatManager,
|
|
111
112
|
private typingService: TypingService,
|
|
112
113
|
private uploadService: UploadService,
|
|
113
|
-
private voiceService: VoiceService
|
|
114
|
+
private voiceService: VoiceService,
|
|
115
|
+
private ttsPlayback: TtsAudioPlaybackCoordinator) { }
|
|
114
116
|
|
|
115
117
|
ngOnInit() {
|
|
116
118
|
// this.updateAttachmentTooltip();
|
|
@@ -120,6 +122,7 @@ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy
|
|
|
120
122
|
if(changes['conversationWith'] && changes['conversationWith'].currentValue !== undefined){
|
|
121
123
|
this.conversationHandlerService = this.chatManager.getConversationHandlerByConversationId(this.conversationWith);
|
|
122
124
|
this.isStreamAudioActive = false;
|
|
125
|
+
this.ttsPlayback.cancelAll();
|
|
123
126
|
void this.stopVoice();
|
|
124
127
|
}
|
|
125
128
|
if(changes['hideTextReply'] && changes['hideTextReply'].currentValue !== undefined){
|
|
@@ -740,10 +743,13 @@ export class ConversationFooterComponent implements OnInit, OnChanges, OnDestroy
|
|
|
740
743
|
} catch (e) {
|
|
741
744
|
this.logger.error('[CONV-FOOTER] onStreamPressed: initVoice failed', e);
|
|
742
745
|
this.isStreamAudioActive = false;
|
|
746
|
+
this.ttsPlayback.cancelAll();
|
|
743
747
|
}
|
|
744
748
|
} else {
|
|
745
749
|
await this.stopVoice();
|
|
746
750
|
this.isStreamAudioActive = false;
|
|
751
|
+
// Close-stream-button clicked: stop any playing/queued TTS audio.
|
|
752
|
+
this.ttsPlayback.cancelAll();
|
|
747
753
|
}
|
|
748
754
|
this.onStreamAudioActiveChange.emit(this.isStreamAudioActive);
|
|
749
755
|
this.logger.log('[CONV-FOOTER] isStreamAudioActive', this.isStreamAudioActive);
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
<div class="stream-audio-
|
|
2
|
-
<svg class="stream-audio-spectrum__svg" viewBox="0 0 100
|
|
1
|
+
<div class="stream-audio-spectrum" [ngStyle]="accentColor ? { color: accentColor } : null">
|
|
2
|
+
<svg class="stream-audio-spectrum__svg" viewBox="0 0 100 32" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
3
3
|
<defs>
|
|
4
|
-
<linearGradient [attr.id]="gradientId" x1="
|
|
4
|
+
<linearGradient [attr.id]="gradientId" x1="0" y1="16" x2="100" y2="16" gradientUnits="userSpaceOnUse">
|
|
5
5
|
<stop offset="0%" stop-color="currentColor" stop-opacity="0.45"/>
|
|
6
6
|
<stop offset="50%" stop-color="currentColor" stop-opacity="1"/>
|
|
7
7
|
<stop offset="100%" stop-color="currentColor" stop-opacity="0.45"/>
|
|
8
8
|
</linearGradient>
|
|
9
9
|
</defs>
|
|
10
|
-
<circle cx="50" cy="50" r="46" fill="currentColor" opacity="0.14"/>
|
|
11
10
|
<path class="stream-audio-spectrum__line"
|
|
12
11
|
[attr.d]="spectrumLinePath"
|
|
13
12
|
fill="none"
|
|
@@ -1,22 +1,21 @@
|
|
|
1
1
|
:host {
|
|
2
2
|
display: block;
|
|
3
|
+
width: 100%;
|
|
4
|
+
flex: 1 1 auto;
|
|
3
5
|
}
|
|
4
6
|
|
|
5
|
-
.stream-audio-
|
|
7
|
+
.stream-audio-spectrum {
|
|
6
8
|
display: flex;
|
|
7
9
|
align-items: center;
|
|
8
10
|
justify-content: center;
|
|
9
|
-
width:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
border: 2px solid currentColor;
|
|
13
|
-
background: var(--content-background-color);
|
|
14
|
-
box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.04);
|
|
11
|
+
width: 100%;
|
|
12
|
+
padding: 0 10px;
|
|
13
|
+
box-sizing: border-box;
|
|
15
14
|
}
|
|
16
15
|
|
|
17
16
|
.stream-audio-spectrum__svg {
|
|
18
|
-
width:
|
|
19
|
-
height:
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 32px;
|
|
20
19
|
display: block;
|
|
21
20
|
}
|
|
22
21
|
|
|
@@ -18,7 +18,7 @@ export class StreamAudioSpectrumComponent implements OnInit, OnChanges {
|
|
|
18
18
|
/** Colore tema (stroke / gradient); opzionale. */
|
|
19
19
|
@Input() accentColor?: string;
|
|
20
20
|
|
|
21
|
-
spectrumLinePath = '
|
|
21
|
+
spectrumLinePath = 'M0,16 L100,16';
|
|
22
22
|
|
|
23
23
|
ngOnInit(): void {
|
|
24
24
|
this.refreshPath();
|
|
@@ -37,11 +37,11 @@ export class StreamAudioSpectrumComponent implements OnInit, OnChanges {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
private buildSpectrumLinePath(intensity: number, t: number): string {
|
|
40
|
-
const x0 =
|
|
41
|
-
const x1 =
|
|
42
|
-
const cy =
|
|
43
|
-
const segments =
|
|
44
|
-
const amp =
|
|
40
|
+
const x0 = 0;
|
|
41
|
+
const x1 = 100;
|
|
42
|
+
const cy = 16;
|
|
43
|
+
const segments = 100;
|
|
44
|
+
const amp = 0.8 + intensity * 6.5;
|
|
45
45
|
const parts: string[] = [];
|
|
46
46
|
for (let i = 0; i <= segments; i++) {
|
|
47
47
|
const p = i / segments;
|
|
@@ -54,7 +54,7 @@ export class StreamAudioSpectrumComponent implements OnInit, OnChanges {
|
|
|
54
54
|
Math.sin(u * 6.8 + t * 1.05) * 0.14 +
|
|
55
55
|
Math.sin(u * 9.1 + t * 0.88) * 0.1;
|
|
56
56
|
const y = cy + amp * wobble;
|
|
57
|
-
const yClamped = Math.min(
|
|
57
|
+
const yClamped = Math.min(30, Math.max(2, y));
|
|
58
58
|
parts.push(i === 0 ? `M${x.toFixed(2)},${yClamped.toFixed(2)}` : `L${x.toFixed(2)},${yClamped.toFixed(2)}`);
|
|
59
59
|
}
|
|
60
60
|
return parts.join('');
|
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
SimpleChanges,
|
|
10
10
|
ViewChild,
|
|
11
11
|
} from '@angular/core';
|
|
12
|
+
import { Subscription } from 'rxjs';
|
|
12
13
|
import { MessageModel } from 'src/chat21-core/models/message';
|
|
13
14
|
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
14
15
|
import { Globals } from 'src/app/utils/globals';
|
|
16
|
+
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
15
17
|
|
|
16
18
|
/** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
|
|
17
19
|
const HAVE_METADATA = 1;
|
|
@@ -48,13 +50,17 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
48
50
|
private destroyed = false;
|
|
49
51
|
private playbackRequested = false;
|
|
50
52
|
private playbackStarted = false;
|
|
53
|
+
private micInterrupted = false;
|
|
51
54
|
private streamAbort?: AbortController;
|
|
52
55
|
private mediaSourceObjectUrl?: string;
|
|
56
|
+
private cancelAllSub?: Subscription;
|
|
57
|
+
private micSpeechSub?: Subscription;
|
|
53
58
|
|
|
54
59
|
constructor(
|
|
55
60
|
private readonly cdr: ChangeDetectorRef,
|
|
56
61
|
private readonly ttsPlayback: TtsAudioPlaybackCoordinator,
|
|
57
62
|
private readonly globals: Globals,
|
|
63
|
+
private readonly voiceService: VoiceService,
|
|
58
64
|
) {}
|
|
59
65
|
|
|
60
66
|
/** `false` = messaggio già in storico: niente autoplay / karaoke. Da `message.isJustRecived`. */
|
|
@@ -87,6 +93,28 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
87
93
|
(this.message?.uid && String(this.message.uid).trim()) ||
|
|
88
94
|
`tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
89
95
|
|
|
96
|
+
// Se l’utente parla al microfono mentre sta ascoltando, interrompi TUTTO (corrente + coda).
|
|
97
|
+
this.micSpeechSub = this.voiceService.speechStart$.subscribe(() => {
|
|
98
|
+
if (this.destroyed) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// interrompi solo se questo messaggio era in riproduzione o in attesa
|
|
102
|
+
if (this.playbackStarted || this.playbackRequested) {
|
|
103
|
+
this.micInterrupted = true;
|
|
104
|
+
this.ttsPlayback.cancelAll();
|
|
105
|
+
this.interruptPlaybackAndRevealText();
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Stop globale (es. mic) notificato dal coordinatore: ogni istanza deve fermarsi e mostrare testo intero.
|
|
110
|
+
this.cancelAllSub = this.ttsPlayback.cancelAll$.subscribe(() => {
|
|
111
|
+
if (this.destroyed) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.micInterrupted = true;
|
|
115
|
+
this.interruptPlaybackAndRevealText();
|
|
116
|
+
});
|
|
117
|
+
|
|
90
118
|
this.onPlaybackEnded = () => {
|
|
91
119
|
this.playbackStarted = false;
|
|
92
120
|
this.cleanupStreaming();
|
|
@@ -138,12 +166,19 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
138
166
|
this.cdr.detectChanges();
|
|
139
167
|
|
|
140
168
|
setTimeout(() => {
|
|
141
|
-
if (this.playbackRequested || this.destroyed) {
|
|
169
|
+
if (this.playbackRequested || this.destroyed || this.micInterrupted) {
|
|
170
|
+
if (this.micInterrupted) {
|
|
171
|
+
this.markAllWordsPast();
|
|
172
|
+
if (this.message) {
|
|
173
|
+
this.message.isJustRecived = false;
|
|
174
|
+
}
|
|
175
|
+
this.cdr.detectChanges();
|
|
176
|
+
}
|
|
142
177
|
return;
|
|
143
178
|
}
|
|
144
179
|
this.playbackRequested = true;
|
|
145
180
|
this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
|
|
146
|
-
if (this.destroyed) {
|
|
181
|
+
if (this.destroyed || this.micInterrupted) {
|
|
147
182
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
148
183
|
return;
|
|
149
184
|
}
|
|
@@ -159,6 +194,8 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
159
194
|
this.destroyed = true;
|
|
160
195
|
this.playbackStarted = false;
|
|
161
196
|
this.cleanupStreaming();
|
|
197
|
+
this.cancelAllSub?.unsubscribe();
|
|
198
|
+
this.micSpeechSub?.unsubscribe();
|
|
162
199
|
|
|
163
200
|
const audio = this.audioRef?.nativeElement;
|
|
164
201
|
if (audio) {
|
|
@@ -182,6 +219,31 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
182
219
|
}
|
|
183
220
|
}
|
|
184
221
|
|
|
222
|
+
private interruptPlaybackAndRevealText(): void {
|
|
223
|
+
this.playbackStarted = false;
|
|
224
|
+
this.cleanupStreaming();
|
|
225
|
+
|
|
226
|
+
const audio = this.audioRef?.nativeElement;
|
|
227
|
+
if (audio) {
|
|
228
|
+
try {
|
|
229
|
+
audio.pause();
|
|
230
|
+
audio.currentTime = 0;
|
|
231
|
+
} catch {
|
|
232
|
+
/* ignore */
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Rimuove se era in coda (o rilascia se era corrente).
|
|
237
|
+
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
238
|
+
|
|
239
|
+
// Mostra tutto il testo (niente "future" invisibili).
|
|
240
|
+
this.markAllWordsPast();
|
|
241
|
+
if (this.message) {
|
|
242
|
+
this.message.isJustRecived = false;
|
|
243
|
+
}
|
|
244
|
+
this.cdr.detectChanges();
|
|
245
|
+
}
|
|
246
|
+
|
|
185
247
|
private startPlayback(audio: HTMLAudioElement): void {
|
|
186
248
|
const src = (this.message as any)?.metadata?.src as string | undefined;
|
|
187
249
|
if (!src) {
|
|
@@ -236,8 +298,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
236
298
|
try {
|
|
237
299
|
const headers: Record<string, string> = {
|
|
238
300
|
'Content-Type': 'application/json',
|
|
239
|
-
'Authorization': `${jwt}`
|
|
240
301
|
};
|
|
302
|
+
if (jwt) {
|
|
303
|
+
headers['Authorization'] = jwt;
|
|
304
|
+
}
|
|
241
305
|
|
|
242
306
|
const response = await fetch(endpoint, {
|
|
243
307
|
method: 'POST',
|
|
@@ -399,11 +463,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
399
463
|
try {
|
|
400
464
|
const headers: Record<string, string> = {
|
|
401
465
|
'Content-Type': 'application/json',
|
|
402
|
-
'Authorization': `${jwt}`
|
|
403
466
|
};
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
467
|
+
if (jwt) {
|
|
468
|
+
headers['Authorization'] = jwt;
|
|
469
|
+
}
|
|
407
470
|
|
|
408
471
|
const response = await fetch(endpoint, {
|
|
409
472
|
method: 'POST',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
|
+
import { Observable, Subject } from 'rxjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Garantisce un solo messaggio TTS in riproduzione alla volta.
|
|
@@ -9,6 +10,10 @@ export class TtsAudioPlaybackCoordinator {
|
|
|
9
10
|
private currentOwnerId: string | null = null;
|
|
10
11
|
private readonly queue: Array<{ ownerId: string; start: () => void }> = [];
|
|
11
12
|
|
|
13
|
+
private readonly cancelAllSource = new Subject<void>();
|
|
14
|
+
/** Emesso quando la riproduzione TTS va interrotta globalmente (es. l’utente parla al microfono). */
|
|
15
|
+
readonly cancelAll$: Observable<void> = this.cancelAllSource.asObservable();
|
|
16
|
+
|
|
12
17
|
/**
|
|
13
18
|
* Richiede l'avvio della riproduzione TTS per `ownerId`.
|
|
14
19
|
* Se non c'è nessun TTS attivo, parte subito; altrimenti viene messo in coda.
|
|
@@ -68,4 +73,14 @@ export class TtsAudioPlaybackCoordinator {
|
|
|
68
73
|
release(ownerId: string): void {
|
|
69
74
|
this.releaseIfCurrent(ownerId);
|
|
70
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Interrompe TUTTA la riproduzione TTS (corrente + coda) e notifica i componenti.
|
|
79
|
+
* I componenti devono fermare l’audio e mostrare il testo per intero.
|
|
80
|
+
*/
|
|
81
|
+
cancelAll(): void {
|
|
82
|
+
this.queue.splice(0, this.queue.length);
|
|
83
|
+
this.currentOwnerId = null;
|
|
84
|
+
this.cancelAllSource.next();
|
|
85
|
+
}
|
|
71
86
|
}
|
|
@@ -33,6 +33,10 @@ export class VoiceService {
|
|
|
33
33
|
/** Emesso a ogni fine segmento parlato: audio WebM + opzionalmente `transcript` / `transcriptionError`. */
|
|
34
34
|
readonly audioSegment$: Observable<VoiceSegmentPayload> = this.audioSegmentSubject.asObservable();
|
|
35
35
|
|
|
36
|
+
private readonly speechStartSubject = new Subject<void>();
|
|
37
|
+
/** Emesso quando il microfono intercetta parlato (VAD speech start). */
|
|
38
|
+
readonly speechStart$: Observable<void> = this.speechStartSubject.asObservable();
|
|
39
|
+
|
|
36
40
|
// 🔊 REALTIME VOLUME STREAM
|
|
37
41
|
private readonly volumeSubject = new BehaviorSubject<number>(0);
|
|
38
42
|
readonly volume$: Observable<number> = this.volumeSubject.asObservable();
|
|
@@ -83,6 +87,7 @@ export class VoiceService {
|
|
|
83
87
|
},
|
|
84
88
|
onSpeechStart: () => {
|
|
85
89
|
this.logger.log('[VoiceService] speech start');
|
|
90
|
+
this.speechStartSubject.next();
|
|
86
91
|
this.startMediaRecorderSegment();
|
|
87
92
|
},
|
|
88
93
|
onSpeechEnd: () => {
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
--chat-footer-logo-height: 30px;
|
|
39
39
|
--chat-footer-close-button-height: 30px;
|
|
40
40
|
--chat-footer-border-radius: 16px;
|
|
41
|
-
--chat-footer-stream-button-height:
|
|
41
|
+
--chat-footer-stream-button-height: 50px;
|
|
42
42
|
--chat-footer-stream-button-padding: 10px 0;
|
|
43
43
|
--chat-footer-background-color: #f6f7fb;
|
|
44
44
|
--chat-footer-color: #1a1a1a;
|