"""Paramatch Classification Frontend — HF Spaces Gradio app. Calls a remote backend (HF Space or URL) via gradio_client for predictions, renders results as styled HTML cards. Set the BACKEND_SPACE env var (or HF Spaces secret) to the backend Space name or URL, e.g. "your-username/paramatch-backend" or "http://localhost:7860". """ from __future__ import annotations import argparse import os import subprocess import tempfile import gradio as gr import librosa from gradio_client import Client, handle_file MAX_DURATION_SEC = 30 # --------------------------------------------------------------------------- # Configuration # --------------------------------------------------------------------------- DEFAULT_TOP_K = 3 # --------------------------------------------------------------------------- # CSS # --------------------------------------------------------------------------- CARD_CSS = """\ .result-container { display: flex; flex-direction: column; gap: 8px; } .result-card { border: 1px solid var(--border-color-primary); border-radius: 12px; padding: 12px 16px; background: var(--background-fill-primary); transition: box-shadow 0.2s; } .result-card:first-child { border: 2px solid var(--color-accent); box-shadow: 0 2px 12px var(--color-accent-soft); } .result-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 4px; } .rank-badge { font-size: 1.2em; min-width: 28px; text-align: center; } .prob-bar-container { flex: 1; height: 6px; background: var(--background-fill-secondary); border-radius: 3px; overflow: hidden; } .prob-bar { height: 100%; border-radius: 3px; transition: width 0.3s ease; } .prob-text { font-size: 0.95em; font-weight: 600; min-width: 52px; text-align: right; } .speaker-name { font-size: 1.1em; padding-left: 38px; color: var(--body-text-color); }""" RANK_ICONS = ["\U0001f947", "\U0001f948", "\U0001f949"] # --------------------------------------------------------------------------- # HTML builder # --------------------------------------------------------------------------- def build_result_html(data: dict) -> str: """Build styled HTML cards from backend response.""" results = data.get("results", []) if not results: return ( '
音声をアップロードして「分析」を押してください
' '対応形式: WAV, MP3, OGG, FLAC など
' "※敬称略
' ) # --------------------------------------------------------------------------- # Backend client # --------------------------------------------------------------------------- backend_space = os.environ.get("BACKEND_SPACE", "") backend_client: Client | None = None def get_client() -> Client: global backend_client if backend_client is None: if not backend_space: raise gr.Error( "BACKEND_SPACE env var is not set. " "Set it to the backend Space name or URL." ) backend_client = Client(backend_space) return backend_client def compress_audio(audio_path: str) -> str: """Compress audio to 16kHz mono OGG Vorbis to reduce network transfer.""" stem = os.path.splitext(os.path.basename(audio_path))[0] out_path = os.path.join(tempfile.gettempdir(), f"{stem}.ogg") subprocess.run( ["ffmpeg", "-y", "-i", audio_path, "-ar", "16000", "-ac", "1", "-c:a", "libvorbis", "-q:a", "4", out_path], capture_output=True, check=True, ) return out_path def process(audio_file: str | None) -> str: if audio_file is None: return build_result_html({}) duration = librosa.get_duration(path=audio_file) if duration > MAX_DURATION_SEC: raise gr.Error( f"音声が長すぎます({duration:.1f}秒)。{MAX_DURATION_SEC}秒以内の音声をアップロードしてください。" ) try: compressed = compress_audio(audio_file) except subprocess.CalledProcessError: raise gr.Error("音声ファイルの読み込みに失敗しました。別のファイルをお試しください。") try: client = get_client() data = client.predict(handle_file(compressed), DEFAULT_TOP_K, api_name="/classify") finally: os.unlink(compressed) if not data.get("results"): return ( '一致する話者が見つかりませんでした
' '別の音声ファイルをお試しください
' "