CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
112 lines
4.7 KiB
Python
112 lines
4.7 KiB
Python
"""Generate illustration images via an image/vision model, to support editing
|
|
and creating images inside files (e.g. a new picture for a slide, or a
|
|
standalone image file).
|
|
|
|
Uses the active provider's OpenAI-compatible ``/images/generations`` endpoint
|
|
(the internal gateway, OpenAI, or any compatible server) — the same base URL +
|
|
API key the chat model already uses. Best-effort and never raises: returns
|
|
``(ok, message_or_path)`` so the UI can fall back gracefully when the endpoint
|
|
or model isn't available. Pure logic (no Qt) → unit-testable with the HTTP
|
|
layer mocked.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
from pathlib import Path
|
|
from typing import Optional, Tuple
|
|
|
|
_DEFAULT_MODEL = "gpt-image-1"
|
|
_TIMEOUT = (15, 180)
|
|
|
|
# Substrings (lowercased) that mark a model as image-GENERATION capable, across
|
|
# providers/gateways — so the AI-edit model picker can suggest one regardless of
|
|
# whether the endpoint is OpenAI, Anthropic-routed, or another gateway.
|
|
_IMAGE_MODEL_MARKERS = (
|
|
"image", "dall-e", "dalle", "imagen", "flux", "stable-diffusion", "sdxl",
|
|
"sd3", "sd-", "grok-2-image", "seedream", "firefly", "titan-image", "photon",
|
|
)
|
|
|
|
|
|
def looks_like_image_model(name: str) -> bool:
|
|
"""Đoán một model có sinh ảnh được không, dựa trên dấu hiệu trong tên.
|
|
|
|
Đoán theo tên vì không provider nào khai báo năng lực này qua API.
|
|
"""
|
|
n = (name or "").lower()
|
|
return any(m in n for m in _IMAGE_MODEL_MARKERS)
|
|
|
|
|
|
def suggest_image_model(models) -> Optional[str]:
|
|
"""Pick the most likely text-to-image model from a provider's model list
|
|
(any provider). Returns None if none look image-capable."""
|
|
for m in models or []:
|
|
if looks_like_image_model(m):
|
|
return m
|
|
return None
|
|
|
|
|
|
def _conf(config):
|
|
"""(base_url, api_key, model, ca_bundle) for image generation, from the
|
|
active provider + optional image_gen overrides in config."""
|
|
prov = config.provider_conf(config.active_provider) if config else {}
|
|
igen = (getattr(config, "data", {}) or {}).get("image_gen", {}) if config else {}
|
|
base = (igen.get("base_url") or prov.get("base_url") or "").rstrip("/")
|
|
key = igen.get("api_key") or prov.get("api_key") or ""
|
|
model = igen.get("model") or _DEFAULT_MODEL
|
|
ca = getattr(config, "ca_bundle", "") if config else ""
|
|
return base, key, model, ca
|
|
|
|
|
|
def is_configured(config) -> bool:
|
|
"""True when an image endpoint can be attempted (a base URL is set). The
|
|
actual call still degrades gracefully if the server/model can't generate."""
|
|
base, _key, _model, _ca = _conf(config)
|
|
return bool(base)
|
|
|
|
|
|
def generate_image(config, prompt: str, out_path: str,
|
|
model: Optional[str] = None, size: str = "1024x1024",
|
|
base_url: Optional[str] = None, api_key: Optional[str] = None) -> Tuple[bool, str]:
|
|
"""Generate an image for ``prompt`` and save it to ``out_path`` (PNG).
|
|
Returns ``(True, out_path)`` or ``(False, reason)``. Never raises.
|
|
|
|
By default the active provider's endpoint is used; pass ``base_url``/``api_key``
|
|
to target a DIFFERENT provider (e.g. an image model discovered on another
|
|
configured provider)."""
|
|
prompt = (prompt or "").strip()
|
|
if not prompt:
|
|
return False, "empty prompt"
|
|
base, key, cfg_model, ca = _conf(config)
|
|
if base_url: # explicit provider override (cross-provider image model)
|
|
base = base_url.rstrip("/")
|
|
key = api_key or ""
|
|
if not base:
|
|
return False, "no image endpoint configured (set a provider base URL or image_gen.base_url)"
|
|
from . import tls_trust
|
|
|
|
url = base + "/images/generations"
|
|
headers = {"Content-Type": "application/json"}
|
|
if key:
|
|
headers["Authorization"] = f"Bearer {key}"
|
|
payload = {"model": model or cfg_model, "prompt": prompt, "n": 1, "size": size}
|
|
try:
|
|
resp = tls_trust.request("post", url, ca_bundle=ca or None, json=payload,
|
|
headers=headers, timeout=_TIMEOUT)
|
|
resp.raise_for_status()
|
|
data = resp.json()
|
|
except Exception as exc: # noqa: BLE001 - endpoint/model unsupported, network, TLS…
|
|
return False, f"image generation failed: {exc}"
|
|
item = (data.get("data") or [{}])[0] if isinstance(data, dict) else {}
|
|
try:
|
|
if item.get("b64_json"):
|
|
Path(out_path).write_bytes(base64.b64decode(item["b64_json"]))
|
|
elif item.get("url"):
|
|
img = tls_trust.request("get", item["url"], ca_bundle=ca or None, timeout=_TIMEOUT)
|
|
img.raise_for_status()
|
|
Path(out_path).write_bytes(img.content)
|
|
else:
|
|
return False, "no image returned by the model"
|
|
except Exception as exc: # noqa: BLE001
|
|
return False, f"could not save image: {exc}"
|
|
return True, out_path
|