projectDetail.js 107 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931
  1. import React, { useEffect, useState, useRef } from 'react';
  2. import { useParams, useHistory } from 'react-router-dom';
  3. import {
  4. Button, Row, Col, Container, Card, Table, Badge, Form,
  5. Modal, InputGroup, FormControl, Dropdown, OverlayTrigger, Tooltip, Alert, ProgressBar, Spinner
  6. } from 'react-bootstrap';
  7. import { toast } from 'react-toastify';
  8. import { bookService, bookInfoService, initDb } from '../../db';
  9. import { cozeService, initCozeService, getAvailableStyles } from '../../utils/cozeExample';
  10. import { WORKFLOW_IDS, hasValidToken } from '../../utils/cozeConfig';
  11. import { getUserSettings } from '../../utils/storageUtils';
  12. import taskQueueManager from '../../utils/taskQueueManager';
  13. import './projectDetail.css';
  14. import path from 'path-browserify';
  15. // 引入AudioPlayer组件
  16. import AudioPlayerComponent from '../../components/audioPlayer';
  17. import lipSyncService from '../../services/lipSyncService';
  18. import cozeUploadService from '../../services/cozeUploadService';
  19. const ProjectDetail = () => {
  20. const { projectId } = useParams();
  21. const history = useHistory();
  22. const [loading, setLoading] = useState(false);
  23. const [silentLoading, setSilentLoading] = useState(false);
  24. const [project, setProject] = useState(null);
  25. const [segments, setSegments] = useState([]);
  26. const [selectedSegments, setSelectedSegments] = useState([]);
  27. const [showMergeModal, setShowMergeModal] = useState(false);
  28. const [showSplitModal, setShowSplitModal] = useState(false);
  29. const [splitSegment, setSplitSegment] = useState(null);
  30. const [newSegments, setNewSegments] = useState([{ text: '', start_time: '', end_time: '' }]);
  31. const [showUploadModal, setShowUploadModal] = useState(false);
  32. const [uploadType, setUploadType] = useState('audio');
  33. const [uploadSegmentId, setUploadSegmentId] = useState(null);
  34. const [selectedFile, setSelectedFile] = useState(null);
  35. const [descriptionText, setDescriptionText] = useState('');
  36. const [showDescriptionModal, setShowDescriptionModal] = useState(false);
  37. const [descriptionSegmentId, setDescriptionSegmentId] = useState(null);
  38. const [showImagePreviewModal, setShowImagePreviewModal] = useState(false);
  39. const [previewImageUrl, setPreviewImageUrl] = useState('');
  40. const [generatingDescriptions, setGeneratingDescriptions] = useState(false);
  41. const [generationProgress, setGenerationProgress] = useState({ current: 0, total: 0 });
  42. const [editingSegmentId, setEditingSegmentId] = useState(null);
  43. const [editingTextField, setEditingTextField] = useState(null);
  44. const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
  45. const [hasDescriptions, setHasDescriptions] = useState(false);
  46. const [stylesList, setStylesList] = useState([]);
  47. const [selectedStyle, setSelectedStyle] = useState('');
  48. const [loadingStyles, setLoadingStyles] = useState(false);
  49. const [generatingImages, setGeneratingImages] = useState(false);
  50. const [imageGenerationProgress, setImageGenerationProgress] = useState({ current: 0, total: 0 });
  51. const [isPaused, setIsPaused] = useState(false);
  52. // 在状态变量中添加项目音频相关状态
  53. const [projectAudioPath, setProjectAudioPath] = useState(null);
  54. const [showAudioPlayer, setShowAudioPlayer] = useState(false);
  55. const [projectVideoPath, setProjectVideoPath] = useState(null);
  56. const [showVideoPlayer, setShowVideoPlayer] = useState(false);
  57. const [showUploadProjectAudioModal, setShowUploadProjectAudioModal] = useState(false);
  58. const [projectAudioFile, setProjectAudioFile] = useState(null);
  59. const [uploadingProjectAudio, setUploadingProjectAudio] = useState(false);
  60. // 对口型相关状态
  61. const [lipSyncProcessing, setLipSyncProcessing] = useState(false);
  62. const [lipSyncProgress, setLipSyncProgress] = useState(0);
  63. const [lipSyncTaskId, setLipSyncTaskId] = useState(null);
  64. const [lipSyncResultUrl, setLipSyncResultUrl] = useState(null);
  65. // 添加导出剪映草稿相关状态
  66. const [exportingDraft, setExportingDraft] = useState(false);
  67. // 添加一个标志位来防止重复调用
  68. const [isGenerating, setIsGenerating] = useState(false);
  69. // 添加一个状态变量防止重复检查
  70. const [isCheckingGeneration, setIsCheckingGeneration] = useState(false);
  71. // 增加新的状态变量来更精确地追踪绘画进度
  72. const [drawingSegmentId, setDrawingSegmentId] = useState(null);
  73. const [totalDrawableSegments, setTotalDrawableSegments] = useState(0);
  74. const [completedDrawings, setCompletedDrawings] = useState(0);
  75. const [isUploading, setIsUploading] = useState(false);
  76. const fileInputRef = useRef(null);
  77. // 辅助函数:根据ID获取单个分镜信息
  78. const getSegmentById = async (segmentId) => {
  79. const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  80. return allSegments.find(segment => segment.id === segmentId);
  81. };
  82. // 加载项目详情
  83. const loadProjectDetail = async (silent = false) => {
  84. try {
  85. if (!silent) {
  86. setLoading(true);
  87. } else {
  88. setSilentLoading(true);
  89. }
  90. const projectData = await bookService.getBookById(parseInt(projectId));
  91. const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  92. setProject(projectData);
  93. setSegments(Array.isArray(segmentsData) ? segmentsData : []);
  94. // 设置当前选中的画风
  95. if (projectData && projectData.style) {
  96. setSelectedStyle(projectData.style);
  97. }
  98. // 加载项目音频路径
  99. if (projectData && projectData.audio_path) {
  100. setProjectAudioPath(projectData.audio_path);
  101. setShowAudioPlayer(true);
  102. } else {
  103. setProjectAudioPath(null);
  104. setShowAudioPlayer(false);
  105. }
  106. // 加载项目视频路径
  107. if (projectData && projectData.video_path) {
  108. setProjectVideoPath(projectData.video_path);
  109. setShowVideoPlayer(true);
  110. } else {
  111. setProjectVideoPath(null);
  112. setShowVideoPlayer(false);
  113. }
  114. // 检查是否已经有描述词
  115. const hasAnyDescriptions = Array.isArray(segmentsData) &&
  116. segmentsData.some(segment => segment.description && segment.description.trim() !== '');
  117. setHasDescriptions(hasAnyDescriptions);
  118. // 检查是否有正在进行的绘图任务
  119. checkOngoingImageGeneration();
  120. } catch (error) {
  121. console.error('加载项目详情失败:', error);
  122. toast.error(`加载项目详情失败: ${error.message}`);
  123. } finally {
  124. if (!silent) {
  125. setLoading(false);
  126. } else {
  127. setSilentLoading(false);
  128. }
  129. }
  130. };
  131. // 获取画风列表
  132. const loadStylesList = async () => {
  133. if (!hasValidToken()) {
  134. toast.warning('未配置Coze API Token,无法获取画风列表');
  135. return;
  136. }
  137. try {
  138. setLoadingStyles(true);
  139. const cozeInstance = initCozeService();
  140. // 调用获取画风列表API
  141. const response = await cozeInstance.getStyleList();
  142. console.log('获取画风列表响应:', response);
  143. // 处理返回结果
  144. let styles = [];
  145. if (response && response.data) {
  146. try {
  147. const responseData = JSON.parse(response.data);
  148. if (responseData.output) {
  149. // 如果返回的是数组
  150. if (Array.isArray(responseData.output)) {
  151. styles = responseData.output;
  152. }
  153. // 如果返回的是对象中包含styles数组
  154. else if (responseData.output.styles && Array.isArray(responseData.output.styles)) {
  155. styles = responseData.output.styles;
  156. }
  157. }
  158. } catch (e) {
  159. console.error('解析画风列表数据失败:', e);
  160. }
  161. }
  162. // 如果没有获取到画风列表,使用默认值
  163. if (styles.length === 0) {
  164. styles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格'];
  165. }
  166. setStylesList(styles);
  167. // 如果项目还没有设置画风,且有可用画风,设置第一个为默认
  168. if (styles.length > 0 && (!selectedStyle || selectedStyle === '')) {
  169. setSelectedStyle(styles[0]);
  170. }
  171. } catch (error) {
  172. console.error('获取画风列表失败:', error);
  173. toast.error(`获取画风列表失败: ${error.message}`);
  174. // 使用默认的画风列表
  175. const defaultStyles = ['真人风格', '古风风格', '卡通风格', '水彩风格', '油画风格'];
  176. setStylesList(defaultStyles);
  177. } finally {
  178. setLoadingStyles(false);
  179. }
  180. };
  181. // 选择画风
  182. const handleStyleSelect = async (style) => {
  183. try {
  184. setLoading(true);
  185. setSelectedStyle(style);
  186. // 保存画风到项目中
  187. await bookService.updateBook(parseInt(projectId), {
  188. style: style
  189. });
  190. toast.success(`画风已设置为: ${style}`);
  191. } catch (error) {
  192. console.error('设置画风失败:', error);
  193. toast.error(`设置画风失败: ${error.message}`);
  194. } finally {
  195. setLoading(false);
  196. }
  197. };
  198. // 返回项目列表
  199. const backToProjectList = () => {
  200. history.push('/');
  201. };
  202. // 格式化时间
  203. const formatTime = (timeString) => {
  204. if (!timeString) return '';
  205. return timeString.replace(',', '.');
  206. };
  207. // 计算持续时间(秒)
  208. const calculateDuration = (startTime, endTime) => {
  209. const parseTime = (time) => {
  210. if (!time) return 0;
  211. const [hours, minutes, seconds] = time.replace(',', '.').split(':').map(parseFloat);
  212. return hours * 3600 + minutes * 60 + seconds;
  213. };
  214. const startSeconds = parseTime(startTime);
  215. const endSeconds = parseTime(endTime);
  216. return (endSeconds - startSeconds).toFixed(2);
  217. };
  218. // 选择/取消选择分镜
  219. const toggleSegmentSelection = (segmentId) => {
  220. setSelectedSegments(prev => {
  221. if (prev.includes(segmentId)) {
  222. return prev.filter(id => id !== segmentId);
  223. } else {
  224. return [...prev, segmentId];
  225. }
  226. });
  227. };
  228. // 显示合并分镜对话框
  229. const showMergeSegmentsModal = () => {
  230. if (selectedSegments.length < 2) {
  231. toast.error('请至少选择两个分镜进行合并');
  232. return;
  233. }
  234. setShowMergeModal(true);
  235. };
  236. // 合并分镜
  237. const mergeSegments = async () => {
  238. try {
  239. setLoading(true);
  240. await bookInfoService.mergeSegments(parseInt(projectId), selectedSegments);
  241. toast.success('分镜合并成功');
  242. setSelectedSegments([]);
  243. setShowMergeModal(false);
  244. await loadProjectDetail();
  245. } catch (error) {
  246. console.error('合并分镜失败:', error);
  247. toast.error(`合并分镜失败: ${error.message}`);
  248. } finally {
  249. setLoading(false);
  250. }
  251. };
  252. // 显示拆分分镜对话框
  253. const showSplitSegmentModal = (segment) => {
  254. setSplitSegment(segment);
  255. setNewSegments([{
  256. text: segment.text || '',
  257. start_time: segment.start_time || '',
  258. end_time: segment.end_time || ''
  259. }]);
  260. setShowSplitModal(true);
  261. };
  262. // 添加新分镜表单
  263. const addNewSegmentForm = () => {
  264. setNewSegments([...newSegments, { text: '', start_time: '', end_time: '' }]);
  265. };
  266. // 删除分镜表单
  267. const removeSegmentForm = (index) => {
  268. if (newSegments.length <= 1) {
  269. toast.error('至少需要一个分镜');
  270. return;
  271. }
  272. const updatedSegments = [...newSegments];
  273. updatedSegments.splice(index, 1);
  274. setNewSegments(updatedSegments);
  275. };
  276. // 更新分镜表单数据
  277. const updateNewSegmentData = (index, field, value) => {
  278. const updatedSegments = [...newSegments];
  279. updatedSegments[index][field] = value;
  280. setNewSegments(updatedSegments);
  281. };
  282. // 拆分分镜
  283. const splitSegmentSubmit = async () => {
  284. try {
  285. // 验证输入
  286. for (const segment of newSegments) {
  287. if (!segment.text || !segment.start_time || !segment.end_time) {
  288. toast.error('请填写所有分镜的文本内容和时间信息');
  289. return;
  290. }
  291. // 验证时间格式
  292. const timePattern = /^\d{2}:\d{2}:\d{2},\d{3}$/;
  293. if (!timePattern.test(segment.start_time) || !timePattern.test(segment.end_time)) {
  294. toast.error('时间格式不正确,应为: HH:MM:SS,SSS');
  295. return;
  296. }
  297. }
  298. setLoading(true);
  299. await bookInfoService.splitSegment(splitSegment.id, newSegments);
  300. toast.success('分镜拆分成功');
  301. setShowSplitModal(false);
  302. setSplitSegment(null);
  303. setNewSegments([{ text: '', start_time: '', end_time: '' }]);
  304. await loadProjectDetail();
  305. } catch (error) {
  306. console.error('拆分分镜失败:', error);
  307. toast.error(`拆分分镜失败: ${error.message}`);
  308. } finally {
  309. setLoading(false);
  310. }
  311. };
  312. // 显示上传对话框
  313. const showUploadModalFor = (segmentId, type) => {
  314. setUploadSegmentId(segmentId);
  315. setUploadType(type);
  316. setSelectedFile(null);
  317. setShowUploadModal(true);
  318. };
  319. // 处理文件选择改变
  320. const handleFileChange = (e) => {
  321. setSelectedFile(e.target.files[0]);
  322. };
  323. // 上传文件
  324. const handleFileUpload = async () => {
  325. if (!selectedFile) {
  326. toast.error('请先选择文件');
  327. return;
  328. }
  329. // 获取当前处理的分镜
  330. const currentSegment = segments.find(s => s.id === uploadSegmentId);
  331. if (!currentSegment) {
  332. toast.error('分镜不存在');
  333. return;
  334. }
  335. try {
  336. setIsUploading(true);
  337. // 如果是图片类型,先上传到Coze获取链接
  338. if (uploadType === 'image') {
  339. toast.info('正在上传图片到云端...');
  340. const imageUrl = await cozeUploadService.uploadFileAndGetLink(selectedFile);
  341. if (!imageUrl) {
  342. throw new Error('获取图片链接失败');
  343. }
  344. // 更新分镜信息 - 保存图片链接
  345. await bookInfoService.updateBookInfo(uploadSegmentId, {
  346. image_path: imageUrl,
  347. draw_status: 2 // 设置为已完成状态
  348. });
  349. // 直接更新本地状态,不刷新整个页面
  350. setSegments(prevSegments =>
  351. prevSegments.map(seg =>
  352. seg.id === uploadSegmentId
  353. ? { ...seg, image_path: imageUrl, draw_status: 2 }
  354. : seg
  355. )
  356. );
  357. toast.success('图片上传成功');
  358. } else {
  359. // 其他类型文件(音频、视频)的处理逻辑
  360. // 创建上传路径
  361. const filePath = `uploads/${uploadType}/${selectedFile.name}`;
  362. // 更新分镜信息
  363. const updateData = {
  364. id: uploadSegmentId
  365. };
  366. if (uploadType === 'audio') {
  367. updateData.audio_path = filePath;
  368. } else if (uploadType === 'video') {
  369. updateData.video_path = filePath;
  370. }
  371. await bookInfoService.updateBookInfo(uploadSegmentId, updateData);
  372. // 直接更新本地状态,不刷新整个页面
  373. setSegments(prevSegments =>
  374. prevSegments.map(seg =>
  375. seg.id === uploadSegmentId
  376. ? { ...seg, ...updateData }
  377. : seg
  378. )
  379. );
  380. toast.success('文件上传成功');
  381. }
  382. // 关闭上传对话框
  383. setShowUploadModal(false);
  384. // 清空选择的文件
  385. setSelectedFile(null);
  386. if (fileInputRef.current) {
  387. fileInputRef.current.value = '';
  388. }
  389. } catch (error) {
  390. console.error('文件上传失败:', error);
  391. toast.error(`文件上传失败: ${error.message}`);
  392. } finally {
  393. setIsUploading(false);
  394. }
  395. };
  396. // 显示描述词对话框
  397. const showDescriptionModalFor = (segment) => {
  398. setDescriptionSegmentId(segment.id);
  399. setDescriptionText(segment.description || '');
  400. setShowDescriptionModal(true);
  401. };
  402. // 保存描述词
  403. const saveDescription = async (segmentId, description) => {
  404. try {
  405. setLoading(true);
  406. await bookInfoService.updateBookInfo(segmentId, {
  407. description: description
  408. });
  409. toast.success('描述词保存成功');
  410. await loadProjectDetail();
  411. } catch (error) {
  412. console.error('保存描述词失败:', error);
  413. toast.error(`保存描述词失败: ${error.message}`);
  414. } finally {
  415. setLoading(false);
  416. }
  417. };
  418. // 保存文本内容
  419. const saveText = async (segmentId, text) => {
  420. try {
  421. setLoading(true);
  422. await bookInfoService.updateBookInfo(segmentId, {
  423. text: text
  424. });
  425. toast.success('文本内容保存成功');
  426. await loadProjectDetail();
  427. } catch (error) {
  428. console.error('保存文本内容失败:', error);
  429. toast.error(`保存文本内容失败: ${error.message}`);
  430. } finally {
  431. setLoading(false);
  432. }
  433. };
  434. // 一键生成描述词
  435. const generateAllDescriptions = async () => {
  436. if (!hasValidToken()) {
  437. toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
  438. return;
  439. }
  440. try {
  441. setGeneratingDescriptions(true);
  442. // 统计所有有文本的分镜数量
  443. const segmentsWithText = segments.filter(segment => segment.text && segment.text.trim() !== '');
  444. setGenerationProgress({ current: 0, total: segmentsWithText.length });
  445. toast.info(`开始生成描述词,共${segmentsWithText.length}个分镜需要处理...`);
  446. // 初始化Coze服务
  447. const cozeInstance = initCozeService();
  448. // 处理每个分镜
  449. let successCount = 0;
  450. let errorCount = 0;
  451. for (const segment of segments) {
  452. if (segment.text && segment.text.trim() !== '') {
  453. try {
  454. setGenerationProgress(prev => ({ ...prev, current: prev.current + 1 }));
  455. console.log(`处理分镜ID ${segment.id} 的描述词生成 (${successCount + errorCount + 1}/${segmentsWithText.length})...`);
  456. // 调用Coze的文本转描述词工作流
  457. const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, {
  458. prompt: segment.text,
  459. });
  460. console.log(`分镜ID ${segment.id} 的API响应:`, JSON.parse(response.data).output);
  461. // 从响应中提取描述词
  462. let description = '';
  463. if (response && response.data) {
  464. description = JSON.parse(response.data).output;
  465. }
  466. // 保存到数据库
  467. await bookInfoService.updateBookInfo(segment.id, {
  468. description: description
  469. });
  470. // 直接更新状态而不是重新加载
  471. setSegments(prevSegments =>
  472. prevSegments.map(seg =>
  473. seg.id === segment.id
  474. ? { ...seg, description: description }
  475. : seg
  476. )
  477. );
  478. console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`);
  479. successCount++;
  480. } catch (error) {
  481. console.error(`分镜ID ${segment.id} 的描述词生成失败:`, error);
  482. errorCount++;
  483. // 尝试获取错误详情
  484. let errorMsg = error.message || '未知错误';
  485. if (error.response) {
  486. try {
  487. const errorData = error.response.data;
  488. errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
  489. } catch (e) {
  490. errorMsg = `API错误: ${error.response.status}`;
  491. }
  492. }
  493. // 更新分镜的错误信息
  494. try {
  495. // 仅在分镜没有描述词的情况下,添加错误信息
  496. if (!segment.description || segment.description.trim() === '') {
  497. await bookInfoService.updateBookInfo(segment.id, {
  498. description: `[生成失败] ${errorMsg}`
  499. });
  500. // 直接更新状态而不是重新加载
  501. setSegments(prevSegments =>
  502. prevSegments.map(seg =>
  503. seg.id === segment.id
  504. ? { ...seg, description: `[生成失败] ${errorMsg}` }
  505. : seg
  506. )
  507. );
  508. }
  509. } catch (saveError) {
  510. console.error('保存错误信息失败:', saveError);
  511. }
  512. }
  513. }
  514. }
  515. // 显示结果统计
  516. if (errorCount > 0 && successCount > 0) {
  517. toast.warning(`描述词生成完成,成功: ${successCount}个,失败: ${errorCount}个`);
  518. } else if (errorCount > 0 && successCount === 0) {
  519. toast.error(`描述词生成全部失败,请检查API配置和网络连接`);
  520. } else {
  521. toast.success(hasDescriptions ? `描述词更新完成,共更新${successCount}个分镜` : `描述词生成完成,共生成${successCount}个分镜`);
  522. }
  523. setHasDescriptions(successCount > 0 || segments.some(segment => segment.description && segment.description.trim() !== ''));
  524. } catch (error) {
  525. console.error('生成描述词失败:', error);
  526. toast.error(`生成描述词失败: ${error.message}`);
  527. } finally {
  528. setGeneratingDescriptions(false);
  529. setGenerationProgress({ current: 0, total: 0 });
  530. }
  531. };
  532. // 处理直接编辑描述词
  533. const handleDirectEditDescription = (segmentId, description) => {
  534. setEditingSegmentId(segmentId);
  535. setEditingTextField('description');
  536. setDescriptionText(description || '');
  537. };
  538. // 处理直接编辑文本内容
  539. const handleDirectEditText = (segmentId, text) => {
  540. setEditingSegmentId(segmentId);
  541. setEditingTextField('text');
  542. setDescriptionText(text || '');
  543. };
  544. // 保存直接编辑的内容
  545. const saveDirectEdit = async () => {
  546. if (!editingSegmentId || !editingTextField) return;
  547. try {
  548. if (editingTextField === 'description') {
  549. await saveDescription(editingSegmentId, descriptionText);
  550. } else if (editingTextField === 'text') {
  551. await saveText(editingSegmentId, descriptionText);
  552. }
  553. // 清除编辑状态
  554. setEditingSegmentId(null);
  555. setEditingTextField(null);
  556. setDescriptionText('');
  557. } catch (error) {
  558. console.error('保存编辑内容失败:', error);
  559. toast.error(`保存失败: ${error.message}`);
  560. }
  561. };
  562. // 取消直接编辑
  563. const cancelDirectEdit = () => {
  564. setEditingSegmentId(null);
  565. setEditingTextField(null);
  566. setDescriptionText('');
  567. };
  568. // 显示删除确认对话框
  569. const showDeleteConfirmation = () => {
  570. setShowDeleteConfirm(true);
  571. };
  572. // 删除项目
  573. const deleteProject = async () => {
  574. try {
  575. setLoading(true);
  576. // 调用删除API
  577. await bookService.deleteBook(parseInt(projectId));
  578. toast.success('项目删除成功');
  579. // 返回首页
  580. history.push('/');
  581. } catch (error) {
  582. console.error('删除项目失败:', error);
  583. toast.error(`删除项目失败: ${error.message}`);
  584. } finally {
  585. setLoading(false);
  586. setShowDeleteConfirm(false);
  587. }
  588. };
  589. // 删除分镜
  590. const deleteSegment = async (segmentId) => {
  591. try {
  592. setLoading(true);
  593. // 调用删除API
  594. await bookInfoService.deleteBookInfo(segmentId);
  595. toast.success('分镜删除成功');
  596. // 重新加载分镜列表
  597. await loadProjectDetail();
  598. } catch (error) {
  599. console.error('删除分镜失败:', error);
  600. toast.error(`删除分镜失败: ${error.message}`);
  601. } finally {
  602. setLoading(false);
  603. }
  604. };
  605. // 为单个分镜生成描述词
  606. const generateSingleDescription = async (segment) => {
  607. if (!hasValidToken()) {
  608. toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
  609. return;
  610. }
  611. if (!segment.text || segment.text.trim() === '') {
  612. toast.warning('分镜文本为空,无法生成描述词');
  613. return;
  614. }
  615. try {
  616. setGeneratingDescriptions(true);
  617. // 设置当前正在处理的分镜ID
  618. setDescriptionSegmentId(segment.id);
  619. toast.info('开始生成描述词,请稍候...');
  620. // 初始化Coze服务
  621. const cozeInstance = initCozeService();
  622. console.log(`为分镜ID ${segment.id} 生成描述词...`);
  623. // 调用Coze的文本转描述词工作流
  624. const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.textToDescription, {
  625. text: segment.text,
  626. style: 'detailed',
  627. language: 'zh'
  628. });
  629. console.log(`分镜ID ${segment.id} 的API响应:`, response);
  630. // 从响应中提取描述词
  631. let description = '';
  632. if (response && response.description) {
  633. description = response.description;
  634. } else if (response && typeof response === 'string') {
  635. description = response;
  636. } else if (response && response.result) {
  637. description = response.result;
  638. } else if (response && response.content) {
  639. description = response.content;
  640. } else {
  641. // 如果API返回格式不一致,尝试从整个响应对象提取有意义的内容
  642. try {
  643. description = JSON.stringify(response);
  644. } catch (e) {
  645. description = `描述词生成成功,但无法解析结果: ${e.message}`;
  646. }
  647. }
  648. // 保存到数据库
  649. await bookInfoService.updateBookInfo(segment.id, {
  650. description: description
  651. });
  652. console.log(`分镜ID ${segment.id} 的描述词生成成功: ${description.slice(0, 30)}...`);
  653. toast.success('描述词生成成功');
  654. // 刷新数据
  655. await loadProjectDetail();
  656. // 设置有描述词的状态
  657. setHasDescriptions(true);
  658. } catch (error) {
  659. console.error(`为分镜ID ${segment.id} 生成描述词失败:`, error);
  660. // 尝试获取错误详情
  661. let errorMsg = error.message || '未知错误';
  662. if (error.response) {
  663. try {
  664. const errorData = error.response.data;
  665. errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
  666. } catch (e) {
  667. errorMsg = `API错误: ${error.response.status}`;
  668. }
  669. }
  670. toast.error(`描述词生成失败: ${errorMsg}`);
  671. } finally {
  672. setGeneratingDescriptions(false);
  673. setDescriptionSegmentId(null);
  674. }
  675. };
  676. // 检查是否有正在进行的绘图任务
  677. const checkOngoingImageGeneration = async () => {
  678. // 防止重复检查
  679. if (isCheckingGeneration || isGenerating) {
  680. console.log('已经在检查或正在生成图片中,跳过重复检查');
  681. return null;
  682. }
  683. try {
  684. setIsCheckingGeneration(true);
  685. console.log('检查是否有正在进行的绘图任务');
  686. // 从数据库获取所有分镜
  687. const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  688. // 检查是否有暂停状态(-1)的分镜
  689. const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1);
  690. // 检查有排队状态(0)或绘画中状态(1)的分镜
  691. const pendingSegments = segmentsData.filter(segment =>
  692. segment.draw_status === 0 || segment.draw_status === 1
  693. );
  694. // 如果既没有暂停的也没有待处理的,确保状态重置为默认
  695. if (pausedSegments.length === 0 && pendingSegments.length === 0) {
  696. // 如果重启后没有正在进行的任务,重置所有状态
  697. setGeneratingImages(false);
  698. setIsPaused(false);
  699. localStorage.removeItem(`project_${projectId}_image_generation`);
  700. return null;
  701. }
  702. // 计算已完成的分镜数量(固定值)
  703. const completedCount = segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length;
  704. // 计算总数(需要处理的分镜 + 已完成的分镜)
  705. // 对于暂停状态,总数是:已完成的 + 暂停的 + 排队的 + 绘画中的
  706. const totalSegments = segmentsData.filter(segment =>
  707. segment.description && segment.description.trim() !== '' && (
  708. segment.draw_status === -1 ||
  709. segment.draw_status === 0 ||
  710. segment.draw_status === 1 ||
  711. (segment.draw_status === 2 && segment.image_path)
  712. )
  713. ).length;
  714. if (pausedSegments.length > 0) {
  715. console.log(`检测到${pausedSegments.length}个处于暂停状态的分镜`);
  716. // 设置UI状态为暂停
  717. setGeneratingImages(true);
  718. setIsPaused(true);
  719. setImageGenerationProgress({
  720. current: completedCount,
  721. total: totalSegments
  722. });
  723. // 保存到localStorage
  724. const currentProgress = {
  725. current: completedCount,
  726. total: totalSegments
  727. };
  728. // 保存到localStorage
  729. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  730. progress: currentProgress,
  731. timestamp: new Date().getTime(),
  732. isPaused: true
  733. }));
  734. return;
  735. }
  736. if (pendingSegments.length > 0) {
  737. console.log(`检测到${pendingSegments.length}个待处理的分镜,继续执行绘图任务`);
  738. // 设置UI状态
  739. setGeneratingImages(true);
  740. setIsPaused(false);
  741. setImageGenerationProgress({
  742. current: completedCount,
  743. total: totalSegments
  744. });
  745. // 检查是否已经在生成图片
  746. if (!generatingImages && !isGenerating) {
  747. // 启动绘图任务,使用setTimeout避免可能的状态更新问题
  748. setTimeout(() => {
  749. generateAllImages(false);
  750. }, 1000);
  751. }
  752. return;
  753. }
  754. } catch (error) {
  755. console.error('检查绘图状态失败:', error);
  756. // 出现错误时确保重置状态为默认
  757. localStorage.removeItem(`project_${projectId}_image_generation`);
  758. setGeneratingImages(false);
  759. setIsPaused(false);
  760. } finally {
  761. setIsCheckingGeneration(false);
  762. }
  763. return null;
  764. };
  765. /**
  766. * 生成所有分镜图片
  767. */
  768. const generateAllImages = async (isForceRedraw = false) => {
  769. // 如果已经在生成中,直接返回
  770. if (isGenerating) {
  771. toast.info('正在绘制图片中,请等待当前操作完成');
  772. return;
  773. }
  774. // 检查是否已选择绘画风格
  775. if (!selectedStyle || selectedStyle.trim() === '') {
  776. toast.error('请先选择绘画风格');
  777. return;
  778. }
  779. try {
  780. setIsGenerating(true);
  781. // 获取需要处理的分镜
  782. let segmentsToProcess = [];
  783. // 计算已经完成的图片数量
  784. const existingCompletedCount = segments.filter(
  785. segment => segment.image_path && segment.draw_status === 2
  786. ).length;
  787. console.log(`当前已完成的图片数量: ${existingCompletedCount}`);
  788. setCompletedDrawings(0); // 重置已完成计数
  789. if (isForceRedraw) {
  790. // 重绘:处理所有有描述词的分镜
  791. const segmentsToRedraw = segments.filter(
  792. segment => segment.description && segment.description.trim() !== ''
  793. );
  794. console.log(`重绘: 选择${segmentsToRedraw.length}个有描述词的分镜`);
  795. // 如果没有需要重绘的分镜,直接返回
  796. if (segmentsToRedraw.length === 0) {
  797. toast.info('没有需要重绘的分镜');
  798. setIsGenerating(false);
  799. return;
  800. }
  801. // 先更新本地状态 - 将所有选定的分镜状态设为排队状态
  802. setSegments(prevSegments =>
  803. prevSegments.map(segment =>
  804. segmentsToRedraw.some(s => s.id === segment.id)
  805. ? { ...segment, draw_status: 0, image_path: null }
  806. : segment
  807. )
  808. );
  809. // 异步更新数据库
  810. await Promise.all(segmentsToRedraw.map(segment =>
  811. bookInfoService.updateBookInfo(segment.id, {
  812. draw_status: 0, // 排队状态
  813. image_path: null // 清除已有图片
  814. })
  815. ));
  816. segmentsToProcess = [...segmentsToRedraw];
  817. } else {
  818. // 继续绘画: 处理没有图片但有描述词的分镜
  819. const segmentsToQueue = segments.filter(
  820. segment => segment.description &&
  821. segment.description.trim() !== '' &&
  822. !segment.image_path &&
  823. segment.draw_status !== 2 // 确保不是已完成状态
  824. );
  825. console.log(`继续绘画: 选择${segmentsToQueue.length}个有描述词但没有图片的分镜`);
  826. // 如果没有需要处理的分镜,直接返回
  827. if (segmentsToQueue.length === 0) {
  828. toast.info('没有需要绘制的分镜');
  829. setIsGenerating(false);
  830. return;
  831. }
  832. // 更新所有需要处理的分镜状态为排队状态
  833. await Promise.all(segmentsToQueue.map(segment =>
  834. bookInfoService.updateBookInfo(segment.id, {
  835. draw_status: 0 // 排队状态
  836. })
  837. ));
  838. segmentsToProcess = [...segmentsToQueue];
  839. }
  840. // 计算总进度
  841. const totalSegmentsToProcess = segmentsToProcess.length;
  842. setTotalDrawableSegments(totalSegmentsToProcess);
  843. // 设置UI状态
  844. setGeneratingImages(true);
  845. setIsPaused(false);
  846. setImageGenerationProgress({ current: 0, total: totalSegmentsToProcess }); // 重置进度为0
  847. // 保存到localStorage
  848. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  849. progress: { current: 0, total: totalSegmentsToProcess },
  850. timestamp: new Date().getTime(),
  851. isPaused: false
  852. }));
  853. // 顺序处理每个分镜
  854. let successCount = 0;
  855. let errorCount = 0;
  856. let shouldContinue = true;
  857. for (let i = 0; i < segmentsToProcess.length && shouldContinue; i++) {
  858. const segment = segmentsToProcess[i];
  859. // 设置当前正在处理的分镜ID
  860. setDrawingSegmentId(segment.id);
  861. // 每次处理前检查是否被暂停
  862. const currentStatus = await checkGenerationStatus();
  863. if (currentStatus === 'paused') {
  864. console.log('检测到暂停状态,停止图片生成');
  865. shouldContinue = false;
  866. break;
  867. } else if (currentStatus === 'canceled') {
  868. console.log('检测到取消状态,停止图片生成');
  869. shouldContinue = false;
  870. break;
  871. }
  872. // 处理前再次检查分镜状态
  873. const segmentBeforeProcess = await getSegmentById(segment.id);
  874. if (segmentBeforeProcess.draw_status !== 0 || segmentBeforeProcess.image_path) {
  875. console.log(`分镜ID ${segment.id} 当前状态不是排队状态或已有图片,跳过处理`);
  876. continue;
  877. }
  878. // 更新分镜状态为绘画中
  879. await bookInfoService.updateBookInfo(segment.id, {
  880. draw_status: 1 // 绘画中状态
  881. });
  882. // 实时更新UI
  883. setSegments(prevSegments =>
  884. prevSegments.map(seg =>
  885. seg.id === segment.id
  886. ? { ...seg, draw_status: 1 }
  887. : seg
  888. )
  889. );
  890. try {
  891. console.log(`开始处理分镜ID ${segment.id},进度 ${i+1}/${totalSegmentsToProcess}`);
  892. // 初始化Coze服务
  893. const cozeInstance = initCozeService({
  894. timeout: 60000 // 设置60秒超时
  895. });
  896. // 调用API生成图片
  897. const response = await cozeInstance.generateImage(
  898. segment.description, // 描述词作为prompt
  899. selectedStyle, // 项目设置的画风
  900. {
  901. width: 1024,
  902. height: 1024,
  903. num_images: 1
  904. }
  905. );
  906. // 每次API调用后检查是否被暂停
  907. const statusAfterAPI = await checkGenerationStatus();
  908. if (statusAfterAPI === 'paused' || statusAfterAPI === 'canceled') {
  909. console.log('API调用后检测到暂停/取消状态,停止后续处理');
  910. shouldContinue = false;
  911. break;
  912. }
  913. // 从响应中提取图片URL
  914. let imagePath = '';
  915. if (response && response.data) {
  916. try {
  917. const responseData = JSON.parse(response.data);
  918. if (responseData.output) {
  919. imagePath = responseData.output;
  920. }
  921. } catch (e) {
  922. console.error('解析图片URL失败:', e);
  923. }
  924. }
  925. // 保存处理结果
  926. if (imagePath) {
  927. await bookInfoService.updateBookInfo(segment.id, {
  928. image_path: imagePath,
  929. draw_status: 2 // 已完成状态
  930. });
  931. console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`);
  932. successCount++;
  933. // 更新处理进度
  934. setCompletedDrawings(prevCount => {
  935. const newCount = prevCount + 1;
  936. console.log(`已完成图片数量更新:${prevCount} -> ${newCount}`);
  937. // 更新进度UI
  938. setImageGenerationProgress({
  939. current: newCount,
  940. total: totalSegmentsToProcess
  941. });
  942. // 保存进度到localStorage
  943. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  944. progress: { current: newCount, total: totalSegmentsToProcess },
  945. timestamp: new Date().getTime(),
  946. isPaused: false
  947. }));
  948. return newCount;
  949. });
  950. // 实时更新UI
  951. setSegments(prevSegments =>
  952. prevSegments.map(seg =>
  953. seg.id === segment.id
  954. ? { ...seg, image_path: imagePath, draw_status: 2 }
  955. : seg
  956. )
  957. );
  958. } else {
  959. // 如果没有成功获取图片URL,更新为错误状态
  960. await bookInfoService.updateBookInfo(segment.id, {
  961. draw_status: 0 // 重新设为排队状态,允许重试
  962. });
  963. errorCount++;
  964. console.error(`无法从响应中提取图片URL,分镜ID: ${segment.id}`);
  965. }
  966. } catch (error) {
  967. // 处理错误
  968. errorCount++;
  969. console.error(`处理分镜ID ${segment.id} 时出错:`, error);
  970. // 将状态设回排队,允许重试
  971. await bookInfoService.updateBookInfo(segment.id, {
  972. draw_status: 0
  973. });
  974. }
  975. }
  976. // 处理完所有分镜后检查最终状态
  977. const projectStatus = await checkGenerationStatus();
  978. if (projectStatus === 'paused') {
  979. toast.info('图片生成已暂停,可以稍后继续');
  980. } else if (projectStatus === 'canceled') {
  981. // 重置所有状态
  982. setGeneratingImages(false);
  983. setIsPaused(false);
  984. setImageGenerationProgress({ current: 0, total: 0 });
  985. localStorage.removeItem(`project_${projectId}_image_generation`);
  986. toast.info('图片生成已取消');
  987. } else {
  988. // 正常完成
  989. setGeneratingImages(false);
  990. setIsPaused(false);
  991. localStorage.removeItem(`project_${projectId}_image_generation`);
  992. if (errorCount === 0) {
  993. toast.success(`图片生成完成,成功生成${successCount}张图片`);
  994. } else {
  995. toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
  996. }
  997. }
  998. } catch (error) {
  999. console.error('批量生成图片过程中发生错误:', error);
  1000. toast.error('图片生成失败: ' + error.message);
  1001. } finally {
  1002. // 清理状态
  1003. setDrawingSegmentId(null);
  1004. setIsGenerating(false);
  1005. }
  1006. };
  1007. // 检查绘画任务状态的辅助函数
  1008. const checkGenerationStatus = async () => {
  1009. // 从数据库获取最新状态
  1010. const currentSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1011. // 检查是否有暂停状态的分镜
  1012. const pausedSegments = currentSegments.filter(segment => segment.draw_status === -1);
  1013. if (pausedSegments.length > 0) {
  1014. return 'paused';
  1015. }
  1016. // 检查是否有排队或绘画中的分镜
  1017. const activeSegments = currentSegments.filter(
  1018. segment => segment.draw_status === 0 || segment.draw_status === 1
  1019. );
  1020. if (activeSegments.length === 0) {
  1021. // 没有活跃任务,可能是全部完成或被取消
  1022. const anyWithDescription = currentSegments.some(
  1023. segment => segment.description && segment.description.trim() !== ''
  1024. );
  1025. if (!anyWithDescription) {
  1026. return 'canceled'; // 没有描述词,视为取消
  1027. }
  1028. const completedCount = currentSegments.filter(
  1029. segment => segment.draw_status === 2 && segment.image_path
  1030. ).length;
  1031. const totalWithDescription = currentSegments.filter(
  1032. segment => segment.description && segment.description.trim() !== ''
  1033. ).length;
  1034. if (completedCount >= totalWithDescription) {
  1035. return 'completed'; // 全部完成
  1036. }
  1037. }
  1038. // 默认为正在进行中
  1039. return 'active';
  1040. };
  1041. // 优化暂停图片生成功能
  1042. const pauseImageGeneration = async () => {
  1043. console.log('点击暂停按钮');
  1044. try {
  1045. // 1. 将UI状态设为暂停
  1046. setIsPaused(true);
  1047. // 2. 获取当前项目的所有分镜
  1048. const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1049. // 3. 找出所有正在绘制(1)或排队(0)的分镜
  1050. const activeSegments = allSegments.filter(segment =>
  1051. segment.draw_status === 0 || segment.draw_status === 1
  1052. );
  1053. if (activeSegments.length === 0) {
  1054. console.log('没有需要暂停的任务');
  1055. return;
  1056. }
  1057. console.log(`找到${activeSegments.length}个活跃分镜需要暂停`);
  1058. // 4. 将它们的状态设置为暂停(-1)
  1059. await Promise.all(activeSegments.map(segment =>
  1060. bookInfoService.updateBookInfo(segment.id, {
  1061. draw_status: -1 // 暂停状态
  1062. })
  1063. ));
  1064. // 5. 保存当前进度到localStorage
  1065. const completedCount = allSegments.filter(segment =>
  1066. segment.draw_status === 2 && segment.image_path
  1067. ).length;
  1068. const totalCount = allSegments.filter(segment =>
  1069. segment.description && segment.description.trim() !== ''
  1070. ).length;
  1071. const currentProgress = {
  1072. current: completedCount,
  1073. total: totalCount
  1074. };
  1075. // 更新UI状态
  1076. setImageGenerationProgress(currentProgress);
  1077. // 保存到localStorage
  1078. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  1079. progress: currentProgress,
  1080. timestamp: new Date().getTime(),
  1081. isPaused: true
  1082. }));
  1083. // 更新本地状态
  1084. setSegments(prevSegments =>
  1085. prevSegments.map(segment =>
  1086. activeSegments.some(active => active.id === segment.id)
  1087. ? { ...segment, draw_status: -1 }
  1088. : segment
  1089. )
  1090. );
  1091. toast.info('图片生成任务已暂停');
  1092. } catch (error) {
  1093. console.error('暂停图片生成失败:', error);
  1094. toast.error('暂停失败: ' + error.message);
  1095. }
  1096. };
  1097. // 优化恢复图片生成功能
  1098. const resumeImageGeneration = async () => {
  1099. console.log('点击继续按钮');
  1100. try {
  1101. // 1. 获取当前项目的所有分镜
  1102. const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1103. // 2. 找出所有暂停状态(-1)的分镜
  1104. const pausedSegments = allSegments.filter(segment => segment.draw_status === -1);
  1105. if (pausedSegments.length === 0) {
  1106. console.log('没有处于暂停状态的分镜');
  1107. return;
  1108. }
  1109. console.log(`找到${pausedSegments.length}个处于暂停状态的分镜`);
  1110. // 3. 将它们的状态设置为排队状态(0)
  1111. await Promise.all(pausedSegments.map(segment =>
  1112. bookInfoService.updateBookInfo(segment.id, {
  1113. draw_status: 0 // 排队状态
  1114. })
  1115. ));
  1116. // 计算已完成的图片数量
  1117. const completedCount = allSegments.filter(segment =>
  1118. segment.draw_status === 2 && segment.image_path
  1119. ).length;
  1120. console.log(`恢复时已完成图片数量: ${completedCount}`);
  1121. setCompletedDrawings(completedCount);
  1122. // 计算总数量(有描述词的分镜总数)
  1123. const totalCount = allSegments.filter(segment =>
  1124. segment.description && segment.description.trim() !== ''
  1125. ).length;
  1126. // 4. 更新UI状态
  1127. setIsPaused(false);
  1128. setGeneratingImages(true); // 确保UI显示为生成中状态
  1129. setSegments(prevSegments =>
  1130. prevSegments.map(segment =>
  1131. pausedSegments.some(paused => paused.id === segment.id)
  1132. ? { ...segment, draw_status: 0 }
  1133. : segment
  1134. )
  1135. );
  1136. // 更新进度显示
  1137. setImageGenerationProgress({
  1138. current: completedCount,
  1139. total: totalCount
  1140. });
  1141. // 5. 更新localStorage中的暂停状态
  1142. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  1143. progress: { current: completedCount, total: totalCount },
  1144. isPaused: false,
  1145. timestamp: new Date().getTime()
  1146. }));
  1147. // 6. 启动绘画流程
  1148. toast.info('继续图片生成任务');
  1149. // 直接调用generateAllImages,不使用setTimeout
  1150. await generateAllImages(false);
  1151. } catch (error) {
  1152. console.error('恢复图片生成失败:', error);
  1153. toast.error('恢复失败: ' + error.message);
  1154. }
  1155. };
  1156. // 取消图片生成
  1157. const cancelImageGeneration = async () => {
  1158. console.log('用户点击取消按钮');
  1159. try {
  1160. // 1. 清空任务队列
  1161. const canceledTasks = taskQueueManager.clearQueue();
  1162. console.log(`已取消队列中的 ${canceledTasks} 个任务`);
  1163. // 2. 获取当前项目的所有分镜
  1164. const allSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1165. // 3. 找出所有非完成状态的分镜(状态不为2的)
  1166. const activeSegments = allSegments.filter(segment =>
  1167. segment.draw_status !== 2
  1168. );
  1169. if (activeSegments.length === 0) {
  1170. console.log('没有需要取消的任务');
  1171. return;
  1172. }
  1173. console.log(`找到${activeSegments.length}个非完成状态的分镜需要取消`);
  1174. // 4. 将它们的状态设置为已完成(2)
  1175. for (const segment of activeSegments) {
  1176. await bookInfoService.updateBookInfo(segment.id, {
  1177. draw_status: 2 // 已完成状态
  1178. });
  1179. }
  1180. // 5. 清除UI状态
  1181. setGeneratingImages(false);
  1182. setIsPaused(false);
  1183. // 重新计算并设置进度,可以看出所有任务都已完成
  1184. const completedCount = allSegments.filter(segment =>
  1185. segment.draw_status === 2 && segment.image_path
  1186. ).length;
  1187. // 在取消状态下,只计算已完成的分镜,因为其他都已被设置为完成状态
  1188. setImageGenerationProgress({
  1189. current: completedCount,
  1190. total: completedCount
  1191. });
  1192. // 6. 清除localStorage
  1193. localStorage.removeItem(`project_${projectId}_image_generation`);
  1194. // 7. 刷新数据
  1195. await loadProjectDetail(true);
  1196. // 只显示一个取消成功的提示
  1197. toast.info('绘图任务已取消');
  1198. } catch (error) {
  1199. console.error('取消图片生成失败:', error);
  1200. toast.error(`取消失败: ${error.message}`);
  1201. // 即使出错也尝试清除状态
  1202. setGeneratingImages(false);
  1203. setIsPaused(false);
  1204. localStorage.removeItem(`project_${projectId}_image_generation`);
  1205. }
  1206. };
  1207. // 单个分镜生成图片函数,使用draw_status字段控制
  1208. const generateSingleImage = async (segment, forceRedraw = false) => {
  1209. if (!hasValidToken()) {
  1210. toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
  1211. return;
  1212. }
  1213. if (!selectedStyle) {
  1214. toast.warning('请先选择项目画风');
  1215. return;
  1216. }
  1217. if (!segment.description || segment.description.trim() === '') {
  1218. toast.warning('分镜描述词为空,无法生成图片');
  1219. return;
  1220. }
  1221. // 如果已有图片且不是强制重绘,则不处理
  1222. if (segment.image_path && !forceRedraw) {
  1223. toast.info('该分镜已有图片,如需重新生成,请使用"重绘"按钮');
  1224. return;
  1225. }
  1226. // 设置分镜为绘画中状态(1)
  1227. await bookInfoService.updateBookInfo(segment.id, {
  1228. draw_status: 1 // 绘画中状态
  1229. });
  1230. // 更新UI状态
  1231. setGeneratingImages(true);
  1232. // 设置进度为0/1,表示开始为单个分镜生成图片
  1233. setImageGenerationProgress({ current: 0, total: 1 });
  1234. toast.info(`开始${forceRedraw ? '重新' : ''}生成图片,请稍候...`);
  1235. // 初始化Coze服务
  1236. const cozeInstance = initCozeService({
  1237. timeout: 60000 // 设置60秒超时
  1238. });
  1239. console.log(`为分镜ID ${segment.id} ${forceRedraw ? '重新' : ''}生成图片...`);
  1240. // 使用任务队列管理器添加任务,确保一次只处理一个API请求
  1241. try {
  1242. // 创建一个异步任务函数
  1243. const imageGenerationTask = async () => {
  1244. console.log(`执行分镜ID ${segment.id} 的图片生成任务`);
  1245. // 再次检查当前分镜状态,确保没有被暂停
  1246. const currentSegment = await getSegmentById(segment.id);
  1247. if (currentSegment.draw_status === -1) {
  1248. console.log('检测到当前分镜已被设置为暂停状态,跳过处理');
  1249. return null;
  1250. }
  1251. // 调用Coze的生成图片工作流
  1252. console.log(`发送API请求,生成分镜ID ${segment.id} 的图片...`);
  1253. const response = await cozeInstance.generateImage(
  1254. segment.description, // 描述词作为prompt
  1255. selectedStyle, // 项目设置的画风
  1256. {
  1257. width: 1024,
  1258. height: 1024,
  1259. num_images: 1
  1260. }
  1261. );
  1262. console.log(`API请求成功完成,分镜ID ${segment.id} 已获得响应`);
  1263. return response;
  1264. };
  1265. // 添加任务到队列,并等待其完成
  1266. const response = await taskQueueManager.addTask(
  1267. imageGenerationTask,
  1268. `分镜ID ${segment.id} 图片生成`
  1269. );
  1270. // 如果返回null,表示任务被跳过
  1271. if (response === null) {
  1272. setGeneratingImages(false);
  1273. // 重置进度状态
  1274. setImageGenerationProgress({ current: 0, total: 0 });
  1275. return;
  1276. }
  1277. // 检查分镜当前状态,如果已设置为暂停状态,则不保存结果
  1278. const segmentAfterApiCall = await getSegmentById(segment.id);
  1279. if (segmentAfterApiCall.draw_status === -1) {
  1280. console.log('API请求完成后检测到分镜已被设置为暂停状态,不保存结果');
  1281. setGeneratingImages(false);
  1282. // 重置进度状态
  1283. setImageGenerationProgress({ current: 0, total: 0 });
  1284. return;
  1285. }
  1286. // 从响应中提取图片URL
  1287. let imagePath = '';
  1288. if (response && response.data) {
  1289. try {
  1290. const responseData = JSON.parse(response.data);
  1291. if (responseData.output && responseData.output) {
  1292. imagePath = responseData.output;
  1293. }
  1294. } catch (e) {
  1295. console.error('解析图片URL失败:', e);
  1296. }
  1297. }
  1298. // 保存到数据库
  1299. if (imagePath) {
  1300. await bookInfoService.updateBookInfo(segment.id, {
  1301. id: segment.id,
  1302. image_path: imagePath,
  1303. draw_status: 2 // 已完成状态
  1304. });
  1305. console.log(`分镜ID ${segment.id} 的图片生成成功: ${imagePath}`);
  1306. // toast.success(`图片${forceRedraw ? '重新生成' : '生成'}成功`);
  1307. // 更新进度为1/1,表示图片生成100%完成
  1308. setImageGenerationProgress({ current: 1, total: 1 });
  1309. // 实时更新图片显示
  1310. setSegments(prevSegments =>
  1311. prevSegments.map(seg =>
  1312. seg.id === segment.id
  1313. ? { ...seg, image_path: imagePath, draw_status: 2 }
  1314. : seg
  1315. )
  1316. );
  1317. } else {
  1318. // 如果未成功获取图片,将状态设置为已完成(2)以避免卡在绘画中状态
  1319. await bookInfoService.updateBookInfo(segment.id, {
  1320. draw_status: 2 // 已完成状态
  1321. });
  1322. throw new Error('无法从响应中提取图片URL');
  1323. }
  1324. // 静默刷新,避免闪烁
  1325. await loadProjectDetail(true);
  1326. } catch (error) {
  1327. console.error(`为分镜ID ${segment.id} 生成图片失败:`, error);
  1328. // 将状态设置为已完成(2)以避免卡在绘画中状态
  1329. await bookInfoService.updateBookInfo(segment.id, {
  1330. draw_status: 2 // 已完成状态
  1331. });
  1332. // 尝试获取错误详情
  1333. let errorMsg = error.message || '未知错误';
  1334. if (error.response) {
  1335. try {
  1336. const errorData = error.response.data;
  1337. errorMsg = errorData.message || errorData.error || JSON.stringify(errorData);
  1338. } catch (e) {
  1339. errorMsg = `API错误: ${error.response.status}`;
  1340. }
  1341. }
  1342. toast.error(`图片生成失败: ${errorMsg}`);
  1343. } finally {
  1344. // 释放状态
  1345. setGeneratingImages(false);
  1346. // 延迟重置进度状态,给用户一个视觉反馈的时间
  1347. setTimeout(() => {
  1348. setImageGenerationProgress({ current: 0, total: 0 });
  1349. }, 1000);
  1350. }
  1351. };
  1352. // 显示图片预览
  1353. const showImagePreview = (imageUrl) => {
  1354. if (imageUrl) {
  1355. setPreviewImageUrl(imageUrl);
  1356. setShowImagePreviewModal(true);
  1357. }
  1358. };
  1359. // 组件加载时初始化
  1360. useEffect(() => {
  1361. let isMounted = true; // 用于防止组件卸载后设置状态
  1362. const initialize = async () => {
  1363. try {
  1364. // 初始化数据库并强制更新结构以确保有draw_status字段
  1365. await initDb(true);
  1366. if (!isMounted) return;
  1367. // 检查是否有任何活动的绘图任务(状态为0或1),将其暂停
  1368. const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1369. const activeSegments = segmentsData.filter(
  1370. segment => segment.draw_status === 0 || segment.draw_status === 1
  1371. );
  1372. // 如果有活动的任务,将它们设置为暂停状态
  1373. if (activeSegments.length > 0) {
  1374. console.log(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`);
  1375. // 将活动任务设置为暂停状态
  1376. for (const segment of activeSegments) {
  1377. await bookInfoService.updateBookInfo(segment.id, {
  1378. draw_status: -1 // 暂停状态
  1379. });
  1380. }
  1381. // 更新本地存储,标记为暂停状态
  1382. const completedCount = segmentsData.filter(
  1383. segment => segment.draw_status === 2 && segment.image_path
  1384. ).length;
  1385. const totalCount = segmentsData.filter(segment =>
  1386. segment.description && segment.description.trim() !== '' && (
  1387. segment.draw_status === -1 ||
  1388. segment.draw_status === 0 ||
  1389. segment.draw_status === 1 ||
  1390. (segment.draw_status === 2 && segment.image_path)
  1391. )
  1392. ).length;
  1393. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  1394. progress: {
  1395. current: completedCount,
  1396. total: totalCount
  1397. },
  1398. timestamp: new Date().getTime(),
  1399. isPaused: true
  1400. }));
  1401. // 显示通知,告知用户绘图任务已被自动暂停
  1402. setTimeout(() => {
  1403. toast.info(`检测到${activeSegments.length}个正在进行的绘图任务,已自动暂停`);
  1404. }, 1500); // 稍微延迟显示,以便不与其他初始化消息重叠
  1405. }
  1406. // 清除可能存在的过期状态
  1407. if (localStorage.getItem(`project_${projectId}_image_generation`)) {
  1408. // 检查时间戳是否超过2小时
  1409. const savedState = JSON.parse(localStorage.getItem(`project_${projectId}_image_generation`));
  1410. const currentTime = new Date().getTime();
  1411. // 如果上次保存时间超过2小时,视为过期状态
  1412. if (currentTime - savedState.timestamp > 2 * 60 * 60 * 1000) {
  1413. console.log('发现过期的绘图状态,重置状态');
  1414. localStorage.removeItem(`project_${projectId}_image_generation`);
  1415. // 重置所有非完成状态的分镜
  1416. const segmentsData = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
  1417. const incompleteSegments = segmentsData.filter(segment => segment.draw_status !== 2);
  1418. for (const segment of incompleteSegments) {
  1419. await bookInfoService.updateBookInfo(segment.id, {
  1420. draw_status: 2 // 设置为已完成状态
  1421. });
  1422. }
  1423. }
  1424. }
  1425. // 加载项目详情和画风列表
  1426. await loadProjectDetail();
  1427. await loadStylesList();
  1428. // 检查是否有正在进行的绘图任务
  1429. await checkOngoingImageGeneration();
  1430. } catch (error) {
  1431. console.error('初始化失败:', error);
  1432. if (!isMounted) return;
  1433. toast.error(`初始化失败: ${error.message}`);
  1434. // 尽量加载项目详情和画风列表
  1435. try {
  1436. await loadProjectDetail();
  1437. await loadStylesList();
  1438. } catch (e) {
  1439. console.error('备用加载失败:', e);
  1440. }
  1441. }
  1442. };
  1443. initialize();
  1444. // 清理函数
  1445. return () => {
  1446. console.log('组件卸载,检查是否需要清理状态');
  1447. isMounted = false;
  1448. // 清空任务队列
  1449. const canceledTasks = taskQueueManager.clearQueue();
  1450. console.log(`组件卸载时清空任务队列,取消了 ${canceledTasks} 个任务`);
  1451. // 获取当前分镜状态
  1452. bookInfoService.getBookInfoByBookId(parseInt(projectId))
  1453. .then(segmentsData => {
  1454. // 检查是否有非完成状态(不是2)的分镜且不是暂停状态(-1)
  1455. const activeSegments = segmentsData.filter(
  1456. segment => segment.draw_status !== 2 && segment.draw_status !== -1
  1457. );
  1458. if (activeSegments.length > 0) {
  1459. console.log(`存在${activeSegments.length}个非暂停的活动绘图任务,设置为已完成状态`);
  1460. // 将所有非暂停的活动任务设置为已完成
  1461. activeSegments.forEach(segment => {
  1462. bookInfoService.updateBookInfo(segment.id, {
  1463. draw_status: 2 // 已完成状态
  1464. }).catch(e => console.error(`更新分镜 ${segment.id} 状态失败:`, e));
  1465. });
  1466. // 清除localStorage
  1467. localStorage.removeItem(`project_${projectId}_image_generation`);
  1468. } else {
  1469. // 检查是否有暂停状态的分镜
  1470. const pausedSegments = segmentsData.filter(segment => segment.draw_status === -1);
  1471. if (pausedSegments.length > 0) {
  1472. console.log(`存在${pausedSegments.length}个暂停的绘图任务,保留暂停状态`);
  1473. // 确保localStorage中的状态与数据库一致
  1474. localStorage.setItem(`project_${projectId}_image_generation`, JSON.stringify({
  1475. progress: {
  1476. current: segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length,
  1477. total: pausedSegments.length + segmentsData.filter(segment => segment.draw_status === 2 && segment.image_path).length
  1478. },
  1479. timestamp: new Date().getTime(),
  1480. isPaused: true
  1481. }));
  1482. } else {
  1483. // 没有需要保留的状态,清除localStorage
  1484. localStorage.removeItem(`project_${projectId}_image_generation`);
  1485. }
  1486. }
  1487. })
  1488. .catch(error => {
  1489. console.error('组件卸载时检查分镜状态失败:', error);
  1490. // 清除localStorage
  1491. localStorage.removeItem(`project_${projectId}_image_generation`);
  1492. });
  1493. };
  1494. }, [projectId]);
  1495. // 处理音频文件选择
  1496. const handleProjectAudioFileChange = (event) => {
  1497. if (event.target.files && event.target.files.length > 0) {
  1498. setProjectAudioFile(event.target.files[0]);
  1499. }
  1500. };
  1501. // 上传项目音频
  1502. const uploadProjectAudio = async () => {
  1503. if (!projectAudioFile) return;
  1504. try {
  1505. setUploadingProjectAudio(true);
  1506. // 读取文件
  1507. const reader = new FileReader();
  1508. const fileReadPromise = new Promise((resolve, reject) => {
  1509. reader.onload = () => resolve(reader.result);
  1510. reader.onerror = (error) => reject(error);
  1511. reader.readAsArrayBuffer(projectAudioFile);
  1512. });
  1513. // 等待文件读取完成
  1514. const fileBuffer = await fileReadPromise;
  1515. // 通过 IPC 通信保存文件
  1516. const result = await window.electron.ipcRenderer.invoke('save-audio-file', {
  1517. projectId,
  1518. fileBuffer: Array.from(new Uint8Array(fileBuffer))
  1519. });
  1520. if (!result.success) {
  1521. throw new Error(result.error || '保存音频文件失败');
  1522. }
  1523. // 将本地文件上传到Coze获取可访问链接
  1524. toast.info('正在将音频上传到云端...');
  1525. const audioUrl = await cozeUploadService.uploadFileAndGetLink(projectAudioFile);
  1526. // 更新项目数据库记录 - 同时保存本地路径和云端链接
  1527. await bookService.updateBook(parseInt(projectId), {
  1528. audio_path: result.filePath,
  1529. audio_url: audioUrl // 新增字段保存云端链接
  1530. });
  1531. // 更新状态
  1532. setProjectAudioPath(result.filePath);
  1533. setShowAudioPlayer(true);
  1534. setProjectAudioFile(null);
  1535. setShowUploadProjectAudioModal(false);
  1536. if (audioUrl) {
  1537. toast.success('项目音频已成功上传并保存云端链接');
  1538. } else {
  1539. toast.success(`项目音频${projectAudioPath ? '更新' : '上传'}成功`);
  1540. toast.warning('云端链接保存失败,将在生成对口型时重新上传');
  1541. }
  1542. } catch (error) {
  1543. console.error('上传项目音频失败:', error);
  1544. toast.error(`上传项目音频失败: ${error.message}`);
  1545. } finally {
  1546. setUploadingProjectAudio(false);
  1547. }
  1548. };
  1549. /**
  1550. * 处理对口型生成
  1551. */
  1552. const handleLipSyncGeneration = async () => {
  1553. try {
  1554. // 检查是否有音频和视频
  1555. if (!project.audio_path || !project.video_path) {
  1556. toast.error('请先上传音频和视频文件');
  1557. return;
  1558. }
  1559. // 确认是否开始处理
  1560. if (!window.confirm('确定要开始生成对口型视频吗?此过程可能需要几分钟时间。')) {
  1561. return;
  1562. }
  1563. setLipSyncProcessing(true);
  1564. setLipSyncProgress(0);
  1565. let audioUrl = project.audio_url; // 尝试使用已保存的云端链接
  1566. let videoUrl = project.video_url; // 尝试使用已保存的云端链接
  1567. // 如果没有保存的云端链接,则进行上传
  1568. if (!audioUrl || !videoUrl) {
  1569. toast.info('正在上传音频和视频文件...');
  1570. // 异步上传音频和视频
  1571. const uploadResults = await Promise.all([
  1572. !audioUrl ? cozeUploadService.uploadProjectMedia(project.audio_path) : Promise.resolve(audioUrl),
  1573. !videoUrl ? cozeUploadService.uploadProjectMedia(project.video_path) : Promise.resolve(videoUrl)
  1574. ]);
  1575. audioUrl = uploadResults[0];
  1576. videoUrl = uploadResults[1];
  1577. // 如果成功获取到链接,保存到数据库中以便下次使用
  1578. if (audioUrl && audioUrl !== project.audio_url) {
  1579. try {
  1580. await bookService.updateBook(project.id, { audio_url: audioUrl });
  1581. console.log('已保存音频云端链接');
  1582. } catch (error) {
  1583. console.error('保存音频云端链接失败:', error);
  1584. }
  1585. }
  1586. if (videoUrl && videoUrl !== project.video_url) {
  1587. try {
  1588. await bookService.updateBook(project.id, { video_url: videoUrl });
  1589. console.log('已保存视频云端链接');
  1590. } catch (error) {
  1591. console.error('保存视频云端链接失败:', error);
  1592. }
  1593. }
  1594. } else {
  1595. toast.info('使用已保存的音频和视频链接...');
  1596. }
  1597. if (!audioUrl || !videoUrl) {
  1598. throw new Error('上传音频或视频文件失败');
  1599. }
  1600. toast.success('文件准备完成,开始处理对口型...');
  1601. // 开始处理对口型任务
  1602. lipSyncService.processLipSyncTask(
  1603. videoUrl,
  1604. audioUrl,
  1605. // 进度更新回调
  1606. (progress, status) => {
  1607. console.log(`对口型进度: ${progress}%, 状态: ${status}`);
  1608. setLipSyncProgress(progress);
  1609. },
  1610. // 完成回调
  1611. async (resultUrl, taskInfo) => {
  1612. console.log('对口型处理完成:', resultUrl);
  1613. setLipSyncResultUrl(resultUrl);
  1614. setLipSyncProcessing(false);
  1615. // 保存结果到项目
  1616. try {
  1617. await bookService.updateBook(project.id, {
  1618. lip_sync_video_path: resultUrl,
  1619. // video_path: resultUrl, // 同时更新视频路径
  1620. lip_sync_task_id: taskInfo.task_id
  1621. });
  1622. toast.success('对口型视频已保存到项目');
  1623. // 重新加载项目详情
  1624. await loadProjectDetail(true);
  1625. } catch (error) {
  1626. console.error('保存对口型结果失败:', error);
  1627. toast.error(`保存失败: ${error.message}`);
  1628. }
  1629. },
  1630. // 错误回调
  1631. (error) => {
  1632. console.error('对口型处理失败:', error);
  1633. toast.error(`对口型处理失败: ${error.message}`);
  1634. setLipSyncProcessing(false);
  1635. }
  1636. );
  1637. } catch (error) {
  1638. console.error('处理对口型失败:', error);
  1639. toast.error(`处理失败: ${error.message}`);
  1640. setLipSyncProcessing(false);
  1641. }
  1642. };
  1643. /**
  1644. * 处理导出剪映草稿
  1645. */
  1646. const handleExportDraft = async () => {
  1647. if (!hasValidToken()) {
  1648. toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
  1649. return;
  1650. }
  1651. // 检查必要条件
  1652. if (!project.audio_path) {
  1653. toast.error('项目缺少音频文件,无法导出剪映草稿');
  1654. return;
  1655. }
  1656. setExportingDraft(true);
  1657. try {
  1658. // 整理分镜数据
  1659. const orderedSegments = [...segments].sort((a, b) => a.segment_id - b.segment_id);
  1660. // 检查分镜是否有必要的信息
  1661. const incompleteSegments = orderedSegments.filter(
  1662. segment => !segment.text || !segment.start_time || !segment.end_time
  1663. );
  1664. if (incompleteSegments.length > 0) {
  1665. toast.error(`有${incompleteSegments.length}个分镜缺少必要信息(文本、开始时间或结束时间),请完善后再导出`);
  1666. return;
  1667. }
  1668. // 构建文本列表 - 按照API要求格式化时间
  1669. const textList = orderedSegments.map((segment) => {
  1670. // 转换时间格式为微秒
  1671. const startTimeMs = convertTimeToMicroseconds(segment.start_time);
  1672. const endTimeMs = convertTimeToMicroseconds(segment.end_time);
  1673. return {
  1674. text: segment.text,
  1675. start_time: startTimeMs,
  1676. end_time: endTimeMs,
  1677. duration: endTimeMs - startTimeMs,
  1678. // 添加可选的图片路径
  1679. image_path: segment.image_path || ""
  1680. };
  1681. });
  1682. const cozeInstance = initCozeService({
  1683. timeout: 120000 // 设置120秒超时
  1684. });
  1685. // 准备所有必要参数
  1686. const params = {
  1687. // 必填参数
  1688. project_id: projectId,
  1689. text_list: textList,
  1690. // 添加音频链接参数
  1691. audio_url: project.audio_url || project.audio_path,
  1692. // 添加可选参数
  1693. video_url: project.lip_sync_video_path || project.video_path || "",
  1694. project_title: project.title || "项目" + projectId
  1695. };
  1696. console.log("导出剪映草稿参数:", JSON.stringify(params, null, 2));
  1697. // 调用导出API - 使用runWorkflow替代exportToDraft
  1698. const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, params);
  1699. if (response && response.data) {
  1700. try {
  1701. // 解析响应数据
  1702. const responseObj = JSON.parse(response.data);
  1703. console.log("导出剪映草稿响应:", responseObj);
  1704. // 检查状态码
  1705. if (response.code === 0) {
  1706. if (responseObj && responseObj.draft_url) {
  1707. const draftUrl = responseObj.draft_url;
  1708. // 保存草稿链接到数据库
  1709. await bookService.updateBook(parseInt(projectId), {
  1710. draft_url: draftUrl
  1711. });
  1712. // 更新本地状态
  1713. setProject(prev => ({
  1714. ...prev,
  1715. draft_url: draftUrl
  1716. }));
  1717. toast.success('剪映草稿导出成功');
  1718. // 刷新项目数据
  1719. await loadProjectDetail(true);
  1720. } else {
  1721. throw new Error('未找到有效的草稿链接');
  1722. }
  1723. } else {
  1724. // 导出失败
  1725. throw new Error(response.msg || '导出失败');
  1726. }
  1727. } catch (e) {
  1728. console.error('解析导出结果失败:', e);
  1729. throw new Error('解析导出结果失败: ' + e.message);
  1730. }
  1731. } else {
  1732. throw new Error('导出失败,未收到有效响应');
  1733. }
  1734. } catch (error) {
  1735. console.error('导出剪映草稿失败:', error);
  1736. toast.error(`导出失败: ${error.message}`);
  1737. } finally {
  1738. setExportingDraft(false);
  1739. }
  1740. };
  1741. // 辅助函数:将时间格式转换为微秒
  1742. const convertTimeToMicroseconds = (timeStr) => {
  1743. if (!timeStr) return 0;
  1744. // 处理时间格式 "HH:MM:SS,SSS"
  1745. const [time, milliseconds] = timeStr.split(',');
  1746. const [hours, minutes, seconds] = time.split(':').map(Number);
  1747. const ms = milliseconds ? parseInt(milliseconds) : 0;
  1748. // 转换为微秒
  1749. return (hours * 3600 + minutes * 60 + seconds) * 1000000 + ms * 1000;
  1750. };
  1751. if (loading && !silentLoading) {
  1752. return null; // 移除加载中的显示
  1753. }
  1754. return (
  1755. <div className="project-detail-page">
  1756. <Container fluid>
  1757. <div className="d-flex justify-content-between align-items-center project-header">
  1758. <div>
  1759. <Button
  1760. variant="outline-secondary"
  1761. onClick={backToProjectList}
  1762. className="me-2"
  1763. >
  1764. 返回列表
  1765. </Button>
  1766. </div>
  1767. <div className="d-flex">
  1768. {selectedSegments.length > 1 && (
  1769. <Button
  1770. variant="primary"
  1771. onClick={showMergeSegmentsModal}
  1772. className="me-2"
  1773. >
  1774. 合并所选分镜
  1775. </Button>
  1776. )}
  1777. </div>
  1778. </div>
  1779. {project ? (
  1780. <div className="project-detail-container mt-4">
  1781. {/* 项目资源和画风设置 */}
  1782. <Card className="mb-4">
  1783. <Card.Header>
  1784. <h5 className="mb-0">项目资源与画风设置</h5>
  1785. </Card.Header>
  1786. <Card.Body>
  1787. <Row className="align-items-center">
  1788. {/* 项目音频 */}
  1789. <Col md={6}>
  1790. <Form.Group>
  1791. <Form.Label>项目音频</Form.Label>
  1792. {projectAudioPath && projectAudioPath.trim() !== '' ? (
  1793. <div className="project-audio-container">
  1794. <AudioPlayerComponent
  1795. audioPath={projectAudioPath}
  1796. label="音频文件"
  1797. onPlay={() => console.log('项目音频开始播放')}
  1798. onPause={() => console.log('项目音频暂停播放')}
  1799. />
  1800. </div>
  1801. ) : (
  1802. <Alert variant="warning" className="mb-0">
  1803. <small>暂无项目音频</small>
  1804. </Alert>
  1805. )}
  1806. </Form.Group>
  1807. </Col>
  1808. {/* 项目视频 */}
  1809. <Col md={3}>
  1810. <Form.Group>
  1811. <Form.Label>项目视频</Form.Label>
  1812. {project.video_path && project.video_path.trim() !== '' ? (
  1813. <div className="project-video-container">
  1814. <video
  1815. className="w-100"
  1816. controls
  1817. src={project.video_path}
  1818. style={{ maxHeight: '150px' }}
  1819. >
  1820. 您的浏览器不支持视频播放
  1821. </video>
  1822. </div>
  1823. ) : (
  1824. <Alert variant="warning" className="mb-0">
  1825. <small>暂无项目视频</small>
  1826. </Alert>
  1827. )}
  1828. </Form.Group>
  1829. </Col>
  1830. {/* 对口型生成区域 */}
  1831. <Col md={3}>
  1832. <Form.Group>
  1833. <Form.Label>对口型视频</Form.Label>
  1834. {lipSyncProcessing ? (
  1835. <div className="lip-sync-progress">
  1836. <ProgressBar
  1837. animated
  1838. now={lipSyncProgress}
  1839. label={`${Math.round(lipSyncProgress)}%`}
  1840. className="mb-2"
  1841. />
  1842. <div className="text-center">
  1843. <small className="text-muted">正在生成对口型视频,请耐心等待...</small>
  1844. </div>
  1845. </div>
  1846. ) : project.lip_sync_video_path ? (
  1847. <div className="lip-sync-video-container">
  1848. <video
  1849. className="w-100"
  1850. controls
  1851. src={project.lip_sync_video_path}
  1852. style={{ maxHeight: '150px' }}
  1853. >
  1854. 您的浏览器不支持视频播放
  1855. </video>
  1856. </div>
  1857. ) : (
  1858. <Alert variant="info" className="mb-0">
  1859. <small>
  1860. 暂无对口型视频
  1861. {(project.audio_path && project.video_path) ?
  1862. <div className="mt-1">
  1863. <small>
  1864. 已上传音频和视频文件,可以点击"生成对口型"按钮开始处理
  1865. </small>
  1866. </div> :
  1867. <div className="mt-1">
  1868. <small>
  1869. 需要先上传音频和视频文件才能生成对口型
  1870. </small>
  1871. </div>
  1872. }
  1873. </small>
  1874. </Alert>
  1875. )}
  1876. </Form.Group>
  1877. <div className="w-100">
  1878. <Button
  1879. variant="primary"
  1880. className="w-100"
  1881. disabled={lipSyncProcessing || !project.audio_path || !project.video_path}
  1882. onClick={handleLipSyncGeneration}
  1883. >
  1884. {lipSyncProcessing ? (
  1885. <>
  1886. <Spinner
  1887. as="span"
  1888. animation="border"
  1889. size="sm"
  1890. role="status"
  1891. aria-hidden="true"
  1892. className="me-1"
  1893. />
  1894. 处理中...
  1895. </>
  1896. ) : project.lip_sync_video_path ? "重新生成对口型" : "生成对口型"}
  1897. </Button>
  1898. </div>
  1899. </Col>
  1900. {/* 对口型操作按钮 */}
  1901. {/* 项目画风选择 */}
  1902. <Col md={4}>
  1903. <Form.Group>
  1904. <Form.Label>项目画风</Form.Label>
  1905. <InputGroup>
  1906. <Form.Select
  1907. value={selectedStyle}
  1908. onChange={(e) => handleStyleSelect(e.target.value)}
  1909. disabled={loadingStyles || stylesList.length === 0}
  1910. >
  1911. {stylesList.length === 0 ? (
  1912. <option>无可用画风</option>
  1913. ) : (
  1914. <>
  1915. <option value="">请选择画风</option>
  1916. {stylesList.map((style, index) => (
  1917. <option key={index} value={style}>
  1918. {style}
  1919. </option>
  1920. ))}
  1921. </>
  1922. )}
  1923. </Form.Select>
  1924. <Button
  1925. variant="outline-secondary"
  1926. onClick={loadStylesList}
  1927. disabled={loadingStyles}
  1928. >
  1929. {loadingStyles ? '加载中...' : '刷新'}
  1930. </Button>
  1931. </InputGroup>
  1932. <Form.Text className="text-muted">
  1933. 选择的画风将应用于生成的图像中
  1934. </Form.Text>
  1935. </Form.Group>
  1936. </Col>
  1937. <Col md={3}>
  1938. <Form.Group>
  1939. <Form.Label>剪映草稿</Form.Label>
  1940. {project.draft_url ? (
  1941. <div className="border rounded p-2 bg-light">
  1942. <div className="text-truncate small" style={{ maxWidth: '100%' }}>
  1943. <a
  1944. href={project.draft_url}
  1945. target="_blank"
  1946. rel="noopener noreferrer"
  1947. title={project.draft_url}
  1948. >
  1949. {project.draft_url}
  1950. </a>
  1951. </div>
  1952. </div>
  1953. ) : (
  1954. <Alert variant="warning" className="mb-0">
  1955. <small>尚未导出剪映草稿</small>
  1956. </Alert>
  1957. )}
  1958. </Form.Group>
  1959. </Col>
  1960. </Row>
  1961. </Card.Body>
  1962. </Card>
  1963. <Card className="mt-4">
  1964. <Card.Header className="d-flex justify-content-between align-items-center">
  1965. <h5 className="mb-0">分镜列表</h5>
  1966. <div className="d-flex align-items-center">
  1967. <Button
  1968. variant="success"
  1969. size="sm"
  1970. onClick={generateAllDescriptions}
  1971. disabled={generatingDescriptions}
  1972. className="me-3"
  1973. >
  1974. {generatingDescriptions
  1975. ? `生成中 ${generationProgress.current}/${generationProgress.total}`
  1976. : (hasDescriptions ? '一键更新描述词' : '一键生成描述词')}
  1977. </Button>
  1978. {isPaused ? (
  1979. <div className="d-flex me-3">
  1980. <Button
  1981. variant="warning"
  1982. size="sm"
  1983. onClick={resumeImageGeneration}
  1984. className="me-1"
  1985. >
  1986. 继续绘画
  1987. </Button>
  1988. <Button
  1989. variant="danger"
  1990. size="sm"
  1991. onClick={cancelImageGeneration}
  1992. >
  1993. 取消绘画
  1994. </Button>
  1995. </div>
  1996. ) : (
  1997. <>
  1998. {generatingImages ? (
  1999. <div className="d-flex me-3">
  2000. <Button
  2001. variant="warning"
  2002. size="sm"
  2003. onClick={pauseImageGeneration}
  2004. >
  2005. 暂停绘画 {imageGenerationProgress.current}/{imageGenerationProgress.total}
  2006. </Button>
  2007. </div>
  2008. ) : (
  2009. <Dropdown className="me-3">
  2010. <Dropdown.Toggle
  2011. variant="info"
  2012. size="sm"
  2013. disabled={!selectedStyle}
  2014. >
  2015. 画图操作
  2016. </Dropdown.Toggle>
  2017. <Dropdown.Menu>
  2018. <Dropdown.Item
  2019. onClick={() => generateAllImages(false)}
  2020. disabled={!selectedStyle}
  2021. >
  2022. 开始绘画
  2023. </Dropdown.Item>
  2024. <Dropdown.Item
  2025. onClick={() => generateAllImages(true)}
  2026. disabled={!selectedStyle}
  2027. >
  2028. 一键重绘
  2029. </Dropdown.Item>
  2030. </Dropdown.Menu>
  2031. </Dropdown>
  2032. )}
  2033. </>
  2034. )}
  2035. <Button
  2036. variant="outline-primary"
  2037. size="sm"
  2038. onClick={() => setSelectedSegments([])}
  2039. className="me-2"
  2040. >
  2041. 清除选择
  2042. </Button>
  2043. <Button
  2044. variant="outline-success"
  2045. size="sm"
  2046. onClick={handleExportDraft}
  2047. disabled={exportingDraft || !project.audio_url || !project.lip_sync_video_path || !segments || segments.length === 0}
  2048. >
  2049. {exportingDraft ? (
  2050. <>
  2051. <Spinner
  2052. as="span"
  2053. animation="border"
  2054. size="sm"
  2055. role="status"
  2056. aria-hidden="true"
  2057. className="me-1"
  2058. />
  2059. 导出中...
  2060. </>
  2061. ) : (
  2062. '导出剪映草稿'
  2063. )}
  2064. </Button>
  2065. </div>
  2066. </Card.Header>
  2067. <Card.Body>
  2068. {segments.length > 0 ? (
  2069. <div className="segments-container">
  2070. <Table responsive striped hover className="segments-table">
  2071. <thead>
  2072. <tr>
  2073. <th width="5%">选择</th>
  2074. <th width="5%">ID</th>
  2075. <th width="25%">文本内容</th>
  2076. <th width="25%">描述词</th>
  2077. <th width="12%">图片</th>
  2078. <th width="10%">操作</th>
  2079. </tr>
  2080. </thead>
  2081. <tbody>
  2082. {segments.map((segment) => (
  2083. <tr key={segment.id} className={selectedSegments.includes(segment.id) ? 'selected' : ''}>
  2084. <td>
  2085. <Form.Check
  2086. type="checkbox"
  2087. checked={selectedSegments.includes(segment.id)}
  2088. onChange={() => toggleSegmentSelection(segment.id)}
  2089. />
  2090. </td>
  2091. <td>
  2092. <Badge bg="primary">{segment.id}</Badge>
  2093. {segment.is_merged && (
  2094. <Badge bg="info" className="ms-1">合并</Badge>
  2095. )}
  2096. </td>
  2097. <td className="segment-text-cell">
  2098. {editingSegmentId === segment.id && editingTextField === 'text' ? (
  2099. <div className="segment-edit-container">
  2100. <Form.Control
  2101. as="textarea"
  2102. rows={3}
  2103. value={descriptionText}
  2104. onChange={(e) => setDescriptionText(e.target.value)}
  2105. className="mb-2"
  2106. />
  2107. <div className="d-flex justify-content-end">
  2108. <Button
  2109. variant="outline-secondary"
  2110. size="sm"
  2111. onClick={cancelDirectEdit}
  2112. className="me-2"
  2113. >
  2114. 取消
  2115. </Button>
  2116. <Button
  2117. variant="primary"
  2118. size="sm"
  2119. onClick={saveDirectEdit}
  2120. >
  2121. 保存
  2122. </Button>
  2123. </div>
  2124. </div>
  2125. ) : (
  2126. <div className="segment-content editable-content" onClick={() => handleDirectEditText(segment.id, segment.text)}>
  2127. <div className="segment-times">
  2128. <small className="text-muted">
  2129. {formatTime(segment.start_time)} → {formatTime(segment.end_time)}
  2130. ({segment.duration}秒)
  2131. </small>
  2132. </div>
  2133. <div className="segment-text">{segment.text || <span className="text-muted">点击添加文本内容</span>}</div>
  2134. </div>
  2135. )}
  2136. </td>
  2137. <td className="segment-description-cell">
  2138. {editingSegmentId === segment.id && editingTextField === 'description' ? (
  2139. <div className="segment-edit-container">
  2140. <Form.Control
  2141. as="textarea"
  2142. rows={3}
  2143. value={descriptionText}
  2144. onChange={(e) => setDescriptionText(e.target.value)}
  2145. className="mb-2"
  2146. />
  2147. <div className="d-flex justify-content-end">
  2148. <Button
  2149. variant="outline-secondary"
  2150. size="sm"
  2151. onClick={cancelDirectEdit}
  2152. className="me-2"
  2153. >
  2154. 取消
  2155. </Button>
  2156. <Button
  2157. variant="primary"
  2158. size="sm"
  2159. onClick={saveDirectEdit}
  2160. >
  2161. 保存
  2162. </Button>
  2163. </div>
  2164. </div>
  2165. ) : (
  2166. <div
  2167. className={`segment-description editable-content ${segment.id === descriptionSegmentId && generatingDescriptions ? 'generating' : ''}`}
  2168. onClick={() => handleDirectEditDescription(segment.id, segment.description)}
  2169. >
  2170. {segment.description || <span className="text-muted">点击添加描述词</span>}
  2171. </div>
  2172. )}
  2173. </td>
  2174. <td>
  2175. {segment.image_path ? (
  2176. <div className="media-cell">
  2177. <div className="image-thumbnail mb-2">
  2178. <img
  2179. src={segment.image_path}
  2180. alt="分镜图片"
  2181. onClick={() => showImagePreview(segment.image_path)}
  2182. style={{ width: '100%', maxWidth: '80px', height: 'auto', cursor: 'pointer' }}
  2183. />
  2184. </div>
  2185. <div className="d-flex flex-wrap justify-content-center">
  2186. <OverlayTrigger
  2187. placement="top"
  2188. overlay={<Tooltip>查看大图</Tooltip>}
  2189. >
  2190. <Button
  2191. variant="outline-success"
  2192. size="sm"
  2193. onClick={() => showImagePreview(segment.image_path)}
  2194. className="me-1 mb-1"
  2195. >
  2196. 查看
  2197. </Button>
  2198. </OverlayTrigger>
  2199. <OverlayTrigger
  2200. placement="top"
  2201. overlay={<Tooltip>重新上传</Tooltip>}
  2202. >
  2203. <Button
  2204. variant="outline-secondary"
  2205. size="sm"
  2206. onClick={() => showUploadModalFor(segment.id, 'image')}
  2207. className="me-1 mb-1"
  2208. >
  2209. 更新
  2210. </Button>
  2211. </OverlayTrigger>
  2212. <OverlayTrigger
  2213. placement="top"
  2214. overlay={<Tooltip>重新生成图片</Tooltip>}
  2215. >
  2216. <Button
  2217. variant="outline-info"
  2218. size="sm"
  2219. onClick={() => generateSingleImage(segment, true)}
  2220. disabled={generatingImages || !selectedStyle || !segment.description || segment.description.trim() === ''}
  2221. className="me-1 mb-1"
  2222. >
  2223. 重绘
  2224. </Button>
  2225. </OverlayTrigger>
  2226. </div>
  2227. </div>
  2228. ) : (
  2229. <div className="media-cell">
  2230. <Button
  2231. variant="outline-secondary"
  2232. size="sm"
  2233. onClick={() => showUploadModalFor(segment.id, 'image')}
  2234. className="mb-2"
  2235. >
  2236. 上传
  2237. </Button>
  2238. {segment.description && segment.description.trim() !== '' && (
  2239. <Button
  2240. variant="outline-info"
  2241. size="sm"
  2242. onClick={() => generateSingleImage(segment, false)}
  2243. disabled={generatingImages || !selectedStyle}
  2244. >
  2245. 生成图片
  2246. </Button>
  2247. )}
  2248. </div>
  2249. )}
  2250. </td>
  2251. <td>
  2252. <Dropdown>
  2253. <Dropdown.Toggle variant="outline-secondary" size="sm" id={`dropdown-${segment.id}`}>
  2254. 操作
  2255. </Dropdown.Toggle>
  2256. <Dropdown.Menu>
  2257. <Dropdown.Item onClick={() => showSplitSegmentModal(segment)}>拆分</Dropdown.Item>
  2258. <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'audio')}>上传音频</Dropdown.Item>
  2259. <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'image')}>上传图片</Dropdown.Item>
  2260. <Dropdown.Item onClick={() => showUploadModalFor(segment.id, 'video')}>上传口播视频</Dropdown.Item>
  2261. <Dropdown.Item
  2262. onClick={() => generateSingleDescription(segment)}
  2263. disabled={generatingDescriptions}
  2264. >
  2265. {segment.description && segment.description.trim() !== '' ? '重新生成描述词' : '生成描述词'}
  2266. </Dropdown.Item>
  2267. <Dropdown.Item
  2268. onClick={() => generateSingleImage(segment, segment.image_path ? true : false)}
  2269. disabled={generatingImages || !selectedStyle || !segment.description || segment.description.trim() === ''}
  2270. >
  2271. {segment.image_path ? '重新生成图片' : '生成图片'}
  2272. </Dropdown.Item>
  2273. <Dropdown.Divider />
  2274. <Dropdown.Item
  2275. className="text-danger"
  2276. onClick={() => deleteSegment(segment.id)}
  2277. >
  2278. 删除分镜
  2279. </Dropdown.Item>
  2280. </Dropdown.Menu>
  2281. </Dropdown>
  2282. </td>
  2283. </tr>
  2284. ))}
  2285. </tbody>
  2286. </Table>
  2287. </div>
  2288. ) : (
  2289. <p className="text-center text-muted">没有分镜数据</p>
  2290. )}
  2291. </Card.Body>
  2292. </Card>
  2293. </div>
  2294. ) : (
  2295. <div className="text-center my-5">
  2296. {loading ? (
  2297. <Spinner animation="border" />
  2298. ) : (
  2299. <Alert variant="warning">项目不存在或已被删除</Alert>
  2300. )}
  2301. </div>
  2302. )}
  2303. </Container>
  2304. {/* 合并分镜对话框 */}
  2305. <Modal show={showMergeModal} onHide={() => setShowMergeModal(false)}>
  2306. <Modal.Header closeButton>
  2307. <Modal.Title>合并分镜</Modal.Title>
  2308. </Modal.Header>
  2309. <Modal.Body>
  2310. <p>确定要合并以下分镜吗?</p>
  2311. <ul>
  2312. {selectedSegments.map(id => {
  2313. const segment = segments.find(s => s.id === id);
  2314. return segment ? (
  2315. <li key={id}>分镜 {id}: {segment.text ? segment.text.substring(0, 30) + (segment.text.length > 30 ? '...' : '') : '无文本'}</li>
  2316. ) : null;
  2317. })}
  2318. </ul>
  2319. </Modal.Body>
  2320. <Modal.Footer>
  2321. <Button variant="secondary" onClick={() => setShowMergeModal(false)}>取消</Button>
  2322. <Button variant="primary" onClick={mergeSegments} disabled={loading}>
  2323. {loading ? '处理中...' : '确定合并'}
  2324. </Button>
  2325. </Modal.Footer>
  2326. </Modal>
  2327. {/* 拆分分镜对话框 */}
  2328. <Modal show={showSplitModal} onHide={() => setShowSplitModal(false)} size="lg">
  2329. <Modal.Header closeButton>
  2330. <Modal.Title>拆分分镜</Modal.Title>
  2331. </Modal.Header>
  2332. <Modal.Body>
  2333. <h6>原分镜内容:</h6>
  2334. <p className="mb-3 p-2 bg-light">
  2335. {splitSegment?.text}
  2336. </p>
  2337. <h6>拆分为:</h6>
  2338. {newSegments.map((segment, index) => (
  2339. <div key={index} className="mb-3 p-3 border rounded">
  2340. <div className="d-flex justify-content-between mb-2">
  2341. <h6>新分镜 #{index + 1}</h6>
  2342. <Button
  2343. variant="outline-danger"
  2344. size="sm"
  2345. onClick={() => removeSegmentForm(index)}
  2346. disabled={newSegments.length <= 1}
  2347. >
  2348. 删除
  2349. </Button>
  2350. </div>
  2351. <Form.Group className="mb-2">
  2352. <Form.Label>文本内容</Form.Label>
  2353. <Form.Control
  2354. as="textarea"
  2355. rows={3}
  2356. value={segment.text}
  2357. onChange={(e) => updateNewSegmentData(index, 'text', e.target.value)}
  2358. />
  2359. </Form.Group>
  2360. <Row>
  2361. <Col>
  2362. <Form.Group>
  2363. <Form.Label>开始时间 (HH:MM:SS,SSS)</Form.Label>
  2364. <Form.Control
  2365. type="text"
  2366. value={segment.start_time}
  2367. onChange={(e) => updateNewSegmentData(index, 'start_time', e.target.value)}
  2368. placeholder="00:00:00,000"
  2369. />
  2370. </Form.Group>
  2371. </Col>
  2372. <Col>
  2373. <Form.Group>
  2374. <Form.Label>结束时间 (HH:MM:SS,SSS)</Form.Label>
  2375. <Form.Control
  2376. type="text"
  2377. value={segment.end_time}
  2378. onChange={(e) => updateNewSegmentData(index, 'end_time', e.target.value)}
  2379. placeholder="00:00:00,000"
  2380. />
  2381. </Form.Group>
  2382. </Col>
  2383. </Row>
  2384. </div>
  2385. ))}
  2386. <Button
  2387. variant="outline-primary"
  2388. onClick={addNewSegmentForm}
  2389. className="mt-2"
  2390. >
  2391. 添加新分镜
  2392. </Button>
  2393. </Modal.Body>
  2394. <Modal.Footer>
  2395. <Button variant="secondary" onClick={() => setShowSplitModal(false)}>取消</Button>
  2396. <Button variant="primary" onClick={splitSegmentSubmit} disabled={loading}>
  2397. {loading ? '处理中...' : '确定拆分'}
  2398. </Button>
  2399. </Modal.Footer>
  2400. </Modal>
  2401. {/* 上传文件对话框 */}
  2402. <Modal show={showUploadModal} onHide={() => setShowUploadModal(false)}>
  2403. <Modal.Header closeButton>
  2404. <Modal.Title>
  2405. {(() => {
  2406. const segment = segments.find(s => s.id === uploadSegmentId);
  2407. const isUpdate = segment && (
  2408. (uploadType === 'audio' && segment.audio_path) ||
  2409. (uploadType === 'image' && segment.image_path) ||
  2410. (uploadType === 'video' && segment.video_path)
  2411. );
  2412. if (isUpdate) {
  2413. return uploadType === 'audio' ? '更新音频' :
  2414. uploadType === 'image' ? '更新图片' : '更新口播视频';
  2415. } else {
  2416. return uploadType === 'audio' ? '上传音频' :
  2417. uploadType === 'image' ? '上传图片' : '上传口播视频';
  2418. }
  2419. })()}
  2420. </Modal.Title>
  2421. </Modal.Header>
  2422. <Modal.Body>
  2423. {(() => {
  2424. const segment = segments.find(s => s.id === uploadSegmentId);
  2425. const existingPath = segment && (
  2426. uploadType === 'audio' ? segment.audio_path :
  2427. uploadType === 'image' ? segment.image_path : segment.video_path
  2428. );
  2429. return (
  2430. <>
  2431. {existingPath && (
  2432. <div className="mb-3">
  2433. <div className="mb-2">当前{uploadType === 'audio' ? '音频' : uploadType === 'image' ? '图片' : '视频'}:</div>
  2434. {uploadType === 'image' && (
  2435. <div className="text-center mb-3">
  2436. <img
  2437. src={existingPath}
  2438. alt="当前图片"
  2439. style={{ maxWidth: '100%', maxHeight: '200px' }}
  2440. className="border rounded"
  2441. />
  2442. </div>
  2443. )}
  2444. {(uploadType === 'audio' || uploadType === 'video') && (
  2445. <div className="text-center text-muted mb-3">
  2446. <small>{existingPath}</small>
  2447. </div>
  2448. )}
  2449. </div>
  2450. )}
  2451. <Form.Group className="mb-3">
  2452. <Form.Label>
  2453. 选择{uploadType === 'audio' ? '音频文件' :
  2454. uploadType === 'image' ? '图片文件' : '视频文件'}
  2455. </Form.Label>
  2456. <Form.Control
  2457. type="file"
  2458. ref={fileInputRef}
  2459. onChange={handleFileChange}
  2460. accept={
  2461. uploadType === 'audio' ? 'audio/*' :
  2462. uploadType === 'image' ? 'image/*' : 'video/*'
  2463. }
  2464. />
  2465. <Form.Text className="text-muted">
  2466. {uploadType === 'audio' ? '支持的格式: MP3, WAV, OGG' :
  2467. uploadType === 'image' ? '支持的格式: JPG, PNG, GIF' : '支持的格式: MP4, WebM, AVI'}
  2468. </Form.Text>
  2469. </Form.Group>
  2470. </>
  2471. );
  2472. })()}
  2473. </Modal.Body>
  2474. <Modal.Footer>
  2475. <Button variant="secondary" onClick={() => setShowUploadModal(false)}>取消</Button>
  2476. <Button
  2477. variant="primary"
  2478. onClick={handleFileUpload}
  2479. disabled={isUploading || !selectedFile}
  2480. >
  2481. {isUploading ? (
  2482. <>
  2483. <Spinner
  2484. as="span"
  2485. animation="border"
  2486. size="sm"
  2487. role="status"
  2488. aria-hidden="true"
  2489. className="me-1"
  2490. />
  2491. 上传中...
  2492. </>
  2493. ) : '上传'}
  2494. </Button>
  2495. </Modal.Footer>
  2496. </Modal>
  2497. {/* 描述词对话框 */}
  2498. <Modal show={showDescriptionModal} onHide={() => setShowDescriptionModal(false)}>
  2499. <Modal.Header closeButton>
  2500. <Modal.Title>编辑描述词</Modal.Title>
  2501. </Modal.Header>
  2502. <Modal.Body>
  2503. <Form.Group>
  2504. <Form.Label>描述词内容</Form.Label>
  2505. <Form.Control
  2506. as="textarea"
  2507. rows={5}
  2508. value={descriptionText}
  2509. onChange={(e) => setDescriptionText(e.target.value)}
  2510. placeholder="请输入描述词..."
  2511. />
  2512. </Form.Group>
  2513. </Modal.Body>
  2514. <Modal.Footer>
  2515. <Button variant="secondary" onClick={() => setShowDescriptionModal(false)}>取消</Button>
  2516. <Button variant="primary" onClick={() => saveDescription(descriptionSegmentId, descriptionText)} disabled={loading}>
  2517. {loading ? '保存中...' : '保存'}
  2518. </Button>
  2519. </Modal.Footer>
  2520. </Modal>
  2521. {/* 删除项目确认对话框 */}
  2522. <Modal show={showDeleteConfirm} onHide={() => setShowDeleteConfirm(false)}>
  2523. <Modal.Header closeButton>
  2524. <Modal.Title>确认删除项目</Modal.Title>
  2525. </Modal.Header>
  2526. <Modal.Body>
  2527. <p>您确定要删除此项目吗?此操作将同时删除所有相关的分镜数据,且不可恢复!</p>
  2528. {project && <p className="text-danger fw-bold">项目名称: {project.title}</p>}
  2529. </Modal.Body>
  2530. <Modal.Footer>
  2531. <Button variant="secondary" onClick={() => setShowDeleteConfirm(false)}>取消</Button>
  2532. <Button variant="danger" onClick={deleteProject} disabled={loading}>
  2533. {loading ? '删除中...' : '确定删除'}
  2534. </Button>
  2535. </Modal.Footer>
  2536. </Modal>
  2537. {/* 图片预览对话框 */}
  2538. <Modal show={showImagePreviewModal} onHide={() => setShowImagePreviewModal(false)}>
  2539. <Modal.Header closeButton>
  2540. <Modal.Title>图片预览</Modal.Title>
  2541. </Modal.Header>
  2542. <Modal.Body>
  2543. <img src={previewImageUrl} alt="图片预览" style={{ width: '100%', height: 'auto' }} />
  2544. </Modal.Body>
  2545. <Modal.Footer>
  2546. <Button variant="secondary" onClick={() => setShowImagePreviewModal(false)}>关闭</Button>
  2547. </Modal.Footer>
  2548. </Modal>
  2549. {/* 上传项目音频对话框 */}
  2550. <Modal show={showUploadProjectAudioModal} onHide={() => setShowUploadProjectAudioModal(false)}>
  2551. <Modal.Header closeButton>
  2552. <Modal.Title>
  2553. {projectAudioPath ? '更新项目音频' : '上传项目音频'}
  2554. </Modal.Title>
  2555. </Modal.Header>
  2556. <Modal.Body>
  2557. {projectAudioPath && (
  2558. <div className="mb-3">
  2559. <div className="mb-2">当前项目音频:</div>
  2560. <div className="text-center text-muted mb-3">
  2561. <small>{projectAudioPath}</small>
  2562. </div>
  2563. </div>
  2564. )}
  2565. <div className="mb-3">
  2566. <Form.Group>
  2567. <Form.Label>选择音频文件</Form.Label>
  2568. <Form.Control
  2569. type="file"
  2570. accept="audio/*"
  2571. onChange={handleProjectAudioFileChange}
  2572. />
  2573. <Form.Text className="text-muted">
  2574. 支持的格式: MP3, WAV, OGG等常见音频格式
  2575. </Form.Text>
  2576. </Form.Group>
  2577. </div>
  2578. </Modal.Body>
  2579. <Modal.Footer>
  2580. <Button variant="secondary" onClick={() => setShowUploadProjectAudioModal(false)}>取消</Button>
  2581. <Button
  2582. variant="primary"
  2583. onClick={uploadProjectAudio}
  2584. disabled={uploadingProjectAudio || !projectAudioFile}
  2585. >
  2586. {uploadingProjectAudio ? '上传中...' : '上传'}
  2587. </Button>
  2588. </Modal.Footer>
  2589. </Modal>
  2590. </div>
  2591. );
  2592. };
  2593. export default ProjectDetail;