YF5AJWFBEKNWN556IXJCCLMHPOYNQM2IMNBIGQ3ABI6IR5NXCV7QC DHIPFBFPF4F7SLMMBVHO4TUFPLFYWY4KGOYZEHOQGUQNZELRSW3AC KZKLAINJJWZ64T5MUZT34LJVQIKBTKZ6EJGD7C7TTSSDGCHEDPMAC ZOSYO3IBH5SCB27UP642O2SGCWQ7ZTQJSZ3PXKZVFQP72S34U3IQC GE3VNRXLBCRRCW3R5CFGYKCAZE2WCQNTKTUW4BKV5GEN3NZKWXTAC QFPEKXL5OUKLT4WECMATSOHWYM24QPHKS6WZAAI5BAEQSAGAK6CQC GVOVKH5R27K75VXGSZCP3X62FGNCSMDVFEKLR3LFXERFB54CHTUQC YE6BZJUKQ7VMYEKKI3WSKTZEBR5NWUUDIN6PGE4W7OTPIY5N3NJQC SMWSHUOWFAP4GURQK3IBTSCIASVETCKBJWZLZKHH4WVOCRWW3BRAC 2P27XV3DGJCRA4SNJENCJYZLPR2XWZMTY7CGYYSJOY4UMDVVO25AC fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak xxhash --file <path>\n\n")fmt.Fprintf(os.Stderr, "Compute XXH64 hash of a file (same format stored in database).\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak xxhash --file recording.wav\n")fmt.Fprintf(os.Stderr, " skraak xxhash --file /path/to/audio.wav | jq '.hash'\n")}
fs.Usage = usagePrinter(fs,"skraak xxhash --file <path>","Compute XXH64 hash of a file (same format stored in database).","skraak xxhash --file recording.wav","skraak xxhash --file /path/to/audio.wav | jq '.hash'",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak time\n\n")fmt.Fprintf(os.Stderr, "Get the current system time with timezone information.\n\n")fmt.Fprintf(os.Stderr, "Examples:\n")fmt.Fprintf(os.Stderr, " skraak time\n")fmt.Fprintf(os.Stderr, " skraak time | jq '.iso'\n")}
fs.Usage = usagePrinter(fs,"skraak time","Get the current system time with timezone information.","skraak time","skraak time | jq '.iso'",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak sql --db <path> [options] <query>\n\n")fmt.Fprintf(os.Stderr, "Execute a SQL SELECT query against the database.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak sql --db ./db/skraak.duckdb \"SELECT COUNT(*) FROM file WHERE active = true\"\n")fmt.Fprintf(os.Stderr, " skraak sql --db ./db/skraak.duckdb --limit 10 \"SELECT * FROM dataset\"\n")}
fs.Usage = usagePrinter(fs,"skraak sql --db <path> [options] <query>","Execute a SQL SELECT query against the database.","skraak sql --db ./db/skraak.duckdb \"SELECT COUNT(*) FROM file WHERE active = true\"","skraak sql --db ./db/skraak.duckdb --limit 10 \"SELECT * FROM dataset\"",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak replay events [options]\n\n")fmt.Fprintf(os.Stderr, "Replay event log into database.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak replay events --db ./backup.duckdb --log ./events.jsonl\n")fmt.Fprintf(os.Stderr, " skraak replay events --db ./backup.duckdb --log ./events.jsonl --dry-run\n")fmt.Fprintf(os.Stderr, " skraak replay events --db ./backup.duckdb --log ./events.jsonl --last 10\n")}
fs.Usage = usagePrinter(fs,"skraak replay events [options]","Replay event log into database.","skraak replay events --db ./backup.duckdb --log ./events.jsonl","skraak replay events --db ./backup.duckdb --log ./events.jsonl --dry-run","skraak replay events --db ./backup.duckdb --log ./events.jsonl --last 10",)
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")}
fs.Usage = usagePrinter(fs,"skraak prepend --folder <path> --prefix <string> [--recursive] [--dry-run]","Rename files by prepending a prefix.\n\n"+"Target files:\n"+" - *.wav, *.WAV (must start with datestring YYYYMMDD_HHMMSS)\n"+" - *.wav.data, *.WAV.data (must start with datestring YYYYMMDD_HHMMSS)\n"+" - log.txt (exact name, always renamed)","skraak prepend --folder ./recordings --prefix LOC001","skraak prepend --folder ./data --prefix SITE_A --recursive","skraak prepend --folder ./test --prefix TEST --dry-run",)
if *folder == "" {fs.Usage()return fmt.Errorf("--folder is required")}if *prefix == "" {fs.Usage()return fmt.Errorf("--prefix is required")
if err := requireFlags(fs, map[string]any{"--folder": *folder, "--prefix": *prefix}); err != nil {return err
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak pattern create [options]\n\n")fmt.Fprintf(os.Stderr, "Create a new cyclic recording pattern.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak pattern create --db ./db/skraak.duckdb --record 60 --sleep 1740\n")fmt.Fprintf(os.Stderr, " # Creates 60s record / 1740s sleep = 30 min cycle\n")}
fs.Usage = usagePrinter(fs,"skraak pattern create [options]","Create a new cyclic recording pattern.","skraak pattern create --db ./db/skraak.duckdb --record 60 --sleep 1740","# Creates 60s record / 1740s sleep = 30 min cycle",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath); err != nil {return err}if err := checkNonZeroFlags(fs,struct {Name stringValue int}{"--record", *record},struct {Name stringValue int}{"--sleep", *sleep},); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--record": *record,"--sleep": *sleep,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak pattern update [options]\n\n")fmt.Fprintf(os.Stderr, "Update an existing recording pattern. Only provided fields are updated.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak pattern update --db ./db/skraak.duckdb --id pattern123 --record 30\n")}
fs.Usage = usagePrinter(fs,"skraak pattern update [options]","Update an existing recording pattern. Only provided fields are updated.","skraak pattern update --db ./db/skraak.duckdb --id pattern123 --record 30",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak metadata --file <path>\n\n")fmt.Fprintf(os.Stderr, "Extract metadata from a WAV file header.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak metadata --file recording.wav\n")fmt.Fprintf(os.Stderr, " skraak metadata --file /path/to/audio.wav | jq '.duration_seconds'\n")}
fs.Usage = usagePrinter(fs,"skraak metadata --file <path>","Extract metadata from a WAV file header.","skraak metadata --file recording.wav","skraak metadata --file /path/to/audio.wav | jq '.duration_seconds'",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak location create [options]\n\n")fmt.Fprintf(os.Stderr, "Create a new location with GPS coordinates.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak location create --db ./db/skraak.duckdb --dataset abc123 --name \"Site A\" --lat -36.85 --lon 174.76 --timezone Pacific/Auckland\n")}
fs.Usage = usagePrinter(fs,"skraak location create [options]","Create a new location with GPS coordinates.","skraak location create --db ./db/skraak.duckdb --dataset abc123 --name \"Site A\" --lat -36.85 --lon 174.76 --timezone Pacific/Auckland",)
if err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--name", *name, "--lat", *lat, "--lon", *lon, "--timezone", *tz); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--name": *name,"--lat": *lat,"--lon": *lon,"--timezone": *tz,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak location update [options]\n\n")fmt.Fprintf(os.Stderr, "Update an existing location. Only provided fields are updated.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak location update --db ./db/skraak.duckdb --id loc123 --name \"New Name\"\n")}
fs.Usage = usagePrinter(fs,"skraak location update [options]","Update an existing location. Only provided fields are updated.","skraak location update --db ./db/skraak.duckdb --id loc123 --name \"New Name\"",)
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")}
fs.Usage = usagePrinter(fs,"skraak isnight --file <path> --lat <lat> --lng <lng> [--timezone <tz>] [--brief]","Determine if a WAV file was recorded at night based on file metadata and GPS coordinates.\n\n"+"Uses the recording midpoint (not start time) for astronomical calculations.\n"+"Timestamp resolution: AudioMoth comment → filename → file modification time.","skraak isnight --file recording.wav --lat -36.85 --lng 174.76","skraak isnight --file recording.wav --lat -36.85 --lng 174.76 --timezone Pacific/Auckland","skraak isnight --file recording.wav --lat 51.51 --lng -0.13 | jq '.solar_night'",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak import bulk [options]\n\n")fmt.Fprintf(os.Stderr, "Bulk import WAV files across multiple locations/clusters using a CSV file.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nCSV format: location_name,location_id,directory_path,date_range,sample_rate,file_count\n")fmt.Fprintf(os.Stderr, "\nMonitor progress: tail -f <log-file>\n")}
fs.Usage = usagePrinter(fs,"skraak import bulk [options]","Bulk import WAV files across multiple locations/clusters using a CSV file.\n\n"+"CSV format: location_name,location_id,directory_path,date_range,sample_rate,file_count\n\n"+"Monitor progress: tail -f <log-file>",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--csv", *csvPath, "--log", *logPath); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--csv": *csvPath,"--log": *logPath,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak import file [options]\n\n")fmt.Fprintf(os.Stderr, "Import a single WAV file into the database.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak import file --db ./db/skraak.duckdb --dataset abc123 --location loc456 --cluster clust789 --file /path/to/file.wav\n")}
fs.Usage = usagePrinter(fs,"skraak import file [options]","Import a single WAV file into the database.","skraak import file --db ./db/skraak.duckdb --dataset abc123 --location loc456 --cluster clust789 --file /path/to/file.wav",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--file", *filePath); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--location": *locationID,"--cluster": *clusterID,"--file": *filePath,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak import folder [options]\n\n")fmt.Fprintf(os.Stderr, "Import all WAV files from a folder into the database.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak import folder --db ./db/skraak.duckdb --dataset abc123 --location loc456 --cluster clust789 --folder /path/to/folder\n")}
fs.Usage = usagePrinter(fs,"skraak import folder [options]","Import all WAV files from a folder into the database.","skraak import folder --db ./db/skraak.duckdb --dataset abc123 --location loc456 --cluster clust789 --folder /path/to/folder",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--location": *locationID,"--cluster": *clusterID,"--folder": *folderPath,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak import segments [options]\n\n")fmt.Fprintf(os.Stderr, "Import segments from AviaNZ .data files into the database.\n")fmt.Fprintf(os.Stderr, "Applies species/calltype mapping from JSON file.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nMapping file format:\n")fmt.Fprintf(os.Stderr, " {\n")fmt.Fprintf(os.Stderr, " \"GSK\": {\"species\": \"Roroa\", \"calltypes\": {\"Male\": \"Male - Solo\"}},\n")fmt.Fprintf(os.Stderr, " \"Don't Know\": {\"species\": \"Don't Know\"}\n")fmt.Fprintf(os.Stderr, " }\n")fmt.Fprintf(os.Stderr, "\nInvariants:\n")fmt.Fprintf(os.Stderr, " - All file hashes must already exist in database for the cluster\n")fmt.Fprintf(os.Stderr, " - All files must have no existing labels (fresh imports only)\n")fmt.Fprintf(os.Stderr, " - All filters, species, and calltypes must exist in database\n")fmt.Fprintf(os.Stderr, " - Bookmark flags are ignored (not stored in database)\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak import segments --db ./db/skraak.duckdb --dataset dset_id123 --location loc_id456 --cluster clust_id789 --folder /path/to/data --mapping mapping.json\n")}
fs.Usage = usagePrinter(fs,"skraak import segments [options]","Import segments from AviaNZ .data files into the database.\n"+"Applies species/calltype mapping from JSON file.\n\n"+"Mapping file format:\n"+" {\n"+" \"GSK\": {\"species\": \"Roroa\", \"calltypes\": {\"Male\": \"Male - Solo\"}},\n"+" \"Don't Know\": {\"species\": \"Don't Know\"}\n"+" }\n\n"+"Invariants:\n"+" - All file hashes must already exist in database for the cluster\n"+" - All files must have no existing labels (fresh imports only)\n"+" - All filters, species, and calltypes must exist in database\n"+" - Bookmark flags are ignored (not stored in database)","skraak import segments --db ./db/skraak.duckdb --dataset dset_id123 --location loc_id456 --cluster clust_id789 --folder /path/to/data --mapping mapping.json",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--cluster", *clusterID, "--folder", *folderPath, "--mapping", *mappingPath); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--location": *locationID,"--cluster": *clusterID,"--folder": *folderPath,"--mapping": *mappingPath,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak import unstructured [options]\n\n")fmt.Fprintf(os.Stderr, "Import WAV files into an unstructured dataset.\n")fmt.Fprintf(os.Stderr, "Files are stored with minimal metadata (hash, duration, sample_rate, file modification time).\n")fmt.Fprintf(os.Stderr, "No location/cluster hierarchy required.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak import unstructured --db ./db/skraak.duckdb --dataset abc123 --folder /path/to/folder\n")fmt.Fprintf(os.Stderr, " skraak import unstructured --db ./db/skraak.duckdb --dataset abc123 --folder /path/to/folder --recursive=false\n")}
fs.Usage = usagePrinter(fs,"skraak import unstructured [options]","Import WAV files into an unstructured dataset.\n"+"Files are stored with minimal metadata (hash, duration, sample_rate, file modification time).\n"+"No location/cluster hierarchy required.","skraak import unstructured --db ./db/skraak.duckdb --dataset abc123 --folder /path/to/folder","skraak import unstructured --db ./db/skraak.duckdb --dataset abc123 --folder /path/to/folder --recursive=false",)
// Validate required flagsif err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--folder", *folderPath); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--folder": *folderPath,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak export dataset --db <path> --id <dataset_id> --output <path> [options]\n\n")fmt.Fprintf(os.Stderr, "Export a dataset with all related data to a new DuckDB database.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb\n")fmt.Fprintf(os.Stderr, " skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb --dry-run\n")fmt.Fprintf(os.Stderr, " skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb --force\n")}
fs.Usage = usagePrinter(fs,"skraak export dataset --db <path> --id <dataset_id> --output <path> [options]","Export a dataset with all related data to a new DuckDB database.","skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb","skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb --dry-run","skraak export dataset --db ./db/skraak.duckdb --id abc123 --output export.duckdb --force",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak create dataset [options]\n\n")fmt.Fprintf(os.Stderr, "Create a new dataset.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak create dataset --db ./db/skraak.duckdb --name \"My Dataset\"\n")fmt.Fprintf(os.Stderr, " skraak create dataset --db ./db/skraak.duckdb --name \"Training Data\" --type train --description \"For ML training\"\n")}
fs.Usage = usagePrinter(fs,"skraak create dataset [options]","Create a new dataset.","skraak create dataset --db ./db/skraak.duckdb --name \"My Dataset\"","skraak create dataset --db ./db/skraak.duckdb --name \"Training Data\" --type train --description \"For ML training\"",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak update dataset [options]\n\n")fmt.Fprintf(os.Stderr, "Update an existing dataset. Only provided fields are updated.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak update dataset --db ./db/skraak.duckdb --id abc123 --name \"Updated Name\"\n")fmt.Fprintf(os.Stderr, " skraak update dataset --db ./db/skraak.duckdb --id abc123 --type train\n")}
fs.Usage = usagePrinter(fs,"skraak update dataset [options]","Update an existing dataset. Only provided fields are updated.","skraak update dataset --db ./db/skraak.duckdb --id abc123 --name \"Updated Name\"","skraak update dataset --db ./db/skraak.duckdb --id abc123 --type train",)
{"all present", []string{"--db", "x.duckdb", "--id", "abc"}, false, nil},{"empty value", []string{"--db", "", "--id", "abc"}, true, []string{"--db"}},{"multiple missing", []string{"--db", "", "--id", ""}, true, []string{"--db", "--id"}},{"no pairs", []string{}, false, nil},{"single missing", []string{"--name", ""}, true, []string{"--name"}},
{"all string present", map[string]any{"--db": "x.duckdb", "--id": "abc"}, false, nil},{"empty string", map[string]any{"--db": "", "--id": "abc"}, true, []string{"--db"}},{"multiple missing strings", map[string]any{"--db": "", "--id": ""}, true, []string{"--db", "--id"}},{"no flags", map[string]any{}, false, nil},{"all int non-zero", map[string]any{"--n": 5, "--m": 1}, false, nil},{"zero int", map[string]any{"--n": 0, "--m": 1}, true, []string{"--n"}},{"multiple zero ints", map[string]any{"--n": 0, "--m": 0}, true, []string{"--m", "--n"}},{"negative int is allowed", map[string]any{"--n": -1}, false, nil},{"mixed types", map[string]any{"--db": "x", "--n": 0}, true, []string{"--n"}},
func TestCheckNonZeroFlags(t *testing.T) {type pair = struct {Name stringValue int
func TestRequireFlagsUnsupportedType(t *testing.T) {err := requireFlags(silentFlagSet(), map[string]any{"--bad": 1.5})if err == nil || !strings.Contains(err.Error(), "unsupported type") {t.Fatalf("expected unsupported type error, got %v", err)
tests := []struct {name stringpairs []pairwantErr boolwantMissing []string}{{"all non-zero", []pair{{"--n", 5}, {"--m", 1}}, false, nil},{"zero value", []pair{{"--n", 0}, {"--m", 1}}, true, []string{"--n"}},{"multiple zero", []pair{{"--n", 0}, {"--m", 0}}, true, []string{"--n", "--m"}},{"empty pairs", nil, false, nil},{"negative is allowed", []pair{{"--n", -1}}, false, nil},}for _, tt := range tests {t.Run(tt.name, func(t *testing.T) {err := checkNonZeroFlags(silentFlagSet(), tt.pairs...)if (err != nil) != tt.wantErr {t.Fatalf("err=%v, wantErr=%v", err, tt.wantErr)}if tt.wantErr {for _, name := range tt.wantMissing {if !strings.Contains(err.Error(), name) {t.Errorf("err %q missing flag name %q", err.Error(), name)}}}})}
// checkFlags checks that the given flag values are non-empty strings.// Returns an error if any are empty (does not call os.Exit).// Each pair is (flagName, flagValue) — e.g. checkFlags(fs, "--db", *dbPath, "--id", *id)func checkFlags(fs *flag.FlagSet, pairs ...string) error {
// requireFlags reports an error listing any required flags whose values are// still at their zero value. Strings must be non-empty; ints must be non-zero.// Map iteration is randomised, so the returned list is sorted for stability.func requireFlags(fs *flag.FlagSet, required map[string]any) error {
for i := 0; i < len(pairs); i += 2 {if pairs[i+1] == "" {missing = append(missing, pairs[i])
for name, v := range required {switch x := v.(type) {case string:if x == "" {missing = append(missing, name)}case int:if x == 0 {missing = append(missing, name)}default:return fmt.Errorf("requireFlags: unsupported type for %s: %T", name, v)
// checkNonZeroFlags checks that the given int flag values are non-zero.// Returns an error if any are zero (does not call os.Exit).func checkNonZeroFlags(fs *flag.FlagSet, pairs ...struct {Name stringValue int}) error {var missing []stringfor _, p := range pairs {if p.Value == 0 {missing = append(missing, p.Name)
// usagePrinter returns an fs.Usage closure that prints a structured header,// the auto-generated flag table (via fs.PrintDefaults), and optional examples.// The flag table reflects the flags as registered — no manual duplication.func usagePrinter(fs *flag.FlagSet, synopsis, description string, examples ...string) func() {return func() {fmt.Fprintf(os.Stderr, "Usage: %s\n\n", synopsis)if description != "" {fmt.Fprintf(os.Stderr, "%s\n\n", description)
}if len(missing) > 0 {fs.Usage()return fmt.Errorf("missing required flags: %v", missing)
fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()if len(examples) > 0 {fmt.Fprintf(os.Stderr, "\nExamples:\n")for _, ex := range examples {fmt.Fprintf(os.Stderr, " %s\n", ex)}}
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak cluster create [options]\n\n")fmt.Fprintf(os.Stderr, "Create a new cluster for grouping recordings.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak cluster create --db ./db/skraak.duckdb --dataset abc123 --location loc456 --name \"2024-01\" --sample-rate 250000\n")}
fs.Usage = usagePrinter(fs,"skraak cluster create [options]","Create a new cluster for grouping recordings.","skraak cluster create --db ./db/skraak.duckdb --dataset abc123 --location loc456 --name \"2024-01\" --sample-rate 250000",)
if err := checkFlags(fs, "--db", *dbPath, "--dataset", *datasetID, "--location", *locationID, "--name", *name, "--sample-rate", *sampleRate); err != nil {
if err := requireFlags(fs, map[string]any{"--db": *dbPath,"--dataset": *datasetID,"--location": *locationID,"--name": *name,"--sample-rate": *sampleRate,}); err != nil {
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak cluster update [options]\n\n")fmt.Fprintf(os.Stderr, "Update an existing cluster. Only provided fields are updated.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak cluster update --db ./db/skraak.duckdb --id clust123 --name \"New Name\"\n")}
fs.Usage = usagePrinter(fs,"skraak cluster update [options]","Update an existing cluster. Only provided fields are updated.","skraak cluster update --db ./db/skraak.duckdb --id clust123 --name \"New Name\"",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak calls propagate [options]\n\n")fmt.Fprintf(os.Stderr, "Propagate verified classifications from one filter to another within a .data file\n")fmt.Fprintf(os.Stderr, "or across every .data file in a folder.\n\n")fmt.Fprintf(os.Stderr, "Only source labels with certainty=100 and matching --species are considered.\n")fmt.Fprintf(os.Stderr, "Target labels (filter=--to) are updated when their certainty is 70 or 0.\n")fmt.Fprintf(os.Stderr, "Updated target labels are set to certainty=90; file reviewer is set to \"Skraak\".\n")fmt.Fprintf(os.Stderr, "Targets already at certainty=100 or 90 are left alone.\n")fmt.Fprintf(os.Stderr, "Files that do not contain both --from and --to filter labels are skipped.\n\n")fmt.Fprintf(os.Stderr, "Exactly one of --file or --folder is required.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak calls propagate --file rec.wav.data \\\n")fmt.Fprintf(os.Stderr, " --from opensoundscape-kiwi-1.2 --to opensoundscape-kiwi-1.5 --species Kiwi\n\n")fmt.Fprintf(os.Stderr, " skraak calls propagate --folder ./recordings \\\n")fmt.Fprintf(os.Stderr, " --from opensoundscape-kiwi-1.2 --to opensoundscape-kiwi-1.5 --species Kiwi\n")}
fs.Usage = usagePrinter(fs,"skraak calls propagate [options]","Propagate verified classifications from one filter to another within a .data file\n"+"or across every .data file in a folder.\n\n"+"Only source labels with certainty=100 and matching --species are considered.\n"+"Target labels (filter=--to) are updated when their certainty is 70 or 0.\n"+"Updated target labels are set to certainty=90; file reviewer is set to \"Skraak\".\n"+"Targets already at certainty=100 or 90 are left alone.\n"+"Files that do not contain both --from and --to filter labels are skipped.\n\n"+"Exactly one of --file or --folder is required.","skraak calls propagate --file rec.wav.data \\"," --from opensoundscape-kiwi-1.2 --to opensoundscape-kiwi-1.5 --species Kiwi","","skraak calls propagate --folder ./recordings \\"," --from opensoundscape-kiwi-1.2 --to opensoundscape-kiwi-1.5 --species Kiwi",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak calls clip-labels [options]\n\n")fmt.Fprintf(os.Stderr, "Generate an OpenSoundScape clip_labels-format CSV from .data files.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nSegment policy:\n")fmt.Fprintf(os.Stderr, " - Real species → contributes mapped class to overlapping clips.\n")fmt.Fprintf(os.Stderr, " - Mapped to __NEGATIVE__ → clip emitted, all class columns False;\n")fmt.Fprintf(os.Stderr, " overrides positives in the same clip.\n")fmt.Fprintf(os.Stderr, " - Mapped to __IGNORE__ → segment contributes no labels to clips.\n")fmt.Fprintf(os.Stderr, " - Gaps → clip emitted with all class columns False.\n")fmt.Fprintf(os.Stderr, "\nIf --output exists: append. Column-set mismatch → hard error.\n")fmt.Fprintf(os.Stderr, "Duplicate (file, start_time, end_time) row → hard error on first.\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak calls clip-labels --folder ./recordings --mapping ./mapping.json\n")fmt.Fprintf(os.Stderr, " skraak calls clip-labels --folder ./recordings --mapping ./mapping.json \\\n")fmt.Fprintf(os.Stderr, " --filter opensoundscape-multi-1.0\n")}
fs.Usage = usagePrinter(fs,"skraak calls clip-labels [options]","Generate an OpenSoundScape clip_labels-format CSV from .data files.\n\n"+"Segment policy:\n"+" - Real species → contributes mapped class to overlapping clips.\n"+" - Mapped to __NEGATIVE__ → clip emitted, all class columns False;\n"+" overrides positives in the same clip.\n"+" - Mapped to __IGNORE__ → segment contributes no labels to clips.\n"+" - Gaps → clip emitted with all class columns False.\n\n"+"If --output exists: append. Column-set mismatch → hard error.\n"+"Duplicate (file, start_time, end_time) row → hard error on first.","skraak calls clip-labels --folder ./recordings --mapping ./mapping.json","skraak calls clip-labels --folder ./recordings --mapping ./mapping.json \\"," --filter opensoundscape-multi-1.0",)
func printClipUsage() {fmt.Fprintf(os.Stderr, "Usage: skraak calls clip [options]\n\n")fmt.Fprintf(os.Stderr, "Generate audio clips and spectrogram images from .data file segments.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fmt.Fprintf(os.Stderr, " --file <path> Path to .data file (required if no --folder)\n")fmt.Fprintf(os.Stderr, " --folder <path> Path to folder containing .data files (required if no --file)\n")fmt.Fprintf(os.Stderr, " --output <path> Output folder for generated clips (required)\n")fmt.Fprintf(os.Stderr, " --prefix <name> Prefix for output filenames (required)\n")fmt.Fprintf(os.Stderr, " --filter <name> Filter by ML model name (optional)\n")fmt.Fprintf(os.Stderr, " --species <name> Filter by species, optionally with calltype (e.g. Kiwi, Kiwi+Duet)\n")fmt.Fprintf(os.Stderr, " --certainty <int> Filter by certainty value (0-100, optional)\n")fmt.Fprintf(os.Stderr, " --size <int> Spectrogram image size in pixels (224-896, default 224)\n")fmt.Fprintf(os.Stderr, " --color Apply L4 colormap to spectrogram (default: grayscale)\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")fmt.Fprintf(os.Stderr, "\nOutput files:\n")fmt.Fprintf(os.Stderr, " <prefix>_<basename>_<start>_<end>.png # spectrogram image\n")fmt.Fprintf(os.Stderr, " <prefix>_<basename>_<start>_<end>.wav # audio clip (16kHz if downsampled)\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " # Clip all segments from a single file\n")fmt.Fprintf(os.Stderr, " skraak calls clip --file recording.data --output ./clips --prefix train\n\n")fmt.Fprintf(os.Stderr, " # Clip only Kiwi segments with color spectrograms at 448px\n")fmt.Fprintf(os.Stderr, " skraak calls clip --folder ./data --output ./clips --prefix kiwi \\\n")fmt.Fprintf(os.Stderr, " --filter opensoundscape-kiwi-1.2 --species Kiwi --size 448 --color\n\n")fmt.Fprintf(os.Stderr, " # Clip Kiwi Duet calls\n")fmt.Fprintf(os.Stderr, " skraak calls clip --folder ./data --output ./clips --prefix duet \\\n")fmt.Fprintf(os.Stderr, " --filter opensoundscape-kiwi-1.2 --species Kiwi+Duet\n")}
size := fs.Int("size", 0, "Spectrogram image size in pixels (224-896)")color := fs.Bool("color", false, "Apply L4 colormap to spectrogram")night := fs.Bool("night", false, "Only clip recordings made during solar night")day := fs.Bool("day", false, "Only clip recordings made during solar day")location := fs.String("location", "", "GPS coordinates and optional timezone")
size := fs.Int("size", 0, "Spectrogram image size in pixels (224-896, default 224)")color := fs.Bool("color", false, "Apply L4 colormap to spectrogram (default: grayscale)")night := fs.Bool("night", false, "Only clip recordings made during solar night (requires --location)")day := fs.Bool("day", false, "Only clip recordings made during solar day (requires --location)")location := fs.String("location", "", "GPS coordinates and optional IANA timezone, e.g. \"-36.85,174.76\" or \"-36.85,174.76,Pacific/Auckland\". Required with --night/--day. Not needed for AudioMoth data.")
fs.Usage = printClipUsage
fs.Usage = usagePrinter(fs,"skraak calls clip [options]","Generate audio clips and spectrogram images from .data file segments.\n\n"+"Output files:\n"+" <prefix>_<basename>_<start>_<end>.png # spectrogram image\n"+" <prefix>_<basename>_<start>_<end>.wav # audio clip (16kHz if downsampled)","# Clip all segments from a single file","skraak calls clip --file recording.data --output ./clips --prefix train","","# Clip only Kiwi segments with color spectrograms at 448px","skraak calls clip --folder ./data --output ./clips --prefix kiwi \\"," --filter opensoundscape-kiwi-1.2 --species Kiwi --size 448 --color","","# Clip Kiwi Duet calls","skraak calls clip --folder ./data --output ./clips --prefix duet \\"," --filter opensoundscape-kiwi-1.2 --species Kiwi+Duet",)
// Returns an error instead of exiting.func validateClipFlags(f clipFlags) error {missing := []string{}
func validateClipFlags(fs *flag.FlagSet, f clipFlags) error {
missing = append(missing, "--file or --folder")}if f.output == "" {missing = append(missing, "--output")
fs.Usage()return fmt.Errorf("missing required flags: [--file or --folder]")
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")}
fs.Usage = usagePrinter(fs,"skraak calls from-preds [options]","Extract clustered bird calls from ML predictions CSV.\n"+"Reads prediction CSV with columns: file, start_time, end_time, <ebird_codes...>\n"+"Each row is a clip with 1=present, 0=absent for each species.\n\n"+"Output:\n"+" With --dot-data=true (default): Writes .data files alongside audio files, outputs JSON summary\n"+" With --dot-data=false: Outputs JSON with clustered calls only (no .data files)\n\n"+"Filter name:\n"+" If --filter is provided, uses that value.\n"+" Otherwise, parses from CSV filename: prefix_filter_date.csv -> filter\n"+" Example: predsST_opensoundscape-kiwi-1.2_2025-11-12.csv -> opensoundscape-kiwi-1.2","# Write .data files (default)","skraak calls from-preds --csv predictions.csv","","# JSON output only (no .data files)","skraak calls from-preds --csv predictions.csv --dot-data=false > calls.json","","# Override filter name","skraak calls from-preds --csv preds.csv --filter my-custom-filter",)
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")}
fs.Usage = usagePrinter(fs,"skraak calls show-images [options]","Display spectrogram images for each segment in a .data file.\n"+"Images are output using the Kitty graphics protocol (or Sixel with --sixel, iTerm2 with --iterm).","skraak calls show-images --file recording.wav.data","skraak calls show-images --file recording.wav.data --color",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak calls from-birda [options]\n\n")fmt.Fprintf(os.Stderr, "Import BirdNET results to .data files.\n")fmt.Fprintf(os.Stderr, "Reads *.BirdNET.results.csv files and creates/merges .data files.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nBehavior:\n")fmt.Fprintf(os.Stderr, " - Filter is always 'BirdNET' (parsed from filename)\n")fmt.Fprintf(os.Stderr, " - If .data file exists with BirdNET filter: error (refuses to clobber)\n")fmt.Fprintf(os.Stderr, " - If .data file exists with different filter: merge segments\n")fmt.Fprintf(os.Stderr, " - Confidence (0.0-1.0) converted to certainty (0-100)\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak calls from-birda --folder ./recordings\n")fmt.Fprintf(os.Stderr, " skraak calls from-birda --file recording.BirdNET.results.csv\n")fmt.Fprintf(os.Stderr, " skraak calls from-birda --folder ./recordings --delete\n")}
fs.Usage = usagePrinter(fs,"skraak calls from-birda [options]","Import BirdNET results to .data files.\n"+"Reads *.BirdNET.results.csv files and creates/merges .data files.\n\n"+"Behavior:\n"+" - Filter is always 'BirdNET' (parsed from filename)\n"+" - If .data file exists with BirdNET filter: error (refuses to clobber)\n"+" - If .data file exists with different filter: merge segments\n"+" - Confidence (0.0-1.0) converted to certainty (0-100)","skraak calls from-birda --folder ./recordings","skraak calls from-birda --file recording.BirdNET.results.csv","skraak calls from-birda --folder ./recordings --delete",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak calls from-raven [options]\n\n")fmt.Fprintf(os.Stderr, "Import Raven selections to .data files.\n")fmt.Fprintf(os.Stderr, "Reads *.selections.txt files and creates/merges .data files.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nBehavior:\n")fmt.Fprintf(os.Stderr, " - Filter is always 'Raven' (parsed from filename)\n")fmt.Fprintf(os.Stderr, " - If .data file exists with Raven filter: error (refuses to clobber)\n")fmt.Fprintf(os.Stderr, " - If .data file exists with different filter: merge segments\n")fmt.Fprintf(os.Stderr, " - Frequency range preserved from Raven selections\n")fmt.Fprintf(os.Stderr, " - Certainty defaults to 70 (no confidence metric in Raven)\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak calls from-raven --folder ./recordings\n")fmt.Fprintf(os.Stderr, " skraak calls from-raven --file recording.Table.1.selections.txt\n")fmt.Fprintf(os.Stderr, " skraak calls from-raven --folder ./recordings --delete\n")}
fs.Usage = usagePrinter(fs,"skraak calls from-raven [options]","Import Raven selections to .data files.\n"+"Reads *.selections.txt files and creates/merges .data files.\n\n"+"Behavior:\n"+" - Filter is always 'Raven' (parsed from filename)\n"+" - If .data file exists with Raven filter: error (refuses to clobber)\n"+" - If .data file exists with different filter: merge segments\n"+" - Frequency range preserved from Raven selections\n"+" - Certainty defaults to 70 (no confidence metric in Raven)","skraak calls from-raven --folder ./recordings","skraak calls from-raven --file recording.Table.1.selections.txt","skraak calls from-raven --folder ./recordings --delete",)
fs.Usage = func() {fmt.Fprintf(os.Stderr, "Usage: skraak calls summarise [options]\n\n")fmt.Fprintf(os.Stderr, "Summarise all .data files in a folder.\n")fmt.Fprintf(os.Stderr, "Outputs JSON with segments array and summary statistics.\n\n")fmt.Fprintf(os.Stderr, "Options:\n")fs.PrintDefaults()fmt.Fprintf(os.Stderr, "\nOutput includes:\n")fmt.Fprintf(os.Stderr, " - segments: array of all segments with labels (omitted with --brief)\n")fmt.Fprintf(os.Stderr, " - data_files_read: count of successfully parsed .data files\n")fmt.Fprintf(os.Stderr, " - data_files_skipped: list of files that failed to parse\n")fmt.Fprintf(os.Stderr, " - total_segments: total number of segments\n")fmt.Fprintf(os.Stderr, " - filters: per-filter statistics (segments, species counts)\n")fmt.Fprintf(os.Stderr, " - review_status: unreviewed/confirmed/dont_know counts\n")fmt.Fprintf(os.Stderr, " - operators/reviewers: unique values found\n")fmt.Fprintf(os.Stderr, "\nExamples:\n")fmt.Fprintf(os.Stderr, " skraak calls summarise --folder ./recordings > summary.json\n")fmt.Fprintf(os.Stderr, " skraak calls summarise --folder ./recordings --brief > summary.json # summary only\n")fmt.Fprintf(os.Stderr, " skraak calls summarise --folder ./recordings --filter opensoundscape-kiwi-1.2 --brief\n")}
fs.Usage = usagePrinter(fs,"skraak calls summarise [options]","Summarise all .data files in a folder.\n"+"Outputs JSON with segments array and summary statistics.\n\n"+"Output includes:\n"+" - segments: array of all segments with labels (omitted with --brief)\n"+" - data_files_read: count of successfully parsed .data files\n"+" - data_files_skipped: list of files that failed to parse\n"+" - total_segments: total number of segments\n"+" - filters: per-filter statistics (segments, species counts)\n"+" - review_status: unreviewed/confirmed/dont_know counts\n"+" - operators/reviewers: unique values found","skraak calls summarise --folder ./recordings > summary.json","skraak calls summarise --folder ./recordings --brief > summary.json # summary only","skraak calls summarise --folder ./recordings --filter opensoundscape-kiwi-1.2 --brief",)