genblaze-google 0.1.0__py3-none-any.whl
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.
- genblaze_google/__init__.py +6 -0
- genblaze_google/_errors.py +19 -0
- genblaze_google/imagen.py +162 -0
- genblaze_google/provider.py +276 -0
- genblaze_google/py.typed +0 -0
- genblaze_google-0.1.0.dist-info/METADATA +128 -0
- genblaze_google-0.1.0.dist-info/RECORD +9 -0
- genblaze_google-0.1.0.dist-info/WHEEL +4 -0
- genblaze_google-0.1.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Shared Google API error mapping — used by provider.py, imagen.py."""
|
|
2
|
+
|
|
3
|
+
from genblaze_core.models.enums import ProviderErrorCode
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def map_google_error(exc: Exception) -> ProviderErrorCode:
|
|
7
|
+
"""Map a Google API exception to a ProviderErrorCode."""
|
|
8
|
+
msg = str(exc).lower()
|
|
9
|
+
if "rate" in msg or "429" in msg or "resource_exhausted" in msg:
|
|
10
|
+
return ProviderErrorCode.RATE_LIMIT
|
|
11
|
+
if "auth" in msg or "401" in msg or "403" in msg or "permission" in msg:
|
|
12
|
+
return ProviderErrorCode.AUTH_FAILURE
|
|
13
|
+
if "invalid" in msg or "400" in msg:
|
|
14
|
+
return ProviderErrorCode.INVALID_INPUT
|
|
15
|
+
if "timeout" in msg or "deadline" in msg:
|
|
16
|
+
return ProviderErrorCode.TIMEOUT
|
|
17
|
+
if "500" in msg or "unavailable" in msg or "internal" in msg:
|
|
18
|
+
return ProviderErrorCode.SERVER_ERROR
|
|
19
|
+
return ProviderErrorCode.UNKNOWN
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""ImagenProvider — adapter for Google Imagen image generation.
|
|
2
|
+
|
|
3
|
+
Synchronous API: client.models.generate_images() returns images directly.
|
|
4
|
+
|
|
5
|
+
Docs: https://ai.google.dev/gemini-api/docs/image-generation
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import tempfile
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import quote
|
|
15
|
+
|
|
16
|
+
from genblaze_core.exceptions import ProviderError
|
|
17
|
+
from genblaze_core.models.asset import Asset
|
|
18
|
+
from genblaze_core.models.enums import Modality, ProviderErrorCode
|
|
19
|
+
from genblaze_core.models.step import Step
|
|
20
|
+
from genblaze_core.providers.base import ProviderCapabilities, SyncProvider
|
|
21
|
+
from genblaze_core.runnable.config import RunnableConfig
|
|
22
|
+
|
|
23
|
+
from genblaze_google._errors import map_google_error
|
|
24
|
+
|
|
25
|
+
_VALID_ASPECT_RATIOS = {"1:1", "3:4", "4:3", "9:16", "16:9"}
|
|
26
|
+
|
|
27
|
+
# Per-image pricing by model (USD)
|
|
28
|
+
_IMAGEN_PRICING: dict[str, float] = {
|
|
29
|
+
"imagen-3.0-generate-002": 0.04,
|
|
30
|
+
"imagen-3.0-fast-generate-001": 0.02,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImagenProvider(SyncProvider):
|
|
35
|
+
"""Provider adapter for Google Imagen image generation.
|
|
36
|
+
|
|
37
|
+
Models: ``imagen-3.0-generate-002`` (latest), ``imagen-3.0-fast-generate-001``.
|
|
38
|
+
|
|
39
|
+
Imagen returns image bytes directly (synchronous, not operation-based).
|
|
40
|
+
Output is saved to files; use ObjectStorageSink for cloud upload.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
api_key: Gemini API key. Falls back to GEMINI_API_KEY env var.
|
|
44
|
+
project: GCP project ID for Vertex AI auth.
|
|
45
|
+
location: GCP region for Vertex AI (default "us-central1").
|
|
46
|
+
output_dir: Directory for output image files (default system temp).
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
name = "google-imagen"
|
|
50
|
+
|
|
51
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
52
|
+
"""Imagen: image generation from text prompts."""
|
|
53
|
+
return ProviderCapabilities(
|
|
54
|
+
supported_modalities=[Modality.IMAGE],
|
|
55
|
+
supported_inputs=["text"],
|
|
56
|
+
models=["imagen-3.0-generate-002", "imagen-3.0-fast-generate-001"],
|
|
57
|
+
output_formats=["image/png", "image/jpeg"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
api_key: str | None = None,
|
|
63
|
+
*,
|
|
64
|
+
project: str | None = None,
|
|
65
|
+
location: str = "us-central1",
|
|
66
|
+
output_dir: str | Path | None = None,
|
|
67
|
+
):
|
|
68
|
+
super().__init__()
|
|
69
|
+
self._api_key = api_key
|
|
70
|
+
self._project = project
|
|
71
|
+
self._location = location
|
|
72
|
+
self._output_dir = Path(output_dir) if output_dir else None
|
|
73
|
+
self._client: Any = None
|
|
74
|
+
|
|
75
|
+
def _get_client(self):
|
|
76
|
+
if self._client is None:
|
|
77
|
+
try:
|
|
78
|
+
from google import genai
|
|
79
|
+
except ImportError as exc:
|
|
80
|
+
raise ProviderError(
|
|
81
|
+
"google-genai package not installed. Run: pip install google-genai"
|
|
82
|
+
) from exc
|
|
83
|
+
if self._project:
|
|
84
|
+
self._client = genai.Client(
|
|
85
|
+
vertexai=True, project=self._project, location=self._location
|
|
86
|
+
)
|
|
87
|
+
else:
|
|
88
|
+
kwargs: dict = {}
|
|
89
|
+
if self._api_key:
|
|
90
|
+
kwargs["api_key"] = self._api_key
|
|
91
|
+
self._client = genai.Client(**kwargs)
|
|
92
|
+
return self._client
|
|
93
|
+
|
|
94
|
+
def generate(self, step: Step, config: RunnableConfig | None = None) -> Step:
|
|
95
|
+
"""Generate image(s) via the Google Imagen API."""
|
|
96
|
+
client = self._get_client()
|
|
97
|
+
try:
|
|
98
|
+
from google.genai import types
|
|
99
|
+
|
|
100
|
+
config_kwargs: dict = {}
|
|
101
|
+
|
|
102
|
+
if "number_of_images" in step.params:
|
|
103
|
+
config_kwargs["number_of_images"] = int(step.params["number_of_images"])
|
|
104
|
+
if "aspect_ratio" in step.params:
|
|
105
|
+
ar = step.params["aspect_ratio"]
|
|
106
|
+
if ar not in _VALID_ASPECT_RATIOS:
|
|
107
|
+
raise ProviderError(
|
|
108
|
+
f"Invalid aspect_ratio={ar!r}. Must be one of {_VALID_ASPECT_RATIOS}",
|
|
109
|
+
error_code=ProviderErrorCode.INVALID_INPUT,
|
|
110
|
+
)
|
|
111
|
+
config_kwargs["aspect_ratio"] = ar
|
|
112
|
+
if "person_generation" in step.params:
|
|
113
|
+
config_kwargs["person_generation"] = step.params["person_generation"]
|
|
114
|
+
if "safety_filter_level" in step.params:
|
|
115
|
+
config_kwargs["safety_filter_level"] = step.params["safety_filter_level"]
|
|
116
|
+
if "output_mime_type" in step.params:
|
|
117
|
+
config_kwargs["output_mime_type"] = step.params["output_mime_type"]
|
|
118
|
+
|
|
119
|
+
gen_config = types.GenerateImagesConfig(**config_kwargs) if config_kwargs else None
|
|
120
|
+
|
|
121
|
+
kwargs: dict = {"model": step.model, "prompt": step.prompt or ""}
|
|
122
|
+
if gen_config is not None:
|
|
123
|
+
kwargs["config"] = gen_config
|
|
124
|
+
|
|
125
|
+
response = client.models.generate_images(**kwargs)
|
|
126
|
+
|
|
127
|
+
# Safety filter can return 0 images without raising an error
|
|
128
|
+
if not response.generated_images:
|
|
129
|
+
raise ProviderError(
|
|
130
|
+
"Imagen returned no images — prompt may have been blocked by safety filters",
|
|
131
|
+
error_code=ProviderErrorCode.INVALID_INPUT,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
mime_type = step.params.get("output_mime_type", "image/png")
|
|
135
|
+
suffix = ".jpeg" if "jpeg" in mime_type else ".png"
|
|
136
|
+
|
|
137
|
+
for i, img in enumerate(response.generated_images):
|
|
138
|
+
if self._output_dir:
|
|
139
|
+
self._output_dir.mkdir(parents=True, exist_ok=True)
|
|
140
|
+
out_path = self._output_dir / f"{step.step_id}_{i}{suffix}"
|
|
141
|
+
else:
|
|
142
|
+
fd, tmp = tempfile.mkstemp(suffix=suffix)
|
|
143
|
+
os.close(fd)
|
|
144
|
+
out_path = Path(tmp)
|
|
145
|
+
|
|
146
|
+
img.image.save(str(out_path))
|
|
147
|
+
file_url = f"file://{quote(str(out_path.resolve()))}"
|
|
148
|
+
step.assets.append(Asset(url=file_url, media_type=mime_type))
|
|
149
|
+
|
|
150
|
+
# Track cost
|
|
151
|
+
price = _IMAGEN_PRICING.get(step.model)
|
|
152
|
+
if price is not None:
|
|
153
|
+
step.cost_usd = price * len(step.assets)
|
|
154
|
+
|
|
155
|
+
return step
|
|
156
|
+
except ProviderError:
|
|
157
|
+
raise
|
|
158
|
+
except Exception as exc:
|
|
159
|
+
raise ProviderError(
|
|
160
|
+
f"Imagen generation failed: {exc}",
|
|
161
|
+
error_code=map_google_error(exc),
|
|
162
|
+
) from exc
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""VeoProvider — adapter for Google Veo video generation.
|
|
2
|
+
|
|
3
|
+
Uses the google-genai SDK with the async operation-based workflow:
|
|
4
|
+
client.models.generate_videos() → poll operation → download video
|
|
5
|
+
|
|
6
|
+
Docs: https://ai.google.dev/gemini-api/docs/video
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from genblaze_core.exceptions import ProviderError
|
|
14
|
+
from genblaze_core.models.asset import Asset, AudioMetadata, Track, VideoMetadata
|
|
15
|
+
from genblaze_core.models.enums import Modality, ProviderErrorCode
|
|
16
|
+
from genblaze_core.models.step import Step
|
|
17
|
+
from genblaze_core.providers.base import BaseProvider, ProviderCapabilities, validate_asset_url
|
|
18
|
+
from genblaze_core.runnable.config import RunnableConfig
|
|
19
|
+
|
|
20
|
+
from genblaze_google._errors import map_google_error
|
|
21
|
+
|
|
22
|
+
# Valid Veo parameter values
|
|
23
|
+
_VALID_ASPECT_RATIOS = {"16:9", "9:16"}
|
|
24
|
+
_VALID_RESOLUTIONS = {"720p", "1080p", "4k"}
|
|
25
|
+
_VALID_DURATIONS = {"4", "6", "8"}
|
|
26
|
+
|
|
27
|
+
# Per-second pricing by model (USD)
|
|
28
|
+
_VEO_PRICING: dict[str, float] = {
|
|
29
|
+
"veo-2.0-generate-001": 0.35,
|
|
30
|
+
"veo-3.0-generate-001": 0.50,
|
|
31
|
+
"veo-3.0-fast-generate-001": 0.25,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class VeoProvider(BaseProvider):
|
|
36
|
+
"""Provider adapter for Google Veo video generation.
|
|
37
|
+
|
|
38
|
+
Models: ``veo-2.0-generate-001`` (stable), ``veo-3.0-generate-001`` (GA, audio),
|
|
39
|
+
``veo-3.0-fast-generate-001`` (GA, fast).
|
|
40
|
+
|
|
41
|
+
Supports both Gemini API (api_key) and Vertex AI (project/location) auth.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
api_key: Gemini API key. Falls back to GEMINI_API_KEY env var.
|
|
45
|
+
project: GCP project ID for Vertex AI auth (mutually exclusive with api_key).
|
|
46
|
+
location: GCP region for Vertex AI (default "us-central1").
|
|
47
|
+
poll_interval: Seconds between operation polls (default 10).
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
name = "google-veo"
|
|
51
|
+
|
|
52
|
+
def get_capabilities(self) -> ProviderCapabilities:
|
|
53
|
+
"""Veo: video generation from text prompts with configurable resolution and duration."""
|
|
54
|
+
return ProviderCapabilities(
|
|
55
|
+
supported_modalities=[Modality.VIDEO],
|
|
56
|
+
supported_inputs=["text"],
|
|
57
|
+
max_duration=8.0,
|
|
58
|
+
resolutions=["720p", "1080p", "4k"],
|
|
59
|
+
models=[
|
|
60
|
+
"veo-2.0-generate-001",
|
|
61
|
+
"veo-3.0-generate-001",
|
|
62
|
+
"veo-3.0-fast-generate-001",
|
|
63
|
+
],
|
|
64
|
+
output_formats=["video/mp4"],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
api_key: str | None = None,
|
|
70
|
+
*,
|
|
71
|
+
project: str | None = None,
|
|
72
|
+
location: str = "us-central1",
|
|
73
|
+
poll_interval: float = 10.0,
|
|
74
|
+
):
|
|
75
|
+
super().__init__()
|
|
76
|
+
self.poll_interval = poll_interval
|
|
77
|
+
self._api_key = api_key
|
|
78
|
+
self._project = project
|
|
79
|
+
self._location = location
|
|
80
|
+
self._client: Any = None
|
|
81
|
+
|
|
82
|
+
def normalize_params(self, params: dict, modality: Any = None) -> dict:
|
|
83
|
+
"""Map standard params to Veo-native names."""
|
|
84
|
+
p = dict(params)
|
|
85
|
+
# duration → duration_seconds (Veo's native key)
|
|
86
|
+
if "duration" in p and "duration_seconds" not in p:
|
|
87
|
+
p["duration_seconds"] = p.pop("duration")
|
|
88
|
+
return p
|
|
89
|
+
|
|
90
|
+
def _get_client(self):
|
|
91
|
+
if self._client is None:
|
|
92
|
+
try:
|
|
93
|
+
from google import genai
|
|
94
|
+
except ImportError as exc:
|
|
95
|
+
raise ProviderError(
|
|
96
|
+
"google-genai package not installed. Run: pip install google-genai"
|
|
97
|
+
) from exc
|
|
98
|
+
|
|
99
|
+
if self._project:
|
|
100
|
+
# Vertex AI auth
|
|
101
|
+
self._client = genai.Client(
|
|
102
|
+
vertexai=True,
|
|
103
|
+
project=self._project,
|
|
104
|
+
location=self._location,
|
|
105
|
+
)
|
|
106
|
+
else:
|
|
107
|
+
# Gemini API key auth
|
|
108
|
+
kwargs: dict = {}
|
|
109
|
+
if self._api_key:
|
|
110
|
+
kwargs["api_key"] = self._api_key
|
|
111
|
+
self._client = genai.Client(**kwargs)
|
|
112
|
+
return self._client
|
|
113
|
+
|
|
114
|
+
def _build_config(self, step: Step) -> Any:
|
|
115
|
+
"""Build a GenerateVideosConfig from step.params."""
|
|
116
|
+
from google.genai import types
|
|
117
|
+
|
|
118
|
+
config_kwargs: dict = {}
|
|
119
|
+
|
|
120
|
+
if "aspect_ratio" in step.params:
|
|
121
|
+
ar = step.params["aspect_ratio"]
|
|
122
|
+
if ar not in _VALID_ASPECT_RATIOS:
|
|
123
|
+
raise ProviderError(
|
|
124
|
+
f"Invalid aspect_ratio={ar!r}. Must be one of {_VALID_ASPECT_RATIOS}",
|
|
125
|
+
error_code=ProviderErrorCode.INVALID_INPUT,
|
|
126
|
+
)
|
|
127
|
+
config_kwargs["aspect_ratio"] = ar
|
|
128
|
+
|
|
129
|
+
if "resolution" in step.params:
|
|
130
|
+
res = step.params["resolution"]
|
|
131
|
+
if res not in _VALID_RESOLUTIONS:
|
|
132
|
+
raise ProviderError(
|
|
133
|
+
f"Invalid resolution={res!r}. Must be one of {_VALID_RESOLUTIONS}",
|
|
134
|
+
error_code=ProviderErrorCode.INVALID_INPUT,
|
|
135
|
+
)
|
|
136
|
+
config_kwargs["resolution"] = res
|
|
137
|
+
|
|
138
|
+
# Duration (normalize_params already mapped duration → duration_seconds)
|
|
139
|
+
raw_dur = step.params.get("duration_seconds")
|
|
140
|
+
if raw_dur is not None:
|
|
141
|
+
dur = str(raw_dur)
|
|
142
|
+
if dur not in _VALID_DURATIONS:
|
|
143
|
+
raise ProviderError(
|
|
144
|
+
f"Invalid duration_seconds={dur!r}. Must be one of {_VALID_DURATIONS}",
|
|
145
|
+
error_code=ProviderErrorCode.INVALID_INPUT,
|
|
146
|
+
)
|
|
147
|
+
config_kwargs["duration_seconds"] = dur
|
|
148
|
+
|
|
149
|
+
if "person_generation" in step.params:
|
|
150
|
+
config_kwargs["person_generation"] = step.params["person_generation"]
|
|
151
|
+
|
|
152
|
+
if "number_of_videos" in step.params:
|
|
153
|
+
config_kwargs["number_of_videos"] = int(step.params["number_of_videos"])
|
|
154
|
+
|
|
155
|
+
if "enhance_prompt" in step.params:
|
|
156
|
+
config_kwargs["enhance_prompt"] = bool(step.params["enhance_prompt"])
|
|
157
|
+
|
|
158
|
+
if step.seed is not None:
|
|
159
|
+
config_kwargs["seed"] = step.seed
|
|
160
|
+
|
|
161
|
+
return types.GenerateVideosConfig(**config_kwargs) if config_kwargs else None
|
|
162
|
+
|
|
163
|
+
def submit(self, step: Step, config: RunnableConfig | None = None) -> Any:
|
|
164
|
+
"""Start a video generation operation."""
|
|
165
|
+
client = self._get_client()
|
|
166
|
+
try:
|
|
167
|
+
gen_config = self._build_config(step)
|
|
168
|
+
kwargs: dict = {
|
|
169
|
+
"model": step.model,
|
|
170
|
+
"prompt": step.prompt or "",
|
|
171
|
+
}
|
|
172
|
+
if gen_config is not None:
|
|
173
|
+
kwargs["config"] = gen_config
|
|
174
|
+
|
|
175
|
+
operation = client.models.generate_videos(**kwargs)
|
|
176
|
+
# Return the provider-native operation name for resume() compatibility
|
|
177
|
+
return operation.name
|
|
178
|
+
except ProviderError:
|
|
179
|
+
raise
|
|
180
|
+
except Exception as exc:
|
|
181
|
+
raise ProviderError(
|
|
182
|
+
f"Veo submit failed: {exc}",
|
|
183
|
+
error_code=map_google_error(exc),
|
|
184
|
+
) from exc
|
|
185
|
+
|
|
186
|
+
def poll(self, prediction_id: Any, config: RunnableConfig | None = None) -> bool:
|
|
187
|
+
"""Check if the video generation operation is done."""
|
|
188
|
+
client = self._get_client()
|
|
189
|
+
try:
|
|
190
|
+
operation = client.operations.get(prediction_id)
|
|
191
|
+
if operation.done:
|
|
192
|
+
self._cache_poll_result(prediction_id, operation)
|
|
193
|
+
return True
|
|
194
|
+
return False
|
|
195
|
+
except ProviderError:
|
|
196
|
+
raise
|
|
197
|
+
except Exception as exc:
|
|
198
|
+
raise ProviderError(
|
|
199
|
+
f"Veo poll failed: {exc}",
|
|
200
|
+
error_code=map_google_error(exc),
|
|
201
|
+
) from exc
|
|
202
|
+
|
|
203
|
+
def fetch_output(self, prediction_id: Any, step: Step) -> Step:
|
|
204
|
+
"""Download generated video(s) and attach asset URLs."""
|
|
205
|
+
client = self._get_client()
|
|
206
|
+
try:
|
|
207
|
+
# Use cached poll result if available, otherwise fetch fresh
|
|
208
|
+
operation = self._get_cached_poll_result(prediction_id)
|
|
209
|
+
if operation is None:
|
|
210
|
+
operation = client.operations.get(prediction_id)
|
|
211
|
+
|
|
212
|
+
# Store provider metadata
|
|
213
|
+
step.provider_payload = {
|
|
214
|
+
"google": {
|
|
215
|
+
"operation_name": getattr(operation, "name", None),
|
|
216
|
+
"model": step.model,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# Check for errors in the operation result
|
|
221
|
+
if hasattr(operation, "error") and operation.error:
|
|
222
|
+
raise ProviderError(
|
|
223
|
+
str(operation.error),
|
|
224
|
+
error_code=ProviderErrorCode.UNKNOWN,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
response = operation.response
|
|
228
|
+
if response is None or not hasattr(response, "generated_videos"):
|
|
229
|
+
raise ProviderError("No video generated in response")
|
|
230
|
+
|
|
231
|
+
# Veo 3.0 models generate audio alongside video
|
|
232
|
+
is_veo3 = step.model.startswith("veo-3")
|
|
233
|
+
|
|
234
|
+
for gv in response.generated_videos:
|
|
235
|
+
video = gv.video
|
|
236
|
+
# Download to get the file URI
|
|
237
|
+
client.files.download(file=video)
|
|
238
|
+
# Use the video's URI as the asset URL
|
|
239
|
+
video_uri = getattr(video, "uri", None)
|
|
240
|
+
if video_uri:
|
|
241
|
+
validate_asset_url(video_uri)
|
|
242
|
+
vm_kwargs: dict[str, Any] = {"has_audio": is_veo3}
|
|
243
|
+
if "resolution" in step.params:
|
|
244
|
+
vm_kwargs["resolution"] = step.params["resolution"]
|
|
245
|
+
asset = Asset(url=video_uri, media_type="video/mp4")
|
|
246
|
+
asset.video = VideoMetadata(**vm_kwargs)
|
|
247
|
+
# Multi-track metadata for Veo 3 (video + generated audio)
|
|
248
|
+
if is_veo3:
|
|
249
|
+
asset.tracks = [
|
|
250
|
+
Track(kind="video", codec="h264"),
|
|
251
|
+
Track(kind="audio", codec="aac", label="generated-audio"),
|
|
252
|
+
]
|
|
253
|
+
asset.audio = AudioMetadata(codec="aac")
|
|
254
|
+
step.assets.append(asset)
|
|
255
|
+
else:
|
|
256
|
+
# Fallback: save locally and use file path
|
|
257
|
+
raise ProviderError(
|
|
258
|
+
"Veo response missing video URI — "
|
|
259
|
+
"use client.files.download() to save locally"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Track cost — accept both native and standard duration key
|
|
263
|
+
price_per_sec = _VEO_PRICING.get(step.model)
|
|
264
|
+
if price_per_sec is not None:
|
|
265
|
+
raw_dur = step.params.get("duration_seconds") or step.params.get("duration")
|
|
266
|
+
duration = int(raw_dur) if raw_dur is not None else 4
|
|
267
|
+
step.cost_usd = price_per_sec * duration * len(step.assets)
|
|
268
|
+
|
|
269
|
+
return step
|
|
270
|
+
except ProviderError:
|
|
271
|
+
raise
|
|
272
|
+
except Exception as exc:
|
|
273
|
+
raise ProviderError(
|
|
274
|
+
f"Veo fetch_output failed: {exc}",
|
|
275
|
+
error_code=map_google_error(exc),
|
|
276
|
+
) from exc
|
genblaze_google/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: genblaze-google
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Google provider adapters for genblaze (Veo video, Imagen image)
|
|
5
|
+
Project-URL: Documentation, https://github.com/backblaze-labs/genblaze
|
|
6
|
+
Project-URL: Repository, https://github.com/backblaze-labs/genblaze
|
|
7
|
+
Project-URL: Issues, https://github.com/backblaze-labs/genblaze/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Typing :: Typed
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: genblaze-core<0.2,>=0.1.0
|
|
14
|
+
Requires-Dist: google-genai>=1.0
|
|
15
|
+
Provides-Extra: dev
|
|
16
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
<!-- last_verified: 2026-04-22 -->
|
|
20
|
+
# genblaze-google
|
|
21
|
+
|
|
22
|
+
**Google provider adapters for [genblaze](https://github.com/backblaze-labs/genblaze) — [Veo](https://deepmind.google/technologies/veo/) text-to-video and [Imagen](https://deepmind.google/technologies/imagen-3/) text-to-image — with SHA-256 provenance manifests on every output.**
|
|
23
|
+
|
|
24
|
+
`genblaze-google` wraps Google's generative media models (Veo 2, Veo 3, Imagen 3) as genblaze providers via the unified `google-genai` SDK. Works with both Gemini API keys and Google Cloud Vertex AI authentication. Compose Veo/Imagen calls into multi-step AI pipelines, persist outputs to [Backblaze B2](https://www.backblaze.com/cloud-storage?utm_source=github&utm_medium=referral&utm_campaign=ai_artifacts&utm_content=genblaze) or any S3-compatible store, and emit a tamper-evident provenance manifest for every run.
|
|
25
|
+
|
|
26
|
+
## Why genblaze-google
|
|
27
|
+
|
|
28
|
+
- **Veo 3 with synchronized audio** — text-to-video + native audio, wrapped in a provenance manifest.
|
|
29
|
+
- **Imagen 3 high-fidelity images** — photorealistic stills with full parameter tracking.
|
|
30
|
+
- **Two auth modes** — Gemini API (`GEMINI_API_KEY`) for quick start, Vertex AI for enterprise / GCP orgs.
|
|
31
|
+
- **Same SDK, any provider** — swap to Sora, Runway, Luma, Flux without rewriting pipeline logic.
|
|
32
|
+
- **Provenance by default** — SHA-256 hash + canonical manifest on every generation.
|
|
33
|
+
- **Durable storage** — plug `genblaze-s3` in for Backblaze B2 / AWS S3 / Cloudflare R2 / MinIO.
|
|
34
|
+
|
|
35
|
+
## Providers + models
|
|
36
|
+
|
|
37
|
+
| Provider class | Modality | Models |
|
|
38
|
+
|---|---|---|
|
|
39
|
+
| `VeoProvider` | video | `veo-3.0-generate-001` (with audio), `veo-3.0-fast-generate-001`, `veo-2.0-generate-001` |
|
|
40
|
+
| `ImagenProvider` | image | `imagen-3.0-generate-002`, `imagen-3.0-fast-generate-001` |
|
|
41
|
+
|
|
42
|
+
Each is registered via entry points (`google-veo`, `google-imagen`).
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install genblaze-google
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Quickstart — Veo 3 text-to-video (with audio)
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export GEMINI_API_KEY="..." # or use Vertex AI auth
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from genblaze_core import Modality, Pipeline
|
|
58
|
+
from genblaze_google import VeoProvider
|
|
59
|
+
|
|
60
|
+
run, manifest = (
|
|
61
|
+
Pipeline("veo-demo")
|
|
62
|
+
.step(VeoProvider(), model="veo-3.0-generate-001",
|
|
63
|
+
prompt="A time-lapse of a coral reef coming to life, colorful fish "
|
|
64
|
+
"swimming through vibrant coral, natural ocean lighting",
|
|
65
|
+
modality=Modality.VIDEO,
|
|
66
|
+
aspect_ratio="16:9", duration_seconds="8", resolution="720p",
|
|
67
|
+
enhance_prompt=True)
|
|
68
|
+
.run(timeout=600)
|
|
69
|
+
)
|
|
70
|
+
print(run.steps[0].assets[0].url, manifest.canonical_hash)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Vertex AI auth instead:
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
provider = VeoProvider(project="my-gcp-project", location="us-central1")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Quickstart — Imagen 3 text-to-image
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from genblaze_google import ImagenProvider
|
|
83
|
+
|
|
84
|
+
run, manifest = (
|
|
85
|
+
Pipeline("imagen-demo")
|
|
86
|
+
.step(ImagenProvider(output_dir="output/images"),
|
|
87
|
+
model="imagen-3.0-generate-002",
|
|
88
|
+
prompt="A photorealistic aerial view of a coral reef teeming with tropical fish",
|
|
89
|
+
modality=Modality.IMAGE, aspect_ratio="16:9")
|
|
90
|
+
.run(timeout=120)
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Persist to Backblaze B2
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from genblaze_core import KeyStrategy, ObjectStorageSink
|
|
98
|
+
from genblaze_s3 import S3StorageBackend
|
|
99
|
+
|
|
100
|
+
storage = ObjectStorageSink(
|
|
101
|
+
S3StorageBackend.for_backblaze("my-bucket"),
|
|
102
|
+
key_strategy=KeyStrategy.HIERARCHICAL,
|
|
103
|
+
)
|
|
104
|
+
# pass sink=storage to .run(…) to push assets + manifest to B2
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
[Backblaze B2](https://www.backblaze.com/cloud-storage?utm_source=github&utm_medium=referral&utm_campaign=ai_artifacts&utm_content=genblaze) is the recommended default sink for genblaze — cost-efficient, S3-compatible, with Object Lock for tamper-evident manifests.
|
|
108
|
+
|
|
109
|
+
## Credentials
|
|
110
|
+
|
|
111
|
+
| Auth mode | Env var / config |
|
|
112
|
+
|---|---|
|
|
113
|
+
| Gemini API (quickest) | `GEMINI_API_KEY` — https://aistudio.google.com/apikey |
|
|
114
|
+
| Vertex AI | `VeoProvider(project=..., location=...)` + `gcloud auth application-default login` |
|
|
115
|
+
|
|
116
|
+
## Documentation
|
|
117
|
+
|
|
118
|
+
- **Main repo**: https://github.com/backblaze-labs/genblaze
|
|
119
|
+
- **Examples**: [`veo_video_pipeline.py`](https://github.com/backblaze-labs/genblaze/blob/main/examples/veo_video_pipeline.py) · [`imagen_pipeline.py`](https://github.com/backblaze-labs/genblaze/blob/main/examples/imagen_pipeline.py)
|
|
120
|
+
|
|
121
|
+
## Related packages
|
|
122
|
+
|
|
123
|
+
- [`genblaze-core`](https://pypi.org/project/genblaze-core/) — the pipeline SDK
|
|
124
|
+
- [`genblaze-s3`](https://pypi.org/project/genblaze-s3/) — durable storage on Backblaze B2 and other S3-compatible backends
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
genblaze_google/__init__.py,sha256=O8gr3PLZUZ-12sA2a34nkMTNrr_tT_K81J2otHuCk4g,216
|
|
2
|
+
genblaze_google/_errors.py,sha256=mkaSFA-o_Q82jTue-nRurcoyMQ8sXCe5PbohWR8v5lE,841
|
|
3
|
+
genblaze_google/imagen.py,sha256=KcswCjHrtSCLnp5J1SkrcNenhMIuRNzkO9e8NKong98,6224
|
|
4
|
+
genblaze_google/provider.py,sha256=cYsHF3npwXy6aBGJXiFW5bHP2yTLzFJ2nc2_O5qori0,10747
|
|
5
|
+
genblaze_google/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
genblaze_google-0.1.0.dist-info/METADATA,sha256=eXEnaPNmjxOT_PMs8u7J6RQjUip0VJvK_JrYPxR-MDw,5197
|
|
7
|
+
genblaze_google-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
genblaze_google-0.1.0.dist-info/entry_points.txt,sha256=9jRx0y51zgPAxz_VJHOnVE2SOcBwDahKDVZMhVk3T-8,109
|
|
9
|
+
genblaze_google-0.1.0.dist-info/RECORD,,
|