Spaces:
Sleeping
Sleeping
| // Utility to handle Base64 conversion | |
| export const blobToBase64 = (blob: Blob): Promise<string> => { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onloadend = () => { | |
| if (typeof reader.result === 'string') { | |
| // Remove data URL prefix | |
| const base64 = reader.result.split(',')[1]; | |
| resolve(base64); | |
| } else { | |
| reject(new Error('Failed to convert blob to base64')); | |
| } | |
| }; | |
| reader.onerror = reject; | |
| reader.readAsDataURL(blob); | |
| }); | |
| }; | |
| // Optimized Image Compression Utility | |
| export const compressImage = (file: File, maxWidth = 1600, quality = 0.7): Promise<string> => { | |
| return new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.readAsDataURL(file); | |
| reader.onload = (event) => { | |
| const img = new Image(); | |
| img.src = event.target?.result as string; | |
| img.onload = () => { | |
| const canvas = document.createElement('canvas'); | |
| let width = img.width; | |
| let height = img.height; | |
| if (width > maxWidth) { | |
| height = Math.round((height * maxWidth) / width); | |
| width = maxWidth; | |
| } | |
| canvas.width = width; | |
| canvas.height = height; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) { | |
| reject(new Error('Canvas context failed')); | |
| return; | |
| } | |
| ctx.drawImage(img, 0, 0, width, height); | |
| // Output as JPEG with quality reduction | |
| const dataUrl = canvas.toDataURL('image/jpeg', quality); | |
| // Remove prefix to get raw base64 | |
| const base64 = dataUrl.split(',')[1]; | |
| resolve(base64); | |
| }; | |
| img.onerror = (err) => reject(err); | |
| }; | |
| reader.onerror = (err) => reject(err); | |
| }); | |
| }; | |
| // --- RAW PCM Decoding Logic --- | |
| export const base64ToUint8Array = (base64: string) => { | |
| const binaryString = window.atob(base64); | |
| const len = binaryString.length; | |
| const bytes = new Uint8Array(len); | |
| for (let i = 0; i < len; i++) { | |
| bytes[i] = binaryString.charCodeAt(i); | |
| } | |
| return bytes; | |
| }; | |
| // Gemini TTS typically returns 24000Hz, single channel, Int16 PCM | |
| export const decodePCM = (data: Uint8Array, ctx: AudioContext) => { | |
| const sampleRate = 24000; | |
| const int16Data = new Int16Array(data.buffer); | |
| const float32Data = new Float32Array(int16Data.length); | |
| // Convert Int16 to Float32 [-1.0, 1.0] | |
| for (let i = 0; i < int16Data.length; i++) { | |
| float32Data[i] = int16Data[i] / 32768.0; | |
| } | |
| const buffer = ctx.createBuffer(1, float32Data.length, sampleRate); | |
| buffer.copyToChannel(float32Data, 0); | |
| return buffer; | |
| }; | |
| export const cleanTextForTTS = (text: string) => { | |
| let clean = text | |
| // 1. Remove Markdown headers, bold, italic markers, and blockquotes | |
| .replace(/([#*`~>_])/g, '') | |
| // 2. Remove links [text](url) -> text | |
| .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') | |
| // 4. Remove common Kaomoji patterns (Moved up) | |
| .replace(/\([^\u4e00-\u9fa5A-Za-z,\.,。]{1,8}\)/g, ''); | |
| // 3. Remove Emojis (Safe way for older Safari) | |
| try { | |
| // Use constructor to avoid parse-time SyntaxError if unsupported | |
| // 'v' flag is better but 'u' is more supported. | |
| const emojiRegex = new RegExp('\\p{Extended_Pictographic}', 'gu'); | |
| clean = clean.replace(emojiRegex, ''); | |
| } catch (e) { | |
| // Fallback for older browsers (approximate ranges) | |
| // This is a basic range for common emojis | |
| clean = clean.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}]/gu, ''); | |
| } | |
| // 5. Consolidate whitespace | |
| return clean.replace(/\s+/g, ' ').trim(); | |
| }; | |