projectDetail.js 111 KB

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