transcribe-cpp-sys 0.2.2

Native FFI bindings for transcribe.cpp, a C/C++ speech-to-text library built on ggml
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
2391
2392
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410
2411
2412
2413
2414
2415
2416
2417
2418
2419
2420
2421
2422
2423
2424
2425
2426
2427
2428
2429
2430
2431
2432
2433
2434
2435
2436
2437
2438
2439
2440
2441
2442
2443
2444
2445
2446
2447
2448
2449
2450
2451
2452
2453
2454
2455
2456
2457
2458
2459
2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
2481
2482
2483
2484
2485
2486
2487
2488
2489
2490
2491
2492
2493
2494
2495
2496
2497
2498
2499
2500
2501
2502
2503
2504
2505
2506
2507
2508
2509
2510
2511
2512
2513
2514
2515
2516
2517
2518
2519
2520
2521
2522
2523
2524
2525
2526
2527
2528
2529
2530
2531
2532
2533
2534
2535
2536
2537
2538
2539
2540
2541
2542
2543
2544
2545
2546
2547
2548
2549
2550
2551
2552
2553
2554
2555
2556
2557
2558
2559
2560
2561
2562
2563
2564
2565
2566
2567
2568
2569
2570
2571
2572
2573
2574
2575
2576
2577
2578
2579
2580
2581
2582
2583
2584
2585
2586
2587
2588
2589
2590
2591
2592
2593
2594
2595
2596
2597
2598
2599
2600
2601
2602
2603
2604
2605
2606
2607
2608
2609
2610
2611
2612
2613
2614
2615
2616
2617
2618
2619
2620
2621
2622
2623
2624
2625
2626
2627
2628
2629
2630
2631
2632
2633
2634
2635
2636
2637
2638
2639
2640
2641
2642
2643
2644
2645
2646
2647
2648
2649
2650
2651
2652
2653
/*
 * transcribe.h - public C API for transcribe.cpp
 *
 * One-header public surface. Callers never need to include <ggml.h>.
 *
 * Threading:
 * - transcribe_model_* functions are thread-safe.
 * - A loaded model may be shared across any number of threads: querying it
 *   (capabilities, metadata, feature probes) and creating sessions from it
 *   are safe concurrently.
 * - KNOWN 0.x LIMITATION — concurrent COMPUTE is not yet supported: at most
 *   one transcribe_run / transcribe_run_batch / active stream may be in
 *   flight across ALL sessions of a given model at a time. Sessions share
 *   the model's backend instances and some per-family model state, so
 *   overlapping runs race (observed: corrupted decodes on CPU, command-
 *   buffer failures on Metal). Callers that want parallel transcription
 *   today should load one model per worker; a per-session backend
 *   architecture lifting this restriction is planned. Serialized use of
 *   many sessions on one model (e.g. a session pool behind a mutex) is
 *   fully supported.
 * - A model must outlive every session created from it.
 * - transcribe_model_free() is only valid after all derived contexts have
 *   been freed and no thread is still using the model.
 * - transcribe_session_* functions are not thread-safe.
 * - A session must be used by at most one thread at a time.
 * - A session may be moved between threads if no two threads ever use it
 *   concurrently.
 * - The log callback may be invoked from any thread (including ggml
 *   worker threads); the userdata pointer must be safe for concurrent
 *   access. The callback must not assume it runs on the caller thread.
 * - transcribe_log_set() should be called ONCE at process startup,
 *   before any models are loaded, contexts are created, or worker
 *   threads exist. That is the only supported usage model in 0.x. The
 *   implementation publishes the callback with release semantics so any
 *   thread that later loads it observes both the callback and any state
 *   the installer wrote before publication. This protection covers the
 *   single startup-time install only.
 * - Calling transcribe_log_set() after threads or models exist is
 *   UNSUPPORTED in 0.x. The implementation is data-race-free (the two
 *   atomics ensure no torn reads), but under concurrent or mid-run
 *   reconfiguration the API does NOT guarantee pair-atomic publication
 *   of (callback, userdata), nor that the previous callback/userdata
 *   will not still be invoked after transcribe_log_set returns (an
 *   in-flight emission on another thread may already hold the old pair).
 *   A caller that needs mid-run reconfiguration must externally
 *   synchronize transcribe_log_set callers AND keep every previous
 *   callback target and userdata alive until no thread can still emit
 *   with them.
 *
 * ABI stability (0.x):
 * - This library is pre-1.0. The on-disk ABI MAY break between 0.x minor
 *   releases. Consumers should rebuild against matching headers and
 *   should not assume any layout, enum value, or symbol set is frozen.
 *
 * Exception safety (C ABI):
 * - No C++ exception escapes a public entry point. Allocation failure maps
 *   to TRANSCRIBE_ERR_OOM, backend/driver failure to TRANSCRIBE_ERR_BACKEND,
 *   and free/teardown functions never fail. The log callback receives
 *   exception detail when available.
 *
 * Params (size-aware structs):
 * - Every caller-owned public struct crossing the ABI carries `struct_size`
 *   as field 0, both for inputs (params, family extensions) and outputs
 *   (capabilities, stream updates, timings, family telemetry). The field
 *   is `uint64_t` for platform-invariant layout across 32/64-bit ABIs.
 * - Callers MUST initialize each struct via its transcribe_*_init()
 *   function (or, for family extensions, the family's init function).
 *   The init function fills sensible defaults and stamps `struct_size`.
 *   `{0}` is NOT accepted as a defaults shortcut: a struct with
 *   `struct_size == 0` is rejected with TRANSCRIBE_ERR_BAD_STRUCT_SIZE,
 *   regardless of any other field's value. The only NULL-equivalent
 *   defaults form is a literal NULL pointer where the entry point
 *   accepts one (every public params pointer does).
 * - For input structs the init function spells out the default values
 *   for every field. For output structs the init function sets
 *   `struct_size` and relies on zero-fill for the rest. The general
 *   convention is "zero means absent / unknown / false / none," but
 *   specific fields may use a different sentinel (e.g. a family stream
 *   extension's transcribe_parakeet_stream_ext::att_context_right uses
 *   -1 for "model default" because 0 is a real value). Each field's doc
 *   spells out its sentinel where it differs from the general rule.
 * - sizeof(struct ...) is evaluated in the caller's translation unit,
 *   so struct_size captures the caller's view of the layout. In 0.x
 *   the library requires struct_size >= the minimum prefix needed by the
 *   entry point. New trailing fields can be appended without raising that
 *   minimum when the library can safely treat their absence as zero/default.
 *   A newer caller with extra trailing fields the older library doesn't
 *   know about is accepted, and the library only writes what it knows so
 *   the caller's tail bytes stay as zero-init. Layout-incompatible changes
 *   (reordering, retyping existing fields) are an ABI break regardless and
 *   are allowed in 0.x without a deprecation cycle.
 * - Family-specific knobs are reached via an opaque, kind-tagged
 *   extension pointer (`struct transcribe_ext`). Callers probe whether
 *   the loaded model accepts a given extension kind in a given slot via
 *   transcribe_model_accepts_ext_kind(), then pass a pointer to a typed
 *   family extension struct (declared in include/transcribe/<family>.h)
 *   whose first field embeds `struct transcribe_ext`. NULL family
 *   extension selects family defaults.
 *
 * Results:
 * - Results are owned by the session and exposed via accessor functions.
 *   Calling transcribe_run() replaces the previous result. All accessors
 *   are safe to call before the first transcribe_run() and return safe
 *   sentinels ("", 0, NaN) when no result is present.
 * - All timestamps are int64_t milliseconds, relative to the original
 *   input audio (never to internally padded audio).
 * - All counts and indices are int.
 * - Out-of-bounds indices are programmer error: in debug builds the
 *   library may assert; in release builds accessors return safe sentinels
 *   instead of reading invalid memory. Callers should bounds-check with
 *   the corresponding transcribe_n_*() accessor before indexing.
 *
 * Result text-pointer lifetime:
 * - Every accessor that returns a `const char *` (transcribe_full_text,
 *   transcribe_detected_language, and the `text` field of segment / word
 *   / token rows copied out by transcribe_get_segment / _word / _token)
 *   returns a borrowed pointer aliasing session-owned storage.
 * - Offline path: the pointer is valid until the NEXT call to
 *   transcribe_run / transcribe_stream_begin / transcribe_stream_reset
 *   / transcribe_session_free on the same session. Read it, copy bytes
 *   if you need them past the next mutating call, and otherwise discard.
 * - Streaming path: transcribe_full_text and row `text` pointers are raw
 *   model snapshot views and may be replaced by every feed/finalize call.
 *   Use transcribe_stream_get_text() for the UI-facing committed/tentative
 *   text views. Its committed_text pointer remains stable until the next
 *   stream mutation, and its bytes are append-only for the life of the
 *   stream.
 * - Bindings that marshal-and-copy at the FFI boundary (Python c_char_p,
 *   Go C.GoString, Rust CStr::to_string, etc.) are safe by construction
 *   regardless of the committed/tentative distinction, and remain the
 *   recommended pattern for any binding that surfaces results as owned
 *   strings in the host language.
 */

#ifndef TRANSCRIBE_H
#define TRANSCRIBE_H

#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>

/* ----------------------------------------------------------------------- */
/* Version                                                                 */
/* ----------------------------------------------------------------------- */
/*
 * Single source of truth for the native library version. The top-level
 * CMakeLists.txt parses these three macros to set the CMake project version and
 * the shared-library VERSION/SOVERSION, and transcribe_version() (declared
 * below) returns the same MAJOR.MINOR.PATCH string. Bump the library version
 * here. Pre-1.0 the on-disk ABI MAY break between minor releases (see "ABI
 * stability" above); the Python binding pins its package version to this value
 * exactly and refuses to load a native provider that does not match.
 */
#define TRANSCRIBE_VERSION_MAJOR 0
#define TRANSCRIBE_VERSION_MINOR 2
#define TRANSCRIBE_VERSION_PATCH 2

#define TRANSCRIBE_VERSION_STRINGIZE_(x) #x
#define TRANSCRIBE_VERSION_STRINGIZE(x)  TRANSCRIBE_VERSION_STRINGIZE_(x)
#define TRANSCRIBE_VERSION                                                                       \
    TRANSCRIBE_VERSION_STRINGIZE(TRANSCRIBE_VERSION_MAJOR)                                       \
    "." TRANSCRIBE_VERSION_STRINGIZE(TRANSCRIBE_VERSION_MINOR) "." TRANSCRIBE_VERSION_STRINGIZE( \
        TRANSCRIBE_VERSION_PATCH)

/* Monotonic integer form (MAJOR*10000 + MINOR*100 + PATCH) for compile-time
 * comparisons, e.g. `#if TRANSCRIBE_VERSION_NUMBER >= 200`. */
#define TRANSCRIBE_VERSION_NUMBER \
    (TRANSCRIBE_VERSION_MAJOR * 10000 + TRANSCRIBE_VERSION_MINOR * 100 + TRANSCRIBE_VERSION_PATCH)

#ifndef TRANSCRIBE_API
#    if defined(_WIN32) && !defined(__GNUC__)
#        if defined(TRANSCRIBE_STATIC)
/* Static archive (lib build or static consumer): no dllimport/export.
        * A consumer linking the static transcribe.lib must NOT see dllimport
        * or the linker chases __imp_* thunks the archive never provides
        * (LNK2019). The static `transcribe` CMake target propagates this
        * PUBLIC; non-CMake static consumers define it themselves. */
#            define TRANSCRIBE_API
#        elif defined(TRANSCRIBE_BUILD)
#            define TRANSCRIBE_API __declspec(dllexport)
#        else
#            define TRANSCRIBE_API __declspec(dllimport)
#        endif
#    else
#        define TRANSCRIBE_API __attribute__((visibility("default")))
#    endif
#endif

