Bump version to 0.8.2; enhance image parameter extraction
Some checks failed
Gitea/kapitanbooru-uploader/pipeline/head There was a failure building this commit

This commit is contained in:
Michał Leśniak 2025-03-28 23:49:28 +01:00
parent df83c9dcdc
commit f3e1463b2b
8 changed files with 161 additions and 15 deletions

13
.vscode/launch.json vendored
View File

@ -49,6 +49,19 @@
"request": "launch", "request": "launch",
"program": "${workspaceFolder}/main.py", "program": "${workspaceFolder}/main.py",
"console": "integratedTerminal" "console": "integratedTerminal"
},
{
"name": "Python: Debug Unit Tests",
"type": "debugpy",
"request": "launch",
"module": "unittest",
"args": [
"discover",
"-s",
"tests"
],
"console": "integratedTerminal"
} }
] ]
} }

9
Jenkinsfile vendored
View File

@ -36,6 +36,15 @@ pipeline {
} }
} }
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') { stage('Publish to Local PyPI') {
steps { steps {
sh '. venv/bin/activate && twine upload --repository-url http://localhost:8090/ -u admin -p admin dist/*' sh '. venv/bin/activate && twine upload --repository-url http://localhost:8090/ -u admin -p admin dist/*'

View File

@ -32,7 +32,7 @@ class ImageBrowser(tk.Tk):
super().__init__() super().__init__()
self.title("Kapitanbooru Uploader") self.title("Kapitanbooru Uploader")
self.geometry("900x600") self.geometry("900x600")
self.version = "0.8.1" self.version = "0.8.2"
self.acknowledged_version = parse_version(self.version) self.acknowledged_version = parse_version(self.version)
self.settings = Settings() self.settings = Settings()

View File

@ -1,6 +1,6 @@
msgid "" msgid ""
msgstr "" 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" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n"
"POT-Creation-Date: 2025-03-27 20:47+0100\n" "POT-Creation-Date: 2025-03-27 20:47+0100\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"

View File

@ -1,6 +1,6 @@
msgid "" msgid ""
msgstr "" 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" "Report-Msgid-Bugs-To: kapitan@mlesniak.pl\n"
"POT-Creation-Date: 2025-03-27 20:47+0100\n" "POT-Creation-Date: 2025-03-27 20:47+0100\n"
"Language: pl\n" "Language: pl\n"

View File

@ -52,7 +52,7 @@ def get_artist_tags(tags_repo: TagsRepo):
# Wzorce i ustawienia związane z tagami # Wzorce i ustawienia związane z tagami
PARAMETERS_PATTERN = re.compile( PARAMETERS_PATTERN = re.compile(
r"^(?P<prompt>.*?)\s+(?:Negative prompt:\s+(?P<negative_prompt>.*?)\s+)?Steps:\s+(?P<steps>\d+),\s+Sampler:\s+(?P<sampler>.*?),\s+Schedule type:\s+(?P<schedule_type>.*?),\s+CFG scale:\s+(?P<cfg_scale>.*?),\s+Seed:\s+(?P<seed>.*?),\s+Size:\s+(?P<size>.*?),\s+Model hash:\s+(?P<model_hash>.*?),\s+Model:\s+(?P<model>.*?),\s+(?:Denoising strength:\s+(?P<denoising_strength>.*?),\s+)?(?:Clip skip: (?P<clip_skip>.*?),\s+)?(?:Hires CFG Scale:\s+(?P<hires_cfg_scale>.*?),\s+Hires upscale:\s+(?P<hires_upscale>.*?),\s+Hires steps:\s+(?P<hires_steps>.*?),\s+Hires upscaler:\s+(?P<hires_upscaler>.*?),\s+)?Version:\s+(?P<version>.*?)\s*$", r"^(?P<prompt>.*?),?\s+(?:Negative prompt:\s+(?P<negative_prompt>.*?)\s+)?Steps:\s+(?P<steps>\d+),\s+Sampler:\s+(?P<sampler>.*?),\s+Schedule type:\s+(?P<schedule_type>.*?),\s+CFG scale:\s+(?P<cfg_scale>.*?),\s+Seed:\s+(?P<seed>.*?),\s+Size:\s+(?P<size>.*?),\s+Model hash:\s+(?P<model_hash>.*?),\s+Model:\s+(?P<model>.*?),\s+(?:Denoising strength:\s+(?P<denoising_strength>.*?),\s+)?(?:Clip skip: (?P<clip_skip>.*?),\s+)?(?:Hires CFG Scale:\s+(?P<hires_cfg_scale>.*?),\s+Hires upscale:\s+(?P<hires_upscale>.*?),\s+Hires steps:\s+(?P<hires_steps>.*?),\s+Hires upscaler:\s+(?P<hires_upscaler>.*?),\s+)?(?:Lora hashes:\s+\"(?P<lora_hashes>.*?)\",\s+)?Version:\s+(?P<version>.*?)\s*$",
re.DOTALL, re.DOTALL,
) )
COEFFICIENT_PATTERN = re.compile(r"^.*?(:\d+|\d+\.\d+)$") COEFFICIENT_PATTERN = re.compile(r"^.*?(:\d+|\d+\.\d+)$")
@ -122,25 +122,53 @@ def extract_parameters(img: Image, file_path: str) -> str:
parameters = "" parameters = ""
lower_path = file_path.lower() lower_path = file_path.lower()
# For PNG: use the custom "parameters" field. # Dla PNG: korzystamy z niestandardowego pola "parameters".
if isinstance(img, PngImagePlugin.PngImageFile): if isinstance(img, PngImagePlugin.PngImageFile):
parameters = img.info.get("parameters", "") 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")): elif lower_path.endswith((".jpg", ".jpeg", ".webp", ".avif")):
exif_data = img.getexif() 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: if user_comment:
# UserComment might be stored as bytes; decode to string. if isinstance(user_comment, bytes) and len(user_comment) >= 8:
if isinstance(user_comment, bytes): header = user_comment[:8]
comment_bytes = user_comment[8:]
if header.startswith(b'ASCII'):
try: try:
parameters = user_comment.decode("utf-8", errors="replace") parameters = comment_bytes.decode("ascii", errors="ignore")
except Exception: except Exception:
parameters = str(user_comment) 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: else:
parameters = str(user_comment) parameters = str(user_comment)
# For GIF: extract the GIF comment. # Dla GIF: wyciągamy komentarz GIF.
elif lower_path.endswith(".gif"): elif lower_path.endswith(".gif"):
comment = img.info.get("comment") comment = img.info.get("comment")
if comment: if comment:

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "kapitanbooru-uploader" name = "kapitanbooru-uploader"
version = "0.8.1" version = "0.8.2"
description = "A GUI application for uploading images to KapitanBooru" description = "A GUI application for uploading images to KapitanBooru"
authors = [{ name = "Michał Leśniak", email = "kapitan@mlesniak.pl" }] authors = [{ name = "Michał Leśniak", email = "kapitan@mlesniak.pl" }]
dependencies = [ dependencies = [

96
tests/test_tags.py Normal file
View File

@ -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,<lora:HYPv1-2:1>,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,<lora:HYPv1-2:1>,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()