novelai-python 0.4.6__tar.gz → 0.4.8__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.6 → novelai_python-0.4.8}/PKG-INFO +51 -24
- {novelai_python-0.4.6 → novelai_python-0.4.8}/README.md +46 -22
- {novelai_python-0.4.6 → novelai_python-0.4.8}/pyproject.toml +5 -2
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/__init__.py +10 -2
- novelai_python-0.4.8/src/novelai_python/_response/ai/generate.py +23 -0
- novelai_python-0.4.8/src/novelai_python/_response/ai/generate_stream.py +23 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/__init__.py +2 -0
- novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/__init__.py +234 -0
- novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_const.py +136 -0
- novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_enum.py +77 -0
- novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_preset.py +225 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/__init__.py +11 -3
- novelai_python-0.4.8/src/novelai_python/sdk/ai/generate_stream.py +173 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/server.py +45 -1
- novelai_python-0.4.8/src/novelai_python/tokenizer/NOTICE.MD +12 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/__init__.py +228 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/bpe_simple_vocab_16e6.txt.gz +0 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/gpt2-genji_tokenizer.json +1 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/gpt2_tokenizer.json +1 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/novelai.model +0 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/novelai_v2.model +0 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/pile_tokenizer.json +1 -0
- novelai_python-0.4.8/src/novelai_python/tokenizer/simple_tokenizer.py +139 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/__init__.py +28 -3
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/paint_mask/__init__.py +7 -6
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/__init__.py +2 -1
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/encode.py +21 -9
- novelai_python-0.4.6/src/novelai_python/tokenizer/__init__.py +0 -30
- {novelai_python-0.4.6 → novelai_python-0.4.8}/LICENSE +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/NOTICE.MD +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_exceptions.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/generate_image.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/generate_voice.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/upscale.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/schema.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/information.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/login.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/subscription.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/ApiToken.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/JwtToken.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/UserAuth.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/_base.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/LICENSE +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/upscale.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/schema.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/information.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/login.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/subscription.py +0 -0
- /novelai_python-0.4.6/src/novelai_python/tokenizer/novelai.model → /novelai_python-0.4.8/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
- /novelai_python-0.4.6/src/novelai_python/tokenizer/novelai_v2.model → /novelai_python-0.4.8/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/useful.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/__init__.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_generate_voice.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_random_prompt.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_server.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_server_run.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_upscale.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_user_information.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_user_login.py +0 -0
- {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_user_subscription.py +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: novelai-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.8
|
|
4
4
|
Summary: NovelAI Python Binding With Pydantic
|
|
5
5
|
Author-Email: sudoskys <coldlando@hotmail.com>
|
|
6
6
|
License: Apache License 2.0
|
|
7
7
|
Project-URL: Repository, https://github.com/LlmKira/novelai-python/
|
|
8
8
|
Project-URL: Issues, https://github.com/LlmKira/novelai-python/issues
|
|
9
|
-
Requires-Python: >=3.
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
10
|
Requires-Dist: elara>=0.5.5
|
|
11
11
|
Requires-Dist: loguru>=0.7.2
|
|
12
12
|
Requires-Dist: pydantic-settings>=2.1.0
|
|
@@ -24,6 +24,9 @@ Requires-Dist: fake-useragent>=1.4.0
|
|
|
24
24
|
Requires-Dist: tenacity>=8.2.3
|
|
25
25
|
Requires-Dist: sentencepiece>=0.2.0
|
|
26
26
|
Requires-Dist: pynacl>=1.5.0
|
|
27
|
+
Requires-Dist: ftfy>=6.2.0
|
|
28
|
+
Requires-Dist: regex>=2023.12.25
|
|
29
|
+
Requires-Dist: tokenizers>=0.15.2
|
|
27
30
|
Requires-Dist: pytest>=7.4.4; extra == "testing"
|
|
28
31
|
Requires-Dist: pytest-asyncio>=0.23.4; extra == "testing"
|
|
29
32
|
Provides-Extra: testing
|
|
@@ -36,10 +39,12 @@ Description-Content-Type: text/markdown
|
|
|
36
39
|
[](https://badge.fury.io/py/novelai-python)
|
|
37
40
|
[](https://pepy.tech/project/novelai_python)
|
|
38
41
|
|
|
39
|
-
✨ NovelAI api python sdk with Pydantic.
|
|
42
|
+
✨ NovelAI api python sdk with Pydantic, modern and user-friendly.
|
|
40
43
|
|
|
41
44
|
The goal of this repository is to use Pydantic to build legitimate requests to access the NovelAI API service.
|
|
42
45
|
|
|
46
|
+
> Python >= 3.9 is required.
|
|
47
|
+
|
|
43
48
|
### Roadmap 🚧
|
|
44
49
|
|
|
45
50
|
- [x] tool.random_prompt
|
|
@@ -52,10 +57,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
52
57
|
- [x] /ai/upscale
|
|
53
58
|
- [x] /ai/generate-image/suggest-tags
|
|
54
59
|
- [x] /ai/generate-voice
|
|
60
|
+
- [x] /ai/generate-stream
|
|
61
|
+
- [x] /ai/generate
|
|
55
62
|
- [ ] /ai/annotate-image
|
|
56
63
|
- [ ] /ai/classify
|
|
57
64
|
- [ ] /ai/generate-prompt
|
|
58
|
-
- [ ] /ai/generate
|
|
59
65
|
|
|
60
66
|
> GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
|
|
61
67
|
> consumption information.
|
|
@@ -78,34 +84,20 @@ import os
|
|
|
78
84
|
from dotenv import load_dotenv
|
|
79
85
|
from pydantic import SecretStr
|
|
80
86
|
|
|
81
|
-
from novelai_python import GenerateImageInfer, ImageGenerateResp,
|
|
87
|
+
from novelai_python import GenerateImageInfer, ImageGenerateResp, ApiCredential
|
|
82
88
|
|
|
83
89
|
load_dotenv()
|
|
84
|
-
|
|
85
90
|
enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
|
|
91
|
+
session = ApiCredential(api_token=SecretStr(os.getenv("NOVELAI_JWT"))) # pst-***
|
|
86
92
|
|
|
87
93
|
|
|
88
94
|
async def main():
|
|
89
|
-
|
|
90
|
-
jwt_token=SecretStr(os.getenv("NOVELAI_JWT")) # ey****
|
|
91
|
-
)
|
|
92
|
-
globe_s1 = ApiCredential(
|
|
93
|
-
api_token=SecretStr(os.getenv("NOVELAI_JWT")) # pst-***
|
|
94
|
-
)
|
|
95
|
-
globe_s2 = LoginCredential(
|
|
96
|
-
username=os.getenv("NOVELAI_USERNAME"),
|
|
97
|
-
password=SecretStr(os.getenv("NOVELAI_PASSWORD"))
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
gen = await GenerateImageInfer.build(
|
|
101
|
-
prompt=f"1girl,{enhance}")
|
|
95
|
+
gen = await GenerateImageInfer.build(prompt=f"1girl,{enhance}")
|
|
102
96
|
cost = gen.calculate_cost(is_opus=True)
|
|
103
97
|
print(f"charge: {cost} if you are vip3")
|
|
104
|
-
|
|
105
|
-
resp = gen.request(session=globe_s)
|
|
98
|
+
resp = gen.request(session=session)
|
|
106
99
|
resp: ImageGenerateResp
|
|
107
100
|
print(resp.meta)
|
|
108
|
-
|
|
109
101
|
file = resp.files[0]
|
|
110
102
|
with open(file[0], "wb") as f:
|
|
111
103
|
f.write(file[1])
|
|
@@ -116,13 +108,48 @@ loop.run_until_complete(main())
|
|
|
116
108
|
|
|
117
109
|
```
|
|
118
110
|
|
|
111
|
+
#### LLM
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
import os
|
|
116
|
+
|
|
117
|
+
from dotenv import load_dotenv
|
|
118
|
+
from pydantic import SecretStr
|
|
119
|
+
|
|
120
|
+
from novelai_python import APIError, LoginCredential
|
|
121
|
+
from novelai_python.sdk.ai.generate import TextLLMModel, LLM
|
|
122
|
+
|
|
123
|
+
load_dotenv()
|
|
124
|
+
username = os.getenv("NOVELAI_USER", None)
|
|
125
|
+
assert username is not None
|
|
126
|
+
# credential = JwtCredential(jwt_token=SecretStr(jwt))
|
|
127
|
+
login_credential = LoginCredential(
|
|
128
|
+
username=os.getenv("NOVELAI_USER"),
|
|
129
|
+
password=SecretStr(os.getenv("NOVELAI_PASS"))
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def chat(prompt: str):
|
|
134
|
+
try:
|
|
135
|
+
agent = LLM.build(prompt=prompt, model=TextLLMModel.Kayra)
|
|
136
|
+
result = await agent.request(session=login_credential)
|
|
137
|
+
except APIError as e:
|
|
138
|
+
raise Exception(f"Error: {e.message}")
|
|
139
|
+
print(f"Result: \n{result.text}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
loop = asyncio.get_event_loop()
|
|
143
|
+
loop.run_until_complete(chat("Hello"))
|
|
144
|
+
```
|
|
145
|
+
|
|
119
146
|
#### Random Prompt
|
|
120
147
|
|
|
121
148
|
```python
|
|
122
149
|
from novelai_python.tool.random_prompt import RandomPromptGenerator
|
|
123
150
|
|
|
124
|
-
|
|
125
|
-
print(
|
|
151
|
+
prompt = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
|
|
152
|
+
print(prompt)
|
|
126
153
|
```
|
|
127
154
|
|
|
128
155
|
#### Run A Server
|
|
@@ -5,10 +5,12 @@
|
|
|
5
5
|
[](https://badge.fury.io/py/novelai-python)
|
|
6
6
|
[](https://pepy.tech/project/novelai_python)
|
|
7
7
|
|
|
8
|
-
✨ NovelAI api python sdk with Pydantic.
|
|
8
|
+
✨ NovelAI api python sdk with Pydantic, modern and user-friendly.
|
|
9
9
|
|
|
10
10
|
The goal of this repository is to use Pydantic to build legitimate requests to access the NovelAI API service.
|
|
11
11
|
|
|
12
|
+
> Python >= 3.9 is required.
|
|
13
|
+
|
|
12
14
|
### Roadmap 🚧
|
|
13
15
|
|
|
14
16
|
- [x] tool.random_prompt
|
|
@@ -21,10 +23,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
|
|
|
21
23
|
- [x] /ai/upscale
|
|
22
24
|
- [x] /ai/generate-image/suggest-tags
|
|
23
25
|
- [x] /ai/generate-voice
|
|
26
|
+
- [x] /ai/generate-stream
|
|
27
|
+
- [x] /ai/generate
|
|
24
28
|
- [ ] /ai/annotate-image
|
|
25
29
|
- [ ] /ai/classify
|
|
26
30
|
- [ ] /ai/generate-prompt
|
|
27
|
-
- [ ] /ai/generate
|
|
28
31
|
|
|
29
32
|
> GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
|
|
30
33
|
> consumption information.
|
|
@@ -47,34 +50,20 @@ import os
|
|
|
47
50
|
from dotenv import load_dotenv
|
|
48
51
|
from pydantic import SecretStr
|
|
49
52
|
|
|
50
|
-
from novelai_python import GenerateImageInfer, ImageGenerateResp,
|
|
53
|
+
from novelai_python import GenerateImageInfer, ImageGenerateResp, ApiCredential
|
|
51
54
|
|
|
52
55
|
load_dotenv()
|
|
53
|
-
|
|
54
56
|
enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
|
|
57
|
+
session = ApiCredential(api_token=SecretStr(os.getenv("NOVELAI_JWT"))) # pst-***
|
|
55
58
|
|
|
56
59
|
|
|
57
60
|
async def main():
|
|
58
|
-
|
|
59
|
-
jwt_token=SecretStr(os.getenv("NOVELAI_JWT")) # ey****
|
|
60
|
-
)
|
|
61
|
-
globe_s1 = ApiCredential(
|
|
62
|
-
api_token=SecretStr(os.getenv("NOVELAI_JWT")) # pst-***
|
|
63
|
-
)
|
|
64
|
-
globe_s2 = LoginCredential(
|
|
65
|
-
username=os.getenv("NOVELAI_USERNAME"),
|
|
66
|
-
password=SecretStr(os.getenv("NOVELAI_PASSWORD"))
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
gen = await GenerateImageInfer.build(
|
|
70
|
-
prompt=f"1girl,{enhance}")
|
|
61
|
+
gen = await GenerateImageInfer.build(prompt=f"1girl,{enhance}")
|
|
71
62
|
cost = gen.calculate_cost(is_opus=True)
|
|
72
63
|
print(f"charge: {cost} if you are vip3")
|
|
73
|
-
|
|
74
|
-
resp = gen.request(session=globe_s)
|
|
64
|
+
resp = gen.request(session=session)
|
|
75
65
|
resp: ImageGenerateResp
|
|
76
66
|
print(resp.meta)
|
|
77
|
-
|
|
78
67
|
file = resp.files[0]
|
|
79
68
|
with open(file[0], "wb") as f:
|
|
80
69
|
f.write(file[1])
|
|
@@ -85,13 +74,48 @@ loop.run_until_complete(main())
|
|
|
85
74
|
|
|
86
75
|
```
|
|
87
76
|
|
|
77
|
+
#### LLM
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
import asyncio
|
|
81
|
+
import os
|
|
82
|
+
|
|
83
|
+
from dotenv import load_dotenv
|
|
84
|
+
from pydantic import SecretStr
|
|
85
|
+
|
|
86
|
+
from novelai_python import APIError, LoginCredential
|
|
87
|
+
from novelai_python.sdk.ai.generate import TextLLMModel, LLM
|
|
88
|
+
|
|
89
|
+
load_dotenv()
|
|
90
|
+
username = os.getenv("NOVELAI_USER", None)
|
|
91
|
+
assert username is not None
|
|
92
|
+
# credential = JwtCredential(jwt_token=SecretStr(jwt))
|
|
93
|
+
login_credential = LoginCredential(
|
|
94
|
+
username=os.getenv("NOVELAI_USER"),
|
|
95
|
+
password=SecretStr(os.getenv("NOVELAI_PASS"))
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def chat(prompt: str):
|
|
100
|
+
try:
|
|
101
|
+
agent = LLM.build(prompt=prompt, model=TextLLMModel.Kayra)
|
|
102
|
+
result = await agent.request(session=login_credential)
|
|
103
|
+
except APIError as e:
|
|
104
|
+
raise Exception(f"Error: {e.message}")
|
|
105
|
+
print(f"Result: \n{result.text}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
loop = asyncio.get_event_loop()
|
|
109
|
+
loop.run_until_complete(chat("Hello"))
|
|
110
|
+
```
|
|
111
|
+
|
|
88
112
|
#### Random Prompt
|
|
89
113
|
|
|
90
114
|
```python
|
|
91
115
|
from novelai_python.tool.random_prompt import RandomPromptGenerator
|
|
92
116
|
|
|
93
|
-
|
|
94
|
-
print(
|
|
117
|
+
prompt = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
|
|
118
|
+
print(prompt)
|
|
95
119
|
```
|
|
96
120
|
|
|
97
121
|
#### Run A Server
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "novelai-python"
|
|
3
|
-
version = "0.4.
|
|
3
|
+
version = "0.4.8"
|
|
4
4
|
description = "NovelAI Python Binding With Pydantic"
|
|
5
5
|
authors = [
|
|
6
6
|
{ name = "sudoskys", email = "coldlando@hotmail.com" },
|
|
@@ -23,8 +23,11 @@ dependencies = [
|
|
|
23
23
|
"tenacity>=8.2.3",
|
|
24
24
|
"sentencepiece>=0.2.0",
|
|
25
25
|
"pynacl>=1.5.0",
|
|
26
|
+
"ftfy>=6.2.0",
|
|
27
|
+
"regex>=2023.12.25",
|
|
28
|
+
"tokenizers>=0.15.2",
|
|
26
29
|
]
|
|
27
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.9"
|
|
28
31
|
readme = "README.md"
|
|
29
32
|
|
|
30
33
|
[project.license]
|
|
@@ -12,14 +12,22 @@ 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
|
|
16
|
-
from .sdk import SuggestTags, SuggestTagsResp
|
|
17
15
|
from .sdk import Information, InformationResp
|
|
16
|
+
from .sdk import LLM, LLMResp
|
|
17
|
+
from .sdk import LLMStream, LLMStreamResp
|
|
18
18
|
from .sdk import Login, LoginResp
|
|
19
19
|
from .sdk import Subscription, SubscriptionResp
|
|
20
|
+
from .sdk import SuggestTags, SuggestTagsResp
|
|
20
21
|
from .sdk import Upscale, UpscaleResp
|
|
22
|
+
from .sdk import VoiceGenerate, VoiceResponse
|
|
21
23
|
|
|
22
24
|
__all__ = [
|
|
25
|
+
"LLM",
|
|
26
|
+
"LLMResp",
|
|
27
|
+
|
|
28
|
+
"LLMStream",
|
|
29
|
+
"LLMStreamResp",
|
|
30
|
+
|
|
23
31
|
"GenerateImageInfer",
|
|
24
32
|
"ImageGenerateResp",
|
|
25
33
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from novelai_python.sdk.ai.generate._enum import TOKENIZER, TextLLMModel # noqa
|
|
6
|
+
from novelai_python.tokenizer import LLMTokenizer
|
|
7
|
+
from novelai_python.utils.encode import b64_to_tokens
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LLMResp(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
response.json().get("output",None)
|
|
16
|
+
"""
|
|
17
|
+
output: str
|
|
18
|
+
text: str
|
|
19
|
+
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=False)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def decode_token(token_str, model: TextLLMModel) -> str:
|
|
23
|
+
return LLMTokenizer.decode(b64_to_tokens(token_str), tokenizer_name=TOKENIZER.get(model))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import Optional, Any
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, ConfigDict
|
|
4
|
+
|
|
5
|
+
from novelai_python.sdk.ai.generate._enum import TOKENIZER, TextLLMModel # noqa
|
|
6
|
+
from novelai_python.tokenizer import LLMTokenizer
|
|
7
|
+
from novelai_python.utils.encode import b64_to_tokens
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LLMStreamResp(BaseModel):
|
|
11
|
+
"""
|
|
12
|
+
{"token":"LRI=","ptr":0,"final":false,"logprobs":null}
|
|
13
|
+
"""
|
|
14
|
+
token: str
|
|
15
|
+
ptr: int
|
|
16
|
+
final: bool
|
|
17
|
+
logprobs: Optional[Any]
|
|
18
|
+
text: Optional[str] = None
|
|
19
|
+
model_config = ConfigDict(extra="ignore", arbitrary_types_allowed=False)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def decode(token_str, model: TextLLMModel) -> str:
|
|
23
|
+
return LLMTokenizer.decode(b64_to_tokens(token_str), tokenizer_name=TOKENIZER.get(model))
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
# @File : __init__.py.py
|
|
5
5
|
# @Software: PyCharm
|
|
6
6
|
|
|
7
|
+
from .ai.generate import LLM, LLMResp # noqa 401
|
|
7
8
|
from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401
|
|
8
9
|
from .ai.generate_image.suggest_tags import SuggestTags, SuggestTagsResp # noqa 401
|
|
10
|
+
from .ai.generate_stream import LLMStream, LLMStreamResp # noqa 401
|
|
9
11
|
from .ai.generate_voice import VoiceGenerate, VoiceResponse # noqa 401
|
|
10
12
|
from .ai.upscale import Upscale, UpscaleResp # noqa 401
|
|
11
13
|
from .user.information import Information, InformationResp # noqa 401
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
# @Author : sudoskys
|
|
3
|
+
from typing import Optional, Union, List
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
import curl_cffi
|
|
7
|
+
import httpx
|
|
8
|
+
import pydantic
|
|
9
|
+
from curl_cffi.requests import AsyncSession
|
|
10
|
+
from loguru import logger
|
|
11
|
+
from pydantic import ConfigDict, PrivateAttr, model_validator, Field
|
|
12
|
+
from tenacity import wait_random, retry, stop_after_attempt, retry_if_exception
|
|
13
|
+
|
|
14
|
+
from ._const import d_repetition_penalty_whitelist, d_bad_words_ids
|
|
15
|
+
from ._enum import TextLLMModel, PenStyle, generate_order, TOKENIZER
|
|
16
|
+
from ._preset import Params, PRESET
|
|
17
|
+
from ...schema import ApiBaseModel
|
|
18
|
+
from ...._exceptions import APIError, SessionHttpError
|
|
19
|
+
from ...._response.ai.generate import LLMResp
|
|
20
|
+
from ....credential import CredentialBase
|
|
21
|
+
from ....tokenizer import LLMTokenizer
|
|
22
|
+
from ....utils.encode import tokens_to_b64
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class LLM(ApiBaseModel):
|
|
26
|
+
"""
|
|
27
|
+
LLM for /ai/generate
|
|
28
|
+
"""
|
|
29
|
+
_endpoint: str = PrivateAttr("https://api.novelai.net")
|
|
30
|
+
input: str = Field(..., description="Base64 encoded token text")
|
|
31
|
+
model: Union[TextLLMModel, str] = TextLLMModel.Kayra
|
|
32
|
+
"""Model to use"""
|
|
33
|
+
parameters: Params = Params()
|
|
34
|
+
|
|
35
|
+
model_config = ConfigDict(extra="ignore")
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def endpoint(self):
|
|
39
|
+
return self._endpoint
|
|
40
|
+
|
|
41
|
+
@endpoint.setter
|
|
42
|
+
def endpoint(self, value):
|
|
43
|
+
self._endpoint = value
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def base_url(self):
|
|
47
|
+
return f"{self.endpoint.strip('/')}/ai/generate"
|
|
48
|
+
|
|
49
|
+
async def necessary_headers(self, request_data) -> dict:
|
|
50
|
+
"""
|
|
51
|
+
:param request_data: dict
|
|
52
|
+
:return: dict
|
|
53
|
+
"""
|
|
54
|
+
return {
|
|
55
|
+
"Host": urlparse(self.endpoint).netloc,
|
|
56
|
+
"accept": "*/*",
|
|
57
|
+
"accept-language": "zh-CN,zh;q=0.9",
|
|
58
|
+
"cache-control": "no-cache",
|
|
59
|
+
"content-type": "application/json",
|
|
60
|
+
"pragma": "no-cache",
|
|
61
|
+
"Referer": "https://novelai.net/",
|
|
62
|
+
"Referrer-Policy": "strict-origin-when-cross-origin"
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@model_validator(mode="after")
|
|
66
|
+
def validate_model(self):
|
|
67
|
+
if isinstance(self.input, str):
|
|
68
|
+
prompt = LLMTokenizer.encode(self.input, tokenizer_name=TOKENIZER.get(self.model))
|
|
69
|
+
self.input = tokens_to_b64(prompt)
|
|
70
|
+
if self.parameters.stop_sequences:
|
|
71
|
+
if not isinstance(self.parameters.stop_sequences, list):
|
|
72
|
+
raise pydantic.ValidationError("stop_sequences must be a list")
|
|
73
|
+
stop_sequences = []
|
|
74
|
+
for i, obj in enumerate(self.parameters.stop_sequences):
|
|
75
|
+
if isinstance(obj, str):
|
|
76
|
+
stop_sequences.append(LLMTokenizer.encode(obj, tokenizer_name=TOKENIZER.get(self.model)))
|
|
77
|
+
elif not isinstance(obj, list):
|
|
78
|
+
raise ValueError(
|
|
79
|
+
f"Expected type 'str' or 'list' for item #{i} of 'stop_sequences', " f"but got '{type(obj)}'"
|
|
80
|
+
)
|
|
81
|
+
self.parameters.stop_sequences = stop_sequences
|
|
82
|
+
if self.parameters.repetition_penalty:
|
|
83
|
+
# adjust repetition penalty value for Sigurd and Euterpe
|
|
84
|
+
if self.model in (TextLLMModel.Sigurd, TextLLMModel.Euterpe):
|
|
85
|
+
rep_pen = self.parameters.repetition_penalty
|
|
86
|
+
self.parameters.repetition_penalty = (0.525 * (rep_pen - 1) / 7) + 1
|
|
87
|
+
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def build(cls,
|
|
92
|
+
prompt: str,
|
|
93
|
+
model: TextLLMModel = TextLLMModel.Kayra,
|
|
94
|
+
min_length: Optional[int] = None,
|
|
95
|
+
max_length: Optional[int] = None,
|
|
96
|
+
temperature: Optional[float] = None,
|
|
97
|
+
top_k: Optional[int] = None,
|
|
98
|
+
top_p: Optional[float] = None,
|
|
99
|
+
frequency_penalty: Optional[float] = None,
|
|
100
|
+
presence_penalty: Optional[float] = None,
|
|
101
|
+
stop_sequences: Optional[Union[List[str]]] = None,
|
|
102
|
+
repetition_penalty: Optional[float] = None,
|
|
103
|
+
pen_style: Optional[PenStyle] = None,
|
|
104
|
+
**kwargs
|
|
105
|
+
) -> "LLM":
|
|
106
|
+
"""
|
|
107
|
+
Generate Prompt
|
|
108
|
+
"""
|
|
109
|
+
assert isinstance(prompt, str), "Prompt must be a string"
|
|
110
|
+
# 清理空值
|
|
111
|
+
kwarg = {
|
|
112
|
+
"min_length": min_length,
|
|
113
|
+
"max_length": max_length,
|
|
114
|
+
"temperature": temperature,
|
|
115
|
+
"top_k": top_k,
|
|
116
|
+
"top_p": top_p,
|
|
117
|
+
"frequency_penalty": frequency_penalty,
|
|
118
|
+
"presence_penalty": presence_penalty,
|
|
119
|
+
"stop_sequences": stop_sequences,
|
|
120
|
+
"repetition_penalty": repetition_penalty,
|
|
121
|
+
"pen_style": pen_style
|
|
122
|
+
}
|
|
123
|
+
kwargs = {**kwarg, **kwargs}
|
|
124
|
+
param = {k: v for k, v in kwargs.items() if v is not None}
|
|
125
|
+
_preset_model = PRESET.get(model.name, {})
|
|
126
|
+
if not _preset_model:
|
|
127
|
+
_build_prop = Params.model_validate(param)
|
|
128
|
+
else:
|
|
129
|
+
_build_prop = _preset_model.model_copy(deep=True, update=param)
|
|
130
|
+
assert _build_prop is not None, "Params validate failed"
|
|
131
|
+
return cls(
|
|
132
|
+
input=prompt,
|
|
133
|
+
model=model,
|
|
134
|
+
parameters=_build_prop
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
@retry(
|
|
138
|
+
wait=wait_random(min=1, max=3),
|
|
139
|
+
stop=stop_after_attempt(3),
|
|
140
|
+
retry=retry_if_exception(lambda e: hasattr(e, "code") and str(e.code) == "500"),
|
|
141
|
+
reraise=True
|
|
142
|
+
)
|
|
143
|
+
async def request(self,
|
|
144
|
+
session: Union[AsyncSession, "CredentialBase"],
|
|
145
|
+
*,
|
|
146
|
+
override_headers: Optional[dict] = None
|
|
147
|
+
) -> LLMResp:
|
|
148
|
+
"""
|
|
149
|
+
生成图片
|
|
150
|
+
:param override_headers:
|
|
151
|
+
:param session: session
|
|
152
|
+
:return: LLMStreamResp
|
|
153
|
+
"""
|
|
154
|
+
# Data Build
|
|
155
|
+
request_data = self.model_dump(mode="json", exclude_none=True)
|
|
156
|
+
request_data["order"] = generate_order(request_data) or []
|
|
157
|
+
assert request_data.get("parameters"), "Parameters is required"
|
|
158
|
+
# Header
|
|
159
|
+
if isinstance(session, AsyncSession):
|
|
160
|
+
session.headers.update(await self.necessary_headers(request_data))
|
|
161
|
+
elif isinstance(session, CredentialBase):
|
|
162
|
+
update_header = await self.necessary_headers(request_data)
|
|
163
|
+
session = await session.get_session(update_headers=update_header)
|
|
164
|
+
if override_headers:
|
|
165
|
+
session.headers.clear()
|
|
166
|
+
session.headers.update(override_headers)
|
|
167
|
+
# Log
|
|
168
|
+
logger.debug(f"LLM request data: {request_data}")
|
|
169
|
+
# Request
|
|
170
|
+
try:
|
|
171
|
+
assert hasattr(session, "post"), "session must have post method."
|
|
172
|
+
response = await session.post(
|
|
173
|
+
self.base_url,
|
|
174
|
+
json=request_data,
|
|
175
|
+
stream=False
|
|
176
|
+
)
|
|
177
|
+
header_type = response.headers.get('Content-Type')
|
|
178
|
+
if "application/json" not in header_type:
|
|
179
|
+
try:
|
|
180
|
+
_msg = response.json()
|
|
181
|
+
except Exception as e:
|
|
182
|
+
_msg = {"statusCode": response.status_code, "message": response.content}
|
|
183
|
+
status_code = _msg.get("statusCode", response.status_code)
|
|
184
|
+
message = _msg.get("message", "Unknown error")
|
|
185
|
+
if status_code == 400:
|
|
186
|
+
raise APIError(
|
|
187
|
+
f"A validation error occured. {message}",
|
|
188
|
+
request=request_data, code=status_code, response=_msg
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
elif status_code == 401:
|
|
192
|
+
raise APIError(
|
|
193
|
+
f"Access Token is incorrect. {message}",
|
|
194
|
+
request=request_data, code=status_code, response=_msg
|
|
195
|
+
)
|
|
196
|
+
elif status_code == 402:
|
|
197
|
+
raise APIError(
|
|
198
|
+
f"An active subscription is required to access this endpoint. {message}",
|
|
199
|
+
request=request_data, code=status_code, response=_msg
|
|
200
|
+
)
|
|
201
|
+
elif status_code == 409:
|
|
202
|
+
raise APIError(
|
|
203
|
+
f"A conflict error occured. {message}",
|
|
204
|
+
request=request_data, code=status_code, response=_msg
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
raise APIError(
|
|
208
|
+
f"An unknown error occured. {response.status_code} {message}",
|
|
209
|
+
request=request_data, code=status_code, response=_msg
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
output = response.json().get("output", None)
|
|
213
|
+
assert output, APIError("No Content Returned",
|
|
214
|
+
request=request_data, code=response.status_code, response=response.content
|
|
215
|
+
)
|
|
216
|
+
return LLMResp(
|
|
217
|
+
output=output,
|
|
218
|
+
text=LLMResp.decode_token(model=self.model, token_str=output)
|
|
219
|
+
)
|
|
220
|
+
except curl_cffi.requests.errors.RequestsError as exc:
|
|
221
|
+
logger.exception(exc)
|
|
222
|
+
raise SessionHttpError(
|
|
223
|
+
"An AsyncSession RequestsError occurred, perhaps it's an SSL error. Please try again later!"
|
|
224
|
+
)
|
|
225
|
+
except httpx.HTTPError as exc:
|
|
226
|
+
logger.exception(exc)
|
|
227
|
+
raise SessionHttpError(
|
|
228
|
+
"An HTTPError occurred, perhaps it's an SSL error. Please try again later!"
|
|
229
|
+
)
|
|
230
|
+
except APIError as e:
|
|
231
|
+
raise e
|
|
232
|
+
except Exception as e:
|
|
233
|
+
logger.opt(exception=e).exception("An unexpected error occurred")
|
|
234
|
+
raise e
|