@vargai/sdk 0.1.1

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 (67) hide show
  1. package/.env.example +24 -0
  2. package/CLAUDE.md +118 -0
  3. package/HIGGSFIELD_REWRITE_SUMMARY.md +300 -0
  4. package/README.md +231 -0
  5. package/SKILLS.md +157 -0
  6. package/STRUCTURE.md +92 -0
  7. package/TEST_RESULTS.md +122 -0
  8. package/action/captions/SKILL.md +170 -0
  9. package/action/captions/index.ts +169 -0
  10. package/action/edit/SKILL.md +235 -0
  11. package/action/edit/index.ts +437 -0
  12. package/action/image/SKILL.md +140 -0
  13. package/action/image/index.ts +105 -0
  14. package/action/sync/SKILL.md +136 -0
  15. package/action/sync/index.ts +145 -0
  16. package/action/transcribe/SKILL.md +179 -0
  17. package/action/transcribe/index.ts +210 -0
  18. package/action/video/SKILL.md +116 -0
  19. package/action/video/index.ts +125 -0
  20. package/action/voice/SKILL.md +125 -0
  21. package/action/voice/index.ts +136 -0
  22. package/biome.json +33 -0
  23. package/bun.lock +842 -0
  24. package/cli/commands/find.ts +58 -0
  25. package/cli/commands/help.ts +70 -0
  26. package/cli/commands/list.ts +49 -0
  27. package/cli/commands/run.ts +237 -0
  28. package/cli/commands/which.ts +66 -0
  29. package/cli/discover.ts +66 -0
  30. package/cli/index.ts +33 -0
  31. package/cli/runner.ts +65 -0
  32. package/cli/types.ts +49 -0
  33. package/cli/ui.ts +185 -0
  34. package/index.ts +75 -0
  35. package/lib/README.md +144 -0
  36. package/lib/ai-sdk/fal.ts +106 -0
  37. package/lib/ai-sdk/replicate.ts +107 -0
  38. package/lib/elevenlabs.ts +382 -0
  39. package/lib/fal.ts +467 -0
  40. package/lib/ffmpeg.ts +467 -0
  41. package/lib/fireworks.ts +235 -0
  42. package/lib/groq.ts +246 -0
  43. package/lib/higgsfield/MIGRATION.md +308 -0
  44. package/lib/higgsfield/README.md +273 -0
  45. package/lib/higgsfield/example.ts +228 -0
  46. package/lib/higgsfield/index.ts +241 -0
  47. package/lib/higgsfield/soul.ts +262 -0
  48. package/lib/higgsfield.ts +176 -0
  49. package/lib/remotion/SKILL.md +823 -0
  50. package/lib/remotion/cli.ts +115 -0
  51. package/lib/remotion/functions.ts +283 -0
  52. package/lib/remotion/index.ts +19 -0
  53. package/lib/remotion/templates.ts +73 -0
  54. package/lib/replicate.ts +304 -0
  55. package/output.txt +1 -0
  56. package/package.json +42 -0
  57. package/pipeline/cookbooks/SKILL.md +285 -0
  58. package/pipeline/cookbooks/remotion-video.md +585 -0
  59. package/pipeline/cookbooks/round-video-character.md +337 -0
  60. package/pipeline/cookbooks/talking-character.md +59 -0
  61. package/scripts/produce-menopause-campaign.sh +202 -0
  62. package/service/music/SKILL.md +229 -0
  63. package/service/music/index.ts +296 -0
  64. package/test-import.ts +7 -0
  65. package/test-services.ts +97 -0
  66. package/tsconfig.json +29 -0
  67. package/utilities/s3.ts +147 -0
