7HRFWKAFNNCCK2GMGHUL6G7KUEQP6TPBSMZEILIY5ELWU4LSUKOAC 4TTJFC6RH47OU4XPTI4C45XZMTAQU2YQPMOFWCE664PJ3GE6HHZQC 7CC2YVZXAIUNWXNNVIO5KOZZFDQQLESFO72SGEDP2C4OZXAWO4KQC Y5RSXHAZFGNPQ26DRXTCHJV4Y4EZLEGFC2FBRBI5MCMOOYCB7CMAC IWA42FYL3SDWJKVE4YOFDOERH5YZ2BHNK3JRQCQMKCZ2R2O7X56QC 4DKGEM6MTXFJ4LET56PNJR7KPDDTALHWKX7EMXXQHCRE7SBMYHIAC XMW7BCZH73KYOJ2H2ZDQQU5NQMBKMX7TWB5HNN5PAB6RYLTWQ5VQC IEFQRPS3TO3XEPPF4BTZO7UGKAAYSGQFEBQTL4SIIRNLHHL4JD6AC func BenchmarkExtractSegment(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(testWAV)b.Logf("full file: %d samples, sr=%d", len(samples), sr)
// Duplicate of convertToFloat64 for benchmarking (unexported in utils)func convertToFloat64Bench(data []byte, bitsPerSample, channels int) []float64 {bytesPerSample := bitsPerSample / 8blockAlign := bytesPerSample * channelsnumSamples := len(data) / blockAlignsamples := make([]float64, numSamples)for i := range numSamples {offset := i * blockAlignsample := int16(binary.LittleEndian.Uint16(data[offset : offset+2]))samples[i] = float64(sample) / 32768.0}return samples}func BenchmarkWriteWAV(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(benchWAV)segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)b.Logf("segment samples=%d", len(segSamples))
func BenchmarkResampleRate(b *testing.B) {samples, _, _ := utils.ReadWAVSamples(testWAV)// Simulate 48kHz source by treating the 16kHz data as 48kHzb.Logf("resampling %d samples from 48000->16000", len(samples))
// ==================== Resample ====================func BenchmarkResampleRate_48k(b *testing.B) {samples, _, _ := utils.ReadWAVSamples(benchWAV)b.Logf("resampling %d samples 48000->16000", len(samples))
func BenchmarkResampleRate250k(b *testing.B) {samples, _, _ := utils.ReadWAVSamples(testWAV)// Simulate 250kHz source (AudioMoth high-gain)b.Logf("resampling %d samples from 250000->16000", len(samples))
func BenchmarkResampleRate_250k(b *testing.B) {samples, _, _ := utils.ReadWAVSamples(benchWAV)b.Logf("resampling %d samples 250000->16000", len(samples))
func BenchmarkSpectrogram(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(testWAV)
func BenchmarkExtractSegment(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(benchWAV)b.Logf("full file: %d samples, sr=%d", len(samples), sr)b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {seg := utils.ExtractSegmentSamples(samples, sr, 872, 895)if len(seg) == 0 {b.Fatal("empty segment")}}}func BenchmarkPowerSpectrumFFT_512(b *testing.B) {n := 512samples, sr, _ := utils.ReadWAVSamples(benchWAV)segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)frameData := make([]float64, n)power := make([]float64, n/2+1)scratch := make([]complex128, n)b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {// Simulate the windowing step (Hann) + FFTfor j := 0; j < n; j++ {frameData[j] = segSamples[j] * 0.5 * (1.0 - math.Cos(2.0*math.Pi*float64(j)/float64(n-1)))}utils.PowerSpectrumFFT(frameData, power, scratch)}}func BenchmarkSpectrogram_23s(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(benchWAV)
func BenchmarkApplyL4Colormap(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(benchWAV)segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)cfg := utils.DefaultSpectrogramConfig(16000)spect := utils.GenerateSpectrogram(segSamples, cfg)b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {colorData := utils.ApplyL4Colormap(spect)if colorData == nil {b.Fatal("nil colormap")}}}
f.Close()os.Remove(f.Name())}}func BenchmarkWriteWAV(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(testWAV)segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)b.Logf("segment samples=%d", len(segSamples))b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {f, _ := os.CreateTemp("", "bench_*.wav")utils.WriteWAVFile(f.Name(), segSamples, sr)
}}// ========== Raw FFT benchmark (to isolate FFT cost from spectrogram overhead) ==========func BenchmarkFFTRaw(b *testing.B) {const windowSize = 512hann := window.Hann(windowSize)samples, sr, _ := utils.ReadWAVSamples(testWAV)segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)numFrames := (len(segSamples)-windowSize)/256 + 1b.Logf("frames=%d", numFrames)frameData := make([]float64, windowSize)b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {for frame := 0; frame < numFrames; frame++ {start := frame * 256for j := 0; j < windowSize; j++ {frameData[j] = segSamples[start+j] * hann[j]}fft.FFTReal(frameData)}}}// ========== convertToFloat64 isolated benchmark ==========func BenchmarkConvertToFloat64_16bit(b *testing.B) {// Simulate 16-bit mono WAV data (same size as test file)numSamples := 14320000data := make([]byte, numSamples*2)// Fill with dummy datafor i := range numSamples {binary.LittleEndian.PutUint16(data[i*2:], uint16(i%65536))}b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {_ = convertToFloat64Bench(data, 16, 1)}}// Duplicate of convertToFloat64 for benchmarking (unexported in utils)func convertToFloat64Bench(data []byte, bitsPerSample, channels int) []float64 {bytesPerSample := bitsPerSample / 8blockAlign := bytesPerSample * channelsnumSamples := len(data) / blockAlignsamples := make([]float64, numSamples)for i := range numSamples {offset := i * blockAlignsample := int16(binary.LittleEndian.Uint16(data[offset : offset+2]))samples[i] = float64(sample) / 32768.0
// ========== Allocation profiling: measure allocs per stage ==========
func BenchmarkFullPipelineWavOnly(b *testing.B) {samples, sr, _ := utils.ReadWAVSamples(benchWAV)b.ResetTimer()b.ReportAllocs()for i := 0; i < b.N; i++ {segSamples := utils.ExtractSegmentSamples(samples, sr, 872, 895)outputSR := srif sr > 16000 {segSamples = utils.ResampleRate(segSamples, sr, 16000)outputSR = 16000}f, _ := os.CreateTemp("", "bench_*.wav")utils.WriteWAVFile(f.Name(), segSamples, outputSR)f.Close()os.Remove(f.Name())}}// ==================== Data dimension report ====================
spect := utils.GenerateSpectrogram(segSamples, cfg)t.Logf("Spectrogram: %d x %d (freq x time)", len(spect), len(spect[0]))
numFrames := (len(segSamples)-cfg.WindowSize)/cfg.HopSize + 1numBins := cfg.WindowSize/2 + 1t.Logf("Spectrogram: %d freq bins x %d time frames = %d values",numBins, numFrames, numBins*numFrames)
// ========== Math benchmarks: isolate costly operations ==========func BenchmarkMathLog10(b *testing.B) {vals := make([]float64, 10000)for i := range vals {vals[i] = float64(i+1) * 0.001}b.ResetTimer()for i := 0; i < b.N; i++ {for _, v := range vals {math.Log10(v)}}
resized448 := utils.ResizeImage(img, 448, 448)t.Logf("Resized 448: %dx%d", resized448.Bounds().Dx(), resized448.Bounds().Dy())
skraak calls classify --folder . --reviewer David --color --filter opensoundscape-multi-1.0 --species lotkoe1 \--bind a=eurbla, \--bind b=nezbel1, \--bind c=comcha, \--bind d=saddle3, \--bind e=pipipi1, \--bind f=nezfan1, \--bind g=gryger1, \--bind i=tui1, \--bind j=nezkak1, \--bind k=kea1, \--bind l=lotkoe1, \--bind m=morepo2, \--bind n=nezrob3, \--bind o=soioys1, \--bind p=malpar2, \--bind r=riflem1, \--bind s=silver3, \--bind t=tomtit1, \--bind u=nezpig2, \--bind v=brncre, \--bind w=weka1, \--bind x=Noise, \--bind z="Don't Know", \--bind 1=Kiwi+Duet, \--bind 2=Kiwi+Female, \--bind 3=Kiwi+Male, \--bind 4=Kiwi, \
skraak calls classify --folder A05/2026-04-06 --reviewer David --color --filter opensoundscape-kiwi-1.2 --species Kiwi+Male \--bind a=eurbla \--bind b=nezbel1 \--bind c=comcha \--bind d=saddle3 \--bind e=pipipi1 \--bind f=nezfan1 \--bind g=gryger1 \--bind i=tui1 \--bind j=nezkak1 \--bind k=kea1 \--bind l=lotkoe1 \--bind m=morepo2 \--bind n=nezrob3 \--bind o=soioys1 \--bind p=malpar2 \--bind r=riflem1 \--bind s=silver3 \--bind t=tomtit1 \--bind u=nezpig2 \--bind v=brncre \--bind w=weka1 \--bind x=Noise \--bind z="Don't Know" \--bind 1=Kiwi+Duet \--bind 2=Kiwi+Female \--bind 3=Kiwi+Male \--bind 4=Kiwi \
## Step 7: Update CSV and Verify
## Step 7: Set Cyclic Recording Pattern (AudioMoth Only)If the data is from an **AudioMoth**, every cluster **must** have a `cyclic_recording_pattern_id` set.### Detecting AudioMoth DataAudioMoth filenames follow the pattern `YYYYMMDD_HHMMSS.wav` (e.g. `20250422_173008.wav`) or contain an AudioMoth prefix (e.g. `AUDIOMOTH_...`). The log.txt file also identifies the device. When in doubt, ask the user.### Determine the Pattern from File IntervalsCompute record_s and sleep_s from the files:```python# record_s = file duration (from WAV header or duration field)# sleep_s = gap between end of one file and start of next# gap = next_start - (this_start + duration)# cycle = record_s + sleep_s (should be constant, e.g. 900s, 1800s, 2400s)# Sample a few consecutive files to confirm the pattern is consistent```### Always Use an Existing PatternQuery existing patterns first — only create a new one if none match:```pythoncmd = ["./skraak", "sql", "--db", db_path,"SELECT id, record_s, sleep_s FROM cyclic_recording_pattern WHERE active = true"]```Match by `record_s` AND `sleep_s`. If a match exists, use that ID. If no match, create a new pattern:```pythoncmd = ["./skraak", "create", "cyclic-recording-pattern","--db", db_path,"--record", str(record_s),"--sleep", str(sleep_s)]```### Set Pattern on Every ClusterAfter clusters are created (via bulk import), set the pattern on each:```pythoncmd = ["./skraak", "update", "cluster","--db", db_path,"--id", cluster_id,"--cyclic-recording-pattern", pattern_id]```
Or use the MCP tool `create_or_update_cluster` with `cyclic_recording_pattern_id`.Include this step in the confirmation summary shown to the user before execution:```PLAN READY - AWAITING CONFIRMATION===================================...Cyclic recording pattern: 895s record / 5s sleep (ID: c-l6D6TPlRVO) — will be set on all clusters```## Step 8: Update CSV and Verify
✅ Cyclic recording pattern set on all clusters (AudioMoth data only)