| |
| """ |
| 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 = 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 |
|
|
| |
| 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 |
|
|
| |
| 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) |
|
|
| |
| 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 |
|
|
| |
| 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}" |
|
|
|
|
| |
| 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, |
| ) |