varg.ai-sdk 0.1.1 → 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 +43 -10
- 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} +58 -68
- 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 -169
- package/action/edit/SKILL.md +0 -235
- package/action/edit/index.ts +0 -437
- package/action/image/SKILL.md +0 -140
- package/action/image/index.ts +0 -105
- package/action/sync/SKILL.md +0 -136
- package/action/sync/index.ts +0 -145
- package/action/transcribe/SKILL.md +0 -179
- package/action/video/SKILL.md +0 -116
- package/action/video/index.ts +0 -125
- package/action/voice/SKILL.md +0 -125
- package/action/voice/index.ts +0 -136
- package/cli/commands/find.ts +0 -58
- package/cli/commands/help.ts +0 -70
- package/cli/commands/list.ts +0 -49
- package/cli/commands/run.ts +0 -237
- package/cli/commands/which.ts +0 -66
- package/cli/discover.ts +0 -66
- package/cli/index.ts +0 -33
- package/cli/runner.ts +0 -65
- package/cli/types.ts +0 -49
- package/cli/ui.ts +0 -185
- package/index.ts +0 -75
- 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 -467
- 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,437 +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 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
|
-
}
|
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`
|
package/action/image/index.ts
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* image generation service combining fal and higgsfield
|
|
4
|
-
* usage: bun run service/image.ts <command> <args>
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ActionMeta } from "../../cli/types";
|
|
8
|
-
import { generateImage } from "../../lib/fal";
|
|
9
|
-
import { generateSoul } from "../../lib/higgsfield";
|
|
10
|
-
import { uploadFromUrl } from "../../utilities/s3";
|
|
11
|
-
|
|
12
|
-
export const meta: ActionMeta = {
|
|
13
|
-
name: "image",
|
|
14
|
-
type: "action",
|
|
15
|
-
description: "generate image from text",
|
|
16
|
-
inputType: "text",
|
|
17
|
-
outputType: "image",
|
|
18
|
-
schema: {
|
|
19
|
-
input: {
|
|
20
|
-
type: "object",
|
|
21
|
-
required: ["prompt"],
|
|
22
|
-
properties: {
|
|
23
|
-
prompt: { type: "string", description: "what to generate" },
|
|
24
|
-
size: {
|
|
25
|
-
type: "string",
|
|
26
|
-
enum: [
|
|
27
|
-
"square_hd",
|
|
28
|
-
"landscape_4_3",
|
|
29
|
-
"portrait_4_3",
|
|
30
|
-
"landscape_16_9",
|
|
31
|
-
],
|
|
32
|
-
default: "landscape_4_3",
|
|
33
|
-
description: "image size/aspect",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
output: { type: "string", format: "file-path", description: "image path" },
|
|
38
|
-
},
|
|
39
|
-
async run(options) {
|
|
40
|
-
const { prompt, size } = options as { prompt: string; size?: string };
|
|
41
|
-
return generateWithFal(prompt, { model: size });
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
export interface ImageGenerationResult {
|
|
46
|
-
imageUrl: string;
|
|
47
|
-
uploaded?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function generateWithFal(
|
|
51
|
-
prompt: string,
|
|
52
|
-
options: { model?: string; upload?: boolean } = {},
|
|
53
|
-
): Promise<ImageGenerationResult> {
|
|
54
|
-
console.log("[service/image] generating with fal");
|
|
55
|
-
|
|
56
|
-
const result = await generateImage({ prompt, model: options.model });
|
|
57
|
-
|
|
58
|
-
const imageUrl = result.data?.images?.[0]?.url;
|
|
59
|
-
if (!imageUrl) {
|
|
60
|
-
throw new Error("no image url in result");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let uploaded: string | undefined;
|
|
64
|
-
if (options.upload) {
|
|
65
|
-
const timestamp = Date.now();
|
|
66
|
-
const objectKey = `images/fal/${timestamp}.png`;
|
|
67
|
-
uploaded = await uploadFromUrl(imageUrl, objectKey);
|
|
68
|
-
console.log(`[service/image] uploaded to ${uploaded}`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return { imageUrl, uploaded };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export async function generateWithSoul(
|
|
75
|
-
prompt: string,
|
|
76
|
-
options: { styleId?: string; upload?: boolean } = {},
|
|
77
|
-
): Promise<ImageGenerationResult> {
|
|
78
|
-
console.log("[service/image] generating with higgsfield soul");
|
|
79
|
-
|
|
80
|
-
const result = await generateSoul({
|
|
81
|
-
prompt,
|
|
82
|
-
styleId: options.styleId,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const imageUrl = result.jobs?.[0]?.results?.raw?.url;
|
|
86
|
-
if (!imageUrl) {
|
|
87
|
-
throw new Error("no image url in result");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let uploaded: string | undefined;
|
|
91
|
-
if (options.upload) {
|
|
92
|
-
const timestamp = Date.now();
|
|
93
|
-
const objectKey = `images/soul/${timestamp}.png`;
|
|
94
|
-
uploaded = await uploadFromUrl(imageUrl, objectKey);
|
|
95
|
-
console.log(`[service/image] uploaded to ${uploaded}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { imageUrl, uploaded };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// cli
|
|
102
|
-
if (import.meta.main) {
|
|
103
|
-
const { runCli } = await import("../../cli/runner");
|
|
104
|
-
runCli(meta);
|
|
105
|
-
}
|