airayven7 commited on
Commit
55adaba
·
verified ·
1 Parent(s): 79d9660

Sync from GitHub 3e78bca

Browse files
Files changed (1) hide show
  1. app.py +129 -63
app.py CHANGED
@@ -12,8 +12,9 @@ the pre-indexed library and answers questions):
12
  retrieval is dense cosine over chunks with parent-page lookup.
13
 
14
  Both hand the retrieved page images to MiniCPM-V for the grounded answer,
15
- in one ZeroGPU call per question. The approach is picked per question in
16
- the UI; each approach has its own store directory and manual list.
 
17
 
18
  Module layout:
19
  models/colembed.py ColEmbed — visual: page embeddings + MaxSim
@@ -27,6 +28,7 @@ Module layout:
27
 
28
  import os
29
  import shutil
 
30
 
31
  import gradio as gr
32
  from huggingface_hub import snapshot_download
@@ -56,11 +58,6 @@ LIBRARIES = {
56
  ),
57
  }
58
 
59
- METHOD_CHOICES = [
60
- ("Visual — ColEmbed page embeddings + MaxSim", "visual"),
61
- ("Parsed — Nemotron Parse chunks + dense retrieval", "parsed"),
62
- ]
63
-
64
 
65
  def sync_library() -> None:
66
  """Pull pre-indexed manuals from the library dataset into /data.
@@ -87,82 +84,151 @@ def sync_library() -> None:
87
  sync_library()
88
 
89
 
90
- def _manuals_update(method: str, current: str | None = None):
91
- """Dropdown update for the chosen approach's library; keeps the current
92
- selection when the same manual is indexed under both approaches."""
93
- store, _ = LIBRARIES[method]
94
- choices = [(d["name"], d["doc_id"]) for d in store.list_docs()]
95
- ids = [doc_id for _, doc_id in choices]
96
- return gr.update(choices=choices, value=current if current in ids else None)
97
-
98
-
99
- def switch_method(method, doc_id):
100
- return _manuals_update(method, doc_id)
101
-
102
-
103
- def refresh_library(method, doc_id):
 
 
 
 
 
104
  """Re-pull the library dataset (incremental) and refresh the dropdown,
105
  so manuals indexed after the Space booted show up without a restart."""
106
  sync_library()
107
- return _manuals_update(method, doc_id)
 
 
108
 
109
 
110
- def ask_library(question, doc_id, method):
 
 
 
111
  if not doc_id:
112
- raise gr.Error("Pick a manual first.")
113
  store, pipeline = LIBRARIES[method]
 
 
 
114
  try:
115
- return pipeline.run(store, question, [doc_id], DEFAULT_TOP_K)
116
  except ValueError as e:
117
- raise gr.Error(str(e)) from e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
 
119
 
120
  with gr.Blocks(title="Repair Guy") as demo:
121
  gr.Markdown(
122
  "# 🔧 Repair Guy\n"
123
- "Ask questions over repair manuals, comparing two local-only retrieval "
124
- "approaches over the same library:\n"
125
- "- **Visual** — pages retrieved as images with "
126
- "[Nemotron ColEmbed v2](https://huggingface.co/nvidia/nemotron-colembed-vl-4b-v2) "
127
- "(late interaction, no parsing).\n"
128
- "- **Parsed** — pages parsed with "
129
- "[Nemotron Parse](https://huggingface.co/nvidia/NVIDIA-Nemotron-Parse-v1.2), "
130
- "figures/tables described by MiniCPM-V, section chunks retrieved with "
131
- "[Llama Nemotron Embed](https://huggingface.co/nvidia/llama-nemotron-embed-vl-1b-v2) "
132
- "and mapped back to their pages.\n\n"
133
- "Either way, MiniCPM-V reads the retrieved pages and answers."
134
  )
135
- with gr.Row():
 
136
  with gr.Column(scale=1):
137
- method_in = gr.Radio(
138
- METHOD_CHOICES, value="visual", label="Retrieval approach"
139
- )
140
  manual_in = gr.Dropdown(label="Manual", choices=[])
141
- lib_refresh_btn = gr.Button("🔄 Sync library", size="sm")
142
- lib_question_in = gr.Textbox(
 
143
  label="Question",
144
  lines=2,
145
  placeholder="e.g. What is the tightening torque for the universal joint flange bolts?",
146
  )
147
- lib_ask_btn = gr.Button("Ask", variant="primary")
148
- with gr.Column(scale=2):
149
- lib_answer_out = gr.Markdown(label="Answer")
150
- lib_pages_out = gr.Gallery(label="Pages used", columns=3, height=420)
151
-
152
- method_in.change(switch_method, inputs=[method_in, manual_in], outputs=[manual_in])
153
- lib_refresh_btn.click(
154
- refresh_library, inputs=[method_in, manual_in], outputs=[manual_in]
155
- )
156
- lib_ask_btn.click(
157
- ask_library, inputs=[lib_question_in, manual_in, method_in],
158
- outputs=[lib_answer_out, lib_pages_out],
159
- )
160
- lib_question_in.submit(
161
- ask_library, inputs=[lib_question_in, manual_in, method_in],
162
- outputs=[lib_answer_out, lib_pages_out],
163
- )
164
- demo.load(switch_method, inputs=[method_in, manual_in], outputs=[manual_in])
165
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
  if __name__ == "__main__":
168
- demo.launch()
 
12
  retrieval is dense cosine over chunks with parent-page lookup.
13
 
14
  Both hand the retrieved page images to MiniCPM-V for the grounded answer,
15
+ in one ZeroGPU call per question. The UI is a side-by-side comparison: one
16
+ manual, one question, and each approach answers in its own column (its own
17
+ GPU call) so retrieval quality and latency can be compared directly.
18
 
19
  Module layout:
20
  models/colembed.py ColEmbed — visual: page embeddings + MaxSim
 
28
 
29
  import os
30
  import shutil
31
+ import time
32
 
33
  import gradio as gr
34
  from huggingface_hub import snapshot_download
 
58
  ),
59
  }
60
 
 
 
 
 
 
61
 
62
  def sync_library() -> None:
63
  """Pull pre-indexed manuals from the library dataset into /data.
 
