XEFQ73KDMQJ5YP5UBGZM3Z2GZUVRCVWMTIADZQSUIJKVDTBODP4AC 4V4GDB73V3NYTXD7XKIQWFWRASZ6SZS66CIOJCO7Z2R7TME2K5QQC 22FIUBOFKBIV3WINZUSBKJO526K52HQV3V33B3DRS6CQWHU5HFPAC 3JA7HYRMHV57SIMGMGPDOMKQ3NBQS2SKOX3EKDHRBQRP7ZPZGFTQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC FRY33K6EGWLU3F5NJJ66AT5RBV6OEOWSDKY3JH2CPKKGPAHOMM6AC OFL26ZLGKGJERNHPURO6CBN4FT3VN5MLASOAPXO6GJVCMSMCLPUAC GQNMVJQBC6DRV5XGK3K5L7YWG2GJUXR7EQE3OHNW72XK6BFY3AHQC E56RSY75LHUPKXPOPPGFDHPSACMRWPHOAD2GREJN3OFPA6QFM64QC 5KIKDA72HM6JFIPKOWGLM2EO7D5PTSK7WEVYV3YZWGMG3M34PJXQC TLLVARZXOP2M3B5VTLF4SYDGMIBPHABE6LJFG77IU53QTSYGTKWAC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC // GenerateSegmentSpectrogram generates a spectrogram image for a time segment.// Handles WAV loading, downsampling, and image creation.// color=true applies L4 colormap, color=false creates grayscale.// imgSize specifies the output image dimensions (clamped to [224, 896]).func GenerateSegmentSpectrogram(dataFilePath string, startTime, endTime float64, color bool, imgSize int) (image.Image, error) {// Derive WAV file path (strip .data suffix)wavPath := strings.TrimSuffix(dataFilePath, ".data")// Read WAV samplessamples, sampleRate, err := ReadWAVSamples(wavPath)if err != nil {return nil, err}// Extract segment samplessegSamples := ExtractSegmentSamples(samples, sampleRate, startTime, endTime)if len(segSamples) == 0 {return nil, nil}// For spectrograms, downsample if sample rate exceeds 16kHzspectSampleRate := sampleRateif sampleRate > DefaultMaxSampleRate {segSamples = ResampleRate(segSamples, sampleRate, DefaultMaxSampleRate)spectSampleRate = DefaultMaxSampleRate}// Generate spectrogramconfig := DefaultSpectrogramConfig(spectSampleRate)spectrogram := GenerateSpectrogram(segSamples, config)if spectrogram == nil {return nil, nil}// Create image (grayscale or color)var img image.Imageif color {colorData := ApplyL4Colormap(spectrogram)img = CreateRGBImage(colorData)} else {img = CreateGrayscaleImage(spectrogram)}if img == nil {return nil, nil}// ResizeimgSize = ClampImageSize(imgSize)return ResizeImage(img, imgSize, imgSize), nil}
wavPath := strings.TrimSuffix(dataPath, ".data")samples, sampleRate, err := utils.ReadWAVSamples(wavPath)if err != nil {return nil}segSamples := utils.ExtractSegmentSamples(samples, sampleRate, seg.StartTime, seg.EndTime)if len(segSamples) == 0 {return nil}// For spectrograms, downsample if sample rate exceeds 16kHzspectSampleRate := sampleRateif sampleRate > utils.DefaultMaxSampleRate {segSamples = utils.ResampleRate(segSamples, sampleRate, utils.DefaultMaxSampleRate)spectSampleRate = utils.DefaultMaxSampleRate}config := utils.DefaultSpectrogramConfig(spectSampleRate)spectrogram := utils.GenerateSpectrogram(segSamples, config)if spectrogram == nil {return nil}var img image.Imageif state.Config.Color {colorData := utils.ApplyL4Colormap(spectrogram)img = utils.CreateRGBImage(colorData)} else {img = utils.CreateGrayscaleImage(spectrogram)}if img == nil {return nil}
imgSize = utils.ClampImageSize(imgSize)return utils.ResizeImage(img, imgSize, imgSize)
img, err := utils.GenerateSegmentSpectrogram(dataPath, seg.StartTime, seg.EndTime, state.Config.Color, imgSize)if err != nil {return nil}return img
config := utils.DefaultSpectrogramConfig(spectSampleRate)for i, segment := range segments {// Extract samples for this segment's time rangesegmentSamples := utils.ExtractSegmentSamples(samples, sampleRate, segment.StartTime, segment.EndTime)if len(segmentSamples) == 0 {continue}// Downsample for spectrogram if neededif sampleRate > utils.DefaultMaxSampleRate {segmentSamples = utils.ResampleRate(segmentSamples, sampleRate, utils.DefaultMaxSampleRate)}// Generate spectrogramspectrogram := utils.GenerateSpectrogram(segmentSamples, config)if spectrogram == nil {continue}// Print segment time info and spectrogram dimensionsfmt.Fprintf(os.Stderr, "Segment %d: %.1fs - %.1fs (%.1fs) spectrogram: %dx%d\n", i+1, segment.StartTime, segment.EndTime, segment.EndTime-segment.StartTime, len(spectrogram[0]), len(spectrogram))// Create image (grayscale or color)var img image.Imageif input.Color {// Apply L4 colormapcolorData := utils.ApplyL4Colormap(spectrogram)img = utils.CreateRGBImage(colorData)} else {img = utils.CreateGrayscaleImage(spectrogram)}if img == nil {
for i, seg := range dataFile.Segments {// Generate spectrogram imageimg, err := utils.GenerateSegmentSpectrogram(input.DataFilePath, seg.StartTime, seg.EndTime, input.Color, imgSize)if err != nil || img == nil {
resized := utils.ResizeImage(img, imgSize, imgSize)
// Print segment infolabelInfo := formatSegmentLabels(seg.Labels)fmt.Fprintf(os.Stderr, "Segment %d: %.1fs - %.1fs (%.1fs)%s\n",i+1, seg.StartTime, seg.EndTime, seg.EndTime-seg.StartTime, labelInfo)
// Parse JSON arrayvar data []anydecoder := json.NewDecoder(file)if err := decoder.Decode(&data); err != nil {return nil, fmt.Errorf("failed to parse .data file: %v", err)
// formatSegmentLabels formats labels for display in segment infofunc formatSegmentLabels(labels []*utils.Label) string {if len(labels) == 0 {return ""
// First element is metadata, rest are segmentsif len(data) < 2 {return nil, nil}var segments []Segmentfor i := 1; i < len(data); i++ {seg, ok := data[i].([]any)if !ok || len(seg) < 5 {continue
var parts []stringfor _, l := range labels {part := l.Speciesif l.CallType != "" {part += "/" + l.CallType
// Parse segment: [start_time, end_time, freq_low, freq_high, labels]startTime, ok1 := seg[0].(float64)endTime, ok2 := seg[1].(float64)freqLow, ok3 := seg[2].(float64)freqHigh, ok4 := seg[3].(float64)if !ok1 || !ok2 || !ok3 || !ok4 {continue
if l.Filter != "" {part += " [" + l.Filter + "]"
## [2026-04-02] Shared spectrogram generation for show-images and classifyRefactored spectrogram image generation into a shared utility function, reducingduplication between `calls show-images` and `calls classify` TUI.**New utility:**- `utils.GenerateSegmentSpectrogram(dataFilePath, startTime, endTime, color, imgSize)` -generates a spectrogram image from a segment, handling WAV loading, downsampling,and image creation in one call.**Changes:**- `utils/spectrogram.go` — Added `GenerateSegmentSpectrogram()` function- `tools/calls_show_images.go` — Now uses `utils.ParseDataFile()` (includes labels) and`GenerateSegmentSpectrogram()`; removed local `Segment` struct and `parseDataFile()`;segment info now shows labels when present- `tui/classify.go` — `generateSpectrogramImage()` now delegates to shared function