Puker_Judge / infer_rank.py
TuWaveGod's picture
Publish two-stage final LoRA adapters and inference guide
c35c7ce verified
Raw
History Blame Contribute Delete
6.36 kB
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import re
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from puker_judge_utils import (
BASE_MODEL_ID,
MODEL_REPO_ID,
encode_image_prompt,
generate_answer,
load_adapter,
print_json,
rank_prompt,
)
BOARD_SIZE = (1280, 820)
CANDIDATE_SIZE = (600, 360)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Choose the best of two to four playing-card candidates."
)
parser.add_argument(
"candidates",
type=Path,
nargs="*",
help="Two to four rectified candidate images.",
)
parser.add_argument(
"--board-image",
type=Path,
help="Use an already constructed board instead of candidate files.",
)
parser.add_argument(
"--candidate-count",
type=int,
help="Required with --board-image; must be between 2 and 4.",
)
parser.add_argument(
"--board-output",
type=Path,
default=Path("candidate_board.jpg"),
)
parser.add_argument("--repo-id", default=MODEL_REPO_ID)
parser.add_argument("--base-model-id", default=BASE_MODEL_ID)
parser.add_argument(
"--int4",
action="store_true",
help="Use bitsandbytes NF4 weights with BF16 compute.",
)
return parser.parse_args()
def load_font(size: int) -> ImageFont.ImageFont:
for path in (
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
"DejaVuSans-Bold.ttf",
):
try:
return ImageFont.truetype(path, size=size)
except OSError:
continue
return ImageFont.load_default()
def build_board(candidate_paths: list[Path]) -> Image.Image:
count = len(candidate_paths)
if not 2 <= count <= 4:
raise ValueError("Provide exactly 2, 3, or 4 candidate images.")
board = Image.new("RGB", BOARD_SIZE, (226, 231, 237))
draw = ImageDraw.Draw(board)
font = load_font(34)
outer_margin = 28
column_gap = 24
row_gap = 24
cell_width = (BOARD_SIZE[0] - 2 * outer_margin - column_gap) // 2
cell_height = (BOARD_SIZE[1] - 2 * outer_margin - row_gap) // 2
label_height = 46
for index, path in enumerate(candidate_paths):
column = index % 2
row = index // 2
x0 = outer_margin + column * (cell_width + column_gap)
y0 = outer_margin + row * (cell_height + row_gap)
x1 = x0 + cell_width
y1 = y0 + cell_height
draw.rounded_rectangle(
(x0, y0, x1, y1),
radius=14,
fill=(244, 246, 248),
outline=(178, 184, 192),
width=2,
)
draw.rounded_rectangle(
(x0 + 12, y0 + 8, x0 + 76, y0 + label_height),
radius=10,
fill=(255, 218, 72),
outline=(45, 48, 52),
width=2,
)
draw.text(
(x0 + 44, y0 + 8 + label_height // 2),
str(index + 1),
font=font,
fill=(25, 31, 42),
anchor="mm",
)
with Image.open(path) as source:
candidate = source.convert("RGB").resize(
CANDIDATE_SIZE,
Image.Resampling.LANCZOS,
)
available_width = cell_width - 36
available_height = cell_height - label_height - 28
candidate.thumbnail(
(available_width, available_height),
Image.Resampling.LANCZOS,
)
paste_x = x0 + (cell_width - candidate.width) // 2
paste_y = y0 + label_height + (
cell_height - label_height - candidate.height
) // 2
draw.rectangle(
(
paste_x + 4,
paste_y + 5,
paste_x + 4 + candidate.width,
paste_y + 5 + candidate.height,
),
fill=(185, 190, 196),
)
board.paste(candidate, (paste_x, paste_y))
return board
def main() -> None:
args = parse_args()
if args.board_image is not None:
if args.candidates:
raise SystemExit(
"Use either candidate files or --board-image, not both."
)
if args.candidate_count is None or not 2 <= args.candidate_count <= 4:
raise SystemExit(
"--candidate-count 2..4 is required with --board-image."
)
with Image.open(args.board_image) as source:
board = source.convert("RGB").copy()
candidate_count = args.candidate_count
candidate_paths: list[Path] = []
else:
if args.candidate_count is not None:
raise SystemExit(
"--candidate-count is inferred when candidate files are used."
)
candidate_paths = args.candidates
candidate_count = len(candidate_paths)
board = build_board(candidate_paths)
args.board_output.parent.mkdir(parents=True, exist_ok=True)
board.save(args.board_output, "JPEG", quality=96, subsampling=0)
model, processor, device, timings = load_adapter(
"rank_adapter",
repo_id=args.repo_id,
base_model_id=args.base_model_id,
int4=args.int4,
)
inputs = encode_image_prompt(
processor,
board,
rank_prompt(candidate_count),
device,
)
raw_output, generation_seconds = generate_answer(
model,
processor,
inputs,
)
match = re.search(r"[1-4]", raw_output)
if match is None or int(match.group(0)) > candidate_count:
raise SystemExit(f"Model returned an invalid answer: {raw_output!r}")
label = int(match.group(0))
payload = {
"selected_label": label,
"raw_output": raw_output,
"candidate_count": candidate_count,
"quantization": "int4-nf4" if args.int4 else "bf16",
"generation_seconds": round(generation_seconds, 4),
**{key: round(value, 4) for key, value in timings.items()},
}
if candidate_paths:
payload["selected_file"] = str(candidate_paths[label - 1].resolve())
payload["board_image"] = str(args.board_output.resolve())
else:
payload["board_image"] = str(args.board_image.resolve())
print_json(payload)
if __name__ == "__main__":
main()