|
@@ -72,6 +72,9 @@ const ProjectDetail = () => {
|
72
|
72
|
// 添加一个标志位来防止重复调用
|
73
|
73
|
const [isGenerating, setIsGenerating] = useState(false);
|
74
|
74
|
|
|
75
|
+ // 添加一个状态变量防止重复检查
|
|
76
|
+ const [isCheckingGeneration, setIsCheckingGeneration] = useState(false);
|
|
77
|
+
|
75
|
78
|
const fileInputRef = useRef(null);
|
76
|
79
|
|
77
|
80
|
// 辅助函数:根据ID获取单个分镜信息
|
|
@@ -721,7 +724,14 @@ const ProjectDetail = () => {
|
721
|
724
|
|
722
|
725
|
// 检查是否有正在进行的绘图任务
|
723
|
726
|
const checkOngoingImageGeneration = async () => {
|
|
727
|
+ // 防止重复检查
|
|
728
|
+ if (isCheckingGeneration || isGenerating) {
|
|
729
|
+ console.log('已经在检查或正在生成图片中,跳过重复检查');
|
|
730
|
+ return null;
|
|
731
|
+ }
|
|
732
|
+
|
724
|
733
|
try {
|
|
734
|
+ setIsCheckingGeneration(true);
|
725
|
735
|
console.log('检查是否有正在进行的绘图任务');
|
726
|
736
|
|
727
|
737
|
// 从数据库获取所有分镜
|
|
@@ -796,8 +806,8 @@ const ProjectDetail = () => {
|
796
|
806
|
});
|
797
|
807
|
|
798
|
808
|
// 检查是否已经在生成图片
|
799
|
|
- if (!generatingImages) {
|
800
|
|
- // 启动绘图任务
|
|
809
|
+ if (!generatingImages && !isGenerating) {
|
|
810
|
+ // 启动绘图任务,使用setTimeout避免可能的状态更新问题
|
801
|
811
|
setTimeout(() => {
|
802
|
812
|
generateAllImages(false);
|
803
|
813
|
}, 1000);
|
|
@@ -812,6 +822,8 @@ const ProjectDetail = () => {
|
812
|
822
|
localStorage.removeItem(`project_${projectId}_image_generation`);
|
813
|
823
|
setGeneratingImages(false);
|
814
|
824
|
setIsPaused(false);
|
|
825
|
+ } finally {
|
|
826
|
+ setIsCheckingGeneration(false);
|
815
|
827
|
}
|
816
|
828
|
|
817
|
829
|
return null;
|
|
@@ -1097,9 +1109,10 @@ const ProjectDetail = () => {
|
1097
|
1109
|
// 检查最终状态
|
1098
|
1110
|
const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
|
1099
|
1111
|
const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
|
|
1112
|
+ const anyPending = finalSegments.some(segment => segment.draw_status === 0 || segment.draw_status === 1);
|
1100
|
1113
|
|
1101
|
|
- if (!stillPaused) {
|
1102
|
|
- // 只有在非暂停状态下才清除生成状态
|
|
1114
|
+ if (!stillPaused && !anyPending) {
|
|
1115
|
+ // 只有在非暂停状态并且没有待处理的分镜时才清除生成状态
|
1103
|
1116
|
setGeneratingImages(false);
|
1104
|
1117
|
setIsPaused(false);
|
1105
|
1118
|
localStorage.removeItem(`project_${projectId}_image_generation`);
|
|
@@ -1109,11 +1122,13 @@ const ProjectDetail = () => {
|
1109
|
1122
|
} else {
|
1110
|
1123
|
toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
|
1111
|
1124
|
}
|
1112
|
|
- } else {
|
|
1125
|
+ } else if (stillPaused) {
|
1113
|
1126
|
toast.info('图片生成已暂停,可以稍后继续');
|
|
1127
|
+ } else if (anyPending) {
|
|
1128
|
+ console.log('还有待处理的分镜,等待下一轮处理');
|
1114
|
1129
|
}
|
1115
|
1130
|
|
1116
|
|
- // 刷新数据
|
|
1131
|
+ // 刷新数据,不触发检查
|
1117
|
1132
|
await loadProjectDetail(true);
|
1118
|
1133
|
} else {
|
1119
|
1134
|
// 继续绘画: 将没有图片的、有描述词的分镜设置为排队状态(0)
|
|
@@ -1365,9 +1380,10 @@ const ProjectDetail = () => {
|
1365
|
1380
|
// 检查最终状态
|
1366
|
1381
|
const finalSegments = await bookInfoService.getBookInfoByBookId(parseInt(projectId));
|
1367
|
1382
|
const stillPaused = finalSegments.some(segment => segment.draw_status === -1);
|
|
1383
|
+ const anyPending = finalSegments.some(segment => segment.draw_status === 0 || segment.draw_status === 1);
|
1368
|
1384
|
|
1369
|
|
- if (!stillPaused) {
|
1370
|
|
- // 只有在非暂停状态下才清除生成状态
|
|
1385
|
+ if (!stillPaused && !anyPending) {
|
|
1386
|
+ // 只有在非暂停状态并且没有待处理的分镜时才清除生成状态
|
1371
|
1387
|
setGeneratingImages(false);
|
1372
|
1388
|
setIsPaused(false);
|
1373
|
1389
|
localStorage.removeItem(`project_${projectId}_image_generation`);
|
|
@@ -1377,11 +1393,13 @@ const ProjectDetail = () => {
|
1377
|
1393
|
} else {
|
1378
|
1394
|
toast.warning(`图片生成结束,成功${successCount}张,失败${errorCount}张`);
|
1379
|
1395
|
}
|
1380
|
|
- } else {
|
|
1396
|
+ } else if (stillPaused) {
|
1381
|
1397
|
toast.info('图片生成已暂停,可以稍后继续');
|
|
1398
|
+ } else if (anyPending) {
|
|
1399
|
+ console.log('还有待处理的分镜,等待下一轮处理');
|
1382
|
1400
|
}
|
1383
|
1401
|
|
1384
|
|
- // 刷新数据
|
|
1402
|
+ // 刷新数据,不触发检查
|
1385
|
1403
|
await loadProjectDetail(true);
|
1386
|
1404
|
}
|
1387
|
1405
|
} finally {
|
|
@@ -2086,68 +2104,105 @@ const ProjectDetail = () => {
|
2086
|
2104
|
* 处理导出剪映草稿
|
2087
|
2105
|
*/
|
2088
|
2106
|
const handleExportDraft = async () => {
|
2089
|
|
- try {
|
2090
|
|
- // 检查是否有音频和视频
|
2091
|
|
- if (!project.audio_url || !project.lip_sync_video_path) {
|
2092
|
|
- toast.error('请先完成对口型视频生成');
|
2093
|
|
- return;
|
2094
|
|
- }
|
|
2107
|
+ if (!hasValidToken()) {
|
|
2108
|
+ toast.error('未配置Coze API Token,请先在首页点击"配置Coze API"按钮进行设置');
|
|
2109
|
+ return;
|
|
2110
|
+ }
|
2095
|
2111
|
|
2096
|
|
- // 检查是否有分镜数据
|
2097
|
|
- if (!segments || segments.length === 0) {
|
2098
|
|
- toast.error('没有可导出的分镜数据');
|
2099
|
|
- return;
|
2100
|
|
- }
|
|
2112
|
+ // 检查必要条件
|
|
2113
|
+ if (!project.audio_path) {
|
|
2114
|
+ toast.error('项目缺少音频文件,无法导出剪映草稿');
|
|
2115
|
+ return;
|
|
2116
|
+ }
|
2101
|
2117
|
|
2102
|
|
- // 检查所有分镜是否都有必要的字段
|
2103
|
|
- const invalidSegments = segments.filter(segment =>
|
2104
|
|
- !segment.start_time ||
|
2105
|
|
- !segment.end_time ||
|
2106
|
|
- !segment.text ||
|
2107
|
|
- !segment.image_path
|
2108
|
|
- );
|
|
2118
|
+ setExportingDraft(true);
|
|
2119
|
+ try {
|
|
2120
|
+ // 整理分镜数据
|
|
2121
|
+ const orderedSegments = [...segments].sort((a, b) => a.segment_id - b.segment_id);
|
2109
|
2122
|
|
2110
|
|
- if (invalidSegments.length > 0) {
|
2111
|
|
- toast.error(`有${invalidSegments.length}个分镜缺少必要信息,请完善后再导出`);
|
|
2123
|
+ // 检查分镜是否有必要的信息
|
|
2124
|
+ const incompleteSegments = orderedSegments.filter(
|
|
2125
|
+ segment => !segment.text || !segment.start_time || !segment.end_time
|
|
2126
|
+ );
|
|
2127
|
+
|
|
2128
|
+ if (incompleteSegments.length > 0) {
|
|
2129
|
+ toast.error(`有${incompleteSegments.length}个分镜缺少必要信息(文本、开始时间或结束时间),请完善后再导出`);
|
2112
|
2130
|
return;
|
2113
|
2131
|
}
|
2114
|
2132
|
|
2115
|
|
- setExportingDraft(true);
|
2116
|
|
-
|
2117
|
|
- // 转换时间格式为微秒
|
2118
|
|
- const textList = segments.map(segment => {
|
2119
|
|
- const startTime = convertTimeToMicroseconds(segment.start_time);
|
2120
|
|
- const endTime = convertTimeToMicroseconds(segment.end_time);
|
2121
|
|
- const duration = endTime - startTime;
|
2122
|
|
-
|
|
2133
|
+ // 构建文本列表 - 按照API要求格式化时间
|
|
2134
|
+ const textList = orderedSegments.map((segment) => {
|
|
2135
|
+ // 转换时间格式为微秒
|
|
2136
|
+ const startTimeMs = convertTimeToMicroseconds(segment.start_time);
|
|
2137
|
+ const endTimeMs = convertTimeToMicroseconds(segment.end_time);
|
|
2138
|
+
|
2123
|
2139
|
return {
|
2124
|
|
- start_time: startTime,
|
2125
|
|
- end_time: endTime,
|
2126
|
|
- duration: duration,
|
2127
|
2140
|
text: segment.text,
|
2128
|
|
- image_path: segment.image_path
|
|
2141
|
+ start_time: startTimeMs,
|
|
2142
|
+ end_time: endTimeMs,
|
|
2143
|
+ duration: endTimeMs - startTimeMs,
|
|
2144
|
+ // 添加可选的图片路径
|
|
2145
|
+ image_path: segment.image_path || ""
|
2129
|
2146
|
};
|
2130
|
2147
|
});
|
2131
|
2148
|
|
2132
|
|
- // 调用工作流API
|
2133
|
|
- const cozeInstance = initCozeService();
|
2134
|
|
- const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, {
|
2135
|
|
- audio_url: project.audio_url,
|
2136
|
|
- video_url: project.lip_sync_video_path,
|
2137
|
|
- text_list: textList
|
|
2149
|
+ const cozeInstance = initCozeService({
|
|
2150
|
+ timeout: 120000 // 设置120秒超时
|
2138
|
2151
|
});
|
2139
|
2152
|
|
|
2153
|
+ // 准备所有必要参数
|
|
2154
|
+ const params = {
|
|
2155
|
+ // 必填参数
|
|
2156
|
+ project_id: projectId,
|
|
2157
|
+ text_list: textList,
|
|
2158
|
+ // 添加音频链接参数
|
|
2159
|
+ audio_url: project.audio_url || project.audio_path,
|
|
2160
|
+ // 添加可选参数
|
|
2161
|
+ video_url: project.lip_sync_video_path || project.video_path || "",
|
|
2162
|
+ project_title: project.title || "项目" + projectId
|
|
2163
|
+ };
|
|
2164
|
+
|
|
2165
|
+ console.log("导出剪映草稿参数:", JSON.stringify(params, null, 2));
|
|
2166
|
+
|
|
2167
|
+ // 调用导出API - 使用runWorkflow替代exportToDraft
|
|
2168
|
+ const response = await cozeInstance.runWorkflow(WORKFLOW_IDS.exportJianyingDraft, params);
|
|
2169
|
+
|
2140
|
2170
|
if (response && response.data) {
|
2141
|
2171
|
try {
|
2142
|
|
- const result = JSON.parse(response.data);
|
2143
|
|
- if (result.output) {
|
2144
|
|
- toast.success('剪映草稿导出成功');
|
2145
|
|
- // 这里可以添加下载草稿文件的逻辑
|
|
2172
|
+ // 解析响应数据
|
|
2173
|
+ const responseObj = JSON.parse(response.data);
|
|
2174
|
+ console.log("导出剪映草稿响应:", responseObj);
|
|
2175
|
+
|
|
2176
|
+ // 检查状态码
|
|
2177
|
+ if (response.code === 0) {
|
|
2178
|
+ if (responseObj && responseObj.draft_url) {
|
|
2179
|
+ const draftUrl = responseObj.draft_url;
|
|
2180
|
+
|
|
2181
|
+ // 保存草稿链接到数据库
|
|
2182
|
+ await bookService.updateBook(parseInt(projectId), {
|
|
2183
|
+ draft_url: draftUrl
|
|
2184
|
+ });
|
|
2185
|
+
|
|
2186
|
+ // 更新本地状态
|
|
2187
|
+ setProject(prev => ({
|
|
2188
|
+ ...prev,
|
|
2189
|
+ draft_url: draftUrl
|
|
2190
|
+ }));
|
|
2191
|
+
|
|
2192
|
+ toast.success('剪映草稿导出成功');
|
|
2193
|
+
|
|
2194
|
+ // 刷新项目数据
|
|
2195
|
+ await loadProjectDetail(true);
|
|
2196
|
+ } else {
|
|
2197
|
+ throw new Error('未找到有效的草稿链接');
|
|
2198
|
+ }
|
2146
|
2199
|
} else {
|
2147
|
|
- throw new Error('导出结果格式不正确');
|
|
2200
|
+ // 导出失败
|
|
2201
|
+ throw new Error(response.msg || '导出失败');
|
2148
|
2202
|
}
|
2149
|
2203
|
} catch (e) {
|
2150
|
|
- throw new Error('解析导出结果失败');
|
|
2204
|
+ console.error('解析导出结果失败:', e);
|
|
2205
|
+ throw new Error('解析导出结果失败: ' + e.message);
|
2151
|
2206
|
}
|
2152
|
2207
|
} else {
|
2153
|
2208
|
throw new Error('导出失败,未收到有效响应');
|
|
@@ -2702,6 +2757,122 @@ const ProjectDetail = () => {
|
2702
|
2757
|
)}
|
2703
|
2758
|
</Card.Body>
|
2704
|
2759
|
</Card>
|
|
2760
|
+
|
|
2761
|
+ <Row className="mt-3">
|
|
2762
|
+ {/* 项目媒体文件区域 */}
|
|
2763
|
+ <Col md={12}>
|
|
2764
|
+ <Card className="mb-3">
|
|
2765
|
+ <Card.Header>
|
|
2766
|
+ <div className="d-flex justify-content-between align-items-center">
|
|
2767
|
+ <h5 className="mb-0">项目媒体</h5>
|
|
2768
|
+ <div>
|
|
2769
|
+ {project.draft_url && (
|
|
2770
|
+ <Button
|
|
2771
|
+ variant="outline-success"
|
|
2772
|
+ size="sm"
|
|
2773
|
+ className="ms-2"
|
|
2774
|
+ onClick={() => {
|
|
2775
|
+ navigator.clipboard.writeText(project.draft_url);
|
|
2776
|
+ toast.success('剪映草稿链接已复制到剪贴板');
|
|
2777
|
+ }}
|
|
2778
|
+ >
|
|
2779
|
+ <i className="bi bi-clipboard"></i> 复制剪映草稿链接
|
|
2780
|
+ </Button>
|
|
2781
|
+ )}
|
|
2782
|
+ <Button
|
|
2783
|
+ variant="outline-primary"
|
|
2784
|
+ size="sm"
|
|
2785
|
+ className="ms-2"
|
|
2786
|
+ onClick={handleExportDraft}
|
|
2787
|
+ disabled={exportingDraft}
|
|
2788
|
+ >
|
|
2789
|
+ {exportingDraft ? (
|
|
2790
|
+ <>
|
|
2791
|
+ <Spinner
|
|
2792
|
+ as="span"
|
|
2793
|
+ animation="border"
|
|
2794
|
+ size="sm"
|
|
2795
|
+ role="status"
|
|
2796
|
+ aria-hidden="true"
|
|
2797
|
+ />
|
|
2798
|
+ <span className="ms-2">导出中...</span>
|
|
2799
|
+ </>
|
|
2800
|
+ ) : (
|
|
2801
|
+ '导出剪映草稿'
|
|
2802
|
+ )}
|
|
2803
|
+ </Button>
|
|
2804
|
+ </div>
|
|
2805
|
+ </div>
|
|
2806
|
+ </Card.Header>
|
|
2807
|
+ <Card.Body>
|
|
2808
|
+ <Row>
|
|
2809
|
+ {/* 项目画风 */}
|
|
2810
|
+ <Col md={3}>
|
|
2811
|
+ <Form.Group>
|
|
2812
|
+ <Form.Label>项目画风</Form.Label>
|
|
2813
|
+ <Dropdown>
|
|
2814
|
+ <Dropdown.Toggle
|
|
2815
|
+ variant="outline-primary"
|
|
2816
|
+ id="dropdown-style"
|
|
2817
|
+ disabled={loadingStyles}
|
|
2818
|
+ className="w-100 text-start"
|
|
2819
|
+ >
|
|
2820
|
+ {selectedStyle || '请选择画风'}
|
|
2821
|
+ {loadingStyles && (
|
|
2822
|
+ <Spinner
|
|
2823
|
+ as="span"
|
|
2824
|
+ animation="border"
|
|
2825
|
+ size="sm"
|
|
2826
|
+ role="status"
|
|
2827
|
+ aria-hidden="true"
|
|
2828
|
+ className="ms-2"
|
|
2829
|
+ />
|
|
2830
|
+ )}
|
|
2831
|
+ </Dropdown.Toggle>
|
|
2832
|
+
|
|
2833
|
+ <Dropdown.Menu className="w-100">
|
|
2834
|
+ {stylesList.map((style, index) => (
|
|
2835
|
+ <Dropdown.Item
|
|
2836
|
+ key={`style-${index}`}
|
|
2837
|
+ onClick={() => handleStyleSelect(style)}
|
|
2838
|
+ >
|
|
2839
|
+ {style}
|
|
2840
|
+ </Dropdown.Item>
|
|
2841
|
+ ))}
|
|
2842
|
+ </Dropdown.Menu>
|
|
2843
|
+ </Dropdown>
|
|
2844
|
+ </Form.Group>
|
|
2845
|
+ </Col>
|
|
2846
|
+
|
|
2847
|
+ {/* 剪映草稿 */}
|
|
2848
|
+ <Col md={3}>
|
|
2849
|
+ <Form.Group>
|
|
2850
|
+ <Form.Label>剪映草稿</Form.Label>
|
|
2851
|
+ {project.draft_url ? (
|
|
2852
|
+ <div className="border rounded p-2 bg-light">
|
|
2853
|
+ <div className="text-truncate small" style={{maxWidth: '100%'}}>
|
|
2854
|
+ <a
|
|
2855
|
+ href={project.draft_url}
|
|
2856
|
+ target="_blank"
|
|
2857
|
+ rel="noopener noreferrer"
|
|
2858
|
+ title={project.draft_url}
|
|
2859
|
+ >
|
|
2860
|
+ {project.draft_url}
|
|
2861
|
+ </a>
|
|
2862
|
+ </div>
|
|
2863
|
+ </div>
|
|
2864
|
+ ) : (
|
|
2865
|
+ <Alert variant="warning" className="mb-0">
|
|
2866
|
+ <small>尚未导出剪映草稿</small>
|
|
2867
|
+ </Alert>
|
|
2868
|
+ )}
|
|
2869
|
+ </Form.Group>
|
|
2870
|
+ </Col>
|
|
2871
|
+ </Row>
|
|
2872
|
+ </Card.Body>
|
|
2873
|
+ </Card>
|
|
2874
|
+ </Col>
|
|
2875
|
+ </Row>
|
2705
|
2876
|
</div>
|
2706
|
2877
|
) : (
|
2707
|
2878
|
<div className="text-center my-5">
|