@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 +21 -0
- package/README.md +153 -0
- package/bun.lock +25 -0
- package/package.json +40 -0
- package/src/global.types.ts +172 -0
- package/src/index.ts +715 -0
- package/src/utils/request.ts +39 -0
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
|
+
[](https://github.com/rohitaryal/whisk-api/actions/workflows/test.yaml)
|
|
4
|
+
[](https://github.com/rohitaryal/whisk-api/blob/main/LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
[](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": "[](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.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 };
|