Spaces:
Running
Running
| {% extends "layout.html" %} | |
| {% block title %}AI Auto-Cut Script & Hook Optimizer - Pum's Toolkit{% endblock %} | |
| {% block styles %} | |
| <style> | |
| .hook-hero { | |
| background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%); | |
| border-radius: var(--radius-xl); | |
| padding: 24px 30px; | |
| color: #fff; | |
| margin-bottom: 24px; | |
| } | |
| .hook-card { | |
| border-left: 4px solid var(--color-primary); | |
| background: #fffef2; | |
| border-radius: var(--radius-md); | |
| padding: 16px; | |
| margin-bottom: 12px; | |
| box-shadow: var(--shadow-card); | |
| border-top: 1px solid var(--color-border); | |
| border-right: 1px solid var(--color-border); | |
| border-bottom: 1px solid var(--color-border); | |
| } | |
| .hook-card.dramatic { border-left-color: #ec4899; } | |
| .hook-card.curious { border-left-color: #f59e0b; } | |
| .hook-card.reverse { border-left-color: #10b981; } | |
| .script-pane { | |
| background: #f8fafc; | |
| border: 1px solid var(--color-border); | |
| border-radius: var(--radius-lg); | |
| padding: 20px; | |
| min-height: 380px; | |
| } | |
| </style> | |
| {% endblock %} | |
| {% block content %} | |
| <div id="loaderOverlay" class="loader-overlay" style="display: none;"> | |
| <div class="spinner-pro"></div> | |
| <p class="mt-3 fw-bold" style="color: var(--color-primary)" id="loaderText">Đang biên tập và tối ưu kịch bản...</p> | |
| </div> | |
| <div class="hook-hero"> | |
| <h2><i class="fas fa-cut me-2"></i>AI Auto-Cut Script & Hook Generator</h2> | |
| <p class="mb-0">Tối ưu hóa nhịp điệu kịch bản, chèn gợi ý phân cảnh B-Roll và tự động sáng tạo tiêu đề giật gân giữ chân người xem.</p> | |
| </div> | |
| <div class="row g-4"> | |
| <!-- Left form inputs --> | |
| <div class="col-lg-5"> | |
| <div class="card card-custom h-100"> | |
| <div class="card-body p-4 d-flex flex-column justify-content-between"> | |
| <div> | |
| <h5 class="fw-bold mb-4"><i class="fas fa-keyboard text-danger me-2"></i>Kịch bản nháp</h5> | |
| <div class="mb-3"> | |
| <label class="form-label fw-bold">Dán kịch bản nháp / Ý tưởng thô <span class="text-danger">*</span></label> | |
| <textarea id="draftScript" class="form-control" rows="8" placeholder="Dán kịch bản hoặc lời thoại nháp của bạn tại đây..."></textarea> | |
| </div> | |
| <div class="row g-2 mb-3"> | |
| <div class="col-12"> | |
| <label class="form-label fw-bold">Nhịp điệu nói (Pace)</label> | |
| <select id="scriptPace" class="form-select form-select-sm"> | |
| <option value="Nhanh, dồn dập (TikTok Shorts style 150 từ/phút)" selected>Nhanh, dồn dập (Shorts/TikTok)</option> | |
| <option value="Vừa phải, trung lập (YouTube Explainer style 130 từ/phút)">Vừa phải (YouTube dài)</option> | |
| <option value="Sâu lắng, chia sẻ chậm rãi (100 từ/phút)">Chậm rãi, sâu lắng (Podcast/Tâm sự)</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label fw-bold"><i class="fab fa-google"></i> Mô hình Gemini</label> | |
| <select id="geminiModel" class="form-select form-select-sm"> | |
| <option value="">Đang tải...</option> | |
| </select> | |
| <div id="modelQuota" class="small text-muted mt-1"></div> | |
| </div> | |
| </div> | |
| <button id="btnOptimize" class="btn-primary-custom py-3 mt-3" style="background: linear-gradient(135deg, #ef4444, #b91c1c)"> | |
| <i class="fas fa-wand-magic-sparkles me-2"></i>BƯỚC 1: TỐI ƯU KỊCH BẢN | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right output --> | |
| <div class="col-lg-7"> | |
| <div class="card card-custom h-100 d-flex flex-column"> | |
| <div class="card-body p-4 d-flex flex-column flex-grow-1"> | |
| <!-- Tabs for Hooks vs Script --> | |
| <ul class="nav nav-pills mb-3" id="scriptTabs" role="tablist"> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link active" id="hooks-tab" data-bs-toggle="pill" data-bs-target="#hooks-content" type="button" role="tab"><i class="fas fa-anchor me-1"></i> Tối ưu Hook 3s</button> | |
| </li> | |
| <li class="nav-item" role="presentation"> | |
| <button class="nav-link" id="editor-tab" data-bs-toggle="pill" data-bs-target="#editor-content" type="button" role="tab"><i class="fas fa-scroll me-1"></i> Kịch bản Auto-Cut</button> | |
| </li> | |
| </ul> | |
| <div class="tab-content flex-grow-1 d-flex flex-column" id="scriptTabContents"> | |
| <!-- Hooks Tab --> | |
| <div class="tab-pane fade show active" id="hooks-content" role="tabpanel"> | |
| <div id="hooksList"> | |
| <div class="text-center text-muted py-5"> | |
| <i class="fas fa-bullhorn fa-3x mb-3 text-disabled"></i> | |
| <p class="mb-0">Sau khi tối ưu kịch bản ở Bước 1, 3 kiểu Hook giữ chân người xem sẽ hiển thị ở đây.</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Editor Tab --> | |
| <div class="tab-pane fade flex-grow-1 d-flex flex-column" id="editor-content" role="tabpanel"> | |
| <!-- Stopwatch stats --> | |
| <div class="d-flex align-items-center justify-content-between p-3 mb-3" style="background: #fdf2f2; border: 1px solid #fee2e2; border-radius: 8px;"> | |
| <div class="d-flex align-items-center gap-2"> | |
| <i class="fas fa-stopwatch text-danger fa-lg"></i> | |
| <div> | |
| <div class="fw-bold text-dark">Thời lượng ước lượng: <span id="estimateSec" class="text-danger">0</span> giây</div> | |
| <div class="small text-muted" id="wordCount">Tổng số từ: 0 từ</div> | |
| </div> | |
| </div> | |
| <button id="copyScriptBtn" class="btn btn-sm btn-outline-danger"><i class="fas fa-copy me-1"></i> Sao chép</button> | |
| </div> | |
| <!-- Editable script textbox --> | |
| <div class="script-pane flex-grow-1 d-flex flex-column"> | |
| <label class="form-label fw-bold text-muted small"><i class="fas fa-edit me-1"></i>Kịch bản tối ưu (Có thể chỉnh sửa trực tiếp)</label> | |
| <textarea id="optimizedScriptText" class="form-control flex-grow-1" style="font-family: 'Courier New', Courier, monospace; line-height: 1.6; font-size: 0.92rem; border: none; background: transparent; resize: none; outline: none;"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="errorArea" class="mt-4 d-none"> | |
| <div class="alert alert-danger d-flex align-items-center gap-2 mb-0"> | |
| <i class="fas fa-exclamation-circle"></i> | |
| <span id="errorMsg"></span> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| let geminiModelList = []; | |
| let savedHooks = {}; | |
| // Load models | |
| async function loadModels() { | |
| try { | |
| const res = await fetch('/api/gemini-models'); | |
| geminiModelList = await res.json(); | |
| const sel = document.getElementById('geminiModel'); | |
| sel.innerHTML = ''; | |
| geminiModelList.forEach(m => { | |
| const opt = document.createElement('option'); | |
| opt.value = m.id; | |
| opt.textContent = `${m.name} — ${m.remaining > 0 ? '✓ Còn ' + m.remaining : '✗ Hết'}`; | |
| if (m.remaining === 0) opt.disabled = true; | |
| sel.appendChild(opt); | |
| }); | |
| updateQuota(); | |
| $('#geminiModel').select2({ width: '100%' }); | |
| } catch(e) { console.error(e); } | |
| } | |
| function updateQuota() { | |
| const m = geminiModelList.find(x => x.id === document.getElementById('geminiModel').value); | |
| const el = document.getElementById('modelQuota'); | |
| if (m) el.innerHTML = m.remaining > 0 | |
| ? `<span style="color:var(--color-primary)"><i class="fas fa-check-circle"></i> Còn <b>${m.remaining}</b> request</span>` | |
| : `<span style="color:#dc3545"><i class="fas fa-times-circle"></i> Đã hết quota</span>`; | |
| } | |
| document.getElementById('geminiModel').addEventListener('change', updateQuota); | |
| loadModels(); | |
| function showError(msg) { document.getElementById('errorMsg').textContent = msg; document.getElementById('errorArea').classList.remove('d-none'); } | |
| function clearError() { document.getElementById('errorArea').classList.add('d-none'); } | |
| // Estimate word count and adjust seconds dynamically | |
| function recalculateDuration() { | |
| const text = document.getElementById('optimizedScriptText').value.trim(); | |
| const wordCount = text ? text.split(/\s+/).length : 0; | |
| document.getElementById('wordCount').textContent = `Tổng số từ: ${wordCount} từ`; | |
| // average reading speed: 130 words per minute (2.16 words per second) | |
| const estSec = Math.round(wordCount / 2.16); | |
| document.getElementById('estimateSec').textContent = estSec; | |
| } | |
| document.getElementById('optimizedScriptText').addEventListener('input', recalculateDuration); | |
| // Step 1: Optimize script | |
| document.getElementById('btnOptimize').addEventListener('click', async () => { | |
| const draft = document.getElementById('draftScript').value.trim(); | |
| if (!draft) { showError('Vui lòng nhập kịch bản nháp!'); return; } | |
| const loader = document.getElementById('loaderOverlay'); | |
| loader.style.display = 'flex'; | |
| clearError(); | |
| try { | |
| const res = await fetch('/api/script-hook-generator/optimize', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| draft_script: draft, | |
| pace: document.getElementById('scriptPace').value, | |
| gemini_model: document.getElementById('geminiModel').value | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.error || !data.success) { showError(data.error || 'Lỗi tối ưu kịch bản.'); return; } | |
| savedHooks = data.hooks; | |
| // Render hooks list | |
| const container = document.getElementById('hooksList'); | |
| container.innerHTML = ''; | |
| const types = { | |
| dramatic: { label: 'Kịch tính (Dramatic)', class: 'dramatic', icon: 'fa-bolt text-danger' }, | |
| curious: { label: 'Gây tò mò (Curious)', class: 'curious', icon: 'fa-question-circle text-warning' }, | |
| reverse: { label: 'Đảo ngược thực tế (Reverse)', class: 'reverse', icon: 'fa-undo text-success' } | |
| }; | |
| for (const [key, details] of Object.entries(types)) { | |
| const hookText = savedHooks[key] || ''; | |
| const div = document.createElement('div'); | |
| div.className = `hook-card ${details.class}`; | |
| div.innerHTML = ` | |
| <div class="d-flex justify-content-between align-items-start mb-2"> | |
| <span class="fw-bold small text-muted"><i class="fas ${details.icon} me-1"></i>${details.label}</span> | |
| <button class="btn btn-xs btn-outline-danger py-0 px-2" style="font-size:0.68rem;" onclick="injectHook('${key}')"><i class="fas fa-plus"></i> Ghép vào kịch bản</button> | |
| </div> | |
| <p class="mb-0 fw-bold fs-6 text-dark">"${hookText}"</p> | |
| `; | |
| container.appendChild(div); | |
| } | |
| // Set optimized script text | |
| document.getElementById('optimizedScriptText').value = data.optimized_script; | |
| document.getElementById('estimateSec').textContent = data.duration_seconds; | |
| recalculateDuration(); | |
| // Open Hooks tab | |
| const hooksTabTrigger = new bootstrap.Tab(document.getElementById('hooks-tab')); | |
| hooksTabTrigger.show(); | |
| await loadModels(); | |
| } catch(e) { showError('Lỗi kết nối.'); } | |
| finally { loader.style.display = 'none'; } | |
| }); | |
| window.injectHook = function(key) { | |
| const hookText = savedHooks[key] || ''; | |
| const currentScript = document.getElementById('optimizedScriptText').value; | |
| // Inject at the beginning | |
| document.getElementById('optimizedScriptText').value = `[HOOK]\n${hookText}\n\n${currentScript}`; | |
| recalculateDuration(); | |
| // Switch to editor tab | |
| const editorTabTrigger = new bootstrap.Tab(document.getElementById('editor-tab')); | |
| editorTabTrigger.show(); | |
| }; | |
| // Copy script | |
| document.getElementById('copyScriptBtn').addEventListener('click', () => { | |
| const text = document.getElementById('optimizedScriptText').value; | |
| navigator.clipboard.writeText(text).then(() => { | |
| const btn = document.getElementById('copyScriptBtn'); | |
| const orig = btn.innerHTML; | |
| btn.innerHTML = '<i class="fas fa-check"></i> Đã sao chép!'; | |
| setTimeout(() => btn.innerHTML = orig, 2000); | |
| }); | |
| }); | |
| </script> | |
| {% endblock %} | |