├── assets └── banner.png ├── .npmignore ├── src ├── index.ts ├── Constants.ts ├── Types.ts ├── Prompt.ts ├── Account.ts ├── Image.ts ├── cli.ts └── ImageFX.ts ├── tsconfig.json ├── examples ├── 3-account-operations.ts ├── 1-generate-and-save.ts ├── 5-generate-caption.ts ├── 4-fetch-from-id.ts └── 2-detailed-prompt.ts ├── tests ├── 1-account.test.ts ├── 4-captions.test.ts ├── 3-image.test.ts └── 2-generate.test.ts ├── package.json ├── .github └── workflows │ └── release.yaml ├── LICENSE ├── .gitignore ├── bun.lock └── README.md /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rohitaryal/imageFX-api/HEAD/assets/banner.png -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ 2 | tests/ 3 | example/ 4 | bun.lock 5 | package-lock.json 6 | tsconfig.json 7 | .github/ 8 | assets/ -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { ImageFX, ImageFXError } from "./ImageFX.js"; 2 | export { Account, AccountError } from "./Account.js"; 3 | export { Prompt, PromptError } from "./Prompt.js"; 4 | export { Image, ImageError } from "./Image.js"; 5 | export { AspectRatio, Model, DefaultHeader } from "./Constants.js"; 6 | export * from "./Types.js"; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "outDir": "./dist", 6 | "declaration": true, 7 | "moduleResolution": "bundler", 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "strict": true 11 | }, 12 | "include": [ 13 | "src" 14 | ] 15 | } -------------------------------------------------------------------------------- /examples/3-account-operations.ts: -------------------------------------------------------------------------------- 1 | import { Account } from "../src/index"; 2 | 3 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 4 | if (!GOOGLE_COOKIE) { 5 | console.log("Cookie is missing :("); 6 | process.exit(1); 7 | } 8 | 9 | // You might not need this at all 10 | 11 | const account = new Account(GOOGLE_COOKIE); 12 | 13 | await account.refreshSession(); // Generates auth token from cookie 14 | 15 | console.log("Username: " + account.user?.name) 16 | console.log("Token: " + account.token); 17 | -------------------------------------------------------------------------------- /examples/1-generate-and-save.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX } from "../src/index"; 2 | 3 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 4 | if (!GOOGLE_COOKIE) { 5 | console.log("Cookie is missing :("); 6 | process.exit(1); 7 | } 8 | 9 | const fx = new ImageFX(GOOGLE_COOKIE); 10 | const generatedImage = await fx.generateImage("A big black cockroach"); 11 | 12 | generatedImage.forEach(image => { 13 | const savedPath = image.save(".cache/"); 14 | console.log("[+] Image saved at: " + savedPath); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/1-account.test.ts: -------------------------------------------------------------------------------- 1 | import { Account } from "../src/index"; 2 | import { expect, test } from "bun:test"; 3 | 4 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 5 | if (!GOOGLE_COOKIE) process.exit(1); 6 | 7 | test("Refresh authorization token", async () => { 8 | const account = new Account(GOOGLE_COOKIE); 9 | await account.refreshSession(); // Complete the test 10 | 11 | expect(account.token).toBeDefined(); 12 | expect(account.user).toBeDefined(); 13 | expect(account.isTokenExpired()).toBe(false); 14 | }); -------------------------------------------------------------------------------- /examples/5-generate-caption.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX } from "../src/index"; 2 | 3 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 4 | if (!GOOGLE_COOKIE) { 5 | console.log("Cookie is missing :("); 6 | process.exit(1); 7 | } 8 | 9 | const fx = new ImageFX(GOOGLE_COOKIE); 10 | 11 | // I want to generate 3 captions using this image 12 | const captions = await fx.generateCaptionsFromImage("/home/erucix/Downloads/1.webp", "webp", 3); 13 | 14 | // Lets print all generated captions 15 | captions.map(caption => console.log("==> " + caption + "\n\n")); 16 | -------------------------------------------------------------------------------- /examples/4-fetch-from-id.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX } from "../src/index"; 2 | 3 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 4 | if (!GOOGLE_COOKIE) { 5 | console.log("Cookie is missing :("); 6 | process.exit(1); 7 | } 8 | 9 | const fx = new ImageFX(GOOGLE_COOKIE); 10 | 11 | // Keep the mediaId here (available in image.mediaId) 12 | // And make sure the id actually belongs to you or public 13 | const image = await fx.getImageFromId("__PUT__ID__HERE__"); 14 | 15 | const savedPath = image.save(".cache/"); 16 | 17 | console.log("[+] Image saved at: " + savedPath); 18 | -------------------------------------------------------------------------------- /tests/4-captions.test.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX } from "../src/index"; 2 | import { test, expect } from "bun:test"; 3 | 4 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 5 | if (!GOOGLE_COOKIE) process.exit(1); 6 | 7 | const fx = new ImageFX(GOOGLE_COOKIE); 8 | 9 | test("Generating 1 caption", async () => { 10 | const captions = await fx.generateCaptionsFromImage("assets/banner.png", "png"); 11 | 12 | expect(captions).toBeDefined() 13 | expect(captions).toBeArray() 14 | expect(captions.length).toBeGreaterThan(0) 15 | expect(captions[0]).toBeString() 16 | }, 30000); -------------------------------------------------------------------------------- /tests/3-image.test.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX } from "../src/index"; 2 | import { test, expect } from "bun:test"; 3 | 4 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 5 | // Please provide mediaGenerationId of any of your generated images 6 | const MEDIA_ID = process.env.MEDIA_ID || "CAMaJGZkNDAzMDgyLWE1YTMtNDc2Yy1iOTVmLTVhNzc4NWE1NDM2ZSIDQ0FFKiRhZWE5M2FmNC1jY2Y3LTQyZjQtYTZjNy1iMzE1ZDgxMWNhZmM"; 7 | if (!GOOGLE_COOKIE || !MEDIA_ID) process.exit(1); 8 | 9 | const fx = new ImageFX(GOOGLE_COOKIE); 10 | 11 | test("Get image from ID", async () => { 12 | const image = await fx.getImageFromId(MEDIA_ID); 13 | 14 | expect(image).toBeDefined() 15 | expect(image.mediaId).toBe(MEDIA_ID!); 16 | }, 30000); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rohitaryal/imagefx-api", 3 | "version": "3.0.2", 4 | "main": "./dist/index.js", 5 | "type": "module", 6 | "types": "./dist/index.d.ts", 7 | "exports": { 8 | "import": "./dist/index.js", 9 | "require": "./dist/index.js" 10 | }, 11 | "bin": { 12 | "imagefx": "./dist/cli.js" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsc && chmod +x ./dist/cli.js", 19 | "prepublishOnly": "npm run build", 20 | "test": "bun test --bail" 21 | }, 22 | "devDependencies": { 23 | "@types/bun": "^1.2.21", 24 | "@types/yargs": "^17.0.33", 25 | "typescript": "^5.9.2" 26 | }, 27 | "dependencies": { 28 | "yargs": "^18.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/2-detailed-prompt.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX, Prompt } from "../src/index"; 2 | 3 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 4 | if (!GOOGLE_COOKIE) { 5 | console.log("Cookie is missing :("); 6 | process.exit(1); 7 | } 8 | 9 | const fx = new ImageFX(GOOGLE_COOKIE); 10 | const prompt = new Prompt({ 11 | seed: 0, 12 | numberOfImages: 4, 13 | prompt: "A guy who likes spongebob", // Don't judge me 14 | aspectRatio: "IMAGE_ASPECT_RATIO_SQUARE", 15 | generationModel: "IMAGEN_3_5", 16 | }) 17 | 18 | const generatedImage = await fx.generateImage(prompt); 19 | 20 | generatedImage.forEach(image => { 21 | const savedPath = image.save(".cache/"); 22 | console.log("[+] Image saved at: " + savedPath); 23 | }); 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: "Test and release to NPM.JS" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | test: 11 | name: Run bun test 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | MEDIA_ID: ${{ secrets.MEDIA_ID }} 16 | GOOGLE_COOKIE: ${{ secrets.GOOGLE_COOKIE }} 17 | 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | 22 | - name: Install Bun 23 | uses: oven-sh/setup-bun@v2 24 | 25 | - name: Install dependencies 26 | run: bun install 27 | 28 | - name: Run test 29 | run: bun test 30 | 31 | publish: 32 | name: Publish to NPM 33 | runs-on: ubuntu-latest 34 | needs: test 35 | env: 36 | NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | 38 | steps: 39 | - name: Checkout 40 | uses: actions/checkout@v4 41 | 42 | - name: Install Bun 43 | uses: oven-sh/setup-bun@v2 44 | 45 | - name: Install Dependencies 46 | run: | 47 | bun install 48 | 49 | - name: Publish to NPM 50 | run: bun publish -p --access public 51 | 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Rohit Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/Constants.ts: -------------------------------------------------------------------------------- 1 | export const Model = Object.freeze({ 2 | IMAGEN_3_5: "IMAGEN_3_5", 3 | } as const); 4 | 5 | export const AspectRatio = Object.freeze({ 6 | SQUARE: "IMAGE_ASPECT_RATIO_SQUARE", 7 | PORTRAIT: "IMAGE_ASPECT_RATIO_PORTRAIT", 8 | LANDSCAPE: "IMAGE_ASPECT_RATIO_LANDSCAPE", 9 | MOBILE_LANDSCAPE: "IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE", 10 | UNSPECIFIED: "IMAGE_ASPECT_RATIO_UNSPECIFIED", 11 | } as const); 12 | 13 | // Can be mutated and shared :) 14 | export const DefaultHeader = new Headers({ 15 | "Origin": "https://labs.google", 16 | "content-type": "application/json", 17 | "Referer": "https://labs.google/fx/tools/image-fx" 18 | }); 19 | 20 | export const ImageType = Object.freeze({ 21 | JPEG: "jpeg", 22 | JPG: "jpg", 23 | JPE: "jpe", 24 | PNG: "png", 25 | GIF: "gif", 26 | WEBP: "webp", 27 | SVG: "svg", 28 | BMP: "bmp", 29 | TIFF: "tiff", 30 | APNG: "apng", 31 | AVIF: "avif", 32 | } as const); 33 | 34 | export type Model = typeof Model[keyof typeof Model] 35 | export type AspectRatio = typeof AspectRatio[keyof typeof AspectRatio] 36 | export type ImageType = typeof ImageType[keyof typeof ImageType]; -------------------------------------------------------------------------------- /src/Types.ts: -------------------------------------------------------------------------------- 1 | import { AspectRatio, Model } from "./Constants.js"; 2 | 3 | export interface PromptArg { 4 | /** 5 | * A specific number that serves as the starting point 6 | * for the random process used to create the image. 7 | * 8 | * Default value: `0` 9 | */ 10 | seed?: number; 11 | 12 | /** 13 | * Textual Description of image to be generated 14 | */ 15 | prompt: string; 16 | 17 | /** 18 | * Number of image to generate in one fetch request. 19 | * Max may be `8` but changes with different account. 20 | * 21 | * Default value: `1` 22 | */ 23 | numberOfImages?: number; 24 | 25 | /** 26 | * The ratio of the width to the height of the image 27 | * to be generated. 28 | * 29 | * Available aspect ratios: 30 | * - `"IMAGE_ASPECT_RATIO_SQUARE"` 31 | * - `"IMAGE_ASPECT_RATIO_PORTRAIT"` 32 | * - `"IMAGE_ASPECT_RATIO_LANDSCAPE"` 33 | * - `"IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE"` 34 | * - `"IMAGE_ASPECT_RATIO_UNSPECIFIED"` 35 | */ 36 | aspectRatio?: AspectRatio; 37 | 38 | /** 39 | * Model to use for image generation. 40 | * 41 | * Note: `"IMAGEN_3_5"` is probably `IMAGEN_4` 42 | * 43 | * Available models: 44 | * - `"IMAGEN_3_5"` 45 | */ 46 | generationModel?: Model; 47 | } 48 | 49 | export interface ImageArg { 50 | seed: number; 51 | modelNameType: Model; 52 | prompt: string; 53 | aspectRatio: AspectRatio; 54 | encodedImage: string; 55 | mediaGenerationId: string; 56 | workflowId: string; 57 | fingerprintLogRecordId: string; 58 | } 59 | 60 | export interface User { 61 | name: string; 62 | email: string; 63 | image: string; 64 | } 65 | 66 | export interface SessionData { 67 | user: User; 68 | expires: string; 69 | access_token: string; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /src/Prompt.ts: -------------------------------------------------------------------------------- 1 | import { AspectRatio, Model } from "./Constants.js"; 2 | import { PromptArg } from "./Types.js"; 3 | 4 | export class PromptError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | this.name = 'PromptError'; 8 | } 9 | } 10 | 11 | /** 12 | * Represents a prompt that describes visual property of image to be generated 13 | */ 14 | export class Prompt { 15 | /** 16 | * A specific number that serves as the starting point 17 | * for the random process used to create the image. 18 | * 19 | * Default value: `0` 20 | */ 21 | seed: number; 22 | /** 23 | * Textual Description of image to be generated 24 | */ 25 | prompt: string; 26 | /** 27 | * Number of image to generate in one fetch request. 28 | * Max may be `8` but changes with different account. 29 | * 30 | * Default value: `1` 31 | */ 32 | numberOfImages: number; 33 | /** 34 | * The ratio of the width to the height of the image 35 | * to be generated. 36 | * 37 | * Available aspect ratios: 38 | * - `"IMAGE_ASPECT_RATIO_SQUARE"` 39 | * - `"IMAGE_ASPECT_RATIO_PORTRAIT"` 40 | * - `"IMAGE_ASPECT_RATIO_LANDSCAPE"` 41 | * - `"IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE"` 42 | * - `"IMAGE_ASPECT_RATIO_UNSPECIFIED"` 43 | */ 44 | aspectRatio: AspectRatio; 45 | /** 46 | * Model to use for image generation. 47 | * 48 | * Note: `"IMAGEN_3_5"` is probably `IMAGEN_4` 49 | * 50 | * Available models: 51 | * - `"IMAGEN_3_5"` 52 | */ 53 | generationModel: Model; 54 | 55 | constructor(args: PromptArg) { 56 | this.seed = args.seed ?? 0; 57 | this.prompt = args.prompt; 58 | this.numberOfImages = args.numberOfImages ?? 1; 59 | this.aspectRatio = args.aspectRatio ?? AspectRatio.LANDSCAPE; 60 | this.generationModel = args.generationModel ?? Model.IMAGEN_3_5; 61 | } 62 | 63 | /** 64 | * Wrapper around `JSON.stringify` that stringifies prompt object 65 | * 66 | * @returns Stringified prompt in JSON format 67 | */ 68 | public toString() { 69 | return JSON.stringify({ 70 | "userInput": { 71 | "candidatesCount": this.numberOfImages, 72 | "prompts": [this.prompt], 73 | "seed": this.seed 74 | }, 75 | "clientContext": { 76 | "sessionId": ";1757113025397", 77 | "tool": "IMAGE_FX" 78 | }, 79 | "modelInput": { 80 | "modelNameType": this.generationModel 81 | }, 82 | "aspectRatio": this.aspectRatio 83 | }); 84 | } 85 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | .auth 132 | .png 133 | -------------------------------------------------------------------------------- /tests/2-generate.test.ts: -------------------------------------------------------------------------------- 1 | import { ImageFX, Prompt, AspectRatio, Model } from "../src/index" 2 | import { describe, expect, test } from "bun:test"; 3 | 4 | const GOOGLE_COOKIE = process.env.GOOGLE_COOKIE; 5 | if (!GOOGLE_COOKIE) process.exit(1); 6 | 7 | const fx = new ImageFX(GOOGLE_COOKIE); 8 | 9 | describe("Different Models", () => { 10 | Object.values(Model).forEach(model => { 11 | const prompt = new Prompt({ 12 | prompt: "A superhuman flying over mars", 13 | generationModel: model, 14 | }); 15 | 16 | test("Selected Model: " + model, async () => { 17 | prompt.generationModel = model; 18 | 19 | const generatedImages = await fx.generateImage(prompt); 20 | 21 | expect(generatedImages).toBeDefined(); 22 | expect(generatedImages.length).toBeGreaterThan(0); 23 | expect(generatedImages[0].model).toBe(model); 24 | }, 30000); 25 | }) 26 | }); 27 | 28 | describe("Aspect Ratios", () => { 29 | Object.values(AspectRatio).forEach(size => { 30 | test("Selected size: " + size, async () => { 31 | const prompt = new Prompt({ 32 | aspectRatio: size, 33 | prompt: "A friend forcing me to write code all the time", 34 | }); 35 | 36 | const generatedImages = await fx.generateImage(prompt); 37 | 38 | expect(generatedImages).toBeDefined(); 39 | expect(generatedImages.length).toBeGreaterThan(0); 40 | expect(generatedImages[0].aspectRatio).toBe(size); 41 | }, 30000); 42 | }) 43 | }); 44 | 45 | test("Image Seed", async () => { 46 | const seed = 1233; 47 | 48 | const prompt = new Prompt({ 49 | seed, 50 | prompt: "A crocodile eating an ice", 51 | }); 52 | 53 | const generatedImages = await fx.generateImage(prompt); 54 | 55 | expect(generatedImages).toBeDefined(); 56 | expect(generatedImages.length).toBeGreaterThan(0); 57 | expect(generatedImages[0].seed).toBe(seed); 58 | }, 30000); 59 | 60 | test("Multiple Parameters", async () => { 61 | const prompt = new Prompt({ 62 | prompt: "A green scary crocodile", 63 | aspectRatio: "IMAGE_ASPECT_RATIO_LANDSCAPE", 64 | generationModel: "IMAGEN_3_5", 65 | numberOfImages: 2, 66 | seed: 200 67 | }); 68 | 69 | const generatedImages = await fx.generateImage(prompt); 70 | 71 | expect(generatedImages).toBeDefined() 72 | 73 | expect(generatedImages).toBeDefined(); 74 | expect(generatedImages.length).toBeGreaterThan(0); /* Not 4, because sometime they dont always 75 | provide the specified number of images. */ 76 | expect(generatedImages[0].prompt).toBe(prompt.prompt); 77 | expect(generatedImages[0].aspectRatio).toBe(prompt.aspectRatio) 78 | expect(generatedImages[0].model).toBe(prompt.generationModel) 79 | expect(generatedImages[0].seed).toBe(prompt.seed); 80 | }, 30000); -------------------------------------------------------------------------------- /src/Account.ts: -------------------------------------------------------------------------------- 1 | import { DefaultHeader } from "./Constants.js"; 2 | import { SessionData, User } from "./Types.js"; 3 | 4 | export class AccountError extends Error { 5 | constructor(message: string, public readonly code?: string) { 6 | super(message); 7 | this.name = 'AccountError'; 8 | } 9 | } 10 | 11 | /** 12 | * Represents user account 13 | */ 14 | export class Account { 15 | /** 16 | * Basic user account detail 17 | */ 18 | public user?: User; 19 | /** 20 | * Authentication token used for image generation 21 | */ 22 | public token?: string; 23 | /** 24 | * Expiry date for token (authentication token) 25 | */ 26 | private tokenExpiry?: Date; 27 | /** 28 | * User account cookie 29 | */ 30 | private readonly cookie: string; 31 | 32 | constructor(cookie: string) { 33 | if (!cookie?.trim()) { 34 | throw new AccountError("Cookie is required and cannot be empty"); 35 | } 36 | 37 | this.cookie = cookie; 38 | } 39 | 40 | /** 41 | * Re-generates and updates authorization token internally 42 | */ 43 | public async refreshSession() { 44 | let sessionResult = await this.fetchSession(); 45 | 46 | if (!sessionResult || !sessionResult.access_token || !sessionResult.expires || !sessionResult.user) { 47 | throw new AccountError("Session response is missing some fields: \n" + JSON.stringify(sessionResult)) 48 | } 49 | 50 | this.user = sessionResult.user; 51 | this.token = sessionResult.access_token; 52 | this.tokenExpiry = new Date(sessionResult.expires); 53 | } 54 | 55 | /** 56 | * Check if current authorization token is expired (buffer: 30s) 57 | * 58 | * @returns Boolean representing if the token is expired 59 | */ 60 | public isTokenExpired() { 61 | if (!this.token || !this.tokenExpiry) { 62 | return true; 63 | } 64 | 65 | return this.tokenExpiry <= new Date(Date.now() - 30 * 1000); 66 | } 67 | 68 | /** 69 | * Returns headers object for authenticated requests. 70 | * You might not need this ever. 71 | * 72 | * @returns Headers of course 73 | */ 74 | public getAuthHeaders() { 75 | if (!this.token) { 76 | throw new AccountError("Cookie or Token is still missing after refresh"); 77 | } 78 | 79 | return new Headers({ 80 | ...DefaultHeader, 81 | "Cookie": this.cookie, 82 | "Authorization": "Bearer " + this.token, 83 | }) 84 | } 85 | 86 | /** 87 | * Fetches session update request's json from labs.google 88 | * 89 | * @returns Promise containing `SessionData` object which contains account session info. 90 | */ 91 | private async fetchSession() { 92 | const response = await fetch("https://labs.google/fx/api/auth/session", { 93 | headers: { ...DefaultHeader, "Cookie": this.cookie } 94 | }); 95 | 96 | if (!response.ok) { 97 | const errorText = await response.text(); 98 | throw new AccountError( 99 | `Authentication failed (${response.status}): ${errorText}` 100 | ); 101 | } 102 | 103 | const sessionData = await response.json() as SessionData; 104 | 105 | if (!sessionData.access_token || !sessionData.expires || !sessionData.user) { 106 | throw new AccountError("Invalid session response: missing required fields"); 107 | } 108 | 109 | return sessionData; 110 | } 111 | } -------------------------------------------------------------------------------- /src/Image.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { AspectRatio, Model } from "./Constants.js"; 3 | import { ImageArg } from "./Types.js"; 4 | import { existsSync, mkdirSync, writeFileSync, } from "fs"; 5 | 6 | export class ImageError extends Error { 7 | constructor(message: string) { 8 | super(message); 9 | this.name = "ImageError"; 10 | } 11 | } 12 | 13 | /** 14 | * Represents a generated image. 15 | */ 16 | export class Image { 17 | /** 18 | * Represents seed value used for this image generation. 19 | * Seed is a specific number that serves as the starting point 20 | * for the random process used to create the image. 21 | */ 22 | public readonly seed: number; 23 | /** 24 | * Model used to generate this imge. 25 | * 26 | * Note: `"IMAGEN_3_5"` is probably `IMAGEN_4` 27 | * 28 | * Available models: 29 | * - `"IMAGEN_3_5"` 30 | */ 31 | public readonly model: Model; // modelNameType 32 | 33 | /** 34 | * Textual description used to generate this image 35 | */ 36 | public readonly prompt: string; 37 | 38 | /** 39 | * Aspect ratio of this image 40 | */ 41 | public readonly aspectRatio: AspectRatio; 42 | 43 | /** 44 | * Unique id assigned to each generated image. 45 | * 46 | * `mediaId` can be used as identifier to download the image. 47 | */ 48 | public readonly mediaId: string; // mediaGenerationId 49 | 50 | /** 51 | * Generated `png` image encoded into base64. 52 | */ 53 | private readonly encodedImage: string; 54 | /** 55 | * Project id is what i can guess 56 | */ 57 | private readonly workflowId: string; 58 | /** 59 | * IDK what this is bro 60 | */ 61 | private readonly fingerprintId: string; // fingerprintLogRecordId 62 | 63 | constructor(args: ImageArg) { 64 | if (!args.encodedImage?.trim()) { 65 | throw new ImageError("Encoded image data is required"); 66 | } 67 | 68 | this.seed = args.seed; 69 | this.prompt = args.prompt; 70 | this.model = args.modelNameType; 71 | this.aspectRatio = args.aspectRatio; 72 | // Unrequired stuffs below :| 73 | this.workflowId = args.workflowId; 74 | this.encodedImage = args.encodedImage; 75 | this.mediaId = args.mediaGenerationId; 76 | this.fingerprintId = args.fingerprintLogRecordId; 77 | } 78 | 79 | /** 80 | * Saves image to specified path with timestamp as name. 81 | * Default path is current directory. 82 | * 83 | * @param filePath Directory for the image to be saved 84 | */ 85 | public save(filePath = ".") { 86 | // Prevent image name collison thus eliminating replacement 87 | const imageName = `image-${crypto.randomUUID().slice(-8)}.png`; 88 | 89 | if (!existsSync(filePath)) { 90 | console.log("[*] Creating destination dir:", filePath) 91 | 92 | try { 93 | mkdirSync(filePath, { recursive: true }); 94 | } catch (err) { 95 | throw new ImageError(`Failed to create directory ${filePath}: ${err instanceof Error ? err.message : 'Unknown error'}`); 96 | } 97 | } 98 | 99 | try { 100 | filePath = join(filePath, imageName); 101 | writeFileSync(filePath, this.encodedImage, "base64"); 102 | 103 | return filePath; 104 | } catch (error) { 105 | throw new ImageError("Failed to save image: " + (error instanceof Error ? error.message : "UNKNOWN ERROR")); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "configVersion": 0, 4 | "workspaces": { 5 | "": { 6 | "name": "@rohitaryal/imagefx-api", 7 | "dependencies": { 8 | "yargs": "^18.0.0", 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "^1.2.21", 12 | "@types/yargs": "^17.0.33", 13 | "typescript": "^5.9.2", 14 | }, 15 | }, 16 | }, 17 | "packages": { 18 | "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], 19 | 20 | "@types/node": ["@types/node@24.3.1", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g=="], 21 | 22 | "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], 23 | 24 | "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], 25 | 26 | "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], 27 | 28 | "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], 29 | 30 | "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], 31 | 32 | "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], 33 | 34 | "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], 35 | 36 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 37 | 38 | "emoji-regex": ["emoji-regex@10.5.0", "", {}, "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg=="], 39 | 40 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 41 | 42 | "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], 43 | 44 | "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 45 | 46 | "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], 47 | 48 | "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 49 | 50 | "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], 51 | 52 | "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], 53 | 54 | "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], 55 | 56 | "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], 57 | 58 | "yargs": ["yargs@18.0.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^7.2.0", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg=="], 59 | 60 | "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imageFX-api (imagen) 2 | Unofficial free reverse engineered api for imageFX(imagen) service provided by [labs.google](https://labs.google) 3 | 4 | ![Banner](https://raw.githubusercontent.com/rohitaryal/imageFX-api/refs/heads/main/assets/banner.png) 5 | 6 | ## Installation 7 | ```bash 8 | npm i -g @rohitaryal/imagefx-api 9 | ``` 10 | 11 | ## Features 12 | 1. Text to image using `IMAGEN_4` 13 | 2. Image to text 14 | 3. Command line support 15 | 16 | ## Usage 17 | `imagefx` can be invoked through both command line and as a module. 18 |
19 | Command Line 20 | 21 | Make sure you have: 22 | 1. Installed `imagefx` globally ([How to install?](#installation)) 23 | 2. Obtained your google account cookies ([How to get cookies?](#help)) 24 | 3. Set env variable `GOOGLE_COOKIE` containing your cookie 25 | 26 | Bash: 27 | ```bash 28 | export GOOGLE_COOKIE="__YOUR__COOKIE__HERE__" 29 | ``` 30 | Command Prompt: 31 | ```bat 32 | set "GOOGLE_COOKIE=__YOUR__COOKIE__HERE__" 33 | ``` 34 | Powershell: 35 | ```ps 36 | $GOOGLE_COOKIE = "__YOUR__GOOGLE__COOKIE__HERE__" 37 | ``` 38 | 39 | #### Basic Usages: 40 | 41 | > **NOTE:** 42 | > If you are using environment variables, keep the quotes around cookie to avoid word-splitting and authentication errors. 43 | > - Linux/macOS: `"$GOOGLE_COOKIE"` 44 | > - PowerShell: `"$env:GOOGLE_COOKIE"` 45 | > - Command Prompt: `"%GOOGLE_COOKIE%"` 46 | 47 | - Generating image with prompt 48 | 49 | ```bash 50 | # saves generated image at current directory 51 | imagefx generate --prompt "A bad friend" --cookie "$GOOGLE_COOKIE" 52 | ``` 53 | - Selecting a specific model 54 | ```bash 55 | # please refer to --help for listing all models 56 | imagefx generate --prompt "An evil company" --model "IMAGEN_3_5" --cookie "$GOOGLE_COOKIE" 57 | ``` 58 | - Selecting a specific aspect ratio 59 | ```bash 60 | # please refer to --help for listing all aspect ratio 61 | imagefx generate --prompt "Reptillian CEO" --size "PORTRAIT" --cookie "$GOOGLE_COOKIE" 62 | ``` 63 | - Saving to specific destination 64 | ```bash 65 | # it will automatically create non-existing directory if possible 66 | imagefx generate --prompt "Netflix but with less fees" --dir ~/Pictures --cookie "$GOOGLE_COOKIE" 67 | ``` 68 | - You can also save image using its media id. 69 | ```bash 70 | imagefx fetch "__MEDIA__ID__HERE__" --cookie "$GOOGLE_COOKIE" 71 | ``` 72 | - Generating prompt/caption using an image as reference. 73 | ```bash 74 | # supported image types: jpeg, jpg, jpe, png, gif, webp, svg, bmp, tiff, apng, avif (not tested with all) 75 | imagefx caption --image /path/to/img.webp --type WEBP --cookie "$GOOGLE_COOKIE" 76 | ``` 77 | Full generation help: 78 | ```text 79 | imagefx generate 80 | 81 | Options: 82 | --version Show version number 83 | -h, --help Show help 84 | -p, --prompt Textual description of image to be generated 85 | -m, --model Model to be used for image generation 86 | -n, --count Number of images to generate 87 | --size, --sz Aspect ratio of image to be generated 88 | -s, --seed Seed value for image to be generated 89 | -r, --retry Number of retries if in case fetch fails 90 | -d, --dir Directory to save generated images 91 | -c, --cookie Google account cookie 92 | ``` 93 | 94 | Full caption generation help: 95 | ```text 96 | Generate detailed caption(s) from image 97 | 98 | Options: 99 | --version Show version number 100 | -h, --help Show help 101 | -i, --image Path to the image to be captioned 102 | -t, --type Type of image (eg: png, jpeg, webp, etc) 103 | -n, --count Number of captions to generate 104 | -c, --cookie Google account cookie 105 | ``` 106 | 107 | Full fetching help: 108 | ```text 109 | imagefx fetch 110 | 111 | Positionals: 112 | mediaId Unique ID of generated image 113 | 114 | Options: 115 | --version Show version number 116 | -h, --help Show help 117 | -d, --dir Directory to save generated images 118 | -c, --cookie Google account cookie 119 | ``` 120 |
121 | 122 |
123 | Importing as module 124 | 125 | - Basic image generation 126 | 127 | ```typescript 128 | import { ImageFX } from "@rohitaryal/imagefx-api"; 129 | 130 | const fx = new ImageFX(process.env.GOOGLE_COOKIE); 131 | 132 | // Generate images 133 | const generatedImage = await fx.generateImage("A big black cockroach"); 134 | 135 | // Iterate over multiple images and save 136 | generatedImage.forEach(image => { 137 | const savedPath = image.save(".cache/"); 138 | console.log("[+] Image saved at: " + savedPath); 139 | }); 140 | ``` 141 | - More descriptive prompt 142 | ```typescript 143 | const fx = new ImageFX(GOOGLE_COOKIE); 144 | 145 | const prompt = new Prompt({ 146 | seed: 0, 147 | numberOfImages: 4, 148 | prompt: "A green spongebob", 149 | generationModel: "IMAGEN_3_5", 150 | aspectRatio: "IMAGE_ASPECT_RATIO_SQUARE", 151 | }); 152 | 153 | // Generate images 154 | const generatedImage = await fx.generateImage(prompt); 155 | 156 | // Iterate over generated images and save 157 | generatedImage.forEach(image => { 158 | const savedPath = image.save(".cache/"); 159 | console.log("[+] Image saved at: " + savedPath); 160 | }); 161 | ``` 162 | 163 | More examples are at: [/examples](https://github.com/rohitaryal/imageFX-api/tree/main/examples) 164 |
165 | 166 | ## Help 167 |
168 | How to extract cookies? 169 | 170 | #### Easy way: 171 | 1. Install [Cookie Editor](https://github.com/Moustachauve/cookie-editor) extension in your browser. 172 | 2. Open [labs.google](https://labs.google/fx/tools/image-fx), make sure you are logged in 173 | 3. Click on Cookie Editor icon from Extensions section. 174 | 4. Click on Export -> Header String 175 | 176 | #### Manual way: 177 | 1. Open [labs.google](https://labs.google/fx/tools/image-fx), make sure you are logged in 178 | 2. Press CTRL + SHIFT + I to open console 179 | 3. Click on Network tab at top of console 180 | 4. Press CTRL + L to clear network logs 181 | 5. Click CTRL + R to refresh page 182 | 6. Click on `image-fx` which should be at top 183 | 7. Goto Request Headers section and copy all the content of Cookie 184 | 185 |
186 | 187 |
188 | ImageFX not available in your country? 189 | 190 | 1. Install a free VPN (Windscribe, Proton, etc) 191 | 2. Open [labs.google](https://labs.google/fx/tools/image-fx) and login 192 | 3. From here follow the "How to extract cookie?" in [HELP](#help) section (above). 193 | 4. Once you have obtained this cookie, you don't need VPN anymore. 194 |
195 | 196 |
197 | Not able to generate images? 198 | 199 | Create an issue [here](https://github.com/rohitaryal/imageFX-api/issues). Make sure the pasted logs don't contain cookie or tokens. 200 |
201 | 202 | ## Contributions 203 | Contribution are welcome but ensure to pass all test cases and follow existing coding standard. 204 | 205 | ## Disclaimer 206 | This project demonstrates usage of Google's private API but is not affiliated with Google. Use at your own risk. -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import yargs from "yargs"; 3 | import { hideBin } from 'yargs/helpers'; 4 | import { AspectRatio, ImageType, Model } from "./Constants.js"; 5 | import { ImageFX } from "./ImageFX.js"; 6 | import { Prompt } from "./Prompt.js"; 7 | 8 | const y = yargs(); 9 | 10 | await y 11 | .scriptName("ImageFX") 12 | .command( 13 | "generate", 14 | "Generate new images", 15 | (yargs) => { 16 | return yargs 17 | .option("prompt", { 18 | alias: "p", 19 | describe: "Textual description of image to be generated", 20 | type: "string", 21 | }) 22 | .option("model", { 23 | alias: "m", 24 | describe: "Model to be used for image generation", 25 | type: "string", 26 | default: "IMAGEN_3_5", 27 | choices: Object.values(Model) 28 | }) 29 | .option("count", { 30 | alias: "n", 31 | describe: "Number of images to generate [Max: 8]", 32 | type: "number", 33 | default: 4, 34 | }) 35 | .option("size", { 36 | alias: "sz", 37 | describe: "Aspect ratio of image to be generated", 38 | type: "string", 39 | default: "LANDSCAPE", 40 | choices: Object.values(AspectRatio).map((value) => value.replace("IMAGE_ASPECT_RATIO_", "")), 41 | }) 42 | .option("seed", { 43 | alias: "s", 44 | describe: "Seed value for image to be generated", 45 | type: "number", 46 | default: 0, 47 | }) 48 | .option("retry", { 49 | alias: "r", 50 | describe: "Number of retries if in case fetch fails", 51 | type: "number", 52 | default: 1 53 | }) 54 | .option("dir", { 55 | alias: "d", 56 | describe: "Directory to save generated images", 57 | type: "string", 58 | default: ".", 59 | }) 60 | .option("cookie", { 61 | alias: "c", 62 | describe: "Google account cookie", 63 | type: "string", 64 | demandOption: true, 65 | }) 66 | }, 67 | async (argv) => { 68 | if (!argv.cookie) { 69 | console.log("Cookie value is missing :(") 70 | return; 71 | } 72 | 73 | if (!argv.prompt) { 74 | argv.prompt = "A prompt engineer who forgets to give prompt to AI"; 75 | } 76 | 77 | const fx = new ImageFX(argv.cookie); 78 | const prompt = new Prompt({ 79 | seed: argv.seed, 80 | prompt: argv.prompt, 81 | numberOfImages: argv.count, 82 | generationModel: argv.model as Model, 83 | aspectRatio: ("IMAGE_ASPECT_RATIO_" + argv.size) as AspectRatio, 84 | }); 85 | 86 | console.log("[*] Generating. Please wait..."); 87 | 88 | const generatedImages = await fx.generateImage(prompt, argv.retry); 89 | generatedImages.forEach((image, index) => { 90 | try { 91 | const savedPath = image.save(argv.dir); 92 | console.log("[+] Image saved at:", savedPath); 93 | } catch (error) { 94 | console.log("[!] Failed to save an image:", error); 95 | } 96 | }); 97 | 98 | return; 99 | } 100 | ) 101 | .command( 102 | "caption", 103 | "Generate detailed caption(s) from image", 104 | (yargs) => { 105 | return yargs 106 | .option("image", { 107 | alias: "i", 108 | describe: "Path to the image to be captioned", 109 | type: "string", 110 | demandOption: true, 111 | }) 112 | .option("type", { 113 | alias: "t", 114 | describe: "Type of image (eg: png, jpeg, webp, etc)", 115 | type: "string", 116 | demandOption: true, 117 | }) 118 | .option("count", { 119 | alias: "n", 120 | describe: "Number of captions to generate", 121 | type: "number", 122 | default: 1, 123 | }) 124 | .option("cookie", { 125 | alias: "c", 126 | describe: "Google account cookie", 127 | type: "string", 128 | demandOption: true, 129 | }) 130 | }, 131 | async (argv) => { 132 | if (!argv.cookie) { 133 | console.log("Cookie value is missing :(") 134 | return; 135 | } 136 | 137 | if (!argv.image) { 138 | console.log("Image is not provided"); 139 | return; 140 | } 141 | 142 | if (!(argv.type in ImageType)) { 143 | console.log("Invalid image type provided, valid types are: \n" + 144 | Object.keys(ImageType).join(", ")); 145 | return; 146 | } 147 | 148 | const fx = new ImageFX(argv.cookie); 149 | 150 | console.log("[*] Generating captions ..."); 151 | 152 | const generatedCaptions = await fx.generateCaptionsFromImage(argv.image, 153 | ImageType[argv.type as keyof typeof ImageType], // Safe typecast ;) 154 | argv.count, 155 | ); 156 | 157 | generatedCaptions.forEach((caption, index) => { 158 | console.log(`[${index + 1}] ${caption}`) 159 | }); 160 | 161 | return; 162 | } 163 | ) 164 | .command( 165 | "fetch ", 166 | "Download a generated image with its mediaId", 167 | (yargs) => { 168 | return yargs 169 | .positional("mediaId", { 170 | describe: "Unique ID of generated image", 171 | type: "string", 172 | demandOption: true, 173 | }) 174 | .option("dir", { 175 | alias: "d", 176 | describe: "Directory to save generated images", 177 | default: ".", 178 | type: "string", 179 | }) 180 | .option("cookie", { 181 | alias: "c", 182 | describe: "Google account cookie", 183 | type: "string", 184 | demandOption: true, 185 | }) 186 | }, 187 | async (argv) => { 188 | const fx = new ImageFX(argv.cookie); 189 | const fetchedImage = await fx.getImageFromId(argv.mediaId); 190 | 191 | try { 192 | const savedPath = fetchedImage.save(argv.dir); 193 | console.log("[+] Image saved at:", savedPath) 194 | } catch (error) { 195 | console.log("[!] Failed to save an image:", error) 196 | } 197 | 198 | return; 199 | } 200 | ) 201 | .demandCommand(1, "You need to use at least one command") 202 | .wrap(Math.min(y.terminalWidth(), 150)) 203 | .help() 204 | .alias("help", "h") 205 | .parse(hideBin(process.argv)); -------------------------------------------------------------------------------- /src/ImageFX.ts: -------------------------------------------------------------------------------- 1 | import { Account } from "./Account.js"; 2 | import { Prompt } from "./Prompt.js"; 3 | import { Image } from "./Image.js"; 4 | import { ImageArg } from "./Types.js"; 5 | import { existsSync, readFileSync } from "fs"; 6 | import { ImageType } from "./Constants.js"; 7 | 8 | export class ImageFXError extends Error { 9 | constructor(message: string) { 10 | super(message); 11 | this.name = "ImageFXError"; 12 | } 13 | } 14 | 15 | /** 16 | * Consider this the entry/main class 17 | */ 18 | export class ImageFX { 19 | /** 20 | * Represents user account and contains session info, cookies, etc 21 | */ 22 | private readonly account: Account; 23 | 24 | constructor(cookie: string) { 25 | if (!cookie?.trim()) { 26 | throw new ImageFXError("Cookie is required and cannot be empty"); 27 | } 28 | 29 | this.account = new Account(cookie); 30 | } 31 | 32 | /** 33 | * Generates image from a given prompt 34 | * 35 | * @param prompt Description of image 36 | * @param retries Number of retries 37 | * @returns List containing generated image(s) 38 | */ 39 | public async generateImage(prompt: string | Prompt, retries = 0) { 40 | if (typeof prompt === "string") { 41 | if (!prompt.trim()) { 42 | throw new ImageFXError("Prompt cannot be empty") 43 | } 44 | prompt = new Prompt({ prompt }); 45 | } 46 | 47 | if (!(prompt instanceof Prompt)) { 48 | throw new ImageFXError("Provided prompt is not an instance of Prompt") 49 | } 50 | 51 | await this.account.refreshSession() 52 | 53 | const generatedImages = await this.fetchImages(prompt, retries); 54 | return generatedImages.map((data: ImageArg) => new Image(data)); 55 | } 56 | 57 | /** 58 | * Gets generated image from its unique media ID (`image.mediaID`) 59 | * @param id Unique media id for a generated image 60 | * @returns Returns image identified by its `id` 61 | */ 62 | public async getImageFromId(id: string) { 63 | if (!id?.trim()) { 64 | throw new ImageFXError("Image ID is required and cannot be empty"); 65 | } 66 | 67 | await this.account.refreshSession(); 68 | 69 | const url = "https://labs.google/fx/api/trpc/media.fetchMedia"; 70 | const params = encodeURIComponent( 71 | JSON.stringify({ json: { mediaKey: id } }) 72 | ); 73 | 74 | try { 75 | const response = await fetch(`${url}?input=${params}`, { 76 | headers: this.account.getAuthHeaders(), 77 | }); 78 | 79 | if (!response.ok) { 80 | const errorText = await response.text(); 81 | throw new ImageFXError(`Server responded with unexpected response (${response.status}): ${errorText}`); 82 | } 83 | 84 | const parsedResponse = await response.json(); 85 | const requestedImage = parsedResponse?.result?.data?.json?.result?.image; 86 | 87 | if (!requestedImage) { 88 | throw new ImageFXError("Server responded with empty image"); 89 | } 90 | 91 | delete requestedImage.mediaVisibility; 92 | delete requestedImage.previousMediaGenerationId; 93 | 94 | return new Image(requestedImage); 95 | } catch (error) { 96 | if (error instanceof ImageFXError) { 97 | throw error; 98 | } 99 | 100 | throw new ImageFXError(`Failed to fetch image: ${error instanceof Error ? error.message : 'Unknown error'}`); 101 | } 102 | } 103 | 104 | /** 105 | * Generate a detailed caption from an image. 106 | * 107 | * @param imagePath Path to the image to be used 108 | * @param count Number of captions to generate 109 | * @param imageType Type of image (png, jpeg, yada yada) 110 | * @returns Array with `count` number of captions (if you are lucky) 111 | */ 112 | public async generateCaptionsFromImage(imagePath: string, imageType: ImageType, count: number = 1) { 113 | if (!existsSync(imagePath)) { 114 | throw new ImageFXError("Image doesn't exist at path: " + imagePath); 115 | } 116 | 117 | let base64EncodedImage: string = ""; 118 | 119 | try { 120 | base64EncodedImage = readFileSync(imagePath, "base64"); 121 | base64EncodedImage = `data:image/${imageType};base64,${base64EncodedImage}`; 122 | } catch (error) { 123 | throw new ImageFXError(`Failed to fetch image: ${error instanceof Error ? error.message : 'Unknown error'}`); 124 | } 125 | 126 | await this.account.refreshSession(); 127 | 128 | const url = "https://labs.google/fx/api/trpc/backbone.captionImage"; 129 | const body = JSON.stringify({ 130 | "json": { 131 | "clientContext": { "sessionId": ";1758297717089", "workflowId": "" }, 132 | "captionInput": { 133 | "candidatesCount": count, 134 | "mediaInput": { 135 | "mediaCategory": "MEDIA_CATEGORY_SUBJECT", 136 | "rawBytes": base64EncodedImage, 137 | } 138 | } 139 | } 140 | }); 141 | 142 | let response: Response; 143 | 144 | try { 145 | response = await fetch(url, { 146 | body, 147 | method: "POST", 148 | headers: this.account.getAuthHeaders() 149 | }); 150 | 151 | if (!response.ok) { 152 | const errorText = await response.text(); 153 | throw new ImageFXError(`Server responded with unexpected response (${response.status}): ${errorText}`); 154 | } 155 | 156 | const parsedResponse = await response.json(); 157 | 158 | const imageCaption: { output: string, mediaGenerationId: string }[] = parsedResponse?.result?.data?.json?.result?.candidates; 159 | 160 | if (!imageCaption || imageCaption.length == 0) { 161 | throw new ImageFXError("Image caption was not in the response: " + await response.text()); 162 | } 163 | 164 | return imageCaption.map(caption => caption.output); 165 | } catch (error) { 166 | if (error instanceof ImageFXError) { 167 | throw error; 168 | } 169 | 170 | throw new ImageFXError(`Failed to fetch image: ${error instanceof Error ? error.message : 'Unknown error'}`); 171 | } 172 | } 173 | 174 | 175 | /** 176 | * Fetches generated images from Google ImageFX API 177 | * 178 | * @param retry Number of retries 179 | * @returns Promise containing list of generated images. 180 | */ 181 | private async fetchImages(prompt: Prompt, retry = 0): Promise { 182 | try { 183 | const response = await fetch("https://aisandbox-pa.googleapis.com/v1:runImageFx", { 184 | method: "POST", 185 | body: prompt.toString(), 186 | headers: this.account.getAuthHeaders(), 187 | }); 188 | 189 | if (!response.ok) { 190 | if (retry > 0) { 191 | console.log("[!] Failed to generate image. Retrying..."); 192 | return this.fetchImages(prompt, retry - 1); 193 | } 194 | 195 | const errorText = await response.text(); 196 | throw new ImageFXError(`Server responded with invalid response (${response.status}): ${errorText}`); 197 | } 198 | 199 | const jsonResponse = await response.json(); 200 | const generatedImages: ImageArg[] | undefined = jsonResponse?.imagePanels?.[0]?.generatedImages; 201 | 202 | if (!generatedImages) { 203 | throw new ImageFXError("Server responded with empty images"); 204 | } 205 | 206 | return generatedImages; 207 | 208 | } catch (error) { 209 | if (retry > 0) { 210 | console.log("[!] Failed to generate image. Retrying..."); 211 | return this.fetchImages(prompt, retry - 1); 212 | } 213 | 214 | if (error instanceof ImageFXError) { 215 | throw error; 216 | } 217 | 218 | throw new ImageFXError(`Failed to generate image: ${error instanceof Error ? error.message : 'Network error'}`); 219 | } 220 | } 221 | } --------------------------------------------------------------------------------