cloud0day3 commited on
Commit
879acfa
·
0 Parent(s):

Kuroko: weights, the file that runs them, settings, dev-split predictions, training log and card

Browse files
Files changed (8) hide show
  1. .gitattributes +35 -0
  2. README.md +165 -0
  3. config.json +7 -0
  4. kuroko.py +191 -0
  5. model.safetensors +3 -0
  6. policy.json +35 -0
  7. predictions-dev.json +0 -0
  8. training-log.jsonl +20 -0
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - en
5
+ pipeline_tag: voice-activity-detection
6
+ library_name: pytorch
7
+ tags:
8
+ - turn-taking
9
+ - end-of-turn
10
+ - interruption
11
+ - backchannel
12
+ - turnbench
13
+ - audio
14
+ ---
15
+
16
+ # Kuroko
17
+
18
+ Kuroko is a turn-taking system for two-channel English conversation, built by PatientDesk AI for
19
+ [TurnBench](https://turnbench.sesame.com). It decides when a speaker's turn has ended, and whether a sound from the
20
+ other speaker is an interruption or an "mhm".
21
+
22
+ **Write-up:** <https://spero.patientdesk.ai/blog/kuroko>
23
+
24
+ This repository holds Kuroko's weights, the one file needed to run them, its settings and its predictions for
25
+ TurnBench's development split. The training code and the training data are not public, and neither are the
26
+ predictions for the test split, at the benchmark's request.
27
+
28
+ ## Run the model
29
+
30
+ The model is a 14.9M-parameter causal transformer. It hears both speakers and answers 25 times a second for each
31
+ of them, from the past only.
32
+
33
+ ```
34
+ pip install torch numpy scipy safetensors soundfile huggingface_hub
35
+ ```
36
+
37
+ ```python
38
+ import sys
39
+
40
+ import soundfile as sf
41
+ from huggingface_hub import snapshot_download
42
+
43
+ path = snapshot_download("cloud0day3/kuroko")
44
+ sys.path.insert(0, path)
45
+ import kuroko
46
+
47
+ model = kuroko.load(path)
48
+ audio, rate = sf.read("conversation.wav", dtype="int16") # (samples, 2): one speaker on each channel
49
+ answers = kuroko.answers(model, kuroko.features(audio, rate)) # (2, 11, n): speaker, answer, 25 a second
50
+ claims = answers[:, kuroko.ANSWERS.index("claim")]
51
+ ```
52
+
53
+ `answers[s, a, k]` is a probability for speaker `s` that has heard the audio up to `(k + 1) / 25` seconds. The 11
54
+ answers are named in `kuroko.ANSWERS`:
55
+
56
+ | Answer | Meaning |
57
+ |---|---|
58
+ | `voice` | The speaker is making a sound |
59
+ | `claim` | That sound claims the floor. This is the answer the events are read from |
60
+ | `hold` | The speaker holds the floor, pauses included |
61
+ | 8 more | Whether the speaker, and whether the partner, will talk for half or more of 0 to 0.2 s, 0.2 to 0.6 s, 0.6 to 1.2 s and 1.2 to 2 s ahead |
62
+
63
+ The model was trained on 16-bit recordings at 48 kHz with one speaker per channel, brought to 16 kHz as
64
+ `kuroko.features` does it. Read that way, on the GPU the predictions in this repository were made on, the released
65
+ files give the answers behind them on all 38 dev conversations. On a CPU an answer differs by less than 0.001,
66
+ which can move a threshold crossing by one answer, 40 ms. Audio of more bits read as floats moves an answer by up
67
+ to 0.02.
68
+
69
+ Only `claim` has been measured against annotations. Offline, a recorded 12.6-minute conversation is answered in 11 s
70
+ on four CPU threads, in 40-second stretches. The model has not been run as a live stream.
71
+
72
+ ## From answers to events
73
+
74
+ The model is one of three parts, and this repository has the code of the model only.
75
+
76
+ 1. **Detector.** [Silero VAD](https://github.com/snakers4/silero-vad) 6.2.3 runs on each channel in 32 ms steps and
77
+ marks stretches of sound. It is certain that a stretch has ended 0.26 s after its last sound.
78
+ 2. **Rules.** The timing rules p99lab published for [turn-1-mini](https://github.com/P99Lab/turn-1-mini), without that
79
+ system's learned parts, with the numbers chosen again on the dev split (`policy.json`). Every stop opens a
80
+ candidate end of turn, which is committed once the partner has taken over or 2.45 s have passed, unless the
81
+ speaker is back.
82
+ 3. **Model.** `claim` commits an interruption when it reaches 0.9, read from 0.32 s after a sound begins, before
83
+ the sound has lasted the 1.3 s the length rule needs. It also tells the end-of-turn rules that the partner has
84
+ taken over, when it reaches 0.5 for the partner's sound.
85
+
86
+ The write-up gives the rules in full.
87
+
88
+ ## Results
89
+
90
+ TurnBench development split: 38 conversations, 1,904 ends of turn, 347 interruptions, scored by the benchmark's
91
+ scorer. The settings were chosen on this split within a false-alarm budget of 8%. "Held out" is the same choice made
92
+ on half of the conversations and scored on the other half, over 500 random halves. Kuroko was submitted to TurnBench
93
+ on November 13, 2026. The test split is scored by the benchmark's maintainers, and its results are theirs to
94
+ publish on the [leaderboard](https://turnbench.sesame.com).
95
+
96
+ | | Recall | False-alarm rate | Median delay | Held-out recall | Held-out false-alarm rate |
97
+ |---|---|---|---|---|---|
98
+ | End of turn | 0.987 (1,879 of 1,904) | 0.079 (84 of 1,063) | 630 ms | 0.983 | 0.082 |
99
+ | Interruption | 1.000 (347 of 347) | 0.073 (272 of 3,733) | 545 ms | 0.988 | 0.061 |
100
+
101
+ To check these numbers, drop `predictions-dev.json` on the benchmark's [dev scorer](https://turnbench.sesame.com/dev).
102
+ It scores the file in the browser.
103
+
104
+ What each part adds, on the same split (recall / false-alarm rate / median delay):
105
+
106
+ | System | End of turn | Interruption |
107
+ |---|---|---|
108
+ | Rules, turn-1-mini's published settings, on our detector | 0.968 / 0.051 / 879 ms | 0.899 / 0.026 / 1,579 ms |
109
+ | Rules, settings chosen on dev | 0.983 / 0.080 / 692 ms | 0.977 / 0.076 / 1,078 ms |
110
+ | **Kuroko**: rules and the model | 0.987 / 0.079 / 630 ms | 1.000 / 0.073 / 545 ms |
111
+
112
+ ## Training data
113
+
114
+ 1,025 hours of two-speaker conversation: 786 hours of [Open Yap 1K](https://theagenticdatacompany.com/open-yap-1k)
115
+ and about 240 hours of PatientDesk's own recordings, made by contributors who agreed to this use. The labels come
116
+ from a timing rule applied in hindsight. None of the data is in this repository.
117
+
118
+ No audio and no label from TurnBench's dev or test split was trained on, and otoSpeech was not used. The dev split
119
+ chose the settings, checked the label rule and compared the systems above.
120
+
121
+ ## Files
122
+
123
+ | File | Contents |
124
+ |---|---|
125
+ | `model.safetensors` | The weights, kept at step 12,000 |
126
+ | `config.json` | The model's shape |
127
+ | `kuroko.py` | The model, its features and how a conversation is answered. Nothing of the training |
128
+ | `policy.json` | The settings of the rules, in seconds and answer levels: `eot` for ends of turn, `int` for interruptions |
129
+ | `predictions-dev.json` | Kuroko's events for the 38 dev conversations, in TurnBench's submission format |
130
+ | `training-log.jsonl` | Training loss and the loss of each answer on held-out conversations, every 600 steps |
131
+
132
+ Each block of `policy.json` is a full settings object, and each rule reads only part of it. The end-of-turn rules
133
+ read `wait_s`, `partner_s`, `deadline_s`, `resumed_s`, `held_s`, `confirm_s`, `partner_claim` and `claim_from_s` from
134
+ `eot`. The interruption rule reads `sustained_s`, `claim_fire`, `claim_from_s` and `claim_rise` from `int`.
135
+ `known_s` and `begun_s` are the detector's delays.
136
+
137
+ ## Limits
138
+
139
+ - These are development numbers on 38 conversations, with settings chosen on them. Zero missed interruptions is an
140
+ in-sample number; held out, recall is 0.988.
141
+ - TurnBench records two people, so the end-of-turn rules can wait for the second person to start talking. A voice
142
+ agent is the second speaker, and in an agent these rules would be left with the 2.45 s timer.
143
+ - A turn's end can be committed a second time, and an interruption is committed for every sound taken as a claim.
144
+ The scorer counts at most one false alarm per pause, and looks for interruptions only near annotated interruptions
145
+ and backchannels. Kept at least 1 s apart, the events score 0.975 at 0.074 on ends of turn and 1.000 at 0.070 on
146
+ interruptions.
147
+ - Studio audio with one clean channel per speaker, English only. Not measured on phone audio, with echo or as a live
148
+ stream.
149
+
150
+ The write-up has the rest: the label rule and its check against the annotators, the ablations, the causality test
151
+ and the credits.
152
+
153
+ ## License
154
+
155
+ `model.safetensors`, `config.json` and `kuroko.py` are released under Apache-2.0.
156
+
157
+ ## Credits
158
+
159
+ TurnBench is by Sesame AI Labs, with the evaluation corpus from Mundo AI and the otoSpeech training set from Oto
160
+ ([Jiang et al., 2026](https://arxiv.org/abs/2608.25218)). The timing rules are p99lab's. The detector is Silero VAD.
161
+ The model's future-activity targets follow voice activity projection
162
+ ([Ekstedt and Skantze, 2022](https://arxiv.org/abs/2205.09812)).
163
+
164
+ Training data: The Agentic Data Company. Open Yap 1K: channel-separated English natural two-speaker conversations.
165
+ 2026. Version 1.0. <https://theagenticdatacompany.com/open-yap-1k>
config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "mels": 80,
3
+ "width": 384,
4
+ "layers": 8,
5
+ "heads": 6,
6
+ "step": 12000
7
+ }
kuroko.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kuroko: of two speakers on separate channels, does the sound a speaker is making claim the floor?
2
+
3
+ Kuroko hears both speakers as log-mel frames and answers 25 times a second for each of them (`ANSWERS`). An
4
+ answer rests on the audio up to its own moment and on nothing later, but for the 0.6 ms a resampler reaches ahead
5
+ at 48 kHz, so the answers can be used while a conversation unfolds. The write-up says how the answers become
6
+ end-of-turn and interruption events: https://spero.patientdesk.ai/blog/kuroko
7
+
8
+ import soundfile as sf
9
+ import kuroko
10
+
11
+ model = kuroko.load(".") # the directory that holds model.safetensors and config.json
12
+ audio, rate = sf.read("conversation.wav", dtype="int16") # (samples, 2): one speaker on each channel
13
+ answers = kuroko.answers(model, kuroko.features(audio, rate)) # (2, 11, n): speaker, answer, 25 a second
14
+ claims = answers[:, kuroko.ANSWERS.index("claim")]
15
+
16
+ This file is the model and what it needs to run, and nothing of how it was trained. It needs numpy, scipy, torch
17
+ and safetensors.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ import math
24
+ from pathlib import Path
25
+
26
+ import numpy as np
27
+ import torch
28
+ from numpy.typing import NDArray
29
+ from scipy.signal import resample_poly
30
+ from torch import nn
31
+
32
+ RATE = 16_000 # the sample rate the model hears
33
+ HOP = 320 # 20 ms: one frame
34
+ WINDOW = 2 * HOP # a frame holds the 40 ms that end with it
35
+ FRAME_HZ = RATE // HOP
36
+ ANSWER_HZ = FRAME_HZ // 2 # the model answers every other frame
37
+ MELS = 80
38
+ LOW_HZ, HIGH_HZ = 60.0, 7_600.0
39
+ WINDOW_S = 40.0 # the most the model looks back
40
+ AHEAD_S = ((0.0, 0.2), (0.2, 0.6), (0.6, 1.2), (1.2, 2.0))
41
+ ANSWERS = (
42
+ "voice", # the speaker is making a sound
43
+ "claim", # that sound claims the floor: the answer the events are read from
44
+ "hold", # the speaker holds the floor, pauses included
45
+ *(f"{who} speaks {start:g} to {end:g} s ahead" for who in ("own", "partner") for start, end in AHEAD_S),
46
+ )
47
+ _BLOCK = 20_000 # frames turned into spectra at once
48
+
49
+
50
+ def mel_filters() -> NDArray[np.float32]:
51
+ """Triangular filters (MELS, WINDOW // 2 + 1), evenly spaced on the mel scale from LOW_HZ to HIGH_HZ."""
52
+ low, high = (2595.0 * math.log10(1.0 + hz / 700.0) for hz in (LOW_HZ, HIGH_HZ))
53
+ edges = 700.0 * (10.0 ** (np.linspace(low, high, MELS + 2) / 2595.0) - 1.0)
54
+ bins = np.fft.rfftfreq(WINDOW, 1.0 / RATE)
55
+ rising = (bins[None] - edges[:-2, None]) / (edges[1:-1] - edges[:-2])[:, None]
56
+ falling = (edges[2:, None] - bins[None]) / (edges[2:] - edges[1:-1])[:, None]
57
+ filters: NDArray[np.float32] = np.clip(np.minimum(rising, falling), 0.0, None).astype(np.float32)
58
+ return filters
59
+
60
+
61
+ def log_mel(audio: NDArray[np.float32]) -> NDArray[np.float32]:
62
+ """Log-mel frames (frames, MELS) of 16 kHz audio: frame k holds the 40 ms that end at sample (k + 1) * HOP."""
63
+ frames = len(audio) // HOP
64
+ padded = np.concatenate([np.zeros(HOP, np.float32), audio[: frames * HOP]])
65
+ windows = np.lib.stride_tricks.sliding_window_view(padded, WINDOW)[::HOP]
66
+ taper = np.hanning(WINDOW).astype(np.float32)
67
+ filters = mel_filters()
68
+ out = np.empty((frames, MELS), np.float32)
69
+ for start in range(0, frames, _BLOCK):
70
+ power = np.abs(np.fft.rfft(windows[start : start + _BLOCK] * taper, axis=1)) ** 2
71
+ out[start : start + _BLOCK] = np.log(power @ filters.T + 1e-10)
72
+ return out
73
+
74
+
75
+ def features(audio: NDArray[np.int16] | NDArray[np.floating], rate: int) -> NDArray[np.float16]:
76
+ """The frames of a conversation given as (samples, 2), one speaker on each channel, at `rate` Hz, as 16-bit
77
+ samples or as floats between -1 and 1: (2, frames, MELS), in the half precision the model learned from.
78
+
79
+ The model was trained on 16-bit recordings at 48 kHz, brought to 16 kHz this way. Read as floats, audio of
80
+ more than 16 bits gives answers that differ in the second decimal."""
81
+ scale = 32768.0 if audio.dtype == np.int16 else 1.0
82
+ step = math.gcd(RATE, rate)
83
+ sides = []
84
+ for channel in (0, 1):
85
+ heard = resample_poly(audio[:, channel].astype(np.float32) / scale, RATE // step, rate // step)
86
+ sides.append(log_mel(heard.astype(np.float32)).astype(np.float16))
87
+ return np.stack(sides)
88
+
89
+
90
+ def _turned(x: torch.Tensor, angle: torch.Tensor) -> torch.Tensor:
91
+ """Rotary positions: each pair of `x`'s last dimension turned by its position's angle."""
92
+ first, second = x[..., 0::2], x[..., 1::2]
93
+ cos, sin = angle.cos(), angle.sin()
94
+ return torch.stack([first * cos - second * sin, first * sin + second * cos], dim=-1).flatten(-2)
95
+
96
+
97
+ class Block(nn.Module):
98
+ """One transformer layer whose attention sees the frames up to its own."""
99
+
100
+ def __init__(self, width: int, heads: int) -> None:
101
+ super().__init__()
102
+ self.heads = heads
103
+ self.before_attention = nn.LayerNorm(width)
104
+ self.query_key_value = nn.Linear(width, 3 * width)
105
+ self.attended = nn.Linear(width, width)
106
+ self.before_mlp = nn.LayerNorm(width)
107
+ self.mlp = nn.Sequential(nn.Linear(width, 4 * width), nn.GELU(), nn.Linear(4 * width, width))
108
+
109
+ def forward(self, x: torch.Tensor, angle: torch.Tensor) -> torch.Tensor:
110
+ batch, frames, width = x.shape
111
+ parts = self.query_key_value(self.before_attention(x)).view(batch, frames, 3, self.heads, -1).transpose(1, 3)
112
+ query, key, value = _turned(parts[:, :, 0], angle), _turned(parts[:, :, 1], angle), parts[:, :, 2]
113
+ mixed = nn.functional.scaled_dot_product_attention(query, key, value, is_causal=True)
114
+ x = x + self.attended(mixed.transpose(1, 2).reshape(batch, frames, width))
115
+ out: torch.Tensor = x + self.mlp(self.before_mlp(x))
116
+ return out
117
+
118
+
119
+ class Kuroko(nn.Module):
120
+ """Frames of both speakers in, a logit for each of `ANSWERS` and each speaker out, at half the frame rate.
121
+ Both speakers go through the same weights, each once as "own" and once as "partner"."""
122
+
123
+ mean: torch.Tensor
124
+ std: torch.Tensor
125
+ turns: torch.Tensor # how fast each pair of an attention head's dimensions turns with position
126
+
127
+ def __init__(self, mels: int = MELS, width: int = 384, layers: int = 8, heads: int = 6) -> None:
128
+ super().__init__()
129
+ self.register_buffer("mean", torch.zeros(mels))
130
+ self.register_buffer("std", torch.ones(mels))
131
+ self.inputs = nn.Conv1d(mels, width, 5)
132
+ self.halve = nn.Conv1d(width, width, 2, stride=2)
133
+ self.join = nn.Linear(2 * width, width)
134
+ self.blocks = nn.ModuleList(Block(width, heads) for _ in range(layers))
135
+ self.after = nn.LayerNorm(width)
136
+ self.answers = nn.Linear(width, len(ANSWERS))
137
+ pairs = width // heads // 2
138
+ self.register_buffer("turns", 1.0 / 10_000 ** (torch.arange(pairs) / pairs), persistent=False)
139
+
140
+ def forward(self, mel: torch.Tensor) -> torch.Tensor:
141
+ """`mel` (batch, 2, frames, mels), from where the model starts to listen, gives logits
142
+ (batch, 2, frames // 2, len(ANSWERS)). An answer sees its own two input frames and what came before."""
143
+ batch, speakers, frames, mels = mel.shape
144
+ frames = frames // 2 * 2
145
+ heard = ((mel[:, :, :frames] - self.mean) / self.std).reshape(batch * speakers, frames, mels).transpose(1, 2)
146
+ heard = nn.functional.gelu(self.inputs(nn.functional.pad(heard, (4, 0)))) # zeros before the audio
147
+ own = nn.functional.gelu(self.halve(heard)).transpose(1, 2).reshape(batch, speakers, frames // 2, -1)
148
+ x = self.join(torch.cat([own, own.flip(1)], dim=-1)).reshape(batch * speakers, frames // 2, -1)
149
+ angle = torch.arange(frames // 2, device=x.device)[:, None] * self.turns
150
+ for block in self.blocks:
151
+ x = block(x, angle)
152
+ logits: torch.Tensor = self.answers(self.after(x)).reshape(batch, speakers, frames // 2, len(ANSWERS))
153
+ return logits
154
+
155
+
156
+ def load(directory: str | Path = ".", device: str | torch.device = "cpu") -> Kuroko:
157
+ """The model whose `config.json` and `model.safetensors` are in a directory, ready to answer."""
158
+ from safetensors.torch import load_file
159
+
160
+ config = json.loads((Path(directory) / "config.json").read_text())
161
+ model = Kuroko(config["mels"], config["width"], config["layers"], config["heads"])
162
+ model.load_state_dict(load_file(Path(directory) / "model.safetensors"))
163
+ return model.to(device).eval()
164
+
165
+
166
+ @torch.no_grad()
167
+ def answers(model: Kuroko, mel: NDArray[np.floating]) -> NDArray[np.float32]:
168
+ """The model's answers for a whole conversation given as (2, frames, MELS): probabilities
169
+ (2, len(ANSWERS), frames // 2), the answer at index k having heard the audio up to (k + 1) / ANSWER_HZ seconds.
170
+
171
+ The model listens to stretches of `WINDOW_S` that start half a stretch apart, and of each but the first only
172
+ the answers of its second half are kept: every answer then comes from a model that has listened for at least
173
+ half a stretch, or from the conversation's start.
174
+
175
+ Each stretch goes through the model by itself and in full precision. Batched or in half precision, an answer
176
+ shifts in its third decimal with what else is in the batch, and with it the frame at which a threshold is
177
+ crossed: the answers before a moment must not change with the audio after it."""
178
+ device = model.mean.device
179
+ window = round(WINDOW_S * FRAME_HZ)
180
+ half = window // 2
181
+ frames = mel.shape[1] // 2 * 2
182
+ out = np.zeros((2, len(ANSWERS), frames // 2), np.float32)
183
+ for start in range(0, max(1, frames - half), half):
184
+ stretch = np.zeros((1, 2, window, mel.shape[2]), np.float32)
185
+ piece = mel[:, start : start + window]
186
+ stretch[0, :, : piece.shape[1]] = piece
187
+ scores = torch.sigmoid(model(torch.from_numpy(stretch).to(device))[0]).permute(0, 2, 1).cpu().numpy()
188
+ keep = 0 if start == 0 else half // 2 # in answer frames
189
+ end = min(frames, start + window) // 2
190
+ out[:, :, start // 2 + keep : end] = scores[:, :, keep : end - start // 2]
191
+ return out
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:194f72b31ede09d7710565e7956f56dbc15adb1dc01fbb35f84a5de3c48789a9
3
+ size 59791756
policy.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "eot": {
3
+ "known_s": 0.26,
4
+ "begun_s": 0.32,
5
+ "wait_s": 0.35000000000000003,
6
+ "partner_s": 1.5,
7
+ "deadline_s": 2.45,
8
+ "resumed_s": 0.55,
9
+ "held_s": 2.9000000000000004,
10
+ "confirm_s": 2.75,
11
+ "sustained_s": 1.2,
12
+ "claim_fire": 0.77,
13
+ "claim_from_s": 0.16,
14
+ "claim_rise": 2.0,
15
+ "partner_claim": 0.5
16
+ },
17
+ "int": {
18
+ "known_s": 0.26,
19
+ "begun_s": 0.32,
20
+ "wait_s": 0.6,
21
+ "partner_s": 0.3,
22
+ "deadline_s": 2.5,
23
+ "resumed_s": 0.5,
24
+ "held_s": 1.5,
25
+ "confirm_s": 2.5,
26
+ "sustained_s": 1.3,
27
+ "claim_fire": 0.9,
28
+ "claim_from_s": 0.32,
29
+ "claim_rise": 0.99,
30
+ "partner_claim": 2.0
31
+ },
32
+ "budget": 0.08,
33
+ "gold": "mundo-ai/turn-benchmark-dev",
34
+ "with_model": true
35
+ }
predictions-dev.json ADDED
The diff for this file is too large to render. See raw diff
 
training-log.jsonl ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"step": 600, "loss": 0.7895789335668087, "validation": {"voice": 0.077, "claim": 0.1373, "hold": 0.1359, "ahead": 0.3698}}
2
+ {"step": 1200, "loss": 0.575088350623846, "validation": {"voice": 0.0711, "claim": 0.1338, "hold": 0.1331, "ahead": 0.3621}}
3
+ {"step": 1800, "loss": 0.5480149773756663, "validation": {"voice": 0.0678, "claim": 0.1278, "hold": 0.1285, "ahead": 0.3581}}
4
+ {"step": 2400, "loss": 0.534358601073424, "validation": {"voice": 0.0643, "claim": 0.123, "hold": 0.1259, "ahead": 0.354}}
5
+ {"step": 3000, "loss": 0.5243895636498928, "validation": {"voice": 0.0627, "claim": 0.1257, "hold": 0.1248, "ahead": 0.3514}}
6
+ {"step": 3600, "loss": 0.5199194972217083, "validation": {"voice": 0.0619, "claim": 0.1222, "hold": 0.1279, "ahead": 0.3525}}
7
+ {"step": 4200, "loss": 0.5104139878352483, "validation": {"voice": 0.0602, "claim": 0.1188, "hold": 0.1221, "ahead": 0.3477}}
8
+ {"step": 4800, "loss": 0.505867060571909, "validation": {"voice": 0.0613, "claim": 0.1199, "hold": 0.1288, "ahead": 0.3494}}
9
+ {"step": 5400, "loss": 0.49904234940807024, "validation": {"voice": 0.0595, "claim": 0.1185, "hold": 0.1204, "ahead": 0.3463}}
10
+ {"step": 6000, "loss": 0.4914438219865163, "validation": {"voice": 0.0573, "claim": 0.1144, "hold": 0.1183, "ahead": 0.3434}}
11
+ {"step": 6600, "loss": 0.4874112403392792, "validation": {"voice": 0.057, "claim": 0.1131, "hold": 0.1191, "ahead": 0.3433}}
12
+ {"step": 7200, "loss": 0.48205591450134916, "validation": {"voice": 0.0561, "claim": 0.1131, "hold": 0.1177, "ahead": 0.3415}}
13
+ {"step": 7800, "loss": 0.4792872371772925, "validation": {"voice": 0.0553, "claim": 0.113, "hold": 0.1162, "ahead": 0.3405}}
14
+ {"step": 8400, "loss": 0.47472050214807193, "validation": {"voice": 0.0558, "claim": 0.113, "hold": 0.1171, "ahead": 0.3389}}
15
+ {"step": 9000, "loss": 0.4704521136979262, "validation": {"voice": 0.0555, "claim": 0.1112, "hold": 0.1155, "ahead": 0.3381}}
16
+ {"step": 9600, "loss": 0.46662275776267054, "validation": {"voice": 0.0549, "claim": 0.112, "hold": 0.1152, "ahead": 0.338}}
17
+ {"step": 10200, "loss": 0.46309285586078963, "validation": {"voice": 0.0551, "claim": 0.1113, "hold": 0.1152, "ahead": 0.3376}}
18
+ {"step": 10800, "loss": 0.462796163012584, "validation": {"voice": 0.0555, "claim": 0.1119, "hold": 0.1153, "ahead": 0.3376}}
19
+ {"step": 11400, "loss": 0.4583358014623324, "validation": {"voice": 0.0544, "claim": 0.1103, "hold": 0.1143, "ahead": 0.3368}}
20
+ {"step": 12000, "loss": 0.4586797767877579, "validation": {"voice": 0.0543, "claim": 0.1104, "hold": 0.1144, "ahead": 0.3367}}