refactor of tui/ second iteration

quietlight
May 18, 2026, 8:53 PM
3ETJ6KPIYI23DLXSKISNJSY3DUGHOACE6CPCPF6V7KJK4EXIADQAC

Dependencies

  • [2] YVFPP5VJ refactor of tui/ first iteration
  • [3] NUOFNUIQ simplified --bandpass
  • [4] KZKLAINJ run out of space on nest, cleaned out
  • [5] 3DVPQOKB big tidy up of tools/
  • [6] P4CJMBYK added first version of --bandpass flag to calls classify, work to do
  • [*] LBWQJEDH minor refactor and more tests for utils/

Change contents

  • edit in utils/spectrogram_test.go at line 4
    [8.818]
    [8.818]
    "os"
    "path/filepath"
  • edit in utils/spectrogram_test.go at line 52
    [8.2112]
    [8.2112]
    }
    }
    func TestClipBaseName(t *testing.T) {
    tests := []struct {
    prefix string
    basename string
    startTime float64
    endTime float64
    want string
    }{
    {"clip", "file", 1.0, 3.0, "clip_file_1_3"},
    {"test", "recording", 0.0, 5.5, "test_recording_0_6"}, // ceil(5.5) = 6
    {"a", "b", 2.7, 4.2, "a_b_2_5"}, // floor(2.7)=2, ceil(4.2)=5
    }
    for _, tt := range tests {
    got := ClipBaseName(tt.prefix, tt.basename, tt.startTime, tt.endTime)
    if got != tt.want {
    t.Errorf("ClipBaseName(%q, %q, %.1f, %.1f) = %q, want %q",
    tt.prefix, tt.basename, tt.startTime, tt.endTime, got, tt.want)
    }
    }
    }
    func TestWAVBasename(t *testing.T) {
    tests := []struct {
    path string
    want string
    }{
    {"/path/to/file.wav.data", "file"},
    {"/audio/2024-01-15_recording.wav.data", "2024-01-15_recording"},
    {"simple.wav.data", "simple"},
    }
    for _, tt := range tests {
    got := WAVBasename(tt.path)
    if got != tt.want {
    t.Errorf("WAVBasename(%q) = %q, want %q", tt.path, got, tt.want)
    }
    }
    }
    func TestClipPaths(t *testing.T) {
    tmp := t.TempDir()
    // Normal case
    pngPath, wavPath, err := ClipPaths(tmp, "clip", "file", 1.0, 3.0)
    if err != nil {
    t.Fatalf("unexpected error: %v", err)
    }
    expectedPng := filepath.Join(tmp, "clip_file_1_3.png")
    expectedWav := filepath.Join(tmp, "clip_file_1_3.wav")
    if pngPath != expectedPng {
    t.Errorf("pngPath = %q, want %q", pngPath, expectedPng)
    }
    if wavPath != expectedWav {
    t.Errorf("wavPath = %q, want %q", wavPath, expectedWav)
    }
    // Collision detection
    os.Create(expectedPng)
    _, _, err = ClipPaths(tmp, "clip", "file", 1.0, 3.0)
    if err == nil {
    t.Error("expected error for existing file")
    }
    }
    func TestWritePNGFile(t *testing.T) {
    tmp := t.TempDir()
    // Create a simple test image (grayscale 2x2)
    gray := [][]uint8{{128, 64}, {32, 16}}
    img := CreateGrayscaleImage(gray)
    if img == nil {
    t.Fatal("failed to create test image")
    }
    path := filepath.Join(tmp, "test.png")
    if err := WritePNGFile(path, img); err != nil {
    t.Fatalf("WritePNGFile failed: %v", err)
    }
    // Verify file exists
    if _, err := os.Stat(path); err != nil {
    t.Errorf("file not created: %v", err)
    }
    // Collision detection
    err := WritePNGFile(path, img)
    if err == nil {
    t.Error("expected error for existing file")
  • edit in utils/spectrogram_test.go at line 146
    [8.2117]
    func TestSpectrogramImageFromSamples(t *testing.T) {
    // Create a simple sine wave
    const sampleRate = 16000
    const duration = 0.1 // 100ms
    samples := make([]float64, int(sampleRate*duration))
    for i := range samples {
    samples[i] = 0.5 // DC signal
    }
    img := SpectrogramImageFromSamples(samples, sampleRate, true, 224)
    if img == nil {
    t.Fatal("expected non-nil image")
    }
    bounds := img.Bounds()
    if bounds.Dx() != 224 || bounds.Dy() != 224 {
    t.Errorf("expected 224x224, got %dx%d", bounds.Dx(), bounds.Dy())
    }
    // Grayscale variant
    imgGray := SpectrogramImageFromSamples(samples, sampleRate, false, 224)
    if imgGray == nil {
    t.Error("expected non-nil grayscale image")
    }
    // Empty samples
    imgEmpty := SpectrogramImageFromSamples(nil, sampleRate, true, 224)
    if imgEmpty != nil {
    t.Error("expected nil for empty samples")
    }
    }
  • edit in utils/spectrogram.go at line 4
    [3.62748]
    [3.62748]
    "fmt"
  • edit in utils/spectrogram.go at line 7
    [3.62765]
    [3.62765]
    "os"
    "path/filepath"
  • edit in utils/spectrogram.go at line 192
    [3.67624]
    [3.67624]
    // SpectrogramImageFromSamples generates a spectrogram image from audio samples.
    // This is the core pipeline: spectrogram -> colormap/grayscale -> image -> resize.
    // Use this when you already have samples (e.g., after bandpass filtering).
    func SpectrogramImageFromSamples(samples []float64, sampleRate int, color bool, imgSize int) image.Image {
    if len(samples) == 0 {
    return nil
    }
    config := DefaultSpectrogramConfig(sampleRate)
    spectrogram := GenerateSpectrogram(samples, config)
    if spectrogram == nil {
    return nil
    }
  • edit in utils/spectrogram.go at line 207
    [3.67625]
    [3.67625]
    var img image.Image
    if color {
    colorData := ApplyL4Colormap(spectrogram)
    img = CreateRGBImage(colorData)
    } else {
    img = CreateGrayscaleImage(spectrogram)
    }
    if img == nil {
    return nil
    }
    imgSize = ClampImageSize(imgSize)
    return ResizeImage(img, imgSize, imgSize)
    }
  • edit in utils/spectrogram.go at line 241
    [3.206][3.68463:68494](),[3.8840][3.68463:68494](),[3.68463][3.68463:68494]()
    spectSampleRate := sampleRate
  • replacement in utils/spectrogram.go at line 243
    [3.321][3.321:362]()
    spectSampleRate = DefaultMaxSampleRate
    [3.321]
    [3.68649]
    sampleRate = DefaultMaxSampleRate
  • edit in utils/spectrogram.go at line 245
    [3.68652]
    [3.68652]
    img := SpectrogramImageFromSamples(segSamples, sampleRate, color, imgSize)
    return img, nil
    }
  • replacement in utils/spectrogram.go at line 250
    [3.68653][3.68653:68830]()
    // Generate spectrogram
    config := DefaultSpectrogramConfig(spectSampleRate)
    spectrogram := GenerateSpectrogram(segSamples, config)
    if spectrogram == nil {
    return nil, nil
    [3.68653]
    [3.68830]
    // WritePNGFile writes an image to a PNG file. Uses O_EXCL to atomically fail
    // if the file already exists. Returns an error with path context on failure.
    func WritePNGFile(path string, img image.Image) error {
    file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
    if err != nil {
    if os.IsExist(err) {
    return fmt.Errorf("file already exists: %s", path)
    }
    return fmt.Errorf("failed to create PNG: %w", err)
  • edit in utils/spectrogram.go at line 260
    [3.68833]
    [3.68833]
    if err := WritePNG(img, file); err != nil {
    _ = file.Close()
    return fmt.Errorf("failed to write PNG: %w", err)
    }
    if err := file.Close(); err != nil {
    return fmt.Errorf("failed to close PNG: %w", err)
    }
    return nil
    }
    // ClipBaseName generates the base filename for a clip in the format:
    // prefix_basename_startTime_endTime
    // Times are integers (floor for start, ceil for end).
    func ClipBaseName(prefix, basename string, startTime, endTime float64) string {
    startInt := int(math.Floor(startTime))
    endInt := int(math.Ceil(endTime))
    return fmt.Sprintf("%s_%s_%d_%d", prefix, basename, startInt, endInt)
    }
  • replacement in utils/spectrogram.go at line 279
    [3.68834][3.68834:69035]()
    // Create image (grayscale or color)
    var img image.Image
    if color {
    colorData := ApplyL4Colormap(spectrogram)
    img = CreateRGBImage(colorData)
    } else {
    img = CreateGrayscaleImage(spectrogram)
    [3.68834]
    [3.69035]
    // ClipPaths returns full PNG and WAV paths for a clip in the given output directory.
    // Also checks that neither file exists. Returns an error if files exist.
    func ClipPaths(outputDir, prefix, basename string, startTime, endTime float64) (pngPath, wavPath string, err error) {
    baseName := ClipBaseName(prefix, basename, startTime, endTime)
    pngPath = filepath.Join(outputDir, baseName+".png")
    wavPath = filepath.Join(outputDir, baseName+".wav")
    if _, err := os.Stat(pngPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", pngPath)
  • replacement in utils/spectrogram.go at line 289
    [3.69038][3.69038:69073]()
    if img == nil {
    return nil, nil
    [3.69038]
    [3.69073]
    if _, err := os.Stat(wavPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", wavPath)
  • edit in utils/spectrogram.go at line 292
    [3.69076]
    [3.69076]
    return pngPath, wavPath, nil
    }
  • replacement in utils/spectrogram.go at line 295
    [3.69077][3.69077:69171]()
    // Resize
    imgSize = ClampImageSize(imgSize)
    return ResizeImage(img, imgSize, imgSize), nil
    [3.69077]
    [3.69171]
    // WAVBasename extracts the base filename from a .data file path.
    // E.g., "/path/to/file.wav.data" -> "file".
    func WAVBasename(dataFilePath string) string {
    wavPath := strings.TrimSuffix(dataFilePath, ".data")
    basename := filepath.Base(wavPath)
    return strings.TrimSuffix(basename, filepath.Ext(basename))
  • file deletion: spectrogram.go (----------)
    [3.227139][2.16223:16261](),[2.16261][2.13272:13272]()
    package tui
    import (
    "fmt"
    "image"
    "os"
    tea "charm.land/bubbletea/v2"
    "skraak/tools/calls"
    "skraak/utils"
    )
    // generateSpectrogramImage creates a resized spectrogram image from a segment.
    func generateSpectrogramImage(state *calls.ClassifyState, dataPath string, seg *utils.Segment) image.Image {
    imgSize := state.Config.ImageSize
    if imgSize == 0 {
    imgSize = utils.SpectrogramDisplaySize
    }
    if state.Config.BandpassLow > 0 || state.Config.BandpassHigh > 0 {
    return generateBandpassSpectrogram(state, dataPath, seg, imgSize)
    }
    img, err := utils.GenerateSegmentSpectrogram(dataPath, seg.StartTime, seg.EndTime, state.Config.Color, imgSize)
    if err != nil {
    return nil
    }
    return img
    }
    // generateBandpassSpectrogram generates a spectrogram from bandpass-shifted audio.
    // Reads the segment, applies bandpass+shift+downsample, then generates spectrogram
    // from the processed samples.
    func generateBandpassSpectrogram(state *calls.ClassifyState, dataPath string, seg *utils.Segment, imgSize int) image.Image {
    wavPath := dataPath[:len(dataPath)-5] // strip ".data"
    samples, sampleRate, err := utils.ReadWAVSegmentSamples(wavPath, seg.StartTime, seg.EndTime)
    if err != nil || len(samples) == 0 {
    return nil
    }
    samples, sampleRate = utils.BandpassShiftFilter(samples, sampleRate, state.Config.BandpassLow, state.Config.BandpassHigh)
    config := utils.DefaultSpectrogramConfig(sampleRate)
    spectrogram := utils.GenerateSpectrogram(samples, config)
    if spectrogram == nil {
    return nil
    }
    var img image.Image
    if 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)
    }
    // inlineImageCmd returns a tea.Cmd that generates and writes an inline image
    // directly to the terminal, bypassing BubbleTea's renderer.
    // gen is the generation at dispatch time; currentGen points to the live counter.
    // If they differ when the image is ready, a newer segment change has occurred
    // and this image is stale — discard it instead of writing.
    func inlineImageCmd(state *calls.ClassifyState, protocol utils.ImageProtocol, gen uint64, currentGen *uint64) func() tea.Msg {
    return func() tea.Msg {
    df := state.CurrentFile()
    seg := state.CurrentSegment()
    if df == nil || seg == nil {
    return nil
    }
    img := generateSpectrogramImage(state, df.FilePath, seg)
    if img == nil {
    return nil
    }
    // Discard if a newer segment change has superseded this one
    if *currentGen != gen {
    return nil
    }
    // Clear previous kitty images before writing new one.
    // Terminal write errors during render are non-recoverable; ignore.
    _ = utils.ClearImages(os.Stdout, protocol)
    _, _ = fmt.Fprint(os.Stdout, "\r\n\r\n")
    _ = utils.WriteImage(img, os.Stdout, protocol)
    return nil
    }
    }
  • file deletion: clip.go (----------)
    [3.227139][2.26229:26260](),[2.26260][2.21152:21152]()
    package tui
    import (
    "fmt"
    "image"
    "os"
    "path/filepath"
    "strings"
    "skraak/tools/calls"
    "skraak/utils"
    )
    // saveClip saves a clip of the current segment to the current working directory
    func saveClip(state *calls.ClassifyState, prefix string) error {
    df := state.CurrentFile()
    seg := state.CurrentSegment()
    if df == nil || seg == nil {
    return fmt.Errorf("no segment selected")
    }
    pngPath, wavOutPath, err := buildClipPaths(df, seg, prefix)
    if err != nil {
    return err
    }
    segSamples, outputSampleRate, err := loadFilteredSegment(state, df, seg)
    if err != nil {
    return err
    }
    // Generate spectrogram image
    resized, err := generateClipSpectrogram(segSamples, outputSampleRate)
    if err != nil {
    return err
    }
    // Write output files
    if err := writeClipPNG(resized, pngPath); err != nil {
    return err
    }
    if err := utils.WriteWAVFile(wavOutPath, segSamples, outputSampleRate); err != nil {
    return fmt.Errorf("failed to write WAV: %w", err)
    }
    return nil
    }
    // loadFilteredSegment reads, bandpass-shifts, and downsamples a segment's audio.
    func loadFilteredSegment(state *calls.ClassifyState, df *utils.DataFile, seg *utils.Segment) ([]float64, int, error) {
    wavPath := df.FilePath[:len(df.FilePath)-5] // strip ".data"
    segSamples, sampleRate, err := utils.ReadWAVSegmentSamples(wavPath, seg.StartTime, seg.EndTime)
    if err != nil {
    return nil, 0, fmt.Errorf("failed to read WAV: %w", err)
    }
    if len(segSamples) == 0 {
    return nil, 0, fmt.Errorf("no samples in segment")
    }
    // Apply bandpass+shift+downsample if configured
    if state.Config.BandpassLow > 0 || state.Config.BandpassHigh > 0 {
    segSamples, sampleRate = utils.BandpassShiftFilter(segSamples, sampleRate, state.Config.BandpassLow, state.Config.BandpassHigh)
    return segSamples, sampleRate, nil
    }
    // No bandpass: downsample if sample rate exceeds default
    if sampleRate > utils.DefaultMaxSampleRate {
    segSamples = utils.ResampleRate(segSamples, sampleRate, utils.DefaultMaxSampleRate)
    sampleRate = utils.DefaultMaxSampleRate
    }
    return segSamples, sampleRate, nil
    }
    // writeClipPNG writes a spectrogram image to a PNG file with proper cleanup.
    func writeClipPNG(img image.Image, path string) error {
    pngFile, err := os.Create(path)
    if err != nil {
    return fmt.Errorf("failed to create PNG: %w", err)
    }
    if err := utils.WritePNG(img, pngFile); err != nil {
    _ = pngFile.Close()
    return fmt.Errorf("failed to write PNG: %w", err)
    }
    if err := pngFile.Close(); err != nil {
    return fmt.Errorf("failed to close PNG: %w", err)
    }
    return nil
    }
    // buildClipPaths constructs output file paths for a clip and checks they don't already exist.
    func buildClipPaths(df *utils.DataFile, seg *utils.Segment, prefix string) (pngPath, wavOutPath string, err error) {
    wavPath := df.FilePath[:len(df.FilePath)-5] // strip ".data"
    basename := wavPath[strings.LastIndex(wavPath, "/")+1:]
    basename = strings.TrimSuffix(basename, ".wav")
    startInt := int(seg.StartTime)
    endInt := int(seg.EndTime)
    if seg.EndTime > float64(endInt) {
    endInt++
    }
    cwd, err := os.Getwd()
    if err != nil {
    return "", "", fmt.Errorf("failed to get working directory: %w", err)
    }
    baseName := fmt.Sprintf("%s_%s_%d_%d", prefix, basename, startInt, endInt)
    pngPath = filepath.Join(cwd, baseName+".png")
    wavOutPath = filepath.Join(cwd, baseName+".wav")
    if _, err := os.Stat(pngPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", pngPath)
    }
    if _, err := os.Stat(wavOutPath); err == nil {
    return "", "", fmt.Errorf("file already exists: %s", wavOutPath)
    }
    return pngPath, wavOutPath, nil
    }
    // generateClipSpectrogram generates a 224px color spectrogram image from audio samples.
    func generateClipSpectrogram(segSamples []float64, sampleRate int) (image.Image, error) {
    config := utils.DefaultSpectrogramConfig(sampleRate)
    spectrogram := utils.GenerateSpectrogram(segSamples, config)
    if spectrogram == nil {
    return nil, fmt.Errorf("failed to generate spectrogram")
    }
    colorData := utils.ApplyL4Colormap(spectrogram)
    img := utils.CreateRGBImage(colorData)
    if img == nil {
    return nil, fmt.Errorf("failed to create image")
    }
    return utils.ResizeImage(img, 224, 224), nil
    }
    // playCurrentSegmentAtSpeed loads and plays the current segment's audio at the given speed.
    // speed=1.0 is normal, speed=0.5 is half speed.
    // Returns an error message string, or empty string on success.
    func playCurrentSegmentAtSpeed(state *calls.ClassifyState, speed float64) string {
    df := state.CurrentFile()
    seg := state.CurrentSegment()
    if df == nil || seg == nil {
    return ""
    }
    segSamples, playSampleRate, err := loadFilteredSegment(state, df, seg)
    if err != nil {
    return fmt.Sprintf("audio: %v", err)
    }
    // Initialize player lazily on first play
    if state.Player == nil {
    player, err := utils.NewAudioPlayer(playSampleRate)
    if err != nil {
    return fmt.Sprintf("audio init: %v", err)
    }
    state.Player = player
    }
    if len(segSamples) > 0 {
    state.PlaybackSpeed = speed
    state.Player.PlayAtSpeed(segSamples, playSampleRate, speed)
    }
    return ""
    }
  • edit in tui/update.go at line 4
    [2.3898]
    [2.3898]
    "fmt"
    "image"
    "os"
  • edit in tui/update.go at line 13
    [2.3972]
    [2.3972]
    "skraak/utils"
  • replacement in tui/update.go at line 36
    [2.4583][2.4583:4677]()
    return tea.Sequence(tea.ClearScreen, inlineImageCmd(m.state, m.protocol(), gen, m.imageGen))
    [2.4583]
    [2.4677]
    return tea.Sequence(tea.ClearScreen, m.inlineImageCmd(gen, m.imageGen))
  • replacement in tui/update.go at line 97
    [2.6237][2.6237:6310]()
    if errMsg := playCurrentSegmentAtSpeed(m.state, speed); errMsg != "" {
    [2.6237]
    [2.6310]
    if errMsg := m.state.PlaySegmentAtSpeed(speed); errMsg != "" {
  • replacement in tui/update.go at line 348
    [2.12241][2.12241:12501]()
    if m.clipInput == "" {
    m.clipMode = false
    return m, nil
    }
    // Save the clip
    err := saveClip(m.state, m.clipInput)
    if err != nil {
    m.err = err.Error()
    } else {
    m.err = "Clip saved: " + m.clipInput
    }
    m.clipMode = false
    return m, nil
    [2.12241]
    [2.12501]
    return m.handleClipEnter()
  • replacement in tui/update.go at line 366
    [2.12839][2.12839:12924]()
    s := msg.String()
    if len(s) == 1 && s[0] >= 32 && s[0] <= 126 { // printable ASCII
    [2.12839]
    [2.12924]
    return m.handleClipPrintable(msg.String())
    }
    // handleClipEnter handles Enter key in clip mode.
    func (m Model) handleClipEnter() (tea.Model, tea.Cmd) {
    if m.clipInput == "" {
    m.clipMode = false
    return m, nil
    }
    m.clipMode = false
    cwd, err := os.Getwd()
    if err != nil {
    m.err = err.Error()
    return m, nil
    }
    _, err = m.state.SaveClip(cwd, m.clipInput)
    if err != nil {
    m.err = err.Error()
    } else {
    m.err = "Clip saved: " + m.clipInput
    }
    return m, nil
    }
    // handleClipPrintable handles printable character input in clip mode.
    func (m Model) handleClipPrintable(s string) (tea.Model, tea.Cmd) {
    if len(s) == 1 && s[0] >= 32 && s[0] <= 126 {
  • edit in tui/update.go at line 398
    [2.12994][2.12994:13010]()
    return m, nil
  • edit in tui/update.go at line 399
    [2.13013][2.13013:13014]()
  • edit in tui/update.go at line 408
    [2.13236]
    // inlineImageCmd returns a tea.Cmd that generates and writes an inline image
    // directly to the terminal, bypassing BubbleTea's renderer.
    // gen is the generation at dispatch time; currentGen points to the live counter.
    // If they differ when the image is ready, a newer segment change has occurred
    // and this image is stale — discard it instead of writing.
    func (m Model) inlineImageCmd(gen uint64, currentGen *uint64) tea.Cmd {
    return func() tea.Msg {
    df := m.state.CurrentFile()
    seg := m.state.CurrentSegment()
    if df == nil || seg == nil {
    return nil
    }
    img := m.generateSpectrogramImage(df.FilePath, seg)
    if img == nil {
    return nil
    }
    // Discard if a newer segment change has superseded this one
    if *currentGen != gen {
    return nil
    }
    // Clear previous kitty images before writing new one.
    // Terminal write errors during render are non-recoverable; ignore.
    _ = utils.ClearImages(os.Stdout, m.protocol())
    _, _ = fmt.Fprint(os.Stdout, "\r\n\r\n")
    _ = utils.WriteImage(img, os.Stdout, m.protocol())
    return nil
    }
    }
    // generateSpectrogramImage creates a resized spectrogram image from a segment.
    func (m Model) generateSpectrogramImage(dataPath string, seg *utils.Segment) image.Image {
    imgSize := m.state.Config.ImageSize
    if imgSize == 0 {
    imgSize = utils.SpectrogramDisplaySize
    }
    // For bandpass, load and filter samples manually
    if m.state.Config.BandpassLow > 0 || m.state.Config.BandpassHigh > 0 {
    wavPath := dataPath[:len(dataPath)-5] // strip ".data"
    samples, sampleRate, err := utils.ReadWAVSegmentSamples(wavPath, seg.StartTime, seg.EndTime)
    if err != nil || len(samples) == 0 {
    return nil
    }
    samples, sampleRate = utils.BandpassShiftFilter(samples, sampleRate, m.state.Config.BandpassLow, m.state.Config.BandpassHigh)
    return utils.SpectrogramImageFromSamples(samples, sampleRate, m.state.Config.Color, imgSize)
    }
    // Standard path
    img, err := utils.GenerateSegmentSpectrogram(dataPath, seg.StartTime, seg.EndTime, m.state.Config.Color, imgSize)
    if err != nil {
    return nil
    }
    return img
    }
  • replacement in tui/model.go at line 136
    [2.19440][2.19440:19511]()
    return inlineImageCmd(m.state, m.protocol(), *m.imageGen, m.imageGen)
    [2.19440]
    [2.19511]
    return m.inlineImageCmd(*m.imageGen, m.imageGen)
  • edit in tools/calls/calls_clip.go at line 5
    [3.257918][3.257918:257935]()
    "image"
    "math"
  • replacement in tools/calls/calls_clip.go at line 335
    [3.268783][3.268783:268803]()
    var files []string
    [3.268783]
    [3.268803]
    // Build paths (ClipPaths checks for existing files)
    pngPath, wavPath, err := utils.ClipPaths(outputDir, prefix, basename, startTime, endTime)
    if err != nil {
    return nil, err
    }
  • edit in tools/calls/calls_clip.go at line 341
    [3.268804][3.268804:269076]()
    // Calculate integer times for filename
    startInt := int(math.Floor(startTime))
    endInt := int(math.Ceil(endTime))
    // Build base filename
    baseName := fmt.Sprintf("%s_%s_%d_%d", prefix, basename, startInt, endInt)
    wavPath := filepath.Join(outputDir, baseName+".wav")
  • replacement in tools/calls/calls_clip.go at line 347
    [3.269269][3.269269:269358]()
    // Determine output sample rate (downsample if > 16kHz)
    outputSampleRate := sampleRate
    [3.269269]
    [3.269358]
    // Downsample if > 16kHz
  • replacement in tools/calls/calls_clip.go at line 350
    [3.269490][3.269490:269538]()
    outputSampleRate = utils.DefaultMaxSampleRate
    [3.269490]
    [3.269538]
    sampleRate = utils.DefaultMaxSampleRate
  • replacement in tools/calls/calls_clip.go at line 353
    [3.269542][3.269542:269780]()
    pngPath := filepath.Join(outputDir, baseName+".png")
    spectSampleRate := outputSampleRate
    config := utils.DefaultSpectrogramConfig(spectSampleRate)
    spectrogram := utils.GenerateSpectrogram(segSamples, config)
    if spectrogram == nil {
    [3.269542]
    [3.269780]
    // Generate spectrogram image
    img := utils.SpectrogramImageFromSamples(segSamples, sampleRate, color, imgSize)
    if img == nil {
  • replacement in tools/calls/calls_clip.go at line 359
    [3.269843][3.269843:270133]()
    // Create image (grayscale or color)
    var img image.Image
    if color {
    colorData := utils.ApplyL4Colormap(spectrogram)
    img = utils.CreateRGBImage(colorData)
    } else {
    img = utils.CreateGrayscaleImage(spectrogram)
    }
    if img == nil {
    return nil, fmt.Errorf("failed to create image")
    [3.269843]
    [3.270133]
    // Write PNG
    if err := utils.WritePNGFile(pngPath, img); err != nil {
    return nil, err
  • edit in tools/calls/calls_clip.go at line 364
    [3.270137][3.270137:270766]()
    resized := utils.ResizeImage(img, imgSize, imgSize)
    // Write PNG (O_EXCL fails atomically if file exists)
    pngFile, err := os.OpenFile(pngPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0644)
    if err != nil {
    if os.IsExist(err) {
    return nil, fmt.Errorf("file already exists: %s", pngPath)
    }
    return nil, fmt.Errorf("failed to create PNG: %w", err)
    }
    if err := utils.WritePNG(resized, pngFile); err != nil {
    _ = pngFile.Close()
    return nil, fmt.Errorf("failed to write PNG: %w", err)
    }
    if err := pngFile.Close(); err != nil {
    return nil, fmt.Errorf("failed to close PNG: %w", err)
    }
    files = append(files, pngPath)
  • replacement in tools/calls/calls_clip.go at line 365
    [3.270780][3.270780:270863]()
    if err := utils.WriteWAVFile(wavPath, segSamples, outputSampleRate); err != nil {
    [3.270780]
    [3.270863]
    if err := utils.WriteWAVFile(wavPath, segSamples, sampleRate); err != nil {
  • edit in tools/calls/calls_clip.go at line 368
    [3.270923][3.270923:270955]()
    files = append(files, wavPath)
  • replacement in tools/calls/calls_clip.go at line 369
    [3.270956][3.270956:270975]()
    return files, nil
    [3.270956]
    [3.270975]
    return []string{pngPath, wavPath}, nil
  • edit in tools/calls/calls_classify.go at line 707
    [3.310561]
    // LoadFilteredSegment reads, bandpass-shifts, and downsamples a segment's audio.
    // Returns samples and effective sample rate after any filtering/downsampling.
    func (s *ClassifyState) LoadFilteredSegment(df *utils.DataFile, seg *utils.Segment) ([]float64, int, error) {
    wavPath := df.FilePath[:len(df.FilePath)-5] // strip ".data"
    segSamples, sampleRate, err := utils.ReadWAVSegmentSamples(wavPath, seg.StartTime, seg.EndTime)
    if err != nil {
    return nil, 0, fmt.Errorf("failed to read WAV: %w", err)
    }
    if len(segSamples) == 0 {
    return nil, 0, fmt.Errorf("no samples in segment")
    }
    // Apply bandpass+shift+downsample if configured
    if s.Config.BandpassLow > 0 || s.Config.BandpassHigh > 0 {
    segSamples, sampleRate = utils.BandpassShiftFilter(segSamples, sampleRate, s.Config.BandpassLow, s.Config.BandpassHigh)
    return segSamples, sampleRate, nil
    }
    // No bandpass: downsample if sample rate exceeds default
    if sampleRate > utils.DefaultMaxSampleRate {
    segSamples = utils.ResampleRate(segSamples, sampleRate, utils.DefaultMaxSampleRate)
    sampleRate = utils.DefaultMaxSampleRate
    }
    return segSamples, sampleRate, nil
    }
    // SaveClip saves a spectrogram PNG and WAV of the current segment to outputDir.
    // The prefix is prepended to the filename.
    func (s *ClassifyState) SaveClip(outputDir, prefix string) ([]string, error) {
    df := s.CurrentFile()
    seg := s.CurrentSegment()
    if df == nil || seg == nil {
    return nil, fmt.Errorf("no segment selected")
    }
    basename := utils.WAVBasename(df.FilePath)
    pngPath, wavPath, err := utils.ClipPaths(outputDir, prefix, basename, seg.StartTime, seg.EndTime)
    if err != nil {
    return nil, err
    }
    segSamples, sampleRate, err := s.LoadFilteredSegment(df, seg)
    if err != nil {
    return nil, err
    }
    // Generate spectrogram image (always color, 224px for clips)
    img := utils.SpectrogramImageFromSamples(segSamples, sampleRate, true, 224)
    if img == nil {
    return nil, fmt.Errorf("failed to generate spectrogram")
    }
    if err := utils.WritePNGFile(pngPath, img); err != nil {
    return nil, err
    }
    if err := utils.WriteWAVFile(wavPath, segSamples, sampleRate); err != nil {
    return nil, fmt.Errorf("failed to write WAV: %w", err)
    }
    return []string{pngPath, wavPath}, nil
    }
    // PlaySegmentAtSpeed loads and plays the current segment's audio at the given speed.
    // speed=1.0 is normal, speed=0.5 is half speed.
    // Returns an error message string, or empty string on success.
    func (s *ClassifyState) PlaySegmentAtSpeed(speed float64) string {
    df := s.CurrentFile()
    seg := s.CurrentSegment()
    if df == nil || seg == nil {
    return ""
    }
    segSamples, playSampleRate, err := s.LoadFilteredSegment(df, seg)
    if err != nil {
    return fmt.Sprintf("audio: %v", err)
    }
    // Initialize player lazily on first play
    if s.Player == nil {
    player, err := utils.NewAudioPlayer(playSampleRate)
    if err != nil {
    return fmt.Sprintf("audio init: %v", err)
    }
    s.Player = player
    }
    if len(segSamples) > 0 {
    s.PlaybackSpeed = speed
    s.Player.PlayAtSpeed(segSamples, playSampleRate, speed)
    }
    return ""
    }