setPreviewVideoUrl(null)}
+
+
setPreviewVideoUrl(null)}
style={{
- position: 'absolute', top: 8, right: 8, width: 28, height: 28, borderRadius: '50%',
- background: 'rgba(0,0,0,0.6)', display: 'flex', alignItems: 'center', justifyContent: 'center',
- cursor: 'pointer', zIndex: 10, transition: 'background 0.15s',
+ position: "absolute",
+ top: 8,
+ right: 8,
+ width: 28,
+ height: 28,
+ borderRadius: "50%",
+ background: "rgba(0,0,0,0.6)",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ cursor: "pointer",
+ zIndex: 10,
+ transition: "background 0.15s",
}}
- onMouseEnter={e => { e.currentTarget.style.background = 'rgba(0,0,0,0.85)'; }}
- onMouseLeave={e => { e.currentTarget.style.background = 'rgba(0,0,0,0.6)'; }}>
-
+ onMouseEnter={(e) => {
+ e.currentTarget.style.background = "rgba(0,0,0,0.85)";
+ }}
+ onMouseLeave={(e) => {
+ e.currentTarget.style.background = "rgba(0,0,0,0.6)";
+ }}
+ >
+
>
)}
diff --git a/video-gen-app/src/pages/InitialReplication.tsx b/video-gen-app/src/pages/InitialReplication.tsx
new file mode 100644
index 00000000..5db88124
--- /dev/null
+++ b/video-gen-app/src/pages/InitialReplication.tsx
@@ -0,0 +1,538 @@
+import React, { useState } from 'react';
+import {
+ Layout,
+ Button,
+ Input,
+ Upload,
+ message,
+ Card,
+ Modal,
+ Table,
+ Space,
+} from 'antd';
+import {
+ PlusOutlined,
+ VideoCameraOutlined,
+ PictureOutlined,
+} from '@ant-design/icons';
+
+const { Header, Content } = Layout;
+const { TextArea } = Input;
+
+const GenerateConver: React.FC = () => {
+
+ const [tableData, setTableData] = useState
(
+ // [
+ // {
+ // id: 1,
+ // image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=product%20image%20red%20gift%20box%20with%20hearts&image_size=square',
+ // originalName: '进圈',
+ // productName: '他趣',
+ // status: '视频成功',
+ // createTime: '2026-05-14 17:49:20',
+ // },
+ // {
+ // id: 2,
+ // image: 'https://neeko-copilot.bytedance.net/api/text_to_image?prompt=luxury%20perfume%20bottle%20golden%20elegant&image_size=square',
+ // originalName: '香水',
+ // productName: '面霜',
+ // status: '视频提示词成功',
+ // createTime: '2026-05-08 08:57:46',
+ // },
+ // ]
+ );
+
+
+ // 表单数据
+ const [originalProductName, setOriginalProductName] = useState('');
+ const [ownProductName, setOwnProductName] = useState('');
+ const [productSellingPoints, setProductSellingPoints] = useState('');
+
+ // 文件状态
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoUrl, setVideoUrl] = useState('');
+ const [imageFile, setImageFile] = useState(null);
+ const [imageUrl, setImageUrl] = useState('');
+
+ // 弹窗状态
+ const [isModalOpen, setIsModalOpen] = useState(false);
+
+ // 文件上传前校验
+ const beforeVideoUpload = (file: File) => {
+ const isVideo = file.type.startsWith('video/');
+ if (!isVideo) {
+ message.error('只能上传视频文件');
+ return false;
+ }
+ const isLt50M = file.size / 1024 / 1024 < 50;
+ if (!isLt50M) {
+ message.error('视频大小不能超过50MB');
+ return false;
+ }
+
+ // 检查视频时长
+ const video = document.createElement('video');
+ video.preload = 'metadata';
+ video.onloadedmetadata = () => {
+ if (video.duration > 15) {
+ message.error('视频时长不能超过15秒');
+ URL.revokeObjectURL(videoUrl);
+ setVideoFile(null);
+ setVideoUrl('');
+ return;
+ }
+ };
+ video.src = URL.createObjectURL(file);
+
+ setVideoFile(file);
+ setVideoUrl(URL.createObjectURL(file));
+ return false;
+ };
+
+ const beforeImageUpload = (file: File) => {
+ const isImage = file.type.startsWith('image/');
+ if (!isImage) {
+ message.error('只能上传图片文件');
+ return false;
+ }
+
+ // 检查图片比例
+ const img = new Image();
+ img.onload = () => {
+ const ratio = img.width / img.height;
+ const isRatioValid = Math.abs(ratio - 0.75) < 0.1 || Math.abs(ratio - 0.5625) < 0.1;
+ // if (!isRatioValid) {
+ // message.warning('建议使用3:4或9:16比例的图片以获得最佳效果');
+ // }
+ };
+ img.src = URL.createObjectURL(file);
+
+ setImageFile(file);
+ setImageUrl(URL.createObjectURL(file));
+ return false;
+ };
+
+ // 删除视频
+ const handleRemoveVideo = () => {
+ if (videoUrl) {
+ URL.revokeObjectURL(videoUrl);
+ }
+ setVideoFile(null);
+ setVideoUrl('');
+ };
+
+ // 删除图片
+ const handleRemoveImage = () => {
+ if (imageUrl) {
+ URL.revokeObjectURL(imageUrl);
+ }
+ setImageFile(null);
+ setImageUrl('');
+ };
+
+ // 处理生成
+ const handleGenerate = () => {
+ if (!originalProductName.trim()) {
+ message.warning('请输入原视频产品名称');
+ return;
+ }
+ if (!ownProductName.trim()) {
+ message.warning('请输入自有产品名称');
+ return;
+ }
+ if (!productSellingPoints.trim()) {
+ message.warning('请输入产品卖点');
+ return;
+ }
+ message.success('正在生成爆款开头复刻视频...');
+ console.log('生成参数:', {
+ originalProductName,
+ ownProductName,
+ productSellingPoints,
+ });
+ };
+
+ return (
+
+
+
+
+
+
+ {/* 顶部标题栏 */}
+
+
+ 爆款开头复刻
+
+
+
+
+ {/* 左侧预览区域 */}
+
+
+ {/* 占位图标 */}
+
+ {/* 底层卡片 */}
+
+ {/* 上层卡片 */}
+
+
+
+ 暂无生成内容
+
+
+ 请完善素材与卖点,生成专属爆款开头复刻视频
+
+
+
+
+
+
+
+ {/* 右侧表单区域 */}
+
+ {/* 上传视频 */}
+
+
+ 上传视频
+
+ {videoUrl ? (
+
+
+
+
+ {videoFile?.name} ({(videoFile?.size ? (videoFile.size / 1024 / 1024).toFixed(2) : 0)}MB)
+
+
+ ) : (
+
+ {
+ (e.currentTarget as HTMLElement).style.borderColor = '#6366f1';
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLElement).style.borderColor = '#d9d9d9';
+ }}
+ >
+
+
+ 您可以通过点击或拖拽上传视频
+
+
+ 支持的文件类型:MP4、MOV|视频最大时长:15 秒 | 最大大小:50M
+
+
+
+ )}
+
+
+ {/* 上传产品图片 */}
+
+
+ 上传产品图片
+
+ {imageUrl ? (
+
+

+
+
+ {imageFile?.name} ({(imageFile?.size ? (imageFile.size / 1024 / 1024).toFixed(2) : 0)}MB)
+
+
+ ) : (
+
+ {
+ (e.currentTarget as HTMLElement).style.borderColor = '#6366f1';
+ }}
+ onMouseLeave={(e) => {
+ (e.currentTarget as HTMLElement).style.borderColor = '#d9d9d9';
+ }}
+ >
+
+
+ 支持 JPG,JPEG,PNG 格式,图片比例为 3:4 或 9:16 效果最佳
+
+
+
+ )}
+
+
+ {/* 原视频产品名称 */}
+
+
+ 原视频产品名称
+
+
setOriginalProductName(e.target.value)}
+ placeholder="请输入原视频产品名称"
+ style={{ borderRadius: 6, height: 32, fontSize: 12 }}
+ maxLength={10}
+ suffix={
{originalProductName.length}/10}
+ />
+
+
+ {/* 自有产品名称 */}
+
+
+ 自有产品名称
+
+
setOwnProductName(e.target.value)}
+ placeholder="请输入自有产品名称"
+ style={{ borderRadius: 6, height: 32, fontSize: 12 }}
+ maxLength={10}
+ suffix={
{ownProductName.length}/10}
+ />
+
+
+ {/* 产品卖点 */}
+
+
+ {/* 立即生成按钮 */}
+
+
+
+
+ {/* 创作记录弹窗 */}
+
setIsModalOpen(false)}
+ width={800}
+ footer={null}
+ style={{ borderRadius: 12 }}
+ >
+ {/* 搜索区域 */}
+
+
+
+
+
+ {/* 表格 */}
+ (
+
+ ),
+ },
+ {
+ title: '原产品名称',
+ dataIndex: 'originalName',
+ key: 'originalName',
+ },
+ {
+ title: '产品名称',
+ dataIndex: 'productName',
+ key: 'productName',
+ },
+ {
+ title: '状态',
+ dataIndex: 'status',
+ key: 'status',
+ },
+ {
+ title: '创建时间',
+ dataIndex: 'createTime',
+ key: 'createTime',
+ },
+ {
+ title: '操作',
+ key: 'action',
+ render: () => (
+
+ 查看详情
+
+ ),
+ },
+ ]}
+ dataSource={tableData}
+ rowKey="id"
+ pagination={false}
+ style={{ fontSize: 13 }}
+ />
+
+
+
+ );
+};
+
+export default GenerateConver;
\ No newline at end of file
diff --git a/video-gen-app/src/pages/ProjectsPage.tsx b/video-gen-app/src/pages/ProjectsPage.tsx
index e97484db..e6f2a874 100644
--- a/video-gen-app/src/pages/ProjectsPage.tsx
+++ b/video-gen-app/src/pages/ProjectsPage.tsx
@@ -156,7 +156,7 @@ const ProjectsPage: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1, #8b5cf6)', border: 'none',
borderRadius: 10, fontWeight: 600, height: 36,
boxShadow: '0 4px 12px rgba(99,102,241,0.25)',
- }}>生成视频
+ }}>生成
{ e?.stopPropagation(); await deleteProject(project.id); message.success('项目已删除'); }}
onCancel={(e) => e?.stopPropagation()}>
diff --git a/video-gen-app/src/pages/RecordsPage.tsx b/video-gen-app/src/pages/RecordsPage.tsx
index a5f4cdbf..ab679942 100644
--- a/video-gen-app/src/pages/RecordsPage.tsx
+++ b/video-gen-app/src/pages/RecordsPage.tsx
@@ -25,6 +25,7 @@ import {
VideoCameraOutlined,
DownloadOutlined,
RocketOutlined,
+ PictureOutlined,
} from '@ant-design/icons';
import { useAppStore } from '../store/useAppStore';
import type { GenerationStatus, AspectRatio, Resolution } from '../types';
@@ -68,6 +69,23 @@ const RecordsPage: React.FC = () => {
};
const openGenModal = (record: any) => {
+ const type: any = record.genType || 'image';
+
+ // 如果是图片类型,直接生成,不需要弹窗
+ if (type === 'image') {
+ setGenerating((p) => ({ ...p, [record.id]: true }));
+ message.loading({ content: `「${record.projectName}」正在生成图片...`, duration: 0, key: record.id });
+ generateVideo(record.id, {}).then(() => {
+ message.success({ content: `「${record.projectName}」图片生成成功!`, key: record.id, duration: 3 });
+ }).catch(() => {
+ message.error({ content: `「${record.projectName}」图片生成失败`, key: record.id, duration: 3 });
+ }).finally(() => {
+ setGenerating((p) => ({ ...p, [record.id]: false }));
+ });
+ return;
+ }
+
+ // 如果是视频类型,显示参数选择弹窗
setGenModal({
recordId: record.id,
projectName: record.projectName,
@@ -124,6 +142,9 @@ const RecordsPage: React.FC = () => {
const prompt = editablePrompts[record.id] ?? record.optimizedPrompt;
const isGenerating = generating[record.id];
const isExpanded = expandedId === record.id;
+ const type: any = record.genType || 'image';
+
+
return (
@@ -138,6 +159,12 @@ const RecordsPage: React.FC = () => {
display: 'flex', alignItems: 'center', gap: 12,
}}
>
+ {/*
+ {type === 'image' ? 图片 : 视频}
+
*/}
+
+ {type === 'image' ?
:
}
+
{/* Expand icon */}
{isExpanded
?
@@ -162,9 +189,11 @@ const RecordsPage: React.FC = () => {
{/* Meta */}
-
+ {type === 'image' ?
+ {record.duration ? `${record.imageSize}` : '-'} · {record.imageProportion || '-'} · {record.imagePx || '-'} · {formatDate(record.createdAt)}
+ :
{record.duration ? `${record.duration}秒` : '-'} · {record.aspectRatio || '-'} · {record.resolution || '-'} · {formatDate(record.createdAt)}
-
+ }
{/* Quick actions */}
e.stopPropagation()}>
@@ -291,7 +320,17 @@ const RecordsPage: React.FC = () => {
display: 'flex', gap: 16, padding: '12px 16px',
borderRadius: 10, background: '#f8f9fc',
}}>
- {[
+ {type === 'image' ? [
+ { label: '分辨率', value: record.imageSize ? `${record.imageSize} ` : '-' },
+ { label: '画面比例', value: record.imageProportion ? `${record.imageProportion} ` : '-' },
+ { label: '画面尺寸', value: record.imageProportion ? `${record.imageProportion} ` : '-' },
+ { label: '消耗积分', value: record.creditsCost ? `${record.creditsCost} ` : '-', highlight: !!record.creditsCost },
+ ].map((item, j) => (
+
+ {item.label}
+ {item.value}
+
+ )) : [
{ label: '时长', value: record.duration ? `${record.duration} 秒` : '-' },
{ label: '画面比例', value: record.aspectRatio || (record.status === 'prompt_optimized' ? '待选择' : '-') },
{ label: '分辨率', value: record.resolution || (record.status === 'prompt_optimized' ? '待选择' : '-') },
@@ -315,7 +354,7 @@ const RecordsPage: React.FC = () => {
background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)',
border: 'none', boxShadow: '0 8px 24px rgba(99,102,241,0.3)',
}}>
- 选择参数生成视频
+ {type === 'video' ? '选择参数生成视频' : '生成图片'}
)}
@@ -327,7 +366,7 @@ const RecordsPage: React.FC = () => {
loading={isGenerating}
onClick={() => openGenModal(record)}
style={{ borderRadius: 12, fontWeight: 600, height: 44 }}>
- 重新生成视频
+ 重新生成{type === 'video' ? '视频' : '图片'}
)}
@@ -343,34 +382,42 @@ const RecordsPage: React.FC = () => {
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
}}>
- 视频生成中...
+ {type === 'video' ? '视频' : '图片'}生成中...
请耐心等待,生成完成后将自动展示
- ) : record.status === 'completed' && record.videoUrl ? (
+ ) : record.status === 'completed' && (record.videoUrl || record.imageUrl) ? (
-
+
}
- onClick={() => { const a = document.createElement('a'); a.href = `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${record.videoUrl}`; a.download = `${record.projectName}.mp4`; a.click(); }}
+ onClick={() => { const a = document.createElement('a'); a.href = `${import.meta.env.VITE_API_BASE || 'http://localhost:8000'}${type === 'video' ? record.videoUrl : record.imageUrl}`; a.download = `${record.projectName}${type === 'video' ? '.mp4' : '.png'}`; a.click(); }}
style={{ background: 'rgba(0,0,0,0.5)', border: 'none', color: '#fff', backdropFilter: 'blur(4px)', borderRadius: 8 }}>
下载
-
+ {type === 'video' ? (
+
+ ) : (
+

+ )}
-
+ {type === 'video' ? : }
{record.projectName}
@@ -390,7 +437,7 @@ const RecordsPage: React.FC = () => {
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
}}>
- 视频生成失败
+ {type === 'video' ? '视频' : '图片'}生成失败
请在左侧点击重新生成
) : (
@@ -400,7 +447,7 @@ const RecordsPage: React.FC = () => {
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', gap: 12,
}}>
-
待生成视频
+
待生成{type === 'video' ? '视频' : '图片'}
点击左侧生成按钮开始生成
)}
diff --git a/video-gen-app/src/pages/RemoveLens.tsx b/video-gen-app/src/pages/RemoveLens.tsx
new file mode 100644
index 00000000..4e555bc1
--- /dev/null
+++ b/video-gen-app/src/pages/RemoveLens.tsx
@@ -0,0 +1,613 @@
+import { useState, useRef, useCallback } from 'react';
+
+interface FrameData {
+ url: string;
+ time: number;
+ selected: boolean;
+}
+
+export default function VideoFrameExtractor() {
+ const [videoFile, setVideoFile] = useState(null);
+ const [videoUrl, setVideoUrl] = useState('');
+ const [frames, setFrames] = useState([]);
+ const [isExtracting, setIsExtracting] = useState(false);
+ const [extractProgress, setExtractProgress] = useState(0);
+ const [selectedFrames, setSelectedFrames] = useState([]);
+ const [error, setError] = useState('');
+
+ const videoRef = useRef(null);
+ const canvasRef = useRef(null);
+
+ const cleanupResources = useCallback(() => {
+ if (videoUrl) {
+ URL.revokeObjectURL(videoUrl);
+ }
+ frames.forEach(frame => {
+ if (frame.url.startsWith('blob:')) {
+ URL.revokeObjectURL(frame.url);
+ }
+ });
+ setFrames([]);
+ setVideoUrl('');
+ setVideoFile(null);
+ setError('');
+ setSelectedFrames([]);
+ }, [videoUrl, frames]);
+
+ const handleFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ if (!file.type.startsWith('video/')) {
+ setError('请选择视频文件');
+ return;
+ }
+
+ if (file.size > 100 * 1024 * 1024) {
+ setError('视频文件大小不能超过 100MB');
+ return;
+ }
+
+ cleanupResources();
+
+ setVideoFile(file);
+ const url = URL.createObjectURL(file);
+ setVideoUrl(url);
+ setError('');
+ };
+
+ const handleDrop = (e: React.DragEvent) => {
+ e.preventDefault();
+ const file = e.dataTransfer.files?.[0];
+ if (!file) return;
+
+ if (!file.type.startsWith('video/')) {
+ setError('请选择视频文件');
+ return;
+ }
+
+ if (file.size > 100 * 1024 * 1024) {
+ setError('视频文件大小不能超过 100MB');
+ return;
+ }
+
+ cleanupResources();
+
+ setVideoFile(file);
+ const url = URL.createObjectURL(file);
+ setVideoUrl(url);
+ setError('');
+ };
+
+ const handleDragOver = (e: React.DragEvent) => {
+ e.preventDefault();
+ };
+
+ /**
+ * 视频帧提取核心函数 - 拆镜功能的主要实现
+ *
+ * 功能说明:
+ * - 从上传的视频中按时间间隔提取帧画面
+ * - 每秒提取一帧,确保覆盖整个视频时长
+ * - 默认选中前4秒的帧(4帧),用于后续创作
+ *
+ * 技术实现:
+ * - 使用 HTML5 Video API 进行视频帧捕获
+ * - 通过 Canvas API 将视频帧转换为图片
+ * - 使用 Promise + 事件监听确保帧数据就绪
+ * - 添加超时机制防止死锁
+ */
+ const extractFrames = useCallback(async () => {
+ // 前置检查:确保视频和画布元素已就绪
+ if (!videoRef.current || !canvasRef.current) return;
+
+ // 初始化提取状态
+ setIsExtracting(true); // 标记正在提取中
+ setExtractProgress(1); // 重置进度为0%
+ setSelectedFrames([]); // 清空已选帧列表
+
+ // 获取视频和画布引用
+ const video = videoRef.current;
+ const canvas = canvasRef.current;
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return; // Canvas 2D上下文获取失败则退出
+
+ // 计算提取参数
+ const duration = video.duration; // 视频总时长(秒)
+ const frameList: FrameData[] = []; // 存储提取的帧数据
+ const extractInterval = 1; // 提取间隔:每1秒抽一帧
+ const totalFrames = Math.max(1, Math.floor(duration / extractInterval)); // 计算总帧数
+
+ try {
+ // 遍历视频时间轴,按间隔提取帧
+ for (let time = 0; time < duration; time += extractInterval) {
+ // 设置视频当前播放位置到目标时间点
+ video.currentTime = time;
+
+ // 等待帧数据就绪(异步等待)
+ await new Promise((resolve) => {
+ /**
+ * 检查视频就绪状态
+ * readyState >= 2 表示当前帧数据已加载完成
+ * readyState 值说明:
+ * - 0 = HAVE_NOTHING: 无数据
+ * - 1 = HAVE_METADATA: 仅元数据
+ * - 2 = HAVE_CURRENT_DATA: 当前帧数据可用
+ * - 3 = HAVE_FUTURE_DATA: 当前帧和后续帧可用
+ * - 4 = HAVE_ENOUGH_DATA: 所有数据可用
+ */
+ const checkReadyState = () => {
+ if (video.readyState >= 2) {
+ processFrame();
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * 处理单帧提取的核心函数
+ */
+ const processFrame = () => {
+ // 清理事件监听器,防止内存泄漏
+ video.removeEventListener('seeked', onSeeked);
+ video.removeEventListener('loadeddata', onLoadedData);
+ clearTimeout(timeoutId); // 清除超时计时器
+
+ // 设置画布尺寸为视频帧尺寸
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+
+ // 将视频当前帧绘制到画布
+ ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
+
+ // 将画布转换为 Base64 格式的图片 URL(JPEG格式,质量0.9)
+ const frameUrl = canvas.toDataURL('image/jpeg', 0.9);
+
+ // 将帧数据添加到列表
+ frameList.push({
+ url: frameUrl, // 帧图片URL
+ time: Math.round(time * 10) / 10, // 帧对应的时间点(保留一位小数)
+ selected: false, // 默认未选中
+ });
+
+ // 更新提取进度
+ setExtractProgress(Math.round((frameList.length / totalFrames) * 100));
+
+ // 完成当前帧提取,继续下一轮
+ resolve();
+ };
+
+ /**
+ * seeked 事件处理:视频定位完成后触发
+ */
+ const onSeeked = () => {
+ checkReadyState();
+ };
+
+ /**
+ * loadeddata 事件处理:帧数据加载完成后触发
+ * 作为 seeked 的备选方案,确保兼容性
+ */
+ const onLoadedData = () => {
+ processFrame();
+ };
+
+ /**
+ * 超时机制:3秒超时防止无限等待
+ * 如果3秒内帧数据仍未就绪,跳过当前帧继续下一帧
+ */
+ const timeoutId = setTimeout(() => {
+ video.removeEventListener('seeked', onSeeked);
+ video.removeEventListener('loadeddata', onLoadedData);
+ console.warn(`Timeout waiting for frame at ${time}s`);
+ resolve(); // 跳过当前帧,继续处理下一帧
+ }, 3000);
+
+ // 先同步检查状态,如果未就绪则注册事件监听器等待
+ if (!checkReadyState()) {
+ video.addEventListener('seeked', onSeeked);
+ video.addEventListener('loadeddata', onLoadedData);
+ }
+ });
+ }
+
+ // 提取完成后,默认选中前4秒的帧(约8帧)
+ const defaultSelected = Math.min(8, frameList.length);
+ const defaultIndices: number[] = [];
+ const updatedFrames = frameList.map((frame, index) => {
+ if (index < defaultSelected) {
+ defaultIndices.push(index);
+ return { ...frame, selected: true }; // 标记为已选中
+ }
+ return frame;
+ });
+
+ // 更新状态:保存提取的帧列表和默认选中的帧索引
+ setFrames(updatedFrames);
+ setSelectedFrames(defaultIndices);
+ } catch (err) {
+ // 提取过程中发生错误
+ setError('拆镜过程中发生错误');
+ console.error('Extract frames error:', err);
+ } finally {
+ // 无论成功或失败,都标记提取完成
+ setIsExtracting(false);
+ setExtractProgress(100);
+ }
+ }, []);
+
+ const toggleFrameSelection = (index: number) => {
+ setSelectedFrames(prev => {
+ if (prev.includes(index)) {
+ return prev.filter(i => i !== index);
+ }
+ return [...prev, index];
+ });
+
+ setFrames(prev =>
+ prev.map((frame, i) =>
+ i === index ? { ...frame, selected: !frame.selected } : frame
+ )
+ );
+ };
+
+ const formatTime = (seconds: number) => {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.round(seconds % 60);
+ return `${mins}:${String(secs).padStart(2, '0')}`;
+ };
+
+ const handleRemoveVideo = () => {
+ cleanupResources();
+ };
+
+ return (
+
+
+ {/* 标题区域 */}
+
+
+
+ 一键拆解画面分镜,助力仿拍或创作
+
+
+
+ {/* 错误提示 */}
+ {error && (
+
+ ⚠️ {error}
+
+ )}
+
+ {/* 上传/视频区域 */}
+
+ {/* 删除按钮 */}
+ {videoUrl && (
+
+ )}
+
+ {/* 未上传状态 */}
+ {!videoUrl && (
+
+
+
+
+ 支持 MP4、MOV 格式视频,最大支持 100MB 的文件,支持上传 3 分钟内的视频,可处理 4-15 秒片段
+
+
+ )}
+
+ {/* 已上传状态 - 视频预览 */}
+ {videoUrl && !frames.length && (
+
+
+
+ 时长: {formatTime(videoRef.current?.duration || 0)}
+
+
+ )}
+
+ {/* 拆镜结果 */}
+ {frames.length > 0 && (
+
+ {/* 视频预览 */}
+
+
+
+
+ {/* 帧选择区域 */}
+
+
+
+
+ 00:00:00 / 00:00:{String(Math.round(videoRef.current?.duration || 0)).padStart(2, '0')}
+
+
+
+ {/* 帧缩略图 */}
+
+ {frames.map((frame, index) => (
+
toggleFrameSelection(index)}
+ style={{
+ flexShrink: 0,
+ width: 60,
+ height: 45,
+ border: frame.selected ? '2px solid #6366f1' : '1px solid #e5e7eb',
+ borderRadius: 6,
+ overflow: 'hidden',
+ cursor: 'pointer',
+ position: 'relative'
+ }}
+ >
+

+ {frame.selected && (
+
+ ✓
+
+ )}
+
+ ))}
+
+
+
+
+ 已选取 {selectedFrames.length} 帧
+
+
+
+
+ )}
+
+ {/* 进度条 */}
+ {isExtracting && (
+
+
+ 正在拆解...
+ {extractProgress}%
+
+
+
+ )}
+
+
+ {/* 操作按钮 */}
+
+
+
+
+ {/* 创作记录 */}
+ {/*
*/}
+
+
+ {/* 隐藏画布 */}
+
+
+ );
+}
\ No newline at end of file
diff --git a/video-gen-app/src/store/useAuthStore.ts b/video-gen-app/src/store/useAuthStore.ts
index 14a15409..0c8ae9ef 100644
--- a/video-gen-app/src/store/useAuthStore.ts
+++ b/video-gen-app/src/store/useAuthStore.ts
@@ -25,11 +25,16 @@ export const useAuthStore = create((set) => ({
set({ user: null });
},
- checkAuth: async () => {
+ checkAuth: async () => {
try {
+ const token = localStorage.getItem('auth_token');
+ if (!token) { set({ user: null, loading: false }); return; }
const user = await api.getUser();
set({ user, loading: false });
- } catch {
+ } catch (error: any) {
+ if (error?.message?.includes('401') || error?.message?.includes('Unauthorized')) {
+ localStorage.removeItem('auth_token');
+ }
set({ user: null, loading: false });
}
},
diff --git a/video-gen-app/src/types/index.ts b/video-gen-app/src/types/index.ts
index 261bcc66..aed2eb2c 100644
--- a/video-gen-app/src/types/index.ts
+++ b/video-gen-app/src/types/index.ts
@@ -65,6 +65,7 @@ export interface GenerationRecord {
originalPrompt: string;
optimizedPrompt?: string;
duration?: number;
+ genType?: number;
aspectRatio?: AspectRatio;
resolution?: Resolution;
status: GenerationStatus;
@@ -77,18 +78,29 @@ export interface GenerationRecord {
errorMessage?: string;
createdAt: string;
generatedAt?: string;
+ imageSize: string;
+ imageProportion: string;
+ imagePx: string;
+ imageUrl: string;
+
}
export interface OptimizeParams {
prompt: string;
duration: number;
+ genType?: any;
+ resolution?: string;
+ aspectRatio?: string;
references?: MediaReference[];
idempotencyKey?: string;
+ image_size:any;
+ image_proportion:any;
+ image_px:any
}
export interface GenerateParams {
- aspectRatio: AspectRatio;
- resolution: Resolution;
+ aspectRatio?: AspectRatio;
+ resolution?: Resolution;
}
export interface OptimizeResult {
diff --git a/video-gen-app/src/utils/uuid.ts b/video-gen-app/src/utils/uuid.ts
new file mode 100644
index 00000000..5c5f3cf0
--- /dev/null
+++ b/video-gen-app/src/utils/uuid.ts
@@ -0,0 +1,26 @@
+export function generateUUID(): string {
+ // 优先使用现代浏览器的 crypto.randomUUID
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
+ return crypto.randomUUID();
+ }
+
+ // 降级方案:使用 crypto.getRandomValues 生成 UUIDv4
+ if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
+ // 设置版本为 4(UUIDv4)
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
+ // 设置变体为 RFC 4122
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
+
+ const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
+ }
+
+ // 最后降级:使用时间戳和随机数
+ const timestamp = Date.now().toString(16).padStart(12, '0');
+ const random = Math.random().toString(16).slice(2, 10).padStart(8, '0');
+ return `${timestamp}-${random}-4xxx-yxxx-${Math.random().toString(16).slice(2, 12)}`.replace(/[xy]/g, (c) => {
+ const r = Math.random() * 16 | 0;
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
+ });
+}
\ No newline at end of file
diff --git a/video-gen-app/tsconfig.app.json b/video-gen-app/tsconfig.app.json
index 864e8e73..60d77c34 100644
--- a/video-gen-app/tsconfig.app.json
+++ b/video-gen-app/tsconfig.app.json
@@ -6,6 +6,8 @@
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
+ "strict": false,
+ "noImplicitAny": false,
/* Bundler mode */
"moduleResolution": "bundler",