-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
Support per-image aspect ratios in generate_images #563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,15 @@ | ||
| # pyright: reportUnknownVariableType=false | ||
| import asyncio | ||
| from typing import Any, Dict, List, Optional, Tuple, Union | ||
| from typing import Any, Dict, List, Literal, Optional, Tuple, TypedDict, Union, cast | ||
|
|
||
| from codegen.utils import extract_html_content | ||
| from config import REPLICATE_API_KEY | ||
| from image_generation.aspect_ratios import ( | ||
| AspectRatio, | ||
| DEFAULT_ASPECT_RATIO, | ||
| SUPPORTED_ASPECT_RATIOS, | ||
| is_supported_aspect_ratio, | ||
| ) | ||
| from image_generation.generation import process_tasks | ||
| from image_generation.replicate import remove_background | ||
|
|
||
|
|
@@ -12,6 +18,11 @@ | |
| from agent.tools.summaries import summarize_text | ||
|
|
||
|
|
||
| class ImagePromptRequest(TypedDict): | ||
| prompt: str | ||
| aspect_ratio: AspectRatio | ||
|
|
||
|
|
||
| class AgentToolRuntime: | ||
| def __init__( | ||
| self, | ||
|
|
@@ -195,14 +206,67 @@ async def _generate_images(self, args: Dict[str, Any]) -> ToolExecutionResult: | |
| summary={"error": "Missing prompts"}, | ||
| ) | ||
|
|
||
| cleaned = [prompt.strip() for prompt in prompts if isinstance(prompt, str)] | ||
| unique_prompts = list(dict.fromkeys([p for p in cleaned if p])) | ||
| if not unique_prompts: | ||
| if "aspect_ratio" in args: | ||
| return ToolExecutionResult( | ||
| ok=False, | ||
| result={ | ||
| "error": ( | ||
| "Top-level aspect_ratio is not supported. " | ||
| "Set aspect_ratio per prompt item in prompts[]." | ||
| ) | ||
| }, | ||
| summary={"error": "Invalid aspect_ratio"}, | ||
| ) | ||
|
|
||
| requests: list[ImagePromptRequest] = [] | ||
| for item in prompts: | ||
| if not isinstance(item, dict): | ||
| return ToolExecutionResult( | ||
| ok=False, | ||
| result={ | ||
| "error": ( | ||
| "Each prompts[] entry must be an object with " | ||
| "'prompt' and optional 'aspect_ratio'." | ||
| ) | ||
| }, | ||
| summary={"error": "Invalid prompts payload"}, | ||
| ) | ||
|
|
||
| prompt = ensure_str(item.get("prompt")).strip() | ||
| if not prompt: | ||
| return ToolExecutionResult( | ||
| ok=False, | ||
| result={"error": "Each prompts[] entry requires a non-empty prompt"}, | ||
| summary={"error": "Missing prompt"}, | ||
| ) | ||
|
|
||
| raw_aspect_ratio = item.get("aspect_ratio", DEFAULT_ASPECT_RATIO) | ||
| if not is_supported_aspect_ratio(raw_aspect_ratio): | ||
| return ToolExecutionResult( | ||
| ok=False, | ||
| result={ | ||
| "error": ( | ||
| f"Unsupported aspect_ratio: {raw_aspect_ratio}. " | ||
| f"Supported values: {', '.join(SUPPORTED_ASPECT_RATIOS)}" | ||
| ) | ||
| }, | ||
| summary={"error": "Invalid aspect_ratio"}, | ||
| ) | ||
| requests.append( | ||
| { | ||
| "prompt": prompt, | ||
| "aspect_ratio": cast(AspectRatio, raw_aspect_ratio), | ||
| } | ||
| ) | ||
|
|
||
| if not requests: | ||
| return ToolExecutionResult( | ||
| ok=False, | ||
| result={"error": "No valid prompts provided"}, | ||
| summary={"error": "No valid prompts"}, | ||
| ) | ||
|
|
||
| model: Literal["dalle3", "flux"] | ||
| if REPLICATE_API_KEY: | ||
| model = "flux" | ||
| api_key = REPLICATE_API_KEY | ||
|
|
@@ -218,19 +282,42 @@ async def _generate_images(self, args: Dict[str, Any]) -> ToolExecutionResult: | |
| api_key = self.openai_api_key | ||
| base_url = self.openai_base_url | ||
|
|
||
| generated = await process_tasks(unique_prompts, api_key, base_url, model) # type: ignore | ||
| merged_results = { | ||
| prompt: url for prompt, url in zip(unique_prompts, generated) | ||
| } | ||
| grouped_requests: dict[AspectRatio, list[tuple[int, str]]] = {} | ||
| for index, request in enumerate(requests): | ||
| grouped_requests.setdefault(request["aspect_ratio"], []).append( | ||
| (index, request["prompt"]) | ||
| ) | ||
|
|
||
| grouped_items = list(grouped_requests.items()) | ||
| grouped_results = await asyncio.gather( | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Launching one Useful? React with 👍 / 👎. |
||
| *[ | ||
| process_tasks( | ||
| prompts=[prompt for _, prompt in entries], | ||
| api_key=api_key, | ||
| base_url=base_url, | ||
| model=model, | ||
| aspect_ratio=aspect_ratio, | ||
| ) | ||
| for aspect_ratio, entries in grouped_items | ||
| ] | ||
| ) | ||
|
|
||
| generated: list[str | None] = [None] * len(requests) | ||
| for (aspect_ratio, entries), urls in zip(grouped_items, grouped_results): | ||
| _ = aspect_ratio | ||
| for (index, _prompt), url in zip(entries, urls): | ||
| generated[index] = url | ||
|
|
||
| summary_items = [ | ||
| { | ||
| "prompt": prompt, | ||
| "prompt": request["prompt"], | ||
| "url": url, | ||
| "aspect_ratio": request["aspect_ratio"], | ||
| "status": "ok" if url else "error", | ||
| } | ||
| for prompt, url in merged_results.items() | ||
| for request, url in zip(requests, generated) | ||
| ] | ||
| result = {"images": merged_results} | ||
| result = {"images": summary_items} | ||
| summary = {"images": summary_items} | ||
| return ToolExecutionResult(ok=True, result=result, summary=summary) | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| from typing import Literal, cast | ||
|
|
||
|
|
||
| AspectRatio = Literal[ | ||
| "1:1", | ||
| "16:9", | ||
| "9:16", | ||
| "3:2", | ||
| "2:3", | ||
| "4:3", | ||
| "3:4", | ||
| "5:4", | ||
| "4:5", | ||
| "21:9", | ||
| "9:21", | ||
| ] | ||
|
|
||
| SUPPORTED_ASPECT_RATIOS: tuple[AspectRatio, ...] = ( | ||
| "1:1", | ||
| "16:9", | ||
| "9:16", | ||
| "3:2", | ||
| "2:3", | ||
| "4:3", | ||
| "3:4", | ||
| "5:4", | ||
| "4:5", | ||
| "21:9", | ||
| "9:21", | ||
| ) | ||
| DEFAULT_ASPECT_RATIO: AspectRatio = "1:1" | ||
|
|
||
| DalleImageSize = Literal["1024x1024", "1792x1024", "1024x1792"] | ||
|
|
||
|
|
||
| def is_supported_aspect_ratio(value: object) -> bool: | ||
| return isinstance(value, str) and value in SUPPORTED_ASPECT_RATIOS | ||
|
|
||
|
|
||
| def normalize_aspect_ratio(value: object) -> AspectRatio: | ||
| if is_supported_aspect_ratio(value): | ||
| return cast(AspectRatio, value) | ||
| return DEFAULT_ASPECT_RATIO | ||
|
|
||
|
|
||
| def aspect_ratio_to_dalle_size(aspect_ratio: AspectRatio) -> DalleImageSize: | ||
| if aspect_ratio == "1:1": | ||
| return "1024x1024" | ||
|
|
||
| width, height = (int(part) for part in aspect_ratio.split(":")) | ||
| return "1792x1024" if width > height else "1024x1792" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The OpenAI strict tool schema now permits
aspect_ratio: nullfor prompt items (see_make_responses_schema_strict, which marks properties nullable and appendsnullto enums), but this validator rejects that payload:item.get("aspect_ratio", DEFAULT_ASPECT_RATIO)returnsNonewhen the key is present with null, thenis_supported_aspect_ratio(None)fails and the tool errors. That makes schema-valid strict tool calls fail at runtime instead of falling back to the default aspect ratio.Useful? React with 👍 / 👎.