GitHub Actions commited on
Commit
527676c
·
1 Parent(s): 49e7344

Deploy from GitHub (a639361)

Browse files
.gitattributes CHANGED
@@ -1,3 +1,3 @@
 
1
  data/*.json filter=lfs diff=lfs merge=lfs -text
2
  data/*.db filter=lfs diff=lfs merge=lfs -text
3
- data/*.faiss filter=lfs diff=lfs merge=lfs -text
 
1
+ data/*.faiss filter=lfs diff=lfs merge=lfs -text
2
  data/*.json filter=lfs diff=lfs merge=lfs -text
3
  data/*.db filter=lfs diff=lfs merge=lfs -text
 
Dockerfile CHANGED
@@ -10,10 +10,12 @@ RUN uv sync --frozen --no-dev
10
  COPY config.py ./
11
  COPY rag/ rag/
12
 
13
- # Pre-export ONNX models at build time (cached in image layer)
14
  RUN uv run python -c "\
15
  from rag.embeddings import load_embedding_model, load_cross_encoder; \
16
- load_embedding_model(); load_cross_encoder()"
 
 
17
 
18
  COPY app.py ./
19
  COPY templates/ templates/
 
10
  COPY config.py ./
11
  COPY rag/ rag/
12
 
13
+ # Pre-download and load quantized ONNX models at build time (cached in layer)
14
  RUN uv run python -c "\
15
  from rag.embeddings import load_embedding_model, load_cross_encoder; \
16
+ print('Loading embedding model...'); load_embedding_model(); \
17
+ print('Loading cross-encoder...'); load_cross_encoder(); \
18
+ print('Models cached.')"
19
 
20
  COPY app.py ./
21
  COPY templates/ templates/
README.md CHANGED
@@ -9,7 +9,7 @@ pinned: false
9
  preload_from_hub:
10
  - sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
11
  - cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
12
- startup_duration_timeout: 5m
13
  ---
14
 
15
  <!-- HuggingFace Spaces frontmatter above -- do not remove -->
@@ -79,6 +79,7 @@ The entire system runs locally with no external API calls, no paid dependencies,
79
  - **Fast** -- sub-2s response times on CPU
80
  - **35,000+ verses** -- complete French Bible (AELF translation)
81
  - **PWA-ready** -- offline support via service worker, installable on mobile
 
82
  - **Self-contained** -- no external APIs, runs entirely on your machine
83
 
84
  ## Live Demo
@@ -112,6 +113,7 @@ Open [http://localhost:8000](http://localhost:8000) in your browser.
112
  | GET | `/health` | Health check (200 `ok` or 503 `loading`) |
113
  | GET | `/robots.txt` | Robots.txt for crawlers |
114
  | GET | `/sitemap.xml` | XML sitemap for crawlers |
 
115
 
116
  ## Architecture
117
 
 
9
  preload_from_hub:
10
  - sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2
11
  - cross-encoder/mmarco-mMiniLMv2-L12-H384-v1
12
+ startup_duration_timeout: 10m
13
  ---
14
 
15
  <!-- HuggingFace Spaces frontmatter above -- do not remove -->
 
79
  - **Fast** -- sub-2s response times on CPU
80
  - **35,000+ verses** -- complete French Bible (AELF translation)
81
  - **PWA-ready** -- offline support via service worker, installable on mobile
82
+ - **Per-verse feedback** -- thumbs up/down on results, synced to HuggingFace Dataset
83
  - **Self-contained** -- no external APIs, runs entirely on your machine
84
 
85
  ## Live Demo
 
113
  | GET | `/health` | Health check (200 `ok` or 503 `loading`) |
114
  | GET | `/robots.txt` | Robots.txt for crawlers |
115
  | GET | `/sitemap.xml` | XML sitemap for crawlers |
116
+ | POST | `/feedback` | Per-verse feedback (fire-and-forget, returns 204) |
117
 
118
  ## Architecture
119
 
config.py CHANGED
@@ -1,6 +1,7 @@
1
  """Central configuration for the RAG Bible pipeline."""
2
 
3
  import os
 
4
  from pathlib import Path
5
 
6
  # Paths
@@ -16,6 +17,15 @@ EMBEDDING_DIMENSION: int = 384
16
  # Cross-encoder model
17
  CROSS_ENCODER_MODEL: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
18
 
 
 
 
 
 
 
 
 
 
19
  # Retrieval parameters
20
  FAISS_TOP_K: int = 20
21
  RERANK_TOP_K: int = 5
 
1
  """Central configuration for the RAG Bible pipeline."""
2
 
3
  import os
4
+ import platform
5
  from pathlib import Path
6
 
7
  # Paths
 
17
  # Cross-encoder model
18
  CROSS_ENCODER_MODEL: str = "cross-encoder/mmarco-mMiniLMv2-L12-H384-v1"
19
 
20
+ # ONNX quantized model file (architecture-specific)
21
+ _machine = platform.machine()
22
+ _onnx_map: dict[str, str] = {
23
+ "x86_64": "onnx/model_qint8_avx512.onnx",
24
+ "AMD64": "onnx/model_qint8_avx512.onnx",
25
+ "arm64": "onnx/model_qint8_arm64.onnx",
26
+ }
27
+ ONNX_FILE_NAME: str = _onnx_map.get(_machine, "onnx/model.onnx")
28
+
29
  # Retrieval parameters
30
  FAISS_TOP_K: int = 20
31
  RERANK_TOP_K: int = 5
pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
  [project]
2
  name = "rag-bible"
3
- version = "1.1.0"
4
  description = "French Bible RAG system with FAISS + cross-encoder reranking"
5
  requires-python = ">=3.12"
6
  dependencies = [
 
1
  [project]
2
  name = "rag-bible"
3
+ version = "1.1.1"
4
  description = "French Bible RAG system with FAISS + cross-encoder reranking"
5
  requires-python = ">=3.12"
6
  dependencies = [
rag/embeddings.py CHANGED
@@ -20,7 +20,11 @@ def load_embedding_model(model_name: str | None = None) -> SentenceTransformer:
20
  Loaded embedding model.
21
  """
22
  name = model_name or config.EMBEDDING_MODEL
23
- return SentenceTransformer(name, backend="onnx")
 
 
 
 
24
 
25
 
26
  def encode_texts(
@@ -71,5 +75,9 @@ def load_cross_encoder(model_name: str | None = None) -> CrossEncoder:
71
  Loaded cross-encoder model.
72
  """
73
  name = model_name or config.CROSS_ENCODER_MODEL
74
- model: CrossEncoder = CrossEncoder(name, backend="onnx")
 
 
 
 
75
  return model
 
20
  Loaded embedding model.
21
  """
22
  name = model_name or config.EMBEDDING_MODEL
23
+ return SentenceTransformer(
24
+ name,
25
+ backend="onnx",
26
+ model_kwargs={"file_name": config.ONNX_FILE_NAME},
27
+ )
28
 
29
 
30
  def encode_texts(
 
75
  Loaded cross-encoder model.
76
  """
77
  name = model_name or config.CROSS_ENCODER_MODEL
78
+ model: CrossEncoder = CrossEncoder(
79
+ name,
80
+ backend="onnx",
81
+ model_kwargs={"file_name": config.ONNX_FILE_NAME},
82
+ )
83
  return model
rag/retrieve.py CHANGED
@@ -1,7 +1,6 @@
1
  """Two-stage retrieval: FAISS vector search + cross-encoder reranking."""
2
 
3
  import json
4
- from concurrent.futures import ThreadPoolExecutor
5
  from pathlib import Path
6
  from typing import Any
7
 
@@ -50,16 +49,10 @@ def load_pipeline(
50
  idx_path = index_path or config.INDEX_PATH
51
  map_path = mapping_path or config.MAPPING_PATH
52
 
53
- with ThreadPoolExecutor(max_workers=4) as pool:
54
- f_index = pool.submit(lambda: faiss.read_index(str(idx_path)))
55
- f_mapping = pool.submit(lambda: _load_mapping(map_path))
56
- f_embed = pool.submit(load_embedding_model)
57
- f_cross = pool.submit(load_cross_encoder)
58
-
59
- index = f_index.result()
60
- mapping = f_mapping.result()
61
- embed_model = f_embed.result()
62
- cross_encoder = f_cross.result()
63
 
64
  return index, mapping, embed_model, cross_encoder
65
 
 
1
  """Two-stage retrieval: FAISS vector search + cross-encoder reranking."""
2
 
3
  import json
 
4
  from pathlib import Path
5
  from typing import Any
6
 
 
49
  idx_path = index_path or config.INDEX_PATH
50
  map_path = mapping_path or config.MAPPING_PATH
51
 
52
+ index = faiss.read_index(str(idx_path))
53
+ mapping = _load_mapping(map_path)
54
+ embed_model = load_embedding_model()
55
+ cross_encoder = load_cross_encoder()
 
 
 
 
 
 
56
 
57
  return index, mapping, embed_model, cross_encoder
58
 
static/index.html CHANGED
@@ -65,7 +65,7 @@
65
  <h2 class="sidebar-title">Historique</h2>
66
  <div class="history-list"></div>
67
  <p class="history-empty">Vos recherches apparaîtront ici</p>
68
- <p class="sidebar-version">v1.1.0</p>
69
  </aside>
70
 
71
  <div class="hero-section">
 
65
  <h2 class="sidebar-title">Historique</h2>
66
  <div class="history-list"></div>
67
  <p class="history-empty">Vos recherches apparaîtront ici</p>
68
+ <p class="sidebar-version">v1.1.1</p>
69
  </aside>
70
 
71
  <div class="hero-section">
static/styles.css CHANGED
@@ -564,43 +564,51 @@ h1 {
564
  color: var(--color-text-card);
565
  }
566
 
567
- /* --- Card Footer / Feedback --- */
568
 
