mirror of
https://github.com/hanzoai/attendant.git
synced 2026-09-30 20:43:25 +00:00
The library had both directions and no service: attend.go ended in an empty main. This closes the loop — a turn is transcribed, our model answers it, and hanzoai/speech gives that answer a voice the room hears. The floor decides when. It frees itself `rest` after the holder stops, which is the pause a person waits out before speaking, and it fills again within `notice` of anyone starting — so an answer still playing when somebody talks is cut off there and the track comes down. Nothing here holds a second notion of turns. Say takes a context and the bytes rather than a path: it returns when the room has heard the audio or the moment it is cancelled, and unpublishes either way. A track left up is a microphone nobody takes down, and every answer added another. A session is now addressed where the open says it lives. A growing transcript is a window in one process's memory and speech runs two replicas behind a Service that round-robins, so pushing to the Service address is a 404 for about half of every turn. The attendant holds no LiveKit signing key: it asks meet for one room's token the way a browser does, carrying one IAM identity to meet, the model and the voice alike. Co-authored-by: Hanzo Dev <[email protected]>
173 lines
5.7 KiB
Bash
Executable File
173 lines
5.7 KiB
Bash
Executable File
#!/bin/sh
|
|
# Every test here claims to catch a specific mistake. This breaks the code that
|
|
# way, on purpose, and requires the test to go red — a test that passes against
|
|
# the bug it was written for is not evidence of anything.
|
|
#
|
|
# A build failure does not count as a red test: the mutation has to compile and
|
|
# run, or all that has been proven is that Go rejects a typo. Checked below.
|
|
set -eu
|
|
export PATH=/usr/local/go/bin:$PATH
|
|
cd "$(dirname "$0")"
|
|
|
|
try() { # name, test, file, from, to
|
|
name=$1 test=$2 file=$3 from=$4 to=$5
|
|
cp "$file" "$file.was"
|
|
python3 - "$file" "$from" "$to" <<-'EOF'
|
|
import sys
|
|
p, a, b = sys.argv[1:4]
|
|
s = open(p).read()
|
|
if s.count(a) != 1:
|
|
sys.exit(f"mutation site appears {s.count(a)} times, wanted 1: {a!r}")
|
|
open(p, "w").write(s.replace(a, b))
|
|
EOF
|
|
|
|
if ! go vet ./... >/dev/null 2>&1; then
|
|
mv "$file.was" "$file"
|
|
printf 'BROKEN %-34s mutation does not compile — proves nothing\n' "$name"
|
|
return 1
|
|
fi
|
|
if go test -run "$test" ./... >/dev/null 2>&1; then
|
|
mv "$file.was" "$file"
|
|
printf 'BLIND %-34s %s still passes with the bug in\n' "$name" "$test"
|
|
return 1
|
|
fi
|
|
mv "$file.was" "$file"
|
|
printf 'caught %-34s %s\n' "$name" "$test"
|
|
}
|
|
|
|
try 'turn opens mid-word' TestTurnBeginsBeforeWeKnewWhoseItWas floor.go \
|
|
'lead = 2500 * time.Millisecond' 'lead = 200 * time.Millisecond'
|
|
|
|
try 'no hysteresis' TestBackchannelDoesNotTakeTheFloor floor.go \
|
|
'return top, now.Sub(f.from) >= grab' 'return top, true'
|
|
|
|
try 'trusts a stale ranking' TestAMemoryOfASpeakerDoesNotHoldTheFloor floor.go \
|
|
'clips[len(clips)-1].at.After(now.Add(-notice))' 'clips[len(clips)-1].at.After(now.Add(-time.Hour))'
|
|
|
|
try 'everyone transcribed' TestOnlyTheHolderIsTranscribed floor.go \
|
|
'if speaker == f.holder {' 'if true {'
|
|
|
|
try 'held audio aliased' TestHeldAudioIsACopy floor.go \
|
|
'pcm: append([]int16(nil), pcm...)' 'pcm: pcm'
|
|
|
|
try 'floor never freed' TestSilenceEndsTheTurn floor.go \
|
|
'now.Sub(f.spoke) > rest' 'now.Sub(f.spoke) > rest*1000'
|
|
|
|
try 'refusal swallowed' TestARefusedTurnIsRetriedNotSwallowed floor.go \
|
|
' case err != nil:
|
|
f.tally.Faults++' ' case err != nil:
|
|
f.holder = who'
|
|
|
|
try 'talk-over uncounted' TestTalkOverIsCounted floor.go \
|
|
'if f.loud[speaker] {' 'if false {'
|
|
|
|
try 'attendant on the floor' TestTheAttendantIsNotOnTheFloor attend.go \
|
|
'if _, mine := p.(*lksdk.LocalParticipant); mine {' 'if false {'
|
|
|
|
try 'turn left open at the end' TestCancellingSettlesTheOpenTurn floor.go \
|
|
' case <-ctx.Done():
|
|
f.Close()' ' case <-ctx.Done():
|
|
_ = f'
|
|
|
|
|
|
|
|
try 'chunks the wrong size' TestAPushIsWholeFramesInsideTheCeiling scribe.go \
|
|
'for len(t.pending) >= chunk {' 'for len(t.pending) >= chunk*3 {'
|
|
|
|
try 'no rollover at the limit' TestALongTurnRollsOverRatherThanBeingRefused scribe.go \
|
|
'if t.sent+float64(len(pcm))/(rate*2) > span.Seconds() {' 'if false {'
|
|
|
|
try 'notes read out of order' TestNotesReadInTheOrderThingsWereSaid scribe.go \
|
|
'seq := n.next
|
|
n.next++' 'seq := -n.next
|
|
n.next++'
|
|
|
|
try 'dropped audio hidden' TestAServiceThatCannotKeepUpLosesAudioOutLoud scribe.go \
|
|
' default:
|
|
t.to.mu.Lock()
|
|
t.to.lost++
|
|
t.to.mu.Unlock()' ' default:
|
|
t.to.mu.Lock()
|
|
t.to.mu.Unlock()'
|
|
|
|
# A select with a default never waits, so adding a timeout case to it changes
|
|
# nothing — the first version of this mutation was a no-op and the run said so.
|
|
# Blocking Hear means taking the default away.
|
|
try 'Hear waits for the service' TestHearDoesNotWaitForTheService scribe.go \
|
|
' select {
|
|
case t.in <- append([]int16(nil), pcm...):
|
|
default:
|
|
t.to.mu.Lock()
|
|
t.to.lost++
|
|
t.to.mu.Unlock()
|
|
}' ' t.in <- append([]int16(nil), pcm...)'
|
|
|
|
try 'recovered audio still called lost' TestTalkOverThatWinsTheFloorIsNotALoss floor.go \
|
|
' if held[i].over {
|
|
f.tally.Over -= len(held[i].pcm) // spoken over the last holder, kept anyway
|
|
}' ' if false {
|
|
f.tally.Over -= len(held[i].pcm)
|
|
}'
|
|
|
|
|
|
# The measured failure, pinned to the constant that fixes it: a rival that must
|
|
# wait only one observer interval loses to a "mhm, yeah".
|
|
try 'grab is one interval' TestBackchannelDoesNotTakeTheFloor floor.go \
|
|
'grab = 2 * time.Second' 'grab = 500 * time.Millisecond'
|
|
|
|
|
|
try 'opening burst frame by frame' TestTheOpeningBurstDoesNotOverrunTheTurn floor.go \
|
|
' turn.Hear(began)' ' for i := 0; i < len(began); i += 320 {
|
|
stop := i + 320
|
|
if stop > len(began) {
|
|
stop = len(began)
|
|
}
|
|
turn.Hear(began[i:stop])
|
|
}'
|
|
|
|
|
|
try 'audio given to two turns' TestAudioIsGivenToOneTurnOnly floor.go \
|
|
' if held[i].sent {
|
|
continue
|
|
}' ' if false {
|
|
continue
|
|
}'
|
|
|
|
|
|
try 'wait starts at the report' TestTheWaitRunsFromWhenTheyStartedTalking floor.go \
|
|
'f.rival, f.from = top, f.began(top, now)' 'f.rival, f.from = top, now'
|
|
|
|
|
|
# A session lives in one pod's memory and the Service address in front of it
|
|
# round-robins: ignoring where the open says it lives is a 404 for half a turn.
|
|
try 'session addressed at the service' TestASessionIsAddressedWhereItLives scribe.go \
|
|
' if got.At == "" {
|
|
return got.ID, n.url, nil
|
|
}
|
|
return got.ID, got.At, nil' ' return got.ID, n.url, nil'
|
|
|
|
try 'answers over the speaker' TestTheAttendantWaitsForTheFloor answer.go \
|
|
' if err := until(ctx, f, free); err != nil {
|
|
return err
|
|
}' ' _ = free'
|
|
|
|
try 'answer cannot be interrupted' TestSomebodyTalkingCutsTheAnswerShort answer.go \
|
|
' go func() {
|
|
if until(talking, f, busy) == nil {
|
|
hush()
|
|
}
|
|
}()' ' _ = busy'
|
|
|
|
try 'answers itself' TestTheAttendantDoesNotAnswerItself answer.go \
|
|
' if heard == a.answered {
|
|
return nil
|
|
}' ' _ = heard'
|
|
|
|
try 'forgets what it said' TestWhatTheAttendantSaidIsInTheRecord answer.go \
|
|
' a.Spoke(a.who, text, time.Now())' ' _ = text'
|
|
|
|
try 'publishes a container the room cannot carry' TestTheAnswerIsAudioTheRoomCanPlay answer.go \
|
|
'"response_format": "opus"' '"response_format": "mp3"'
|
|
|
|
echo 'every test earns its keep'
|