Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/model-family-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ Inputs:
- Reference dumps come from the per-family `scripts/dump_reference_*.py`.
- Tolerances come from `tests/tolerances/<family>.json`.
- Transcript artifacts are `transcript.json`; when the reference writes
one, validation requires exact C++ vs reference text equality.
one, validation requires exact C++ vs reference text equality. When the
reference artifact also carries word rows, validation additionally compares
each word's start and end against the family's `timestamps` tolerance.

Do not grow bespoke C++ tensor-comparison smoke tests per family. C++
smokes should cover load/run/API behavior; the dump/compare workflow
Expand Down Expand Up @@ -155,15 +157,20 @@ Tolerances are data, not C++ literals. The source of truth is:
tests/tolerances/<family>.json
```

Use the current flat tensor-name mapping across families:
Use the current flat mapping across families, with tensor names and the optional
`timestamps` entry as top-level keys:

```json
{
"_comment": ["why the tolerances are what they are"],
"timestamps": {"max_abs_ms": 80.0},
"enc.final": {"max_abs": 1e-4, "mean_abs": 1e-6}
}
```

`timestamps.max_abs_ms` bounds each word's start and end deviation from the
reference in milliseconds; `validate.py` uses it for the word-timestamp compare.

Generic fallback tolerances belong in the tool, for example
`compare_tensors.py --max-abs` and `--mean-abs`. Family-specific
tensor tolerances belong in `tests/tolerances/<family>.json`.
Expand Down
5 changes: 5 additions & 0 deletions docs/models/parakeet-tdt-0.6b-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ a transcript with optional token-level timestamps. It is not a streaming model,
does not translate, and v2 has no multilingual capability. For multilingual
see v3.

Word timings are numerically validated against the NeMo reference within one
encoder frame (80 ms). Token tables are dumped on both sides for divergence
localization but are not compared; punctuation-only tokens follow NeMo's
convention and are zero-length at the previous token's end.

See NVIDIA's [model card](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2)
for training data, intended use, and upstream evaluation methodology.

Expand Down
6 changes: 6 additions & 0 deletions docs/models/parakeet-tdt-0.6b-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Estonian, Finnish, French, German, Greek, Hungarian, Italian, Latvian,
Lithuanian, Maltese, Polish, Portuguese, Romanian, Russian, Slovak, Slovenian,
Spanish, Swedish, Ukrainian.

Word timings are numerically validated against the NeMo reference within one
encoder frame (80 ms); on the validation clip v3 matches the reference
exactly. Token tables are dumped on both sides for divergence localization
but are not compared; punctuation-only tokens follow NeMo's convention and
are zero-length at the previous token's end.

See NVIDIA's [model card](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v3)
for training data, intended use, and upstream evaluation methodology.

Expand Down
10 changes: 6 additions & 4 deletions docs/tools/validate.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ convention.
```text
validate.py ref → runs dump_reference_<family>_*.py → writes build/validate/<family>/<variant>/<case>/ref/
validate.py cpp → runs transcribe-cli with TRANSCRIBE_DUMP_DIR → writes build/validate/.../cpp/
validate.py compare → runs compare_tensors.py with tests/tolerances/<family>.json
validate.py compare → runs compare_tensors.py with tests/tolerances/<family>.json; compares transcript text and, when the reference dump carries word rows, word timestamps
validate.py all → ref + cpp + compare in sequence
```

Expand Down Expand Up @@ -42,9 +42,11 @@ uv run scripts/validate.py cpp --family cohere --gguf models/cohere-transcribe-0

## Exit codes

- `0` — all contract tensors within tolerance.
- `1` — one or more contract tensors out of tolerance, missing, or
shape-mismatched.
- `0` — all contract tensors are within tolerance and transcript text matches;
when the reference dump carries word rows, all word timestamps are within the
family's `timestamps` budget.
- `1` — any tensor is out of tolerance, missing, or shape-mismatched; any
required transcript text or word timestamp is missing or mismatched.

Exit-code driven, no interactive prompts (per project policy).

Expand Down
124 changes: 118 additions & 6 deletions examples/cli/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "wav.h"

#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
Expand Down Expand Up @@ -110,6 +111,113 @@ std::string batch_segments_json(const transcribe_session * ctx, int i) {
return out;
}

void append_word_json(std::string & out, const struct transcribe_word & wrd) {
char head[96];
std::snprintf(head, sizeof(head), "{\"t0_ms\":%lld,\"t1_ms\":%lld,\"text\":\"", static_cast<long long>(wrd.t0_ms),
static_cast<long long>(wrd.t1_ms));
out += head;
out += json_escape(wrd.text != nullptr ? wrd.text : "");
out += "\"}";
}

void append_token_json(std::string & out, const struct transcribe_token & tok) {
char head[96];
std::snprintf(head, sizeof(head), "{\"t0_ms\":%lld,\"t1_ms\":%lld,\"text\":\"", static_cast<long long>(tok.t0_ms),
static_cast<long long>(tok.t1_ms));
out += head;
out += json_escape(tok.text != nullptr ? tok.text : "");
char tail[96];
if (std::isfinite(tok.p)) {
std::snprintf(tail, sizeof(tail), "\",\"id\":%d,\"p\":%.9g}", tok.id, static_cast<double>(tok.p));
} else {
std::snprintf(tail, sizeof(tail), "\",\"id\":%d,\"p\":null}", tok.id);
}
out += tail;
}

std::string words_json(const transcribe_session * ctx) {
const transcribe_timestamp_kind kind = transcribe_returned_timestamp_kind(ctx);
if (kind != TRANSCRIBE_TIMESTAMPS_WORD && kind != TRANSCRIBE_TIMESTAMPS_TOKEN) {
return {};
}
const int n_words = transcribe_n_words(ctx);
if (n_words <= 0) {
return {};
}
std::string out = ",\"words\":[";
for (int w = 0; w < n_words; ++w) {
if (w > 0) {
out += ",";
}
struct transcribe_word wrd;
transcribe_word_init(&wrd);
(void) transcribe_get_word(ctx, w, &wrd);
append_word_json(out, wrd);
}
out += "]";
return out;
}

std::string tokens_json(const transcribe_session * ctx) {
if (transcribe_returned_timestamp_kind(ctx) != TRANSCRIBE_TIMESTAMPS_TOKEN) {
return {};
}
const int n_tokens = transcribe_n_tokens(ctx);
if (n_tokens <= 0) {
return {};
}
std::string out = ",\"tokens\":[";
for (int t = 0; t < n_tokens; ++t) {
if (t > 0) {
out += ",";
}
struct transcribe_token tok;
transcribe_token_init(&tok);
(void) transcribe_get_token(ctx, t, &tok);
append_token_json(out, tok);
}
out += "]";
return out;
}

std::string batch_words_json(const transcribe_session * ctx, int i) {
if (transcribe_batch_returned_timestamp_kind(ctx, i) < TRANSCRIBE_TIMESTAMPS_WORD) {
return {};
}
const int n_words = transcribe_batch_n_words(ctx, i);
std::string out = ",\"words\":[";
for (int w = 0; w < n_words; ++w) {
if (w > 0) {
out += ",";
}
struct transcribe_word wrd;
transcribe_word_init(&wrd);
(void) transcribe_batch_get_word(ctx, i, w, &wrd);
append_word_json(out, wrd);
}
out += "]";
return out;
}

std::string batch_tokens_json(const transcribe_session * ctx, int i) {
if (transcribe_batch_returned_timestamp_kind(ctx, i) != TRANSCRIBE_TIMESTAMPS_TOKEN) {
return {};
}
const int n_tokens = transcribe_batch_n_tokens(ctx, i);
std::string out = ",\"tokens\":[";
for (int t = 0; t < n_tokens; ++t) {
if (t > 0) {
out += ",";
}
struct transcribe_token tok;
transcribe_token_init(&tok);
(void) transcribe_batch_get_token(ctx, i, t, &tok);
append_token_json(out, tok);
}
out += "]";
return out;
}

struct cli_args {
std::string wav_path;
std::string model_path;
Expand Down Expand Up @@ -828,6 +936,8 @@ int main(int argc, char ** argv) {
if (args.batch_jsonl) {
const std::string escaped = json_escape(text);
const std::string segments = batch_segments_json(ctx, static_cast<int>(k));
const std::string words = batch_words_json(ctx, static_cast<int>(k));
const std::string tokens = batch_tokens_json(ctx, static_cast<int>(k));
std::string err_field;
if (ust != TRANSCRIBE_OK) {
err_field = ",\"error\":\"";
Expand All @@ -842,11 +952,11 @@ int main(int argc, char ** argv) {
transcribe_timings_init(&tm);
(void) transcribe_batch_get_timings(ctx, static_cast<int>(k), &tm);
std::printf(
"{\"file\":\"%s\",\"text\":\"%s\"%s,"
"{\"file\":\"%s\",\"text\":\"%s\"%s%s%s,"
"\"mel_ms\":%.1f,\"encode_ms\":%.1f,"
"\"decode_ms\":%.1f%s}\n",
wav.c_str(), escaped.c_str(), segments.c_str(), (double) tm.mel_ms, (double) tm.encode_ms,
(double) tm.decode_ms, err_field.c_str());
wav.c_str(), escaped.c_str(), segments.c_str(), words.c_str(), tokens.c_str(),
(double) tm.mel_ms, (double) tm.encode_ms, (double) tm.decode_ms, err_field.c_str());
} else {
std::printf("[%zu/%zu] %s", src_index[k] + 1, total, wav.c_str());
if (ust == TRANSCRIBE_OK) {
Expand Down Expand Up @@ -971,18 +1081,20 @@ int main(int argc, char ** argv) {
(void) transcribe_get_timings(ctx, &tm);
const std::string escaped = json_escape(text);
const std::string segments = segments_json(ctx);
const std::string words = words_json(ctx);
const std::string tokens = tokens_json(ctx);
std::string err_field;
if (run_st != TRANSCRIBE_OK) {
err_field = ",\"error\":\"";
err_field += json_escape(transcribe_status_string(run_st));
err_field += "\"";
}
std::printf(
"{\"file\":\"%s\",\"text\":\"%s\"%s,"
"{\"file\":\"%s\",\"text\":\"%s\"%s%s%s,"
"\"mel_ms\":%.1f,\"encode_ms\":%.1f,"
"\"decode_ms\":%.1f%s}\n",
wav.c_str(), escaped.c_str(), segments.c_str(), (double) tm.mel_ms, (double) tm.encode_ms,
(double) tm.decode_ms, err_field.c_str());
wav.c_str(), escaped.c_str(), segments.c_str(), words.c_str(), tokens.c_str(),
(double) tm.mel_ms, (double) tm.encode_ms, (double) tm.decode_ms, err_field.c_str());
} else {
std::printf("[%zu/%zu] %s", i + 1, wav_paths.size(), wav.c_str());
if (run_st == TRANSCRIBE_OK) {
Expand Down
Loading