| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>AI Video Studio</title> |
| <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@400;500;600;700;800&display=swap" rel="stylesheet"> |
| <style> |
| :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-premium: #FFC107; |
| --accent-premium-glow: rgba(255, 193, 7, 0.3); |
| --success-color: #38A169; |
| --danger-color: #e53e3e; |
| --danger-color-hover: #c53030; |
| --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 core-pulse { 0%, 100% { transform: scale(0.95); box-shadow: 0 0 25px var(--accent-primary-glow); } 50% { transform: scale(1.05); box-shadow: 0 0 50px rgba(74, 108, 250, 0.5); } } |
| @keyframes ring-rotate { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } |
| @keyframes background-pan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } |
| @keyframes badge-fade-in { from { opacity: 0; transform: translateY(-10px) scale(0.95); } to { opacity: 1; transform: translateY(0) scale(1); } } |
| @keyframes slideInUp { from { opacity: 0; transform: translateY(30px); } to { opacity: 1; transform: translateY(0); } } |
| |
| |
| @keyframes shake { |
| 0%, 100% { transform: translateX(0); } |
| 20%, 60% { transform: translateX(-6px); } |
| 40%, 80% { transform: translateX(6px); } |
| } |
| .shake-animation { |
| animation: shake 0.4s cubic-bezier(.36,.07,.19,.97) both !important; |
| background: linear-gradient(95deg, #e53e3e 0%, #c53030 100%) !important; |
| box-shadow: 0 6px 12px -3px rgba(229, 62, 62, 0.4) !important; |
| } |
| |
| |
| .project-warning-box { |
| display: none; |
| background: rgba(229, 62, 62, 0.08); |
| border: 1px solid rgba(229, 62, 62, 0.3); |
| color: var(--danger-color); |
| padding: 12px 16px; |
| border-radius: var(--radius-input); |
| margin-top: 15px; |
| font-size: 0.9rem; |
| font-weight: 600; |
| text-align: center; |
| line-height: 1.6; |
| animation: fadeIn 0.4s ease-out; |
| } |
| .project-warning-box svg { |
| width: 20px; height: 20px; vertical-align: middle; margin-left: 5px; margin-top: -3px; |
| } |
| |
| @keyframes modal-fade-in { from { opacity: 0; } to { opacity: 1; } } |
| @keyframes modal-zoom-in { from { transform: translate(-50%, -50%) scale(0.9); } to { transform: translate(-50%, -50%) scale(1); } } |
| |
| .modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(26, 32, 44, 0.5); backdrop-filter: blur(8px); z-index: 1000; animation: modal-fade-in 0.3s ease-out; } |
| .modal-overlay.active { display: block; } |
| .modal-content { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background-color: var(--panel-bg); padding: 2.5rem; border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); width: 90%; max-width: 450px; text-align: center; animation: modal-zoom-in 0.3s cubic-bezier(0.4, 0, 0.2, 1); } |
| .modal-icon { width: 60px; height: 60px; margin: 0 auto 1.5rem; background: linear-gradient(45deg, var(--accent-primary-glow), rgba(15, 212, 168, 0.2)); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--accent-primary); } |
| .modal-icon svg { width: 32px; height: 32px; } |
| .modal-title { font-size: 1.3rem; font-weight: 700; color: var(--text-primary); margin: 0 0 1rem 0; } |
| .modal-text { font-size: 0.95rem; color: var(--text-secondary); line-height: 1.7; margin: 0 0 2rem 0; } |
| .modal-buttons { display: flex; gap: 1rem; width: 100%; } |
| .modal-button { flex: 1; padding: 0.8rem 1.5rem; background: var(--accent-primary); color: white; border: none; border-radius: var(--radius-btn); font-family: var(--app-font); font-weight: 600; font-size: 1rem; cursor: pointer; transition: var(--transition-smooth); } |
| .modal-button:hover { background: var(--accent-primary-hover); transform: translateY(-2px); } |
| .modal-button.secondary { background: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); } |
| .modal-button.secondary:hover { background-color: var(--panel-border); color: var(--text-primary); } |
| .modal-footer-text { margin-top: 1.5rem; font-size: 0.85rem; color: var(--text-tertiary); } |
| |
| #confirm-reset-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); } |
| #confirm-reset-modal .modal-button:not(.secondary) { background-color: var(--danger-color); } |
| #confirm-reset-modal .modal-button:not(.secondary):hover { background-color: var(--danger-color-hover); } |
| |
| #extend-guide-modal .modal-icon { color: var(--accent-premium); background: rgba(255, 193, 7, 0.1); } |
| #extend-guide-modal .modal-text { font-size: 0.85rem; text-align: right; line-height: 1.8; max-height: 50vh; overflow-y: auto; padding-left: 10px; } |
| |
| #report-video-modal .modal-icon { color: var(--danger-color); background: rgba(229, 62, 62, 0.1); } |
| .report-options-container { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin-bottom: 1.5rem; } |
| .report-option-btn { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); padding: 8px 14px; border-radius: var(--radius-btn); cursor: pointer; transition: all 0.2s ease; } |
| .report-option-btn:hover { background-color: var(--panel-border); color: var(--text-primary); } |
| #custom-report-section { display: none; width: 100%; } |
| #report-video-modal textarea { min-height: 80px; margin-top: 1rem; } |
| #emoji-container { margin-top: 10px; } .emoji { cursor: pointer; font-size: 24px; margin: 0 5px; } |
| #report-success-message { color: var(--success-color); font-weight: 600; display: none; margin-top: 1rem; } |
| |
| body { font-family: var(--app-font); background-color: var(--app-bg); color: var(--text-primary); margin: 0; padding: 2.5rem 1rem; display: flex; justify-content: center; align-items: flex-start; min-height: 100vh; } |
| .container { max-width: 820px; width: 100%; } |
| header { position: relative; text-align: center; margin-bottom: 2.5rem; padding: 2rem 0; animation: fadeIn 0.8s 0.1s ease-out backwards; overflow: hidden; } |
| #neural-network-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; } |
| .header-content { position: relative; z-index: 2; } |
| .ai-orb-container { width: 150px; height: 150px; margin: 0 auto 1rem; position: relative; display: flex; align-items: center; justify-content: center; } |
| .orb-background { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(ellipse at center, rgba(74, 108, 250, 0.15) 0%, transparent 70%), linear-gradient(45deg, rgba(15, 212, 168, 0.05), rgba(74, 108, 250, 0.05)); border-radius: 50%; animation: background-pan 12s ease-in-out infinite; background-size: 200% 200%; } |
| .orb-core { width: 60px; height: 60px; border-radius: 50%; background: radial-gradient(circle, #394A7A 0%, #1A202C 70%); box-shadow: 0 0 25px var(--accent-primary-glow); display: flex; align-items: center; justify-content: center; position: relative; z-index: 2; animation: core-pulse 4s ease-in-out infinite; border: 1px solid rgba(74, 108, 250, 0.3); } |
| .play-icon { width: 24px; height: 24px; color: var(--accent-secondary); filter: drop-shadow(0 0 8px rgba(15, 212, 168, 0.7)); transition: var(--transition-smooth); } |
| .orb-core:hover .play-icon { transform: scale(1.1); color: #fff; filter: drop-shadow(0 0 12px rgba(15, 212, 168, 1)); } |
| .orb-ring { position: absolute; top: 50%; left: 50%; border-style: solid; border-color: transparent; border-radius: 50%; z-index: 1; mix-blend-mode: screen; } |
| .orb-ring.one { width: 100px; height: 100px; margin-top: -50px; margin-left: -50px; border-width: 2px; border-top-color: var(--accent-primary); border-right-color: var(--accent-primary); animation: ring-rotate 8s linear infinite; filter: blur(1px); } |
| .orb-ring.two { width: 120px; height: 120px; margin-top: -60px; margin-left: -60px; border-width: 1px; border-bottom-color: var(--accent-secondary); animation: ring-rotate 10s linear infinite reverse; } |
| .orb-ring.three { width: 140px; height: 140px; margin-top: -70px; margin-left: -70px; border-width: 2px; border-left-color: rgba(255, 255, 255, 0.5); border-right-color: rgba(255, 255, 255, 0.5); animation: ring-rotate 12s linear infinite; filter: blur(1px); opacity: 0.5; } |
| h1 { font-size: 2.8rem; font-weight: 800; margin: 0; background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; } |
| .subtitle { font-size: 1.1rem; color: var(--text-secondary); margin-top: 0.5rem; } |
| .instruction { font-size: 0.95rem; color: var(--text-tertiary); margin-top: 0.75rem; font-weight: 500; } |
| |
| main { padding: 3rem; background-color: var(--panel-bg); border-radius: var(--radius-card); box-shadow: var(--shadow-xl); border: 1px solid var(--panel-border); animation: fadeIn 0.8s 0.3s ease-out backwards; } |
| .form-group { margin-bottom: 2.5rem; } |
| .form-group:last-child { margin-bottom: 0; } |
| .form-label { display: flex; align-items: center; gap: 0.75rem; font-weight: 700; color: var(--text-primary); font-size: 1.2em; margin-bottom: 1.2rem; } |
| .form-label svg { width: 24px; height: 24px; color: var(--accent-primary); } |
| .prompt-guide { font-size: 0.85rem; color: var(--text-tertiary); margin-top: -0.8rem; margin-bottom: 1rem; display: none; } |
| |
| #image-drop-zone { position: relative; border: 2px dashed var(--input-border); border-radius: var(--radius-input); padding: 2.5rem; text-align: center; cursor: pointer; transition: var(--transition-smooth); background-color: var(--input-bg); min-height: 200px; display: flex; flex-direction: column; justify-content: center; align-items: center; overflow: hidden; } |
| #image-drop-zone.drag-over, #image-drop-zone:hover:not(.has-image) { border-color: var(--accent-primary); background-color: #fff; box-shadow: 0 0 15px var(--accent-primary-glow); } |
| #image-drop-zone.has-image { border-style: solid; border-color: var(--success-color); padding: 0; cursor: default; } |
| .upload-content { display: flex; flex-direction: column; align-items: center; gap: 1rem; } |
| .upload-icon svg { width: 48px; height: 48px; color: var(--accent-primary); stroke-width: 1.5; opacity: 0.8; } |
| #image-drop-zone p { margin: 0; color: var(--text-secondary); font-weight: 500; } |
| #imagePreview { display: none; width: 100%; height: 100%; object-fit: contain; position: absolute; top: 0; left: 0; } |
| #image-drop-zone.has-image .upload-content { display: none; } |
| #image-drop-zone.has-image #imagePreview { display: block; } |
| |
| #remove-image-btn { position: absolute; top: 12px; left: 12px; width: 32px; height: 32px; background-color: rgba(26, 32, 44, 0.7); color: white; border: none; border-radius: 50%; font-size: 22px; font-weight: bold; line-height: 32px; text-align: center; cursor: pointer; opacity: 0; visibility: hidden; transform: scale(0.8); transition: all 0.25s ease-out; z-index: 10; } |
| #image-drop-zone.has-image #remove-image-btn { opacity: 1; visibility: visible; transform: scale(1); } |
| #remove-image-btn:hover { background-color: var(--danger-color); transform: scale(1.1) rotate(90deg); } |
| |
| textarea { width: 100%; padding: 1rem 1.2rem; border-radius: var(--radius-input); border: 1px solid var(--input-border); background-color: var(--input-bg); color: var(--text-primary); box-shadow: var(--shadow-sm) inset; font-family: var(--app-font); font-size: 1rem; box-sizing: border-box; transition: var(--transition-smooth); min-height: 120px; resize: vertical; } |
| textarea:focus { outline: none; border-color: var(--accent-primary); box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; background-color: var(--panel-bg); } |
| textarea:disabled { background-color: var(--input-border); color: var(--text-tertiary); } |
| |
| .slider-group { margin-top: 2rem; margin-bottom: 1.5rem; } |
| .slider-label-container { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } |
| .slider-label { font-weight: 600; color: var(--text-secondary); font-size: 0.95rem; display: flex; align-items: center; gap: 8px; } |
| .slider-label.small-text { font-size: 0.8rem; font-weight: 500; } |
| .info-icon { width: 20px; height: 20px; background-color: var(--input-border); color: var(--text-tertiary); border-radius: 50%; font-size: 14px; font-weight: 700; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s ease; } |
| .info-icon:hover { background-color: var(--accent-primary); color: white; transform: scale(1.1); } |
| .slider-value { font-weight: 700; font-size: 1rem; color: var(--accent-primary); background-color: var(--input-bg); padding: 0.25rem 0.75rem; border-radius: 8px; border: 1px solid var(--input-border); min-width: 80px; text-align: center; } |
| .slider-wrapper { display: flex; align-items: center; gap: 1rem; } |
| .slider-icon { width: 22px; height: 22px; color: var(--text-tertiary); flex-shrink: 0; opacity: 0.8; } |
| input[type="range"] { -webkit-appearance: none; appearance: none; flex-grow: 1; width: 100%; height: 12px; background: var(--input-bg); border-radius: var(--radius-input); outline: none; cursor: pointer; direction: rtl; box-shadow: inset 0 1px 3px rgba(0,0,0,0.1); transition: var(--transition-smooth); } |
| input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; margin-top: -6px; } |
| input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: linear-gradient(145deg, var(--accent-primary), var(--accent-secondary)); border-radius: 50%; border: 3px solid white; box-shadow: var(--shadow-md); transition: all 0.2s ease-in-out; } |
| input[type="range"]:hover { box-shadow: inset 0 1px 3px rgba(0,0,0,0.1), 0 0 0 4px var(--accent-primary-glow); } |
| input[type="range"]:hover::-webkit-slider-thumb { box-shadow: 0 0 0 10px var(--accent-primary-glow), var(--shadow-md); transform: scale(1.1); } |
| input[type="range"]:active::-webkit-slider-thumb { transform: scale(1.2); box-shadow: 0 0 0 12px var(--accent-primary-glow), var(--shadow-md); } |
| |
| #generateButton { display: flex; align-items: center; justify-content: center; gap: 0.75rem; width: 100%; padding: 1.1rem; font-size: 1.2rem; font-weight: 700; 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); margin-top: 1rem; } |
| #generateButton svg { width: 24px; height: 24px; margin-left: 4px; filter: drop-shadow(0 0 5px rgba(255,255,255,0.5)); } |
| #generateButton:hover:not(:disabled) { transform: translateY(-5px) scale(1.02); box-shadow: 0 8px 20px -4px var(--accent-primary-glow); } |
| #generateButton:disabled { background: var(--text-tertiary); cursor: not-allowed; box-shadow: none; opacity: 0.7; } |
| |
| #subscription-status-badge { display: none; padding: 6px 16px; border-radius: 20px; font-size: 0.9em; font-weight: 700; margin-top: 1.25rem; letter-spacing: 0.5px; animation: badge-fade-in 0.6s 0.5s ease-out backwards; } |
| #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); } |
| #credit-info-section { margin-top: 1.5rem; margin-bottom: 0.5rem; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-input); text-align: center; font-size: 0.95rem; font-weight: 500; color: var(--text-secondary); border: 1px solid var(--input-border); display: none; } |
| #upgrade-button { margin-top: 1rem; width: 100%; padding: 1rem; font-size: 1.1rem; font-weight: 700; background: linear-gradient(95deg, #FFD54F, #FFC107 100%); color: #212529; border: none; border-radius: var(--radius-btn); cursor: pointer; box-shadow: 0 8px 20px -5px rgba(255, 193, 7, 0.3); display: none; } |
| |
| #results-container { min-height: 250px; position: relative; padding: 1rem; background-color: var(--input-bg); border-radius: var(--radius-card); border: 2px dashed var(--input-border); box-shadow: var(--shadow-sm) inset; transition: var(--transition-smooth); display: flex; flex-direction: column; align-items: center; justify-content: center; } |
| #statusSection { display: none; width: 100%; } |
| #statusSection.active { display: block; animation: fadeIn 0.5s; } |
| #criticalErrorSection { display: none; width: 100%; } |
| #criticalErrorSection.active { display: flex; flex-direction: column; align-items: center; justify-content: center; animation: fadeIn 0.5s; } |
| |
| #latest-result-wrapper { width: 100%; } |
| #previous-results-container { display: none; width: 100%; margin-top: 2.5rem; } |
| .results-separator { border: 0; height: 1px; background-image: linear-gradient(to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0)); margin: 2rem 0; } |
| #previous-results-title { text-align: center; font-weight: 600; color: var(--text-secondary); margin-bottom: 2rem; font-size: 1.1rem; } |
| #previous-results-wrapper { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1.5rem; } |
| |
| .video-result-card { background: var(--panel-bg); border-radius: var(--radius-card); padding: 1rem; border: 1px solid var(--panel-border); box-shadow: var(--shadow-md); animation: slideInUp 0.6s cubic-bezier(0.4, 0, 0.2, 1); } |
| .video-result-card.latest { padding: 0; box-shadow: none; border: none; background: transparent; text-align: center; } |
| .video-result-card video { width: 100%; border-radius: var(--radius-input); margin: 0 auto 1rem; display: block; background-color: #000; box-shadow: var(--shadow-lg); } |
| .video-result-card.latest video { max-width: 100%; border-radius: 20px; box-shadow: var(--shadow-xl); } |
| .video-info { display: flex; align-items: center; justify-content: center; text-align: center; color: var(--text-tertiary); font-size: 0.85rem; margin-bottom: 1.5rem; } |
| .video-result-card.latest .video-info { margin-top: 1.5rem; } |
| .video-controls { display: flex; justify-content: center; gap: 1rem; flex-wrap: wrap; } |
| |
| .video-button { padding: 0.7rem 1.5rem; border-radius: var(--radius-btn); border: none; cursor: pointer; font-family: var(--app-font); font-weight: 600; font-size: 1rem; transition: var(--transition-smooth); display: inline-flex; align-items: center; gap: 0.5rem; text-decoration: none;} |
| .video-button.primary { background-color: var(--accent-primary); color: white; } |
| .video-button.primary:hover:not(:disabled) { background-color: var(--accent-primary-hover); transform: translateY(-2px); } |
| .video-button.secondary { background-color: var(--accent-premium); color: #333; } |
| .video-button.secondary:hover:not(:disabled) { background-color: #ffb300; transform: translateY(-2px); } |
| .video-button:not(.primary):not(.secondary) { background-color: var(--input-bg); color: var(--text-secondary); border: 1px solid var(--input-border); } |
| .video-button:not(.primary):not(.secondary):hover:not(:disabled) { background-color: var(--panel-border); color: var(--text-primary); } |
| .video-button:disabled { cursor: not-allowed; opacity: 0.6; } |
| |
| #statusMessages { max-height: 150px; overflow-y: auto; width: 100%; padding: 0.5rem; margin-bottom: 1rem; } |
| .status-message { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius-input); font-size: 0.9rem; background: var(--panel-bg); border: 1px solid var(--panel-border); color: var(--text-secondary); } |
| .status-message.success { border-left: 4px solid var(--success-color); color: var(--success-color); } |
| .status-message.error { border-left: 4px solid var(--danger-color); color: var(--danger-color); } |
| .status-icon { width: 18px; height: 18px; } |
| |
| #aiLoader { display: none; align-items: center; justify-content: center; } |
| .generator-container { position: relative; width: 400px; max-width: 100%; height: 300px; border: 2px solid #38bdf8; border-radius: 20px; overflow: hidden; box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); animation: pulse-loader 5s infinite cubic-bezier(0.4, 0, 0.6, 1); background-color: #161b22; color: #f0f6fc; } |
| @keyframes pulse-loader { 0% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } 50% { box-shadow: 0 0 60px rgba(56, 189, 248, 0.7); } 100% { box-shadow: 0 0 40px rgba(56, 189, 248, 0.3); } } |
| .noise-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><rect width="100" height="100" fill="none"/><filter id="noise"><feTurbulence type="fractalNoise" baseFrequency="0.5" numOctaves="4" stitchTiles="stitch"/></filter><rect width="100%" height="100%" filter="url(%23noise)" opacity="0.6"/></svg>') repeat; opacity: 1; animation: fade-noise 7s infinite ease-in-out; } @keyframes fade-noise { 0% { opacity: 1; filter: blur(5px); } 30% { opacity: 0.8; filter: blur(2px); } 100% { opacity: 0; filter: blur(0px); } } |
| .sketch-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; filter: grayscale(1) contrast(1.5) blur(3px); opacity: 0; animation: reveal-sketch 7s infinite ease-in-out; } @keyframes reveal-sketch { 0% { opacity: 0; } 20% { opacity: 1; } 60% { opacity: 0.5; } 100% { opacity: 0; } } |
| .building-layer { position: absolute; top: 0; left: 0; width: 100%; height: 100%; filter: blur(15px); opacity: 0; animation: denoise-color 7s infinite ease-in-out; } @keyframes denoise-color { 0% { opacity: 0; } 40% { opacity: 0.6; filter: blur(5px); } 100% { opacity: 1; filter: blur(0px); } } |
| .pixel-grid { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: repeating-linear-gradient(0deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px), repeating-linear-gradient(90deg, transparent 0 1px, rgba(255,255,255,0.1) 1px 2px); opacity: 1; animation: dissolve-grid 7s infinite ease-in-out; } @keyframes dissolve-grid { 0% { opacity: 1; } 70% { opacity: 0.5; } 100% { opacity: 0; } } |
| .particles { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: radial-gradient(circle, rgba(56, 189, 248, 0.2) 0%, transparent 50%); animation: flow-particles 7s infinite cubic-bezier(0.4, 0, 0.6, 1); } @keyframes flow-particles { 0% { transform: translate(0, 0) scale(1); opacity: 0.5; } 50% { transform: translate(10px, -15px) scale(1.05); opacity: 0.8; } 100% { transform: translate(0, 0) scale(1); opacity: 0.5; } } |
| .text-overlay { position: absolute; top: 45%; left: 50%; transform: translate(-50%, -50%); font-size: 24px; font-weight: 700; text-shadow: 0 0 20px rgba(56, 189, 248, 0.8); animation: glow-text 7s infinite ease-in-out; font-family: var(--app-font); width: 90%; text-align: center;} @keyframes glow-text { 0% { opacity: 0.7; } 50% { opacity: 1; } 100% { opacity: 0.7; } } |
| .progress-bar { position: absolute; bottom: 0; left: 0; width: 0%; height: 6px; background: linear-gradient(to right, #38bdf8, #bb86fc, #facc15); transition: width 0.3s linear; } |
| |
| .regenerate-icon, .report-flag-icon { display: inline-block; width: 16px; height: 16px; margin: 0 8px; vertical-align: middle; cursor: pointer; transition: all 0.4s ease; color: var(--text-tertiary); } |
| .regenerate-icon:hover { color: var(--accent-primary); transform: rotate(180deg); } |
| .report-flag-icon:hover { color: var(--danger-color); } |
| |
| #frame-selector-modal .modal-content { max-width: 650px; } |
| #frame-previews { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 1rem; } |
| .frame-preview-item { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; } |
| .frame-preview-item img { width: 100%; border-radius: var(--radius-input); border: 3px solid transparent; cursor: pointer; transition: all 0.2s ease-in-out; } |
| .frame-preview-item img:hover { border-color: var(--accent-primary); transform: scale(1.05); } |
| .frame-preview-item span { font-size: 0.8rem; font-weight: 600; color: var(--text-secondary); } |
| #frame-loader { border: 4px solid var(--input-border); border-top: 4px solid var(--accent-primary); border-radius: 50%; width: 40px; height: 40px; animation: ring-rotate 1s linear infinite; margin: 2rem auto; } |
| |
| @media (max-width: 768px) { main { padding: 3rem 1.5rem; } h1 { font-size: 2.2rem; } .generator-container { height: 250px; } .text-overlay { font-size: 18px; } #previous-results-wrapper { grid-template-columns: 1fr; } } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <canvas id="neural-network-canvas"></canvas> |
| <div class="header-content"> |
| <div class="ai-orb-container"> |
| <div class="orb-background"></div> |
| <div class="orb-core"> |
| <svg class="play-icon" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z"></path></svg> |
| </div> |
| <div class="orb-ring one"></div><div class="orb-ring two"></div><div class="orb-ring three"></div> |
| </div> |
| <h1>استودیو ویدیو هوش مصنوعی</h1> |
| <p class="subtitle">تصاویر خود را با قدرت هوش مصنوعی به ویدیو سینمایی تبدیل کنید</p> |
| <p class="instruction">ابتدا تصویر خود را تولید کرده و اینجا آپلود کنید تا ویدیوی خود را بسازید.</p> |
| <div id="subscription-status-badge" style="display:inline-block;"></div> |
| </div> |
| </header> |
| <main> |
| <div id="main-controls"> |
| <div class="form-group"> |
| <div class="form-label"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4Z"/></svg> |
| ۱. تصویر پروژه |
| </div> |
| <label id="image-drop-zone" for="imageFile"> |
| <div class="upload-content"> |
| <div class="upload-icon"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="17 8 12 3 7 8"></polyline><line x1="12" y1="3" x2="12" y2="15"></line></svg> |
| </div> |
| <p>فایل تصویر را اینجا بکشید یا برای انتخاب کلیک کنید</p> |
| </div> |
| <img id="imagePreview" src="" alt=""> |
| <button id="remove-image-btn" type="button" title="شروع پروژه جدید (حذف تصویر و نتایج)">×</button> |
| </label> |
| <input type="file" id="imageFile" accept="image/jpeg, image/png, image/webp" hidden> |
| </div> |
| <div class="form-group"> |
| <label for="prompt" class="form-label"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg> |
| ۲. دستور ساخت |
| </label> |
| <p class="prompt-guide" id="prompt-guide-text">ثانیههای جدید ادامه ویدیو رو میتونید توصیف کنید یا با پرامپت قبلی ادامه دهید</p> |
| <textarea id="prompt" rows="4" placeholder="مثال: زوم آهسته به بیرون در حالی که برگها در باد میرقصند و نور خورشید از بین درختان میتابد"></textarea> |
| |
| <div class="slider-group"> |
| <div class="slider-label-container"> |
| <label for="durationSlider" class="slider-label" id="duration-label">مدت زمان<span class="info-icon" id="duration-info-icon">!</span></label> |
| <span id="durationValue" class="slider-value">5.0 ثانیه</span> |
| </div> |
| <div class="slider-wrapper"> |
| <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72"></path></svg> |
| <input type="range" id="durationSlider" min="1.0" max="5.0" value="5.0" step="1.0"> |
| <svg class="slider-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline></svg> |
| </div> |
| </div> |
| |
| <div id="credit-info-section"></div> |
| <button id="upgrade-button">⭐️ ارتقا به نسخه کامل و نامحدود</button> |
| |
| <button id="generateButton"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/></svg> |
| <span>ساخت ویدیو جادویی</span> |
| </button> |
| |
| |
| <div id="project-warning-message" class="project-warning-box"> |
| شما یک پروژه از قبل دارید. اگر پروژه قبلی را طولانی نمیکنید، آن را از بخش پایین حذف کرده و مجدداً ویدیو جدید بسازید. |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg> |
| </div> |
| </div> |
| </div> |
| |
| <div class="form-group"> |
| <div class="form-label"> |
| <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 3L12 8L17 10L12 12L10 17L8 12L3 10L8 8L10 3z"/><path d="M21 14l-1.5 3-3-1.5 3-3 1.5 3z"/><path d="M19.5 2.5l-3 1.5 1.5 3 3-1.5-1.5-3z"/></svg> |
| ۳. گالری نتایج |
| </div> |
| <div id="results-container"> |
| <div id="statusSection"> |
| <div id="statusMessages"></div> |
| <div id="aiLoader" style="display: none;"> |
| <div class="generator-container"> |
| <div class="noise-layer"></div> |
| <div class="sketch-layer"></div> |
| <div class="building-layer"></div> |
| <div class="pixel-grid"></div> |
| <div class="particles"></div> |
| <div class="text-overlay">در حال پردازش ویدیو...</div> |
| <div class="progress-bar" id="tv-progress-bar"></div> |
| </div> |
| </div> |
| </div> |
| <div id="criticalErrorSection"></div> |
| <hr id="gallery-separator" class="results-separator" style="display: none;"> |
| <div id="results-gallery"> |
| <div id="latest-result-wrapper"></div> |
| <div id="previous-results-container"> |
| <hr class="results-separator"> |
| <h3 id="previous-results-title">نسخههای قبلی</h3> |
| <div id="previous-results-wrapper"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
| |
| <div id="restart-section" style="display: none; text-align: center; margin-top: 2rem;"> |
| <button id="btnRestart" class="video-button"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><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"/></svg> |
| شروع یک پروژه جدید |
| </button> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <div class="modal-overlay" id="duration-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></div> |
| <h3 class="modal-title">راهنمای مدت زمان</h3> |
| <p class="modal-text">گاهی به دلیل شلوغی سرور، ساخت ویدیوهای طولانی با محدودیت مواجه میشود.<br><br>اگر با خطا مواجه شدید، لطفا مدت زمان را کاهش داده و دوباره امتحان کنید.</p> |
| <div class="modal-buttons"><button class="modal-button" id="close-duration-modal">متوجه شدم</button></div> |
| </div> |
| </div> |
| |
| <div class="modal-overlay" id="regenerate-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/></svg></div> |
| <h3 class="modal-title">تولید مجدد کلیپ آخر؟</h3> |
| <p class="modal-text">اگر آخرین خروجی باب میل شما نبود میتوانید مجدداً ثانیههای آخر که ساخته شده را دوباره تولید کنید.<br>و یا اگر فکر میکردید فریم آخر سیاه انتخاب شده، میتوانید فریم را دستی انتخاب کنید.</p> |
| <div class="modal-buttons"><button class="modal-button secondary" id="cancel-regenerate-btn">انصراف</button><button class="modal-button" id="confirm-regenerate-btn">تایید و ساخت مجدد</button></div> |
| <p class="modal-footer-text"><a href="#" id="select-frame-btn" style="color: var(--accent-primary); font-weight: 600; text-decoration: none;">انتخاب فریم آخر به صورت دستی</a></p> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="frame-selector-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><circle cx="8.5" cy="8.5" r="1.5"></circle><polyline points="21 15 16 10 5 21"></polyline></svg></div> |
| <h3 class="modal-title">انتخاب فریم برای ادامه</h3> |
| <p class="modal-text">بهترین حالت خودکار 0.05 ثانیه است. اگر این فریم یک تصویر سیاه بود، گزینههای دیگر را انتخاب کنید.</p> |
| <div id="frame-loader"></div> |
| <div id="frame-previews"></div> |
| </div> |
| </div> |
| |
| <div class="modal-overlay" id="extend-guide-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg></div> |
| <h3 class="modal-title">راهنمای طولانی کردن ویدیو</h3> |
| <p class="modal-text"> |
| شما میتوانید ویدیوی خود را به صورت نامحدود و مرحله به مرحله طولانیتر کنید.<br><br> |
| <b>مثل یک کارگردان عمل کنید.</b> |
| با هر بار طولانی کردن امکان اضافه کردن زمان به ویدیو وجود داره. شما به عنوان کارگردان میتونید هر چند ثانیه از ویدیو رو بصورت دلخواه توصیف کنید تا بر اساس نیاز شما دقیقا به ویدیو اضافه بشه.<br><br> |
| <b>چگونه کار میکند؟</b> |
| فریم آخر ویدیوی فعلی به عنوان تصویر شروع کلیپ بعدی استفاده میشود. سپس ویدیوی جدید با قبلی میکس شده و یک کلیپ یکپارچه تحویل میدهد.<br><br> |
| <b>نکته مهم:</b> تمام مراحل پروژه شما در حافظه مرورگر ذخیره میشود. حتی اگر صفحه را ببندید، میتوانید بعدا برگردید و پروژه را از همانجا ادامه دهید. |
| </p> |
| <div class="modal-buttons"><button class="modal-button" id="confirm-guide-btn">اوکی، متوجه شدم. ادامه دادن</button></div> |
| </div> |
| </div> |
|
|
| <div class="modal-overlay" id="report-video-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line></svg></div> |
| <h3 class="modal-title">گزارش ویدیو</h3> |
| <div id="report-form-content"> |
| <p class="modal-text">لطفا دلیل گزارش خود را انتخاب یا وارد کنید.</p> |
| <div class="report-options-container"> |
| <button class="report-option-btn" data-reason="محتوای نامناسب">محتوای نامناسب</button> |
| <button class="report-option-btn" data-reason="محتوای نادرست">محتوای نادرست</button> |
| <button class="report-option-btn" id="report-other-btn">دیگر</button> |
| </div> |
| <div id="custom-report-section"> |
| <textarea id="custom-report-text" placeholder="توضیحات بیشتر..."></textarea> |
| <div id="emoji-container"> |
| <span class="emoji">😊</span><span class="emoji">😡</span><span class="emoji">😢</span> |
| </div> |
| <div class="modal-buttons" style="margin-top: 1.5rem;"> |
| <button class="modal-button" id="send-report-btn">ارسال گزارش</button> |
| </div> |
| </div> |
| </div> |
| <div id="report-success-message"> |
| <p>گزارش شما با موفقیت ثبت شد. از همکاری شما متشکریم.</p> |
| </div> |
| </div> |
| </div> |
| |
| <div class="modal-overlay" id="confirm-reset-modal"> |
| <div class="modal-content"> |
| <div class="modal-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg></div> |
| <h3 class="modal-title">آیا مطمئن هستید؟</h3> |
| <p class="modal-text">با این کار تمام نتایج فعلی حذف شده و پروژه از ابتدا شروع میشود.</p> |
| <div class="modal-buttons"> |
| <button class="modal-button secondary" id="toast-cancel-btn">انصراف</button> |
| <button class="modal-button" id="toast-confirm-btn">تایید و حذف</button> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const projectDB = { |
| db: null, |
| init() { return new Promise((resolve, reject) => { const request = indexedDB.open('AIStudioDB_V2', 1); request.onupgradeneeded = event => { const db = event.target.result; if (!db.objectStoreNames.contains('projectState')) { db.createObjectStore('projectState'); } }; request.onsuccess = event => { this.db = event.target.result; resolve(); }; request.onerror = event => { console.error('IndexedDB error:', event.target.errorCode); reject(event.target.errorCode); }; }); }, |
| save(key, value) { return new Promise((resolve, reject) => { if (!this.db) return reject("DB not initialized"); const transaction = this.db.transaction(['projectState'], 'readwrite'); const store = transaction.objectStore('projectState'); const request = store.put(value, key); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); }, |
| get(key) { return new Promise((resolve, reject) => { if (!this.db) return reject("DB not initialized"); const transaction = this.db.transaction(['projectState'], 'readonly'); const store = transaction.objectStore('projectState'); const request = store.get(key); request.onsuccess = () => resolve(request.result); request.onerror = (event) => reject(event.target.error); }); }, |
| delete(key) { return new Promise((resolve, reject) => { if (!this.db) return reject("DB not initialized"); const transaction = this.db.transaction(['projectState'], 'readwrite'); const store = transaction.objectStore('projectState'); const request = store.delete(key); request.onsuccess = () => resolve(); request.onerror = (event) => reject(event.target.error); }); } |
| }; |
| |
| |
| let userSubscriptionStatus = 'free', userFingerprint = null, countdownInterval = null; const PREMIUM_PAGE_ID = '1149636'; |
| async function getBrowserFingerprint() { const components = [navigator.userAgent, navigator.language, screen.width + 'x' + screen.height, new Date().getTimezoneOffset(), "canvas-test"]; const fingerprintString = components.join('---'); let hash = 0; for (let i = 0; i < fingerprintString.length; i++) { hash = ((hash << 5) - hash) + fingerprintString.charCodeAt(i); hash |= 0; } return 'fp_' + Math.abs(hash).toString(16); } |
| function isUserPaid(userObject) { return userObject?.isLogin && userObject.accessible_pages?.some(p => p == PREMIUM_PAGE_ID); } |
| function updateUIForSubscriptionStatus(status) { |
| userSubscriptionStatus = status; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); const subscriptionBadge = document.getElementById('subscription-status-badge'); |
| if (status === 'paid') { |
| subscriptionBadge.textContent = 'نسخه نامحدود'; subscriptionBadge.className = 'paid-badge'; creditInfoDiv.style.display = 'none'; upgradeBtn.style.display = 'none'; genBtn.disabled = false; if(countdownInterval) clearInterval(countdownInterval); |
| } else { |
| subscriptionBadge.textContent = 'نسخه رایگان'; subscriptionBadge.className = 'free-badge'; checkFreeUserCredit(); |
| } |
| subscriptionBadge.style.display = 'inline-block'; |
| } |
| async function checkFreeUserCredit() { |
| if (!userFingerprint) return; const creditInfoDiv = document.getElementById('credit-info-section'); const upgradeBtn = document.getElementById('upgrade-button'); const genBtn = document.getElementById('generateButton'); |
| try { |
| const response = await fetch('/api/check-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); |
| const result = await response.json(); |
| if (userSubscriptionStatus !== 'paid') { creditInfoDiv.style.display = 'block'; } |
| if (result.limit_reached) { |
| genBtn.disabled = true; upgradeBtn.style.display = 'block'; startCountdown(result.reset_timestamp); |
| } else { |
| genBtn.disabled = false; upgradeBtn.style.display = 'none'; if(countdownInterval) clearInterval(countdownInterval); creditInfoDiv.textContent = `شما ${result.credits_remaining} اعتبار ساخت ویدیوی رایگان در این هفته دارید.`; |
| } |
| } catch (error) { creditInfoDiv.textContent = "خطا در بررسی اعتبار."; genBtn.disabled = true; } |
| } |
| function startCountdown(resetTimestamp) { |
| if (countdownInterval) clearInterval(countdownInterval); const creditInfoDiv = document.getElementById('credit-info-section'); |
| const updateTimer = () => { |
| const timeLeft = Math.max(0, resetTimestamp - (Date.now() / 1000)); |
| if (timeLeft === 0) { clearInterval(countdownInterval); creditInfoDiv.textContent = 'در حال بروزرسانی اعتبار...'; setTimeout(checkFreeUserCredit, 2000); return; } |
| const d = Math.floor(timeLeft / 86400); const h = Math.floor((timeLeft % 86400) / 3600); const m = Math.floor((timeLeft % 3600) / 60); |
| creditInfoDiv.textContent = `اعتبار شما تمام شده. زمان باقیمانده: ${d} روز و ${h} ساعت و ${m} دقیقه`; |
| }; |
| updateTimer(); countdownInterval = setInterval(updateTimer, 60000); |
| } |
| window.addEventListener('message', (event) => { |
| if (event.data?.type === 'USER_DATA_RESPONSE') { |
| if (event.data.error || !event.data.payload) { updateUIForSubscriptionStatus('free'); return; } |
| try { const userObject = JSON.parse(event.data.payload); updateUIForSubscriptionStatus(isUserPaid(userObject) ? 'paid' : 'free'); } |
| catch (e) { updateUIForSubscriptionStatus('free'); } |
| } |
| }); |
| document.getElementById('upgrade-button').addEventListener('click', () => { parent.postMessage({ type: 'NAVIGATE_TO_PREMIUM' }, '*'); }); |
| |
| |
| const mainControls = document.getElementById('main-controls'); |
| const resultsContainer = document.getElementById('results-container'); |
| const latestResultWrapper = document.getElementById('latest-result-wrapper'); |
| const previousResultsContainer = document.getElementById('previous-results-container'); |
| const previousResultsWrapper = document.getElementById('previous-results-wrapper'); |
| const restartSection = document.getElementById('restart-section'); |
| const imageFileInput = document.getElementById('imageFile'); |
| const imagePreview = document.getElementById('imagePreview'); |
| const imageDropZone = document.getElementById('image-drop-zone'); |
| const removeImageBtn = document.getElementById('remove-image-btn'); |
| const promptInput = document.getElementById('prompt'); |
| const promptGuideText = document.getElementById('prompt-guide-text'); |
| const generateButton = document.getElementById('generateButton'); |
| const generateButtonText = generateButton.querySelector('span'); |
| const criticalErrorSection = document.getElementById('criticalErrorSection'); |
| const statusSection = document.getElementById('statusSection'); |
| const statusMessagesDiv = document.getElementById('statusMessages'); |
| const aiLoader = document.getElementById('aiLoader'); |
| const loaderTextOverlay = document.querySelector('#aiLoader .text-overlay'); |
| const tvProgressBar = document.getElementById('tv-progress-bar'); |
| const btnRestart = document.getElementById('btnRestart'); |
| const durationSlider = document.getElementById('durationSlider'); |
| const durationValueDisplay = document.getElementById('durationValue'); |
| const durationInfoIcon = document.getElementById('duration-info-icon'); |
| const durationModal = document.getElementById('duration-modal'); |
| const durationLabel = document.getElementById('duration-label'); |
| const gallerySeparator = document.getElementById('gallery-separator'); |
| const closeDurationModalBtn = document.getElementById('close-duration-modal'); |
| const confirmResetModal = document.getElementById('confirm-reset-modal'); |
| const toastConfirmBtn = document.getElementById('toast-confirm-btn'); |
| const toastCancelBtn = document.getElementById('toast-cancel-btn'); |
| const regenerateModal = document.getElementById('regenerate-modal'); |
| const confirmRegenerateBtn = document.getElementById('confirm-regenerate-btn'); |
| const cancelRegenerateBtn = document.getElementById('cancel-regenerate-btn'); |
| const selectFrameBtn = document.getElementById('select-frame-btn'); |
| const frameSelectorModal = document.getElementById('frame-selector-modal'); |
| const frameLoader = document.getElementById('frame-loader'); |
| const framePreviews = document.getElementById('frame-previews'); |
| const extendGuideModal = document.getElementById('extend-guide-modal'); |
| const confirmGuideBtn = document.getElementById('confirm-guide-btn'); |
| const reportVideoModal = document.getElementById('report-video-modal'); |
| const reportFormContent = document.getElementById('report-form-content'); |
| const customReportSection = document.getElementById('custom-report-section'); |
| const reportSuccessMessage = document.getElementById('report-success-message'); |
| const projectWarningBox = document.getElementById('project-warning-message'); |
| |
| |
| let currentImageFile = null, currentImageObjectURL = null, lastAttemptedPayload = null, pollTimer = null; |
| let videoBlobs = new Map(); |
| let originalUserPromptForExtension = ''; |
| let extensionJob = null; |
| let manualFrameBlob = null; |
| let hasSeenExtendGuide = false; |
| let currentReportingVideoId = null; |
| let fakeProgressInterval = null; |
| |
| function updateSliderStyle(slider) { const percentage = ((slider.value - slider.min) / (slider.max - slider.min)) * 100; const colorStops = `var(--accent-primary), var(--accent-secondary)`; slider.style.background = `linear-gradient(to left, ${colorStops} ${percentage}%, var(--input-bg) ${percentage}%)`; } |
| function setGenerateButtonState(text, disabled) { generateButtonText.textContent = text; generateButton.disabled = disabled; } |
| |
| function setUiLock(locked) { |
| generateButton.disabled = locked; |
| imageFileInput.disabled = locked; |
| const allExtendButtons = document.querySelectorAll('.video-button.secondary'); |
| allExtendButtons.forEach(btn => { btn.disabled = locked; }); |
| } |
| |
| function setAiLoaderText(text) { if (loaderTextOverlay) loaderTextOverlay.textContent = text; } |
| |
| |
| function startFakeProgress() { |
| clearInterval(fakeProgressInterval); |
| let currentProg = 0; |
| const targetSeconds = 150; |
| const intervalMs = 200; |
| const increment = 100 / (targetSeconds * 1000 / intervalMs); |
| |
| tvProgressBar.style.width = '0%'; |
| fakeProgressInterval = setInterval(() => { |
| currentProg += increment; |
| if(currentProg > 98) { |
| clearInterval(fakeProgressInterval); |
| currentProg = 98; |
| } |
| tvProgressBar.style.width = `${currentProg}%`; |
| }, intervalMs); |
| } |
| |
| function finishFakeProgress() { |
| clearInterval(fakeProgressInterval); |
| tvProgressBar.style.width = '100%'; |
| } |
| |
| function addStatusMessage(message, type = 'info') { |
| if (criticalErrorSection.classList.contains('active') && type === 'error') return; |
| statusSection.classList.add('active'); |
| const messageDiv = document.createElement('div'); |
| messageDiv.className = `status-message ${type}`; |
| const iconSvg = type === 'success' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/></svg>' : type === 'error' ? '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>' : '<svg class="status-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>'; |
| messageDiv.innerHTML = `${iconSvg}<span class="status-text">${message}</span>`; |
| statusMessagesDiv.insertBefore(messageDiv, statusMessagesDiv.firstChild); |
| } |
| |
| function clearStatusMessages() { |
| statusMessagesDiv.innerHTML = ''; |
| clearInterval(fakeProgressInterval); |
| tvProgressBar.style.width = '0%'; |
| aiLoader.style.display = 'none'; |
| } |
| |
| function showCriticalError(message) { |
| const allExtendButtons = document.querySelectorAll('.video-button.secondary'); |
| allExtendButtons.forEach(btn => { btn.disabled = true; }); |
| if (latestResultWrapper.children.length > 0) { gallerySeparator.style.display = 'block'; } |
| extensionJob = null; |
| if (pollTimer) clearInterval(pollTimer); |
| clearInterval(fakeProgressInterval); |
| |
| criticalErrorSection.innerHTML = `<div style="text-align: center; color: var(--danger-color); font-weight: 500; margin-bottom: 1.5rem; line-height: 1.6;"><p>${message}</p></div><div class="video-controls"><button id="simpleErrorGoBackBtn" class="video-button">بازگشت</button><button id="simpleErrorRetryBtn" class="video-button primary">تلاش مجدد</button></div>`; |
| document.getElementById('simpleErrorGoBackBtn').addEventListener('click', hideCriticalError); |
| document.getElementById('simpleErrorRetryBtn').addEventListener('click', retryLastAttempt); |
| |
| statusSection.classList.remove('active'); |
| criticalErrorSection.classList.add('active'); |
| aiLoader.style.display = 'none'; |
| generateButton.disabled = true; |
| } |
| |
| function hideCriticalError() { |
| criticalErrorSection.classList.remove('active'); |
| criticalErrorSection.innerHTML = ''; |
| gallerySeparator.style.display = 'none'; |
| setGenerateButtonState("ساخت ویدیو جادویی", false); |
| setUiLock(false); |
| if (extensionJob?.baseVideoId) { |
| const latestCard = document.getElementById(extensionJob.baseVideoId); |
| if (latestCard) { |
| const extendBtn = latestCard.querySelector('.video-button.secondary'); |
| if (extendBtn) extendBtn.disabled = false; |
| } |
| } |
| } |
| |
| async function retryLastAttempt() { |
| if (!lastAttemptedPayload) { showCriticalError("اطلاعات کافی برای تلاش مجدد وجود ندارد."); return; } |
| hideCriticalError(); |
| clearStatusMessages(); |
| setUiLock(true); |
| if (latestResultWrapper.children.length > 0) { gallerySeparator.style.display = 'block'; } |
| statusSection.classList.add('active'); |
| aiLoader.style.display = 'flex'; |
| startFakeProgress(); |
| await proceedWithVideoGeneration(); |
| } |
| |
| async function initializeForm() { |
| if(pollTimer) clearInterval(pollTimer); |
| clearInterval(fakeProgressInterval); |
| hideCriticalError(); |
| projectWarningBox.style.display = 'none'; |
| latestResultWrapper.innerHTML = ''; previousResultsWrapper.innerHTML = ''; previousResultsContainer.style.display = 'none'; |
| videoBlobs.forEach(data => URL.revokeObjectURL(data.objectURL)); videoBlobs.clear(); |
| lastAttemptedPayload = null; extensionJob = null; manualFrameBlob = null; |
| hasSeenExtendGuide = false; |
| gallerySeparator.style.display = 'none'; |
| imageFileInput.value = ''; |
| if (currentImageObjectURL) { URL.revokeObjectURL(currentImageObjectURL); } |
| currentImageObjectURL = null; imagePreview.src = ''; |
| imageDropZone.classList.remove('has-image'); currentImageFile = null; |
| promptInput.value = ''; restartSection.style.display = 'none'; |
| setUiLock(false); setGenerateButtonState("ساخت ویدیو جادویی", false); |
| durationLabel.innerHTML = 'مدت زمان<span class="info-icon" id="duration-info-icon">!</span>'; |
| durationLabel.classList.remove('small-text'); |
| promptGuideText.style.display = 'none'; |
| updateSliderStyle(durationSlider); |
| } |
| |
| function createVideoResultCard(videoId, blob, infoText, remoteUrl) { |
| const objectURL = URL.createObjectURL(blob); |
| videoBlobs.set(videoId, { blob, objectURL, infoText, remoteUrl }); |
| const card = document.createElement('div'); |
| card.className = 'video-result-card'; |
| card.id = videoId; |
| const video = document.createElement('video'); |
| video.src = objectURL; |
| video.controls = true; video.playsInline = true; video.loop = true; video.autoplay = true; video.muted = true; |
| |
| const info = document.createElement('p'); |
| info.className = 'video-info'; |
| const infoSpan = document.createElement('span'); |
| infoSpan.textContent = infoText; |
| info.appendChild(infoSpan); |
| |
| const reportIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
| reportIcon.setAttribute('class', 'report-flag-icon'); |
| reportIcon.setAttribute('viewBox', '0 0 24 24'); |
| reportIcon.setAttribute('fill', 'none'); |
| reportIcon.setAttribute('stroke', 'currentColor'); |
| reportIcon.setAttribute('stroke-width', '2'); |
| reportIcon.setAttribute('stroke-linecap', 'round'); |
| reportIcon.setAttribute('stroke-linejoin', 'round'); |
| reportIcon.setAttribute('title', 'گزارش این ویدیو'); |
| reportIcon.innerHTML = '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"></path><line x1="4" y1="22" x2="4" y2="15"></line>'; |
| reportIcon.dataset.videoId = videoId; |
| reportIcon.addEventListener('click', handleReportClick); |
| |
| const textNode = infoSpan.childNodes[0]; |
| if (textNode && textNode.nodeValue && textNode.nodeValue.includes('تمدید شده')) { |
| const mainText = textNode.nodeValue.match(/ویدیوی تمدید شده/)[0]; |
| const restText = textNode.nodeValue.substring(mainText.length); |
| infoSpan.textContent = mainText; |
| infoSpan.insertAdjacentElement('afterend', reportIcon); |
| const restSpan = document.createElement('span'); |
| restSpan.textContent = restText; |
| reportIcon.insertAdjacentElement('afterend', restSpan); |
| } else { |
| info.appendChild(reportIcon); |
| } |
| |
| const controlsDiv = document.createElement('div'); |
| controlsDiv.className = 'video-controls'; |
| |
| const btnDownload = document.createElement('button'); |
| btnDownload.className = 'video-button primary'; |
| btnDownload.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/></svg> دانلود ویدیو`; |
| btnDownload.addEventListener('click', (e) => { |
| e.preventDefault(); |
| const vData = videoBlobs.get(videoId); |
| const targetUrl = vData.remoteUrl ? vData.remoteUrl : vData.objectURL; |
| parent.postMessage({ |
| type: 'DOWNLOAD_REQUEST', |
| url: targetUrl |
| }, '*'); |
| }); |
| |
| const btnExtend = document.createElement('button'); |
| btnExtend.className = 'video-button secondary'; |
| btnExtend.dataset.videoId = videoId; |
| btnExtend.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M9 21H3v-6"/><path d="M21 3l-7 7"/><path d="M3 21l7-7"/></svg> طولانی کردن ویدیو`; |
| btnExtend.addEventListener('click', handleExtendClick); |
| |
| controlsDiv.appendChild(btnDownload); |
| controlsDiv.appendChild(btnExtend); |
| card.appendChild(video); |
| card.appendChild(info); |
| card.appendChild(controlsDiv); |
| return card; |
| } |
| |
| function handleExtendClick(event) { |
| const extendButton = event.currentTarget; |
| if (extendButton.disabled) return; |
| |
| const videoId = extendButton.dataset.videoId; |
| const baseVideoData = videoBlobs.get(videoId); |
| if (!baseVideoData) { showCriticalError("خطا: ویدیوی اصلی برای تمدید یافت نشد."); return; } |
| |
| if (!hasSeenExtendGuide && !baseVideoData.infoText.includes('تمدید شده')) { |
| extendGuideModal.classList.add('active'); |
| } else { |
| prepareForExtension(videoId, baseVideoData); |
| } |
| } |
| |
| function prepareForExtension(videoId, baseVideoData) { |
| setUiLock(true); |
| aiLoader.style.display = 'none'; |
| clearStatusMessages(); |
| projectWarningBox.style.display = 'none'; |
| |
| let currentClipCount = 1; |
| const match = baseVideoData.infoText.match(/ترکیب (\d+)/); |
| if (match && match[1]) { currentClipCount = parseInt(match[1], 10); } |
| |
| extensionJob = { baseVideoId: videoId, baseVideoBlob: baseVideoData.blob, clipCount: currentClipCount }; |
| originalUserPromptForExtension = promptInput.value.trim(); |
| setGenerateButtonState("ادامه ویدیو را بساز", false); |
| generateButton.disabled = false; |
| promptInput.disabled = false; |
| durationLabel.textContent = 'به ادامه ویدیو چند ثانیه اضافه بشه؟'; |
| durationLabel.classList.add('small-text'); |
| promptGuideText.style.display = 'block'; |
| addStatusMessage("برای ادامه، دستور جدید را وارد کرده و روی دکمه 'ادامه ویدیو را بساز' کلیک کنید.", 'info'); |
| generateButton.scrollIntoView({ behavior: 'smooth', block: 'center' }); |
| } |
| |
| async function handleRegenerateClick() { |
| regenerateModal.classList.remove('active'); |
| const latestCard = latestResultWrapper.querySelector('.video-result-card.latest'); |
| if (!latestCard) return; |
| |
| const videoIdToRemove = latestCard.id; |
| videoBlobs.delete(videoIdToRemove); |
| latestResultWrapper.innerHTML = ''; |
| |
| const lastPreviousCard = previousResultsWrapper.firstElementChild; |
| if (lastPreviousCard) { |
| previousResultsWrapper.removeChild(lastPreviousCard); |
| lastPreviousCard.classList.add('latest'); |
| const extendBtn = lastPreviousCard.querySelector('.video-button.secondary'); |
| if (extendBtn) extendBtn.style.display = 'inline-flex'; |
| latestResultWrapper.appendChild(lastPreviousCard); |
| |
| if(lastPreviousCard.textContent.includes('تمدید شده')) { |
| const infoElement = lastPreviousCard.querySelector('.video-info'); |
| const regenerateIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
| regenerateIcon.setAttribute('class', 'regenerate-icon'); |
| regenerateIcon.setAttribute('viewBox', '0 0 24 24'); |
| regenerateIcon.setAttribute('fill', 'none'); |
| regenerateIcon.setAttribute('stroke', 'currentColor'); |
| regenerateIcon.setAttribute('stroke-width', '2'); |
| regenerateIcon.setAttribute('stroke-linecap', 'round'); |
| regenerateIcon.setAttribute('stroke-linejoin', 'round'); |
| regenerateIcon.setAttribute('title', 'تولید مجدد کلیپ آخر'); |
| regenerateIcon.innerHTML = `<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>`; |
| regenerateIcon.addEventListener('click', (e) => { e.stopPropagation(); regenerateModal.classList.add('active'); }); |
| infoElement.appendChild(regenerateIcon); |
| } |
| |
| if (previousResultsWrapper.children.length === 0) { |
| previousResultsContainer.style.display = 'none'; |
| } |
| |
| const newLatestExtendBtn = lastPreviousCard.querySelector('.video-button.secondary'); |
| if (newLatestExtendBtn) { newLatestExtendBtn.click(); } |
| } |
| } |
| |
| async function mergeVideosOnServer(baseVideoBlob, newClipBlob) { |
| try { |
| setAiLoaderText("میکس ویدیوها در سرور ابری..."); addStatusMessage('ارسال ویدیوها برای میکس در سرور...', 'info'); |
| |
| const formData = new FormData(); |
| formData.append('base_video', baseVideoBlob, 'base.mp4'); |
| formData.append('new_clip', newClipBlob, 'new.mp4'); |
| |
| const response = await fetch('/api/merge-videos', { method: 'POST', body: formData }); |
| |
| if (response.ok) { |
| return await response.blob(); |
| } else { |
| const errText = await response.text(); |
| throw new Error(errText); |
| } |
| } catch (error) { |
| showCriticalError(`خطا در میکس ویدیو در سرور: ${error.message}`); |
| return null; |
| } |
| } |
| |
| async function handleVideoResult(newClipBlob, runId, remoteUrl) { |
| try { |
| let finalBlob, infoText; |
| const isExtensionAttempt = lastAttemptedPayload.isExtension || !!lastAttemptedPayload.baseVideoBlob; |
| |
| if (isExtensionAttempt) { |
| hasSeenExtendGuide = true; |
| const baseVideoToMerge = extensionJob?.baseVideoBlob || lastAttemptedPayload.baseVideoBlob; |
| const clipCount = extensionJob?.clipCount || 1; |
| const mergedBlob = await mergeVideosOnServer(baseVideoToMerge, newClipBlob); |
| if (!mergedBlob) return; |
| finalBlob = mergedBlob; |
| |
| remoteUrl = null; |
| |
| infoText = `ویدیوی تمدید شده (ترکیب ${clipCount + 1} کلیپ)`; |
| } else { |
| finalBlob = newClipBlob; |
| infoText = `ویدیو اولیه`; |
| |
| if (userSubscriptionStatus === 'free') { |
| addStatusMessage('ویدیو ساخته شد، در حال ثبت اعتبار...', 'info'); |
| try { await fetch('/api/use-credit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ fingerprint: userFingerprint }) }); checkFreeUserCredit(); } |
| catch (creditError) { console.error("Error deducting credit:", creditError); } |
| } |
| } |
| |
| const existingLatestCard = latestResultWrapper.querySelector('.video-result-card'); |
| if (existingLatestCard) { |
| existingLatestCard.classList.remove('latest'); |
| const oldRegenerateIcon = existingLatestCard.querySelector('.regenerate-icon'); |
| if(oldRegenerateIcon) oldRegenerateIcon.remove(); |
| const extendBtn = existingLatestCard.querySelector('.video-button.secondary'); |
| if (extendBtn) extendBtn.style.display = 'none'; |
| previousResultsWrapper.prepend(existingLatestCard); |
| previousResultsContainer.style.display = 'block'; |
| } |
| |
| const newVideoId = `video-result-${Date.now()}`; |
| const videoCard = createVideoResultCard(newVideoId, finalBlob, infoText, remoteUrl); |
| videoCard.classList.add('latest'); |
| |
| if (isExtensionAttempt) { |
| const infoElement = videoCard.querySelector('.video-info'); |
| const regenerateIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
| regenerateIcon.setAttribute('class', 'regenerate-icon'); |
| regenerateIcon.setAttribute('viewBox', '0 0 24 24'); |
| regenerateIcon.setAttribute('fill', 'none'); |
| regenerateIcon.setAttribute('stroke', 'currentColor'); |
| regenerateIcon.setAttribute('stroke-width', '2'); |
| regenerateIcon.setAttribute('stroke-linecap', 'round'); |
| regenerateIcon.setAttribute('stroke-linejoin', 'round'); |
| regenerateIcon.setAttribute('title', 'تولید مجدد کلیپ آخر'); |
| regenerateIcon.innerHTML = `<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>`; |
| regenerateIcon.addEventListener('click', (e) => { e.stopPropagation(); regenerateModal.classList.add('active'); }); |
| infoElement.appendChild(regenerateIcon); |
| } |
| |
| latestResultWrapper.innerHTML = ''; |
| latestResultWrapper.appendChild(videoCard); |
| |
| await projectDB.save('galleryState', { latestVideoId: newVideoId, videos: Array.from(videoBlobs.entries()).map(([id, data]) => ({id, blob: data.blob, infoText: data.infoText, remoteUrl: data.remoteUrl})) }).catch(err => console.error("Failed to save gallery state:", err)); |
| |
| addStatusMessage('ویدیو شما آماده شد! 🎉', 'success'); |
| finishFakeProgress(); |
| |
| setTimeout(() => { |
| gallerySeparator.style.display = 'none'; |
| statusSection.classList.remove('active'); |
| aiLoader.style.display = 'none'; |
| restartSection.style.display = 'block'; |
| setUiLock(false); |
| setGenerateButtonState("ساخت ویدیو جادویی", false); |
| durationLabel.innerHTML = 'مدت زمان<span class="info-icon" id="duration-info-icon">!</span>'; |
| durationLabel.classList.remove('small-text'); |
| promptGuideText.style.display = 'none'; |
| extensionJob = null; |
| }, 1000); |
| |
| } catch (error) { |
| showCriticalError("خطا در پردازش فایل ویدیو. لطفاً مجدداً تلاش کنید."); |
| } |
| } |
| |
| |
| const proceedWithVideoGeneration = async () => { |
| const { imageFile, userPrompt, duration, isExtension } = lastAttemptedPayload; |
| |
| try { |
| setAiLoaderText('۱/۳: آپلود تصویر و بهینه سازی دستور...'); |
| |
| const formData = new FormData(); |
| formData.append('image', imageFile, 'image.png'); |
| formData.append('prompt', userPrompt); |
| formData.append('duration', duration); |
| |
| addStatusMessage('در حال ارسال درخواست به سرور...', 'info'); |
| |
| const response = await fetch('/api/generate-video', { method: 'POST', body: formData }); |
| const data = await response.json(); |
| |
| if (data.status === 'success') { |
| if(data.translated) { |
| addStatusMessage(`دستور شما ترجمه شد: ${data.translated}`, 'success'); |
| } |
| setAiLoaderText('۲/۳: در حال ساخت ویدیو در سرور ابری قدرتمند هوش مصنوعی...'); |
| |
| pollTimer = setInterval(async () => { |
| try { |
| const statusRes = await fetch(`/api/status/${data.run_id}`); |
| const statusData = await statusRes.json(); |
| |
| if (statusData.status === 'ready') { |
| clearInterval(pollTimer); |
| setAiLoaderText('۳/۳: ویدیو آماده شد، در حال دریافت...'); |
| addStatusMessage('ویدیو با موفقیت ساخته شد، در حال دریافت...', 'success'); |
| |
| const vidRes = await fetch(statusData.url); |
| const newClipBlob = await vidRes.blob(); |
| const absoluteRemoteUrl = window.location.origin + statusData.url; |
| |
| await handleVideoResult(newClipBlob, data.run_id, absoluteRemoteUrl); |
| } |
| } catch (pollErr) { |
| console.error('Polling error:', pollErr); |
| } |
| }, 5000); |
| } else { |
| throw new Error(data.message || 'خطا در برقراری ارتباط با سرور.'); |
| } |
| } catch (error) { |
| showCriticalError(`خطا: ${error.message}`); |
| } |
| }; |
| |
| async function startGenerationProcess() { |
| let imageToProcess, promptToUse, isExtensionRequest, baseVideoBlob, clipCount; |
| |
| promptToUse = promptInput.value.trim(); |
| const promptToUseLower = promptToUse.toLowerCase(); |
| const forbiddenWords = ["sex", "sexy", "porn", "nude", "erotic", "سکس", "سکسی", "پورن", "شهوانی", "برهنه", "لخت"]; |
| |
| if (forbiddenWords.some(word => promptToUseLower.includes(word))) { |
| showCriticalError("متن ورودی شما حاوی کلمات نامناسب است. لطفاً آن را اصلاح کرده و مجدداً تلاش کنید."); |
| setUiLock(false); |
| statusSection.classList.remove('active'); |
| aiLoader.style.display = 'none'; |
| return; |
| } |
| |
| if (extensionJob) { |
| isExtensionRequest = true; |
| if (manualFrameBlob) { |
| imageToProcess = manualFrameBlob; |
| manualFrameBlob = null; |
| } else { |
| const videoElement = document.querySelector(`#${extensionJob.baseVideoId} video`); |
| if (!videoElement) { showCriticalError("ویدیوی مرجع برای گرفتن فریم آخر پیدا نشد!"); return; } |
| addStatusMessage('در حال گرفتن آخرین فریم از ویدیو...', 'info'); |
| try { |
| const rawBlob = await captureLastFrame(videoElement); |
| imageToProcess = new File([rawBlob], "last_frame.png", { type: "image/png" }); |
| } catch (e) { |
| showCriticalError(`خطا در گرفتن فریم آخر ویدیو: ${e.message}`); |
| setUiLock(false); return; |
| } |
| } |
| promptToUse = promptToUse || originalUserPromptForExtension; |
| baseVideoBlob = extensionJob.baseVideoBlob; |
| clipCount = extensionJob.clipCount; |
| } else { |
| isExtensionRequest = false; |
| if (!currentImageFile) { |
| addStatusMessage('لطفاً یک تصویر انتخاب کنید.', 'error'); |
| setUiLock(false); |
| statusSection.classList.remove('active'); |
| aiLoader.style.display = 'none'; |
| return; |
| } |
| imageToProcess = currentImageFile; |
| } |
| |
| lastAttemptedPayload = { imageFile: imageToProcess, userPrompt: promptToUse, duration: parseFloat(durationSlider.value), isExtension: isExtensionRequest, baseVideoBlob, clipCount }; |
| |
| if (userSubscriptionStatus !== 'paid') { |
| try { |
| const response = await fetch('/api/check-credit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ fingerprint: userFingerprint }) }); |
| const result = await response.json(); |
| if (result.limit_reached) { |
| updateUIForSubscriptionStatus('free'); |
| showCriticalError("محدودیت ساخت ویدیوی رایگان شما برای این هفته به پایان رسیده است."); |
| return; |
| } |
| } catch (err) { |
| const errorMessage = !navigator.onLine ? "عدم اتصال به اینترنت." : "خطا در سیستم اعتبار."; |
| showCriticalError(errorMessage); |
| return; |
| } |
| } |
| |
| await proceedWithVideoGeneration(); |
| } |
| |
| generateButton.addEventListener('click', async () => { |
| if (generateButton.disabled) return; |
| |
| if (!extensionJob && videoBlobs.size > 0) { |
| projectWarningBox.style.display = 'block'; |
| generateButton.classList.add('shake-animation'); |
| setTimeout(() => { generateButton.classList.remove('shake-animation'); }, 400); |
| return; |
| } |
| projectWarningBox.style.display = 'none'; |
| |
| hideCriticalError(); |
| clearStatusMessages(); |
| setUiLock(true); |
| if (latestResultWrapper.children.length > 0) { gallerySeparator.style.display = 'block'; } |
| statusSection.classList.add('active'); |
| aiLoader.style.display = 'flex'; |
| startFakeProgress(); |
| await startGenerationProcess(); |
| }); |
| |
| async function loadStoredProject() { |
| try { |
| const storedImage = await projectDB.get('userImage'); |
| if (storedImage) { |
| currentImageFile = new File([storedImage], "stored_image.png", { type: storedImage.type }); |
| if (currentImageObjectURL) URL.revokeObjectURL(currentImageObjectURL); |
| currentImageObjectURL = URL.createObjectURL(currentImageFile); |
| |
| imagePreview.onload = () => { imageDropZone.classList.add('has-image'); }; |
| imagePreview.src = currentImageObjectURL; |
| } |
| const galleryState = await projectDB.get('galleryState'); |
| if (galleryState && galleryState.videos.length > 0) { |
| hasSeenExtendGuide = galleryState.videos.length > 1; |
| galleryState.videos.forEach(videoData => { |
| const card = createVideoResultCard(videoData.id, videoData.blob, videoData.infoText, videoData.remoteUrl); |
| if (videoData.id === galleryState.latestVideoId) { |
| card.classList.add('latest'); |
| latestResultWrapper.appendChild(card); |
| |
| const extendBtn = card.querySelector('.video-button.secondary'); |
| if (extendBtn) extendBtn.style.display = 'inline-flex'; |
| |
| if (videoData.infoText.includes('تمدید شده')) { |
| const infoElement = card.querySelector('.video-info'); |
| if (infoElement && !card.querySelector('.regenerate-icon')) { |
| const regenerateIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg"); |
| regenerateIcon.setAttribute('class', 'regenerate-icon'); |
| regenerateIcon.setAttribute('viewBox', '0 0 24 24'); |
| regenerateIcon.setAttribute('fill', 'none'); |
| regenerateIcon.setAttribute('stroke', 'currentColor'); |
| regenerateIcon.setAttribute('stroke-width', '2'); |
| regenerateIcon.setAttribute('stroke-linecap', 'round'); |
| regenerateIcon.setAttribute('stroke-linejoin', 'round'); |
| regenerateIcon.setAttribute('title', 'تولید مجدد کلیپ آخر'); |
| regenerateIcon.innerHTML = `<path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"/><path d="M21 3v5h-5"/><path d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"/><path d="M3 21v-5h5"/>`; |
| regenerateIcon.addEventListener('click', (e) => { e.stopPropagation(); regenerateModal.classList.add('active'); }); |
| infoElement.appendChild(regenerateIcon); |
| } |
| } |
| } else { |
| const extendBtn = card.querySelector('.video-button.secondary'); |
| if (extendBtn) extendBtn.style.display = 'none'; |
| previousResultsWrapper.prepend(card); |
| } |
| }); |
| |
| if (previousResultsWrapper.children.length > 0) { |
| previousResultsContainer.style.display = 'block'; |
| } |
| restartSection.style.display = 'block'; |
| } |
| } catch(error) { console.error("Could not load project from DB:", error); } |
| } |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| userFingerprint = await getBrowserFingerprint(); |
| parent.postMessage({ type: 'REQUEST_USER_DATA' }, '*'); |
| try { await projectDB.init(); await initializeForm(); await loadStoredProject(); } |
| catch (error) { console.error("Failed to init DB or load project:", error); } |
| }); |
| |
| durationSlider.addEventListener('input', () => { const value = parseFloat(durationSlider.value); durationValueDisplay.textContent = `${value.toFixed(1)} ثانیه`; updateSliderStyle(durationSlider); }); |
| |
| function captureFrameAtTime(videoElement, time) { |
| return new Promise((resolve, reject) => { |
| videoElement.pause(); |
| const onSeeked = () => { |
| videoElement.removeEventListener('seeked', onSeeked); |
| setTimeout(() => { |
| const canvas = document.createElement('canvas'); |
| canvas.width = videoElement.videoWidth; |
| canvas.height = videoElement.videoHeight; |
| if (canvas.width === 0 || canvas.height === 0) { return reject(new Error('ابعاد ویدیو نامعتبر است (صفر).')); } |
| const ctx = canvas.getContext('2d'); |
| try { |
| ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height); |
| canvas.toBlob(blob => { if (blob) resolve(blob); else reject(new Error('تبدیل Canvas به Blob ناموفق بود.')); }, 'image/png'); |
| } catch (e) { reject(new Error(`خطای امنیتی هنگام خواندن فریم ویدیو: ${e.message}`)); } |
| }, 100); |
| }; |
| const startSeeking = () => { videoElement.addEventListener('seeked', onSeeked, { once: true }); videoElement.currentTime = Math.max(0, Math.min(videoElement.duration, time)); }; |
| if (videoElement.readyState >= 1) { startSeeking(); } else { videoElement.addEventListener('loadedmetadata', startSeeking, { once: true }); } |
| }); |
| } |
| |
| function captureLastFrame(videoElement) { return captureFrameAtTime(videoElement, Math.max(0, videoElement.duration - 0.05)); } |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, ev => ev.preventDefault())); |
| ['dragenter', 'dragover'].forEach(e => imageDropZone.addEventListener(e, () => { if (!imageDropZone.classList.contains('has-image')) imageDropZone.classList.add('drag-over'); })); |
| ['dragleave', 'drop'].forEach(e => imageDropZone.addEventListener(e, () => imageDropZone.classList.remove('drag-over'))); |
| imageDropZone.addEventListener('drop', e => { if (e.dataTransfer.files.length > 0) { imageFileInput.files = e.dataTransfer.files; imageFileInput.dispatchEvent(new Event('change')); } }); |
| |
| btnRestart.addEventListener('click', () => confirmResetModal.classList.add('active')); |
| removeImageBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if(latestResultWrapper.children.length > 0 || currentImageFile) { confirmResetModal.classList.add('active'); } else { initializeForm(); } }); |
| toastConfirmBtn.addEventListener('click', async () => { confirmResetModal.classList.remove('active'); await initializeForm(); await Promise.all([projectDB.delete('userImage'), projectDB.delete('galleryState')]).catch(err => console.error(err)); }); |
| toastCancelBtn.addEventListener('click', () => confirmResetModal.classList.remove('active')); |
| confirmResetModal.addEventListener('click', (e) => { if (e.target === confirmResetModal) confirmResetModal.classList.remove('active'); }); |
| |
| imageFileInput.addEventListener('change', async function(event) { |
| const file = event.target.files[0]; |
| if (!file) return; |
| |
| generateButton.disabled = true; |
| hideCriticalError(); |
| projectWarningBox.style.display = 'none'; |
| |
| if (!file.type.startsWith('image/')) { |
| addStatusMessage('فرمت فایل نامعتبر است. لطفاً یک تصویر انتخاب کنید.', 'error'); |
| currentImageFile = null; imageFileInput.value = ''; generateButton.disabled = false; |
| return; |
| } |
| |
| await initializeForm(); |
| await Promise.all([projectDB.delete('userImage'), projectDB.delete('galleryState')]); |
| |
| currentImageFile = file; |
| if (currentImageObjectURL) URL.revokeObjectURL(currentImageObjectURL); |
| currentImageObjectURL = URL.createObjectURL(file); |
| |
| imagePreview.onload = async () => { |
| try { imageDropZone.classList.add('has-image'); await projectDB.save('userImage', currentImageFile); } |
| catch (err) { console.error("خطا در ذخیره فایل جدید:", err); showCriticalError("خطایی در ذخیره فایل جدید رخ داد."); } |
| }; |
| |
| imagePreview.onerror = () => { showCriticalError("خطا در بارگذاری فایل تصویر. ممکن است فایل خراب باشد."); initializeForm(); }; |
| imagePreview.src = currentImageObjectURL; |
| }); |
| |
| durationInfoIcon.addEventListener('click', () => durationModal.classList.add('active')); |
| closeDurationModalBtn.addEventListener('click', () => durationModal.classList.remove('active')); |
| durationModal.addEventListener('click', (e) => { if (e.target === durationModal) durationModal.classList.remove('active'); }); |
| |
| confirmRegenerateBtn.addEventListener('click', handleRegenerateClick); |
| cancelRegenerateBtn.addEventListener('click', () => regenerateModal.classList.remove('active')); |
| regenerateModal.addEventListener('click', (e) => { if (e.target === regenerateModal) regenerateModal.classList.remove('active'); }); |
| |
| selectFrameBtn.addEventListener('click', async (e) => { |
| e.preventDefault(); |
| regenerateModal.classList.remove('active'); |
| frameSelectorModal.classList.add('active'); |
| frameLoader.style.display = 'block'; |
| framePreviews.innerHTML = ''; |
| const baseCard = previousResultsWrapper.firstElementChild; |
| if (!baseCard) { frameSelectorModal.classList.remove('active'); showCriticalError("ویدیوی مرجع برای انتخاب فریم یافت نشد."); return; } |
| const videoElement = baseCard.querySelector('video'); |
| const offsets = [0.05, 0.08, 0.1]; |
| |
| try { |
| if (videoElement.readyState < 1) { |
| await new Promise((resolve, reject) => { videoElement.addEventListener('loadedmetadata', resolve, {once: true}); videoElement.addEventListener('error', reject, {once: true}); }); |
| } |
| const framePromises = offsets.map(offset => captureFrameAtTime(videoElement, videoElement.duration - offset)); |
| const frameBlobs = await Promise.all(framePromises); |
| |
| frameLoader.style.display = 'none'; |
| frameBlobs.forEach((blob, index) => { |
| const item = document.createElement('div'); |
| item.className = 'frame-preview-item'; |
| const img = document.createElement('img'); |
| const objectURL = URL.createObjectURL(blob); |
| img.src = objectURL; |
| img.onload = () => URL.revokeObjectURL(objectURL); |
| img.addEventListener('click', () => { |
| manualFrameBlob = blob; |
| frameSelectorModal.classList.remove('active'); |
| handleRegenerateClick(); |
| }); |
| const label = document.createElement('span'); label.textContent = `فریم از ${offsets[index]} ثانیه قبل`; |
| item.appendChild(img); item.appendChild(label); framePreviews.appendChild(item); |
| }); |
| } catch (err) { frameSelectorModal.classList.remove('active'); showCriticalError(`خطا در استخراج فریم: ${err.message}`); } |
| }); |
| frameSelectorModal.addEventListener('click', (e) => { if (e.target === frameSelectorModal) frameSelectorModal.classList.remove('active'); }); |
| |
| confirmGuideBtn.addEventListener('click', () => { |
| extendGuideModal.classList.remove('active'); |
| hasSeenExtendGuide = true; |
| const latestCard = latestResultWrapper.querySelector('.video-result-card.latest'); |
| if (latestCard) { |
| const videoId = latestCard.id; |
| const baseVideoData = videoBlobs.get(videoId); |
| if (baseVideoData) { prepareForExtension(videoId, baseVideoData); } |
| } |
| }); |
| extendGuideModal.addEventListener('click', (e) => { if (e.target === extendGuideModal) confirmGuideBtn.click(); }); |
| |
| function handleReportClick(event) { |
| currentReportingVideoId = event.currentTarget.dataset.videoId; |
| reportFormContent.style.display = 'block'; |
| reportSuccessMessage.style.display = 'none'; |
| customReportSection.style.display = 'none'; |
| reportVideoModal.querySelector('#custom-report-text').value = ''; |
| reportVideoModal.classList.add('active'); |
| } |
| |
| function sendReportToServer(reportText, videoUrl) { console.log(`گزارش ارسال شد: \nمتن: ${reportText}\nآدرس ویدیو: ${videoUrl}`); } |
| |
| reportVideoModal.querySelectorAll('.report-option-btn').forEach(btn => { |
| btn.addEventListener('click', () => { |
| if (btn.id === 'report-other-btn') { customReportSection.style.display = 'block'; return; } |
| const videoData = videoBlobs.get(currentReportingVideoId); |
| if (videoData) { |
| sendReportToServer(btn.dataset.reason, videoData.objectURL); |
| reportFormContent.style.display = 'none'; |
| reportSuccessMessage.style.display = 'block'; |
| setTimeout(() => reportVideoModal.classList.remove('active'), 2000); |
| } |
| }); |
| }); |
| |
| reportVideoModal.querySelector('#send-report-btn').addEventListener('click', () => { |
| const customText = reportVideoModal.querySelector('#custom-report-text').value.trim(); |
| if (customText && currentReportingVideoId) { |
| const videoData = videoBlobs.get(currentReportingVideoId); |
| if (videoData) { |
| sendReportToServer(customText, videoData.objectURL); |
| reportFormContent.style.display = 'none'; |
| reportSuccessMessage.style.display = 'block'; |
| setTimeout(() => reportVideoModal.classList.remove('active'), 2000); |
| } |
| } |
| }); |
| reportVideoModal.addEventListener('click', (e) => { if(e.target === reportVideoModal) reportVideoModal.classList.remove('active'); }); |
| reportVideoModal.querySelectorAll('.emoji').forEach(emoji => { emoji.addEventListener('click', () => { reportVideoModal.querySelector('#custom-report-text').value += emoji.textContent; }); }); |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| const canvas = document.getElementById('neural-network-canvas'); |
| if (!canvas) return; const header = canvas.parentElement; const ctx = canvas.getContext('2d'); |
| let particles = []; const particleCount = 20; const maxDistance = 100; |
| const computedStyles = getComputedStyle(document.documentElement); |
| const particleColor = computedStyles.getPropertyValue('--accent-primary').trim() || '#4A6CFA'; |
| const lineColor = computedStyles.getPropertyValue('--text-tertiary').trim() || '#8A94A6'; |
| function resizeCanvas() { canvas.width = header.clientWidth; canvas.height = header.clientHeight; init(); } |
| class Particle { constructor() { this.x = Math.random() * canvas.width; this.y = Math.random() * canvas.height; this.vx = (Math.random() - 0.5) * 0.3; this.vy = (Math.random() - 0.5) * 0.3; this.radius = 1.2; } update() { this.x += this.vx; this.y += this.vy; if (this.x < 0 || this.x > canvas.width) this.vx *= -1; if (this.y < 0 || this.y > canvas.height) this.vy *= -1; } draw() { ctx.beginPath(); ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2); ctx.fillStyle = particleColor; ctx.fill(); } } |
| function init() { particles = []; for (let i = 0; i < particleCount; i++) { particles.push(new Particle()); } } |
| function connectParticles() { for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const dx = particles[i].x - particles[j].x; const dy = particles[i].y - particles[j].y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(particles[i].x, particles[i].y); ctx.lineTo(particles[j].x, particles[j].y); ctx.strokeStyle = lineColor; ctx.lineWidth = 0.2; ctx.globalAlpha = 1 - distance / maxDistance; ctx.stroke(); } } } ctx.globalAlpha = 1; } |
| function animate() { ctx.clearRect(0, 0, canvas.width, canvas.height); particles.forEach(particle => { particle.update(); particle.draw(); }); connectParticles(); requestAnimationFrame(animate); } |
| window.addEventListener('resize', resizeCanvas); resizeCanvas(); animate(); |
| }); |
| </script> |
| </body> |
| </html> |