IZEEEQS5ES4AYNGZ7RE54QYUBREN6NA6PML242JR5N7EKVDCKT3AC 3ZSBMHZBDIR3SABGTLQVBGMC2YQCRM4XNVDYGSASBF6FXYJ4J74QC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC 7CC2YVZXAIUNWXNNVIO5KOZZFDQQLESFO72SGEDP2C4OZXAWO4KQC L7ON3P7TC6NDF7HTGGWKTCM5E3E54W5S5ZED5NCDJJV7HC62TODAC IWA42FYL3SDWJKVE4YOFDOERH5YZ2BHNK3JRQCQMKCZ2R2O7X56QC NKQAT3RE4IBIWXVMI5LJUINDPHTANNMORZ5N2JFA4AN6UUB72KGAC KS7LFF6M5Y6UGBBA7G63BJRR5XS4P4R3PSZPG752NSGZ3Z6GY72QC 7NS27QXZMVTZBK4VPMYL5IKGSTTAWR6NDG5SOVITNX44VNIRZPMAC 2GJMZ6YA6OPHNS5KFFFI6POQ2BJ33SSS3NIPXYBFTJSN4BZBVEVAC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC JQVXBZT5FKGPPIT5VJM3AWXZUJJTPFSCFSQOV4MS2WISPNAHH7JAC package toolsimport ("os""path/filepath""testing")func TestShouldPrependFile(t *testing.T) {tests := []struct {name stringfilename stringprefix stringwantRename boolwantReason string}{// WAV files with datestring{"wav with datestring", "20250920_011509.wav", "LOC", true, ""},{"WAV with datestring", "20250920_011509.WAV", "LOC", true, ""},{"wav.data with datestring", "20250920_011509.wav.data", "LOC", true, ""},{"WAV.data with datestring", "20250920_011509.WAV.data", "LOC", true, ""},// Already prefixed{"already prefixed wav", "LOC_20250920_011509.wav", "LOC", false, "already prefixed"},{"already prefixed log.txt", "LOC_log.txt", "LOC", false, "already prefixed"},// No datestring{"no datestring wav", "mok_nearcamp2_20250920.wav", "LOC", false, "no datestring prefix"},{"no datestring WAV", "recording.WAV", "LOC", false, "no datestring prefix"},// log.txt{"log.txt", "log.txt", "LOC", true, ""},// Non-target files (silently ignored){"readme", "README.txt", "LOC", false, ""},{"random file", "something.mp3", "LOC", false, ""},{"LOG.TXT uppercase", "LOG.TXT", "LOC", false, ""}, // Only lowercase log.txt matches}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {gotRename, gotReason := shouldPrependFile(tt.filename, tt.prefix)if gotRename != tt.wantRename {t.Errorf("shouldPrependFile() gotRename = %v, want %v", gotRename, tt.wantRename)}if gotReason != tt.wantReason {t.Errorf("shouldPrependFile() gotReason = %v, want %v", gotReason, tt.wantReason)}})}}func TestPrepend(t *testing.T) {// Create temp foldertmpDir, err := os.MkdirTemp("", "prepend_test")if err != nil {t.Fatalf("Failed to create temp dir: %v", err)}defer os.RemoveAll(tmpDir)// Create test filestestFiles := []string{"20250920_011509.wav","20250920_011509.wav.data","log.txt","mok_nearcamp2_20250920.wav","README.txt",}for _, f := range testFiles {if err := os.WriteFile(filepath.Join(tmpDir, f), []byte{}, 0644); err != nil {t.Fatalf("Failed to create test file: %v", err)}}// Run prependoutput, err := Prepend(PrependInput{Folder: tmpDir,Prefix: "TEST",Recursive: false,DryRun: false,})if err != nil {t.Fatalf("Prepend() error = %v", err)}// Verify renamed filesif len(output.Renamed) != 3 {t.Errorf("Expected 3 renamed files, got %d", len(output.Renamed))}// Verify skipped filesif len(output.Skipped) != 1 {t.Errorf("Expected 1 skipped file, got %d", len(output.Skipped))}// Verify files were actually renamedif _, err := os.Stat(filepath.Join(tmpDir, "TEST_20250920_011509.wav")); os.IsNotExist(err) {t.Error("Expected TEST_20250920_011509.wav to exist")}if _, err := os.Stat(filepath.Join(tmpDir, "TEST_log.txt")); os.IsNotExist(err) {t.Error("Expected TEST_log.txt to exist")}if _, err := os.Stat(filepath.Join(tmpDir, "mok_nearcamp2_20250920.wav")); os.IsNotExist(err) {t.Error("Expected mok_nearcamp2_20250920.wav to still exist (skipped)")}}func TestPrependRecursive(t *testing.T) {// Create temp folder with subfoldertmpDir, err := os.MkdirTemp("", "prepend_test")if err != nil {t.Fatalf("Failed to create temp dir: %v", err)}defer os.RemoveAll(tmpDir)subDir := filepath.Join(tmpDir, "subfolder")if err := os.Mkdir(subDir, 0755); err != nil {t.Fatalf("Failed to create subfolder: %v", err)}// Create test filesfiles := map[string]string{filepath.Join(tmpDir, "20250920_011509.wav"): "",filepath.Join(subDir, "20250921_120000.wav"): "",filepath.Join(subDir, "log.txt"): "",}for f := range files {if err := os.WriteFile(f, []byte{}, 0644); err != nil {t.Fatalf("Failed to create test file: %v", err)}}// Run prepend with recursiveoutput, err := Prepend(PrependInput{Folder: tmpDir,Prefix: "TEST",Recursive: true,DryRun: false,})if err != nil {t.Fatalf("Prepend() error = %v", err)}// Should rename files in both foldersif len(output.Renamed) != 3 {t.Errorf("Expected 3 renamed files (recursive), got %d", len(output.Renamed))}// Verify subfolder file was renamedif _, err := os.Stat(filepath.Join(subDir, "TEST_20250921_120000.wav")); os.IsNotExist(err) {t.Error("Expected TEST_20250921_120000.wav in subfolder to exist")}}func TestPrependDryRun(t *testing.T) {tmpDir, err := os.MkdirTemp("", "prepend_test")if err != nil {t.Fatalf("Failed to create temp dir: %v", err)}defer os.RemoveAll(tmpDir)// Create test filetestFile := filepath.Join(tmpDir, "20250920_011509.wav")if err := os.WriteFile(testFile, []byte{}, 0644); err != nil {t.Fatalf("Failed to create test file: %v", err)}// Run prepend with dry-runoutput, err := Prepend(PrependInput{Folder: tmpDir,Prefix: "TEST",Recursive: false,DryRun: true,})if err != nil {t.Fatalf("Prepend() error = %v", err)}// Should report renamed filesif len(output.Renamed) != 1 {t.Errorf("Expected 1 renamed file in dry-run output, got %d", len(output.Renamed))}// But file should NOT be renamedif _, err := os.Stat(filepath.Join(tmpDir, "TEST_20250920_011509.wav")); !os.IsNotExist(err) {t.Error("Expected file NOT to be renamed in dry-run mode")}}func TestPrependIdempotent(t *testing.T) {tmpDir, err := os.MkdirTemp("", "prepend_test")if err != nil {t.Fatalf("Failed to create temp dir: %v", err)}defer os.RemoveAll(tmpDir)// Create test fileif err := os.WriteFile(filepath.Join(tmpDir, "20250920_011509.wav"), []byte{}, 0644); err != nil {t.Fatalf("Failed to create test file: %v", err)}// Run prepend twicefor i := 0; i < 2; i++ {output, err := Prepend(PrependInput{Folder: tmpDir,Prefix: "TEST",Recursive: false,DryRun: false,})if err != nil {t.Fatalf("Prepend() iteration %d error = %v", i, err)}if i == 0 {// First run should renameif len(output.Renamed) != 1 {t.Errorf("First run: expected 1 renamed file, got %d", len(output.Renamed))}} else {// Second run should skip (already prefixed)if len(output.Renamed) != 0 {t.Errorf("Second run: expected 0 renamed files, got %d", len(output.Renamed))}if len(output.Skipped) != 1 {t.Errorf("Second run: expected 1 skipped file, got %d", len(output.Skipped))}}}}
package toolsimport ("fmt""os""path/filepath""regexp""strings")// PrependInput contains the parameters for the prepend operation.type PrependInput struct {Folder stringPrefix stringRecursive boolDryRun bool}// PrependResult contains the result of a single file rename operation.type PrependResult struct {Old string `json:"old"`New string `json:"new"`}// PrependSkipped contains info about a skipped file.type PrependSkipped struct {File string `json:"file"`Reason string `json:"reason"`}// PrependError contains info about a failed rename.type PrependError struct {File string `json:"file"`Error string `json:"error"`}// PrependOutput contains the complete result of the prepend operation.type PrependOutput struct {Folder string `json:"folder"`Prefix string `json:"prefix"`Recursive bool `json:"recursive"`DryRun bool `json:"dry_run"`Renamed []PrependResult `json:"renamed"`Skipped []PrependSkipped `json:"skipped"`Errors []PrependError `json:"errors"`}// datestringRegex matches filenames starting with YYYYMMDD_HHMMSS.var datestringRegex = regexp.MustCompile(`^\d{8}_\d{6}\.`)// Prepend renames files in a folder by prepending a prefix.// WAV files (.wav, .WAV) and their .data files are only renamed if they start with a datestring.// log.txt is always renamed if present.func Prepend(input PrependInput) (*PrependOutput, error) {output := &PrependOutput{Folder: input.Folder,Prefix: input.Prefix,Recursive: input.Recursive,DryRun: input.DryRun,Renamed: []PrependResult{},Skipped: []PrependSkipped{},Errors: []PrependError{},}// Collect folders to processfolders := []string{input.Folder}if input.Recursive {entries, err := os.ReadDir(input.Folder)if err != nil {return nil, fmt.Errorf("failed to read folder: %w", err)}for _, entry := range entries {if entry.IsDir() {folders = append(folders, filepath.Join(input.Folder, entry.Name()))}}}// Process each folderfor _, folder := range folders {entries, err := os.ReadDir(folder)if err != nil {return nil, fmt.Errorf("failed to read folder %s: %w", folder, err)}for _, entry := range entries {if entry.IsDir() {continue}filename := entry.Name()oldPath := filepath.Join(folder, filename)shouldRename, skipReason := shouldPrependFile(filename, input.Prefix)if !shouldRename {if skipReason != "" {output.Skipped = append(output.Skipped, PrependSkipped{File: oldPath,Reason: skipReason,})}continue}newFilename := input.Prefix + "_" + filenamenewPath := filepath.Join(folder, newFilename)if input.DryRun {output.Renamed = append(output.Renamed, PrependResult{Old: oldPath,New: newPath,})continue}// Perform the renameif err := os.Rename(oldPath, newPath); err != nil {output.Errors = append(output.Errors, PrependError{File: oldPath,Error: err.Error(),})continue}output.Renamed = append(output.Renamed, PrependResult{Old: oldPath,New: newPath,})}}return output, nil}// shouldPrependFile determines if a file should be prepended.// Returns (shouldRename, skipReason). If shouldRename is false and skipReason is empty,// the file is not a target type (silently ignored).func shouldPrependFile(filename, prefix string) (bool, string) {lowerName := strings.ToLower(filename)// Check if already prefixed (applies to all target files)if strings.HasPrefix(filename, prefix+"_") {// Only report as "already prefixed" if it's a target file typeif filename == prefix+"_log.txt" || isWavOrData(lowerName) {return false, "already prefixed"}return false, ""}// Check for log.txt (exact match, case-sensitive as per spec)if filename == "log.txt" {return true, ""}// Check for WAV files and their .data filesif !isWavOrData(lowerName) {return false, "" // Not a target file type, silently ignore}// Check for datestring prefix (YYYYMMDD_HHMMSS.)if !datestringRegex.MatchString(filename) {return false, "no datestring prefix"}return true, ""}// isWavOrData checks if the lowercase filename is a .wav or .wav.data filefunc isWavOrData(lowerName string) bool {return strings.HasSuffix(lowerName, ".wav") || strings.HasSuffix(lowerName, ".wav.data")}
for folder in acmd = `skraak calls clip \--folder $folder \--prefix Wendy24 \--output /home/david/go/src/skraak/.claude/skills/skraak-check-call-classification/Australasian\ Bittern \--species Bittern \--filter M \--size 224 \--color`run(cmd) # execute the commandendskraak calls clip --folder . --prefix Internet --output "/home/david/go/src/skraak/.claude/skills/skraak-check-call-classification/Chaffinch" --species chaffinch --filter M --size 224 --colorusing FileIO, FLAC, DSP, PerceptualColourMaps, ImageTransformations, StatsBasefunction get_image_from_sample(sample, f) #sample::Vector{Float64}S = DSP.spectrogram(sample, 400, 2; fs = convert(Int, f))i = S.powerif minimum(i) == 0.0l = i |> vec |> unique |> sortreplace!(i, 0.0 => l[2])endimage =#! format: offDSP.pow2db.(i) |>x -> x .+ abs(minimum(x)) |>x -> x ./ maximum(x) |>x -> reverse(x, dims = 1) |>x -> PerceptualColourMaps.applycolourmap(x, cmap("L4")) |>#x -> RGB.(x) |>x -> ImageTransformations.imresize(x, 224, 224) |>x -> Float32.(x)#! format: onreturn imageend
package cmdimport ("encoding/json""flag""fmt""os""skraak/tools")// RunPrepend handles the "prepend" subcommandfunc RunPrepend(args []string) {fs := flag.NewFlagSet("prepend", flag.ExitOnError)folder := fs.String("folder", "", "Target folder path (required)")prefix := fs.String("prefix", "", "String to prepend to filenames (required)")recursive := fs.Bool("recursive", false, "Include 1 level of subfolders")dryRun := fs.Bool("dry-run", false, "Show what would be renamed without doing it")fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak prepend --folder <path> --prefix <string> [--recursive] [--dry-run]\n\n")fmt.Fprintf(os.Stderr, "Rename files by prepending a prefix.\n\n")fmt.Fprintf(os.Stderr, "Target files:\n")fmt.Fprintf(os.Stderr, " - *.wav, *.WAV (must start with datestring YYYYMMDD_HHMMSS)\n")fmt.Fprintf(os.Stderr, " - *.wav.data, *.WAV.data (must start with datestring YYYYMMDD_HHMMSS)\n")fmt.Fprintf(os.Stderr, " - log.txt (exact name, always renamed)\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak prepend --folder ./recordings --prefix LOC001\n")fmt.Fprintf(os.Stderr, " skraak prepend --folder ./data --prefix SITE_A --recursive\n")fmt.Fprintf(os.Stderr, " skraak prepend --folder ./test --prefix TEST --dry-run\n")}if err := fs.Parse(args); err != nil {os.Exit(1)}if *folder == "" {fmt.Fprintf(os.Stderr, "Error: --folder is required\n\n")fs.Usage()os.Exit(1)}if *prefix == "" {fmt.Fprintf(os.Stderr, "Error: --prefix is required\n\n")fs.Usage()os.Exit(1)}// Run the prepend operationoutput, err := tools.Prepend(tools.PrependInput{Folder: *folder,Prefix: *prefix,Recursive: *recursive,DryRun: *dryRun,})if err != nil {fmt.Fprintf(os.Stderr, "Error: %v\n", err)os.Exit(1)}// Output as JSONenc := json.NewEncoder(os.Stdout)enc.SetIndent("", " ")enc.Encode(output)}
# Rename files with location prefix./skraak prepend --folder ./recordings --prefix LOC001 # WAV files with datestring + log.txt./skraak prepend --folder ./data --prefix SITE_A --recursive # Include 1 level of subfolders./skraak prepend --folder ./test --prefix TEST --dry-run # Preview changes
## [2026-04-04] New `prepend` commandRename WAV files, their .data files, and log.txt by prepending a location prefix.**Usage:**```bashskraak prepend --folder <path> --prefix <string> [--recursive] [--dry-run]```**Target files:**- `*.wav`, `*.WAV` — Only if starting with datestring `YYYYMMDD_HHMMSS`- `*.wav.data`, `*.WAV.data` — Only if starting with datestring `YYYYMMDD_HHMMSS`- `log.txt` — Always renamed (exact name match)**Flags:**- `--folder <path>` — Target folder (required)- `--prefix <string>` — String to prepend (required)- `--recursive` — Include 1 level of subfolders- `--dry-run` — Show what would be renamed without doing it**Behavior:**- Files already starting with `<prefix>_` are skipped with reason "already prefixed"- WAV files without datestring prefix are skipped with reason "no datestring prefix"- Non-target files are silently ignored- Idempotent: running twice is safe**Examples:**```bash# Rename files in a folderskraak prepend --folder ./recordings --prefix LOC001# Include subfolders (1 level deep)skraak prepend --folder ./data --prefix SITE_A --recursive# Preview changesskraak prepend --folder ./test --prefix TEST --dry-run```