novelai-python 0.4.6__tar.gz → 0.4.7__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.7}/PKG-INFO +47 -23
  2. {novelai_python-0.4.6 → novelai_python-0.4.7}/README.md +43 -22
  3. {novelai_python-0.4.6 → novelai_python-0.4.7}/pyproject.toml +4 -1
  4. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/__init__.py +10 -2
  5. novelai_python-0.4.7/src/novelai_python/_response/ai/generate.py +23 -0
  6. novelai_python-0.4.7/src/novelai_python/_response/ai/generate_stream.py +23 -0
  7. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/__init__.py +2 -0
  8. novelai_python-0.4.7/src/novelai_python/sdk/ai/generate/__init__.py +234 -0
  9. novelai_python-0.4.7/src/novelai_python/sdk/ai/generate/_const.py +136 -0
  10. novelai_python-0.4.7/src/novelai_python/sdk/ai/generate/_enum.py +77 -0
  11. novelai_python-0.4.7/src/novelai_python/sdk/ai/generate/_preset.py +225 -0
  12. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_image/__init__.py +11 -3
  13. novelai_python-0.4.7/src/novelai_python/sdk/ai/generate_stream.py +173 -0
  14. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/server.py +46 -1
  15. novelai_python-0.4.7/src/novelai_python/tokenizer/NOTICE.MD +12 -0
  16. novelai_python-0.4.7/src/novelai_python/tokenizer/__init__.py +228 -0
  17. novelai_python-0.4.7/src/novelai_python/tokenizer/bpe_simple_vocab_16e6.txt.gz +0 -0
  18. novelai_python-0.4.7/src/novelai_python/tokenizer/gpt2-genji_tokenizer.json +1 -0
  19. novelai_python-0.4.7/src/novelai_python/tokenizer/gpt2_tokenizer.json +1 -0
  20. novelai_python-0.4.7/src/novelai_python/tokenizer/novelai.model +0 -0
  21. novelai_python-0.4.7/src/novelai_python/tokenizer/novelai_v2.model +0 -0
  22. novelai_python-0.4.7/src/novelai_python/tokenizer/pile_tokenizer.json +1 -0
  23. novelai_python-0.4.7/src/novelai_python/tokenizer/simple_tokenizer.py +139 -0
  24. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/utils/__init__.py +2 -1
  25. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/utils/encode.py +21 -9
  26. novelai_python-0.4.6/src/novelai_python/tokenizer/__init__.py +0 -30
  27. {novelai_python-0.4.6 → novelai_python-0.4.7}/LICENSE +0 -0
  28. {novelai_python-0.4.6 → novelai_python-0.4.7}/NOTICE.MD +0 -0
  29. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_exceptions.py +0 -0
  30. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/__init__.py +0 -0
  31. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/ai/__init__.py +0 -0
  32. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/ai/generate_image.py +0 -0
  33. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/ai/generate_voice.py +0 -0
  34. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/ai/upscale.py +0 -0
  35. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/schema.py +0 -0
  36. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/user/__init__.py +0 -0
  37. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/user/information.py +0 -0
  38. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/user/login.py +0 -0
  39. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/_response/user/subscription.py +0 -0
  40. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/credential/ApiToken.py +0 -0
  41. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/credential/JwtToken.py +0 -0
  42. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/credential/UserAuth.py +0 -0
  43. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/credential/__init__.py +0 -0
  44. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/credential/_base.py +0 -0
  45. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/LICENSE +0 -0
  46. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/__init__.py +0 -0
  47. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_image/_const.py +0 -0
  48. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_image/_enum.py +0 -0
  49. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_image/suggest_tags.py +0 -0
  50. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_voice/__init__.py +0 -0
  51. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/generate_voice/_enum.py +0 -0
  52. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/ai/upscale.py +0 -0
  53. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/schema.py +0 -0
  54. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/user/__init__.py +0 -0
  55. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/user/information.py +0 -0
  56. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/user/login.py +0 -0
  57. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/sdk/user/subscription.py +0 -0
  58. /novelai_python-0.4.6/src/novelai_python/tokenizer/novelai.model → /novelai_python-0.4.7/src/novelai_python/tokenizer/nerdstash_v1.model +0 -0
  59. /novelai_python-0.4.6/src/novelai_python/tokenizer/novelai_v2.model → /novelai_python-0.4.7/src/novelai_python/tokenizer/nerdstash_v2.model +0 -0
  60. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/__init__.py +0 -0
  61. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/image_metadata/__init__.py +0 -0
  62. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/image_metadata/lsb_extractor.py +0 -0
  63. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/image_metadata/lsb_injector.py +0 -0
  64. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/paint_mask/__init__.py +0 -0
  65. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/random_prompt/__init__.py +0 -0
  66. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/random_prompt/tag.py +0 -0
  67. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/random_prompt/tag_character.py +0 -0
  68. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/tool/random_prompt/tag_nsfw.py +0 -0
  69. {novelai_python-0.4.6 → novelai_python-0.4.7}/src/novelai_python/utils/useful.py +0 -0
  70. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/__init__.py +0 -0
  71. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_generate_voice.py +0 -0
  72. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_random_prompt.py +0 -0
  73. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_server.py +0 -0
  74. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_server_run.py +0 -0
  75. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_upscale.py +0 -0
  76. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_user_information.py +0 -0
  77. {novelai_python-0.4.6 → novelai_python-0.4.7}/tests/test_user_login.py +0 -0
  78. {novelai_python-0.4.6 → novelai_python-0.4.7}/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.6
