Image-Text-to-Text
Transformers
English
Chinese
eviscene
multimodal
image-factuality
agent
sft
distillation
Instructions to use ZiAnwang/EviScene with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use ZiAnwang/EviScene with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("image-text-to-text", model="ZiAnwang/EviScene")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("ZiAnwang/EviScene", device_map="auto") - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- vLLM
How to use ZiAnwang/EviScene with vLLM:
Install from pip and serve model
# Install vLLM from pip: pip install vllm # Start the vLLM server: vllm serve "ZiAnwang/EviScene" # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:8000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker
docker model run hf.co/ZiAnwang/EviScene
- SGLang
How to use ZiAnwang/EviScene with SGLang:
Install from pip and serve model
# Install SGLang from pip: pip install sglang # Start the SGLang server: python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }'Use Docker images
docker run --gpus all \ --shm-size 32g \ -p 30000:30000 \ -v ~/.cache/huggingface:/root/.cache/huggingface \ --env "HF_TOKEN=<secret>" \ --ipc=host \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path "ZiAnwang/EviScene" \ --host 0.0.0.0 \ --port 30000 # Call the server using curl (OpenAI-compatible API): curl -X POST "http://localhost:30000/v1/completions" \ -H "Content-Type: application/json" \ --data '{ "model": "ZiAnwang/EviScene", "prompt": "Once upon a time,", "max_tokens": 512, "temperature": 0.5 }' - Docker Model Runner
How to use ZiAnwang/EviScene with Docker Model Runner:
docker model run hf.co/ZiAnwang/EviScene
Download scripts/run_direct_qa_baseline.py from ZiAnwang/EviScene: direct link, hf CLI and curl.
- Browser
- Download file 20.5 kB
-
https://huggingface.co/ZiAnwang/EviScene/resolve/main/scripts/run_direct_qa_baseline.py
- Command line
-
hf download hf://ZiAnwang/EviScene/scripts/run_direct_qa_baseline.py
-
curl -L -o run_direct_qa_baseline.py https://huggingface.co/ZiAnwang/EviScene/resolve/main/scripts/run_direct_qa_baseline.py
20.5 kB
| """Run the provider-neutral image-only direct-QA baseline. | |
| The model receives only: | |
| one image + the shared direct-QA prompt | |
| Dataset claims, labels, source URLs, capability cells, and generation prompts | |
| are never included in the model request. Gold labels are used only for local | |
| evaluation after the response has been received. | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import asyncio | |
| import base64 | |
| import json | |
| import mimetypes | |
| import random | |
| import sys | |
| import time | |
| from collections import Counter | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from typing import Any, Mapping, Sequence | |
| from dotenv import load_dotenv | |
| sys.path.insert(0, str(Path(__file__).resolve().parents[1])) | |
| load_dotenv() | |
| from eviscene.integrations.gemini import GeminiInteractionsClient, extract_text | |
| DEFAULT_PROMPT = """You are an Image Factual Verifier. | |
| Given only one image, identify the main real-world fact or situation presented by the image and judge whether it is correct. | |
| Consider the image as a whole. You may use visible people, objects, actions, text, numbers, places, times, and relationships as clues, but include only details that the image actually presents. Do not force the image into a more specific event, background, identity, or relationship than it conveys. | |
| The verdict must be based on the fact or situation identified from the image. Visual style, image quality, or apparent AI/editing artifacts are not by themselves proof that the expressed fact is true or false, and must not replace evaluation of the factual content. If the expressed fact itself concerns image generation or editing, evaluate that fact directly. | |
| Output only JSON: | |
| { | |
| "core_fact": "...", | |
| "verdict": "real" or "fake", | |
| "reason": "..." | |
| }""" | |
| def _parse_json_object(text: str) -> tuple[dict[str, Any] | None, str | None]: | |
| candidate = text.strip() | |
| if candidate.startswith("```"): | |
| lines = candidate.splitlines() | |
| if lines and lines[0].strip().startswith("```"): | |
| lines = lines[1:] | |
| if lines and lines[-1].strip() == "```": | |
| lines = lines[:-1] | |
| candidate = "\n".join(lines).strip() | |
| try: | |
| value = json.loads(candidate) | |
| except json.JSONDecodeError as exc: | |
| return None, str(exc) | |
| if not isinstance(value, dict): | |
| return None, "model output is not a JSON object" | |
| return value, None | |
| def _thought_text(value: Any) -> list[str]: | |
| if isinstance(value, str): | |
| return [value] if value.strip() else [] | |
| if isinstance(value, Mapping): | |
| chunks: list[str] = [] | |
| for item in value.values(): | |
| chunks.extend(_thought_text(item)) | |
| return chunks | |
| if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): | |
| chunks = [] | |
| for item in value: | |
| chunks.extend(_thought_text(item)) | |
| return chunks | |
| return [] | |
| def extract_native_thought(payload: Mapping[str, Any]) -> str: | |
| """Extract provider-returned thought summaries, never model JSON fields.""" | |
| chunks: list[str] = [] | |
| for step in payload.get("steps", []) or []: | |
| if not isinstance(step, Mapping): | |
| continue | |
| if str(step.get("type", "")).strip().lower() != "thought": | |
| continue | |
| for field_name in ("text", "summary", "content"): | |
| chunks.extend(_thought_text(step.get(field_name))) | |
| return "\n".join(dict.fromkeys(chunk.strip() for chunk in chunks if chunk.strip())) | |
| def _resolve_image_path(row: Mapping[str, Any], image_root: Path) -> Path: | |
| raw = str( | |
| row.get("unified_image_path") | |
| or row.get("image_path") | |
| or row.get("local_image_path") | |
| or "" | |
| ).strip() | |
| if not raw: | |
| raise ValueError("manifest row has no image path") | |
| path = Path(raw).expanduser() | |
| return path if path.is_absolute() else image_root / path | |
| def _case_id(row: Mapping[str, Any]) -> str: | |
| return str( | |
| row.get("unified_case_id") | |
| or row.get("case_id") | |
| or row.get("record_id") | |
| or row.get("unified_image_path") | |
| or row.get("image_path") | |
| or "" | |
| ).strip() | |
| def _gold_verdict(row: Mapping[str, Any]) -> str | None: | |
| return { | |
| "supported": "real", | |
| "refuted": "fake", | |
| "real": "real", | |
| "fake": "fake", | |
| }.get(str(row.get("factual_status") or row.get("label") or "").strip().lower()) | |
| def _image_input(path: Path) -> dict[str, str]: | |
| mime_type = mimetypes.guess_type(path.name)[0] or "image/jpeg" | |
| return { | |
| "type": "image", | |
| "mime_type": mime_type, | |
| "data": base64.b64encode(path.read_bytes()).decode("ascii"), | |
| } | |
| def _usage_thought_tokens(payload: Mapping[str, Any]) -> int: | |
| usage = payload.get("usage") | |
| if not isinstance(usage, Mapping): | |
| return 0 | |
| return int( | |
| usage.get("total_thought_tokens", usage.get("thought_tokens", 0)) or 0 | |
| ) | |
| def _load_rows(manifest: Path, image_root: Path) -> list[dict[str, Any]]: | |
| rows: list[dict[str, Any]] = [] | |
| seen: set[str] = set() | |
| with manifest.open(encoding="utf-8") as handle: | |
| for line_number, line in enumerate(handle, start=1): | |
| if not line.strip(): | |
| continue | |
| try: | |
| row = json.loads(line) | |
| except json.JSONDecodeError as exc: | |
| raise ValueError(f"invalid JSON at {manifest}:{line_number}") from exc | |
| if not isinstance(row, dict): | |
| raise ValueError(f"manifest row is not an object: {manifest}:{line_number}") | |
| case_id = _case_id(row) | |
| if not case_id: | |
| raise ValueError(f"manifest row has no stable case ID: {manifest}:{line_number}") | |
| if case_id in seen: | |
| raise ValueError(f"duplicate case ID {case_id!r}: {manifest}:{line_number}") | |
| seen.add(case_id) | |
| row["_resolved_image_path"] = str(_resolve_image_path(row, image_root)) | |
| rows.append(row) | |
| return rows | |
| def _stratified_sample( | |
| rows: Sequence[dict[str, Any]], | |
| *, | |
| limit: int, | |
| field: str, | |
| seed: int, | |
| ) -> list[dict[str, Any]]: | |
| """Sample rows round-robin across one manifest category field.""" | |
| if limit <= 0 or limit >= len(rows): | |
| selected = list(rows) | |
| random.Random(seed).shuffle(selected) | |
| return selected | |
| groups: dict[str, list[dict[str, Any]]] = {} | |
| for row in rows: | |
| value = str(row.get(field) or "<empty>").strip() or "<empty>" | |
| groups.setdefault(value, []).append(row) | |
| rng = random.Random(seed) | |
| for group in groups.values(): | |
| rng.shuffle(group) | |
| selected: list[dict[str, Any]] = [] | |
| keys = sorted(groups) | |
| while len(selected) < limit: | |
| added = False | |
| for key in keys: | |
| group = groups[key] | |
| if not group: | |
| continue | |
| selected.append(group.pop()) | |
| added = True | |
| if len(selected) >= limit: | |
| break | |
| if not added: | |
| break | |
| rng.shuffle(selected) | |
| return selected | |
| def _load_completed_ids(results_path: Path) -> set[str]: | |
| if not results_path.is_file(): | |
| return set() | |
| latest_status: dict[str, str] = {} | |
| with results_path.open(encoding="utf-8") as handle: | |
| for line in handle: | |
| if not line.strip(): | |
| continue | |
| try: | |
| row = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| case_id = str(row.get("case_id") or "").strip() | |
| if case_id: | |
| latest_status[case_id] = str(row.get("status") or "").strip() | |
| return { | |
| case_id | |
| for case_id, status in latest_status.items() | |
| if status == "completed" | |
| } | |
| async def _run_one( | |
| row: Mapping[str, Any], | |
| *, | |
| client: GeminiInteractionsClient, | |
| prompt: str, | |
| model: str, | |
| response_format: Mapping[str, Any], | |
| generation_config: Mapping[str, Any], | |
| semaphore: asyncio.Semaphore, | |
| ) -> dict[str, Any]: | |
| case_id = _case_id(row) | |
| relative_image = str( | |
| row.get("unified_image_path") | |
| or row.get("image_path") | |
| or row.get("local_image_path") | |
| or "" | |
| ) | |
| image_path = Path(str(row["_resolved_image_path"])) | |
| started = time.monotonic() | |
| result: dict[str, Any] = { | |
| "case_id": case_id, | |
| "image_path": relative_image, | |
| "model": model, | |
| "status": "error", | |
| "started_at": datetime.now(timezone.utc).isoformat(), | |
| "gold_verdict": _gold_verdict(row), | |
| } | |
| try: | |
| if not image_path.is_file(): | |
| raise FileNotFoundError(str(image_path)) | |
| # Deliberately construct the request from only image + shared prompt. | |
| request_input = [_image_input(image_path), {"type": "text", "text": prompt}] | |
| async with semaphore: | |
| payload = await client.create( | |
| model=model, | |
| input=request_input, | |
| response_format=response_format, | |
| generation_config=generation_config, | |
| store=True, | |
| ) | |
| output_text = extract_text(payload) | |
| parsed, parse_error = _parse_json_object(output_text) | |
| native_thought = extract_native_thought(payload) | |
| predicted = ( | |
| str(parsed.get("verdict", "")).strip().lower() | |
| if parsed is not None | |
| else "" | |
| ) | |
| gold = result["gold_verdict"] | |
| result.update( | |
| { | |
| "status": "completed", | |
| "elapsed_seconds": round(time.monotonic() - started, 3), | |
| "interaction_id": payload.get("id"), | |
| "interaction_status": payload.get("status"), | |
| "native_thought": native_thought, | |
| "native_thought_chars": len(native_thought), | |
| "native_thought_tokens": _usage_thought_tokens(payload), | |
| "model_output_text": output_text, | |
| "model_output": parsed, | |
| "json_parse_error": parse_error, | |
| "predicted_verdict": predicted or None, | |
| "correct": bool(gold and predicted and predicted == gold), | |
| "usage": payload.get("usage"), | |
| } | |
| ) | |
| except Exception as exc: | |
| result.update( | |
| { | |
| "elapsed_seconds": round(time.monotonic() - started, 3), | |
| "error_type": type(exc).__name__, | |
| "error": str(exc), | |
| } | |
| ) | |
| return result | |
| def _write_json(path: Path, value: Mapping[str, Any]) -> None: | |
| with path.open("w", encoding="utf-8") as handle: | |
| json.dump(value, handle, ensure_ascii=False, indent=2) | |
| handle.write("\n") | |
| def _build_summary( | |
| records: Sequence[Mapping[str, Any]], | |
| *, | |
| args: argparse.Namespace, | |
| manifest: Path, | |
| output_dir: Path, | |
| selection_counts: Mapping[str, int], | |
| ) -> dict[str, Any]: | |
| latest: dict[str, Mapping[str, Any]] = {} | |
| for record in records: | |
| case_id = str(record.get("case_id") or "") | |
| if case_id: | |
| latest[case_id] = record | |
| values = list(latest.values()) | |
| completed = [row for row in values if row.get("status") == "completed"] | |
| comparable = [row for row in completed if row.get("gold_verdict") in {"real", "fake"}] | |
| correct = [row for row in comparable if row.get("correct")] | |
| thought = [row for row in completed if row.get("native_thought_chars", 0) > 0] | |
| valid_json = [ | |
| row | |
| for row in completed | |
| if isinstance(row.get("model_output"), dict) | |
| and not row.get("json_parse_error") | |
| ] | |
| errors = [row for row in values if row.get("status") != "completed"] | |
| error_types = Counter(str(row.get("error_type") or "unknown") for row in errors) | |
| return { | |
| "schema_version": "ifv-direct-qa-baseline-run-v1", | |
| "created_at": datetime.now(timezone.utc).isoformat(), | |
| "model": args.model, | |
| "thinking_level": args.thinking_level, | |
| "thinking_summaries": "auto", | |
| "max_output_tokens": args.max_output_tokens, | |
| "concurrency": args.concurrency, | |
| "manifest": str(manifest), | |
| "output_dir": str(output_dir), | |
| "input_contract": "image_only", | |
| "prompt_contract": "shared_direct_qa_prompt", | |
| "selection": { | |
| "stratify_by": args.stratify_by, | |
| "seed": args.seed, | |
| "category_counts": dict(selection_counts), | |
| }, | |
| "counts": { | |
| "manifest_rows_selected": len(values), | |
| "completed": len(completed), | |
| "errors": len(errors), | |
| "valid_json": len(valid_json), | |
| "native_thought_present": len(thought), | |
| "native_thought_absent": len(completed) - len(thought), | |
| "gold_comparable": len(comparable), | |
| "correct": len(correct), | |
| }, | |
| "accuracy_on_completed_comparable": ( | |
| round(len(correct) / len(comparable), 6) if comparable else None | |
| ), | |
| "thought_presence_rate_on_completed": ( | |
| round(len(thought) / len(completed), 6) if completed else None | |
| ), | |
| "error_types": dict(error_types), | |
| "result_file": str(output_dir / "results.jsonl"), | |
| "prompt_file": str(output_dir / "prompt.txt"), | |
| "config_file": str(output_dir / "run-config.json"), | |
| } | |
| async def _run(args: argparse.Namespace) -> int: | |
| manifest = Path(args.manifest).expanduser().resolve() | |
| image_root = ( | |
| Path(args.image_root).expanduser().resolve() | |
| if args.image_root | |
| else manifest.parent | |
| ) | |
| output_dir = Path(args.output_dir).expanduser().resolve() | |
| output_dir.mkdir(parents=True, exist_ok=True) | |
| results_path = output_dir / "results.jsonl" | |
| prompt = ( | |
| Path(args.prompt_file).expanduser().read_text(encoding="utf-8") | |
| if args.prompt_file | |
| else DEFAULT_PROMPT | |
| ).strip() | |
| if not prompt: | |
| raise ValueError("direct-QA prompt must not be empty") | |
| rows = _load_rows(manifest, image_root) | |
| if args.case_id: | |
| wanted = set(args.case_id) | |
| rows = [row for row in rows if _case_id(row) in wanted] | |
| if args.stratify_by: | |
| rows = _stratified_sample( | |
| rows, | |
| limit=args.limit if args.limit > 0 else len(rows), | |
| field=args.stratify_by, | |
| seed=args.seed, | |
| ) | |
| elif args.limit > 0: | |
| rows = rows[: args.limit] | |
| if args.resume: | |
| completed_ids = _load_completed_ids(results_path) | |
| rows = [row for row in rows if _case_id(row) not in completed_ids] | |
| selection_counts = Counter( | |
| str(row.get(args.stratify_by) or "<empty>") | |
| for row in rows | |
| ) if args.stratify_by else Counter() | |
| schema = { | |
| "type": "object", | |
| "properties": { | |
| "core_fact": {"type": "string"}, | |
| "verdict": {"type": "string", "enum": ["real", "fake"]}, | |
| "reason": {"type": "string"}, | |
| }, | |
| "required": ["core_fact", "verdict", "reason"], | |
| "additionalProperties": False, | |
| } | |
| response_format = { | |
| "type": "text", | |
| "mime_type": "application/json", | |
| "schema": schema, | |
| } | |
| generation_config = { | |
| "max_output_tokens": args.max_output_tokens, | |
| "thinking_level": args.thinking_level, | |
| "thinking_summaries": "auto", | |
| } | |
| config = { | |
| "schema_version": "ifv-direct-qa-baseline-config-v1", | |
| "model": args.model, | |
| "thinking_level": args.thinking_level, | |
| "thinking_summaries": "auto", | |
| "max_output_tokens": args.max_output_tokens, | |
| "timeout": args.timeout, | |
| "max_retries": args.max_retries, | |
| "concurrency": args.concurrency, | |
| "stratify_by": args.stratify_by, | |
| "seed": args.seed, | |
| "manifest": str(manifest), | |
| "image_root": str(image_root), | |
| "selected_rows": len(rows), | |
| "selection_category_counts": dict(selection_counts), | |
| "input_contract": "image_only", | |
| "request_fields": ["image", "shared_prompt"], | |
| } | |
| (output_dir / "prompt.txt").write_text(prompt + "\n", encoding="utf-8") | |
| _write_json(output_dir / "run-config.json", config) | |
| existing_records: list[dict[str, Any]] = [] | |
| if results_path.is_file(): | |
| with results_path.open(encoding="utf-8") as handle: | |
| for line in handle: | |
| if line.strip(): | |
| try: | |
| value = json.loads(line) | |
| except json.JSONDecodeError: | |
| continue | |
| if isinstance(value, dict): | |
| existing_records.append(value) | |
| semaphore = asyncio.Semaphore(max(1, args.concurrency)) | |
| tasks: list[asyncio.Task[dict[str, Any]]] = [] | |
| async with GeminiInteractionsClient( | |
| timeout=args.timeout, | |
| max_retries=args.max_retries, | |
| ) as client: | |
| for row in rows: | |
| tasks.append( | |
| asyncio.create_task( | |
| _run_one( | |
| row, | |
| client=client, | |
| prompt=prompt, | |
| model=args.model, | |
| response_format=response_format, | |
| generation_config=generation_config, | |
| semaphore=semaphore, | |
| ) | |
| ) | |
| ) | |
| with results_path.open("a", encoding="utf-8") as handle: | |
| for task in asyncio.as_completed(tasks): | |
| result = await task | |
| existing_records.append(result) | |
| handle.write(json.dumps(result, ensure_ascii=False) + "\n") | |
| handle.flush() | |
| print( | |
| json.dumps( | |
| { | |
| "case_id": result.get("case_id"), | |
| "status": result.get("status"), | |
| "elapsed_seconds": result.get("elapsed_seconds"), | |
| "thought_present": bool(result.get("native_thought_chars")), | |
| "predicted_verdict": result.get("predicted_verdict"), | |
| "correct": result.get("correct"), | |
| "error_type": result.get("error_type"), | |
| }, | |
| ensure_ascii=False, | |
| ), | |
| flush=True, | |
| ) | |
| summary = _build_summary( | |
| existing_records, | |
| args=args, | |
| manifest=manifest, | |
| output_dir=output_dir, | |
| selection_counts=selection_counts, | |
| ) | |
| _write_json(output_dir / "summary.json", summary) | |
| print(json.dumps(summary, ensure_ascii=False, indent=2)) | |
| return 0 | |
| def main() -> int: | |
| parser = argparse.ArgumentParser( | |
| description="Run image-only direct QA with a shared comparison prompt." | |
| ) | |
| parser.add_argument("--manifest", required=True) | |
| parser.add_argument("--output-dir", required=True) | |
| parser.add_argument("--image-root") | |
| parser.add_argument("--prompt-file") | |
| parser.add_argument("--model", default="gemini-3.1-pro-preview") | |
| parser.add_argument( | |
| "--thinking-level", | |
| choices=("low", "medium", "high"), | |
| default="high", | |
| ) | |
| parser.add_argument("--max-output-tokens", type=int, default=4096) | |
| parser.add_argument("--timeout", type=float, default=240.0) | |
| parser.add_argument("--max-retries", type=int, default=2) | |
| parser.add_argument("--concurrency", type=int, default=4) | |
| parser.add_argument("--limit", type=int, default=0) | |
| parser.add_argument( | |
| "--stratify-by", | |
| choices=( | |
| "factual_status", | |
| "construction_subroute", | |
| "target_route", | |
| "target_capability_cell", | |
| "production_class", | |
| ), | |
| help="Round-robin sample evenly across values of this manifest field.", | |
| ) | |
| parser.add_argument("--seed", type=int, default=20260829) | |
| parser.add_argument("--case-id", action="append") | |
| parser.add_argument("--resume", action="store_true") | |
| args = parser.parse_args() | |
| if args.max_output_tokens <= 0: | |
| parser.error("--max-output-tokens must be positive") | |
| if args.timeout <= 0: | |
| parser.error("--timeout must be positive") | |
| if args.max_retries < 0: | |
| parser.error("--max-retries must be non-negative") | |
| if args.concurrency <= 0: | |
| parser.error("--concurrency must be positive") | |
| return asyncio.run(_run(args)) | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) | |