84
  sync_library()
85
 
86
 
87
+ def _manual_choices() -> list[tuple[str, str]]:
88
+ """One shared dropdown across both libraries (doc ids are name slugs, so
89
+ the same manual lands on the same id in both); manuals indexed under only
90
+ one approach are labeled with it."""
91
+ docs: dict[str, dict] = {}
92
+ for method, (store, _) in LIBRARIES.items():
93
+ for d in store.list_docs():
94
+ entry = docs.setdefault(d["doc_id"], {"name": d["name"], "methods": []})
95
+ entry["methods"].append(method)
96
+ choices = []
97
+ for doc_id, info in sorted(docs.items(), key=lambda kv: kv[1]["name"].lower()):
98
+ label = info["name"]
99
+ if len(info["methods"]) < len(LIBRARIES):
100
+ label += f" — {info['methods'][0]} only"
101
+ choices.append((label, doc_id))
102
+ return choices
103
+
104
+
105
+ def refresh_library(doc_id):
106
  """Re-pull the library dataset (incremental) and refresh the dropdown,
107
  so manuals indexed after the Space booted show up without a restart."""
108
  sync_library()
109
+ choices = _manual_choices()
110
+ ids = [v for _, v in choices]
111
+ return gr.update(choices=choices, value=doc_id if doc_id in ids else None)
112
 
113
 
114
+ def _ask(method: str, question, doc_id):
115
+ """One approach's column: (timing line, answer markdown, gallery).
116
+ Soft in-column messages instead of gr.Error so that when both columns run
117
+ off one click, one failing doesn't kill the other."""
118
  if not doc_id:
119
+ return "", "*Pick a manual first.*", []
120
  store, pipeline = LIBRARIES[method]
121
+ if not store.exists(doc_id):
122
+ return "", f"*This manual isn't indexed with the {method} approach yet.*", []
123
+ start = time.monotonic()
124
  try:
125
+ answer, gallery = pipeline.run(store, question, [doc_id], DEFAULT_TOP_K)
126
  except ValueError as e:
