use std::{
fs,
io::{Read, Write},
path::PathBuf,
};
const MODEL_URL: &str =
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin";
const MODEL_FILENAME: &str = "ggml-large-v3-turbo.bin";
const MODEL_SHA256: &str = "1fc70f774d38eb169993ac391eea357ef47c88757ef72ee5943879b7e8e2bc69";
const WAV_URL: &str = "https://github.com/ggerganov/whisper.cpp/raw/master/samples/jfk.wav";
const WAV_FILENAME: &str = "jfk.wav";
const WAV_SHA256: &str = "59dfb9a4acb36fe2a2affc14bacbee2920ff435cb13cc314a08c13f66ba7860e";
const MODEL_W2V_URL: &str =
"https://huggingface.co/onnx-community/wav2vec2-base-960h-ONNX/resolve/main/onnx/model.onnx";
const MODEL_W2V_FILENAME: &str = "wav2vec2-base-960h.onnx";
const MODEL_W2V_SHA256: &str = "00b7cc69516c1ab63c429e63a2b543e4d42bb77441ec5b98ee935de175b00de1";
const TOKENIZER_W2V_URL: &str =
"https://huggingface.co/onnx-community/wav2vec2-base-960h-ONNX/resolve/main/tokenizer.json";
const TOKENIZER_W2V_FILENAME: &str = "wav2vec2-base-960h-tokenizer.json";
const TOKENIZER_W2V_SHA256: &str =
"df57f576f5ef16a454ae2776dcc777ffef0bc824113043043b7218c829fc7405";
const MODEL_W2V_JA_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-japanese-onnx/resolve/main/model.onnx";
const MODEL_W2V_JA_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-japanese.onnx";
const MODEL_W2V_JA_SHA256: &str =
"1157d2e1078392f6469e87993d879e3af569fb9754a443c539dd5886cfbd4c5e";
const TOKENIZER_W2V_JA_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-japanese-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_JA_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-japanese-tokenizer.json";
const TOKENIZER_W2V_JA_SHA256: &str =
"f6390130dea2fc0902dfe5e7b66b249f49d99c26ae08f14265dcd8d67121c4c2";
const MODEL_W2V_ZH_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-chinese-zh-cn-onnx/resolve/main/model.onnx";
const MODEL_W2V_ZH_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-chinese-zh-cn.onnx";
const MODEL_W2V_ZH_SHA256: &str =
"4e92f1d33b6bf89b709d5e4512a0c98dcaafd37a9bf7928452b05b01edb83029";
const TOKENIZER_W2V_ZH_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-chinese-zh-cn-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_ZH_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-chinese-zh-cn-tokenizer.json";
const TOKENIZER_W2V_ZH_SHA256: &str =
"7bb5c156e0ea01980f42ae1904193834132019ae8a6276a9957805cd5a6b37f5";
const MODEL_W2V_KO_URL: &str =
"https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-korean-onnx/resolve/main/model.onnx";
const MODEL_W2V_KO_FILENAME: &str = "kresnik--wav2vec2-large-xlsr-korean.onnx";
const MODEL_W2V_KO_SHA256: &str =
"c43c01d7827bda6aaae60b04b722fea9a63399dd94b495166e4ddb529cf81a54";
const TOKENIZER_W2V_KO_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-korean-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_KO_FILENAME: &str = "kresnik--wav2vec2-large-xlsr-korean-tokenizer.json";
const TOKENIZER_W2V_KO_SHA256: &str =
"2890d0bbe027b185a4a429f4ca295c1e74f92f792c1517e76405db93ed36cf1c";
const MODEL_W2V_ES_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-spanish-onnx/resolve/main/model.onnx";
const MODEL_W2V_ES_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-spanish.onnx";
const MODEL_W2V_ES_SHA256: &str =
"3478c4d9beeee5d5f46ef3be4b4cfb896bed6b2baf2498c0b98123a7878e406a";
const TOKENIZER_W2V_ES_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-spanish-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_ES_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-spanish-tokenizer.json";
const TOKENIZER_W2V_ES_SHA256: &str =
"11f754c360f8fadde294adaeb0aa4d621887b6f1b40a89a447de8dfe4972cee4";
const MODEL_W2V_FR_URL: &str =
"https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-french-onnx/resolve/main/model.onnx";
const MODEL_W2V_FR_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-french.onnx";
const MODEL_W2V_FR_SHA256: &str =
"a26a555381f6525fbdc155a94664d5eafa0dab48f6c0194d42afe423af7be02b";
const TOKENIZER_W2V_FR_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-french-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_FR_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-french-tokenizer.json";
const TOKENIZER_W2V_FR_SHA256: &str =
"9e195f634c1bd2dbcc3062b176e482ac3a22653b2a47035819208c73b6895d74";
const MODEL_W2V_DE_URL: &str =
"https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-german-onnx/resolve/main/model.onnx";
const MODEL_W2V_DE_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-german.onnx";
const MODEL_W2V_DE_SHA256: &str =
"ee286242d24b0b0a07112692cff8a1486fc0373f180b21e6b8c7470ec17a42a2";
const TOKENIZER_W2V_DE_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-german-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_DE_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-german-tokenizer.json";
const TOKENIZER_W2V_DE_SHA256: &str =
"c722046285ab31f846408457417176d1c9cd3c53e15adff763ccdb746f490e58";
const MODEL_W2V_IT_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-italian-onnx/resolve/main/model.onnx";
const MODEL_W2V_IT_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-italian.onnx";
const MODEL_W2V_IT_SHA256: &str =
"4c07d4d3bc86ff0d52a16d60dae69ce6aa7b9cc8363fe3cdc61321eb4ee2cf0f";
const TOKENIZER_W2V_IT_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-italian-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_IT_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-italian-tokenizer.json";
const TOKENIZER_W2V_IT_SHA256: &str =
"856aa99e17e10afc77c278782d8068f1624123ae2e692764102446b737e1e3ac";
const MODEL_W2V_PT_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-portuguese-onnx/resolve/main/model.onnx";
const MODEL_W2V_PT_FILENAME: &str = "jonatasgrosman--wav2vec2-large-xlsr-53-portuguese.onnx";
const MODEL_W2V_PT_SHA256: &str =
"c101cedd8f9c5ade278e5ed8c698975b1f1048545e0eb29744786b0f7159d536";
const TOKENIZER_W2V_PT_URL: &str = "https://huggingface.co/FinDIT-Studio/wav2vec2-large-xlsr-53-portuguese-onnx/resolve/main/tokenizer.json";
const TOKENIZER_W2V_PT_FILENAME: &str =
"jonatasgrosman--wav2vec2-large-xlsr-53-portuguese-tokenizer.json";
const TOKENIZER_W2V_PT_SHA256: &str =
"841f77f1a38b2b96629e49df36eb52278f6e0181fa73f9aa934d70a19463c315";
struct W2vFixture {
code: &'static str,
env_prefix: &'static str,
approx_mb: u32,
model_url: &'static str,
model_filename: &'static str,
model_sha256: &'static str,
tokenizer_url: &'static str,
tokenizer_filename: &'static str,
tokenizer_sha256: &'static str,
}
const W2V_FIXTURES: &[W2vFixture] = &[
W2vFixture {
code: "en",
env_prefix: "ASRY_W2V",
approx_mb: 378,
model_url: MODEL_W2V_URL,
model_filename: MODEL_W2V_FILENAME,
model_sha256: MODEL_W2V_SHA256,
tokenizer_url: TOKENIZER_W2V_URL,
tokenizer_filename: TOKENIZER_W2V_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_SHA256,
},
W2vFixture {
code: "ja",
env_prefix: "ASRY_W2V_JA",
approx_mb: 1200,
model_url: MODEL_W2V_JA_URL,
model_filename: MODEL_W2V_JA_FILENAME,
model_sha256: MODEL_W2V_JA_SHA256,
tokenizer_url: TOKENIZER_W2V_JA_URL,
tokenizer_filename: TOKENIZER_W2V_JA_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_JA_SHA256,
},
W2vFixture {
code: "zh",
env_prefix: "ASRY_W2V_ZH",
approx_mb: 1200,
model_url: MODEL_W2V_ZH_URL,
model_filename: MODEL_W2V_ZH_FILENAME,
model_sha256: MODEL_W2V_ZH_SHA256,
tokenizer_url: TOKENIZER_W2V_ZH_URL,
tokenizer_filename: TOKENIZER_W2V_ZH_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_ZH_SHA256,
},
W2vFixture {
code: "ko",
env_prefix: "ASRY_W2V_KO",
approx_mb: 1200,
model_url: MODEL_W2V_KO_URL,
model_filename: MODEL_W2V_KO_FILENAME,
model_sha256: MODEL_W2V_KO_SHA256,
tokenizer_url: TOKENIZER_W2V_KO_URL,
tokenizer_filename: TOKENIZER_W2V_KO_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_KO_SHA256,
},
W2vFixture {
code: "es",
env_prefix: "ASRY_W2V_ES",
approx_mb: 1200,
model_url: MODEL_W2V_ES_URL,
model_filename: MODEL_W2V_ES_FILENAME,
model_sha256: MODEL_W2V_ES_SHA256,
tokenizer_url: TOKENIZER_W2V_ES_URL,
tokenizer_filename: TOKENIZER_W2V_ES_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_ES_SHA256,
},
W2vFixture {
code: "fr",
env_prefix: "ASRY_W2V_FR",
approx_mb: 1200,
model_url: MODEL_W2V_FR_URL,
model_filename: MODEL_W2V_FR_FILENAME,
model_sha256: MODEL_W2V_FR_SHA256,
tokenizer_url: TOKENIZER_W2V_FR_URL,
tokenizer_filename: TOKENIZER_W2V_FR_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_FR_SHA256,
},
W2vFixture {
code: "de",
env_prefix: "ASRY_W2V_DE",
approx_mb: 1200,
model_url: MODEL_W2V_DE_URL,
model_filename: MODEL_W2V_DE_FILENAME,
model_sha256: MODEL_W2V_DE_SHA256,
tokenizer_url: TOKENIZER_W2V_DE_URL,
tokenizer_filename: TOKENIZER_W2V_DE_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_DE_SHA256,
},
W2vFixture {
code: "it",
env_prefix: "ASRY_W2V_IT",
approx_mb: 1200,
model_url: MODEL_W2V_IT_URL,
model_filename: MODEL_W2V_IT_FILENAME,
model_sha256: MODEL_W2V_IT_SHA256,
tokenizer_url: TOKENIZER_W2V_IT_URL,
tokenizer_filename: TOKENIZER_W2V_IT_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_IT_SHA256,
},
W2vFixture {
code: "pt",
env_prefix: "ASRY_W2V_PT",
approx_mb: 1200,
model_url: MODEL_W2V_PT_URL,
model_filename: MODEL_W2V_PT_FILENAME,
model_sha256: MODEL_W2V_PT_SHA256,
tokenizer_url: TOKENIZER_W2V_PT_URL,
tokenizer_filename: TOKENIZER_W2V_PT_FILENAME,
tokenizer_sha256: TOKENIZER_W2V_PT_SHA256,
},
];
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=assets/wav2vec2_base_960h_tokenizer.json");
println!("cargo:rerun-if-env-changed=ASRY_OFFLINE");
println!("cargo:rerun-if-env-changed=ASRY_FETCH_MODEL");
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_ALIGNMENT");
println!("cargo:rerun-if-env-changed=ASRY_FETCH_W2V");
for fixture in W2V_FIXTURES {
println!("cargo:rustc-check-cfg=cfg(asry_w2v_{})", fixture.code);
}
if std::env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("windows") {
println!("cargo:rustc-link-lib=advapi32");
}
if let Err(e) = codegen_wav2vec2_base_960h_tokens() {
panic!("failed to codegen bundled wav2vec2 tokens: {e}");
}
if std::env::var("ASRY_OFFLINE").is_ok() {
if let Ok(raw) = std::env::var("ASRY_FETCH_W2V")
&& !parse_w2v_selection(&raw).is_empty()
{
println!(
"cargo:warning=ASRY_OFFLINE is set, so ASRY_FETCH_W2V={raw:?} was ignored: no \
wav2vec2 alignment fixtures were fetched, and the alignment tests will report \
`ignored`, not run. Unset ASRY_OFFLINE to honor the fetch request."
);
}
eprintln!("[asry build.rs] ASRY_OFFLINE set; skipping model fetch");
return;
}
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let models_dir = manifest_dir.join("models");
fetch_whisper_fixtures(&models_dir);
fetch_wav2vec2_fixtures(&models_dir);
}
fn fetch_whisper_fixtures(models_dir: &std::path::Path) {
if std::env::var("ASRY_FETCH_MODEL").is_err() {
return;
}
if std::env::var("CARGO_FEATURE_RUNNER").is_err() {
return;
}
if let Err(e) = fs::create_dir_all(models_dir) {
eprintln!("[asry build.rs] cannot create {models_dir:?}: {e}");
return;
}
let Some(target_dir) = find_target_dir() else {
eprintln!("[asry build.rs] cannot determine target dir; skipping fetch");
return;
};
let fixture_dir = target_dir.join("asry-test-fixtures");
if let Err(e) = fs::create_dir_all(&fixture_dir) {
eprintln!("[asry build.rs] cannot create {fixture_dir:?}: {e}");
return;
}
let model_path = models_dir.join(MODEL_FILENAME);
if !fetch_with_sha(MODEL_URL, &model_path, MODEL_SHA256) {
return;
}
println!(
"cargo:rustc-env=ASRY_WHISPER_MODEL={}",
model_path.display()
);
fetch_jfk_wav(&fixture_dir);
}
fn fetch_jfk_wav(fixture_dir: &std::path::Path) {
let wav_path = fixture_dir.join(WAV_FILENAME);
if wav_path.exists() {
if let Ok(true) = verify_sha256(&wav_path, WAV_SHA256) {
println!("cargo:rustc-env=ASRY_JFK_WAV={}", wav_path.display());
return;
}
let _ = fs::remove_file(&wav_path);
}
eprintln!("[asry build.rs] downloading {} ({})", WAV_FILENAME, WAV_URL);
if download(WAV_URL, &wav_path).is_err() {
let _ = fs::remove_file(&wav_path);
return;
}
if let Ok(true) = verify_sha256(&wav_path, WAV_SHA256) {
println!("cargo:rustc-env=ASRY_JFK_WAV={}", wav_path.display());
}
}
fn fetch_wav2vec2_fixtures(models_dir: &std::path::Path) {
let Ok(raw) = std::env::var("ASRY_FETCH_W2V") else {
return;
};
let selected = parse_w2v_selection(&raw);
if selected.is_empty() {
return;
}
if std::env::var("CARGO_FEATURE_ALIGNMENT").is_err() {
println!(
"cargo:warning=ASRY_FETCH_W2V={raw:?} was set, but the `alignment` feature is off: \
nothing was fetched, and the alignment tests are not compiled into this build. \
Re-run with `--features alignment`."
);
return;
}
if let Err(e) = fs::create_dir_all(models_dir) {
panic!(
"asry build.rs: ASRY_FETCH_W2V is set, but the models directory could not be \
created.\n \
path: {}\n \
cause: {e}\n\n\
An explicitly requested alignment fixture fetch cannot proceed without it. This \
is a hard error, not a warning: a soft skip would cache a build with the \
alignment gate silently disabled, and repairing the filesystem would not re-run \
this script (Cargo caches the successful build). Remove whatever occupies that \
path (e.g. a regular file named `models` shadowing the directory) or fix its \
permissions, then re-build.",
models_dir.display()
);
}
for fixture in selected {
match fetch_align_fixture(models_dir, fixture) {
Ok(()) => println!("cargo:rustc-cfg=asry_w2v_{}", fixture.code),
Err(cause) => println!(
"cargo:warning=ASRY_FETCH_W2V: `{}` fixture unavailable — {cause}. Its alignment \
tests will report `ignored`, not run. This is a fetch/mirror failure, not a \
request to re-run the same command.",
fixture.code
),
}
}
}
fn parse_w2v_selection(raw: &str) -> Vec<&'static W2vFixture> {
let lowered = raw.trim().to_ascii_lowercase();
match lowered.as_str() {
"" | "0" | "no" | "off" | "false" => return Vec::new(),
"1" | "all" | "yes" | "on" | "true" => return W2V_FIXTURES.iter().collect(),
_ => {}
}
let mut selected: Vec<&'static W2vFixture> = Vec::new();
for token in lowered.split([',', ' ', '\t']) {
let code = token.trim();
if code.is_empty() {
continue;
}
let Some(fixture) = W2V_FIXTURES.iter().find(|f| f.code == code) else {
panic!("{}", w2v_selection_error(raw, Some(code)));
};
if !selected.iter().any(|f| f.code == fixture.code) {
selected.push(fixture);
}
}
if selected.is_empty() {
panic!("{}", w2v_selection_error(raw, None));
}
selected
}
fn w2v_selection_error(raw: &str, offending: Option<&str>) -> String {
let codes: Vec<&str> = W2V_FIXTURES.iter().map(|f| f.code).collect();
let headline = match offending {
Some(code) => format!("ASRY_FETCH_W2V: unknown language code {code:?} (in {raw:?})"),
None => format!("ASRY_FETCH_W2V={raw:?} selects no languages"),
};
format!(
"{headline}\n\n\
Valid values:\n \
ASRY_FETCH_W2V=en one language ({en_mb} MB) — the usual choice\n \
ASRY_FETCH_W2V=en,ja a comma-separated subset\n \
ASRY_FETCH_W2V=1 every language (~10 GB); `all` is a synonym\n \
ASRY_FETCH_W2V=0 fetch nothing (same as leaving it unset)\n\n\
Known language codes: {codes}\n\n\
Refusing to continue. Quietly fetching nothing would leave the alignment tests\n\
reporting `ignored` with no hint as to why — which is the exact failure this\n\
opt-in was built to prevent.",
codes = codes.join(", "),
en_mb = W2V_FIXTURES
.iter()
.find(|f| f.code == "en")
.map_or(378, |f| f.approx_mb),
)
}
fn fetch_align_fixture(models_dir: &std::path::Path, fixture: &W2vFixture) -> Result<(), String> {
eprintln!(
"[asry build.rs] wav2vec2 alignment fixture `{}` (~{} MB)",
fixture.code, fixture.approx_mb
);
let model_path = models_dir.join(fixture.model_filename);
let tokenizer_path = models_dir.join(fixture.tokenizer_filename);
println!("cargo:rerun-if-changed={}", model_path.display());
println!("cargo:rerun-if-changed={}", tokenizer_path.display());
obtain_pinned(fixture.model_url, &model_path, fixture.model_sha256)
.map_err(|c| format!("model {}: {c}", fixture.model_filename))?;
obtain_pinned(
fixture.tokenizer_url,
&tokenizer_path,
fixture.tokenizer_sha256,
)
.map_err(|c| format!("tokenizer {}: {c}", fixture.tokenizer_filename))?;
println!(
"cargo:rustc-env={}_MODEL={}",
fixture.env_prefix,
model_path.display()
);
println!(
"cargo:rustc-env={}_TOKENIZER={}",
fixture.env_prefix,
tokenizer_path.display()
);
Ok(())
}
fn fetch_with_sha(url: &str, dest: &std::path::Path, expected_sha: &str) -> bool {
if dest.exists() {
if let Ok(true) = verify_sha256(dest, expected_sha) {
return true;
}
eprintln!(
"[asry build.rs] cached {:?} has wrong checksum; re-downloading",
dest
);
let _ = std::fs::remove_file(dest);
}
eprintln!(
"[asry build.rs] downloading {} ({})",
dest.file_name().unwrap_or_default().to_string_lossy(),
url
);
if let Err(e) = download(url, dest) {
eprintln!("[asry build.rs] download failed: {e}");
let _ = std::fs::remove_file(dest);
return false;
}
match verify_sha256(dest, expected_sha) {
Ok(true) => true,
Ok(false) => {
eprintln!("[asry build.rs] SHA-256 mismatch; aborting");
let _ = std::fs::remove_file(dest);
false
}
Err(e) => {
eprintln!("[asry build.rs] SHA-256 verify I/O: {e}");
false
}
}
}
fn obtain_pinned(url: &str, dest: &std::path::Path, expected_sha: &str) -> Result<(), String> {
if dest.exists() {
match verify_sha256(dest, expected_sha) {
Ok(true) => return Ok(()),
Ok(false) => {
let actual = sha256_hex(dest).unwrap_or_else(|e| format!("<unreadable: {e}>"));
panic!(
"asry build.rs: cached alignment fixture failed its SHA-256 pin.\n \
file: {}\n \
expected: {expected_sha}\n \
actual: {actual}\n\n\
These are not the pinned, provenance-verified bytes the parity reference is\n\
validated against. Remove the file to force a fresh, verified re-download, or\n\
restore the correct bytes. Emitting its cfg anyway would let the alignment\n\
tests run against un-advertised content.",
dest.display()
);
}
Err(e) => return Err(format!("reading cached {}: {e}", dest.display())),
}
}
eprintln!(
"[asry build.rs] downloading {} ({url})",
dest.file_name().unwrap_or_default().to_string_lossy()
);
if let Err(e) = download(url, dest) {
let _ = fs::remove_file(dest);
return Err(format!("download failed: {e}"));
}
match verify_sha256(dest, expected_sha) {
Ok(true) => Ok(()),
Ok(false) => {
let actual = sha256_hex(dest).unwrap_or_else(|e| format!("<unreadable: {e}>"));
let _ = fs::remove_file(dest);
Err(format!(
"freshly downloaded bytes failed the SHA-256 pin (expected {expected_sha}, got {actual})"
))
}
Err(e) => Err(format!("SHA-256 verify I/O error: {e}")),
}
}
fn find_target_dir() -> Option<PathBuf> {
let out = std::env::var_os("OUT_DIR")?;
let mut p = PathBuf::from(&out);
while let Some(parent) = p.parent().map(PathBuf::from) {
if parent.file_name().and_then(|s| s.to_str()) == Some("target") || parent.ends_with("target") {
return Some(parent);
}
p = parent;
}
None
}
fn download(url: &str, dest: &std::path::Path) -> std::io::Result<()> {
let resp = ureq::get(url)
.call()
.map_err(|e| std::io::Error::other(format!("{e}")))?;
let mut reader = resp.into_body().into_reader();
let mut writer = fs::File::create(dest)?;
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = reader.read(&mut buf)?;
if n == 0 {
break;
}
writer.write_all(&buf[..n])?;
}
writer.flush()
}
fn sha256_hex(path: &std::path::Path) -> std::io::Result<String> {
use sha2::{Digest, Sha256};
let mut f = fs::File::open(path)?;
let mut hasher = Sha256::new();
let mut buf = vec![0u8; 64 * 1024];
loop {
let n = f.read(&mut buf)?;
if n == 0 {
break;
}
hasher.update(&buf[..n]);
}
Ok(hex_encode(&hasher.finalize()))
}
fn verify_sha256(path: &std::path::Path, expected: &str) -> std::io::Result<bool> {
if expected.len() != 64
|| !expected
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
{
return Ok(false);
}
Ok(sha256_hex(path)? == expected)
}
fn hex_encode(bytes: &[u8]) -> String {
let mut s = String::with_capacity(bytes.len() * 2);
for b in bytes {
s.push_str(&format!("{:02x}", b));
}
s
}
fn codegen_wav2vec2_base_960h_tokens() -> Result<(), String> {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").map_err(|e| format!("CARGO_MANIFEST_DIR not set: {e}"))?;
let json_path = std::path::PathBuf::from(&manifest_dir)
.join("assets")
.join("wav2vec2_base_960h_tokenizer.json");
let json_bytes =
fs::read(&json_path).map_err(|e| format!("read {}: {e}", json_path.display()))?;
let parsed: serde_json::Value =
serde_json::from_slice(&json_bytes).map_err(|e| format!("parse tokenizer.json: {e}"))?;
let vocab = parsed
.get("model")
.and_then(|m| m.get("vocab"))
.and_then(|v| v.as_object())
.ok_or_else(|| "tokenizer.json missing model.vocab object".to_string())?;
let mut entries: Vec<(String, u32)> = Vec::with_capacity(vocab.len());
for (token, id_val) in vocab {
let id = id_val
.as_u64()
.ok_or_else(|| format!("vocab[{token:?}] is not an integer"))?;
let id_u32 =
u32::try_from(id).map_err(|e| format!("vocab[{token:?}] id {id} > u32::MAX: {e}"))?;
entries.push((token.clone(), id_u32));
}
entries.sort_by_key(|(_, id)| *id);
let pad_id = lookup_id(&entries, "<pad>").ok_or_else(|| "vocab missing `<pad>`".to_string())?;
let unk_id = lookup_id(&entries, "<unk>").ok_or_else(|| "vocab missing `<unk>`".to_string())?;
let delim_id =
lookup_id(&entries, "|").ok_or_else(|| "vocab missing `|` (word delimiter)".to_string())?;
let mut out = String::with_capacity(entries.len() * 24 + 256);
out.push_str(
"// Generated by build.rs from assets/wav2vec2_base_960h_tokenizer.json — DO NOT EDIT.\n\
// The bundled wav2vec2-base-960h vocab, sorted by id ascending.\n\n",
);
out.push_str(&format!(
"/// CTC blank token id (`<pad>` in wav2vec2's vocab).\npub const PAD_TOKEN_ID: u32 = {pad_id};\n\n"
));
out.push_str(&format!(
"/// `<unk>` (out-of-vocab) token id.\npub const UNK_TOKEN_ID: u32 = {unk_id};\n\n"
));
out.push_str(&format!(
"/// `|` word-delimiter token id.\npub const DELIMITER_TOKEN_ID: u32 = {delim_id};\n\n"
));
out.push_str(&format!(
"/// Vocab as `(token, id)` pairs, sorted by id ascending.\n\
/// {} entries.\n\
pub const VOCAB: &[(&str, u32)] = &[\n",
entries.len()
));
for (token, id) in &entries {
let escaped = token.replace('\\', "\\\\").replace('\"', "\\\"");
out.push_str(&format!(" (\"{escaped}\", {id}),\n"));
}
out.push_str("];\n");
let out_dir = std::env::var("OUT_DIR").map_err(|e| format!("OUT_DIR not set: {e}"))?;
let dest = std::path::PathBuf::from(out_dir).join("wav2vec2_base_960h_tokens.rs");
fs::write(&dest, out).map_err(|e| format!("write {}: {e}", dest.display()))?;
Ok(())
}
fn lookup_id(entries: &[(String, u32)], token: &str) -> Option<u32> {
entries.iter().find(|(t, _)| t == token).map(|(_, id)| *id)
}