| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>DARKMEDIA-X | SIMPLE UI</title> |
| <style> |
| :root { |
| --bg-deep: #050505; |
| --bg-panel: #0a0a0a; |
| --accent-red: #ff3333; |
| --text-main: #eee; |
| --border-color: #222; |
| } |
| * { box-sizing: border-box; outline: none; } |
| body { |
| background: var(--bg-deep); |
| color: var(--text-main); |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| margin: 0; |
| height: 100vh; |
| display: flex; |
| overflow: hidden; |
| } |
| |
| |
| .sidebar { |
| width: 300px; |
| background: var(--bg-panel); |
| border-right: 1px solid var(--border-color); |
| display: flex; |
| flex-direction: column; |
| } |
| .header { |
| padding: 20px; |
| font-size: 14px; |
| font-weight: 900; |
| letter-spacing: 2px; |
| color: var(--accent-red); |
| border-bottom: 1px solid var(--border-color); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .engine-status-bar { |
| display: flex; |
| gap: 5px; |
| padding: 8px 20px; |
| background: rgba(0,0,0,0.3); |
| border-bottom: 1px solid var(--border-color); |
| } |
| .engine-tag { |
| font-size: 8px; |
| padding: 2px 6px; |
| border-radius: 3px; |
| font-weight: bold; |
| text-transform: uppercase; |
| opacity: 0.3; |
| border: 1px solid #444; |
| transition: 0.3s; |
| } |
| .engine-tag.active { |
| opacity: 1; |
| border-color: var(--accent-red); |
| color: var(--accent-red); |
| box-shadow: 0 0 5px rgba(255,51,51,0.3); |
| } |
| .engine-tag.nvidia.active { |
| border-color: #76b900; |
| color: #76b900; |
| box-shadow: 0 0 5px rgba(118,185,0,0.3); |
| } |
| .story-list { |
| flex-grow: 1; |
| overflow-y: auto; |
| padding: 10px; |
| } |
| .story-item { |
| padding: 12px 20px; |
| background: #111; |
| margin-bottom: 5px; |
| cursor: pointer; |
| border-radius: 4px; |
| border: 1px solid transparent; |
| transition: 0.2s; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .story-item:hover { background: #1a1a1a; } |
| |
| .category-header { |
| padding: 10px 15px; |
| font-size: 10px; |
| font-weight: 900; |
| color: #444; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| background: rgba(0,0,0,0.3); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-top: 15px; |
| border-bottom: 1px solid #111; |
| } |
| .btn-delete-cat { |
| background: transparent; |
| border: none; |
| color: #333; |
| cursor: pointer; |
| font-size: 12px; |
| padding: 2px 5px; |
| border-radius: 3px; |
| } |
| .btn-delete-cat:hover { |
| color: #ff4d4d; |
| background: rgba(255,0,0,0.1); |
| } |
| |
| .story-item.selected { |
| border-color: var(--accent-red); |
| background: rgba(255, 51, 51, 0.1); |
| } |
| |
| .img-badge { |
| font-size: 10px; |
| background: #222; |
| color: #666; |
| padding: 2px 8px; |
| border-radius: 10px; |
| font-weight: bold; |
| min-width: 15px; |
| text-align: center; |
| border: 1px solid #333; |
| } |
| .img-badge.ready { |
| background: rgba(76, 175, 80, 0.1); |
| color: #4caf50; |
| border-color: #2e7d32; |
| } |
| |
| |
| |
| .main-content { |
| flex-grow: 1; |
| display: flex; |
| flex-direction: column; |
| padding: 40px; |
| align-items: center; |
| overflow-y: auto; |
| position: relative; |
| } |
| |
| |
| .global-spinner { |
| display: none; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: rgba(0, 0, 0, 0.85); |
| z-index: 9999; |
| justify-content: center; |
| align-items: center; |
| flex-direction: column; |
| } |
| .global-spinner.active { |
| display: flex; |
| } |
| .spinner-ring { |
| width: 80px; |
| height: 80px; |
| border: 6px solid #333; |
| border-top: 6px solid #ff3333; |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 8px; |
| height: 8px; |
| } |
| ::-webkit-scrollbar-track { |
| background: #1a1a1a; |
| border-radius: 4px; |
| } |
| ::-webkit-scrollbar-thumb { |
| background: linear-gradient(135deg, #c00 0%, #800 100%); |
| border-radius: 4px; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: linear-gradient(135deg, #f00 0%, #a00 100%); |
| } |
| ::-webkit-scrollbar-corner { |
| background: #1a1a1a; |
| } |
| .spinner-text { |
| color: #fff; |
| margin-top: 20px; |
| font-size: 18px; |
| font-weight: bold; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| } |
| .spinner-subtext { |
| color: #888; |
| margin-top: 10px; |
| font-size: 12px; |
| } |
| .spinner-progress { |
| width: 200px; |
| height: 8px; |
| background: #222; |
| border-radius: 4px; |
| margin-top: 15px; |
| overflow: hidden; |
| } |
| .spinner-progress-bar { |
| height: 100%; |
| background: linear-gradient(90deg, #ff3333, #ff6666); |
| border-radius: 4px; |
| width: 0%; |
| transition: width 0.3s ease; |
| } |
| .spinner-percent { |
| color: #fff; |
| margin-top: 8px; |
| font-size: 14px; |
| font-weight: bold; |
| } |
| @keyframes spin { |
| 0% { transform: rotate(0deg); } |
| 100% { transform: rotate(360deg); } |
| } |
| |
| |
| .log-panel { |
| width: 350px; |
| background: #080808; |
| border-left: 1px solid var(--border-color); |
| display: flex; |
| flex-direction: column; |
| font-family: 'Consolas', 'Monaco', monospace; |
| } |
| .log-header { |
| padding: 15px 20px; |
| font-size: 11px; |
| font-weight: bold; |
| color: #555; |
| text-transform: uppercase; |
| letter-spacing: 2px; |
| border-bottom: 1px solid var(--border-color); |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .log-content { |
| flex-grow: 1; |
| padding: 15px; |
| font-size: 10px; |
| line-height: 1.4; |
| overflow-y: auto; |
| color: #888; |
| scroll-behavior: smooth; |
| } |
| .log-line { |
| margin-bottom: 2px; |
| border-left: 2px solid transparent; |
| padding-left: 8px; |
| white-space: pre-wrap; |
| word-break: break-word; |
| } |
| |
| .log-content::-webkit-scrollbar { |
| width: 4px; |
| } |
| .log-content::-webkit-scrollbar-track { |
| background: #050505; |
| } |
| .log-content::-webkit-scrollbar-thumb { |
| background: #222; |
| border-radius: 2px; |
| } |
| .log-content::-webkit-scrollbar-thumb:hover { |
| background: var(--accent-red); |
| } |
| .log-line.info { border-color: #222; } |
| .log-line.success { color: #4caf50; border-color: #2e7d32; } |
| .log-line.error { color: #f44336; border-color: #c62828; } |
| .log-line.warning { color: #ff9800; border-color: #ef6c00; } |
| .log-line.ai { color: #9c27b0; border-color: #7b1fa2; } |
| |
| .workflow-container { |
| width: 100%; |
| max-width: 800px; |
| background: var(--bg-panel); |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| padding: 40px; |
| box-shadow: 0 10px 30px rgba(0,0,0,0.5); |
| } |
| |
| .step { |
| margin-bottom: 30px; |
| padding-bottom: 30px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| .step:last-child { border-bottom: none; margin-bottom: 0; padding-bottom: 0; } |
| |
| .step-title { |
| font-size: 12px; |
| color: #888; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 15px; |
| } |
| |
| h2 { margin: 0 0 20px 0; font-weight: 300; } |
| |
| .action-btn { |
| background: #1a1a1a; |
| color: #fff; |
| border: 1px solid #333; |
| padding: 15px 30px; |
| font-size: 14px; |
| font-weight: bold; |
| border-radius: 4px; |
| cursor: pointer; |
| transition: 0.3s; |
| width: 100%; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| .action-btn:hover:not(:disabled) { |
| background: #222; |
| border-color: #555; |
| } |
| .action-btn.primary { |
| background: rgba(255,51,51,0.1); |
| border-color: var(--accent-red); |
| color: var(--accent-red); |
| } |
| .action-btn.primary:hover:not(:disabled) { |
| background: var(--accent-red); |
| color: #fff; |
| box-shadow: 0 0 15px rgba(255,51,51,0.5); |
| } |
| .action-btn:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| } |
| |
| .wizard-nav { |
| display: flex; |
| margin-bottom: 30px; |
| border-bottom: 1px solid var(--border-color); |
| } |
| .wizard-step { |
| flex: 1; |
| text-align: center; |
| padding: 15px; |
| color: #666; |
| font-size: 14px; |
| font-weight: bold; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| cursor: pointer; |
| border-bottom: 3px solid transparent; |
| transition: 0.3s; |
| } |
| .wizard-step:hover { color: #aaa; } |
| .wizard-step.active { |
| color: var(--accent-red); |
| border-bottom-color: var(--accent-red); |
| } |
| .step-content { |
| display: none; |
| } |
| .step-content.active { |
| display: block; |
| animation: fadeIn 0.3s ease-out; |
| } |
| |
| .loader { |
| width: 16px; |
| height: 16px; |
| border: 2px solid rgba(255, 51, 51, 0.2); |
| border-radius: 50%; |
| border-top-color: var(--accent-red); |
| animation: spin 0.8s linear infinite; |
| display: inline-block; |
| vertical-align: middle; |
| margin-right: 8px; |
| } |
| @keyframes spin { |
| to { transform: rotate(360deg); } |
| } |
| |
| |
| .action-btn.loading { |
| background: #111 !important; |
| border-color: #333 !important; |
| color: #666 !important; |
| cursor: not-allowed !important; |
| } |
| |
| .action-btn.loading:hover { |
| transform: none !important; |
| box-shadow: none !important; |
| background: #111 !important; |
| border-color: #333 !important; |
| } |
| |
| .action-btn.primary.loading { |
| background: #111 !important; |
| border-color: #333 !important; |
| color: #666 !important; |
| } |
| |
| .action-btn.primary.loading:hover { |
| background: #111 !important; |
| border-color: #333 !important; |
| color: #666 !important; |
| } |
| |
| .pulse-loading { |
| display: flex; |
| gap: 5px; |
| justify-content: center; |
| padding: 20px; |
| } |
| .pulse-loading div { |
| width: 8px; |
| height: 8px; |
| background: var(--accent-red); |
| border-radius: 50%; |
| animation: pulse 0.6s infinite alternate; |
| box-shadow: 0 0 10px var(--accent-red); |
| } |
| .pulse-loading div:nth-child(2) { animation-delay: 0.2s; } |
| .pulse-loading div:nth-child(3) { animation-delay: 0.4s; } |
| @keyframes pulse { |
| from { transform: scale(0.8); opacity: 0.3; } |
| to { transform: scale(1.2); opacity: 1; } |
| } |
| |
| |
| .status-panel { |
| margin-top: 40px; |
| width: 100%; |
| max-width: 800px; |
| } |
| .status-text { |
| font-family: monospace; |
| color: #aaa; |
| margin-bottom: 10px; |
| display: flex; |
| justify-content: space-between; |
| } |
| .preview-box { |
| width: 100%; |
| height: 300px; |
| background: #000; |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| overflow: hidden; |
| position: relative; |
| } |
| .preview-box img { |
| max-width: 100%; |
| max-height: 100%; |
| object-fit: contain; |
| z-index: 1; |
| } |
| |
| |
| .gallery-section { |
| margin-top: 30px; |
| width: 100%; |
| max-width: 800px; |
| } |
| .gallery-header { |
| font-size: 11px; |
| color: #666; |
| font-weight: bold; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| margin-bottom: 10px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .gallery-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); |
| gap: 10px; |
| background: #080808; |
| border: 1px solid var(--border-color); |
| border-radius: 8px; |
| padding: 15px; |
| min-height: 100px; |
| } |
| .gallery-item { |
| aspect-ratio: 9/16; |
| background: #111; |
| border-radius: 4px; |
| overflow: hidden; |
| border: 1px solid #222; |
| position: relative; |
| cursor: pointer; |
| transition: all 0.2s; |
| } |
| .gallery-item:hover { |
| border-color: var(--accent-red); |
| transform: scale(1.05); |
| z-index: 10; |
| } |
| .gallery-item img, .gallery-item video { |
| width: 100%; |
| height: 100%; |
| object-fit: cover; |
| } |
| .video-item::after { |
| content: '▶'; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| transform: translate(-50%, -50%); |
| font-size: 24px; |
| color: rgba(255,255,255,0.8); |
| background: rgba(0,0,0,0.4); |
| width: 40px; |
| height: 40px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| border-radius: 50%; |
| border: 2px solid rgba(255,255,255,0.5); |
| pointer-events: none; |
| transition: all 0.2s; |
| } |
| .gallery-item:hover.video-item::after { |
| background: var(--accent-red); |
| border-color: #fff; |
| transform: translate(-50%, -50%) scale(1.2); |
| } |
| .gallery-item .scene-num { |
| position: absolute; |
| top: 5px; |
| left: 5px; |
| right: 5px; |
| background: rgba(0,0,0,0.6); |
| color: #fff; |
| font-size: 8px; |
| padding: 2px 4px; |
| border-radius: 2px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| .btn-delete-img { |
| background: rgba(192, 0, 0, 0.8); |
| color: #fff; |
| border: 1px solid #f44; |
| border-radius: 4px; |
| width: 20px; |
| height: 18px; |
| font-size: 12px; |
| font-weight: bold; |
| cursor: pointer; |
| line-height: 1; |
| padding: 0; |
| opacity: 0.7; |
| transition: all 0.2s; |
| } |
| .btn-delete-img:hover { |
| background: #f00; |
| opacity: 1; |
| transform: scale(1.1); |
| } |
| #preview-narration { |
| position: absolute; |
| bottom: 20px; |
| width: 90%; |
| text-align: center; |
| color: white; |
| text-shadow: 1px 1px 4px #000, -1px -1px 4px #000, 0 0 8px #000; |
| font-weight: bold; |
| z-index: 10; |
| } |
| |
| .nav-link { |
| position: absolute; |
| top: 20px; |
| right: 20px; |
| color: #666; |
| text-decoration: none; |
| font-size: 11px; |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| .nav-link:hover { color: #fff; } |
| |
| |
| .custom-modal-overlay { |
| position: fixed; |
| top: 0; left: 0; right: 0; bottom: 0; |
| background: rgba(0,0,0,0.8); |
| backdrop-filter: blur(5px); |
| display: none; |
| align-items: center; |
| justify-content: center; |
| z-index: 9999; |
| } |
| .custom-modal { |
| background: var(--bg-panel); |
| border: 1px solid var(--accent-red); |
| border-radius: 8px; |
| padding: 30px; |
| width: 90%; |
| max-width: 400px; |
| box-shadow: 0 10px 40px rgba(255,51,51,0.2); |
| text-align: center; |
| animation: modalFadeIn 0.2s ease-out; |
| } |
| @keyframes modalFadeIn { |
| from { opacity: 0; transform: scale(0.95); } |
| to { opacity: 1; transform: scale(1); } |
| } |
| .custom-modal h3 { |
| margin: 0 0 15px 0; |
| color: var(--accent-red); |
| font-size: 14px; |
| letter-spacing: 2px; |
| text-transform: uppercase; |
| } |
| .custom-modal p { |
| margin: 0 0 25px 0; |
| color: #ccc; |
| font-size: 14px; |
| line-height: 1.5; |
| } |
| .custom-modal-buttons { |
| display: flex; |
| gap: 15px; |
| justify-content: center; |
| } |
| .custom-modal-btn { |
| background: #111; |
| border: 1px solid #333; |
| color: #fff; |
| padding: 10px 20px; |
| border-radius: 4px; |
| cursor: pointer; |
| font-weight: bold; |
| transition: 0.2s; |
| } |
| .custom-modal-btn:hover { |
| background: #222; |
| border-color: #555; |
| } |
| .custom-modal-btn.primary { |
| background: rgba(255,51,51,0.1); |
| border-color: var(--accent-red); |
| color: var(--accent-red); |
| } |
| .custom-modal-btn.primary:hover { |
| background: var(--accent-red); |
| color: #fff; |
| box-shadow: 0 0 10px rgba(255,51,51,0.4); |
| } |
| |
| |
| .loading-overlay { |
| position: fixed; |
| inset: 0; |
| display: none; |
| align-items: center; |
| justify-content: center; |
| flex-direction: column; |
| gap: 15px; |
| background: rgba(0,0,0,0.85); |
| backdrop-filter: blur(4px); |
| -webkit-backdrop-filter: blur(4px); |
| z-index: 9999; |
| text-align: center; |
| transition: all 0.5s ease; |
| } |
| .loading-overlay.mini-mode { |
| inset: auto 370px 20px auto; |
| width: 320px; |
| height: auto; |
| padding: 20px; |
| background: rgba(10, 10, 10, 0.95) !important; |
| border: 1px solid var(--accent-red); |
| border-radius: 8px; |
| backdrop-filter: blur(10px) !important; |
| box-shadow: 0 10px 40px rgba(0,0,0,0.8), 0 0 20px rgba(255, 51, 51, 0.2); |
| z-index: 99999; |
| pointer-events: auto; |
| } |
| .loading-overlay.mini-mode .master-loader-box { |
| padding: 0; |
| border: none; |
| background: none; |
| width: 100%; |
| } |
| .loading-overlay.mini-mode .loading-spinner { |
| width: 30px; |
| height: 30px; |
| border-width: 2px; |
| margin-bottom: 10px; |
| } |
| .loading-overlay.mini-mode .loading-text-master { |
| font-size: 10px; |
| margin-bottom: 10px; |
| line-height: 1.4; |
| } |
| .loading-overlay.mini-mode div[style*="width: 250px"] { |
| width: 100% !important; |
| } |
| .master-loader-box { |
| position: relative; |
| padding: 40px; |
| border: 1px solid rgba(255, 51, 51, 0.3); |
| background: rgba(10, 10, 10, 0.8); |
| box-shadow: 0 0 50px rgba(255, 51, 51, 0.1); |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| overflow: hidden; |
| } |
| .master-loader-box::after { |
| content: ""; |
| position: absolute; |
| top: -100%; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: linear-gradient(to bottom, transparent, rgba(255, 51, 51, 0.2), transparent); |
| animation: scan-line 2s linear infinite; |
| pointer-events: none; |
| } |
| @keyframes scan-line { |
| 0% { top: -100%; } |
| 100% { top: 100%; } |
| } |
| .loading-spinner-master { |
| width: 60px; |
| height: 60px; |
| border: 2px solid rgba(255, 51, 51, 0.1); |
| border-top-color: var(--accent-red); |
| border-right-color: var(--accent-red); |
| border-radius: 50%; |
| animation: spin 1s cubic-bezier(0.5, 0.1, 0.5, 1) infinite; |
| box-shadow: 0 0 20px rgba(255, 51, 51, 0.2); |
| } |
| .loading-text-master { |
| font-size: 11px; |
| letter-spacing: 4px; |
| color: var(--accent-red); |
| font-family: "Courier New", monospace; |
| font-weight: 900; |
| text-transform: uppercase; |
| animation: text-pulse 1.5s ease-in-out infinite; |
| } |
| @keyframes text-pulse { |
| 0%, 100% { opacity: 0.4; filter: blur(1px); } |
| 50% { opacity: 1; filter: blur(0); } |
| } |
| |
| .btn-delete-story { |
| background: transparent; |
| border: none; |
| color: #444; |
| cursor: pointer; |
| padding: 5px; |
| border-radius: 4px; |
| transition: all 0.2s; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| } |
| .story-item:hover .btn-delete-story { color: #888; } |
| .btn-delete-story:hover { color: #ff4d4d !important; background: rgba(255,0,0,0.1); } |
| </style> |
| </head> |
| <body> |
|
|
| |
| <div id="global-master-loader" class="loading-overlay"> |
| <div class="master-loader-box"> |
| <div class="loading-spinner-master"></div> |
| <div style="height: 20px;"></div> |
| <div class="loading-text-master" id="master-loader-text">PROCESSING</div> |
|
|
| |
| <div style="width: 250px; height: 4px; background: rgba(255,255,255,0.05); border-radius: 2px; margin-top: 20px; position: relative; overflow: hidden; border: 1px solid rgba(255,51,51,0.2);"> |
| <div id="master-loader-bar" style="position: absolute; top: 0; left: 0; height: 100%; width: 0%; background: var(--accent-red); box-shadow: 0 0 10px var(--accent-red); transition: width 0.3s ease;"></div> |
| </div> |
|
|
| <div style="display: flex; justify-content: space-between; width: 250px; margin-top: 8px;"> |
| <div id="master-loader-status" style="font-size: 8px; color: #666; font-family: monospace; letter-spacing: 1px; text-transform: uppercase;">Neural Link: Active</div> |
| <div id="master-loader-timer" style="font-size: 8px; color: var(--accent-red); font-family: monospace; font-weight: bold;">0.0s</div> |
| </div> |
|
|
| <div id="master-loader-story" style="font-size: 8px; color: #333; margin-top: 15px; font-family: monospace; letter-spacing: 2px; text-align: center;">DARKMEDIA-X NEURAL LINK</div> |
| </div> |
| </div> |
| <div class="custom-modal-overlay" id="custom-modal-overlay"> |
| <div class="custom-modal"> |
| <h3 id="custom-modal-title">SYSTEM MESSAGE</h3> |
| <p id="custom-modal-text">Are you sure?</p> |
| <div class="custom-modal-buttons"> |
| <button class="custom-modal-btn" id="custom-modal-cancel">Annuler</button> |
| <button class="custom-modal-btn primary" id="custom-modal-confirm">OK</button> |
| </div> |
| </div> |
| </div> |
|
|
| <a href="/index.html" class="nav-link">➔ Switch to PRO STUDIO</a> |
|
|
| <div class="sidebar"> |
| <div class="header"> |
| <span>STORIES</span> |
| <div style="display:flex; gap:10px; align-items:center;"> |
| <span style="cursor:pointer; color:#888;" onclick="loadStories()">↻</span> |
| </div> |
| </div> |
| <div class="engine-status-bar"> |
| <div class="engine-tag active" id="tag-hf">HUGGING FACE</div> |
| </div> |
| <div class="story-list" id="story-list"> |
| <div style="padding: 20px; text-align: center; color: #666;">Loading stories...</div> |
| </div> |
| </div> |
|
|
| <div class="main-content"> |
| <div class="global-spinner" id="global-spinner"> |
| <div class="spinner-ring"></div> |
| <div class="spinner-text" id="spinner-text">Génération en cours</div> |
| <div class="spinner-subtext" id="spinner-subtext">Veuillez patienter...</div> |
| <div class="spinner-progress"> |
| <div class="spinner-progress-bar" id="spinner-progress-bar"></div> |
| </div> |
| <div class="spinner-percent" id="spinner-percent">0%</div> |
| </div> |
| <div class="workflow-container"> |
| |
| <div class="wizard-nav"> |
| <div class="wizard-step active" id="nav-step1" onclick="showStep(1)">1. SÉLECTION</div> |
| <div class="wizard-step" id="nav-step2" onclick="showStep(2)">2. VISUELS</div> |
| <div class="wizard-step" id="nav-step3" onclick="showStep(3)">3. MONTAGE</div> |
| </div> |
|
|
| <div class="step-content active" id="content-step1"> |
| <div class="step-title">Étape 1 : Choisissez une histoire</div> |
| <div id="selection-placeholder" style="text-align: center; padding: 40px 0;"> |
| <h2 id="selected-title" style="color: #444;">Aucune histoire sélectionnée</h2> |
| <p style="color: #333; font-size: 12px; max-width: 400px; margin: 0 auto 30px;">Veuillez sélectionner une histoire dans la barre latérale gauche pour commencer la production.</p> |
| </div> |
| |
| <div id="ai-controls" style="display: none;"> |
| <div style="display: flex; gap: 10px; align-items: center; margin: 15px 0;"> |
| <span style="font-size: 11px; color: #666; font-weight: bold; text-transform: uppercase;">AI Mode :</span> |
| <span style="color:#ff9800; font-size:11px; font-weight:bold;">HUGGING FACE</span> |
| </div> |
| </div> |
|
|
| <button id="btn-extend" class="action-btn" style="background:#1a1a2e; border-color:#3f3f6e; color:#fff; display:none; margin-top:5px;" onclick="extendTo10Scenes()">✨ Étendre à 10 scènes (AI)</button> |
| |
| <div style="display: flex; flex-direction: column; gap: 10px; margin-top: 30px;"> |
| <button class="action-btn primary" id="btn-next-1" disabled onclick="showStep(2)">Suivant ➔</button> |
| </div> |
| </div> |
| |
| <div class="step-content" id="content-step2"> |
| <div class="step-title" style="text-transform: uppercase; font-size: 11px; letter-spacing: 1px; color: #888; margin-bottom: 10px;">ÉTAPE 2 : MODE DE CRÉATION VISUELLE</div> |
| <p style="color:#aaa; font-size:12px; margin-bottom:20px;">Choisissez comment illustrer les scènes (Images fixes classiques, Dessin animé ou Mode NVIDIA haute performance).</p> |
|
|
| <div style="display:flex; gap:10px; margin-bottom:15px; align-items:center;"> |
| <input type="checkbox" id="force-regen" style="width:16px; height:16px; accent-color:#ff4444; margin:0; cursor:pointer;"> |
| <label for="force-regen" style="color:#ff4444; font-size:11px; cursor:pointer;">Forcer la régénération (--force)</label> |
| </div> |
|
|
| <div style="display: grid; grid-template-columns: 1fr; gap: 10px;"> |
| <button id="btn-generate" class="action-btn" disabled style="background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); border: 2px solid #0f3460; color: #e94560; font-weight: 900; letter-spacing: 2px; font-size: 14px; padding: 18px; border-radius: 8px; text-transform: uppercase; box-shadow: 0 4px 15px rgba(233, 69, 96, 0.2); transition: all 0.3s ease; cursor: pointer;" onmouseover="if(!this.disabled){this.style.boxShadow='0 6px 25px rgba(233,69,96,0.4)'; this.style.transform='translateY(-2px)';}" onmouseout="if(!this.disabled){this.style.boxShadow='0 4px 15px rgba(233,69,96,0.2)'; this.style.transform='translateY(0)';}" onclick="generateImages()">📸 Générer les Images <span style="font-size:12px; opacity:0.8; margin-left:8px; background: rgba(233,69,96,0.2); padding: 3px 10px; border-radius: 12px;">(10)</span></button> |
| </div> |
|
|
| <div style="margin-top:25px; border-top: 1px solid #1a1a1a; padding-top: 25px;"> |
| <label class="pro-field-label" style="font-size:10px; color:#555; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; display:block;">Style Artistique (Post-Processing)</label> |
| <select id="artistic-style-select" class="pro-field" style="background:#0a0a0a; border:1px solid #333; color:#aaa; padding:12px; width:100%; border-radius:4px; font-size:12px;" onchange="previewArtStyle(this.value)"> |
| <option value="none">Original (AI Native)</option> |
| <option value="oil_paint">Peinture à l'huile (Magick)</option> |
| <option value="charcoal">Fusain (Magick)</option> |
| <option value="sketch">Crayon (Magick)</option> |
| <option value="vintage">Vieux Film (Magick)</option> |
| <option value="night_vision">Vision Nocturne (Magick)</option> |
| <option value="pixel_art">Pixel Art (Aseprite)</option> |
| <option value="vhs_static">VHS Glitch (OpenCV)</option> |
| </select> |
| <div id="art-style-preview" style="margin-top:15px; display:none; text-align:center;"> |
| <canvas id="art-preview-canvas" style="max-width:100%; max-height:200px; border:1px solid #333; border-radius:4px;"></canvas> |
| <div style="font-size:9px; color:#555; margin-top:5px;">Aperçu de l'effet</div> |
| </div> |
| <button id="btn-apply-style" class="action-btn" style="margin-top:15px; width:100%; background:#1a0a2e; border:1px solid #3d1b6e; color:#fff; font-weight:bold; letter-spacing:1px; font-size:11px; padding:10px;" onclick="applyArtStyleToStory()" disabled>✨ APPLIQUER LE STYLE À TOUTES LES IMAGES</button> |
| </div> |
|
|
| <div style="display: flex; gap: 10px; margin-top: 30px;"> |
| <button class="action-btn" style="background: transparent; border-color: #333; color: #aaa;" onclick="showStep(1)">⬅ Retour</button> |
| <button class="action-btn primary" onclick="showStep(3)">Suivant ➔</button> |
| </div> |
| </div> |
| |
| <div class="step-content" id="content-step3"> |
| <div class="step-title">Étape 3 : Rendu Final</div> |
| <p style="color:#aaa; font-size:12px; margin-bottom:20px;">Assemblez la vidéo finale avec la voix IA, la musique et les effets spéciaux.</p> |
| |
| <div style="margin-bottom:25px; background: rgba(0,0,0,0.2); padding: 15px; border-radius: 6px; border: 1px solid #222;"> |
| <label class="pro-field-label" style="font-size:10px; color:#555; text-transform:uppercase; letter-spacing:1px; margin-bottom:10px; display:block;">Choix du Narrateur</label> |
| <select id="voice-select" class="pro-field" style="background:#0a0a0a; border:1px solid #333; color:#aaa; padding:12px; width:100%; border-radius:4px; font-size:12px;"> |
| <option value="fr-FR-DeniseNeural">France - Denise (Standard)</option> |
| <option value="fr-FR-HenriNeural">France - Henri (Standard)</option> |
| <option value="fr-CA-SylvieNeural">Québec - Sylvie (Standard)</option> |
| <option value="fr-CA-JeanNeural">Québec - Jean (Standard)</option> |
| <optgroup label="EXPRESSIVE AI (BARK)"> |
| <option value="bark_fr_man">Français Expressif (Homme)</option> |
| <option value="bark_fr_woman">Français Expressif (Femme)</option> |
| <option value="bark_en_horror">English Horror (Deep)</option> |
| </optgroup> |
| </select> |
| </div> |
|
|
| <button id="btn-render" class="action-btn primary" disabled onclick="renderVideo()">🎬 Faire le Montage Vidéo</button> |
| |
| |
| <div id="video-success-panel" style="display:none; margin-top:20px; padding:15px; background:rgba(76,175,80,0.1); border:2px solid #4caf50; border-radius:8px; text-align:center;"> |
| <div style="color:#4caf50; font-size:24px; margin-bottom:10px;">✅ VIDÉO GÉNÉRÉE !</div> |
| <div id="video-filename" style="color:#888; font-size:11px; margin-bottom:15px;"></div> |
| <div style="display:flex; gap:10px; justify-content:center; flex-wrap:wrap;"> |
| <button onclick="playVideo()" style="background:#4caf50; color:#fff; border:none; padding:10px 20px; border-radius:4px; cursor:pointer; font-weight:bold;">▶️ REGARDER</button> |
| <button onclick="downloadVideo()" style="background:#2196f3; color:#fff; border:none; padding:10px 20px; border-radius:4px; cursor:pointer; font-weight:bold;">⬇️ TÉLÉCHARGER</button> |
| <button onclick="uploadToYoutube()" style="background:#ff4444; color:#fff; border:none; padding:10px 20px; border-radius:4px; cursor:pointer; font-weight:bold;">📤 YOUTUBE</button> |
| </div> |
| </div> |
| |
| |
| <div id="video-preview-container" style="display:none; margin-top:20px; max-width:100%;"> |
| <video id="video-preview-player" controls style="width:100%; max-height:300px; border-radius:8px; border:2px solid #333;"> |
| Votre navigateur ne supporte pas la lecture vidéo. |
| </video> |
| </div> |
|
|
| <div style="display: flex; gap: 10px; margin-top: 30px;"> |
| <button class="action-btn" style="background: transparent; border-color: #333; color: #aaa;" onclick="showStep(2)">⬅ Retour</button> |
| </div> |
| </div> |
| |
| </div> |
| |
| <div class="status-panel"> |
| <div class="status-text"> |
| <div style="display: flex; flex-direction: column; gap: 2px;"> |
| <span id="status-step">En attente...</span> |
| <span id="active-engine-display" style="font-size: 9px; color: var(--accent-red); font-weight: bold; text-transform: uppercase; letter-spacing: 1px;">Moteur : HUGGING FACE</span> |
| </div> |
| <span id="status-progress">0%</span> |
| </div> |
| <div class="preview-box"> |
| <div id="preview-placeholder" style="color:#333; font-weight:bold; letter-spacing:2px;">NO SIGNAL</div> |
| <img id="live-preview" src="" style="display:none;"> |
| <div id="preview-narration"></div> |
| </div> |
| </div> |
|
|
| |
| <div class="gallery-section" id="gallery-section" style="display: none;"> |
| <div class="gallery-header"> |
| <span>Galerie d'Assets</span> |
| <span id="gallery-count" style="font-size: 9px; opacity: 0.7;">0 images</span> |
| </div> |
| <div class="gallery-grid" id="gallery-grid"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="gallery-section" id="video-gallery-section" style="display: none; margin-top: 30px; border-top: 1px solid #222; padding-top: 20px;"> |
| <div class="gallery-header"> |
| <span>Vidéos Produites</span> |
| <span id="video-gallery-count" style="font-size: 9px; opacity: 0.7;">0 vidéos</span> |
| </div> |
| <div class="gallery-grid" id="video-gallery-grid" style="grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));"> |
| |
| </div> |
| </div> |
|
|
| |
| <div id="carousel-overlay" style="display:none; position:fixed; inset:0; background:rgba(0,0,0,0.95); z-index:9999; align-items:center; justify-content:center; flex-direction:column; gap:15px;" onclick="closeCarousel()"> |
| <div style="position:relative; max-width:90vw; max-height:80vh;" onclick="event.stopPropagation()"> |
| <img id="carousel-img" src="" style="max-width:90vw; max-height:80vh; object-fit:contain; border:2px solid #333; border-radius:4px;"> |
| <button onclick="carouselPrev(); event.stopPropagation();" style="position:absolute; left:-50px; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.7); border:1px solid #555; color:#fff; font-size:24px; width:40px; height:40px; border-radius:50%; cursor:pointer;">‹</button> |
| <button onclick="carouselNext(); event.stopPropagation();" style="position:absolute; right:-50px; top:50%; transform:translateY(-50%); background:rgba(0,0,0,0.7); border:1px solid #555; color:#fff; font-size:24px; width:40px; height:40px; border-radius:50%; cursor:pointer;">›</button> |
| </div> |
| <div id="carousel-label" style="color:#888; font-size:12px; font-family:monospace;"></div> |
| <button onclick="closeCarousel()" style="position:absolute; top:20px; right:20px; background:rgba(255,51,51,0.8); border:none; color:#fff; font-size:18px; width:36px; height:36px; border-radius:50%; cursor:pointer;">×</button> |
| </div> |
| </div> |
|
|
| |
| <div class="log-panel" style="display:none;"> |
| <div class="log-header"> |
| <span>Console Logs</span> |
| </div> |
| <div class="log-content" id="log-content"> |
| <div class="log-line info">Logs disponibles uniquement en déploiement local.</div> |
| </div> |
| </div> |
|
|
| <script> |
| // --- Auth token injector + API base URL redirect --- |
| const API_BASE_URL = "https://cybermedia-darkmedia-x-api.hf.space"; |
| (function() { |
| const _origFetch = window.fetch; |
| window.fetch = async function(resource, init) { |
| init = init || {}; |
| init.headers = init.headers || {}; |
| if (typeof init.headers === 'object' && !(init.headers instanceof Headers)) { |
| const token = localStorage.getItem("auth_token"); |
| if (token && typeof resource === 'string' && resource.startsWith('/api/')) { |
| init.headers['Authorization'] = 'Bearer ' + token; |
| } |
| } |
| // Redirect API calls to HF Space backend |
| if (typeof resource === 'string' && resource.startsWith('/api/')) { |
| resource = API_BASE_URL + resource; |
| } |
| return _origFetch(resource, init); |
| }; |
| })(); |
| |
| function customAlert(message, title="SYSTEM MESSAGE") { |
| return new Promise((resolve) => { |
| const overlay = document.getElementById("custom-modal-overlay"); |
| const btnCancel = document.getElementById("custom-modal-cancel"); |
| const btnConfirm = document.getElementById("custom-modal-confirm"); |
| |
| document.getElementById("custom-modal-title").textContent = title; |
| document.getElementById("custom-modal-text").textContent = message; |
| |
| btnCancel.style.display = "none"; // Hide cancel for alert |
| overlay.style.display = "flex"; |
| |
| btnConfirm.onclick = () => { |
| overlay.style.display = "none"; |
| btnConfirm.onclick = null; |
| resolve(); |
| }; |
| }); |
| } |
| |
| function customConfirm(message, title="SYSTEM MESSAGE") { |
| return new Promise((resolve) => { |
| const overlay = document.getElementById("custom-modal-overlay"); |
| const btnCancel = document.getElementById("custom-modal-cancel"); |
| const btnConfirm = document.getElementById("custom-modal-confirm"); |
| |
| document.getElementById("custom-modal-title").textContent = title; |
| document.getElementById("custom-modal-text").textContent = message; |
| |
| btnCancel.style.display = "block"; // Show cancel for confirm |
| overlay.style.display = "flex"; |
| |
| const cleanup = () => { |
| overlay.style.display = "none"; |
| btnCancel.onclick = null; |
| btnConfirm.onclick = null; |
| }; |
| |
| btnCancel.onclick = () => { cleanup(); resolve(false); }; |
| btnConfirm.onclick = () => { cleanup(); resolve(true); }; |
| }); |
| |
| } |
| |
| function onAiModeChange(mode) { |
| // No-op: engine select removed, always Hugging Face |
| } |
| |
| let selectedStory = null; |
| |
| let isTaskRunning = false; |
| |
| function showGlobalSpinner(text = "Génération en cours", subtext = "Veuillez patienter...") { |
| document.getElementById('spinner-text').textContent = text; |
| document.getElementById('spinner-subtext').textContent = subtext; |
| document.getElementById('spinner-progress-bar').style.width = '0%'; |
| document.getElementById('spinner-percent').textContent = '0%'; |
| document.getElementById('global-spinner').classList.add('active'); |
| } |
| |
| function hideGlobalSpinner() { |
| document.getElementById('global-spinner').classList.remove('active'); |
| } |
| |
| function updateSpinnerProgress(percent) { |
| const bar = document.getElementById('spinner-progress-bar'); |
| const text = document.getElementById('spinner-percent'); |
| bar.style.width = percent + '%'; |
| text.textContent = percent + '%'; |
| } |
| |
| // Button loading state management |
| function setButtonLoading(buttonId, isLoading, loadingText = null) { |
| const button = document.getElementById(buttonId); |
| if (!button) return; |
| |
| if (isLoading) { |
| button.setAttribute('data-original-text', button.innerHTML); |
| button.classList.add('loading'); |
| |
| let displayText = loadingText || button.textContent.trim(); |
| // Remove any existing spinner |
| button.querySelectorAll('.btn-spinner').forEach(el => el.remove()); |
| |
| // Add spinner |
| const spinner = document.createElement('span'); |
| spinner.className = 'btn-spinner'; |
| spinner.innerHTML = '<span class="loader"></span>'; |
| |
| // Update button content |
| button.innerHTML = ''; |
| button.appendChild(spinner); |
| |
| const textSpan = document.createElement('span'); |
| textSpan.textContent = displayText; |
| button.appendChild(textSpan); |
| |
| button.disabled = true; |
| } else { |
| button.classList.remove('loading'); |
| const originalText = button.getAttribute('data-original-text'); |
| if (originalText) { |
| button.innerHTML = originalText; |
| button.removeAttribute('data-original-text'); |
| } |
| button.disabled = false; |
| } |
| } |
| |
| function resetButtonLoading(buttonId) { |
| setButtonLoading(buttonId, false); |
| } |
| |
| // Helper to set multiple buttons loading |
| function setButtonsLoading(buttonIds, isLoading, loadingTexts = null) { |
| buttonIds.forEach((id, idx) => { |
| const text = loadingTexts && loadingTexts[idx] ? loadingTexts[idx] : null; |
| setButtonLoading(id, isLoading, text); |
| }); |
| } |
| |
| function showStep(stepIndex, force = false) { |
| if (isTaskRunning && !force) return; // Block manual navigation during tasks unless forced |
| // Update Nav |
| for (let i = 1; i <= 3; i++) { |
| document.getElementById(`nav-step${i}`).classList.remove('active'); |
| const content = document.getElementById(`content-step${i}`); |
| content.classList.remove('active'); |
| // Ensure display is managed for the check in interval |
| content.style.display = (i === stepIndex) ? 'block' : 'none'; |
| } |
| document.getElementById(`nav-step${stepIndex}`).classList.add('active'); |
| document.getElementById(`content-step${stepIndex}`).classList.add('active'); |
| } |
| |
| function showToast(message, type = "info") { |
| const toast = document.createElement("div"); |
| toast.style.position = "fixed"; |
| toast.style.bottom = "20px"; |
| toast.style.left = "50%"; |
| toast.style.transform = "translateX(-50%)"; |
| toast.style.background = type === "error" ? "#ff4444" : "#4caf50"; |
| toast.style.color = "white"; |
| toast.style.padding = "10px 20px"; |
| toast.style.borderRadius = "4px"; |
| toast.style.zIndex = "10000"; |
| toast.style.fontSize = "12px"; |
| toast.style.fontWeight = "bold"; |
| toast.style.boxShadow = "0 4px 12px rgba(0,0,0,0.5)"; |
| toast.textContent = message.toUpperCase(); |
| document.body.appendChild(toast); |
| setTimeout(() => toast.remove(), 3000); |
| } |
| |
| async function loadStories() { |
| try { |
| const r = await fetch("/api/stories"); |
| const d = await r.json(); |
| const stories = d.stories || []; |
| |
| const list = document.getElementById("story-list"); |
| list.innerHTML = ""; |
| |
| if (stories.length === 0) { |
| list.innerHTML = ` |
| <div style="padding: 30px 20px; text-align: center; color: #444;"> |
| <div style="font-size: 24px; margin-bottom: 10px;">📂</div> |
| <div style="font-size: 10px; font-weight: bold; color: #666; letter-spacing: 1px; text-transform: uppercase; margin-bottom: 10px;">Aucune histoire trouvée</div> |
| <div style="font-size: 9px; line-height: 1.4; color: #555;"> |
| Synchronisez vos données locales avec <b>sync_to_blob.py</b> ou créez une nouvelle histoire sur le Studio Pro. |
| </div> |
| </div> |
| `; |
| return; |
| } |
| |
| // Group by category |
| const grouped = {}; |
| stories.forEach(s => { |
| const cat = s.category || "General"; |
| if (!grouped[cat]) grouped[cat] = []; |
| grouped[cat].push(s); |
| }); |
| |
| Object.keys(grouped).sort().forEach(cat => { |
| // Category Header |
| const header = document.createElement("div"); |
| header.className = "category-header"; |
| header.innerHTML = ` |
| <span>${cat}</span> |
| <button class="btn-delete-cat" title="Supprimer tout le dossier ${cat}" onclick="event.stopPropagation(); deleteCategory('${cat}')">🗑️</button> |
| `; |
| list.appendChild(header); |
| |
| grouped[cat].forEach(s => { |
| const div = document.createElement("div"); |
| div.className = "story-item"; |
| div.id = `story-${s.id.replace(/[^a-zA-Z0-9]/g, '-')}`; |
| |
| // Nom de l'histoire |
| const leftPart = document.createElement("div"); |
| leftPart.style.display = "flex"; |
| leftPart.style.alignItems = "center"; |
| leftPart.style.gap = "10px"; |
| leftPart.style.overflow = "hidden"; |
| |
| const nameSpan = document.createElement("span"); |
| let displayTitle = (s.title || s.id); |
| if (displayTitle.includes('/')) displayTitle = displayTitle.split('/').pop(); |
| nameSpan.textContent = displayTitle; |
| nameSpan.style.overflow = "hidden"; |
| nameSpan.style.textOverflow = "ellipsis"; |
| nameSpan.style.whiteSpace = "nowrap"; |
| leftPart.appendChild(nameSpan); |
| |
| // Badge pour le nombre d'images générées vs total scènes |
| const imgCount = s.image_count || 0; |
| const totalScenes = s.total_scenes || 11; |
| const badge = document.createElement("span"); |
| badge.className = "img-badge"; |
| badge.textContent = `${imgCount}/${totalScenes}`; |
| if (imgCount >= totalScenes) badge.classList.add('ready'); |
| leftPart.appendChild(badge); |
| div.appendChild(leftPart); |
| |
| const deleteBtn = document.createElement("button"); |
| deleteBtn.className = "btn-delete-story"; |
| deleteBtn.innerHTML = "🗑️"; |
| deleteBtn.title = "Supprimer cette histoire"; |
| deleteBtn.onclick = (e) => { |
| e.stopPropagation(); |
| deleteStory(s.id); |
| }; |
| div.appendChild(deleteBtn); |
| |
| div.onclick = () => { |
| if (isTaskRunning) return; |
| selectStory(s, div); |
| }; |
| list.appendChild(div); |
| }); |
| }); |
| |
| // Auto-select story if one is running |
| checkOngoingTask(stories); |
| } catch (e) { |
| console.error("Failed to load stories", e); |
| } |
| } |
| |
| async function deleteCategory(category) { |
| if (!category || category === "General") { |
| await customAlert("Impossible de supprimer le dossier racine directement. Supprimez les histoires individuellement."); |
| return; |
| } |
| |
| if (!(await customConfirm(`⚠️ SUPPRESSION DU DOSSIER "${category.toUpperCase()}"\n\nÊtes-vous sûr de vouloir supprimer ce dossier et TOUTES les histoires qu'il contient ?`))) return; |
| |
| showLoading(`SUPPRESSION ${category.toUpperCase()}`); |
| try { |
| // Le backend delete_story accepte un chemin de dossier |
| const r = await fetch(`/api/stories/${encodeURIComponent(category)}`, { |
| method: "DELETE" |
| }); |
| const d = await r.json(); |
| if (d.status === "success") { |
| await loadStories(); |
| await customAlert(`Le dossier "${category}" a été supprimé.`); |
| } else { |
| await customAlert("Erreur : " + (d.message || "Inconnue")); |
| } |
| } catch(e) { |
| await customAlert("Erreur de connexion."); |
| } |
| hideLoading(); |
| } |
| |
| async function deleteStory(id) { |
| if (!id) return; |
| const storyName = id.includes('/') ? id.split('/').pop() : id; |
| if (!(await customConfirm(`⚠️ SUPPRESSION DÉFINITIVE\n\nÊtes-vous sûr de vouloir supprimer "${storyName.toUpperCase()}" ?\nCela effacera tous les fichiers associés.`))) return; |
| |
| showLoading("SUPPRESSION EN COURS"); |
| try { |
| const r = await fetch(`/api/stories/${encodeURIComponent(id)}`, { |
| method: "DELETE" |
| }); |
| const d = await r.json(); |
| if (d.status === "success") { |
| if (selectedStory && selectedStory.id === id) { |
| selectedStory = null; |
| document.getElementById('selected-title').innerHTML = "Aucune histoire sélectionnée"; |
| document.getElementById('btn-next-1').disabled = true; |
| document.getElementById('gallery-section').style.display = "none"; |
| document.getElementById('video-gallery-section').style.display = "none"; |
| } |
| await loadStories(); |
| await customAlert(`L'histoire "${storyName}" a été supprimée.`); |
| } else { |
| await customAlert("Erreur : " + (d.message || "Inconnue")); |
| } |
| } catch(e) { |
| await customAlert("Erreur de connexion lors de la suppression."); |
| } |
| hideLoading(); |
| } |
| |
| async function cleanProject() { |
| const check = document.getElementById("delete-all-check").checked; |
| if (!check) { |
| await customAlert("Veuillez cocher la case pour confirmer la suppression totale."); |
| return; |
| } |
| |
| if (!(await customConfirm("⚠️ ACTION IRRÉVERSIBLE\n\nCela va supprimer ABSOLUMENT TOUTES les histoires du projet. Continuer ?"))) return; |
| |
| showLoading("NETTOYAGE DU PROJET"); |
| try { |
| // On récupère la liste actuelle |
| const r = await fetch("/api/stories"); |
| const d = await r.json(); |
| const stories = d.stories || []; |
| |
| // On les supprime une par une (le backend rmtree déjà le dossier) |
| for (const s of stories) { |
| await fetch(`/api/stories/${encodeURIComponent(s.id)}`, { method: "DELETE" }); |
| } |
| |
| selectedStory = null; |
| document.getElementById('selected-title').innerHTML = "Aucune histoire sélectionnée"; |
| document.getElementById('btn-next-1').disabled = true; |
| document.getElementById('gallery-section').style.display = "none"; |
| document.getElementById('video-gallery-section').style.display = "none"; |
| document.getElementById("delete-all-check").checked = false; |
| |
| await loadStories(); |
| await customAlert("Le projet a été entièrement nettoyé."); |
| } catch(e) { |
| await customAlert("Erreur lors du nettoyage."); |
| } |
| hideLoading(); |
| } |
| |
| let masterLoaderInterval = null; |
| let masterLoaderStartTime = 0; |
| |
| function showLoading(text = "PROCESSING", simulatedDuration = 30, blocking = true) { |
| const loader = document.getElementById("global-master-loader"); |
| const txt = document.getElementById("master-loader-text"); |
| const bar = document.getElementById("master-loader-bar"); |
| const timer = document.getElementById("master-loader-timer"); |
| const status = document.getElementById("master-loader-status"); |
| |
| if (!loader) return; |
| |
| if (blocking) { |
| loader.classList.remove('mini-mode'); |
| } else { |
| loader.classList.add('mini-mode'); |
| } |
| |
| // Reset |
| clearInterval(masterLoaderInterval); |
| masterLoaderStartTime = Date.now(); |
| if (bar) bar.style.width = "0%"; |
| if (txt) txt.textContent = text; |
| if (status) status.textContent = "Neural Link: Active"; |
| |
| loader.style.display = "flex"; |
| |
| // Start Simulation / Timer |
| masterLoaderInterval = setInterval(() => { |
| const elapsed = (Date.now() - masterLoaderStartTime) / 1000; |
| if (timer) timer.textContent = elapsed.toFixed(1) + "s"; |
| |
| // Simulated progress for AI tasks (goes up to 95% and slows down) |
| if (bar) { |
| let progress = (elapsed / simulatedDuration) * 100; |
| if (progress > 95) progress = 95 + (progress - 95) * 0.1; // Slow down at the end |
| if (progress > 99) progress = 99; |
| bar.style.width = progress + "%"; |
| } |
| }, 100); |
| } |
| |
| function hideLoading() { |
| const loader = document.getElementById("global-master-loader"); |
| const bar = document.getElementById("master-loader-bar"); |
| |
| clearInterval(masterLoaderInterval); |
| |
| if (bar) bar.style.width = "100%"; // Finish line |
| |
| setTimeout(() => { |
| if (loader) { |
| loader.style.display = "none"; |
| loader.classList.remove('mini-mode'); |
| loader.style.background = ""; |
| loader.style.backdropFilter = ""; |
| } |
| }, 300); |
| } |
| |
| async function checkOngoingTask(storiesList = null) { |
| try { |
| const r = await fetch("/api/status"); |
| const status = await r.json(); |
| |
| if (status.story_id && (!selectedStory || selectedStory.id !== status.story_id)) { |
| console.log("Found ongoing task for story:", status.story_id); |
| |
| let stories = storiesList; |
| if (!stories) { |
| const rS = await fetch("/api/stories"); |
| const dS = await rS.json(); |
| stories = dS.stories || []; |
| } |
| |
| const story = stories.find(s => |
| s.id === status.story_id || |
| s.title === status.story || |
| (status.story_id && s.id.endsWith(status.story_id)) || |
| (status.story && s.title.includes(status.story)) |
| ); |
| if (story) { |
| const id = `story-${story.id.replace(/[^a-zA-Z0-9]/g, '-')}`; |
| const element = document.getElementById(id); |
| if (element) { |
| selectStory(story, element, true); // force = true |
| } |
| } |
| } |
| } catch (e) { |
| console.error("Error checking ongoing task:", e); |
| } |
| } |
| |
| // Global gallery images for carousel |
| let currentGalleryImages = []; |
| let currentGalleryIndex = 0; |
| |
| async function loadGallery() { |
| if (!selectedStory) return; |
| |
| const grid = document.getElementById("gallery-grid"); |
| const section = document.getElementById("gallery-section"); |
| |
| section.style.display = "block"; |
| |
| if (grid.innerHTML === "" || grid.innerHTML.includes("Loading")) { |
| grid.innerHTML = '<div class="pulse-loading"><div></div><div></div><div></div></div>'; |
| } |
| |
| try { |
| const r = await fetch(`/api/images?story_id=${encodeURIComponent(selectedStory.id)}`); |
| const d = await r.json(); |
| const images = d.images || []; |
| |
| currentGalleryImages = images; |
| |
| const count = document.getElementById("gallery-count"); |
| count.textContent = `${images.length} images`; |
| |
| if (images.length === 0) { |
| grid.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: #444;">Aucune image générée pour le moment.</div>'; |
| return; |
| } |
| |
| const html = images.map((img, idx) => ` |
| <div class="gallery-item" onclick="openCarousel(${idx})"> |
| <img src="${API_BASE_URL}${img.url}" loading="lazy" alt="${img.filename}" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%2250%%22 x=%2250%%22 text-anchor=%22middle%22 fill=%22%23666%22 font-size=%2214%22>ERR</text></svg>'"> |
| <div class="scene-num">${img.filename.replace('.png', '').replace('scene_', 'SCÈNE ')} <button class="btn-delete-img" onclick="event.stopPropagation(); deleteImage('${encodeURIComponent(selectedStory.id)}', '${img.filename}', this)" title="Supprimer">×</button></div> |
| </div> |
| `).join(''); |
| |
| if (grid.innerHTML !== html) { |
| grid.innerHTML = html; |
| } |
| } catch (e) { |
| console.error("Failed to load gallery", e); |
| } |
| } |
| |
| function openCarousel(idx) { |
| currentGalleryIndex = idx; |
| const overlay = document.getElementById("carousel-overlay"); |
| const img = document.getElementById("carousel-img"); |
| const label = document.getElementById("carousel-label"); |
| |
| if (currentGalleryImages.length === 0) return; |
| |
| img.src = API_BASE_URL + currentGalleryImages[idx].url; |
| label.textContent = currentGalleryImages[idx].filename; |
| overlay.style.display = "flex"; |
| } |
| |
| function closeCarousel() { |
| document.getElementById("carousel-overlay").style.display = "none"; |
| } |
| |
| function carouselPrev() { |
| currentGalleryIndex = (currentGalleryIndex - 1 + currentGalleryImages.length) % currentGalleryImages.length; |
| document.getElementById("carousel-img").src = API_BASE_URL + currentGalleryImages[currentGalleryIndex].url; |
| document.getElementById("carousel-label").textContent = currentGalleryImages[currentGalleryIndex].filename; |
| } |
| |
| function carouselNext() { |
| currentGalleryIndex = (currentGalleryIndex + 1) % currentGalleryImages.length; |
| document.getElementById("carousel-img").src = API_BASE_URL + currentGalleryImages[currentGalleryIndex].url; |
| document.getElementById("carousel-label").textContent = currentGalleryImages[currentGalleryIndex].filename; |
| } |
| |
| async function deleteImage(storyId, filename, btn) { |
| if (!confirm(`Supprimer ${filename} ?`)) return; |
| |
| const btnEl = btn || (event && event.target); |
| if (!btnEl) return; |
| const origText = btnEl.innerHTML; |
| btnEl.innerHTML = '⏳'; |
| btnEl.disabled = true; |
| |
| try { |
| const r = await fetch(`/api/images?story_id=${encodeURIComponent(storyId)}&filename=${encodeURIComponent(filename)}`, { |
| method: "DELETE" |
| }); |
| |
| if (r.ok) { |
| showToast(`${filename} supprimé`, "success"); |
| document.getElementById("gallery-grid").innerHTML = '<div class="pulse-loading"><div></div><div></div><div></div></div>'; |
| setTimeout(() => loadGallery(), 100); |
| } else { |
| showToast("Erreur lors de la suppression", "error"); |
| btnEl.innerHTML = origText; |
| btnEl.disabled = false; |
| } |
| } catch (e) { |
| console.error("Failed to delete image", e); |
| showToast("Erreur lors de la suppression", "error"); |
| btnEl.innerHTML = origText; |
| btnEl.disabled = false; |
| } |
| } |
| |
| async function loadVideos() { |
| if (!selectedStory) return; |
| |
| const grid = document.getElementById("video-gallery-grid"); |
| const section = document.getElementById("video-gallery-section"); |
| |
| // Show section |
| section.style.display = "block"; |
| |
| try { |
| const r = await fetch("/api/videos"); |
| const d = await r.json(); |
| const videos = d.videos || []; |
| |
| // Filter videos for the selected story |
| const storyVideos = videos.filter(v => |
| (v.story && v.story.toLowerCase().includes(selectedStory.id.toLowerCase())) || |
| (v.filename && v.filename.toLowerCase().includes(selectedStory.id.toLowerCase())) |
| ); |
| |
| const count = document.getElementById("video-gallery-count"); |
| count.textContent = `${storyVideos.length} vidéos`; |
| |
| if (storyVideos.length === 0) { |
| grid.innerHTML = '<div style="grid-column: 1/-1; padding: 20px; text-align: center; color: #444;">Aucune vidéo produite pour le moment.</div>'; |
| return; |
| } |
| |
| const html = storyVideos.map((vid, idx) => ` |
| <div class="gallery-item video-item" onclick="playSpecificVideo('${vid.path}', '${vid.filename.replace(/'/g, "\\'")}')"> |
| <video src="${API_BASE_URL}/api/video/play/${vid.path}#t=0.5" preload="metadata" muted></video> |
| <div class="scene-num" style="background: rgba(255,51,51,0.8); font-weight: bold; font-size: 7px;">${vid.filename.replace('.mp4', '').toUpperCase()}</div> |
| </div> |
| `).join(''); |
| |
| if (grid.innerHTML !== html) { |
| grid.innerHTML = html; |
| } |
| } catch (e) { |
| console.error("Failed to load videos", e); |
| } |
| } |
| |
| function playSpecificVideo(path, filename) { |
| currentVideoPath = path; |
| const player = document.getElementById('video-preview-player'); |
| const container = document.getElementById('video-preview-container'); |
| const successPanel = document.getElementById('video-success-panel'); |
| const fn = document.getElementById('video-filename'); |
| |
| player.src = API_BASE_URL + '/api/video/play/' + path; |
| container.style.display = 'block'; |
| |
| if (successPanel && fn) { |
| fn.textContent = filename; |
| successPanel.style.display = 'block'; |
| } |
| |
| showStep(3, true); // Jump to montage step to see the player |
| setTimeout(() => player.play(), 100); |
| } |
| |
| function selectStory(story, element, force = false) { |
| if (isTaskRunning && !force) return; |
| selectedStory = story; |
| document.querySelectorAll('.story-item').forEach(el => el.classList.remove('selected')); |
| element.classList.add('selected'); |
| |
| const scenesCount = story.total_scenes || 10; |
| document.getElementById('selected-title').innerHTML = `${story.title || story.id} <span style="font-size:14px; color:#aaa; background:#222; padding:4px 10px; border-radius:12px; margin-left:10px; vertical-align: middle;">${scenesCount} scènes</span>`; |
| document.getElementById('selected-title').style.color = ""; // Reset color |
| |
| // Toggle placeholder/controls |
| document.getElementById('selection-placeholder').querySelector('p').style.display = 'none'; |
| document.getElementById('ai-controls').style.display = 'block'; |
| |
| const btnGen = document.getElementById('btn-generate'); |
| btnGen.disabled = false; |
| btnGen.innerHTML = `📸 Générer les Images <span style="font-size:11px; opacity:0.7; margin-left:5px;">(${scenesCount})</span>`; |
| |
| document.getElementById('btn-render').disabled = false; |
| document.getElementById('btn-next-1').disabled = false; |
| |
| // Show extend button only if less than 10 scenes |
| const btnExtend = document.getElementById('btn-extend'); |
| if (scenesCount < 10) { |
| btnExtend.style.display = "block"; |
| } else { |
| btnExtend.style.display = "none"; |
| } |
| |
| // Load Gallery |
| loadGallery(); |
| loadVideos(); |
| |
| // If forced selection (auto), make sure we are on the right step if running |
| if (force && isTaskRunning) { |
| // Optional: automatically jump to visual step if generating |
| // showStep(2); |
| } |
| } |
| |
| async function extendTo10Scenes() { |
| if (!selectedStory || isTaskRunning) return; |
| if (!(await customConfirm("Voulez-vous que l'IA prolonge cette histoire jusqu'à 10 scènes ?"))) return; |
| |
| const aiMode = document.getElementById('ai-engine-select').value; |
| setButtonLoading('btn-extend', true, `⏳ Écriture en cours (${aiMode.toUpperCase()})...`); |
| isTaskRunning = true; |
| |
| try { |
| // 1. Charger le contenu actuel |
| const rContent = await fetch(`/api/stories/content/${selectedStory.id.replace(/ /g, '%20')}?t=${Date.now()}`); |
| const dContent = await rContent.json(); |
| if (dContent.status !== "success") throw new Error("Impossible de charger l'histoire."); |
| |
| let safeContent = dContent.content; |
| if (safeContent.length > 5000) safeContent = safeContent.slice(0, 5000); |
| |
| // 2. Demander à Gemini |
| const prompt = `Voici le script de ma vidéo actuelle :\n\n${safeContent}\n\nContinue cette histoire pour atteindre exactement 10 scènes au total. L'histoire doit obligatoirement avoir une fin tragique ou mystérieuse à la scène 10.\n\nPour CHAQUE nouvelle scène, tu DOIS respecter scrupuleusement ce format :\n## Scène X : [Titre de la scène]\n**Visual Prompt** : [Description visuelle détaillée style dark anime]\nNarration : "Texte court et percutant (15-20 mots max) que l'IA va lire"\n\nNe renvoie QUE la SUITE du texte en Markdown (à partir de la scène suivante), sans introduction, sans conclusion et sans aucun message supplémentaire. NE RÉPÈTE PAS les scènes existantes.`; |
| |
| const rGemini = await fetch("/api/gemini/ask", { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({"prompt": prompt, "mode": aiMode}) |
| }); |
| |
| const dGemini = await rGemini.json(); |
| if (dGemini.status !== "success") throw new Error(dGemini.response || "Erreur AI."); |
| |
| let newText = dGemini.response.replace(/^```[a-zA-Z]*\n/, "").replace(/```$/, "").trim(); |
| const finalContent = safeContent.trim() + "\n\n" + newText; |
| |
| // 3. Normaliser (SMART FIX) |
| const rNorm = await fetch("/api/stories/normalize_ai", { |
| method: "POST", |
| headers: {"Content-Type":"application/json"}, |
| body: JSON.stringify({ content: finalContent, mode: aiMode }) |
| }); |
| const dNorm = await rNorm.json(); |
| const contentToSave = dNorm.status === "success" ? dNorm.normalized.trim() : finalContent; |
| |
| // 4. Sauvegarder |
| const rSave = await fetch("/api/stories/save", { |
| method: "POST", |
| headers: {"Content-Type":"application/json"}, |
| body: JSON.stringify({ content: contentToSave, path: dContent.path || selectedStory.id }) |
| }); |
| const dSave = await rSave.json(); |
| if (dSave.status !== "success") throw new Error("Erreur lors de la sauvegarde."); |
| |
| // RIGUEUR : Mise à jour immédiate de l'état local |
| selectedStory.image_count = 10; |
| |
| // Rafraîchir l'affichage du titre et des boutons sans forcer un clic utilisateur |
| const scenesCount = 10; |
| document.getElementById('selected-title').innerHTML = `${selectedStory.title || selectedStory.id} <span style="font-size:14px; color:#aaa; background:#222; padding:4px 10px; border-radius:12px; margin-left:10px; vertical-align: middle;">${scenesCount} scènes</span>`; |
| |
| const btnGen = document.getElementById('btn-generate'); |
| btnGen.innerHTML = `📸 Générer les Images <span style="font-size:11px; opacity:0.7; margin-left:5px;">(${scenesCount})</span>`; |
| document.getElementById('btn-extend').style.display = "none"; |
| |
| await customAlert("Succès ! L'histoire a été prolongée et normalisée à 10 scènes."); |
| loadStories(); // Met à jour la liste latérale en arrière-plan |
| } catch (e) { |
| await customAlert("Erreur : " + e.message, "ERROR"); |
| } finally { |
| setButtonLoading('btn-extend', false); |
| isTaskRunning = false; |
| } |
| } |
| |
| async function generateImages() { |
| if (!selectedStory || isTaskRunning) return; |
| |
| const forceRegen = document.getElementById('force-regen').checked; |
| const artStyle = document.getElementById('artistic-style-select').value; |
| |
| setButtonsLoading(['btn-generate', 'btn-cartoon', 'btn-render'], true, ["Génération..."]); |
| isTaskRunning = true; |
| showLoading("Génération d'images", "Création des visuais..."); |
| |
| try { |
| await fetch("/api/stories/launch", { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({ |
| story_path: selectedStory.path, |
| story_id: selectedStory.id, |
| regenerate_all: forceRegen, |
| config: { images_only: true, art_profile: artStyle, ai_mode: document.getElementById("ai-mode-select")?.value || "hybrid" } |
| }) |
| }); |
| } catch(e) { |
| await customAlert("Erreur: " + e.message, "ERROR"); |
| isTaskRunning = false; |
| setButtonsLoading(['btn-generate', 'btn-cartoon', 'btn-render'], false); |
| resetButtons(); |
| } |
| } |
| |
| async function generateCartoon() { |
| if (!selectedStory || isTaskRunning) return; |
| const seconds = document.getElementById('cartoon-seconds').value || 5; |
| const artStyle = document.getElementById('artistic-style-select').value; |
| |
| setButtonsLoading(['btn-generate', 'btn-cartoon', 'btn-render'], true, [null, null, "⏳ Démarrage..."]); |
| isTaskRunning = true; |
| showLoading("Génération Animation", "En cours..."); |
| |
| try { |
| await fetch("/api/stories/launch", { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({ |
| story_path: selectedStory.path, |
| story_id: selectedStory.id, |
| config: { images_only: true, image_gen_mode: "wan2", duration: parseInt(seconds), art_profile: artStyle, ai_mode: document.getElementById("ai-mode-select")?.value || "hybrid" } |
| }) |
| }); |
| } catch(e) { |
| await customAlert("Erreur: " + e.message, "ERROR"); |
| isTaskRunning = false; |
| setButtonsLoading(['btn-generate', 'btn-cartoon', 'btn-render'], false); |
| resetButtons(); |
| } |
| } |
| |
| async function renderVideo() { |
| if (!selectedStory || isTaskRunning) return; |
| |
| const artStyle = document.getElementById('artistic-style-select').value; |
| setButtonsLoading(['btn-render', 'btn-generate', 'btn-cartoon'], true, ["⏳ Démarrage..."]); |
| isTaskRunning = true; |
| showLoading("Montage Vidéo", "Assemblage en cours..."); |
| |
| try { |
| await fetch("/api/stories/launch", { |
| method: "POST", |
| headers: {"Content-Type": "application/json"}, |
| body: JSON.stringify({ |
| story_path: selectedStory.path, |
| story_id: selectedStory.id, |
| config: { remix_only: true, sync_montage: true, art_profile: artStyle, ai_mode: document.getElementById("ai-mode-select")?.value || "hybrid" } |
| }) |
| }); |
| } catch(e) { |
| await customAlert("Erreur: " + e.message, "ERROR"); |
| isTaskRunning = false; |
| setButtonsLoading(['btn-render', 'btn-generate', 'btn-cartoon'], false); |
| resetButtons(); |
| } |
| } |
| |
| async function restartEngine() { |
| if (!(await customConfirm('ATTENTION: Cela va tuer TOUS les processus de rendu en cours (FFMPEG, etc.) et réinitialiser l\'état du moteur.\n\nContinuer ?'))) return; |
| |
| showLoading("REDÉMARRAGE...", "Kill & Reset en cours..."); |
| |
| try { |
| const resKill = await fetch('/api/engine/kill_all', { method: 'POST' }); |
| const dataKill = await resKill.json(); |
| |
| await fetch('/api/engine/reset', { method: 'POST' }); |
| |
| await customAlert(`Moteur réinitialisé avec succès.\nProcessus arrêtés : ${dataKill.killed_count}`); |
| window.location.reload(); |
| } catch(e) { |
| await customAlert("Erreur lors du redémarrage : " + e.message, "ERROR"); |
| } |
| hideLoading(); |
| } |
| |
| function resetButtons() { |
| if (!selectedStory) return; |
| const scenesCount = selectedStory.total_scenes || 10; |
| const btnGen = document.getElementById('btn-generate'); |
| btnGen.innerHTML = `📸 Générer les Images <span style="font-size:11px; opacity:0.7; margin-left:5px;">(${scenesCount})</span>`; |
| btnGen.disabled = false; |
| |
| const btnCartoon = document.getElementById('btn-cartoon'); |
| btnCartoon.innerHTML = `🎥 Générer Dessin Animé`; |
| btnCartoon.disabled = false; |
| |
| document.getElementById('btn-render').textContent = "🎬 Faire le Montage Vidéo"; |
| document.getElementById('btn-render').disabled = false; |
| } |
| |
| async function updateLogs() { |
| try { |
| const r = await fetch("/api/logs"); |
| const d = await r.json(); |
| const logs = d.logs || []; |
| const container = document.getElementById("log-content"); |
| |
| // Only update if we have logs and they are different |
| const currentContent = container.innerHTML; |
| let newHtml = ""; |
| |
| logs.forEach(line => { |
| let type = "info"; |
| if (line.includes("✅") || line.includes("Succès")) type = "success"; |
| if (line.includes("❌") || line.includes("Erreur") || line.includes("Error")) type = "error"; |
| if (line.includes("⚠️") || line.includes("WARN")) type = "warning"; |
| if (line.includes("🧠") || line.includes("Expert") || line.includes("AI")) type = "ai"; |
| |
| newHtml += `<div class="log-line ${type}">${line}</div>`; |
| }); |
| |
| if (newHtml && newHtml !== container.getAttribute('data-last-html')) { |
| container.innerHTML = newHtml; |
| container.setAttribute('data-last-html', newHtml); |
| container.scrollTop = container.scrollHeight; |
| } |
| } catch (e) {} |
| } |
| |
| // Monitoring system |
| setInterval(async () => { |
| try { |
| const r = await fetch("/api/status"); |
| const status = await r.json(); |
| |
| const rSys = await fetch("/api/system"); |
| const sys = await rSys.json(); |
| |
| // Update Engine Tags - HF is always active |
| const hfTag = document.getElementById('tag-hf'); |
| if (hfTag) hfTag.classList.add('active'); |
| |
| const isRunning = status.status === "running" || (status.progress > 0 && status.progress < 100); |
| |
| // RIGUEUR : Libération forcée si le moteur est en attente |
| if (status.step.toLowerCase().includes("attente") || status.step.toLowerCase().includes("terminé") || status.step.toLowerCase().includes("prêt")) { |
| isTaskRunning = false; |
| hideGlobalSpinner(); |
| } else { |
| isTaskRunning = isRunning; |
| } |
| |
| if (status.progress >= 100) { |
| isTaskRunning = false; |
| hideGlobalSpinner(); |
| } |
| |
| // Extract step info for button text (e.g. "Génération 1/10") |
| let countText = ""; |
| const match = status.step.match(/(\d+\/\d+)/); |
| if (match) { |
| countText = " (" + match[1] + ")"; |
| } |
| |
| const storyPrefix = status.story ? status.story.toUpperCase() + " // " : ""; |
| document.getElementById('status-step').textContent = storyPrefix + status.step; |
| document.getElementById('status-progress').textContent = status.progress + "%"; |
| |
| // Display engine info |
| if (status.core_version) { |
| const coreDisp = document.getElementById('active-engine-display'); |
| coreDisp.textContent = `Moteur : HUGGING FACE | ${status.core_version.toUpperCase()}`; |
| } |
| |
| // Update spinner progress bar |
| if (isTaskRunning && status.progress > 0) { |
| updateSpinnerProgress(status.progress); |
| } |
| |
| if (isRunning) { |
| // Désactivation sélective des boutons critiques |
| document.getElementById('btn-generate').disabled = true; |
| document.getElementById('btn-render').disabled = true; |
| |
| const loader = document.getElementById("global-master-loader"); |
| if (loader) { |
| loader.style.display = "flex"; |
| loader.classList.add('mini-mode'); |
| // On laisse le CSS gérer le background et le flou via .mini-mode |
| loader.style.background = ""; |
| loader.style.backdropFilter = ""; |
| document.getElementById("master-loader-text").textContent = status.step.toUpperCase(); |
| |
| // Add story name to the loader |
| const storyEl = document.getElementById("master-loader-story"); |
| if (storyEl) { |
| if (status.story && status.story !== "Aucune") { |
| storyEl.textContent = `PROJET : ${status.story.toUpperCase()}`; |
| storyEl.style.color = "var(--accent-red)"; |
| } else { |
| storyEl.textContent = "DARKMEDIA-X NEURAL LINK"; |
| storyEl.style.color = "#333"; |
| } |
| } |
| } |
| |
| // Auto-jump to correct step based on task |
| const stepText = status.step.toLowerCase(); |
| if (stepText.includes("génér") || stepText.includes("imag") || stepText.includes("visuel")) { |
| if (!document.getElementById('content-step2').classList.contains('active')) { |
| showStep(2, true); |
| } |
| document.getElementById('btn-generate').textContent = "⏳ GÉNÉRATION EN COURS..." + countText; |
| } else if (stepText.includes("montage") || stepText.includes("rendu") || stepText.includes("mix")) { |
| if (!document.getElementById('content-step3').classList.contains('active')) { |
| showStep(3, true); |
| } |
| document.getElementById('btn-render').textContent = "⏳ MONTAGE EN COURS..." + countText; |
| } |
| |
| const img = document.getElementById('live-preview'); |
| let url = `/api/current_frame?t=${Date.now()}`; |
| if(selectedStory) url += `&story_id=${encodeURIComponent(selectedStory.id)}`; |
| |
| img.src = url; |
| img.style.display = "block"; |
| document.getElementById('preview-placeholder').style.display = "none"; |
| |
| // Narration display logic for simple UI |
| const narrationOverlay = document.getElementById("preview-narration"); |
| if (narrationOverlay && selectedStory) { |
| // Since simple UI doesn't have the full editor, we just show the step |
| // or leave it blank if no specific text is loaded |
| narrationOverlay.textContent = ""; |
| } |
| |
| // Refresh Gallery |
| loadGallery(); |
| loadVideos(); |
| |
| } else { |
| document.getElementById('live-preview').style.display = "none"; |
| document.getElementById('preview-placeholder').style.display = "block"; |
| document.querySelectorAll('.story-item').forEach(el => el.style.pointerEvents = 'auto'); |
| |
| const loader = document.getElementById("global-master-loader"); |
| if (loader && loader.classList.contains('mini-mode')) { |
| hideLoading(); |
| } |
| |
| resetButtons(); |
| |
| // Check for video completion |
| checkVideoCompletion(); |
| } |
| } catch(e) {} |
| }, 1000); |
| |
| // Video completion checker |
| let currentVideoPath = null; |
| async function checkVideoCompletion() { |
| try { |
| const r = await fetch("/api/videos"); |
| const d = await r.json(); |
| const videos = d.videos || []; |
| |
| if (selectedStory && videos.length > 0) { |
| // Get most recent video for this story |
| const storyVideo = videos.find(v => |
| (v.story && v.story.toLowerCase().includes(selectedStory.id.toLowerCase())) || |
| (v.filename && v.filename.toLowerCase().includes(selectedStory.id.toLowerCase())) |
| ); |
| |
| if (storyVideo) { |
| currentVideoPath = storyVideo.path; |
| showVideoSuccess(storyVideo.filename || "Video ready", currentVideoPath); |
| loadVideos(); |
| } |
| } |
| } catch(e) {} |
| } |
| |
| function showVideoSuccess(filename, path) { |
| const panel = document.getElementById('video-success-panel'); |
| const fn = document.getElementById('video-filename'); |
| fn.textContent = filename; |
| panel.style.display = 'block'; |
| } |
| |
| function playVideo() { |
| const player = document.getElementById('video-preview-player'); |
| const container = document.getElementById('video-preview-container'); |
| if (currentVideoPath) { |
| player.src = API_BASE_URL + '/api/video/play/' + currentVideoPath; |
| container.style.display = 'block'; |
| player.play(); |
| } |
| } |
| |
| function downloadVideo() { |
| if (currentVideoPath) { |
| window.open(API_BASE_URL + '/api/video/play/' + currentVideoPath, '_blank'); |
| } |
| } |
| |
| function uploadToYoutube() { |
| customAlert("Fonctionnalité YouTube: Utilisez 'npm run sync:youtube' dans le terminal pour uploader.", "INFO"); |
| } |
| |
| // Art Style Preview |
| let artStyleCache = {}; |
| |
| function previewArtStyle(style) { |
| const previewDiv = document.getElementById('art-style-preview'); |
| const canvas = document.getElementById('art-preview-canvas'); |
| const ctx = canvas.getContext('2d'); |
| |
| if (style === 'none') { |
| previewDiv.style.display = 'none'; |
| return; |
| } |
| |
| // Use first image from gallery as preview source |
| if (currentGalleryImages.length === 0) { |
| previewDiv.style.display = 'block'; |
| canvas.width = 200; |
| canvas.height = 200; |
| ctx.fillStyle = '#111'; |
| ctx.fillRect(0, 0, 200, 200); |
| ctx.fillStyle = '#666'; |
| ctx.font = '12px monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText('Sélectionnez une histoire', 100, 100); |
| ctx.fillText('avec des images d\'abord', 100, 120); |
| return; |
| } |
| |
| const img = new Image(); |
| img.crossOrigin = 'anonymous'; |
| img.onload = function() { |
| // Scale down for preview |
| const maxW = 300, maxH = 200; |
| let w = img.width, h = img.height; |
| if (w > maxW) { h = h * maxW / w; w = maxW; } |
| if (h > maxH) { w = w * maxH / h; h = maxH; } |
| canvas.width = w; |
| canvas.height = h; |
| ctx.drawImage(img, 0, 0, w, h); |
| |
| // Apply style filter |
| applyArtStyle(ctx, w, h, style); |
| previewDiv.style.display = 'block'; |
| }; |
| img.onerror = function() { |
| previewDiv.style.display = 'block'; |
| canvas.width = 200; |
| canvas.height = 200; |
| ctx.fillStyle = '#111'; |
| ctx.fillRect(0, 0, 200, 200); |
| ctx.fillStyle = '#ff4444'; |
| ctx.font = '12px monospace'; |
| ctx.textAlign = 'center'; |
| ctx.fillText('Erreur de chargement', 100, 100); |
| }; |
| img.src = API_BASE_URL + currentGalleryImages[0].url; |
| } |
| |
| function applyArtStyle(ctx, w, h, style) { |
| const imageData = ctx.getImageData(0, 0, w, h); |
| const data = imageData.data; |
| |
| switch(style) { |
| case 'oil_paint': |
| // Simplified oil paint: reduce colors + slight blur effect |
| for (let i = 0; i < data.length; i += 4) { |
| data[i] = Math.floor(data[i] / 32) * 32; |
| data[i+1] = Math.floor(data[i+1] / 32) * 32; |
| data[i+2] = Math.floor(data[i+2] / 32) * 32; |
| } |
| break; |
| case 'charcoal': |
| // Grayscale + high contrast |
| for (let i = 0; i < data.length; i += 4) { |
| const avg = (data[i] + data[i+1] + data[i+2]) / 3; |
| const val = avg > 128 ? 255 : 0; |
| data[i] = data[i+1] = data[i+2] = val; |
| } |
| break; |
| case 'sketch': |
| // Light grayscale |
| for (let i = 0; i < data.length; i += 4) { |
| const avg = (data[i] + data[i+1] + data[i+2]) / 3; |
| const val = Math.min(255, avg * 1.5 + 50); |
| data[i] = data[i+1] = data[i+2] = val; |
| } |
| break; |
| case 'vintage': |
| // Warm tones + slight desaturation |
| for (let i = 0; i < data.length; i += 4) { |
| data[i] = Math.min(255, data[i] * 1.2 + 20); |
| data[i+1] = Math.min(255, data[i+1] * 1.0 + 10); |
| data[i+2] = Math.min(255, data[i+2] * 0.8); |
| } |
| break; |
| case 'night_vision': |
| // Green tint |
| for (let i = 0; i < data.length; i += 4) { |
| const avg = (data[i] + data[i+1] + data[i+2]) / 3; |
| data[i] = Math.min(255, avg * 0.3); |
| data[i+1] = Math.min(255, avg * 1.2); |
| data[i+2] = Math.min(255, avg * 0.3); |
| } |
| break; |
| case 'pixel_art': |
| // Pixelate: sample every 8th pixel |
| const blockSize = 8; |
| for (let y = 0; y < h; y += blockSize) { |
| for (let x = 0; x < w; x += blockSize) { |
| const idx = (y * w + x) * 4; |
| const r = data[idx], g = data[idx+1], b = data[idx+2]; |
| for (let dy = 0; dy < blockSize && y+dy < h; dy++) { |
| for (let dx = 0; dx < blockSize && x+dx < w; dx++) { |
| const i = ((y+dy) * w + (x+dx)) * 4; |
| data[i] = r; data[i+1] = g; data[i+2] = b; |
| } |
| } |
| } |
| } |
| break; |
| case 'vhs_static': |
| // Add noise + slight color shift |
| for (let i = 0; i < data.length; i += 4) { |
| const noise = (Math.random() - 0.5) * 40; |
| data[i] = Math.min(255, Math.max(0, data[i] + noise + 10)); |
| data[i+1] = Math.min(255, Math.max(0, data[i+1] + noise)); |
| data[i+2] = Math.min(255, Math.max(0, data[i+2] + noise - 10)); |
| } |
| break; |
| } |
| |
| ctx.putImageData(imageData, 0, 0); |
| } |
| |
| async function applyArtStyleToStory() { |
| if (!selectedStory) return; |
| const style = document.getElementById('artistic-style-select').value; |
| if (style === 'none') { |
| showToast("Sélectionnez un style d'abord", "warning"); |
| return; |
| } |
| |
| const btn = document.getElementById('btn-apply-style'); |
| const origText = btn.innerHTML; |
| btn.innerHTML = '⏳ Traitement en cours...'; |
| btn.disabled = true; |
| |
| try { |
| const r = await fetch("/api/stories/apply_art_style", { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ story_id: selectedStory.id, style: style }) |
| }); |
| const d = await r.json(); |
| |
| if (d.status === "success") { |
| showToast(d.message, "success"); |
| loadGallery(); // Refresh gallery to show styled images |
| } else { |
| showToast("Erreur: " + (d.message || "Unknown"), "error"); |
| } |
| } catch (e) { |
| showToast("Erreur de connexion", "error"); |
| } |
| |
| btn.innerHTML = origText; |
| btn.disabled = false; |
| } |
| |
| // Keyboard navigation for carousel |
| document.addEventListener('keydown', (e) => { |
| const overlay = document.getElementById("carousel-overlay"); |
| if (overlay.style.display === "flex") { |
| if (e.key === "Escape") closeCarousel(); |
| if (e.key === "ArrowLeft") carouselPrev(); |
| if (e.key === "ArrowRight") carouselNext(); |
| } |
| }); |
| |
| // Init |
| loadStories(); |
| </script> |
| </body> |
| </html> |
|
|