feat(thumbnails): thumbnail format, quality and size as server settings
Thumbnail format (webp default, jxl, jpeg), quality (1-100, default 80) and size limit (128-2048 px, default 512) are server settings in the config.json thumbnails section, saved with the settings revision. The never-read gallery thumbnail_size is gone: older saves wrote its unused default 256 into config.json. One module names previews (<stem>_thumb.webp/.jxl/.jpg) and finds the preview a source owns, preferring the configured format; legacy names are still found. The encoder module writes every preview atomically and verifies it decodes. JPEG XL needs no new dependency: Pillow (natively or with pillow_jxl), ffmpeg with libjxl, or cjxl; without one the settings refuse jxl (400 jxl-unavailable) and GET says why. Encoder processes are tracked, bounded and killed at shutdown.
This commit is contained in:
@@ -1328,6 +1328,103 @@ class TestSettingsMisc(AdminAPITestCase):
|
||||
self.assertEqual(resp.status, 500)
|
||||
|
||||
|
||||
class TestThumbnailSettings(AdminAPITestCase):
|
||||
"""#317: thumbnail format, quality and size limit are server settings."""
|
||||
|
||||
URL = "/prompt_manager/settings"
|
||||
|
||||
def _encoder(self, name):
|
||||
from py.api import thumbnail_encode
|
||||
|
||||
return patch.object(thumbnail_encode, "detect_jxl_encoder", return_value=name)
|
||||
|
||||
async def test_get_reports_the_settings_and_the_encoders(self):
|
||||
PromptManagerConfig.update_config(
|
||||
{"thumbnails": {"format": "webp", "quality": 80, "edge": 512}}
|
||||
)
|
||||
with self._encoder(None):
|
||||
body = await (await self.client.request("GET", self.URL)).json()
|
||||
settings = body["settings"]
|
||||
self.assertEqual(
|
||||
(
|
||||
settings["thumbnail_format"],
|
||||
settings["thumbnail_quality"],
|
||||
settings["thumbnail_edge"],
|
||||
),
|
||||
("webp", 80, 512),
|
||||
)
|
||||
jxl = body["thumbnail_encoders"]["jxl"]
|
||||
self.assertEqual((jxl["available"], jxl["encoder"]), (False, None))
|
||||
self.assertIn("pillow-jxl-plugin", jxl["reason"])
|
||||
with self._encoder("cjxl"):
|
||||
body = await (await self.client.request("GET", self.URL)).json()
|
||||
self.assertEqual(
|
||||
body["thumbnail_encoders"]["jxl"],
|
||||
{"available": True, "encoder": "cjxl", "reason": None},
|
||||
)
|
||||
|
||||
async def test_save_commits_the_thumbnails_section(self):
|
||||
self._write_config({"custom": {"keep": 1}})
|
||||
resp = await self.client.request(
|
||||
"POST",
|
||||
self.URL,
|
||||
json={
|
||||
"thumbnail_format": "jpeg",
|
||||
"thumbnail_quality": 60,
|
||||
"thumbnail_edge": 300,
|
||||
},
|
||||
)
|
||||
self.assertEqual(resp.status, 200)
|
||||
saved = self._read_config()
|
||||
self.assertEqual(
|
||||
saved["thumbnails"], {"format": "jpeg", "quality": 60, "edge": 300}
|
||||
)
|
||||
self.assertEqual(saved["custom"], {"keep": 1})
|
||||
self.assertEqual(
|
||||
(
|
||||
PromptManagerConfig.THUMBNAIL_FORMAT,
|
||||
PromptManagerConfig.THUMBNAIL_QUALITY,
|
||||
PromptManagerConfig.THUMBNAIL_EDGE,
|
||||
),
|
||||
("jpeg", 60, 300),
|
||||
)
|
||||
|
||||
async def test_bad_values_are_refused_and_change_nothing(self):
|
||||
before = PromptManagerConfig.get_config()["thumbnails"]
|
||||
for payload in (
|
||||
{"thumbnail_format": "png"},
|
||||
{"thumbnail_quality": 0},
|
||||
{"thumbnail_quality": "80"},
|
||||
{"thumbnail_edge": 4096},
|
||||
{"thumbnail_edge": True},
|
||||
):
|
||||
resp = await self.client.request("POST", self.URL, json=payload)
|
||||
self.assertEqual(resp.status, 400, payload)
|
||||
self.assertEqual(PromptManagerConfig.get_config()["thumbnails"], before)
|
||||
self.assertFalse(os.path.exists(self.config_path))
|
||||
|
||||
async def test_jpeg_xl_without_an_encoder_is_refused_with_a_code(self):
|
||||
with self._encoder(None):
|
||||
resp = await self.client.request(
|
||||
"POST", self.URL, json={"thumbnail_format": "jxl", "result_timeout": 7}
|
||||
)
|
||||
body = await resp.json()
|
||||
self.assertEqual((resp.status, body["code"]), (400, "jxl-unavailable"))
|
||||
self.assertIn("pillow-jxl-plugin", body["error"])
|
||||
self.assertNotEqual(PromptManagerConfig.THUMBNAIL_FORMAT, "jxl")
|
||||
self.assertNotEqual(PromptManagerConfig.RESULT_TIMEOUT, 7)
|
||||
self.assertFalse(os.path.exists(self.config_path))
|
||||
|
||||
async def test_jpeg_xl_with_an_encoder_is_saved(self):
|
||||
with self._encoder("ffmpeg"):
|
||||
resp = await self.client.request(
|
||||
"POST", self.URL, json={"thumbnail_format": "jxl"}
|
||||
)
|
||||
self.assertEqual(resp.status, 200)
|
||||
self.assertEqual(self._read_config()["thumbnails"]["format"], "jxl")
|
||||
self.assertEqual(PromptManagerConfig.THUMBNAIL_FORMAT, "jxl")
|
||||
|
||||
|
||||
class TestDiagnostics(AdminAPITestCase):
|
||||
|
||||
def _install_monitor(self, monitor):
|
||||
|
||||
Reference in New Issue
Block a user