This commit is contained in:
海龙 2025-08-19 02:02:14 +08:00
commit 87efe72eeb
10 changed files with 139 additions and 62 deletions

View File

@ -872,7 +872,18 @@ export const updateShotPrompt = async (request: {
/** 镜头描述 */ /** 镜头描述 */
shot_descriptions: task_item; shot_descriptions: task_item;
}): Promise<ApiResponse<any>> => { }): Promise<ApiResponse<any>> => {
return post("/movie/update_shot_prompt", request); // 过滤掉第一层的空字符串字段
const filteredDesc = Object.entries(request.shot_descriptions).reduce<Record<string, any>>((acc, [key, value]) => {
if (value !== '') {
acc[key] = value;
}
return acc;
}, {});
return post("/movie/update_shot_prompt", {
...request,
shot_descriptions: filteredDesc
});
}; };
/** /**

View File

@ -180,6 +180,8 @@ export const useShotService = (): UseShotService => {
try { try {
setLoading(true); setLoading(true);
console.log('shotInfo-selectedSegment', selectedSegment);
// 调用API重新生成视频片段返回任务状态信息 // 调用API重新生成视频片段返回任务状态信息
const taskResult = await vidoEditUseCase.regenerateVideoSegment( const taskResult = await vidoEditUseCase.regenerateVideoSegment(
projectId, projectId,
@ -209,6 +211,7 @@ export const useShotService = (): UseShotService => {
) )
); );
} }
setIntervalIdHandler(projectId);
// 返回当前选中的片段因为现在API返回的是任务状态而不是完整的片段 // 返回当前选中的片段因为现在API返回的是任务状态而不是完整的片段
return selectedSegment!; return selectedSegment!;
} catch (error) { } catch (error) {

View File

@ -256,7 +256,7 @@ export class TextToShotAdapter {
currentScript += node.text; currentScript += node.text;
} }
if (node.type === 'characterToken') { if (node.type === 'characterToken') {
currentScript = currentScript + node.attrs.name + ' ' + node.attrs.id; currentScript = currentScript + node.attrs.name + ' [' + node.attrs.id + ']';
} }
}); });
} }

View File

@ -188,9 +188,9 @@ export class VideoSegmentEditUseCase {
this.loading = true; this.loading = true;
const shot_descriptions = VideoSegmentEntityAdapter.lensTypeToTaskItem(shot_Lens); const shot_descriptions = VideoSegmentEntityAdapter.lensTypeToTaskItem(shot_Lens);
// 如果有shot_id先保存分镜数据 // 如果有shot_id先保存分镜数据
if (shot_id) { // if (shot_id) {
await this.saveShotPrompt(project_id, shot_id, shot_descriptions); // await this.saveShotPrompt(project_id, shot_id, shot_descriptions);
} // }
const response = await regenerateShot({ const response = await regenerateShot({
project_id, project_id,

View File

@ -119,7 +119,7 @@ export function useWorkflowData() {
}, [scriptBlocksMemo]); }, [scriptBlocksMemo]);
// 监听继续 请求更新数据 // 监听继续 请求更新数据
useUpdateEffect(() => { useUpdateEffect(() => {
if (taskObject.status !== 'IN_PROGRESS') { if (taskObject.status === 'COMPLETED' || taskObject.status === 'FAILED') {
return; return;
} }
if (isPauseWorkFlow) { if (isPauseWorkFlow) {

View File

@ -82,3 +82,5 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
</div> </div>
); );
}); });
CharacterEditor.displayName = 'CharacterEditor';

View File

@ -58,6 +58,7 @@ export function EditModal({
// 添加一个状态来标记是否是从切换tab触发的提醒 // 添加一个状态来标记是否是从切换tab触发的提醒
const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null); const [pendingSwitchTabId, setPendingSwitchTabId] = useState<string | null>(null);
const [disabledBtn, setDisabledBtn] = useState(false); const [disabledBtn, setDisabledBtn] = useState(false);
const [isRemindCloseOpen, setIsRemindCloseOpen] = useState(false);
useEffect(() => { useEffect(() => {
setCurrentIndex(currentSketchIndex); setCurrentIndex(currentSketchIndex);
@ -166,6 +167,23 @@ export function EditModal({
setResetKey(resetKey + 1); setResetKey(resetKey + 1);
} }
const handleClickClose = () => {
// TODO 关闭前 检查 当前tab 下是否有更新 如果有更新 则提醒用户 是否确认应用
// 暂时 默认弹出提醒
setIsRemindCloseOpen(true);
}
const handleConfirmApply = () => {
console.log('handleConfirmApply');
setIsRemindCloseOpen(false);
handleConfirmGotoFallback();
}
const handleCloseRemindClosePanel = () => {
setIsRemindCloseOpen(false);
onClose();
}
const renderTabContent = () => { const renderTabContent = () => {
switch (activeTab) { switch (activeTab) {
case '0': case '0':
@ -235,7 +253,6 @@ export function EditModal({
initial={{ opacity: 0 }} initial={{ opacity: 0 }}
animate={{ opacity: 1 }} animate={{ opacity: 1 }}
exit={{ opacity: 0 }} exit={{ opacity: 0 }}
onClick={onClose}
/> />
{/* 弹窗内容 */} {/* 弹窗内容 */}
@ -290,7 +307,7 @@ export function EditModal({
{/* 关闭按钮 */} {/* 关闭按钮 */}
<motion.button <motion.button
className="p-2 rounded-full hover:bg-white/10 transition-colors" className="p-2 rounded-full hover:bg-white/10 transition-colors"
onClick={onClose} onClick={handleClickClose}
whileHover={{ rotate: 90 }} whileHover={{ rotate: 90 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
> >
@ -407,6 +424,38 @@ export function EditModal({
</div> </div>
</div> </div>
</FloatingGlassPanel> </FloatingGlassPanel>
{/* 提醒用户 关闭当前弹窗 是否确认应用 */}
<FloatingGlassPanel
open={isRemindCloseOpen}
width='500px'
clickMaskClose={false}
>
<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">If you have modified the content, closing it will lose the modified content. Do you want to apply the modifications?</p>
</div>
<div className="flex gap-3 mt-2">
<button
onClick={() => handleConfirmApply()}
data-alt="confirm-replace-button"
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" />
Apply
</button>
<button
onClick={() => handleCloseRemindClosePanel()}
data-alt="ignore-button"
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" />
Ignore
</button>
</div>
</div>
</FloatingGlassPanel>
</> </>
)} )}
</AnimatePresence> </AnimatePresence>

View File

@ -11,7 +11,7 @@ interface CharacterTokenOptions {
export function CharacterToken(props: ReactNodeViewProps) { export function CharacterToken(props: ReactNodeViewProps) {
const [showRoleList, setShowRoleList] = useState(false); const [showRoleList, setShowRoleList] = useState(false);
const [listPosition, setListPosition] = useState({ top: 0, left: 0 }); const [listPosition, setListPosition] = useState({ top: 0, left: 0, bottom: 0 });
const { name } = props.node.attrs as ScriptRoleEntity; const { name } = props.node.attrs as ScriptRoleEntity;
const extension = props.extension as Node<CharacterTokenOptions>; const extension = props.extension as Node<CharacterTokenOptions>;
const roles = extension.options.roles || []; const roles = extension.options.roles || [];
@ -31,6 +31,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
// 计算理想的顶部位置在token下方 // 计算理想的顶部位置在token下方
let top = tokenRect.bottom + 8; // 8px 间距 let top = tokenRect.bottom + 8; // 8px 间距
let left = tokenRect.left; let left = tokenRect.left;
let bottom = tokenRect.top;
// 检查是否超出底部 // 检查是否超出底部
if (top + listRect.height > viewportHeight) { if (top + listRect.height > viewportHeight) {
@ -44,10 +45,18 @@ export function CharacterToken(props: ReactNodeViewProps) {
left = viewportWidth - listRect.width - 8; left = viewportWidth - listRect.width - 8;
} }
// 检查是否超出顶部
if (bottom - listRect.height < 0) {
// 如果超出顶部将列表显示在token下方
bottom = tokenRect.bottom + 8;
}
// 确保不会超出左侧 // 确保不会超出左侧
left = Math.max(8, left); left = Math.max(8, left);
// 确保不会超顶部
top = Math.max(0, top);
setListPosition({ top, left }); setListPosition({ top, left, bottom });
}; };
// 监听窗口大小变化 // 监听窗口大小变化
@ -107,7 +116,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
exit={{ opacity: 0, y: 4 }} exit={{ opacity: 0, y: 4 }}
transition={{ duration: 0.2 }} transition={{ duration: 0.2 }}
ref={listRef} ref={listRef}
className="fixed w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-[51]" className="fixed w-64 rounded-lg backdrop-blur-md bg-white/10 border border-white/20 p-2 z-[51] overflow-y-auto"
style={{ style={{
top: listPosition.top, top: listPosition.top,
left: listPosition.left left: listPosition.left

View File

@ -26,17 +26,11 @@ const createEmptyShot = (): Shot => ({
name: `shot${Date.now()}`, name: `shot${Date.now()}`,
shotDescContent: [{ shotDescContent: [{
type: 'paragraph', type: 'paragraph',
content: [{ content: []
type: 'text',
text: 'Add shot description here...'
}]
}], }],
shotDialogsContent: [{ shotDialogsContent: [{
type: 'paragraph', type: 'paragraph',
content: [{ content: []
type: 'text',
text: 'Add shot dialogue here...'
}]
}] }]
}); });

View File

@ -56,10 +56,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
useEffect(() => { useEffect(() => {
if (pendingRegeneration) { if (pendingRegeneration) {
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens);
regenerateVideoSegment(); regenerateVideoSegment();
setPendingRegeneration(false); setPendingRegeneration(false);
setIsRegenerate(false);
} }
}, [shotData[selectedIndex]?.lens]); }, [pendingRegeneration]);
// 监听当前选中index变化 // 监听当前选中index变化
useEffect(() => { useEffect(() => {
@ -179,7 +181,9 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
...shotData[selectedIndex], ...shotData[selectedIndex],
lens: shotInfo lens: shotInfo
}); });
setTimeout(() => {
setPendingRegeneration(true); setPendingRegeneration(true);
}, 1000);
}; };
// 新增分镜 // 新增分镜
@ -342,16 +346,20 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<div className="space-y-4 col-span-1"> <div className="space-y-4 col-span-1">
{/* 选中的视频预览 */} {/* 选中的视频预览 */}
<> <>
{shotData[selectedIndex]?.status === 0 && ( {(shotData[selectedIndex]?.status === 0) && (
<div className="w-full h-full flex items-center gap-1 justify-center rounded-lg bg-black/30"> <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" /> <Loader2 className="w-4 h-4 animate-spin text-blue-500" />
<span className="text-white/50">Loading...</span> <span className="text-white/50">Loading...</span>
</div> </div>
)} )}
{shotData[selectedIndex]?.status === 1 && ( <AnimatePresence mode="wait">
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
<motion.div <motion.div
className="aspect-video rounded-lg overflow-hidden relative group" className="aspect-video rounded-lg overflow-hidden relative group"
layoutId={`video-preview-${selectedIndex}`} key={`video-preview-${selectedIndex}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
> >
<PersonDetectionScene <PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url} videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
@ -387,10 +395,11 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
</motion.div> </motion.div>
</motion.div> </motion.div>
)} )}
{shotData[selectedIndex]?.status === 2 && ( </AnimatePresence>
{(shotData[selectedIndex]?.status === 2 || !shotData[selectedIndex]?.videoUrl.length) && (
<div className="w-full h-full flex gap-1 items-center justify-center rounded-lg bg-red-500/10"> <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" /> <CircleX className="w-4 h-4 text-red-500" />
<span className="text-white/50"></span> <span className="text-white/50">Failed, click to regenerate</span>
</div> </div>
)} )}
</> </>