@Bugs-Bunny בשמחה....
א
אריה AI
@אריה AI
-
VoiceMaster אתר קריינות משוכלל!!! -
VoiceMaster אתר קריינות משוכלל!!!לאחר זמן רב,
שישבתי יומם ולילה לחשוב איך כל אדם שצריך איזשהו קריינות, יוכל ליצור אותה בקלות.
לכן בניתי את האתר הזה
קודם כל יש להיכנס ל-Google AI Studio וליצור מפתח API חדש ולעלות לאתר למעלה בצד שמאל.
ואחרי זה תיצרו את הקריינות...או שתפתחו פנקס רשימות ואז תשמרו את זה על שם index.html ותפתחו אותו...
<!DOCTYPE html> <html lang="he" dir="rtl"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>VoiceMaster AI Studio ✨</title> <script src="https://cdn.tailwindcss.com"></script> <style> @import url('https://fonts.googleapis.com/css2?family=Assistant:wght@300;400;600;700;800&display=swap'); body { font-family: 'Assistant', sans-serif; background: #f1f5f9; transition: background-color 0.3s ease, color 0.3s ease; } .dark-mode { background: #020617; color: #f8fafc; } .card { background: white; border: 1px solid #e2e8f0; transition: all 0.3s ease; } .dark-mode .card { background: #1e293b; border-color: #334155; } .loading-spinner { border: 2px solid rgba(255,255,255,0.3); border-top: 2px solid currentColor; border-radius: 50%; width: 16px; height: 16px; animation: spin 0.8s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .ai-glow { box-shadow: 0 0 25px rgba(99, 102, 241, 0.15); } .feature-badge { background: linear-gradient(90deg, #4f46e5, #9333ea); color: white; padding: 2px 8px; border-radius: 99px; font-size: 10px; font-weight: bold; } /* התאמת רכיבי שמע למכשירים ניידים */ audio { max-width: 100%; height: 36px; } </style> </head> <body class="min-h-screen py-4 sm:py-8 px-3 sm:px-6 md:px-8 transition-colors duration-500" id="bodyTag"> <div class="max-w-7xl mx-auto"> <header class="flex flex-wrap justify-between items-center gap-4 mb-6 sm:mb-8 pb-4 border-b border-slate-200 dark:border-slate-800"> <div class="flex items-center gap-3 sm:gap-4"> <div class="bg-indigo-600 p-2.5 sm:p-3 rounded-2xl text-white shadow-xl flex-shrink-0"> <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 sm:h-8 sm:w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" /> </svg> </div> <div> <h1 class="text-2xl sm:text-3xl font-black text-slate-800 dark:text-white tracking-tight leading-none uppercase">Voice<span class="text-indigo-600">Master</span></h1> <p class="text-[10px] sm:text-xs font-bold text-slate-400 uppercase tracking-widest mt-1">AI Smart Narration Hub</p> </div> </div> <div class="flex items-center gap-2"> <div class="relative"> <button id="apiKeyBtn" class="p-2.5 sm:p-3 rounded-xl card shadow-sm hover:scale-105 transition-all flex items-center gap-2 text-xs font-bold text-slate-600 dark:text-slate-300"> <svg class="w-4 h-4 sm:w-5 sm:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/></svg> <span id="apiKeyStatus" class="hidden sm:inline">הגדר מפתח</span> </button> <div id="apiKeyPopover" class="hidden absolute left-0 sm:left-auto right-0 sm:right-0 mt-2 w-72 sm:w-80 p-4 card rounded-2xl shadow-2xl z-50"> <h4 class="font-bold text-sm mb-2 dark:text-white">Gemini API Key</h4> <input id="apiKeyInput" type="password" class="w-full p-2.5 text-xs bg-slate-50 dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 outline-none focus:ring-2 focus:ring-indigo-500 mb-2 dark:text-white" placeholder="AIzaSy..."> <div class="flex gap-2"> <button id="saveApiKey" class="flex-1 py-2 bg-indigo-600 text-white text-[11px] font-bold rounded-lg hover:bg-indigo-700 active:scale-95 transition-all">שמור מפתח</button> <button id="clearApiKey" class="flex-1 py-2 bg-red-50 text-red-600 text-[11px] font-bold rounded-lg hover:bg-red-100 active:scale-95 transition-all">מחק</button> </div> </div> </div> <button id="toggleDark" class="p-2.5 sm:p-3 rounded-xl card shadow-sm hover:scale-105 active:scale-95 transition-all"> <svg class="h-5 w-5 sm:h-6 sm:w-6 text-slate-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"/> </svg> </button> </div> </header> <div class="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8"> <div class="lg:col-span-5 space-y-6"> <div class="card p-4 sm:p-6 rounded-2xl sm:rounded-[2rem] shadow-lg ai-glow border-indigo-100 dark:border-indigo-900/30"> <div class="flex items-center justify-between mb-3 sm:mb-4"> <h3 class="font-bold text-sm sm:text-base text-indigo-900 dark:text-indigo-300">יצירת תוכן חכמה</h3> </div> <div class="flex gap-2 mb-3 sm:mb-4"> <input id="promptInput" type="text" class="flex-grow p-3 bg-slate-50 dark:bg-slate-800 rounded-xl text-xs sm:text-sm border-none outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white" placeholder="תאר מה לכתוב (למשל: פרסומת לשירות חדש)..."> <button id="magicWriteBtn" class="p-3 bg-indigo-600 text-white rounded-xl hover:bg-indigo-700 active:scale-95 transition-all flex items-center justify-center min-w-[48px] sm:min-w-[50px]"> <span id="magicIcon">✨</span> <div id="magicLoader" class="loading-spinner hidden"></div> </button> </div> <div class="grid grid-cols-2 sm:grid-cols-3 gap-2"> <button onclick="smartEdit('הפוך את הטקסט ליותר רגוע ונעים')" class="text-[11px] font-bold py-2 px-1 bg-slate-100 dark:bg-slate-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-950/40 dark:text-slate-200 active:scale-95 transition-all truncate">✨ ריכוך</button> <button onclick="smartEdit('הפוך את הטקסט ליותר שיווקי ואנרגטי')" class="text-[11px] font-bold py-2 px-1 bg-slate-100 dark:bg-slate-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-950/40 dark:text-slate-200 active:scale-95 transition-all truncate">✨ שיווקי</button> <button onclick="smartEdit('קצר את הטקסט ב-30% מבלי לאבד משמעות')" class="text-[11px] font-bold py-2 px-1 bg-slate-100 dark:bg-slate-800 rounded-lg hover:bg-indigo-50 dark:hover:bg-indigo-950/40 dark:text-slate-200 active:scale-95 transition-all truncate">✨ קיצור</button> <button onclick="translateText()" class="text-[11px] font-bold py-2 px-1 bg-indigo-50 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300 rounded-lg hover:bg-indigo-100 active:scale-95 transition-all truncate">✨ תרגם לאנגלית</button> <button onclick="generatePodcastScript()" class="text-[11px] font-bold py-2 px-1 bg-indigo-50 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300 rounded-lg hover:bg-indigo-100 active:scale-95 transition-all truncate">✨ תסריט פודקאסט</button> <button onclick="summarizeText()" class="text-[11px] font-bold py-2 px-1 bg-indigo-50 dark:bg-indigo-900/40 text-indigo-700 dark:text-indigo-300 rounded-lg hover:bg-indigo-100 active:scale-95 transition-all truncate">✨ סיכום נקודות</button> </div> </div> <div class="card p-4 sm:p-7 rounded-2xl sm:rounded-[2.5rem] shadow-xl border-none"> <div class="flex flex-wrap justify-between items-center gap-2 mb-2"> <label class="text-xs sm:text-sm font-bold text-slate-500 dark:text-slate-400">טקסט לקריינות</label> <button id="analyzeToneBtn" class="text-[10px] sm:text-[11px] font-bold text-indigo-600 bg-indigo-50 dark:bg-indigo-900/30 px-2.5 py-1 rounded-full hover:bg-indigo-100 active:scale-95 transition-all flex items-center gap-1"> <span>✨ התאם סגנון אוטומטית</span> <div id="toneLoader" class="loading-spinner hidden !w-3 !h-3"></div> </button> </div> <textarea id="textInput" rows="5" class="w-full p-3 sm:p-4 bg-slate-50 dark:bg-slate-800/50 rounded-xl sm:rounded-2xl outline-none border-2 border-transparent focus:border-indigo-500 transition-all text-base sm:text-lg dark:text-white resize-y" placeholder="הטקסט יופיע כאן..."></textarea> <div class="flex gap-2 mt-3 mb-5"> <button id="vocalizeBtn" class="flex-1 py-2.5 sm:py-3 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 rounded-xl text-xs font-bold hover:bg-indigo-50 dark:hover:bg-indigo-900/20 active:scale-95 transition-all flex items-center justify-center gap-2 border border-slate-100 dark:border-slate-700"> <span>✨ ניקוד אוטומטי</span> <div id="vocalizeLoader" class="loading-spinner hidden"></div> </button> </div> <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 sm:gap-4 mb-6"> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mr-1 block mb-1">קול</label> <select id="voiceSelect" class="w-full p-3 bg-slate-100 dark:bg-slate-800 dark:text-white rounded-xl text-xs sm:text-sm font-bold border-none outline-none cursor-pointer"> <optgroup label="גברים (Male)"> <option value="Zephyr">Zephyr (קליל)</option> <option value="Fenrir">Fenrir (עמוק)</option> <option value="Puck">Puck (שובב)</option> <option value="Charon">Charon (סמכותי)</option> <option value="Orus">Orus (קלאסי)</option> <option value="Enceladus">Enceladus (צעיר)</option> <option value="Iapetus">Iapetus (חם)</option> <option value="Algieba">Algieba (נמרץ)</option> <option value="Rasalgethi">Rasalgethi (בוגר)</option> <option value="Achernar">Achernar (יציב)</option> <option value="Alnilam">Alnilam (בהיר)</option> <option value="Schedar">Schedar (נעים)</option> <option value="Achird">Achird (שידור)</option> <option value="Zubenelgenubi">Zubenelgenubi (עמוק מאוד)</option> <option value="Sadachbia">Sadachbia (רך)</option> <option value="Sadaltager">Sadaltager (טבעי)</option> <option value="Sulafat">Sulafat (עוצמתי)</option> </optgroup> <optgroup label="נשים (Female)"> <option value="Kore" selected>Kore (מקצועי)</option> <option value="Leda">Leda (עדין)</option> <option value="Aoede">Aoede (רך)</option> <option value="Callirrhoe">Callirrhoe (רהוט)</option> <option value="Autonoe">Autonoe (אנרגטי)</option> <option value="Umbriel">Umbriel (מסתורי)</option> <option value="Despina">Despina (מהיר)</option> <option value="Erinome">Erinome (ידידותי)</option> <option value="Algenib">Algenib (חדשותי)</option> <option value="Laomedeia">Laomedeia (מרגיע)</option> <option value="Gacrux">Gacrux (אלגנטי)</option> <option value="Pulcherrima">Pulcherrima (עשיר)</option> <option value="Vindemiatrix">Vindemiatrix (סבלני)</option> </optgroup> </select> </div> <div> <label class="text-[10px] font-bold text-slate-400 uppercase mr-1 block mb-1">סגנון והגשה</label> <select id="styleSelect" class="w-full p-3 bg-slate-100 dark:bg-slate-800 dark:text-white rounded-xl text-xs sm:text-sm font-bold border-none outline-none cursor-pointer"> <option value="natural">טבעי (Natural)</option> <option value="cheerful">שמח (Cheerful)</option> <option value="serious">רציני (Serious)</option> <option value="whisper">לחישה (Whisper)</option> <option value="excited">נרגש (Excited)</option> <option value="calm">רגוע (Calm)</option> <option value="authoritative">סמכותי (Authoritative)</option> <option value="friendly">ידידותי (Friendly)</option> <option value="professional">מקצועי (Professional)</option> <option value="storytelling">מספר סיפורים (Storytelling)</option> <option value="shouting">צעקה (Shouting)</option> <option value="terrified">מבועת (Terrified)</option> <option value="sad">עצוב (Sad)</option> <option value="angry">כועס (Angry)</option> <option value="hopeful">מלא תקווה (Hopeful)</option> </select> </div> </div> <button id="generateBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 active:scale-[0.98] text-white font-bold py-3.5 sm:py-4 rounded-xl sm:rounded-2xl shadow-lg transition-all flex items-center justify-center gap-3"> <span id="btnText" class="text-sm sm:text-base">הפק קריינות עכשיו</span> <div id="btnLoader" class="loading-spinner hidden !border-t-white"></div> </button> </div> </div> <div class="lg:col-span-7"> <div class="flex justify-between items-center mb-4"> <h2 class="text-lg sm:text-xl font-bold dark:text-white">הקלטות אחרונות</h2> <button id="clearTracks" class="text-xs text-slate-400 hover:text-red-500 font-bold transition-colors">נקה הכל</button> </div> <div id="tracksContainer" class="space-y-3 sm:space-y-4"> <div id="noTracks" class="text-center py-16 sm:py-24 border-2 border-dashed border-slate-300 dark:border-slate-800 rounded-2xl sm:rounded-[2.5rem] text-slate-400 font-bold text-sm sm:text-base"> היסטוריית הקריינות שלך תישמר כאן </div> </div> </div> </div> </div> <div id="toast" class="fixed bottom-4 sm:bottom-8 left-4 sm:left-8 right-4 sm:right-auto px-4 sm:px-6 py-3 rounded-xl shadow-2xl opacity-0 transition-all pointer-events-none z-50 transform translate-y-4 flex items-center justify-center sm:justify-start gap-2 max-w-md"> <span id="toastIcon"></span> <span id="toastMessage" class="text-xs sm:text-sm font-semibold"></span> </div> <script> // Global State & Config let currentApiKey = localStorage.getItem('gemini_api_key') || ""; const modelText = "gemini-3.5-flash"; const modelAudio = "gemini-2.5-flash-preview-tts"; const els = { textInput: document.getElementById('textInput'), promptInput: document.getElementById('promptInput'), generateBtn: document.getElementById('generateBtn'), magicWriteBtn: document.getElementById('magicWriteBtn'), analyzeToneBtn: document.getElementById('analyzeToneBtn'), voiceSelect: document.getElementById('voiceSelect'), styleSelect: document.getElementById('styleSelect'), tracksContainer: document.getElementById('tracksContainer'), noTracks: document.getElementById('noTracks'), body: document.getElementById('bodyTag'), toast: document.getElementById('toast'), toastMessage: document.getElementById('toastMessage'), // API Key elements apiKeyBtn: document.getElementById('apiKeyBtn'), apiKeyPopover: document.getElementById('apiKeyPopover'), apiKeyInput: document.getElementById('apiKeyInput'), saveApiKey: document.getElementById('saveApiKey'), clearApiKey: document.getElementById('clearApiKey'), apiKeyStatus: document.getElementById('apiKeyStatus') }; // Initialize UI function updateApiStatus() { if (currentApiKey) { els.apiKeyStatus.innerText = "מפתח מוגדר ✅"; els.apiKeyStatus.classList.add('text-green-600'); els.apiKeyInput.value = currentApiKey; } else { els.apiKeyStatus.innerText = "הגדר מפתח ⚠️"; els.apiKeyStatus.classList.remove('text-green-600'); els.apiKeyInput.value = ""; } } updateApiStatus(); // API Key Popover Toggle els.apiKeyBtn.onclick = (e) => { e.stopPropagation(); els.apiKeyPopover.classList.toggle('hidden'); }; els.saveApiKey.onclick = () => { const key = els.apiKeyInput.value.trim(); if (key) { currentApiKey = key; localStorage.setItem('gemini_api_key', key); updateApiStatus(); els.apiKeyPopover.classList.add('hidden'); showToast("מפתח ה-API נשמר בהצלחה", "success"); } }; els.clearApiKey.onclick = () => { currentApiKey = ""; localStorage.removeItem('gemini_api_key'); updateApiStatus(); showToast("מפתח ה-API נמחק"); }; document.addEventListener('click', () => els.apiKeyPopover.classList.add('hidden')); els.apiKeyPopover.onclick = (e) => e.stopPropagation(); // Utility: API Caller with retry logic async function apiCall(model, payload, retries = 5) { if (!currentApiKey) { showToast("אנא הגדר מפתח API בתפריט למעלה"); els.apiKeyPopover.classList.remove('hidden'); throw new Error("Missing API Key"); } const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:${model.includes('tts') ? 'generateContent' : 'generateContent'}?key=${currentApiKey}`; for (let i = 0; i < retries; i++) { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); if (response.ok) return data; if (response.status === 429) { await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000)); } else if (response.status === 403 || response.status === 401) { showToast("מפתח ה-API אינו תקין"); throw new Error("Invalid API Key"); } else { throw new Error(data.error?.message || "Communication Error"); } } catch (e) { if (i === retries - 1) throw e; } } } function showToast(msg, type = 'error') { els.toastMessage.innerText = msg; els.toast.className = `fixed bottom-4 sm:bottom-8 left-4 sm:left-8 right-4 sm:right-auto px-4 sm:px-6 py-3 rounded-xl shadow-2xl transition-all z-50 transform translate-y-0 opacity-100 ${type === 'error' ? 'bg-red-600' : 'bg-indigo-600'} text-white max-w-md flex items-center justify-center sm:justify-start gap-2`; setTimeout(() => { els.toast.classList.add('opacity-0', 'translate-y-4'); }, 4000); } // --- LLM Features --- // ✨ Magic Write els.magicWriteBtn.onclick = async () => { const prompt = els.promptInput.value.trim(); if (!prompt) return showToast("תאר מה לכתוב בתיבה"); toggleLoader('magic', true); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `כתוב סקריפט קצר ומקצועי לקריינות בעברית עבור הנושא הבא: "${prompt}". החזר רק את הטקסט של הסקריפט ללא הסברים או כותרות.` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } toggleLoader('magic', false); }; // ✨ Smart Edit async function smartEdit(instruction) { const text = els.textInput.value.trim(); if (!text) return showToast("הכנס קודם טקסט לתיבה"); showToast("מעבד טקסט...", "success"); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `${instruction} על הטקסט הבא: "${text}". החזר רק את הטקסט המעובד.` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } } // ✨ Translate async function translateText() { const text = els.textInput.value.trim(); if (!text) return showToast("אין טקסט לתרגום"); showToast("מתרגם לאנגלית...", "success"); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `Translate the following text to fluent English suitable for narration: "${text}". Return only the translation.` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } } // ✨ Podcast Script async function generatePodcastScript() { const topic = els.promptInput.value.trim() || "Technology"; showToast("יוצר תסריט פודקאסט...", "success"); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `Create a short podcast script dialogue between two people about: "${topic}". Return only the dialogue in Hebrew.` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } } // ✨ Summarize async function summarizeText() { const text = els.textInput.value.trim(); if (!text) return showToast("אין טקסט לסיכום"); showToast("מסכם לנקודות...", "success"); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `Summarize the following text into 3 clear key points for narration in Hebrew: "${text}".` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } } // ✨ Tone Analysis els.analyzeToneBtn.onclick = async () => { const text = els.textInput.value.trim(); if (!text) return showToast("אין טקסט לניתוח"); toggleLoader('tone', true); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `Analyze the sentiment of the following text and pick the best speaking style from this list: (natural, cheerful, serious, whisper, excited, calm, authoritative, friendly, professional, storytelling, shouting, terrified, sad, angry, hopeful). Return ONLY the English word. Text: "${text}"` }] }] }); const choice = data.candidates?.[0]?.content?.parts?.[0]?.text?.trim().toLowerCase(); if (choice) { const cleanChoice = choice.replace(/[^a-z]/g, ''); const option = Array.from(els.styleSelect.options).find(o => o.value === cleanChoice); if (option) { els.styleSelect.value = cleanChoice; showToast(`הסגנון הותאם ל: ${option.text}`, "success"); } } } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } toggleLoader('tone', false); }; // ✨ Vocalization (Nikud) document.getElementById('vocalizeBtn').onclick = async () => { const text = els.textInput.value.trim(); if (!text) return showToast("אין טקסט לניקוד"); toggleLoader('vocalize', true); try { const data = await apiCall(modelText, { contents: [{ parts: [{ text: `הוסף ניקוד דקדוקי מלא ומדויק לטקסט הבא בעברית: "${text}". החזר רק את הטקסט המנוקד.` }] }] }); const result = data.candidates?.[0]?.content?.parts?.[0]?.text; if (result) els.textInput.value = result.trim(); } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } toggleLoader('vocalize', false); }; // --- TTS Core --- els.generateBtn.onclick = async () => { const text = els.textInput.value.trim(); if (!text) return showToast("הכנס טקסט לקריינות"); toggleLoader('btn', true); try { const result = await apiCall(modelAudio, { contents: [{ parts: [{ text: `Say in a ${els.styleSelect.value} style: ${text}` }] }], generationConfig: { responseModalities: ["AUDIO"], speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: els.voiceSelect.value } } } } }); const pcmData = result.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data; const mimeType = result.candidates?.[0]?.content?.parts?.[0]?.inlineData?.mimeType || ""; const sampleRate = parseInt(mimeType.split('rate=')[1]) || 24000; if (pcmData) { const blob = pcmToWav(pcmData, sampleRate); addTrack(blob, text.substring(0, 30)); showToast("קריינות הופקה בהצלחה!", "success"); } } catch (e) { if(e.message !== "Missing API Key") showToast(e.message); } toggleLoader('btn', false); }; // --- Helpers --- function toggleLoader(key, show) { const l = document.getElementById(key + 'Loader'); const i = document.getElementById(key + 'Icon'); const t = document.getElementById(key + 'Text'); if (l) l.classList.toggle('hidden', !show); if (i) i.classList.toggle('hidden', show); if (t) t.innerText = show ? "מעבד..." : "הפק קריינות עכשיו"; if (key === 'btn') els.generateBtn.disabled = show; } function pcmToWav(base64, rate) { const binary = atob(base64); const bytes = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i); const header = new ArrayBuffer(44); const v = new DataView(header); const s = (off, str) => { for (let i = 0; i < str.length; i++) v.setUint8(off + i, str.charCodeAt(i)); }; s(0, 'RIFF'); v.setUint32(4, 36 + bytes.length, true); s(8, 'WAVE'); s(12, 'fmt '); v.setUint32(16, 16, true); v.setUint16(20, 1, true); v.setUint16(22, 1, true); v.setUint32(24, rate, true); v.setUint32(28, rate * 2, true); v.setUint16(32, 2, true); v.setUint16(34, 16, true); s(36, 'data'); v.setUint32(40, bytes.length, true); return new Blob([header, bytes], { type: 'audio/wav' }); } function addTrack(blob, title) { els.noTracks.classList.add('hidden'); const url = URL.createObjectURL(blob); const div = document.createElement('div'); div.className = "card p-3 sm:p-4 rounded-xl sm:rounded-2xl flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 shadow-sm border-slate-100 dark:border-slate-800"; div.innerHTML = ` <div class="flex-grow w-full sm:w-auto"> <div class="font-bold text-xs sm:text-sm truncate dark:text-white">${title}...</div> <div class="text-[10px] text-slate-400 mt-0.5">${els.voiceSelect.value} | ${els.styleSelect.value}</div> </div> <div class="flex items-center justify-between sm:justify-end w-full sm:w-auto gap-2 border-t sm:border-t-0 pt-2 sm:pt-0 border-slate-100 dark:border-slate-800"> <audio src="${url}" controls class="w-full sm:w-48"></audio> <a href="${url}" download="voice_${Date.now()}.wav" class="p-2 hover:bg-indigo-50 dark:hover:bg-slate-700 rounded-lg text-indigo-600 flex-shrink-0"> <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg> </a> </div> `; els.tracksContainer.prepend(div); } document.getElementById('toggleDark').onclick = () => els.body.classList.toggle('dark-mode'); document.getElementById('clearTracks').onclick = () => { els.tracksContainer.querySelectorAll('.card').forEach(c => c.remove()); els.noTracks.classList.remove('hidden'); }; </script> </body> </html>בהצלחה!!!