569
- .card-footer {
570
  display: flex;
571
- justify-content: flex-end;
572
  gap: var(--space-xs);
573
- padding-top: var(--space-sm);
574
- border-top: 1px solid var(--color-border);
575
- margin-top: var(--space-sm);
 
 
 
576
  }
577
 
578
  .feedback-btn {
579
  display: flex;
580
  align-items: center;
581
  justify-content: center;
582
- width: var(--touch-target-desktop);
583
- height: var(--touch-target-desktop);
584
  padding: 0;
585
  border: none;
586
  border-radius: var(--radius-sm);
587
  background: transparent;
588
  color: var(--color-text-muted);
589
  cursor: pointer;
 
590
  transition: color var(--transition-quick),
591
- background-color var(--transition-quick);
 
592
  }
593
 
594
  .feedback-btn:hover {
595
  background: var(--color-highlight);
 
596
  }
597
 
598
  .feedback-up[aria-pressed="true"] {
599
  color: var(--color-score-high);
 
600
  }
601
 
602
  .feedback-down[aria-pressed="true"] {
603
  color: var(--color-feedback-error);
 
604
  }
605
 
606
  /* --- Status Messages --- */
@@ -1018,8 +1026,8 @@ textarea:focus-visible {
1018
  }
1019
 
1020
  .feedback-btn {
1021
- width: var(--touch-target-mobile);
1022
- height: var(--touch-target-mobile);
1023
  }
