forked from 77media/video-flow
Merge branch 'dev' into prod
This commit is contained in:
commit
9541eb8a8d
@ -1,16 +1,16 @@
|
||||
|
||||
# 测试
|
||||
NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai
|
||||
NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com
|
||||
NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback
|
||||
NEXT_PUBLIC_CUT_URL_TO = https://smartcut.huiying.video
|
||||
# NEXT_PUBLIC_JAVA_URL = https://auth.test.movieflow.ai
|
||||
# NEXT_PUBLIC_BASE_URL = https://77.smartvideo.py.qikongjian.com
|
||||
# NEXT_PUBLIC_CUT_URL = https://77.smartcut.py.qikongjian.com
|
||||
# NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.net/api/auth/google/callback
|
||||
# NEXT_PUBLIC_CUT_URL_TO = https://smartcut.huiying.video
|
||||
# 生产
|
||||
# NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai
|
||||
# NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai
|
||||
# NEXT_PUBLIC_CUT_URL = https://smartcut.api.movieflow.ai
|
||||
# NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.ai/api/auth/google/callback
|
||||
# NEXT_PUBLIC_CUT_URL_TO = https://smartcut.movieflow.ai
|
||||
NEXT_PUBLIC_JAVA_URL = https://auth.movieflow.ai
|
||||
NEXT_PUBLIC_BASE_URL = https://api.video.movieflow.ai
|
||||
NEXT_PUBLIC_CUT_URL = https://smartcut.api.movieflow.ai
|
||||
NEXT_PUBLIC_GOOGLE_REDIRECT_URI=https://www.movieflow.ai/api/auth/google/callback
|
||||
NEXT_PUBLIC_CUT_URL_TO = https://smartcut.movieflow.ai
|
||||
# 通用
|
||||
# 当前域名配置
|
||||
NEXT_PUBLIC_FRONTEND_URL = https://www.movieflow.ai
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { ApiResponse } from './common';
|
||||
import { showH5QueueNotification } from '../components/QueueBox/H5QueueNotication';
|
||||
import { notification } from 'antd';
|
||||
import { showQueueNotification } from '../components/QueueBox/QueueNotication';
|
||||
|
||||
/** 队列状态枚举 */
|
||||
export enum QueueStatus {
|
||||
@ -75,7 +74,6 @@ export async function withQueuePolling<T>(
|
||||
const cancel = () => {
|
||||
isCancelled = true;
|
||||
try { closeModal?.(); } catch {}
|
||||
notification.destroy(); // 兼容旧弹层
|
||||
onCancel?.();
|
||||
cancelTokens.delete(pollId);
|
||||
};
|
||||
@ -104,14 +102,14 @@ export async function withQueuePolling<T>(
|
||||
position !== undefined && waiting !== undefined) {
|
||||
// 打开或更新 H5 弹窗(仅允许 Cancel 关闭,Refresh 触发刷新)
|
||||
try { closeModal?.(); } catch {}
|
||||
closeModal = showH5QueueNotification(
|
||||
closeModal = showQueueNotification(
|
||||
position,
|
||||
waiting,
|
||||
status,
|
||||
cancel,
|
||||
async () => {
|
||||
// 触发一次立刻刷新:重置 attempts 的等待,直接递归调用 poll()
|
||||
// 不关闭弹窗,由 showH5QueueNotification 保持打开
|
||||
// 不关闭弹窗,由 showQueueNotification 保持打开
|
||||
attempts = Math.max(0, attempts - 1);
|
||||
}
|
||||
);
|
||||
@ -123,7 +121,6 @@ export async function withQueuePolling<T>(
|
||||
|
||||
// 检查是否达到最大尝试次数
|
||||
if (attempts >= maxAttempts) {
|
||||
notification.destroy(); // 关闭通知
|
||||
throw new Error('Exceeded the maximum polling limit');
|
||||
}
|
||||
|
||||
@ -135,17 +132,14 @@ export async function withQueuePolling<T>(
|
||||
// 如果状态为ready,结束轮询
|
||||
if (response.code !== 202 && response.data) {
|
||||
try { closeModal?.(); } catch {}
|
||||
notification.destroy(); // 兼容旧弹层
|
||||
onSuccess?.(response.data);
|
||||
return response;
|
||||
}
|
||||
|
||||
try { closeModal?.(); } catch {}
|
||||
notification.destroy(); // 兼容旧弹层
|
||||
return response;
|
||||
} catch (error) {
|
||||
try { closeModal?.(); } catch {}
|
||||
notification.destroy(); // 兼容旧弹层
|
||||
if (error instanceof Error) {
|
||||
onError?.(error);
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ interface H5QueueNotificationProps {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
function H5QueueNotificationModal(props: H5QueueNotificationProps) {
|
||||
function QueueNotificationModal(props: H5QueueNotificationProps) {
|
||||
const { position, estimatedMinutes, status, onCancel, onClose } = props;
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@ -39,11 +39,11 @@ function H5QueueNotificationModal(props: H5QueueNotificationProps) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-alt="h5-queue-overlay"
|
||||
data-alt="queue-overlay"
|
||||
className="fixed inset-0 z-[1000] flex items-center justify-center bg-black/60"
|
||||
>
|
||||
<div
|
||||
data-alt="h5-queue-modal"
|
||||
data-alt="queue-modal"
|
||||
className="relative w-11/12 max-w-sm rounded-2xl bg-white/5 border border-white/10 backdrop-blur-xl shadow-2xl p-6 text-white"
|
||||
>
|
||||
{/* 去除右上角关闭按钮,避免除取消以外的关闭路径 */}
|
||||
@ -116,7 +116,7 @@ function H5QueueNotificationModal(props: H5QueueNotificationProps) {
|
||||
* @param {() => void} onCancel - Callback when user confirms cancel.
|
||||
* @returns {() => void} - Close function to dismiss the modal programmatically.
|
||||
*/
|
||||
export function showH5QueueNotification(
|
||||
export function showQueueNotification(
|
||||
position: number,
|
||||
estimatedMinutes: number,
|
||||
status: QueueStatus,
|
||||
@ -128,7 +128,7 @@ export function showH5QueueNotification(
|
||||
}
|
||||
|
||||
const mount = document.createElement('div');
|
||||
mount.setAttribute('data-alt', 'h5-queue-root');
|
||||
mount.setAttribute('data-alt', 'queue-root');
|
||||
document.body.appendChild(mount);
|
||||
|
||||
let root: Root | null = null;
|
||||
@ -151,7 +151,7 @@ export function showH5QueueNotification(
|
||||
};
|
||||
|
||||
root.render(
|
||||
<H5QueueNotificationModal
|
||||
<QueueNotificationModal
|
||||
position={position}
|
||||
estimatedMinutes={estimatedMinutes}
|
||||
status={status}
|
||||
@ -1,237 +0,0 @@
|
||||
import { notification } from 'antd';
|
||||
|
||||
const darkGlassStyle = {
|
||||
background: 'rgba(30, 32, 40, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
WebkitBackdropFilter: 'blur(10px)',
|
||||
border: '1px solid rgba(255, 255, 255, 0.08)',
|
||||
borderRadius: '8px',
|
||||
boxShadow: '0 4px 16px rgba(0, 0, 0, 0.4)',
|
||||
padding: '12px 16px',
|
||||
};
|
||||
|
||||
/** AI导演工作室容器样式 */
|
||||
const studioContainerStyle = {
|
||||
position: 'relative' as const,
|
||||
width: '100%',
|
||||
height: '100px',
|
||||
marginBottom: '16px',
|
||||
background: 'rgba(26, 27, 30, 0.6)',
|
||||
borderRadius: '8px',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
};
|
||||
|
||||
/** AI导演组件 */
|
||||
const AIDirector = () => (
|
||||
<div className="ai-director">
|
||||
<svg width="80" height="80" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
{/* AI导演的圆形头部 */}
|
||||
<circle cx="50" cy="40" r="25" fill="#F6B266"/>
|
||||
{/* 眼睛 */}
|
||||
<circle cx="40" cy="35" r="5" fill="#2A2B2E"/>
|
||||
<circle cx="60" cy="35" r="5" fill="#2A2B2E"/>
|
||||
{/* 笑容 */}
|
||||
<path d="M40 45 Q50 55 60 45" stroke="#2A2B2E" strokeWidth="3" strokeLinecap="round"/>
|
||||
{/* 导演帽 */}
|
||||
<path d="M25 30 H75 V25 H25" fill="#2A2B2E"/>
|
||||
{/* 身体 */}
|
||||
<rect x="35" y="65" width="30" height="25" fill="#F6B266"/>
|
||||
{/* 手臂 - 动画中会移动 */}
|
||||
<rect className="director-arm" x="25" y="70" width="15" height="5" fill="#F6B266"/>
|
||||
<rect className="director-arm" x="60" y="70" width="15" height="5" fill="#F6B266"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 工作进度条组件 */
|
||||
const ProgressTimeline = () => (
|
||||
<div className="progress-timeline" style={{
|
||||
position: 'absolute',
|
||||
bottom: '10px',
|
||||
left: '20px',
|
||||
right: '20px',
|
||||
height: '4px',
|
||||
background: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: '2px',
|
||||
}}>
|
||||
<div className="progress-indicator" style={{
|
||||
width: '30%',
|
||||
height: '100%',
|
||||
background: '#F6B266',
|
||||
borderRadius: '2px',
|
||||
animation: 'progress 2s ease-in-out infinite',
|
||||
}}/>
|
||||
</div>
|
||||
);
|
||||
|
||||
/** 工作台元素组件 */
|
||||
const Workstation = () => (
|
||||
<div className="workstation" style={{
|
||||
position: 'absolute',
|
||||
bottom: '20px',
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-around',
|
||||
}}>
|
||||
{/* 小型场景图标,会在动画中浮动 */}
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<div key={i} className={`scene-icon scene-${i}`} style={{
|
||||
width: '20px',
|
||||
height: '20px',
|
||||
background: 'rgba(246, 178, 102, 0.3)',
|
||||
borderRadius: '4px',
|
||||
animation: `float ${1 + i * 0.5}s ease-in-out infinite alternate`,
|
||||
}}/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* 显示队列等待通知
|
||||
* @param position - 当前队列位置
|
||||
* @param estimatedMinutes - 预计等待分钟数
|
||||
*/
|
||||
export const showQueueNotification = (
|
||||
position: number,
|
||||
estimatedMinutes: number,
|
||||
status: string,
|
||||
onCancel: () => void
|
||||
) => {
|
||||
const notificationKey = 'queueNotification';
|
||||
|
||||
// 创建或更新通知内容
|
||||
const notificationContent = (
|
||||
<div data-alt="queue-notification" style={{ minWidth: '320px' }}>
|
||||
{/* AI导演工作室场景 */}
|
||||
<div style={studioContainerStyle}>
|
||||
<AIDirector />
|
||||
<Workstation />
|
||||
<ProgressTimeline />
|
||||
</div>
|
||||
|
||||
{/* 队列信息 */}
|
||||
<div style={{
|
||||
fontSize: '13px',
|
||||
color: 'rgba(255, 255, 255, 0.9)',
|
||||
marginBottom: '12px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
background: 'rgba(246, 178, 102, 0.1)',
|
||||
padding: '8px 12px',
|
||||
borderRadius: '6px',
|
||||
}}>
|
||||
<span style={{ marginRight: '8px' }}>🎬</span>
|
||||
{status === 'process' ? `Your work is being produced. Please wait until it is completed before creating a new work.` : `Your work is waiting for production at the ${position} position`}
|
||||
</div>
|
||||
|
||||
{/* 预计等待时间 */}
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: 'rgba(255, 255, 255, 0.65)',
|
||||
marginBottom: '12px',
|
||||
}}>
|
||||
{status !== 'process' && `Estimated waiting time: about ${estimatedMinutes} minutes`}
|
||||
</div>
|
||||
|
||||
{/* 取消按钮 */}
|
||||
<button
|
||||
onClick={() => {
|
||||
onCancel?.();
|
||||
notification.destroy(notificationKey);
|
||||
}}
|
||||
style={{
|
||||
color: 'rgb(250 173 20 / 90%)',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: 0,
|
||||
fontSize: '12px',
|
||||
fontWeight: 500,
|
||||
textDecoration: 'underline',
|
||||
textUnderlineOffset: '2px',
|
||||
textDecorationColor: 'rgb(250 173 20 / 60%)',
|
||||
transition: 'all 0.2s ease',
|
||||
}}
|
||||
data-alt="cancel-queue-button"
|
||||
>
|
||||
Cancel queue →
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 打开或更新通知
|
||||
notification.open({
|
||||
key: notificationKey,
|
||||
message: null,
|
||||
description: notificationContent,
|
||||
duration: 0,
|
||||
placement: 'topRight',
|
||||
style: {
|
||||
...darkGlassStyle,
|
||||
border: '1px solid rgba(246, 178, 102, 0.2)',
|
||||
},
|
||||
className: 'director-studio-notification',
|
||||
closeIcon: null,
|
||||
});
|
||||
};
|
||||
|
||||
// 添加必要的CSS动画
|
||||
const styles = `
|
||||
.ai-director {
|
||||
animation: bounce 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.director-arm {
|
||||
transform-origin: center;
|
||||
animation: wave 1s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: translateY(0); }
|
||||
50% { transform: translateY(-5px); }
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0% { transform: rotate(-5deg); }
|
||||
100% { transform: rotate(5deg); }
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0); }
|
||||
100% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes progress {
|
||||
0% { width: 0%; }
|
||||
50% { width: 60%; }
|
||||
100% { width: 30%; }
|
||||
}
|
||||
|
||||
.director-studio-notification {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(100%); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
.scene-0 { animation-delay: 0s; }
|
||||
.scene-1 { animation-delay: 0.2s; }
|
||||
.scene-2 { animation-delay: 0.4s; }
|
||||
`;
|
||||
|
||||
// 将样式注入到页面
|
||||
if (typeof document !== 'undefined') {
|
||||
const styleSheet = document.createElement('style');
|
||||
styleSheet.textContent = styles;
|
||||
document.head.appendChild(styleSheet);
|
||||
}
|
||||
|
||||
// 配置通知
|
||||
notification.config({
|
||||
maxCount: 3,
|
||||
});
|
||||
@ -17,30 +17,12 @@ import { SaveEditUseCase } from "@/app/service/usecase/SaveEditUseCase";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import SmartChatBox from "@/components/SmartChatBox/SmartChatBox";
|
||||
import { Drawer, Tooltip, notification } from 'antd';
|
||||
import { exportVideoWithRetry } from '@/utils/export-service';
|
||||
import { EditPoint as EditPointType } from './work-flow/video-edit/types';
|
||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||
import { H5ProgressToastProvider, useH5ProgressToast } from '@/components/ui/h5-progress-toast';
|
||||
|
||||
const WorkFlow = React.memo(function WorkFlow() {
|
||||
const { isMobile, isTablet, isDesktop } = useDeviceType();
|
||||
// 通过全局事件桥接 H5ProgressToast(Provider 在本组件 JSX 中,逻辑层无法直接使用 hook)
|
||||
const emitToastShow = useCallback((params: { title?: string; progress?: number }) => {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:show', { detail: params }));
|
||||
}, []);
|
||||
const emitToastUpdate = useCallback((params: { title?: string; progress?: number }) => {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:update', { detail: params }));
|
||||
}, []);
|
||||
const emitToastHide = useCallback(() => {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:hide'));
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
console.log("init-WorkFlow");
|
||||
return () => {
|
||||
console.log("unmount-WorkFlow");
|
||||
// 不在卸载时强制隐藏,避免严格模式下二次卸载导致刚显示就被关闭
|
||||
};
|
||||
}, [emitToastHide]);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [isEditModalOpen, setIsEditModalOpen] = React.useState(false);
|
||||
const [activeEditTab, setActiveEditTab] = React.useState('1');
|
||||
@ -53,22 +35,6 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
const [selectedView, setSelectedView] = React.useState<'final' | 'video' | null>(null);
|
||||
|
||||
const [aiEditingResult, setAiEditingResult] = React.useState<any>(null);
|
||||
// const aiEditingButtonRef = useRef<{ handleAIEditing: () => Promise<void> }>(null);
|
||||
const [editingStatus, setEditingStatus] = React.useState<'initial' | 'idle' | 'success' | 'error'>('initial');
|
||||
// const [iframeAiEditingKey, setIframeAiEditingKey] = React.useState<string>(`iframe-ai-editing-${Date.now()}`);
|
||||
const [isEditingInProgress, setIsEditingInProgress] = React.useState(false);
|
||||
const isEditingInProgressRef = useRef(false);
|
||||
// 导出进度状态
|
||||
const [exportProgress, setExportProgress] = React.useState<{
|
||||
status: 'processing' | 'completed' | 'failed';
|
||||
percentage: number;
|
||||
message: string;
|
||||
stage?: string;
|
||||
taskId?: string;
|
||||
} | null>(null);
|
||||
const editingTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const editingProgressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const editingProgressStartRef = useRef<number>(0);
|
||||
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId') || '';
|
||||
@ -76,134 +42,7 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
const userId = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
|
||||
|
||||
SaveEditUseCase.setProjectId(episodeId);
|
||||
const [isHandleEdit, setIsHandleEdit] = React.useState(false);
|
||||
// 使用 ref 存储 handleTestExport 避免循环依赖
|
||||
const handleTestExportRef = useRef<(() => Promise<any>) | null>(null);
|
||||
|
||||
// 导出进度回调处理
|
||||
const handleExportProgress = useCallback((progressData: {
|
||||
status: 'processing' | 'completed' | 'failed';
|
||||
percentage: number;
|
||||
message: string;
|
||||
stage?: string;
|
||||
taskId?: string;
|
||||
}) => {
|
||||
console.log('📊 导出进度更新:', progressData);
|
||||
setExportProgress(progressData);
|
||||
|
||||
// 根据状态显示不同的通知 - 已注释
|
||||
/*
|
||||
if (progressData.status === 'processing') {
|
||||
notification.info({
|
||||
message: '导出进度',
|
||||
description: `${progressData.message} (${progressData.percentage}%)`,
|
||||
placement: 'topRight',
|
||||
duration: 2,
|
||||
key: 'export-progress'
|
||||
});
|
||||
} else if (progressData.status === 'completed') {
|
||||
notification.success({
|
||||
message: '导出成功',
|
||||
description: progressData.message,
|
||||
placement: 'topRight',
|
||||
duration: 5,
|
||||
key: 'export-progress'
|
||||
});
|
||||
} else if (progressData.status === 'failed') {
|
||||
notification.error({
|
||||
message: '导出失败',
|
||||
description: progressData.message,
|
||||
placement: 'topRight',
|
||||
duration: 8,
|
||||
key: 'export-progress'
|
||||
});
|
||||
}
|
||||
*/
|
||||
}, []);
|
||||
// 处理编辑计划生成完成的回调
|
||||
const handleEditPlanGenerated = useCallback(() => {
|
||||
console.log('✨ 编辑计划生成完成,开始AI剪辑');
|
||||
setIsHandleEdit(true);
|
||||
setEditingStatus('idle');
|
||||
// setIsEditingInProgress(true); // 已移除该状态变量
|
||||
isEditingInProgressRef.current = true;
|
||||
|
||||
// 改为调用测试剪辑计划导出按钮方法
|
||||
// aiEditingButtonRef.current?.handleAIEditing();
|
||||
// 使用 ref 调用避免循环依赖
|
||||
setTimeout(() => {
|
||||
handleTestExportRef.current?.();
|
||||
}, 0);
|
||||
|
||||
const title = isMobile ? 'editing...' : 'Performing intelligent editing...';
|
||||
|
||||
// 显示进度提示并启动超时定时器
|
||||
// emitToastShow({ title: title, progress: 0 });
|
||||
// 启动自动推进到 90% 的进度(8分钟)
|
||||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressStartRef.current = Date.now();
|
||||
const totalMs = 8 * 60 * 1000;
|
||||
// editingProgressIntervalRef.current = setInterval(() => {
|
||||
// const elapsed = Date.now() - editingProgressStartRef.current;
|
||||
// const pct = Math.min(90, Math.max(0, Math.floor((elapsed / totalMs) * 90)));
|
||||
// emitToastUpdate({ progress: pct });
|
||||
// }, 250);
|
||||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||||
editingTimeoutRef.current = setTimeout(() => {
|
||||
console.log('❌ Editing timeout - retrying...');
|
||||
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
|
||||
if (editingProgressIntervalRef.current) {
|
||||
clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressIntervalRef.current = null;
|
||||
}
|
||||
// emitToastHide();
|
||||
setTimeout(() => {
|
||||
// emitToastShow({ title: 'Retry intelligent editing...', progress: 0 });
|
||||
// 重试阶段自动推进(5分钟到 90%)
|
||||
if (editingProgressIntervalRef.current) clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressStartRef.current = Date.now();
|
||||
const retryTotalMs = 5 * 60 * 1000;
|
||||
editingProgressIntervalRef.current = setInterval(() => {
|
||||
const elapsed = Date.now() - editingProgressStartRef.current;
|
||||
const pct = Math.min(90, Math.max(0, Math.floor((elapsed / retryTotalMs) * 90)));
|
||||
// emitToastUpdate({ progress: pct });
|
||||
}, 250);
|
||||
if (editingTimeoutRef.current) clearTimeout(editingTimeoutRef.current);
|
||||
editingTimeoutRef.current = setTimeout(() => {
|
||||
console.log('Editing retry failed');
|
||||
localStorage.removeItem(`isLoaded_plan_${episodeId}`);
|
||||
setTimeout(() => {
|
||||
setEditingStatus('error');
|
||||
setIsEditingInProgress(false);
|
||||
isEditingInProgressRef.current = false;
|
||||
if (editingProgressIntervalRef.current) {
|
||||
clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressIntervalRef.current = null;
|
||||
}
|
||||
// emitToastHide();
|
||||
}, 5000);
|
||||
}, 5 * 60 * 1000);
|
||||
}, 200);
|
||||
}, 8 * 60 * 1000);
|
||||
}, [episodeId, emitToastHide, emitToastShow, emitToastUpdate]); // 移除 isEditingInProgress 依赖
|
||||
|
||||
/** 处理导出失败 */
|
||||
const handleExportFailed = useCallback(() => {
|
||||
console.log('Export failed, setting error status');
|
||||
setEditingStatus('error');
|
||||
// setIsEditingInProgress(false); // 已移除该状态变量
|
||||
isEditingInProgressRef.current = false;
|
||||
if (editingTimeoutRef.current) {
|
||||
clearTimeout(editingTimeoutRef.current);
|
||||
editingTimeoutRef.current = null;
|
||||
}
|
||||
if (editingProgressIntervalRef.current) {
|
||||
clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressIntervalRef.current = null;
|
||||
}
|
||||
// emitToastHide();
|
||||
}, [emitToastHide]);
|
||||
|
||||
// 使用自定义 hooks 管理状态
|
||||
const {
|
||||
taskObject,
|
||||
@ -222,12 +61,8 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
showGotoCutButton,
|
||||
generateEditPlan,
|
||||
handleRetryVideo,
|
||||
isShowAutoEditing,
|
||||
aspectRatio
|
||||
} = useWorkflowData({
|
||||
onEditPlanGenerated: handleEditPlanGenerated,
|
||||
editingStatus: editingStatus,
|
||||
onExportFailed: handleExportFailed
|
||||
});
|
||||
|
||||
const {
|
||||
@ -251,63 +86,11 @@ const WorkFlow = React.memo(function WorkFlow() {
|
||||
}
|
||||
}, [taskObject?.final?.url]);
|
||||
|
||||
// 监听粗剪是否完成
|
||||
useEffect(() => {
|
||||
console.log('🎬 final video useEffect triggered:', {
|
||||
finalUrl: taskObject.final.url,
|
||||
isHandleEdit
|
||||
});
|
||||
|
||||
if (taskObject.final.url && isHandleEdit) {
|
||||
console.log('🎉 显示编辑完成通知');
|
||||
// 完成:推进到 100 并清理超时计时器
|
||||
if (editingTimeoutRef.current) {
|
||||
clearTimeout(editingTimeoutRef.current);
|
||||
editingTimeoutRef.current = null;
|
||||
}
|
||||
if (editingProgressIntervalRef.current) {
|
||||
clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressIntervalRef.current = null;
|
||||
}
|
||||
// emitToastUpdate({ title: 'Editing successful', progress: 100 });
|
||||
console.log('Editing successful');
|
||||
localStorage.setItem(`isLoaded_plan_${episodeId}`, 'true');
|
||||
setEditingStatus('success');
|
||||
setIsEditingInProgress(false);
|
||||
isEditingInProgressRef.current = false;
|
||||
// setTimeout(() => {
|
||||
// emitToastHide();
|
||||
// }, 3000);
|
||||
}
|
||||
}, [taskObject.final, isHandleEdit, episodeId, emitToastHide, emitToastUpdate]);
|
||||
|
||||
const handleEditModalOpen = useCallback((tab: string) => {
|
||||
setActiveEditTab(tab);
|
||||
setIsEditModalOpen(true);
|
||||
}, []);
|
||||
|
||||
// AI剪辑回调函数
|
||||
const handleAIEditingComplete = useCallback((finalVideoUrl: string) => {
|
||||
console.log('🎉 AI剪辑完成,最终视频URL:', finalVideoUrl);
|
||||
|
||||
// 更新任务对象的最终视频状态
|
||||
setAnyAttribute('final', {
|
||||
url: finalVideoUrl,
|
||||
note: 'ai_edited'
|
||||
});
|
||||
|
||||
// 切换到最终视频阶段
|
||||
setAnyAttribute('currentStage', 'final_video');
|
||||
|
||||
// setAiEditingInProgress(false); // 已移除该状态变量
|
||||
}, [setAnyAttribute]);
|
||||
|
||||
const handleAIEditingError = useCallback((error: string) => {
|
||||
console.error('❌ AI剪辑失败:', error);
|
||||
// 这里可以显示错误提示
|
||||
// setAiEditingInProgress(false); // 已移除该状态变量
|
||||
}, []);
|
||||
|
||||
// 视频编辑描述提交处理函数
|
||||
const handleVideoEditDescriptionSubmit = useCallback((editPoint: EditPointType, description: string) => {
|
||||
console.log('🎬 视频编辑描述提交:', { editPoint, description });
|
||||
@ -339,77 +122,6 @@ Please process this video editing request.`;
|
||||
});
|
||||
}, [currentSketchIndex, isSmartChatBoxOpen]);
|
||||
|
||||
// 测试导出接口的处理函数(使用封装的导出服务)
|
||||
const handleTestExport = useCallback(async () => {
|
||||
console.log('🧪 开始测试导出接口...');
|
||||
console.log('📊 当前taskObject状态:', {
|
||||
currentStage: taskObject.currentStage,
|
||||
videosCount: taskObject.videos?.data?.length || 0,
|
||||
completedVideos: taskObject.videos?.data?.filter(v => v.video_status === 1).length || 0
|
||||
});
|
||||
|
||||
try {
|
||||
// 使用封装的导出服务,传递进度回调
|
||||
const result = await exportVideoWithRetry(episodeId, taskObject, handleExportProgress);
|
||||
console.log('🎉 导出服务完成,结果:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ 导出服务失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}, [episodeId, taskObject, handleExportProgress]);
|
||||
|
||||
// 将 handleTestExport 赋值给 ref
|
||||
React.useEffect(() => {
|
||||
handleTestExportRef.current = handleTestExport;
|
||||
}, [handleTestExport]);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// iframe智能剪辑回调函数 - 已注释
|
||||
/*
|
||||
const handleIframeAIEditingComplete = useCallback((result: any) => {
|
||||
console.log('🎉 iframe AI剪辑完成,结果:', result);
|
||||
|
||||
// 保存剪辑结果
|
||||
setAiEditingResult(result);
|
||||
|
||||
// 更新任务对象的最终视频状态
|
||||
setAnyAttribute('final', {
|
||||
url: result.videoUrl,
|
||||
note: 'ai_edited_iframe'
|
||||
});
|
||||
|
||||
// 切换到最终视频阶段
|
||||
setAnyAttribute('currentStage', 'final_video');
|
||||
|
||||
// setAiEditingInProgress(false); // 已移除该状态变量
|
||||
}, [setAnyAttribute]);
|
||||
*/
|
||||
|
||||
/*
|
||||
const handleIframeAIEditingError = useCallback((error: string) => {
|
||||
console.error('❌ iframe AI剪辑失败:', error);
|
||||
// setAiEditingInProgress(false); // 已移除该状态变量
|
||||
}, []);
|
||||
*/
|
||||
|
||||
/*
|
||||
const handleIframeAIEditingProgress = useCallback((progress: number, message: string) => {
|
||||
console.log(`📊 AI剪辑进度: ${progress}% - ${message}`);
|
||||
setAiEditingInProgress(true);
|
||||
// 收到显式进度时停止自动推进,防止倒退
|
||||
if (editingProgressIntervalRef.current) {
|
||||
clearInterval(editingProgressIntervalRef.current);
|
||||
editingProgressIntervalRef.current = null;
|
||||
}
|
||||
emitToastUpdate({ title: message, progress });
|
||||
}, [emitToastUpdate]);
|
||||
*/
|
||||
|
||||
return (
|
||||
<H5ProgressToastProvider>
|
||||
<H5ToastBridge />
|
||||
@ -441,7 +153,6 @@ Please process this video editing request.`;
|
||||
currentLoadingText={currentLoadingText}
|
||||
roles={taskObject.roles.data}
|
||||
isPauseWorkFlow={isPauseWorkFlow}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
setIsPauseWorkFlow={setIsPauseWorkFlow}
|
||||
/>
|
||||
@ -478,7 +189,7 @@ Please process this video editing request.`;
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
showGotoCutButton={showGotoCutButton}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
@ -535,8 +246,6 @@ Please process this video editing request.`;
|
||||
setPreviewVideoUrl(url);
|
||||
setPreviewVideoId(id);
|
||||
}}
|
||||
showGotoCutButton={showGotoCutButton || editingStatus !== 'idle'}
|
||||
onGotoCut={generateEditPlan}
|
||||
isSmartChatBoxOpen={isSmartChatBoxOpen}
|
||||
onRetryVideo={(video_id) => handleRetryVideo(video_id)}
|
||||
onSelectView={(view) => setSelectedView(view)}
|
||||
@ -585,68 +294,6 @@ Please process this video editing request.`;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI剪辑按钮 - 已注释,不加载iframe */}
|
||||
{/*
|
||||
{
|
||||
isShowAutoEditing && (
|
||||
<div className="fixed right-[2rem] top-[8rem] z-[49]">
|
||||
<Tooltip title="AI智能剪辑" placement="left">
|
||||
<AIEditingIframeButton
|
||||
key={iframeAiEditingKey}
|
||||
ref={aiEditingButtonRef}
|
||||
projectId={episodeId}
|
||||
token={localStorage.getItem("token") || ""}
|
||||
userId={userId.toString()}
|
||||
size="md"
|
||||
onComplete={handleIframeAIEditingComplete}
|
||||
onError={handleIframeAIEditingError}
|
||||
onProgress={handleIframeAIEditingProgress}
|
||||
autoStart={process.env.NODE_ENV !== 'development'}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
*/}
|
||||
|
||||
{/* 导出进度显示 - 已注释 */}
|
||||
{/*
|
||||
{exportProgress && exportProgress.status === 'processing' && (
|
||||
<div className="fixed right-[1rem] bottom-[20rem] z-[49]">
|
||||
<div className="backdrop-blur-lg bg-black/30 border border-white/20 rounded-lg p-4 max-w-xs">
|
||||
<div className="text-white text-sm mb-2">
|
||||
导出进度: {exportProgress.percentage}%
|
||||
</div>
|
||||
<div className="w-full bg-gray-700 rounded-full h-2 mb-2">
|
||||
<div
|
||||
className="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${exportProgress.percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-gray-300 text-xs">
|
||||
{exportProgress.message}
|
||||
{exportProgress.stage && ` (${exportProgress.stage})`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
*/}
|
||||
|
||||
{/* 测试导出接口按钮 - 隐藏显示(仍可通过逻辑调用) */}
|
||||
<div
|
||||
className="fixed right-[1rem] bottom-[16rem] z-[49]"
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<Tooltip title="测试剪辑计划导出接口" placement="left">
|
||||
<GlassIconButton
|
||||
icon={TestTube}
|
||||
size='md'
|
||||
onClick={handleTestExport}
|
||||
className="backdrop-blur-lg"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
{/* 智能对话按钮 */}
|
||||
<div
|
||||
className={`fixed right-[1rem] z-[49] ${isMobile ? 'bottom-[9rem]' : 'bottom-[10rem]'}`}
|
||||
|
||||
@ -35,10 +35,6 @@ interface H5MediaViewerProps {
|
||||
onOpenChat?: () => void;
|
||||
/** 设置聊天预览视频 */
|
||||
setVideoPreview?: (url: string, id: string) => void;
|
||||
/** 显示跳转至剪辑平台按钮 */
|
||||
showGotoCutButton?: boolean;
|
||||
/** 跳转至剪辑平台 */
|
||||
onGotoCut?: () => void;
|
||||
/** 智能对话是否打开(H5可忽略布局调整,仅占位) */
|
||||
isSmartChatBoxOpen?: boolean;
|
||||
/** 失败重试生成视频 */
|
||||
@ -210,8 +206,6 @@ export function H5MediaViewer({
|
||||
setCurrentSketchIndex,
|
||||
onOpenChat,
|
||||
setVideoPreview,
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
isSmartChatBoxOpen,
|
||||
onRetryVideo,
|
||||
onSelectView,
|
||||
@ -369,9 +363,6 @@ export function H5MediaViewer({
|
||||
<video
|
||||
ref={(el) => (videoRefs.current[idx] = el)}
|
||||
className="w-full h-full object-contain [transform:translateZ(0)] [backface-visibility:hidden] [will-change:transform] bg-black cursor-pointer"
|
||||
style={{
|
||||
maxHeight: '100%',
|
||||
}}
|
||||
src={url}
|
||||
preload="metadata"
|
||||
playsInline
|
||||
@ -453,9 +444,7 @@ export function H5MediaViewer({
|
||||
return (
|
||||
<div key={`h5-image-${idx}`} data-alt="image-slide" className="relative w-full h-full flex justify-center">
|
||||
{showImage ? (
|
||||
<img src={url} alt="scene" className="w-full h-full object-contain" style={{
|
||||
maxHeight: '100%',
|
||||
}} />
|
||||
<img src={url} alt="scene" className="w-full h-full object-contain" />
|
||||
) : (
|
||||
<div className="w-full aspect-auto min-h-[200px] flex items-center justify-center bg-black/10 relative" data-alt="image-status" style={{
|
||||
height: imageWrapperHeight,
|
||||
@ -632,7 +621,6 @@ export function H5MediaViewer({
|
||||
if (hasFinalVideo) {
|
||||
all.push(taskObject.final.url);
|
||||
}
|
||||
console.log('h5-media-viewer:all', all);
|
||||
await downloadAllVideos(all);
|
||||
},
|
||||
});
|
||||
|
||||
@ -27,8 +27,16 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
currentLoadingText,
|
||||
className
|
||||
}) => {
|
||||
/** Check if task has failed */
|
||||
const isFailed = taskObject.status === 'FAILED'
|
||||
|
||||
/** Calculate current stage based on taskObject state */
|
||||
const currentStage = useMemo(() => {
|
||||
/** If task failed, show at final stage */
|
||||
if (isFailed) {
|
||||
return 3 /** Final stage to show failure */
|
||||
}
|
||||
|
||||
/** Check if roles & scenes are completed */
|
||||
const rolesCompleted =
|
||||
taskObject.roles?.total_count > 0 &&
|
||||
@ -71,6 +79,7 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
|
||||
return 0 /** Default to script stage */
|
||||
}, [
|
||||
isFailed,
|
||||
taskObject.currentStage,
|
||||
taskObject.roles?.data,
|
||||
taskObject.roles?.total_count,
|
||||
@ -134,6 +143,10 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
return Math.min(Math.round((videosCount / videosTotal) * 100), 95)
|
||||
|
||||
case 3: /** Final video stage */
|
||||
/** If task failed, show 60% progress */
|
||||
if (isFailed) {
|
||||
return 60
|
||||
}
|
||||
/** If final.url exists, show 100% */
|
||||
if (taskObject.final?.url) {
|
||||
return 100
|
||||
@ -164,9 +177,9 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
segmentProgress
|
||||
}
|
||||
})
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
currentStage,
|
||||
isFailed,
|
||||
scriptData,
|
||||
taskObject.roles?.data,
|
||||
taskObject.roles?.total_count,
|
||||
@ -185,6 +198,8 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
<div data-alt="progress-segments" className="flex items-center gap-1 relative">
|
||||
{segments.map(({ stage, config, isCompleted, isCurrent, segmentProgress }) => {
|
||||
const Icon = config.icon
|
||||
/** Check if this is the last stage and task has failed */
|
||||
const isFailedStage = isFailed && stage === 3
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -197,7 +212,7 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
data-alt="progress-fill"
|
||||
className="absolute inset-0 rounded-full z-0 backdrop-blur-md"
|
||||
style={{
|
||||
background: `${config.color}80`
|
||||
background: isFailedStage ? '#ef444480' : `${config.color}80`
|
||||
}}
|
||||
initial={{ width: '0%' }}
|
||||
animate={{ width: `${segmentProgress}%` }}
|
||||
@ -219,13 +234,13 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
x: '-50%',
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
rotate: [0, 360],
|
||||
rotate: isFailedStage ? 0 : [0, 360],
|
||||
transition: {
|
||||
left: { duration: 0.6, ease: 'easeInOut' },
|
||||
x: { duration: 0 },
|
||||
opacity: { duration: 0.3 },
|
||||
scale: { duration: 0.3 },
|
||||
rotate: { duration: 2, repeat: Infinity, ease: 'linear' }
|
||||
rotate: isFailedStage ? { duration: 0 } : { duration: 2, repeat: Infinity, ease: 'linear' }
|
||||
}
|
||||
}}
|
||||
exit={{
|
||||
@ -238,8 +253,8 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
data-alt="icon-wrapper"
|
||||
className="w-3 h-3 rounded-full bg-white/90 backdrop-blur-sm flex items-center justify-center shadow-lg"
|
||||
style={{
|
||||
boxShadow: `0 0 8px ${config.color}80`,
|
||||
background: `${config.color}`
|
||||
boxShadow: isFailedStage ? '0 0 8px #ef444480' : `0 0 8px ${config.color}80`,
|
||||
background: isFailedStage ? '#ef4444' : `${config.color}`
|
||||
}}
|
||||
>
|
||||
{/* <Icon className="w-2 h-2" style={{ color: config.color }} /> */}
|
||||
@ -250,7 +265,7 @@ const H5ProgressBar: React.FC<H5ProgressBarProps> = ({
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Glow effect for current stage */}
|
||||
{isCurrent && segmentProgress < 100 && (
|
||||
{isCurrent && segmentProgress < 100 && !isFailedStage && (
|
||||
<motion.div
|
||||
data-alt="glow-effect"
|
||||
className="absolute inset-0 rounded-full z-10"
|
||||
|
||||
@ -21,7 +21,6 @@ interface TaskInfoProps {
|
||||
currentLoadingText: string;
|
||||
roles: any[];
|
||||
isPauseWorkFlow: boolean;
|
||||
showGotoCutButton: boolean;
|
||||
onGotoCut?: () => void;
|
||||
setIsPauseWorkFlow: (isPauseWorkFlow: boolean) => void;
|
||||
}
|
||||
@ -136,7 +135,6 @@ export function TaskInfo({
|
||||
currentLoadingText,
|
||||
roles,
|
||||
isPauseWorkFlow,
|
||||
showGotoCutButton,
|
||||
onGotoCut,
|
||||
setIsPauseWorkFlow
|
||||
}: TaskInfoProps) {
|
||||
@ -151,6 +149,7 @@ export function TaskInfo({
|
||||
|
||||
// 监听 currentLoadingText
|
||||
useEffect(() => {
|
||||
console.log('currentLoadingText', currentLoadingText);
|
||||
// 清理之前的定时器
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
@ -358,42 +357,6 @@ export function TaskInfo({
|
||||
/>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
|
||||
{/* <motion.div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: stageColor }}
|
||||
animate={!isPauseWorkFlow ? {
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [1, 0.5, 1],
|
||||
transition: {
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 0.2,
|
||||
delay: 0.3
|
||||
}
|
||||
} : {}}
|
||||
/>
|
||||
<motion.div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: stageColor }}
|
||||
animate={!isPauseWorkFlow ? {
|
||||
scale: [1, 1.5, 1],
|
||||
opacity: [1, 0.5, 1],
|
||||
transition: {
|
||||
duration: 1,
|
||||
repeat: Infinity,
|
||||
repeatDelay: 0.2,
|
||||
delay: 0.3
|
||||
}
|
||||
} : {}}
|
||||
/> */}
|
||||
|
||||
{/* // 跳转剪辑按钮
|
||||
{showGotoCutButton && (
|
||||
<Tooltip placement="top" title='AI-powered editing platform'>
|
||||
<GlassIconButton icon={Scissors} size='sm' onClick={onGotoCut} />
|
||||
</Tooltip>
|
||||
)} */}
|
||||
</motion.div>
|
||||
)
|
||||
)
|
||||
|
||||
@ -118,7 +118,6 @@ export function ThumbnailGrid({
|
||||
}
|
||||
}
|
||||
|
||||
console.log('changedIndex_thumbnail-grid', changedIndex);
|
||||
|
||||
|
||||
if (changedIndex !== -1) {
|
||||
|
||||
@ -7,49 +7,24 @@ import { useScriptService } from "@/app/service/Interaction/ScriptService";
|
||||
import { useUpdateEffect } from '@/app/hooks/useUpdateEffect';
|
||||
import { LOADING_TEXT_MAP, TaskObject, Status, Stage } from '@/api/DTO/movieEdit';
|
||||
import { AspectRatioValue } from '@/components/ChatInputBox/AspectRatioSelector';
|
||||
import { useDeviceType } from '@/hooks/useDeviceType';
|
||||
import { cutUrlTo, errorConfig } from '@/lib/env';
|
||||
|
||||
interface UseWorkflowDataProps {
|
||||
onEditPlanGenerated?: () => void;
|
||||
editingStatus?: string;
|
||||
onExportFailed?: () => void;
|
||||
|
||||
}
|
||||
|
||||
export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFailed }: UseWorkflowDataProps = {}) {
|
||||
export function useWorkflowData({}: UseWorkflowDataProps = {}) {
|
||||
const searchParams = useSearchParams();
|
||||
const episodeId = searchParams.get('episodeId') || '';
|
||||
const from = searchParams.get('from') || '';
|
||||
const token = localStorage.getItem('token') || '';
|
||||
const useid = JSON.parse(localStorage.getItem("currentUser") || '{}').id || NaN;
|
||||
// H5进度提示事件桥接
|
||||
const emitToastShow = (params: { title?: string; progress?: number }) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:show', { detail: params }));
|
||||
}
|
||||
};
|
||||
const emitToastUpdate = (params: { title?: string; progress?: number }) => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:update', { detail: params }));
|
||||
}
|
||||
};
|
||||
const emitToastHide = () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('h5Toast:hide'));
|
||||
}
|
||||
};
|
||||
|
||||
const { isMobile, isTablet, isDesktop } = useDeviceType();
|
||||
|
||||
const cutUrl = cutUrlTo;
|
||||
console.log('cutUrl', cutUrl);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("init-useWorkflowData");
|
||||
return () => {
|
||||
console.log("unmount-useWorkflowData");
|
||||
// 组件卸载时隐藏H5进度提示
|
||||
// emitToastHide();
|
||||
};
|
||||
}, []);
|
||||
// 查看缓存中 是否已经 加载过 这个项目的 剪辑计划
|
||||
@ -90,7 +65,6 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
const [canGoToCut, setCanGoToCut] = useState(false);
|
||||
const [isShowError, setIsShowError] = useState(false);
|
||||
const [isAnalyzing, setIsAnalyzing] = useState(false);
|
||||
const [isGenerateEditPlan, setIsGenerateEditPlan] = useState(false);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const [isLoadingGenerateEditPlan, setIsLoadingGenerateEditPlan] = useState(false);
|
||||
const [state, setState] = useState({
|
||||
@ -156,102 +130,13 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
}
|
||||
}, [taskObject.currentStage]);
|
||||
|
||||
const generateEditPlan = useCallback(async (retryCount: number) => {
|
||||
if (isLoadedRef.current) {
|
||||
return;
|
||||
}
|
||||
// 显示生成剪辑计划进度提示
|
||||
// !emitToastShow({ title: isMobile ? 'Preparing for editing...' : `Generating intelligent editing plan... ${retryCount ? 'Retry Time: ' + retryCount : ''}`, progress: 0 });
|
||||
// 平滑推进到 80%,后续阶段接管
|
||||
const start = Date.now();
|
||||
const duration = 3 * 60 * 1000; // 3分钟推进到 80%
|
||||
let interval: NodeJS.Timeout | null = null;
|
||||
const stop = () => { if (interval) { clearInterval(interval); interval = null; } };
|
||||
interval = setInterval(() => {
|
||||
const elapsed = Date.now() - start;
|
||||
const pct = Math.min(80, Math.floor((elapsed / duration) * 80));
|
||||
// emitToastUpdate({ progress: pct });
|
||||
if (pct >= 80) stop();
|
||||
}, 300);
|
||||
// 先停止轮询
|
||||
await new Promise(resolve => {
|
||||
setNeedStreamData(false);
|
||||
resolve(true);
|
||||
});
|
||||
setIsLoadingGenerateEditPlan(true);
|
||||
try {
|
||||
const response = await getGenerateEditPlan({ project_id: episodeId });
|
||||
if (!response.data.editing_plan) {
|
||||
throw new Error(response.message);
|
||||
}
|
||||
|
||||
console.error('生成剪辑计划成功');
|
||||
setIsGenerateEditPlan(true);
|
||||
isLoadedRef.current = 'true';
|
||||
setNeedStreamData(true);
|
||||
|
||||
// 触发回调,通知父组件计划生成完成
|
||||
console.log('📞 calling onEditPlanGenerated callback');
|
||||
onEditPlanGenerated?.();
|
||||
setIsLoadingGenerateEditPlan(false);
|
||||
stop();
|
||||
} catch (error) {
|
||||
console.error('生成剪辑计划失败:', error);
|
||||
setNeedStreamData(true);
|
||||
setIsGenerateEditPlan(false);
|
||||
|
||||
setTimeout(() => {
|
||||
// emitToastHide();
|
||||
setIsLoadingGenerateEditPlan(false);
|
||||
}, 8000);
|
||||
stop();
|
||||
}
|
||||
}, [episodeId, onEditPlanGenerated]);
|
||||
|
||||
const openEditPlan = useCallback(async () => {
|
||||
window.open(`${cutUrl}/ai-editor/${episodeId}?token=${token}&user_id=${useid}`, '_target');
|
||||
}, [episodeId, cutUrl, token, useid]);
|
||||
|
||||
// useEffect(() => {
|
||||
// // 主动触发剪辑
|
||||
// if (canGoToCut && taskObject.currentStage === 'video' && !isShowError) {
|
||||
// generateEditPlan(retryCount - 1);
|
||||
// }
|
||||
// }, [canGoToCut, taskObject.currentStage, isShowError, generateEditPlan, retryCount]);
|
||||
|
||||
useEffect(() => {
|
||||
// 加载剪辑计划结束 并且 失败了 重试
|
||||
if (!isLoadingGenerateEditPlan && !isGenerateEditPlan) {
|
||||
setRetryCount((r) => r + 1);
|
||||
}
|
||||
}, [isLoadingGenerateEditPlan, isGenerateEditPlan]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isShowError) {
|
||||
window.msg.error('Too many failed storyboards, unable to execute automatic editing.', 8000);
|
||||
setCurrentLoadingText(LOADING_TEXT_MAP.toManyFailed);
|
||||
// 停止轮询
|
||||
setNeedStreamData(false);
|
||||
// emitToastHide();
|
||||
}
|
||||
if (editingStatus === 'error') {
|
||||
window.msg.error('Editing failed, Please click the scissors button to go to the intelligent editing platform.', 8000);
|
||||
setCurrentLoadingText(LOADING_TEXT_MAP.editingError);
|
||||
// 停止轮询
|
||||
setNeedStreamData(false);
|
||||
// emitToastHide();
|
||||
}
|
||||
}, [isShowError, editingStatus]);
|
||||
|
||||
|
||||
useUpdateEffect(() => {
|
||||
console.log('-----look-taskObject_find_changed-----', taskObject);
|
||||
if (taskObject.currentStage === 'script') {
|
||||
if (scriptBlocksMemo.length > 0) {
|
||||
console.log('应用剧本');
|
||||
// 自动模式下 应用剧本;手动模式 需要点击 下一步 触发
|
||||
// 确保仅自动触发一次
|
||||
// state.mode.includes('auto') && loadingText.current !== LOADING_TEXT_MAP.character && applyScript();
|
||||
loadingText.current = LOADING_TEXT_MAP.character;
|
||||
} else {
|
||||
loadingText.current = LOADING_TEXT_MAP.script;
|
||||
@ -288,10 +173,18 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
if (taskObject.status === 'COMPLETED') {
|
||||
loadingText.current = LOADING_TEXT_MAP.complete;
|
||||
}
|
||||
setCurrentLoadingText(loadingText.current);
|
||||
}, [scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
|
||||
if (taskObject.status === 'FAILED') {
|
||||
if (isShowError) {
|
||||
loadingText.current = LOADING_TEXT_MAP.toManyFailed;
|
||||
window.msg.error('Too many failed storyboards, unable to execute automatic editing.', 8000);
|
||||
} else {
|
||||
loadingText.current = LOADING_TEXT_MAP.editingError;
|
||||
window.msg.error('Editing failed, Please click the scissors button to go to the intelligent editing platform.', 8000);
|
||||
}
|
||||
}
|
||||
|
||||
// 将 sketchCount 和 videoCount 放到 redux 中 每一次变化也要更新
|
||||
setCurrentLoadingText(loadingText.current);
|
||||
}, [isShowError, scriptBlocksMemo, taskObject.currentStage, taskObject.scenes.data, taskObject.roles.data, taskObject.videos.data, taskObject.status], {mode: 'none'});
|
||||
|
||||
// 添加手动播放控制
|
||||
const handleManualPlay = useCallback(async () => {
|
||||
@ -312,9 +205,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
|
||||
const all_task_data = response.data;
|
||||
const { current: taskCurrent } = tempTaskObject;
|
||||
|
||||
console.log('---look-all_task_data', all_task_data);
|
||||
console.log('---look-tempTaskObject', taskCurrent);
|
||||
let combinerVideoUrl = '';
|
||||
|
||||
// 收集所有需要更新的状态
|
||||
let stateUpdates = JSON.stringify(taskCurrent);
|
||||
@ -353,7 +244,6 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
realSketchResultData = taskCurrent.scenes.data.filter((item: any) => item.status !== 0);
|
||||
}
|
||||
console.log('---look-realSketchResultData', realSketchResultData);
|
||||
taskCurrent.scenes.total_count = task.task_result.total_count;
|
||||
if (task.task_status !== 'COMPLETED' || taskCurrent.scenes.total_count !== realSketchResultData.length) {
|
||||
taskCurrent.currentStage = 'scene';
|
||||
@ -402,39 +292,18 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
});
|
||||
}
|
||||
taskCurrent.videos.data = videoList;
|
||||
console.log('----------正在生成视频中', realTaskResultData.length);
|
||||
break;
|
||||
}
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
console.log('----------视频生成完成');
|
||||
// 视频生成完成
|
||||
// 暂时没有音频生成 直接跳过
|
||||
// 视频分析
|
||||
const error_totle = taskCurrent.videos.data.filter((item: any) => item.video_status === 2).length;
|
||||
const total_count = taskCurrent.videos.data.length;
|
||||
let analyze_video_completed_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video' && item.task_status !== 'INIT' && item.task_status !== 'RUNNING').length;
|
||||
let analyze_video_total_count = all_task_data.filter((item: any) => item.task_name === 'generate_analyze_video').length;
|
||||
|
||||
// 检查是否需要开始显示视频分析进度
|
||||
// 只在第一次检测到视频分析任务时显示通知
|
||||
if (analyze_video_total_count > 0 && !isAnalyzing && analyze_video_completed_count !== analyze_video_total_count) {
|
||||
setIsAnalyzing(true);
|
||||
// 显示准备剪辑计划的提示
|
||||
// emitToastShow({ title: isMobile ? 'Preparing for editing...' : 'Preparing intelligent editing plan...', progress: 0 });
|
||||
}
|
||||
|
||||
if (analyze_video_total_count && analyze_video_completed_count === analyze_video_total_count) {
|
||||
// 视频分析完成
|
||||
if(error_totle !== total_count) {
|
||||
setCanGoToCut(true);
|
||||
// 重置进度条,显示生成剪辑计划进度
|
||||
setIsAnalyzing(false);
|
||||
// 不主动隐藏,交由后续阶段覆盖标题与进度
|
||||
} else {
|
||||
setIsShowError(true);
|
||||
setIsAnalyzing(false);
|
||||
// emitToastHide();
|
||||
}
|
||||
if(error_totle === total_count) {
|
||||
setIsShowError(true);
|
||||
taskCurrent.status = 'FAILED';
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -442,18 +311,10 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
// 合成视频
|
||||
if (task.task_name === 'combiner_videos') {
|
||||
if (task.task_status === 'COMPLETED') {
|
||||
taskCurrent.currentStage = 'final_video';
|
||||
taskCurrent.final.url = task.task_result.video_url;
|
||||
taskCurrent.final.note = 'combiner';
|
||||
taskCurrent.status = 'COMPLETED';
|
||||
combinerVideoUrl = task.task_result.video_url;
|
||||
}
|
||||
if (task.task_status === 'FAILED' || task.task_status === 'ERROR') {
|
||||
taskCurrent.status = 'FAILED';
|
||||
|
||||
// 触发导出失败回调
|
||||
if (onExportFailed) {
|
||||
onExportFailed();
|
||||
}
|
||||
// 停止轮询
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
@ -470,6 +331,11 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
if (task.task_status === 'FAILED' || task.task_status === 'ERROR') {
|
||||
// 使用合成视频地址
|
||||
taskCurrent.currentStage = 'final_video';
|
||||
taskCurrent.final.url = combinerVideoUrl;
|
||||
taskCurrent.final.note = 'combiner';
|
||||
taskCurrent.status = 'COMPLETED';
|
||||
// 停止轮询
|
||||
setNeedStreamData(false);
|
||||
}
|
||||
@ -489,13 +355,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
}
|
||||
}
|
||||
|
||||
console.log('-----look-tempTaskObject-----', loadingText.current);
|
||||
|
||||
// 设置最终的状态更新
|
||||
setCurrentLoadingText(loadingText.current);
|
||||
|
||||
if (JSON.stringify(taskCurrent) !== stateUpdates) {
|
||||
console.log('-----look-tempTaskObject-changed-----', taskCurrent);
|
||||
// 强制更新,使用新的对象引用确保触发更新
|
||||
setTaskObject(prev => {
|
||||
const newState = JSON.parse(JSON.stringify({...prev, ...taskCurrent}));
|
||||
@ -506,7 +366,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error);
|
||||
}
|
||||
}, [episodeId, needStreamData, onExportFailed, errorConfig, isAnalyzing]);
|
||||
}, [episodeId, needStreamData, errorConfig, isAnalyzing]);
|
||||
|
||||
// 轮询获取流式数据
|
||||
useUpdateEffect(() => {
|
||||
@ -560,7 +420,6 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
setCurrentLoadingText(LOADING_TEXT_MAP.initializing);
|
||||
// 如果没有标题,轮询获取
|
||||
const titleResponse = await getScriptTitle({ project_id: episodeId });
|
||||
console.log('titleResponse', titleResponse);
|
||||
if (titleResponse.successful) {
|
||||
taskCurrent.title = titleResponse.data.name;
|
||||
taskCurrent.tags = titleResponse.data.description.tags || [];
|
||||
@ -622,10 +481,8 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
taskCurrent.videos.total_count = data.video.total_count;
|
||||
const videoList = [];
|
||||
let videoUrls: string[] = [];
|
||||
console.log('----------data.video.data', data.video.data);
|
||||
for (const video of data.video.data) {
|
||||
videoUrls = video.urls ? video.urls.filter((url: null | string) => url !== null) : [];
|
||||
console.log('----------videoUrls', videoUrls);
|
||||
let video_status = video.video_status === undefined ? (videoUrls.length > 0 ? 1 : 0) : video.video_status;
|
||||
// 每一项 video 有多个视频 默认取存在的项
|
||||
videoList.push({
|
||||
@ -676,10 +533,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
taskCurrent.status = 'COMPLETED';
|
||||
}
|
||||
|
||||
console.log('---look-taskData', taskCurrent);
|
||||
|
||||
if (taskCurrent.currentStage === 'script') {
|
||||
console.log('开始初始化剧本', original_text,episodeId);
|
||||
// TODO 为什么一开始没项目id
|
||||
original_text && initializeFromProject(episodeId, original_text).then(() => {
|
||||
});
|
||||
@ -698,8 +552,7 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
});
|
||||
|
||||
// 设置是否需要获取流式数据
|
||||
// setNeedStreamData(taskCurrent.status !== 'COMPLETED');
|
||||
setNeedStreamData(true);
|
||||
setNeedStreamData(taskCurrent.status !== 'COMPLETED');
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化失败:', error);
|
||||
@ -786,10 +639,9 @@ export function useWorkflowData({ onEditPlanGenerated, editingStatus, onExportFa
|
||||
applyScript,
|
||||
fallbackToStep,
|
||||
originalText: state.originalText,
|
||||
showGotoCutButton: (canGoToCut && (isGenerateEditPlan || taskObject.currentStage === 'final_video') || isShowError) ? true : false,
|
||||
showGotoCutButton: (taskObject.status === 'FAILED' || taskObject.status === 'COMPLETED') ? true : false,
|
||||
generateEditPlan: openEditPlan,
|
||||
handleRetryVideo,
|
||||
isShowAutoEditing: canGoToCut && taskObject.currentStage !== 'final_video' && isGenerateEditPlan && !isShowError ? true : false,
|
||||
aspectRatio: state.aspectRatio
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user