Codex commited on
Commit
eab742e
·
1 Parent(s): ff505fe

feat: add sponsor model strategy selector

Browse files
README.md CHANGED
@@ -23,6 +23,7 @@ GitHub repo: [awilliams88/neighbordocs](https://github.com/awilliams88/neighbord
23
 
24
  - Accepts a PDF, TXT, or MD upload plus optional user notes.
25
  - Extracts readable text from the first pages of a PDF.
 
26
  - Shows an extracted-text preview.
27
  - Produces a plain-English summary and next-step checklist.
28
 
@@ -47,9 +48,10 @@ Award surfaces:
47
  | Model or tool | Role | Status | Parameter count |
48
  | --- | --- | --- | --- |
49
  | `pypdf` | Basic PDF text extraction | Active | Not a model |
50
- | `nvidia/NVIDIA-Nemotron-Parse-v1.1` | Layout-aware PDF/PPT extraction | Planned | <1B class |
51
  | Small text reasoner | Summary, obligations, deadlines, next actions | Planned | <=4B target |
52
- | `openbmb/MiniCPM-V-4.6` | Optional scanned-image/document understanding | Candidate | 1B class |
 
53
 
54
  The final model list will stay within the hackathon rule that every model must
55
  be under 32B total parameters. The first planned sponsor path is NVIDIA
@@ -60,6 +62,7 @@ Nemotron Parse because document extraction is central to the product.
60
  ```text
61
  Upload + notes
62
  -> file type router
 
63
  -> PDF/text extraction
64
  -> extracted text preview
65
  -> model or rule-based document interpreter
@@ -72,6 +75,7 @@ Key files:
72
  - `src/neighbordocs/config.py` - app constants, URLs, and model plan.
73
  - `src/neighbordocs/core.py` - document extraction and analysis logic.
74
  - `src/neighbordocs/ui.py` - Gradio layout and event wiring.
 
75
  - `requirements.txt` - runtime dependencies.
76
  - `run.sh` - setup, lint, format, verify, and local app launch.
77
 
 
23
 
24
  - Accepts a PDF, TXT, or MD upload plus optional user notes.
25
  - Extracts readable text from the first pages of a PDF.
26
+ - Lets the user choose a sponsor-aligned model strategy.
27
  - Shows an extracted-text preview.
28
  - Produces a plain-English summary and next-step checklist.
29
 
 
48
  | Model or tool | Role | Status | Parameter count |
49
  | --- | --- | --- | --- |
50
  | `pypdf` | Basic PDF text extraction | Active | Not a model |
51
+ | `nvidia/NVIDIA-Nemotron-Parse-v1.1` | Layout-aware PDF/PPT extraction | Recommended path | <1B |
52
  | Small text reasoner | Summary, obligations, deadlines, next actions | Planned | <=4B target |
53
+ | `openbmb/MiniCPM-V-4.6` | Scanned-image/document understanding | Candidate | 1B class |
54
+ | Cohere tiny multilingual LLM family | Translation and local-language explanation | Candidate | 3.3B class |
55
 
56
  The final model list will stay within the hackathon rule that every model must
57
  be under 32B total parameters. The first planned sponsor path is NVIDIA
 
62
  ```text
63
  Upload + notes
64
  -> file type router
65
+ -> sponsor model strategy selector
66
  -> PDF/text extraction
67
  -> extracted text preview
68
  -> model or rule-based document interpreter
 
75
  - `src/neighbordocs/config.py` - app constants, URLs, and model plan.
76
  - `src/neighbordocs/core.py` - document extraction and analysis logic.
77
  - `src/neighbordocs/ui.py` - Gradio layout and event wiring.
78
+ - `examples/` - sample demo documents for judges and screenshots.
79
  - `requirements.txt` - runtime dependencies.
80
  - `run.sh` - setup, lint, format, verify, and local app launch.
81
 
examples/sample_bill.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ City Utilities
2
+ Statement Date: June 3, 2026
3
+ Account: 1047-5521
4
+ Amount Due: $82.40
5
+ Due Date: June 18, 2026
6
+
7
+ Your previous balance was paid in full. This bill covers electricity and water
8
+ service from May 1 through May 31. A late fee of $8.00 may be charged if payment
9
+ is received after the due date. Call customer support if the amount looks wrong
10
+ or if you need a payment extension.
examples/school_notice.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ Riverside Middle School
2
+ Family Notice
3
+
4
+ The seventh grade field trip is scheduled for Friday, June 12, 2026. Students
5
+ must bring a signed permission slip, packed lunch, water bottle, and comfortable
6
+ shoes. Buses leave at 8:30 AM and return by 3:15 PM. Permission slips are due by
7
+ Tuesday, June 9. Contact the front office if your child needs medication support
8
+ or transportation help.
requirements.txt CHANGED
@@ -1 +1,3 @@
 
1
  pypdf==6.13.0
 
 
1
+ gradio==6.16.0
2
  pypdf==6.13.0
3
+ ruff==0.15.16
run.sh CHANGED
@@ -13,7 +13,6 @@ setup() {
13
  # shellcheck disable=SC1091
14
  source .venv/bin/activate
15
  python -m pip install --disable-pip-version-check -r requirements.txt
16
- python -m pip install --disable-pip-version-check gradio==6.16.0 ruff==0.15.16
17
  }
18
 
19
  ensure_venv() {
 
13
  # shellcheck disable=SC1091
14
  source .venv/bin/activate
15
  python -m pip install --disable-pip-version-check -r requirements.txt
 
16
  }
17
 
18
  ensure_venv() {
src/neighbordocs/config.py CHANGED
@@ -6,27 +6,45 @@ APP_DESCRIPTION = "Plain-English help for everyday paperwork."
6
  SUPPORTED_SUFFIXES = {".pdf", ".txt", ".md"}
7
  PDF_PAGE_LIMIT = 3
8
  PREVIEW_LIMIT = 2000
 
9
 
10
  GITHUB_URL = "https://github.com/awilliams88/neighbordocs"
11
  SPACE_URL = "https://huggingface.co/spaces/build-small-hackathon/neighbordocs"
12
 
13
- MODEL_PLAN = [
14
- {
15
- "name": "pypdf",
16
- "role": "Basic PDF text extraction",
17
- "status": "active",
18
- "parameters": "not a model",
 
 
19
  },
20
- {
21
- "name": "nvidia/NVIDIA-Nemotron-Parse-v1.1",
22
- "role": "Planned layout-aware document parsing",
23
- "status": "planned",
24
- "parameters": "<1B class",
 
 
25
  },
26
- {
27
- "name": "Small <=4B text reasoner",
28
- "role": "Planned summary, obligations, deadlines, and next actions",
29
- "status": "planned",
 
 
 
 
 
 
 
30
  "parameters": "<=4B target",
 
 
 
31
  },
32
- ]
 
 
 
 
6
  SUPPORTED_SUFFIXES = {".pdf", ".txt", ".md"}
7
  PDF_PAGE_LIMIT = 3
8
  PREVIEW_LIMIT = 2000
9
+ DEFAULT_MODEL_KEY = "nvidia_parse"
10
 
11
  GITHUB_URL = "https://github.com/awilliams88/neighbordocs"
12
  SPACE_URL = "https://huggingface.co/spaces/build-small-hackathon/neighbordocs"
13
 
14
+ MODEL_CHOICES = {
15
+ "nvidia_parse": {
16
+ "label": "NVIDIA Nemotron Parse + tiny reasoner",
17
+ "model": "nvidia/NVIDIA-Nemotron-Parse-v1.1",
18
+ "parameters": "<1B parser + <=4B reasoner target",
19
+ "sponsor": "NVIDIA",
20
+ "best_for": "Forms, bills, reports, tables, structured PDFs, and document intelligence.",
21
+ "status": "recommended path",
22
  },
23
+ "openbmb_vision": {
24
+ "label": "OpenBMB MiniCPM-V document vision",
25
+ "model": "openbmb/MiniCPM-V-4.6",
26
+ "parameters": "1B class",
27
+ "sponsor": "OpenBMB",
28
+ "best_for": "Image uploads, screenshots, scanned paperwork, OCR-heavy documents.",
29
+ "status": "candidate path",
30
  },
31
+ "cohere_multilingual": {
32
+ "label": "Cohere tiny multilingual explanation",
33
+ "model": "Cohere tiny multilingual LLM family",
34
+ "parameters": "3.3B class",
35
+ "sponsor": "Cohere",
36
+ "best_for": "Translation, multilingual summaries, cross-lingual paperwork help.",
37
+ "status": "candidate path",
38
+ },
39
+ "tiny_local": {
40
+ "label": "Tiny local reasoner",
41
+ "model": "MiniCPM 1B / <=4B local text model",
42
  "parameters": "<=4B target",
43
+ "sponsor": "Tiny Titan / Off the Grid",
44
+ "best_for": "Small CPU/ZeroGPU-friendly summary and checklist generation.",
45
+ "status": "badge path",
46
  },
47
+ }
48
+
49
+ MODEL_LABELS = [choice["label"] for choice in MODEL_CHOICES.values()]
50
+ MODEL_KEY_BY_LABEL = {choice["label"]: key for key, choice in MODEL_CHOICES.items()}
src/neighbordocs/core.py CHANGED
@@ -5,12 +5,20 @@ from pathlib import Path
5
 
6
  from pypdf import PdfReader
7
 
8
- from .config import PDF_PAGE_LIMIT, PREVIEW_LIMIT, SUPPORTED_SUFFIXES
 
 
 
 
 
 
 
9
 
10
 
11
  @dataclass(frozen=True)
12
  class DocumentReport:
13
  preview: str
 
14
  summary: str
15
  checklist: str
16
 
@@ -32,15 +40,31 @@ def extract_text(file_path: str | None) -> str:
32
  return f"Unsupported file type: {suffix or 'unknown'}. Try one of: {allowed}."
33
 
34
 
35
- def analyze_document(file_path: str | None, notes: str) -> DocumentReport:
 
 
 
 
36
  text = extract_text(file_path)
37
  preview = text[:PREVIEW_LIMIT] if text else "No readable text found."
38
  user_context = notes.strip()
 
39
 
40
- summary = _build_summary(text, user_context)
41
- checklist = _build_checklist(text, user_context)
 
42
 
43
- return DocumentReport(preview=preview, summary=summary, checklist=checklist)
 
 
 
 
 
 
 
 
 
 
44
 
45
 
46
  def _extract_pdf_text(path: Path) -> str:
@@ -50,14 +74,27 @@ def _extract_pdf_text(path: Path) -> str:
50
  return text or "No text could be extracted from the PDF."
51
 
52
 
53
- def _build_summary(text: str, notes: str) -> str:
 
 
 
 
 
 
 
 
 
 
 
 
54
  if not text or text == "No file uploaded.":
55
  return "Upload a document to generate a plain-English explanation."
56
 
57
  bullets = [
58
- "This first version extracts readable document text and prepares it for small-model reasoning.",
59
- "The final app will explain the document, surface obligations, and identify dates or next actions.",
60
- "The next implementation step is adding the selected sponsor model path from the README.",
 
61
  ]
62
 
63
  if notes:
@@ -66,7 +103,7 @@ def _build_summary(text: str, notes: str) -> str:
66
  return "\n".join(f"- {bullet}" for bullet in bullets)
67
 
68
 
69
- def _build_checklist(text: str, notes: str) -> str:
70
  if not text or text == "No file uploaded.":
71
  return "- Upload a PDF, TXT, or MD file."
72
 
@@ -74,6 +111,7 @@ def _build_checklist(text: str, notes: str) -> str:
74
  "Confirm the document type and sender.",
75
  "Look for due dates, payment amounts, signatures, or missing attachments.",
76
  "Ask a follow-up question if any term or obligation is unclear.",
 
77
  ]
78
 
79
  if notes:
 
5
 
6
  from pypdf import PdfReader
7
 
8
+ from .config import (
9
+ DEFAULT_MODEL_KEY,
10
+ MODEL_CHOICES,
11
+ MODEL_KEY_BY_LABEL,
12
+ PDF_PAGE_LIMIT,
13
+ PREVIEW_LIMIT,
14
+ SUPPORTED_SUFFIXES,
15
+ )
16
 
17
 
18
  @dataclass(frozen=True)
19
  class DocumentReport:
20
  preview: str
21
+ model_path: str
22
  summary: str
23
  checklist: str
24
 
 
40
  return f"Unsupported file type: {suffix or 'unknown'}. Try one of: {allowed}."
41
 
42
 
43
+ def analyze_document(
44
+ file_path: str | None,
45
+ notes: str,
46
+ model_label: str | None,
47
+ ) -> DocumentReport:
48
  text = extract_text(file_path)
49
  preview = text[:PREVIEW_LIMIT] if text else "No readable text found."
50
  user_context = notes.strip()
51
+ model_choice = get_model_choice(model_label)
52
 
53
+ model_path = _build_model_path(model_choice)
54
+ summary = _build_summary(text, user_context, model_choice)
55
+ checklist = _build_checklist(text, user_context, model_choice)
56
 
57
+ return DocumentReport(
58
+ preview=preview,
59
+ model_path=model_path,
60
+ summary=summary,
61
+ checklist=checklist,
62
+ )
63
+
64
+
65
+ def get_model_choice(model_label: str | None) -> dict[str, str]:
66
+ key = MODEL_KEY_BY_LABEL.get(model_label or "", DEFAULT_MODEL_KEY)
67
+ return MODEL_CHOICES[key]
68
 
69
 
70
  def _extract_pdf_text(path: Path) -> str:
 
74
  return text or "No text could be extracted from the PDF."
75
 
76
 
77
+ def _build_model_path(model_choice: dict[str, str]) -> str:
78
+ return "\n".join(
79
+ [
80
+ f"Selected path: {model_choice['label']}",
81
+ f"Primary model: {model_choice['model']}",
82
+ f"Sponsor surface: {model_choice['sponsor']}",
83
+ f"Parameter compliance: {model_choice['parameters']}",
84
+ f"Status: {model_choice['status']}",
85
+ ]
86
+ )
87
+
88
+
89
+ def _build_summary(text: str, notes: str, model_choice: dict[str, str]) -> str:
90
  if not text or text == "No file uploaded.":
91
  return "Upload a document to generate a plain-English explanation."
92
 
93
  bullets = [
94
+ "Readable document text was extracted and prepared for small-model reasoning.",
95
+ f"Recommended sponsor path: {model_choice['label']}.",
96
+ f"Why this path fits: {model_choice['best_for']}",
97
+ "The final model-backed version will explain the document, surface obligations, and identify dates or next actions.",
98
  ]
99
 
100
  if notes:
 
103
  return "\n".join(f"- {bullet}" for bullet in bullets)
104
 
105
 
106
+ def _build_checklist(text: str, notes: str, model_choice: dict[str, str]) -> str:
107
  if not text or text == "No file uploaded.":
108
  return "- Upload a PDF, TXT, or MD file."
109
 
 
111
  "Confirm the document type and sender.",
112
  "Look for due dates, payment amounts, signatures, or missing attachments.",
113
  "Ask a follow-up question if any term or obligation is unclear.",
114
+ f"Use the selected model path for: {model_choice['best_for']}",
115
  ]
