R1: streaming end-of-turn heads on the Smart Turn v3.2 encoder
Browse files- AMI_LICENCE.txt +307 -0
- ATTRIBUTION.md +47 -0
- LICENSE +25 -0
- README.md +225 -0
- SHA256SUMS +12 -0
- eot-heads-v32.onnx +3 -0
- eval/ami_dev.csv +0 -0
- eval/ami_eval.py +451 -0
- eval/ami_eval_reference.md +48 -0
- eval/ami_test.csv +0 -0
- rules/eager_tables.csv +9 -0
- rules/theta_tables.csv +7 -0
- smart-turn-v3.2-encoder-pooled.onnx +3 -0
AMI_LICENCE.txt
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
The AMI corpus and its annotations are released under the Creative
|
| 2 |
+
Commons Attribution 4.0 International Public License agreement (CC BY
|
| 3 |
+
4.0). Use of this data implies agreement with the terms below. See
|
| 4 |
+
also: https://creativecommons.org/licenses/by/4.0/
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
THE WORK (AS DEFINED BELOW) IS PROVIDED UNDER THE TERMS OF THIS
|
| 8 |
+
CREATIVE COMMONS PUBLIC LICENSE ("CCPL" OR "LICENSE"). THE WORK IS
|
| 9 |
+
PROTECTED BY COPYRIGHT AND/OR OTHER APPLICABLE LAW. ANY USE OF THE
|
| 10 |
+
WORK OTHER THAN AS AUTHORIZED UNDER THIS LICENSE OR COPYRIGHT LAW IS
|
| 11 |
+
PROHIBITED.
|
| 12 |
+
|
| 13 |
+
BY EXERCISING ANY RIGHTS TO THE WORK PROVIDED HERE, YOU ACCEPT AND
|
| 14 |
+
AGREE TO BE BOUND BY THE TERMS OF THIS LICENSE. THE LICENSOR GRANTS
|
| 15 |
+
YOU THE RIGHTS CONTAINED HERE IN CONSIDERATION OF YOUR ACCEPTANCE OF
|
| 16 |
+
SUCH TERMS AND CONDITIONS.
|
| 17 |
+
|
| 18 |
+
Section 1 – Definitions.
|
| 19 |
+
|
| 20 |
+
a. Adapted Material means material subject to Copyright and Similar
|
| 21 |
+
Rights that is derived from or based upon the Licensed Material and in
|
| 22 |
+
which the Licensed Material is translated, altered, arranged,
|
| 23 |
+
transformed, or otherwise modified in a manner requiring permission
|
| 24 |
+
under the Copyright and Similar Rights held by the Licensor. For
|
| 25 |
+
purposes of this Public License, where the Licensed Material is a
|
| 26 |
+
musical work, performance, or sound recording, Adapted Material is
|
| 27 |
+
always produced where the Licensed Material is synched in timed
|
| 28 |
+
relation with a moving image.
|
| 29 |
+
|
| 30 |
+
b. Adapter's License means the license You apply to Your Copyright and
|
| 31 |
+
Similar Rights in Your contributions to Adapted Material in accordance
|
| 32 |
+
with the terms and conditions of this Public License.
|
| 33 |
+
|
| 34 |
+
c. Copyright and Similar Rights means copyright and/or similar rights
|
| 35 |
+
closely related to copyright including, without limitation,
|
| 36 |
+
performance, broadcast, sound recording, and Sui Generis Database
|
| 37 |
+
Rights, without regard to how the rights are labeled or
|
| 38 |
+
categorized. For purposes of this Public License, the rights specified
|
| 39 |
+
in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
|
| 40 |
+
|
| 41 |
+
d. Effective Technological Measures means those measures that, in the
|
| 42 |
+
absence of proper authority, may not be circumvented under laws
|
| 43 |
+
fulfilling obligations under Article 11 of the WIPO Copyright Treaty
|
| 44 |
+
adopted on December 20, 1996, and/or similar international agreements.
|
| 45 |
+
|
| 46 |
+
e. Exceptions and Limitations means fair use, fair dealing, and/or any
|
| 47 |
+
other exception or limitation to Copyright and Similar Rights that
|
| 48 |
+
applies to Your use of the Licensed Material.
|
| 49 |
+
|
| 50 |
+
f. Licensed Material means the artistic or literary work, database, or
|
| 51 |
+
other material to which the Licensor applied this Public License.
|
| 52 |
+
|
| 53 |
+
g. Licensed Rights means the rights granted to You subject to the
|
| 54 |
+
terms and conditions of this Public License, which are limited to all
|
| 55 |
+
Copyright and Similar Rights that apply to Your use of the Licensed
|
| 56 |
+
Material and that the Licensor has authority to license.
|
| 57 |
+
|
| 58 |
+
h. Licensor means the individual(s) or entity(ies) granting rights
|
| 59 |
+
under this Public License.
|
| 60 |
+
|
| 61 |
+
i. Share means to provide material to the public by any means or
|
| 62 |
+
process that requires permission under the Licensed Rights, such as
|
| 63 |
+
reproduction, public display, public performance, distribution,
|
| 64 |
+
dissemination, communication, or importation, and to make material
|
| 65 |
+
available to the public including in ways that members of the public
|
| 66 |
+
may access the material from a place and at a time individually chosen
|
| 67 |
+
by them.
|
| 68 |
+
|
| 69 |
+
j. Sui Generis Database Rights means rights other than copyright
|
| 70 |
+
resulting from Directive 96/9/EC of the European Parliament and of the
|
| 71 |
+
Council of 11 March 1996 on the legal protection of databases, as
|
| 72 |
+
amended and/or succeeded, as well as other essentially equivalent
|
| 73 |
+
rights anywhere in the world.
|
| 74 |
+
|
| 75 |
+
k. You means the individual or entity exercising the Licensed Rights
|
| 76 |
+
under this Public License. Your has a corresponding meaning.
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
Section 2 – Scope.
|
| 80 |
+
|
| 81 |
+
a. License grant.
|
| 82 |
+
|
| 83 |
+
1. Subject to the terms and conditions of this Public License, the
|
| 84 |
+
Licensor hereby grants You a worldwide, royalty-free,
|
| 85 |
+
non-sublicensable, non-exclusive, irrevocable license to exercise the
|
| 86 |
+
Licensed Rights in the Licensed Material to:
|
| 87 |
+
A. reproduce and Share the Licensed Material, in whole or in part; and
|
| 88 |
+
B. produce, reproduce, and Share Adapted Material.
|
| 89 |
+
|
| 90 |
+
2. Exceptions and Limitations. For the avoidance of doubt, where
|
| 91 |
+
Exceptions and Limitations apply to Your use, this Public License does
|
| 92 |
+
not apply, and You do not need to comply with its terms and
|
| 93 |
+
conditions.
|
| 94 |
+
|
| 95 |
+
3. Term. The term of this Public License is specified in Section 6(a).
|
| 96 |
+
|
| 97 |
+
4. Media and formats; technical modifications allowed. The Licensor
|
| 98 |
+
authorizes You to exercise the Licensed Rights in all media and
|
| 99 |
+
formats whether now known or hereafter created, and to make technical
|
| 100 |
+
modifications necessary to do so. The Licensor waives and/or agrees
|
| 101 |
+
not to assert any right or authority to forbid You from making
|
| 102 |
+
technical modifications necessary to exercise the Licensed Rights,
|
| 103 |
+
including technical modifications necessary to circumvent Effective
|
| 104 |
+
Technological Measures. For purposes of this Public License, simply
|
| 105 |
+
making modifications authorized by this Section 2(a)(4) never produces
|
| 106 |
+
Adapted Material.
|
| 107 |
+
|
| 108 |
+
5. Downstream recipients.
|
| 109 |
+
|
| 110 |
+
A. Offer from the Licensor – Licensed Material. Every recipient of
|
| 111 |
+
the Licensed Material automatically receives an offer from the
|
| 112 |
+
Licensor to exercise the Licensed Rights under the terms and
|
| 113 |
+
conditions of this Public License.
|
| 114 |
+
|
| 115 |
+
B. No downstream restrictions. You may not offer or impose any
|
| 116 |
+
additional or different terms or conditions on, or apply any
|
| 117 |
+
Effective Technological Measures to, the Licensed Material if doing
|
| 118 |
+
so restricts exercise of the Licensed Rights by any recipient of the
|
| 119 |
+
Licensed Material.
|
| 120 |
+
|
| 121 |
+
6. No endorsement. Nothing in this Public License constitutes or may
|
| 122 |
+
be construed as permission to assert or imply that You are, or that
|
| 123 |
+
Your use of the Licensed Material is, connected with, or sponsored,
|
| 124 |
+
endorsed, or granted official status by, the Licensor or others
|
| 125 |
+
designated to receive attribution as provided in Section
|
| 126 |
+
3(a)(1)(A)(i).
|
| 127 |
+
|
| 128 |
+
b. Other rights.
|
| 129 |
+
|
| 130 |
+
1. Moral rights, such as the right of integrity, are not licensed
|
| 131 |
+
under this Public License, nor are publicity, privacy, and/or other
|
| 132 |
+
similar personality rights; however, to the extent possible, the
|
| 133 |
+
Licensor waives and/or agrees not to assert any such rights held by
|
| 134 |
+
the Licensor to the limited extent necessary to allow You to exercise
|
| 135 |
+
the Licensed Rights, but not otherwise.
|
| 136 |
+
|
| 137 |
+
2. Patent and trademark rights are not licensed under this Public License.
|
| 138 |
+
|
| 139 |
+
3. To the extent possible, the Licensor waives any right to collect
|
| 140 |
+
royalties from You for the exercise of the Licensed Rights, whether
|
| 141 |
+
directly or through a collecting society under any voluntary or
|
| 142 |
+
waivable statutory or compulsory licensing scheme. In all other cases
|
| 143 |
+
the Licensor expressly reserves any right to collect such royalties.
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
Section 3 – License Conditions.
|
| 147 |
+
|
| 148 |
+
Your exercise of the Licensed Rights is expressly made subject to the
|
| 149 |
+
following conditions.
|
| 150 |
+
|
| 151 |
+
a. Attribution.
|
| 152 |
+
|
| 153 |
+
1. If You Share the Licensed Material (including in modified form),
|
| 154 |
+
You must:
|
| 155 |
+
|
| 156 |
+
A. retain the following if it is supplied by the Licensor with the
|
| 157 |
+
Licensed Material:
|
| 158 |
+
|
| 159 |
+
i. identification of the creator(s) of the Licensed Material and
|
| 160 |
+
any others designated to receive attribution, in any reasonable
|
| 161 |
+
manner requested by the Licensor (including by pseudonym if
|
| 162 |
+
designated);
|
| 163 |
+
|
| 164 |
+
ii. a copyright notice;
|
| 165 |
+
|
| 166 |
+
iii. a notice that refers to this Public License;
|
| 167 |
+
|
| 168 |
+
iv. a notice that refers to the disclaimer of warranties;
|
| 169 |
+
|
| 170 |
+
v. a URI or hyperlink to the Licensed Material to the extent
|
| 171 |
+
reasonably practicable;
|
| 172 |
+
|
| 173 |
+
B. indicate if You modified the Licensed Material and retain an
|
| 174 |
+
indication of any previous modifications; and
|
| 175 |
+
|
| 176 |
+
C. indicate the Licensed Material is licensed under this Public
|
| 177 |
+
License, and include the text of, or the URI or hyperlink to, this
|
| 178 |
+
Public License.
|
| 179 |
+
|
| 180 |
+
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable
|
| 181 |
+
manner based on the medium, means, and context in which You Share the
|
| 182 |
+
Licensed Material. For example, it may be reasonable to satisfy the
|
| 183 |
+
conditions by providing a URI or hyperlink to a resource that includes
|
| 184 |
+
the required information.
|
| 185 |
+
|
| 186 |
+
3. If requested by the Licensor, You must remove any of the
|
| 187 |
+
information required by Section 3(a)(1)(A) to the extent reasonably
|
| 188 |
+
practicable.
|
| 189 |
+
|
| 190 |
+
4. If You Share Adapted Material You produce, the Adapter's License
|
| 191 |
+
You apply must not prevent recipients of the Adapted Material from
|
| 192 |
+
complying with this Public License.
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
Section 4 – Sui Generis Database Rights.
|
| 196 |
+
|
| 197 |
+
Where the Licensed Rights include Sui Generis Database Rights that
|
| 198 |
+
apply to Your use of the Licensed Material:
|
| 199 |
+
|
| 200 |
+
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to
|
| 201 |
+
extract, reuse, reproduce, and Share all or a substantial portion of
|
| 202 |
+
the contents of the database;
|
| 203 |
+
|
| 204 |
+
b. if You include all or a substantial portion of the database
|
| 205 |
+
contents in a database in which You have Sui Generis Database Rights,
|
| 206 |
+
then the database in which You have Sui Generis Database Rights (but
|
| 207 |
+
not its individual contents) is Adapted Material; and
|
| 208 |
+
|
| 209 |
+
c. You must comply with the conditions in Section 3(a) if You Share
|
| 210 |
+
all or a substantial portion of the contents of the database.
|
| 211 |
+
|
| 212 |
+
For the avoidance of doubt, this Section 4 supplements and does not
|
| 213 |
+
replace Your obligations under this Public License where the Licensed
|
| 214 |
+
Rights include other Copyright and Similar Rights.
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
Section 5 – Disclaimer of Warranties and Limitation of Liability.
|
| 218 |
+
|
| 219 |
+
a. Unless otherwise separately undertaken by the Licensor, to the
|
| 220 |
+
extent possible, the Licensor offers the Licensed Material as-is and
|
| 221 |
+
as-available, and makes no representations or warranties of any kind
|
| 222 |
+
concerning the Licensed Material, whether express, implied, statutory,
|
| 223 |
+
or other. This includes, without limitation, warranties of title,
|
| 224 |
+
merchantability, fitness for a particular purpose, non-infringement,
|
| 225 |
+
absence of latent or other defects, accuracy, or the presence or
|
| 226 |
+
absence of errors, whether or not known or discoverable. Where
|
| 227 |
+
disclaimers of warranties are not allowed in full or in part, this
|
| 228 |
+
disclaimer may not apply to You.
|
| 229 |
+
|
| 230 |
+
b. To the extent possible, in no event will the Licensor be liable to
|
| 231 |
+
You on any legal theory (including, without limitation, negligence) or
|
| 232 |
+
otherwise for any direct, special, indirect, incidental,
|
| 233 |
+
consequential, punitive, exemplary, or other losses, costs, expenses,
|
| 234 |
+
or damages arising out of this Public License or use of the Licensed
|
| 235 |
+
Material, even if the Licensor has been advised of the possibility of
|
| 236 |
+
such losses, costs, expenses, or damages. Where a limitation of
|
| 237 |
+
liability is not allowed in full or in part, this limitation may not
|
| 238 |
+
apply to You.
|
| 239 |
+
|
| 240 |
+
c. The disclaimer of warranties and limitation of liability provided
|
| 241 |
+
above shall be interpreted in a manner that, to the extent possible,
|
| 242 |
+
most closely approximates an absolute disclaimer and waiver of all
|
| 243 |
+
liability.
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
Section 6 – Term and Termination.
|
| 247 |
+
|
| 248 |
+
a. This Public License applies for the term of the Copyright and
|
| 249 |
+
Similar Rights licensed here. However, if You fail to comply with this
|
| 250 |
+
Public License, then Your rights under this Public License terminate
|
| 251 |
+
automatically.
|
| 252 |
+
|
| 253 |
+
b. Where Your right to use the Licensed Material has terminated under
|
| 254 |
+
Section 6(a), it reinstates:
|
| 255 |
+
|
| 256 |
+
1. automatically as of the date the violation is cured, provided it
|
| 257 |
+
is cured within 30 days of Your discovery of the violation; or
|
| 258 |
+
|
| 259 |
+
2. upon express reinstatement by the Licensor.
|
| 260 |
+
|
| 261 |
+
For the avoidance of doubt, this Section 6(b) does not affect any
|
| 262 |
+
right the Licensor may have to seek remedies for Your violations of
|
| 263 |
+
this Public License.
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
c. For the avoidance of doubt, the Licensor may also offer the
|
| 267 |
+
Licensed Material under separate terms or conditions or stop
|
| 268 |
+
distributing the Licensed Material at any time; however, doing so will
|
| 269 |
+
not terminate this Public License.
|
| 270 |
+
|
| 271 |
+
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
|
| 272 |
+
License.
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
Section 7 – Other Terms and Conditions.
|
| 276 |
+
|
| 277 |
+
a. The Licensor shall not be bound by any additional or different
|
| 278 |
+
terms or conditions communicated by You unless expressly agreed.
|
| 279 |
+
|
| 280 |
+
b. Any arrangements, understandings, or agreements regarding the
|
| 281 |
+
Licensed Material not stated herein are separate from and independent
|
| 282 |
+
of the terms and conditions of this Public License.
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
Section 8 – Interpretation.
|
| 286 |
+
|
| 287 |
+
a. For the avoidance of doubt, this Public License does not, and shall
|
| 288 |
+
not be interpreted to, reduce, limit, restrict, or impose conditions
|
| 289 |
+
on any use of the Licensed Material that could lawfully be made
|
| 290 |
+
without permission under this Public License.
|
| 291 |
+
|
| 292 |
+
b. To the extent possible, if any provision of this Public License is
|
| 293 |
+
deemed unenforceable, it shall be automatically reformed to the
|
| 294 |
+
minimum extent necessary to make it enforceable. If the provision
|
| 295 |
+
cannot be reformed, it shall be severed from this Public License
|
| 296 |
+
without affecting the enforceability of the remaining terms and
|
| 297 |
+
conditions.
|
| 298 |
+
|
| 299 |
+
c. No term or condition of this Public License will be waived and no
|
| 300 |
+
failure to comply consented to unless expressly agreed to by the
|
| 301 |
+
Licensor.
|
| 302 |
+
|
| 303 |
+
d. Nothing in this Public License constitutes or may be interpreted as
|
| 304 |
+
a limitation upon, or waiver of, any privileges and immunities that
|
| 305 |
+
apply to the Licensor or You, including from the legal processes of
|
| 306 |
+
any jurisdiction or authority.
|
| 307 |
+
|
ATTRIBUTION.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Attribution and licences
|
| 2 |
+
|
| 3 |
+
## This release
|
| 4 |
+
|
| 5 |
+
The trained heads (`eot-heads-v32.onnx`), the decision-rule tables (`rules/`), the evaluation
|
| 6 |
+
script and manifests (`eval/`) and the model card are released by susuROBO under the BSD 2-Clause
|
| 7 |
+
License (`LICENSE`).
|
| 8 |
+
|
| 9 |
+
## Encoder: Smart Turn v3.2 (Daily / Pipecat)
|
| 10 |
+
|
| 11 |
+
`smart-turn-v3.2-encoder-pooled.onnx` is Pipecat's `smart-turn-v3.2-gpu.onnx`
|
| 12 |
+
(https://huggingface.co/pipecat-ai/smart-turn-v3, BSD 2-Clause, Copyright (c) 2024–2025 Daily)
|
| 13 |
+
with one change: the attention-pooled 384-d vector that feeds the model's own classifier is added
|
| 14 |
+
as a second graph output (`sum_1`). Weights are unchanged, so the first output (`logits`) is
|
| 15 |
+
bit-exact with the original model. The encoder architecture is OpenAI's Whisper-tiny encoder
|
| 16 |
+
(MIT), as fine-tuned by Pipecat on `pipecat-ai/smart-turn-data-v3.2-train`.
|
| 17 |
+
|
| 18 |
+
## Training data of the heads
|
| 19 |
+
|
| 20 |
+
- `pipecat-ai/smart-turn-data-v3.2-train` (Pipecat; the τ-padded and pre-offset variants described
|
| 21 |
+
in the model card are derived from it on the fly and are not redistributed).
|
| 22 |
+
- **AMI Meeting Corpus** (University of Edinburgh and the AMI consortium),
|
| 23 |
+
https://groups.inf.ed.ac.uk/ami/corpus/, released under the Creative Commons Attribution 4.0
|
| 24 |
+
International licence (`AMI_LICENCE.txt`, the licence text shipped with the corpus annotations,
|
| 25 |
+
release 1.7). The per-speaker close-talking headset recordings and the NXT word alignments were
|
| 26 |
+
used; the heads were trained on the 137 Kaldi-recipe training meetings.
|
| 27 |
+
|
| 28 |
+
Citation: J. Carletta et al., "The AMI Meeting Corpus: A Pre-announcement", in *Machine Learning
|
| 29 |
+
for Multimodal Interaction (MLMI 2005)*, LNCS 3869, Springer, 2006, pp. 28–39.
|
| 30 |
+
|
| 31 |
+
## Evaluation manifests (`eval/ami_dev.csv`, `eval/ami_test.csv`)
|
| 32 |
+
|
| 33 |
+
Derived from the AMI Meeting Corpus (CC BY 4.0) and redistributed under the same licence with
|
| 34 |
+
attribution to the corpus authors above. Each row is one labelled silence of one speaker:
|
| 35 |
+
`meeting`, `speaker` (NXT agent letter), `channel` (headset number in the corpus audio), `t` (the
|
| 36 |
+
speaker's VAD stop, seconds into the meeting), `label` (`complete`: another participant took the
|
| 37 |
+
floor within 3 s; `incomplete`: the speaker resumed), `gap_s` (incomplete: length of the pause),
|
| 38 |
+
`b_onset_s` (complete: the next speaker's onset after `t`), `a_turn_s` (speech in the speaker's turn
|
| 39 |
+
so far). The VAD comes from Silero run on the headset audio and gated by the transcript; the
|
| 40 |
+
labelling rules are documented in the public repository (`docs/a2_ami.md`). The dev split is the
|
| 41 |
+
Kaldi AMI recipe's dev set (18 meetings), the test split its eval set (16 meetings). Audio is not
|
| 42 |
+
redistributed: `eval/ami_eval.py` downloads the headset files it needs from the corpus mirror.
|
| 43 |
+
|
| 44 |
+
## Not used
|
| 45 |
+
|
| 46 |
+
Nothing in this release was trained or fitted on Meta's Seamless Interaction dataset (CC BY-NC) or
|
| 47 |
+
on Meta's Casual Conversations dataset.
|
LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
BSD 2-Clause License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026, susuROBO
|
| 4 |
+
Copyright (c) 2024–2025, Daily
|
| 5 |
+
|
| 6 |
+
Redistribution and use in source and binary forms, with or without
|
| 7 |
+
modification, are permitted provided that the following conditions are met:
|
| 8 |
+
|
| 9 |
+
1. Redistributions of source code must retain the above copyright notice, this
|
| 10 |
+
list of conditions and the following disclaimer.
|
| 11 |
+
|
| 12 |
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
| 13 |
+
this list of conditions and the following disclaimer in the documentation
|
| 14 |
+
and/or other materials provided with the distribution.
|
| 15 |
+
|
| 16 |
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
| 17 |
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
| 18 |
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
| 19 |
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
| 20 |
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
| 21 |
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
| 22 |
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
| 23 |
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
| 24 |
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
| 25 |
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
README.md
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: bsd-2-clause
|
| 3 |
+
library_name: onnx
|
| 4 |
+
pipeline_tag: audio-classification
|
| 5 |
+
tags:
|
| 6 |
+
- turn-detection
|
| 7 |
+
- end-of-turn
|
| 8 |
+
- voice-agents
|
| 9 |
+
- pipecat
|
| 10 |
+
- streaming
|
| 11 |
+
- onnx
|
| 12 |
+
datasets:
|
| 13 |
+
- pipecat-ai/smart-turn-data-v3.2-train
|
| 14 |
+
language:
|
| 15 |
+
- en
|
| 16 |
+
- multilingual
|
| 17 |
+
base_model: pipecat-ai/smart-turn-v3
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
# Smart Turn streaming end-of-turn heads (R1, audio-only)
|
| 21 |
+
|
| 22 |
+
A silence-aware, streaming end-of-turn detector for voice agents, built on Pipecat's frozen
|
| 23 |
+
[Smart Turn v3.2](https://huggingface.co/pipecat-ai/smart-turn-v3) encoder. Instead of one query
|
| 24 |
+
at the VAD stop followed by a 3 s timeout, it scores the conversation every 100 ms on the CPU and
|
| 25 |
+
decides at 0.2 / 0.5 / 1.2 s of silence with thresholds fitted to a false-alarm budget. On real
|
| 26 |
+
multi-party conversation (AMI Meeting Corpus) it ends the turn on 5 % of mid-turn pauses where the
|
| 27 |
+
stock rule fires on 42 %, and brings the 90th-percentile response time from 3.2 s to 1.3 s. It runs
|
| 28 |
+
at about 9 % of one Apple M4 core single-threaded, with numpy and onnxruntime only.
|
| 29 |
+
|
| 30 |
+
Code (Pipecat 1.14 turn analyzer, eager-then-commit stop strategy, training and evaluation):
|
| 31 |
+
[github.com/OpenHRIai/smart-turn-streaming](https://github.com/OpenHRIai/smart-turn-streaming).
|
| 32 |
+
|
| 33 |
+

|
| 34 |
+
|
| 35 |
+

|
| 36 |
+
|
| 37 |
+

|
| 38 |
+
|
| 39 |
+
## Files
|
| 40 |
+
|
| 41 |
+
| file | size | what |
|
| 42 |
+
|---|---|---|
|
| 43 |
+
| `smart-turn-v3.2-encoder-pooled.onnx` | 31 MB | Pipecat's Smart Turn v3.2 (Whisper-tiny encoder + its classifier), weights unchanged, with the pooled 384-d vector exposed as a second output. Input `input_features` (1, 80, 800) Whisper log-mel of the last 8 s; outputs `logits` (the stock model's P(complete)) and `sum_1` (the pooled vector). |
|
| 44 |
+
| `eot-heads-v32.onnx` | 0.7 MB | The trained heads: LayerNorm + Linear on the pooled vector, a GRU over the last second of log-mel (20 × 80 at 50 ms), the silence length, a fusion MLP. Inputs `pooled` (1, 384), `mel20` (1, 20, 80), `tau_feat` (1, 3) = [τ / 2.5, log1p(τ), speaking]; output `probs` (1, 2) = [P(turn complete), P(turn ends within 0.4 s)]. 172k parameters. |
|
| 45 |
+
| `rules/theta_tables.csv` | | Commit thresholds θ(0.2 s), θ(0.5 s), θ(1.2 s) fitted on AMI dev at false-alarm budgets of 5 / 10 / 15 %, with their AMI test numbers. |
|
| 46 |
+
| `rules/eager_tables.csv` | | Eager thresholds for Pipecat's speculation path, fitted on AMI dev at budgets of LLM generations spent on pauses (20–50 %). |
|
| 47 |
+
| `eval/ami_eval.py`, `eval/ami_{dev,test}.csv`, `eval/ami_eval_reference.md` | | Reproduces the AMI tables below from the public corpus audio (numpy, onnxruntime, soundfile); the reference file is its output on 2026-10-25. |
|
| 48 |
+
| `LICENSE`, `AMI_LICENCE.txt`, `ATTRIBUTION.md` | | BSD 2-Clause for this release; CC BY 4.0 for the AMI-derived manifests; sources and citations. |
|
| 49 |
+
|
| 50 |
+
## How it works
|
| 51 |
+
|
| 52 |
+
Two cadences in one object on one CPU thread, plus one worker thread for the encoder:
|
| 53 |
+
|
| 54 |
+
| path | when | work | cost (Apple M4, 1 ORT thread) |
|
| 55 |
+
|---|---|---|---|
|
| 56 |
+
| mel | every 20 ms audio frame | incremental Whisper log-mel, one 10 ms column at a time (exact to 0.003 against the reference extractor on real audio) | 0.02 ms |
|
| 57 |
+
| heads | every 100 ms, and at each silence-clock point | last 1 s of mel → GRU, cached pooled vector, τ → P(complete) | 0.7 ms |
|
| 58 |
+
| encoder | every 320 ms while the user speaks; at τ = 0, 0.2, 0.5, 0.8, 1.2, 1.8, 2.5 s of silence (the grid the heads were trained on), where the decision waits for it | 8 s window → Whisper-tiny → pooled vector, on a worker thread | 22 ms (46 ms when the core is otherwise idle) |
|
| 59 |
+
|
| 60 |
+
The decision rule queries P(complete) at 0.2, 0.5 and 1.2 s of silence while the pause lasts and
|
| 61 |
+
commits at the first offset where it exceeds θ(offset); Pipecat's `stop_secs` (3 s) stays as the
|
| 62 |
+
backstop. The budget is the product knob: `far10` for an agent that must not interrupt, `far15` when
|
| 63 |
+
a fast reply matters more. A second, looser table (`eager_tables.csv`) on the same decision points
|
| 64 |
+
drives Pipecat's speculation path (`UserTurnSpeculation` → `SpeculationGate`): the LLM starts
|
| 65 |
+
generating at the eager point, the response is held until the commit and discarded if the user
|
| 66 |
+
resumes.
|
| 67 |
+
|
| 68 |
+
Why silence awareness has to be trained: 79 % of the clips in `smart-turn-data-v3.2` end within
|
| 69 |
+
0.2 s of the speech, so the published models never saw a pause, and their false-alarm rate on
|
| 70 |
+
incomplete utterances rises from 5 % at the offset to 19 % after 2.5 s of silence. The heads were
|
| 71 |
+
trained on windows ending τ ∈ {0 … 2.5} s after the acoustic offset (clips padded with their own
|
| 72 |
+
room tone) plus 43k real conversational pauses from AMI. The GRU over the last second of mel is the
|
| 73 |
+
component that carries the silence awareness (AUC at 1.2 s of silence 0.74 with it, 0.66 without).
|
| 74 |
+
|
| 75 |
+
## Results
|
| 76 |
+
|
| 77 |
+
### AMI Meeting Corpus, test split (511 turn ends, 4,304 mid-turn pauses, 16 meetings, 4-party)
|
| 78 |
+
|
| 79 |
+
Offline, windows ending at τ seconds of silence (`eval/ami_eval.py` reproduces this table):
|
| 80 |
+
|
| 81 |
+
| τ (s) | AUC heads | AUC frozen Smart Turn v3.2 |
|
| 82 |
+
|---|---|---|
|
| 83 |
+
| 0.2 | 0.775 | 0.671 |
|
| 84 |
+
| 0.5 | 0.776 | 0.638 |
|
| 85 |
+
| 1.2 | 0.742 | 0.575 |
|
| 86 |
+
| 2.5 | 0.735 | 0.619 |
|
| 87 |
+
|
| 88 |
+
Over three training seeds the AUC at 0.2 s is 0.78 ± 0.01 and at 1.2 s 0.73 ± 0.01.
|
| 89 |
+
|
| 90 |
+
Decision rule, thresholds fitted on AMI dev, scored on AMI test; a turn end with no commit is
|
| 91 |
+
charged the 3 s timeout:
|
| 92 |
+
|
| 93 |
+
| rule | θ at 0.2 / 0.5 / 1.2 s | recall | false alarms on pauses | mean latency | FAR on pauses ≥ 1.5 s |
|
| 94 |
+
|---|---|---|---|---|---|
|
| 95 |
+
| stock rule (one query at the VAD stop, θ 0.5), frozen v3.2 | 0.50 / – / – | 0.51 | 30.4 % | 1.56 s | 0.45 |
|
| 96 |
+
| stock rule, heads | 0.50 / – / – | 0.64 | 25.5 % | 1.21 s | 0.46 |
|
| 97 |
+
| re-query, 10 % budget, frozen v3.2 | 1.00 / 1.00 / 0.28 | 0.70 | 10.6 % | 1.74 s | 0.61 |
|
| 98 |
+
| **re-query, 10 % budget, heads (`far10`)** | 0.92 / 0.98 / 0.86 | **0.80** | **9.4 %** | **1.48 s** | 0.50 |
|
| 99 |
+
| re-query, 15 % budget, frozen v3.2 | 1.00 / 1.00 / 0.02 | 0.87 | 13.7 % | 1.44 s | 0.78 |
|
| 100 |
+
| **re-query, 15 % budget, heads (`far15`)** | 0.98 / 0.88 / 0.80 | **0.86** | **13.3 %** | **1.21 s** | 0.61 |
|
| 101 |
+
| no model: end every turn at 1.2 s of silence | – | 1.00 | 18.0 % | 1.20 s | 1.00 |
|
| 102 |
+
|
| 103 |
+
### Regression guard: `smart-turn-data-v3.2` test (6,000 clips, balanced)
|
| 104 |
+
|
| 105 |
+
| τ (s) | AUC heads / frozen v3.2 | recall at 5 % FAR, heads / frozen |
|
| 106 |
+
|---|---|---|
|
| 107 |
+
| 0.0 | 0.975 / 0.978 | 0.87 / 0.89 |
|
| 108 |
+
| 0.2 | 0.976 / 0.976 | 0.87 / 0.86 |
|
| 109 |
+
| 1.2 | 0.972 / 0.964 | 0.84 / 0.77 |
|
| 110 |
+
| 2.5 | 0.965 / 0.955 | 0.81 / 0.72 |
|
| 111 |
+
|
| 112 |
+
Per-language and per-source AUC at τ = 0.2 s is unchanged within noise. The heads are calibrated
|
| 113 |
+
differently from the frozen model (class-weighted loss), so use the fitted tables, not θ = 0.5.
|
| 114 |
+
|
| 115 |
+
### End to end in Pipecat 1.14 (real-time replay of 160 AMI test silences through Silero VAD and `UserTurnProcessor`)
|
| 116 |
+
|
| 117 |
+
| analyzer and rule | false alarms on pauses | turn ends committed by the model (not the 3 s backstop) | commit latency p50 / p90 | response time p50 / p90 / mean at 0.8 s of LLM latency | LLM generations spent on pauses |
|
| 118 |
+
|---|---|---|---|---|---|
|
| 119 |
+
| stock `LocalSmartTurnAnalyzerV3`, v3.2 fp32 | 42.5 % | 0.70 | 0.23 / 3.20 s | 1.03 / 4.00 / 1.89 s | 42.5 % |
|
| 120 |
+
| streaming, `far10` | 2.5 % | 0.78 | 1.21 / 3.00 s | 2.01 / 3.80 / 2.36 s | 2.5 % |
|
| 121 |
+
| streaming, `far15` | 5.0 % | 0.93 | 1.19 / 1.27 s | 1.99 / 2.07 / 1.92 s | 5.0 % |
|
| 122 |
+
| **streaming, `far15` + eager 30 %** | **5.0 %** | **0.93** | 1.19 / 1.26 s | **1.31 / 2.04 / 1.55 s** | 18.8 % |
|
| 123 |
+
|
| 124 |
+
With the eager stage the median response time matches the stock analyzer within 0.3 s, the p90 is
|
| 125 |
+
halved, an eighth of the user turns are split, and only 2.5 % of pauses are audibly interrupted
|
| 126 |
+
(stock: 7.5 %).
|
| 127 |
+
|
| 128 |
+
### Cost (Apple M4, single ORT thread, numpy + onnxruntime)
|
| 129 |
+
|
| 130 |
+
| | |
|
| 131 |
+
|---|---|
|
| 132 |
+
| detector share of one core, tight loop | 9.1 % (encoder refreshes included) |
|
| 133 |
+
| whole Pipecat process (Silero VAD, pipeline, analyzer) | 16–19 % of one core at real-time pace (stock analyzer: 9 %) |
|
| 134 |
+
| heads hop | 0.7 ms |
|
| 135 |
+
| encoder refresh | 22 ms (46 ms at real-time pace on an otherwise idle machine) |
|
| 136 |
+
| pipeline-task wall time per audio frame, encoder on the worker thread | p99 4–5 ms (inline: 45 ms); the 0.5 s and 1.2 s decisions 0.2 ms |
|
| 137 |
+
| resident memory added by the analyzer | +114 MB (stock: +76 MB) |
|
| 138 |
+
| model files | 32 MB |
|
| 139 |
+
|
| 140 |
+
A cloud vCPU is about 3× slower than an M4 core for this encoder (Pipecat's own figure for the same
|
| 141 |
+
architecture on Pipecat Cloud), so expect roughly 25–30 % of one vCPU for the detector alone.
|
| 142 |
+
|
| 143 |
+
## Usage
|
| 144 |
+
|
| 145 |
+
### In Pipecat (recommended)
|
| 146 |
+
|
| 147 |
+
```python
|
| 148 |
+
from eot.pipecat_analyzer import StreamingEOTAnalyzer, StreamingEOTParams
|
| 149 |
+
from eot.pipecat_eager_strategy import TurnAnalyzerEagerUserTurnStopStrategy
|
| 150 |
+
from pipecat.turns.user_turn_strategies import UserTurnStrategies
|
| 151 |
+
from pipecat.turns.user_start import VADUserTurnStartStrategy
|
| 152 |
+
|
| 153 |
+
analyzer = StreamingEOTAnalyzer(
|
| 154 |
+
encoder_onnx="smart-turn-v3.2-encoder-pooled.onnx",
|
| 155 |
+
heads_onnx="eot-heads-v32.onnx",
|
| 156 |
+
params=StreamingEOTParams.from_rule("far15", eager="far15_gen30"), # commit and eager tables
|
| 157 |
+
)
|
| 158 |
+
strategies = UserTurnStrategies(
|
| 159 |
+
start=[VADUserTurnStartStrategy()],
|
| 160 |
+
stop=[TurnAnalyzerEagerUserTurnStopStrategy(turn_analyzer=analyzer)], # or pipecat's TurnAnalyzerUserTurnStopStrategy
|
| 161 |
+
)
|
| 162 |
+
```
|
| 163 |
+
|
| 164 |
+
`eot/` is in the repository linked above; the analyzer needs 16 kHz audio and Pipecat's default VAD
|
| 165 |
+
`stop_secs` of 0.2 s. `StreamingEOTParams.from_rule("far10")` without an eager table gives the
|
| 166 |
+
one-stage rule.
|
| 167 |
+
|
| 168 |
+
### Standalone streaming (numpy + onnxruntime)
|
| 169 |
+
|
| 170 |
+
```python
|
| 171 |
+
from eot.streaming import StreamingEOT
|
| 172 |
+
|
| 173 |
+
eot = StreamingEOT("smart-turn-v3.2-encoder-pooled.onnx", "eot-heads-v32.onnx", worker=True)
|
| 174 |
+
for frame, is_speech in audio_frames(): # 20 ms float32 frames in [-1, 1] and the VAD's verdict
|
| 175 |
+
out = eot.push(frame, is_speech) # None, or {"p_end", "p_eager", "tau", "stale_s", ...} at a hop
|
| 176 |
+
if out and not out["speaking"] and out["tau"] >= 1.2 and out["p_end"] >= 0.80:
|
| 177 |
+
... # end of turn (the far15 table's 1.2 s threshold)
|
| 178 |
+
```
|
| 179 |
+
|
| 180 |
+
### One window, offline
|
| 181 |
+
|
| 182 |
+
```python
|
| 183 |
+
import numpy as np, onnxruntime as ort
|
| 184 |
+
enc = ort.InferenceSession("smart-turn-v3.2-encoder-pooled.onnx")
|
| 185 |
+
heads = ort.InferenceSession("eot-heads-v32.onnx")
|
| 186 |
+
feats = whisper_logmel(last_8_seconds) # (80, 800); eval/ami_eval.py has a numpy implementation
|
| 187 |
+
logits, pooled = enc.run(None, {"input_features": feats[None]})
|
| 188 |
+
mel20 = feats[:, -100:].reshape(80, 20, 5).mean(-1).T[None] # last 1 s at 50 ms steps
|
| 189 |
+
tau = 0.5 # seconds of silence at the window end
|
| 190 |
+
tau_feat = np.array([[tau / 2.5, np.log1p(tau), 0.0]], np.float32)
|
| 191 |
+
p_end, p_eager = heads.run(None, {"pooled": pooled, "mel20": mel20, "tau_feat": tau_feat})[0][0]
|
| 192 |
+
```
|
| 193 |
+
|
| 194 |
+
## Limitations
|
| 195 |
+
|
| 196 |
+
- **Conversational evaluation is English-only.** The silence-aware behaviour was trained and measured on
|
| 197 |
+
English meetings (AMI); the per-language check covers 23 languages but only on isolated utterances
|
| 198 |
+
(`smart-turn-data`), where the heads stay within noise of the frozen model in every language. Whether
|
| 199 |
+
the pause thresholds transfer to other languages' conversational timing is not measured.
|
| 200 |
+
- Meetings have a ceiling set by the labels: ending every turn at 1.2 s of silence
|
| 201 |
+
gives recall 1.0 at 18 % false alarms, and below that budget about a quarter of turn ends still
|
| 202 |
+
wait for the backstop, for every model tried.
|
| 203 |
+
- Long pauses (≥ 1.5 s) remain the weak spot: 50–60 % false alarms at the 10–15 % budgets.
|
| 204 |
+
- The second output (`p_eager`, "the turn ends within 0.4 s") is not usable as a free-running
|
| 205 |
+
trigger during speech (AUC 0.70 on AMI; ~22 false fires per minute of speech). The eager tables use
|
| 206 |
+
P(complete) at the silence-clock points instead.
|
| 207 |
+
- Every decision waits for an encoder refresh of its own window: the 0.2 s decision at the VAD stop for up
|
| 208 |
+
to 50 ms (`wait_s`), the 0.5 s and 1.2 s decisions for up to 80 ms (`max_defer_s`); the encoder takes
|
| 209 |
+
22–46 ms on an M4 and about 3× that on a cloud vCPU, so raise the allowances there. If the refresh is
|
| 210 |
+
late the decision uses the cached vector, up to 320 ms old, and the analyzer records the staleness of
|
| 211 |
+
every decision. With the refresh in time the streaming decisions equal the offline evaluation's within
|
| 212 |
+
0.001 AUC on AMI.
|
| 213 |
+
- Labels are by outcome, not intent: a speaker who was cut off counts as a complete turn.
|
| 214 |
+
|
| 215 |
+
## Reproduce the AMI tables
|
| 216 |
+
|
| 217 |
+
```
|
| 218 |
+
pip install numpy onnxruntime soundfile
|
| 219 |
+
python eval/ami_eval.py --audio-dir ./ami_audio # downloads 34 headset files (~3 GB), ~10 min on an M4
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
## Citation
|
| 223 |
+
|
| 224 |
+
If you use this model, please cite Pipecat's Smart Turn (the encoder) and the AMI Meeting Corpus
|
| 225 |
+
(`ATTRIBUTION.md`).
|
SHA256SUMS
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
8f5e4b1f7e4dfff70198bb2867f14b75f2e3afd94944334de21ac4b9712fa29e README.md
|
| 2 |
+
dd4613e925f3d6770eb3705863dc30d0cd6427456445ae533d5906bf6687ad12 LICENSE
|
| 3 |
+
d52fd9c7c19eec9f0bec3107554c10937203e261259d73b01637a61501fec7f7 AMI_LICENCE.txt
|
| 4 |
+
2418f9016f3ba767fc400e9b6732bb3b6a31c8e2c72672861a084f44a7499c87 ATTRIBUTION.md
|
| 5 |
+
c9c1d5905088965e754fcf718765c897a779658b5fdc6ae5de471eeab41c49c0 rules/theta_tables.csv
|
| 6 |
+
3a63a46477243d1ff6e7620a96aa8cde257209b40506202312f6aff139c4e5ad rules/eager_tables.csv
|
| 7 |
+
ccdfe3ad97d9ff204644c53a027610bfaf50c20b00ee991931109a016fb3e942 eval/ami_eval.py
|
| 8 |
+
9f4fb10b3f0488e5bb2faa0b3247364c68b8e074db5e77be39045008e05049cc eval/ami_dev.csv
|
| 9 |
+
77ca3e93c35c951d2cc337154d70d94ff1f9ba71e06b3d9e08cc958f0b1034a0 eval/ami_test.csv
|
| 10 |
+
8fccc07d5262daa94989fb7f5ff92a993a38038291e33c5cb217e24b8d53e4ec eval/ami_eval_reference.md
|
| 11 |
+
37e2a4fe22e1c4efaeba4df40141e422a3b96519db9160ae9134962a936ba4ea smart-turn-v3.2-encoder-pooled.onnx
|
| 12 |
+
8d5b4039c9d7c27e22e84d372c343077489f2fe291ccd67caeeb5ce45b05ce1a eot-heads-v32.onnx
|
eot-heads-v32.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8d5b4039c9d7c27e22e84d372c343077489f2fe291ccd67caeeb5ce45b05ce1a
|
| 3 |
+
size 696344
|
eval/ami_dev.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
eval/ami_eval.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Reproduce the AMI end-of-turn evaluation of the Smart Turn v2 streaming heads (release R1).
|
| 3 |
+
|
| 4 |
+
python ami_eval.py --audio-dir ./ami_audio # downloads the 34 test (and dev) headset files, ~3 GB
|
| 5 |
+
python ami_eval.py --audio-dir ./ami_audio --splits test --no-fit # test only, shipped thresholds, ~5 min
|
| 6 |
+
|
| 7 |
+
What it computes, from the public AMI Meeting Corpus audio and the labelled-silence manifests shipped
|
| 8 |
+
next to this script (``ami_dev.csv``, ``ami_test.csv``; CC BY 4.0, see ../ATTRIBUTION.md):
|
| 9 |
+
|
| 10 |
+
1. For every labelled silence (a VAD stop of speaker A at time ``t``) and every silence offset tau in
|
| 11 |
+
``--taus`` the 8 s window of A's headset audio ending at ``t + tau``. For complete turns the audio after
|
| 12 |
+
the next speaker's onset (``t + b_onset_s``) is replaced by A's own room tone, because a deployed
|
| 13 |
+
microphone never hears the agent's reply; for incomplete turns a window exists only while the pause is
|
| 14 |
+
still going on (``tau < gap_s``).
|
| 15 |
+
2. Whisper log-mel features (80 x 800, as ``WhisperFeatureExtractor(chunk_length=8)`` with
|
| 16 |
+
``do_normalize=True``), the frozen Smart Turn v3.2 encoder with its pooled 384-d vector exposed, and
|
| 17 |
+
the trained heads (pooled vector + last 1 s of mel + tau) -> P(complete). The frozen model's own
|
| 18 |
+
probability is kept as the baseline.
|
| 19 |
+
3. AUC (complete vs incomplete) per tau for the heads and the frozen model, and the deployed decision
|
| 20 |
+
rule: query at 0.2 / 0.5 / 1.2 s of silence while the pause lasts, commit COMPLETE at the first offset
|
| 21 |
+
with P >= theta(offset); a complete with no commit is charged the 3 s timeout. Thresholds are the
|
| 22 |
+
shipped tables (``../rules/theta_tables.csv``) and, unless ``--no-fit``, re-fitted on the dev split under
|
| 23 |
+
false-alarm budgets of 5 / 10 / 15 % (exhaustive grid, min mean latency), then scored on test.
|
| 24 |
+
|
| 25 |
+
Dependencies: numpy, onnxruntime, soundfile. CPU only; about 16k encoder windows for dev + test at the
|
| 26 |
+
three rule offsets (~7 min single-threaded on an Apple M4). Scores are cached in ``--cache`` so the
|
| 27 |
+
tables can be re-made without re-running the models.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
from __future__ import annotations
|
| 31 |
+
|
| 32 |
+
import argparse
|
| 33 |
+
import csv
|
| 34 |
+
import itertools
|
| 35 |
+
import json
|
| 36 |
+
import math
|
| 37 |
+
import os
|
| 38 |
+
import sys
|
| 39 |
+
import time
|
| 40 |
+
import urllib.request
|
| 41 |
+
from pathlib import Path
|
| 42 |
+
|
| 43 |
+
import numpy as np
|
| 44 |
+
|
| 45 |
+
SR = 16000
|
| 46 |
+
WIN_S = 8.0
|
| 47 |
+
N_FFT, HOP, N_MELS, WIN_FRAMES = 400, 160, 80, 800
|
| 48 |
+
MEL20_FRAMES, MEL20_GROUP = 100, 5
|
| 49 |
+
OFFSETS = (0.2, 0.5, 1.2)
|
| 50 |
+
TAU_GRID = (0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.2, 1.8, 2.5)
|
| 51 |
+
TIMEOUT_S = 3.0
|
| 52 |
+
MIRROR = "https://groups.inf.ed.ac.uk/ami/AMICorpusMirror/amicorpus"
|
| 53 |
+
HERE = Path(__file__).resolve().parent
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# --- Whisper log-mel in numpy ---------------------------------------------------------------------
|
| 57 |
+
|
| 58 |
+
def _hz_to_mel(f):
|
| 59 |
+
f = np.asarray(f, dtype=np.float64)
|
| 60 |
+
m = 3.0 * f / 200.0
|
| 61 |
+
hi = f >= 1000.0
|
| 62 |
+
m[hi] = 15.0 + np.log(f[hi] / 1000.0) * (27.0 / np.log(6.4))
|
| 63 |
+
return m
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _mel_to_hz(m):
|
| 67 |
+
m = np.asarray(m, dtype=np.float64)
|
| 68 |
+
f = 200.0 * m / 3.0
|
| 69 |
+
hi = m >= 15.0
|
| 70 |
+
f[hi] = 1000.0 * np.exp(np.log(6.4) / 27.0 * (m[hi] - 15.0))
|
| 71 |
+
return f
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def mel_filters(n_freq: int = N_FFT // 2 + 1, n_mels: int = N_MELS, sr: int = SR) -> np.ndarray:
|
| 75 |
+
"""(201, 80) Slaney-normalised triangular filterbank, identical to transformers' ``mel_filter_bank``."""
|
| 76 |
+
fft_freqs = np.linspace(0.0, sr / 2, n_freq)
|
| 77 |
+
mel_pts = np.linspace(_hz_to_mel(np.array([0.0]))[0], _hz_to_mel(np.array([sr / 2]))[0], n_mels + 2)
|
| 78 |
+
f = _mel_to_hz(mel_pts)
|
| 79 |
+
d = np.diff(f)
|
| 80 |
+
slopes = f[None, :] - fft_freqs[:, None]
|
| 81 |
+
fb = np.maximum(0.0, np.minimum(-slopes[:, :-2] / d[None, :-1], slopes[:, 2:] / d[None, 1:]))
|
| 82 |
+
enorm = 2.0 / (f[2: n_mels + 2] - f[:n_mels])
|
| 83 |
+
return (fb * enorm[None, :]).astype(np.float32)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
_FILTERS = mel_filters()
|
| 87 |
+
_WINDOW = np.hanning(N_FFT + 1)[:-1].astype(np.float32)
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def whisper_logmel(x: np.ndarray) -> np.ndarray:
|
| 91 |
+
"""(80, 800) features of exactly 8 s of 16 kHz audio, as WhisperFeatureExtractor(chunk_length=8)
|
| 92 |
+
with padding='max_length', truncation=True, do_normalize=True gives them."""
|
| 93 |
+
x = np.asarray(x, dtype=np.float64)
|
| 94 |
+
assert len(x) == int(WIN_S * SR), len(x)
|
| 95 |
+
x = (x - x.mean()) / np.sqrt(x.var() + 1e-7) # do_normalize
|
| 96 |
+
pad = N_FFT // 2
|
| 97 |
+
xp = np.concatenate([x[1: pad + 1][::-1], x, x[-pad - 1: -1][::-1]]) # reflect padding, centre=True
|
| 98 |
+
n_frames = 1 + (len(xp) - N_FFT) // HOP # 801; the last one is dropped
|
| 99 |
+
idx = np.arange(N_FFT)[None, :] + HOP * np.arange(n_frames)[:, None]
|
| 100 |
+
frames = xp[idx] * _WINDOW
|
| 101 |
+
spec = np.abs(np.fft.rfft(frames, axis=1)) ** 2 # (801, 201)
|
| 102 |
+
mel = spec[:-1] @ _FILTERS # (800, 80)
|
| 103 |
+
ls = np.log10(np.maximum(mel, 1e-10))
|
| 104 |
+
ls = np.maximum(ls, ls.max() - 8.0)
|
| 105 |
+
return ((ls + 4.0) / 4.0).T.astype(np.float32)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def mel20(feats: np.ndarray) -> np.ndarray:
|
| 109 |
+
"""(20, 80): the last second of the features averaged to 50 ms steps (the heads' fresh-mel input)."""
|
| 110 |
+
return feats[:, -MEL20_FRAMES:].reshape(N_MELS, MEL20_FRAMES // MEL20_GROUP, MEL20_GROUP).mean(-1).T.astype(np.float32)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# --- windows ----------------------------------------------------------------------------------------
|
| 114 |
+
|
| 115 |
+
def room_tone(x: np.ndarray, sr: int, t_off: float, n: int, rng: np.random.Generator) -> np.ndarray:
|
| 116 |
+
"""``n`` samples of room tone tiled from the audio after ``t_off`` (0.5 s tiles, random order, some
|
| 117 |
+
reversed, 10 ms crossfades), as the training windows were built."""
|
| 118 |
+
tail = x[int(round(t_off * sr)):]
|
| 119 |
+
if n <= 0:
|
| 120 |
+
return np.zeros(0, np.float32)
|
| 121 |
+
if len(tail) < int(0.05 * sr):
|
| 122 |
+
nf = len(x) // 320
|
| 123 |
+
if nf >= 5:
|
| 124 |
+
fr = x[: nf * 320].reshape(nf, 320).astype(np.float64)
|
| 125 |
+
level = float(np.percentile(np.sqrt(np.mean(fr * fr, axis=1)), 5))
|
| 126 |
+
else:
|
| 127 |
+
level = float(np.sqrt(np.mean(np.square(x, dtype=np.float64)))) if len(x) else 0.0
|
| 128 |
+
level = float(np.clip(level, 10 ** (-70 / 20), 10 ** (-45 / 20)))
|
| 129 |
+
return (rng.standard_normal(n) * level).astype(np.float32)
|
| 130 |
+
fade = int(10 * sr / 1000)
|
| 131 |
+
tile = min(len(tail), int(0.5 * sr))
|
| 132 |
+
out = np.zeros(n + tile, np.float32)
|
| 133 |
+
ramp = np.linspace(0.0, 1.0, fade, dtype=np.float32)
|
| 134 |
+
pos = 0
|
| 135 |
+
while pos < n:
|
| 136 |
+
start = int(rng.integers(0, len(tail) - tile + 1))
|
| 137 |
+
seg = tail[start: start + tile].copy()
|
| 138 |
+
if rng.random() < 0.5:
|
| 139 |
+
seg = seg[::-1]
|
| 140 |
+
if pos > 0 and tile > fade:
|
| 141 |
+
seg[:fade] *= ramp
|
| 142 |
+
out[pos - fade: pos] *= ramp[::-1]
|
| 143 |
+
out[pos - fade: pos - fade + tile] += seg
|
| 144 |
+
pos += tile - fade
|
| 145 |
+
else:
|
| 146 |
+
out[pos: pos + tile] = seg
|
| 147 |
+
pos += tile
|
| 148 |
+
return out[:n]
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def cut_window(x: np.ndarray, end_s: float, t_off: float, cut_s: float | None, rng: np.random.Generator) -> np.ndarray:
|
| 152 |
+
"""8 s ending at ``end_s``: zeros before the recording, A's room tone after ``cut_s`` (the next
|
| 153 |
+
speaker's onset) or after the end of the recording."""
|
| 154 |
+
if cut_s is not None:
|
| 155 |
+
x = x[: int(round(cut_s * SR))]
|
| 156 |
+
n = int(WIN_S * SR)
|
| 157 |
+
end = int(round(end_s * SR))
|
| 158 |
+
start = end - n
|
| 159 |
+
out = np.zeros(n, np.float32)
|
| 160 |
+
s0, s1 = max(start, 0), min(end, len(x))
|
| 161 |
+
if s1 > s0:
|
| 162 |
+
out[s0 - start: s1 - start] = x[s0:s1]
|
| 163 |
+
if end > len(x):
|
| 164 |
+
pad_from = max(len(x), start)
|
| 165 |
+
out[pad_from - start:] = room_tone(x, SR, t_off, end - pad_from, rng)
|
| 166 |
+
return out
|
| 167 |
+
|
| 168 |
+
|
| 169 |
+
# --- data -------------------------------------------------------------------------------------------
|
| 170 |
+
|
| 171 |
+
def read_manifest(path: Path) -> list[dict]:
|
| 172 |
+
rows = []
|
| 173 |
+
with open(path, newline="") as f:
|
| 174 |
+
for r in csv.DictReader(f):
|
| 175 |
+
rows.append(dict(meeting=r["meeting"], speaker=r["speaker"], channel=int(r["channel"]), t=float(r["t"]),
|
| 176 |
+
label=int(r["label"] == "complete"),
|
| 177 |
+
gap_s=float(r["gap_s"]) if r["gap_s"] else math.nan,
|
| 178 |
+
b_onset_s=float(r["b_onset_s"]) if r["b_onset_s"] else math.nan))
|
| 179 |
+
return rows
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def headset_path(audio_dir: Path, meeting: str, channel: int) -> Path:
|
| 183 |
+
return audio_dir / meeting / f"{meeting}.Headset-{channel}.wav"
|
| 184 |
+
|
| 185 |
+
|
| 186 |
+
def fetch_audio(audio_dir: Path, rows: list[dict], retries: int = 4) -> None:
|
| 187 |
+
need = sorted({(r["meeting"], r["channel"]) for r in rows})
|
| 188 |
+
missing = [(m, c) for m, c in need if not headset_path(audio_dir, m, c).exists()]
|
| 189 |
+
if not missing:
|
| 190 |
+
return
|
| 191 |
+
print(f"downloading {len(missing)} headset files (~40 MB each) from {MIRROR} into {audio_dir}", flush=True)
|
| 192 |
+
for m, c in missing:
|
| 193 |
+
url = f"{MIRROR}/{m}/audio/{m}.Headset-{c}.wav"
|
| 194 |
+
dest = headset_path(audio_dir, m, c)
|
| 195 |
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
| 196 |
+
for attempt in range(retries):
|
| 197 |
+
try:
|
| 198 |
+
tmp = dest.with_suffix(".part")
|
| 199 |
+
with urllib.request.urlopen(url, timeout=120) as r, open(tmp, "wb") as f:
|
| 200 |
+
size = int(r.headers.get("Content-Length", "0"))
|
| 201 |
+
while True:
|
| 202 |
+
chunk = r.read(1 << 20)
|
| 203 |
+
if not chunk:
|
| 204 |
+
break
|
| 205 |
+
f.write(chunk)
|
| 206 |
+
if size and tmp.stat().st_size != size:
|
| 207 |
+
raise OSError(f"size mismatch {tmp.stat().st_size} != {size}")
|
| 208 |
+
tmp.replace(dest)
|
| 209 |
+
print(f" got {dest.name}", flush=True)
|
| 210 |
+
break
|
| 211 |
+
except Exception as e: # noqa: BLE001
|
| 212 |
+
if attempt == retries - 1:
|
| 213 |
+
raise
|
| 214 |
+
print(f" retry {dest.name}: {e}", flush=True)
|
| 215 |
+
time.sleep(5 * (attempt + 1))
|
| 216 |
+
|
| 217 |
+
|
| 218 |
+
# --- models -----------------------------------------------------------------------------------------
|
| 219 |
+
|
| 220 |
+
class Models:
|
| 221 |
+
def __init__(self, encoder: Path, heads: Path, threads: int):
|
| 222 |
+
import onnxruntime as ort
|
| 223 |
+
so = ort.SessionOptions()
|
| 224 |
+
so.intra_op_num_threads = threads
|
| 225 |
+
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
|
| 226 |
+
self.enc = ort.InferenceSession(str(encoder), sess_options=so, providers=["CPUExecutionProvider"])
|
| 227 |
+
self.heads = ort.InferenceSession(str(heads), sess_options=so, providers=["CPUExecutionProvider"])
|
| 228 |
+
self.enc_in = self.enc.get_inputs()[0].name
|
| 229 |
+
outs = [o.name for o in self.enc.get_outputs()]
|
| 230 |
+
self.enc_prob, self.enc_pooled = outs[0], outs[-1]
|
| 231 |
+
|
| 232 |
+
def score(self, feats: np.ndarray, taus: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
|
| 233 |
+
"""feats (B, 80, 800), taus (B,) -> (p_end of the heads (B,), p of the frozen model (B,))."""
|
| 234 |
+
prob, pooled = self.enc.run([self.enc_prob, self.enc_pooled], {self.enc_in: feats.astype(np.float32)})
|
| 235 |
+
m20 = np.stack([mel20(f) for f in feats])
|
| 236 |
+
tau_feat = np.stack([taus / 2.5, np.log1p(taus), np.zeros_like(taus)], axis=1).astype(np.float32)
|
| 237 |
+
p = self.heads.run(None, {"pooled": pooled.astype(np.float32), "mel20": m20, "tau_feat": tau_feat})[0]
|
| 238 |
+
return p[:, 0].astype(np.float64), prob.reshape(-1).astype(np.float64)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def score_split(rows: list[dict], taus: tuple, models: Models, audio_dir: Path, seed: int, batch: int = 16, tag: str = "") -> dict:
|
| 242 |
+
"""Per silence and tau: P(complete) of the heads and of the frozen model (nan where no window)."""
|
| 243 |
+
rng = np.random.default_rng(seed)
|
| 244 |
+
P = np.full((len(rows), len(taus)), np.nan)
|
| 245 |
+
PB = np.full((len(rows), len(taus)), np.nan)
|
| 246 |
+
import soundfile as sf
|
| 247 |
+
by_file: dict[tuple, list[int]] = {}
|
| 248 |
+
for i, r in enumerate(rows):
|
| 249 |
+
by_file.setdefault((r["meeting"], r["channel"]), []).append(i)
|
| 250 |
+
jobs = [] # (row index, tau index, window)
|
| 251 |
+
t0, done, total = time.time(), 0, sum(len(v) for v in by_file.values())
|
| 252 |
+
|
| 253 |
+
def flush():
|
| 254 |
+
if not jobs:
|
| 255 |
+
return
|
| 256 |
+
feats = np.stack([whisper_logmel(w) for _, _, w in jobs])
|
| 257 |
+
p, pb = models.score(feats, np.array([taus[k] for _, k, _ in jobs], np.float64))
|
| 258 |
+
for (i, k, _), a, b in zip(jobs, p, pb):
|
| 259 |
+
P[i, k], PB[i, k] = a, b
|
| 260 |
+
jobs.clear()
|
| 261 |
+
|
| 262 |
+
for (meeting, channel), idx in by_file.items():
|
| 263 |
+
x, sr = sf.read(headset_path(audio_dir, meeting, channel), dtype="float32")
|
| 264 |
+
if sr != SR:
|
| 265 |
+
raise ValueError(f"{meeting} headset {channel}: {sr} Hz, expected 16 kHz")
|
| 266 |
+
if x.ndim > 1:
|
| 267 |
+
x = x[:, 0]
|
| 268 |
+
for i in idx:
|
| 269 |
+
r = rows[i]
|
| 270 |
+
for k, tau in enumerate(taus):
|
| 271 |
+
if not r["label"] and not (tau < r["gap_s"]):
|
| 272 |
+
continue # the pause ended before this offset
|
| 273 |
+
cut = r["t"] + r["b_onset_s"] if (r["label"] and not math.isnan(r["b_onset_s"]) and tau > r["b_onset_s"]) else None
|
| 274 |
+
jobs.append((i, k, cut_window(x, r["t"] + tau, r["t"], cut, rng)))
|
| 275 |
+
if len(jobs) >= batch:
|
| 276 |
+
flush()
|
| 277 |
+
done += len(idx)
|
| 278 |
+
print(f" {tag}{done}/{total} silences, {time.time() - t0:.0f} s", end="\r", file=sys.stderr, flush=True)
|
| 279 |
+
flush()
|
| 280 |
+
print(file=sys.stderr)
|
| 281 |
+
return dict(p=P, pb=PB, label=np.array([r["label"] for r in rows]), gap=np.array([r["gap_s"] for r in rows]), taus=np.array(taus))
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
# --- metrics ----------------------------------------------------------------------------------------
|
| 285 |
+
|
| 286 |
+
def auc(y: np.ndarray, s: np.ndarray) -> float:
|
| 287 |
+
"""ROC AUC by the rank statistic (ties counted half)."""
|
| 288 |
+
y, s = np.asarray(y), np.asarray(s)
|
| 289 |
+
pos, neg = s[y == 1], s[y == 0]
|
| 290 |
+
if not len(pos) or not len(neg):
|
| 291 |
+
return math.nan
|
| 292 |
+
order = np.argsort(np.concatenate([pos, neg]), kind="mergesort")
|
| 293 |
+
ranks = np.empty(len(order)); ranks[order] = np.arange(1, len(order) + 1)
|
| 294 |
+
allv = np.concatenate([pos, neg])
|
| 295 |
+
# average ranks for ties
|
| 296 |
+
uniq, inv, counts = np.unique(allv, return_inverse=True, return_counts=True)
|
| 297 |
+
sums = np.bincount(inv, weights=ranks)
|
| 298 |
+
ranks = (sums / counts)[inv]
|
| 299 |
+
return float((ranks[: len(pos)].sum() - len(pos) * (len(pos) + 1) / 2) / (len(pos) * len(neg)))
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
def simulate(p, avail, label, gap, theta, offsets=OFFSETS) -> dict:
|
| 303 |
+
th = np.asarray(theta)[None, :]
|
| 304 |
+
hit = avail & (np.nan_to_num(p, nan=-1.0) >= th)
|
| 305 |
+
first = np.where(hit.any(1), hit.argmax(1), -1)
|
| 306 |
+
comp, inc = label == 1, label == 0
|
| 307 |
+
lat = np.full(comp.sum(), TIMEOUT_S)
|
| 308 |
+
ok = first[comp] >= 0
|
| 309 |
+
lat[ok] = np.asarray(offsets)[first[comp][ok]]
|
| 310 |
+
fa = first[inc] >= 0
|
| 311 |
+
bins = {"<0.5": gap[inc] < 0.5, "0.5-1.5": (gap[inc] >= 0.5) & (gap[inc] < 1.5), ">=1.5": gap[inc] >= 1.5}
|
| 312 |
+
return dict(theta=tuple(float(t) for t in theta), recall=float(ok.mean()), far=float(fa.mean()),
|
| 313 |
+
p50=float(np.percentile(lat, 50)), p90=float(np.percentile(lat, 90)), mean=float(lat.mean()),
|
| 314 |
+
far_by_bin={k: (float(fa[m].mean()) if m.any() else math.nan) for k, m in bins.items()},
|
| 315 |
+
n_complete=int(comp.sum()), n_incomplete=int(inc.sum()))
|
| 316 |
+
|
| 317 |
+
|
| 318 |
+
def fit(p, avail, label, gap, target_far: float, offsets=OFFSETS, grid=np.round(np.arange(0.0, 1.0001, 0.02), 2)) -> dict:
|
| 319 |
+
"""Exhaustive grid over theta per offset with FAR <= target, minimising the mean latency (misses charged 3 s)."""
|
| 320 |
+
inc, comp = label == 0, label == 1
|
| 321 |
+
K = len(offsets)
|
| 322 |
+
hits = [np.stack([avail[:, k] & (np.nan_to_num(p[:, k], nan=-1.0) >= t) for t in grid]) for k in range(K)]
|
| 323 |
+
best = None
|
| 324 |
+
for combo in itertools.product(range(len(grid)), repeat=K):
|
| 325 |
+
h = np.stack([hits[k][combo[k]] for k in range(K)], axis=1)
|
| 326 |
+
first = np.where(h.any(1), h.argmax(1), -1)
|
| 327 |
+
if (first[inc] >= 0).mean() > target_far:
|
| 328 |
+
continue
|
| 329 |
+
rec = (first[comp] >= 0).mean()
|
| 330 |
+
lat = np.where(first[comp] >= 0, np.asarray(offsets)[np.maximum(first[comp], 0)], TIMEOUT_S).mean()
|
| 331 |
+
key = (lat, -rec)
|
| 332 |
+
if best is None or key < best[0]:
|
| 333 |
+
best = (key, tuple(grid[i] for i in combo))
|
| 334 |
+
theta = best[1] if best else tuple([1.01] * K)
|
| 335 |
+
return simulate(p, avail, label, gap, theta, offsets)
|
| 336 |
+
|
| 337 |
+
|
| 338 |
+
def rule_inputs(sc: dict, offsets=OFFSETS):
|
| 339 |
+
cols = [int(np.where(np.isclose(sc["taus"], o))[0][0]) for o in offsets]
|
| 340 |
+
p, pb = sc["p"][:, cols], sc["pb"][:, cols]
|
| 341 |
+
label, gap = sc["label"], sc["gap"]
|
| 342 |
+
avail = (label[:, None] == 1) | (np.nan_to_num(gap, nan=np.inf)[:, None] > np.asarray(offsets)[None, :])
|
| 343 |
+
avail &= ~np.isnan(p)
|
| 344 |
+
# the rule is scored on silences that have a decision point before the resumption
|
| 345 |
+
keep = avail.any(1)
|
| 346 |
+
return p[keep], pb[keep], avail[keep], label[keep], gap[keep]
|
| 347 |
+
|
| 348 |
+
|
| 349 |
+
def rule_row(name: str, r: dict) -> str:
|
| 350 |
+
th = " / ".join(f"{t:.2f}" if t <= 1 else "-" for t in r["theta"])
|
| 351 |
+
fb = " / ".join(f"{r['far_by_bin'][k]:.2f}" for k in ("<0.5", "0.5-1.5", ">=1.5"))
|
| 352 |
+
return f"| {name} | {th} | {r['recall']:.2f} | {r['far']:.3f} | {r['p50']:.2f} s | {r['p90']:.2f} s | {r['mean']:.2f} s | {fb} |"
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def read_theta_tables(path: Path) -> list[tuple[str, tuple]]:
|
| 356 |
+
out = []
|
| 357 |
+
if not path.exists():
|
| 358 |
+
return out
|
| 359 |
+
with open(path, newline="") as f:
|
| 360 |
+
for r in csv.DictReader(f):
|
| 361 |
+
if r["model"] == "heads":
|
| 362 |
+
out.append((f"shipped table, {float(r['far_budget']):.0%} budget", tuple(float(t) for t in json.loads(r["theta"].replace("(", "[").replace(")", "]")))))
|
| 363 |
+
return out
|
| 364 |
+
|
| 365 |
+
|
| 366 |
+
# --- main -------------------------------------------------------------------------------------------
|
| 367 |
+
|
| 368 |
+
def main():
|
| 369 |
+
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
| 370 |
+
ap.add_argument("--audio-dir", default="./ami_audio", help="AMI headset audio, <meeting>/<meeting>.Headset-<n>.wav (downloaded when missing)")
|
| 371 |
+
ap.add_argument("--no-download", action="store_true")
|
| 372 |
+
ap.add_argument("--manifest-dir", default=str(HERE), help="directory with ami_dev.csv / ami_test.csv")
|
| 373 |
+
ap.add_argument("--encoder", default=str(HERE.parent / "smart-turn-v3.2-encoder-pooled.onnx"))
|
| 374 |
+
ap.add_argument("--heads", default=str(HERE.parent / "eot-heads-v32.onnx"))
|
| 375 |
+
ap.add_argument("--theta-tables", default=str(HERE.parent / "rules" / "theta_tables.csv"))
|
| 376 |
+
ap.add_argument("--splits", nargs="*", default=["dev", "test"])
|
| 377 |
+
ap.add_argument("--taus", nargs="*", default=None, help="silence offsets to score (default: the rule offsets 0.2 0.5 1.2; 'all' = the trained grid)")
|
| 378 |
+
ap.add_argument("--budgets", nargs="*", type=float, default=[0.05, 0.10, 0.15])
|
| 379 |
+
ap.add_argument("--no-fit", action="store_true", help="skip the threshold fit on dev (score the shipped tables only)")
|
| 380 |
+
ap.add_argument("--threads", type=int, default=1)
|
| 381 |
+
ap.add_argument("--seed", type=int, default=0)
|
| 382 |
+
ap.add_argument("--cache", default="./ami_eval_scores.npz")
|
| 383 |
+
ap.add_argument("--out", default="./ami_eval.md")
|
| 384 |
+
a = ap.parse_args()
|
| 385 |
+
|
| 386 |
+
taus = TAU_GRID if (a.taus and a.taus[0] == "all") else tuple(float(t) for t in (a.taus or OFFSETS))
|
| 387 |
+
for o in OFFSETS:
|
| 388 |
+
if not any(np.isclose(o, taus)):
|
| 389 |
+
sys.exit(f"--taus must include the rule offsets {OFFSETS}")
|
| 390 |
+
audio_dir = Path(a.audio_dir)
|
| 391 |
+
rows = {sp: read_manifest(Path(a.manifest_dir) / f"ami_{sp}.csv") for sp in a.splits}
|
| 392 |
+
if not a.no_download:
|
| 393 |
+
fetch_audio(audio_dir, [r for rs in rows.values() for r in rs])
|
| 394 |
+
|
| 395 |
+
cache = dict(np.load(a.cache, allow_pickle=True)) if os.path.exists(a.cache) else {}
|
| 396 |
+
scores = {}
|
| 397 |
+
models = None
|
| 398 |
+
for sp in a.splits:
|
| 399 |
+
key = f"{sp}:{','.join(f'{t:g}' for t in taus)}:{a.seed}"
|
| 400 |
+
if key in cache:
|
| 401 |
+
scores[sp] = cache[key].item()
|
| 402 |
+
print(f"{sp}: scores from {a.cache}", file=sys.stderr)
|
| 403 |
+
continue
|
| 404 |
+
if models is None:
|
| 405 |
+
models = Models(Path(a.encoder), Path(a.heads), a.threads)
|
| 406 |
+
print(f"{sp}: scoring {len(rows[sp])} silences at tau = {taus}", file=sys.stderr, flush=True)
|
| 407 |
+
scores[sp] = score_split(rows[sp], taus, models, audio_dir, a.seed, tag=f"{sp} ")
|
| 408 |
+
cache[key] = np.array(scores[sp], dtype=object)
|
| 409 |
+
np.savez(a.cache, **cache)
|
| 410 |
+
|
| 411 |
+
lines = [f"# AMI end-of-turn evaluation ({', '.join(a.splits)}; thresholds {'shipped' if a.no_fit else 'shipped and re-fitted on dev'})", ""]
|
| 412 |
+
for sp in a.splits:
|
| 413 |
+
sc = scores[sp]
|
| 414 |
+
lines += [f"## AMI {sp}: AUC of P(complete) per silence offset", "",
|
| 415 |
+
"| tau (s) | n complete / incomplete | AUC heads | AUC frozen Smart Turn v3.2 | mean P on incompletes: heads / frozen |", "|---|---|---|---|---|"]
|
| 416 |
+
for k, tau in enumerate(sc["taus"]):
|
| 417 |
+
ok = ~np.isnan(sc["p"][:, k])
|
| 418 |
+
y, p, pb = sc["label"][ok], sc["p"][ok, k], sc["pb"][ok, k]
|
| 419 |
+
lines.append(f"| {tau:g} | {int((y == 1).sum())} / {int((y == 0).sum())} | {auc(y, p):.3f} | {auc(y, pb):.3f} | "
|
| 420 |
+
f"{p[y == 0].mean():.3f} / {pb[y == 0].mean():.3f} |")
|
| 421 |
+
lines.append("")
|
| 422 |
+
test_sp = "test" if "test" in scores else a.splits[-1]
|
| 423 |
+
P, PB, A, L, G = rule_inputs(scores[test_sp])
|
| 424 |
+
hdr = ["| rule | theta at 0.2 / 0.5 / 1.2 s | recall | FAR | p50 | p90 | mean latency | FAR by pause length <0.5 / 0.5-1.5 / >=1.5 s |",
|
| 425 |
+
"|---|---|---|---|---|---|---|---|"]
|
| 426 |
+
lines += [f"## Decision rule on AMI {test_sp} ({int((L == 1).sum())} complete / {int((L == 0).sum())} incomplete silences with a decision point; "
|
| 427 |
+
f"misses charged {TIMEOUT_S:g} s)", "", "**Heads (this release)**", ""] + hdr
|
| 428 |
+
lines.append(rule_row("one-shot at the VAD stop, theta 0.5 (stock rule)", simulate(P, A, L, G, (0.5, 1.01, 1.01))))
|
| 429 |
+
for name, th in read_theta_tables(Path(a.theta_tables)):
|
| 430 |
+
lines.append(rule_row(name, simulate(P, A, L, G, th)))
|
| 431 |
+
fitted = {}
|
| 432 |
+
if not a.no_fit and "dev" in scores:
|
| 433 |
+
Pd, PBd, Ad, Ld, Gd = rule_inputs(scores["dev"])
|
| 434 |
+
for b in a.budgets:
|
| 435 |
+
t_fit = time.time()
|
| 436 |
+
fitted[b] = (fit(Pd, Ad, Ld, Gd, b), fit(PBd, Ad, Ld, Gd, b))
|
| 437 |
+
print(f"fit at {b:.0%}: {time.time() - t_fit:.0f} s", file=sys.stderr)
|
| 438 |
+
lines.append(rule_row(f"re-fitted on dev, {b:.0%} budget", simulate(P, A, L, G, fitted[b][0]["theta"])))
|
| 439 |
+
lines += ["", "**Frozen Smart Turn v3.2 (baseline, same protocol)**", ""] + hdr
|
| 440 |
+
lines.append(rule_row("one-shot at the VAD stop, theta 0.5 (stock rule)", simulate(PB, A, L, G, (0.5, 1.01, 1.01))))
|
| 441 |
+
for b, (_, rb) in fitted.items():
|
| 442 |
+
lines.append(rule_row(f"re-fitted on dev, {b:.0%} budget", simulate(PB, A, L, G, rb["theta"])))
|
| 443 |
+
lines += ["", "**No model**", ""] + hdr
|
| 444 |
+
lines.append(rule_row("silence only, commit at 1.2 s", simulate(np.ones_like(P), A, L, G, (1.01, 1.01, 0.0))))
|
| 445 |
+
lines += ["", f"Scores cached in `{a.cache}`; audio from {MIRROR} (AMI Meeting Corpus, CC BY 4.0)."]
|
| 446 |
+
Path(a.out).write_text("\n".join(lines) + "\n")
|
| 447 |
+
print("\n".join(lines))
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
if __name__ == "__main__":
|
| 451 |
+
main()
|
eval/ami_eval_reference.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AMI end-of-turn evaluation (dev, test; thresholds shipped and re-fitted on dev)
|
| 2 |
+
|
| 3 |
+
## AMI dev: AUC of P(complete) per silence offset
|
| 4 |
+
|
| 5 |
+
| tau (s) | n complete / incomplete | AUC heads | AUC frozen Smart Turn v3.2 | mean P on incompletes: heads / frozen |
|
| 6 |
+
|---|---|---|---|---|
|
| 7 |
+
| 0.2 | 734 / 3195 | 0.748 | 0.675 | 0.251 / 0.299 |
|
| 8 |
+
| 0.5 | 734 / 1827 | 0.722 | 0.640 | 0.398 / 0.414 |
|
| 9 |
+
| 1.2 | 734 / 624 | 0.698 | 0.616 | 0.727 / 0.450 |
|
| 10 |
+
|
| 11 |
+
## AMI test: AUC of P(complete) per silence offset
|
| 12 |
+
|
| 13 |
+
| tau (s) | n complete / incomplete | AUC heads | AUC frozen Smart Turn v3.2 | mean P on incompletes: heads / frozen |
|
| 14 |
+
|---|---|---|---|---|
|
| 15 |
+
| 0.2 | 511 / 3820 | 0.775 | 0.671 | 0.256 / 0.309 |
|
| 16 |
+
| 0.5 | 511 / 2256 | 0.776 | 0.638 | 0.357 / 0.427 |
|
| 17 |
+
| 1.2 | 511 / 688 | 0.738 | 0.577 | 0.713 / 0.533 |
|
| 18 |
+
|
| 19 |
+
## Decision rule on AMI test (511 complete / 3820 incomplete silences with a decision point; misses charged 3 s)
|
| 20 |
+
|
| 21 |
+
**Heads (this release)**
|
| 22 |
+
|
| 23 |
+
| rule | theta at 0.2 / 0.5 / 1.2 s | recall | FAR | p50 | p90 | mean latency | FAR by pause length <0.5 / 0.5-1.5 / >=1.5 s |
|
| 24 |
+
|---|---|---|---|---|---|---|---|
|
| 25 |
+
| one-shot at the VAD stop, theta 0.5 (stock rule) | 0.50 / - / - | 0.64 | 0.254 | 0.20 s | 3.00 s | 1.21 s | 0.19 / 0.24 / 0.46 |
|
| 26 |
+
| shipped table, 5% budget | 0.94 / 0.94 / 0.94 | 0.47 | 0.046 | 3.00 s | 3.00 s | 2.05 s | 0.00 / 0.03 / 0.21 |
|
| 27 |
+
| shipped table, 10% budget | 0.92 / 0.98 / 0.86 | 0.79 | 0.094 | 1.20 s | 3.00 s | 1.49 s | 0.01 / 0.04 / 0.50 |
|
| 28 |
+
| shipped table, 15% budget | 0.98 / 0.88 / 0.80 | 0.86 | 0.133 | 1.20 s | 3.00 s | 1.21 s | 0.00 / 0.09 / 0.61 |
|
| 29 |
+
| re-fitted on dev, 5% budget | 0.94 / 0.94 / 0.94 | 0.47 | 0.046 | 3.00 s | 3.00 s | 2.05 s | 0.00 / 0.03 / 0.21 |
|
| 30 |
+
| re-fitted on dev, 10% budget | 1.00 / 0.94 / 0.86 | 0.79 | 0.090 | 1.20 s | 3.00 s | 1.49 s | 0.00 / 0.04 / 0.50 |
|
| 31 |
+
| re-fitted on dev, 15% budget | 0.98 / 0.88 / 0.80 | 0.86 | 0.133 | 1.20 s | 3.00 s | 1.21 s | 0.00 / 0.09 / 0.61 |
|
| 32 |
+
|
| 33 |
+
**Frozen Smart Turn v3.2 (baseline, same protocol)**
|
| 34 |
+
|
| 35 |
+
| rule | theta at 0.2 / 0.5 / 1.2 s | recall | FAR | p50 | p90 | mean latency | FAR by pause length <0.5 / 0.5-1.5 / >=1.5 s |
|
| 36 |
+
|---|---|---|---|---|---|---|---|
|
| 37 |
+
| one-shot at the VAD stop, theta 0.5 (stock rule) | 0.50 / - / - | 0.51 | 0.303 | 0.20 s | 3.00 s | 1.56 s | 0.23 / 0.30 / 0.45 |
|
| 38 |
+
| re-fitted on dev, 5% budget | 1.00 / 1.00 / 0.96 | 0.37 | 0.056 | 3.00 s | 3.00 s | 2.34 s | 0.00 / 0.02 / 0.31 |
|
| 39 |
+
| re-fitted on dev, 10% budget | 1.00 / 1.00 / 0.26 | 0.71 | 0.106 | 1.20 s | 3.00 s | 1.72 s | 0.00 / 0.04 / 0.61 |
|
| 40 |
+
| re-fitted on dev, 15% budget | 1.00 / 1.00 / 0.02 | 0.87 | 0.137 | 1.20 s | 3.00 s | 1.44 s | 0.00 / 0.05 / 0.78 |
|
| 41 |
+
|
| 42 |
+
**No model**
|
| 43 |
+
|
| 44 |
+
| rule | theta at 0.2 / 0.5 / 1.2 s | recall | FAR | p50 | p90 | mean latency | FAR by pause length <0.5 / 0.5-1.5 / >=1.5 s |
|
| 45 |
+
|---|---|---|---|---|---|---|---|
|
| 46 |
+
| silence only, commit at 1.2 s | - / - / 0.00 | 1.00 | 0.180 | 1.20 s | 1.20 s | 1.20 s | 0.00 / 0.08 / 1.00 |
|
| 47 |
+
|
| 48 |
+
Scores cached in `./ami_eval_scores.npz`; audio from https://groups.inf.ed.ac.uk/ami/AMICorpusMirror/amicorpus (AMI Meeting Corpus, CC BY 4.0).
|
eval/ami_test.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
rules/eager_tables.csv
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
commit,budget,theta_e,theta_c
|
| 2 |
+
far10,0.2,"(0.92, 0.84, 0.68)","(0.92, 0.98, 0.86)"
|
| 3 |
+
far10,0.3,"(0.92, 0.54, 0.2)","(0.92, 0.98, 0.86)"
|
| 4 |
+
far10,0.4,"(0.74, 0.24, 0.02)","(0.92, 0.98, 0.86)"
|
| 5 |
+
far10,0.5,"(0.42, 0.1, 0.02)","(0.92, 0.98, 0.86)"
|
| 6 |
+
far15,0.2,"(0.92, 0.84, 0.68)","(0.98, 0.88, 0.8)"
|
| 7 |
+
far15,0.3,"(0.84, 0.56, 0.42)","(0.98, 0.88, 0.8)"
|
| 8 |
+
far15,0.4,"(0.72, 0.24, 0.04)","(0.98, 0.88, 0.8)"
|
| 9 |
+
far15,0.5,"(0.42, 0.1, 0.02)","(0.98, 0.88, 0.8)"
|
rules/theta_tables.csv
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
model,far_budget,theta,test_recall,test_far,test_mean,test_p90,test_far_ge15
|
| 2 |
+
heads,0.05,"(0.94, 0.94, 0.94)",0.472,0.046,2.05,3.0,0.21
|
| 3 |
+
frozen_v32,0.05,"(1.0, 1.0, 0.96)",0.368,0.055,2.34,3.0,0.31
|
| 4 |
+
heads,0.1,"(0.92, 0.98, 0.86)",0.796,0.094,1.48,3.0,0.5
|
| 5 |
+
frozen_v32,0.1,"(1.0, 1.0, 0.28)",0.699,0.106,1.74,3.0,0.61
|
| 6 |
+
heads,0.15,"(0.98, 0.88, 0.8)",0.863,0.133,1.21,3.0,0.61
|
| 7 |
+
frozen_v32,0.15,"(1.0, 1.0, 0.02)",0.867,0.137,1.44,3.0,0.78
|
smart-turn-v3.2-encoder-pooled.onnx
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:37e2a4fe22e1c4efaeba4df40141e422a3b96519db9160ae9134962a936ba4ea
|
| 3 |
+
size 32411234
|