@chat21/chat21-web-widget 5.1.32-rc9 → 5.1.33-rc11
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/.angular-mcp-cache/package.json +1 -0
- package/.cursor/angular18-accessibility-auditor-skill.md +442 -0
- package/.cursor/mcp.json +15 -0
- package/.github/workflows/playwright.yml +27 -0
- package/.playwright-mcp/console-2026-05-08T15-31-09-000Z.log +17 -0
- package/.playwright-mcp/console-2026-05-08T15-32-19-412Z.log +89 -0
- package/.playwright-mcp/console-2026-05-08T16-18-48-424Z.log +133 -0
- package/.playwright-mcp/console-2026-05-11T12-54-06-869Z.log +13 -0
- package/.playwright-mcp/console-2026-05-11T12-54-56-229Z.log +147 -0
- package/.playwright-mcp/console-2026-05-11T12-55-47-174Z.log +183 -0
- package/.playwright-mcp/console-2026-05-11T15-34-03-590Z.log +210 -0
- package/.playwright-mcp/console-2026-05-12T15-07-31-880Z.log +118 -0
- package/.playwright-mcp/page-2026-05-08T15-32-19-900Z.yml +851 -0
- package/.playwright-mcp/page-2026-05-08T15-32-47-264Z.yml +857 -0
- package/.playwright-mcp/page-2026-05-08T15-33-17-089Z.yml +1110 -0
- package/.playwright-mcp/page-2026-05-08T15-33-23-486Z.yml +1069 -0
- package/.playwright-mcp/page-2026-05-08T15-33-45-390Z.yml +1076 -0
- package/.playwright-mcp/page-2026-05-08T15-33-52-666Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-01-338Z.yml +1085 -0
- package/.playwright-mcp/page-2026-05-08T15-34-07-227Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-13-875Z.yml +1072 -0
- package/.playwright-mcp/page-2026-05-08T15-34-21-885Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-34-32-755Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T15-35-09-607Z.yml +1119 -0
- package/.playwright-mcp/page-2026-05-08T15-35-14-242Z.yml +1109 -0
- package/.playwright-mcp/page-2026-05-08T16-18-48-671Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-08T16-18-52-753Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-13-919Z.yml +68 -0
- package/.playwright-mcp/page-2026-05-08T16-19-17-977Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-25-733Z.yml +120 -0
- package/.playwright-mcp/page-2026-05-08T16-19-29-252Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-19-39-269Z.yml +80 -0
- package/.playwright-mcp/page-2026-05-08T16-19-43-915Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-04-407Z.yml +81 -0
- package/.playwright-mcp/page-2026-05-08T16-20-08-984Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-32-397Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-20-58-658Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-21-12-320Z.yml +86 -0
- package/.playwright-mcp/page-2026-05-08T16-21-39-154Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-21-45-420Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-21-062Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-22-58-232Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-23-36-520Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-08T16-23-46-805Z.yml +100 -0
- package/.playwright-mcp/page-2026-05-08T16-23-55-169Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-24-26-574Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-25-34-414Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-25-59-831Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-26-21-809Z.yml +91 -0
- package/.playwright-mcp/page-2026-05-08T16-26-47-443Z.yml +105 -0
- package/.playwright-mcp/page-2026-05-08T16-26-56-136Z.png +0 -0
- package/.playwright-mcp/page-2026-05-08T16-27-59-610Z.yml +48 -0
- package/.playwright-mcp/page-2026-05-11T12-54-07-180Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-11T12-54-56-946Z.yml +4 -0
- package/.playwright-mcp/page-2026-05-11T12-55-47-503Z.yml +24 -0
- package/.playwright-mcp/page-2026-05-11T12-56-00-766Z.yml +28 -0
- package/.playwright-mcp/page-2026-05-11T12-56-06-438Z.yml +90 -0
- package/.playwright-mcp/page-2026-05-11T12-57-56-838Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-58-00-124Z.yml +106 -0
- package/.playwright-mcp/page-2026-05-11T12-59-08-836Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-12-088Z.yml +61 -0
- package/.playwright-mcp/page-2026-05-11T12-59-26-215Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-29-519Z.yml +69 -0
- package/.playwright-mcp/page-2026-05-11T12-59-37-309Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-11T12-59-39-968Z.yml +79 -0
- package/.playwright-mcp/page-2026-05-11T12-59-45-983Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T12-59-49-951Z.yml +78 -0
- package/.playwright-mcp/page-2026-05-11T15-34-04-515Z.yml +0 -0
- package/.playwright-mcp/page-2026-05-12T15-07-32-171Z.yml +44 -0
- package/.playwright-mcp/page-2026-05-12T15-08-09-820Z.yml +119 -0
- package/CHANGELOG.md +61 -4
- package/angular.json +20 -3
- package/deploy_amazon_beta.sh +7 -17
- package/deploy_amazon_prod.sh +41 -0
- package/docs/TILEDESK_WIDGET_ACCESSIBILITY_STATEMENT_COMPLETE.md +379 -0
- package/env.sample +3 -2
- package/mocks/voice-websocket-mock/server.cjs +245 -0
- package/package.json +7 -3
- package/playwright.config.ts +41 -0
- package/src/app/app.component.html +2 -2
- package/src/app/app.component.scss +25 -14
- package/src/app/app.component.spec.ts +21 -6
- package/src/app/app.module.ts +4 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.html +19 -11
- package/src/app/component/conversation-detail/conversation/conversation.component.scss +28 -0
- package/src/app/component/conversation-detail/conversation/conversation.component.spec.ts +644 -75
- package/src/app/component/conversation-detail/conversation/conversation.component.ts +63 -17
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.html +25 -13
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.spec.ts +123 -5
- package/src/app/component/conversation-detail/conversation-audio-recorder/conversation-audio-recorder.component.ts +1 -0
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.html +17 -7
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.scss +15 -3
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.spec.ts +242 -149
- package/src/app/component/conversation-detail/conversation-content/conversation-content.component.ts +7 -6
- package/src/app/component/conversation-detail/conversation-emojii/conversation-emojii.component.spec.ts +53 -3
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component copy.html +172 -0
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.html +112 -61
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.scss +133 -16
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.spec.ts +452 -78
- package/src/app/component/conversation-detail/conversation-footer/conversation-footer.component.ts +198 -84
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.html +113 -53
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.scss +12 -4
- package/src/app/component/conversation-detail/conversation-header/conversation-header.component.spec.ts +274 -29
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.html +23 -9
- package/src/app/component/conversation-detail/conversation-internal-frame/conversation-internal-frame.component.spec.ts +80 -8
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.html +29 -23
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.spec.ts +185 -16
- package/src/app/component/conversation-detail/conversation-preview/conversation-preview.component.ts +34 -14
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.html +46 -18
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.scss +60 -2
- package/src/app/component/conversation-detail/stream-audio-spectrum/stream-audio-spectrum.component.ts +135 -5
- package/src/app/component/error-alert/error-alert.component.spec.ts +65 -5
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.html +16 -7
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.scss +21 -0
- package/src/app/component/eyeeye-catcher-card/eyeeye-catcher-card.component.spec.ts +89 -7
- package/src/app/component/form/form-builder/form-builder.component.html +1 -1
- package/src/app/component/form/form-builder/form-builder.component.spec.ts +163 -21
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.html +8 -4
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.scss +10 -5
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.spec.ts +90 -16
- package/src/app/component/form/inputs/form-checkbox/form-checkbox.component.ts +26 -0
- package/src/app/component/form/inputs/form-label/form-label.component.spec.ts +45 -11
- package/src/app/component/form/inputs/form-radio-button/form-radio-button.component.spec.ts +24 -6
- package/src/app/component/form/inputs/form-select/form-select.component.spec.ts +14 -5
- package/src/app/component/form/inputs/form-text/form-text.component.html +14 -12
- package/src/app/component/form/inputs/form-text/form-text.component.scss +11 -1
- package/src/app/component/form/inputs/form-text/form-text.component.spec.ts +113 -17
- package/src/app/component/form/inputs/form-text/form-text.component.ts +35 -3
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.html +13 -11
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.scss +6 -5
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.spec.ts +149 -13
- package/src/app/component/form/inputs/form-textarea/form-textarea.component.ts +26 -0
- package/src/app/component/form/prechat-form/prechat-form.component.html +14 -11
- package/src/app/component/form/prechat-form/prechat-form.component.spec.ts +102 -10
- package/src/app/component/form/prechat-form/prechat-form.component.ts +8 -1
- package/src/app/component/form/prechat-form-test-mock.ts +35 -0
- package/src/app/component/home/home.component.html +38 -31
- package/src/app/component/home/home.component.scss +4 -2
- package/src/app/component/home/home.component.spec.ts +226 -11
- package/src/app/component/home-conversations/home-conversations.component.html +30 -26
- package/src/app/component/home-conversations/home-conversations.component.scss +3 -0
- package/src/app/component/home-conversations/home-conversations.component.spec.ts +212 -36
- package/src/app/component/last-message/last-message.component.html +15 -9
- package/src/app/component/last-message/last-message.component.scss +16 -2
- package/src/app/component/last-message/last-message.component.spec.ts +204 -23
- package/src/app/component/launcher-button/launcher-button.component.html +8 -13
- package/src/app/component/launcher-button/launcher-button.component.spec.ts +104 -8
- package/src/app/component/list-all-conversations/list-all-conversations.component.html +12 -17
- package/src/app/component/list-all-conversations/list-all-conversations.component.scss +2 -0
- package/src/app/component/list-conversations/list-conversations.component.html +22 -22
- package/src/app/component/menu-options/menu-options.component.html +30 -20
- package/src/app/component/menu-options/menu-options.component.spec.ts +125 -9
- package/src/app/component/message/audio/audio.component.html +13 -15
- package/src/app/component/message/audio/audio.component.spec.ts +140 -5
- package/src/app/component/message/audio/audio.component.ts +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.scss +1 -0
- package/src/app/component/message/audio-sync/audio-sync.component.spec.ts +81 -1
- package/src/app/component/message/audio-sync/audio-sync.component.ts +133 -86
- package/src/app/component/message/avatar/avatar.component.html +2 -2
- package/src/app/component/message/avatar/avatar.component.spec.ts +99 -7
- package/src/app/component/message/bubble-message/bubble-message.component.html +39 -52
- package/src/app/component/message/bubble-message/bubble-message.component.scss +59 -1
- package/src/app/component/message/bubble-message/bubble-message.component.spec.ts +154 -57
- package/src/app/component/message/bubble-message/bubble-message.component.ts +152 -110
- package/src/app/component/message/buttons/action-button/action-button.component.html +3 -4
- package/src/app/component/message/buttons/action-button/action-button.component.spec.ts +49 -5
- package/src/app/component/message/buttons/link-button/link-button.component.scss +5 -8
- package/src/app/component/message/buttons/link-button/link-button.component.spec.ts +50 -5
- package/src/app/component/message/buttons/text-button/text-button.component.spec.ts +44 -5
- package/src/app/component/message/carousel/carousel.component.html +29 -16
- package/src/app/component/message/carousel/carousel.component.scss +20 -8
- package/src/app/component/message/carousel/carousel.component.spec.ts +80 -3
- package/src/app/component/message/carousel/carousel.component.ts +16 -0
- package/src/app/component/message/frame/frame.component.html +9 -4
- package/src/app/component/message/frame/frame.component.spec.ts +34 -15
- package/src/app/component/message/frame/frame.component.ts +7 -2
- package/src/app/component/message/html/html.component.html +1 -1
- package/src/app/component/message/html/html.component.scss +1 -1
- package/src/app/component/message/html/html.component.spec.ts +24 -7
- package/src/app/component/message/image/image.component.html +12 -10
- package/src/app/component/message/image/image.component.scss +16 -0
- package/src/app/component/message/image/image.component.spec.ts +101 -15
- package/src/app/component/message/image/image.component.ts +90 -51
- package/src/app/component/message/info-message/info-message.component.spec.ts +26 -14
- package/src/app/component/message/json-sources/json-sources.component.html +38 -0
- package/src/app/component/message/json-sources/json-sources.component.scss +201 -0
- package/src/app/component/message/json-sources/json-sources.component.ts +89 -0
- package/src/app/component/message/like-unlike/like-unlike.component.html +7 -9
- package/src/app/component/message/like-unlike/like-unlike.component.spec.ts +31 -3
- package/src/app/component/message/return-receipt/return-receipt.component.spec.ts +38 -17
- package/src/app/component/message/text/text.component.html +3 -3
- package/src/app/component/message/text/text.component.scss +80 -86
- package/src/app/component/message/text/text.component.spec.ts +106 -13
- package/src/app/component/message-attachment/message-attachment.component.spec.ts +134 -13
- package/src/app/component/selection-department/selection-department.component.html +21 -23
- package/src/app/component/selection-department/selection-department.component.spec.ts +159 -14
- package/src/app/component/selection-department/selection-department.component.ts +8 -1
- package/src/app/component/send-button/send-button.component.html +5 -13
- package/src/app/component/send-button/send-button.component.spec.ts +2 -2
- package/src/app/component/star-rating-widget/star-rating-widget.component.html +51 -81
- package/src/app/directives/tooltip.directive.spec.ts +8 -4
- package/src/app/modals/confirm-close/confirm-close.component.html +20 -8
- package/src/app/modals/confirm-close/confirm-close.component.scss +3 -0
- package/src/app/modals/confirm-close/confirm-close.component.spec.ts +13 -4
- package/src/app/modals/confirm-close/confirm-close.component.ts +8 -1
- package/src/app/pipe/html-entites-encode.pipe.spec.ts +35 -2
- package/src/app/pipe/marked.pipe.spec.ts +38 -2
- package/src/app/pipe/marked.pipe.ts +51 -41
- package/src/app/providers/app-config.service.ts +4 -2
- package/src/app/providers/brand.service.spec.ts +23 -2
- package/src/app/providers/brand.service.ts +1 -1
- package/src/app/providers/global-settings.service.spec.ts +1009 -14
- package/src/app/providers/global-settings.service.ts +59 -2
- package/src/app/providers/json-sources-parser.service.ts +175 -0
- package/src/app/providers/translator.service.ts +24 -6
- package/src/app/providers/tts-audio-playback-coordinator.service.spec.ts +117 -0
- package/src/app/providers/tts-audio-playback-coordinator.service.ts +39 -16
- package/src/app/providers/url-preview.service.ts +82 -0
- package/src/app/providers/voice/audio.types.ts +6 -0
- package/src/app/providers/voice/voice-streaming.service.spec.ts +23 -0
- package/src/app/providers/voice/voice-streaming.service.ts +702 -0
- package/src/app/providers/voice/voice-streaming.types.ts +112 -0
- package/src/app/providers/voice/voice.service.spec.ts +170 -3
- package/src/app/providers/voice/voice.service.ts +691 -17
- package/src/app/sass/_variables.scss +1 -1
- package/src/app/sass/animations.scss +19 -1
- package/src/app/utils/globals.ts +14 -0
- package/src/app/utils/json-sources-utils.ts +27 -0
- package/src/app/utils/url-utils.ts +98 -0
- package/src/app/utils/utils-resources.ts +1 -1
- package/src/assets/i18n/en.json +106 -100
- package/src/assets/i18n/es.json +107 -101
- package/src/assets/i18n/fr.json +107 -101
- package/src/assets/i18n/it.json +107 -99
- package/src/assets/sounds/keyboard.mp3 +0 -0
- package/src/assets/twp/index-dev.html +18 -0
- package/src/assets/twp/tiledesk_widget_files/widget-css-override-example.css +14 -0
- package/src/chat21-core/providers/chat-manager.spec.ts +72 -0
- package/src/chat21-core/providers/scripts/script.service.spec.ts +12 -2
- package/src/chat21-core/utils/constants.ts +4 -0
- package/src/chat21-core/utils/utils-message.ts +23 -1
- package/src/widget-config-template.json +3 -1
- package/src/widget-config.json +28 -27
- package/tests/widget-form-rich.spec.ts +67 -0
- package/tests/widget-index-dev-settings.spec.ts +52 -0
- package/tests/widget-twp-iframe.spec.ts +39 -0
|
@@ -1,23 +1,158 @@
|
|
|
1
1
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { LoggerInstance } from 'src/chat21-core/providers/logger/loggerInstance';
|
|
3
|
+
import { CustomLogger } from 'src/chat21-core/providers/logger/customLogger';
|
|
4
|
+
import { NGXLogger } from 'ngx-logger';
|
|
2
5
|
|
|
3
6
|
import { AudioComponent } from './audio.component';
|
|
4
7
|
|
|
5
|
-
describe('
|
|
8
|
+
describe('AudioComponent', () => {
|
|
6
9
|
let component: AudioComponent;
|
|
7
10
|
let fixture: ComponentFixture<AudioComponent>;
|
|
11
|
+
const ngxlogger = jasmine.createSpyObj('NGXLogger', ['log', 'trace', 'debug', 'warn', 'error', 'info']);
|
|
12
|
+
const customLogger = new CustomLogger(ngxlogger);
|
|
13
|
+
const arrayBuf = new ArrayBuffer(64);
|
|
14
|
+
|
|
15
|
+
const fakeBuffer = {
|
|
16
|
+
duration: 90,
|
|
17
|
+
getChannelData: () => new Float32Array(4000),
|
|
18
|
+
} as unknown as AudioBuffer;
|
|
8
19
|
|
|
9
20
|
beforeEach(async () => {
|
|
21
|
+
LoggerInstance.setInstance(customLogger);
|
|
22
|
+
spyOn(window, 'fetch').and.returnValue(
|
|
23
|
+
Promise.resolve({
|
|
24
|
+
arrayBuffer: () => Promise.resolve(arrayBuf),
|
|
25
|
+
} as Response),
|
|
26
|
+
);
|
|
27
|
+
spyOn(AudioContext.prototype, 'decodeAudioData').and.returnValue(Promise.resolve(fakeBuffer));
|
|
28
|
+
|
|
10
29
|
await TestBed.configureTestingModule({
|
|
11
|
-
declarations: [
|
|
30
|
+
declarations: [AudioComponent],
|
|
12
31
|
})
|
|
13
|
-
|
|
32
|
+
.overrideComponent(AudioComponent, {
|
|
33
|
+
set: {
|
|
34
|
+
template: `
|
|
35
|
+
<div class="audio-container">
|
|
36
|
+
<div class="audio-track"></div>
|
|
37
|
+
<div class="audio-player-custom">
|
|
38
|
+
<audio #audioElement></audio>
|
|
39
|
+
<canvas #canvasElement width="120" height="32"></canvas>
|
|
40
|
+
</div>
|
|
41
|
+
</div>`,
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
.compileComponents();
|
|
14
45
|
|
|
15
46
|
fixture = TestBed.createComponent(AudioComponent);
|
|
16
47
|
component = fixture.componentInstance;
|
|
17
|
-
|
|
48
|
+
component.stylesMap = new Map<string, string>([
|
|
49
|
+
['bubbleSentBackground', 'rgba(10, 20, 30, 1)'],
|
|
50
|
+
['bubbleSentTextColor', '#112233'],
|
|
51
|
+
]);
|
|
52
|
+
component.color = '#000000';
|
|
18
53
|
});
|
|
19
54
|
|
|
20
|
-
it('should create', () => {
|
|
55
|
+
it('should create', async () => {
|
|
56
|
+
const blob = new Blob([new Uint8Array(arrayBuf.byteLength)], { type: 'audio/wav' });
|
|
57
|
+
component.audioBlob = blob;
|
|
58
|
+
fixture.detectChanges();
|
|
59
|
+
await fixture.whenStable();
|
|
60
|
+
fixture.detectChanges();
|
|
21
61
|
expect(component).toBeTruthy();
|
|
22
62
|
});
|
|
63
|
+
|
|
64
|
+
it('formatTime should pad seconds under 10', () => {
|
|
65
|
+
expect(component.formatTime(0)).toBe('0:00');
|
|
66
|
+
expect(component.formatTime(9)).toBe('0:09');
|
|
67
|
+
expect(component.formatTime(70)).toBe('1:10');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('extractFirstColor should parse first rgba from gradient string', () => {
|
|
71
|
+
expect(component.extractFirstColor('linear-gradient(rgba(1, 2, 3, 0.5), red)')).toBe('rgba(1, 2, 3, 0.5)');
|
|
72
|
+
expect(component.extractFirstColor('no-color')).toBeNull();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('drawWaveform should return early when canvas context missing', () => {
|
|
76
|
+
const canvas = document.createElement('canvas');
|
|
77
|
+
spyOn(canvas, 'getContext').and.returnValue(null);
|
|
78
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
79
|
+
(component as any).audioBuffer = fakeBuffer;
|
|
80
|
+
(component as any).audioDuration = 10;
|
|
81
|
+
(component as any).audioElement = {
|
|
82
|
+
nativeElement: { currentTime: 0, paused: true },
|
|
83
|
+
};
|
|
84
|
+
expect(() => component.drawWaveform(fakeBuffer)).not.toThrow();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('drawWaveform should render bars when context exists', () => {
|
|
88
|
+
const fillRect = jasmine.createSpy('fillRect');
|
|
89
|
+
const clearRect = jasmine.createSpy('clearRect');
|
|
90
|
+
const canvas = document.createElement('canvas');
|
|
91
|
+
canvas.width = 200;
|
|
92
|
+
canvas.height = 40;
|
|
93
|
+
spyOn(canvas, 'getContext').and.returnValue({ fillRect, clearRect } as any);
|
|
94
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
95
|
+
(component as any).audioElement = {
|
|
96
|
+
nativeElement: { currentTime: 0, paused: true },
|
|
97
|
+
};
|
|
98
|
+
(component as any).audioDuration = 10;
|
|
99
|
+
component.drawWaveform(fakeBuffer);
|
|
100
|
+
expect(clearRect).toHaveBeenCalled();
|
|
101
|
+
expect(fillRect).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('ngAfterViewInit with blob should wire object URL and CSS vars', async () => {
|
|
105
|
+
const blob = new Blob([new Uint8Array(128)], { type: 'audio/wav' });
|
|
106
|
+
component.audioBlob = blob;
|
|
107
|
+
spyOn(URL, 'createObjectURL').and.returnValue('blob:mock-audio');
|
|
108
|
+
fixture.detectChanges();
|
|
109
|
+
await fixture.whenStable();
|
|
110
|
+
fixture.detectChanges();
|
|
111
|
+
expect(component.rawAudioUrl).toBe('blob:mock-audio');
|
|
112
|
+
expect(URL.createObjectURL).toHaveBeenCalledWith(blob);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('ngAfterViewInit with metadata.src should fetch and decode', async () => {
|
|
116
|
+
component.audioBlob = null;
|
|
117
|
+
component.metadata = { src: 'blob:from-meta' };
|
|
118
|
+
fixture.detectChanges();
|
|
119
|
+
await fixture.whenStable();
|
|
120
|
+
fixture.detectChanges();
|
|
121
|
+
expect(window.fetch).toHaveBeenCalled();
|
|
122
|
+
expect(component.audioDuration).toBe(90);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('playPauseAudio should toggle play state when buffer ready', () => {
|
|
126
|
+
spyOn(window, 'requestAnimationFrame').and.stub();
|
|
127
|
+
(component as any).audioBuffer = fakeBuffer;
|
|
128
|
+
(component as any).audioDuration = 10;
|
|
129
|
+
const play = jasmine.createSpy('play').and.returnValue(Promise.resolve());
|
|
130
|
+
const pause = jasmine.createSpy('pause');
|
|
131
|
+
const canvas = document.createElement('canvas');
|
|
132
|
+
canvas.width = 120;
|
|
133
|
+
canvas.height = 32;
|
|
134
|
+
spyOn(canvas, 'getContext').and.returnValue({
|
|
135
|
+
fillRect: jasmine.createSpy(),
|
|
136
|
+
clearRect: jasmine.createSpy(),
|
|
137
|
+
} as any);
|
|
138
|
+
(component as any).waveformCanvas = { nativeElement: canvas };
|
|
139
|
+
(component as any).audioElement = {
|
|
140
|
+
nativeElement: { paused: true, currentTime: 0, play, pause, ontimeupdate: null as any, onended: null as any },
|
|
141
|
+
};
|
|
142
|
+
(component as any).audioContext = { resume: jasmine.createSpy().and.returnValue(Promise.resolve()) };
|
|
143
|
+
|
|
144
|
+
component.playPauseAudio();
|
|
145
|
+
expect(play).toHaveBeenCalled();
|
|
146
|
+
expect(component.isPlaying).toBe(true);
|
|
147
|
+
|
|
148
|
+
(component as any).audioElement.nativeElement.paused = false;
|
|
149
|
+
component.playPauseAudio();
|
|
150
|
+
expect(pause).toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('getAudioDuration should set audioDuration from decoded buffer', async () => {
|
|
154
|
+
component.metadata = { src: 'blob:x' };
|
|
155
|
+
await component.getAudioDuration();
|
|
156
|
+
expect(component.audioDuration).toBe(90);
|
|
157
|
+
});
|
|
23
158
|
});
|
|
@@ -18,6 +18,7 @@ export class AudioComponent implements AfterViewInit {
|
|
|
18
18
|
@Input() metadata: any | null = null;
|
|
19
19
|
@Input() color: string;
|
|
20
20
|
@Input() stylesMap: Map<string, string>;
|
|
21
|
+
@Input() translationMap: Map<string, string>;
|
|
21
22
|
|
|
22
23
|
audioUrl: SafeUrl | null = null;
|
|
23
24
|
rawAudioUrl: string | null = null;
|
|
@@ -1,14 +1,39 @@
|
|
|
1
1
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
2
3
|
|
|
3
4
|
import { AudioSyncComponent } from './audio-sync.component';
|
|
5
|
+
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
6
|
+
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
7
|
+
import { Globals } from 'src/app/utils/globals';
|
|
4
8
|
|
|
5
9
|
describe('AudioSyncComponent', () => {
|
|
6
10
|
let component: AudioSyncComponent;
|
|
7
11
|
let fixture: ComponentFixture<AudioSyncComponent>;
|
|
12
|
+
let voiceService: { proxyTtsStreamUrl: string | null; proxyTtsUrl: string | null };
|
|
8
13
|
|
|
9
14
|
beforeEach(async () => {
|
|
15
|
+
voiceService = {
|
|
16
|
+
proxyTtsStreamUrl: 'https://speech.example.com/api/tts/stream',
|
|
17
|
+
proxyTtsUrl: 'https://speech.example.com/api/tts',
|
|
18
|
+
};
|
|
19
|
+
|
|
10
20
|
await TestBed.configureTestingModule({
|
|
11
|
-
|
|
21
|
+
declarations: [AudioSyncComponent],
|
|
22
|
+
imports: [CommonModule],
|
|
23
|
+
providers: [
|
|
24
|
+
{
|
|
25
|
+
provide: TtsAudioPlaybackCoordinator,
|
|
26
|
+
useValue: {
|
|
27
|
+
requestStart: (_ownerId: string, start: () => void) => start(),
|
|
28
|
+
releaseIfCurrent: jasmine.createSpy('releaseIfCurrent'),
|
|
29
|
+
release: jasmine.createSpy('release'),
|
|
30
|
+
stopAllPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
|
|
31
|
+
preemptPlayback$: { subscribe: () => ({ unsubscribe: () => undefined }) },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{ provide: Globals, useValue: { tiledeskToken: 'JWT test-token', jwt: '' } },
|
|
35
|
+
{ provide: VoiceService, useValue: voiceService },
|
|
36
|
+
],
|
|
12
37
|
})
|
|
13
38
|
.compileComponents();
|
|
14
39
|
|
|
@@ -20,4 +45,59 @@ describe('AudioSyncComponent', () => {
|
|
|
20
45
|
it('should create', () => {
|
|
21
46
|
expect(component).toBeTruthy();
|
|
22
47
|
});
|
|
48
|
+
|
|
49
|
+
it('starts TTS playback from the proxy streaming endpoint first', () => {
|
|
50
|
+
component.message = {
|
|
51
|
+
uid: 'm1',
|
|
52
|
+
type: 'tts',
|
|
53
|
+
text: 'hello',
|
|
54
|
+
metadata: {},
|
|
55
|
+
isJustRecived: true,
|
|
56
|
+
} as any;
|
|
57
|
+
const audio = document.createElement('audio');
|
|
58
|
+
const startStreaming = spyOn(component as any, 'startStreamingFromEndpoint').and.stub();
|
|
59
|
+
|
|
60
|
+
(component as any).startPlayback(audio);
|
|
61
|
+
|
|
62
|
+
expect(startStreaming).toHaveBeenCalledWith(
|
|
63
|
+
audio,
|
|
64
|
+
'https://speech.example.com/api/tts/stream',
|
|
65
|
+
'https://speech.example.com/api/tts',
|
|
66
|
+
undefined,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('requests browser-compatible MP3 for proxy REST TTS by default', () => {
|
|
71
|
+
component.message = {
|
|
72
|
+
uid: 'm1',
|
|
73
|
+
type: 'tts',
|
|
74
|
+
text: 'hello',
|
|
75
|
+
metadata: {},
|
|
76
|
+
} as any;
|
|
77
|
+
|
|
78
|
+
const body = (component as any).buildTtsRequestBody({});
|
|
79
|
+
|
|
80
|
+
expect(body).toEqual({
|
|
81
|
+
text: 'hello',
|
|
82
|
+
streaming: true,
|
|
83
|
+
outputFormat: 'mp3_44100_128',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('does not override an explicit TTS outputFormat from message voice settings', () => {
|
|
88
|
+
component.message = {
|
|
89
|
+
uid: 'm1',
|
|
90
|
+
type: 'tts',
|
|
91
|
+
text: 'hello',
|
|
92
|
+
metadata: {},
|
|
93
|
+
} as any;
|
|
94
|
+
|
|
95
|
+
const body = (component as any).buildTtsRequestBody({ outputFormat: 'pcm_16000' });
|
|
96
|
+
|
|
97
|
+
expect(body).toEqual({
|
|
98
|
+
text: 'hello',
|
|
99
|
+
streaming: true,
|
|
100
|
+
outputFormat: 'pcm_16000',
|
|
101
|
+
});
|
|
102
|
+
});
|
|
23
103
|
});
|
|
@@ -12,11 +12,12 @@ import {
|
|
|
12
12
|
import { Subscription } from 'rxjs';
|
|
13
13
|
import { MessageModel } from 'src/chat21-core/models/message';
|
|
14
14
|
import { TtsAudioPlaybackCoordinator } from 'src/app/providers/tts-audio-playback-coordinator.service';
|
|
15
|
-
import { Globals } from 'src/app/utils/globals';
|
|
16
15
|
import { VoiceService } from 'src/app/providers/voice/voice.service';
|
|
16
|
+
import { Globals } from 'src/app/utils/globals';
|
|
17
17
|
|
|
18
18
|
/** HAVE_METADATA: metadati già disponibili (tipico audio servito da cache). */
|
|
19
19
|
const HAVE_METADATA = 1;
|
|
20
|
+
const BROWSER_TTS_OUTPUT_FORMAT = 'mp3_44100_128';
|
|
20
21
|
|
|
21
22
|
@Component({
|
|
22
23
|
selector: 'chat-audio-sync',
|
|
@@ -50,11 +51,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
50
51
|
private destroyed = false;
|
|
51
52
|
private playbackRequested = false;
|
|
52
53
|
private playbackStarted = false;
|
|
53
|
-
private micInterrupted = false;
|
|
54
54
|
private streamAbort?: AbortController;
|
|
55
55
|
private mediaSourceObjectUrl?: string;
|
|
56
|
-
private
|
|
57
|
-
private
|
|
56
|
+
private stopAllSub?: Subscription;
|
|
57
|
+
private preemptSub?: Subscription;
|
|
58
58
|
|
|
59
59
|
constructor(
|
|
60
60
|
private readonly cdr: ChangeDetectorRef,
|
|
@@ -93,28 +93,6 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
93
93
|
(this.message?.uid && String(this.message.uid).trim()) ||
|
|
94
94
|
`tts-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
|
|
95
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
|
-
|
|
118
96
|
this.onPlaybackEnded = () => {
|
|
119
97
|
this.playbackStarted = false;
|
|
120
98
|
this.cleanupStreaming();
|
|
@@ -166,19 +144,12 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
166
144
|
this.cdr.detectChanges();
|
|
167
145
|
|
|
168
146
|
setTimeout(() => {
|
|
169
|
-
if (this.playbackRequested || this.destroyed
|
|
170
|
-
if (this.micInterrupted) {
|
|
171
|
-
this.markAllWordsPast();
|
|
172
|
-
if (this.message) {
|
|
173
|
-
this.message.isJustRecived = false;
|
|
174
|
-
}
|
|
175
|
-
this.cdr.detectChanges();
|
|
176
|
-
}
|
|
147
|
+
if (this.playbackRequested || this.destroyed) {
|
|
177
148
|
return;
|
|
178
149
|
}
|
|
179
150
|
this.playbackRequested = true;
|
|
180
151
|
this.ttsPlayback.requestStart(this.playbackOwnerId, () => {
|
|
181
|
-
if (this.destroyed
|
|
152
|
+
if (this.destroyed) {
|
|
182
153
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
183
154
|
return;
|
|
184
155
|
}
|
|
@@ -188,14 +159,59 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
188
159
|
this.startPlayback(audio);
|
|
189
160
|
});
|
|
190
161
|
}, 200);
|
|
162
|
+
|
|
163
|
+
// Stop signal: user pressed X while this TTS was playing or queued.
|
|
164
|
+
this.stopAllSub = this.ttsPlayback.stopAllPlayback$.subscribe(() => {
|
|
165
|
+
if (!this.playbackRequested && !this.playbackStarted) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
this.destroyed = true;
|
|
169
|
+
this.playbackStarted = false;
|
|
170
|
+
this.cleanupStreaming();
|
|
171
|
+
try {
|
|
172
|
+
audio.pause();
|
|
173
|
+
audio.currentTime = 0;
|
|
174
|
+
} catch {
|
|
175
|
+
/* ignore */
|
|
176
|
+
}
|
|
177
|
+
this.markAllWordsPast();
|
|
178
|
+
if (this.message) {
|
|
179
|
+
this.message.isJustRecived = false;
|
|
180
|
+
}
|
|
181
|
+
this.cdr.detectChanges();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// Preempt signal: a newer message requested start while this one was playing.
|
|
185
|
+
// Only react when the emitted id matches this component's own ownerId.
|
|
186
|
+
this.preemptSub = this.ttsPlayback.preemptPlayback$.subscribe((stoppedId) => {
|
|
187
|
+
if (stoppedId !== this.playbackOwnerId) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.playbackStarted = false;
|
|
191
|
+
this.cleanupStreaming();
|
|
192
|
+
try {
|
|
193
|
+
audio.pause();
|
|
194
|
+
audio.currentTime = 0;
|
|
195
|
+
} catch {
|
|
196
|
+
/* ignore */
|
|
197
|
+
}
|
|
198
|
+
this.markAllWordsPast();
|
|
199
|
+
if (this.message) {
|
|
200
|
+
this.message.isJustRecived = false;
|
|
201
|
+
}
|
|
202
|
+
this.cdr.detectChanges();
|
|
203
|
+
// No releaseIfCurrent call — the coordinator already cleared currentOwnerId before emitting.
|
|
204
|
+
});
|
|
191
205
|
}
|
|
192
206
|
|
|
193
207
|
ngOnDestroy(): void {
|
|
194
208
|
this.destroyed = true;
|
|
195
209
|
this.playbackStarted = false;
|
|
196
210
|
this.cleanupStreaming();
|
|
197
|
-
this.
|
|
198
|
-
this.
|
|
211
|
+
this.stopAllSub?.unsubscribe();
|
|
212
|
+
this.stopAllSub = undefined;
|
|
213
|
+
this.preemptSub?.unsubscribe();
|
|
214
|
+
this.preemptSub = undefined;
|
|
199
215
|
|
|
200
216
|
const audio = this.audioRef?.nativeElement;
|
|
201
217
|
if (audio) {
|
|
@@ -219,34 +235,29 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
219
235
|
}
|
|
220
236
|
}
|
|
221
237
|
|
|
222
|
-
private
|
|
223
|
-
this.
|
|
224
|
-
this.cleanupStreaming();
|
|
238
|
+
private startPlayback(audio: HTMLAudioElement): void {
|
|
239
|
+
const messageSrc = (this.message as any)?.metadata?.src as string | undefined;
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
audio
|
|
231
|
-
|
|
232
|
-
/* ignore */
|
|
241
|
+
if (this.message?.type === 'tts') {
|
|
242
|
+
const streamEndpoint = this.voiceService.proxyTtsStreamUrl;
|
|
243
|
+
const fullFileEndpoint = this.voiceService.proxyTtsUrl;
|
|
244
|
+
if (streamEndpoint) {
|
|
245
|
+
this.startStreamingFromEndpoint(audio, streamEndpoint, fullFileEndpoint, messageSrc);
|
|
246
|
+
return;
|
|
233
247
|
}
|
|
248
|
+
if (fullFileEndpoint) {
|
|
249
|
+
this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (messageSrc) {
|
|
253
|
+
this.playDirectUrl(audio, messageSrc);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.handlePlaybackError();
|
|
257
|
+
return;
|
|
234
258
|
}
|
|
235
259
|
|
|
236
|
-
|
|
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
|
-
|
|
247
|
-
private startPlayback(audio: HTMLAudioElement): void {
|
|
248
|
-
const src = (this.message as any)?.metadata?.src as string | undefined;
|
|
249
|
-
if (!src) {
|
|
260
|
+
if (!messageSrc) {
|
|
250
261
|
this.playbackStarted = false;
|
|
251
262
|
this.ttsPlayback.releaseIfCurrent(this.playbackOwnerId);
|
|
252
263
|
this.markAllWordsPast();
|
|
@@ -257,11 +268,10 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
257
268
|
return;
|
|
258
269
|
}
|
|
259
270
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
271
|
+
this.playDirectUrl(audio, messageSrc);
|
|
272
|
+
}
|
|
264
273
|
|
|
274
|
+
private playDirectUrl(audio: HTMLAudioElement, src: string): void {
|
|
265
275
|
audio.src = src;
|
|
266
276
|
try {
|
|
267
277
|
audio.currentTime = 0;
|
|
@@ -271,16 +281,40 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
271
281
|
audio.play().catch(() => this.handlePlaybackError());
|
|
272
282
|
}
|
|
273
283
|
|
|
274
|
-
private startStreamingFromEndpoint(
|
|
284
|
+
private startStreamingFromEndpoint(
|
|
285
|
+
audio: HTMLAudioElement,
|
|
286
|
+
endpoint: string,
|
|
287
|
+
fullFileEndpoint?: string | null,
|
|
288
|
+
directFallbackSrc?: string,
|
|
289
|
+
): void {
|
|
275
290
|
this.cleanupStreaming();
|
|
276
291
|
|
|
277
292
|
const jwt = this.getJwtToken();
|
|
278
293
|
const voiceSettings = this.getVoiceSettingsBody();
|
|
279
294
|
const requestBody = this.buildTtsRequestBody(voiceSettings);
|
|
295
|
+
let fallbackUsed = false;
|
|
296
|
+
const fallback = () => {
|
|
297
|
+
if (fallbackUsed) {
|
|
298
|
+
this.handlePlaybackError();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
fallbackUsed = true;
|
|
302
|
+
this.cleanupStreaming();
|
|
303
|
+
if (fullFileEndpoint) {
|
|
304
|
+
this.fetchFullFileFromEndpoint(audio, fullFileEndpoint);
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (directFallbackSrc) {
|
|
308
|
+
this.playDirectUrl(audio, directFallbackSrc);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
this.handlePlaybackError();
|
|
312
|
+
};
|
|
313
|
+
|
|
280
314
|
// <audio src="..."> non può inviare header/body: serve fetch().
|
|
281
315
|
const hasMse = typeof (window as any).MediaSource !== 'undefined';
|
|
282
316
|
if (!hasMse) {
|
|
283
|
-
|
|
317
|
+
fallback();
|
|
284
318
|
return;
|
|
285
319
|
}
|
|
286
320
|
|
|
@@ -298,10 +332,8 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
298
332
|
try {
|
|
299
333
|
const headers: Record<string, string> = {
|
|
300
334
|
'Content-Type': 'application/json',
|
|
335
|
+
'Authorization': `${jwt}`
|
|
301
336
|
};
|
|
302
|
-
if (jwt) {
|
|
303
|
-
headers['Authorization'] = jwt;
|
|
304
|
-
}
|
|
305
337
|
|
|
306
338
|
const response = await fetch(endpoint, {
|
|
307
339
|
method: 'POST',
|
|
@@ -314,14 +346,15 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
314
346
|
}
|
|
315
347
|
|
|
316
348
|
const headerType = (response.headers.get('content-type') || '').split(';')[0].trim();
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
349
|
+
if (headerType && !MediaSourceCtor.isTypeSupported(headerType)) {
|
|
350
|
+
// Fallback: fetch completo e play via blob (no streaming).
|
|
351
|
+
fallback();
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
320
354
|
|
|
355
|
+
const mime = headerType || 'audio/mpeg';
|
|
321
356
|
if (!MediaSourceCtor.isTypeSupported(mime)) {
|
|
322
|
-
|
|
323
|
-
// Fallback: fetch completo e play via blob (no streaming).
|
|
324
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
357
|
+
fallback();
|
|
325
358
|
return;
|
|
326
359
|
}
|
|
327
360
|
|
|
@@ -362,15 +395,14 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
362
395
|
) as ArrayBuffer;
|
|
363
396
|
sourceBuffer.appendBuffer(ab);
|
|
364
397
|
} catch {
|
|
365
|
-
|
|
366
|
-
this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
398
|
+
fallback();
|
|
367
399
|
}
|
|
368
400
|
};
|
|
369
401
|
|
|
370
402
|
sourceBuffer.addEventListener('updateend', () => {
|
|
371
403
|
if (!started && this.playbackStarted && !this.destroyed) {
|
|
372
404
|
started = true;
|
|
373
|
-
audio.play().catch(() =>
|
|
405
|
+
audio.play().catch(() => fallback());
|
|
374
406
|
}
|
|
375
407
|
pump();
|
|
376
408
|
});
|
|
@@ -394,7 +426,7 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
394
426
|
tryEndOfStream();
|
|
395
427
|
} catch {
|
|
396
428
|
if (!abort.signal.aborted) {
|
|
397
|
-
|
|
429
|
+
fallback();
|
|
398
430
|
}
|
|
399
431
|
}
|
|
400
432
|
};
|
|
@@ -463,10 +495,8 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
463
495
|
try {
|
|
464
496
|
const headers: Record<string, string> = {
|
|
465
497
|
'Content-Type': 'application/json',
|
|
498
|
+
'Authorization': `${jwt}`
|
|
466
499
|
};
|
|
467
|
-
if (jwt) {
|
|
468
|
-
headers['Authorization'] = jwt;
|
|
469
|
-
}
|
|
470
500
|
|
|
471
501
|
const response = await fetch(endpoint, {
|
|
472
502
|
method: 'POST',
|
|
@@ -493,16 +523,33 @@ export class AudioSyncComponent implements AfterViewInit, OnChanges, OnDestroy {
|
|
|
493
523
|
}
|
|
494
524
|
}
|
|
495
525
|
|
|
496
|
-
private
|
|
526
|
+
private fetchFullFileFromEndpoint(audio: HTMLAudioElement, endpoint: string): void {
|
|
527
|
+
const jwt = this.getJwtToken();
|
|
528
|
+
const voiceSettings = this.getVoiceSettingsBody();
|
|
529
|
+
const requestBody = this.buildTtsRequestBody(voiceSettings, false);
|
|
530
|
+
void this.fetchAsBlobAndPlay(audio, endpoint, jwt, requestBody);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
private buildTtsRequestBody(voiceSettings: unknown, streaming = true): unknown {
|
|
497
534
|
const text = this.message?.text ?? '';
|
|
498
535
|
if (
|
|
499
536
|
voiceSettings &&
|
|
500
537
|
typeof voiceSettings === 'object' &&
|
|
501
538
|
!Array.isArray(voiceSettings)
|
|
502
539
|
) {
|
|
503
|
-
return {
|
|
540
|
+
return {
|
|
541
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
542
|
+
...(voiceSettings as Record<string, unknown>),
|
|
543
|
+
text,
|
|
544
|
+
streaming,
|
|
545
|
+
};
|
|
504
546
|
}
|
|
505
|
-
return {
|
|
547
|
+
return {
|
|
548
|
+
voiceSettings,
|
|
549
|
+
text,
|
|
550
|
+
streaming,
|
|
551
|
+
outputFormat: BROWSER_TTS_OUTPUT_FORMAT,
|
|
552
|
+
};
|
|
506
553
|
}
|
|
507
554
|
|
|
508
555
|
private markAllWordsPast(): void {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<div class="c21-icon-avatar">
|
|
2
2
|
<div class="c21-avatar-image profile_image">
|
|
3
3
|
<!-- is a BOT -->
|
|
4
|
-
<img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
|
|
4
|
+
<img *ngIf="senderID?.indexOf('bot_') !== -1 || senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'Bot'" role="img" (error)="onBotImgError($event)" (load)="onLoadedBot($event)"/>
|
|
5
5
|
<!-- is a HUMAN -->
|
|
6
|
-
<img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
|
|
6
|
+
<img *ngIf="senderID?.indexOf('bot_') === -1 && !senderFullname.toLowerCase().includes('bot')" [src]="url" [attr.alt]="senderFullname || 'User'" role="img" (error)="onHumanImgError($event)" (load)="onLoadedHuman($event)"/>
|
|
7
7
|
</div>
|
|
8
8
|
</div>
|
|
9
9
|
|