Veu3 / Yek.html
Hamed744's picture
Update Yek.html
abc7c31 verified
<!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: #1a2980;
--app-header-grad-end: #26d0ce;
--app-panel-bg: #FFFFFF;
--app-input-bg: #F8F9FA;
--app-button-bg: #5f27cd;
--app-button-hover-bg: #481e9e;
--app-main-bg: linear-gradient(170deg, #F3E8FF 0%, #E0F2FE 100%);
--app-text-primary: #2c3e50;
--app-text-secondary: #555;
--app-border-color: #E0E0E0;
--radius-card: 24px;
--radius-input: 12px;
--shadow-card: 0 10px 30px -5px rgba(0,0,0,0.08);
--shadow-button: 0 4px 15px -2px rgba(95, 39, 205, 0.4);
--speaker-selected-glow: 0 0 15px rgba(95, 39, 205, 0.5);
}
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;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.container { max-width: 800px; 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 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: 0.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: 2.5rem; }
label { display: block; font-weight: 700; color: var(--app-text-primary); font-size: 1.1em; margin-bottom: 0.8rem; }
textarea, input[type="text"] { width: 100%; padding: 1rem; border-radius: var(--radius-input); border: 2px solid var(--app-border-color); background-color: var(--app-input-bg); box-shadow: none; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: all 0.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(95, 39, 205, 0.2); background-color: #fff; }
#selected-speaker-display { text-align: center; }
#selected-speaker-card { display: inline-flex; align-items: center; background: #fff; border-radius: 99px; padding: 10px; box-shadow: 0 5px 20px rgba(0,0,0,0.1); border: 2px solid var(--app-border-color); }
#selected-speaker-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin-left: 15px; background-color: #eee; }
#selected-speaker-info h3 { margin: 0; font-size: 1.4em; }
#selected-speaker-info p { margin: 5px 0 0; color: var(--app-text-secondary); font-size: 0.9em; }
#change-speaker-btn { display: block; margin: 1rem auto 0; padding: 8px 20px; border-radius: 8px; background-color: var(--app-input-bg); border: 1px solid var(--app-border-color); cursor: pointer; font-family: var(--app-font); font-weight: 500; transition: all 0.2s ease; }
#change-speaker-btn:hover { background-color: #e9ecef; border-color: #ced4da; }
#speaker-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.5); backdrop-filter: blur(5px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity 0.3s ease; }
#speaker-modal.visible { display: flex; opacity: 1; }
.modal-content { background: #fff; padding: 2rem; border-radius: var(--radius-card); width: 90%; max-width: 700px; max-height: 80vh; overflow-y: auto; transform: scale(0.95); transition: transform 0.3s ease; }
#speaker-modal.visible .modal-content { transform: scale(1); }
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem; }
.modal-header h2 { margin: 0; }
.close-modal-btn { background: none; border: none; font-size: 2rem; cursor: pointer; color: #aaa; transition: color 0.2s ease; }
.close-modal-btn:hover { color: #333; }
#speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1rem; }
@media (min-width: 576px) { #speaker-grid { grid-template-columns: repeat(4, 1fr); } }
.speaker-card { cursor: pointer; transition: all 0.3s ease; }
.speaker-card .speaker-visual { border: 3px solid transparent; border-radius: var(--radius-card); overflow: hidden; text-align: center; box-shadow: 0 4px 10px rgba(0,0,0,0.05); position: relative; background-color: #fff; }
.speaker-card:hover .speaker-visual { transform: translateY(-3px); box-shadow: 0 6px 15px rgba(0,0,0,0.1); }
.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: 0.7rem 0.5rem; font-weight: 500; font-size: 0.9em; }
.speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--app-button-bg); box-shadow: var(--speaker-selected-glow); }
.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: 0.2rem 0.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 0.3s ease; box-shadow: var(--shadow-button); }
#generate-btn:hover:not(:disabled) { background-color: var(--app-button-hover-bg); transform: translateY(-2px); box-shadow: 0 6px 20px -3px rgba(95, 39, 205, 0.5); }
#generate-btn:disabled { background-color: #999; cursor: not-allowed; box-shadow: none; }
#output-section { margin-top: 2.5rem; padding: 1.5rem; background-color: #fff; border-radius: var(--radius-card); min-height: 100px; display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 1rem; border: 1px solid var(--app-border-color); }
#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>آلفا 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="selected-speaker-display">
<div id="selected-speaker-card">
<img id="selected-speaker-img" src="" alt="عکس گوینده">
<div id="selected-speaker-info">
<h3 id="selected-speaker-name"></h3>
<p>برای تغییر، روی دکمه زیر کلیک کنید</p>
</div>
</div>
<button type="button" id="change-speaker-btn">تغییر گوینده</button>
</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>
<div id="speaker-modal">
<div class="modal-content">
<div class="modal-header">
<h2>انتخاب گوینده</h2>
<button type="button" class="close-modal-btn">×</button>
</div>
<div id="speaker-grid"></div>
</div>
</div>
<input type="hidden" id="selected_speaker_id_storage" value="Charon"> <!-- مقدار پیش‌فرض از کد پایتون -->
<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;
// --- لیست گویندگان با اسامی فارسی و ID انگلیسی مطابق با کد پایتون ---
const speakers = [
{ id: "Achird", name: "آرش (مرد)" },
{ id: "Zubenelgenubi", name: "زهره (زن)" },
{ id: "Vindemiatrix", name: "ویدا (زن)" },
{ id: "Sadachbia", name: "سارا (زن)" },
{ id: "Sadaltager", name: "سامان (مرد)" },
{ id: "Sulafat", name: "سولماز (زن)" },
{ id: "Laomedeia", name: "لیلا (زن)" },
{ id: "Achernar", name: "آرمان (مرد)" },
{ id: "Alnilam", name: "آیدا (زن)" },
{ id: "Schedar", name: "شهاب (مرد)" },
{ id: "Gacrux", name: "کاوه (مرد)" },
{ id: "Pulcherrima", name: "پریا (زن)" },
{ id: "Umbriel", name: "امید (مرد)" },
{ id: "Algieba", name: "آزاده (زن)" },
{ id: "Despina", name: "دینا (زن)" },
{ id: "Erinome", name: "ایرج (مرد)" },
{ id: "Algenib", name: "آرشام (مرد)" },
{ id: "Rasalthgeti", name: "رها (زن)" },
{ id: "Orus", name: "اردلان (مرد)" },
{ id: "Aoede", name: "آیدین (؟)" }, // نام خنثی
{ id: "Callirrhoe", name: "کیمیا (زن)" },
{ id: "Autonoe", name: "آتنا (زن)" },
{ id: "Enceladus", name: "انوش (مرد)" },
{ id: "Iapetus", name: "یاشار (مرد)" },
{ id: "Zephyr", name: "نسیم (زن)" }, // در مثال Gradio زنانه بود
{ id: "Puck", name: "پویا (مرد)" },
{ id: "Charon", name: "کارن (مرد)" }, // پیش‌فرض در Gradio شما
{ id: "Kore", name: "کوروش (مرد)" },
{ id: "Fenrir", name: "فرید (مرد)" },
{ id: "Leda", name: "لیدا (زن)" }
];
const form = document.getElementById('tts-form');
const textInput = document.getElementById('text-input');
const promptInput = document.getElementById('prompt-input');
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');
const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage');
const speakerModal = document.getElementById('speaker-modal');
const changeSpeakerBtn = document.getElementById('change-speaker-btn');
const closeModalBtn = document.querySelector('.close-modal-btn');
const speakerGridInModal = document.getElementById('speaker-grid');
const selectedSpeakerImgDisplay = document.getElementById('selected-speaker-img');
const selectedSpeakerNameDisplay = document.getElementById('selected-speaker-name');
function getSpeakerById(id) {
return speakers.find(s => s.id === id);
}
function getImageUrl(speaker, index) {
const genderHint = speaker.name.toLowerCase();
let gender = 'lego'; // پیشفرض برای موارد نامشخص
if (genderHint.includes('(مرد)')) gender = 'men';
else if (genderHint.includes('(زن)')) gender = 'women';
const imageIndex = (index * 5 + (speaker.id.length % 10)) % 100; // فرمول کمی پیچیده‌تر برای تنوع بیشتر
return `https://randomuser.me/api/portraits/${gender}/${imageIndex}.jpg`;
}
function updateSelectedSpeakerDisplay(speakerId) {
const speaker = getSpeakerById(speakerId);
if (speaker) {
const speakerIndex = speakers.findIndex(s => s.id === speakerId);
selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex);
selectedSpeakerNameDisplay.textContent = speaker.name;
selectedSpeakerIdStorage.value = speaker.id;
}
}
function createSpeakerCardsInModal() {
speakerGridInModal.innerHTML = '';
speakers.forEach((speaker, index) => {
const card = document.createElement('label');
card.className = 'speaker-card';
card.setAttribute('for', `modal-speaker-${speaker.id}`);
const isChecked = speaker.id === selectedSpeakerIdStorage.value ? 'checked' : '';
card.innerHTML = `
<input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
<div class="speaker-visual">
<img src="${getImageUrl(speaker, index)}" alt="عکس گوینده ${speaker.name}" loading="lazy">
<div class="speaker-name">${speaker.name}</div>
</div>
`;
card.addEventListener('click', () => {
updateSelectedSpeakerDisplay(speaker.id);
setTimeout(() => speakerModal.classList.remove('visible'), 100);
});
speakerGridInModal.appendChild(card);
});
}
changeSpeakerBtn.addEventListener('click', () => {
createSpeakerCardsInModal();
speakerModal.classList.add('visible');
});
closeModalBtn.addEventListener('click', () => speakerModal.classList.remove('visible'));
speakerModal.addEventListener('click', (e) => {
if (e.target === speakerModal) {
speakerModal.classList.remove('visible');
}
});
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 = selectedSpeakerIdStorage.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}`);
}
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));
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) { /* نادیده گرفتن خطاهای پارس */ }
}
if (finalFilePath) break;
}
if (finalFilePath) {
statusMessage.textContent = 'فایل صوتی با موفقیت دریافت شد!';
const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
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 = '🚀 تولید و پخش صدا';
}
}
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value);
form.addEventListener('submit', generateAudio);
});
</script>
</body>
</html>