Update index.html
Browse files- index.html +452 -357
index.html
CHANGED
@@ -3,7 +3,8 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>Alpha TTS - نسل جدید تبدیل متن به صدا</title>
|
|
|
7 |
<style>
|
8 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap');
|
9 |
|
@@ -20,6 +21,7 @@
|
|
20 |
--accent-secondary-hover: #059669;
|
21 |
--input-bg: #F8FAFC;
|
22 |
--input-border-focus: var(--accent-primary);
|
|
|
23 |
|
24 |
--radius-card: 24px;
|
25 |
--radius-input: 14px;
|
@@ -30,9 +32,9 @@
|
|
30 |
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
31 |
|
32 |
/* Custom Audio Player Colors */
|
33 |
-
--waveform-color-active:
|
34 |
-
--waveform-color-inactive: #D0D9E6;
|
35 |
-
--waveform-dashed-line-color: #E0E4E9;
|
36 |
}
|
37 |
|
38 |
@keyframes fadeInDown {
|
@@ -51,12 +53,23 @@
|
|
51 |
from { opacity: 1; transform: scale(1) translateY(0); }
|
52 |
to { opacity: 0; transform: scale(0.8) translateY(20px); }
|
53 |
}
|
54 |
-
|
55 |
-
/* Keyframe for loading spinner in button */
|
56 |
@keyframes spin {
|
57 |
from { transform: rotate(0deg); }
|
58 |
to { transform: rotate(360deg); }
|
59 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
60 |
|
61 |
body {
|
62 |
font-family: var(--app-font);
|
@@ -87,6 +100,7 @@
|
|
87 |
text-align: center;
|
88 |
margin-bottom: 1.5rem;
|
89 |
animation: fadeInDown 0.8s 0.1s ease-out backwards;
|
|
|
90 |
}
|
91 |
.app-header h1 {
|
92 |
font-size: 2.1em;
|
@@ -105,6 +119,36 @@
|
|
105 |
font-weight: 400;
|
106 |
line-height: 1.6;
|
107 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
108 |
|
109 |
.main-content {
|
110 |
padding: 2.5rem;
|
@@ -189,6 +233,7 @@
|
|
189 |
padding: 0 0.2rem;
|
190 |
}
|
191 |
#char-count { font-weight: 600; color: var(--accent-primary); }
|
|
|
192 |
|
193 |
|
194 |
/* --- نمایش گوینده منتخب --- */
|
@@ -205,6 +250,8 @@
|
|
205 |
position: relative;
|
206 |
margin-bottom: 1.2rem;
|
207 |
cursor: pointer;
|
|
|
|
|
208 |
}
|
209 |
#selected-speaker-card:hover {
|
210 |
transform: translateY(-6px) scale(1.03);
|
@@ -272,6 +319,8 @@
|
|
272 |
padding: 2rem;
|
273 |
border-radius: var(--radius-card);
|
274 |
width: 90%;
|
|
|
|
|
275 |
box-shadow: var(--shadow-strong);
|
276 |
border: 1px solid var(--panel-border);
|
277 |
opacity: 0; /* For animation */
|
@@ -295,7 +344,6 @@
|
|
295 |
/* --- مودال گالری گویندگان (اختصاصی) --- */
|
296 |
#speaker-modal .modal-dialog {
|
297 |
max-width: 700px;
|
298 |
-
max-height: 85vh; overflow-y: auto;
|
299 |
}
|
300 |
#speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.2rem; }
|
301 |
@media (min-width: 640px) { #speaker-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } }
|
@@ -452,11 +500,16 @@
|
|
452 |
transform: none;
|
453 |
}
|
454 |
#generate-btn:disabled::before { display: none; }
|
455 |
-
#generate-btn
|
|
|
|
|
|
|
|
|
456 |
display:inline-block; margin-left: 0.5em; width:1.2em; height:1.2em;
|
457 |
-
vertical-align: middle;
|
458 |
}
|
459 |
|
|
|
460 |
/* --- Output Section (now the card container for status/loading/player) --- */
|
461 |
#output-section {
|
462 |
margin-top: 3rem;
|
@@ -486,7 +539,14 @@
|
|
486 |
display: none !important;
|
487 |
}
|
488 |
|
489 |
-
#status-message {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
490 |
|
491 |
|
492 |
/* --- انیمیشن پردازش --- */
|
@@ -558,40 +618,40 @@
|
|
558 |
box-sizing: border-box;
|
559 |
flex-direction: column;
|
560 |
gap: 1.2rem;
|
|
|
561 |
}
|
562 |
|
563 |
.audio-waveform-container {
|
564 |
display: flex;
|
565 |
align-items: center;
|
566 |
-
gap: 1rem;
|
567 |
width: 100%;
|
568 |
margin-bottom: 1rem;
|
569 |
}
|
570 |
.audio-time {
|
571 |
font-size: 0.9em;
|
572 |
color: var(--text-secondary);
|
573 |
-
min-width: 40px;
|
574 |
text-align: center;
|
575 |
-
font-variant-numeric: tabular-nums;
|
576 |
-
user-select: none;
|
577 |
}
|
578 |
|
579 |
-
/* Waveform Display (Canvas now) */
|
580 |
.audio-waveform {
|
581 |
flex-grow: 1;
|
582 |
-
height: 60px;
|
583 |
position: relative;
|
584 |
-
overflow: hidden;
|
585 |
-
display: flex;
|
586 |
-
align-items: center;
|
587 |
-
justify-content: center;
|
588 |
-
margin-bottom: 0.5rem;
|
589 |
}
|
590 |
|
591 |
#audio-waveform-canvas {
|
592 |
-
display: block;
|
593 |
-
max-width: 100%;
|
594 |
-
height: 100%;
|
595 |
}
|
596 |
|
597 |
.audio-waveform-dashed-line {
|
@@ -604,7 +664,7 @@
|
|
604 |
background-position: center;
|
605 |
background-size: 10px 1px;
|
606 |
transform: translateY(-50%);
|
607 |
-
z-index: 1;
|
608 |
}
|
609 |
|
610 |
/* Audio Controls */
|
@@ -612,18 +672,18 @@
|
|
612 |
display: flex;
|
613 |
justify-content: center;
|
614 |
align-items: center;
|
615 |
-
gap: 1.5rem;
|
616 |
-
margin-bottom: 1rem;
|
617 |
}
|
618 |
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn, .audio-speed-btn {
|
619 |
background: none;
|
620 |
border: none;
|
621 |
cursor: pointer;
|
622 |
-
padding: 8px;
|
623 |
transition: transform 0.2s, opacity 0.2s;
|
624 |
}
|
625 |
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn {
|
626 |
-
color: var(--text-secondary);
|
627 |
}
|
628 |
.audio-skip-btn:hover, .audio-play-pause-btn-large:hover, .audio-volume-btn:hover {
|
629 |
opacity: 0.8;
|
@@ -637,9 +697,17 @@
|
|
637 |
fill: currentColor;
|
638 |
}
|
639 |
.audio-play-pause-btn-large {
|
640 |
-
padding: 0;
|
641 |
width: 50px;
|
642 |
height: 50px;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
643 |
}
|
644 |
.audio-play-pause-btn-large svg {
|
645 |
width: 38px;
|
@@ -650,7 +718,7 @@
|
|
650 |
.audio-utility-controls {
|
651 |
display: flex;
|
652 |
align-items: center;
|
653 |
-
justify-content: space-between;
|
654 |
width: 100%;
|
655 |
}
|
656 |
.audio-volume-btn svg {
|
@@ -669,12 +737,14 @@
|
|
669 |
text-align: center;
|
670 |
color: var(--text-primary);
|
671 |
box-shadow: var(--shadow-subtle);
|
|
|
672 |
}
|
673 |
.audio-speed-btn:hover {
|
674 |
background-color: var(--input-bg);
|
|
|
|
|
675 |
}
|
676 |
-
|
677 |
-
/* Hide the default audio player */
|
678 |
#hidden-audio-player { display: none !important; }
|
679 |
|
680 |
/* Responsive adjustments for mobile */
|
@@ -690,13 +760,13 @@
|
|
690 |
}
|
691 |
#audio-player-content {
|
692 |
padding: 1rem;
|
693 |
-
gap: 0.8rem;
|
694 |
}
|
695 |
.audio-waveform-container {
|
696 |
-
gap: 0.5rem;
|
697 |
}
|
698 |
.audio-time {
|
699 |
-
font-size: 0.85em;
|
700 |
min-width: 35px;
|
701 |
}
|
702 |
.audio-skip-btn svg {
|
@@ -712,7 +782,7 @@
|
|
712 |
height: 32px;
|
713 |
}
|
714 |
.audio-controls-group {
|
715 |
-
gap: 1rem;
|
716 |
}
|
717 |
.audio-volume-btn svg {
|
718 |
width: 20px;
|
@@ -722,35 +792,55 @@
|
|
722 |
font-size: 0.8em;
|
723 |
padding: 5px 10px;
|
724 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
725 |
}
|
726 |
</style>
|
727 |
</head>
|
728 |
<body>
|
729 |
<div class="container">
|
730 |
<header class="app-header">
|
731 |
-
<
|
732 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
733 |
</header>
|
734 |
|
735 |
<main class="main-content">
|
736 |
<form id="tts-form">
|
737 |
<div class="form-group">
|
738 |
<label for="text-input">📝 متن مورد نظر شما</label>
|
739 |
-
<textarea id="text-input" rows="4" placeholder="متن خود را اینجا وارد کنید..."></textarea>
|
740 |
<div class="char-counter-wrapper">
|
741 |
-
<span id="char-count">0</span> / <span id="char-max">
|
|
|
742 |
</div>
|
743 |
</div>
|
744 |
<div class="form-group">
|
745 |
<label for="prompt-input">🗣️ توصیف سبک و لحن گفتار (اختیاری)</label>
|
746 |
-
<input type="text" id="prompt-input" placeholder="مثال: با لحنی رسمی و موقر، یا شاد و کودکانه">
|
|
|
747 |
</div>
|
748 |
|
749 |
<div class="form-group">
|
750 |
<label>🎤 انتخاب گوینده حرفهای</label>
|
751 |
<div id="selected-speaker-display">
|
752 |
-
<div id="selected-speaker-card"
|
753 |
-
<img id="selected-speaker-img" src="" alt="عکس گوینده">
|
754 |
<div id="selected-speaker-info">
|
755 |
<h3 id="selected-speaker-name"></h3>
|
756 |
<p id="selected-speaker-desc">گوینده پیشفرض</p>
|
@@ -768,19 +858,21 @@
|
|
768 |
<div class="form-group">
|
769 |
<div class="label-with-info">
|
770 |
<label for="temperature-slider">🌡️ میزان خلاقیت و نوآوری صدا</label>
|
771 |
-
<div class="info-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر">!
|
772 |
</div>
|
773 |
</div>
|
774 |
<div class="slider-container">
|
775 |
-
<input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9">
|
776 |
<span id="temperature-value">0.9</span>
|
777 |
</div>
|
778 |
</div>
|
779 |
|
780 |
-
<button type="submit" id="generate-btn"
|
|
|
|
|
781 |
</form>
|
782 |
|
783 |
-
<div id="output-section">
|
784 |
<div id="status-message">صدای تولید شده در اینجا نمایش داده خواهد شد.</div>
|
785 |
<div id="loading-animation-wrapper">
|
786 |
<div class="orbital-loader">
|
@@ -795,7 +887,7 @@
|
|
795 |
<div id="audio-player-content">
|
796 |
<div class="audio-waveform-container">
|
797 |
<span class="audio-time audio-current-time">0:00</span>
|
798 |
-
<div class="audio-waveform">
|
799 |
<canvas id="audio-waveform-canvas"></canvas>
|
800 |
<div class="audio-waveform-dashed-line"></div>
|
801 |
</div>
|
@@ -803,27 +895,27 @@
|
|
803 |
</div>
|
804 |
|
805 |
<div class="audio-controls-group">
|
806 |
-
<button class="audio-skip-btn backward">
|
807 |
-
<svg viewBox="0 0 24 24"><path d="M11 16V8l-4 4 4 4zm4-12v16l7-8-7-8z"></path></svg>
|
808 |
</button>
|
809 |
-
<button class="audio-play-pause-btn-large">
|
810 |
<svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg>
|
811 |
<svg viewBox="0 0 24 24" class="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg>
|
812 |
</button>
|
813 |
-
<button class="audio-skip-btn forward">
|
814 |
-
<svg viewBox="0 0 24 24"><path d="M13 16V8l4 4-4 4zM9 4v16L2 12l7-8z"></path></svg>
|
815 |
</button>
|
816 |
</div>
|
817 |
|
818 |
<div class="audio-utility-controls">
|
819 |
-
<button class="audio-volume-btn">
|
820 |
<svg viewBox="0 0 24 24" class="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
|
821 |
<svg viewBox="0 0 24 24" class="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg>
|
822 |
</button>
|
823 |
-
<button class="audio-speed-btn">1x</button>
|
824 |
</div>
|
825 |
</div>
|
826 |
-
<audio id="hidden-audio-player"
|
827 |
</div>
|
828 |
</main>
|
829 |
</div>
|
@@ -832,8 +924,8 @@
|
|
832 |
<div id="speaker-modal" class="modal-overlay">
|
833 |
<div class="modal-dialog">
|
834 |
<div class="modal-header">
|
835 |
-
<h2>گالری گویندگان آلفا
|
836 |
-
<button type="button" class="close-modal-btn" data-modal-id="speaker-modal" aria-label="بستن
|
837 |
</div>
|
838 |
<div id="speaker-grid">
|
839 |
<!-- Speaker cards will be generated here by JS -->
|
@@ -850,8 +942,8 @@
|
|
850 |
</div>
|
851 |
<div id="info-modal-content">
|
852 |
<p>این تنظیم مشخص میکند که هوش مصنوعی تا چه حد در تولید صدا <strong>خلاقیت</strong> به خرج دهد.</p>
|
853 |
-
<p><strong>مقادیر
|
854 |
-
<p><strong>مقادیر
|
855 |
<span class="range-info">محدوده پیشنهادی: ۰.۱ (پایدار) تا ۱.۵ (بسیار خلاق)</span>
|
856 |
</div>
|
857 |
</div>
|
@@ -862,43 +954,39 @@
|
|
862 |
|
863 |
<script>
|
864 |
document.addEventListener('DOMContentLoaded', () => {
|
865 |
-
|
866 |
-
const
|
867 |
-
const GET_DATA_URL_BASE = `${HF_SPACE_URL}/gradio_api/queue/data`;
|
868 |
-
const FILE_URL_BASE = `${HF_SPACE_URL}/gradio_api/file=`;
|
869 |
-
const FN_INDEX = 1;
|
870 |
-
|
871 |
const speakers = [
|
872 |
-
{ id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرتمند و رسا" },
|
873 |
-
{ id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین" },
|
874 |
-
{ id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی" },
|
875 |
-
{ id: "Zubenelgenubi", name: "رویا (زن)", desc: "گرم و صمیمی" },
|
876 |
-
{ id: "Vindemiatrix", name: "کیان (مرد)", desc: "باوقار و رسمی" },
|
877 |
-
{ id: "Sadachbia", name: "پریسا (زن)", desc: "شاداب و پویا" },
|
878 |
-
{ id: "Sadaltager", name: "آرش (مرد)", desc: "مطمئن و تاثیرگذار" },
|
879 |
-
{ id: "Sulafat", name: "شبنم (زن)", desc: "آرام و متین" },
|
880 |
-
{ id: "Laomedeia", name: "سهیل (مرد)", desc: "دوستانه و گیرا" },
|
881 |
-
{ id: "Achernar", name: "مریم (زن)", desc: "حرفهای و واضح" },
|
882 |
-
{ id: "Alnilam", name: "بهرام (مرد)", desc: "حماسی و نافذ" },
|
883 |
-
{ id: "Schedar", name: "نگار (زن)", desc: "مهربان و شیرین" },
|
884 |
-
{ id: "Gacrux", name: "فرید (مرد)", desc: "پخته و قابل اعتماد" },
|
885 |
-
{ id: "Pulcherrima", name: "سارا (زن)", desc: "جذاب و مدرن" },
|
886 |
-
{ id: "Umbriel", name: "مانی (مرد)", desc: "خلاق و متفاوت" },
|
887 |
-
{ id: "Algieba", name: "آناهیتا (زن)", desc: "با اصالت و شیک" },
|
888 |
-
{ id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی" },
|
889 |
-
{ id: "Erinome", name: "رسا (مرد)", desc: "شفاف و گویا" },
|
890 |
-
{ id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت" },
|
891 |
-
{ id: "Rasalthgeti", name: "الهه (زن)", desc: "اسرارآمیز و فریبنده" },
|
892 |
-
{ id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان" },
|
893 |
-
{ id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوشآهنگ" },
|
894 |
-
{ id: "Callirrhoe", name: "نیما (مرد)", desc: "روایتگر و قصهگو" },
|
895 |
-
{ id: "Autonoe", name: "هستی (زن)", "desc": "طبیعی و خودمانی" },
|
896 |
-
{ id: "Enceladus", name: "کامیار (مرد)", desc: "مصمم و جدی" },
|
897 |
-
{ id: "Iapetus", name: "ستاره (زن)", desc: "درخشان و گیرا" },
|
898 |
-
{ id: "Puck", name: "پویا (مرد)", desc: "بازیگوش و سرزنده" },
|
899 |
-
{ id: "Kore", name: "مهتاب (زن)", desc: "نجواگر و آرامشبخش" },
|
900 |
-
{ id: "Fenrir", name: "سام (مرد)", desc: "جسور و بیباک" },
|
901 |
-
{ id: "Leda", name: "لیدا (زن)", desc: "کلاسیک و باوقار" }
|
902 |
];
|
903 |
|
904 |
const form = document.getElementById('tts-form');
|
@@ -907,6 +995,7 @@
|
|
907 |
const tempSlider = document.getElementById('temperature-slider');
|
908 |
const tempValueSpan = document.getElementById('temperature-value');
|
909 |
const generateBtn = document.getElementById('generate-btn');
|
|
|
910 |
|
911 |
const outputSection = document.getElementById('output-section');
|
912 |
const statusMessage = document.getElementById('status-message');
|
@@ -942,46 +1031,49 @@
|
|
942 |
const volumeMuteIcon = volumeBtn.querySelector('.volume-mute-icon');
|
943 |
const speedBtn = audioPlayerContent.querySelector('.audio-speed-btn');
|
944 |
|
945 |
-
let currentPlaybackSpeedIndex = 0;
|
946 |
const playbackSpeeds = [1.0, 1.25, 1.5, 0.75]; // Adjusted speeds as common values
|
947 |
-
|
948 |
let audioPeaks = []; // Stores calculated peaks for waveform
|
949 |
-
let audioContext = null; // Will be initialized when needed
|
950 |
-
|
951 |
|
952 |
// Character Counter
|
953 |
const charCountSpan = document.getElementById('char-count');
|
954 |
const charMaxSpan = document.getElementById('char-max');
|
955 |
-
const
|
956 |
charMaxSpan.textContent = MAX_CHARS.toLocaleString('fa-IR');
|
957 |
|
958 |
-
|
959 |
-
|
960 |
-
|
961 |
-
|
962 |
-
|
963 |
-
|
964 |
-
|
965 |
-
|
966 |
-
|
|
|
|
|
|
|
|
|
|
|
967 |
|
968 |
function getSpeakerById(id) {
|
969 |
return speakers.find(s => s.id === id) || speakers[0];
|
970 |
}
|
971 |
|
972 |
-
|
973 |
-
|
974 |
-
const
|
975 |
-
const
|
976 |
-
|
977 |
-
|
978 |
-
return `https://randomuser.me/api/portraits/${
|
979 |
}
|
980 |
|
981 |
function updateSelectedSpeakerDisplay(speakerId) {
|
982 |
const speaker = getSpeakerById(speakerId);
|
983 |
-
|
984 |
-
selectedSpeakerImgDisplay.src = getImageUrl(speaker, speakerIndex, 'large');
|
985 |
selectedSpeakerImgDisplay.alt = `عکس گوینده ${speaker.name}`;
|
986 |
selectedSpeakerNameDisplay.textContent = speaker.name;
|
987 |
selectedSpeakerDescDisplay.textContent = speaker.desc || "گوینده منتخب شما";
|
@@ -990,7 +1082,7 @@
|
|
990 |
|
991 |
function createSpeakerCardsInModal() {
|
992 |
speakerGridInModal.innerHTML = '';
|
993 |
-
speakers.forEach((speaker
|
994 |
const cardLabel = document.createElement('label');
|
995 |
cardLabel.className = 'speaker-card';
|
996 |
cardLabel.setAttribute('for', `modal-speaker-${speaker.id}`);
|
@@ -998,15 +1090,12 @@
|
|
998 |
cardLabel.innerHTML = `
|
999 |
<input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
|
1000 |
<div class="speaker-visual">
|
1001 |
-
<img src="${getImageUrl(speaker,
|
1002 |
<div class="speaker-name">${speaker.name}</div>
|
1003 |
</div>
|
1004 |
`;
|
1005 |
-
|
1006 |
-
|
1007 |
-
const radio = cardLabel.querySelector('input[type="radio"]');
|
1008 |
-
if(radio) radio.checked = true;
|
1009 |
-
}
|
1010 |
updateSelectedSpeakerDisplay(speaker.id);
|
1011 |
hideModal(speakerModal);
|
1012 |
});
|
@@ -1014,9 +1103,10 @@
|
|
1014 |
});
|
1015 |
}
|
1016 |
|
1017 |
-
// ---
|
1018 |
function showModal(modalElement) {
|
1019 |
modalElement.classList.add('visible');
|
|
|
1020 |
setTimeout(() => {
|
1021 |
const firstFocusable = modalElement.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
1022 |
if (firstFocusable) firstFocusable.focus();
|
@@ -1030,149 +1120,243 @@
|
|
1030 |
}, { once: true });
|
1031 |
}
|
1032 |
|
1033 |
-
//
|
1034 |
-
changeSpeakerBtn.addEventListener('click', () => {
|
1035 |
-
createSpeakerCardsInModal();
|
1036 |
-
showModal(speakerModal);
|
1037 |
-
});
|
1038 |
-
selectedSpeakerCard.addEventListener('click', () => {
|
1039 |
-
createSpeakerCardsInModal();
|
1040 |
-
showModal(speakerModal);
|
1041 |
-
});
|
1042 |
-
|
1043 |
-
// Info Modal Listener
|
1044 |
-
tempInfoIcon.addEventListener('click', () => showModal(infoModal));
|
1045 |
-
tempInfoIcon.addEventListener('keydown', (e) => {
|
1046 |
-
if (e.key === 'Enter' || e.key === ' ') {
|
1047 |
-
e.preventDefault();
|
1048 |
-
showModal(infoModal);
|
1049 |
-
}
|
1050 |
-
});
|
1051 |
|
1052 |
-
//
|
1053 |
-
|
1054 |
-
|
1055 |
-
if (e.target === overlay) {
|
1056 |
-
hideModal(overlay);
|
1057 |
-
}
|
1058 |
-
});
|
1059 |
-
});
|
1060 |
-
document.querySelectorAll('.close-modal-btn').forEach(button => {
|
1061 |
-
button.addEventListener('click', () => {
|
1062 |
-
const modalId = button.dataset.modalId;
|
1063 |
-
if (modalId) {
|
1064 |
-
hideModal(document.getElementById(modalId));
|
1065 |
-
}
|
1066 |
-
});
|
1067 |
-
});
|
1068 |
-
document.addEventListener('keydown', (e) => {
|
1069 |
-
if (e.key === 'Escape') {
|
1070 |
-
document.querySelectorAll('.modal-overlay.visible').forEach(hideModal);
|
1071 |
-
}
|
1072 |
-
});
|
1073 |
|
1074 |
-
|
|
|
|
|
1075 |
|
1076 |
-
|
|
|
1077 |
|
1078 |
-
|
1079 |
-
if (isNaN(seconds) || seconds < 0) return '0:00';
|
1080 |
-
const minutes = Math.floor(seconds / 60);
|
1081 |
-
const remainingSeconds = Math.floor(seconds % 60);
|
1082 |
-
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
1083 |
-
}
|
1084 |
|
1085 |
-
|
1086 |
-
function drawWaveform(progressRatio = 0) {
|
1087 |
-
waveformCtx.clearRect(0, 0, audioWaveformCanvas.width, audioWaveformCanvas.height); // Clear canvas
|
1088 |
|
1089 |
-
const barWidth = 3
|
1090 |
-
const barGap = 2
|
1091 |
const totalBarAndGap = barWidth + barGap;
|
1092 |
-
const numBars = Math.floor(
|
1093 |
-
const offset = (
|
1094 |
|
1095 |
const activeBars = Math.floor(progressRatio * numBars);
|
1096 |
|
1097 |
for (let i = 0; i < numBars; i++) {
|
1098 |
-
const
|
1099 |
-
const
|
1100 |
-
|
1101 |
-
|
1102 |
-
|
|
|
1103 |
getComputedStyle(document.documentElement).getPropertyValue('--waveform-color-inactive').trim();
|
1104 |
|
1105 |
const x = offset + i * totalBarAndGap;
|
1106 |
-
const y = (
|
1107 |
|
1108 |
waveformCtx.fillRect(x, y, barWidth, barHeight);
|
1109 |
}
|
1110 |
}
|
1111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1112 |
// Update custom player UI (play/pause icon, time, progress)
|
1113 |
function updatePlayerUI() {
|
1114 |
if (hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) {
|
1115 |
playIcon.style.display = 'block';
|
1116 |
pauseIcon.style.display = 'none';
|
|
|
1117 |
} else {
|
1118 |
playIcon.style.display = 'none';
|
1119 |
pauseIcon.style.display = 'block';
|
|
|
1120 |
}
|
1121 |
|
1122 |
const currentTime = hiddenAudioPlayer.currentTime;
|
1123 |
const duration = hiddenAudioPlayer.duration;
|
1124 |
|
1125 |
audioCurrentTimeSpan.textContent = formatTime(currentTime);
|
|
|
|
|
1126 |
|
1127 |
if (isFinite(duration) && duration > 0) {
|
1128 |
audioTotalTimeSpan.textContent = formatTime(duration);
|
1129 |
const progressRatio = currentTime / duration;
|
1130 |
-
drawWaveform(progressRatio);
|
1131 |
} else {
|
1132 |
audioTotalTimeSpan.textContent = '0:00';
|
1133 |
-
drawWaveform(0);
|
1134 |
}
|
1135 |
}
|
1136 |
|
1137 |
-
//
|
1138 |
-
|
1139 |
-
|
1140 |
-
|
1141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1142 |
}
|
1143 |
-
|
|
|
|
|
1144 |
|
1145 |
-
|
1146 |
-
|
1147 |
-
|
1148 |
-
|
1149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1150 |
|
1151 |
-
|
1152 |
-
const peaks = [];
|
1153 |
|
1154 |
-
|
1155 |
-
|
1156 |
-
|
1157 |
-
const start = i * samplesPerBar;
|
1158 |
-
const end = Math.min(start + samplesPerBar, channelData.length);
|
1159 |
|
1160 |
-
|
1161 |
-
|
1162 |
-
|
1163 |
-
|
1164 |
-
|
1165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1166 |
}
|
1167 |
-
|
1168 |
-
|
1169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1170 |
}
|
1171 |
-
|
1172 |
-
drawWaveform(0); // Draw initial waveform
|
1173 |
-
}
|
1174 |
|
1175 |
-
//
|
1176 |
playPauseBtn.addEventListener('click', () => {
|
1177 |
if (hiddenAudioPlayer.paused) {
|
1178 |
hiddenAudioPlayer.play();
|
@@ -1190,6 +1374,7 @@
|
|
1190 |
hiddenAudioPlayer.muted = !hiddenAudioPlayer.muted;
|
1191 |
volumeHighIcon.style.display = hiddenAudioPlayer.muted ? 'none' : 'block';
|
1192 |
volumeMuteIcon.style.display = hiddenAudioPlayer.muted ? 'block' : 'none';
|
|
|
1193 |
});
|
1194 |
speedBtn.addEventListener('click', () => {
|
1195 |
currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length;
|
@@ -1198,28 +1383,34 @@
|
|
1198 |
speedBtn.textContent = `${newSpeed}x`;
|
1199 |
});
|
1200 |
|
|
|
1201 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI);
|
1202 |
hiddenAudioPlayer.addEventListener('loadedmetadata', async () => {
|
1203 |
-
// Ensure AudioContext is
|
1204 |
if (!audioContext) {
|
1205 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
1206 |
}
|
1207 |
-
//
|
1208 |
-
|
1209 |
-
|
1210 |
-
|
1211 |
-
|
1212 |
-
|
1213 |
-
|
1214 |
-
|
1215 |
-
|
1216 |
-
|
1217 |
-
|
1218 |
-
|
1219 |
-
|
1220 |
-
|
|
|
|
|
1221 |
}
|
1222 |
updatePlayerUI(); // Update UI with total duration and initial state
|
|
|
|
|
|
|
1223 |
});
|
1224 |
hiddenAudioPlayer.addEventListener('play', updatePlayerUI);
|
1225 |
hiddenAudioPlayer.addEventListener('pause', updatePlayerUI);
|
@@ -1233,156 +1424,60 @@
|
|
1233 |
window.addEventListener('resize', () => {
|
1234 |
clearTimeout(resizeTimeout);
|
1235 |
resizeTimeout = setTimeout(() => {
|
1236 |
-
if (outputSection.classList.contains('has-content') &&
|
1237 |
-
// Re-
|
1238 |
-
|
1239 |
-
// A more efficient way for real apps might be to store the AudioBuffer.
|
1240 |
-
// For simplicity here, we trigger the loadedmetadata logic again.
|
1241 |
-
hiddenAudioPlayer.dispatchEvent(new Event('loadedmetadata'));
|
1242 |
}
|
1243 |
}, 250); // Debounce resize events
|
1244 |
});
|
1245 |
|
1246 |
|
1247 |
-
// ---
|
1248 |
-
function showLoadingState() {
|
1249 |
-
audioPlayerContent.style.display = 'none';
|
1250 |
-
outputSection.classList.remove('has-content');
|
1251 |
-
statusMessage.style.display = 'none'; // Hide initial message
|
1252 |
-
loadingAnimationWrapper.style.display = 'flex';
|
1253 |
-
generateBtn.disabled = true;
|
1254 |
-
generateBtn.innerHTML = `
|
1255 |
-
<svg aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101" style="display:inline-block; margin-left: 0.5em; width:1.2em; height:1.2em; vertical-align: middle; animation: spin 1s linear infinite;">
|
1256 |
-
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
|
1257 |
-
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
|
1258 |
-
</svg>
|
1259 |
-
در حال پردازش...
|
1260 |
-
`;
|
1261 |
-
}
|
1262 |
-
|
1263 |
-
function showResultState(isSuccess, message = '') {
|
1264 |
-
loadingAnimationWrapper.style.display = 'none';
|
1265 |
-
if (isSuccess) {
|
1266 |
-
statusMessage.style.display = 'none';
|
1267 |
-
audioPlayerContent.style.display = 'flex';
|
1268 |
-
outputSection.classList.add('has-content');
|
1269 |
-
// Play is triggered by loadedmetadata listener after waveform processing
|
1270 |
-
} else {
|
1271 |
-
statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
|
1272 |
-
statusMessage.style.display = 'block';
|
1273 |
-
audioPlayerContent.style.display = 'none';
|
1274 |
-
outputSection.classList.remove('has-content');
|
1275 |
-
hiddenAudioPlayer.pause();
|
1276 |
-
hiddenAudioPlayer.src = '';
|
1277 |
-
audioPeaks = []; // Clear waveform data
|
1278 |
-
drawWaveform(0); // Clear canvas
|
1279 |
-
}
|
1280 |
-
generateBtn.disabled = false;
|
1281 |
-
generateBtn.innerHTML = '✨ تولید صدا با آلفا';
|
1282 |
-
}
|
1283 |
-
|
1284 |
async function generateAudio(event) {
|
1285 |
event.preventDefault();
|
1286 |
-
showLoadingState();
|
1287 |
|
1288 |
-
const text = textInput.value;
|
1289 |
-
if (!text
|
1290 |
-
showResultState(false, '
|
1291 |
return;
|
1292 |
}
|
1293 |
if (text.length > MAX_CHARS) {
|
1294 |
showResultState(false, `خطا: طول متن بیش از ${MAX_CHARS.toLocaleString('fa-IR')} نویسه است.`);
|
1295 |
return;
|
1296 |
}
|
|
|
|
|
|
|
|
|
|
|
1297 |
|
1298 |
-
|
|
|
|
|
|
|
1299 |
const temperatureVal = parseFloat(tempSlider.value);
|
1300 |
const selectedSpeakerVal = selectedSpeakerIdStorage.value;
|
1301 |
-
const sessionHash = Math.random().toString(36).substring(2);
|
1302 |
-
|
1303 |
-
const payload = {
|
1304 |
-
fn_index: FN_INDEX,
|
1305 |
-
data: [false, null, text, promptVal, selectedSpeakerVal, temperatureVal],
|
1306 |
-
event_data: null,
|
1307 |
-
session_hash: sessionHash
|
1308 |
-
};
|
1309 |
|
1310 |
try {
|
1311 |
-
|
1312 |
-
|
1313 |
-
headers: { "Content-Type": "application/json" },
|
1314 |
-
body: JSON.stringify(payload)
|
1315 |
-
});
|
1316 |
|
1317 |
-
|
1318 |
-
|
1319 |
-
|
1320 |
-
throw new Error(`خطا در برقراری ارتباط با سرویس آلفا (${joinQueueResponse.status}). لطفا لحظاتی دیگر تلاش کنید.`);
|
1321 |
-
}
|
1322 |
-
|
1323 |
-
let finalFilePath = null;
|
1324 |
-
const startTime = Date.now();
|
1325 |
-
const timeoutDuration = 90000;
|
1326 |
-
|
1327 |
-
while (Date.now() - startTime < timeoutDuration) {
|
1328 |
-
const dataResponse = await fetch(`${GET_DATA_URL_BASE}?session_hash=${sessionHash}`);
|
1329 |
-
if (!dataResponse.ok) {
|
1330 |
-
const errorBody = await dataResponse.text();
|
1331 |
-
console.error("Get Data Error Body:", errorBody);
|
1332 |
-
throw new Error(`خطا در دریافت داده از سرویس (${dataResponse.status})`);
|
1333 |
-
}
|
1334 |
-
|
1335 |
-
const responseText = await dataResponse.text();
|
1336 |
-
const lines = responseText.trim().split('\n');
|
1337 |
-
|
1338 |
-
for (const line of lines) {
|
1339 |
-
if (!line.startsWith('data:')) continue;
|
1340 |
-
try {
|
1341 |
-
const data = JSON.parse(line.substring(5));
|
1342 |
-
if (data.msg === 'process_generating' || data.msg === 'process_starts') {
|
1343 |
-
// console.log("Processing:", data.output?.progress_data?.[0]?.desc || data.msg);
|
1344 |
-
} else if (data.msg === 'process_completed') {
|
1345 |
-
if (data.success && data.output.data && data.output.data[0] && (data.output.data[0].name || data.output.data[0].path)) {
|
1346 |
-
finalFilePath = data.output.data[0].name || data.output.data[0].path;
|
1347 |
-
} else {
|
1348 |
-
console.error("Invalid server response structure or unsuccessful processing:", data);
|
1349 |
-
throw new Error(data.output?.error || 'خطای ناشناخته در پردازش سرور.');
|
1350 |
-
}
|
1351 |
-
break;
|
1352 |
-
} else if (data.msg === 'queue_full') {
|
1353 |
-
throw new Error('صف پردازش پر است. لطفا کمی بعد تلاش کنید.');
|
1354 |
-
}
|
1355 |
-
} catch (e) {
|
1356 |
-
console.warn("Error parsing JSON from stream:", e, "Line:", line);
|
1357 |
-
}
|
1358 |
-
}
|
1359 |
-
if (finalFilePath) break;
|
1360 |
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
1361 |
-
}
|
1362 |
-
|
1363 |
-
if (finalFilePath) {
|
1364 |
-
const audioUrl = `${FILE_URL_BASE}${finalFilePath}`;
|
1365 |
-
hiddenAudioPlayer.src = audioUrl;
|
1366 |
-
showResultState(true);
|
1367 |
-
} else if (Date.now() - startTime >= timeoutDuration) {
|
1368 |
-
throw new Error('پردازش بیش از حد طول کشید و متوقف شد.');
|
1369 |
-
} else {
|
1370 |
-
throw new Error('فایل صوتی از سرور دریافت نشد یا پردازش ناموفق بود.');
|
1371 |
-
}
|
1372 |
|
|
|
1373 |
} catch (error) {
|
1374 |
console.error('خطا در فرآیند تولید صدا:', error);
|
1375 |
-
showResultState(false, `${error.message}`);
|
1376 |
}
|
1377 |
}
|
1378 |
|
1379 |
// Initial setup
|
1380 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
|
1381 |
form.addEventListener('submit', generateAudio);
|
1382 |
-
|
1383 |
-
|
1384 |
-
audioPlayerContent.style.display = 'none';
|
1385 |
-
outputSection.classList.remove('has-content');
|
1386 |
});
|
1387 |
</script>
|
1388 |
</body>
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>Alpha TTS Pro - نسل جدید تبدیل متن به صدا</title>
|
7 |
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%233B82F6' d='M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'/%3E%3C/svg%3E" type="image/svg+xml">
|
8 |
<style>
|
9 |
@import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap');
|
10 |
|
|
|
21 |
--accent-secondary-hover: #059669;
|
22 |
--input-bg: #F8FAFC;
|
23 |
--input-border-focus: var(--accent-primary);
|
24 |
+
--error-color: #EF4444;
|
25 |
|
26 |
--radius-card: 24px;
|
27 |
--radius-input: 14px;
|
|
|
32 |
--transition-bounce: all 0.4s cubic-bezier(0.68, -0.55, 0.27, 1.55);
|
33 |
|
34 |
/* Custom Audio Player Colors */
|
35 |
+
--waveform-color-active: var(--accent-primary);
|
36 |
+
--waveform-color-inactive: #D0D9E6;
|
37 |
+
--waveform-dashed-line-color: #E0E4E9;
|
38 |
}
|
39 |
|
40 |
@keyframes fadeInDown {
|
|
|
53 |
from { opacity: 1; transform: scale(1) translateY(0); }
|
54 |
to { opacity: 0; transform: scale(0.8) translateY(20px); }
|
55 |
}
|
|
|
|
|
56 |
@keyframes spin {
|
57 |
from { transform: rotate(0deg); }
|
58 |
to { transform: rotate(360deg); }
|
59 |
}
|
60 |
+
@keyframes pulse {
|
61 |
+
0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.7); }
|
62 |
+
70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(59, 130, 246, 0); }
|
63 |
+
100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(59, 130, 246, 0); }
|
64 |
+
}
|
65 |
+
@keyframes typing {
|
66 |
+
from { width: 0 }
|
67 |
+
to { width: 100% }
|
68 |
+
}
|
69 |
+
@keyframes blink-caret {
|
70 |
+
from, to { border-color: transparent }
|
71 |
+
50% { border-color: var(--accent-primary); }
|
72 |
+
}
|
73 |
|
74 |
body {
|
75 |
font-family: var(--app-font);
|
|
|
100 |
text-align: center;
|
101 |
margin-bottom: 1.5rem;
|
102 |
animation: fadeInDown 0.8s 0.1s ease-out backwards;
|
103 |
+
position: relative; /* For credits display */
|
104 |
}
|
105 |
.app-header h1 {
|
106 |
font-size: 2.1em;
|
|
|
119 |
font-weight: 400;
|
120 |
line-height: 1.6;
|
121 |
}
|
122 |
+
.credits-display {
|
123 |
+
position: absolute;
|
124 |
+
top: 0;
|
125 |
+
right: 0;
|
126 |
+
background-color: var(--panel-bg);
|
127 |
+
border: 1px solid var(--panel-border);
|
128 |
+
padding: 0.6rem 1.2rem;
|
129 |
+
border-radius: var(--radius-input);
|
130 |
+
font-size: 0.85em;
|
131 |
+
font-weight: 600;
|
132 |
+
color: var(--text-primary);
|
133 |
+
box-shadow: var(--shadow-subtle);
|
134 |
+
display: flex;
|
135 |
+
align-items: center;
|
136 |
+
gap: 0.4rem;
|
137 |
+
}
|
138 |
+
.credits-display svg {
|
139 |
+
width: 1.2em; height: 1.2em;
|
140 |
+
fill: var(--accent-secondary);
|
141 |
+
}
|
142 |
+
@media (max-width: 600px) {
|
143 |
+
.credits-display {
|
144 |
+
position: relative;
|
145 |
+
top: auto;
|
146 |
+
right: auto;
|
147 |
+
margin: 1rem auto 0;
|
148 |
+
width: fit-content;
|
149 |
+
}
|
150 |
+
}
|
151 |
+
|
152 |
|
153 |
.main-content {
|
154 |
padding: 2.5rem;
|
|
|
233 |
padding: 0 0.2rem;
|
234 |
}
|
235 |
#char-count { font-weight: 600; color: var(--accent-primary); }
|
236 |
+
#char-count.error { color: var(--error-color); }
|
237 |
|
238 |
|
239 |
/* --- نمایش گوینده منتخب --- */
|
|
|
250 |
position: relative;
|
251 |
margin-bottom: 1.2rem;
|
252 |
cursor: pointer;
|
253 |
+
text-align: right;
|
254 |
+
max-width: 90%;
|
255 |
}
|
256 |
#selected-speaker-card:hover {
|
257 |
transform: translateY(-6px) scale(1.03);
|
|
|
319 |
padding: 2rem;
|
320 |
border-radius: var(--radius-card);
|
321 |
width: 90%;
|
322 |
+
max-height: 90vh; /* Ensure modal fits viewport */
|
323 |
+
overflow-y: auto; /* Enable scrolling for content overflow */
|
324 |
box-shadow: var(--shadow-strong);
|
325 |
border: 1px solid var(--panel-border);
|
326 |
opacity: 0; /* For animation */
|
|
|
344 |
/* --- مودال گالری گویندگان (اختصاصی) --- */
|
345 |
#speaker-modal .modal-dialog {
|
346 |
max-width: 700px;
|
|
|
347 |
}
|
348 |
#speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 1.2rem; }
|
349 |
@media (min-width: 640px) { #speaker-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); } }
|
|
|
500 |
transform: none;
|
501 |
}
|
502 |
#generate-btn:disabled::before { display: none; }
|
503 |
+
#generate-btn .button-text {
|
504 |
+
display: inline-block;
|
505 |
+
vertical-align: middle;
|
506 |
+
}
|
507 |
+
#generate-btn svg.spinner {
|
508 |
display:inline-block; margin-left: 0.5em; width:1.2em; height:1.2em;
|
509 |
+
vertical-align: middle; animation: spin 1s linear infinite;
|
510 |
}
|
511 |
|
512 |
+
|
513 |
/* --- Output Section (now the card container for status/loading/player) --- */
|
514 |
#output-section {
|
515 |
margin-top: 3rem;
|
|
|
539 |
display: none !important;
|
540 |
}
|
541 |
|
542 |
+
#status-message {
|
543 |
+
font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em;
|
544 |
+
animation: fadeIn 0.5s ease-out;
|
545 |
+
}
|
546 |
+
#status-message.error {
|
547 |
+
color: var(--error-color);
|
548 |
+
font-weight: 600;
|
549 |
+
}
|
550 |
|
551 |
|
552 |
/* --- انیمیشن پردازش --- */
|
|
|
618 |
box-sizing: border-box;
|
619 |
flex-direction: column;
|
620 |
gap: 1.2rem;
|
621 |
+
animation: fadeIn 0.5s ease-out;
|
622 |
}
|
623 |
|
624 |
.audio-waveform-container {
|
625 |
display: flex;
|
626 |
align-items: center;
|
627 |
+
gap: 1rem;
|
628 |
width: 100%;
|
629 |
margin-bottom: 1rem;
|
630 |
}
|
631 |
.audio-time {
|
632 |
font-size: 0.9em;
|
633 |
color: var(--text-secondary);
|
634 |
+
min-width: 40px;
|
635 |
text-align: center;
|
636 |
+
font-variant-numeric: tabular-nums;
|
637 |
+
user-select: none;
|
638 |
}
|
639 |
|
|
|
640 |
.audio-waveform {
|
641 |
flex-grow: 1;
|
642 |
+
height: 60px;
|
643 |
position: relative;
|
644 |
+
overflow: hidden;
|
645 |
+
display: flex;
|
646 |
+
align-items: center;
|
647 |
+
justify-content: center;
|
648 |
+
margin-bottom: 0.5rem;
|
649 |
}
|
650 |
|
651 |
#audio-waveform-canvas {
|
652 |
+
display: block;
|
653 |
+
max-width: 100%;
|
654 |
+
height: 100%;
|
655 |
}
|
656 |
|
657 |
.audio-waveform-dashed-line {
|
|
|
664 |
background-position: center;
|
665 |
background-size: 10px 1px;
|
666 |
transform: translateY(-50%);
|
667 |
+
z-index: 1;
|
668 |
}
|
669 |
|
670 |
/* Audio Controls */
|
|
|
672 |
display: flex;
|
673 |
justify-content: center;
|
674 |
align-items: center;
|
675 |
+
gap: 1.5rem;
|
676 |
+
margin-bottom: 1rem;
|
677 |
}
|
678 |
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn, .audio-speed-btn {
|
679 |
background: none;
|
680 |
border: none;
|
681 |
cursor: pointer;
|
682 |
+
padding: 8px;
|
683 |
transition: transform 0.2s, opacity 0.2s;
|
684 |
}
|
685 |
.audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn {
|
686 |
+
color: var(--text-secondary);
|
687 |
}
|
688 |
.audio-skip-btn:hover, .audio-play-pause-btn-large:hover, .audio-volume-btn:hover {
|
689 |
opacity: 0.8;
|
|
|
697 |
fill: currentColor;
|
698 |
}
|
699 |
.audio-play-pause-btn-large {
|
700 |
+
padding: 0;
|
701 |
width: 50px;
|
702 |
height: 50px;
|
703 |
+
border-radius: 50%;
|
704 |
+
background-color: var(--accent-primary);
|
705 |
+
color: white;
|
706 |
+
box-shadow: 0 4px 10px -2px rgba(59, 130, 246, 0.35);
|
707 |
+
}
|
708 |
+
.audio-play-pause-btn-large:hover {
|
709 |
+
background-color: var(--accent-primary-hover);
|
710 |
+
transform: translateY(-2px) scale(1.05);
|
711 |
}
|
712 |
.audio-play-pause-btn-large svg {
|
713 |
width: 38px;
|
|
|
718 |
.audio-utility-controls {
|
719 |
display: flex;
|
720 |
align-items: center;
|
721 |
+
justify-content: space-between;
|
722 |
width: 100%;
|
723 |
}
|
724 |
.audio-volume-btn svg {
|
|
|
737 |
text-align: center;
|
738 |
color: var(--text-primary);
|
739 |
box-shadow: var(--shadow-subtle);
|
740 |
+
padding: 5px 12px;
|
741 |
}
|
742 |
.audio-speed-btn:hover {
|
743 |
background-color: var(--input-bg);
|
744 |
+
transform: translateY(-2px);
|
745 |
+
box-shadow: var(--shadow-medium);
|
746 |
}
|
747 |
+
|
|
|
748 |
#hidden-audio-player { display: none !important; }
|
749 |
|
750 |
/* Responsive adjustments for mobile */
|
|
|
760 |
}
|
761 |
#audio-player-content {
|
762 |
padding: 1rem;
|
763 |
+
gap: 0.8rem;
|
764 |
}
|
765 |
.audio-waveform-container {
|
766 |
+
gap: 0.5rem;
|
767 |
}
|
768 |
.audio-time {
|
769 |
+
font-size: 0.85em;
|
770 |
min-width: 35px;
|
771 |
}
|
772 |
.audio-skip-btn svg {
|
|
|
782 |
height: 32px;
|
783 |
}
|
784 |
.audio-controls-group {
|
785 |
+
gap: 1rem;
|
786 |
}
|
787 |
.audio-volume-btn svg {
|
788 |
width: 20px;
|
|
|
792 |
font-size: 0.8em;
|
793 |
padding: 5px 10px;
|
794 |
}
|
795 |
+
#selected-speaker-card {
|
796 |
+
flex-direction: column;
|
797 |
+
padding: 1.2rem 1rem;
|
798 |
+
max-width: none;
|
799 |
+
margin-bottom: 1rem;
|
800 |
+
}
|
801 |
+
#selected-speaker-card img {
|
802 |
+
margin: 0 0 10px 0;
|
803 |
+
}
|
804 |
+
#selected-speaker-info {
|
805 |
+
text-align: center;
|
806 |
+
}
|
807 |
}
|
808 |
</style>
|
809 |
</head>
|
810 |
<body>
|
811 |
<div class="container">
|
812 |
<header class="app-header">
|
813 |
+
<div class="credits-display" aria-label="اعتبار باقیمانده شما">
|
814 |
+
<svg viewBox="0 0 24 24" fill="currentColor">
|
815 |
+
<path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2zm.293 15.293a1 1 0 001.414 0l4-4a1 1 0 00-1.414-1.414L13 14.586V7a1 1 0 00-2 0v7.586l-2.293-2.293a1 1 0 00-1.414 1.414l4 4z"></path>
|
816 |
+
</svg>
|
817 |
+
<span id="credits-left">10,000</span> کاراکتر باقیمانده
|
818 |
+
</div>
|
819 |
+
<h1>تبدیل متن به صدا با هوش مصنوعی آلفا پرو</h1>
|
820 |
+
<p>صدایی نو، تجربهای نوین در تبدیل متن به گفتار با هوش مصنوعی پیشرفته برای کاربران ویژه.</p>
|
821 |
</header>
|
822 |
|
823 |
<main class="main-content">
|
824 |
<form id="tts-form">
|
825 |
<div class="form-group">
|
826 |
<label for="text-input">📝 متن مورد نظر شما</label>
|
827 |
+
<textarea id="text-input" rows="4" placeholder="متن خود را اینجا وارد کنید..." aria-required="true"></textarea>
|
828 |
<div class="char-counter-wrapper">
|
829 |
+
<span id="char-count">0</span> / <span id="char-max">5000</span> نویسه
|
830 |
+
<span id="char-error-message" style="display:none; color:var(--error-color); margin-right: 10px;"></span>
|
831 |
</div>
|
832 |
</div>
|
833 |
<div class="form-group">
|
834 |
<label for="prompt-input">🗣️ توصیف سبک و لحن گفتار (اختیاری)</label>
|
835 |
+
<input type="text" id="prompt-input" placeholder="مثال: با لحنی رسمی و موقر، یا شاد و کودکانه" aria-describedby="prompt-info">
|
836 |
+
<small id="prompt-info" style="display: block; margin-top: 5px; color: var(--text-secondary); font-size: 0.85em;">این ویژگی به هوش مصنوعی کمک میکند تا لحن دقیقتری ایجاد کند.</small>
|
837 |
</div>
|
838 |
|
839 |
<div class="form-group">
|
840 |
<label>🎤 انتخاب گوینده حرفهای</label>
|
841 |
<div id="selected-speaker-display">
|
842 |
+
<div id="selected-speaker-card" role="button" tabindex="0" aria-label="برای تغییر گوینده کلیک کنید">
|
843 |
+
<img id="selected-speaker-img" src="" alt="عکس گوینده" loading="lazy">
|
844 |
<div id="selected-speaker-info">
|
845 |
<h3 id="selected-speaker-name"></h3>
|
846 |
<p id="selected-speaker-desc">گوینده پیشفرض</p>
|
|
|
858 |
<div class="form-group">
|
859 |
<div class="label-with-info">
|
860 |
<label for="temperature-slider">🌡️ میزان خلاقیت و نوآوری صدا</label>
|
861 |
+
<div class="info-icon" id="temp-info-icon" role="button" tabindex="0" aria-label="اطلاعات بیشتر درباره خلاقیت صدا">!
|
862 |
</div>
|
863 |
</div>
|
864 |
<div class="slider-container">
|
865 |
+
<input type="range" id="temperature-slider" min="0.1" max="1.5" step="0.05" value="0.9" aria-valuemin="0.1" aria-valuemax="1.5" aria-valuenow="0.9" aria-label="تنظیم میزان خلاقیت صدا">
|
866 |
<span id="temperature-value">0.9</span>
|
867 |
</div>
|
868 |
</div>
|
869 |
|
870 |
+
<button type="submit" id="generate-btn">
|
871 |
+
<span class="button-text">✨ تولید صدا با آلفا</span>
|
872 |
+
</button>
|
873 |
</form>
|
874 |
|
875 |
+
<div id="output-section" aria-live="polite">
|
876 |
<div id="status-message">صدای تولید شده در اینجا نمایش داده خواهد شد.</div>
|
877 |
<div id="loading-animation-wrapper">
|
878 |
<div class="orbital-loader">
|
|
|
887 |
<div id="audio-player-content">
|
888 |
<div class="audio-waveform-container">
|
889 |
<span class="audio-time audio-current-time">0:00</span>
|
890 |
+
<div class="audio-waveform" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="پیشرفت پخش صدا">
|
891 |
<canvas id="audio-waveform-canvas"></canvas>
|
892 |
<div class="audio-waveform-dashed-line"></div>
|
893 |
</div>
|
|
|
895 |
</div>
|
896 |
|
897 |
<div class="audio-controls-group">
|
898 |
+
<button class="audio-skip-btn backward" aria-label="5 ثانیه به عقب">
|
899 |
+
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M11 16V8l-4 4 4 4zm4-12v16l7-8-7-8z"></path></svg>
|
900 |
</button>
|
901 |
+
<button class="audio-play-pause-btn-large" aria-label="پخش/مکث">
|
902 |
<svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg>
|
903 |
<svg viewBox="0 0 24 24" class="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg>
|
904 |
</button>
|
905 |
+
<button class="audio-skip-btn forward" aria-label="5 ثانیه به جلو">
|
906 |
+
<svg viewBox="0 0 24 24" width="24" height="24"><path d="M13 16V8l4 4-4 4zM9 4v16L2 12l7-8z"></path></svg>
|
907 |
</button>
|
908 |
</div>
|
909 |
|
910 |
<div class="audio-utility-controls">
|
911 |
+
<button class="audio-volume-btn" aria-label="بیصدا کردن/فعال کردن صدا">
|
912 |
<svg viewBox="0 0 24 24" class="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg>
|
913 |
<svg viewBox="0 0 24 24" class="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg>
|
914 |
</button>
|
915 |
+
<button class="audio-speed-btn" aria-label="تغییر سرعت پخش">1x</button>
|
916 |
</div>
|
917 |
</div>
|
918 |
+
<audio id="hidden-audio-player" preload="auto"></audio>
|
919 |
</div>
|
920 |
</main>
|
921 |
</div>
|
|
|
924 |
<div id="speaker-modal" class="modal-overlay">
|
925 |
<div class="modal-dialog">
|
926 |
<div class="modal-header">
|
927 |
+
<h2>گالری گویندگان آلفا پرو</h2>
|
928 |
+
<button type="button" class="close-modal-btn" data-modal-id="speaker-modal" aria-label="بستن گالری گویندگان">×</button>
|
929 |
</div>
|
930 |
<div id="speaker-grid">
|
931 |
<!-- Speaker cards will be generated here by JS -->
|
|
|
942 |
</div>
|
943 |
<div id="info-modal-content">
|
944 |
<p>این تنظیم مشخص میکند که هوش مصنوعی تا چه حد در تولید صدا <strong>خلاقیت</strong> به خرج دهد.</p>
|
945 |
+
<p><strong>مقادیر بالاتر (مثلاً ۱.۰ تا ۱.۵):</strong> منجر به صدایی متنوعتر، پویاتر و گاهی اوقات غیرمنتظرهتر میشود. مناسب برای زمانی که به دنبال لحنی خاص و منحصربهفرد هستید.</p>
|
946 |
+
<p><strong>مقادیر پایینتر (مثلاً ۰.۱ تا ۰.۵):</strong> صدایی پایدارتر، قابل پیشبینیتر و نزدیکتر به صدای استاندارد گوینده تولید میکند. مناسب برای خوانش متون رسمی یا زمانی که ثبات لحن اهمیت دارد.</p>
|
947 |
<span class="range-info">محدوده پیشنهادی: ۰.۱ (پایدار) تا ۱.۵ (بسیار خلاق)</span>
|
948 |
</div>
|
949 |
</div>
|
|
|
954 |
|
955 |
<script>
|
956 |
document.addEventListener('DOMContentLoaded', () => {
|
957 |
+
// --- CONSTANTS & DOM ELEMENTS ---
|
958 |
+
const MAX_CHARS = 5000; // Reduced max chars for a "paid" tier, adjust as needed
|
|
|
|
|
|
|
|
|
959 |
const speakers = [
|
960 |
+
{ id: "Charon", name: "شهاب (مرد)", desc: "صدایی قدرتمند و رسا", gender: "men" },
|
961 |
+
{ id: "Zephyr", name: "آوا (زن)", desc: "لطیف و دلنشین", gender: "women" },
|
962 |
+
{ id: "Achird", name: "نوید (مرد)", desc: "جوان و پرانرژی", gender: "men" },
|
963 |
+
{ id: "Zubenelgenubi", name: "رویا (زن)", desc: "گرم و صمیمی", gender: "women" },
|
964 |
+
{ id: "Vindemiatrix", name: "کیان (مرد)", desc: "باوقار و رسمی", gender: "men" },
|
965 |
+
{ id: "Sadachbia", name: "پریسا (زن)", desc: "شاداب و پویا", gender: "women" },
|
966 |
+
{ id: "Sadaltager", name: "آرش (مرد)", desc: "مطمئن و تاثیرگذار", gender: "men" },
|
967 |
+
{ id: "Sulafat", name: "شبنم (زن)", desc: "آرام و متین", gender: "women" },
|
968 |
+
{ id: "Laomedeia", name: "سهیل (مرد)", desc: "دوستانه و گیرا", gender: "men" },
|
969 |
+
{ id: "Achernar", name: "مریم (زن)", desc: "حرفهای و واضح", gender: "women" },
|
970 |
+
{ id: "Alnilam", name: "بهرام (مرد)", desc: "حماسی و نافذ", gender: "men" },
|
971 |
+
{ id: "Schedar", name: "نگار (زن)", desc: "مهربان و شیرین", gender: "women" },
|
972 |
+
{ id: "Gacrux", name: "فرید (مرد)", desc: "پخته و قابل اعتماد", gender: "men" },
|
973 |
+
{ id: "Pulcherrima", name: "سارا (زن)", desc: "جذاب و مدرن", gender: "women" },
|
974 |
+
{ id: "Umbriel", name: "مانی (مرد)", desc: "خلاق و متفاوت", gender: "men" },
|
975 |
+
{ id: "Algieba", name: "آناهیتا (زن)", desc: "با اصالت و شیک", gender: "women" },
|
976 |
+
{ id: "Despina", name: "دلنواز (زن)", desc: "هنری و احساسی", gender: "women" },
|
977 |
+
{ id: "Erinome", name: "رسا (مرد)", desc: "شفاف و گویا", gender: "men" },
|
978 |
+
{ id: "Algenib", name: "امید (مرد)", desc: "انگیزه بخش و مثبت", gender: "men" },
|
979 |
+
{ id: "Rasalthgeti", name: "الهه (زن)", desc: "اسرارآمیز و فریبنده", gender: "women" },
|
980 |
+
{ id: "Orus", name: "بردیا (مرد)", desc: "ورزشی و پرهیجان", gender: "men" },
|
981 |
+
{ id: "Aoede", name: "ترانه (زن)", desc: "موزیکال و خوشآهنگ", gender: "women" },
|
982 |
+
{ id: "Callirrhoe", name: "نیما (مرد)", desc: "روایتگر و قصهگو", gender: "men" },
|
983 |
+
{ id: "Autonoe", name: "هستی (زن)", "desc": "طبیعی و خودمانی", gender: "women" },
|
984 |
+
{ id: "Enceladus", name: "کامیار (مرد)", desc: "مصمم و جدی", gender: "men" },
|
985 |
+
{ id: "Iapetus", name: "ستاره (زن)", desc: "درخشان و گیرا", gender: "women" },
|
986 |
+
{ id: "Puck", name: "پویا (مرد)", desc: "بازیگوش و سرزنده", gender: "men" },
|
987 |
+
{ id: "Kore", name: "مهتاب (زن)", desc: "نجواگر و آرامشبخش", gender: "women" },
|
988 |
+
{ id: "Fenrir", name: "سام (مرد)", desc: "جسور و بیباک", gender: "men" },
|
989 |
+
{ id: "Leda", name: "لیدا (زن)", desc: "کلاسیک و باوقار", gender: "women" }
|
990 |
];
|
991 |
|
992 |
const form = document.getElementById('tts-form');
|
|
|
995 |
const tempSlider = document.getElementById('temperature-slider');
|
996 |
const tempValueSpan = document.getElementById('temperature-value');
|
997 |
const generateBtn = document.getElementById('generate-btn');
|
998 |
+
const generateButtonText = generateBtn.querySelector('.button-text');
|
999 |
|
1000 |
const outputSection = document.getElementById('output-section');
|
1001 |
const statusMessage = document.getElementById('status-message');
|
|
|
1031 |
const volumeMuteIcon = volumeBtn.querySelector('.volume-mute-icon');
|
1032 |
const speedBtn = audioPlayerContent.querySelector('.audio-speed-btn');
|
1033 |
|
|
|
1034 |
const playbackSpeeds = [1.0, 1.25, 1.5, 0.75]; // Adjusted speeds as common values
|
1035 |
+
let currentPlaybackSpeedIndex = 0;
|
1036 |
let audioPeaks = []; // Stores calculated peaks for waveform
|
1037 |
+
let audioContext = null; // Will be initialized when needed for waveform
|
1038 |
+
let audioBufferCache = null; // Cache for the decoded audio buffer
|
1039 |
|
1040 |
// Character Counter
|
1041 |
const charCountSpan = document.getElementById('char-count');
|
1042 |
const charMaxSpan = document.getElementById('char-max');
|
1043 |
+
const charErrorMessage = document.getElementById('char-error-message');
|
1044 |
charMaxSpan.textContent = MAX_CHARS.toLocaleString('fa-IR');
|
1045 |
|
1046 |
+
// Credits Display (Placeholder)
|
1047 |
+
const creditsLeftSpan = document.getElementById('credits-left');
|
1048 |
+
let currentCredits = 10000; // Initial mock credits
|
1049 |
+
creditsLeftSpan.textContent = currentCredits.toLocaleString('fa-IR');
|
1050 |
+
|
1051 |
+
|
1052 |
+
// --- HELPER FUNCTIONS ---
|
1053 |
+
|
1054 |
+
function formatTime(seconds) {
|
1055 |
+
if (isNaN(seconds) || seconds < 0) return '0:00';
|
1056 |
+
const minutes = Math.floor(seconds / 60);
|
1057 |
+
const remainingSeconds = Math.floor(seconds % 60);
|
1058 |
+
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
1059 |
+
}
|
1060 |
|
1061 |
function getSpeakerById(id) {
|
1062 |
return speakers.find(s => s.id === id) || speakers[0];
|
1063 |
}
|
1064 |
|
1065 |
+
// Uses randomuser.me for diverse, consistent placeholder images
|
1066 |
+
function getImageUrl(speaker, size = 'thumb') {
|
1067 |
+
const seed = speaker.id.charCodeAt(0) + speaker.id.length; // Simple seed
|
1068 |
+
const genderPath = speaker.gender === 'men' ? 'men' : 'women';
|
1069 |
+
const imageIndex = (seed % 100) + 1; // Ensure index is 1-100
|
1070 |
+
const sizePath = size === 'thumb' ? 'thumb/' : '';
|
1071 |
+
return `https://randomuser.me/api/portraits/${sizePath}${genderPath}/${imageIndex}.jpg`;
|
1072 |
}
|
1073 |
|
1074 |
function updateSelectedSpeakerDisplay(speakerId) {
|
1075 |
const speaker = getSpeakerById(speakerId);
|
1076 |
+
selectedSpeakerImgDisplay.src = getImageUrl(speaker, 'large');
|
|
|
1077 |
selectedSpeakerImgDisplay.alt = `عکس گوینده ${speaker.name}`;
|
1078 |
selectedSpeakerNameDisplay.textContent = speaker.name;
|
1079 |
selectedSpeakerDescDisplay.textContent = speaker.desc || "گوینده منتخب شما";
|
|
|
1082 |
|
1083 |
function createSpeakerCardsInModal() {
|
1084 |
speakerGridInModal.innerHTML = '';
|
1085 |
+
speakers.forEach((speaker) => {
|
1086 |
const cardLabel = document.createElement('label');
|
1087 |
cardLabel.className = 'speaker-card';
|
1088 |
cardLabel.setAttribute('for', `modal-speaker-${speaker.id}`);
|
|
|
1090 |
cardLabel.innerHTML = `
|
1091 |
<input type="radio" name="modal_speaker_selection" value="${speaker.id}" id="modal-speaker-${speaker.id}" ${isChecked}>
|
1092 |
<div class="speaker-visual">
|
1093 |
+
<img src="${getImageUrl(speaker, 'thumb')}" alt="${speaker.name}" loading="lazy">
|
1094 |
<div class="speaker-name">${speaker.name}</div>
|
1095 |
</div>
|
1096 |
`;
|
1097 |
+
// Attach click listener to the label itself, which implicitly selects the radio
|
1098 |
+
cardLabel.addEventListener('click', () => {
|
|
|
|
|
|
|
1099 |
updateSelectedSpeakerDisplay(speaker.id);
|
1100 |
hideModal(speakerModal);
|
1101 |
});
|
|
|
1103 |
});
|
1104 |
}
|
1105 |
|
1106 |
+
// --- MODAL MANAGEMENT ---
|
1107 |
function showModal(modalElement) {
|
1108 |
modalElement.classList.add('visible');
|
1109 |
+
// Set focus to the first focusable element for accessibility
|
1110 |
setTimeout(() => {
|
1111 |
const firstFocusable = modalElement.querySelector('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
1112 |
if (firstFocusable) firstFocusable.focus();
|
|
|
1120 |
}, { once: true });
|
1121 |
}
|
1122 |
|
1123 |
+
// --- AUDIO PLAYER VISUALIZATION (WAVEFORM) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1124 |
|
1125 |
+
// Function to draw waveform on canvas
|
1126 |
+
function drawWaveform(progressRatio = 0) {
|
1127 |
+
if (!audioWaveformCanvas || !waveformCtx) return;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1128 |
|
1129 |
+
const dpr = window.devicePixelRatio || 1; // Get device pixel ratio
|
1130 |
+
const canvasWidth = audioWaveformCanvas.offsetWidth * dpr;
|
1131 |
+
const canvasHeight = audioWaveformCanvas.offsetHeight * dpr;
|
1132 |
|
1133 |
+
audioWaveformCanvas.width = canvasWidth;
|
1134 |
+
audioWaveformCanvas.height = canvasHeight;
|
1135 |
|
1136 |
+
waveformCtx.clearRect(0, 0, canvasWidth, canvasHeight); // Clear canvas
|
|
|
|
|
|
|
|
|
|
|
1137 |
|
1138 |
+
if (audioPeaks.length === 0) return; // No peaks to draw
|
|
|
|
|
1139 |
|
1140 |
+
const barWidth = 3 * dpr;
|
1141 |
+
const barGap = 2 * dpr;
|
1142 |
const totalBarAndGap = barWidth + barGap;
|
1143 |
+
const numBars = Math.floor(canvasWidth / totalBarAndGap);
|
1144 |
+
const offset = (canvasWidth - (numBars * totalBarAndGap)) / 2; // Center bars
|
1145 |
|
1146 |
const activeBars = Math.floor(progressRatio * numBars);
|
1147 |
|
1148 |
for (let i = 0; i < numBars; i++) {
|
1149 |
+
const peakIndex = Math.floor((i / numBars) * audioPeaks.length);
|
1150 |
+
const peak = audioPeaks[peakIndex] || 0;
|
1151 |
+
const barHeight = peak * canvasHeight * 0.9; // Scale height slightly to prevent touching edges
|
1152 |
+
|
1153 |
+
waveformCtx.fillStyle = i < activeBars ?
|
1154 |
+
getComputedStyle(document.documentElement).getPropertyValue('--waveform-color-active').trim() :
|
1155 |
getComputedStyle(document.documentElement).getPropertyValue('--waveform-color-inactive').trim();
|
1156 |
|
1157 |
const x = offset + i * totalBarAndGap;
|
1158 |
+
const y = (canvasHeight - barHeight) / 2; // Center vertically
|
1159 |
|
1160 |
waveformCtx.fillRect(x, y, barWidth, barHeight);
|
1161 |
}
|
1162 |
}
|
1163 |
|
1164 |
+
// Process audio data for waveform visualization
|
1165 |
+
async function processAudioForWaveform(audioBuffer) {
|
1166 |
+
if (!audioBuffer) {
|
1167 |
+
audioPeaks = [];
|
1168 |
+
return;
|
1169 |
+
}
|
1170 |
+
|
1171 |
+
// Get mono audio data (or average stereo if available)
|
1172 |
+
const channelData = audioBuffer.numberOfChannels > 0 ? audioBuffer.getChannelData(0) : [];
|
1173 |
+
if (audioBuffer.numberOfChannels > 1) {
|
1174 |
+
const channelData2 = audioBuffer.getChannelData(1);
|
1175 |
+
for (let i = 0; i < channelData.length; i++) {
|
1176 |
+
channelData[i] = (channelData[i] + channelData2[i]) / 2;
|
1177 |
+
}
|
1178 |
+
}
|
1179 |
+
|
1180 |
+
const barTarget = 150; // Target number of bars for calculation, independent of canvas size
|
1181 |
+
const samplesPerBar = Math.floor(channelData.length / barTarget);
|
1182 |
+
const peaks = [];
|
1183 |
+
|
1184 |
+
for (let i = 0; i < barTarget; i++) {
|
1185 |
+
let maxPeak = 0;
|
1186 |
+
const start = i * samplesPerBar;
|
1187 |
+
const end = Math.min(start + samplesPerBar, channelData.length);
|
1188 |
+
|
1189 |
+
for (let j = start; j < end; j++) {
|
1190 |
+
const value = Math.abs(channelData[j]);
|
1191 |
+
if (value > maxPeak) {
|
1192 |
+
maxPeak = value;
|
1193 |
+
}
|
1194 |
+
}
|
1195 |
+
peaks.push(maxPeak); // Store raw peak
|
1196 |
+
}
|
1197 |
+
audioPeaks = peaks;
|
1198 |
+
drawWaveform(0); // Draw initial waveform
|
1199 |
+
}
|
1200 |
+
|
1201 |
// Update custom player UI (play/pause icon, time, progress)
|
1202 |
function updatePlayerUI() {
|
1203 |
if (hiddenAudioPlayer.paused || hiddenAudioPlayer.ended) {
|
1204 |
playIcon.style.display = 'block';
|
1205 |
pauseIcon.style.display = 'none';
|
1206 |
+
playPauseBtn.setAttribute('aria-label', 'پخش');
|
1207 |
} else {
|
1208 |
playIcon.style.display = 'none';
|
1209 |
pauseIcon.style.display = 'block';
|
1210 |
+
playPauseBtn.setAttribute('aria-label', 'مکث');
|
1211 |
}
|
1212 |
|
1213 |
const currentTime = hiddenAudioPlayer.currentTime;
|
1214 |
const duration = hiddenAudioPlayer.duration;
|
1215 |
|
1216 |
audioCurrentTimeSpan.textContent = formatTime(currentTime);
|
1217 |
+
audioWaveformCanvas.parentNode.setAttribute('aria-valuenow', isFinite(duration) && duration > 0 ? Math.floor((currentTime / duration) * 100) : 0);
|
1218 |
+
|
1219 |
|
1220 |
if (isFinite(duration) && duration > 0) {
|
1221 |
audioTotalTimeSpan.textContent = formatTime(duration);
|
1222 |
const progressRatio = currentTime / duration;
|
1223 |
+
drawWaveform(progressRatio);
|
1224 |
} else {
|
1225 |
audioTotalTimeSpan.textContent = '0:00';
|
1226 |
+
drawWaveform(0);
|
1227 |
}
|
1228 |
}
|
1229 |
|
1230 |
+
// --- STATE MANAGEMENT ---
|
1231 |
+
function showLoadingState() {
|
1232 |
+
audioPlayerContent.style.display = 'none';
|
1233 |
+
outputSection.classList.remove('has-content');
|
1234 |
+
statusMessage.style.display = 'none';
|
1235 |
+
loadingAnimationWrapper.style.display = 'flex';
|
1236 |
+
generateBtn.disabled = true;
|
1237 |
+
generateBtn.innerHTML = `
|
1238 |
+
<svg class="spinner" aria-hidden="true" role="status" fill="currentColor" viewBox="0 0 100 101">
|
1239 |
+
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB"/>
|
1240 |
+
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0492C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor"/>
|
1241 |
+
</svg>
|
1242 |
+
<span class="button-text">در حال پردازش...</span>
|
1243 |
+
`;
|
1244 |
+
}
|
1245 |
+
|
1246 |
+
function showResultState(isSuccess, message = '', audioUrl = null) {
|
1247 |
+
loadingAnimationWrapper.style.display = 'none';
|
1248 |
+
if (isSuccess) {
|
1249 |
+
statusMessage.style.display = 'none';
|
1250 |
+
audioPlayerContent.style.display = 'flex';
|
1251 |
+
outputSection.classList.add('has-content');
|
1252 |
+
hiddenAudioPlayer.src = audioUrl;
|
1253 |
+
hiddenAudioPlayer.load(); // Reload to trigger loadedmetadata
|
1254 |
+
// Play is handled by loadedmetadata listener after waveform processing
|
1255 |
+
} else {
|
1256 |
+
statusMessage.textContent = message || 'خطایی رخ داد. لطفاً دوباره تلاش کنید.';
|
1257 |
+
statusMessage.classList.add('error');
|
1258 |
+
statusMessage.style.display = 'block';
|
1259 |
+
audioPlayerContent.style.display = 'none';
|
1260 |
+
outputSection.classList.remove('has-content');
|
1261 |
+
hiddenAudioPlayer.pause();
|
1262 |
+
hiddenAudioPlayer.src = '';
|
1263 |
+
audioPeaks = []; // Clear waveform data
|
1264 |
+
drawWaveform(0); // Clear canvas
|
1265 |
+
audioBufferCache = null; // Clear cached buffer
|
1266 |
}
|
1267 |
+
generateBtn.disabled = false;
|
1268 |
+
generateBtn.innerHTML = '<span class="button-text">✨ تولید صدا با آلفا</span>';
|
1269 |
+
}
|
1270 |
|
1271 |
+
// --- SIMULATED BACKEND API CALL ---
|
1272 |
+
// THIS IS A MOCK FUNCTION. REPLACE IT WITH YOUR ACTUAL BACKEND API CALL!
|
1273 |
+
async function simulateBackendApiCall(text, prompt, speakerId, temperature) {
|
1274 |
+
console.log("Simulating API call with:", { text, prompt, speakerId, temperature });
|
1275 |
+
return new Promise((resolve, reject) => {
|
1276 |
+
const delay = 3000 + Math.random() * 2000; // Simulate network delay
|
1277 |
+
setTimeout(() => {
|
1278 |
+
if (Math.random() > 0.1) { // Simulate 90% success rate
|
1279 |
+
// In a real application, your backend would return the URL to the generated audio
|
1280 |
+
// after securely processing the request and potentially deducting credits.
|
1281 |
+
const mockAudioUrls = [
|
1282 |
+
"https://file-examples.com/storage/fe395e28a56247c413b5e43/2017/11/file_example_MP3_700KB.mp3",
|
1283 |
+
"https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3",
|
1284 |
+
"https://www.mfiles.co.uk/mp3-midi/test-mp3.mp3", // Shorter for quick test
|
1285 |
+
"https://www.kozco.com/tech/piano2-mp3.mp3"
|
1286 |
+
];
|
1287 |
+
const randomUrl = mockAudioUrls[Math.floor(Math.random() * mockAudioUrls.length)];
|
1288 |
+
resolve({ audioUrl: randomUrl, message: 'صدا با موفقیت تولید شد!' });
|
1289 |
+
} else {
|
1290 |
+
reject(new Error('متاسفانه، سرور در حال حاضر مشغول است یا خطایی رخ داده. لطفا لحظاتی دیگر تلاش کنید.'));
|
1291 |
+
}
|
1292 |
+
}, delay);
|
1293 |
+
});
|
1294 |
+
}
|
1295 |
|
1296 |
+
// --- EVENT HANDLERS ---
|
|
|
1297 |
|
1298 |
+
textInput.addEventListener('input', () => {
|
1299 |
+
const currentLength = textInput.value.length;
|
1300 |
+
charCountSpan.textContent = currentLength.toLocaleString('fa-IR');
|
|
|
|
|
1301 |
|
1302 |
+
if (currentLength > MAX_CHARS) {
|
1303 |
+
charCountSpan.classList.add('error');
|
1304 |
+
charErrorMessage.textContent = `متن شما ${currentLength - MAX_CHARS} نویسه بیشتر از حد مجاز است!`;
|
1305 |
+
charErrorMessage.style.display = 'inline';
|
1306 |
+
generateBtn.disabled = true; // Disable button if over limit
|
1307 |
+
} else {
|
1308 |
+
charCountSpan.classList.remove('error');
|
1309 |
+
charErrorMessage.style.display = 'none';
|
1310 |
+
generateBtn.disabled = false; // Re-enable if within limit (and other conditions met)
|
1311 |
+
}
|
1312 |
+
// Also check if text is empty to disable button
|
1313 |
+
if (currentLength === 0) {
|
1314 |
+
generateBtn.disabled = true;
|
1315 |
+
}
|
1316 |
+
});
|
1317 |
+
|
1318 |
+
tempSlider.addEventListener('input', () => { tempValueSpan.textContent = tempSlider.value; });
|
1319 |
+
|
1320 |
+
changeSpeakerBtn.addEventListener('click', () => {
|
1321 |
+
createSpeakerCardsInModal();
|
1322 |
+
showModal(speakerModal);
|
1323 |
+
});
|
1324 |
+
selectedSpeakerCard.addEventListener('click', () => {
|
1325 |
+
createSpeakerCardsInModal();
|
1326 |
+
showModal(speakerModal);
|
1327 |
+
});
|
1328 |
+
|
1329 |
+
tempInfoIcon.addEventListener('click', () => showModal(infoModal));
|
1330 |
+
tempInfoIcon.addEventListener('keydown', (e) => {
|
1331 |
+
if (e.key === 'Enter' || e.key === ' ') {
|
1332 |
+
e.preventDefault();
|
1333 |
+
showModal(infoModal);
|
1334 |
+
}
|
1335 |
+
});
|
1336 |
+
|
1337 |
+
// General Modal Close Listeners (for all modals)
|
1338 |
+
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
1339 |
+
overlay.addEventListener('click', (e) => {
|
1340 |
+
if (e.target === overlay) {
|
1341 |
+
hideModal(overlay);
|
1342 |
}
|
1343 |
+
});
|
1344 |
+
});
|
1345 |
+
document.querySelectorAll('.close-modal-btn').forEach(button => {
|
1346 |
+
button.addEventListener('click', () => {
|
1347 |
+
const modalId = button.dataset.modalId;
|
1348 |
+
if (modalId) {
|
1349 |
+
hideModal(document.getElementById(modalId));
|
1350 |
+
}
|
1351 |
+
});
|
1352 |
+
});
|
1353 |
+
document.addEventListener('keydown', (e) => {
|
1354 |
+
if (e.key === 'Escape') {
|
1355 |
+
document.querySelectorAll('.modal-overlay.visible').forEach(hideModal);
|
1356 |
}
|
1357 |
+
});
|
|
|
|
|
1358 |
|
1359 |
+
// Audio Player Controls
|
1360 |
playPauseBtn.addEventListener('click', () => {
|
1361 |
if (hiddenAudioPlayer.paused) {
|
1362 |
hiddenAudioPlayer.play();
|
|
|
1374 |
hiddenAudioPlayer.muted = !hiddenAudioPlayer.muted;
|
1375 |
volumeHighIcon.style.display = hiddenAudioPlayer.muted ? 'none' : 'block';
|
1376 |
volumeMuteIcon.style.display = hiddenAudioPlayer.muted ? 'block' : 'none';
|
1377 |
+
volumeBtn.setAttribute('aria-label', hiddenAudioPlayer.muted ? 'فعال کردن صدا' : 'بیصدا کردن صدا');
|
1378 |
});
|
1379 |
speedBtn.addEventListener('click', () => {
|
1380 |
currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length;
|
|
|
1383 |
speedBtn.textContent = `${newSpeed}x`;
|
1384 |
});
|
1385 |
|
1386 |
+
// Audio Player Events
|
1387 |
hiddenAudioPlayer.addEventListener('timeupdate', updatePlayerUI);
|
1388 |
hiddenAudioPlayer.addEventListener('loadedmetadata', async () => {
|
1389 |
+
// Ensure AudioContext is initialized for waveform processing
|
1390 |
if (!audioContext) {
|
1391 |
audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
1392 |
}
|
1393 |
+
if (!audioBufferCache) { // Only decode if not already cached
|
1394 |
+
try {
|
1395 |
+
const response = await fetch(hiddenAudioPlayer.src);
|
1396 |
+
if (!response.ok) throw new Error(`Failed to fetch audio: ${response.statusText}`);
|
1397 |
+
const arrayBuffer = await response.arrayBuffer();
|
1398 |
+
audioBufferCache = await audioContext.decodeAudioData(arrayBuffer);
|
1399 |
+
await processAudioForWaveform(audioBufferCache);
|
1400 |
+
} catch (e) {
|
1401 |
+
console.error("Error decoding audio for waveform:", e);
|
1402 |
+
audioPeaks = [];
|
1403 |
+
audioBufferCache = null;
|
1404 |
+
statusMessage.textContent = 'خطا در بارگذاری موج صوتی.';
|
1405 |
+
statusMessage.classList.add('error');
|
1406 |
+
}
|
1407 |
+
} else {
|
1408 |
+
await processAudioForWaveform(audioBufferCache); // Redraw if cached
|
1409 |
}
|
1410 |
updatePlayerUI(); // Update UI with total duration and initial state
|
1411 |
+
if (!hiddenAudioPlayer.paused) { // If it was playing when metadata loaded
|
1412 |
+
hiddenAudioPlayer.play().catch(e => console.error("Autoplay failed:", e)); // Attempt to play
|
1413 |
+
}
|
1414 |
});
|
1415 |
hiddenAudioPlayer.addEventListener('play', updatePlayerUI);
|
1416 |
hiddenAudioPlayer.addEventListener('pause', updatePlayerUI);
|
|
|
1424 |
window.addEventListener('resize', () => {
|
1425 |
clearTimeout(resizeTimeout);
|
1426 |
resizeTimeout = setTimeout(() => {
|
1427 |
+
if (outputSection.classList.contains('has-content') && audioBufferCache) {
|
1428 |
+
// Re-draw waveform if audio is loaded and output section is visible
|
1429 |
+
drawWaveform(hiddenAudioPlayer.currentTime / hiddenAudioPlayer.duration);
|
|
|
|
|
|
|
1430 |
}
|
1431 |
}, 250); // Debounce resize events
|
1432 |
});
|
1433 |
|
1434 |
|
1435 |
+
// --- FORM SUBMISSION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1436 |
async function generateAudio(event) {
|
1437 |
event.preventDefault();
|
|
|
1438 |
|
1439 |
+
const text = textInput.value.trim();
|
1440 |
+
if (!text) {
|
1441 |
+
showResultState(false, 'لطفاً متن مورد نظر خود را وارد کنید.');
|
1442 |
return;
|
1443 |
}
|
1444 |
if (text.length > MAX_CHARS) {
|
1445 |
showResultState(false, `خطا: طول متن بیش از ${MAX_CHARS.toLocaleString('fa-IR')} نویسه است.`);
|
1446 |
return;
|
1447 |
}
|
1448 |
+
// Simulate credit deduction
|
1449 |
+
if (currentCredits < text.length) {
|
1450 |
+
showResultState(false, `اعتبار شما برای این درخواست کافی نیست. شما ${currentCredits.toLocaleString('fa-IR')} کاراکتر و این متن ${text.length.toLocaleString('fa-IR')} کاراکتر است.`);
|
1451 |
+
return;
|
1452 |
+
}
|
1453 |
|
1454 |
+
showLoadingState();
|
1455 |
+
statusMessage.classList.remove('error'); // Clear previous error styling
|
1456 |
+
|
1457 |
+
const promptVal = promptInput.value.trim();
|
1458 |
const temperatureVal = parseFloat(tempSlider.value);
|
1459 |
const selectedSpeakerVal = selectedSpeakerIdStorage.value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1460 |
|
1461 |
try {
|
1462 |
+
// Call the simulated backend API
|
1463 |
+
const result = await simulateBackendApiCall(text, promptVal, selectedSpeakerVal, temperatureVal);
|
|
|
|
|
|
|
1464 |
|
1465 |
+
// If successful, deduct credits (mock)
|
1466 |
+
currentCredits -= text.length;
|
1467 |
+
creditsLeftSpan.textContent = currentCredits.toLocaleString('fa-IR');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1468 |
|
1469 |
+
showResultState(true, 'صدا با موفقیت تولید شد!', result.audioUrl);
|
1470 |
} catch (error) {
|
1471 |
console.error('خطا در فرآیند تولید صدا:', error);
|
1472 |
+
showResultState(false, `${error.message || 'خطای ناشناخته در تولید صدا.'}`);
|
1473 |
}
|
1474 |
}
|
1475 |
|
1476 |
// Initial setup
|
1477 |
updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || speakers[0].id);
|
1478 |
form.addEventListener('submit', generateAudio);
|
1479 |
+
textInput.dispatchEvent(new Event('input')); // Trigger char counter on load
|
1480 |
+
showResultState(false, 'صدای تولید شده در اینجا نمایش داده خواهد شد.');
|
|
|
|
|
1481 |
});
|
1482 |
</script>
|
1483 |
</body>
|