116
 
117
  if notes:
src/neighbordocs/ui.py CHANGED
@@ -2,7 +2,15 @@ from __future__ import annotations
2
 
3
  import gradio as gr
4
 
5
- from .config import APP_DESCRIPTION, APP_TITLE, GITHUB_URL, SPACE_URL
 
 
 
 
 
 
 
 
6
  from .core import analyze_document
7
 
8
 
@@ -21,25 +29,57 @@ def create_app() -> gr.Blocks:
21
  placeholder="Example: explain this bill and tell me what I need to do next.",
22
  )
23
 
 
 
 
 
 
 
 
24
  run_button = gr.Button("Analyze", variant="primary")
25
 
26
  with gr.Row():
27
  extracted_output = gr.Textbox(label="Extracted text", lines=12)
 
 
 
28
  summary_output = gr.Textbox(label="Plain-English summary", lines=12)
 
29
 
30
- checklist_output = gr.Textbox(label="Next-action checklist", lines=8)
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
  gr.Markdown(f"[GitHub repo]({GITHUB_URL}) | [Hugging Face Space]({SPACE_URL})")
33
 
34
  run_button.click(
35
  fn=_analyze_for_ui,
36
- inputs=[file_input, notes_input],
37
- outputs=[extracted_output, summary_output, checklist_output],
 
 
 
 
 
38
  )