#ifdef __cplusplus
extern "C" {
#endif

/* ----------------------------------------------------------------------- */
/* Status                                                                  */
/* ----------------------------------------------------------------------- */

typedef enum {
    TRANSCRIBE_OK                         = 0,
    TRANSCRIBE_ERR_INVALID_ARG            = 1,
    TRANSCRIBE_ERR_NOT_IMPLEMENTED        = 2,
    TRANSCRIBE_ERR_FILE_NOT_FOUND         = 3,
    TRANSCRIBE_ERR_GGUF                   = 4,
    TRANSCRIBE_ERR_UNSUPPORTED_ARCH       = 5,
    TRANSCRIBE_ERR_UNSUPPORTED_VARIANT    = 6,
    TRANSCRIBE_ERR_OOM                    = 7,
    TRANSCRIBE_ERR_BACKEND                = 8,
    /* Reserved; not currently returned. */
    TRANSCRIBE_ERR_SAMPLE_RATE            = 9,
    TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE   = 10,
    TRANSCRIBE_ERR_UNSUPPORTED_TASK       = 11,
    /*
     * Returned by transcribe_run when the caller asks for a
     * timestamp granularity finer than the model can produce.
     * Compared against transcribe_capabilities::max_timestamp_kind,
     * which is set per family at load time. AUTO never trips this
     * code: the dispatcher passes AUTO through unconditionally and
     * the per-family run() handler resolves it to the model's
     * maximum when it assembles the result.
     */
    TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS = 12,
    /*
     * Returned by transcribe_run when the caller's abort callback
     * returns true during the run. Partial results from chunks that
     * completed before the abort are preserved on the session and
     * readable via the normal result accessors; transcribe_was_aborted
     * distinguishes partial-from-abort from complete.
     */
    TRANSCRIBE_ERR_ABORTED                = 13,
    /*
     * Returned by every entry point that takes a caller-owned size-
     * aware struct (params, stream params, capabilities out, timings
     * out, family extension) when the supplied struct_size is zero or
     * smaller than the minimum the call path requires. Distinct from
     * INVALID_ARG so bindings can match on "caller forgot to call the
     * transcribe_*_init function" specifically.
     */
    TRANSCRIBE_ERR_BAD_STRUCT_SIZE        = 14,
    /* Reserved; not currently returned. */
    TRANSCRIBE_ERR_UNSUPPORTED_PNC        = 15,
    /* Reserved; not currently returned. */
    TRANSCRIBE_ERR_UNSUPPORTED_ITN        = 16,
    /*
     * Returned by transcribe_run / transcribe_run_batch when the input
     * audio is longer than the loaded model can process in a single
     * decode. Hard-context-cap families (LLM-style decoders: qwen3_asr,
     * canary_qwen, funasr_nano, granite, granite_nar, voxtral, cohere,
     * canary) reject an over-length clip UP FRONT — before the decode
     * (and, where the binding limit is the encoder's positional table,
     * before the encoder) — by comparing the audio's prefill token count
     * against the model's context window. The usable ceiling is published
     * as transcribe_capabilities::max_audio_ms, so a caller can size input
     * before calling rather than discovering the limit on failure.
     *
     * Distinct from INVALID_ARG so a caller can tell "audio too long for
     * this model" from a malformed argument. This is the "couldn't start"
     * signal; the symmetric "started, couldn't finish" outcome is
     * TRANSCRIBE_ERR_OUTPUT_TRUNCATED.
     *
     * Chunked / unbounded families normally report max_audio_ms == 0 and
     * have no practical input limit. Whisper and parakeet never return this
     * for length. voxtral_realtime is the exception: it still reports
     * unbounded because its wall is far beyond practical clips, but
     * transcribe_run / transcribe_run_batch return this if input crosses the
     * decoder's absolute position cap.
     *
     * See docs/input-limits.md for the full contract.
     */
    TRANSCRIBE_ERR_INPUT_TOO_LONG         = 17,
    /*
     * Returned by transcribe_run when the decode stopped because it hit
     * the model's context / generation budget BEFORE the model emitted
     * end-of-stream — i.e. the transcript is incomplete. This is the
     * "started, couldn't finish" counterpart to INPUT_TOO_LONG, and it is
     * a hard non-OK status by design: a truncated transcript must not be
     * mistaken for a complete one.
     *
     * The partial transcript IS preserved and readable through the normal
     * result accessors (transcribe_full_text, segments, words, tokens),
     * exactly like TRANSCRIBE_ERR_ABORTED — a caller that wants the partial
     * output reads it after checking the status. transcribe_was_truncated()
     * remains as supplemental state (it is true whenever this status is
     * returned). Unlike INPUT_TOO_LONG this cannot be predicted from input
     * length (it depends on how long the transcript runs), so it is
     * reported after the fact rather than gated up front.
     *
     * In transcribe_run_batch this is a PER-UTTERANCE status: the whole-
     * batch call still returns TRANSCRIBE_OK and the truncated utterance
     * carries this code in transcribe_batch_status(session, i), the same
     * way per-utterance INVALID_ARG / INPUT_TOO_LONG are reported.
     *
     * Streaming does NOT use this code: an active stream is incremental and
     * has its own terminal-state machine (see transcribe_stream_*).
     * See docs/input-limits.md for the full contract.
     */
    TRANSCRIBE_ERR_OUTPUT_TRUNCATED       = 18,
} transcribe_status;

/*
 * Returns a statically allocated, NUL-terminated description of a status
 * code. The returned pointer must not be freed and is valid for the
 * lifetime of the process.
 *
 * The parameter is `int` (not `transcribe_status`) for two reasons:
 *   1. It matches the convention of `strerror(int)` and lets callers pass
 *      an arbitrary integer without forming a bogus enum value.
 *   2. Casting an out-of-range integer to a C++ enum without a fixed
 *      underlying type is undefined behavior. Taking an int means the
 *      "out of range returns 'unknown status'" contract can be exercised
 *      portably without tripping UBSan.
 *
 * Any value of transcribe_status converts implicitly to int at the call
 * site, so existing C and C++ callers continue to compile unchanged.
 */
TRANSCRIBE_API const char * transcribe_status_string(int status);

/* ----------------------------------------------------------------------- */
/* Version                                                                 */
/* ----------------------------------------------------------------------- */
/*
 * Runtime version of the loaded native library. The numeric components are
 * also available at compile time as TRANSCRIBE_VERSION_MAJOR/MINOR/PATCH (and
 * the composed string as TRANSCRIBE_VERSION) near the top of this header.
 *
 * Both return borrowed pointers into static storage: never free them, and
 * treat them as valid for the life of the process.
 */
/* "MAJOR.MINOR.PATCH", e.g. "0.1.0". Equals the TRANSCRIBE_VERSION macro the
 * caller compiled against; a mismatch means the header and the linked library
 * disagree. */
TRANSCRIBE_API const char * transcribe_version(void);
/* Short git commit the library was built from, or "unknown" when the build
 * tree carried no git metadata (e.g. an unpacked source tarball). */
TRANSCRIBE_API const char * transcribe_version_commit(void);

/* ----------------------------------------------------------------------- */
/* ABI metadata                                                            */
/* ----------------------------------------------------------------------- */
/*
 * The native library's sizeof/alignof for the public structs that cross the
 * ABI. A binding (Python ctypes, Rust, Swift, ...) declares its own view of
 * each struct, then verifies that view against these values before it
 * constructs any real instance. This is the safe alternative the "Params"
 * section calls for: query the size up front instead of calling a *_init()
 * function on a buffer that might be smaller than the library expects.
 *
 * `which` selects a struct by transcribe_abi_struct. An unknown id (e.g. a
 * newer binding asking an older library about a struct it predates) returns 0,
 * which the binding MUST treat as "cannot verify," never as "size 0." The enum
 * is append-only; do not renumber existing values.
 */
typedef enum {
    TRANSCRIBE_ABI_MODEL_LOAD_PARAMS = 0,
    TRANSCRIBE_ABI_SESSION_PARAMS    = 1,
    TRANSCRIBE_ABI_RUN_PARAMS        = 2,
    TRANSCRIBE_ABI_STREAM_PARAMS     = 3,
    TRANSCRIBE_ABI_CAPABILITIES      = 4,
    TRANSCRIBE_ABI_TIMINGS           = 5,
    TRANSCRIBE_ABI_SEGMENT           = 6,
    TRANSCRIBE_ABI_WORD              = 7,
    TRANSCRIBE_ABI_TOKEN             = 8,
    TRANSCRIBE_ABI_STREAM_UPDATE     = 9,
    TRANSCRIBE_ABI_STREAM_TEXT       = 10,
    TRANSCRIBE_ABI_SESSION_LIMITS    = 11,
    TRANSCRIBE_ABI_EXT               = 12,
    TRANSCRIBE_ABI_DEVICE_INFO       = 13,
    TRANSCRIBE_ABI_SPEAKER_SEGMENT   = 14,
} transcribe_abi_struct;

/* sizeof / alignof of the selected public struct, or 0 for an unknown id.
 * For every struct that carries a struct_size field, the size returned here
 * equals the value its transcribe_*_init() stamps into struct_size. */
TRANSCRIBE_API size_t transcribe_abi_struct_size(transcribe_abi_struct which);
TRANSCRIBE_API size_t transcribe_abi_struct_align(transcribe_abi_struct which);

/* ----------------------------------------------------------------------- */
/* Logging                                                                 */
/* ----------------------------------------------------------------------- */

/*
 * This enum is the source of truth for callers. ggml's log levels do NOT
 * share these numeric values (its DEBUG/INFO/WARN/ERROR ordering differs),
 * so the internal pass-through layer maps ggml levels onto this enum
 * before any message reaches the installed callback; callers only ever
 * see these values. CONT marks a fragment continuing the previous message
 * (ggml emits these for progress output); hosts that want one line per
 * message may join CONT fragments onto the preceding text.
 */
typedef enum {
    TRANSCRIBE_LOG_LEVEL_NONE  = 0,
    TRANSCRIBE_LOG_LEVEL_INFO  = 1,
    TRANSCRIBE_LOG_LEVEL_WARN  = 2,
    TRANSCRIBE_LOG_LEVEL_ERROR = 3,
    TRANSCRIBE_LOG_LEVEL_DEBUG = 4,
    TRANSCRIBE_LOG_LEVEL_CONT  = 5, /* continue previous line */
} transcribe_log_level;

typedef void (*transcribe_log_callback)(transcribe_log_level level, const char * msg, void * userdata);

/*
 * Global log sink. Three states:
 *
 *   never called          library messages that warrant surfacing go to
 *                         stderr (the dev/CLI default); ggml's internal
 *                         logging keeps its own stderr default.
 *   cb != NULL            every library message AND ggml's internal
 *                         diagnostics (mapped onto transcribe_log_level)
 *                         route to the callback. Caveat: ggml compiles
 *                         its per-module load-failure chatter OUT of
 *                         release builds, so the reliable "why am I not
 *                         on the GPU?" signals are the one-line device
 *                         summary transcribe_init_backends() emits
 *                         through this sink after each fresh scan, and
 *                         the transcribe_backend_* device accessors.
 *   cb == NULL            logging explicitly disabled: library and ggml
 *                         messages are dropped, nothing goes to stderr.
 *
 * The callback must not throw. If it does anyway, the exception is contained
 * at the emission site, the message is dropped, and a note is written to
 * stderr.
 *
 * Call once at process startup (see the threading contract above).
 */
TRANSCRIBE_API void transcribe_log_set(transcribe_log_callback cb, void * userdata);

/* ----------------------------------------------------------------------- */
/* Task / timestamps                                                       */
/* ----------------------------------------------------------------------- */

typedef enum {
    TRANSCRIBE_TASK_TRANSCRIBE = 0,
    TRANSCRIBE_TASK_TRANSLATE  = 1,
} transcribe_task;

/*
 * Timestamp policy: transcribe_run_params_init() requests AUTO, the richest
 * supported output. AUTO requests the richest granularity compatible with the model and
 * the other selected run tasks (for example, a prompt-selected diarization
 * task may not compose with the model's separate timestamp task).
 * The dispatcher never rejects AUTO, and the per-family run() handler
 * resolves it to the finest granularity the model can actually
 * produce when it assembles the result. A non-AUTO request is treated
 * as a ceiling: if the request is finer than the model's max,
 * transcribe_run returns TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS. If the
 * request is coarser-or-equal, the family handler emits only that
 * granularity and any finer per-run data is elided. The actual
 * granularity returned by a run is reported by
 * transcribe_returned_timestamp_kind(session).
 */
typedef enum {
    TRANSCRIBE_TIMESTAMPS_NONE    = 0,
    TRANSCRIBE_TIMESTAMPS_AUTO    = 1,
    TRANSCRIBE_TIMESTAMPS_SEGMENT = 2,
    TRANSCRIBE_TIMESTAMPS_WORD    = 3,
    TRANSCRIBE_TIMESTAMPS_TOKEN   = 4,
} transcribe_timestamp_kind;

/*
 * KV cache type for the encoder's flash-attention path.
 *
 * AUTO:  f16 when model weights are quantized, f32 when weights are f32.
 *        Best default — matches the bandwidth profile of the model.
 * F32:   full-precision KV. Maximum accuracy, highest bandwidth.
 * F16:   half-precision KV. Halves attention bandwidth with minimal
 *        precision loss (~3 decimal digits). Best for bandwidth-bound
 *        backends (integrated GPUs, CPU).
 */
typedef enum {
    TRANSCRIBE_KV_TYPE_AUTO = 0,
    TRANSCRIBE_KV_TYPE_F32  = 1,
    TRANSCRIBE_KV_TYPE_F16  = 2,
} transcribe_kv_type;

/*
 * Punctuation + capitalization (PNC) toggle on the run params.
 *
 * Each family that exposes a runtime PNC toggle reads this field; families
 * that do not (whisper, parakeet, ...) ignore it. The dispatcher emits a
 * WARN-level log message when a non-DEFAULT value is set against a model
 * for which transcribe_model_supports(model, TRANSCRIBE_FEATURE_PNC)
 * returns false, then proceeds with the model's default behavior. Use
 * that probe to pre-check.
 *
 *   DEFAULT (0): the family's shipped default. Zero-init via
 *                transcribe_run_params_init() gives this value. The family
 *                picks the value its model card published WER numbers
 *                against (canary: pnc on; others: family-specific).
 *   OFF:         explicitly disable runtime PNC. Supporting families
 *                emit lowercase, de-punctuated text. Non-supporting
 *                families ignore (with WARN).
 *   ON:          explicitly enable runtime PNC. Supporting families
 *                emit punctuated, capitalized text. Non-supporting
 *                families ignore (with WARN).
 */
enum transcribe_pnc_mode {
    TRANSCRIBE_PNC_MODE_DEFAULT = 0,
    TRANSCRIBE_PNC_MODE_OFF     = 1,
    TRANSCRIBE_PNC_MODE_ON      = 2,
};

/*
 * Inverse text normalization (ITN) toggle on the run params.
 *
 * Symmetric semantics to transcribe_pnc_mode but for ITN — the
 * transformation that renders numbers, dates, currencies, etc. in formal
 * form ("twenty twenty four" → "2024", "ten dollars" → "$10"). Each
 * family that exposes a runtime ITN toggle reads this field; families
 * that do not ignore it. Non-DEFAULT values against a model for which
 * transcribe_model_supports(model, TRANSCRIBE_FEATURE_ITN) returns false
 * emit a WARN and proceed with default behavior.
 *
 *   DEFAULT (0): family default. Zero-init gives this value.
 *   OFF:         explicit ITN off. Supporting families emit verbatim
 *                spoken-form text. Non-supporting families ignore (WARN).
 *   ON:          explicit ITN on. Supporting families apply ITN.
 *                Non-supporting families ignore (WARN).
 *
 * Some families bundle PNC and ITN (e.g. their ITN toggle also flips
 * punctuation/casing as a side effect). See the family doc for the
 * observed bundling.
 */
enum transcribe_itn_mode {
    TRANSCRIBE_ITN_MODE_DEFAULT = 0,
    TRANSCRIBE_ITN_MODE_OFF     = 1,
    TRANSCRIBE_ITN_MODE_ON      = 2,
};

/*
 * Speaker-diarization toggle on the run params.
 *
 * Symmetric semantics to transcribe_pnc_mode / transcribe_itn_mode but
 * for speaker attribution. Families whose model emits speaker-attributed
 * output read this field; families that do not ignore it. Non-DEFAULT
 * values against a model for which
 * transcribe_model_supports(model, TRANSCRIBE_FEATURE_DIARIZATION)
 * returns false emit a WARN and proceed with default behavior.
 *
 * When ON resolves, the model's speaker markers are parsed into
 * structured results: segment rows carry speaker_id and the
 * speaker-segment accessors (transcribe_n_speaker_segments /
 * transcribe_get_speaker_segment) are populated, with the markers
 * stripped from the text. How a family produces markers is
 * family-specific: moss always emits them (parsing is pure host-side
 * post-processing), while granite-speech-4.1-2b-plus emits them only
 * when its prompt requests the speaker-attribution task, so there ON
 * changes the instruction the model is given. See each family doc.
 *
 *   DEFAULT (0): library default: OFF for every family. Zero-init gives
 *                this value.
 *   OFF:         no speaker attribution. Families where diarization is a
 *                requested task do not request it. Families such as moss
 *                whose model always emits inline metadata still strip that
 *                metadata so full_text remains a clean transcript.
 *   ON:          request (if needed) and parse speaker markers into
 *                segment speaker_id + speaker segments.
 */
enum transcribe_diarize_mode {
    TRANSCRIBE_DIARIZE_MODE_DEFAULT = 0,
    TRANSCRIBE_DIARIZE_MODE_OFF     = 1,
    TRANSCRIBE_DIARIZE_MODE_ON      = 2,
};

/* ----------------------------------------------------------------------- */
/* Handles                                                                 */
/* ----------------------------------------------------------------------- */

struct transcribe_model;
struct transcribe_session;

/* ----------------------------------------------------------------------- */
/* Family extensions                                                       */
/* ----------------------------------------------------------------------- */

/*
 * Generic header for every typed family extension struct. The kind tag
 * names the family schema (one FourCC-style 32-bit value per schema,
 * allocated in docs/extension-kinds.md); the size is the caller's
 * sizeof of the full extension struct, captured in the caller's
 * translation unit so a newer library only reads what fits.
 *
 * Each family declares its extension struct in
 * include/transcribe/<family>.h with `struct transcribe_ext ext` as
 * field 0 and a transcribe_<family>_<name>_ext_init() function that
 * fills `ext.size`, `ext.kind`, and per-field defaults from the
 * caller's perspective. `ext.size` is `uint64_t` so the family
 * extension layout is platform-invariant across 32/64-bit ABIs;
 * transcribe_ext_check() also takes `uint64_t` for `min_size`.
 *
 * Pointer-to-first-member is well-defined: because `ext` sits at
 * field 0, the address of the family struct equals the address of its
 * embedded transcribe_ext, and family handlers can cast the generic
 * pointer back to the typed family struct after a successful
 * transcribe_ext_check.
 *
 * Include transcribe/extensions.h to pull in every family extension
 * header shipped by this install. Binding generators should usually
 * point at that umbrella header; direct C/C++ callers that only need
 * the generic ABI can keep including transcribe.h. See
 * docs/extension-kinds.md for the registered family extension headers
 * and kind values.
 */
struct transcribe_ext {
    uint64_t size;
    uint32_t kind;
};

/*
 * Validate the universal shape of an extension before the family casts
 * its pointer to the typed struct.
 *
 *   ext == NULL                    -> TRANSCRIBE_OK (family decides
 *                                     what NULL means; usually "defaults").
 *   ext->size < sizeof(transcribe_ext)
 *                                  -> TRANSCRIBE_ERR_BAD_STRUCT_SIZE.
 *   ext->kind != expected_kind     -> TRANSCRIBE_ERR_INVALID_ARG.
 *   ext->size < min_size           -> TRANSCRIBE_ERR_BAD_STRUCT_SIZE.
 *   otherwise                       -> TRANSCRIBE_OK.
 *
 * The family handler owns `expected_kind` (its own TRANSCRIBE_EXT_KIND_*
 * constant) and `min_size` (typically sizeof of the kind's minimum
 * supported layout). The helper exists so every family rejects malformed
 * input identically.
 */
TRANSCRIBE_API transcribe_status transcribe_ext_check(const struct transcribe_ext * ext,
                                                      uint32_t                      expected_kind,
                                                      uint64_t                      min_size);

/*
 * Extension slot — the API surface a typed family extension is pointed
 * at. Slot acceptance is a model-and-slot concern; the dispatcher
 * validates `slot` matches the call site before delegating to the
 * family. A stream-only extension pointed at transcribe_run_params is
 * INVALID_ARG, not silently-ignored. Add new slots by appending to
 * this enum (SESSION / MODEL_LOAD / TOKENIZE would each be one new
 * value, no new probe function); do not renumber.
 */
typedef enum {
    /* transcribe_run_params::family */
    TRANSCRIBE_EXT_SLOT_RUN    = 0,
    /* transcribe_stream_params::family */
    TRANSCRIBE_EXT_SLOT_STREAM = 1,
} transcribe_ext_slot;

/*
 * Returns true when the loaded model variant accepts the named
 * extension kind in the given slot. The probe is per-loaded-model-
 * variant AND per-slot — a family may ship multiple extension kinds
 * that apply to different variants (e.g. parakeet streaming has
 * cache-aware and chunked-attention variants whose stream knobs are
 * different kinds), and the same kind may only be legal on one slot
 * (a streaming extension on the RUN slot is not "ignored"; it is
 * rejected by transcribe_run with INVALID_ARG).
 *
 * Returns false if model is NULL, if the model's family has no
 * extension surface for the requested slot at all, or if the kind is
 * unknown for that slot. The dispatcher's call-site validation always
 * routes through this probe with the correct slot for the entry point.
 *
 * Callers should write intent-first probes ("does this model accept
 * the parakeet stream kind on the STREAM slot?"), not discovery loops
 * over every known kind. See docs/extension-kinds.md for the registered
 * kinds and their slots.
 */
TRANSCRIBE_API bool transcribe_model_accepts_ext_kind(const struct transcribe_model * model,
                                                      transcribe_ext_slot             slot,
                                                      uint32_t                        kind);

/* ----------------------------------------------------------------------- */
/* Params                                                                  */
/* ----------------------------------------------------------------------- */

/*
 * Backend request.
 *
 * AUTO    Pick the best available backend. Takes the first GPU device
 *         that successfully initializes, probing every discrete GPU
 *         before any integrated GPU; within a tier, devices are tried
 *         in ggml's device registry order — which is build-time
 *         prioritized (Metal on Apple, Vulkan / CUDA / ROCm / SYCL on
 *         Linux, …). An integrated GPU is selected only when no
 *         discrete GPU initializes. Host-memory accelerators (BLAS,
 *         AMX, …) are additionally layered onto the scheduler when
 *         present — they run on the same memory as the CPU backend
 *         and are orthogonal to the GPU/CPU split. Always succeeds:
 *         CPU is the final fallback when no GPU initializes.
 *
 * CPU     Strict CPU only. No GPU, no IGPU, and no host-memory
 *         accelerators (BLAS/AMX). This is the right choice for
 *         numerical reference runs, cross-platform determinism, and
 *         for exercising the pure-CPU kernels under test. Always
 *         succeeds.
 *
 * CPU_ACCEL  CPU primary plus host-memory accelerators (BLAS/AMX/…
 *            whatever ggml registers as ACCEL). primary_kind stays
 *            Cpu so any CPU-keyed policy still triggers, but mat-mul-
 *            shaped graph nodes that fit the accel backend's gates
 *            (large F32 src1, ne ≥ 32) get dispatched there. Useful
 *            when GPU is unavailable or undesired but you still want
 *            production-grade CPU throughput. Requires the build to
 *            include the relevant accel backend (e.g. GGML_BLAS=ON).
 *            Falls back to plain CPU semantics if no accel device is
 *            registered. Always succeeds.
 *
 * METAL   Require the Metal backend. Returns TRANSCRIBE_ERR_BACKEND
 *         if Metal is not available in this build. Host-memory
 *         accelerators are still layered on when present.
 *
 * VULKAN  Require the Vulkan backend. Returns TRANSCRIBE_ERR_BACKEND
 *         if Vulkan is not available in this build. Host-memory
 *         accelerators are still layered on when present.
 *
 * CUDA    Require the NVIDIA CUDA backend. Returns TRANSCRIBE_ERR_BACKEND
 *         if CUDA is not available in this build.
 *
 * ROCM    Require the AMD ROCm backend. Returns TRANSCRIBE_ERR_BACKEND
 *         if ROCm is not available in this build.
 *
 * Callers that need to know which backend they actually landed on
 * can query transcribe_model_backend() after load.
 */
typedef enum {
    TRANSCRIBE_BACKEND_AUTO      = 0,
    TRANSCRIBE_BACKEND_CPU       = 1,
    TRANSCRIBE_BACKEND_METAL     = 2,
    TRANSCRIBE_BACKEND_VULKAN    = 3,
    TRANSCRIBE_BACKEND_CPU_ACCEL = 4,
    TRANSCRIBE_BACKEND_CUDA      = 5,
    TRANSCRIBE_BACKEND_ROCM      = 6,
} transcribe_backend_request;

/* ----------------------------------------------------------------------- */
/* Backend modules and device discovery                                    */
/* ----------------------------------------------------------------------- */
/*
 * In dynamic-backend builds (GGML_BACKEND_DL, selected by the
 * TRANSCRIBE_GGML_BACKEND_DL build option), compute backends (CPU, Vulkan,
 * CUDA, ...) are separate loadable modules shipped next to the library in a
 * provider artifact directory, not compiled into it. A host (a Python
 * wheel, a Rust crate, an app bundle) loads them ONCE, before the first
 * model load, by pointing the library at that directory. In static builds
 * the compiled-in backends are already registered, and this call is a
 * harmless no-op for a directory containing no modules.
 *
 * The search is strictly package-local: only artifact_dir is scanned —
 * never the executable directory, the working directory, or any system
 * path. (ggml additionally honors the GGML_BACKEND_PATH environment
 * variable as an explicit user override naming one out-of-tree backend
 * module; an unset environment means a fully package-local load.)
 *
 * A module whose system dependencies are missing — e.g. the Vulkan module
 * on a machine with no Vulkan loader, or with a loader but no driver —
 * fails to load quietly and is skipped; the remaining backends keep
 * working. That degradation is the designed behavior: ship Vulkan by
 * default, fall back to CPU on machines that cannot run it. Use the device
 * accessors below to see what actually registered, and
 * transcribe_backend_available() to probe one kind.
 *
 * Idempotent per directory: repeat calls with an already-scanned directory
 * return the SAME status as the first scan without re-loading — including
 * the TRANSCRIBE_ERR_BACKEND case, which is therefore NOT retryable in
 * this process (e.g. installing a driver after the first call requires a
 * new process to pick up). Concurrent calls to this function are
 * serialized against each other; "thread-safe" extends no further — the
 * load mutates the global device registry, so it must complete before any
 * thread calls the device accessors below or loads a model. That ordering
 * is automatic under the supported usage (call once, before the first
 * model load).
 *
 * artifact_dir is UTF-8, like every path crossing this API; on Windows
 * it is converted to a wide path internally.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG     artifact_dir is NULL or empty.
 *   TRANSCRIBE_ERR_FILE_NOT_FOUND  artifact_dir is not an existing directory.
 *   TRANSCRIBE_ERR_BACKEND         after loading, the process has zero
 *                                  registered compute devices (a dynamic
 *                                  build pointed at a directory with no
 *                                  usable modules: nothing could run).
 *   TRANSCRIBE_OK                  otherwise.
 */
TRANSCRIBE_API transcribe_status transcribe_init_backends(const char * artifact_dir);

/*
 * Package-local default for dynamic-backend builds.
 *
 * In a dynamic-backend build, resolves the directory containing the loaded
 * libtranscribe itself and delegates to transcribe_init_backends(dir). This
 * keeps the search package-local: it does NOT scan the executable directory,
 * current working directory, or system paths. Ship backend modules next to
 * libtranscribe for this helper to find them.
 *
 * In non-dynamic builds the compute backends are compiled in, so this is a
 * no-op returning TRANSCRIBE_OK.
 */
TRANSCRIBE_API transcribe_status transcribe_init_backends_default(void);

/*
 * Opaque process-local compute-device handle. Handles are owned by the
 * runtime, remain valid for the life of the process, and must not be freed.
 * They may be compared for equality but are not persistent identifiers; use
 * transcribe_device_get_info() and its device_id field for persistence.
 */
struct transcribe_device;
typedef struct transcribe_device * transcribe_device_t;

/*
 * Number of compute devices currently registered with the runtime
 * (compiled-in backends plus any modules loaded by
 * transcribe_init_backends). A device is something a model can be placed
 * on: the CPU, an Apple GPU via Metal, a Vulkan GPU, ...
 */
TRANSCRIBE_API int transcribe_device_count(void);

/*
 * Return the registered device at `index`, or NULL when index is out of
 * range. The returned handle is runtime-owned and process-local.
 *
 * IMPORTANT: NULL is also the automatic-selection sentinel in
 * transcribe_model_load_params::device. Always check this return value before
 * assigning it to model-load params; assigning an unchecked out-of-range
 * result would request automatic selection rather than exact selection.
 */
TRANSCRIBE_API transcribe_device_t transcribe_device_get(int index);

/*
 * Device type: ggml's vendor-agnostic classification of a device,
 * orthogonal to `kind` below (which carries the vendor: metal/vulkan/cuda/
 * rocm/...). Backends report this classification themselves, so treat it as a
 * runtime hint about CPU/GPU/IGPU/ACCEL placement rather than a portable
 * hardware-memory taxonomy. The numeric values mirror ggml's device-type
 * enum.
 */
typedef enum {
    TRANSCRIBE_DEVICE_TYPE_CPU   = 0, /* CPU using system memory */
    TRANSCRIBE_DEVICE_TYPE_GPU   = 1, /* backend-reported GPU */
    TRANSCRIBE_DEVICE_TYPE_IGPU  = 2, /* backend-reported integrated GPU */
    TRANSCRIBE_DEVICE_TYPE_ACCEL = 3, /* host-memory accelerator (BLAS/AMX) */
} transcribe_device_type;

/*
 * One registered compute device.
 *
 * name / description / kind / device_id are borrowed pointers into
 * runtime-owned storage: valid for the life of the process, never freed by
 * the caller.
 *
 * kind is the library's vendor classification, one of: "cpu", "accel" (a
 * host-memory accelerator such as BLAS/AMX), "metal", "vulkan", "cuda",
 * "rocm", "sycl", "gpu" (an unrecognized GPU), or "unknown". device_type is
 * the orthogonal CPU/GPU/IGPU/ACCEL axis.
 *
 * device_id is a stable hardware identifier when the backend reports one
 * (for PCI devices the lower-case bus id "domain:bus:device.function", e.g.
 * "0000:c1:00.0"), or NULL when unknown (e.g. Metal).
 *
 * memory_total is the device's reported capacity in bytes. memory_free is a
 * SNAPSHOT of available bytes at the moment this struct was filled; it goes
 * stale the instant anything allocates — re-call the accessor to refresh it,
 * as every fill re-queries the driver live. Both numbers are backend-defined
 * and NOT comparable across kinds: on Apple unified memory `total` is the
 * recommended max working-set size (not system RAM) and `free` nets out only
 * this process's allocations; on a discrete GPU they are device-global; on
 * the CPU they are system RAM. 0 means the backend does not report it.
 */
struct transcribe_device_info {
    uint64_t               struct_size;  /* sizeof(*this); set by _init() */
    const char *           name;         /* ggml device name, e.g. "Metal" */
    const char *           description;  /* human-readable, e.g. "Apple M4 Max" */
    const char *           kind;         /* vendor kind string; see above */
    const char *           device_id;    /* stable hw id (PCI bus id) or NULL */
    uint64_t               memory_total; /* reported capacity in bytes, or 0 */
    uint64_t               memory_free;  /* available bytes snapshot, or 0 */
    transcribe_device_type device_type;  /* CPU/GPU/IGPU/ACCEL axis */
};

TRANSCRIBE_API void transcribe_device_info_init(struct transcribe_device_info * p);

/*
 * Fill *out (initialized via transcribe_device_info_init) with information
 * about `device`. memory_free is live as of this call; re-invoke to refresh it
 * (e.g. to poll a device's available memory over time).
 *
 * Returns TRANSCRIBE_ERR_INVALID_ARG if device or out is NULL or device is
 * not from this runtime's registry. Returns TRANSCRIBE_ERR_BAD_STRUCT_SIZE if
 * out fails the struct-size check.
 */
TRANSCRIBE_API transcribe_status transcribe_device_get_info(transcribe_device_t             device,
                                                            struct transcribe_device_info * out);

/*
 * Whether a backend request can be satisfied by some registered device:
 * AUTO whenever any device exists; CPU and CPU_ACCEL when a CPU device
 * exists; METAL / VULKAN / CUDA / ROCM when a device of that kind exists.
 * Unknown or invalid request values answer false (never an error). This is the
 * probe a binding uses to turn `backend="vulkan"` on a machine without
 * Vulkan into a clear exception instead of a failed model load.
 */
TRANSCRIBE_API bool transcribe_backend_available(transcribe_backend_request kind);

/*
 * Return the compute device this loaded model is running on — the device
 * that owns its weights and runs most of its graph. Returns NULL if model is
 * NULL or has no resolved compute device. Pass the returned handle to
 * transcribe_device_get_info() for metadata and a live memory snapshot.
 */
TRANSCRIBE_API transcribe_device_t transcribe_model_device(const struct transcribe_model * model);

/*
 * Initialization of caller-owned params structs.
 *
 * Every params struct below is initialized by its transcribe_*_init()
 * function, which fills sensible defaults and the struct_size field:
 *
 *     struct transcribe_run_params p;
 *     transcribe_run_params_init(&p);   // defaults + struct_size
 *     p.timestamps = TRANSCRIBE_TIMESTAMPS_WORD;
 *
 * Defaults shortcut: pass NULL wherever a `const struct transcribe_*_params *`
 * is accepted to get pure defaults without declaring a struct at all.
 * NULL is the only defaults form. A struct with `struct_size == 0` —
 * including `struct X p = {0};` — is REJECTED with
 * TRANSCRIBE_ERR_BAD_STRUCT_SIZE; this is deliberate so that
 * uninitialized stack memory whose struct_size byte happens to be zero
 * cannot silently masquerade as "all defaults." The init function (or
 * NULL) is the only safe calling convention.
 *
 * The init functions are one-argument by design: they assume the caller
 * and library agree on the struct layout, which holds for the supported
 * distribution model (the library is built/shipped with its consumers).
 */

/*
 * Model load params.
 *
 * backend:    which backend to request. See transcribe_backend_request
 *             for the semantics of each value. Default is AUTO.
 *
 * device:  NULL (the default) applies the backend's automatic policy. AUTO
 *          probes every discrete GPU before integrated GPUs and finally falls
 *          back to CPU; an explicit GPU backend picks the first matching
 *          device. A non-NULL handle selects that exact registered device,
 *          including the device returned at index 0.
 *
 *          Exact selection never silently falls back to another primary
 *          device. With backend=AUTO, the selected device determines the
 *          backend. With an explicit backend, the device must match it. CPU
 *          and CPU_ACCEL accept an exact CPU device; ACCEL devices cannot be
 *          selected as a primary. Invalid, foreign, or mismatched handles are
 *          rejected with TRANSCRIBE_ERR_INVALID_ARG.
 *
 *          Handles are process-local. Persist device_id (when available), then
 *          enumerate and resolve a fresh handle in each process.
 */
struct transcribe_model_load_params {
    uint64_t                   struct_size;
    transcribe_backend_request backend;
    transcribe_device_t        device;
};

TRANSCRIBE_API void transcribe_model_load_params_init(struct transcribe_model_load_params * params);

/*
 * Session init params.
 *
 * n_threads: number of CPU threads for ops that run on CPU. 0 means
 *            "library picks a sensible default".
 *
 * kv_type:   data type for K/V activations in flash attention.
 *            AUTO (default) uses f16 for quantized models, f32 for f32.
 *
 * n_ctx:     optional cap on the decoder context window, in tokens, for
 *            families with a decoder KV cache. It is a memory knob, not an
 *            accuracy knob.
 *              0 (default): use the model's true maximum, read from GGUF
 *                           metadata. This is the right value for almost
 *                           every caller.
 *              > 0:         lower the ceiling to bound KV-cache memory. A
 *                           value above the model maximum is clamped DOWN
 *                           to it — the knob can only narrow, never extend
 *                           past what the model was trained to support.
 *            For decoder-context-bound families (audio-tokens + prompt +
 *            generation share one window), lowering n_ctx lowers the
 *            effective input limit. That effective limit is a per-session
 *            value: read it via transcribe_session_get_limits()
 *            (effective_max_audio_ms), NOT transcribe_capabilities::
 *            max_audio_ms, which is model-level and reflects only the
 *            default context. For encoder-bound families, n_ctx may lower
 *            decoder KV memory / output budget without lowering the input-
 *            audio bound. Ignored by chunked / unbounded families (whisper,
 *            parakeet, voxtral_realtime) — they have no lowerable ceiling, so
 *            a non-zero n_ctx is a no-op there. A negative value returns
 *            TRANSCRIBE_ERR_INVALID_ARG.
 */
struct transcribe_session_params {
    uint64_t           struct_size;
    int                n_threads;
    transcribe_kv_type kv_type;
    int32_t            n_ctx;
};

TRANSCRIBE_API void transcribe_session_params_init(struct transcribe_session_params * params);

/*
 * Per-run params.
 *
 * v1 accepts only 16 kHz mono float32 PCM. There is no caller-supplied
 * sample rate; the library does not link a resampler; the caller is
 * responsible for resampling external audio (e.g. with sox or ffmpeg)
 * before calling transcribe_run. A future revision will introduce a
 * caller-declared input rate, at which point TRANSCRIBE_ERR_SAMPLE_RATE
 * (currently reserved) will become observable.
 *
 * task:        TRANSCRIBE or TRANSLATE. The model must declare support
 *              for translate via its capabilities; otherwise the run
 *              returns TRANSCRIBE_ERR_UNSUPPORTED_TASK.
 *
 * timestamps:  requested granularity. Default params request AUTO,
 *              which selects the finest granularity compatible with
 *              the model and other selected run tasks.
 *
 * pnc:         punctuation+capitalization runtime toggle. See
 *              transcribe_pnc_mode. DEFAULT is always safe. Non-DEFAULT
 *              values against models for which
 *              transcribe_model_supports(model, TRANSCRIBE_FEATURE_PNC) is
 *              false emit a WARN and proceed with the model's default
 *              behavior.
 *
 * itn:         inverse text normalization runtime toggle. See
 *              transcribe_itn_mode. DEFAULT is always safe. Non-DEFAULT
 *              values against models for which
 *              transcribe_model_supports(model, TRANSCRIBE_FEATURE_ITN) is
 *              false emit a WARN and proceed with the model's default
 *              behavior.
 *
 * diarize:     speaker-attribution runtime toggle. See
 *              transcribe_diarize_mode. DEFAULT is always safe. Non-DEFAULT
 *              values against models for which
 *              transcribe_model_supports(model, TRANSCRIBE_FEATURE_DIARIZATION)
 *              is false emit a WARN and proceed with the model's default
 *              behavior.
 *
 * language:        source language hint as a BCP-47-ish short code, or
 *                  NULL to autodetect (only if the model supports it).
 *
 * target_language: target language for translation tasks, or NULL.
 *
 * String-pointer lifetime (language / target_language): caller-owned, and
 * the library copies what it needs before the API call returns. This holds
 * for transcribe_run / transcribe_run_batch (synchronous) AND for
 * transcribe_stream_begin: the dispatcher copies these strings into
 * session-owned storage at begin, so the caller may free its params —
 * including the strings — the moment begin returns, even though the stream
 * keeps running. Bindings rely on this: ctypes/FFI-managed string buffers
 * die when the begin wrapper returns.
 *
 * keep_special_tags: keep special vocabulary tags (e.g. <|...|>) in the
 *                     returned text fields. Default (false) strips them
 *                     for clean transcripts; set true to keep the tags
 *                     inline. Honored by the families whose models emit
 *                     such tags (canary, parakeet, sensevoice); parakeet
 *                     additionally includes/excludes the tag tokens in
 *                     its public token rows. Most callers should prefer
 *                     transcribe_raw_text(), which returns the pre-
 *                     cleanup decode for EVERY family without giving up
 *                     the clean transcribe_full_text.
 *
 * family:      optional family-specific extension. NULL selects family
 *              defaults. The pointed-to object is caller-owned; the
 *              library copies any values it needs out of it before
 *              transcribe_run returns, so the caller may free the
 *              extension storage immediately after the call. Each
 *              family declares its typed extension struct in
 *              include/transcribe/<family>.h with `struct transcribe_ext
 *              ext` as field 0. Use transcribe_model_accepts_ext_kind
 *              to probe whether the loaded model accepts a given kind
 *              before pointing `family` at it.
 */
struct transcribe_run_params {
    uint64_t struct_size;

    transcribe_task               task;
    transcribe_timestamp_kind     timestamps;
    enum transcribe_pnc_mode      pnc;
    enum transcribe_itn_mode      itn;
    enum transcribe_diarize_mode  diarize;
    const char *                  language;
    const char *                  target_language;
    bool                          keep_special_tags;
    const struct transcribe_ext * family;

    /*
     * spec_k_drafts: n-gram-lookup speculative-decode draft length for the
     *   offline autoregressive decode step. Family-portable strategy knob;
     *   the family decides how K maps to its internal verify graph.
     *
     *   Convention:
     *     -1: family default (each family picks its tuned K).
     *      0: spec decoding explicitly disabled — standard 1-token-per-step
     *         autoregression. Use this for byte-equal reproduction of
     *         pre-spec behavior or when measuring baseline performance.
     *     >0: draft K tokens per verify pass. Practical range is 1..8;
     *         optimal K is hardware-dependent (compute-bound hardware
     *         prefers small K, bandwidth-bound prefers larger K — see
     *         docs/models/<family>.md for per-family guidance).
     *
     *   Families gate this via transcribe_capabilities::supports_spec_decode.
     *   Setting spec_k_drafts != -1 on a family with
     *   supports_spec_decode == false is silently ignored (the run proceeds
     *   as ordinary autoregression). Probe the capability bit if you want
     *   to know whether the field will take effect.
     */
    int32_t spec_k_drafts;
};

TRANSCRIBE_API void transcribe_run_params_init(struct transcribe_run_params * params);

/* ----------------------------------------------------------------------- */
/* Capabilities                                                            */
/* ----------------------------------------------------------------------- */

/*
 * Capabilities are semantic model properties read from GGUF KV (with
 * architecture defaults filling in absent keys). They are immutable
 * after a successful transcribe_model_load_file() and never change
 * based on backend fallback.
 *
 * languages is an array of n_languages NUL-terminated short codes,
 * owned by the model. The pointer is valid until transcribe_model_free.
 *
 * max_timestamp_kind is the finest granularity the family's run()
 * handler can produce. NONE means the model emits text but no
 * alignment data; SEGMENT / WORD / TOKEN expose progressively finer
 * alignment. The dispatcher rejects any transcribe_run request
 * whose timestamps field is finer than this value. AUTO is not
 * validated against this field: the dispatcher passes AUTO through
 * unconditionally and the per-family run() handler resolves AUTO
 * to its own max_timestamp_kind when it assembles the result.
 */
/*
 * Capabilities are split by character, not by size:
 *
 *   - Values and bools-that-gate-allied-data live on
 *     `transcribe_capabilities`. The struct holds data (sample rate,
 *     language list, max timestamp kind, streaming hints) plus the
 *     three "gate" bools whose meaning is inseparable from neighboring
 *     fields (supports_streaming gates the streaming hint trio;
 *     supports_translate gates the task enum and target_language
 *     semantics on transcribe_run_params; supports_language_detect gates
 *     the languages array and autodetect mode).
 *
 *   - Unallied behavioral toggles (initial prompt, temperature
 *     fallback, long-form chunking, cancellation, PNC, ITN, and
 *     anything similar in the future) live behind the
 *     transcribe_model_supports() probe, keyed by transcribe_feature.
 *     A new feature is +1 enum value with no struct_size churn.
 */
struct transcribe_capabilities {
    uint64_t struct_size;

    int32_t                   native_sample_rate;
    int                       n_languages;
    const char * const *      languages;
    transcribe_timestamp_kind max_timestamp_kind;

    /*
     * Gate bools with allied struct data. Kept here so reading the
     * struct gives a coherent picture of "what tasks does this model
     * support and what data does it expose for them."
     *
     *   supports_language_detect: gates the languages[] array's role.
     *     False means the autodetect path is unavailable; the listed
     *     languages (if any) are still the model's supported set when
     *     the caller passes a hint via transcribe_run_params::language.
     *
     *   supports_translate: gates TRANSCRIBE_TASK_TRANSLATE on
     *     transcribe_run_params, plus the meaning of
     *     transcribe_run_params::target_language. Hard-error gate; the
     *     dispatcher rejects TRANSLATE against models with
     *     supports_translate == false.
     *
     *   supports_streaming: gates the streaming entry points
     *     (transcribe_stream_*). Hard-error gate at
     *     transcribe_stream_begin. Streaming configuration and any
     *     latency hints are family-specific and live on the family
     *     stream extension, not on this struct.
     */
    bool supports_language_detect;
    bool supports_translate;
    bool supports_streaming;

    /*
     * supports_spec_decode: gates transcribe_run_params::spec_k_drafts.
     *   True means the family's offline (transcribe_run / transcribe_run_batch)
     *   path implements n-gram-lookup speculative decoding. A non-zero
     *   spec_k_drafts on a model with supports_spec_decode == false is
     *   silently ignored — the run proceeds as ordinary autoregression. This
     *   is a soft gate (no error) because spec is purely a performance
     *   strategy; callers can probe this bit if they want to know whether
     *   passing K will actually do anything.
     */
    bool supports_spec_decode;

    /*
     * Streaming timing hints intentionally do NOT live here. Streaming
     * configuration is irreducibly family-specific (cache-aware
     * right-context frames, buffered (left, chunk, right) tuples,
     * autoregressive decode throttles), and any single generic number
     * is a lossy projection of a family menu whose authoritative,
     * selectable form lives in the family stream extension
     * (include/transcribe/<family>.h) and the family doc
     * (docs/models/<variant>.md). supports_streaming above is the
     * generic gate ("can this model stream?"); "how, and with what
     * latency tradeoffs?" is answered by the family extension. If a real
     * consumer ever needs a generic latency menu, the right shape is a
     * dedicated streaming-preset enumeration query, not flat fields here.
     */

    /*
     * max_audio_ms: the longest audio this model can process in one
     * transcribe_run, in milliseconds of 16 kHz mono input. This is the
     * single number a caller checks to size input before calling.
     *
     *   0  = no practical limit. The family chunks long audio internally
     *        (whisper), is otherwise unbounded by sequence length
     *        (parakeet), or has only an impractically large absolute safety
     *        cap (voxtral_realtime). A family-specific absolute cap may still
     *        reject input past the model's true wall; see docs/input-limits.md.
     *   >0 = a usable ceiling. Its meaning depends on the family's limit
     *        kind, but the number is honest in both cases:
     *          - hard-context-cap families: the bound the library ENFORCES
     *            up front. It is derived from the decoder context window
     *            minus the fixed prompt and a minimum generation reserve.
     *            A longer clip returns TRANSCRIBE_ERR_INPUT_TOO_LONG before
     *            the decode. This is a MODEL-level value reported at the
     *            model's default context (transcribe_session_params::n_ctx
     *            == 0); a session that lowers n_ctx lowers the effective
     *            limit below this number, but this field is not re-derived
     *            per session.
     *          - soft-window families (gigaam, sensevoice, medasr): the
     *            advisory window the model was trained on. Longer input is
     *            accepted but emits a WARN and may be less accurate; it is
     *            not rejected.
     *
     * Zero-init via transcribe_capabilities_init() yields 0; a family that
     * does not set it is therefore reported as unbounded. See
     * docs/input-limits.md for the full contract.
     */
    int64_t max_audio_ms;

    /*
     * translate_target_languages / n_translate_target_languages: the set
     * of target language codes accepted for TRANSCRIBE_TASK_TRANSLATE —
     * the target-side twin of `languages` (which is the valid set for the
     * transcribe-side `language` hint). supports_translate gates whether
     * translation runs at all; this list narrows WHICH targets are valid.
     * A TRANSLATE run whose run_params::target_language is non-NULL and
     * absent from a non-empty list is rejected with
     * TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE before any decode.
     *
     * n == 0 / NULL is "not advertised" — an information gap, not a claim
     * of zero targets — exactly the convention an empty `languages` uses.
     * GGUFs predating stt.translation.target_languages report 0 here even
     * when supports_translate is true; the gate is then inert and any
     * family-level target/pair checks (e.g. canary's pivot pairs) still
     * apply on top.
     */
    int                  n_translate_target_languages;
    const char * const * translate_target_languages;
};

TRANSCRIBE_API void transcribe_capabilities_init(struct transcribe_capabilities * out);

/*
 * Read model capabilities into caller-owned storage. The caller
 * initializes *out_caps via transcribe_capabilities_init() (zero-fill);
 * the library writes only the fields that fit and leaves tail bytes
 * beyond the caller's struct_size untouched.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG     model or out_caps is NULL.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE out_caps->struct_size is 0 or
 *                                  smaller than the library's minimum.
 *
 * Pointer fields written by the library (e.g. `languages`) point at
 * model-owned storage and remain valid until transcribe_model_free().
 * The output struct itself is caller-owned.
 */
TRANSCRIBE_API transcribe_status transcribe_model_get_capabilities(const struct transcribe_model *  model,
                                                                   struct transcribe_capabilities * out_caps);

/*
 * Per-feature capability probe. Pure yes/no advisories that don't
 * gate any allied struct data live here instead of as boolean fields
 * on transcribe_capabilities. Adding a feature is +1 enum value with
 * no struct_size advance, which makes the surface easy to grow
 * without ABI churn.
 *
 * Feature meanings:
 *
 *   INITIAL_PROMPT       The model accepts a free-text or token
 *                        prompt to bias decoding. Today: whisper
 *                        only; reached via transcribe_whisper_run_ext.
 *
 *   TEMPERATURE_FALLBACK The model runs a multi-tier temperature loop
 *                        with metric-driven fallback. Today: whisper.
 *
 *   LONG_FORM            The model exposes a long-form chunker that
 *                        handles audio longer than its native window
 *                        in a single transcribe_run call. Today:
 *                        whisper.
 *
 *   CANCELLATION         transcribe_set_abort_callback fires between
 *                        chunks / decode steps and the family hook
 *                        honors it.
 *
 *   PNC                  The model exposes a runtime toggle for
 *                        punctuation + capitalization via
 *                        transcribe_run_params::pnc. False does NOT mean
 *                        the model produces no PNC — only that the
 *                        caller cannot control whether PNC appears.
 *                        Non-DEFAULT pnc against a model where this
 *                        returns false emits a WARN and proceeds.
 *
 *   ITN                  The model exposes a runtime toggle for
 *                        inverse text normalization via
 *                        transcribe_run_params::itn. Same "can-control
 *                        vs does-emit" distinction as PNC; non-DEFAULT
 *                        itn against an unsupported model warns.
 *
 *   DIARIZATION          The model emits speaker-attributed output and
 *                        the runtime exposes a toggle via
 *                        transcribe_run_params::diarize. When true,
 *                        segment rows carry speaker_id and the
 *                        speaker-segment accessors are populated (mode
 *                        permitting). False does NOT mean multi-speaker
 *                        audio is mis-transcribed — only that speaker
 *                        attribution is unavailable. Non-DEFAULT diarize
 *                        against a model where this returns false emits
 *                        a WARN and proceeds.
 *
 * Returns false on NULL model or unknown feature enum.
 */
typedef enum {
    TRANSCRIBE_FEATURE_INITIAL_PROMPT       = 0,
    TRANSCRIBE_FEATURE_TEMPERATURE_FALLBACK = 1,
    TRANSCRIBE_FEATURE_LONG_FORM            = 2,
    TRANSCRIBE_FEATURE_CANCELLATION         = 3,
    TRANSCRIBE_FEATURE_PNC                  = 4,
    TRANSCRIBE_FEATURE_ITN                  = 5,
    TRANSCRIBE_FEATURE_DIARIZATION          = 6,
} transcribe_feature;

TRANSCRIBE_API bool transcribe_model_supports(const struct transcribe_model * model, transcribe_feature feature);

/*
 * Stable identifier strings read from GGUF and runtime state.
 *
 *   transcribe_model_arch_string(): the general.architecture key,
 *     e.g. "parakeet". Immutable for the lifetime of the model.
 *
 *   transcribe_model_variant_string(): the stt.variant key, e.g.
 *     "tdt-0.6b-v2". Immutable for the lifetime of the model. May be
 *     an empty string if the underlying GGUF did not carry a variant
 *     and the architecture handler had no default to substitute.
 *
 *   transcribe_model_backend(): the runtime backend currently bound
 *     to this model, e.g. "cpu", "metal", "vulkan", "cuda", "ROCm". This is
 *     the mechanism for detecting CPU fallback when GPU was requested.
 *
 *     Returns an empty string when no runtime backend is currently
 *     bound to the model. This covers both (a) model == NULL, the
 *     safe-sentinel pattern used throughout this header, and (b) a
 *     model that has been loaded but is not yet bound to a runtime
 *     backend - for example, a model whose on-disk metadata and
 *     vocabulary have been parsed but whose compute graph has not
 *     been materialized on any backend yet. Callers can distinguish
 *     the two by null-checking the model pointer.
 *
 *     Backend is a runtime fact, reported separately from the
 *     model's semantic capabilities (see
 *     transcribe_model_get_capabilities), which never change based on
 *     backend state.
 *
 * All three return statically allocated or model-owned strings; the
 * caller must not free them and they remain valid until the model is
 * freed.
 */
TRANSCRIBE_API const char * transcribe_model_arch_string(const struct transcribe_model * model);
TRANSCRIBE_API const char * transcribe_model_variant_string(const struct transcribe_model * model);
TRANSCRIBE_API const char * transcribe_model_backend(const struct transcribe_model * model);

/*
 * Generic GGUF string-metadata getter, modeled on llama_model_meta_val_str.
 * Looks up a scalar-string metadata key written by the converter and returns
 * its value; this is how human-facing identity is read rather than a typed
 * accessor per field. Common keys:
 *
 *   "general.name"         friendly label, e.g. "Whisper Large v3"
 *   "general.license"      SPDX expression, e.g. "apache-2.0" (or "other")
 *   "general.license.name" human-friendly license name
 *   "general.license.link" URL to the license text
 *   "general.author", "general.organization", "general.repo_url", ...
 *
 * Returns a model-owned string (valid until the model is freed; do not free
 * it) or an empty string "" when model is NULL, key is NULL, or the key is
 * absent. Only scalar-string KVs are exposed (numeric hyperparameters and
 * arrays such as the token list are not). There is no fallback to the variant
 * slug — for that, use transcribe_model_variant_string().
 */
TRANSCRIBE_API const char * transcribe_model_meta_val_str(const struct transcribe_model * model, const char * key);

/* ----------------------------------------------------------------------- */
/* Lifecycle                                                               */
/* ----------------------------------------------------------------------- */

/*
 * Load a GGUF model from disk.
 *
 * path is UTF-8, like every path crossing this API; on Windows it is
 * converted to a wide path internally, so non-ASCII paths load
 * correctly regardless of the process ANSI code page.
 *
 * params may be NULL for all library defaults. To customize, initialize
 * a struct with transcribe_model_load_params_init() and set fields. A
 * struct with struct_size == 0 (including `{0}`) is rejected with
 * BAD_STRUCT_SIZE — defaults come from NULL, never from an uninitialized
 * struct. See the "Params" section at the top of this header.
 *
 * On success, *out_model is set and the caller owns it. On failure,
 * *out_model is set to NULL and a non-OK status is returned.
 */
TRANSCRIBE_API transcribe_status transcribe_model_load_file(const char *                                path,
                                                            const struct transcribe_model_load_params * params,
                                                            struct transcribe_model **                  out_model);

/*
 * Free a model. Only valid after every session derived from this model
 * has been freed and no thread is still using the model. Passing NULL
 * is a no-op.
 */
TRANSCRIBE_API void transcribe_model_free(struct transcribe_model * model);

/*
 * Initialize a transcription session bound to a loaded model. Multiple
 * sessions may be created from the same model in parallel; each session
 * is single-threaded. The model must outlive every session derived from
 * it (the session borrows, but does not own, the model).
 *
 * params may be NULL for library defaults, or initialize a struct with
 * transcribe_session_params_init(). See transcribe_model_load_file.
 */
TRANSCRIBE_API transcribe_status transcribe_session_init(struct transcribe_model *                model,
                                                         const struct transcribe_session_params * params,
                                                         struct transcribe_session **             out_session);

/*
 * Free a session.
 *
 * If the session was created via transcribe_open it owns the model it
 * loaded; transcribe_session_free destroys the session and then frees
 * the model in the same call. Sessions created via the two-step
 * transcribe_session_init path borrow their model; this function leaves
 * that model alone.
 *
 * The caller does not need to know which path created the session — both
 * are handled correctly. Passing NULL is a no-op.
 */
TRANSCRIBE_API void transcribe_session_free(struct transcribe_session * session);

/*
 * Convenience: load a model and open a session against it in one call,
 * for the common "I just want to transcribe one stream" case. Bundles
 * transcribe_model_load_file + transcribe_session_init.
 *
 * The returned session OWNS the model it loaded; transcribe_session_free
 * (or its alias transcribe_close) frees both. To share one model across
 * multiple sessions (e.g. one per thread), use the two-step
 * transcribe_model_load_file + transcribe_session_init API instead —
 * that is the only reason to reach past this function.
 *
 * load_params and session_params may each be NULL for library defaults.
 *
 * On success *out_session is set and the caller owns it. On failure
 * *out_session is set to NULL and a non-OK status is returned (the same
 * status transcribe_model_load_file / transcribe_session_init would
 * return; no partial state leaks).
 */
TRANSCRIBE_API transcribe_status transcribe_open(const char *                                path,
                                                 const struct transcribe_model_load_params * load_params,
                                                 const struct transcribe_session_params *    session_params,
                                                 struct transcribe_session **                out_session);

/*
 * Deprecated alias for transcribe_session_free. Both honor owns_model and
 * free the owned model when present, so a caller may use either with any
 * session. New code should call transcribe_session_free directly. Passing
 * NULL is a no-op.
 */
TRANSCRIBE_API void transcribe_close(struct transcribe_session * session);

/*
 * Borrow the model bound to a session. The returned pointer is owned by
 * the library — do not free it — and remains valid for the session's
 * lifetime. This lets convenience-path (transcribe_open) callers reach
 * model introspection such as transcribe_model_backend() and the
 * capabilities query without holding a separate model handle. Returns
 * NULL if session is NULL.
 */
TRANSCRIBE_API const struct transcribe_model * transcribe_get_model(const struct transcribe_session * session);

/* ----------------------------------------------------------------------- */
/* Run                                                                     */
/* ----------------------------------------------------------------------- */

/*
 * Run one batch transcription.
 *
 * pcm:        mono float32 PCM samples in [-1.0, 1.0] at 16 kHz.
 * n_samples:  number of samples in pcm. Must be strictly positive;
 *             a non-positive count returns TRANSCRIBE_ERR_INVALID_ARG
 *             (same rule as transcribe_stream_feed).
 * params:     run params, or NULL for defaults.
 *
 * v1 supports only 16 kHz mono float32 PCM and does not link a
 * resampler; the caller is responsible for resampling external audio
 * before calling this function.
 *
 * On success, results are populated on the session and may be read via
 * the accessors below. Calling transcribe_run() again replaces the
 * previous result on the same session.
 *
 * TRANSCRIBE_ERR_BACKEND from this function (and batch/streaming run calls)
 * is recoverable in-process: free the session and model, reload with
 * transcribe_model_load_params::backend = TRANSCRIBE_BACKEND_CPU, and retry.
 */
TRANSCRIBE_API transcribe_status transcribe_run(struct transcribe_session *          session,
                                                const float *                        pcm,
                                                int                                  n_samples,
                                                const struct transcribe_run_params * params);

/* ----------------------------------------------------------------------- */
/* Batch run (offline)                                                     */
/* ----------------------------------------------------------------------- */

/*
 * Run N offline transcriptions in one call.
 *
 * This is a throughput entry point: families with a batched compute path
 * (today: see the per-family docs) process all N utterances in a single
 * device dispatch, which can roughly double throughput on a GPU that a
 * single utterance underutilizes. Families without a batched path still
 * work — the library falls back to running each utterance in turn — so
 * every model accepts this call; only the speedup is family-dependent.
 *
 * This is the offline counterpart to transcribe_run; it is NOT a streaming
 * primitive. For a well-formed single utterance it is equivalent to
 * transcribe_run with n == 1, but the two are NOT exactly equivalent for
 * malformed input: transcribe_run(NULL pcm / n_samples <= 0) fails top-level
 * with TRANSCRIBE_ERR_INVALID_ARG and leaves the previous result untouched,
 * whereas a malformed single utterance inside a batch (with valid top-level
 * args) is a per-utterance failure — the call returns TRANSCRIBE_OK,
 * transcribe_batch_status(session, 0) carries the INVALID_ARG, and the
 * previous result has already been replaced by the (failed) batch.
 *
 *   session:    a session in a non-ACTIVE-stream state. As with
 *               transcribe_run, a session is single-threaded: do not call
 *               this concurrently with any other call on the same session.
 *   pcm:        array of n pointers, each to mono float32 PCM in [-1, 1]
 *               at 16 kHz. pcm[i] is the i-th utterance.
 *   n_samples:  array of n sample counts; n_samples[i] is the length of
 *               pcm[i].
 *   n:          number of utterances. Must be strictly positive.
 *   params:     run params shared by every utterance in the batch, or
 *               NULL for defaults. v1 applies one params (task, language
 *               hint, timestamps, ...) to the whole batch; per-utterance
 *               params is a future extension.
 *
 * Variable-length utterances are handled by the library: a batched family
 * pads the batch to its longest utterance internally and masks the padding.
 * Grouping utterances of similar length per call ("bucketing") minimizes
 * wasted compute but is not required.
 *
 * Result model: results are read back with the transcribe_batch_* accessors
 * below, indexed by utterance. transcribe_batch_n_results() returns the
 * count. The legacy single-result accessors (transcribe_full_text, etc.)
 * alias utterance 0 after a batch run. A subsequent transcribe_run clears
 * the batch results.
 *
 * Return value is the WHOLE-BATCH status:
 *   TRANSCRIBE_OK                  the dispatch ran. Per-utterance success
 *                                  or failure is read via
 *                                  transcribe_batch_status(session, i); a
 *                                  malformed single utterance (pcm[i] NULL
 *                                  or n_samples[i] <= 0) fails only that
 *                                  utterance and is reported there.
 *   TRANSCRIBE_ERR_INVALID_ARG     session / pcm / n_samples NULL, n <= 0,
 *                                  or the session is in an ACTIVE stream.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE params->struct_size below the minimum.
 *   TRANSCRIBE_ERR_NOT_IMPLEMENTED the model has no run path at all.
 *   ... plus the same shared-param rejections as transcribe_run
 *       (TRANSCRIBE_ERR_UNSUPPORTED_TASK / _TIMESTAMPS / _LANGUAGE), which
 *       apply to the whole batch and are reported before any utterance
 *       runs (the previous result snapshot is preserved on these).
 *   TRANSCRIBE_ERR_ABORTED         the abort callback fired. Completed
 *                                  utterances are retained with their real
 *                                  status; any utterance that does NOT
 *                                  complete reports TRANSCRIBE_ERR_ABORTED,
 *                                  whether it was aborted mid-decode or was
 *                                  never retained as a completed result by the
 *                                  batch path. Dispatcher-synthesized slots
 *                                  specifically mean "did not complete because
 *                                  the batch was aborted" — not that the
 *                                  utterance itself reached an abort
 *                                  checkpoint. As on a successful return,
 *                                  transcribe_batch_n_results is n (one slot
 *                                  per input utterance).
 */
TRANSCRIBE_API transcribe_status transcribe_run_batch(struct transcribe_session *          session,
                                                      const float * const *                pcm,
                                                      const int *                          n_samples,
                                                      int                                  n,
                                                      const struct transcribe_run_params * params);

/* ----------------------------------------------------------------------- */
/* Cancellation                                                            */
/* ----------------------------------------------------------------------- */

/*
 * Abort callback. Polled between chunks AND between decode steps (after
 * each token). Abort latency is therefore one decode step — typically
 * 10-50 ms on CPU, sub-ms on Metal. Mid-encoder abort is not supported
 * today (would require ggml hooks that do not exist).
 *
 * Returning true aborts the in-flight run: transcribe_run returns
 * TRANSCRIBE_ERR_ABORTED and result accessors (transcribe_full_text,
 * transcribe_n_segments, etc.) expose the partial content from chunks
 * that completed before abort. Use transcribe_was_aborted(session) to
 * distinguish partial-from-abort from complete.
 *
 * The next successful transcribe_run() replaces the result atomically,
 * as today.
 *
 * The callback fires on the run thread. If the caller wants to trigger
 * abort from another thread, the common pattern is to store an
 * std::atomic<bool> inside user_data and have the callback load it.
 */
typedef bool (*transcribe_abort_callback)(void * user_data);

/*
 * Install or clear the abort callback for a session. Passing cb=NULL
 * clears any previously installed callback. Safe to call before the
 * first transcribe_run. No-op if session is NULL.
 *
 * Not thread-safe with respect to an in-flight transcribe_run on the
 * same session — the session is single-threaded-at-a-time per the
 * threading contract above. Callers that need to trigger abort from
 * another thread should do so by flipping state inside the callback's
 * user_data, not by swapping the callback itself.
 */
TRANSCRIBE_API void transcribe_set_abort_callback(struct transcribe_session * session,
                                                  transcribe_abort_callback   cb,
                                                  void *                      user_data);

/*
 * True if the most recent transcribe_run was aborted by the installed
 * callback returning true. Reset to false at the top of each
 * transcribe_run. Returns false if session is NULL.
 */
TRANSCRIBE_API bool transcribe_was_aborted(const struct transcribe_session * session);

/*
 * Supplemental flag for output truncation. True if the most recent decode
 * stopped at the model's context / generation cap before end-of-stream,
 * leaving the transcript incomplete. The partial transcript is preserved
 * and readable through the normal result accessors. Reset to false at the
 * start of each new decode — transcribe_run, transcribe_run_batch, and
 * transcribe_stream_begin (the same lifecycle as transcribe_was_aborted).
 * Returns false if session is NULL.
 *
 * Two paths set it, and they differ in whether a status also reports it:
 *
 *   - Offline (transcribe_run / transcribe_run_batch): the flag is true
 *     exactly when the run returned TRANSCRIBE_ERR_OUTPUT_TRUNCATED (or, in
 *     a batch, when a per-utterance status is OUTPUT_TRUNCATED), so the run
 *     status is the authoritative signal and this accessor is a convenience
 *     for a caller that has lost it.
 *
 *   - Streaming (transcribe_stream_*): OUTPUT_TRUNCATED is NOT used. An
 *     active stream has its own terminal-state machine, and stream_feed /
 *     stream_finalize return the status of that step, not a verdict on the
 *     whole transcript — so they return TRANSCRIBE_OK even when the stream
 *     reached its absolute position cap (forcing the stream to FAILED would
 *     discard the committed text the caller has been consuming). There, this
 *     flag is the ONLY signal of truncation: a streaming caller must check
 *     it after finalize.
 *
 * Distinct from the "couldn't start" rejection: input that cannot fit at
 * all is rejected before the decode with TRANSCRIBE_ERR_INPUT_TOO_LONG;
 * output truncation cannot be predicted from input length (it depends on
 * how long the transcript runs), so it is reported after the fact. A WARN
 * is also logged. See docs/input-limits.md.
 */
TRANSCRIBE_API bool transcribe_was_truncated(const struct transcribe_session * session);

/* ----------------------------------------------------------------------- */
/* Session limits                                                          */
/* ----------------------------------------------------------------------- */

/*
 * Effective limits for a specific session. This is the session-level companion
 * to the model-level transcribe_capabilities::max_audio_ms: for families whose
 * audio consumes decoder context, it accounts for the session's
 * transcribe_session_params::n_ctx cap. For encoder-bound families, n_ctx may
 * lower decoder KV memory without lowering the input-audio bound.
 *
 *   effective_n_ctx:        the decoder context cap, in tokens, in force for
 *                           this session: model_max_ctx when n_ctx == 0,
 *                           else min(n_ctx, model_max_ctx). 0 means the
 *                           family has no context cap (chunked / unbounded).
 *
 *   effective_max_audio_ms: the longest audio this session accepts before
 *                           TRANSCRIBE_ERR_INPUT_TOO_LONG, in milliseconds
 *                           of 16 kHz mono input. For decoder-context-bound
 *                           families this is derived from effective_n_ctx; for
 *                           encoder-bound families it remains the encoder
 *                           input bound even when n_ctx lowers decoder memory.
 *                           0 means no practical limit. Advisory /
 *                           representative in the same way as
 *                           transcribe_capabilities::max_audio_ms (it assumes
 *                           a representative prompt; the exact per-call bound
 *                           shifts slightly with the prompt).
 *
 *   max_kv_bytes:           the worst-case decoder KV-cache bytes for ONE
 *                           utterance at effective_n_ctx, for memory
 *                           budgeting. It is exact for the session's kv_type
 *                           (the families resolve AUTO and F16 to f16 KV and
 *                           use f32 only for an explicit F32 request, so this
 *                           reflects 2 or 4 bytes/element accordingly). It is
 *                           the ceiling, not the amount allocated for any
 *                           single run (the cache grows to fit each input).
 *                           One scaling caveat the caller applies itself: it
 *                           is per-utterance, so transcribe_run_batch
 *                           allocates roughly batch_size x this. 0 if the
 *                           family has no decoder KV cache.
 */
struct transcribe_session_limits {
    uint64_t struct_size;
    int32_t  effective_n_ctx;
    int64_t  effective_max_audio_ms;
    int64_t  max_kv_bytes;
};

TRANSCRIBE_API void transcribe_session_limits_init(struct transcribe_session_limits * out);

/*
 * Read the effective limits for a session into caller-owned storage. The
 * caller initializes *out via transcribe_session_limits_init(); the library
 * writes only the prefix that fits in both the caller's struct_size and the
 * library's view.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG     session or out is NULL.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE out->struct_size is 0 or below the
 *                                  library minimum.
 *   TRANSCRIBE_OK                  otherwise.
 */
TRANSCRIBE_API transcribe_status transcribe_session_get_limits(const struct transcribe_session *  session,
                                                               struct transcribe_session_limits * out);

/* ----------------------------------------------------------------------- */
/* Streaming                                                               */
/* ----------------------------------------------------------------------- */

/*
 * Streaming is a mode on transcribe_session, not a separate handle. A
 * session is in exactly one of four lifecycle states at any time, and
 * the result accessors (transcribe_full_text, segments, words, tokens)
 * return the current raw model snapshot of the active stream.
 *
 * Text semantics during an active stream:
 *
 *   full_text      = raw current model hypothesis
 *   committed_text = append-only UI/input prefix
 *   tentative_text = volatile raw suffix after raw_tentative_start_bytes
 *   display_text   = committed_text + tentative_text
 *
 * The committed-count accessors (transcribe_stream_n_committed_*)
 * remain as low-level raw row boundary hints for consumers that inspect
 * tokens/words/segments. They are monotonic commitment high-water marks
 * and may exceed the current raw row counts after the model revises or
 * shrinks its latest hypothesis; clamp them to transcribe_n_* before
 * indexing current raw rows. UI consumers should prefer
 * transcribe_stream_get_text(), because committed_text is independent from
 * the raw hypothesis and may not be a prefix of full_text after the model
 * revises already-committed text.
 *
 * Streaming timestamp note: unlike transcribe_run(), an active stream
 * may keep token rows populated (transcribe_n_tokens > 0) even when the
 * run was started with timestamps == TRANSCRIBE_TIMESTAMPS_NONE, because
 * the committed/tentative progress boundary is token-indexed and the
 * incremental-commit accessors need the token rows to exist. The
 * timestamps request is validated against the model's capabilities at
 * transcribe_stream_begin (a request finer than the model's max is
 * rejected there), but it is NOT an instruction to elide structural
 * token rows from the streaming result. Whether those rows carry real
 * per-token timestamps is a separate question answered by the family:
 * a family with no token-level alignment (e.g. moonshine_streaming) can
 * honestly populate token rows for the progress boundary while
 * transcribe_returned_timestamp_kind() still reports NONE. Always use
 * transcribe_returned_timestamp_kind() to discover the finest timestamp
 * fields actually populated; do not infer it from transcribe_n_tokens().
 *
 * Cancellation reuses the existing session abort callback. The
 * callback is polled at chunk boundaries and decode-step boundaries
 * during feed/finalize; on cancellation the active call returns
 * TRANSCRIBE_ERR_ABORTED, partial results remain readable, and the
 * stream transitions to FAILED (transcribe_was_aborted distinguishes
 * caller cancellation from other terminal statuses).
 *
 * Multiple concurrent streams: use the existing model/session
 * threading model. Create multiple contexts from one loaded model,
 * then run at most one stream per session. The model is shared and
 * read-only; each session owns its own stream state.
 *
 * Streaming params (transcribe_stream_params) carry an optional,
 * kind-tagged family extension pointer. Family-specific extension
 * structs live in include/transcribe/<family>.h and are reached
 * intent-first: a caller that wants to set a parakeet streaming knob
 * probes transcribe_model_accepts_ext_kind for the parakeet stream
 * kind on TRANSCRIBE_EXT_SLOT_STREAM and, if accepted, points
 * transcribe_stream_params::family at an extension struct initialized
 * via that family's transcribe_<family>_<name>_ext_init() function.
 */

enum transcribe_stream_state {
    TRANSCRIBE_STREAM_IDLE     = 0,
    TRANSCRIBE_STREAM_ACTIVE   = 1,
    TRANSCRIBE_STREAM_FINISHED = 2,
    TRANSCRIBE_STREAM_FAILED   = 3,
};

/*
 * Generic public commitment policy. Families still own model-specific
 * streaming mechanics (chunk sizes, cache windows, decoder throttles);
 * this enum controls when the session's UI-facing committed_text grows.
 *
 * AUTO:        Use the dispatcher-selected stable-prefix implementation
 *              for the model family. Current streaming families use a
 *              family-provided boundary; future families fall back to
 *              the generic text-agreement implementation until they are
 *              assigned an explicit default.
 * ON_FINALIZE: During feed calls committed_text stays empty and the raw
 *              hypothesis is exposed as tentative_text. Finalize commits
 *              the final raw text because no earlier bytes were committed.
 * STABLE_PREFIX:
 *              Commit only a raw prefix accepted by the selected stable-
 *              prefix implementation. Known families may use token-id or
 *              native commit boundaries; others use generic text
 *              agreement. committed_text is append-only and finalize
 *              never rewrites previously committed bytes.
 */
typedef enum {
    TRANSCRIBE_STREAM_COMMIT_AUTO          = 0,
    TRANSCRIBE_STREAM_COMMIT_ON_FINALIZE   = 1,
    TRANSCRIBE_STREAM_COMMIT_STABLE_PREFIX = 2,
} transcribe_stream_commit_policy;

/*
 * Streaming run params.
 *
 * struct_size:                   sizeof(*this) captured by the caller.
 *                                Initialized via transcribe_stream_params_init().
 *
 * family:                        Optional family-specific extension. NULL
 *                                selects family defaults. The pointed-to
 *                                object is caller-owned and the library
 *                                copies any values it needs out of it
 *                                before transcribe_stream_begin returns;
 *                                the caller may free the extension storage
 *                                immediately after begin. Each family
 *                                declares its typed extension struct in
 *                                include/transcribe/<family>.h with
 *                                `struct transcribe_ext ext` as field 0.
 *                                Use transcribe_model_accepts_ext_kind to
 *                                probe whether the loaded model accepts a
 *                                given kind before pointing `family` at it.
 *
 * commit_policy:                 Generic UI-facing commitment policy.
 *                                Zero-init/default is AUTO.
 *
 * stable_prefix_agreement_n:      For STABLE_PREFIX and AUTO implementations
 *                                based on repeated hypotheses, number of
 *                                consecutive text or token-id hypotheses that
 *                                must agree on a prefix before it is appended
 *                                to committed_text. Families with native
 *                                irreversible commit boundaries may ignore
 *                                this field. 0 selects the library default
 *                                (currently 3): a prefix must reproduce
 *                                across three consecutive hypotheses before
 *                                it is committed.
 */
struct transcribe_stream_params {
    uint64_t                        struct_size;
    const struct transcribe_ext *   family;
    transcribe_stream_commit_policy commit_policy;
    uint32_t                        stable_prefix_agreement_n;
};

TRANSCRIBE_API void transcribe_stream_params_init(struct transcribe_stream_params * params);

/*
 * Optional per-call change metadata returned by feed/finalize.
 *
 *   struct_size        Caller's sizeof(*this). Initialized via
 *                      transcribe_stream_update_init() (zero-fill).
 *   result_changed     True when any observable property of the
 *                      snapshot changed on this call: the text vectors
 *                      (tokens / words / segments / full_text), the
 *                      committed/tentative text views, committed raw
 *                      row boundary hints, or a lifecycle transition
 *                      that semantically closes the stream.
 *                      Inspect the accessors after the call to read
 *                      the new snapshot. Treat result_changed as a
 *                      strict subset of `revision changed`: if
 *                      `revision` advanced you can assume
 *                      `result_changed` is also true.
 *   is_final           True only on the finalize call's update. Set
 *                      by the dispatcher after the family hook
 *                      returns; family hooks cannot override.
 *   revision           Monotonic snapshot counter. Advances whenever
 *                      any observable property of the result changes
 *                      under the rules described for `result_changed`,
 *                      including finalize cases where text is unchanged
 *                      but the committed/tentative stream view or
 *                      lifecycle moved. Mirrors
 *                      transcribe_stream_revision(session) after the
 *                      call returns. UI consumers should diff against
 *                      the previous value rather than treating each
 *                      bump as "text changed."
 *   input_received_ms  Total audio received by the stream since begin.
 *   audio_committed_ms Family-reported audio progress / drain hint.
 *                      It is not a byte boundary into committed_text.
 *                      ON_FINALIZE reports 0 during feed calls because
 *                      committed_text is empty until finalize.
 *   buffered_ms        Audio still buffered inside the family's
 *                      streaming state (frontend carry +
 *                      lookahead/right-context requirement). Caller
 *                      may use this as a "drain" hint.
 *   committed_changed True when committed_text changed on this call.
 *                      committed_text is append-only for the life of
 *                      the stream; a true value means bytes were appended
 *                      or, for ON_FINALIZE with no prior committed bytes,
 *                      the final raw text was installed.
 *   tentative_changed True when tentative_text changed on this call.
 *
 * update is nullable. Passing NULL means the caller will inspect the
 * session directly (revision + committed-count accessors). Audio
 * cursor fields are only available via this struct.
 *
 * Zero-means-absent contract: every field on this struct is designed
 * so the zero value encodes "absent / unknown / false / none." This
 * is what makes the transcribe_stream_update_init() zero-fill safe for
 * forward ABI: a new caller paired with an older library reads tail
 * fields the older library never wrote, and those bytes must remain
 * zero.
 */
struct transcribe_stream_update {
    uint64_t struct_size;
    bool     result_changed;
    bool     is_final;
    int32_t  revision;
    int64_t  input_received_ms;
    int64_t  audio_committed_ms;
    int64_t  buffered_ms;
    bool     committed_changed;
    bool     tentative_changed;
};

TRANSCRIBE_API void transcribe_stream_update_init(struct transcribe_stream_update * out);

/*
 * UI-facing stream text snapshot.
 *
 * full_text is the raw current model hypothesis, matching
 * transcribe_full_text(session). During ACTIVE it may rewrite anywhere.
 *
 * committed_text is the API-stable display/input prefix. Once bytes are
 * exposed through committed_text they are never rewritten by later feed or
 * finalize calls on this stream. They remain until begin/reset/free.
 *
 * tentative_text is the volatile raw suffix after raw_tentative_start_bytes.
 * It may be replaced on every feed. UI consumers that want no committed
 * flicker render committed_text + tentative_text; consumers that want the
 * model's current truth render full_text.
 *
 * committed_text is best-effort, not a correctness guarantee. For models
 * that re-attend over a growing audio context (e.g. moonshine_streaming),
 * the raw hypothesis can revise a byte that was already committed. Because
 * committed_text is append-only it is NOT rolled back: when full_text
 * diverges before the committed boundary, committed_text + tentative_text
 * no longer reconstruct full_text, so the committed/tentative seam is
 * transiently incoherent mid-stream. full_text is the authoritative raw
 * hypothesis at all times; committed_text trades that authority for a
 * flicker-free, append-only prefix. Consumers needing exact truth should
 * render full_text. Raising stable_prefix_agreement_n makes a wrong commit
 * less likely at the cost of committing later.
 *
 * On successful stream_finalize, tentative_text is empty. For ON_FINALIZE,
 * committed_text becomes final full_text. For policies that committed bytes
 * before finalize, finalize only appends compatible remaining suffix bytes;
 * if final full_text disagrees with already committed_text, committed_text
 * is not silently rewritten and full_text remains the authoritative raw
 * final transcript.
 *
 * All pointers are borrowed session-owned storage. They remain valid until
 * the next transcribe_stream_feed / transcribe_stream_finalize /
 * transcribe_stream_begin / transcribe_stream_reset / transcribe_run /
 * transcribe_session_free on the same session.
 */
struct transcribe_stream_text {
    uint64_t     struct_size;
    const char * full_text;
    uint64_t     full_text_bytes;
    const char * committed_text;
    uint64_t     committed_text_bytes;
    const char * tentative_text;
    uint64_t     tentative_text_bytes;
    uint64_t     raw_tentative_start_bytes;
};

TRANSCRIBE_API void transcribe_stream_text_init(struct transcribe_stream_text * out);

TRANSCRIBE_API transcribe_status transcribe_stream_get_text(const struct transcribe_session * session,
                                                            struct transcribe_stream_text *   out);

/*
 * Begin a streaming run on session.
 *
 * run_params and stream_params may be NULL for library defaults
 * (transcribe task, no timestamps, no family extension). To customize,
 * initialize via transcribe_run_params_init() / transcribe_stream_params_init()
 * and set fields. The "session==NULL is INVALID_ARG, struct_size==0 is
 * BAD_STRUCT_SIZE" rules from the top-of-header "Params" block apply.
 *
 * On success the session transitions IDLE/FINISHED/FAILED -> ACTIVE
 * and every result-visible field is cleared: text, segments, words,
 * tokens, detected language, stream revision, committed counts,
 * audio cursors, timings, was_aborted, and last stream status. The
 * installed abort callback, the loaded model, and any reusable
 * stream buffers held by the family are preserved.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG       session NULL, session in ACTIVE
 *                                    state, out-of-range enum in
 *                                    run_params, or an extension whose
 *                                    kind is unknown to / not accepted
 *                                    by the loaded model on the
 *                                    TRANSCRIBE_EXT_SLOT_STREAM slot.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE   non-null run_params or stream_params
 *                                    has a struct_size below what this
 *                                    entry point requires (including
 *                                    struct_size == 0), or
 *                                    stream_params->family is too small
 *                                    to contain a transcribe_ext header,
 *                                    or (for a family that implements the
 *                                    preflight, see below) a family
 *                                    extension whose size is below its
 *                                    kind's minimum. All of these are
 *                                    reported before the previous result
 *                                    snapshot is cleared.
 *                                    Use the init functions to avoid.
 *   TRANSCRIBE_ERR_NOT_IMPLEMENTED   Model has no streaming hooks or
 *                                    capabilities advertise
 *                                    supports_streaming == false.
 *                                    All three of stream_begin /
 *                                    stream_feed / stream_finalize must
 *                                    be wired for begin to succeed;
 *                                    stream_reset is optional.
 *   TRANSCRIBE_ERR_UNSUPPORTED_TASK  TRANSCRIBE_TASK_TRANSLATE; v1
 *                                    streaming is transcribe-only.
 *   TRANSCRIBE_ERR_UNSUPPORTED_TIMESTAMPS
 *                                    Requested granularity finer than
 *                                    the model's max_timestamp_kind.
 *   TRANSCRIBE_ERR_UNSUPPORTED_LANGUAGE
 *                                    run_params->language not in the
 *                                    model's declared language list.
 *
 * Failure semantics — three checkpoints, split by whether the previous
 * result snapshot survives:
 *
 *   1. Dispatcher preflight. Top-level argument checks run first:
 *      NULL session, ACTIVE-state rejection, struct_size failures,
 *      out-of-range run_params enums, unknown / unaccepted extension
 *      kind for the STREAM slot, TRANSLATE, granularity finer than the
 *      model's max, and unsupported language. On failure the call
 *      returns without entering ACTIVE and WITHOUT clearing the
 *      previous result snapshot. Lifecycle state is untouched.
 *
 *   2. Family preflight (optional, via the family's stream_validate
 *      hook). For families that implement it, family-specific value
 *      checks — e.g. a streaming extension field outside the model's
 *      trained menu, or below its kind's minimum size — run here, still
 *      BEFORE the snapshot is cleared and BEFORE the lifecycle moves.
 *      A non-OK return is delivered to the caller with the previous
 *      result snapshot fully preserved and no FAILED transition, so a
 *      caller-side config typo cannot destroy the prior utterance's
 *      transcript. A family that does not implement stream_validate
 *      performs these checks in stream_begin.
 *
 *   3. Post-clear stream_begin. Once the preflight checks pass, the dispatcher
 *      clears the result snapshot, enters ACTIVE, and calls the
 *      family's stream_begin hook. Any non-OK status from this point
 *      (config a non-preflighting family only catches here, allocation
 *      failure, etc.) transitions the stream to FAILED and preserves
 *      the status in transcribe_stream_last_status. The result snapshot
 *      in this case is whatever the hook wrote before failing —
 *      typically empty.
 *
 * Params lifetime: everything the caller passes is copied out before this
 * function returns. run_params strings (language / target_language) are
 * copied into session-owned storage, and family extensions follow their
 * documented copy-out contract — so the caller may free both params
 * structs and every pointer inside them the moment begin returns, while
 * the stream stays ACTIVE. Nothing handed to begin needs to outlive it.
 */
TRANSCRIBE_API transcribe_status transcribe_stream_begin(struct transcribe_session *             session,
                                                         const struct transcribe_run_params *    run_params,
                                                         const struct transcribe_stream_params * stream_params);

/*
 * Feed PCM into the active stream. 16 kHz mono float32, same as
 * transcribe_run.
 *
 * pcm must be non-null and n_samples must be strictly greater than
 * zero. Polling the stream without supplying audio is unsupported —
 * use the stream accessors (transcribe_stream_revision,
 * transcribe_stream_get_text, transcribe_stream_n_committed_*,
 * transcribe_stream_last_status, transcribe_stream_get_state) to inspect
 * state without progressing the stream.
 *
 * update is nullable; when non-null the dispatcher zero-initializes
 * it before calling the family hook, so callers may rely on a clean
 * struct even on early-return error paths.
 *
 * Returns TRANSCRIBE_ERR_INVALID_ARG when session is NULL, when state
 * is not ACTIVE, or on malformed input. A terminal non-OK status
 * from the family hook transitions the stream to FAILED and is
 * preserved in transcribe_stream_last_status. TRANSCRIBE_ERR_ABORTED
 * is a terminal status; transcribe_was_aborted distinguishes it from
 * other failures.
 */
TRANSCRIBE_API transcribe_status transcribe_stream_feed(struct transcribe_session *       session,
                                                        const float *                     pcm,
                                                        int                               n_samples,
                                                        struct transcribe_stream_update * update);

/*
 * Signal end of input. Flushes buffered audio, satisfies right-
 * context / lookahead requirements, and emits remaining text.
 *
 * On success the session transitions ACTIVE -> FINISHED. On a
 * terminal non-OK status the session transitions to FAILED and the
 * status is preserved in transcribe_stream_last_status.
 *
 * Returns TRANSCRIBE_ERR_INVALID_ARG when session is NULL or state is
 * not ACTIVE.
 */
TRANSCRIBE_API transcribe_status transcribe_stream_finalize(struct transcribe_session *       session,
                                                            struct transcribe_stream_update * update);

/*
 * Abandon the current stream without finalizing.
 *
 * Always returns the session to IDLE and clears every result-visible
 * field, every stream-snapshot counter, last_status, and the family's
 * per-utterance streaming state. Allocated stream buffers held by the
 * family are preserved for reuse — full memory release is
 * transcribe_session_free.
 *
 * Reset from IDLE clears any stale result/snapshot from a previous
 * stream or run. Reset from FINISHED or FAILED clears the surviving
 * result text and snapshot counters as well.
 *
 * No-op if session is NULL.
 */
TRANSCRIBE_API void transcribe_stream_reset(struct transcribe_session * session);

/*
 * Current stream lifecycle state. Returns TRANSCRIBE_STREAM_IDLE if
 * session is NULL.
 */
TRANSCRIBE_API enum transcribe_stream_state transcribe_stream_get_state(const struct transcribe_session * session);

/*
 * Monotonic snapshot revision counter. Advances whenever any observable
 * property of the streaming result changes: text vectors (tokens /
 * words / segments / full_text), committed-prefix counts, or a
 * lifecycle transition that promotes the snapshot (finalize moves
 * tentative output to committed even when the text is unchanged).
 * Reset to 0 by begin / reset / run. Returns 0 if session is NULL.
 *
 * Diff against the previous value when redrawing — a bump does not
 * by itself mean the visible text changed; it means "something about
 * the snapshot moved, re-read the accessors."
 */
TRANSCRIBE_API int transcribe_stream_revision(const struct transcribe_session * session);

/*
 * Low-level raw row boundary hints. In older streaming APIs these were
 * the primary committed-prefix interface. They remain useful for callers
 * that inspect raw token/word/segment rows, but the strong API-stable
 * display contract is now transcribe_stream_get_text(). A committed text
 * prefix may be independent from the current raw rows after the model
 * revises already-committed bytes.
 *
 * Return 0 if session is NULL.
 */
TRANSCRIBE_API int transcribe_stream_n_committed_segments(const struct transcribe_session * session);
TRANSCRIBE_API int transcribe_stream_n_committed_words(const struct transcribe_session * session);
TRANSCRIBE_API int transcribe_stream_n_committed_tokens(const struct transcribe_session * session);

/*
 * Last terminal status of the stream. Preserves the failing status
 * after a feed/finalize call transitioned the stream to FAILED, so
 * the caller can inspect it after the fact. Reset to TRANSCRIBE_OK
 * by begin / reset / run.
 *
 * Returns TRANSCRIBE_OK if session is NULL or no terminal status has
 * been recorded on the current stream.
 */
TRANSCRIBE_API transcribe_status transcribe_stream_last_status(const struct transcribe_session * session);

/* ----------------------------------------------------------------------- */
/* Tokenization                                                            */
/* ----------------------------------------------------------------------- */

/*
 * Tokenize plain UTF-8 text into the model's vocabulary, with
 * add_special_tokens=False semantics (no BOS/EOS, no <|...|> markers).
 * Special tokens present in the input are not recognized and will be
 * BPE-encoded piece-by-piece, which is almost never what the caller
 * wants — render special tokens via direct id paths and pass only the
 * plain-text fragments through this function.
 *
 * Buffer contract (mirrors whisper.cpp's n_max convention):
 *
 *   >= 0             Number of tokens written to `tokens[0..return-1]`.
 *   negative of N    Buffer too small; N tokens were needed. Caller
 *                    reallocates and retries. `tokens` content is
 *                    unspecified in this case (may be partially
 *                    written).
 *   INT_MIN          Hard error: model or text is NULL, the model's
 *                    tokenizer does not support encode for this
 *                    vocabulary (e.g. a SentencePiece family without
 *                    encode wired), or the vocab file is malformed.
 *
 * Plumbed per family:
 *   - Whisper      (GPT-2 byte-level BPE)
 *   - Qwen3-ASR    (Qwen2 byte-level BPE)
 *   - Parakeet     (SentencePiece; currently returns INT_MIN)
 *   - Cohere ASR   (SentencePiece; currently returns INT_MIN)
 */
TRANSCRIBE_API int transcribe_tokenize(const struct transcribe_model * model,
                                       const char *                    text,
                                       int32_t *                       tokens,
                                       size_t                          n_max);

/* ----------------------------------------------------------------------- */
/* Timings                                                                 */
/* ----------------------------------------------------------------------- */

/*
 * Per-call timings collected by the most recent transcribe_run on a
 * session, plus the wall-clock load time of the model the session is
 * bound to. All values are in milliseconds.
 *
 *   load_ms    one-time wall-clock cost of transcribe_model_load_file,
 *              captured on the model and surfaced via every session
 *              derived from it. Not affected by transcribe_reset_timings
 *              (it's a model-scoped fact).
 *   mel_ms     time to compute the mel front-end on the most recent
 *              transcribe_run. Reset to 0 by transcribe_reset_timings.
 *   encode_ms  time to run the encoder forward pass on the most recent
 *              transcribe_run.
 *   decode_ms  time to run the decoder (predictor + joint + token
 *              search) on the most recent transcribe_run.
 *
 * Output struct: caller initializes via transcribe_timings_init() (zero-
 * fill); the library writes only the fields that fit within
 * struct_size. Every field is a numeric value where zero encodes
 * "unknown / not measured."
 */
struct transcribe_timings {
    uint64_t struct_size;
    float    load_ms;
    float    mel_ms;
    float    encode_ms;
    float    decode_ms;
};

TRANSCRIBE_API void transcribe_timings_init(struct transcribe_timings * out);

/*
 * Read the current timings from a session into caller-owned storage.
 * Safe to call before any transcribe_run; mel_ms / encode_ms /
 * decode_ms will be 0. load_ms is non-zero as soon as the underlying
 * model is loaded.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG     session or out_timings is NULL.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE out_timings->struct_size is 0 or
 *                                  smaller than the library's minimum.
 */
TRANSCRIBE_API transcribe_status transcribe_get_timings(const struct transcribe_session * session,
                                                        struct transcribe_timings *       out_timings);

/*
 * Pretty-print the current timings to the registered log callback at
 * INFO level (or stderr if no callback is installed). No-op if session
 * is NULL.
 */
TRANSCRIBE_API void transcribe_print_timings(const struct transcribe_session * session);

/*
 * Reset the per-run timing accumulators (mel_ms, encode_ms,
 * decode_ms) to 0. Does NOT touch load_ms — that's a model-scoped
 * fact. No-op if session is NULL.
 */
TRANSCRIBE_API void transcribe_reset_timings(struct transcribe_session * session);

/* ----------------------------------------------------------------------- */
/* Result accessors - top level                                            */
/* ----------------------------------------------------------------------- */

TRANSCRIBE_API const char * transcribe_full_text(const struct transcribe_session * session);

/*
 * The model's decoded output BEFORE family post-processing — inline
 * diarization markers (moss `[0.48][S01]`, granite `[Speaker N]:`),
 * timestamp/special tokens (whisper), language/event/emotion tags
 * (sensevoice, parakeet, canary), chat-envelope prefixes (qwen3_asr),
 * and whitespace trims are all still present. transcribe_full_text is
 * always the clean transcript; this is the escape hatch for callers who
 * want what the model actually emitted (debugging, custom parsing).
 * Equal to full_text modulo whitespace for families that emit clean
 * text natively. Session-owned; same lifetime as transcribe_full_text.
 * Empty string before any successful run.
 */
TRANSCRIBE_API const char *              transcribe_raw_text(const struct transcribe_session * session);
TRANSCRIBE_API transcribe_timestamp_kind transcribe_returned_timestamp_kind(const struct transcribe_session * session);
TRANSCRIBE_API int                       transcribe_n_segments(const struct transcribe_session * session);
TRANSCRIBE_API int                       transcribe_n_words(const struct transcribe_session * session);
TRANSCRIBE_API int                       transcribe_n_tokens(const struct transcribe_session * session);

/*
 * The language the model itself predicted on the most recent run, as a
 * short ISO code ("en", "zh", "yue", "ja", "ko", ...). Returns an empty
 * string when:
 *   - no successful run has happened on this session yet, or
 *   - the caller passed an explicit `params->language` hint (the
 *     library does not echo hints back through this field; callers
 *     already know what they asked for), or
 *   - the model does not support language detection (English-only
 *     Whispers, families without an LID head), or
 *   - the family's LID head produced a non-language sentinel for this
 *     audio (e.g. SenseVoice's <|nospeech|>).
 *
 * Returned pointer is owned by the session. See "Result text-pointer
 * lifetime" at the top of this header for invalidation rules; in
 * streaming mode every transcribe_stream_feed / _finalize call may
 * invalidate it.
 */
TRANSCRIBE_API const char * transcribe_detected_language(const struct transcribe_session * session);

/* ----------------------------------------------------------------------- */
/* Result accessors - per-item rows                                        */
/* ----------------------------------------------------------------------- */

/*
 * Per-item results are exposed as caller-owned copy-out structs. Three
 * accessors (transcribe_get_segment / _word / _token) take a row index
 * and a pointer to an init-function-initialized struct; the library
 * writes the row's fields into the caller's buffer.
 *
 * `text` pointers in these structs are session-owned. The full lifetime
 * contract is in the "Result text-pointer lifetime" block at the top of
 * this header; in short:
 *   - One-shot path: valid until the next transcribe_run() /
 *     transcribe_stream_begin() / transcribe_stream_reset() /
 *     transcribe_session_free() call on the same session.
 *   - Streaming path: row `text` aliases the raw model snapshot and may
 *     be invalidated by every transcribe_stream_feed() /
 *     transcribe_stream_finalize() call. Copy it before the next feed if
 *     you need to hold it. Use transcribe_stream_get_text() for
 *     API-stable committed/provisional display text.
 *
 * Out-of-range index (i < 0 or i >= the corresponding transcribe_n_*())
 * returns TRANSCRIBE_OK with the caller's struct left as zero-init:
 * `text == NULL`, every scalar 0, and (for tokens) `p == 0.0f`. There
 * is no empty-string sentinel — bindings distinguish "row not present"
 * from "row present with empty text" by null-checking `text`.
 *
 * Forward-ABI: struct_size in each row struct is captured at the
 * caller's TU; the library writes only the fields that fit and never
 * touches tail bytes beyond the caller's struct_size. New fields are
 * appended at the end.
 */

struct transcribe_segment {
    uint64_t     struct_size;
    int64_t      t0_ms;
    int64_t      t1_ms;
    int          first_word;
    int          n_words;
    int          first_token;
    int          n_tokens;
    const char * text;       /* session-owned; see lifetime note above */
    int32_t      speaker_id; /* 1-based; 0 = no speaker attribution */
};

TRANSCRIBE_API void transcribe_segment_init(struct transcribe_segment * out);

struct transcribe_word {
    uint64_t     struct_size;
    int64_t      t0_ms;
    int64_t      t1_ms;
    int          seg_index;
    int          first_token;
    int          n_tokens;
    const char * text; /* session-owned; see lifetime note above */
};

TRANSCRIBE_API void transcribe_word_init(struct transcribe_word * out);

/*
 * transcribe_token::p is the per-token probability when the
 * architecture produces one, or NaN when it does not. The semantic of
 * "token probability" varies by family (joint softmax for transducer,
 * per-step softmax for autoregressive, per-frame argmax probability
 * for CTC) and callers should treat it as a confidence hint, not a
 * calibrated probability.
 *
 * On out-of-range index `p` follows the zero-init rule (0.0f, not
 * NaN); inspect `text != NULL` to distinguish a present row.
 */
struct transcribe_token {
    uint64_t     struct_size;
    int          id;
    float        p;
    int64_t      t0_ms;
    int64_t      t1_ms;
    int          seg_index;
    int          word_index;
    const char * text; /* session-owned; see lifetime note above */
};

TRANSCRIBE_API void transcribe_token_init(struct transcribe_token * out);

/*
 * Read one segment / word / token row into caller-owned storage.
 *
 * Returns:
 *   TRANSCRIBE_ERR_INVALID_ARG     out is NULL.
 *   TRANSCRIBE_ERR_BAD_STRUCT_SIZE out->struct_size is 0 or smaller
 *                                  than the library's minimum.
 *   TRANSCRIBE_OK                  otherwise. The caller's struct is
 *                                  written when session is non-NULL, the
 *                                  session has a result, and i is in
 *                                  range; otherwise it stays as
 *                                  zero-initialized by INIT (text NULL,
 *                                  scalars 0).
 */
TRANSCRIBE_API transcribe_status transcribe_get_segment(const struct transcribe_session * session,
                                                        int                               i,
                                                        struct transcribe_segment *       out);

TRANSCRIBE_API transcribe_status transcribe_get_word(const struct transcribe_session * session,
                                                     int                               i,
                                                     struct transcribe_word *          out);

TRANSCRIBE_API transcribe_status transcribe_get_token(const struct transcribe_session * session,
                                                      int                               i,
                                                      struct transcribe_token *         out);

/* ----------------------------------------------------------------------- */
/* Speaker-segment results (diarization)                                   */
/* ----------------------------------------------------------------------- */

/*
 * "Who spoke when" rows, populated when the run resolved diarization ON
 * for a model with TRANSCRIBE_FEATURE_DIARIZATION (see
 * transcribe_diarize_mode). Rows are ordered by emission order of the
 * model's speaker turns; rows MAY overlap in time (two speakers talking
 * at once are two overlapping rows). A model that attributes text but
 * carries no timing information reports t0_ms == t1_ms == 0 ("absent"
 * per the zero-sentinel rule).
 *
 * These rows are the transcript-independent view of speaker activity.
 * The transcript-attached view is transcribe_segment::speaker_id.
 *
 * p is the attribution confidence when the model produces one, or NaN
 * when it does not (same convention as transcribe_token::p). On
 * out-of-range index `p` follows the zero-init rule (0.0f, not NaN);
 * inspect `speaker_id != 0` to distinguish a present row.
 *
 * Empty (count 0) whenever diarization did not run: unsupported family,
 * diarize mode OFF, or no speaker markers recognized in this result.
 */
struct transcribe_speaker_segment {
    uint64_t struct_size;
    int64_t  t0_ms;
    int64_t  t1_ms;
    int32_t  speaker_id; /* 1-based; 0 = row not present */
    float    p;          /* confidence hint; NaN = not produced */
};

TRANSCRIBE_API void transcribe_speaker_segment_init(struct transcribe_speaker_segment * out);

/* 0 before any run, on NULL session, or when diarization did not run. */
TRANSCRIBE_API int transcribe_n_speaker_segments(const struct transcribe_session * session);

/*
 * Read one speaker-segment row into caller-owned storage. Same contract
 * as transcribe_get_segment: INVALID_ARG on NULL out, BAD_STRUCT_SIZE on
 * a zero/short struct_size, otherwise OK with the struct written when i
 * is in range and left zero-initialized when it is not.
 */
TRANSCRIBE_API transcribe_status transcribe_get_speaker_segment(const struct transcribe_session *   session,
                                                                int                                 i,
                                                                struct transcribe_speaker_segment * out);

/* ----------------------------------------------------------------------- */
/* Batch result accessors                                                  */
/* ----------------------------------------------------------------------- */

/*
 * Read back the per-utterance results produced by transcribe_run_batch.
 * These mirror the single-result accessors above with a leading utterance
 * index `i` in [0, transcribe_batch_n_results(session)).
 *
 * After a successful transcribe_run, n_results is 1 and index 0 is the
 * single result, so callers may use either accessor family interchangeably
 * for the n == 1 case. After transcribe_run_batch, index 0 also aliases
 * the legacy single-result accessors.
 *
 * Lifetime and out-of-range rules match the single-result accessors:
 *   - Returned `const char *` and row `text` pointers are session-owned
 *     and valid until the next transcribe_run / transcribe_run_batch /
 *     transcribe_stream_begin / transcribe_stream_reset /
 *     transcribe_session_free on the same session.
 *   - An out-of-range utterance index `i`, or an out-of-range row index
 *     `j`, yields a safe sentinel: "" / 0 / NaN for scalar returns, and a
 *     zero-initialized struct (text == NULL) for the copy-out accessors,
 *     which still return TRANSCRIBE_OK (struct-size faults and a NULL
 *     `out` return their usual non-OK codes).
 *   - Two accessors are deliberate exceptions to the sentinel rule and
 *     instead return TRANSCRIBE_ERR_INVALID_ARG for an out-of-range (or
 *     negative) utterance index `i`: transcribe_batch_status (TRANSCRIBE_OK
 *     is reserved for a real per-utterance success, so it cannot also
 *     sentinel a missing utterance) and transcribe_batch_get_timings
 *     (timings are utterance-scoped, not a row accessor like
 *     transcribe_batch_get_segment / _word / _token, so a missing utterance
 *     is an error rather than an empty row). The per-function docs below are
 *     authoritative where they differ from this summary.
 */

/*
 * Number of per-utterance results available. 0 before any run, or if
 * session is NULL. 1 after a successful transcribe_run. n after a
 * transcribe_run_batch with n utterances that returns OK or
 * TRANSCRIBE_ERR_ABORTED — one slot per input utterance, including
 * utterances that failed individually (non-OK transcribe_batch_status,
 * empty rows) and, after an abort, utterances not retained as completed
 * results (present as TRANSCRIBE_ERR_ABORTED slots). After a whole-batch
 * fault other than abort (OOM, backend error) the count is unspecified;
 * the non-OK top-level return is the signal in that case.
 */
TRANSCRIBE_API int transcribe_batch_n_results(const struct transcribe_session * session);

/*
 * Per-utterance terminal status. TRANSCRIBE_OK when utterance i produced a
 * result; otherwise the status that failed that utterance (e.g.
 * TRANSCRIBE_ERR_INVALID_ARG for a malformed input, or a backend error).
 * Returns TRANSCRIBE_ERR_INVALID_ARG if session is NULL or i is out of
 * range.
 */
TRANSCRIBE_API transcribe_status transcribe_batch_status(const struct transcribe_session * session, int i);

TRANSCRIBE_API const char * transcribe_batch_full_text(const struct transcribe_session * session, int i);

/* Per-utterance raw text; same contract as transcribe_raw_text. */
TRANSCRIBE_API const char * transcribe_batch_raw_text(const struct transcribe_session * session, int i);

TRANSCRIBE_API transcribe_timestamp_kind
transcribe_batch_returned_timestamp_kind(const struct transcribe_session * session, int i);

TRANSCRIBE_API const char * transcribe_batch_detected_language(const struct transcribe_session * session, int i);

TRANSCRIBE_API int transcribe_batch_n_segments(const struct transcribe_session * session, int i);

TRANSCRIBE_API int transcribe_batch_n_words(const struct transcribe_session * session, int i);

TRANSCRIBE_API int transcribe_batch_n_tokens(const struct transcribe_session * session, int i);

/*
 * Copy out row `j` of utterance `i`. The struct is initialized exactly as
 * for transcribe_get_segment / _word / _token (caller calls the matching
 * _init first); the only difference is the utterance index `i`.
 */
TRANSCRIBE_API transcribe_status transcribe_batch_get_segment(const struct transcribe_session * session,
                                                              int                               i,
                                                              int                               j,
                                                              struct transcribe_segment *       out);

TRANSCRIBE_API transcribe_status transcribe_batch_get_word(const struct transcribe_session * session,
                                                           int                               i,
                                                           int                               j,
                                                           struct transcribe_word *          out);

TRANSCRIBE_API transcribe_status transcribe_batch_get_token(const struct transcribe_session * session,
                                                            int                               i,
                                                            int                               j,
                                                            struct transcribe_token *         out);

/* Speaker-segment batch mirrors; same contracts as the single-result pair. */
TRANSCRIBE_API int transcribe_batch_n_speaker_segments(const struct transcribe_session * session, int i);

TRANSCRIBE_API transcribe_status transcribe_batch_get_speaker_segment(const struct transcribe_session *   session,
                                                                      int                                 i,
                                                                      int                                 j,
                                                                      struct transcribe_speaker_segment * out);

/*
 * Per-utterance timings for a batched run. Mirrors transcribe_get_timings but
 * indexed by utterance. load_ms is the model-scoped load time (same for every
 * utterance). mel_ms / encode_ms / decode_ms are this utterance's stage times;
 * for a batched encoder the encode_ms is the shared batch encode time
 * amortized across the batch (so the sum over utterances equals the real batch
 * encode time), while decode_ms is the genuine per-utterance host-decode cost.
 * This is what lets a caller see how run time splits between the GPU encoder
 * and the host decoder as batch size grows.
 *
 * Returns TRANSCRIBE_ERR_INVALID_ARG on NULL args or out-of-range i,
 * TRANSCRIBE_ERR_BAD_STRUCT_SIZE on an uninitialized out struct.
 */
TRANSCRIBE_API transcribe_status transcribe_batch_get_timings(const struct transcribe_session * session,
                                                              int                               i,
                                                              struct transcribe_timings *       out);

#ifdef __cplusplus
} /* extern "C" */
#endif

#endif /* TRANSCRIBE_H */