paxmod-guard 0.1.0__tar.gz
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.
- paxmod_guard-0.1.0/.gitignore +36 -0
- paxmod_guard-0.1.0/PKG-INFO +56 -0
- paxmod_guard-0.1.0/README.md +43 -0
- paxmod_guard-0.1.0/paxmod_guard/__init__.py +219 -0
- paxmod_guard-0.1.0/pyproject.toml +14 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Python-generated files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[oc]
|
|
4
|
+
build/
|
|
5
|
+
dist/
|
|
6
|
+
wheels/
|
|
7
|
+
*.egg-info
|
|
8
|
+
|
|
9
|
+
# Virtual environments
|
|
10
|
+
.venv
|
|
11
|
+
.aws-ml.env
|
|
12
|
+
.aws-ml.instance
|
|
13
|
+
.venv/
|
|
14
|
+
train/models/
|
|
15
|
+
train/data/
|
|
16
|
+
bench/data/external/
|
|
17
|
+
.gcp-ml.env
|
|
18
|
+
.ml-ledger
|
|
19
|
+
logs/
|
|
20
|
+
.hf-token
|
|
21
|
+
js/node_modules/
|
|
22
|
+
|
|
23
|
+
# Large or regenerable outputs (labelled data, per-row results); suite files hold non-commercial benchmark text
|
|
24
|
+
label/out/
|
|
25
|
+
label/relabel/
|
|
26
|
+
label/*.log
|
|
27
|
+
bench/results/
|
|
28
|
+
bench/data/suite*.jsonl
|
|
29
|
+
*.tgz
|
|
30
|
+
.ml-ledger-pending
|
|
31
|
+
data_private/
|
|
32
|
+
|
|
33
|
+
# v10 raw source downloads (licence-checked public data; regenerable)
|
|
34
|
+
data_raw/
|
|
35
|
+
data_pool/
|
|
36
|
+
release/
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: paxmod-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Local safety classifier for AI companions, chatbots and kids' game chat: self-harm, grooming, sexual content, jailbreaks and more, on your own CPU in milliseconds.
|
|
5
|
+
Author: Paxmod
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Keywords: ai-safety,chatbot,child-safety,companion,guardrails,llm,moderation,onnx
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Requires-Dist: onnxruntime>=1.17
|
|
11
|
+
Requires-Dist: tokenizers>=0.15
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# paxmod-guard
|
|
15
|
+
|
|
16
|
+
Safety checks for AI companions, chatbots and kids' game chat, running on your own CPU. No API key, no network calls
|
|
17
|
+
after the first model download, and no message ever leaves your server. The Python twin of the
|
|
18
|
+
[`@paxmod/guard`](https://www.npmjs.com/package/@paxmod/guard) npm package: same model, same scores.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install paxmod-guard
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from paxmod_guard import Guard
|
|
26
|
+
|
|
27
|
+
guard = Guard() # downloads the model once (about 220 MB) to ~/.cache/paxmod-guard
|
|
28
|
+
|
|
29
|
+
# 1. Check what the user sent before your model sees it
|
|
30
|
+
r = guard.check_input(user_message)
|
|
31
|
+
if r.crisis_reply:
|
|
32
|
+
return r.crisis_reply # self-harm risk: answer with crisis support, not the persona
|
|
33
|
+
if "jailbreak" in r.labels:
|
|
34
|
+
return "Let's keep things friendly."
|
|
35
|
+
|
|
36
|
+
# 2. Check what your bot wrote before the user sees it
|
|
37
|
+
reply = llm.generate(user_message)
|
|
38
|
+
if guard.check_output(reply).flagged:
|
|
39
|
+
return regenerate_or_refuse()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Needs about 1 GB of memory: run it in a container or on a server, not in small serverless functions.
|
|
43
|
+
|
|
44
|
+
Game or community chat, with the messages before it, or a whole conversation (grooming and other patterns that no
|
|
45
|
+
single message shows):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
guard.check_chat("wanna add me on snap", context=[{"role": "player", "speaker": "B", "text": "im 12"}])
|
|
49
|
+
guard.check_conversation([{"role": "player", "speaker": "A", "text": "..."}, ...])
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Each result has `flagged`, `labels`, `scores` (every label, 0 to 1) and `latency_ms`. Signal labels (grooming tactics,
|
|
53
|
+
distress, swearing, age questions, romance) are always scored but only flag when enabled:
|
|
54
|
+
`Guard(enable=["secrecy", "risk_check"])`. Thresholds can be overridden per label: `Guard(thresholds={"sexual": 0.7})`.
|
|
55
|
+
|
|
56
|
+
Licence: Apache-2.0. Model card and labels: https://huggingface.co/paxmod/guard
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# paxmod-guard
|
|
2
|
+
|
|
3
|
+
Safety checks for AI companions, chatbots and kids' game chat, running on your own CPU. No API key, no network calls
|
|
4
|
+
after the first model download, and no message ever leaves your server. The Python twin of the
|
|
5
|
+
[`@paxmod/guard`](https://www.npmjs.com/package/@paxmod/guard) npm package: same model, same scores.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install paxmod-guard
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from paxmod_guard import Guard
|
|
13
|
+
|
|
14
|
+
guard = Guard() # downloads the model once (about 220 MB) to ~/.cache/paxmod-guard
|
|
15
|
+
|
|
16
|
+
# 1. Check what the user sent before your model sees it
|
|
17
|
+
r = guard.check_input(user_message)
|
|
18
|
+
if r.crisis_reply:
|
|
19
|
+
return r.crisis_reply # self-harm risk: answer with crisis support, not the persona
|
|
20
|
+
if "jailbreak" in r.labels:
|
|
21
|
+
return "Let's keep things friendly."
|
|
22
|
+
|
|
23
|
+
# 2. Check what your bot wrote before the user sees it
|
|
24
|
+
reply = llm.generate(user_message)
|
|
25
|
+
if guard.check_output(reply).flagged:
|
|
26
|
+
return regenerate_or_refuse()
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Needs about 1 GB of memory: run it in a container or on a server, not in small serverless functions.
|
|
30
|
+
|
|
31
|
+
Game or community chat, with the messages before it, or a whole conversation (grooming and other patterns that no
|
|
32
|
+
single message shows):
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
guard.check_chat("wanna add me on snap", context=[{"role": "player", "speaker": "B", "text": "im 12"}])
|
|
36
|
+
guard.check_conversation([{"role": "player", "speaker": "A", "text": "..."}, ...])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Each result has `flagged`, `labels`, `scores` (every label, 0 to 1) and `latency_ms`. Signal labels (grooming tactics,
|
|
40
|
+
distress, swearing, age questions, romance) are always scored but only flag when enabled:
|
|
41
|
+
`Guard(enable=["secrecy", "risk_check"])`. Thresholds can be overridden per label: `Guard(thresholds={"sexual": 0.7})`.
|
|
42
|
+
|
|
43
|
+
Licence: Apache-2.0. Model card and labels: https://huggingface.co/paxmod/guard
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""paxmod-guard: a local safety classifier for AI companions, chatbots and kids' game chat.
|
|
2
|
+
|
|
3
|
+
Runs Paxmod's Guard model on your own CPU with ONNX Runtime. No API calls; no message leaves the machine.
|
|
4
|
+
A Python port of the @paxmod/guard npm SDK: same model files, same input format, same scores.
|
|
5
|
+
|
|
6
|
+
from paxmod_guard import Guard
|
|
7
|
+
guard = Guard() # downloads the model once (about 220 MB) to ~/.cache/paxmod-guard
|
|
8
|
+
r = guard.check_input(user_message)
|
|
9
|
+
if r.crisis_reply: ... # self-harm risk: answer with crisis support, not the persona
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import math
|
|
15
|
+
import re
|
|
16
|
+
import os
|
|
17
|
+
import time
|
|
18
|
+
import urllib.request
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable, Iterable, Optional
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import onnxruntime as ort
|
|
25
|
+
from tokenizers import Tokenizer
|
|
26
|
+
|
|
27
|
+
__all__ = ["Guard", "Result", "download_model", "MODEL_VERSION", "DEFAULT_CRISIS_REPLY"]
|
|
28
|
+
|
|
29
|
+
MODEL_VERSION = "paxmodguard-v1"
|
|
30
|
+
MODEL_FILES = ["model.int8.onnx", "tokenizer.json", "tokenizer_config.json", "guard.json"]
|
|
31
|
+
DEFAULT_MODEL_URL = "https://huggingface.co/paxmod/guard/resolve/main"
|
|
32
|
+
DEFAULT_CRISIS_REPLY = (
|
|
33
|
+
"It sounds like you're going through something really painful, and I'm glad you told me. "
|
|
34
|
+
"I'm an AI, so please reach out to someone who can help right now. In the US you can call or text 988 "
|
|
35
|
+
"(Suicide & Crisis Lifeline). Elsewhere, findahelpline.com lists free, confidential lines in your country."
|
|
36
|
+
)
|
|
37
|
+
# v10 renamed self_harm to self_harm_intent; either triggers the crisis reply.
|
|
38
|
+
CRISIS = ("self_harm_intent", "self_harm")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _cache_dir() -> Path:
|
|
42
|
+
if os.environ.get("PAXMOD_GUARD_CACHE"):
|
|
43
|
+
return Path(os.environ["PAXMOD_GUARD_CACHE"])
|
|
44
|
+
return Path(os.environ.get("XDG_CACHE_HOME") or Path.home() / ".cache") / "paxmod-guard"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def download_model(cache_dir: Optional[str] = None, model_url: Optional[str] = None,
|
|
48
|
+
on_progress: Optional[Callable[[str], None]] = None) -> Path:
|
|
49
|
+
"""Download the model files if they are not there yet. Returns the model directory."""
|
|
50
|
+
url = model_url or os.environ.get("PAXMOD_GUARD_MODEL_URL") or DEFAULT_MODEL_URL
|
|
51
|
+
d = Path(cache_dir) if cache_dir else _cache_dir() / MODEL_VERSION
|
|
52
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
for f in MODEL_FILES:
|
|
54
|
+
dest = d / f
|
|
55
|
+
if dest.exists():
|
|
56
|
+
continue
|
|
57
|
+
if on_progress:
|
|
58
|
+
on_progress(f"Downloading {f}")
|
|
59
|
+
part = dest.with_suffix(dest.suffix + f".{os.getpid()}.part")
|
|
60
|
+
with urllib.request.urlopen(f"{url}/{MODEL_VERSION}/{f}") as res, open(part, "wb") as out:
|
|
61
|
+
while chunk := res.read(1 << 20):
|
|
62
|
+
out.write(chunk)
|
|
63
|
+
part.rename(dest) # atomic: a half-written file is never mistaken for a finished one
|
|
64
|
+
return d
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Result:
|
|
69
|
+
flagged: bool
|
|
70
|
+
labels: list[str]
|
|
71
|
+
scores: dict[str, float]
|
|
72
|
+
latency_ms: float
|
|
73
|
+
direction: Optional[str] = None
|
|
74
|
+
crisis_reply: Optional[str] = None
|
|
75
|
+
turns: Optional[int] = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class _Stats:
|
|
80
|
+
checks: int = 0
|
|
81
|
+
flagged: int = 0
|
|
82
|
+
crisis_referrals: int = 0
|
|
83
|
+
since: str = field(default_factory=lambda: time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Guard:
|
|
87
|
+
"""Load the Guard model. Downloads it on first use unless `model_dir` points at local files.
|
|
88
|
+
|
|
89
|
+
enable: signal labels (grooming tactics, distress, swearing, age questions, romance) to count towards flags;
|
|
90
|
+
they are always scored. include_unsafe: also count the general "unsafe" score.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, model_dir: Optional[str] = None, *, enable: Iterable[str] = (), include_unsafe: bool = False,
|
|
94
|
+
thresholds: Optional[dict[str, float]] = None, threads: int = 2, window_tokens: int = 128,
|
|
95
|
+
crisis_reply: str = DEFAULT_CRISIS_REPLY, on_crisis: Optional[Callable[[Result], None]] = None):
|
|
96
|
+
d = Path(model_dir) if model_dir else download_model()
|
|
97
|
+
meta = json.loads((d / "guard.json").read_text())
|
|
98
|
+
self.labels: list[str] = meta["labels"]
|
|
99
|
+
self.model: str = meta.get("version", MODEL_VERSION)
|
|
100
|
+
self.thresholds = {**meta.get("thresholds", {}), **(thresholds or {})}
|
|
101
|
+
self.max_length = meta.get("max_length", 256)
|
|
102
|
+
self.window_tokens = window_tokens
|
|
103
|
+
self.crisis_reply, self.on_crisis = crisis_reply, on_crisis
|
|
104
|
+
on = set(enable) | ({"unsafe"} if include_unsafe else set())
|
|
105
|
+
self._off = {l for l in meta.get("default_off", ["unsafe"]) if l not in on}
|
|
106
|
+
self.tokenizer = Tokenizer.from_file(str(d / "tokenizer.json"))
|
|
107
|
+
so = ort.SessionOptions(); so.intra_op_num_threads = threads
|
|
108
|
+
self.session = ort.InferenceSession(str(d / "model.int8.onnx"), so, providers=["CPUExecutionProvider"])
|
|
109
|
+
self.contextual = "grooming" in self.labels
|
|
110
|
+
self._stats = _Stats()
|
|
111
|
+
|
|
112
|
+
# Long messages are split into overlapping windows at two sizes, full-length for context and short so one bad
|
|
113
|
+
# sentence in a long message is not drowned out. A label's score is its highest across all windows.
|
|
114
|
+
def _windows(self, text: str, short: bool) -> list[list[int]]:
|
|
115
|
+
ids = self.tokenizer.encode(re.sub(" {2,}", " ", text)).ids # as the npm SDK (its tokenizer splits space runs differently)
|
|
116
|
+
cls, sep, body = ids[0], ids[-1], ids[1:-1]
|
|
117
|
+
sizes = [self.max_length - 2]
|
|
118
|
+
small = min(self.max_length, self.window_tokens) - 2
|
|
119
|
+
if short and small < sizes[0] and len(body) > small:
|
|
120
|
+
sizes.append(small)
|
|
121
|
+
out = []
|
|
122
|
+
for size in sizes:
|
|
123
|
+
stride = int(size * 0.75)
|
|
124
|
+
i = 0
|
|
125
|
+
while True:
|
|
126
|
+
out.append([cls, *body[i:i + size], sep])
|
|
127
|
+
if i + size >= len(body):
|
|
128
|
+
break
|
|
129
|
+
i += stride
|
|
130
|
+
return out
|
|
131
|
+
|
|
132
|
+
def _score(self, texts: list[str], short: bool = True) -> dict[str, float]:
|
|
133
|
+
wins = sorted((w for t in texts for w in self._windows(t, short)), key=len)
|
|
134
|
+
best = np.full(len(self.labels), -np.inf)
|
|
135
|
+
for k in range(0, len(wins), 8): # small batches of similar length: little padding
|
|
136
|
+
batch = wins[k:k + 8]; n = len(batch[-1])
|
|
137
|
+
ids = np.zeros((len(batch), n), dtype=np.int64); mask = np.zeros_like(ids)
|
|
138
|
+
for b, w in enumerate(batch):
|
|
139
|
+
ids[b, :len(w)] = w; mask[b, :len(w)] = 1
|
|
140
|
+
logits = self.session.run(["logits"], {"input_ids": ids, "attention_mask": mask})[0]
|
|
141
|
+
best = np.maximum(best, logits.max(0))
|
|
142
|
+
return {l: 1 / (1 + math.exp(-float(v))) for l, v in zip(self.labels, best)}
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _cut(t: str, n: int = 300) -> str:
|
|
146
|
+
return t if len(t) <= n else t[:n] + "…" # as in training: 300 for earlier turns
|
|
147
|
+
|
|
148
|
+
@staticmethod
|
|
149
|
+
def _tag(turn: dict) -> str:
|
|
150
|
+
role = turn.get("role") or {"output": "ai", "chat": "player"}.get(turn.get("direction"), "user")
|
|
151
|
+
if role == "ai": return "[AI]"
|
|
152
|
+
if role == "player": return f"[CHAT:{turn['speaker']}]" if turn.get("speaker") else "[CHAT]"
|
|
153
|
+
return "[USER]"
|
|
154
|
+
|
|
155
|
+
def _with_context(self, text: str, direction: str, context: list[dict]) -> str:
|
|
156
|
+
target = f"[TARGET] {self._tag({'direction': direction})} {self._cut(text, 1000)}" # the judged turn: 1000, as in training
|
|
157
|
+
earlier = [f"{self._tag(t)} {self._cut(t.get('text') or '')}" for t in context]
|
|
158
|
+
for k in range(len(earlier) + 1):
|
|
159
|
+
s = " ".join(earlier[k:] + [target])
|
|
160
|
+
if k == len(earlier) or len(self.tokenizer.encode(s).ids) <= self.max_length:
|
|
161
|
+
return s
|
|
162
|
+
return target
|
|
163
|
+
|
|
164
|
+
def _flags(self, scores: dict[str, float]) -> list[str]:
|
|
165
|
+
return [l for l in self.labels if l not in self._off and scores[l] >= self.thresholds.get(l, 0.5)]
|
|
166
|
+
|
|
167
|
+
def check(self, text: str, direction: str = "input", context: Optional[list[dict]] = None) -> Result:
|
|
168
|
+
"""direction: "input" = a person writing to an AI, "output" = the AI, "chat" = a person writing to other people.
|
|
169
|
+
context: earlier turns, e.g. [{"role": "player", "speaker": "B", "text": "..."}] (role user / ai / player)."""
|
|
170
|
+
if not isinstance(text, str):
|
|
171
|
+
raise TypeError("paxmod_guard: text must be a string")
|
|
172
|
+
t0 = time.perf_counter()
|
|
173
|
+
if not self.contextual:
|
|
174
|
+
inp = f"{'[AI]' if direction == 'output' else '[USER]'} {text}"
|
|
175
|
+
elif context:
|
|
176
|
+
inp = self._with_context(text, direction, context)
|
|
177
|
+
else:
|
|
178
|
+
inp = f"{'[CHAT]' if direction == 'chat' else self._tag({'direction': direction})} {text}" # read whole, in windows
|
|
179
|
+
scores = self._score([inp], not (self.contextual and context)) if text.strip() else {l: 0.0 for l in self.labels}
|
|
180
|
+
flags = self._flags(scores)
|
|
181
|
+
r = Result(bool(flags), flags, scores, round((time.perf_counter() - t0) * 1000, 1), direction)
|
|
182
|
+
self._stats.checks += 1; self._stats.flagged += bool(flags)
|
|
183
|
+
if direction != "output" and any(l in flags for l in CRISIS):
|
|
184
|
+
r.crisis_reply = self.crisis_reply; self._stats.crisis_referrals += 1
|
|
185
|
+
if self.on_crisis: self.on_crisis(r)
|
|
186
|
+
return r
|
|
187
|
+
|
|
188
|
+
def check_input(self, text: str, context: Optional[list[dict]] = None) -> Result:
|
|
189
|
+
"""Check a message from the user before your bot sees it."""
|
|
190
|
+
return self.check(text, "input", context)
|
|
191
|
+
|
|
192
|
+
def check_output(self, text: str, context: Optional[list[dict]] = None) -> Result:
|
|
193
|
+
"""Check your bot's reply before the user sees it."""
|
|
194
|
+
return self.check(text, "output", context)
|
|
195
|
+
|
|
196
|
+
def check_chat(self, text: str, context: Optional[list[dict]] = None) -> Result:
|
|
197
|
+
"""Check a message one person wrote to others (game or community chat)."""
|
|
198
|
+
return self.check(text, "chat", context)
|
|
199
|
+
|
|
200
|
+
def check_conversation(self, turns: list[dict]) -> Result:
|
|
201
|
+
"""Judge a whole stretch of conversation, e.g. a DM with both sides, including patterns no single message
|
|
202
|
+
shows (grooming). Read as in training: windows of up to 40 turns, overlapping by half."""
|
|
203
|
+
if not self.contextual:
|
|
204
|
+
raise RuntimeError("paxmod_guard: check_conversation needs a v9 or later model")
|
|
205
|
+
t0 = time.perf_counter()
|
|
206
|
+
parts, i = [], 0
|
|
207
|
+
while True:
|
|
208
|
+
parts.append("[CONVERSATION] " + " ".join(f"{self._tag(t)} {self._cut(t.get('text') or '')}" for t in turns[i:i + 40]))
|
|
209
|
+
if i + 40 >= len(turns):
|
|
210
|
+
break
|
|
211
|
+
i += 20
|
|
212
|
+
scores = self._score(parts, False)
|
|
213
|
+
flags = self._flags(scores)
|
|
214
|
+
self._stats.checks += 1; self._stats.flagged += bool(flags)
|
|
215
|
+
return Result(bool(flags), flags, scores, round((time.perf_counter() - t0) * 1000, 1), turns=len(turns))
|
|
216
|
+
|
|
217
|
+
def stats(self) -> dict:
|
|
218
|
+
"""Counts since this guard was created: checks, flagged messages, crisis referrals (e.g. for SB 243 reports)."""
|
|
219
|
+
return dict(self._stats.__dict__)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "paxmod-guard"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Local safety classifier for AI companions, chatbots and kids' game chat: self-harm, grooming, sexual content, jailbreaks and more, on your own CPU in milliseconds."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "Apache-2.0"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
authors = [{ name = "Paxmod" }]
|
|
9
|
+
keywords = ["guardrails", "ai-safety", "moderation", "companion", "chatbot", "llm", "onnx", "child-safety"]
|
|
10
|
+
dependencies = ["onnxruntime>=1.17", "tokenizers>=0.15", "numpy>=1.24"]
|
|
11
|
+
|
|
12
|
+
[build-system]
|
|
13
|
+
requires = ["hatchling"]
|
|
14
|
+
build-backend = "hatchling.build"
|