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

View File

@ -256,7 +256,7 @@ export class TextToShotAdapter {
currentScript += node.text;
}
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;
const shot_descriptions = VideoSegmentEntityAdapter.lensTypeToTaskItem(shot_Lens);
// 如果有shot_id先保存分镜数据
if (shot_id) {
await this.saveShotPrompt(project_id, shot_id, shot_descriptions);
}
// if (shot_id) {
// await this.saveShotPrompt(project_id, shot_id, shot_descriptions);
// }
const response = await regenerateShot({
project_id,

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ interface CharacterTokenOptions {
export function CharacterToken(props: ReactNodeViewProps) {
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 extension = props.extension as Node<CharacterTokenOptions>;
const roles = extension.options.roles || [];
@ -31,6 +31,7 @@ export function CharacterToken(props: ReactNodeViewProps) {
// 计算理想的顶部位置在token下方
let top = tokenRect.bottom + 8; // 8px 间距
let left = tokenRect.left;
let bottom = tokenRect.top;
// 检查是否超出底部
if (top + listRect.height > viewportHeight) {
@ -44,10 +45,18 @@ export function CharacterToken(props: ReactNodeViewProps) {
left = viewportWidth - listRect.width - 8;
}
// 检查是否超出顶部
if (bottom - listRect.height < 0) {
// 如果超出顶部将列表显示在token下方
bottom = tokenRect.bottom + 8;
}
// 确保不会超出左侧
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 }}
transition={{ duration: 0.2 }}
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={{
top: listPosition.top,
left: listPosition.left

View File

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

View File

@ -56,10 +56,12 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
useEffect(() => {
if (pendingRegeneration) {
console.log('pendingRegeneration', pendingRegeneration, shotData[selectedIndex]?.lens);
regenerateVideoSegment();
setPendingRegeneration(false);
setIsRegenerate(false);
}
}, [shotData[selectedIndex]?.lens]);
}, [pendingRegeneration]);
// 监听当前选中index变化
useEffect(() => {
@ -179,7 +181,9 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
...shotData[selectedIndex],
lens: shotInfo
});
setPendingRegeneration(true);
setTimeout(() => {
setPendingRegeneration(true);
}, 1000);
};
// 新增分镜
@ -342,55 +346,60 @@ export const ShotTabContent = (props: ShotTabContentProps) => {
<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">
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
<span className="text-white/50">Loading...</span>
</div>
)}
{shotData[selectedIndex]?.status === 1 && (
<motion.div
className="aspect-video rounded-lg overflow-hidden relative group"
layoutId={`video-preview-${selectedIndex}`}
>
<PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
detections={detections}
scanState={scanState}
triggerScan={scanState === 'scanning'}
triggerSuccess={scanState === 'detected'}
onScanTimeout={handleScanTimeout}
onScanExit={handleScanExit}
onDetectionsChange={handleDetectionsChange}
onPersonClick={handlePersonClick}
/>
<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' ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : scanState === 'detected' ? (
<X className="w-4 h-4" />
) : (
<User className="w-4 h-4" />
)}
</motion.button>
<AnimatePresence mode="wait">
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
<motion.div
className="aspect-video rounded-lg overflow-hidden relative group"
key={`video-preview-${selectedIndex}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<PersonDetectionScene
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
detections={detections}
scanState={scanState}
triggerScan={scanState === 'scanning'}
triggerSuccess={scanState === 'detected'}
onScanTimeout={handleScanTimeout}
onScanExit={handleScanExit}
onDetectionsChange={handleDetectionsChange}
onPersonClick={handlePersonClick}
/>
<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' ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : scanState === 'detected' ? (
<X className="w-4 h-4" />
) : (
<User className="w-4 h-4" />
)}
</motion.button>
</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">
<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>
)}
</>