From cde5aaa9b730ca63790c9a3b841db88e8e6ef4be Mon Sep 17 00:00:00 2001 From: enne2 Date: Fri, 28 Aug 2026 18:51:37 +0200 Subject: [PATCH] feat: add verified image programmatic edits --- README.md | 18 +- docs/vocal-workflow.md | 3 +- extensions/index.ts | 261 +++++++++++++++++++++------- package.json | 3 +- skills/agy-create-verified/SKILL.md | 39 +++++ 5 files changed, 253 insertions(+), 71 deletions(-) create mode 100644 skills/agy-create-verified/SKILL.md diff --git a/README.md b/README.md index 3c17b0e..6ac9979 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/vocal-workflow.md b/docs/vocal-workflow.md index 6ade2e1..a298771 100644 --- a/docs/vocal-workflow.md +++ b/docs/vocal-workflow.md @@ -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): diff --git a/extensions/index.ts b/extensions/index.ts index 6303533..466566d 100644 --- a/extensions/index.ts +++ b/extensions/index.ts @@ -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 { async function runOpenCVScript(script: string, imagePath: string): Promise { 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 { + 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; } diff --git a/package.json b/package.json index 5acb58b..8ed4652 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "license": "MIT", "type": "module", "pi": { - "extensions": ["./extensions"] + "extensions": ["./extensions"], + "skills": ["./skills"] }, "peerDependencies": { "@earendil-works/pi-coding-agent": "*", diff --git a/skills/agy-create-verified/SKILL.md b/skills/agy-create-verified/SKILL.md new file mode 100644 index 0000000..0352c27 --- /dev/null +++ b/skills/agy-create-verified/SKILL.md @@ -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.