arch + features: Lua jarvis.speak() / jarvis.llm() API, /commands GUI page, games / mouse / random_choice packs
Architecture — DRY at the Lua API surface:
lua/api/tts.rs (new): jarvis.speak(text, opts?). opts.lang = ISO
two-letter code (default "ru"); auto-picks first installed voice
matching the culture. opts.async = true|false (default true). The
9 packs that previously inlined a 60-character PS-SAPI string for
every spoken reply can now reduce to one line. Refactored fun/,
dice/, clipboard_read/ as proof — fun/ask.lua went from 75 lines
of HTTP+SAPI boilerplate to ~28 lines of intent code.
lua/api/llm.rs (new): jarvis.llm(messages, opts?). messages is a
list of {role, content} tables, opts.max_tokens / .temperature /
.top_p. Returns the assistant string or nil. Gated by sandbox
allows_http() (so Standard+ packs only). Internally uses the
existing jarvis_core::llm::LlmClient::complete_with, no duplicated
HTTP plumbing.
llm/client.rs: split complete() into complete_with(messages,
max_tokens, temperature, top_p) for the new caller; old complete()
is now a thin wrapper at temp=0.7 top_p=1.0 for backward compat.
GUI — /commands page rewrite:
Replaces the "Раздел в разработке" placeholder. Calls Tauri command
get_commands_list (already existed in tauri_commands/commands.rs,
was wired but unused). Renders a search-filterable card grid:
- cmd id (monospace)
- type badge with colour by kind (lua=blue, ahk=red, cli=grey,
voice=green, terminate=red, stop_chaining=violet)
- sandbox badge
- description line (if non-empty)
- all phrases for currentLanguage, fallback to en, fallback to
first available; each phrase in a small inline pill
Toolbar: text input with magnifier icon + reload button (↻).
Counter line shows visible/total + "Скажи Джарвис + любую фразу"
hint. Counts down as you type the filter.
New packs (3, total 33 → 36):
games/ — launch_game / list_games. Reads or creates
%USERPROFILE%\Documents\jarvis-games.json (the sample preloads dota,
cs2, elden ring, witcher 3, factorio + minecraft launcher path +
fortnite epic URI). Trigger-strip + fuzzy name/alias match, then
launches via steam://rungameid/N (Steam), epic:// (Epic Games), or
start "" "path" (anything else). list_games speaks the configured
library.
mouse/ — left/right/middle/double click + scroll up/down. Single
dispatch.lua + _mouse_helper.ps1. PS P/Invokes user32!mouse_event
with the right flag pair (LEFTDOWN+LEFTUP / etc.), 30ms gap, doubles
do two LEFT cycles with a 50ms gap, wheel uses ±360 ticks.
random_choice/ — "выбери из X или Y или Z". Strips the trigger,
splits on " или " / " or " / ", " / " либо " / " чи ", math.random
picks one, speaks "Я выбираю N".
cargo test commands::tests still 3/3. 36 packs verified via
jarvis-gui startup log.
This commit is contained in:
parent
9d8b917149
commit
a16d2401e7
17 changed files with 765 additions and 83 deletions
|
|
@ -103,13 +103,23 @@ impl LlmClient {
|
|||
}
|
||||
|
||||
pub fn complete(&self, messages: &[ChatMessage], max_tokens: u32) -> Result<String, LlmError> {
|
||||
self.complete_with(messages, max_tokens, 0.7, 1.0)
|
||||
}
|
||||
|
||||
pub fn complete_with(
|
||||
&self,
|
||||
messages: &[ChatMessage],
|
||||
max_tokens: u32,
|
||||
temperature: f32,
|
||||
top_p: f32,
|
||||
) -> Result<String, LlmError> {
|
||||
let url = format!("{}/chat/completions", self.base_url.trim_end_matches('/'));
|
||||
let body = ChatRequest {
|
||||
model: &self.model,
|
||||
messages,
|
||||
max_tokens,
|
||||
temperature: 0.7,
|
||||
top_p: 1.0,
|
||||
temperature,
|
||||
top_p,
|
||||
};
|
||||
|
||||
let resp = self
|
||||
|
|
|
|||
|
|
@ -4,4 +4,6 @@ pub mod context;
|
|||
pub mod http;
|
||||
pub mod fs;
|
||||
pub mod state;
|
||||
pub mod system;
|
||||
pub mod system;
|
||||
pub mod tts;
|
||||
pub mod llm;
|
||||
64
crates/jarvis-core/src/lua/api/llm.rs
Normal file
64
crates/jarvis-core/src/lua/api/llm.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
use mlua::{Lua, Table, Value};
|
||||
|
||||
use crate::llm::{ChatMessage, LlmClient};
|
||||
|
||||
// Lua-callable wrapper around jarvis_core::llm::LlmClient.
|
||||
// Centralises the Groq plumbing so command scripts can just say:
|
||||
// local reply = jarvis.llm(
|
||||
// { {role='system', content='...'}, {role='user', content='...'} },
|
||||
// { temperature = 0.0, max_tokens = 64 }
|
||||
// )
|
||||
// Returns the assistant text (string) or nil on any failure (missing token,
|
||||
// network error, empty response, etc.) — callers should always check `if reply`.
|
||||
pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
|
||||
let chat_fn = lua.create_function(|lua, (messages, opts): (Table, Option<Table>)| {
|
||||
let mut chat_messages = Vec::new();
|
||||
for pair in messages.sequence_values::<Table>() {
|
||||
let msg = pair?;
|
||||
let role = msg.get::<String>("role").unwrap_or_else(|_| "user".to_string());
|
||||
let content = msg.get::<String>("content").unwrap_or_default();
|
||||
if content.is_empty() {
|
||||
continue;
|
||||
}
|
||||
chat_messages.push(match role.as_str() {
|
||||
"system" => ChatMessage::system(content),
|
||||
"assistant" => ChatMessage::assistant(content),
|
||||
_ => ChatMessage::user(content),
|
||||
});
|
||||
}
|
||||
if chat_messages.is_empty() {
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
|
||||
let mut max_tokens: u32 = 256;
|
||||
let mut temperature: f32 = 0.7;
|
||||
let mut top_p: f32 = 1.0;
|
||||
if let Some(o) = opts {
|
||||
if let Ok(v) = o.get::<u32>("max_tokens") { max_tokens = v; }
|
||||
if let Ok(v) = o.get::<f32>("temperature") { temperature = v; }
|
||||
if let Ok(v) = o.get::<f32>("top_p") { top_p = v; }
|
||||
}
|
||||
|
||||
let client = match LlmClient::from_env() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
log::warn!("[Lua llm] no client: {}", e);
|
||||
return Ok(Value::Nil);
|
||||
}
|
||||
};
|
||||
|
||||
match client.complete_with(&chat_messages, max_tokens, temperature, top_p) {
|
||||
Ok(reply) => {
|
||||
let s = lua.create_string(reply.trim())?;
|
||||
Ok(Value::String(s))
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("[Lua llm] request failed: {}", e);
|
||||
Ok(Value::Nil)
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
jarvis.set("llm", chat_fn)?;
|
||||
Ok(())
|
||||
}
|
||||
63
crates/jarvis-core/src/lua/api/tts.rs
Normal file
63
crates/jarvis-core/src/lua/api/tts.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use mlua::{Lua, Table};
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
pub fn register(lua: &Lua, jarvis: &Table) -> mlua::Result<()> {
|
||||
// jarvis.speak(text, opts?)
|
||||
// opts.lang: ISO 2-letter code (ru, en, de, ...). default "ru"
|
||||
// opts.async: if true, fire-and-forget; if false, block until done. default true
|
||||
let speak_fn = lua.create_function(|_, (text, opts): (String, Option<Table>)| {
|
||||
let mut iso = "ru".to_string();
|
||||
let mut detached = true;
|
||||
|
||||
if let Some(t) = opts {
|
||||
if let Ok(v) = t.get::<String>("lang") { iso = v; }
|
||||
if let Ok(v) = t.get::<bool>("async") { detached = v; }
|
||||
}
|
||||
|
||||
speak_via_sapi(&text, &iso, detached);
|
||||
Ok(())
|
||||
})?;
|
||||
jarvis.set("speak", speak_fn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn speak_via_sapi(text: &str, iso: &str, detached: bool) {
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
let escaped = text.replace('\'', "''").replace('\r', " ").replace('\n', " ");
|
||||
let safe_iso = if iso.chars().all(|c| c.is_ascii_alphabetic()) && iso.len() == 2 {
|
||||
iso.to_lowercase()
|
||||
} else {
|
||||
"ru".to_string()
|
||||
};
|
||||
let ps = format!(
|
||||
"Add-Type -AssemblyName System.Speech; \
|
||||
$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; \
|
||||
foreach ($v in $s.GetInstalledVoices()) {{ \
|
||||
if ($v.VoiceInfo.Culture.TwoLetterISOLanguageName -eq '{}') {{ \
|
||||
try {{ $s.SelectVoice($v.VoiceInfo.Name); break }} catch {{}} \
|
||||
}} \
|
||||
}} \
|
||||
$s.Speak('{}')",
|
||||
safe_iso, escaped
|
||||
);
|
||||
|
||||
let mut cmd = Command::new("powershell");
|
||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null());
|
||||
|
||||
if detached {
|
||||
let _ = cmd.spawn();
|
||||
} else {
|
||||
let _ = cmd.status();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn speak_via_sapi(text: &str, _iso: &str, _detached: bool) {
|
||||
log::info!("[Lua tts] would speak: {}", text);
|
||||
}
|
||||
|
|
@ -76,10 +76,12 @@ impl LuaEngine {
|
|||
api::core::register(&self.lua, &jarvis)?;
|
||||
api::audio::register(&self.lua, &jarvis)?;
|
||||
api::context::register(&self.lua, &jarvis, context)?;
|
||||
|
||||
api::tts::register(&self.lua, &jarvis)?;
|
||||
|
||||
// sandbox-controlled APIs
|
||||
if self.sandbox.allows_http() {
|
||||
api::http::register(&self.lua, &jarvis)?;
|
||||
api::llm::register(&self.lua, &jarvis)?;
|
||||
}
|
||||
|
||||
if self.sandbox.allows_state() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue