openvoiceui 1.0.0
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/.env.example +104 -0
- package/Dockerfile +30 -0
- package/LICENSE +21 -0
- package/README.md +638 -0
- package/SETUP.md +360 -0
- package/app.py +232 -0
- package/auto-approve-devices.js +111 -0
- package/cli/index.js +372 -0
- package/config/__init__.py +4 -0
- package/config/default.yaml +43 -0
- package/config/flags.yaml +67 -0
- package/config/loader.py +203 -0
- package/config/providers.yaml +71 -0
- package/config/speech_normalization.yaml +182 -0
- package/config/theme.json +4 -0
- package/data/greetings.json +25 -0
- package/default-pages/ai-image-creator.html +915 -0
- package/default-pages/bulk-image-uploader.html +492 -0
- package/default-pages/desktop.html +2865 -0
- package/default-pages/file-explorer.html +854 -0
- package/default-pages/interactive-map.html +655 -0
- package/default-pages/style-guide.html +1005 -0
- package/default-pages/website-setup.html +1623 -0
- package/deploy/openclaw/Dockerfile +46 -0
- package/deploy/openvoiceui.service +30 -0
- package/deploy/setup-nginx.sh +50 -0
- package/deploy/setup-sudo.sh +306 -0
- package/deploy/skill-runner/Dockerfile +19 -0
- package/deploy/skill-runner/requirements.txt +14 -0
- package/deploy/skill-runner/server.py +269 -0
- package/deploy/supertonic/Dockerfile +22 -0
- package/deploy/supertonic/server.py +79 -0
- package/docker-compose.pinokio.yml +11 -0
- package/docker-compose.yml +59 -0
- package/greetings.json +25 -0
- package/index.html +65 -0
- package/inject-device-identity.js +142 -0
- package/package.json +82 -0
- package/profiles/default.json +114 -0
- package/profiles/manager.py +354 -0
- package/profiles/schema.json +337 -0
- package/prompts/voice-system-prompt.md +149 -0
- package/providers/__init__.py +39 -0
- package/providers/base.py +63 -0
- package/providers/llm/__init__.py +12 -0
- package/providers/llm/base.py +71 -0
- package/providers/llm/clawdbot_provider.py +112 -0
- package/providers/llm/zai_provider.py +115 -0
- package/providers/registry.py +320 -0
- package/providers/stt/__init__.py +12 -0
- package/providers/stt/base.py +58 -0
- package/providers/stt/webspeech_provider.py +49 -0
- package/providers/stt/whisper_provider.py +100 -0
- package/providers/tts/__init__.py +20 -0
- package/providers/tts/base.py +91 -0
- package/providers/tts/groq_provider.py +74 -0
- package/providers/tts/supertonic_provider.py +72 -0
- package/requirements.txt +38 -0
- package/routes/__init__.py +10 -0
- package/routes/admin.py +515 -0
- package/routes/canvas.py +1315 -0
- package/routes/chat.py +51 -0
- package/routes/conversation.py +2158 -0
- package/routes/elevenlabs_hybrid.py +306 -0
- package/routes/greetings.py +98 -0
- package/routes/icons.py +279 -0
- package/routes/image_gen.py +364 -0
- package/routes/instructions.py +190 -0
- package/routes/music.py +838 -0
- package/routes/onboarding.py +43 -0
- package/routes/pi.py +62 -0
- package/routes/profiles.py +215 -0
- package/routes/report_issue.py +68 -0
- package/routes/static_files.py +533 -0
- package/routes/suno.py +664 -0
- package/routes/theme.py +81 -0
- package/routes/transcripts.py +199 -0
- package/routes/vision.py +348 -0
- package/routes/workspace.py +288 -0
- package/server.py +1510 -0
- package/services/__init__.py +1 -0
- package/services/auth.py +143 -0
- package/services/canvas_versioning.py +239 -0
- package/services/db_pool.py +107 -0
- package/services/gateway.py +16 -0
- package/services/gateway_manager.py +333 -0
- package/services/gateways/__init__.py +12 -0
- package/services/gateways/base.py +110 -0
- package/services/gateways/compat.py +264 -0
- package/services/gateways/openclaw.py +1134 -0
- package/services/health.py +100 -0
- package/services/memory_client.py +455 -0
- package/services/paths.py +26 -0
- package/services/speech_normalizer.py +285 -0
- package/services/tts.py +270 -0
- package/setup-config.js +262 -0
- package/sounds/air_horn.mp3 +0 -0
- package/sounds/bruh.mp3 +0 -0
- package/sounds/crowd_cheer.mp3 +0 -0
- package/sounds/gunshot.mp3 +0 -0
- package/sounds/impact.mp3 +0 -0
- package/sounds/lets_go.mp3 +0 -0
- package/sounds/record_stop.mp3 +0 -0
- package/sounds/rewind.mp3 +0 -0
- package/sounds/sad_trombone.mp3 +0 -0
- package/sounds/scratch_long.mp3 +0 -0
- package/sounds/yeah.mp3 +0 -0
- package/src/adapters/ClawdBotAdapter.js +264 -0
- package/src/adapters/_template.js +133 -0
- package/src/adapters/elevenlabs-classic.js +841 -0
- package/src/adapters/elevenlabs-hybrid.js +812 -0
- package/src/adapters/hume-evi.js +676 -0
- package/src/admin.html +1339 -0
- package/src/app.js +8802 -0
- package/src/core/Config.js +173 -0
- package/src/core/EmotionEngine.js +307 -0
- package/src/core/EventBridge.js +180 -0
- package/src/core/EventBus.js +117 -0
- package/src/core/VoiceSession.js +607 -0
- package/src/face/BaseFace.js +259 -0
- package/src/face/EyeFace.js +208 -0
- package/src/face/HaloSmokeFace.js +509 -0
- package/src/face/manifest.json +27 -0
- package/src/face/previews/eyes.svg +16 -0
- package/src/face/previews/orb.svg +29 -0
- package/src/features/MusicPlayer.js +620 -0
- package/src/features/Soundboard.js +128 -0
- package/src/providers/DeepgramSTT.js +472 -0
- package/src/providers/DeepgramStreamingSTT.js +766 -0
- package/src/providers/GroqSTT.js +559 -0
- package/src/providers/TTSPlayer.js +323 -0
- package/src/providers/WebSpeechSTT.js +479 -0
- package/src/providers/tts/BaseTTSProvider.js +81 -0
- package/src/providers/tts/HumeProvider.js +77 -0
- package/src/providers/tts/SupertonicProvider.js +174 -0
- package/src/providers/tts/index.js +140 -0
- package/src/shell/adapter-registry.js +154 -0
- package/src/shell/caller-bridge.js +35 -0
- package/src/shell/camera-bridge.js +28 -0
- package/src/shell/canvas-bridge.js +32 -0
- package/src/shell/commercial-bridge.js +44 -0
- package/src/shell/face-bridge.js +44 -0
- package/src/shell/music-bridge.js +60 -0
- package/src/shell/orchestrator.js +233 -0
- package/src/shell/profile-discovery.js +303 -0
- package/src/shell/sounds-bridge.js +28 -0
- package/src/shell/transcript-bridge.js +61 -0
- package/src/shell/waveform-bridge.js +33 -0
- package/src/styles/base.css +2862 -0
- package/src/styles/face.css +417 -0
- package/src/styles/pi-overrides.css +89 -0
- package/src/styles/theme-dark.css +67 -0
- package/src/test-tts.html +175 -0
- package/src/ui/AppShell.js +544 -0
- package/src/ui/ProfileSwitcher.js +228 -0
- package/src/ui/SessionControl.js +240 -0
- package/src/ui/face/FacePicker.js +195 -0
- package/src/ui/face/FaceRenderer.js +309 -0
- package/src/ui/settings/PlaylistEditor.js +366 -0
- package/src/ui/settings/SettingsPanel.css +684 -0
- package/src/ui/settings/SettingsPanel.js +419 -0
- package/src/ui/settings/TTSVoicePreview.js +210 -0
- package/src/ui/themes/ThemeManager.js +213 -0
- package/src/ui/visualizers/BaseVisualizer.js +29 -0
- package/src/ui/visualizers/PartyFXVisualizer.css +291 -0
- package/src/ui/visualizers/PartyFXVisualizer.js +637 -0
- package/static/emulators/jsdos/js-dos.css +1 -0
- package/static/emulators/jsdos/js-dos.js +22 -0
- package/static/favicon.svg +55 -0
- package/static/icons/apple-touch-icon.png +0 -0
- package/static/icons/favicon-32.png +0 -0
- package/static/icons/icon-192.png +0 -0
- package/static/icons/icon-512.png +0 -0
- package/static/install.html +449 -0
- package/static/manifest.json +26 -0
- package/static/sw.js +21 -0
- package/tts_providers/__init__.py +136 -0
- package/tts_providers/base_provider.py +319 -0
- package/tts_providers/groq_provider.py +155 -0
- package/tts_providers/hume_provider.py +226 -0
- package/tts_providers/providers_config.json +119 -0
- package/tts_providers/qwen3_provider.py +371 -0
- package/tts_providers/resemble_provider.py +315 -0
- package/tts_providers/supertonic_provider.py +557 -0
- package/tts_providers/supertonic_tts.py +399 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TTSPlayer — Frontend TTS audio playback with waveform analysis
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.html (VoiceConversation.playTTS, ClawbotMode.playAudio,
|
|
5
|
+
* startAnalyserAnimation, stopAnalyserAnimation, base64ToArrayBuffer/Blob helpers).
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { TTSPlayer } from './providers/TTSPlayer.js';
|
|
9
|
+
* const player = new TTSPlayer();
|
|
10
|
+
* await player.init();
|
|
11
|
+
* player.onAmplitude = (value) => waveformModule.setAmplitude(value);
|
|
12
|
+
* player.onSpeakingChange = (isSpeaking) => { ... };
|
|
13
|
+
* await player.play(base64Audio); // AudioContext path
|
|
14
|
+
* player.queue(base64Audio); // Queue path (ClawbotMode style)
|
|
15
|
+
* player.stop();
|
|
16
|
+
*/
|
|
17
|
+
export class TTSPlayer {
|
|
18
|
+
constructor() {
|
|
19
|
+
// AudioContext-based path (used by VoiceConversation / direct TTS)
|
|
20
|
+
this.audioContext = null;
|
|
21
|
+
this.gainNode = null;
|
|
22
|
+
this.analyser = null;
|
|
23
|
+
this.analyserData = null;
|
|
24
|
+
this.analyserAnimationId = null;
|
|
25
|
+
this.currentSource = null; // BufferSourceNode
|
|
26
|
+
|
|
27
|
+
// Queue-based path (used by ClawbotMode for streamed chunks)
|
|
28
|
+
this.audioQueue = [];
|
|
29
|
+
this.currentAudio = null; // HTMLAudioElement
|
|
30
|
+
this.isPlaying = false;
|
|
31
|
+
|
|
32
|
+
// Volume boost — iOS Safari web audio is quieter than native apps.
|
|
33
|
+
// GainNode values > 1.0 amplify the signal. Default 1.5x on mobile, 1.0x desktop.
|
|
34
|
+
this._isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
35
|
+
this.gain = this._isMobile ? 1.8 : 1.0;
|
|
36
|
+
|
|
37
|
+
// Callbacks
|
|
38
|
+
this.onAmplitude = null; // (value: 0-1) => void
|
|
39
|
+
this.onSpeakingChange = null; // (isSpeaking: boolean) => void
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Initialize AudioContext and analyser.
|
|
44
|
+
* Must be called after a user gesture on some browsers.
|
|
45
|
+
*/
|
|
46
|
+
async init() {
|
|
47
|
+
if (this.audioContext) return;
|
|
48
|
+
|
|
49
|
+
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
50
|
+
|
|
51
|
+
// GainNode for volume boost (especially on mobile where web audio is quieter)
|
|
52
|
+
this.gainNode = this.audioContext.createGain();
|
|
53
|
+
this.gainNode.gain.value = this.gain;
|
|
54
|
+
|
|
55
|
+
this.analyser = this.audioContext.createAnalyser();
|
|
56
|
+
this.analyser.fftSize = 256;
|
|
57
|
+
this.analyser.smoothingTimeConstant = 0.3;
|
|
58
|
+
this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
|
|
59
|
+
|
|
60
|
+
// Chain: source → gainNode → analyser → destination
|
|
61
|
+
this.gainNode.connect(this.analyser);
|
|
62
|
+
this.analyser.connect(this.audioContext.destination);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Set TTS volume gain (1.0 = normal, 2.0 = 2x boost).
|
|
67
|
+
* Values above 1.0 amplify the signal for quieter devices.
|
|
68
|
+
*/
|
|
69
|
+
setGain(value) {
|
|
70
|
+
this.gain = Math.max(0, Math.min(3.0, value));
|
|
71
|
+
if (this.gainNode) {
|
|
72
|
+
this.gainNode.gain.value = this.gain;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ensure AudioContext is resumed (needed after iOS/browser auto-suspend).
|
|
78
|
+
*/
|
|
79
|
+
async ensureRunning() {
|
|
80
|
+
if (!this.audioContext) await this.init();
|
|
81
|
+
if (this.audioContext.state === 'suspended') {
|
|
82
|
+
await this.audioContext.resume();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// -------------------------------------------------------------------------
|
|
87
|
+
// AudioContext path — play base64 audio directly via decodeAudioData
|
|
88
|
+
// -------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Decode and play a base64-encoded WAV/MP3 via AudioContext.
|
|
92
|
+
* Drives the waveform analyser animation while playing.
|
|
93
|
+
* @param {string} audioBase64
|
|
94
|
+
* @returns {Promise<void>} Resolves when playback ends
|
|
95
|
+
*/
|
|
96
|
+
async play(audioBase64) {
|
|
97
|
+
await this.ensureRunning();
|
|
98
|
+
this._notifySpeaking(true);
|
|
99
|
+
|
|
100
|
+
return new Promise((resolve) => {
|
|
101
|
+
try {
|
|
102
|
+
const arrayBuffer = this._base64ToArrayBuffer(audioBase64);
|
|
103
|
+
|
|
104
|
+
this.audioContext.decodeAudioData(arrayBuffer, (audioBuffer) => {
|
|
105
|
+
// Stop previous source if any
|
|
106
|
+
if (this.currentSource) {
|
|
107
|
+
try { this.currentSource.stop(); } catch (_) {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const source = this.audioContext.createBufferSource();
|
|
111
|
+
source.buffer = audioBuffer;
|
|
112
|
+
source.connect(this.gainNode);
|
|
113
|
+
|
|
114
|
+
source.onended = () => {
|
|
115
|
+
this._stopAnalyserAnimation();
|
|
116
|
+
if (this.onAmplitude) this.onAmplitude(0);
|
|
117
|
+
this._notifySpeaking(false);
|
|
118
|
+
this.currentSource = null;
|
|
119
|
+
resolve();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
this.currentSource = source;
|
|
123
|
+
source.start(0);
|
|
124
|
+
this._startAnalyserAnimation();
|
|
125
|
+
|
|
126
|
+
}, (err) => {
|
|
127
|
+
console.error('[TTSPlayer] decodeAudioData failed:', err);
|
|
128
|
+
this._notifySpeaking(false);
|
|
129
|
+
resolve();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('[TTSPlayer] play() failed:', error);
|
|
134
|
+
this._notifySpeaking(false);
|
|
135
|
+
resolve();
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// -------------------------------------------------------------------------
|
|
141
|
+
// Queue path — for streaming/chunked audio (HTMLAudioElement-based)
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Add a base64-encoded audio chunk to the queue and start playing if idle.
|
|
146
|
+
* @param {string} base64Audio
|
|
147
|
+
* @param {string} [mimeType='audio/wav']
|
|
148
|
+
*/
|
|
149
|
+
queue(base64Audio, mimeType = 'audio/wav') {
|
|
150
|
+
try {
|
|
151
|
+
const blob = this._base64ToBlob(base64Audio, mimeType);
|
|
152
|
+
const url = URL.createObjectURL(blob);
|
|
153
|
+
const audio = new Audio(url);
|
|
154
|
+
|
|
155
|
+
audio.onended = () => {
|
|
156
|
+
URL.revokeObjectURL(url);
|
|
157
|
+
this._playNext();
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
audio.onerror = (e) => {
|
|
161
|
+
console.error('[TTSPlayer] Audio element error:', e);
|
|
162
|
+
URL.revokeObjectURL(url);
|
|
163
|
+
this._playNext();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
this.audioQueue.push(audio);
|
|
167
|
+
|
|
168
|
+
if (!this.isPlaying) {
|
|
169
|
+
this._playNext();
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('[TTSPlayer] queue() failed:', error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
_playNext() {
|
|
177
|
+
if (this.audioQueue.length === 0) {
|
|
178
|
+
this.currentAudio = null;
|
|
179
|
+
this.isPlaying = false;
|
|
180
|
+
if (this.onAmplitude) this.onAmplitude(0);
|
|
181
|
+
this._notifySpeaking(false);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.currentAudio = this.audioQueue.shift();
|
|
186
|
+
this.isPlaying = true;
|
|
187
|
+
this._notifySpeaking(true);
|
|
188
|
+
|
|
189
|
+
// Route HTMLAudioElement through AudioContext gain for volume boost
|
|
190
|
+
if (this.gainNode && this.audioContext) {
|
|
191
|
+
try {
|
|
192
|
+
if (!this.currentAudio._mediaSource) {
|
|
193
|
+
this.currentAudio._mediaSource = this.audioContext.createMediaElementSource(this.currentAudio);
|
|
194
|
+
this.currentAudio._mediaSource.connect(this.gainNode);
|
|
195
|
+
}
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Fallback: if AudioContext routing fails, just play directly
|
|
198
|
+
console.warn('[TTSPlayer] MediaElementSource fallback:', e.message);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const promise = this.currentAudio.play();
|
|
203
|
+
if (promise) {
|
|
204
|
+
promise.catch(err => {
|
|
205
|
+
console.error('[TTSPlayer] Audio play blocked:', err.message);
|
|
206
|
+
this._playNext();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// -------------------------------------------------------------------------
|
|
212
|
+
// Stop / clear
|
|
213
|
+
// -------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stop all current and queued audio.
|
|
217
|
+
*/
|
|
218
|
+
stop() {
|
|
219
|
+
// Stop AudioContext source
|
|
220
|
+
if (this.currentSource) {
|
|
221
|
+
try { this.currentSource.stop(); } catch (_) {}
|
|
222
|
+
this.currentSource = null;
|
|
223
|
+
}
|
|
224
|
+
this._stopAnalyserAnimation();
|
|
225
|
+
|
|
226
|
+
// Stop queue-based audio
|
|
227
|
+
if (this.currentAudio) {
|
|
228
|
+
this.currentAudio.pause();
|
|
229
|
+
this.currentAudio = null;
|
|
230
|
+
}
|
|
231
|
+
this.audioQueue = [];
|
|
232
|
+
this.isPlaying = false;
|
|
233
|
+
|
|
234
|
+
if (this.onAmplitude) this.onAmplitude(0);
|
|
235
|
+
this._notifySpeaking(false);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Release AudioContext resources.
|
|
240
|
+
*/
|
|
241
|
+
destroy() {
|
|
242
|
+
this.stop();
|
|
243
|
+
if (this.audioContext) {
|
|
244
|
+
this.audioContext.close().catch(() => {});
|
|
245
|
+
this.audioContext = null;
|
|
246
|
+
this.analyser = null;
|
|
247
|
+
this.analyserData = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// -------------------------------------------------------------------------
|
|
252
|
+
// Waveform analyser animation
|
|
253
|
+
// -------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
_startAnalyserAnimation() {
|
|
256
|
+
if (this.analyserAnimationId) return;
|
|
257
|
+
|
|
258
|
+
const tick = () => {
|
|
259
|
+
if (!this.analyser) {
|
|
260
|
+
this.analyserAnimationId = null;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.analyser.getByteFrequencyData(this.analyserData);
|
|
265
|
+
|
|
266
|
+
// Average voice-range frequencies (lower 60% of bins)
|
|
267
|
+
const voiceRange = Math.floor(this.analyserData.length * 0.6);
|
|
268
|
+
let sum = 0;
|
|
269
|
+
for (let i = 0; i < voiceRange; i++) {
|
|
270
|
+
sum += this.analyserData[i];
|
|
271
|
+
}
|
|
272
|
+
const average = sum / voiceRange;
|
|
273
|
+
const normalized = average / 255;
|
|
274
|
+
|
|
275
|
+
// Boost so the mouth animation is visibly active
|
|
276
|
+
const boosted = normalized > 0.05
|
|
277
|
+
? Math.max(0.3, normalized * 2.5)
|
|
278
|
+
: 0;
|
|
279
|
+
|
|
280
|
+
if (this.onAmplitude) this.onAmplitude(Math.min(1, boosted));
|
|
281
|
+
|
|
282
|
+
this.analyserAnimationId = requestAnimationFrame(tick);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
tick();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
_stopAnalyserAnimation() {
|
|
289
|
+
if (this.analyserAnimationId) {
|
|
290
|
+
cancelAnimationFrame(this.analyserAnimationId);
|
|
291
|
+
this.analyserAnimationId = null;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// -------------------------------------------------------------------------
|
|
296
|
+
// Helpers
|
|
297
|
+
// -------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
_notifySpeaking(isSpeaking) {
|
|
300
|
+
if (this.onSpeakingChange) this.onSpeakingChange(isSpeaking);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_base64ToArrayBuffer(base64) {
|
|
304
|
+
const binary = atob(base64);
|
|
305
|
+
const bytes = new Uint8Array(binary.length);
|
|
306
|
+
for (let i = 0; i < binary.length; i++) {
|
|
307
|
+
bytes[i] = binary.charCodeAt(i);
|
|
308
|
+
}
|
|
309
|
+
return bytes.buffer;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
_base64ToBlob(base64, mimeType) {
|
|
313
|
+
const byteCharacters = atob(base64);
|
|
314
|
+
const byteNumbers = new Array(byteCharacters.length);
|
|
315
|
+
for (let i = 0; i < byteCharacters.length; i++) {
|
|
316
|
+
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
|
317
|
+
}
|
|
318
|
+
const byteArray = new Uint8Array(byteNumbers);
|
|
319
|
+
return new Blob([byteArray], { type: mimeType });
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export default TTSPlayer;
|