Text Classification
Transformers
Safetensors
Thai
English
openthai_systemone
feature-extraction
system-one
decision-model
thai
qwen3.5
quantized
compressed-tensors
llm-compressor
custom_code
Instructions to use iapp/OpenThai-SystemOne-FP8-Dynamic with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use iapp/OpenThai-SystemOne-FP8-Dynamic with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="iapp/OpenThai-SystemOne-FP8-Dynamic", trust_remote_code=True)# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("iapp/OpenThai-SystemOne-FP8-Dynamic", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download images.py from iapp/OpenThai-SystemOne-FP8-Dynamic: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/iapp/OpenThai-SystemOne-FP8-Dynamic/resolve/main/images.py
- Command line
-
hf download hf://iapp/OpenThai-SystemOne-FP8-Dynamic/images.py
-
curl -L -o images.py https://huggingface.co/iapp/OpenThai-SystemOne-FP8-Dynamic/resolve/main/images.py
10.4 kB
| """Image inputs for the vision variant: loading, resizing to the ViT token budget, and the patch-grid geometry | |
| that the `point` head works in. | |
| Geometry: the vision tower emits one token per (patch_size * spatial_merge_size)^2 pixel block (32x32 px for | |
| Qwen3.5), row-major over the merged grid (hm, wm) = (h / merge, w / merge) where (t, h, w) = image_grid_thw. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import io | |
| from dataclasses import dataclass, field | |
| from typing import Any, Dict, List, Optional, Sequence, Tuple, Union | |
| DEFAULT_MAX_PIXELS = 1280 * 800 # ~1000 visual tokens at 32 px/token | |
| HIGH_DETAIL_MAX_PIXELS = 1920 * 1200 # ~2280 tokens | |
| DEFAULT_MIN_PIXELS = 64 * 64 | |
| MAX_IMAGES_PER_REQUEST = 4 | |
| MAX_VISUAL_TOKENS = 8192 | |
| def load_image(src: Any): | |
| """PIL image from: PIL.Image, bytes, data-URI / base64 string, http(s) URL, or a filesystem path.""" | |
| from PIL import Image | |
| if isinstance(src, Image.Image): | |
| return src.convert("RGB") | |
| if isinstance(src, (bytes, bytearray)): | |
| return Image.open(io.BytesIO(src)).convert("RGB") | |
| if hasattr(src, "shape"): # numpy array (H, W, 3) | |
| return Image.fromarray(src).convert("RGB") | |
| if not isinstance(src, str): | |
| raise TypeError(f"unsupported image source {type(src)}") | |
| if src.startswith("data:"): | |
| _, _, payload = src.partition(",") | |
| return Image.open(io.BytesIO(base64.b64decode(payload))).convert("RGB") | |
| if src.startswith(("http://", "https://")): | |
| import urllib.request | |
| req = urllib.request.Request(src, headers={"User-Agent": "openthai-systemone/1.0"}) | |
| with urllib.request.urlopen(req, timeout=20) as r: | |
| return Image.open(io.BytesIO(r.read())).convert("RGB") | |
| if len(src) > 512 and "/" not in src[:16] and not src.startswith("."): | |
| try: # bare base64 | |
| return Image.open(io.BytesIO(base64.b64decode(src, validate=True))).convert("RGB") | |
| except Exception: | |
| pass | |
| return Image.open(src).convert("RGB") | |
| class ImageRef: | |
| """One image attached to a request.""" | |
| id: str | |
| image: Any # anything load_image accepts | |
| detail: str = "auto" # auto | high | |
| role: Optional[str] = None # free text tag, shown in the sequence (screenshot, frame, photo, document) | |
| def parse(cls, obj: Union["ImageRef", Dict[str, Any]]) -> "ImageRef": | |
| if isinstance(obj, ImageRef): | |
| return obj | |
| d = dict(obj) | |
| src = d.get("image", d.get("data", d.get("url", d.get("path")))) | |
| if src is None: | |
| raise ValueError("image needs one of: image / data / url / path") | |
| return cls(id=str(d.get("id", "img")), image=src, detail=str(d.get("detail", "auto")), role=d.get("role")) | |
| class ProcessedImages: | |
| ids: List[str] | |
| pixel_values: Any # torch.Tensor (sum_patches, C*T*P*P) | |
| grid_thw: List[List[int]] # per image (t, h, w) in patches | |
| sizes: List[Tuple[int, int]] # original (W, H) | |
| n_tokens: List[int] = field(default_factory=list) | |
| merge: int = 2 | |
| def tokens_for(self, i: int) -> int: | |
| t, h, w = self.grid_thw[i] | |
| return t * h * w // (self.merge * self.merge) | |
| def merged_grid(self, i: int) -> Tuple[int, int]: | |
| t, h, w = self.grid_thw[i] | |
| return h // self.merge, w // self.merge | |
| def fast_patchify(im, image_processor, *, min_pixels: int, max_pixels: int, device=None): | |
| """Qwen2-VL preprocessing without the HF processor's overhead (~3 ms vs ~40 ms per frame on a busy CPU). | |
| Same maths: smart_resize to multiples of patch*merge within [min_pixels, max_pixels], bicubic resize, rescale, | |
| normalise with the processor's mean/std, duplicate the frame temporal_patch_size times, and flatten patches in the | |
| processor's (t, h/merge, w/merge, merge, merge, C, T, p, p) order. tests/test_vision.py checks equality with the HF | |
| processor output. | |
| """ | |
| import numpy as np | |
| import torch | |
| from PIL import Image | |
| from transformers.models.qwen2_vl.image_processing_qwen2_vl import smart_resize | |
| patch = int(getattr(image_processor, "patch_size", 16)) | |
| merge = int(getattr(image_processor, "merge_size", 2)) | |
| tp = int(getattr(image_processor, "temporal_patch_size", 2)) | |
| mean = torch.tensor(image_processor.image_mean, dtype=torch.float32).view(3, 1, 1) | |
| std = torch.tensor(image_processor.image_std, dtype=torch.float32).view(3, 1, 1) | |
| w, h = im.size | |
| rh, rw = smart_resize(h, w, factor=patch * merge, min_pixels=min_pixels, max_pixels=max_pixels) | |
| if (rw, rh) != (w, h): | |
| im = im.resize((rw, rh), Image.BICUBIC) | |
| x = torch.from_numpy(np.asarray(im, dtype=np.uint8)) | |
| if device is not None: # normalise + patchify on the GPU: the CPU only resizes and uploads 3 bytes/pixel | |
| x = x.to(device, non_blocking=True) | |
| mean, std = mean.to(device), std.to(device) | |
| x = x.permute(2, 0, 1).float() / 255.0 # (C, H, W) | |
| x = (x - mean) / std | |
| gh, gw = rh // patch, rw // patch | |
| x = x.unsqueeze(0).expand(tp, -1, -1, -1) # (T, C, H, W) | |
| x = x.reshape(tp, 3, gh // merge, merge, patch, gw // merge, merge, patch) | |
| # HF order: (grid_t, temporal, C, gh//m, m, p, gw//m, m, p) -> transpose(0, 3, 6, 4, 7, 2, 1, 5, 8) | |
| x = x.unsqueeze(0) # (1, T, C, gh/m, m, p, gw/m, m, p) | |
| x = x.permute(0, 3, 6, 4, 7, 2, 1, 5, 8).reshape(gh * gw, 3 * tp * patch * patch) | |
| return x, [1, gh, gw] | |
| def process_images(image_processor, refs: Sequence[ImageRef], *, max_pixels: int = DEFAULT_MAX_PIXELS, | |
| min_pixels: int = DEFAULT_MIN_PIXELS, fast: bool = True, device=None) -> ProcessedImages: | |
| """Preprocess each image (own detail cap each) and concatenate. fast=True uses fast_patchify (identical output).""" | |
| import torch | |
| if len(refs) > MAX_IMAGES_PER_REQUEST: | |
| raise ValueError(f"at most {MAX_IMAGES_PER_REQUEST} images per request") | |
| merge = int(getattr(image_processor, "merge_size", 2)) | |
| pv, grids, sizes, ids, ntoks = [], [], [], [], [] | |
| for r in refs: | |
| im = load_image(r.image) | |
| sizes.append(im.size) | |
| cap = HIGH_DETAIL_MAX_PIXELS if r.detail == "high" else max_pixels | |
| if fast: | |
| x, g = fast_patchify(im, image_processor, min_pixels=min_pixels, max_pixels=cap, device=device) | |
| pv.append(x) | |
| else: | |
| out = image_processor(images=[im], size={"shortest_edge": min_pixels, "longest_edge": cap}, return_tensors="pt") | |
| pv.append(out["pixel_values"]) | |
| g = [int(v) for v in out["image_grid_thw"][0].tolist()] | |
| grids.append(g) | |
| ids.append(r.id) | |
| ntoks.append(g[0] * g[1] * g[2] // (merge * merge)) | |
| if sum(ntoks) > MAX_VISUAL_TOKENS: | |
| raise ValueError(f"images need {sum(ntoks)} visual tokens > {MAX_VISUAL_TOKENS}; lower detail or send fewer images") | |
| return ProcessedImages(ids=ids, pixel_values=torch.cat(pv, 0), grid_thw=grids, sizes=sizes, n_tokens=ntoks, merge=merge) | |
| def bbox_to_token_target(bbox: Optional[Sequence[float]], hm: int, wm: int) -> "torch.Tensor": | |
| """Soft target over the hm*wm tokens (+1 null slot at the end) for a normalised bbox [x0,y0,x1,y1] in 0..1. | |
| Cells whose area overlaps the box get weight = overlap fraction of the cell (GUI-Actor style multi-patch | |
| supervision); normalised to sum 1. bbox None -> all mass on the null slot ("not on screen"). | |
| """ | |
| import torch | |
| tgt = torch.zeros(hm * wm + 1) | |
| if bbox is None: | |
| tgt[-1] = 1.0 | |
| return tgt | |
| x0, y0, x1, y1 = [float(v) for v in bbox] | |
| x0, x1 = sorted((max(0.0, min(1.0, x0)), max(0.0, min(1.0, x1)))) | |
| y0, y1 = sorted((max(0.0, min(1.0, y0)), max(0.0, min(1.0, y1)))) | |
| # a degenerate (point) box marks the cell that contains it | |
| if x1 - x0 < 1e-6: | |
| x0, x1 = max(0.0, x0 - 0.5 / wm), min(1.0, x0 + 0.5 / wm) | |
| if y1 - y0 < 1e-6: | |
| y0, y1 = max(0.0, y0 - 0.5 / hm), min(1.0, y0 + 0.5 / hm) | |
| for r in range(hm): | |
| cy0, cy1 = r / hm, (r + 1) / hm | |
| oy = max(0.0, min(cy1, y1) - max(cy0, y0)) | |
| if oy <= 0: | |
| continue | |
| for c in range(wm): | |
| cx0, cx1 = c / wm, (c + 1) / wm | |
| ox = max(0.0, min(cx1, x1) - max(cx0, x0)) | |
| if ox > 0: | |
| tgt[r * wm + c] = ox * oy * hm * wm # fraction of the cell covered | |
| if tgt[:-1].sum() <= 0: | |
| tgt[-1] = 1.0 | |
| else: | |
| tgt = tgt / tgt.sum() | |
| return tgt | |
| def token_xy(index: int, hm: int, wm: int) -> Tuple[float, float]: | |
| """Centre of merged token `index` in normalised image coordinates.""" | |
| r, c = divmod(index, wm) | |
| return (c + 0.5) / wm, (r + 0.5) / hm | |
| def heatmap_to_candidates(probs: Sequence[float], hm: int, wm: int, *, rel_threshold: float = 0.3, max_candidates: int = 5): | |
| """GUI-Actor decoding: threshold at rel_threshold * max, 8-connected components, mass-weighted centroids. | |
| probs: hm*wm token probabilities (null slot already removed). Returns list of {x, y, p, bbox} sorted by p. | |
| """ | |
| n = hm * wm | |
| p = [float(v) for v in probs[:n]] | |
| if n == 0 or max(p) <= 0: | |
| return [] | |
| thr = rel_threshold * max(p) | |
| keep = {i for i, v in enumerate(p) if v >= thr} | |
| seen, comps = set(), [] | |
| for start in sorted(keep, key=lambda i: -p[i]): | |
| if start in seen: | |
| continue | |
| comp, stack = [], [start] | |
| seen.add(start) | |
| while stack: | |
| i = stack.pop() | |
| comp.append(i) | |
| r, c = divmod(i, wm) | |
| for dr in (-1, 0, 1): | |
| for dc in (-1, 0, 1): | |
| rr, cc = r + dr, c + dc | |
| if 0 <= rr < hm and 0 <= cc < wm: | |
| j = rr * wm + cc | |
| if j in keep and j not in seen: | |
| seen.add(j) | |
| stack.append(j) | |
| mass = sum(p[i] for i in comp) | |
| xs = [token_xy(i, hm, wm) for i in comp] | |
| x = sum(px * p[i] for (px, _), i in zip(xs, comp)) / mass | |
| y = sum(py * p[i] for (_, py), i in zip(xs, comp)) / mass | |
| rows = [i // wm for i in comp] | |
| cols = [i % wm for i in comp] | |
| bbox = [min(cols) / wm, min(rows) / hm, (max(cols) + 1) / wm, (max(rows) + 1) / hm] | |
| comps.append({"x": round(x, 4), "y": round(y, 4), "p": round(mass, 4), "bbox": [round(v, 4) for v in bbox]}) | |
| comps.sort(key=lambda d: -d["p"]) | |
| return comps[:max_candidates] | |