feat: add verified image programmatic edits

This commit is contained in:
enne2
2026-08-28 18:51:37 +02:00
parent ace461ff59
commit cde5aaa9b7
5 changed files with 253 additions and 71 deletions
+14 -4
View File
@@ -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
```
Restituisce l'immagine finale + report di verifica (verdetto visione e metriche
OpenCV per ogni iterazione). Parametri: `requirements`, `outputDir`,
`maxIterations` (default 3), `useOpenCV` (default true), `model`.
Restituisce l'immagine candidata finale + report di verifica (verdetto visione,
metriche OpenCV ed eventuale edit programmatico per ogni iterazione). Parametri:
`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`)
@@ -149,12 +158,13 @@ conferma con la trascrizione e il piano proposto:
🎙️ Conferma vocale
Trascrizione: "Aggiorna i docs..."
📋 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)
- **Esc** → annulla, nessuna azione
- **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
Questa modalità evita che l'interpretazione vocale avvii autonomamente loop
+2 -1
View File
@@ -110,12 +110,13 @@ Poi un **overlay TUI** mostra trascrizione + piano e attende la conferma:
🎙️ Conferma vocale
Trascrizione: "Aggiorna i docs..."
📋 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)
- **Esc** → annulla, nessuna azione
- **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
Per tornare all'esecuzione diretta (autonomia piena):
+196 -65
View File
@@ -41,6 +41,7 @@ const CONV_DIR = path.join(os.homedir(), ".gemini", "antigravity-cli", "conversa
const DEFAULT_TIMEOUT_MS = 180_000; // 3 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)
@@ -545,8 +546,10 @@ async function generateOpenCVScript(requirements: string): Promise<string> {
async function runOpenCVScript(script: string, imagePath: string): Promise<string> {
if (!script) return "";
try {
const { stdout } = await execFileAsync("python3", ["-c", script, imagePath], {
const { stdout } = await execFileAsync("python3", ["-I", "-c", script, imagePath], {
timeout: 20_000,
cwd: os.tmpdir(),
env: { PATH: process.env.PATH ?? "", HOME: os.homedir() },
});
return stdout.trim();
} 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
// finestra breve (il multi-step reasoning lancia prompt simili in sequenza).
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
// 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 {
action: "send" | "cancel" | "record";
action: "send" | "cancel" | "record" | "literal";
text: string;
}
@@ -1215,7 +1277,7 @@ async function showVoicePlanOverlay(
"dim",
editing
? "✏️ 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,
0,
@@ -1262,6 +1324,12 @@ async function showVoicePlanOverlay(
tui.requestRender();
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)) {
done({ action: "record", text });
return;
@@ -2279,15 +2347,14 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "agy",
label: "agy (Antigravity subagent)",
description:
"Delega un task generico a Google Antigravity CLI (agy), che usa Gemini multimodale. " +
"Utile per conversazioni di ragionamento multi-turno, generazione immagini e analisi di file. " +
"Per task specializzati usa agy_generate, agy_edit, agy_inpaint, agy_style_transfer, agy_compose, " +
"agy_analyze, agy_transcribe, agy_video.",
"Use Google Antigravity CLI for text-only multi-turn reasoning or general chat. " +
"Do NOT use for image generation/editing, image analysis, audio transcription, video analysis, or text-to-speech: " +
"use the dedicated agy_* tool instead.",
parameters: Type.Object({
prompt: Type.String({ description: "Il task/prompt da dare a agy." }),
mode: Type.Optional(
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)')." })),
@@ -2296,7 +2363,7 @@ export default function agyExtension(pi: ExtensionAPI) {
),
newConversation: Type.Optional(Type.Boolean({ description: "true per forzare una nuova conversazione." })),
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." })),
injectContext: Type.Optional(
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",
label: "agy generate image",
description:
"Genera un'immagine da zero con parametri strutturati (soggetto, azione, luogo, composizione, stile, luce, formato). " +
"Usa la formula narrativa [Soggetto]+[Azione]+[Luogo]+[Composizione]+[Stile] per il massimo controllo.",
"Generate a new image from a structured subject, action, location, composition, style, lighting, and format. " +
"Use ONLY for image generation from scratch, not editing or image analysis.",
parameters: Type.Object({
subject: Type.String({ description: "Soggetto: chi/cosa è nell'immagine. Sii specifico." }),
action: Type.Optional(Type.String({ description: "Azione: cosa sta succedendo." })),
@@ -2395,9 +2462,8 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "agy_edit",
label: "agy edit image",
description:
"Modifica un'immagine esistente in modo controllato usando la formula Keep+Change+Add+Render. " +
"Specifica cosa mantenere invariato, cosa cambiare, cosa aggiungere e il target di resa. " +
"Un solo cambiamento per turno per il massimo controllo.",
"Edit an existing image using Keep, Change, Add, and Render. " +
"Use ONLY for controlled image edits; use agy_inpaint for changing one specific region.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base da modificare." }),
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",
label: "agy inpaint (edit zona specifica)",
description:
"Modifica SOLO una parte specifica dell'immagine lasciando il resto intatto (inpainting / semantic masking). " +
"Definisci l'elemento target e la sostituzione; tutto il resto resta identico.",
"Change ONLY one specified region or element of an image and preserve everything else. " +
"Use for localized inpainting; use agy_edit for broader controlled edits.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
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",
label: "agy style transfer",
description:
"Applica uno stile artistico a un'immagine preservando il contenuto e la composizione originali " +
"(style transfer). Es. trasformare una foto in un dipinto Van Gogh.",
"Apply an artistic style to an existing image while preserving its content and composition. " +
"Use ONLY for style transfer, not general image edits or analysis.",
parameters: Type.Object({
baseImage: Type.String({ description: "Percorso dell'immagine base." }),
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",
label: "agy compose (combina immagini)",
description:
"Combina più immagini in una nuova composizione (multi-image composition / fusion). " +
"Specifica il ruolo di ciascuna immagine e l'istruzione di fusione.",
"Combine two or more existing images into one new composition. " +
"Use ONLY for multi-image fusion; use agy_edit for editing one image.",
parameters: Type.Object({
images: Type.Array(Type.String({ description: "Percorsi delle immagini da combinare." }), {
description: "Lista di percorsi immagine (fino a ~6-14).",
@@ -2579,8 +2645,8 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "agy_character",
label: "agy character consistency",
description:
"Mantiene la consistenza di un personaggio/oggetto attraverso più generazioni o edit. " +
"Usa un'immagine di riferimento e un token di consistenza per preservare le caratteristiche.",
"Generate or edit an image while preserving a character or object's identity from a reference image. " +
"Use ONLY when identity consistency is required.",
parameters: Type.Object({
referenceImage: Type.String({ description: "Percorso dell'immagine di riferimento del personaggio." }),
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({
name: "agy_analyze",
label: "agy analyze file",
label: "agy analyze image fallback",
description:
"Analizza un file (immagine, audio o video) con Gemini multimodale. " +
"Per immagini: descrizione, OCR, analisi. Per audio: contenuto, trascrizione. Per video: scene, contenuto.",
"Analyze a local image with Gemini ONLY as a fallback when the current Pi model cannot inspect images natively. " +
"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({
filePath: Type.String({ description: "Percorso del file da analizzare." }),
question: Type.Optional(Type.String({ description: "Domanda specifica sull'analisi." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
yolo: Type.Optional(Type.Boolean({ description: "true per --dangerously-skip-permissions (necessario per audio/video)." })),
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: "Specific question about the image." })),
model: Type.Optional(Type.String({ description: "Gemini model used for fallback analysis." })),
yolo: Type.Optional(Type.Boolean({ description: "true to pass --dangerously-skip-permissions; use only when explicitly required." })),
}),
async execute(toolCallId, params, signal, onUpdate, ctx) {
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
? `Analizza il file al percorso ${p.filePath}. ${p.question}`
: `Analizza il file al percorso ${p.filePath} e descrivilo in dettaglio.`;
? `Analyze the image at ${filePath}. ${p.question}`
: `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({
prompt,
mode: "analyze",
model: p.model,
stateless: true,
filePaths: [p.filePath],
filePaths: [filePath],
yolo: p.yolo,
signal,
});
return {
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",
label: "agy create verified image",
description:
"Genera un'immagine che soddisfi i requisiti, la verifica iterativamente con visione " +
"testuale e metriche OpenCV, e la rigenera finché non rispetta i requisiti (o fino a un limite). " +
"Ideale quando l'orchestratore non ha visione e serve un'immagine conforme a specifiche precise.",
"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. " +
"Use ONLY when iterative visual verification or deterministic correction is required; use agy_generate for one-shot generation.",
parameters: Type.Object({
requirements: Type.String({ description: "Requisiti precisi che l'immagine deve soddisfare." }),
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)." })),
useOpenCV: Type.Optional(Type.Boolean({ description: "Esegui metriche OpenCV oggettive (default true)." })),
model: Type.Optional(Type.String({ description: "Modello agy." })),
maxIterations: Type.Optional(Type.Number({ description: "Maximum iterations (default 3, max 5)." })),
useOpenCV: Type.Optional(Type.Boolean({ description: "Run objective OpenCV checks (default true)." })),
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) {
const p = params as any;
const requirements = String(p.requirements ?? "").trim();
const maxIter = Math.min(Math.max(Number(p.maxIterations ?? 3) || 3, 1), 5);
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...");
@@ -2688,6 +2781,7 @@ export default function agyExtension(pi: ExtensionAPI) {
imagePath: string;
verdict: string;
opencv: string;
programmaticEdit: string;
}[] = [];
let finalText = "";
@@ -2698,6 +2792,19 @@ export default function agyExtension(pi: ExtensionAPI) {
toolUpdate(onUpdate, "agy_create_verified: generazione script OpenCV dinamico...");
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++) {
toolUpdate(
@@ -2725,10 +2832,24 @@ export default function agyExtension(pi: ExtensionAPI) {
break;
}
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
const judgePrompt =
`Analizza il file al percorso ${imgPath}. Requisiti richiesti: ${requirements}. ` +
// 2) Analisi visione (giudice): PASS/FAIL + problemi rispetto ai requisiti
const judgePrompt =
`Analizza il file al percorso ${candidateImage}. Requisiti richiesti: ${requirements}. ` +
`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.`;
const judgeRes = await executeAgy({
@@ -2736,7 +2857,7 @@ export default function agyExtension(pi: ExtensionAPI) {
mode: "analyze",
model: p.model,
stateless: true,
filePaths: [imgPath],
filePaths: [candidateImage],
yolo: true,
signal,
});
@@ -2748,7 +2869,7 @@ export default function agyExtension(pi: ExtensionAPI) {
let cvMetrics = "";
let ocvPass: boolean | null = null;
if (useCV && cvScript) {
const out = await runOpenCVScript(cvScript, imgPath);
const out = await runOpenCVScript(cvScript, candidateImage);
cvMetrics = out;
try {
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
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) {
finalText =
`✅ Immagine verificata dopo ${iter} iterazione/i.\nPercorso: ${imgPath}` +
`✅ Immagine verificata dopo ${iter} iterazione/i.\nPercorso: ${candidateImage}` +
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
`\nVerdetto visione:\n${verdict}`;
break;
}
if (iter === maxIter) {
finalText =
`⚠️ Requisiti non soddisfatti dopo ${maxIter} iterazioni.\nPercorso: ${imgPath}` +
`⚠️ Requisiti non soddisfatti dopo ${maxIter} iterazioni.\nPercorso: ${candidateImage}` +
(cvMetrics ? `\nMetriche OpenCV: ${cvMetrics}` : "") +
`\nUltimo verdetto visione:\n${verdict}`;
}
@@ -2803,8 +2924,8 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "agy_transcribe",
label: "agy transcribe audio",
description:
"Trascrive il contenuto di un file audio (voce, discorso) in testo usando la Gemini API diretta " +
"(veloce e affidabile; agy CLI non supporta audio). Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
"Transcribe speech or other audio into text with the direct Gemini API. " +
"Use ONLY for audio; use agy_analyze for images and agy_video for video.",
parameters: Type.Object({
filePath: Type.String({ description: "Percorso del file audio (wav, mp3, m4a, ecc.)." }),
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",
label: "agy analyze video",
description:
"Analizza un file video: descrive scene, contenuto, codec, risoluzione, tracce audio. " +
"Richiede --yolo (auto-approva gli strumenti).",
"Analyze a video: scenes, content, codec, resolution, and audio tracks. " +
"Use ONLY for video; use agy_analyze for images and agy_transcribe for audio-only files.",
parameters: Type.Object({
filePath: Type.String({ description: "Percorso del file video (mp4, mov, ecc.)." }),
question: Type.Optional(Type.String({ description: "Domanda specifica sul video." })),
@@ -2873,9 +2994,8 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "agy_tts",
label: "agy TTS (text to speech)",
description:
"Converte un testo in audio (TTS) usando la Gemini API (gemini-2.5-flash-preview-tts) e lo riproduce. " +
"Utile per notifiche vocali, sintesi parlate e aggiornamenti di stato. " +
"Richiede la key Gemini in ~/.agy-chat/gemini-key o GEMINI_API_KEY.",
"Convert text to an audio file with Gemini TTS. Use ONLY when an audio file or playback is explicitly requested; " +
"for normal voice output use the canonical tts_speak tool.",
parameters: Type.Object({
text: Type.String({ description: "Il testo da pronunciare." }),
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",
label: "agy conversation state",
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({
action: Type.Union(
[Type.Literal("id"), Type.Literal("list"), Type.Literal("reset")],
@@ -2972,12 +3093,9 @@ export default function agyExtension(pi: ExtensionAPI) {
name: "antigravity_chat",
label: "antigravity direct protocol chat",
description:
"Chatta direttamente con i server di inferenza di Antigravity via protocollo cloudcode-pa (v1internal), " +
"usando il token OAuth dell'account (nessun subprocess agy, streaming SSE nativo). " +
"Modelli del gateway: gemini-3.5-flash (default, economico), gemini-3.6-flash-medium, gemini-3.6-flash-high, " +
"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.",
"Send one text or multimodal request directly to Antigravity's cloudcode-pa gateway, without the agy subprocess. " +
"Use ONLY when direct gateway access is explicitly needed; for normal delegation use agy. " +
"Respect per-model quota and send requests serially; do not use for batch automation.",
parameters: Type.Object({
prompt: Type.String({ description: "Il messaggio da inviare al modello" }),
model: Type.Optional(
@@ -3231,6 +3349,19 @@ export default function agyExtension(pi: ExtensionAPI) {
ctx.ui.notify("Registra di nuovo con F12.", "info");
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;
}
+2 -1
View File
@@ -6,7 +6,8 @@
"license": "MIT",
"type": "module",
"pi": {
"extensions": ["./extensions"]
"extensions": ["./extensions"],
"skills": ["./skills"]
},
"peerDependencies": {
"@earendil-works/pi-coding-agent": "*",
+39
View File
@@ -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.