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.
Files changed (78) hide show
  1. {novelai_python-0.4.6 → novelai_python-0.4.8}/PKG-INFO +51 -24
  2. {novelai_python-0.4.6 → novelai_python-0.4.8}/README.md +46 -22
  3. {novelai_python-0.4.6 → novelai_python-0.4.8}/pyproject.toml +5 -2
  4. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/__init__.py +10 -2
  5. novelai_python-0.4.8/src/novelai_python/_response/ai/generate.py +23 -0
  6. novelai_python-0.4.8/src/novelai_python/_response/ai/generate_stream.py +23 -0
  7. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/__init__.py +2 -0
  8. novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/__init__.py +234 -0
  9. novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_const.py +136 -0
  10. novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_enum.py +77 -0
  11. novelai_python-0.4.8/src/novelai_python/sdk/ai/generate/_preset.py +225 -0
  12. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/__init__.py +11 -3
  13. novelai_python-0.4.8/src/novelai_python/sdk/ai/generate_stream.py +173 -0
  14. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/server.py +45 -1
  15. novelai_python-0.4.8/src/novelai_python/tokenizer/NOTICE.MD +12 -0
  16. novelai_python-0.4.8/src/novelai_python/tokenizer/__init__.py +228 -0
  17. novelai_python-0.4.8/src/novelai_python/tokenizer/bpe_simple_vocab_16e6.txt.gz +0 -0
  18. novelai_python-0.4.8/src/novelai_python/tokenizer/gpt2-genji_tokenizer.json +1 -0
  19. novelai_python-0.4.8/src/novelai_python/tokenizer/gpt2_tokenizer.json +1 -0
  20. novelai_python-0.4.8/src/novelai_python/tokenizer/novelai.model +0 -0
  21. novelai_python-0.4.8/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  22. novelai_python-0.4.8/src/novelai_python/tokenizer/pile_tokenizer.json +1 -0
  23. novelai_python-0.4.8/src/novelai_python/tokenizer/simple_tokenizer.py +139 -0
  24. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/__init__.py +28 -3
  25. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/paint_mask/__init__.py +7 -6
  26. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/__init__.py +2 -1
  27. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/encode.py +21 -9
  28. novelai_python-0.4.6/src/novelai_python/tokenizer/__init__.py +0 -30
  29. {novelai_python-0.4.6 → novelai_python-0.4.8}/LICENSE +0 -0
  30. {novelai_python-0.4.6 → novelai_python-0.4.8}/NOTICE.MD +0 -0
  31. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_exceptions.py +0 -0
  32. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/__init__.py +0 -0
  33. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/__init__.py +0 -0
  34. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/generate_image.py +0 -0
  35. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  36. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/ai/upscale.py +0 -0
  37. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/schema.py +0 -0
  38. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/__init__.py +0 -0
  39. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/information.py +0 -0
  40. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/login.py +0 -0
  41. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/_response/user/subscription.py +0 -0
  42. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/ApiToken.py +0 -0
  43. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/JwtToken.py +0 -0
  44. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/UserAuth.py +0 -0
  45. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/__init__.py +0 -0
  46. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/credential/_base.py +0 -0
  47. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/LICENSE +0 -0
  48. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/__init__.py +0 -0
  49. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
  50. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
  51. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  52. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
  53. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
  54. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/ai/upscale.py +0 -0
  55. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/schema.py +0 -0
  56. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/__init__.py +0 -0
  57. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/information.py +0 -0
  58. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/login.py +0 -0
  59. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/sdk/user/subscription.py +0 -0
  60. /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
  61. /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
  62. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/__init__.py +0 -0
  63. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
  64. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  65. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  66. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag.py +0 -0
  67. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
  68. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
  69. {novelai_python-0.4.6 → novelai_python-0.4.8}/src/novelai_python/utils/useful.py +0 -0
  70. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/__init__.py +0 -0
  71. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_generate_voice.py +0 -0
  72. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_random_prompt.py +0 -0
  73. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_server.py +0 -0
  74. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_server_run.py +0 -0
  75. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_upscale.py +0 -0
  76. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_user_information.py +0 -0
  77. {novelai_python-0.4.6 → novelai_python-0.4.8}/tests/test_user_login.py +0 -0
  78. {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.6
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.8
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
  [![PyPI version](https://badge.fury.io/py/novelai-python.svg)](https://badge.fury.io/py/novelai-python)
37
40
  [![Downloads](https://pepy.tech/badge/novelai_python)](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, JwtCredential, LoginCredential, ApiCredential
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
- globe_s = JwtCredential(
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
- s = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
125
- print(s)
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
  [![PyPI version](https://badge.fury.io/py/novelai-python.svg)](https://badge.fury.io/py/novelai-python)
6
6
  [![Downloads](https://pepy.tech/badge/novelai_python)](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, JwtCredential, LoginCredential, ApiCredential
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
- globe_s = JwtCredential(
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
- s = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
94
- print(s)
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.6"
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.8"
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