genderfluid-tiny 1.0.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.
@@ -0,0 +1,78 @@
1
+ """genderfluid-tiny: Ultra-tiny name-gender association classifier."""
2
+
3
+ from genderfluid.inference import (
4
+ predict_name,
5
+ predict_names,
6
+ GenderfluidModel,
7
+ )
8
+
9
+
10
+ def classify_name(name: str) -> str:
11
+ """
12
+ Return just the classification string.
13
+
14
+ Returns one of: "girl-associated", "boy-associated", "uncertain"
15
+
16
+ Example::
17
+
18
+ from genderfluid import classify_name
19
+
20
+ classify_name("Emma") # "girl-associated"
21
+ classify_name("James") # "boy-associated"
22
+ classify_name("Alex") # "uncertain"
23
+ """
24
+ return predict_name(name)["classification"]
25
+
26
+
27
+ def is_girl_name(name: str) -> bool:
28
+ """
29
+ Return True if the name is classified as girl-associated.
30
+
31
+ Example::
32
+
33
+ from genderfluid import is_girl_name
34
+
35
+ if is_girl_name("Emma"):
36
+ print("girl name")
37
+ """
38
+ return predict_name(name)["classification"] == "girl-associated"
39
+
40
+
41
+ def is_boy_name(name: str) -> bool:
42
+ """
43
+ Return True if the name is classified as boy-associated.
44
+
45
+ Example::
46
+
47
+ from genderfluid import is_boy_name
48
+
49
+ if is_boy_name("James"):
50
+ print("boy name")
51
+ """
52
+ return predict_name(name)["classification"] == "boy-associated"
53
+
54
+
55
+ def name_probability(name: str) -> float:
56
+ """
57
+ Return the girl-associated probability as a float between 0 and 1.
58
+
59
+ Example::
60
+
61
+ from genderfluid import name_probability
62
+
63
+ p = name_probability("Emma") # 0.97
64
+ p = name_probability("Alex") # 0.27
65
+ """
66
+ return predict_name(name)["girl_associated_probability"]
67
+
68
+
69
+ __version__ = "1.0.0"
70
+ __all__ = [
71
+ "predict_name",
72
+ "predict_names",
73
+ "GenderfluidModel",
74
+ "classify_name",
75
+ "is_girl_name",
76
+ "is_boy_name",
77
+ "name_probability",
78
+ ]
@@ -0,0 +1,6 @@
1
+ """Allow running as: python -m genderfluid"""
2
+
3
+ from genderfluid.cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,110 @@
1
+ """Probability calibration analysis."""
2
+
3
+ import numpy as np
4
+
5
+
6
+ def calibration_error(
7
+ y_true: np.ndarray,
8
+ y_proba: np.ndarray,
9
+ n_bins: int = 10,
10
+ ) -> float:
11
+ """
12
+ Compute Expected Calibration Error (ECE).
13
+
14
+ Args:
15
+ y_true: True class labels
16
+ y_proba: Predicted probabilities
17
+ n_bins: Number of bins for calibration
18
+
19
+ Returns:
20
+ Expected calibration error
21
+ """
22
+ bin_boundaries = np.linspace(0, 1, n_bins + 1)
23
+ ece = 0.0
24
+
25
+ for i in range(n_bins):
26
+ lower = bin_boundaries[i]
27
+ upper = bin_boundaries[i + 1]
28
+
29
+ # Find samples in this probability bin
30
+ mask = (y_proba.max(axis=1) >= lower) & (y_proba.max(axis=1) < upper)
31
+ if mask.sum() == 0:
32
+ continue
33
+
34
+ bin_proba = y_proba[mask].max(axis=1)
35
+ bin_true = y_true[mask] == np.argmax(y_proba[mask], axis=1)
36
+
37
+ avg_confidence = bin_proba.mean()
38
+ avg_accuracy = bin_true.mean()
39
+
40
+ ece += mask.sum() / len(y_true) * abs(avg_accuracy - avg_confidence)
41
+
42
+ return float(ece)
43
+
44
+
45
+ def reliability_data(
46
+ y_true: np.ndarray,
47
+ y_proba: np.ndarray,
48
+ n_bins: int = 10,
49
+ ) -> list[dict]:
50
+ """
51
+ Compute reliability diagram data.
52
+
53
+ Returns list of dicts with bin_lower, bin_upper, avg_confidence, avg_accuracy, count.
54
+ """
55
+ bin_boundaries = np.linspace(0, 1, n_bins + 1)
56
+ result = []
57
+
58
+ for i in range(n_bins):
59
+ lower = bin_boundaries[i]
60
+ upper = bin_boundaries[i + 1]
61
+
62
+ mask = (y_proba.max(axis=1) >= lower) & (y_proba.max(axis=1) < upper)
63
+ if mask.sum() == 0:
64
+ result.append({
65
+ "bin_lower": float(lower),
66
+ "bin_upper": float(upper),
67
+ "avg_confidence": float((lower + upper) / 2),
68
+ "avg_accuracy": 0.0,
69
+ "count": 0,
70
+ })
71
+ continue
72
+
73
+ bin_proba = y_proba[mask].max(axis=1)
74
+ bin_true = (y_true[mask] == np.argmax(y_proba[mask], axis=1)).astype(float)
75
+
76
+ result.append({
77
+ "bin_lower": float(lower),
78
+ "bin_upper": float(upper),
79
+ "avg_confidence": float(bin_proba.mean()),
80
+ "avg_accuracy": float(bin_true.mean()),
81
+ "count": int(mask.sum()),
82
+ })
83
+
84
+ return result
85
+
86
+
87
+ def confusion_matrix(
88
+ y_true: np.ndarray,
89
+ y_pred: np.ndarray,
90
+ labels: list[str],
91
+ ) -> dict:
92
+ """
93
+ Compute confusion matrix as a dict.
94
+
95
+ Returns:
96
+ {label: {predicted_label: count}}
97
+ """
98
+ n_labels = len(labels)
99
+ matrix = np.zeros((n_labels, n_labels), dtype=int)
100
+
101
+ for true, pred in zip(y_true, y_pred):
102
+ matrix[true, pred] += 1
103
+
104
+ result = {}
105
+ for i, true_label in enumerate(labels):
106
+ result[true_label] = {}
107
+ for j, pred_label in enumerate(labels):
108
+ result[true_label][pred_label] = int(matrix[i, j])
109
+
110
+ return result
@@ -0,0 +1,181 @@
1
+ """Logistic regression classifier with lightweight probability calibration."""
2
+
3
+ import numpy as np
4
+ from sklearn.linear_model import LogisticRegression
5
+ from typing import Optional
6
+
7
+
8
+ LABELS = ["girl-associated", "boy-associated", "uncertain"]
9
+ LABEL_TO_IDX = {label: i for i, label in enumerate(LABELS)}
10
+ NUM_CLASSES = 3
11
+ MIN_EXAMPLES_PER_CLASS = 5
12
+
13
+
14
+ def _sigmoid(x):
15
+ return 1.0 / (1.0 + np.exp(-np.clip(x, -30, 30)))
16
+
17
+
18
+ class NameClassifier:
19
+ """
20
+ Logistic regression classifier for name-gender association.
21
+
22
+ Uses lightweight sigmoid (Platt) calibration instead of
23
+ CalibratedClassifierCV to keep memory usage minimal.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ C: float = 1.0,
29
+ max_iter: int = 1000,
30
+ min_confidence: float = 0.70,
31
+ ):
32
+ self.C = C
33
+ self.max_iter = max_iter
34
+ self.min_confidence = min_confidence
35
+ self.model: Optional[LogisticRegression] = None
36
+ # Per-class sigmoid calibration parameters: A, B for sigmoid(A*raw + B)
37
+ self.calib_A: Optional[np.ndarray] = None
38
+ self.calib_B: Optional[np.ndarray] = None
39
+
40
+ def train(
41
+ self,
42
+ X: np.ndarray,
43
+ y: np.ndarray,
44
+ sample_weight: Optional[np.ndarray] = None,
45
+ ) -> None:
46
+ """Train the classifier with all 3 classes guaranteed."""
47
+ rng = np.random.RandomState(42)
48
+ unique_classes = set(y.tolist())
49
+ missing = set(range(NUM_CLASSES)) - unique_classes
50
+
51
+ X_train = X
52
+ y_train = y.copy()
53
+ sw_train = sample_weight.copy() if sample_weight is not None else None
54
+
55
+ for cls in sorted(missing):
56
+ n_pad = MIN_EXAMPLES_PER_CLASS
57
+ X_pad = rng.randn(n_pad, X.shape[1]).astype(np.float32) * 0.001
58
+ y_pad = np.full(n_pad, cls, dtype=int)
59
+ X_train = np.vstack([X_train, X_pad])
60
+ y_train = np.concatenate([y_train, y_pad])
61
+ if sw_train is not None:
62
+ w_pad = np.full(n_pad, 0.001)
63
+ sw_train = np.concatenate([sw_train, w_pad])
64
+ else:
65
+ sw_train = np.concatenate([
66
+ np.ones(len(y), dtype=np.float32),
67
+ np.full(n_pad, 0.001, dtype=np.float32),
68
+ ])
69
+
70
+ self.model = LogisticRegression(
71
+ C=self.C,
72
+ max_iter=self.max_iter,
73
+ solver="lbfgs",
74
+ random_state=42,
75
+ )
76
+ self.model.fit(X_train, y_train, sample_weight=sw_train)
77
+
78
+ # Lightweight Platt scaling calibration on a holdout from training data
79
+ n = len(y_train)
80
+ cal_size = min(2000, n // 5)
81
+ idx = rng.choice(n, cal_size, replace=False)
82
+ X_cal, y_cal = X_train[idx], y_train[idx]
83
+
84
+ raw_proba = self.model.predict_proba(X_cal)
85
+ # Fit per-class sigmoid: P(y=c|raw) = sigmoid(A * logit(raw) + B)
86
+ self.calib_A = np.ones(NUM_CLASSES, dtype=np.float32)
87
+ self.calib_B = np.zeros(NUM_CLASSES, dtype=np.float32)
88
+
89
+ for c in range(NUM_CLASSES):
90
+ targets = (y_cal == c).astype(np.float32)
91
+ if targets.sum() < 5 or (1 - targets).sum() < 5:
92
+ continue
93
+ raw = raw_proba[:, c].clip(1e-7, 1 - 1e-7)
94
+ logit = np.log(raw / (1 - raw))
95
+ # Simple linear fit: A, B via least squares
96
+ # Platt scaling: minimize targets * log(sigmoid(A*l+B)) + (1-targets) * log(1-sigmoid(A*l+B))
97
+ # Use a robust 2-parameter fit
98
+ best_A, best_B = 1.0, 0.0
99
+ best_loss = float("inf")
100
+ for A_try in [0.5, 1.0, 1.5, 2.0]:
101
+ for B_try in [-1.0, -0.5, 0.0, 0.5, 1.0]:
102
+ s = _sigmoid(A_try * logit + B_try)
103
+ s = s.clip(1e-7, 1 - 1e-7)
104
+ loss = -np.mean(targets * np.log(s) + (1 - targets) * np.log(1 - s))
105
+ if loss < best_loss:
106
+ best_loss = loss
107
+ best_A, best_B = A_try, B_try
108
+ self.calib_A[c] = best_A
109
+ self.calib_B[c] = best_B
110
+
111
+ del X_cal, y_cal, raw_proba
112
+ import gc
113
+ gc.collect()
114
+
115
+ def _calibrate_proba(self, raw_proba: np.ndarray) -> np.ndarray:
116
+ """Apply sigmoid calibration to raw probabilities."""
117
+ if self.calib_A is None:
118
+ return raw_proba
119
+
120
+ calibrated = np.zeros_like(raw_proba)
121
+ for c in range(raw_proba.shape[1]):
122
+ raw = raw_proba[:, c].clip(1e-7, 1 - 1e-7)
123
+ logit = np.log(raw / (1 - raw))
124
+ calibrated[:, c] = _sigmoid(self.calib_A[c] * logit + self.calib_B[c])
125
+
126
+ # Renormalize
127
+ row_sums = calibrated.sum(axis=1, keepdims=True)
128
+ row_sums = np.maximum(row_sums, 1e-10)
129
+ calibrated /= row_sums
130
+ return calibrated
131
+
132
+ def predict_proba(self, X: np.ndarray) -> np.ndarray:
133
+ """Get calibrated probabilities. Always returns shape (n, 3)."""
134
+ if self.model is None:
135
+ raise RuntimeError("Model not trained. Call train() first.")
136
+
137
+ raw_proba = self.model.predict_proba(X)
138
+
139
+ # Ensure 3-column output
140
+ if raw_proba.shape[1] < NUM_CLASSES:
141
+ full = np.zeros((raw_proba.shape[0], NUM_CLASSES), dtype=np.float32)
142
+ for i, c in enumerate(self.model.classes_):
143
+ full[:, c] = raw_proba[:, i]
144
+ for c in range(NUM_CLASSES):
145
+ if c not in self.model.classes_:
146
+ full[:, c] = 1.0 / NUM_CLASSES
147
+ row_sums = full.sum(axis=1, keepdims=True)
148
+ full /= np.maximum(row_sums, 1e-10)
149
+ raw_proba = full
150
+
151
+ return self._calibrate_proba(raw_proba.astype(np.float32))
152
+
153
+ def predict(self, X: np.ndarray) -> tuple:
154
+ """
155
+ Predict class and probabilities.
156
+ Returns (class_indices, probabilities) where probabilities shape is (n_samples, 3)
157
+ """
158
+ proba = self.predict_proba(X)
159
+
160
+ max_proba = np.max(proba, axis=1)
161
+ below_threshold = max_proba < self.min_confidence
162
+
163
+ class_indices = np.argmax(proba, axis=1)
164
+ class_indices[below_threshold] = LABEL_TO_IDX["uncertain"]
165
+
166
+ return class_indices, proba
167
+
168
+ def get_config(self) -> dict:
169
+ return {
170
+ "C": self.C,
171
+ "max_iter": self.max_iter,
172
+ "min_confidence": self.min_confidence,
173
+ }
174
+
175
+ @classmethod
176
+ def from_config(cls, config: dict) -> "NameClassifier":
177
+ return cls(
178
+ C=config.get("C", 1.0),
179
+ max_iter=config.get("max_iter", 1000),
180
+ min_confidence=config.get("min_confidence", 0.70),
181
+ )
genderfluid/cli.py ADDED
@@ -0,0 +1,270 @@
1
+ """Full-featured CLI for genderfluid-tiny."""
2
+
3
+ import argparse
4
+ import json
5
+ import sys
6
+ import os
7
+ import time
8
+
9
+ SUBCOMMANDS = {"predict", "stats", "benchmark", "interactive"}
10
+
11
+
12
+ def _load_model(model_path=None):
13
+ from genderfluid import GenderfluidModel
14
+ return GenderfluidModel(model_path)
15
+
16
+
17
+ def format_result(result: dict, color: bool = False) -> str:
18
+ lines = [f"Name: {result['name']}", ""]
19
+
20
+ girl = result["girl_associated_probability"] * 100
21
+ boy = result["boy_associated_probability"] * 100
22
+ unc = result["uncertain_probability"] * 100
23
+
24
+ if color:
25
+ lines.append(f"Girl-associated: \033[35m{girl:.1f}%\033[0m")
26
+ lines.append(f"Boy-associated: \033[34m{boy:.1f}%\033[0m")
27
+ lines.append(f"Uncertain: {unc:.1f}%")
28
+ else:
29
+ lines.append(f"Girl-associated: {girl:.1f}%")
30
+ lines.append(f"Boy-associated: {boy:.1f}%")
31
+ lines.append(f"Uncertain: {unc:.1f}%")
32
+
33
+ lines.append("")
34
+ lines.append(f"Classification: {result['classification']}")
35
+ lines.append(f"Confidence: {result['confidence']}")
36
+
37
+ if "warning" in result:
38
+ lines.append(f"Warning: {result['warning']}")
39
+ if "context_warning" in result:
40
+ lines.append(f"Note: {result['context_warning']}")
41
+ return "\n".join(lines)
42
+
43
+
44
+ def format_compare(results: list[dict]) -> str:
45
+ lines = [
46
+ f"{'Name':<25} {'Classification':<20} {'Girl':>6} {'Boy':>6} {'Confidence':<10}",
47
+ "-" * 70,
48
+ ]
49
+ for r in results:
50
+ g = f"{r['girl_associated_probability']*100:.0f}%"
51
+ b = f"{r['boy_associated_probability']*100:.0f}%"
52
+ lines.append(f"{r['name']:<25} {r['classification']:<20} {g:>6} {b:>6} {r['confidence']:<10}")
53
+ return "\n".join(lines)
54
+
55
+
56
+ def interactive_mode(model=None, color: bool = False):
57
+ print("Name Gender Association Predictor")
58
+ print("Type 'quit' to exit.\n")
59
+
60
+ while True:
61
+ try:
62
+ name = input("Name > ").strip()
63
+ except (EOFError, KeyboardInterrupt):
64
+ print()
65
+ break
66
+
67
+ if not name or name.lower() in ("quit", "exit", "q"):
68
+ break
69
+
70
+ try:
71
+ t0 = time.time()
72
+ if model:
73
+ result = model.predict(name)
74
+ else:
75
+ from genderfluid import predict_name
76
+ result = predict_name(name)
77
+ elapsed = (time.time() - t0) * 1000
78
+ print()
79
+ print(format_result(result, color=color))
80
+ print(f" ({elapsed:.1f} ms)")
81
+ print()
82
+ except FileNotFoundError as e:
83
+ print(f"Error: {e}")
84
+ break
85
+ except Exception as e:
86
+ print(f"Error: {e}")
87
+
88
+
89
+ def cmd_predict(args):
90
+ model = _load_model(args.model)
91
+
92
+ if args.file:
93
+ with open(args.file, "r", encoding="utf-8") as f:
94
+ names = [line.strip() for line in f if line.strip()]
95
+
96
+ t0 = time.time()
97
+ results = model.predict_batch(names)
98
+ elapsed = time.time() - t0
99
+
100
+ if args.json:
101
+ for r in results:
102
+ print(json.dumps(r, ensure_ascii=False))
103
+ else:
104
+ print(format_compare(results))
105
+
106
+ print(f"\n{len(names)} names in {elapsed*1000:.1f} ms ({len(names)/elapsed:.0f} names/sec)")
107
+ return
108
+
109
+ if args.compare:
110
+ names = [n.strip() for n in args.compare if n.strip()]
111
+ if not names:
112
+ print("Error: no names provided")
113
+ return
114
+
115
+ t0 = time.time()
116
+ results = model.predict_batch(names)
117
+ elapsed = time.time() - t0
118
+
119
+ if args.json:
120
+ print(json.dumps(results, ensure_ascii=False, indent=2))
121
+ else:
122
+ print(format_compare(results))
123
+ print(f"\n{len(names)} names in {elapsed*1000:.1f} ms")
124
+ return
125
+
126
+ if args.name:
127
+ t0 = time.time()
128
+ result = model.predict(args.name)
129
+ elapsed = (time.time() - t0) * 1000
130
+
131
+ if args.json:
132
+ print(json.dumps(result, ensure_ascii=False, indent=2))
133
+ else:
134
+ print(format_result(result, color=args.color))
135
+ if args.verbose:
136
+ print(f"\n Latency: {elapsed:.1f} ms")
137
+ return
138
+
139
+ print("Usage: genderfluid predict <name> [options]")
140
+ print(" genderfluid predict --compare <name1> <name2> ...")
141
+ print(" genderfluid predict --file names.txt")
142
+
143
+
144
+ def cmd_stats(args):
145
+ model = _load_model(args.model)
146
+ meta = model.metadata
147
+ size = os.path.getsize(model.model_path)
148
+
149
+ print("Model Statistics")
150
+ print("=" * 40)
151
+ print(f" Model: genderfluid-tiny")
152
+ print(f" Version: {meta.get('version', 'unknown')}")
153
+ print(f" Size: {size / (1024*1024):.2f} MB ({size:,} bytes)")
154
+ if "feature_dimensions" in meta:
155
+ print(f" Features: {meta['feature_dimensions']}")
156
+ if "data_source" in meta:
157
+ print(f" Data: {meta['data_source']}")
158
+ if "train_size" in meta:
159
+ print(f" Train: {meta['train_size']:,}")
160
+ if "val_size" in meta:
161
+ print(f" Validation: {meta['val_size']:,}")
162
+ if "test_size" in meta:
163
+ print(f" Test: {meta['test_size']:,}")
164
+ if "test_f1" in meta:
165
+ print(f" Test F1: {meta['test_f1']:.3f}")
166
+ if "validation_f1" in meta:
167
+ print(f" Val F1: {meta['validation_f1']:.3f}")
168
+ print(f" Classes: girl-associated, boy-associated, uncertain")
169
+
170
+
171
+ def cmd_benchmark(args):
172
+ from genderfluid import GenderfluidModel
173
+
174
+ print("Benchmark")
175
+ print("=" * 40)
176
+
177
+ t0 = time.time()
178
+ model = GenderfluidModel(args.model)
179
+ load_time = (time.time() - t0) * 1000
180
+ size = os.path.getsize(model.model_path)
181
+ print(f" Model size: {size / (1024*1024):.2f} MB")
182
+ print(f" Loading time: {load_time:.1f} ms")
183
+
184
+ test_names = ["Emma", "James", "Alex", "Michelle Renatta Chan", "Max", "Taylor",
185
+ "Elva Retta", "Sam", "Jordan", "Chris"]
186
+
187
+ # Single name
188
+ times = []
189
+ for _ in range(10):
190
+ for name in test_names:
191
+ t0 = time.time()
192
+ model.predict(name)
193
+ times.append((time.time() - t0) * 1000)
194
+ avg_single = sum(times) / len(times)
195
+ print(f" Single name: {avg_single:.2f} ms")
196
+
197
+ # Batch
198
+ for batch_size in [10, 100, 1000]:
199
+ batch = (test_names * (batch_size // len(test_names) + 1))[:batch_size]
200
+ t0 = time.time()
201
+ model.predict_batch(batch)
202
+ elapsed = (time.time() - t0) * 1000
203
+ throughput = batch_size / (elapsed / 1000)
204
+ print(f" Batch {batch_size:>4}: {elapsed:>7.1f} ms ({throughput:,.0f} names/sec)")
205
+
206
+ try:
207
+ import resource
208
+ mem = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
209
+ print(f" Peak RSS: {mem:.0f} MB")
210
+ except (ImportError, AttributeError):
211
+ pass
212
+
213
+
214
+ def _build_parser():
215
+ parser = argparse.ArgumentParser(
216
+ prog="genderfluid",
217
+ description="genderfluid-tiny: name gender association classifier",
218
+ )
219
+ parser.add_argument("--version", action="version", version="genderfluid-tiny 1.0.0")
220
+ parser.add_argument("--color", action="store_true", help="colored output")
221
+ parser.add_argument("--verbose", "-v", action="store_true", help="verbose output")
222
+
223
+ sub = parser.add_subparsers(dest="command")
224
+
225
+ p_predict = sub.add_parser("predict", help="predict gender association for a name")
226
+ p_predict.add_argument("name", nargs="?", help="name to classify")
227
+ p_predict.add_argument("--file", "-f", help="file with one name per line")
228
+ p_predict.add_argument("--compare", "-c", nargs="+", help="compare multiple names")
229
+ p_predict.add_argument("--json", "-j", action="store_true", help="output JSON")
230
+ p_predict.add_argument("--model", "-m", help="path to model file")
231
+
232
+ p_stats = sub.add_parser("stats", help="show model statistics")
233
+ p_stats.add_argument("--model", "-m", help="path to model file")
234
+
235
+ p_bench = sub.add_parser("benchmark", help="run inference benchmark")
236
+ p_bench.add_argument("--model", "-m", help="path to model file")
237
+
238
+ p_inter = sub.add_parser("interactive", help="interactive prediction mode")
239
+ p_inter.add_argument("--model", "-m", help="path to model file")
240
+
241
+ return parser
242
+
243
+
244
+ def main():
245
+ parser = _build_parser()
246
+
247
+ # Detect backward-compatible calling: predict.py "Elva Retta"
248
+ # sys.argv[0] ends with predict.py and argv[1] is not a subcommand or flag
249
+ if len(sys.argv) > 1:
250
+ first = sys.argv[1]
251
+ if first not in SUBCOMMANDS and not first.startswith("-") and first != "--help":
252
+ # Looks like a bare name -- treat as: predict <name>
253
+ sys.argv = [sys.argv[0], "predict"] + sys.argv[1:]
254
+
255
+ args = parser.parse_args()
256
+
257
+ if args.command == "predict":
258
+ cmd_predict(args)
259
+ elif args.command == "stats":
260
+ cmd_stats(args)
261
+ elif args.command == "benchmark":
262
+ cmd_benchmark(args)
263
+ elif args.command == "interactive":
264
+ interactive_mode(_load_model(getattr(args, 'model', None)))
265
+ else:
266
+ parser.print_help()
267
+
268
+
269
+ if __name__ == "__main__":
270
+ main()