vidpipe 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 (213) hide show
  1. package/README.md +243 -0
  2. package/assets/fonts/Montserrat-Bold.ttf +0 -0
  3. package/assets/fonts/Montserrat-Regular.ttf +0 -0
  4. package/assets/fonts/OFL.txt +93 -0
  5. package/dist/__tests__/agents.test.d.ts +2 -0
  6. package/dist/__tests__/agents.test.d.ts.map +1 -0
  7. package/dist/__tests__/agents.test.js +434 -0
  8. package/dist/__tests__/agents.test.js.map +1 -0
  9. package/dist/__tests__/aspectRatio.test.d.ts +2 -0
  10. package/dist/__tests__/aspectRatio.test.d.ts.map +1 -0
  11. package/dist/__tests__/aspectRatio.test.js +406 -0
  12. package/dist/__tests__/aspectRatio.test.js.map +1 -0
  13. package/dist/__tests__/captionGenerator.test.d.ts +2 -0
  14. package/dist/__tests__/captionGenerator.test.d.ts.map +1 -0
  15. package/dist/__tests__/captionGenerator.test.js +435 -0
  16. package/dist/__tests__/captionGenerator.test.js.map +1 -0
  17. package/dist/__tests__/config.test.d.ts +2 -0
  18. package/dist/__tests__/config.test.d.ts.map +1 -0
  19. package/dist/__tests__/config.test.js +81 -0
  20. package/dist/__tests__/config.test.js.map +1 -0
  21. package/dist/__tests__/faceDetection.test.d.ts +2 -0
  22. package/dist/__tests__/faceDetection.test.d.ts.map +1 -0
  23. package/dist/__tests__/faceDetection.test.js +372 -0
  24. package/dist/__tests__/faceDetection.test.js.map +1 -0
  25. package/dist/__tests__/ffmpegTools.test.d.ts +2 -0
  26. package/dist/__tests__/ffmpegTools.test.d.ts.map +1 -0
  27. package/dist/__tests__/ffmpegTools.test.js +464 -0
  28. package/dist/__tests__/ffmpegTools.test.js.map +1 -0
  29. package/dist/__tests__/integration/captionBurn.test.d.ts +2 -0
  30. package/dist/__tests__/integration/captionBurn.test.d.ts.map +1 -0
  31. package/dist/__tests__/integration/captionBurn.test.js +103 -0
  32. package/dist/__tests__/integration/captionBurn.test.js.map +1 -0
  33. package/dist/__tests__/integration/clipComposite.test.d.ts +2 -0
  34. package/dist/__tests__/integration/clipComposite.test.d.ts.map +1 -0
  35. package/dist/__tests__/integration/clipComposite.test.js +56 -0
  36. package/dist/__tests__/integration/clipComposite.test.js.map +1 -0
  37. package/dist/__tests__/integration/faceDetection.test.d.ts +2 -0
  38. package/dist/__tests__/integration/faceDetection.test.d.ts.map +1 -0
  39. package/dist/__tests__/integration/faceDetection.test.js +85 -0
  40. package/dist/__tests__/integration/faceDetection.test.js.map +1 -0
  41. package/dist/__tests__/integration/ffmpegPipeline.test.d.ts +2 -0
  42. package/dist/__tests__/integration/ffmpegPipeline.test.d.ts.map +1 -0
  43. package/dist/__tests__/integration/ffmpegPipeline.test.js +88 -0
  44. package/dist/__tests__/integration/ffmpegPipeline.test.js.map +1 -0
  45. package/dist/__tests__/integration/fixture.d.ts +19 -0
  46. package/dist/__tests__/integration/fixture.d.ts.map +1 -0
  47. package/dist/__tests__/integration/fixture.js +112 -0
  48. package/dist/__tests__/integration/fixture.js.map +1 -0
  49. package/dist/__tests__/integration/fixture.test.d.ts +2 -0
  50. package/dist/__tests__/integration/fixture.test.d.ts.map +1 -0
  51. package/dist/__tests__/integration/fixture.test.js +27 -0
  52. package/dist/__tests__/integration/fixture.test.js.map +1 -0
  53. package/dist/__tests__/integration/realCaptions.test.d.ts +2 -0
  54. package/dist/__tests__/integration/realCaptions.test.d.ts.map +1 -0
  55. package/dist/__tests__/integration/realCaptions.test.js +226 -0
  56. package/dist/__tests__/integration/realCaptions.test.js.map +1 -0
  57. package/dist/__tests__/integration/realPipeline.test.d.ts +2 -0
  58. package/dist/__tests__/integration/realPipeline.test.d.ts.map +1 -0
  59. package/dist/__tests__/integration/realPipeline.test.js +210 -0
  60. package/dist/__tests__/integration/realPipeline.test.js.map +1 -0
  61. package/dist/__tests__/integration/silenceRemoval.test.d.ts +2 -0
  62. package/dist/__tests__/integration/silenceRemoval.test.d.ts.map +1 -0
  63. package/dist/__tests__/integration/silenceRemoval.test.js +93 -0
  64. package/dist/__tests__/integration/silenceRemoval.test.js.map +1 -0
  65. package/dist/__tests__/pipeline.test.d.ts +2 -0
  66. package/dist/__tests__/pipeline.test.d.ts.map +1 -0
  67. package/dist/__tests__/pipeline.test.js +434 -0
  68. package/dist/__tests__/pipeline.test.js.map +1 -0
  69. package/dist/__tests__/services.test.d.ts +2 -0
  70. package/dist/__tests__/services.test.d.ts.map +1 -0
  71. package/dist/__tests__/services.test.js +655 -0
  72. package/dist/__tests__/services.test.js.map +1 -0
  73. package/dist/__tests__/silenceRemoval.test.d.ts +2 -0
  74. package/dist/__tests__/silenceRemoval.test.d.ts.map +1 -0
  75. package/dist/__tests__/silenceRemoval.test.js +266 -0
  76. package/dist/__tests__/silenceRemoval.test.js.map +1 -0
  77. package/dist/__tests__/singlePassEdit.test.d.ts +2 -0
  78. package/dist/__tests__/singlePassEdit.test.d.ts.map +1 -0
  79. package/dist/__tests__/singlePassEdit.test.js +321 -0
  80. package/dist/__tests__/singlePassEdit.test.js.map +1 -0
  81. package/dist/__tests__/smoke.test.d.ts +2 -0
  82. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  83. package/dist/__tests__/smoke.test.js +8 -0
  84. package/dist/__tests__/smoke.test.js.map +1 -0
  85. package/dist/__tests__/utilities.test.d.ts +2 -0
  86. package/dist/__tests__/utilities.test.d.ts.map +1 -0
  87. package/dist/__tests__/utilities.test.js +268 -0
  88. package/dist/__tests__/utilities.test.js.map +1 -0
  89. package/dist/agents/BaseAgent.d.ts +52 -0
  90. package/dist/agents/BaseAgent.d.ts.map +1 -0
  91. package/dist/agents/BaseAgent.js +108 -0
  92. package/dist/agents/BaseAgent.js.map +1 -0
  93. package/dist/agents/BlogAgent.d.ts +3 -0
  94. package/dist/agents/BlogAgent.d.ts.map +1 -0
  95. package/dist/agents/BlogAgent.js +163 -0
  96. package/dist/agents/BlogAgent.js.map +1 -0
  97. package/dist/agents/ChapterAgent.d.ts +11 -0
  98. package/dist/agents/ChapterAgent.d.ts.map +1 -0
  99. package/dist/agents/ChapterAgent.js +191 -0
  100. package/dist/agents/ChapterAgent.js.map +1 -0
  101. package/dist/agents/MediumVideoAgent.d.ts +3 -0
  102. package/dist/agents/MediumVideoAgent.d.ts.map +1 -0
  103. package/dist/agents/MediumVideoAgent.js +219 -0
  104. package/dist/agents/MediumVideoAgent.js.map +1 -0
  105. package/dist/agents/ShortsAgent.d.ts +3 -0
  106. package/dist/agents/ShortsAgent.d.ts.map +1 -0
  107. package/dist/agents/ShortsAgent.js +243 -0
  108. package/dist/agents/ShortsAgent.js.map +1 -0
  109. package/dist/agents/SilenceRemovalAgent.d.ts +9 -0
  110. package/dist/agents/SilenceRemovalAgent.d.ts.map +1 -0
  111. package/dist/agents/SilenceRemovalAgent.js +208 -0
  112. package/dist/agents/SilenceRemovalAgent.js.map +1 -0
  113. package/dist/agents/SocialMediaAgent.d.ts +4 -0
  114. package/dist/agents/SocialMediaAgent.d.ts.map +1 -0
  115. package/dist/agents/SocialMediaAgent.js +248 -0
  116. package/dist/agents/SocialMediaAgent.js.map +1 -0
  117. package/dist/agents/SummaryAgent.d.ts +11 -0
  118. package/dist/agents/SummaryAgent.d.ts.map +1 -0
  119. package/dist/agents/SummaryAgent.js +333 -0
  120. package/dist/agents/SummaryAgent.js.map +1 -0
  121. package/dist/config/brand.d.ts +29 -0
  122. package/dist/config/brand.d.ts.map +1 -0
  123. package/dist/config/brand.js +83 -0
  124. package/dist/config/brand.js.map +1 -0
  125. package/dist/config/environment.d.ts +36 -0
  126. package/dist/config/environment.d.ts.map +1 -0
  127. package/dist/config/environment.js +44 -0
  128. package/dist/config/environment.js.map +1 -0
  129. package/dist/config/logger.d.ts +5 -0
  130. package/dist/config/logger.d.ts.map +1 -0
  131. package/dist/config/logger.js +13 -0
  132. package/dist/config/logger.js.map +1 -0
  133. package/dist/index.d.ts +2 -0
  134. package/dist/index.d.ts.map +1 -0
  135. package/dist/index.js +135 -0
  136. package/dist/index.js.map +1 -0
  137. package/dist/pipeline.d.ts +57 -0
  138. package/dist/pipeline.d.ts.map +1 -0
  139. package/dist/pipeline.js +287 -0
  140. package/dist/pipeline.js.map +1 -0
  141. package/dist/services/captionGeneration.d.ts +7 -0
  142. package/dist/services/captionGeneration.d.ts.map +1 -0
  143. package/dist/services/captionGeneration.js +29 -0
  144. package/dist/services/captionGeneration.js.map +1 -0
  145. package/dist/services/fileWatcher.d.ts +19 -0
  146. package/dist/services/fileWatcher.d.ts.map +1 -0
  147. package/dist/services/fileWatcher.js +120 -0
  148. package/dist/services/fileWatcher.js.map +1 -0
  149. package/dist/services/gitOperations.d.ts +3 -0
  150. package/dist/services/gitOperations.d.ts.map +1 -0
  151. package/dist/services/gitOperations.js +43 -0
  152. package/dist/services/gitOperations.js.map +1 -0
  153. package/dist/services/socialPosting.d.ts +38 -0
  154. package/dist/services/socialPosting.d.ts.map +1 -0
  155. package/dist/services/socialPosting.js +102 -0
  156. package/dist/services/socialPosting.js.map +1 -0
  157. package/dist/services/transcription.d.ts +3 -0
  158. package/dist/services/transcription.d.ts.map +1 -0
  159. package/dist/services/transcription.js +100 -0
  160. package/dist/services/transcription.js.map +1 -0
  161. package/dist/services/videoIngestion.d.ts +3 -0
  162. package/dist/services/videoIngestion.d.ts.map +1 -0
  163. package/dist/services/videoIngestion.js +103 -0
  164. package/dist/services/videoIngestion.js.map +1 -0
  165. package/dist/tools/captions/captionGenerator.d.ts +84 -0
  166. package/dist/tools/captions/captionGenerator.d.ts.map +1 -0
  167. package/dist/tools/captions/captionGenerator.js +390 -0
  168. package/dist/tools/captions/captionGenerator.js.map +1 -0
  169. package/dist/tools/ffmpeg/aspectRatio.d.ts +101 -0
  170. package/dist/tools/ffmpeg/aspectRatio.d.ts.map +1 -0
  171. package/dist/tools/ffmpeg/aspectRatio.js +338 -0
  172. package/dist/tools/ffmpeg/aspectRatio.js.map +1 -0
  173. package/dist/tools/ffmpeg/audioExtraction.d.ts +16 -0
  174. package/dist/tools/ffmpeg/audioExtraction.d.ts.map +1 -0
  175. package/dist/tools/ffmpeg/audioExtraction.js +86 -0
  176. package/dist/tools/ffmpeg/audioExtraction.js.map +1 -0
  177. package/dist/tools/ffmpeg/captionBurning.d.ts +8 -0
  178. package/dist/tools/ffmpeg/captionBurning.d.ts.map +1 -0
  179. package/dist/tools/ffmpeg/captionBurning.js +71 -0
  180. package/dist/tools/ffmpeg/captionBurning.js.map +1 -0
  181. package/dist/tools/ffmpeg/clipExtraction.d.ts +23 -0
  182. package/dist/tools/ffmpeg/clipExtraction.d.ts.map +1 -0
  183. package/dist/tools/ffmpeg/clipExtraction.js +178 -0
  184. package/dist/tools/ffmpeg/clipExtraction.js.map +1 -0
  185. package/dist/tools/ffmpeg/faceDetection.d.ts +127 -0
  186. package/dist/tools/ffmpeg/faceDetection.d.ts.map +1 -0
  187. package/dist/tools/ffmpeg/faceDetection.js +500 -0
  188. package/dist/tools/ffmpeg/faceDetection.js.map +1 -0
  189. package/dist/tools/ffmpeg/frameCapture.d.ts +10 -0
  190. package/dist/tools/ffmpeg/frameCapture.d.ts.map +1 -0
  191. package/dist/tools/ffmpeg/frameCapture.js +48 -0
  192. package/dist/tools/ffmpeg/frameCapture.js.map +1 -0
  193. package/dist/tools/ffmpeg/silenceDetection.d.ts +10 -0
  194. package/dist/tools/ffmpeg/silenceDetection.d.ts.map +1 -0
  195. package/dist/tools/ffmpeg/silenceDetection.js +55 -0
  196. package/dist/tools/ffmpeg/silenceDetection.js.map +1 -0
  197. package/dist/tools/ffmpeg/singlePassEdit.d.ts +25 -0
  198. package/dist/tools/ffmpeg/singlePassEdit.d.ts.map +1 -0
  199. package/dist/tools/ffmpeg/singlePassEdit.js +123 -0
  200. package/dist/tools/ffmpeg/singlePassEdit.js.map +1 -0
  201. package/dist/tools/search/exaClient.d.ts +8 -0
  202. package/dist/tools/search/exaClient.d.ts.map +1 -0
  203. package/dist/tools/search/exaClient.js +38 -0
  204. package/dist/tools/search/exaClient.js.map +1 -0
  205. package/dist/tools/whisper/whisperClient.d.ts +3 -0
  206. package/dist/tools/whisper/whisperClient.d.ts.map +1 -0
  207. package/dist/tools/whisper/whisperClient.js +77 -0
  208. package/dist/tools/whisper/whisperClient.js.map +1 -0
  209. package/dist/types/index.d.ts +305 -0
  210. package/dist/types/index.d.ts.map +1 -0
  211. package/dist/types/index.js +44 -0
  212. package/dist/types/index.js.map +1 -0
  213. package/package.json +63 -0
