feat: add verified image programmatic edits
This commit is contained in:
@@ -41,9 +41,18 @@ genera immagine (agy) → analizza con visione (PASS/FAIL vs requisiti)
|
|||||||
→ rigenera con prompt di correzione se FAIL → ripete fino a maxIterations
|
→ rigenera con prompt di correzione se FAIL → ripete fino a maxIterations
|
||||||
```
|
```
|
||||||
|
|
||||||
Restituisce l'immagine finale + report di verifica (verdetto visione e metriche
|
Restituisce l'immagine candidata finale + report di verifica (verdetto visione,
|
||||||
OpenCV per ogni iterazione). Parametri: `requirements`, `outputDir`,
|
metriche OpenCV ed eventuale edit programmatico per ogni iterazione). Parametri:
|
||||||
`maxIterations` (default 3), `useOpenCV` (default true), `model`.
|
`requirements`, `outputDir`, `maxIterations` (default 3), `useOpenCV` (default true),
|
||||||
|
`editInstructions`, `editScript`, `model`.
|
||||||
|
|
||||||
|
Con `editInstructions` il tool genera una trasformazione deterministica da applicare
|
||||||
|
prima della verifica a ogni iterazione. In alternativa `editScript` accetta uno script
|
||||||
|
Python esplicito con contratto `sys.argv[1]` input e `sys.argv[2]` output. Gli script
|
||||||
|
sono eseguiti con timeout, directory di lavoro temporanea e allowlist di `cv2`,
|
||||||
|
`numpy`, `PIL`, `json`, `sys` e `math`; import, rete, subprocess e accesso arbitrario
|
||||||
|
al filesystem vengono rifiutati. Un edit fallito rende automaticamente non conforme
|
||||||
|
l'iterazione.
|
||||||
|
|
||||||
## Configurazione persistente (`/agy:config`)
|
## Configurazione persistente (`/agy:config`)
|
||||||
|
|
||||||
@@ -149,12 +158,13 @@ conferma con la trascrizione e il piano proposto:
|
|||||||
🎙️ Conferma vocale
|
🎙️ Conferma vocale
|
||||||
Trascrizione: "Aggiorna i docs..."
|
Trascrizione: "Aggiorna i docs..."
|
||||||
📋 Piano proposto: ...
|
📋 Piano proposto: ...
|
||||||
Enter esegui • Esc annulla • E modifica • F12 registra di nuovo
|
Enter esegui • Esc annulla • E modifica • Spazio testo letterale • F12 registra di nuovo
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Enter** → esegue il piano (invia il risultato a pi)
|
- **Enter** → esegue il piano (invia il risultato a pi)
|
||||||
- **Esc** → annulla, nessuna azione
|
- **Esc** → annulla, nessuna azione
|
||||||
- **E** → modifica il testo del piano manualmente
|
- **E** → modifica il testo del piano manualmente
|
||||||
|
- **Spazio** → chiude il popup e inserisce nell'editor la **trascrizione letterale** di quanto dettato, senza inviare il piano proposto
|
||||||
- **F12** → registra di nuovo
|
- **F12** → registra di nuovo
|
||||||
|
|
||||||
Questa modalità evita che l'interpretazione vocale avvii autonomamente loop
|
Questa modalità evita che l'interpretazione vocale avvii autonomamente loop
|
||||||
|
|||||||
@@ -110,12 +110,13 @@ Poi un **overlay TUI** mostra trascrizione + piano e attende la conferma:
|
|||||||
🎙️ Conferma vocale
|
🎙️ Conferma vocale
|
||||||
Trascrizione: "Aggiorna i docs..."
|
Trascrizione: "Aggiorna i docs..."
|
||||||
📋 Piano proposto: ...
|
📋 Piano proposto: ...
|
||||||
Enter esegui • Esc annulla • E modifica • F12 registra di nuovo
|
Enter esegui • Esc annulla • E modifica • Spazio testo letterale • F12 registra di nuovo
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Enter** → esegue (invia il risultato a pi)
|
- **Enter** → esegue (invia il risultato a pi)
|
||||||
- **Esc** → annulla, nessuna azione
|
- **Esc** → annulla, nessuna azione
|
||||||
- **E** → modifica il testo del piano manualmente
|
- **E** → modifica il testo del piano manualmente
|
||||||
|
- **Spazio** → chiude l'overlay e inserisce nell'editor la **trascrizione letterale** di quanto dettato, senza inviare il piano proposto
|
||||||
- **F12** → registra di nuovo
|
- **F12** → registra di nuovo
|
||||||
|
|
||||||
Per tornare all'esecuzione diretta (autonomia piena):
|
Per tornare all'esecuzione diretta (autonomia piena):
|
||||||
|
|||||||
+196
-65
@@ -41,6 +41,7 @@ const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversa
|
|||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 180_000; // 3 min
|
const DEFAULT_TIMEOUT_MS = 180_000; // 3 min
|
||||||
const IMAGE_TIMEOUT_MS = 300_000; // 5 min
|
const IMAGE_TIMEOUT_MS = 300_000; // 5 min
|
||||||
|
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp", ".tiff", ".svg"]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Configurazione persistente (~/.config/agy-pi/config.json)
|
// Configurazione persistente (~/.config/agy-pi/config.json)
|
||||||
@@ -545,8 +546,10 @@ async function generateOpenCVScript(requirements: string): Promise<string> {
|
|||||||
async function runOpenCVScript(script: string, imagePath: string): Promise<string> {
|
async function runOpenCVScript(script: string, imagePath: string): Promise<string> {
|
||||||
if (!script) return "";
|
if (!script) return "";
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execFileAsync("python3", ["-c", script, imagePath], {
|
const { stdout } = await execFileAsync("python3", ["-I", "-c", script, imagePath], {
|
||||||
timeout: 20_000,
|
timeout: 20_000,
|
||||||
|
cwd: os.tmpdir(),
|
||||||
|
env: { PATH: process.env.PATH ?? "", HOME: os.homedir() },
|
||||||
});
|
});
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
} catch {
|
} catch {
|
||||||
@@ -554,6 +557,64 @@ async function runOpenCVScript(script: string, imagePath: string): Promise<strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Genera un edit deterministico opzionale. Lo script usa argv[1] come input e
|
||||||
|
// argv[2] come output e deve stampare un singolo JSON con findings/errors.
|
||||||
|
async function generateProgrammaticEditScript(requirements: string, editInstructions: string): Promise<string> {
|
||||||
|
const prompt =
|
||||||
|
`Write ONE Python script (code only) that applies this deterministic image edit: ${editInstructions}.\n` +
|
||||||
|
`The resulting image must still target these requirements: ${requirements}.\n` +
|
||||||
|
`Contract: read input image from sys.argv[1], write output image to sys.argv[2], and print one JSON object ` +
|
||||||
|
`{"findings": ["..."], "errors": ["..."]}. Use only cv2, numpy, PIL/Pillow, json, sys, math. ` +
|
||||||
|
`Do not import or use os, pathlib, subprocess, socket, network, arbitrary file access, eval, exec, or dynamic imports. ` +
|
||||||
|
`Do not access any path except argv[1] and argv[2]. Preserve quality and dimensions unless explicitly requested. ` +
|
||||||
|
`Return only Python code, without markdown fences.`;
|
||||||
|
return (await geminiText(prompt)).replace(/```python|```/g, "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static rejection is deliberately conservative: generated/user scripts are
|
||||||
|
// still run in an isolated temp cwd with a minimal environment and timeout.
|
||||||
|
function validateProgrammaticEditScript(script: string): string | null {
|
||||||
|
if (!script.trim()) return "Edit script is empty.";
|
||||||
|
if (script.length > 30_000) return "Edit script exceeds the thirty-thousand character limit.";
|
||||||
|
if (/\b(?:os|pathlib|subprocess|socket|requests|urllib|shutil|glob|asyncio|ctypes|pickle)\b/i.test(script))
|
||||||
|
return "Edit script uses a forbidden module or identifier.";
|
||||||
|
if (/\b(?:open|eval|exec|compile|__import__|globals|locals|input|getattr|setattr|vars|dir)\s*\(/i.test(script) || /__/.test(script))
|
||||||
|
return "Edit script uses a forbidden operation.";
|
||||||
|
if (/(?:['\"])(?:\/(?:[^'\"]+)|~\/|\.\.\/|[A-Za-z]:\\)/.test(script))
|
||||||
|
return "Edit script contains a filesystem path; use only sys.argv input/output paths.";
|
||||||
|
const imports = [...script.matchAll(/(?:from|import)\s+([A-Za-z_][\w.]*)/g)].map((m) => m[1].split(".")[0]);
|
||||||
|
const allowed = new Set(["cv2", "numpy", "np", "PIL", "Image", "json", "sys", "math"]);
|
||||||
|
const forbiddenImport = imports.find((name) => !allowed.has(name));
|
||||||
|
return forbiddenImport ? `Edit script imports forbidden module: ${forbiddenImport}.` : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runProgrammaticEditScript(script: string, imagePath: string, outputPath: string): Promise<{ ok: boolean; report: string }> {
|
||||||
|
const validationError = validateProgrammaticEditScript(script);
|
||||||
|
if (validationError) return { ok: false, report: validationError };
|
||||||
|
try {
|
||||||
|
// Prefer bubblewrap when available: no network, temporary /tmp, and a
|
||||||
|
// read-only host view. Static validation remains necessary defense in depth.
|
||||||
|
const useBubblewrap = fs.existsSync("/usr/bin/bwrap");
|
||||||
|
if (useBubblewrap) {
|
||||||
|
fs.closeSync(fs.openSync(outputPath, "a"));
|
||||||
|
}
|
||||||
|
const command = useBubblewrap ? "/usr/bin/bwrap" : "python3";
|
||||||
|
const args = useBubblewrap
|
||||||
|
? ["--ro-bind", "/", "/", "--bind", outputPath, outputPath, "--dev", "/dev", "--proc", "/proc", "--unshare-net", "--chdir", os.tmpdir(), "--", "python3", "-I", "-c", script, imagePath, outputPath]
|
||||||
|
: ["-I", "-c", script, imagePath, outputPath];
|
||||||
|
const { stdout, stderr } = await execFileAsync(command, args, {
|
||||||
|
timeout: 20_000,
|
||||||
|
cwd: os.tmpdir(),
|
||||||
|
env: { PATH: process.env.PATH ?? "", HOME: os.homedir() },
|
||||||
|
maxBuffer: 2 * 1024 * 1024,
|
||||||
|
});
|
||||||
|
if (!fs.existsSync(outputPath)) return { ok: false, report: "Edit script completed without producing the output image." };
|
||||||
|
return { ok: true, report: (stdout || stderr || "").trim() };
|
||||||
|
} catch (error: any) {
|
||||||
|
return { ok: false, report: String(error?.stderr || error?.message || error).slice(0, 2000) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una
|
// Cache anti-ridondanza: evita ricerche web ripetute sullo stesso topic in una
|
||||||
// finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
|
// finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
|
||||||
let lastSearch = { topic: "", time: 0 };
|
let lastSearch = { topic: "", time: 0 };
|
||||||
@@ -1175,10 +1236,11 @@ function getConversationContext(ctx: any, maxEntries = 8): string {
|
|||||||
// ===========================================================================
|
// ===========================================================================
|
||||||
// Opzione 4 — Workflow vocale a 2 fasi: overlay TUI con trascrizione + piano
|
// Opzione 4 — Workflow vocale a 2 fasi: overlay TUI con trascrizione + piano
|
||||||
// e conferma utente prima dell'esecuzione (Enter esegui, Esc annulla, E modifica,
|
// e conferma utente prima dell'esecuzione (Enter esegui, Esc annulla, E modifica,
|
||||||
// F12 registra di nuovo). Riusa l'infrastruttura overlay di /agy:key e /agy:status.
|
// Spazio testo letterale nell'editor senza inviare, F12 registra di nuovo).
|
||||||
|
// Riusa l'infrastruttura overlay di /agy:key e /agy:status.
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
interface VoicePlanDecision {
|
interface VoicePlanDecision {
|
||||||
action: "send" | "cancel" | "record";
|
action: "send" | "cancel" | "record" | "literal";
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1215,7 +1277,7 @@ async function showVoicePlanOverlay(
|
|||||||
"dim",
|
"dim",
|
||||||
editing
|
editing
|
||||||
? "✏️ Modifica: digitando cambia il testo • Enter applica • Esc annulla"
|
? "✏️ Modifica: digitando cambia il testo • Enter applica • Esc annulla"
|
||||||
: "Enter esegui • Esc annulla • E modifica • F12 registra di nuovo",
|
: "Enter esegui • Esc annulla • E modifica • Spazio testo letterale • F12 registra di nuovo",
|
||||||
),
|
),
|
||||||
1,
|
1,
|
||||||
0,
|
0,
|
||||||
@@ -1262,6 +1324,12 @@ async function showVoicePlanOverlay(
|
|||||||
tui.requestRender();
|
tui.requestRender();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// Spazio: chiude l'overlay e inserisce la TRASCRIZIONE LETTERALE
|
||||||
|
// nell'editor, senza inviare il prompt proposto.
|
||||||
|
if (matchesKey(data, Key.space)) {
|
||||||
|
done({ action: "literal", text });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (matchesKey(data, Key.f12)) {
|
if (matchesKey(data, Key.f12)) {
|
||||||
done({ action: "record", text });
|
done({ action: "record", text });
|
||||||
return;
|
return;
|
||||||
@@ -2279,15 +2347,14 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy",
|
name: "agy",
|
||||||
label: "agy (Antigravity subagent)",
|
label: "agy (Antigravity subagent)",
|
||||||
description:
|
description:
|
||||||
"Delega un task generico a Google Antigravity CLI (agy), che usa Gemini multimodale. " +
|
"Use Google Antigravity CLI for text-only multi-turn reasoning or general chat. " +
|
||||||
"Utile per conversazioni di ragionamento multi-turno, generazione immagini e analisi di file. " +
|
"Do NOT use for image generation/editing, image analysis, audio transcription, video analysis, or text-to-speech: " +
|
||||||
"Per task specializzati usa agy_generate, agy_edit, agy_inpaint, agy_style_transfer, agy_compose, " +
|
"use the dedicated agy_* tool instead.",
|
||||||
"agy_analyze, agy_transcribe, agy_video.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
prompt: Type.String({ description: "Il task/prompt da dare a agy." }),
|
prompt: Type.String({ description: "Il task/prompt da dare a agy." }),
|
||||||
mode: Type.Optional(
|
mode: Type.Optional(
|
||||||
Type.Union([Type.Literal("chat"), Type.Literal("image"), Type.Literal("analyze")], {
|
Type.Union([Type.Literal("chat"), Type.Literal("image"), Type.Literal("analyze")], {
|
||||||
description: "chat (default) | image | analyze",
|
description: "chat only (default); image/analyze are legacy modes—use dedicated tools.",
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
model: Type.Optional(Type.String({ description: "Modello agy (es. 'Gemini 3.1 Pro (High)')." })),
|
model: Type.Optional(Type.String({ description: "Modello agy (es. 'Gemini 3.1 Pro (High)')." })),
|
||||||
@@ -2296,7 +2363,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
),
|
),
|
||||||
newConversation: Type.Optional(Type.Boolean({ description: "true per forzare una nuova conversazione." })),
|
newConversation: Type.Optional(Type.Boolean({ description: "true per forzare una nuova conversazione." })),
|
||||||
addDir: Type.Optional(Type.String({ description: "Cartella da aggiungere al workspace agy." })),
|
addDir: Type.Optional(Type.String({ description: "Cartella da aggiungere al workspace agy." })),
|
||||||
filePath: Type.Optional(Type.String({ description: "File (immagine/audio/video) da analizzare." })),
|
filePath: Type.Optional(Type.String({ description: "Optional attachment for legacy use; prefer the dedicated image, audio, or video tool." })),
|
||||||
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions." })),
|
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions." })),
|
||||||
injectContext: Type.Optional(
|
injectContext: Type.Optional(
|
||||||
Type.Boolean({ description: "Inietta contesto ambiente/sessione/web nel prompt (default true)." }),
|
Type.Boolean({ description: "Inietta contesto ambiente/sessione/web nel prompt (default true)." }),
|
||||||
@@ -2340,8 +2407,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_generate",
|
name: "agy_generate",
|
||||||
label: "agy generate image",
|
label: "agy generate image",
|
||||||
description:
|
description:
|
||||||
"Genera un'immagine da zero con parametri strutturati (soggetto, azione, luogo, composizione, stile, luce, formato). " +
|
"Generate a new image from a structured subject, action, location, composition, style, lighting, and format. " +
|
||||||
"Usa la formula narrativa [Soggetto]+[Azione]+[Luogo]+[Composizione]+[Stile] per il massimo controllo.",
|
"Use ONLY for image generation from scratch, not editing or image analysis.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
subject: Type.String({ description: "Soggetto: chi/cosa è nell'immagine. Sii specifico." }),
|
subject: Type.String({ description: "Soggetto: chi/cosa è nell'immagine. Sii specifico." }),
|
||||||
action: Type.Optional(Type.String({ description: "Azione: cosa sta succedendo." })),
|
action: Type.Optional(Type.String({ description: "Azione: cosa sta succedendo." })),
|
||||||
@@ -2395,9 +2462,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_edit",
|
name: "agy_edit",
|
||||||
label: "agy edit image",
|
label: "agy edit image",
|
||||||
description:
|
description:
|
||||||
"Modifica un'immagine esistente in modo controllato usando la formula Keep+Change+Add+Render. " +
|
"Edit an existing image using Keep, Change, Add, and Render. " +
|
||||||
"Specifica cosa mantenere invariato, cosa cambiare, cosa aggiungere e il target di resa. " +
|
"Use ONLY for controlled image edits; use agy_inpaint for changing one specific region.",
|
||||||
"Un solo cambiamento per turno per il massimo controllo.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
baseImage: Type.String({ description: "Percorso dell'immagine base da modificare." }),
|
baseImage: Type.String({ description: "Percorso dell'immagine base da modificare." }),
|
||||||
keep: Type.String({ description: "Cosa mantenere invariato (es. 'soggetto, posa, illuminazione, composizione')." }),
|
keep: Type.String({ description: "Cosa mantenere invariato (es. 'soggetto, posa, illuminazione, composizione')." }),
|
||||||
@@ -2445,8 +2511,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_inpaint",
|
name: "agy_inpaint",
|
||||||
label: "agy inpaint (edit zona specifica)",
|
label: "agy inpaint (edit zona specifica)",
|
||||||
description:
|
description:
|
||||||
"Modifica SOLO una parte specifica dell'immagine lasciando il resto intatto (inpainting / semantic masking). " +
|
"Change ONLY one specified region or element of an image and preserve everything else. " +
|
||||||
"Definisci l'elemento target e la sostituzione; tutto il resto resta identico.",
|
"Use for localized inpainting; use agy_edit for broader controlled edits.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
|
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
|
||||||
target: Type.String({ description: "L'elemento specifico da modificare (es. 'la maglietta del soggetto')." }),
|
target: Type.String({ description: "L'elemento specifico da modificare (es. 'la maglietta del soggetto')." }),
|
||||||
@@ -2490,8 +2556,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_style_transfer",
|
name: "agy_style_transfer",
|
||||||
label: "agy style transfer",
|
label: "agy style transfer",
|
||||||
description:
|
description:
|
||||||
"Applica uno stile artistico a un'immagine preservando il contenuto e la composizione originali " +
|
"Apply an artistic style to an existing image while preserving its content and composition. " +
|
||||||
"(style transfer). Es. trasformare una foto in un dipinto Van Gogh.",
|
"Use ONLY for style transfer, not general image edits or analysis.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
|
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
|
||||||
style: Type.String({ description: "Lo stile da applicare (es. 'pittura Van Gogh', 'architectural drawing', 'film noir')." }),
|
style: Type.String({ description: "Lo stile da applicare (es. 'pittura Van Gogh', 'architectural drawing', 'film noir')." }),
|
||||||
@@ -2534,8 +2600,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_compose",
|
name: "agy_compose",
|
||||||
label: "agy compose (combina immagini)",
|
label: "agy compose (combina immagini)",
|
||||||
description:
|
description:
|
||||||
"Combina più immagini in una nuova composizione (multi-image composition / fusion). " +
|
"Combine two or more existing images into one new composition. " +
|
||||||
"Specifica il ruolo di ciascuna immagine e l'istruzione di fusione.",
|
"Use ONLY for multi-image fusion; use agy_edit for editing one image.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
images: Type.Array(Type.String({ description: "Percorsi delle immagini da combinare." }), {
|
images: Type.Array(Type.String({ description: "Percorsi delle immagini da combinare." }), {
|
||||||
description: "Lista di percorsi immagine (fino a ~6-14).",
|
description: "Lista di percorsi immagine (fino a ~6-14).",
|
||||||
@@ -2579,8 +2645,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_character",
|
name: "agy_character",
|
||||||
label: "agy character consistency",
|
label: "agy character consistency",
|
||||||
description:
|
description:
|
||||||
"Mantiene la consistenza di un personaggio/oggetto attraverso più generazioni o edit. " +
|
"Generate or edit an image while preserving a character or object's identity from a reference image. " +
|
||||||
"Usa un'immagine di riferimento e un token di consistenza per preservare le caratteristiche.",
|
"Use ONLY when identity consistency is required.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
referenceImage: Type.String({ description: "Percorso dell'immagine di riferimento del personaggio." }),
|
referenceImage: Type.String({ description: "Percorso dell'immagine di riferimento del personaggio." }),
|
||||||
name: Type.String({ description: "Nome/token del personaggio (es. 'Maya-giacca-blu')." }),
|
name: Type.String({ description: "Nome/token del personaggio (es. 'Maya-giacca-blu')." }),
|
||||||
@@ -2616,39 +2682,56 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// TOOL: agy_analyze — analisi di un file (immagine/audio/video)
|
// TOOL: agy_analyze — fallback per analisi immagini quando Pi è text-only
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
pi.registerTool({
|
pi.registerTool({
|
||||||
name: "agy_analyze",
|
name: "agy_analyze",
|
||||||
label: "agy analyze file",
|
label: "agy analyze image fallback",
|
||||||
description:
|
description:
|
||||||
"Analizza un file (immagine, audio o video) con Gemini multimodale. " +
|
"Analyze a local image with Gemini ONLY as a fallback when the current Pi model cannot inspect images natively. " +
|
||||||
"Per immagini: descrizione, OCR, analisi. Per audio: contenuto, trascrizione. Per video: scene, contenuto.",
|
"Use for image understanding or OCR. Do NOT use for audio (use agy_transcribe), video (use agy_video), " +
|
||||||
|
"image generation/editing, or images the current model can already see.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
filePath: Type.String({ description: "Percorso del file da analizzare." }),
|
filePath: Type.String({ description: "Local image path only: PNG, JPG, JPEG, GIF, WebP, BMP, TIFF, or SVG. Never audio or video." }),
|
||||||
question: Type.Optional(Type.String({ description: "Domanda specifica sull'analisi." })),
|
question: Type.Optional(Type.String({ description: "Specific question about the image." })),
|
||||||
model: Type.Optional(Type.String({ description: "Modello agy." })),
|
model: Type.Optional(Type.String({ description: "Gemini model used for fallback analysis." })),
|
||||||
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions (necessario per audio/video)." })),
|
yolo: Type.Optional(Type.Boolean({ description: "true to pass --dangerously-skip-permissions; use only when explicitly required." })),
|
||||||
}),
|
}),
|
||||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||||
const p = params as any;
|
const p = params as any;
|
||||||
|
const filePath = String(p.filePath ?? "").trim();
|
||||||
|
const ext = path.extname(filePath).toLowerCase();
|
||||||
|
if (!IMAGE_EXTENSIONS.has(ext)) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "agy_analyze accepts images only. Use agy_transcribe for audio or agy_video for video." }],
|
||||||
|
details: { error: "unsupported_media_type", filePath },
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (ctx.model?.input?.includes("image")) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "The current Pi model supports native image input; agy_analyze is not needed." }],
|
||||||
|
details: { error: "native_vision_available", model: ctx.model.id },
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
const prompt = p.question
|
const prompt = p.question
|
||||||
? `Analizza il file al percorso ${p.filePath}. ${p.question}`
|
? `Analyze the image at ${filePath}. ${p.question}`
|
||||||
: `Analizza il file al percorso ${p.filePath} e descrivilo in dettaglio.`;
|
: `Analyze the image at ${filePath} and describe it in detail.`;
|
||||||
|
|
||||||
toolUpdate(onUpdate, "agy_analyze: analisi file...");
|
toolUpdate(onUpdate, "agy_analyze: analisi immagine...");
|
||||||
const res = await executeAgy({
|
const res = await executeAgy({
|
||||||
prompt,
|
prompt,
|
||||||
mode: "analyze",
|
mode: "analyze",
|
||||||
model: p.model,
|
model: p.model,
|
||||||
stateless: true,
|
stateless: true,
|
||||||
filePaths: [p.filePath],
|
filePaths: [filePath],
|
||||||
yolo: p.yolo,
|
yolo: p.yolo,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
content: [{ type: "text", text: res.text }],
|
content: [{ type: "text", text: res.text }],
|
||||||
details: { filePath: p.filePath, exitCode: res.exitCode },
|
details: { filePath, exitCode: res.exitCode },
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2662,21 +2745,31 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_create_verified",
|
name: "agy_create_verified",
|
||||||
label: "agy create verified image",
|
label: "agy create verified image",
|
||||||
description:
|
description:
|
||||||
"Genera un'immagine che soddisfi i requisiti, la verifica iterativamente con visione " +
|
"Generate an image, optionally apply a deterministic Python image edit, inspect it, and regenerate it until the requirements pass or the iteration limit is reached. " +
|
||||||
"testuale e metriche OpenCV, e la rigenera finché non rispetta i requisiti (o fino a un limite). " +
|
"Use ONLY when iterative visual verification or deterministic correction is required; use agy_generate for one-shot generation.",
|
||||||
"Ideale quando l'orchestratore non ha visione e serve un'immagine conforme a specifiche precise.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
requirements: Type.String({ description: "Requisiti precisi che l'immagine deve soddisfare." }),
|
requirements: Type.String({ description: "Requisiti precisi che l'immagine deve soddisfare." }),
|
||||||
outputDir: Type.Optional(Type.String({ description: "Cartella dove salvare l'immagine finale." })),
|
outputDir: Type.Optional(Type.String({ description: "Cartella dove salvare l'immagine finale." })),
|
||||||
maxIterations: Type.Optional(Type.Number({ description: "Limite massimo di iterazioni (default 3, max 5)." })),
|
maxIterations: Type.Optional(Type.Number({ description: "Maximum iterations (default 3, max 5)." })),
|
||||||
useOpenCV: Type.Optional(Type.Boolean({ description: "Esegui metriche OpenCV oggettive (default true)." })),
|
useOpenCV: Type.Optional(Type.Boolean({ description: "Run objective OpenCV checks (default true)." })),
|
||||||
model: Type.Optional(Type.String({ description: "Modello agy." })),
|
editInstructions: Type.Optional(Type.String({ description: "Optional deterministic edit to apply each iteration before verification; use precise, measurable instructions." })),
|
||||||
|
editScript: Type.Optional(Type.String({ description: "Optional Python script for a deterministic edit. Must read argv[1], write argv[2], and use only the allowlisted image libraries." })),
|
||||||
|
model: Type.Optional(Type.String({ description: "Antigravity model." })),
|
||||||
}),
|
}),
|
||||||
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
||||||
const p = params as any;
|
const p = params as any;
|
||||||
const requirements = String(p.requirements ?? "").trim();
|
const requirements = String(p.requirements ?? "").trim();
|
||||||
const maxIter = Math.min(Math.max(Number(p.maxIterations ?? 3) || 3, 1), 5);
|
const maxIter = Math.min(Math.max(Number(p.maxIterations ?? 3) || 3, 1), 5);
|
||||||
const useCV = p.useOpenCV ?? true;
|
const useCV = p.useOpenCV ?? true;
|
||||||
|
const editInstructions = String(p.editInstructions ?? "").trim();
|
||||||
|
const suppliedEditScript = String(p.editScript ?? "").trim();
|
||||||
|
if (editInstructions && suppliedEditScript) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: "Provide either editInstructions or editScript, not both." }],
|
||||||
|
details: { error: "conflicting_edit_inputs" },
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
toolUpdate(onUpdate, "agy_create_verified: avvio loop di creazione verificata...");
|
toolUpdate(onUpdate, "agy_create_verified: avvio loop di creazione verificata...");
|
||||||
|
|
||||||
@@ -2688,6 +2781,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
imagePath: string;
|
imagePath: string;
|
||||||
verdict: string;
|
verdict: string;
|
||||||
opencv: string;
|
opencv: string;
|
||||||
|
programmaticEdit: string;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
let finalText = "";
|
let finalText = "";
|
||||||
|
|
||||||
@@ -2698,6 +2792,19 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
toolUpdate(onUpdate, "agy_create_verified: generazione script OpenCV dinamico...");
|
toolUpdate(onUpdate, "agy_create_verified: generazione script OpenCV dinamico...");
|
||||||
cvScript = await generateOpenCVScript(requirements);
|
cvScript = await generateOpenCVScript(requirements);
|
||||||
}
|
}
|
||||||
|
let editScript = suppliedEditScript;
|
||||||
|
if (editInstructions) {
|
||||||
|
toolUpdate(onUpdate, "agy_create_verified: generazione edit deterministico...");
|
||||||
|
editScript = await generateProgrammaticEditScript(requirements, editInstructions);
|
||||||
|
}
|
||||||
|
const editValidationError = editScript ? validateProgrammaticEditScript(editScript) : null;
|
||||||
|
if (editValidationError) {
|
||||||
|
return {
|
||||||
|
content: [{ type: "text", text: `Edit programmatico rifiutato: ${editValidationError}` }],
|
||||||
|
details: { error: "invalid_programmatic_edit", validation: editValidationError },
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
for (let iter = 1; iter <= maxIter; iter++) {
|
for (let iter = 1; iter <= maxIter; iter++) {
|
||||||
toolUpdate(
|
toolUpdate(
|
||||||
@@ -2725,10 +2832,24 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
currentImage = imgPath;
|
currentImage = imgPath;
|
||||||
|
let programmaticEdit = "";
|
||||||
|
let editPassed = true;
|
||||||
|
if (editScript) {
|
||||||
|
const editRoot = p.outputDir ? path.resolve(String(p.outputDir)) : os.tmpdir();
|
||||||
|
try { fs.mkdirSync(editRoot, { recursive: true }); } catch { /* il runner segnalerà l'errore */ }
|
||||||
|
const editedPath = path.join(editRoot, `agy-verified-edit-${process.pid}-${iter}-${Math.random().toString(36).slice(2)}.png`);
|
||||||
|
toolUpdate(onUpdate, `agy_create_verified: edit programmatico ${iter}/${maxIter}...`);
|
||||||
|
const editResult = await runProgrammaticEditScript(editScript, imgPath, editedPath);
|
||||||
|
programmaticEdit = editResult.report;
|
||||||
|
editPassed = editResult.ok;
|
||||||
|
if (editPassed) currentImage = editedPath;
|
||||||
|
else lastIssues = `Edit programmatico fallito: ${programmaticEdit}`;
|
||||||
|
}
|
||||||
|
const candidateImage = currentImage!;
|
||||||
|
|
||||||
// 2) Analisi visione (giudice): PASS/FAIL + problemi rispetto ai requisiti
|
// 2) Analisi visione (giudice): PASS/FAIL + problemi rispetto ai requisiti
|
||||||
const judgePrompt =
|
const judgePrompt =
|
||||||
`Analizza il file al percorso ${imgPath}. Requisiti richiesti: ${requirements}. ` +
|
`Analizza il file al percorso ${candidateImage}. Requisiti richiesti: ${requirements}. ` +
|
||||||
`Verifica se l'immagine li rispetta. Rispondi iniziando con \"PASS:\" o \"FAIL:\", ` +
|
`Verifica se l'immagine li rispetta. Rispondi iniziando con \"PASS:\" o \"FAIL:\", ` +
|
||||||
`poi elenca sinteticamente (max 3 punti) le deviazioni rispetto ai requisiti in caso di FAIL.`;
|
`poi elenca sinteticamente (max 3 punti) le deviazioni rispetto ai requisiti in caso di FAIL.`;
|
||||||
const judgeRes = await executeAgy({
|
const judgeRes = await executeAgy({
|
||||||
@@ -2736,7 +2857,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
mode: "analyze",
|
mode: "analyze",
|
||||||
model: p.model,
|
model: p.model,
|
||||||
stateless: true,
|
stateless: true,
|
||||||
filePaths: [imgPath],
|
filePaths: [candidateImage],
|
||||||
yolo: true,
|
yolo: true,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
@@ -2748,7 +2869,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
let cvMetrics = "";
|
let cvMetrics = "";
|
||||||
let ocvPass: boolean | null = null;
|
let ocvPass: boolean | null = null;
|
||||||
if (useCV && cvScript) {
|
if (useCV && cvScript) {
|
||||||
const out = await runOpenCVScript(cvScript, imgPath);
|
const out = await runOpenCVScript(cvScript, candidateImage);
|
||||||
cvMetrics = out;
|
cvMetrics = out;
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(out);
|
const parsed = JSON.parse(out);
|
||||||
@@ -2766,20 +2887,20 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Conforme solo se visione PASS e (se disponibile) anche OpenCV PASS
|
// Conforme solo se visione PASS e (se disponibile) anche OpenCV PASS
|
||||||
const pass = visionPass && (ocvPass === null || ocvPass === true);
|
const pass = editPassed && visionPass && (ocvPass === null || ocvPass === true);
|
||||||
|
|
||||||
report.push({ iteration: iter, pass, imagePath: imgPath, verdict, opencv: cvMetrics });
|
report.push({ iteration: iter, pass, imagePath: candidateImage, verdict, opencv: cvMetrics, programmaticEdit });
|
||||||
|
|
||||||
if (pass) {
|
if (pass) {
|
||||||
finalText =
|
finalText =
|
||||||
`✅ Immagine verificata dopo ${iter} iterazione/i.\nPercorso: ${imgPath}` +
|
`✅ Immagine verificata dopo ${iter} iterazione/i.\nPercorso: ${candidateImage}` +
|
||||||
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
|
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
|
||||||
`\nVerdetto visione:\n${verdict}`;
|
`\nVerdetto visione:\n${verdict}`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (iter === maxIter) {
|
if (iter === maxIter) {
|
||||||
finalText =
|
finalText =
|
||||||
`⚠️ Requisiti non soddisfatti dopo ${maxIter} iterazioni.\nPercorso: ${imgPath}` +
|
`⚠️ Requisiti non soddisfatti dopo ${maxIter} iterazioni.\nPercorso: ${candidateImage}` +
|
||||||
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
|
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
|
||||||
`\nUltimo verdetto visione:\n${verdict}`;
|
`\nUltimo verdetto visione:\n${verdict}`;
|
||||||
}
|
}
|
||||||
@@ -2803,8 +2924,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_transcribe",
|
name: "agy_transcribe",
|
||||||
label: "agy transcribe audio",
|
label: "agy transcribe audio",
|
||||||
description:
|
description:
|
||||||
"Trascrive il contenuto di un file audio (voce, discorso) in testo usando la Gemini API diretta " +
|
"Transcribe speech or other audio into text with the direct Gemini API. " +
|
||||||
"(veloce e affidabile; agy CLI non supporta audio). Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
|
"Use ONLY for audio; use agy_analyze for images and agy_video for video.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }),
|
filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }),
|
||||||
language: Type.Optional(Type.String({ description: "Lingua del contenuto (es. 'italiano', 'english')." })),
|
language: Type.Optional(Type.String({ description: "Lingua del contenuto (es. 'italiano', 'english')." })),
|
||||||
@@ -2836,8 +2957,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_video",
|
name: "agy_video",
|
||||||
label: "agy analyze video",
|
label: "agy analyze video",
|
||||||
description:
|
description:
|
||||||
"Analizza un file video: descrive scene, contenuto, codec, risoluzione, tracce audio. " +
|
"Analyze a video: scenes, content, codec, resolution, and audio tracks. " +
|
||||||
"Richiede --yolo (auto-approva gli strumenti).",
|
"Use ONLY for video; use agy_analyze for images and agy_transcribe for audio-only files.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
filePath: Type.String({ description: "Percorso del file video (mp4, mov, ecc.)." }),
|
filePath: Type.String({ description: "Percorso del file video (mp4, mov, ecc.)." }),
|
||||||
question: Type.Optional(Type.String({ description: "Domanda specifica sul video." })),
|
question: Type.Optional(Type.String({ description: "Domanda specifica sul video." })),
|
||||||
@@ -2873,9 +2994,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_tts",
|
name: "agy_tts",
|
||||||
label: "agy TTS (text to speech)",
|
label: "agy TTS (text to speech)",
|
||||||
description:
|
description:
|
||||||
"Converte un testo in audio (TTS) usando la Gemini API (gemini-2.5-flash-preview-tts) e lo riproduce. " +
|
"Convert text to an audio file with Gemini TTS. Use ONLY when an audio file or playback is explicitly requested; " +
|
||||||
"Utile per notifiche vocali, sintesi parlate e aggiornamenti di stato. " +
|
"for normal voice output use the canonical tts_speak tool.",
|
||||||
"Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
text: Type.String({ description: "Il testo da pronunciare." }),
|
text: Type.String({ description: "Il testo da pronunciare." }),
|
||||||
play: Type.Optional(Type.Boolean({ description: "true per riprodurre l'audio (default: true)." })),
|
play: Type.Optional(Type.Boolean({ description: "true per riprodurre l'audio (default: true)." })),
|
||||||
@@ -2924,7 +3044,8 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "agy_conversation",
|
name: "agy_conversation",
|
||||||
label: "agy conversation state",
|
label: "agy conversation state",
|
||||||
description:
|
description:
|
||||||
"Gestisce lo stato della conversazione agy: mostra l'ID corrente, elenca le conversazioni, o azzera lo stato.",
|
"Administrative control for agy conversation state: show the current ID, list stored conversations, or reset state. " +
|
||||||
|
"Use only when the user explicitly requests conversation-state management.",
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
action: Type.Union(
|
action: Type.Union(
|
||||||
[Type.Literal("id"), Type.Literal("list"), Type.Literal("reset")],
|
[Type.Literal("id"), Type.Literal("list"), Type.Literal("reset")],
|
||||||
@@ -2972,12 +3093,9 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
name: "antigravity_chat",
|
name: "antigravity_chat",
|
||||||
label: "antigravity direct protocol chat",
|
label: "antigravity direct protocol chat",
|
||||||
description:
|
description:
|
||||||
"Chatta direttamente con i server di inferenza di Antigravity via protocollo cloudcode-pa (v1internal), " +
|
"Send one text or multimodal request directly to Antigravity's cloudcode-pa gateway, without the agy subprocess. " +
|
||||||
"usando il token OAuth dell'account (nessun subprocess agy, streaming SSE nativo). " +
|
"Use ONLY when direct gateway access is explicitly needed; for normal delegation use agy. " +
|
||||||
"Modelli del gateway: gemini-3.5-flash (default, economico), gemini-3.6-flash-medium, gemini-3.6-flash-high, " +
|
"Respect per-model quota and send requests serially; do not use for batch automation.",
|
||||||
"gemini-3.1-pro, gemini-3.1-pro-high, claude-sonnet-4.6, claude-opus-4.6, gpt-oss-120b. " +
|
|
||||||
"ATTENZIONE: canale account con quota per-modello e ToS Google (l'uso automatizzato massiccio può far scattare re-auth/ban). " +
|
|
||||||
"Usare con moderazione: una richiesta alla volta, niente batch automatici, preferire gemini-3.5-flash per task semplici.",
|
|
||||||
parameters: Type.Object({
|
parameters: Type.Object({
|
||||||
prompt: Type.String({ description: "Il messaggio da inviare al modello" }),
|
prompt: Type.String({ description: "Il messaggio da inviare al modello" }),
|
||||||
model: Type.Optional(
|
model: Type.Optional(
|
||||||
@@ -3231,6 +3349,19 @@ export default function agyExtension(pi: ExtensionAPI) {
|
|||||||
ctx.ui.notify("Registra di nuovo con F12.", "info");
|
ctx.ui.notify("Registra di nuovo con F12.", "info");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (decision.action === "literal") {
|
||||||
|
// Non invia il piano proposto: inserisce la dettatura letterale
|
||||||
|
// nell'area del prompt (ripristinando l'eventuale testo editor consumato).
|
||||||
|
const literal = editorText ? `${editorText}\n\n${transcript}` : transcript;
|
||||||
|
try {
|
||||||
|
ctx.ui.pasteToEditor?.(literal);
|
||||||
|
} catch {
|
||||||
|
/* ignora */
|
||||||
|
}
|
||||||
|
ctx.ui.notify("Trascrizione letterale inserita nel prompt (non inviata)", "info");
|
||||||
|
playSound("done");
|
||||||
|
return;
|
||||||
|
}
|
||||||
finalText = decision.text;
|
finalText = decision.text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -6,7 +6,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"pi": {
|
"pi": {
|
||||||
"extensions": ["./extensions"]
|
"extensions": ["./extensions"],
|
||||||
|
"skills": ["./skills"]
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@earendil-works/pi-coding-agent": "*",
|
"@earendil-works/pi-coding-agent": "*",
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: agy-create-verified
|
||||||
|
description: "Operational guide for iterative image generation with visual and deterministic programmatic verification and edits."
|
||||||
|
---
|
||||||
|
|
||||||
|
# Verified image workflow
|
||||||
|
|
||||||
|
Use `agy_create_verified` only when a generated image must be checked and possibly corrected across iterations. Use `agy_generate` for one-shot or purely aesthetic work.
|
||||||
|
|
||||||
|
## Prepare the request
|
||||||
|
|
||||||
|
Separate requirements into:
|
||||||
|
|
||||||
|
- **Semantic:** subject, pose, style, mood, scene.
|
||||||
|
- **Layout:** aspect ratio, placement, count, alignment, spacing.
|
||||||
|
- **Measurable:** dimensions, dominant colors, geometry, contrast, text regions.
|
||||||
|
|
||||||
|
Write acceptance criteria as explicit pass/fail statements. OpenCV can verify measurable criteria; Gemini judges semantic criteria. Do not claim a subjective requirement is objectively verified.
|
||||||
|
|
||||||
|
## Deterministic edits
|
||||||
|
|
||||||
|
Use `editInstructions` for a repeatable transformation such as resize, crop, color correction, thresholding, masking, watermark placement, or geometric cleanup. Use `editScript` only for an explicit trusted script.
|
||||||
|
|
||||||
|
The edit script contract is:
|
||||||
|
|
||||||
|
- read input from `sys.argv[1]`;
|
||||||
|
- write only the output image to `sys.argv[2]`;
|
||||||
|
- use only `cv2`, `numpy`, `PIL`, `json`, `sys`, and `math`;
|
||||||
|
- do not use network, subprocesses, dynamic imports, or arbitrary filesystem access.
|
||||||
|
|
||||||
|
The programmatic edit runs before visual/OpenCV verification, so its output—not the raw generated image—is the candidate for the next iteration.
|
||||||
|
|
||||||
|
## Budget and interpretation
|
||||||
|
|
||||||
|
- Use `maxIterations=2` for simple corrections, 3 normally, and 4–5 only when justified.
|
||||||
|
- Keep `useOpenCV=true` when at least one criterion is measurable; otherwise it adds cost without reliable evidence.
|
||||||
|
- A `PASS` is valid only for the final candidate and available checks.
|
||||||
|
- A `FAIL` result is the latest candidate, not a compliant image. Report unmet criteria explicitly.
|
||||||
|
- Inspect the per-iteration report for edit errors, unchanged progress, regressions, and failed OpenCV output before presenting the result as verified.
|
||||||
Reference in New Issue
Block a user