Image Classification
Transformers
Safetensors
qwen3_5
feature-extraction
clef
bitsandbytes
quantized
4-bit precision
Instructions to use Aikimi/clef-nf4 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Aikimi/clef-nf4 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-classification", model="Aikimi/clef-nf4") pipe("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/hub/parrots.png")# pip install -U transformers accelerate # Load model directly from transformers import AutoProcessor, AutoModel processor = AutoProcessor.from_pretrained("Aikimi/clef-nf4") model = AutoModel.from_pretrained("Aikimi/clef-nf4", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download clef_runtime/core.py from Aikimi/clef-nf4: direct link, hf CLI and curl.
- Browser
- Download file 16 kB
-
https://huggingface.co/Aikimi/clef-nf4/resolve/main/clef_runtime/core.py
- Command line
-
hf download hf://Aikimi/clef-nf4/clef_runtime/core.py
-
curl -L -o core.py https://huggingface.co/Aikimi/clef-nf4/resolve/main/clef_runtime/core.py
16 kB
| """Small, torch-free contracts for images, schemas and immutable decision runs.""" | |
| from __future__ import annotations | |
| import copy | |
| import csv | |
| import hashlib | |
| import io | |
| import json | |
| import math | |
| import os | |
| from collections import defaultdict, deque | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[2] | |
| RUNTIME = ROOT / "models" / "Clef" | |
| OUTPUTS = ROOT / "outputs" / "clef" | |
| PREPROCESSING_VERSION = "exif-rgb-size-v2" | |
| MODELS = { | |
| "clef-flash": {"repo": "Cloudflare/clef-flash", "revision": "17f0b0ad64efb65d273590632833508766b2aae6"}, | |
| "clef": {"repo": "Cloudflare/clef", "revision": "2f3de3dd85f379784083b0814d997ab627200f0c"}, | |
| } | |
| PROFILES = { | |
| "flash-int8": { | |
| "label": "Flash INT8 · 軽量", | |
| "model": "clef-flash", | |
| "precision": "int8", | |
| "cpu_embeddings": False, | |
| "min_vram_gib": 12, | |
| }, | |
| "clef-24gb": { | |
| "label": "Clef NF4 · 24GB", | |
| "model": "clef", | |
| "precision": "nf4", | |
| "cpu_embeddings": False, | |
| "min_vram_gib": 20, | |
| }, | |
| "clef-16gb": { | |
| "label": "Clef NF4 · 16GB", | |
| "model": "clef", | |
| "precision": "nf4", | |
| "cpu_embeddings": True, | |
| "min_vram_gib": 14, | |
| }, | |
| "flash-bf16": { | |
| "label": "Flash BF16 · 比較用", | |
| "model": "clef-flash", | |
| "precision": "bf16", | |
| "cpu_embeddings": False, | |
| "min_vram_gib": 20, | |
| }, | |
| } | |
| TEMPLATES = { | |
| "顔・手の見直し(参考)": { | |
| "style": { | |
| "type": "choice", | |
| "instructions": "人物の主な写り方を分類してください。", | |
| "criteria": { | |
| "closeup": "顔のアップ", | |
| "upper": "上半身中心", | |
| "full": "全身または足まで見える", | |
| "other": "人物なし・その他", | |
| }, | |
| }, | |
| "face": {"type": "noul", "instructions": "人物の顔に、目や口の数・位置の異常や崩れがある。"}, | |
| "hands": {"type": "noul", "instructions": "見えている手や指に、本数や形の異常がある。"}, | |
| "marks": {"type": "noul", "instructions": "画像に文字、ロゴ、署名、透かしのいずれかがある。"}, | |
| "background": { | |
| "type": "choice", | |
| "instructions": "背景を分類してください。", | |
| "criteria": { | |
| "simple": "単色・単純な背景", | |
| "indoor": "屋内の背景", | |
| "outdoor": "屋外の背景", | |
| "other": "その他", | |
| }, | |
| }, | |
| }, | |
| "画像の評価": { | |
| "style": { | |
| "type": "choice", | |
| "instructions": "画像の主な表現形式を分類してください。", | |
| "criteria": { | |
| "photo": "写真または写実的な画像", | |
| "illustration": "イラスト・アニメ・絵画", | |
| "design": "図・ポスター・グラフィックデザイン", | |
| }, | |
| }, | |
| "quality": { | |
| "type": "score", | |
| "instructions": "画像の見た目の完成度を評価してください。", | |
| "criteria": [ | |
| "大きな破綻や欠落がある", | |
| "目立つ問題があり修正が必要", | |
| "おおむね整っている", | |
| "細部まで整い完成度が高い", | |
| ], | |
| }, | |
| "text": {"type": "noul", "instructions": "画像に読める文字が含まれている。"}, | |
| }, | |
| "生成画像の確認": { | |
| "artifact": {"type": "noul", "instructions": "画像に不自然な形、重複、形状の破綻が見られる。"}, | |
| "composition": { | |
| "type": "score", | |
| "instructions": "構図の明瞭さと主題の伝わりやすさを評価してください。", | |
| "criteria": ["主題が不明瞭", "主題は分かるが構図に問題がある", "主題が明瞭で構図が整っている"], | |
| }, | |
| }, | |
| "文章・JSONの分類": { | |
| "department": { | |
| "type": "choice", | |
| "instructions": "問い合わせを担当するチームはどれですか。", | |
| "criteria": {"billing": "請求や支払い", "technical": "技術的な問題や障害", "other": "その他"}, | |
| }, | |
| "outage": {"type": "noul", "instructions": "サービスが停止している。"}, | |
| }, | |
| } | |
| TEMPLATES["人物イラストの仕分け"] = { | |
| key: copy.deepcopy(TEMPLATES["顔・手の見直し(参考)"][key]) for key in ("style", "marks", "background") | |
| } | |
| class ClefError(ValueError): | |
| """An input or installation problem to show directly in the Studio.""" | |
| def atomic_json(path, data): | |
| path = Path(path) | |
| path.parent.mkdir(parents=True, exist_ok=True) | |
| temporary = path.with_name(path.name + ".tmp") | |
| temporary.write_text(json.dumps(data, ensure_ascii=False, indent=2, allow_nan=False), encoding="utf-8") | |
| os.replace(temporary, path) | |
| def sha256(path): | |
| digest = hashlib.sha256() | |
| with Path(path).open("rb") as stream: | |
| for chunk in iter(lambda: stream.read(8 * 1024 * 1024), b""): | |
| digest.update(chunk) | |
| return digest.hexdigest() | |
| def canonical_hash(value): | |
| return hashlib.sha256( | |
| json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() | |
| ).hexdigest() | |
| def _unique(pairs): | |
| result = {} | |
| for key, value in pairs: | |
| if key in result: | |
| raise ClefError(f"IDが重複しています: {key}") | |
| result[key] = value | |
| return result | |
| def parse_schema(value): | |
| if isinstance(value, str): | |
| if len(value) > 100_000: | |
| raise ClefError("判断項目のJSONは100,000文字以内にしてください。") | |
| try: | |
| value = json.loads(value, object_pairs_hook=_unique) | |
| except json.JSONDecodeError as exc: | |
| raise ClefError(f"JSONの{exc.lineno}行目・{exc.colno}文字目を確認してください。") from exc | |
| if not isinstance(value, dict) or not 1 <= len(value) <= 32: | |
| raise ClefError("判断項目は1〜32件のオブジェクトにしてください。") | |
| for qid, question in value.items(): | |
| if not isinstance(qid, str) or not qid.strip() or len(qid) > 80: | |
| raise ClefError("項目IDは1〜80文字にしてください。") | |
| if not isinstance(question, dict) or question.get("type") not in {"choice", "score", "noul"}: | |
| raise ClefError(f"{qid}: 種類はchoice / score / noulを指定してください。") | |
| if set(question) - {"type", "instructions", "criteria"}: | |
| raise ClefError(f"{qid}: 未対応の設定があります。") | |
| if not isinstance(question.get("instructions", ""), str): | |
| raise ClefError(f"{qid}: 判断することは文章で指定してください。") | |
| criteria, kind = question.get("criteria"), question["type"] | |
| if kind == "choice": | |
| if not isinstance(criteria, dict) or not 2 <= len(criteria) <= 64: | |
| raise ClefError(f"{qid}: 選択肢はIDと説明の組を2〜64件指定してください。") | |
| if any( | |
| not isinstance(k, str) or not k.strip() or not isinstance(v, str) or not v.strip() | |
| for k, v in criteria.items() | |
| ): | |
| raise ClefError(f"{qid}: 選択肢のIDと説明を入力してください。") | |
| elif kind == "score": | |
| if ( | |
| not isinstance(criteria, list) | |
| or not 2 <= len(criteria) <= 32 | |
| or any(not isinstance(x, str) or not x.strip() for x in criteria) | |
| ): | |
| raise ClefError(f"{qid}: 段階の説明を低い順に2〜32件指定してください。") | |
| elif criteria is not None and ( | |
| not isinstance(criteria, dict) | |
| or set(criteria) - {"true", "false"} | |
| or any(not isinstance(x, str) for x in criteria.values()) | |
| ): | |
| raise ClefError(f"{qid}: 真偽の説明はtrue / falseで指定してください。") | |
| return copy.deepcopy(value) | |
| def edit_question(schema, previous, qid, kind, instructions, options): | |
| result = copy.deepcopy(schema) | |
| qid = (qid or "").strip() | |
| if qid in result and qid != previous: | |
| raise ClefError(f"IDが重複しています: {qid}") | |
| question = {"type": kind, "instructions": (instructions or "").strip()} | |
| lines = [x.strip() for x in (options or "").splitlines() if x.strip()] | |
| if kind == "choice": | |
| pairs = [] | |
| for line in lines: | |
| if ":" not in line: | |
| raise ClefError("選択肢は1行に「ID: 説明」の形で指定してください。") | |
| key, description = line.split(":", 1) | |
| pairs.append((key.strip(), description.strip())) | |
| question["criteria"] = _unique(pairs) | |
| elif kind == "score": | |
| question["criteria"] = lines | |
| if previous and previous != qid: | |
| result.pop(previous, None) | |
| result[qid] = question | |
| return parse_schema(result) | |
| def integer(value, label, lower, upper): | |
| try: | |
| number = int(value) | |
| if ( | |
| isinstance(value, bool) | |
| or not math.isfinite(float(value)) | |
| or float(value) != number | |
| or not lower <= number <= upper | |
| ): | |
| raise ValueError | |
| return number | |
| except (ValueError, TypeError, OverflowError) as exc: | |
| raise ClefError(f"{label}は{lower}〜{upper}の整数にしてください。") from exc | |
| def validate_request(request): | |
| profile = request.get("profile") | |
| if profile not in PROFILES: | |
| raise ClefError("実行プロファイルを選んでください。") | |
| schema = parse_schema(request.get("questions")) | |
| state = request.get("state", "") | |
| if not isinstance(state, (str, dict, list, int, float, bool, type(None))): | |
| raise ClefError("補足入力は文章またはJSONにしてください。") | |
| if len(json.dumps(state, ensure_ascii=False)) > 60_000: | |
| raise ClefError("補足入力は60,000文字以内にしてください。") | |
| return { | |
| "profile": profile, | |
| "questions": schema, | |
| "state": state, | |
| "max_pixels": integer(request.get("max_pixels", 262144), "画像の最大画素数", 65536, 1048576), | |
| "max_length": integer(request.get("max_length", 4096), "入力トークン上限", 512, 8192), | |
| } | |
| def fingerprint(request, items): | |
| profile = PROFILES[request["profile"]] | |
| return canonical_hash( | |
| { | |
| "request": request, | |
| "model": MODELS[profile["model"]], | |
| "profile": profile, | |
| "inputs": [{"kind": item.get("kind", "image"), "sha256": item["sha256"]} for item in items], | |
| "preprocessing": PREPROCESSING_VERSION, | |
| } | |
| ) | |
| def summary(answer, question): | |
| kind = answer["type"] | |
| if kind == "noul": | |
| return f"P(真) {answer['noul']:.3f}" | |
| if kind == "choice": | |
| choice = answer["choice"] | |
| return f"{choice} · p={answer['confidence']:.3f}" | |
| return f"期待値 {answer['score']:.2f} (0–{len(question['criteria']) - 1})" | |
| def ambiguity(answer): | |
| if answer["type"] == "noul": | |
| return 1 - 2 * abs(answer["noul"] - 0.5) | |
| probabilities = list(answer["probabilities"].values()) | |
| return -sum(p * math.log(p) for p in probabilities if p > 0) / math.log(len(probabilities)) | |
| def compare_runs(left, right): | |
| if left.get("preprocessing") != right.get("preprocessing"): | |
| raise ClefError("比較には画像の前処理の一致が必要です。異なる前処理の結果は比較できません。") | |
| for key in ("questions", "state", "max_pixels"): | |
| if canonical_hash(left["request"].get(key)) != canonical_hash(right["request"].get(key)): | |
| raise ClefError("比較には判断項目・補足入力・処理解像度の一致が必要です。") | |
| other = defaultdict(deque) | |
| for item in right["items"]: | |
| if item["status"] == "done": | |
| other[(item.get("kind", "image"), item["sha256"])].append(item) | |
| rows = [] | |
| for item in left["items"]: | |
| key = (item.get("kind", "image"), item["sha256"]) | |
| if item["status"] != "done" or not other[key]: | |
| continue | |
| matched = other[key].popleft() | |
| differences = {} | |
| for qid, answer in item["answers"].items(): | |
| target = matched["answers"][qid] | |
| if answer["type"] == "noul": | |
| delta = target["noul"] - answer["noul"] | |
| differences[qid] = { | |
| "delta": delta, | |
| "changed": (answer["noul"] >= 0.5) != (target["noul"] >= 0.5), | |
| "max_probability_delta": abs(delta), | |
| } | |
| else: | |
| delta = max(abs(p - target["probabilities"][key]) for key, p in answer["probabilities"].items()) | |
| changed = ( | |
| answer.get("choice") != target.get("choice") | |
| if answer["type"] == "choice" | |
| else max(answer["probabilities"], key=answer["probabilities"].get) | |
| != max(target["probabilities"], key=target["probabilities"].get) | |
| ) | |
| differences[qid] = {"changed": changed, "max_probability_delta": delta} | |
| if answer["type"] == "score": | |
| differences[qid]["delta"] = target["score"] - answer["score"] | |
| rows.append( | |
| { | |
| "name": item["name"], | |
| "sha256": item["sha256"], | |
| "differences": differences, | |
| "changed": any(x["changed"] for x in differences.values()), | |
| "max_probability_delta": max(x["max_probability_delta"] for x in differences.values()), | |
| } | |
| ) | |
| return sorted(rows, key=lambda x: x["max_probability_delta"], reverse=True) | |
| def export_csv(run): | |
| schema = run["request"]["questions"] | |
| columns = ["name", "sha256", "status", "error"] + [ | |
| col for qid in schema for col in ("answer:" + qid, "confidence:" + qid) | |
| ] | |
| stream = io.StringIO(newline="") | |
| writer = csv.DictWriter(stream, fieldnames=columns) | |
| writer.writeheader() | |
| for item in run["items"]: | |
| row = {key: item.get(key, "") for key in columns[:4]} | |
| for qid, answer in item.get("answers", {}).items(): | |
| row["answer:" + qid] = answer.get("choice", answer.get("score", answer.get("noul", ""))) | |
| row["confidence:" + qid] = answer.get("confidence", "") | |
| for key, value in row.items(): | |
| if isinstance(value, str) and value.lstrip().startswith(("=", "+", "-", "@")): | |
| row[key] = "'" + value | |
| writer.writerow(row) | |
| return stream.getvalue() | |
| def source_manifest(root, model, verify_hashes=False): | |
| directory = Path(root) / "source" / model | |
| try: | |
| manifest = json.loads((directory / "release.json").read_text(encoding="utf-8")) | |
| except (OSError, ValueError) as exc: | |
| raise ClefError( | |
| f"{model}のBF16元重みは未導入です。aikimi-clef-setup.bat --source --model {model}を実行してください。" | |
| ) from exc | |
| if manifest.get("revision") != MODELS[model]["revision"] or manifest.get("repo") != MODELS[model]["repo"]: | |
| raise ClefError("Clefの固定リビジョンが一致しません。セットアップを再実行してください。") | |
| for item in manifest["files"]: | |
| path = directory / item["path"] | |
| if ( | |
| not path.resolve().is_relative_to(directory.resolve()) | |
| or not path.is_file() | |
| or path.stat().st_size != item["size"] | |
| ): | |
| raise ClefError(f"Clef配布物が不足・変更されています: {item['path']}") | |
| if (verify_hashes or path.suffix == ".py") and sha256(path) != item["sha256"]: | |
| raise ClefError(f"ClefのSHA-256が一致しません: {item['path']}") | |
| return directory, manifest | |