视频生成失败 重新生成

This commit is contained in:
北枳 2025-09-01 21:25:33 +08:00
parent d2dd4e57e4
commit 8012c1e47a
18 changed files with 202 additions and 55 deletions

View File

@ -1,3 +1,4 @@
NEXT_PUBLIC_JAVA_URL = https://77.app.java.auth.qikongjian.com
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
# NEXT_PUBLIC_BASE_URL = https://pre.movieflow.api.huiying.video
NEXT_PUBLIC_API_BASE_URL = https://77.api.qikongjian.com

View File

@ -619,6 +619,10 @@ export interface RoleResponse {
/**缓存 */
character_draft: string;
}
export interface RealRoleResponse {
system_characters: [];
project_characters: RoleResponse[];
}
export interface Role {

View File

@ -16,7 +16,7 @@ import {
} from "@/app/service/domain/valueObject";
import { task_item, VideoSegmentEntityAdapter } from "@/app/service/adapter/oldErrAdapter";
import { VideoFlowProjectResponse, NewCharacterItem, NewCharacterListResponse, CharacterListByProjectWithHighlightResponse, CharacterUpdateAndRegenerateRequest, CharacterUpdateAndRegenerateResponse } from "./DTO/movieEdit";
import { RoleResponse } from "./DTO/movieEdit";
import { RealRoleResponse } from "./DTO/movieEdit";
import { RoleRecognitionResponse } from "./DTO/movieEdit";
/**
@ -1165,14 +1165,14 @@ export const getSimilarCharacters = async (request: {
/**
*
* @param request -
* @returns Promise<ApiResponse<RoleResponse[]>>
* @returns Promise<ApiResponse<RealRoleResponse>>
*/
export const getCharacterListByProjectWithHighlight = async (request: {
/** 项目ID */
project_id: string;
/** 每个角色最多提取的高亮关键词数量 */
max_keywords?: number;
}): Promise<ApiResponse<RoleResponse>> => {
}): Promise<ApiResponse<any>> => {
return post("/character/list_by_project_with_highlight", request);
};

View File

@ -188,7 +188,7 @@ export const useRoleShotServiceHook = (projectId: string,selectRole?:RoleEntity,
shot_id: shot.id, // 单个分镜ID
character_replacements: characterReplacements,
wait_for_completion: false, // 不等待完成,异步处理
character_draft: JSON.stringify(newDraftRoleList)
character_draft: newDraftRoleList ? JSON.stringify(newDraftRoleList) : ""
})
}))

View File

