feat: air-gapped deployment guide, no implicit downloads

- Ship tiktoken's cl100k_base inside the package and build the encoding
  from it, so token counting never downloads anything.
- Default EMBEDDINGS_CACHE_DIR to <data home>/models instead of FastEmbed's
  temp dir, and read tokenizer.json and repo metadata from that cache, so
  a model downloads once and survives reboots.
- TTS_PROVIDER=none and STT_PROVIDER=none switch the speech features off:
  the endpoints return 404, audio files fail to ingest with a clear
  message, /api/config reports tts_available/stt_available, and the UI
  hides the Speak and microphone buttons.
- Drop the Google Fonts Roboto import from the web UI.
- prefetch-models fills the cache the app reads; verify-offline checks the
  packaged encoding.
- Docs: new Air-Gapped Deployment guide, settings and cache notes.
This commit is contained in:
Alex committed 2026-09-15 17:54:24 +01:00
1 parent ca69d1ea29
commit 7da46c2bea
38 files changed
+100892 -95

No files matched your search

+5 -1
View File
@@ -2,7 +2,7 @@ from pathlib import Path
from typing import Dict, Union
from docsgpt.core.settings import settings
from docsgpt.parser.file.base_parser import BaseParser
from docsgpt.parser.file.base_parser import BaseParser, DocumentParseError
from docsgpt.stt.stt_creator import STTCreator
from docsgpt.stt.upload_limits import enforce_audio_file_size_limit
@@ -17,6 +17,10 @@ class AudioParser(BaseParser):
def parse_file(self, file: Path, errors: str = "ignore") -> Union[str, list[str]]:
_ = errors
if not STTCreator.is_enabled(settings.STT_PROVIDER):
raise DocumentParseError(
f"{file.name}: audio files need speech-to-text, which is disabled (STT_PROVIDER=none)."
)
try:
enforce_audio_file_size_limit(file.stat().st_size)
except OSError:
+9 -7
View File
@@ -217,19 +217,21 @@ class HuggingFaceCounter(TokenCounter):
def _tokenizer_file(repo: str) -> str:
"""Path to ``repo``'s ``tokenizer.json``, from the hub cache when present.
"""Path to ``repo``'s ``tokenizer.json``, from the embedding model cache when present.
A warmed cache (the Docker image bakes the default models) answers without
touching the network. ``hf_hub_download`` would otherwise revalidate the
revision with a HEAD request on every process start, and stall for the
etag timeout on a host that cannot reach huggingface.co.
FastEmbed's snapshot of the repository in ``EMBEDDINGS_CACHE_DIR`` already
holds the file, so a warmed cache answers without touching the network.
``hf_hub_download`` would otherwise revalidate the revision with a HEAD
request on every process start, and stall for the etag timeout on a host
that cannot reach huggingface.co.
"""
from huggingface_hub import hf_hub_download
cache_dir = settings.EMBEDDINGS_CACHE_DIR or None
try:
return hf_hub_download(repo, "tokenizer.json", local_files_only=True)
return hf_hub_download(repo, "tokenizer.json", local_files_only=True, cache_dir=cache_dir)
except Exception: # noqa: BLE001 -- not cached: fetch it
return hf_hub_download(repo, "tokenizer.json")
return hf_hub_download(repo, "tokenizer.json", cache_dir=cache_dir)
def _load_hf_counter(repo: str) -> Optional[HuggingFaceCounter]: