@weirdfingers/baseboards 0.9.6 → 0.9.7
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/dist/index.js +560 -469
- package/dist/index.js.map +1 -1
- package/package.json +2 -5
- package/templates/README.md +0 -122
- package/templates/api/.env.example +0 -65
- package/templates/api/ARTIFACT_RESOLUTION_GUIDE.md +0 -148
- package/templates/api/Dockerfile +0 -32
- package/templates/api/README.md +0 -264
- package/templates/api/alembic/env.py +0 -114
- package/templates/api/alembic/script.py.mako +0 -28
- package/templates/api/alembic/versions/20250101_000000_initial_schema.py +0 -506
- package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +0 -75
- package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +0 -467
- package/templates/api/alembic/versions/20251202_000000_add_artifact_lineage.py +0 -134
- package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +0 -88
- package/templates/api/alembic.ini +0 -36
- package/templates/api/config/generators.yaml +0 -237
- package/templates/api/config/storage_config.yaml +0 -26
- package/templates/api/docs/ADDING_GENERATORS.md +0 -409
- package/templates/api/docs/GENERATORS_API.md +0 -502
- package/templates/api/docs/MIGRATIONS.md +0 -472
- package/templates/api/docs/TESTING_LIVE_APIS.md +0 -417
- package/templates/api/docs/storage_providers.md +0 -337
- package/templates/api/pyproject.toml +0 -205
- package/templates/api/src/boards/__init__.py +0 -10
- package/templates/api/src/boards/api/app.py +0 -172
- package/templates/api/src/boards/api/auth.py +0 -75
- package/templates/api/src/boards/api/endpoints/__init__.py +0 -3
- package/templates/api/src/boards/api/endpoints/jobs.py +0 -76
- package/templates/api/src/boards/api/endpoints/setup.py +0 -505
- package/templates/api/src/boards/api/endpoints/sse.py +0 -129
- package/templates/api/src/boards/api/endpoints/storage.py +0 -155
- package/templates/api/src/boards/api/endpoints/tenant_registration.py +0 -296
- package/templates/api/src/boards/api/endpoints/uploads.py +0 -149
- package/templates/api/src/boards/api/endpoints/webhooks.py +0 -13
- package/templates/api/src/boards/auth/__init__.py +0 -15
- package/templates/api/src/boards/auth/adapters/__init__.py +0 -27
- package/templates/api/src/boards/auth/adapters/auth0.py +0 -220
- package/templates/api/src/boards/auth/adapters/base.py +0 -73
- package/templates/api/src/boards/auth/adapters/clerk.py +0 -172
- package/templates/api/src/boards/auth/adapters/jwt.py +0 -122
- package/templates/api/src/boards/auth/adapters/none.py +0 -102
- package/templates/api/src/boards/auth/adapters/oidc.py +0 -284
- package/templates/api/src/boards/auth/adapters/supabase.py +0 -110
- package/templates/api/src/boards/auth/context.py +0 -35
- package/templates/api/src/boards/auth/factory.py +0 -129
- package/templates/api/src/boards/auth/middleware.py +0 -221
- package/templates/api/src/boards/auth/provisioning.py +0 -129
- package/templates/api/src/boards/auth/tenant_extraction.py +0 -278
- package/templates/api/src/boards/cli.py +0 -354
- package/templates/api/src/boards/config.py +0 -131
- package/templates/api/src/boards/database/__init__.py +0 -7
- package/templates/api/src/boards/database/cli.py +0 -110
- package/templates/api/src/boards/database/connection.py +0 -292
- package/templates/api/src/boards/database/models.py +0 -19
- package/templates/api/src/boards/database/seed_data.py +0 -182
- package/templates/api/src/boards/dbmodels/__init__.py +0 -441
- package/templates/api/src/boards/generators/__init__.py +0 -57
- package/templates/api/src/boards/generators/artifact_resolution.py +0 -405
- package/templates/api/src/boards/generators/artifacts.py +0 -53
- package/templates/api/src/boards/generators/base.py +0 -144
- package/templates/api/src/boards/generators/implementations/__init__.py +0 -14
- package/templates/api/src/boards/generators/implementations/fal/__init__.py +0 -25
- package/templates/api/src/boards/generators/implementations/fal/audio/__init__.py +0 -23
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_music_generation.py +0 -171
- package/templates/api/src/boards/generators/implementations/fal/audio/beatoven_sound_effect_generation.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_text_to_speech.py +0 -176
- package/templates/api/src/boards/generators/implementations/fal/audio/chatterbox_tts_turbo.py +0 -195
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_sound_effects_v2.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/audio/elevenlabs_tts_eleven_v3.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_elevenlabs_tts_turbo_v2_5.py +0 -206
- package/templates/api/src/boards/generators/implementations/fal/audio/fal_minimax_speech_26_hd.py +0 -237
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_music_v2.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/audio/minimax_speech_2_6_turbo.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/__init__.py +0 -63
- package/templates/api/src/boards/generators/implementations/fal/image/bytedance_seedream_v45_edit.py +0 -219
- package/templates/api/src/boards/generators/implementations/fal/image/clarity_upscaler.py +0 -220
- package/templates/api/src/boards/generators/implementations/fal/image/crystal_upscaler.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/image/fal_ideogram_character.py +0 -227
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2.py +0 -203
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_edit.py +0 -230
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro.py +0 -204
- package/templates/api/src/boards/generators/implementations/fal/image/flux_2_pro_edit.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_kontext.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/flux_pro_ultra.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gemini_25_flash_image_edit.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_15_edit.py +0 -216
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_5.py +0 -177
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_edit_image.py +0 -182
- package/templates/api/src/boards/generators/implementations/fal/image/gpt_image_1_mini.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_character_edit.py +0 -299
- package/templates/api/src/boards/generators/implementations/fal/image/ideogram_v2.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/image/imagen4_preview_fast.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_edit.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro.py +0 -179
- package/templates/api/src/boards/generators/implementations/fal/image/nano_banana_pro_edit.py +0 -226
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image.py +0 -249
- package/templates/api/src/boards/generators/implementations/fal/image/qwen_image_edit.py +0 -244
- package/templates/api/src/boards/generators/implementations/fal/image/reve_edit.py +0 -178
- package/templates/api/src/boards/generators/implementations/fal/image/reve_text_to_image.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/image/seedream_v45_text_to_image.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/utils.py +0 -61
- package/templates/api/src/boards/generators/implementations/fal/video/__init__.py +0 -77
- package/templates/api/src/boards/generators/implementations/fal/video/bytedance_seedance_v1_pro_text_to_video.py +0 -209
- package/templates/api/src/boards/generators/implementations/fal/video/creatify_lipsync.py +0 -161
- package/templates/api/src/boards/generators/implementations/fal/video/fal_bytedance_seedance_v1_pro_image_to_video.py +0 -222
- package/templates/api/src/boards/generators/implementations/fal/video/fal_minimax_hailuo_02_standard_text_to_video.py +0 -152
- package/templates/api/src/boards/generators/implementations/fal/video/fal_pixverse_lipsync.py +0 -197
- package/templates/api/src/boards/generators/implementations/fal/video/fal_sora_2_text_to_video.py +0 -173
- package/templates/api/src/boards/generators/implementations/fal/video/infinitalk.py +0 -221
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_pro.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_ai_avatar_v2_standard.py +0 -159
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_image_to_video.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/kling_video_v2_5_turbo_pro_text_to_video.py +0 -168
- package/templates/api/src/boards/generators/implementations/fal/video/minimax_hailuo_2_3_pro_image_to_video.py +0 -153
- package/templates/api/src/boards/generators/implementations/fal/video/sora2_image_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_image_to_video_pro.py +0 -175
- package/templates/api/src/boards/generators/implementations/fal/video/sora_2_text_to_video_pro.py +0 -163
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2.py +0 -167
- package/templates/api/src/boards/generators/implementations/fal/video/sync_lipsync_v2_pro.py +0 -155
- package/templates/api/src/boards/generators/implementations/fal/video/veed_fabric_1_0.py +0 -180
- package/templates/api/src/boards/generators/implementations/fal/video/veed_lipsync.py +0 -174
- package/templates/api/src/boards/generators/implementations/fal/video/veo3.py +0 -194
- package/templates/api/src/boards/generators/implementations/fal/video/veo31.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast.py +0 -190
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_fast_image_to_video.py +0 -191
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_first_last_frame_to_video.py +0 -187
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_image_to_video.py +0 -183
- package/templates/api/src/boards/generators/implementations/fal/video/veo31_reference_to_video.py +0 -172
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_image_to_video.py +0 -212
- package/templates/api/src/boards/generators/implementations/fal/video/wan_25_preview_text_to_video.py +0 -208
- package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py +0 -158
- package/templates/api/src/boards/generators/implementations/kie/__init__.py +0 -11
- package/templates/api/src/boards/generators/implementations/kie/base.py +0 -316
- package/templates/api/src/boards/generators/implementations/kie/image/__init__.py +0 -3
- package/templates/api/src/boards/generators/implementations/kie/image/nano_banana_edit.py +0 -190
- package/templates/api/src/boards/generators/implementations/kie/utils.py +0 -98
- package/templates/api/src/boards/generators/implementations/kie/video/__init__.py +0 -8
- package/templates/api/src/boards/generators/implementations/kie/video/veo3.py +0 -161
- package/templates/api/src/boards/generators/implementations/openai/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/audio/whisper.py +0 -69
- package/templates/api/src/boards/generators/implementations/openai/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/openai/image/dalle3.py +0 -96
- package/templates/api/src/boards/generators/implementations/replicate/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/image/flux_pro.py +0 -88
- package/templates/api/src/boards/generators/implementations/replicate/video/__init__.py +0 -1
- package/templates/api/src/boards/generators/implementations/replicate/video/lipsync.py +0 -73
- package/templates/api/src/boards/generators/loader.py +0 -253
- package/templates/api/src/boards/generators/registry.py +0 -114
- package/templates/api/src/boards/generators/resolution.py +0 -632
- package/templates/api/src/boards/generators/testmods/class_gen.py +0 -34
- package/templates/api/src/boards/generators/testmods/import_side_effect.py +0 -35
- package/templates/api/src/boards/graphql/__init__.py +0 -7
- package/templates/api/src/boards/graphql/access_control.py +0 -136
- package/templates/api/src/boards/graphql/mutations/root.py +0 -148
- package/templates/api/src/boards/graphql/queries/root.py +0 -116
- package/templates/api/src/boards/graphql/resolvers/__init__.py +0 -8
- package/templates/api/src/boards/graphql/resolvers/auth.py +0 -12
- package/templates/api/src/boards/graphql/resolvers/board.py +0 -1053
- package/templates/api/src/boards/graphql/resolvers/generation.py +0 -666
- package/templates/api/src/boards/graphql/resolvers/generator.py +0 -50
- package/templates/api/src/boards/graphql/resolvers/lineage.py +0 -381
- package/templates/api/src/boards/graphql/resolvers/upload.py +0 -463
- package/templates/api/src/boards/graphql/resolvers/user.py +0 -25
- package/templates/api/src/boards/graphql/schema.py +0 -81
- package/templates/api/src/boards/graphql/types/board.py +0 -102
- package/templates/api/src/boards/graphql/types/generation.py +0 -166
- package/templates/api/src/boards/graphql/types/generator.py +0 -17
- package/templates/api/src/boards/graphql/types/user.py +0 -47
- package/templates/api/src/boards/jobs/repository.py +0 -153
- package/templates/api/src/boards/logging.py +0 -195
- package/templates/api/src/boards/middleware.py +0 -339
- package/templates/api/src/boards/progress/__init__.py +0 -4
- package/templates/api/src/boards/progress/models.py +0 -25
- package/templates/api/src/boards/progress/publisher.py +0 -64
- package/templates/api/src/boards/py.typed +0 -0
- package/templates/api/src/boards/redis_pool.py +0 -118
- package/templates/api/src/boards/storage/__init__.py +0 -52
- package/templates/api/src/boards/storage/base.py +0 -363
- package/templates/api/src/boards/storage/config.py +0 -187
- package/templates/api/src/boards/storage/factory.py +0 -288
- package/templates/api/src/boards/storage/implementations/__init__.py +0 -27
- package/templates/api/src/boards/storage/implementations/gcs.py +0 -340
- package/templates/api/src/boards/storage/implementations/local.py +0 -201
- package/templates/api/src/boards/storage/implementations/s3.py +0 -294
- package/templates/api/src/boards/storage/implementations/supabase.py +0 -218
- package/templates/api/src/boards/tenant_isolation.py +0 -446
- package/templates/api/src/boards/validation.py +0 -262
- package/templates/api/src/boards/workers/__init__.py +0 -1
- package/templates/api/src/boards/workers/actors.py +0 -274
- package/templates/api/src/boards/workers/cli.py +0 -125
- package/templates/api/src/boards/workers/context.py +0 -348
- package/templates/api/src/boards/workers/middleware.py +0 -58
- package/templates/api/src/py.typed +0 -0
- package/templates/compose.web.yaml +0 -35
- package/templates/compose.yaml +0 -116
- package/templates/docker/env.example +0 -23
- package/templates/web/.env.example +0 -28
- package/templates/web/Dockerfile +0 -51
- package/templates/web/components.json +0 -22
- package/templates/web/imageLoader.js +0 -18
- package/templates/web/next-env.d.ts +0 -5
- package/templates/web/next.config.js +0 -36
- package/templates/web/package.json +0 -41
- package/templates/web/postcss.config.mjs +0 -7
- package/templates/web/public/favicon.ico +0 -0
- package/templates/web/src/app/boards/[boardId]/page.tsx +0 -353
- package/templates/web/src/app/globals.css +0 -123
- package/templates/web/src/app/layout.tsx +0 -31
- package/templates/web/src/app/lineage/[generationId]/page.tsx +0 -235
- package/templates/web/src/app/page.tsx +0 -35
- package/templates/web/src/app/providers.tsx +0 -18
- package/templates/web/src/components/boards/ArtifactInputSlots.tsx +0 -206
- package/templates/web/src/components/boards/ArtifactPreview.tsx +0 -466
- package/templates/web/src/components/boards/GenerationGrid.tsx +0 -282
- package/templates/web/src/components/boards/GenerationInput.tsx +0 -370
- package/templates/web/src/components/boards/GeneratorSelector.tsx +0 -272
- package/templates/web/src/components/boards/UploadArtifact.tsx +0 -563
- package/templates/web/src/components/header.tsx +0 -32
- package/templates/web/src/components/theme-provider.tsx +0 -10
- package/templates/web/src/components/theme-toggle.tsx +0 -75
- package/templates/web/src/components/ui/alert-dialog.tsx +0 -157
- package/templates/web/src/components/ui/button.tsx +0 -58
- package/templates/web/src/components/ui/card.tsx +0 -92
- package/templates/web/src/components/ui/dropdown-menu.tsx +0 -200
- package/templates/web/src/components/ui/navigation-menu.tsx +0 -168
- package/templates/web/src/components/ui/toast.tsx +0 -128
- package/templates/web/src/components/ui/toaster.tsx +0 -35
- package/templates/web/src/components/ui/use-toast.ts +0 -187
- package/templates/web/src/hooks/useGeneratorMRU.ts +0 -57
- package/templates/web/src/lib/utils.ts +0 -6
- package/templates/web/tsconfig.json +0 -41
package/templates/api/src/boards/generators/implementations/fal/video/wan_pro_image_to_video.py
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
WAN-Pro 2.1 image-to-video generator.
|
|
3
|
-
|
|
4
|
-
A premium image-to-video model that generates high-quality 1080p videos at 30fps
|
|
5
|
-
with up to 6 seconds duration, converting static images into dynamic video content
|
|
6
|
-
with exceptional motion diversity.
|
|
7
|
-
|
|
8
|
-
Based on Fal AI's fal-ai/wan-pro/image-to-video model.
|
|
9
|
-
See: https://fal.ai/models/fal-ai/wan-pro/image-to-video
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
import os
|
|
13
|
-
|
|
14
|
-
from pydantic import BaseModel, Field
|
|
15
|
-
|
|
16
|
-
from ....artifacts import ImageArtifact
|
|
17
|
-
from ....base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class WanProImageToVideoInput(BaseModel):
|
|
21
|
-
"""Input schema for WAN-Pro 2.1 image-to-video generation.
|
|
22
|
-
|
|
23
|
-
Artifact fields (image) are automatically detected via type introspection
|
|
24
|
-
and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
image: ImageArtifact = Field(description="The image to generate the video from")
|
|
28
|
-
prompt: str = Field(description="Text prompt describing the desired video content and motion")
|
|
29
|
-
seed: int | None = Field(
|
|
30
|
-
default=None,
|
|
31
|
-
description="Random seed for reproducibility. If not specified, a random seed will be used",
|
|
32
|
-
)
|
|
33
|
-
enable_safety_checker: bool = Field(
|
|
34
|
-
default=True,
|
|
35
|
-
description="Whether to enable the safety checker for content moderation",
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
class FalWanProImageToVideoGenerator(BaseGenerator):
|
|
40
|
-
"""Generator for creating videos from static images using WAN-Pro 2.1."""
|
|
41
|
-
|
|
42
|
-
name = "fal-wan-pro-image-to-video"
|
|
43
|
-
description = "Fal: WAN-Pro 2.1 - Generate high-quality 1080p videos from static images"
|
|
44
|
-
artifact_type = "video"
|
|
45
|
-
|
|
46
|
-
def get_input_schema(self) -> type[WanProImageToVideoInput]:
|
|
47
|
-
"""Return the input schema for this generator."""
|
|
48
|
-
return WanProImageToVideoInput
|
|
49
|
-
|
|
50
|
-
async def generate(
|
|
51
|
-
self, inputs: WanProImageToVideoInput, context: GeneratorExecutionContext
|
|
52
|
-
) -> GeneratorResult:
|
|
53
|
-
"""Generate video using fal.ai wan-pro/image-to-video."""
|
|
54
|
-
# Check for API key
|
|
55
|
-
if not os.getenv("FAL_KEY"):
|
|
56
|
-
raise ValueError("API configuration invalid. Missing FAL_KEY environment variable")
|
|
57
|
-
|
|
58
|
-
# Import fal_client
|
|
59
|
-
try:
|
|
60
|
-
import fal_client
|
|
61
|
-
except ImportError as e:
|
|
62
|
-
raise ImportError(
|
|
63
|
-
"fal.ai SDK is required for FalWanProImageToVideoGenerator. "
|
|
64
|
-
"Install with: pip install weirdfingers-boards[generators-fal]"
|
|
65
|
-
) from e
|
|
66
|
-
|
|
67
|
-
# Upload image artifact to Fal's public storage
|
|
68
|
-
# Fal API requires publicly accessible URLs, but our storage_url might be:
|
|
69
|
-
# - Localhost URLs (not publicly accessible)
|
|
70
|
-
# - Private S3 buckets (not publicly accessible)
|
|
71
|
-
# So we upload to Fal's temporary storage first
|
|
72
|
-
from ..utils import upload_artifacts_to_fal
|
|
73
|
-
|
|
74
|
-
image_urls = await upload_artifacts_to_fal([inputs.image], context)
|
|
75
|
-
|
|
76
|
-
# Prepare arguments for fal.ai API
|
|
77
|
-
arguments = {
|
|
78
|
-
"image_url": image_urls[0],
|
|
79
|
-
"prompt": inputs.prompt,
|
|
80
|
-
"enable_safety_checker": inputs.enable_safety_checker,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
# Only add seed if provided (allow API to use random if not specified)
|
|
84
|
-
if inputs.seed is not None:
|
|
85
|
-
arguments["seed"] = inputs.seed
|
|
86
|
-
|
|
87
|
-
# Submit async job
|
|
88
|
-
handler = await fal_client.submit_async(
|
|
89
|
-
"fal-ai/wan-pro/image-to-video",
|
|
90
|
-
arguments=arguments,
|
|
91
|
-
)
|
|
92
|
-
|
|
93
|
-
# Store external job ID
|
|
94
|
-
await context.set_external_job_id(handler.request_id)
|
|
95
|
-
|
|
96
|
-
# Stream progress updates
|
|
97
|
-
from .....progress.models import ProgressUpdate
|
|
98
|
-
|
|
99
|
-
event_count = 0
|
|
100
|
-
async for event in handler.iter_events(with_logs=True):
|
|
101
|
-
event_count += 1
|
|
102
|
-
# Sample every 3rd event to avoid spam
|
|
103
|
-
if event_count % 3 == 0:
|
|
104
|
-
# Extract logs if available
|
|
105
|
-
logs = getattr(event, "logs", None)
|
|
106
|
-
if logs:
|
|
107
|
-
# Join log entries into a single message
|
|
108
|
-
if isinstance(logs, list):
|
|
109
|
-
message = " | ".join(str(log) for log in logs if log)
|
|
110
|
-
else:
|
|
111
|
-
message = str(logs)
|
|
112
|
-
|
|
113
|
-
if message:
|
|
114
|
-
await context.publish_progress(
|
|
115
|
-
ProgressUpdate(
|
|
116
|
-
job_id=handler.request_id,
|
|
117
|
-
status="processing",
|
|
118
|
-
progress=50.0,
|
|
119
|
-
phase="processing",
|
|
120
|
-
message=message,
|
|
121
|
-
)
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
# Get final result
|
|
125
|
-
result = await handler.get()
|
|
126
|
-
|
|
127
|
-
# Extract video from result
|
|
128
|
-
# Expected structure: {"video": {"url": "...", "content_type": "...", ...}}
|
|
129
|
-
video_data = result.get("video")
|
|
130
|
-
if not video_data:
|
|
131
|
-
raise ValueError("No video returned from fal.ai API")
|
|
132
|
-
|
|
133
|
-
video_url = video_data.get("url")
|
|
134
|
-
if not video_url:
|
|
135
|
-
raise ValueError("Video missing URL in fal.ai response")
|
|
136
|
-
|
|
137
|
-
# Store video result
|
|
138
|
-
# WAN-Pro generates 1080p videos at 30fps with up to 6 seconds duration
|
|
139
|
-
artifact = await context.store_video_result(
|
|
140
|
-
storage_url=video_url,
|
|
141
|
-
format="mp4",
|
|
142
|
-
width=1920,
|
|
143
|
-
height=1080,
|
|
144
|
-
duration=6, # Maximum duration
|
|
145
|
-
fps=30,
|
|
146
|
-
output_index=0,
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
return GeneratorResult(outputs=[artifact])
|
|
150
|
-
|
|
151
|
-
async def estimate_cost(self, inputs: WanProImageToVideoInput) -> float:
|
|
152
|
-
"""Estimate cost for this generation in USD.
|
|
153
|
-
|
|
154
|
-
Note: Pricing information not available in Fal documentation.
|
|
155
|
-
Using placeholder value that should be updated with actual pricing.
|
|
156
|
-
"""
|
|
157
|
-
# TODO: Update with actual pricing from Fal when available
|
|
158
|
-
return 0.10 # Placeholder estimate for premium image-to-video generation
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
"""Kie.ai generator implementations."""
|
|
2
|
-
|
|
3
|
-
from .image.nano_banana_edit import KieNanoBananaEditGenerator, NanoBananaEditInput
|
|
4
|
-
from .video.veo3 import KieVeo3Generator, KieVeo3Input
|
|
5
|
-
|
|
6
|
-
__all__ = [
|
|
7
|
-
"KieNanoBananaEditGenerator",
|
|
8
|
-
"NanoBananaEditInput",
|
|
9
|
-
"KieVeo3Generator",
|
|
10
|
-
"KieVeo3Input",
|
|
11
|
-
]
|
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
"""Base classes for Kie.ai generators.
|
|
2
|
-
|
|
3
|
-
Provides common functionality shared across all Kie.ai generator implementations,
|
|
4
|
-
including API key validation, HTTP client setup, response validation, and polling logic.
|
|
5
|
-
|
|
6
|
-
Kie.ai supports two API patterns:
|
|
7
|
-
- Market API: Unified endpoint for 30+ models using /api/v1/jobs endpoints
|
|
8
|
-
- Dedicated API: Model-specific endpoints with custom paths
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import asyncio
|
|
12
|
-
import os
|
|
13
|
-
from abc import abstractmethod
|
|
14
|
-
from typing import Any, ClassVar, Literal
|
|
15
|
-
|
|
16
|
-
import httpx
|
|
17
|
-
|
|
18
|
-
from ....progress.models import ProgressUpdate
|
|
19
|
-
from ...base import BaseGenerator, GeneratorExecutionContext
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class KieBaseGenerator(BaseGenerator):
|
|
23
|
-
"""Base class for all Kie.ai generators with common functionality.
|
|
24
|
-
|
|
25
|
-
Provides shared methods for API key management, HTTP requests,
|
|
26
|
-
response validation, and external job ID storage.
|
|
27
|
-
|
|
28
|
-
Subclasses must define:
|
|
29
|
-
- api_pattern: Either "market" or "dedicated"
|
|
30
|
-
- model_id: Model identifier (for market) or endpoint path (for dedicated)
|
|
31
|
-
"""
|
|
32
|
-
|
|
33
|
-
# Subclasses must define these
|
|
34
|
-
api_pattern: ClassVar[Literal["market", "dedicated"]]
|
|
35
|
-
model_id: str
|
|
36
|
-
|
|
37
|
-
def _get_api_key(self) -> str:
|
|
38
|
-
"""Get and validate KIE_API_KEY from environment.
|
|
39
|
-
|
|
40
|
-
Returns:
|
|
41
|
-
The API key string
|
|
42
|
-
|
|
43
|
-
Raises:
|
|
44
|
-
ValueError: If KIE_API_KEY is not set
|
|
45
|
-
"""
|
|
46
|
-
api_key = os.getenv("KIE_API_KEY")
|
|
47
|
-
if not api_key:
|
|
48
|
-
raise ValueError("API configuration invalid. Missing KIE_API_KEY environment variable")
|
|
49
|
-
return api_key
|
|
50
|
-
|
|
51
|
-
def _validate_response(self, response: dict[str, Any]) -> None:
|
|
52
|
-
"""Validate standard Kie.ai response structure.
|
|
53
|
-
|
|
54
|
-
All Kie.ai APIs return responses with a "code" field where 200 indicates success.
|
|
55
|
-
|
|
56
|
-
Args:
|
|
57
|
-
response: The JSON response from Kie.ai API
|
|
58
|
-
|
|
59
|
-
Raises:
|
|
60
|
-
ValueError: If the response code is not 200
|
|
61
|
-
"""
|
|
62
|
-
if response.get("code") != 200:
|
|
63
|
-
error_msg = response.get("msg", "Unknown error")
|
|
64
|
-
raise ValueError(f"Kie.ai API error: {error_msg}")
|
|
65
|
-
|
|
66
|
-
async def _make_request(
|
|
67
|
-
self,
|
|
68
|
-
url: str,
|
|
69
|
-
method: Literal["GET", "POST"],
|
|
70
|
-
api_key: str,
|
|
71
|
-
json: dict[str, Any] | None = None,
|
|
72
|
-
timeout: float = 30.0,
|
|
73
|
-
) -> dict[str, Any]:
|
|
74
|
-
"""Make HTTP request to Kie.ai API with standard error handling.
|
|
75
|
-
|
|
76
|
-
Args:
|
|
77
|
-
url: Full URL to request
|
|
78
|
-
method: HTTP method (GET or POST)
|
|
79
|
-
api_key: API key for authorization
|
|
80
|
-
json: Request body for POST requests
|
|
81
|
-
timeout: Request timeout in seconds
|
|
82
|
-
|
|
83
|
-
Returns:
|
|
84
|
-
The validated JSON response
|
|
85
|
-
|
|
86
|
-
Raises:
|
|
87
|
-
ValueError: If the request fails or returns an error response
|
|
88
|
-
"""
|
|
89
|
-
async with httpx.AsyncClient() as client:
|
|
90
|
-
if method == "POST":
|
|
91
|
-
response = await client.post(
|
|
92
|
-
url,
|
|
93
|
-
json=json,
|
|
94
|
-
headers={
|
|
95
|
-
"Authorization": f"Bearer {api_key}",
|
|
96
|
-
"Content-Type": "application/json",
|
|
97
|
-
},
|
|
98
|
-
timeout=timeout,
|
|
99
|
-
)
|
|
100
|
-
else:
|
|
101
|
-
response = await client.get(
|
|
102
|
-
url,
|
|
103
|
-
headers={"Authorization": f"Bearer {api_key}"},
|
|
104
|
-
timeout=timeout,
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
if response.status_code != 200:
|
|
108
|
-
raise ValueError(
|
|
109
|
-
f"Kie.ai API request failed: {response.status_code} {response.text}"
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
result = response.json()
|
|
113
|
-
self._validate_response(result)
|
|
114
|
-
return result
|
|
115
|
-
|
|
116
|
-
@abstractmethod
|
|
117
|
-
async def _poll_for_completion(
|
|
118
|
-
self,
|
|
119
|
-
task_id: str,
|
|
120
|
-
api_key: str,
|
|
121
|
-
context: GeneratorExecutionContext,
|
|
122
|
-
) -> dict[str, Any]:
|
|
123
|
-
"""Poll for task completion.
|
|
124
|
-
|
|
125
|
-
Subclasses implement this based on their API pattern (Market vs Dedicated).
|
|
126
|
-
|
|
127
|
-
Args:
|
|
128
|
-
task_id: The task ID to poll
|
|
129
|
-
api_key: API key for authorization
|
|
130
|
-
context: Generator execution context for progress updates
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
The completed task data containing results
|
|
134
|
-
|
|
135
|
-
Raises:
|
|
136
|
-
ValueError: If polling fails or task fails
|
|
137
|
-
"""
|
|
138
|
-
pass
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
class KieMarketAPIGenerator(KieBaseGenerator):
|
|
142
|
-
"""Base class for Kie.ai Market API generators.
|
|
143
|
-
|
|
144
|
-
Market API is used for 30+ models through a unified endpoint.
|
|
145
|
-
- Submit: POST /api/v1/jobs/createTask with model parameter
|
|
146
|
-
- Status: GET /api/v1/jobs/recordInfo?taskId={id}
|
|
147
|
-
- Status field: "state" with values: "waiting", "pending", "processing", "success", "failed"
|
|
148
|
-
"""
|
|
149
|
-
|
|
150
|
-
api_pattern: ClassVar[Literal["market"]] = "market"
|
|
151
|
-
|
|
152
|
-
async def _poll_for_completion(
|
|
153
|
-
self,
|
|
154
|
-
task_id: str,
|
|
155
|
-
api_key: str,
|
|
156
|
-
context: GeneratorExecutionContext,
|
|
157
|
-
max_polls: int = 120,
|
|
158
|
-
poll_interval: int = 10,
|
|
159
|
-
) -> dict[str, Any]:
|
|
160
|
-
"""Poll Market API for task completion using state field.
|
|
161
|
-
|
|
162
|
-
Args:
|
|
163
|
-
task_id: The task ID to poll
|
|
164
|
-
api_key: API key for authorization
|
|
165
|
-
context: Generator execution context for progress updates
|
|
166
|
-
max_polls: Maximum number of polling attempts (default: 120 = 20 minutes)
|
|
167
|
-
poll_interval: Seconds between polls (default: 10)
|
|
168
|
-
|
|
169
|
-
Returns:
|
|
170
|
-
The completed task data from the "data" field
|
|
171
|
-
|
|
172
|
-
Raises:
|
|
173
|
-
ValueError: If task fails or times out
|
|
174
|
-
"""
|
|
175
|
-
status_url = f"https://api.kie.ai/api/v1/jobs/recordInfo?taskId={task_id}"
|
|
176
|
-
|
|
177
|
-
async with httpx.AsyncClient() as client:
|
|
178
|
-
for poll_count in range(max_polls):
|
|
179
|
-
# Don't sleep on first poll - check status immediately
|
|
180
|
-
if poll_count > 0:
|
|
181
|
-
await asyncio.sleep(poll_interval)
|
|
182
|
-
|
|
183
|
-
status_response = await client.get(
|
|
184
|
-
status_url,
|
|
185
|
-
headers={"Authorization": f"Bearer {api_key}"},
|
|
186
|
-
timeout=30.0,
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
if status_response.status_code != 200:
|
|
190
|
-
raise ValueError(
|
|
191
|
-
f"Status check failed: {status_response.status_code} {status_response.text}"
|
|
192
|
-
)
|
|
193
|
-
|
|
194
|
-
status_result = status_response.json()
|
|
195
|
-
self._validate_response(status_result)
|
|
196
|
-
|
|
197
|
-
task_data = status_result.get("data", {})
|
|
198
|
-
state = task_data.get("state")
|
|
199
|
-
|
|
200
|
-
if state == "success":
|
|
201
|
-
return task_data
|
|
202
|
-
elif state == "failed":
|
|
203
|
-
error_msg = task_data.get("failMsg", "Unknown error")
|
|
204
|
-
raise ValueError(f"Generation failed: {error_msg}")
|
|
205
|
-
elif state not in ["waiting", "pending", "processing", None]:
|
|
206
|
-
raise ValueError(
|
|
207
|
-
f"Unknown state '{state}' from Kie.ai API. Full response: {status_result}"
|
|
208
|
-
)
|
|
209
|
-
|
|
210
|
-
# Publish progress
|
|
211
|
-
progress = min(90, (poll_count / max_polls) * 100)
|
|
212
|
-
await context.publish_progress(
|
|
213
|
-
ProgressUpdate(
|
|
214
|
-
job_id=task_id,
|
|
215
|
-
status="processing",
|
|
216
|
-
progress=progress,
|
|
217
|
-
phase="processing",
|
|
218
|
-
)
|
|
219
|
-
)
|
|
220
|
-
else:
|
|
221
|
-
timeout_minutes = (max_polls * poll_interval) / 60
|
|
222
|
-
raise ValueError(f"Generation timed out after {timeout_minutes} minutes")
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
class KieDedicatedAPIGenerator(KieBaseGenerator):
|
|
226
|
-
"""Base class for Kie.ai Dedicated API generators.
|
|
227
|
-
|
|
228
|
-
Dedicated APIs have model-specific endpoints with custom paths.
|
|
229
|
-
- Submit: POST /api/v1/{model}/generate (no model parameter in body)
|
|
230
|
-
- Status: GET /api/v1/{model}/record-info?taskId={id}
|
|
231
|
-
- Status field: "successFlag" with values: 0 (processing), 1 (success), 2/3 (failed)
|
|
232
|
-
"""
|
|
233
|
-
|
|
234
|
-
api_pattern: ClassVar[Literal["dedicated"]] = "dedicated"
|
|
235
|
-
|
|
236
|
-
@abstractmethod
|
|
237
|
-
def _get_status_url(self, task_id: str) -> str:
|
|
238
|
-
"""Get the status check URL for this specific dedicated API.
|
|
239
|
-
|
|
240
|
-
Each dedicated API has its own status endpoint path.
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
task_id: The task ID to check status for
|
|
244
|
-
|
|
245
|
-
Returns:
|
|
246
|
-
Full URL for status checking
|
|
247
|
-
"""
|
|
248
|
-
pass
|
|
249
|
-
|
|
250
|
-
async def _poll_for_completion(
|
|
251
|
-
self,
|
|
252
|
-
task_id: str,
|
|
253
|
-
api_key: str,
|
|
254
|
-
context: GeneratorExecutionContext,
|
|
255
|
-
max_polls: int = 180,
|
|
256
|
-
poll_interval: int = 10,
|
|
257
|
-
) -> dict[str, Any]:
|
|
258
|
-
"""Poll Dedicated API for task completion using successFlag field.
|
|
259
|
-
|
|
260
|
-
Args:
|
|
261
|
-
task_id: The task ID to poll
|
|
262
|
-
api_key: API key for authorization
|
|
263
|
-
context: Generator execution context for progress updates
|
|
264
|
-
max_polls: Maximum number of polling attempts (default: 180 = 30 minutes)
|
|
265
|
-
poll_interval: Seconds between polls (default: 10)
|
|
266
|
-
|
|
267
|
-
Returns:
|
|
268
|
-
The completed task data from the "data" field
|
|
269
|
-
|
|
270
|
-
Raises:
|
|
271
|
-
ValueError: If task fails or times out
|
|
272
|
-
"""
|
|
273
|
-
status_url = self._get_status_url(task_id)
|
|
274
|
-
|
|
275
|
-
async with httpx.AsyncClient() as client:
|
|
276
|
-
for poll_count in range(max_polls):
|
|
277
|
-
# Don't sleep on first poll - check status immediately
|
|
278
|
-
if poll_count > 0:
|
|
279
|
-
await asyncio.sleep(poll_interval)
|
|
280
|
-
|
|
281
|
-
status_response = await client.get(
|
|
282
|
-
status_url,
|
|
283
|
-
headers={"Authorization": f"Bearer {api_key}"},
|
|
284
|
-
timeout=30.0,
|
|
285
|
-
)
|
|
286
|
-
|
|
287
|
-
if status_response.status_code != 200:
|
|
288
|
-
raise ValueError(
|
|
289
|
-
f"Status check failed: {status_response.status_code} {status_response.text}"
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
status_result = status_response.json()
|
|
293
|
-
self._validate_response(status_result)
|
|
294
|
-
|
|
295
|
-
task_data = status_result.get("data", {})
|
|
296
|
-
success_flag = task_data.get("successFlag")
|
|
297
|
-
|
|
298
|
-
if success_flag == 1:
|
|
299
|
-
return task_data
|
|
300
|
-
elif success_flag in [2, 3]:
|
|
301
|
-
error_msg = task_data.get("errorMsg", "Unknown error")
|
|
302
|
-
raise ValueError(f"Generation failed: {error_msg}")
|
|
303
|
-
|
|
304
|
-
# Publish progress
|
|
305
|
-
progress = min(90, (poll_count / max_polls) * 100)
|
|
306
|
-
await context.publish_progress(
|
|
307
|
-
ProgressUpdate(
|
|
308
|
-
job_id=task_id,
|
|
309
|
-
status="processing",
|
|
310
|
-
progress=progress,
|
|
311
|
-
phase="processing",
|
|
312
|
-
)
|
|
313
|
-
)
|
|
314
|
-
else:
|
|
315
|
-
timeout_minutes = (max_polls * poll_interval) / 60
|
|
316
|
-
raise ValueError(f"Generation timed out after {timeout_minutes} minutes")
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Kie.ai nano-banana image-to-image editing generator.
|
|
3
|
-
|
|
4
|
-
Edit images using Kie.ai's google/nano-banana-edit model (powered by Google Gemini).
|
|
5
|
-
Supports editing multiple input images with a text prompt.
|
|
6
|
-
|
|
7
|
-
Based on Kie.ai's google/nano-banana-edit model.
|
|
8
|
-
See: https://docs.kie.ai/market/google/nano-banana-edit
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
from typing import Literal
|
|
12
|
-
|
|
13
|
-
from pydantic import BaseModel, Field
|
|
14
|
-
|
|
15
|
-
from ....artifacts import ImageArtifact
|
|
16
|
-
from ....base import GeneratorExecutionContext, GeneratorResult
|
|
17
|
-
from ..base import KieMarketAPIGenerator
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class NanoBananaEditInput(BaseModel):
|
|
21
|
-
"""Input schema for nano-banana image editing.
|
|
22
|
-
|
|
23
|
-
Artifact fields (like image_sources) are automatically detected via type
|
|
24
|
-
introspection and resolved from generation IDs to ImageArtifact objects.
|
|
25
|
-
"""
|
|
26
|
-
|
|
27
|
-
prompt: str = Field(
|
|
28
|
-
description="The prompt for image editing",
|
|
29
|
-
max_length=5000,
|
|
30
|
-
)
|
|
31
|
-
image_sources: list[ImageArtifact] = Field(
|
|
32
|
-
description="List of input images for editing (from previous generations)",
|
|
33
|
-
min_length=1,
|
|
34
|
-
max_length=10,
|
|
35
|
-
)
|
|
36
|
-
output_format: Literal["png", "jpeg"] = Field(
|
|
37
|
-
default="png",
|
|
38
|
-
description="Output image format",
|
|
39
|
-
)
|
|
40
|
-
image_size: Literal[
|
|
41
|
-
"1:1",
|
|
42
|
-
"9:16",
|
|
43
|
-
"16:9",
|
|
44
|
-
"3:4",
|
|
45
|
-
"4:3",
|
|
46
|
-
"3:2",
|
|
47
|
-
"2:3",
|
|
48
|
-
"5:4",
|
|
49
|
-
"4:5",
|
|
50
|
-
"21:9",
|
|
51
|
-
"auto",
|
|
52
|
-
] = Field(
|
|
53
|
-
default="1:1",
|
|
54
|
-
description="Output image aspect ratio",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class KieNanoBananaEditGenerator(KieMarketAPIGenerator):
|
|
59
|
-
"""nano-banana image editing generator using Kie.ai Market API."""
|
|
60
|
-
|
|
61
|
-
name = "kie-nano-banana-edit"
|
|
62
|
-
artifact_type = "image"
|
|
63
|
-
description = "Kie.ai: Google nano-banana edit - AI-powered image editing with Gemini"
|
|
64
|
-
|
|
65
|
-
# Market API configuration
|
|
66
|
-
model_id = "google/nano-banana-edit"
|
|
67
|
-
|
|
68
|
-
def get_input_schema(self) -> type[NanoBananaEditInput]:
|
|
69
|
-
return NanoBananaEditInput
|
|
70
|
-
|
|
71
|
-
async def generate(
|
|
72
|
-
self, inputs: NanoBananaEditInput, context: GeneratorExecutionContext
|
|
73
|
-
) -> GeneratorResult:
|
|
74
|
-
"""Edit images using Kie.ai google/nano-banana-edit model."""
|
|
75
|
-
# Get API key using base class method
|
|
76
|
-
api_key = self._get_api_key()
|
|
77
|
-
|
|
78
|
-
# Upload image artifacts to Kie.ai's public storage
|
|
79
|
-
# Kie.ai API requires publicly accessible URLs, but our storage_url might be:
|
|
80
|
-
# - Localhost URLs (not publicly accessible)
|
|
81
|
-
# - Private S3 buckets (not publicly accessible)
|
|
82
|
-
# So we upload to Kie.ai's temporary storage first
|
|
83
|
-
from ..utils import upload_artifacts_to_kie
|
|
84
|
-
|
|
85
|
-
image_urls = await upload_artifacts_to_kie(inputs.image_sources, context)
|
|
86
|
-
|
|
87
|
-
# Prepare request body for Market API
|
|
88
|
-
body = {
|
|
89
|
-
"model": self.model_id,
|
|
90
|
-
"input": {
|
|
91
|
-
"prompt": inputs.prompt,
|
|
92
|
-
"image_urls": image_urls,
|
|
93
|
-
"output_format": inputs.output_format,
|
|
94
|
-
"image_size": inputs.image_size,
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
# Submit task using base class method
|
|
99
|
-
submit_url = "https://api.kie.ai/api/v1/jobs/createTask"
|
|
100
|
-
result = await self._make_request(submit_url, "POST", api_key, json=body)
|
|
101
|
-
|
|
102
|
-
# Extract task ID with safe dictionary access
|
|
103
|
-
data = result.get("data", {})
|
|
104
|
-
task_id = data.get("taskId")
|
|
105
|
-
|
|
106
|
-
if not task_id:
|
|
107
|
-
raise ValueError(f"No taskId returned from Kie.ai API. Response: {result}")
|
|
108
|
-
|
|
109
|
-
# Store external job ID
|
|
110
|
-
await context.set_external_job_id(task_id)
|
|
111
|
-
|
|
112
|
-
# Poll for completion using base class method
|
|
113
|
-
task_data = await self._poll_for_completion(task_id, api_key, context)
|
|
114
|
-
|
|
115
|
-
# Extract outputs from resultJson
|
|
116
|
-
result_json = task_data.get("resultJson")
|
|
117
|
-
if result_json:
|
|
118
|
-
import json
|
|
119
|
-
|
|
120
|
-
result_data = json.loads(result_json)
|
|
121
|
-
else:
|
|
122
|
-
result_data = task_data.get("result")
|
|
123
|
-
|
|
124
|
-
if not result_data:
|
|
125
|
-
raise ValueError("No result data returned from Kie.ai API")
|
|
126
|
-
|
|
127
|
-
# Extract image URLs from result
|
|
128
|
-
# The response structure may vary, but typically contains image URLs
|
|
129
|
-
# Based on the API pattern, result should contain the generated images
|
|
130
|
-
images = result_data.get("images", [])
|
|
131
|
-
|
|
132
|
-
if not images:
|
|
133
|
-
# Sometimes the result might directly contain URLs in different structure
|
|
134
|
-
# Try to extract from common patterns
|
|
135
|
-
if isinstance(result_data, dict):
|
|
136
|
-
# Check for common response patterns
|
|
137
|
-
if "resultUrls" in result_data:
|
|
138
|
-
# Market API returns resultUrls as array of strings
|
|
139
|
-
images = [{"url": url} for url in result_data["resultUrls"]]
|
|
140
|
-
elif "image_urls" in result_data:
|
|
141
|
-
images = [{"url": url} for url in result_data["image_urls"]]
|
|
142
|
-
elif "url" in result_data:
|
|
143
|
-
images = [{"url": result_data["url"]}]
|
|
144
|
-
else:
|
|
145
|
-
raise ValueError(f"No images found in result: {result_data}")
|
|
146
|
-
else:
|
|
147
|
-
raise ValueError("No images returned from Kie.ai API")
|
|
148
|
-
|
|
149
|
-
# Store each image using output_index
|
|
150
|
-
artifacts = []
|
|
151
|
-
for idx, image_data in enumerate(images):
|
|
152
|
-
if isinstance(image_data, str):
|
|
153
|
-
image_url = image_data
|
|
154
|
-
width = 1024
|
|
155
|
-
height = 1024
|
|
156
|
-
elif isinstance(image_data, dict):
|
|
157
|
-
image_url = image_data.get("url")
|
|
158
|
-
# Extract dimensions if available, otherwise use sensible defaults
|
|
159
|
-
width = image_data.get("width", 1024)
|
|
160
|
-
height = image_data.get("height", 1024)
|
|
161
|
-
else:
|
|
162
|
-
raise ValueError(f"Unexpected image data format: {type(image_data)}")
|
|
163
|
-
|
|
164
|
-
if not image_url:
|
|
165
|
-
raise ValueError(f"Image {idx} missing URL in Kie.ai response")
|
|
166
|
-
|
|
167
|
-
# Store with appropriate output_index
|
|
168
|
-
artifact = await context.store_image_result(
|
|
169
|
-
storage_url=image_url,
|
|
170
|
-
format=inputs.output_format,
|
|
171
|
-
width=width,
|
|
172
|
-
height=height,
|
|
173
|
-
output_index=idx,
|
|
174
|
-
)
|
|
175
|
-
artifacts.append(artifact)
|
|
176
|
-
|
|
177
|
-
return GeneratorResult(outputs=artifacts)
|
|
178
|
-
|
|
179
|
-
async def estimate_cost(self, inputs: NanoBananaEditInput) -> float:
|
|
180
|
-
"""Estimate cost for nano-banana edit generation.
|
|
181
|
-
|
|
182
|
-
nano-banana/edit uses Google Gemini for image editing.
|
|
183
|
-
Estimated at $0.025 per image (approximately 35% cheaper than Fal's $0.039).
|
|
184
|
-
|
|
185
|
-
Note: Actual pricing should be verified at https://kie.ai/pricing
|
|
186
|
-
"""
|
|
187
|
-
# Cost per image - this is an estimate and should be updated with actual pricing
|
|
188
|
-
per_image_cost = 0.025
|
|
189
|
-
num_images = len(inputs.image_sources)
|
|
190
|
-
return per_image_cost * num_images
|