ai_v/static/js/video.js

337 lines
13 KiB
JavaScript
Raw Normal View History

document.addEventListener('DOMContentLoaded', () => {
lucide.createIcons();
const submitBtn = document.getElementById('submitBtn');
const pointsDisplay = document.getElementById('pointsDisplay');
const headerPoints = document.getElementById('headerPoints');
const resultVideo = document.getElementById('resultVideo');
const finalWrapper = document.getElementById('finalWrapper');
const placeholder = document.getElementById('placeholder');
const statusInfo = document.getElementById('statusInfo');
const promptInput = document.getElementById('promptInput');
const fileInput = document.getElementById('fileInput');
const imagePreview = document.getElementById('imagePreview');
const modelSelect = document.getElementById('modelSelect');
const ratioSelect = document.getElementById('ratioSelect');
const promptTemplates = document.getElementById('promptTemplates');
const enhancePrompt = document.getElementById('enhancePrompt');
// 历史记录相关
const historyList = document.getElementById('historyList');
const historyCount = document.getElementById('historyCount');
const historyEmpty = document.getElementById('historyEmpty');
const loadMoreBtn = document.getElementById('loadMoreBtn');
let uploadedImageUrl = null;
let historyPage = 1;
let isLoadingHistory = false;
// 初始化配置
async function initConfig() {
try {
const r = await fetch('/api/config');
if (!r.ok) throw new Error('API 响应失败');
const d = await r.json();
if (d.video_models && d.video_models.length > 0) {
modelSelect.innerHTML = d.video_models.map(m =>
`<option value="${m.value}">${m.label} (${m.cost}积分)</option>`
).join('');
}
if (d.video_prompts && d.video_prompts.length > 0) {
promptTemplates.innerHTML = d.video_prompts.map(p =>
`<button type="button" class="px-3 py-1.5 bg-slate-100 hover:bg-slate-200 text-slate-600 rounded-lg text-[9px] font-bold transition-all whitespace-nowrap" onclick="applyTemplate('${p.value.replace(/'/g, "\\'")}')">${p.label}</button>`
).join('');
}
} catch (e) {
console.error('加载系统配置失败:', e);
}
}
// 载入历史记录
async function loadHistory(page = 1, append = false) {
if (isLoadingHistory) return;
isLoadingHistory = true;
try {
const r = await fetch(`/api/history?page=${page}&per_page=10`);
const d = await r.json();
// 过滤出有视频的记录
const videoRecords = d.history.filter(item => {
const urls = item.urls || [];
return urls.some(u => u.type === 'video' || (typeof u === 'string' && u.endsWith('.mp4')));
});
if (videoRecords.length > 0) {
const html = videoRecords.map(item => {
const videoObj = item.urls.find(u => u.type === 'video') || { url: item.urls[0] };
const videoUrl = typeof videoObj === 'string' ? videoObj : videoObj.url;
return `
<div class="group bg-white rounded-2xl p-3 border border-slate-100 hover:border-indigo-200 hover:shadow-md transition-all cursor-pointer animate-in fade-in slide-in-from-right-2" onclick="playHistoryVideo('${videoUrl}')">
<div class="relative aspect-video bg-black rounded-xl overflow-hidden mb-2">
<div class="absolute inset-0 flex items-center justify-center bg-slate-900/40 opacity-0 group-hover:opacity-100 transition-opacity">
<i data-lucide="play-circle" class="w-10 h-10 text-white"></i>
</div>
<video src="${videoUrl}" class="w-full h-full object-cover"></video>
</div>
<div class="space-y-1">
<p class="text-[10px] text-slate-600 font-medium line-clamp-2 leading-relaxed">${item.prompt || '无描述'}</p>
<div class="flex items-center justify-between pt-1">
<span class="text-[8px] text-slate-300 font-black uppercase tracking-tighter">${item.created_at}</span>
<div class="flex gap-1">
<button onclick="event.stopPropagation(); downloadUrl('${videoUrl}')" class="p-1 hover:text-indigo-600 transition-colors">
<i data-lucide="download" class="w-3 h-3"></i>
</button>
</div>
</div>
</div>
</div>
`;
}).join('');
if (append) {
historyList.insertAdjacentHTML('beforeend', html);
} else {
historyList.innerHTML = html;
}
historyEmpty.classList.add('hidden');
historyCount.innerText = videoRecords.length + (append ? parseInt(historyCount.innerText) : 0);
if (d.history.length >= 10) {
loadMoreBtn.classList.remove('hidden');
} else {
loadMoreBtn.classList.add('hidden');
}
} else if (!append) {
historyEmpty.classList.remove('hidden');
historyList.innerHTML = '';
}
lucide.createIcons();
} catch (e) {
console.error('加载历史失败:', e);
} finally {
isLoadingHistory = false;
}
}
window.playHistoryVideo = (url) => {
showVideo(url);
window.scrollTo({ top: 0, behavior: 'smooth' });
};
window.downloadUrl = (url) => {
// 使用后端代理强制下载,绕过跨域限制
showToast('开始下载...', 'info');
const filename = `vision-video-${Date.now()}.mp4`;
const proxyUrl = `/api/download_proxy?url=${encodeURIComponent(url)}&filename=${filename}`;
// 创建隐藏的 iframe 触发下载,相比 a 标签兼容性更好
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = proxyUrl;
document.body.appendChild(iframe);
// 1分钟后清理 iframe
setTimeout(() => document.body.removeChild(iframe), 60000);
};
loadMoreBtn.onclick = () => {
historyPage++;
loadHistory(historyPage, true);
};
initConfig();
loadHistory();
window.applyTemplate = (text) => {
promptInput.value = text;
showToast('已应用提示词模板', 'success');
};
// 上传图片逻辑
fileInput.onchange = async (e) => {
const files = e.target.files;
if (files.length === 0) return;
const formData = new FormData();
formData.append('images', files[0]);
try {
submitBtn.disabled = true;
const r = await fetch('/api/upload', { method: 'POST', body: formData });
const d = await r.json();
if (d.urls && d.urls.length > 0) {
uploadedImageUrl = d.urls[0];
imagePreview.innerHTML = `
<div class="relative w-20 h-20 rounded-xl overflow-hidden border border-indigo-200 shadow-sm group">
<img src="${uploadedImageUrl}" class="w-full h-full object-cover">
<div class="absolute inset-0 bg-black/40 hidden group-hover:flex items-center justify-center cursor-pointer" onclick="removeImage()">
<i data-lucide="x" class="w-5 h-5 text-white"></i>
</div>
</div>
`;
lucide.createIcons();
}
} catch (err) {
showToast('图片上传失败', 'error');
} finally {
submitBtn.disabled = false;
}
};
window.removeImage = () => {
uploadedImageUrl = null;
imagePreview.innerHTML = '';
fileInput.value = '';
};
// 提交生成任务
submitBtn.onclick = async () => {
const prompt = promptInput.value.trim();
if (!prompt) return showToast('请输入视频描述', 'warning');
const payload = {
prompt,
model: modelSelect.value,
enhance_prompt: enhancePrompt ? enhancePrompt.checked : false,
aspect_ratio: ratioSelect ? ratioSelect.value : '9:16',
images: uploadedImageUrl ? [uploadedImageUrl] : []
};
try {
setLoading(true);
const r = await fetch('/api/video/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const d = await r.json();
if (d.error) {
showToast(d.error, 'error');
setLoading(false);
} else if (d.task_id) {
showToast(d.message || '任务已提交', 'info');
pollTaskStatus(d.task_id);
} else {
showToast('未知异常,请重试', 'error');
setLoading(false);
}
} catch (err) {
console.error('提交生成异常:', err);
showToast('任务提交失败', 'error');
setLoading(false);
}
};
async function pollTaskStatus(taskId) {
let attempts = 0;
const maxAttempts = 180; // 提升到 15 分钟
const check = async () => {
try {
const r = await fetch(`/api/task_status/${taskId}?t=${Date.now()}`);
if (!r.ok) {
setTimeout(check, 5000);
return;
}
const d = await r.json();
if (d.status === 'complete') {
setLoading(false);
showVideo(d.video_url);
refreshUserPoints();
// 刷新历史列表
loadHistory(1, false);
} else if (d.status === 'error') {
showToast(d.message || '生成失败', 'error');
setLoading(false);
refreshUserPoints();
} else {
attempts++;
if (attempts >= maxAttempts) {
showToast('生成超时,请稍后在历史记录中查看', 'warning');
setLoading(false);
return;
}
setTimeout(check, 5000);
}
} catch (e) {
setTimeout(check, 5000);
}
};
check();
}
function showVideo(url) {
if (!url) return;
try {
placeholder.classList.add('hidden');
finalWrapper.classList.remove('hidden');
resultVideo.src = url;
resultVideo.load();
const playPromise = resultVideo.play();
if (playPromise !== undefined) {
playPromise.catch(e => console.warn('自动播放被拦截'));
}
} catch (err) {
console.error('展示视频失败:', err);
}
}
const closePreviewBtn = document.getElementById('closePreviewBtn');
if (closePreviewBtn) {
closePreviewBtn.onclick = () => {
finalWrapper.classList.add('hidden');
placeholder.classList.remove('hidden');
resultVideo.pause();
resultVideo.src = "";
};
}
function setLoading(isLoading) {
submitBtn.disabled = isLoading;
if (isLoading) {
statusInfo.classList.remove('hidden');
submitBtn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i><span>导演创作中...</span>';
// 如果正在生成,确保回到预览背景状态(如果当前正在播放旧视频)
if (!finalWrapper.classList.contains('hidden')) {
finalWrapper.classList.add('hidden');
placeholder.classList.remove('hidden');
}
} else {
statusInfo.classList.add('hidden');
submitBtn.innerHTML = '<i data-lucide="clapperboard" class="w-5 h-5"></i><span class="text-base font-bold tracking-widest">开始生成视频</span>';
}
if (window.lucide) lucide.createIcons();
}
async function refreshUserPoints() {
try {
const r = await fetch('/api/auth/me');
const d = await r.json();
if (d.points !== undefined) {
if (pointsDisplay) pointsDisplay.innerText = d.points;
if (headerPoints) headerPoints.innerText = d.points;
}
} catch (e) { }
}
document.getElementById('downloadBtn').onclick = () => {
const url = resultVideo.src;
if (!url) return;
downloadUrl(url);
};
if (regenBtn) {
regenBtn.onclick = () => {
submitBtn.click();
};
}
});