""" Pidgin Whisper — HuggingFace Space demo (dark mode). Loads `openai/whisper-large-v3-turbo` + the LoRA adapter from `michaelodafe/whisper-pidgin-v1`, merges in-memory, and transcribes audio uploaded or recorded by the user. Targets free-CPU Spaces; latency is ~5–15 s per clip on CPU. """ # --- workaround for gradio_client bug --- # JSON Schema permits `additionalProperties: false/true` (a bool). # Some gradio_client versions don't handle this, crashing `api_info` # with `TypeError: argument of type 'bool' is not iterable`. That # crash bubbles up as "No API found" in the browser. Patch before # importing gradio so the routes module picks up the fixed helpers. import gradio_client.utils as _gcu _orig_json_to_py = _gcu._json_schema_to_python_type def _safe_json_to_py(schema, defs=None): if isinstance(schema, bool): return "Any" if schema else "None" return _orig_json_to_py(schema, defs) _gcu._json_schema_to_python_type = _safe_json_to_py _orig_get_type = _gcu.get_type def _safe_get_type(schema): if isinstance(schema, bool): return "Any" if schema else "None" return _orig_get_type(schema) _gcu.get_type = _safe_get_type # --- end workaround --- import os import re import gradio as gr import librosa import numpy as np import torch from peft import PeftModel from transformers import WhisperForConditionalGeneration, WhisperProcessor BASE = "openai/whisper-large-v3-turbo" ADAPTER = "michaelodafe/whisper-pidgin-v1" MAX_SECONDS = 30 # Shared decode helpers (hotword prompt + punctuation/casing formatter). from decode import INITIAL_PROMPT, format_output print("Loading processor + base model + adapter...") processor = WhisperProcessor.from_pretrained(BASE, language="english", task="transcribe") base = WhisperForConditionalGeneration.from_pretrained(BASE, torch_dtype=torch.float32) peft_model = PeftModel.from_pretrained(base, ADAPTER) model = peft_model.merge_and_unload().eval() model.generation_config.language = "english" model.generation_config.task = "transcribe" model.generation_config.forced_decoder_ids = None model.generation_config.suppress_tokens = [] try: prompt_ids = processor.get_prompt_ids(INITIAL_PROMPT, return_tensors="pt") if prompt_ids.dim() > 1: prompt_ids = prompt_ids.squeeze() except Exception as e: print(f"prompt_ids disabled: {e}") prompt_ids = None print("Model ready.") @torch.no_grad() def transcribe(audio): if audio is None: return "Please record or upload a Pidgin audio clip first." try: arr, sr = librosa.load(audio, sr=16000, mono=True) arr = arr.astype("float32") if len(arr) < 1600: return "Audio is too short — speak for at least half a second." if len(arr) > MAX_SECONDS * 16000: arr = arr[: MAX_SECONDS * 16000] inputs = processor(arr, sampling_rate=16000, return_tensors="pt") gen_kwargs = {"max_length": 225} if prompt_ids is not None: gen_kwargs["prompt_ids"] = prompt_ids output_ids = model.generate(inputs.input_features, **gen_kwargs) text = processor.batch_decode(output_ids, skip_special_tokens=True)[0] return format_output(text) except Exception as e: return f"⚠️ Error: {type(e).__name__}: {e}" # Dark-mode palette PAGE_BG = "#0b1220" CARD_BG = "#141c2e" CARD_BORDER = "#293548" INPUT_BG = "#0f172a" TEXT_PRIMARY = "#f1f5f9" TEXT_MUTED = "#94a3b8" NG_GREEN = "#008751" # flag green (kept identical for visual identity) UI_GREEN = "#22c55e" # brighter for dark contrast UI_GREEN_HOVER = "#16a34a" LINK_GREEN = "#4ade80" WARN_BG = "#3f2a08" WARN_TEXT = "#fde68a" WARN_BORDER = "#854d0e" BADGE_GREEN_BG = "#14532d" BADGE_GREEN_TEXT = "#86efac" GRADIO_BADGE_BG = "#7c2d12" GRADIO_BADGE_TEXT = "#fdba74" THEME = gr.themes.Soft( primary_hue="green", secondary_hue="orange", neutral_hue="slate", font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], ).set( # backgrounds body_background_fill=PAGE_BG, body_background_fill_dark=PAGE_BG, background_fill_primary=CARD_BG, background_fill_primary_dark=CARD_BG, background_fill_secondary=CARD_BG, background_fill_secondary_dark=CARD_BG, block_background_fill=CARD_BG, block_background_fill_dark=CARD_BG, input_background_fill=INPUT_BG, input_background_fill_dark=INPUT_BG, # text body_text_color=TEXT_PRIMARY, body_text_color_dark=TEXT_PRIMARY, body_text_color_subdued=TEXT_MUTED, body_text_color_subdued_dark=TEXT_MUTED, block_label_text_color=TEXT_PRIMARY, block_label_text_color_dark=TEXT_PRIMARY, block_title_text_color=TEXT_PRIMARY, block_title_text_color_dark=TEXT_PRIMARY, # borders border_color_primary=CARD_BORDER, border_color_primary_dark=CARD_BORDER, input_border_color=CARD_BORDER, input_border_color_dark=CARD_BORDER, block_border_color=CARD_BORDER, block_border_color_dark=CARD_BORDER, # primary button button_primary_background_fill=UI_GREEN, button_primary_background_fill_dark=UI_GREEN, button_primary_background_fill_hover=UI_GREEN_HOVER, button_primary_background_fill_hover_dark=UI_GREEN_HOVER, button_primary_text_color="white", button_primary_text_color_dark="white", button_primary_border_color=UI_GREEN, button_primary_border_color_dark=UI_GREEN, # secondary button button_secondary_background_fill="#1e293b", button_secondary_background_fill_dark="#1e293b", button_secondary_text_color=TEXT_PRIMARY, button_secondary_text_color_dark=TEXT_PRIMARY, button_secondary_border_color=CARD_BORDER, button_secondary_border_color_dark=CARD_BORDER, # general layout block_radius="12px", block_border_width="1px", ) CSS = f""" /* Force dark even if user OS prefers light */ :root, html, body {{ color-scheme: dark !important; }} .gradio-container {{ max-width: 760px !important; margin: 0 auto !important; padding: 24px 16px 32px !important; background: {PAGE_BG} !important; }} /* --- Hero --- */ #pw-hero {{ padding: 8px 4px 24px; }} #pw-hero-row {{ display: flex; align-items: flex-start; gap: 16px; flex-wrap: wrap; }} #pw-flag {{ width: 44px; height: 64px; border-radius: 6px; flex-shrink: 0; background: linear-gradient(to right, {NG_GREEN} 33%, #ffffff 33%, #ffffff 66%, {NG_GREEN} 66%); box-shadow: 0 2px 6px rgba(0,0,0,0.4); }} #pw-title-block {{ flex: 1; min-width: 220px; }} #pw-title-line {{ display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }} #pw-title {{ font-size: 2.0rem; font-weight: 800; color: {TEXT_PRIMARY}; margin: 0; letter-spacing: -0.02em; }} #pw-sub {{ color: {TEXT_MUTED}; font-size: 0.95rem; margin-top: 2px; }} .pw-badge {{ display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 0.78rem; font-weight: 600; background: {BADGE_GREEN_BG}; color: {BADGE_GREEN_TEXT}; }} .pw-badge::before {{ content: "●"; font-size: 0.5rem; color: {LINK_GREEN}; }} #pw-desc {{ color: {TEXT_PRIMARY}; margin-top: 16px; font-size: 0.95rem; line-height: 1.55; opacity: 0.92; }} #pw-links {{ margin-top: 8px; font-size: 0.9rem; }} #pw-links a {{ color: {LINK_GREEN}; text-decoration: none; font-weight: 600; }} #pw-links a:hover {{ text-decoration: underline; }} #pw-links .sep {{ color: {TEXT_MUTED}; margin: 0 6px; }} /* --- Cards (wraps audio + textbox) --- */ .pw-card {{ background: {CARD_BG} !important; border: 1px solid {CARD_BORDER} !important; border-radius: 12px !important; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3) !important; padding: 16px !important; margin-bottom: 16px !important; }} .pw-card-head {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 0.95rem; font-weight: 600; color: {TEXT_PRIMARY}; }} .pw-card-head .icon {{ margin-right: 6px; }} /* --- Big primary button --- */ .pw-btn button {{ width: 100% !important; padding: 14px 20px !important; font-size: 1rem !important; font-weight: 700 !important; border-radius: 10px !important; background: {UI_GREEN} !important; color: white !important; border: none !important; box-shadow: 0 1px 3px rgba(34, 197, 94, 0.3) !important; }} .pw-btn button:hover {{ background: {UI_GREEN_HOVER} !important; }} /* --- Warning banner --- */ #pw-warn {{ background: {WARN_BG}; color: {WARN_TEXT}; border: 1px solid {WARN_BORDER}; border-radius: 10px; padding: 10px 14px; font-size: 0.88rem; display: flex; align-items: center; gap: 10px; margin-bottom: 20px; }} /* --- Footer --- */ #pw-footer {{ display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; margin-top: 18px; padding-top: 18px; border-top: 1px solid {CARD_BORDER}; color: {TEXT_MUTED}; font-size: 0.85rem; }} #pw-footer a {{ color: {LINK_GREEN}; text-decoration: none; font-weight: 600; }} #pw-footer a:hover {{ text-decoration: underline; }} .pw-gradio-badge {{ background: {GRADIO_BADGE_BG}; color: {GRADIO_BADGE_TEXT}; font-weight: 600; padding: 3px 10px; border-radius: 999px; font-size: 0.78rem; }} /* --- Force-dark Gradio internals --- */ .gradio-container, .gradio-container * {{ color-scheme: dark; }} .gradio-container .gr-textbox textarea, .gradio-container input[type="text"], .gradio-container textarea {{ background: {INPUT_BG} !important; color: {TEXT_PRIMARY} !important; border-color: {CARD_BORDER} !important; }} .gradio-container .placeholder, .gradio-container ::placeholder {{ color: {TEXT_MUTED} !important; opacity: 0.7; }} /* trim Gradio's default block paddings inside our cards */ .pw-card .gradio-block {{ padding: 0 !important; border: none !important; background: transparent !important; box-shadow: none !important; }} """ # Force `?__theme=dark` so Gradio loads its dark internals (audio waveform colors, # dropdowns, etc.). Updates URL without a reload; takes effect immediately. FORCE_DARK_JS = """ () => { const params = new URLSearchParams(window.location.search); if (params.get('__theme') !== 'dark') { params.set('__theme', 'dark'); const newUrl = window.location.pathname + '?' + params.toString(); window.history.replaceState({}, '', newUrl); } document.documentElement.classList.add('dark'); document.body && document.body.classList.add('dark'); } """ with gr.Blocks(theme=THEME, css=CSS, title="Pidgin Whisper", fill_height=False, js=FORCE_DARK_JS) as demo: gr.HTML( """