@ -44,7 +44,7 @@ export class RoleEditUseCase {
});
if (response.successful) {
const roleList = this.parseProjectRoleList(response.data);
const roleList = this.parseProjectRoleList(response.data.project_characters);
console.log('roleList', roleList)
return roleList;
} else {

View File

@ -54,7 +54,8 @@ const WorkFlow = React.memo(function WorkFlow() {
fallbackToStep,
originalText,
showGotoCutButton,
generateEditPlan
generateEditPlan,
handleRetryVideo
} = useWorkflowData();
const {
@ -126,6 +127,7 @@ const WorkFlow = React.memo(function WorkFlow() {
showGotoCutButton={showGotoCutButton}
onGotoCut={generateEditPlan}
isSmartChatBoxOpen={isSmartChatBoxOpen}
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
/>
</div>
)}

View File

@ -2,7 +2,7 @@
import React, { useRef, useEffect, useState, SetStateAction, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors } from 'lucide-react';
import { Edit3, Play, Pause, Volume2, VolumeX, Maximize, Minimize, Loader2, X, Scissors, RotateCcw, MessageCircleMore } from 'lucide-react';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { GlassIconButton } from '@/components/ui/glass-icon-button';
import { ScriptRenderer } from '@/components/script-renderer/ScriptRenderer';
@ -29,6 +29,7 @@ interface MediaViewerProps {
showGotoCutButton?: boolean;
onGotoCut: () => void;
isSmartChatBoxOpen: boolean;
onRetryVideo?: (video_id: string) => void;
}
export const MediaViewer = React.memo(function MediaViewer({
@ -47,7 +48,8 @@ export const MediaViewer = React.memo(function MediaViewer({
setVideoPreview,
showGotoCutButton,
onGotoCut,
isSmartChatBoxOpen
isSmartChatBoxOpen,
onRetryVideo
}: MediaViewerProps) {
const mainVideoRef = useRef<HTMLVideoElement>(null);
const finalVideoRef = useRef<HTMLVideoElement>(null);
@ -354,6 +356,13 @@ export const MediaViewer = React.memo(function MediaViewer({
<div className="absolute top-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight
}}>
<Tooltip placement="top" title='Edit'>
<GlassIconButton
icon={Edit3}
size="sm"
onClick={() => handleEditClick('3', 'final')}
/>
</Tooltip>
{showGotoCutButton && (
<Tooltip placement="top" title='Go to AI-powered editing platform'>
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
@ -361,23 +370,6 @@ export const MediaViewer = React.memo(function MediaViewer({
)}
</div>
{/* 操作按钮组 */}
{/* <AnimatePresence>
<motion.div
className="absolute top-4 right-4 z-10 gap-2 hidden group-hover:flex"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2 }}
>
<GlassIconButton
icon={Edit3}
size="sm"
onClick={() => handleEditClick('3', 'final')}
/>
</motion.div>
</AnimatePresence> */}
{/* 底部控制区域 */}
<motion.div
className="absolute bottom-16 left-4 z-10 flex items-center gap-3"
@ -437,9 +429,11 @@ export const MediaViewer = React.memo(function MediaViewer({
{/* 生成失败 */}
{taskObject.videos.data[currentSketchIndex].video_status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center">
<div className="text-red-500 text-2xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<span>Failed</span>
<div
className="text-red-500 text-2xl font-bold flex items-center gap-2"
>
<RotateCcw className="w-10 h-10" />
<span>Retry</span>
</div>
</div>
)}
@ -477,9 +471,20 @@ export const MediaViewer = React.memo(function MediaViewer({
<div className="absolute top-4 right-4 z-[21] flex items-center gap-2 transition-right duration-100" style={{
right: toosBtnRight
}}>
{/* 重试按钮 */}
{taskObject.videos.data[currentSketchIndex].video_status === 2 && (
<Tooltip placement="top" title="Retry video">
<GlassIconButton icon={RotateCcw} size='sm' onClick={() => {
const video = taskObject.videos.data[currentSketchIndex];
if (onRetryVideo && video?.video_id) {
onRetryVideo(video.video_id);
}
}} />
</Tooltip>
)}
{/* 添加到chat去编辑 按钮 */}
<Tooltip placement="top" title="Edit video with chat">
<GlassIconButton icon={Edit3} size='sm' onClick={() => {
<GlassIconButton icon={MessageCircleMore} size='sm' onClick={() => {
const currentVideo = taskObject.videos.data[currentSketchIndex];
if (currentVideo && currentVideo.urls && currentVideo.urls.length > 0 && setVideoPreview) {
setVideoPreview(currentVideo.urls[0], currentVideo.video_id);

View File

@ -4,7 +4,7 @@ import React, { useRef, useEffect, useState, useCallback } from 'react';
import { motion } from 'framer-motion';
import { Skeleton } from '@/components/ui/skeleton';
import { ProgressiveReveal, presets } from '@/components/ui/progressive-reveal';
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video } from 'lucide-react';
import { Loader2, X, SquareUserRound, MapPinHouse, Clapperboard, Video, RotateCcw } from 'lucide-react';
import { TaskObject } from '@/api/DTO/movieEdit';
interface ThumbnailGridProps {
@ -187,7 +187,7 @@ export function ThumbnailGrid({
{taskObject.videos.data[index].video_status === 2 && (
<div className="absolute inset-0 bg-red-500/10 flex items-center justify-center z-20">
<div className="text-red-500 text-xl font-bold flex items-center gap-2">
<X className="w-10 h-10" />
<RotateCcw className="w-10 h-10" />
</div>
</div>
)}

View File

@ -109,10 +109,11 @@ export const useEditData = (tabType: string, originalText?: string) => {
useEffect(() => {
console.log('useEditData-----videoSegments', videoSegments, scriptRoles);
setShotData(videoSegments);
setRoleData(scriptRoles);
// setRoleData(scriptRoles);
}, [videoSegments, scriptRoles]);
useEffect(() => {
console.log('useEditData-----roleList', roleList);
setRoleData(roleList);
// setRoleData(mockRoleData);
}, [roleList]);

View File

@ -2,7 +2,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useSearchParams } from 'next/navigation';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan } from '@/api/video_flow';
import { detailScriptEpisodeNew, getScriptTitle, getRunningStreamData, pauseMovieProjectPlan, resumeMovieProjectPlan, getGenerateEditPlan, regenerateShot } from '@/api/video_flow';
import { useScriptService } from "@/app/service/Interaction/ScriptService";
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
@ -200,9 +200,9 @@ export function useWorkflowData() {
// 收集所有需要更新的状态
let stateUpdates = JSON.stringify(taskCurrent);
// 视频分析
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'IN_PROCESS').length;
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'INIT').length;
let analyze_video_total_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video').length;
if (analyze_video_completed_count === analyze_video_total_count) {
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
setCanGoToCut(true);
}
@ -524,6 +524,29 @@ export function useWorkflowData() {
}
};
// 重试生成视频
const handleRetryVideo = async (video_id: string) => {
try {
// 重置视频状态为生成中
setTaskObject(prev => {
const newState = JSON.parse(JSON.stringify(prev));
const videoIndex = newState.videos.data.findIndex((v: any) => v.video_id === video_id);
if (videoIndex !== -1) {
newState.videos.data[videoIndex].video_status = 0;
}
return newState;
});
// 调用重新生成接口
await regenerateShot({ project_id: episodeId, shot_id: video_id });
// 重新开启轮询 如果轮询结束的话
if (!needStreamData) setNeedStreamData(true);
} catch (error) {
console.error('重试生成视频失败:', error);
}
};
// 回退到 指定状态 重新获取数据
const fallbackToStep = (step: string) => {
console.log('fallbackToStep', step);
@ -564,6 +587,7 @@ export function useWorkflowData() {
originalText: state.originalText,
// showGotoCutButton: from && currentLoadingText.includes('Post-production') ? true : false,
showGotoCutButton: canGoToCut ? true : false,
generateEditPlan
generateEditPlan,
handleRetryVideo
};
}

View File

@ -3,9 +3,9 @@ import { motion, AnimatePresence } from 'framer-motion';
import { SquarePen, Lightbulb, Navigation, Globe, Copy, SendHorizontal, X, Plus } from 'lucide-react';
import { ScriptData, ScriptBlock, ScriptContent, ThemeTagBgColor, ThemeType } from './types';
import ContentEditable, { ContentEditableEvent } from 'react-contenteditable';
import { toast } from 'sonner';
import { SelectDropdown } from '@/components/ui/select-dropdown';
import { TypewriterText } from '@/components/workflow/work-office/common/TypewriterText';
import { msg } from '@/utils/message';
interface ScriptRendererProps {
data: any[];
@ -126,11 +126,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
const handleThemeTagChange = (value: string[]) => {
console.log('主题标签更改', value);
if (value.length > 5) {
toast.error('最多可选择5个主题标签', {
duration: 3000,
position: 'top-center',
richColors: true,
});
msg.error('最多可选择5个主题标签', 3000);
return;
}
setAddThemeTag(value);
@ -208,6 +204,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
default:
return (
<>
{/* 需要权限控制 */}
<AnimatePresence>
{(isHovered || isActive) && (
<motion.div
@ -219,6 +216,9 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
<SquarePen
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
onClick={() => {
// 提示权限不够
msg.error('No permission!');
return;
handleEditBlock(block);
}}
/>
@ -226,7 +226,7 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
className="w-6 h-6 p-1 cursor-pointer text-gray-600 hover:text-blue-500 transition-colors"
onClick={() => {
navigator.clipboard.writeText(block.content.map(item => item.text).join('\n'));
toast.success('Copied!');
msg.success('Copied!');
}}
/>
</motion.div>
@ -237,7 +237,8 @@ export const ScriptRenderer: React.FC<ScriptRendererProps> = ({ data, setIsPause
renderEditBlock(block)
) : (
block.content.map((item, index) => (
<div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
// <div key={index} onDoubleClick={() => handleEditBlock(block)}>{renderContent(item)}</div>
<div key={index}>{renderContent(item)}</div>
))
)}
</div>

View File

@ -12,6 +12,7 @@ interface CharacterEditorProps {
highlight: TagValueObject[];
onSmartPolish: (text: string) => void;
onUpdateText: (text: string) => void;
disabled?: boolean;
}
export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
@ -19,7 +20,8 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
description,
highlight,
onSmartPolish,
onUpdateText
onUpdateText,
disabled
}, ref) => {
const [isOptimizing, setIsOptimizing] = useState(false);
const [content, setContent] = useState<any[]>([]);
@ -70,7 +72,7 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
<div className={cn("space-y-2 border border-white/10 relative p-2 rounded-[0.5rem] pb-12", className)}>
{/* 自由输入区域 */}
{
!isInit && <MainEditor content={content} onChangeContent={handleChangeContent} />
!isInit && <MainEditor content={content} onChangeContent={handleChangeContent} disabled={disabled} />
}
{/* 智能润色按钮 */}

View File

@ -12,6 +12,7 @@ import { useEditData } from '@/components/pages/work-flow/use-edit-data';
import { useSearchParams } from 'next/navigation';
import { RoleEntity } from '@/app/service/domain/Entities';
import { Role } from '@/api/DTO/movieEdit';
import { msg } from '@/utils/message';
interface CharacterTabContentProps {
originalRoles: Role[];
@ -112,6 +113,8 @@ CharacterTabContentProps
const handleSmartPolish = (text: string) => {
// 然后调用优化角色文本
msg.error('No permission!');
return;
optimizeRoleText(text);
};
@ -209,6 +212,8 @@ CharacterTabContentProps
};
const handleOpenReplaceLibrary = async () => {
msg.error('No permission!');
return;
setIsLoadingLibrary(true);
setIsReplaceLibraryOpen(true);
setShowAddToLibrary(true);
@ -217,6 +222,8 @@ CharacterTabContentProps
};
const handleRegenerate = async () => {
msg.error('No permission!');
return;
console.log('Regenerate');
setIsRegenerate(true);
// const text = characterEditorRef.current.getRoleText();
@ -230,6 +237,8 @@ CharacterTabContentProps
};
const handleUploadClick = () => {
msg.error('No permission!');
return;
fileInputRef.current?.click();
};
@ -339,7 +348,7 @@ CharacterTabContentProps
height='100%'
enableAnimation={enableAnimation}
/>
{/* 应用角色按钮 */}
{/* 应用角色按钮 暂时注释需要权限控制 */}
<div className='absolute top-3 right-3 flex gap-2'>
<motion.button
className="p-2 bg-black/50 hover:bg-black/70
@ -373,6 +382,7 @@ CharacterTabContentProps
highlight={selectedRole?.tags || []}
onSmartPolish={handleSmartPolish}
onUpdateText={(text: string) => updateRoleText(text)}
disabled={true}
/>
{/* 重新生成按钮、替换形象按钮 */}
<div className="grid grid-cols-1 gap-2">

View File

@ -13,6 +13,7 @@ import { MusicTabContent } from './music-tab-content';
import FloatingGlassPanel from './FloatingGlassPanel';
import { SaveEditUseCase } from '@/app/service/usecase/SaveEditUseCase';
import { TaskObject } from '@/api/DTO/movieEdit';
import { msg } from '@/utils/message';
interface EditModalProps {
isOpen: boolean;
@ -123,6 +124,8 @@ export function EditModal({
}
const handleSave = () => {
msg.error('No permission!');
return;
console.log('handleSave');
// setIsRemindFallbackOpen(true);
if (activeTab === '0') {
@ -140,6 +143,8 @@ export function EditModal({
}
const handleConfirmGotoFallback = () => {
msg.error('No permission!');
return;
setDisabledBtn(true);
console.log('handleConfirmGotoFallback');
SaveEditUseCase.saveData();
@ -165,6 +170,8 @@ export function EditModal({
}
const handleReset = () => {
msg.error('No permission!');
return;
console.log('handleReset');
// 重置当前tab修改的数据
setIsRemindResetOpen(true);

View File

@ -7,9 +7,10 @@ import { HighlightTextExtension } from './HighlightText';
interface MainEditorProps {
content: any[];
onChangeContent?: (content: any[]) => void;
disabled?: boolean;
}
export default function MainEditor({ content, onChangeContent }: MainEditorProps) {
export default function MainEditor({ content, onChangeContent, disabled }: MainEditorProps) {
const [renderContent, setRenderContent] = useState<any[]>(content);
useEffect(() => {
@ -33,7 +34,7 @@ export default function MainEditor({ content, onChangeContent }: MainEditorProps
},
editorProps: {
attributes: {
class: 'prose prose-invert max-w-none focus:outline-none'
class: `prose prose-invert max-w-none focus:outline-none ${disabled ? 'cursor-not-allowed' : ''}`
},
handleDOMEvents: {
keydown: (view, event) => {
@ -50,7 +51,7 @@ export default function MainEditor({ content, onChangeContent }: MainEditorProps
}
},
onCreate: ({ editor }) => {
editor.setOptions({ editable: true });
editor.setOptions({ editable: !disabled });
},
onUpdate: ({ editor }) => {
try {

View File

@ -13,6 +13,7 @@ import HorizontalScroller from './HorizontalScroller';
import { useEditData } from '@/components/pages/work-flow/use-edit-data';
import { RoleEntity, VideoSegmentEntity } from '@/app/service/domain/Entities';
import { ShotVideo } from '@/api/DTO/movieEdit';
import { msg } from '@/utils/message';
interface ShotTabContentProps {
currentSketchIndex: number;
@ -444,7 +445,11 @@ export const ShotTabContent = forwardRef<
<motion.div className='absolute top-4 right-4 flex gap-2'>
{/* 人物替换按钮 */}
<motion.button
onClick={() => handleScan()}
onClick={() => {
msg.error('No permission!');
return;
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'
@ -496,7 +501,11 @@ export const ShotTabContent = forwardRef<
<span>Add Shot</span>
</motion.button> */}
<motion.button
onClick={() => handleRegenerate()}
onClick={() => {
msg.error('No permission!');
return;
handleRegenerate();
}}
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 disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}

View File

@ -116,10 +116,10 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.19",
"@types/react-grid-layout": "^1.3.5",
"jest": "^30.0.5",
"ts-jest": "^29.4.0"
"jest": "^29.7.0",
"ts-jest": "^29.1.2"
}
}

80
utils/message.tsx Normal file
View File

@ -0,0 +1,80 @@
import { message } from 'antd';
import type { MessageArgsProps } from 'antd/es/message';
/**
*
* antd message API
*/
class MessageUtil {
/**
*
* @param content -
* @param duration - () 3
* @param options -
*/
success(content: string, duration = 3, options?: Omit<MessageArgsProps, 'content'>) {
message.success({ content, duration, ...options });
}
/**
*
* @param content -
* @param duration - () 3
* @param options -
*/
error(content: string, duration = 3, options?: Omit<MessageArgsProps, 'content'>) {
message.error({ content, duration, ...options });
}
/**
*
* @param content -
* @param duration - () 3
* @param options -
*/
warning(content: string, duration = 3, options?: Omit<MessageArgsProps, 'content'>) {
message.warning({ content, duration, ...options });
}
/**
*
* @param content -
* @param duration - () 3
* @param options -
*/
info(content: string, duration = 3, options?: Omit<MessageArgsProps, 'content'>) {
message.info({ content, duration, ...options });
}
/**
*
* @param content -
* @param duration - () 0
* @param options -
* @returns -
*/
loading(content: string, duration = 0, options?: Omit<MessageArgsProps, 'content'>) {
return message.loading({ content, duration, ...options });
}
/**
*
*/
destroy() {
message.destroy();
}
/**
*
* @param options -
*/
config(options: MessageArgsProps) {
message.config(options);
}
}
// 导出单例实例
export const msg = new MessageUtil();
// 为了方便使用,也导出单独的方法
export const { success, error, warning, info, loading, destroy, config } = msg;