| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Alpha TTS - استودیوی پادکست</title> |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;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); |
| --accent-premium: #FFC107; |
| --accent-premium-glow: rgba(255, 193, 7, 0.3); |
| --waveform-color-active: var(--accent-primary); |
| --waveform-color-inactive: #D0D9E6; |
| --waveform-dashed-line-color: #E0E4E9; |
| --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-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| @keyframes fadeIn { from { opacity: 0; transform: translateY(15px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes modalZoomIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } } |
| @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 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 shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } } |
| .shake-animation { animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; } |
| @keyframes ai-core-pulse { 0% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(74, 108, 250, 0.7); } 70% { transform: scale(1); box-shadow: 0 0 0 20px rgba(74, 108, 250, 0); } 100% { transform: scale(0.95); box-shadow: 0 0 0 0 rgba(74, 108, 250, 0); } } |
| @keyframes ai-orbit { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes ai-text-fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } |
| @keyframes shake-button { 10%, 90% { transform: translate3d(-1px, 0, 0) scale(1.02); } 20%, 80% { transform: translate3d(2px, 0, 0) scale(1.02); } 30%, 50%, 70% { transform: translate3d(-3px, 0, 0) scale(1.02); } 40%, 60% { transform: translate3d(3px, 0, 0) scale(1.02); } } |
| .shake-it { animation: shake-button 0.8s cubic-bezier(.36,.07,.19,.97) both; } |
| .validation-message { text-align: center; color: var(--text-secondary); font-weight: 600; margin-top: 1rem; opacity: 0; transform: translateY(10px); transition: opacity 0.4s ease, transform 0.4s ease; height: 0; overflow: hidden; } |
| .validation-message.visible { opacity: 1; transform: translateY(0); height: auto; } |
| @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } |
| #subscription-status-badge { display: inline-block; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1rem; letter-spacing: 0.5px; text-shadow: 0 1px 2px rgba(0,0,0,0.1); animation: badge-fade-in 0.6s 0.5s ease-out backwards; display: none; } |
| #subscription-status-badge.free-badge { background: linear-gradient(45deg, #6c757d, #495057); color: white; box-shadow: 0 4px 10px rgba(108, 117, 125, 0.3); } |
| #subscription-status-badge.paid-badge { background: linear-gradient(45deg, var(--accent-premium), #ffca2c); color: #333; box-shadow: 0 4px 10px var(--accent-premium-glow); } |
| @keyframes upgrade-btn-pulse { 0% { transform: scale(1); box-shadow: 0 8px 20px -5px var(--accent-premium-glow); } 50% { transform: scale(1.05); box-shadow: 0 12px 25px -5px var(--accent-premium-glow); } 100% { transform: scale(1); box-shadow: 0 8px 20px -5px var(--accent-premium-glow); } } |
| #upgrade-premium-btn { display: none; width: 100%; margin-top: 1.5rem; padding: 1rem; font-family: var(--app-font); font-size: 1.1em; font-weight: 800; color: #212529; background: linear-gradient(95deg, #FFD54F, var(--accent-premium) 100%); border: none; border-radius: var(--radius-btn); cursor: pointer; transition: transform 0.2s ease, box-shadow 0.2s ease; box-shadow: 0 8px 20px -5px var(--accent-premium-glow); animation: upgrade-btn-pulse 2.5s infinite; } |
| #upgrade-premium-btn:hover { transform: translateY(-3px); box-shadow: 0 12px 25px -5px rgba(255, 193, 7, 0.4); animation-play-state: paused; } |
| 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: 2.5rem 0; min-height: 100vh; display: flex; justify-content: center; align-items: flex-start; overflow-x: hidden; background-image: radial-gradient(var(--text-tertiary) 0.5px, transparent 0.5px); background-size: 20px 20px; background-position: -10px -10px; } |
| .app-container { max-width: 820px; width: 92%; margin: 0 auto; } |
| .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: 2.5rem; 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.5rem; } |
| .form-group-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.2rem; } |
| label { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.2em; } |
| #project-speakers-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 1.5rem; text-align: center; } |
| .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; } |
| .speaker-display-card:hover:not(#add-speaker-card) { border-color: var(--accent-primary); transform: translateY(-5px); box-shadow: var(--shadow-md); } |
| |
| .speaker-display-card img, |
| .custom-voice-avatar { |
| width: 80px; |
| height: 80px; |
| border-radius: 50%; |
| object-fit: cover; |
| border: 4px solid var(--panel-bg); |
| box-shadow: var(--shadow-md); |
| margin: 0 auto 1rem auto; |
| display: block; |
| } |
| .custom-voice-avatar { |
| background: linear-gradient(135deg, #667eea, #764ba2); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .speaker-display-card h3 { margin: 0 0 0.25rem 0; font-size: 1.1em; font-weight: 800; } |
| .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; } |
| .remove-speaker-btn:hover { color: #e53e3e; transform: scale(1.1); background: #fff; } |
| #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; } |
| #add-speaker-card:hover { border-color: var(--accent-primary); background: var(--accent-primary-glow); } |
| #add-speaker-card .plus-icon { font-size: 2.5rem; line-height: 1; margin-bottom: 0.5rem; } |
| #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); } |
| .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); } |
| .script-turn:last-child { margin-bottom: 0; } |
| .turn-speaker-selector { flex-shrink: 0; width: 180px; position: relative; } |
| .turn-content { flex-grow: 1; } |
| .turn-content textarea { width: 100%; min-height: 100px; 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; } |
| .turn-content textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow); } |
| .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; } |
| .remove-turn-btn:hover { color: #e53e3e; transform: scale(1.1); } |
| #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); } |
| #add-turn-btn:hover { background: var(--panel-bg); border-color: var(--accent-primary); color: var(--accent-primary); } |
| .slider-container { display: flex; align-items: center; gap: 1.5rem; } |
| input[type="range"] { flex-grow: 1; -webkit-appearance: none; appearance: none; width: 100%; height: 6px; background: var(--input-border); border-radius: 3px; outline: none; cursor: pointer; } |
| input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: #fff; border-radius: 50%; cursor: pointer; border: 4px solid var(--accent-primary); box-shadow: var(--shadow-md); margin-top: -9px; } |
| .temperature-value { font-weight: 700; background-color: var(--input-bg); padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 45px; text-align: center; color: var(--accent-primary); } |
| .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); } |
| .generate-btn:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px var(--accent-secondary-glow); } |
| .generate-btn:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 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; padding: 2rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; } |
| .output-section.has-content { padding: 0; background-color: transparent; border: none; min-height: auto; box-shadow: none; } |
| .status-message { font-weight: 500; color: var(--text-secondary); text-align: center; font-size: 1.1em; } |
| .status-message.error { color: #c53030; font-weight: 600; } |
| .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 { 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 { transform: translateY(2px); border-bottom-width: 0px; margin-top: calc(1.5rem + 4px); } |
| .beautiful-download-btn svg { width: 24px; height: 24px; fill: currentColor; } |
| #clear-history-btn { display: none; align-items: center; justify-content: center; gap: 8px; width: 100%; max-width: 300px; margin: 2rem 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:hover { border-color: #fc8181; color: #e53e3e; background-color: #fff5f5; transform: translateY(-2px); } |
| #clear-history-btn svg { width: 18px; height: 18px; fill: currentColor; } |
| .loading-animation-wrapper { display: none; flex-direction: column; align-items: center; justify-content: center; gap: 1.5rem; 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 0.2s 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; } |
| #generation-progress-container { width: 100%; max-width: 500px; margin-top: 1rem; } |
| .progress-grid { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px; margin-top: 10px; } |
| .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; } |
| .progress-item.pending { background-color: #EDF2F7; animation: pulse-gray 1.5s infinite; } |
| .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); } |
| .progress-item.error { background-color: #FC8181; color: white; border-color: #F56565; } |
| @keyframes pulse-gray { 0% { opacity: 0.6; } 50% { opacity: 1; } 100% { opacity: 0.6; } } |
| .audio-player-content { display: none; width: 100%; padding: 1.5rem; box-sizing: border-box; flex-direction: column; gap: 1.2rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-lg); border: 1px solid var(--panel-border); animation: fadeIn 0.5s ease-out; } |
| .audio-waveform-container { display: flex; align-items: center; gap: 1rem; width: 100%; margin-bottom: 1rem; } |
| .audio-time { font-size: 0.9em; color: var(--text-secondary); min-width: 40px; text-align: center; font-variant-numeric: tabular-nums; user-select: none; } |
| .audio-waveform { flex-grow: 1; height: 60px; position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; margin-bottom: 0.5rem; } |
| .audio-waveform-canvas { display: block; max-width: 100%; height: 100%; } |
| .audio-waveform-dashed-line { position: absolute; top: 50%; left: 0; width: 100%; height: 1px; background-image: linear-gradient(to right, var(--waveform-dashed-line-color) 33%, transparent 0%); background-position: center; background-size: 10px 1px; z-index: 1; } |
| .audio-controls-group { display: flex; justify-content: center; align-items: center; gap: 1.5rem; margin-bottom: 1rem; } |
| .audio-skip-btn, .audio-play-pause-btn-large, .audio-volume-btn { background: none; border: none; cursor: pointer; padding: 8px; transition: transform 0.2s, opacity 0.2s; color: var(--text-secondary); } |
| .audio-skip-btn:hover, .audio-play-pause-btn-large:hover, .audio-volume-btn:hover { color: var(--accent-primary); opacity: 0.9; } |
| .audio-skip-btn:active, .audio-play-pause-btn-large:active, .audio-volume-btn:active { transform: scale(0.9); } |
| .audio-skip-btn svg { width: 28px; height: 28px; fill: currentColor; } |
| .audio-play-pause-btn-large { padding: 0; width: 50px; height: 50px; } |
| .audio-play-pause-btn-large svg { width: 38px; height: 38px; fill: currentColor; } |
| .audio-utility-controls { display: flex; align-items: center; justify-content: space-between; width: 100%; } |
| .audio-volume-btn svg { width: 24px; height: 24px; fill: currentColor; } |
| .audio-speed-btn { font-family: var(--app-font); font-size: 0.9em; font-weight: 600; background-color: var(--panel-bg); border: 1px solid var(--panel-border); border-radius: 8px; min-width: 40px; text-align: center; color: var(--text-primary); box-shadow: var(--shadow-md); padding: 0.4rem 0.6rem; cursor: pointer; } |
| .audio-speed-btn:hover { background-color: var(--input-bg); } |
| .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; } |
| .turn-player-container.visible { display: flex; } |
| .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; } |
| .turn-play-btn:hover { background: var(--accent-primary-hover); transform: scale(1.1); } |
| .turn-play-btn svg { width: 20px; height: 20px; fill: currentColor; } |
| .turn-play-btn .pause-icon { display: none; } |
| .turn-player-container.playing .turn-play-btn .play-icon { display: none; } |
| .turn-player-container.playing .turn-play-btn .pause-icon { display: block; } |
| .loading-state-wrapper { display: flex; align-items: center; gap: 6px; margin-right: auto; } |
| .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; } |
| .loading-state-wrapper .loading-text { font-size: 0.85em; font-weight: 600; color: var(--accent-primary); white-space: nowrap; transition: all 0.3s; } |
| .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; } |
| .turn-retry-btn:hover { border-color: var(--accent-secondary); color: var(--accent-secondary); background: var(--accent-secondary-glow); } |
| .turn-retry-btn svg { width: 16px; height: 16px; fill: currentColor; } |
| .replace-success-message { font-family: var(--app-font); font-size: 0.85em; font-weight: 600; padding: 0.4rem 0.8rem; border-radius: var(--radius-btn); background-color: var(--accent-secondary-glow); color: var(--accent-secondary); white-space: nowrap; display: flex; align-items: center; justify-content: center; margin-right: auto; opacity: 0; transform: translateY(10px); animation: fadeInOut 3s forwards; } |
| .main-update-message { font-family: var(--app-font); font-size: 0.75em; font-weight: 600; padding: 0.3rem 0.8rem; border-radius: var(--radius-btn); background: linear-gradient(90deg, #e6fffa, #b2f5ea); color: #2c7a7b; border: 1px solid #81e6d9; white-space: nowrap; display: flex; align-items: center; justify-content: center; margin-right: auto; box-shadow: 0 2px 5px rgba(0,0,0,0.05); opacity: 0; transform: translateY(10px); } |
| .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: 1000; opacity: 0; transition: opacity 0.3s; } |
| .modal-overlay.visible { display: flex; opacity: 1; } |
| .modal-dialog { background: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); width: 90%; max-width: 750px; max-height: 85vh; box-shadow: var(--shadow-lg); border: 1px solid var(--panel-border); animation: modalZoomIn 0.4s cubic-bezier(0.4, 0, 0.2, 1) forwards; position: relative; display: flex; flex-direction: column; } |
| .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: none; border: none; font-size: 2.5rem; cursor: pointer; color: var(--text-tertiary); transition: var(--transition-smooth); line-height: 1; } |
| .close-modal-btn:hover { color: var(--accent-primary); transform: rotate(90deg); } |
| .modal-body { overflow-y: auto; } |
| #speaker-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 1.5rem; } |
| .speaker-card { cursor: pointer; transition: var(--transition-smooth); text-align: center; padding: 0.5rem; position: relative; } |
| .speaker-card:hover:not(.disabled) { transform: translateY(-8px); } |
| .speaker-card .speaker-visual { border: 3px solid transparent; border-radius: 18px; overflow: hidden; box-shadow: var(--shadow-md); transition: var(--transition-smooth); } |
| .speaker-card:hover:not(.disabled) .speaker-visual { box-shadow: var(--shadow-lg); } |
| .speaker-card img { width: 100%; height: 130px; object-fit: cover; display: block; background-color: #e0e0e0; border-radius: 14px; } |
| .speaker-card .speaker-name { padding: 0.8rem 0.4rem 0.2rem; font-weight: 600; font-size: 0.95em; color: var(--text-secondary); } |
| .speaker-card.disabled { opacity: 0.4; cursor: not-allowed; } |
| .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); } |
| .custom-select-trigger:hover { border-color: var(--accent-primary); } |
| .custom-select-trigger img { width: 40px; height: 40px; border-radius: 10px; object-fit: cover; } |
| .custom-select-trigger span { font-weight: 600; flex-grow: 1; } |
| .custom-select-trigger .arrow { margin-right: auto; width: 20px; height: 20px; color: var(--text-tertiary); transition: transform 0.2s; } |
| .custom-select-container.open .arrow { transform: rotate(180deg); } |
| .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; } |
| .custom-select-container.open .custom-select-options { opacity: 1; transform: translateY(0); pointer-events: auto; } |
| .custom-select-option { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem; cursor: pointer; } |
| .custom-select-option:hover { background-color: var(--input-bg); } |
| .custom-select-option img { width: 40px; height: 40px; border-radius: 10px; object-fit: cover; } |
| .custom-select-option span { font-weight: 500; } |
| .no-speaker-option { padding: 1rem; text-align: center; color: var(--text-secondary); } |
| .ai-script-btn { background: transparent; border: 1px solid var(--accent-primary); color: var(--accent-primary); padding: 0.4rem 0.9rem; font-size: 0.9em; border-radius: var(--radius-btn); font-weight: 700; cursor: pointer; transition: var(--transition-smooth); display: flex; align-items: center; gap: 8px; } |
| .ai-script-btn:hover:not(:disabled) { background: var(--accent-primary-glow); box-shadow: var(--shadow-md); } |
| #ai-modal .modal-dialog { max-width: 480px; padding: 32px 36px; min-height: 480px; display: flex; align-items: center; } |
| #ai-modal .modal-content-wrapper, #ai-modal .modal-loading-wrapper { width: 100%; transition: opacity 0.4s ease-in-out, transform 0.4s ease-in-out; } |
| #ai-modal .modal-content-wrapper:not(.active), #ai-modal .modal-loading-wrapper:not(.active) { opacity: 0; transform: scale(0.95); pointer-events: none; position: absolute; right: 36px; left: 36px; } |
| #ai-modal .modal-loading-wrapper { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 40px; } |
| #ai-modal .loader-animation { width: 120px; height: 120px; position: relative; display: flex; align-items: center; justify-content: center; } |
| #ai-modal .core-pulse { width: 25px; height: 25px; background-color: var(--accent-primary); border-radius: 50%; animation: ai-core-pulse 2s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95); } |
| #ai-modal .ai-orbit { position: absolute; border-radius: 50%; border: 2px dashed var(--panel-border); animation: ai-orbit 10s linear infinite; } |
| #ai-modal .ai-orbit:nth-child(2) { width: 70px; height: 70px; animation-duration: 8s; animation-direction: reverse; } |
| #ai-modal .ai-orbit:nth-child(3) { width: 110px; height: 110px; animation-duration: 12s; } |
| #ai-modal .ai-orbit::before { content: ''; position: absolute; width: 10px; height: 10px; border-radius: 50%; background-color: var(--text-primary); top: 5px; left: 5px; } |
| #ai-modal .ai-orbit:nth-child(2)::before { background-color: var(--accent-secondary); top: auto; bottom: 8px; left: 8px; } |
| #ai-modal .ai-orbit:nth-child(3)::before { background-color: var(--accent-primary); top: 10px; right: 10px; left: auto; } |
| #loading-text-ai { font-size: 18px; font-weight: 600; background-image: linear-gradient(100deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-align: center; min-height: 27px; animation: ai-text-fade-in 0.5s ease; } |
| #ai-modal .modal-header { padding-bottom: 0; border-bottom: none; margin-bottom: 0; } |
| #ai-modal .modal-header h2 { font-size: 28px; font-weight: 800; } |
| #ai-modal .modal-body p { color: var(--text-secondary); line-height: 1.8; margin: 24px 0; font-size: 1rem; } |
| #ai-modal textarea { width: 100%; min-height: 150px; padding: 16px; border: 2px solid var(--input-border); border-radius: var(--radius-input); font-family: var(--app-font); font-size: 1rem; resize: vertical; box-sizing: border-box; background-color: #fff; transition: border-color 0.3s ease, box-shadow 0.3s ease; } |
| #ai-modal textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 4px var(--accent-primary-glow); } |
| #ai-modal .modal-footer { margin-top: 28px; padding-top: 0; border-top: none; } |
| #generate-script-ai-btn { width: 100%; padding: 16px 24px; font-family: var(--app-font); font-size: 18px; font-weight: 700; background-image: linear-gradient(100deg, var(--accent-primary), var(--accent-secondary)); background-size: 200% 100%; box-shadow: 0 8px 20px -5px var(--accent-primary-glow); transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.3s cubic-bezier(0.16, 1, 0.3, 1), background-position 0.4s ease; } |
| #generate-script-ai-btn:hover:not(:disabled) { background-position: right center; } |
| #ai-status-message { margin: 1rem 0 0 0; text-align: center; display: none; } |
| #ai-status-message.error { color: #c53030; font-weight: 700; } |
| .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: flex; align-items: center; justify-content: center; z-index: 1001; opacity: 0; pointer-events: none; transition: opacity 0.3s ease; } |
| .confirm-modal-overlay.visible { opacity: 1; pointer-events: auto; } |
| .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 cubic-bezier(0.4, 0, 0.2, 1); } |
| .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(--input-bg); color: var(--text-primary); border: 1px solid var(--input-border); } |
| .cancel-btn:hover { background-color: var(--panel-border); } |
| #countdown-timer { display: none; margin-top: 20px; padding: 15px; background-color: #fff9e6; border-radius: 8px; border: 1px solid #ffeeba; animation: fadeIn 0.5s ease; } |
| .timer-content { display: flex; align-items: center; justify-content: center; gap: 12px; } |
| .clock-icon { width: 28px; height: 28px; animation: spin 20s linear infinite; flex-shrink: 0; } |
| #time-left { font-size: 1.2rem; font-weight: 700; color: #856404; } |
| .timer-text { font-size: 0.9rem; color: #856404; } |
| @media (max-width: 768px) { .script-turn { flex-direction: column; } .remove-turn-btn { align-self: flex-end; margin-top: -1.5rem; } .turn-speaker-selector { width: 100%; } } |
| |
| |
| .custom-voice-btn-container { |
| grid-column: span 2; |
| display: flex; |
| } |
| |
| .add-custom-voice-btn { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| gap: 15px; |
| width: 100%; |
| padding: 1rem; |
| background: linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%); |
| border: 2px dashed #cbd5e0; |
| border-radius: 20px; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| position: relative; |
| overflow: hidden; |
| height: 100%; |
| box-sizing: border-box; |
| } |
| |
| .add-custom-voice-btn:hover { |
| border-color: var(--accent-primary); |
| background: #fff; |
| box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1); |
| transform: translateY(-3px); |
| } |
| |
| .custom-voice-icon-wrapper { |
| width: 50px; |
| height: 50px; |
| background: var(--accent-primary-glow); |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: var(--accent-primary); |
| font-size: 1.5rem; |
| flex-shrink: 0; |
| } |
| |
| .custom-voice-text { |
| text-align: right; |
| } |
| |
| .custom-voice-text h4 { |
| margin: 0 0 3px 0; |
| font-size: 1.1rem; |
| font-weight: 800; |
| color: var(--text-primary); |
| } |
| |
| .custom-voice-text p { |
| margin: 0; |
| font-size: 0.85rem; |
| color: var(--text-secondary); |
| line-height: 1.4; |
| } |
| |
| |
| .custom-speaker-card .speaker-visual { |
| border: 3px solid transparent; |
| height: 130px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background-color: transparent; |
| } |
| |
| .custom-speaker-card .custom-voice-avatar { |
| margin: 0; |
| } |
| |
| |
| .card-menu-btn { |
| position: absolute; |
| top: 8px; |
| left: 8px; |
| background: rgba(255, 255, 255, 0.8); |
| border: none; |
| border-radius: 50%; |
| width: 28px; |
| height: 28px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| cursor: pointer; |
| z-index: 10; |
| font-size: 18px; |
| line-height: 1; |
| color: var(--text-secondary); |
| transition: all 0.2s; |
| } |
| .card-menu-btn:hover { |
| background: #fff; |
| color: var(--accent-primary); |
| transform: scale(1.1); |
| } |
| .card-menu-dropdown { |
| position: absolute; |
| top: 40px; |
| left: 8px; |
| background: #fff; |
| border-radius: 12px; |
| box-shadow: 0 4px 15px rgba(0,0,0,0.15); |
| padding: 5px; |
| z-index: 20; |
| display: none; |
| min-width: 140px; |
| border: 1px solid var(--panel-border); |
| } |
| .card-menu-dropdown.active { |
| display: block; |
| animation: fadeIn 0.2s ease; |
| } |
| .card-menu-item { |
| padding: 8px 12px; |
| font-size: 0.9em; |
| color: var(--text-primary); |
| cursor: pointer; |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| transition: background 0.2s; |
| } |
| .card-menu-item:hover { |
| background-color: var(--input-bg); |
| color: var(--accent-primary); |
| } |
| |
| .card-menu-item.delete-item { |
| color: #e53e3e; |
| } |
| .card-menu-item.delete-item:hover { |
| background-color: #fff5f5; |
| color: #c53030; |
| } |
| |
| .audio-preview-mini { |
| position: absolute; |
| bottom: 5px; |
| right: 5px; |
| width: 24px; |
| height: 24px; |
| background: white; |
| border-radius: 50%; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: var(--accent-primary); |
| box-shadow: 0 2px 5px rgba(0,0,0,0.2); |
| z-index: 2; |
| } |
| |
| @keyframes pulse-soft { |
| 0% { box-shadow: 0 0 0 0 rgba(74, 108, 250, 0.4); } |
| 70% { box-shadow: 0 0 0 10px rgba(74, 108, 250, 0); } |
| 100% { box-shadow: 0 0 0 0 rgba(74, 108, 250, 0); } |
| } |
| .add-custom-voice-btn:hover .custom-voice-icon-wrapper { |
| animation: pulse-soft 2s infinite; |
| } |
| |
| |
| .glass-vertical-card { |
| display: none; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| text-align: center; |
| max-width: 320px; |
| width: 100%; |
| padding: 24px 20px; |
| background: rgba(255, 255, 255, 0.75); |
| backdrop-filter: blur(12px); |
| -webkit-backdrop-filter: blur(12px); |
| border-radius: 24px; |
| border: 1px solid rgba(255, 255, 255, 0.9); |
| box-shadow: 0 15px 35px -5px rgba(67, 56, 202, 0.15), 0 5px 15px -5px rgba(0, 0, 0, 0.05); |
| position: relative; |
| overflow: hidden; |
| animation: popIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; |
| opacity: 0; |
| transform: scale(0.9); |
| margin-top: 2rem; |
| } |
| |
| @keyframes popIn { |
| to { opacity: 1; transform: scale(1); } |
| } |
| |
| .icon-circle-wrapper { |
| width: 60px; |
| height: 60px; |
| border-radius: 50%; |
| background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| color: white; |
| margin-bottom: 16px; |
| box-shadow: 0 8px 20px rgba(99, 102, 241, 0.35); |
| position: relative; |
| z-index: 2; |
| } |
| |
| .icon-circle-wrapper::after { |
| content: ''; |
| position: absolute; |
| top: 0; left: 0; right: 0; bottom: 0; |
| border-radius: 50%; |
| border: 2px solid #8b5cf6; |
| opacity: 0; |
| animation: ripple 2s infinite cubic-bezier(0, 0.2, 0.8, 1); |
| z-index: -1; |
| } |
| |
| @keyframes ripple { |
| 0% { transform: scale(1); opacity: 0.6; } |
| 100% { transform: scale(1.6); opacity: 0; } |
| } |
| |
| #ai-progress-percent { |
| font-family: var(--app-font); |
| font-weight: 800; |
| font-size: 1.2rem; |
| letter-spacing: 1px; |
| text-shadow: 0 2px 4px rgba(0,0,0,0.2); |
| } |
| |
| .status-title { |
| margin: 0 0 6px 0; |
| font-size: 0.95rem; |
| font-weight: 800; |
| color: #312e81; |
| } |
| |
| .status-text-small { |
| margin: 0; |
| font-size: 0.75rem; |
| line-height: 1.6; |
| color: #64748b; |
| font-weight: 500; |
| padding: 0 5px; |
| } |
| |
| .bottom-loader { |
| position: absolute; |
| bottom: 0; |
| left: 0; |
| width: 100%; |
| height: 3px; |
| background: rgba(99, 102, 241, 0.1); |
| } |
| |
| .bottom-loader::after { |
| content: ''; |
| position: absolute; |
| left: 0; |
| top: 0; |
| height: 100%; |
| width: 40%; |
| background: linear-gradient(90deg, transparent, #6366f1, #a855f7, transparent); |
| animation: load-slide 1.5s infinite ease-in-out; |
| } |
| |
| @keyframes load-slide { |
| 0% { left: -40%; } |
| 100% { left: 100%; } |
| } |
| |
| @media (max-width: 380px) { |
| .custom-voice-btn-container { |
| grid-column: span 1; |
| } |
| .add-custom-voice-btn { |
| flex-direction: column; |
| text-align: center; |
| gap: 10px; |
| } |
| .custom-voice-text { |
| text-align: center; |
| } |
| } |
| </style> |
| </head> |
| <body> |
| <div class="app-container"> |
| <header class="app-header"> |
| <h1>استودیوی ساخت پادکست</h1> |
| <p>گویندگان خود را به پروژه اضافه کنید، سناریو را بنویسید و پادکست خود را تحویل بگیرید.</p> |
| <div id="subscription-status-badge"></div> |
| </header> |
| <main class="main-content"> |
| <form id="podcast-form" onsubmit="return false;"> |
| <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"> |
| <div class="form-group-header"> |
| <label>📜 سناریوی گفتگو</label> |
| <button type="button" id="open-ai-modal-btn" class="ai-script-btn"> ✨ساخت پروژه با هوش مصنوعی </button> |
| </div> |
| <div id="podcast-script-container"></div> |
| <button type="button" id="add-turn-btn">+ افزودن نوبت گفتگو</button> |
| </div> |
| <div class="form-group"> |
| <label for="temperature-slider-podcast">🌡️ خلاقیت صدا</label> |
| <div class="slider-container"> |
| <input type="range" id="temperature-slider-podcast" min="0.1" max="1.5" step="0.05" value="0.9"> |
| <span id="temperature-value-podcast" class="temperature-value">0.9</span> |
| </div> |
| </div> |
| <button type="submit" id="generate-btn-podcast" class="generate-btn"> |
| <span class="btn-text">🎙️ ساخت پادکست</span> |
| <div class="spinner"></div> |
| </button> |
| <p id="form-validation-message" class="validation-message"></p> |
| <div id="countdown-timer"> |
| <div class="timer-content"> |
| <svg class="clock-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#856404"><path d="M12 2C6.486 2 2 6.486 2 12s4.486 10 10 10 10-4.486 10-10S17.514 2 12 2zm0 18c-4.411 0-8-3.589-8-8s3.589-8 8-8 8 3.589 8 8-3.589 8-8 8z"></path><path d="M13 7h-2v6l5.25 3.15.75-1.23-4.5-2.67z"></path></svg> |
| <div> |
| <div id="time-left">...</div> |
| <div class="timer-text">زمان باقیمانده تا دریافت اعتبار رایگان</div> |
| </div> |
| </div> |
| </div> |
| <button type="button" id="upgrade-premium-btn"> ⭐️ ارتقا به نسخه کامل و نامحدود </button> |
| </form> |
| <div id="output-section-podcast" class="output-section"> |
| <div id="status-message-podcast" class="status-message">پادکست نهایی در اینجا ظاهر خواهد شد.</div> |
| <div id="loading-animation-wrapper-podcast" 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" id="loading-text-podcast">در حال پردازش همزمان...</p> |
| <div id="generation-progress-container"> |
| <div class="progress-grid" id="progress-grid"></div> |
| </div> |
|
|
| |
| <div id="custom-voice-warning-container" class="glass-vertical-card"> |
| <div class="icon-circle-wrapper"> |
| <span id="ai-progress-percent">0%</span> |
| </div> |
| <h5 class="status-title">مدل اختصاصی هوشمند</h5> |
| <p class="status-text-small"> |
| شما در حال ساخت پادکست با مدل اختصاصی هستید. فرایند مدل اختصاصی چند مرحلهای بوده و برای ساخت زمان بیشتری نیاز است. لطفاً صبور باشید، ممکن است ۲ تا ۳ دقیقه زمان بیشتری نیاز باشد. |
| </p> |
| <div class="bottom-loader"></div> |
| </div> |
|
|
| </div> |
| <div id="audio-player-content-podcast" class="audio-player-content"> |
| <div class="audio-waveform-container"> |
| <span class="audio-time audio-current-time">0:00</span> |
| <div class="audio-waveform"> |
| <canvas class="audio-waveform-canvas"></canvas> |
| <div class="audio-waveform-dashed-line"></div> |
| </div> |
| <span class="audio-time audio-total-time">0:00</span> |
| </div> |
| <div class="audio-controls-group"> |
| <button type="button" class="audio-skip-btn backward" title="پرش به عقب"> |
| <svg viewBox="0 0 24 24"><path d="M11 16V8l-4 4 4 4zm4-12v16l7-8-7-8z"></path></svg> |
| </button> |
| <button type="button" class="audio-play-pause-btn-large"> |
| <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> |
| <button type="button" class="audio-skip-btn forward" title="پرش به جلو"> |
| <svg viewBox="0 0 24 24"><path d="M13 16V8l4 4-4 4zM9 4v16L2 12l7-8z"></path></svg> |
| </button> |
| </div> |
| <div class="audio-utility-controls"> |
| <button type="button" class="audio-volume-btn" title="قطع/وصل صدا"> |
| <svg viewBox="0 0 24 24" class="volume-high-icon"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"></path></svg> |
| <svg viewBox="0 0 24 24" class="volume-mute-icon" style="display:none;"><path d="M7 9v6h4l5 5V4L11 9H7zM16.5 12c0-1.77-1.02-3.29-2.5-4.03v2.21l2.45 2.45c.03-.2.05-.41.05-.63zM19 12c0 .94-.23 1.82-.68 2.6L19 14.88c.45-.88.7-1.88.7-2.88 0-4.01-2.99-7.14-7-8.05v2.06c2.89.86 5 3.54 5 6.71zM4.55 4L2 6.55 9.45 14H7v6h4l5 5V14.55l4.05 4.05L22 18 12 8 4.55 4z"></path></svg> |
| </button> |
| <button type="button" class="audio-speed-btn" title="سرعت پخش">1x</button> |
| </div> |
| </div> |
| <button type="button" id="internal-download-btn" class="beautiful-download-btn" style="display:none;"> |
| <svg viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z"></path></svg> دانلود با کیفیت اصلی |
| </button> |
| <button type="button" id="clear-history-btn"> |
| <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> |
| </div> |
| </main> |
| <audio id="hidden-audio-player" style="display: none;"></audio> |
| |
| <input type="file" id="custom-voice-input" accept="audio/*" style="display: none;"> |
| |
| <input type="file" id="replace-voice-input" accept="audio/*" style="display: none;"> |
|
|
| <div id="speaker-modal" class="modal-overlay"> |
| <div class="modal-dialog"> |
| <div class="modal-header"> |
| <h2 id="modal-title">انتخاب گوینده برای افزودن به پروژه</h2> |
| <button type="button" class="close-modal-btn">×</button> |
| </div> |
| <div class="modal-body"><div id="speaker-grid"></div></div> |
| </div> |
| </div> |
| |
| |
| <div id="custom-voice-info-modal" class="modal-overlay"> |
| <div class="modal-dialog" style="max-width: 500px; text-align: center;"> |
| <div class="modal-header" style="justify-content: center; border: none; padding-bottom: 0;"> |
| <div style="width: 70px; height: 70px; background: var(--accent-primary-glow); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--accent-primary); font-size: 2rem; margin-bottom: 1rem;"> |
| <svg width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg> |
| </div> |
| </div> |
| <h3 style="margin-top:0; font-size: 1.5rem; font-weight: 800; color: var(--text-primary);">ساخت صدای اختصاصی</h3> |
| <div class="modal-body" style="padding-top: 1rem;"> |
| <p style="color: var(--text-secondary); line-height: 1.8; font-size: 0.95rem; text-align: justify;"> |
| با اضافه کردن یک نمونه صدا هوش مصنوعی آلفا پادکست رو بر اساس صدای همون شخصی تولید میکند. یک فایل صوتی با کیفیت بالا بدون نویز در حد صدای استدیو اضافه کنید، بهترین حالت این است که صدای مدل که اضافه میکنید بین ۳ تا ۳۰ ثانیه باشه، هوش مصنوعی آلفا تن صدای شما رو یاد میگیره و پادکست با صدای که اضافه کردید خوانده خواهد شد. این صدا به عنوان مدل در گالری گویندگان اضافه میشود. |
| </p> |
| <div style="background: #fff9db; color: #856404; padding: 12px; border-radius: 12px; font-size: 0.9rem; margin-top: 20px; border: 1px solid #ffeeba; display:flex; align-items:center; gap:10px; text-align: right;"> |
| <span style="font-size:1.2rem">⏳</span> |
| <span>دقت کنید ساخت پادکست با صدای اختصاصی زمان بیشتری نیاز دارد. همچنین کیفیت صدای خروجی مدل شما به کیفیت صدای ورودی شما مرتبط است.</span> |
| </div> |
| </div> |
| <div class="modal-footer" style="border: none; justify-content: center; padding-top: 1.5rem;"> |
| <button id="confirm-custom-voice-btn" class="generate-btn" style="width: 100%; padding: 1rem; font-size: 1.1rem;"> |
| اوکی متوجه شدم اضافه کردن صدا |
| </button> |
| </div> |
| </div> |
| </div> |
|
|
| <div id="ai-modal" class="modal-overlay"> |
| <div class="modal-dialog"> |
| <div id="ai-content-wrapper" class="modal-content-wrapper active"> |
| <div class="modal-header"> |
| <h2>ساخت پروژه با هوش مصنوعی</h2> |
| <button type="button" class="close-modal-btn">×</button> |
| </div> |
| <div class="modal-body"> |
| <p>یک موضوع (مثلاً: "تاریخچه پیدایش قهوه رو با سه گوینده بساز") یا یک مقاله کامل را وارد کنید. هوش مصنوعی بهترین تیم گویندگان را انتخاب کرده و سناریوی گفتگو را تولید خواهد کرد.</p> |
| <textarea id="ai-prompt-textarea" placeholder="موضوع یا متن مقاله خود را اینجا وارد کنید..."></textarea> |
| <p id="ai-status-message"></p> |
| </div> |
| <div class="modal-footer"> |
| <button type="button" id="generate-script-ai-btn" class="generate-btn"> 🚀 پروژه را بساز </button> |
| </div> |
| </div> |
| <div id="ai-loading-wrapper" class="modal-loading-wrapper"> |
| <div class="loader-animation"> |
| <div class="core-pulse"></div> |
| <div class="ai-orbit"></div> |
| <div class="ai-orbit"></div> |
| </div> |
| <p id="loading-text-ai">در حال آمادهسازی...</p> |
| </div> |
| </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> |
| |
| <div id="confirm-delete-speaker-modal" class="confirm-modal-overlay"> |
| <div class="confirm-modal-dialog"> |
| <div style="margin-bottom: 15px; width: 60px; height: 60px; background: #fed7d7; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 15px auto; color: #c53030;"> |
| <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg> |
| </div> |
| <h3>حذف صدای اختصاصی</h3> |
| <p>آیا از حذف این مدل صدای اختصاصی مطمئن هستید؟ این عملیات غیرقابل بازگشت است.</p> |
| <div class="confirm-modal-actions"> |
| <button id="cancel-delete-speaker-btn" class="cancel-btn">انصراف</button> |
| <button id="confirm-delete-speaker-btn" class="confirm-btn">بله، حذف کن</button> |
| </div> |
| </div> |
| </div> |
| </div> |
| <script> |
| document.addEventListener('DOMContentLoaded', () => { |
| const TTS_API_ENDPOINT = '/api/generate'; |
| const AI_CREATE_API_ENDPOINT = '/api/create-full-podcast'; |
| const AI_STATUS_API_ENDPOINT = '/api/podcast-status/'; |
| const PREMIUM_PAGE_ID = '1149636'; |
| const PREMIUM_URL = '#/nav/online/news/getSingle/1149636/eyJpdiI6ImNtTno4RDdUcWIrVmg5azNpUEM4b1E9PSIsInZhbHVlIjoiTGFhUHQ3Q0N6NnRvZWpoYXdMY2lQdGhzZkc0WFhaLzBhdVBpeE4zM2NGR25nRVN5VnFKT0dMZ0x1TDlPbUx4MyIsIm1hYyI6Ijg1MDExMWY2ODQ1YTNlYWEyNWM3NGFlMTcyYzBkNzExMTY4NzM2OTVmM2U5YjVmM2E2NTNhMmFmOThmNGE2ZDYiLCJ0YWciOiIifQ==/20934991'; |
| let userSubscriptionStatus = 'free'; |
| let userFingerprint = null; |
| let countdownInterval = null; |
| let lastAddedSpeakerIndex = -1; |
| let speakerIdToEdit = null; |
| let speakerIdToDelete = null; |
| |
| |
| let customVoiceTimer = null; |
| |
| function startCustomVoiceProgress() { |
| const container = document.getElementById('custom-voice-warning-container'); |
| const percentElement = document.getElementById('ai-progress-percent'); |
| if (!container || !percentElement) return; |
| |
| container.style.display = 'flex'; |
| let percentage = 0; |
| percentElement.textContent = '0%'; |
| |
| if (customVoiceTimer) clearInterval(customVoiceTimer); |
| customVoiceTimer = setInterval(() => { |
| if (percentage < 99) { |
| percentage++; |
| percentElement.textContent = percentage + '%'; |
| } else { |
| clearInterval(customVoiceTimer); |
| } |
| }, 3000); |
| } |
| |
| function stopCustomVoiceProgress() { |
| const container = document.getElementById('custom-voice-warning-container'); |
| if (container) container.style.display = 'none'; |
| if (customVoiceTimer) clearInterval(customVoiceTimer); |
| } |
| |
| |
| const defaultSpeakers = [ |
| { id: "Charon", name: "شهاب (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/a18705_25IMG-۲۰۲۵۰۷۰۵-۱۱۰۵۴۹.jpg" }, |
| { id: "Zephyr", name: "آوا (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/029605_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۲۵۲.jpg" }, |
| { id: "Achird", name: "نوید (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/697e05_25IMG-۲۰۲۵۰۶۰۹-۰۶۴۶۳۷.jpg" }, |
| { id: "Zubenelgenubi", name: "آرمان (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/a8a705_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۶۲۹.jpg" }, |
| { id: "Vindemiatrix", name: "مهسا (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/d74d05_25IMG-۲۰۲۵۰۷۰۵-۱۱۱۸۳۸.jpg" }, |
| { id: "Rasalgethi", name: "دانا (مرد)", gender: "male", desc: "خبری و آموزنده", imgUrl: "https://uploadkon.ir/uploads/57e425_25IMG-20250925-112825-749.jpg" }, |
| { id: "Sadachbia", name: "سامان (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/580205_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۳۳۰.jpg" }, |
| { id: "Sadaltager", name: "آرش (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/c4db05_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۵۰۰.jpg" }, |
| { id: "Sulafat", name: "شبنم (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/995005_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۶۱۱.jpg" }, |
| { id: "Laomedeia", name: "سحر (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/660705_25IMG-۲۰۲۵۰۷۰۵-۱۱۳۷۵۴.jpg" }, |
| { id: "Achernar", name: "مریم (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/4c2905_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۰۳۶.jpg" }, |
| { id: "Alnilam", name: "بهرام (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/f0c205_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۲۲۰.jpg" }, |
| { id: "Schedar", name: "نیکان (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/d37a05_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۳۲۵.jpg" }, |
| { id: "Gacrux", name: "فرناز (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/495b09_25IMG-20251109-104135-304.jpg" }, |
| { id: "Pulcherrima", name: "سارا (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/acb105_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۷۴۳.jpg" }, |
| { id: "Umbriel", name: "مانی (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/68b505_25IMG-۲۰۲۵۰۷۰۵-۱۱۴۹۱۴.jpg" }, |
| { id: "Algieba", name: "آرتین (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/571005_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۰۳۹.jpg" }, |
| { id: "Despina", name: "دلنواز (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/5d7805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۲۲۲.jpg" }, |
| { id: "Erinome", name: "روژان (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/aa8805_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۳۴۹.jpg" }, |
| { id: "Algenib", name: "امید (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/a63c05_25IMG-۲۰۲۵۰۷۰۵-۱۱۵۹۲۱.jpg" }, |
| { id: "Orus", name: "بردیا (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/8bc405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۴۳۳.jpg" }, |
| { id: "Aoede", name: "ترانه (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/9cb405_25IMG-۲۰۲۵۰۷۰۵-۱۲۱۸۵۰.jpg" }, |
| { id: "Callirrhoe", name: "نیکو (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/ee5f05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۰۴۷.jpg" }, |
| { id: "Autonoe", name: "هستی (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/9b0505_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۲۲۲.jpg" }, |
| { id: "Enceladus", name: "کامیار (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/127805_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۴۱۴.jpg" }, |
| { id: "Iapetus", name: "کیانوش (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/c98b05_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۶۰۵.jpg" }, |
| { id: "Puck", name: "پویا (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/ca3605_25IMG-۲۰۲۵۰۷۰۵-۱۲۲۸۳۹.jpg" }, |
| { id: "Kore", name: "مهتاب (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/b66605_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۰۳۵.jpg" }, |
| { id: "Fenrir", name: "سام (مرد)", gender: "male", imgUrl: "https://uploadkon.ir/uploads/03c005_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۴۱۳.jpg" }, |
| { id: "Leda", name: "لیدا (زن)", gender: "female", imgUrl: "https://uploadkon.ir/uploads/710305_25IMG-۲۰۲۵۰۷۰۵-۱۲۳۷۳۱.jpg" } |
| ]; |
| |
| let speakers = [...defaultSpeakers]; |
| let activeSpeakers = [], masterAudioBlobs = [], currentlyPlayingTurnPlayer = null; |
| let audioPeaks = [], currentPlaybackSpeedIndex = 0; |
| const playbackSpeeds = [1.0, 1.25, 1.5, 0.75]; |
| const mainAudioPlayer = document.getElementById('hidden-audio-player'); |
| const form = document.getElementById('podcast-form'); |
| const scriptContainer = document.getElementById('podcast-script-container'); |
| const addTurnBtn = document.getElementById('add-turn-btn'); |
| const tempSlider = document.getElementById('temperature-slider-podcast'); |
| const tempValueSpan = document.getElementById('temperature-value-podcast'); |
| const generateBtn = document.getElementById('generate-btn-podcast'); |
| const validationMessage = document.getElementById('form-validation-message'); |
| const outputSection = document.getElementById('output-section-podcast'); |
| const statusMessage = document.getElementById('status-message-podcast'); |
| const clearHistoryBtn = document.getElementById('clear-history-btn'); |
| const loadingAnimationWrapper = document.getElementById('loading-animation-wrapper-podcast'); |
| const loadingText = document.getElementById('loading-text-podcast'); |
| const progressGrid = document.getElementById('progress-grid'); |
| const playerContent = document.getElementById('audio-player-content-podcast'); |
| const speakerModal = document.getElementById('speaker-modal'); |
| const modalTitleElement = document.getElementById('modal-title'); |
| const addSpeakerCard = document.getElementById('add-speaker-card'); |
| const projectSpeakersContainer = document.getElementById('project-speakers-container'); |
| const audioContext = new (window.AudioContext || window.webkitAudioContext)(); |
| const aiModal = document.getElementById('ai-modal'); |
| const openAiModalBtn = document.getElementById('open-ai-modal-btn'); |
| const generateAiBtn = document.getElementById('generate-script-ai-btn'); |
| const aiPromptTextarea = document.getElementById('ai-prompt-textarea'); |
| const aiStatusMessage = document.getElementById('ai-status-message'); |
| const aiContentWrapper = document.getElementById('ai-content-wrapper'); |
| const aiLoadingWrapper = document.getElementById('ai-loading-wrapper'); |
| const aiLoadingText = document.getElementById('loading-text-ai'); |
| const aiStatusMessages = ["در حال تحلیل موضوع...", "انتخاب هوشمند گویندگان...", "نوشتن پیشنویس سناریو...", "بازبینی و نهاییسازی متن...", "آمادهسازی پروژه لطفاً صبور باشید..."]; |
| const internalDownloadBtn = document.getElementById('internal-download-btn'); |
| let pollingInterval = null, statusInterval = null; |
| const confirmDeleteModal = document.getElementById('confirm-delete-modal'); |
| const confirmDeleteBtn = document.getElementById('confirm-delete-action-btn'); |
| const cancelDeleteBtn = document.getElementById('cancel-delete-btn'); |
| const subscriptionBadge = document.getElementById('subscription-status-badge'); |
| const upgradeBtn = document.getElementById('upgrade-premium-btn'); |
| const countdownContainer = document.getElementById('countdown-timer'); |
| const timeLeftDisplay = document.getElementById('time-left'); |
| const confirmDeleteSpeakerModal = document.getElementById('confirm-delete-speaker-modal'); |
| |
| const dbName = "PodcastStudioAudioDB_V2"; |
| const audioStoreName = "audioChunks"; |
| const refStoreName = "customVoiceRefs"; |
| let dbInstance; |
| |
| function initDB() { |
| return new Promise((resolve, reject) => { |
| const request = indexedDB.open(dbName, 2); |
| request.onerror = (event) => reject(event); |
| request.onsuccess = (event) => { |
| dbInstance = event.target.result; |
| resolve(dbInstance); |
| }; |
| request.onupgradeneeded = (event) => { |
| const db = event.target.result; |
| if (!db.objectStoreNames.contains(audioStoreName)) { |
| db.createObjectStore(audioStoreName, { keyPath: "index" }); |
| } |
| if (!db.objectStoreNames.contains(refStoreName)) { |
| db.createObjectStore(refStoreName, { keyPath: "id" }); |
| } |
| }; |
| }); |
| } |
| |
| async function saveAudioToDB(index, blob) { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([audioStoreName], "readwrite"); |
| const store = transaction.objectStore(audioStoreName); |
| const request = store.put({ index: index, blob: blob }); |
| request.onsuccess = () => resolve(); |
| request.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function getAudioFromDB(index) { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([audioStoreName], "readonly"); |
| const store = transaction.objectStore(audioStoreName); |
| const request = store.get(index); |
| request.onsuccess = (event) => resolve(event.target.result ? event.target.result.blob : null); |
| request.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function getAllAudioFromDB() { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([audioStoreName], "readonly"); |
| const store = transaction.objectStore(audioStoreName); |
| const request = store.getAll(); |
| request.onsuccess = (event) => { |
| const items = event.target.result; |
| items.sort((a, b) => a.index - b.index); |
| resolve(items); |
| }; |
| request.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function clearAudioDB() { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([audioStoreName], "readwrite"); |
| const store = transaction.objectStore(audioStoreName); |
| const request = store.clear(); |
| request.onsuccess = () => resolve(); |
| request.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function saveRefAudioToDB(id, blob, name) { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([refStoreName], "readwrite"); |
| const store = transaction.objectStore(refStoreName); |
| store.put({ id: id, blob: blob, name: name }); |
| transaction.oncomplete = () => resolve(); |
| transaction.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function getRefAudioFromDB(id) { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([refStoreName], "readonly"); |
| const store = transaction.objectStore(refStoreName); |
| const req = store.get(id); |
| req.onsuccess = (e) => resolve(e.target.result); |
| req.onerror = (e) => reject(e); |
| }); |
| } |
| |
| async function deleteRefAudioFromDB(id) { |
| if (!dbInstance) await initDB(); |
| return new Promise((resolve, reject) => { |
| const transaction = dbInstance.transaction([refStoreName], "readwrite"); |
| const store = transaction.objectStore(refStoreName); |
| const req = store.delete(id); |
| req.onsuccess = () => resolve(); |
| req.onerror = (e) => reject(e); |
| }); |
| } |
| |
| const customVoiceInput = document.getElementById('custom-voice-input'); |
| const replaceVoiceInput = document.getElementById('replace-voice-input'); |
| |
| const blobToBase64 = (blob) => { |
| return new Promise((resolve, reject) => { |
| const reader = new FileReader(); |
| reader.onloadend = () => resolve(reader.result); |
| reader.onerror = reject; |
| reader.readAsDataURL(blob); |
| }); |
| }; |
| |
| customVoiceInput.addEventListener('change', async (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| if (file.size > 5 * 1024 * 1024) { alert("حجم فایل صدا باید کمتر از 5 مگابایت باشد."); return; } |
| |
| const customId = `custom_${Date.now()}`; |
| const customName = `صدای اختصاصی ${speakers.filter(s => s.id.startsWith('custom')).length + 1}`; |
| await saveRefAudioToDB(customId, file, customName); |
| const newSpeaker = { id: customId, name: customName, gender: "custom", isCustom: true, imgUrl: null }; |
| speakers.push(newSpeaker); |
| saveCustomSpeakersMetadata(); |
| addSpeakerToProject(customId); |
| document.getElementById('speaker-modal').classList.remove('visible'); |
| alert("صدای اختصاصی با موفقیت اضافه شد!"); |
| }); |
| |
| replaceVoiceInput.addEventListener('change', async (e) => { |
| const file = e.target.files[0]; |
| if (!file) return; |
| if (file.size > 5 * 1024 * 1024) { alert("حجم فایل صدا باید کمتر از 5 مگابایت باشد."); return; } |
| if (!speakerIdToEdit) return; |
| |
| const speaker = speakers.find(s => s.id === speakerIdToEdit); |
| if (speaker) { |
| await saveRefAudioToDB(speakerIdToEdit, file, speaker.name); |
| alert("فایل صوتی با موفقیت جایگزین شد."); |
| speakerIdToEdit = null; |
| } |
| }); |
| |
| function saveCustomSpeakersMetadata() { |
| const customOnly = speakers.filter(s => s.isCustom); |
| localStorage.setItem('customSpeakersMeta', JSON.stringify(customOnly)); |
| } |
| |
| async function loadCustomSpeakers() { |
| const meta = localStorage.getItem('customSpeakersMeta'); |
| if (meta) { |
| const customList = JSON.parse(meta); |
| for (const s of customList) { |
| const data = await getRefAudioFromDB(s.id); |
| if (data) { |
| speakers.push(s); |
| } |
| } |
| } |
| } |
| |
| window.playRefPreview = async (id) => { |
| const data = await getRefAudioFromDB(id); |
| if (data && data.blob) { |
| const url = URL.createObjectURL(data.blob); |
| const audio = new Audio(url); |
| audio.play(); |
| } |
| }; |
| |
| window.renameCustomSpeaker = (id) => { |
| const speaker = speakers.find(s => s.id === id); |
| if (!speaker) return; |
| const newName = prompt("نام جدید را وارد کنید:", speaker.name); |
| if (newName && newName.trim()) { |
| speaker.name = newName.trim(); |
| saveCustomSpeakersMetadata(); |
| getRefAudioFromDB(id).then(data => { if(data) saveRefAudioToDB(id, data.blob, newName.trim()); }); |
| openSpeakerModal(); renderActiveSpeakers(); |
| } |
| }; |
| |
| window.triggerReplaceAudio = (id) => { speakerIdToEdit = id; replaceVoiceInput.click(); }; |
| |
| window.triggerDeleteCustomSpeaker = (id) => { |
| speakerIdToDelete = id; |
| confirmDeleteSpeakerModal.classList.add('visible'); |
| }; |
| |
| document.getElementById('confirm-custom-voice-btn').onclick = () => { document.getElementById('custom-voice-info-modal').classList.remove('visible'); document.getElementById('custom-voice-input').click(); }; |
| document.getElementById('custom-voice-info-modal').addEventListener('click', (e) => { if (e.target.id === 'custom-voice-info-modal') e.target.classList.remove('visible'); }); |
| |
| |
| async function getBrowserFingerprint() { |
| const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset()]; |
| try { |
| const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); ctx.textBaseline = "top"; ctx.font = "14px 'Arial'"; ctx.textBaseline = "alphabetic"; ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20); ctx.fillStyle = "#069"; ctx.fillText("a1b2c3d4e5f6g7h8i9j0_!@#$%^&*()", 2, 15); components.push(canvas.toDataURL()); |
| } catch (e) { components.push("canvas-error"); } |
| const fingerprintString = components.join('~~~'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { const char = fingerprintString.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); |
| } |
| function startCountdown(resetTimestamp) { |
| if (countdownInterval) clearInterval(countdownInterval); |
| countdownContainer.style.display = 'block'; |
| const updateTimer = () => { |
| const now = Date.now() / 1000; const timeLeft = Math.max(0, resetTimestamp - now); |
| if (timeLeft === 0) { clearInterval(countdownInterval); countdownContainer.style.display = 'none'; updateUIWithServerStatus(); return; } |
| const d = Math.floor(timeLeft / 86400), h = Math.floor((timeLeft % 86400) / 3600), m = Math.floor((timeLeft % 3600) / 60), s = Math.floor(timeLeft % 60); |
| let parts = []; if (d > 0) parts.push(`${d} روز`); if (h > 0) parts.push(`${h} ساعت`); if (m > 0) parts.push(`${m} دقیقه`); if (s > 0) parts.push(`${s} ثانیه`); timeLeftDisplay.textContent = parts.slice(0, 3).join(' و '); |
| }; updateTimer(); countdownInterval = setInterval(updateTimer, 1000); |
| } |
| async function updateUIWithServerStatus() { |
| if (!userFingerprint) return; |
| if (userSubscriptionStatus === 'paid') { generateBtn.disabled = false; generateAiBtn.disabled = false; upgradeBtn.style.display = 'none'; validationMessage.classList.remove('visible'); countdownContainer.style.display = 'none'; if (countdownInterval) clearInterval(countdownInterval); return; } |
| validationMessage.textContent = 'در حال بررسی اعتبار...'; validationMessage.classList.add('visible'); |
| try { |
| const response = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); |
| if (!response.ok) throw new Error('Server response was not ok.'); |
| const result = await response.json(); generateAiBtn.disabled = false; |
| if (result.limit_reached) { generateBtn.disabled = true; upgradeBtn.style.display = 'block'; validationMessage.textContent = 'هر هفته در نسخه رایگان امکان ساخت پنج پادکست وجود داره اعتبار ساخت پادکست شما در این هفته تمام شده است.'; startCountdown(result.reset_timestamp); } |
| else { generateBtn.disabled = false; upgradeBtn.style.display = 'none'; validationMessage.textContent = `شما ${result.credits_remaining} اعتبار ساخت پادکست در این هفته دارید.`; countdownContainer.style.display = 'none'; if (countdownInterval) clearInterval(countdownInterval); } |
| } catch (error) { console.error("Credit check failed:", error); validationMessage.textContent = "خطا در بررسی اعتبار. لطفاً صفحه را رفرش کنید."; } |
| } |
| function isUserPaid(userObject) { return userObject && userObject.isLogin && userObject.accessible_pages && (userObject.accessible_pages.includes(PREMIUM_PAGE_ID) || userObject.accessible_pages.includes(parseInt(PREMIUM_PAGE_ID))); } |
| function updateUIForSubscriptionStatus(status) { userSubscriptionStatus = status; if (status === 'paid') { subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; } else { subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; } subscriptionBadge.style.display = 'inline-block'; updateUIWithServerStatus(); } |
| window.addEventListener('message', (event) => { if (event.data && event.data.type === 'USER_STATUS_RESPONSE') { try { const userObject = JSON.parse(event.data.payload); const status = isUserPaid(userObject) ? 'paid' : 'free'; updateUIForSubscriptionStatus(status); } catch (e) { updateUIForSubscriptionStatus('free'); } } }); |
| upgradeBtn.addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM', payload: { url: PREMIUM_URL } }, '*'); }); |
| |
| const showAiModal = () => aiModal.classList.add('visible'); |
| const hideAiModal = () => { aiModal.classList.remove('visible'); resetAiModalView(); }; |
| openAiModalBtn.addEventListener('click', showAiModal); |
| aiModal.querySelector('.close-modal-btn').addEventListener('click', hideAiModal); |
| aiModal.addEventListener('click', e => { if (e.target === aiModal) hideAiModal(); }); |
| |
| async function mergeAudioBlobs(blobs) { |
| const decodedBuffers=await Promise.all(blobs.filter(b=>b&&b.size>100).map(async blob=>{try{const arrayBuffer=await blob.arrayBuffer();if(audioContext.state==='suspended'){await audioContext.resume()}return await audioContext.decodeAudioData(arrayBuffer)}catch(e){console.error("Error decoding audio chunk:",e);return null}}));const validBuffers=decodedBuffers.filter(buffer=>buffer);if(validBuffers.length===0){return null}const totalLength=validBuffers.reduce((total,buffer)=>total+buffer.length,0);const outputBuffer=audioContext.createBuffer(validBuffers[0].numberOfChannels,totalLength,validBuffers[0].sampleRate);let offset=0;for(const buffer of validBuffers){for(let channel=0;channel<buffer.numberOfChannels;channel++){outputBuffer.getChannelData(channel).set(buffer.getChannelData(channel),offset)}offset+=buffer.length}return outputBuffer |
| } |
| function bufferToWav(buffer) { const numOfChan=buffer.numberOfChannels,length=buffer.length*numOfChan*2+44,bufferArr=new ArrayBuffer(length),view=new DataView(bufferArr),channels=[],sampleRate=buffer.sampleRate;let offset=0,pos=0;const writeString=(s)=>{for(let i=0;i<s.length;i++)view.setUint8(pos++,s.charCodeAt(i))};writeString('RIFF');view.setUint32(pos,36+length,true);pos+=4;writeString('WAVE');writeString('fmt ');view.setUint32(pos,16,true);pos+=4;view.setUint16(pos,1,true);pos+=2;view.setUint16(pos,numOfChan,true);pos+=2;view.setUint32(pos,sampleRate,true);pos+=4;view.setUint32(pos,sampleRate*2*numOfChan,true);pos+=4;view.setUint16(pos,numOfChan*2,true);pos+=2;view.setUint16(pos,16,true);pos+=2;writeString('data');view.setUint32(pos,length-pos-4,true);pos+=4;for(let i=0;i<buffer.numberOfChannels;i++)channels.push(buffer.getChannelData(i));while(pos<length){for(let i=0;i<numOfChan;i++){let s=Math.max(-1,Math.min(1,channels[i][offset]));s=s<0?s*0x8000:s*0x7FFF;view.setInt16(pos,s,true);pos+=2}offset++}return new Blob([view],{type:'audio/wav'})} |
| function resetAiModalView() { if(statusInterval) clearInterval(statusInterval); if(pollingInterval) clearInterval(pollingInterval); pollingInterval = null; statusInterval = null; aiContentWrapper.classList.add('active'); aiLoadingWrapper.classList.remove('active'); updateUIWithServerStatus();} |
| |
| async function attemptAiCreation() { |
| try { |
| const prompt = aiPromptTextarea.value.trim(); |
| const allSpeakerData = speakers.map(s => ({ id: s.id, name: s.name, gender: s.gender })); |
| const response = await fetch(AI_CREATE_API_ENDPOINT, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt, available_speakers: allSpeakerData }) }); |
| if (response.status !== 202) { const errorText = await response.text(); throw new Error(`Server status ${response.status}: ${errorText}`); } |
| const responseData = await response.json(); |
| pollForResult(responseData.task_id); |
| } catch (error) { setTimeout(attemptAiCreation, 3000); } |
| } |
| function pollForResult(taskId) { |
| pollingInterval = setInterval(async () => { |
| try { |
| const response = await fetch(`${AI_STATUS_API_ENDPOINT}${taskId}`); |
| if (!response.ok) throw new Error("Polling error"); |
| const result = await response.json(); |
| if (result.status === 'completed') { clearInterval(pollingInterval); clearInterval(statusInterval); populateProjectWithAiData(result.data); } else if (result.status === 'failed') { clearInterval(pollingInterval); setTimeout(attemptAiCreation, 3000); } |
| } catch (error) { console.error('Polling error', error); } |
| }, 3000); |
| } |
| function populateProjectWithAiData(data) { |
| const selectedSpeakerIds = data.selected_speakers; |
| const scriptTurns = data.script; |
| activeSpeakers = []; |
| selectedSpeakerIds.forEach(id => { const speaker = speakers.find(s => s.id === id); if (speaker && !activeSpeakers.some(s => s.id === id)) activeSpeakers.push(speaker); }); |
| renderActiveSpeakers(); |
| scriptContainer.innerHTML = ''; |
| masterAudioBlobs = []; |
| scriptTurns.forEach(turn => addScriptTurn(turn.speaker_id, turn.dialogue)); |
| hideAiModal(); saveState(); |
| } |
| generateAiBtn.addEventListener('click', async () => { |
| if (generateAiBtn.disabled) return; |
| const prompt = aiPromptTextarea.value.trim(); |
| if (!prompt) { aiStatusMessage.style.display = 'block'; aiStatusMessage.textContent='لطفا موضوع را وارد کنید'; setTimeout(()=>aiStatusMessage.style.display='none', 2000); return; } |
| generateAiBtn.disabled = true; aiContentWrapper.classList.remove('active'); aiLoadingWrapper.classList.add('active'); |
| let messageIndex = 0; aiLoadingText.textContent = aiStatusMessages[messageIndex]; |
| statusInterval = setInterval(() => { messageIndex = (messageIndex + 1) % aiStatusMessages.length; aiLoadingText.textContent = aiStatusMessages[messageIndex]; }, 2000); |
| attemptAiCreation(); |
| }); |
| |
| 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')}`; }; |
| const processAudioForWaveform = (audioBuffer) => { |
| if (!audioBuffer) { audioPeaks = []; return; } |
| const data = audioBuffer.getChannelData(0); const samples = Math.floor(audioBuffer.duration * 40); if (samples === 0) { audioPeaks = []; return; } |
| const peaks = []; const sampleSize = Math.floor(data.length / samples); |
| for (let i = 0; i < samples; i++) { let max = 0; const start = i * sampleSize; for (let j = 0; j < sampleSize; j++) { const val = Math.abs(data[start + j]); if (val > max) max = val; } peaks.push(Math.min(1, Math.max(0, max * 1.5))); } |
| audioPeaks = peaks; |
| }; |
| const drawWaveform = (canvas, progressRatio) => { |
| if (!canvas || !audioPeaks.length) return; |
| const ctx = canvas.getContext('2d'); const dpr = window.devicePixelRatio || 1; canvas.width = canvas.offsetWidth * dpr; canvas.height = canvas.offsetHeight * dpr; ctx.scale(dpr, dpr); |
| const width = canvas.offsetWidth; const height = canvas.offsetHeight; ctx.clearRect(0, 0, width, height); |
| const barWidth = 3; const barGap = 2; const totalBarWidth = barWidth + barGap; const numBars = Math.floor(width / totalBarWidth); const offset = (width - numBars * totalBarWidth) / 2; |
| ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--waveform-color-inactive').trim(); |
| for (let i = 0; i < numBars; i++) { const idx = Math.floor((i / numBars) * audioPeaks.length); const barH = (audioPeaks[idx] || 0) * height; ctx.fillRect(offset + i * totalBarWidth, (height - barH) / 2, barWidth, barH); } |
| const activeFillEnd = progressRatio * width; |
| if (activeFillEnd > 0) { |
| ctx.save(); ctx.beginPath(); ctx.rect(0, 0, activeFillEnd, height); ctx.clip(); ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--waveform-color-active').trim(); |
| for (let i = 0; i < numBars; i++) { const idx = Math.floor((i / numBars) * audioPeaks.length); const barH = (audioPeaks[idx] || 0) * height; ctx.fillRect(offset + i * totalBarWidth, (height - barH) / 2, barWidth, barH); } |
| ctx.restore(); |
| } |
| }; |
| function setupMainPlayerListeners() { |
| const pp = playerContent.querySelector('.audio-play-pause-btn-large'); const sb = playerContent.querySelector('.audio-skip-btn.backward'); const sf = playerContent.querySelector('.audio-skip-btn.forward'); const vb = playerContent.querySelector('.audio-volume-btn'); const sp = playerContent.querySelector('.audio-speed-btn'); |
| pp.onclick = () => mainAudioPlayer.paused ? mainAudioPlayer.play() : mainAudioPlayer.pause(); |
| sb.onclick = () => mainAudioPlayer.currentTime = Math.max(0, mainAudioPlayer.currentTime - 5); |
| sf.onclick = () => mainAudioPlayer.currentTime = Math.min(mainAudioPlayer.duration, mainAudioPlayer.currentTime + 5); |
| vb.onclick = () => { mainAudioPlayer.muted = !mainAudioPlayer.muted; vb.querySelector('.volume-high-icon').style.display = mainAudioPlayer.muted ? 'none' : 'block'; vb.querySelector('.volume-mute-icon').style.display = mainAudioPlayer.muted ? 'block' : 'none'; }; |
| sp.onclick = () => { currentPlaybackSpeedIndex = (currentPlaybackSpeedIndex + 1) % playbackSpeeds.length; mainAudioPlayer.playbackRate = playbackSpeeds[currentPlaybackSpeedIndex]; sp.textContent = `${playbackSpeeds[currentPlaybackSpeedIndex]}x`; }; |
| ['timeupdate', 'play', 'pause', 'ended'].forEach(e => mainAudioPlayer.addEventListener(e, updateMainPlayerUI)); window.addEventListener('resize', updateMainPlayerUI); |
| } |
| const updateMainPlayerUI = () => { |
| const isPlaying = !(mainAudioPlayer.paused || mainAudioPlayer.ended); |
| playerContent.querySelectorAll('.play-icon').forEach(i => i.style.display = isPlaying ? 'none' : 'block'); |
| playerContent.querySelectorAll('.pause-icon').forEach(i => i.style.display = isPlaying ? 'block' : 'none'); |
| playerContent.querySelectorAll('.audio-current-time').forEach(s => s.textContent = formatTime(mainAudioPlayer.currentTime)); |
| playerContent.querySelectorAll('.audio-total-time').forEach(s => s.textContent = isFinite(mainAudioPlayer.duration) ? formatTime(mainAudioPlayer.duration) : '0:00'); |
| const canvas = playerContent.querySelector('.audio-waveform-canvas'); |
| if (canvas) drawWaveform(canvas, isFinite(mainAudioPlayer.duration) && mainAudioPlayer.duration > 0 ? mainAudioPlayer.currentTime / mainAudioPlayer.duration : 0); |
| }; |
| |
| let saveTimeout; |
| function saveState() { clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { const state = { activeSpeakers: activeSpeakers.map(s => s.id), turns: Array.from(scriptContainer.querySelectorAll('.script-turn')).map(t => ({ speakerId: t.querySelector('.custom-select-container').dataset.selectedId, text: t.querySelector('textarea').value.trim() })), temperature: tempSlider.value }; localStorage.setItem('podcastStudioState', JSON.stringify(state)); }, 500); } |
| async function resetToDefaultState() { localStorage.removeItem('podcastStudioState'); await clearAudioDB(); activeSpeakers = []; scriptContainer.innerHTML = ''; masterAudioBlobs = []; addSpeakerToProject(speakers[0].id); addSpeakerToProject(speakers[1].id); addScriptTurn(speakers[0].id); addScriptTurn(speakers[1].id, '', true); tempSlider.value = 0.9; tempValueSpan.textContent = '0.9'; showUIState('initial', 'پادکست نهایی در اینجا ظاهر خواهد شد.'); } |
| async function loadState() { |
| try { |
| const saved = localStorage.getItem('podcastStudioState'); |
| if (saved) { |
| const state = JSON.parse(saved); |
| activeSpeakers = []; |
| if (state.activeSpeakers) state.activeSpeakers.forEach(sid => { const s = speakers.find(sp => sp.id === sid); if(s) activeSpeakers.push(s); }); else { activeSpeakers.push(speakers[0], speakers[1]); } |
| renderActiveSpeakers(); |
| scriptContainer.innerHTML = ''; |
| if (state.turns) state.turns.forEach(t => addScriptTurn(t.speakerId, t.text, true)); else { addScriptTurn(speakers[0].id, '', true); addScriptTurn(speakers[1].id, '', true); } |
| if (state.temperature) { tempSlider.value = state.temperature; tempValueSpan.textContent = state.temperature; } |
| const chunks = await getAllAudioFromDB(); |
| masterAudioBlobs = []; |
| if (chunks.length > 0) { |
| chunks.forEach(i => { if (i.index < state.turns.length) masterAudioBlobs[i.index] = i.blob; }); |
| if (masterAudioBlobs.some(b => b)) { rebuildFinalAudio(); scriptContainer.querySelectorAll('.script-turn').forEach((d, i) => { if (masterAudioBlobs[i]) setupTurnPlayer(d, i); }); } |
| } |
| clearHistoryBtn.style.display = 'inline-flex'; |
| } else resetToDefaultState(); |
| } catch (e) { resetToDefaultState(); } |
| } |
| function showUIState(state, msg = '') { |
| const spinner = generateBtn.querySelector(".spinner"); const txt = generateBtn.querySelector(".btn-text"); |
| if (state !== 'loading') updateUIWithServerStatus(); |
| spinner.style.display = state === 'loading' ? 'inline-block' : 'none'; txt.textContent = state === 'loading' ? 'در حال ساخت...' : '🎙️ ساخت پادکست'; |
| outputSection.classList.toggle('has-content', state === 'result'); playerContent.style.display = 'none'; statusMessage.style.display = 'none'; loadingAnimationWrapper.style.display = 'none'; clearHistoryBtn.style.display = 'none'; internalDownloadBtn.style.display = 'none'; |
| |
| |
| if (state !== 'loading') { |
| stopCustomVoiceProgress(); |
| } |
| |
| if (state === 'error') { statusMessage.style.display = 'block'; statusMessage.classList.add('error'); statusMessage.textContent = msg; if (masterAudioBlobs.length > 0) clearHistoryBtn.style.display = 'flex'; } |
| else if (state === 'loading') { loadingAnimationWrapper.style.display = 'flex'; loadingText.textContent = msg; } |
| else if (state === 'result' && masterAudioBlobs.length > 0) { playerContent.style.display = 'flex'; internalDownloadBtn.style.display = 'flex'; clearHistoryBtn.style.display = 'flex'; } |
| } |
| |
| function renderActiveSpeakers() { |
| const container = document.getElementById('project-speakers-container'); |
| const addCard = document.getElementById('add-speaker-card'); |
| document.querySelectorAll('.speaker-display-card:not(#add-speaker-card)').forEach(c => c.remove()); |
| activeSpeakers.forEach(speaker => { |
| const card = document.createElement('div'); card.className = 'speaker-display-card'; card.dataset.id = speaker.id; |
| let visualHTML = ''; |
| if (speaker.isCustom) { |
| visualHTML = `<div class="custom-voice-avatar"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg></div>`; |
| } else { |
| visualHTML = `<img src="${speaker.imgUrl}" alt="${speaker.name}">`; |
| } |
| card.innerHTML = `<button type="button" class="remove-speaker-btn" title="حذف">×</button>${visualHTML}<h3>${speaker.name}</h3>`; |
| container.insertBefore(card, addCard); |
| card.querySelector('.remove-speaker-btn').addEventListener('click', (e) => { e.stopPropagation(); removeSpeakerFromProject(speaker.id); }); |
| card.addEventListener('click', () => { speakerToReplaceId = speaker.id; openSpeakerModal(); }); |
| }); |
| updateAllTurnSelects(); saveState(); |
| } |
| |
| function openSpeakerModal() { |
| const grid = document.getElementById('speaker-grid'); grid.innerHTML = ''; |
| const activeIds = activeSpeakers.map(s => s.id); |
| document.getElementById('modal-title').textContent = speakerToReplaceId ? "جایگزینی گوینده" : "انتخاب گوینده"; |
| |
| speakers.forEach(speaker => { |
| const card = document.createElement('div'); card.className = 'speaker-card custom-speaker-card'; |
| if (activeIds.includes(speaker.id) && speaker.id !== speakerToReplaceId) card.classList.add('disabled'); |
| let visualContent; |
| |
| let menuHtml = ''; |
| if (speaker.isCustom) { |
| menuHtml = `<button class="card-menu-btn" onclick="event.stopPropagation(); this.nextElementSibling.classList.toggle('active');">⋮</button><div class="card-menu-dropdown"><div class="card-menu-item" onclick="event.stopPropagation(); renameCustomSpeaker('${speaker.id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>تغییر نام</div><div class="card-menu-item" onclick="event.stopPropagation(); triggerReplaceAudio('${speaker.id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>تغییر فایل صدا</div><div class="card-menu-item delete-item" onclick="event.stopPropagation(); triggerDeleteCustomSpeaker('${speaker.id}')"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>حذف</div></div>`; |
| visualContent = `<div class="speaker-visual"><div class="custom-voice-avatar"><svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg></div><div class="audio-preview-mini" onclick="event.stopPropagation(); playRefPreview('${speaker.id}')"><svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></div></div>`; |
| } else { |
| visualContent = `<div class="speaker-visual"><img src="${speaker.imgUrl}" alt="${speaker.name}"></div>`; |
| } |
| |
| card.innerHTML = `${menuHtml}${visualContent}<div class="speaker-name">${speaker.name}</div>`; |
| |
| if (!card.classList.contains('disabled')) { |
| card.addEventListener('click', () => { |
| if (speakerToReplaceId) { |
| const idx = activeSpeakers.findIndex(s => s.id === speakerToReplaceId); |
| if (idx > -1) { |
| activeSpeakers[idx] = speakers.find(s => s.id === speaker.id); |
| |
| |
| document.querySelectorAll('.script-turn .custom-select-container').forEach(select => { |
| if (select.dataset.selectedId === speakerToReplaceId) { |
| select.dataset.selectedId = speaker.id; |
| } |
| }); |
| } |
| lastAddedSpeakerIndex = -1; |
| speakerToReplaceId = null; |
| } else { |
| addSpeakerToProject(speaker.id); |
| } |
| renderActiveSpeakers(); |
| document.getElementById('speaker-modal').classList.remove('visible'); |
| saveState(); |
| }); |
| } |
| grid.appendChild(card); |
| }); |
| |
| const customBtn = document.createElement('div'); |
| customBtn.className = 'custom-voice-btn-container'; |
| customBtn.innerHTML = `<div class="add-custom-voice-btn" onclick="document.getElementById('custom-voice-info-modal').classList.add('visible')"><div class="custom-voice-icon-wrapper"><svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line><line x1="16" y1="6" x2="22" y2="6"></line><line x1="19" y1="3" x2="19" y2="9"></line></svg></div><div class="custom-voice-text"><h4>ساخت صدای اختصاصی</h4><p>با آپلود یک نمونه صدا، پادکست را با صدای خودتان یا صدای دلخواه بسازید.</p></div></div>`; |
| grid.appendChild(customBtn); |
| document.getElementById('speaker-modal').classList.add('visible'); |
| } |
| |
| document.addEventListener('click', (e) => { |
| if(!e.target.closest('.card-menu-btn')) { |
| document.querySelectorAll('.card-menu-dropdown.active').forEach(d => d.classList.remove('active')); |
| } |
| const trig = e.target.closest('.custom-select-trigger'); const opt = e.target.closest('.custom-select-option'); |
| document.querySelectorAll('.custom-select-container.open').forEach(c => { if (!c.contains(e.target)) c.classList.remove('open'); }); |
| if (trig) trig.closest('.custom-select-container').classList.toggle('open'); |
| else if (opt) { const c = opt.closest('.custom-select-container'); c.dataset.selectedId = opt.dataset.id; updateSingleTurnSelect(c, opt.dataset.id); c.classList.remove('open'); saveState(); } |
| }); |
| |
| function addSpeakerToProject(speakerId) { |
| if (!activeSpeakers.some(s => s.id === speakerId)) { |
| const speaker = speakers.find(s => s.id === speakerId); |
| if (speaker) { activeSpeakers.push(speaker); renderActiveSpeakers(); lastAddedSpeakerIndex = -1; } |
| } |
| } |
| function removeSpeakerFromProject(speakerId) { |
| activeSpeakers = activeSpeakers.filter(s => s.id !== speakerId); |
| renderActiveSpeakers(); |
| document.querySelectorAll('.script-turn').forEach(turnDiv => { |
| const select = turnDiv.querySelector('.custom-select-container'); |
| if (select.dataset.selectedId === speakerId) { select.dataset.selectedId = activeSpeakers.length > 0 ? activeSpeakers[0].id : ''; updateSingleTurnSelect(select); } |
| }); |
| lastAddedSpeakerIndex = -1; |
| } |
| function updateAllTurnSelects() { document.querySelectorAll('.custom-select-container').forEach(c => updateSingleTurnSelect(c, c.dataset.selectedId)); } |
| function updateSingleTurnSelect(container, preSelectedId = null) { |
| const trigger = container.querySelector('.custom-select-trigger'); |
| const optionsDiv = container.querySelector('.custom-select-options'); |
| optionsDiv.innerHTML = ''; |
| if (activeSpeakers.length === 0) { trigger.innerHTML = `<span>گویندهای انتخاب نشده</span><svg class="arrow" viewBox="0 0 24 24"><path fill="currentColor" d="M7,10L12,15L17,10H7Z"></path></svg>`; container.dataset.selectedId = ''; return; } |
| let selectedSpeaker = activeSpeakers.find(s => s.id === preSelectedId); |
| if (!selectedSpeaker) { selectedSpeaker = activeSpeakers[0]; preSelectedId = selectedSpeaker.id; } |
| const getIcon = (s) => s.isCustom ? `<div style="width:40px;height:40px;border-radius:10px;background:#764ba2;display:flex;align-items:center;justify-content:center;color:white"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 3c1.66 0 3 1.34 3 3s-1.34 3-3 3-3-1.34-3-3 1.34-3 3-3zm0 14.2c-2.5 0-4.71-1.28-6-3.22.03-1.99 4-3.08 6-3.08 1.99 0 5.97 1.09 6 3.08-1.29 1.94-3.5 3.22-6 3.22z"/></svg></div>` : `<img src="${s.imgUrl}" style="width:40px;height:40px;border-radius:10px;object-fit:cover;">`; |
| activeSpeakers.forEach(s => { |
| const opt = document.createElement('div'); opt.className = 'custom-select-option'; opt.dataset.id = s.id; |
| opt.innerHTML = `${getIcon(s)}<span>${s.name}</span>`; |
| optionsDiv.appendChild(opt); |
| }); |
| trigger.innerHTML = `${getIcon(selectedSpeaker)}<span>${selectedSpeaker.name}</span><svg class="arrow" viewBox="0 0 24 24" style="margin-right:auto;width:20px"><path fill="currentColor" d="M7,10L12,15L17,10H7Z"></path></svg>`; |
| container.dataset.selectedId = preSelectedId; |
| } |
| |
| function addScriptTurn(initialSpeakerId = null, initialText = '', fromLoad = false) { |
| if (activeSpeakers.length === 0) return; |
| const turnDiv = document.createElement('div'); turnDiv.className = 'script-turn'; |
| turnDiv.innerHTML = `<div class="turn-speaker-selector"><div class="custom-select-container" data-selected-id=""><div class="custom-select-trigger"></div><div class="custom-select-options"></div></div></div><div class="turn-content"><textarea placeholder="متن گفتگو..."></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" style="display:none;">فایل جدید جایگزین شد</span><span class="main-update-message" style="display:none;">فایل اصلی بروز شد</span></div></div><button type="button" class="remove-turn-btn" title="حذف">×</button>`; |
| const select = turnDiv.querySelector('.custom-select-container'); const ta = turnDiv.querySelector('textarea'); |
| ta.value = initialText; let sid = initialSpeakerId || activeSpeakers[0]?.id; if (!activeSpeakers.some(s => s.id === sid)) sid = activeSpeakers[0]?.id; |
| updateSingleTurnSelect(select, sid); |
| ta.addEventListener('input', saveState); select.addEventListener('click', saveState); |
| turnDiv.querySelector('.remove-turn-btn').addEventListener('click', async () => { const idx = Array.from(scriptContainer.children).indexOf(turnDiv); if (idx > -1) masterAudioBlobs.splice(idx, 1); turnDiv.remove(); await clearAudioDB(); masterAudioBlobs.forEach(async (b, i) => { if (b) await saveAudioToDB(i, b); }); if(masterAudioBlobs.length>0) rebuildFinalAudio(); else showUIState('initial'); lastAddedSpeakerIndex=-1; saveState(); }); |
| scriptContainer.appendChild(turnDiv); |
| if (!fromLoad) lastAddedSpeakerIndex = activeSpeakers.findIndex(s => s.id === sid); |
| } |
| |
| |
| async function handleRetry(turnDiv, turnIndex) { |
| const pc = turnDiv.querySelector('.turn-player-container'); |
| const rb = pc.querySelector('.turn-retry-btn'); |
| const lsw = pc.querySelector('.loading-state-wrapper'); |
| const lt = pc.querySelector('.loading-text'); |
| const succMsg = pc.querySelector('.replace-success-message'); |
| const mainMsg = pc.querySelector('.main-update-message'); |
| const ta = turnDiv.querySelector('textarea'); |
| const sid = turnDiv.querySelector('.custom-select-container').dataset.selectedId; |
| |
| if (!ta.value.trim() || !sid) return alert("متن یا گوینده نامعتبر است."); |
| |
| rb.style.display = 'none'; |
| lsw.style.display = 'flex'; |
| succMsg.style.display = 'none'; |
| mainMsg.style.display = 'none'; |
| |
| |
| const spk = speakers.find(s => s.id === sid); |
| const isCustom = spk && spk.isCustom; |
| let customMsgTimer; |
| |
| if(isCustom) { |
| lt.textContent = "قطعه اختصاصی زمان بیشتری نیاز داره برای تغییر لطفاً صبور باشید"; |
| lt.style.fontSize = "0.7em"; |
| customMsgTimer = setTimeout(() => { |
| lt.textContent = "در حال جایگزین صدا..."; |
| lt.style.fontSize = ""; |
| }, 7000); |
| } else { |
| lt.textContent = "در حال جایگزین صدا..."; |
| lt.style.fontSize = ""; |
| } |
| |
| try { |
| let payload = { text: ta.value.trim(), speaker: sid, temperature: parseFloat(tempSlider.value) }; |
| if (isCustom) { const rd = await getRefAudioFromDB(spk.id); if (rd && rd.blob) payload.ref_audio_base64 = await blobToBase64(rd.blob); } |
| |
| const res = await fetch(TTS_API_ENDPOINT, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }); |
| if (!res.ok) throw new Error(await res.text()); |
| const blob = await res.blob(); |
| |
| clearTimeout(customMsgTimer); |
| |
| |
| masterAudioBlobs[turnIndex] = blob; |
| await saveAudioToDB(turnIndex, blob); |
| pc.querySelector('.turn-audio').src = URL.createObjectURL(blob); |
| |
| |
| lsw.style.display = 'none'; |
| succMsg.style.display = 'flex'; |
| succMsg.style.animation = 'none'; |
| void succMsg.offsetWidth; |
| succMsg.style.animation = 'fadeInOut 2.5s forwards'; |
| |
| setTimeout(async () => { |
| succMsg.style.display = 'none'; |
| mainMsg.style.display = 'flex'; |
| mainMsg.style.animation = 'none'; |
| void mainMsg.offsetWidth; |
| mainMsg.style.animation = 'fadeInOut 2.5s forwards'; |
| |
| await rebuildFinalAudio(); |
| |
| setTimeout(() => { |
| mainMsg.style.display = 'none'; |
| rb.style.display = 'flex'; |
| }, 2500); |
| }, 2500); |
| |
| } catch (e) { |
| clearTimeout(customMsgTimer); |
| lsw.style.display = 'none'; |
| rb.style.display = 'flex'; |
| alert("تلاش مجدد شکست خورد."); |
| } |
| } |
| |
| async function rebuildFinalAudio() { |
| showUIState('loading', 'میکس مجدد...'); try { const m = await mergeAudioBlobs(masterAudioBlobs); if (!m) { showUIState('initial'); return; } mainAudioPlayer.src = URL.createObjectURL(bufferToWav(m)); mainAudioPlayer.onloadedmetadata = () => { processAudioForWaveform(m); showUIState('result'); updateMainPlayerUI(); }; } catch (e) { showUIState('error', e.message); } |
| } |
| |
| async function proceedWithGeneration(shouldDeductCredit) { |
| |
| const scriptData = Array.from(scriptContainer.querySelectorAll('.script-turn')).map((t, index) => ({ |
| element: t, index: index, speakerId: t.querySelector('.custom-select-container').dataset.selectedId, text: t.querySelector('textarea').value.trim() |
| })).filter(d => d.text.length > 0 && d.speakerId); |
| |
| if (activeSpeakers.length === 0) { alert('لطفا حداقل یک گوینده اضافه کنید.'); return; } |
| if (scriptData.length === 0) { generateBtn.classList.add('shake-it'); validationMessage.textContent = 'لطفاً سناریو را کامل کنید.'; validationMessage.classList.add('visible'); setTimeout(() => { generateBtn.classList.remove('shake-it'); validationMessage.classList.remove('visible'); }, 4000); return; } |
| |
| generateBtn.disabled = true; progressGrid.innerHTML = ''; |
| scriptData.forEach((item, i) => { const ind = document.createElement('div'); ind.className = 'progress-item pending'; ind.id = `progress-segment-${item.index}`; ind.textContent = i + 1; progressGrid.appendChild(ind); }); |
| |
| showUIState('loading', `در حال پردازش (0 از ${scriptData.length} تکمیل شده)`); |
| |
| |
| const hasCustomVoice = scriptData.some(item => { |
| const spk = speakers.find(s => s.id === item.speakerId); |
| return spk && spk.isCustom; |
| }); |
| |
| if (hasCustomVoice) { |
| startCustomVoiceProgress(); |
| } else { |
| stopCustomVoiceProgress(); |
| } |
| |
| |
| masterAudioBlobs = new Array(scriptData.length).fill(null); await clearAudioDB(); |
| |
| try { |
| let completedCount = 0; |
| const promises = scriptData.map(async segment => { |
| const spk = speakers.find(s => s.id === segment.speakerId); |
| let payload = { text: segment.text, speaker: segment.speakerId, temperature: parseFloat(tempSlider.value) }; |
| if (spk && spk.isCustom) { |
| const refData = await getRefAudioFromDB(spk.id); |
| if (refData && refData.blob) payload.ref_audio_base64 = await blobToBase64(refData.blob); |
| } |
| |
| return fetch(TTS_API_ENDPOINT, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(payload) }) |
| .then(async res => { |
| if (res.ok) { |
| const blob = await res.blob(); |
| const ind = document.getElementById(`progress-segment-${segment.index}`); if(ind){ ind.classList.remove('pending'); ind.classList.add('done'); ind.textContent = '✓'; } |
| completedCount++; loadingText.textContent = `در حال پردازش (${completedCount} از ${scriptData.length} تکمیل شده)`; |
| return { index: segment.index, blob: blob }; |
| } else { |
| const ind = document.getElementById(`progress-segment-${segment.index}`); if(ind){ ind.classList.remove('pending'); ind.classList.add('error'); ind.textContent = '!'; } |
| throw new Error(await res.text()); |
| } |
| }); |
| }); |
| |
| const results = await Promise.all(promises); |
| results.forEach(async r => { masterAudioBlobs[r.index] = r.blob; await saveAudioToDB(r.index, r.blob); }); |
| loadingText.textContent = 'میکس نهایی صدا...'; |
| const merged = await mergeAudioBlobs(masterAudioBlobs); |
| if (!merged) throw new Error("میکس صدا شکست خورد."); |
| mainAudioPlayer.src = URL.createObjectURL(bufferToWav(merged)); |
| mainAudioPlayer.onloadedmetadata = () => { processAudioForWaveform(merged); showUIState('result'); }; |
| scriptData.forEach(i => setupTurnPlayer(i.element, i.index)); |
| |
| if (shouldDeductCredit) { fetch('/api/use-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); } |
| |
| } catch (e) { console.error(e); showUIState('error', `عملیات شکست خورد: ${e.message}`); } |
| } |
| |
| function setupTurnPlayer(div, idx) { |
| const pc = div.querySelector('.turn-player-container'); const pb = pc.querySelector('.turn-play-btn'); const ta = pc.querySelector('.turn-audio'); const rb = pc.querySelector('.turn-retry-btn'); |
| if (masterAudioBlobs[idx]) { if(ta.src) URL.revokeObjectURL(ta.src); ta.src = URL.createObjectURL(masterAudioBlobs[idx]); } else { pc.classList.remove('visible'); return; } |
| pb.onclick = () => { if (currentlyPlayingTurnPlayer && currentlyPlayingTurnPlayer !== ta) { currentlyPlayingTurnPlayer.pause(); currentlyPlayingTurnPlayer.closest('.turn-player-container').classList.remove('playing'); } if (ta.paused) { ta.play(); currentlyPlayingTurnPlayer = ta; } else ta.pause(); }; |
| ta.onplay = () => pc.classList.add('playing'); ta.onpause = () => pc.classList.remove('playing'); ta.onended = () => { pc.classList.remove('playing'); currentlyPlayingTurnPlayer = null; }; |
| rb.onclick = () => handleRetry(div, idx); pc.classList.add('visible'); |
| } |
| |
| |
| document.getElementById('confirm-delete-speaker-btn').addEventListener('click', async () => { |
| if (speakerIdToDelete) { |
| |
| speakers = speakers.filter(s => s.id !== speakerIdToDelete); |
| |
| saveCustomSpeakersMetadata(); |
| |
| await deleteRefAudioFromDB(speakerIdToDelete); |
| |
| if (activeSpeakers.some(s => s.id === speakerIdToDelete)) { |
| removeSpeakerFromProject(speakerIdToDelete); |
| } |
| |
| |
| openSpeakerModal(); |
| |
| |
| confirmDeleteSpeakerModal.classList.remove('visible'); |
| speakerIdToDelete = null; |
| } |
| }); |
| |
| document.getElementById('cancel-delete-speaker-btn').addEventListener('click', () => { |
| confirmDeleteSpeakerModal.classList.remove('visible'); |
| speakerIdToDelete = null; |
| }); |
| |
| form.addEventListener('submit', async () => { if (generateBtn.disabled) return; if (userSubscriptionStatus === 'paid') { await proceedWithGeneration(false); } else { try { const r = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); const res = await r.json(); if (res.limit_reached) { generateBtn.disabled = true; upgradeBtn.style.display = 'block'; validationMessage.textContent = 'اعتبار تمام شد.'; startCountdown(res.reset_timestamp); return; } await proceedWithGeneration(true); } catch (e) { showUIState('error', e.message); } } }); |
| clearHistoryBtn.onclick = () => confirmDeleteModal.classList.add('visible'); confirmDeleteBtn.onclick = () => { resetToDefaultState(); confirmDeleteModal.classList.remove('visible'); }; cancelDeleteBtn.onclick = () => confirmDeleteModal.classList.remove('visible'); |
| addSpeakerCard.onclick = () => { speakerToReplaceId = null; openSpeakerModal(); }; |
| document.querySelector('.close-modal-btn').onclick = () => document.getElementById('speaker-modal').classList.remove('visible'); |
| internalDownloadBtn.onclick = async () => { if(!mainAudioPlayer.src)return; try{ const b = await (await fetch(mainAudioPlayer.src)).blob(); window.parent.postMessage({ type: 'PROCESS_AND_DOWNLOAD_AUDIO', blob: b }, '*'); } catch(e){ alert("Download failed"); }}; |
| addTurnBtn.onclick = () => { if (activeSpeakers.length === 0) return alert('گوینده اضافه کنید'); lastAddedSpeakerIndex = (lastAddedSpeakerIndex + 1) % activeSpeakers.length; addScriptTurn(activeSpeakers[lastAddedSpeakerIndex].id); }; |
| tempSlider.oninput = () => { tempValueSpan.textContent = tempSlider.value; saveState(); }; |
| |
| (async () => { |
| await initDB(); await loadCustomSpeakers(); await loadState(); initializeApp(); |
| })(); |
| async function initializeApp() { userFingerprint = await getBrowserFingerprint(); parent.postMessage({ type: 'REQUEST_USER_STATUS' }, '*'); setupMainPlayerListeners(); } |
| }); |
| </script> |
| </body> |
| </html> |