"""③ upscale — Topaz Text Refine 2배. 공개 채널의 공연 포스터는 전부 750px대라 그대로는 못 쓴다. Bytedance Upscale은 광선을 재작화하고 글자에 없던 무늬를 그려서(MAE 13 vs 5) Text Refine만 쓴다. 목적은 확대보다 GIF 128색 밴딩 해소다 — 밴딩이 남으면 생성 모델이 그것을 무늬로 읽는다. """ import io import tempfile from contextlib import contextmanager from pathlib import Path import httpx from PIL import Image from models.upscale import UpscaleResult from settings import settings from utils.higgsfield import (TOPAZ_IMAGE, TOPAZ_TEXT_REFINE, assert_account, parse_result_url, run_cli) OUTPUT_WIDTH, OUTPUT_HEIGHT = 1500, 2000 UPSCALE_CREDITS = 2 CLI_TIMEOUT = 420 MIN_IMAGE_BYTES = 10_000 @contextmanager def as_png_path(image: bytes | Path): """CLI의 --image가 파일 경로만 받으므로 임시 파일로 내린다. NOL 포스터는 팔레트 GIF(모드 P)로 오는데 그대로 올리면 잡이 failed로 돌아온다. RGB PNG로 정규화해 보낸다. """ source = Image.open(image if isinstance(image, Path) else io.BytesIO(image)) with tempfile.NamedTemporaryFile(suffix=".png") as handle: source.convert("RGB").save(handle, "PNG") handle.flush() yield Path(handle.name) async def upscale_poster(poster: bytes | Path) -> UpscaleResult: await assert_account(settings.higgsfield_account) # 크레딧 쓰기 전에 with as_png_path(poster) as source: output = await run_cli( "generate", "create", TOPAZ_IMAGE, "--image", str(source), "--output_width", str(OUTPUT_WIDTH), "--output_height", str(OUTPUT_HEIGHT), "--variant", TOPAZ_TEXT_REFINE, "--wait", "--wait-timeout", "4m", "--json", timeout=CLI_TIMEOUT) async with httpx.AsyncClient(timeout=300, follow_redirects=True) as client: response = await client.get(parse_result_url(output)) response.raise_for_status() if len(response.content) < MIN_IMAGE_BYTES: raise RuntimeError(f"업스케일 결과가 너무 작다 ({len(response.content)}바이트)") return UpscaleResult(image=response.content, credits=UPSCALE_CREDITS, variant=TOPAZ_TEXT_REFINE)