R4Q43KYCGC5QDFR2NA2LVHPWFPPAFTWTQZXNHSAEUVOQXRKGVSQAC UE4TOOVXPEWPYVE5UEC6OTKYDUDVBTRV22HNOZ4XES2PWCBT24YAC MF6A3QKQ6SU7PYAHJOO7YFCRB47DO7FHRA7OIZ7KHQJYJR77PAIQC 7LEKFNNPRYCWKDC5OWOBACWFB4WY3PU3HVLR5DGFMEUG3BJABBHQC GQ2FYLG2VGDDKQ7TJTCFM2XPLJOAX2N737OZWPRCNKSFIH7YJ6EQC SB4FZEB6ZLUHQNM3M76OGNNJY6THOF55S6JO6Q7IGXWE7OA7INFAC 3QTZPFWEDJVI4FKNCJFOAEHTI5C7P2NQANRIW7D54RHXKETNEYLQC 4DKGEM6MTXFJ4LET56PNJR7KPDDTALHWKX7EMXXQHCRE7SBMYHIAC 3ZSBMHZBDIR3SABGTLQVBGMC2YQCRM4XNVDYGSASBF6FXYJ4J74QC 7CC2YVZXAIUNWXNNVIO5KOZZFDQQLESFO72SGEDP2C4OZXAWO4KQC Y5RSXHAZFGNPQ26DRXTCHJV4Y4EZLEGFC2FBRBI5MCMOOYCB7CMAC VYN52YS5VIYJX2QSI7DKU2TL5W6TCHLMXNEAZC6IHXQ6O2FRAZNQC TTJZWSWFENRMFQGSEEC4DKCLPFTDWJR5XHUGKEE34HT2RDGJV52AC GXVVTHNXT2IZPR4OB77VMU6GXFEA5TUFZ2MHMA5ASU2DSTFPLDLQC 77NADKVL6Q6KOSJBVFFRNURE6CCKWON6YBEB7B2GTKI5GJOOPXBAC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC File string `json:"file" jsonschema:"Path to .data file (required if no folder)"`Folder string `json:"folder" jsonschema:"Path to folder containing .data files (required if no file)"`Output string `json:"output" jsonschema:"required,Output folder for generated clips"`Prefix string `json:"prefix" jsonschema:"required,Prefix for output filenames"`Filter string `json:"filter" jsonschema:"Filter by ML model name"`Species string `json:"species" jsonschema:"Filter by species, optionally with calltype (e.g. Kiwi, Kiwi+Duet)"`Certainty int `json:"certainty" jsonschema:"Filter by certainty value (0-100, -1 for no filter)"`Size int `json:"size" jsonschema:"Spectrogram image size in pixels (224-896, default 224)"`Color bool `json:"color" jsonschema:"Apply L4 colormap to spectrogram (default: grayscale)"`WavOnly bool `json:"wav_only" jsonschema:"Generate only WAV clips, skip spectrogram PNG generation"`
File string `json:"file" jsonschema:"Path to .data file (required if no folder)"`Folder string `json:"folder" jsonschema:"Path to folder containing .data files (required if no file)"`Output string `json:"output" jsonschema:"required,Output folder for generated clips"`Prefix string `json:"prefix" jsonschema:"required,Prefix for output filenames"`Filter string `json:"filter" jsonschema:"Filter by ML model name"`Species string `json:"species" jsonschema:"Filter by species, optionally with calltype (e.g. Kiwi, Kiwi+Duet)"`Certainty int `json:"certainty" jsonschema:"Filter by certainty value (0-100, -1 for no filter)"`Size int `json:"size" jsonschema:"Spectrogram image size in pixels (224-896, default 224)"`Color bool `json:"color" jsonschema:"Apply L4 colormap to spectrogram (default: grayscale)"`WavOnly bool `json:"wav_only" jsonschema:"Generate only WAV clips, skip spectrogram PNG generation"`Night bool `json:"night" jsonschema:"Only clip recordings made during solar night (requires Lat and Lng)"`Lat float64 `json:"lat" jsonschema:"Latitude for night filtering (required with Night)"`Lng float64 `json:"lng" jsonschema:"Longitude for night filtering (required with Night)"`Timezone string `json:"timezone" jsonschema:"IANA timezone ID for filename timestamps (e.g. Pacific/Auckland). Required for non-AudioMoth recorders when using --night"`
clips, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.WavOnly)
clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.WavOnly, input.Night, input.Lat, input.Lng, input.Timezone)
clips, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.WavOnly)results <- fileResult{clips: clips, errs: errs}
clips, skipped, errs := processFile(dataPath, input.Output, input.Prefix, input.Filter, speciesName, callType, input.Certainty, imgSize, input.Color, input.WavOnly, input.Night, input.Lat, input.Lng, input.Timezone)results <- fileResult{clips: clips, skipped: skipped, errs: errs}
// processFile processes a single .data file and returns generated clips and errorsfunc processFile(dataPath, outputDir, prefix, filter, speciesName, callType string, certainty, imgSize int, color, wavOnly bool) ([]string, []string) {
// processFile processes a single .data file and returns generated clips, night-skipped count, and errorsfunc processFile(dataPath, outputDir, prefix, filter, speciesName, callType string, certainty, imgSize int, color, wavOnly, night bool, lat, lng float64, timezone string) ([]string, int, []string) {
return nil, nil // No matches, not an error
return nil, 0, nil // No matches, not an error}// Night filter: check WAV header only (cheaper than reading full audio).// Skip daytime recordings before paying the cost of ReadWAVSamples.if night {result, err := IsNight(IsNightInput{FilePath: wavPath,Lat: lat,Lng: lng,Timezone: timezone,})if err != nil {fmt.Fprintf(os.Stderr, "warning: skipping %s (isnight error: %v)\n", wavPath, err)return nil, 0, nil}if !result.SolarNight {fmt.Fprintf(os.Stderr, "skipped (daytime): %s\n", wavPath)return nil, 1, nil}
fmt.Fprintf(os.Stderr, " --night Only clip recordings made during solar night (requires --lat and --lng)\n")fmt.Fprintf(os.Stderr, " --lat <float> Latitude in decimal degrees (required with --night)\n")fmt.Fprintf(os.Stderr, " --lng <float> Longitude in decimal degrees (required with --night)\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")
case "--lng":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --lng requires a value\n")os.Exit(1)}v, err := strconv.ParseFloat(args[i+1], 64)if err != nil {fmt.Fprintf(os.Stderr, "Error: --lng must be a number\n")os.Exit(1)}lng = vlngSet = truei += 2case "--timezone":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --timezone requires a value\n")os.Exit(1)}timezone = args[i+1]i += 2
## [2026-04-18] `calls clip --night`: filter to solar-night recordings onlyAdds `--night`, `--lat`, `--lng`, and `--timezone` flags to `skraak calls clip`.When `--night` is set, each recording is checked against solar sunrise/sunset atthe given coordinates before its audio is loaded — daytime files are skippedentirely, saving the cost of reading WAV audio for files that would produce nouseful clips.`--timezone` is not needed for AudioMoth recorders (timestamp comes from the WAVcomment in UTC). It is required for recorders that embed **local time** in thefilename (e.g. DOC AR4) — without it the filename is parsed as UTC and`solar_night` will be wrong. Pass `--timezone Pacific/Auckland` or theappropriate IANA zone.The JSON output gains a `night_skipped` field (omitted when 0) counting how manyfiles were filtered out. Skipped filenames are logged to stderr.```bashskraak calls clip --folder ./data --output ./clips --prefix kiwi \--species Kiwi --night --lat -40.85 --lng 172.81# Non-AudioMoth (DOC AR4, filename in local time):skraak calls clip --folder ./data --output ./clips --prefix kiwi \--species Kiwi --night --lat -40.85 --lng 172.81 --timezone Pacific/Auckland```**Files changed:**- `tools/calls_clip.go` — `CallsClipInput` (Night/Lat/Lng/Timezone fields),`CallsClipOutput` (NightSkipped field), `processFile` night-filter block.- `cmd/calls_clip.go` — flag parsing, `--night` requires lat/lng validation,updated usage/help text.