133 lines
3.7 KiB
Python
133 lines
3.7 KiB
Python
#!/usr/bin/env python
|
|
"""Profile hot paths in NoteFlow audio processing.
|
|
|
|
Run with: python scripts/profile_hot_paths.py
|
|
Output: Profile statistics to stdout, sorted by cumulative time.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import cProfile
|
|
import io
|
|
import pstats
|
|
|
|
import numpy as np
|
|
from numpy.typing import NDArray
|
|
|
|
from noteflow.config.constants import DEFAULT_SAMPLE_RATE
|
|
from noteflow.infrastructure.asr.segmenter import Segmenter, SegmenterConfig
|
|
from noteflow.infrastructure.asr.streaming_vad import StreamingVad
|
|
from noteflow.infrastructure.audio.levels import RmsLevelProvider
|
|
|
|
# Simulation parameters
|
|
SAMPLE_RATE = DEFAULT_SAMPLE_RATE
|
|
CHUNK_SIZE = 1600 # 100ms at 16kHz
|
|
SIMULATION_SECONDS = 60 # Simulate 1 minute of audio
|
|
CHUNKS_PER_SECOND = SAMPLE_RATE // CHUNK_SIZE
|
|
|
|
|
|
AudioChunk = NDArray[np.float32]
|
|
|
|
|
|
def generate_audio_stream(seconds: int) -> list[AudioChunk]:
|
|
"""Generate simulated audio chunks (alternating speech/silence)."""
|
|
chunks: list[AudioChunk] = []
|
|
total_chunks = seconds * CHUNKS_PER_SECOND
|
|
|
|
for i in range(total_chunks):
|
|
# Simulate speech/silence pattern (5s speech, 2s silence)
|
|
if (i // CHUNKS_PER_SECOND) % 7 < 5:
|
|
# Speech - higher amplitude
|
|
chunk = np.random.randn(CHUNK_SIZE).astype(np.float32) * 0.3
|
|
else:
|
|
# Silence - low amplitude
|
|
chunk = np.random.randn(CHUNK_SIZE).astype(np.float32) * 0.001
|
|
chunks.append(chunk)
|
|
|
|
return chunks
|
|
|
|
|
|
def process_stream(chunks: list[AudioChunk]) -> dict[str, int]:
|
|
"""Process audio stream through VAD + Segmenter pipeline."""
|
|
vad = StreamingVad()
|
|
segmenter = Segmenter(config=SegmenterConfig(sample_rate=SAMPLE_RATE))
|
|
rms_provider = RmsLevelProvider()
|
|
|
|
segments_emitted = 0
|
|
speech_chunks = 0
|
|
silence_chunks = 0
|
|
|
|
for chunk in chunks:
|
|
# VAD processing
|
|
is_speech = vad.process_chunk(chunk)
|
|
|
|
# RMS computation (for VU meter)
|
|
_ = rms_provider.get_rms(chunk)
|
|
_ = rms_provider.get_db(chunk)
|
|
|
|
# Segmenter processing
|
|
for _segment in segmenter.process_audio(chunk, is_speech):
|
|
segments_emitted += 1
|
|
|
|
if is_speech:
|
|
speech_chunks += 1
|
|
else:
|
|
silence_chunks += 1
|
|
|
|
# Flush remaining audio
|
|
final_segment = segmenter.flush()
|
|
if final_segment is not None:
|
|
segments_emitted += 1
|
|
|
|
return {
|
|
"segments_emitted": segments_emitted,
|
|
"speech_chunks": speech_chunks,
|
|
"silence_chunks": silence_chunks,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
"""Run profiling and output statistics."""
|
|
print(f"Generating {SIMULATION_SECONDS}s of simulated audio...")
|
|
chunks = generate_audio_stream(SIMULATION_SECONDS)
|
|
print(f"Generated {len(chunks)} chunks ({len(chunks) / CHUNKS_PER_SECOND:.1f}s)")
|
|
|
|
print("\nProfiling audio processing pipeline...")
|
|
profiler = cProfile.Profile()
|
|
profiler.enable()
|
|
|
|
result = process_stream(chunks)
|
|
|
|
profiler.disable()
|
|
|
|
print(f"\nResults: {result}")
|
|
print("\n" + "=" * 80)
|
|
print("PROFILE RESULTS (sorted by cumulative time)")
|
|
print("=" * 80)
|
|
|
|
# Output profile stats
|
|
stream = io.StringIO()
|
|
stats = pstats.Stats(profiler, stream=stream)
|
|
stats.strip_dirs()
|
|
stats.sort_stats(pstats.SortKey.CUMULATIVE)
|
|
stats.print_stats(50) # Top 50 functions
|
|
|
|
print(stream.getvalue())
|
|
|
|
# Also show by tottime
|
|
print("\n" + "=" * 80)
|
|
print("PROFILE RESULTS (sorted by total time in function)")
|
|
print("=" * 80)
|
|
|
|
stream2 = io.StringIO()
|
|
stats2 = pstats.Stats(profiler, stream=stream2)
|
|
stats2.strip_dirs()
|
|
stats2.sort_stats(pstats.SortKey.TIME)
|
|
stats2.print_stats(30) # Top 30 functions
|
|
|
|
print(stream2.getvalue())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|