U6JEEU5O477ZOJ5UMRMOJSGPEJEU6Q7KMPKUSDF56CYVUJWL7QBQC return Model{state: state}
// Pre-compute bindings help textvar bindings []stringfor _, b := range state.Config.Bindings {if b.CallType != "" {bindings = append(bindings, fmt.Sprintf("%s=%s/%s", b.Key, b.Species, b.CallType))} else {bindings = append(bindings, fmt.Sprintf("%s=%s", b.Key, b.Species))}}bindingsHelp := strings.Join(bindings, " ")return Model{state: state,bindingsHelp: bindingsHelp,}
case tickMsg:// Check for key buffer timeoutif m.keyBuffer != "" && time.Since(m.lastKey) > time.Duration(m.state.Config.KeyDelay*float64(time.Second)) {// Try to parse and apply bindingif result := m.state.ParseKeyBuffer(m.keyBuffer); result != nil {m.state.ApplyBinding(result)if err := m.state.Save(); err != nil {m.err = err.Error()}m.keyBuffer = ""m.moveToNext()} else {m.err = fmt.Sprintf("Unknown binding: %s", m.keyBuffer)m.keyBuffer = ""}}return m, tickCmd()
// Add to key bufferif len(msg.String()) == 1 && isAlphaNum(msg.String()[0]) {m.keyBuffer += msg.String()m.lastKey = time.Now()// If 2 chars, process immediatelyif len(m.keyBuffer) >= 2 {if result := m.state.ParseKeyBuffer(m.keyBuffer); result != nil {m.state.ApplyBinding(result)if err := m.state.Save(); err != nil {m.err = err.Error()}m.keyBuffer = ""m.moveToNext()} else {m.err = fmt.Sprintf("Unknown binding: %s", m.keyBuffer)m.keyBuffer = ""
// Check for bindingif len(msg.String()) == 1 {key := msg.String()if result := m.state.ParseKeyBuffer(key); result != nil {m.state.ApplyBinding(result)if err := m.state.Save(); err != nil {m.err = err.Error()
var bindings []stringfor _, b := range m.state.Config.Bindings {if b.CallType != "" {bindings = append(bindings, fmt.Sprintf("%s=%s/%s", b.Key, b.Species, b.CallType))} else {bindings = append(bindings, fmt.Sprintf("%s=%s", b.Key, b.Species))}}b.WriteString(helpStyle.Render(strings.Join(bindings, " ")))
b.WriteString(helpStyle.Render(m.bindingsHelp))
// renderSpectrogram generates and outputs a spectrogram imagefunc (m *Model) renderSpectrogram(b *strings.Builder, dataPath string, seg *utils.Segment) {// Derive WAV pathwavPath := strings.TrimSuffix(dataPath, ".data")// Load WAV if not cached or file changedif m.wavPath != wavPath {samples, sampleRate, err := utils.ReadWAVSamples(wavPath)if err != nil {b.WriteString("[error loading WAV]")return}m.samples = samplesm.sampleRate = sampleRatem.wavPath = wavPath}// Extract segment samplessegSamples := utils.ExtractSegmentSamples(m.samples, m.sampleRate, seg.StartTime, seg.EndTime)if len(segSamples) == 0 {b.WriteString("[no samples]")return}// Generate spectrogramconfig := utils.DefaultSpectrogramConfig(m.sampleRate)spectrogram := utils.GenerateSpectrogram(segSamples, config)if spectrogram == nil {b.WriteString("[error generating spectrogram]")return}// Create imagevar img image.Imageif m.state.Config.Color {colorData := utils.ApplyL4Colormap(spectrogram)img = utils.CreateRGBImage(colorData)} else {img = utils.CreateGrayscaleImage(spectrogram)}if img == nil {b.WriteString("[error creating image]")return}// Resize to 224x224resized := utils.ResizeImage(img, 224, 224)// Clear previous images and output new one via Kitty protocolutils.ClearKittyImages(b)utils.WriteKittyImage(resized, b)}
{"k", &BindingResult{Species: []string{"Kiwi"}}, false},{"n", &BindingResult{Species: []string{"Don't Know"}}, false},{"p", &BindingResult{Species: []string{"Morepork"}}, false},
{"k", &BindingResult{Species: "Kiwi"}, false},{"d", &BindingResult{Species: "Kiwi", CallType: "Duet"}, false},{"n", &BindingResult{Species: "Don't Know"}, false},{"p", &BindingResult{Species: "Morepork"}, false},
{"kd", &BindingResult{Species: []string{"Kiwi"}, CallType: "Duet"}, false},{"kf", &BindingResult{Species: []string{"Kiwi"}, CallType: "Female"}, false},{"kp", &BindingResult{Species: []string{"Kiwi", "Morepork"}}, false}, // species+species{"dp", nil, true}, // calltype + species not allowed{"abc", nil, true}, // too long
t.Errorf("ParseKeyBuffer(%q).CallType = %q, want %q", tt.buffer, got.CallType, tt.want.CallType)}if len(got.Species) != len(tt.want.Species) {t.Errorf("ParseKeyBuffer(%q).Species = %v, want %v", tt.buffer, got.Species, tt.want.Species)
t.Errorf("ParseKeyBuffer(%q).CallType = %q, want %q", tt.key, got.CallType, tt.want.CallType)
}}func TestApplyBindingSpeciesSpecies(t *testing.T) {bindings := []KeyBinding{{Key: "k", Species: "Kiwi"},{Key: "p", Species: "Morepork"},}df := &utils.DataFile{Meta: &utils.DataMeta{},Segments: []*utils.Segment{{StartTime: 10.0,EndTime: 20.0,Labels: []*utils.Label{{Species: "Unknown", Certainty: 50, Filter: "test-filter"},},},},}state := &ClassifyState{Config: ClassifyConfig{Filter: "test-filter",Reviewer: "David",Bindings: bindings,},DataFiles: []*utils.DataFile{df},FileIdx: 0,SegmentIdx: 0,}// Apply "kp" = Kiwi + Morepork (two labels)result := &BindingResult{Species: []string{"Kiwi", "Morepork"}}state.ApplyBinding(result)// Should have 2 labels nowif len(df.Segments[0].Labels) != 2 {t.Errorf("expected 2 labels, got %d", len(df.Segments[0].Labels))}// Check both species present (sorted alphabetically)species := make(map[string]bool)for _, l := range df.Segments[0].Labels {species[l.Species] = true}if !species["Kiwi"] || !species["Morepork"] {t.Errorf("expected Kiwi and Morepork, got %v", species)
if df.Segments[0].Labels[0].CallType != "Duet" {t.Errorf("expected CallType=Duet, got %s", df.Segments[0].Labels[0].CallType)
if df.Segments[0].Labels[0].CallType != "" {t.Errorf("expected CallType='', got %s (should be removed)", df.Segments[0].Labels[0].CallType)
// Single key bindingif len(buffer) == 1 {for _, b := range s.Config.Bindings {if b.Key == buffer {return &BindingResult{Species: []string{b.Species},CallType: b.CallType,}
// ParseKeyBuffer parses a single key into binding resultfunc (s *ClassifyState) ParseKeyBuffer(key string) *BindingResult {for _, b := range s.Config.Bindings {if b.Key == key {return &BindingResult{Species: b.Species,CallType: b.CallType,
}return nil}// Two keys: try species+calltype or species+specieskey1 := buffer[0:1]key2 := buffer[1:2]var binding1, binding2 *KeyBindingfor i := range s.Config.Bindings {if s.Config.Bindings[i].Key == key1 {binding1 = &s.Config.Bindings[i]}if s.Config.Bindings[i].Key == key2 {binding2 = &s.Config.Bindings[i]}}if binding1 == nil || binding2 == nil {return nil}// species + calltypeif binding2.CallType != "" {return &BindingResult{Species: []string{binding1.Species},CallType: binding2.CallType,
// No matching labels, add new onesfor _, sp := range result.Species {seg.Labels = append(seg.Labels, &utils.Label{Species: sp,Certainty: certainty,Filter: s.Config.Filter,CallType: result.CallType,})}
// No matching labels, add new oneseg.Labels = append(seg.Labels, &utils.Label{Species: result.Species,Certainty: certainty,Filter: s.Config.Filter,CallType: result.CallType,})
if result.CallType != "" {filterLabels[0].CallType = result.CallType}// If two species, add second labelif len(result.Species) > 1 {seg.Labels = append(seg.Labels, &utils.Label{Species: result.Species[1],Certainty: certainty,Filter: s.Config.Filter,})}
filterLabels[0].CallType = result.CallType // always set (empty = remove)
test equality of calls from julia code and go codej1 = JSON3.read(read("predsST_opensoundscape-kiwi-1.2_2025-11-12.json", String))j2 = JSON3.read(read("predsST_opensoundscape-kiwi-1.2_2025-11-12_julia.json", String))issetequal(Dict.(j1.calls), Dict.(j2.calls))
// Marker for bindings section, no valuei++case "--species":if i+1 >= len(args) {fmt.Fprintf(os.Stderr, "Error: --species requires a value\n")os.Exit(1)}// Parse key="Species" formatkey, species := parseBinding(args[i+1])bindings = append(bindings, tools.KeyBinding{Key: key, Species: species})i += 2case "--call_type":
// Parse key="CallType" formatkey, calltype := parseBinding(args[i+1])bindings = append(bindings, tools.KeyBinding{Key: key, CallType: calltype})
// Parse key="Species" or key="Species"+"CallType" formatbinding := parseBind(args[i+1])bindings = append(bindings, binding)
return parts[0], parts[1]
key := parts[0]value := parts[1]// Check for Species+CallType formatif strings.Contains(value, "+") {valueParts := strings.SplitN(value, "+", 2)return tools.KeyBinding{Key: key,Species: valueParts[0],CallType: valueParts[1],}}// Species onlyreturn tools.KeyBinding{Key: key,Species: value,}
fmt.Fprintf(os.Stderr, " skraak calls classify --folder ./data --reviewer David --species k=Kiwi\n")
fmt.Fprintf(os.Stderr, " skraak calls classify --folder ./data --reviewer David --bind k=Kiwi\n")fmt.Fprintf(os.Stderr, " skraak calls classify --folder ./data --reviewer David \\\n")fmt.Fprintf(os.Stderr, " --bind k=Kiwi --bind d='Kiwi+Duet' --bind n='Don''t Know'\n")