Sync from GitHub 72cbdd8
Browse files
README.md
CHANGED
|
@@ -11,6 +11,7 @@ pinned: false
|
|
| 11 |
preload_from_hub:
|
| 12 |
- nvidia/nemotron-colembed-vl-4b-v2
|
| 13 |
- openbmb/MiniCPM-V-4_5
|
|
|
|
| 14 |
license: mit
|
| 15 |
---
|
| 16 |
|
|
|
|
| 11 |
preload_from_hub:
|
| 12 |
- nvidia/nemotron-colembed-vl-4b-v2
|
| 13 |
- openbmb/MiniCPM-V-4_5
|
| 14 |
+
- nvidia/llama-nemotron-embed-vl-1b-v2
|
| 15 |
license: mit
|
| 16 |
---
|
| 17 |
|
app.py
CHANGED
|
@@ -12,7 +12,8 @@ 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.
|
|
|
|
| 16 |
|
| 17 |
Module layout:
|
| 18 |
models/colembed.py ColEmbed β visual: page embeddings + MaxSim
|
|
@@ -21,7 +22,7 @@ Module layout:
|
|
| 21 |
core/visual_store.py VisualStore β on-disk per-page token embeddings
|
| 22 |
core/parsed_store.py ParsedStore β chunks + dense embedding matrix
|
| 23 |
pipelines/visual_ask.py VisualAskPipeline
|
| 24 |
-
pipelines/parsed_ask.py ParsedAskPipeline
|
| 25 |
"""
|
| 26 |
|
| 27 |
import os
|
|
@@ -37,12 +38,28 @@ from core.constants import (
|
|
| 37 |
PREINDEXED_DIR,
|
| 38 |
VISUAL_SUBDIR,
|
| 39 |
)
|
|
|
|
| 40 |
from core.visual_store import VisualStore
|
|
|
|
| 41 |
from pipelines.visual_ask import VisualAskPipeline
|
| 42 |
|
| 43 |
-
# Construct once at startup (the models load onto cuda here, in the main
|
| 44 |
-
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
def sync_library() -> None:
|
|
@@ -70,22 +87,32 @@ def sync_library() -> None:
|
|
| 70 |
sync_library()
|
| 71 |
|
| 72 |
|
| 73 |
-
def
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
"""Re-pull the library dataset (incremental) and refresh the dropdown,
|
| 79 |
so manuals indexed after the Space booted show up without a restart."""
|
| 80 |
sync_library()
|
| 81 |
-
return
|
| 82 |
|
| 83 |
|
| 84 |
-
def ask_library(question, doc_id):
|
| 85 |
if not doc_id:
|
| 86 |
raise gr.Error("Pick a manual first.")
|
|
|
|
| 87 |
try:
|
| 88 |
-
return
|
| 89 |
except ValueError as e:
|
| 90 |
raise gr.Error(str(e)) from e
|
| 91 |
|
|
@@ -93,13 +120,23 @@ def ask_library(question, doc_id):
|
|
| 93 |
with gr.Blocks(title="Repair Guy") as demo:
|
| 94 |
gr.Markdown(
|
| 95 |
"# π§ Repair Guy\n"
|
| 96 |
-
"Ask questions over repair manuals
|
|
|
|
|
|
|
| 97 |
"[Nemotron ColEmbed v2](https://huggingface.co/nvidia/nemotron-colembed-vl-4b-v2) "
|
| 98 |
-
"(late interaction, no parsing)
|
| 99 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
)
|
| 101 |
with gr.Row():
|
| 102 |
with gr.Column(scale=1):
|
|
|
|
|
|
|
|
|
|
| 103 |
manual_in = gr.Dropdown(label="Manual", choices=[])
|
| 104 |
lib_refresh_btn = gr.Button("π Sync library", size="sm")
|
| 105 |
lib_question_in = gr.Textbox(
|
|
@@ -112,16 +149,19 @@ with gr.Blocks(title="Repair Guy") as demo:
|
|
| 112 |
lib_answer_out = gr.Markdown(label="Answer")
|
| 113 |
lib_pages_out = gr.Gallery(label="Pages used", columns=3, height=420)
|
| 114 |
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
| 116 |
lib_ask_btn.click(
|
| 117 |
-
ask_library, inputs=[lib_question_in, manual_in],
|
| 118 |
outputs=[lib_answer_out, lib_pages_out],
|
| 119 |
)
|
| 120 |
lib_question_in.submit(
|
| 121 |
-
ask_library, inputs=[lib_question_in, manual_in],
|
| 122 |
outputs=[lib_answer_out, lib_pages_out],
|
| 123 |
)
|
| 124 |
-
demo.load(
|
| 125 |
|
| 126 |
|
| 127 |
if __name__ == "__main__":
|
|
|
|
| 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
|
|
|
|
| 22 |
core/visual_store.py VisualStore β on-disk per-page token embeddings
|
| 23 |
core/parsed_store.py ParsedStore β chunks + dense embedding matrix
|
| 24 |
pipelines/visual_ask.py VisualAskPipeline
|
| 25 |
+
pipelines/parsed_ask.py ParsedAskPipeline
|
| 26 |
"""
|
| 27 |
|
| 28 |
import os
|
|
|
|
| 38 |
PREINDEXED_DIR,
|
| 39 |
VISUAL_SUBDIR,
|
| 40 |
)
|
| 41 |
+
from core.parsed_store import ParsedStore
|
| 42 |
from core.visual_store import VisualStore
|
| 43 |
+
from pipelines.parsed_ask import ParsedAskPipeline
|
| 44 |
from pipelines.visual_ask import VisualAskPipeline
|
| 45 |
|
| 46 |
+
# Construct once at startup (the models load onto cuda here, in the main
|
| 47 |
+
# process). Both pipelines expose the same run(store, question, doc_ids, top_k).
|
| 48 |
+
LIBRARIES = {
|
| 49 |
+
"visual": (
|
| 50 |
+
VisualStore(os.path.join(PREINDEXED_DIR, VISUAL_SUBDIR)),
|
| 51 |
+
VisualAskPipeline(),
|
| 52 |
+
),
|
| 53 |
+
"parsed": (
|
| 54 |
+
ParsedStore(os.path.join(PREINDEXED_DIR, PARSED_SUBDIR)),
|
| 55 |
+
ParsedAskPipeline(),
|
| 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:
|
|
|
|
| 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 |
|
|
|
|
| 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(
|
|
|
|
| 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__":
|