forked from 77media/video-flow
新增文本解析适配器和视频编辑数据处理逻辑,重构分镜编辑器以支持角色和对话内容的动态加载,优化角色标记和文本节点的处理方式。
This commit is contained in:
parent
c48b706e9a
commit
03aa092a08
221
app/service/adapter/textToShot.ts
Normal file
221
app/service/adapter/textToShot.ts
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
import { ContentItem, LensType, SimpleCharacter } from '../domain/valueObject';
|
||||||
|
|
||||||
|
// 定义角色属性接口
|
||||||
|
interface CharacterAttributes {
|
||||||
|
name: string;
|
||||||
|
// gender: string;
|
||||||
|
// age: string;
|
||||||
|
avatar: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义文本节点接口
|
||||||
|
interface TextNode {
|
||||||
|
type: 'text';
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义角色标记节点接口
|
||||||
|
interface CharacterTokenNode {
|
||||||
|
type: 'characterToken';
|
||||||
|
attrs: CharacterAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义内容节点类型(文本或角色标记)
|
||||||
|
type ContentNode = TextNode | CharacterTokenNode;
|
||||||
|
|
||||||
|
// 定义段落接口
|
||||||
|
interface Paragraph {
|
||||||
|
type: 'paragraph';
|
||||||
|
content: ContentNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义shot 接口
|
||||||
|
interface Shot {
|
||||||
|
name: string;
|
||||||
|
shotDescContent: Paragraph[];
|
||||||
|
shotDialogsContent: Paragraph[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TextToShotAdapter {
|
||||||
|
/**
|
||||||
|
* 解析文本,识别角色并转换为节点数组
|
||||||
|
* @param text 要解析的文本
|
||||||
|
* @param roles 角色列表
|
||||||
|
* @returns ContentNode[] 节点数组
|
||||||
|
*/
|
||||||
|
public static parseText(text: string, roles: SimpleCharacter[]): ContentNode[] {
|
||||||
|
const nodes: ContentNode[] = [];
|
||||||
|
let currentText = text;
|
||||||
|
|
||||||
|
// 按角色名称长度降序排序,避免短名称匹配到长名称的一部分
|
||||||
|
const sortedRoles = [...roles].sort((a, b) => b.name.length - a.name.length);
|
||||||
|
|
||||||
|
while (currentText.length > 0) {
|
||||||
|
let matchFound = false;
|
||||||
|
|
||||||
|
// 尝试匹配角色
|
||||||
|
for (const role of sortedRoles) {
|
||||||
|
if (currentText.startsWith(role.name)) {
|
||||||
|
// 如果当前文本以角色名开头
|
||||||
|
if (currentText.length > role.name.length) {
|
||||||
|
// 添加角色标记节点
|
||||||
|
nodes.push({
|
||||||
|
type: 'characterToken',
|
||||||
|
attrs: {
|
||||||
|
name: role.name,
|
||||||
|
avatar: role.imageUrl
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 移除已处理的角色名
|
||||||
|
currentText = currentText.slice(role.name.length);
|
||||||
|
matchFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!matchFound) {
|
||||||
|
// 如果没有找到角色匹配,处理普通文本
|
||||||
|
// 查找下一个可能的角色名位置
|
||||||
|
let nextRoleIndex = currentText.length;
|
||||||
|
for (const role of sortedRoles) {
|
||||||
|
const index = currentText.indexOf(role.name);
|
||||||
|
if (index !== -1 && index < nextRoleIndex) {
|
||||||
|
nextRoleIndex = index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加文本节点
|
||||||
|
const textContent = currentText.slice(0, nextRoleIndex);
|
||||||
|
if (textContent) {
|
||||||
|
nodes.push({
|
||||||
|
type: 'text',
|
||||||
|
text: textContent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 移除已处理的文本
|
||||||
|
currentText = currentText.slice(nextRoleIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
private readonly ShotData: Shot;
|
||||||
|
constructor(shotData: Shot) {
|
||||||
|
this.ShotData = shotData;
|
||||||
|
}
|
||||||
|
|
||||||
|
toShot() {
|
||||||
|
return this.ShotData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 LensType 转换为 Paragraph 格式
|
||||||
|
* @param lensType LensType 实例
|
||||||
|
* @returns Paragraph 格式的数据
|
||||||
|
*/
|
||||||
|
public static fromLensType(lensType: LensType, roles: SimpleCharacter[]): Shot {
|
||||||
|
const shotDescContent: Paragraph[] = [];
|
||||||
|
const shotDialogsContent: Paragraph[] = [];
|
||||||
|
|
||||||
|
// 处理镜头描述 通过roles name 匹配镜头描述中出现的角色 并添加到shotDescContent
|
||||||
|
if (lensType.script) {
|
||||||
|
const descNodes = TextToShotAdapter.parseText(lensType.script, roles);
|
||||||
|
shotDescContent.push({
|
||||||
|
type: 'paragraph',
|
||||||
|
content: descNodes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对话内容 通过roles name 匹配对话内容中出现的角色 并添加到shotDialogsContent
|
||||||
|
lensType.content.forEach(item => {
|
||||||
|
const dialogNodes = TextToShotAdapter.parseText(item.content, roles);
|
||||||
|
|
||||||
|
// 确保对话内容以角色标记开始
|
||||||
|
const roleMatch = roles.find(role => role.name === item.roleName);
|
||||||
|
if (roleMatch) {
|
||||||
|
const dialogContent: Paragraph = {
|
||||||
|
type: 'paragraph',
|
||||||
|
content: [{
|
||||||
|
type: 'characterToken',
|
||||||
|
attrs: {
|
||||||
|
name: roleMatch.name,
|
||||||
|
avatar: roleMatch.imageUrl
|
||||||
|
}},
|
||||||
|
...dialogNodes
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
shotDialogsContent.push(dialogContent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: lensType.name,
|
||||||
|
shotDescContent,
|
||||||
|
shotDialogsContent
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Paragraph 转换为 LensType 格式
|
||||||
|
* @param paragraphData Paragraph 格式的数据
|
||||||
|
* @returns LensType 实例
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 将 Shot 格式转换为 LensType 格式
|
||||||
|
* @param shotData Shot 格式的数据
|
||||||
|
* @returns LensType 实例
|
||||||
|
*/
|
||||||
|
public static toLensType(shotData: Shot): LensType {
|
||||||
|
const content: ContentItem[] = [];
|
||||||
|
let currentScript = '';
|
||||||
|
|
||||||
|
// 处理镜头描述
|
||||||
|
if (shotData.shotDescContent.length > 0) {
|
||||||
|
// 合并所有描述段落的文本内容
|
||||||
|
shotData.shotDescContent.forEach(paragraph => {
|
||||||
|
paragraph.content.forEach(node => {
|
||||||
|
if (node.type === 'text') {
|
||||||
|
currentScript += node.text;
|
||||||
|
}
|
||||||
|
if (node.type === 'characterToken') {
|
||||||
|
currentScript += node.attrs.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理对话内容
|
||||||
|
shotData.shotDialogsContent.forEach(paragraph => {
|
||||||
|
let dialogRoleName = '';
|
||||||
|
let dialogContent = '';
|
||||||
|
|
||||||
|
// 遍历段落内容
|
||||||
|
paragraph.content.forEach((node, index) => {
|
||||||
|
if (node.type === 'characterToken') {
|
||||||
|
// 记录说话角色的名称
|
||||||
|
index === 0 && (dialogRoleName = node.attrs.name);
|
||||||
|
index !== 0 && (dialogContent += node.attrs.name);
|
||||||
|
} else if (node.type === 'text') {
|
||||||
|
// 累积对话内容
|
||||||
|
dialogContent += node.text;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果有角色名和对话内容,添加到结果中
|
||||||
|
if (dialogRoleName && dialogContent) {
|
||||||
|
content.push({
|
||||||
|
roleName: dialogRoleName,
|
||||||
|
content: dialogContent.trim()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new LensType(
|
||||||
|
shotData.name, // 使用 Shot 中的 name
|
||||||
|
currentScript.trim(),
|
||||||
|
content
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
108
components/pages/work-flow/use-edit-data.tsx
Normal file
108
components/pages/work-flow/use-edit-data.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useShotService } from "@/app/service/Interaction/ShotService";
|
||||||
|
import { useSearchParams } from 'next/navigation';
|
||||||
|
|
||||||
|
const mockShotData = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: 'Shot 1',
|
||||||
|
sketchUrl: 'https://example.com/sketch.png',
|
||||||
|
videoUrl: ['https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ1-0-20250725023719.mp4'],
|
||||||
|
status: 1, // 0:视频加载中 1:任务已完成 2:任务失败
|
||||||
|
lens: [
|
||||||
|
{
|
||||||
|
name: 'Shot 1',
|
||||||
|
script: '镜头聚焦在 President Alfred King 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。',
|
||||||
|
content: [{
|
||||||
|
roleName: 'President Alfred King',
|
||||||
|
content: '我需要一个镜头,镜头聚焦在 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
id: '2',
|
||||||
|
name: 'Shot 2',
|
||||||
|
sketchUrl: 'https://example.com/sketch.png',
|
||||||
|
videoUrl: ['https://video-base-imf.oss-ap-southeast-7.aliyuncs.com/uploads/FJ3-0-20250725023725.mp4'],
|
||||||
|
status: 1, // 0:视频加载中 1:任务已完成 2:任务失败
|
||||||
|
lens: [
|
||||||
|
{
|
||||||
|
name: 'Shot 1',
|
||||||
|
script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。',
|
||||||
|
content: [{
|
||||||
|
roleName: 'Samuel Ryan',
|
||||||
|
content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
id: '3',
|
||||||
|
name: 'Shot 3',
|
||||||
|
sketchUrl: 'https://example.com/sketch.png',
|
||||||
|
videoUrl: [],
|
||||||
|
status: 0, // 0:视频加载中 1:任务已完成 2:任务失败
|
||||||
|
lens: [
|
||||||
|
{
|
||||||
|
name: 'Shot 1',
|
||||||
|
script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。',
|
||||||
|
content: [{
|
||||||
|
roleName: 'Samuel Ryan',
|
||||||
|
content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}, {
|
||||||
|
id: '4',
|
||||||
|
name: 'Shot 4',
|
||||||
|
sketchUrl: 'https://example.com/sketch.png',
|
||||||
|
videoUrl: [],
|
||||||
|
status: 2, // 0:视频加载中 1:任务已完成 2:任务失败
|
||||||
|
lens: [
|
||||||
|
{
|
||||||
|
name: 'Shot 1',
|
||||||
|
script: '镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。',
|
||||||
|
content: [{
|
||||||
|
roleName: 'Samuel Ryan',
|
||||||
|
content: '我需要一个镜头,镜头聚焦在 Samuel Ryan 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。'
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export const useEditData = (tabType: string) => {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const projectId = searchParams.get('episodeId') || '';
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [shotData, setShotData] = useState<any[]>(mockShotData);
|
||||||
|
|
||||||
|
const {
|
||||||
|
videoSegments,
|
||||||
|
getVideoSegmentList,
|
||||||
|
setSelectedSegment,
|
||||||
|
regenerateVideoSegment
|
||||||
|
} = useShotService();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabType === 'shot') {
|
||||||
|
getVideoSegmentList(projectId).then(() => {
|
||||||
|
console.log('useEditData-----videoSegments', videoSegments);
|
||||||
|
// setShotData(videoSegments);
|
||||||
|
setLoading(false);
|
||||||
|
}).catch((err) => {
|
||||||
|
console.log('useEditData-----err', err);
|
||||||
|
setShotData(mockShotData);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}, [tabType]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
loading,
|
||||||
|
shotData,
|
||||||
|
setSelectedSegment,
|
||||||
|
regenerateVideoSegment
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
import { Node, mergeAttributes } from '@tiptap/core'
|
import { Node, mergeAttributes } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tiptap/react'
|
import { ReactNodeViewRenderer, NodeViewWrapper, ReactNodeViewProps } from '@tiptap/react'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { useState } from 'react'
|
import { useState, useRef, useEffect } from 'react'
|
||||||
import { Check } from 'lucide-react'
|
import { Check, CircleUserRound } from 'lucide-react'
|
||||||
|
|
||||||
interface CharacterAttributes {
|
interface CharacterAttributes {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
@ -22,10 +22,59 @@ interface CharacterTokenOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CharacterToken(props: ReactNodeViewProps) {
|
export function CharacterToken(props: ReactNodeViewProps) {
|
||||||
const [showRoleList, setShowRoleList] = useState(false)
|
const [showRoleList, setShowRoleList] = useState(false);
|
||||||
const { name, avatar } = props.node.attrs as CharacterAttributes
|
const [listPosition, setListPosition] = useState({ top: 0, left: 0 });
|
||||||
const extension = props.extension as Node<CharacterTokenOptions>
|
const { name, avatar } = props.node.attrs as CharacterAttributes;
|
||||||
const roles = extension.options.roles || []
|
const extension = props.extension as Node<CharacterTokenOptions>;
|
||||||
|
const roles = extension.options.roles || [];
|
||||||
|
|
||||||
|
const tokenRef = useRef<HTMLSpanElement>(null);
|
||||||
|
const listRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 计算下拉列表的位置
|
||||||
|
const updateListPosition = () => {
|
||||||
|
if (!tokenRef.current || !listRef.current) return;
|
||||||
|
|
||||||
|
const tokenRect = tokenRef.current.getBoundingClientRect();
|
||||||
|
const listRect = listRef.current.getBoundingClientRect();
|
||||||
|
const viewportHeight = window.innerHeight;
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
|
||||||
|
// 计算理想的顶部位置(在token下方)
|
||||||
|
let top = tokenRect.bottom + 8; // 8px 间距
|
||||||
|
let left = tokenRect.left;
|
||||||
|
|
||||||
|
// 检查是否超出底部
|
||||||
|
if (top + listRect.height > viewportHeight) {
|
||||||
|
// 如果超出底部,将列表显示在token上方
|
||||||
|
top = tokenRect.top - listRect.height - 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超出右侧
|
||||||
|
if (left + listRect.width > viewportWidth) {
|
||||||
|
// 如果超出右侧,将列表右对齐
|
||||||
|
left = viewportWidth - listRect.width - 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保不会超出左侧
|
||||||
|
left = Math.max(8, left);
|
||||||
|
|
||||||
|
setListPosition({ top, left });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
useEffect(() => {
|
||||||
|
if (showRoleList) {
|
||||||
|
updateListPosition();
|
||||||
|
window.addEventListener('resize', updateListPosition);
|
||||||
|
window.addEventListener('scroll', updateListPosition);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateListPosition);
|
||||||
|
window.removeEventListener('scroll', updateListPosition);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [showRoleList]);
|
||||||
|
|
||||||
const handleRoleSelect = (role: Role) => {
|
const handleRoleSelect = (role: Role) => {
|
||||||
const { editor } = props;
|
const { editor } = props;
|
||||||
@ -47,11 +96,16 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
return (
|
return (
|
||||||
<NodeViewWrapper
|
<NodeViewWrapper
|
||||||
as="span"
|
as="span"
|
||||||
|
ref={tokenRef}
|
||||||
data-alt="character-token"
|
data-alt="character-token"
|
||||||
contentEditable={false}
|
contentEditable={false}
|
||||||
className="relative inline text-blue-400 cursor-pointer hover:text-blue-300 transition-colors duration-200"
|
className="relative inline text-blue-400 cursor-pointer hover:text-blue-300 transition-colors duration-200"
|
||||||
onMouseLeave={() => setShowRoleList(false)}
|
onMouseLeave={() => setShowRoleList(false)}
|
||||||
onMouseEnter={() => setShowRoleList(true)}
|
onMouseEnter={() => {
|
||||||
|
setShowRoleList(true);
|
||||||
|
// 延迟一帧执行位置更新,确保列表已渲染
|
||||||
|
requestAnimationFrame(updateListPosition);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
|
|
||||||
@ -63,7 +117,12 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: 4 }}
|
exit={{ opacity: 0, y: 4 }}
|
||||||
transition={{ duration: 0.2 }}
|
transition={{ duration: 0.2 }}
|
||||||
className="absolute top-full left-0 mt-2 w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-50"
|
ref={listRef}
|
||||||
|
className="fixed w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-[51]"
|
||||||
|
style={{
|
||||||
|
top: listPosition.top,
|
||||||
|
left: listPosition.left
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
{roles.map((role) => {
|
{roles.map((role) => {
|
||||||
@ -93,6 +152,26 @@ export function CharacterToken(props: ReactNodeViewProps) {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{/* 旁白 */}
|
||||||
|
<div
|
||||||
|
data-alt="role-item"
|
||||||
|
className={`flex items-center gap-3 p-2 rounded-lg cursor-pointer transition-colors duration-200
|
||||||
|
${name === '旁白' ? 'bg-blue-500/20 text-blue-400' : 'hover:bg-white/5 text-gray-200'}`}
|
||||||
|
onClick={() => handleRoleSelect({ name: '旁白', url: '' })}
|
||||||
|
>
|
||||||
|
<div className="relative">
|
||||||
|
<CircleUserRound
|
||||||
|
className={`w-10 h-10 rounded-full border transition-all duration-200`}
|
||||||
|
/>
|
||||||
|
{name === '旁白' && (
|
||||||
|
<div className="absolute -top-1 -right-1 bg-blue-500 rounded-full p-0.5">
|
||||||
|
<Check className="w-3 h-3 text-white" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="flex-1">旁白</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
import React, { forwardRef, useRef, useState } from "react";
|
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
||||||
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
import { Plus, X, UserRoundPlus, MessageCirclePlus, MessageCircleMore, ClipboardType } from "lucide-react";
|
||||||
import ShotEditor from "./ShotEditor";
|
import ShotEditor from "./ShotEditor";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
|
||||||
|
|
||||||
|
|
||||||
interface Shot {
|
interface Shot {
|
||||||
id: string;
|
name: string;
|
||||||
shotDescContent: any[];
|
shotDescContent: any[];
|
||||||
shotDialogsContent: any[];
|
shotDialogsContent: any[];
|
||||||
}
|
}
|
||||||
@ -21,7 +23,7 @@ interface CharacterToken {
|
|||||||
|
|
||||||
const mockShotsData = [
|
const mockShotsData = [
|
||||||
{
|
{
|
||||||
id: 'shot1',
|
name: 'shot1',
|
||||||
shotDescContent: [{
|
shotDescContent: [{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [
|
content: [
|
||||||
@ -43,7 +45,7 @@ const mockShotsData = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const createEmptyShot = (): Shot => ({
|
const createEmptyShot = (): Shot => ({
|
||||||
id: `shot${Date.now()}`,
|
name: `shot${Date.now()}`,
|
||||||
shotDescContent: [{
|
shotDescContent: [{
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
content: [
|
content: [
|
||||||
@ -60,14 +62,35 @@ const createEmptyShot = (): Shot => ({
|
|||||||
|
|
||||||
interface ShotsEditorProps {
|
interface ShotsEditorProps {
|
||||||
roles: any[];
|
roles: any[];
|
||||||
|
shotInfo: any[];
|
||||||
|
style?: React.CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles }, ref) => {
|
export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles, shotInfo, style }, ref) => {
|
||||||
const [currentShotIndex, setCurrentShotIndex] = useState(0);
|
const [currentShotIndex, setCurrentShotIndex] = useState(0);
|
||||||
const [shots, setShots] = useState<Shot[]>(mockShotsData);
|
const [shots, setShots] = useState<Shot[]>([]);
|
||||||
const descEditorRef = useRef<any>(null);
|
const descEditorRef = useRef<any>(null);
|
||||||
const dialogEditorRef = useRef<any>(null);
|
const dialogEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('-==========shotInfo===========-', shotInfo);
|
||||||
|
if (shotInfo) {
|
||||||
|
const shots = shotInfo.map((shot) => {
|
||||||
|
return TextToShotAdapter.fromLensType(shot, roles);
|
||||||
|
});
|
||||||
|
console.log('-==========shots===========-', shots);
|
||||||
|
setShots(shots as Shot[]);
|
||||||
|
}
|
||||||
|
}, [shotInfo]);
|
||||||
|
|
||||||
|
const getShotInfo = () => {
|
||||||
|
console.log('-==========shots===========-', shots);
|
||||||
|
const shotInfo = shots.map((shot) => {
|
||||||
|
return TextToShotAdapter.toLensType(shot);
|
||||||
|
});
|
||||||
|
return shotInfo;
|
||||||
|
}
|
||||||
|
|
||||||
const addShot = () => {
|
const addShot = () => {
|
||||||
if (shots.length > 3) {
|
if (shots.length > 3) {
|
||||||
toast.error('不能超过4个分镜', {
|
toast.error('不能超过4个分镜', {
|
||||||
@ -167,16 +190,17 @@ export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles }, ref) =>
|
|||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
React.useImperativeHandle(ref, () => ({
|
React.useImperativeHandle(ref, () => ({
|
||||||
addShot,
|
addShot,
|
||||||
|
getShotInfo
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-2" style={style}>
|
||||||
{/* 分镜标签(可删除)、新增分镜标签 */}
|
{/* 分镜标签(可删除)、新增分镜标签 */}
|
||||||
<div data-alt="shots-tabs" className="flex items-center gap-2">
|
<div data-alt="shots-tabs" className="flex items-center gap-2">
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{shots.map((shot, index) => (
|
{shots.map((shot, index) => (
|
||||||
<div
|
<div
|
||||||
key={shot.id}
|
key={shot.name}
|
||||||
data-alt="shot-tab"
|
data-alt="shot-tab"
|
||||||
className={`group flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer transition-all
|
className={`group flex items-center gap-2 px-3 py-1.5 rounded-md cursor-pointer transition-all
|
||||||
${currentShotIndex === index
|
${currentShotIndex === index
|
||||||
@ -212,60 +236,63 @@ export const ShotsEditor = forwardRef<any, ShotsEditorProps>(({ roles }, ref) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分镜内容 */}
|
{/* 分镜内容 */}
|
||||||
<div className="flex flex-col gap-3 border border-white/10 p-2 rounded-[0.5rem]" key={currentShotIndex}>
|
{shots[currentShotIndex] && (
|
||||||
{/* 分镜描述 添加角色 */}
|
<div className="flex flex-col gap-3 border border-white/10 p-2 rounded-[0.5rem]" key={currentShotIndex} style={{minHeight: 'calc(100% - 2rem)'}}>
|
||||||
<div data-alt="shot-description-section" className="flex flex-col gap-1">
|
{/* 分镜描述 添加角色 */}
|
||||||
<div className="flex items-center gap-2">
|
<div data-alt="shot-description-section" className="flex flex-col gap-1">
|
||||||
<ClipboardType className="w-5 h-5 text-white/60" />
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-lg font-medium text-white/60">分镜描述</span>
|
<ClipboardType className="w-5 h-5 text-white/60" />
|
||||||
<button
|
<span className="text-lg font-medium text-white/60">分镜描述</span>
|
||||||
data-alt="add-character-desc"
|
<button
|
||||||
className="p-1 rounded-md"
|
data-alt="add-character-desc"
|
||||||
onClick={() => handleAddCharacterToDesc()}
|
className="p-1 rounded-md"
|
||||||
>
|
onClick={() => handleAddCharacterToDesc()}
|
||||||
<UserRoundPlus className="w-5 h-5 text-blue-600" />
|
>
|
||||||
</button>
|
<UserRoundPlus className="w-5 h-5 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分镜描述内容 可视化编辑 */}
|
||||||
|
<ShotEditor
|
||||||
|
ref={descEditorRef}
|
||||||
|
content={shots[currentShotIndex].shotDescContent}
|
||||||
|
onCharacterClick={() => {}}
|
||||||
|
roles={roles}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分镜描述内容 可视化编辑 */}
|
{/* 分镜对话 添加角色 添加对话 */}
|
||||||
<ShotEditor
|
<div data-alt="shot-dialog-section" className="flex flex-col gap-1">
|
||||||
ref={descEditorRef}
|
<div className="flex items-center gap-2">
|
||||||
content={shots[currentShotIndex].shotDescContent}
|
<MessageCircleMore className="w-5 h-5 text-white/60" />
|
||||||
onCharacterClick={() => {}}
|
<span className="text-lg font-medium text-white/60">分镜对话</span>
|
||||||
roles={roles}
|
<button
|
||||||
/>
|
data-alt="add-character-dialog"
|
||||||
</div>
|
className="p-1 rounded-md"
|
||||||
|
onClick={() => handleAddCharacterToDialog()}
|
||||||
|
>
|
||||||
|
<UserRoundPlus className="w-5 h-5 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
data-alt="add-new-dialog"
|
||||||
|
className="p-1 rounded-md"
|
||||||
|
onClick={() => handleAddNewDialog()}
|
||||||
|
>
|
||||||
|
<MessageCirclePlus className="w-5 h-5 text-blue-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 分镜对话 添加角色 添加对话 */}
|
{/* 分镜对话内容 可视化编辑 */}
|
||||||
<div data-alt="shot-dialog-section" className="flex flex-col gap-1">
|
<ShotEditor
|
||||||
<div className="flex items-center gap-2">
|
ref={dialogEditorRef}
|
||||||
<MessageCircleMore className="w-5 h-5 text-white/60" />
|
content={shots[currentShotIndex].shotDialogsContent}
|
||||||
<span className="text-lg font-medium text-white/60">分镜对话</span>
|
onCharacterClick={() => {}}
|
||||||
<button
|
roles={roles}
|
||||||
data-alt="add-character-dialog"
|
/>
|
||||||
className="p-1 rounded-md"
|
|
||||||
onClick={() => handleAddCharacterToDialog()}
|
|
||||||
>
|
|
||||||
<UserRoundPlus className="w-5 h-5 text-blue-600" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
data-alt="add-new-dialog"
|
|
||||||
className="p-1 rounded-md"
|
|
||||||
onClick={() => handleAddNewDialog()}
|
|
||||||
>
|
|
||||||
<MessageCirclePlus className="w-5 h-5 text-blue-600" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 分镜对话内容 可视化编辑 */}
|
|
||||||
<ShotEditor
|
|
||||||
ref={dialogEditorRef}
|
|
||||||
content={shots[currentShotIndex].shotDialogsContent}
|
|
||||||
onCharacterClick={() => {}}
|
|
||||||
roles={roles}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -2,18 +2,15 @@
|
|||||||
|
|
||||||
import React, { useRef, useEffect, useState } from 'react';
|
import React, { useRef, useEffect, useState } from 'react';
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
import { Trash2, RefreshCw, Play, Pause, Volume2, VolumeX, Upload, Library, Video, User, MapPin, Settings, Loader2, X, Plus } from 'lucide-react';
|
import { RefreshCw, User, Loader2, X, Plus, Video, CircleX } from 'lucide-react';
|
||||||
import { GlassIconButton } from './glass-icon-button';
|
|
||||||
import { cn } from '@/public/lib/utils';
|
import { cn } from '@/public/lib/utils';
|
||||||
import { ReplaceVideoModal } from './replace-video-modal';
|
|
||||||
import { MediaPropertiesModal } from './media-properties-modal';
|
|
||||||
import { DramaLineChart } from './drama-line-chart';
|
|
||||||
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
import { PersonDetection, PersonDetectionScene } from './person-detection';
|
||||||
import { ShotsEditor } from './shot-editor/ShotsEditor';
|
import { ShotsEditor } from './shot-editor/ShotsEditor';
|
||||||
import { CharacterLibrarySelector } from './character-library-selector';
|
import { CharacterLibrarySelector } from './character-library-selector';
|
||||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||||
import HorizontalScroller from './HorizontalScroller';
|
import HorizontalScroller from './HorizontalScroller';
|
||||||
|
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||||
|
|
||||||
interface ShotTabContentProps {
|
interface ShotTabContentProps {
|
||||||
taskSketch: any[];
|
taskSketch: any[];
|
||||||
@ -30,10 +27,16 @@ export function ShotTabContent({
|
|||||||
isPlaying: externalIsPlaying = true,
|
isPlaying: externalIsPlaying = true,
|
||||||
roles = []
|
roles = []
|
||||||
}: ShotTabContentProps) {
|
}: ShotTabContentProps) {
|
||||||
const editorRef = useRef<any>(null);
|
const {
|
||||||
|
loading,
|
||||||
|
shotData,
|
||||||
|
setSelectedSegment,
|
||||||
|
regenerateVideoSegment
|
||||||
|
} = useEditData('shot');
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
const videoPlayerRef = useRef<HTMLVideoElement>(null);
|
||||||
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
|
const [isPlaying, setIsPlaying] = React.useState(externalIsPlaying);
|
||||||
const [isMediaPropertiesModalOpen, setIsMediaPropertiesModalOpen] = React.useState(false);
|
|
||||||
|
|
||||||
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
const [detections, setDetections] = useState<PersonDetection[]>([]);
|
||||||
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle');
|
const [scanState, setScanState] = useState<'idle' | 'scanning' | 'detected'>('idle');
|
||||||
@ -41,19 +44,21 @@ export function ShotTabContent({
|
|||||||
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
const [isReplaceLibraryOpen, setIsReplaceLibraryOpen] = useState(false);
|
||||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||||
|
|
||||||
const [shots, setShots] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const shotsEditorRef = useRef<any>(null);
|
const shotsEditorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
// 监听当前选中index变化
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('shotTabContent-----shotData', shotData);
|
||||||
|
if (shotData.length > 0) {
|
||||||
|
setSelectedSegment(shotData[selectedIndex]);
|
||||||
|
}
|
||||||
|
}, [selectedIndex]);
|
||||||
|
|
||||||
// 监听外部播放状态变化
|
// 监听外部播放状态变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsPlaying(externalIsPlaying);
|
setIsPlaying(externalIsPlaying);
|
||||||
}, [externalIsPlaying]);
|
}, [externalIsPlaying]);
|
||||||
|
|
||||||
// 确保 taskSketch 是数组
|
|
||||||
const sketches = Array.isArray(taskSketch) ? taskSketch : [];
|
|
||||||
|
|
||||||
// 视频播放控制
|
// 视频播放控制
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoPlayerRef.current) {
|
if (videoPlayerRef.current) {
|
||||||
@ -125,6 +130,18 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 点击按钮重新生成
|
||||||
|
const handleRegenerate = () => {
|
||||||
|
console.log('regenerate');
|
||||||
|
const shotInfo = shotsEditorRef.current.getShotInfo();
|
||||||
|
console.log('shotTabContent-----shotInfo', shotInfo);
|
||||||
|
setSelectedSegment({
|
||||||
|
...shotData[selectedIndex],
|
||||||
|
lens: shotInfo
|
||||||
|
});
|
||||||
|
regenerateVideoSegment();
|
||||||
|
};
|
||||||
|
|
||||||
// 新增分镜
|
// 新增分镜
|
||||||
const handleAddShot = () => {
|
const handleAddShot = () => {
|
||||||
console.log('add shot');
|
console.log('add shot');
|
||||||
@ -134,14 +151,26 @@ export function ShotTabContent({
|
|||||||
// 切换选择分镜
|
// 切换选择分镜
|
||||||
const handleSelectShot = (index: number) => {
|
const handleSelectShot = (index: number) => {
|
||||||
// 切换前 判断数据是否发生变化
|
// 切换前 判断数据是否发生变化
|
||||||
onSketchSelect(index);
|
|
||||||
|
setSelectedIndex(index);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果没有数据,显示空状态
|
// 如果loading 显示loading状态
|
||||||
if (sketches.length === 0) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||||
<p>No sketch data</p>
|
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||||
|
<p>Loading...</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有数据,显示空状态
|
||||||
|
if (shotData.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||||
|
<Video className="w-16 h-16 mb-4" />
|
||||||
|
<p>No video data</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -158,28 +187,41 @@ export function ShotTabContent({
|
|||||||
<HorizontalScroller
|
<HorizontalScroller
|
||||||
itemWidth={128}
|
itemWidth={128}
|
||||||
gap={0}
|
gap={0}
|
||||||
selectedIndex={currentSketchIndex}
|
selectedIndex={selectedIndex}
|
||||||
onItemClick={(i: number) => handleSelectShot(i)}
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{sketches.map((sketch, index) => (
|
{shotData.map((shot, index) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={sketch.id || index}
|
key={shot.id || index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
'relative flex-shrink-0 w-32 aspect-video rounded-lg overflow-hidden cursor-pointer group',
|
||||||
currentSketchIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
selectedIndex === index ? 'ring-2 ring-blue-500' : 'hover:ring-2 hover:ring-blue-500/50'
|
||||||
)}
|
)}
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
whileTap={{ scale: 0.98 }}
|
whileTap={{ scale: 0.98 }}
|
||||||
>
|
>
|
||||||
<video
|
{shot.status === 0 && (
|
||||||
src={sketch.url}
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
className="w-full h-full object-cover"
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
muted
|
</div>
|
||||||
loop
|
)}
|
||||||
playsInline
|
{shot.status === 1 && (
|
||||||
onMouseEnter={(e) => e.currentTarget.play()}
|
<video
|
||||||
onMouseLeave={(e) => e.currentTarget.pause()}
|
src={shot.videoUrl[0]}
|
||||||
/>
|
className="w-full h-full object-cover"
|
||||||
|
muted
|
||||||
|
loop
|
||||||
|
playsInline
|
||||||
|
onMouseEnter={(e) => e.currentTarget.play()}
|
||||||
|
onMouseLeave={(e) => e.currentTarget.pause()}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{/* 任务失败 */}
|
||||||
|
{shot.status === 2 && (
|
||||||
|
<div className="w-full h-full flex items-center justify-center bg-red-500/10">
|
||||||
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
<div className="absolute bottom-0 left-0 right-0 p-1 bg-gradient-to-t from-black/60 to-transparent">
|
||||||
<span className="text-xs text-white/90">Shot {index + 1}</span>
|
<span className="text-xs text-white/90">Shot {index + 1}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -202,14 +244,14 @@ export function ShotTabContent({
|
|||||||
<HorizontalScroller
|
<HorizontalScroller
|
||||||
itemWidth={'auto'}
|
itemWidth={'auto'}
|
||||||
gap={0}
|
gap={0}
|
||||||
selectedIndex={currentSketchIndex}
|
selectedIndex={selectedIndex}
|
||||||
onItemClick={(i: number) => handleSelectShot(i)}
|
onItemClick={(i: number) => handleSelectShot(i)}
|
||||||
>
|
>
|
||||||
{sketches.map((video, index) => {
|
{shotData.map((shot, index) => {
|
||||||
const isActive = currentSketchIndex === index;
|
const isActive = selectedIndex === index;
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
key={video.id || index}
|
key={shot.id || index}
|
||||||
className={cn(
|
className={cn(
|
||||||
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
'flex-shrink-0 cursor-pointer transition-all duration-300',
|
||||||
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
isActive ? 'text-white' : 'text-white/50 hover:text-white/80'
|
||||||
@ -220,10 +262,16 @@ export function ShotTabContent({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="text-sm whitespace-nowrap">
|
<span className="text-sm whitespace-nowrap flex items-center gap-1">
|
||||||
Shot {index + 1}
|
<span className='text-white/50'>Shot {index + 1}</span>
|
||||||
|
{shot.status === 0 && (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
|
)}
|
||||||
|
{shot.status === 2 && (
|
||||||
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
{index < sketches.length - 1 && (
|
{index < shotData.length - 1 && (
|
||||||
<span className="text-white/20">|</span>
|
<span className="text-white/20">|</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -249,94 +297,66 @@ export function ShotTabContent({
|
|||||||
{/* 视频预览和操作 */}
|
{/* 视频预览和操作 */}
|
||||||
<div className="space-y-4 col-span-1">
|
<div className="space-y-4 col-span-1">
|
||||||
{/* 选中的视频预览 */}
|
{/* 选中的视频预览 */}
|
||||||
<motion.div
|
<>
|
||||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
{shotData[selectedIndex]?.status === 0 && (
|
||||||
layoutId={`video-preview-${currentSketchIndex}`}
|
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30">
|
||||||
>
|
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||||
<PersonDetectionScene
|
<span className="text-white/50">Loading...</span>
|
||||||
videoSrc={sketches[currentSketchIndex]?.url}
|
</div>
|
||||||
detections={detections}
|
)}
|
||||||
triggerScan={scanState === 'scanning'}
|
{shotData[selectedIndex]?.status === 1 && (
|
||||||
onScanTimeout={handleScanTimeout}
|
<motion.div
|
||||||
onScanExit={handleScanTimeout}
|
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||||
onDetectionsChange={handleDetectionsChange}
|
layoutId={`video-preview-${selectedIndex}`}
|
||||||
onPersonClick={handlePersonClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* <video
|
|
||||||
ref={videoPlayerRef}
|
|
||||||
src={sketches[currentSketchIndex]?.url}
|
|
||||||
className="w-full h-full object-cover"
|
|
||||||
loop
|
|
||||||
autoPlay={false}
|
|
||||||
playsInline
|
|
||||||
controls
|
|
||||||
muted={isMuted}
|
|
||||||
onTimeUpdate={handleTimeUpdate}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
|
||||||
{/* 人物替换按钮 */}
|
|
||||||
<motion.button
|
|
||||||
onClick={() => handleScan()}
|
|
||||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
|
||||||
${scanState === 'detected'
|
|
||||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
|
||||||
: 'bg-black/50 hover:bg-black/70 text-white'
|
|
||||||
}`}
|
|
||||||
whileHover={{ scale: 1.05 }}
|
|
||||||
whileTap={{ scale: 0.95 }}
|
|
||||||
>
|
>
|
||||||
{scanState === 'scanning' ? (
|
<PersonDetectionScene
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
videoSrc={shotData[selectedIndex]?.videoUrl[0]}
|
||||||
) : scanState === 'detected' ? (
|
detections={detections}
|
||||||
<X className="w-4 h-4" />
|
triggerScan={scanState === 'scanning'}
|
||||||
) : (
|
onScanTimeout={handleScanTimeout}
|
||||||
<User className="w-4 h-4" />
|
onScanExit={handleScanTimeout}
|
||||||
)}
|
onDetectionsChange={handleDetectionsChange}
|
||||||
</motion.button>
|
onPersonClick={handlePersonClick}
|
||||||
{/* 场景替换按钮 */}
|
/>
|
||||||
{/* <motion.button
|
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
||||||
onClick={() => console.log('Replace scene')}
|
{/* 人物替换按钮 */}
|
||||||
className="p-2 bg-black/50 hover:bg-black/70
|
<motion.button
|
||||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
onClick={() => handleScan()}
|
||||||
whileHover={{ scale: 1.05 }}
|
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||||
whileTap={{ scale: 0.95 }}
|
${scanState === 'detected'
|
||||||
>
|
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||||
<MapPin className="w-4 h-4" />
|
: 'bg-black/50 hover:bg-black/70 text-white'
|
||||||
</motion.button> */}
|
}`}
|
||||||
{/* 运镜按钮 */}
|
whileHover={{ scale: 1.05 }}
|
||||||
{/* <motion.button
|
whileTap={{ scale: 0.95 }}
|
||||||
onClick={() => console.log('Replace shot')}
|
>
|
||||||
disabled={true}
|
{scanState === 'scanning' ? (
|
||||||
className="p-2 bg-black/50 hover:bg-black/70
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
text-white rounded-full backdrop-blur-sm transition-colors z-10"
|
) : scanState === 'detected' ? (
|
||||||
whileHover={{ scale: 1.05 }}
|
<X className="w-4 h-4" />
|
||||||
whileTap={{ scale: 0.95 }}
|
) : (
|
||||||
>
|
<User className="w-4 h-4" />
|
||||||
<Video className="w-4 h-4" />
|
)}
|
||||||
</motion.button> */}
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
{/* 更多设置 点击打开 More properties 弹窗 */}
|
</motion.div>
|
||||||
{/* <motion.button
|
)}
|
||||||
className='p-2 bg-black/50 hover:bg-black/70
|
{shotData[selectedIndex]?.status === 2 && (
|
||||||
text-white rounded-full backdrop-blur-sm transition-colors z-10'
|
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
|
||||||
style={{textDecorationLine: 'underline'}}
|
<CircleX className="w-4 h-4 text-red-500" />
|
||||||
onClick={() => setIsMediaPropertiesModalOpen(true)}
|
<span className="text-white/50">任务失败,点击重新生成</span>
|
||||||
whileHover={{ scale: 1.02 }}
|
</div>
|
||||||
whileTap={{ scale: 0.98 }}
|
)}
|
||||||
>
|
</>
|
||||||
<Settings className="w-4 h-4" />
|
|
||||||
</motion.button> */}
|
|
||||||
</motion.div>
|
|
||||||
</motion.div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 基础配置 */}
|
{/* 基础配置 */}
|
||||||
<div className='space-y-4 col-span-1'>
|
<div className='space-y-4 col-span-1' key={selectedIndex}>
|
||||||
<ShotsEditor
|
<ShotsEditor
|
||||||
ref={shotsEditorRef}
|
ref={shotsEditorRef}
|
||||||
roles={roles}
|
roles={roles}
|
||||||
|
shotInfo={shotData[selectedIndex].lens}
|
||||||
|
style={{height: 'calc(100% - 4rem)'}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 重新生成按钮、新增分镜按钮 */}
|
{/* 重新生成按钮、新增分镜按钮 */}
|
||||||
@ -352,7 +372,7 @@ export function ShotTabContent({
|
|||||||
<span>Add Shot</span>
|
<span>Add Shot</span>
|
||||||
</motion.button>
|
</motion.button>
|
||||||
<motion.button
|
<motion.button
|
||||||
onClick={() => console.log('Regenerate')}
|
onClick={() => handleRegenerate()}
|
||||||
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
className="flex items-center justify-center gap-2 px-4 py-3 bg-blue-500/10 hover:bg-blue-500/20
|
||||||
text-blue-500 rounded-lg transition-colors"
|
text-blue-500 rounded-lg transition-colors"
|
||||||
whileHover={{ scale: 1.02 }}
|
whileHover={{ scale: 1.02 }}
|
||||||
@ -367,15 +387,6 @@ export function ShotTabContent({
|
|||||||
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{/* Media Properties 弹窗 */}
|
|
||||||
<MediaPropertiesModal
|
|
||||||
isOpen={isMediaPropertiesModalOpen}
|
|
||||||
onClose={() => setIsMediaPropertiesModalOpen(false)}
|
|
||||||
taskSketch={taskSketch}
|
|
||||||
currentSketchIndex={currentSketchIndex}
|
|
||||||
onSketchSelect={onSketchSelect}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FloatingGlassPanel
|
<FloatingGlassPanel
|
||||||
open={isReplacePanelOpen}
|
open={isReplacePanelOpen}
|
||||||
width='66vw'
|
width='66vw'
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user