diff --git a/src/core/schema/shared.ts b/src/core/schema/shared.ts index 916510a0..fea0924b 100644 --- a/src/core/schema/shared.ts +++ b/src/core/schema/shared.ts @@ -75,7 +75,6 @@ export type TranscriptionProvider = z.infer; // Provider name choices export const providerNameSchema = z.enum([ "fal", - "replicate", "elevenlabs", "higgsfield", "groq", diff --git a/src/definitions/actions/captions.ts b/src/definitions/actions/captions.ts index 37334802..d5139681 100644 --- a/src/definitions/actions/captions.ts +++ b/src/definitions/actions/captions.ts @@ -7,7 +7,6 @@ import { writeFileSync } from "node:fs"; import { z } from "zod"; import { captionStyleSchema, filePathSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { ffmpegProvider } from "../../providers/ffmpeg"; import { transcribe } from "./transcribe"; // Input schema with Zod @@ -117,7 +116,7 @@ export async function addCaptions( // Extract audio first const audioPath = video.replace(/\.[^.]+$/, "_audio.mp3"); - await ffmpegProvider.extractAudio(video, audioPath); + await Bun.$`ffmpeg -y -i ${video} -vn -acodec libmp3lame ${audioPath}`.quiet(); // Transcribe const result = await transcribe({ @@ -158,7 +157,7 @@ export async function addCaptions( console.log(`[captions] burning subtitles...`); // For now, just copy the video (proper implementation would use subtitles filter) - await ffmpegProvider.convertFormat({ input: video, output }); + await Bun.$`ffmpeg -y -i ${video} -c copy ${output}`.quiet(); console.log(`[captions] saved to ${output}`); return output; diff --git a/src/definitions/actions/grok-edit.ts b/src/definitions/actions/grok-edit.ts index 94307b2b..6b949a6a 100644 --- a/src/definitions/actions/grok-edit.ts +++ b/src/definitions/actions/grok-edit.ts @@ -3,10 +3,11 @@ * Edit videos using xAI's Grok Imagine Video model */ +import { fal } from "@fal-ai/client"; import { z } from "zod"; import { filePathSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; +import { ensureUrl, logQueueUpdate } from "./utils"; // Resolution enum matching the API spec const grokEditResolutionSchema = z @@ -57,10 +58,15 @@ export const definition: ActionDefinition = { console.log("[action/grok-edit] editing video with Grok Imagine"); - const result = await falProvider.grokEditVideo({ - prompt, - videoUrl: video, - resolution, + const inputUrl = await ensureUrl(video); + const result = await fal.subscribe("xai/grok-imagine-video/edit-video", { + input: { + prompt, + video_url: inputUrl, + resolution: resolution ?? "auto", + }, + logs: true, + onQueueUpdate: logQueueUpdate("grok-edit"), }); const data = result.data as { @@ -100,10 +106,15 @@ export async function grokEditVideo( ): Promise { console.log("[grok-edit] editing video"); - const result = await falProvider.grokEditVideo({ - prompt, - videoUrl, - resolution: options.resolution, + const inputUrl = await ensureUrl(videoUrl); + const result = await fal.subscribe("xai/grok-imagine-video/edit-video", { + input: { + prompt, + video_url: inputUrl, + resolution: options.resolution ?? "auto", + }, + logs: true, + onQueueUpdate: logQueueUpdate("grok-edit"), }); const data = result.data as { @@ -116,13 +127,13 @@ export async function grokEditVideo( }; }; - const url = data?.video?.url; - if (!url) { + const resultUrl = data?.video?.url; + if (!resultUrl) { throw new Error("No video URL in result"); } return { - videoUrl: url, + videoUrl: resultUrl, width: data.video?.width, height: data.video?.height, duration: data.video?.duration, diff --git a/src/definitions/actions/image.ts b/src/definitions/actions/image.ts index dbd4ec57..5afc62a9 100644 --- a/src/definitions/actions/image.ts +++ b/src/definitions/actions/image.ts @@ -3,12 +3,12 @@ * Routes to Fal or Higgsfield based on options */ +import { fal } from "@fal-ai/client"; +import { HiggsfieldClient } from "@higgsfield/client"; import { z } from "zod"; import { imageSizeSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; -import { higgsfieldProvider } from "../../providers/higgsfield"; -import { storageProvider } from "../../providers/storage"; +import { logQueueUpdate } from "./utils"; // Input schema with Zod const imageInputSchema = z.object({ @@ -69,57 +69,50 @@ export interface ImageGenerationResult { export async function generateWithFal( prompt: string, - options: { imageSize?: string; upload?: boolean } = {}, + options: { imageSize?: string } = {}, ): Promise { console.log("[image] generating with fal"); - const result = await falProvider.generateImage({ - prompt, - imageSize: options.imageSize, - }); + type FalResult = { data: { images?: Array<{ url?: string }> } }; + const result = (await fal.subscribe("fal-ai/flux-pro/v1.1" as string, { + input: { + prompt, + image_size: options.imageSize || "landscape_4_3", + }, + logs: true, + onQueueUpdate: logQueueUpdate("image"), + })) as FalResult; - const imageUrl = (result.data as { images?: Array<{ url?: string }> }) - ?.images?.[0]?.url; + const imageUrl = result.data?.images?.[0]?.url; if (!imageUrl) { throw new Error("No image URL in result"); } - let uploaded: string | undefined; - if (options.upload) { - const timestamp = Date.now(); - const objectKey = `images/fal/${timestamp}.png`; - uploaded = await storageProvider.uploadFromUrl(imageUrl, objectKey); - console.log(`[image] uploaded to ${uploaded}`); - } - - return { imageUrl, uploaded }; + return { imageUrl }; } export async function generateWithSoul( prompt: string, - options: { styleId?: string; upload?: boolean } = {}, + options: { styleId?: string } = {}, ): Promise { console.log("[image] generating with higgsfield soul"); - const result = await higgsfieldProvider.generateSoul({ + const client = new HiggsfieldClient({ + apiKey: process.env.HIGGSFIELD_API_KEY || process.env.HF_API_KEY, + apiSecret: process.env.HIGGSFIELD_SECRET || process.env.HF_API_SECRET, + }); + + const jobSet = await client.generate("/v1/text2image/soul", { prompt, - styleId: options.styleId, + ...(options.styleId && { style_id: options.styleId }), }); - const imageUrl = result.jobs?.[0]?.results?.raw?.url; + const imageUrl = jobSet?.jobs?.[0]?.results?.raw?.url; if (!imageUrl) { throw new Error("No image URL in result"); } - let uploaded: string | undefined; - if (options.upload) { - const timestamp = Date.now(); - const objectKey = `images/soul/${timestamp}.png`; - uploaded = await storageProvider.uploadFromUrl(imageUrl, objectKey); - console.log(`[image] uploaded to ${uploaded}`); - } - - return { imageUrl, uploaded }; + return { imageUrl }; } export default definition; diff --git a/src/definitions/actions/music.ts b/src/definitions/actions/music.ts index aa72717b..41c86ebb 100644 --- a/src/definitions/actions/music.ts +++ b/src/definitions/actions/music.ts @@ -4,11 +4,11 @@ */ import { writeFile } from "node:fs/promises"; +import { fal } from "@fal-ai/client"; import { z } from "zod"; import { audioFormatSchema, filePathSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; -import { storageProvider } from "../../providers/storage"; +import { logQueueUpdate } from "./utils"; // Input schema with Zod const musicInputSchema = z.object({ @@ -121,17 +121,21 @@ export async function generateMusic( if (prompt) console.log(`[music] prompt: ${prompt}`); if (tags) console.log(`[music] tags: ${tags.join(", ")}`); - const result = await falProvider.textToMusic({ - prompt, - tags, - lyricsPrompt: lyrics, - seed, - promptStrength, - balanceStrength, - numSongs, - outputFormat: format, - outputBitRate: bitRate, - bpm, + const result = await fal.subscribe("fal-ai/sonauto/bark", { + input: { + prompt, + tags, + lyrics_prompt: lyrics, + seed, + prompt_strength: promptStrength, + balance_strength: balanceStrength, + num_songs: numSongs, + output_format: format, + output_bit_rate: bitRate, + bpm, + }, + logs: true, + onQueueUpdate: logQueueUpdate("music"), }); const musicResult: MusicResult = { @@ -181,24 +185,6 @@ export async function generateMusic( } } - // Upload to storage if requested - if (upload) { - const uploadUrls: string[] = []; - for (let i = 0; i < musicResult.audio.length; i++) { - const audio = musicResult.audio[i]; - if (!audio) continue; - - const objectKey = `music/${Date.now()}-${i + 1}.${format || "wav"}`; - const uploadUrl = await storageProvider.uploadFromUrl( - audio.url, - objectKey, - ); - uploadUrls.push(uploadUrl); - console.log(`[music] uploaded to ${uploadUrl}`); - } - musicResult.uploadUrls = uploadUrls; - } - return musicResult; } diff --git a/src/definitions/actions/qwen-angles.ts b/src/definitions/actions/qwen-angles.ts index 7760ad80..7d33da12 100644 --- a/src/definitions/actions/qwen-angles.ts +++ b/src/definitions/actions/qwen-angles.ts @@ -3,10 +3,11 @@ * Generates same scene from different camera angles (azimuth/elevation) */ +import { fal } from "@fal-ai/client"; import { z } from "zod"; import { filePathSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; +import { ensureUrl, logQueueUpdate } from "./utils"; // Input schema with Zod const qwenAnglesInputSchema = z.object({ @@ -123,20 +124,29 @@ export const definition: ActionDefinition = { console.log("[action/qwen-angles] adjusting camera angle"); - const result = await falProvider.qwenMultipleAngles({ - imageUrl: image, - horizontalAngle, - verticalAngle, - zoom, - additionalPrompt: prompt, - loraScale, - guidanceScale, - numInferenceSteps, - negativePrompt, - seed, - outputFormat, - numImages, - }); + const imageUrl = await ensureUrl(image); + const result = await fal.subscribe( + "fal-ai/qwen-image-edit-2511-multiple-angles", + { + input: { + image_urls: [imageUrl], + horizontal_angle: horizontalAngle ?? 0, + vertical_angle: verticalAngle ?? 0, + zoom: zoom ?? 5, + additional_prompt: prompt, + lora_scale: loraScale ?? 1, + guidance_scale: guidanceScale ?? 4.5, + num_inference_steps: numInferenceSteps ?? 28, + acceleration: "regular", + negative_prompt: negativePrompt ?? "", + seed, + output_format: outputFormat ?? "png", + num_images: numImages ?? 1, + }, + logs: true, + onQueueUpdate: logQueueUpdate("qwen-angles"), + }, + ); const data = result.data as { images?: Array<{ url: string }>; @@ -182,20 +192,29 @@ export async function qwenAngles( ): Promise { console.log("[qwen-angles] adjusting camera angle"); - const result = await falProvider.qwenMultipleAngles({ - imageUrl, - horizontalAngle: options.horizontalAngle, - verticalAngle: options.verticalAngle, - zoom: options.zoom, - additionalPrompt: options.prompt, - loraScale: options.loraScale, - guidanceScale: options.guidanceScale, - numInferenceSteps: options.numInferenceSteps, - negativePrompt: options.negativePrompt, - seed: options.seed, - outputFormat: options.outputFormat, - numImages: options.numImages, - }); + const url = await ensureUrl(imageUrl); + const result = await fal.subscribe( + "fal-ai/qwen-image-edit-2511-multiple-angles", + { + input: { + image_urls: [url], + horizontal_angle: options.horizontalAngle ?? 0, + vertical_angle: options.verticalAngle ?? 0, + zoom: options.zoom ?? 5, + additional_prompt: options.prompt, + lora_scale: options.loraScale ?? 1, + guidance_scale: options.guidanceScale ?? 4.5, + num_inference_steps: options.numInferenceSteps ?? 28, + acceleration: "regular", + negative_prompt: options.negativePrompt ?? "", + seed: options.seed, + output_format: options.outputFormat ?? "png", + num_images: options.numImages ?? 1, + }, + logs: true, + onQueueUpdate: logQueueUpdate("qwen-angles"), + }, + ); const data = result.data as { images?: Array<{ url: string }>; diff --git a/src/definitions/actions/sync.ts b/src/definitions/actions/sync.ts index ab6e7031..3b3a39b9 100644 --- a/src/definitions/actions/sync.ts +++ b/src/definitions/actions/sync.ts @@ -3,6 +3,7 @@ * Audio-to-video synchronization */ +import { fal } from "@fal-ai/client"; import { z } from "zod"; import { filePathSchema, @@ -10,8 +11,7 @@ import { videoDurationStringSchema, } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; -import { ffmpegProvider } from "../../providers/ffmpeg"; +import { ensureUrl, logQueueUpdate } from "./utils"; // Input schema with Zod const syncInputSchema = z.object({ @@ -72,12 +72,22 @@ export async function lipsync(options: LipsyncOptions): Promise { console.log("[sync] generating lip-synced video with wan-25..."); - const result = await falProvider.wan25({ - imageUrl: image, - audioUrl: audio, - prompt, - duration, - resolution, + const imageUrl = await ensureUrl(image); + const audioUrl = await ensureUrl(audio); + + const result = await fal.subscribe("fal-ai/wan-25-preview/image-to-video", { + input: { + prompt, + image_url: imageUrl, + audio_url: audioUrl, + resolution: resolution || "480p", + duration: duration || "5", + negative_prompt: + "low resolution, error, worst quality, low quality, defects", + enable_prompt_expansion: true, + }, + logs: true, + onQueueUpdate: logQueueUpdate("sync"), }); const videoUrl = result.data?.video?.url; @@ -100,12 +110,7 @@ export async function lipsyncOverlay(options: { console.log("[sync] overlaying lip-synced video..."); - // This would require more complex ffmpeg operations - // For now, just return the lip-synced video as-is - await ffmpegProvider.convertFormat({ - input: lipsyncedVideo, - output: outputPath, - }); + await Bun.$`ffmpeg -y -i ${lipsyncedVideo} -c copy ${outputPath}`.quiet(); return outputPath; } @@ -116,11 +121,7 @@ export async function lipsyncOverlay(options: { export async function lipsyncWav2Lip(options: Wav2LipOptions): Promise { console.warn("[sync] wav2lip not yet implemented, using wan-25 fallback"); - // For now, just copy the video - await ffmpegProvider.convertFormat({ - input: options.videoPath, - output: options.outputPath, - }); + await Bun.$`ffmpeg -y -i ${options.videoPath} -c copy ${options.outputPath}`.quiet(); return options.outputPath; } diff --git a/src/definitions/actions/utils.ts b/src/definitions/actions/utils.ts new file mode 100644 index 00000000..90c3e413 --- /dev/null +++ b/src/definitions/actions/utils.ts @@ -0,0 +1,42 @@ +/** + * Shared utilities for action definitions + * Replaces old provider helpers with direct client calls + */ + +import { fal } from "@fal-ai/client"; + +const falApiKey = process.env.FAL_API_KEY ?? process.env.FAL_KEY; +if (falApiKey) { + fal.config({ credentials: falApiKey }); +} + +/** + * Ensure a path or URL is a remote URL. + * If it's a local file path, uploads to fal storage first. + */ +export async function ensureUrl(pathOrUrl: string): Promise { + if (pathOrUrl.startsWith("http://") || pathOrUrl.startsWith("https://")) { + return pathOrUrl; + } + + const file = Bun.file(pathOrUrl); + if (!(await file.exists())) { + throw new Error(`Local file not found: ${pathOrUrl}`); + } + + const buffer = await file.arrayBuffer(); + return fal.storage.upload(new Blob([buffer])); +} + +/** + * Standard fal queue update logger + */ +export function logQueueUpdate(prefix: string) { + return (update: { status: string; logs?: Array<{ message: string }> }) => { + if (update.status === "IN_PROGRESS") { + console.log( + `[${prefix}] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, + ); + } + }; +} diff --git a/src/definitions/actions/video.ts b/src/definitions/actions/video.ts index d34e0db0..3be7fecb 100644 --- a/src/definitions/actions/video.ts +++ b/src/definitions/actions/video.ts @@ -3,6 +3,7 @@ * Routes to appropriate video generation models based on input */ +import { fal } from "@fal-ai/client"; import { z } from "zod"; import { aspectRatioSchema, @@ -10,8 +11,7 @@ import { videoDurationSchema, } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { falProvider } from "../../providers/fal"; -import { storageProvider } from "../../providers/storage"; +import { ensureUrl, logQueueUpdate } from "./utils"; // Input schema with Zod const videoInputSchema = z.object({ @@ -51,25 +51,40 @@ export const definition: ActionDefinition = { }, ], execute: async (inputs) => { - // inputs is now fully typed as VideoInput - no more `as` cast! const { prompt, image, duration, aspectRatio } = inputs; - let result: { data?: { video?: { url?: string }; duration?: number } }; + type FalResult = { data: { video?: { url?: string } } }; + let result: FalResult; if (image) { console.log("[action/video] generating video from image"); - result = await falProvider.imageToVideo({ - prompt, - imageUrl: image, - duration, - }); + const imageUrl = await ensureUrl(image); + result = (await fal.subscribe( + "fal-ai/kling-video/v2.5-turbo/pro/image-to-video" as string, + { + input: { + prompt, + image_url: imageUrl, + duration: String(duration || 5), + }, + logs: true, + onQueueUpdate: logQueueUpdate("video"), + }, + )) as FalResult; } else { console.log("[action/video] generating video from text"); - result = await falProvider.textToVideo({ - prompt, - duration, - aspectRatio, - }); + result = (await fal.subscribe( + "fal-ai/kling-video/v2.5-turbo/pro/text-to-video" as string, + { + input: { + prompt, + duration: String(duration || 5), + aspect_ratio: aspectRatio || "16:9", + }, + logs: true, + onQueueUpdate: logQueueUpdate("video"), + }, + )) as FalResult; } const videoUrl = result.data?.video?.url; @@ -77,10 +92,7 @@ export const definition: ActionDefinition = { throw new Error("No video URL in result"); } - return { - videoUrl, - duration: result.data?.duration, - }; + return { videoUrl }; }, }; @@ -94,70 +106,62 @@ export interface VideoGenerationResult { export async function generateVideoFromImage( prompt: string, imageUrl: string, - options: { duration?: 5 | 10; upload?: boolean } = {}, + options: { duration?: 5 | 10 } = {}, ): Promise { console.log("[video] generating video from image"); - const result = await falProvider.imageToVideo({ - prompt, - imageUrl, - duration: options.duration, - }); + type FalResult = { data: { video?: { url?: string } } }; + const url = await ensureUrl(imageUrl); + const result = (await fal.subscribe( + "fal-ai/kling-video/v2.5-turbo/pro/image-to-video" as string, + { + input: { + prompt, + image_url: url, + duration: String(options.duration || 5), + }, + logs: true, + onQueueUpdate: logQueueUpdate("video"), + }, + )) as FalResult; const videoUrl = result.data?.video?.url; if (!videoUrl) { throw new Error("No video URL in result"); } - let uploaded: string | undefined; - if (options.upload) { - const timestamp = Date.now(); - const objectKey = `videos/generated/${timestamp}.mp4`; - uploaded = await storageProvider.uploadFromUrl(videoUrl, objectKey); - console.log(`[video] uploaded to ${uploaded}`); - } - - return { - videoUrl, - duration: result.data?.duration, - uploaded, - }; + return { videoUrl }; } export async function generateVideoFromText( prompt: string, options: { duration?: 5 | 10; - upload?: boolean; aspectRatio?: "16:9" | "9:16" | "1:1"; } = {}, ): Promise { console.log("[video] generating video from text"); - const result = await falProvider.textToVideo({ - prompt, - duration: options.duration, - aspectRatio: options.aspectRatio, - }); + type FalResult = { data: { video?: { url?: string } } }; + const result = (await fal.subscribe( + "fal-ai/kling-video/v2.5-turbo/pro/text-to-video" as string, + { + input: { + prompt, + duration: String(options.duration || 5), + aspect_ratio: options.aspectRatio || "16:9", + }, + logs: true, + onQueueUpdate: logQueueUpdate("video"), + }, + )) as FalResult; const videoUrl = result.data?.video?.url; if (!videoUrl) { throw new Error("No video URL in result"); } - let uploaded: string | undefined; - if (options.upload) { - const timestamp = Date.now(); - const objectKey = `videos/generated/${timestamp}.mp4`; - uploaded = await storageProvider.uploadFromUrl(videoUrl, objectKey); - console.log(`[video] uploaded to ${uploaded}`); - } - - return { - videoUrl, - duration: result.data?.duration, - uploaded, - }; + return { videoUrl }; } export default definition; diff --git a/src/definitions/actions/voice.ts b/src/definitions/actions/voice.ts index ccbcd2dc..e7af3cea 100644 --- a/src/definitions/actions/voice.ts +++ b/src/definitions/actions/voice.ts @@ -3,11 +3,10 @@ * Text-to-speech via ElevenLabs */ +import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; import { z } from "zod"; import { filePathSchema, voiceNameSchema } from "../../core/schema/shared"; import type { ActionDefinition, ZodSchema } from "../../core/schema/types"; -import { elevenlabsProvider, VOICES } from "../../providers/elevenlabs"; -import { storageProvider } from "../../providers/storage"; // Input schema with Zod const voiceInputSchema = z.object({ @@ -58,17 +57,16 @@ export interface VoiceResult { uploadUrl?: string; } -// Voice name to ID mapping const VOICE_MAP: Record = { - rachel: VOICES.RACHEL, - domi: VOICES.DOMI, - bella: VOICES.BELLA, - antoni: VOICES.ANTONI, - elli: VOICES.ELLI, - josh: VOICES.JOSH, - arnold: VOICES.ARNOLD, - adam: VOICES.ADAM, - sam: VOICES.SAM, + rachel: "21m00Tcm4TlvDq8ikWAM", + domi: "AZnzlk1XvdvUeBnXmlld", + bella: "EXAVITQu4vr4xnSDxMaL", + antoni: "ErXwobaYiN019PkySvjV", + elli: "MF3mGyEYCl7XYWbV9V6O", + josh: "TxGEqnHWrfWFTfGW9XjX", + arnold: "VR6AewLTigWG4xSOukaG", + adam: "pNInz6obpgDQGcFmaJgB", + sam: "yoZ06aMxZJJ28mfd3POQ", }; export async function generateVoice( @@ -90,30 +88,31 @@ export async function generateVoice( const voiceId = VOICE_MAP[voice.toLowerCase()] || voice; - const audio = await elevenlabsProvider.textToSpeech({ + const client = new ElevenLabsClient({ + apiKey: process.env.ELEVENLABS_API_KEY, + }); + + const audioStream = await client.textToSpeech.convert(voiceId, { text, - voiceId, - outputPath, + modelId: "eleven_multilingual_v2", }); - const result: VoiceResult = { + const chunks: Buffer[] = []; + for await (const chunk of audioStream) { + chunks.push(Buffer.from(chunk)); + } + const audio = Buffer.concat(chunks); + + if (outputPath) { + await Bun.write(outputPath, audio); + console.log(`[voice] saved to ${outputPath}`); + } + + return { audio, provider, voiceId, }; - - // Upload to storage if requested - if (upload && outputPath) { - const objectKey = `voice/${Date.now()}-${voice}.mp3`; - const uploadUrl = await storageProvider.uploadLocalFile( - outputPath, - objectKey, - ); - result.uploadUrl = uploadUrl; - console.log(`[voice] uploaded to ${uploadUrl}`); - } - - return result; } export default definition; diff --git a/src/definitions/models/flux.ts b/src/definitions/models/flux.ts index 9bc38528..cbed9511 100644 --- a/src/definitions/models/flux.ts +++ b/src/definitions/models/flux.ts @@ -44,11 +44,10 @@ export const definition: ModelDefinition = { name: "flux", description: "Flux Pro image generation model for high-quality images from text", - providers: ["fal", "replicate"], + providers: ["fal"], defaultProvider: "fal", providerModels: { fal: "fal-ai/flux-pro/v1.1", - replicate: "black-forest-labs/flux-1.1-pro", }, schema, }; diff --git a/src/definitions/models/kling.ts b/src/definitions/models/kling.ts index 7e4426e9..abd1c95b 100644 --- a/src/definitions/models/kling.ts +++ b/src/definitions/models/kling.ts @@ -44,11 +44,10 @@ export const definition: ModelDefinition = { name: "kling", description: "Kling video generation model for high-quality video from text or image", - providers: ["fal", "replicate"], + providers: ["fal"], defaultProvider: "fal", providerModels: { fal: "fal-ai/kling-video/v2.5-turbo/pro", - replicate: "fofr/kling-v1.5", }, schema, }; diff --git a/src/definitions/models/nano-banana-pro.ts b/src/definitions/models/nano-banana-pro.ts index 1a933cdf..91f7c564 100644 --- a/src/definitions/models/nano-banana-pro.ts +++ b/src/definitions/models/nano-banana-pro.ts @@ -90,11 +90,10 @@ export const definition: ModelDefinition = { name: "nano-banana-pro", description: "Google Nano Banana Pro (Gemini 3 Pro Image) for text-to-image generation and image editing. Provide image_urls for editing, omit for generation.", - providers: ["fal", "replicate"], + providers: ["fal"], defaultProvider: "fal", providerModels: { fal: "fal-ai/nano-banana-pro", - replicate: "google/nano-banana-pro", }, schema, }; diff --git a/src/definitions/models/wan.ts b/src/definitions/models/wan.ts index d1e0464d..4c20af45 100644 --- a/src/definitions/models/wan.ts +++ b/src/definitions/models/wan.ts @@ -42,11 +42,10 @@ export const definition: ModelDefinition = { type: "model", name: "wan", description: "Wan-25 model for audio-driven video generation with lip sync", - providers: ["fal", "replicate"], + providers: ["fal"], defaultProvider: "fal", providerModels: { fal: "fal-ai/wan-25-preview/image-to-video", - replicate: "wan-video/wan-2.5-i2v", }, schema, }; diff --git a/src/index.ts b/src/index.ts index 30eceb38..078731f5 100755 --- a/src/index.ts +++ b/src/index.ts @@ -44,76 +44,41 @@ export { addAudio, // Base BaseProvider, - BatchSize, chatCompletion, concatVideos, convertFireworksToSRT, convertFormat, - createSoulId, downloadToFile, - // ElevenLabs - ElevenLabsProvider, - elevenlabsProvider, ensureUrl, extractAudio, - // Fal - FalProvider, // FFmpeg FFmpegProvider, // Fireworks FireworksProvider, fadeVideo, - falProvider, ffmpegProvider, fireworksProvider, GROQ_MODELS, // Groq GroqProvider, - generateImage, - generateMusicElevenlabs, generatePresignedUrl, - generateSoul, - generateSoundEffect, getExtension, getPublicUrl, getVideoDuration, - getVoice, groqProvider, - // Higgsfield - HiggsfieldProvider, - higgsfieldProvider, - imageToImage, - imageToVideo, listModels, - listSoulIds, - listSoulStyles, - listVoices, - MODELS, ProviderRegistry, probe, providers, - // Replicate - ReplicateProvider, - replicateProvider, resizeVideo, - runImage, - runModel, - runVideo, - SoulQuality, - SoulSize, // Storage StorageProvider, splitAtTimestamps, storageProvider, - textToMusic, - textToSpeech, - textToVideo, transcribeWithFireworks, trimVideo, uploadBuffer, uploadFile, uploadFromUrl, - VOICES, - wan25, xfadeVideos, } from "./providers"; diff --git a/src/providers/apify.ts b/src/providers/apify.ts deleted file mode 100644 index 7910fe24..00000000 --- a/src/providers/apify.ts +++ /dev/null @@ -1,269 +0,0 @@ -/** - * Apify provider for running actors and web scraping - * Supports TikTok scraping, web scraping, and other Apify actors - */ - -import { ApifyClient } from "apify-client"; -import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types"; -import { BaseProvider } from "./base"; - -export interface ApifyProviderConfig extends ProviderConfig { - /** Apify API token (defaults to APIFY_TOKEN env var) */ - token?: string; -} - -export interface RunActorOptions { - /** Actor ID in format "username/actor-name" */ - actorId: string; - /** Input parameters for the actor */ - input?: Record; - /** Whether to wait for the actor to finish (default: true) */ - waitForFinish?: boolean; -} - -export interface ApifyRunResult { - /** Run ID */ - runId: string; - /** Dataset ID containing results */ - datasetId: string; - /** Run status */ - status: string; - /** Result items (if waitForFinish was true) */ - items?: unknown[]; -} - -export class ApifyProvider extends BaseProvider { - readonly name = "apify"; - private client: ApifyClient; - - constructor(config?: ApifyProviderConfig) { - super(config); - this.client = new ApifyClient({ - token: config?.token || process.env.APIFY_TOKEN, - }); - } - - /** - * Submit an actor run job - * @param model - Actor ID (e.g., "clockworks/tiktok-scraper") - * @param inputs - Actor input parameters - */ - async submit( - model: string, - inputs: Record, - _config?: ProviderConfig, - ): Promise { - console.log(`[apify] submitting actor: ${model}`); - - const actor = this.client.actor(model); - const run = await actor.start(inputs); - - console.log(`[apify] run started: ${run.id}`); - return run.id; - } - - /** - * Get the status of an actor run - */ - async getStatus(jobId: string): Promise { - const run = await this.client.run(jobId).get(); - - if (!run) { - return { status: "failed", error: "Run not found" }; - } - - const statusMap: Record = { - READY: "queued", - RUNNING: "processing", - SUCCEEDED: "completed", - FAILED: "failed", - ABORTED: "cancelled", - ABORTING: "cancelled", - TIMED_OUT: "failed", - }; - - return { - status: statusMap[run.status] ?? "processing", - output: run.defaultDatasetId, - error: run.status === "FAILED" ? "Actor run failed" : undefined, - }; - } - - /** - * Get the results of a completed actor run - */ - async getResult(jobId: string): Promise { - const run = await this.client.run(jobId).get(); - - if (!run?.defaultDatasetId) { - throw new Error("Run not found or has no dataset"); - } - - const { items } = await this.client - .dataset(run.defaultDatasetId) - .listItems(); - return items; - } - - /** - * Cancel a running actor - */ - override async cancel(jobId: string): Promise { - await this.client.run(jobId).abort(); - console.log(`[apify] run aborted: ${jobId}`); - } - - // ============================================================================ - // High-level convenience methods - // ============================================================================ - - /** - * Run an actor and optionally wait for results - */ - async runActor(options: RunActorOptions): Promise { - console.log(`[apify] running actor: ${options.actorId}`); - - const actor = this.client.actor(options.actorId); - - if (options.waitForFinish !== false) { - // call() waits for the run to finish - const run = await actor.call(options.input); - console.log(`[apify] run completed: ${run.id}`); - - // Get results from dataset - const { items } = await this.client - .dataset(run.defaultDatasetId) - .listItems(); - console.log(`[apify] retrieved ${items.length} items`); - - return { - runId: run.id, - datasetId: run.defaultDatasetId, - status: run.status, - items, - }; - } - - // start() returns immediately - const run = await actor.start(options.input); - console.log(`[apify] run started: ${run.id}`); - - return { - runId: run.id, - datasetId: run.defaultDatasetId, - status: run.status, - }; - } - - /** - * Get items from a dataset - */ - async getDataset(datasetId: string): Promise { - console.log(`[apify] fetching dataset: ${datasetId}`); - const { items } = await this.client.dataset(datasetId).listItems(); - console.log(`[apify] retrieved ${items.length} items`); - return items; - } - - /** - * Get run info - */ - async getRunInfo(runId: string) { - console.log(`[apify] fetching run info: ${runId}`); - const run = await this.client.run(runId).get(); - return run; - } - - /** - * Wait for a run to finish - */ - async waitForRun(runId: string) { - console.log(`[apify] waiting for run: ${runId}`); - const run = await this.client.run(runId).waitForFinish(); - console.log(`[apify] run finished with status: ${run?.status}`); - return run; - } - - /** - * Get value from key-value store - */ - async getKeyValueStoreValue(storeId: string, key: string) { - console.log(`[apify] fetching key "${key}" from store: ${storeId}`); - const value = await this.client.keyValueStore(storeId).getRecord(key); - return value; - } - - /** - * Download videos from scraped results using yt-dlp - */ - async downloadVideos( - items: Array<{ webVideoUrl?: string }>, - outputDir = "output/videos", - ): Promise { - const urls = items - .map((item) => item.webVideoUrl) - .filter((url): url is string => !!url); - - console.log(`[apify] downloading ${urls.length} videos`); - - // Create download dir if needed - await Bun.$`mkdir -p ${outputDir}`; - - const downloaded: string[] = []; - - for (const url of urls) { - console.log(`[apify] downloading: ${url}`); - try { - await Bun.$`yt-dlp -o "${outputDir}/%(id)s.%(ext)s" ${url}`; - downloaded.push(url); - console.log(`[apify] downloaded successfully`); - } catch (err) { - console.error(`[apify] failed to download ${url}:`, err); - } - } - - console.log(`[apify] all downloads complete. saved to: ${outputDir}`); - return downloaded; - } -} - -// Popular actors registry -export const ACTORS = { - TIKTOK: { - SCRAPER: "clockworks/tiktok-scraper", - HASHTAG: "clockworks/tiktok-hashtag-scraper", - PROFILE: "clockworks/tiktok-profile-scraper", - }, - WEB: { - SCRAPER: "apify/web-scraper", - CHEERIO: "apify/cheerio-scraper", - PUPPETEER: "apify/puppeteer-scraper", - }, - SOCIAL: { - INSTAGRAM: "apify/instagram-scraper", - TWITTER: "apify/twitter-scraper", - YOUTUBE: "bernardo/youtube-scraper", - }, -}; - -// Export singleton instance -export const apifyProvider = new ApifyProvider(); - -// Export convenience functions -export const runActor = (options: RunActorOptions) => - apifyProvider.runActor(options); - -export const getDataset = (datasetId: string) => - apifyProvider.getDataset(datasetId); - -export const getRunInfo = (runId: string) => apifyProvider.getRunInfo(runId); - -export const waitForRun = (runId: string) => apifyProvider.waitForRun(runId); - -export const getKeyValueStoreValue = (storeId: string, key: string) => - apifyProvider.getKeyValueStoreValue(storeId, key); - -export const downloadVideos = ( - items: Array<{ webVideoUrl?: string }>, - outputDir?: string, -) => apifyProvider.downloadVideos(items, outputDir); diff --git a/src/providers/elevenlabs.ts b/src/providers/elevenlabs.ts deleted file mode 100644 index 88a70ff3..00000000 --- a/src/providers/elevenlabs.ts +++ /dev/null @@ -1,217 +0,0 @@ -/** - * ElevenLabs provider for voice generation and text-to-speech - */ - -import { writeFileSync } from "node:fs"; -import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js"; -import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types"; -import { BaseProvider } from "./base"; - -export class ElevenLabsProvider extends BaseProvider { - readonly name = "elevenlabs"; - private _client: ElevenLabsClient | null = null; - - /** - * Lazy initialization of the client to avoid errors when API keys aren't set - */ - private get client(): ElevenLabsClient { - if (!this._client) { - const apiKey = this.config.apiKey || process.env.ELEVENLABS_API_KEY; - if (!apiKey) { - throw new Error( - "ElevenLabs API key not found. Set ELEVENLABS_API_KEY environment variable.", - ); - } - this._client = new ElevenLabsClient({ apiKey }); - } - return this._client; - } - - async submit( - _model: string, - _inputs: Record, - _config?: ProviderConfig, - ): Promise { - // ElevenLabs is synchronous, so we generate immediately and return a fake ID - const jobId = `el_${Date.now()}_${Math.random().toString(36).slice(2)}`; - console.log(`[elevenlabs] starting generation: ${jobId}`); - return jobId; - } - - async getStatus(_jobId: string): Promise { - // ElevenLabs is synchronous - return { status: "completed" }; - } - - async getResult(_jobId: string): Promise { - return null; - } - - // ============================================================================ - // High-level convenience methods - // ============================================================================ - - async textToSpeech(options: { - text: string; - voiceId?: string; - modelId?: string; - outputPath?: string; - }): Promise { - const { - text, - voiceId = VOICES.RACHEL, - modelId = "eleven_multilingual_v2", - outputPath, - } = options; - - console.log(`[elevenlabs] generating speech with voice ${voiceId}...`); - - const audio = await this.client.textToSpeech.convert(voiceId, { - text, - modelId, - outputFormat: "mp3_44100_128", - }); - - const reader = audio.getReader(); - const chunks: Uint8Array[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - - const buffer = Buffer.concat(chunks); - - if (outputPath) { - writeFileSync(outputPath, buffer); - console.log(`[elevenlabs] saved to ${outputPath}`); - } - - console.log(`[elevenlabs] generated ${buffer.length} bytes`); - return buffer; - } - - async listVoices() { - console.log(`[elevenlabs] fetching voices...`); - const response = await this.client.voices.getAll(); - console.log(`[elevenlabs] found ${response.voices.length} voices`); - return response.voices; - } - - async getVoice(voiceId: string) { - console.log(`[elevenlabs] fetching voice ${voiceId}...`); - const voice = await this.client.voices.get(voiceId); - console.log(`[elevenlabs] found voice: ${voice.name}`); - return voice; - } - - async generateMusic(options: { - prompt: string; - musicLengthMs?: number; - outputPath?: string; - }): Promise { - const { prompt, musicLengthMs, outputPath } = options; - - console.log(`[elevenlabs] generating music from prompt: "${prompt}"...`); - - const audio = await this.client.music.compose({ - prompt, - musicLengthMs, - modelId: "music_v1", - }); - - const reader = audio.getReader(); - const chunks: Uint8Array[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - - const buffer = Buffer.concat(chunks); - - if (outputPath) { - writeFileSync(outputPath, buffer); - console.log(`[elevenlabs] saved to ${outputPath}`); - } - - console.log(`[elevenlabs] generated ${buffer.length} bytes`); - return buffer; - } - - async generateSoundEffect(options: { - text: string; - durationSeconds?: number; - promptInfluence?: number; - loop?: boolean; - outputPath?: string; - }): Promise { - const { - text, - durationSeconds, - promptInfluence = 0.3, - loop = false, - outputPath, - } = options; - - console.log(`[elevenlabs] generating sound effect: "${text}"...`); - - const audio = await this.client.textToSoundEffects.convert({ - text, - durationSeconds, - promptInfluence, - loop, - }); - - const reader = audio.getReader(); - const chunks: Uint8Array[] = []; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); - } - - const buffer = Buffer.concat(chunks); - - if (outputPath) { - writeFileSync(outputPath, buffer); - console.log(`[elevenlabs] saved to ${outputPath}`); - } - - console.log(`[elevenlabs] generated ${buffer.length} bytes`); - return buffer; - } -} - -// Popular voices -export const VOICES = { - RACHEL: "21m00Tcm4TlvDq8ikWAM", - DOMI: "AZnzlk1XvdvUeBnXmlld", - BELLA: "EXAVITQu4vr4xnSDxMaL", - ANTONI: "ErXwobaYiN019PkySvjV", - ELLI: "MF3mGyEYCl7XYWbV9V6O", - JOSH: "TxGEqnHWrfWFTfGW9XjX", - ARNOLD: "VR6AewLTigWG4xSOukaG", - ADAM: "pNInz6obpgDQGcFmaJgB", - SAM: "yoZ06aMxZJJ28mfd3POQ", -}; - -// Export singleton instance (lazy initialization means no error on import) -export const elevenlabsProvider = new ElevenLabsProvider(); - -// Re-export convenience functions for backward compatibility -export const textToSpeech = ( - options: Parameters[0], -) => elevenlabsProvider.textToSpeech(options); -export const listVoices = () => elevenlabsProvider.listVoices(); -export const getVoice = (voiceId: string) => - elevenlabsProvider.getVoice(voiceId); -export const generateMusic = ( - options: Parameters[0], -) => elevenlabsProvider.generateMusic(options); -export const generateSoundEffect = ( - options: Parameters[0], -) => elevenlabsProvider.generateSoundEffect(options); diff --git a/src/providers/fal.ts b/src/providers/fal.ts deleted file mode 100644 index fcce259d..00000000 --- a/src/providers/fal.ts +++ /dev/null @@ -1,588 +0,0 @@ -/** - * fal.ai provider for video and image generation - * Supports Kling, Flux, Wan and other models - */ - -import { fal } from "@fal-ai/client"; -import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types"; -import { BaseProvider, ensureUrl } from "./base"; - -const falApiKey = process.env.FAL_API_KEY ?? process.env.FAL_KEY; -if (falApiKey) { - fal.config({ credentials: falApiKey }); -} - -export class FalProvider extends BaseProvider { - readonly name = "fal"; - - // Track model per job for status/result calls - private jobModels = new Map(); - - async submit( - model: string, - inputs: Record, - _config?: ProviderConfig, - ): Promise { - // Handle nano-banana-pro routing: use /edit endpoint when image_urls provided - const resolvedModel = this.resolveModelEndpoint(model, inputs); - - // Upload local files if needed - const processedInputs = await this.processInputs(inputs); - - const result = await fal.queue.submit(resolvedModel, { - input: processedInputs, - }); - - // Store model for later status/result calls - this.jobModels.set(result.request_id, resolvedModel); - - return result.request_id; - } - - /** - * Resolve model endpoint based on inputs - * Handles special routing for models like nano-banana-pro (text-to-image vs image-to-image) - */ - private resolveModelEndpoint( - model: string, - inputs: Record, - ): string { - // Nano Banana Pro: use /edit endpoint when image_urls are provided - if (model === "fal-ai/nano-banana-pro") { - const imageUrls = inputs.image_urls as string[] | undefined; - if (imageUrls && imageUrls.length > 0) { - return "fal-ai/nano-banana-pro/edit"; - } - } - return model; - } - - async getStatus(jobId: string): Promise { - const model = this.jobModels.get(jobId); - if (!model) { - throw new Error(`Unknown job: ${jobId}`); - } - - const status = await fal.queue.status(model, { requestId: jobId }); - - const statusMap: Record = { - IN_QUEUE: "queued", - IN_PROGRESS: "processing", - COMPLETED: "completed", - FAILED: "failed", - }; - - // @ts-expect-error - logs may exist on some status types - const logs = status.logs?.map((l: { message: string }) => l.message); - - return { - status: statusMap[status.status] ?? "processing", - logs, - }; - } - - async getResult(jobId: string): Promise { - const model = this.jobModels.get(jobId); - if (!model) { - throw new Error(`Unknown job: ${jobId}`); - } - - const result = await fal.queue.result(model, { requestId: jobId }); - - // Clean up job model mapping after getting result - this.jobModels.delete(jobId); - - return result.data; - } - - override async uploadFile( - file: File | Blob | ArrayBuffer, - _filename?: string, - ): Promise { - const blob = - file instanceof ArrayBuffer - ? new Blob([file]) - : file instanceof Blob - ? file - : file; - - const url = await fal.storage.upload(blob); - return url; - } - - /** - * Process inputs, uploading local files as needed - */ - private async processInputs( - inputs: Record, - ): Promise> { - const processed: Record = {}; - - for (const [key, value] of Object.entries(inputs)) { - if (typeof value === "string" && this.looksLikeLocalPath(value)) { - processed[key] = await ensureUrl(value, (buffer) => - this.uploadFile(buffer), - ); - } else { - processed[key] = value; - } - } - - return processed; - } - - private looksLikeLocalPath(value: string): boolean { - return ( - !value.startsWith("http://") && - !value.startsWith("https://") && - (value.includes("/") || value.includes("\\")) - ); - } - - // ============================================================================ - // High-level convenience methods (preserved from original lib/fal.ts) - // ============================================================================ - - async imageToVideo(args: { - prompt: string; - imageUrl: string; - modelVersion?: string; - duration?: 5 | 10; - tailImageUrl?: string; - }) { - const modelId = `fal-ai/kling-video/${args.modelVersion || "v2.5-turbo/pro"}/image-to-video`; - - console.log(`[fal] starting image-to-video: ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const imageUrl = await ensureUrl(args.imageUrl, (buffer) => - this.uploadFile(buffer), - ); - const tailImageUrl = args.tailImageUrl - ? await ensureUrl(args.tailImageUrl, (buffer) => this.uploadFile(buffer)) - : undefined; - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - image_url: imageUrl, - duration: args.duration || 5, - ...(tailImageUrl && { tail_image_url: tailImageUrl }), - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - async textToVideo(args: { - prompt: string; - modelVersion?: string; - duration?: 5 | 10; - aspectRatio?: "16:9" | "9:16" | "1:1"; - }) { - const modelId = `fal-ai/kling-video/${args.modelVersion || "v2.5-turbo/pro"}/text-to-video`; - - console.log(`[fal] starting text-to-video: ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - duration: args.duration || 5, - aspect_ratio: args.aspectRatio || "16:9", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - async generateImage(args: { - prompt: string; - model?: string; - imageSize?: string; - }) { - const modelId = args.model || "fal-ai/flux-pro/v1.1"; - - console.log(`[fal] generating image with ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - image_size: args.imageSize || "landscape_4_3", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - async imageToImage(args: { - prompt: string; - imageUrl: string; - aspectRatio?: string; - }) { - const modelId = "fal-ai/nano-banana-pro/edit"; - - console.log(`[fal] starting image-to-image: ${modelId}`); - - const imageUrl = await ensureUrl(args.imageUrl, (buffer) => - this.uploadFile(buffer), - ); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - image_urls: [imageUrl], - aspect_ratio: (args.aspectRatio || "1:1") as - | "16:9" - | "9:16" - | "1:1" - | "21:9" - | "3:2" - | "4:3" - | "5:4" - | "4:5" - | "3:4" - | "2:3", - resolution: "2K", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - async wan25(args: { - prompt: string; - imageUrl: string; - audioUrl: string; - resolution?: "480p" | "720p" | "1080p"; - duration?: "5" | "10"; - negativePrompt?: string; - }) { - const modelId = "fal-ai/wan-25-preview/image-to-video"; - - console.log(`[fal] starting wan-25: ${modelId}`); - - const imageUrl = await ensureUrl(args.imageUrl, (buffer) => - this.uploadFile(buffer), - ); - const audioUrl = await ensureUrl(args.audioUrl, (buffer) => - this.uploadFile(buffer), - ); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - image_url: imageUrl, - audio_url: audioUrl, - resolution: args.resolution || "480p", - duration: args.duration || "5", - negative_prompt: - args.negativePrompt || - "low resolution, error, worst quality, low quality, defects", - enable_prompt_expansion: true, - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - async textToMusic(args: { - prompt?: string; - tags?: string[]; - lyricsPrompt?: string; - seed?: number; - promptStrength?: number; - balanceStrength?: number; - numSongs?: 1 | 2; - outputFormat?: "flac" | "mp3" | "wav" | "ogg" | "m4a"; - outputBitRate?: 128 | 192 | 256 | 320; - bpm?: number | "auto"; - }) { - const modelId = "fal-ai/sonauto/bark"; - - console.log(`[fal] starting text-to-music: ${modelId}`); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - tags: args.tags, - lyrics_prompt: args.lyricsPrompt, - seed: args.seed, - prompt_strength: args.promptStrength, - balance_strength: args.balanceStrength, - num_songs: args.numSongs, - output_format: args.outputFormat, - output_bit_rate: args.outputBitRate, - bpm: args.bpm, - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - // ============================================================================ - // Grok Imagine Video methods (xAI) - // ============================================================================ - - /** - * Generate video from text using Grok Imagine Video - * Supports 1-15 second videos at 480p or 720p resolution - */ - async grokTextToVideo(args: { - prompt: string; - duration?: number; - aspectRatio?: "16:9" | "4:3" | "3:2" | "1:1" | "2:3" | "3:4" | "9:16"; - resolution?: "480p" | "720p"; - }) { - const modelId = "xai/grok-imagine-video/text-to-video"; - - console.log(`[fal] starting grok text-to-video: ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - duration: args.duration ?? 6, - aspect_ratio: args.aspectRatio ?? "16:9", - resolution: args.resolution ?? "720p", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - /** - * Generate video from image using Grok Imagine Video - * Supports 1-15 second videos at 480p or 720p resolution - */ - async grokImageToVideo(args: { - prompt: string; - imageUrl: string; - duration?: number; - aspectRatio?: - | "auto" - | "16:9" - | "4:3" - | "3:2" - | "1:1" - | "2:3" - | "3:4" - | "9:16"; - resolution?: "480p" | "720p"; - }) { - const modelId = "xai/grok-imagine-video/image-to-video"; - - console.log(`[fal] starting grok image-to-video: ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const imageUrl = await ensureUrl(args.imageUrl, (buffer) => - this.uploadFile(buffer), - ); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - image_url: imageUrl, - duration: args.duration ?? 6, - aspect_ratio: args.aspectRatio ?? "auto", - resolution: args.resolution ?? "720p", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - /** - * Edit video using Grok Imagine Video - * Video will be resized to max 854x480 and truncated to 8 seconds - */ - async grokEditVideo(args: { - prompt: string; - videoUrl: string; - resolution?: "auto" | "480p" | "720p"; - }) { - const modelId = "xai/grok-imagine-video/edit-video"; - - console.log(`[fal] starting grok edit-video: ${modelId}`); - console.log(`[fal] prompt: ${args.prompt}`); - - const videoUrl = await ensureUrl(args.videoUrl, (buffer) => - this.uploadFile(buffer), - ); - - const result = await fal.subscribe(modelId, { - input: { - prompt: args.prompt, - video_url: videoUrl, - resolution: args.resolution ?? "auto", - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } - - // ============================================================================ - // Qwen Image Edit 2511 Multiple Angles - // ============================================================================ - - /** - * Adjust camera angle of an image using Qwen Image Edit 2511 Multiple Angles - * Generates same scene from different angles (azimuth/elevation) - */ - async qwenMultipleAngles(args: { - imageUrl: string; - horizontalAngle?: number; - verticalAngle?: number; - zoom?: number; - additionalPrompt?: string; - loraScale?: number; - imageSize?: string | { width: number; height: number }; - guidanceScale?: number; - numInferenceSteps?: number; - acceleration?: "none" | "regular"; - negativePrompt?: string; - seed?: number; - outputFormat?: "png" | "jpeg" | "webp"; - numImages?: number; - }) { - const modelId = "fal-ai/qwen-image-edit-2511-multiple-angles"; - - console.log(`[fal] starting qwen multiple angles: ${modelId}`); - - const imageUrl = await ensureUrl(args.imageUrl, (buffer) => - this.uploadFile(buffer), - ); - - const result = await fal.subscribe(modelId, { - input: { - image_urls: [imageUrl], - horizontal_angle: args.horizontalAngle ?? 0, - vertical_angle: args.verticalAngle ?? 0, - zoom: args.zoom ?? 5, - additional_prompt: args.additionalPrompt, - lora_scale: args.loraScale ?? 1, - image_size: args.imageSize, - guidance_scale: args.guidanceScale ?? 4.5, - num_inference_steps: args.numInferenceSteps ?? 28, - acceleration: args.acceleration ?? "regular", - negative_prompt: args.negativePrompt ?? "", - seed: args.seed, - output_format: args.outputFormat ?? "png", - num_images: args.numImages ?? 1, - }, - logs: true, - onQueueUpdate: (update) => { - if (update.status === "IN_PROGRESS") { - console.log( - `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`, - ); - } - }, - }); - - console.log("[fal] completed!"); - return result; - } -} - -// Export singleton instance -export const falProvider = new FalProvider(); - -// Re-export convenience functions for backward compatibility -export const imageToVideo = ( - args: Parameters[0], -) => falProvider.imageToVideo(args); -export const textToVideo = (args: Parameters[0]) => - falProvider.textToVideo(args); -export const generateImage = ( - args: Parameters[0], -) => falProvider.generateImage(args); -export const imageToImage = ( - args: Parameters[0], -) => falProvider.imageToImage(args); -export const wan25 = (args: Parameters[0]) => - falProvider.wan25(args); -export const textToMusic = (args: Parameters[0]) => - falProvider.textToMusic(args); diff --git a/src/providers/higgsfield.ts b/src/providers/higgsfield.ts deleted file mode 100644 index 2b6c3911..00000000 --- a/src/providers/higgsfield.ts +++ /dev/null @@ -1,145 +0,0 @@ -/** - * Higgsfield provider for Soul image generation and character creation - */ - -import { - BatchSize, - HiggsfieldClient, - InputImageType, - SoulQuality, - SoulSize, -} from "@higgsfield/client"; -import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types"; -import { BaseProvider } from "./base"; - -export class HiggsfieldProvider extends BaseProvider { - readonly name = "higgsfield"; - private _client: HiggsfieldClient | null = null; - - /** - * Lazy initialization of the client to avoid errors when API keys aren't set - */ - private get client(): HiggsfieldClient { - if (!this._client) { - const apiKey = - this.config.apiKey || - process.env.HIGGSFIELD_API_KEY || - process.env.HF_API_KEY; - const apiSecret = - process.env.HIGGSFIELD_SECRET || process.env.HF_API_SECRET; - - if (!apiKey || !apiSecret) { - throw new Error( - "Higgsfield API credentials not found. Set HIGGSFIELD_API_KEY/HF_API_KEY and HIGGSFIELD_SECRET/HF_API_SECRET environment variables.", - ); - } - - this._client = new HiggsfieldClient({ apiKey, apiSecret }); - } - return this._client; - } - - async submit( - model: string, - inputs: Record, - _config?: ProviderConfig, - ): Promise { - const jobSet = await this.client.generate(model as "/v1/text2image/soul", { - prompt: inputs.prompt as string, - width_and_height: - (inputs.widthAndHeight as (typeof SoulSize)[keyof typeof SoulSize]) || - SoulSize.PORTRAIT_1152x2048, - quality: - (inputs.quality as (typeof SoulQuality)[keyof typeof SoulQuality]) || - SoulQuality.HD, - style_id: inputs.styleId as string | undefined, - batch_size: - (inputs.batchSize as (typeof BatchSize)[keyof typeof BatchSize]) || - BatchSize.SINGLE, - enhance_prompt: (inputs.enhancePrompt as boolean) ?? false, - }); - - console.log(`[higgsfield] job submitted: ${jobSet.id}`); - return jobSet.id; - } - - async getStatus(_jobId: string): Promise { - // Higgsfield jobs complete synchronously via submit - return { status: "completed" }; - } - - async getResult(_jobId: string): Promise { - return null; - } - - // ============================================================================ - // High-level convenience methods - // ============================================================================ - - async generateSoul(args: { - prompt: string; - widthAndHeight?: (typeof SoulSize)[keyof typeof SoulSize]; - quality?: (typeof SoulQuality)[keyof typeof SoulQuality]; - styleId?: string; - batchSize?: (typeof BatchSize)[keyof typeof BatchSize]; - enhancePrompt?: boolean; - }) { - console.log("[higgsfield] generating soul image"); - console.log(`[higgsfield] prompt: ${args.prompt}`); - - const jobSet = await this.client.generate("/v1/text2image/soul", { - prompt: args.prompt, - width_and_height: args.widthAndHeight || SoulSize.PORTRAIT_1152x2048, - quality: args.quality || SoulQuality.HD, - style_id: args.styleId, - batch_size: args.batchSize || BatchSize.SINGLE, - enhance_prompt: args.enhancePrompt ?? false, - }); - - console.log(`[higgsfield] job created: ${jobSet.id}`); - return jobSet; - } - - async listSoulStyles() { - console.log("[higgsfield] fetching soul styles"); - return this.client.getSoulStyles(); - } - - async createSoulId(args: { name: string; imageUrls: string[] }) { - console.log(`[higgsfield] creating soul id: ${args.name}`); - console.log(`[higgsfield] images: ${args.imageUrls.length}`); - - const soulId = await this.client.createSoulId({ - name: args.name, - input_images: args.imageUrls.map((url) => ({ - type: InputImageType.IMAGE_URL, - image_url: url, - })), - }); - - console.log(`[higgsfield] soul id created: ${soulId.id}`); - return soulId; - } - - async listSoulIds(page = 1, pageSize = 20) { - console.log("[higgsfield] listing soul ids"); - return this.client.listSoulIds(page, pageSize); - } -} - -// Re-export useful enums -export { BatchSize, SoulQuality, SoulSize }; - -// Export singleton instance (lazy initialization means no error on import) -export const higgsfieldProvider = new HiggsfieldProvider(); - -// Re-export convenience functions for backward compatibility -export const generateSoul = ( - args: Parameters[0], -) => higgsfieldProvider.generateSoul(args); -export const listSoulStyles = () => higgsfieldProvider.listSoulStyles(); -export const createSoulId = ( - args: Parameters[0], -) => higgsfieldProvider.createSoulId(args); -export const listSoulIds = (page?: number, pageSize?: number) => - higgsfieldProvider.listSoulIds(page, pageSize); diff --git a/src/providers/index.ts b/src/providers/index.ts index 4881cc56..48f6cb89 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,25 +1,8 @@ /** * Provider exports - * Central registry of all available providers + * Local processing tools and infrastructure providers */ -export type { - ApifyProviderConfig, - ApifyRunResult, - RunActorOptions, -} from "./apify"; -// Apify provider (web scraping / actors) -export { - ACTORS, - ApifyProvider, - apifyProvider, - downloadVideos, - getDataset, - getKeyValueStoreValue, - getRunInfo, - runActor, - waitForRun, -} from "./apify"; export type { ProviderResult } from "./base"; // Base provider infrastructure export { @@ -30,28 +13,6 @@ export { ProviderRegistry, providers, } from "./base"; -// ElevenLabs provider (voice/audio) -export { - ElevenLabsProvider, - elevenlabsProvider, - generateMusic as generateMusicElevenlabs, - generateSoundEffect, - getVoice, - listVoices, - textToSpeech, - VOICES, -} from "./elevenlabs"; -// Fal.ai provider (video/image generation) -export { - FalProvider, - falProvider, - generateImage, - imageToImage, - imageToVideo, - textToMusic, - textToVideo, - wan25, -} from "./fal"; export type { ProbeResult } from "./ffmpeg"; // FFmpeg provider (local video editing) export { @@ -86,27 +47,6 @@ export { listModels, transcribeAudio, } from "./groq"; -// Higgsfield provider (Soul image generation) -export { - BatchSize, - createSoulId, - generateSoul, - HiggsfieldProvider, - higgsfieldProvider, - listSoulIds, - listSoulStyles, - SoulQuality, - SoulSize, -} from "./higgsfield"; -// Replicate provider (video/image generation) -export { - MODELS, - ReplicateProvider, - replicateProvider, - runImage, - runModel, - runVideo, -} from "./replicate"; export type { StorageConfig } from "./storage"; // Storage provider (Cloudflare R2 / S3) export { @@ -120,24 +60,13 @@ export { } from "./storage"; // Register all providers -import { apifyProvider } from "./apify"; import { providers } from "./base"; -import { elevenlabsProvider } from "./elevenlabs"; -import { falProvider } from "./fal"; import { ffmpegProvider } from "./ffmpeg"; import { fireworksProvider } from "./fireworks"; import { groqProvider } from "./groq"; -import { higgsfieldProvider } from "./higgsfield"; -import { replicateProvider } from "./replicate"; import { storageProvider } from "./storage"; -// Auto-register all providers -providers.register(apifyProvider); -providers.register(falProvider); -providers.register(replicateProvider); -providers.register(elevenlabsProvider); +providers.register(ffmpegProvider); providers.register(groqProvider); providers.register(fireworksProvider); -providers.register(higgsfieldProvider); -providers.register(ffmpegProvider); providers.register(storageProvider); diff --git a/src/providers/replicate.ts b/src/providers/replicate.ts deleted file mode 100644 index a7b62b93..00000000 --- a/src/providers/replicate.ts +++ /dev/null @@ -1,147 +0,0 @@ -/** - * Replicate provider for video and image generation - * Supports Minimax, Kling, Luma, Flux, and other models - */ - -import Replicate from "replicate"; -import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types"; -import { BaseProvider } from "./base"; - -export class ReplicateProvider extends BaseProvider { - readonly name = "replicate"; - private client: Replicate; - - constructor(config?: ProviderConfig) { - super(config); - this.client = new Replicate({ - auth: config?.apiKey || process.env.REPLICATE_API_TOKEN || "", - }); - } - - async submit( - model: string, - inputs: Record, - _config?: ProviderConfig, - ): Promise { - // Transform inputs for provider-specific field names - const transformedInputs = this.transformInputs(model, inputs); - - const prediction = await this.client.predictions.create({ - model: model as `${string}/${string}`, - input: transformedInputs, - }); - - console.log(`[replicate] job submitted: ${prediction.id}`); - return prediction.id; - } - - /** - * Transform inputs for provider-specific field names - */ - private transformInputs( - model: string, - inputs: Record, - ): Record { - // Nano Banana Pro: Replicate uses 'image_input' instead of 'image_urls' - if (model === "google/nano-banana-pro" && inputs.image_urls) { - const { image_urls, ...rest } = inputs; - return { - ...rest, - image_input: image_urls, - }; - } - return inputs; - } - - async getStatus(jobId: string): Promise { - const prediction = await this.client.predictions.get(jobId); - - const statusMap: Record = { - starting: "queued", - processing: "processing", - succeeded: "completed", - failed: "failed", - canceled: "cancelled", - }; - - return { - status: statusMap[prediction.status] ?? "processing", - output: prediction.output, - error: - typeof prediction.error === "string" ? prediction.error : undefined, - }; - } - - async getResult(jobId: string): Promise { - const prediction = await this.client.predictions.get(jobId); - return prediction.output; - } - - override async cancel(jobId: string): Promise { - await this.client.predictions.cancel(jobId); - } - - // ============================================================================ - // High-level convenience methods - // ============================================================================ - - async runModel(model: string, input: Record) { - console.log(`[replicate] running ${model}...`); - - const output = await this.client.run(model as `${string}/${string}`, { - input, - }); - - console.log(`[replicate] completed`); - return output; - } - - async runVideo(options: { model: string; input: Record }) { - return this.runModel(options.model, options.input); - } - - async runImage(options: { model: string; input: Record }) { - return this.runModel(options.model, options.input); - } - - async listPredictions() { - return this.client.predictions.list(); - } - - async getPrediction(id: string) { - return this.client.predictions.get(id); - } -} - -// Popular models registry -export const MODELS = { - VIDEO: { - MINIMAX: "minimax/video-01", - KLING: "fofr/kling-v1.5", - LUMA: "fofr/ltx-video", - RUNWAY_GEN3: "replicate/runway-gen3-turbo", - WAN_2_5: "wan-video/wan-2.5-i2v", - }, - IMAGE: { - FLUX_PRO: "black-forest-labs/flux-1.1-pro", - FLUX_DEV: "black-forest-labs/flux-dev", - FLUX_SCHNELL: "black-forest-labs/flux-schnell", - STABLE_DIFFUSION: "stability-ai/sdxl", - NANO_BANANA_PRO: "google/nano-banana-pro", - }, -}; - -// Export singleton instance -export const replicateProvider = new ReplicateProvider(); - -// Re-export convenience functions for backward compatibility -export const runModel = (model: string, input: Record) => - replicateProvider.runModel(model, input); -export const runVideo = (options: { - model: string; - input: Record; -}) => replicateProvider.runVideo(options); -export const runImage = (options: { - model: string; - input: Record; -}) => replicateProvider.runImage(options); diff --git a/src/tests/all.test.ts b/src/tests/all.test.ts index 7b3f5850..ec796501 100644 --- a/src/tests/all.test.ts +++ b/src/tests/all.test.ts @@ -130,21 +130,6 @@ await test("registry search works", async () => { console.log("\nšŸ”Œ PROVIDER TESTS\n"); -await test("fal provider is registered", async () => { - const fal = providers.get("fal"); - if (!fal) throw new Error("Fal provider not found"); -}); - -await test("replicate provider is registered", async () => { - const replicate = providers.get("replicate"); - if (!replicate) throw new Error("Replicate provider not found"); -}); - -await test("elevenlabs provider is registered", async () => { - const el = providers.get("elevenlabs"); - if (!el) throw new Error("ElevenLabs provider not found"); -}); - await test("groq provider is registered", async () => { const groq = providers.get("groq"); if (!groq) throw new Error("Groq provider not found"); @@ -308,15 +293,18 @@ console.log("\n🌐 LIVE API TESTS (requires API keys)\n"); await test( "fal: generate image with flux", async () => { - const { falProvider } = await import("../providers/fal"); - const result = await falProvider.generateImage({ - prompt: "a cute cat sitting on a rainbow", - imageSize: "square", + const { fal } = await import("@fal-ai/client"); + const result = await fal.subscribe("fal-ai/flux-pro/v1.1" as string, { + input: { + prompt: "a cute cat sitting on a rainbow", + image_size: "square", + }, + logs: true, }); - if (!result?.data?.images?.[0]?.url) { - throw new Error("No image URL in result"); - } - console.log(` Generated: ${result.data.images[0].url}`); + const url = (result.data as { images?: Array<{ url?: string }> }) + ?.images?.[0]?.url; + if (!url) throw new Error("No image URL in result"); + console.log(` Generated: ${url}`); }, !hasApiKey(["FAL_API_KEY", "FAL_KEY"]), ); @@ -324,64 +312,42 @@ await test( await test( "fal: text-to-video with kling", async () => { - const { falProvider } = await import("../providers/fal"); - const result = await falProvider.textToVideo({ - prompt: "ocean waves crashing on beach", - duration: 5, - }); - if (!result?.data?.video?.url) { - throw new Error("No video URL in result"); - } - console.log(` Generated: ${result.data.video.url}`); - }, - !hasApiKey(["FAL_API_KEY", "FAL_KEY"]), -); - -// Replicate tests -await test( - "replicate: run flux image generation", - async () => { - const { replicateProvider, MODELS } = await import( - "../providers/replicate" + const { fal } = await import("@fal-ai/client"); + const result = await fal.subscribe( + "fal-ai/kling-video/v2.5-turbo/pro/text-to-video" as string, + { + input: { prompt: "ocean waves crashing on beach", duration: "5" }, + logs: true, + }, ); - const result = await replicateProvider.runImage({ - model: MODELS.IMAGE.FLUX_SCHNELL, - input: { prompt: "a mountain landscape at sunset" }, - }); - console.log(` Generated: ${JSON.stringify(result).slice(0, 100)}...`); + const url = (result.data as { video?: { url?: string } })?.video?.url; + if (!url) throw new Error("No video URL in result"); + console.log(` Generated: ${url}`); }, - !hasApiKey("REPLICATE_API_TOKEN"), + !hasApiKey(["FAL_API_KEY", "FAL_KEY"]), ); // ElevenLabs tests await test( "elevenlabs: text-to-speech", async () => { - const { elevenlabsProvider } = await import("../providers/elevenlabs"); - const buffer = await elevenlabsProvider.textToSpeech({ + const { ElevenLabsClient } = await import("@elevenlabs/elevenlabs-js"); + const client = new ElevenLabsClient({ + apiKey: process.env.ELEVENLABS_API_KEY, + }); + const stream = await client.textToSpeech.convert("21m00Tcm4TlvDq8ikWAM", { text: "Hello, this is a test of the varg SDK.", + modelId: "eleven_multilingual_v2", }); - if (buffer.length === 0) { - throw new Error("Empty audio buffer"); - } + const chunks: Buffer[] = []; + for await (const chunk of stream) chunks.push(Buffer.from(chunk)); + const buffer = Buffer.concat(chunks); + if (buffer.length === 0) throw new Error("Empty audio buffer"); console.log(` Generated: ${buffer.length} bytes of audio`); }, !hasApiKey("ELEVENLABS_API_KEY"), ); -await test( - "elevenlabs: list voices", - async () => { - const { elevenlabsProvider } = await import("../providers/elevenlabs"); - const voices = await elevenlabsProvider.listVoices(); - if (voices.length === 0) { - throw new Error("No voices found"); - } - console.log(` Found ${voices.length} voices`); - }, - !hasApiKey("ELEVENLABS_API_KEY"), -); - // Groq tests await test( "groq: chat completion", @@ -416,8 +382,12 @@ await test( await test( "higgsfield: list soul styles", async () => { - const { higgsfieldProvider } = await import("../providers/higgsfield"); - const styles = await higgsfieldProvider.listSoulStyles(); + const { HiggsfieldClient } = await import("@higgsfield/client"); + const client = new HiggsfieldClient({ + apiKey: process.env.HIGGSFIELD_API_KEY || process.env.HF_API_KEY, + apiSecret: process.env.HIGGSFIELD_SECRET || process.env.HF_API_SECRET, + }); + const styles = await client.getSoulStyles(); console.log(` Found styles: ${JSON.stringify(styles).slice(0, 100)}...`); }, !hasApiKey(["HIGGSFIELD_API_KEY", "HF_API_KEY"]), diff --git a/src/tests/unit.test.ts b/src/tests/unit.test.ts index 1dd6e564..bc6ce6a9 100644 --- a/src/tests/unit.test.ts +++ b/src/tests/unit.test.ts @@ -354,16 +354,7 @@ for (const skill of allSkills) { console.log(`\n${dim("─ Provider Registration ─\n")}`); -const expectedProviders = [ - "fal", - "replicate", - "elevenlabs", - "groq", - "fireworks", - "higgsfield", - "ffmpeg", - "storage", -]; +const expectedProviders = ["groq", "fireworks", "ffmpeg", "storage"]; for (const name of expectedProviders) { test(`provider '${name}' is registered`, () => {