This commit is contained in:
海龙 2025-08-19 05:13:36 +08:00
commit 47a85428f5
4 changed files with 175 additions and 69 deletions

View File

@ -1118,6 +1118,19 @@ export const checkShotVideoStatus = async (request: {
return post<ApiResponse<any>>("/check_shot_video_status", request);
};
/**
*
* @description
* @param request -
* @returns Promise<ApiResponse<any>>
*/
export const getNewShotVideo = async (request: {
/** 项目ID */
task_id: string;
}): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>("/movie/check_shot_video_status", request);
};
/**
*
* @description

View File

@ -1,4 +1,4 @@
import { useState, useCallback, useEffect } from "react";
import { useState, useCallback, useEffect, useRef } from "react";
import {
VideoSegmentEditUseCase,
} from "../usecase/ShotEditUsecase";
@ -10,6 +10,7 @@ import { ScriptRoleEntity, VideoSegmentEntity } from "../domain/Entities";
import { LensType, SimpleCharacter } from "../domain/valueObject";
import { getUploadToken, uploadToQiniu } from "@/api/common";
import { SaveEditUseCase } from "../usecase/SaveEditUseCase";
import { getNewShotVideo } from "@/api/video_flow";
/**
* Hook接口
@ -97,7 +98,6 @@ export const useShotService = (): UseShotService => {
setProjectId(projectId);
setVideoSegments(segments);
setScriptRoles(roles);
setIntervalIdHandler(projectId);
} catch (error) {
console.error("获取视频片段列表失败:", error);
} finally {
@ -107,56 +107,6 @@ export const useShotService = (): UseShotService => {
[vidoEditUseCase]
);
const setIntervalIdHandler = async (projectId: string): Promise<void> => {
// 每次执行前先清除之前的定时器,确保只存在一个定时器
if (intervalId) {
clearInterval(intervalId);
setIntervalId(null);
}
// 定义定时任务每5秒执行一次
const newIntervalId = setInterval(async () => {
try {
const { segments } = await vidoEditUseCase.getVideoSegmentList(projectId,()=>{
if (intervalId) {
clearInterval(intervalId);
setIntervalId(null);
}
});
setVideoSegments((prevSegments) => {
const existingSegmentsMap = new Map(
prevSegments.map((segment) => [segment.id, segment])
);
const segmentsToUpdate = segments.filter(
(segment) => segment.id !== selectedSegment?.id
);
segmentsToUpdate.forEach((newSegment) => {
const existingSegment = existingSegmentsMap.get(newSegment.id);
if (existingSegment) {
existingSegmentsMap.set(newSegment.id, {
...existingSegment,
videoUrl: newSegment.videoUrl,
status: newSegment.status,
sketchUrl: newSegment.sketchUrl,
lens: newSegment.lens,
});
} else {
existingSegmentsMap.set(newSegment.id, newSegment);
}
});
return Array.from(existingSegmentsMap.values());
});
} catch (error) {
console.error("定时获取视频片段列表失败:", error);
}
}, 30000);
setIntervalId(newIntervalId);
};
// 组件卸载时清理定时器
useEffect(() => {
return () => {
@ -167,6 +117,145 @@ export const useShotService = (): UseShotService => {
};
}, [intervalId]);
/**
*
* @param taskId - ID
* @returns Promise<void>
*/
// 使用 ref 来跟踪组件是否已卸载
const isComponentMounted = useRef(true);
// 在组件挂载时设置为 true卸载时设置为 false
useEffect(() => {
isComponentMounted.current = true;
return () => {
isComponentMounted.current = false;
};
}, []);
const pollVideoStatus = useCallback(async (taskId: string): Promise<void> => {
const maxAttempts = 60; // 最大轮询次数
const interval = 10000; // 轮询间隔时间(毫秒)
let attempts = 0;
let timeoutId: NodeJS.Timeout;
const poll = async (): Promise<void> => {
// 如果组件已卸载,停止轮询
if (!isComponentMounted.current) {
if (timeoutId) {
clearTimeout(timeoutId);
}
return;
}
try {
const result = await getNewShotVideo({ task_id: taskId });
if (!result?.data) {
// 更新 selectedSegment
setSelectedSegment(prev => ({
...prev!,
videoUrl: [],
video_status: 2 // 设置为失败状态
}));
// 同步更新 videoSegments 中的对应项
setVideoSegments(prev =>
prev.map(segment =>
segment.id === selectedSegment!.id
? {
...segment,
videoUrl: [],
video_status: 2,
status: 2 // 更新片段状态为失败
}
: segment
)
);
}
const { status, urls, message } = result.data;
// 如果任务完成或失败,更新视频片段数据
if (status === 'COMPLETED' || status === 'FAILED') {
if (selectedSegment && status === 'COMPLETED' && urls?.length > 0) {
// 更新 selectedSegment
const updatedVideos = urls.map((url: string) => ({
video_url: url,
video_id: selectedSegment!.id,
video_status: 1
}));
setSelectedSegment(prev => ({
...prev!,
videoUrl: updatedVideos,
video_status: 1 // 设置为已完成状态
}));
// 同步更新 videoSegments 中的对应项
setVideoSegments(prev =>
prev.map(segment =>
segment.id === selectedSegment.id
? {
...segment,
videoUrl: updatedVideos,
video_status: 1,
status: 1 // 更新片段状态为完成
}
: segment
)
);
} else {
// 更新 selectedSegment
setSelectedSegment(prev => ({
...prev!,
videoUrl: [],
video_status: 2 // 设置为失败状态
}));
// 同步更新 videoSegments 中的对应项
setVideoSegments(prev =>
prev.map(segment =>
segment.id === selectedSegment!.id
? {
...segment,
videoUrl: [],
video_status: 2,
status: 2 // 更新片段状态为失败
}
: segment
)
);
}
return;
}
// 如果未完成且未达到最大尝试次数,继续轮询
if (attempts < maxAttempts && isComponentMounted.current) {
attempts++;
timeoutId = setTimeout(poll, interval);
} else {
setSelectedSegment(prev => ({
...prev!,
video_status: 2 // 设置为失败状态
}));
}
} catch (error) {
setSelectedSegment(prev => ({
...prev!,
video_status: 2 // 设置为失败状态
}));
}
};
await poll();
}, [selectedSegment]);
/**
*
* @param shotPrompt
@ -180,8 +269,6 @@ export const useShotService = (): UseShotService => {
try {
setLoading(true);
console.log('shotInfo-selectedSegment', selectedSegment);
// 调用API重新生成视频片段返回任务状态信息
const taskResult = await vidoEditUseCase.regenerateVideoSegment(
projectId,
@ -198,6 +285,7 @@ export const useShotService = (): UseShotService => {
video_ids: [selectedSegment!.id],
},
]);
// 如果重新生成的是现有片段,更新其状态为处理中 (0: 视频加载中)
if (selectedSegment) {
setVideoSegments((prev) =>
@ -205,14 +293,18 @@ export const useShotService = (): UseShotService => {
segment.id === selectedSegment.id
? {
...segment,
videoUrl: [],
video_status: 0,
status: 0, // 设置为视频加载中状态
}
: segment
)
);
// 开始轮询视频生成状态
await pollVideoStatus(taskResult.task_id);
}
setIntervalIdHandler(projectId);
// 返回当前选中的片段因为现在API返回的是任务状态而不是完整的片段
return selectedSegment!;
} catch (error) {
console.error("重新生成视频片段失败:", error);
@ -220,7 +312,7 @@ export const useShotService = (): UseShotService => {
} finally {
setLoading(false);
}
}, [projectId, selectedSegment, vidoEditUseCase]);
}, [projectId, selectedSegment, vidoEditUseCase, pollVideoStatus]);
/**
* AI优化视频内容

View File

@ -166,6 +166,7 @@ CharacterTabContentProps
console.log('Selected shots:', selectedShots);
console.log('Add to library:', addToLibrary);
setIsReplacePanelOpen(false);
onClose();
await applyRoleToSelectedShots(selectedRole || {} as RoleEntity);
if(addToLibrary){
await saveRoleToLibrary();
@ -468,3 +469,5 @@ CharacterTabContentProps
</div>
);
});
CharacterTabContent.displayName = 'CharacterTabContent';

View File

@ -51,15 +51,16 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
const [pendingRegeneration, setPendingRegeneration] = useState(false);
useEffect(() => {
console.log('shotTabContent-----scriptRoles', scriptRoles);
}, [scriptRoles]);
console.log('shotTabContent-----shotData', shotData);
}, [shotData]);
useEffect(() => {
if (pendingRegeneration) {
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens);
regenerateVideoSegment();
setPendingRegeneration(false);
setIsRegenerate(false);
regenerateVideoSegment().then(() => {
setPendingRegeneration(false);
setIsRegenerate(false);
});
}
}, [pendingRegeneration]);
@ -243,14 +244,15 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{shot.status === 0 && (
{(shot.status === 0 || shot.videoUrl.length === 0) && (
<div className="w-full h-full flex items-center justify-center">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
</div>
)}
{shot.status === 1 && shot.videoUrl[0] ? (
{shot.status === 1 && shot.videoUrl[0] && (
<video
src={shot.videoUrl[0].video_url}
key={shot.videoUrl[0].video_url}
className="w-full h-full object-cover"
muted
loop
@ -258,10 +260,6 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
onMouseEnter={(e) => e.currentTarget.play()}
onMouseLeave={(e) => e.currentTarget.pause()}
/>
) : (
<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>
)}
{/* 任务失败 */}
{shot.status === 2 && (
@ -396,7 +394,7 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
</motion.div>
)}
</AnimatePresence>
{(shotData[selectedIndex]?.status === 2 || !shotData[selectedIndex]?.videoUrl.length) && (
{(shotData[selectedIndex]?.status === 2) && (
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10">
<CircleX className="w-4 h-4 text-red-500" />
<span className="text-white/50">Failed, click to regenerate</span>