tungpth's picture
Deploy slim ZeroGPU bundle with pinned model startup downloads (part 6)
86e797b verified
Raw History Blame Contribute Delete
8.71 kB
"""Small command-line entry point; paths are relative to the working directory."""
import json
from pathlib import Path
from typing import Annotated
import typer
from visual_doc_rag import data
app = typer.Typer(no_args_is_help=True, pretty_exceptions_enable=False)
Subset = Annotated[str, typer.Option(help="Dataset subset: hr or industrial.")]
DataDir = Annotated[Path, typer.Option(help="Local data/cache directory; ignored by Git.")]
def check_subset(subset: str) -> None:
if subset not in data.SUBSETS:
raise typer.BadParameter(f"Choose one of: {', '.join(data.SUBSETS)}", param_hint="--subset")
@app.command()
def build_demo(
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
output: Annotated[Path | None, typer.Option(help="New bundle directory.")] = None,
model: Annotated[
str, typer.Option(help="Model used by the existing sample cache.")
] = "gemini-3.5-flash-lite",
space: Annotated[bool, typer.Option(help="Include Hugging Face Space configuration.")] = False,
) -> None:
"""Package offline retrieval and five cached samples, without calling the provider."""
from visual_doc_rag.demo import build_demo as pack
check_subset(subset)
try:
result = pack(subset, data_dir, output, model, space)
except Exception as error:
typer.echo(f"Demo build failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(f"Demo bundle: {result}")
@app.command()
def download(
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
revision: Annotated[
str, typer.Option(help="HF branch, tag or commit; resolved to a SHA.")
] = "main",
) -> None:
"""Download corpus, queries, qrels and document metadata (no original PDFs)."""
check_subset(subset)
try:
manifest = data.download(subset, data_dir, revision)
except Exception as error:
typer.echo(f"Download failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(json.dumps(manifest, indent=2))
@app.command()
def inspect(subset: Subset = "hr", data_dir: DataDir = Path("data")) -> None:
"""Inspect local tables, validate joins and save aggregate statistics/sample pages."""
check_subset(subset)
try:
report = data.inspect(subset, data_dir)
except Exception as error:
typer.echo(f"Inspection failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(json.dumps({k: v for k, v in report.items() if k != "features"}, indent=2))
typer.echo(f"Saved schema and statistics: {data_dir / subset / 'inspection.json'}")
if not all(report["integrity_checks"].values()):
typer.echo("Integrity checks failed; review inspection.json before indexing.", err=True)
raise typer.Exit(1)
@app.command()
def pages(subset: Subset = "hr", data_dir: DataDir = Path("data")) -> None:
"""Export all original page images to a revision-aware PNG cache."""
from visual_doc_rag.pages import export_pages
check_subset(subset)
try:
result = export_pages(subset, data_dir)
except Exception as error:
typer.echo(f"Page export failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(f"Exported {len(result['pages'])} pages; {result['image_bytes'] / 1e6:.1f} MB")
@app.command()
def index(
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
device: Annotated[str, typer.Option(help="auto, cuda or cpu for page encoding.")] = "auto",
mode: Annotated[str, typer.Option(help="all, text or visual.")] = "all",
model_revision: Annotated[
str, typer.Option(help="Adapter revision; reuse cached pin by default.")
] = "main",
base_revision: Annotated[
str | None, typer.Option(help="Optional pinned base-model revision.")
] = None,
) -> None:
"""Build BM25 and visual indexes; reuse completed page embeddings on retry."""
import torch
from visual_doc_rag.text_index import TextIndex
from visual_doc_rag.visual_index import build_visual
check_subset(subset)
if mode not in {"all", "text", "visual"} or device not in {"auto", "cuda", "cpu"}:
raise typer.BadParameter("Use --mode all/text/visual and --device auto/cuda/cpu.")
torch.set_num_threads(4)
if device == "auto":
device = "cuda" if torch.cuda.is_available() else "cpu"
try:
source, tables = data.load_tables(subset, data_dir)
if mode in {"all", "text"}:
result = TextIndex.build(tables["corpus"], data_dir / subset / "indexes", source)
typer.echo(f"BM25: {json.dumps(result)}")
if mode in {"all", "visual"}:
result = build_visual(subset, data_dir, source, device, model_revision, base_revision)
typer.echo(json.dumps({k: v for k, v in result.items() if k != "pages"}, indent=2))
except Exception as error:
typer.echo(f"Index build failed: {error}", err=True)
raise typer.Exit(1) from error
@app.command(name="eval")
def eval_command(
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
runs_dir: Annotated[
Path, typer.Option(help="Directory for timestamped metric reports.")
] = Path("runs"),
) -> None:
"""Evaluate text/visual/hybrid on every English query, with CPU latency."""
import torch
from visual_doc_rag.evaluate import evaluate, format_table
check_subset(subset)
torch.set_num_threads(4)
try:
output, report = evaluate(subset, data_dir, runs_dir)
except Exception as error:
typer.echo(f"Evaluation failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(format_table(report))
typer.echo(f"Queries: {report['n_queries']}; pages: {report['n_pages']}")
typer.echo(f"Saved: {output / 'metrics.json'}")
@app.command()
def search(
query: Annotated[str, typer.Argument(help="Question or keywords to retrieve pages for.")],
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
method: Annotated[str, typer.Option(help="text, visual or hybrid.")] = "hybrid",
top_k: Annotated[int, typer.Option(min=1, help="Number of page results to display.")] = 5,
export: Annotated[
bool, typer.Option(help="Save an HTML report, page images and JSON under runs/search/.")
] = False,
) -> None:
"""Retrieve pages and print local image paths, without generating an answer."""
from visual_doc_rag.retrieve import PageRetriever
check_subset(subset)
if method not in {"text", "visual", "hybrid"} or not query.strip():
raise typer.BadParameter("Provide a nonempty query and --method text/visual/hybrid.")
try:
retriever = PageRetriever(subset, data_dir)
results = retriever.search(query, method, top_k)
output = {"method": method, "pages": results}
if export:
from visual_doc_rag.search_export import export_search
output["exported_report"] = str(
export_search(query, method, retriever.source, results, retriever.tables["corpus"])
)
except Exception as error:
typer.echo(f"Search failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(json.dumps(output, indent=2))
@app.command()
def ask(
query: Annotated[str, typer.Argument(help="Question to answer using retrieved page images.")],
subset: Subset = "hr",
data_dir: DataDir = Path("data"),
method: Annotated[str, typer.Option(help="text, visual or hybrid.")] = "hybrid",
export: Annotated[bool, typer.Option(help="Save an answer and evidence HTML report.")] = False,
) -> None:
"""Answer from the top-3 pages with validated citations (requires GEMINI_API_KEY)."""
from visual_doc_rag.serve import AnswerService
check_subset(subset)
if method not in {"text", "visual", "hybrid"} or not query.strip():
raise typer.BadParameter("Provide a nonempty query and --method text/visual/hybrid.")
try:
service = AnswerService(subset, data_dir)
output = service.ask(query, method)
if export:
from visual_doc_rag.search_export import export_search
output["exported_report"] = str(
export_search(
output["query"],
method,
service.retriever.source,
output["pages"],
service.retriever.tables["corpus"],
answer=output,
)
)
except Exception as error:
typer.echo(f"Answer failed: {error}", err=True)
raise typer.Exit(1) from error
typer.echo(json.dumps(output, indent=2, ensure_ascii=False))