"""C-04 Provider 抽象层:本地 Pillow 实现 + HTTP 预留。""" from __future__ import annotations import io import os from PIL import Image class BaseProvider: name = 'base' def upscale(self, raw: bytes, scale: int) -> bytes: # pragma: no cover raise NotImplementedError class PillowProvider(BaseProvider): """本地放大:LANCZOS 重采样。零 key、零外部调用,mock 链路即生产链路。""" name = 'pillow' MAX_INPUT_BYTES = 10 * 1024 * 1024 MAX_DIM = 4096 def upscale(self, raw: bytes, scale: int) -> bytes: if scale not in (2, 4): raise ValueError('仅支持 2x / 4x') if len(raw) > self.MAX_INPUT_BYTES: raise ValueError('图片过大(>10MB)') img = Image.open(io.BytesIO(raw)) img.load() if img.width > self.MAX_DIM or img.height > self.MAX_DIM: raise ValueError('图片尺寸过大') out = img.resize((img.width * scale, img.height * scale), Image.Resampling.LANCZOS) buf = io.BytesIO() fmt = (img.format or 'PNG').upper() if fmt in ('JPEG', 'JPG'): if out.mode in ('RGBA', 'P'): out = out.convert('RGB') out.save(buf, 'JPEG', quality=95, optimize=True) return buf.getvalue() out.save(buf, 'PNG', optimize=True) return buf.getvalue() class HTTPProvider(BaseProvider): """预留第三方网关:baseURL/key 全走环境变量。未配置时直接抛错(由视图转 503)。""" name = 'http' def __init__(self) -> None: self.base = os.environ.get('AITOOL_API_BASE', '') self.key = os.environ.get('AITOOL_API_KEY', '') def upscale(self, raw: bytes, scale: int) -> bytes: # pragma: no cover import urllib.request if not self.base or not self.key: raise RuntimeError('AITOOL_API_BASE / AITOOL_API_KEY 未配置') req = urllib.request.Request( f'{self.base.rstrip("/")}/upscale', data=raw, headers={'Authorization': f'Bearer {self.key}', 'X-Scale': str(scale)}, method='POST', ) with urllib.request.urlopen(req, timeout=30) as resp: return resp.read() def get_provider() -> BaseProvider: which = os.environ.get('AITOOL_PROVIDER', 'pillow').lower() if which == 'http': return HTTPProvider() return PillowProvider()