| """Command-line interface.""" |
|
|
| from __future__ import annotations |
|
|
| import json |
|
|
| import typer |
| from rich.console import Console |
|
|
| from veil_pgd.config import get_settings |
|
|
| app = typer.Typer( |
| add_completion=False, |
| help="veil-pgd: image-side protection for vision-language training data", |
| ) |
| console = Console() |
|
|
|
|
| @app.command() |
| def protect( |
| image: str = typer.Argument(..., help="Path to the input image"), |
| truth: str = typer.Option(..., "--truth", "-t", help="True label of the image"), |
| skip_blackbox: bool = typer.Option(False, help="Skip Tier B (no API calls)"), |
| ): |
| """Find and render the best protective overlay for IMAGE.""" |
| from veil_pgd.pipeline import protect_image |
|
|
| res = protect_image(image, truth, skip_blackbox=skip_blackbox) |
| console.print_json(json.dumps(res.manifest)) |
|
|
|
|
| @app.command() |
| def evaluate( |
| manifest: str = typer.Argument(..., help="CSV of image_path,truth per line"), |
| out: str = typer.Option("artifacts/eval", help="Output dir"), |
| ): |
| """Run the eval harness over a labeled image set (single stealth level).""" |
| from veil_pgd.eval.harness import run_eval |
| from veil_pgd.eval.report import to_markdown |
|
|
| report = run_eval(manifest, out) |
| console.print(to_markdown(report)) |
|
|
|
|
| @app.command() |
| def sweep( |
| manifest: str = typer.Argument(..., help="CSV of image_path,truth per line"), |
| levels: str = typer.Option("strict,medium,loose", help="Comma-separated stealth levels"), |
| out: str = typer.Option("artifacts/eval", help="Output dir"), |
| skip_blackbox: bool = typer.Option(False, help="Skip Tier B (surrogate-only, no API calls)"), |
| ): |
| """Stealth sweep: map attack strength vs human-visibility across levels.""" |
| from veil_pgd.eval.harness import run_sweep |
|
|
| report = run_sweep( |
| manifest, [x.strip() for x in levels.split(",")], out, |
| skip_blackbox=skip_blackbox, |
| ) |
| console.print("[bold]Stealth frontier[/bold] (level: mean_dist | success@0.5 | stealth_pass)") |
| for row in report["frontier"]: |
| console.print( |
| f" {row['level']:>7}: {row['mean_distance']:.3f} | " |
| f"{row['success@0.5']:.1%} | {row['stealth_pass_rate']:.1%}" |
| ) |
|
|
|
|
| @app.command() |
| def doctor(): |
| """Check config + connectivity to klaus-3 services and OpenRouter.""" |
| import httpx |
|
|
| s = get_settings() |
| console.print(f"[bold]OpenRouter key set:[/bold] {bool(s.openrouter_api_key)}") |
| console.print(f"[bold]Targets:[/bold] {', '.join(s.blackbox_target_models)}") |
| for name, url in [ |
| ("qwen", s.klaus3_qwen_base_url), |
| ("gemma4b", s.klaus3_gemma4b_base_url), |
| ("vision-svc", s.klaus3_vision_service_url), |
| ]: |
| try: |
| r = httpx.get(url.rstrip("/") + "/models", timeout=5.0) |
| ok = r.status_code < 500 |
| except Exception as e: |
| ok = False |
| console.print(f" {name}: [red]unreachable[/red] ({e})") |
| continue |
| console.print(f" {name}: {'[green]ok[/green]' if ok else '[yellow]?[/yellow]'} ({url})") |
|
|
|
|
| if __name__ == "__main__": |
| app() |
|
|