@rohitaryal/whisk-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
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.
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # whisk-api
2
+
3
+ [![Test](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml/badge.svg)](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml)
4
+ [![License](https://img.shields.io/npm/l/whisk-api.svg)](https://github.com/rohitaryal/whisk-api/blob/main/LICENSE)
5
+ [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
6
+ [![Node.js](https://img.shields.io/badge/Node.js-339933?logo=node.js&logoColor=white)](https://nodejs.org/)
7
+ [![Bun.js](https://img.shields.io/badge/Bun.js-000000?logo=bun&logoColor=pink)](https://nodejs.org/)
8
+
9
+ An unofficial TypeScript/JavaScript API wrapper for Google Labs' Whisk image generation platform.
10
+
11
+ ## Features
12
+
13
+ - **Image Generation**: Create high-quality images from text prompts
14
+ - **Image Refinement**: Enhance and modify existing generated images
15
+ - **Project Management**: Organize generations into projects with full CRUD operations
16
+ - **Media Management**: Access generation history and download images
17
+ - **Multiple Models**: Support for various Imagen models with different capabilities
18
+ - **Type Safety**: Full TypeScript support with comprehensive type definitions
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ bun i whisk-api
24
+ # or
25
+ npm i whisk-api
26
+ ```
27
+
28
+ ## Quick Start
29
+
30
+ ```typescript
31
+ import Whisk from 'whisk-api';
32
+
33
+ const whisk = new Whisk({
34
+ cookie: "your_google_labs_cookie_here"
35
+ });
36
+
37
+ // Generate an image
38
+ const result = await whisk.generateImage({
39
+ prompt: "A serene mountain landscape at sunset"
40
+ });
41
+
42
+ if (result.Ok) {
43
+ const imageData = result.Ok.imagePanels[0]?.generatedImages[0]?.encodedImage;
44
+ whisk.saveImage(imageData, "mountain_sunset.png");
45
+ }
46
+ ```
47
+
48
+ ## Authentication
49
+
50
+ You'll need to obtain your Google Labs session cookie:
51
+
52
+ 1. Visit [labs.google/fx/tools/whisk](https://labs.google/fx/tools/whisk)
53
+ 2. Open browser developer tools (F12)
54
+ 3. Go to Application/Storage → Cookies
55
+ 4. Copy the cookie value and use it in your configuration
56
+
57
+ ## Supported Models
58
+
59
+ | Model | Description | Capabilities |
60
+ |-------|-------------|--------------|
61
+ | **Imagen 2** | Second generation model | Standard quality image generation |
62
+ | **Imagen 3** | Third generation model | Improved quality and prompt adherence |
63
+ | **Imagen 3.1** | Enhanced version of Imagen 3 | Better detail rendering |
64
+ | **Imagen 4** | Latest generation model | Highest quality, best prompt understanding |
65
+ | **Imagen 3 Portrait** | Portrait-optimized variant | Specialized for portrait generation |
66
+ | **Imagen 3 Landscape** | Landscape-optimized variant | Specialized for landscape generation |
67
+ | **Imagen 3 Portrait 3:4** | Portrait with 3:4 aspect ratio | Fixed aspect ratio portraits |
68
+ | **Imagen 3 Landscape 4:3** | Landscape with 4:3 aspect ratio | Fixed aspect ratio landscapes |
69
+
70
+ ## Examples
71
+
72
+ The library includes comprehensive examples in the [`examples/`](examples/) directory:
73
+
74
+ - [Getting authorization tokens](examples/1_get_auth_tokens.ts)
75
+ - [Checking credit status](examples/2_get_credit_status.ts)
76
+ - [Creating projects](examples/3_create_new_project.ts)
77
+ - [Managing project history](examples/4_list_all_project_history.ts)
78
+ - [Project content management](examples/5_get_content_of_projects.ts)
79
+ - [Deleting projects](examples/6_delete_projects.ts)
80
+ - [Renaming projects](examples/7_rename_project.ts)
81
+ - [Image generation history](examples/8_get_image_generation_history.ts)
82
+ - [Saving images](examples/9_save_images.ts)
83
+ - [Basic image generation](examples/10_generate_image.ts)
84
+ - [Image refinement](examples/11_refine_image.ts)
85
+
86
+ ## API Reference
87
+
88
+ ### Core Methods
89
+
90
+ - `generateImage(prompt)` - Generate images from text prompts
91
+ - `refineImage(refinementRequest)` - Refine existing images with new prompts
92
+ - `getProjectHistory(limit)` - Retrieve project history
93
+ - `getImageHistory(limit)` - Retrieve image generation history
94
+ - `getNewProjectId(title)` - Create new projects
95
+ - `deleteProjects(projectIds)` - Delete multiple projects
96
+ - `renameProject(newName, projectId)` - Rename existing projects
97
+ - `saveImage(base64Data, fileName)` - Save images to disk
98
+ - `getAuthorizationToken()` - Generate authentication tokens
99
+
100
+ ### Response Format
101
+
102
+ All methods return a `Result<T>` type with either:
103
+ - `Ok`: Contains the successful response data
104
+ - `Err`: Contains error information
105
+
106
+ ```typescript
107
+ const result = await whisk.generateImage({ prompt: "example" });
108
+ if (result.Err) {
109
+ console.error("Generation failed:", result.Err);
110
+ } else {
111
+ console.log("Success:", result.Ok);
112
+ }
113
+ ```
114
+
115
+ ## Development
116
+
117
+ ```bash
118
+ # Install dependencies
119
+ bun install
120
+
121
+ # Run tests
122
+ bun test
123
+
124
+ # Set up environment
125
+ export COOKIE="your_cookie_here"
126
+ ```
127
+
128
+ ## Testing
129
+
130
+ The test suite requires a valid Google Labs cookie. Set the `COOKIE` environment variable and run:
131
+
132
+ ```bash
133
+ bun test
134
+ ```
135
+
136
+ ## Limitations
137
+
138
+ - Requires valid Google Labs authentication
139
+ - Rate limiting applies based on Google's policies
140
+ - Regional availability may vary
141
+ - Unofficial API subject to changes
142
+
143
+ ## Contributing
144
+
145
+ Contributions are welcome. Please ensure all tests pass and follow the existing code style.
146
+
147
+ ## License
148
+
149
+ This project is for educational and research purposes. Please respect Google's terms of service when using this library.
150
+
151
+ ## Disclaimer
152
+
153
+ This is an unofficial API wrapper and is not affiliated with Google. Use at your own risk and ensure compliance with Google's terms of service.
package/bun.lock ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "whisk-api",
6
+ "devDependencies": {
7
+ "@types/bun": "latest",
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5",
11
+ },
12
+ },
13
+ },
14
+ "packages": {
15
+ "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="],
16
+
17
+ "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="],
18
+
19
+ "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="],
20
+
21
+ "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
22
+
23
+ "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
24
+ }
25
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@rohitaryal/whisk-api",
3
+ "module": "index.ts",
4
+ "type": "module",
5
+ "private": false,
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ },
12
+ "scripts": {
13
+ "test": "bun test"
14
+ },
15
+ "main": "src/index.ts",
16
+ "description": "[![Test](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml/badge.svg)](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml) [![License](https://img.shields.io/npm/l/whisk-api.svg)](https://github.com/rohitaryal/whisk-api/blob/main/LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-007ACC?logo=typescript&logoColor=white)](https://www.typescriptlang.org/) [![Node.js](https://img.shields.io/badge/Node.js-339933?logo=node.js&logoColor=white)](https://nodejs.org/) [![Bun.js](https://img.shields.io/badge/Bun.js-000000?logo=bun&logoColor=pink)](https://nodejs.org/)",
17
+ "version": "1.0.0",
18
+ "directories": {
19
+ "example": "examples",
20
+ "test": "tests"
21
+ },
22
+ "dependencies": {
23
+ "bun-types": "^1.2.14",
24
+ "typescript": "^5.8.3",
25
+ "undici-types": "^6.21.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/rohitaryal/whisk-api.git"
30
+ },
31
+ "keywords": [
32
+ "whisk"
33
+ ],
34
+ "author": "rohitaryal",
35
+ "license": "MIT",
36
+ "bugs": {
37
+ "url": "https://github.com/rohitaryal/whisk-api/issues"
38
+ },
39
+ "homepage": "https://github.com/rohitaryal/whisk-api#readme"
40
+ }
@@ -0,0 +1,172 @@
1
+ export type ImageModel =
2
+ | "IMAGEN_2"
3
+ | "IMAGEN_3"
4
+ | "IMAGEN_3_1"
5
+ | "IMAGEN_3_5"
6
+ | "IMAGEN_3_PORTRAIT"
7
+ | "IMAGEN_3_LANDSCAPE"
8
+ | "IMAGEN_3_PORTRAIT_THREE_FOUR"
9
+ | "IMAGEN_3_LANDSCAPE_FOUR_THREE";
10
+ export type AspectRatio =
11
+ | "IMAGE_ASPECT_RATIO_SQUARE"
12
+ | "IMAGE_ASPECT_RATIO_PORTRAIT"
13
+ | "IMAGE_ASPECT_RATIO_LANDSCAPE"
14
+ | "IMAGE_ASPECT_RATIO_UNSPECIFIED"
15
+ | "IMAGE_ASPECT_RATIO_LANDSCAPE_FOUR_THREE"
16
+ | "IMAGE_ASPECT_RATIO_PORTRAIT_THREE_FOUR";
17
+
18
+ export interface Credentials {
19
+ cookie: string;
20
+ authorizationKey?: string;
21
+ }
22
+
23
+ export interface Result<T> {
24
+ Ok?: T;
25
+ Err?: Error;
26
+ }
27
+
28
+ export interface Request {
29
+ url: string;
30
+ headers: Headers;
31
+ body?: string;
32
+ method:
33
+ | "GET"
34
+ | "POST"
35
+ | "HEAD"
36
+ | "OPTIONS"
37
+ | "PUT"
38
+ | "PATCH"
39
+ | "DELETE";
40
+ }
41
+
42
+ export interface Prompt {
43
+ seed?: number;
44
+ prompt: string;
45
+ projectId?: string;
46
+ imageModel?: ImageModel;
47
+ aspectRatio?: AspectRatio;
48
+ }
49
+
50
+ export interface Projects {
51
+ name: string;
52
+ media: Media;
53
+ displayName: string;
54
+ createTime: string;
55
+ }
56
+
57
+ export interface Media {
58
+ name: string;
59
+ image: Image;
60
+ mediaGenerationId: MediaGenerationId;
61
+ }
62
+
63
+ export interface Image {
64
+ seed: number;
65
+ prompt: string;
66
+ modelNameType: string;
67
+ previousMediaGenerationId: string;
68
+ workflowId: string;
69
+ fingerprintLogRecordId: string;
70
+ }
71
+
72
+ export interface MediaGenerationId {
73
+ mediaType: string;
74
+ workflowId: string;
75
+ workflowStepId: string;
76
+ mediaKey: string;
77
+ }
78
+
79
+ export interface Images {
80
+ name: string;
81
+ media: Media;
82
+ createTime: string;
83
+ }
84
+
85
+ export interface FetchedImage {
86
+ name: string;
87
+ image: FetchedImageDetails;
88
+ createTime: string;
89
+ backboneMetadata: BackboneMetadata;
90
+ mediaGenerationId: MediaGenerationId;
91
+ }
92
+
93
+ export interface FetchedImageDetails {
94
+ encodedImage: string;
95
+ seed: number;
96
+ mediaGenerationId: string;
97
+ mediaVisibility: "PRIVATE" | "PUBLIC";
98
+ prompt: string;
99
+ modelNameType: string;
100
+ previousMediaGenerationId: string;
101
+ workflowId: string;
102
+ fingerprintLogRecordId: string;
103
+ }
104
+
105
+ export interface BackboneMetadata {
106
+ mediaCategory: "MEDIA_CATEGORY_BOARD";
107
+ recipeInput: RecipeInput;
108
+ }
109
+
110
+ export interface RecipeInput {
111
+ userInput: UserInput;
112
+ mediaInputs: MediaInput[];
113
+ }
114
+
115
+ export interface UserInput {
116
+ userInstructions: string;
117
+ }
118
+
119
+ export interface MediaInput {
120
+ mediaCategory: "MEDIA_CATEGORY_BOARD";
121
+ }
122
+
123
+ export interface ImageMetadata {
124
+ name: string;
125
+ image: ImageDetails;
126
+ createTime: string;
127
+ backboneMetadata: BackboneMetadata;
128
+ }
129
+
130
+ export interface ImageDetails {
131
+ mediaGenerationId: string;
132
+ mediaVisibility: "PRIVATE" | "PUBLIC";
133
+ prompt: string;
134
+ }
135
+
136
+ export interface GenerationResult {
137
+ imagePanels: ImagePanel[];
138
+ workflowId: string;
139
+ }
140
+
141
+ export interface ImagePanel {
142
+ prompt: string;
143
+ generatedImages: GeneratedImage[];
144
+ }
145
+
146
+ export interface GeneratedImage {
147
+ encodedImage: string;
148
+ seed: number;
149
+ mediaGenerationId: string;
150
+ prompt: string;
151
+ isMaskEditedImage?: boolean
152
+ modelNameType?: string;
153
+ workflowId?: string;
154
+ fingerprintLogRecordId?: string;
155
+ imageModel?: ImageModel;
156
+ }
157
+
158
+ export interface RefinementRequest {
159
+ existingPrompt: string;
160
+ newRefinement: string;
161
+ base64image: string;
162
+ /**
163
+ * The media key of the image you want to refine.
164
+ * You can get this by calling `getImageHistory()[0...N].name`
165
+ */
166
+ imageId: string;
167
+ seed?: number;
168
+ count?: number,
169
+ imageModel?: ImageModel;
170
+ aspectRatio?: AspectRatio;
171
+ projectId?: string;
172
+ }
package/src/index.ts ADDED
@@ -0,0 +1,715 @@
1
+ import type { Credentials, FetchedImage, GenerationResult, ImageMetadata, Images, Projects, Prompt, RefinementRequest, Result } from "./global.types";
2
+ import type { Request } from "./global.types";
3
+ import { request } from "./utils/request";
4
+ import { writeFileSync } from "fs";
5
+
6
+ export default class Whisk {
7
+ credentials: Credentials;
8
+
9
+ constructor(credentials: Credentials) {
10
+ if (!credentials.cookie || credentials.cookie == "INVALID_COOKIE") {
11
+ throw new Error("Cookie is missing or invalid.")
12
+ }
13
+
14
+ this.credentials = structuredClone(credentials)
15
+ }
16
+
17
+ async #checkCredentials() {
18
+ if (!this.credentials.cookie) {
19
+ throw new Error("Credentials are not set. Please provide a valid cookie.");
20
+ }
21
+
22
+ if (!this.credentials.authorizationKey) {
23
+ const resp = await this.getAuthorizationToken();
24
+
25
+ if (resp.Err || !resp.Ok) {
26
+ throw new Error("Failed to get authorization token: " + resp.Err);
27
+ }
28
+
29
+ this.credentials.authorizationKey = resp.Ok;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Check if `Whisk` is available in your region.
35
+ *
36
+ * This un-availability can be easily bypassed by
37
+ * generating authorization token from a region where
38
+ * its available. Use VPN with US regions.
39
+ */
40
+ async isAvailable(): Promise<Result<boolean>> {
41
+ const req: Request = {
42
+ body: "{}",
43
+ method: "POST",
44
+ url: "https://aisandbox-pa.googleapis.com/v1:checkAppAvailability",
45
+ headers: new Headers({ // The API key might not work next-time (unsure)
46
+ "Content-Type": "text/plain;charset=UTF-8",
47
+ "X-Goog-Api-Key": "AIzaSyBtrm0o5ab1c-Ec8ZuLcGt3oJAA5VWt3pY",
48
+ }),
49
+ };
50
+
51
+ const response = await request(req);
52
+ if (response.Err || !response.Ok) {
53
+ return { Err: response.Err };
54
+ }
55
+
56
+ try {
57
+ const responseBody = JSON.parse(response.Ok);
58
+ return { Ok: responseBody.availabilityState === "AVAILABLE" };
59
+ } catch (err) {
60
+ return { Err: new Error("Failed to parse response: " + response.Ok) };
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Generates the authorization token for the user.
66
+ * This generated token is required to make *most* of API calls.
67
+ */
68
+ async getAuthorizationToken(): Promise<Result<string>> {
69
+ // Not on this one
70
+ // await this.#checkCredentials();
71
+ if (!this.credentials.cookie) {
72
+ return { Err: new Error("Empty or invalid cookies.") }
73
+ }
74
+
75
+ const req: Request = {
76
+ method: "GET",
77
+ url: "https://labs.google/fx/api/auth/session",
78
+ headers: new Headers({ "Cookie": String(this.credentials.cookie) }),
79
+ };
80
+
81
+ const resp = await request(req);
82
+ if (resp.Err || !resp.Ok) {
83
+ return { Err: resp.Err }
84
+ }
85
+
86
+ try {
87
+ const parsedResp = JSON.parse(resp.Ok);
88
+ const token = parsedResp?.access_token;
89
+
90
+ if (!token) {
91
+ return { Err: new Error("Failed to get session token: " + resp.Ok) }
92
+ }
93
+
94
+ // Let's not mutate the credentials directly
95
+ // this.credentials.authorizationKey = token;
96
+ return { Ok: String(token) };
97
+ } catch (err) {
98
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Get the current credit status of the user. This is for `veo` only and not `whisk`.
104
+ */
105
+ async getCreditStatus(): Promise<Result<number>> {
106
+ await this.#checkCredentials();
107
+
108
+ const req: Request = {
109
+ method: "POST",
110
+ body: JSON.stringify({ "tool": "BACKBONE", "videoModel": "VEO_2_1_I2V" }), // Unknown of other models
111
+ url: "https://aisandbox-pa.googleapis.com/v1:GetUserVideoCreditStatusAction",
112
+ headers: new Headers({ "Authorization": String(this.credentials.authorizationKey) }),
113
+ };
114
+
115
+ const response = await request(req);
116
+ if (response.Err || !response.Ok) {
117
+ return { Err: response.Err };
118
+ }
119
+
120
+ try {
121
+ const responseBody = JSON.parse(response.Ok);
122
+
123
+ // Other properties don't seem to be useful
124
+ return { Ok: Number(responseBody.credits) }
125
+ } catch (err) {
126
+ return { Err: new Error("Failed to parse response: " + response.Ok) };
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Generates a new project ID (a unique identifier for each project) for the
132
+ * given title so that you can start generating images in that specific project.
133
+ *
134
+ * @param projectTitle The name you want to give to the project.
135
+ */
136
+ async getNewProjectId(projectTitle: string): Promise<Result<string>> {
137
+ await this.#checkCredentials();
138
+
139
+ const req: Request = {
140
+ method: "POST",
141
+ // Long ass JSON
142
+ body: JSON.stringify({
143
+ "json": {
144
+ "clientContext": {
145
+ "tool": "BACKBONE",
146
+ "sessionId": ";1748266079775" // Doesn't matter whatever the value is
147
+ // But probably the last login time
148
+ },
149
+ "workflowMetadata": { "workflowName": projectTitle }
150
+ }
151
+ }),
152
+ url: "https://labs.google/fx/api/trpc/media.createOrUpdateWorkflow",
153
+ headers: new Headers({ "Cookie": String(this.credentials.cookie) }),
154
+ };
155
+
156
+ const resp = await request(req);
157
+ if (resp.Err || !resp.Ok) {
158
+ return { Err: resp.Err }
159
+ }
160
+
161
+ try {
162
+ const parsedResp = JSON.parse(resp.Ok);
163
+ const workflowID = parsedResp?.result?.data?.json?.result?.workflowId;
164
+
165
+ return workflowID ? { Ok: String(workflowID) } : { Err: new Error("Failed to create new library" + resp.Ok) };
166
+ } catch (err) {
167
+ return { Err: new Error("Failed to parse response: " + resp.Ok) }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get all of your project history or library.
173
+ *
174
+ * @param limitCount The number of projects you want to fetch.
175
+ */
176
+ async getProjectHistory(limitCount: number): Promise<Result<Projects[]>> {
177
+ await this.#checkCredentials();
178
+
179
+ const reqJson = {
180
+ "json": {
181
+ "rawQuery": "",
182
+ "type": "BACKBONE",
183
+ "subtype": "PROJECT",
184
+ "limit": limitCount,
185
+ "cursor": null
186
+ },
187
+ "meta": { "values": { "cursor": ["undefined"] } }
188
+ };
189
+
190
+ const req: Request = {
191
+ method: "GET",
192
+ headers: new Headers({
193
+ "Content-Type": "application/json",
194
+ "Cookie": String(this.credentials.cookie),
195
+ }),
196
+ url: `https://labs.google/fx/api/trpc/media.fetchUserHistory?input=` + JSON.stringify(reqJson),
197
+ };
198
+
199
+ const resp = await request(req);
200
+ if (resp.Err || !resp.Ok) {
201
+ return { Err: resp.Err }
202
+ }
203
+
204
+ try {
205
+ const parsedResp = JSON.parse(resp.Ok);
206
+ const workflowList = parsedResp?.result?.data?.json?.result?.userWorkflows;
207
+
208
+ // More cases required here
209
+ if (workflowList && Array.isArray(workflowList)) {
210
+ return { Ok: workflowList as Projects[] }
211
+ }
212
+
213
+ return { Err: new Error("Failed to get project history: " + resp.Ok) }
214
+ } catch (err) {
215
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Get the image history of the user.
221
+ *
222
+ * @param limitCount The number of images you want to fetch.
223
+ */
224
+ async getImageHistory(limitCount: number): Promise<Result<Images[]>> {
225
+ await this.#checkCredentials();
226
+
227
+ // No upper known limit
228
+ if (limitCount <= 0) {
229
+ return { Err: new Error("Limit count must be between 1 and 100.") };
230
+ }
231
+
232
+ const reqJson = {
233
+ "json": {
234
+ "rawQuery": "",
235
+ "type": "BACKBONE",
236
+ "subtype": "IMAGE",
237
+ "limit": limitCount,
238
+ "cursor": null
239
+ },
240
+ "meta": { "values": { "cursor": ["undefined"] } }
241
+ };
242
+
243
+ const req: Request = {
244
+ method: "GET",
245
+ headers: new Headers({
246
+ "Content-Type": "application/json",
247
+ "Cookie": String(this.credentials.cookie),
248
+ }),
249
+ url: `https://labs.google/fx/api/trpc/media.fetchUserHistory?input=` + JSON.stringify(reqJson),
250
+ };
251
+
252
+ const resp = await request(req);
253
+ if (resp.Err || !resp.Ok) return { Err: resp.Err }
254
+
255
+ try {
256
+ const parsedResp = JSON.parse(resp.Ok);
257
+ const mediaList = parsedResp?.result?.data?.json?.result?.userWorkflows;
258
+
259
+ // More cases required here
260
+ if (mediaList && Array.isArray(mediaList)) {
261
+ return { Ok: mediaList as Images[] }
262
+ }
263
+
264
+ return { Err: new Error("Failed to get image history: " + resp.Ok) }
265
+ } catch (err) {
266
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
267
+ }
268
+ }
269
+
270
+ /**
271
+ * Fetches the content of a project by its ID.
272
+ *
273
+ * @param projectId The ID of the project you want to fetch content from.
274
+ */
275
+ async getProjectContent(projectId: string): Promise<Result<ImageMetadata[]>> {
276
+ await this.#checkCredentials();
277
+
278
+ if (!projectId) {
279
+ return { Err: new Error("Project ID is required to fetch project content.") };
280
+ }
281
+
282
+ const reqJson = { "json": { "workflowId": projectId } };
283
+ const req: Request = {
284
+ method: "GET",
285
+ headers: new Headers({
286
+ "Content-Type": "application/json",
287
+ "Cookie": String(this.credentials.cookie),
288
+ }),
289
+ url: `https://labs.google/fx/api/trpc/media.getProjectWorkflow?input=` + JSON.stringify(reqJson),
290
+ };
291
+
292
+ const resp = await request(req);
293
+ if (resp.Err || !resp.Ok) {
294
+ return { Err: resp.Err }
295
+ }
296
+
297
+ try {
298
+ const parsedResp = JSON.parse(resp.Ok);
299
+ const mediaList = parsedResp?.result?.data?.json?.result?.media;
300
+
301
+ // More cases required here
302
+ if (!mediaList || !Array.isArray(mediaList)) {
303
+ return { Err: new Error("Failed to get project content: " + resp.Ok) };
304
+ }
305
+
306
+ return { Ok: mediaList as ImageMetadata[] };
307
+ } catch (err) {
308
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
309
+ }
310
+ }
311
+
312
+
313
+ /**
314
+ * Rename a project title.
315
+ *
316
+ * @param newName New name for your project
317
+ * @param projectId Identifier for project that you need to rename
318
+ */
319
+ async renameProject(newName: string, projectId: string): Promise<Result<string>> {
320
+ if (!this.credentials.cookie) {
321
+ return { Err: new Error("Cookie field is empty") };
322
+ }
323
+
324
+ const reqJson = {
325
+ "json": {
326
+ "workflowId": projectId,
327
+ "clientContext": {
328
+ "sessionId": ";1748333296243",
329
+ "tool": "BACKBONE",
330
+ "workflowId": projectId
331
+ },
332
+ "workflowMetadata": { "workflowName": newName }
333
+ }
334
+ };
335
+
336
+ const req: Request = {
337
+ method: "POST",
338
+ body: JSON.stringify(reqJson),
339
+ headers: new Headers({
340
+ "Content-Type": "application/json",
341
+ "Cookie": String(this.credentials.cookie),
342
+ }),
343
+ url: "https://labs.google/fx/api/trpc/media.createOrUpdateWorkflow",
344
+ };
345
+
346
+ const resp = await request(req);
347
+ if (resp.Err || !resp.Ok) {
348
+ return { Err: resp.Err };
349
+ }
350
+
351
+ try {
352
+ const parsedBody = JSON.parse(resp.Ok);
353
+ const workflowId = parsedBody?.result?.data?.json?.result?.workflowId;
354
+
355
+ if (parsedBody.error || !workflowId) {
356
+ return { Err: new Error("Failed to rename project: " + resp.Ok) }
357
+ }
358
+
359
+ return { Ok: String(workflowId) }
360
+ } catch (err) {
361
+ return { Err: new Error("Failed to parse JSON: " + resp.Ok) }
362
+ }
363
+ }
364
+
365
+ /**
366
+ * Delete project(s) from libary
367
+ *
368
+ * @param projectIds Array of project id that you need to delete.
369
+ */
370
+ async deleteProjects(projectIds: string[]): Promise<Result<boolean>> {
371
+ if (!this.credentials.cookie) {
372
+ return { Err: new Error("Cookie field is empty") };
373
+ }
374
+
375
+ const reqJson = {
376
+ "json":
377
+ {
378
+ "parent": "userProject/",
379
+ "names": projectIds,
380
+ }
381
+ };
382
+
383
+ const req: Request = {
384
+ method: "POST",
385
+ body: JSON.stringify(reqJson),
386
+ url: "https://labs.google/fx/api/trpc/media.deleteMedia",
387
+ headers: new Headers({
388
+ "Content-Type": "application/json",
389
+ "Cookie": String(this.credentials.cookie),
390
+ }
391
+ )
392
+ };
393
+
394
+ const resp = await request(req);
395
+ if (resp.Err || !resp.Ok) {
396
+ return { Err: resp.Err }
397
+ }
398
+
399
+ try {
400
+ const parsedResp = JSON.parse(resp.Ok);
401
+
402
+ if (parsedResp.error) {
403
+ return { Err: new Error("Failed to delete media: " + resp.Ok) }
404
+ }
405
+
406
+ return { Ok: true };
407
+ } catch (err) {
408
+ return { Err: new Error("Failed to parse JSON: " + resp.Ok) }
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Fetches the base64 encoded image from its media key (name).
414
+ * Media key can be obtained by calling: `getImageHistory()[0...N].name`
415
+ *
416
+ * @param mediaKey The media key of the image you want to fetch.
417
+ */
418
+ async getMedia(mediaKey: string): Promise<Result<FetchedImage>> {
419
+ await this.#checkCredentials();
420
+
421
+ if (!mediaKey) {
422
+ return { Err: new Error("Media key is required to fetch the image.") };
423
+ }
424
+
425
+ const reqJson = { "json": { "mediaKey": mediaKey } };
426
+ const req: Request = {
427
+ method: "GET",
428
+ headers: new Headers({
429
+ "Content-Type": "application/json",
430
+ "Cookie": String(this.credentials.cookie),
431
+ }),
432
+ url: `https://labs.google/fx/api/trpc/media.fetchMedia?input=` + JSON.stringify(reqJson),
433
+ };
434
+
435
+ const resp = await request(req);
436
+ if (resp.Err || !resp.Ok) {
437
+ return { Err: resp.Err }
438
+ }
439
+
440
+ try {
441
+ const parsedResp = JSON.parse(resp.Ok);
442
+ const image = parsedResp?.result?.data?.json?.result;
443
+
444
+ if (!image) {
445
+ return { Err: new Error("Failed to get media: " + resp.Ok) };
446
+ }
447
+
448
+ return { Ok: image as FetchedImage };
449
+ } catch (err) {
450
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Generates an image based on the provided prompt.
456
+ *
457
+ * @param prompt The prompt containing the details for image generation.
458
+ */
459
+ async generateImage(prompt: Prompt): Promise<Result<GenerationResult>> {
460
+ await this.#checkCredentials();
461
+
462
+ if (!prompt || !prompt.prompt) {
463
+ return { Err: new Error("Invalid prompt. Please provide a valid prompt and projectId") };
464
+ }
465
+
466
+ // You missed the projectId, so let's create a new one
467
+ if (!prompt.projectId) {
468
+ const id = await this.getNewProjectId("New Project");
469
+ if (id.Err || !id.Ok)
470
+ return { Err: id.Err }
471
+
472
+ prompt.projectId = id.Ok;
473
+ }
474
+
475
+ // Because seed can be zero
476
+ if (prompt.seed == undefined) {
477
+ prompt.seed = 0;
478
+ }
479
+
480
+ if (!prompt.imageModel) {
481
+ prompt.imageModel = "IMAGEN_3_5";
482
+ }
483
+
484
+ if (!prompt.aspectRatio) {
485
+ prompt.aspectRatio = "IMAGE_ASPECT_RATIO_LANDSCAPE"; // Default in frontend
486
+ }
487
+
488
+ const reqJson = {
489
+ "clientContext": {
490
+ "workflowId": prompt.projectId,
491
+ "tool": "BACKBONE",
492
+ "sessionId": ";1748281496093"
493
+ },
494
+ "imageModelSettings": {
495
+ "imageModel": prompt.imageModel,
496
+ "aspectRatio": prompt.aspectRatio,
497
+ },
498
+ "seed": prompt.seed,
499
+ "prompt": prompt.prompt,
500
+ "mediaCategory": "MEDIA_CATEGORY_BOARD"
501
+ };
502
+
503
+ const req: Request = {
504
+ method: "POST",
505
+ body: JSON.stringify(reqJson),
506
+ url: "https://aisandbox-pa.googleapis.com/v1/whisk:generateImage",
507
+ headers: new Headers({
508
+ "Content-Type": "application/json",
509
+ "Authorization": `Bearer ${String(this.credentials.authorizationKey)}`, // Requires bearer
510
+ }),
511
+ };
512
+
513
+ const resp = await request(req);
514
+ if (resp.Err || !resp.Ok) {
515
+ return { Err: resp.Err }
516
+ }
517
+
518
+ try {
519
+ const parsedResp = JSON.parse(resp.Ok);
520
+ if (parsedResp.error) {
521
+ return { Err: new Error("Failed to generate image: " + resp.Ok) }
522
+ }
523
+
524
+ return { Ok: parsedResp as GenerationResult }
525
+ } catch (err) {
526
+ return { Err: new Error("Failed to parse response:" + resp.Ok) }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Refine a generated image.
532
+ *
533
+ * Refination actually happens in the followin way:
534
+ * 1. Client provides an image (base64 encoded) to refine with new prompt eg: "xyz".
535
+ * 2. Server responds with *a new prompt describing your image* eg: AI-Mix("pqr", "xyz")
536
+ * Where `pqr` - Description of original image
537
+ * 3. Client requests image re-generation as: AI-Mix("pqr", "xyz")
538
+ * 4. Server responds with new base64 encoded image
539
+ */
540
+ async refineImage(ref: RefinementRequest): Promise<Result<GenerationResult>> {
541
+ await this.#checkCredentials();
542
+
543
+ if (ref.seed == undefined) {
544
+ ref.seed = 0;
545
+ }
546
+
547
+ if (!ref.aspectRatio) {
548
+ ref.aspectRatio = "IMAGE_ASPECT_RATIO_LANDSCAPE"; // Default in frontend
549
+ }
550
+
551
+ if (!ref.imageModel) {
552
+ ref.imageModel = "IMAGEN_3_5"; // Default in frontend (This is actually Imagen 4)
553
+ }
554
+
555
+ if (!ref.count) {
556
+ ref.count = 1; // Default in frontend
557
+ }
558
+
559
+ const reqJson = {
560
+ "json": {
561
+ "existingPrompt": ref.existingPrompt,
562
+ "textInput": ref.newRefinement,
563
+ "editingImage": {
564
+ "imageId": ref.imageId,
565
+ "base64Image": ref.base64image,
566
+ "category": "STORYBOARD",
567
+ "prompt": ref.existingPrompt,
568
+ "mediaKey": ref.imageId,
569
+ "isLoading": false,
570
+ "isFavorite": null,
571
+ "isActive": true,
572
+ "isPreset": false,
573
+ "isSelected": false,
574
+ "index": 0,
575
+ "imageObjectUrl": "blob:https://labs.google/1c612ac4-ecdf-4f77-9898-82ac488ad77f",
576
+ "recipeInput": {
577
+ "mediaInputs": [],
578
+ "userInput": {
579
+ "userInstructions": ref.existingPrompt
580
+ }
581
+ },
582
+ "currentImageAction": "REFINING",
583
+ "seed": ref.seed
584
+ },
585
+ "sessionId": ";1748338835952" // doesn't matter
586
+ },
587
+ "meta": {
588
+ "values": {
589
+ "editingImage.isFavorite": [
590
+ "undefined"
591
+ ]
592
+ }
593
+ }
594
+ };
595
+
596
+ const req: Request = {
597
+ method: "POST",
598
+ body: JSON.stringify(reqJson),
599
+ url: "https://labs.google/fx/api/trpc/backbone.generateRewrittenPrompt",
600
+ headers: new Headers({
601
+ "Content-Type": "application/json",
602
+ "Cookie": String(this.credentials.cookie),
603
+ }),
604
+ };
605
+
606
+ const resp = await request(req);
607
+ if (resp.Err || !resp.Ok) {
608
+ return { Err: resp.Err }
609
+ }
610
+
611
+ let parsedResp;
612
+ try {
613
+ parsedResp = JSON.parse(resp.Ok);
614
+ if (parsedResp.error) {
615
+ return { Err: new Error("Failed to refine image: " + resp.Ok) };
616
+ }
617
+ }
618
+ catch (err) {
619
+ return { Err: new Error("Failed to parse response: " + resp.Ok) };
620
+ }
621
+
622
+ const newPrompt = parsedResp?.result?.data?.json;
623
+ if (!newPrompt) {
624
+ return { Err: new Error("Failed to get new prompt from response: " + resp.Ok) };
625
+ }
626
+
627
+ const reqJson2 = {
628
+ "userInput": {
629
+ "candidatesCount": ref.count,
630
+ "seed": ref.seed,
631
+ "prompts": [newPrompt],
632
+ "mediaCategory": "MEDIA_CATEGORY_BOARD",
633
+ "recipeInput": {
634
+ "userInput": {
635
+ "userInstructions": newPrompt,
636
+ },
637
+ "mediaInputs": []
638
+ }
639
+ },
640
+ "clientContext": {
641
+ "sessionId": ";1748338835952", // can be anything
642
+ "tool": "BACKBONE",
643
+ "workflowId": ref.projectId,
644
+ },
645
+ "modelInput": {
646
+ "modelNameType": ref.imageModel
647
+ },
648
+ "aspectRatio": ref.aspectRatio
649
+ }
650
+
651
+ const req2: Request = {
652
+ method: "POST",
653
+ body: JSON.stringify(reqJson2),
654
+ url: "https://aisandbox-pa.googleapis.com/v1:runBackboneImageGeneration",
655
+ headers: new Headers({
656
+ "Content-Type": "text/plain;charset=UTF-8", // Yes
657
+ "Authorization": `Bearer ${String(this.credentials.authorizationKey)}`, // Requires bearer
658
+ }),
659
+ };
660
+
661
+ const resp2 = await request(req2);
662
+ if (resp2.Err || !resp2.Ok) {
663
+ return { Err: resp2.Err }
664
+ }
665
+
666
+ try {
667
+ const parsedResp2 = JSON.parse(resp2.Ok);
668
+ if (parsedResp2.error) {
669
+ return { Err: new Error("Failed to refine image: " + resp2.Ok) };
670
+ }
671
+
672
+ return { Ok: parsedResp2 as GenerationResult };
673
+ } catch (err) {
674
+ return { Err: new Error("Failed to parse response: " + resp2.Ok) };
675
+ }
676
+ }
677
+
678
+ /**
679
+ * Save image to a file with the given name.
680
+ *
681
+ * @param image The base64 encoded image string.
682
+ * @param fileName The name of the file where the image will be saved.
683
+ */
684
+ saveImage(image: string, fileName: string): Error | null {
685
+ try {
686
+ writeFileSync(fileName, image, { encoding: 'base64' });
687
+ return null;
688
+ } catch (err) {
689
+ return new Error("Failed to save image: " + err);
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Save image from its id directly
695
+ *
696
+ * @param imageId The ID of the image you want to save.
697
+ * @param fileName The name of the file where the image will be saved.
698
+ */
699
+ async saveImageDirect(imageId: string, fileName: string): Promise<Result<boolean>> {
700
+ const image = await this.getMedia(imageId);
701
+
702
+ if (image.Err || !image.Ok) {
703
+ return { Err: image.Err };
704
+ }
705
+
706
+ try {
707
+ writeFileSync(fileName, image.Ok.image.encodedImage, { encoding: 'base64' });
708
+ return { Ok: true };
709
+ } catch (err) {
710
+ return {
711
+ Err: new Error("Failed to save image: " + err)
712
+ }
713
+ }
714
+ }
715
+ }
@@ -0,0 +1,39 @@
1
+ import type { Request, Result } from "../global.types";
2
+
3
+ const request = async function (req: Request): Promise<Result<string>> {
4
+ req.headers.set("Origin", "https://labs.google");
5
+ req.headers.set("Referer", "https://labs.google/fx/tools/whisk");
6
+
7
+ // console.log(req.headers);
8
+
9
+ // if (!req.headers.has("Authorization")) {
10
+ // console.warn("Warning: Request is missing authorization headers: " + req.url)
11
+ // }
12
+
13
+ try {
14
+ const reqs = await fetch(req.url, {
15
+ body: req.body,
16
+ method: req.method,
17
+ headers: req.headers,
18
+ });
19
+
20
+ const res = await reqs.text();
21
+
22
+ if (!reqs.ok) {
23
+ return {
24
+ Err: new Error(res)
25
+ }
26
+ }
27
+
28
+ return {
29
+ Ok: res,
30
+ };
31
+
32
+ } catch (err) {
33
+ return {
34
+ Err: (err instanceof Error) ? err : new Error("Failed to fetch.")
35
+ }
36
+ }
37
+ }
38
+
39
+ export { request };