| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>تولید صدای هوشمند و پادکست | AI Sada</title> |
| <meta name="description" content="با AI Sada، متن فارسی خود را به صدایی طبیعی و با کیفیت استودیویی تبدیل کنید."> |
| |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;600;700;800;900&display=swap'); |
| |
| :root { |
| --app-font: 'Vazirmatn', sans-serif; --app-bg: #F8F9FC; --panel-bg: #FFFFFF; |
| --panel-border: #EAEFF7; --input-bg: #F6F8FB; --input-border: #E1E7EF; |
| --text-primary: #1A202C; --text-secondary: #626F86; --text-tertiary: #8A94A6; |
| --accent-primary: #4A6CFA; --accent-primary-hover: #3553D6; --accent-primary-glow: rgba(74, 108, 250, 0.25); |
| --accent-secondary: #0FD4A8; --accent-secondary-hover: #0DA986; --accent-secondary-glow: rgba(15, 212, 168, 0.2); |
| --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03); --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04); |
| --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05); |
| --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05); |
| --radius-card: 24px; --radius-btn: 14px; --radius-input: 12px; |
| --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); |
| --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); |
| --card-bg: #FFFFFF; |
| } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes fadeInOut { 0% { opacity: 0; transform: translateY(10px); } 10% { opacity: 1; transform: translateY(0); } 90% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-10px); } } |
| @keyframes modalZoomIn { from { opacity: 0; transform: scale(0.9) translateY(20px); } to { opacity: 1; transform: scale(1) translateY(0); } } |
| @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes rotate-loader-orbital { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes orbit-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes satellite-pulse-1 { from { transform: scale(0.7) translateX(-50%); opacity: 0.6; } to { transform: scale(1.1) translateX(-50%); opacity: 1; } } |
| @keyframes satellite-pulse-2 { from { transform: scale(0.7) translateY(-50%); opacity: 0.6; } to { transform: scale(1.1) translateY(-50%); opacity: 1; } } |
| @keyframes satellite-pulse-3 { from { transform: scale(0.7) translateX(50%); opacity: 0.6; } to { transform: scale(1.1) translateX(50%); opacity: 1; } } |
| @keyframes pulse-soft { 0%, 100% { opacity: 1; } 50% { opacity: 0.7; } } |
| @keyframes bounce { 0%, 20%, 50%, 80%, 100% {transform: translateY(0);} 40% {transform: translateY(-15px);} 60% {transform: translateY(-7px);} } |
| |
| |
| .podcast-studio-container { width: 100%; animation: fadeIn 0.5s ease-out; } |
| .podcast-studio-container .form-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; } |
| .podcast-studio-container #project-speakers-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1.5rem; text-align: center; } |
| .podcast-studio-container .speaker-display-card { position: relative; background-color: var(--input-bg); border-radius: var(--radius-card); padding: 1.5rem 1rem; border: 2px solid var(--input-border); transition: var(--transition-smooth); text-align: center; } |
| .podcast-studio-container .speaker-display-card:hover:not(#add-speaker-card) { border-color: var(--accent-primary); transform: translateY(-5px); box-shadow: var(--shadow-md); } |
| .podcast-studio-container .speaker-display-card img { width: 80px; height: 80px; border-radius: 50%; object-fit: cover; margin: 0 auto 1rem auto; display: block; border: 4px solid var(--panel-bg); box-shadow: var(--shadow-md); } |
| .podcast-studio-container .speaker-display-card h3 { margin: 0 0 0.25rem 0; font-size: 1.1em; font-weight: 800; } |
| .podcast-studio-container .remove-speaker-btn { position: absolute; top: 12px; left: 12px; background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(4px); border: 1px solid var(--panel-border); color: var(--text-secondary); width: 28px; height: 28px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; line-height: 1; transition: all 0.2s; z-index: 5; } |
| .podcast-studio-container .remove-speaker-btn:hover { color: #e53e3e; transform: scale(1.1); background: #fff; } |
| .podcast-studio-container #add-speaker-card { display: flex; flex-direction: column; align-items: center; justify-content: center; font-weight: 700; color: var(--accent-primary); border: 2px dashed var(--input-border); cursor: pointer; background-color: transparent; } |
| .podcast-studio-container #add-speaker-card:hover { border-color: var(--accent-primary); background: var(--accent-primary-glow); } |
| .podcast-studio-container #add-speaker-card .plus-icon { font-size: 2.5rem; line-height: 1; margin-bottom: 0.5rem; } |
| .podcast-studio-container #podcast-script-container { border: 1px solid var(--input-border); border-radius: var(--radius-card); padding: 1rem; background-color: var(--input-bg); box-shadow: inset 0 2px 4px rgba(0,0,0,0.02); } |
| .podcast-studio-container .script-turn { display: flex; gap: 1rem; margin-bottom: 1.5rem; background-color: var(--panel-bg); padding: 1rem; border-radius: var(--radius-input); border: 1px solid var(--panel-border); box-shadow: var(--shadow-md); } |
| .podcast-studio-container .script-turn:last-child { margin-bottom: 0; } |
| .podcast-studio-container .turn-speaker-selector { flex-shrink: 0; width: 180px; position: relative; } |
| .podcast-studio-container .turn-content { flex-grow: 1; } |
| .podcast-studio-container .turn-content textarea { width: 100%; min-height: 80px; resize: vertical; padding: 0.8rem 1rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: #fff; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; } |
| .podcast-studio-container .turn-content textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow); } |
| .podcast-studio-container .remove-turn-btn { flex-shrink: 0; background: none; border: none; color: var(--text-tertiary); font-size: 1.5rem; cursor: pointer; align-self: center; transition: all 0.2s; } |
| .podcast-studio-container .remove-turn-btn:hover { color: #e53e3e; transform: scale(1.1); } |
| .podcast-studio-container #add-turn-btn { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; padding: 1rem; font-size: 1em; font-weight: 700; background: transparent; color: var(--text-primary); border: 2px dashed var(--input-border); border-radius: var(--radius-btn); cursor: pointer; margin-top: 1.5rem; transition: var(--transition-smooth); } |
| .podcast-studio-container #add-turn-btn:hover { background: var(--panel-bg); border-color: var(--accent-primary); color: var(--accent-primary); } |
| .podcast-studio-container .custom-select-trigger { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: #fff; cursor: pointer; transition: var(--transition-smooth); height: 100%; box-sizing: border-box; } |
| .podcast-studio-container .custom-select-trigger:hover { border-color: var(--accent-primary); } |
| .podcast-studio-container .custom-select-trigger img { width: 40px; height: 40px; border-radius: 10px; object-fit: cover; } |
| .podcast-studio-container .custom-select-trigger span { font-weight: 600; flex-grow: 1; } |
| .podcast-studio-container .custom-select-trigger .arrow { margin-right: auto; width: 20px; height: 20px; color: var(--text-tertiary); transition: transform 0.2s; } |
| .podcast-studio-container .custom-select-container.open .arrow { transform: rotate(180deg); } |
| .podcast-studio-container .custom-select-options { position: absolute; top: calc(100% + 8px); left: 0; width: 100%; background: var(--panel-bg); border-radius: var(--radius-input); border: 1px solid var(--panel-border); box-shadow: var(--shadow-lg); z-index: 10; max-height: 250px; overflow-y: auto; opacity: 0; transform: translateY(10px); pointer-events: none; transition: all 0.25s ease-out; } |
| .podcast-studio-container .custom-select-container.open .custom-select-options { opacity: 1; transform: translateY(0); pointer-events: auto; } |
| .podcast-studio-container .custom-select-option { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; cursor: pointer; } |
| .podcast-studio-container .custom-select-option:hover { background-color: var(--input-bg); } |
| .podcast-studio-container .custom-select-option img { width: 40px; height: 40px; border-radius: 10px; object-fit: cover; } |
| .podcast-studio-container .custom-select-option span { font-weight: 500; } |
| .podcast-studio-container #generation-progress-container { width: 100%; max-width: 500px; margin-top: 1rem; } |
| .podcast-studio-container .progress-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 10px; } |
| .podcast-studio-container .progress-item { width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 8px; background-color: #EDF2F7; border: 1px solid #E2E8F0; font-size: 0.8em; font-weight: 700; color: #718096; transition: all 0.3s ease; } |
| .podcast-studio-container .progress-item.pending { background-color: #EDF2F7; animation: pulse-gray 1.5s infinite; } |
| @keyframes pulse-gray { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } |
| .podcast-studio-container .progress-item.done { background-color: var(--accent-secondary); color: white; border-color: var(--accent-secondary); transform: scale(1.1); box-shadow: 0 2px 5px var(--accent-secondary-glow); } |
| .podcast-studio-container .progress-item.error { background-color: #FC8181; color: white; border-color: #F56565; transform: scale(1.1); } |
| .podcast-studio-container .turn-player-container { display: none; align-items: center; gap: 1rem; padding: 0.75rem; margin-top: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); border: 1px solid var(--panel-border); position: relative; } |
| .podcast-studio-container .turn-player-container.visible { display: flex; } |
| .podcast-studio-container .turn-play-btn { background: var(--accent-primary); color: white; border: none; width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: var(--transition-smooth); flex-shrink: 0; } |
| .podcast-studio-container .turn-play-btn:hover { background: var(--accent-primary-hover); transform: scale(1.1); } |
| .podcast-studio-container .turn-play-btn svg { width: 20px; height: 20px; fill: currentColor; flex-shrink: 0; } |
| .podcast-studio-container .turn-play-btn .play-icon { margin-left: 2px; } |
| .podcast-studio-container .turn-play-btn .pause-icon { display: none; } |
| .podcast-studio-container .turn-player-container.playing .turn-play-btn .play-icon { display: none; } |
| .podcast-studio-container .turn-player-container.playing .turn-play-btn .pause-icon { display: block; } |
| .podcast-studio-container .loading-state-wrapper { display: flex; align-items: center; gap: 6px; margin-right: auto; } |
| .podcast-studio-container .loading-state-wrapper .spinner { width: 18px; height: 18px; border: 2px solid var(--text-tertiary); border-top-color: var(--accent-primary); border-radius: 50%; animation: spin 0.8s linear infinite; display: inline-block; } |
| .podcast-studio-container .loading-state-wrapper .loading-text { font-size: 0.85em; font-weight: 600; color: var(--accent-primary); white-space: nowrap; transition: all 0.3s; } |
| .podcast-studio-container .turn-retry-btn { font-family: var(--app-font); font-size: 0.85em; font-weight: 600; background: none; border: 1px solid var(--input-border); color: var(--text-secondary); padding: 0.4rem 0.8rem; border-radius: var(--radius-btn); cursor: pointer; transition: var(--transition-smooth); margin-right: auto; display: flex; align-items: center; gap: 6px; } |
| .podcast-studio-container .turn-retry-btn:hover { border-color: var(--accent-secondary); color: var(--accent-secondary); background: var(--accent-secondary-glow); } |
| .podcast-studio-container .replace-success-message, .podcast-studio-container .main-update-message { font-family: var(--app-font); font-size: 0.85em; font-weight: 600; padding: 0.4rem 0.8rem; border-radius: var(--radius-btn); white-space: nowrap; display: none; align-items: center; justify-content: center; margin-right: auto; } |
| .podcast-studio-container .replace-success-message { background-color: var(--accent-secondary-glow); color: var(--accent-secondary); } |
| .podcast-studio-container .main-update-message { background: linear-gradient(90deg, #e6fffa, #b2f5ea); color: #2c7a7b; border: 1px solid #81e6d9; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } |
| |
| .beautiful-download-btn { display: flex; align-items: center; justify-content: center; gap: 12px; width: 100%; max-width: 400px; margin: 1.5rem auto 0.5rem auto; padding: 16px 24px; background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%); color: #fff; border: none; border-radius: 14px; font-family: var(--app-font); font-weight: 800; font-size: 1.1em; cursor: pointer; position: relative; overflow: hidden; box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.1), 0 2px 4px -1px rgba(37, 99, 235, 0.06); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); border-bottom: 4px solid #1d4ed8; } |
| .beautiful-download-btn:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 15px -3px rgba(37, 99, 235, 0.2), 0 4px 6px -2px rgba(37, 99, 235, 0.1); filter: brightness(1.05); } |
| .beautiful-download-btn:active:not(:disabled) { transform: translateY(2px); border-bottom-width: 0px; margin-top: calc(1.5rem + 4px); } |
| |
| #clear-history-btn-podcast { display: flex; align-items: center; justify-content: center; gap: 8px; width: 100%; max-width: 300px; margin: 1rem auto 0 auto; padding: 12px; background-color: #fff; border: 2px dashed #cbd5e0; color: #718096; font-family: var(--app-font); font-size: 0.95em; font-weight: 700; border-radius: 12px; cursor: pointer; transition: all 0.3s ease; } |
| #clear-history-btn-podcast:hover { border-color: #fc8181; color: #e53e3e; background-color: #fff5f5; transform: translateY(-2px); } |
| #clear-history-btn-podcast svg { width: 18px; height: 18px; fill: currentColor; } |
| |
| @media (max-width: 768px) { .podcast-studio-container .script-turn { flex-direction: column; } .podcast-studio-container .remove-turn-btn { align-self: flex-end; margin-top: -1.5rem; margin-right: -0.5rem; } .podcast-studio-container .turn-speaker-selector { width: 100%; } } |
| |
| |
| #long-text-modal .modal-dialog { max-width: 600px; background: linear-gradient(160deg, #ffffff 0%, #f7f9fc 100%); padding: 3rem 2.5rem; max-height: 90vh; overflow-y: auto; } |
| #long-text-modal .modal-header { border: none; justify-content: center; flex-direction: column; text-align: center; margin-bottom: 1.5rem; } |
| #long-text-modal h2 { font-size: 1.8rem; background: linear-gradient(90deg, #4A6CFA, #0FD4A8); -webkit-background-clip: text; -webkit-text-fill-color: transparent; margin-bottom: 0.5rem; } |
| #long-text-modal .header-icon { font-size: 3.5rem; margin-bottom: 1rem; animation: bounce 2s infinite; } |
| #long-text-modal p.desc { color: var(--text-secondary); font-size: 1rem; line-height: 1.8; margin-bottom: 1.5rem; text-align: center; } |
| .long-text-options { display: grid; grid-template-columns: 1fr; gap: 12px; } |
| .option-card { display: flex; align-items: center; background: #fff; border: 2px solid #e2e8f0; border-radius: 16px; padding: 12px; cursor: pointer; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); gap: 12px; } |
| .option-card:hover { border-color: var(--accent-primary); transform: translateY(-3px); box-shadow: 0 8px 16px rgba(74, 108, 250, 0.1); } |
| .option-icon { width: 45px; height: 45px; background: #edf2f7; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 1.4rem; color: var(--text-secondary); } |
| .option-content { flex-grow: 1; text-align: right; } |
| .option-title { font-weight: 800; font-size: 1rem; color: var(--text-primary); margin-bottom: 2px; } |
| .option-subtitle { font-size: 0.85rem; color: var(--text-secondary); line-height: 1.4; } |
| .option-arrow { color: var(--text-tertiary); transition: transform 0.3s ease; font-size: 1.2rem; } .option-card:hover .option-arrow { color: var(--accent-primary); transform: translateX(-5px); } |
| .special-ai-card { background: linear-gradient(135deg, #f3f4f6 0%, #ffffff 100%); border-color: #d1d5db; box-shadow: 0 4px 6px rgba(0,0,0,0.05); } |
| .special-ai-card:hover { border-color: #8b5cf6; background: linear-gradient(135deg, #f5f3ff 0%, #ffffff 100%); box-shadow: 0 10px 20px rgba(139, 92, 246, 0.15); } |
| .special-ai-card .option-icon { background: #ede9fe; color: #8b5cf6; } |
| .special-ai-card .option-title { background: linear-gradient(90deg, #7c3aed, #4f46e5); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } |
| |
| .confirm-modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(18, 24, 38, 0.6); backdrop-filter: blur(8px); display: none; align-items: center; justify-content: center; z-index: 1001; opacity: 0; transition: opacity 0.3s ease; } |
| .confirm-modal-overlay.visible { display: flex; opacity: 1; } |
| .confirm-modal-dialog { background: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); width: 90%; max-width: 450px; box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); text-align: center; transform: scale(0.95); transition: transform 0.3s; } |
| .confirm-modal-overlay.visible .confirm-modal-dialog { transform: scale(1); } |
| .confirm-modal-dialog h3 { font-size: 1.5em; font-weight: 800; margin: 0 0 1rem 0; color: var(--text-primary); } |
| .confirm-modal-dialog p { color: var(--text-secondary); margin: 0 0 2rem 0; line-height: 1.7; } |
| .confirm-modal-actions { display: flex; gap: 1rem; justify-content: center; } |
| .confirm-btn, .cancel-btn { font-family: var(--app-font); font-size: 1em; font-weight: 700; padding: 0.8rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; transition: all 0.2s ease; flex-grow: 1; } |
| .confirm-btn { background-color: #e53e3e; color: white; box-shadow: 0 4px 10px -2px rgba(229, 62, 62, 0.4); } |
| .confirm-btn:hover { background-color: #c53030; transform: translateY(-2px); box-shadow: 0 6px 14px -3px rgba(229, 62, 62, 0.5); } |
| .cancel-btn { background-color: var(--accent-primary); color: white; box-shadow: 0 4px 10px -2px rgba(74, 108, 250, 0.4); } |
| .cancel-btn:hover { background-color: var(--accent-primary-hover); transform: translateY(-2px); box-shadow: 0 6px 14px -3px rgba(74, 108, 250, 0.5); } |
| |
| .ai-podcast-card { grid-column: 1 / -1; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 18px !important; display: flex !important; flex-direction: row !important; align-items: center !important; justify-content: space-between !important; padding: 0 1.5rem !important; margin-top: 10px; cursor: pointer; min-height: 90px; } |
| .ai-podcast-card:hover { transform: translateY(-5px) scale(1.02) !important; box-shadow: 0 15px 30px rgba(118, 75, 162, 0.6) !important; } |
| .ai-podcast-content { text-align: right; } |
| .ai-podcast-title { font-size: 1.3rem; font-weight: 900; color: white; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; } |
| .ai-podcast-subtitle { font-size: 0.9rem; color: rgba(255,255,255,0.9); font-weight: 500; } |
| .ai-podcast-icon { font-size: 2.5rem; margin-left: 1rem; animation: pulse-soft 2s infinite; } |
| .ai-arrow-icon { width: 30px; height: 30px; background: rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white; transition: 0.3s; } |
| .ai-podcast-card:hover .ai-arrow-icon { background: white; color: #764ba2; transform: translateX(-5px); } |
| |
| #ai-podcast-modal .modal-header h2 { background: linear-gradient(90deg, #FF0080, #7928CA); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-size: 1.8rem; } |
| #ai-podcast-input { width: 100%; min-height: 180px; padding: 1.2rem; border-radius: 16px; border: 2px solid var(--input-border); background-color: var(--input-bg); font-family: var(--app-font); font-size: 1rem; line-height: 1.8; resize: vertical; transition: all 0.3s; margin: 1.5rem 0; } |
| #ai-podcast-input:focus { outline: none; border-color: #7928CA; background-color: white; box-shadow: 0 0 0 4px rgba(121, 40, 202, 0.1); } |
| |
| .sample-player-container { position: absolute; top: 50%; left: 4px; transform: translateY(-50%); display: flex; flex-direction: column; gap: 4px; z-index: 6; } |
| .sample-play-btn { width: 24px; height: 24px; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0; background: linear-gradient(145deg, rgba(255, 255, 255, 0.25), rgba(0, 0, 0, 0.25)); border: 1px solid rgba(255, 255, 255, 0.15); } |
| .sample-play-btn .play-icon-svg { width: 11px; height: 11px; fill: #fff; } |
| .sample-play-btn .playing-indicator { display: none; width: 10px; height: 8px; justify-content: space-between; align-items: flex-end; } |
| .sample-play-btn .playing-indicator span { width: 2px; height: 100%; background-color: #fff; border-radius: 1px; animation: sound-wave 1.2s ease-in-out infinite; } |
| .sample-play-btn.playing { background: var(--accent-secondary); } |
| .sample-play-btn.playing .play-icon-svg { display: none; } |
| .sample-play-btn.playing .playing-indicator { display: flex; } |
| |
| #sample-text-display { position: fixed; bottom: 25px; left: 50%; width: 90%; max-width: 600px; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.6); border-radius: 20px; padding: 20px 24px; box-shadow: 0 20px 40px -10px rgba(0, 0, 0, 0.15); z-index: 2000; text-align: right; direction: rtl; display: flex; flex-direction: column; align-items: flex-start; opacity: 0; visibility: hidden; transform: translateX(-50%) translateY(30px) scale(0.95); transition: all 0.5s; } |
| #sample-text-display.active { opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0) scale(1); } |
| #sample-text-display h4 { margin: 0 0 10px; color: var(--accent-primary); font-size: 0.8em; font-weight: 800; display: flex; align-items: center; gap: 8px; } |
| #sample-text-display h4::before { content: '\f10d'; font-family: 'Font Awesome 6 Free'; font-weight: 900; font-size: 1.2em; opacity: 0.7; } |
| #sample-text-display p { margin: 0; color: var(--text-primary); font-weight: 500; font-size: 0.9em; line-height: 1.7; } |
| |
| #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 1.5rem; padding-bottom: 0; transition: padding-bottom 0.4s; } |
| .speaker-card { cursor: pointer; transition: var(--transition-smooth); text-align: center; position: relative; border-radius: var(--radius-card); padding: 0.5rem; } |
| .speaker-card:hover { transform: translateY(-8px); } |
| .speaker-card input[type="radio"] { display: none; } |
| .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: 18px; overflow: hidden; position: relative; background-color: var(--input-bg); height: 130px; width: 100%; transition: all 0.3s; } |
| .speaker-card img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 14px; transition: transform 0.4s; } |
| .speaker-card:hover img { transform: scale(1.1); } |
| .speaker-card .speaker-name { padding: 0.8rem 0.4rem 0.2rem; font-weight: 600; font-size: 0.95em; color: var(--text-secondary); } |
| .speaker-card input[type="radio"]:checked + .speaker-visual { border-color: var(--accent-secondary); box-shadow: 0 0 25px -5px var(--accent-secondary-glow); transform: scale(1.05); } |
| .speaker-card.already-selected { opacity: 0.5; cursor: not-allowed; } |
| |
| html { scroll-behavior: smooth; } |
| body { font-family: var(--app-font); direction: rtl; background-color: var(--app-bg); color: var(--text-primary); font-size: 16px; line-height: 1.8; margin: 0; padding: 0; min-height: 100vh; -webkit-font-smoothing: antialiased; } |
| .page-wrapper { max-width: 820px; width: 92%; margin: 0 auto; padding: 2.5rem 0; } |
| .app-container { max-width: 820px; width: 100%; margin: 0 auto; margin-bottom: 5rem; } |
| .app-header { padding: 0.5rem 0 2.5rem 0; text-align: center; margin-bottom: 1.5rem; animation: fadeIn 0.8s 0.2s ease-out backwards; } |
| .app-header h1 { font-size: 2.5em; font-weight: 900; margin: 0 0 0.8rem 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; } |
| .app-header p { font-size: 1.05em; color: var(--text-secondary); margin-top: 0; opacity: 0.9; font-weight: 400; line-height: 1.7; } |
| .main-content { position:relative; padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.4s ease-out backwards; } |
| .form-group { margin-bottom: 2.2rem; } |
| label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.1em; margin-bottom: 1rem; } |
| textarea, input[type="text"] { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); } |
| textarea:focus, input[type="text"]:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow); background-color: var(--panel-bg); } |
| textarea { min-height: 120px; resize: vertical; } |
| |
| .generate-btn { display: flex; align-items: center; justify-content: center; gap: 10px; width: 100%; padding: 1.1rem 1.5rem; font-size: 1.25em; font-weight: 800; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff; border: none; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.3s ease; box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px var(--accent-secondary-glow); position: relative; overflow: hidden; letter-spacing: 0.5px; } |
| .generate-btn:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); } |
| .generate-btn:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; color: rgba(255,255,255,0.7); } |
| .generate-btn .spinner { width: 20px; height: 20px; border: 3px solid rgba(255, 255, 255, 0.4); border-top-color: #fff; border-radius: 50%; animation: spin 0.8s linear infinite; display: none;} |
| |
| .output-section { margin-top: 3rem; display: flex; align-items: center; justify-content: center; flex-direction: column; min-height: 220px; position: relative; box-sizing: border-box; padding: 2rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); transition: var(--transition-smooth); } |
| .output-section.has-content { background-color: var(--panel-bg); border: 1px solid var(--panel-border); box-shadow: var(--shadow-lg); padding: 0; min-height: auto; } |
| .status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; } |
| .status-message.error { color: #e53e3e; background-color: #fed7d7; padding: 1rem; border-radius: var(--radius-input); border: 1px solid #f56565; } |
| .loading-animation-wrapper { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 1.8rem; width: 100%; } |
| .orbital-loader { width: 110px; height: 110px; position: relative; animation: rotate-loader-orbital 10s linear infinite; } |
| .orbit { position: absolute; top: 50%; left: 50%; border: 2px dashed rgba(74, 108, 250, 0.35); border-radius: 50%; transform-origin: center center; } |
| .orbit:nth-child(1) { width: 35px; height: 35px; margin: -17.5px 0 0 -17.5px; animation: orbit-spin 2.8s linear infinite reverse; } |
| .orbit:nth-child(2) { width: 65px; height: 65px; margin: -32.5px 0 0 -32.5px; animation: orbit-spin 3.8s linear infinite; } |
| .orbit:nth-child(3) { width: 95px; height: 95px; margin: -47.5px 0 0 -47.5px; animation: orbit-spin 4.8s linear infinite reverse; } |
| .orbit .satellite { position: absolute; width: 10px; height: 10px; border-radius: 50%; background-color: var(--accent-primary); box-shadow: 0 0 8px var(--accent-primary), 0 0 12px var(--accent-secondary); } |
| .orbit:nth-child(1) .satellite { top: -5px; left: 50%; animation: satellite-pulse-1 1.4s ease-in-out infinite alternate; } |
| .orbit:nth-child(2) .satellite { top: 50%; left: -5px; background-color: var(--accent-secondary); animation: satellite-pulse-2 1.4s 0.2s ease-in-out infinite alternate; } |
| .orbit:nth-child(3) .satellite { bottom: -5px; right: 50%; animation: satellite-pulse-3 1.4s 0.4s ease-in-out infinite alternate;} |
| .loading-text { font-size: 1.2em; font-weight: 700; color: var(--text-primary); text-align: center; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } |
| .audio-player-content { display: none; width: 100%; animation: fadeIn 0.5s ease-out; } |
| |
| .simple-player-container { display: flex; flex-direction: column; align-items: center; gap: 1rem; width: 100%; padding: 1.5rem; box-sizing: border-box; background-color: var(--input-bg); border-radius: var(--radius-input); border: 1px solid var(--panel-border); margin-top: 10px;} |
| .play-controls { display: flex; align-items: center; gap: 1rem; width: 100%; } |
| .play-pause-btn-simple { background: linear-gradient(135deg, var(--accent-secondary), var(--accent-primary)); color: white; border: none; border-radius: 50%; width: 55px; height: 55px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 4px 15px rgba(74, 108, 250, 0.4); flex-shrink: 0; } |
| .play-pause-btn-simple svg { width: 28px; height: 28px; fill: currentColor; } |
| .progress-wrapper { display: flex; align-items: center; gap: 1rem; width: 100%; } |
| .time-display { font-size: 0.9em; color: var(--text-secondary); font-variant-numeric: tabular-nums; min-width: 40px; text-align: center; } |
| .audio-progress-bar { -webkit-appearance: none; appearance: none; width: 100%; height: 8px; background: #e2e8f0; border-radius: 5px; outline: none; cursor: pointer; flex-grow: 1;} |
| .audio-progress-bar::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; background: var(--accent-primary); border-radius: 50%; cursor: pointer; border: 3px solid white; box-shadow: var(--shadow-md); } |
| .audio-download-btn-new { display: inline-flex; align-items: center; justify-content: center; gap: 10px; width: 100%; max-width: 320px; margin-top: 0.5rem; padding: 0.9rem 1.5rem; font-size: 1.1em; font-weight: 700; background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); color: #fff !important; text-decoration: none !important; border: none; border-radius: var(--radius-btn); cursor: pointer; } |
| .audio-download-btn-new svg { width: 20px; height: 20px; fill: currentColor; } |
| |
| #standard-view .char-counter-wrapper { font-size: 0.85em; color: var(--text-tertiary); text-align: left; margin-top: 0.75rem; padding: 0 0.2rem; } |
| #standard-view #char-count { font-weight: 600; color: var(--accent-primary); } |
| #standard-view #selected-speaker-display { text-align: center; margin-top: 1.5rem; } |
| #standard-view #selected-speaker-card { display: inline-flex; align-items: center; background: linear-gradient(135deg, var(--input-bg) 0%, var(--panel-bg) 100%); border-radius: 50px; padding: 0.75rem 0.75rem 0.75rem 1.5rem; box-shadow: var(--shadow-md); border: 1px solid var(--panel-border); cursor: pointer; margin-bottom: 1.5rem; transition: all 0.3s; } |
| #standard-view #selected-speaker-card:hover { transform: translateY(-6px); box-shadow: var(--shadow-lg); border-color: var(--accent-primary); } |
| #standard-view #selected-speaker-card img { width: 60px; height: 60px; border-radius: 50%; object-fit: cover; margin-left: 1rem; border: 3px solid var(--accent-secondary); } |
| #standard-view #selected-speaker-info { text-align: right; } |
| #standard-view #selected-speaker-info h3 { margin: 0; font-size: 1.25em; font-weight: 800; } |
| #standard-view #selected-speaker-info p { margin: 2px 0 0; color: var(--text-secondary); font-size: 0.85em; font-weight: 500; } |
| #standard-view #change-speaker-btn { display: inline-flex; align-items: center; justify-content: center; padding: 12px 24px; border-radius: var(--radius-btn); background: var(--panel-bg); border: 1px solid var(--input-border); color: var(--text-primary); cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1em; transition: var(--transition-smooth); box-shadow: var(--shadow-md); } |
| #standard-view #change-speaker-btn:hover { background: var(--input-bg); border-color: var(--accent-primary); color: var(--accent-primary); } |
| |
| .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(18, 24, 38, 0.6); backdrop-filter: blur(10px); display: none; align-items: center; justify-content: center; z-index: 1000; opacity: 0; transition: opacity var(--transition-smooth); } |
| .modal-overlay.visible { display: flex; opacity: 1; } |
| .modal-dialog { background: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); width: 90%; box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); opacity: 0; animation: modalZoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; position: relative; } |
| .modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid var(--panel-border); } |
| .modal-header h2 { margin: 0; font-size: 1.8em; font-weight: 800; color: var(--accent-primary); } |
| .close-modal-btn { background: linear-gradient(90deg, #8A2BE2, #EC4899); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; text-fill-color: transparent; border: none; font-size: 3rem; font-weight: 700; cursor: pointer; transition: var(--transition-smooth); line-height: 1; padding: 0; } |
| #speaker-modal .modal-dialog { max-width: 750px; max-height: 85vh; overflow-y: auto; } |
| |
| @media (max-width: 600px) { .page-wrapper { padding: 1.5rem 0; } .main-content { padding: 1.5rem; } .app-header h1 { font-size: 2em; } #standard-view #selected-speaker-card { flex-direction: column; padding: 1rem; border-radius: var(--radius-card); } #standard-view #selected-speaker-card img { margin-left: 0; margin-bottom: 1rem; } } |
| |
| .landing-section { padding: 4rem 0; text-align: center; border-top: 1px solid var(--panel-border); } |
| .landing-section .subtitle { font-size: 1.1em; color: var(--text-secondary); max-width: 600px; margin: 0 auto 3rem auto; } |
| .features-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-top: 3rem; } |
| .feature-card { background-color: var(--panel-bg); padding: 2rem; border-radius: var(--radius-card); box-shadow: var(--shadow-lg); border: 1px solid var(--panel-border); transition: all 0.3s ease; } |
| .feature-card .icon { font-size: 2.5rem; margin-bottom: 1rem; color: var(--accent-primary); } |
| .feature-card h3 { font-size: 1.3em; margin-bottom: 0.5rem; color: var(--text-primary); } |
| .site-footer { text-align: center; padding: 2rem 0; margin-top: 3rem; border-top: 1px solid var(--panel-border); color: var(--text-tertiary); } |
| </style> |
| </head> |
| <body> |
|
|
| <div class="page-wrapper"> |
| <div class="app-container"> |
| <div id="standard-view"> |
| <header class="app-header"> |
| <h1>مولد صدای هوشمند Ai Sada</h1> |
| <p>کیفیت استودیو، قدرت هوش مصنوعی. متن فارسی را به صدایی طبیعی و با کیفیت استودیویی تبدیل کنید یا پادکستهای چند نفره بسازید.</p> |
| </header> |
| <main class="main-content"> |
| <form id="standard-tts-form" onsubmit="return false;"> |
| <div class="form-group"> |
| <label for="text-input-standard">📝 متن اصلی</label> |
| <textarea id="text-input-standard" rows="5" placeholder="متن خود را برای تبدیل به گفتار اینجا وارد کنید..."></textarea> |
| <div class="char-counter-wrapper"><span id="char-count">0</span> / <span id="char-max">80000</span> نویسه</div> |
| </div> |
| <div class="form-group"> |
| <label for="prompt-input-standard">🗣️ توصیف لحن و احساس (اختیاری)</label> |
| <input type="text" id="prompt-input-standard" placeholder="مثال: با لحنی آرام و قصهگو"> |
| </div> |
| <div class="form-group"> |
| <label>🎤 گوینده منتخب</label> |
| <div id="selected-speaker-display"> |
| <div id="selected-speaker-card" title="برای تغییر گوینده کلیک کنید"> |
| <img id="selected-speaker-img" src="" alt="عکس گوینده"> |
| <div id="selected-speaker-info"> |
| <h3 id="selected-speaker-name"></h3> |
| <p id="selected-speaker-desc"></p> |
| </div> |
| </div> |
| <button type="button" id="change-speaker-btn">تغییر گوینده</button> |
| </div> |
| </div> |
|
|
| <button type="submit" id="generate-btn-standard" class="generate-btn"> |
| <span class="btn-text">✨ خلق صدا با آلفا</span> |
| <div class="spinner"></div> |
| </button> |
| </form> |
| <div id="output-section-standard" class="output-section"> |
| <div id="status-message-standard" class="status-message">صدای تولید شده در اینجا ظاهر خواهد شد.</div> |
| <div id="loading-animation-wrapper-standard" class="loading-animation-wrapper"> |
| <div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div> |
| <p class="loading-text">در حال پردازش هوشمند و تولید صدا...</p> |
| </div> |
| <div id="audio-player-content-standard" class="audio-player-content"></div> |
| </div> |
| </main> |
| </div> |
|
|
| <audio id="hidden-audio-player" style="display: none;"></audio> |
| <audio id="sample-audio-player" style="display: none;"></audio> |
| </div> |
|
|
| <div id="sample-text-display"> |
| <h4>متن تبدیل شده ⬇️</h4> |
| <p id="sample-text-content">...</p> |
| </div> |
|
|
| |
| |
| |
|
|
| |
| <div id="speaker-modal" class="modal-overlay"> |
| <div class="modal-dialog"> |
| <div class="modal-header"> |
| <h2>گالری گویندگان آلفا</h2> |
| <button type="button" class="close-modal-btn" data-modal-id="speaker-modal">×</button> |
| </div> |
| <div id="speaker-grid"></div> |
| </div> |
| </div> |
|
|
| |
| <div id="long-text-modal" class="modal-overlay"> |
| <div class="modal-dialog"> |
| <div class="modal-header"> |
| <div class="header-icon">📜</div> |
| <h2>متن شما طولانی است</h2> |
| </div> |
| <p class="desc">متن های طولانی در چند مرحله ساخته میشود. بهترین حالت برای متن های طولانی انتخاب دو گوینده (خانم/آقا) یا حالت پادکست است. لطفاً نحوه تبدیل را انتخاب کنید.</p> |
| <div class="long-text-options"> |
| <div class="option-card" data-mode="single"> |
| <div class="option-icon">👤</div> |
| <div class="option-content"><div class="option-title">با یک گوینده</div><div class="option-subtitle">متن به صورت یکپارچه با گوینده انتخابی شما خوانده میشود.</div></div> |
| <div class="option-arrow">❮</div> |
| </div> |
| <div class="option-card" data-mode="double"> |
| <div class="option-icon">👥</div> |
| <div class="option-content"><div class="option-title">با دو گوینده</div><div class="option-subtitle">متن به صورت هوشمند بین دو گوینده تصادفی تقسیم میشود.</div></div> |
| <div class="option-arrow">❮</div> |
| </div> |
| <div class="option-card" data-mode="multi"> |
| <div class="option-icon">🎭</div> |
| <div class="option-content"><div class="option-title">با چندین گوینده</div><div class="option-subtitle">هوش مصنوعی متن را تحلیل کرده و با چندین گوینده تصادفی اجرا میکند.</div></div> |
| <div class="option-arrow">❮</div> |
| </div> |
| <div class="option-card special-ai-card" data-mode="ai-podcast"> |
| <div class="option-icon">🎙️</div> |
| <div class="option-content"><div class="option-title">تبدیل متن به پادکست</div><div class="option-subtitle">هوش مصنوعی متن شما را به سناریوی پادکست حرفهای تبدیل میکند.</div></div> |
| <div class="option-arrow">✨</div> |
| </div> |
| </div> |
| <div style="text-align: center; margin-top: 1.5rem;"> |
| <button type="button" class="close-modal-btn" data-modal-id="long-text-modal" style="font-size: 1rem; color: var(--text-tertiary); border: 1px solid var(--input-border); padding: 0.5rem 1rem; border-radius: 8px;">انصراف</button> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="ai-podcast-modal" class="modal-overlay"> |
| <div class="modal-dialog" style="max-width: 600px;"> |
| <div class="modal-header" style="border-bottom: none; padding-bottom: 0;"> |
| <div style="display: flex; align-items: center; gap: 12px;"> |
| <span style="font-size: 2.5rem; animation: bounce 2s infinite;">✨</span> |
| <h2>ساخت پروژه با هوش مصنوعی</h2> |
| </div> |
| <button type="button" class="close-modal-btn" data-modal-id="ai-podcast-modal">×</button> |
| </div> |
| <p style="color: var(--text-secondary); margin-top: 1rem; line-height: 1.8; font-size: 1.05rem;"> |
| یک موضوع یا مقاله کامل را وارد کنید. هوش مصنوعی بهترین تیم گویندگان را انتخاب کرده و سناریوی گفتگو را تولید خواهد کرد. |
| </p> |
| <textarea id="ai-podcast-input" placeholder="موضوع یا متن مقاله خود را اینجا وارد کنید..."></textarea> |
| <button type="button" id="start-ai-podcast-action-btn" class="beautiful-download-btn" style="width: 100%; margin: 0; background: linear-gradient(135deg, #FF0080 0%, #7928CA 100%); border-bottom-color: #5a1da8;"> |
| 🚀 پروژه را بساز |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="confirm-delete-modal" class="confirm-modal-overlay"> |
| <div class="confirm-modal-dialog"> |
| <h3>تایید عملیات</h3> |
| <p>آیا از بازنشانی پروژه به حالت اولیه مطمئن هستید؟</p> |
| <div class="confirm-modal-actions"> |
| <button id="cancel-delete-btn" class="cancel-btn">انصراف</button> |
| <button id="confirm-delete-action-btn" class="confirm-btn">بله، حذف کن</button> |
| </div> |
| </div> |
| </div> |
|
|
| <input type="hidden" id="selected_speaker_id_storage" value="Charon"> |
|
|
| |
| |
| |
|
|
| <section class="landing-section features"> |
| <h2>ویژگیهای کلیدی Ai Sada</h2> |
| <p class="subtitle">ابزارهایی قدرتمند برای خلق تجربههای صوتی بینظیر.</p> |
| <div class="features-grid"> |
| <div class="feature-card"><div class="icon">🔊</div><h3>کیفیت صدای استودیویی</h3><p>صداهای تولید شده با هوش مصنوعی پیشرفته ما، کاملا طبیعی، شفاف و بدون نویز هستند.</p></div> |
| <div class="feature-card"><div class="icon">🎭</div><h3>۳۰ گوینده متنوع</h3><p>از میان گالری وسیعی از صداهای مرد و زن با احساسات و لحنهای مختلف، گوینده دلخواه خود را انتخاب کنید.</p></div> |
| <div class="feature-card"><div class="icon">🎙️</div><h3>پادکست ساز هوشمند</h3><p>با نوشتن یک موضوع، هوش مصنوعی سناریویی چند نفره نوشته و آن را به پادکست صوتی تبدیل میکند.</p></div> |
| </div> |
| </section> |
|
|
| <footer class="site-footer"><p>© 1404 - تمام حقوق برای <a href="#">Ai Sada</a> محفوظ است.</p></footer> |
| </div> |
|
|
| |
| |
| |
|
|
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| const API_BASE = 'https://opera8-podgen.hf.space/api'; |
| |
| |
| let podcastDB; |
| const PODCAST_DB_NAME = 'AISada_PodcastDB'; |
| const PODCAST_DB_VERSION = 1; |
| |
| let activePodcastSpeakers =[]; |
| let podcastMasterAudioBlobs =[]; |
| let speakerToReplaceId = null; |
| let currentlyPlayingTurnPlayer = null; |
| let podcastAudioPlayer = new Audio(); |
| let lastAddedSpeakerIndex = -1; |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| let isMainSelectorAction = false; |
| let currentPodcastMode = null; |
| |
| |
| const allSpeakers =[ |
| { id: "Charon", name: "شهاب (مرد)", gender: "male", desc: "صدایی قدرمند و رسا", imgUrl: "https://uploadkon.ir/uploads/a18705_25IMG-۲۰۲۵۰۷۰۵-۱۱۰۵۴۹.jpg", samples:["https://uploadkon.ir/uploads/c17c06_26شهاب-یک-2-.mp3", "https://uploadkon.ir/uploads/af0d06_26شهاب-دو-2-.mp3", "https://uploadkon.ir/uploads/9e2806_26شهاب-سه-2-.mp3", "https://uploadkon.ir/uploads/35bd06_26شهاب-چهار-2-.mp3"], sampleTexts:["", "", "هاهاها ﺗﺒﺪﯾﻞ متن (خنده زیاد) شما ﺑﻪ ﺻﺪﺍﯾﯽ ﮐﻪ ﻫﻤﺪﻟﯽ ﻭ ﺩﺭﮎ ﺭﺍ ﺑﺮﻣﯽﺍﻧﮕﯿﺰﺩ.(خنده)", "ﺗﺒﺪﯾﻞ ﻣﺘﻦ ﺷﻤﺎﺑﻪ ﺻﺪﺍﯾﯽ ﮐﻪ ﻫﻤﺪﻟﯽ ﻭ ﺩﺭﮎ ﺭﺍ ﺑﺮﻣﯽﺍﻧﮕﯿﺰﺩ.(متن با صدای ترس و لرز بخون)"] }, |
| { id: "Zephyr", name: "آوا (زن)", gender: "female", desc: "لطیف و دلنشین", imgUrl: "https://uploadkon.ir/uploads/029605_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۲۵۲.jpg", samples:["https://uploadkon.ir/uploads/920e06_26آوا-یک-2-.mp3", "https://uploadkon.ir/uploads/f50c06_26آوا-دو-2-.mp3", "https://uploadkon.ir/uploads/bc8e06_26آوا-سه-2-.mp3", "https://uploadkon.ir/uploads/1b2e06_26آوا-چهار-2-.mp3"], sampleTexts:["", "", "هاهاها ﺗﺒﺪﯾﻞ ﻣﺘﻦ ﺷﻤﺎ(خنده زیاد) ﺑﻪ ﺻﺪﺍﯾﯽ ﮐﻪ ﻫﻤﺪﻟﯽ ﻭ ﺩﺭﮎ ﺭﺍ ﺑﺮﻣﯽﺍﻧﮕﯿﺰﺩ.(خنده)", "وای ﺗﺒﺪﯾﻞ ﻣﺘﻦ ﺷﻤﺎﺑﻪ ﺻﺪﺍﯾﯽ ﮐﻪ ﻫﻤﺪﻟﯽ ﻭ ﺩﺭﮎ ﺭﺍ ﺑﺮﻣﯽﺍﻧﮕﯿﺰﺩ.(با تعجب و هیجان زیاد)"] }, |
| { id: "Achird", name: "نوید (مرد)", gender: "male", desc: "جوان و پرانرژی", imgUrl: "https://uploadkon.ir/uploads/697e05_25IMG-۲۰۲۵۰۶۰۹-۰۶۴۶۳۷.jpg", samples:["https://uploadkon.ir/uploads/f8b206_26نوید-یک-2-.mp3", "https://uploadkon.ir/uploads/9c0d06_26نوید-دو-2-.mp3", "https://uploadkon.ir/uploads/26f006_26نوید-سه-2-.mp3", "https://uploadkon.ir/uploads/5e0306_26نوید-چهار-2-.mp3"], sampleTexts:["", "", "", "ﻫﺮ ﮐﻠﻤﻪ ﺍﺯ ﻧﻮﺷﺘﻪﻫﺎﯾﺘﺎﻥ، ﺑﺎ ﺁﺭﺍﻣﺸﯽ ﻋﻤﯿﻖ ﺩﺭ ﻗﺎﻟﺐ ﺻﺪﺍ ﺟﺎﺭﯼ ﻣﯽﺷﻮﺩ.(ﺁﺭﺍﻣﺶﺑﺨﺶ ﻭ ﺗﺴﮑﯿﻦﺩﻫﻨﺪﻩ)"] }, |
| { id: "Zubenelgenubi", name: "آرمان (مرد)", gender: "male", desc: "گرم و صمیمی", imgUrl: "https://uploadkon.ir/uploads/a8a705_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۶۲۹.jpg", samples:["https://uploadkon.ir/uploads/bf9406_26آرمان-یک-2-.mp3", "https://uploadkon.ir/uploads/edaf06_26آرمان-دو-2-.mp3", "https://uploadkon.ir/uploads/2e7a06_26آرمان-سه-2-.mp3", "https://uploadkon.ir/uploads/456306_26آرمان-چهار-2-.mp3"], sampleTexts:["", "دستورات را به وضوح و بدون هیچ ابهامی صادر کنید.(با لحنی قاطع، محکم و فرماندهانه، شبیه به یک افسر ارتش که دستور عملیات میدهد) 🎖️", "آخرین اخبار و تحولات مهم را با دقت و بیطرفی کامل به اطلاع عموم برسانید. (با لحنی رسمی، خنثی و خبری، همانند یک مجری خبر حرفهای در بخش خبری شبانگاهی)", "این غیرقابل تحمل است! دیگر نمیتوانم این وضعیت را تحمل کنم! (با صدای بلند، خشمگین و همراه با فریاد، مناسب برای دیالوگ یک شخصیت در اوج عصبانیت در یک صحنه دراماتیک فیلم)"] }, |
| { id: "Vindemiatrix", name: "مهسا (زن)", gender: "female", desc: "باوقار و رسمی", imgUrl: "https://uploadkon.ir/uploads/d74d05_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۸۳۸.jpg", samples:["https://uploadkon.ir/uploads/749806_26مهسا-یک-2-.mp3", "https://uploadkon.ir/uploads/15c706_26مهسا-دو-2-.mp3", "https://uploadkon.ir/uploads/fc2406_26مهسا-سه-2-.mp3", "https://uploadkon.ir/uploads/978506_26مهسا-چهار-2-.mp3"], sampleTexts:["", "به سرعت و با دقت، تمامی جزئیات را برای شروع ماموریت نهایی بررسی کنید. (با لحنی هیجانزده و کمی اضطرابآور، شبیه به یک عامل مخفی که دستورات فوری دریافت میکند)", "و در پایان، با آرزوی بهترینها برای شما، این برنامه به اتمام میرسد. (با لحنی آرام، ملایم و دلنشین، مانند یک گوینده کتاب صوتی در پایان یک داستان آرامشبخش) 📚", "وای خدای من! باورم نمیشه این اتفاق افتاد! نه، این امکان نداره! (با صدایی مملو از تعجب و ناباوری شدید، همراه با کمی ترس یا شوک، مناسب برای یک صحنه غیرمنتظره در فیلم) 😲"] }, |
| { id: "Rasalgethi", name: "دانا (مرد)", gender: "male", desc: "خبری و آموزنده", imgUrl: "https://uploadkon.ir/uploads/57e425_25IMG-20250925-112825-749.jpg", samples:["https://uploadkon.ir/uploads/2a0a06_26دانا-یک-2-.mp3", "https://uploadkon.ir/uploads/6daa06_26دانا-دو-2-.mp3", "https://uploadkon.ir/uploads/fb3a06_26دانا-سه-2-.mp3", "https://uploadkon.ir/uploads/5b0c06_26دانا-چهار-2-.mp3"], sampleTexts:["", "به یاد داشته باشید، هر قدمی که برمیداریم، ما را به هدف نزدیکتر میکند. مصمم باشید! (با لحنی انگیزشی و الهامبخش، شبیه به یک مربی زندگی که به شاگردانش روحیه میدهد) 🚀", "در این شب مهتابی، داستانهای کهن از دل تاریخ سر برمیآورند و خاطرات را زنده میکنند. (با لحنی مرموز و کمی وهمآلود، مناسب برای روایت یک داستان شبحوار یا افسانه قدیمی)", "من به شما هشدار داده بودم! اما شما گوش نکردید! حالا همه چیز از دست رفته است! (با صدایی پر از یاس و ناامیدی، همراه با حس سرزنش و پشیمانی شدید، مناسب برای دیالوگ یک شخصیت در یک موقعیت بحرانی) "] }, |
| { id: "Sadachbia", name: "سامان (مرد)", gender: "male", desc: "شاداب و پویا", imgUrl: "https://uploadkon.ir/uploads/580205_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۳۳۰.jpg", samples:["https://uploadkon.ir/uploads/2f4b06_26سامان-یک-2-.mp3", "https://uploadkon.ir/uploads/bbc806_26سامان-دو-2-.mp3", "https://uploadkon.ir/uploads/0a9906_26سامان-سه-2-.mp3", "https://uploadkon.ir/uploads/c94b06_26سامان-چهار-2-.mp3"], sampleTexts:["", "تو باید قوی باشی! هیچ چیز نمیتواند تو را از رسیدن به هدفت باز دارد! (با لحنی حماسی، قهرمانانه و بسیار تشویقکننده، شبیه به یک رهبر که در لحظه نبرد به سربازانش روحیه میدهد) ⚔️", "و حالا، لحظاتی را با موسیقی کلاسیک و آرامشبخش، در کنار ما سپری کنید. (با لحنی بسیار ملایم، آرام و موزون، شبیه به گوینده یک برنامه رادیویی آرامشبخش شبانه) 🎶", "وای! این چه بویی بود؟! حالم داره بد میشه! (با صدایی که نشاندهنده انزجار شدید، تهوع و چندش است، مناسب برای واکنش به یک بوی بسیار بد یا منظرهای ناخوشایند)"] }, |
| { id: "Sadaltager", name: "آرش (مرد)", gender: "male", desc: "مطمئن و تاثیرگذار", imgUrl: "https://uploadkon.ir/uploads/c4db05_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۵۰۰.jpg", samples:["https://uploadkon.ir/uploads/7a9406_26آرش-یک-2-.mp3", "https://uploadkon.ir/uploads/efe806_26آرش-دو-2-.mp3", "https://uploadkon.ir/uploads/278506_26آرش-سه-2-.mp3", "https://uploadkon.ir/uploads/8a7506_26آرش-چهار-2-.mp3"], sampleTexts:["", "توجه! توجه! خطر نزدیک است! همه به پناهگاه بروید! (با لحنی اضطراری، بلند و هشداردهنده، شبیه به اعلام وضعیت قرمز در یک سیستم هشدار عمومی) 🚨", "و اینگونه بود که هر دو عاشق، در زیر نور ماه، عهد ابدی بستند. (با لحنی عاشقانه، لطیف و رؤیایی، مناسب برای روایت یک داستان عاشقانه یا شعر احساسی) ", "هاهاها! چه شوخی مسخرهای! واقعاً فکر کردی من باور میکنم؟ (با صدای خنده تمسخرآمیز و کنایهآلود، همراه با حس بیاعتقادی و ریشخند) 😂"] }, |
| { id: "Sulafat", name: "شبنم (زن)", gender: "female", desc: "آرام و متین", imgUrl: "https://uploadkon.ir/uploads/995005_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۶۱۱.jpg", samples:["https://uploadkon.ir/uploads/014706_26شبنم-یک-2-.mp3", "https://uploadkon.ir/uploads/098c06_26شبنم-دو-2-.mp3", "https://uploadkon.ir/uploads/2c0706_26شبنم-سه-2-.mp3", "https://uploadkon.ir/uploads/66bd06_26شبنم-چهار-2-.mp3"], sampleTexts:["", "گزارش نهایی را با دقت و جزئیات کامل، برای بررسی فوری ارائه دهید. (با لحنی جدی، رسمی و کمی آمرانه، شبیه به یک بازپرس که از یک مأمور اطلاعات میخواهد)", "آه، چه منظرهی زیبایی! گویی نقاشیای از بهشت پیش روی من است. (با صدایی مملو از تحسین، شگفتی و ذوقزدگی، هنگام دیدن چیزی بسیار زیبا) 😍", "من که به تو گفتم! این کار را نکن! حالا ببین چه بلایی سرمان آمد! (با لحنی پشیمان، سرزنشکننده و همراه با حس \"من گفته بودم\"، مناسب برای موقعیتی که پیشبینی منفی به حقیقت پیوسته است)"] }, |
| { id: "Laomedeia", name: "سحر (زن)", gender: "female", desc: "دوستانه و گیرا", imgUrl: "https://uploadkon.ir/uploads/660705_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۷۵۴.jpg", samples:["https://uploadkon.ir/uploads/836c06_26سحر-یک-2-.mp3", "https://uploadkon.ir/uploads/b0ce06_26سحر-دو-2-.mp3", "https://uploadkon.ir/uploads/e2d306_26سحر-سه-2-.mp3", "https://uploadkon.ir/uploads/1f3306_26سحر-چهار-2-.mp3"], sampleTexts:["", "تو تنها امید ما هستی! باید این مأموریت را به پایان برسانی، به هر قیمتی! (با لحنی دراماتیک، التماسآمیز و پر از امید، شبیه به یک شخصیت که از قهرمان داستان کمک میخواهد)", "صبح بخیر ایران! امروز با یک خبر فوقالعاده دیگر در خدمت شما هستیم. (با لحنی شاد، پرانرژی و صبحگاهی، مانند یک مجری رادیو در ابتدای برنامه) ☀️", "آاااخ! پام گیر کرد! افتادم! خیلی درد میکنه! (با صدای جیغ، ناله و درد شدید، مناسب برای واکنش به یک حادثه ناگهانی و آسیب دیدن)"] }, |
| { id: "Achernar", name: "مریم (زن)", gender: "female", desc: "حرفهای و واضح", imgUrl: "https://uploadkon.ir/uploads/4c2905_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۰۳۶.jpg", samples:["https://uploadkon.ir/uploads/9bd906_26مریم-یک-2-.mp3", "https://uploadkon.ir/uploads/c15006_26مریم-دو-2-.mp3", "https://uploadkon.ir/uploads/b00106_26مریم-سه-2-.mp3", "https://uploadkon.ir/uploads/8a2806_26مریم-چهار-2-.mp3"], sampleTexts:["", "", "تمامی واحدها، به موقعیت اعلامشده اعزام شوید! تکرار میکنм، اعزام شوید! (با لحنی قاطع، محکم و اضطراری، شبیه به فرمانده عملیات در بیسیم) 🚨", "اوه، واقعاً؟ من که اصلاً تعجب نکردم! (با لحنی بیتفاوت، طعنهآمیز و کمی کنایهآلود)"] }, |
| { id: "Alnilam", name: "بهرام (مرد)", gender: "male", desc: "حماسی و نافذ", imgUrl: "https://uploadkon.ir/uploads/f0c205_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۲۲۰.jpg", samples:["https://uploadkon.ir/uploads/7f8306_26بهرام-یک-2-.mp3", "https://uploadkon.ir/uploads/35ee06_26بهرام-دو-2-.mp3", "https://uploadkon.ir/uploads/77fd06_26بهرام-سه-2-.mp3", "https://uploadkon.ir/uploads/3e1106_26بهرام-چهار-2-.mp3"], sampleTexts:["", "", "واقعاً فکر میکنی من این داستان رو باور میکنм؟ (با لحنی تمسخرآمیز و بیباور) 🤨", "هی، گوش کن! یه راز دارم که فقط به تو میگم. (با لحنی مرموز، شیطنتآمیز و نجواگونه)"] }, |
| { id: "Schedar", name: "نیکان (مرد)", gender: "male", desc: "مهربان و شیرین", imgUrl: "https://uploadkon.ir/uploads/d37a05_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۳۲۵.jpg", samples:["https://uploadkon.ir/uploads/390a06_26نیکان-یک-2-.mp3", "https://uploadkon.ir/uploads/0e6d06_26نیکان-دو-2-.mp3", "https://uploadkon.ir/uploads/f72606_26نیکان-سه-2-.mp3", "https://uploadkon.ir/uploads/5cb006_26نیکان-چهار-2-.mp3"], sampleTexts:["", "", "این دیگر واقعاً شورش را درآورده است! بس کن! (با لحنی خسته، کلافه و عصبی از تکرار یک موضوع) ", "واو! این فوقالعاده است! هرگز فکر نمیکردم چنین چیزی ممکن باشد! (با لحنی سرشار از هیجان، شگفتی و ذوقزدگی) 🤩"] }, |
| { id: "Gacrux", name: "فرناز (زن)", gender: "female", desc: "پخته و قابل اعتماد", imgUrl: "https://uploadkon.ir/uploads/495b09_25IMG-20251109-104135-304.jpg", samples:["https://uploadkon.ir/uploads/7d4b06_26فرناز-یک-2-.mp3", "https://uploadkon.ir/uploads/df5806_26فرناز-دو-2-.mp3", "https://uploadkon.ir/uploads/023d06_26فرناز-سه-2-.mp3", "https://uploadkon.ir/uploads/328f06_26فرناز-چهار-2-.mp3"], sampleTexts:["", "", "این دیگر واقعاً غیرقابل قبوله! باید یه کاری بکنیم! (با لحنی معترض، خشمگین و مطالبهگر)", "آیا واقعاً این امکانپذیر است؟ من که باورم نمیشود! (با لحنی کنجکاو، کمی شکاک و متعجب) 🤔"] }, |
| { id: "Pulcherrima", name: "سارا (زن)", gender: "female", desc: "جذاب و مدرن", imgUrl: "https://uploadkon.ir/uploads/acb105_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۷۴۳.jpg", samples:["https://uploadkon.ir/uploads/142c06_26سارا-یک-2-.mp3", "https://uploadkon.ir/uploads/246c06_26سارا-دو-2-.mp3", "https://uploadkon.ir/uploads/4baa06_26سارا-سه-2-.mp3", "https://uploadkon.ir/uploads/6c5406_26سارا-چهار-2-.mp3"], sampleTexts:["", "", "هر سرباز باید آماده نبرد باشد! هیچ شکی در کار نیست! (با لحنی قاطع، جدی و آماده برای مبارزه، شبیه به یک فرمانده نظامی در میدان جنگ) 🪖", "واای! این دیگه چه فیلمی بود؟! اصلاً نفهمیدم چی شد! (با لحنی گیج، کمی ناامید و متعجب از یک اتفاق غیرمنتظره یا نامفهوم) "] }, |
| { id: "Umbriel", name: "مانی (مرد)", gender: "male", desc: "خلاق و متفاوت", imgUrl: "https://uploadkon.ir/uploads/68b505_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۹۱۴.jpg", samples:["https://uploadkon.ir/uploads/62e006_26مانی-یک-2-.mp3", "https://uploadkon.ir/uploads/8af206_26مانی-دو-2-.mp3", "https://uploadkon.ir/uploads/a1f406_26مانی-سه-2-.mp3", "https://uploadkon.ir/uploads/4d2506_26مانی-چهار-2-.mp3"], sampleTexts:["", "", "وای! این بهترین هدیهای بود که تا به حال گرفتم! ممنونم، ممنونم! (با لحنی سرشار از ذوق، شادی کودکانه و تشکر فراوان) 🎁", "ببینید، این موضوع بسیار ساده است، فقط کافیست کمی دقت کنید. (با لحنی آموزشدهنده، کمی از بالا به پایین و صبورانه) "] }, |
| { id: "Algieba", name: "آرتین (مرد)", gender: "male", desc: "با اصالت و شیک", imgUrl: "https://uploadkon.ir/uploads/571005_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۰۳۹.jpg", samples:["https://uploadkon.ir/uploads/dbed06_26آرتین-یک-2-.mp3", "https://uploadkon.ir/uploads/865706_26آرتین-دو-2-.mp3", "https://uploadkon.ir/uploads/5e5806_26آرتین-سه-2-.mp3", "https://uploadkon.ir/uploads/0d8206_26آرتین-چهار-2-.mp3"], sampleTexts:["", "", "تو جرئت نداری این کار رو بکنی! من بهت هشدار میدم! (با لحنی پر از تهدید، خشم و اخطار جدی) ", "خب، پس قرار شد ساعت ۸ شب، جلوی سینما باشیم، درسته؟ (با لحنی معمولی، دوستانه و تأییدکننده برای یک قرار ملاقات) 🗓️🤝"] }, |
| { id: "Despina", name: "دلنواز (زن)", gender: "female", desc: "هنری و احساسی", imgUrl: "https://uploadkon.ir/uploads/5d7805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۲۲۲.jpg", samples:["https://uploadkon.ir/uploads/a3b506_26دلنواز-یک-2-.mp3", "https://uploadkon.ir/uploads/f67c06_26دلنواز-دو-2-.mp3", "https://uploadkon.ir/uploads/180106_26دلنواز-سه-2-.mp3", "https://uploadkon.ir/uploads/d31f06_26دلنواز-چهار-2-.mp3"], sampleTexts:["", "", "آیا واقعاً این تنها راه حل است؟ من فکر نمیکنم. (با لحنی متفکر، شکاک و کمی ناامید)", "من که گفتم! این یک پیروزی بزرگ برای تیم ماست! آفرین به همه! (با لحنی پرشور، شاد و با صدای بلند و هیجانزده، شبیه به یک مربی ورزشی پس از برد) 🏆🎉"] }, |
| { id: "Erinome", name: "روژان (زن)", gender: "female", desc: "شفاف و گویا", imgUrl: "https://uploadkon.ir/uploads/aa8805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۳۴۹.jpg", samples:["https://uploadkon.ir/uploads/dea706_26روژان-یک-2-.mp3", "https://uploadkon.ir/uploads/496b06_26روژان-دو-2-.mp3", "https://uploadkon.ir/uploads/185e06_26روژان-سه-2-.mp3", "https://uploadkon.ir/uploads/b2f306_26روژان-چهار-2-.mp3"], sampleTexts:["", "", "فکر میکنм این بهترین فرصتی است که تا به حال داشتهایم؛ نباید آن را از دست بدهیم. (با لحنی امیدوار، متفکر و کمی محرمانه) ", "من به شما هشدار داده بودم که این مسیر پرخطر است، اما شما گوش نکردید. (با لحنی سرزنشآمیز، همراه با حس تأسف و من گفته بودم) "] }, |
| { id: "Algenib", name: "امید (مرد)", gender: "male", desc: "انگیزه بخش و مثبت", imgUrl: "https://uploadkon.ir/uploads/a63c05_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۹۲۱.jpg", samples:["https://uploadkon.ir/uploads/3e9806_26امید-یک-2-.mp3", "https://uploadkon.ir/uploads/6c3306_26امید-دو-2-.mp3", "https://uploadkon.ir/uploads/65fc06_26امید-سه-2-.mp3", "https://uploadkon.ir/uploads/367f06_26امید-چهار-2-.mp3"], sampleTexts:["", "", "نه، نه! این امکان نداره! ما باید همین الان فرار کنیم! اونا دارن میان! (با لحنی مملو از ترس، وحشت و اضطراب شدید، شبیه به یک شخصیت که در حال فرار از خطر است) ", "فکر میکنм بهترین راه اینه که یواشکی وارد بشیم و کسی هم متوجه نشه. (با لحنی آرام، مرموز و شیطنتآمیز، مناسب برای برنامهریزی یک کار مخفیانه) "] }, |
| { id: "Orus", name: "بردیا (مرد)", gender: "male", desc: "ورزشی و پرهیجان", imgUrl: "https://uploadkon.ir/uploads/8bc405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۴۳۳.jpg", samples:["https://uploadkon.ir/uploads/699d06_26بردیا-یک-2-.mp3", "https://uploadkon.ir/uploads/b6ad06_26بردیا-دو-2-.mp3", "https://uploadkon.ir/uploads/6a6506_26بردیا-سه-2-.mp3", "https://uploadkon.ir/uploads/8a1006_26بردیا-چهار-2-.mp3"], sampleTexts:["", "", "تبریک میگویم! این موفقیت بزرگ، نتیجه تلاش بیوقفه و اراده قوی شماست! (با لحنی سرشار از شادی، تحسین و تبریک صمیمانه) ", "من که بهت گفته بودم این کار رو نکن! حالا ببین چه بلایی سرمون اومد! (با لحنی سرزنشآمیز و همراه با حس من گفته بودم، کمی خشمگین و ناامید) "] }, |
| { id: "Aoede", name: "ترانه (زن)", gender: "female", desc: "موزیکال و خوشآهنگ", imgUrl: "https://uploadkon.ir/uploads/9cb405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۸۵۰.jpg", samples:["https://uploadkon.ir/uploads/b67306_26ترانه-یک-2-.mp3", "https://uploadkon.ir/uploads/154806_26ترانه-دو-2-.mp3", "https://uploadkon.ir/uploads/ee3806_26ترانه-سه-2-.mp3", "https://uploadkon.ir/uploads/b53806_26ترانه-چهار-2-.mp3"], sampleTexts:["", "", "به هیچ وجه کوتاه نمیآیم! این حق من است و باید به دستش بیاورم! (با لحنی سرسخت، مصمم و بسیار با اراده، شبیه به کسی که برای حقش میجنگد) ", "من واقعاً نمیدانм چه کار کنم! کاملاً گیج شدهام و راه حلی پیدا نمیکنм! (با صدایی مملو از سردرگمی، استیصال و ناامیدی) "] }, |
| { id: "Callirrhoe", name: "نیکو (زن)", gender: "female", desc: "روایتگر و قصهگو", imgUrl: "https://uploadkon.ir/uploads/ee5f05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۰۴۷.jpg", samples:["https://uploadkon.ir/uploads/b53806_26نیکو-یک-2-.mp3", "https://uploadkon.ir/uploads/e1aa06_26نیکو-دو-2-.mp3", "https://uploadkon.ir/uploads/d74f06_26نیکو-سه-2-.mp3", "https://uploadkon.ir/uploads/32fe06_26نیکو-چهار-2-.mp3"], sampleTexts:["", "", "باید هرچه سریعتر به نقطه امن برسیم! زمان زیادی نداریم! (با لحنی مضطرب، قاطع و سراسیمه، شبیه به کسی که در حال فرار از خطر است) 🏃♀️", "عجب! واقعاً انتظار داشتی که این اتفاق بیفته؟ من که از اول میدونستم! (با لحنی طعنهآمیز، همراه با حس من گفته بودم و کمی خستگی از پیشبینی درست) "] }, |
| { id: "Autonoe", name: "هستی (زن)", gender: "female", desc: "طبیعی و خودمانی", imgUrl: "https://uploadkon.ir/uploads/9b0505_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۲۲۲.jpg", samples:["https://uploadkon.ir/uploads/758506_26هستی-یک-2-.mp3", "https://uploadkon.ir/uploads/b69e06_26هستی-دو-2-.mp3", "https://uploadkon.ir/uploads/9dce06_26هستی-سه-2-.mp3", "https://uploadkon.ir/uploads/ccee06_26هستی-چهار-2-.mp3"], sampleTexts:["", "", "هرگز از جستجوی دانش دست نکش، زیرا نور خرد راهگشای تاریکیهاست. (با لحنی آرام، حکیمانه و پر از تجربه، شبیه به یک استاد پیر) ", "وااای! ببین! یه پروانه خیلی خوشگل نشست رو دستم! باورم نمیشه! (با لحنی سرشار از شگفتی، ذوق کودکانه و هیجان محض) 🦋🤩"] }, |
| { id: "Enceladus", name: "کامیار (مرد)", gender: "male", desc: "مصمم و جدی", imgUrl: "https://uploadkon.ir/uploads/127805_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۴۱۴.jpg", samples:["https://uploadkon.ir/uploads/566606_26کامیار-یک-2-.mp3", "https://uploadkon.ir/uploads/882806_26کامیار-دو-2-.mp3", "https://uploadkon.ir/uploads/32b006_26کامیار-سه-2-.mp3", "https://uploadkon.ir/uploads/843506_26کامیار-چهار-2-.mp3"], sampleTexts:["", "", "توجه، توجه! یک پیام فوری از ستاد فرماندهی! آماده دریافت دستورات جدید باشید! (با لحنی رسمی، قاطع و کمی اضطراری، شبیه به اعلامیه نظامی) 📢", "من که گفتم! این تصمیم کاملاً اشتباه بود و حالا همه باید عواقبش رو تحمل کنیم! (با لحنی مأیوس، سرزنشگر و همراه با حس من میدانستم)"] }, |
| { id: "Iapetus", name: "کیانوش (مرد)", gender: "male", desc: "درخشان و گیرا", imgUrl: "https://uploadkon.ir/uploads/c98b05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۶۰۵.jpg", samples:["https://uploadkon.ir/uploads/63fd06_26کیانوش-یک-2-.mp3", "https://uploadkon.ir/uploads/f6f406_26کیانوش-دو-2-.mp3", "https://uploadkon.ir/uploads/1b2106_26کیانوش-سه-2-.mp3", "https://uploadkon.ir/uploads/33f206_26کیانوش-چهار-2-.mp3"], sampleTexts:["", "", "و در سمت راست شما، میتوانید منظرهای بینظیر از آبشار نیاگارا را مشاهده کنید. (با لحنی هیجانزده، اطلاعاتی و راهنمایانه، شبیه به یک تورلیدر پرشور) 🏞️", "آه! دیگر بس است! چقدر باید این حرفها را تکرار کنم؟ (با صدایی مملو از کلافگی، خستگی مفرط و عصبانیت از تکرار) "] }, |
| { id: "Puck", name: "پویا (مرد)", gender: "male", desc: "بازیگوش و سرزنده", imgUrl: "https://uploadkon.ir/uploads/ca3605_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۸۳۹.jpg", samples:["https://uploadkon.ir/uploads/7d1306_26پویا-یک-2-.mp3", "https://uploadkon.ir/uploads/681206_26پویا-دو-2-.mp3", "https://uploadkon.ir/uploads/c90b06_26پویا-سه-2-.mp3", "https://uploadkon.ir/uploads/12e906_26پویا-چهار-2-.mp3"], sampleTexts:["", "", "من هرگز نمیتوانм نبود او را باور کنم؛ جای خالیاش همیشه در قلب من خواهد بود و مرا آزار میدهد. (با لحنی غمگین، حزنانگیز و مملو از اندوه عمیق)", "هی، یه فکری به سرم زده! بیا یه ماجراجویی کوچیک داشته باشیم، کسی هم خبردار نشه! (با لحنی شیطنتآمیز، وسوسهکننده و کمی مخفیانه، شبیه به کسی که قصد کاری هیجانانگیز دارد) "] }, |
| { id: "Kore", name: "مهتاب (زن)", gender: "female", desc: "نجواگر و آرامشبخش", imgUrl: "https://uploadkon.ir/uploads/b66605_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۰۳۵.jpg", samples:["https://uploadkon.ir/uploads/9bfc06_26مهتاب-یک-2-.mp3", "https://uploadkon.ir/uploads/e31006_26مهتاب-دو-2-.mp3", "https://uploadkon.ir/uploads/fdc506_26مهتاب-سه-2-.mp3", "https://uploadkon.ir/uploads/071d06_26مهتاب-چهار-2-.mp3"], sampleTexts:["", "", "هرگز اجازه ندهید ترس، شما را از رسیدن به آرزوهایتان باز دارد. شجاع باشید! (با لحنی الهامبخش، قدرتمند و تشویقکننده، شبیه به یک سخنران انگیزشی)", "خب، فکر کنم وقتشه که این راز رو فاش کنیم؛ دیگه نمیتونм بیشتر از این نگهش دارم! (با لحنی هیجانزده، کمی مرموز و همراه با حس افشاگری) "] }, |
| { id: "Fenrir", name: "سام (مرد)", gender: "male", desc: "جسور و بیباک", imgUrl: "https://uploadkon.ir/uploads/03c005_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۴۱۳.jpg", samples:["https://uploadkon.ir/uploads/467f06_26سام-یک-2-.mp3", "https://uploadkon.ir/uploads/793c06_26سام-دو-2-.mp3", "https://uploadkon.ir/uploads/937606_26سام-سه-2-.mp3", "https://uploadkon.ir/uploads/bcc006_26سام-چهار-2-.mp3"], sampleTexts:["", "", "توجه! یک موجود ناشناخته در حال نزدیک شدن است! همه هوشیار باشید! (با لحنی هراسآلود، اضطراری و هشداردهنده، شبیه به اعلام خطر در یک فیلم علمی-تخیلی) 👽🚨", "آه خدای من! این چه غذای لذیذی است! هرگز چیزی به این خوشمزگی نخورده بودم! (با لحنی سرشار از رضایت، لذت و شگفتی از طعم یک غذا) "] }, |
| { id: "Leda", name: "لیدا (زن)", gender: "female", desc: "کلاسیک و باوقار", imgUrl: "https://uploadkon.ir/uploads/710305_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۷۳۱.jpg", samples:["https://uploadkon.ir/uploads/547606_26لیدا-یک-2-.mp3", "https://uploadkon.ir/uploads/f9b706_26لیدا-دو-2-.mp3", "https://uploadkon.ir/uploads/312706_26لیدا-سه-2-.mp3", "https://uploadkon.ir/uploads/07e006_26لیدا-چهار-2-.mp3"], sampleTexts:["", "", "توجه! تمامی سیستمها در حالت اضطراری قرار گیرند! نفوذ شناسایی شد! (با لحنی کاملاً فنی، بیاحساس و هشداردهنده، شبیه به صدای یک کامپیوتر مرکزی در حالت خطر) 🤖🚨", "\"واو! این معرکه است! اصلاً فکرش رو هم نمیکردм که اینقدر خوب باشه!\" (با لحنی هیجانزده، پر از تحسین و کمی شوک مثبت)"] } |
| ]; |
| |
| |
| const standardView = document.getElementById('standard-view'); |
| const speakerModal = document.getElementById('speaker-modal'); |
| const speakerGridInModal = document.getElementById('speaker-grid'); |
| const selectedSpeakerIdStorage = document.getElementById('selected_speaker_id_storage'); |
| const changeSpeakerBtn = document.getElementById('change-speaker-btn'); |
| const selectedSpeakerCard = document.getElementById('selected-speaker-card'); |
| const selectedSpeakerImg = document.getElementById('selected-speaker-img'); |
| const selectedSpeakerName = document.getElementById('selected-speaker-name'); |
| const selectedSpeakerDesc = document.getElementById('selected-speaker-desc'); |
| const longTextModal = document.getElementById('long-text-modal'); |
| const confirmDeleteModal = document.getElementById('confirm-delete-modal'); |
| const mainOutputSection = document.getElementById('output-section-standard'); |
| const sampleAudioPlayer = document.getElementById('sample-audio-player'); |
| const sampleTextDisplay = document.getElementById('sample-text-display'); |
| const sampleTextContent = document.getElementById('sample-text-content'); |
| const mainAudioPlayer = document.getElementById('hidden-audio-player'); |
| |
| |
| function isWavFile(buffer) { if (buffer.byteLength < 4) return false; const view = new DataView(buffer); return (view.getUint8(0) === 82 && view.getUint8(1) === 73 && view.getUint8(2) === 70 && view.getUint8(3) === 70); } |
| |
| const showModal = (modal) => { if(modal) modal.classList.add('visible'); }; |
| const hideModal = (modal) => { if(modal) modal.classList.remove('visible'); if (modal && modal.id === 'speaker-modal') { sampleAudioPlayer.pause(); stopSamplePlayer(); } }; |
| |
| |
| function initPodcastDB() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(PODCAST_DB_NAME, PODCAST_DB_VERSION); |
| request.onupgradeneeded = (e) => { |
| podcastDB = e.target.result; |
| if (!podcastDB.objectStoreNames.contains('project')) { podcastDB.createObjectStore('project'); } |
| if (!podcastDB.objectStoreNames.contains('audioSegments')) { podcastDB.createObjectStore('audioSegments'); } |
| }; |
| request.onsuccess = (e) => { podcastDB = e.target.result; resolve(podcastDB); }; |
| request.onerror = (e) => reject(e); |
| }); |
| } |
| |
| function saveProjectState(data) { |
| if(!podcastDB) return; |
| const tx = podcastDB.transaction('project', 'readwrite'); |
| tx.objectStore('project').put(data, 'currentState'); |
| } |
| |
| async function saveAudioSegment(index, blob) { |
| if (!podcastDB) return; |
| return new Promise((resolve, reject) => { |
| const tx = podcastDB.transaction('audioSegments', 'readwrite'); |
| const request = blob ? tx.objectStore('audioSegments').put(blob, index) : tx.objectStore('audioSegments').delete(index); |
| tx.oncomplete = () => resolve(); |
| tx.onerror = () => reject(tx.error); |
| }); |
| } |
| |
| function loadProjectState() { |
| return new Promise((resolve, reject) => { |
| if (!podcastDB) return resolve(null); |
| const tx = podcastDB.transaction(['project', 'audioSegments'], 'readonly'); |
| const pStore = tx.objectStore('project'); |
| const aStore = tx.objectStore('audioSegments'); |
| const stateReq = pStore.get('currentState'); |
| const audioKeysReq = aStore.getAllKeys(); |
| const audioValuesReq = aStore.getAll(); |
| |
| Promise.all([ |
| new Promise(res => stateReq.onsuccess = () => res(stateReq.result)), |
| new Promise(res => audioKeysReq.onsuccess = () => res(audioKeysReq.result)), |
| new Promise(res => audioValuesReq.onsuccess = () => res(audioValuesReq.result)) |
| ]).then(([state, audioKeys, audioValues]) => { |
| const audioData =[]; |
| if (audioKeys && audioValues) { |
| audioKeys.forEach((key, i) => { audioData[key] = audioValues[i]; }); |
| } |
| resolve({ state, audioData }); |
| }).catch(reject); |
| }); |
| } |
| |
| async function clearProjectState() { |
| return new Promise((resolve, reject) => { |
| if(!podcastDB) return resolve(); |
| const tx = podcastDB.transaction(['project', 'audioSegments'], 'readwrite'); |
| tx.objectStore('project').clear(); |
| tx.objectStore('audioSegments').clear(); |
| tx.oncomplete = () => resolve(); |
| tx.onerror = () => reject(tx.error); |
| }); |
| } |
| |
| |
| const updateSelectedSpeakerDisplay = (speakerId) => { |
| let speaker = allSpeakers.find(s => s.id === speakerId) || allSpeakers[0]; |
| |
| selectedSpeakerImg.src = speaker.imgUrl; |
| selectedSpeakerName.textContent = speaker.name; |
| selectedSpeakerDesc.textContent = speaker.desc; |
| selectedSpeakerIdStorage.value = speaker.id; |
| |
| const isPodcastMode = !!document.querySelector('.podcast-studio-container'); |
| if (isPodcastMode && currentPodcastMode === 'single' && isMainSelectorAction) { |
| if (speaker && activePodcastSpeakers.length > 0 && activePodcastSpeakers[0].id !== speaker.id) { |
| activePodcastSpeakers[0] = speaker; |
| document.querySelectorAll('.podcast-studio-container .custom-select-container').forEach(sel => { |
| updatePodcastTurnSelect(sel, speaker.id); |
| }); |
| renderActivePodcastSpeakers(); |
| triggerSave(); |
| } |
| } |
| }; |
| |
| const showSampleText = (text) => { sampleTextContent.textContent = text; sampleTextDisplay.classList.add('active'); speakerGridInModal.style.paddingBottom = (sampleTextDisplay.offsetHeight + 40) + 'px'; }; |
| const hideSampleText = () => { sampleTextDisplay.classList.remove('active'); speakerGridInModal.style.paddingBottom = '0'; }; |
| const stopSamplePlayer = () => { const playingBtn = document.querySelector('.sample-play-btn.playing'); if (playingBtn) playingBtn.classList.remove('playing'); hideSampleText(); }; |
| sampleAudioPlayer.addEventListener('pause', stopSamplePlayer); |
| sampleAudioPlayer.addEventListener('ended', stopSamplePlayer); |
| |
| const createSpeakerCardsInModal = () => { |
| speakerGridInModal.innerHTML = ''; |
| |
| const podcastContainer = document.getElementById('project-speakers-container'); |
| const isPodcastView = podcastContainer && window.getComputedStyle(podcastContainer).display !== 'none'; |
| |
| allSpeakers.forEach((speaker) => { |
| const card = document.createElement('label'); |
| let isDimmed = false, isSelected = false; |
| |
| if (isPodcastView && !isMainSelectorAction) { |
| if (speakerToReplaceId) { |
| if (activePodcastSpeakers.some(s => s.id === speaker.id && s.id !== speakerToReplaceId)) isDimmed = true; |
| if (speaker.id === speakerToReplaceId) isSelected = true; |
| } else { |
| if (activePodcastSpeakers.some(s => s.id === speaker.id)) isDimmed = true; |
| } |
| } else { |
| if (speaker.id === selectedSpeakerIdStorage.value) isSelected = true; |
| } |
| |
| card.className = `speaker-card ${isDimmed ? 'already-selected' : ''}`; |
| |
| let samplesHTML = ''; |
| if (speaker.samples && speaker.samples.length > 0) { |
| samplesHTML += '<div class="sample-player-container">'; |
| speaker.samples.forEach((sampleUrl, index) => { |
| samplesHTML += `<button type="button" class="sample-play-btn" data-src="${sampleUrl}" data-text-index="${index}" data-speaker-id="${speaker.id}"><svg class="play-icon-svg" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"></path></svg><div class="playing-indicator"><span></span><span></span><span></span></div></button>`; |
| }); |
| samplesHTML += '</div>'; |
| } |
| |
| card.innerHTML = `<input type="radio" name="modal_speaker_selection" value="${speaker.id}" ${isSelected ? 'checked' : ''}><div class="speaker-visual"><img src="${speaker.imgUrl}" alt="${speaker.name}" loading="lazy">${samplesHTML}</div><div class="speaker-name">${speaker.name}</div>`; |
| |
| if (!isDimmed) { |
| card.addEventListener('click', (e) => { |
| if (e.target.closest('.sample-play-btn')) return; |
| e.preventDefault(); e.stopPropagation(); |
| const newSpeakerObj = speaker; |
| |
| if (isPodcastView && !isMainSelectorAction) { |
| if (speakerToReplaceId) { |
| const indexToReplace = activePodcastSpeakers.findIndex(s => s.id === speakerToReplaceId); |
| if (indexToReplace > -1) { |
| const oldId = activePodcastSpeakers[indexToReplace].id; |
| activePodcastSpeakers[indexToReplace] = newSpeakerObj; |
| document.querySelectorAll('.podcast-studio-container .custom-select-container').forEach(sel => { |
| if (sel.dataset.selectedId === oldId) { |
| sel.dataset.selectedId = newSpeakerObj.id; |
| updatePodcastTurnSelect(sel, newSpeakerObj.id); |
| } |
| }); |
| } |
| speakerToReplaceId = null; |
| } else { |
| activePodcastSpeakers.push(newSpeakerObj); |
| } |
| renderActivePodcastSpeakers(); |
| triggerSave(); |
| hideModal(speakerModal); |
| } else { |
| updateSelectedSpeakerDisplay(newSpeakerObj.id); |
| hideModal(speakerModal); |
| } |
| }); |
| } |
| |
| const sampleBtns = card.querySelectorAll('.sample-play-btn'); |
| sampleBtns.forEach(btn => { |
| btn.addEventListener('click', (e) => { |
| e.preventDefault(); e.stopPropagation(); |
| const audioSrc = btn.dataset.src; const textIndex = parseInt(btn.dataset.textIndex); const currentlyPlayingBtn = document.querySelector('.sample-play-btn.playing'); |
| if (currentlyPlayingBtn && currentlyPlayingBtn === btn) { sampleAudioPlayer.pause(); btn.classList.remove('playing'); hideSampleText(); } |
| else { |
| if (currentlyPlayingBtn) currentlyPlayingBtn.classList.remove('playing'); |
| sampleAudioPlayer.src = audioSrc; sampleAudioPlayer.play(); btn.classList.add('playing'); |
| if (speaker.sampleTexts[textIndex]) showSampleText(speaker.sampleTexts[textIndex]); else hideSampleText(); |
| } |
| }); |
| }); |
| speakerGridInModal.appendChild(card); |
| }); |
| |
| if (!isPodcastView || isMainSelectorAction) { |
| const podcastCard = document.createElement('div'); |
| podcastCard.className = 'ai-podcast-card'; |
| podcastCard.innerHTML = ` |
| <div class="ai-podcast-icon">🎙️</div> |
| <div class="ai-podcast-content"> |
| <div class="ai-podcast-title">ساخت پادکست هوشمند</div> |
| <div class="ai-podcast-subtitle">تبدیل موضوع یا مقاله به گفتگوی حرفهای</div> |
| </div> |
| <div class="ai-arrow-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M15 18l-6-6 6-6"/></svg></div> |
| `; |
| podcastCard.addEventListener('click', () => { |
| hideModal(speakerModal); |
| document.getElementById('ai-podcast-input').value = ''; |
| showModal(document.getElementById('ai-podcast-modal')); |
| }); |
| speakerGridInModal.appendChild(podcastCard); |
| } |
| }; |
| |
| |
| function showLoadingState(container, message, showProgressGrid = false, segmentCount = 0) { |
| container.classList.remove('has-content'); |
| container.style.display = 'flex'; |
| let progressGridHTML = ''; |
| if (showProgressGrid) { |
| let items = ''; |
| for (let i = 0; i < segmentCount; i++) items += `<div class="progress-item pending" id="progress-segment-${i}">${i + 1}</div>`; |
| progressGridHTML = `<div id="generation-progress-container"><div class="progress-grid">${items}</div></div>`; |
| } |
| container.innerHTML = `<div class="loading-animation-wrapper" style="display: flex;"><div class="orbital-loader"><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div><div class="orbit"><div class="satellite"></div></div></div><p class="loading-text" id="podcast-loading-text">${message}</p>${progressGridHTML}</div>`; |
| } |
| |
| function showErrorState(container, message) { |
| container.classList.remove('has-content'); |
| container.style.display = 'flex'; |
| container.innerHTML = `<div class="status-message error">${message || 'خطا در پردازش.'}</div>`; |
| } |
| |
| function splitText(text, maxLength = 450) { |
| const sentences = text.match(/[^.!?]+[.!?]*\s*/g) || [text]; |
| const chunks =[]; |
| let currentChunk = ""; |
| for (const sentence of sentences) { |
| if (currentChunk.length + sentence.length <= maxLength) currentChunk += sentence; |
| else { if (currentChunk) chunks.push(currentChunk.trim()); currentChunk = sentence; } |
| } |
| if (currentChunk) chunks.push(currentChunk.trim()); |
| return chunks; |
| } |
| |
| function triggerSave() { |
| const scriptTurns = document.querySelectorAll('.podcast-studio-container .script-turn'); |
| if (scriptTurns.length === 0) return; |
| const scriptData = Array.from(scriptTurns).map((turn) => ({ |
| speakerId: turn.querySelector('.custom-select-container').dataset.selectedId, |
| dialogue: turn.querySelector('textarea').value |
| })); |
| saveProjectState({ |
| mode: 'podcast', |
| originalText: document.getElementById('text-input-standard').value, |
| scriptData: scriptData, |
| activeSpeakers: activePodcastSpeakers.map(s => s.id) |
| }); |
| } |
| |
| function setupPodcastInterface(scriptData, container) { |
| container.className = ''; |
| container.style.cssText = 'margin-top: 3rem; min-height: auto; padding: 0; background: transparent; border: none; box-shadow: none;'; |
| container.innerHTML = ''; |
| |
| const studioContainer = document.createElement('div'); |
| studioContainer.className = 'podcast-studio-container'; |
| studioContainer.innerHTML = ` |
| <div class="form-group"><label>🎤 تیم گویندگان</label><div id="project-speakers-container"><div id="add-speaker-card" class="speaker-display-card"><div class="plus-icon">+</div><h3>افزودن گوینده</h3></div></div></div> |
| <div class="form-group"> |
| <label>📜 سناریوی گفتگو</label> |
| <div id="podcast-script-container"></div> |
| <button type="button" id="add-turn-btn">+ افزودن نوبت گفتگو</button> |
| </div> |
| <button type="submit" id="generate-btn-podcast" class="generate-btn"><span class="btn-text">🎙️ ساخت فایل صوتی نهایی</span><div class="spinner"></div></button> |
| <div id="podcast-output-section" class="output-section" style="margin-top: 2rem; border-style: solid; background-color: transparent;"> |
| <div class="status-message">فایل نهایی در اینجا ظاهر خواهد شد.</div> |
| </div> |
| <button type="button" id="clear-history-btn-podcast"> |
| <svg viewBox="0 0 24 24"><path d="M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z"></path></svg> |
| حذف کامل پروژه و بازگشت |
| </button> |
| `; |
| container.appendChild(studioContainer); |
| |
| if (activePodcastSpeakers.length === 0) { |
| activePodcastSpeakers =[...new Set(scriptData.map(item => item.speakerId))].map(id => allSpeakers.find(s => s.id === id)).filter(Boolean); |
| } |
| if (activePodcastSpeakers.length === 0 && scriptData.length > 0) activePodcastSpeakers = [allSpeakers[0]]; |
| |
| renderActivePodcastSpeakers(); |
| const scriptContainer = studioContainer.querySelector('#podcast-script-container'); |
| scriptData.forEach(turn => addPodcastScriptTurn(turn.speakerId, turn.dialogue, scriptContainer)); |
| |
| studioContainer.querySelector('#add-speaker-card').addEventListener('click', () => { isMainSelectorAction = false; speakerToReplaceId = null; createSpeakerCardsInModal(); showModal(speakerModal); }); |
| studioContainer.querySelector('#add-turn-btn').addEventListener('click', () => { |
| if (activePodcastSpeakers.length === 0) return alert('لطفا ابتدا یک گوینده به پروژه اضافه کنید.'); |
| lastAddedSpeakerIndex = (lastAddedSpeakerIndex + 1) % activePodcastSpeakers.length; |
| addPodcastScriptTurn(activePodcastSpeakers[lastAddedSpeakerIndex].id, '', scriptContainer); |
| triggerSave(); |
| }); |
| studioContainer.querySelector('#generate-btn-podcast').addEventListener('click', generatePodcastAudio); |
| studioContainer.querySelector('#clear-history-btn-podcast').addEventListener('click', () => showModal(confirmDeleteModal)); |
| |
| triggerSave(); |
| studioContainer.scrollIntoView({ behavior: 'smooth' }); |
| } |
| |
| function renderActivePodcastSpeakers() { |
| const container = document.getElementById('project-speakers-container'); |
| if (!container) return; |
| container.querySelectorAll('.speaker-display-card:not(#add-speaker-card)').forEach(c => c.remove()); |
| activePodcastSpeakers.forEach(speaker => { |
| const card = document.createElement('div'); |
| card.className = 'speaker-display-card'; |
| card.innerHTML = `<button type="button" class="remove-speaker-btn">×</button><img src="${speaker.imgUrl}" alt="${speaker.name}"><h3>${speaker.name}</h3>`; |
| container.insertBefore(card, container.querySelector('#add-speaker-card')); |
| card.querySelector('.remove-speaker-btn').addEventListener('click', (e) => { e.stopPropagation(); removePodcastSpeaker(speaker.id); }); |
| card.addEventListener('click', () => { isMainSelectorAction = false; speakerToReplaceId = speaker.id; createSpeakerCardsInModal(); showModal(speakerModal); }); |
| }); |
| document.querySelectorAll('.podcast-studio-container .custom-select-container').forEach(c => updatePodcastTurnSelect(c, c.dataset.selectedId)); |
| } |
| |
| function removePodcastSpeaker(speakerId) { |
| if (activePodcastSpeakers.length <= 1) { alert("پروژه باید حداقل یک گوینده داشته باشد."); return; } |
| activePodcastSpeakers = activePodcastSpeakers.filter(s => s.id !== speakerId); |
| const newSpeakerId = activePodcastSpeakers[0].id; |
| document.querySelectorAll(`.podcast-studio-container .custom-select-container[data-selected-id="${speakerId}"]`).forEach(sel => { |
| updatePodcastTurnSelect(sel, newSpeakerId); |
| }); |
| renderActivePodcastSpeakers(); |
| triggerSave(); |
| } |
| |
| function addPodcastScriptTurn(speakerId, text, container) { |
| const turnDiv = document.createElement('div'); |
| turnDiv.className = 'script-turn'; |
| const turnIndex = container.children.length; |
| turnDiv.innerHTML = ` |
| <div class="turn-speaker-selector"><div class="custom-select-container" data-selected-id="${speakerId}"><div class="custom-select-trigger"></div><div class="custom-select-options"></div></div></div> |
| <div class="turn-content"> |
| <textarea>${text || ''}</textarea> |
| <div class="turn-player-container"> |
| <button type="button" class="turn-play-btn"><svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg><svg viewBox="0 0 24 24" class="pause-icon"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg></button> |
| <audio class="turn-audio" style="display:none;"></audio> |
| <div class="loading-state-wrapper" style="display:none;"><div class="spinner"></div><span class="loading-text">در حال جایگزینی...</span></div> |
| <button type="button" class="turn-retry-btn"><svg viewBox="0 0 24 24"><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"></path></svg> جایگزین قطعه</button> |
| <span class="replace-success-message">جایگزین شد</span> |
| <span class="main-update-message">فایل اصلی بروز شد</span> |
| </div> |
| </div> |
| <button type="button" class="remove-turn-btn" title="حذف نوبت">×</button>`; |
| container.appendChild(turnDiv); |
| |
| turnDiv.querySelector('textarea').addEventListener('input', triggerSave); |
| updatePodcastTurnSelect(turnDiv.querySelector('.custom-select-container'), speakerId); |
| turnDiv.querySelector('.turn-retry-btn').addEventListener('click', () => handleRetry(turnDiv, turnIndex)); |
| turnDiv.querySelector('.remove-turn-btn').addEventListener('click', async () => { |
| const index = Array.from(container.children).indexOf(turnDiv); |
| if (index > -1) { |
| podcastMasterAudioBlobs[index] = null; |
| await saveAudioSegment(index, null); |
| turnDiv.remove(); |
| if (podcastAudioPlayer.src) await rebuildFinalAudio(false); |
| triggerSave(); |
| } |
| }); |
| } |
| |
| function updatePodcastTurnSelect(container, selectedId) { |
| if(!container) return; |
| const trigger = container.querySelector('.custom-select-trigger'); |
| const optionsDiv = container.querySelector('.custom-select-options'); |
| optionsDiv.innerHTML = ''; |
| let selectedSpeaker = activePodcastSpeakers.find(s => s.id === selectedId) || activePodcastSpeakers[0]; |
| if(!selectedSpeaker) return; |
| |
| trigger.innerHTML = `<img src="${selectedSpeaker.imgUrl}"><span>${selectedSpeaker.name}</span><svg class="arrow" viewBox="0 0 24 24"><path fill="currentColor" d="M7,10L12,15L17,10H7Z"></path></svg>`; |
| activePodcastSpeakers.forEach(s => { |
| const opt = document.createElement('div'); |
| opt.className = 'custom-select-option'; |
| opt.dataset.id = s.id; |
| opt.innerHTML = `<img src="${s.imgUrl}"><span>${s.name}</span>`; |
| optionsDiv.appendChild(opt); |
| }); |
| container.dataset.selectedId = selectedSpeaker.id; |
| } |
| |
| async function initiateAiPodcastGeneration(userText) { |
| hideModal(longTextModal); |
| await clearProjectState(); |
| activePodcastSpeakers =[]; |
| podcastMasterAudioBlobs =[]; |
| |
| showLoadingState(mainOutputSection, "در حال اتصال به هوش مصنوعی..."); |
| |
| try { |
| const response = await fetch(`${API_BASE}/create-full-podcast`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| prompt: userText, |
| available_speakers: allSpeakers.map(s => ({ id: s.id, name: s.name, gender: s.gender })) |
| }) |
| }); |
| |
| if (!response.ok) throw new Error("خطای سرور در ساخت پادکست"); |
| const data = await response.json(); |
| if (!data.task_id) throw new Error('شناسه تسک دریافت نشد.'); |
| |
| const taskId = data.task_id; |
| showLoadingState(mainOutputSection, "هوش مصنوعی در حال نوشتن سناریو..."); |
| |
| const pollInterval = setInterval(async () => { |
| try { |
| const statusRes = await fetch(`${API_BASE}/podcast-status/${taskId}`); |
| if (!statusRes.ok) throw new Error("خطا در بررسی وضعیت"); |
| |
| const result = await statusRes.json(); |
| |
| if (result.status === 'completed') { |
| clearInterval(pollInterval); |
| if (result.data && result.data.selected_speakers) { |
| activePodcastSpeakers = result.data.selected_speakers.map(id => allSpeakers.find(s => s.id === id)).filter(Boolean); |
| } |
| if (activePodcastSpeakers.length === 0) activePodcastSpeakers = [allSpeakers[0]]; |
| |
| if (result.data && result.data.script) { |
| const aiScriptData = result.data.script.map(turn => ({ speakerId: turn.speaker_id, dialogue: turn.dialogue })); |
| setupPodcastInterface(aiScriptData, mainOutputSection); |
| } else throw new Error("فرمت سناریوی دریافتی صحیح نیست."); |
| |
| } else if (result.status === 'failed') { |
| clearInterval(pollInterval); |
| showErrorState(mainOutputSection, "هوش مصنوعی موفق به پردازش متن نشد."); |
| } |
| } catch (e) { |
| clearInterval(pollInterval); |
| showErrorState(mainOutputSection, "خطا: " + e.message); |
| } |
| }, 3000); |
| |
| } catch (error) { |
| showErrorState(mainOutputSection, error.message); |
| } |
| } |
| |
| async function initiatePodcastConversion(mode) { |
| const text = document.getElementById('text-input-standard').value.trim(); |
| if(mode === 'ai-podcast'){ |
| if (!text) { alert('لطفا ابتدا متنی وارد کنید.'); return; } |
| initiateAiPodcastGeneration(text); |
| return; |
| } |
| |
| hideModal(longTextModal); |
| await clearProjectState(); |
| activePodcastSpeakers =[]; |
| podcastMasterAudioBlobs =[]; |
| |
| const chunks = splitText(text); |
| let scriptData =[]; |
| let speakersPool =[]; |
| |
| if (mode === 'single') { |
| const currentId = selectedSpeakerIdStorage.value; |
| speakersPool.push(allSpeakers.find(s => s.id === currentId) || allSpeakers[0]); |
| } else { |
| const numSpeakers = (mode === 'double') ? 2 : 3; |
| const shuffledSpeakers = [...allSpeakers].sort(() => 0.5 - Math.random()); |
| speakersPool = shuffledSpeakers.slice(0, numSpeakers); |
| } |
| |
| chunks.forEach((chunk, index) => { |
| scriptData.push({ speakerId: speakersPool[index % speakersPool.length].id, dialogue: chunk }); |
| }); |
| |
| setupPodcastInterface(scriptData, mainOutputSection); |
| } |
| |
| async function asyncPool(poolLimit, array, iteratorFn) { |
| const ret =[]; const executing =[]; |
| for (const item of array) { |
| const p = Promise.resolve().then(() => iteratorFn(item)); |
| ret.push(p); |
| if (poolLimit <= array.length) { |
| const e = p.then(() => executing.splice(executing.indexOf(e), 1)); |
| executing.push(e); |
| if (executing.length >= poolLimit) await Promise.race(executing); |
| } |
| } |
| return Promise.all(ret); |
| } |
| |
| function generateSegmentWithRetry(segment, loadingTextElement, totalSegments, completedCounter) { |
| if (podcastMasterAudioBlobs[segment.index]) { |
| const progressItem = document.getElementById(`progress-segment-${segment.index}`); |
| if (progressItem) progressItem.className = 'progress-item done'; |
| completedCounter.count++; |
| if (loadingTextElement) loadingTextElement.textContent = `در حال پردازش (${completedCounter.count} از ${totalSegments} تکمیل شده)`; |
| setupTurnPlayer(segment.element, segment.index); |
| return Promise.resolve(); |
| } |
| |
| return new Promise(async (resolve, reject) => { |
| let attempts = 0; |
| const maxAttempts = 3; |
| |
| while (attempts < maxAttempts) { |
| attempts++; |
| try { |
| const response = await fetch(`${API_BASE}/generate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| text: segment.dialogue, |
| speaker: segment.speakerId, |
| temperature: 1.5, |
| prompt: '' |
| }) |
| }); |
| |
| if (!response.ok) throw new Error("خطای سرور"); |
| |
| const responseBuffer = await response.arrayBuffer(); |
| let audioBlob = null; |
| let jobId = null; |
| |
| if (isWavFile(responseBuffer)) { |
| audioBlob = new Blob([responseBuffer], { type: 'audio/wav' }); |
| } else { |
| const data = JSON.parse(new TextDecoder().decode(responseBuffer)); |
| if (data.job_id) jobId = data.job_id; |
| else throw new Error("خطای پاسخ سرور"); |
| } |
| |
| if (audioBlob) { |
| podcastMasterAudioBlobs[segment.index] = audioBlob; |
| await saveAudioSegment(segment.index, audioBlob); |
| setupTurnPlayer(segment.element, segment.index); |
| completedCounter.count++; |
| if (loadingTextElement) loadingTextElement.textContent = `در حال پردازش (${completedCounter.count} از ${totalSegments} تکمیل شده)`; |
| const progressItem = document.getElementById(`progress-segment-${segment.index}`); |
| if (progressItem) progressItem.className = 'progress-item done'; |
| resolve(); return; |
| } |
| |
| if (jobId) { |
| let pollAttempts = 0; |
| const poll = setInterval(async () => { |
| pollAttempts++; |
| if (pollAttempts > 60) { clearInterval(poll); reject(new Error('پایان زمان انتظار')); return; } |
| try { |
| const statusRes = await fetch(`${API_BASE}/check_status`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ job_id: jobId }) |
| }); |
| |
| if(!statusRes.ok) throw new Error("خطا در بررسی وضعیت"); |
| const statusBuffer = await statusRes.arrayBuffer(); |
| if (isWavFile(statusBuffer)) { |
| clearInterval(poll); |
| const finalBlob = new Blob([statusBuffer], { type: 'audio/wav' }); |
| podcastMasterAudioBlobs[segment.index] = finalBlob; |
| await saveAudioSegment(segment.index, finalBlob); |
| setupTurnPlayer(segment.element, segment.index); |
| completedCounter.count++; |
| if (loadingTextElement) loadingTextElement.textContent = `در حال پردازش (${completedCounter.count} از ${totalSegments} تکمیل شده)`; |
| const progressItem = document.getElementById(`progress-segment-${segment.index}`); |
| if (progressItem) progressItem.className = 'progress-item done'; |
| resolve(); |
| } else { |
| const statusData = JSON.parse(new TextDecoder().decode(statusBuffer)); |
| if (statusData.status === 'error' || statusData.status === 'failed') { |
| clearInterval(poll); reject(new Error('پردازش ناموفق بود')); |
| } |
| } |
| } catch (e) { |
| clearInterval(poll); reject(e); |
| } |
| }, 3000); |
| return; |
| } |
| |
| } catch (error) { |
| if (attempts >= maxAttempts) { |
| const progressItem = document.getElementById(`progress-segment-${segment.index}`); |
| if(progressItem) progressItem.className = 'progress-item error'; |
| reject(error); return; |
| } |
| await new Promise(res => setTimeout(res, 3000)); |
| } |
| } |
| }); |
| } |
| |
| async function generatePodcastAudio() { |
| const generateBtn = document.getElementById('generate-btn-podcast'); |
| const outputSection = document.getElementById('podcast-output-section'); |
| if (generateBtn.disabled) return; |
| |
| triggerSave(); |
| const scriptData = Array.from(document.querySelectorAll('.podcast-studio-container .script-turn')).map((turn, index) => ({ |
| element: turn, index: index, |
| speakerId: turn.querySelector('.custom-select-container').dataset.selectedId, |
| dialogue: turn.querySelector('textarea').value.trim() |
| })); |
| |
| showLoadingState(outputSection, `در حال پردازش (0 از ${scriptData.length} تکمیل شده)`, true, scriptData.length); |
| const loadingTextElement = document.getElementById('podcast-loading-text'); |
| generateBtn.querySelector(".spinner").style.display = 'inline-block'; |
| generateBtn.querySelector(".btn-text").textContent = 'در حال ساخت...'; |
| generateBtn.disabled = true; |
| |
| let completedCounter = { count: 0 }; |
| try { |
| await asyncPool(5, scriptData, (segment) => generateSegmentWithRetry(segment, loadingTextElement, scriptData.length, completedCounter)); |
| showLoadingState(outputSection, "در حال ترکیب نهایی صدا..."); |
| await rebuildFinalAudio(true); |
| } catch (error) { |
| showErrorState(outputSection, error.message); |
| } finally { |
| generateBtn.querySelector(".spinner").style.display = 'none'; |
| generateBtn.querySelector(".btn-text").textContent = '🎙️ ساخت فایل صوتی نهایی'; |
| generateBtn.disabled = false; |
| } |
| } |
| |
| async function handleRetry(turnDiv, turnIndex) { |
| const playerContainer = turnDiv.querySelector('.turn-player-container'); |
| const retryBtn = playerContainer.querySelector('.turn-retry-btn'); |
| const loadingWrapper = playerContainer.querySelector('.loading-state-wrapper'); |
| const successMsg = playerContainer.querySelector('.replace-success-message'); |
| const mainMsg = playerContainer.querySelector('.main-update-message'); |
| |
| retryBtn.style.display = 'none'; |
| loadingWrapper.style.display = 'flex'; |
| successMsg.style.display = 'none'; |
| mainMsg.style.display = 'none'; |
| |
| try { |
| const response = await fetch(`${API_BASE}/generate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| text: turnDiv.querySelector('textarea').value.trim(), |
| speaker: turnDiv.querySelector('.custom-select-container').dataset.selectedId, |
| temperature: 1.5, prompt: '' |
| }) |
| }); |
| |
| if (!response.ok) throw new Error('خطا در تولید'); |
| |
| const responseBuffer = await response.arrayBuffer(); |
| let finalBlob = null, jobId = null; |
| |
| if (isWavFile(responseBuffer)) finalBlob = new Blob([responseBuffer], { type: 'audio/wav' }); |
| else { |
| const data = JSON.parse(new TextDecoder("utf-8").decode(responseBuffer)); |
| if (data.job_id) jobId = data.job_id; |
| } |
| |
| if (!finalBlob && jobId) { |
| finalBlob = await new Promise((resolve, reject) => { |
| const poll = setInterval(async () => { |
| try { |
| const statusRes = await fetch(`${API_BASE}/check_status`, { |
| method: 'POST', headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({ job_id: jobId }) |
| }); |
| const statusBuffer = await statusRes.arrayBuffer(); |
| if (isWavFile(statusBuffer)) { |
| clearInterval(poll); resolve(new Blob([statusBuffer], { type: 'audio/wav' })); |
| } else { |
| const statusData = JSON.parse(new TextDecoder().decode(statusBuffer)); |
| if (statusData.status === 'error' || statusData.status === 'failed') { clearInterval(poll); reject(new Error('Generation failed.')); } |
| } |
| } catch { clearInterval(poll); reject(new Error('Polling failed.')); } |
| }, 3000); |
| }); |
| } |
| if (!finalBlob) throw new Error('مشکل در دریافت فایل.'); |
| |
| podcastMasterAudioBlobs[turnIndex] = finalBlob; |
| await saveAudioSegment(turnIndex, finalBlob); |
| setupTurnPlayer(turnDiv, turnIndex); |
| |
| loadingWrapper.style.display = 'none'; |
| successMsg.style.display = 'flex'; |
| successMsg.style.animation = 'fadeInOut 2.5s forwards'; |
| |
| setTimeout(async () => { |
| successMsg.style.display = 'none'; |
| if (podcastMasterAudioBlobs.every(b => b)) { |
| mainMsg.style.display = 'flex'; |
| mainMsg.style.animation = 'fadeInOut 2.5s forwards'; |
| await rebuildFinalAudio(false); |
| } |
| setTimeout(() => { mainMsg.style.display = 'none'; retryBtn.style.display = 'flex'; }, 2500); |
| }, 2500); |
| } catch (error) { |
| alert("خطا در جایگزینی قطعه."); |
| loadingWrapper.style.display = 'none'; retryBtn.style.display = 'flex'; |
| } |
| } |
| |
| async function rebuildFinalAudio(isInitialBuild = false) { |
| const outputSection = document.getElementById('podcast-output-section'); |
| if (!outputSection) return; |
| const mergedBlob = await mergeAudioBlobs(podcastMasterAudioBlobs.filter(Boolean)); |
| |
| if (mergedBlob) { |
| outputSection.innerHTML = ''; outputSection.classList.add('has-content'); |
| const playerContent = document.createElement('div'); |
| playerContent.className = 'audio-player-content'; playerContent.style.display = 'block'; |
| outputSection.appendChild(playerContent); |
| |
| const audioUrl = URL.createObjectURL(mergedBlob); |
| podcastAudioPlayer.src = audioUrl; |
| |
| playerContent.innerHTML = `<div id="final-podcast-player"></div>`; |
| createPlayerInstance('final-podcast-player', podcastAudioPlayer); |
| const newDownloadBtn = outputSection.querySelector('.audio-download-btn-new'); |
| if(newDownloadBtn) newDownloadBtn.href = audioUrl; |
| |
| if (isInitialBuild) { document.querySelectorAll('.podcast-studio-container .script-turn').forEach((turn, index) => { setupTurnPlayer(turn, index); }); } |
| } else if (isInitialBuild) { |
| showErrorState(outputSection, "خطا در ترکیب فایل نهایی."); |
| } |
| } |
| |
| function setupTurnPlayer(turnDiv, index) { |
| const playerContainer = turnDiv.querySelector('.turn-player-container'); |
| const playBtn = playerContainer.querySelector('.turn-play-btn'); |
| const audio = playerContainer.querySelector('.turn-audio'); |
| const blob = podcastMasterAudioBlobs[index]; |
| if (blob) { |
| if (audio.src) URL.revokeObjectURL(audio.src); |
| audio.src = URL.createObjectURL(blob); |
| playerContainer.classList.add('visible'); |
| |
| playBtn.onclick = () => { |
| if (currentlyPlayingTurnPlayer && currentlyPlayingTurnPlayer !== audio) currentlyPlayingTurnPlayer.pause(); |
| if (audio.paused) { audio.play(); currentlyPlayingTurnPlayer = audio; } else audio.pause(); |
| }; |
| audio.onplay = () => playerContainer.classList.add('playing'); |
| audio.onpause = audio.onended = () => { |
| playerContainer.classList.remove('playing'); |
| if(currentlyPlayingTurnPlayer === audio) currentlyPlayingTurnPlayer = null; |
| }; |
| } |
| } |
| |
| async function mergeAudioBlobs(blobs) { |
| if (!blobs || blobs.length === 0) return null; |
| if (blobs.length === 1) return blobs[0]; |
| const decodedBuffers = await Promise.all(blobs.map(blob => blob.arrayBuffer().then(buffer => audioContext.decodeAudioData(buffer)))); |
| const totalLength = decodedBuffers.reduce((acc, buffer) => acc + buffer.length, 0); |
| const finalBuffer = audioContext.createBuffer(decodedBuffers[0].numberOfChannels, totalLength, decodedBuffers[0].sampleRate); |
| let offset = 0; |
| for (const buffer of decodedBuffers) { |
| for (let channel = 0; channel < buffer.numberOfChannels; channel++) finalBuffer.getChannelData(channel).set(buffer.getChannelData(channel), offset); |
| offset += buffer.length; |
| } |
| return bufferToWave(finalBuffer); |
| } |
| |
| function bufferToWave(abuffer) { |
| const numOfChan = abuffer.numberOfChannels, length = abuffer.length * numOfChan * 2 + 44, buffer = new ArrayBuffer(length), view = new DataView(buffer), channels =[]; let i, sample, offset = 0, pos = 0; |
| const setUint16 = (data) => { view.setUint16(pos, data, true); pos += 2; }; |
| const setUint32 = (data) => { view.setUint32(pos, data, true); pos += 4; }; |
| setUint32(0x46464952); setUint32(length - 8); setUint32(0x45564157); setUint32(0x20746d66); setUint32(16); setUint16(1); setUint16(numOfChan); setUint32(abuffer.sampleRate); setUint32(abuffer.sampleRate * 2 * numOfChan); setUint16(numOfChan * 2); setUint16(16); setUint32(0x61746164); setUint32(length - pos - 4); |
| for (i = 0; i < abuffer.numberOfChannels; i++) channels.push(abuffer.getChannelData(i)); |
| while (pos < length) { for (i = 0; i < numOfChan; i++) { sample = Math.max(-1, Math.min(1, channels[i][offset])); sample = (0.5 + sample < 0 ? sample * 32768 : sample * 32767) | 0; view.setInt16(pos, sample, true); pos += 2; } offset++; } |
| return new Blob([view], { type: 'audio/wav' }); |
| } |
| |
| |
| const formatTime = (s) => { if (isNaN(s) || s < 0) return '0:00'; const m = Math.floor(s / 60); return `${m}:${Math.floor(s % 60).toString().padStart(2, '0')}`; }; |
| |
| function createPlayerInstance(containerId, audioPlayer = mainAudioPlayer) { |
| const playerContent = document.getElementById(containerId); |
| if (!playerContent) return; |
| playerContent.innerHTML = `<div class="simple-player-container"><div class="play-controls"><button type="button" class="play-pause-btn-simple" aria-label="Play/Pause"><svg viewBox="0 0 24 24" class="play-icon"><path d="M8 5v14l11-7z"></path></svg><svg viewBox="0 0 24 24" class="pause-icon" style="display:none;"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"></path></svg></button><div class="progress-wrapper"><span class="time-display current-time">0:00</span><input type="range" class="audio-progress-bar" value="0" min="0" max="100" step="0.1"><span class="time-display total-time">0:00</span></div></div><a href="#" class="audio-download-btn-new" download="ai_sada_output.wav" title="دانلود فایل صوتی"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19.35 10.04A7.49 7.49 0 0 0 12 4C9.11 4 6.6 5.64 5.35 8.04A5.994 5.994 0 0 0 0 14a6 6 0 0 0 6 6h13a5 5 0 0 0 5-5c0-2.64-2.05-4.78-4.65-4.96zM17 13l-5 5-5-5h3V9h4v4h3z"></path></svg><span>دانلود فایل صوتی</span></a></div>`; |
| const playPauseBtn = playerContent.querySelector('.play-pause-btn-simple'); |
| const progressBar = playerContent.querySelector('.audio-progress-bar'); |
| |
| playPauseBtn.addEventListener('click', () => { audioPlayer.paused ? audioPlayer.play() : audioPlayer.pause(); }); |
| progressBar.addEventListener('input', () => { if (!isNaN(audioPlayer.duration)) audioPlayer.currentTime = (progressBar.value / 100) * audioPlayer.duration; }); |
| audioPlayer.addEventListener('timeupdate', () => { |
| const { currentTime, duration } = audioPlayer; |
| progressBar.value = duration > 0 ? (currentTime / duration) * 100 : 0; |
| playerContent.querySelector('.current-time').textContent = formatTime(currentTime); |
| if (isFinite(duration)) playerContent.querySelector('.total-time').textContent = formatTime(duration); |
| }); |
| audioPlayer.addEventListener('loadedmetadata', () => { if (isFinite(audioPlayer.duration)) playerContent.querySelector('.total-time').textContent = formatTime(audioPlayer.duration); }); |
| audioPlayer.addEventListener('play', () => { playPauseBtn.querySelector('.play-icon').style.display = 'none'; playPauseBtn.querySelector('.pause-icon').style.display = 'block'; }); |
| audioPlayer.addEventListener('pause', () => { playPauseBtn.querySelector('.play-icon').style.display = 'block'; playPauseBtn.querySelector('.pause-icon').style.display = 'none'; }); |
| audioPlayer.addEventListener('ended', () => { playPauseBtn.querySelector('.play-icon').style.display = 'block'; playPauseBtn.querySelector('.pause-icon').style.display = 'none'; progressBar.value = 0; playerContent.querySelector('.current-time').textContent = "0:00"; }); |
| } |
| |
| (function() { |
| const form = document.getElementById('standard-tts-form'); if (!form) return; |
| const textInput = form.querySelector('#text-input-standard'), promptInput = form.querySelector('#prompt-input-standard'), generateBtn = form.querySelector('#generate-btn-standard'), btnText = generateBtn.querySelector('.btn-text'), btnSpinner = generateBtn.querySelector('.spinner'), outputSection = document.getElementById('output-section-standard'), statusMessage = outputSection.querySelector('#status-message-standard'), loadingAnimation = outputSection.querySelector('#loading-animation-wrapper-standard'), playerContent = outputSection.querySelector('#audio-player-content-standard'), charCount = form.querySelector('#char-count'), charMax = form.querySelector('#char-max'); |
| |
| const MAX_CHARS = 80000; |
| if(charMax) charMax.textContent = MAX_CHARS.toLocaleString('fa-IR'); |
| |
| textInput.addEventListener('input', () => { |
| const len = textInput.value.length; |
| charCount.textContent = len.toLocaleString('fa-IR'); |
| charCount.style.color = len > MAX_CHARS ? 'red' : 'var(--accent-primary)'; |
| }); |
| |
| const showResultState = (isSuccess, msg = '') => { |
| loadingAnimation.style.display = 'none'; |
| if (isSuccess) { playerContent.style.display = 'block'; outputSection.classList.add('has-content'); } |
| else { statusMessage.innerHTML = msg || 'خطا.'; statusMessage.style.display = 'block'; statusMessage.classList.add('error'); } |
| generateBtn.disabled = false; btnSpinner.style.display = 'none'; btnText.textContent = '✨ خلق صدا با آلفا'; |
| }; |
| |
| form.addEventListener('submit', async () => { |
| if (generateBtn.disabled) return; |
| if (document.querySelector('.podcast-studio-container')) { |
| alert('یک پروژه پادکست فعال است. لطفاً ابتدا پروژه قبلی را پاک کنید.'); |
| return; |
| } |
| const text = textInput.value.trim(); |
| if (!text) { showResultState(false, '<b>خطا:</b> متن ورودی خالی است.'); return; } |
| if (text.length > 500) { showModal(longTextModal); return; } |
| |
| generateBtn.disabled = true; btnSpinner.style.display = 'inline-block'; btnText.textContent = 'در حال پردازش...'; playerContent.style.display = 'none'; outputSection.classList.remove('has-content'); statusMessage.style.display = 'none'; loadingAnimation.style.display = 'flex'; mainAudioPlayer.src = ''; |
| |
| try { |
| const response = await fetch(`${API_BASE}/generate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| text: text, |
| prompt: promptInput.value, |
| speaker: selectedSpeakerIdStorage.value, |
| temperature: 1.5 |
| }) |
| }); |
| |
| if (!response.ok) throw new Error("خطای ارتباط با سرور"); |
| const blob = await response.blob(); |
| const audioUrl = URL.createObjectURL(blob); |
| |
| mainAudioPlayer.src = audioUrl; |
| const downloadLink = playerContent.querySelector('.audio-download-btn-new'); |
| if (downloadLink) downloadLink.href = audioUrl; |
| |
| showResultState(true); |
| } catch (error) { |
| showResultState(false, `<b>خطا:</b> ${error.message}`); |
| } |
| }); |
| createPlayerInstance('audio-player-content-standard'); |
| })(); |
| |
| |
| function setupEventListeners() { |
| [changeSpeakerBtn, selectedSpeakerCard].forEach(el => el && el.addEventListener('click', () => { isMainSelectorAction = true; createSpeakerCardsInModal(); showModal(speakerModal); })); |
| document.querySelectorAll('.modal-overlay').forEach(o => o.addEventListener('click', (e) => (e.target === o) && hideModal(o))); |
| document.querySelectorAll('.close-modal-btn').forEach(b => b.addEventListener('click', () => hideModal(b.closest('.modal-overlay')))); |
| |
| longTextModal.querySelectorAll('.option-card').forEach(card => card.addEventListener('click', (e) => { |
| initiatePodcastConversion(e.currentTarget.dataset.mode); |
| })); |
| |
| document.getElementById('cancel-delete-btn').addEventListener('click', () => hideModal(confirmDeleteModal)); |
| document.getElementById('confirm-delete-action-btn').addEventListener('click', async () => { await clearProjectState(); hideModal(confirmDeleteModal); window.location.reload(); }); |
| document.getElementById('start-ai-podcast-action-btn').addEventListener('click', () => { const text = document.getElementById('ai-podcast-input').value.trim(); if (!text) { alert('لطفاً موضوع را وارد کنید.'); return; } hideModal(document.getElementById('ai-podcast-modal')); currentPodcastMode = 'ai-podcast'; initiateAiPodcastGeneration(text); }); |
| |
| document.body.addEventListener('click', (e) => { |
| const selectTrigger = e.target.closest('.podcast-studio-container .custom-select-trigger'); |
| const selectOption = e.target.closest('.podcast-studio-container .custom-select-option'); |
| document.querySelectorAll('.podcast-studio-container .custom-select-container.open').forEach(openContainer => { if (!selectTrigger || openContainer !== selectTrigger.parentElement) openContainer.classList.remove('open'); }); |
| if (selectTrigger) selectTrigger.parentElement.classList.toggle('open'); |
| else if (selectOption) { const container = selectOption.closest('.custom-select-container'); updatePodcastTurnSelect(container, selectOption.dataset.id); container.classList.remove('open'); triggerSave(); } |
| }); |
| } |
| |
| async function initializeApp() { |
| await initPodcastDB(); |
| setupEventListeners(); |
| |
| const savedData = await loadProjectState(); |
| if (savedData && savedData.state && savedData.state.mode === 'podcast') { |
| const mainTextInput = document.getElementById('text-input-standard'); |
| if (savedData.state.originalText) { |
| mainTextInput.value = savedData.state.originalText; |
| document.getElementById('char-count').textContent = savedData.state.originalText.length.toLocaleString('fa-IR'); |
| } |
| |
| podcastMasterAudioBlobs = savedData.audioData ||[]; |
| if (savedData.state.activeSpeakers) { |
| activePodcastSpeakers = savedData.state.activeSpeakers.map(id => allSpeakers.find(s => s.id === id)).filter(Boolean); |
| } |
| if (activePodcastSpeakers.length === 1) currentPodcastMode = 'single'; |
| else if (activePodcastSpeakers.length > 1) currentPodcastMode = 'multi'; |
| |
| setupPodcastInterface(savedData.state.scriptData, mainOutputSection); |
| document.querySelectorAll('.podcast-studio-container .script-turn').forEach((turn, index) => { if(podcastMasterAudioBlobs[index]) setupTurnPlayer(turn, index); }); |
| if(podcastMasterAudioBlobs.filter(Boolean).length > 0) await rebuildFinalAudio(false); |
| |
| const initialSpeakerId = (activePodcastSpeakers.length > 0) ? activePodcastSpeakers[0].id : allSpeakers[0].id; |
| updateSelectedSpeakerDisplay(initialSpeakerId); |
| } else { |
| updateSelectedSpeakerDisplay(selectedSpeakerIdStorage.value || allSpeakers[0].id); |
| } |
| } |
| |
| initializeApp(); |
| }); |
| </script> |
| </body> |
| </html> |