#!/usr/bin/env node /** * fill-brave-images.ts — Brave-only pass for remaining disease images. * * Runs at 1 request/sec (Brave rate limit). * Updates diseases.json and Turso DB. * * Usage: cd apps/web && npx tsx scripts/fill-brave-images.ts */ import dotenv from "dotenv"; dotenv.config({ path: resolve(__dirname, "../.env.local") }); import { readFileSync, writeFileSync } from "fs"; import { resolve } from "path"; import { createClient } from "@libsql/client"; import { closeDb } from "../src/lib/db/index"; const DISEASES_JSON = resolve(__dirname, "../src/data/diseases.json"); const BRAVE_KEY = process.env.BRAVE_API_KEY ?? ""; interface DiseaseSeed { id: string; plantId: string; name: string; scientificName: string; imageUrl?: string; [key: string]: unknown; } function load(): DiseaseSeed[] { return JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; } async function searchBraveImage(query: string): Promise { const url = new URL("https://api.search.brave.com/res/v1/images/search"); url.searchParams.set("q", query); url.searchParams.set("count", "3"); for (let attempt = 0; attempt < 3; attempt++) { try { const res = await fetch(url.toString(), { headers: { "X-Subscription-Token": BRAVE_KEY, Accept: "application/json" }, }); if (res.status === 429) { await new Promise((r) => setTimeout(r, 5000 * 2 ** attempt)); continue; } if (!res.ok) return null; const data = (await res.json()) as { results?: Array<{ url: string; thumbnail?: { src?: string } }>; }; const results = data?.results ?? []; if (results.length === 0) return null; // Prefer non-stock direct-looking images for (const r of results) { const src = r.thumbnail?.src ?? r.url; if (src && !/(dreamstime|shutterstock|alamy|istock|123rf)/i.test(src)) return src; } return results[0].thumbnail?.src ?? results[0].url; } catch { await new Promise((r) => setTimeout(r, 2000)); } } return null; } async function main() { console.log("\nšŸ” Brave Image Search — remaining disease images\n"); if (!BRAVE_KEY) { console.log("āŒ No BRAVE_API_KEY in .env.local\n"); process.exit(1); } const diseases = load(); const pending = diseases.filter((d) => !d.imageUrl); console.log(`šŸ“‹ ${pending.length} diseases need images\n`); let found = 0; for (let i = 0; i < pending.length; i++) { const d = pending[i]; const plant = diseases.find((p) => p.id === d.plantId); const plantName = plant?.name ?? d.plantId; const query = `${d.name} ${plantName} plant disease symptom`; process.stdout.write(` [${String(i + 1).padStart(2, " ")}/${pending.length}] ${d.name.padEnd(35)} `); const url = await searchBraveImage(query); if (url) { d.imageUrl = url; found++; console.log(`āœ…`); } else { console.log(`āŒ`); } // 1 req/sec rate limit await new Promise((r) => setTimeout(r, 1100)); } // Write updated JSON writeFileSync(DISEASES_JSON, JSON.stringify(diseases, null, 2) + "\n", "utf-8"); console.log(`\nāœ… diseases.json updated: ${found}/${pending.length} images found\n`); // Update DB try { const dbUrl = process.env.DATABASE_URL; const dbToken = process.env.DATABASE_TOKEN; if (dbUrl && dbToken) { const raw = createClient({ url: dbUrl, authToken: dbToken }); const updates = pending.filter((d) => d.imageUrl); for (let i = 0; i < updates.length; i += 50) { await raw.batch( updates.slice(i, i + 50).map((d) => ({ sql: "UPDATE diseases SET image_url = ? WHERE id = ?", args: [d.imageUrl!, d.id], })), "write", ); } raw.close(); console.log(`āœ… Turso DB updated: ${updates.length} rows`); } else { console.log("ā­ļø Skipping DB — no DATABASE_URL/TOKEN"); } } catch (err) { console.log(` āš ļø DB: ${err instanceof Error ? err.message : err}`); } // Summary const finalDiseases = JSON.parse(readFileSync(DISEASES_JSON, "utf-8")) as DiseaseSeed[]; const stillMissing = finalDiseases.filter((d) => !d.imageUrl); console.log(`\n${"═".repeat(50)}`); console.log(`šŸ“Š FINAL: ${finalDiseases.length} total`); console.log(` With images: ${finalDiseases.length - stillMissing.length}`); console.log(` Still missing: ${stillMissing.length}`); if (stillMissing.length > 0) { console.log(`\nStill need human curation:`); for (const d of stillMissing) { console.log(` āŒ ${d.name} (${d.id})`); } } console.log(`${"═".repeat(50)}\n`); closeDb(); } main().catch((err) => { console.error("\nāŒ Fatal:", err); process.exit(1); });