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()