From f3e1463b2ba0695a68da213fd0fcd932645a423a Mon Sep 17 00:00:00 2001 From: Kapitan Date: Fri, 28 Mar 2025 23:49:28 +0100 Subject: [PATCH] Bump version to 0.8.2; enhance image parameter extraction --- .vscode/launch.json | 13 +++ Jenkinsfile | 9 ++ kapitanbooru_uploader/ImageBrowser.py | 2 +- .../locales/en/LC_MESSAGES/messages.po | 2 +- .../locales/pl/LC_MESSAGES/messages.po | 2 +- kapitanbooru_uploader/tag_processing.py | 50 +++++++--- pyproject.toml | 2 +- tests/test_tags.py | 96 +++++++++++++++++++ 8 files changed, 161 insertions(+), 15 deletions(-) create mode 100644 tests/test_tags.py diff --git a/.vscode/launch.json b/.vscode/launch.json index 7961e36..052469b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -49,6 +49,19 @@ "request": "launch", "program": "${workspaceFolder}/main.py", "console": "integratedTerminal" + }, + { + "name": "Python: Debug Unit Tests", + "type": "debugpy", + "request": "launch", + "module": "unittest", + "args": [ + "discover", + "-s", + "tests" + ], + "console": "integratedTerminal" } + ] } \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile index eeae4f7..d8c88d9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -35,6 +35,15 @@ pipeline { sh '. venv/bin/activate && python -m build' // Builds the package } } + + stage('Run Unit Tests') { + steps { + // For unittest: + sh '. venv/bin/activate && python -m unittest discover -s tests' + // For pytest (if preferred): + // sh '. venv/bin/activate && pytest' + } + } stage('Publish to Local PyPI') { steps { diff --git a/kapitanbooru_uploader/ImageBrowser.py b/kapitanbooru_uploader/ImageBrowser.py index ced0502..7a4e596 100644 --- a/kapitanbooru_uploader/ImageBrowser.py +++ b/kapitanbooru_uploader/ImageBrowser.py @@ -32,7 +32,7 @@ class ImageBrowser(tk.Tk): super().__init__() self.title("Kapitanbooru Uploader") self.geometry("900x600") - self.version = "0.8.1" + self.version = "0.8.2" self.acknowledged_version = parse_version(self.version) self.settings = Settings() diff --git a/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po b/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po index 558b5d5..eee9b08 100644 --- a/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po +++ b/kapitanbooru_uploader/locales/en/LC_MESSAGES/messages.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"Project-Id-Version: Kapitanbooru Uploader 0.8.1\n" +"Project-Id-Version: Kapitanbooru Uploader 0.8.2\n" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n" "POT-Creation-Date: 2025-03-27 20:47+0100\n" "Content-Type: text/plain; charset=UTF-8\n" diff --git a/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po b/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po index e40f08d..442b6e7 100644 --- a/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po +++ b/kapitanbooru_uploader/locales/pl/LC_MESSAGES/messages.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"Project-Id-Version: Kapitanbooru Uploader 0.8.1\n" +"Project-Id-Version: Kapitanbooru Uploader 0.8.2\n" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n" "POT-Creation-Date: 2025-03-27 20:47+0100\n" "Language: pl\n" diff --git a/kapitanbooru_uploader/tag_processing.py b/kapitanbooru_uploader/tag_processing.py index bdfdf4c..1a4c539 100644 --- a/kapitanbooru_uploader/tag_processing.py +++ b/kapitanbooru_uploader/tag_processing.py @@ -52,7 +52,7 @@ def get_artist_tags(tags_repo: TagsRepo): # Wzorce i ustawienia związane z tagami PARAMETERS_PATTERN = re.compile( - r"^(?P.*?)\s+(?:Negative prompt:\s+(?P.*?)\s+)?Steps:\s+(?P\d+),\s+Sampler:\s+(?P.*?),\s+Schedule type:\s+(?P.*?),\s+CFG scale:\s+(?P.*?),\s+Seed:\s+(?P.*?),\s+Size:\s+(?P.*?),\s+Model hash:\s+(?P.*?),\s+Model:\s+(?P.*?),\s+(?:Denoising strength:\s+(?P.*?),\s+)?(?:Clip skip: (?P.*?),\s+)?(?:Hires CFG Scale:\s+(?P.*?),\s+Hires upscale:\s+(?P.*?),\s+Hires steps:\s+(?P.*?),\s+Hires upscaler:\s+(?P.*?),\s+)?Version:\s+(?P.*?)\s*$", + r"^(?P.*?),?\s+(?:Negative prompt:\s+(?P.*?)\s+)?Steps:\s+(?P\d+),\s+Sampler:\s+(?P.*?),\s+Schedule type:\s+(?P.*?),\s+CFG scale:\s+(?P.*?),\s+Seed:\s+(?P.*?),\s+Size:\s+(?P.*?),\s+Model hash:\s+(?P.*?),\s+Model:\s+(?P.*?),\s+(?:Denoising strength:\s+(?P.*?),\s+)?(?:Clip skip: (?P.*?),\s+)?(?:Hires CFG Scale:\s+(?P.*?),\s+Hires upscale:\s+(?P.*?),\s+Hires steps:\s+(?P.*?),\s+Hires upscaler:\s+(?P.*?),\s+)?(?:Lora hashes:\s+\"(?P.*?)\",\s+)?Version:\s+(?P.*?)\s*$", re.DOTALL, ) COEFFICIENT_PATTERN = re.compile(r"^.*?(:\d+|\d+\.\d+)$") @@ -122,25 +122,53 @@ def extract_parameters(img: Image, file_path: str) -> str: parameters = "" lower_path = file_path.lower() - # For PNG: use the custom "parameters" field. + # Dla PNG: korzystamy z niestandardowego pola "parameters". if isinstance(img, PngImagePlugin.PngImageFile): parameters = img.info.get("parameters", "") - # For JPEG, WebP, and AVIF: extract EXIF UserComment (EXIF tag 37510). + # Dla JPEG, WebP i AVIF: wyciągamy EXIF UserComment (tag 37510) ze zagnieżdżonego słownika (tag 34665). elif lower_path.endswith((".jpg", ".jpeg", ".webp", ".avif")): exif_data = img.getexif() - user_comment = exif_data.get(37510) + exif_ifd = exif_data.get_ifd(34665) + user_comment = None + if exif_ifd: + user_comment = exif_ifd.get(37510) if user_comment: - # UserComment might be stored as bytes; decode to string. - if isinstance(user_comment, bytes): - try: - parameters = user_comment.decode("utf-8", errors="replace") - except Exception: - parameters = str(user_comment) + if isinstance(user_comment, bytes) and len(user_comment) >= 8: + header = user_comment[:8] + comment_bytes = user_comment[8:] + if header.startswith(b'ASCII'): + try: + parameters = comment_bytes.decode("ascii", errors="ignore") + except Exception: + parameters = str(comment_bytes) + elif header.startswith(b'UNICODE'): + # Sprawdzenie obecności BOM w danych + if comment_bytes[:2] in (b'\xff\xfe', b'\xfe\xff'): + try: + parameters = comment_bytes.decode("utf-16", errors="ignore") + except UnicodeDecodeError: + parameters = comment_bytes.decode("utf-16-be", errors="ignore") + else: + try: + parameters = comment_bytes.decode("utf-16-be", errors="ignore") + except UnicodeDecodeError: + parameters = comment_bytes.decode("utf-16", errors="ignore") + elif header.startswith(b'JIS'): + try: + parameters = comment_bytes.decode("shift_jis", errors="ignore") + except Exception: + parameters = str(comment_bytes) + else: + # Fallback: próba dekodowania jako ASCII + try: + parameters = comment_bytes.decode("ascii", errors="ignore") + except Exception: + parameters = str(comment_bytes) else: parameters = str(user_comment) - # For GIF: extract the GIF comment. + # Dla GIF: wyciągamy komentarz GIF. elif lower_path.endswith(".gif"): comment = img.info.get("comment") if comment: diff --git a/pyproject.toml b/pyproject.toml index 11841ef..9188cd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "kapitanbooru-uploader" -version = "0.8.1" +version = "0.8.2" description = "A GUI application for uploading images to KapitanBooru" authors = [{ name = "Michał Leśniak", email = "kapitan@mlesniak.pl" }] dependencies = [ diff --git a/tests/test_tags.py b/tests/test_tags.py new file mode 100644 index 0000000..12a77e2 --- /dev/null +++ b/tests/test_tags.py @@ -0,0 +1,96 @@ +import unittest +from kapitanbooru_uploader import tag_processing + + +class TestTagProcessing(unittest.TestCase): + def test_PARAMETERS_PATTERN(self): + # Test for a valid tag with parameters + test_data = [ + r"""masterpiece,best quality,amazing quality,explicit,1girl,cat ears,animal ear fluff,very short hair,messy hair,two-tone hair,streaked hair,red streaks,black hair,orange eyes,slit pupils,tan,tomboy,blush,heart,muscular female,pussy,nipples,thick thighs,wide hips,completely nude,black choker,fingernails,bracelet,emo fashion,black nails,outdoors, depth of field,cowboy shot,,colossal breasts,looking at viewer,seductive smile,tall female,(from below), colossal thighs,aroused,lactation,excessive lactation, pussy juice, +Negative prompt: bad quality,worst quality,worst detail,sketch,censor +Steps: 28, Sampler: Euler a, Schedule type: Automatic, CFG scale: 5, Seed: 1508055966, Size: 768x1344, Model hash: 89cb4ec0a9, Model: waiNSFWIllustrious_v120, Denoising strength: 0.4, Clip skip: 2, Hires CFG Scale: 5, Hires upscale: 2, Hires steps: 30, Hires upscaler: R-ESRGAN 4x+ Anime6B, Lora hashes: "LowRA: 0dfc93870ba3, add-detail-xl: 9c783c8ce46c, badhands: 96b1245a1ab8", Version: f1.7.0-v1.10.1RC-latest-2164-g9535244a""", + r"""1girl, nekomata okayu, hololive, nsfw, +simple background, +solo, looking at viewer, seductive smile, huge breasts, sexually suggestive, +full body, dutch angle, +masterpiece, high score, great score, absurdres +Negative prompt: lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry +Steps: 28, Sampler: Euler a, Schedule type: Automatic, CFG scale: 5, Seed: 520762214, Size: 768x1344, Model hash: 6327eca98b, Model: animagine-xl-4.0-opt, Denoising strength: 0.4, Hires CFG Scale: 5, Hires upscale: 2, Hires steps: 30, Hires upscaler: R-ESRGAN 4x+ Anime6B, Version: f1.7.0-v1.10.1RC-latest-2164-g9535244a""", + r"""score_9, score_8_up, score_7_up, score_6_up, score_5_up, score_4_up, a girl with medium breasts and gigantic thighs standing at a gym wearing black sports bra and black yoga pants, 1girl, medium breasts, standing, gym, black sports bra, black pants, yoga pants +Steps: 28, Sampler: Euler a, Schedule type: Automatic, CFG scale: 5, Seed: 3876891898, Size: 768x1344, Model hash: 67ab2fd8ec, Model: ponyDiffusionV6XL_v6StartWithThisOne, Denoising strength: 0.4, Clip skip: 2, Hires CFG Scale: 5, Hires upscale: 2, Hires steps: 30, Hires upscaler: R-ESRGAN 4x+ Anime6B, Version: f1.7.0-v1.10.1RC-latest-2164-g9535244a""", + ] + expected = [ + { + "prompt": r"""masterpiece,best quality,amazing quality,explicit,1girl,cat ears,animal ear fluff,very short hair,messy hair,two-tone hair,streaked hair,red streaks,black hair,orange eyes,slit pupils,tan,tomboy,blush,heart,muscular female,pussy,nipples,thick thighs,wide hips,completely nude,black choker,fingernails,bracelet,emo fashion,black nails,outdoors, depth of field,cowboy shot,,colossal breasts,looking at viewer,seductive smile,tall female,(from below), colossal thighs,aroused,lactation,excessive lactation, pussy juice""", + "negative_prompt": "bad quality,worst quality,worst detail,sketch,censor", + "steps": "28", + "sampler": "Euler a", + "schedule_type": "Automatic", + "cfg_scale": "5", + "seed": "1508055966", + "size": "768x1344", + "model_hash": "89cb4ec0a9", + "model": "waiNSFWIllustrious_v120", + "denoising_strength": "0.4", + "clip_skip": "2", + "hires_cfg_scale": "5", + "hires_upscale": "2", + "hires_steps": "30", + "hires_upscaler": "R-ESRGAN 4x+ Anime6B", + "lora_hashes": "LowRA: 0dfc93870ba3, add-detail-xl: 9c783c8ce46c, badhands: 96b1245a1ab8", + "version": "f1.7.0-v1.10.1RC-latest-2164-g9535244a", + }, + { + "prompt": r"""1girl, nekomata okayu, hololive, nsfw, +simple background, +solo, looking at viewer, seductive smile, huge breasts, sexually suggestive, +full body, dutch angle, +masterpiece, high score, great score, absurdres""", + "negative_prompt": "lowres, bad anatomy, bad hands, text, error, missing finger, extra digits, fewer digits, cropped, worst quality, low quality, low score, bad score, average score, signature, watermark, username, blurry", + "steps": "28", + "sampler": "Euler a", + "schedule_type": "Automatic", + "cfg_scale": "5", + "seed": "520762214", + "size": "768x1344", + "model_hash": "6327eca98b", + "model": "animagine-xl-4.0-opt", + "denoising_strength": "0.4", + "clip_skip": None, + "hires_cfg_scale": "5", + "hires_upscale": "2", + "hires_steps": "30", + "hires_upscaler": "R-ESRGAN 4x+ Anime6B", + "lora_hashes": None, + "version": "f1.7.0-v1.10.1RC-latest-2164-g9535244a", + }, + { + "prompt": r"""score_9, score_8_up, score_7_up, score_6_up, score_5_up, score_4_up, a girl with medium breasts and gigantic thighs standing at a gym wearing black sports bra and black yoga pants, 1girl, medium breasts, standing, gym, black sports bra, black pants, yoga pants""", + "negative_prompt": None, + "steps": "28", + "sampler": "Euler a", + "schedule_type": "Automatic", + "cfg_scale": "5", + "seed": "3876891898", + "size": "768x1344", + "model_hash": "67ab2fd8ec", + "model": "ponyDiffusionV6XL_v6StartWithThisOne", + "denoising_strength": "0.4", + "clip_skip": "2", + "hires_cfg_scale": "5", + "hires_upscale": "2", + "hires_steps": "30", + "hires_upscaler": "R-ESRGAN 4x+ Anime6B", + "lora_hashes": None, + "version": "f1.7.0-v1.10.1RC-latest-2164-g9535244a", + }, + ] + for i, tag in enumerate(test_data): + with self.subTest(i=i): + result = tag_processing.PARAMETERS_PATTERN.search(tag) + self.assertIsNotNone(result) + self.assertEqual(result.groupdict(), expected[i]) + + +if __name__ == "__main__": + unittest.main()