feat: pipeline multimodale diretta per F12 con modelli Gemini
This commit is contained in:
+219
-51
@@ -728,18 +728,20 @@ function stageExternalFiles(
|
||||
}
|
||||
|
||||
// Estrae dal briefing JSON prodotto dall'interpretazione vocale i campi per
|
||||
// l'orchestratore: prompt pulito, se serve ricerca web e suggerimenti.
|
||||
// l'orchestratore: trascrizione, prompt pulito, se serve ricerca web e suggerimenti.
|
||||
function extractVoiceBriefing(text: string): {
|
||||
transcript: string;
|
||||
cleanPrompt: string;
|
||||
needsSearch: boolean;
|
||||
searchHints: string[];
|
||||
} {
|
||||
const result = { cleanPrompt: "", needsSearch: false, searchHints: [] as string[] };
|
||||
const result = { transcript: "", cleanPrompt: "", needsSearch: false, searchHints: [] as string[] };
|
||||
try {
|
||||
const start = text.indexOf("{");
|
||||
const end = text.lastIndexOf("}");
|
||||
if (start >= 0 && end > start) {
|
||||
const obj = JSON.parse(text.slice(start, end + 1));
|
||||
if (typeof obj.trascrizione_corretta === "string") result.transcript = obj.trascrizione_corretta.trim();
|
||||
if (typeof obj.prompt_utente_pulito === "string") result.cleanPrompt = obj.prompt_utente_pulito.trim();
|
||||
result.needsSearch = String(obj.ricerca_necessaria).toLowerCase() === "true";
|
||||
if (Array.isArray(obj.suggerimenti_ricerca)) {
|
||||
@@ -1442,6 +1444,10 @@ interface AntgModelInfo {
|
||||
thinkingLevel?: "low" | "medium" | "high";
|
||||
}
|
||||
const ANTG_MODEL_MAP: Record<string, AntgModelInfo> = {
|
||||
"gemini-3.7-flash": { backend: "gemini-3.7-flash-medium" },
|
||||
"gemini-3.7-flash-medium": { backend: "gemini-3.7-flash-medium" },
|
||||
"gemini-3.7-flash-low": { backend: "gemini-3.7-flash-low" },
|
||||
"gemini-3.7-flash-high": { backend: "gemini-3.7-flash-high" },
|
||||
"gemini-3.5-flash": { backend: "gemini-3.5-flash-low" },
|
||||
"gemini-3.6-flash-medium": { backend: "gemini-3.6-flash-medium" },
|
||||
"gemini-3.6-flash-high": { backend: "gemini-3.6-flash-high" },
|
||||
@@ -1451,7 +1457,7 @@ const ANTG_MODEL_MAP: Record<string, AntgModelInfo> = {
|
||||
"claude-opus-4.6": { backend: "claude-opus-4-6-thinking" },
|
||||
"gpt-oss-120b": { backend: "gpt-oss-120b-medium" },
|
||||
};
|
||||
const ANTG_DEFAULT_MODEL = "gemini-3.5-flash";
|
||||
const ANTG_DEFAULT_MODEL = "gemini-3.7-flash-medium";
|
||||
|
||||
let antgToken: { access: string; expiresAtMs: number; refresh: string } | null = null;
|
||||
let antgProject: { pid: string; base: string } | null = null;
|
||||
@@ -1583,7 +1589,8 @@ async function antgGetProject(): Promise<{ pid: string; base: string }> {
|
||||
}
|
||||
|
||||
interface AntgGenerateOpts {
|
||||
prompt: string;
|
||||
prompt?: string;
|
||||
parts?: any[];
|
||||
model?: string;
|
||||
system?: string;
|
||||
maxOutputTokens?: number;
|
||||
@@ -1649,8 +1656,9 @@ async function antgGenerate(opts: AntgGenerateOpts): Promise<{ text: string; det
|
||||
};
|
||||
if (mi.thinkingLevel) gc.thinkingConfig = { thinkingLevel: mi.thinkingLevel };
|
||||
if (opts.thinking && opts.thinking !== "off") gc.thinkingConfig = { thinkingLevel: opts.thinking };
|
||||
const userParts = opts.parts ?? (opts.prompt ? [{ text: opts.prompt }] : []);
|
||||
const request: any = {
|
||||
contents: [{ role: "user", parts: [{ text: opts.prompt }] }],
|
||||
contents: [{ role: "user", parts: userParts }],
|
||||
generationConfig: gc,
|
||||
};
|
||||
if (opts.system) request.systemInstruction = { parts: [{ text: opts.system }] };
|
||||
@@ -1710,6 +1718,137 @@ async function antgGenerate(opts: AntgGenerateOpts): Promise<{ text: string; det
|
||||
return run as Promise<{ text: string; details: any }>;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pipeline Multimodale Diretta (Gemini): invio diretto dell'audio senza STT
|
||||
// ---------------------------------------------------------------------------
|
||||
function isGeminiModel(model: any): boolean {
|
||||
if (!model) return true; // se non specificato, default ad Antigravity/Gemini
|
||||
const id = (model.id || "").toLowerCase();
|
||||
const provider = (model.provider || "").toLowerCase();
|
||||
if (id.startsWith("gemini-") || id.includes("gemini")) return true;
|
||||
if (provider === "google" || provider === "google-vertex") return true;
|
||||
if (provider === "antigravity") {
|
||||
if (id.startsWith("claude-") || id.startsWith("gpt-oss")) return false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
interface AudioInterpretationResult {
|
||||
transcript: string;
|
||||
cleanPrompt: string;
|
||||
needsSearch: boolean;
|
||||
searchHints: string[];
|
||||
rawText: string;
|
||||
}
|
||||
|
||||
async function interpretAudioDirectGemini(
|
||||
audioFile: string,
|
||||
editorText: string,
|
||||
context: string,
|
||||
targetModel?: string,
|
||||
): Promise<AudioInterpretationResult> {
|
||||
if (!audioHasSpeech(audioFile)) {
|
||||
throw new Error("Nessun parlato rilevato nell'audio (silenzio o volume troppo basso)");
|
||||
}
|
||||
|
||||
let sendFile = audioFile;
|
||||
let mimeType = "audio/wav";
|
||||
if (audioFile.toLowerCase().endsWith(".wav")) {
|
||||
const ogg = audioFile.replace(/\.wav$/i, ".ogg");
|
||||
try {
|
||||
await execFileAsync(
|
||||
"ffmpeg",
|
||||
["-hide_banner", "-loglevel", "error", "-y", "-i", audioFile, "-ac", "1", "-ar", "16000", "-c:a", "libopus", "-b:a", "16k", ogg],
|
||||
{ timeout: 60_000 },
|
||||
);
|
||||
sendFile = ogg;
|
||||
mimeType = "audio/ogg";
|
||||
} catch {
|
||||
/* fallback su wav */
|
||||
}
|
||||
}
|
||||
const b64 = fs.readFileSync(sendFile).toString("base64");
|
||||
|
||||
const promptInstructions =
|
||||
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
|
||||
`Sei un analista tecnico/middleware per un agente AI orchestratore. Ascolta la traccia audio allegata (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi eseguirà le azioni.` +
|
||||
(editorText ? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}` : "") +
|
||||
`\n\nContesto della conversazione precedente:\n${context || "(nessuno)"}` +
|
||||
`\n\nRestituisci UN SOLO oggetto JSON (nessun markdown aggiuntivo tranne il blocco json) con questi campi esatti:` +
|
||||
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
|
||||
`\nRegole:` +
|
||||
`\n- trascrizione_corretta: la trascrizione fedele e completa delle parole pronunciate nell'audio in italiano.` +
|
||||
`\n- prompt_utente_pulito: la richiesta rielaborata e pulita che l'orchestratore userà come prompt effettivo.` +
|
||||
`\n- NON eseguire alcuno strumento o azione; solo analisi, trascrizione e briefing in JSON.` +
|
||||
`\n- ricerca_necessaria=true se serve verificare best practices, documentazione o librerie aggiornate.` +
|
||||
`\n- suggerimenti_ricerca: termini chiave per la ricerca web se necessaria.`;
|
||||
|
||||
const parts = [
|
||||
{ text: promptInstructions },
|
||||
{ inlineData: { mimeType, data: b64 } },
|
||||
];
|
||||
|
||||
const model = targetModel && targetModel.startsWith("gemini-") ? targetModel : "gemini-3.7-flash-medium";
|
||||
|
||||
try {
|
||||
const res = await antgGenerate({
|
||||
parts,
|
||||
model,
|
||||
maxOutputTokens: 3000,
|
||||
thinking: "low",
|
||||
stream: true,
|
||||
});
|
||||
const rawText = res.text.trim();
|
||||
const briefing = extractVoiceBriefing(rawText);
|
||||
return {
|
||||
transcript: briefing.transcript || briefing.cleanPrompt || rawText,
|
||||
cleanPrompt: briefing.cleanPrompt || rawText,
|
||||
needsSearch: briefing.needsSearch,
|
||||
searchHints: briefing.searchHints,
|
||||
rawText,
|
||||
};
|
||||
} catch (err: any) {
|
||||
// Fallback su Gemini API diretta se Antigravity account token fallisce
|
||||
let key = getConfig("geminiApiKey") ?? process.env.GEMINI_API_KEY ?? "";
|
||||
if (!key) {
|
||||
try {
|
||||
key = fs.readFileSync(path.join(AGY_CHAT_DIR, "gemini-key"), "utf8").trim();
|
||||
} catch {
|
||||
/* ignora */
|
||||
}
|
||||
}
|
||||
if (key) {
|
||||
const apiModel = process.env.AGY_GEMINI_MODEL ?? "gemini-3.5-flash";
|
||||
const body = {
|
||||
contents: [{ parts: [{ text: promptInstructions }, { inline_data: { mime_type: mimeType, data: b64 } }] }],
|
||||
};
|
||||
const res = await fetch(
|
||||
`https://generativelanguage.googleapis.com/v1beta/models/${apiModel}:generateContent?key=${key}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: AbortSignal.timeout(120_000),
|
||||
},
|
||||
);
|
||||
if (res.ok) {
|
||||
const data: any = await res.json();
|
||||
const rawText = data?.candidates?.[0]?.content?.parts?.[0]?.text?.trim() ?? "";
|
||||
const briefing = extractVoiceBriefing(rawText);
|
||||
return {
|
||||
transcript: briefing.transcript || briefing.cleanPrompt || rawText,
|
||||
cleanPrompt: briefing.cleanPrompt || rawText,
|
||||
needsSearch: briefing.needsSearch,
|
||||
searchHints: briefing.searchHints,
|
||||
rawText,
|
||||
};
|
||||
}
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Provider "antigravity" — modelli del gateway come provider pi nativo
|
||||
// Registrato con pi.registerProvider() + streamSimple: appare nel selettore
|
||||
@@ -2977,56 +3116,85 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
ctx.ui.notify("Registrazione fermata, ottimizzazione audio...", "info");
|
||||
const optimized = await optimizeAudio(file);
|
||||
|
||||
ctx.ui.notify("Trascrizione in corso...", "info");
|
||||
const tr = await transcribeAudio(optimized);
|
||||
if (!tr.text) {
|
||||
ctx.ui.setStatus("agy-rec", "");
|
||||
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
|
||||
playSound("cancel");
|
||||
return;
|
||||
}
|
||||
const transcript = tr.text;
|
||||
|
||||
// Legge il testo scritto a mano nel campo editor prima della registrazione
|
||||
// e lo combina con la trascrizione audio (comportamento "multimodale").
|
||||
const editorText = (ctx.ui.getEditorText?.() ?? "").trim();
|
||||
|
||||
// Interpreta con Gemini (testo) — framing inter-agent: Gemini è un analista
|
||||
// che scrive un briefing per l'orchestratore (NON risponde all'utente),
|
||||
// in JSON strutturato, con direttiva plan-first (nessuna esecuzione).
|
||||
ctx.ui.notify("Interpretazione con Gemini...", "info");
|
||||
const context = getConversationContext(ctx);
|
||||
const res = await executeAgy({
|
||||
prompt:
|
||||
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
|
||||
`Sei un analista tecnico/middleware per un agente AI orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi risponderà all'utente.` +
|
||||
`\n\nAnalizza la richiesta vocale (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore.` +
|
||||
`\n\nTrascrizione vocale:\n${transcript}` +
|
||||
(editorText
|
||||
? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}`
|
||||
: "") +
|
||||
`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
|
||||
`\n\nRestituisci UN SOLO oggetto JSON (nessun testo aggiuntivo) con questi campi:` +
|
||||
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
|
||||
`\nRegole:` +
|
||||
`\n- NON eseguire alcuno strumento o azione; solo analisi e briefing.` +
|
||||
`\n- Nessun saluto o testo rivolto all'utente: solo JSON tecnico per l'orchestratore.` +
|
||||
`\n- prompt_utente_pulito = la richiesta rielaborata che l'orchestratore userà come prompt verso l'utente.` +
|
||||
`\n- ricerca_necessaria=true se serve verificare best practices, versioni, documentazione o dati aggiornati.` +
|
||||
`\n- suggerimenti_ricerca: se ricerca_necessaria, indica all'orchestratore di usare la ricerca web (Perplexity) e su cosa.`,
|
||||
stateless: true,
|
||||
model: "Gemini 3.6 Flash (Medium)",
|
||||
yolo: true,
|
||||
});
|
||||
|
||||
let transcript = "";
|
||||
let finalText = "";
|
||||
let needsSearch = false;
|
||||
let searchHints: string[] = [];
|
||||
|
||||
const activeModel = ctx.model;
|
||||
const useDirectMultimodal = isGeminiModel(activeModel);
|
||||
|
||||
if (useDirectMultimodal) {
|
||||
// =========================================================================
|
||||
// Pipeline Multimodale Diretta (Gemini): l'audio viene inviato direttamente
|
||||
// al modello senza passare per un riconoscitore vocale STT separato.
|
||||
// =========================================================================
|
||||
ctx.ui.notify("Interpretazione vocale diretta con Gemini...", "info");
|
||||
try {
|
||||
const directRes = await interpretAudioDirectGemini(
|
||||
optimized,
|
||||
editorText,
|
||||
context,
|
||||
activeModel?.id,
|
||||
);
|
||||
transcript = directRes.transcript || directRes.cleanPrompt;
|
||||
finalText = directRes.cleanPrompt;
|
||||
needsSearch = directRes.needsSearch;
|
||||
searchHints = directRes.searchHints;
|
||||
} catch (err: any) {
|
||||
ctx.ui.notify(`Elaborazione diretta fallita: ${err.message}. Fallback su trascrizione STT...`, "warning");
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback o modello non-Gemini: pipeline a 2 passaggi (STT + Briefing)
|
||||
if (!finalText) {
|
||||
ctx.ui.notify("Trascrizione in corso...", "info");
|
||||
const tr = await transcribeAudio(optimized);
|
||||
if (!tr.text) {
|
||||
ctx.ui.setStatus("agy-rec", "");
|
||||
ctx.ui.notify(`Trascrizione fallita: ${tr.error ?? "vuota"}`, "error");
|
||||
playSound("cancel");
|
||||
return;
|
||||
}
|
||||
transcript = tr.text;
|
||||
|
||||
ctx.ui.notify("Interpretazione con Gemini...", "info");
|
||||
const res = await executeAgy({
|
||||
prompt:
|
||||
`[CONTESTO INTERNO — COMUNICAZIONE TRA AGENTI]\n` +
|
||||
`Sei un analista tecnico/middleware per un agente AI orchestratore. NON rispondere all'utente: il tuo output sarà letto SOLO dall'orchestratore, che poi risponderà all'utente.` +
|
||||
`\n\nAnalizza la richiesta vocale (e l'eventuale testo dell'editor) e produci un briefing strutturato per l'orchestratore.` +
|
||||
`\n\nTrascrizione vocale:\n${transcript}` +
|
||||
(editorText
|
||||
? `\n\nTesto scritto dall'utente nel campo di input (da combinare con la voce):\n${editorText}`
|
||||
: "") +
|
||||
`\n\nContesto della conversazione:\n${context || "(nessuno)"}` +
|
||||
`\n\nRestituisci UN SOLO oggetto JSON (nessun testo aggiuntivo) con questi campi:` +
|
||||
`\n{"trascrizione_corretta":"...","intent_analisi":"...","note_per_agent":"...","azioni_raccomandate":["..."],"prompt_utente_pulito":"...","ricerca_necessaria":true/false,"suggerimenti_ricerca":["..."]}` +
|
||||
`\nRegole:` +
|
||||
`\n- NON eseguire alcuno strumento o azione; solo analisi e briefing.` +
|
||||
`\n- Nessun saluto o testo rivolto all'utente: solo JSON tecnico per l'orchestratore.` +
|
||||
`\n- prompt_utente_pulito = la richiesta rielaborata che l'orchestratore userà come prompt verso l'utente.` +
|
||||
`\n- ricerca_necessaria=true se serve verificare best practices, versioni, documentazione o dati aggiornati.` +
|
||||
`\n- suggerimenti_ricerca: se ricerca_necessaria, indica all'orchestratore di usare la ricerca web (Perplexity) e su cosa.`,
|
||||
stateless: true,
|
||||
model: "Gemini 3.6 Flash (Medium)",
|
||||
yolo: true,
|
||||
});
|
||||
const briefing = extractVoiceBriefing(res.text);
|
||||
finalText = briefing.cleanPrompt || res.text.trim() || transcript;
|
||||
needsSearch = briefing.needsSearch;
|
||||
searchHints = briefing.searchHints;
|
||||
}
|
||||
|
||||
ctx.ui.setStatus("agy-rec", "");
|
||||
|
||||
// Estrae il briefing JSON; fallback sul testo grezzo se non parsato
|
||||
const briefing = extractVoiceBriefing(res.text);
|
||||
let finalText = briefing.cleanPrompt || res.text.trim() || transcript;
|
||||
|
||||
// Guida per l'agente successivo: usa la ricerca web/Perplexity se necessario
|
||||
if (briefing.needsSearch) {
|
||||
const hints = briefing.searchHints.length ? ` Suggerimenti: ${briefing.searchHints.join("; ")}` : "";
|
||||
if (needsSearch) {
|
||||
const hints = searchHints.length ? ` Suggerimenti: ${searchHints.join("; ")}` : "";
|
||||
finalText += `\n\n[Nota per l'agente: per rispondere correttamente, usa la ricerca web (Perplexity) per verificare best practices/versioni/documentazione aggiornate.${hints}]`;
|
||||
}
|
||||
|
||||
@@ -3061,7 +3229,7 @@ export default function agyExtension(pi: ExtensionAPI) {
|
||||
} else {
|
||||
pi.sendUserMessage(finalText, { deliverAs: "followUp" });
|
||||
}
|
||||
ctx.ui.notify("✅ Trascrizione inviata come prompt a pi", "info");
|
||||
ctx.ui.notify("✅ Richiesta vocale inviata come prompt a pi", "info");
|
||||
playSound("done");
|
||||
|
||||
// notifica vocale (config ttsNotify o AGY_TTS_NOTIFY=0 per disattivare)
|
||||
|
||||
Reference in New Issue
Block a user