|
@@ -3,7 +3,8 @@ import { Button, Form, Spinner, Card, ListGroup, Modal, InputGroup, Alert } from
|
3
|
3
|
import { toast } from 'react-toastify';
|
4
|
4
|
import './style.css';
|
5
|
5
|
import { bookService, bookInfoService } from '../../db';
|
6
|
|
-import { processProjectAudio } from '../../utils/audioProcessor'; // 导入音频处理工具
|
|
6
|
+// 移除音频处理器导入,因为不再需要分割音频
|
|
7
|
+// import { processProjectAudio } from '../../utils/audioProcessor';
|
7
|
8
|
// 替换Node.js模块为浏览器兼容版本
|
8
|
9
|
// const fs = require('fs');
|
9
|
10
|
// const path = require('path');
|
|
@@ -12,7 +13,7 @@ import path from 'path-browserify';
|
12
|
13
|
import os from 'os-browserify/browser';
|
13
|
14
|
// fs模块在浏览器中不可用,需要通过IPC调用主进程
|
14
|
15
|
|
15
|
|
-const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
|
16
|
+const SubtitleUpload = ({ onProjectCreated, projectAudioFile, projectVideoFile }) => {
|
16
|
17
|
const [file, setFile] = useState(null);
|
17
|
18
|
const [isLoading, setIsLoading] = useState(false);
|
18
|
19
|
const [subtitles, setSubtitles] = useState([]);
|
|
@@ -22,6 +23,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
22
|
23
|
const [isCreating, setIsCreating] = useState(false);
|
23
|
24
|
const [processingAudio, setProcessingAudio] = useState(false);
|
24
|
25
|
const [audioProgress, setAudioProgress] = useState(0);
|
|
26
|
+ const [processingVideo, setProcessingVideo] = useState(false);
|
25
|
27
|
|
26
|
28
|
// 处理文件选择
|
27
|
29
|
const handleFileChange = (e) => {
|
|
@@ -133,6 +135,11 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
133
|
135
|
toast.error('请先上传音频文件(必选)');
|
134
|
136
|
return;
|
135
|
137
|
}
|
|
138
|
+
|
|
139
|
+ if (!projectVideoFile) {
|
|
140
|
+ toast.error('请先上传视频文件(必选)');
|
|
141
|
+ return;
|
|
142
|
+ }
|
136
|
143
|
|
137
|
144
|
setShowCreateModal(true);
|
138
|
145
|
};
|
|
@@ -206,7 +213,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
206
|
213
|
return {
|
207
|
214
|
success: true,
|
208
|
215
|
path: result.filePath,
|
209
|
|
- relativePath: path.join('audio', fileName)
|
|
216
|
+ absolutePath: result.filePath
|
210
|
217
|
};
|
211
|
218
|
} else {
|
212
|
219
|
throw new Error(result.error);
|
|
@@ -216,10 +223,11 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
216
|
223
|
console.warn('Electron IPC不可用,无法保存文件到本地');
|
217
|
224
|
|
218
|
225
|
// 模拟成功返回,用于开发环境测试
|
|
226
|
+ const fullPath = path.join(audioDir, fileName);
|
219
|
227
|
return {
|
220
|
228
|
success: true,
|
221
|
|
- path: path.join(audioDir, fileName),
|
222
|
|
- relativePath: path.join('audio', fileName)
|
|
229
|
+ path: fullPath,
|
|
230
|
+ absolutePath: fullPath
|
223
|
231
|
};
|
224
|
232
|
}
|
225
|
233
|
} catch (error) {
|
|
@@ -231,6 +239,60 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
231
|
239
|
}
|
232
|
240
|
};
|
233
|
241
|
|
|
242
|
+ // 保存视频文件到本地
|
|
243
|
+ const saveVideoFile = async (videoFile, projectId) => {
|
|
244
|
+ try {
|
|
245
|
+ const audioDir = await getAppDataPath();
|
|
246
|
+ if (!audioDir) {
|
|
247
|
+ throw new Error('无法获取视频存储路径');
|
|
248
|
+ }
|
|
249
|
+
|
|
250
|
+ const fileName = `project_${projectId}_video.mp4`;
|
|
251
|
+
|
|
252
|
+ // 读取文件内容
|
|
253
|
+ const buffer = await videoFile.arrayBuffer();
|
|
254
|
+
|
|
255
|
+ // 检查electron对象是否存在
|
|
256
|
+ if (typeof window.electron !== 'undefined' && window.electron.ipcRenderer) {
|
|
257
|
+ // 通过IPC调用保存文件
|
|
258
|
+ const result = await window.electron.ipcRenderer.invoke('save-audio-file', {
|
|
259
|
+ buffer: Array.from(new Uint8Array(buffer)),
|
|
260
|
+ fileName: fileName,
|
|
261
|
+ directory: audioDir,
|
|
262
|
+ projectId: projectId,
|
|
263
|
+ isVideo: true
|
|
264
|
+ });
|
|
265
|
+
|
|
266
|
+ if (result.success) {
|
|
267
|
+ return {
|
|
268
|
+ success: true,
|
|
269
|
+ path: result.filePath,
|
|
270
|
+ absolutePath: result.filePath
|
|
271
|
+ };
|
|
272
|
+ } else {
|
|
273
|
+ throw new Error(result.error);
|
|
274
|
+ }
|
|
275
|
+ } else {
|
|
276
|
+ // 处理非Electron环境(例如开发环境中的Web浏览器)
|
|
277
|
+ console.warn('Electron IPC不可用,无法保存文件到本地');
|
|
278
|
+
|
|
279
|
+ // 模拟成功返回,用于开发环境测试
|
|
280
|
+ const fullPath = path.join(audioDir, fileName);
|
|
281
|
+ return {
|
|
282
|
+ success: true,
|
|
283
|
+ path: fullPath,
|
|
284
|
+ absolutePath: fullPath
|
|
285
|
+ };
|
|
286
|
+ }
|
|
287
|
+ } catch (error) {
|
|
288
|
+ console.error('保存视频文件失败:', error);
|
|
289
|
+ return {
|
|
290
|
+ success: false,
|
|
291
|
+ error: error.message
|
|
292
|
+ };
|
|
293
|
+ }
|
|
294
|
+ };
|
|
295
|
+
|
234
|
296
|
// 创建项目
|
235
|
297
|
const handleCreateProject = async () => {
|
236
|
298
|
if (!projectTitle.trim()) {
|
|
@@ -243,16 +305,13 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
243
|
305
|
return;
|
244
|
306
|
}
|
245
|
307
|
|
|
308
|
+ if (!projectVideoFile) {
|
|
309
|
+ toast.error('请上传视频文件(必选)');
|
|
310
|
+ return;
|
|
311
|
+ }
|
|
312
|
+
|
246
|
313
|
setIsCreating(true);
|
247
|
314
|
try {
|
248
|
|
- // 创建项目记录
|
249
|
|
- const bookId = await bookService.createBook({
|
250
|
|
- title: projectTitle,
|
251
|
|
- subtitle_path: file ? file.name : null,
|
252
|
|
- audio_path: projectAudioFile ? projectAudioFile.name : null,
|
253
|
|
- created_at: new Date().toISOString()
|
254
|
|
- });
|
255
|
|
-
|
256
|
315
|
// 为每个字幕段计算持续时间
|
257
|
316
|
const processedSubtitles = subtitles.map(sub => {
|
258
|
317
|
// 解析开始和结束时间
|
|
@@ -272,7 +331,16 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
272
|
331
|
};
|
273
|
332
|
});
|
274
|
333
|
|
275
|
|
- // 创建项目详细信息(先不包含音频路径)
|
|
334
|
+ // 创建项目记录
|
|
335
|
+ const bookId = await bookService.createBook({
|
|
336
|
+ title: projectTitle,
|
|
337
|
+ subtitle_path: file ? file.name : null,
|
|
338
|
+ audio_path: projectAudioFile ? projectAudioFile.name : null,
|
|
339
|
+ video_path: projectVideoFile ? projectVideoFile.name : null,
|
|
340
|
+ created_at: new Date().toISOString()
|
|
341
|
+ });
|
|
342
|
+
|
|
343
|
+ // 创建项目详细信息
|
276
|
344
|
const bookInfoPromises = processedSubtitles.map(sub => {
|
277
|
345
|
return bookInfoService.createBookInfo({
|
278
|
346
|
book_id: bookId,
|
|
@@ -284,50 +352,42 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
284
|
352
|
});
|
285
|
353
|
});
|
286
|
354
|
|
287
|
|
- const createdSegments = await Promise.all(bookInfoPromises);
|
|
355
|
+ await Promise.all(bookInfoPromises);
|
288
|
356
|
|
289
|
|
- // 处理音频文件
|
|
357
|
+ // 处理音频文件 - 仅保存路径
|
290
|
358
|
setProcessingAudio(true);
|
291
|
|
- toast.info('正在处理音频文件,请稍候...');
|
|
359
|
+ toast.info('正在保存音频文件,请稍候...');
|
292
|
360
|
|
293
|
361
|
// 保存完整音频文件
|
294
|
362
|
const savedAudio = await saveAudioFile(projectAudioFile, bookId);
|
295
|
363
|
|
296
|
364
|
if (savedAudio.success) {
|
297
|
|
- // 更新项目记录中的音频路径为相对路径
|
|
365
|
+ // 更新项目记录中的音频路径为绝对路径
|
298
|
366
|
await bookService.updateBook(bookId, {
|
299
|
|
- audio_path: savedAudio.relativePath
|
|
367
|
+ audio_path: savedAudio.absolutePath
|
300
|
368
|
});
|
301
|
|
-
|
302
|
|
- // 获取所有已创建的分镜信息
|
303
|
|
- const segments = await bookInfoService.getBookInfoByBookId(bookId);
|
304
|
|
-
|
305
|
|
- // 切割音频
|
306
|
|
- const audioProcessResult = await processProjectAudio(
|
307
|
|
- bookId,
|
308
|
|
- savedAudio.path,
|
309
|
|
- segments,
|
310
|
|
- getAppDataPath()
|
311
|
|
- );
|
312
|
|
-
|
313
|
|
- if (audioProcessResult.success) {
|
314
|
|
- // 更新每个分镜的音频路径
|
315
|
|
- const updatePromises = audioProcessResult.segments.map(segment => {
|
316
|
|
- return bookInfoService.updateBookInfo(segment.segmentId, {
|
317
|
|
- audio_path: segment.relativePath
|
318
|
|
- });
|
319
|
|
- });
|
320
|
|
-
|
321
|
|
- await Promise.all(updatePromises);
|
322
|
|
-
|
323
|
|
- toast.success(`音频处理完成,已为 ${audioProcessResult.processedSegments}/${audioProcessResult.totalSegments} 个分镜生成独立音频`);
|
324
|
|
- } else {
|
325
|
|
- toast.warning(`音频切割过程中出现一些问题: ${audioProcessResult.error}`);
|
326
|
|
- }
|
|
369
|
+ toast.success('音频文件路径已保存');
|
327
|
370
|
} else {
|
328
|
371
|
toast.warning(`保存音频文件失败: ${savedAudio.error}`);
|
329
|
372
|
}
|
330
|
373
|
|
|
374
|
+ // 处理视频文件 - 仅保存路径
|
|
375
|
+ setProcessingVideo(true);
|
|
376
|
+ toast.info('正在保存视频文件,请稍候...');
|
|
377
|
+
|
|
378
|
+ // 保存视频文件
|
|
379
|
+ const savedVideo = await saveVideoFile(projectVideoFile, bookId);
|
|
380
|
+
|
|
381
|
+ if (savedVideo.success) {
|
|
382
|
+ // 更新项目记录中的视频路径为绝对路径
|
|
383
|
+ await bookService.updateBook(bookId, {
|
|
384
|
+ video_path: savedVideo.absolutePath
|
|
385
|
+ });
|
|
386
|
+ toast.success('视频文件路径已保存');
|
|
387
|
+ } else {
|
|
388
|
+ toast.warning(`保存视频文件失败: ${savedVideo.error}`);
|
|
389
|
+ }
|
|
390
|
+
|
331
|
391
|
toast.success('项目创建成功!');
|
332
|
392
|
setShowCreateModal(false);
|
333
|
393
|
|
|
@@ -337,6 +397,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
337
|
397
|
setFileName('');
|
338
|
398
|
setSubtitles([]);
|
339
|
399
|
setProcessingAudio(false);
|
|
400
|
+ setProcessingVideo(false);
|
340
|
401
|
|
341
|
402
|
// 调用回调函数
|
342
|
403
|
if (typeof onProjectCreated === 'function') {
|
|
@@ -348,6 +409,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
348
|
409
|
} finally {
|
349
|
410
|
setIsCreating(false);
|
350
|
411
|
setProcessingAudio(false);
|
|
412
|
+ setProcessingVideo(false);
|
351
|
413
|
}
|
352
|
414
|
};
|
353
|
415
|
|
|
@@ -427,7 +489,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
427
|
489
|
variant="success"
|
428
|
490
|
size="lg"
|
429
|
491
|
onClick={openCreateProjectModal}
|
430
|
|
- disabled={!projectAudioFile}
|
|
492
|
+ disabled={!projectAudioFile || !projectVideoFile}
|
431
|
493
|
>
|
432
|
494
|
创建项目
|
433
|
495
|
</Button>
|
|
@@ -436,6 +498,11 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
436
|
498
|
请先上传音频文件(必选)
|
437
|
499
|
</div>
|
438
|
500
|
)}
|
|
501
|
+ {!projectVideoFile && (
|
|
502
|
+ <div className="text-danger mt-2">
|
|
503
|
+ 请先上传视频文件(必选)
|
|
504
|
+ </div>
|
|
505
|
+ )}
|
439
|
506
|
</div>
|
440
|
507
|
</>
|
441
|
508
|
)}
|
|
@@ -443,8 +510,8 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
443
|
510
|
</Card>
|
444
|
511
|
|
445
|
512
|
{/* 创建项目模态框 */}
|
446
|
|
- <Modal show={showCreateModal} onHide={() => !isCreating && !processingAudio && setShowCreateModal(false)}>
|
447
|
|
- <Modal.Header closeButton={!isCreating && !processingAudio}>
|
|
513
|
+ <Modal show={showCreateModal} onHide={() => !isCreating && !processingAudio && !processingVideo && setShowCreateModal(false)}>
|
|
514
|
+ <Modal.Header closeButton={!isCreating && !processingAudio && !processingVideo}>
|
448
|
515
|
<Modal.Title>创建新项目</Modal.Title>
|
449
|
516
|
</Modal.Header>
|
450
|
517
|
<Modal.Body>
|
|
@@ -456,7 +523,7 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
456
|
523
|
placeholder="请输入项目标题"
|
457
|
524
|
value={projectTitle}
|
458
|
525
|
onChange={(e) => setProjectTitle(e.target.value)}
|
459
|
|
- disabled={isCreating || processingAudio}
|
|
526
|
+ disabled={isCreating || processingAudio || processingVideo}
|
460
|
527
|
/>
|
461
|
528
|
</Form.Group>
|
462
|
529
|
<Form.Group className="mt-3">
|
|
@@ -478,6 +545,15 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
478
|
545
|
</InputGroup>
|
479
|
546
|
</Form.Group>
|
480
|
547
|
<Form.Group className="mt-3">
|
|
548
|
+ <Form.Label>视频文件</Form.Label>
|
|
549
|
+ <InputGroup>
|
|
550
|
+ <Form.Control
|
|
551
|
+ readOnly
|
|
552
|
+ value={projectVideoFile ? projectVideoFile.name : '未选择视频文件'}
|
|
553
|
+ />
|
|
554
|
+ </InputGroup>
|
|
555
|
+ </Form.Group>
|
|
556
|
+ <Form.Group className="mt-3">
|
481
|
557
|
<Form.Label>字幕条数</Form.Label>
|
482
|
558
|
<Form.Control
|
483
|
559
|
readOnly
|
|
@@ -485,10 +561,10 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
485
|
561
|
/>
|
486
|
562
|
</Form.Group>
|
487
|
563
|
|
488
|
|
- {processingAudio && (
|
|
564
|
+ {(processingAudio || processingVideo) && (
|
489
|
565
|
<div className="mt-3">
|
490
|
566
|
<Alert variant="info">
|
491
|
|
- 正在处理音频文件,请稍候...这可能需要一些时间。
|
|
567
|
+ 正在保存{processingAudio ? '音频' : '视频'}文件,请稍候...
|
492
|
568
|
</Alert>
|
493
|
569
|
</div>
|
494
|
570
|
)}
|
|
@@ -498,16 +574,16 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
498
|
574
|
<Button
|
499
|
575
|
variant="secondary"
|
500
|
576
|
onClick={() => setShowCreateModal(false)}
|
501
|
|
- disabled={isCreating || processingAudio}
|
|
577
|
+ disabled={isCreating || processingAudio || processingVideo}
|
502
|
578
|
>
|
503
|
579
|
取消
|
504
|
580
|
</Button>
|
505
|
581
|
<Button
|
506
|
582
|
variant="primary"
|
507
|
583
|
onClick={handleCreateProject}
|
508
|
|
- disabled={isCreating || processingAudio}
|
|
584
|
+ disabled={isCreating || processingAudio || processingVideo}
|
509
|
585
|
>
|
510
|
|
- {isCreating || processingAudio ? (
|
|
586
|
+ {isCreating || processingAudio || processingVideo ? (
|
511
|
587
|
<>
|
512
|
588
|
<Spinner
|
513
|
589
|
as="span"
|
|
@@ -516,7 +592,10 @@ const SubtitleUpload = ({ onProjectCreated, projectAudioFile }) => {
|
516
|
592
|
role="status"
|
517
|
593
|
aria-hidden="true"
|
518
|
594
|
/>
|
519
|
|
- <span className="ml-2">{processingAudio ? '处理音频中...' : '创建中...'}</span>
|
|
595
|
+ <span className="ml-2">
|
|
596
|
+ {processingAudio ? '保存音频中...' :
|
|
597
|
+ processingVideo ? '保存视频中...' : '创建中...'}
|
|
598
|
+ </span>
|
520
|
599
|
</>
|
521
|
600
|
) : (
|
522
|
601
|
'确认创建'
|