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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
@@ -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):
|
||||
|
||||
+194
-63
@@ -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}. ` +
|
||||
`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
@@ -6,7 +6,8 @@
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"pi": {
|
||||
"extensions": ["./extensions"]
|
||||
"extensions": ["./extensions"],
|
||||
"skills": ["./skills"]
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@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