mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
* fix: remove all automatic third-party egress (OSM tiles, Scalar fonts, editor Google Fonts, AI model download fallbacks) Phone-home audit follow-up. The product no longer makes any automatic third-party request; user-initiated click-outs stay, and production now fails closed on missing AI models. 1. GPS leak via OSM tiles: the strip-metadata panel auto-loaded tile.openstreetmap.org tiles encoding the photo's GPS position. The Leaflet mini-map is gone; coordinates render as text plus an explicit View on map link (openstreetmap.org, opens on click only). Removed tile.openstreetmap.org from the CSP img-src, dropped the leaflet dependency, added the viewOnMap i18n key to all 21 locales. 2. Scalar docs fonts: /api/docs loaded Inter and JetBrains Mono from fonts.scalar.com. Scalar now renders with withDefaultFonts: false and both --scalar-font and --scalar-font-code pinned to system stacks; fonts.scalar.com removed from the docs CSP font-src. Verified by injecting GET /api/docs/: config carries withDefaultFonts false and the served page has no fonts.scalar.com reference. 3. Editor Google Fonts: the editor font picker built fonts.googleapis.com stylesheet URLs for 25 web fonts the served CSP already blocked. The remote loading path is deleted; the picker now offers system fonts only, with a SELF_HOSTED_FONTS seam (FontFace API, same origin) for bundling fonts later. Unknown families saved in old documents fall back to the browser default. 4. Python sidecar fails closed on model downloads: new packages/ai/python/offline_guard.py gates every runtime download fallback (inpaint, outpaint, restore, noise_removal, detect_faces, enhance_faces, face_landmarks, red_eye_removal, remove_bg, ocr, transcribe, upscale) behind SNAPOTTER_ALLOW_MODEL_DOWNLOAD=1 with an actionable error. Bundled models keep working untouched. 5. OCR and transcription library-internal downloads: unbundled PaddleOCR language and detection fallbacks now raise the guard error naming the language instead of resolving models over the network; faster-whisper gets local_files_only when downloads are off. 6. GFPGAN and CodeFormer cwd-relative weights: facexlib and codeformer-pip resolve helper weights relative to the process cwd and fetch them from GitHub when absent. They are now symlinked from the installed bundle files under MODELS_PATH/gfpgan/facelib before the libraries load, failing closed when unresolvable. Defense in depth: HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1 are set in the runtime image and in the sidecar spawn env; install_feature.py lifts them for user-initiated bundle installs and restores them afterwards (it can run in-process inside the dispatcher). SNAPOTTER_ALLOW_MODEL_DOWNLOAD is documented in .env.example, default off. Validation: typecheck 9/9 workspaces, Biome clean on touched files, 5178 unit tests pass, py_compile on all touched scripts, guard behavior exercised in both dispatcher exec and per-request import modes, zero remaining runtime references to the three hosts. Docker build and live AI inference need post-merge verification on the GPU host. Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7 * fix: allow AI model downloads by default, make strict offline mode opt-in Product call: ease of use first. The download gating from the previous commit inverts its default: runtime model fetches (public model weights only, never user data) are allowed out of the box so AI tools self-heal, and SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 becomes the explicit strict offline mode for airgapped deployments, where every fallback raises the actionable error instead of fetching. Changes: offline_guard blocks only on an explicit 0/false; the unconditional HF_HUB_OFFLINE/TRANSFORMERS_OFFLINE image ENV is removed and bridge.ts sets those flags for the sidecar only in strict mode; .env.example documents the new default; install_feature's lift/restore stays. All bundled-path preferences, pre-existence checks, and symlink pre-placement remain, so installed bundles never trigger a download. The OSM, Scalar font, and editor font fixes are unchanged. Validation rerun: typecheck 9/9, Biome clean on touched files, 5178 unit tests pass, py_compile on touched scripts, guard behavior verified for unset/1 (allowed) and 0/false (blocked with the new message). Claude-Session: https://claude.ai/code/session_01XGB4pGvTvb7sUX4JN745U7
98 lines
2.9 KiB
Python
98 lines
2.9 KiB
Python
"""Speech-to-text transcription using faster-whisper (CTranslate2)."""
|
|
import sys
|
|
import json
|
|
import os
|
|
|
|
from gpu import gpu_available
|
|
|
|
|
|
MODELS_PATH = os.environ.get(
|
|
"MODELS_PATH",
|
|
os.path.join(os.environ.get("DATA_DIR", "/data"), "ai", "models"),
|
|
)
|
|
|
|
|
|
def emit_progress(percent, stage):
|
|
"""Emit structured progress to stderr for bridge.ts to capture."""
|
|
print(json.dumps({"progress": percent, "stage": stage}), file=sys.stderr, flush=True)
|
|
|
|
|
|
def main():
|
|
input_path = sys.argv[1]
|
|
settings = json.loads(sys.argv[2]) if len(sys.argv) > 2 else {}
|
|
|
|
language = settings.get("language", "auto")
|
|
# "task" is accepted for forward-compat but only "transcribe" is used today
|
|
_task = settings.get("task", "transcribe")
|
|
|
|
try:
|
|
emit_progress(5, "Loading model")
|
|
|
|
# Lazy import -- faster_whisper is only available when the
|
|
# transcription bundle is installed; keeping it lazy lets
|
|
# py_compile succeed without the dependency.
|
|
from faster_whisper import WhisperModel
|
|
|
|
model_dir = os.path.join(MODELS_PATH, "faster-whisper-small")
|
|
|
|
# When the bundled model dir is absent, faster-whisper treats the
|
|
# argument as a Hugging Face repo id and downloads it; strict offline
|
|
# mode blocks that fallback with a clear error.
|
|
from offline_guard import downloads_allowed, ensure_download_allowed
|
|
if not os.path.isdir(model_dir):
|
|
ensure_download_allowed("Whisper transcription model (faster-whisper-small)")
|
|
|
|
if gpu_available():
|
|
device, compute_type = "cuda", "float16"
|
|
else:
|
|
device, compute_type = "cpu", "int8"
|
|
|
|
model = WhisperModel(
|
|
model_dir,
|
|
device=device,
|
|
compute_type=compute_type,
|
|
local_files_only=not downloads_allowed(),
|
|
)
|
|
|
|
emit_progress(20, "Transcribing")
|
|
|
|
lang_arg = None if language == "auto" else language
|
|
segments_iter, info = model.transcribe(
|
|
input_path,
|
|
language=lang_arg,
|
|
vad_filter=True,
|
|
)
|
|
|
|
detected_language = info.language if info else (language if language != "auto" else "en")
|
|
|
|
segments = []
|
|
batch_count = 0
|
|
for seg in segments_iter:
|
|
segments.append({
|
|
"start": round(seg.start, 3),
|
|
"end": round(seg.end, 3),
|
|
"text": seg.text.strip(),
|
|
})
|
|
batch_count += 1
|
|
if batch_count % 5 == 0:
|
|
emit_progress(min(20 + batch_count, 90), "Transcribing")
|
|
|
|
emit_progress(95, "Done")
|
|
|
|
full_text = " ".join(s["text"] for s in segments)
|
|
|
|
print(json.dumps({
|
|
"success": True,
|
|
"language": detected_language,
|
|
"segments": segments,
|
|
"text": full_text,
|
|
}))
|
|
|
|
except Exception as e:
|
|
print(json.dumps({"error": str(e)}))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|