Spaces:
Running
Running
File size: 7,998 Bytes
5dd4a4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | import ast
import io
import json
import os
import tempfile
import time
import unittest
from html import escape
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
from PIL import Image
from image_inversion_job_api import (
ImageInversionJobRequest,
ImageInversionJobSettings,
decode_image_bytes,
)
class AppV1IntegrationTests(unittest.TestCase):
def setUp(self):
"""隔离加载应用定义,为集成测试准备六类模型结果。"""
# 只加载本次集成涉及的实际业务定义,避免测试时启动 Gradio 或下载权重。
app_path = Path(__file__).resolve().parents[1] / "app.py"
self.tree = ast.parse(app_path.read_text(encoding="utf-8"))
definition_names = {
"Tagger", "ImageValidationError", "_format_size", "validate_and_open_image",
"format_tags_html", "generate_summary_text_content", "execute_image_inversion_job",
}
constant_names = {
"MAX_UPLOAD_BYTES", "MAX_IMAGE_SIDE", "MAX_IMAGE_PIXELS",
"MAX_DECOMPRESSED_BYTES", "ALLOWED_IMAGE_FORMATS", "ASSETS_REPO_ID",
"ASSETS_REVISION",
}
definitions = [
node for node in ast.walk(self.tree)
if (
isinstance(node, (ast.ClassDef, ast.FunctionDef))
and node.name in definition_names
) or (
isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id in constant_names for target in node.targets)
)
]
self.scope = {
"Image": Image, "ImageInversionJobRequest": ImageInversionJobRequest,
"os": os, "time": time, "json": json, "escape": escape,
}
exec(compile(ast.Module(body=definitions, type_ignores=[]), str(app_path), "exec"), self.scope)
self.tagger = self.scope["Tagger"].__new__(self.scope["Tagger"])
self.tagger.device = "cpu"
self.handler_output = {
"feature_scores": {"blue_eyes": 0.71, "long_hair": 0.98},
"character_scores": {"test_character": 0.84},
"ip_scores": {"test_series": 0.76},
"style_scores": {"anime": 0.89},
"meta_scores": {"highres": 0.91},
"rating_scores": {"general": 0.99},
"_params": {"general_threshold": 0.17, "character_threshold": 0.27},
"_timings": {"inference_s": 0.125},
}
self.tagger.handler = Mock(return_value=self.handler_output)
self.scope["tagger_instance"] = self.tagger
def test_predict_preserves_all_categories_and_real_ip_confidence(self):
"""验证完整类别元数据和真实 IP 置信度。"""
image = Image.new("RGBA", (32, 32), (10, 20, 30, 0))
result, translation_order, metadata = self.tagger.predict(image)
self.assertEqual(list(result), ["general", "characters", "ips"])
self.assertEqual(result["general"], {"long hair": 0.98, "blue eyes": 0.71})
self.assertEqual(translation_order["general"], ["long hair", "blue eyes"])
self.assertEqual(result["ips"], {"test series": 0.76})
self.assertEqual(metadata["tags"], {
"general": self.handler_output["feature_scores"],
"character": self.handler_output["character_scores"],
"copyright": self.handler_output["ip_scores"],
"style": self.handler_output["style_scores"],
"meta": self.handler_output["meta_scores"],
"rating": self.handler_output["rating_scores"],
})
call_data = self.tagger.handler.call_args.args[0]
self.assertIs(call_data["inputs"], image)
self.assertTrue(call_data["parameters"]["include_scores"])
self.assertEqual(call_data["parameters"]["general_threshold"], 0.17)
self.assertEqual(call_data["parameters"]["character_threshold"], 0.27)
def test_job_keeps_six_fields_and_translation_alignment(self):
"""验证任务接口保持六个字段且翻译不串位。"""
self.scope["translate_texts"] = Mock(return_value=["长发", "蓝眼睛", "测试角色", "测试作品"])
payload = ImageInversionJobRequest(input_image_url="https://example.com/image.png")
result = self.scope["execute_image_inversion_job"](payload, Image.new("RGB", (32, 32)), "job-id")
self.assertEqual(set(result), {
"status_markdown", "general_tags_html", "character_tags_html",
"ip_tags_html", "summary_text", "metadata",
})
self.scope["translate_texts"].assert_called_once_with(
["long hair", "blue eyes", "test character", "test series"],
src_lang="auto", tgt_lang="zh",
)
self.assertIn("长发", result["general_tags_html"])
self.assertIn("测试角色", result["character_tags_html"])
self.assertIn("测试作品", result["ip_tags_html"])
self.assertIn("0.760", result["ip_tags_html"])
self.assertIn("test series/*测试作品*/", result["summary_text"])
self.assertEqual(len(result["metadata"]["tags"]), 6)
# 整个翻译调用异常也不能影响标签和新增元数据的交付。
self.scope["translate_texts"].side_effect = RuntimeError("translation unavailable")
untranslated = self.scope["execute_image_inversion_job"](payload, Image.new("RGB", (32, 32)), "job-id")
self.assertEqual(set(untranslated), set(result))
self.assertIn("test series", untranslated["summary_text"])
self.assertNotIn("测试作品", untranslated["ip_tags_html"])
self.assertEqual(untranslated["metadata"]["tags"], result["metadata"]["tags"])
def test_ui_and_api_defaults_match_v1_thresholds(self):
"""验证页面和任务接口的默认阈值与 v1.0 推荐值一致。"""
slider_nodes = [
node for node in ast.walk(self.tree)
if isinstance(node, ast.Assign)
and any(isinstance(target, ast.Name) and target.id in {"gen_slider", "char_slider"} for target in node.targets)
]
self.scope["gr"] = SimpleNamespace(Slider=lambda *args, **kwargs: kwargs)
exec(compile(ast.Module(body=slider_nodes, type_ignores=[]), "app-sliders", "exec"), self.scope)
payload = ImageInversionJobRequest(input_image_url="https://example.com/image.png")
self.assertEqual(self.scope["gen_slider"]["value"], 0.17)
self.assertEqual(self.scope["char_slider"]["value"], 0.27)
self.assertEqual(payload.general_threshold, 0.17)
self.assertEqual(payload.character_threshold, 0.27)
def test_upload_and_job_decode_preserve_transparency(self):
"""验证上传和远程解码均将透明背景留给官方处理器。"""
settings = ImageInversionJobSettings(api_key="x" * 32, allowed_hosts=frozenset({"example.com"}))
for mode in ("RGBA", "P"):
with self.subTest(mode=mode):
image = Image.new("RGBA", (32, 32), (10, 20, 30, 0))
if mode == "P":
image = image.convert("P")
buffer = io.BytesIO()
image.save(buffer, format="PNG")
decoded = decode_image_bytes(buffer.getvalue(), "image/png", settings)
self.assertEqual(decoded.convert("RGBA").getpixel((0, 0))[3], 0)
# 仅创建并显式删除当前测试的单个文件,不创建需递归清理的目录。
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as source:
source.write(buffer.getvalue())
source_path = Path(source.name).resolve()
try:
uploaded = self.scope["validate_and_open_image"](str(source_path))
self.assertEqual(uploaded.convert("RGBA").getpixel((0, 0))[3], 0)
finally:
source_path.unlink()
if __name__ == "__main__":
unittest.main()
|