1024
 
1025
  .example-query {
 
564
  color: var(--color-text-card);
565
  }
566
 
567
+ /* --- Header Actions / Feedback --- */
568
 
569
+ .card-header-actions {
570
  display: flex;
571
+ align-items: center;
572
  gap: var(--space-xs);
573
+ }
574
+
575
+ .feedback-group {
576
+ display: flex;
577
+ align-items: center;
578
+ gap: 2px;
579
  }
580
 
581
  .feedback-btn {
582
  display: flex;
583
  align-items: center;
584
  justify-content: center;
585
+ width: 28px;
586
+ height: 28px;
587
  padding: 0;
588
  border: none;
589
  border-radius: var(--radius-sm);
590
  background: transparent;
591
  color: var(--color-text-muted);
592
  cursor: pointer;
593
+ opacity: 0.5;
594
  transition: color var(--transition-quick),
595
+ background-color var(--transition-quick),
596
+ opacity var(--transition-quick);
597
  }
598
 
599
  .feedback-btn:hover {
600
  background: var(--color-highlight);
601
+ opacity: 1;
602
  }
603
 
604
  .feedback-up[aria-pressed="true"] {
605
  color: var(--color-score-high);
606
+ opacity: 1;
607
  }
608
 
609
  .feedback-down[aria-pressed="true"] {
610
  color: var(--color-feedback-error);
611
+ opacity: 1;
612
  }
