הספקתי לסדר ממש קצת מהבעיות באתר שלי, בעקבות שיש כאלה שבבין הזמנים יצטרכו את זה העליתי לכאן את הקובץ.
אחד מהדברים שסודרו זה העיצוב שלא עובד בכל הדרכים...
תפתחו פנקס רשימות ואז תשמרו את זה על שם 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>