Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
# DocuParse

[![Open in Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://docuparse.streamlit.app)

**Intelligent document parser for financial filings** - Extracts text, tables, and validates data from SEC 10-K/10-Q documents using multiple AI models and cross-verification.

🔗 **Live results dashboard:** [docuparse.streamlit.app](https://docuparse.streamlit.app)

## What This Does

DocuParse is a complete pipeline that:
Expand Down Expand Up @@ -40,6 +44,10 @@ ls data/exports/

## 📊 Results Dashboard

[![Open in Streamlit](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://docuparse.streamlit.app)

**Live app:** [docuparse.streamlit.app](https://docuparse.streamlit.app)

An interactive Streamlit dashboard visualizes the pipeline's recorded outputs —
evaluation metrics, per-stage benchmarks (runtime & memory), build-vs-buy cost
analysis, distribution drift, and the analysis reports. It reads the committed
Expand Down
63 changes: 62 additions & 1 deletion dashboard/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,16 @@ def _fmt(value, digits: int = 4) -> str:
return "—" if value is None else f"{value:.{digits}f}"


def render_provenance() -> None:
"""A banner shown on every page so viewers know the data is a static snapshot."""
when = dl.data_as_of() or "a previous run"
st.info(
f"📌 **Static snapshot — figures are from the pipeline run on {when}.** "
"This dashboard reads committed result files; it does **not** run the "
"extraction pipeline live."
)


# --------------------------------------------------------------------------- #
# Sidebar
# --------------------------------------------------------------------------- #
Expand All @@ -41,7 +51,8 @@ def _fmt(value, digits: int = 4) -> str:
)
page = st.sidebar.radio(
"View",
["Overview", "Benchmarks", "Cost (Build vs Buy)", "Distribution Drift", "Reports"],
["About & Skills", "Overview", "Benchmarks", "Cost (Build vs Buy)",
"Distribution Drift", "Reports"],
)
st.sidebar.info(
"This dashboard visualizes recorded runs. A live 'upload a PDF and parse' "
Expand Down Expand Up @@ -224,11 +235,61 @@ def render_reports() -> None:
st.markdown(dl.read_report(reports[choice]))


# --------------------------------------------------------------------------- #
# About & Skills — the landing page
# --------------------------------------------------------------------------- #
# A curated shortlist of the most valuable concepts behind the project — not an
# exhaustive catalogue of every library used.
KEY_SKILLS = [
("Document AI & layout understanding",
"Parsing complex financial PDFs with OCR fallback and layout/table models "
"(Docling, LayoutParser, Tesseract, Camelot)."),
("Reproducible ML pipelines (MLOps)",
"A staged, parameterized DVC pipeline from download through export."),
("Quantitative evaluation",
"Text WER/CER and table precision/recall/F1, with regression and "
"distribution-drift monitoring."),
("Data validation",
"Cross-verifying extracted figures against authoritative SEC XBRL data."),
("Performance & cost engineering",
"Per-stage runtime/memory benchmarking and a build-vs-buy cost analysis."),
]


def render_about() -> None:
st.title("📄 DocuParse — Financial Filing Parser")
st.markdown(
"An end-to-end pipeline that extracts text, tables, and structure from "
"**SEC financial filings (10-K / 10-Q)**, measures the extraction quality, "
"and cross-checks the numbers against authoritative **XBRL** data. "
"The tabs on the left present the pipeline's recorded results."
)

st.subheader("🧠 Key concepts & skills")
for title, desc in KEY_SKILLS:
st.markdown(f"- **{title}** — {desc}")

st.subheader("🛠️ Built with")
st.markdown("`Python` · `DVC` · `Docling` · `pandas` · `Streamlit`")

st.subheader("🔗 Links")
st.markdown(
"- **Source code:** https://github.com/Effyrt/Docuparse\n"
"- **Demo video:** "
"https://drive.google.com/file/d/1w8RPBch1nPV8BpZIw0tFLPD1BmK0rkfN/view\n"
"- **Interactive tutorial (CodeLabs):** "
"https://codelabs-preview.appspot.com/?file_id=1eoeyKHeNX_qYq6m8oL37XLQMEoLCK7Xv02sBSGAGbwg#0"
)


PAGES = {
"About & Skills": render_about,
"Overview": render_overview,
"Benchmarks": render_benchmarks,
"Cost (Build vs Buy)": render_cost,
"Distribution Drift": render_drift,
"Reports": render_reports,
}

render_provenance()
PAGES[page]()
44 changes: 39 additions & 5 deletions dashboard/data_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ def _read_json(path: Path) -> Optional[Any]:
return None


def _latest(pattern: str) -> Optional[Path]:
"""Return the most recently modified file matching a glob under the repo."""
matches = glob.glob(str(REPO_ROOT / pattern))
def _latest(pattern: str, root: Path = REPO_ROOT) -> Optional[Path]:
"""Return the most recently modified file matching a glob under ``root``."""
matches = glob.glob(str(root / pattern))
if not matches:
return None
return Path(max(matches, key=os.path.getmtime))
Expand All @@ -55,6 +55,40 @@ def load_metrics_history(root: Path = REPO_ROOT) -> List[Dict[str, Any]]:
return sorted(data, key=lambda r: r.get("timestamp", ""))


def _all_timestamps(root: Path = REPO_ROOT) -> List[str]:
"""Collect ISO timestamps from every result file that records one."""
stamps: List[str] = []

bench = load_benchmark(root).get("benchmark_info", {}) or {}
stamps.append(bench.get("timestamp"))

stamps.append((load_drift(root) or {}).get("timestamp"))

history = load_metrics_history(root)
if history:
stamps.append(history[-1].get("timestamp"))

summary = _read_json(root / "evaluation" / "latest_evaluation_summary.json") or {}
stamps.append(summary.get("evaluation_timestamp"))

cost_info = (load_cost_analysis(root).get("analysis_info", {}) or {})
stamps.append(cost_info.get("timestamp"))

return [s for s in stamps if s]


def data_as_of(root: Path = REPO_ROOT) -> Optional[str]:
"""Return the date (YYYY-MM-DD) of the most recent recorded pipeline run.

Used to tell viewers the dashboard shows a static snapshot, not live data.
ISO-8601 strings sort lexicographically, so max() gives the latest.
"""
stamps = _all_timestamps(root)
if not stamps:
return None
return max(stamps)[:10]


def metric_cards(metrics: Dict[str, Any]) -> List[Dict[str, Any]]:
"""Shape the headline metrics into display cards with pass/fail vs thresholds.

Expand Down Expand Up @@ -115,7 +149,7 @@ def metric_cards(metrics: Dict[str, Any]) -> List[Dict[str, Any]]:
# --------------------------------------------------------------------------- #
def load_benchmark(root: Path = REPO_ROOT) -> Dict[str, Any]:
"""Load the most recent corrected pipeline benchmark."""
path = _latest("benchmarks/results/CORRECTED_pipeline_benchmark_*.json")
path = _latest("benchmarks/results/CORRECTED_pipeline_benchmark_*.json", root)
return _read_json(path) if path else {}


Expand Down Expand Up @@ -152,7 +186,7 @@ def benchmark_failures(benchmark: Dict[str, Any]) -> List[Dict[str, Any]]:
# --------------------------------------------------------------------------- #
def load_cost_analysis(root: Path = REPO_ROOT) -> Dict[str, Any]:
"""Load the most recent cloud vs infrastructure cost analysis."""
path = _latest("benchmarks/results/cost_analysis_*.json")
path = _latest("benchmarks/results/cost_analysis_*.json", root)
return _read_json(path) if path else {}


Expand Down
24 changes: 24 additions & 0 deletions tests/unit/test_dashboard_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,27 @@ def test_real_reports_listed():
names = {p.name for p in dl.list_reports()}
assert "benchmarks.md" in names
assert "xbrl_cross_verification_report.md" in names


# --- snapshot-date provenance ----------------------------------------------- #
def test_data_as_of_none_for_empty_root(tmp_path):
assert dl.data_as_of(tmp_path) is None


def test_data_as_of_picks_latest_date(tmp_path):
# Only a metrics history file exists; data_as_of should return its latest date.
hist_dir = tmp_path / "evaluation" / "metrics"
hist_dir.mkdir(parents=True)
(hist_dir / "metrics_history.json").write_text(
'[{"timestamp": "2024-01-01T10:00:00"}, '
'{"timestamp": "2025-06-15T12:00:00"}]',
encoding="utf-8",
)
assert dl.data_as_of(tmp_path) == "2025-06-15"


def test_data_as_of_real_data_is_a_date():
as_of = dl.data_as_of()
assert as_of is not None
# YYYY-MM-DD shape
assert len(as_of) == 10 and as_of[4] == "-" and as_of[7] == "-"
Loading