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,321 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ const { mockExecFile, mockMkdtemp, mockCopyFile, mockReaddir, mockUnlink, mockRmdir } = vi.hoisted(() => ({
3
+ mockExecFile: vi.fn(),
4
+ mockMkdtemp: vi.fn(),
5
+ mockCopyFile: vi.fn(),
6
+ mockReaddir: vi.fn(),
7
+ mockUnlink: vi.fn(),
8
+ mockRmdir: vi.fn(),
9
+ }));
10
+ vi.mock('child_process', () => ({
11
+ execFile: mockExecFile,
12
+ }));
13
+ vi.mock('fs', async (importOriginal) => {
14
+ const original = await importOriginal();
15
+ return {
16
+ ...original,
17
+ promises: {
18
+ ...original.promises,
19
+ mkdtemp: mockMkdtemp,
20
+ copyFile: mockCopyFile,
21
+ readdir: mockReaddir,
22
+ unlink: mockUnlink,
23
+ rmdir: mockRmdir,
24
+ },
25
+ };
26
+ });
27
+ vi.mock('../config/logger.js', () => ({
28
+ default: {
29
+ info: vi.fn(),
30
+ warn: vi.fn(),
31
+ error: vi.fn(),
32
+ debug: vi.fn(),
33
+ },
34
+ }));
35
+ import { buildFilterComplex, singlePassEdit, singlePassEditAndCaption } from '../tools/ffmpeg/singlePassEdit.js';
36
+ describe('buildFilterComplex', () => {
37
+ describe('basic filter generation', () => {
38
+ it('produces correct trim+setpts+concat for 2 segments', () => {
39
+ const segments = [
40
+ { start: 0, end: 5.5 },
41
+ { start: 8.2, end: 12.0 },
42
+ ];
43
+ const result = buildFilterComplex(segments);
44
+ const lines = result.split(';\n');
45
+ // 2 segments × 2 (video + audio) + 1 concat = 5 filter parts
46
+ expect(lines).toHaveLength(5);
47
+ // Video trim for segment 0
48
+ expect(lines[0]).toBe('[0:v]trim=start=0.000:end=5.500,setpts=PTS-STARTPTS[v0]');
49
+ // Audio trim for segment 0
50
+ expect(lines[1]).toBe('[0:a]atrim=start=0.000:end=5.500,asetpts=PTS-STARTPTS[a0]');
51
+ // Video trim for segment 1
52
+ expect(lines[2]).toBe('[0:v]trim=start=8.200:end=12.000,setpts=PTS-STARTPTS[v1]');
53
+ // Audio trim for segment 1
54
+ expect(lines[3]).toBe('[0:a]atrim=start=8.200:end=12.000,asetpts=PTS-STARTPTS[a1]');
55
+ // Concat
56
+ expect(lines[4]).toBe('[v0][a0][v1][a1]concat=n=2:v=1:a=1[outv][outa]');
57
+ });
58
+ it('formats timestamps to 3 decimal places', () => {
59
+ const segments = [{ start: 1.1, end: 2.22 }];
60
+ const result = buildFilterComplex(segments);
61
+ expect(result).toContain('start=1.100');
62
+ expect(result).toContain('end=2.220');
63
+ });
64
+ it('each segment has paired video and audio trim filters', () => {
65
+ const segments = [
66
+ { start: 0, end: 3 },
67
+ { start: 5, end: 10 },
68
+ { start: 15, end: 20 },
69
+ ];
70
+ const result = buildFilterComplex(segments);
71
+ const lines = result.split(';\n');
72
+ for (let i = 0; i < segments.length; i++) {
73
+ const videoLine = lines[i * 2];
74
+ const audioLine = lines[i * 2 + 1];
75
+ expect(videoLine).toContain(`[0:v]trim=start=${segments[i].start.toFixed(3)}:end=${segments[i].end.toFixed(3)}`);
76
+ expect(videoLine).toContain(`setpts=PTS-STARTPTS[v${i}]`);
77
+ expect(audioLine).toContain(`[0:a]atrim=start=${segments[i].start.toFixed(3)}:end=${segments[i].end.toFixed(3)}`);
78
+ expect(audioLine).toContain(`asetpts=PTS-STARTPTS[a${i}]`);
79
+ }
80
+ });
81
+ it('concat n= matches segment count', () => {
82
+ const segments = [
83
+ { start: 0, end: 1 },
84
+ { start: 2, end: 3 },
85
+ { start: 4, end: 5 },
86
+ { start: 6, end: 7 },
87
+ ];
88
+ const result = buildFilterComplex(segments);
89
+ expect(result).toContain('concat=n=4:v=1:a=1');
90
+ });
91
+ it('concat inputs list all segment pairs in order', () => {
92
+ const segments = [
93
+ { start: 0, end: 1 },
94
+ { start: 2, end: 3 },
95
+ { start: 4, end: 5 },
96
+ ];
97
+ const result = buildFilterComplex(segments);
98
+ expect(result).toContain('[v0][a0][v1][a1][v2][a2]concat=n=3');
99
+ });
100
+ });
101
+ describe('with captions', () => {
102
+ it('appends ASS subtitle filter after concat', () => {
103
+ const segments = [
104
+ { start: 0, end: 5 },
105
+ { start: 8, end: 12 },
106
+ ];
107
+ const result = buildFilterComplex(segments, { assFilename: 'captions.ass' });
108
+ const lines = result.split(';\n');
109
+ // Last line should be the ASS filter
110
+ expect(lines[lines.length - 1]).toContain('ass=captions.ass');
111
+ expect(lines[lines.length - 1]).toContain('[outv]');
112
+ });
113
+ it('uses intermediate labels [cv][ca] for concat when captions enabled', () => {
114
+ const segments = [{ start: 0, end: 5 }];
115
+ const result = buildFilterComplex(segments, { assFilename: 'subs.ass' });
116
+ expect(result).toContain('concat=n=1:v=1:a=1[cv][ca]');
117
+ expect(result).not.toContain('[outv][outa]');
118
+ });
119
+ it('sets fontsdir parameter correctly', () => {
120
+ const segments = [{ start: 0, end: 5 }];
121
+ const result = buildFilterComplex(segments, {
122
+ assFilename: 'captions.ass',
123
+ fontsdir: '/tmp/fonts',
124
+ });
125
+ expect(result).toContain('fontsdir=/tmp/fonts');
126
+ });
127
+ it('defaults fontsdir to "." when not specified', () => {
128
+ const segments = [{ start: 0, end: 5 }];
129
+ const result = buildFilterComplex(segments, { assFilename: 'captions.ass' });
130
+ expect(result).toContain('fontsdir=.');
131
+ });
132
+ it('without captions uses [outv][outa] labels directly', () => {
133
+ const segments = [{ start: 0, end: 5 }];
134
+ const result = buildFilterComplex(segments);
135
+ expect(result).toContain('[outv][outa]');
136
+ expect(result).not.toContain('[cv]');
137
+ expect(result).not.toContain('[ca]');
138
+ expect(result).not.toContain('ass=');
139
+ });
140
+ });
141
+ describe('edge cases', () => {
142
+ it('single segment produces valid filter', () => {
143
+ const segments = [{ start: 0, end: 60 }];
144
+ const result = buildFilterComplex(segments);
145
+ const lines = result.split(';\n');
146
+ expect(lines).toHaveLength(3); // video trim, audio trim, concat
147
+ expect(result).toContain('concat=n=1:v=1:a=1[outv][outa]');
148
+ expect(result).toContain('[v0][a0]');
149
+ });
150
+ it('handles 10+ segments correctly', () => {
151
+ const segments = Array.from({ length: 12 }, (_, i) => ({
152
+ start: i * 10,
153
+ end: i * 10 + 8,
154
+ }));
155
+ const result = buildFilterComplex(segments);
156
+ const lines = result.split(';\n');
157
+ // 12 segments × 2 + 1 concat = 25 lines
158
+ expect(lines).toHaveLength(25);
159
+ expect(result).toContain('concat=n=12:v=1:a=1');
160
+ // Check double-digit indices
161
+ expect(result).toContain('[v10]');
162
+ expect(result).toContain('[a11]');
163
+ });
164
+ it('handles very short segments without negative durations', () => {
165
+ const segments = [
166
+ { start: 5.001, end: 5.002 },
167
+ ];
168
+ const result = buildFilterComplex(segments);
169
+ expect(result).toContain('start=5.001:end=5.002');
170
+ // No negative values in the output
171
+ expect(result).not.toMatch(/-\d+\.\d+/);
172
+ });
173
+ it('handles segments starting at 0', () => {
174
+ const segments = [{ start: 0, end: 1 }];
175
+ const result = buildFilterComplex(segments);
176
+ expect(result).toContain('start=0.000:end=1.000');
177
+ });
178
+ it('handles high-precision floating point timestamps', () => {
179
+ const segments = [{ start: 1.23456789, end: 9.87654321 }];
180
+ const result = buildFilterComplex(segments);
181
+ // toFixed(3) rounds correctly
182
+ expect(result).toContain('start=1.235');
183
+ expect(result).toContain('end=9.877');
184
+ });
185
+ });
186
+ describe('input validation', () => {
187
+ it('throws on empty segments array', () => {
188
+ expect(() => buildFilterComplex([])).toThrow('keepSegments must not be empty');
189
+ });
190
+ });
191
+ });
192
+ describe('singlePassEdit', () => {
193
+ const segments = [
194
+ { start: 0, end: 5 },
195
+ { start: 10, end: 15 },
196
+ ];
197
+ beforeEach(() => {
198
+ vi.clearAllMocks();
199
+ mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
200
+ cb(null, '', '');
201
+ });
202
+ });
203
+ it('calls execFile with correct input and output paths', async () => {
204
+ await singlePassEdit('/input.mp4', segments, '/output.mp4');
205
+ expect(mockExecFile).toHaveBeenCalledOnce();
206
+ const [cmd, args] = mockExecFile.mock.calls[0];
207
+ expect(cmd).toMatch(/ffmpeg/);
208
+ expect(args).toContain('/input.mp4');
209
+ expect(args[args.length - 1]).toBe('/output.mp4');
210
+ });
211
+ it('includes -filter_complex matching buildFilterComplex output', async () => {
212
+ await singlePassEdit('/input.mp4', segments, '/output.mp4');
213
+ const args = mockExecFile.mock.calls[0][1];
214
+ const fcIdx = args.indexOf('-filter_complex');
215
+ expect(fcIdx).toBeGreaterThan(-1);
216
+ expect(args[fcIdx + 1]).toBe(buildFilterComplex(segments));
217
+ });
218
+ it('uses -preset ultrafast and -threads 4', async () => {
219
+ await singlePassEdit('/input.mp4', segments, '/output.mp4');
220
+ const args = mockExecFile.mock.calls[0][1];
221
+ expect(args).toContain('-preset');
222
+ expect(args[args.indexOf('-preset') + 1]).toBe('ultrafast');
223
+ expect(args).toContain('-threads');
224
+ expect(args[args.indexOf('-threads') + 1]).toBe('4');
225
+ });
226
+ it('maps [outv] and [outa]', async () => {
227
+ await singlePassEdit('/input.mp4', segments, '/output.mp4');
228
+ const args = mockExecFile.mock.calls[0][1];
229
+ expect(args).toContain('[outv]');
230
+ expect(args).toContain('[outa]');
231
+ });
232
+ it('returns output path on success', async () => {
233
+ const result = await singlePassEdit('/input.mp4', segments, '/output.mp4');
234
+ expect(result).toBe('/output.mp4');
235
+ });
236
+ it('rejects with error on FFmpeg failure', async () => {
237
+ mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
238
+ cb(new Error('ffmpeg crash'), '', 'some stderr');
239
+ });
240
+ await expect(singlePassEdit('/input.mp4', segments, '/output.mp4'))
241
+ .rejects.toThrow('Single-pass edit failed: ffmpeg crash');
242
+ });
243
+ it('passes maxBuffer option to execFile', async () => {
244
+ await singlePassEdit('/input.mp4', segments, '/output.mp4');
245
+ const opts = mockExecFile.mock.calls[0][2];
246
+ expect(opts.maxBuffer).toBe(50 * 1024 * 1024);
247
+ });
248
+ });
249
+ describe('singlePassEditAndCaption', () => {
250
+ const segments = [
251
+ { start: 0, end: 5 },
252
+ { start: 10, end: 15 },
253
+ ];
254
+ const tempDir = '/tmp/caption-abc123';
255
+ beforeEach(() => {
256
+ vi.clearAllMocks();
257
+ mockMkdtemp.mockResolvedValue(tempDir);
258
+ mockCopyFile.mockResolvedValue(undefined);
259
+ mockReaddir.mockResolvedValue(['Montserrat-Bold.ttf', 'readme.txt']);
260
+ mockUnlink.mockResolvedValue(undefined);
261
+ mockRmdir.mockResolvedValue(undefined);
262
+ mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
263
+ mockReaddir.mockResolvedValueOnce(['captions.ass', 'Montserrat-Bold.ttf']);
264
+ cb(null, '', '');
265
+ });
266
+ });
267
+ it('creates a temp directory via mkdtemp', async () => {
268
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
269
+ expect(mockMkdtemp).toHaveBeenCalledOnce();
270
+ });
271
+ it('copies ASS file to temp directory', async () => {
272
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
273
+ const copyArgs = mockCopyFile.mock.calls.map((c) => [c[0], c[1]]);
274
+ const assCopy = copyArgs.find((c) => c[0] === '/captions.ass');
275
+ expect(assCopy).toBeDefined();
276
+ expect(assCopy[1]).toContain('captions.ass');
277
+ });
278
+ it('copies .ttf font files to temp directory but skips non-font files', async () => {
279
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
280
+ const copyDests = mockCopyFile.mock.calls.map((c) => c[1]);
281
+ expect(copyDests.some((d) => d.includes('Montserrat-Bold.ttf'))).toBe(true);
282
+ expect(copyDests.some((d) => d.includes('readme.txt'))).toBe(false);
283
+ });
284
+ it('builds filter_complex with ass filter and fontsdir', async () => {
285
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
286
+ const args = mockExecFile.mock.calls[0][1];
287
+ const fcIdx = args.indexOf('-filter_complex');
288
+ const fc = args[fcIdx + 1];
289
+ expect(fc).toContain('ass=captions.ass');
290
+ expect(fc).toContain('fontsdir=.');
291
+ });
292
+ it('maps [ca] instead of [outa] for captioned output', async () => {
293
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
294
+ const args = mockExecFile.mock.calls[0][1];
295
+ expect(args).toContain('[ca]');
296
+ expect(args).not.toContain('[outa]');
297
+ });
298
+ it('runs execFile with cwd set to temp directory', async () => {
299
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
300
+ const opts = mockExecFile.mock.calls[0][2];
301
+ expect(opts.cwd).toBe(tempDir);
302
+ });
303
+ it('returns output path on success', async () => {
304
+ const result = await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
305
+ expect(result).toBe('/output.mp4');
306
+ });
307
+ it('cleans up temp directory after completion', async () => {
308
+ await singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4');
309
+ expect(mockRmdir).toHaveBeenCalled();
310
+ });
311
+ it('rejects with error on FFmpeg failure and still cleans up', async () => {
312
+ mockExecFile.mockImplementation((_cmd, _args, _opts, cb) => {
313
+ mockReaddir.mockResolvedValueOnce(['captions.ass']);
314
+ cb(new Error('encode failed'), '', 'error details');
315
+ });
316
+ await expect(singlePassEditAndCaption('/input.mp4', segments, '/captions.ass', '/output.mp4'))
317
+ .rejects.toThrow('Single-pass edit failed: encode failed');
318
+ expect(mockRmdir).toHaveBeenCalled();
319
+ });
320
+ });
321
+ //# sourceMappingURL=singlePassEdit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singlePassEdit.test.js","sourceRoot":"","sources":["../../src/__tests__/singlePassEdit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=smoke.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/smoke.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,8 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ describe('Smoke Test', () => {
3
+ it('should import environment config', async () => {
4
+ const mod = await import('../config/environment.js');
5
+ expect(mod).toBeDefined();
6
+ });
7
+ });
8
+ //# sourceMappingURL=smoke.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smoke.test.js","sourceRoot":"","sources":["../../src/__tests__/smoke.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,0BAA0B,CAAC,CAAC;QACrD,MAAM,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=utilities.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utilities.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/utilities.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,268 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ // ── Mocks must be declared before imports ──────────────────────────────
3
+ // Mock logger to suppress console output in tests
4
+ vi.mock('../config/logger.js', () => {
5
+ const mockLogger = {
6
+ info: vi.fn(),
7
+ warn: vi.fn(),
8
+ error: vi.fn(),
9
+ debug: vi.fn(),
10
+ level: 'info',
11
+ };
12
+ return { default: mockLogger, setVerbose: vi.fn() };
13
+ });
14
+ // Mock environment config
15
+ vi.mock('../config/environment.js', () => ({
16
+ getConfig: vi.fn().mockReturnValue({
17
+ BRAND_PATH: '/fake/brand.json',
18
+ OPENAI_API_KEY: 'test-key',
19
+ EXA_API_KEY: 'test-exa-key',
20
+ }),
21
+ initConfig: vi.fn(),
22
+ }));
23
+ // Mock fs for brand.ts and whisperClient.ts
24
+ vi.mock('fs', async () => {
25
+ const actual = await vi.importActual('fs');
26
+ return {
27
+ ...actual,
28
+ default: {
29
+ ...actual,
30
+ existsSync: vi.fn().mockReturnValue(true),
31
+ readFileSync: vi.fn().mockReturnValue('{}'),
32
+ statSync: vi.fn().mockReturnValue({ size: 1024 * 1024 }),
33
+ createReadStream: vi.fn().mockReturnValue('fake-stream'),
34
+ },
35
+ };
36
+ });
37
+ // Mock openai for whisperClient.ts
38
+ const mockCreate = vi.fn().mockResolvedValue({
39
+ text: 'Hello world',
40
+ language: 'en',
41
+ duration: 5.0,
42
+ segments: [{ id: 0, start: 0, end: 1, text: 'Hello world' }],
43
+ words: [
44
+ { word: 'Hello', start: 0, end: 0.5 },
45
+ { word: 'world', start: 0.6, end: 1.0 },
46
+ ],
47
+ });
48
+ vi.mock('openai', () => ({
49
+ default: class MockOpenAI {
50
+ audio = {
51
+ transcriptions: {
52
+ create: mockCreate,
53
+ },
54
+ };
55
+ },
56
+ }));
57
+ // Mock exa-js for exaClient.ts
58
+ const mockSearchAndContents = vi.fn().mockResolvedValue({
59
+ results: [
60
+ { title: 'Test Result', url: 'https://test.com', text: 'test content' },
61
+ ],
62
+ });
63
+ vi.mock('exa-js', () => ({
64
+ default: class MockExa {
65
+ searchAndContents = mockSearchAndContents;
66
+ },
67
+ }));
68
+ // ── Imports (after mocks) ──────────────────────────────────────────────
69
+ import fs from 'fs';
70
+ import { getConfig } from '../config/environment.js';
71
+ // ========================================================================
72
+ // 1. brand.ts
73
+ // ========================================================================
74
+ describe('brand.ts', () => {
75
+ const fakeBrand = {
76
+ name: 'TestBrand',
77
+ handle: '@test',
78
+ tagline: 'Test tagline',
79
+ voice: { tone: 'casual', personality: 'fun', style: 'brief' },
80
+ advocacy: { primary: ['a11y'], interests: ['tech'], avoids: ['spam'] },
81
+ customVocabulary: ['Copilot', 'TypeScript', 'FFmpeg'],
82
+ hashtags: { always: ['#test'], preferred: ['#dev'], platforms: {} },
83
+ contentGuidelines: {
84
+ shortsFocus: 'key moments',
85
+ blogFocus: 'education',
86
+ socialFocus: 'engagement',
87
+ },
88
+ };
89
+ beforeEach(() => {
90
+ vi.resetModules();
91
+ vi.mocked(fs.existsSync).mockReturnValue(true);
92
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(fakeBrand));
93
+ });
94
+ it('getBrandConfig() returns parsed brand config', async () => {
95
+ const { getBrandConfig } = await import('../config/brand.js');
96
+ const config = getBrandConfig();
97
+ expect(config.name).toBe('TestBrand');
98
+ expect(config.handle).toBe('@test');
99
+ expect(config.customVocabulary).toContain('Copilot');
100
+ });
101
+ it('getBrandConfig() returns defaults when file not found', async () => {
102
+ vi.mocked(fs.existsSync).mockReturnValue(false);
103
+ const { getBrandConfig } = await import('../config/brand.js');
104
+ const config = getBrandConfig();
105
+ expect(config.name).toBe('Creator');
106
+ expect(config.handle).toBe('@creator');
107
+ });
108
+ it('getBrandConfig() caches — second call does not re-read', async () => {
109
+ const { getBrandConfig } = await import('../config/brand.js');
110
+ getBrandConfig();
111
+ const callsAfterFirst = vi.mocked(fs.readFileSync).mock.calls.length;
112
+ getBrandConfig();
113
+ const callsAfterSecond = vi.mocked(fs.readFileSync).mock.calls.length;
114
+ // Second call should not trigger another read
115
+ expect(callsAfterSecond).toBe(callsAfterFirst);
116
+ });
117
+ it('getWhisperPrompt() includes vocabulary words', async () => {
118
+ const { getWhisperPrompt } = await import('../config/brand.js');
119
+ const prompt = getWhisperPrompt();
120
+ expect(prompt).toContain('Copilot');
121
+ expect(prompt).toContain('TypeScript');
122
+ expect(prompt).toContain('FFmpeg');
123
+ });
124
+ });
125
+ // ========================================================================
126
+ // 2. logger.ts
127
+ // ========================================================================
128
+ describe('logger.ts', () => {
129
+ it('default export has expected logging methods', async () => {
130
+ // Import the REAL logger (not the mock) for interface checks
131
+ const loggerMod = await import('../config/logger.js');
132
+ const log = loggerMod.default;
133
+ expect(typeof log.info).toBe('function');
134
+ expect(typeof log.warn).toBe('function');
135
+ expect(typeof log.error).toBe('function');
136
+ expect(typeof log.debug).toBe('function');
137
+ });
138
+ it('setVerbose is exported as a function', async () => {
139
+ const { setVerbose } = await import('../config/logger.js');
140
+ expect(typeof setVerbose).toBe('function');
141
+ });
142
+ it('real logger: setVerbose changes level to debug', async () => {
143
+ // Temporarily import real logger by resetting modules
144
+ vi.resetModules();
145
+ // Dynamically import without the mock to test real behaviour
146
+ const realLogger = await vi.importActual('../config/logger.js');
147
+ realLogger.setVerbose();
148
+ expect(realLogger.default.level).toBe('debug');
149
+ });
150
+ it('real logger is a winston Logger instance', async () => {
151
+ vi.resetModules();
152
+ const realLogger = await vi.importActual('../config/logger.js');
153
+ // Winston loggers have a `transports` array
154
+ expect(realLogger.default).toHaveProperty('transports');
155
+ expect(Array.isArray(realLogger.default.transports)).toBe(true);
156
+ });
157
+ });
158
+ // ========================================================================
159
+ // 3. whisperClient.ts
160
+ // ========================================================================
161
+ describe('whisperClient.ts', () => {
162
+ beforeEach(() => {
163
+ vi.resetModules();
164
+ mockCreate.mockClear();
165
+ vi.mocked(fs.existsSync).mockReturnValue(true);
166
+ vi.mocked(fs.statSync).mockReturnValue({ size: 1024 * 1024 });
167
+ vi.mocked(fs.createReadStream).mockReturnValue('fake-stream');
168
+ vi.mocked(getConfig).mockReturnValue({
169
+ OPENAI_API_KEY: 'test-key',
170
+ BRAND_PATH: '/fake/brand.json',
171
+ EXA_API_KEY: '',
172
+ });
173
+ });
174
+ it('transcribeAudio() returns correct transcript structure', async () => {
175
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
176
+ const result = await transcribeAudio('/fake/audio.mp3');
177
+ expect(result.text).toBe('Hello world');
178
+ expect(result.language).toBe('en');
179
+ expect(result.duration).toBe(5.0);
180
+ expect(result.segments).toHaveLength(1);
181
+ expect(result.words).toHaveLength(2);
182
+ });
183
+ it('transcribeAudio() parses words correctly', async () => {
184
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
185
+ const result = await transcribeAudio('/fake/audio.mp3');
186
+ expect(result.words[0]).toEqual({ word: 'Hello', start: 0, end: 0.5 });
187
+ expect(result.words[1]).toEqual({ word: 'world', start: 0.6, end: 1.0 });
188
+ });
189
+ it('transcribeAudio() throws when file not found', async () => {
190
+ vi.mocked(fs.existsSync).mockReturnValue(false);
191
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
192
+ await expect(transcribeAudio('/missing/audio.mp3')).rejects.toThrow('Audio file not found');
193
+ });
194
+ it('transcribeAudio() throws when file exceeds 25MB', async () => {
195
+ vi.mocked(fs.statSync).mockReturnValue({
196
+ size: 30 * 1024 * 1024,
197
+ });
198
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
199
+ await expect(transcribeAudio('/fake/large.mp3')).rejects.toThrow("exceeds Whisper's 25MB limit");
200
+ });
201
+ it('transcribeAudio() handles API 401 error', async () => {
202
+ mockCreate.mockRejectedValueOnce(Object.assign(new Error('Unauthorized'), { status: 401 }));
203
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
204
+ await expect(transcribeAudio('/fake/audio.mp3')).rejects.toThrow('OpenAI API authentication failed');
205
+ });
206
+ it('transcribeAudio() handles API 429 rate limit', async () => {
207
+ mockCreate.mockRejectedValueOnce(Object.assign(new Error('Rate limited'), { status: 429 }));
208
+ const { transcribeAudio } = await import('../tools/whisper/whisperClient.js');
209
+ await expect(transcribeAudio('/fake/audio.mp3')).rejects.toThrow('rate limit exceeded');
210
+ });
211
+ });
212
+ // ========================================================================
213
+ // 4. exaClient.ts
214
+ // ========================================================================
215
+ describe('exaClient.ts', () => {
216
+ beforeEach(() => {
217
+ vi.resetModules();
218
+ mockSearchAndContents.mockClear();
219
+ mockSearchAndContents.mockResolvedValue({
220
+ results: [
221
+ { title: 'Test Result', url: 'https://test.com', text: 'test content' },
222
+ ],
223
+ });
224
+ vi.mocked(getConfig).mockReturnValue({
225
+ EXA_API_KEY: 'test-exa-key',
226
+ OPENAI_API_KEY: '',
227
+ BRAND_PATH: '',
228
+ });
229
+ });
230
+ it('searchWeb() returns results', async () => {
231
+ const { searchWeb } = await import('../tools/search/exaClient.js');
232
+ const results = await searchWeb('TypeScript');
233
+ expect(results).toHaveLength(1);
234
+ expect(results[0].title).toBe('Test Result');
235
+ expect(results[0].url).toBe('https://test.com');
236
+ expect(results[0].snippet).toBe('test content');
237
+ });
238
+ it('searchWeb() returns empty array when EXA_API_KEY not set', async () => {
239
+ vi.mocked(getConfig).mockReturnValue({
240
+ EXA_API_KEY: '',
241
+ OPENAI_API_KEY: '',
242
+ BRAND_PATH: '',
243
+ });
244
+ const { searchWeb } = await import('../tools/search/exaClient.js');
245
+ const results = await searchWeb('test');
246
+ expect(results).toEqual([]);
247
+ });
248
+ it('searchWeb() returns empty array on API error', async () => {
249
+ mockSearchAndContents.mockRejectedValueOnce(new Error('API failure'));
250
+ const { searchWeb } = await import('../tools/search/exaClient.js');
251
+ const results = await searchWeb('test');
252
+ expect(results).toEqual([]);
253
+ });
254
+ it('searchTopics() returns map of topic → results', async () => {
255
+ const { searchTopics } = await import('../tools/search/exaClient.js');
256
+ const map = await searchTopics(['topic1', 'topic2']);
257
+ expect(map).toBeInstanceOf(Map);
258
+ expect(map.size).toBe(2);
259
+ expect(map.get('topic1')).toHaveLength(1);
260
+ expect(map.get('topic2')).toHaveLength(1);
261
+ });
262
+ it('searchTopics() passes numResults=3 per topic', async () => {
263
+ const { searchTopics } = await import('../tools/search/exaClient.js');
264
+ await searchTopics(['single-topic']);
265
+ expect(mockSearchAndContents).toHaveBeenCalledWith('single-topic', expect.objectContaining({ numResults: 3 }));
266
+ });
267
+ });
268
+ //# sourceMappingURL=utilities.test.js.map