From c092b2df754dce3adc699518c11edceae4818b00 Mon Sep 17 00:00:00 2001 From: Yihang Liu Date: Fri, 27 Sep 2024 11:19:34 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84Cropper=20(#380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor cropper interface; remove baidu aip; add libfacedetect face recognition --- CHANGELOG.md | 4 + config.yml | 14 +- javsp/__main__.py | 76 +++++----- javsp/{core => }/avid.py | 4 +- javsp/{core => }/chromium.py | 0 javsp/{core => }/config.py | 15 +- javsp/core/baidu_aip.py | 246 -------------------------------- javsp/cropper/__init__.py | 9 ++ javsp/cropper/interface.py | 24 ++++ javsp/cropper/slimeface_crop.py | 33 +++++ javsp/cropper/utils.py | 27 ++++ javsp/{core => }/datatype.py | 5 +- javsp/{core => }/file.py | 8 +- javsp/{core => }/func.py | 2 +- javsp/{core => }/image.py | 30 +--- javsp/{core => }/lib.py | 2 +- javsp/{core => }/nfo.py | 4 +- javsp/{core => }/print.py | 0 javsp/web/airav.py | 4 +- javsp/web/arzon.py | 2 +- javsp/web/arzon_iv.py | 6 +- javsp/web/avsox.py | 4 +- javsp/web/avwiki.py | 2 +- javsp/web/base.py | 2 +- javsp/web/dl_getchu.py | 2 +- javsp/web/fanza.py | 4 +- javsp/web/fc2.py | 6 +- javsp/web/fc2fan.py | 4 +- javsp/web/fc2ppvdb.py | 4 +- javsp/web/gyutto.py | 2 +- javsp/web/jav321.py | 2 +- javsp/web/javbus.py | 6 +- javsp/web/javdb.py | 10 +- javsp/web/javlib.py | 4 +- javsp/web/javmenu.py | 2 +- javsp/web/mgstage.py | 4 +- javsp/web/njav.py | 4 +- javsp/web/prestige.py | 2 +- javsp/web/proxyfree.py | 2 - javsp/web/translate.py | 18 +-- poetry.lock | 214 +++++++++++++++------------ pyproject.toml | 6 +- tools/call_crawler.py | 2 +- tools/check_genre.py | 2 +- tools/config_migration.py | 10 +- unittest/test_avid.py | 2 +- unittest/test_crawlers.py | 2 +- unittest/test_file.py | 2 +- unittest/test_func.py | 2 +- unittest/test_lib.py | 2 +- 50 files changed, 340 insertions(+), 503 deletions(-) rename javsp/{core => }/avid.py (99%) rename javsp/{core => }/chromium.py (100%) rename javsp/{core => }/config.py (94%) delete mode 100644 javsp/core/baidu_aip.py create mode 100644 javsp/cropper/__init__.py create mode 100644 javsp/cropper/interface.py create mode 100644 javsp/cropper/slimeface_crop.py create mode 100644 javsp/cropper/utils.py rename javsp/{core => }/datatype.py (98%) rename javsp/{core => }/file.py (98%) rename javsp/{core => }/func.py (99%) rename javsp/{core => }/image.py (54%) rename javsp/{core => }/lib.py (97%) rename javsp/{core => }/nfo.py (98%) rename javsp/{core => }/print.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9efced17d..cf0733d87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ 参见[Package javsp](https://github.com/Yuukiy/JavSP/pkgs/container/javsp)。 - 添加新的爬虫`arzon`, `arzon_iv` [#377](https://github.com/Yuukiy/JavSP/pull/377) +- Slimeface人脸识别 [#380](https://github.com/Yuukiy/JavSP/pull/380) ### Changed - 使用 Poetry 作为构建系统 [134b279](https://github.com/Yuukiy/JavSP/commit/134b279151aead587db0b12d1a30781f2e1be5b1) @@ -33,8 +34,11 @@ ``` - 为了引入对类型注释的支持,最低Python版本现在为3.10 +- 重构封面剪裁逻辑 [#380](https://github.com/Yuukiy/JavSP/pull/380) + ### Removed - Pyinstaller 打包描述文件 [134b279](https://github.com/Yuukiy/JavSP/commit/134b279151aead587db0b12d1a30781f2e1be5b1) - requirements.txt [134b279](https://github.com/Yuukiy/JavSP/commit/134b279151aead587db0b12d1a30781f2e1be5b1) - MovieID.ignore_whole_word 功能和ignore_regex重复 [e096d83](https://github.com/Yuukiy/JavSP/commit/e096d8394a4db29bb4a1123b3d05021de201207d) - NamingRule.media_servers:由于不常用删除,之后会出更general的解决方案 [#353](https://github.com/Yuukiy/JavSP/issues/353) +- Baidu AIP人脸识别,请使用Slimeface替代。 diff --git a/config.yml b/config.yml index 8a2ff9e44..53fac4863 100644 --- a/config.yml +++ b/config.yml @@ -130,16 +130,10 @@ summarizer: - '^HHL' # 要使用的图像识别引擎,详细配置见文档 https://github.com/Yuukiy/JavSP/wiki/AI-%7C-%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB # NOTE: 此处无法直接对应,请参照注释手动填入 - engine: null #null表示禁用图像剪裁 - ## 使用百度人体分析应用: {{{ + engine: null # null表示禁用图像剪裁 + ## 使用Slimeface: {{{ # engine: - # name: baidu_aip - # # 百度人体分析应用的AppID - # app_id: '' - # # 百度人体分析应用的API Key - # api_key: '' - # # 百度人体分析应用的Secret Key - # secret_key: '' + # name: slimeface ## }}} fanart: @@ -150,7 +144,7 @@ summarizer: # 是否下载剧照? enabled: true # 间隔的两次封面爬取请求之间应该间隔多久 - scrap_interval: PT2S + scrap_interval: PT1.5S ################################ translator: diff --git a/javsp/__main__.py b/javsp/__main__.py index b62012ff6..7771170e7 100644 --- a/javsp/__main__.py +++ b/javsp/__main__.py @@ -4,6 +4,7 @@ import json import time import logging +from PIL import Image from pydantic import ValidationError from pydantic_extra_types.pendulum_dt import Duration import requests @@ -21,8 +22,8 @@ pretty_errors.configure(display_link=True) -from javsp.core.print import TqdmOut -from javsp.core.baidu_aip import aip_crop_poster +from javsp.print import TqdmOut +from javsp.cropper import Cropper, get_cropper # 将StreamHandler的stream修改为TqdmOut,以与Tqdm协同工作 @@ -34,17 +35,17 @@ logger = logging.getLogger('main') -from javsp.core.lib import resource_path -from javsp.core.nfo import write_nfo -from javsp.core.file import * -from javsp.core.func import * -from javsp.core.image import * -from javsp.core.datatype import Movie, MovieInfo +from javsp.lib import resource_path +from javsp.nfo import write_nfo +from javsp.file import * +from javsp.func import * +from javsp.image import * +from javsp.datatype import Movie, MovieInfo from javsp.web.base import download from javsp.web.exceptions import * from javsp.web.translate import translate_movie_info -from javsp.core.config import BaiduAipEngine, Cfg, CrawlerID +from javsp.config import Cfg, CrawlerID actressAliasMap = {} @@ -376,25 +377,30 @@ def reviewMovieID(all_movies, root): print() -SUBTITLE_MARK_FILE = os.path.abspath(resource_path('image/sub_mark.png')) -UNCENSORED_MARK_FILE = os.path.abspath(resource_path('image/unc_mark.png')) -def crop_poster_wrapper(fanart_file, poster_file, engine: BaiduAipEngine | None, hard_sub=False, uncensored=False): - """包装各种海报裁剪方法,提供统一的调用""" - if engine is BaiduAipEngine: - try: - aip_crop_poster(fanart_file, engine.app_id, engine.api_key, poster=poster_file) - except Exception as e: - logger.debug('人脸识别失败,回退到常规裁剪方法') - logger.debug(e, exc_info=True) - crop_poster(fanart_file, poster_file) - else: - crop_poster(fanart_file, poster_file) - if Cfg().summarizer.cover.add_label: - if hard_sub == True: - add_label_to_poster(poster_file, SUBTITLE_MARK_FILE, LabelPostion.BOTTOM_RIGHT) - if uncensored == True: - add_label_to_poster(poster_file, UNCENSORED_MARK_FILE, LabelPostion.BOTTOM_LEFT) +SUBTITLE_MARK_FILE = Image.open(os.path.abspath(resource_path('image/sub_mark.png'))) +UNCENSORED_MARK_FILE = Image.open(os.path.abspath(resource_path('image/unc_mark.png'))) + +def process_poster(movie: Movie): + def should_use_ai_crop_match(label): + for r in Cfg().summarizer.cover.crop.on_id_pattern: + re.match(r, label) + return True + return False + crop_engine = None + if (movie.info.uncensored or + movie.data_src == 'fc2' or + should_use_ai_crop_match(movie.info.label.upper())): + crop_engine = Cfg().summarizer.cover.crop.engine + cropper = get_cropper(crop_engine) + fanart_image = Image.open(movie.fanart_file) + fanart_cropped = cropper.crop(fanart_image) + if Cfg().summarizer.cover.add_label: + if movie.hard_sub: + fanart_cropped = add_label_to_poster(fanart_cropped, SUBTITLE_MARK_FILE, LabelPostion.BOTTOM_RIGHT) + if movie.uncensored: + fanart_cropped = add_label_to_poster(fanart_cropped, UNCENSORED_MARK_FILE, LabelPostion.BOTTOM_LEFT) + fanart_cropped.save(movie.poster_file) def RunNormalMode(all_movies): """普通整理模式""" @@ -455,22 +461,8 @@ def check_step(result, msg='步骤错误'): actual_ext = os.path.splitext(pic_path)[1] movie.poster_file = os.path.splitext(movie.poster_file)[0] + actual_ext - def should_use_ai_crop_match(label): - for r in Cfg().summarizer.cover.crop.on_id_pattern: - re.match(r, label) - return True - return False - - crop_engine = None + process_poster(movie) - if (movie.info.uncensored or - movie.data_src == 'fc2' or - should_use_ai_crop_match(movie.info.label.upper())): - crop_engine = Cfg().summarizer.cover.crop.engine - inner_bar.set_description('使用AI裁剪海报封面') - else: - inner_bar.set_description('裁剪海报封面') - crop_poster_wrapper(movie.fanart_file, movie.poster_file, crop_engine, movie.hard_sub, movie.uncensored) check_step(True) if Cfg().summarizer.extra_fanarts.enabled: diff --git a/javsp/core/avid.py b/javsp/avid.py similarity index 99% rename from javsp/core/avid.py rename to javsp/avid.py index 4fa0d764c..f535f1fee 100644 --- a/javsp/core/avid.py +++ b/javsp/avid.py @@ -7,9 +7,7 @@ __all__ = ['get_id', 'get_cid', 'guess_av_type'] -from javsp.core.config import Cfg - -Path('1.png').stem +from javsp.config import Cfg def get_id(filepath_str: str) -> str: """从给定的文件路径中提取番号(DVD ID)""" diff --git a/javsp/core/chromium.py b/javsp/chromium.py similarity index 100% rename from javsp/core/chromium.py rename to javsp/chromium.py diff --git a/javsp/core/config.py b/javsp/config.py similarity index 94% rename from javsp/core/config.py rename to javsp/config.py index edd75acdb..3fbc8f071 100644 --- a/javsp/core/config.py +++ b/javsp/config.py @@ -2,12 +2,12 @@ from enum import Enum from typing import Dict, List, Literal, TypeAlias, Union from confz import BaseConfig, CLArgSource, EnvSource, FileSource -from pydantic import ByteSize, Discriminator, Field, NonNegativeInt, PositiveInt, field_validator +from pydantic import ByteSize, Field, NonNegativeInt, PositiveInt from pydantic_extra_types.pendulum_dt import Duration from pydantic_core import Url from pathlib import Path -from javsp.core.lib import resource_path +from javsp.lib import resource_path class Scanner(BaseConfig): ignored_id_pattern: List[str] @@ -143,15 +143,12 @@ class ExtraFanartSummarize(BaseConfig): enabled: bool scrap_interval: Duration -class BaiduAipEngine(BaseConfig): - name: Literal['baidu_aip'] - app_id: str - api_key: str - secret_key: str +class SlimefaceEngine(BaseConfig): + name: Literal['slimeface'] class CoverCrop(BaseConfig): - engine: BaiduAipEngine | None - on_id_pattern: list[str] + engine: SlimefaceEngine | None + on_id_pattern: list[str] class CoverSummarize(BaseConfig): basename_pattern: str diff --git a/javsp/core/baidu_aip.py b/javsp/core/baidu_aip.py deleted file mode 100644 index d9af9d107..000000000 --- a/javsp/core/baidu_aip.py +++ /dev/null @@ -1,246 +0,0 @@ -"""百度AI开放平台的人体分析方案""" -import os -import sys -import json -import random -import logging -from hashlib import md5 -from datetime import datetime - -from aip import AipBodyAnalysis -from PIL import Image, ImageDraw, ImageFont, ImageOps - -from javsp.core.config import Cfg -from javsp.core.lib import resource_path - -logger = logging.getLogger(__name__) - - -class AipClient(): - def __init__(self, app_id: str, api_key: str) -> None: - # 保存已经识别过的图片的结果,减少请求次数 - self.file = resource_path('data/baidu_aip.cache.json') - dir_path = os.path.dirname(self.file) - os.makedirs(dir_path) - if os.path.exists(self.file): - with open(self.file, 'rt', encoding='utf-8') as f: - self.cache = json.load(f) - else: - self.cache = {} - self.client = AipBodyAnalysis(app_id, api_key, Cfg().summarizer.cover.crop.engine.secret_key) - - def analysis(self, pic_path): - with open(pic_path, 'rb') as f: - pic = f.read() - hash = md5(pic).hexdigest() - if hash in self.cache: - return self.cache[hash]['result'] - else: - now = datetime.now().isoformat() - try: - result = self.client.bodyAnalysis(pic) - except Exception as e: - logger.debug(e, exc_info=True) - raise - if 'error_code' in result: - raise Exception(f"Baidu AIP error {result['error_code']}: {result['error_msg']}") - record = {'pic': os.path.basename(pic_path), 'time': now, 'result': result} - self.cache[hash] = record - with open(self.file, 'wt', encoding='utf-8') as f: - json.dump(self.cache, f, ensure_ascii=False) - return result - - -def choose_center(body_parts): - # 寻找关键部位作为图片的中心点,顺序依次为: - # 鼻子, 左右眼, 左右耳, 左右嘴角, 头顶, 颈部, 左右肩 - if 'nose' in body_parts: - return body_parts['nose'] - pair_parts = (('left_eye', 'right_eye'), - ('left_ear', 'right_ear'), - ('left_mouth_corner', 'right_mouth_corner')) - for parts_name in pair_parts: - pair = [body_parts.get(i) for i in parts_name] - if all(pair): - return {i: (pair[0][i]+pair[1][i])/2 for i in pair[0].keys()} - for part in ('top_head', 'neck'): - if part in body_parts: - return body_parts[part] - pair = [body_parts.get(i) for i in ('left_shoulder', 'right_shoulder')] - if all(pair): - return {i: (pair[0][i]+pair[1][i])/2 for i in pair[0].keys()} - return {} - - -def fit_crop_box(box, persons): - """根据人体框信息调整裁剪框,以包含更完整的人体区域""" - if len(persons) == 0: - return box - elif len(persons) == 1: - loc = persons[0]['location'] - left0, right0 = loc['left'], loc['left']+loc['width'] - top0, bottom0 = loc['top'], loc['top']+loc['height'] - else: - allow_score = persons[0]['total_score'] * 0.7 - allow_persons = [i for i in persons if i['total_score'] >= allow_score] - locs = [] - # 求取一个包含重叠的几个人体框区域的矩形 - for i in allow_persons: - loc = i['location'] - le, ri = loc['left'], loc['left']+loc['width'] - to, bo = loc['top'], loc['top']+loc['height'] - locs.append((le, to, ri, bo)) - locs.sort(key=lambda x: x[0]) # sort by postion left - le0, to0, ri0, bo0 = locs[0] - for (le, to, ri, bo) in locs[1:]: - if le0 <= le < ri0 and (to0 <= to < bo0 or to0 <= bo < bo0): - to0 = max(to, to0) - ri0 = max(ri, ri0) - bo0 = max(bo, bo0) - left0, top0, right0, bottom0 = le0, to0, ri0, bo0 - # 调整裁剪框位置 - (left, top, right, bottom) = box - if left < left0 < right < right0: - move = min(left0-left, right0-right) - left, right = left+move, right+move - if left0 < left < right0 < right: - move = min(left-left0, right-right0) - left, right = left-move, right-move - if top < top0 < bottom < bottom0: - move = min(top0-top, bottom0-bottom) - top, bottom = top+move, bottom+move - if top0 < top < bottom0 < bottom: - move = min(top-top0, bottom-bottom0) - top, bottom = top-move, bottom-move - return (left, top, right, bottom) - - -def aip_crop_poster(fanart, app_id: str, api_key: str, poster='', hw_ratio=1.42): - """将给定的fanart图片文件裁剪为适合poster尺寸的图片""" - - ai = AipClient(app_id, api_key) - - r = ai.analysis(fanart) - im = ImageOps.exif_transpose(Image.open(fanart)) - # 计算识别到的各人体框区域的权重 - for person in r['person_info']: - # 当关键点得分大于0.2的个数大于3,且人体框的分数大于0.03时,才认为是有效人体 - valid_parts = [k for k, v in person['body_parts'].items() if v['score'] > 0.2] - if not (len(valid_parts) > 3 and person['location']['score'] > 0.03): - person['total_score'] = 0 - continue - loc = person['location'] - score, width, height = loc['score'], loc['width'], loc['height'] - # 为每个识别到的区域计算权重,综合考虑区域相对于图片大小的占比和人体识别置信度 - nose_weight = 100 if 'nose' in person['body_parts'] else 30 - total_score = (width*height)/(im.width*im.height) * score * nose_weight - person['total_score'] = total_score - # 计算裁剪框大小(方法同image.py) - fanart_w, fanart_h = im.size - poster_w = int(fanart_h / hw_ratio) - if poster_w <= fanart_w: - poster_h = fanart_h - else: - poster_w, poster_h = fanart_w, int(fanart_w * hw_ratio) - # 采信得分最高的一个人体框 - persons = sorted(r['person_info'], key=lambda x: x['total_score'], reverse=True) - body_parts = persons[0]['body_parts'] - valid_parts = {k: v for k, v in body_parts.items() if v['score'] > 0.3} - if not valid_parts: - raise Exception('Baidu AIP error: 人体识别未获得有效结果') - center = choose_center(valid_parts) - loc = persons[0]['location'] - top0, left0, width0, height0 = loc['top'], loc['left'], loc['width'], loc['height'] - if not center: - # 找不到人体关键部位时,使用人体框中心作为裁剪中心点 - center['x'] = left0 + width0/2 - center['y'] = top0 + height0/2 - left2, top2 = center['x']-poster_w/2, center['y']-poster_h/2 - right2, bottom2 = center['x']+poster_w/2, center['y']+poster_h/2 - # 调整裁剪框的位置,确保裁剪框没有超出图片范围 - if left2 < 0: - left2, right2 = 0, right2-left2 - if top2 < 0: - top2, bottom2 = 0, bottom2-top2 - if right2 > fanart_w: - left2, right2 = left2-(right2-fanart_w), fanart_w - if bottom2 > fanart_h: - top2, bottom2 = top2-(bottom2-fanart_h), fanart_h - # 当裁剪框的一边超出人体框且另一边有移动余量时,移动裁剪框使其对齐人体框 - box = fit_crop_box((left2, top2, right2, bottom2), persons) - # 裁剪图片 - im_poster = im.crop(box) - if im_poster.mode != 'RGB': - im_poster = im_poster.convert('RGB') - im_poster.save(poster, quality=95) - # 调试模式下显示图片结果和标注 - if globals().get('baidu_aip_debug'): - im2 = draw_marks(im, r) - draw = ImageDraw.Draw(im2) - draw_labeled_box(draw, box, outline='red', width=2, label='CROP') - im2.show() - - -# 下面的函数主要用于调试,正常功能中不会触发 - -def random_color(): - # 降低红色取值范围,以便与红色的裁剪框区分开来 - r = random.randint(0, 180) - g = random.randint(0, 255) - b = random.randint(0, 255) - rgb = [r, g, b] - return tuple(rgb) - - -def calc_ellipse(center, r=1.5): - x, y = center - return (x-r, y-r, x+r, y+r) - - -def draw_labeled_box(draw: ImageDraw, xy, fill=None, outline=None, width=1, label=''): - draw.rectangle(xy, fill, outline, width) - scale = min(xy[2]-xy[0], xy[3]-xy[1]) - fontsize = max(12, min(40, int(scale/15))) - try: - fnt = ImageFont.truetype("consola.ttf", fontsize) - except: - fnt = ImageFont.load_default() - if label: - tw, th = draw.textsize(label, font=fnt) - textbox = (xy[0], xy[1], xy[0]+tw, xy[1]+th) - draw.rectangle(textbox, outline) - draw.text((xy[0], xy[1]), label, font=fnt) - - -def draw_marks(im0, data): - """在图片上画出人脸识别到的关键点信息""" - im = im0.copy() - draw = ImageDraw.Draw(im) - for person in data['person_info']: - color = random_color() - for part, info in person['body_parts'].items(): - point = (info['x'], info['y']) - if part == 'nose': - draw.ellipse(calc_ellipse(point, r=2), fill=color, outline=color) - else: - draw.ellipse(calc_ellipse(point), fill=color) - loc = person['location'] - rec = (loc['left'], loc['top'], loc['left']+loc['width'], loc['top']+loc['height']) - label = f"{loc['width']:.0f}x{loc['height']:.0f}, {loc['score']:.3}\n" + \ - f"score: {person['total_score']:.3f}" - draw_labeled_box(draw, rec, outline=color, width=2, label=label) - return im - - -if __name__ == "__main__": - pass - # import pretty_errors - # pretty_errors.configure(display_link=True) - # baidu_aip_debug = True - # files = sys.argv[1:] or ["FC2-1283407-fanart.jpg"] - # for file in files: - # if os.path.exists(file): - # base, ext = os.path.splitext(file) - # poster = base.replace('_fanart', '') + '_poster' + ext - # aip_crop_poster(file, poster) - # print('Crop poster to: ' + poster) diff --git a/javsp/cropper/__init__.py b/javsp/cropper/__init__.py new file mode 100644 index 000000000..e9c340873 --- /dev/null +++ b/javsp/cropper/__init__.py @@ -0,0 +1,9 @@ +from javsp.config import SlimefaceEngine +from javsp.cropper.interface import Cropper, DefaultCropper +from javsp.cropper.slimeface_crop import SlimefaceCropper + +def get_cropper(engine: SlimefaceEngine | None) -> Cropper: + if engine is None: + return DefaultCropper() + if engine.name == 'slimeface': + return SlimefaceCropper() diff --git a/javsp/cropper/interface.py b/javsp/cropper/interface.py new file mode 100644 index 000000000..710c2b630 --- /dev/null +++ b/javsp/cropper/interface.py @@ -0,0 +1,24 @@ +from PIL.Image import Image +from abc import ABC, abstractmethod +class Cropper(ABC): + @abstractmethod + def crop_specific(self, fanart: Image, ratio: float) -> Image: + pass + + def crop(self, fanart: Image, ratio: float | None = None) -> Image: + if ratio is None: + ratio = 1.42 + return self.crop_specific(fanart, ratio) + +class DefaultCropper(Cropper): + def crop_specific(self, fanart: Image, ratio: float) -> Image: + """将给定的fanart图片文件裁剪为适合poster尺寸的图片""" + (fanart_w, fanart_h) = fanart.size + (poster_w, poster_h) = \ + (int(fanart_h / ratio), fanart_h) \ + if fanart_h / fanart_w < ratio \ + else (fanart_w, int(fanart_w * ratio)) # 图片太“瘦”时以宽度来定裁剪高度 + + box = (poster_w - fanart_w, 0, poster_w, poster_h) + fanart.crop(box) + return fanart diff --git a/javsp/cropper/slimeface_crop.py b/javsp/cropper/slimeface_crop.py new file mode 100644 index 000000000..a6e3ca29a --- /dev/null +++ b/javsp/cropper/slimeface_crop.py @@ -0,0 +1,33 @@ +from PIL import Image +from javsp.cropper.interface import Cropper, DefaultCropper +from javsp.cropper.utils import get_bound_box_by_face +from slimeface import detectRGB + +class SlimefaceCropper(Cropper): + def crop_specific(self, fanart: Image.Image, ratio: float) -> Image.Image: + try: + bbox_confs = detectRGB(fanart.width, fanart.height, fanart.convert('RGB').tobytes()) + bbox_confs.sort(key=lambda conf_bbox: -conf_bbox[4]) # last arg stores confidence + face = bbox_confs[0][:-1] + poster_box = get_bound_box_by_face(face, fanart.size, ratio) + return fanart.crop(poster_box) + except: + return DefaultCropper().crop_specific(fanart, ratio) + +if __name__ == '__main__': + from argparse import ArgumentParser + + arg_parser = ArgumentParser(prog='slimeface crop') + + arg_parser.add_argument('-i', '--image', help='path to image to detect') + + args, _ = arg_parser.parse_known_args() + + if(args.image is None): + print("USAGE: slimeface_crop.py -i/--image [path]") + exit(1) + + input = Image.open(args.image) + im = SlimefaceCropper().crop(input) + im.save('output.png') + diff --git a/javsp/cropper/utils.py b/javsp/cropper/utils.py new file mode 100644 index 000000000..b11b48eee --- /dev/null +++ b/javsp/cropper/utils.py @@ -0,0 +1,27 @@ +def get_poster_size(image_shape: tuple[int, int], ratio: float) -> tuple[int, int]: + (fanart_w, fanart_h) = image_shape + (poster_w, poster_h) = \ + (int(fanart_h / ratio), fanart_h) \ + if fanart_h / fanart_w < ratio \ + else (fanart_w, int(fanart_w * ratio)) # 图片太“瘦”时以宽度来定裁剪高度 + return (poster_w, poster_h) + +def get_bound_box_by_face(face: tuple[int, int, int, int], image_shape: tuple[int, int], ratio: float) -> tuple[int, int, int, int]: + """ + returns (left, upper, right, lower) + """ + + (fanart_w, fanart_h) = image_shape + (poster_w, poster_h) = get_poster_size(image_shape, ratio) + + # face coordinates + fx, fy, fw, fh = face + + # face center + cx, cy = fx + fw / 2, fy + fh / 2 + + poster_left = max(cx - poster_w / 2, 0) + poster_left = min(poster_left, fanart_w - poster_w) + poster_left = int(poster_left) + return (poster_left, 0, poster_left + poster_w, poster_h) + diff --git a/javsp/core/datatype.py b/javsp/datatype.py similarity index 98% rename from javsp/core/datatype.py rename to javsp/datatype.py index 2437ea819..4bfdd7171 100644 --- a/javsp/core/datatype.py +++ b/javsp/datatype.py @@ -1,5 +1,4 @@ """定义数据类型和一些通用性的对数据类型的操作""" -from enum import Enum import os import csv import json @@ -7,8 +6,8 @@ import logging from functools import cached_property -from javsp.core.config import Cfg -from javsp.core.lib import resource_path, detect_special_attr +from javsp.config import Cfg +from javsp.lib import resource_path, detect_special_attr logger = logging.getLogger(__name__) diff --git a/javsp/core/file.py b/javsp/file.py similarity index 98% rename from javsp/core/file.py rename to javsp/file.py index 1226eeb7b..9ae6b0f8b 100644 --- a/javsp/core/file.py +++ b/javsp/file.py @@ -13,10 +13,10 @@ __all__ = ['scan_movies', 'get_fmt_size', 'get_remaining_path_len', 'replace_illegal_chars', 'get_failed_when_scan', 'find_subtitle_in_dir'] -from javsp.core.avid import * -from javsp.core.lib import re_escape -from javsp.core.config import Cfg -from javsp.core.datatype import Movie +from javsp.avid import * +from javsp.lib import re_escape +from javsp.config import Cfg +from javsp.datatype import Movie logger = logging.getLogger(__name__) failed_items = [] diff --git a/javsp/core/func.py b/javsp/func.py similarity index 99% rename from javsp/core/func.py rename to javsp/func.py index 6363c0392..403c5deeb 100644 --- a/javsp/core/func.py +++ b/javsp/func.py @@ -25,7 +25,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from javsp.web.base import * -from javsp.core.lib import re_escape, resource_path +from javsp.lib import re_escape, resource_path __all__ = ['select_folder', 'get_scan_dir', 'remove_trail_actor_in_title', diff --git a/javsp/core/image.py b/javsp/image.py similarity index 54% rename from javsp/core/image.py rename to javsp/image.py index 7a5761a3c..280e2fd75 100644 --- a/javsp/core/image.py +++ b/javsp/image.py @@ -5,7 +5,7 @@ from PIL import Image, ImageOps -__all__ = ['valid_pic', 'crop_poster', 'get_pic_size', 'add_label_to_poster', 'LabelPostion'] +__all__ = ['valid_pic', 'get_pic_size', 'add_label_to_poster', 'LabelPostion'] logger = logging.getLogger(__name__) @@ -21,27 +21,6 @@ def valid_pic(pic_path): return False -def crop_poster(fanart_file, poster_file): - """将给定的fanart图片文件裁剪为适合poster尺寸的图片""" - # Kodi的宽高比为2:3,但是按照这个比例来裁剪会导致poster画面不完整, - # 因此按照poster画面比例来裁剪,这样的话虽然在显示时可能有轻微变形,但是画面是完整的 - fanart = Image.open(fanart_file) - fanart_w, fanart_h = fanart.size - # 1.42 = 2535/1785(高清封面), 539/379(普通封面) - poster_w = int(fanart_h / 1.42) - if poster_w <= fanart_w: - poster_h = fanart_h - else: - # 图片太“瘦”时以宽度来定裁剪高度 - poster_w, poster_h = fanart_w, int(fanart_w * 1.42) - # (left, upper, right, lower) - box = (fanart_w-poster_w, 0, fanart_w, poster_h) - poster = fanart.crop(box) - if poster.mode != 'RGB': - poster = poster.convert('RGB') - # quality: from doc, default is 75, values above 95 should be avoided - poster.save(poster_file, quality=95) - # 位置枚举 class LabelPostion(Enum): """水印位置枚举""" @@ -50,10 +29,9 @@ class LabelPostion(Enum): BOTTOM_LEFT = 3 BOTTOM_RIGHT = 4 -def add_label_to_poster(poster_file:str, mark_pic_file: str, pos: LabelPostion): +def add_label_to_poster(poster: Image.Image, mark_pic_file: Image.Image, pos: LabelPostion) -> Image.Image: """向poster中添加标签(水印)""" - poster = Image.open(poster_file) - mark_img = Image.open(mark_pic_file).convert('RGBA') + mark_img = mark_pic_file.convert('RGBA') r,g,b,a = mark_img.split() # 计算水印位置 if pos == LabelPostion.TOP_LEFT: @@ -65,7 +43,7 @@ def add_label_to_poster(poster_file:str, mark_pic_file: str, pos: LabelPostion): elif pos == LabelPostion.BOTTOM_RIGHT: box = (poster.size[0] - mark_img.size[0], poster.size[1] - mark_img.size[1]) poster.paste(mark_img, box=box, mask=a) - poster.save(poster_file, quality=95) + return poster def get_pic_size(pic_path): diff --git a/javsp/core/lib.py b/javsp/lib.py similarity index 97% rename from javsp/core/lib.py rename to javsp/lib.py index e6fb7c558..3b6932d76 100644 --- a/javsp/core/lib.py +++ b/javsp/lib.py @@ -20,7 +20,7 @@ def resource_path(path: str) -> str: if getattr(sys, "frozen", False): return path else: - path_joined = Path(__file__).parent.parent.parent / path + path_joined = Path(__file__).parent.parent / path return str(path_joined) diff --git a/javsp/core/nfo.py b/javsp/nfo.py similarity index 98% rename from javsp/core/nfo.py rename to javsp/nfo.py index 9f7b8a342..573aa0cc3 100644 --- a/javsp/core/nfo.py +++ b/javsp/nfo.py @@ -3,8 +3,8 @@ from lxml.builder import E -from javsp.core.datatype import MovieInfo -from javsp.core.config import Cfg +from javsp.datatype import MovieInfo +from javsp.config import Cfg def write_nfo(info: MovieInfo, nfo_file): diff --git a/javsp/core/print.py b/javsp/print.py similarity index 100% rename from javsp/core/print.py rename to javsp/print.py diff --git a/javsp/web/airav.py b/javsp/web/airav.py index dcffdaaa0..22e9fdbf7 100644 --- a/javsp/web/airav.py +++ b/javsp/web/airav.py @@ -6,8 +6,8 @@ from javsp.web.base import Request from javsp.web.exceptions import * -from javsp.core.config import Cfg -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg +from javsp.datatype import MovieInfo # 初始化Request实例 request = Request(use_scraper=True) diff --git a/javsp/web/arzon.py b/javsp/web/arzon.py index 62c94e89c..7704d74b7 100644 --- a/javsp/web/arzon.py +++ b/javsp/web/arzon.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from javsp.web.base import request_get from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo import requests from lxml import html diff --git a/javsp/web/arzon_iv.py b/javsp/web/arzon_iv.py index 83a8fc054..7a1ea0a80 100644 --- a/javsp/web/arzon_iv.py +++ b/javsp/web/arzon_iv.py @@ -5,9 +5,9 @@ import re sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from web.base import request_get -from web.exceptions import * -from core.datatype import MovieInfo +from javsp.web.base import request_get +from javsp.web.exceptions import * +from javsp.datatype import MovieInfo import requests from lxml import html diff --git a/javsp/web/avsox.py b/javsp/web/avsox.py index 168ea88cc..3333e24e5 100644 --- a/javsp/web/avsox.py +++ b/javsp/web/avsox.py @@ -3,8 +3,8 @@ from javsp.web.base import get_html from javsp.web.exceptions import * -from javsp.core.config import Cfg, CrawlerID -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg, CrawlerID +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/avwiki.py b/javsp/web/avwiki.py index 42a963847..fbd4ecbb3 100644 --- a/javsp/web/avwiki.py +++ b/javsp/web/avwiki.py @@ -4,7 +4,7 @@ from javsp.web.base import * from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) base_url = 'https://av-wiki.net' diff --git a/javsp/web/base.py b/javsp/web/base.py index 38cfad4cb..717b5168a 100644 --- a/javsp/web/base.py +++ b/javsp/web/base.py @@ -14,7 +14,7 @@ from requests.models import Response -from javsp.core.config import Cfg +from javsp.config import Cfg from javsp.web.exceptions import * diff --git a/javsp/web/dl_getchu.py b/javsp/web/dl_getchu.py index 2c61aafaf..15267f1f7 100644 --- a/javsp/web/dl_getchu.py +++ b/javsp/web/dl_getchu.py @@ -4,7 +4,7 @@ from javsp.web.base import resp2html, request_get from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/fanza.py b/javsp/web/fanza.py index 6bd8c0f51..c9bd5acea 100644 --- a/javsp/web/fanza.py +++ b/javsp/web/fanza.py @@ -10,8 +10,8 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from javsp.web.base import Request, resp2html from javsp.web.exceptions import * -from javsp.core.config import Cfg -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/fc2.py b/javsp/web/fc2.py index 7a716454d..66be7ae4e 100644 --- a/javsp/web/fc2.py +++ b/javsp/web/fc2.py @@ -4,9 +4,9 @@ from javsp.web.base import get_html, request_get, resp2html from javsp.web.exceptions import * -from javsp.core.config import Cfg -from javsp.core.lib import strftime_to_minutes -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg +from javsp.lib import strftime_to_minutes +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/fc2fan.py b/javsp/web/fc2fan.py index afcdb8a8d..229b3e3df 100644 --- a/javsp/web/fc2fan.py +++ b/javsp/web/fc2fan.py @@ -9,8 +9,8 @@ from javsp.web.base import resp2html from javsp.web.exceptions import * -from javsp.core.config import Cfg -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/fc2ppvdb.py b/javsp/web/fc2ppvdb.py index 81a539d19..b0ad60892 100644 --- a/javsp/web/fc2ppvdb.py +++ b/javsp/web/fc2ppvdb.py @@ -5,8 +5,8 @@ from javsp.web.base import get_html from javsp.web.exceptions import * -from javsp.core.lib import strftime_to_minutes -from javsp.core.datatype import MovieInfo +from javsp.lib import strftime_to_minutes +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/gyutto.py b/javsp/web/gyutto.py index a553f66ce..db7d6c795 100644 --- a/javsp/web/gyutto.py +++ b/javsp/web/gyutto.py @@ -4,7 +4,7 @@ from javsp.web.base import resp2html, request_get from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/jav321.py b/javsp/web/jav321.py index 3f64a0262..4e42617a5 100644 --- a/javsp/web/jav321.py +++ b/javsp/web/jav321.py @@ -5,7 +5,7 @@ from javsp.web.base import post_html from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/javbus.py b/javsp/web/javbus.py index 8059599c6..bb610e55a 100644 --- a/javsp/web/javbus.py +++ b/javsp/web/javbus.py @@ -4,9 +4,9 @@ from javsp.web.base import * from javsp.web.exceptions import * -from javsp.core.func import * -from javsp.core.config import Cfg, CrawlerID -from javsp.core.datatype import MovieInfo, GenreMap +from javsp.func import * +from javsp.config import Cfg, CrawlerID +from javsp.datatype import MovieInfo, GenreMap logger = logging.getLogger(__name__) diff --git a/javsp/web/javdb.py b/javsp/web/javdb.py index 39e2599d6..b788a10a7 100644 --- a/javsp/web/javdb.py +++ b/javsp/web/javdb.py @@ -5,11 +5,11 @@ from javsp.web.base import Request, resp2html from javsp.web.exceptions import * -from javsp.core.func import * -from javsp.core.avid import guess_av_type -from javsp.core.config import Cfg, CrawlerID -from javsp.core.datatype import MovieInfo, GenreMap -from javsp.core.chromium import get_browsers_cookies +from javsp.func import * +from javsp.avid import guess_av_type +from javsp.config import Cfg, CrawlerID +from javsp.datatype import MovieInfo, GenreMap +from javsp.chromium import get_browsers_cookies # 初始化Request实例。使用scraper绕过CloudFlare后,需要指定网页语言,否则可能会返回其他语言网页,影响解析 diff --git a/javsp/web/javlib.py b/javsp/web/javlib.py index d81676bb9..85f77b75f 100644 --- a/javsp/web/javlib.py +++ b/javsp/web/javlib.py @@ -6,8 +6,8 @@ from javsp.web.base import Request, read_proxy, resp2html from javsp.web.exceptions import * from javsp.web.proxyfree import get_proxy_free_url -from javsp.core.config import Cfg, CrawlerID -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg, CrawlerID +from javsp.datatype import MovieInfo # 初始化Request实例 diff --git a/javsp/web/javmenu.py b/javsp/web/javmenu.py index d32012a0e..5296a69cd 100644 --- a/javsp/web/javmenu.py +++ b/javsp/web/javmenu.py @@ -3,7 +3,7 @@ from javsp.web.base import Request, resp2html from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo request = Request() diff --git a/javsp/web/mgstage.py b/javsp/web/mgstage.py index 0099c1c79..4904e51db 100644 --- a/javsp/web/mgstage.py +++ b/javsp/web/mgstage.py @@ -5,8 +5,8 @@ from javsp.web.base import Request, resp2html from javsp.web.exceptions import * -from javsp.core.config import Cfg -from javsp.core.datatype import MovieInfo +from javsp.config import Cfg +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/njav.py b/javsp/web/njav.py index 762ef59d9..f94e943f3 100644 --- a/javsp/web/njav.py +++ b/javsp/web/njav.py @@ -6,8 +6,8 @@ from javsp.web.base import get_html from javsp.web.exceptions import * -from javsp.core.lib import strftime_to_minutes -from javsp.core.datatype import MovieInfo +from javsp.lib import strftime_to_minutes +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/prestige.py b/javsp/web/prestige.py index fad34acac..f6884c658 100644 --- a/javsp/web/prestige.py +++ b/javsp/web/prestige.py @@ -5,7 +5,7 @@ from javsp.web.base import * from javsp.web.exceptions import * -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo logger = logging.getLogger(__name__) diff --git a/javsp/web/proxyfree.py b/javsp/web/proxyfree.py index 0ec98207a..89c1e63a4 100644 --- a/javsp/web/proxyfree.py +++ b/javsp/web/proxyfree.py @@ -52,7 +52,6 @@ def _get_javbus_urls() -> list: def _get_javlib_urls() -> list: html = get_html('https://github.com/javlibcom') - print(html) text = html.xpath("//div[@class='p-note user-profile-bio mb-3 js-user-profile-bio f4']")[0].text_content() match = re.search(r'[\w\.]+', text, re.A) if match: @@ -62,7 +61,6 @@ def _get_javlib_urls() -> list: def _get_javdb_urls() -> list: html = get_html('https://jav524.app') - print(html) js_links = html.xpath("//script[@src]/@src") for link in js_links: if '/js/index' in link: diff --git a/javsp/web/translate.py b/javsp/web/translate.py index 9c1f9a769..2e762cb15 100644 --- a/javsp/web/translate.py +++ b/javsp/web/translate.py @@ -13,8 +13,8 @@ __all__ = ['translate', 'translate_movie_info'] -from javsp.core.config import BaiduTranslateEngine, BingTranslateEngine, Cfg, ClaudeTranslateEngine, GoogleTranslateEngine, OpenAITranslateEngine, TranslateEngine -from javsp.core.datatype import MovieInfo +from javsp.config import BaiduTranslateEngine, BingTranslateEngine, Cfg, ClaudeTranslateEngine, GoogleTranslateEngine, OpenAITranslateEngine, TranslateEngine +from javsp.datatype import MovieInfo from javsp.web.base import read_proxy @@ -66,9 +66,7 @@ def translate(texts, engine: Union[ """ rtn = {} err_msg = '' - if engine is None: - return {'trans': texts} - elif engine is BaiduTranslateEngine: + if engine.name == 'baidu': result = baidu_translate(texts, engine.app_id, engine.api_key) if 'error_code' not in result: # 百度翻译的结果中的组表示的是按换行符分隔的不同段落,而不是句子 @@ -76,7 +74,7 @@ def translate(texts, engine: Union[ rtn = {'trans': '\n'.join(paragraphs)} else: err_msg = "{}: {}: {}".format(engine, result['error_code'], result['error_msg']) - elif engine is BingTranslateEngine: + elif engine.name == 'bing': # 使用动态词典保护原文中的女优名,防止翻译后认不出来 for i in actress: texts = texts.replace(i, f'{i}') @@ -99,7 +97,7 @@ def translate(texts, engine: Union[ rtn = {'trans': trans, 'orig_break': orig_break, 'trans_break': trans_break} else: err_msg = "{}: {}: {}".format(engine, result['error']['code'], result['error']['message']) - elif engine is ClaudeTranslateEngine: + elif engine.name == 'claude': try: result = claude_translate(texts, engine.api_key) if 'error_code' not in result: @@ -108,7 +106,7 @@ def translate(texts, engine: Union[ err_msg = "{}: {}: {}".format(engine, result['error_code'], result['error_msg']) except Exception as e: err_msg = "{}: {}: Exception: {}".format(engine, -2, repr(e)) - elif engine is OpenAITranslateEngine: + elif engine.name == 'openai': try: result = openai_translate(texts, engine.url, engine.api_key, engine.model) if 'error_code' not in result: @@ -117,7 +115,7 @@ def translate(texts, engine: Union[ err_msg = "{}: {}: {}".format(engine, result['error_code'], result['error_msg']) except Exception as e: err_msg = "{}: {}: Exception: {}".format(engine, -2, repr(e)) - elif engine is GoogleTranslateEngine: + elif engine.name == 'google': try: result = google_trans(texts) # 经测试,翻译成功时会带有'sentences'字段;失败时不带,也没有故障码 @@ -131,6 +129,8 @@ def translate(texts, engine: Union[ err_msg = "{}: {}: {}".format(engine, result['error_code'], result['error_msg']) except Exception as e: err_msg = "{}: {}: Exception: {}".format(engine, -2, repr(e)) + else: + return {'trans': texts} def baidu_translate(texts, app_id, api_key, to='zh'): """使用百度翻译文本(默认翻译为简体中文)""" diff --git a/poetry.lock b/poetry.lock index 7a93cacb4..1c92293a3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -16,24 +16,6 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" -[[package]] -name = "baidu-aip" -version = "2.2.18.0" -description = "Baidu AIP SDK" -optional = false -python-versions = "*" -files = [ - {file = "baidu-aip-2.2.18.0.tar.gz", hash = "sha256:c7f03c671027dcae1ccaa60631ec5811c8e9e51e2ebb505a748a213d0f0b5b88"}, -] - -[package.dependencies] -requests = "*" - -[package.source] -type = "legacy" -url = "https://pypi.tuna.tsinghua.edu.cn/simple" -reference = "mirrors" - [[package]] name = "certifi" version = "2024.8.30" @@ -1067,80 +1049,79 @@ reference = "mirrors" [[package]] name = "pillow" -version = "10.3.0" +version = "10.2.0" description = "Python Imaging Library (Fork)" optional = false python-versions = ">=3.8" files = [ - {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"}, - {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"}, - {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"}, - {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"}, - {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"}, - {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"}, - {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"}, - {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"}, - {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"}, - {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"}, - {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"}, - {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"}, - {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"}, - {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"}, - {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"}, - {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"}, - {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"}, - {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"}, - {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"}, - {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"}, - {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"}, - {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"}, - {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"}, - {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"}, - {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"}, - {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"}, - {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"}, - {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"}, - {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"}, - {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"}, - {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"}, - {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"}, - {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, ] [package.extras] @@ -1728,6 +1709,45 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" +[[package]] +name = "slimeface" +version = "2024.9.27" +description = "A face detection library based on libfacedetection" +optional = false +python-versions = "*" +files = [ + {file = "slimeface-2024.9.27-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:65b94d86df4efe5b1b2340c460fa565e034ec206f7047b8a9638655994a046f6"}, + {file = "slimeface-2024.9.27-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbd603962459d3d8bee82e6bbafc97c5a16bb6c85c7081c3c0272ff04775b8e2"}, + {file = "slimeface-2024.9.27-cp310-cp310-win_amd64.whl", hash = "sha256:e8a7d75bfb308c15342e8471c26fa3bcb36b26670079590fd3c53c4386b1d052"}, + {file = "slimeface-2024.9.27-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0a8c1157b44bcc7b6d13147560aed00c2046f0ba07de198e7882a816168e3f61"}, + {file = "slimeface-2024.9.27-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:add4c258da8be39d9c9ec97b44c9cb487dca94c0876084cf81d8700641635929"}, + {file = "slimeface-2024.9.27-cp311-cp311-win_amd64.whl", hash = "sha256:40b85c407966a5cff3d48ca62b6f0584d48ca1ff815b0a532f428d627b70dc2a"}, + {file = "slimeface-2024.9.27-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9ee38343b0ef95e2653e3fba6cf3137d60735566e6cd39cd5a6a8ffb87999323"}, + {file = "slimeface-2024.9.27-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed8855e14300a0893b14ac6efdc6663b27e4422fdf9a9e8b56725b9ca3a455ed"}, + {file = "slimeface-2024.9.27-cp312-cp312-win_amd64.whl", hash = "sha256:3f1d09feaa2ac2e0613b5bd559603b61b5d3034c0ba372bd2085a069ee2d3c78"}, + {file = "slimeface-2024.9.27-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e0ab7f85c9b036e54a9fe1fe923e23d028a1d152a6f92fc16b7f6e0db58a14a3"}, + {file = "slimeface-2024.9.27-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:631169294505b08a52406f3b266fe57d45074dbd8d1c5c3a24ebc7cb707c2f42"}, + {file = "slimeface-2024.9.27-cp313-cp313-win_amd64.whl", hash = "sha256:6b93c0c7c2a48abe777dcf4845130ff82e8730afea28b1d2942d2ef3bdc20e2b"}, + {file = "slimeface-2024.9.27-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:318d5ef4e44c30f04ceecf37d66d54f825dead035b92e5cb768c822a0843bd5e"}, + {file = "slimeface-2024.9.27-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f006d3eb536667aa9947bd6b95ddfc8fc6e11707106459275d10e3e3a57e41"}, + {file = "slimeface-2024.9.27-cp36-cp36m-win_amd64.whl", hash = "sha256:54d0f910c67d8bf655fbc1d819ad05ff186a41f882bcc5aa35f4ccbe84add7cd"}, + {file = "slimeface-2024.9.27-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b27b9f0daf9bdddc249e87ef03296245c78f862565b5955ee898c5622b974df6"}, + {file = "slimeface-2024.9.27-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:973126c054d2d3d56848375105c15571b5f15f7d87a391d9f7c0060b1c54941b"}, + {file = "slimeface-2024.9.27-cp37-cp37m-win_amd64.whl", hash = "sha256:6f6ed8194d954b576849c50bc67ad9bfcacc8dd85bee8aab907e8e33fe6d6a85"}, + {file = "slimeface-2024.9.27-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:09610b653165b7252c81ab984fe2ad9e1fe61d0b1d58cea41c7c1cdf638e1769"}, + {file = "slimeface-2024.9.27-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f271b4b676120861ccf68e33423aaa45c02211f9b4921083dbc66003de832c89"}, + {file = "slimeface-2024.9.27-cp38-cp38-win_amd64.whl", hash = "sha256:6b693a1fe6cb11161ca61ddad585fd45f1b903f92e282807d16df87f98471dd9"}, + {file = "slimeface-2024.9.27-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3d11d154aabefd1be63953e14152f3da7e4bc125e9ba3b65be0900a737a7df9b"}, + {file = "slimeface-2024.9.27-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:681d603dfff1a58841194394059e299688202e582ca089599983d5be1703f286"}, + {file = "slimeface-2024.9.27-cp39-cp39-win_amd64.whl", hash = "sha256:9f8f38391bcaabd24336c1c13107ff078e1a1a1d3ea4421d9320eaaecad13bb9"}, + {file = "slimeface-2024.9.27.tar.gz", hash = "sha256:e237283e2059f0fbe5cbe5412d4bcb4e986237221758c3259daf097be04b9c70"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "time-machine" version = "2.15.0" @@ -1924,6 +1944,22 @@ type = "legacy" url = "https://pypi.tuna.tsinghua.edu.cn/simple" reference = "mirrors" +[[package]] +name = "types-pillow" +version = "10.2.0.20240822" +description = "Typing stubs for Pillow" +optional = false +python-versions = ">=3.8" +files = [ + {file = "types-Pillow-10.2.0.20240822.tar.gz", hash = "sha256:559fb52a2ef991c326e4a0d20accb3bb63a7ba8d40eb493e0ecb0310ba52f0d3"}, + {file = "types_Pillow-10.2.0.20240822-py3-none-any.whl", hash = "sha256:d9dab025aba07aeb12fd50a6799d4eac52a9603488eca09d7662543983f16c5d"}, +] + +[package.source] +type = "legacy" +url = "https://pypi.tuna.tsinghua.edu.cn/simple" +reference = "mirrors" + [[package]] name = "typing-extensions" version = "4.12.2" @@ -2005,4 +2041,4 @@ reference = "mirrors" [metadata] lock-version = "2.0" python-versions = "<3.13,>=3.10" -content-hash = "c3f4057bbbd2559af45ec852f777bd48a60ad22fae34290d381285aa65a0d6ed" +content-hash = "056b2f7a21b0286a04a5ecadb809f6472c636348fe07976ac42c9c47c620f04c" diff --git a/pyproject.toml b/pyproject.toml index 87a548c6a..a5e1b4d10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ format = "v{base}.{distance}" python = "<3.13,>=3.10" cloudscraper = "1.2.71" colorama = "0.4.4" -pillow = "10.3.0" +pillow = "10.2.0" pretty-errors = "1.2.19" requests = "2.31.0" tqdm = "4.59.0" @@ -25,11 +25,10 @@ pywin32-ctypes = {version = "^0.2.2", markers = "sys_platform == 'win32'"} cryptography = "^42.0.5" pycryptodome = "^3.20.0" lxml = {extras = ["html-clean"], version = "^5.2.1"} -baidu-aip = "^2.2.18.0" confz = "^2.0.1" pydantic-extra-types = "^2.9.0" pendulum = "^3.0.0" - +slimeface = "^2024.9.27" [tool.poetry.scripts] javsp = "javsp.__main__:entry" @@ -46,6 +45,7 @@ pytest = "^8.1.1" flake8 = "^7.0.0" cx-freeze = "^7.2.2" types-lxml = "^2024.4.14" +types-pillow = "^10.2.0.20240822" [build-system] requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] diff --git a/tools/call_crawler.py b/tools/call_crawler.py index 573b53188..ed17b4ba2 100644 --- a/tools/call_crawler.py +++ b/tools/call_crawler.py @@ -13,7 +13,7 @@ file_dir = os.path.dirname(__file__) data_dir = os.path.abspath(os.path.join(file_dir, '../unittest/data')) sys.path.insert(0, os.path.abspath(os.path.join(file_dir, '..'))) -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo # 搜索抓取器并导入它们 diff --git a/tools/check_genre.py b/tools/check_genre.py index e873ab250..dd562dc65 100644 --- a/tools/check_genre.py +++ b/tools/check_genre.py @@ -18,7 +18,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from javsp.web.base import * -from javsp.core.config import cfg +from javsp.config import cfg def get_javbus_genre(): diff --git a/tools/config_migration.py b/tools/config_migration.py index 3196c6bf3..95adc45d6 100644 --- a/tools/config_migration.py +++ b/tools/config_migration.py @@ -178,15 +178,9 @@ def fix_pat(p): # 要使用的图像识别引擎,详细配置见文档 https://github.com/Yuukiy/JavSP/wiki/AI-%7C-%E4%BA%BA%E8%84%B8%E8%AF%86%E5%88%AB # NOTE: 此处无法直接对应,请参照注释手动填入 engine: null #null表示禁用图像剪裁 - ## 使用百度人体分析应用: {{{{{{ + ## 使用Slimeface: {{{{{{ # engine: - # name: baidu_aip - # # 百度人体分析应用的AppID - # app_id: '' - # # 百度人体分析应用的API Key - # api_key: '' - # # 百度人体分析应用的Secret Key - # secret_key: '' + # name: slimeface ## }}}}}} fanart: diff --git a/unittest/test_avid.py b/unittest/test_avid.py index 790abfe3e..ca0c0008f 100644 --- a/unittest/test_avid.py +++ b/unittest/test_avid.py @@ -6,7 +6,7 @@ file_dir = os.path.dirname(__file__) sys.path.insert(0, os.path.abspath(os.path.join(file_dir, '..'))) -from javsp.core.avid import get_id, get_cid +from javsp.avid import get_id, get_cid @pytest.fixture diff --git a/unittest/test_crawlers.py b/unittest/test_crawlers.py index ac3873287..3b0257e07 100644 --- a/unittest/test_crawlers.py +++ b/unittest/test_crawlers.py @@ -9,7 +9,7 @@ data_dir = os.path.join(file_dir, 'data') sys.path.insert(0, os.path.abspath(os.path.join(file_dir, '..'))) -from javsp.core.datatype import MovieInfo +from javsp.datatype import MovieInfo from javsp.web.exceptions import CrawlerError, SiteBlocked diff --git a/unittest/test_file.py b/unittest/test_file.py index 7fb2ec4e3..df83467e0 100644 --- a/unittest/test_file.py +++ b/unittest/test_file.py @@ -7,7 +7,7 @@ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from javsp.core.file import scan_movies +from javsp.file import scan_movies tmp_folder = 'TMP_' + ''.join(random.choices(string.ascii_uppercase, k=6)) diff --git a/unittest/test_func.py b/unittest/test_func.py index cea7a23fb..ca6d0560f 100644 --- a/unittest/test_func.py +++ b/unittest/test_func.py @@ -3,7 +3,7 @@ import random sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from javsp.core.func import * +from javsp.func import * def test_remove_trail_actor_in_title(): diff --git a/unittest/test_lib.py b/unittest/test_lib.py index 5ac7e65cb..43a05338c 100644 --- a/unittest/test_lib.py +++ b/unittest/test_lib.py @@ -2,7 +2,7 @@ import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -from javsp.core.lib import * +from javsp.lib import * def test_detect_special_attr():