varg.ai-sdk 0.1.0 → 0.4.0-alpha.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.
- package/.claude/settings.local.json +1 -1
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +23 -0
- package/.husky/README.md +102 -0
- package/.husky/commit-msg +6 -0
- package/.husky/pre-commit +9 -0
- package/.husky/pre-push +6 -0
- package/.size-limit.json +8 -0
- package/.test-hooks.ts +5 -0
- package/CLAUDE.md +10 -3
- package/CONTRIBUTING.md +150 -0
- package/LICENSE.md +53 -0
- package/README.md +56 -209
- package/SKILLS.md +26 -10
- package/biome.json +7 -1
- package/bun.lock +1286 -0
- package/commitlint.config.js +22 -0
- package/docs/index.html +1130 -0
- package/docs/prompting.md +326 -0
- package/docs/react.md +834 -0
- package/docs/sdk.md +812 -0
- package/ffmpeg/CLAUDE.md +68 -0
- package/package.json +48 -8
- package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
- package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
- package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
- package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
- package/pipeline/cookbooks/text-to-tiktok.md +669 -0
- package/pipeline/cookbooks/trendwatching.md +156 -0
- package/plan.md +281 -0
- package/scripts/.gitkeep +0 -0
- package/src/ai-sdk/cache.ts +142 -0
- package/src/ai-sdk/examples/cached-generation.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
- package/src/ai-sdk/examples/duet-video.ts +56 -0
- package/src/ai-sdk/examples/editly-composition.ts +63 -0
- package/src/ai-sdk/examples/editly-test.ts +57 -0
- package/src/ai-sdk/examples/editly-video-test.ts +52 -0
- package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
- package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
- package/src/ai-sdk/examples/music-generation.ts +19 -0
- package/src/ai-sdk/examples/openai-sora.ts +34 -0
- package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
- package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
- package/src/ai-sdk/examples/talking-lion.ts +55 -0
- package/src/ai-sdk/examples/video-generation.ts +39 -0
- package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
- package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
- package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
- package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
- package/src/ai-sdk/file-cache.ts +112 -0
- package/src/ai-sdk/file.ts +238 -0
- package/src/ai-sdk/generate-element.ts +92 -0
- package/src/ai-sdk/generate-music.ts +46 -0
- package/src/ai-sdk/generate-video.ts +165 -0
- package/src/ai-sdk/index.ts +72 -0
- package/src/ai-sdk/music-model.ts +110 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
- package/src/ai-sdk/providers/editly/index.ts +817 -0
- package/src/ai-sdk/providers/editly/layers.ts +776 -0
- package/src/ai-sdk/providers/editly/plan.md +144 -0
- package/src/ai-sdk/providers/editly/types.ts +328 -0
- package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
- package/src/ai-sdk/providers/fal-provider.ts +512 -0
- package/src/ai-sdk/providers/higgsfield.ts +379 -0
- package/src/ai-sdk/providers/openai.ts +251 -0
- package/src/ai-sdk/providers/replicate.ts +16 -0
- package/src/ai-sdk/video-model.ts +185 -0
- package/src/cli/commands/find.tsx +137 -0
- package/src/cli/commands/help.tsx +85 -0
- package/src/cli/commands/index.ts +6 -0
- package/src/cli/commands/list.tsx +238 -0
- package/src/cli/commands/render.tsx +71 -0
- package/src/cli/commands/run.tsx +511 -0
- package/src/cli/commands/which.tsx +253 -0
- package/src/cli/index.ts +114 -0
- package/src/cli/quiet.ts +44 -0
- package/src/cli/types.ts +32 -0
- package/src/cli/ui/components/Badge.tsx +29 -0
- package/src/cli/ui/components/DataTable.tsx +51 -0
- package/src/cli/ui/components/Header.tsx +23 -0
- package/src/cli/ui/components/HelpBlock.tsx +44 -0
- package/src/cli/ui/components/KeyValue.tsx +33 -0
- package/src/cli/ui/components/OptionRow.tsx +81 -0
- package/src/cli/ui/components/Separator.tsx +23 -0
- package/src/cli/ui/components/StatusBox.tsx +108 -0
- package/src/cli/ui/components/VargBox.tsx +51 -0
- package/src/cli/ui/components/VargProgress.tsx +36 -0
- package/src/cli/ui/components/VargSpinner.tsx +34 -0
- package/src/cli/ui/components/VargText.tsx +56 -0
- package/src/cli/ui/components/index.ts +19 -0
- package/src/cli/ui/index.ts +12 -0
- package/src/cli/ui/render.ts +35 -0
- package/src/cli/ui/theme.ts +63 -0
- package/src/cli/utils.ts +78 -0
- package/src/core/executor/executor.ts +201 -0
- package/src/core/executor/index.ts +13 -0
- package/src/core/executor/job.ts +214 -0
- package/src/core/executor/pipeline.ts +222 -0
- package/src/core/index.ts +11 -0
- package/src/core/registry/index.ts +9 -0
- package/src/core/registry/loader.ts +149 -0
- package/src/core/registry/registry.ts +221 -0
- package/src/core/registry/resolver.ts +206 -0
- package/src/core/schema/helpers.ts +134 -0
- package/src/core/schema/index.ts +8 -0
- package/src/core/schema/shared.ts +102 -0
- package/src/core/schema/types.ts +279 -0
- package/src/core/schema/validator.ts +92 -0
- package/src/definitions/actions/captions.ts +261 -0
- package/src/definitions/actions/edit.ts +298 -0
- package/src/definitions/actions/image.ts +125 -0
- package/src/definitions/actions/index.ts +114 -0
- package/src/definitions/actions/music.ts +205 -0
- package/src/definitions/actions/sync.ts +128 -0
- package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +63 -90
- package/src/definitions/actions/upload.ts +111 -0
- package/src/definitions/actions/video.ts +163 -0
- package/src/definitions/actions/voice.ts +119 -0
- package/src/definitions/index.ts +23 -0
- package/src/definitions/models/elevenlabs.ts +50 -0
- package/src/definitions/models/flux.ts +56 -0
- package/src/definitions/models/index.ts +36 -0
- package/src/definitions/models/kling.ts +56 -0
- package/src/definitions/models/llama.ts +54 -0
- package/src/definitions/models/nano-banana-pro.ts +102 -0
- package/src/definitions/models/sonauto.ts +68 -0
- package/src/definitions/models/soul.ts +65 -0
- package/src/definitions/models/wan.ts +54 -0
- package/src/definitions/models/whisper.ts +44 -0
- package/src/definitions/skills/index.ts +12 -0
- package/src/definitions/skills/talking-character.ts +87 -0
- package/src/definitions/skills/text-to-tiktok.ts +97 -0
- package/src/index.ts +118 -0
- package/src/providers/apify.ts +269 -0
- package/src/providers/base.ts +264 -0
- package/src/providers/elevenlabs.ts +217 -0
- package/src/providers/fal.ts +392 -0
- package/src/providers/ffmpeg.ts +544 -0
- package/src/providers/fireworks.ts +193 -0
- package/src/providers/groq.ts +149 -0
- package/src/providers/higgsfield.ts +145 -0
- package/src/providers/index.ts +143 -0
- package/src/providers/replicate.ts +147 -0
- package/src/providers/storage.ts +206 -0
- package/src/react/cli.ts +52 -0
- package/src/react/elements.ts +146 -0
- package/src/react/examples/branching.tsx +66 -0
- package/src/react/examples/captions-demo.tsx +37 -0
- package/src/react/examples/character-video.tsx +84 -0
- package/src/react/examples/grid.tsx +53 -0
- package/src/react/examples/layouts-demo.tsx +57 -0
- package/src/react/examples/madi.tsx +60 -0
- package/src/react/examples/music-test.tsx +35 -0
- package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
- package/src/react/examples/orange-portrait.tsx +41 -0
- package/src/react/examples/split-element-demo.tsx +60 -0
- package/src/react/examples/split-layout-demo.tsx +60 -0
- package/src/react/examples/split.tsx +41 -0
- package/src/react/examples/video-grid.tsx +46 -0
- package/src/react/index.ts +43 -0
- package/src/react/layouts/grid.tsx +28 -0
- package/src/react/layouts/index.ts +2 -0
- package/src/react/layouts/split.tsx +20 -0
- package/src/react/react.test.ts +309 -0
- package/src/react/render.ts +21 -0
- package/src/react/renderers/animate.ts +59 -0
- package/src/react/renderers/captions.ts +297 -0
- package/src/react/renderers/clip.ts +248 -0
- package/src/react/renderers/context.ts +17 -0
- package/src/react/renderers/image.ts +109 -0
- package/src/react/renderers/index.ts +22 -0
- package/src/react/renderers/music.ts +60 -0
- package/src/react/renderers/packshot.ts +84 -0
- package/src/react/renderers/progress.ts +173 -0
- package/src/react/renderers/render.ts +243 -0
- package/src/react/renderers/slider.ts +69 -0
- package/src/react/renderers/speech.ts +53 -0
- package/src/react/renderers/split.ts +91 -0
- package/src/react/renderers/subtitle.ts +16 -0
- package/src/react/renderers/swipe.ts +75 -0
- package/src/react/renderers/title.ts +17 -0
- package/src/react/renderers/utils.ts +124 -0
- package/src/react/renderers/video.ts +127 -0
- package/src/react/runtime/jsx-dev-runtime.ts +43 -0
- package/src/react/runtime/jsx-runtime.ts +35 -0
- package/src/react/types.ts +232 -0
- package/src/studio/index.ts +26 -0
- package/src/studio/scanner.ts +102 -0
- package/src/studio/server.ts +554 -0
- package/src/studio/stages.ts +251 -0
- package/src/studio/step-renderer.ts +279 -0
- package/src/studio/types.ts +60 -0
- package/src/studio/ui/cache.html +303 -0
- package/src/studio/ui/index.html +1820 -0
- package/src/tests/all.test.ts +509 -0
- package/src/tests/index.ts +33 -0
- package/src/tests/unit.test.ts +403 -0
- package/tsconfig.cli.json +8 -0
- package/tsconfig.json +21 -3
- package/TEST_RESULTS.md +0 -122
- package/action/captions/SKILL.md +0 -170
- package/action/captions/index.ts +0 -227
- package/action/edit/SKILL.md +0 -235
- package/action/edit/index.ts +0 -493
- package/action/image/SKILL.md +0 -140
- package/action/image/index.ts +0 -112
- package/action/sync/SKILL.md +0 -136
- package/action/sync/index.ts +0 -187
- package/action/transcribe/SKILL.md +0 -179
- package/action/video/SKILL.md +0 -116
- package/action/video/index.ts +0 -135
- package/action/voice/SKILL.md +0 -125
- package/action/voice/index.ts +0 -201
- package/index.ts +0 -38
- package/lib/README.md +0 -144
- package/lib/ai-sdk/fal.ts +0 -106
- package/lib/ai-sdk/replicate.ts +0 -107
- package/lib/elevenlabs.ts +0 -382
- package/lib/fal.ts +0 -478
- package/lib/ffmpeg.ts +0 -467
- package/lib/fireworks.ts +0 -235
- package/lib/groq.ts +0 -246
- package/lib/higgsfield.ts +0 -176
- package/lib/remotion/SKILL.md +0 -823
- package/lib/remotion/cli.ts +0 -115
- package/lib/remotion/functions.ts +0 -283
- package/lib/remotion/index.ts +0 -19
- package/lib/remotion/templates.ts +0 -73
- package/lib/replicate.ts +0 -304
- package/output.txt +0 -1
- package/test-import.ts +0 -7
- package/test-services.ts +0 -97
- package/utilities/s3.ts +0 -147
package/action/edit/index.ts
DELETED
|
@@ -1,493 +0,0 @@
|
|
|
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 {
|
|
11
|
-
type AddAudioOptions,
|
|
12
|
-
addAudio,
|
|
13
|
-
type ConcatVideosOptions,
|
|
14
|
-
type ConvertFormatOptions,
|
|
15
|
-
concatVideos,
|
|
16
|
-
convertFormat,
|
|
17
|
-
extractAudio,
|
|
18
|
-
type ResizeVideoOptions,
|
|
19
|
-
resizeVideo,
|
|
20
|
-
type TrimVideoOptions,
|
|
21
|
-
trimVideo,
|
|
22
|
-
} from "../../lib/ffmpeg";
|
|
23
|
-
|
|
24
|
-
// types
|
|
25
|
-
export interface EditPipelineStep {
|
|
26
|
-
operation:
|
|
27
|
-
| "concat"
|
|
28
|
-
| "add_audio"
|
|
29
|
-
| "resize"
|
|
30
|
-
| "trim"
|
|
31
|
-
| "convert"
|
|
32
|
-
| "extract_audio";
|
|
33
|
-
// options should contain all parameters except 'output' which is added by the pipeline
|
|
34
|
-
// biome-ignore lint/suspicious/noExplicitAny: pipeline options are validated at runtime by underlying ffmpeg functions
|
|
35
|
-
options: Record<string, any>;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface EditPipelineOptions {
|
|
39
|
-
steps: EditPipelineStep[];
|
|
40
|
-
finalOutput: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface PrepareForSocialOptions {
|
|
44
|
-
input: string;
|
|
45
|
-
output: string;
|
|
46
|
-
platform: "tiktok" | "instagram" | "youtube-shorts" | "youtube" | "twitter";
|
|
47
|
-
withAudio?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface CreateMontageOptions {
|
|
51
|
-
clips: string[];
|
|
52
|
-
output: string;
|
|
53
|
-
maxClipDuration?: number;
|
|
54
|
-
targetResolution?: { width: number; height: number };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// core functions
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* run a series of editing operations in sequence
|
|
61
|
-
* each step uses output from previous step as input
|
|
62
|
-
*/
|
|
63
|
-
export async function editPipeline(
|
|
64
|
-
options: EditPipelineOptions,
|
|
65
|
-
): Promise<string> {
|
|
66
|
-
const { steps, finalOutput } = options;
|
|
67
|
-
|
|
68
|
-
if (!steps || steps.length === 0) {
|
|
69
|
-
throw new Error("at least one step is required");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
console.log(`[edit] running ${steps.length} editing steps...`);
|
|
73
|
-
|
|
74
|
-
for (let i = 0; i < steps.length; i++) {
|
|
75
|
-
const step = steps[i];
|
|
76
|
-
if (!step) {
|
|
77
|
-
throw new Error(`step ${i} is undefined`);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const isLastStep = i === steps.length - 1;
|
|
81
|
-
const output = isLastStep
|
|
82
|
-
? finalOutput
|
|
83
|
-
: `/tmp/edit-step-${i}${extname(finalOutput)}`;
|
|
84
|
-
|
|
85
|
-
console.log(`[edit] step ${i + 1}/${steps.length}: ${step.operation}`);
|
|
86
|
-
|
|
87
|
-
switch (step.operation) {
|
|
88
|
-
case "concat":
|
|
89
|
-
await concatVideos({
|
|
90
|
-
...step.options,
|
|
91
|
-
output,
|
|
92
|
-
} as ConcatVideosOptions);
|
|
93
|
-
break;
|
|
94
|
-
|
|
95
|
-
case "add_audio":
|
|
96
|
-
await addAudio({
|
|
97
|
-
...step.options,
|
|
98
|
-
output,
|
|
99
|
-
} as AddAudioOptions);
|
|
100
|
-
break;
|
|
101
|
-
|
|
102
|
-
case "resize":
|
|
103
|
-
await resizeVideo({
|
|
104
|
-
...step.options,
|
|
105
|
-
output,
|
|
106
|
-
} as ResizeVideoOptions);
|
|
107
|
-
break;
|
|
108
|
-
|
|
109
|
-
case "trim":
|
|
110
|
-
await trimVideo({
|
|
111
|
-
...step.options,
|
|
112
|
-
output,
|
|
113
|
-
} as TrimVideoOptions);
|
|
114
|
-
break;
|
|
115
|
-
|
|
116
|
-
case "convert":
|
|
117
|
-
await convertFormat({
|
|
118
|
-
...step.options,
|
|
119
|
-
output,
|
|
120
|
-
} as ConvertFormatOptions);
|
|
121
|
-
break;
|
|
122
|
-
|
|
123
|
-
case "extract_audio":
|
|
124
|
-
await extractAudio((step.options as { input: string }).input, output);
|
|
125
|
-
break;
|
|
126
|
-
|
|
127
|
-
default:
|
|
128
|
-
throw new Error(`unknown operation: ${step.operation}`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
console.log(`[edit] pipeline complete: ${finalOutput}`);
|
|
133
|
-
return finalOutput;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* prepare video for social media platform
|
|
138
|
-
* automatically sets correct aspect ratio and resolution
|
|
139
|
-
*/
|
|
140
|
-
export async function prepareForSocial(
|
|
141
|
-
options: PrepareForSocialOptions,
|
|
142
|
-
): Promise<string> {
|
|
143
|
-
const { input, output, platform, withAudio } = options;
|
|
144
|
-
|
|
145
|
-
if (!input || !output || !platform) {
|
|
146
|
-
throw new Error("input, output, and platform are required");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (!existsSync(input)) {
|
|
150
|
-
throw new Error(`input file not found: ${input}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
console.log(`[edit] preparing video for ${platform}...`);
|
|
154
|
-
|
|
155
|
-
const platformSpecs: Record<
|
|
156
|
-
string,
|
|
157
|
-
{ width: number; height: number; aspectRatio: string }
|
|
158
|
-
> = {
|
|
159
|
-
tiktok: { width: 1080, height: 1920, aspectRatio: "9:16" },
|
|
160
|
-
instagram: { width: 1080, height: 1920, aspectRatio: "9:16" },
|
|
161
|
-
"youtube-shorts": { width: 1080, height: 1920, aspectRatio: "9:16" },
|
|
162
|
-
youtube: { width: 1920, height: 1080, aspectRatio: "16:9" },
|
|
163
|
-
twitter: { width: 1280, height: 720, aspectRatio: "16:9" },
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
const spec = platformSpecs[platform];
|
|
167
|
-
if (!spec) {
|
|
168
|
-
throw new Error(`unknown platform: ${platform}`);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const steps: EditPipelineStep[] = [];
|
|
172
|
-
|
|
173
|
-
// resize to platform specs
|
|
174
|
-
steps.push({
|
|
175
|
-
operation: "resize",
|
|
176
|
-
options: {
|
|
177
|
-
input,
|
|
178
|
-
width: spec.width,
|
|
179
|
-
height: spec.height,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// add audio if provided
|
|
184
|
-
if (withAudio) {
|
|
185
|
-
if (!existsSync(withAudio)) {
|
|
186
|
-
throw new Error(`audio file not found: ${withAudio}`);
|
|
187
|
-
}
|
|
188
|
-
steps.push({
|
|
189
|
-
operation: "add_audio",
|
|
190
|
-
options: {
|
|
191
|
-
videoPath: input,
|
|
192
|
-
audioPath: withAudio,
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
return editPipeline({ steps, finalOutput: output });
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* create a montage from multiple video clips
|
|
202
|
-
* optionally trim clips and resize to consistent resolution
|
|
203
|
-
*/
|
|
204
|
-
export async function createMontage(
|
|
205
|
-
options: CreateMontageOptions,
|
|
206
|
-
): Promise<string> {
|
|
207
|
-
const { clips, output, maxClipDuration, targetResolution } = options;
|
|
208
|
-
|
|
209
|
-
if (!clips || clips.length === 0) {
|
|
210
|
-
throw new Error("at least one clip is required");
|
|
211
|
-
}
|
|
212
|
-
if (!output) {
|
|
213
|
-
throw new Error("output is required");
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
console.log(`[edit] creating montage from ${clips.length} clips...`);
|
|
217
|
-
|
|
218
|
-
// validate all clips exist
|
|
219
|
-
for (const clip of clips) {
|
|
220
|
-
if (!existsSync(clip)) {
|
|
221
|
-
throw new Error(`clip not found: ${clip}`);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
let processedClips = clips;
|
|
226
|
-
|
|
227
|
-
// trim clips if max duration specified
|
|
228
|
-
if (maxClipDuration) {
|
|
229
|
-
console.log(`[edit] trimming clips to ${maxClipDuration}s...`);
|
|
230
|
-
processedClips = [];
|
|
231
|
-
|
|
232
|
-
for (let i = 0; i < clips.length; i++) {
|
|
233
|
-
const clip = clips[i];
|
|
234
|
-
if (!clip) {
|
|
235
|
-
throw new Error(`clip ${i} is undefined`);
|
|
236
|
-
}
|
|
237
|
-
const trimmedPath = `/tmp/montage-clip-${i}${extname(clip)}`;
|
|
238
|
-
await trimVideo({
|
|
239
|
-
input: clip,
|
|
240
|
-
output: trimmedPath,
|
|
241
|
-
start: 0,
|
|
242
|
-
duration: maxClipDuration,
|
|
243
|
-
});
|
|
244
|
-
processedClips.push(trimmedPath);
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// resize clips if target resolution specified
|
|
249
|
-
if (targetResolution) {
|
|
250
|
-
console.log(
|
|
251
|
-
`[edit] resizing clips to ${targetResolution.width}x${targetResolution.height}...`,
|
|
252
|
-
);
|
|
253
|
-
const resizedClips = [];
|
|
254
|
-
|
|
255
|
-
for (let i = 0; i < processedClips.length; i++) {
|
|
256
|
-
const clip = processedClips[i];
|
|
257
|
-
if (!clip) {
|
|
258
|
-
throw new Error(`clip ${i} is undefined`);
|
|
259
|
-
}
|
|
260
|
-
const resizedPath = `/tmp/montage-resized-${i}${extname(clip)}`;
|
|
261
|
-
await resizeVideo({
|
|
262
|
-
input: clip,
|
|
263
|
-
output: resizedPath,
|
|
264
|
-
width: targetResolution.width,
|
|
265
|
-
height: targetResolution.height,
|
|
266
|
-
});
|
|
267
|
-
resizedClips.push(resizedPath);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
processedClips = resizedClips;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// concatenate all clips
|
|
274
|
-
return concatVideos({
|
|
275
|
-
inputs: processedClips,
|
|
276
|
-
output,
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* quick trim: trim video to specific segment
|
|
282
|
-
*/
|
|
283
|
-
export async function quickTrim(
|
|
284
|
-
input: string,
|
|
285
|
-
output: string,
|
|
286
|
-
start: number,
|
|
287
|
-
end?: number,
|
|
288
|
-
): Promise<string> {
|
|
289
|
-
if (!input || !output) {
|
|
290
|
-
throw new Error("input and output are required");
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
const duration = end ? end - start : undefined;
|
|
294
|
-
|
|
295
|
-
console.log(
|
|
296
|
-
`[edit] trimming video from ${start}s${duration ? ` for ${duration}s` : ""}...`,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
return trimVideo({ input, output, start, duration });
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* quick resize: resize video to common aspect ratios
|
|
304
|
-
*/
|
|
305
|
-
export async function quickResize(
|
|
306
|
-
input: string,
|
|
307
|
-
output: string,
|
|
308
|
-
preset: "vertical" | "square" | "landscape" | "4k",
|
|
309
|
-
): Promise<string> {
|
|
310
|
-
if (!input || !output) {
|
|
311
|
-
throw new Error("input and output are required");
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const presets: Record<
|
|
315
|
-
string,
|
|
316
|
-
{ width: number; height: number; label: string }
|
|
317
|
-
> = {
|
|
318
|
-
vertical: { width: 1080, height: 1920, label: "9:16 (1080x1920)" },
|
|
319
|
-
square: { width: 1080, height: 1080, label: "1:1 (1080x1080)" },
|
|
320
|
-
landscape: { width: 1920, height: 1080, label: "16:9 (1920x1080)" },
|
|
321
|
-
"4k": { width: 3840, height: 2160, label: "4K (3840x2160)" },
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const spec = presets[preset];
|
|
325
|
-
if (!spec) {
|
|
326
|
-
throw new Error(`unknown preset: ${preset}`);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
console.log(`[edit] resizing to ${spec.label}...`);
|
|
330
|
-
|
|
331
|
-
return resizeVideo({
|
|
332
|
-
input,
|
|
333
|
-
output,
|
|
334
|
-
width: spec.width,
|
|
335
|
-
height: spec.height,
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* merge multiple videos with optional audio overlay
|
|
341
|
-
*/
|
|
342
|
-
export async function mergeWithAudio(
|
|
343
|
-
videos: string[],
|
|
344
|
-
audio: string,
|
|
345
|
-
output: string,
|
|
346
|
-
): Promise<string> {
|
|
347
|
-
if (!videos || videos.length === 0) {
|
|
348
|
-
throw new Error("at least one video is required");
|
|
349
|
-
}
|
|
350
|
-
if (!audio || !output) {
|
|
351
|
-
throw new Error("audio and output are required");
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
console.log(`[edit] merging ${videos.length} videos with audio...`);
|
|
355
|
-
|
|
356
|
-
// first concatenate videos
|
|
357
|
-
const tempVideo = `/tmp/merged-video${extname(output)}`;
|
|
358
|
-
await concatVideos({
|
|
359
|
-
inputs: videos,
|
|
360
|
-
output: tempVideo,
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
// then add audio
|
|
364
|
-
return addAudio({
|
|
365
|
-
videoPath: tempVideo,
|
|
366
|
-
audioPath: audio,
|
|
367
|
-
output,
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// cli
|
|
372
|
-
async function cli() {
|
|
373
|
-
const args = process.argv.slice(2);
|
|
374
|
-
const command = args[0];
|
|
375
|
-
|
|
376
|
-
if (!command || command === "help") {
|
|
377
|
-
console.log(`
|
|
378
|
-
usage:
|
|
379
|
-
bun run service/edit.ts <command> [args]
|
|
380
|
-
|
|
381
|
-
commands:
|
|
382
|
-
social <input> <output> <platform> [audioPath] prepare for social media
|
|
383
|
-
montage <output> <clip1> <clip2> [clip3...] create montage from clips
|
|
384
|
-
trim <input> <output> <start> [end] quick trim
|
|
385
|
-
resize <input> <output> <preset> quick resize
|
|
386
|
-
merge_audio <audio> <output> <video1> [video2...] merge videos with audio
|
|
387
|
-
|
|
388
|
-
platforms:
|
|
389
|
-
tiktok, instagram, youtube-shorts, youtube, twitter
|
|
390
|
-
|
|
391
|
-
resize presets:
|
|
392
|
-
vertical (9:16), square (1:1), landscape (16:9), 4k
|
|
393
|
-
|
|
394
|
-
examples:
|
|
395
|
-
bun run service/edit.ts social raw.mp4 tiktok.mp4 tiktok
|
|
396
|
-
bun run service/edit.ts social raw.mp4 ig.mp4 instagram audio.mp3
|
|
397
|
-
bun run service/edit.ts montage output.mp4 clip1.mp4 clip2.mp4 clip3.mp4
|
|
398
|
-
bun run service/edit.ts trim long.mp4 short.mp4 10 30
|
|
399
|
-
bun run service/edit.ts resize raw.mp4 vertical.mp4 vertical
|
|
400
|
-
bun run service/edit.ts merge_audio song.mp3 final.mp4 clip1.mp4 clip2.mp4
|
|
401
|
-
`);
|
|
402
|
-
process.exit(0);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
switch (command) {
|
|
407
|
-
case "social": {
|
|
408
|
-
const input = args[1];
|
|
409
|
-
const output = args[2];
|
|
410
|
-
const platform = args[3] as PrepareForSocialOptions["platform"];
|
|
411
|
-
const withAudio = args[4];
|
|
412
|
-
|
|
413
|
-
if (!input || !output || !platform) {
|
|
414
|
-
throw new Error("input, output, and platform are required");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
await prepareForSocial({ input, output, platform, withAudio });
|
|
418
|
-
break;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
case "montage": {
|
|
422
|
-
const output = args[1];
|
|
423
|
-
const clips = args.slice(2);
|
|
424
|
-
|
|
425
|
-
if (!output || clips.length === 0) {
|
|
426
|
-
throw new Error("output and at least one clip are required");
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
await createMontage({ clips, output });
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
case "trim": {
|
|
434
|
-
const input = args[1];
|
|
435
|
-
const output = args[2];
|
|
436
|
-
const startArg = args[3];
|
|
437
|
-
const endArg = args[4];
|
|
438
|
-
|
|
439
|
-
if (!input || !output || !startArg) {
|
|
440
|
-
throw new Error("input, output, and start are required");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
const start = Number.parseFloat(startArg);
|
|
444
|
-
const end = endArg ? Number.parseFloat(endArg) : undefined;
|
|
445
|
-
|
|
446
|
-
if (Number.isNaN(start) || (endArg && Number.isNaN(end))) {
|
|
447
|
-
throw new Error("start and end must be valid numbers");
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
await quickTrim(input, output, start, end);
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
case "resize": {
|
|
455
|
-
const input = args[1];
|
|
456
|
-
const output = args[2];
|
|
457
|
-
const preset = args[3] as "vertical" | "square" | "landscape" | "4k";
|
|
458
|
-
|
|
459
|
-
if (!input || !output || !preset) {
|
|
460
|
-
throw new Error("input, output, and preset are required");
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
await quickResize(input, output, preset);
|
|
464
|
-
break;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
case "merge_audio": {
|
|
468
|
-
const audio = args[1];
|
|
469
|
-
const output = args[2];
|
|
470
|
-
const videos = args.slice(3);
|
|
471
|
-
|
|
472
|
-
if (!audio || !output || videos.length === 0) {
|
|
473
|
-
throw new Error("audio, output, and at least one video are required");
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
await mergeWithAudio(videos, audio, output);
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
default:
|
|
481
|
-
console.error(`unknown command: ${command}`);
|
|
482
|
-
console.log("run 'bun run service/edit.ts help' for usage");
|
|
483
|
-
process.exit(1);
|
|
484
|
-
}
|
|
485
|
-
} catch (error) {
|
|
486
|
-
console.error("[edit] error:", error);
|
|
487
|
-
process.exit(1);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (import.meta.main) {
|
|
492
|
-
cli();
|
|
493
|
-
}
|
package/action/image/SKILL.md
DELETED
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: image-generation
|
|
3
|
-
description: generate ai images using fal (flux models) or higgsfield soul characters. use when user wants to create images, headshots, character portraits, or needs image generation with specific models.
|
|
4
|
-
allowed-tools: Read, Bash
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
# image generation
|
|
8
|
-
|
|
9
|
-
generate ai images using multiple providers with automatic s3 upload support.
|
|
10
|
-
|
|
11
|
-
## providers
|
|
12
|
-
|
|
13
|
-
### fal (flux models)
|
|
14
|
-
- high quality image generation
|
|
15
|
-
- supports flux-pro, flux-dev, and other flux models
|
|
16
|
-
- configurable model selection
|
|
17
|
-
- automatic image opening on generation
|
|
18
|
-
|
|
19
|
-
### higgsfield soul
|
|
20
|
-
- character headshot generation
|
|
21
|
-
- consistent character style
|
|
22
|
-
- professional portrait quality
|
|
23
|
-
- custom style references
|
|
24
|
-
|
|
25
|
-
## usage
|
|
26
|
-
|
|
27
|
-
### generate with fal
|
|
28
|
-
```bash
|
|
29
|
-
bun run service/image.ts fal "a beautiful sunset over mountains" [model] [upload]
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
**parameters:**
|
|
33
|
-
- `prompt` (required): text description of the image
|
|
34
|
-
- `model` (optional): fal model to use (default: flux-pro)
|
|
35
|
-
- `upload` (optional): "true" to upload to s3
|
|
36
|
-
|
|
37
|
-
**example:**
|
|
38
|
-
```bash
|
|
39
|
-
bun run service/image.ts fal "professional headshot, studio lighting" true
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### generate with soul
|
|
43
|
-
```bash
|
|
44
|
-
bun run service/image.ts soul "friendly person smiling" [styleId] [upload]
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
**parameters:**
|
|
48
|
-
- `prompt` (required): character description
|
|
49
|
-
- `styleId` (optional): custom higgsfield style reference
|
|
50
|
-
- `upload` (optional): "true" to upload to s3
|
|
51
|
-
|
|
52
|
-
**example:**
|
|
53
|
-
```bash
|
|
54
|
-
bun run service/image.ts soul "professional business woman" true
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
## as library
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
import { generateWithFal, generateWithSoul } from "./service/image"
|
|
61
|
-
|
|
62
|
-
// fal generation
|
|
63
|
-
const falResult = await generateWithFal("sunset over ocean", {
|
|
64
|
-
model: "fal-ai/flux-pro/v1.1",
|
|
65
|
-
upload: true
|
|
66
|
-
})
|
|
67
|
-
console.log(falResult.imageUrl)
|
|
68
|
-
console.log(falResult.uploaded) // s3 url if upload=true
|
|
69
|
-
|
|
70
|
-
// soul generation
|
|
71
|
-
const soulResult = await generateWithSoul("friendly character", {
|
|
72
|
-
upload: true
|
|
73
|
-
})
|
|
74
|
-
console.log(soulResult.imageUrl)
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## output
|
|
78
|
-
|
|
79
|
-
returns `ImageGenerationResult`:
|
|
80
|
-
```typescript
|
|
81
|
-
{
|
|
82
|
-
imageUrl: string, // direct image url
|
|
83
|
-
uploaded?: string // s3 url if upload requested
|
|
84
|
-
}
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## when to use
|
|
88
|
-
|
|
89
|
-
use this skill when:
|
|
90
|
-
- generating images from text descriptions
|
|
91
|
-
- creating character headshots or portraits
|
|
92
|
-
- need consistent character style (use soul)
|
|
93
|
-
- need high quality photorealistic images (use fal)
|
|
94
|
-
- preparing images for video generation pipeline
|
|
95
|
-
|
|
96
|
-
## nsfw filtering and content moderation
|
|
97
|
-
|
|
98
|
-
fal.ai has content safety filters that may flag images as nsfw:
|
|
99
|
-
|
|
100
|
-
**common triggers:**
|
|
101
|
-
- prompts mentioning "athletic wear", "fitted sportswear", "gym clothes"
|
|
102
|
-
- certain body descriptions even when clothed
|
|
103
|
-
- prompts that could be interpreted as revealing clothing
|
|
104
|
-
|
|
105
|
-
**symptoms:**
|
|
106
|
-
- image generation returns but file is empty (often 7.6KB)
|
|
107
|
-
- no error message, just an unusable file
|
|
108
|
-
- happens inconsistently across similar prompts
|
|
109
|
-
|
|
110
|
-
**solutions:**
|
|
111
|
-
- specify modest, full-coverage clothing explicitly:
|
|
112
|
-
- ✅ "long sleeve athletic top and full length leggings"
|
|
113
|
-
- ✅ "fully covered in modest workout attire"
|
|
114
|
-
- ❌ "athletic wear" (too vague, may trigger filter)
|
|
115
|
-
- ❌ "fitted sportswear" (may trigger filter)
|
|
116
|
-
- add "professional", "modest", "appropriate" to descriptions
|
|
117
|
-
- if multiple images in batch get flagged, adjust prompts to be more explicit about coverage
|
|
118
|
-
- always check output file sizes - empty files (< 10KB) indicate nsfw filtering
|
|
119
|
-
|
|
120
|
-
**example:**
|
|
121
|
-
```bash
|
|
122
|
-
# ❌ may get flagged as nsfw
|
|
123
|
-
bun run service/image.ts fal "woman in athletic wear"
|
|
124
|
-
|
|
125
|
-
# ✅ less likely to trigger filter
|
|
126
|
-
bun run service/image.ts fal "woman wearing long sleeve athletic top and full length leggings"
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## environment variables
|
|
130
|
-
|
|
131
|
-
required:
|
|
132
|
-
- `FAL_API_KEY` - for fal image generation
|
|
133
|
-
- `HIGGSFIELD_API_KEY` - for soul character generation
|
|
134
|
-
- `HIGGSFIELD_SECRET` - for higgsfield authentication
|
|
135
|
-
|
|
136
|
-
optional (for s3 upload):
|
|
137
|
-
- `CLOUDFLARE_R2_API_URL`
|
|
138
|
-
- `CLOUDFLARE_ACCESS_KEY_ID`
|
|
139
|
-
- `CLOUDFLARE_ACCESS_SECRET`
|
|
140
|
-
- `CLOUDFLARE_R2_BUCKET`
|