package cmd
import (
"encoding/json"
"flag"
"fmt"
"os"
"skraak/tools"
)
func RunCalls(args []string) {
if len(args) < 1 {
printCallsUsage()
os.Exit(1)
}
switch args[0] {
case "from-preds":
runCallsFromPreds(args[1:])
case "show-images":
runCallsShowImages(args[1:])
case "classify":
RunCallsClassify(args[1:])
default:
fmt.Fprintf(os.Stderr, "Unknown calls subcommand: %s\n\n", args[0])
printCallsUsage()
os.Exit(1)
}
}
func printCallsUsage() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls <subcommand> [options]\n\n")
fmt.Fprintf(os.Stderr, "Subcommands:\n")
fmt.Fprintf(os.Stderr, " from-preds Extract clustered calls from ML predictions CSV\n")
fmt.Fprintf(os.Stderr, " show-images Display spectrogram images from .data file\n")
fmt.Fprintf(os.Stderr, " classify Review and classify segments in .data files\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv predictions.csv\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv preds.csv --dot-data=false > calls.json\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv preds.csv --filter my-filter\n")
fmt.Fprintf(os.Stderr, " skraak calls show-images --file recording.wav.data\n")
fmt.Fprintf(os.Stderr, " skraak calls classify --folder ./data --reviewer David --bind k=Kiwi\n")
fmt.Fprintf(os.Stderr, " skraak calls classify --folder ./data --reviewer David \\\n")
fmt.Fprintf(os.Stderr, " --bind k=Kiwi --bind d='Kiwi+Duet' --bind n='Don''t Know'\n")
}
func runCallsFromPreds(args []string) {
fs := flag.NewFlagSet("calls from-preds", flag.ExitOnError)
csvPath := fs.String("csv", "", "Path to predictions CSV file (required)")
filter := fs.String("filter", "", "Filter name for .data files (default: parse from CSV filename)")
dotData := fs.Bool("dot-data", true, "Write .data files alongside audio files (default: true)")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls from-preds [options]\n\n")
fmt.Fprintf(os.Stderr, "Extract clustered bird calls from ML predictions CSV.\n")
fmt.Fprintf(os.Stderr, "Reads prediction CSV with columns: file, start_time, end_time, <ebird_codes...>\n")
fmt.Fprintf(os.Stderr, "Each row is a clip with 1=present, 0=absent for each species.\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
fs.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nOutput:\n")
fmt.Fprintf(os.Stderr, " With --dot-data=true (default): Writes .data files alongside audio files, outputs JSON summary\n")
fmt.Fprintf(os.Stderr, " With --dot-data=false: Outputs JSON with clustered calls only (no .data files)\n")
fmt.Fprintf(os.Stderr, "\nFilter name:\n")
fmt.Fprintf(os.Stderr, " If --filter is provided, uses that value.\n")
fmt.Fprintf(os.Stderr, " Otherwise, parses from CSV filename: prefix_filter_date.csv -> filter\n")
fmt.Fprintf(os.Stderr, " Example: predsST_opensoundscape-kiwi-1.2_2025-11-12.csv -> opensoundscape-kiwi-1.2\n")
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " # Write .data files (default)\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv predictions.csv\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " # JSON output only (no .data files)\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv predictions.csv --dot-data=false > calls.json\n")
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, " # Override filter name\n")
fmt.Fprintf(os.Stderr, " skraak calls from-preds --csv preds.csv --filter my-custom-filter\n")
}
if err := fs.Parse(args); err != nil {
os.Exit(1)
}
if *csvPath == "" {
fmt.Fprintf(os.Stderr, "Error: --csv is required\n\n")
fs.Usage()
os.Exit(1)
}
filterName := *filter
if filterName == "" {
filterName = tools.ParseFilterFromFilename(*csvPath)
if filterName == "" {
fmt.Fprintf(os.Stderr, "Error: Could not parse filter from filename. Use --filter flag.\n")
fmt.Fprintf(os.Stderr, "Expected format: prefix_filter_date.csv (e.g., predsST_opensoundscape-kiwi-1.2_2025-11-12.csv)\n")
os.Exit(1)
}
}
input := tools.CallsFromPredsInput{
CSVPath: *csvPath,
Filter: filterName,
WriteDotData: *dotData,
ProgressHandler: func(processed, total int, message string) {
if total > 0 {
percent := float64(processed) / float64(total) * 100
fmt.Fprintf(os.Stderr, "\rProcessing WAV files: %d/%d (%.0f%%)", processed, total, percent)
if processed == total {
fmt.Fprintf(os.Stderr, "\n")
}
}
},
}
if *dotData {
fmt.Fprintf(os.Stderr, "Extracting calls from predictions: %s\n", *csvPath)
fmt.Fprintf(os.Stderr, "Filter: %s\n", filterName)
fmt.Fprintf(os.Stderr, "Writing .data files: enabled\n")
} else {
fmt.Fprintf(os.Stderr, "Extracting calls from predictions: %s\n", *csvPath)
fmt.Fprintf(os.Stderr, "Filter: %s\n", filterName)
fmt.Fprintf(os.Stderr, "Writing .data files: disabled (--dot-data=false)\n")
}
output, err := tools.CallsFromPreds(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Found %d clustered calls across %d species\n",
output.TotalCalls, len(output.SpeciesCount))
fmt.Fprintf(os.Stderr, "Clip duration: %.1fs, Gap threshold: %.1fs\n",
output.ClipDuration, output.GapThreshold)
if *dotData {
fmt.Fprintf(os.Stderr, "Data files written: %d, skipped: %d\n",
output.DataFilesWritten, output.DataFilesSkipped)
}
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
enc.Encode(output)
}
func runCallsShowImages(args []string) {
fs := flag.NewFlagSet("calls show-images", flag.ExitOnError)
filePath := fs.String("file", "", "Path to .data file (required)")
color := fs.Bool("color", false, "Apply L4 colormap (default: false, grayscale)")
imgDims := fs.Int("img-dims", 0, "Spectrogram size in pixels (224-448, default 448)")
sixel := fs.Bool("sixel", false, "Use sixel graphics protocol (default: kitty)")
iterm := fs.Bool("iterm", false, "Use iTerm2 inline image protocol")
fs.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: skraak calls show-images [options]\n\n")
fmt.Fprintf(os.Stderr, "Display spectrogram images for each segment in a .data file.\n")
fmt.Fprintf(os.Stderr, "Images are output using the Kitty graphics protocol (or Sixel with --sixel, iTerm2 with --iterm).\n\n")
fmt.Fprintf(os.Stderr, "Options:\n")
fs.PrintDefaults()
fmt.Fprintf(os.Stderr, "\nExamples:\n")
fmt.Fprintf(os.Stderr, " skraak calls show-images --file recording.wav.data\n")
fmt.Fprintf(os.Stderr, " skraak calls show-images --file recording.wav.data --color\n")
}
if err := fs.Parse(args); err != nil {
os.Exit(1)
}
if *filePath == "" {
fmt.Fprintf(os.Stderr, "Error: --file is required\n\n")
fs.Usage()
os.Exit(1)
}
input := tools.CallsShowImagesInput{
DataFilePath: *filePath,
Color: *color,
ImageSize: *imgDims,
Sixel: *sixel,
ITerm: *iterm,
}
fmt.Fprintf(os.Stderr, "Showing spectrogram images for: %s\n", *filePath)
if *color {
fmt.Fprintf(os.Stderr, "Color: L4 colormap (Black-Red-Yellow)\n")
}
output, err := tools.CallsShowImages(input)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
fmt.Fprintf(os.Stderr, "Displayed %d segment(s) from %s\n", output.SegmentsShown, output.WavFile)
}