fihus commited on
Commit
38fcf6b
·
verified ·
1 Parent(s): 94729fa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +99 -51
app.py CHANGED
@@ -28,62 +28,78 @@ import re
28
  import sys
29
  import time
30
  import unicodedata
31
- import pymupdf
32
  import numpy as np
33
  import streamlit as st
34
 
35
  # =============================================================================
36
- # Konfigurace (PŘESUNUTO NAHORU)
37
  # =============================================================================
 
38
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
39
 
 
 
 
 
 
 
 
 
 
 
 
40
  INDEX_DIR = os.path.join(BASE_DIR, "index")
41
  FAISS_PATH = os.path.join(INDEX_DIR, "positions.faiss")
42
  META_PATH = os.path.join(INDEX_DIR, "positions_metadata.json")
43
 
44
- # Model fallback chain: fine-tuned base
45
- MODEL_PATHS = [
46
- os.path.join(BASE_DIR, "models", "jobbert-v3-czsk-final"),
47
- "TechWolf/JobBERT-v3",
48
- ]
49
 
50
- # Přidej aktuální adresář do PYTHONPATH
51
- sys.path.insert(0, BASE_DIR)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
- # Nyní už můžeme bezpečně importovat z tvých lokálních souborů
54
  from cv_parser import extract_cv_text, summarize_cv
55
  from rag_engine import (
56
  synthesize_profile,
57
  generate_response,
58
- check_llm_available as check_ollama_available,
59
  llm_generate as ollama_generate,
60
  _fallback_response,
61
  )
62
 
63
- # Auto-build index + model při prvním startu (HF Spaces)
64
- if not os.path.exists(FAISS_PATH):
65
- try:
66
- import subprocess
67
- subprocess.run(
68
- ["python", "build_index_startup.py"],
69
- capture_output=True,
70
- text=True,
71
- check=True
72
- )
73
- except subprocess.CalledProcessError as e:
74
- st.error("🚨 Chyba při stavění FAISS indexu!")
75
- st.code(e.stderr)
76
- st.stop()
77
-
78
- # Kolik kandidátů vytáhnout z FAISS (velká rezerva po filtru)
79
- TOP_K_SEARCH = 40
80
- # Kolik finálně zobrazit uživateli
81
- TOP_K_SHOW = 5
82
-
83
- # Exclusion filter: embedding-based threshold a hard cap
84
- EXCLUSION_SIM_THRESHOLD = 0.88
85
- EXCLUSION_MAX_COUNT = 30
86
-
87
 
88
  # =============================================================================
89
  # Lazy loading (cache pro Streamlit)
@@ -430,15 +446,20 @@ def main():
430
 
431
  ollama_ok = check_ollama_available()
432
  if ollama_ok:
433
- st.success("Ollama: připojena\n(llama3.2)")
434
  else:
435
- st.warning("Ollama: nedostupná\n(fallback režim)")
436
 
437
  st.divider()
438
  st.markdown("**Parametry**")
439
  st.caption(f"Vyhledání: top-{TOP_K_SEARCH}")
440
  st.caption(f"Zobrazení: top-{TOP_K_SHOW}")
441
  st.caption(f"Exclusion práh: {EXCLUSION_SIM_THRESHOLD:.2f}")
 
 
 
 
 
442
 
443
  st.divider()
444
  st.caption("Diplomová práce – Filip Husein")
@@ -560,6 +581,11 @@ def main():
560
  exclude_ids=exclusion_ids,
561
  )
562
 
 
 
 
 
 
563
  # 6) Generace odpovědi
564
  with st.spinner("Generuji doporučení..."):
565
  if ollama_ok:
@@ -621,31 +647,34 @@ def main():
621
  st.markdown(f"### 📊 Top {min(TOP_K_SHOW, len(results))} pozic ({elapsed:.1f}s)")
622
 
623
  for pos in results[:TOP_K_SHOW]:
624
- score = pos["score"]
625
- display_pct = 50 + int(50 * (score - 0.15) / (0.80 - 0.15)) # 0.15→50%, 0.80→100%
626
- display_pct = max(20, min(99, display_pct))
627
- if score >= 0.7:
628
- badge_class = "score-high"
629
  badge_emoji = "🟢"
630
- elif score >= 0.4:
631
- badge_class = "score-mid"
632
  badge_emoji = "🟡"
 
633
  else:
634
- badge_class = "score-low"
635
- badge_emoji = "🔴"
 
 
 
 
636
 