39
 
40
  return demo
41
 
42
 
43
- def _analyze_for_ui(file_path: str | None, notes: str) -> tuple[str, str, str]:
44
- report = analyze_document(file_path, notes)
45
- return report.preview, report.summary, report.checklist
 
 
 
 
 
2
 
3
  import gradio as gr
4
 
5
+ from .config import (
6
+ APP_DESCRIPTION,
7
+ APP_TITLE,
8
+ DEFAULT_MODEL_KEY,
9
+ GITHUB_URL,
10
+ MODEL_CHOICES,
11
+ MODEL_LABELS,
12
+ SPACE_URL,
13
+ )
14
  from .core import analyze_document
15
 
16
 
 
29
  placeholder="Example: explain this bill and tell me what I need to do next.",
30
  )
31
 
32
+ model_input = gr.Dropdown(
33
+ choices=MODEL_LABELS,
34
+ value=MODEL_CHOICES[DEFAULT_MODEL_KEY]["label"],
35
+ label="Model strategy",
36
+ interactive=True,
37
+ )
38
+
39
  run_button = gr.Button("Analyze", variant="primary")
40
 
41
  with gr.Row():
42
  extracted_output = gr.Textbox(label="Extracted text", lines=12)
43
+ model_output = gr.Textbox(label="Selected model path", lines=8)
44
+
45
+ with gr.Row():
46
  summary_output = gr.Textbox(label="Plain-English summary", lines=12)
47
+ checklist_output = gr.Textbox(label="Next-action checklist", lines=12)
48
 
49
+ gr.Examples(
50
+ examples=[
51
+ [
52
+ "examples/sample_bill.txt",
53
+ "Explain what I owe and what happens next.",
54
+ ],
55
+ [
56
+ "examples/school_notice.txt",
57
+ "Summarize what a parent needs to remember.",
58
+ ],
59
+ ],
60
+ inputs=[file_input, notes_input],
61
+ )
62
 
63
  gr.Markdown(f"[GitHub repo]({GITHUB_URL}) | [Hugging Face Space]({SPACE_URL})")
64
 
65
  run_button.click(
66
  fn=_analyze_for_ui,
67
+ inputs=[file_input, notes_input, model_input],
68
+ outputs=[
69
+ extracted_output,
70
+ model_output,
71
+ summary_output,
72
+ checklist_output,
73
+ ],
74
  )
75
 
76
  return demo
77
 
78
 
79
+ def _analyze_for_ui(
80
+ file_path: str | None,
81
+ notes: str,
82
+ model_label: str | None,
83
+ ) -> tuple[str, str, str, str]:
84
+ report = analyze_document(file_path, notes, model_label)
85
+ return report.preview, report.model_path, report.summary, report.checklist