const { fromBuffer } = require('file-type'); const FormData = require("form-data"); const crypto = require("node:crypto"); const axios = require("axios"); const Jimp = require("jimp"); class GridPlus { constructor() { this.ins = axios.create({ baseURL: 'https://api.grid.plus/v1', headers: { 'user-agent': 'Mozilla/5.0 (Android 15; Mobile; SM-F958; rv:130.0) Gecko/130.0 Firefox/130.0', 'X-AppID': '808645', 'X-Platform': 'h5', 'X-Version': '8.9.7', 'X-SessionToken': '', 'X-UniqueID': this.uid(), 'X-GhostID': this.uid(), 'X-DeviceID': this.uid(), 'X-MCC': 'id-ID', sig: `XX${this.uid()+this.uid()}` } }) } uid() { return crypto.randomUUID().replace(/-/g, '') } form(dt) { const form = new FormData(); Object.entries(dt).forEach(([key, value]) => { form.append(key, String(value)); }); return form } async upload(buff, method) { try { if (!Buffer.isBuffer(buff)) throw 'data is not buffer!'; const { mime, ext } = (await fromBuffer(buff)) || {}; const d = await this.ins.post('/ai/web/nologin/getuploadurl', this.form({ ext, method })).then(i => i.data); await axios.put(d.data.upload_url, buff, { headers: { 'content-type': mime } }); return d.data.img_url; } catch(e) { throw new Error('An error occurred while uploading the image'); } } async task({ path, data, sl = () => false }) { const [start, interval, timeout] = [ Date.now(), 3000, 60000 ]; return new Promise(async (resolve, reject) => { const check = async () => { if (Date.now() - start > timeout) { return reject(new Error(`Polling timed out for task`)); }; try { const dt = await this.ins({ url: path, method: data ? 'POST' : 'GET', ...(data ? { data } : {}) }); if (!!dt.data.errmsg?.trim()) { reject(new Error(`Something a error with message: ${dt.data.errmsg}`)); }; if (!!sl(dt.data)) { return resolve(dt.data); }; setTimeout(check, interval); } catch (error) { reject(error); } }; check(); }); } async edit(buff, prompt) { try { const up = await this.upload(buff, 'wn_aistyle_nano'); const dn = await this.ins.post('/ai/nano/upload', this.form({ prompt, url: up })).then(i => i.data); if (!dn.task_id) throw 'taskId not found on request'; const res = await this.task({ path: `/ai/nano/get_result/${dn.task_id}`, sl: (dt) => dt.code === 0 && !!dt.image_url, }); return res.image_url } catch(e) { throw new Error('Something error, message: ' + e.message); } } } async function createGrid(imageUrls) { const imageBuffers = await Promise.all( imageUrls.map(url => axios.get(url, { responseType: 'arraybuffer' }).then(r => Buffer.from(r.data))) ); const images = await Promise.all( imageBuffers.map(async buffer => { const img = await Jimp.default ? Jimp.default.read(buffer) : new Promise((resolve, reject) => { new Jimp(buffer, (err, image) => { if (err) reject(err); else resolve(image); }); }); return img; }) ); const count = images.length; const targetSize = 1024; let cols, rows; if (count === 1) { const img = images[0]; await img.resize(targetSize, targetSize); return await img.getBufferAsync(Jimp.MIME_JPEG); } else if (count === 2) { cols = 2; rows = 1; } else if (count === 3) { cols = 2; rows = 2; } else { cols = 2; rows = 2; } const cellSize = Math.floor(targetSize / cols); const gridHeight = cellSize * rows; const grid = await new Jimp(targetSize, gridHeight, 0xFFFFFFFF); for (let i = 0; i < count && i < 4; i++) { const col = i % cols; const row = Math.floor(i / cols); const x = col * cellSize; const y = row * cellSize; const resized = await images[i].cover(cellSize, cellSize); grid.composite(resized, x, y); } return await grid.getBufferAsync(Jimp.MIME_JPEG); } const handler = async (req, res) => { try { const { prompt, img_url1, img_url2, img_url3, img_url4, key } = req.query; if (!key) { return res.status(400).json({ success: false, error: 'Missing required parameter: key' }); } if (!prompt) { return res.status(400).json({ success: false, error: 'Missing required parameter: prompt' }); } const imageUrls = [img_url1, img_url2, img_url3, img_url4].filter(Boolean); if (imageUrls.length === 0) { return res.status(400).json({ success: false, error: 'At least one image URL is required (img_url1)' }); } if (imageUrls.length > 4) { return res.status(400).json({ success: false, error: 'Maximum 4 images allowed' }); } const imageBuffer = await createGrid(imageUrls); const gridPlus = new GridPlus(); const result = await gridPlus.edit(imageBuffer, prompt); return res.json({ author: "Herza", success: true, data: { prompt: prompt, image_url: result, original_urls: imageUrls, image_count: imageUrls.length }, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ success: false, error: error.message, timestamp: new Date().toISOString() }); } }; module.exports = { name: 'Nano Banana', description: 'AI Image Editing using Grid Plus - Edit 1-4 images with text prompts', type: 'GET', routes: ['api/AI/nanobanana'], tags: ['ai', 'image', 'editing', 'nanobanana', 'multi-image'], parameters: ['prompt', 'img_url1', 'img_url2', 'img_url3', 'img_url4', 'key'], limit: 5, enabled: true, main: ['AI'], handler };