forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
4cf967de35
@ -1,4 +1,4 @@
|
||||
import { ContentItem, LensType, SimpleCharacter } from '../domain/valueObject';
|
||||
import { ContentItem, LensType, SimpleCharacter, TagValueObject } from '../domain/valueObject';
|
||||
|
||||
// 定义角色属性接口
|
||||
interface CharacterAttributes {
|
||||
@ -8,6 +8,12 @@ interface CharacterAttributes {
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
// 定义高亮属性接口
|
||||
interface HighlightAttributes {
|
||||
text: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
// 定义文本节点接口
|
||||
interface TextNode {
|
||||
type: 'text';
|
||||
@ -20,8 +26,14 @@ interface CharacterTokenNode {
|
||||
attrs: CharacterAttributes;
|
||||
}
|
||||
|
||||
// 定义高亮节点接口
|
||||
interface HighlightNode {
|
||||
type: 'highlightText';
|
||||
attrs: HighlightAttributes;
|
||||
}
|
||||
|
||||
// 定义内容节点类型(文本或角色标记)
|
||||
type ContentNode = TextNode | CharacterTokenNode;
|
||||
type ContentNode = TextNode | CharacterTokenNode | HighlightNode;
|
||||
|
||||
// 定义段落接口
|
||||
interface Paragraph {
|
||||
@ -100,6 +112,68 @@ export class TextToShotAdapter {
|
||||
|
||||
return nodes;
|
||||
}
|
||||
/**
|
||||
* 解析高亮文本,识别tag并转换为节点数组
|
||||
* @param text 要解析的文本
|
||||
* @param tags 标签列表
|
||||
* @returns ContentNode[] 节点数组
|
||||
*/
|
||||
public static parseHighlight(text: string, tags: TagValueObject[]): ContentNode[] {
|
||||
const nodes: ContentNode[] = [];
|
||||
let currentText = text;
|
||||
// 按内容长度降序排序,避免短名称匹配到长名称的一部分
|
||||
const sortedTags = [...tags].sort((a, b) => String(b.content).length - String(a.content).length);
|
||||
|
||||
while (currentText.length > 0) {
|
||||
let matchFound = false;
|
||||
|
||||
// 尝试匹配
|
||||
for (const tag of sortedTags) {
|
||||
if (currentText.startsWith(String(tag.content))) {
|
||||
// 如果当前文本以tag内容开头
|
||||
if (currentText.length > String(tag.content).length) {
|
||||
// 添加标记节点
|
||||
nodes.push({
|
||||
type: 'highlightText',
|
||||
attrs: {
|
||||
text: String(tag.content),
|
||||
color: tag?.color || 'yellow'
|
||||
}
|
||||
});
|
||||
// 移除已处理的tag内容
|
||||
currentText = currentText.slice(String(tag.content).length);
|
||||
matchFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!matchFound) {
|
||||
// 如果没有找到tag匹配,处理普通文本
|
||||
// 查找下一个可能的tag内容位置
|
||||
let nextTagIndex = currentText.length;
|
||||
for (const tag of sortedTags) {
|
||||
const index = currentText.indexOf(String(tag.content));
|
||||
if (index !== -1 && index < nextTagIndex) {
|
||||
nextTagIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加文本节点
|
||||
const textContent = currentText.slice(0, nextTagIndex);
|
||||
if (textContent) {
|
||||
nodes.push({
|
||||
type: 'text',
|
||||
text: textContent
|
||||
});
|
||||
}
|
||||
// 移除已处理的文本
|
||||
currentText = currentText.slice(nextTagIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}
|
||||
private readonly ShotData: Shot;
|
||||
constructor(shotData: Shot) {
|
||||
this.ShotData = shotData;
|
||||
@ -225,4 +299,29 @@ export class TextToShotAdapter {
|
||||
content
|
||||
);
|
||||
}
|
||||
|
||||
public static fromTextToRole(description: string, tags: TagValueObject[]): Paragraph[] {
|
||||
const paragraph: Paragraph = {
|
||||
type: 'paragraph',
|
||||
content: []
|
||||
};
|
||||
const highlightNodes = TextToShotAdapter.parseHighlight(description, tags);
|
||||
paragraph.content.push(...highlightNodes);
|
||||
return [paragraph];
|
||||
}
|
||||
public static fromRoleToText(paragraphs: Paragraph[]): string {
|
||||
let text = '';
|
||||
paragraphs.forEach(paragraph => {
|
||||
paragraph.content.forEach(node => {
|
||||
if (node.type === 'highlightText') {
|
||||
text += node.attrs.text;
|
||||
} else if (node.type === 'text') {
|
||||
text += node.text;
|
||||
} else if (node.type === 'characterToken') {
|
||||
text += node.attrs.name;
|
||||
}
|
||||
});
|
||||
});
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@ -93,6 +93,8 @@ export interface TagValueObject {
|
||||
loadingProgress: number;
|
||||
/** 禁止编辑 */
|
||||
disableEdit: boolean;
|
||||
/** 颜色 */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -46,7 +46,8 @@ export default function WorkFlow() {
|
||||
mode,
|
||||
setIsPauseWorkFlow,
|
||||
setAnyAttribute,
|
||||
applyScript
|
||||
applyScript,
|
||||
fallbackToStep
|
||||
} = useWorkflowData();
|
||||
|
||||
const {
|
||||
@ -225,8 +226,8 @@ export default function WorkFlow() {
|
||||
|
||||
{/* 暂停/播放按钮 */}
|
||||
{
|
||||
currentStep !== '6' && (
|
||||
<div className="absolute right-12 bottom-16 z-[9999] flex gap-4">
|
||||
(currentStep !== '6' && currentStep !== '0') && (
|
||||
<div className="absolute right-12 bottom-16 z-[49] flex gap-4">
|
||||
<GlassIconButton
|
||||
icon={isPauseWorkFlow ? Play : Pause}
|
||||
size='lg'
|
||||
@ -273,6 +274,8 @@ export default function WorkFlow() {
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
setAnyAttribute={setAnyAttribute}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
applyScript={applyScript}
|
||||
fallbackToStep={fallbackToStep}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
||||
@ -2,74 +2,21 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useShotService } from "@/app/service/Interaction/ShotService";
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useRoleServiceHook } from "@/app/service/Interaction/RoleService";
|
||||
|
||||
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 愤怒的脸上,他被压制住了。他发出一声绝望的吼叫,盖过了枪声。'
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
const mockRoleData = [{
|
||||
id: '1',
|
||||
name: 'KAPI',
|
||||
imageUrl: 'https://c.huiying.video/images/420bfb4f-b5d4-475c-a2fb-5e40af770b29.jpg',
|
||||
generateText: 'A 3 to 5-year-old boy with a light to medium olive skin tone, full cheeks, and warm brown eyes. He has short, straight, dark brown hair, neatly styled with a part on his left side. His facial structure includes a small, slightly upturned nose. His lips are typically held in a slight, gentle, closed-mouth smile, which can part to show his small, white teeth.',
|
||||
tags: [
|
||||
{ id: '1', content: 'boy', color: 'red' },
|
||||
{ id: '2', content: '3 to 5-year-old', color: 'yellow' },
|
||||
{ id: '3', content: 'light to medium olive skin tone', color: 'green' },
|
||||
{ id: '4', content: 'full cheeks', color: 'blue' },
|
||||
{ id: '5', content: 'warm brown eyes', color: 'purple' },
|
||||
]
|
||||
}]
|
||||
|
||||
export const useEditData = (tabType: string) => {
|
||||
const searchParams = useSearchParams();
|
||||
@ -77,6 +24,8 @@ export const useEditData = (tabType: string) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [shotData, setShotData] = useState<any[]>([]);
|
||||
|
||||
const [roleData, setRoleData] = useState<any[]>([]);
|
||||
|
||||
const {
|
||||
videoSegments,
|
||||
getVideoSegmentList,
|
||||
@ -85,13 +34,34 @@ export const useEditData = (tabType: string) => {
|
||||
filterRole
|
||||
} = useShotService();
|
||||
|
||||
const {
|
||||
roleList,
|
||||
selectedRole,
|
||||
userRoleLibrary,
|
||||
fetchRoleList,
|
||||
selectRole,
|
||||
fetchUserRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
} = useRoleServiceHook();
|
||||
|
||||
useEffect(() => {
|
||||
if (tabType === 'shot') {
|
||||
getVideoSegmentList(projectId).then(() => {
|
||||
setLoading(false);
|
||||
}).catch((err) => {
|
||||
console.log('useEditData-----err', err);
|
||||
setShotData(mockShotData);
|
||||
setShotData([]);
|
||||
setLoading(false);
|
||||
});
|
||||
} else if (tabType === 'role') {
|
||||
fetchUserRoleLibrary();
|
||||
fetchRoleList(projectId).then(() => {
|
||||
setLoading(false);
|
||||
}).catch((err) => {
|
||||
console.log('useEditData-----err', err);
|
||||
setRoleData([]);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
@ -102,11 +72,25 @@ export const useEditData = (tabType: string) => {
|
||||
setShotData(videoSegments);
|
||||
}, [videoSegments]);
|
||||
|
||||
useEffect(() => {
|
||||
// setRoleData(roleList);
|
||||
setRoleData(mockRoleData);
|
||||
}, [roleList]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
// shot
|
||||
shotData,
|
||||
setSelectedSegment,
|
||||
regenerateVideoSegment,
|
||||
filterRole
|
||||
filterRole,
|
||||
// role
|
||||
roleData,
|
||||
selectRole,
|
||||
selectedRole,
|
||||
userRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData } from '@/api/video_flow';
|
||||
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan } from '@/api/video_flow';
|
||||
import { useAppDispatch, useAppSelector } from '@/lib/store/hooks';
|
||||
import { setSketchCount, setVideoCount } from '@/lib/store/workflowSlice';
|
||||
import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||
@ -96,7 +96,7 @@ export function useWorkflowData() {
|
||||
console.log('开始初始化剧本', originalText);
|
||||
originalText && initializeFromProject(episodeId, originalText).then(() => {
|
||||
console.log('应用剧本');
|
||||
// 默认模式下 应用剧本
|
||||
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
||||
mode.includes('auto') && applyScript();
|
||||
});
|
||||
}, [originalText]);
|
||||
@ -110,7 +110,11 @@ export function useWorkflowData() {
|
||||
}, [scriptBlocksMemo]);
|
||||
// 监听继续 请求更新数据
|
||||
useEffect(() => {
|
||||
|
||||
if (isPauseWorkFlow) {
|
||||
pauseMovieProjectPlan({ project_id: episodeId });
|
||||
} else {
|
||||
resumeMovieProjectPlan({ project_id: episodeId });
|
||||
}
|
||||
}, [isPauseWorkFlow]);
|
||||
|
||||
// 自动开始播放一轮
|
||||
@ -568,6 +572,12 @@ export function useWorkflowData() {
|
||||
}
|
||||
};
|
||||
|
||||
// 回退到 指定状态 重新获取数据
|
||||
const fallbackToStep = (step: string) => {
|
||||
setCurrentStep(step);
|
||||
setNeedStreamData(true);
|
||||
}
|
||||
|
||||
// 重试加载数据
|
||||
const retryLoadData = () => {
|
||||
setDataLoadError(null);
|
||||
@ -617,6 +627,7 @@ export function useWorkflowData() {
|
||||
mode,
|
||||
setIsPauseWorkFlow,
|
||||
setAnyAttribute,
|
||||
applyScript
|
||||
applyScript,
|
||||
fallbackToStep
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { useState, useRef } from "react";
|
||||
import React, { useState, useRef, useEffect, forwardRef } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Sparkles, X, Plus, RefreshCw } from 'lucide-react';
|
||||
import MainEditor from "./main-editor/MainEditor";
|
||||
import { cn } from "@/public/lib/utils";
|
||||
import { TextToShotAdapter } from "@/app/service/adapter/textToShot";
|
||||
import { TagValueObject } from "@/app/service/domain/valueObject";
|
||||
|
||||
interface CharacterEditorProps {
|
||||
className?: string;
|
||||
description: string;
|
||||
highlight: TagValueObject[];
|
||||
onSmartPolish: (text: string) => void;
|
||||
}
|
||||
|
||||
const mockContent = [
|
||||
@ -30,20 +35,50 @@ const mockContent = [
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
export default function CharacterEditor({
|
||||
export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
||||
className,
|
||||
}: CharacterEditorProps) {
|
||||
description,
|
||||
highlight,
|
||||
onSmartPolish
|
||||
}, ref) => {
|
||||
const [isOptimizing, setIsOptimizing] = useState(false);
|
||||
const [content, setContent] = useState<any[]>([]);
|
||||
const [isInit, setIsInit] = useState(true);
|
||||
|
||||
const handleSmartPolish = async () => {
|
||||
|
||||
setIsOptimizing(true);
|
||||
console.log('-==========handleSmartPolish===========-', content);
|
||||
const text = TextToShotAdapter.fromRoleToText(content);
|
||||
console.log('-==========getText===========-', text);
|
||||
onSmartPolish(text);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setIsInit(true);
|
||||
console.log('-==========description===========-', description);
|
||||
console.log('-==========highlight===========-', highlight);
|
||||
const paragraphs = TextToShotAdapter.fromTextToRole(description, highlight);
|
||||
console.log('-==========paragraphs===========-', paragraphs);
|
||||
setContent(paragraphs);
|
||||
setTimeout(() => {
|
||||
setIsInit(false);
|
||||
setIsOptimizing(false);
|
||||
}, 100);
|
||||
}, [description, highlight]);
|
||||
|
||||
// 暴露方法给父组件
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getRoleText: () => {
|
||||
return TextToShotAdapter.fromRoleToText(content);
|
||||
}
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem] pb-12", className)}>
|
||||
{/* 自由输入区域 */}
|
||||
<MainEditor content={mockContent} />
|
||||
{
|
||||
!isInit && <MainEditor content={content} onChangeContent={setContent} />
|
||||
}
|
||||
|
||||
{/* 智能润色按钮 */}
|
||||
<motion.button
|
||||
@ -60,4 +95,4 @@ export default function CharacterEditor({
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@ -2,13 +2,13 @@ import React, { useState, useRef, useEffect } from 'react';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { ImageUp, Library, Play, Pause, RefreshCw, Wand2, Users, Check, ReplaceAll, X, TriangleAlert } from 'lucide-react';
|
||||
import { cn } from '@/public/lib/utils';
|
||||
import CharacterEditor from './character-editor';
|
||||
import { CharacterEditor } from './character-editor';
|
||||
import ImageBlurTransition from './ImageBlurTransition';
|
||||
import FloatingGlassPanel from './FloatingGlassPanel';
|
||||
import { ReplaceCharacterPanel, mockShots, mockCharacter } from './replace-character-panel';
|
||||
import { CharacterLibrarySelector } from './character-library-selector';
|
||||
import HorizontalScroller from './HorizontalScroller';
|
||||
import { useRoleServiceHook } from '@/app/service/Interaction/RoleService';
|
||||
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
|
||||
|
||||
interface Appearance {
|
||||
hairStyle: string;
|
||||
@ -63,8 +63,6 @@ export function CharacterTabContent({
|
||||
onSketchSelect,
|
||||
roles = [mockRole]
|
||||
}: CharacterTabContentProps) {
|
||||
const [localRole, setLocalRole] = useState(mockRole);
|
||||
const [currentRole, setCurrentRole] = useState(roles[currentRoleIndex]);
|
||||
const [isReplacePanelOpen, setIsReplacePanelOpen] = useState(false);
|
||||
const [replacePanelKey, setReplacePanelKey] = useState(0);
|
||||
const [ignoreReplace, setIgnoreReplace] = useState(false);
|
||||
@ -74,14 +72,33 @@ export function CharacterTabContent({
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [enableAnimation, setEnableAnimation] = useState(true);
|
||||
const [showAddToLibrary, setShowAddToLibrary] = useState(true);
|
||||
const {fetchRoleList,roleList,fetchUserRoleLibrary,userRoleLibrary} = useRoleServiceHook()
|
||||
const characterEditorRef = useRef<any>(null);
|
||||
|
||||
const {
|
||||
loading,
|
||||
roleData,
|
||||
selectRole,
|
||||
selectedRole,
|
||||
userRoleLibrary,
|
||||
optimizeRoleText,
|
||||
updateRoleText,
|
||||
regenerateRole
|
||||
} = useEditData('role');
|
||||
|
||||
useEffect(() => {
|
||||
// 从url 获取 episodeId 作为projctId
|
||||
const projectId = new URLSearchParams(window.location.search).get('episodeId');
|
||||
if (projectId) {
|
||||
fetchRoleList(projectId);
|
||||
console.log('-==========roleData===========-', roleData);
|
||||
if (roleData.length > 0) {
|
||||
selectRole(roleData[selectRoleIndex].id);
|
||||
}
|
||||
}, [fetchRoleList]);
|
||||
}, [selectRoleIndex, roleData]);
|
||||
|
||||
const handleSmartPolish = (text: string) => {
|
||||
// 首先更新
|
||||
updateRoleText(text);
|
||||
// 然后调用优化角色文本
|
||||
optimizeRoleText(text);
|
||||
};
|
||||
|
||||
const handleConfirmGotoReplace = () => {
|
||||
setIsRemindReplacePanelOpen(false);
|
||||
setIsReplacePanelOpen(true);
|
||||
@ -94,10 +111,7 @@ export function CharacterTabContent({
|
||||
|
||||
const handleReplaceCharacter = (url: string) => {
|
||||
setEnableAnimation(true);
|
||||
setCurrentRole({
|
||||
...currentRole,
|
||||
url: url
|
||||
});
|
||||
// 替换角色
|
||||
|
||||
setIsReplacePanelOpen(true);
|
||||
};
|
||||
@ -115,7 +129,7 @@ export function CharacterTabContent({
|
||||
};
|
||||
|
||||
const handleChangeRole = (index: number) => {
|
||||
if (currentRole.url !== roles[selectRoleIndex].url && !ignoreReplace) {
|
||||
if (selectedRole?.imageUrl !== roleData[selectRoleIndex].imageUrl && !ignoreReplace) {
|
||||
// 提示 角色已修改,弹出替换角色面板
|
||||
setIsRemindReplacePanelOpen(true);
|
||||
return;
|
||||
@ -125,7 +139,6 @@ export function CharacterTabContent({
|
||||
setIgnoreReplace(false);
|
||||
|
||||
setSelectRoleIndex(index);
|
||||
setCurrentRole(roles[index]);
|
||||
};
|
||||
|
||||
// 从角色库中选择角色
|
||||
@ -146,12 +159,16 @@ export function CharacterTabContent({
|
||||
const handleOpenReplaceLibrary = () => {
|
||||
setIsReplaceLibraryOpen(true);
|
||||
setShowAddToLibrary(true);
|
||||
fetchUserRoleLibrary();
|
||||
};
|
||||
|
||||
const handleRegenerate = () => {
|
||||
console.log('Regenerate');
|
||||
setShowAddToLibrary(true);
|
||||
const text = characterEditorRef.current.getRoleText();
|
||||
console.log('-==========text===========-', text);
|
||||
// 重生前 更新 当前项 generateText
|
||||
updateRoleText(text);
|
||||
// 然后调用重新生成角色
|
||||
regenerateRole();
|
||||
};
|
||||
|
||||
const handleUploadClick = () => {
|
||||
@ -177,12 +194,22 @@ export function CharacterTabContent({
|
||||
event.target.value = '';
|
||||
};
|
||||
|
||||
// 如果loading 显示loading状态
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<div className="w-12 h-12 mb-4 animate-spin rounded-full border-b-2 border-blue-600" />
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 如果没有角色数据,显示占位内容
|
||||
if (!roles || roles.length === 0) {
|
||||
if (roleData.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[400px] text-white/50">
|
||||
<Users className="w-16 h-16 mb-4" />
|
||||
<p>No character data</p>
|
||||
<p>No role data</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -210,7 +237,7 @@ export function CharacterTabContent({
|
||||
selectedIndex={selectRoleIndex}
|
||||
onItemClick={(i: number) => handleChangeRole(i)}
|
||||
>
|
||||
{roleList.map((role, index) => (
|
||||
{roleData.map((role, index) => (
|
||||
<motion.div
|
||||
key={`role-${index}`}
|
||||
className={cn(
|
||||
@ -248,8 +275,8 @@ export function CharacterTabContent({
|
||||
{/* 角色预览图 */}
|
||||
<div className="w-full h-full mx-auto rounded-lg relative group">
|
||||
<ImageBlurTransition
|
||||
src={currentRole.url}
|
||||
alt={currentRole.name}
|
||||
src={roleData[selectRoleIndex].imageUrl || ''}
|
||||
alt={roleData[selectRoleIndex].name}
|
||||
width='100%'
|
||||
height='100%'
|
||||
enableAnimation={enableAnimation}
|
||||
@ -281,7 +308,11 @@ export function CharacterTabContent({
|
||||
{/* 右列:角色信息 */}
|
||||
<div className="space-y-4">
|
||||
<CharacterEditor
|
||||
ref={characterEditorRef}
|
||||
className="min-h-[calc(100%-4rem)]"
|
||||
description={roleData[selectRoleIndex].generateText || ''}
|
||||
highlight={roleData[selectRoleIndex].tags || []}
|
||||
onSmartPolish={handleSmartPolish}
|
||||
/>
|
||||
{/* 重新生成按钮、替换形象按钮 */}
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
|
||||
@ -29,6 +29,7 @@ interface EditModalProps {
|
||||
isPauseWorkFlow: boolean;
|
||||
scriptData: any[] | null;
|
||||
applyScript: any;
|
||||
fallbackToStep: any;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
@ -57,7 +58,8 @@ export function EditModal({
|
||||
setAnyAttribute,
|
||||
isPauseWorkFlow,
|
||||
scriptData,
|
||||
applyScript
|
||||
applyScript,
|
||||
fallbackToStep
|
||||
}: EditModalProps) {
|
||||
const [activeTab, setActiveTab] = useState(activeEditTab);
|
||||
const [currentIndex, setCurrentIndex] = useState(currentSketchIndex);
|
||||
@ -104,6 +106,13 @@ export function EditModal({
|
||||
|
||||
const handleConfirmGotoFallback = () => {
|
||||
console.log('handleConfirmGotoFallback');
|
||||
if (activeTab === '0') {
|
||||
fallbackToStep('0');
|
||||
// 应用剧本
|
||||
applyScript();
|
||||
} else {
|
||||
fallbackToStep('1');
|
||||
}
|
||||
}
|
||||
const handleCloseRemindFallbackPanel = () => {
|
||||
setIsRemindFallbackOpen(false);
|
||||
@ -295,7 +304,7 @@ export function EditModal({
|
||||
<div className="flex flex-col items-center gap-4 text-white py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<TriangleAlert className="w-6 h-6 text-yellow-400" />
|
||||
<p className="text-lg font-medium">将重新生成视频并剪辑,是否需要继续?</p>
|
||||
<p className="text-lg font-medium">The task will be regenerated and edited. Do you want to continue?</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 mt-2">
|
||||
@ -305,7 +314,7 @@ export function EditModal({
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<Undo2 className="w-4 h-4" />
|
||||
继续
|
||||
Continue
|
||||
</button>
|
||||
|
||||
<button
|
||||
@ -314,7 +323,7 @@ export function EditModal({
|
||||
className="px-4 py-2 bg-gray-600 hover:bg-gray-700 rounded-md transition-colors duration-200 flex items-center gap-2"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
取消
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,19 +1,27 @@
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { EditorContent, useEditor } from '@tiptap/react';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import { HighlightTextExtension } from './HighlightText';
|
||||
|
||||
interface MainEditorProps {
|
||||
content: any[];
|
||||
onChangeContent?: (content: any[]) => void;
|
||||
}
|
||||
|
||||
export default function MainEditor({ content }: MainEditorProps) {
|
||||
export default function MainEditor({ content, onChangeContent }: MainEditorProps) {
|
||||
const [renderContent, setRenderContent] = useState<any[]>(content);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeContent?.(renderContent);;
|
||||
}, [renderContent]);
|
||||
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
HighlightTextExtension,
|
||||
],
|
||||
content: { type: 'doc', content: content },
|
||||
content: { type: 'doc', content: renderContent },
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: 'prose prose-invert max-w-none focus:outline-none'
|
||||
@ -23,6 +31,12 @@ export default function MainEditor({ content }: MainEditorProps) {
|
||||
onCreate: ({ editor }) => {
|
||||
editor.setOptions({ editable: true })
|
||||
},
|
||||
onUpdate: ({ editor }) => {
|
||||
const json = editor.getJSON();
|
||||
flushSync(() => {
|
||||
setRenderContent(json.content);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
|
||||
@ -236,7 +236,7 @@ export function ShotTabContent({
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm whitespace-nowrap flex items-center gap-1">
|
||||
<span className='text-white/50'>Segment {index + 1}</span>
|
||||
<span>Segment {index + 1}</span>
|
||||
{shot.status === 0 && (
|
||||
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
|
||||
)}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user