File size: 3,674 Bytes
eab734a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | """CLI: stt — statement to TikZ."""
from __future__ import annotations
import json
from pathlib import Path
from typing import Optional
import typer
from statement_to_tikz.pipeline import load_ir, run_pipeline
app = typer.Typer(
add_completion=False,
help="Convert geometry statements into constraint-solved TikZ diagrams.",
)
def _split_out(out: Path) -> tuple[Path, str]:
"""Interpret --out out/fig as directory out, basename fig."""
if out.suffix in {".tex", ".pdf", ".json"}:
return out.parent or Path("."), out.stem
parent = out.parent
name = out.name or "fig"
if parent == Path(""):
parent = Path(".")
return parent, name
@app.command()
def main(
statement: Optional[str] = typer.Argument(
None,
help="Geometry statement text, or path to a .txt file",
),
out: Path = typer.Option(
Path("out/fig"),
"--out",
"-o",
help="Output path prefix (directory/basename without extension)",
),
ir_path: Optional[Path] = typer.Option(
None,
"--ir",
help="Load GeometryIR JSON instead of formalizing a statement",
),
emit_only: bool = typer.Option(
False,
"--emit-only",
help="With --ir: solve and emit without calling the LLM",
),
no_compile: bool = typer.Option(
False,
"--no-compile",
help="Skip pdflatex/latexmk",
),
schematic: bool = typer.Option(
False,
"--schematic",
help="Force schematic layout",
),
no_offline: bool = typer.Option(
False,
"--no-offline",
help="Disable offline pattern formalizer; require LLM",
),
tol: float = typer.Option(1e-4, "--tol", help="Exact-mode residual tolerance"),
) -> None:
"""Generate TikZ from a statement or IR file."""
if statement is None and ir_path is None:
typer.echo(
"Usage: stt STATEMENT [-o out/fig] | stt --ir scene.ir.json --emit-only"
)
raise typer.Exit(code=1)
text: str | None = None
ir = None
if ir_path is not None:
ir = load_ir(ir_path)
if not emit_only and statement:
text = statement
else:
text = ir.statement or None
elif statement is not None:
p = Path(statement)
if p.is_file():
text = p.read_text(encoding="utf-8").strip()
else:
text = statement
out_dir, basename = _split_out(out)
result = run_pipeline(
text,
ir=ir,
out_dir=out_dir,
basename=basename,
tol=tol,
compile_pdf=not no_compile,
allow_offline_formalizer=not no_offline,
force_schematic=schematic,
)
typer.echo(f"mode: {result['mode']}")
typer.echo(f"max_residual: {result['max_residual']:.6g}")
if result.get("paths"):
for k, v in result["paths"].items():
if v:
typer.echo(f"{k}: {v}")
compile_info = (result.get("report") or {}).get("compile")
if compile_info:
if compile_info.get("skipped"):
typer.echo(f"compile skipped: {compile_info.get('error')}")
elif compile_info.get("ok"):
typer.echo("compile: ok")
else:
typer.echo(
f"compile: failed "
f"({compile_info.get('error') or compile_info.get('returncode')})"
)
raise typer.Exit(code=2)
summary = {
"mode": result["mode"],
"max_residual": result["max_residual"],
"paths": result.get("paths"),
}
typer.echo(json.dumps(summary))
if __name__ == "__main__":
app()
|