forked from 77media/video-flow
Merge branch 'dev' of https://git.qikongjian.com/77media/video-flow into dev
This commit is contained in:
commit
87efe72eeb
@ -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
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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 + ']';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -82,3 +82,5 @@ export const CharacterEditor = forwardRef<any, CharacterEditorProps>(({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
CharacterEditor.displayName = 'CharacterEditor';
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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...'
|
|
||||||
}]
|
|
||||||
}]
|
}]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
setPendingRegeneration(true);
|
setTimeout(() => {
|
||||||
|
setPendingRegeneration(true);
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 新增分镜
|
// 新增分镜
|
||||||
@ -342,55 +346,60 @@ 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">
|
||||||
<motion.div
|
{shotData[selectedIndex]?.status === 1 && shotData[selectedIndex]?.videoUrl.length && (
|
||||||
className="aspect-video rounded-lg overflow-hidden relative group"
|
<motion.div
|
||||||
layoutId={`video-preview-${selectedIndex}`}
|
className="aspect-video rounded-lg overflow-hidden relative group"
|
||||||
>
|
key={`video-preview-${selectedIndex}`}
|
||||||
<PersonDetectionScene
|
initial={{ opacity: 0 }}
|
||||||
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
|
animate={{ opacity: 1 }}
|
||||||
detections={detections}
|
exit={{ opacity: 0 }}
|
||||||
scanState={scanState}
|
>
|
||||||
triggerScan={scanState === 'scanning'}
|
<PersonDetectionScene
|
||||||
triggerSuccess={scanState === 'detected'}
|
videoSrc={shotData[selectedIndex]?.videoUrl[0].video_url}
|
||||||
onScanTimeout={handleScanTimeout}
|
detections={detections}
|
||||||
onScanExit={handleScanExit}
|
scanState={scanState}
|
||||||
onDetectionsChange={handleDetectionsChange}
|
triggerScan={scanState === 'scanning'}
|
||||||
onPersonClick={handlePersonClick}
|
triggerSuccess={scanState === 'detected'}
|
||||||
/>
|
onScanTimeout={handleScanTimeout}
|
||||||
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
onScanExit={handleScanExit}
|
||||||
{/* 人物替换按钮 */}
|
onDetectionsChange={handleDetectionsChange}
|
||||||
<motion.button
|
onPersonClick={handlePersonClick}
|
||||||
onClick={() => handleScan()}
|
/>
|
||||||
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
<motion.div className='absolute top-4 right-4 flex gap-2'>
|
||||||
${scanState === 'detected'
|
{/* 人物替换按钮 */}
|
||||||
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
<motion.button
|
||||||
: 'bg-black/50 hover:bg-black/70 text-white'
|
onClick={() => handleScan()}
|
||||||
}`}
|
className={`p-2 backdrop-blur-sm transition-colors z-10 rounded-full
|
||||||
whileHover={{ scale: 1.05 }}
|
${scanState === 'detected'
|
||||||
whileTap={{ scale: 0.95 }}
|
? 'bg-cyan-500/50 hover:bg-cyan-500/70 text-white'
|
||||||
>
|
: 'bg-black/50 hover:bg-black/70 text-white'
|
||||||
{scanState === 'scanning' ? (
|
}`}
|
||||||
<Loader2 className="w-4 h-4 animate-spin" />
|
whileHover={{ scale: 1.05 }}
|
||||||
) : scanState === 'detected' ? (
|
whileTap={{ scale: 0.95 }}
|
||||||
<X className="w-4 h-4" />
|
>
|
||||||
) : (
|
{scanState === 'scanning' ? (
|
||||||
<User className="w-4 h-4" />
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
)}
|
) : scanState === 'detected' ? (
|
||||||
</motion.button>
|
<X className="w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<User className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
)}
|
||||||
)}
|
</AnimatePresence>
|
||||||
{shotData[selectedIndex]?.status === 2 && (
|
{(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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user