Juan Esteban Agudelo Ortiz
increased the max tokens generation for avoiding final rendering errors.
70b5ee4 | import re | |
| import tempfile | |
| from pathlib import Path | |
| import gradio as gr | |
| from huggingface_hub import hf_hub_download | |
| # ── Directory setup ──────────────────────────────────────────────────────────── | |
| BASE_DIR = Path(".") | |
| DATA_DIR = BASE_DIR / "data" | |
| MODELS_DIR = DATA_DIR / "models" | |
| INDEX_DIR = DATA_DIR / "index" | |
| UPLOADS_DIR = DATA_DIR / "uploads" | |
| OUT_DIR = DATA_DIR / "outputs" | |
| for d in [MODELS_DIR, INDEX_DIR, UPLOADS_DIR, OUT_DIR]: | |
| d.mkdir(parents=True, exist_ok=True) | |
| # ── Model download ───────────────────────────────────────────────────────────── | |
| GENERATOR_PATH = MODELS_DIR / "qwen2.5-3b-instruct-q4_k_m.gguf" | |
| if not GENERATOR_PATH.exists(): | |
| print("Downloading Qwen2.5-3B-Instruct (~2GB)...") | |
| hf_hub_download( | |
| repo_id = "Qwen/Qwen2.5-3B-Instruct-GGUF", | |
| filename = "qwen2.5-3b-instruct-q4_k_m.gguf", | |
| local_dir = str(MODELS_DIR), | |
| ) | |
| print("Download complete.") | |
| # ── Pipeline initialization (runs once at startup) ───────────────────────────── | |
| from src.indexing import get_embed_model | |
| from src.generation import load_language_model | |
| embed_model = get_embed_model() | |
| llm = load_language_model(GENERATOR_PATH) | |
| # ── Helper imports (after model init to avoid circular import timing) ────────── | |
| import fitz | |
| from src.chunking import fixed_size_chunking | |
| from src.indexing import load_or_build_index | |
| from src.generation import generate_flashcard | |
| from src.export import export_to_pdf | |
| def process_inputs( | |
| pdf_rows, # list of (path, page_start, page_end) for each visible PDF row | |
| notes_image, | |
| ) -> str: | |
| text_parts = [] | |
| if pdf_rows: | |
| from src.ingestion import extract_text_from_pdf | |
| for pf, page_start, page_end in pdf_rows: | |
| doc = fitz.open(pf) | |
| total_pages = len(doc) | |
| start = max(0, page_start - 1) | |
| end = min(total_pages, page_end) | |
| sub = fitz.open() | |
| sub.insert_pdf(doc, from_page=start, to_page=end - 1) | |
| tmp_path = Path(tempfile.mktemp(suffix=".pdf")) | |
| sub.save(str(tmp_path)) | |
| doc.close() | |
| text_parts.append(extract_text_from_pdf(tmp_path)) | |
| tmp_path.unlink() | |
| if notes_image: | |
| from src.ingestion import extract_text_from_image | |
| for img_file in notes_image: | |
| text_parts.append(extract_text_from_image(Path(img_file))) | |
| return "\n\n".join(text_parts) | |
| def _safe_filename(topic: str) -> str: | |
| safe = re.sub(r"[^\w\s-]", "", topic) | |
| safe = re.sub(r"\s+", "_", safe.strip()) | |
| return safe or "output" | |
| MAX_PDF_ROWS = 4 | |
| MAX_ROWS = 8 | |
| def _parse_mode(raw: str) -> str: | |
| return "summary" if "sum" in raw.strip().lower() else "flashcard" | |
| def generate_card_callback( | |
| notes_image, | |
| pdf_visible, | |
| query_visible, | |
| *rest, | |
| ): | |
| P = MAX_PDF_ROWS | |
| Q = MAX_ROWS | |
| pdf_files = list(rest[:P]) | |
| pdf_starts = list(rest[P:2*P]) | |
| pdf_ends = list(rest[2*P:3*P]) | |
| topics = list(rest[3*P:3*P+Q]) | |
| modes = list(rest[3*P+Q:3*P+2*Q]) | |
| ref_images = list(rest[3*P+2*Q:]) | |
| pdf_rows = [ | |
| (pf, int(ps or 1), int(pe or 20)) | |
| for pf, ps, pe, v in zip(pdf_files, pdf_starts, pdf_ends, pdf_visible) | |
| if v and pf | |
| ] | |
| valid = [ | |
| (t.strip(), m, r) | |
| for t, m, r, v in zip(topics, modes, ref_images, query_visible) | |
| if v and t.strip() | |
| ] | |
| if not valid: | |
| yield "Please add at least one topic.", None | |
| return | |
| pdf_paths = [] | |
| try: | |
| yield "Extracting text from files...", None | |
| text = process_inputs(pdf_rows, notes_image) | |
| if not text.strip(): | |
| yield "No text could be extracted from the provided files.", None | |
| return | |
| yield "Chunking and indexing text...", None | |
| chunks = fixed_size_chunking(text) | |
| index = load_or_build_index( | |
| chunks = chunks, | |
| collection_name = f"session_{hash(text[:100])}", | |
| persist_dir = INDEX_DIR / f"session_{hash(text[:100])}", | |
| ) | |
| total = len(valid) | |
| for i, (topic, mode_raw, ref_img_path) in enumerate(valid, 1): | |
| gradio_mode = _parse_mode(mode_raw) | |
| yield f"[{i}/{total}] Generating '{topic}' ({gradio_mode})...", pdf_paths or None | |
| result = generate_flashcard( | |
| query = topic, | |
| index = index, | |
| llm = llm, | |
| embed_model = embed_model, | |
| mode = gradio_mode, | |
| ) | |
| ref_image_pil = None | |
| if ref_img_path and gradio_mode == "flashcard": | |
| from PIL import Image as PILImage | |
| ref_image_pil = PILImage.open(ref_img_path).convert("RGB") | |
| pdf_out = OUT_DIR.resolve() / f"{_safe_filename(topic)}_{gradio_mode}.pdf" | |
| export_to_pdf(result, pdf_out, reference_image=ref_image_pil) | |
| pdf_paths.append(str(pdf_out)) | |
| label = result.concept if gradio_mode == "flashcard" else result.topic | |
| yield f"[{i}/{total}] Done: {label}", pdf_paths | |
| yield f"All {total} generation(s) complete.", pdf_paths | |
| except Exception as e: | |
| yield f"Error: {e}", pdf_paths or None | |
| # ── Gradio interface ─────────────────────────────────────────────────────────── | |
| with gr.Blocks(title="Flashcard Generator") as demo: | |
| gr.Markdown(""" | |
| # Flashcard Generator | |
| Generate structured study flash cards from your documents using a local AI model. | |
| Upload a PDF, handwritten notes, or a reference image, enter a topic, and get a | |
| downloadable flash card or consolidated summary. | |
| """) | |
| with gr.Tab("Generate"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| gr.Markdown("**PDF documents**") | |
| pdf_visible_state = gr.State([True] + [False] * (MAX_PDF_ROWS - 1)) | |
| pdf_file_inputs = [] | |
| pdf_start_inputs = [] | |
| pdf_end_inputs = [] | |
| pdf_del_btns = [] | |
| pdf_rows_ui = [] | |
| for i in range(MAX_PDF_ROWS): | |
| with gr.Row(visible=(i == 0)) as pdf_row: | |
| pf = gr.File( | |
| label="PDF", | |
| file_types=[".pdf"], | |
| scale=3, | |
| show_label=False, | |
| ) | |
| ps = gr.Number(value=1, minimum=1, precision=0, label="From page", scale=1) | |
| pe = gr.Number(value=20, minimum=1, precision=0, label="To page", scale=1) | |
| pd_ = gr.Button("✕", size="sm", scale=0, min_width=40) | |
| pdf_file_inputs.append(pf) | |
| pdf_start_inputs.append(ps) | |
| pdf_end_inputs.append(pe) | |
| pdf_del_btns.append(pd_) | |
| pdf_rows_ui.append(pdf_row) | |
| for i, d in enumerate(pdf_del_btns): | |
| def _on_pdf_delete(vis, idx=i): | |
| new = list(vis) | |
| new[idx] = False | |
| return [gr.Row(visible=v) for v in new] + [new] | |
| d.click(_on_pdf_delete, inputs=pdf_visible_state, outputs=pdf_rows_ui + [pdf_visible_state]) | |
| add_pdf_btn = gr.Button("+ Add PDF", size="sm") | |
| def _on_pdf_add(vis): | |
| new = list(vis) | |
| for j in range(len(new)): | |
| if not new[j]: | |
| new[j] = True | |
| break | |
| return [gr.Row(visible=v) for v in new] + [new] | |
| add_pdf_btn.click(_on_pdf_add, inputs=pdf_visible_state, outputs=pdf_rows_ui + [pdf_visible_state]) | |
| notes_input = gr.File( | |
| label="Handwritten notes (optional, multiple allowed)", | |
| file_types=[".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".webp"], | |
| file_count="multiple", | |
| ) | |
| gr.Markdown("**Queries**") | |
| visible_state = gr.State([i < 3 for i in range(MAX_ROWS)]) | |
| topic_boxes = [] | |
| mode_radios = [] | |
| ref_image_inputs = [] | |
| del_btns = [] | |
| rows_ui = [] | |
| for i in range(MAX_ROWS): | |
| with gr.Row(visible=(i < 3)) as row: | |
| t = gr.Textbox( | |
| placeholder="e.g. alcanos, photosynthesis...", | |
| show_label=False, | |
| scale=4, | |
| min_width=200, | |
| ) | |
| m = gr.Radio( | |
| choices=["Flash Card", "Summary"], | |
| value="Flash Card", | |
| show_label=False, | |
| scale=2, | |
| ) | |
| r = gr.Image( | |
| label="Reference image", | |
| type="filepath", | |
| show_label=False, | |
| scale=1, | |
| min_width=80, | |
| height=80, | |
| visible=True, | |
| ) | |
| d = gr.Button("✕", size="sm", scale=0, min_width=40) | |
| topic_boxes.append(t) | |
| mode_radios.append(m) | |
| ref_image_inputs.append(r) | |
| del_btns.append(d) | |
| rows_ui.append(row) | |
| # Toggle ref image visibility when mode changes | |
| for m, r in zip(mode_radios, ref_image_inputs): | |
| def _on_mode_change(val, ref=r): | |
| return gr.Image(visible=(val == "Flash Card")) | |
| m.change(_on_mode_change, inputs=m, outputs=r) | |
| # Wire delete buttons after all rows exist so outputs list is complete | |
| for i, d in enumerate(del_btns): | |
| def _on_delete(vis, idx=i): | |
| new = list(vis) | |
| new[idx] = False | |
| return [gr.Row(visible=v) for v in new] + [new] | |
| d.click(_on_delete, inputs=visible_state, outputs=rows_ui + [visible_state]) | |
| add_btn = gr.Button("+ Add query", size="sm") | |
| def _on_add(vis): | |
| new = list(vis) | |
| for j in range(len(new)): | |
| if not new[j]: | |
| new[j] = True | |
| break | |
| return [gr.Row(visible=v) for v in new] + [new] | |
| add_btn.click(_on_add, inputs=visible_state, outputs=rows_ui + [visible_state]) | |
| with gr.Row(): | |
| submit_btn = gr.Button("Generate", variant="primary") | |
| cancel_btn = gr.Button("Cancel", variant="stop") | |
| with gr.Column(scale=1): | |
| status_output = gr.Textbox(label="Status", interactive=False) | |
| pdf_output = gr.File(label="Download PDFs", file_count="multiple") | |
| gen_event = submit_btn.click( | |
| fn = generate_card_callback, | |
| inputs = [notes_input, pdf_visible_state, visible_state] | |
| + pdf_file_inputs + pdf_start_inputs + pdf_end_inputs | |
| + topic_boxes + mode_radios + ref_image_inputs, | |
| outputs = [status_output, pdf_output], | |
| ) | |
| cancel_btn.click(fn=None, cancels=[gen_event]) | |
| with gr.Tab("About"): | |
| gr.Markdown(""" | |
| ## How to use | |
| 1. Upload one or more PDFs — each row has its own "From page" / "To page" range; use **+ Add PDF** to add more | |
| 2. Optionally upload handwritten notes images or a reference image | |
| 3. Fill in the **Queries** — type a topic on the left, pick Flash Card or Summary on the right | |
| 4. Use **+ Add query** to add more rows, or **✕** to remove one | |
| 5. Click Generate and download all PDFs | |
| > **Tip:** For best results, use specific single-concept queries like "amide", | |
| > "photosynthesis", or "Newton's first law" rather than broad queries like | |
| > "all strategies" or "overview of everything". | |
| ## Limitations | |
| - Generation takes 1-3 minutes on CPU | |
| - Equation recognition is not supported in this version | |
| - Context precision may be limited for complex multi-topic queries | |
| ## Model | |
| Running locally with Qwen2.5-3B-Instruct (GGUF Q4_K_M) via llama.cpp. | |
| No data is sent to any external server. | |
| """) | |
| if __name__ == "__main__": | |
| demo.launch(server_name="0.0.0.0", server_port=7860, share=False) | |