637
- with st.expander(
638
- f"{badge_emoji} #{pos['rank']} – {pos['title']} ({score:.0%})",
639
- expanded=(pos['rank'] <= 2),
640
- ):
641
  cols = st.columns([3, 1])
642
  with cols[0]:
643
- st.markdown(f"**{pos['title']}**")
644
  desc = pos.get("description", "")
645
  if desc:
646
  st.write(desc[:400] + ("..." if len(desc) > 400 else ""))
647
  with cols[1]:
648
  st.metric("Shoda", f"{score:.0%}")
 
 
649
 
650
  # Metadata
651
  meta_parts = []
@@ -664,6 +693,25 @@ def main():
664
 
665
  if meta_parts:
666
  st.markdown(" · ".join(meta_parts))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
667
  else:
668
  st.warning(
669
  "Po odfiltrování aktuální pozice nezbyly žádné kandidátní pozice. "
 
28
  import sys
29
  import time
30
  import unicodedata
31
+
32
  import numpy as np
33
  import streamlit as st
34
 
35
  # =============================================================================
36
+ # Konfigurace musí být DEFINOVÁNA před prvním použitím BASE_DIR
37
  # =============================================================================
38
+
39
  BASE_DIR = os.path.dirname(os.path.abspath(__file__))
40
 
41
+ # Přidej aktuální adresář do PYTHONPATH
42
+ sys.path.insert(0, BASE_DIR)
43
+
44
+ # Model – fallback chain: HF snapshot (stažený v build_index_startup.py) → base
45
+ MODEL_PATHS = [
46
+ os.path.join(BASE_DIR, "model"), # HF snapshot
47
+ os.path.join(BASE_DIR, "models", "jobbert-v3-czsk-hn-final"), # lokální (volitelné)
48
+ os.path.join(BASE_DIR, "models", "jobbert-v3-czsk-final"), # fallback
49
+ "TechWolf/JobBERT-v3", # base
50
+ ]
51
+
52
  INDEX_DIR = os.path.join(BASE_DIR, "index")
53
  FAISS_PATH = os.path.join(INDEX_DIR, "positions.faiss")
54
  META_PATH = os.path.join(INDEX_DIR, "positions_metadata.json")
55
 
56
+ # Kolik kandidátů vytáhnout z FAISS (velká rezerva po filtru)
57
+ TOP_K_SEARCH = 40
58
+ # Kolik finálně zobrazit uživateli
59
+ TOP_K_SHOW = 5
 
60
 
61
+ # ── Exclusion filter ──
62
+ # 0.88 = jen opravdu podobné role (varianty "Junior/Senior X"), ne příbuzné obory.
63
+ EXCLUSION_SIM_THRESHOLD = 0.88
64
+ EXCLUSION_MAX_COUNT = 30 # hard cap – nikdy nevyhodíme víc pozic
65
+
66
+ # ── Rescale raw cosine similarity pro user-friendly zobrazení ──
67
+ # Fine-tuned model má gap cca pos=0.83, neg=0.08.
68
+ # Relevantní "sousední" role padnou do range 0.25–0.70.
69
+ # Mapujeme: 0.15 → 50%, 0.85 → 99% (lineárně), clamp na [20%, 99%].
70
+ DISPLAY_MIN_RAW = 0.15
71
+ DISPLAY_MAX_RAW = 0.85
72
+ DISPLAY_MIN_PCT = 50.0
73
+ DISPLAY_MAX_PCT = 99.0
74
+
75
+
76
+ def display_score(raw: float) -> float:
77
+ """Přemapuj raw cosine similarity na user-facing procento (0–100)."""
78
+ if raw is None:
79
+ return 0.0
80
+ t = (raw - DISPLAY_MIN_RAW) / (DISPLAY_MAX_RAW - DISPLAY_MIN_RAW)
81
+ t = max(0.0, min(1.0, t))
82
+ pct = DISPLAY_MIN_PCT + t * (DISPLAY_MAX_PCT - DISPLAY_MIN_PCT)
83
+ return max(20.0, min(99.0, pct))
84
+
85
+
86
+ # =============================================================================
87
+ # Auto-build index + model při prvním startu (HF Spaces)
88
+ # =============================================================================
89
+
90
+ if not os.path.exists(FAISS_PATH):
91
+ import subprocess
92
+ subprocess.run(["python", "build_index_startup.py"], check=True)
93
 
 
94
  from cv_parser import extract_cv_text, summarize_cv
95
  from rag_engine import (
96
  synthesize_profile,
97
  generate_response,
98
+ check_llm_available as check_ollama_available, # alias kvůli rest kódu
99
  llm_generate as ollama_generate,
100
  _fallback_response,
101
  )
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
  # =============================================================================
105
  # Lazy loading (cache pro Streamlit)
 
446
 
447
  ollama_ok = check_ollama_available()
448
  if ollama_ok:
449
+ st.success("LLM: Groq (llama-3.3-70b)")
450
  else:
451
+ st.warning("LLM: nedostupný\n(fallback režim)")
452
 
453
  st.divider()
454
  st.markdown("**Parametry**")
455
  st.caption(f"Vyhledání: top-{TOP_K_SEARCH}")
456
  st.caption(f"Zobrazení: top-{TOP_K_SHOW}")
457
  st.caption(f"Exclusion práh: {EXCLUSION_SIM_THRESHOLD:.2f}")
458
+ st.caption(f"Exclusion cap: max {EXCLUSION_MAX_COUNT}")
459
+
460
+ st.divider()
461
+ show_debug = st.checkbox("🔧 Debug info", value=False,
462
+ help="Zobrazí raw cosine similarity a interní metriky (pro autora/vedoucího)")
463
 
464
  st.divider()
465
  st.caption("Diplomová práce – Filip Husein")
 
581
  exclude_ids=exclusion_ids,
582
  )