3
+ Version: 0.4.7
4
4
  Summary: NovelAI Python Binding With Pydantic
5
5
  Author-Email: sudoskys <coldlando@hotmail.com>
6
6
  License: Apache License 2.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,7 +39,7 @@ 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
 
@@ -52,10 +55,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
52
55
  - [x] /ai/upscale
53
56
  - [x] /ai/generate-image/suggest-tags
54
57
  - [x] /ai/generate-voice
58
+ - [x] /ai/generate-stream
59
+ - [x] /ai/generate
55
60
  - [ ] /ai/annotate-image
56
61
  - [ ] /ai/classify
57
62
  - [ ] /ai/generate-prompt
58
- - [ ] /ai/generate
59
63
 
60
64
  > GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
61
65
  > consumption information.
@@ -78,34 +82,20 @@ import os
78
82
  from dotenv import load_dotenv
79
83
  from pydantic import SecretStr
80
84
 
81
- from novelai_python import GenerateImageInfer, ImageGenerateResp, JwtCredential, LoginCredential, ApiCredential
85
+ from novelai_python import GenerateImageInfer, ImageGenerateResp, ApiCredential
82
86
 
83
87
  load_dotenv()
84
-
85
88
  enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
89
+ session = ApiCredential(api_token=SecretStr(os.getenv("NOVELAI_JWT"))) # pst-***
86
90
 
87
91
 
88
92
  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}")
93
+ gen = await GenerateImageInfer.build(prompt=f"1girl,{enhance}")
102
94
  cost = gen.calculate_cost(is_opus=True)
103
95
  print(f"charge: {cost} if you are vip3")
104
-
105
- resp = gen.request(session=globe_s)
96
+ resp = gen.request(session=session)
106
97
  resp: ImageGenerateResp
107
98
  print(resp.meta)
108
-
109
99
  file = resp.files[0]
110
100
  with open(file[0], "wb") as f:
111
101
  f.write(file[1])
@@ -116,13 +106,47 @@ loop.run_until_complete(main())
116
106
 
117
107
  ```
118
108
 
109
+ #### LLM
110
+
111
+ ```python
112
+ import asyncio
113
+ import os
114
+
115
+ from dotenv import load_dotenv
116
+
117
+ from novelai_python import APIError, Login
118
+ from novelai_python.sdk.ai.generate import TextLLMModel, LLM
119
+
120
+ load_dotenv()
121
+ username = os.getenv("NOVELAI_USER", None)
122
+ assert username is not None
123
+ # credential = JwtCredential(jwt_token=SecretStr(jwt))
124
+ login_credential = Login.build(
125
+ user_name=os.getenv("NOVELAI_USER"),
126
+ password=os.getenv("NOVELAI_PASS")
127
+ )
128
+
129
+
130
+ async def chat(prompt: str):
131
+ try:
132
+ agent = LLM.build(prompt=prompt, model=TextLLMModel.Kayra)
133
+ result = await agent.request(session=login_credential)
134
+ except APIError as e:
135
+ raise Exception(f"Error: {e.message}")
136
+ print(f"Result: \n{result.text}")
137
+
138
+
139
+ loop = asyncio.get_event_loop()
140
+ loop.run_until_complete(chat("Hello"))
141
+ ```
142
+
119
143
  #### Random Prompt
120
144
 
121
145
  ```python
122
146
  from novelai_python.tool.random_prompt import RandomPromptGenerator
123
147
 
