feat(etapa3): modelo parakeet-tdt-0.6b-v3-int8 como padrão, GGUF legado mantido
This commit is contained in:
1 parent
695a0d5c42
commit
c142a11ed7
2 files changed
+308
-67
No files matched your search
+298
-62
@@ -6,8 +6,34 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::error::{AppError, AppResult};
|
use crate::error::{AppError, AppResult};
|
||||||
|
|
||||||
pub const DEFAULT_MODEL_ID: &str = "parakeet-tdt-0.6b-v3-q8_0";
|
/// Modelo padrão usado pelo `SherpaOnnxTranscriber` (Etapa 2).
|
||||||
pub const DEFAULT_MODEL_URL: &str = "https://huggingface.co/cstr/parakeet-tdt-0.6b-v3-GGUF/resolve/main/parakeet-tdt-0.6b-v3-q8_0.gguf";
|
/// É um pacote multi-arquivo do release `asr-models` do k2-fsa/sherpa-onnx.
|
||||||
|
pub const SHERPA_MODEL_ID: &str = "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8";
|
||||||
|
|
||||||
|
/// ID legado do modelo GGUF (mantido para o botão "Baixar modelo" não
|
||||||
|
/// quebrar builds existentes que ainda apontam para o catálogo antigo).
|
||||||
|
pub const GGUF_MODEL_ID: &str = "parakeet-tdt-0.6b-v3-q8_0";
|
||||||
|
pub const GGUF_MODEL_URL: &str = "https://huggingface.co/cstr/parakeet-tdt-0.6b-v3-GGUF/resolve/main/parakeet-tdt-0.6b-v3-q8_0.gguf";
|
||||||
|
|
||||||
|
/// ID canônico de modelo a usar como "principal" (o que o app baixa por padrão).
|
||||||
|
pub const DEFAULT_MODEL_ID: &str = SHERPA_MODEL_ID;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(tag = "kind", rename_all = "camelCase")]
|
||||||
|
pub enum ModelFormat {
|
||||||
|
/// Arquivo único (ex.: `.gguf`).
|
||||||
|
SingleFile { filename: String, size_bytes: u64 },
|
||||||
|
/// Pacote multi-arquivo (ex.: sherpa-onnx v3 int8 com 4 ONNX).
|
||||||
|
/// `archive_url` é o tarball `.tar.bz2`; `archive_size_bytes` é o tamanho esperado;
|
||||||
|
/// `extracted_subdir` é o subdiretório dentro do tarball;
|
||||||
|
/// `required_files` é a lista de arquivos esperados pós-extração.
|
||||||
|
Archive {
|
||||||
|
archive_url: String,
|
||||||
|
archive_size_bytes: u64,
|
||||||
|
extracted_subdir: String,
|
||||||
|
required_files: Vec<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@@ -15,10 +41,8 @@ pub struct OfflineModel {
|
|||||||
pub id: String,
|
pub id: String,
|
||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
pub repository: String,
|
pub repository: String,
|
||||||
pub filename: String,
|
|
||||||
pub quantization: String,
|
pub quantization: String,
|
||||||
pub size_bytes: u64,
|
pub format: ModelFormat,
|
||||||
pub download_url: String,
|
|
||||||
pub languages: Vec<String>,
|
pub languages: Vec<String>,
|
||||||
pub recommended_for_simple_pc: bool,
|
pub recommended_for_simple_pc: bool,
|
||||||
}
|
}
|
||||||
@@ -36,7 +60,8 @@ pub enum ModelInstallStatus {
|
|||||||
pub struct ModelWithStatus {
|
pub struct ModelWithStatus {
|
||||||
pub model: OfflineModel,
|
pub model: OfflineModel,
|
||||||
pub status: ModelInstallStatus,
|
pub status: ModelInstallStatus,
|
||||||
pub local_path: Option<PathBuf>,
|
/// Diretório onde o modelo está (ou deveria estar) instalado.
|
||||||
|
pub local_dir: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -48,14 +73,24 @@ pub struct ModelDownloadResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn catalog() -> Vec<OfflineModel> {
|
pub fn catalog() -> Vec<OfflineModel> {
|
||||||
vec![OfflineModel {
|
vec![
|
||||||
id: DEFAULT_MODEL_ID.to_string(),
|
OfflineModel {
|
||||||
display_name: "Parakeet TDT 0.6B v3 Q8".to_string(),
|
id: SHERPA_MODEL_ID.to_string(),
|
||||||
repository: "cstr/parakeet-tdt-0.6b-v3-GGUF".to_string(),
|
display_name: "Parakeet TDT 0.6B v3 (sherpa-onnx int8)".to_string(),
|
||||||
filename: "parakeet-tdt-0.6b-v3-q8_0.gguf".to_string(),
|
repository: "k2-fsa/sherpa-onnx asr-models".to_string(),
|
||||||
quantization: "Q8_0".to_string(),
|
quantization: "int8".to_string(),
|
||||||
size_bytes: 745_121_600,
|
format: ModelFormat::Archive {
|
||||||
download_url: DEFAULT_MODEL_URL.to_string(),
|
archive_url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8.tar.bz2"
|
||||||
|
.to_string(),
|
||||||
|
archive_size_bytes: 670_000_000,
|
||||||
|
extracted_subdir: "sherpa-onnx-nemo-parakeet-tdt-0.6b-v3-int8".to_string(),
|
||||||
|
required_files: vec![
|
||||||
|
"encoder.int8.onnx".to_string(),
|
||||||
|
"decoder.int8.onnx".to_string(),
|
||||||
|
"joiner.int8.onnx".to_string(),
|
||||||
|
"tokens.txt".to_string(),
|
||||||
|
],
|
||||||
|
},
|
||||||
languages: vec![
|
languages: vec![
|
||||||
"bg", "hr", "cs", "da", "nl", "en", "et", "fi", "fr", "de", "el", "hu", "it", "lv",
|
"bg", "hr", "cs", "da", "nl", "en", "et", "fi", "fr", "de", "el", "hu", "it", "lv",
|
||||||
"lt", "mt", "pl", "pt", "ro", "sk", "sl", "es", "sv", "ru", "uk",
|
"lt", "mt", "pl", "pt", "ro", "sk", "sl", "es", "sv", "ru", "uk",
|
||||||
@@ -64,34 +99,74 @@ pub fn catalog() -> Vec<OfflineModel> {
|
|||||||
.map(str::to_string)
|
.map(str::to_string)
|
||||||
.collect(),
|
.collect(),
|
||||||
recommended_for_simple_pc: true,
|
recommended_for_simple_pc: true,
|
||||||
}]
|
},
|
||||||
|
// Modelo legado GGUF (CrispASR). Continua aparecendo no catálogo para
|
||||||
|
// o botão "Baixar modelo" não quebrar builds existentes.
|
||||||
|
OfflineModel {
|
||||||
|
id: GGUF_MODEL_ID.to_string(),
|
||||||
|
display_name: "Parakeet TDT 0.6B v3 (GGUF Q8 - legados)".to_string(),
|
||||||
|
repository: "cstr/parakeet-tdt-0.6b-v3-GGUF".to_string(),
|
||||||
|
quantization: "Q8_0".to_string(),
|
||||||
|
format: ModelFormat::SingleFile {
|
||||||
|
filename: "parakeet-tdt-0.6b-v3-q8_0.gguf".to_string(),
|
||||||
|
size_bytes: 745_121_600,
|
||||||
|
},
|
||||||
|
languages: vec!["pt".to_string(), "en".to_string()],
|
||||||
|
recommended_for_simple_pc: false,
|
||||||
|
},
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model_statuses(model_dir: &Path) -> Vec<ModelWithStatus> {
|
pub fn model_statuses(model_dir: &Path) -> Vec<ModelWithStatus> {
|
||||||
catalog()
|
catalog()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|model| {
|
.map(|model| {
|
||||||
let path = model_dir.join(&model.filename);
|
let local_dir = model_install_dir(model_dir, &model);
|
||||||
let status = status_for_path(&path, model.size_bytes);
|
let status = status_for_model(&local_dir, &model);
|
||||||
ModelWithStatus {
|
ModelWithStatus {
|
||||||
model,
|
model,
|
||||||
local_path: path.exists().then_some(path),
|
|
||||||
status,
|
status,
|
||||||
|
local_dir,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_for_path(path: &Path, expected_size: u64) -> ModelInstallStatus {
|
fn model_install_dir(model_dir_root: &Path, model: &OfflineModel) -> PathBuf {
|
||||||
match fs::metadata(path) {
|
model_dir_root.join(&model.id)
|
||||||
Ok(metadata) if metadata.len() >= expected_size => ModelInstallStatus::Ready,
|
}
|
||||||
|
|
||||||
|
fn status_for_model(install_dir: &Path, model: &OfflineModel) -> ModelInstallStatus {
|
||||||
|
match &model.format {
|
||||||
|
ModelFormat::SingleFile {
|
||||||
|
filename,
|
||||||
|
size_bytes,
|
||||||
|
} => {
|
||||||
|
let path = install_dir.join(filename);
|
||||||
|
match fs::metadata(&path) {
|
||||||
|
Ok(metadata) if metadata.len() >= *size_bytes => ModelInstallStatus::Ready,
|
||||||
Ok(metadata) => ModelInstallStatus::Error(format!(
|
Ok(metadata) => ModelInstallStatus::Error(format!(
|
||||||
"arquivo incompleto: {} de {} bytes",
|
"arquivo incompleto: {} de {} bytes",
|
||||||
metadata.len(),
|
metadata.len(),
|
||||||
expected_size
|
size_bytes
|
||||||
)),
|
)),
|
||||||
Err(_) => ModelInstallStatus::NotInstalled,
|
Err(_) => ModelInstallStatus::NotInstalled,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ModelFormat::Archive { required_files, .. } => {
|
||||||
|
let mut missing: Vec<&str> = Vec::new();
|
||||||
|
for file in required_files {
|
||||||
|
if !install_dir.join(file).exists() {
|
||||||
|
missing.push(file.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if missing.is_empty() {
|
||||||
|
ModelInstallStatus::Ready
|
||||||
|
} else {
|
||||||
|
ModelInstallStatus::Error(format!("arquivos ausentes: {}", missing.join(", ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_model() -> OfflineModel {
|
pub fn default_model() -> OfflineModel {
|
||||||
@@ -101,26 +176,60 @@ pub fn default_model() -> OfflineModel {
|
|||||||
.expect("catálogo deve conter o modelo padrão")
|
.expect("catálogo deve conter o modelo padrão")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_model_path(model_dir: &Path) -> PathBuf {
|
/// Retorna o diretório onde o modelo sherpa-onnx v3 int8 deve estar
|
||||||
model_dir.join(default_model().filename)
|
/// instalado (com os 4 arquivos ONNX dentro).
|
||||||
|
pub fn sherpa_model_dir(model_dir_root: &Path) -> PathBuf {
|
||||||
|
model_install_dir(model_dir_root, &default_model())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Retorna o caminho do arquivo `.gguf` legado (se ainda em uso).
|
||||||
|
pub fn gguf_model_path(model_dir_root: &Path) -> PathBuf {
|
||||||
|
model_dir_root
|
||||||
|
.join(GGUF_MODEL_ID)
|
||||||
|
.join("parakeet-tdt-0.6b-v3-q8_0.gguf")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn download_default_model(model_dir: &Path) -> AppResult<ModelDownloadResult> {
|
pub fn download_default_model(model_dir: &Path) -> AppResult<ModelDownloadResult> {
|
||||||
let model = default_model();
|
let model = default_model();
|
||||||
fs::create_dir_all(model_dir).map_err(|error| AppError::Io(error.to_string()))?;
|
let install_dir = model_install_dir(model_dir, &model);
|
||||||
|
fs::create_dir_all(&install_dir).map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
|
||||||
let final_path = model_dir.join(&model.filename);
|
match &model.format {
|
||||||
let temp_path = model_dir.join(format!("{}.partial", model.filename));
|
ModelFormat::SingleFile {
|
||||||
|
filename,
|
||||||
|
size_bytes,
|
||||||
|
} => download_single_file(&model, &install_dir, filename, *size_bytes),
|
||||||
|
ModelFormat::Archive {
|
||||||
|
archive_url,
|
||||||
|
archive_size_bytes,
|
||||||
|
extracted_subdir,
|
||||||
|
required_files,
|
||||||
|
} => download_archive(
|
||||||
|
&model,
|
||||||
|
&install_dir,
|
||||||
|
archive_url,
|
||||||
|
*archive_size_bytes,
|
||||||
|
extracted_subdir,
|
||||||
|
required_files,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut response = reqwest::blocking::get(&model.download_url)
|
fn download_single_file(
|
||||||
|
model: &OfflineModel,
|
||||||
|
install_dir: &Path,
|
||||||
|
filename: &str,
|
||||||
|
expected_size: u64,
|
||||||
|
) -> AppResult<ModelDownloadResult> {
|
||||||
|
let final_path = install_dir.join(filename);
|
||||||
|
let temp_path = install_dir.join(format!("{filename}.partial"));
|
||||||
|
let mut response = reqwest::blocking::get(model.download_url_if_single())
|
||||||
.map_err(|error| AppError::Network(error.to_string()))?
|
.map_err(|error| AppError::Network(error.to_string()))?
|
||||||
.error_for_status()
|
.error_for_status()
|
||||||
.map_err(|error| AppError::Network(error.to_string()))?;
|
.map_err(|error| AppError::Network(error.to_string()))?;
|
||||||
|
|
||||||
let mut file = fs::File::create(&temp_path).map_err(|error| AppError::Io(error.to_string()))?;
|
let mut file = fs::File::create(&temp_path).map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
let mut buffer = [0_u8; 1024 * 128];
|
let mut buffer = [0_u8; 1024 * 128];
|
||||||
let mut bytes_written = 0_u64;
|
let mut bytes_written = 0_u64;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let read = response
|
let read = response
|
||||||
.read(&mut buffer)
|
.read(&mut buffer)
|
||||||
@@ -132,62 +241,189 @@ pub fn download_default_model(model_dir: &Path) -> AppResult<ModelDownloadResult
|
|||||||
.map_err(|error| AppError::Io(error.to_string()))?;
|
.map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
bytes_written += read as u64;
|
bytes_written += read as u64;
|
||||||
}
|
}
|
||||||
|
|
||||||
file.flush()
|
file.flush()
|
||||||
.map_err(|error| AppError::Io(error.to_string()))?;
|
.map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
if bytes_written < expected_size {
|
||||||
if bytes_written < model.size_bytes {
|
|
||||||
let _ = fs::remove_file(&temp_path);
|
let _ = fs::remove_file(&temp_path);
|
||||||
return Err(AppError::InvalidModel(format!(
|
return Err(AppError::InvalidModel(format!(
|
||||||
"download incompleto: {bytes_written} de {} bytes",
|
"download incompleto: {bytes_written} de {expected_size} bytes"
|
||||||
model.size_bytes
|
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::rename(&temp_path, &final_path).map_err(|error| AppError::Io(error.to_string()))?;
|
fs::rename(&temp_path, &final_path).map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
|
||||||
Ok(ModelDownloadResult {
|
Ok(ModelDownloadResult {
|
||||||
model_id: model.id,
|
model_id: model.id.clone(),
|
||||||
path: final_path,
|
path: final_path,
|
||||||
bytes_written,
|
bytes_written,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn download_archive(
|
||||||
|
model: &OfflineModel,
|
||||||
|
install_dir: &Path,
|
||||||
|
archive_url: &str,
|
||||||
|
expected_archive_size: u64,
|
||||||
|
extracted_subdir: &str,
|
||||||
|
required_files: &[String],
|
||||||
|
) -> AppResult<ModelDownloadResult> {
|
||||||
|
let temp_archive = install_dir.join("archive.tar.bz2");
|
||||||
|
let mut response = reqwest::blocking::get(archive_url)
|
||||||
|
.map_err(|error| AppError::Network(error.to_string()))?
|
||||||
|
.error_for_status()
|
||||||
|
.map_err(|error| AppError::Network(error.to_string()))?;
|
||||||
|
let mut file =
|
||||||
|
fs::File::create(&temp_archive).map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
let mut buffer = [0_u8; 1024 * 128];
|
||||||
|
let mut bytes_written = 0_u64;
|
||||||
|
loop {
|
||||||
|
let read = response
|
||||||
|
.read(&mut buffer)
|
||||||
|
.map_err(|error| AppError::Network(error.to_string()))?;
|
||||||
|
if read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
file.write_all(&buffer[..read])
|
||||||
|
.map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
bytes_written += read as u64;
|
||||||
|
}
|
||||||
|
file.flush()
|
||||||
|
.map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
if bytes_written < expected_archive_size / 2 {
|
||||||
|
// tolerância: tarballs do HF às vezes têm tamanho declarado errado
|
||||||
|
let _ = fs::remove_file(&temp_archive);
|
||||||
|
return Err(AppError::InvalidModel(format!(
|
||||||
|
"download incompleto: {bytes_written} bytes (esperado ~{expected_archive_size})"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extrai o tarball usando `tar` do sistema (funciona em Windows+Linux+macOS).
|
||||||
|
let sep = std::path::MAIN_SEPARATOR.to_string();
|
||||||
|
let prefix = install_dir.to_string_lossy().to_string();
|
||||||
|
let prefix_with_sep = if prefix.ends_with(&sep) {
|
||||||
|
prefix.clone()
|
||||||
|
} else {
|
||||||
|
format!("{prefix}{sep}")
|
||||||
|
};
|
||||||
|
let rel_archive = temp_archive
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
.strip_prefix(&prefix_with_sep)
|
||||||
|
.map(str::to_string)
|
||||||
|
.unwrap_or_else(|| temp_archive.to_string_lossy().to_string());
|
||||||
|
let status = std::process::Command::new("tar")
|
||||||
|
.arg("-xjf")
|
||||||
|
.arg(&rel_archive)
|
||||||
|
.current_dir(install_dir)
|
||||||
|
.status()
|
||||||
|
.map_err(|error| AppError::Io(format!("falha ao executar tar: {error}")))?;
|
||||||
|
if !status.success() {
|
||||||
|
let _ = fs::remove_file(&temp_archive);
|
||||||
|
return Err(AppError::Io(format!(
|
||||||
|
"tar falhou com status {status} ao extrair {rel_archive}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let _ = fs::remove_file(&temp_archive);
|
||||||
|
|
||||||
|
// Move os arquivos extraídos de `install_dir/<subdir>/...` para `install_dir/...`
|
||||||
|
// para que `sherpa-onnx-offline --tokens=install_dir/tokens.txt` funcione.
|
||||||
|
let src_dir = install_dir.join(extracted_subdir);
|
||||||
|
if !src_dir.exists() {
|
||||||
|
return Err(AppError::InvalidModel(format!(
|
||||||
|
"subdiretório esperado {extracted_subdir:?} não encontrado após extração"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
for file in required_files {
|
||||||
|
let from = src_dir.join(file);
|
||||||
|
let to = install_dir.join(file);
|
||||||
|
if from.exists() {
|
||||||
|
if to.exists() {
|
||||||
|
let _ = fs::remove_file(&to);
|
||||||
|
}
|
||||||
|
fs::rename(&from, &to).map_err(|error| AppError::Io(error.to_string()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&src_dir);
|
||||||
|
|
||||||
|
Ok(ModelDownloadResult {
|
||||||
|
model_id: model.id.clone(),
|
||||||
|
path: install_dir.to_path_buf(),
|
||||||
|
bytes_written,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OfflineModel {
|
||||||
|
/// URL de download para modelos SingleFile. Mantida para retrocompatibilidade.
|
||||||
|
pub fn download_url_if_single(&self) -> String {
|
||||||
|
if let ModelFormat::SingleFile { filename: _, .. } = &self.format {
|
||||||
|
// O GGUF legado continua no HF; o sherpa-onnx já tem seu próprio caminho
|
||||||
|
// via Archive. Para SingleFile, usamos a URL do GGUF por id.
|
||||||
|
match self.id.as_str() {
|
||||||
|
GGUF_MODEL_ID => GGUF_MODEL_URL.to_string(),
|
||||||
|
_ => String::new(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn catalog_contains_parakeet_q8_model() {
|
fn catalog_contains_sherpa_parakeet_v3() {
|
||||||
let model = default_model();
|
let model = default_model();
|
||||||
|
assert_eq!(model.id, SHERPA_MODEL_ID);
|
||||||
assert_eq!(model.repository, "cstr/parakeet-tdt-0.6b-v3-GGUF");
|
assert_eq!(model.quantization, "int8");
|
||||||
assert_eq!(model.filename, "parakeet-tdt-0.6b-v3-q8_0.gguf");
|
assert!(matches!(model.format, ModelFormat::Archive { .. }));
|
||||||
assert_eq!(model.quantization, "Q8_0");
|
|
||||||
assert_eq!(model.download_url, DEFAULT_MODEL_URL);
|
|
||||||
assert!(model.languages.contains(&"pt".to_string()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn status_detects_ready_model_by_size() {
|
fn catalog_keeps_gguf_legacy() {
|
||||||
let unique = SystemTime::now()
|
let gguf = catalog()
|
||||||
.duration_since(UNIX_EPOCH)
|
.into_iter()
|
||||||
.map(|duration| duration.as_nanos())
|
.find(|model| model.id == GGUF_MODEL_ID)
|
||||||
|
.expect("GGUF legado deve continuar no catálogo");
|
||||||
|
assert!(matches!(gguf.format, ModelFormat::SingleFile { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sherpa_model_dir_returns_path() {
|
||||||
|
let root = std::env::temp_dir();
|
||||||
|
let dir = sherpa_model_dir(&root);
|
||||||
|
assert!(dir.ends_with(SHERPA_MODEL_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn archive_model_status_reports_missing_files() {
|
||||||
|
let unique = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let dir = std::env::temp_dir().join(format!("transcript-model-test-{unique}"));
|
let dir = std::env::temp_dir().join(format!("transcript-archive-test-{unique}"));
|
||||||
fs::create_dir_all(&dir).unwrap_or_else(|error| panic!("create temp dir: {error}"));
|
fs::create_dir_all(&dir).unwrap_or_else(|error| panic!("create dir: {error}"));
|
||||||
let path = dir.join("model.gguf");
|
let model = default_model();
|
||||||
fs::write(&path, [1_u8, 2, 3, 4]).unwrap_or_else(|error| panic!("write model: {error}"));
|
let status = status_for_model(&dir, &model);
|
||||||
|
assert!(matches!(status, ModelInstallStatus::Error(_)));
|
||||||
|
|
||||||
assert_eq!(status_for_path(&path, 4), ModelInstallStatus::Ready);
|
// cria o primeiro arquivo e checa de novo
|
||||||
assert!(matches!(
|
if let ModelFormat::Archive { required_files, .. } = &model.format {
|
||||||
status_for_path(&path, 5),
|
fs::write(dir.join(&required_files[0]), b"placeholder")
|
||||||
ModelInstallStatus::Error(_)
|
.unwrap_or_else(|error| panic!("write file: {error}"));
|
||||||
));
|
}
|
||||||
|
let status = status_for_model(&dir, &model);
|
||||||
|
assert!(matches!(status, ModelInstallStatus::Error(_)));
|
||||||
|
|
||||||
let _ = fs::remove_file(path);
|
// cria todos e checa Ready
|
||||||
let _ = fs::remove_dir(dir);
|
if let ModelFormat::Archive { required_files, .. } = &model.format {
|
||||||
|
for file in required_files {
|
||||||
|
fs::write(dir.join(file), b"x")
|
||||||
|
.unwrap_or_else(|error| panic!("write {file}: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let status = status_for_model(&dir, &model);
|
||||||
|
assert_eq!(status, ModelInstallStatus::Ready);
|
||||||
|
|
||||||
|
let _ = fs::remove_dir_all(dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,7 @@ use crate::app_state::{AppSnapshot, ManagedAppState};
|
|||||||
use crate::diagnostics::{audio_or_error, log_attempt, tail, TranscriptionAttempt};
|
use crate::diagnostics::{audio_or_error, log_attempt, tail, TranscriptionAttempt};
|
||||||
use crate::injection::{FallbackInjector, InsertMethod, InsertRequest, InsertResult, TextInjector};
|
use crate::injection::{FallbackInjector, InsertMethod, InsertRequest, InsertResult, TextInjector};
|
||||||
use crate::models::{
|
use crate::models::{
|
||||||
default_model_path, download_default_model, model_statuses, ModelDownloadResult,
|
download_default_model, model_statuses, ModelDownloadResult,
|
||||||
ModelWithStatus,
|
ModelWithStatus,
|
||||||
};
|
};
|
||||||
use crate::settings::AppSettings;
|
use crate::settings::AppSettings;
|
||||||
@@ -61,7 +61,7 @@ pub(crate) fn stop_recording_and_transcribe(
|
|||||||
audio_path: audio_path.clone(),
|
audio_path: audio_path.clone(),
|
||||||
audio_size_bytes: 0,
|
audio_size_bytes: 0,
|
||||||
sidecar: "parakeet-main".to_string(),
|
sidecar: "parakeet-main".to_string(),
|
||||||
model: default_model_path(&state.model_dir).display().to_string(),
|
model: state.sherpa_model_dir().display().to_string(),
|
||||||
exit_code: None,
|
exit_code: None,
|
||||||
stdout_tail: String::new(),
|
stdout_tail: String::new(),
|
||||||
stderr_tail: String::new(),
|
stderr_tail: String::new(),
|
||||||
@@ -105,7 +105,7 @@ pub(crate) fn stop_recording_and_transcribe(
|
|||||||
audio_path: audio_path.clone(),
|
audio_path: audio_path.clone(),
|
||||||
audio_size_bytes: audio_size,
|
audio_size_bytes: audio_size,
|
||||||
sidecar: "parakeet-main".to_string(),
|
sidecar: "parakeet-main".to_string(),
|
||||||
model: default_model_path(&state.model_dir).display().to_string(),
|
model: state.sherpa_model_dir().display().to_string(),
|
||||||
exit_code: None,
|
exit_code: None,
|
||||||
stdout_tail: tail(&result.text, 1024),
|
stdout_tail: tail(&result.text, 1024),
|
||||||
stderr_tail: String::new(),
|
stderr_tail: String::new(),
|
||||||
@@ -126,7 +126,7 @@ pub(crate) fn stop_recording_and_transcribe(
|
|||||||
audio_path: audio_path.clone(),
|
audio_path: audio_path.clone(),
|
||||||
audio_size_bytes: audio_size,
|
audio_size_bytes: audio_size,
|
||||||
sidecar: "parakeet-main".to_string(),
|
sidecar: "parakeet-main".to_string(),
|
||||||
model: default_model_path(&state.model_dir).display().to_string(),
|
model: state.sherpa_model_dir().display().to_string(),
|
||||||
exit_code: None,
|
exit_code: None,
|
||||||
stdout_tail: String::new(),
|
stdout_tail: String::new(),
|
||||||
stderr_tail: String::new(),
|
stderr_tail: String::new(),
|
||||||
@@ -241,7 +241,12 @@ mod tests {
|
|||||||
// transcriber não falhar antes de invocar o sidecar inexistente.
|
// transcriber não falhar antes de invocar o sidecar inexistente.
|
||||||
let model_dir = unique_path("sherpa-empty-model");
|
let model_dir = unique_path("sherpa-empty-model");
|
||||||
std::fs::create_dir_all(&model_dir).unwrap_or_else(|error| panic!("mkdir model: {error}"));
|
std::fs::create_dir_all(&model_dir).unwrap_or_else(|error| panic!("mkdir model: {error}"));
|
||||||
for name in ["tokens.txt", "encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx"] {
|
for name in [
|
||||||
|
"tokens.txt",
|
||||||
|
"encoder.int8.onnx",
|
||||||
|
"decoder.int8.onnx",
|
||||||
|
"joiner.int8.onnx",
|
||||||
|
] {
|
||||||
std::fs::write(model_dir.join(name), b"placeholder")
|
std::fs::write(model_dir.join(name), b"placeholder")
|
||||||
.unwrap_or_else(|error| panic!("write {name}: {error}"));
|
.unwrap_or_else(|error| panic!("write {name}: {error}"));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user