|
<!DOCTYPE html> |
|
<html lang="fa" dir="rtl"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>Alpha TTS - رابط کاربری نهایی</title> |
|
<style> |
|
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap'); |
|
:root{--app-font:'Vazirmatn',sans-serif;--app-header-grad-start:#2980b9;--app-header-grad-end:#2ecc71;--app-panel-bg:#FFFFFF;--app-input-bg:#F7F7F7;--app-button-bg:#2979FF;--app-main-bg:linear-gradient(170deg, #E0F2FE 0%, #F3E8FF 100%);--app-text-primary:#333;--app-text-secondary:#555;--app-border-color:#E0E0E0;--radius-card:20px;--radius-input:12px;--shadow-card:0 10px 30px -5px rgba(0,0,0,0.1);--shadow-button:0 4px 10px -2px rgba(41,121,255,0.5);--speaker-selected-border:3px solid var(--app-button-bg);} |
|
body{font-family:var(--app-font);direction:rtl;background:var(--app-main-bg);color:var(--app-text-primary);font-size:16px;line-height:1.65;margin:0;padding:0;min-height:100vh;display:flex;flex-direction:column;} |
|
.container{max-width:900px;width:95%;margin:0 auto;padding-bottom:40px;} |
|
.app-header{padding:3rem 1.5rem 5rem 1.5rem;text-align:center;background-image:linear-gradient(135deg, var(--app-header-grad-start) 0%, var(--app-header-grad-end) 100%);color:white;border-bottom-left-radius:var(--radius-card);border-bottom-right-radius:var(--radius-card);box-shadow:0 6px 20px -5px rgba(0,0,0,0.2);} |
|
.app-header h1{font-size:2.8em;font-weight:800;margin:0 0 .5rem 0;text-shadow:0 2px 4px rgba(0,0,0,0.15);} |
|
.app-header p{font-size:1.2em;color:rgba(255,255,255,0.9);margin-top:0;opacity:.9;} |
|
.main-content{padding:2.5rem;margin:-3.5rem auto 2rem auto;background-color:var(--app-panel-bg);border-radius:var(--radius-card);box-shadow:var(--shadow-card);} |
|
.form-group{margin-bottom:2rem;} |
|
label{display:block;font-weight:700;color:var(--app-text-primary);font-size:1.1em;margin-bottom:.8rem;} |
|
textarea,input[type=text]{width:100%;padding:1rem;border-radius:var(--radius-input);border:1px solid var(--app-border-color);background-color:var(--app-input-bg);box-shadow:inset 0 1px 2px rgba(0,0,0,0.05);font-family:var(--app-font);font-size:1rem;box-sizing:border-box;transition:all .2s ease-in-out;} |
|
textarea:focus,input[type=text]:focus{outline:none;border-color:var(--app-button-bg);box-shadow:0 0 0 3px rgba(41,121,255,0.2);} |
|
#speaker-grid{display:grid;grid-template-columns:repeat(auto-fill, minmax(120px, 1fr));gap:1.5rem;} |
|
.speaker-card{cursor:pointer;border:3px solid transparent;border-radius:var(--radius-card);overflow:hidden;text-align:center;transition:all .3s ease;box-shadow:0 4px 15px rgba(0,0,0,0.08);position:relative;} |
|
.speaker-card:hover{transform:translateY(-5px);box-shadow:0 8px 25px rgba(0,0,0,0.12);} |
|
.speaker-card input[type=radio]{display:none;} |
|
.speaker-card img{width:100%;height:120px;object-fit:cover;display:block;background-color:#eee;} |
|
.speaker-card .speaker-name{padding:.8rem .5rem;font-weight:500;background-color:rgba(255,255,255,0.8);backdrop-filter:blur(5px);transition:background-color .3s ease;} |
|
.speaker-card input[type=radio]:checked + .speaker-visual{border-color:var(--app-button-bg);box-shadow:0 8px 25px rgba(41,121,255,0.3);transform:translateY(-5px);} |
|
.speaker-card input[type=radio]:checked + .speaker-visual .speaker-name{background-color:var(--app-button-bg);color:white;font-weight:700;} |
|
.slider-container{display:flex;align-items:center;gap:1rem;} |
|
input[type=range]{flex-grow:1;cursor:pointer;} |
|
#temperature-value{font-weight:bold;background-color:var(--app-input-bg);padding:.2rem .8rem;border-radius:8px;border:1px solid var(--app-border-color);min-width:40px;text-align:center;} |
|
#generate-btn{width:100%;padding:1rem 1.5rem;font-size:1.2em;font-weight:700;font-family:var(--app-font);background:var(--app-button-bg);color:white;border:none;border-radius:var(--radius-input);cursor:pointer;transition:all .3s ease;box-shadow:var(--shadow-button);} |
|
#generate-btn:hover:not(:disabled){filter:brightness(1.1);transform:translateY(-2px);box-shadow:0 6px 12px -3px rgba(41,121,255,0.6);} |
|
#generate-btn:disabled{background-color:#999;cursor:not-allowed;box-shadow:none;} |
|
#output-section{margin-top:2.5rem;padding:1.5rem;background-color:var(--app-input-bg);border-radius:var(--radius-card);min-height:100px;display:flex;align-items:center;justify-content:center;flex-direction:column;gap:1rem;} |
|
#status-message{font-weight:500;color:var(--app-text-secondary);} |
|
#audio-player{width:100%;margin-top:1rem;display:none;} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="container"> |
|
<header class="app-header"> |
|
<h1>Alpha TTS</h1> |
|
<p>جادوی تبدیل متن به صدا با رابط کاربری سفارشی شما</p> |
|
</header> |
|
|
|
<main class="main-content"> |
|
<form id="tts-form"> |
|
<div class="form-group"> |
|
<label for="text-input">📝 متن برای تبدیل</label> |
|
<textarea id="text-input" rows="5" placeholder="اینجا متن خود را به فارسی وارد کنید...">این یک آزمایش برای بررسی کیفیت صدای تولید شده توسط هوش مصنوعی آلفا است.</textarea> |
|
</div> |
|
<div class="form-group"> |
|
<label for="prompt-input">🗣️ سبک و لحن گفتار (اختیاری)</label> |
|
<input type="text" id="prompt-input" value="با صدایی طبیعی و روان." placeholder="مثال: با لحنی شاد و پرانرژی"> |
|
</div> |
|
<div class="form-group"> |
|
<label>🎤 گوینده و لهجه را انتخاب کنید</label> |
|
<div id="speaker-grid"></div> |
|
</div> |
|
<div class="form-group"> |
|
<label for="temperature-slider">🌡️ میزان خلاقیت صدا (0.1 تا 1.5)</label> |
|
<div class="slider-container"> |
|
<input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9"> |
|
<span id="temperature-value">0.9</span> |
|
</div> |
|
</div> |
|
<button type="submit" id="generate-btn">🚀 تولید و پخش صدا</button> |
|
</form> |
|
<div id="output-section"> |
|
<div id="status-message">خروجی صدا در اینجا نمایش داده میشود</div> |
|
<audio id="audio-player" controls></audio> |
|
</div> |
|
</main> |
|
</div> |
|
|
|
<script> |
|
document.addEventListener('DOMContentLoaded', () => { |
|
const HF_SPACE_URL = "https://hamed744-ttspro.hf.space"; |
|
|
|
const JOIN_QUEUE_URL = `${HF_SPACE_URL}/gradio_api/queue/join`; |
|
const GET_DATA_URL_BASE = `${HF_SPACE_URL}/gradio_api/queue/data`; |
|
const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`; |
|
|
|
|
|
const FN_INDEX = 1; |
|
|
|
const speakers = ["Achird", "Zubenelgenubi", "Vindemiatrix", "Sadachbia", "Sadaltager", "Sulafat", "Laomedeia", "Achernar", "Alnilam", "Schedar", "Gacrux", "Pulcherrima", "Umbriel", "Algieba", "Despina", "Erinome", "Algenib", "Rasalthgeti", "Orus", "Aoede", "Callirrhoe", "Autonoe", "Enceladus", "Iapetus", "Zephyr", "Puck", "Charon", "Kore", "Fenrir", "Leda"]; |
|
|
|
const form = document.getElementById('tts-form'); |
|
const textInput = document.getElementById('text-input'); |
|
const promptInput = document.getElementById('prompt-input'); |
|
const speakerGrid = document.getElementById('speaker-grid'); |
|
const tempSlider = document.getElementById('temperature-slider'); |
|
const tempValueSpan = document.getElementById('temperature-value'); |
|
const generateBtn = document.getElementById('generate-btn'); |
|
const statusMessage = document.getElementById('status-message'); |
|
const audioPlayer = document.getElementById('audio-player'); |
|
|
|
function createSpeakerCards() { |
|
speakers.forEach((speakerName) => { |
|
const card = document.createElement('label'); |
|
card.className = 'speaker-card'; |
|
card.setAttribute('for', `speaker-${speakerName}`); |
|
const imageUrl = `https://picsum.photos/seed/${speakerName}/200/200`; |
|
const isChecked = speakerName === 'Charon' ? 'checked' : ''; |
|
card.innerHTML = `<input type="radio" name="speaker" value="${speakerName}" id="speaker-${speakerName}" ${isChecked}><div class="speaker-visual"><img src="${imageUrl}" alt="عکس گوینده ${speakerName}"><div class="speaker-name">${speakerName}</div></div>`; |
|
speakerGrid.appendChild(card); |
|
}); |
|
} |
|
|
|
tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; }); |
|
|
|
async function generateAudio(event) { |
|
event.preventDefault(); |
|
generateBtn.disabled = true; |
|
generateBtn.textContent = 'در حال پردازش...'; |
|
statusMessage.textContent = 'در حال ارسال درخواست به سرور...'; |
|
audioPlayer.style.display = 'none'; |
|
audioPlayer.src = ''; |
|
|
|
const text = textInput.value; |
|
const prompt = promptInput.value; |
|
const temperature = parseFloat(tempSlider.value); |
|
const selectedSpeaker = document.querySelector('input[name="speaker"]:checked').value; |
|
const sessionHash = Math.random().toString(36).substring(2); |
|
|
|
if (!text.trim()) { |
|
statusMessage.textContent = 'خطا: متن ورودی نمیتواند خالی باشد.'; |
|
generateBtn.disabled = false; |
|
generateBtn.textContent = '🚀 تولید و پخش صدا'; |
|
return; |
|
} |
|
|
|
const payload = { |
|
fn_index: FN_INDEX, |
|
data: [false, null, text, prompt, selectedSpeaker, temperature], |
|
event_data: null, |
|
session_hash: sessionHash |
|
}; |
|
|
|
try { |
|
|
|
const joinQueueResponse = await fetch(JOIN_QUEUE_URL, { |
|
method: "POST", |
|
headers: { "Content-Type": "application/json" }, |
|
body: JSON.stringify(payload) |
|
}); |
|
|
|
if (!joinQueueResponse.ok) { |
|
const errorBody = await joinQueueResponse.text(); |
|
throw new Error(`خطا در اتصال به صف (${joinQueueResponse.status}): ${errorBody}`); |
|
} |
|
|
|
console.log("با موفقیت به صف متصل شد. در انتظار داده..."); |
|
statusMessage.textContent = 'در انتظار نتیجه از سرور...'; |
|
|
|
|
|
const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`); |
|
const reader = dataResponse.body.getReader(); |
|
const decoder = new TextDecoder(); |
|
let finalFilePath = null; |
|
let buffer = ''; |
|
|
|
while (true) { |
|
const { value, done } = await reader.read(); |
|
if (done) break; |
|
|
|
buffer += decoder.decode(value, { stream: true }); |
|
const lines = buffer.split('\n'); |
|
buffer = lines.pop(); |
|
|
|
for (const line of lines) { |
|
if (!line.startsWith('data:')) continue; |
|
|
|
try { |
|
const data = JSON.parse(line.substring(5)); |
|
console.log('پیام دریافتی از سرور:', data); |
|
|
|
if (data.msg === 'process_generating') { |
|
statusMessage.textContent = 'سرور در حال تولید فایل صوتی است...'; |
|
} |
|
if (data.msg === 'process_completed') { |
|
if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) { |
|
finalFilePath = data.output.data[0].name || data.output.data[0].path; |
|
} else { |
|
console.error("ساختار پیام موفقیت مورد انتظار نبود:", data); |
|
} |
|
break; |
|
} |
|
} catch (e) { |
|
console.warn("خطا در پارس کردن خط:", line, e); |
|
} |
|
} |
|
if (finalFilePath) break; |
|
} |
|
|
|
if (finalFilePath) { |
|
statusMessage.textContent = 'فایل صوتی با موفقیت دریافت شد!'; |
|
const audioUrl = `${FILE_URL_BASE}${finalFilePath}`; |
|
console.log("آدرس نهایی فایل صوتی:", audioUrl); |
|
audioPlayer.src = audioUrl; |
|
audioPlayer.style.display = 'block'; |
|
audioPlayer.play(); |
|
} else { |
|
throw new Error('فایل صوتی از سرور دریافت نشد. کنسول را برای اطلاعات بیشتر بررسی کنید.'); |
|
} |
|
|
|
} catch (error) { |
|
console.error('یک خطا در فرآیند رخ داد:', error); |
|
statusMessage.textContent = `یک خطا رخ داد: ${error.message}`; |
|
} finally { |
|
generateBtn.disabled = false; |
|
generateBtn.textContent = '🚀 تولید و پخش صدا'; |
|
} |
|
} |
|
|
|
createSpeakerCards(); |
|
form.addEventListener('submit', generateAudio); |
|
}); |
|
</script> |
|
</body> |
|
</html> |