613
 
614
  /* --- Status Messages --- */
 
1026
  }
1027
 
1028
  .feedback-btn {
1029
+ width: 32px;
1030
+ height: 32px;
1031
  }
1032
 
1033
  .example-query {
templates/results.html CHANGED
@@ -19,32 +19,34 @@
19
  data-score="{{ r.score }}">
20
  <div class="card-header">
21
  <span><strong>{{ r.book_title }}</strong> — {{ r.chapter }}{% if r.verse %}:{{ r.verse }}{% endif %}</span>
22
- <span class="score-badge
23
- {%- if r.pct >= 80 %} score-high
24
- {%- elif r.pct >= 50 %} score-mid
25
- {%- elif r.pct >= 30 %} score-low
26
- {%- else %} score-minimal
27
- {%- endif %}">{{ r.label }} ({{ r.pct }}%)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  </div>
29
  {% include "context_verses.html" %}
30
- <div class="card-footer">
31
- <button type="button" class="feedback-btn feedback-up"
32
- data-feedback="up" aria-label="Pertinent" aria-pressed="false">
33
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
34
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
35
- <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
36
- <path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
37
- </svg>
38
- </button>
39
- <button type="button" class="feedback-btn feedback-down"
40
- data-feedback="down" aria-label="Non pertinent" aria-pressed="false">
41
- <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
42
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
43
- <path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
44
- <path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
45
- </svg>
46
- </button>
47
- </div>
48
  </div>
49
  </div>
50
  {% endfor %}
 
19
  data-score="{{ r.score }}">
20
  <div class="card-header">
21
  <span><strong>{{ r.book_title }}</strong> — {{ r.chapter }}{% if r.verse %}:{{ r.verse }}{% endif %}</span>
22
+ <div class="card-header-actions">
23
+ <span class="score-badge
24
+ {%- if r.pct >= 80 %} score-high
25
+ {%- elif r.pct >= 50 %} score-mid
26
+ {%- elif r.pct >= 30 %} score-low
27
+ {%- else %} score-minimal
28
+ {%- endif %}">{{ r.label }} ({{ r.pct }}%)</span>
29
+ <div class="feedback-group">
30
+ <button type="button" class="feedback-btn feedback-up"
31
+ data-feedback="up" aria-label="Pertinent" aria-pressed="false">
32
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
33
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
34
+ <path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3H14z"/>
35
+ <path d="M7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/>
36
+ </svg>
37
+ </button>
38
+ <button type="button" class="feedback-btn feedback-down"
39
+ data-feedback="down" aria-label="Non pertinent" aria-pressed="false">
40
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
41
+ stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
42
+ <path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3H10z"/>
43
+ <path d="M17 2h3a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2h-3"/>
44
+ </svg>
45
+ </button>
46
+ </div>
47
+ </div>
48
  </div>
49
  {% include "context_verses.html" %}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  </div>
51
  </div>
52
  {% endfor %}