Fork channel

Create a new channel as a copy of main.

Rename channel

Rename main to:

Delete channel

Delete main? This cannot be undone.

calls_clip_labels.go
package cmd

import (
	"encoding/json"
	"flag"
	"fmt"
	"os"
	"sort"

	"skraak/tools/calls"
)

// runCallsClipLabels handles the "calls clip-labels" subcommand.
func runCallsClipLabels(args []string) error {
	fs := flag.NewFlagSet("calls clip-labels", flag.ExitOnError)
	folder := fs.String("folder", "", "Folder containing .data files (required)")
	mapping := fs.String("mapping", "", "Path to mapping.json (required)")
	filter := fs.String("filter", "", "Restrict to a single filter name (default: all filters)")
	output := fs.String("output", "./clip_labels.csv", "Output CSV path")
	clipDuration := fs.Float64("clip-duration", 4.0, "Clip duration in seconds")
	clipOverlap := fs.Float64("clip-overlap", 0.5, "Clip overlap in seconds")
	minLabelOverlap := fs.Float64("min-label-overlap", 0.25, "Minimum overlap (s) for an annotation to label a clip")
	finalClip := fs.String("final-clip", "full", "Trailing-clip behaviour: full | remainder | extend | none")

	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",
	)

	if err := fs.Parse(args); err != nil {
		return fmt.Errorf("parsing arguments: %w", err)
	}

	if err := requireFlags(fs, map[string]any{"--folder": *folder, "--mapping": *mapping}); err != nil {
		return err
	}

	input := calls.CallsClipLabelsInput{
		Folder:          *folder,
		MappingPath:     *mapping,
		Filter:          *filter,
		OutputPath:      *output,
		ClipDuration:    *clipDuration,
		ClipOverlap:     *clipOverlap,
		MinLabelOverlap: *minLabelOverlap,
		FinalClip:       *finalClip,
	}

	fmt.Fprintf(os.Stderr, "Folder:    %s\n", *folder)
	fmt.Fprintf(os.Stderr, "Mapping:   %s\n", *mapping)
	fmt.Fprintf(os.Stderr, "Output:    %s\n", *output)
	fmt.Fprintf(os.Stderr, "Clip:      duration=%.3fs overlap=%.3fs final=%s min-label-overlap=%.3fs\n",
		*clipDuration, *clipOverlap, *finalClip, *minLabelOverlap)
	if *filter != "" {
		fmt.Fprintf(os.Stderr, "Filter:    %s\n", *filter)
	}

	out, err := calls.CallsClipLabels(input)
	if err != nil {
		return fmt.Errorf("clip-labels: %w", err)
	}

	fmt.Fprintf(os.Stderr, "\nResults\n")
	fmt.Fprintf(os.Stderr, "  .data files parsed:         %d\n", out.DataFilesParsed)
	fmt.Fprintf(os.Stderr, "  Segments ignored (__IGNORE__): %d\n", out.SegmentsIgnored)
	fmt.Fprintf(os.Stderr, "  Clips excluded (__IGNORE__):  %d\n", out.ClipsIgnored)
	fmt.Fprintf(os.Stderr, "  Clips emitted:              %d\n", out.RowsWritten)
	fmt.Fprintf(os.Stderr, "    negative (__NEGATIVE__):  %d\n", out.ClipsNegative)
	fmt.Fprintf(os.Stderr, "    all-False (gap):          %d\n", out.ClipsAllFalseGap)
	if out.AppendedToFile {
		fmt.Fprintf(os.Stderr, "  Appended to file:           yes (%d existing rows)\n", out.ExistingRowsFound)
	}
	fmt.Fprintf(os.Stderr, "\nPer-class True counts:\n")
	keys := make([]string, 0, len(out.PerClassTrueCount))
	for k := range out.PerClassTrueCount {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		fmt.Fprintf(os.Stderr, "  %-30s %d\n", k+":", out.PerClassTrueCount[k])
	}

	enc := json.NewEncoder(os.Stdout)
	enc.SetIndent("", "  ")
	if err := enc.Encode(out); err != nil {
		return fmt.Errorf("encoding output: %w", err)
	}
	return nil
}