@@ -0,0 +1,235 @@
1
+ ---
2
+ name: video-editing
3
+ description: edit videos with ffmpeg operations including resize, trim, concat, social media optimization, and montage creation. use for video editing, format conversion, social media prep, merging clips, or batch video operations.
4
+ allowed-tools: Read, Bash
5
+ ---
6
+
7
+ # video editing
8
+
9
+ comprehensive video editing service combining ffmpeg operations into common workflows.
10
+
11
+ ## commands
12
+
13
+ ### prepare for social media
14
+ ```bash
15
+ bun run service/edit.ts social <input> <output> <platform> [audioPath]
16
+ ```
17
+
18
+ automatically resize and optimize for platform specs:
19
+ - **tiktok**: 1080x1920 (9:16)
20
+ - **instagram**: 1080x1920 (9:16)
21
+ - **youtube-shorts**: 1080x1920 (9:16)
22
+ - **youtube**: 1920x1080 (16:9)
23
+ - **twitter**: 1280x720 (16:9)
24
+
25
+ **example:**
26
+ ```bash
27
+ bun run service/edit.ts social raw.mp4 tiktok.mp4 tiktok
28
+ bun run service/edit.ts social raw.mp4 ig.mp4 instagram audio.mp3
29
+ ```
30
+
31
+ ### create montage
32
+ ```bash
33
+ bun run service/edit.ts montage <output> <clip1> <clip2> [clip3...]
34
+ ```
35
+
36
+ combine multiple video clips into one:
37
+
38
+ **example:**
39
+ ```bash
40
+ bun run service/edit.ts montage final.mp4 intro.mp4 main.mp4 outro.mp4
41
+ ```
42
+
43
+ ### quick trim
44
+ ```bash
45
+ bun run service/edit.ts trim <input> <output> <start> [end]
46
+ ```
47
+
48
+ extract a segment from video:
49
+
50
+ **example:**
51
+ ```bash
52
+ bun run service/edit.ts trim long.mp4 short.mp4 10 30
53
+ # extracts seconds 10-30
54
+ ```
55
+
56
+ ### quick resize
57
+ ```bash
58
+ bun run service/edit.ts resize <input> <output> <preset>
59
+ ```
60
+
61
+ resize to common aspect ratios:
62
+ - **vertical**: 9:16 (1080x1920)
63
+ - **square**: 1:1 (1080x1080)
64
+ - **landscape**: 16:9 (1920x1080)
65
+ - **4k**: 3840x2160
66
+
67
+ **example:**
68
+ ```bash
69
+ bun run service/edit.ts resize raw.mp4 vertical.mp4 vertical
70
+ ```
71
+
72
+ ### merge with audio
73
+ ```bash
74
+ bun run service/edit.ts merge_audio <audio> <output> <video1> [video2...]
75
+ ```
76
+
77
+ concatenate videos and add audio overlay:
78
+
79
+ **example:**
80
+ ```bash
81
+ bun run service/edit.ts merge_audio song.mp3 final.mp4 clip1.mp4 clip2.mp4
82
+ ```
83
+
84
+ ## as library
85
+
86
+ ```typescript
87
+ import {
88
+ prepareForSocial,
89
+ createMontage,
90
+ quickTrim,
91
+ quickResize,
92
+ mergeWithAudio,
93
+ editPipeline
94
+ } from "./service/edit"
95
+
96
+ // social media optimization
97
+ await prepareForSocial({
98
+ input: "raw.mp4",
99
+ output: "tiktok.mp4",
100
+ platform: "tiktok",
101
+ withAudio: "audio.mp3"
102
+ })
103
+
104
+ // create montage
105
+ await createMontage({
106
+ clips: ["clip1.mp4", "clip2.mp4"],
107
+ output: "montage.mp4",
108
+ maxClipDuration: 5,
109
+ targetResolution: { width: 1920, height: 1080 }
110
+ })
111
+
112
+ // trim video
113
+ await quickTrim("video.mp4", "trimmed.mp4", 10, 30)
114
+
115
+ // resize video
116
+ await quickResize("video.mp4", "resized.mp4", "vertical")
117
+
118
+ // merge videos with audio
119
+ await mergeWithAudio(
120
+ ["clip1.mp4", "clip2.mp4"],
121
+ "audio.mp3",
122
+ "final.mp4"
123
+ )
124
+ ```
125
+
126
+ ## advanced: edit pipeline
127
+
128
+ chain multiple operations:
129
+
130
+ ```typescript
131
+ import { editPipeline } from "./service/edit"
132
+
133
+ await editPipeline({
134
+ steps: [
135
+ {
136
+ operation: "resize",
137
+ options: {
138
+ input: "raw.mp4",
139
+ width: 1080,
140
+ height: 1920
141
+ }
142
+ },
143
+ {
144
+ operation: "add_audio",
145
+ options: {
146
+ videoPath: "temp.mp4",
147
+ audioPath: "music.mp3"
148
+ }
149
+ },
150
+ {
151
+ operation: "trim",
152
+ options: {
153
+ input: "temp2.mp4",
154
+ start: 0,
155
+ duration: 30
156
+ }
157
+ }
158
+ ],
159
+ finalOutput: "final.mp4"
160
+ })
161
+ ```
162
+
163
+ **available operations:**
164
+ - `concat` - concatenate videos
165
+ - `add_audio` - overlay audio
166
+ - `resize` - change dimensions
167
+ - `trim` - extract segment
168
+ - `convert` - change format
169
+ - `extract_audio` - extract audio track
170
+
171
+ ## when to use
172
+
173
+ use this skill when:
174
+ - preparing videos for social media platforms
175
+ - combining multiple video clips
176
+ - extracting segments from longer videos
177
+ - resizing to specific aspect ratios
178
+ - adding background music to video montages
179
+ - batch processing video files
180
+ - optimizing videos for specific platforms
181
+
182
+ ## typical workflows
183
+
184
+ ### social media content
185
+ 1. create or edit raw video
186
+ 2. add captions (captions service)
187
+ 3. prepare for platform (this service)
188
+ 4. upload
189
+
190
+ ### video montage
191
+ 1. collect multiple clips
192
+ 2. create montage (this service)
193
+ 3. add audio overlay (this service)
194
+ 4. add captions if needed (captions service)
195
+
196
+ ### talking character for social
197
+ 1. generate character (image service)
198
+ 2. animate (video service)
199
+ 3. sync with voice (sync service)
200
+ 4. add captions (captions service)
201
+ 5. optimize for tiktok/instagram (this service)
202
+
203
+ ## tips
204
+
205
+ **social media optimization:**
206
+ - use platform-specific presets for correct aspect ratio
207
+ - vertical format (9:16) works for tiktok, instagram reels, youtube shorts
208
+ - landscape (16:9) works for youtube, twitter
209
+
210
+ **montage creation:**
211
+ - all clips should have similar resolution for best results
212
+ - use `maxClipDuration` to keep montage paced
213
+ - `targetResolution` ensures consistent quality
214
+
215
+ **trimming:**
216
+ - start time is in seconds
217
+ - end time is optional (omit to trim to end of video)
218
+ - use for extracting highlights or removing unwanted sections
219
+
220
+ ## environment variables
221
+
222
+ no api keys required - uses ffmpeg
223
+
224
+ **system requirements:**
225
+ - ffmpeg must be installed
226
+ - `brew install ffmpeg` (macos)
227
+ - `apt-get install ffmpeg` (linux)
228
+
229
+ ## processing time
230
+
231
+ depends on operation and video size:
232
+ - trim: 5-10 seconds
233
+ - resize: 10-30 seconds
234
+ - concat: 10-30 seconds per clip
235
+ - social optimization: 15-45 seconds
@@ -0,0 +1,437 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * video editing service
5
+ * combines multiple ffmpeg operations into common workflows
6
+ */
7
+
8
+ import { existsSync } from "node:fs";
9
+ import { extname } from "node:path";
10
+ import type { ActionMeta } from "../../cli/types";
11
+ import {
12
+ type AddAudioOptions,
13
+ addAudio,
14
+ type ConcatVideosOptions,
15
+ type ConvertFormatOptions,
16
+ concatVideos,
17
+ convertFormat,
18
+ extractAudio,
19
+ type ResizeVideoOptions,
20
+ resizeVideo,
21
+ type TrimVideoOptions,
22
+ trimVideo,
23
+ } from "../../lib/ffmpeg";
24
+
25
+ export const meta: ActionMeta = {
26
+ name: "edit",
27
+ type: "action",
28
+ description: "trim/resize video",
29
+ inputType: "video",
30
+ outputType: "video",
31
+ schema: {
32
+ input: {
33
+ type: "object",
34
+ required: ["input", "output"],
35
+ properties: {
36
+ input: {
37
+ type: "string",
38
+ format: "file-path",
39
+ description: "input video file",
40
+ },
41
+ output: {
42
+ type: "string",
43
+ format: "file-path",
44
+ description: "output video path",
45
+ },
46
+ start: {
47
+ type: "number",
48
+ description: "start time in seconds (for trim)",
49
+ },
50
+ duration: {
51
+ type: "number",
52
+ description: "duration in seconds (for trim)",
53
+ },
54
+ preset: {
55
+ type: "string",
56
+ enum: ["vertical", "square", "landscape", "4k"],
57
+ description: "resize preset",
58
+ },
59
+ },
60
+ },
61
+ output: { type: "string", format: "file-path", description: "video path" },
62
+ },
63
+ async run(options) {
64
+ const { input, output, start, duration, preset } = options as {
65
+ input: string;
66
+ output: string;
67
+ start?: number;
68
+ duration?: number;
69
+ preset?: "vertical" | "square" | "landscape" | "4k";
70
+ };
71
+ if (preset) {
72
+ return quickResize(input, output, preset);
73
+ }
74
+ if (start !== undefined) {
75
+ return quickTrim(
76
+ input,
77
+ output,
78
+ start,
79
+ duration ? start + duration : undefined,
80
+ );
81
+ }
82
+ throw new Error("specify --start for trim or --preset for resize");
83
+ },
84
+ };
85
+
86
+ // types
87
+ export interface EditPipelineStep {
88
+ operation:
89
+ | "concat"
90
+ | "add_audio"
91
+ | "resize"
92
+ | "trim"
93
+ | "convert"
94
+ | "extract_audio";
95
+ // options should contain all parameters except 'output' which is added by the pipeline
96
+ // biome-ignore lint/suspicious/noExplicitAny: pipeline options are validated at runtime by underlying ffmpeg functions
97
+ options: Record<string, any>;
98
+ }
99
+
100
+ export interface EditPipelineOptions {
101
+ steps: EditPipelineStep[];
102
+ finalOutput: string;
103
+ }
104
+
105
+ export interface PrepareForSocialOptions {
106
+ input: string;
107
+ output: string;
108
+ platform: "tiktok" | "instagram" | "youtube-shorts" | "youtube" | "twitter";
109
+ withAudio?: string;
110
+ }
111
+
112
+ export interface CreateMontageOptions {
113
+ clips: string[];
114
+ output: string;
115
+ maxClipDuration?: number;
116
+ targetResolution?: { width: number; height: number };
117
+ }
118
+
119
+ // core functions
120
+
121
+ /**
122
+ * run a series of editing operations in sequence
123
+ * each step uses output from previous step as input
124
+ */
125
+ export async function editPipeline(
126
+ options: EditPipelineOptions,
127
+ ): Promise<string> {
128
+ const { steps, finalOutput } = options;
129
+
130
+ if (!steps || steps.length === 0) {
131
+ throw new Error("at least one step is required");
132
+ }
133
+
134
+ console.log(`[edit] running ${steps.length} editing steps...`);
135
+
136
+ for (let i = 0; i < steps.length; i++) {
137
+ const step = steps[i];
138
+ if (!step) {
139
+ throw new Error(`step ${i} is undefined`);
140
+ }
141
+
142
+ const isLastStep = i === steps.length - 1;
143
+ const output = isLastStep
144
+ ? finalOutput
145
+ : `/tmp/edit-step-${i}${extname(finalOutput)}`;
146
+
147
+ console.log(`[edit] step ${i + 1}/${steps.length}: ${step.operation}`);
148
+
149
+ switch (step.operation) {
150
+ case "concat":
151
+ await concatVideos({
152
+ ...step.options,
153
+ output,
154
+ } as ConcatVideosOptions);
155
+ break;
156
+
157
+ case "add_audio":
158
+ await addAudio({
159
+ ...step.options,
160
+ output,
161
+ } as AddAudioOptions);
162
+ break;
163
+
164
+ case "resize":
165
+ await resizeVideo({
166
+ ...step.options,
167
+ output,
168
+ } as ResizeVideoOptions);
169
+ break;
170
+
171
+ case "trim":
172
+ await trimVideo({
173
+ ...step.options,
174
+ output,
175
+ } as TrimVideoOptions);
176
+ break;
177
+
178
+ case "convert":
179
+ await convertFormat({
180
+ ...step.options,
181
+ output,
182
+ } as ConvertFormatOptions);
183
+ break;
184
+
185
+ case "extract_audio":
186
+ await extractAudio((step.options as { input: string }).input, output);
187
+ break;
188
+
189
+ default:
190
+ throw new Error(`unknown operation: ${step.operation}`);
191
+ }
192
+ }
193
+
194
+ console.log(`[edit] pipeline complete: ${finalOutput}`);
195
+ return finalOutput;
196
+ }
197
+
198
+ /**
199
+ * prepare video for social media platform
200
+ * automatically sets correct aspect ratio and resolution
201
+ */
202
+ export async function prepareForSocial(
203
+ options: PrepareForSocialOptions,
204
+ ): Promise<string> {
205
+ const { input, output, platform, withAudio } = options;
206
+
207
+ if (!input || !output || !platform) {
208
+ throw new Error("input, output, and platform are required");
209
+ }
210
+
211
+ if (!existsSync(input)) {
212
+ throw new Error(`input file not found: ${input}`);
213
+ }
214
+
215
+ console.log(`[edit] preparing video for ${platform}...`);
216
+
217
+ const platformSpecs: Record<
218
+ string,
219
+ { width: number; height: number; aspectRatio: string }
220
+ > = {
221
+ tiktok: { width: 1080, height: 1920, aspectRatio: "9:16" },
222
+ instagram: { width: 1080, height: 1920, aspectRatio: "9:16" },
223
+ "youtube-shorts": { width: 1080, height: 1920, aspectRatio: "9:16" },
224
+ youtube: { width: 1920, height: 1080, aspectRatio: "16:9" },
225
+ twitter: { width: 1280, height: 720, aspectRatio: "16:9" },
226
+ };
227
+
228
+ const spec = platformSpecs[platform];
229
+ if (!spec) {
230
+ throw new Error(`unknown platform: ${platform}`);
231
+ }
232
+
233
+ const steps: EditPipelineStep[] = [];
234
+
235
+ // resize to platform specs
236
+ steps.push({
237
+ operation: "resize",
238
+ options: {
239
+ input,
240
+ width: spec.width,
241
+ height: spec.height,
242
+ },
243
+ });
244
+
245
+ // add audio if provided
246
+ if (withAudio) {
247
+ if (!existsSync(withAudio)) {
248
+ throw new Error(`audio file not found: ${withAudio}`);
249
+ }
250
+ steps.push({
251
+ operation: "add_audio",
252
+ options: {
253
+ videoPath: input,
254
+ audioPath: withAudio,
255
+ },
256
+ });
257
+ }
258
+
259
+ return editPipeline({ steps, finalOutput: output });
260
+ }
261
+
262
+ /**
263
+ * create a montage from multiple video clips
264
+ * optionally trim clips and resize to consistent resolution
265
+ */
266
+ export async function createMontage(
267
+ options: CreateMontageOptions,
268
+ ): Promise<string> {
269
+ const { clips, output, maxClipDuration, targetResolution } = options;
270
+
271
+ if (!clips || clips.length === 0) {
272
+ throw new Error("at least one clip is required");
273
+ }
274
+ if (!output) {
275
+ throw new Error("output is required");
276
+ }
277
+
278
+ console.log(`[edit] creating montage from ${clips.length} clips...`);
279
+
280
+ // validate all clips exist
281
+ for (const clip of clips) {
282
+ if (!existsSync(clip)) {
283
+ throw new Error(`clip not found: ${clip}`);
284
+ }
285
+ }
286
+
287
+ let processedClips = clips;
288
+
289
+ // trim clips if max duration specified
290
+ if (maxClipDuration) {
291
+ console.log(`[edit] trimming clips to ${maxClipDuration}s...`);
292
+ processedClips = [];
293
+
294
+ for (let i = 0; i < clips.length; i++) {
295
+ const clip = clips[i];
296
+ if (!clip) {
297
+ throw new Error(`clip ${i} is undefined`);
298
+ }
299
+ const trimmedPath = `/tmp/montage-clip-${i}${extname(clip)}`;
300
+ await trimVideo({
301
+ input: clip,
302
+ output: trimmedPath,
303
+ start: 0,
304
+ duration: maxClipDuration,
305
+ });
306
+ processedClips.push(trimmedPath);
307
+ }
308
+ }
309
+
310
+ // resize clips if target resolution specified
311
+ if (targetResolution) {
312
+ console.log(
313
+ `[edit] resizing clips to ${targetResolution.width}x${targetResolution.height}...`,
314
+ );
315
+ const resizedClips = [];
316
+
317
+ for (let i = 0; i < processedClips.length; i++) {
318
+ const clip = processedClips[i];
319
+ if (!clip) {
320
+ throw new Error(`clip ${i} is undefined`);
321
+ }
322
+ const resizedPath = `/tmp/montage-resized-${i}${extname(clip)}`;
323
+ await resizeVideo({
324
+ input: clip,
325
+ output: resizedPath,
326
+ width: targetResolution.width,
327
+ height: targetResolution.height,
328
+ });
329
+ resizedClips.push(resizedPath);
330
+ }
331
+
332
+ processedClips = resizedClips;
333
+ }
334
+
335
+ // concatenate all clips
336
+ return concatVideos({
337
+ inputs: processedClips,
338
+ output,
339
+ });
340
+ }
341
+
342
+ /**
343
+ * quick trim: trim video to specific segment
344
+ */
345
+ export async function quickTrim(
346
+ input: string,
347
+ output: string,
348
+ start: number,
349
+ end?: number,
350
+ ): Promise<string> {
351
+ if (!input || !output) {
352
+ throw new Error("input and output are required");
353
+ }
354
+
355
+ const duration = end ? end - start : undefined;
356
+
357
+ console.log(
358
+ `[edit] trimming video from ${start}s${duration ? ` for ${duration}s` : ""}...`,
359
+ );
360
+
361
+ return trimVideo({ input, output, start, duration });
362
+ }
363
+
364
+ /**
365
+ * quick resize: resize video to common aspect ratios
366
+ */
367
+ export async function quickResize(
368
+ input: string,
369
+ output: string,
370
+ preset: "vertical" | "square" | "landscape" | "4k",
371
+ ): Promise<string> {
372
+ if (!input || !output) {
373
+ throw new Error("input and output are required");
374
+ }
375
+
376
+ const presets: Record<
377
+ string,
378
+ { width: number; height: number; label: string }
379
+ > = {
380
+ vertical: { width: 1080, height: 1920, label: "9:16 (1080x1920)" },
381
+ square: { width: 1080, height: 1080, label: "1:1 (1080x1080)" },
382
+ landscape: { width: 1920, height: 1080, label: "16:9 (1920x1080)" },
383
+ "4k": { width: 3840, height: 2160, label: "4K (3840x2160)" },
384
+ };
385
+
386
+ const spec = presets[preset];
387
+ if (!spec) {
388
+ throw new Error(`unknown preset: ${preset}`);
389
+ }
390
+
391
+ console.log(`[edit] resizing to ${spec.label}...`);
392
+
393
+ return resizeVideo({
394
+ input,
395
+ output,
396
+ width: spec.width,
397
+ height: spec.height,
398
+ });
399
+ }
400
+
401
+ /**
402
+ * merge multiple videos with optional audio overlay
403
+ */
404
+ export async function mergeWithAudio(
405
+ videos: string[],
406
+ audio: string,
407
+ output: string,
408
+ ): Promise<string> {
409
+ if (!videos || videos.length === 0) {
410
+ throw new Error("at least one video is required");
411
+ }
412
+ if (!audio || !output) {
413
+ throw new Error("audio and output are required");
414
+ }
415
+
416
+ console.log(`[edit] merging ${videos.length} videos with audio...`);
417
+
418
+ // first concatenate videos
419
+ const tempVideo = `/tmp/merged-video${extname(output)}`;
420
+ await concatVideos({
421
+ inputs: videos,
422
+ output: tempVideo,
423
+ });
424
+
425
+ // then add audio
426
+ return addAudio({
427
+ videoPath: tempVideo,
428
+ audioPath: audio,
429
+ output,
430
+ });
431
+ }
432
+
433
+ // cli
434
+ if (import.meta.main) {
435
+ const { runCli } = await import("../../cli/runner");
436
+ runCli(meta);
437
+ }