124
- s = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
125
- print(s)
148
+ prompt = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
149
+ print(prompt)
126
150
  ```
127
151
 
128
152
  #### Run A Server
@@ -5,7 +5,7 @@
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
 
@@ -21,10 +21,11 @@ The goal of this repository is to use Pydantic to build legitimate requests to a
21
21
  - [x] /ai/upscale
22
22
  - [x] /ai/generate-image/suggest-tags
23
23
  - [x] /ai/generate-voice
24
+ - [x] /ai/generate-stream
25
+ - [x] /ai/generate
24
26
  - [ ] /ai/annotate-image
25
27
  - [ ] /ai/classify
26
28
  - [ ] /ai/generate-prompt
27
- - [ ] /ai/generate
28
29
 
29
30
  > GenerateImageInfer.calculate_cost is correct in most cases, but please request account information to get accurate
30
31
  > consumption information.
@@ -47,34 +48,20 @@ import os
47
48
  from dotenv import load_dotenv
48
49
  from pydantic import SecretStr
49
50
 
50
- from novelai_python import GenerateImageInfer, ImageGenerateResp, JwtCredential, LoginCredential, ApiCredential
51
+ from novelai_python import GenerateImageInfer, ImageGenerateResp, ApiCredential
51
52
 
52
53
  load_dotenv()
53
-
54
54
  enhance = "year 2023,dynamic angle, best quality, amazing quality, very aesthetic, absurdres"
55
+ session = ApiCredential(api_token=SecretStr(os.getenv("NOVELAI_JWT"))) # pst-***
55
56
 
56
57
 
57
58
  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}")
59
+ gen = await GenerateImageInfer.build(prompt=f"1girl,{enhance}")
71
60
  cost = gen.calculate_cost(is_opus=True)
72
61
  print(f"charge: {cost} if you are vip3")
73
-
74
- resp = gen.request(session=globe_s)
62
+ resp = gen.request(session=session)
75
63
  resp: ImageGenerateResp
76
64
  print(resp.meta)
77
-
78
65
  file = resp.files[0]
79
66
  with open(file[0], "wb") as f:
80
67
  f.write(file[1])
@@ -85,13 +72,47 @@ loop.run_until_complete(main())
85
72
 
86
73
  ```
87
74
 
75
+ #### LLM
76
+
77
+ ```python
78
+ import asyncio
79
+ import os
80
+
81
+ from dotenv import load_dotenv
82
+
83
+ from novelai_python import APIError, Login
84
+ from novelai_python.sdk.ai.generate import TextLLMModel, LLM
85
+
86
+ load_dotenv()
87
+ username = os.getenv("NOVELAI_USER", None)
88
+ assert username is not None
89
+ # credential = JwtCredential(jwt_token=SecretStr(jwt))
90
+ login_credential = Login.build(
91
+ user_name=os.getenv("NOVELAI_USER"),
92
+ password=os.getenv("NOVELAI_PASS")
93
+ )
94
+
95
+
96
+ async def chat(prompt: str):
97
+ try:
98
+ agent = LLM.build(prompt=prompt, model=TextLLMModel.Kayra)
99
+ result = await agent.request(session=login_credential)
100
+ except APIError as e:
101
+ raise Exception(f"Error: {e.message}")
102
+ print(f"Result: \n{result.text}")
103
+
104
+
105
+ loop = asyncio.get_event_loop()
106
+ loop.run_until_complete(chat("Hello"))
107
+ ```
108
+
88
109
  #### Random Prompt
89
110
 
90
111
  ```python
91
112
  from novelai_python.tool.random_prompt import RandomPromptGenerator
92
113
 
93
- s = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
94
- print(s)
114
+ prompt = RandomPromptGenerator(nsfw_enabled=False).random_prompt()
115
+ print(prompt)
95
116
  ```
96
117
 
97
118
  #### Run A Server
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "novelai-python"
3
- version = "0.4.6"
3
+ version = "0.4.7"
4
4
  description = "NovelAI Python Binding With Pydantic"
5
5
  authors = [
6
6
  { name = "sudoskys", email = "coldlando@hotmail.com" },
@@ -23,6 +23,9 @@ 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
30
  requires-python = ">=3.8"
28
31
  readme = "README.md"
@@ -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 Stream for /ai/generate-stream
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
+ "A validation error occured.",
188
+ request=request_data, code=status_code, response=_msg
189
+ )
190
+
191
+ elif status_code == 401:
192
+ raise APIError(
193
+ "Access Token is incorrect.",
194
+ request=request_data, code=status_code, response=_msg
195
+ )
196
+ elif status_code == 402:
197
+ raise APIError(
198
+ "An active subscription is required to access this endpoint.",
199
+ request=request_data, code=status_code, response=_msg
200
+ )
201
+ elif status_code == 409:
202
+ raise APIError(
203
+ "A conflict error occured.",
204
+ request=request_data, code=status_code, response=_msg
205
+ )
206
+ else:
207
+ raise APIError(
208
+ f"An unknown error occured.{response.status_code}",
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