109 lines
No EOL
3.4 KiB
TypeScript
109 lines
No EOL
3.4 KiB
TypeScript
import { tool } from "@opencode-ai/plugin"
|
|
|
|
async function parseImageInput(input: string) {
|
|
// Accepts file path ("./img.png") or data URL ("data:image/png;base64,...")
|
|
if (input.startsWith("data:")) {
|
|
const base64 = input.split(",")[1]
|
|
const mime = input.substring(5, input.indexOf(";"))
|
|
return { mime, base64 }
|
|
}
|
|
// Treat as file path
|
|
const file = Bun.file(input)
|
|
const arr = await file.arrayBuffer()
|
|
const base64 = Buffer.from(arr).toString("base64")
|
|
// Best-effort mime
|
|
const mime = file.type || "image/png"
|
|
return { mime, base64 }
|
|
}
|
|
|
|
async function callGeminiAPI(mime: string, base64: string, prompt: string) {
|
|
const apiKey = process.env.GEMINI_API_KEY
|
|
if (!apiKey) throw new Error("Set GEMINI_API_KEY in your environment")
|
|
|
|
const res = await fetch(
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-image-preview:generateContent",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-goog-api-key": apiKey,
|
|
},
|
|
body: JSON.stringify({
|
|
inputs: [{ mimeType: mime, data: base64 }],
|
|
contents: [{ parts: [{ text: prompt }]}],
|
|
}),
|
|
}
|
|
)
|
|
|
|
if (!res.ok) throw new Error(`API error: ${await res.text()}`)
|
|
const json = await res.json()
|
|
const b64 = json?.candidates?.[0]?.content?.parts?.[0]?.inlineData?.data
|
|
if (!b64) throw new Error("No image data returned")
|
|
|
|
return b64
|
|
}
|
|
|
|
export const edit = tool({
|
|
description: "Edit an image using Gemini with file path or data URL",
|
|
args: {
|
|
image: tool.schema.string().describe("File path or data URL"),
|
|
prompt: tool.schema.string().describe("Edit instruction"),
|
|
output: tool.schema.string().optional().describe("Output filename (default edited.png)"),
|
|
},
|
|
async execute(args, context) {
|
|
try {
|
|
const { mime, base64 } = await parseImageInput(args.image)
|
|
const resultBase64 = await callGeminiAPI(mime, base64, args.prompt)
|
|
|
|
const out = args.output || "edited.png"
|
|
await Bun.write(out, Buffer.from(resultBase64, "base64"))
|
|
return `Saved edited image to ${out}`
|
|
} catch (error) {
|
|
return `Error: ${error.message}`
|
|
}
|
|
},
|
|
})
|
|
|
|
export const analyze = tool({
|
|
description: "Analyze an image using Gemini without editing",
|
|
args: {
|
|
image: tool.schema.string().describe("File path or data URL"),
|
|
question: tool.schema.string().describe("What to analyze about the image"),
|
|
},
|
|
async execute(args, context) {
|
|
try {
|
|
const { mime, base64 } = await parseImageInput(args.image)
|
|
|
|
const apiKey = process.env.GEMINI_API_KEY
|
|
if (!apiKey) return "Set GEMINI_API_KEY in your environment"
|
|
|
|
const res = await fetch(
|
|
"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent",
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"x-goog-api-key": apiKey,
|
|
},
|
|
body: JSON.stringify({
|
|
contents: [{
|
|
parts: [
|
|
{ text: args.question },
|
|
{ inlineData: { mimeType: mime, data: base64 } }
|
|
]
|
|
}],
|
|
}),
|
|
}
|
|
)
|
|
|
|
if (!res.ok) return `API error: ${await res.text()}`
|
|
const json = await res.json()
|
|
const text = json?.candidates?.[0]?.content?.parts?.[0]?.text
|
|
if (!text) return "No analysis returned"
|
|
|
|
return text
|
|
} catch (error) {
|
|
return `Error: ${error.message}`
|
|
}
|
|
},
|
|
}) |