#!/usr/bin/env python3 """ Gradio web frontend for Audio Transcription Pipeline. Serves on port 8080 and communicates with the FastAPI backend on port 8081. """ import json import os import sys import gradio as gr import requests # Backend URL — use environment variable or default to the public URL BACKEND_URL = os.environ.get( "BACKEND_URL", "http://732f2c14-2061-4e03-b9ae-3991488db047.heyneo.so/pV8tkPgOgiO1hVq3u6kbfV9wwIIoOm8M/bknd", ) HEADERS = {"Accept": "application/json"} def health_check(): """Check if backend is healthy.""" try: r = requests.get(f"{BACKEND_URL}/health", timeout=5) r.raise_for_status() data = r.json() return f"✅ Status: {data['status']} — v{data['version']}" except Exception as e: return f"❌ Backend unreachable: {e}" def transcribe_audio(file, language, vad_filter, include_summary, include_ascii): """Upload and transcribe an audio file via the backend API.""" if file is None: return "No file provided.", None, None, None # Prepare form data lang = language if language and language != "auto" else None files = {"file": (os.path.basename(file), open(file, "rb"), "audio/wav")} data = { "language": lang or "", "vad_filter": str(vad_filter).lower(), "include_summary": str(include_summary).lower(), "include_ascii": str(include_ascii).lower(), } try: r = requests.post( f"{BACKEND_URL}/transcribe", files=files, data=data, timeout=300, ) r.raise_for_status() result = r.json() except Exception as e: return f"❌ Transcription failed: {e}", None, None, None # Build text output lines = [] segments = result.get("segments", []) audience_responses = result.get("audience_responses", []) summary = result.get("summary", {}) ascii_frames = result.get("ascii_frames", []) metadata = result.get("metadata", {}) lines.append("=" * 60) lines.append("TRANSCRIPTION RESULT") lines.append("=" * 60) dur = metadata.get("duration", "N/A") proc = metadata.get("api_processing_time_seconds", metadata.get("processing_time_seconds", "?")) lines.append(f"Duration: {dur:.1f}s" if isinstance(dur, (int, float)) else f"Duration: {dur}") lines.append(f"Processing: {proc}s" if isinstance(proc, (int, float)) else f"Processing: {proc}") lines.append(f"Segments: {len(segments)}") lines.append("") if segments: lines.append("--- TRANSCRIPT ---") for seg in segments: speaker = seg.get("speaker", "?") text = seg.get("text", "").strip() start = seg.get("start", 0) end = seg.get("end", 0) audience = seg.get("audience_response", "") tag = f" [{audience}]" if audience and audience != "unknown" else "" lines.append(f" [{start:6.1f}s - {end:6.1f}s] {speaker}: {text}{tag}") lines.append("") if audience_responses: lines.append("--- AUDIENCE RESPONSES ---") for resp in audience_responses[:20]: lines.append( f" [{resp.get('start', 0):.1f}s-{resp.get('end', 0):.1f}s] " f"{resp.get('response_class', '?')} " f"(conf: {resp.get('confidence', 0):.2f})" ) if len(audience_responses) > 20: lines.append(f" ... and {len(audience_responses) - 20} more") lines.append("") if summary and summary.get("overview"): lines.append("--- SUMMARY ---") lines.append(f"Overview: {summary.get('overview', 'N/A')}") decisions = summary.get("decisions", []) if decisions: lines.append("Decisions:") for d in decisions: lines.append(f" - {d}") actions = summary.get("action_items", []) if actions: lines.append("Action Items:") for a in actions: lines.append(f" - {a}") topics = summary.get("topics", []) if topics: lines.append("Topics:") for t in topics: lines.append(f" - {t}") lines.append("") if ascii_frames: lines.append(f"--- ASCII SPECTROGRAM: {len(ascii_frames)} frames ---") for frame_data in ascii_frames[:3]: lines.append(f"t={frame_data.get('timestamp', 0):.1f}s") frame_text = frame_data.get("frame", "") for line in frame_text.split("\n")[:4]: lines.append(f" |{line}") if len(ascii_frames) > 3: lines.append(f" ... and {len(ascii_frames) - 3} more frames") lines.append("") text_output = "\n".join(lines) json_output = json.dumps(result, indent=2, ensure_ascii=False, default=str) # Build a simple frame display for first 10 frames frame_previews = [] for fd in ascii_frames[:10]: ts = fd.get("timestamp", 0) text = fd.get("frame", "") frame_previews.append(f"--- Frame @ {ts:.1f}s ---\n{text}") return text_output, json_output, frame_previews, result def ascii_viz(file, columns, rows, fps, mode): """Generate ASCII spectrogram visualization for an audio file.""" if file is None: return "No file provided." import librosa # Use local processing for ASCII viz try: from pipeline.ascii_spectrogram import AsciiSpectrogram audio, sr = librosa.load(file, sr=16000, mono=True) duration = len(audio) / sr viz = AsciiSpectrogram(columns=columns, rows=rows, fps=fps) frames = list(viz.generate_frames(audio, sr, fps=fps)) result = f"Generated {len(frames)} frames from {duration:.1f}s audio\n\n" for ascii_text, timestamp in frames[:5]: result += f"=== Frame @ {timestamp:.1f}s ===\n{ascii_text}\n\n" if len(frames) > 5: result += f"... and {len(frames) - 5} more frames\n" return result except Exception as e: return f"❌ ASCII viz failed: {e}" # Build the Gradio interface with gr.Blocks( title="Audio Transcription Pipeline", ) as demo: gr.Markdown( """ # 🎙️ Audio Transcription Pipeline Transcribe, classify audience responses, diarize speakers, summarize meetings, and visualize audio as ASCII spectrograms. """ ) with gr.Tab("Transcribe"): with gr.Row(): with gr.Column(scale=2): file_input = gr.File( label="Upload Audio File", file_types=[".wav", ".mp3", ".m4a", ".ogg", ".flac"], ) with gr.Row(): lang_input = gr.Dropdown( choices=["auto", "en", "es", "fr", "de", "zh", "ja", "ko"], value="auto", label="Language", ) vad_toggle = gr.Checkbox(value=True, label="VAD Filter") with gr.Row(): summary_toggle = gr.Checkbox(value=True, label="Include Summary") ascii_toggle = gr.Checkbox(value=False, label="Include ASCII Viz") transcribe_btn = gr.Button( "🚀 Transcribe", variant="primary", size="lg" ) with gr.Column(scale=3): output_text = gr.Textbox( label="Results", lines=25, max_lines=40, ) with gr.Accordion("JSON Output", open=False): output_json = gr.JSON(label="Raw JSON") with gr.Row(): with gr.Column(): frame_output = gr.HTML(label="ASCII Frame Preview") with gr.Column(): health_output = gr.Textbox( label="Backend Status", lines=1, interactive=False ) health_btn = gr.Button("Check Backend Health", size="sm") transcribe_btn.click( fn=transcribe_audio, inputs=[file_input, lang_input, vad_toggle, summary_toggle, ascii_toggle], outputs=[output_text, output_json, frame_output, gr.State()], ) health_btn.click(fn=health_check, inputs=[], outputs=[health_output]) with gr.Tab("ASCII Spectrogram"): with gr.Row(): with gr.Column(): ascii_file = gr.File( label="Upload Audio for ASCII Viz", file_types=[".wav", ".mp3", ".m4a", ".ogg", ".flac"], ) with gr.Row(): ascii_cols = gr.Slider( minimum=40, maximum=160, value=80, step=10, label="Columns" ) ascii_rows = gr.Slider( minimum=10, maximum=40, value=20, step=5, label="Rows" ) with gr.Row(): ascii_fps = gr.Slider( minimum=5, maximum=30, value=10, step=5, label="FPS" ) ascii_mode = gr.Radio( choices=["spectrogram", "waveform", "combined"], value="spectrogram", label="Mode", ) ascii_btn = gr.Button("🎨 Generate ASCII Viz", variant="secondary") with gr.Column(): ascii_output = gr.Textbox( label="ASCII Output", lines=25, max_lines=40, ) ascii_btn.click( fn=ascii_viz, inputs=[ascii_file, ascii_cols, ascii_rows, ascii_fps, ascii_mode], outputs=[ascii_output], ) with gr.Tab("About"): gr.Markdown( """ ## About This Pipeline This audio transcription pipeline processes audio files through multiple stages: 1. **Transcription** — faster-whisper (CTranslate2 INT8 on CPU) with word-level timestamps and VAD filtering 2. **Audience Classification** — AST (Audio Spectrogram Transformer) with 527 AudioSet classes mapped to 25 audience response categories 3. **Speaker Diarization** — pyannote-audio (or mock when no HF token) 4. **Meeting Summarization** — Qwen2.5-0.5B-Instruct via llama-cpp-python GGUF 5. **ASCII Spectrogram** — GlyphCast-inspired ASCII art conversion of mel-spectrogram frames ### System Requirements - CPU only (no GPU required) - ~15 GB RAM (uses ~5 GB) - 4+ CPU cores recommended ### Backend API The FastAPI backend is available at port 8081 with endpoints: - `GET /health` — Health check - `POST /transcribe` — Full pipeline transcription - `GET /ascii-viz` — ASCII spectrogram frames """ ) if __name__ == "__main__": demo.launch( server_name="0.0.0.0", server_port=8080, share=False, )