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

+17
View File
@@ -345,6 +345,15 @@ class StoreAttachment(Resource):
return make_response(jsonify({"success": False, "error": "Failed to store attachment"}), 400)
_STT_DISABLED_MESSAGE = "Speech-to-text is disabled on this server."
_TTS_DISABLED_MESSAGE = "Text-to-speech is disabled on this server."
def _feature_disabled(message: str):
"""404 for a speech feature whose provider is set to ``none``."""
return make_response(jsonify({"success": False, "message": message}), 404)
@attachments_ns.route("/stt")
class SpeechToText(Resource):
@api.expect(
@@ -368,6 +377,8 @@ class SpeechToText(Resource):
jsonify({"success": False, "message": "Authentication required"}),
401,
)
if not STTCreator.is_enabled(settings.STT_PROVIDER):
return _feature_disabled(_STT_DISABLED_MESSAGE)
file = request.files.get("file")
if not file or file.filename == "":
@@ -440,6 +451,8 @@ class LiveSpeechToTextStart(Resource):
jsonify({"success": False, "message": "Authentication required"}),
401,
)
if not STTCreator.is_enabled(settings.STT_PROVIDER):
return _feature_disabled(_STT_DISABLED_MESSAGE)
redis_client = _require_live_stt_redis()
if hasattr(redis_client, "status_code"):
@@ -501,6 +514,8 @@ class LiveSpeechToTextChunk(Resource):
jsonify({"success": False, "message": "Authentication required"}),
401,
)
if not STTCreator.is_enabled(settings.STT_PROVIDER):
return _feature_disabled(_STT_DISABLED_MESSAGE)
redis_client = _require_live_stt_redis()
if hasattr(redis_client, "status_code"):
@@ -817,6 +832,8 @@ class TextToSpeech(Resource):
@api.expect(tts_model)
@api.doc(description="Synthesize audio speech from text")
def post(self):
if not TTSCreator.is_enabled(settings.TTS_PROVIDER):
return _feature_disabled(_TTS_DISABLED_MESSAGE)
data = request.get_json()
text = data["text"]
try: