ndmhung6's picture
feat: implement 4 new AI creator tools for youtube and tiktok with interactive mockups and planners
6640988
Raw
History Blame Contribute Delete
14.1 kB
{% 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 %}