XLL6JFARO2VSNEOJQPXPKZLYYYF7XQGBCR2QJSIFF2RKYZKI25LAC GQ2FYLG2VGDDKQ7TJTCFM2XPLJOAX2N737OZWPRCNKSFIH7YJ6EQC FRY33K6EGWLU3F5NJJ66AT5RBV6OEOWSDKY3JH2CPKKGPAHOMM6AC DBOROCRFD6A5SJBMFYFEJI5S5M77X4EFEK6KDQWA5QDMQJKIHRWQC 22FIUBOFKBIV3WINZUSBKJO526K52HQV3V33B3DRS6CQWHU5HFPAC ULWBYMSXYZEE7BJQ2B4HTX2JAABBNJWZTUFIQ56NF5RWXODXRTHQC SDBVLSDDRPQF62XXKJKM2RQLMXOKKHOYRVUF6DIUDFRYCGL2DW3QC 2IURSWW3ZXRBH3DPJO437YE2FGMG54MPFL6W6UCEF5HODPVWVVTQC EW7VBNMGWFBC73ZUDLB4LIK2HWFKA74ZUTUDG4J575ZQHEFHW4UQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC }return m, nil}return m, nil}// handleClipKey handles key presses in clip modefunc (m Model) handleClipKey(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {key := msg.Key()// Enter: save clipif key.Code == tea.KeyEnter {if m.clipInput == "" {m.clipMode = falsereturn m, nil}// Save the cliperr := saveClip(m.state, m.clipInput)if err != nil {m.err = err.Error()} else {m.err = "Clip saved: " + m.clipInput}m.clipMode = falsereturn m, nil}// Escape: cancelif key.Code == tea.KeyEscape || key.Code == tea.KeyEsc {m.clipMode = falsereturn m, nil}// Backspace: remove last characterif key.Code == tea.KeyBackspace {if len(m.clipInput) > 0 {m.clipInput = m.clipInput[:len(m.clipInput)-1]
// saveClip saves a clip of the current segment to the current working directoryfunc saveClip(state *tools.ClassifyState, prefix string) error {df := state.CurrentFile()seg := state.CurrentSegment()if df == nil || seg == nil {return fmt.Errorf("no segment selected")}// Get WAV pathwavPath := strings.TrimSuffix(df.FilePath, ".data")// Get basename without path and extensionbasename := wavPath[strings.LastIndex(wavPath, "/")+1:]basename = strings.TrimSuffix(basename, ".wav")
// Calculate integer times for filenamestartInt := int(seg.StartTime)endInt := int(seg.EndTime)if seg.EndTime > float64(endInt) {endInt++ // ceil}// Build output paths (current working directory)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")// Check if files already existif _, 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)}// Read WAV samplessamples, sampleRate, err := utils.ReadWAVSamples(wavPath)if err != nil {return fmt.Errorf("failed to read WAV: %w", err)}// Extract segment samplessegSamples := utils.ExtractSegmentSamples(samples, sampleRate, seg.StartTime, seg.EndTime)if len(segSamples) == 0 {return fmt.Errorf("no samples in segment")}// Determine output sample rate (downsample if > 16kHz)outputSampleRate := sampleRateif sampleRate > utils.DefaultMaxSampleRate {segSamples = utils.ResampleRate(segSamples, sampleRate, utils.DefaultMaxSampleRate)outputSampleRate = utils.DefaultMaxSampleRate}// Generate spectrogram (224px, color)config := utils.DefaultSpectrogramConfig(outputSampleRate)spectrogram := utils.GenerateSpectrogram(segSamples, config)if spectrogram == nil {return fmt.Errorf("failed to generate spectrogram")}colorData := utils.ApplyL4Colormap(spectrogram)img := utils.CreateRGBImage(colorData)if img == nil {return fmt.Errorf("failed to create image")}resized := utils.ResizeImage(img, 224, 224)// Write PNGpngFile, err := os.Create(pngPath)if err != nil {return fmt.Errorf("failed to create PNG: %w", err)}if err := utils.WritePNG(resized, pngFile); err != nil {pngFile.Close()return fmt.Errorf("failed to write PNG: %w", err)}pngFile.Close()// Write WAVif err := utils.WriteWAVFile(wavOutPath, segSamples, outputSampleRate); err != nil {return fmt.Errorf("failed to write WAV: %w", err)}return nil}
b.WriteString(helpDarkStyle.Render(wrapText("[esc]quit [,]prev [.]next [space]comment [ctrl+g]goto [ctrl+d]bookmark [ctrl+,]prev-bk [ctrl+.]next-bk [enter]play [shift+enter]½speed", wrapWidth)))
b.WriteString(helpDarkStyle.Render(wrapText("[esc]quit [,]prev [.]next [space]comment [ctrl+s]clip [ctrl+g]goto [ctrl+d]bookmark [ctrl+,]prev-bk [ctrl+.]next-bk [enter]play [shift+enter]½speed", wrapWidth)))
// renderClipDialog renders the clip prefix input dialogfunc (m Model) renderClipDialog(b *strings.Builder) {inputLine := m.clipInput + "█"helpLine := "[enter]save [esc]cancel"// Render boxcontent := fmt.Sprintf("Clip prefix:\n%s\n%s", inputLine, helpLine)b.WriteString(commentBoxStyle.Render(content))}
## [2026-04-02] Clip feature in `calls classify` TUIAdded `ctrl+s` keybinding to save a clip of the current segment directly fromthe classification TUI.**Keybinding:** `ctrl+s` → type prefix → `enter` to save, `esc` to cancel**Output files:**- `<prefix>_<basename>_<start>_<end>.png` — 224x224 color spectrogram (L4 colormap)- `<prefix>_<basename>_<start>_<end>.wav` — audio clip (16kHz if downsampled)
Files are saved to the current working directory where `skraak` was launched.Error if files already exist (no overwrite).**Changes:**- `tui/classify.go` — Added `clipMode` state, `handleClipKey()`, `renderClipDialog()`,and `saveClip()` function; added `ctrl+s` keybinding; updated help line