← back to SmokeShop
lib/gemini.js
170 lines
/**
* Gemini API Helper — Text + Image generation
* Uses gemini-2.0-flash for text, gemini-2.0-flash-exp-image-generation for images
*/
const fs = require('fs');
const path = require('path');
const GEMINI_TEXT_KEY = '${GEMINI_API_KEY}';
const GEMINI_TEXT_MODEL = 'gemini-2.0-flash';
const GEMINI_IMAGE_MODEL = 'gemini-2.5-flash-image';
const BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/models';
/**
* Generate text content via Gemini
*/
async function generateText(prompt, options = {}) {
const model = options.model || GEMINI_TEXT_MODEL;
const key = options.apiKey || GEMINI_TEXT_KEY;
const url = `${BASE_URL}/${model}:generateContent?key=${key}`;
const parts = [{ text: prompt }];
if (options.images) {
for (const img of options.images) {
parts.push({ inlineData: { mimeType: img.mimeType || 'image/png', data: img.data } });
}
}
const body = {
contents: [{ parts }],
generationConfig: {
temperature: options.temperature || 0.8,
maxOutputTokens: options.maxTokens || 1024,
},
};
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Gemini text error (${res.status}): ${err}`);
}
const data = await res.json();
const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) throw new Error('No text in Gemini response');
return text;
}
/**
* Generate an image via Gemini
* Returns { imageBuffer, text } — the image as a Buffer and any accompanying text
*/
async function generateImage(prompt, options = {}) {
const model = options.model || GEMINI_IMAGE_MODEL;
const key = options.apiKey || GEMINI_TEXT_KEY;
const url = `${BASE_URL}/${model}:generateContent?key=${key}`;
const body = {
contents: [{ parts: [{ text: prompt }] }],
generationConfig: {
responseModalities: ['TEXT', 'IMAGE'],
temperature: options.temperature || 0.7,
},
};
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Gemini image error (${res.status}): ${err}`);
}
const data = await res.json();
const parts = data.candidates?.[0]?.content?.parts || [];
let imageBuffer = null;
let text = '';
for (const part of parts) {
if (part.inlineData) {
imageBuffer = Buffer.from(part.inlineData.data, 'base64');
}
if (part.text) {
text += part.text;
}
}
if (!imageBuffer) throw new Error('No image in Gemini response');
return { imageBuffer, text };
}
/**
* Generate an Instagram post image with text overlay prompt
*/
async function generateInstagramImage(options) {
const { title, category, template, storeName, storeAddress } = options;
const categoryPrompts = {
community: 'a warm, moody interior shot of a trendy smoke shop lounge area with ambient lighting, dark wood shelves, neon accent signs, community bulletin board, cozy neighborhood hangout vibe, indoor scene only',
product: 'a stylish product showcase display with premium accessories, artisan hand-blown glassware, rolling papers, and CBD products arranged on dark velvet, moody studio lighting, high-end retail aesthetic, close-up product photography',
promo: 'a sleek retail counter display inside a modern smoke shop with neon signage, glass display cases glowing with warm light, dark interior aesthetic, professional retail photography, indoor scene only',
};
const categoryScene = categoryPrompts[category] || categoryPrompts.product;
const prompt = `Create a square 1080x1080 Instagram post image for a smoke shop called "${storeName || 'Smoke & Vape Depot'}".
Scene: ${categoryScene}
The image MUST include:
- Bold text overlay reading: "${title}" in large white letters with a subtle dark shadow
- The store name "${storeName || 'SMOKE & VAPE DEPOT'}" at the bottom in a smaller accent color
- "${storeAddress || '6100 Reseda Blvd, Reseda CA'}" in small text below the store name
- A consistent decorative border on all 4 sides (${template === 'b' ? 'orange/amber' : 'red/crimson'} color)
- "21+ ONLY" disclaimer in small text at the very bottom
- Dark, moody aesthetic with ${template === 'b' ? 'warm orange' : 'deep red'} accent colors
Style: Professional Instagram marketing post, high contrast, modern design, square format.
DO NOT include any tobacco or vaping imagery. Focus on the aesthetic and branding.
DO NOT use outdoor scenes, street photos, storefronts, or sidewalks. ALL scenes must be indoor or studio shots only.`;
return generateImage(prompt);
}
/**
* Generate Instagram caption with hashtags via Gemini
*/
async function generateCaption(options) {
const { title, category, storeName } = options;
const prompt = `Write an Instagram caption for Smoke & Vape Depot (6100 Reseda Blvd, Reseda CA 91335).
Post title: "${title}"
Category: ${category}
STORE BACKGROUND:
- Located on Reseda Blvd in the San Fernando Valley (818 area code), open late until 12 AM
- Right next to Wingstop (6048 Reseda Blvd) — regulars grab wings then stop by
- Products: RAW papers, Clipper lighters, Zippo, GRAV Labs & MAV Glass & Diamond Glass (glassware), Charlotte's Web & cbdMD (CBD), Santa Cruz Shredder grinders, King Palm wraps
- Neighborhood: diverse Reseda community, growing foodie scene, local barbershops, taquerias, Reseda Park
- Voice: friendly, authentic, neighborhood vibe — like a real shop owner, NOT corporate
RULES:
- Keep it engaging and authentic, 2-3 sentences max
- Include a call to action (stop by, come check it out, swing through)
- For community posts: reference Reseda neighborhood, local spots, SFValley pride
- For product posts: mention specific brands we carry by name
- For promo posts: mention hours (open until 12 AM), location perks, or deals
- NEVER reference smoking, vaping, or tobacco use
- NEVER make health claims
- End with "21+ only"
- Include 8-10 hashtags on a new line: #SmokeAndVapeDepot #ResedaCA #SFValley #818 #ShopLocal plus category-specific ones
- Keep caption under 300 characters (excluding hashtags)
Return ONLY the caption text, nothing else.`;
return generateText(prompt, { temperature: 0.9 });
}
module.exports = { generateText, generateImage, generateInstagramImage, generateCaption };