diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6bb511e..c9dfbcf 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -34,12 +34,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y libsndfile1 ffmpeg + sudo apt-get install -y libsndfile1 ffmpeg portaudio19-dev - name: Install system dependencies (macOS) if: runner.os == 'macOS' run: | - brew install libsndfile ffmpeg + brew install libsndfile ffmpeg portaudio - name: Install system dependencies (Windows) if: runner.os == 'Windows' @@ -84,6 +84,18 @@ jobs: print('[OK] All imports successful') asr = StreamingASR(debug=False) print(f'[OK] StreamingASR initialized: {asr.chunk_size_ms}ms chunks') + + # Test microphone feature availability (should not fail in CI) + try: + import sounddevice as sd + print('[OK] sounddevice imported successfully') + # Don't test actual microphone functionality in CI + print('[INFO] Microphone functionality available (not tested in CI)') + except ImportError as e: + print(f'[WARNING] sounddevice not available: {e}') + except Exception as e: + print(f'[INFO] sounddevice available but no audio devices in CI: {e}') + except Exception as e: print(f'[ERROR] Import failed: {e}') sys.exit(1) diff --git a/README.md b/README.md index 14e1dc3..783f452 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ for result in asr.stream_from_file("audio.wav"): ### Python API +#### Streaming from File ```python from ViStreamASR import StreamingASR @@ -67,6 +68,21 @@ for result in asr.stream_from_file("audio.wav"): print(f"Final: {result['text']}") ``` +#### Streaming from Microphone +```python +from ViStreamASR import StreamingASR + +# Initialize ASR +asr = StreamingASR() + +# Process audio file +for result in asr.stream_from_microphone(duration_seconds=10): + if result['partial']: + print(f"Partial: {result['text']}") + if result['final']: + print(f"Final: {result['text']}") +``` + ### Command Line ```bash diff --git a/example.py b/example.py index c513fa3..e5cbb4c 100644 --- a/example.py +++ b/example.py @@ -1,47 +1,54 @@ #!/usr/bin/env python3 """ Example script demonstrating ViStreamASR usage. +This script tests all features using the local codebase before building. """ import os import sys +import argparse -def main(): - print("๐ŸŽค ViStreamASR Example") - print("=" * 40) +# Add src directory to path to use local codebase instead of installed library +sys.path.insert(0, 'src') + +def test_file_streaming(audio_file, chunk_size=640, debug=True): + """Test file streaming functionality.""" + print(f"๐ŸŽต Testing File Streaming") + print(f"=" * 50) try: - from ViStreamASR import StreamingASR - print("โœ… ViStreamASR imported successfully") + from streaming import StreamingASR + print("โœ… Local StreamingASR imported successfully") except ImportError as e: - print(f"โŒ Failed to import ViStreamASR: {e}") - print("๐Ÿ’ก Try running: pip install -e .") - return 1 + print(f"โŒ Failed to import local StreamingASR: {e}") + print("๐Ÿ’ก Make sure you're running from the project root directory") + return False - # Check if example audio file exists - audio_file = "resource/linh_ref_long.wav" if not os.path.exists(audio_file): - print(f"โŒ Example audio file not found: {audio_file}") - return 1 + print(f"โŒ Audio file not found: {audio_file}") + return False print(f"๐Ÿ“ Using audio file: {audio_file}") # Initialize StreamingASR print(f"\n๐Ÿ”„ Initializing StreamingASR...") - asr = StreamingASR(chunk_size_ms=640, debug=True) + asr = StreamingASR(chunk_size_ms=chunk_size, debug=debug) # Run streaming transcription - print(f"\n๐ŸŽต Starting streaming transcription...") + print(f"\n๐ŸŽต Starting file streaming transcription...") print(f"=" * 60) final_segments = [] + partial_count = 0 try: for result in asr.stream_from_file(audio_file): chunk_info = result.get('chunk_info', {}) if result.get('partial'): - print(f"๐Ÿ“ [PARTIAL {chunk_info.get('chunk_id', '?'):3d}] {result['text']}") + partial_count += 1 + text = result['text'][:60] + "..." if len(result['text']) > 60 else result['text'] + print(f"๐Ÿ“ [PARTIAL {chunk_info.get('chunk_id', '?'):3d}] {text}") if result.get('final'): final_text = result['text'] @@ -50,26 +57,229 @@ def main(): print(f"-" * 60) except KeyboardInterrupt: - print(f"\nโน๏ธ Interrupted by user") - return 0 + print(f"\nโน๏ธ File streaming interrupted by user") + return True except Exception as e: - print(f"\nโŒ Error during streaming: {e}") + print(f"\nโŒ Error during file streaming: {e}") import traceback traceback.print_exc() - return 1 + return False + + # Show results + print(f"\n๐Ÿ“Š FILE STREAMING RESULTS") + print(f"=" * 40) + print(f"๐Ÿ“ Partial updates: {partial_count}") + print(f"๐Ÿ“ Final segments: {len(final_segments)}") + + if final_segments: + print(f"\n๐Ÿ“ Complete Transcription:") + print(f"-" * 40) + complete_text = " ".join(final_segments) + print(f"{complete_text}") + + print(f"\nโœ… File streaming test completed successfully!") + return True + +def test_microphone_streaming(duration=10, chunk_size=640, debug=True): + """Test microphone streaming functionality.""" + print(f"\n๐ŸŽค Testing Microphone Streaming") + print(f"=" * 50) + + try: + from streaming import StreamingASR + print("โœ… Local StreamingASR imported successfully") + except ImportError as e: + print(f"โŒ Failed to import local StreamingASR: {e}") + return False + + try: + import sounddevice as sd + print("โœ… sounddevice library available") + + # Test if microphone is available + devices = sd.query_devices() + input_devices = [d for d in devices if d['max_input_channels'] > 0] + if not input_devices: + print("โŒ No input devices (microphones) found") + return False + print(f"๐ŸŽค Found {len(input_devices)} input device(s)") + + except ImportError: + print("โŒ sounddevice library not installed. Install with: pip install sounddevice") + return False + except Exception as e: + print(f"โŒ Error checking audio devices: {e}") + return False + + # Initialize StreamingASR + print(f"\n๐Ÿ”„ Initializing StreamingASR for microphone...") + asr = StreamingASR(chunk_size_ms=chunk_size, debug=debug) + + # Run microphone streaming + print(f"\n๐ŸŽค Starting microphone streaming for {duration} seconds...") + print(f"๐Ÿ”Š Please speak into your microphone...") + print(f"=" * 60) + + final_segments = [] + partial_count = 0 + + try: + for result in asr.stream_from_microphone(duration_seconds=duration): + chunk_info = result.get('chunk_info', {}) + + if result.get('partial'): + partial_count += 1 + text = result['text'][:60] + "..." if len(result['text']) > 60 else result['text'] + print(f"๐ŸŽ™๏ธ [PARTIAL {chunk_info.get('chunk_id', '?'):3d}] {text}") + + if result.get('final'): + final_text = result['text'] + final_segments.append(final_text) + print(f"โœ… [FINAL {chunk_info.get('chunk_id', '?'):3d}] {final_text}") + print(f"-" * 60) + + except KeyboardInterrupt: + print(f"\nโน๏ธ Microphone streaming interrupted by user") + return True + except Exception as e: + print(f"\nโŒ Error during microphone streaming: {e}") + import traceback + traceback.print_exc() + return False # Show results - print(f"\n๐Ÿ“Š RESULTS") + print(f"\n๐Ÿ“Š MICROPHONE STREAMING RESULTS") print(f"=" * 40) + print(f"๐Ÿ“ Partial updates: {partial_count}") print(f"๐Ÿ“ Final segments: {len(final_segments)}") - print(f"\n๐Ÿ“ Complete Transcription:") - print(f"-" * 40) - complete_text = " ".join(final_segments) - print(f"{complete_text}") + if final_segments: + print(f"\n๐Ÿ“ Transcribed from microphone:") + print(f"-" * 40) + complete_text = " ".join(final_segments) + print(f"{complete_text}") + else: + print(f"โ„น๏ธ No speech detected or transcribed during recording") - print(f"\nโœ… Example completed successfully!") - return 0 + print(f"\nโœ… Microphone streaming test completed successfully!") + return True + +def test_asr_engine(): + """Test low-level ASREngine functionality.""" + print(f"\n๐Ÿ”ง Testing ASREngine (Low-level API)") + print(f"=" * 50) + + try: + from core import ASREngine + print("โœ… Local ASREngine imported successfully") + except ImportError as e: + print(f"โŒ Failed to import local ASREngine: {e}") + return False + + try: + # Test ASREngine initialization + engine = ASREngine(chunk_size_ms=640, debug_mode=True) + print("โœ… ASREngine initialized successfully") + + # Test model initialization + print("๐Ÿ”„ Initializing models (this may take a moment)...") + engine.initialize_models() + print("โœ… Models initialized successfully") + + # Test RTF calculation + rtf = engine.get_asr_rtf() + print(f"๐Ÿ“Š Current RTF: {rtf:.3f}x") + + # Test state reset + engine.reset_state() + print("โœ… State reset successful") + + print(f"\nโœ… ASREngine test completed successfully!") + return True + + except Exception as e: + print(f"\nโŒ Error during ASREngine testing: {e}") + import traceback + traceback.print_exc() + return False + +def main(): + parser = argparse.ArgumentParser( + description="ViStreamASR Feature Test Script", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + python example.py # Test all features + python example.py --file-only # Test only file streaming + python example.py --mic-only # Test only microphone streaming + python example.py --mic-duration 15 # Test microphone for 15 seconds + python example.py --show-debug # Enable debug output + """ + ) + + parser.add_argument('--file-only', action='store_true', + help='Test only file streaming functionality') + parser.add_argument('--mic-only', action='store_true', + help='Test only microphone streaming functionality') + parser.add_argument('--engine-only', action='store_true', + help='Test only ASREngine low-level functionality') + parser.add_argument('--audio-file', default="resource/linh_ref_long.wav", + help='Audio file for testing (default: resource/linh_ref_long.wav)') + parser.add_argument('--mic-duration', type=int, default=10, + help='Duration for microphone test in seconds (default: 10)') + parser.add_argument('--chunk-size', type=int, default=640, + help='Chunk size in milliseconds (default: 640)') + parser.add_argument('--show-debug', action='store_true', default=False, + help='Enable debug output') + + args = parser.parse_args() + + print("๐ŸŽค ViStreamASR Feature Test") + print("=" * 50) + print("๐Ÿงช Testing local codebase (src/) instead of installed library") + print() + + debug_mode = args.show_debug + test_results = [] + + # Test file streaming + if not args.mic_only and not args.engine_only: + file_result = test_file_streaming(args.audio_file, args.chunk_size, debug_mode) + test_results.append(("File Streaming", file_result)) + + # Test microphone streaming + if not args.file_only and not args.engine_only: + mic_result = test_microphone_streaming(args.mic_duration, args.chunk_size, debug_mode) + test_results.append(("Microphone Streaming", mic_result)) + + # Test ASREngine + if not args.file_only and not args.mic_only: + engine_result = test_asr_engine() + test_results.append(("ASREngine", engine_result)) + + # Summary + print(f"\n๐Ÿ TEST SUMMARY") + print(f"=" * 50) + + passed = 0 + failed = 0 + + for test_name, result in test_results: + status = "โœ… PASSED" if result else "โŒ FAILED" + print(f"{status} {test_name}") + if result: + passed += 1 + else: + failed += 1 + + print(f"\n๐Ÿ“Š Results: {passed} passed, {failed} failed") + + if failed == 0: + print(f"๐ŸŽ‰ All tests passed! The codebase is ready for building.") + return 0 + else: + print(f"โš ๏ธ Some tests failed. Please review the issues above.") + return 1 if __name__ == "__main__": sys.exit(main()) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ff76c2a..629b73b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ torchaudio>=2.5.0 numpy>=1.19.0 requests>=2.25.0 flashlight-text -librosa \ No newline at end of file +librosa +sounddevice>=0.5.2 diff --git a/setup.py b/setup.py index 933ad58..ceafde2 100644 --- a/setup.py +++ b/setup.py @@ -65,6 +65,7 @@ def get_author(): "requests>=2.25.0", "flashlight-text", "librosa", + "sounddevice>=0.4.0", ], extras_require={ "dev": [ diff --git a/src/__init__.py b/src/__init__.py index c0d7e06..8f49527 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -4,7 +4,7 @@ A simple and efficient library for real-time Vietnamese speech recognition. """ -__version__ = "0.1.3" +__version__ = "0.1.4" __author__ = "ViStreamASR Team" # Handle imports for both installed package and development mode diff --git a/src/cli.py b/src/cli.py index fd8b77c..b29d5bc 100644 --- a/src/cli.py +++ b/src/cli.py @@ -38,6 +38,8 @@ 'brain': '๐Ÿง ' if sys.stdout.encoding and 'utf' in sys.stdout.encoding.lower() else '[MODEL]', 'rocket': '๐Ÿš€' if sys.stdout.encoding and 'utf' in sys.stdout.encoding.lower() else '[GPU]', 'stopwatch': 'โฑ๏ธ' if sys.stdout.encoding and 'utf' in sys.stdout.encoding.lower() else '[TIME]', + 'speaker': '๐Ÿ”Š' if sys.stdout.encoding and 'utf' in sys.stdout.encoding.lower() else '[SPEAKER]', + 'stop': 'โน๏ธ' if sys.stdout.encoding and 'utf' in sys.stdout.encoding.lower() else '[STOP]', } # Handle import for both installed package and development mode @@ -47,7 +49,7 @@ from streaming import StreamingASR -def transcribe_file_streaming(audio_file, chunk_size_ms=640, auto_finalize_after=15.0, debug=True): +def transcribe_file_streaming(audio_file, chunk_size_ms=640, auto_finalize_after=15.0, debug=False): """ Transcribe an audio file using streaming ASR. @@ -142,6 +144,121 @@ def transcribe_file_streaming(audio_file, chunk_size_ms=640, auto_finalize_after return 0 +def transcribe_microphone_streaming(duration_seconds=None, chunk_size_ms=640, auto_finalize_after=15.0, debug=False): + """ + Transcribe from microphone using streaming ASR. + + Args: + duration_seconds: Maximum duration to record (None for infinite) + chunk_size_ms: Chunk size in milliseconds + auto_finalize_after: Maximum duration before auto-finalization (seconds) + debug: Enable debug logging + """ + print(f"{symbols['mic']} ViStreamASR Microphone Transcription") + print(f"=" * 50) + print(f"{symbols['speaker']} Recording from microphone") + print(f"{symbols['ruler']} Chunk size: {chunk_size_ms}ms") + print(f"{symbols['clock']} Auto-finalize after: {auto_finalize_after}s") + if duration_seconds: + print(f"{symbols['stopwatch']} Duration: {duration_seconds}s") + else: + print(f"{symbols['stopwatch']} Duration: Unlimited (Press Ctrl+C to stop)") + print(f"{symbols['tool']} Debug mode: {debug}") + print() + + # Check if microphone is available + try: + import sounddevice as sd + devices = sd.query_devices() + input_devices = [d for d in devices if d['max_input_channels'] > 0] + if not input_devices: + print(f"โŒ Error: No microphone devices found") + return 1 + print(f"{symbols['check']} Found {len(input_devices)} microphone device(s)") + except ImportError: + print(f"โŒ Error: sounddevice library not installed. Install with: pip install sounddevice") + return 1 + except Exception as e: + print(f"โŒ Error checking microphone: {e}") + return 1 + + # Initialize StreamingASR + print(f"๐Ÿ”„ Initializing ViStreamASR...") + asr = StreamingASR( + chunk_size_ms=chunk_size_ms, + auto_finalize_after=auto_finalize_after, + debug=debug + ) + + # Collect results + final_segments = [] + current_partial = "" + + # Start streaming + print(f"\n๐ŸŽค Starting microphone streaming...") + print(f"{symbols['speaker']} Please speak into your microphone...") + print(f"=" * 60) + + start_time = time.time() + + try: + for result in asr.stream_from_microphone(duration_seconds=duration_seconds): + chunk_info = result.get('chunk_info', {}) + + if result.get('partial') and result.get('text'): + current_partial = result['text'] + print(f"{symbols['memo']} [PARTIAL {chunk_info.get('chunk_id', '?'):3d}] {current_partial}") + + if result.get('final') and result.get('text'): + final_text = result['text'] + final_segments.append(final_text) + current_partial = "" + print(f"{symbols['check']} [FINAL {chunk_info.get('chunk_id', '?'):3d}] {final_text}") + print(f"-" * 60) + + except KeyboardInterrupt: + print(f"\n{symbols['stop']} Microphone streaming stopped by user") + return 0 + except Exception as e: + print(f"\nโŒ Error during microphone streaming: {e}") + return 1 + + # Final results + end_time = time.time() + total_time = end_time - start_time + + print(f"\n๐Ÿ“Š MICROPHONE TRANSCRIPTION RESULTS") + print(f"=" * 50) + print(f"{symbols['stopwatch']} Recording time: {total_time:.2f} seconds") + print(f"{symbols['memo']} Final segments: {len(final_segments)}") + + if final_segments: + print(f"\n{symbols['memo']} Complete Transcription:") + print(f"=" * 60) + complete_transcription = " ".join(final_segments) + # Wrap text at 80 characters for better readability + words = complete_transcription.split() + lines = [] + current_line = "" + for word in words: + if len(current_line + " " + word) <= 80: + current_line += (" " if current_line else "") + word + else: + if current_line: + lines.append(current_line) + current_line = word + if current_line: + lines.append(current_line) + + for line in lines: + print(line) + else: + print(f"\n{symbols['memo']} No speech detected or transcribed during recording") + + print(f"\n{symbols['check']} Microphone transcription completed successfully!") + return 0 + + def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( @@ -149,18 +266,20 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - %(prog)s transcribe audio.wav # Basic transcription with default settings + %(prog)s transcribe audio.wav # Basic file transcription %(prog)s transcribe audio.wav --chunk-size 640 # Use 640ms chunks - %(prog)s transcribe audio.wav --no-debug # Disable debug logging - %(prog)s transcribe audio.wav --auto-finalize-after 15 # Auto-finalize after 20 seconds - %(prog)s transcribe audio.wav --chunk-size 640 --no-debug # High-level view only + %(prog)s transcribe audio.wav --show-debug # Enable debug logging + + %(prog)s microphone # Record from microphone indefinitely + %(prog)s microphone --duration 30 # Record for 30 seconds + %(prog)s microphone --chunk-size 500 --show-debug # Custom settings with debug """ ) # Subcommands subparsers = parser.add_subparsers(dest='command', help='Available commands') - # Transcribe command (renamed from demo) + # Transcribe command (for audio files) transcribe_parser = subparsers.add_parser( 'transcribe', help='Transcribe audio file using streaming ASR', @@ -183,9 +302,40 @@ def main(): help='Maximum duration in seconds before auto-finalizing a segment (default: 15.0s)' ) transcribe_parser.add_argument( - '--no-debug', + '--show-debug', + action='store_true', + help='Enable debug logging (show detailed processing information)' + ) + + # Microphone command + microphone_parser = subparsers.add_parser( + 'microphone', + aliases=['mic'], + help='Transcribe from microphone using streaming ASR', + description='Record from microphone and show real-time transcription results' + ) + microphone_parser.add_argument( + '--duration', + type=float, + default=None, + help='Maximum duration to record in seconds (default: unlimited, press Ctrl+C to stop)' + ) + microphone_parser.add_argument( + '--chunk-size', + type=int, + default=640, + help='Chunk size in milliseconds (default: 640ms for optimal performance)' + ) + microphone_parser.add_argument( + '--auto-finalize-after', + type=float, + default=15.0, + help='Maximum duration in seconds before auto-finalizing a segment (default: 15.0s)' + ) + microphone_parser.add_argument( + '--show-debug', action='store_true', - help='Disable debug logging (show only transcription results)' + help='Enable debug logging (show detailed processing information)' ) # Info command @@ -203,12 +353,19 @@ def main(): args = parser.parse_args() if args.command == 'transcribe': - debug_mode = not args.no_debug return transcribe_file_streaming( args.audio_file, chunk_size_ms=args.chunk_size, auto_finalize_after=args.auto_finalize_after, - debug=debug_mode + debug=args.show_debug + ) + + elif args.command == 'microphone': + return transcribe_microphone_streaming( + duration_seconds=args.duration, + chunk_size_ms=args.chunk_size, + auto_finalize_after=args.auto_finalize_after, + debug=args.show_debug ) elif args.command == 'info': @@ -225,7 +382,11 @@ def main(): print(f" vistream-asr transcribe audio.wav") print(f" vistream-asr transcribe audio.wav --chunk-size 500") print(f" vistream-asr transcribe audio.wav --auto-finalize-after 20") - print(f" vistream-asr transcribe audio.wav --no-debug") + print(f" vistream-asr transcribe audio.wav --show-debug") + print(f" vistream-asr microphone") + print(f" vistream-asr microphone --duration 30") + print(f" vistream-asr microphone --chunk-size 500") + print(f" vistream-asr microphone --show-debug") # Check if model is cached try: diff --git a/src/streaming.py b/src/streaming.py index 450acf3..2c3120f 100644 --- a/src/streaming.py +++ b/src/streaming.py @@ -13,6 +13,7 @@ from pathlib import Path import sys import numpy as np +import sounddevice as sd # Handle imports for both installed package and development mode try: @@ -186,25 +187,81 @@ def stream_from_file(self, audio_file: str, chunk_size_ms: Optional[int] = None) print(f"{symbols['ruler']} Total time: {total_time:.2f}s") print(f"{symbols['check']} ๏ฟฝ๏ฟฝ RTF: {rtf:.2f}x") print(f"{symbols['check']} โšก Speedup: {duration/total_time:.1f}x") - + def stream_from_microphone(self, duration_seconds: Optional[float] = None) -> Generator[Dict[str, Any], None, None]: """ Stream ASR results from microphone input. - + Args: duration_seconds: Maximum duration to record (None for infinite) - + Yields: dict: Same format as stream_from_file() - - Note: - This is a placeholder - real microphone streaming would require - additional audio capture libraries like sounddevice or pyaudio. """ - raise NotImplementedError( - "Microphone streaming not implemented yet. " - "This would require additional audio capture dependencies." - ) + self._ensure_engine_initialized() + samplerate = 16000 + chunk_size = self.chunk_size_ms + chunk_size_samples = int(samplerate * chunk_size / 1000.0) + if self.debug: + print( + f"{symbols['wave']} [StreamingASR] Starting microphone stream at {samplerate}Hz, chunk size: {chunk_size}ms ({chunk_size_samples} samples)") + self.engine.reset_state() + start_time = time.time() + buffer = np.zeros((0,), dtype=np.float32) + chunk_id = 0 + + def callback(indata, frames, time_info, status): + nonlocal buffer + if status: + print(status, file=sys.stderr) + buffer = np.concatenate((buffer, indata[:, 0])) + + with sd.InputStream(samplerate=samplerate, channels=1, dtype='float32', callback=callback): + while True: + if duration_seconds is not None and (time.time() - start_time) > duration_seconds: + is_last = True + else: + is_last = False + if len(buffer) >= chunk_size_samples or is_last: + chunk = buffer[:chunk_size_samples] + buffer = buffer[chunk_size_samples:] + if len(chunk) == 0: + if is_last: + break + else: + time.sleep(0.01) + continue + chunk_id += 1 + if self.debug: + print( + f"{symbols['tool']} [StreamingASR] Processing mic chunk {chunk_id} ({len(chunk)} samples)") + result = self.engine.process_audio(chunk, is_last=is_last) + chunk_info = { + 'chunk_id': chunk_id, + 'samples': len(chunk), + 'duration_ms': len(chunk) / samplerate * 1000, + 'is_last': is_last + } + if result.get('current_transcription'): + yield { + 'partial': True, + 'final': False, + 'text': result['current_transcription'], + 'chunk_info': chunk_info + } + if result.get('new_final_text'): + yield { + 'partial': False, + 'final': True, + 'text': result['new_final_text'], + 'chunk_info': chunk_info + } + if is_last: + break + else: + time.sleep(0.01) + if self.debug: + print(f"{symbols['check']} [StreamingASR] Microphone streaming complete.") def _load_audio_file(self, audio_file: str) -> Optional[Dict[str, Any]]: """Load and prepare audio file for ASR processing."""