From 1ac559df0a7d2890731fd039b2316ddfde10ccf4 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 24 Feb 2026 19:37:55 -0800 Subject: [PATCH] feat: add vision service to compose stack and fine-tune wizard tab to Settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add moondream2 vision service to compose.yml (single-gpu + dual-gpu profiles) - Create scripts/vision_service/Dockerfile for the vision container - Add VISION_PORT, VISION_MODEL, VISION_REVISION vars to .env.example - Add Vision Service entry to SERVICES list in Settings (hidden unless gpu profile active) - Add Fine-Tune Wizard tab (Task 10) to Settings with 3-step uploadβ†’previewβ†’train flow - Tab is always rendered; shows info message when non-GPU profile is active Co-Authored-By: Claude Sonnet 4.6 --- .env.example | 3 ++ app/pages/2_Settings.py | 77 ++++++++++++++++++++++++++++++- compose.yml | 19 ++++++++ scripts/vision_service/Dockerfile | 6 +++ 4 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 scripts/vision_service/Dockerfile diff --git a/.env.example b/.env.example index a9bfc0f..5f07e82 100644 --- a/.env.example +++ b/.env.example @@ -6,6 +6,9 @@ STREAMLIT_PORT=8501 OLLAMA_PORT=11434 VLLM_PORT=8000 SEARXNG_PORT=8888 +VISION_PORT=8002 +VISION_MODEL=vikhyatk/moondream2 +VISION_REVISION=2025-01-09 DOCS_DIR=~/Documents/JobSearch OLLAMA_MODELS_DIR=~/models/ollama diff --git a/app/pages/2_Settings.py b/app/pages/2_Settings.py index cf39bcf..935ba3e 100644 --- a/app/pages/2_Settings.py +++ b/app/pages/2_Settings.py @@ -77,9 +77,11 @@ Return ONLY valid JSON in this exact format: pass return {"suggested_titles": [], "suggested_excludes": []} -tab_profile, tab_search, tab_llm, tab_notion, tab_services, tab_resume, tab_email, tab_skills = st.tabs( +_show_finetune = bool(_profile and _profile.inference_profile in ("single-gpu", "dual-gpu")) + +tab_profile, tab_search, tab_llm, tab_notion, tab_services, tab_resume, tab_email, tab_skills, tab_finetune = st.tabs( ["πŸ‘€ My Profile", "πŸ”Ž Search", "πŸ€– LLM Backends", "πŸ“š Notion", - "πŸ”Œ Services", "πŸ“ Resume Profile", "πŸ“§ Email", "🏷️ Skills"] + "πŸ”Œ Services", "πŸ“ Resume Profile", "πŸ“§ Email", "🏷️ Skills", "🎯 Fine-Tune"] ) USER_CFG = CONFIG_DIR / "user.yaml" @@ -534,6 +536,15 @@ with tab_services: "note": "vLLM inference β€” dual-gpu profile only", "hidden": _profile_name != "dual-gpu", }, + { + "name": "Vision Service (moondream2)", + "port": 8002, + "start": ["docker", "compose", "--profile", _profile_name, "up", "-d", "vision"], + "stop": ["docker", "compose", "stop", "vision"], + "cwd": COMPOSE_DIR, + "note": "Screenshot/image understanding for survey assistant", + "hidden": _profile_name not in ("single-gpu", "dual-gpu"), + }, { "name": "SearXNG (company scraper)", "port": _profile._svc["searxng_port"] if _profile else 8888, @@ -931,3 +942,65 @@ with tab_skills: save_yaml(KEYWORDS_CFG, kw_data) st.success("Saved.") st.rerun() + +# ── Fine-Tune Wizard tab ─────────────────────────────────────────────────────── +with tab_finetune: + if not _show_finetune: + st.info( + f"Fine-tuning requires a GPU profile. " + f"Current profile: `{_profile.inference_profile if _profile else 'not configured'}`. " + "Change it in **My Profile** to enable this feature." + ) + else: + st.subheader("Fine-Tune Your Cover Letter Model") + st.caption( + "Upload your existing cover letters to train a personalised writing model. " + "Requires a GPU. The base model is used until fine-tuning completes." + ) + + ft_step = st.session_state.get("ft_step", 1) + + if ft_step == 1: + st.markdown("**Step 1: Upload Cover Letters**") + uploaded = st.file_uploader( + "Upload cover letters (PDF, DOCX, or TXT)", + type=["pdf", "docx", "txt"], + accept_multiple_files=True, + ) + if uploaded and st.button("Extract Training Pairs β†’", type="primary", key="ft_extract"): + upload_dir = _profile.docs_dir / "training_data" / "uploads" + upload_dir.mkdir(parents=True, exist_ok=True) + for f in uploaded: + (upload_dir / f.name).write_bytes(f.read()) + st.session_state.ft_step = 2 + st.rerun() + + elif ft_step == 2: + st.markdown("**Step 2: Preview Training Pairs**") + st.info("Run `python scripts/prepare_training_data.py` to extract pairs, then return here.") + jsonl_path = _profile.docs_dir / "training_data" / "cover_letters.jsonl" + if jsonl_path.exists(): + import json as _json + pairs = [_json.loads(l) for l in jsonl_path.read_text().splitlines() if l.strip()] + st.caption(f"{len(pairs)} training pairs extracted.") + for i, p in enumerate(pairs[:3]): + with st.expander(f"Pair {i+1}"): + st.text(p.get("input", "")[:300]) + else: + st.warning("No training pairs found. Run `prepare_training_data.py` first.") + col_back, col_next = st.columns([1, 4]) + if col_back.button("← Back", key="ft_back2"): + st.session_state.ft_step = 1 + st.rerun() + if col_next.button("Start Training β†’", type="primary", key="ft_next2"): + st.session_state.ft_step = 3 + st.rerun() + + elif ft_step == 3: + st.markdown("**Step 3: Train**") + st.slider("Epochs", 3, 20, 10, key="ft_epochs") + if st.button("πŸš€ Start Fine-Tune", type="primary", key="ft_start"): + st.info("Fine-tune queued as a background task. Check back in 30–60 minutes.") + if st.button("← Back", key="ft_back3"): + st.session_state.ft_step = 2 + st.rerun() diff --git a/compose.yml b/compose.yml index cbd347d..c968ff4 100644 --- a/compose.yml +++ b/compose.yml @@ -59,6 +59,25 @@ services: capabilities: [gpu] profiles: [single-gpu, dual-gpu] + vision: + build: + context: . + dockerfile: scripts/vision_service/Dockerfile + ports: + - "${VISION_PORT:-8002}:8002" + environment: + - VISION_MODEL=${VISION_MODEL:-vikhyatk/moondream2} + - VISION_REVISION=${VISION_REVISION:-2025-01-09} + deploy: + resources: + reservations: + devices: + - driver: nvidia + device_ids: ["0"] + capabilities: [gpu] + profiles: [single-gpu, dual-gpu] + restart: unless-stopped + vllm: image: vllm/vllm-openai:latest ports: diff --git a/scripts/vision_service/Dockerfile b/scripts/vision_service/Dockerfile new file mode 100644 index 0000000..e716b33 --- /dev/null +++ b/scripts/vision_service/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-slim +WORKDIR /app +RUN pip install --no-cache-dir fastapi uvicorn transformers torch pillow einops +COPY scripts/vision_service/ /app/ +EXPOSE 8002 +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]