@weirdfingers/baseboards 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,140 @@
1
+ """
2
+ Base generator classes and interfaces for the Boards generators system.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Protocol, runtime_checkable
7
+
8
+ from pydantic import BaseModel
9
+
10
+ from ..progress.models import ProgressUpdate
11
+ from .artifacts import (
12
+ AudioArtifact,
13
+ DigitalArtifact,
14
+ ImageArtifact,
15
+ TextArtifact,
16
+ VideoArtifact,
17
+ )
18
+
19
+
20
+ class GeneratorResult(BaseModel):
21
+ """All generators return a list of urls to the artifacts they produce."""
22
+
23
+ outputs: list[DigitalArtifact]
24
+
25
+
26
+ class BaseGenerator(ABC):
27
+ """
28
+ Abstract base class for all generators in the Boards system.
29
+
30
+ Generators define input/output schemas using Pydantic models and implement
31
+ the generation logic using provider SDKs directly.
32
+ """
33
+
34
+ # Class attributes that subclasses must define
35
+ name: str
36
+ artifact_type: str # 'image', 'video', 'audio', 'text', 'lora'
37
+ description: str
38
+
39
+ @abstractmethod
40
+ def get_input_schema(self) -> type[BaseModel]:
41
+ """
42
+ Return the Pydantic model class that defines the input schema for this generator.
43
+
44
+ Returns:
45
+ Type[BaseModel]: Pydantic model class for input validation
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ async def generate(
51
+ self, inputs: BaseModel, context: "GeneratorExecutionContext"
52
+ ) -> GeneratorResult:
53
+ """
54
+ Execute the generation process using the provided inputs.
55
+
56
+ Args:
57
+ inputs: Validated input data matching the input schema
58
+
59
+ Returns:
60
+ BaseModel: Generated output matching the output schema
61
+ """
62
+ pass
63
+
64
+ @abstractmethod
65
+ async def estimate_cost(self, inputs: BaseModel) -> float:
66
+ """
67
+ Estimate the cost of running this generation in USD.
68
+
69
+ Args:
70
+ inputs: Input data for cost estimation
71
+
72
+ Returns:
73
+ float: Estimated cost in USD
74
+ """
75
+ pass
76
+
77
+ def __repr__(self) -> str:
78
+ return f"<{self.__class__.__name__}(name='{self.name}', type='{self.artifact_type}')>"
79
+
80
+
81
+ @runtime_checkable
82
+ class GeneratorExecutionContext(Protocol):
83
+ """Typed protocol for the execution context passed to generators.
84
+
85
+ This protocol defines the interface that generators can use to interact
86
+ with storage, database, and progress tracking systems.
87
+ """
88
+
89
+ async def resolve_artifact(self, artifact: DigitalArtifact) -> str:
90
+ """Resolve an artifact to a local file path for use with provider SDKs."""
91
+ ...
92
+
93
+ async def store_image_result(
94
+ self,
95
+ storage_url: str,
96
+ format: str,
97
+ width: int,
98
+ height: int,
99
+ ) -> ImageArtifact:
100
+ """Store an image result to permanent storage."""
101
+ ...
102
+
103
+ async def store_video_result(
104
+ self,
105
+ storage_url: str,
106
+ format: str,
107
+ width: int,
108
+ height: int,
109
+ duration: float | None = None,
110
+ fps: float | None = None,
111
+ ) -> VideoArtifact:
112
+ """Store a video result to permanent storage."""
113
+ ...
114
+
115
+ async def store_audio_result(
116
+ self,
117
+ storage_url: str,
118
+ format: str,
119
+ duration: float | None = None,
120
+ sample_rate: int | None = None,
121
+ channels: int | None = None,
122
+ ) -> AudioArtifact:
123
+ """Store an audio result to permanent storage."""
124
+ ...
125
+
126
+ async def store_text_result(
127
+ self,
128
+ content: str,
129
+ format: str,
130
+ ) -> TextArtifact:
131
+ """Store a text result to permanent storage."""
132
+ ...
133
+
134
+ async def publish_progress(self, update: ProgressUpdate) -> None:
135
+ """Publish a progress update for this generation."""
136
+ ...
137
+
138
+ async def set_external_job_id(self, external_id: str) -> None:
139
+ """Set the external job ID from the provider (e.g., Replicate prediction ID)."""
140
+ ...
@@ -0,0 +1,12 @@
1
+ """
2
+ Built-in generator implementations for the Boards system.
3
+
4
+ This package contains example generators that demonstrate how to integrate
5
+ various AI services using their native SDKs.
6
+
7
+ Import this module to automatically register all built-in generators:
8
+ import boards.generators.implementations
9
+ """
10
+
11
+ # Import all generator modules to trigger registration
12
+ from . import audio, image, video
@@ -0,0 +1,3 @@
1
+ """Audio generation implementations."""
2
+
3
+ from . import whisper
@@ -0,0 +1,66 @@
1
+ """
2
+ Whisper audio transcription using OpenAI API.
3
+
4
+ Demonstrates audio processing generator that outputs text.
5
+ """
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from ...artifacts import AudioArtifact
10
+ from ...base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
11
+
12
+
13
+ class WhisperInput(BaseModel):
14
+ """Input schema for Whisper transcription."""
15
+
16
+ audio_source: AudioArtifact = Field(description="Audio file to transcribe")
17
+ language: str = Field(default="en", description="Language code (e.g., 'en', 'es', 'fr')")
18
+ prompt: str = Field(default="", description="Optional prompt to guide transcription")
19
+
20
+
21
+ class WhisperGenerator(BaseGenerator):
22
+ """Whisper speech-to-text transcription using OpenAI API."""
23
+
24
+ name = "whisper"
25
+ artifact_type = "text"
26
+ description = "OpenAI Whisper - speech-to-text transcription"
27
+
28
+ def get_input_schema(self) -> type[WhisperInput]:
29
+ return WhisperInput
30
+
31
+ async def generate(
32
+ self, inputs: WhisperInput, context: GeneratorExecutionContext
33
+ ) -> GeneratorResult:
34
+ """Transcribe audio using OpenAI Whisper."""
35
+ try:
36
+ from openai import AsyncOpenAI
37
+ except ImportError as e:
38
+ raise ValueError("Required dependencies not available") from e
39
+
40
+ client = AsyncOpenAI()
41
+
42
+ # Resolve audio artifact to file path via context
43
+ audio_file_path = await context.resolve_artifact(inputs.audio_source)
44
+
45
+ # Use OpenAI SDK for transcription
46
+ with open(audio_file_path, "rb") as audio_file:
47
+ transcript = await client.audio.transcriptions.create(
48
+ model="whisper-1",
49
+ file=audio_file,
50
+ language=inputs.language,
51
+ prompt=inputs.prompt,
52
+ )
53
+
54
+ # Create text artifact
55
+ text_artifact = await context.store_text_result(
56
+ content=transcript.text,
57
+ format="plain",
58
+ )
59
+
60
+ return GeneratorResult(outputs=[text_artifact])
61
+
62
+ async def estimate_cost(self, inputs: WhisperInput) -> float:
63
+ """Estimate cost for Whisper transcription."""
64
+ # Whisper pricing is $0.006 per minute
65
+ duration_minutes = (inputs.audio_source.duration or 60) / 60
66
+ return duration_minutes * 0.006
@@ -0,0 +1,3 @@
1
+ """Image generation implementations."""
2
+
3
+ from . import dalle3, flux_pro
@@ -0,0 +1,93 @@
1
+ """
2
+ DALL-E 3 generator using OpenAI API.
3
+
4
+ Demonstrates integration with OpenAI's SDK for image generation.
5
+ """
6
+
7
+ import os
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from ...base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
12
+
13
+
14
+ class DallE3Input(BaseModel):
15
+ """Input schema for DALL-E 3 image generation."""
16
+
17
+ prompt: str = Field(description="Text prompt for image generation")
18
+ size: str = Field(
19
+ default="1024x1024",
20
+ description="Image size",
21
+ pattern="^(1024x1024|1024x1792|1792x1024)$",
22
+ )
23
+ quality: str = Field(default="standard", description="Image quality", pattern="^(standard|hd)$")
24
+ style: str = Field(default="vivid", description="Image style", pattern="^(vivid|natural)$")
25
+
26
+
27
+ class DallE3Generator(BaseGenerator):
28
+ """DALL-E 3 image generator using OpenAI API."""
29
+
30
+ name = "dall-e-3"
31
+ artifact_type = "image"
32
+ description = "OpenAI's DALL-E 3 - advanced text-to-image generation"
33
+
34
+ def get_input_schema(self) -> type[DallE3Input]:
35
+ return DallE3Input
36
+
37
+ async def generate(
38
+ self, inputs: DallE3Input, context: GeneratorExecutionContext
39
+ ) -> GeneratorResult:
40
+ """Generate image using OpenAI DALL-E 3."""
41
+ # Check for API key
42
+ if not os.getenv("OPENAI_API_KEY"):
43
+ raise ValueError("API configuration invalid")
44
+
45
+ # Import SDK directly
46
+ try:
47
+ from openai import AsyncOpenAI
48
+ except ImportError as e:
49
+ raise ValueError("Required dependencies not available") from e
50
+
51
+ client = AsyncOpenAI()
52
+
53
+ # Use OpenAI SDK directly
54
+ response = await client.images.generate(
55
+ model="dall-e-3",
56
+ prompt=inputs.prompt,
57
+ size=inputs.size, # pyright: ignore[reportArgumentType]
58
+ quality=inputs.quality, # pyright: ignore[reportArgumentType]
59
+ style=inputs.style, # pyright: ignore[reportArgumentType]
60
+ n=1,
61
+ )
62
+
63
+ # Get the generated image URL
64
+ if not response.data or not response.data[0].url:
65
+ raise ValueError("No image generated")
66
+ image_url = response.data[0].url
67
+
68
+ # Parse dimensions from size
69
+ width, height = map(int, inputs.size.split("x"))
70
+
71
+ # Store via context (downloads from OpenAI and uploads to our storage)
72
+ image_artifact = await context.store_image_result(
73
+ storage_url=image_url,
74
+ format="png", # DALL-E 3 outputs PNG
75
+ width=width,
76
+ height=height,
77
+ )
78
+
79
+ return GeneratorResult(outputs=[image_artifact])
80
+
81
+ async def estimate_cost(self, inputs: DallE3Input) -> float:
82
+ """Estimate cost for DALL-E 3 generation."""
83
+ # DALL-E 3 pricing varies by quality and size
84
+ if inputs.quality == "hd":
85
+ if inputs.size in ["1024x1792", "1792x1024"]:
86
+ return 0.080 # HD, non-square
87
+ else:
88
+ return 0.080 # HD, square
89
+ else: # standard quality
90
+ if inputs.size in ["1024x1792", "1792x1024"]:
91
+ return 0.040 # Standard, non-square
92
+ else:
93
+ return 0.040 # Standard, square
@@ -0,0 +1,85 @@
1
+ """
2
+ FLUX.1.1 Pro generator using Replicate API.
3
+
4
+ This demonstrates the simple pattern for creating generators:
5
+ 1. Define Pydantic input/output models
6
+ 2. Implement generation logic using provider SDK directly
7
+ 3. Register with the global registry
8
+ """
9
+
10
+ import os
11
+ from collections.abc import AsyncIterator
12
+
13
+ from pydantic import BaseModel, Field
14
+
15
+ from ...base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
16
+
17
+
18
+ class FluxProInput(BaseModel):
19
+ """Input schema for FLUX.1.1 Pro image generation."""
20
+
21
+ prompt: str = Field(description="Text prompt for image generation")
22
+ aspect_ratio: str = Field(
23
+ default="1:1",
24
+ description="Image aspect ratio",
25
+ pattern="^(1:1|16:9|21:9|2:3|3:2|4:5|5:4|9:16|9:21)$",
26
+ )
27
+ safety_tolerance: int = Field(default=2, ge=1, le=5, description="Safety tolerance level (1-5)")
28
+
29
+
30
+ class FluxProGenerator(BaseGenerator):
31
+ """FLUX.1.1 Pro image generator using Replicate."""
32
+
33
+ name = "flux-pro"
34
+ artifact_type = "image"
35
+ description = "FLUX.1.1 [pro] by Black Forest Labs - high-quality image generation"
36
+
37
+ def get_input_schema(self) -> type[FluxProInput]:
38
+ return FluxProInput
39
+
40
+ async def generate(
41
+ self, inputs: FluxProInput, context: GeneratorExecutionContext
42
+ ) -> GeneratorResult:
43
+ """Generate image using Replicate FLUX.1.1 Pro model."""
44
+ # Check for API key
45
+ if not os.getenv("REPLICATE_API_TOKEN"):
46
+ raise ValueError("API configuration invalid. Missing REPLICATE_API_TOKEN")
47
+
48
+ # Import SDK directly
49
+ try:
50
+ import replicate
51
+ from replicate.helpers import FileOutput
52
+ except ImportError as e:
53
+ raise ValueError("Required dependencies not available") from e
54
+
55
+ # Use Replicate SDK directly
56
+ prediction: FileOutput | AsyncIterator[FileOutput] = await replicate.async_run(
57
+ "black-forest-labs/flux-1.1-pro",
58
+ input={
59
+ "prompt": inputs.prompt,
60
+ "aspect_ratio": inputs.aspect_ratio,
61
+ "safety_tolerance": inputs.safety_tolerance,
62
+ },
63
+ )
64
+
65
+ # If prediction is an async iterator, get the first item; else, use as is.
66
+ if isinstance(prediction, AsyncIterator):
67
+ file_output = await anext(prediction)
68
+ else:
69
+ file_output = prediction
70
+
71
+ output_url = file_output.url
72
+
73
+ image_artifact = await context.store_image_result(
74
+ storage_url=output_url,
75
+ format="png",
76
+ width=1024,
77
+ height=1024,
78
+ )
79
+
80
+ return GeneratorResult(outputs=[image_artifact])
81
+
82
+ async def estimate_cost(self, inputs: FluxProInput) -> float:
83
+ """Estimate cost for FLUX.1.1 Pro generation."""
84
+ # FLUX.1.1 Pro typically costs around $0.055 per generation
85
+ return 0.055
@@ -0,0 +1,3 @@
1
+ """Video generation implementations."""
2
+
3
+ from . import lipsync
@@ -0,0 +1,70 @@
1
+ """
2
+ Lipsync generator using Replicate API.
3
+
4
+ This demonstrates how generators can use multiple artifact inputs
5
+ with automatic artifact resolution.
6
+ """
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from ...artifacts import AudioArtifact, VideoArtifact
11
+ from ...base import BaseGenerator, GeneratorExecutionContext, GeneratorResult
12
+
13
+
14
+ class LipsyncInput(BaseModel):
15
+ """Input schema for lipsync generation."""
16
+
17
+ audio_source: AudioArtifact = Field(description="Audio track for lip sync")
18
+ video_source: VideoArtifact = Field(description="Video to sync lips in")
19
+ prompt: str | None = Field(None, description="Optional prompt for generation")
20
+
21
+
22
+ class LipsyncGenerator(BaseGenerator):
23
+ """Lipsync generator that syncs lips in video to audio."""
24
+
25
+ name = "lipsync"
26
+ artifact_type = "video"
27
+ description = "Sync lips in video to match audio track"
28
+
29
+ def get_input_schema(self) -> type[LipsyncInput]:
30
+ return LipsyncInput
31
+
32
+ async def generate(
33
+ self, inputs: LipsyncInput, context: GeneratorExecutionContext
34
+ ) -> GeneratorResult:
35
+ """Generate lip-synced video."""
36
+ # Import SDK directly
37
+ try:
38
+ import replicate # type: ignore
39
+ except ImportError as e:
40
+ raise ValueError("Required dependencies not available") from e
41
+
42
+ # Resolve artifacts via context
43
+ audio_file = await context.resolve_artifact(inputs.audio_source)
44
+ video_file = await context.resolve_artifact(inputs.video_source)
45
+
46
+ # Use Replicate SDK directly with proper file handling
47
+ with open(audio_file, "rb") as audio_f, open(video_file, "rb") as video_f:
48
+ result = await replicate.async_run(
49
+ "cjwbw/wav2lip",
50
+ input={
51
+ "audio": audio_f,
52
+ "video": video_f,
53
+ },
54
+ )
55
+
56
+ # Store output and create artifact via context
57
+ video_artifact = await context.store_video_result(
58
+ storage_url=str(result),
59
+ format="mp4",
60
+ width=inputs.video_source.width,
61
+ height=inputs.video_source.height,
62
+ duration=inputs.audio_source.duration,
63
+ )
64
+
65
+ return GeneratorResult(outputs=[video_artifact])
66
+
67
+ async def estimate_cost(self, inputs: LipsyncInput) -> float:
68
+ """Estimate cost for lipsync generation."""
69
+ # Wav2Lip is typically free on Replicate, but let's add a small cost
70
+ return 0.01