qev 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qev/__init__.py +28 -0
- qev/__main__.py +5 -0
- qev/cli.py +130 -0
- qev/community.py +10 -0
- qev/download.py +118 -0
- qev/playground/__init__.py +1 -0
- qev/playground/app.py +243 -0
- qev/playground/engine.py +121 -0
- qev/playground/presets.json +270 -0
- qev/playground/samples/LICENSE.txt +21 -0
- qev/playground/samples/README.md +21 -0
- qev/playground/samples/provenance.json +75 -0
- qev/playground/samples/sample-01.jpg +0 -0
- qev/playground/samples/sample-02.jpg +0 -0
- qev/playground/samples/sample-03.jpg +0 -0
- qev/playground/samples/sample-04.jpg +0 -0
- qev/playground/samples/sample-05.jpg +0 -0
- qev/playground/samples/sample-06.jpg +0 -0
- qev/runtime.py +69 -0
- qev-0.2.0.dist-info/METADATA +271 -0
- qev-0.2.0.dist-info/RECORD +63 -0
- qev-0.2.0.dist-info/WHEEL +4 -0
- qev-0.2.0.dist-info/entry_points.txt +3 -0
- qev-0.2.0.dist-info/licenses/LICENSE +201 -0
- qev-0.2.0.dist-info/licenses/NOTICE +71 -0
- veyra/__init__.py +3 -0
- veyra/augment_data.py +133 -0
- veyra/average_adapters.py +54 -0
- veyra/backbone.py +167 -0
- veyra/benchmark.py +136 -0
- veyra/binding_head.py +32 -0
- veyra/build_data.py +383 -0
- veyra/calibrate.py +130 -0
- veyra/candidates.py +60 -0
- veyra/checkpoint.py +82 -0
- veyra/cli.py +171 -0
- veyra/constants.py +5 -0
- veyra/data.py +110 -0
- veyra/decision_metrics.py +126 -0
- veyra/evaluate.py +108 -0
- veyra/evidence_data.py +228 -0
- veyra/features.py +195 -0
- veyra/head.py +99 -0
- veyra/interventions.py +169 -0
- veyra/model.py +84 -0
- veyra/option_model.py +432 -0
- veyra/packing.py +100 -0
- veyra/policy_data.py +255 -0
- veyra/policy_refresh.py +181 -0
- veyra/probability.py +72 -0
- veyra/proper_learning.py +100 -0
- veyra/reasoning_workspace.py +32 -0
- veyra/release_gate.py +146 -0
- veyra/replay.py +90 -0
- veyra/schema.py +86 -0
- veyra/server.py +57 -0
- veyra/synthetic_audit.py +66 -0
- veyra/training.py +191 -0
- veyra/transfer_learning.py +84 -0
- veyra/workflow_consistency.py +107 -0
- veyra/workflow_facts.py +54 -0
- veyra/workflow_learning.py +108 -0
- veyra/workflow_release_gate.py +85 -0
qev/__init__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""QEV public interface to the evaluated multimodal decision runtime."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.2.0"
|
|
4
|
+
__all__ = ["DecisionRequest", "QEV", "load", "__version__"]
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def load(checkpoint=None, *, device="auto", cache_dir=None, offline=False):
|
|
8
|
+
"""Load QEV, downloading its pinned weights on the first use.
|
|
9
|
+
|
|
10
|
+
Later calls reuse the local model cache. ``offline=True`` requires all weights
|
|
11
|
+
to be available already. ``QEV.load`` remains the original low-level loader.
|
|
12
|
+
"""
|
|
13
|
+
from qev.runtime import load_model
|
|
14
|
+
|
|
15
|
+
return load_model(checkpoint, device=device, cache_dir=cache_dir, offline=offline)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def __getattr__(name):
|
|
19
|
+
# CLI help and packaging metadata do not need to initialize PyTorch.
|
|
20
|
+
if name == "QEV":
|
|
21
|
+
from veyra.option_model import OptionModel
|
|
22
|
+
|
|
23
|
+
return OptionModel
|
|
24
|
+
if name == "DecisionRequest":
|
|
25
|
+
from veyra.schema import DecisionRequest
|
|
26
|
+
|
|
27
|
+
return DecisionRequest
|
|
28
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
qev/__main__.py
ADDED
qev/cli.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""SDK, local playground and first-use model downloads in one installed command."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from qev import __version__
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main(argv=None):
|
|
13
|
+
parser = argparse.ArgumentParser(prog="qev")
|
|
14
|
+
parser.add_argument("--version", action="version", version=f"qev {__version__}")
|
|
15
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
16
|
+
download = commands.add_parser("download", help="Download the pinned model for later use")
|
|
17
|
+
download.add_argument("--cache-dir")
|
|
18
|
+
download.add_argument("--output", "--checkpoint", type=Path)
|
|
19
|
+
download.add_argument("--checkpoint-only", action="store_true")
|
|
20
|
+
download.add_argument("--base-only", action="store_true")
|
|
21
|
+
download.add_argument(
|
|
22
|
+
"--offline", action="store_true", default=os.environ.get("QEV_OFFLINE") == "1"
|
|
23
|
+
)
|
|
24
|
+
for name in ("predict", "serve", "playground"):
|
|
25
|
+
command = commands.add_parser(name)
|
|
26
|
+
command.add_argument(
|
|
27
|
+
"--checkpoint", type=Path, help="Checkpoint folder; default: QEV cache"
|
|
28
|
+
)
|
|
29
|
+
command.add_argument("--device", choices=["auto", "cpu", "cuda"], default="auto")
|
|
30
|
+
command.add_argument("--cache-dir", help="Hugging Face cache; also accepts QEV_CACHE_DIR")
|
|
31
|
+
command.add_argument(
|
|
32
|
+
"--offline", action="store_true", default=os.environ.get("QEV_OFFLINE") == "1"
|
|
33
|
+
)
|
|
34
|
+
if name == "predict":
|
|
35
|
+
command.add_argument("--request", type=Path, required=True)
|
|
36
|
+
else:
|
|
37
|
+
command.add_argument("--host", default="127.0.0.1")
|
|
38
|
+
command.add_argument("--port", type=int, default=7860 if name == "playground" else 8000)
|
|
39
|
+
if name == "serve":
|
|
40
|
+
command.add_argument("--image-root", type=Path, default=Path.cwd())
|
|
41
|
+
else:
|
|
42
|
+
command.add_argument(
|
|
43
|
+
"--open", action="store_true", help="Open the playground in a browser"
|
|
44
|
+
)
|
|
45
|
+
for name in ("star", "support"):
|
|
46
|
+
command = commands.add_parser(name, help=f"Show the QEV {name} page")
|
|
47
|
+
command.add_argument("--open", action="store_true", help="Open the page in your browser")
|
|
48
|
+
args = parser.parse_args(argv)
|
|
49
|
+
if args.command == "download" and args.base_only and args.checkpoint_only:
|
|
50
|
+
parser.error("--base-only and --checkpoint-only cannot be combined")
|
|
51
|
+
if hasattr(args, "port") and not 1 <= args.port <= 65535:
|
|
52
|
+
parser.error("--port must be between 1 and 65535")
|
|
53
|
+
try:
|
|
54
|
+
run(args)
|
|
55
|
+
except (ValueError, RuntimeError, OSError) as error:
|
|
56
|
+
parser.exit(1, f"qev: {error}\n")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run(args):
|
|
60
|
+
if args.command in {"star", "support"}:
|
|
61
|
+
from qev.community import REPOSITORY_URL, SUPPORT_URL
|
|
62
|
+
|
|
63
|
+
url = REPOSITORY_URL if args.command == "star" else SUPPORT_URL
|
|
64
|
+
print(url)
|
|
65
|
+
if args.open:
|
|
66
|
+
import webbrowser
|
|
67
|
+
|
|
68
|
+
webbrowser.open(url)
|
|
69
|
+
return
|
|
70
|
+
from qev.runtime import cache_home, load_model, prepare_checkpoint, resolve_cache
|
|
71
|
+
|
|
72
|
+
def progress(message):
|
|
73
|
+
print(message, file=sys.stderr, flush=True)
|
|
74
|
+
|
|
75
|
+
if args.command == "download":
|
|
76
|
+
from qev.download import CHECKPOINT_REVISION, download_backbone, download_checkpoint
|
|
77
|
+
|
|
78
|
+
folder = args.output or cache_home() / "checkpoints" / CHECKPOINT_REVISION
|
|
79
|
+
cache = resolve_cache(args.cache_dir)
|
|
80
|
+
if args.checkpoint_only:
|
|
81
|
+
result = {
|
|
82
|
+
"checkpoint": str(download_checkpoint(folder, cache, local_files_only=args.offline))
|
|
83
|
+
}
|
|
84
|
+
elif args.base_only:
|
|
85
|
+
result = {"backbone_cache": download_backbone(cache, local_files_only=args.offline)}
|
|
86
|
+
else:
|
|
87
|
+
folder, cache = prepare_checkpoint(
|
|
88
|
+
folder, cache_dir=cache, offline=args.offline, progress=progress
|
|
89
|
+
)
|
|
90
|
+
result = {"checkpoint": str(folder), "backbone_cache": cache}
|
|
91
|
+
print(json.dumps(result, indent=2))
|
|
92
|
+
return
|
|
93
|
+
if args.command == "playground":
|
|
94
|
+
from qev.playground.app import launch
|
|
95
|
+
|
|
96
|
+
launch(
|
|
97
|
+
checkpoint=args.checkpoint,
|
|
98
|
+
device=args.device,
|
|
99
|
+
cache_dir=args.cache_dir,
|
|
100
|
+
offline=args.offline,
|
|
101
|
+
host=args.host,
|
|
102
|
+
port=args.port,
|
|
103
|
+
inbrowser=args.open,
|
|
104
|
+
)
|
|
105
|
+
return
|
|
106
|
+
import torch
|
|
107
|
+
|
|
108
|
+
from qev import DecisionRequest
|
|
109
|
+
|
|
110
|
+
torch.set_num_threads(4)
|
|
111
|
+
request = None
|
|
112
|
+
if args.command == "predict":
|
|
113
|
+
request = DecisionRequest.from_json(args.request.read_text(encoding="utf-8"))
|
|
114
|
+
elif not args.image_root.is_dir():
|
|
115
|
+
raise ValueError("--image-root must be an existing directory")
|
|
116
|
+
model = load_model(
|
|
117
|
+
args.checkpoint,
|
|
118
|
+
device=args.device,
|
|
119
|
+
cache_dir=args.cache_dir,
|
|
120
|
+
offline=args.offline,
|
|
121
|
+
progress=progress,
|
|
122
|
+
)
|
|
123
|
+
if request is not None:
|
|
124
|
+
print(json.dumps(model.predict(request, args.request.parent.resolve()), indent=2))
|
|
125
|
+
else:
|
|
126
|
+
import uvicorn
|
|
127
|
+
|
|
128
|
+
from veyra.server import create_app
|
|
129
|
+
|
|
130
|
+
uvicorn.run(create_app(model, args.image_root), host=args.host, port=args.port)
|
qev/community.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Public community links; opening a page always requires a user action."""
|
|
2
|
+
|
|
3
|
+
REPOSITORY_URL = "https://github.com/ken-jo/qev"
|
|
4
|
+
SUPPORT_URL = "https://github.com/sponsors/ken-jo"
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def community_markdown():
|
|
8
|
+
return (
|
|
9
|
+
"[Star QEV on GitHub](" + REPOSITORY_URL + ") · [GitHub Sponsors](" + SUPPORT_URL + ")"
|
|
10
|
+
)
|
qev/download.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Download the immutable QEV decision checkpoint and its upstream backbone."""
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
CHECKPOINT_ID = "ken-jo/qev"
|
|
10
|
+
# The repository rename preserves this original checkpoint revision.
|
|
11
|
+
CHECKPOINT_REVISION = "0d2d13ffb3c392071ea00b6bdb903c4b84dff48d"
|
|
12
|
+
CHECKPOINT_HASHES = {
|
|
13
|
+
"head.safetensors": "84b57aeeb987f73416ac0f796150957d1c5beb1ed373733053e46438f77a8fee",
|
|
14
|
+
"manifest.json": "d83f9910196c6e801658ca3816c3d2bd4a849ba3da6ff059a0edf7c6028e898d",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def download_checkpoint(output: Path, cache_dir: str, *, local_files_only=False) -> Path:
|
|
19
|
+
"""Copy verified checkpoint files into an explicit local directory."""
|
|
20
|
+
from filelock import FileLock
|
|
21
|
+
from huggingface_hub import hf_hub_download
|
|
22
|
+
|
|
23
|
+
output = output.resolve()
|
|
24
|
+
existing = []
|
|
25
|
+
for name, expected in CHECKPOINT_HASHES.items():
|
|
26
|
+
target = output / name
|
|
27
|
+
if target.exists():
|
|
28
|
+
if hashlib.sha256(target.read_bytes()).hexdigest() != expected:
|
|
29
|
+
raise ValueError(f"Refusing to replace a different checkpoint file: {target}")
|
|
30
|
+
existing.append(name)
|
|
31
|
+
if len(existing) == len(CHECKPOINT_HASHES):
|
|
32
|
+
return output
|
|
33
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
locks = Path(cache_dir) / ".qev-locks"
|
|
35
|
+
locks.mkdir(parents=True, exist_ok=True)
|
|
36
|
+
lock_name = hashlib.sha256(os.path.normcase(str(output)).encode()).hexdigest() + ".lock"
|
|
37
|
+
with FileLock(str(locks / lock_name)):
|
|
38
|
+
for name, expected in CHECKPOINT_HASHES.items():
|
|
39
|
+
destination = output / name
|
|
40
|
+
if destination.exists():
|
|
41
|
+
if hashlib.sha256(destination.read_bytes()).hexdigest() != expected:
|
|
42
|
+
raise ValueError(
|
|
43
|
+
f"Refusing to replace a different checkpoint file: {destination}"
|
|
44
|
+
)
|
|
45
|
+
continue
|
|
46
|
+
source = Path(
|
|
47
|
+
hf_hub_download(
|
|
48
|
+
CHECKPOINT_ID,
|
|
49
|
+
name,
|
|
50
|
+
revision=CHECKPOINT_REVISION,
|
|
51
|
+
cache_dir=cache_dir,
|
|
52
|
+
local_files_only=local_files_only,
|
|
53
|
+
token=False,
|
|
54
|
+
)
|
|
55
|
+
)
|
|
56
|
+
raw = source.read_bytes()
|
|
57
|
+
if hashlib.sha256(raw).hexdigest() != expected:
|
|
58
|
+
raise ValueError(f"Checkpoint integrity check failed: {name}")
|
|
59
|
+
temporary = None
|
|
60
|
+
try:
|
|
61
|
+
with tempfile.NamedTemporaryFile(
|
|
62
|
+
dir=output, suffix=".partial", delete=False
|
|
63
|
+
) as stream:
|
|
64
|
+
temporary = Path(stream.name)
|
|
65
|
+
stream.write(raw)
|
|
66
|
+
os.replace(temporary, destination)
|
|
67
|
+
finally:
|
|
68
|
+
if temporary is not None:
|
|
69
|
+
temporary.unlink(missing_ok=True)
|
|
70
|
+
return output
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def download_backbone(cache_dir: str, *, local_files_only=False) -> str:
|
|
74
|
+
"""Download the exact upstream weights and preprocessing files."""
|
|
75
|
+
from huggingface_hub import snapshot_download
|
|
76
|
+
|
|
77
|
+
from veyra.constants import MODEL_ID, MODEL_REVISION
|
|
78
|
+
|
|
79
|
+
folder = Path(
|
|
80
|
+
snapshot_download(
|
|
81
|
+
MODEL_ID,
|
|
82
|
+
revision=MODEL_REVISION,
|
|
83
|
+
cache_dir=cache_dir,
|
|
84
|
+
local_files_only=local_files_only,
|
|
85
|
+
allow_patterns=["*.json", "*.safetensors", "*.jinja", "*.txt"],
|
|
86
|
+
token=False,
|
|
87
|
+
)
|
|
88
|
+
)
|
|
89
|
+
required = {
|
|
90
|
+
"config.json",
|
|
91
|
+
"preprocessor_config.json",
|
|
92
|
+
"tokenizer.json",
|
|
93
|
+
"tokenizer_config.json",
|
|
94
|
+
"chat_template.jinja",
|
|
95
|
+
"merges.txt",
|
|
96
|
+
"vocab.json",
|
|
97
|
+
"video_preprocessor_config.json",
|
|
98
|
+
"model.safetensors.index.json",
|
|
99
|
+
}
|
|
100
|
+
index = folder / "model.safetensors.index.json"
|
|
101
|
+
if index.is_file():
|
|
102
|
+
required.update(json.loads(index.read_text("utf-8"))["weight_map"].values())
|
|
103
|
+
missing = []
|
|
104
|
+
for name in required:
|
|
105
|
+
# Hub snapshots may symlink weights into the adjacent blob cache.
|
|
106
|
+
path = folder / name
|
|
107
|
+
if (
|
|
108
|
+
Path(name).is_absolute()
|
|
109
|
+
or ".." in Path(name).parts
|
|
110
|
+
or not path.is_file()
|
|
111
|
+
or path.stat().st_size == 0
|
|
112
|
+
):
|
|
113
|
+
missing.append(name)
|
|
114
|
+
if missing:
|
|
115
|
+
from huggingface_hub.errors import LocalEntryNotFoundError
|
|
116
|
+
|
|
117
|
+
raise LocalEntryNotFoundError("Incomplete Qwen snapshot: " + ", ".join(sorted(missing)))
|
|
118
|
+
return str(folder)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""English playground and licensed sample photographs included with the QEV SDK."""
|
qev/playground/app.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""English local playground shipped inside the QEV Python distribution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
os.environ.setdefault("GRADIO_ANALYTICS_ENABLED", "False")
|
|
12
|
+
import gradio as gr # noqa: E402
|
|
13
|
+
|
|
14
|
+
from qev.community import community_markdown # noqa: E402
|
|
15
|
+
from qev.playground.engine import DemoEngine # noqa: E402
|
|
16
|
+
|
|
17
|
+
ROOT = Path(__file__).resolve().parent
|
|
18
|
+
PRESETS = json.loads((ROOT / "presets.json").read_text("utf-8"))
|
|
19
|
+
LABELS = {
|
|
20
|
+
"support": "Text / Route a support request",
|
|
21
|
+
"policy": "Text / Apply an order policy",
|
|
22
|
+
"uncertain": "Text / Check insufficient evidence",
|
|
23
|
+
"photo": "Image / Choose the material",
|
|
24
|
+
"photo_score": "Image / Score visibility",
|
|
25
|
+
"photo_truth": "Image / Judge a proposition",
|
|
26
|
+
"photo_policy": "Image + text / Apply a sorting policy",
|
|
27
|
+
}
|
|
28
|
+
SAMPLES = [
|
|
29
|
+
("sample-01.jpg", "Glass bottle"),
|
|
30
|
+
("sample-02.jpg", "Newspaper"),
|
|
31
|
+
("sample-03.jpg", "Cardboard"),
|
|
32
|
+
("sample-04.jpg", "Plastic bottle"),
|
|
33
|
+
("sample-05.jpg", "Metal can"),
|
|
34
|
+
("sample-06.jpg", "Packaging pouch"),
|
|
35
|
+
]
|
|
36
|
+
CSS = """
|
|
37
|
+
.gradio-container { max-width: 1200px !important; }
|
|
38
|
+
#intro { padding: 12px 0 20px; }
|
|
39
|
+
#intro h1 { letter-spacing: -.04em; font-size: 34px; line-height: 1.12; }
|
|
40
|
+
#intro p { max-width: 760px; }
|
|
41
|
+
#run-button { min-height: 48px; }
|
|
42
|
+
footer { display: none !important; }
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def preset_values(key):
|
|
47
|
+
preset = PRESETS[key]
|
|
48
|
+
question = preset["questions"][0]
|
|
49
|
+
options = question["options"]
|
|
50
|
+
criteria = "\n".join(
|
|
51
|
+
item["text"] if question["type"] == "score" else f"{item['key']} | {item['text']}"
|
|
52
|
+
for item in options
|
|
53
|
+
)
|
|
54
|
+
image = str(ROOT / "samples" / (preset["sampleId"] + ".jpg")) if "sampleId" in preset else None
|
|
55
|
+
return preset["text"], image, question["instructions"], question["type"], criteria
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def default_criteria(kind):
|
|
59
|
+
if kind == "noul":
|
|
60
|
+
return "false | The proposition is false.\ntrue | The proposition is true."
|
|
61
|
+
if kind == "score":
|
|
62
|
+
return (
|
|
63
|
+
"Low: the criterion is not met.\nMedium: the criterion is partly met.\n"
|
|
64
|
+
"High: the criterion is fully met."
|
|
65
|
+
)
|
|
66
|
+
return "yes | The evidence meets the criterion.\nno | The evidence does not meet the criterion."
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def runtime_notice(device, shared_gpu=False):
|
|
70
|
+
if shared_gpu:
|
|
71
|
+
return (
|
|
72
|
+
"**Shared GPU demo.** GPU acceleration is enabled. Response time depends on "
|
|
73
|
+
"GPU availability, input size and the queue. Daily usage limits apply."
|
|
74
|
+
)
|
|
75
|
+
if device == "cuda":
|
|
76
|
+
return (
|
|
77
|
+
"**GPU demo.** GPU acceleration is enabled. "
|
|
78
|
+
"Response time depends on input size and the queue."
|
|
79
|
+
)
|
|
80
|
+
return (
|
|
81
|
+
"**CPU demo.** Responses may take several seconds. GPU hosting can make model "
|
|
82
|
+
"processing faster; input size and queue time also affect how long you wait."
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def build_demo(engine):
|
|
87
|
+
def predict(*args):
|
|
88
|
+
try:
|
|
89
|
+
return engine.predict(*args)
|
|
90
|
+
except (ValueError, TypeError) as error:
|
|
91
|
+
raise gr.Error(str(error)) from error
|
|
92
|
+
except Exception as error:
|
|
93
|
+
logging.exception("Demo inference failed")
|
|
94
|
+
raise gr.Error(
|
|
95
|
+
"The model could not process this request. Please try a shorter example."
|
|
96
|
+
) from error
|
|
97
|
+
|
|
98
|
+
initial = preset_values("support")
|
|
99
|
+
with gr.Blocks(title="QEV", delete_cache=(300, 600)) as demo:
|
|
100
|
+
gr.Markdown(
|
|
101
|
+
"# QEV\n"
|
|
102
|
+
"Provide the evidence. Define your candidates. Inspect the probabilities.\n\n"
|
|
103
|
+
"Dynamic text and image decisions with **Qwen3.5-2B**. "
|
|
104
|
+
"[Model](https://huggingface.co/ken-jo/qev) · "
|
|
105
|
+
"[Data](https://huggingface.co/datasets/ken-jo/qev-data) · "
|
|
106
|
+
"[Source](https://github.com/ken-jo/qev)",
|
|
107
|
+
elem_id="intro",
|
|
108
|
+
)
|
|
109
|
+
gr.Markdown(runtime_notice(engine.device))
|
|
110
|
+
with gr.Row(equal_height=False):
|
|
111
|
+
with gr.Column(scale=6):
|
|
112
|
+
example = gr.Dropdown(
|
|
113
|
+
choices=[(value, key) for key, value in LABELS.items()],
|
|
114
|
+
value="support",
|
|
115
|
+
label="Start with an example",
|
|
116
|
+
)
|
|
117
|
+
evidence = gr.Textbox(
|
|
118
|
+
value=initial[0], label="Evidence and context", lines=4, max_lines=8
|
|
119
|
+
)
|
|
120
|
+
image = gr.Image(
|
|
121
|
+
type="pil",
|
|
122
|
+
image_mode="RGB",
|
|
123
|
+
sources=["upload", "clipboard"],
|
|
124
|
+
label="Image (optional)",
|
|
125
|
+
height=250,
|
|
126
|
+
)
|
|
127
|
+
gr.Examples(
|
|
128
|
+
examples=[[str(ROOT / "samples" / name)] for name, _ in SAMPLES],
|
|
129
|
+
inputs=[image],
|
|
130
|
+
label="Sample photographs",
|
|
131
|
+
cache_examples=False,
|
|
132
|
+
)
|
|
133
|
+
gr.Markdown(
|
|
134
|
+
"Photos: TrashNet, Gary Thung & Mindy Yang (MIT). These examples overlap "
|
|
135
|
+
"development data; they are not a benchmark."
|
|
136
|
+
)
|
|
137
|
+
kind = gr.Radio(
|
|
138
|
+
["choice", "score", "noul"], value=initial[3], label="Decision type"
|
|
139
|
+
)
|
|
140
|
+
question = gr.Textbox(value=initial[2], label="Question and decision rule", lines=2)
|
|
141
|
+
criteria = gr.Textbox(
|
|
142
|
+
value=initial[4], label="Candidate descriptions / score levels", lines=5
|
|
143
|
+
)
|
|
144
|
+
gr.Markdown(
|
|
145
|
+
"**choice:** `label | description`, one per line. "
|
|
146
|
+
"**score:** one description per line, from level 0 upward. "
|
|
147
|
+
"**noul:** use `false | ...` and `true | ...`."
|
|
148
|
+
)
|
|
149
|
+
resolution = gr.Dropdown(
|
|
150
|
+
["Original", "64", "128", "192", "256", "384", "512", "768", "1024"],
|
|
151
|
+
value="Original",
|
|
152
|
+
label="Image long edge (pixels)",
|
|
153
|
+
info=(
|
|
154
|
+
"Resize without cropping or upscaling. "
|
|
155
|
+
"The model then applies its trained pixel budget."
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
run = gr.Button("Run decision", variant="primary", elem_id="run-button")
|
|
159
|
+
with gr.Column(scale=5):
|
|
160
|
+
summary = gr.Textbox(label="Decision", lines=6, interactive=False)
|
|
161
|
+
distribution = gr.Label(label="Candidate probabilities", num_top_classes=16)
|
|
162
|
+
gr.Markdown(
|
|
163
|
+
"Probabilities are model estimates, not verified accuracy. "
|
|
164
|
+
"An abstained prediction needs review. "
|
|
165
|
+
"Score is an expected level, not a confidence percentage."
|
|
166
|
+
)
|
|
167
|
+
with gr.Accordion("Request and response JSON", open=False):
|
|
168
|
+
request = gr.Code(label="Request", language="json", interactive=False)
|
|
169
|
+
response = gr.JSON(label="Response")
|
|
170
|
+
with gr.Accordion("Scope and limitations", open=False):
|
|
171
|
+
gr.Markdown(
|
|
172
|
+
"One image, one question and 2–16 candidates per demo request. "
|
|
173
|
+
"The full API supports up to four questions. OCR and spatial reasoning "
|
|
174
|
+
"are weak; 2048 experiments produced no wins. English is the main "
|
|
175
|
+
"evaluated language. Inputs are processed on the machine running QEV. "
|
|
176
|
+
"Per-request image files are removed after inference; cached uploads "
|
|
177
|
+
"expire after approximately 10 minutes."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def clear_results():
|
|
181
|
+
return "Inputs changed. Run a decision to see the new result.", None, None, None
|
|
182
|
+
|
|
183
|
+
result_outputs = [summary, distribution, request, response]
|
|
184
|
+
example.change(
|
|
185
|
+
preset_values,
|
|
186
|
+
[example],
|
|
187
|
+
[evidence, image, question, kind, criteria],
|
|
188
|
+
queue=False,
|
|
189
|
+
api_name=False,
|
|
190
|
+
).then(clear_results, [], result_outputs, queue=False, api_name=False)
|
|
191
|
+
kind.input(default_criteria, [kind], [criteria], queue=False, api_name=False)
|
|
192
|
+
for control in (evidence, image, question, kind, criteria, resolution):
|
|
193
|
+
control.input(clear_results, [], result_outputs, queue=False, api_name=False)
|
|
194
|
+
run.click(
|
|
195
|
+
predict,
|
|
196
|
+
[evidence, image, question, kind, criteria, resolution],
|
|
197
|
+
[summary, distribution, request, response],
|
|
198
|
+
api_name="predict",
|
|
199
|
+
concurrency_limit=1,
|
|
200
|
+
concurrency_id="model",
|
|
201
|
+
)
|
|
202
|
+
gr.Markdown(community_markdown())
|
|
203
|
+
return demo
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def launch(
|
|
207
|
+
*,
|
|
208
|
+
checkpoint=None,
|
|
209
|
+
device="auto",
|
|
210
|
+
cache_dir=None,
|
|
211
|
+
offline=False,
|
|
212
|
+
host="127.0.0.1",
|
|
213
|
+
port=7860,
|
|
214
|
+
inbrowser=False,
|
|
215
|
+
):
|
|
216
|
+
logging.basicConfig(level=logging.INFO)
|
|
217
|
+
|
|
218
|
+
def progress(message):
|
|
219
|
+
print(message, file=sys.stderr, flush=True)
|
|
220
|
+
|
|
221
|
+
print("QEV local playground. Optional: star or support QEV at https://github.com/ken-jo/qev")
|
|
222
|
+
engine = DemoEngine(
|
|
223
|
+
device=device,
|
|
224
|
+
checkpoint=checkpoint,
|
|
225
|
+
cache_dir=cache_dir,
|
|
226
|
+
offline=offline,
|
|
227
|
+
).load(progress=progress)
|
|
228
|
+
demo = build_demo(engine)
|
|
229
|
+
demo.queue(max_size=8, default_concurrency_limit=1)
|
|
230
|
+
demo.launch(
|
|
231
|
+
server_name=host,
|
|
232
|
+
server_port=port,
|
|
233
|
+
inbrowser=inbrowser,
|
|
234
|
+
share=False,
|
|
235
|
+
show_error=False,
|
|
236
|
+
max_file_size="10mb",
|
|
237
|
+
css=CSS,
|
|
238
|
+
theme=gr.themes.Base(primary_hue="blue"),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
if __name__ == "__main__":
|
|
243
|
+
launch()
|
qev/playground/engine.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Public demo adapter around the released, unchanged decision runtime."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
import time
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
from PIL import Image, ImageOps
|
|
13
|
+
|
|
14
|
+
from qev import DecisionRequest
|
|
15
|
+
from qev.runtime import load_model, resolve_cache, resolve_device
|
|
16
|
+
|
|
17
|
+
MAX_IMAGE_PIXELS = 16_000_000
|
|
18
|
+
RESOLUTIONS = (64, 128, 192, 256, 384, 512, 768, 1024)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_request(text, question, kind, criteria, has_image):
|
|
22
|
+
if len(text) > 6000 or len(question) > 1000 or len(criteria) > 6000:
|
|
23
|
+
raise ValueError("Please shorten the evidence, question or candidate descriptions.")
|
|
24
|
+
lines = [line.strip() for line in criteria.splitlines() if line.strip()]
|
|
25
|
+
if kind == "score":
|
|
26
|
+
choices = lines
|
|
27
|
+
elif kind in {"choice", "noul"}:
|
|
28
|
+
choices = {}
|
|
29
|
+
for line in lines:
|
|
30
|
+
if "|" not in line:
|
|
31
|
+
raise ValueError("Use one candidate per line: label | description.")
|
|
32
|
+
label, description = (part.strip() for part in line.split("|", 1))
|
|
33
|
+
if label in choices:
|
|
34
|
+
raise ValueError("Candidate labels must be unique.")
|
|
35
|
+
choices[label] = description
|
|
36
|
+
if kind == "noul" and set(choices) != {"false", "true"}:
|
|
37
|
+
raise ValueError("A noul question needs exactly the labels false and true.")
|
|
38
|
+
else:
|
|
39
|
+
raise ValueError("Choose choice, score or noul.")
|
|
40
|
+
return DecisionRequest.model_validate(
|
|
41
|
+
{
|
|
42
|
+
"state": {"text": text, "images": [{"path": "image.png"}] if has_image else []},
|
|
43
|
+
"questions": {
|
|
44
|
+
"decision": {"type": kind, "instructions": question, "criteria": choices}
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def prepare_image(image, resolution):
|
|
51
|
+
if image is None:
|
|
52
|
+
return None, "No image"
|
|
53
|
+
if image.width * image.height > MAX_IMAGE_PIXELS:
|
|
54
|
+
raise ValueError("Use an image of at most 16 megapixels.")
|
|
55
|
+
if resolution != "Original" and int(resolution) not in RESOLUTIONS:
|
|
56
|
+
raise ValueError("Choose one of the available image resolutions.")
|
|
57
|
+
prepared = ImageOps.exif_transpose(image).convert("RGB")
|
|
58
|
+
source_size = prepared.size
|
|
59
|
+
if resolution != "Original":
|
|
60
|
+
edge = int(resolution)
|
|
61
|
+
prepared.thumbnail((edge, edge), Image.Resampling.LANCZOS)
|
|
62
|
+
return prepared, f"{source_size[0]} x {source_size[1]} -> {prepared.width} x {prepared.height}"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DemoEngine:
|
|
66
|
+
def __init__(self, device="auto", checkpoint=None, cache_dir=None, offline=False):
|
|
67
|
+
self.device = resolve_device(device)
|
|
68
|
+
self.checkpoint = checkpoint
|
|
69
|
+
self.cache_dir = resolve_cache(cache_dir)
|
|
70
|
+
self.offline = offline or os.environ.get("QEV_OFFLINE") == "1"
|
|
71
|
+
self.model = None
|
|
72
|
+
|
|
73
|
+
def load(self, progress=None):
|
|
74
|
+
torch.set_num_threads(4)
|
|
75
|
+
self.model = load_model(
|
|
76
|
+
self.checkpoint,
|
|
77
|
+
device=self.device,
|
|
78
|
+
cache_dir=self.cache_dir,
|
|
79
|
+
offline=self.offline,
|
|
80
|
+
progress=progress,
|
|
81
|
+
)
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def predict(self, text, image, question, kind, criteria, resolution="Original"):
|
|
85
|
+
if self.model is None:
|
|
86
|
+
raise RuntimeError("The model is not ready yet. Please try again shortly.")
|
|
87
|
+
request = build_request(text, question, kind, criteria, image is not None)
|
|
88
|
+
prepared, geometry = prepare_image(image, resolution)
|
|
89
|
+
try:
|
|
90
|
+
with tempfile.TemporaryDirectory(prefix="qev-") as temporary:
|
|
91
|
+
if prepared is not None:
|
|
92
|
+
prepared.save(Path(temporary) / "image.png")
|
|
93
|
+
if self.device == "cuda":
|
|
94
|
+
torch.cuda.synchronize()
|
|
95
|
+
start = time.perf_counter()
|
|
96
|
+
result = self.model.predict(request, image_root=Path(temporary))
|
|
97
|
+
if self.device == "cuda":
|
|
98
|
+
torch.cuda.synchronize()
|
|
99
|
+
elapsed = (time.perf_counter() - start) * 1000
|
|
100
|
+
finally:
|
|
101
|
+
if prepared is not None:
|
|
102
|
+
prepared.close()
|
|
103
|
+
answer = result["answers"]["decision"]
|
|
104
|
+
probabilities = answer["probabilities"]
|
|
105
|
+
top = max(probabilities, key=probabilities.get)
|
|
106
|
+
if kind == "score":
|
|
107
|
+
expected = sum(float(key) * value for key, value in probabilities.items())
|
|
108
|
+
decision = f"Expected score: {expected:.3f} (levels start at 0)"
|
|
109
|
+
elif kind == "noul":
|
|
110
|
+
decision = f"Probability true: {probabilities['true']:.1%}"
|
|
111
|
+
else:
|
|
112
|
+
decision = f"Top candidate: {top} ({probabilities[top]:.1%})"
|
|
113
|
+
status = "ABSTAINED - review the evidence" if answer["abstained"] else "Answer accepted"
|
|
114
|
+
summary = (
|
|
115
|
+
f"{status}\n{decision}\n"
|
|
116
|
+
f"Model computation: {elapsed:.0f} ms on {self.device.upper()}\n"
|
|
117
|
+
f"Image: {geometry}\nQueue, upload and network time are excluded."
|
|
118
|
+
)
|
|
119
|
+
result["demo"] = {"device": self.device, "model_ms": round(elapsed, 2), "image": geometry}
|
|
120
|
+
# Keep schema paths as JSON text: Gradio clients otherwise interpret them as files.
|
|
121
|
+
return summary, probabilities, json.dumps(request.model_dump(), indent=2), result
|