├── 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 | 
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 | }
--------------------------------------------------------------------------------