@rohitaryal/whisk-api 1.0.1 → 1.0.2
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/README.md +3 -3
- package/package.json +5 -2
- package/src/global.types.ts +172 -0
- package/src/index.ts +715 -0
- package/src/utils/request.ts +39 -0
package/README.md
CHANGED
|
@@ -20,15 +20,15 @@ An unofficial TypeScript/JavaScript API wrapper for Google Labs' Whisk image gen
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
|
-
bun i whisk-api
|
|
23
|
+
bun i @rohitaryal/whisk-api
|
|
24
24
|
# or
|
|
25
|
-
npm i whisk-api
|
|
25
|
+
npm i @rohitaryal/whisk-api
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
## Quick Start
|
|
29
29
|
|
|
30
30
|
```typescript
|
|
31
|
-
import Whisk from 'whisk-api';
|
|
31
|
+
import Whisk from '@rohitaryal/whisk-api';
|
|
32
32
|
|
|
33
33
|
const whisk = new Whisk({
|
|
34
34
|
cookie: "your_google_labs_cookie_here"
|
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"main": "src/index.ts",
|
|
16
16
|
"description": "[](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml) [](https://github.com/rohitaryal/whisk-api/blob/main/LICENSE) [](https://www.typescriptlang.org/) [](https://nodejs.org/) [](https://nodejs.org/)",
|
|
17
|
-
"version": "1.0.
|
|
17
|
+
"version": "1.0.2",
|
|
18
18
|
"directories": {
|
|
19
19
|
"example": "examples",
|
|
20
20
|
"test": "tests"
|
|
@@ -29,7 +29,10 @@
|
|
|
29
29
|
"url": "git+https://github.com/rohitaryal/whisk-api.git"
|
|
30
30
|
},
|
|
31
31
|
"keywords": [
|
|
32
|
-
"whisk"
|
|
32
|
+
"whisk",
|
|
33
|
+
"whisk-api",
|
|
34
|
+
"ai-image-generator",
|
|
35
|
+
"labs-google"
|
|
33
36
|
],
|
|
34
37
|
"author": "rohitaryal",
|
|
35
38
|
"license": "MIT",
|
|
@@ -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 };
|