@huiqinghuang/videocut-cli 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/videocut.js +2 -0
  2. package/dist/commands/apply-edits.d.ts +4 -0
  3. package/dist/commands/apply-edits.d.ts.map +1 -0
  4. package/dist/commands/apply-edits.js +27 -0
  5. package/dist/commands/apply-edits.js.map +1 -0
  6. package/dist/commands/cut-video.d.ts +5 -0
  7. package/dist/commands/cut-video.d.ts.map +1 -0
  8. package/dist/commands/cut-video.js +33 -0
  9. package/dist/commands/cut-video.js.map +1 -0
  10. package/dist/commands/generate-readable.d.ts +4 -0
  11. package/dist/commands/generate-readable.d.ts.map +1 -0
  12. package/dist/commands/generate-readable.js +33 -0
  13. package/dist/commands/generate-readable.js.map +1 -0
  14. package/dist/commands/generate-review.d.ts +4 -0
  15. package/dist/commands/generate-review.d.ts.map +1 -0
  16. package/dist/commands/generate-review.js +33 -0
  17. package/dist/commands/generate-review.js.map +1 -0
  18. package/dist/commands/generate-subtitles.d.ts +4 -0
  19. package/dist/commands/generate-subtitles.d.ts.map +1 -0
  20. package/dist/commands/generate-subtitles.js +68 -0
  21. package/dist/commands/generate-subtitles.js.map +1 -0
  22. package/dist/commands/review-server.d.ts +4 -0
  23. package/dist/commands/review-server.d.ts.map +1 -0
  24. package/dist/commands/review-server.js +344 -0
  25. package/dist/commands/review-server.js.map +1 -0
  26. package/dist/commands/transcribe.d.ts +4 -0
  27. package/dist/commands/transcribe.d.ts.map +1 -0
  28. package/dist/commands/transcribe.js +114 -0
  29. package/dist/commands/transcribe.js.map +1 -0
  30. package/dist/core/edits.d.ts +10 -0
  31. package/dist/core/edits.d.ts.map +1 -0
  32. package/dist/core/edits.js +110 -0
  33. package/dist/core/edits.js.map +1 -0
  34. package/dist/core/subtitle.d.ts +6 -0
  35. package/dist/core/subtitle.d.ts.map +1 -0
  36. package/dist/core/subtitle.js +87 -0
  37. package/dist/core/subtitle.js.map +1 -0
  38. package/dist/core/types.d.ts +68 -0
  39. package/dist/core/types.d.ts.map +1 -0
  40. package/dist/core/types.js +2 -0
  41. package/dist/core/types.js.map +1 -0
  42. package/dist/core/video.d.ts +13 -0
  43. package/dist/core/video.d.ts.map +1 -0
  44. package/dist/core/video.js +188 -0
  45. package/dist/core/video.js.map +1 -0
  46. package/dist/index.d.ts +3 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +46 -0
  49. package/dist/index.js.map +1 -0
  50. package/package.json +45 -0
  51. package/static/assets/index-BeR4WwzJ.js +54 -0
  52. package/static/assets/index-CsW22Sz0.css +1 -0
  53. package/static/index.html +13 -0
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js');
@@ -0,0 +1,4 @@
1
+ export declare function applyEdits(subtitlesPath: string, editsPath: string, options: {
2
+ output?: string;
3
+ }): void;
4
+ //# sourceMappingURL=apply-edits.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-edits.d.ts","sourceRoot":"","sources":["../../src/commands/apply-edits.ts"],"names":[],"mappings":"AAKA,wBAAgB,UAAU,CACxB,aAAa,EAAE,MAAM,EACrB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,IAAI,CA2BN"}
@@ -0,0 +1,27 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { applyEditsToOpted, deepClone } from '../core/edits.js';
4
+ export function applyEdits(subtitlesPath, editsPath, options) {
5
+ const optedFile = path.resolve(subtitlesPath);
6
+ const editsFile = path.resolve(editsPath);
7
+ if (!fs.existsSync(optedFile)) {
8
+ console.error(`❌ 找不到字幕文件: ${optedFile}`);
9
+ process.exit(1);
10
+ }
11
+ const opted = JSON.parse(fs.readFileSync(optedFile, 'utf8'));
12
+ if (!Array.isArray(opted) || opted.length === 0) {
13
+ console.error('❌ 字幕不是数组或为空');
14
+ process.exit(1);
15
+ }
16
+ if (!fs.existsSync(editsFile)) {
17
+ console.error(`❌ 找不到编辑文件: ${editsFile}`);
18
+ process.exit(1);
19
+ }
20
+ const edits = JSON.parse(fs.readFileSync(editsFile, 'utf8'));
21
+ const edited = applyEditsToOpted(deepClone(opted), edits);
22
+ const outDir = path.dirname(optedFile);
23
+ const outFile = options.output || path.join(outDir, 'subtitles_words_edited.json');
24
+ fs.writeFileSync(outFile, JSON.stringify(edited, null, 2), 'utf8');
25
+ console.log(`✅ 已保存: ${outFile}`);
26
+ }
27
+ //# sourceMappingURL=apply-edits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-edits.js","sourceRoot":"","sources":["../../src/commands/apply-edits.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAGhE,MAAM,UAAU,UAAU,CACxB,aAAqB,EACrB,SAAiB,EACjB,OAA4B;IAE5B,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC9C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAE1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,cAAc,SAAS,EAAE,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAgB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,cAAc,SAAS,EAAE,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAU,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC;IAE1D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACvC,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,6BAA6B,CAAC,CAAC;IACnF,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACnE,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,5 @@
1
+ export declare function cutVideo(videoPath: string, segmentsPath: string, options: {
2
+ output?: string;
3
+ project?: string;
4
+ }): void;
5
+ //# sourceMappingURL=cut-video.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cut-video.d.ts","sourceRoot":"","sources":["../../src/commands/cut-video.ts"],"names":[],"mappings":"AAKA,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,IAAI,CAmCN"}
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { cutVideo as doCutVideo } from '../core/video.js';
4
+ export function cutVideo(videoPath, segmentsPath, options) {
5
+ const videoFile = path.resolve(videoPath);
6
+ const segmentsFile = path.resolve(segmentsPath);
7
+ if (!fs.existsSync(videoFile)) {
8
+ console.error(`❌ 找不到视频文件: ${videoFile}`);
9
+ process.exit(1);
10
+ }
11
+ if (!fs.existsSync(segmentsFile)) {
12
+ console.error(`❌ 找不到片段文件: ${segmentsFile}`);
13
+ process.exit(1);
14
+ }
15
+ const segments = JSON.parse(fs.readFileSync(segmentsFile, 'utf8'));
16
+ if (!Array.isArray(segments) || segments.length === 0) {
17
+ console.error('❌ 删除片段为空或格式错误');
18
+ process.exit(1);
19
+ }
20
+ const baseName = path.basename(videoFile, '.mp4');
21
+ const dir = path.dirname(videoFile);
22
+ const outputFile = options.output || path.join(dir, `${baseName}_cut.mp4`);
23
+ const projectPath = options.project;
24
+ console.log(`📹 输入视频: ${videoFile}`);
25
+ console.log(`📹 输出视频: ${outputFile}`);
26
+ console.log(`✂️ 删除片段数: ${segments.length}`);
27
+ const result = doCutVideo(videoFile, segments, outputFile, projectPath);
28
+ console.log(`\n✅ 剪辑完成!`);
29
+ console.log(` 原时长: ${result.originalDuration.toFixed(2)}s`);
30
+ console.log(` 新时长: ${result.newDuration.toFixed(2)}s`);
31
+ console.log(` 删除: ${(result.originalDuration - result.newDuration).toFixed(2)}s`);
32
+ }
33
+ //# sourceMappingURL=cut-video.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cut-video.js","sourceRoot":"","sources":["../../src/commands/cut-video.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,QAAQ,IAAI,UAAU,EAAkB,MAAM,kBAAkB,CAAC;AAG1E,MAAM,UAAU,QAAQ,CACtB,SAAiB,EACjB,YAAoB,EACpB,OAA8C;IAE9C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAEhD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,KAAK,CAAC,cAAc,SAAS,EAAE,CAAC,CAAC;QACzC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC,EAAE,CAAC;QACjC,OAAO,CAAC,KAAK,CAAC,cAAc,YAAY,EAAE,CAAC,CAAC;QAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAoB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IACpF,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtD,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAC/B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,UAAU,CAAC,CAAC;IAC3E,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC;IAEpC,OAAO,CAAC,GAAG,CAAC,YAAY,SAAS,EAAE,CAAC,CAAC;IACrC,OAAO,CAAC,GAAG,CAAC,YAAY,UAAU,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,GAAG,CAAC,aAAa,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAE5C,MAAM,MAAM,GAAG,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,CAAC,CAAC;IAExE,OAAO,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACzB,OAAO,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,gBAAgB,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,WAAW,MAAM,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACzD,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,MAAM,CAAC,gBAAgB,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function generateReview(subtitlesPath: string, options: {
2
+ output?: string;
3
+ }): void;
4
+ //# sourceMappingURL=generate-readable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-readable.d.ts","sourceRoot":"","sources":["../../src/commands/generate-readable.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,CAC5B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,IAAI,CA+BN"}
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ export function generateReview(subtitlesPath, options) {
4
+ const sourceFile = path.resolve(subtitlesPath);
5
+ if (!fs.existsSync(sourceFile)) {
6
+ console.error(`❌ 找不到字幕文件: ${sourceFile}`);
7
+ process.exit(1);
8
+ }
9
+ const opted = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
10
+ if (!Array.isArray(opted) || opted.length === 0) {
11
+ console.error('❌ 字幕不是数组或为空');
12
+ process.exit(1);
13
+ }
14
+ let out = '';
15
+ opted.forEach((u, i) => {
16
+ if (u.opt === 'blank') {
17
+ out += `${i}|blank_${((u.end_time - u.start_time) / 1000).toFixed(1)}s\n`;
18
+ }
19
+ else {
20
+ out += `${i}|${u.text}\n`;
21
+ if (u.words) {
22
+ u.words.forEach((w, j) => {
23
+ out += `${j}|${w.text}\n`;
24
+ });
25
+ }
26
+ }
27
+ });
28
+ const outFile = options.output || path.join(path.dirname(sourceFile), '..', '2_analysis', 'readable.txt');
29
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
30
+ fs.writeFileSync(outFile, out, 'utf8');
31
+ console.log(`✅ 已保存: ${outFile}`);
32
+ }
33
+ //# sourceMappingURL=generate-readable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-readable.js","sourceRoot":"","sources":["../../src/commands/generate-readable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,UAAU,cAAc,CAC5B,aAAqB,EACrB,OAA4B;IAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,cAAc,UAAU,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAgB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,CAAC,OAAO,CAAC,CAAC,CAAY,EAAE,CAAS,EAAE,EAAE;QACxC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YACtB,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;YAC1B,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACZ,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;oBACvB,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;gBAC5B,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;IAC1G,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function generateReview(subtitlesPath: string, options: {
2
+ output?: string;
3
+ }): void;
4
+ //# sourceMappingURL=generate-review.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-review.d.ts","sourceRoot":"","sources":["../../src/commands/generate-review.ts"],"names":[],"mappings":"AAIA,wBAAgB,cAAc,CAC5B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,IAAI,CA+BN"}
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ export function generateReview(subtitlesPath, options) {
4
+ const sourceFile = path.resolve(subtitlesPath);
5
+ if (!fs.existsSync(sourceFile)) {
6
+ console.error(`❌ 找不到字幕文件: ${sourceFile}`);
7
+ process.exit(1);
8
+ }
9
+ const opted = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
10
+ if (!Array.isArray(opted) || opted.length === 0) {
11
+ console.error('❌ 字幕不是数组或为空');
12
+ process.exit(1);
13
+ }
14
+ let out = '';
15
+ opted.forEach((u, i) => {
16
+ if (u.opt === 'blank') {
17
+ out += `${i}|blank_${((u.end_time - u.start_time) / 1000).toFixed(1)}s\n`;
18
+ }
19
+ else {
20
+ out += `${i}|${u.text}\n`;
21
+ if (u.words) {
22
+ u.words.forEach((w, j) => {
23
+ out += ` ${j}|${w.text}\n`;
24
+ });
25
+ }
26
+ }
27
+ });
28
+ const outFile = options.output || path.join(path.dirname(sourceFile), '..', '2_analysis', 'readable.txt');
29
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
30
+ fs.writeFileSync(outFile, out, 'utf8');
31
+ console.log(`✅ 已保存: ${outFile}`);
32
+ }
33
+ //# sourceMappingURL=generate-review.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-review.js","sourceRoot":"","sources":["../../src/commands/generate-review.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,UAAU,cAAc,CAC5B,aAAqB,EACrB,OAA4B;IAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC;IAC/C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,cAAc,UAAU,EAAE,CAAC,CAAC;QAC1C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,KAAK,GAAgB,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAChD,OAAO,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QAC7B,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,CAAC,OAAO,CAAC,CAAC,CAAY,EAAE,CAAS,EAAE,EAAE;QACxC,IAAI,CAAC,CAAC,GAAG,KAAK,OAAO,EAAE,CAAC;YACtB,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC;QAC5E,CAAC;aAAM,CAAC;YACN,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;YAC1B,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;gBACZ,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;oBACvB,GAAG,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC;gBAC9B,CAAC,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,cAAc,CAAC,CAAC;IAC1G,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IACvC,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function generateSubtitles(jsonPath: string, options: {
2
+ output?: string;
3
+ }): void;
4
+ //# sourceMappingURL=generate-subtitles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-subtitles.d.ts","sourceRoot":"","sources":["../../src/commands/generate-subtitles.ts"],"names":[],"mappings":"AA0DA,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,IAAI,CAoBN"}
@@ -0,0 +1,68 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ const GAP_MS = 100;
4
+ function removeSpeechAttribute(obj) {
5
+ if (obj && typeof obj === 'object' && obj.attribute && obj.attribute.event === 'speech') {
6
+ const keys = Object.keys(obj.attribute);
7
+ if (keys.length === 1 && keys[0] === 'event') {
8
+ delete obj.attribute;
9
+ }
10
+ }
11
+ }
12
+ function makeGapNode(startTime, endTime) {
13
+ return { opt: 'blank', start_time: startTime, end_time: endTime, text: '' };
14
+ }
15
+ function isEmptyNode(item) {
16
+ const text = (item.text != null ? String(item.text) : '').trim();
17
+ const start = typeof item.start_time === 'number' ? item.start_time : 0;
18
+ const end = typeof item.end_time === 'number' ? item.end_time : start;
19
+ return start === end && !text;
20
+ }
21
+ function editNode(cur) {
22
+ removeSpeechAttribute(cur);
23
+ cur.opt = 'keep';
24
+ }
25
+ function produceGapNode(cur, preEndTime) {
26
+ const currStart = typeof cur.start_time === 'number' ? cur.start_time : preEndTime;
27
+ const gapMs = currStart - preEndTime;
28
+ return gapMs > GAP_MS ? makeGapNode(preEndTime, currStart) : null;
29
+ }
30
+ function loopItems(items, parentStartTime = 0) {
31
+ if (!Array.isArray(items))
32
+ return;
33
+ let i = 0;
34
+ while (i < items.length) {
35
+ if (isEmptyNode(items[i])) {
36
+ items.splice(i, 1);
37
+ continue;
38
+ }
39
+ editNode(items[i]);
40
+ const preEndTime = i > 0 ? items[i - 1].end_time : parentStartTime;
41
+ const gap = produceGapNode(items[i], preEndTime);
42
+ if (gap) {
43
+ items.splice(i, 0, gap);
44
+ i++;
45
+ }
46
+ loopItems(items[i].words, items[i].start_time);
47
+ i++;
48
+ }
49
+ }
50
+ export function generateSubtitles(jsonPath, options) {
51
+ const sourceFile = path.resolve(jsonPath);
52
+ if (!fs.existsSync(sourceFile)) {
53
+ console.error(`❌ 找不到文件: ${sourceFile}`);
54
+ process.exit(1);
55
+ }
56
+ const source = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
57
+ if (!Array.isArray(source.utterances)) {
58
+ console.error('❌ 缺少 utterances 数组');
59
+ process.exit(1);
60
+ }
61
+ loopItems(source.utterances);
62
+ const outDir = path.dirname(path.dirname(sourceFile));
63
+ const outFile = options.output || path.join(outDir, 'common', 'subtitles_words.json');
64
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
65
+ fs.writeFileSync(outFile, JSON.stringify(source.utterances, null, 2), 'utf8');
66
+ console.log(`✅ 已保存: ${outFile}`);
67
+ }
68
+ //# sourceMappingURL=generate-subtitles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generate-subtitles.js","sourceRoot":"","sources":["../../src/commands/generate-subtitles.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAGxB,MAAM,MAAM,GAAG,GAAG,CAAC;AAEnB,SAAS,qBAAqB,CAAC,GAAQ;IACrC,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,SAAS,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACxF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACxC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,EAAE,CAAC;YAC7C,OAAO,GAAG,CAAC,SAAS,CAAC;QACvB,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,WAAW,CAAC,SAAiB,EAAE,OAAe;IACrD,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,IAAS;IAC5B,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACjE,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACxE,MAAM,GAAG,GAAG,OAAO,IAAI,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC;IACtE,OAAO,KAAK,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC;AAChC,CAAC;AAED,SAAS,QAAQ,CAAC,GAAQ;IACxB,qBAAqB,CAAC,GAAG,CAAC,CAAC;IAC3B,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC;AACnB,CAAC;AAED,SAAS,cAAc,CAAC,GAAQ,EAAE,UAAkB;IAClD,MAAM,SAAS,GAAG,OAAO,GAAG,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC;IACnF,MAAM,KAAK,GAAG,SAAS,GAAG,UAAU,CAAC;IACrC,OAAO,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACpE,CAAC;AAED,SAAS,SAAS,CAAC,KAAY,EAAE,eAAe,GAAG,CAAC;IAClD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO;IAClC,IAAI,CAAC,GAAG,CAAC,CAAC;IACV,OAAO,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC;QACxB,IAAI,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1B,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;YACnB,SAAS;QACX,CAAC;QACD,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QAEnB,MAAM,UAAU,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,eAAe,CAAC;QACnE,MAAM,GAAG,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC;QACjD,IAAI,GAAG,EAAE,CAAC;YACR,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,CAAC,CAAC;YACxB,CAAC,EAAE,CAAC;QACN,CAAC;QACD,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;QAC/C,CAAC,EAAE,CAAC;IACN,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,QAAgB,EAChB,OAA4B;IAE5B,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,KAAK,CAAC,YAAY,UAAU,EAAE,CAAC,CAAC;QACxC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC;IAC/D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;QACpC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,SAAS,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;IAE7B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IACtD,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,sBAAsB,CAAC,CAAC;IACtF,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC9E,OAAO,CAAC,GAAG,CAAC,UAAU,OAAO,EAAE,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function reviewServer(port: number | undefined, options: {
2
+ path?: string;
3
+ }): void;
4
+ //# sourceMappingURL=review-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-server.d.ts","sourceRoot":"","sources":["../../src/commands/review-server.ts"],"names":[],"mappings":"AA8JA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,YAAO,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CA6NlF"}
@@ -0,0 +1,344 @@
1
+ import http from 'http';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { cutVideo } from '../core/video.js';
6
+ import { buildDeleteSegmentsFromDeletes } from '../core/edits.js';
7
+ import { generateSrt, buildSubtitlesFromEditedOpted, burnSubtitles } from '../core/subtitle.js';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+ const MIME_TYPES = {
11
+ '.html': 'text/html',
12
+ '.js': 'application/javascript',
13
+ '.css': 'text/css',
14
+ '.json': 'application/json',
15
+ '.mp3': 'audio/mpeg',
16
+ '.mp4': 'video/mp4',
17
+ };
18
+ function getProjects(rootPath) {
19
+ const list = [];
20
+ const common = path.join(rootPath, 'common');
21
+ const wordsFile = path.join(common, 'subtitles_words_edited.json');
22
+ const wordsFallback = path.join(common, 'subtitles_words.json');
23
+ if (fs.existsSync(wordsFile) || fs.existsSync(wordsFallback)) {
24
+ const id = path.basename(path.dirname(rootPath));
25
+ list.push({
26
+ id,
27
+ name: id,
28
+ path: rootPath,
29
+ hasEdited: fs.existsSync(wordsFile),
30
+ });
31
+ return list;
32
+ }
33
+ if (!fs.existsSync(rootPath) || !fs.statSync(rootPath).isDirectory()) {
34
+ return list;
35
+ }
36
+ const dirs = fs.readdirSync(rootPath);
37
+ for (const d of dirs) {
38
+ const projectRoot = path.join(rootPath, d, 'clipping');
39
+ const commonDir = path.join(projectRoot, 'common');
40
+ const edited = path.join(commonDir, 'subtitles_words_edited.json');
41
+ const fallback = path.join(commonDir, 'subtitles_words.json');
42
+ if (fs.existsSync(edited) || fs.existsSync(fallback)) {
43
+ list.push({
44
+ id: d,
45
+ name: d,
46
+ path: projectRoot,
47
+ hasEdited: fs.existsSync(edited),
48
+ });
49
+ }
50
+ }
51
+ return list;
52
+ }
53
+ function getProjectById(rootPath, projectId) {
54
+ return getProjects(rootPath).find((p) => p.id === projectId) || null;
55
+ }
56
+ function findMp4InDir(dir) {
57
+ try {
58
+ const mp4s = fs.readdirSync(dir).filter((f) => f.endsWith('.mp4') && !f.endsWith('_cut.mp4'));
59
+ if (mp4s.length > 0)
60
+ return path.join(dir, mp4s[0]);
61
+ }
62
+ catch { }
63
+ return null;
64
+ }
65
+ function findVideoFile(project, rootPath) {
66
+ const parentDir = path.dirname(project.path);
67
+ // 1. output 父目录(symlink 所在位置)
68
+ const inParent = findMp4InDir(parentDir);
69
+ if (inParent)
70
+ return inParent;
71
+ // 2. 项目目录自身
72
+ const inProject = findMp4InDir(project.path);
73
+ if (inProject)
74
+ return inProject;
75
+ // 3. 向上逐级查找 videos/ 文件夹(最多 3 层)
76
+ let ancestor = parentDir;
77
+ for (let i = 0; i < 3; i++) {
78
+ const videosDir = path.join(ancestor, 'videos');
79
+ const inVideos = findMp4InDir(videosDir);
80
+ if (inVideos)
81
+ return inVideos;
82
+ const next = path.dirname(ancestor);
83
+ if (next === ancestor)
84
+ break;
85
+ ancestor = next;
86
+ }
87
+ // 4. macro_notes 目录(兼容旧目录结构)
88
+ const macroDir = path.resolve(rootPath, '..', '..', '..', 'macro_notes');
89
+ if (fs.existsSync(macroDir)) {
90
+ const videoName = project.id.replace(/^\d{4}-\d{2}-\d{2}_/, '');
91
+ const candidate = path.join(macroDir, videoName + '.mp4');
92
+ if (fs.existsSync(candidate))
93
+ return candidate;
94
+ }
95
+ return null;
96
+ }
97
+ function flattenWords(opted) {
98
+ const out = [];
99
+ opted.forEach((node, parentIndex) => {
100
+ const parentOpt = node.opt || 'keep';
101
+ if (Array.isArray(node.words) && node.words.length > 0) {
102
+ node.words.forEach((w, childIndex) => {
103
+ const start = typeof w.start_time === 'number' ? w.start_time / 1000 : (node.start_time || 0) / 1000;
104
+ const end = typeof w.end_time === 'number' ? w.end_time / 1000 : (node.end_time || 0) / 1000;
105
+ const opt = parentOpt === 'del' ? 'del' : w.opt || 'keep';
106
+ out.push({ start, end, text: (w.text || '').trim(), opt, parentIndex, childIndex });
107
+ });
108
+ }
109
+ else {
110
+ const start = (node.start_time || 0) / 1000;
111
+ const end = (node.end_time || 0) / 1000;
112
+ out.push({ start, end, text: (node.text || '').trim(), opt: parentOpt, parentIndex, childIndex: undefined });
113
+ }
114
+ });
115
+ return out;
116
+ }
117
+ function loadProjectWordsRaw(project) {
118
+ const commonDir = path.join(project.path, 'common');
119
+ const editedPath = path.join(commonDir, 'subtitles_words_edited.json');
120
+ const wordsPath = path.join(commonDir, 'subtitles_words.json');
121
+ const rawPath = fs.existsSync(editedPath) ? editedPath : wordsPath;
122
+ return {
123
+ rawPath,
124
+ opted: JSON.parse(fs.readFileSync(rawPath, 'utf8')),
125
+ };
126
+ }
127
+ function normalizeEditsPayload(payload) {
128
+ if (Array.isArray(payload)) {
129
+ return { deletes: payload.map((seg) => ({ start: seg.start, end: seg.end })), burnSubtitle: false };
130
+ }
131
+ const deletes = Array.isArray(payload?.deletes) ? payload.deletes : [];
132
+ const burnSubtitle = Boolean(payload?.burnSubtitle);
133
+ return { deletes, burnSubtitle };
134
+ }
135
+ export function reviewServer(port = 8899, options) {
136
+ const rootPath = path.resolve(options.path || path.join(process.cwd(), 'output'));
137
+ const staticDir = path.join(__dirname, '..', '..', 'static');
138
+ const server = http.createServer((req, res) => {
139
+ res.setHeader('Access-Control-Allow-Origin', '*');
140
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
141
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
142
+ if (req.method === 'OPTIONS') {
143
+ res.writeHead(200);
144
+ res.end();
145
+ return;
146
+ }
147
+ const urlPath = req.url?.split('?')[0] || '/';
148
+ if (req.method === 'GET' && urlPath === '/api/projects') {
149
+ try {
150
+ const projects = getProjects(rootPath);
151
+ res.writeHead(200, { 'Content-Type': 'application/json' });
152
+ res.end(JSON.stringify(projects));
153
+ }
154
+ catch (err) {
155
+ res.writeHead(500, { 'Content-Type': 'application/json' });
156
+ res.end(JSON.stringify({ error: err.message }));
157
+ }
158
+ return;
159
+ }
160
+ const dataMatch = urlPath.match(/^\/api\/data\/(.+)$/);
161
+ if (req.method === 'GET' && dataMatch) {
162
+ const projectId = decodeURIComponent(dataMatch[1]);
163
+ const project = getProjectById(rootPath, projectId);
164
+ if (!project) {
165
+ res.writeHead(404, { 'Content-Type': 'application/json' });
166
+ res.end(JSON.stringify({ error: 'Project not found' }));
167
+ return;
168
+ }
169
+ try {
170
+ const { opted } = loadProjectWordsRaw(project);
171
+ const words = flattenWords(opted);
172
+ const autoSelected = [];
173
+ words.forEach((w, i) => {
174
+ if (w.opt === 'del')
175
+ autoSelected.push(i);
176
+ });
177
+ res.writeHead(200, { 'Content-Type': 'application/json' });
178
+ res.end(JSON.stringify({ words, autoSelected }));
179
+ }
180
+ catch (err) {
181
+ res.writeHead(500, { 'Content-Type': 'application/json' });
182
+ res.end(JSON.stringify({ error: err.message }));
183
+ }
184
+ return;
185
+ }
186
+ const videoMatch = urlPath.match(/^\/api\/video\/(.+)$/);
187
+ if (req.method === 'GET' && videoMatch) {
188
+ const projectId = decodeURIComponent(videoMatch[1]);
189
+ const project = getProjectById(rootPath, projectId);
190
+ if (!project) {
191
+ res.writeHead(404);
192
+ res.end('Not Found');
193
+ return;
194
+ }
195
+ const videoPath = findVideoFile(project, rootPath);
196
+ if (!videoPath) {
197
+ res.writeHead(404);
198
+ res.end('Video not found');
199
+ return;
200
+ }
201
+ const stat = fs.statSync(videoPath);
202
+ if (req.headers.range) {
203
+ const range = req.headers.range.replace('bytes=', '').split('-');
204
+ const start = parseInt(range[0], 10);
205
+ const end = range[1] ? parseInt(range[1], 10) : stat.size - 1;
206
+ res.writeHead(206, {
207
+ 'Content-Type': 'video/mp4',
208
+ 'Content-Range': `bytes ${start}-${end}/${stat.size}`,
209
+ 'Accept-Ranges': 'bytes',
210
+ 'Content-Length': end - start + 1,
211
+ });
212
+ fs.createReadStream(videoPath, { start, end }).pipe(res);
213
+ }
214
+ else {
215
+ res.writeHead(200, {
216
+ 'Content-Type': 'video/mp4',
217
+ 'Content-Length': stat.size,
218
+ 'Accept-Ranges': 'bytes',
219
+ });
220
+ fs.createReadStream(videoPath).pipe(res);
221
+ }
222
+ return;
223
+ }
224
+ const cutMatch = urlPath.match(/^\/api\/cut\/(.+)$/);
225
+ if (req.method === 'POST' && cutMatch) {
226
+ const projectId = decodeURIComponent(cutMatch[1]);
227
+ const project = getProjectById(rootPath, projectId);
228
+ if (!project) {
229
+ res.writeHead(404, { 'Content-Type': 'application/json' });
230
+ res.end(JSON.stringify({ success: false, error: 'Project not found' }));
231
+ return;
232
+ }
233
+ let body = '';
234
+ req.on('data', (chunk) => (body += chunk));
235
+ req.on('end', () => {
236
+ try {
237
+ const requestPayload = JSON.parse(body);
238
+ const inputPath = findVideoFile(project, rootPath);
239
+ if (!inputPath) {
240
+ res.writeHead(500, { 'Content-Type': 'application/json' });
241
+ res.end(JSON.stringify({ success: false, error: 'No .mp4 found for project' }));
242
+ return;
243
+ }
244
+ const baseName = path.basename(inputPath, '.mp4');
245
+ const outputDir = path.dirname(project.path);
246
+ const outputFile = path.join(outputDir, `${baseName}_cut.mp4`);
247
+ const { opted } = loadProjectWordsRaw(project);
248
+ const normalized = normalizeEditsPayload(requestPayload);
249
+ const deleteSegments = buildDeleteSegmentsFromDeletes(opted, normalized.deletes);
250
+ if (deleteSegments.length === 0) {
251
+ res.writeHead(400, { 'Content-Type': 'application/json' });
252
+ res.end(JSON.stringify({ success: false, error: '删除片段为空,请先选择要删除的内容' }));
253
+ return;
254
+ }
255
+ const editsPath = path.join(project.path, 'edits.json');
256
+ const deletePath = path.join(project.path, 'delete_segments.json');
257
+ fs.writeFileSync(editsPath, JSON.stringify({ deletes: normalized.deletes }, null, 2));
258
+ fs.writeFileSync(deletePath, JSON.stringify(deleteSegments, null, 2));
259
+ console.log(`📝 保存编辑: ${editsPath}`);
260
+ console.log(`📝 保存 ${deleteSegments.length} 个删除片段: ${deletePath}`);
261
+ const cutResult = cutVideo(inputPath, deleteSegments, outputFile, project.path);
262
+ const originalDuration = cutResult.originalDuration;
263
+ const newDuration = cutResult.newDuration;
264
+ const deletedDuration = originalDuration - newDuration;
265
+ const savedPercent = ((deletedDuration / originalDuration) * 100).toFixed(1);
266
+ let subtitleOutput = null;
267
+ let srtPath = null;
268
+ if (normalized.burnSubtitle) {
269
+ const subtitles = buildSubtitlesFromEditedOpted(opted, cutResult.audioOffset, cutResult.keepSegments);
270
+ srtPath = path.join(outputDir, `${baseName}_cut.srt`);
271
+ fs.writeFileSync(srtPath, generateSrt(subtitles));
272
+ subtitleOutput = path.join(outputDir, `${baseName}_cut_字幕.mp4`);
273
+ burnSubtitles(outputFile, srtPath, subtitleOutput);
274
+ }
275
+ res.writeHead(200, { 'Content-Type': 'application/json' });
276
+ res.end(JSON.stringify({
277
+ success: true,
278
+ output: outputFile,
279
+ subtitleOutput,
280
+ srtPath,
281
+ editsPath,
282
+ deletePath,
283
+ originalDuration: originalDuration.toFixed(2),
284
+ newDuration: newDuration.toFixed(2),
285
+ deletedDuration: deletedDuration.toFixed(2),
286
+ savedPercent,
287
+ message: normalized.burnSubtitle
288
+ ? `剪辑+烧录完成: ${subtitleOutput || outputFile}`
289
+ : `剪辑完成: ${outputFile}`,
290
+ }));
291
+ }
292
+ catch (err) {
293
+ console.error('❌ 剪辑失败:', err.message);
294
+ res.writeHead(500, { 'Content-Type': 'application/json' });
295
+ res.end(JSON.stringify({ success: false, error: err.message }));
296
+ }
297
+ });
298
+ return;
299
+ }
300
+ if (req.method === 'GET' && (urlPath === '/' || urlPath === '/index.html')) {
301
+ const indexPath = path.join(staticDir, 'index.html');
302
+ if (!fs.existsSync(indexPath)) {
303
+ res.writeHead(404);
304
+ res.end('Static files not built. Run: npm run build:ui');
305
+ return;
306
+ }
307
+ const stat = fs.statSync(indexPath);
308
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Content-Length': stat.size });
309
+ fs.createReadStream(indexPath).pipe(res);
310
+ return;
311
+ }
312
+ const assetMatch = urlPath.match(/^\/assets\/(.+)$/);
313
+ if (req.method === 'GET' && assetMatch) {
314
+ const assetPath = path.join(staticDir, 'assets', assetMatch[1]);
315
+ if (!fs.existsSync(assetPath)) {
316
+ res.writeHead(404);
317
+ res.end('Not Found');
318
+ return;
319
+ }
320
+ const ext = path.extname(assetPath).toLowerCase();
321
+ const mime = MIME_TYPES[ext] || 'application/octet-stream';
322
+ const stat = fs.statSync(assetPath);
323
+ res.writeHead(200, { 'Content-Type': mime, 'Content-Length': stat.size });
324
+ fs.createReadStream(assetPath).pipe(res);
325
+ return;
326
+ }
327
+ res.writeHead(404);
328
+ res.end('Not Found');
329
+ });
330
+ server.listen(port, () => {
331
+ const projects = getProjects(rootPath);
332
+ console.log(`
333
+ 🎬 审核服务器已启动
334
+ 📍 地址: http://localhost:${port}
335
+ 📂 根路径: ${rootPath}
336
+ 📋 项目数: ${projects.length}
337
+
338
+ 操作说明:
339
+ 1. 打开网页,选择项目 Tab
340
+ 2. 审核选择要删除的片段,点击「🎬 执行剪辑」
341
+ `);
342
+ });
343
+ }
344
+ //# sourceMappingURL=review-server.js.map