#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
offline_bg.config — 离线背景预处理的全部超参集中一处。

每个数字后面都注明来源：【实测】= 八轮 RTX5090 基准实测出来的，改之前先看 CLAUDE.md；
【默认】= 工程默认值，可调；【未标定】= 还没有实测依据，默认关闭。
"""
from dataclasses import dataclass, field
from typing import Optional, Tuple


@dataclass
class OfflineBGConfig:
    # ========== ② IC-Light 重打光 ==========
    sd_model: str = "stablediffusionapi/realistic-vision-v51"
    iclight_repo: str = "lllyasviel/ic-light"
    iclight_file: str = "iclight_sd15_fc.safetensors"

    # 🔴 工作分辨率必须是 64 的倍数（SD1.5 UNet 三次下采样，非 64 倍数会在
    #    上采样处尺寸对不齐）。768×448 是 v8 实测通过的尺寸，1.0–1.7s/张。
    #    背景先缩到这里重打光，再放大回目标分辨率——因为最终还要按摄像头画质
    #    降级（摄像头比 AI 背景软 16.6 倍），高分辨率细节本来就要被抹掉。
    work_size: Tuple[int, int] = (768, 448)          # 【实测】

    steps: int = 25                                   # 【实测】v8 用值
    strength: float = 0.92                            # 【实测】v8 用值
    cfg_scale: float = 2.0                            # 【实测】v8 用值
    negative_prompt: str = "lowres, bad quality, distorted"
    # 🔴 prompt 对光照方向无效（实测 +20.90 vs +20.51），方向完全靠渐变图注入。
    #    这里的 prompt 只负责画面风格/内容语义，别指望它控方向。
    prompt_tpl: str = "sunlight from the {side}, warm indoor scene"

    # ③ 重试时逐级抬 strength：img2img 起点噪声越多，原图自带的光照越压得住。
    retry_strengths: Tuple[float, ...] = (0.92, 0.95, 0.98)   # 【默认】
    max_attempts: int = 3                                      # 【默认】
    base_seed: int = 12345

    # ========== ③ 方向闭环校验 ==========
    # 🔴 判据必须是「相对原图的增量」而不是绝对亮度差：v8 的原背景本身
    #    lr_diff=+45.05（提示词写了窗户在左），若重打光失败退化成原图，
    #    绝对判据会把它误判成"左光成功"。
    # 实测增量：left = 126.07-45.05 = +81.0 ；right = -120.99-45.05 = -166.0
    # 取 30 ≈ 较小裕度 81 的 37%，既有余量又不会放水。
    min_delta: float = 30.0                            # 【实测标定】
    # 除增量外，还要求输出图的绝对符号也对（左光 → 左半确实更亮）
    require_absolute_sign: bool = True                 # 【默认】

    # ========== ① 摄像头光照画像 ==========
    light_thresh: float = 0.35        # 【实测标定】front 0.22 / top 0.73 / 左右 1.3 的间隙
    profile_frames: int = 9           # 【默认】取几帧做画像（>1 才能估噪声）
    min_face_frames: int = 1          # 【默认】至少几帧检到人脸才认方向
    # front（无明显方向光）怎么办：
    #   skip   = 不重打光，只做画质匹配（保守，只走已验证路径）—— 默认
    #   radial = 用中心亮四周暗的径向图重打光成均匀布光（未实测，慎用）
    front_strategy: str = "skip"      # 【默认】

    # ========== ④ 画质降级匹配 ==========
    # 🔴 必须在放大到目标分辨率之后做：模糊 σ 是分辨率相关量，
    #    在 768×448 上解出的 σ 拿到 1080p 上是错的。
    # 噪声估计器的"0.699 倍系统偏差"已在 2026-08-11 定位并修掉：真根因是
    # 「灰度域测量 vs 逐通道注入」域不一致（0.669 = 三通道→灰度的 std 衰减），
    # 不是估计器有偏。改成逐通道估计后 8 点标定平均 1.015 倍（见 calib_noise.py），
    # 所以这里保持 1.0，**不要**再去固化任何校正系数。
    noise_gain: float = 1.0                            # 【实测标定·勿改】
    noise_min_sigma: float = 0.2      # 【实测】低于此值不加噪（压缩视频素材 σ≈0）
    # 注入噪声最多可占摄像头背景 Laplacian 方差的多少。摄像头噪声被编码器空间低通过，
    # 直接按时域 σ 注白噪声会高频过量（实测占满甚至超过 100% → 目标不可达）。
    noise_lap_budget: float = 0.35    # 【实测标定】
    # 锐度度量：textured = 按块取高分位（只比"有纹理处"，不被平坦区稀释）；
    # laplacian = 全图方差（旧口径，与画面内容量强耦合，平坦背景会把目标压到接近 0）
    sharp_metric: str = "textured"    # 【实测标定】
    sharp_block: int = 64
    sharp_pct: float = 80.0
    # 🔴 锐度基准用**整帧**（含人物），色彩统计才用掩膜排除人物——两者目的不同：
    #    色彩要避开肤色污染；锐度要的是"这台相机能把细节拍多实"，而摄像头背景常常是
    #    一面无纹理的墙，根本不携带这个信息。实测影棚素材：整帧 10.4 / 仅背景区 4.3，
    #    换纹理块口径后仅背景区更低到 1.05 → 目标被压到不可达，成品糊成一团雾。
    #    （v7 那次被接受的画质匹配用的就是整帧口径：摄像头 16.2 vs AI 背景 269。）
    sharp_use_bg_mask: bool = False   # 【实测标定·勿改】
    sharp_err_warn: float = 0.15      # 【默认】锐度误差超过 15% 在记录里告警

    # ========== ⑤ 背景库 ==========
    library_root: str = "bg_library"
    # 落库时同时保留中间产物，便于事后归因（磁盘便宜，返工贵）
    keep_intermediate: bool = True

    # ========== 其它 ==========
    device: str = "cuda"
    # 目标输出尺寸；None = 跟随摄像头帧尺寸（推荐，实时合成不用再缩放）
    target_size: Optional[Tuple[int, int]] = None

    def prompt_for(self, side: str, extra: str = "") -> str:
        base = self.prompt_tpl.format(side=side)
        return f"{base}, {extra}".strip().strip(",") if extra else base


DEFAULT = OfflineBGConfig()
