tidy up lat lng timezone api for calls clip cmd
Dependencies
- [2]
GPQSOVBPcyclo complexity over 25 - [3]
YUIQQPXYremoved --wav-only from calls clip cmd - [4]
KZKLAINJrun out of space on nest, cleaned out - [5]
KLUEQ6X5cyclo 21+ - [6]
GVOVKH5Rmore cyclo refactoring - [7]
2P27XV3Dfixed cyclo over 30
Change contents
- edit in tools/calls_clip.go at line 10
"strconv" - replacement in tools/calls_clip.go at line 29
Night bool `json:"night"`Day bool `json:"day"`Lat float64 `json:"lat"`Lng float64 `json:"lng"`Timezone string `json:"timezone"`Night bool `json:"night"`Day bool `json:"day"`Location string `json:"location,omitempty"` - edit in tools/calls_clip.go at line 71
// Parse location into lat/lng/timezonevar lat, lng float64var timezone stringif input.Location != "" {var err errorlat, lng, timezone, err = parseLocation(input.Location)if err != nil {output.Errors = append(output.Errors, err.Error())return output, err}} - replacement in tools/calls_clip.go at line 85
processFilesSequential(&output, filePaths, input, speciesName, callType, imgSize)processFilesSequential(&output, filePaths, input, speciesName, callType, imgSize, lat, lng, timezone) - replacement in tools/calls_clip.go at line 87
processFilesParallel(&output, filePaths, input, speciesName, callType, imgSize)processFilesParallel(&output, filePaths, input, speciesName, callType, imgSize, lat, lng, timezone) - edit in tools/calls_clip.go at line 125
}// parseLocation parses a "lat,lng[,timezone]" string into its components.// Timezone is optional and defaults to "" (UTC).func parseLocation(location string) (lat, lng float64, timezone string, err error) {parts := strings.Split(location, ",")if len(parts) < 2 || len(parts) > 3 {return 0, 0, "", fmt.Errorf("--location must be \"lat,lng\" or \"lat,lng,timezone\" (got %d parts)", len(parts))}lat, err = strconv.ParseFloat(strings.TrimSpace(parts[0]), 64)if err != nil {return 0, 0, "", fmt.Errorf("--location: invalid latitude: %s", parts[0])}lng, err = strconv.ParseFloat(strings.TrimSpace(parts[1]), 64)if err != nil {return 0, 0, "", fmt.Errorf("--location: invalid longitude: %s", parts[1])}if len(parts) == 3 {timezone = strings.TrimSpace(parts[2])}return lat, lng, timezone, nil - replacement in tools/calls_clip.go at line 149
func processFilesSequential(output *CallsClipOutput, filePaths []string, input CallsClipInput, speciesName, callType string, imgSize int) {func processFilesSequential(output *CallsClipOutput, filePaths []string, input CallsClipInput, speciesName, callType string, imgSize int, lat, lng float64, timezone string) { - replacement in tools/calls_clip.go at line 151
clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.Night, input.Day, input.Lat, input.Lng, input.Timezone)clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.Night, input.Day, lat, lng, timezone) - replacement in tools/calls_clip.go at line 157
func processFilesParallel(output *CallsClipOutput, filePaths []string, input CallsClipInput, speciesName, callType string, imgSize int) {func processFilesParallel(output *CallsClipOutput, filePaths []string, input CallsClipInput, speciesName, callType string, imgSize int, lat, lng float64, timezone string) { - replacement in tools/calls_clip.go at line 172
clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.Night, input.Day, input.Lat, input.Lng, input.Timezone)clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.Night, input.Day, lat, lng, timezone) - edit in cmd/calls_clip.go at line 41
// nextFloat parses the next argument as a float64, or exits with an error.func (p *clipArgParser) nextFloat(flag string) float64 {s := p.nextValue(flag)v, err := strconv.ParseFloat(s, 64)if err != nil {fmt.Fprintf(os.Stderr, "Error: %s must be a number\n", flag)os.Exit(1)}return v} - replacement in cmd/calls_clip.go at line 63
fmt.Fprintf(os.Stderr, " --night Only clip recordings made during solar night (requires --lat and --lng)\n")fmt.Fprintf(os.Stderr, " --day Only clip recordings made during solar day (requires --lat and --lng)\n")fmt.Fprintf(os.Stderr, " --lat <float> Latitude in decimal degrees (required with --night or --day)\n")fmt.Fprintf(os.Stderr, " --lng <float> Longitude in decimal degrees (required with --night or --day)\n")fmt.Fprintf(os.Stderr, " --timezone <zone> IANA timezone ID (e.g. Pacific/Auckland). Required for non-AudioMoth\n")fmt.Fprintf(os.Stderr, " recorders whose filenames embed local time (e.g. DOC AR4).\n")fmt.Fprintf(os.Stderr, " AudioMoth files embed a UTC timestamp in the WAV comment, so\n")fmt.Fprintf(os.Stderr, " --timezone is not needed for AudioMoth data.\n")fmt.Fprintf(os.Stderr, " --night Only clip recordings made during solar night (requires --location)\n")fmt.Fprintf(os.Stderr, " --day Only clip recordings made during solar day (requires --location)\n")fmt.Fprintf(os.Stderr, " --location <lat,lng[,tz]> GPS coordinates and optional IANA timezone\n")fmt.Fprintf(os.Stderr, " e.g. --location \"-36.85,174.76\" or --location \"-36.85,174.76,Pacific/Auckland\"\n")fmt.Fprintf(os.Stderr, " Required with --night or --day. Timezone defaults to UTC.\n")fmt.Fprintf(os.Stderr, " Not needed for AudioMoth data (UTC from WAV comment).\n") - edit in cmd/calls_clip.go at line 91
timezone string - replacement in cmd/calls_clip.go at line 96
lat float64lng float64latSet boollngSet boollocation string - replacement in cmd/calls_clip.go at line 136[4.16413]→[4.1139273:1139289](∅→∅),[4.1139273]→[4.1139273:1139289](∅→∅),[4.1139289]→[4.27912:27959](∅→∅),[4.27959]→[4.1139583:1139599](∅→∅),[4.1139583]→[4.1139583:1139599](∅→∅),[4.1139599]→[4.27960:28007](∅→∅),[4.28007]→[4.1139893:1139914](∅→∅),[4.1139893]→[4.1139893:1139914](∅→∅),[4.1139914]→[4.28008:28041](∅→∅)
case "--lat":f.lat = p.nextFloat(arg)f.latSet = truecase "--lng":f.lng = p.nextFloat(arg)f.lngSet = truecase "--timezone":f.timezone = p.nextValue(arg)case "--location":f.location = p.nextUniqueValue(arg, f.location) - replacement in cmd/calls_clip.go at line 175[4.1140845]→[4.28265:28318](∅→∅),[4.28318]→[4.1140890:1140973](∅→∅),[4.1140890]→[4.1140890:1140973](∅→∅)
if (f.night || f.day) && (!f.latSet || !f.lngSet) {fmt.Fprintf(os.Stderr, "Error: --night/--day requires both --lat and --lng\n\n")if (f.night || f.day) && f.location == "" {fmt.Fprintf(os.Stderr, "Error: --night/--day requires --location\n\n") - replacement in cmd/calls_clip.go at line 211
Lat: f.lat,Lng: f.lng,Timezone: f.timezone,Location: f.location, - edit in CHANGELOG.md at line 4
## [2026-05-05] `calls clip`: replace --lat/--lng/--timezone with --location; remove --wav-only**Breaking CLI change.** Two flag changes to `skraak calls clip`:1. **`--lat`, `--lng`, `--timezone` → `--location`** — The three GPS/timezoneflags are replaced by a single `--location "lat,lng[,timezone]"` flag.Timezone is optional (defaults to UTC; not needed for AudioMoth).This makes invalid states unrepresentable (you can't pass lat without lng).Before:```bashskraak calls clip --folder ./data --output ./clips --prefix kiwi \--species Kiwi --night --lat -40.85 --lng 172.81 --timezone Pacific/Auckland```After:```bashskraak calls clip --folder ./data --output ./clips --prefix kiwi \--species Kiwi --night --location "-40.85,172.81,Pacific/Auckland"``` - edit in CHANGELOG.md at line 25
2. **`--wav-only` removed** — This flag skipped spectrogram PNG generation.Removed as unnecessary; the tool's primary purpose is generating trainingdata (PNG+WAV pairs).**Files changed:**- `cmd/calls_clip.go` — flag parsing, validation, usage text- `tools/calls_clip.go` — `CallsClipInput` struct, `parseLocation` helper,removed `wavOnly` from function signatures, unwrapped spectrogram conditional- `tools/calls_clip_bench_test.go` — removed `BenchmarkFullPipelineWavOnly`