"""Vision-based technical QA for Codex-generated SMA images."""
from __future__ import annotations
import json
import os
import re
import subprocess
from pathlib import Path
from typing import Any
CODEX = os.environ.get("SMA_CODEX_BINARY", "/usr/bin/codex")
CODEX_HOME = Path(os.environ.get("CODEX_HOME", "/root/.codex")).resolve()
TRUSTED_WORKSPACE = Path(os.environ.get("SMA_CODEX_WORKSPACE", "/opt/struktur/social-media-agent/.codex-workspace")).resolve()
CRITERIA = (
"technical_device_plausible", "cables_and_connections", "measurement_method",
"anatomy", "building_geometry", "topic_match",
)
class CodexImageQAError(RuntimeError):
"""QA could not produce a trustworthy structured result."""
def _image_paths() -> set[Path]:
root = CODEX_HOME / "generated_images"
return {p.resolve() for p in root.glob("**/*") if p.is_file() and p.suffix.lower() in {".png", ".jpg", ".jpeg", ".webp"}}
def _extract_json(text: str) -> dict[str, Any]:
for match in re.finditer(r"\{", text):
depth = 0
in_string = False
escaped = False
for i in range(match.start(), len(text)):
ch = text[i]
if in_string:
if escaped: escaped = False
elif ch == "\\": escaped = True
elif ch == '"': in_string = False
elif ch == '"': in_string = True
elif ch == "{": depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
try:
value = json.loads(text[match.start():i + 1])
if isinstance(value, dict): return value
except json.JSONDecodeError: break
break
raise CodexImageQAError("Codex-QA lieferte kein strukturiertes JSON")
def parse_qa_response(text: str) -> dict[str, Any]:
raw = _extract_json(text)
result: dict[str, Any] = {}
for key in CRITERIA + ("overall",):
value = str(raw.get(key, "FAIL")).upper().strip()
result[key] = "PASS" if value == "PASS" else "FAIL"
reasons = raw.get("failure_reasons", [])
if isinstance(reasons, str): reasons = [reasons]
result["failure_reasons"] = [str(x) for x in reasons if str(x).strip()]
confidence = raw.get("confidence")
result["confidence"] = confidence if isinstance(confidence, (int, float)) else None
if any(result[key] == "FAIL" for key in CRITERIA):
result["overall"] = "FAIL"
if result["overall"] == "FAIL" and not result["failure_reasons"]:
result["failure_reasons"] = ["Mindestens ein technisches QA-Kriterium ist nicht bestanden."]
return result
def qa_passes(result: dict[str, Any]) -> bool:
return result.get("overall") == "PASS" and all(result.get(key) == "PASS" for key in CRITERIA)
def run_image_qa(image_path: Path, topic: str, post_text: str = "", *, timeout: int = 600) -> dict[str, Any]:
image_path = Path(image_path).resolve()
if not image_path.is_file(): raise CodexImageQAError(f"Bilddatei fehlt: {image_path}")
if not TRUSTED_WORKSPACE.is_dir(): raise CodexImageQAError(f"Codex-Arbeitskontext fehlt: {TRUSTED_WORKSPACE}")
prompt = f'''Technische Bild-QA. Prüfe das angehängte Bild streng und ausschließlich nach diesen Kriterien. Thema des Packages: {topic}. Inhalt des Posts: {post_text[:2000]}
Bewerte jedes Kriterium mit exakt PASS oder FAIL. Ein einzelner kritischer FAIL erzwingt overall FAIL. Prüfe: plausibles professionelles Messgerät für die behauptete Tätigkeit; nachvollziehbare Kabel und Anschlüsse ohne aus Wand/Boden/Körper zu entstehen; technisch sinnvolle Messhandlung; anatomisch plausible Person/Hände; widerspruchsfreie Bauteil- und Umgebungsgeometrie; fachlicher Bezug zum konkreten Thema/Post.
Antworte ausschließlich als valides JSON ohne Markdown und ohne zusätzlichen Text mit exakt diesen Schlüsseln: technical_device_plausible, cables_and_connections, measurement_method, anatomy, building_geometry, topic_match, overall, failure_reasons (Array von Strings), confidence (Zahl 0 bis 1). Keine Bildgenerierung, kein Dateischreiben, kein API-Aufruf, keine Veröffentlichung.'''
before = _image_paths()
env = os.environ.copy(); env["HOME"] = "/root"; env["CODEX_HOME"] = str(CODEX_HOME); env.pop("OPENAI_API_KEY", None)
command = [CODEX, "exec", "--sandbox", "read-only", "--skip-git-repo-check", "--ephemeral", "-C", str(TRUSTED_WORKSPACE), "--image", str(image_path), "--json", prompt]
try:
completed = subprocess.run(command, env=env, text=True, capture_output=True, timeout=timeout, check=False)
except (OSError, subprocess.TimeoutExpired) as exc:
raise CodexImageQAError(f"Codex-QA-Prozess fehlgeschlagen: {type(exc).__name__}") from exc
if completed.returncode != 0: raise CodexImageQAError(f"Codex-QA-Prozess Exit {completed.returncode}")
if _image_paths() != before: raise CodexImageQAError("QA-Prozess hat unerwartet Bildartefakte erzeugt")
result = parse_qa_response(completed.stdout)
result["image_path"] = str(image_path)
result["provider_path"] = "Codex built-in vision via --image"
result["codex_exit"] = completed.returncode
return result