// Utility to handle Base64 conversion export const blobToBase64 = (blob: Blob): Promise => { 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 => { 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(); };