Files
noteflow/scripts/profile_hot_paths.py
2026-01-02 10:11:45 +00:00

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()