mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-17 04:10:04 +02:00
Lot 9 : dictée vocale tenue par la façade (LOKI_DICTATE=rust)
La façade sert /api/transcribe et /api/dictate/* quand LOKI_DICTATE=rust (expérimental ; le Go reste le défaut) et supervise elle-même le serveur de transcription sherpa-onnx : lancé à la demande sur la boucle locale, GPU masqués et sans le jeton interne, prêt dès que sa socket répond, mort au démarrage expliquée, éteint après dix minutes d'inactivité ou quand les réglages changent. Modèles Parakeet téléchargés et décompressés au fil de l'eau (tar.bz2, entrées hors du dossier refusées, liens ignorés), protocole WebSocket identique (en-tête de 8 octets, trames de 10 Kio). Côté Go, facade_dictate.go remet ces routes à la façade quand elles arrivent par le tunnel du relais, et asrShutdownVite (libération de la VRAM, optimiseur) lui demande l'arrêt par l'API interne. Parité : parity_dictate_test.go tire du Go la lecture de treize WAV, les chemins d'archive, réglages, catalogue, arguments et treize appels de route, rejoués par tests/dictate.rs ; supervision testée contre un faux sherpa-onnx qui parle le protocole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016tcGfJWefW5kUGoC2wL8cv
This commit is contained in:
21 files changed
+2699
-19
No files matched your search
@@ -30,6 +30,8 @@ pub struct Ctx {
|
||||
/// catalogue Hugging Face) tenues par la façade (`LOKI_MODELS=rust`) ;
|
||||
/// `None` : au Go.
|
||||
pub models: Option<std::sync::Arc<crate::models::Models>>,
|
||||
/// Dictée vocale tenue par la façade (`LOKI_DICTATE=rust`) ; `None` : au Go.
|
||||
pub dictate: Option<std::sync::Arc<crate::dictate::Dictation>>,
|
||||
/// Jeton de l'API interne : une requête qui le porte vient du Go.
|
||||
pub internal_token: String,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,7 @@ pub mod auth;
|
||||
pub mod chat;
|
||||
pub mod chatapi;
|
||||
pub mod child;
|
||||
pub mod dictate;
|
||||
pub mod downloads;
|
||||
pub mod engine;
|
||||
pub mod engstatus;
|
||||
|
||||
@@ -121,6 +121,9 @@ fn serve() -> ExitCode {
|
||||
// LOKI_MODELS=rust : les routes des modèles aussi (le Go remet alors
|
||||
// celles du tunnel et ne vide plus les .part).
|
||||
let models_rust = models_held_by_facade();
|
||||
// LOKI_DICTATE=rust : la dictée vocale aussi (le Go lui demande alors
|
||||
// d'arrêter son serveur quand il libère la VRAM).
|
||||
let dictate_rust = held_by_facade("LOKI_DICTATE");
|
||||
let mut env = vec![(auth::INTERNAL_ENV.to_string(), token.clone())];
|
||||
if chat_rust {
|
||||
env.push(("LOKI_CHAT".into(), "rust".into()));
|
||||
@@ -128,7 +131,10 @@ fn serve() -> ExitCode {
|
||||
if models_rust {
|
||||
env.push(("LOKI_MODELS".into(), "rust".into()));
|
||||
}
|
||||
if chat_rust || models_rust {
|
||||
if dictate_rust {
|
||||
env.push(("LOKI_DICTATE".into(), "rust".into()));
|
||||
}
|
||||
if chat_rust || models_rust || dictate_rust {
|
||||
env.push(("LOKI_FACADE_ADDR".into(), format!("127.0.0.1:{web_port}")));
|
||||
}
|
||||
let sup = child::Supervisor::spawn(go_bin, go_port, env);
|
||||
@@ -167,6 +173,11 @@ fn serve() -> ExitCode {
|
||||
None
|
||||
};
|
||||
|
||||
let dictate = dictate_rust.then(|| {
|
||||
eprintln!("[loki-rs] dictée tenue par la façade (LOKI_DICTATE=rust)");
|
||||
std::sync::Arc::new(loki_rs::dictate::Dictation::new(&engine_paths.home))
|
||||
});
|
||||
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], web_port));
|
||||
let listener = match tokio::net::TcpListener::bind(addr).await {
|
||||
Ok(l) => l,
|
||||
@@ -184,6 +195,7 @@ fn serve() -> ExitCode {
|
||||
engine: engine_paths,
|
||||
chat,
|
||||
models,
|
||||
dictate,
|
||||
};
|
||||
let res = proxy::serve(listener, cfg, shutdown_signal()).await;
|
||||
eprintln!("[loki-rs] arrêt demandé — arrêt du serveur Go");
|
||||
@@ -205,7 +217,12 @@ fn chat_held_by_facade() -> bool {
|
||||
|
||||
/// `LOKI_MODELS=rust` : routes des modèles tenues par la façade.
|
||||
fn models_held_by_facade() -> bool {
|
||||
std::env::var("LOKI_MODELS").is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
||||
held_by_facade("LOKI_MODELS")
|
||||
}
|
||||
|
||||
/// `<VAR>=rust` : ce lot est tenu par la façade (le Go par défaut).
|
||||
fn held_by_facade(var: &str) -> bool {
|
||||
std::env::var(var).is_ok_and(|v| v.trim().eq_ignore_ascii_case("rust"))
|
||||
}
|
||||
|
||||
/// Les `.part` d'un téléchargement tué ne reprendront jamais : vidés au
|
||||
|
||||
@@ -396,6 +396,27 @@ pub fn base_name(p: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// `exec.LookPath` : le chemin du programme dans le PATH (exécutable).
|
||||
pub fn look_path(name: &str) -> Option<String> {
|
||||
let path = std::env::var_os("PATH")?;
|
||||
std::env::split_paths(&path)
|
||||
.map(|dir| dir.join(name))
|
||||
.find(|p| {
|
||||
std::fs::metadata(p).is_ok_and(|m| {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
m.is_file() && m.permissions().mode() & 0o111 != 0
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
m.is_file()
|
||||
}
|
||||
})
|
||||
})
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Message d'une `*os.PathError` du Go : « op chemin: raison ». La raison
|
||||
/// suit la forme des `syscall.Errno` du Go sous Unix (minuscule en tête, sans
|
||||
/// le « (os error N) » de Rust).
|
||||
|
||||
@@ -66,6 +66,9 @@ pub struct Config {
|
||||
/// Routes des modèles tenues par la façade (`LOKI_MODELS=rust`) : servies
|
||||
/// ici ; `None` : relayées au Go.
|
||||
pub models: Option<Arc<crate::models::Models>>,
|
||||
/// Dictée vocale tenue par la façade (`LOKI_DICTATE=rust`) : servie ici ;
|
||||
/// `None` : relayée au Go.
|
||||
pub dictate: Option<Arc<crate::dictate::Dictation>>,
|
||||
}
|
||||
|
||||
struct Proxy {
|
||||
@@ -94,6 +97,7 @@ pub async fn serve(
|
||||
engine: cfg.engine,
|
||||
chat: cfg.chat,
|
||||
models: cfg.models,
|
||||
dictate: cfg.dictate,
|
||||
internal_token: cfg.internal_token.clone(),
|
||||
},
|
||||
});
|
||||
@@ -133,10 +137,11 @@ pub async fn serve(
|
||||
impl Proxy {
|
||||
async fn handle(&self, mut req: Request<Incoming>) -> Response<Body> {
|
||||
// Le Go (et lui seul : jeton tiré à chaque démarrage) remet à la
|
||||
// façade le trafic du chat et des modèles qui lui arrive sans passer
|
||||
// par elle.
|
||||
let from_go = (self.ctx.chat.is_some() || self.ctx.models.is_some())
|
||||
&& self.ctx.from_go(req.headers());
|
||||
// façade le trafic du chat, des modèles et de la dictée qui lui arrive
|
||||
// sans passer par elle.
|
||||
let from_go =
|
||||
(self.ctx.chat.is_some() || self.ctx.models.is_some() || self.ctx.dictate.is_some())
|
||||
&& self.ctx.from_go(req.headers());
|
||||
// Jamais de confiance accordée sur la foi d'un en-tête du client :
|
||||
// ni « je viens du tunnel », ni « je suis la façade ».
|
||||
req.headers_mut().remove(VIA_TUNNEL);
|
||||
@@ -150,6 +155,13 @@ impl Proxy {
|
||||
{
|
||||
return chatapi::serve_internal(chat, req).await;
|
||||
}
|
||||
if from_go
|
||||
&& let Some(d) = &self.ctx.dictate
|
||||
&& path == crate::dictate::INTERNAL_STOP
|
||||
{
|
||||
d.shutdown_fast().await;
|
||||
return plain(StatusCode::NO_CONTENT, "");
|
||||
}
|
||||
// L'API interne du Go n'est jamais joignable de l'extérieur.
|
||||
if path.starts_with("/internal/") {
|
||||
return plain(StatusCode::NOT_FOUND, "404 page not found");
|
||||
@@ -169,6 +181,11 @@ impl Proxy {
|
||||
{
|
||||
return chatapi::serve(&self.ctx, chat, req, from_go).await;
|
||||
}
|
||||
if let Some(d) = &self.ctx.dictate
|
||||
&& crate::dictate::handles(path)
|
||||
{
|
||||
return crate::dictate::serve(&self.ctx, d, req, from_go).await;
|
||||
}
|
||||
if let Some(m) = &self.ctx.models
|
||||
&& crate::models::handles(path)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user