File size: 3,789 Bytes
436dbdd deb5a88 436dbdd deb5a88 c63fd20 deb5a88 436dbdd deb5a88 436dbdd deb5a88 436dbdd deb5a88 436dbdd deb5a88 436dbdd deb5a88 436dbdd deb5a88 335025a 436dbdd 335025a 436dbdd 335025a 436dbdd deb5a88 436dbdd c63fd20 436dbdd deb5a88 c63fd20 deb5a88 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | """Gradio + ZeroGPU Space: upload a repair manual, ask questions.
Visual RAG with no parsing/chunking: every PDF page is embedded as an image
with Nemotron ColEmbed v2 (multi-vector, late interaction) when the manual is
uploaded. A question is answered by embedding the query, running MaxSim over
the page embeddings streamed from disk, and handing the top pages to MiniCPM-V.
Module layout:
models/colembed.py ColEmbed β embedding model + GPU embed/search
models/minicpm.py MiniCPM β remote VLM that answers over page images
core/store.py Store β on-disk per-page token embeddings
core/pdf.py render_pages β PDF -> RGB page images (CPU)
pipelines/ingest.py IngestPipeline β PDF -> embeddings -> store
pipelines/ask.py AskPipeline β question -> retrieve -> answer
app.py this file: builds the objects + Gradio UI
"""
import os
import gradio as gr
from core.constants import DEFAULT_TOP_K
from core.store import Store, slugify
from models.colembed import ColEmbed
from models.minicpm import MiniCPM
from pipelines.ask import AskPipeline
from pipelines.ingest import IngestPipeline
# Construct once at startup (the model loads onto cuda here, in the main process).
store = Store()
embedder = ColEmbed()
ingest_pipeline = IngestPipeline(embedder, store)
ask_pipeline = AskPipeline(embedder, store, MiniCPM())
def index_manual(pdf_file, progress=gr.Progress()):
"""Runs on upload: embed the manual's pages (or reuse a previous index)."""
if not pdf_file:
return None, ""
name = os.path.splitext(os.path.basename(pdf_file))[0].replace("_", " ")
doc_id = slugify(name)
if store.exists(doc_id):
pages = len(store.meta(doc_id)["pages"])
return doc_id, f"**{name}** is already indexed ({pages} pages) β ask away."
try:
doc = ingest_pipeline.run(
pdf_file, name, lambda frac, desc: progress(frac, desc=desc)
)
except ValueError as e:
raise gr.Error(str(e)) from e
return doc_id, f"Indexed **{doc['name']}** ({doc['pages']} pages) β ask away."
def answer_question(question, doc_id):
if not doc_id:
raise gr.Error("Upload a repair manual first.")
try:
return ask_pipeline.run(question, [doc_id], DEFAULT_TOP_K)
except ValueError as e:
raise gr.Error(str(e)) from e
with gr.Blocks(title="Repair Guy") as demo:
gr.Markdown(
"# π§ Repair Guy\n"
"Upload a repair manual (PDF) and ask it questions. Pages are embedded "
"with [Nemotron ColEmbed v2](https://huggingface.co/nvidia/nemotron-colembed-vl-4b-v2) "
"on upload; answers come from MiniCPM-V reading the most relevant pages."
)
doc_state = gr.State(None)
with gr.Row():
with gr.Column(scale=1):
pdf_in = gr.File(
label="Repair manual (PDF)", file_types=[".pdf"], type="filepath"
)
status_out = gr.Markdown()
question_in = gr.Textbox(
label="Question",
lines=2,
placeholder="e.g. What is the tightening torque for the universal joint flange bolts?",
)
ask_btn = gr.Button("Ask", variant="primary")
with gr.Column(scale=2):
answer_out = gr.Markdown(label="Answer")
pages_out = gr.Gallery(label="Pages used", columns=3, height=420)
pdf_in.upload(index_manual, inputs=[pdf_in], outputs=[doc_state, status_out])
ask_btn.click(
answer_question, inputs=[question_in, doc_state], outputs=[answer_out, pages_out]
)
question_in.submit(
answer_question, inputs=[question_in, doc_state], outputs=[answer_out, pages_out]
)
if __name__ == "__main__":
demo.launch()
|