novelai-python 0.4.3__tar.gz → 0.4.5__tar.gz
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.
- {novelai_python-0.4.3 → novelai_python-0.4.5}/PKG-INFO +2 -2
- {novelai_python-0.4.3 → novelai_python-0.4.5}/README.md +1 -1
- {novelai_python-0.4.3 → novelai_python-0.4.5}/pyproject.toml +1 -1
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/__init__.py +4 -0
- novelai_python-0.4.5/src/novelai_python/_response/ai/generate_voice.py +7 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/__init__.py +1 -0
- novelai_python-0.4.5/src/novelai_python/sdk/ai/_enum.py +54 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/__init__.py +26 -41
- novelai_python-0.4.5/src/novelai_python/sdk/ai/generate_voice.py +201 -0
- novelai_python-0.4.5/tests/test_generate_voice.py +34 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/LICENSE +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/NOTICE.MD +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/credential/JwtToken.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/credential/UserAuth.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/upscale.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/user/information.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/user/login.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/user/subscription.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/server.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tokenizer/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tokenizer/novelai.model +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tokenizer/novelai_v2.model +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/tag.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/utils/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/utils/encode.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/utils/useful.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/__init__.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_server.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_server_run.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_upscale.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_user_information.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_user_login.py +0 -0
- {novelai_python-0.4.3 → novelai_python-0.4.5}/tests/test_user_subscription.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: novelai-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.5
|
|
4
4
|
Summary: NovelAI Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
@@ -51,11 +51,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
51
51
|
- [x] /user/information
|
|
52
52
|
- [x] /ai/upscale
|
|
53
53
|
- [x] /ai/generate-image/suggest-tags
|
|
54
|
+
- [x] /ai/generate-voice
|
|
54
55
|
- [ ] /ai/annotate-image
|
|
55
56
|
- [ ] /ai/classify
|
|
56
57
|
- [ ] /ai/generate-prompt
|
|
57
58
|
- [ ] /ai/generate
|
|
58
|
-
- [ ] /ai/generate-voice
|
|
59
59
|
|
|
60
60
|
> GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
|
|
61
61
|
> consumption information.
|
|
@@ -20,11 +20,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
20
20
|
- [x] /user/information
|
|
21
21
|
- [x] /ai/upscale
|
|
22
22
|
- [x] /ai/generate-image/suggest-tags
|
|
23
|
+
- [x] /ai/generate-voice
|
|
23
24
|
- [ ] /ai/annotate-image
|
|
24
25
|
- [ ] /ai/classify
|
|
25
26
|
- [ ] /ai/generate-prompt
|
|
26
27
|
- [ ] /ai/generate
|
|
27
|
-
- [ ] /ai/generate-voice
|
|
28
28
|
|
|
29
29
|
> GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
|
|
30
30
|
> consumption information.
|
|
@@ -12,6 +12,7 @@ from ._exceptions import (
|
|
|
12
12
|
)
|
|
13
13
|
from .credential import JwtCredential, LoginCredential, ApiCredential
|
|
14
14
|
from .sdk import GenerateImageInfer, ImageGenerateResp
|
|
15
|
+
from .sdk import VoiceGenerate, VoiceResponse
|
|
15
16
|
from .sdk import SuggestTags, SuggestTagsResp
|
|
16
17
|
from .sdk import Information, InformationResp
|
|
17
18
|
from .sdk import Login, LoginResp
|
|
@@ -22,6 +23,9 @@ __all__ = [
|
|
|
22
23
|
"GenerateImageInfer",
|
|
23
24
|
"ImageGenerateResp",
|
|
24
25
|
|
|
26
|
+
"VoiceGenerate",
|
|
27
|
+
"VoiceResponse",
|
|
28
|
+
|
|
25
29
|
"Upscale",
|
|
26
30
|
"UpscaleResp",
|
|
27
31
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
|
|
8
8
|
from .ai.generate_image.suggest_tags import SuggestTags, SuggestTagsResp # noqa 401
|
|
9
|
+
from .ai.generate_voice import VoiceGenerate, VoiceResponse # noqa 401
|
|
9
10
|
from .ai.upscale import Upscale, UpscaleResp # noqa 401
|
|
10
11
|
from .user.information import Information, InformationResp # noqa 401
|
|
11
12
|
from .user.login import Login, LoginResp # noqa 401
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Speaker(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Speaker for /ai/generated_voice
|
|
10
|
+
"""
|
|
11
|
+
sid: int = -1
|
|
12
|
+
seed: Optional[str] = None
|
|
13
|
+
name: str
|
|
14
|
+
category: str
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def version(self):
|
|
18
|
+
return "v2" if self.sid is None else "v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class VoiceSpeakerV1(Enum):
|
|
22
|
+
"""
|
|
23
|
+
Speaker for /ai/generated_voice
|
|
24
|
+
"""
|
|
25
|
+
Cyllene = Speaker(sid=17, name="Cyllene", category="female")
|
|
26
|
+
Leucosia = Speaker(sid=95, name="Leucosia", category="female")
|
|
27
|
+
Crina = Speaker(sid=44, name="Crina", category="female")
|
|
28
|
+
Hespe = Speaker(sid=80, name="Hespe", category="female")
|
|
29
|
+
Ida = Speaker(sid=106, name="Ida", category="female")
|
|
30
|
+
Alseid = Speaker(sid=6, name="Alseid", category="male")
|
|
31
|
+
Daphnis = Speaker(sid=10, name="Daphnis", category="male")
|
|
32
|
+
Echo = Speaker(sid=16, name="Echo", category="male")
|
|
33
|
+
Thel = Speaker(sid=41, name="Thel", category="male")
|
|
34
|
+
Nomios = Speaker(sid=77, name="Nomios", category="male")
|
|
35
|
+
# SeedInput = Speaker(sid=-1, name="Seed Input", category="custom")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class VoiceSpeakerV2(Enum):
|
|
39
|
+
"""
|
|
40
|
+
Speaker for /ai/generated_voice
|
|
41
|
+
"""
|
|
42
|
+
Ligeia = Speaker(name="Ligeia", category="unisex", seed="Anananan")
|
|
43
|
+
Aini = Speaker(name="Aini", category="female", seed="Aini")
|
|
44
|
+
Orea = Speaker(name="Orea", category="female", seed="Orea")
|
|
45
|
+
Claea = Speaker(name="Claea", category="female", seed="Claea")
|
|
46
|
+
Lim = Speaker(name="Lim", category="female", seed="Lim")
|
|
47
|
+
Aurae = Speaker(name="Aurae", category="female", seed="Aurae")
|
|
48
|
+
Naia = Speaker(name="Naia", category="female", seed="Naia")
|
|
49
|
+
Aulon = Speaker(name="Aulon", category="male", seed="Aulon")
|
|
50
|
+
Elei = Speaker(name="Elei", category="male", seed="Elei")
|
|
51
|
+
Ogma = Speaker(name="Ogma", category="male", seed="Ogma")
|
|
52
|
+
Raid = Speaker(name="Raid", category="male", seed="Raid")
|
|
53
|
+
Pega = Speaker(name="Pega", category="male", seed="Pega")
|
|
54
|
+
Lam = Speaker(name="Lam", category="male", seed="Lam")
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/__init__.py
RENAMED
|
@@ -9,7 +9,7 @@ import math
|
|
|
9
9
|
import random
|
|
10
10
|
from copy import deepcopy
|
|
11
11
|
from io import BytesIO
|
|
12
|
-
from typing import Optional, Union
|
|
12
|
+
from typing import Optional, Union, Tuple
|
|
13
13
|
from urllib.parse import urlparse
|
|
14
14
|
from zipfile import ZipFile
|
|
15
15
|
|
|
@@ -17,6 +17,7 @@ import curl_cffi
|
|
|
17
17
|
import cv2
|
|
18
18
|
import httpx
|
|
19
19
|
import numpy as np
|
|
20
|
+
from PIL import Image
|
|
20
21
|
from curl_cffi.requests import AsyncSession
|
|
21
22
|
from loguru import logger
|
|
22
23
|
from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator, model_validator, Field
|
|
@@ -30,7 +31,6 @@ from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, Sess
|
|
|
30
31
|
from ...._response.ai.generate_image import ImageGenerateResp
|
|
31
32
|
from ....credential import CredentialBase
|
|
32
33
|
from ....utils import try_jsonfy
|
|
33
|
-
from PIL import Image, ImageEnhance
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class GenerateImageInfer(ApiBaseModel):
|
|
@@ -159,8 +159,7 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
159
159
|
@staticmethod
|
|
160
160
|
def add_image_to_black_background(
|
|
161
161
|
image: Union[str, bytes],
|
|
162
|
-
|
|
163
|
-
height: int = 448,
|
|
162
|
+
target_size: Tuple[int, int] = (448, 448),
|
|
164
163
|
transparency: bool = False
|
|
165
164
|
):
|
|
166
165
|
|
|
@@ -168,49 +167,35 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
168
167
|
缩放图像到指定的黑色透明背景上,使其尽可能大且保持比例。
|
|
169
168
|
:param transparency: 是否透明
|
|
170
169
|
:param image: 图像
|
|
171
|
-
:param
|
|
172
|
-
:param height: 高
|
|
170
|
+
:param target_size: 目标尺寸
|
|
173
171
|
:return: 新图像
|
|
174
172
|
"""
|
|
175
173
|
|
|
176
174
|
if isinstance(image, str):
|
|
177
175
|
image = base64.b64decode(image)
|
|
178
|
-
|
|
179
|
-
# Decode the image from the base64 string
|
|
176
|
+
# Decode the image from the base64 string
|
|
180
177
|
npimg = np.frombuffer(image, np.uint8)
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
width_ratio = width / open_image.shape[1]
|
|
187
|
-
height_ratio = height / open_image.shape[0]
|
|
188
|
-
ratio = min(width_ratio, height_ratio)
|
|
189
|
-
|
|
190
|
-
# Create new image size
|
|
191
|
-
new_image_size = (int(open_image.shape[1] * ratio), int(open_image.shape[0] * ratio))
|
|
192
|
-
|
|
178
|
+
# Load the image with OpenCV
|
|
179
|
+
img = cv2.imdecode(npimg, cv2.IMREAD_UNCHANGED)
|
|
180
|
+
# Calculate the aspect ratio (keeping aspect ratio)
|
|
181
|
+
ratio = min(target_size[0] / img.shape[1], target_size[1] / img.shape[0])
|
|
182
|
+
new_image_size = (int(img.shape[1] * ratio), int(img.shape[0] * ratio))
|
|
193
183
|
# Resize the image
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
new_image[position[1]:position[1] + open_image.shape[0],
|
|
210
|
-
position[0]:position[0] + open_image.shape[1]] = open_image
|
|
211
|
-
|
|
212
|
-
# Encode image to base64 string
|
|
213
|
-
_, buffer = cv2.imencode('.png', new_image)
|
|
184
|
+
img = cv2.resize(img, new_image_size, interpolation=cv2.INTER_LINEAR)
|
|
185
|
+
# Check if the image has alpha channel (transparency)
|
|
186
|
+
if img.shape[2] == 3: # no alpha channel, add one
|
|
187
|
+
img = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
|
|
188
|
+
# Create new image with black background
|
|
189
|
+
delta_w = target_size[0] - img.shape[1]
|
|
190
|
+
delta_h = target_size[1] - img.shape[0]
|
|
191
|
+
top, bottom = delta_h // 2, delta_h - (delta_h // 2)
|
|
192
|
+
left, right = delta_w // 2, delta_w - (delta_w // 2)
|
|
193
|
+
# If the transparency flag is set the background is transparent, otherwise it is black
|
|
194
|
+
color = [0, 0, 0, 0] if transparency else [0, 0, 0, 255]
|
|
195
|
+
# Add padding to the image
|
|
196
|
+
new_img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color)
|
|
197
|
+
# Convert the image to base64
|
|
198
|
+
_, buffer = cv2.imencode(".png", new_img)
|
|
214
199
|
return base64.b64encode(buffer).decode("utf-8")
|
|
215
200
|
|
|
216
201
|
# Validators
|
|
@@ -289,7 +274,7 @@ class GenerateImageInfer(ApiBaseModel):
|
|
|
289
274
|
"lowres, {bad}, error, fewer, extra, missing, worst quality, jpeg artifacts, bad quality, "
|
|
290
275
|
"watermark, unfinished, displeasing, chromatic aberration, signature, extra digits, artistic error, "
|
|
291
276
|
"username, scan, [abstract], bad anatomy, bad hands, @_@, mismatched pupils, heart-shaped pupils, "
|
|
292
|
-
"glowing eyes,
|
|
277
|
+
"glowing eyes, lowres, bad anatomy, bad hands, text, error, missing fingers, "
|
|
293
278
|
"extra digit, fewer digits, cropped, worst quality, low quality, normal quality, "
|
|
294
279
|
f"jpeg artifacts, signature, watermark, username, blurry "
|
|
295
280
|
f",{self.parameters.negative_prompt}"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Author : sudoskys
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from io import BytesIO
|
|
6
|
+
from typing import Optional, Union, Literal
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
from zipfile import ZipFile
|
|
9
|
+
|
|
10
|
+
import curl_cffi
|
|
11
|
+
import httpx
|
|
12
|
+
import pydantic
|
|
13
|
+
from curl_cffi.requests import AsyncSession
|
|
14
|
+
from loguru import logger
|
|
15
|
+
from pydantic import ConfigDict, PrivateAttr, Field, model_validator
|
|
16
|
+
from tenacity import wait_random, retry, stop_after_attempt, retry_if_exception
|
|
17
|
+
|
|
18
|
+
from ..schema import ApiBaseModel
|
|
19
|
+
from ..._exceptions import APIError, AuthError, SessionHttpError
|
|
20
|
+
from ..._response.ai.generate_voice import VoiceResponse
|
|
21
|
+
from ...credential import CredentialBase
|
|
22
|
+
from ...utils import try_jsonfy
|
|
23
|
+
from ._enum import VoiceSpeakerV1, VoiceSpeakerV2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class VoiceGenerate(ApiBaseModel):
|
|
27
|
+
"""
|
|
28
|
+
Voice Generate for /ai/generate-voice
|
|
29
|
+
"""
|
|
30
|
+
_endpoint: str = PrivateAttr("https://api.novelai.net")
|
|
31
|
+
text: str = Field(..., description="Text to generate voice", max_length=1000)
|
|
32
|
+
"""Limit 1000 characters"""
|
|
33
|
+
voice: int = Field(default=-1, description="Seed", ge=-1)
|
|
34
|
+
"""Voice engine sid"""
|
|
35
|
+
seed: Optional[str] = Field(None, description="seed_seed", max_length=50)
|
|
36
|
+
"""
|
|
37
|
+
Starting with a common first name will have a relevant influence on pitch and intonation.
|
|
38
|
+
The voice for any given seed is liable to change in the future as we continue to develop the TTS.
|
|
39
|
+
"""
|
|
40
|
+
opus: bool = False
|
|
41
|
+
version: Union[Literal["v2", "v1"], str] = "v2"
|
|
42
|
+
|
|
43
|
+
model_config = ConfigDict(extra="ignore")
|
|
44
|
+
|
|
45
|
+
@model_validator(mode="after")
|
|
46
|
+
def validate_model(self):
|
|
47
|
+
# Validate seed
|
|
48
|
+
if self.voice == -1 and self.seed is None:
|
|
49
|
+
raise pydantic.ValidationError("Seed must be set when voice is -1")
|
|
50
|
+
return self
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def base_url(self):
|
|
54
|
+
return f"{self.endpoint.strip('/')}/ai/generate-voice"
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def endpoint(self):
|
|
58
|
+
return self._endpoint
|
|
59
|
+
|
|
60
|
+
@endpoint.setter
|
|
61
|
+
def endpoint(self, value):
|
|
62
|
+
self._endpoint = value
|
|
63
|
+
|
|
64
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
65
|
+
"""
|
|
66
|
+
:param request_data: dict
|
|
67
|
+
:return: dict
|
|
68
|
+
"Sec-Ch-Ua": '"Edge";v="123", "Not:A-Brand";v="8"',
|
|
69
|
+
"Sec-Ch-Ua-Mobile": "?0",
|
|
70
|
+
"Sec-Ch-Ua-Platform": '"Windows"',
|
|
71
|
+
"Sec-Fetch-Dest": "empty",
|
|
72
|
+
"Sec-Fetch-Mode": "cors",
|
|
73
|
+
"Sec-Fetch-Site": "same-site",
|
|
74
|
+
"""
|
|
75
|
+
return {
|
|
76
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
77
|
+
"Accept": "*/*",
|
|
78
|
+
"Accept-Language": "zh-CN,zh;q=0.9",
|
|
79
|
+
"Cache-Control": "no-cache",
|
|
80
|
+
"Origin": "https://novelai.net",
|
|
81
|
+
"Pragma": "no-cache",
|
|
82
|
+
"Referer": "https://novelai.net/",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@classmethod
|
|
86
|
+
def build(cls,
|
|
87
|
+
text: str,
|
|
88
|
+
voice_engine: Union[VoiceSpeakerV1, VoiceSpeakerV2, str],
|
|
89
|
+
*,
|
|
90
|
+
opus: bool = False
|
|
91
|
+
) -> "VoiceGenerate":
|
|
92
|
+
"""
|
|
93
|
+
生成图片
|
|
94
|
+
:param opus: unknown
|
|
95
|
+
:param text: str
|
|
96
|
+
:param voice_engine: VoiceSpeakerV1 or VoiceSpeakerV2 or str
|
|
97
|
+
:return: VoiceGenerate instance
|
|
98
|
+
:raises: ValueError
|
|
99
|
+
"""
|
|
100
|
+
if isinstance(voice_engine, str):
|
|
101
|
+
return cls(
|
|
102
|
+
text=text,
|
|
103
|
+
voice=-1,
|
|
104
|
+
seed=voice_engine,
|
|
105
|
+
opus=opus,
|
|
106
|
+
version="v2"
|
|
107
|
+
)
|
|
108
|
+
if isinstance(voice_engine, VoiceSpeakerV2):
|
|
109
|
+
return cls(
|
|
110
|
+
text=text,
|
|
111
|
+
seed=voice_engine.value.seed,
|
|
112
|
+
voice=voice_engine.value.sid,
|
|
113
|
+
opus=opus,
|
|
114
|
+
version="v2"
|
|
115
|
+
)
|
|
116
|
+
if isinstance(voice_engine, VoiceSpeakerV1):
|
|
117
|
+
return cls(
|
|
118
|
+
text=text,
|
|
119
|
+
seed=voice_engine.value.seed,
|
|
120
|
+
voice=voice_engine.value.sid,
|
|
121
|
+
opus=opus,
|
|
122
|
+
version="v1"
|
|
123
|
+
)
|
|
124
|
+
raise ValueError("Invalid voice engine")
|
|
125
|
+
|
|
126
|
+
@retry(
|
|
127
|
+
wait=wait_random(min=1, max=3),
|
|
128
|
+
stop=stop_after_attempt(3),
|
|
129
|
+
retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
|
|
130
|
+
reraise=True
|
|
131
|
+
)
|
|
132
|
+
async def request(self,
|
|
133
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
134
|
+
*,
|
|
135
|
+
override_headers: Optional[dict] = None
|
|
136
|
+
) -> VoiceResponse:
|
|
137
|
+
"""
|
|
138
|
+
生成图片
|
|
139
|
+
:param override_headers:
|
|
140
|
+
:param session: session
|
|
141
|
+
:return: VoiceResponse
|
|
142
|
+
"""
|
|
143
|
+
# Data Build
|
|
144
|
+
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
145
|
+
# Header
|
|
146
|
+
if isinstance(session, AsyncSession):
|
|
147
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
148
|
+
elif isinstance(session, CredentialBase):
|
|
149
|
+
update_header = await self.necessary_headers(request_data)
|
|
150
|
+
session = await session.get_session(update_headers=update_header)
|
|
151
|
+
if override_headers:
|
|
152
|
+
session.headers.clear()
|
|
153
|
+
session.headers.update(override_headers)
|
|
154
|
+
# Log
|
|
155
|
+
logger.debug(f"Voice request data: {request_data}")
|
|
156
|
+
# Request
|
|
157
|
+
try:
|
|
158
|
+
assert hasattr(session, "get"), "session must have get method."
|
|
159
|
+
response = await session.get(
|
|
160
|
+
self.base_url,
|
|
161
|
+
params=request_data
|
|
162
|
+
)
|
|
163
|
+
header_type = response.headers.get('Content-Type')
|
|
164
|
+
if header_type not in ['audio/mpeg', 'audio/ogg', 'audio/opus']:
|
|
165
|
+
logger.warning(
|
|
166
|
+
f"Error with content type: {header_type} and code: {response.status_code}"
|
|
167
|
+
)
|
|
168
|
+
try:
|
|
169
|
+
_msg = response.json()
|
|
170
|
+
except Exception as e:
|
|
171
|
+
logger.warning(e)
|
|
172
|
+
if not isinstance(response.content, str) and len(response.content) < 50:
|
|
173
|
+
raise APIError(
|
|
174
|
+
message=f"Unexpected content: {header_type} with code: {response.status_code}",
|
|
175
|
+
request=request_data,
|
|
176
|
+
code=response.status_code,
|
|
177
|
+
response="UnJsoned content"
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
181
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
182
|
+
message = _msg.get("message", "Unknown error")
|
|
183
|
+
if status_code in [400]:
|
|
184
|
+
# Validation tts version error
|
|
185
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
186
|
+
raise APIError(message, request=request_data, code=status_code, response=_msg)
|
|
187
|
+
return VoiceResponse(
|
|
188
|
+
meta=request_data,
|
|
189
|
+
audio=response.content,
|
|
190
|
+
)
|
|
191
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
192
|
+
logger.exception(exc)
|
|
193
|
+
raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!")
|
|
194
|
+
except httpx.HTTPError as exc:
|
|
195
|
+
logger.exception(exc)
|
|
196
|
+
raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!")
|
|
197
|
+
except APIError as e:
|
|
198
|
+
raise e
|
|
199
|
+
except Exception as e:
|
|
200
|
+
logger.opt(exception=e).exception("An Unexpected error occurred")
|
|
201
|
+
raise e
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
from curl_cffi.requests import AsyncSession
|
|
5
|
+
|
|
6
|
+
from src.novelai_python.sdk.ai.generate_voice import VoiceGenerate, VoiceResponse
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@pytest.mark.asyncio
|
|
10
|
+
async def test_request():
|
|
11
|
+
# Arrange
|
|
12
|
+
mock_response = mock.MagicMock()
|
|
13
|
+
mock_response.content = b'audio_content'
|
|
14
|
+
mock_response.headers = {'Content-Type': 'audio/mpeg'}
|
|
15
|
+
session = mock.MagicMock(spec=AsyncSession)
|
|
16
|
+
session.get = mock.AsyncMock(return_value=mock_response)
|
|
17
|
+
session.headers = {}
|
|
18
|
+
|
|
19
|
+
voice_generate = VoiceGenerate(
|
|
20
|
+
text="Hello, world!",
|
|
21
|
+
voice=-1,
|
|
22
|
+
seed="seed",
|
|
23
|
+
opus=False,
|
|
24
|
+
version="v2"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
# Act
|
|
28
|
+
result = await voice_generate.request(session=session)
|
|
29
|
+
|
|
30
|
+
# Assert
|
|
31
|
+
session.get.assert_called_once()
|
|
32
|
+
assert isinstance(result, VoiceResponse)
|
|
33
|
+
assert result.audio == b'audio_content'
|
|
34
|
+
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/ai/generate_image.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/information.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/_response/user/subscription.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/_const.py
RENAMED
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/sdk/ai/generate_image/_enum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/image_metadata/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/image_metadata/lsb_injector.py
RENAMED
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/paint_mask/__init__.py
RENAMED
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/tag_character.py
RENAMED
|
File without changes
|
{novelai_python-0.4.3 → novelai_python-0.4.5}/src/novelai_python/tool/random_prompt/tag_nsfw.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|