feat(etapa2): transcriber sherpa-onnx-offline em Rust
This commit is contained in:
1 parent
c5baf7a23f
commit
695a0d5c42
11 files changed
+1585
-173
No files matched your search
@@ -0,0 +1,91 @@
|
||||
// Baixa fixtures oficiais do sherpa-onnx para `src-tauri/tests/fixtures/`.
|
||||
//
|
||||
// O áudio `parakeet-sample.wav` vem dentro do tarball oficial do modelo
|
||||
// Parakeet TDT v2 int8. É o áudio usado nos docs do sherpa-onnx para validar
|
||||
// a CLI `sherpa-onnx-offline`. Funciona com o v3 (mesma família de modelo).
|
||||
//
|
||||
// Uso:
|
||||
// node scripts/setup-fixtures.mjs
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
createWriteStream,
|
||||
statSync,
|
||||
renameSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join, resolve, sep as pathSep } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
const repoRoot = resolve(__dirname, "..");
|
||||
const fixturesDir = join(repoRoot, "src-tauri", "tests", "fixtures");
|
||||
mkdirSync(fixturesDir, { recursive: true });
|
||||
|
||||
const FIXTURES = [
|
||||
{
|
||||
name: "parakeet-sample.wav",
|
||||
url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8.tar.bz2",
|
||||
extractFrom: "sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/test_wavs/0.wav",
|
||||
minSize: 10_000,
|
||||
},
|
||||
];
|
||||
|
||||
async function download(url, dest) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`download falhou: ${res.status} ${res.statusText} em ${url}`,
|
||||
);
|
||||
}
|
||||
if (!res.body) {
|
||||
throw new Error(`resposta vazia em ${url}`);
|
||||
}
|
||||
await pipeline(res.body, createWriteStream(dest));
|
||||
return statSync(dest).size;
|
||||
}
|
||||
|
||||
for (const fixture of FIXTURES) {
|
||||
const dest = join(fixturesDir, fixture.name);
|
||||
if (existsSync(dest) && statSync(dest).size > fixture.minSize) {
|
||||
console.log(
|
||||
`✓ fixture já presente em ${dest} (${statSync(dest).size} bytes)`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
console.log(`↓ baixando ${fixture.url}`);
|
||||
const tmpArchive = join(fixturesDir, "fixture.tar.bz2");
|
||||
const size = await download(fixture.url, tmpArchive);
|
||||
if (size < 1_000_000) {
|
||||
throw new Error(
|
||||
`fixture ${fixture.name} tarball muito pequeno: ${size} bytes`,
|
||||
);
|
||||
}
|
||||
// `tar` no Windows tem problema com paths absolutos em `-C`; usa `cwd`+relativo.
|
||||
const relArchive = tmpArchive.startsWith(fixturesDir + pathSep)
|
||||
? tmpArchive.slice(fixturesDir.length + 1)
|
||||
: tmpArchive;
|
||||
execFileSync("tar", ["-xjf", relArchive], {
|
||||
cwd: fixturesDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
const extractedPath = join(fixturesDir, fixture.extractFrom);
|
||||
if (!existsSync(extractedPath)) {
|
||||
throw new Error(
|
||||
`caminho extraído ${extractedPath} não existe; verificar nome interno`,
|
||||
);
|
||||
}
|
||||
renameSync(extractedPath, dest);
|
||||
rmSync(tmpArchive, { force: true });
|
||||
const finalSize = statSync(dest).size;
|
||||
if (finalSize < fixture.minSize) {
|
||||
throw new Error(
|
||||
`fixture ${fixture.name} parece corrompida: ${finalSize} bytes (esperado > ${fixture.minSize})`,
|
||||
);
|
||||
}
|
||||
console.log(`✓ fixture pronta em ${dest} (${finalSize} bytes)`);
|
||||
}
|
||||
+146
-103
@@ -14,13 +14,13 @@
|
||||
|
||||
import { execFileSync } from "node:child_process";
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
chmodSync,
|
||||
createWriteStream,
|
||||
statSync,
|
||||
renameSync,
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
chmodSync,
|
||||
createWriteStream,
|
||||
statSync,
|
||||
renameSync,
|
||||
copyFileSync,
|
||||
} from "node:fs";
|
||||
import { dirname, join, resolve, sep as pathSep } from "node:path";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
@@ -36,132 +36,175 @@ mkdirSync(binariesDir, { recursive: true });
|
||||
const DEFAULT_VERSION = "v1.12.23";
|
||||
|
||||
const PLATFORM_MAP = [
|
||||
{ keys: ["linux", "x64"], asset: "sherpa-onnx-v1.12.23-linux-x64-shared.tar.bz2", triple: "x86_64-unknown-linux-gnu", ext: "" },
|
||||
{ keys: ["linux", "arm64"], asset: "sherpa-onnx-v1.12.23-linux-aarch64-shared.tar.bz2", triple: "aarch64-unknown-linux-gnu", ext: "" },
|
||||
{ keys: ["darwin", "x64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "x86_64-apple-darwin", ext: "" },
|
||||
{ keys: ["darwin", "arm64"], asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2", triple: "aarch64-apple-darwin", ext: "" },
|
||||
{ keys: ["win32", "x64"], asset: "sherpa-onnx-v1.12.23-win-x64-shared.tar.bz2", triple: "x86_64-pc-windows-msvc", ext: ".exe" },
|
||||
{
|
||||
keys: ["linux", "x64"],
|
||||
asset: "sherpa-onnx-v1.12.23-linux-x64-shared.tar.bz2",
|
||||
triple: "x86_64-unknown-linux-gnu",
|
||||
ext: "",
|
||||
},
|
||||
{
|
||||
keys: ["linux", "arm64"],
|
||||
asset: "sherpa-onnx-v1.12.23-linux-aarch64-shared.tar.bz2",
|
||||
triple: "aarch64-unknown-linux-gnu",
|
||||
ext: "",
|
||||
},
|
||||
{
|
||||
keys: ["darwin", "x64"],
|
||||
asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2",
|
||||
triple: "x86_64-apple-darwin",
|
||||
ext: "",
|
||||
},
|
||||
{
|
||||
keys: ["darwin", "arm64"],
|
||||
asset: "sherpa-onnx-v1.12.23-osx-universal2-shared.tar.bz2",
|
||||
triple: "aarch64-apple-darwin",
|
||||
ext: "",
|
||||
},
|
||||
{
|
||||
keys: ["win32", "x64"],
|
||||
asset: "sherpa-onnx-v1.12.23-win-x64-shared.tar.bz2",
|
||||
triple: "x86_64-pc-windows-msvc",
|
||||
ext: ".exe",
|
||||
},
|
||||
];
|
||||
|
||||
const RELEASE_BASE = "https://github.com/k2-fsa/sherpa-onnx/releases/download";
|
||||
|
||||
function detectTriple() {
|
||||
if (process.env.SHERPA_ONNX_TRIPLE) return process.env.SHERPA_ONNX_TRIPLE;
|
||||
try {
|
||||
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||
if (line) return line.replace("host:", "").trim();
|
||||
} catch {
|
||||
// cai para detecção por platform/arch
|
||||
}
|
||||
const os = process.platform;
|
||||
const archName = process.arch;
|
||||
return PLATFORM_MAP.find(
|
||||
(p) => (p.keys[0] === "win32" ? os === "win32" : p.keys[0] === os) && p.keys[1] === archName,
|
||||
)?.triple;
|
||||
if (process.env.SHERPA_ONNX_TRIPLE) return process.env.SHERPA_ONNX_TRIPLE;
|
||||
try {
|
||||
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||
if (line) return line.replace("host:", "").trim();
|
||||
} catch {
|
||||
// cai para detecção por platform/arch
|
||||
}
|
||||
const os = process.platform;
|
||||
const archName = process.arch;
|
||||
return PLATFORM_MAP.find(
|
||||
(p) =>
|
||||
(p.keys[0] === "win32" ? os === "win32" : p.keys[0] === os) &&
|
||||
p.keys[1] === archName,
|
||||
)?.triple;
|
||||
}
|
||||
|
||||
function pickAsset(triple) {
|
||||
const found = PLATFORM_MAP.find((p) => p.triple === triple);
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Não há binário pré-buildado para o target ${triple}. Edite scripts/setup-sidecar.mjs para apontar para um build customizado.`,
|
||||
);
|
||||
}
|
||||
return found;
|
||||
const found = PLATFORM_MAP.find((p) => p.triple === triple);
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
`Não há binário pré-buildado para o target ${triple}. Edite scripts/setup-sidecar.mjs para apontar para um build customizado.`,
|
||||
);
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
async function download(url, dest) {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(`download falhou: ${res.status} ${res.statusText} em ${url}`);
|
||||
}
|
||||
if (!res.body) {
|
||||
throw new Error(`resposta vazia em ${url}`);
|
||||
}
|
||||
await pipeline(res.body, createWriteStream(dest));
|
||||
return statSync(dest).size;
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
`download falhou: ${res.status} ${res.statusText} em ${url}`,
|
||||
);
|
||||
}
|
||||
if (!res.body) {
|
||||
throw new Error(`resposta vazia em ${url}`);
|
||||
}
|
||||
await pipeline(res.body, createWriteStream(dest));
|
||||
return statSync(dest).size;
|
||||
}
|
||||
|
||||
function extractTarBzip2(archive, cwd) {
|
||||
// `tar` (bsdtar) no Windows não aceita bem paths com backslash nem `D:`
|
||||
// em `-C`. Usamos `cwd` + path relativo para evitar isso.
|
||||
const prefix = cwd.endsWith(pathSep) ? cwd : cwd + pathSep;
|
||||
const relArchive = archive.startsWith(prefix) ? archive.slice(prefix.length) : archive;
|
||||
execFileSync("tar", ["-xjf", relArchive], { stdio: "inherit", cwd });
|
||||
// `tar` (bsdtar) no Windows não aceita bem paths com backslash nem `D:`
|
||||
// em `-C`. Usamos `cwd` + path relativo para evitar isso.
|
||||
const prefix = cwd.endsWith(pathSep) ? cwd : cwd + pathSep;
|
||||
const relArchive = archive.startsWith(prefix)
|
||||
? archive.slice(prefix.length)
|
||||
: archive;
|
||||
execFileSync("tar", ["-xjf", relArchive], { stdio: "inherit", cwd });
|
||||
}
|
||||
|
||||
function moveExtractedBin(extractedBinDir, expectedNames, dest) {
|
||||
for (const name of expectedNames) {
|
||||
const candidate = join(extractedBinDir, name);
|
||||
if (existsSync(candidate)) {
|
||||
renameSync(candidate, dest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`binário sherpa-onnx-offline não encontrado em ${extractedBinDir}; nomes esperados: ${expectedNames.join(", ")}`,
|
||||
);
|
||||
for (const name of expectedNames) {
|
||||
const candidate = join(extractedBinDir, name);
|
||||
if (existsSync(candidate)) {
|
||||
renameSync(candidate, dest);
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new Error(
|
||||
`binário sherpa-onnx-offline não encontrado em ${extractedBinDir}; nomes esperados: ${expectedNames.join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
function copyExtraSharedLibs(extractedRoot, ext) {
|
||||
// As DLLs adicionais (onnxruntime, providers_shared) precisam acompanhar o
|
||||
// executável em runtime. Copiamos para `src-tauri/binaries/` lado a lado
|
||||
// do binário renomeado; o empacotamento final via tauri.conf.json
|
||||
// resources cuida de movê-las para junto do .exe no instalador.
|
||||
const binDir = join(extractedRoot, "bin");
|
||||
if (!existsSync(binDir)) return;
|
||||
for (const file of ["onnxruntime.dll", "onnxruntime_providers_shared.dll", "cargs.dll"]) {
|
||||
const src = join(binDir, file);
|
||||
if (existsSync(src)) {
|
||||
const destName = ext === ".exe" ? file : file.replace(/\.dll$/, ".so");
|
||||
copyFileSync(src, join(binariesDir, destName));
|
||||
}
|
||||
}
|
||||
// As DLLs adicionais (onnxruntime, providers_shared) precisam acompanhar o
|
||||
// executável em runtime. Copiamos para `src-tauri/binaries/` lado a lado
|
||||
// do binário renomeado; o empacotamento final via tauri.conf.json
|
||||
// resources cuida de movê-las para junto do .exe no instalador.
|
||||
const binDir = join(extractedRoot, "bin");
|
||||
if (!existsSync(binDir)) return;
|
||||
for (const file of [
|
||||
"onnxruntime.dll",
|
||||
"onnxruntime_providers_shared.dll",
|
||||
"cargs.dll",
|
||||
]) {
|
||||
const src = join(binDir, file);
|
||||
if (existsSync(src)) {
|
||||
const destName = ext === ".exe" ? file : file.replace(/\.dll$/, ".so");
|
||||
copyFileSync(src, join(binariesDir, destName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const triple = detectTriple();
|
||||
if (!triple) {
|
||||
throw new Error("Não foi possível detectar o target triple do host.");
|
||||
}
|
||||
const asset = pickAsset(triple);
|
||||
const version = process.env.SHERPA_ONNX_VERSION || DEFAULT_VERSION;
|
||||
const assetName = asset.asset.replace(/v1\.12\.23/, version);
|
||||
const versionNoV = version.replace(/^v/, "");
|
||||
const url = `${RELEASE_BASE}/v${versionNoV}/${assetName}`;
|
||||
const finalPath = join(binariesDir, `sherpa-onnx-offline-${triple}${asset.ext}`);
|
||||
const triple = detectTriple();
|
||||
if (!triple) {
|
||||
throw new Error("Não foi possível detectar o target triple do host.");
|
||||
}
|
||||
const asset = pickAsset(triple);
|
||||
const version = process.env.SHERPA_ONNX_VERSION || DEFAULT_VERSION;
|
||||
const assetName = asset.asset.replace(/v1\.12\.23/, version);
|
||||
const versionNoV = version.replace(/^v/, "");
|
||||
const url = `${RELEASE_BASE}/v${versionNoV}/${assetName}`;
|
||||
const finalPath = join(
|
||||
binariesDir,
|
||||
`sherpa-onnx-offline-${triple}${asset.ext}`,
|
||||
);
|
||||
|
||||
if (existsSync(finalPath) && statSync(finalPath).size > 1024 * 1024) {
|
||||
console.log(`✓ sidecar já presente em ${finalPath} (${statSync(finalPath).size} bytes)`);
|
||||
return;
|
||||
}
|
||||
if (existsSync(finalPath) && statSync(finalPath).size > 1024 * 1024) {
|
||||
console.log(
|
||||
`✓ sidecar já presente em ${finalPath} (${statSync(finalPath).size} bytes)`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const archivePath = join(binariesDir, assetName);
|
||||
console.log(`↓ baixando ${url}`);
|
||||
const size = await download(url, archivePath);
|
||||
console.log(`✓ ${size} bytes baixados em ${archivePath}`);
|
||||
const archivePath = join(binariesDir, assetName);
|
||||
console.log(`↓ baixando ${url}`);
|
||||
const size = await download(url, archivePath);
|
||||
console.log(`✓ ${size} bytes baixados em ${archivePath}`);
|
||||
|
||||
console.log("↓ extraindo…");
|
||||
extractTarBzip2(archivePath, binariesDir);
|
||||
console.log("↓ extraindo…");
|
||||
extractTarBzip2(archivePath, binariesDir);
|
||||
|
||||
const extractedRoot = join(binariesDir, assetName.replace(/\.tar\.bz2$/, ""));
|
||||
const expected = asset.ext === ".exe" ? ["sherpa-onnx-offline.exe", "sherpa-onnx-offline"] : ["sherpa-onnx-offline"];
|
||||
moveExtractedBin(join(extractedRoot, "bin"), expected, finalPath);
|
||||
copyExtraSharedLibs(extractedRoot, asset.ext);
|
||||
const extractedRoot = join(binariesDir, assetName.replace(/\.tar\.bz2$/, ""));
|
||||
const expected =
|
||||
asset.ext === ".exe"
|
||||
? ["sherpa-onnx-offline.exe", "sherpa-onnx-offline"]
|
||||
: ["sherpa-onnx-offline"];
|
||||
moveExtractedBin(join(extractedRoot, "bin"), expected, finalPath);
|
||||
copyExtraSharedLibs(extractedRoot, asset.ext);
|
||||
|
||||
if (asset.ext === "") {
|
||||
try {
|
||||
chmodSync(finalPath, 0o755);
|
||||
} catch {
|
||||
// ignore em Windows
|
||||
}
|
||||
}
|
||||
if (asset.ext === "") {
|
||||
try {
|
||||
chmodSync(finalPath, 0o755);
|
||||
} catch {
|
||||
// ignore em Windows
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`✓ sidecar pronto em ${finalPath}`);
|
||||
console.log(`✓ sidecar pronto em ${finalPath}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(`falha: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
console.error(`falha: ${error.message}`);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
@@ -11,43 +11,50 @@ const repoRoot = join(__dirname, "..");
|
||||
const SUPPORTED_HOST = "x86_64-pc-windows-msvc";
|
||||
|
||||
function detectHostTriple() {
|
||||
try {
|
||||
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||
return line ? line.replace("host:", "").trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const output = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
|
||||
const line = output.split("\n").find((l) => l.startsWith("host:"));
|
||||
return line ? line.replace("host:", "").trim() : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
describe("setup-sidecar", () => {
|
||||
const hostTriple = detectHostTriple();
|
||||
const hostTriple = detectHostTriple();
|
||||
|
||||
beforeAll(() => {
|
||||
if (!hostTriple) {
|
||||
console.warn("rustc não disponível; pulando teste de sidecar");
|
||||
}
|
||||
});
|
||||
beforeAll(() => {
|
||||
if (!hostTriple) {
|
||||
console.warn("rustc não disponível; pulando teste de sidecar");
|
||||
}
|
||||
});
|
||||
|
||||
it("baixa o binário oficial e o coloca em src-tauri/binaries com o target triple", async () => {
|
||||
if (!hostTriple) return;
|
||||
if (hostTriple !== SUPPORTED_HOST) {
|
||||
console.log(`host triple ${hostTriple} fora da matriz de testes; pulando`);
|
||||
return;
|
||||
}
|
||||
it("baixa o binário oficial e o coloca em src-tauri/binaries com o target triple", async () => {
|
||||
if (!hostTriple) return;
|
||||
if (hostTriple !== SUPPORTED_HOST) {
|
||||
console.log(
|
||||
`host triple ${hostTriple} fora da matriz de testes; pulando`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const binaryPath = join(repoRoot, "src-tauri", "binaries", `sherpa-onnx-offline-${hostTriple}.exe`);
|
||||
const binaryPath = join(
|
||||
repoRoot,
|
||||
"src-tauri",
|
||||
"binaries",
|
||||
`sherpa-onnx-offline-${hostTriple}.exe`,
|
||||
);
|
||||
|
||||
if (existsSync(binaryPath)) {
|
||||
rmSync(binaryPath, { force: true });
|
||||
}
|
||||
if (existsSync(binaryPath)) {
|
||||
rmSync(binaryPath, { force: true });
|
||||
}
|
||||
|
||||
execFileSync("node", [join(repoRoot, "scripts", "setup-sidecar.mjs")], {
|
||||
stdio: "pipe",
|
||||
env: { ...process.env, SHERPA_ONNX_VERSION: "v1.12.23" },
|
||||
});
|
||||
execFileSync("node", [join(repoRoot, "scripts", "setup-sidecar.mjs")], {
|
||||
stdio: "pipe",
|
||||
env: { ...process.env, SHERPA_ONNX_VERSION: "v1.12.23" },
|
||||
});
|
||||
|
||||
expect(existsSync(binaryPath)).toBe(true);
|
||||
expect(statSync(binaryPath).size).toBeGreaterThan(1024 * 1024);
|
||||
}, 180_000);
|
||||
expect(existsSync(binaryPath)).toBe(true);
|
||||
expect(statSync(binaryPath).size).toBeGreaterThan(1024 * 1024);
|
||||
}, 180_000);
|
||||
});
|
||||
Reference in new issue
Block a user