mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
62229d9d0a
Signed-off-by: phernandez <paul@basicmachines.co>
167 lines
4.6 KiB
Python
Executable File
167 lines
4.6 KiB
Python
Executable File
#!/usr/bin/env -S uv run --script
|
|
# /// script
|
|
# requires-python = ">=3.12"
|
|
# dependencies = [
|
|
# "openai>=1.100.2",
|
|
# "python-dotenv>=1.1.0",
|
|
# "typer>=0.9.0",
|
|
# ]
|
|
# ///
|
|
"""Generate a BM Bossbot infographic with the OpenAI Images API."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import time
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Annotated, Any
|
|
|
|
import typer
|
|
from dotenv import load_dotenv
|
|
from openai import OpenAI
|
|
|
|
|
|
DEFAULT_MODEL = "gpt-image-2"
|
|
DEFAULT_SIZE = "1536x1024"
|
|
DEFAULT_QUALITY = "high"
|
|
DEFAULT_FORMAT = "webp"
|
|
DEFAULT_COMPRESSION = 90
|
|
app = typer.Typer(
|
|
add_completion=False,
|
|
help="Generate Basic Memory infographics with the OpenAI Images API.",
|
|
no_args_is_help=True,
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GeneratedImage:
|
|
path: Path
|
|
revised_prompt: str | None
|
|
|
|
|
|
def validate_output_path(path: Path, *, repo_root: Path | None = None) -> Path:
|
|
root = (repo_root or Path.cwd()).resolve()
|
|
output = path.resolve()
|
|
allowed_root = (root / "docs" / "assets" / "infographics").resolve()
|
|
if not output.is_relative_to(allowed_root):
|
|
allowed_path = allowed_root.relative_to(root).as_posix()
|
|
raise ValueError(f"Output path must be under {allowed_path}")
|
|
if output.suffix != ".webp":
|
|
raise ValueError("Output path must end with .webp")
|
|
return output
|
|
|
|
|
|
def generate_image_result(
|
|
*,
|
|
prompt: str,
|
|
output_path: Path,
|
|
model: str = DEFAULT_MODEL,
|
|
size: str = DEFAULT_SIZE,
|
|
quality: str = DEFAULT_QUALITY,
|
|
output_format: str = DEFAULT_FORMAT,
|
|
output_compression: int = DEFAULT_COMPRESSION,
|
|
client: Any | None = None,
|
|
retries: int = 2,
|
|
) -> GeneratedImage:
|
|
output = validate_output_path(output_path)
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
load_dotenv()
|
|
openai_client = client or OpenAI()
|
|
|
|
for attempt in range(retries + 1):
|
|
try:
|
|
response = openai_client.images.generate(
|
|
model=model,
|
|
prompt=prompt,
|
|
size=size,
|
|
quality=quality,
|
|
output_format=output_format,
|
|
output_compression=output_compression,
|
|
)
|
|
image = response.data[0]
|
|
image_b64 = image.b64_json
|
|
if not image_b64:
|
|
raise RuntimeError("OpenAI image response did not include b64_json")
|
|
output.write_bytes(base64.b64decode(image_b64))
|
|
return GeneratedImage(path=output, revised_prompt=image.revised_prompt)
|
|
except Exception:
|
|
if attempt >= retries:
|
|
raise
|
|
time.sleep(2**attempt)
|
|
|
|
raise RuntimeError("Image generation retry loop exited unexpectedly")
|
|
|
|
|
|
def generate_image(
|
|
*,
|
|
prompt: str,
|
|
output_path: Path,
|
|
model: str = DEFAULT_MODEL,
|
|
size: str = DEFAULT_SIZE,
|
|
quality: str = DEFAULT_QUALITY,
|
|
output_format: str = DEFAULT_FORMAT,
|
|
output_compression: int = DEFAULT_COMPRESSION,
|
|
client: Any | None = None,
|
|
retries: int = 2,
|
|
) -> Path:
|
|
return generate_image_result(
|
|
prompt=prompt,
|
|
output_path=output_path,
|
|
model=model,
|
|
size=size,
|
|
quality=quality,
|
|
output_format=output_format,
|
|
output_compression=output_compression,
|
|
client=client,
|
|
retries=retries,
|
|
).path
|
|
|
|
|
|
@app.command()
|
|
def generate(
|
|
prompt_file: Annotated[
|
|
Path,
|
|
typer.Option(
|
|
"--prompt-file",
|
|
exists=True,
|
|
dir_okay=False,
|
|
readable=True,
|
|
help="Markdown/text prompt file to send to the image model.",
|
|
),
|
|
],
|
|
output: Annotated[Path, typer.Option("--output", help="Output .webp path.")],
|
|
model: Annotated[str, typer.Option("--model", help="OpenAI image model.")] = DEFAULT_MODEL,
|
|
size: Annotated[str, typer.Option("--size", help="Image size.")] = DEFAULT_SIZE,
|
|
quality: Annotated[str, typer.Option("--quality", help="Image quality.")] = DEFAULT_QUALITY,
|
|
output_compression: Annotated[
|
|
int,
|
|
typer.Option(
|
|
"--output-compression",
|
|
min=0,
|
|
max=100,
|
|
help="WebP output compression.",
|
|
),
|
|
] = DEFAULT_COMPRESSION,
|
|
retries: Annotated[int, typer.Option("--retries", min=0, help="Retry attempts.")] = 2,
|
|
) -> None:
|
|
"""Generate an infographic from a prompt file."""
|
|
output = generate_image(
|
|
prompt=prompt_file.read_text(encoding="utf-8"),
|
|
output_path=output,
|
|
model=model,
|
|
size=size,
|
|
quality=quality,
|
|
output_compression=output_compression,
|
|
retries=retries,
|
|
)
|
|
typer.echo(output)
|
|
|
|
|
|
def main() -> None:
|
|
app()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|