Voice (dictate, read aloud) and Draw a picture over /v1, with the app's models scope
The server kept voice and images on /v1 (models scope, which the app's token already asks for); /me's services and scopes decide what is offered. Rust sends the recording to /v1/audio/transcriptions, reads /v1/audio/speech back as a data: URL (CSP media-src data:), and for /v1/images/generations takes only the id of each of the person's own files from the answer and fetches /v1/files/<id>/content from the app's own server. The microphone is allowed only within seconds of the page's own Dictate (voice_arm); WebKitGTK's media stream is switched on through its settings getter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
16 files changed
+1024
-4
No files matched your search
@@ -38,6 +38,12 @@ All notable changes to LLeMbas Desktop. The format follows
|
|||||||
- **A chat's tools and skills.** Tools and skills… in a chat's menu switches the tool families
|
- **A chat's tools and skills.** Tools and skills… in a chat's menu switches the tool families
|
||||||
(web search among them) and the skills on or off for that chat only, as the web's chat settings
|
(web search among them) and the skills on or off for that chat only, as the web's chat settings
|
||||||
do, and lists the commands it was told to always allow, with Ask me about these again.
|
do, and lists the commands it was told to always allow, with Ask me about these again.
|
||||||
|
- **Voice.** Dictate, beside Send, records a message and writes down what was said where the
|
||||||
|
cursor is, through the server's speech to text; a reply's Read this aloud reads it in the
|
||||||
|
account's own voice (without its code), and stops when pressed again. Each shows only where the
|
||||||
|
server has it on for the account. The microphone is allowed only right after Dictate is pressed.
|
||||||
|
- **Draw a picture.** From Ctrl+K, where the server can draw for the account: a prompt and a
|
||||||
|
shape, and the picture under it. Only the account's own pictures on its own server are shown.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -504,6 +504,22 @@ KEYS = (
|
|||||||
"Always allowed here",
|
"Always allowed here",
|
||||||
"Ask me about these again",
|
"Ask me about these again",
|
||||||
"Nothing to switch in this chat.",
|
"Nothing to switch in this chat.",
|
||||||
|
# voice.js, draw.js: dictation, read aloud, and drawing a picture
|
||||||
|
"Dictate a message",
|
||||||
|
"Stop dictating",
|
||||||
|
"Writing down…",
|
||||||
|
"The microphone could not be opened: %(why)s",
|
||||||
|
"Read this aloud",
|
||||||
|
"The server's answer was not audio.",
|
||||||
|
"Draw a picture",
|
||||||
|
"What to draw…",
|
||||||
|
"Shape",
|
||||||
|
"Square",
|
||||||
|
"Wide",
|
||||||
|
"Tall",
|
||||||
|
"Draw",
|
||||||
|
"Drawing… a picture can take a minute or two.",
|
||||||
|
"The server's answer had no picture in it.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
|
# The app's own sentences. The mode names (Manual, Edit, Auto, Plan) stay English, as the
|
||||||
@@ -761,6 +777,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Zatím si nic nepamatuje.",
|
"Nothing remembered yet.": "Zatím si nic nepamatuje.",
|
||||||
"Tools and skills…": "Nástroje a dovednosti…",
|
"Tools and skills…": "Nástroje a dovednosti…",
|
||||||
"Nothing to switch in this chat.": "V této konverzaci není co přepnout.",
|
"Nothing to switch in this chat.": "V této konverzaci není co přepnout.",
|
||||||
|
"Stop dictating": "Zastavit diktování",
|
||||||
|
"Writing down…": "Zapisuji…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Mikrofon se nepodařilo otevřít: %(why)s",
|
||||||
|
"Read this aloud": "Přečíst nahlas",
|
||||||
|
"The server's answer was not audio.": "Odpověď serveru nebyla zvuk.",
|
||||||
|
"What to draw…": "Co nakreslit…",
|
||||||
|
"Shape": "Tvar",
|
||||||
|
"Square": "Čtverec",
|
||||||
|
"Wide": "Široký",
|
||||||
|
"Tall": "Vysoký",
|
||||||
|
"Draw": "Nakreslit",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Kreslím… obrázek může trvat minutu nebo dvě.",
|
||||||
|
"The server's answer had no picture in it.": "V odpovědi serveru nebyl žádný obrázek.",
|
||||||
},
|
},
|
||||||
"de": {
|
"de": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Melden Sie sich erneut an: Der Server nimmt die Anmeldung dieser App nicht mehr an.",
|
||||||
@@ -1013,6 +1042,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Noch nichts behalten.",
|
"Nothing remembered yet.": "Noch nichts behalten.",
|
||||||
"Tools and skills…": "Werkzeuge und Fähigkeiten…",
|
"Tools and skills…": "Werkzeuge und Fähigkeiten…",
|
||||||
"Nothing to switch in this chat.": "In diesem Chat gibt es nichts umzuschalten.",
|
"Nothing to switch in this chat.": "In diesem Chat gibt es nichts umzuschalten.",
|
||||||
|
"Stop dictating": "Diktieren beenden",
|
||||||
|
"Writing down…": "Wird aufgeschrieben…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Das Mikrofon konnte nicht geöffnet werden: %(why)s",
|
||||||
|
"Read this aloud": "Vorlesen",
|
||||||
|
"The server's answer was not audio.": "Die Antwort des Servers war kein Audio.",
|
||||||
|
"What to draw…": "Was gezeichnet werden soll…",
|
||||||
|
"Shape": "Form",
|
||||||
|
"Square": "Quadratisch",
|
||||||
|
"Wide": "Breit",
|
||||||
|
"Tall": "Hoch",
|
||||||
|
"Draw": "Zeichnen",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Wird gezeichnet… ein Bild kann ein bis zwei Minuten dauern.",
|
||||||
|
"The server's answer had no picture in it.": "Die Antwort des Servers enthielt kein Bild.",
|
||||||
},
|
},
|
||||||
"es": {
|
"es": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Vuelva a iniciar sesión: el servidor ya no acepta el inicio de sesión de esta aplicación.",
|
||||||
@@ -1265,6 +1307,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Todavía no se recuerda nada.",
|
"Nothing remembered yet.": "Todavía no se recuerda nada.",
|
||||||
"Tools and skills…": "Herramientas y habilidades…",
|
"Tools and skills…": "Herramientas y habilidades…",
|
||||||
"Nothing to switch in this chat.": "No hay nada que activar o desactivar en este chat.",
|
"Nothing to switch in this chat.": "No hay nada que activar o desactivar en este chat.",
|
||||||
|
"Stop dictating": "Dejar de dictar",
|
||||||
|
"Writing down…": "Transcribiendo…",
|
||||||
|
"The microphone could not be opened: %(why)s": "No se pudo abrir el micrófono: %(why)s",
|
||||||
|
"Read this aloud": "Leer en voz alta",
|
||||||
|
"The server's answer was not audio.": "La respuesta del servidor no era audio.",
|
||||||
|
"What to draw…": "Qué dibujar…",
|
||||||
|
"Shape": "Forma",
|
||||||
|
"Square": "Cuadrada",
|
||||||
|
"Wide": "Ancha",
|
||||||
|
"Tall": "Alta",
|
||||||
|
"Draw": "Dibujar",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Dibujando… una imagen puede tardar uno o dos minutos.",
|
||||||
|
"The server's answer had no picture in it.": "La respuesta del servidor no traía ninguna imagen.",
|
||||||
},
|
},
|
||||||
"fr": {
|
"fr": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Reconnectez-vous : le serveur n’accepte plus la connexion de cette application.",
|
||||||
@@ -1517,6 +1572,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Rien n’est encore retenu.",
|
"Nothing remembered yet.": "Rien n’est encore retenu.",
|
||||||
"Tools and skills…": "Outils et compétences…",
|
"Tools and skills…": "Outils et compétences…",
|
||||||
"Nothing to switch in this chat.": "Rien à activer ou désactiver dans cette conversation.",
|
"Nothing to switch in this chat.": "Rien à activer ou désactiver dans cette conversation.",
|
||||||
|
"Stop dictating": "Arrêter la dictée",
|
||||||
|
"Writing down…": "Transcription…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Le micro n’a pas pu être ouvert : %(why)s",
|
||||||
|
"Read this aloud": "Lire à voix haute",
|
||||||
|
"The server's answer was not audio.": "La réponse du serveur n’était pas de l’audio.",
|
||||||
|
"What to draw…": "Que dessiner…",
|
||||||
|
"Shape": "Forme",
|
||||||
|
"Square": "Carrée",
|
||||||
|
"Wide": "Large",
|
||||||
|
"Tall": "Haute",
|
||||||
|
"Draw": "Dessiner",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Dessin en cours… une image peut prendre une minute ou deux.",
|
||||||
|
"The server's answer had no picture in it.": "La réponse du serveur ne contenait aucune image.",
|
||||||
},
|
},
|
||||||
"hu": {
|
"hu": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Jelentkezzen be újra: a szerver már nem fogadja el ennek az alkalmazásnak a bejelentkezését.",
|
||||||
@@ -1769,6 +1837,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Még nincs megjegyezve semmi.",
|
"Nothing remembered yet.": "Még nincs megjegyezve semmi.",
|
||||||
"Tools and skills…": "Eszközök és készségek…",
|
"Tools and skills…": "Eszközök és készségek…",
|
||||||
"Nothing to switch in this chat.": "Ebben a beszélgetésben nincs mit átkapcsolni.",
|
"Nothing to switch in this chat.": "Ebben a beszélgetésben nincs mit átkapcsolni.",
|
||||||
|
"Stop dictating": "Diktálás leállítása",
|
||||||
|
"Writing down…": "Leírás…",
|
||||||
|
"The microphone could not be opened: %(why)s": "A mikrofont nem sikerült megnyitni: %(why)s",
|
||||||
|
"Read this aloud": "Felolvasás",
|
||||||
|
"The server's answer was not audio.": "A szerver válasza nem hang volt.",
|
||||||
|
"What to draw…": "Mit rajzoljon…",
|
||||||
|
"Shape": "Forma",
|
||||||
|
"Square": "Négyzet",
|
||||||
|
"Wide": "Széles",
|
||||||
|
"Tall": "Magas",
|
||||||
|
"Draw": "Rajzolás",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Rajzolás… egy kép egy-két percig is eltarthat.",
|
||||||
|
"The server's answer had no picture in it.": "A szerver válaszában nem volt kép.",
|
||||||
},
|
},
|
||||||
"it": {
|
"it": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Accedi di nuovo: il server non accetta più l’accesso di questa app.",
|
||||||
@@ -2021,6 +2102,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Ancora niente da ricordare.",
|
"Nothing remembered yet.": "Ancora niente da ricordare.",
|
||||||
"Tools and skills…": "Strumenti e abilità…",
|
"Tools and skills…": "Strumenti e abilità…",
|
||||||
"Nothing to switch in this chat.": "Non c’è niente da attivare o disattivare in questa chat.",
|
"Nothing to switch in this chat.": "Non c’è niente da attivare o disattivare in questa chat.",
|
||||||
|
"Stop dictating": "Interrompi la dettatura",
|
||||||
|
"Writing down…": "Trascrizione…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Impossibile aprire il microfono: %(why)s",
|
||||||
|
"Read this aloud": "Leggi ad alta voce",
|
||||||
|
"The server's answer was not audio.": "La risposta del server non era audio.",
|
||||||
|
"What to draw…": "Cosa disegnare…",
|
||||||
|
"Shape": "Forma",
|
||||||
|
"Square": "Quadrata",
|
||||||
|
"Wide": "Larga",
|
||||||
|
"Tall": "Alta",
|
||||||
|
"Draw": "Disegna",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Disegno in corso… un’immagine può richiedere un minuto o due.",
|
||||||
|
"The server's answer had no picture in it.": "La risposta del server non conteneva immagini.",
|
||||||
},
|
},
|
||||||
"pl": {
|
"pl": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Zaloguj się ponownie: serwer nie przyjmuje już logowania tej aplikacji.",
|
||||||
@@ -2273,6 +2367,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Na razie nic nie zapamiętano.",
|
"Nothing remembered yet.": "Na razie nic nie zapamiętano.",
|
||||||
"Tools and skills…": "Narzędzia i umiejętności…",
|
"Tools and skills…": "Narzędzia i umiejętności…",
|
||||||
"Nothing to switch in this chat.": "W tym czacie nie ma czego przełączać.",
|
"Nothing to switch in this chat.": "W tym czacie nie ma czego przełączać.",
|
||||||
|
"Stop dictating": "Zatrzymaj dyktowanie",
|
||||||
|
"Writing down…": "Zapisywanie…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Nie udało się otworzyć mikrofonu: %(why)s",
|
||||||
|
"Read this aloud": "Przeczytaj na głos",
|
||||||
|
"The server's answer was not audio.": "Odpowiedź serwera nie była dźwiękiem.",
|
||||||
|
"What to draw…": "Co narysować…",
|
||||||
|
"Shape": "Kształt",
|
||||||
|
"Square": "Kwadrat",
|
||||||
|
"Wide": "Szeroki",
|
||||||
|
"Tall": "Wysoki",
|
||||||
|
"Draw": "Narysuj",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Rysowanie… obraz może zająć minutę lub dwie.",
|
||||||
|
"The server's answer had no picture in it.": "W odpowiedzi serwera nie było obrazu.",
|
||||||
},
|
},
|
||||||
"sk": {
|
"sk": {
|
||||||
"Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.",
|
"Sign in again: the server no longer accepts this app's sign-in.": "Prihláste sa znova: server už neprijíma prihlásenie tejto aplikácie.",
|
||||||
@@ -2525,6 +2632,19 @@ OWN: dict[str, dict[str, str]] = {
|
|||||||
"Nothing remembered yet.": "Zatiaľ si nič nepamätá.",
|
"Nothing remembered yet.": "Zatiaľ si nič nepamätá.",
|
||||||
"Tools and skills…": "Nástroje a schopnosti…",
|
"Tools and skills…": "Nástroje a schopnosti…",
|
||||||
"Nothing to switch in this chat.": "V tejto konverzácii nie je čo prepnúť.",
|
"Nothing to switch in this chat.": "V tejto konverzácii nie je čo prepnúť.",
|
||||||
|
"Stop dictating": "Zastaviť diktovanie",
|
||||||
|
"Writing down…": "Zapisujem…",
|
||||||
|
"The microphone could not be opened: %(why)s": "Mikrofón sa nepodarilo otvoriť: %(why)s",
|
||||||
|
"Read this aloud": "Prečítať nahlas",
|
||||||
|
"The server's answer was not audio.": "Odpoveď servera nebola zvuk.",
|
||||||
|
"What to draw…": "Čo nakresliť…",
|
||||||
|
"Shape": "Tvar",
|
||||||
|
"Square": "Štvorec",
|
||||||
|
"Wide": "Široký",
|
||||||
|
"Tall": "Vysoký",
|
||||||
|
"Draw": "Nakresliť",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Kreslím… obrázok môže trvať minútu alebo dve.",
|
||||||
|
"The server's answer had no picture in it.": "V odpovedi servera nebol žiadny obrázok.",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Generated
+1
@@ -2149,6 +2149,7 @@ dependencies = [
|
|||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
"tungstenite",
|
"tungstenite",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
"webkit2gtk",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ tungstenite = { version = "0.30", default-features = false, features = ["handsha
|
|||||||
[target.'cfg(unix)'.dependencies]
|
[target.'cfg(unix)'.dependencies]
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
# The webview's own settings: getUserMedia is off in WebKitGTK until the app turns it on.
|
||||||
|
# The same crate and version wry already builds on.
|
||||||
|
webkit2gtk = { version = "2.0", features = ["v2_4"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
lto = true
|
lto = true
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ use serde::Serialize;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
use tauri::menu::{Menu, MenuItem, PredefinedMenuItem};
|
||||||
use tauri::tray::TrayIconBuilder;
|
use tauri::tray::TrayIconBuilder;
|
||||||
|
use tauri::webview::{PermissionKind, PermissionResponse};
|
||||||
use tauri::window::Color;
|
use tauri::window::Color;
|
||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, DragDropEvent, Emitter, LogicalPosition, Manager, RunEvent, Url, WebviewUrl,
|
AppHandle, DragDropEvent, Emitter, LogicalPosition, Manager, RunEvent, Url, WebviewUrl,
|
||||||
@@ -122,6 +123,9 @@ struct App {
|
|||||||
watch: watch::Watch,
|
watch: watch::Watch,
|
||||||
/// The files last dropped on the window: what `attach_dropped` may send, and nothing else.
|
/// The files last dropped on the window: what `attach_dropped` may send, and nothing else.
|
||||||
dropped: Mutex<Vec<PathBuf>>,
|
dropped: Mutex<Vec<PathBuf>>,
|
||||||
|
/// Until when the page's own Dictate button may have the microphone (`voice_arm`): a request
|
||||||
|
/// for it at any other moment is refused.
|
||||||
|
mic_until: Mutex<Option<std::time::Instant>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TrayItems {
|
struct TrayItems {
|
||||||
@@ -153,6 +157,13 @@ impl App {
|
|||||||
Some((server.app, account.token))
|
Some((server.app, account.token))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The server's own address and the app's token: for the `/v1` routes (voice, pictures).
|
||||||
|
fn door_v1(&self) -> Option<(String, String)> {
|
||||||
|
let server = self.server.lock().unwrap().clone()?;
|
||||||
|
let account = self.account.lock().unwrap().clone()?;
|
||||||
|
Some((server.base, account.token))
|
||||||
|
}
|
||||||
|
|
||||||
/// The folders the agent may use: what the explorer and the viewer may read.
|
/// The folders the agent may use: what the explorer and the viewer may read.
|
||||||
fn roots(&self) -> Vec<String> {
|
fn roots(&self) -> Vec<String> {
|
||||||
self.remote
|
self.remote
|
||||||
@@ -452,6 +463,77 @@ async fn model_image(handle: AppHandle, path: String) -> Result<String, ApiError
|
|||||||
.map_err(|e| ApiError::new(0, e.to_string()))?
|
.map_err(|e| ApiError::new(0, e.to_string()))?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Voice and pictures (`/v1`, the app's token holds the `models` scope) -----------------------
|
||||||
|
|
||||||
|
/// Run a `/v1` call off the page's thread, as the signed-in account; a 401 signs the app out.
|
||||||
|
async fn on_v1<T: Send + 'static>(
|
||||||
|
handle: &AppHandle,
|
||||||
|
f: impl FnOnce(&str, &str) -> Result<T, ApiError> + Send + 'static,
|
||||||
|
) -> Result<T, ApiError> {
|
||||||
|
let Some((base, token)) = handle.state::<App>().door_v1() else {
|
||||||
|
return Err(ApiError::new(401, "Not signed in."));
|
||||||
|
};
|
||||||
|
let result = tauri::async_runtime::spawn_blocking(move || f(&base, &token))
|
||||||
|
.await
|
||||||
|
.map_err(|e| ApiError::new(0, e.to_string()))?;
|
||||||
|
if matches!(&result, Err(e) if e.status == 401) {
|
||||||
|
token_refused(handle);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The page is about to ask for the microphone, because the person pressed Dictate: for the
|
||||||
|
/// next few seconds a request for it is allowed (`permission`), and at no other time.
|
||||||
|
#[tauri::command]
|
||||||
|
fn voice_arm(app: tauri::State<'_, App>) {
|
||||||
|
*app.mic_until.lock().unwrap() =
|
||||||
|
Some(std::time::Instant::now() + std::time::Duration::from_secs(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recording made in the composer, in words: its bytes as the raw body of the call, its media
|
||||||
|
/// type in `x-type`.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn transcribe(
|
||||||
|
handle: AppHandle,
|
||||||
|
request: tauri::ipc::Request<'_>,
|
||||||
|
) -> Result<String, ApiError> {
|
||||||
|
let tauri::ipc::InvokeBody::Raw(bytes) = request.body() else {
|
||||||
|
return Err(ApiError::new(400, "No recording."));
|
||||||
|
};
|
||||||
|
if bytes.len() as u64 > server::RECORDING_LIMIT {
|
||||||
|
return Err(ApiError::new(413, "That recording is too long."));
|
||||||
|
}
|
||||||
|
let kind = request
|
||||||
|
.headers()
|
||||||
|
.get("x-type")
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string();
|
||||||
|
let bytes = bytes.clone();
|
||||||
|
on_v1(&handle, move |base, token| {
|
||||||
|
server::transcribe(base, token, &kind, &bytes)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reply read aloud, as a `data:` URL of its audio.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn speak(handle: AppHandle, text: String) -> Result<String, ApiError> {
|
||||||
|
on_v1(&handle, move |base, token| {
|
||||||
|
server::speak(base, token, &text)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pictures drawn by the instance for `prompt`, as `data:` URLs.
|
||||||
|
#[tauri::command]
|
||||||
|
async fn draw(handle: AppHandle, prompt: String, size: String) -> Result<Vec<String>, ApiError> {
|
||||||
|
on_v1(&handle, move |base, token| {
|
||||||
|
server::draw(base, token, &prompt, &size)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// How the CLI is started in a terminal here (`lembas`, or the bundled one by its path): the
|
/// How the CLI is started in a terminal here (`lembas`, or the bundled one by its path): the
|
||||||
/// first word of "continue in a terminal" for an agent chat on this computer.
|
/// first word of "continue in a terminal" for an agent chat on this computer.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -1249,7 +1331,39 @@ fn build_main(handle: &AppHandle) -> tauri::Result<WebviewWindow> {
|
|||||||
.decorations(false)
|
.decorations(false)
|
||||||
.background_color(theme.background())
|
.background_color(theme.background())
|
||||||
.on_navigation(allowed)
|
.on_navigation(allowed)
|
||||||
|
.on_permission_request(permission)
|
||||||
.build()
|
.build()
|
||||||
|
.inspect(media_stream)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WebKitGTK answers no permission request its app does not, and has no prompt of its own. The
|
||||||
|
/// microphone is allowed when the page's own Dictate button has just asked for it (`voice_arm`,
|
||||||
|
/// which only the app's page can call: sealed frames have no bridge), once; nothing else is.
|
||||||
|
fn permission(view: tauri::Webview, kind: PermissionKind) -> PermissionResponse {
|
||||||
|
if kind != PermissionKind::Microphone {
|
||||||
|
return PermissionResponse::Default;
|
||||||
|
}
|
||||||
|
let app = view.app_handle().state::<App>();
|
||||||
|
let armed = app.mic_until.lock().unwrap().take();
|
||||||
|
if armed.is_some_and(|until| std::time::Instant::now() <= until) {
|
||||||
|
PermissionResponse::Allow
|
||||||
|
} else {
|
||||||
|
PermissionResponse::Deny
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `getUserMedia` is off in WebKitGTK unless the app turns it on (it is on in other webviews).
|
||||||
|
/// The settings are read with their own getter: the `settings` property is write-only.
|
||||||
|
fn media_stream(window: &WebviewWindow) {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let _ = window.with_webview(|pv| {
|
||||||
|
use webkit2gtk::{SettingsExt, WebViewExt};
|
||||||
|
if let Some(settings) = pv.inner().settings() {
|
||||||
|
settings.set_enable_media_stream(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
let _ = window;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The window shows the app's own page, and the sealed frames inside it; nothing else, ever.
|
/// The window shows the app's own page, and the sealed frames inside it; nothing else, ever.
|
||||||
@@ -1710,6 +1824,10 @@ pub fn run() {
|
|||||||
sealed_close,
|
sealed_close,
|
||||||
attach_pick,
|
attach_pick,
|
||||||
library_upload,
|
library_upload,
|
||||||
|
voice_arm,
|
||||||
|
transcribe,
|
||||||
|
speak,
|
||||||
|
draw,
|
||||||
attach_dropped,
|
attach_dropped,
|
||||||
attach_bytes,
|
attach_bytes,
|
||||||
set_theme,
|
set_theme,
|
||||||
@@ -1770,6 +1888,7 @@ pub fn run() {
|
|||||||
sealed: sealed::Pages::default(),
|
sealed: sealed::Pages::default(),
|
||||||
watch: watch::Watch::default(),
|
watch: watch::Watch::default(),
|
||||||
dropped: Mutex::new(Vec::new()),
|
dropped: Mutex::new(Vec::new()),
|
||||||
|
mic_until: Mutex::new(None),
|
||||||
});
|
});
|
||||||
// Without a tray (no AppIndicator library, no StatusNotifier host) the app still
|
// Without a tray (no AppIndicator library, no StatusNotifier host) the app still
|
||||||
// starts: the title bar's menu has every item the tray has.
|
// starts: the title bar's menu has every item the tray has.
|
||||||
|
|||||||
+228
-1
@@ -652,10 +652,15 @@ fn header_name(name: &str) -> String {
|
|||||||
|
|
||||||
/// The body of a `multipart/form-data` request with one file in the field `field`.
|
/// The body of a `multipart/form-data` request with one file in the field `field`.
|
||||||
fn multipart(boundary: &str, field: &str, name: &str, bytes: &[u8]) -> Vec<u8> {
|
fn multipart(boundary: &str, field: &str, name: &str, bytes: &[u8]) -> Vec<u8> {
|
||||||
|
multipart_typed(boundary, field, name, "application/octet-stream", bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The same, with the file's own media type (a recording says what it is: `audio/webm`).
|
||||||
|
fn multipart_typed(boundary: &str, field: &str, name: &str, kind: &str, bytes: &[u8]) -> Vec<u8> {
|
||||||
let mut body = Vec::with_capacity(bytes.len() + 256);
|
let mut body = Vec::with_capacity(bytes.len() + 256);
|
||||||
body.extend_from_slice(
|
body.extend_from_slice(
|
||||||
format!(
|
format!(
|
||||||
"--{boundary}\r\nContent-Disposition: form-data; name=\"{field}\"; filename=\"{}\"\r\nContent-Type: application/octet-stream\r\n\r\n",
|
"--{boundary}\r\nContent-Disposition: form-data; name=\"{field}\"; filename=\"{}\"\r\nContent-Type: {kind}\r\n\r\n",
|
||||||
header_name(name)
|
header_name(name)
|
||||||
)
|
)
|
||||||
.as_bytes(),
|
.as_bytes(),
|
||||||
@@ -707,6 +712,192 @@ pub fn upload_to_library(
|
|||||||
send_file(app, token, &path, "files", name, bytes)
|
send_file(app, token, &path, "files", name, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Voice and pictures: the OpenAI-compatible routes of `/v1`, under the `models` scope ------------
|
||||||
|
|
||||||
|
/// The most a recording may be here: the server's own limit is 25 MB.
|
||||||
|
pub const RECORDING_LIMIT: u64 = 25 * 1024 * 1024;
|
||||||
|
/// The most a reply read aloud may be, as audio.
|
||||||
|
pub const SPEECH_LIMIT: u64 = 32 * 1024 * 1024;
|
||||||
|
/// The most a generated picture may be.
|
||||||
|
pub const DRAWN_LIMIT: u64 = 24 * 1024 * 1024;
|
||||||
|
/// The media types a recording may say it is; anything else goes as `audio/webm`.
|
||||||
|
const RECORDING_TYPES: [&str; 5] = [
|
||||||
|
"audio/webm",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/mp4",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// A recording's media type as it may be sent: one of ours, its parameters (`;codecs=opus`) off.
|
||||||
|
pub fn recording_type(kind: &str) -> &'static str {
|
||||||
|
let bare = kind
|
||||||
|
.split(';')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
RECORDING_TYPES
|
||||||
|
.iter()
|
||||||
|
.find(|t| **t == bare)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or("audio/webm")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A recording, in words: `POST /v1/audio/transcriptions` (the instance's own model, the
|
||||||
|
/// account's language). The text the server heard.
|
||||||
|
pub fn transcribe(base: &str, token: &str, kind: &str, bytes: &[u8]) -> Result<String, ApiError> {
|
||||||
|
if bytes.is_empty() {
|
||||||
|
return Err(ApiError::new(400, "The recording was empty."));
|
||||||
|
}
|
||||||
|
let kind = recording_type(kind);
|
||||||
|
let ext = kind
|
||||||
|
.rsplit('/')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("webm")
|
||||||
|
.replace("mpeg", "mp3");
|
||||||
|
let nanos = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.map(|d| d.as_nanos())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let boundary = format!("llembas-desktop-{nanos:x}");
|
||||||
|
let res = agent(Duration::from_secs(300))
|
||||||
|
.post(&format!("{base}/v1/audio/transcriptions"))
|
||||||
|
.set("authorization", &format!("Bearer {token}"))
|
||||||
|
.set("accept", "application/json")
|
||||||
|
.set(
|
||||||
|
"content-type",
|
||||||
|
&format!("multipart/form-data; boundary={boundary}"),
|
||||||
|
)
|
||||||
|
.send_bytes(&multipart_typed(
|
||||||
|
&boundary,
|
||||||
|
"file",
|
||||||
|
&format!("speech.{ext}"),
|
||||||
|
kind,
|
||||||
|
bytes,
|
||||||
|
))?;
|
||||||
|
let v: Value = res
|
||||||
|
.into_json()
|
||||||
|
.map_err(|_| ApiError::new(0, "The server's answer was not JSON.".to_string()))?;
|
||||||
|
Ok(v.get("text")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `bytes` of a media type of `kinds` (by its prefix), as a `data:` URL; at most `limit`.
|
||||||
|
fn as_data_url(res: ureq::Response, prefix: &str, limit: u64) -> Result<String, ApiError> {
|
||||||
|
let kind = res
|
||||||
|
.content_type()
|
||||||
|
.split(';')
|
||||||
|
.next()
|
||||||
|
.unwrap_or("")
|
||||||
|
.trim()
|
||||||
|
.to_ascii_lowercase();
|
||||||
|
let fits = if prefix == "image/" {
|
||||||
|
IMAGE_TYPES.contains(&kind.as_str())
|
||||||
|
} else {
|
||||||
|
kind.starts_with(prefix)
|
||||||
|
&& kind
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || "/-+.".contains(c))
|
||||||
|
};
|
||||||
|
if !fits {
|
||||||
|
return Err(ApiError::new(
|
||||||
|
415,
|
||||||
|
format!("Not what was asked for: {kind}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
res.into_reader()
|
||||||
|
.take(limit)
|
||||||
|
.read_to_end(&mut bytes)
|
||||||
|
.map_err(|e| ApiError::new(0, format!("The server's answer could not be read: {e}")))?;
|
||||||
|
if bytes.len() as u64 >= limit {
|
||||||
|
return Err(ApiError::new(413, "The server's answer is too large."));
|
||||||
|
}
|
||||||
|
Ok(format!("data:{kind};base64,{}", base64(&bytes)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A reply read aloud: `POST /v1/audio/speech` in the account's own voice and speed. The audio,
|
||||||
|
/// as a `data:` URL the page plays.
|
||||||
|
pub fn speak(base: &str, token: &str, text: &str) -> Result<String, ApiError> {
|
||||||
|
let text = text.trim();
|
||||||
|
if text.is_empty() {
|
||||||
|
return Err(ApiError::new(400, "There is nothing to read aloud."));
|
||||||
|
}
|
||||||
|
let res = agent(Duration::from_secs(300))
|
||||||
|
.post(&format!("{base}/v1/audio/speech"))
|
||||||
|
.set("authorization", &format!("Bearer {token}"))
|
||||||
|
.send_json(serde_json::json!({ "input": text, "response_format": "mp3" }))?;
|
||||||
|
as_data_url(res, "audio/", SPEECH_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The id of one of the person's own files in a URL `/v1/images/generations` answered with:
|
||||||
|
/// `…/v1/files/<id>/content`, and nothing else. The picture is then fetched from this app's own
|
||||||
|
/// server by that id -- never from the address the answer named.
|
||||||
|
pub fn own_file_id(url: &str) -> Option<String> {
|
||||||
|
let rest = url.split(['?', '#']).next()?;
|
||||||
|
let tail = rest.strip_suffix("/content")?;
|
||||||
|
let (head, id) = tail.rsplit_once('/')?;
|
||||||
|
let ok = head.ends_with("/v1/files")
|
||||||
|
&& !id.is_empty()
|
||||||
|
&& id.len() <= 128
|
||||||
|
&& id
|
||||||
|
.chars()
|
||||||
|
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
|
||||||
|
ok.then(|| id.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sizes a picture may be asked for: the instance rounds to what its workflow takes.
|
||||||
|
const SIZES: [&str; 3] = ["1024x1024", "1344x768", "768x1344"];
|
||||||
|
|
||||||
|
/// Pictures drawn by the instance: `POST /v1/images/generations` (its own workflow and default
|
||||||
|
/// checkpoint), each then read from the person's own files on this server. `data:` URLs.
|
||||||
|
pub fn draw(base: &str, token: &str, prompt: &str, size: &str) -> Result<Vec<String>, ApiError> {
|
||||||
|
let prompt = prompt.trim();
|
||||||
|
if prompt.is_empty() {
|
||||||
|
return Err(ApiError::new(400, "Say what to draw."));
|
||||||
|
}
|
||||||
|
let mut body = serde_json::json!({ "prompt": prompt, "n": 1, "response_format": "url" });
|
||||||
|
if SIZES.contains(&size) {
|
||||||
|
body["size"] = Value::String(size.to_string());
|
||||||
|
}
|
||||||
|
let res = agent(Duration::from_secs(900))
|
||||||
|
.post(&format!("{base}/v1/images/generations"))
|
||||||
|
.set("authorization", &format!("Bearer {token}"))
|
||||||
|
.set("accept", "application/json")
|
||||||
|
.send_json(body)?;
|
||||||
|
let v: Value = res
|
||||||
|
.into_json()
|
||||||
|
.map_err(|_| ApiError::new(0, "The server's answer was not JSON.".to_string()))?;
|
||||||
|
let ids: Vec<String> = v
|
||||||
|
.get("data")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|items| {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.filter_map(|i| i.get("url").and_then(Value::as_str).and_then(own_file_id))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
if ids.is_empty() {
|
||||||
|
return Err(ApiError::new(
|
||||||
|
0,
|
||||||
|
"The server's answer had no picture of yours in it.",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ids.iter()
|
||||||
|
.map(|id| {
|
||||||
|
let res = agent(Duration::from_secs(60))
|
||||||
|
.get(&format!("{base}/v1/files/{id}/content"))
|
||||||
|
.set("authorization", &format!("Bearer {token}"))
|
||||||
|
.call()?;
|
||||||
|
as_data_url(res, "image/", DRAWN_LIMIT)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
/// One `multipart/form-data` POST of one file to `path` on the app's door; the JSON answer.
|
/// One `multipart/form-data` POST of one file to `path` on the app's door; the JSON answer.
|
||||||
fn send_file(
|
fn send_file(
|
||||||
app: &str,
|
app: &str,
|
||||||
@@ -985,6 +1176,42 @@ mod tests {
|
|||||||
assert_eq!(base64(&[0xfb, 0xff]), "+/8=");
|
assert_eq!(base64(&[0xfb, 0xff]), "+/8=");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_drawn_picture_is_read_only_as_one_of_the_persons_own_files() {
|
||||||
|
assert_eq!(
|
||||||
|
own_file_id("https://ai.example/v1/files/abc_12-3/content").as_deref(),
|
||||||
|
Some("abc_12-3")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
own_file_id("http://other/v1/files/x/content?t=1").as_deref(),
|
||||||
|
Some("x")
|
||||||
|
);
|
||||||
|
assert_eq!(own_file_id("https://ai.example/v1/files/../content"), None);
|
||||||
|
assert_eq!(own_file_id("https://ai.example/v1/files//content"), None);
|
||||||
|
assert_eq!(own_file_id("https://ai.example/uploads/x/content"), None);
|
||||||
|
assert_eq!(
|
||||||
|
own_file_id("https://ai.example/v1/files/a%2Fb/content"),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(own_file_id("javascript:alert(1)"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_recording_says_one_of_the_known_types() {
|
||||||
|
assert_eq!(recording_type("audio/webm;codecs=opus"), "audio/webm");
|
||||||
|
assert_eq!(recording_type("Audio/OGG"), "audio/ogg");
|
||||||
|
assert_eq!(recording_type("text/html"), "audio/webm");
|
||||||
|
let body = String::from_utf8(multipart_typed(
|
||||||
|
"B",
|
||||||
|
"file",
|
||||||
|
"speech.webm",
|
||||||
|
"audio/webm",
|
||||||
|
b"x",
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
assert!(body.contains("filename=\"speech.webm\"\r\nContent-Type: audio/webm\r\n"));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_file_goes_as_one_multipart_field() {
|
fn a_file_goes_as_one_multipart_field() {
|
||||||
let body = multipart("XYZ", "file", "a\"b\r\n.txt", b"hello");
|
let body = multipart("XYZ", "file", "a\"b\r\n.txt", b"hello");
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [],
|
"windows": [],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc: http://ipc.localhost; frame-src llembas-sealed: http://llembas-sealed.localhost; object-src 'none'; base-uri 'none'; form-action 'none'",
|
"csp": "default-src 'self'; img-src 'self' data:; media-src data:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src ipc: http://ipc.localhost; frame-src llembas-sealed: http://llembas-sealed.localhost; object-src 'none'; base-uri 'none'; form-action 'none'",
|
||||||
"dangerousDisableAssetCspModification": [
|
"dangerousDisableAssetCspModification": [
|
||||||
"style-src"
|
"style-src"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1398,6 +1398,134 @@ test("H8: at 390 px the server's pane and the switches stay inside the window",
|
|||||||
assert.deepEqual(p.errors, [])
|
assert.deepEqual(p.errors, [])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// I8: voice (/v1/audio, through Rust) and drawing a picture (/v1/images, through Rust).
|
||||||
|
|
||||||
|
const PNG = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
|
||||||
|
|
||||||
|
function i8({ stt = true, tts = true, images = true, scopes = ["app", "models"] } = {}, commands = {}) {
|
||||||
|
const reply = { id: "m6", role: "assistant", created_at: STAMP, complete: true, stopped: false, error: null, model: "qwen", metrics: null, steps: [{ kind: "text", markdown: "Lembas is **baked** in leaves.\n\n```\nsecret\n```" }] }
|
||||||
|
return {
|
||||||
|
status: status(),
|
||||||
|
routes: baseRoutes({
|
||||||
|
"GET /me": { id: "u1", name: "Ada Example", email: "ada@example.org", theme: "moria", language: "en", permissions: {}, services: { stt, tts, images, search: false, fetch: false }, scopes },
|
||||||
|
"GET /chats/chat1": { chat: chatRow(), messages: [userMessage("m5", "How is lembas made?"), reply], more: false, live: null },
|
||||||
|
}),
|
||||||
|
commands: { transcribe: "baked in leaves", speak: "data:audio/mpeg;base64,SUQz", draw: [PNG, "https://elsewhere.example/x.png"], ...commands },
|
||||||
|
streams: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A microphone and a recorder that record two bytes: the page's own, the browser's untouched.
|
||||||
|
async function fakeMicrophone(p) {
|
||||||
|
await p.evaluate(() => {
|
||||||
|
const track = { stop() {} }
|
||||||
|
// The test's page is not a secure context, so it has no mediaDevices of its own.
|
||||||
|
if (!navigator.mediaDevices) Object.defineProperty(navigator, "mediaDevices", { value: {}, configurable: true })
|
||||||
|
navigator.mediaDevices.getUserMedia = async () => ({ getTracks: () => [track] })
|
||||||
|
window.MediaRecorder = class extends EventTarget {
|
||||||
|
static isTypeSupported(x) {
|
||||||
|
return x === "audio/webm;codecs=opus"
|
||||||
|
}
|
||||||
|
constructor(stream, options) {
|
||||||
|
super()
|
||||||
|
this.mimeType = options?.mimeType || ""
|
||||||
|
this.state = "inactive"
|
||||||
|
}
|
||||||
|
start() {
|
||||||
|
this.state = "recording"
|
||||||
|
}
|
||||||
|
stop() {
|
||||||
|
this.state = "inactive"
|
||||||
|
const data = new Event("dataavailable")
|
||||||
|
data.data = new Blob([new Uint8Array([1, 2])], { type: this.mimeType })
|
||||||
|
this.dispatchEvent(data)
|
||||||
|
this.dispatchEvent(new Event("stop"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HTMLMediaElement.prototype.play = async function () {}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
test("I8: Dictate records, asks Rust for the microphone first, and puts the words where the cursor is", async () => {
|
||||||
|
const p = await page(i8())
|
||||||
|
await p.waitForSelector("#dictate:not([hidden])")
|
||||||
|
await fakeMicrophone(p)
|
||||||
|
await p.fill("#composer-text", "Lembas is")
|
||||||
|
await p.click("#dictate")
|
||||||
|
await p.waitForSelector('#dictate.is-recording[aria-pressed="true"]')
|
||||||
|
const order = await p.evaluate(() => window.__mock.calls.map((c) => c.cmd).filter((c) => c === "voice_arm"))
|
||||||
|
assert.deepEqual(order, ["voice_arm"])
|
||||||
|
await shot(p, "i8-dictate-1280")
|
||||||
|
await p.click("#dictate")
|
||||||
|
await p.waitForFunction(() => document.getElementById("composer-text").value === "Lembas is baked in leaves")
|
||||||
|
assert.equal(await p.getAttribute("#dictate", "aria-pressed"), "false")
|
||||||
|
assert.ok(await p.evaluate(() => window.__mock.calls.some((c) => c.cmd === "transcribe")))
|
||||||
|
assert.deepEqual(p.errors, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("I8: no Dictate, Read aloud or Draw where the server has none of them, or the token lacks models", async () => {
|
||||||
|
let p = await page(i8({ stt: false, tts: false, images: false }))
|
||||||
|
await p.click('.chatrow[data-id="chat1"]')
|
||||||
|
await p.waitForSelector('.msg--assistant[data-id="m6"]')
|
||||||
|
assert.equal(await p.isHidden("#dictate"), true)
|
||||||
|
assert.equal(await p.locator(".msg__speak").count(), 0)
|
||||||
|
await p.keyboard.press("Control+k")
|
||||||
|
assert.equal(await p.locator(".palette__item >> text=Draw a picture").count(), 0)
|
||||||
|
await p.context().close()
|
||||||
|
p = await page(i8({ scopes: ["app"] }))
|
||||||
|
await p.waitForSelector("#composer")
|
||||||
|
await p.waitForFunction(() => window.__mock.api("GET", "/me").length === 1)
|
||||||
|
assert.equal(await p.isHidden("#dictate"), true)
|
||||||
|
assert.deepEqual(p.errors, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("I8: Read aloud speaks a reply's words without its markdown or code, and stops when pressed again", async () => {
|
||||||
|
const p = await page(i8())
|
||||||
|
await fakeMicrophone(p)
|
||||||
|
await p.click('.chatrow[data-id="chat1"]')
|
||||||
|
await p.waitForSelector('.msg--assistant[data-id="m6"] .msg__speak')
|
||||||
|
await p.click('.msg--assistant[data-id="m6"] .msg__speak')
|
||||||
|
await p.waitForFunction(() => window.__mock.calls.some((c) => c.cmd === "speak"))
|
||||||
|
assert.deepEqual((await p.evaluate(() => window.__mock.calls.find((c) => c.cmd === "speak").args)), { text: "Lembas is baked in leaves." })
|
||||||
|
await p.waitForSelector('.msg__speak[aria-pressed="true"]:not(.is-busy)')
|
||||||
|
await p.click('.msg--assistant[data-id="m6"] .msg__speak')
|
||||||
|
await p.waitForSelector('.msg__speak[aria-pressed="false"]')
|
||||||
|
assert.deepEqual(p.errors, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("I8: Draw a picture shows only the server's own pictures, as Rust read them", async () => {
|
||||||
|
const p = await page(i8())
|
||||||
|
await p.waitForFunction(() => window.__mock.api("GET", "/me").length === 1)
|
||||||
|
await p.keyboard.press("Control+k")
|
||||||
|
await p.fill(".palette__input", "draw")
|
||||||
|
await p.click(".palette__item >> text=Draw a picture")
|
||||||
|
await p.fill(".draw__prompt", "A lembas loaf on a leaf")
|
||||||
|
await p.selectOption(".draw__size", "1344x768")
|
||||||
|
await p.click(".draw button[type=submit]")
|
||||||
|
await p.waitForSelector(".draw__pic img")
|
||||||
|
assert.deepEqual((await p.evaluate(() => window.__mock.calls.find((c) => c.cmd === "draw").args)), { prompt: "A lembas loaf on a leaf", size: "1344x768" })
|
||||||
|
const srcs = await p.$$eval(".draw__pic img", (l) => l.map((x) => x.getAttribute("src")))
|
||||||
|
assert.deepEqual(srcs, [PNG])
|
||||||
|
await shot(p, "i8-draw-1280")
|
||||||
|
assert.deepEqual(p.errors, [])
|
||||||
|
})
|
||||||
|
|
||||||
|
test("I8: at 390 px the composer with Dictate and the drawing dialog stay inside the window", async () => {
|
||||||
|
const p = await page(i8(), { width: 390, height: 844 })
|
||||||
|
await p.waitForSelector("#dictate:not([hidden])")
|
||||||
|
assert.ok(await noSideways(p, 390), "nothing scrolls sideways")
|
||||||
|
await shot(p, "i8-composer-390")
|
||||||
|
await p.keyboard.press("Control+k")
|
||||||
|
await p.fill(".palette__input", "draw")
|
||||||
|
await p.click(".palette__item >> text=Draw a picture")
|
||||||
|
await p.fill(".draw__prompt", "A lembas loaf")
|
||||||
|
await p.click(".draw button[type=submit]")
|
||||||
|
await p.waitForSelector(".draw__pic img")
|
||||||
|
assert.ok(await noSideways(p, 390), "nothing scrolls sideways")
|
||||||
|
await shot(p, "i8-draw-390")
|
||||||
|
assert.deepEqual(p.errors, [])
|
||||||
|
})
|
||||||
|
|
||||||
// --- Run ----------------------------------------------------------------------------------------
|
// --- Run ----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
const only = process.argv[2] || ""
|
const only = process.argv[2] || ""
|
||||||
|
|||||||
+19
-2
@@ -1168,6 +1168,18 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
|
|||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
}
|
}
|
||||||
.dialog::backdrop { background: var(--scrim); }
|
.dialog::backdrop { background: var(--scrim); }
|
||||||
|
.dialog--wide { width: min(var(--w-prose), calc(100vw - 2 * var(--sp-4))); }
|
||||||
|
|
||||||
|
/* Draw a picture (draw.js): the prompt, a shape and two buttons on one line, the pictures below. */
|
||||||
|
.draw .dialog__title { display: flex; gap: var(--sp-2); align-items: center; }
|
||||||
|
.draw__prompt { width: 100%; }
|
||||||
|
.draw__bar { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: var(--sp-2); align-items: center; }
|
||||||
|
.draw__size { justify-self: start; max-width: 100%; }
|
||||||
|
.draw__out { display: grid; gap: var(--sp-3); max-height: 60vh; overflow-y: auto; }
|
||||||
|
.draw__out:empty { display: none; }
|
||||||
|
.draw__pic { display: grid; gap: var(--sp-1); margin: 0; }
|
||||||
|
.draw__pic img { display: block; width: 100%; height: auto; border-radius: var(--radius-lg); background: var(--surface); }
|
||||||
|
.draw__pic figcaption { font-size: var(--text-xs); color: var(--ink-muted); overflow-wrap: anywhere; }
|
||||||
.dialog__body { display: grid; gap: var(--sp-3); padding: var(--sp-4); }
|
.dialog__body { display: grid; gap: var(--sp-3); padding: var(--sp-4); }
|
||||||
.dialog__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-ui); }
|
.dialog__title { font: 600 var(--text-md) / var(--leading-tight) var(--font-ui); }
|
||||||
/* A line to copy: as it is, on one line that scrolls on its own rather than the dialog. */
|
/* A line to copy: as it is, on one line that scrolls on its own rather than the dialog. */
|
||||||
@@ -1276,11 +1288,16 @@ details[open] > summary .step__chev .icon { transform: rotate(90deg); }
|
|||||||
.composer__files { justify-content: start; }
|
.composer__files { justify-content: start; }
|
||||||
.composer__bar {
|
.composer__bar {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: var(--chip-h) minmax(0, 1fr) auto auto;
|
grid-template-columns: var(--chip-h) minmax(0, 1fr) auto auto auto;
|
||||||
gap: var(--sp-2);
|
gap: var(--sp-2);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
.composer[data-attach="false"] .composer__bar { grid-template-columns: minmax(0, 1fr) auto auto; }
|
.composer[data-attach="false"] .composer__bar { grid-template-columns: minmax(0, 1fr) auto auto auto; }
|
||||||
|
/* Dictate: a round tool beside Send; while it records it says so in the accent. */
|
||||||
|
.composer__mic { width: var(--round); height: var(--round); border-radius: var(--radius-full); }
|
||||||
|
.composer .composer__mic.is-recording { color: var(--accent-ink); background: var(--accent); }
|
||||||
|
.composer__mic.is-busy, .msg__speak.is-busy { opacity: 0.6; cursor: progress; }
|
||||||
|
.msg__speak[aria-pressed="true"] { color: var(--accent); }
|
||||||
.composer__attach { width: var(--chip-h); height: var(--chip-h); border: var(--border-w) solid var(--border); border-radius: var(--radius-full); }
|
.composer__attach { width: var(--chip-h); height: var(--chip-h); border: var(--border-w) solid var(--border); border-radius: var(--radius-full); }
|
||||||
/* The chips stay on one line; where they do not fit, the line scrolls by itself. */
|
/* The chips stay on one line; where they do not fit, the line scrolls by itself. */
|
||||||
.composer__chips { display: flex; flex-wrap: nowrap; gap: var(--sp-1); min-width: 0; align-items: center; overflow-x: auto; scrollbar-width: none; }
|
.composer__chips { display: flex; flex-wrap: nowrap; gap: var(--sp-1); min-width: 0; align-items: center; overflow-x: auto; scrollbar-width: none; }
|
||||||
|
|||||||
+120
@@ -116,8 +116,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Smazat tuto konverzaci?",
|
"Delete this chat?": "Smazat tuto konverzaci?",
|
||||||
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
|
"Delete this scheduled task?": "Odstranit tuto naplánovanou úlohu?",
|
||||||
"Deny": "Zamítnout",
|
"Deny": "Zamítnout",
|
||||||
|
"Dictate a message": "Nadiktovat zprávu",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
|
"Disconnected. Close and reopen to reconnect.": "Odpojeno. Pro opětovné připojení zavřete a znovu otevřete.",
|
||||||
"Done": "Hotovo",
|
"Done": "Hotovo",
|
||||||
|
"Draw": "Nakreslit",
|
||||||
|
"Draw a picture": "Nakreslit obrázek",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Kreslím… obrázek může trvat minutu nebo dvě.",
|
||||||
"Drop the files to attach them": "Pusťte soubory a přiloží se",
|
"Drop the files to attach them": "Pusťte soubory a přiloží se",
|
||||||
"Due %(when)s": "Naplánováno na %(when)s",
|
"Due %(when)s": "Naplánováno na %(when)s",
|
||||||
"Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
|
"Each run sees the ones before it.": "Každé spuštění vidí ta předchozí.",
|
||||||
@@ -248,6 +252,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "Ve frontě",
|
"Queued": "Ve frontě",
|
||||||
"Queued: {{ahead}} ahead": "Ve frontě: před ním {{ahead}}",
|
"Queued: {{ahead}} ahead": "Ve frontě: před ním {{ahead}}",
|
||||||
"Read again": "Načíst znovu",
|
"Read again": "Načíst znovu",
|
||||||
|
"Read this aloud": "Přečíst nahlas",
|
||||||
"Reading the prompt": "Čte se výzva",
|
"Reading the prompt": "Čte se výzva",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Čte se výzva: {{done}} / {{total}} tokenů ({{percent}} %)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Čte se výzva: {{done}} / {{total}} tokenů ({{percent}} %)",
|
||||||
"Reasoning effort": "Náročnost uvažování",
|
"Reasoning effort": "Náročnost uvažování",
|
||||||
@@ -290,6 +295,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Set the time yourself": "Nastavit čas sami",
|
"Set the time yourself": "Nastavit čas sami",
|
||||||
"Settings": "Nastavení",
|
"Settings": "Nastavení",
|
||||||
|
"Shape": "Tvar",
|
||||||
"Show %(n)s more": "Zobrazit další (%(n)s)",
|
"Show %(n)s more": "Zobrazit další (%(n)s)",
|
||||||
"Shown by the agent": "Ukázal agent",
|
"Shown by the agent": "Ukázal agent",
|
||||||
"Sidebar": "Postranní panel",
|
"Sidebar": "Postranní panel",
|
||||||
@@ -303,11 +309,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Vynecháno",
|
"Skipped": "Vynecháno",
|
||||||
"Something to remember…": "Něco k zapamatování…",
|
"Something to remember…": "Něco k zapamatování…",
|
||||||
"Source": "Zdroj",
|
"Source": "Zdroj",
|
||||||
|
"Square": "Čtverec",
|
||||||
"Start the agent": "Spustit agenta",
|
"Start the agent": "Spustit agenta",
|
||||||
"Stop": "Zastavit",
|
"Stop": "Zastavit",
|
||||||
"Stop %(name)s": "Zastavit %(name)s",
|
"Stop %(name)s": "Zastavit %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Zastavit %(name)s a členy pod ním? Opustí tým.",
|
"Stop %(name)s and the members under it? They leave the team.": "Zastavit %(name)s a členy pod ním? Opustí tým.",
|
||||||
"Stop %(name)s and the subagents under it?": "Zastavit %(name)s a podagenty pod ním?",
|
"Stop %(name)s and the subagents under it?": "Zastavit %(name)s a podagenty pod ním?",
|
||||||
|
"Stop dictating": "Zastavit diktování",
|
||||||
"Stop the agent": "Zastavit agenta",
|
"Stop the agent": "Zastavit agenta",
|
||||||
"Stopped": "Zastaveno",
|
"Stopped": "Zastaveno",
|
||||||
"Stopped.": "Zastaveno.",
|
"Stopped.": "Zastaveno.",
|
||||||
@@ -319,6 +327,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuto jen tady. Co je na začátku zapnuté, je vaše výchozí nastavení pro tento model v Nastavení → Modely.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuto jen tady. Co je na začátku zapnuté, je vaše výchozí nastavení pro tento model v Nastavení → Modely.",
|
||||||
"System prompt": "Systémový pokyn",
|
"System prompt": "Systémový pokyn",
|
||||||
"Taken in while it ran:": "Převzato během běhu:",
|
"Taken in while it ran:": "Převzato během běhu:",
|
||||||
|
"Tall": "Vysoký",
|
||||||
"Tasks: %(n)s": "Úlohy: %(n)s",
|
"Tasks: %(n)s": "Úlohy: %(n)s",
|
||||||
"Team member": "Člen týmu",
|
"Team member": "Člen týmu",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů",
|
"Team: %(size)s of at most %(max)s subagents": "Tým: %(size)s z nejvýše %(max)s podagentů",
|
||||||
@@ -346,6 +355,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "Instrukce, kterou dostane každé spuštění samostatně: uveďte vše, co potřebuje.",
|
"The instruction each run is given, on its own: say everything it needs.": "Instrukce, kterou dostane každé spuštění samostatně: uveďte vše, co potřebuje.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Jen rozhraní. Modely odpovídají v jazyce, ve kterém jim píšete, bez ohledu na toto nastavení.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Jen rozhraní. Modely odpovídají v jazyce, ve kterém jim píšete, bez ohledu na toto nastavení.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Posledních %(n)s spuštění selhalo: %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Mikrofon se nepodařilo otevřít: %(why)s",
|
||||||
"The model's own": "Podle modelu",
|
"The model's own": "Podle modelu",
|
||||||
"The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí",
|
"The most an agent chat may do here": "Nejvíc, co tu agentní konverzace smí",
|
||||||
"The next command": "Další příkaz",
|
"The next command": "Další příkaz",
|
||||||
@@ -357,6 +367,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "Odpověď je hotová.",
|
"The reply is ready.": "Odpověď je hotová.",
|
||||||
"The result is no longer there.": "Výsledek už tu není.",
|
"The result is no longer there.": "Výsledek už tu není.",
|
||||||
"The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s",
|
"The server refused this computer's link: %(why)s": "Server odmítl propojení tohoto počítače: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "V odpovědi serveru nebyl žádný obrázek.",
|
||||||
|
"The server's answer was not audio.": "Odpověď serveru nebyla zvuk.",
|
||||||
"The terminal could not be loaded.": "Terminál se nepodařilo načíst.",
|
"The terminal could not be loaded.": "Terminál se nepodařilo načíst.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potřebuje novější server LLeMbas. Požádejte jeho správce o aktualizaci.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potřebuje novější server LLeMbas. Požádejte jeho správce o aktualizaci.",
|
||||||
"Theme": "Vzhled",
|
"Theme": "Vzhled",
|
||||||
@@ -399,10 +411,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Čeká se na {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Čeká se na {{model}}: {{reason}}",
|
||||||
"What it does": "Co dělá",
|
"What it does": "Co dělá",
|
||||||
"What should happen, and when": "Co se má stát a kdy",
|
"What should happen, and when": "Co se má stát a kdy",
|
||||||
|
"What to draw…": "Co nakreslit…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Co si o vás vaše modely pamatují napříč konverzacemi. Cokoli tu můžete změnit nebo odstranit.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Co si o vás vaše modely pamatují napříč konverzacemi. Cokoli tu můžete změnit nebo odstranit.",
|
||||||
"When": "Kdy",
|
"When": "Kdy",
|
||||||
"When it matters": "Když na tom záleží",
|
"When it matters": "Když na tom záleží",
|
||||||
|
"Wide": "Široký",
|
||||||
"Writing": "Píše",
|
"Writing": "Píše",
|
||||||
|
"Writing down…": "Zapisuji…",
|
||||||
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
|
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
|
||||||
"Writing…": "Píše se…",
|
"Writing…": "Píše se…",
|
||||||
"Written to %(path)s": "Zapsáno do %(path)s",
|
"Written to %(path)s": "Zapsáno do %(path)s",
|
||||||
@@ -566,8 +581,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Diesen Chat löschen?",
|
"Delete this chat?": "Diesen Chat löschen?",
|
||||||
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
|
"Delete this scheduled task?": "Diese geplante Aufgabe löschen?",
|
||||||
"Deny": "Ablehnen",
|
"Deny": "Ablehnen",
|
||||||
|
"Dictate a message": "Nachricht diktieren",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
|
"Disconnected. Close and reopen to reconnect.": "Getrennt. Schließen und erneut öffnen, um neu zu verbinden.",
|
||||||
"Done": "Fertig",
|
"Done": "Fertig",
|
||||||
|
"Draw": "Zeichnen",
|
||||||
|
"Draw a picture": "Ein Bild zeichnen",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Wird gezeichnet… ein Bild kann ein bis zwei Minuten dauern.",
|
||||||
"Drop the files to attach them": "Dateien ablegen, um sie anzuhängen",
|
"Drop the files to attach them": "Dateien ablegen, um sie anzuhängen",
|
||||||
"Due %(when)s": "Fällig %(when)s",
|
"Due %(when)s": "Fällig %(when)s",
|
||||||
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
|
"Each run sees the ones before it.": "Jeder Lauf sieht die vorherigen.",
|
||||||
@@ -698,6 +717,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "In der Warteschlange",
|
"Queued": "In der Warteschlange",
|
||||||
"Queued: {{ahead}} ahead": "In der Warteschlange: {{ahead}} davor",
|
"Queued: {{ahead}} ahead": "In der Warteschlange: {{ahead}} davor",
|
||||||
"Read again": "Neu lesen",
|
"Read again": "Neu lesen",
|
||||||
|
"Read this aloud": "Vorlesen",
|
||||||
"Reading the prompt": "Lese den Prompt",
|
"Reading the prompt": "Lese den Prompt",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lese den Prompt: {{done}} / {{total}} Tokens ({{percent}} %)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lese den Prompt: {{done}} / {{total}} Tokens ({{percent}} %)",
|
||||||
"Reasoning effort": "Denkaufwand",
|
"Reasoning effort": "Denkaufwand",
|
||||||
@@ -740,6 +760,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Set the time yourself": "Zeit selbst festlegen",
|
"Set the time yourself": "Zeit selbst festlegen",
|
||||||
"Settings": "Einstellungen",
|
"Settings": "Einstellungen",
|
||||||
|
"Shape": "Form",
|
||||||
"Show %(n)s more": "%(n)s weitere anzeigen",
|
"Show %(n)s more": "%(n)s weitere anzeigen",
|
||||||
"Shown by the agent": "Vom Agenten gezeigt",
|
"Shown by the agent": "Vom Agenten gezeigt",
|
||||||
"Sidebar": "Seitenleiste",
|
"Sidebar": "Seitenleiste",
|
||||||
@@ -753,11 +774,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Ausgelassen",
|
"Skipped": "Ausgelassen",
|
||||||
"Something to remember…": "Etwas, das behalten werden soll…",
|
"Something to remember…": "Etwas, das behalten werden soll…",
|
||||||
"Source": "Quelltext",
|
"Source": "Quelltext",
|
||||||
|
"Square": "Quadratisch",
|
||||||
"Start the agent": "Agent starten",
|
"Start the agent": "Agent starten",
|
||||||
"Stop": "Stoppen",
|
"Stop": "Stoppen",
|
||||||
"Stop %(name)s": "%(name)s stoppen",
|
"Stop %(name)s": "%(name)s stoppen",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "%(name)s und die Mitglieder darunter stoppen? Sie verlassen das Team.",
|
"Stop %(name)s and the members under it? They leave the team.": "%(name)s und die Mitglieder darunter stoppen? Sie verlassen das Team.",
|
||||||
"Stop %(name)s and the subagents under it?": "%(name)s und die Subagenten darunter stoppen?",
|
"Stop %(name)s and the subagents under it?": "%(name)s und die Subagenten darunter stoppen?",
|
||||||
|
"Stop dictating": "Diktieren beenden",
|
||||||
"Stop the agent": "Agent anhalten",
|
"Stop the agent": "Agent anhalten",
|
||||||
"Stopped": "Angehalten",
|
"Stopped": "Angehalten",
|
||||||
"Stopped.": "Angehalten.",
|
"Stopped.": "Angehalten.",
|
||||||
@@ -769,6 +792,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Nur hier ausgeschaltet. Was anfangs an ist, ist Ihr Standard für dieses Modell, unter Einstellungen → Modelle.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Nur hier ausgeschaltet. Was anfangs an ist, ist Ihr Standard für dieses Modell, unter Einstellungen → Modelle.",
|
||||||
"System prompt": "Systemprompt",
|
"System prompt": "Systemprompt",
|
||||||
"Taken in while it ran:": "Während des Laufs übernommen:",
|
"Taken in while it ran:": "Während des Laufs übernommen:",
|
||||||
|
"Tall": "Hoch",
|
||||||
"Tasks: %(n)s": "Aufgaben: %(n)s",
|
"Tasks: %(n)s": "Aufgaben: %(n)s",
|
||||||
"Team member": "Teammitglied",
|
"Team member": "Teammitglied",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten",
|
"Team: %(size)s of at most %(max)s subagents": "Team: %(size)s von höchstens %(max)s Subagenten",
|
||||||
@@ -796,6 +820,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "Die Anweisung, die jeder Lauf für sich erhält: Sagen Sie alles, was er braucht.",
|
"The instruction each run is given, on its own: say everything it needs.": "Die Anweisung, die jeder Lauf für sich erhält: Sagen Sie alles, was er braucht.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Nur die Oberfläche. Modelle antworten in der Sprache, in der Sie ihnen schreiben, egal was hier steht.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Nur die Oberfläche. Modelle antworten in der Sprache, in der Sie ihnen schreiben, egal was hier steht.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Die letzten %(n)s Läufe sind fehlgeschlagen: %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Das Mikrofon konnte nicht geöffnet werden: %(why)s",
|
||||||
"The model's own": "Die des Modells",
|
"The model's own": "Die des Modells",
|
||||||
"The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf",
|
"The most an agent chat may do here": "Das Meiste, was ein Agent-Chat hier darf",
|
||||||
"The next command": "Nächster Befehl",
|
"The next command": "Nächster Befehl",
|
||||||
@@ -807,6 +832,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "Die Antwort ist fertig.",
|
"The reply is ready.": "Die Antwort ist fertig.",
|
||||||
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
|
"The result is no longer there.": "Das Ergebnis ist nicht mehr da.",
|
||||||
"The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s",
|
"The server refused this computer's link: %(why)s": "Der Server hat die Verknüpfung dieses Computers abgelehnt: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "Die Antwort des Servers enthielt kein Bild.",
|
||||||
|
"The server's answer was not audio.": "Die Antwort des Servers war kein Audio.",
|
||||||
"The terminal could not be loaded.": "Das Terminal konnte nicht geladen werden.",
|
"The terminal could not be loaded.": "Das Terminal konnte nicht geladen werden.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Das Terminal braucht einen neueren LLeMbas-Server. Bitten Sie seinen Administrator, ihn zu aktualisieren.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Das Terminal braucht einen neueren LLeMbas-Server. Bitten Sie seinen Administrator, ihn zu aktualisieren.",
|
||||||
"Theme": "Design",
|
"Theme": "Design",
|
||||||
@@ -849,10 +876,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Warte auf {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Warte auf {{model}}: {{reason}}",
|
||||||
"What it does": "Was sie tut",
|
"What it does": "Was sie tut",
|
||||||
"What should happen, and when": "Was soll wann geschehen?",
|
"What should happen, and when": "Was soll wann geschehen?",
|
||||||
|
"What to draw…": "Was gezeichnet werden soll…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Was Ihre Modelle über Sie behalten, über alle Chats hinweg. Hier können Sie alles ändern oder entfernen.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Was Ihre Modelle über Sie behalten, über alle Chats hinweg. Hier können Sie alles ändern oder entfernen.",
|
||||||
"When": "Wann",
|
"When": "Wann",
|
||||||
"When it matters": "Wenn es zählt",
|
"When it matters": "Wenn es zählt",
|
||||||
|
"Wide": "Breit",
|
||||||
"Writing": "Schreibe",
|
"Writing": "Schreibe",
|
||||||
|
"Writing down…": "Wird aufgeschrieben…",
|
||||||
"Writing: {{rate}} tok/s": "Schreibe: {{rate}} Tok/s",
|
"Writing: {{rate}} tok/s": "Schreibe: {{rate}} Tok/s",
|
||||||
"Writing…": "Schreibt…",
|
"Writing…": "Schreibt…",
|
||||||
"Written to %(path)s": "Geschrieben nach %(path)s",
|
"Written to %(path)s": "Geschrieben nach %(path)s",
|
||||||
@@ -1016,8 +1046,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "¿Eliminar este chat?",
|
"Delete this chat?": "¿Eliminar este chat?",
|
||||||
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
|
"Delete this scheduled task?": "¿Eliminar esta tarea programada?",
|
||||||
"Deny": "Denegar",
|
"Deny": "Denegar",
|
||||||
|
"Dictate a message": "Dictar un mensaje",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
|
"Disconnected. Close and reopen to reconnect.": "Desconectado. Cierre y vuelva a abrir para reconectar.",
|
||||||
"Done": "Hecho",
|
"Done": "Hecho",
|
||||||
|
"Draw": "Dibujar",
|
||||||
|
"Draw a picture": "Dibujar una imagen",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Dibujando… una imagen puede tardar uno o dos minutos.",
|
||||||
"Drop the files to attach them": "Suelte los archivos para adjuntarlos",
|
"Drop the files to attach them": "Suelte los archivos para adjuntarlos",
|
||||||
"Due %(when)s": "Prevista %(when)s",
|
"Due %(when)s": "Prevista %(when)s",
|
||||||
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
|
"Each run sees the ones before it.": "Cada ejecución ve las anteriores.",
|
||||||
@@ -1148,6 +1182,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "En cola",
|
"Queued": "En cola",
|
||||||
"Queued: {{ahead}} ahead": "En cola: {{ahead}} por delante",
|
"Queued: {{ahead}} ahead": "En cola: {{ahead}} por delante",
|
||||||
"Read again": "Leer de nuevo",
|
"Read again": "Leer de nuevo",
|
||||||
|
"Read this aloud": "Leer en voz alta",
|
||||||
"Reading the prompt": "Leyendo el prompt",
|
"Reading the prompt": "Leyendo el prompt",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Leyendo el prompt: {{done}} / {{total}} tokens ({{percent}} %)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Leyendo el prompt: {{done}} / {{total}} tokens ({{percent}} %)",
|
||||||
"Reasoning effort": "Esfuerzo de razonamiento",
|
"Reasoning effort": "Esfuerzo de razonamiento",
|
||||||
@@ -1190,6 +1225,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Servidor",
|
"Server": "Servidor",
|
||||||
"Set the time yourself": "Fijar la hora usted mismo",
|
"Set the time yourself": "Fijar la hora usted mismo",
|
||||||
"Settings": "Ajustes",
|
"Settings": "Ajustes",
|
||||||
|
"Shape": "Forma",
|
||||||
"Show %(n)s more": "Mostrar %(n)s más",
|
"Show %(n)s more": "Mostrar %(n)s más",
|
||||||
"Shown by the agent": "Mostrado por el agente",
|
"Shown by the agent": "Mostrado por el agente",
|
||||||
"Sidebar": "Barra lateral",
|
"Sidebar": "Barra lateral",
|
||||||
@@ -1203,11 +1239,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Omitida",
|
"Skipped": "Omitida",
|
||||||
"Something to remember…": "Algo que recordar…",
|
"Something to remember…": "Algo que recordar…",
|
||||||
"Source": "Código fuente",
|
"Source": "Código fuente",
|
||||||
|
"Square": "Cuadrada",
|
||||||
"Start the agent": "Iniciar el agente",
|
"Start the agent": "Iniciar el agente",
|
||||||
"Stop": "Detener",
|
"Stop": "Detener",
|
||||||
"Stop %(name)s": "Detener %(name)s",
|
"Stop %(name)s": "Detener %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "¿Detener %(name)s y a los miembros que dependen de él? Dejan el equipo.",
|
"Stop %(name)s and the members under it? They leave the team.": "¿Detener %(name)s y a los miembros que dependen de él? Dejan el equipo.",
|
||||||
"Stop %(name)s and the subagents under it?": "¿Detener a %(name)s y a los subagentes que tiene debajo?",
|
"Stop %(name)s and the subagents under it?": "¿Detener a %(name)s y a los subagentes que tiene debajo?",
|
||||||
|
"Stop dictating": "Dejar de dictar",
|
||||||
"Stop the agent": "Detener el agente",
|
"Stop the agent": "Detener el agente",
|
||||||
"Stopped": "Detenido",
|
"Stopped": "Detenido",
|
||||||
"Stopped.": "Detenido.",
|
"Stopped.": "Detenido.",
|
||||||
@@ -1219,6 +1257,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Desactivado solo aquí. Lo que está activado al principio es su valor predeterminado para este modelo, en Ajustes → Modelos.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Desactivado solo aquí. Lo que está activado al principio es su valor predeterminado para este modelo, en Ajustes → Modelos.",
|
||||||
"System prompt": "Prompt de sistema",
|
"System prompt": "Prompt de sistema",
|
||||||
"Taken in while it ran:": "Incorporado mientras se ejecutaba:",
|
"Taken in while it ran:": "Incorporado mientras se ejecutaba:",
|
||||||
|
"Tall": "Alta",
|
||||||
"Tasks: %(n)s": "Tareas: %(n)s",
|
"Tasks: %(n)s": "Tareas: %(n)s",
|
||||||
"Team member": "Miembro del equipo",
|
"Team member": "Miembro del equipo",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes",
|
"Team: %(size)s of at most %(max)s subagents": "Equipo: %(size)s de un máximo de %(max)s subagentes",
|
||||||
@@ -1246,6 +1285,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "La instrucción que recibe cada ejecución por sí sola: diga todo lo que necesita.",
|
"The instruction each run is given, on its own: say everything it needs.": "La instrucción que recibe cada ejecución por sí sola: diga todo lo que necesita.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo la interfaz. Los modelos responden en el idioma en el que usted les escribe, diga lo que diga esto.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo la interfaz. Los modelos responden en el idioma en el que usted les escribe, diga lo que diga esto.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Las últimas %(n)s ejecuciones fallaron: %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "No se pudo abrir el micrófono: %(why)s",
|
||||||
"The model's own": "La del modelo",
|
"The model's own": "La del modelo",
|
||||||
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
|
"The most an agent chat may do here": "Lo máximo que un chat de agente puede hacer aquí",
|
||||||
"The next command": "El comando siguiente",
|
"The next command": "El comando siguiente",
|
||||||
@@ -1257,6 +1297,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "La respuesta está lista.",
|
"The reply is ready.": "La respuesta está lista.",
|
||||||
"The result is no longer there.": "El resultado ya no está.",
|
"The result is no longer there.": "El resultado ya no está.",
|
||||||
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
|
"The server refused this computer's link: %(why)s": "El servidor rechazó la vinculación de este equipo: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "La respuesta del servidor no traía ninguna imagen.",
|
||||||
|
"The server's answer was not audio.": "La respuesta del servidor no era audio.",
|
||||||
"The terminal could not be loaded.": "No se ha podido cargar el terminal.",
|
"The terminal could not be loaded.": "No se ha podido cargar el terminal.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "El terminal necesita un servidor LLeMbas más reciente. Pida a su administrador que lo actualice.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "El terminal necesita un servidor LLeMbas más reciente. Pida a su administrador que lo actualice.",
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
@@ -1299,10 +1341,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Esperando a {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Esperando a {{model}}: {{reason}}",
|
||||||
"What it does": "Qué hace",
|
"What it does": "Qué hace",
|
||||||
"What should happen, and when": "Qué debe ocurrir, y cuándo",
|
"What should happen, and when": "Qué debe ocurrir, y cuándo",
|
||||||
|
"What to draw…": "Qué dibujar…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Lo que sus modelos recuerdan de usted entre chats. Aquí puede cambiar o quitar cualquier cosa.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Lo que sus modelos recuerdan de usted entre chats. Aquí puede cambiar o quitar cualquier cosa.",
|
||||||
"When": "Cuándo",
|
"When": "Cuándo",
|
||||||
"When it matters": "Cuando importa",
|
"When it matters": "Cuando importa",
|
||||||
|
"Wide": "Ancha",
|
||||||
"Writing": "Escribiendo",
|
"Writing": "Escribiendo",
|
||||||
|
"Writing down…": "Transcribiendo…",
|
||||||
"Writing: {{rate}} tok/s": "Escribiendo: {{rate}} tok/s",
|
"Writing: {{rate}} tok/s": "Escribiendo: {{rate}} tok/s",
|
||||||
"Writing…": "Escribiendo…",
|
"Writing…": "Escribiendo…",
|
||||||
"Written to %(path)s": "Escrito en %(path)s",
|
"Written to %(path)s": "Escrito en %(path)s",
|
||||||
@@ -1466,8 +1511,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Supprimer cette conversation ?",
|
"Delete this chat?": "Supprimer cette conversation ?",
|
||||||
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
|
"Delete this scheduled task?": "Supprimer cette tâche planifiée ?",
|
||||||
"Deny": "Refuser",
|
"Deny": "Refuser",
|
||||||
|
"Dictate a message": "Dicter un message",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
|
"Disconnected. Close and reopen to reconnect.": "Déconnecté. Fermez et rouvrez pour vous reconnecter.",
|
||||||
"Done": "Terminé",
|
"Done": "Terminé",
|
||||||
|
"Draw": "Dessiner",
|
||||||
|
"Draw a picture": "Dessiner une image",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Dessin en cours… une image peut prendre une minute ou deux.",
|
||||||
"Drop the files to attach them": "Déposez les fichiers pour les joindre",
|
"Drop the files to attach them": "Déposez les fichiers pour les joindre",
|
||||||
"Due %(when)s": "Prévue %(when)s",
|
"Due %(when)s": "Prévue %(when)s",
|
||||||
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
|
"Each run sees the ones before it.": "Chaque exécution voit les précédentes.",
|
||||||
@@ -1598,6 +1647,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "En file d’attente",
|
"Queued": "En file d’attente",
|
||||||
"Queued: {{ahead}} ahead": "En file d’attente : {{ahead}} avant",
|
"Queued: {{ahead}} ahead": "En file d’attente : {{ahead}} avant",
|
||||||
"Read again": "Relire",
|
"Read again": "Relire",
|
||||||
|
"Read this aloud": "Lire à voix haute",
|
||||||
"Reading the prompt": "Lecture du prompt",
|
"Reading the prompt": "Lecture du prompt",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lecture du prompt : {{done}} / {{total}} jetons ({{percent}} %)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lecture du prompt : {{done}} / {{total}} jetons ({{percent}} %)",
|
||||||
"Reasoning effort": "Effort de raisonnement",
|
"Reasoning effort": "Effort de raisonnement",
|
||||||
@@ -1640,6 +1690,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Serveur",
|
"Server": "Serveur",
|
||||||
"Set the time yourself": "Régler l'heure vous-même",
|
"Set the time yourself": "Régler l'heure vous-même",
|
||||||
"Settings": "Paramètres",
|
"Settings": "Paramètres",
|
||||||
|
"Shape": "Forme",
|
||||||
"Show %(n)s more": "Afficher %(n)s de plus",
|
"Show %(n)s more": "Afficher %(n)s de plus",
|
||||||
"Shown by the agent": "Montré par l'agent",
|
"Shown by the agent": "Montré par l'agent",
|
||||||
"Sidebar": "Barre latérale",
|
"Sidebar": "Barre latérale",
|
||||||
@@ -1653,11 +1704,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Ignorée",
|
"Skipped": "Ignorée",
|
||||||
"Something to remember…": "Quelque chose à retenir…",
|
"Something to remember…": "Quelque chose à retenir…",
|
||||||
"Source": "Source",
|
"Source": "Source",
|
||||||
|
"Square": "Carrée",
|
||||||
"Start the agent": "Démarrer l'agent",
|
"Start the agent": "Démarrer l'agent",
|
||||||
"Stop": "Arrêter",
|
"Stop": "Arrêter",
|
||||||
"Stop %(name)s": "Arrêter %(name)s",
|
"Stop %(name)s": "Arrêter %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Arrêter %(name)s et les membres qui en dépendent ? Ils quittent l’équipe.",
|
"Stop %(name)s and the members under it? They leave the team.": "Arrêter %(name)s et les membres qui en dépendent ? Ils quittent l’équipe.",
|
||||||
"Stop %(name)s and the subagents under it?": "Arrêter %(name)s et les sous-agents en dessous ?",
|
"Stop %(name)s and the subagents under it?": "Arrêter %(name)s et les sous-agents en dessous ?",
|
||||||
|
"Stop dictating": "Arrêter la dictée",
|
||||||
"Stop the agent": "Arrêter l'agent",
|
"Stop the agent": "Arrêter l'agent",
|
||||||
"Stopped": "Arrêté",
|
"Stopped": "Arrêté",
|
||||||
"Stopped.": "Arrêté.",
|
"Stopped.": "Arrêté.",
|
||||||
@@ -1669,6 +1722,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Désactivé ici seulement. Ce qui est activé au départ est votre réglage par défaut pour ce modèle, dans Paramètres → Modèles.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Désactivé ici seulement. Ce qui est activé au départ est votre réglage par défaut pour ce modèle, dans Paramètres → Modèles.",
|
||||||
"System prompt": "Prompt système",
|
"System prompt": "Prompt système",
|
||||||
"Taken in while it ran:": "Pris en compte pendant l'exécution :",
|
"Taken in while it ran:": "Pris en compte pendant l'exécution :",
|
||||||
|
"Tall": "Haute",
|
||||||
"Tasks: %(n)s": "Tâches : %(n)s",
|
"Tasks: %(n)s": "Tâches : %(n)s",
|
||||||
"Team member": "Membre de l’équipe",
|
"Team member": "Membre de l’équipe",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus",
|
"Team: %(size)s of at most %(max)s subagents": "Équipe : %(size)s sur %(max)s sous-agents au plus",
|
||||||
@@ -1696,6 +1750,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "L'instruction donnée à chaque exécution, seule : dites tout ce dont elle a besoin.",
|
"The instruction each run is given, on its own: say everything it needs.": "L'instruction donnée à chaque exécution, seule : dites tout ce dont elle a besoin.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "L'interface uniquement. Les modèles répondent dans la langue dans laquelle vous leur écrivez, quoi qu'indique ce réglage.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "L'interface uniquement. Les modèles répondent dans la langue dans laquelle vous leur écrivez, quoi qu'indique ce réglage.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Les %(n)s dernières exécutions ont échoué : %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Le micro n’a pas pu être ouvert : %(why)s",
|
||||||
"The model's own": "Celle du modèle",
|
"The model's own": "Celle du modèle",
|
||||||
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
|
"The most an agent chat may do here": "Le maximum qu'une conversation d'agent peut faire ici",
|
||||||
"The next command": "La commande suivante",
|
"The next command": "La commande suivante",
|
||||||
@@ -1707,6 +1762,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "La réponse est prête.",
|
"The reply is ready.": "La réponse est prête.",
|
||||||
"The result is no longer there.": "Le résultat n'est plus là.",
|
"The result is no longer there.": "Le résultat n'est plus là.",
|
||||||
"The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s",
|
"The server refused this computer's link: %(why)s": "Le serveur a refusé la liaison de cet ordinateur : %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "La réponse du serveur ne contenait aucune image.",
|
||||||
|
"The server's answer was not audio.": "La réponse du serveur n’était pas de l’audio.",
|
||||||
"The terminal could not be loaded.": "Le terminal n'a pas pu être chargé.",
|
"The terminal could not be loaded.": "Le terminal n'a pas pu être chargé.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Le terminal nécessite un serveur LLeMbas plus récent. Demandez à son administrateur de le mettre à jour.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Le terminal nécessite un serveur LLeMbas plus récent. Demandez à son administrateur de le mettre à jour.",
|
||||||
"Theme": "Thème",
|
"Theme": "Thème",
|
||||||
@@ -1749,10 +1806,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "En attente de {{model}} : {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "En attente de {{model}} : {{reason}}",
|
||||||
"What it does": "Ce qu’elle fait",
|
"What it does": "Ce qu’elle fait",
|
||||||
"What should happen, and when": "Ce qui doit se passer, et quand",
|
"What should happen, and when": "Ce qui doit se passer, et quand",
|
||||||
|
"What to draw…": "Que dessiner…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Ce que vos modèles retiennent de vous, d’une conversation à l’autre. Modifiez ou supprimez ce que vous voulez ici.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Ce que vos modèles retiennent de vous, d’une conversation à l’autre. Modifiez ou supprimez ce que vous voulez ici.",
|
||||||
"When": "Quand",
|
"When": "Quand",
|
||||||
"When it matters": "Quand c'est important",
|
"When it matters": "Quand c'est important",
|
||||||
|
"Wide": "Large",
|
||||||
"Writing": "Écriture",
|
"Writing": "Écriture",
|
||||||
|
"Writing down…": "Transcription…",
|
||||||
"Writing: {{rate}} tok/s": "Écriture : {{rate}} jetons/s",
|
"Writing: {{rate}} tok/s": "Écriture : {{rate}} jetons/s",
|
||||||
"Writing…": "Rédaction…",
|
"Writing…": "Rédaction…",
|
||||||
"Written to %(path)s": "Écrit dans %(path)s",
|
"Written to %(path)s": "Écrit dans %(path)s",
|
||||||
@@ -1916,8 +1976,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Törli ezt a csevegést?",
|
"Delete this chat?": "Törli ezt a csevegést?",
|
||||||
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
|
"Delete this scheduled task?": "Törli ezt az ütemezett feladatot?",
|
||||||
"Deny": "Elutasítás",
|
"Deny": "Elutasítás",
|
||||||
|
"Dictate a message": "Üzenet diktálása",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Kapcsolat bontva. Az újrakapcsolódáshoz zárja be és nyissa meg újra.",
|
"Disconnected. Close and reopen to reconnect.": "Kapcsolat bontva. Az újrakapcsolódáshoz zárja be és nyissa meg újra.",
|
||||||
"Done": "Kész",
|
"Done": "Kész",
|
||||||
|
"Draw": "Rajzolás",
|
||||||
|
"Draw a picture": "Kép rajzolása",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Rajzolás… egy kép egy-két percig is eltarthat.",
|
||||||
"Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz",
|
"Drop the files to attach them": "Engedje el a fájlokat a csatoláshoz",
|
||||||
"Due %(when)s": "Esedékes: %(when)s",
|
"Due %(when)s": "Esedékes: %(when)s",
|
||||||
"Each run sees the ones before it.": "Minden futás látja az előzőeket.",
|
"Each run sees the ones before it.": "Minden futás látja az előzőeket.",
|
||||||
@@ -2048,6 +2112,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "Sorban áll",
|
"Queued": "Sorban áll",
|
||||||
"Queued: {{ahead}} ahead": "Sorban áll: {{ahead}} van előtte",
|
"Queued: {{ahead}} ahead": "Sorban áll: {{ahead}} van előtte",
|
||||||
"Read again": "Újraolvasás",
|
"Read again": "Újraolvasás",
|
||||||
|
"Read this aloud": "Felolvasás",
|
||||||
"Reading the prompt": "A prompt olvasása",
|
"Reading the prompt": "A prompt olvasása",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "A prompt olvasása: {{done}} / {{total}} token ({{percent}}%)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "A prompt olvasása: {{done}} / {{total}} token ({{percent}}%)",
|
||||||
"Reasoning effort": "Gondolkodási erőfeszítés",
|
"Reasoning effort": "Gondolkodási erőfeszítés",
|
||||||
@@ -2090,6 +2155,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Kiszolgáló",
|
"Server": "Kiszolgáló",
|
||||||
"Set the time yourself": "Az időpont megadása kézzel",
|
"Set the time yourself": "Az időpont megadása kézzel",
|
||||||
"Settings": "Beállítások",
|
"Settings": "Beállítások",
|
||||||
|
"Shape": "Forma",
|
||||||
"Show %(n)s more": "Még %(n)s megjelenítése",
|
"Show %(n)s more": "Még %(n)s megjelenítése",
|
||||||
"Shown by the agent": "Az ügynök mutatta",
|
"Shown by the agent": "Az ügynök mutatta",
|
||||||
"Sidebar": "Oldalsáv",
|
"Sidebar": "Oldalsáv",
|
||||||
@@ -2103,11 +2169,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Kihagyva",
|
"Skipped": "Kihagyva",
|
||||||
"Something to remember…": "Valami, amit meg kell jegyezni…",
|
"Something to remember…": "Valami, amit meg kell jegyezni…",
|
||||||
"Source": "Forrás",
|
"Source": "Forrás",
|
||||||
|
"Square": "Négyzet",
|
||||||
"Start the agent": "Ügynök indítása",
|
"Start the agent": "Ügynök indítása",
|
||||||
"Stop": "Leállítás",
|
"Stop": "Leállítás",
|
||||||
"Stop %(name)s": "%(name)s leállítása",
|
"Stop %(name)s": "%(name)s leállítása",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Leállítja %(name)s tagot és az alatta lévőket? Kilépnek a csapatból.",
|
"Stop %(name)s and the members under it? They leave the team.": "Leállítja %(name)s tagot és az alatta lévőket? Kilépnek a csapatból.",
|
||||||
"Stop %(name)s and the subagents under it?": "Leállítod: %(name)s, és az alatta lévő alügynököket?",
|
"Stop %(name)s and the subagents under it?": "Leállítod: %(name)s, és az alatta lévő alügynököket?",
|
||||||
|
"Stop dictating": "Diktálás leállítása",
|
||||||
"Stop the agent": "Ügynök leállítása",
|
"Stop the agent": "Ügynök leállítása",
|
||||||
"Stopped": "Leállítva",
|
"Stopped": "Leállítva",
|
||||||
"Stopped.": "Leállítva.",
|
"Stopped.": "Leállítva.",
|
||||||
@@ -2119,6 +2187,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Csak itt kikapcsolva. Hogy kezdetben mi van bekapcsolva, az ennél a modellnél az Ön alapértelmezése, a Beállítások → Modellek alatt.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Csak itt kikapcsolva. Hogy kezdetben mi van bekapcsolva, az ennél a modellnél az Ön alapértelmezése, a Beállítások → Modellek alatt.",
|
||||||
"System prompt": "Rendszerprompt",
|
"System prompt": "Rendszerprompt",
|
||||||
"Taken in while it ran:": "Futás közben átvéve:",
|
"Taken in while it ran:": "Futás közben átvéve:",
|
||||||
|
"Tall": "Magas",
|
||||||
"Tasks: %(n)s": "Feladatok: %(n)s",
|
"Tasks: %(n)s": "Feladatok: %(n)s",
|
||||||
"Team member": "Csapattag",
|
"Team member": "Csapattag",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből",
|
"Team: %(size)s of at most %(max)s subagents": "Csapat: %(size)s a legfeljebb %(max)s alügynökből",
|
||||||
@@ -2146,6 +2215,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "Az utasítás, amelyet minden futás önmagában kap: mondjon el mindent, amire szüksége van.",
|
"The instruction each run is given, on its own: say everything it needs.": "Az utasítás, amelyet minden futás önmagában kap: mondjon el mindent, amire szüksége van.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Csak a felület. A modellek azon a nyelven válaszolnak, amelyen ír nekik, függetlenül ettől a beállítástól.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Csak a felület. A modellek azon a nyelven válaszolnak, amelyen ír nekik, függetlenül ettől a beállítástól.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Az utolsó %(n)s futás sikertelen: %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "A mikrofont nem sikerült megnyitni: %(why)s",
|
||||||
"The model's own": "A modell sajátja",
|
"The model's own": "A modell sajátja",
|
||||||
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
|
"The most an agent chat may do here": "A legtöbb, amit egy ügynök-csevegés itt tehet",
|
||||||
"The next command": "Következő parancs",
|
"The next command": "Következő parancs",
|
||||||
@@ -2157,6 +2227,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "A válasz kész.",
|
"The reply is ready.": "A válasz kész.",
|
||||||
"The result is no longer there.": "Az eredmény már nincs meg.",
|
"The result is no longer there.": "Az eredmény már nincs meg.",
|
||||||
"The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s",
|
"The server refused this computer's link: %(why)s": "A szerver elutasította ennek a számítógépnek a kapcsolatát: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "A szerver válaszában nem volt kép.",
|
||||||
|
"The server's answer was not audio.": "A szerver válasza nem hang volt.",
|
||||||
"The terminal could not be loaded.": "A terminált nem sikerült betölteni.",
|
"The terminal could not be loaded.": "A terminált nem sikerült betölteni.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "A terminálhoz újabb LLeMbas-szerver kell. Kérje meg a rendszergazdáját, hogy frissítse.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "A terminálhoz újabb LLeMbas-szerver kell. Kérje meg a rendszergazdáját, hogy frissítse.",
|
||||||
"Theme": "Téma",
|
"Theme": "Téma",
|
||||||
@@ -2199,10 +2271,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Várakozás a(z) {{model}} modellre: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Várakozás a(z) {{model}} modellre: {{reason}}",
|
||||||
"What it does": "Mit csinál",
|
"What it does": "Mit csinál",
|
||||||
"What should happen, and when": "Mi történjen, és mikor",
|
"What should happen, and when": "Mi történjen, és mikor",
|
||||||
|
"What to draw…": "Mit rajzoljon…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Amit a modelljei megjegyeznek Önről a beszélgetéseken át. Itt bármit módosíthat vagy eltávolíthat.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Amit a modelljei megjegyeznek Önről a beszélgetéseken át. Itt bármit módosíthat vagy eltávolíthat.",
|
||||||
"When": "Mikor",
|
"When": "Mikor",
|
||||||
"When it matters": "Ha számít",
|
"When it matters": "Ha számít",
|
||||||
|
"Wide": "Széles",
|
||||||
"Writing": "Írás",
|
"Writing": "Írás",
|
||||||
|
"Writing down…": "Leírás…",
|
||||||
"Writing: {{rate}} tok/s": "Írás: {{rate}} token/s",
|
"Writing: {{rate}} tok/s": "Írás: {{rate}} token/s",
|
||||||
"Writing…": "Írás…",
|
"Writing…": "Írás…",
|
||||||
"Written to %(path)s": "Kiírva ide: %(path)s",
|
"Written to %(path)s": "Kiírva ide: %(path)s",
|
||||||
@@ -2366,8 +2441,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Eliminare questa chat?",
|
"Delete this chat?": "Eliminare questa chat?",
|
||||||
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
|
"Delete this scheduled task?": "Eliminare questa attività pianificata?",
|
||||||
"Deny": "Nega",
|
"Deny": "Nega",
|
||||||
|
"Dictate a message": "Detta un messaggio",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
|
"Disconnected. Close and reopen to reconnect.": "Disconnesso. Chiudi e riapri per riconnetterti.",
|
||||||
"Done": "Fatto",
|
"Done": "Fatto",
|
||||||
|
"Draw": "Disegna",
|
||||||
|
"Draw a picture": "Disegna un'immagine",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Disegno in corso… un’immagine può richiedere un minuto o due.",
|
||||||
"Drop the files to attach them": "Rilasci i file per allegarli",
|
"Drop the files to attach them": "Rilasci i file per allegarli",
|
||||||
"Due %(when)s": "Prevista %(when)s",
|
"Due %(when)s": "Prevista %(when)s",
|
||||||
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
|
"Each run sees the ones before it.": "Ogni esecuzione vede quelle precedenti.",
|
||||||
@@ -2498,6 +2577,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "In coda",
|
"Queued": "In coda",
|
||||||
"Queued: {{ahead}} ahead": "In coda: {{ahead}} davanti",
|
"Queued: {{ahead}} ahead": "In coda: {{ahead}} davanti",
|
||||||
"Read again": "Rileggi",
|
"Read again": "Rileggi",
|
||||||
|
"Read this aloud": "Leggi ad alta voce",
|
||||||
"Reading the prompt": "Lettura del prompt",
|
"Reading the prompt": "Lettura del prompt",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lettura del prompt: {{done}} / {{total}} token ({{percent}}%)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Lettura del prompt: {{done}} / {{total}} token ({{percent}}%)",
|
||||||
"Reasoning effort": "Sforzo di ragionamento",
|
"Reasoning effort": "Sforzo di ragionamento",
|
||||||
@@ -2540,6 +2620,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Set the time yourself": "Imposta tu l’orario",
|
"Set the time yourself": "Imposta tu l’orario",
|
||||||
"Settings": "Impostazioni",
|
"Settings": "Impostazioni",
|
||||||
|
"Shape": "Forma",
|
||||||
"Show %(n)s more": "Mostra altri %(n)s",
|
"Show %(n)s more": "Mostra altri %(n)s",
|
||||||
"Shown by the agent": "Mostrato dall'agente",
|
"Shown by the agent": "Mostrato dall'agente",
|
||||||
"Sidebar": "Barra laterale",
|
"Sidebar": "Barra laterale",
|
||||||
@@ -2553,11 +2634,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Saltata",
|
"Skipped": "Saltata",
|
||||||
"Something to remember…": "Qualcosa da ricordare…",
|
"Something to remember…": "Qualcosa da ricordare…",
|
||||||
"Source": "Sorgente",
|
"Source": "Sorgente",
|
||||||
|
"Square": "Quadrata",
|
||||||
"Start the agent": "Avvia l'agente",
|
"Start the agent": "Avvia l'agente",
|
||||||
"Stop": "Ferma",
|
"Stop": "Ferma",
|
||||||
"Stop %(name)s": "Ferma %(name)s",
|
"Stop %(name)s": "Ferma %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Fermare %(name)s e i membri sotto di lui? Lasciano la squadra.",
|
"Stop %(name)s and the members under it? They leave the team.": "Fermare %(name)s e i membri sotto di lui? Lasciano la squadra.",
|
||||||
"Stop %(name)s and the subagents under it?": "Fermare %(name)s e i subagenti sotto di esso?",
|
"Stop %(name)s and the subagents under it?": "Fermare %(name)s e i subagenti sotto di esso?",
|
||||||
|
"Stop dictating": "Interrompi la dettatura",
|
||||||
"Stop the agent": "Ferma l'agente",
|
"Stop the agent": "Ferma l'agente",
|
||||||
"Stopped": "Fermato",
|
"Stopped": "Fermato",
|
||||||
"Stopped.": "Interrotto.",
|
"Stopped.": "Interrotto.",
|
||||||
@@ -2569,6 +2652,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Disattivato solo qui. Ciò che è attivo all'inizio è il tuo predefinito per questo modello, in Impostazioni → Modelli.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Disattivato solo qui. Ciò che è attivo all'inizio è il tuo predefinito per questo modello, in Impostazioni → Modelli.",
|
||||||
"System prompt": "Prompt di sistema",
|
"System prompt": "Prompt di sistema",
|
||||||
"Taken in while it ran:": "Accolto durante l'esecuzione:",
|
"Taken in while it ran:": "Accolto durante l'esecuzione:",
|
||||||
|
"Tall": "Alta",
|
||||||
"Tasks: %(n)s": "Attività: %(n)s",
|
"Tasks: %(n)s": "Attività: %(n)s",
|
||||||
"Team member": "Membro della squadra",
|
"Team member": "Membro della squadra",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti",
|
"Team: %(size)s of at most %(max)s subagents": "Squadra: %(size)s su un massimo di %(max)s sottoagenti",
|
||||||
@@ -2596,6 +2680,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "L’istruzione che ogni esecuzione riceve da sola: di’ tutto ciò che le serve.",
|
"The instruction each run is given, on its own: say everything it needs.": "L’istruzione che ogni esecuzione riceve da sola: di’ tutto ciò che le serve.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo l'interfaccia. I modelli rispondono nella lingua in cui scrivi loro, qualunque cosa dica questa impostazione.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Solo l'interfaccia. I modelli rispondono nella lingua in cui scrivi loro, qualunque cosa dica questa impostazione.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Le ultime %(n)s esecuzioni non sono riuscite: %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Impossibile aprire il microfono: %(why)s",
|
||||||
"The model's own": "Quello del modello",
|
"The model's own": "Quello del modello",
|
||||||
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
|
"The most an agent chat may do here": "Il massimo che una chat agente può fare qui",
|
||||||
"The next command": "Il comando successivo",
|
"The next command": "Il comando successivo",
|
||||||
@@ -2607,6 +2692,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "La risposta è pronta.",
|
"The reply is ready.": "La risposta è pronta.",
|
||||||
"The result is no longer there.": "Il risultato non c’è più.",
|
"The result is no longer there.": "Il risultato non c’è più.",
|
||||||
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
|
"The server refused this computer's link: %(why)s": "Il server ha rifiutato il collegamento di questo computer: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "La risposta del server non conteneva immagini.",
|
||||||
|
"The server's answer was not audio.": "La risposta del server non era audio.",
|
||||||
"The terminal could not be loaded.": "Impossibile caricare il terminale.",
|
"The terminal could not be loaded.": "Impossibile caricare il terminale.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Il terminale richiede un server LLeMbas più recente. Chiedi al suo amministratore di aggiornarlo.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Il terminale richiede un server LLeMbas più recente. Chiedi al suo amministratore di aggiornarlo.",
|
||||||
"Theme": "Tema",
|
"Theme": "Tema",
|
||||||
@@ -2649,10 +2736,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "In attesa di {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "In attesa di {{model}}: {{reason}}",
|
||||||
"What it does": "Cosa fa",
|
"What it does": "Cosa fa",
|
||||||
"What should happen, and when": "Cosa deve succedere, e quando",
|
"What should happen, and when": "Cosa deve succedere, e quando",
|
||||||
|
"What to draw…": "Cosa disegnare…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Ciò che i tuoi modelli ricordano di te tra una chat e l’altra. Qui puoi cambiare o rimuovere qualsiasi cosa.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Ciò che i tuoi modelli ricordano di te tra una chat e l’altra. Qui puoi cambiare o rimuovere qualsiasi cosa.",
|
||||||
"When": "Quando",
|
"When": "Quando",
|
||||||
"When it matters": "Quando conta",
|
"When it matters": "Quando conta",
|
||||||
|
"Wide": "Larga",
|
||||||
"Writing": "Scrittura",
|
"Writing": "Scrittura",
|
||||||
|
"Writing down…": "Trascrizione…",
|
||||||
"Writing: {{rate}} tok/s": "Scrittura: {{rate}} tok/s",
|
"Writing: {{rate}} tok/s": "Scrittura: {{rate}} tok/s",
|
||||||
"Writing…": "Sta scrivendo…",
|
"Writing…": "Sta scrivendo…",
|
||||||
"Written to %(path)s": "Scritto in %(path)s",
|
"Written to %(path)s": "Scritto in %(path)s",
|
||||||
@@ -2816,8 +2906,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Usunąć ten czat?",
|
"Delete this chat?": "Usunąć ten czat?",
|
||||||
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
|
"Delete this scheduled task?": "Usunąć to zaplanowane zadanie?",
|
||||||
"Deny": "Odrzuć",
|
"Deny": "Odrzuć",
|
||||||
|
"Dictate a message": "Podyktuj wiadomość",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
|
"Disconnected. Close and reopen to reconnect.": "Rozłączono. Aby połączyć ponownie, zamknij i otwórz ponownie.",
|
||||||
"Done": "Gotowe",
|
"Done": "Gotowe",
|
||||||
|
"Draw": "Narysuj",
|
||||||
|
"Draw a picture": "Narysuj obraz",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Rysowanie… obraz może zająć minutę lub dwie.",
|
||||||
"Drop the files to attach them": "Upuść pliki, aby je dołączyć",
|
"Drop the files to attach them": "Upuść pliki, aby je dołączyć",
|
||||||
"Due %(when)s": "Zaplanowane na %(when)s",
|
"Due %(when)s": "Zaplanowane na %(when)s",
|
||||||
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
|
"Each run sees the ones before it.": "Każde uruchomienie widzi poprzednie.",
|
||||||
@@ -2948,6 +3042,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "W kolejce",
|
"Queued": "W kolejce",
|
||||||
"Queued: {{ahead}} ahead": "W kolejce: przed nim {{ahead}}",
|
"Queued: {{ahead}} ahead": "W kolejce: przed nim {{ahead}}",
|
||||||
"Read again": "Wczytaj ponownie",
|
"Read again": "Wczytaj ponownie",
|
||||||
|
"Read this aloud": "Przeczytaj na głos",
|
||||||
"Reading the prompt": "Czytanie promptu",
|
"Reading the prompt": "Czytanie promptu",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Czytanie promptu: {{done}} / {{total}} tokenów ({{percent}}%)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Czytanie promptu: {{done}} / {{total}} tokenów ({{percent}}%)",
|
||||||
"Reasoning effort": "Poziom rozumowania",
|
"Reasoning effort": "Poziom rozumowania",
|
||||||
@@ -2990,6 +3085,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Serwer",
|
"Server": "Serwer",
|
||||||
"Set the time yourself": "Ustaw czas samodzielnie",
|
"Set the time yourself": "Ustaw czas samodzielnie",
|
||||||
"Settings": "Ustawienia",
|
"Settings": "Ustawienia",
|
||||||
|
"Shape": "Kształt",
|
||||||
"Show %(n)s more": "Pokaż jeszcze %(n)s",
|
"Show %(n)s more": "Pokaż jeszcze %(n)s",
|
||||||
"Shown by the agent": "Pokazane przez agenta",
|
"Shown by the agent": "Pokazane przez agenta",
|
||||||
"Sidebar": "Pasek boczny",
|
"Sidebar": "Pasek boczny",
|
||||||
@@ -3003,11 +3099,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Pominięty",
|
"Skipped": "Pominięty",
|
||||||
"Something to remember…": "Coś do zapamiętania…",
|
"Something to remember…": "Coś do zapamiętania…",
|
||||||
"Source": "Źródło",
|
"Source": "Źródło",
|
||||||
|
"Square": "Kwadrat",
|
||||||
"Start the agent": "Uruchom agenta",
|
"Start the agent": "Uruchom agenta",
|
||||||
"Stop": "Zatrzymać",
|
"Stop": "Zatrzymać",
|
||||||
"Stop %(name)s": "Zatrzymaj %(name)s",
|
"Stop %(name)s": "Zatrzymaj %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Zatrzymać %(name)s i członków pod nim? Opuszczą zespół.",
|
"Stop %(name)s and the members under it? They leave the team.": "Zatrzymać %(name)s i członków pod nim? Opuszczą zespół.",
|
||||||
"Stop %(name)s and the subagents under it?": "Zatrzymać %(name)s i podagentów pod nim?",
|
"Stop %(name)s and the subagents under it?": "Zatrzymać %(name)s i podagentów pod nim?",
|
||||||
|
"Stop dictating": "Zatrzymaj dyktowanie",
|
||||||
"Stop the agent": "Zatrzymaj agenta",
|
"Stop the agent": "Zatrzymaj agenta",
|
||||||
"Stopped": "Zatrzymano",
|
"Stopped": "Zatrzymano",
|
||||||
"Stopped.": "Zatrzymano.",
|
"Stopped.": "Zatrzymano.",
|
||||||
@@ -3019,6 +3117,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Wyłączone tylko tutaj. To, co jest początkowo włączone, to ustawienie domyślne dla tego modelu w Ustawienia → Modele.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Wyłączone tylko tutaj. To, co jest początkowo włączone, to ustawienie domyślne dla tego modelu w Ustawienia → Modele.",
|
||||||
"System prompt": "Prompt systemowy",
|
"System prompt": "Prompt systemowy",
|
||||||
"Taken in while it ran:": "Przejęte w trakcie działania:",
|
"Taken in while it ran:": "Przejęte w trakcie działania:",
|
||||||
|
"Tall": "Wysoki",
|
||||||
"Tasks: %(n)s": "Zadania: %(n)s",
|
"Tasks: %(n)s": "Zadania: %(n)s",
|
||||||
"Team member": "Członek zespołu",
|
"Team member": "Członek zespołu",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów",
|
"Team: %(size)s of at most %(max)s subagents": "Zespół: %(size)s z najwyżej %(max)s podagentów",
|
||||||
@@ -3046,6 +3145,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "Instrukcja, którą każde uruchomienie dostaje samodzielnie: podaj wszystko, czego potrzebuje.",
|
"The instruction each run is given, on its own: say everything it needs.": "Instrukcja, którą każde uruchomienie dostaje samodzielnie: podaj wszystko, czego potrzebuje.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Tylko interfejs. Modele odpowiadają w języku, w którym się do nich pisze, bez względu na to ustawienie.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Tylko interfejs. Modele odpowiadają w języku, w którym się do nich pisze, bez względu na to ustawienie.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Ostatnie uruchomienia nie powiodły się (%(n)s): %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Nie udało się otworzyć mikrofonu: %(why)s",
|
||||||
"The model's own": "Domyślne modelu",
|
"The model's own": "Domyślne modelu",
|
||||||
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
|
"The most an agent chat may do here": "Najwięcej, co czat agenta może tu zrobić",
|
||||||
"The next command": "Następne polecenie",
|
"The next command": "Następne polecenie",
|
||||||
@@ -3057,6 +3157,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "Odpowiedź jest gotowa.",
|
"The reply is ready.": "Odpowiedź jest gotowa.",
|
||||||
"The result is no longer there.": "Wyniku już nie ma.",
|
"The result is no longer there.": "Wyniku już nie ma.",
|
||||||
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
|
"The server refused this computer's link: %(why)s": "Serwer odrzucił połączenie tego komputera: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "W odpowiedzi serwera nie było obrazu.",
|
||||||
|
"The server's answer was not audio.": "Odpowiedź serwera nie była dźwiękiem.",
|
||||||
"The terminal could not be loaded.": "Nie udało się wczytać terminala.",
|
"The terminal could not be loaded.": "Nie udało się wczytać terminala.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminal wymaga nowszego serwera LLeMbas. Poproś jego administratora o aktualizację.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminal wymaga nowszego serwera LLeMbas. Poproś jego administratora o aktualizację.",
|
||||||
"Theme": "Motyw",
|
"Theme": "Motyw",
|
||||||
@@ -3099,10 +3201,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Czekam na {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Czekam na {{model}}: {{reason}}",
|
||||||
"What it does": "Co robi",
|
"What it does": "Co robi",
|
||||||
"What should happen, and when": "Co ma się stać i kiedy",
|
"What should happen, and when": "Co ma się stać i kiedy",
|
||||||
|
"What to draw…": "Co narysować…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Co Twoje modele pamiętają o Tobie we wszystkich czatach. Tutaj możesz wszystko zmienić lub usunąć.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Co Twoje modele pamiętają o Tobie we wszystkich czatach. Tutaj możesz wszystko zmienić lub usunąć.",
|
||||||
"When": "Kiedy",
|
"When": "Kiedy",
|
||||||
"When it matters": "Gdy to ważne",
|
"When it matters": "Gdy to ważne",
|
||||||
|
"Wide": "Szeroki",
|
||||||
"Writing": "Pisanie",
|
"Writing": "Pisanie",
|
||||||
|
"Writing down…": "Zapisywanie…",
|
||||||
"Writing: {{rate}} tok/s": "Pisanie: {{rate}} tok/s",
|
"Writing: {{rate}} tok/s": "Pisanie: {{rate}} tok/s",
|
||||||
"Writing…": "Pisze…",
|
"Writing…": "Pisze…",
|
||||||
"Written to %(path)s": "Zapisano do %(path)s",
|
"Written to %(path)s": "Zapisano do %(path)s",
|
||||||
@@ -3266,8 +3371,12 @@ const LEMBAS_I18N = {
|
|||||||
"Delete this chat?": "Zmazať túto konverzáciu?",
|
"Delete this chat?": "Zmazať túto konverzáciu?",
|
||||||
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
|
"Delete this scheduled task?": "Odstrániť túto naplánovanú úlohu?",
|
||||||
"Deny": "Odmietnuť",
|
"Deny": "Odmietnuť",
|
||||||
|
"Dictate a message": "Nadiktovať správu",
|
||||||
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
|
"Disconnected. Close and reopen to reconnect.": "Odpojené. Zavrite a znova otvorte, aby sa pripojilo.",
|
||||||
"Done": "Hotovo",
|
"Done": "Hotovo",
|
||||||
|
"Draw": "Nakresliť",
|
||||||
|
"Draw a picture": "Nakresliť obrázok",
|
||||||
|
"Drawing… a picture can take a minute or two.": "Kreslím… obrázok môže trvať minútu alebo dve.",
|
||||||
"Drop the files to attach them": "Pustite súbory a priložia sa",
|
"Drop the files to attach them": "Pustite súbory a priložia sa",
|
||||||
"Due %(when)s": "Naplánované na %(when)s",
|
"Due %(when)s": "Naplánované na %(when)s",
|
||||||
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
|
"Each run sees the ones before it.": "Každé spustenie vidí tie predchádzajúce.",
|
||||||
@@ -3398,6 +3507,7 @@ const LEMBAS_I18N = {
|
|||||||
"Queued": "Vo fronte",
|
"Queued": "Vo fronte",
|
||||||
"Queued: {{ahead}} ahead": "Vo fronte: pred ním {{ahead}}",
|
"Queued: {{ahead}} ahead": "Vo fronte: pred ním {{ahead}}",
|
||||||
"Read again": "Načítať znova",
|
"Read again": "Načítať znova",
|
||||||
|
"Read this aloud": "Prečítať nahlas",
|
||||||
"Reading the prompt": "Číta sa výzva",
|
"Reading the prompt": "Číta sa výzva",
|
||||||
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Číta sa výzva: {{done}} / {{total}} tokenov ({{percent}} %)",
|
"Reading the prompt: {{done}} / {{total}} tokens ({{percent}}%)": "Číta sa výzva: {{done}} / {{total}} tokenov ({{percent}} %)",
|
||||||
"Reasoning effort": "Náročnosť uvažovania",
|
"Reasoning effort": "Náročnosť uvažovania",
|
||||||
@@ -3440,6 +3550,7 @@ const LEMBAS_I18N = {
|
|||||||
"Server": "Server",
|
"Server": "Server",
|
||||||
"Set the time yourself": "Nastaviť čas sami",
|
"Set the time yourself": "Nastaviť čas sami",
|
||||||
"Settings": "Nastavenia",
|
"Settings": "Nastavenia",
|
||||||
|
"Shape": "Tvar",
|
||||||
"Show %(n)s more": "Zobraziť ďalšie (%(n)s)",
|
"Show %(n)s more": "Zobraziť ďalšie (%(n)s)",
|
||||||
"Shown by the agent": "Ukázal agent",
|
"Shown by the agent": "Ukázal agent",
|
||||||
"Sidebar": "Bočný panel",
|
"Sidebar": "Bočný panel",
|
||||||
@@ -3453,11 +3564,13 @@ const LEMBAS_I18N = {
|
|||||||
"Skipped": "Vynechané",
|
"Skipped": "Vynechané",
|
||||||
"Something to remember…": "Niečo na zapamätanie…",
|
"Something to remember…": "Niečo na zapamätanie…",
|
||||||
"Source": "Zdroj",
|
"Source": "Zdroj",
|
||||||
|
"Square": "Štvorec",
|
||||||
"Start the agent": "Spustiť agenta",
|
"Start the agent": "Spustiť agenta",
|
||||||
"Stop": "Zastaviť",
|
"Stop": "Zastaviť",
|
||||||
"Stop %(name)s": "Zastaviť %(name)s",
|
"Stop %(name)s": "Zastaviť %(name)s",
|
||||||
"Stop %(name)s and the members under it? They leave the team.": "Zastaviť %(name)s a členov pod ním? Opustia tím.",
|
"Stop %(name)s and the members under it? They leave the team.": "Zastaviť %(name)s a členov pod ním? Opustia tím.",
|
||||||
"Stop %(name)s and the subagents under it?": "Zastaviť %(name)s a podagentov pod ním?",
|
"Stop %(name)s and the subagents under it?": "Zastaviť %(name)s a podagentov pod ním?",
|
||||||
|
"Stop dictating": "Zastaviť diktovanie",
|
||||||
"Stop the agent": "Zastaviť agenta",
|
"Stop the agent": "Zastaviť agenta",
|
||||||
"Stopped": "Zastavené",
|
"Stopped": "Zastavené",
|
||||||
"Stopped.": "Zastavené.",
|
"Stopped.": "Zastavené.",
|
||||||
@@ -3469,6 +3582,7 @@ const LEMBAS_I18N = {
|
|||||||
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuté len tu. Čo je na začiatku zapnuté, je vaša predvoľba pre tento model v Nastavenia → Modely.",
|
"Switched off here only. What is on to begin with is your default for this model, in Settings → Models.": "Vypnuté len tu. Čo je na začiatku zapnuté, je vaša predvoľba pre tento model v Nastavenia → Modely.",
|
||||||
"System prompt": "Systémový pokyn",
|
"System prompt": "Systémový pokyn",
|
||||||
"Taken in while it ran:": "Prevzaté počas behu:",
|
"Taken in while it ran:": "Prevzaté počas behu:",
|
||||||
|
"Tall": "Vysoký",
|
||||||
"Tasks: %(n)s": "Úlohy: %(n)s",
|
"Tasks: %(n)s": "Úlohy: %(n)s",
|
||||||
"Team member": "Člen tímu",
|
"Team member": "Člen tímu",
|
||||||
"Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov",
|
"Team: %(size)s of at most %(max)s subagents": "Tím: %(size)s z najviac %(max)s podagentov",
|
||||||
@@ -3496,6 +3610,7 @@ const LEMBAS_I18N = {
|
|||||||
"The instruction each run is given, on its own: say everything it needs.": "Inštrukcia, ktorú dostane každé spustenie samostatne: uveďte všetko, čo potrebuje.",
|
"The instruction each run is given, on its own: say everything it needs.": "Inštrukcia, ktorú dostane každé spustenie samostatne: uveďte všetko, čo potrebuje.",
|
||||||
"The interface only. Models answer in the language you write to them in, whatever this says.": "Len rozhranie. Modely odpovedajú v jazyku, v ktorom im píšete, bez ohľadu na toto nastavenie.",
|
"The interface only. Models answer in the language you write to them in, whatever this says.": "Len rozhranie. Modely odpovedajú v jazyku, v ktorom im píšete, bez ohľadu na toto nastavenie.",
|
||||||
"The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s",
|
"The last %(n)s runs failed: %(why)s": "Posledné spustenia zlyhali (%(n)s): %(why)s",
|
||||||
|
"The microphone could not be opened: %(why)s": "Mikrofón sa nepodarilo otvoriť: %(why)s",
|
||||||
"The model's own": "Podľa modelu",
|
"The model's own": "Podľa modelu",
|
||||||
"The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie",
|
"The most an agent chat may do here": "Najviac, čo tu agentská konverzácia smie",
|
||||||
"The next command": "Nasledujúci príkaz",
|
"The next command": "Nasledujúci príkaz",
|
||||||
@@ -3507,6 +3622,8 @@ const LEMBAS_I18N = {
|
|||||||
"The reply is ready.": "Odpoveď je hotová.",
|
"The reply is ready.": "Odpoveď je hotová.",
|
||||||
"The result is no longer there.": "Výsledok už tu nie je.",
|
"The result is no longer there.": "Výsledok už tu nie je.",
|
||||||
"The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s",
|
"The server refused this computer's link: %(why)s": "Server odmietol prepojenie tohto počítača: %(why)s",
|
||||||
|
"The server's answer had no picture in it.": "V odpovedi servera nebol žiadny obrázok.",
|
||||||
|
"The server's answer was not audio.": "Odpoveď servera nebola zvuk.",
|
||||||
"The terminal could not be loaded.": "Terminál sa nepodarilo načítať.",
|
"The terminal could not be loaded.": "Terminál sa nepodarilo načítať.",
|
||||||
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potrebuje novší server LLeMbas. Požiadajte jeho správcu o aktualizáciu.",
|
"The terminal needs a newer LLeMbas server. Ask its administrator to update it.": "Terminál potrebuje novší server LLeMbas. Požiadajte jeho správcu o aktualizáciu.",
|
||||||
"Theme": "Vzhľad",
|
"Theme": "Vzhľad",
|
||||||
@@ -3549,10 +3666,13 @@ const LEMBAS_I18N = {
|
|||||||
"Waiting for {{model}}: {{reason}}": "Čaká sa na {{model}}: {{reason}}",
|
"Waiting for {{model}}: {{reason}}": "Čaká sa na {{model}}: {{reason}}",
|
||||||
"What it does": "Čo robí",
|
"What it does": "Čo robí",
|
||||||
"What should happen, and when": "Čo sa má stať a kedy",
|
"What should happen, and when": "Čo sa má stať a kedy",
|
||||||
|
"What to draw…": "Čo nakresliť…",
|
||||||
"What your models remember about you, across chats. Change or remove anything here.": "Čo si o vás vaše modely pamätajú naprieč konverzáciami. Čokoľvek tu môžete zmeniť alebo odstrániť.",
|
"What your models remember about you, across chats. Change or remove anything here.": "Čo si o vás vaše modely pamätajú naprieč konverzáciami. Čokoľvek tu môžete zmeniť alebo odstrániť.",
|
||||||
"When": "Kedy",
|
"When": "Kedy",
|
||||||
"When it matters": "Keď na tom záleží",
|
"When it matters": "Keď na tom záleží",
|
||||||
|
"Wide": "Široký",
|
||||||
"Writing": "Píše",
|
"Writing": "Píše",
|
||||||
|
"Writing down…": "Zapisujem…",
|
||||||
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
|
"Writing: {{rate}} tok/s": "Píše: {{rate}} tok/s",
|
||||||
"Writing…": "Píše sa…",
|
"Writing…": "Píše sa…",
|
||||||
"Written to %(path)s": "Zapísané do %(path)s",
|
"Written to %(path)s": "Zapísané do %(path)s",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
<div class="composer__bar">
|
<div class="composer__bar">
|
||||||
<button type="button" class="tool composer__attach" id="attach" aria-label="Attach files" title="Attach files" data-i18n-aria-label data-i18n-title></button>
|
<button type="button" class="tool composer__attach" id="attach" aria-label="Attach files" title="Attach files" data-i18n-aria-label data-i18n-title></button>
|
||||||
<div class="composer__chips" id="composer-chips"></div>
|
<div class="composer__chips" id="composer-chips"></div>
|
||||||
|
<button type="button" class="tool composer__mic" id="dictate" hidden aria-pressed="false"></button>
|
||||||
<button type="button" class="round round--stop" id="stop" hidden aria-label="Stop" title="Stop" data-i18n-aria-label data-i18n-title></button>
|
<button type="button" class="round round--stop" id="stop" hidden aria-label="Stop" title="Stop" data-i18n-aria-label data-i18n-title></button>
|
||||||
<button type="submit" class="round" id="send" aria-label="Send" title="Send" data-i18n-aria-label data-i18n-title></button>
|
<button type="submit" class="round" id="send" aria-label="Send" title="Send" data-i18n-aria-label data-i18n-title></button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { unarchive } from "./archived.js"
|
|||||||
import { isProgress, progressLine, progressTicks } from "./progress.js"
|
import { isProgress, progressLine, progressTicks } from "./progress.js"
|
||||||
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
|
import { aimTerminal, terminalButton, terminalOpen, toggleTerminal } from "./terminal.js"
|
||||||
import { scopePopover } from "./scope.js"
|
import { scopePopover } from "./scope.js"
|
||||||
|
import { readAloudButton } from "./voice.js"
|
||||||
|
|
||||||
const MODES = ["manual", "edit", "auto", "plan"]
|
const MODES = ["manual", "edit", "auto", "plan"]
|
||||||
|
|
||||||
@@ -292,6 +293,7 @@ function assistantMessage(m, chat) {
|
|||||||
"div",
|
"div",
|
||||||
{ class: "msg__actions" },
|
{ class: "msg__actions" },
|
||||||
action(ICONS.copy, t("Copy"), () => copyText(replyText(m))),
|
action(ICONS.copy, t("Copy"), () => copyText(replyText(m))),
|
||||||
|
readAloudButton(m.id, () => replyText(m)),
|
||||||
// Only the last reply: CSS shows it there and nowhere else (app.css, .msg__regen).
|
// Only the last reply: CSS shows it there and nowhere else (app.css, .msg__regen).
|
||||||
action(ICONS.refresh, t("Regenerate reply"), () => regenerate(m), "msg__regen"),
|
action(ICONS.refresh, t("Regenerate reply"), () => regenerate(m), "msg__regen"),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -101,6 +101,9 @@ export const ICONS = {
|
|||||||
bell: ["M8 1.5a4.5 4.5 0 014.5 4.5v3l1.5 2.5H2L3.5 9V6A4.5 4.5 0 018 1.5zM8 3a3 3 0 00-3 3v3.42l-.35.58h6.7L11 9.42V6a3 3 0 00-3-3z", "M6.25 12.5h3.5a1.75 1.75 0 01-3.5 0z"],
|
bell: ["M8 1.5a4.5 4.5 0 014.5 4.5v3l1.5 2.5H2L3.5 9V6A4.5 4.5 0 018 1.5zM8 3a3 3 0 00-3 3v3.42l-.35.58h6.7L11 9.42V6a3 3 0 00-3-3z", "M6.25 12.5h3.5a1.75 1.75 0 01-3.5 0z"],
|
||||||
archive: ["M1.5 2.5h13v3.5h-13zM3 4v.5h10V4z", "M2.5 6.5H4V12h8V6.5h1.5v7h-11z", "M6 7.5h4V9H6z"],
|
archive: ["M1.5 2.5h13v3.5h-13zM3 4v.5h10V4z", "M2.5 6.5H4V12h8V6.5h1.5v7h-11z", "M6 7.5h4V9H6z"],
|
||||||
contrast: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M8 3a5 5 0 010 10z"],
|
contrast: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M8 3a5 5 0 010 10z"],
|
||||||
|
mic: ["M8 1.5a2.5 2.5 0 012.5 2.5v4a2.5 2.5 0 01-5 0V4A2.5 2.5 0 018 1.5zM8 3a1 1 0 00-1 1v4a1 1 0 002 0V4a1 1 0 00-1-1z", "M3.5 7.5H5a3 3 0 006 0h1.5a4.5 4.5 0 01-3.75 4.44V14h-1.5v-2.06A4.5 4.5 0 013.5 7.5z"],
|
||||||
|
speaker: ["M2 6h2.5L8 3v10L4.5 10H2z", "M10.2 5.2a4 4 0 010 5.6l-1.06-1.06a2.5 2.5 0 000-3.48z", "M12.3 3.1a7 7 0 010 9.8l-1.06-1.06a5.5 5.5 0 000-7.68z"],
|
||||||
|
picture: ["M1.5 2.5h13v11h-13zM3 4v6.4l2.75-2.75 2.5 2.5 1.75-1.75L13 11.4V4z", "M10.5 5a1.25 1.25 0 110 2.5 1.25 1.25 0 010-2.5z"],
|
||||||
info: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M7.25 7h1.5v4.5h-1.5zM7.25 4.5h1.5V6h-1.5z"],
|
info: ["M8 1.5a6.5 6.5 0 110 13 6.5 6.5 0 010-13zM8 3a5 5 0 100 10A5 5 0 008 3z", "M7.25 7h1.5v4.5h-1.5zM7.25 4.5h1.5V6h-1.5z"],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
// Draw a picture: the instance's own image generation (`POST /v1/images/generations`, its
|
||||||
|
// workflow and default checkpoint), from Ctrl+K. Offered only where `GET /me` says the account may
|
||||||
|
// (`services.images`) and the token holds `models`.
|
||||||
|
//
|
||||||
|
// The pictures are shown only as Rust read them: each one of the person's own files on this
|
||||||
|
// server (`/v1/files/<id>/content`, fetched by id from the app's own server, never from an
|
||||||
|
// address the answer named), handed over as a `data:` URL of an image type -- checked again here
|
||||||
|
// before it becomes an <img>.
|
||||||
|
|
||||||
|
import { h, icon, ICONS } from "./dom.js"
|
||||||
|
import { invoke, why } from "./bridge.js"
|
||||||
|
|
||||||
|
const PICTURE = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/=]+$/
|
||||||
|
|
||||||
|
/** Whether pictures can be drawn here, for the account `me` (`GET /me`). */
|
||||||
|
export function canDraw(me) {
|
||||||
|
const models = !Array.isArray(me?.scopes) || me.scopes.includes("models")
|
||||||
|
return !!me?.services?.images && models && me?.permissions?.["tools.image"] !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The dialog: a prompt, a shape, Draw, and the pictures under it. */
|
||||||
|
export function openDraw() {
|
||||||
|
const prompt = h("textarea", { class: "textarea draw__prompt", rows: "3", placeholder: t("What to draw…"), "aria-label": t("What to draw…") })
|
||||||
|
const size = h(
|
||||||
|
"select",
|
||||||
|
{ class: "draw__size", "aria-label": t("Shape") },
|
||||||
|
h("option", { value: "1024x1024", text: t("Square") }),
|
||||||
|
h("option", { value: "1344x768", text: t("Wide") }),
|
||||||
|
h("option", { value: "768x1344", text: t("Tall") }),
|
||||||
|
)
|
||||||
|
const status = h("p", { class: "status", role: "status", hidden: true })
|
||||||
|
const out = h("div", { class: "draw__out" })
|
||||||
|
const go = h("button", { type: "submit", class: "btn btn--primary", text: t("Draw") })
|
||||||
|
const form = h(
|
||||||
|
"form",
|
||||||
|
{ class: "dialog__body draw", method: "dialog" },
|
||||||
|
h("h2", { class: "dialog__title" }, icon(ICONS.picture), h("span", { text: t("Draw a picture") })),
|
||||||
|
prompt,
|
||||||
|
h("div", { class: "draw__bar" }, size, h("button", { type: "button", class: "btn", text: t("Close"), onclick: () => close() }), go),
|
||||||
|
status,
|
||||||
|
out,
|
||||||
|
)
|
||||||
|
const dlg = h("dialog", { class: "dialog dialog--wide" }, form)
|
||||||
|
let asked = 0
|
||||||
|
const close = () => {
|
||||||
|
asked++
|
||||||
|
dlg.close()
|
||||||
|
dlg.remove()
|
||||||
|
}
|
||||||
|
const said = (text, error = false) => {
|
||||||
|
status.hidden = !text
|
||||||
|
status.textContent = text
|
||||||
|
status.className = `status${error ? " is-error" : ""}`
|
||||||
|
status.setAttribute("role", error ? "alert" : "status")
|
||||||
|
}
|
||||||
|
form.addEventListener("submit", async (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const words = prompt.value.trim()
|
||||||
|
if (!words) return prompt.focus()
|
||||||
|
const mine = ++asked
|
||||||
|
go.disabled = true
|
||||||
|
said(t("Drawing… a picture can take a minute or two."))
|
||||||
|
try {
|
||||||
|
const urls = await invoke("draw", { prompt: words, size: size.value })
|
||||||
|
if (mine !== asked) return
|
||||||
|
const pictures = (urls || []).filter((u) => PICTURE.test(u))
|
||||||
|
if (!pictures.length) throw { detail: t("The server's answer had no picture in it.") }
|
||||||
|
out.prepend(...pictures.map((src) => h("figure", { class: "draw__pic" }, h("img", { src, alt: words }), h("figcaption", { text: words }))))
|
||||||
|
said("")
|
||||||
|
} catch (err) {
|
||||||
|
if (mine === asked) said(why(err), true)
|
||||||
|
} finally {
|
||||||
|
if (mine === asked) go.disabled = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
prompt.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) form.requestSubmit()
|
||||||
|
})
|
||||||
|
dlg.addEventListener("cancel", (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
document.body.append(dlg)
|
||||||
|
dlg.showModal()
|
||||||
|
prompt.focus()
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ import { initAgent, link } from "./agent.js"
|
|||||||
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
|
import { initScheduled, showScheduled, scheduledCommands } from "./scheduled.js"
|
||||||
import { initArchived, showArchived } from "./archived.js"
|
import { initArchived, showArchived } from "./archived.js"
|
||||||
import { showMemory } from "./memory.js"
|
import { showMemory } from "./memory.js"
|
||||||
|
import { initVoice, voiceFor } from "./voice.js"
|
||||||
|
import { canDraw, openDraw } from "./draw.js"
|
||||||
import { closePopover } from "./popup.js"
|
import { closePopover } from "./popup.js"
|
||||||
import { noteSubagentsCap } from "./models.js"
|
import { noteSubagentsCap } from "./models.js"
|
||||||
|
|
||||||
@@ -94,6 +96,7 @@ async function startApp() {
|
|||||||
try {
|
try {
|
||||||
me = await get("/me")
|
me = await get("/me")
|
||||||
noteMe(me)
|
noteMe(me)
|
||||||
|
voiceFor(me)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
$("thread-note").textContent = why(err)
|
$("thread-note").textContent = why(err)
|
||||||
$("thread-note").hidden = false
|
$("thread-note").hidden = false
|
||||||
@@ -233,6 +236,7 @@ function commands() {
|
|||||||
{ label: t("Memory"), run: () => showMemoryPage() },
|
{ label: t("Memory"), run: () => showMemoryPage() },
|
||||||
{ label: t("Settings"), run: () => showSettings(true) },
|
{ label: t("Settings"), run: () => showSettings(true) },
|
||||||
]
|
]
|
||||||
|
if (canDraw(me)) out.push({ label: t("Draw a picture"), run: openDraw })
|
||||||
if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel })
|
if (openChatId()) out.push({ label: panelOpen() ? t("Close the activity panel") : t("Open the activity panel"), run: togglePanel })
|
||||||
if (terminalAvailable()) out.push({ label: terminalOpen() ? t("Close terminal") : t("Terminal"), run: toggleTerminal })
|
if (terminalAvailable()) out.push({ label: terminalOpen() ? t("Close terminal") : t("Terminal"), run: toggleTerminal })
|
||||||
if (status?.linked) out.push({ label: status.agent_wanted ? t("Stop the agent") : t("Start the agent"), run: () => invoke("set_agent", { on: !status.agent_wanted }) })
|
if (status?.linked) out.push({ label: status.agent_wanted ? t("Stop the agent") : t("Start the agent"), run: () => invoke("set_agent", { on: !status.agent_wanted }) })
|
||||||
@@ -280,6 +284,7 @@ async function boot() {
|
|||||||
$("thread-note").hidden = !text
|
$("thread-note").hidden = !text
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
initVoice()
|
||||||
initChat({
|
initChat({
|
||||||
status: () => status,
|
status: () => status,
|
||||||
thisDevices,
|
thisDevices,
|
||||||
|
|||||||
+180
@@ -0,0 +1,180 @@
|
|||||||
|
// Voice: a message dictated in the composer, and a reply read aloud. Both are the instance's own
|
||||||
|
// speech services on the OpenAI-compatible routes, which the app's token reaches through its
|
||||||
|
// `models` scope (src-tauri/src/server.rs):
|
||||||
|
//
|
||||||
|
// POST /v1/audio/transcriptions the recording -> { text } (Rust: `transcribe`)
|
||||||
|
// POST /v1/audio/speech { input } -> the audio (Rust: `speak`, a data: URL)
|
||||||
|
//
|
||||||
|
// Each is offered only where `GET /me` says the instance has it for this account (`services.stt`,
|
||||||
|
// `services.tts`) and the token holds `models` (`scopes`), so no button is there that can only fail.
|
||||||
|
// The microphone is asked for only when Dictate is pressed: Rust allows it for that moment alone.
|
||||||
|
|
||||||
|
import { $, h, icon, ICONS } from "./dom.js"
|
||||||
|
import { invoke, why } from "./bridge.js"
|
||||||
|
|
||||||
|
let me = null
|
||||||
|
|
||||||
|
const hasModels = () => !Array.isArray(me?.scopes) || me.scopes.includes("models")
|
||||||
|
/** Whether a message can be dictated here. */
|
||||||
|
export const canDictate = () => !!me?.services?.stt && hasModels() && me?.permissions?.["audio.transcribe"] !== false
|
||||||
|
/** Whether a reply can be read aloud here. */
|
||||||
|
export const canSpeak = () => !!me?.services?.tts && hasModels() && me?.permissions?.["audio.listen"] !== false
|
||||||
|
|
||||||
|
function say(text) {
|
||||||
|
const box = $("thread-note")
|
||||||
|
if (!box) return
|
||||||
|
box.textContent = text
|
||||||
|
box.hidden = !text
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Dictate --------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let recorder = null // the recording in progress
|
||||||
|
let busy = false // a recording being turned into words
|
||||||
|
|
||||||
|
function drawMic() {
|
||||||
|
const b = $("dictate")
|
||||||
|
if (!b) return
|
||||||
|
b.hidden = !canDictate()
|
||||||
|
const on = !!recorder
|
||||||
|
b.classList.toggle("is-recording", on)
|
||||||
|
b.classList.toggle("is-busy", busy)
|
||||||
|
b.disabled = busy
|
||||||
|
b.setAttribute("aria-pressed", String(on))
|
||||||
|
const label = busy ? t("Writing down…") : on ? t("Stop dictating") : t("Dictate a message")
|
||||||
|
b.setAttribute("aria-label", label)
|
||||||
|
b.title = label
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
say("")
|
||||||
|
let stream
|
||||||
|
try {
|
||||||
|
await invoke("voice_arm")
|
||||||
|
stream = await navigator.mediaDevices.getUserMedia({ audio: true })
|
||||||
|
} catch (err) {
|
||||||
|
say(t("The microphone could not be opened: %(why)s", { why: err?.message || why(err) }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const type = ["audio/webm;codecs=opus", "audio/ogg;codecs=opus", "audio/webm"].find((x) => window.MediaRecorder?.isTypeSupported?.(x))
|
||||||
|
let rec
|
||||||
|
try {
|
||||||
|
rec = new MediaRecorder(stream, type ? { mimeType: type } : undefined)
|
||||||
|
} catch (err) {
|
||||||
|
stream.getTracks().forEach((x) => x.stop())
|
||||||
|
say(t("The microphone could not be opened: %(why)s", { why: err?.message || why(err) }))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const chunks = []
|
||||||
|
rec.addEventListener("dataavailable", (e) => e.data.size && chunks.push(e.data))
|
||||||
|
rec.addEventListener("stop", async () => {
|
||||||
|
stream.getTracks().forEach((x) => x.stop())
|
||||||
|
recorder = null
|
||||||
|
const blob = new Blob(chunks, { type: rec.mimeType || type || "audio/webm" })
|
||||||
|
if (!blob.size) return drawMic()
|
||||||
|
busy = true
|
||||||
|
drawMic()
|
||||||
|
try {
|
||||||
|
const text = await invoke("transcribe", new Uint8Array(await blob.arrayBuffer()), { headers: { "x-type": blob.type } })
|
||||||
|
intoComposer(text)
|
||||||
|
} catch (err) {
|
||||||
|
say(why(err))
|
||||||
|
}
|
||||||
|
busy = false
|
||||||
|
drawMic()
|
||||||
|
})
|
||||||
|
recorder = rec
|
||||||
|
rec.start()
|
||||||
|
drawMic()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The words heard, where the cursor is in the message box, a space either side where needed. */
|
||||||
|
function intoComposer(text) {
|
||||||
|
text = String(text || "").trim()
|
||||||
|
if (!text) return
|
||||||
|
const box = $("composer-text")
|
||||||
|
const at = box.selectionStart ?? box.value.length
|
||||||
|
const end = box.selectionEnd ?? at
|
||||||
|
const before = box.value.slice(0, at)
|
||||||
|
const after = box.value.slice(end)
|
||||||
|
const words = `${before && !/\s$/.test(before) ? " " : ""}${text}${after && !/^\s/.test(after) ? " " : ""}`
|
||||||
|
box.value = before + words + after
|
||||||
|
box.selectionStart = box.selectionEnd = before.length + words.length
|
||||||
|
box.dispatchEvent(new Event("input"))
|
||||||
|
box.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stop a recording (a chat closed, the window hidden): what was said is still written down. */
|
||||||
|
export function stopDictating() {
|
||||||
|
if (recorder?.state === "recording") recorder.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Read aloud -----------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let playing = null // { id, audio, button }
|
||||||
|
|
||||||
|
/** A reply's words without the markdown around them, and without its code. */
|
||||||
|
export function plain(markdown) {
|
||||||
|
return String(markdown || "")
|
||||||
|
.replace(/```[\s\S]*?```/g, " ")
|
||||||
|
.replace(/!?\[([^\]]*)]\([^)]*\)/g, "$1")
|
||||||
|
.replace(/[*_`#>|~]/g, "")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopReading() {
|
||||||
|
if (!playing) return
|
||||||
|
playing.audio?.pause()
|
||||||
|
playing.button.setAttribute("aria-pressed", "false")
|
||||||
|
playing.button.classList.remove("is-busy")
|
||||||
|
playing = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Read aloud button of a reply (`id`), reading `text()` when pressed; null where it cannot. */
|
||||||
|
export function readAloudButton(id, text) {
|
||||||
|
if (!canSpeak()) return null
|
||||||
|
const label = t("Read this aloud")
|
||||||
|
const b = h("button", { type: "button", class: "msg__act msg__speak", "aria-label": label, title: label, "aria-pressed": "false" }, icon(ICONS.speaker))
|
||||||
|
b.addEventListener("click", async () => {
|
||||||
|
if (playing?.id === id) return stopReading()
|
||||||
|
stopReading()
|
||||||
|
const words = plain(text())
|
||||||
|
if (!words) return
|
||||||
|
const mine = { id, audio: null, button: b }
|
||||||
|
playing = mine
|
||||||
|
b.setAttribute("aria-pressed", "true")
|
||||||
|
b.classList.add("is-busy")
|
||||||
|
try {
|
||||||
|
const url = await invoke("speak", { text: words })
|
||||||
|
if (playing !== mine) return
|
||||||
|
// Only audio the app's own server sent, as Rust read it: a data: URL of an audio type.
|
||||||
|
if (!/^data:audio\/[a-z0-9.+-]+;base64,/i.test(url)) throw new Error(t("The server's answer was not audio."))
|
||||||
|
mine.audio = new Audio(url)
|
||||||
|
b.classList.remove("is-busy")
|
||||||
|
mine.audio.addEventListener("ended", () => playing === mine && stopReading())
|
||||||
|
await mine.audio.play()
|
||||||
|
} catch (err) {
|
||||||
|
if (playing === mine) stopReading()
|
||||||
|
say(why(err?.message ? { detail: err.message } : err))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What the account may use, from `GET /me`: the composer's Dictate button is drawn by it. */
|
||||||
|
export function voiceFor(account) {
|
||||||
|
me = account
|
||||||
|
drawMic()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initVoice() {
|
||||||
|
const b = $("dictate")
|
||||||
|
b.replaceChildren(icon(ICONS.mic))
|
||||||
|
b.addEventListener("click", () => {
|
||||||
|
if (busy) return
|
||||||
|
if (recorder) recorder.stop()
|
||||||
|
else start()
|
||||||
|
})
|
||||||
|
drawMic()
|
||||||
|
}
|
||||||
Reference in new issue
Block a user