Explorer
/proc/4/root/tmp/codex_image_qa.py
← Zurück ↓ Download
"""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