import React, { useEffect, useState, useRef } from 'react'; import { useParams, useHistory } from 'react-router-dom'; import { Button, Row, Col, Container, Card, Table, Badge, Form, Modal, InputGroup, FormControl, Dropdown, OverlayTrigger, Tooltip, Alert, ProgressBar, Spinner } from 'react-bootstrap'; import { toast } from 'react-toastify'; import { bookService, bookInfoService, initDb } from '../../db'; import { cozeService, initCozeService, getAvailableStyles } from '../../utils/cozeExample'; import { WORKFLOW_IDS, hasValidToken } from '../../utils/cozeConfig'; import { getUserSettings } from '../../utils/storageUtils'; import taskQueueManager from '../../utils/taskQueueManager'; import './projectDetail.css'; import path from 'path-browserify'; // 引入AudioPlayer组件 import AudioPlayerComponent from '../../components/audioPlayer'; import lipSyncService from '../../services/lipSyncService'; import cozeUploadService from '../../services/cozeUploadService'; const ProjectDetail = () => { const { projectId } = useParams(); const history = useHistory(); const [loading, setLoading] = useState(false); const [silentLoading, setSilentLoading] = useState(false); const [project, setProject] = useState(null); const [segments, setSegments] = useState([]); const [selectedSegments, setSelectedSegments] = useState([]); const [showMergeModal, setShowMergeModal] = useState(false); const [showSplitModal, setShowSplitModal] = useState(false); const [splitSegment, setSplitSegment] = useState(null); const [newSegments, setNewSegments] = useState([{ text: '', start_time: '', end_time: '' }]); const [showUploadModal, setShowUploadModal] = useState(false); const [uploadType, setUploadType] = useState('audio'); const [uploadSegmentId, setUploadSegmentId] = useState(null); const [selectedFile, setSelectedFile] = useState(null); const [descriptionText, setDescriptionText] = useState(''); const [showDescriptionModal, setShowDescriptionModal] = useState(false); const [descriptionSegmentId, setDescriptionSegmentId] = useState(null); const [showImagePreviewModal, setShowImagePreviewModal] = useState(false); const [previewImageUrl, setPreviewImageUrl] = useState(''); const [generatingDescriptions, setGeneratingDescriptions] = useState(false); const [generationProgress, setGenerationProgress] = useState({ current: 0, total: 0 }); const [editingSegmentId, setEditingSegmentId] = useState(null); const [editingTextField, setEditingTextField] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [hasDescriptions, setHasDescriptions] = useState(false); const [stylesList, setStylesList] = useState([]); const [selectedStyle, setSelectedStyle] = useState(''); const [loadingStyles, setLoadingStyles] = useState(false); const [generatingImages, setGeneratingImages] = useState(false); const [imageGenerationProgress, setImageGenerationProgress] = useState({ current: 0, total: 0 }); const [isPaused, setIsPaused] = useState(false); // 在状态变量中添加项目音频相关状态 const [projectAudioPath, setProjectAudioPath] = useState(null); const [showAudioPlayer, setShowAudioPlayer] = useState(false); const [projectVideoPath, setProjectVideoPath] = useState(null); const [showVideoPlayer, setShowVideoPlayer] = useState(false); const [showUploadProjectAudioModal, setShowUploadProjectAudioModal] = useState(false); const [projectAudioFile, setProjectAudioFile] = useState(null); const [uploadingProjectAudio, setUploadingProjectAudio] = useState(false); // 对口型相关状态 const [lipSyncProcessing, setLipSyncProcessing] = useState(false); const [lipSyncProgress, setLipSyncProgress] = useState(0); const [lipSyncTaskId, setLipSyncTaskId] = useState(null); const [lipSyncResultUrl, setLipSyncResultUrl] = useState(null); // 添加导出剪映草稿相关状态 const [exportingDraft, setExportingDraft] = useState(false); // 添加一个标志位来防止重复调用 const [isGenerating, setIsGenerating] = useState(false); // 添加一个状态变量防止重复检查 const [isCheckingGeneration, setIsCheckingGeneration] = useState(false); // 增加新的状态变量来更精确地追踪绘画进度 const [drawingSegmentId, setDrawingSegmentId] = useState(null); const [totalDrawableSegments, setTotalDrawableSegments] = useState(0); const [completedDrawings, setCompletedDrawings] = useState(0); const [isUploading, setIsUploading] = useState(false); const fileInputRef = useRef(null); // 辅助函数:根据ID获取单个分镜信息 const getSegmentById = async (segmentId) => { const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); return allSegments.find(segment => segment.id === segmentId); }; // 加载项目详情 const loadProjectDetail = async (silent = false) => { try { if (!silent) { setLoading(true); } else { setSilentLoading(true); } const projectData = await bookService.getBookById(parseInt(projectId)); const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); setProject(projectData); setSegments(Array.isArray(segmentsData) ? segmentsData : []); // 设置当前选中的画风 if (projectData && projectData.style) { setSelectedStyle(projectData.style); } // 加载项目音频路径 if (projectData && projectData.audio_path) { setProjectAudioPath(projectData.audio_path); setShowAudioPlayer(true); } else { setProjectAudioPath(null); setShowAudioPlayer(false); } // 加载项目视频路径 if (projectData && projectData.video_path) { setProjectVideoPath(projectData.video_path); setShowVideoPlayer(true); } else { setProjectVideoPath(null); setShowVideoPlayer(false); } // 检查是否已经有描述词 const hasAnyDescriptions = Array.isArray(segmentsData) && segmentsData.some(segment => segment.description && segment.description.trim() !== ''); setHasDescriptions(hasAnyDescriptions); // 检查是否有正在进行的绘图任务 checkOngoingImageGeneration(); } catch (error) { console.error('加载项目详情失败:', error); toast.error(`加载项目详情失败: ${error.message}`); } finally { if (!silent) { setLoading(false); } else { setSilentLoading(false); } } }; // 获取画风列表 const loadStylesList = async () => { if (!hasValidToken()) { toast.warning('未配置Coze API Token,无法获取画风列表'); return; } try { setLoadingStyles(true); const cozeInstance = initCozeService(); // 调用获取画风列表API const response = await cozeInstance.getStyleList(); console.log('获取画风列表响应:', response); // 处理返回结果 let styles = []; if (response && response.data) { try { const responseData = JSON.parse(response.data); if (responseData.output) { // 如果返回的是数组 if (Array.isArray(responseData.output)) { styles = responseData.output; } // 如果返回的是对象中包含styles数组 else if (responseData.output.styles && Array.isArray(responseData.output.styles)) { styles = responseData.output.styles; } } } catch (e) { console.error('解析画风列表数据失败:', e); } } // 如果没有获取到画风列表,使用默认值 if (styles.length === 0) { styles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格']; } setStylesList(styles); // 如果项目还没有设置画风,且有可用画风,设置第一个为默认 if (styles.length > 0 && (!selectedStyle || selectedStyle === '')) { setSelectedStyle(styles[0]); } } catch (error) { console.error('获取画风列表失败:', error); toast.error(`获取画风列表失败: ${error.message}`); // 使用默认的画风列表 const defaultStyles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格']; setStylesList(defaultStyles); } finally { setLoadingStyles(false); } }; // 选择画风 const handleStyleSelect = async (style) => { try { setLoading(true); setSelectedStyle(style); // 保存画风到项目中 await bookService.updateBook(parseInt(projectId), { style: style }); toast.success(`画风已设置为: ${style}`); } catch (error) { console.error('设置画风失败:', error); toast.error(`设置画风失败: ${error.message}`); } finally { setLoading(false); } }; // 返回项目列表 const backToProjectList = () => { history.push('/'); }; // 格式化时间 const formatTime = (timeString) => { if (!timeString) return ''; return timeString.replace(',', '.'); }; // 计算持续时间(秒) const calculateDuration = (startTime, endTime) => { const parseTime = (time) => { if (!time) return 0; const [hours, minutes, seconds] = time.replace(',', '.').split(':').map(parseFloat); return hours * 3600 + minutes * 60 + seconds; }; const startSeconds = parseTime(startTime); const endSeconds = parseTime(endTime); return (endSeconds - startSeconds).toFixed(2); }; // 选择/取消选择分镜 const toggleSegmentSelection = (segmentId) => { setSelectedSegments(prev => { if (prev.includes(segmentId)) { return prev.filter(id => id !== segmentId); } else { return [...prev, segmentId]; } }); }; // 显示合并分镜对话框 const showMergeSegmentsModal = () => { if (selectedSegments.length < 2) { toast.error('请至少选择两个分镜进行合并'); return; } setShowMergeModal(true); }; // 合并分镜 const mergeSegments = async () => { try { setLoading(true); await bookInfoService.mergeSegments(parseInt(projectId), selectedSegments); toast.success('分镜合并成功'); setSelectedSegments([]); setShowMergeModal(false); await loadProjectDetail(); } catch (error) { console.error('合并分镜失败:', error); toast.error(`合并分镜失败: ${error.message}`); } finally { setLoading(false); } }; // 显示拆分分镜对话框 const showSplitSegmentModal = (segment) => { setSplitSegment(segment); setNewSegments([{ text: segment.text || '', start_time: segment.start_time || '', end_time: segment.end_time || '' }]); setShowSplitModal(true); }; // 添加新分镜表单 const addNewSegmentForm = () => { setNewSegments([...newSegments, { text: '', start_time: '', end_time: '' }]); }; // 删除分镜表单 const removeSegmentForm = (index) => { if (newSegments.length <= 1) { toast.error('至少需要一个分镜'); return; } const updatedSegments = [...newSegments]; updatedSegments.splice(index, 1); setNewSegments(updatedSegments); }; // 更新分镜表单数据 const updateNewSegmentData = (index, field, value) => { const updatedSegments = [...newSegments]; updatedSegments[index][field] = value; setNewSegments(updatedSegments); }; // 拆分分镜 const splitSegmentSubmit = async () => { try { // 验证输入 for (const segment of newSegments) { if (!segment.text || !segment.start_time || !segment.end_time) { toast.error('请填写所有分镜的文本内容和时间信息'); return; } // 验证时间格式 const timePattern = /^\d{2}:\d{2}:\d{2},\d{3}$/; if (!timePattern.test(segment.start_time) || !timePattern.test(segment.end_time)) { toast.error('时间格式不正确,应为: HH:MM:SS,SSS'); return; } } setLoading(true); await bookInfoService.splitSegment(splitSegment.id, newSegments); toast.success('分镜拆分成功'); setShowSplitModal(false); setSplitSegment(null); setNewSegments([{ text: '', start_time: '', end_time: '' }]); await loadProjectDetail(); } catch (error) { console.error('拆分分镜失败:', error); toast.error(`拆分分镜失败: ${error.message}`); } finally { setLoading(false); } }; // 显示上传对话框 const showUploadModalFor = (segmentId, type) => { setUploadSegmentId(segmentId); setUploadType(type); setSelectedFile(null); setShowUploadModal(true); }; // 处理文件选择改变 const handleFileChange = (e) => { setSelectedFile(e.target.files[0]); }; // 上传文件 const handleFileUpload = async () => { if (!selectedFile) { toast.error('请先选择文件'); return; } // 获取当前处理的分镜 const currentSegment = segments.find(s => s.id === uploadSegmentId); if (!currentSegment) { toast.error('分镜不存在'); return; } try { setIsUploading(true); // 如果是图片类型,先上传到Coze获取链接 if (uploadType === 'image') { toast.info('正在上传图片到云端...'); const imageUrl = await cozeUploadService.uploadFileAndGetLink(selectedFile); if (!imageUrl) { throw new Error('获取图片链接失败'); } // 更新分镜信息 - 保存图片链接 await bookInfoService.updateBookInfo(uploadSegmentId, { image_path: imageUrl, draw_status: 2 // 设置为已完成状态 }); // 直接更新本地状态,不刷新整个页面 setSegments(prevSegments => prevSegments.map(seg => seg.id === uploadSegmentId ? { ...seg, image_path: imageUrl, draw_status: 2 } : seg ) ); toast.success('图片上传成功'); } else { // 其他类型文件(音频、视频)的处理逻辑 // 创建上传路径 const filePath = `uploads/${uploadType}/${selectedFile.name}`; // 更新分镜信息 const updateData = { id: uploadSegmentId }; if (uploadType === 'audio') { updateData.audio_path = filePath; } else if (uploadType === 'video') { updateData.video_path = filePath; } await bookInfoService.updateBookInfo(uploadSegmentId, updateData); // 直接更新本地状态,不刷新整个页面 setSegments(prevSegments => prevSegments.map(seg => seg.id === uploadSegmentId ? { ...seg, ...updateData } : seg ) ); toast.success('文件上传成功'); } // 关闭上传对话框 setShowUploadModal(false); // 清空选择的文件 setSelectedFile(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } } catch (error) { console.error('文件上传失败:', error); toast.error(`文件上传失败: ${error.message}`); } finally { setIsUploading(false); } }; // 显示描述词对话框 const showDescriptionModalFor = (segment) => { setDescriptionSegmentId(segment.id); setDescriptionText(segment.description || ''); setShowDescriptionModal(true); }; // 保存描述词 const saveDescription = async (segmentId, description) => { try { setLoading(true); await bookInfoService.updateBookInfo(segmentId, { description: description }); toast.success('描述词保存成功'); await loadProjectDetail(); } catch (error) { console.error('保存描述词失败:', error); toast.error(`保存描述词失败: ${error.message}`); } finally { setLoading(false); } }; // 保存文本内容 const saveText = async (segmentId, text) => { try { setLoading(true); await bookInfoService.updateBookInfo(segmentId, { text: text }); toast.success('文本内容保存成功'); await loadProjectDetail(); } catch (error) { console.error('保存文本内容失败:', error); toast.error(`保存文本内容失败: ${error.message}`); } finally { setLoading(false); } }; // 一键生成描述词 const generateAllDescriptions = async () => { if (!hasValidToken()) { toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置'); return; } try { setGeneratingDescriptions(true); // 统计所有有文本的分镜数量 const segmentsWithText = segments.filter(segment => segment.text && segment.text.trim() !== ''); setGenerationProgress({ current: 0, total: segmentsWithText.length }); toast.info(`开始生成描述词,共${segmentsWithText.length}个分镜需要处理...`); // 初始化Coze服务 const cozeInstance = initCozeService(); // 处理每个分镜 let successCount = 0; let errorCount = 0; for (const segment of segments) { if (segment.text && segment.text.trim() !== '') { try { setGenerationProgress(prev => ({ ...prev, current: prev.current + 1 })); console.log(`处理分镜ID ${segment.id} 的描述词生成 (${successCount + errorCount + 1}/${segmentsWithText.length})...`); // 调用Coze的文本转描述词工作流 const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, { prompt: segment.text, }); console.log(`分镜ID ${segment.id} 的API响应:`, JSON.parse(response.data).output); // 从响应中提取描述词 let description = ''; if (response && response.data) { description = JSON.parse(response.data).output; } // 保存到数据库 await bookInfoService.updateBookInfo(segment.id, { description: description }); // 直接更新状态而不是重新加载 setSegments(prevSegments => prevSegments.map(seg => seg.id === segment.id ? { ...seg, description: description } : seg ) ); console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`); successCount++; } catch (error) { console.error(`分镜ID ${segment.id} 的描述词生成失败:`, error); errorCount++; // 尝试获取错误详情 let errorMsg = error.message || '未知错误'; if (error.response) { try { const errorData = error.response.data; errorMsg = errorData.message || errorData.error || JSON.stringify(errorData); } catch (e) { errorMsg = `API错误: ${error.response.status}`; } } // 更新分镜的错误信息 try { // 仅在分镜没有描述词的情况下,添加错误信息 if (!segment.description || segment.description.trim() === '') { await bookInfoService.updateBookInfo(segment.id, { description: `[生成失败] ${errorMsg}` }); // 直接更新状态而不是重新加载 setSegments(prevSegments => prevSegments.map(seg => seg.id === segment.id ? { ...seg, description: `[生成失败] ${errorMsg}` } : seg ) ); } } catch (saveError) { console.error('保存错误信息失败:', saveError); } } } } // 显示结果统计 if (errorCount > 0 && successCount > 0) { toast.warning(`描述词生成完成,成功: ${successCount}个,失败: ${errorCount}个`); } else if (errorCount > 0 && successCount === 0) { toast.error(`描述词生成全部失败,请检查API配置和网络连接`); } else { toast.success(hasDescriptions ? `描述词更新完成,共更新${successCount}个分镜` : `描述词生成完成,共生成${successCount}个分镜`); } setHasDescriptions(successCount > 0 || segments.some(segment => segment.description && segment.description.trim() !== '')); } catch (error) { console.error('生成描述词失败:', error); toast.error(`生成描述词失败: ${error.message}`); } finally { setGeneratingDescriptions(false); setGenerationProgress({ current: 0, total: 0 }); } }; // 处理直接编辑描述词 const handleDirectEditDescription = (segmentId, description) => { setEditingSegmentId(segmentId); setEditingTextField('description'); setDescriptionText(description || ''); }; // 处理直接编辑文本内容 const handleDirectEditText = (segmentId, text) => { setEditingSegmentId(segmentId); setEditingTextField('text'); setDescriptionText(text || ''); }; // 保存直接编辑的内容 const saveDirectEdit = async () => { if (!editingSegmentId || !editingTextField) return; try { if (editingTextField === 'description') { await saveDescription(editingSegmentId, descriptionText); } else if (editingTextField === 'text') { await saveText(editingSegmentId, descriptionText); } // 清除编辑状态 setEditingSegmentId(null); setEditingTextField(null); setDescriptionText(''); } catch (error) { console.error('保存编辑内容失败:', error); toast.error(`保存失败: ${error.message}`); } }; // 取消直接编辑 const cancelDirectEdit = () => { setEditingSegmentId(null); setEditingTextField(null); setDescriptionText(''); }; // 显示删除确认对话框 const showDeleteConfirmation = () => { setShowDeleteConfirm(true); }; // 删除项目 const deleteProject = async () => { try { setLoading(true); // 调用删除API await bookService.deleteBook(parseInt(projectId)); toast.success('项目删除成功'); // 返回首页 history.push('/'); } catch (error) { console.error('删除项目失败:', error); toast.error(`删除项目失败: ${error.message}`); } finally { setLoading(false); setShowDeleteConfirm(false); } }; // 删除分镜 const deleteSegment = async (segmentId) => { try { setLoading(true); // 调用删除API await bookInfoService.deleteBookInfo(segmentId); toast.success('分镜删除成功'); // 重新加载分镜列表 await loadProjectDetail(); } catch (error) { console.error('删除分镜失败:', error); toast.error(`删除分镜失败: ${error.message}`); } finally { setLoading(false); } }; // 为单个分镜生成描述词 const generateSingleDescription = async (segment) => { if (!hasValidToken()) { toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置'); return; } if (!segment.text || segment.text.trim() === '') { toast.warning('分镜文本为空,无法生成描述词'); return; } try { setGeneratingDescriptions(true); // 设置当前正在处理的分镜ID setDescriptionSegmentId(segment.id); toast.info('开始生成描述词,请稍候...'); // 初始化Coze服务 const cozeInstance = initCozeService(); console.log(`为分镜ID ${segment.id} 生成描述词...`); // 调用Coze的文本转描述词工作流 const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, { text: segment.text, style: 'detailed', language: 'zh' }); console.log(`分镜ID ${segment.id} 的API响应:`, response); // 从响应中提取描述词 let description = ''; if (response && response.description) { description = response.description; } else if (response && typeof response === 'string') { description = response; } else if (response && response.result) { description = response.result; } else if (response && response.content) { description = response.content; } else { // 如果API返回格式不一致,尝试从整个响应对象提取有意义的内容 try { description = JSON.stringify(response); } catch (e) { description = `描述词生成成功,但无法解析结果: ${e.message}`; } } // 保存到数据库 await bookInfoService.updateBookInfo(segment.id, { description: description }); console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`); toast.success('描述词生成成功'); // 刷新数据 await loadProjectDetail(); // 设置有描述词的状态 setHasDescriptions(true); } catch (error) { console.error(`为分镜ID ${segment.id} 生成描述词失败:`, error); // 尝试获取错误详情 let errorMsg = error.message || '未知错误'; if (error.response) { try { const errorData = error.response.data; errorMsg = errorData.message || errorData.error || JSON.stringify(errorData); } catch (e) { errorMsg = `API错误: ${error.response.status}`; } } toast.error(`描述词生成失败: ${errorMsg}`); } finally { setGeneratingDescriptions(false); setDescriptionSegmentId(null); } }; // 检查是否有正在进行的绘图任务 const checkOngoingImageGeneration = async () => { // 防止重复检查 if (isCheckingGeneration || isGenerating) { console.log('已经在检查或正在生成图片中,跳过重复检查'); return null; } try { setIsCheckingGeneration(true); console.log('检查是否有正在进行的绘图任务'); // 从数据库获取所有分镜 const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); // 检查是否有暂停状态(-1)的分镜 const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1); // 检查有排队状态(0)或绘画中状态(1)的分镜 const pendingSegments = segmentsData.filter(segment => segment.draw_status === 0 || segment.draw_status === 1 ); // 如果既没有暂停的也没有待处理的,确保状态重置为默认 if (pausedSegments.length === 0 && pendingSegments.length === 0) { // 如果重启后没有正在进行的任务,重置所有状态 setGeneratingImages(false); setIsPaused(false); localStorage.removeItem(`project_${projectId}_image_generation`); return null; } // 计算已完成的分镜数量(固定值) const completedCount = segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length; // 计算总数(需要处理的分镜 + 已完成的分镜) // 对于暂停状态,总数是:已完成的 + 暂停的 + 排队的 + 绘画中的 const totalSegments = segmentsData.filter(segment => segment.description && segment.description.trim() !== '' && ( segment.draw_status === -1 || segment.draw_status === 0 || segment.draw_status === 1 || (segment.draw_status === 2 && segment.image_path) ) ).length; if (pausedSegments.length > 0) { console.log(`检测到${pausedSegments.length}个处于暂停状态的分镜`); // 设置UI状态为暂停 setGeneratingImages(true); setIsPaused(true); setImageGenerationProgress({ current: completedCount, total: totalSegments }); // 保存到localStorage const currentProgress = { current: completedCount, total: totalSegments }; // 保存到localStorage localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: currentProgress, timestamp: new Date().getTime(), isPaused: true })); return; } if (pendingSegments.length > 0) { console.log(`检测到${pendingSegments.length}个待处理的分镜,继续执行绘图任务`); // 设置UI状态 setGeneratingImages(true); setIsPaused(false); setImageGenerationProgress({ current: completedCount, total: totalSegments }); // 检查是否已经在生成图片 if (!generatingImages && !isGenerating) { // 启动绘图任务,使用setTimeout避免可能的状态更新问题 setTimeout(() => { generateAllImages(false); }, 1000); } return; } } catch (error) { console.error('检查绘图状态失败:', error); // 出现错误时确保重置状态为默认 localStorage.removeItem(`project_${projectId}_image_generation`); setGeneratingImages(false); setIsPaused(false); } finally { setIsCheckingGeneration(false); } return null; }; /** * 生成所有分镜图片 */ const generateAllImages = async (isForceRedraw = false) => { // 如果已经在生成中,直接返回 if (isGenerating) { toast.info('正在绘制图片中,请等待当前操作完成'); return; } // 检查是否已选择绘画风格 if (!selectedStyle || selectedStyle.trim() === '') { toast.error('请先选择绘画风格'); return; } try { setIsGenerating(true); // 获取需要处理的分镜 let segmentsToProcess = []; // 计算已经完成的图片数量 const existingCompletedCount = segments.filter( segment => segment.image_path && segment.draw_status === 2 ).length; console.log(`当前已完成的图片数量: ${existingCompletedCount}`); setCompletedDrawings(0); // 重置已完成计数 if (isForceRedraw) { // 重绘:处理所有有描述词的分镜 const segmentsToRedraw = segments.filter( segment => segment.description && segment.description.trim() !== '' ); console.log(`重绘: 选择${segmentsToRedraw.length}个有描述词的分镜`); // 如果没有需要重绘的分镜,直接返回 if (segmentsToRedraw.length === 0) { toast.info('没有需要重绘的分镜'); setIsGenerating(false); return; } // 先更新本地状态 - 将所有选定的分镜状态设为排队状态 setSegments(prevSegments => prevSegments.map(segment => segmentsToRedraw.some(s => s.id === segment.id) ? { ...segment, draw_status: 0, image_path: null } : segment ) ); // 异步更新数据库 await Promise.all(segmentsToRedraw.map(segment => bookInfoService.updateBookInfo(segment.id, { draw_status: 0, // 排队状态 image_path: null // 清除已有图片 }) )); segmentsToProcess = [...segmentsToRedraw]; } else { // 继续绘画: 处理没有图片但有描述词的分镜 const segmentsToQueue = segments.filter( segment => segment.description && segment.description.trim() !== '' && !segment.image_path && segment.draw_status !== 2 // 确保不是已完成状态 ); console.log(`继续绘画: 选择${segmentsToQueue.length}个有描述词但没有图片的分镜`); // 如果没有需要处理的分镜,直接返回 if (segmentsToQueue.length === 0) { toast.info('没有需要绘制的分镜'); setIsGenerating(false); return; } // 更新所有需要处理的分镜状态为排队状态 await Promise.all(segmentsToQueue.map(segment => bookInfoService.updateBookInfo(segment.id, { draw_status: 0 // 排队状态 }) )); segmentsToProcess = [...segmentsToQueue]; } // 计算总进度 const totalSegmentsToProcess = segmentsToProcess.length; setTotalDrawableSegments(totalSegmentsToProcess); // 设置UI状态 setGeneratingImages(true); setIsPaused(false); setImageGenerationProgress({ current: 0, total: totalSegmentsToProcess }); // 重置进度为0 // 保存到localStorage localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: { current: 0, total: totalSegmentsToProcess }, timestamp: new Date().getTime(), isPaused: false })); // 顺序处理每个分镜 let successCount = 0; let errorCount = 0; let shouldContinue = true; for (let i = 0; i < segmentsToProcess.length && shouldContinue; i++) { const segment = segmentsToProcess[i]; // 设置当前正在处理的分镜ID setDrawingSegmentId(segment.id); // 每次处理前检查是否被暂停 const currentStatus = await checkGenerationStatus(); if (currentStatus === 'paused') { console.log('检测到暂停状态,停止图片生成'); shouldContinue = false; break; } else if (currentStatus === 'canceled') { console.log('检测到取消状态,停止图片生成'); shouldContinue = false; break; } // 处理前再次检查分镜状态 const segmentBeforeProcess = await getSegmentById(segment.id); if (segmentBeforeProcess.draw_status !== 0 || segmentBeforeProcess.image_path) { console.log(`分镜ID ${segment.id} 当前状态不是排队状态或已有图片,跳过处理`); continue; } // 更新分镜状态为绘画中 await bookInfoService.updateBookInfo(segment.id, { draw_status: 1 // 绘画中状态 }); // 实时更新UI setSegments(prevSegments => prevSegments.map(seg => seg.id === segment.id ? { ...seg, draw_status: 1 } : seg ) ); try { console.log(`开始处理分镜ID ${segment.id},进度 ${i+1}/${totalSegmentsToProcess}`); // 初始化Coze服务 const cozeInstance = initCozeService({ timeout: 60000 // 设置60秒超时 }); // 调用API生成图片 const response = await cozeInstance.generateImage( segment.description, // 描述词作为prompt selectedStyle, // 项目设置的画风 { width: 1024, height: 1024, num_images: 1 } ); // 每次API调用后检查是否被暂停 const statusAfterAPI = await checkGenerationStatus(); if (statusAfterAPI === 'paused' || statusAfterAPI === 'canceled') { console.log('API调用后检测到暂停/取消状态,停止后续处理'); shouldContinue = false; break; } // 从响应中提取图片URL let imagePath = ''; if (response && response.data) { try { const responseData = JSON.parse(response.data); if (responseData.output) { imagePath = responseData.output; } } catch (e) { console.error('解析图片URL失败:', e); } } // 保存处理结果 if (imagePath) { await bookInfoService.updateBookInfo(segment.id, { image_path: imagePath, draw_status: 2 // 已完成状态 }); console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`); successCount++; // 更新处理进度 setCompletedDrawings(prevCount => { const newCount = prevCount + 1; console.log(`已完成图片数量更新:${prevCount} -> ${newCount}`); // 更新进度UI setImageGenerationProgress({ current: newCount, total: totalSegmentsToProcess }); // 保存进度到localStorage localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: { current: newCount, total: totalSegmentsToProcess }, timestamp: new Date().getTime(), isPaused: false })); return newCount; }); // 实时更新UI setSegments(prevSegments => prevSegments.map(seg => seg.id === segment.id ? { ...seg, image_path: imagePath, draw_status: 2 } : seg ) ); } else { // 如果没有成功获取图片URL,更新为错误状态 await bookInfoService.updateBookInfo(segment.id, { draw_status: 0 // 重新设为排队状态,允许重试 }); errorCount++; console.error(`无法从响应中提取图片URL,分镜ID: ${segment.id}`); } } catch (error) { // 处理错误 errorCount++; console.error(`处理分镜ID ${segment.id} 时出错:`, error); // 将状态设回排队,允许重试 await bookInfoService.updateBookInfo(segment.id, { draw_status: 0 }); } } // 处理完所有分镜后检查最终状态 const projectStatus = await checkGenerationStatus(); if (projectStatus === 'paused') { toast.info('图片生成已暂停,可以稍后继续'); } else if (projectStatus === 'canceled') { // 重置所有状态 setGeneratingImages(false); setIsPaused(false); setImageGenerationProgress({ current: 0, total: 0 }); localStorage.removeItem(`project_${projectId}_image_generation`); toast.info('图片生成已取消'); } else { // 正常完成 setGeneratingImages(false); setIsPaused(false); localStorage.removeItem(`project_${projectId}_image_generation`); if (errorCount === 0) { toast.success(`图片生成完成,成功生成${successCount}张图片`); } else { toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`); } } } catch (error) { console.error('批量生成图片过程中发生错误:', error); toast.error('图片生成失败: ' + error.message); } finally { // 清理状态 setDrawingSegmentId(null); setIsGenerating(false); } }; // 检查绘画任务状态的辅助函数 const checkGenerationStatus = async () => { // 从数据库获取最新状态 const currentSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); // 检查是否有暂停状态的分镜 const pausedSegments = currentSegments.filter(segment => segment.draw_status === -1); if (pausedSegments.length > 0) { return 'paused'; } // 检查是否有排队或绘画中的分镜 const activeSegments = currentSegments.filter( segment => segment.draw_status === 0 || segment.draw_status === 1 ); if (activeSegments.length === 0) { // 没有活跃任务,可能是全部完成或被取消 const anyWithDescription = currentSegments.some( segment => segment.description && segment.description.trim() !== '' ); if (!anyWithDescription) { return 'canceled'; // 没有描述词,视为取消 } const completedCount = currentSegments.filter( segment => segment.draw_status === 2 && segment.image_path ).length; const totalWithDescription = currentSegments.filter( segment => segment.description && segment.description.trim() !== '' ).length; if (completedCount >= totalWithDescription) { return 'completed'; // 全部完成 } } // 默认为正在进行中 return 'active'; }; // 优化暂停图片生成功能 const pauseImageGeneration = async () => { console.log('点击暂停按钮'); try { // 1. 将UI状态设为暂停 setIsPaused(true); // 2. 获取当前项目的所有分镜 const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); // 3. 找出所有正在绘制(1)或排队(0)的分镜 const activeSegments = allSegments.filter(segment => segment.draw_status === 0 || segment.draw_status === 1 ); if (activeSegments.length === 0) { console.log('没有需要暂停的任务'); return; } console.log(`找到${activeSegments.length}个活跃分镜需要暂停`); // 4. 将它们的状态设置为暂停(-1) await Promise.all(activeSegments.map(segment => bookInfoService.updateBookInfo(segment.id, { draw_status: -1 // 暂停状态 }) )); // 5. 保存当前进度到localStorage const completedCount = allSegments.filter(segment => segment.draw_status === 2 && segment.image_path ).length; const totalCount = allSegments.filter(segment => segment.description && segment.description.trim() !== '' ).length; const currentProgress = { current: completedCount, total: totalCount }; // 更新UI状态 setImageGenerationProgress(currentProgress); // 保存到localStorage localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: currentProgress, timestamp: new Date().getTime(), isPaused: true })); // 更新本地状态 setSegments(prevSegments => prevSegments.map(segment => activeSegments.some(active => active.id === segment.id) ? { ...segment, draw_status: -1 } : segment ) ); toast.info('图片生成任务已暂停'); } catch (error) { console.error('暂停图片生成失败:', error); toast.error('暂停失败: ' + error.message); } }; // 优化恢复图片生成功能 const resumeImageGeneration = async () => { console.log('点击继续按钮'); try { // 1. 获取当前项目的所有分镜 const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); // 2. 找出所有暂停状态(-1)的分镜 const pausedSegments = allSegments.filter(segment => segment.draw_status === -1); if (pausedSegments.length === 0) { console.log('没有处于暂停状态的分镜'); return; } console.log(`找到${pausedSegments.length}个处于暂停状态的分镜`); // 3. 将它们的状态设置为排队状态(0) await Promise.all(pausedSegments.map(segment => bookInfoService.updateBookInfo(segment.id, { draw_status: 0 // 排队状态 }) )); // 计算已完成的图片数量 const completedCount = allSegments.filter(segment => segment.draw_status === 2 && segment.image_path ).length; console.log(`恢复时已完成图片数量: ${completedCount}`); setCompletedDrawings(completedCount); // 计算总数量(有描述词的分镜总数) const totalCount = allSegments.filter(segment => segment.description && segment.description.trim() !== '' ).length; // 4. 更新UI状态 setIsPaused(false); setGeneratingImages(true); // 确保UI显示为生成中状态 setSegments(prevSegments => prevSegments.map(segment => pausedSegments.some(paused => paused.id === segment.id) ? { ...segment, draw_status: 0 } : segment ) ); // 更新进度显示 setImageGenerationProgress({ current: completedCount, total: totalCount }); // 5. 更新localStorage中的暂停状态 localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: { current: completedCount, total: totalCount }, isPaused: false, timestamp: new Date().getTime() })); // 6. 启动绘画流程 toast.info('继续图片生成任务'); // 直接调用generateAllImages,不使用setTimeout await generateAllImages(false); } catch (error) { console.error('恢复图片生成失败:', error); toast.error('恢复失败: ' + error.message); } }; // 取消图片生成 const cancelImageGeneration = async () => { console.log('用户点击取消按钮'); try { // 1. 清空任务队列 const canceledTasks = taskQueueManager.clearQueue(); console.log(`已取消队列中的 ${canceledTasks} 个任务`); // 2. 获取当前项目的所有分镜 const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); // 3. 找出所有非完成状态的分镜(状态不为2的) const activeSegments = allSegments.filter(segment => segment.draw_status !== 2 ); if (activeSegments.length === 0) { console.log('没有需要取消的任务'); return; } console.log(`找到${activeSegments.length}个非完成状态的分镜需要取消`); // 4. 将它们的状态设置为已完成(2) for (const segment of activeSegments) { await bookInfoService.updateBookInfo(segment.id, { draw_status: 2 // 已完成状态 }); } // 5. 清除UI状态 setGeneratingImages(false); setIsPaused(false); // 重新计算并设置进度,可以看出所有任务都已完成 const completedCount = allSegments.filter(segment => segment.draw_status === 2 && segment.image_path ).length; // 在取消状态下,只计算已完成的分镜,因为其他都已被设置为完成状态 setImageGenerationProgress({ current: completedCount, total: completedCount }); // 6. 清除localStorage localStorage.removeItem(`project_${projectId}_image_generation`); // 7. 刷新数据 await loadProjectDetail(true); // 只显示一个取消成功的提示 toast.info('绘图任务已取消'); } catch (error) { console.error('取消图片生成失败:', error); toast.error(`取消失败: ${error.message}`); // 即使出错也尝试清除状态 setGeneratingImages(false); setIsPaused(false); localStorage.removeItem(`project_${projectId}_image_generation`); } }; // 单个分镜生成图片函数,使用draw_status字段控制 const generateSingleImage = async (segment, forceRedraw = false) => { if (!hasValidToken()) { toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置'); return; } if (!selectedStyle) { toast.warning('请先选择项目画风'); return; } if (!segment.description || segment.description.trim() === '') { toast.warning('分镜描述词为空,无法生成图片'); return; } // 如果已有图片且不是强制重绘,则不处理 if (segment.image_path && !forceRedraw) { toast.info('该分镜已有图片,如需重新生成,请使用"重绘"按钮'); return; } // 设置分镜为绘画中状态(1) await bookInfoService.updateBookInfo(segment.id, { draw_status: 1 // 绘画中状态 }); // 更新UI状态 setGeneratingImages(true); // 设置进度为0/1,表示开始为单个分镜生成图片 setImageGenerationProgress({ current: 0, total: 1 }); toast.info(`开始${forceRedraw ? '重新' : ''}生成图片,请稍候...`); // 初始化Coze服务 const cozeInstance = initCozeService({ timeout: 60000 // 设置60秒超时 }); console.log(`为分镜ID ${segment.id} ${forceRedraw ? '重新' : ''}生成图片...`); // 使用任务队列管理器添加任务,确保一次只处理一个API请求 try { // 创建一个异步任务函数 const imageGenerationTask = async () => { console.log(`执行分镜ID ${segment.id} 的图片生成任务`); // 再次检查当前分镜状态,确保没有被暂停 const currentSegment = await getSegmentById(segment.id); if (currentSegment.draw_status === -1) { console.log('检测到当前分镜已被设置为暂停状态,跳过处理'); return null; } // 调用Coze的生成图片工作流 console.log(`发送API请求,生成分镜ID ${segment.id} 的图片...`); const response = await cozeInstance.generateImage( segment.description, // 描述词作为prompt selectedStyle, // 项目设置的画风 { width: 1024, height: 1024, num_images: 1 } ); console.log(`API请求成功完成,分镜ID ${segment.id} 已获得响应`); return response; }; // 添加任务到队列,并等待其完成 const response = await taskQueueManager.addTask( imageGenerationTask, `分镜ID ${segment.id} 图片生成` ); // 如果返回null,表示任务被跳过 if (response === null) { setGeneratingImages(false); // 重置进度状态 setImageGenerationProgress({ current: 0, total: 0 }); return; } // 检查分镜当前状态,如果已设置为暂停状态,则不保存结果 const segmentAfterApiCall = await getSegmentById(segment.id); if (segmentAfterApiCall.draw_status === -1) { console.log('API请求完成后检测到分镜已被设置为暂停状态,不保存结果'); setGeneratingImages(false); // 重置进度状态 setImageGenerationProgress({ current: 0, total: 0 }); return; } // 从响应中提取图片URL let imagePath = ''; if (response && response.data) { try { const responseData = JSON.parse(response.data); if (responseData.output && responseData.output) { imagePath = responseData.output; } } catch (e) { console.error('解析图片URL失败:', e); } } // 保存到数据库 if (imagePath) { await bookInfoService.updateBookInfo(segment.id, { id: segment.id, image_path: imagePath, draw_status: 2 // 已完成状态 }); console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`); // toast.success(`图片${forceRedraw ? '重新生成' : '生成'}成功`); // 更新进度为1/1,表示图片生成100%完成 setImageGenerationProgress({ current: 1, total: 1 }); // 实时更新图片显示 setSegments(prevSegments => prevSegments.map(seg => seg.id === segment.id ? { ...seg, image_path: imagePath, draw_status: 2 } : seg ) ); } else { // 如果未成功获取图片,将状态设置为已完成(2)以避免卡在绘画中状态 await bookInfoService.updateBookInfo(segment.id, { draw_status: 2 // 已完成状态 }); throw new Error('无法从响应中提取图片URL'); } // 静默刷新,避免闪烁 await loadProjectDetail(true); } catch (error) { console.error(`为分镜ID ${segment.id} 生成图片失败:`, error); // 将状态设置为已完成(2)以避免卡在绘画中状态 await bookInfoService.updateBookInfo(segment.id, { draw_status: 2 // 已完成状态 }); // 尝试获取错误详情 let errorMsg = error.message || '未知错误'; if (error.response) { try { const errorData = error.response.data; errorMsg = errorData.message || errorData.error || JSON.stringify(errorData); } catch (e) { errorMsg = `API错误: ${error.response.status}`; } } toast.error(`图片生成失败: ${errorMsg}`); } finally { // 释放状态 setGeneratingImages(false); // 延迟重置进度状态,给用户一个视觉反馈的时间 setTimeout(() => { setImageGenerationProgress({ current: 0, total: 0 }); }, 1000); } }; // 显示图片预览 const showImagePreview = (imageUrl) => { if (imageUrl) { setPreviewImageUrl(imageUrl); setShowImagePreviewModal(true); } }; // 组件加载时初始化 useEffect(() => { let isMounted = true; // 用于防止组件卸载后设置状态 const initialize = async () => { try { // 初始化数据库并强制更新结构以确保有draw_status字段 await initDb(true); if (!isMounted) return; // 检查是否有任何活动的绘图任务(状态为0或1),将其暂停 const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); const activeSegments = segmentsData.filter( segment => segment.draw_status === 0 || segment.draw_status === 1 ); // 如果有活动的任务,将它们设置为暂停状态 if (activeSegments.length > 0) { console.log(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`); // 将活动任务设置为暂停状态 for (const segment of activeSegments) { await bookInfoService.updateBookInfo(segment.id, { draw_status: -1 // 暂停状态 }); } // 更新本地存储,标记为暂停状态 const completedCount = segmentsData.filter( segment => segment.draw_status === 2 && segment.image_path ).length; const totalCount = segmentsData.filter(segment => segment.description && segment.description.trim() !== '' && ( segment.draw_status === -1 || segment.draw_status === 0 || segment.draw_status === 1 || (segment.draw_status === 2 && segment.image_path) ) ).length; localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: { current: completedCount, total: totalCount }, timestamp: new Date().getTime(), isPaused: true })); // 显示通知,告知用户绘图任务已被自动暂停 setTimeout(() => { toast.info(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`); }, 1500); // 稍微延迟显示,以便不与其他初始化消息重叠 } // 清除可能存在的过期状态 if (localStorage.getItem(`project_${projectId}_image_generation`)) { // 检查时间戳是否超过2小时 const savedState = JSON.parse(localStorage.getItem(`project_${projectId}_image_generation`)); const currentTime = new Date().getTime(); // 如果上次保存时间超过2小时,视为过期状态 if (currentTime - savedState.timestamp > 2 * 60 * 60 * 1000) { console.log('发现过期的绘图状态,重置状态'); localStorage.removeItem(`project_${projectId}_image_generation`); // 重置所有非完成状态的分镜 const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId)); const incompleteSegments = segmentsData.filter(segment => segment.draw_status !== 2); for (const segment of incompleteSegments) { await bookInfoService.updateBookInfo(segment.id, { draw_status: 2 // 设置为已完成状态 }); } } } // 加载项目详情和画风列表 await loadProjectDetail(); await loadStylesList(); // 检查是否有正在进行的绘图任务 await checkOngoingImageGeneration(); } catch (error) { console.error('初始化失败:', error); if (!isMounted) return; toast.error(`初始化失败: ${error.message}`); // 尽量加载项目详情和画风列表 try { await loadProjectDetail(); await loadStylesList(); } catch (e) { console.error('备用加载失败:', e); } } }; initialize(); // 清理函数 return () => { console.log('组件卸载,检查是否需要清理状态'); isMounted = false; // 清空任务队列 const canceledTasks = taskQueueManager.clearQueue(); console.log(`组件卸载时清空任务队列,取消了 ${canceledTasks} 个任务`); // 获取当前分镜状态 bookInfoService.getBookInfoByBookId(parseInt(projectId)) .then(segmentsData => { // 检查是否有非完成状态(不是2)的分镜且不是暂停状态(-1) const activeSegments = segmentsData.filter( segment => segment.draw_status !== 2 && segment.draw_status !== -1 ); if (activeSegments.length > 0) { console.log(`存在${activeSegments.length}个非暂停的活动绘图任务,设置为已完成状态`); // 将所有非暂停的活动任务设置为已完成 activeSegments.forEach(segment => { bookInfoService.updateBookInfo(segment.id, { draw_status: 2 // 已完成状态 }).catch(e => console.error(`更新分镜 ${segment.id} 状态失败:`, e)); }); // 清除localStorage localStorage.removeItem(`project_${projectId}_image_generation`); } else { // 检查是否有暂停状态的分镜 const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1); if (pausedSegments.length > 0) { console.log(`存在${pausedSegments.length}个暂停的绘图任务,保留暂停状态`); // 确保localStorage中的状态与数据库一致 localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({ progress: { current: segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length, total: pausedSegments.length + segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length }, timestamp: new Date().getTime(), isPaused: true })); } else { // 没有需要保留的状态,清除localStorage localStorage.removeItem(`project_${projectId}_image_generation`); } } }) .catch(error => { console.error('组件卸载时检查分镜状态失败:', error); // 清除localStorage localStorage.removeItem(`project_${projectId}_image_generation`); }); }; }, [projectId]); // 处理音频文件选择 const handleProjectAudioFileChange = (event) => { if (event.target.files && event.target.files.length > 0) { setProjectAudioFile(event.target.files[0]); } }; // 上传项目音频 const uploadProjectAudio = async () => { if (!projectAudioFile) return; try { setUploadingProjectAudio(true); // 读取文件 const reader = new FileReader(); const fileReadPromise = new Promise((resolve, reject) => { reader.onload = () => resolve(reader.result); reader.onerror = (error) => reject(error); reader.readAsArrayBuffer(projectAudioFile); }); // 等待文件读取完成 const fileBuffer = await fileReadPromise; // 通过 IPC 通信保存文件 const result = await window.electron.ipcRenderer.invoke('save-audio-file', { projectId, fileBuffer: Array.from(new Uint8Array(fileBuffer)) }); if (!result.success) { throw new Error(result.error || '保存音频文件失败'); } // 将本地文件上传到Coze获取可访问链接 toast.info('正在将音频上传到云端...'); const audioUrl = await cozeUploadService.uploadFileAndGetLink(projectAudioFile); // 更新项目数据库记录 - 同时保存本地路径和云端链接 await bookService.updateBook(parseInt(projectId), { audio_path: result.filePath, audio_url: audioUrl // 新增字段保存云端链接 }); // 更新状态 setProjectAudioPath(result.filePath); setShowAudioPlayer(true); setProjectAudioFile(null); setShowUploadProjectAudioModal(false); if (audioUrl) { toast.success('项目音频已成功上传并保存云端链接'); } else { toast.success(`项目音频${projectAudioPath ? '更新' : '上传'}成功`); toast.warning('云端链接保存失败,将在生成对口型时重新上传'); } } catch (error) { console.error('上传项目音频失败:', error); toast.error(`上传项目音频失败: ${error.message}`); } finally { setUploadingProjectAudio(false); } }; /** * 处理对口型生成 */ const handleLipSyncGeneration = async () => { try { // 检查是否有音频和视频 if (!project.audio_path || !project.video_path) { toast.error('请先上传音频和视频文件'); return; } // 确认是否开始处理 if (!window.confirm('确定要开始生成对口型视频吗?此过程可能需要几分钟时间。')) { return; } setLipSyncProcessing(true); setLipSyncProgress(0); let audioUrl = project.audio_url; // 尝试使用已保存的云端链接 let videoUrl = project.video_url; // 尝试使用已保存的云端链接 // 如果没有保存的云端链接,则进行上传 if (!audioUrl || !videoUrl) { toast.info('正在上传音频和视频文件...'); // 异步上传音频和视频 const uploadResults = await Promise.all([ !audioUrl ? cozeUploadService.uploadProjectMedia(project.audio_path) : Promise.resolve(audioUrl), !videoUrl ? cozeUploadService.uploadProjectMedia(project.video_path) : Promise.resolve(videoUrl) ]); audioUrl = uploadResults[0]; videoUrl = uploadResults[1]; // 如果成功获取到链接,保存到数据库中以便下次使用 if (audioUrl && audioUrl !== project.audio_url) { try { await bookService.updateBook(project.id, { audio_url: audioUrl }); console.log('已保存音频云端链接'); } catch (error) { console.error('保存音频云端链接失败:', error); } } if (videoUrl && videoUrl !== project.video_url) { try { await bookService.updateBook(project.id, { video_url: videoUrl }); console.log('已保存视频云端链接'); } catch (error) { console.error('保存视频云端链接失败:', error); } } } else { toast.info('使用已保存的音频和视频链接...'); } if (!audioUrl || !videoUrl) { throw new Error('上传音频或视频文件失败'); } toast.success('文件准备完成,开始处理对口型...'); // 开始处理对口型任务 lipSyncService.processLipSyncTask( videoUrl, audioUrl, // 进度更新回调 (progress, status) => { console.log(`对口型进度: ${progress}%, 状态: ${status}`); setLipSyncProgress(progress); }, // 完成回调 async (resultUrl, taskInfo) => { console.log('对口型处理完成:', resultUrl); setLipSyncResultUrl(resultUrl); setLipSyncProcessing(false); // 保存结果到项目 try { await bookService.updateBook(project.id, { lip_sync_video_path: resultUrl, // video_path: resultUrl, // 同时更新视频路径 lip_sync_task_id: taskInfo.task_id }); toast.success('对口型视频已保存到项目'); // 重新加载项目详情 await loadProjectDetail(true); } catch (error) { console.error('保存对口型结果失败:', error); toast.error(`保存失败: ${error.message}`); } }, // 错误回调 (error) => { console.error('对口型处理失败:', error); toast.error(`对口型处理失败: ${error.message}`); setLipSyncProcessing(false); } ); } catch (error) { console.error('处理对口型失败:', error); toast.error(`处理失败: ${error.message}`); setLipSyncProcessing(false); } }; /** * 处理导出剪映草稿 */ const handleExportDraft = async () => { if (!hasValidToken()) { toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置'); return; } // 检查必要条件 if (!project.audio_path) { toast.error('项目缺少音频文件,无法导出剪映草稿'); return; } setExportingDraft(true); try { // 整理分镜数据 const orderedSegments = [...segments].sort((a, b) => a.segment_id - b.segment_id); // 检查分镜是否有必要的信息 const incompleteSegments = orderedSegments.filter( segment => !segment.text || !segment.start_time || !segment.end_time ); if (incompleteSegments.length > 0) { toast.error(`有${incompleteSegments.length}个分镜缺少必要信息(文本、开始时间或结束时间),请完善后再导出`); return; } // 构建文本列表 - 按照API要求格式化时间 const textList = orderedSegments.map((segment) => { // 转换时间格式为微秒 const startTimeMs = convertTimeToMicroseconds(segment.start_time); const endTimeMs = convertTimeToMicroseconds(segment.end_time); return { text: segment.text, start_time: startTimeMs, end_time: endTimeMs, duration: endTimeMs - startTimeMs, // 添加可选的图片路径 image_path: segment.image_path || "" }; }); const cozeInstance = initCozeService({ timeout: 120000 // 设置120秒超时 }); // 准备所有必要参数 const params = { // 必填参数 project_id: projectId, text_list: textList, // 添加音频链接参数 audio_url: project.audio_url || project.audio_path, // 添加可选参数 video_url: project.lip_sync_video_path || project.video_path || "", project_title: project.title || "项目" + projectId }; console.log("导出剪映草稿参数:", JSON.stringify(params, null, 2)); // 调用导出API - 使用runWorkflow替代exportToDraft const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, params); if (response && response.data) { try { // 解析响应数据 const responseObj = JSON.parse(response.data); console.log("导出剪映草稿响应:", responseObj); // 检查状态码 if (response.code === 0) { if (responseObj && responseObj.draft_url) { const draftUrl = responseObj.draft_url; // 保存草稿链接到数据库 await bookService.updateBook(parseInt(projectId), { draft_url: draftUrl }); // 更新本地状态 setProject(prev => ({ ...prev, draft_url: draftUrl })); toast.success('剪映草稿导出成功'); // 刷新项目数据 await loadProjectDetail(true); } else { throw new Error('未找到有效的草稿链接'); } } else { // 导出失败 throw new Error(response.msg || '导出失败'); } } catch (e) { console.error('解析导出结果失败:', e); throw new Error('解析导出结果失败: ' + e.message); } } else { throw new Error('导出失败,未收到有效响应'); } } catch (error) { console.error('导出剪映草稿失败:', error); toast.error(`导出失败: ${error.message}`); } finally { setExportingDraft(false); } }; // 辅助函数:将时间格式转换为微秒 const convertTimeToMicroseconds = (timeStr) => { if (!timeStr) return 0; // 处理时间格式 "HH:MM:SS,SSS" const [time, milliseconds] = timeStr.split(','); const [hours, minutes, seconds] = time.split(':').map(Number); const ms = milliseconds ? parseInt(milliseconds) : 0; // 转换为微秒 return (hours * 3600 + minutes * 60 + seconds) * 1000000 + ms * 1000; }; if (loading && !silentLoading) { return null; // 移除加载中的显示 } return (
{selectedSegments.length > 1 && ( )}
{project ? (
{/* 项目资源和画风设置 */}
项目资源与画风设置
{/* 项目音频 */} 项目音频 {projectAudioPath && projectAudioPath.trim() !== '' ? (
console.log('项目音频开始播放')} onPause={() => console.log('项目音频暂停播放')} />
) : ( 暂无项目音频 )}
{/* 项目视频 */} 项目视频 {project.video_path && project.video_path.trim() !== '' ? (
) : ( 暂无项目视频 )}
{/* 对口型生成区域 */} 对口型视频 {lipSyncProcessing ? (
正在生成对口型视频,请耐心等待...
) : project.lip_sync_video_path ? (
) : ( 暂无对口型视频 {(project.audio_path && project.video_path) ?
已上传音频和视频文件,可以点击"生成对口型"按钮开始处理
:
需要先上传音频和视频文件才能生成对口型
}
)}
{/* 对口型操作按钮 */} {/* 项目画风选择 */} 项目画风 handleStyleSelect(e.target.value)} disabled={loadingStyles || stylesList.length === 0} > {stylesList.length === 0 ? ( ) : ( <> {stylesList.map((style, index) => ( ))} )} 选择的画风将应用于生成的图像中 剪映草稿 {project.draft_url ? ( ) : ( 尚未导出剪映草稿 )}
分镜列表
{isPaused ? (
) : ( <> {generatingImages ? (
) : ( 画图操作 generateAllImages(false)} disabled={!selectedStyle} > 开始绘画 generateAllImages(true)} disabled={!selectedStyle} > 一键重绘 )} )}
{segments.length > 0 ? (
{segments.map((segment) => ( ))}
选择 ID 文本内容 描述词 图片 操作
toggleSegmentSelection(segment.id)} /> {segment.id} {segment.is_merged && ( 合并 )} {editingSegmentId === segment.id && editingTextField === 'text' ? (
setDescriptionText(e.target.value)} className="mb-2" />
) : (
handleDirectEditText(segment.id, segment.text)}>
{formatTime(segment.start_time)} → {formatTime(segment.end_time)} ({segment.duration}秒)
{segment.text || 点击添加文本内容}
)}
{editingSegmentId === segment.id && editingTextField === 'description' ? (
setDescriptionText(e.target.value)} className="mb-2" />
) : (
handleDirectEditDescription(segment.id, segment.description)} > {segment.description || 点击添加描述词}
)}
{segment.image_path ? (
分镜图片 showImagePreview(segment.image_path)} style={{ width: '100%', maxWidth: '80px', height: 'auto', cursor: 'pointer' }} />
查看大图} > 重新上传} > 重新生成图片} >
) : (
{segment.description && segment.description.trim() !== '' && ( )}
)}
操作 showSplitSegmentModal(segment)}>拆分 showUploadModalFor(segment.id, 'audio')}>上传音频 showUploadModalFor(segment.id, 'image')}>上传图片 showUploadModalFor(segment.id, 'video')}>上传口播视频 generateSingleDescription(segment)} disabled={generatingDescriptions} > {segment.description && segment.description.trim() !== '' ? '重新生成描述词' : '生成描述词'} generateSingleImage(segment, segment.image_path ? true : false)} disabled={generatingImages || !selectedStyle || !segment.description || segment.description.trim() === ''} > {segment.image_path ? '重新生成图片' : '生成图片'} deleteSegment(segment.id)} > 删除分镜
) : (

没有分镜数据

)}
) : (
{loading ? ( ) : ( 项目不存在或已被删除 )}
)}
{/* 合并分镜对话框 */} setShowMergeModal(false)}> 合并分镜

