Next version. Models + scripts updated. 2
This commit is contained in:
@@ -60,7 +60,7 @@ from climbingboardgpt.tokenization import (
|
||||
make_placement_lookup,
|
||||
vocab_payload,
|
||||
)
|
||||
from climbingboardgpt.utils import json_safe, safe_train_test_split, set_seed, write_json
|
||||
from climbingboardgpt.utils import assign_group_splits, json_safe, set_seed, write_json
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
@@ -101,8 +101,8 @@ Examples:
|
||||
parser.add_argument(
|
||||
"--seed",
|
||||
type=int,
|
||||
default=42,
|
||||
help="Random seed for reproducible splits (default: 42)",
|
||||
default=3,
|
||||
help="Random seed for reproducible splits (default: 3)",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -244,41 +244,33 @@ def main() -> None:
|
||||
df_routes["ids_with_grade"] = df_routes["tokens_with_grade"].apply(lambda tokens: encode(tokens, stoi))
|
||||
df_routes["ids_no_grade"] = df_routes["tokens_no_grade"].apply(lambda tokens: encode(tokens, stoi))
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# Step 6: Train/val/test split (stratified)
|
||||
# Step 6: Train/val/test split (grouped by logical climb)
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# We split 80/10/10, stratified by board_key × grouped_v.
|
||||
# This ensures both boards and all difficulty levels are represented
|
||||
# in each split, which is critical for fair evaluation.
|
||||
# A single climb UUID can appear at multiple wall angles. We therefore
|
||||
# split by (board_key, uuid), not by individual rows. This avoids putting
|
||||
# one angle of a climb in train and another angle of the same climb in test.
|
||||
#
|
||||
# Stratification prevents scenarios like "all V14 climbs end up in
|
||||
# the test set while training has none."
|
||||
# The split is stratified by board_key × grouped_v at the group level when
|
||||
# possible. The row proportions may differ slightly from 80/10/10 because
|
||||
# some climbs have more angle entries than others, but this is preferable
|
||||
# to route-level leakage or brittle UUID-overwrite logic.
|
||||
df_routes["split_stratum"] = (
|
||||
df_routes["board_key"].astype(str)
|
||||
+ "__V"
|
||||
+ df_routes["grouped_v"].astype(str)
|
||||
)
|
||||
|
||||
train_df, temp_df = safe_train_test_split(
|
||||
df_routes["split"] = assign_group_splits(
|
||||
df_routes,
|
||||
group_cols=["board_key", "uuid"],
|
||||
test_size=0.20,
|
||||
random_state=args.seed,
|
||||
stratify_col="split_stratum",
|
||||
)
|
||||
val_df, test_df = safe_train_test_split(
|
||||
temp_df,
|
||||
test_size=0.50,
|
||||
val_size_within_temp=0.50,
|
||||
random_state=args.seed,
|
||||
stratify_col="split_stratum",
|
||||
)
|
||||
|
||||
split_map = {}
|
||||
split_map.update({uuid: "train" for uuid in train_df["uuid"]})
|
||||
split_map.update({uuid: "val" for uuid in val_df["uuid"]})
|
||||
split_map.update({uuid: "test" for uuid in test_df["uuid"]})
|
||||
df_routes["split"] = df_routes["uuid"].map(split_map)
|
||||
|
||||
print(f"\nSplit counts:")
|
||||
print("\nSplit counts:")
|
||||
print(df_routes.groupby(["board_key", "split"]).size().unstack(fill_value=0))
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
@@ -357,4 +349,4 @@ def main() -> None:
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
main()
|
||||
|
||||
@@ -99,7 +99,7 @@ accuracy (within ±1 V-grade).
|
||||
parser.add_argument("--num-layers", type=int, default=4, help="Number of transformer layers")
|
||||
parser.add_argument("--dim-feedforward", type=int, default=256, help="Feedforward dimension")
|
||||
parser.add_argument("--dropout", type=float, default=0.10, help="Dropout probability")
|
||||
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
||||
parser.add_argument("--seed", type=int, default=3, help="Random seed")
|
||||
parser.add_argument("--device", type=str, default=None, help="Device (cpu or cuda)")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -104,7 +104,7 @@ specific board, or leave unset to generate for all boards.
|
||||
parser.add_argument("--generate-board", type=str, default=None, help="Board key: tb2 or kilter")
|
||||
parser.add_argument("--generate-angles", type=str, default=None, help="Comma-separated angles")
|
||||
parser.add_argument("--generate-grades", type=str, default=None, help="Comma-separated V-grades")
|
||||
parser.add_argument("--seed", type=int, default=42, help="Random seed")
|
||||
parser.add_argument("--seed", type=int, default=3, help="Random seed")
|
||||
parser.add_argument("--device", type=str, default=None, help="Device (cpu or cuda)")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
@@ -202,7 +202,15 @@ def main() -> None:
|
||||
lambda records: frozenset(int(record["placement_id"]) for record in records)
|
||||
)
|
||||
|
||||
validity = pd.DataFrame(df_generated["hold_records"].apply(validity_from_records).tolist())
|
||||
validity = pd.DataFrame(
|
||||
df_generated.apply(
|
||||
lambda row: validity_from_records(
|
||||
row["hold_records"],
|
||||
requested_board_prefix=row.get("requested_board_prefix"),
|
||||
),
|
||||
axis=1,
|
||||
).tolist()
|
||||
)
|
||||
df_eval = pd.concat([df_generated.reset_index(drop=True), validity], axis=1)
|
||||
|
||||
print(f"Evaluated generated routes: {len(df_eval):,}")
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate ClimbingBoardGPT routes and save board visualizations.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generate four TB2 V6 climbs at 40 degrees:
|
||||
|
||||
python scripts/demo_generate_and_visualize.py --board tb2 --angle 40 --grade 6 --n 4
|
||||
|
||||
Generate Kilter climbs with placement labels:
|
||||
|
||||
python scripts/demo_generate_and_visualize.py --board kilter --angle 35 --grade 5 --annotate
|
||||
|
||||
The script writes:
|
||||
- generated_routes.csv
|
||||
- generated_route_001.png
|
||||
- generated_route_001.svg
|
||||
- ...
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
|
||||
from climbingboardgpt.inference import (
|
||||
generate_route,
|
||||
load_board_for_demo,
|
||||
load_grade_predictor,
|
||||
load_route_generator,
|
||||
predict_route_grade,
|
||||
)
|
||||
from climbingboardgpt.visualization import load_token_metadata, visualize_route_result
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate ClimbingBoardGPT routes and save route visualizations.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--board", choices=["tb2", "kilter"], required=True)
|
||||
parser.add_argument("--angle", type=int, default=40)
|
||||
parser.add_argument("--grade", type=int, default=6, help="Target grouped V-grade.")
|
||||
parser.add_argument("--n", type=int, default=4, help="Number of routes to sample.")
|
||||
parser.add_argument("--temperature", type=float, default=0.9)
|
||||
parser.add_argument("--top-k", type=int, default=50)
|
||||
parser.add_argument("--max-new-tokens", type=int, default=40)
|
||||
parser.add_argument("--annotate", action="store_true", help="Label route holds by placement ID.")
|
||||
parser.add_argument("--device", type=str, default=None, help="cpu, cuda, or omit for auto.")
|
||||
parser.add_argument("--torch-threads", type=int, default=None, help="Optional CPU thread cap for VPS demos.")
|
||||
parser.add_argument(
|
||||
"--model-path",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "models" / "joint_route_gpt_generator.pth",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--grade-model-path",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "models" / "joint_transformer_grade_predictor.pth",
|
||||
help="Optional grade-predictor checkpoint used to score generated routes.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-grade-prediction",
|
||||
action="store_true",
|
||||
help="Skip grade-predictor scoring even if the checkpoint exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokenized-dir",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "data" / "processed" / "tokenized",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "outputs" / "demo_routes",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--background-image",
|
||||
type=Path,
|
||||
default=None,
|
||||
help=(
|
||||
"Optional board image to draw under the scatter plot. "
|
||||
"If omitted, the script automatically uses images/tb2_board_12x12_composite.png "
|
||||
"for TB2 and images/kilter-original-16x12_compose.png for Kilter when present."
|
||||
),
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
|
||||
def default_background_for_board(board: str) -> Path | None:
|
||||
candidates = {
|
||||
"tb2": REPO_ROOT / "images" / "tb2_board_12x12_composite.png",
|
||||
"kilter": REPO_ROOT / "images" / "kilter-original-16x12_compose.png",
|
||||
}
|
||||
path = candidates.get(board)
|
||||
return path if path is not None and path.exists() else None
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
board_config = load_board_for_demo(args.board, config_dir=REPO_ROOT / "configs")
|
||||
generator = load_route_generator(args.model_path, device=args.device, torch_threads=args.torch_threads)
|
||||
token_meta = load_token_metadata(args.tokenized_dir)
|
||||
background_image = args.background_image or default_background_for_board(args.board)
|
||||
|
||||
grade_predictor = None
|
||||
if not args.no_grade_prediction:
|
||||
if args.grade_model_path.exists():
|
||||
grade_predictor = load_grade_predictor(
|
||||
args.grade_model_path,
|
||||
device=args.device,
|
||||
torch_threads=args.torch_threads,
|
||||
)
|
||||
else:
|
||||
print(f"Grade predictor not found at {args.grade_model_path}; skipping grade prediction.")
|
||||
|
||||
run_dir = args.out_dir / args.board / f"angle_{args.angle}" / f"V{args.grade}"
|
||||
run_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
rows = []
|
||||
for i in range(1, args.n + 1):
|
||||
result = generate_route(
|
||||
generator=generator,
|
||||
board_config=board_config,
|
||||
angle=args.angle,
|
||||
grade=args.grade,
|
||||
temperature=args.temperature,
|
||||
top_k=args.top_k,
|
||||
max_new_tokens=args.max_new_tokens,
|
||||
)
|
||||
|
||||
if grade_predictor is not None:
|
||||
grade_result = predict_route_grade(grade_predictor, result["tokens"])
|
||||
result.update(grade_result)
|
||||
result["critic_v_error"] = (
|
||||
int(result["predicted_grouped_v"]) - int(result["requested_grouped_v"])
|
||||
)
|
||||
|
||||
rows.append(result)
|
||||
|
||||
stem = f"generated_route_{i:03d}"
|
||||
png_path = run_dir / f"{stem}.png"
|
||||
svg_path = run_dir / f"{stem}.svg"
|
||||
|
||||
fig, _, _ = visualize_route_result(
|
||||
result,
|
||||
df_token_meta=token_meta,
|
||||
output_path=png_path,
|
||||
annotate=args.annotate,
|
||||
background_image=background_image,
|
||||
)
|
||||
fig.savefig(svg_path, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
print(f"[{i}/{args.n}] {result['frames']}")
|
||||
print(f" valid={result['basic_valid']} holds={result['n_hold_tokens']}")
|
||||
if "predicted_grouped_v" in result:
|
||||
print(
|
||||
f" predicted=V{result['predicted_grouped_v']} "
|
||||
f"(difficulty={result['predicted_display_difficulty']:.2f}, "
|
||||
f"error={result['critic_v_error']:+d} V)"
|
||||
)
|
||||
try:
|
||||
png_display = png_path.resolve().relative_to(REPO_ROOT.resolve())
|
||||
except Exception:
|
||||
png_display = png_path
|
||||
print(f" saved {png_display}")
|
||||
|
||||
df = pd.DataFrame(rows)
|
||||
df["tokens_json"] = df["tokens"].apply(json.dumps)
|
||||
df.drop(columns=["tokens"]).to_csv(run_dir / "generated_routes.csv", index=False)
|
||||
|
||||
if background_image is not None:
|
||||
try:
|
||||
bg_display = background_image.relative_to(REPO_ROOT)
|
||||
except Exception:
|
||||
bg_display = background_image
|
||||
print(f"Using background image: {bg_display}")
|
||||
else:
|
||||
print("Using background image: none (coordinate-board style only)")
|
||||
|
||||
print("\nSaved route table:")
|
||||
print(run_dir / "generated_routes.csv")
|
||||
print("\nOutput directory:")
|
||||
print(run_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convenience wrapper: generate and visualize Kilter routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(REPO_ROOT / "scripts" / "demo_generate_and_visualize.py"),
|
||||
"--board",
|
||||
"kilter",
|
||||
*sys.argv[1:],
|
||||
]
|
||||
raise SystemExit(subprocess.call(cmd))
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convenience wrapper: generate and visualize TB2 routes."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(REPO_ROOT / "scripts" / "demo_generate_and_visualize.py"),
|
||||
"--board",
|
||||
"tb2",
|
||||
*sys.argv[1:],
|
||||
]
|
||||
raise SystemExit(subprocess.call(cmd))
|
||||
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Predict a climb grade from board, angle, and BoardLib frames string.
|
||||
|
||||
Examples
|
||||
--------
|
||||
Generic:
|
||||
|
||||
python scripts/demo_predict_grade.py \
|
||||
--board tb2 \
|
||||
--angle 40 \
|
||||
--frames 'p652r5p631r6p322r6p326r7'
|
||||
|
||||
TB2 wrapper:
|
||||
|
||||
python scripts/demo_predict_tb2.py \
|
||||
--angle 40 \
|
||||
--frames 'p652r5p631r6p322r6p326r7'
|
||||
|
||||
Kilter wrapper:
|
||||
|
||||
python scripts/demo_predict_kilter.py \
|
||||
--angle 40 \
|
||||
--frames 'p1127r12p1196r13p1388r14'
|
||||
|
||||
Add ``--visualize`` to save a PNG/SVG overlay using the board background.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(REPO_ROOT / "src"))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from climbingboardgpt.inference import (
|
||||
frames_to_grade_model_tokens,
|
||||
load_board_for_demo,
|
||||
load_grade_predictor,
|
||||
predict_frames_grade,
|
||||
)
|
||||
from climbingboardgpt.visualization import load_token_metadata, visualize_route_tokens
|
||||
|
||||
|
||||
def default_background_for_board(board: str) -> Path | None:
|
||||
candidates = {
|
||||
"tb2": REPO_ROOT / "images" / "tb2_board_12x12_composite.png",
|
||||
"kilter": REPO_ROOT / "images" / "kilter-original-16x12_compose.png",
|
||||
}
|
||||
path = candidates.get(board)
|
||||
return path if path is not None and path.exists() else None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Predict climb grade from board, angle, and frames string.",
|
||||
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
||||
)
|
||||
parser.add_argument("--board", choices=["tb2", "kilter"], required=True)
|
||||
parser.add_argument("--angle", type=int, required=True)
|
||||
parser.add_argument("--frames", type=str, required=True)
|
||||
parser.add_argument("--device", type=str, default=None, help="cpu, cuda, or omit for auto.")
|
||||
parser.add_argument("--torch-threads", type=int, default=None, help="Optional CPU thread cap.")
|
||||
parser.add_argument(
|
||||
"--grade-model-path",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "models" / "joint_transformer_grade_predictor.pth",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--tokenized-dir",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "data" / "processed" / "tokenized",
|
||||
)
|
||||
parser.add_argument("--json", action="store_true", help="Print JSON instead of human-readable text.")
|
||||
parser.add_argument("--show-tokens", action="store_true", help="Print the model token sequence.")
|
||||
parser.add_argument("--visualize", action="store_true", help="Save a board-background visualization.")
|
||||
parser.add_argument("--annotate", action="store_true", help="Label route holds by placement ID.")
|
||||
parser.add_argument(
|
||||
"--out-dir",
|
||||
type=Path,
|
||||
default=REPO_ROOT / "outputs" / "grade_predictions",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-name",
|
||||
type=str,
|
||||
default=None,
|
||||
help="Output image/table stem. Defaults to <board>_angle_<angle>_prediction.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--background-image",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Optional background image override.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
|
||||
board_config = load_board_for_demo(args.board, config_dir=REPO_ROOT / "configs")
|
||||
token_meta = load_token_metadata(args.tokenized_dir)
|
||||
predictor = load_grade_predictor(
|
||||
args.grade_model_path,
|
||||
device=args.device,
|
||||
torch_threads=args.torch_threads,
|
||||
)
|
||||
|
||||
result = predict_frames_grade(
|
||||
grade_predictor=predictor,
|
||||
frames=args.frames,
|
||||
angle=args.angle,
|
||||
board_config=board_config,
|
||||
df_token_meta=token_meta,
|
||||
)
|
||||
|
||||
if args.json:
|
||||
print(json.dumps(result, indent=2))
|
||||
else:
|
||||
print(f"Board: {result['board_display_name']} ({result['board_key']})")
|
||||
print(f"Angle: {result['requested_angle']}°")
|
||||
print(f"Frames: {result['frames']}")
|
||||
print(f"Predicted: V{result['predicted_grouped_v']}")
|
||||
print(f"Difficulty: {result['predicted_display_difficulty']:.3f}")
|
||||
if args.show_tokens:
|
||||
print()
|
||||
print("Model tokens:")
|
||||
print(result["sequence"])
|
||||
|
||||
if args.visualize:
|
||||
out_dir = args.out_dir / args.board / f"angle_{args.angle}"
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
stem = args.output_name or f"{args.board}_angle_{args.angle}_prediction"
|
||||
png_path = out_dir / f"{stem}.png"
|
||||
svg_path = out_dir / f"{stem}.svg"
|
||||
json_path = out_dir / f"{stem}.json"
|
||||
|
||||
background_image = args.background_image or default_background_for_board(args.board)
|
||||
title = (
|
||||
f"{result['board_display_name']} predicted "
|
||||
f"V{result['predicted_grouped_v']} @ {args.angle}°"
|
||||
)
|
||||
subtitle = (
|
||||
f"difficulty={result['predicted_display_difficulty']:.2f} | "
|
||||
f"frames={args.frames}"
|
||||
)
|
||||
|
||||
fig, _, _ = visualize_route_tokens(
|
||||
tokens=result["tokens"],
|
||||
df_token_meta=token_meta,
|
||||
board_key=args.board,
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
output_path=png_path,
|
||||
annotate=args.annotate,
|
||||
background_image=background_image,
|
||||
)
|
||||
fig.savefig(svg_path, bbox_inches="tight")
|
||||
plt.close(fig)
|
||||
|
||||
json_path.write_text(json.dumps(result, indent=2), encoding="utf-8")
|
||||
|
||||
print()
|
||||
if background_image is not None:
|
||||
try:
|
||||
bg_display = background_image.relative_to(REPO_ROOT)
|
||||
except Exception:
|
||||
bg_display = background_image
|
||||
print(f"Using background image: {bg_display}")
|
||||
print(f"Saved PNG: {png_path}")
|
||||
print(f"Saved SVG: {svg_path}")
|
||||
print(f"Saved JSON: {json_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convenience wrapper: predict grade for a Kilter frames string."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(REPO_ROOT / "scripts" / "demo_predict_grade.py"),
|
||||
"--board",
|
||||
"kilter",
|
||||
*sys.argv[1:],
|
||||
]
|
||||
raise SystemExit(subprocess.call(cmd))
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Convenience wrapper: predict grade for a TB2 frames string."""
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmd = [
|
||||
sys.executable,
|
||||
str(REPO_ROOT / "scripts" / "demo_predict_grade.py"),
|
||||
"--board",
|
||||
"tb2",
|
||||
*sys.argv[1:],
|
||||
]
|
||||
raise SystemExit(subprocess.call(cmd))
|
||||
Reference in New Issue
Block a user