nekoai-api 0.1.1__py3-none-any.whl
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.
- nekoai/__init__.py +3 -0
- nekoai/__main__.py +40 -0
- nekoai/_version.py +1 -0
- nekoai/client.py +522 -0
- nekoai/config.py +6 -0
- nekoai/constant.py +153 -0
- nekoai/exceptions.py +38 -0
- nekoai/types/__init__.py +37 -0
- nekoai/types/director.py +159 -0
- nekoai/types/host.py +12 -0
- nekoai/types/image.py +40 -0
- nekoai/types/metadata.py +549 -0
- nekoai/types/parameters.py +137 -0
- nekoai/types/user.py +32 -0
- nekoai/utils.py +170 -0
- nekoai_api-0.1.1.dist-info/METADATA +438 -0
- nekoai_api-0.1.1.dist-info/RECORD +21 -0
- nekoai_api-0.1.1.dist-info/WHEEL +5 -0
- nekoai_api-0.1.1.dist-info/entry_points.txt +2 -0
- nekoai_api-0.1.1.dist-info/licenses/LICENSE +21 -0
- nekoai_api-0.1.1.dist-info/top_level.txt +1 -0
nekoai/__init__.py
ADDED
nekoai/__main__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from argparse import ArgumentParser
|
|
4
|
+
|
|
5
|
+
from .client import NovelAI
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def async_main():
|
|
9
|
+
parser = ArgumentParser()
|
|
10
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
11
|
+
|
|
12
|
+
# Login command
|
|
13
|
+
login_parser = subparsers.add_parser(
|
|
14
|
+
"login", help="Login to NovelAI to get your access token"
|
|
15
|
+
)
|
|
16
|
+
login_parser.add_argument("username", help="NovelAI username")
|
|
17
|
+
login_parser.add_argument("password", help="NovelAI password")
|
|
18
|
+
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.command == "login":
|
|
22
|
+
client = NovelAI(username=args.username, password=args.password)
|
|
23
|
+
await client.init()
|
|
24
|
+
|
|
25
|
+
access_token = await client.get_access_token()
|
|
26
|
+
print(access_token)
|
|
27
|
+
else:
|
|
28
|
+
parser.print_help()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main():
|
|
32
|
+
"""
|
|
33
|
+
Synchronous entry point for the package.
|
|
34
|
+
This function is used when the package is run as a script through the entry point.
|
|
35
|
+
"""
|
|
36
|
+
return asyncio.run(async_main())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
nekoai/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.1"
|
nekoai/client.py
ADDED
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import zipfile
|
|
4
|
+
from asyncio import Task
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import TYPE_CHECKING, Optional
|
|
7
|
+
|
|
8
|
+
from httpx import AsyncClient, ReadTimeout
|
|
9
|
+
from loguru import logger
|
|
10
|
+
from pydantic import validate_call
|
|
11
|
+
|
|
12
|
+
from .constant import EmotionLevel, EmotionOptions
|
|
13
|
+
from .types import Image, Metadata, User
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from .types.director import DirectorRequest
|
|
17
|
+
|
|
18
|
+
from .constant import HEADERS, Endpoint, Host
|
|
19
|
+
from .exceptions import AuthError, TimeoutError
|
|
20
|
+
from .types.host import HostInstance
|
|
21
|
+
from .utils import ResponseParser, encode_access_key, parse_image
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def running(func) -> callable:
|
|
25
|
+
"""
|
|
26
|
+
Decorator to check if client is running before making a request.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
async def wrapper(self: "NovelAI", *args, **kwargs):
|
|
30
|
+
if not self.running:
|
|
31
|
+
await self.init(auto_close=self.auto_close, close_delay=self.close_delay)
|
|
32
|
+
if self.running:
|
|
33
|
+
return await func(self, *args, **kwargs)
|
|
34
|
+
|
|
35
|
+
raise Exception(
|
|
36
|
+
f"Invalid function call: NAIClient.{func.__name__}. Client initialization failed."
|
|
37
|
+
)
|
|
38
|
+
else:
|
|
39
|
+
return await func(self, *args, **kwargs)
|
|
40
|
+
|
|
41
|
+
return wrapper
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NovelAI:
|
|
45
|
+
"""
|
|
46
|
+
Async httpx client interface to interact with NovelAI's service.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
username: `str`, optional
|
|
51
|
+
NovelAI username, usually an email address (required if token is not provided)
|
|
52
|
+
password: `str`, optional
|
|
53
|
+
NovelAI password (required if token is not provided)
|
|
54
|
+
token: `str`, optional
|
|
55
|
+
NovelAI access token (required if username/password is not provided)
|
|
56
|
+
proxy: `dict`, optional
|
|
57
|
+
Proxy to use for the client
|
|
58
|
+
|
|
59
|
+
Notes
|
|
60
|
+
-----
|
|
61
|
+
Either a username/password combination or a token must be provided.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
__slots__ = [
|
|
65
|
+
"user",
|
|
66
|
+
"proxy",
|
|
67
|
+
"client",
|
|
68
|
+
"running",
|
|
69
|
+
"auto_close",
|
|
70
|
+
"close_delay",
|
|
71
|
+
"close_task",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
username: str = None,
|
|
77
|
+
password: str = None,
|
|
78
|
+
token: str = None,
|
|
79
|
+
proxy: dict | None = None,
|
|
80
|
+
):
|
|
81
|
+
self.user = User(username=username, password=password, token=token)
|
|
82
|
+
if not self.user.validate_auth():
|
|
83
|
+
raise ValueError("Either username/password or token must be provided")
|
|
84
|
+
|
|
85
|
+
self.proxy = proxy
|
|
86
|
+
self.client: AsyncClient | None = None
|
|
87
|
+
self.running: bool = False
|
|
88
|
+
self.auto_close: bool = False
|
|
89
|
+
self.close_delay: float = 300
|
|
90
|
+
self.close_task: Task | None = None
|
|
91
|
+
|
|
92
|
+
async def init(
|
|
93
|
+
self, timeout: float = 30, auto_close: bool = False, close_delay: float = 300
|
|
94
|
+
) -> None:
|
|
95
|
+
"""
|
|
96
|
+
Get access token and implement Authorization header.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
timeout: `float`, optional
|
|
101
|
+
Request timeout of the client in seconds. Used to limit the max waiting time when sending a request
|
|
102
|
+
auto_close: `bool`, optional
|
|
103
|
+
If `True`, the client will close connections and clear resource usage after a certain period
|
|
104
|
+
of inactivity. Useful for keep-alive services
|
|
105
|
+
close_delay: `float`, optional
|
|
106
|
+
Time to wait before auto-closing the client in seconds. Effective only if `auto_close` is `True`
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
self.client = AsyncClient(
|
|
110
|
+
timeout=timeout, proxy=self.proxy, headers=HEADERS
|
|
111
|
+
)
|
|
112
|
+
self.client.headers["Authorization"] = (
|
|
113
|
+
f"Bearer {await self.get_access_token()}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self.running = True
|
|
117
|
+
logger.success("NovelAI client initialized successfully.")
|
|
118
|
+
|
|
119
|
+
self.auto_close = auto_close
|
|
120
|
+
self.close_delay = close_delay
|
|
121
|
+
if self.auto_close:
|
|
122
|
+
await self.reset_close_task()
|
|
123
|
+
except Exception:
|
|
124
|
+
await self.close()
|
|
125
|
+
raise
|
|
126
|
+
|
|
127
|
+
async def close(self, delay: float = 0) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Close the client after a certain period of inactivity, or call manually to close immediately.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
delay: `float`, optional
|
|
134
|
+
Time to wait before closing the client in seconds
|
|
135
|
+
"""
|
|
136
|
+
if delay:
|
|
137
|
+
await asyncio.sleep(delay)
|
|
138
|
+
|
|
139
|
+
if self.close_task:
|
|
140
|
+
self.close_task.cancel()
|
|
141
|
+
self.close_task = None
|
|
142
|
+
|
|
143
|
+
await self.client.aclose()
|
|
144
|
+
self.running = False
|
|
145
|
+
|
|
146
|
+
async def reset_close_task(self) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Reset the timer for closing the client when a new request is made.
|
|
149
|
+
"""
|
|
150
|
+
if self.close_task:
|
|
151
|
+
self.close_task.cancel()
|
|
152
|
+
self.close_task = None
|
|
153
|
+
self.close_task = asyncio.create_task(self.close(self.close_delay))
|
|
154
|
+
|
|
155
|
+
async def get_access_token(self) -> str:
|
|
156
|
+
"""
|
|
157
|
+
Get access token for NovelAI API authorization.
|
|
158
|
+
|
|
159
|
+
If a token is directly provided, it will be used.
|
|
160
|
+
Otherwise, send post request to /user/login endpoint to get user's access token.
|
|
161
|
+
|
|
162
|
+
Returns
|
|
163
|
+
-------
|
|
164
|
+
`str`
|
|
165
|
+
NovelAI access token which is used in the Authorization header with the Bearer scheme
|
|
166
|
+
|
|
167
|
+
Raises
|
|
168
|
+
------
|
|
169
|
+
`novelai.exceptions.AuthError`
|
|
170
|
+
If the account credentials are incorrect
|
|
171
|
+
"""
|
|
172
|
+
# Use token directly if provided
|
|
173
|
+
if self.user.token:
|
|
174
|
+
return self.user.token
|
|
175
|
+
|
|
176
|
+
# Otherwise authenticate with username/password
|
|
177
|
+
access_key = encode_access_key(self.user)
|
|
178
|
+
|
|
179
|
+
response = await self.client.post(
|
|
180
|
+
url=f"{Host.API.value.url}{Endpoint.LOGIN.value}",
|
|
181
|
+
json={
|
|
182
|
+
"key": access_key,
|
|
183
|
+
},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Exceptions are handled in self.init
|
|
187
|
+
ResponseParser(response).handle_status_code()
|
|
188
|
+
|
|
189
|
+
return response.json()["accessToken"]
|
|
190
|
+
|
|
191
|
+
@running
|
|
192
|
+
@validate_call
|
|
193
|
+
async def generate_image(
|
|
194
|
+
self,
|
|
195
|
+
metadata: Metadata | None = None,
|
|
196
|
+
host: Host | HostInstance = Host.WEB,
|
|
197
|
+
verbose: bool = False,
|
|
198
|
+
is_opus: bool = False,
|
|
199
|
+
**kwargs,
|
|
200
|
+
) -> list[Image]:
|
|
201
|
+
"""
|
|
202
|
+
Send post request to /ai/generate-image endpoint for image generation.
|
|
203
|
+
|
|
204
|
+
Parameters
|
|
205
|
+
----------
|
|
206
|
+
metadata: `novelai.Metadata`
|
|
207
|
+
Metadata object containing parameters required for image generation
|
|
208
|
+
host: `Host` or `HostInstance`, optional
|
|
209
|
+
Host to send the request. Can be:
|
|
210
|
+
- A predefined host from `novelai.constant.Host` (API or WEB)
|
|
211
|
+
- A custom host created with `Host.custom(url, accept)`
|
|
212
|
+
- A direct `HostInstance` object
|
|
213
|
+
verbose: `bool`, optional
|
|
214
|
+
If `True`, will log the estimated Anlas cost before sending the request
|
|
215
|
+
is_opus: `bool`, optional
|
|
216
|
+
Use with `verbose` to calculate the cost based on your subscription tier
|
|
217
|
+
**kwargs: `Any`
|
|
218
|
+
If `metadata` is not provided, these parameters are used to create a `novelai.Metadata` object
|
|
219
|
+
|
|
220
|
+
Returns
|
|
221
|
+
-------
|
|
222
|
+
`list[novelai.Image]`
|
|
223
|
+
List of `Image` objects containing the generated image and its metadata
|
|
224
|
+
|
|
225
|
+
Raises
|
|
226
|
+
------
|
|
227
|
+
`novelai.exceptions.TimeoutError`
|
|
228
|
+
If the request time exceeds the client's timeout value
|
|
229
|
+
`novelai.exceptions.AuthError`
|
|
230
|
+
If the access token is incorrect or expired
|
|
231
|
+
"""
|
|
232
|
+
if metadata is None:
|
|
233
|
+
metadata = Metadata(**kwargs)
|
|
234
|
+
|
|
235
|
+
if verbose:
|
|
236
|
+
logger.info(
|
|
237
|
+
f"Generating image... estimated Anlas cost: {metadata.calculate_cost(is_opus)}"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
if self.auto_close:
|
|
241
|
+
await self.reset_close_task()
|
|
242
|
+
|
|
243
|
+
# Get the actual host instance (whether from enum or direct HostInstance)
|
|
244
|
+
host_instance = host.value if isinstance(host, Host) else host
|
|
245
|
+
|
|
246
|
+
try:
|
|
247
|
+
# Use model_dump_for_api which properly formats the request for different model versions
|
|
248
|
+
payload = metadata.model_dump_for_api()
|
|
249
|
+
|
|
250
|
+
print(f"Payload: {payload}")
|
|
251
|
+
|
|
252
|
+
response = await self.client.post(
|
|
253
|
+
url=f"{host_instance.url}{Endpoint.IMAGE.value}",
|
|
254
|
+
json=payload,
|
|
255
|
+
)
|
|
256
|
+
except ReadTimeout:
|
|
257
|
+
raise TimeoutError(
|
|
258
|
+
"Request timed out, please try again. If the problem persists, consider setting a higher `timeout` value when initiating NAIClient."
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
ResponseParser(response).handle_status_code()
|
|
263
|
+
except AuthError:
|
|
264
|
+
await self.close()
|
|
265
|
+
raise
|
|
266
|
+
|
|
267
|
+
assert (
|
|
268
|
+
response.headers["Content-Type"] == host_instance.accept
|
|
269
|
+
), f"Invalid response content type. Expected '{host_instance.accept}', got '{response.headers['Content-Type']}'."
|
|
270
|
+
|
|
271
|
+
# Use the host name or 'custom' for the filename
|
|
272
|
+
host_name = host_instance.name.lower()
|
|
273
|
+
|
|
274
|
+
return [
|
|
275
|
+
Image(
|
|
276
|
+
filename=f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{host_name}_p{i}.png",
|
|
277
|
+
data=data,
|
|
278
|
+
metadata=metadata,
|
|
279
|
+
)
|
|
280
|
+
for i, data in enumerate(ResponseParser(response).parse_zip_content())
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
@running
|
|
284
|
+
async def use_director_tool(self, request: "DirectorRequest") -> "Image":
|
|
285
|
+
"""
|
|
286
|
+
Send request to /ai/augment-image endpoint for using NovelAI's Director tools.
|
|
287
|
+
|
|
288
|
+
Parameters
|
|
289
|
+
----------
|
|
290
|
+
request: `DirectorRequest`
|
|
291
|
+
The director tool request containing the necessary parameters
|
|
292
|
+
|
|
293
|
+
Returns
|
|
294
|
+
-------
|
|
295
|
+
`Image`
|
|
296
|
+
An image object containing the generated image
|
|
297
|
+
|
|
298
|
+
Raises
|
|
299
|
+
------
|
|
300
|
+
`novelai.exceptions.TimeoutError`
|
|
301
|
+
If the request time exceeds the client's timeout value
|
|
302
|
+
`novelai.exceptions.AuthError`
|
|
303
|
+
If the access token is incorrect or expired
|
|
304
|
+
"""
|
|
305
|
+
if self.auto_close:
|
|
306
|
+
await self.reset_close_task()
|
|
307
|
+
|
|
308
|
+
host_instance = Host.WEB.value
|
|
309
|
+
try:
|
|
310
|
+
json_data = request.model_dump(mode="json", exclude_none=True)
|
|
311
|
+
response = await self.client.post(
|
|
312
|
+
url=f"{host_instance.url}{Endpoint.DIRECTOR.value}",
|
|
313
|
+
json=json_data,
|
|
314
|
+
)
|
|
315
|
+
except ReadTimeout:
|
|
316
|
+
raise TimeoutError(
|
|
317
|
+
"Request timed out, please try again. If the problem persists, consider setting a higher `timeout` value when initiating NAIClient."
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
try:
|
|
321
|
+
ResponseParser(response).handle_status_code()
|
|
322
|
+
except AuthError:
|
|
323
|
+
await self.close()
|
|
324
|
+
raise
|
|
325
|
+
|
|
326
|
+
if not response.content:
|
|
327
|
+
logger.error("Received empty response from the server.")
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
image_data = self.handle_decompression(response.content)
|
|
331
|
+
|
|
332
|
+
# Director tool responses are not zipped, but directly return a single image
|
|
333
|
+
return Image(
|
|
334
|
+
filename=f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{request.req_type}.png",
|
|
335
|
+
data=image_data,
|
|
336
|
+
metadata=None,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def handle_decompression(self, compressed_data: bytes) -> bytes:
|
|
340
|
+
"""
|
|
341
|
+
Handle decompression of the response content.
|
|
342
|
+
|
|
343
|
+
Parameters
|
|
344
|
+
----------
|
|
345
|
+
response: `bytes`
|
|
346
|
+
The response content to decompress
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
`bytes`
|
|
351
|
+
The decompressed response content
|
|
352
|
+
"""
|
|
353
|
+
with zipfile.ZipFile(io.BytesIO(compressed_data)) as zf:
|
|
354
|
+
|
|
355
|
+
# List the contents to see what files are inside
|
|
356
|
+
file_names = zf.namelist()
|
|
357
|
+
return zf.read(file_names[0])
|
|
358
|
+
|
|
359
|
+
@running
|
|
360
|
+
async def lineart(self, image: str) -> "Image":
|
|
361
|
+
"""
|
|
362
|
+
Convert an image to line art using the Director tool.
|
|
363
|
+
|
|
364
|
+
Parameters
|
|
365
|
+
----------
|
|
366
|
+
image: `str`
|
|
367
|
+
path to the image file
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
`Image`
|
|
372
|
+
The processed image
|
|
373
|
+
"""
|
|
374
|
+
from .types.director import LineArtRequest
|
|
375
|
+
|
|
376
|
+
width, height, base64_image = parse_image(image)
|
|
377
|
+
|
|
378
|
+
request = LineArtRequest(width=width, height=height, image=base64_image)
|
|
379
|
+
return await self.use_director_tool(request)
|
|
380
|
+
|
|
381
|
+
@running
|
|
382
|
+
async def sketch(self, image: str) -> "Image":
|
|
383
|
+
"""
|
|
384
|
+
Convert an image to sketch using the Director tool.
|
|
385
|
+
Parameters
|
|
386
|
+
----------
|
|
387
|
+
image: `str`
|
|
388
|
+
path to the image file
|
|
389
|
+
|
|
390
|
+
Returns
|
|
391
|
+
-------
|
|
392
|
+
`Image`
|
|
393
|
+
The processed image
|
|
394
|
+
"""
|
|
395
|
+
from .types.director import SketchRequest
|
|
396
|
+
|
|
397
|
+
width, height, base64_image = parse_image(image)
|
|
398
|
+
|
|
399
|
+
request = SketchRequest(width=width, height=height, image=base64_image)
|
|
400
|
+
return await self.use_director_tool(request)
|
|
401
|
+
|
|
402
|
+
@running
|
|
403
|
+
async def background_removal(self, image: str) -> "Image":
|
|
404
|
+
"""
|
|
405
|
+
Remove background from an image using the Director tool.
|
|
406
|
+
|
|
407
|
+
Parameters
|
|
408
|
+
----------
|
|
409
|
+
image: `str`
|
|
410
|
+
Base64-encoded image
|
|
411
|
+
width: `int`
|
|
412
|
+
Width of the image in pixels
|
|
413
|
+
height: `int`
|
|
414
|
+
Height of the image in pixels
|
|
415
|
+
|
|
416
|
+
Returns
|
|
417
|
+
-------
|
|
418
|
+
`Image`
|
|
419
|
+
The processed image with background removed
|
|
420
|
+
"""
|
|
421
|
+
from .types.director import BackgroundRemovalRequest
|
|
422
|
+
|
|
423
|
+
width, height, base64_image = parse_image(image)
|
|
424
|
+
|
|
425
|
+
request = BackgroundRemovalRequest(
|
|
426
|
+
width=width, height=height, image=base64_image
|
|
427
|
+
)
|
|
428
|
+
return await self.use_director_tool(request)
|
|
429
|
+
|
|
430
|
+
@running
|
|
431
|
+
async def declutter(self, image: str) -> "Image":
|
|
432
|
+
"""
|
|
433
|
+
Declutter an image using the Director tool.
|
|
434
|
+
|
|
435
|
+
Parameters
|
|
436
|
+
----------
|
|
437
|
+
image: `str`
|
|
438
|
+
path to the image file
|
|
439
|
+
|
|
440
|
+
Returns
|
|
441
|
+
-------
|
|
442
|
+
`Image`
|
|
443
|
+
The processed image
|
|
444
|
+
"""
|
|
445
|
+
from .types.director import DeclutterRequest
|
|
446
|
+
|
|
447
|
+
width, height, base64_image = parse_image(image)
|
|
448
|
+
|
|
449
|
+
request = DeclutterRequest(width=width, height=height, image=base64_image)
|
|
450
|
+
return await self.use_director_tool(request)
|
|
451
|
+
|
|
452
|
+
@running
|
|
453
|
+
async def colorize(self, image: str, width: int, height: int) -> "Image":
|
|
454
|
+
"""
|
|
455
|
+
Colorize a line art or sketch using the Director tool.
|
|
456
|
+
|
|
457
|
+
Parameters
|
|
458
|
+
----------
|
|
459
|
+
image: `str`
|
|
460
|
+
Base64-encoded image
|
|
461
|
+
width: `int`
|
|
462
|
+
Width of the image in pixels
|
|
463
|
+
height: `int`
|
|
464
|
+
Height of the image in pixels
|
|
465
|
+
|
|
466
|
+
Returns
|
|
467
|
+
-------
|
|
468
|
+
`Image`
|
|
469
|
+
The colorized image
|
|
470
|
+
"""
|
|
471
|
+
from .types.director import ColorizeRequest
|
|
472
|
+
|
|
473
|
+
request = ColorizeRequest(width=width, height=height, image=image)
|
|
474
|
+
return await self.use_director_tool(request)
|
|
475
|
+
|
|
476
|
+
@running
|
|
477
|
+
async def change_emotion(
|
|
478
|
+
self,
|
|
479
|
+
image: str,
|
|
480
|
+
emotion: "EmotionOptions",
|
|
481
|
+
prompt: Optional[str] = "",
|
|
482
|
+
emotion_level: "EmotionLevel" = EmotionLevel.NORMAL,
|
|
483
|
+
) -> "Image":
|
|
484
|
+
"""
|
|
485
|
+
Change the emotion of a character in an image using the Director tool.
|
|
486
|
+
|
|
487
|
+
Parameters
|
|
488
|
+
----------
|
|
489
|
+
image: `str`
|
|
490
|
+
path to the image file
|
|
491
|
+
motion: EmotionOptions
|
|
492
|
+
The target emotion to apply
|
|
493
|
+
prompt: str
|
|
494
|
+
Addtional prompt for the request
|
|
495
|
+
emotion_level: EmotionLevel, optional
|
|
496
|
+
Strength level of the emotion, defaults to NORMAL
|
|
497
|
+
|
|
498
|
+
Returns
|
|
499
|
+
-------
|
|
500
|
+
`Image`
|
|
501
|
+
The image with modified emotion
|
|
502
|
+
"""
|
|
503
|
+
from .types.director import EmotionRequest
|
|
504
|
+
|
|
505
|
+
# Validate inputs are proper enums
|
|
506
|
+
if not isinstance(emotion, EmotionOptions):
|
|
507
|
+
emotion = EmotionOptions(emotion)
|
|
508
|
+
|
|
509
|
+
if not isinstance(emotion_level, EmotionLevel):
|
|
510
|
+
emotion_level = EmotionLevel(emotion_level)
|
|
511
|
+
|
|
512
|
+
width, height, base64_image = parse_image(image)
|
|
513
|
+
|
|
514
|
+
request = EmotionRequest.create(
|
|
515
|
+
width=width,
|
|
516
|
+
height=height,
|
|
517
|
+
image=base64_image,
|
|
518
|
+
emotion=emotion,
|
|
519
|
+
prompt=prompt,
|
|
520
|
+
emotion_level=emotion_level,
|
|
521
|
+
)
|
|
522
|
+
return await self.use_director_tool(request)
|