Instructions to use kesha-humonen/runic-reader-2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use kesha-humonen/runic-reader-2 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download code/ground.py from kesha-humonen/runic-reader-2: direct link, hf CLI and curl.
- Browser
- Download file 3.7 kB
-
https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/ground.py
- Command line
-
hf download hf://kesha-humonen/runic-reader-2/code/ground.py
-
curl -L -o ground.py https://huggingface.co/kesha-humonen/runic-reader-2/resolve/main/code/ground.py
3.7 kB
| """Zero-shot localization of the runic inscription with Qwen3.5 (bounding box in 0-1000 coordinates). | |
| Writes one JSON line per image: {"image", "raw", "box"} where box is [x1, y1, x2, y2] in pixels of | |
| the stored image, or null when no box could be parsed. Resumable. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import torch | |
| sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) | |
| from qwen_common import clean_generation, load_model, stop_ids # noqa: E402 | |
| from PIL import Image | |
| PROMPT = ("Locate the runic inscription in this image: the area that contains the carved or painted runes, " | |
| "including all lines of runes. Output only its bounding box as JSON: " | |
| "{\"bbox_2d\": [x1, y1, x2, y2]} with coordinates between 0 and 1000.") | |
| def parse_box(text, w, h): | |
| m = re.search(r"\[([^\[\]]*)\]", text) # first coordinate list; the "2" in the key "bbox_2d" is not a coordinate | |
| nums = re.findall(r"-?\d+(?:\.\d+)?", m.group(1) if m else "") | |
| if len(nums) < 4: | |
| return None | |
| x1, y1, x2, y2 = [float(v) for v in nums[:4]] | |
| if max(x1, y1, x2, y2) <= 1.0: # relative 0-1 | |
| scale_x, scale_y = w, h | |
| else: | |
| scale_x, scale_y = w / 1000.0, h / 1000.0 | |
| x1, x2 = sorted((x1 * scale_x, x2 * scale_x)) | |
| y1, y2 = sorted((y1 * scale_y, y2 * scale_y)) | |
| x1, y1 = max(0, x1), max(0, y1) | |
| x2, y2 = min(w, x2), min(h, y2) | |
| if x2 - x1 < 0.05 * w or y2 - y1 < 0.05 * h: | |
| return None | |
| return [round(x1), round(y1), round(x2), round(y2)] | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--model", required=True) | |
| ap.add_argument("--inputs", required=True) | |
| ap.add_argument("--image_root", required=True) | |
| ap.add_argument("--out", required=True) | |
| ap.add_argument("--bs", type=int, default=16) | |
| args = ap.parse_args() | |
| images = [] | |
| for p in args.inputs.split(","): | |
| for l in open(p): | |
| im = json.loads(l)["image"] | |
| if im not in images: | |
| images.append(im) | |
| done = set() | |
| if os.path.exists(args.out): | |
| done = {json.loads(l)["image"] for l in open(args.out)} | |
| todo = [i for i in images if i not in done] | |
| print("images", len(images), "todo", len(todo), flush=True) | |
| processor, model = load_model(args.model) | |
| model.to("cuda").eval() | |
| tok = processor.tokenizer | |
| tok.padding_side = "left" | |
| msgs = [{"role": "user", "content": [{"type": "image"}, {"type": "text", "text": PROMPT}]}] | |
| prompt = processor.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True, enable_thinking=False) | |
| fout = open(args.out, "a") | |
| for b in range(0, len(todo), args.bs): | |
| chunk = todo[b:b + args.bs] | |
| ims = [] | |
| for name in chunk: | |
| im = Image.open(os.path.join(args.image_root, name)).convert("RGB") | |
| ims.append(im) | |
| enc = processor(text=[prompt] * len(ims), images=ims, return_tensors="pt", padding=True) | |
| enc = {k: v.to("cuda") for k, v in enc.items()} | |
| with torch.no_grad(), torch.autocast("cuda", dtype=torch.bfloat16): | |
| out = model.generate(**enc, max_new_tokens=40, do_sample=False, eos_token_id=stop_ids(tok), | |
| pad_token_id=tok.pad_token_id) | |
| gen = out[:, enc["input_ids"].shape[1]:] | |
| for name, im, g in zip(chunk, ims, gen): | |
| raw = clean_generation(tok.decode(g, skip_special_tokens=False)) | |
| fout.write(json.dumps({"image": name, "raw": raw, "box": parse_box(raw, *im.size)}) + "\n") | |
| fout.flush() | |
| print(f"{min(b + args.bs, len(todo))}/{len(todo)}", flush=True) | |
| print("DONE", flush=True) | |
| if __name__ == "__main__": | |
| main() | |