Files
wehub-resource-sync 8ef9db2513
Deploy to GitHub Pages / deploy (push) Has been cancelled
Deploy to GitHub Pages / build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:56:49 +08:00

236 lines
8.8 KiB
Python

#!/usr/bin/env python3
"""
Gemini Image Generation Backend
Generates images via the Google GenAI API (Gemini).
Used by image_gen.py as a backend module.
Configuration keys:
GEMINI_API_KEY (required)
GEMINI_BASE_URL (optional) Custom API endpoint for proxy services
GEMINI_MODEL (optional) Override default model
Dependencies:
pip install google-genai Pillow
"""
import sys
from pathlib import Path
_SCRIPTS_DIR = Path(__file__).resolve().parents[1]
if str(_SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPTS_DIR))
from console_encoding import configure_utf8_stdio # noqa: E402
configure_utf8_stdio()
if __name__ == "__main__":
print(__doc__)
print("Use via: python3 skills/ppt-master/scripts/image_gen.py \"prompt\" --backend gemini")
raise SystemExit(0 if any(arg in {"-h", "--help", "help"} for arg in sys.argv[1:]) else 1)
import os
import time
import threading
from google import genai
from google.genai import types
from image_backends.backend_common import (
MAX_RETRIES,
is_rate_limit_error,
normalize_image_size,
resolve_output_path,
retry_delay,
save_image_bytes,
)
# ╔═════════════════════════════════════════════════════════════════╗
# ║ Constants ║
# ╚════════════════════════════════════════════════════════════════╝
VALID_ASPECT_RATIOS = [
"1:1", "1:4", "1:8",
"2:3", "3:2", "3:4", "4:1", "4:3",
"4:5", "5:4", "8:1", "9:16", "16:9", "21:9"
]
VALID_IMAGE_SIZES = ["512px", "1K", "2K", "4K"]
DEFAULT_MODEL = "gemini-3.1-flash-image-preview"
# ╔════════════════════════════════════════════════════════════════╗
# ║ Image Generation ║
# ╚══════════════════════════════════════════════════════════════════╝
def _generate_image(api_key: str, prompt: str,
aspect_ratio: str = "1:1", image_size: str = "1K",
output_dir: str = None, filename: str = None,
model: str = DEFAULT_MODEL, base_url: str = None) -> str:
"""
Image generation via Gemini API (streaming).
Returns:
Path of the saved image file
Raises:
RuntimeError: When generation fails
"""
if base_url:
client = genai.Client(api_key=api_key, http_options={'base_url': base_url})
else:
client = genai.Client(api_key=api_key)
config_kwargs = {
"response_modalities": ["IMAGE"],
"image_config": types.ImageConfig(
aspect_ratio=aspect_ratio,
image_size=image_size,
),
}
if "flash" in model.lower():
config_kwargs["thinking_config"] = types.ThinkingConfig(
thinking_level="MINIMAL",
)
config = types.GenerateContentConfig(**config_kwargs)
mode_label = "Proxy Mode" if base_url else "Official Mode"
print(f"[Gemini - {mode_label}]")
if base_url:
print(f" Base URL: {base_url}")
print(f" Model: {model}")
print(f" Prompt: {prompt[:120]}{'...' if len(prompt) > 120 else ''}")
print(f" Aspect Ratio: {aspect_ratio}")
print(f" Image Size: {image_size}")
print()
start_time = time.time()
print(f" [..] Generating...", end="", flush=True)
heartbeat_stop = threading.Event()
def _heartbeat():
while not heartbeat_stop.is_set():
heartbeat_stop.wait(5)
if not heartbeat_stop.is_set():
elapsed = time.time() - start_time
print(f" {elapsed:.0f}s...", end="", flush=True)
hb_thread = threading.Thread(target=_heartbeat, daemon=True)
hb_thread.start()
last_image_data = None
chunk_count = 0
total_bytes = 0
for chunk in client.models.generate_content_stream(
model=model,
contents=[prompt],
config=config,
):
elapsed = time.time() - start_time
if chunk.parts is None:
continue
for part in chunk.parts:
if part.text is not None:
print(f"\n Model says: {part.text}", end="", flush=True)
elif part.inline_data is not None:
chunk_count += 1
data_size = len(part.inline_data.data) if part.inline_data.data else 0
total_bytes += data_size
size_str = f"{data_size / 1024:.0f}KB" if data_size < 1048576 else f"{data_size / 1048576:.1f}MB"
print(f"\n [OK] Chunk #{chunk_count} received ({size_str}, {elapsed:.1f}s)", end="", flush=True)
last_image_data = part
heartbeat_stop.set()
hb_thread.join(timeout=1)
elapsed = time.time() - start_time
print(f"\n [DONE] Stream complete ({elapsed:.1f}s, {chunk_count} chunk(s), {total_bytes / 1024:.0f}KB total)")
if last_image_data is not None and last_image_data.inline_data is not None:
if chunk_count > 1:
print(f" Keeping the final chunk (highest quality).")
path = resolve_output_path(prompt, output_dir, filename, ".png")
return save_image_bytes(
last_image_data.inline_data.data,
path,
content_type=getattr(last_image_data.inline_data, "mime_type", None),
)
raise RuntimeError("No image was generated. The server may have refused the request.")
# ╔════════════════════════════════════════════════════════════════╗
# ║ Public Entry Point ║
# ╚════════════════════════════════════════════════════════════════╝
def generate(prompt: str,
aspect_ratio: str = "1:1", image_size: str = "1K",
output_dir: str = None, filename: str = None,
model: str = None, max_retries: int = MAX_RETRIES) -> str:
"""
Gemini image generation with automatic retry.
Reads credentials from the current process environment or a `.env` file:
GEMINI_API_KEY
GEMINI_BASE_URL
GEMINI_MODEL (optional override)
Args:
prompt: Prompt text
aspect_ratio: Aspect ratio (e.g. "16:9", "1:1")
image_size: Image size ("512px", "1K", "2K", "4K", case-insensitive)
output_dir: Output directory
filename: Output filename (without extension)
model: Model name (default: gemini-3.1-flash-image-preview)
max_retries: Maximum number of retries
Returns:
Path of the saved image file
"""
api_key = os.environ.get("GEMINI_API_KEY")
base_url = os.environ.get("GEMINI_BASE_URL")
if not api_key:
raise ValueError(
"No API key found. Set GEMINI_API_KEY in the current environment or a .env file."
)
if model is None:
model = os.environ.get("GEMINI_MODEL") or DEFAULT_MODEL
image_size = normalize_image_size(image_size)
if aspect_ratio not in VALID_ASPECT_RATIOS:
raise ValueError(f"Invalid aspect ratio '{aspect_ratio}'. Valid: {VALID_ASPECT_RATIOS}")
if image_size not in VALID_IMAGE_SIZES:
raise ValueError(f"Invalid image size '{image_size}'. Valid: {VALID_IMAGE_SIZES}")
last_error = None
for attempt in range(max_retries + 1):
try:
return _generate_image(api_key, prompt,
aspect_ratio, image_size, output_dir,
filename, model, base_url)
except Exception as e:
last_error = e
if attempt < max_retries and is_rate_limit_error(e):
delay = retry_delay(attempt, rate_limited=True)
print(f"\n [WARN] Rate limit hit (attempt {attempt + 1}/{max_retries + 1}). "
f"Waiting {delay}s before retry...")
time.sleep(delay)
elif attempt < max_retries:
delay = retry_delay(attempt, rate_limited=False)
print(f"\n [WARN] Error (attempt {attempt + 1}/{max_retries + 1}): {e}. "
f"Retrying in {delay}s...")
time.sleep(delay)
else:
break
raise RuntimeError(f"Failed after {max_retries + 1} attempts. Last error: {last_error}")