diff --git a/playground/suggest_tag.py b/playground/suggest_tag.py new file mode 100644 index 0000000..68ace47 --- /dev/null +++ b/playground/suggest_tag.py @@ -0,0 +1,38 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/2/13 下午8:25 +# @Author : sudoskys +# @File : suggest_tag.py +# @Software: PyCharm +import asyncio +import os + +from dotenv import load_dotenv +from loguru import logger +from pydantic import SecretStr + +from novelai_python import APIError +from novelai_python import SuggestTags, SuggestTagsResp, JwtCredential + +load_dotenv() + +token = None +jwt = os.getenv("NOVELAI_JWT") or token + + +async def main(): + globe_s = JwtCredential(jwt_token=SecretStr(jwt)) + try: + _res = await SuggestTags().request( + session=globe_s + ) + _res: SuggestTagsResp + print(f"Information: {_res}") + print(_res.model_dump()) + except APIError as e: + logger.exception(e) + print(e.__dict__) + return + + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/pyproject.toml b/pyproject.toml index 25c1931..01aa89e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "novelai-python" -version = "0.3.1" +version = "0.3.2" description = "Novelai Python Binding With Pydantic" authors = [ { name = "sudoskys", email = "coldlando@hotmail.com" }, diff --git a/src/novelai_python/__init__.py b/src/novelai_python/__init__.py index fbeaefe..d02793c 100644 --- a/src/novelai_python/__init__.py +++ b/src/novelai_python/__init__.py @@ -13,6 +13,7 @@ ) from .credential import JwtCredential, LoginCredential, ApiCredential from .sdk import GenerateImageInfer, ImageGenerateResp +from .sdk import SuggestTags, SuggestTagsResp from .sdk import Information, InformationResp from .sdk import Login, LoginResp from .sdk import Subscription, SubscriptionResp @@ -31,6 +32,9 @@ "Login", "LoginResp", + "SuggestTags", + "SuggestTagsResp", + "Information", "InformationResp", diff --git a/src/novelai_python/_response/ai/generate_image.py b/src/novelai_python/_response/ai/generate_image.py index 673802e..1986cdc 100644 --- a/src/novelai_python/_response/ai/generate_image.py +++ b/src/novelai_python/_response/ai/generate_image.py @@ -22,3 +22,12 @@ def query_params(self, key: str, default=None): if not isinstance(self.meta.raw_request.get("parameters"), dict): raise Exception("Resp parameters is not dict") return self.meta.raw_request.get("parameters").get(key, default) + + +class SuggestTagsResp(RespBase): + class Tag(BaseModel): + tag: str + count: int + confidence: float + + tags: List[Tag] = None diff --git a/src/novelai_python/credential/ApiToken.py b/src/novelai_python/credential/ApiToken.py index b073d90..525df5f 100644 --- a/src/novelai_python/credential/ApiToken.py +++ b/src/novelai_python/credential/ApiToken.py @@ -25,6 +25,7 @@ async def get_session(self, timeout: int = 180, update_headers: dict = None): "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "Authorization": f"Bearer {self.api_token.get_secret_value()}", "Content-Type": "application/json", "Origin": "https://novelai.net", diff --git a/src/novelai_python/credential/JwtToken.py b/src/novelai_python/credential/JwtToken.py index 7550ac9..b7247ad 100644 --- a/src/novelai_python/credential/JwtToken.py +++ b/src/novelai_python/credential/JwtToken.py @@ -24,7 +24,7 @@ async def get_session(self, timeout: int = 180, update_headers: dict = None): self._session = AsyncSession(timeout=timeout, headers={ "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "Accept-Encoding": "gzip, deflate, br", "Authorization": f"Bearer {self.jwt_token.get_secret_value()}", "Content-Type": "application/json", diff --git a/src/novelai_python/credential/UserAuth.py b/src/novelai_python/credential/UserAuth.py index 89ca8a9..ff82499 100644 --- a/src/novelai_python/credential/UserAuth.py +++ b/src/novelai_python/credential/UserAuth.py @@ -30,7 +30,7 @@ async def get_session(self, timeout: int = 180, update_headers: dict = None): resp = await Login.build(user_name=self.username, password=self.password.get_secret_value()).request() self._session = AsyncSession(timeout=timeout, headers={ "Accept": "*/*", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", "Authorization": f"Bearer {resp.accessToken}", diff --git a/src/novelai_python/sdk/__init__.py b/src/novelai_python/sdk/__init__.py index dcefc18..e0d5afc 100644 --- a/src/novelai_python/sdk/__init__.py +++ b/src/novelai_python/sdk/__init__.py @@ -5,7 +5,8 @@ # @Software: PyCharm from .ai.generate_image import GenerateImageInfer, ImageGenerateResp # noqa 401 +from .ai.generate_image.suggest_tags import SuggestTags, SuggestTagsResp # noqa 401 +from .ai.upscale import Upscale, UpscaleResp # noqa 401 from .user.information import Information, InformationResp # noqa 401 from .user.login import Login, LoginResp # noqa 401 from .user.subscription import Subscription, SubscriptionResp # noqa 401 -from .ai.upscale import Upscale, UpscaleResp # noqa 401 \ No newline at end of file diff --git a/src/novelai_python/sdk/ai/generate_image.py b/src/novelai_python/sdk/ai/generate_image/__init__.py similarity index 89% rename from src/novelai_python/sdk/ai/generate_image.py rename to src/novelai_python/sdk/ai/generate_image/__init__.py index 11f933f..a8de372 100644 --- a/src/novelai_python/sdk/ai/generate_image.py +++ b/src/novelai_python/sdk/ai/generate_image/__init__.py @@ -1,14 +1,13 @@ # -*- coding: utf-8 -*- -# @Time : 2024/1/26 上午11:21 +# @Time : 2024/2/13 下午8:08 # @Author : sudoskys -# @File : generate_image.py +# @File : __init__.py.py # @Software: PyCharm import base64 import json import math import random from copy import deepcopy -from enum import Enum, IntEnum from io import BytesIO from typing import Optional, Union from urllib.parse import urlparse @@ -21,84 +20,12 @@ from pydantic import BaseModel, ConfigDict, PrivateAttr, field_validator, model_validator, Field from typing_extensions import override -from ..schema import ApiBaseModel -from ..._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError -from ..._response.ai.generate_image import ImageGenerateResp -from ...credential import CredentialBase -from ...utils import try_jsonfy, NovelAiMetadata - - -class Sampler(Enum): - K_EULER = "k_euler" - K_EULER_ANCESTRAL = "k_euler_ancestral" - K_DPMPP_2S_ANCESTRAL = "k_dpmpp_2s_ancestral" - K_DPMPP_2M = "k_dpmpp_2m" - K_DPMPP_SDE = "k_dpmpp_sde" - DDIM_V3 = "ddim_v3" - - -class NoiseSchedule(Enum): - NATIVE = "native" - KARRAS = "karras" - EXPONENTIAL = "exponential" - POLYEXPONENTIAL = "polyexponential" - - -class UCPreset(IntEnum): - TYPE0 = 0 - TYPE1 = 1 - TYPE2 = 2 - TYPE3 = 3 - - -class Action(Enum): - GENERATE = "generate" - """Generate Image""" - IMG2IMG = "img2img" - """Image to Image""" - INFILL = "infill" - """Inpainting""" - - -class Model(Enum): - NAI_DIFFUSION_3 = "nai-diffusion-3" - NAI_DIFFUSION_3_INPAINTING = "nai-diffusion-3-inpainting" - - NAI_DIFFUSION = "nai-diffusion" - NAI_DIFFUSION_INPAINTING = "nai-diffusion-inpainting" - - SAFE_DIFFUSION = "safe-diffusion" - SAFE_DIFFUSION_INPAINTING = "safe-diffusion-inpainting" - - NAI_DIFFUSION_FURRY = "nai-diffusion-furry" - FURRY_DIFFUSION_INPAINTING = "furry-diffusion-inpainting" - - -class ControlNetModel(Enum): - HED = "hed" - """边缘检测""" - MIDAS = "midas" - """景深""" - FAKE_SCRIBBLE = "fake_scribble" - """伪涂鸦""" - M_LSD = "mlsd" - """(建筑)线条检测""" - LANDSCAPER = "uniformer" - """风景生成""" - - -class Resolution(Enum): - RES_512_768 = (512, 768) - RES_768_512 = (768, 512) - RES_640_640 = (640, 640) - RES_832_1216 = (832, 1216) - RES_1216_832 = (1216, 832) - RES_1024_1024 = (1024, 1024) - RES_1024_1536 = (1024, 1536) - RES_1536_1024 = (1536, 1024) - RES_1472_1472 = (1472, 1472) - RES_1088_1920 = (1088, 1920) - RES_1920_1088 = (1920, 1088) +from ._enum import Model, Sampler, NoiseSchedule, ControlNetModel, Action, UCPreset +from ...schema import ApiBaseModel +from ...._exceptions import APIError, AuthError, ConcurrentGenerationError, SessionHttpError +from ...._response.ai.generate_image import ImageGenerateResp +from ....credential import CredentialBase +from ....utils import try_jsonfy, NovelAiMetadata class GenerateImageInfer(ApiBaseModel): @@ -395,7 +322,7 @@ async def necessary_headers(self, request_data) -> dict: "Sec-Fetch-Site": "same-site", "Pragma": "no-cache", "Cache-Control": "no-cache", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0" + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", } async def request(self, diff --git a/src/novelai_python/sdk/ai/generate_image/_enum.py b/src/novelai_python/sdk/ai/generate_image/_enum.py new file mode 100644 index 0000000..8cfee4d --- /dev/null +++ b/src/novelai_python/sdk/ai/generate_image/_enum.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/2/13 下午8:10 +# @Author : sudoskys +# @File : _enum.py +# @Software: PyCharm +from enum import Enum, IntEnum + + +class Sampler(Enum): + K_EULER = "k_euler" + K_EULER_ANCESTRAL = "k_euler_ancestral" + K_DPMPP_2S_ANCESTRAL = "k_dpmpp_2s_ancestral" + K_DPMPP_2M = "k_dpmpp_2m" + K_DPMPP_SDE = "k_dpmpp_sde" + DDIM_V3 = "ddim_v3" + + +class NoiseSchedule(Enum): + NATIVE = "native" + KARRAS = "karras" + EXPONENTIAL = "exponential" + POLYEXPONENTIAL = "polyexponential" + + +class UCPreset(IntEnum): + TYPE0 = 0 + TYPE1 = 1 + TYPE2 = 2 + TYPE3 = 3 + + +class Action(Enum): + GENERATE = "generate" + """Generate Image""" + IMG2IMG = "img2img" + """Image to Image""" + INFILL = "infill" + """Inpainting""" + + +class ControlNetModel(Enum): + HED = "hed" + """边缘检测""" + MIDAS = "midas" + """景深""" + FAKE_SCRIBBLE = "fake_scribble" + """伪涂鸦""" + M_LSD = "mlsd" + """(建筑)线条检测""" + LANDSCAPER = "uniformer" + """风景生成""" + + +class Resolution(Enum): + RES_512_768 = (512, 768) + RES_768_512 = (768, 512) + RES_640_640 = (640, 640) + RES_832_1216 = (832, 1216) + RES_1216_832 = (1216, 832) + RES_1024_1024 = (1024, 1024) + RES_1024_1536 = (1024, 1536) + RES_1536_1024 = (1536, 1024) + RES_1472_1472 = (1472, 1472) + RES_1088_1920 = (1088, 1920) + RES_1920_1088 = (1920, 1088) + +class Model(Enum): + NAI_DIFFUSION_3 = "nai-diffusion-3" + NAI_DIFFUSION_3_INPAINTING = "nai-diffusion-3-inpainting" + + NAI_DIFFUSION = "nai-diffusion" + NAI_DIFFUSION_INPAINTING = "nai-diffusion-inpainting" + + SAFE_DIFFUSION = "safe-diffusion" + SAFE_DIFFUSION_INPAINTING = "safe-diffusion-inpainting" + + NAI_DIFFUSION_FURRY = "nai-diffusion-furry" + FURRY_DIFFUSION_INPAINTING = "furry-diffusion-inpainting" diff --git a/src/novelai_python/sdk/ai/generate_image/suggest_tags.py b/src/novelai_python/sdk/ai/generate_image/suggest_tags.py new file mode 100644 index 0000000..0406376 --- /dev/null +++ b/src/novelai_python/sdk/ai/generate_image/suggest_tags.py @@ -0,0 +1,127 @@ +# -*- coding: utf-8 -*- +# @Time : 2024/2/13 下午8:09 +# @Author : sudoskys +# @File : suggest-tags.py +# @Software: PyCharm +from typing import Optional, Union +from urllib.parse import urlparse + +import curl_cffi +import httpx +from curl_cffi.requests import AsyncSession +from loguru import logger +from pydantic import PrivateAttr + +from ._enum import Model +from ...schema import ApiBaseModel +from ...._exceptions import APIError, AuthError, SessionHttpError +from ...._response.ai.generate_image import SuggestTagsResp +from ....credential import CredentialBase +from ....utils import try_jsonfy + + +class SuggestTags(ApiBaseModel): + _endpoint: Optional[str] = PrivateAttr("https://api.novelai.net") + model: Model = Model.NAI_DIFFUSION_3 + prompt: str = "landscape" + + @property + def endpoint(self): + return self._endpoint + + @endpoint.setter + def endpoint(self, value): + self._endpoint = value + + @property + def base_url(self): + return f"{self.endpoint.strip('/')}/ai/generate-image/suggest-tags" + + async def necessary_headers(self, request_data) -> dict: + return { + "Host": urlparse(self.endpoint).netloc, + "Accept": "*/*", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", + "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", + "Accept-Encoding": "gzip, deflate, br", + "Access-Control-Allow-Origin": "*", + "Referer": "https://novelai.net/", + "Content-Type": "application/json", + "Origin": "https://novelai.net", + "Connection": "keep-alive", + "Sec-Fetch-Dest": "empty", + "Sec-Fetch-Mode": "cors", + "Sec-Fetch-Site": "same-site", + "Pragma": "no-cache", + "Cache-Control": "no-cache", + "TE": "trailers", + } + + async def request(self, + session: Union[AsyncSession, CredentialBase], + *, + override_headers: Optional[dict] = None + ) -> SuggestTagsResp: + """ + Request to get user subscription information + :param override_headers: + :param session: + :return: + """ + # Data Build + request_data = self.model_dump(mode="json", exclude_none=True) + if isinstance(session, AsyncSession): + session.headers.update(await self.necessary_headers(request_data)) + elif isinstance(session, CredentialBase): + session = await session.get_session(update_headers=await self.necessary_headers(request_data)) + # Header + if override_headers: + session.headers.clear() + session.headers.update(override_headers) + logger.debug("SuggestTags") + try: + assert hasattr(session, "get"), "session must have get method." + response = await session.get( + url=self.base_url + "?" + "&".join([f"{k}={v}" for k, v in request_data.items()]) + ) + if "application/json" not in response.headers.get('Content-Type') or response.status_code != 200: + logger.error( + f"Error with content type: {response.headers.get('Content-Type')} and code: {response.status_code}" + ) + try: + _msg = response.json() + except Exception as e: + logger.warning(e) + if not isinstance(response.content, str) and len(response.content) < 50: + raise APIError( + message=f"Unexpected content type: {response.headers.get('Content-Type')}", + request=request_data, + code=response.status_code, + response=try_jsonfy(response.content) + ) + else: + _msg = {"statusCode": response.status_code, "message": response.content} + status_code = _msg.get("statusCode", response.status_code) + message = _msg.get("message", "Unknown error") + if status_code in [400, 401, 402]: + # 400 : validation error + # 401 : unauthorized + # 402 : payment required + # 409 : conflict + raise AuthError(message, request=request_data, code=status_code, response=_msg) + if status_code in [500]: + # An unknown error occured. + raise APIError(message, request=request_data, code=status_code, response=_msg) + raise APIError(message, request=request_data, code=status_code, response=_msg) + return SuggestTagsResp.model_validate(response.json()) + except curl_cffi.requests.errors.RequestsError as exc: + logger.exception(exc) + raise SessionHttpError("An AsyncSession RequestsError occurred, maybe SSL error. Try again later!") + except httpx.HTTPError as exc: + logger.exception(exc) + raise SessionHttpError("An HTTPError occurred, maybe SSL error. Try again later!") + except APIError as e: + raise e + except Exception as e: + logger.opt(exception=e).exception("An Unexpected error occurred") + raise e diff --git a/src/novelai_python/sdk/ai/upscale.py b/src/novelai_python/sdk/ai/upscale.py index cc4de17..dc4d7b8 100644 --- a/src/novelai_python/sdk/ai/upscale.py +++ b/src/novelai_python/sdk/ai/upscale.py @@ -73,7 +73,7 @@ async def necessary_headers(self, request_data) -> dict: return { "Host": urlparse(self.endpoint).netloc, "Accept": "*/*", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:123.0) Gecko/20100101 Firefox/123.0", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_2_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", "Referer": "https://novelai.net/", diff --git a/src/novelai_python/sdk/user/information.py b/src/novelai_python/sdk/user/information.py index 6cc33b8..3cfbb90 100644 --- a/src/novelai_python/sdk/user/information.py +++ b/src/novelai_python/sdk/user/information.py @@ -4,6 +4,7 @@ # @File : information.py # @Software: PyCharm from typing import Optional, Union +from urllib.parse import urlparse import curl_cffi import httpx @@ -35,7 +36,7 @@ def endpoint(self, value): async def necessary_headers(self, request_data) -> dict: return { - "Host": "api.novelai.net", + "Host": urlparse(self.endpoint).netloc, "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", diff --git a/src/novelai_python/sdk/user/subscription.py b/src/novelai_python/sdk/user/subscription.py index e013e18..4fd9ccc 100644 --- a/src/novelai_python/sdk/user/subscription.py +++ b/src/novelai_python/sdk/user/subscription.py @@ -4,6 +4,7 @@ # @File : subscription.py.py # @Software: PyCharm from typing import Optional, Union +from urllib.parse import urlparse import curl_cffi import httpx @@ -35,7 +36,7 @@ def endpoint(self, value): async def necessary_headers(self, request_data) -> dict: return { - "Host": "api.novelai.net", + "Host": urlparse(self.endpoint).netloc, "Accept": "*/*", "Accept-Language": "zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2", "Accept-Encoding": "gzip, deflate, br", diff --git a/src/novelai_python/server.py b/src/novelai_python/server.py index fe82911..94c7faf 100644 --- a/src/novelai_python/server.py +++ b/src/novelai_python/server.py @@ -15,6 +15,7 @@ from .credential import JwtCredential, SecretStr from .sdk.ai.generate_image import GenerateImageInfer +from .sdk.ai.generate_image.suggest_tags import SuggestTags from .sdk.ai.upscale import Upscale from .sdk.user.information import Information from .sdk.user.login import Login @@ -118,6 +119,25 @@ async def upscale( return JSONResponse(status_code=500, content=e.__dict__) +@app.get("/ai/generate_image/suggest_tags") +async def suggest_tags( + req: SuggestTags, + current_token: str = Depends(get_current_token) +): + """ + 生成建议 + :param current_token: Authorization + :param req: SuggestTags + :return: + """ + try: + _result = await req.request(session=get_session(current_token)) + return _result.model_dump() + except Exception as e: + logger.exception(e) + return JSONResponse(status_code=500, content=e.__dict__) + + @app.post("/ai/generate_image") async def generate_image( req: GenerateImageInfer,