Buckets:
| #!/usr/bin/env python3 | |
| """ | |
| Audio Transcription Pipeline CLI. | |
| Process audio files through transcription, audience classification, | |
| diarization, summarization, and ASCII spectrogram visualization. | |
| Usage: | |
| python main.py transcribe <audio_path> [--output json] | |
| python main.py summarize <audio_path> [--output json] | |
| python main.py audience <audio_path> [--output json] | |
| python main.py ascii-viz <audio_path> [--output file] | |
| python main.py stream <device> [--output json] | |
| python main.py all <audio_path> [--output json] [--ascii] | |
| """ | |
| import argparse | |
| import json | |
| import logging | |
| import os | |
| import sys | |
| from typing import Dict, Any | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(levelname)s:%(name)s:%(message)s", | |
| ) | |
| logger = logging.getLogger(__name__) | |
| # Disable verbose logging from libraries | |
| logging.getLogger("faster_whisper").setLevel(logging.WARNING) | |
| logging.getLogger("transformers").setLevel(logging.WARNING) | |
| logging.getLogger("librosa").setLevel(logging.WARNING) | |
| logging.getLogger("pipeline").setLevel(logging.INFO) | |
| def _import_pipeline(): | |
| """Import pipeline modules (lazy to avoid slow startup for help).""" | |
| from pipeline.transcriber import Transcriber | |
| from pipeline.audience_classifier import AudienceResponseClassifier | |
| from pipeline.diarizer import Diarizer | |
| from pipeline.summarizer import MeetingSummarizer | |
| from pipeline.ascii_spectrogram import AsciiSpectrogram | |
| from pipeline.orchestrator import AudioPipeline | |
| return { | |
| "Transcriber": Transcriber, | |
| "AudienceResponseClassifier": AudienceResponseClassifier, | |
| "Diarizer": Diarizer, | |
| "MeetingSummarizer": MeetingSummarizer, | |
| "AsciiSpectrogram": AsciiSpectrogram, | |
| "AudioPipeline": AudioPipeline, | |
| } | |
| def _output_result(result: Dict[str, Any], output_format: str, output_path: str = None): | |
| """Output result in specified format.""" | |
| # Convert non-serializable items | |
| clean = _clean_for_json(result) | |
| if output_format == "json": | |
| output = json.dumps(clean, indent=2, ensure_ascii=False, default=str) | |
| if output_path: | |
| with open(output_path, "w") as f: | |
| f.write(output) | |
| logger.info(f"Output written to {output_path}") | |
| else: | |
| print(output) | |
| else: | |
| # Text format | |
| _print_text_result(clean, output_path) | |
| def _clean_for_json(obj): | |
| """Recursively clean objects for JSON serialization.""" | |
| if isinstance(obj, dict): | |
| return {k: _clean_for_json(v) for k, v in obj.items() if not k.startswith("_")} | |
| elif isinstance(obj, list): | |
| return [_clean_for_json(item) for item in obj] | |
| elif isinstance(obj, float): | |
| if obj != obj: # NaN check | |
| return None | |
| return obj | |
| return obj | |
| def _print_text_result(result: Dict[str, Any], output_path: str = None): | |
| """Print result in human-readable text format.""" | |
| lines = [] | |
| if "error" in result: | |
| lines.append(f"ERROR: {result['error']}") | |
| else: | |
| # Metadata | |
| meta = result.get("metadata", {}) | |
| lines.append("=" * 60) | |
| lines.append("AUDIO TRANSCRIPTION PIPELINE RESULT") | |
| lines.append("=" * 60) | |
| lines.append( | |
| f"Duration: {meta.get('duration', 'N/A'):.1f}s | " | |
| f"Segments: {meta.get('num_segments', 0)} | " | |
| f"Processing: {meta.get('processing_time_seconds', 0):.1f}s" | |
| ) | |
| lines.append("") | |
| # Segments | |
| segments = result.get("segments", []) | |
| 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", "") | |
| conf = seg.get("confidence", 0) | |
| 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("") | |
| # Audience responses | |
| ar = result.get("audience_responses", []) | |
| if ar: | |
| lines.append("--- AUDIENCE RESPONSES ---") | |
| for resp in ar[:10]: # Show first 10 | |
| 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(ar) > 10: | |
| lines.append(f" ... and {len(ar) - 10} more") | |
| lines.append("") | |
| # Summary | |
| summary = result.get("summary", {}) | |
| 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("") | |
| # ASCII frames (show summary count only) | |
| frames = result.get("ascii_frames", []) | |
| if frames: | |
| lines.append(f"--- ASCII SPECTROGRAM ---") | |
| lines.append(f" {len(frames)} frames generated") | |
| lines.append(f" First frame preview:") | |
| first = frames[0] | |
| if isinstance(first, dict): | |
| lines.append(f" t={first.get('timestamp', 0):.1f}s") | |
| frame_text = first.get("frame", "") | |
| for line in frame_text.split("\n")[:5]: | |
| lines.append(f" |{line}") | |
| else: | |
| lines.append(f" {str(first)[:60]}...") | |
| lines.append("") | |
| output = "\n".join(lines) | |
| if output_path: | |
| with open(output_path, "w") as f: | |
| f.write(output) | |
| logger.info(f"Output written to {output_path}") | |
| else: | |
| print(output) | |
| def cmd_transcribe(args): | |
| """Transcribe an audio file.""" | |
| # Hybrid mode: use Encoder-Projector-LLM pipeline | |
| if getattr(args, "hybrid", False): | |
| from hybrid_model.infer import run_pipeline | |
| # Determine paths | |
| models_dir = "models" | |
| hybrid_dir = "hybrid_model" | |
| qwen_gguf = os.path.join(models_dir, "Qwen3-8B-Q4_K_M.gguf") | |
| qwen_fallback = os.path.join(models_dir, "qwen2.5-0.5b-instruct-q4_k_m.gguf") | |
| projector_ckpt = os.path.join(hybrid_dir, "projector_checkpoint_best.pt") | |
| if os.path.exists(qwen_gguf): | |
| llm_path = qwen_gguf | |
| elif os.path.exists(qwen_fallback): | |
| llm_path = qwen_fallback | |
| logger.warning("Qwen3-8B not found, using Qwen2.5-0.5B fallback") | |
| else: | |
| logger.error("No LLM GGUF found for hybrid mode!") | |
| sys.exit(1) | |
| logger.info(f"Hybrid mode: LLM={llm_path}, Projector={projector_ckpt}") | |
| result_text = run_pipeline( | |
| audio_path=args.audio_path, | |
| llm_path=llm_path, | |
| projector_checkpoint=projector_ckpt, | |
| max_new_tokens=200, | |
| temperature=0.1, | |
| refine_with_llm_flag=True, | |
| ) | |
| # Estimate duration from audio for metadata | |
| import librosa | |
| try: | |
| audio_dur = librosa.get_duration(path=args.audio_path) | |
| except Exception: | |
| audio_dur = 0.0 | |
| # Build result dict | |
| result = { | |
| "metadata": { | |
| "hybrid_mode": True, | |
| "llm": os.path.basename(llm_path), | |
| "duration": audio_dur, | |
| "num_segments": 1, | |
| "processing_time_seconds": 0.0, | |
| }, | |
| "segments": [{"start": 0, "end": audio_dur, "speaker": "SPEAKER_00", "text": result_text}], | |
| } | |
| _output_result(result, args.output, getattr(args, "output_file", None)) | |
| return | |
| # Standard pipeline mode | |
| mods = _import_pipeline() | |
| pipeline = mods["AudioPipeline"](enable_summarizer=False, enable_ascii=False) | |
| result = pipeline.process_file( | |
| args.audio_path, | |
| language=args.language, | |
| vad_filter=not args.no_vad, | |
| ) | |
| _output_result(result, args.output, getattr(args, "output_file", None)) | |
| def cmd_summarize(args): | |
| """Transcribe and summarize a meeting audio.""" | |
| mods = _import_pipeline() | |
| pipeline = mods["AudioPipeline"]( | |
| enable_summarizer=True, enable_ascii=False | |
| ) | |
| result = pipeline.process_file( | |
| args.audio_path, | |
| language=args.language, | |
| vad_filter=not args.no_vad, | |
| ) | |
| _output_result(result, args.output, getattr(args, "output_file", None)) | |
| def cmd_audience(args): | |
| """Classify audience responses in an audio file.""" | |
| mods = _import_pipeline() | |
| classifier = mods["AudienceResponseClassifier"]() | |
| if args.output == "json": | |
| result = classifier.classify_file(args.audio_path) | |
| print(json.dumps(result, indent=2)) | |
| else: | |
| result = classifier.classify_file(args.audio_path) | |
| print(f"Audience Responses ({len(result)} detections):") | |
| for resp in result: | |
| print( | |
| f" [{resp['start']:.1f}s-{resp['end']:.1f}s] " | |
| f"{resp['response_class']} ({resp['confidence']:.2f})" | |
| ) | |
| def cmd_ascii_viz(args): | |
| """Generate ASCII spectrogram visualization.""" | |
| import librosa | |
| mods = _import_pipeline() | |
| audio, sr = librosa.load(args.audio_path, sr=16000, mono=True) | |
| duration = len(audio) / sr | |
| viz = mods["AsciiSpectrogram"]( | |
| columns=args.columns, | |
| rows=args.rows, | |
| fps=args.fps, | |
| mode=args.mode, | |
| ) | |
| frames = list(viz.generate_frames(audio, sr)) | |
| print(f"Generated {len(frames)} frames from {duration:.1f}s audio") | |
| if args.output == "file" or args.output_file: | |
| path = args.output_file or f"ascii_output_{os.path.basename(args.audio_path)}.txt" | |
| with open(path, "w") as f: | |
| for ascii_text, timestamp in frames: | |
| f.write(f"--- Frame @ {timestamp:.1f}s ---\n{ascii_text}\n\n") | |
| print(f"ASCII frames written to: {path}") | |
| else: | |
| # Print first few frames | |
| for i, (ascii_text, timestamp) in enumerate(frames[:5]): | |
| print(f"\n=== Frame {i + 1} @ {timestamp:.1f}s ===") | |
| print(ascii_text) | |
| if len(frames) > 5: | |
| print(f"\n... and {len(frames) - 5} more frames") | |
| def cmd_stream(args): | |
| """Process stream from microphone or file.""" | |
| # For now, just read a file in streaming mode | |
| import librosa | |
| import soundfile as sf | |
| import tempfile | |
| mods = _import_pipeline() | |
| pipeline = mods["AudioPipeline"](enable_summarizer=False, enable_ascii=False) | |
| # Load audio and chunk it | |
| audio, sr = librosa.load(args.audio_path, sr=16000, mono=True) | |
| chunk_size = sr * 2 # 2-second chunks | |
| def chunk_gen(): | |
| for i in range(0, len(audio), chunk_size): | |
| yield audio[i : i + chunk_size] | |
| print(f"Stream processing: {args.audio_path}") | |
| for i, result in enumerate(pipeline.process_stream(chunk_gen(), sr)): | |
| segments = result.get("segments", []) | |
| print(f"Chunk {i + 1}: {len(segments)} segments") | |
| for seg in segments[:2]: | |
| print(f" [{seg.get('start', 0):.1f}s] {seg.get('speaker', '?')}: {seg.get('text', '')[:60]}") | |
| def cmd_all(args): | |
| """Run full pipeline (transcribe + audience + diarize + summarize + optional ASCII).""" | |
| mods = _import_pipeline() | |
| pipeline = mods["AudioPipeline"]( | |
| enable_summarizer=True, | |
| enable_ascii=args.ascii, | |
| ) | |
| result = pipeline.process_file( | |
| args.audio_path, | |
| language=args.language, | |
| vad_filter=not args.no_vad, | |
| ) | |
| _output_result(result, args.output, getattr(args, "output_file", None)) | |
| def main(): | |
| parser = argparse.ArgumentParser( | |
| description="Audio Transcription Pipeline — transcribe, classify, " | |
| "diarize, summarize, and visualize audio", | |
| formatter_class=argparse.RawDescriptionHelpFormatter, | |
| epilog=""" | |
| Examples: | |
| python main.py transcribe meeting.wav | |
| python main.py transcribe meeting.wav --output json --no-vad | |
| python main.py summarize meeting.wav --output json | |
| python main.py all meeting.wav --ascii --output json | |
| python main.py audience presentation.wav | |
| python main.py ascii-viz music.mp3 --mode combined --columns 100 | |
| """, | |
| ) | |
| parser.add_argument( | |
| "--verbose", "-v", action="store_true", help="Enable verbose logging" | |
| ) | |
| subparsers = parser.add_subparsers(dest="command", help="Command to execute") | |
| # transcribe | |
| p_transcribe = subparsers.add_parser( | |
| "transcribe", help="Transcribe audio to text" | |
| ) | |
| p_transcribe.add_argument("audio_path", help="Path to audio file") | |
| p_transcribe.add_argument( | |
| "--output", choices=["text", "json"], default="text", help="Output format" | |
| ) | |
| p_transcribe.add_argument( | |
| "--output-file", "-o", help="Write output to file instead of stdout" | |
| ) | |
| p_transcribe.add_argument( | |
| "--language", "-l", help="Language code (e.g., 'en'). Default: auto-detect" | |
| ) | |
| p_transcribe.add_argument( | |
| "--no-vad", action="store_true", help="Disable VAD filtering" | |
| ) | |
| p_transcribe.add_argument( | |
| "--hybrid", action="store_true", | |
| help="Use hybrid Encoder-Projector-LLM model (Whisper+Qwen3-8B)" | |
| ) | |
| p_transcribe.set_defaults(func=cmd_transcribe) | |
| # summarize | |
| p_summarize = subparsers.add_parser( | |
| "summarize", help="Transcribe and summarize meeting audio" | |
| ) | |
| p_summarize.add_argument("audio_path", help="Path to audio file") | |
| p_summarize.add_argument( | |
| "--output", choices=["text", "json"], default="text", help="Output format" | |
| ) | |
| p_summarize.add_argument( | |
| "--output-file", "-o", help="Write output to file instead of stdout" | |
| ) | |
| p_summarize.add_argument( | |
| "--language", "-l", help="Language code (default: auto-detect)" | |
| ) | |
| p_summarize.add_argument( | |
| "--no-vad", action="store_true", help="Disable VAD filtering" | |
| ) | |
| p_summarize.set_defaults(func=cmd_summarize) | |
| # audience | |
| p_audience = subparsers.add_parser( | |
| "audience", help="Classify audience responses in audio" | |
| ) | |
| p_audience.add_argument("audio_path", help="Path to audio file") | |
| p_audience.add_argument( | |
| "--output", choices=["text", "json"], default="text", help="Output format" | |
| ) | |
| p_audience.set_defaults(func=cmd_audience) | |
| # ascii-viz | |
| p_ascii = subparsers.add_parser( | |
| "ascii-viz", help="Generate ASCII spectrogram visualization" | |
| ) | |
| p_ascii.add_argument("audio_path", help="Path to audio file") | |
| p_ascii.add_argument( | |
| "--mode", | |
| choices=["spectrogram", "waveform", "combined"], | |
| default="spectrogram", | |
| help="Visualization mode", | |
| ) | |
| p_ascii.add_argument("--columns", type=int, default=80, help="ASCII width") | |
| p_ascii.add_argument("--rows", type=int, default=20, help="ASCII height") | |
| p_ascii.add_argument("--fps", type=int, default=10, help="Frames per second") | |
| p_ascii.add_argument( | |
| "--output", choices=["text", "file"], default="text", help="Output format" | |
| ) | |
| p_ascii.add_argument( | |
| "--output-file", "-o", help="Write output to file" | |
| ) | |
| p_ascii.set_defaults(func=cmd_ascii_viz) | |
| # stream | |
| p_stream = subparsers.add_parser( | |
| "stream", help="Stream process audio (chunked from file)" | |
| ) | |
| p_stream.add_argument("audio_path", help="Path to audio file to stream") | |
| p_stream.add_argument( | |
| "--output", choices=["text", "json"], default="text", help="Output format" | |
| ) | |
| p_stream.set_defaults(func=cmd_stream) | |
| # all | |
| p_all = subparsers.add_parser( | |
| "all", help="Run full pipeline (transcribe + audience + diarize + summarize)" | |
| ) | |
| p_all.add_argument("audio_path", help="Path to audio file") | |
| p_all.add_argument( | |
| "--output", choices=["text", "json"], default="text", help="Output format" | |
| ) | |
| p_all.add_argument( | |
| "--output-file", "-o", help="Write output to file instead of stdout" | |
| ) | |
| p_all.add_argument( | |
| "--language", "-l", help="Language code (default: auto-detect)" | |
| ) | |
| p_all.add_argument( | |
| "--no-vad", action="store_true", help="Disable VAD filtering" | |
| ) | |
| p_all.add_argument( | |
| "--ascii", action="store_true", help="Include ASCII spectrogram frames" | |
| ) | |
| p_all.set_defaults(func=cmd_all) | |
| args = parser.parse_args() | |
| if args.verbose: | |
| logging.getLogger().setLevel(logging.DEBUG) | |
| if not hasattr(args, "func"): | |
| parser.print_help() | |
| sys.exit(1) | |
| args.func(args) | |
| if __name__ == "__main__": | |
| main() |
Xet Storage Details
- Size:
- 17.3 kB
- Xet hash:
- 1ef2e03e950b50bed6fd55894755afcab5c2c897d5dd02765b80f4f3074bc092
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.