确定要合并以下分镜吗?

    {selectedSegments.map(id => { const segment = segments.find(s => s.id === id); return segment ? (
  • 分镜 {id}: {segment.text ? segment.text.substring(0, 30) + (segment.text.length > 30 ? '...' : '') : '无文本'}
  • ) : null; })}
{/* 拆分分镜对话框 */} setShowSplitModal(false)} size="lg"> 拆分分镜
原分镜内容:

{splitSegment?.text}

拆分为:
{newSegments.map((segment, index) => (
新分镜 #{index + 1}
文本内容 updateNewSegmentData(index, 'text', e.target.value)} /> 开始时间 (HH:MM:SS,SSS) updateNewSegmentData(index, 'start_time', e.target.value)} placeholder="00:00:00,000" /> 结束时间 (HH:MM:SS,SSS) updateNewSegmentData(index, 'end_time', e.target.value)} placeholder="00:00:00,000" />
))}
{/* 上传文件对话框 */} setShowUploadModal(false)}> {(() => { const segment = segments.find(s => s.id === uploadSegmentId); const isUpdate = segment && ( (uploadType === 'audio' && segment.audio_path) || (uploadType === 'image' && segment.image_path) || (uploadType === 'video' && segment.video_path) ); if (isUpdate) { return uploadType === 'audio' ? '更新音频' : uploadType === 'image' ? '更新图片' : '更新口播视频'; } else { return uploadType === 'audio' ? '上传音频' : uploadType === 'image' ? '上传图片' : '上传口播视频'; } })()} {(() => { const segment = segments.find(s => s.id === uploadSegmentId); const existingPath = segment && ( uploadType === 'audio' ? segment.audio_path : uploadType === 'image' ? segment.image_path : segment.video_path ); return ( <> {existingPath && (
当前{uploadType === 'audio' ? '音频' : uploadType === 'image' ? '图片' : '视频'}:
{uploadType === 'image' && (
当前图片
)} {(uploadType === 'audio' || uploadType === 'video') && (
{existingPath}
)}
)} 选择{uploadType === 'audio' ? '音频文件' : uploadType === 'image' ? '图片文件' : '视频文件'} {uploadType === 'audio' ? '支持的格式: MP3, WAV, OGG' : uploadType === 'image' ? '支持的格式: JPG, PNG, GIF' : '支持的格式: MP4, WebM, AVI'} ); })()}
{/* 描述词对话框 */} setShowDescriptionModal(false)}> 编辑描述词 描述词内容 setDescriptionText(e.target.value)} placeholder="请输入描述词..." /> {/* 删除项目确认对话框 */} setShowDeleteConfirm(false)}> 确认删除项目

您确定要删除此项目吗?此操作将同时删除所有相关的分镜数据,且不可恢复!

{project &&

项目名称: {project.title}

}
{/* 图片预览对话框 */} setShowImagePreviewModal(false)}> 图片预览 图片预览 {/* 上传项目音频对话框 */} setShowUploadProjectAudioModal(false)}> {projectAudioPath ? '更新项目音频' : '上传项目音频'} {projectAudioPath && (
当前项目音频:
{projectAudioPath}
)}
选择音频文件 支持的格式: MP3, WAV, OGG等常见音频格式
); }; export default ProjectDetail;