@@ -0,0 +1,338 @@
1
+ import { execFile } from 'child_process';
2
+ import { promises as fs } from 'fs';
3
+ import pathMod from 'path';
4
+ import logger from '../../config/logger';
5
+ import { detectWebcamRegion, getVideoResolution } from './faceDetection';
6
+ const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
7
+ /**
8
+ * Maps each platform to its preferred aspect ratio.
9
+ * Multiple platforms may share a ratio (e.g. TikTok + Reels both use 9:16),
10
+ * which lets {@link generatePlatformVariants} deduplicate encodes.
11
+ */
12
+ export const PLATFORM_RATIOS = {
13
+ 'tiktok': '9:16',
14
+ 'youtube-shorts': '9:16',
15
+ 'instagram-reels': '9:16',
16
+ 'instagram-feed': '4:5',
17
+ 'linkedin': '1:1',
18
+ 'youtube': '16:9',
19
+ 'twitter': '1:1',
20
+ };
21
+ /**
22
+ * Canonical pixel dimensions for each aspect ratio.
23
+ * Width is always 1080 px for non-landscape ratios (the standard vertical
24
+ * video width); landscape stays at 1920×1080 for full HD.
25
+ */
26
+ export const DIMENSIONS = {
27
+ '16:9': { width: 1920, height: 1080 },
28
+ '9:16': { width: 1080, height: 1920 },
29
+ '1:1': { width: 1080, height: 1080 },
30
+ '4:5': { width: 1080, height: 1350 },
31
+ };
32
+ // ── Helpers ──────────────────────────────────────────────────────────────────
33
+ /**
34
+ * Build the FFmpeg `-vf` filter string for a simple center-crop conversion.
35
+ *
36
+ * This is the **fallback** used when smart layout (webcam detection + split-screen)
37
+ * is unavailable. It center-crops the source frame to the target aspect ratio,
38
+ * discarding content on the sides (or top/bottom).
39
+ *
40
+ * **Letterbox mode**: instead of cropping, scales the video to fit inside the
41
+ * target dimensions and pads the remaining space with black bars. Useful when
42
+ * you don't want to lose any content (e.g. screen recordings with important
43
+ * edges).
44
+ *
45
+ * **Crop formulas** assume a 16:9 landscape source. `ih` = input height,
46
+ * `iw` = input width. We compute the crop width from the height to maintain
47
+ * the target ratio, then center the crop horizontally.
48
+ *
49
+ * @param targetRatio - The desired output aspect ratio
50
+ * @param letterbox - If true, pad with black bars instead of cropping
51
+ * @returns An FFmpeg `-vf` filter string
52
+ */
53
+ function buildCropFilter(targetRatio, letterbox) {
54
+ if (letterbox) {
55
+ const { width, height } = DIMENSIONS[targetRatio];
56
+ // Scale to fit within target dimensions, then pad with black bars
57
+ return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:black`;
58
+ }
59
+ switch (targetRatio) {
60
+ case '9:16':
61
+ // Center-crop landscape to portrait: crop width = ih*9/16, keep full height
62
+ return 'crop=ih*9/16:ih:(iw-ih*9/16)/2:0,scale=1080:1920';
63
+ case '1:1':
64
+ // Center-crop to square: use height as the dimension (smaller axis for 16:9)
65
+ return 'crop=ih:ih:(iw-ih)/2:0,scale=1080:1080';
66
+ case '4:5':
67
+ // Center-crop landscape to 4:5: crop width = ih*4/5, keep full height
68
+ return 'crop=ih*4/5:ih:(iw-ih*4/5)/2:0,scale=1080:1350';
69
+ case '16:9':
70
+ // Same ratio — just ensure standard dimensions
71
+ return 'scale=1920:1080';
72
+ }
73
+ }
74
+ // ── Public API ───────────────────────────────────────────────────────────────
75
+ /**
76
+ * Convert a video's aspect ratio using FFmpeg center-crop.
77
+ *
78
+ * - 16:9 → 9:16: crops the center column to portrait
79
+ * - 16:9 → 1:1: crops to a center square
80
+ * - Same ratio: stream-copies without re-encoding
81
+ *
82
+ * @returns The output path on success
83
+ */
84
+ export async function convertAspectRatio(inputPath, outputPath, targetRatio, options = {}) {
85
+ const outputDir = pathMod.dirname(outputPath);
86
+ await fs.mkdir(outputDir, { recursive: true });
87
+ const sourceRatio = '16:9'; // our videos are always landscape
88
+ // Same ratio — stream copy
89
+ if (sourceRatio === targetRatio && !options.letterbox) {
90
+ logger.info(`Aspect ratio already ${targetRatio}, copying → ${outputPath}`);
91
+ await fs.copyFile(inputPath, outputPath);
92
+ return outputPath;
93
+ }
94
+ const vf = buildCropFilter(targetRatio, options.letterbox ?? false);
95
+ logger.info(`Converting aspect ratio to ${targetRatio} (filter: ${vf}) → ${outputPath}`);
96
+ const args = [
97
+ '-y',
98
+ '-i', inputPath,
99
+ '-vf', vf,
100
+ '-c:v', 'libx264',
101
+ '-preset', 'ultrafast',
102
+ '-crf', '23',
103
+ '-c:a', 'copy',
104
+ '-threads', '4',
105
+ outputPath,
106
+ ];
107
+ return new Promise((resolve, reject) => {
108
+ execFile(ffmpegPath, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
109
+ if (error) {
110
+ logger.error(`Aspect ratio conversion failed: ${stderr || error.message}`);
111
+ reject(new Error(`Aspect ratio conversion failed: ${stderr || error.message}`));
112
+ return;
113
+ }
114
+ logger.info(`Aspect ratio conversion complete: ${outputPath}`);
115
+ resolve(outputPath);
116
+ });
117
+ });
118
+ }
119
+ /**
120
+ * Shared smart conversion: detects a webcam overlay in the source video and
121
+ * builds a **split-screen** layout (screen on top, webcam on bottom).
122
+ *
123
+ * ### Why split-screen?
124
+ * Screen recordings with a webcam overlay (e.g. top-right corner) waste space
125
+ * when naively center-cropped to portrait/square. The split-screen approach
126
+ * gives the screen content and webcam each their own dedicated panel, making
127
+ * both fully visible in a narrow frame.
128
+ *
129
+ * ### Algorithm
130
+ * 1. Run {@link detectWebcamRegion} to find the webcam bounding box.
131
+ * 2. **Screen crop**: exclude the webcam columns so only the screen content
132
+ * remains, then scale to `targetW × screenH` (letterboxing if needed).
133
+ * 3. **Webcam crop**: aspect-ratio-match the webcam region to `targetW × camH`.
134
+ * If the webcam is wider than the target, we keep full height and
135
+ * center-crop width; if taller, we keep full width and center-crop height.
136
+ * This ensures the webcam fills its panel edge-to-edge with **no black bars**.
137
+ * 4. **vstack**: vertically stack `[screen][cam]` into the final frame.
138
+ *
139
+ * Falls back to simple center-crop ({@link buildCropFilter}) if no webcam is
140
+ * detected.
141
+ *
142
+ * @param inputPath - Source video (assumed 16:9 landscape with optional webcam overlay)
143
+ * @param outputPath - Destination path for the converted video
144
+ * @param config - Layout geometry (see {@link SmartLayoutConfig})
145
+ * @returns The output path on success
146
+ */
147
+ async function convertWithSmartLayout(inputPath, outputPath, config) {
148
+ const { label, targetW, screenH, camH, fallbackRatio } = config;
149
+ const outputDir = pathMod.dirname(outputPath);
150
+ await fs.mkdir(outputDir, { recursive: true });
151
+ const webcam = await detectWebcamRegion(inputPath);
152
+ if (!webcam) {
153
+ logger.info(`[${label}] No webcam found, falling back to center-crop`);
154
+ return convertAspectRatio(inputPath, outputPath, fallbackRatio);
155
+ }
156
+ const resolution = await getVideoResolution(inputPath);
157
+ // Determine screen crop region (exclude webcam area using detected bounds)
158
+ let screenCropX;
159
+ let screenCropW;
160
+ if (webcam.position === 'top-right' || webcam.position === 'bottom-right') {
161
+ screenCropX = 0;
162
+ screenCropW = webcam.x;
163
+ }
164
+ else {
165
+ screenCropX = webcam.x + webcam.width;
166
+ screenCropW = Math.max(0, resolution.width - screenCropX);
167
+ }
168
+ // Crop webcam to match target bottom-section aspect ratio, then scale to fill
169
+ const targetAR = targetW / camH;
170
+ const webcamAR = webcam.width / webcam.height;
171
+ let faceX, faceY, faceW, faceH;
172
+ if (webcamAR > targetAR) {
173
+ // Webcam wider than target: keep full height, center-crop width
174
+ faceH = webcam.height;
175
+ faceW = Math.round(faceH * targetAR);
176
+ faceX = webcam.x + Math.round((webcam.width - faceW) / 2);
177
+ faceY = webcam.y;
178
+ }
179
+ else {
180
+ // Webcam taller than target: keep full width, center-crop height
181
+ faceW = webcam.width;
182
+ faceH = Math.round(faceW / targetAR);
183
+ faceX = webcam.x;
184
+ faceY = webcam.y + Math.round((webcam.height - faceH) / 2);
185
+ }
186
+ const filterComplex = [
187
+ `[0:v]crop=${screenCropW}:ih:${screenCropX}:0,scale=${targetW}:${screenH}:force_original_aspect_ratio=decrease,` +
188
+ `pad=${targetW}:${screenH}:(ow-iw)/2:(oh-ih)/2:black[screen]`,
189
+ `[0:v]crop=${faceW}:${faceH}:${faceX}:${faceY},scale=${targetW}:${camH}[cam]`,
190
+ '[screen][cam]vstack[out]',
191
+ ].join(';');
192
+ logger.info(`[${label}] Split-screen layout: webcam at ${webcam.position} → ${outputPath}`);
193
+ const args = [
194
+ '-y',
195
+ '-i', inputPath,
196
+ '-filter_complex', filterComplex,
197
+ '-map', '[out]',
198
+ '-map', '0:a',
199
+ '-c:v', 'libx264',
200
+ '-preset', 'ultrafast',
201
+ '-crf', '23',
202
+ '-c:a', 'aac',
203
+ '-b:a', '128k',
204
+ '-threads', '4',
205
+ outputPath,
206
+ ];
207
+ return new Promise((resolve, reject) => {
208
+ execFile(ffmpegPath, args, { maxBuffer: 10 * 1024 * 1024 }, (error, _stdout, stderr) => {
209
+ if (error) {
210
+ logger.error(`[${label}] FFmpeg failed: ${stderr || error.message}`);
211
+ reject(new Error(`${label} conversion failed: ${stderr || error.message}`));
212
+ return;
213
+ }
214
+ logger.info(`[${label}] Complete: ${outputPath}`);
215
+ resolve(outputPath);
216
+ });
217
+ });
218
+ }
219
+ /**
220
+ * Smart portrait (9:16) conversion → 1080×1920.
221
+ *
222
+ * Screen panel: 1080×1248 (65%), Webcam panel: 1080×672 (35%).
223
+ * Total: 1080×1920 — standard TikTok / Reels / Shorts dimensions.
224
+ *
225
+ * Falls back to center-crop 9:16 if no webcam is detected.
226
+ *
227
+ * @param inputPath - Source landscape video
228
+ * @param outputPath - Destination path for the portrait video
229
+ */
230
+ export async function convertToPortraitSmart(inputPath, outputPath) {
231
+ return convertWithSmartLayout(inputPath, outputPath, {
232
+ label: 'SmartPortrait',
233
+ targetW: 1080,
234
+ screenH: 1248,
235
+ camH: 672,
236
+ fallbackRatio: '9:16',
237
+ });
238
+ }
239
+ /**
240
+ * Smart square (1:1) conversion → 1080×1080.
241
+ *
242
+ * Screen panel: 1080×700 (65%), Webcam panel: 1080×380 (35%).
243
+ * Total: 1080×1080 — standard LinkedIn / Twitter square format.
244
+ *
245
+ * Falls back to center-crop 1:1 if no webcam is detected.
246
+ *
247
+ * @param inputPath - Source landscape video
248
+ * @param outputPath - Destination path for the square video
249
+ */
250
+ export async function convertToSquareSmart(inputPath, outputPath) {
251
+ return convertWithSmartLayout(inputPath, outputPath, {
252
+ label: 'SmartSquare',
253
+ targetW: 1080,
254
+ screenH: 700,
255
+ camH: 380,
256
+ fallbackRatio: '1:1',
257
+ });
258
+ }
259
+ /**
260
+ * Smart feed (4:5) conversion → 1080×1350.
261
+ *
262
+ * Screen panel: 1080×878 (65%), Webcam panel: 1080×472 (35%).
263
+ * Total: 1080×1350 — Instagram feed's preferred tall format.
264
+ *
265
+ * Falls back to center-crop 4:5 if no webcam is detected.
266
+ *
267
+ * @param inputPath - Source landscape video
268
+ * @param outputPath - Destination path for the 4:5 video
269
+ */
270
+ export async function convertToFeedSmart(inputPath, outputPath) {
271
+ return convertWithSmartLayout(inputPath, outputPath, {
272
+ label: 'SmartFeed',
273
+ targetW: 1080,
274
+ screenH: 878,
275
+ camH: 472,
276
+ fallbackRatio: '4:5',
277
+ });
278
+ }
279
+ /**
280
+ * Generate platform-specific aspect-ratio variants of a short clip.
281
+ *
282
+ * ### Routing logic
283
+ * 1. Maps each requested platform to its aspect ratio via {@link PLATFORM_RATIOS}.
284
+ * 2. **Deduplicates by ratio** — if TikTok and Reels both need 9:16, only one
285
+ * encode is performed and both platforms reference the same output file.
286
+ * 3. Skips 16:9 entirely since the source is already landscape.
287
+ * 4. Routes each ratio to its smart converter (portrait / square / feed) for
288
+ * split-screen layout, falling back to {@link convertAspectRatio} for any
289
+ * ratio without a smart converter.
290
+ *
291
+ * @param inputPath - Source video (16:9 landscape)
292
+ * @param outputDir - Directory to write variant files into
293
+ * @param slug - Base filename slug (e.g. "my-video-short-1")
294
+ * @param platforms - Platforms to generate for (default: tiktok + linkedin)
295
+ * @returns Array of variant metadata (one entry per platform, deduplicated files)
296
+ */
297
+ export async function generatePlatformVariants(inputPath, outputDir, slug, platforms = ['tiktok', 'linkedin']) {
298
+ await fs.mkdir(outputDir, { recursive: true });
299
+ // Deduplicate by aspect ratio to avoid redundant encodes
300
+ const ratioMap = new Map();
301
+ for (const p of platforms) {
302
+ const ratio = PLATFORM_RATIOS[p];
303
+ if (ratio === '16:9')
304
+ continue; // skip — original is already 16:9
305
+ const list = ratioMap.get(ratio) ?? [];
306
+ list.push(p);
307
+ ratioMap.set(ratio, list);
308
+ }
309
+ const variants = [];
310
+ for (const [ratio, associatedPlatforms] of ratioMap) {
311
+ const suffix = ratio === '9:16' ? 'portrait' : ratio === '4:5' ? 'feed' : 'square';
312
+ const outPath = pathMod.join(outputDir, `${slug}-${suffix}.mp4`);
313
+ try {
314
+ if (ratio === '9:16') {
315
+ await convertToPortraitSmart(inputPath, outPath);
316
+ }
317
+ else if (ratio === '1:1') {
318
+ await convertToSquareSmart(inputPath, outPath);
319
+ }
320
+ else if (ratio === '4:5') {
321
+ await convertToFeedSmart(inputPath, outPath);
322
+ }
323
+ else {
324
+ await convertAspectRatio(inputPath, outPath, ratio);
325
+ }
326
+ const dims = DIMENSIONS[ratio];
327
+ for (const p of associatedPlatforms) {
328
+ variants.push({ platform: p, aspectRatio: ratio, path: outPath, width: dims.width, height: dims.height });
329
+ }
330
+ }
331
+ catch (err) {
332
+ const message = err instanceof Error ? err.message : String(err);
333
+ logger.warn(`Skipping ${ratio} variant for ${slug}: ${message}`);
334
+ }
335
+ }
336
+ return variants;
337
+ }
338
+ //# sourceMappingURL=aspectRatio.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"aspectRatio.js","sourceRoot":"","sources":["../../../src/tools/ffmpeg/aspectRatio.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,OAAO,EAAE,QAAQ,IAAI,EAAE,EAAE,MAAM,IAAI,CAAA;AACnC,OAAO,OAAO,MAAM,MAAM,CAAA;AAC1B,OAAO,MAAM,MAAM,qBAAqB,CAAA;AACxC,OAAO,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAExE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,WAAW,IAAI,QAAQ,CAAA;AAuBtD;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAkC;IAC5D,QAAQ,EAAE,MAAM;IAChB,gBAAgB,EAAE,MAAM;IACxB,iBAAiB,EAAE,MAAM;IACzB,gBAAgB,EAAE,KAAK;IACvB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,MAAM;IACjB,SAAS,EAAE,KAAK;CACjB,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAA2D;IAChF,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACrC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACrC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACpC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;CACrC,CAAA;AAOD,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAS,eAAe,CAAC,WAAwB,EAAE,SAAkB;IACnE,IAAI,SAAS,EAAE,CAAC;QACd,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,UAAU,CAAC,WAAW,CAAC,CAAA;QACjD,kEAAkE;QAClE,OAAO,SAAS,KAAK,IAAI,MAAM,6CAA6C,KAAK,IAAI,MAAM,4BAA4B,CAAA;IACzH,CAAC;IAED,QAAQ,WAAW,EAAE,CAAC;QACpB,KAAK,MAAM;YACT,4EAA4E;YAC5E,OAAO,kDAAkD,CAAA;QAC3D,KAAK,KAAK;YACR,6EAA6E;YAC7E,OAAO,wCAAwC,CAAA;QACjD,KAAK,KAAK;YACR,sEAAsE;YACtE,OAAO,gDAAgD,CAAA;QACzD,KAAK,MAAM;YACT,+CAA+C;YAC/C,OAAO,iBAAiB,CAAA;IAC5B,CAAC;AACH,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,UAAkB,EAClB,WAAwB,EACxB,UAA0B,EAAE;IAE5B,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE9C,MAAM,WAAW,GAAgB,MAAM,CAAA,CAAC,kCAAkC;IAE1E,2BAA2B;IAC3B,IAAI,WAAW,KAAK,WAAW,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACtD,MAAM,CAAC,IAAI,CAAC,wBAAwB,WAAW,eAAe,UAAU,EAAE,CAAC,CAAA;QAC3E,MAAM,EAAE,CAAC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC,CAAA;QACxC,OAAO,UAAU,CAAA;IACnB,CAAC;IAED,MAAM,EAAE,GAAG,eAAe,CAAC,WAAW,EAAE,OAAO,CAAC,SAAS,IAAI,KAAK,CAAC,CAAA;IACnE,MAAM,CAAC,IAAI,CAAC,8BAA8B,WAAW,aAAa,EAAE,OAAO,UAAU,EAAE,CAAC,CAAA;IAExF,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,IAAI,EAAE,SAAS;QACf,KAAK,EAAE,EAAE;QACT,MAAM,EAAE,SAAS;QACjB,SAAS,EAAE,WAAW;QACtB,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,MAAM;QACd,UAAU,EAAE,GAAG;QACf,UAAU;KACX,CAAA;IAED,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC7C,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YACrF,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,CAAC,KAAK,CAAC,mCAAmC,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;gBAC1E,MAAM,CAAC,IAAI,KAAK,CAAC,mCAAmC,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;gBAC/E,OAAM;YACR,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,qCAAqC,UAAU,EAAE,CAAC,CAAA;YAC9D,OAAO,CAAC,UAAU,CAAC,CAAA;QACrB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAgCD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,KAAK,UAAU,sBAAsB,CACnC,SAAiB,EACjB,UAAkB,EAClB,MAAyB;IAEzB,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,GAAG,MAAM,CAAA;IAC/D,MAAM,SAAS,GAAG,OAAO,CAAC,OAAO,CAAC,UAAU,CAAC,CAAA;IAC7C,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE9C,MAAM,MAAM,GAAG,MAAM,kBAAkB,CAAC,SAAS,CAAC,CAAA;IAElD,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,gDAAgD,CAAC,CAAA;QACtE,OAAO,kBAAkB,CAAC,SAAS,EAAE,UAAU,EAAE,aAAa,CAAC,CAAA;IACjE,CAAC;IAED,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,SAAS,CAAC,CAAA;IAEtD,2EAA2E;IAC3E,IAAI,WAAmB,CAAA;IACvB,IAAI,WAAmB,CAAA;IACvB,IAAI,MAAM,CAAC,QAAQ,KAAK,WAAW,IAAI,MAAM,CAAC,QAAQ,KAAK,cAAc,EAAE,CAAC;QAC1E,WAAW,GAAG,CAAC,CAAA;QACf,WAAW,GAAG,MAAM,CAAC,CAAC,CAAA;IACxB,CAAC;SAAM,CAAC;QACN,WAAW,GAAG,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,CAAA;QACrC,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,GAAG,WAAW,CAAC,CAAA;IAC3D,CAAC;IAED,8EAA8E;IAC9E,MAAM,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAA;IAC/B,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,CAAA;IAE7C,IAAI,KAAa,EAAE,KAAa,EAAE,KAAa,EAAE,KAAa,CAAA;IAC9D,IAAI,QAAQ,GAAG,QAAQ,EAAE,CAAC;QACxB,gEAAgE;QAChE,KAAK,GAAG,MAAM,CAAC,MAAM,CAAA;QACrB,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAA;QACpC,KAAK,GAAG,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;QACzD,KAAK,GAAG,MAAM,CAAC,CAAC,CAAA;IAClB,CAAC;SAAM,CAAC;QACN,iEAAiE;QACjE,KAAK,GAAG,MAAM,CAAC,KAAK,CAAA;QACpB,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,QAAQ,CAAC,CAAA;QACpC,KAAK,GAAG,MAAM,CAAC,CAAC,CAAA;QAChB,KAAK,GAAG,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAA;IAC5D,CAAC;IAED,MAAM,aAAa,GAAG;QACpB,aAAa,WAAW,OAAO,WAAW,YAAY,OAAO,IAAI,OAAO,wCAAwC;YAC9G,OAAO,OAAO,IAAI,OAAO,oCAAoC;QAC/D,aAAa,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,KAAK,UAAU,OAAO,IAAI,IAAI,OAAO;QAC7E,0BAA0B;KAC3B,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;IAEX,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,oCAAoC,MAAM,CAAC,QAAQ,MAAM,UAAU,EAAE,CAAC,CAAA;IAE3F,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,IAAI,EAAE,SAAS;QACf,iBAAiB,EAAE,aAAa;QAChC,MAAM,EAAE,OAAO;QACf,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,SAAS;QACjB,SAAS,EAAE,WAAW;QACtB,MAAM,EAAE,IAAI;QACZ,MAAM,EAAE,KAAK;QACb,MAAM,EAAE,MAAM;QACd,UAAU,EAAE,GAAG;QACf,UAAU;KACX,CAAA;IAED,OAAO,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC7C,QAAQ,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;YACrF,IAAI,KAAK,EAAE,CAAC;gBACV,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,oBAAoB,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAA;gBACpE,MAAM,CAAC,IAAI,KAAK,CAAC,GAAG,KAAK,uBAAuB,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAA;gBAC3E,OAAM;YACR,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,IAAI,KAAK,eAAe,UAAU,EAAE,CAAC,CAAA;YACjD,OAAO,CAAC,UAAU,CAAC,CAAA;QACrB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,SAAiB,EACjB,UAAkB;IAElB,OAAO,sBAAsB,CAAC,SAAS,EAAE,UAAU,EAAE;QACnD,KAAK,EAAE,eAAe;QACtB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,IAAI;QACb,IAAI,EAAE,GAAG;QACT,aAAa,EAAE,MAAM;KACtB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,SAAiB,EACjB,UAAkB;IAElB,OAAO,sBAAsB,CAAC,SAAS,EAAE,UAAU,EAAE;QACnD,KAAK,EAAE,aAAa;QACpB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,IAAI,EAAE,GAAG;QACT,aAAa,EAAE,KAAK;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,SAAiB,EACjB,UAAkB;IAElB,OAAO,sBAAsB,CAAC,SAAS,EAAE,UAAU,EAAE;QACnD,KAAK,EAAE,WAAW;QAClB,OAAO,EAAE,IAAI;QACb,OAAO,EAAE,GAAG;QACZ,IAAI,EAAE,GAAG;QACT,aAAa,EAAE,KAAK;KACrB,CAAC,CAAA;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,SAAiB,EACjB,SAAiB,EACjB,IAAY,EACZ,YAAwB,CAAC,QAAQ,EAAE,UAAU,CAAC;IAE9C,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAE9C,yDAAyD;IACzD,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAA;IACnD,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,KAAK,GAAG,eAAe,CAAC,CAAC,CAAC,CAAA;QAChC,IAAI,KAAK,KAAK,MAAM;YAAE,SAAQ,CAAC,kCAAkC;QACjE,MAAM,IAAI,GAAG,QAAQ,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAA;QACtC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACZ,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAC3B,CAAC;IAED,MAAM,QAAQ,GAAoG,EAAE,CAAA;IAEpH,KAAK,MAAM,CAAC,KAAK,EAAE,mBAAmB,CAAC,IAAI,QAAQ,EAAE,CAAC;QACpD,MAAM,MAAM,GAAG,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;QAClF,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,IAAI,IAAI,MAAM,MAAM,CAAC,CAAA;QAEhE,IAAI,CAAC;YACH,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;gBACrB,MAAM,sBAAsB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAClD,CAAC;iBAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;gBAC3B,MAAM,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAChD,CAAC;iBAAM,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;gBAC3B,MAAM,kBAAkB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAC9C,CAAC;iBAAM,CAAC;gBACN,MAAM,kBAAkB,CAAC,SAAS,EAAE,OAAO,EAAE,KAAK,CAAC,CAAA;YACrD,CAAC;YACD,MAAM,IAAI,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;YAC9B,KAAK,MAAM,CAAC,IAAI,mBAAmB,EAAE,CAAC;gBACpC,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAA;YAC3G,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YAChE,MAAM,CAAC,IAAI,CAAC,YAAY,KAAK,gBAAgB,IAAI,KAAK,OAAO,EAAE,CAAC,CAAA;QAClE,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAA;AACjB,CAAC"}
@@ -0,0 +1,16 @@
1
+ export interface ExtractAudioOptions {
2
+ /** Output format: 'mp3' (default, smaller) or 'wav' */
3
+ format?: 'mp3' | 'wav';
4
+ }
5
+ /**
6
+ * Extract audio from a video file to mono MP3 at 64kbps (small enough for Whisper).
7
+ * A 10-minute video produces ~5MB MP3 vs ~115MB WAV.
8
+ */
9
+ export declare function extractAudio(videoPath: string, outputPath: string, options?: ExtractAudioOptions): Promise<string>;
10
+ /**
11
+ * Split an audio file into chunks of approximately `maxChunkSizeMB` each.
12
+ * Uses ffmpeg to split by duration calculated from the file size.
13
+ * Returns an array of chunk file paths.
14
+ */
15
+ export declare function splitAudioIntoChunks(audioPath: string, maxChunkSizeMB?: number): Promise<string[]>;
16
+ //# sourceMappingURL=audioExtraction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioExtraction.d.ts","sourceRoot":"","sources":["../../../src/tools/ffmpeg/audioExtraction.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAClC,uDAAuD;IACvD,MAAM,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC;CACxB;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,MAAM,CAAC,CA4BjB;AAED;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,cAAc,GAAE,MAAW,GAC1B,OAAO,CAAC,MAAM,EAAE,CAAC,CAyCnB"}
@@ -0,0 +1,86 @@
1
+ import ffmpeg from 'fluent-ffmpeg';
2
+ import { promises as fs } from 'fs';
3
+ import path from 'path';
4
+ import logger from '../../config/logger';
5
+ const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
6
+ const ffprobePath = process.env.FFPROBE_PATH || 'ffprobe';
7
+ ffmpeg.setFfmpegPath(ffmpegPath);
8
+ ffmpeg.setFfprobePath(ffprobePath);
9
+ /**
10
+ * Extract audio from a video file to mono MP3 at 64kbps (small enough for Whisper).
11
+ * A 10-minute video produces ~5MB MP3 vs ~115MB WAV.
12
+ */
13
+ export async function extractAudio(videoPath, outputPath, options = {}) {
14
+ const { format = 'mp3' } = options;
15
+ const outputDir = path.dirname(outputPath);
16
+ await fs.mkdir(outputDir, { recursive: true });
17
+ logger.info(`Extracting audio (${format}): ${videoPath} → ${outputPath}`);
18
+ return new Promise((resolve, reject) => {
19
+ const command = ffmpeg(videoPath).noVideo().audioChannels(1);
20
+ if (format === 'mp3') {
21
+ command.audioCodec('libmp3lame').audioBitrate('64k').audioFrequency(16000);
22
+ }
23
+ else {
24
+ command.audioCodec('pcm_s16le').audioFrequency(16000);
25
+ }
26
+ command
27
+ .output(outputPath)
28
+ .on('end', () => {
29
+ logger.info(`Audio extraction complete: ${outputPath}`);
30
+ resolve(outputPath);
31
+ })
32
+ .on('error', (err) => {
33
+ logger.error(`Audio extraction failed: ${err.message}`);
34
+ reject(new Error(`Audio extraction failed: ${err.message}`));
35
+ })
36
+ .run();
37
+ });
38
+ }
39
+ /**
40
+ * Split an audio file into chunks of approximately `maxChunkSizeMB` each.
41
+ * Uses ffmpeg to split by duration calculated from the file size.
42
+ * Returns an array of chunk file paths.
43
+ */
44
+ export async function splitAudioIntoChunks(audioPath, maxChunkSizeMB = 24) {
45
+ const stats = await fs.stat(audioPath);
46
+ const fileSizeMB = stats.size / (1024 * 1024);
47
+ if (fileSizeMB <= maxChunkSizeMB) {
48
+ return [audioPath];
49
+ }
50
+ const duration = await getAudioDuration(audioPath);
51
+ const numChunks = Math.ceil(fileSizeMB / maxChunkSizeMB);
52
+ const chunkDuration = duration / numChunks;
53
+ const ext = path.extname(audioPath);
54
+ const base = audioPath.slice(0, -ext.length);
55
+ const chunkPaths = [];
56
+ logger.info(`Splitting ${fileSizeMB.toFixed(1)}MB audio into ${numChunks} chunks ` +
57
+ `(~${chunkDuration.toFixed(0)}s each)`);
58
+ for (let i = 0; i < numChunks; i++) {
59
+ const startTime = i * chunkDuration;
60
+ const chunkPath = `${base}_chunk${i}${ext}`;
61
+ chunkPaths.push(chunkPath);
62
+ await new Promise((resolve, reject) => {
63
+ const cmd = ffmpeg(audioPath)
64
+ .setStartTime(startTime)
65
+ .setDuration(chunkDuration)
66
+ .audioCodec('copy')
67
+ .output(chunkPath)
68
+ .on('end', () => resolve())
69
+ .on('error', (err) => reject(new Error(`Chunk split failed: ${err.message}`)));
70
+ cmd.run();
71
+ });
72
+ logger.info(`Created chunk ${i + 1}/${numChunks}: ${chunkPath}`);
73
+ }
74
+ return chunkPaths;
75
+ }
76
+ /** Get the duration of an audio file in seconds using ffprobe. */
77
+ function getAudioDuration(audioPath) {
78
+ return new Promise((resolve, reject) => {
79
+ ffmpeg.ffprobe(audioPath, (err, metadata) => {
80
+ if (err)
81
+ return reject(new Error(`ffprobe failed: ${err.message}`));
82
+ resolve(metadata.format.duration ?? 0);
83
+ });
84
+ });
85
+ }
86
+ //# sourceMappingURL=audioExtraction.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"audioExtraction.js","sourceRoot":"","sources":["../../../src/tools/ffmpeg/audioExtraction.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Burn ASS subtitles into video (hard-coded subtitles).
3
+ * Uses direct execFile instead of fluent-ffmpeg to avoid Windows path escaping issues.
4
+ * Copies the ASS file to a temp dir and uses a relative path to dodge the Windows
5
+ * drive-letter colon being parsed as an FFmpeg filter option separator.
6
+ */
7
+ export declare function burnCaptions(videoPath: string, assPath: string, outputPath: string): Promise<string>;
8
+ //# sourceMappingURL=captionBurning.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captionBurning.d.ts","sourceRoot":"","sources":["../../../src/tools/ffmpeg/captionBurning.ts"],"names":[],"mappings":"AAWA;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,CA6DjB"}
@@ -0,0 +1,71 @@
1
+ import { execFile } from 'child_process';
2
+ import { promises as fs } from 'fs';
3
+ import pathMod from 'path';
4
+ import os from 'os';
5
+ import { fileURLToPath } from 'url';
6
+ import logger from '../../config/logger';
7
+ const ffmpegPath = process.env.FFMPEG_PATH || 'ffmpeg';
8
+ const __dirname = pathMod.dirname(fileURLToPath(import.meta.url));
9
+ const FONTS_DIR = pathMod.resolve(__dirname, '..', '..', '..', 'assets', 'fonts');
10
+ /**
11
+ * Burn ASS subtitles into video (hard-coded subtitles).
12
+ * Uses direct execFile instead of fluent-ffmpeg to avoid Windows path escaping issues.
13
+ * Copies the ASS file to a temp dir and uses a relative path to dodge the Windows
14
+ * drive-letter colon being parsed as an FFmpeg filter option separator.
15
+ */
16
+ export async function burnCaptions(videoPath, assPath, outputPath) {
17
+ const outputDir = pathMod.dirname(outputPath);
18
+ await fs.mkdir(outputDir, { recursive: true });
19
+ logger.info(`Burning captions into video → ${outputPath}`);
20
+ // Create a dedicated temp dir so we can use colon-free relative paths
21
+ const workDir = await fs.mkdtemp(pathMod.join(os.tmpdir(), 'caption-'));
22
+ const tempAss = pathMod.join(workDir, 'captions.ass');
23
+ const tempOutput = pathMod.join(workDir, 'output.mp4');
24
+ await fs.copyFile(assPath, tempAss);
25
+ // Copy bundled fonts so libass can find them via fontsdir=.
26
+ const fontFiles = await fs.readdir(FONTS_DIR);
27
+ for (const f of fontFiles) {
28
+ if (f.endsWith('.ttf') || f.endsWith('.otf')) {
29
+ await fs.copyFile(pathMod.join(FONTS_DIR, f), pathMod.join(workDir, f));
30
+ }
31
+ }
32
+ // Use just the filename — no drive letter, no colons
33
+ const args = [
34
+ '-y',
35
+ '-i', videoPath,
36
+ '-vf', 'ass=captions.ass:fontsdir=.',
37
+ '-c:a', 'copy',
38
+ '-c:v', 'libx264',
39
+ '-preset', 'ultrafast',
40
+ '-crf', '23',
41
+ '-threads', '4',
42
+ tempOutput,
43
+ ];
44
+ return new Promise((resolve, reject) => {
45
+ execFile(ffmpegPath, args, { cwd: workDir, maxBuffer: 10 * 1024 * 1024 }, async (error, _stdout, stderr) => {
46
+ const cleanup = async () => {
47
+ const files = await fs.readdir(workDir).catch(() => []);
48
+ for (const f of files) {
49
+ await fs.unlink(pathMod.join(workDir, f)).catch(() => { });
50
+ }
51
+ await fs.rmdir(workDir).catch(() => { });
52
+ };
53
+ if (error) {
54
+ await cleanup();
55
+ logger.error(`Caption burning failed: ${stderr || error.message}`);
56
+ reject(new Error(`Caption burning failed: ${stderr || error.message}`));
57
+ return;
58
+ }
59
+ try {
60
+ await fs.rename(tempOutput, outputPath);
61
+ }
62
+ catch {
63
+ await fs.copyFile(tempOutput, outputPath);
64
+ }
65
+ await cleanup();
66
+ logger.info(`Captions burned: ${outputPath}`);
67
+ resolve(outputPath);
68
+ });
69
+ });
70
+ }
71
+ //# sourceMappingURL=captionBurning.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"captionBurning.js","sourceRoot":"","sources":["../../../src/tools/ffmpeg/captionBurning.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import { ShortSegment } from '../../types';
2
+ /**
3
+ * Extract a single clip segment using stream copy (-c copy) for speed.
4
+ * @param buffer Seconds of padding added before start and after end (default 1.0)
5
+ */
6
+ export declare function extractClip(videoPath: string, start: number, end: number, outputPath: string, buffer?: number): Promise<string>;
7
+ /**
8
+ * Extract multiple non-contiguous segments and concatenate them into one clip.
9
+ * Each segment is padded by `buffer` seconds on both sides for smoother cuts.
10
+ * Re-encodes and uses concat demuxer for clean joins.
11
+ * @param buffer Seconds of padding added before start and after end of each segment (default 1.0)
12
+ */
13
+ export declare function extractCompositeClip(videoPath: string, segments: ShortSegment[], outputPath: string, buffer?: number): Promise<string>;
14
+ /**
15
+ * Extract multiple non-contiguous segments and concatenate them with crossfade
16
+ * transitions using FFmpeg xfade/acrossfade filters.
17
+ * Falls back to extractCompositeClip if only one segment is provided.
18
+ *
19
+ * @param transitionDuration Crossfade duration in seconds (default 0.5)
20
+ * @param buffer Seconds of padding added before/after each segment (default 1.0)
21
+ */
22
+ export declare function extractCompositeClipWithTransitions(videoPath: string, segments: ShortSegment[], outputPath: string, transitionDuration?: number, buffer?: number): Promise<string>;
23
+ //# sourceMappingURL=clipExtraction.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clipExtraction.d.ts","sourceRoot":"","sources":["../../../src/tools/ffmpeg/clipExtraction.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAO3C;;;GAGG;AACH,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAY,GACnB,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,YAAY,EAAE,EACxB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,MAAY,GACnB,OAAO,CAAC,MAAM,CAAC,CAgEjB;AAED;;;;;;;GAOG;AACH,wBAAsB,mCAAmC,CACvD,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,YAAY,EAAE,EACxB,UAAU,EAAE,MAAM,EAClB,kBAAkB,GAAE,MAAY,EAChC,MAAM,GAAE,MAAY,GACnB,OAAO,CAAC,MAAM,CAAC,CA0FjB"}