diff --git a/node/image-generation-with-gemini/.gitignore b/node/image-generation-with-gemini/.gitignore new file mode 100644 index 00000000..46afb6b3 --- /dev/null +++ b/node/image-generation-with-gemini/.gitignore @@ -0,0 +1,133 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Directory used by Appwrite CLI for local development +.appwrite \ No newline at end of file diff --git a/node/image-generation-with-gemini/.prettierrc.json b/node/image-generation-with-gemini/.prettierrc.json new file mode 100644 index 00000000..0a725205 --- /dev/null +++ b/node/image-generation-with-gemini/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/node/image-generation-with-gemini/README.md b/node/image-generation-with-gemini/README.md new file mode 100644 index 00000000..4ea436c8 --- /dev/null +++ b/node/image-generation-with-gemini/README.md @@ -0,0 +1,108 @@ +# ⚡ Image Generation with Gemini (Node) + +Generate images using Google's Gemini API, save them locally, and automatically upload them to an Appwrite Storage bucket. + +## 🧰 Usage + +### CLI + +- Generate and upload to Appwrite Storage + +```powershell +node src/main.js "A cat running inside a stadium" +``` + + + +### Bucket setup helper (optional) + +Ensures the bucket exists (creates it if missing): + +```powershell +node src/setup.js +``` +Or the upload function in utils.js does this itself. + +Images are written to `./output` as `gemini-image-.png`. + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | ---------------------------- | +| Runtime | Node (>=18) | +| Entrypoint | `src/main.js` | +| Output folder | `output/` | +| Upload provider | Appwrite Storage | +| Bucket (default) | `Generated_Images` | + +> Note: If you plan to run this as an Appwrite Function, set Entrypoint to `src/main.js` and ensure the environment variables below are configured in Appwrite as well. + +## 🔒 Environment Variables + +Set these in a local `.env` file (or inject in your platform): + +| Variable | Required | Sample Value | Notes | +| --------------------------------- | -------- | -------------------------------------------- | ---------------------------------------------------------------- | +| `GEMINI_API_KEY` | Yes | `AIza...` | Google Gemini API key | +| `GEMINI_MODEL` | No | `gemini-2.0-flash-preview-image-generation` | Defaults to the value shown | + +| `APPWRITE_FUNCTION_API_ENDPOINT` | Yes\* | `https://cloud.appwrite.io/v1` | Appwrite endpoint | +| `APPWRITE_FUNCTION_PROJECT_ID` | Yes* | `YOUR_PROJECT_ID` | Appwrite project ID | +| `APPWRITE_FUNCTION_API_KEY` | Yes* | `YOUR_API_KEY` | Appwrite API key with Storage permissions | +| `APPWRITE_BUCKET_ID` | No | `Generated_Images` | Bucket ID used/created | + +\* Required only when uploading to Appwrite. + +Example `.env` (placeholder values): + +```dotenv +GEMINI_API_KEY=YOUR_GEMINI_API_KEY +GEMINI_MODEL=gemini-2.0-flash-preview-image-generation + + +# Appwrite creds (needed when uploading) +APPWRITE_FUNCTION_API_ENDPOINT=https://cloud.appwrite.io/v1 +APPWRITE_FUNCTION_PROJECT_ID=YOUR_PROJECT_ID +APPWRITE_FUNCTION_API_KEY=YOUR_API_KEY +APPWRITE_BUCKET_ID=Generated_Images +``` + + +## ✅ Expected output + +On success, you'll see logs like: + +```text +Image saved as D:\...\output\gemini-image-10.png +✓ Image created at: D:\...\output\gemini-image-10.png +📤 Uploading to Appwrite... +✓ Image uploaded to Appwrite with file ID: +✓ Upload complete! File ID: +``` + + +## 🧯 Troubleshooting + + +- "Cannot read properties of undefined (reading 'size')" + - Run `npm install` to ensure dependencies are present + + - Use Node 18+ (the SDK relies on `fetch`, `File`, etc.) + +- 401 / 403 errors when uploading + - Check `APPWRITE_FUNCTION_API_KEY` and `APPWRITE_FUNCTION_PROJECT_ID` + - Ensure the API key has Storage permissions + + +## 📦 Install + +```powershell +npm install +``` + +Optionally, format code: + +```powershell +npm run format +``` + diff --git a/node/image-generation-with-gemini/env.d.ts b/node/image-generation-with-gemini/env.d.ts new file mode 100644 index 00000000..3f6f016c --- /dev/null +++ b/node/image-generation-with-gemini/env.d.ts @@ -0,0 +1,18 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + // Google Gemini + GEMINI_API_KEY: string; + GEMINI_MODEL?: string; + + // Appwrite + APPWRITE_FUNCTION_API_ENDPOINT?: string; + APPWRITE_FUNCTION_PROJECT_ID?: string; + APPWRITE_FUNCTION_API_KEY?: string; + APPWRITE_BUCKET_ID?: string; + + } + } +} + +export {}; diff --git a/node/image-generation-with-gemini/output/gemini-image-1.png b/node/image-generation-with-gemini/output/gemini-image-1.png new file mode 100644 index 00000000..78e669e5 Binary files /dev/null and b/node/image-generation-with-gemini/output/gemini-image-1.png differ diff --git a/node/image-generation-with-gemini/package-lock.json b/node/image-generation-with-gemini/package-lock.json new file mode 100644 index 00000000..f7193d98 --- /dev/null +++ b/node/image-generation-with-gemini/package-lock.json @@ -0,0 +1,397 @@ +{ + "name": "image-generation-with-gemini", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "image-generation-with-gemini", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@google/genai": "^1.25.0", + "@google/generative-ai": "^0.24.1", + "dotenv": "^17.2.3", + "node-appwrite": "^14.1.0" + }, + "devDependencies": { + "prettier": "^3.6.2" + } + }, + "node_modules/@google/genai": { + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.25.0.tgz", + "integrity": "sha512-IBNyel/umavam98SQUfvQSvh/Rp6Ql2fysQLqPyWZr5K8d768X9AO+JZU4o+3qvFDUBA0dVYUSkxyYonVcICvA==", + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^9.14.2", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.11.4" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gcp-metadata": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", + "integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==", + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^6.1.1", + "google-logging-utils": "^0.0.2", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library": { + "version": "9.15.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz", + "integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==", + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-logging-utils": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", + "integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "license": "MIT", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/node-appwrite": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/node-appwrite/-/node-appwrite-14.2.0.tgz", + "integrity": "sha512-sPPA+JzdBJRS+lM6azX85y3/6iyKQYlHcXCbjMuWLROh6IiU9EfXRW3XSUTa5HDoBrlo8ve+AnVA6BIjQfUs1g==", + "license": "BSD-3-Clause", + "dependencies": { + "node-fetch-native-with-agent": "1.7.2" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native-with-agent": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-fetch-native-with-agent/-/node-fetch-native-with-agent-1.7.2.tgz", + "integrity": "sha512-5MaOOCuJEvcckoz7/tjdx1M6OusOY6Xc5f459IaruGStWnKzlI1qpNgaAwmn4LmFYcsSlj+jBMk84wmmRxfk5g==", + "license": "MIT" + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/node/image-generation-with-gemini/package.json b/node/image-generation-with-gemini/package.json new file mode 100644 index 00000000..c0291cbc --- /dev/null +++ b/node/image-generation-with-gemini/package.json @@ -0,0 +1,22 @@ +{ + "name": "image-generation-with-gemini", + "version": "1.0.0", + "description": "Generate images using gemini API and upload to appwrite storage automatically.", + "main": "src/main.js", + "author": "imnb57", + "license": "ISC", + "type": "module", + "scripts": { + "format": "prettier --write .", + "setup": "node src/setup.js" + }, + "keywords": [], + "devDependencies": { + "prettier": "^3.6.2" + }, + "dependencies": { + "@google/genai": "^1.25.0", + "node-appwrite": "^14.1.0", + "dotenv": "^17.2.3" + } +} diff --git a/node/image-generation-with-gemini/src/appwrite.js b/node/image-generation-with-gemini/src/appwrite.js new file mode 100644 index 00000000..66bb4ffd --- /dev/null +++ b/node/image-generation-with-gemini/src/appwrite.js @@ -0,0 +1,94 @@ +import { Client, Databases, ID, Storage } from 'node-appwrite'; +import fs from 'node:fs'; +import path from 'node:path'; +// IMPORTANT: Storage.createFile looks for an instance of File from 'node-fetch-native-with-agent' +// (used internally by the Appwrite Node SDK). Passing a Blob will fail with `reading 'size'`. +import { File } from 'node-fetch-native-with-agent'; + +class AppwriteService { + constructor(apiKey) { + const endpoint = process.env.APPWRITE_FUNCTION_API_ENDPOINT; + const projectId = process.env.APPWRITE_FUNCTION_PROJECT_ID; + + if (!endpoint || !projectId) { + throw new Error( + "APPWRITE_FUNCTION_API_ENDPOINT and APPWRITE_FUNCTION_PROJECT_ID must be set" + ); + } + + const client = new Client(); + client + .setEndpoint(endpoint) + .setProject(projectId) + .setKey(apiKey); + + this.databases = new Databases(client); + this.storage = new Storage(client); + } + + /** + * Create a file in Appwrite Storage. + * Accepts a Web File, Blob, or Buffer. Will normalize to the File class that Appwrite expects. + * @param {string} bucketId + * @param {File|Blob|Buffer} fileLike + * @param {object} opts + * @param {string} [opts.fileName] + * @param {string} [opts.mimeType] + */ + async createFile(bucketId, fileLike, opts = {}) { + const fileName = opts.fileName || 'image.png'; + const mimeType = opts.mimeType || 'image/png'; + + let file = fileLike; + // Normalize to File type expected by SDK + if (!(file instanceof File)) { + if (file && typeof file.arrayBuffer === 'function') { + // It's a Blob + const ab = await file.arrayBuffer(); + file = new File([ab], fileName, { type: mimeType }); + } else if (Buffer.isBuffer(file)) { + file = new File([file], fileName, { type: mimeType }); + } else { + throw new Error('createFile expects a File, Blob, or Buffer'); + } + } + + return await this.storage.createFile(bucketId, ID.unique(), file); + } + + /** + * Create a file from a local filesystem path. + * @param {string} bucketId + * @param {string} filePath + */ + async createFileFromPath(bucketId, filePath) { + const buffer = fs.readFileSync(filePath); + const fileName = path.basename(filePath); + const file = new File([buffer], fileName, { type: 'image/png' }); + return await this.storage.createFile(bucketId, ID.unique(), file); + } + + /** + * @param {string} bucketId + * @returns {Promise} + */ + async doesGeneratedImageBucketExist(bucketId) { + try { + await this.storage.getBucket(bucketId); + return true; + } catch (err) { + if (err.code === 404) return false; + throw err; + } + } + + async setupGeneratedImageBucket(bucketId) { + try { + await this.storage.createBucket(bucketId, 'Image generated'); + } catch (err) { + if (err.code !== 409) throw err; + } + } +} + +export default AppwriteService; diff --git a/node/image-generation-with-gemini/src/main.js b/node/image-generation-with-gemini/src/main.js new file mode 100644 index 00000000..32d58aa2 --- /dev/null +++ b/node/image-generation-with-gemini/src/main.js @@ -0,0 +1,16 @@ +// main.js +import { generateImage, uploadImageToAppwrite } from "./utils.js"; + +const prompt = process.argv[2] || "A futuristic city at sunset"; + +(async () => { + try { + const imagePath = await generateImage(prompt); + console.log(`Image created at: ${imagePath}`); + console.log("Uploading to Appwrite..."); + const uploadResult = await uploadImageToAppwrite(imagePath); + console.log(`Upload complete! File ID: ${uploadResult.$id}`); + } catch (error) { + console.error("Failed:", error.message); + } +})(); diff --git a/node/image-generation-with-gemini/src/setup.js b/node/image-generation-with-gemini/src/setup.js new file mode 100644 index 00000000..f6e9cb5e --- /dev/null +++ b/node/image-generation-with-gemini/src/setup.js @@ -0,0 +1,29 @@ +import dotenv from 'dotenv'; +import AppwriteService from './appwrite.js'; + +dotenv.config(); + +async function setup() { + try { + const apiKey = process.env.APPWRITE_FUNCTION_API_KEY; + if (!apiKey) { + throw new Error("APPWRITE_FUNCTION_API_KEY is not set in environment variables"); + } + + const bucketId = process.env.APPWRITE_BUCKET_ID ?? 'Generated_Images'; + + const appwrite = new AppwriteService(apiKey); + + if (await appwrite.doesGeneratedImageBucketExist(bucketId)) { + console.log(`Bucket exists.`); + } else { + await appwrite.setupGeneratedImageBucket(bucketId); + console.log(`Bucket created.`); + } + } catch (error) { + console.error("Setup failed:", error.message); + process.exit(1); + } +} + +setup(); diff --git a/node/image-generation-with-gemini/src/utils.js b/node/image-generation-with-gemini/src/utils.js new file mode 100644 index 00000000..2fedd1ff --- /dev/null +++ b/node/image-generation-with-gemini/src/utils.js @@ -0,0 +1,99 @@ +import { GoogleGenAI,Modality } from "@google/genai"; +import fs from "node:fs"; +import path from "path"; +import { fileURLToPath } from 'url'; +import dotenv from 'dotenv' +import AppwriteService from './appwrite.js'; +dotenv.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + + +const ai = new GoogleGenAI({ +apiKey:process.env.GEMINI_API_KEY +}); + +// Ensure output directory exists +const outputDir = path.resolve(__dirname, "../output"); +if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir); +} + +// Get next available image number +function getNextImageNumber(baseName = "gemini-image") { + const files = fs.readdirSync(outputDir) + .filter(f => f.startsWith(baseName) && f.endsWith(".png")); + const numbers = files.map(f => { + const match = f.match(/(\d+)\.png$/); + return match ? parseInt(match[1], 10) : 0; + }); + return numbers.length ? Math.max(...numbers) + 1 : 1; +} + +export async function generateImage(prompt) { + const imageNumber = getNextImageNumber(); + const outputName = path.join(outputDir, `gemini-image-${imageNumber}.png`); + try { + const response = await ai.models.generateContent({ + model: process.env.GEMINI_MODEL || 'gemini-2.0-flash-preview-image-generation', + contents: prompt, + config: { responseModalities: [Modality.TEXT, Modality.IMAGE] } + }); + + // Validate that we got at least one candidate back + if (!response.candidates || response.candidates.length === 0) { + throw new Error("No candidates returned from Gemini API"); + } + + const candidate = response.candidates[0]; + // Validate structure of the first candidate + if (!candidate.content || !candidate.content.parts) { + throw new Error("Invalid response structure from Gemini API"); + } + + for (const part of candidate.content.parts) { + if (part.inlineData) { + const buffer = Buffer.from(part.inlineData.data, "base64"); + fs.writeFileSync(outputName, buffer); + console.log(`Image saved as ${outputName}`); + return outputName; + } + } + + throw new Error("No image data returned from Gemini API"); + } catch (err) { + console.error("Error generating image:", err); + throw err; + } +} + +export async function uploadImageToAppwrite(imagePath) { + try { + const bucketId = process.env.APPWRITE_BUCKET_ID ?? 'Generated_Images'; + const apiKey = process.env.APPWRITE_FUNCTION_API_KEY; + + if (!apiKey) { + throw new Error("APPWRITE_FUNCTION_API_KEY is not set in environment variables"); + } + + const appwrite = new AppwriteService(apiKey); + + // Check if bucket exists, create if it doesn't + if (!(await appwrite.doesGeneratedImageBucketExist(bucketId))) { + await appwrite.setupGeneratedImageBucket(bucketId); + console.log(`Bucket '${bucketId}' created`); + } + + // Upload the file + const result = await appwrite.createFileFromPath(bucketId, imagePath); + console.log(`Image uploaded to Appwrite with file ID: ${result.$id}`); + + return result; + } catch (err) { + console.error("Error uploading image to Appwrite:", err.message); + throw err; + } +} +