77NADKVL6Q6KOSJBVFFRNURE6CCKWON6YBEB7B2GTKI5GJOOPXBAC B6H6NFYKNJIF4SSBKRAW6GTDWQ25IB27JWAFIYOKGS2EFNO2QSCQC IFVRAERTCCDICNTYTG3TX2WASB6RXQQEJWWXQMQZJSQDQ3HLE5OQC NKQAT3RE4IBIWXVMI5LJUINDPHTANNMORZ5N2JFA4AN6UUB72KGAC IZEEEQS5ES4AYNGZ7RE54QYUBREN6NA6PML242JR5N7EKVDCKT3AC KS7LFF6M5Y6UGBBA7G63BJRR5XS4P4R3PSZPG752NSGZ3Z6GY72QC 7NS27QXZMVTZBK4VPMYL5IKGSTTAWR6NDG5SOVITNX44VNIRZPMAC GXVVTHNXT2IZPR4OB77VMU6GXFEA5TUFZ2MHMA5ASU2DSTFPLDLQC 2GJMZ6YA6OPHNS5KFFFI6POQ2BJ33SSS3NIPXYBFTJSN4BZBVEVAC CCML2MSKZBXNILUOG7526T6PMCCNTFZ4B6SBAXUMRG7PCOEFG4EQC W3A2EECCD23SVHJZN6MXPH2PAVFHH5CNFD2XHPQRRW6M4GUTG3FAC AMM2YGFLZLZJJHPCTIKMZEXKMHM6ARTPPZVDRPH2CIPCDM2BDQ6QC package toolsimport ("fmt""strings""time""github.com/sixdouglas/suncalc""skraak/utils")// IsNightInput defines the input parameters for the isnight tooltype IsNightInput struct {FilePath string `json:"file_path" jsonschema:"required,Path to WAV file"`Lat float64 `json:"lat" jsonschema:"required,Latitude in decimal degrees"`Lng float64 `json:"lng" jsonschema:"required,Longitude in decimal degrees"`Timezone string `json:"timezone,omitempty" jsonschema:"IANA timezone ID (e.g. Pacific/Auckland). Required for filename timestamps without AudioMoth"`}// IsNightOutput defines the output structure for the isnight tooltype IsNightOutput struct {FilePath string `json:"file_path"`TimestampUTC string `json:"timestamp_utc"`SolarNight bool `json:"solar_night"`CivilNight bool `json:"civil_night"`MoonPhase float64 `json:"moon_phase"`DurationSec float64 `json:"duration_seconds"`TimestampSrc string `json:"timestamp_source"`MidpointUTC string `json:"midpoint_utc"`SunriseUTC string `json:"sunrise_utc,omitempty"`SunsetUTC string `json:"sunset_utc,omitempty"`DawnUTC string `json:"dawn_utc,omitempty"`DuskUTC string `json:"dusk_utc,omitempty"`}// IsNight determines if a WAV file was recorded at night based on its// metadata timestamp and the given GPS coordinates.//// Timestamp resolution order:// 1. AudioMoth comment (timezone embedded)// 2. Filename timestamp + timezone offset (requires --timezone)// 3. File modification time (system local time)func IsNight(input IsNightInput) (IsNightOutput, error) {var output IsNightOutput// Step 1: Parse WAV headermetadata, err := utils.ParseWAVHeader(input.FilePath)if err != nil {return output, fmt.Errorf("WAV header parsing failed: %w", err)}output.DurationSec = metadata.Duration// Step 2: Resolve timestamp (use file mod time as fallback)tsResult, err := utils.ResolveTimestamp(metadata, input.FilePath, input.Timezone, true)if err != nil {return output, fmt.Errorf("cannot determine recording timestamp: %w", err)}// Determine timestamp source labeltsSource := "file_mod_time"if tsResult.IsAudioMoth {tsSource = "audiomoth_comment"} else if utils.HasTimestampFilename(input.FilePath) {tsSource = "filename"}// Step 3: Calculate astronomical data using recording midpointastroData := utils.CalculateAstronomicalData(tsResult.Timestamp.UTC(),metadata.Duration,input.Lat,input.Lng,)// Step 4: Get sun event times for informational outputmidpoint := utils.CalculateMidpointTime(tsResult.Timestamp.UTC(), metadata.Duration)sunTimes := suncalc.GetTimes(midpoint, input.Lat, input.Lng)output.FilePath = input.FilePathoutput.TimestampUTC = tsResult.Timestamp.UTC().Format(time.RFC3339)output.SolarNight = astroData.SolarNightoutput.CivilNight = astroData.CivilNightoutput.MoonPhase = astroData.MoonPhaseoutput.TimestampSrc = tsSourceoutput.MidpointUTC = midpoint.Format(time.RFC3339)if sr, ok := sunTimes[suncalc.Sunrise]; ok && !sr.Value.IsZero() {output.SunriseUTC = sr.Value.UTC().Format(time.RFC3339)}if ss, ok := sunTimes[suncalc.Sunset]; ok && !ss.Value.IsZero() {output.SunsetUTC = ss.Value.UTC().Format(time.RFC3339)}if d, ok := sunTimes[suncalc.Dawn]; ok && !d.Value.IsZero() {output.DawnUTC = d.Value.UTC().Format(time.RFC3339)}if dk, ok := sunTimes[suncalc.Dusk]; ok && !dk.Value.IsZero() {output.DuskUTC = dk.Value.UTC().Format(time.RFC3339)}return output, nil}// String returns a human-readable summary of the isnight resultfunc (o IsNightOutput) String() string {var sb strings.Buildersb.WriteString(fmt.Sprintf("File: %s\n", o.FilePath))sb.WriteString(fmt.Sprintf("Timestamp (UTC): %s\n", o.TimestampUTC))sb.WriteString(fmt.Sprintf("Midpoint (UTC): %s\n", o.MidpointUTC))sb.WriteString(fmt.Sprintf("Duration: %.1f seconds\n", o.DurationSec))sb.WriteString(fmt.Sprintf("Source: %s\n", o.TimestampSrc))sb.WriteString(fmt.Sprintf("Solar night: %v\n", o.SolarNight))sb.WriteString(fmt.Sprintf("Civil night: %v\n", o.CivilNight))sb.WriteString(fmt.Sprintf("Moon phase: %.2f\n", o.MoonPhase))if o.SunriseUTC != "" {sb.WriteString(fmt.Sprintf("Sunrise (UTC): %s\n", o.SunriseUTC))}if o.SunsetUTC != "" {sb.WriteString(fmt.Sprintf("Sunset (UTC): %s\n", o.SunsetUTC))}if o.DawnUTC != "" {sb.WriteString(fmt.Sprintf("Dawn (UTC): %s\n", o.DawnUTC))}if o.DuskUTC != "" {sb.WriteString(fmt.Sprintf("Dusk (UTC): %s\n", o.DuskUTC))}return sb.String()}
package cmdimport ("encoding/json""flag""fmt""os""skraak/tools")// RunIsNight handles the "isnight" subcommandfunc RunIsNight(args []string) {fs := flag.NewFlagSet("isnight", flag.ExitOnError)filePath := fs.String("file", "", "Path to WAV file (required)")lat := fs.Float64("lat", 0, "Latitude in decimal degrees (required)")lng := fs.Float64("lng", 0, "Longitude in decimal degrees (required)")timezone := fs.String("timezone", "UTC", "IANA timezone ID for filename timestamps (e.g. Pacific/Auckland)")brief := fs.Bool("brief", false, "Output only file_path and solar_night (saves tokens for batch use)")fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak isnight --file <path> --lat <lat> --lng <lng> [--timezone <tz>] [--brief]\n\n")fmt.Fprintf(os.Stderr, "Determine if a WAV file was recorded at night based on file metadata and GPS coordinates.\n\n")fmt.Fprintf(os.Stderr, "Uses the recording midpoint (not start time) for astronomical calculations.\n")fmt.Fprintf(os.Stderr, "Timestamp resolution: AudioMoth comment → filename → file modification time.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak isnight --file recording.wav --lat -36.85 --lng 174.76\n")fmt.Fprintf(os.Stderr, " skraak isnight --file recording.wav --lat -36.85 --lng 174.76 --timezone Pacific/Auckland\n")fmt.Fprintf(os.Stderr, " skraak isnight --file recording.wav --lat 51.51 --lng -0.13 | jq '.solar_night'\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)}if *lat == 0 && *lng == 0 {fmt.Fprintf(os.Stderr, "Error: --lat and --lng are required\n\n")fs.Usage()os.Exit(1)}output, err := tools.IsNight(tools.IsNightInput{FilePath: *filePath,Lat: *lat,Lng: *lng,Timezone: *timezone,})if err != nil {fmt.Fprintf(os.Stderr, "Error: %v\n", err)os.Exit(1)}if *brief {enc := json.NewEncoder(os.Stdout)enc.Encode(map[string]any{"file_path": output.FilePath,"solar_night": output.SolarNight,})} else {enc := json.NewEncoder(os.Stdout)enc.SetIndent("", " ")enc.Encode(output)}}
./skraak isnight --file recording.wav --lat -36.85 --lng 174.76 # Was it night when recorded?./skraak isnight --file recording.wav --lat -36.85 --lng 174.76 --brief # Just file_path + solar_night./skraak isnight --file recording.wav --lat -36.85 --lng 174.76 --timezone Pacific/Auckland # Non-UTC timezone
**`isnight`** — Night detection for bioacoustic recordings. Determines if a WAV file was recorded at night (between sunset and sunrise) at the given GPS coordinates. The recording timestamp is read from the WAV file metadata, not from the filename — this works reliably because bioacoustic recorders (AudioMoth, BAR-LT, Song Meter, etc.) embed an accurate timestamp in the WAV header at the time of recording. AudioMoth comments are parsed automatically including the embedded UTC offset. For non-AudioMoth files without a recognized filename pattern, the timestamp falls back to the file modification time. Use `--brief` for batch/agent use to return only `file_path` and `solar_night`.
Adds a standalone CLI command to check if a WAV file was recorded at night,without needing a database connection.```skraak isnight --file recording.wav --lat -36.85 --lng 174.76```Determines the recording timestamp from WAV metadata (AudioMoth comment →filename pattern → file modification time), then calculates sunrise/sunsetat the given GPS coordinates using the recording midpoint. Returns JSON with` solar_night`, `civil_night`, `moon_phase`, and sun event times.Optional `--timezone` flag (default UTC) is used for filename-based timestamps;AudioMoth comments embed their own timezone. Use `--brief` for batch/agentuse to return only `file_path` and `solar_night` (compact JSON, saves tokens).**Files added:**- `tools/isnight.go` — IsNight tool (MCP-free core logic)- `cmd/isnight.go` — CLI command (flags → tool → JSON output)**Files changed:**- `main.go` — Register `isnight` command and usage text