583
 
584
+ # 5b) Rescale skóre pro zobrazení (raw → user-friendly %)
585
+ for r in results:
586
+ r["raw_score"] = r["score"] # uchovat pro debug
587
+ r["score"] = display_score(r["raw_score"]) / 100.0 # nahradit displayem
588
+
589
  # 6) Generace odpovědi
590
  with st.spinner("Generuji doporučení..."):
591
  if ollama_ok:
 
647
  st.markdown(f"### 📊 Top {min(TOP_K_SHOW, len(results))} pozic ({elapsed:.1f}s)")
648
 
649
  for pos in results[:TOP_K_SHOW]:
650
+ score = pos["score"] # už rescaled (0–1)
651
+ raw = pos.get("raw_score") # původní cosine similarity
652
+ # Badge podle rescaled skóre (≥80 % výborná, 60–80 % dobrá, <60 % inspirace)
653
+ if score >= 0.80:
 
654
  badge_emoji = "🟢"
655
+ badge_label = "Výborná shoda"
656
+ elif score >= 0.60:
657
  badge_emoji = "🟡"
658
+ badge_label = "Dobrá shoda"
659
  else:
660
+ badge_emoji = "🔵"
661
+ badge_label = "Zajímavá inspirace"
662
+
663
+ header = f"{badge_emoji} #{pos['rank']} – {pos['title']} ({score:.0%})"
664
+ if show_debug and raw is not None:
665
+ header += f" [raw={raw:.3f}]"
666
 
667
+ with st.expander(header, expanded=(pos['rank'] <= 2)):
 
 
 
668
  cols = st.columns([3, 1])
669
  with cols[0]:
670
+ st.markdown(f"**{pos['title']}** · {badge_label}")
671
  desc = pos.get("description", "")
672
  if desc:
673
  st.write(desc[:400] + ("..." if len(desc) > 400 else ""))
674
  with cols[1]:
675
  st.metric("Shoda", f"{score:.0%}")
676
+ if show_debug and raw is not None:
677
+ st.caption(f"raw cos: {raw:.3f}")
678
 
679
  # Metadata
680
  meta_parts = []
 
693
 
694
  if meta_parts:
695
  st.markdown(" · ".join(meta_parts))
696
+
697
+ # Debug tabulka s raw skóre
698
+ if show_debug:
699
+ st.divider()
700
+ st.markdown("#### 🔧 Debug – raw cosine similarity")
701
+ debug_rows = [
702
+ {
703
+ "rank": r["rank"],
704
+ "title": r["title"][:60],
705
+ "raw_cos": round(r.get("raw_score", 0), 4),
706
+ "display_%": f"{r['score']*100:.1f}%",
707
+ }
708
+ for r in results[:TOP_K_SHOW]
709
+ ]
710
+ st.table(debug_rows)
711
+ st.caption(
712
+ f"Mapování: raw {DISPLAY_MIN_RAW:.2f} → {DISPLAY_MIN_PCT:.0f} % | "
713
+ f"raw {DISPLAY_MAX_RAW:.2f} → {DISPLAY_MAX_PCT:.0f} % (lineárně, clamp 20–99 %)"
714
+ )
715
  else:
716
  st.warning(
717
  "Po odfiltrování aktuální pozice nezbyly žádné kandidátní pozice. "