127
+ return "", f"*{e}*", []
128
+ return f"⏱️ answered in {time.monotonic() - start:.1f}s", answer, gallery
129
+
130
+
131
+ def ask_visual(question, doc_id):
132
+ return _ask("visual", question, doc_id)
133
+
134
+
135
+ def ask_parsed(question, doc_id):
136
+ return _ask("parsed", question, doc_id)
137
+
138
+
139
+ CSS = """
140
+ .app-header { text-align: center; margin: 0.5em 0 0.2em; }
141
+ .app-header p { color: var(--body-text-color-subdued); margin-top: 0.3em; }
142
+ .approach-card {
143
+ border-radius: 14px !important;
144
+ border-top: 4px solid var(--card-accent) !important;
145
+ }
146
+ .visual-card { --card-accent: #e8590c; }
147
+ .parsed-card { --card-accent: #0c8599; }
148
+ .approach-card h3 { margin: 0.1em 0 0; }
149
+ .pipeline-steps {
150
+ color: var(--body-text-color-subdued);
151
+ font-size: 0.85em;
152
+ line-height: 1.7;
153
+ }
154
+ .pipeline-steps code { font-size: 0.95em; }
155
+ .timing { color: var(--body-text-color-subdued); font-size: 0.9em; min-height: 1.2em; }
156
+ """
157
+
158
+ VISUAL_CARD = """### 🖼️ Visual
159
+ **ColEmbed late interaction** — pages stay images; nothing is parsed or chunked.
160
+
161
+ <div class="pipeline-steps">
162
+
163
+ `page image` → `multi-vector embedding` → `MaxSim vs. query` → `top pages` → `MiniCPM‑V answers`
164
+
165
+ **Index:** heavy (5–12 MB/page) · **Strengths:** immune to parsing errors, sees layout and diagrams natively
166
+ </div>"""
167
+
168
+ PARSED_CARD = """### 📄 Parsed
169
+ **Parse + dense chunks** — pages become structured text; figures and tables become descriptions.
170
+
171
+ <div class="pipeline-steps">
172
+
173
+ `Nemotron Parse` → `MiniCPM‑V describes figures/tables` → `section chunks` → `dense cosine` → `parent pages` → `MiniCPM‑V answers`
174
+
175
+ **Index:** tiny (a few MB/manual) · **Strengths:** inspectable chunks, precise hits on one table or diagram
176
+ </div>"""
177
 
178
 
179
  with gr.Blocks(title="Repair Guy") as demo:
180
  gr.Markdown(
181
  "# 🔧 Repair Guy\n"
182
+ "Two local-only ways to search a repair manual, side by side pick a "
183
+ "manual, ask once, compare what each approach retrieves and answers.",
184
+ elem_classes="app-header",
 
 
 
 
 
 
 
 
185
  )
186
+
187
+ with gr.Row(equal_height=True):
188
  with gr.Column(scale=1):
 
 
 
189
  manual_in = gr.Dropdown(label="Manual", choices=[])
190
+ refresh_btn = gr.Button("🔄 Sync library", size="sm")
191
+ with gr.Column(scale=2):
192
+ question_in = gr.Textbox(
193
  label="Question",
194
  lines=2,
195
  placeholder="e.g. What is the tightening torque for the universal joint flange bolts?",
196
  )
197
+ with gr.Row():
198
+ both_btn = gr.Button("⚡ Ask both", variant="primary")
199
+
200
+ with gr.Row(equal_height=False):
201
+ with gr.Column(variant="panel", elem_classes="approach-card visual-card"):
202
+ gr.Markdown(VISUAL_CARD)
203
+ vis_btn = gr.Button("Ask with Visual", size="sm")
204
+ vis_time = gr.Markdown(elem_classes="timing")
205
+ vis_answer = gr.Markdown(label="Answer")
206
+ vis_pages = gr.Gallery(label="Pages used", columns=3, height=300)
207
+ with gr.Column(variant="panel", elem_classes="approach-card parsed-card"):
208
+ gr.Markdown(PARSED_CARD)
209
+ par_btn = gr.Button("Ask with Parsed", size="sm")
210
+ par_time = gr.Markdown(elem_classes="timing")
211
+ par_answer = gr.Markdown(label="Answer")
212
+ par_pages = gr.Gallery(label="Pages used", columns=3, height=300)
213
+
214
+ inputs = [question_in, manual_in]
215
+ vis_outputs = [vis_time, vis_answer, vis_pages]
216
+ par_outputs = [par_time, par_answer, par_pages]
217
+
218
+ vis_btn.click(ask_visual, inputs=inputs, outputs=vis_outputs)
219
+ par_btn.click(ask_parsed, inputs=inputs, outputs=par_outputs)
220
+ # Two listeners on one event: both columns run off a single click/submit.
221
+ both_btn.click(ask_visual, inputs=inputs, outputs=vis_outputs)
222
+ both_btn.click(ask_parsed, inputs=inputs, outputs=par_outputs)
223
+ question_in.submit(ask_visual, inputs=inputs, outputs=vis_outputs)
224
+ question_in.submit(ask_parsed, inputs=inputs, outputs=par_outputs)
225
+
226
+ refresh_btn.click(refresh_library, inputs=[manual_in], outputs=[manual_in])
227
+ demo.load(lambda: gr.update(choices=_manual_choices()), outputs=[manual_in])
228
+
229
+
230
+ # Gradio 6 takes theme/css at launch(), not in the Blocks constructor.
231
+ LAUNCH_KWARGS = dict(theme=gr.themes.Soft(primary_hue="orange"), css=CSS)
232
 
233
  if __name__ == "__main__":
234
+ demo.launch(**LAUNCH_KWARGS)