import os # CRITICAL FIX: MUST BE THE FIRST LINE os.environ["KMP_DUPLICATE_LIB_OK"] = "TRUE" import streamlit as st import math import config import database as db import utils from i18n import t, label_display, SUPPORTED_LANGUAGES, DEFAULT_LANGUAGE from services import session_manager as sm from services.whisper_service import load_whisper_model from components.uploader import render_uploader from components.gallery import render_gallery from components.labeler import render_labeler from components.recorder import render_recorder from components.downloader import render_downloader from components.image_protection import inject_image_protection from services.auth_service import require_auth, do_logout # ── PAGE CONFIG ────────────────────────────────────────────────────────────── st.set_page_config( page_title=config.APP_TITLE, layout="wide", page_icon=config.APP_ICON, ) # ── FIX: Prevent horizontal layout shift from scrollbar appearing/disappearing # HF Spaces renders Streamlit inside an iframe. The scroll container is NOT # but internal Streamlit elements. We target every possible scroll # container and use scrollbar-gutter:stable (modern) + overflow-y:scroll (fallback). st.markdown(""" """, unsafe_allow_html=True) # ── IMAGE PROTECTION (prevent download / right-click save) ─────────────────── inject_image_protection() # ── AUTHENTICATION GATE ─────────────────────────────────────────────────────── if not require_auth(): st.stop() # ── UI LANGUAGE (initialize before anything renders) ───────────────────────── if "ui_language" not in st.session_state: st.session_state.ui_language = DEFAULT_LANGUAGE # ── SESSION INITIALIZATION ────────────────────────────────────────────────── sm.init_session() # Check inactivity timeout if sm.check_session_timeout(config.SESSION_TIMEOUT_MINUTES): if sm.has_undownloaded_data(): summary = sm.get_session_data_summary() st.warning(t("session_expired", minutes=config.SESSION_TIMEOUT_MINUTES, total=summary['total'], labeled=summary['labeled'], with_audio=summary['with_audio'])) else: st.info(t("session_expired_clean")) sm.clear_session() sm.init_session() # ── DATABASE (metadata only — never images or audio) ──────────────────────── utils.setup_env() try: active_db_type = db.init_db() except Exception as e: st.error(t("db_error", error=str(e))) st.stop() # ── SIDEBAR ────────────────────────────────────────────────────────────────── with st.sidebar: st.title(t("settings")) # Language selector lang_codes = list(SUPPORTED_LANGUAGES.keys()) lang_names = list(SUPPORTED_LANGUAGES.values()) current_lang_idx = lang_codes.index(st.session_state.ui_language) if st.session_state.ui_language in lang_codes else 0 selected_ui_lang = st.selectbox( t("ui_language"), lang_names, index=current_lang_idx, key="_ui_language_selector", ) new_lang_code = lang_codes[lang_names.index(selected_ui_lang)] if new_lang_code != st.session_state.ui_language: st.session_state.ui_language = new_lang_code st.rerun() st.divider() # Doctor name doctor = st.text_input( t("doctor_name"), value=st.session_state.get("doctor_name", ""), ) if doctor != st.session_state.get("doctor_name", ""): st.session_state.doctor_name = doctor st.divider() # Whisper language (select FIRST so models can be filtered) lang_keys = list(config.WHISPER_LANGUAGE_OPTIONS.keys()) lang_labels = list(config.WHISPER_LANGUAGE_OPTIONS.values()) selected_lang_display = st.selectbox(t("dictation_language"), lang_labels, index=0) selected_language = lang_keys[lang_labels.index(selected_lang_display)] # Whisper model — filtered by selected language # Models ending in ".en" → English only. Others → multilingual. # "large" and "turbo" are multilingual and work for all languages. if selected_language == "en": available_models = [ m for m in config.WHISPER_MODEL_OPTIONS if m.endswith(".en") or m in ("large", "turbo") ] else: available_models = [ m for m in config.WHISPER_MODEL_OPTIONS if not m.endswith(".en") ] selected_model = st.selectbox( t("whisper_model"), available_models, index=0, ) st.divider() # ── Session progress ───────────────────────────────────────────────────── labeled, total = sm.get_labeling_progress() st.subheader(t("current_session")) st.caption(f"{t('db_type')}: **{active_db_type}**") if total > 0: st.write(f"{t('images_loaded')}: **{total}**") st.write(f"{t('labeled_count')}: **{labeled}** / {total}") st.progress(labeled / total if total > 0 else 0) else: st.info(t("no_images")) st.divider() # ── Annotation History (from DB) — Grouped by image ──────────────────────── st.subheader(t("history")) search_input = st.text_input( t("search_image"), value=st.session_state.get("history_search", ""), ) if search_input != st.session_state.get("history_search", ""): st.session_state.history_search = search_input st.session_state.history_page = 1 st.rerun() if "history_page" not in st.session_state: st.session_state.history_page = 1 ITEMS_PER_PAGE = 5 try: history_groups, total_items = db.get_history_grouped( st.session_state.get("history_search", ""), st.session_state.history_page, ITEMS_PER_PAGE, ) except Exception as e: st.error(t("history_error", error=str(e))) history_groups, total_items = [], 0 if not history_groups: st.caption(t("no_records")) else: for group in history_groups: fname = group["imageFilename"] annotations = group["annotations"] n_annotations = len(annotations) latest = annotations[0] latest_label = latest.get("label") or "—" # Badge showing number of labelings badge = f" ({n_annotations}x)" if n_annotations > 1 else "" with st.expander(f"📄 {fname}{badge} — {latest_label}"): for i, ann in enumerate(annotations): ts = str(ann.get("createdAt", ""))[:16] label = ann.get("label") or "—" doctor = ann.get("doctorName") or "—" text = ann.get("transcription", "") or "" preview = (text[:60] + "…") if len(text) > 60 else text if n_annotations > 1: st.markdown( f"**#{i + 1}** — `{ts}`" ) st.write(f"**{t('label_header')}:** {label_display(label) if label != '—' else label}") st.write(f"**{t('doctor_header')}:** {doctor}") if preview: st.caption(f"📝 {preview}") else: st.caption(f"_{t('no_transcription')}_") if i < n_annotations - 1: st.divider() total_pages = max(1, math.ceil(total_items / ITEMS_PER_PAGE)) if total_pages > 1: c1, c2, c3 = st.columns([1, 2, 1]) with c1: if st.session_state.history_page > 1: if st.button("◀️"): st.session_state.history_page -= 1 st.rerun() with c2: st.markdown( f"