Compare commits
3 Commits
680f5a656a
...
23b1fe692b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23b1fe692b | ||
|
|
7bb9718c34 | ||
|
|
7da2fcb5e5 |
@@ -65,6 +65,9 @@ OLLAMA_CHAT_MODEL=qwen2.5:3b
|
||||
# default qwen2.5:3b, which ollama-init pulls automatically. Set it equal to
|
||||
# OLLAMA_CHAT_MODEL to run everything on one resident model instead (saves VRAM
|
||||
# at the cost of slower routing when the chat model is large).
|
||||
# NEVER set this LARGER than OLLAMA_CHAT_MODEL: the auxiliary calls would then
|
||||
# run on the bigger, slower model and add latency to every command (the exact
|
||||
# opposite of the split's purpose). Keep it <= the chat model, blank, or equal.
|
||||
OLLAMA_INTENT_MODEL=
|
||||
OLLAMA_EMBED_MODEL=nomic-embed-text
|
||||
WHISPER_MODEL=small
|
||||
@@ -227,6 +230,7 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.gpu-linux.yml
|
||||
# OLLAMA_CHAT_MODEL=qwen2.5:7b # quality (needs ~5GB VRAM + whisper small)
|
||||
# OLLAMA_CHAT_MODEL=qwen2.5:3b # speed (fits easily, faster on 8GB GPUs)
|
||||
# WHISPER_MODEL=small # small frees VRAM for a bigger LLM; medium=more accurate
|
||||
# STT_BEAM_SIZE=5 # beam search (5) > greedy (1) for accuracy; lower for speed
|
||||
# MELO_DEVICE=cuda # cpu if no GPU on the bot host
|
||||
|
||||
# --- Settings web UI (http://localhost:8765/settings on the bot host) ---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 자비스 운영자 지시
|
||||
|
||||
- 너의 이름은 자비스다.
|
||||
- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 최대한 간결하게, 한두 문장으로 답한다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다.
|
||||
- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 무조건 한 문장으로만 답한다. 두 문장 이상 쓰지 않는다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다.
|
||||
- 정해진 문구에만 반응하지 말고, 실제 사람처럼 말의 뉘앙스와 맥락으로 의도를 알아듣고 처리한다.
|
||||
|
||||
화면 속 크롬(방송 화면)에서 유튜브를 다룰 때 (화면에 보여야 하므로 반드시 on-screen 브라우저 제어 도구로 수행한다):
|
||||
|
||||
@@ -87,6 +87,17 @@ VAD_MIN_SPEECH_MS = int(os.environ.get("VAD_MIN_SPEECH_MS", "200"))
|
||||
# Korean phrase decoded as Chinese) and shaves a little latency. Empty = auto.
|
||||
STT_LANGUAGE = os.environ.get("STT_LANGUAGE", "ko").strip() or None
|
||||
|
||||
# Whisper decoding accuracy knobs. beam_size=1 is greedy decoding — fast but the
|
||||
# least accurate; beam search (5 is the Whisper default) explores alternatives
|
||||
# and noticeably improves recognition on short, accented, or noisy Discord-mic
|
||||
# speech. condition_on_previous_text=False stops Whisper from feeding a previous
|
||||
# clip's transcript back in as a prompt, which on isolated short utterances
|
||||
# causes repetition loops and drift rather than helping. Both are env-tunable so
|
||||
# accuracy/latency can be traded without a code change (lower STT_BEAM_SIZE for
|
||||
# speed, raise it for accuracy).
|
||||
STT_BEAM_SIZE = max(1, int(os.environ.get("STT_BEAM_SIZE", "5")))
|
||||
STT_CONDITION_ON_PREV = os.environ.get("STT_CONDITION_ON_PREV", "0") in ("1", "true", "True", "yes", "on")
|
||||
|
||||
# TTS engine: "edge" (Microsoft Edge TTS, natural Korean neural voice) is the
|
||||
# primary voice. "melo" (a warm MeloTTS worker) and "piper" remain selectable.
|
||||
def _tts_engine_setting() -> str:
|
||||
@@ -243,7 +254,12 @@ def transcribe(wav_bytes: bytes) -> dict:
|
||||
print("[bridge] no speech detected (VAD) — skipping STT", flush=True)
|
||||
return {"text": "", "language": None, "note": "음성 아님(VAD 차단)"}
|
||||
|
||||
segments, info = _whisper.transcribe(audio, beam_size=1, language=STT_LANGUAGE)
|
||||
segments, info = _whisper.transcribe(
|
||||
audio,
|
||||
beam_size=STT_BEAM_SIZE,
|
||||
language=STT_LANGUAGE,
|
||||
condition_on_previous_text=STT_CONDITION_ON_PREV,
|
||||
)
|
||||
# Second line of defence: drop non-speech / hallucinated segments by
|
||||
# Whisper's own no_speech_prob. The no_speech_prob hard cutoff (plus the VAD
|
||||
# pre-gate above) is what rejects noise/hallucinations. The avg_logprob
|
||||
|
||||
@@ -97,6 +97,9 @@ services:
|
||||
PLANNER_ENABLED: ${PLANNER_ENABLED:-0}
|
||||
# Lock STT to Korean (skip Whisper auto-detect).
|
||||
STT_LANGUAGE: ${STT_LANGUAGE:-ko}
|
||||
# Whisper decode accuracy: beam search (5) over greedy (1) lifts recognition
|
||||
# on short/noisy Discord speech. Lower to 1 for minimum latency.
|
||||
STT_BEAM_SIZE: ${STT_BEAM_SIZE:-5}
|
||||
VOICE_SILENCE_MS: ${VOICE_SILENCE_MS:-600}
|
||||
BRIDGE_URL: http://127.0.0.1:8765
|
||||
# Split-deployment role: full (default, all-in-one), browser (only the
|
||||
|
||||
@@ -19,7 +19,7 @@ Every distinct LLM call in Jarvis, what feeds it, what consumes it, and how it i
|
||||
- Time + location context (re-injected each turn)
|
||||
- Tool schema: native via `generate_tools_json_schema()` ([src/jarvis/tools/registry.py](src/jarvis/tools/registry.py)) or text fallback via `_text_tool_call_guidance()` ([engine.py:68](src/jarvis/reply/engine.py:68))
|
||||
- Tool results from prior turns (raw or digested — see #5)
|
||||
- **Output**: OpenAI-style `{content, tool_calls, thinking}`. Consumed by the tool orchestrator and TTS pipeline. Natural-language content is delivered immediately; no post-turn evaluator runs.
|
||||
- **Output**: OpenAI-style `{content, tool_calls, thinking}`. Consumed by the tool orchestrator and TTS pipeline. Natural-language content is delivered immediately; no post-turn evaluator runs. Spoken-answer length: the persona (`system_prompt.py`) and `voice_style` (`prompts/system.py`) both constrain the reply to a SINGLE sentence — any dry aside must fold into that one sentence as a trailing clause, never a second sentence. This keeps TTS latency down (synth time scales with text length) and matches the `agents/llm.md` operator instruction.
|
||||
- **Limits**: `num_ctx: 8192` (explicit). Output `num_predict: cfg.ollama_num_predict` (default 512, `0`/negative disables) caps generated tokens per turn — a worst-case latency guard for short spoken answers; the headroom stays above tool-call JSON so it does not truncate tool calls (both native and text tool-call paths). Timeout `llm_chat_timeout_sec` (45s). Auto-fallback from native to text tool-calls on HTTP 400 (`ToolsNotSupportedError`), sticky for the session. Risk: `fetch_web_page` truncates at 50,000 chars (~37k tokens) — mitigated for SMALL models by tool-result digest (#5) which compresses the payload before it enters the messages history. LARGE models receive the raw payload and may silently see a truncated context.
|
||||
|
||||
## 2. Intent Judge
|
||||
|
||||
@@ -43,7 +43,7 @@ from jarvis.reply.prompts import (
|
||||
Both model sizes share these base components:
|
||||
- `asr_note`: Voice transcription error handling
|
||||
- `inference_guidance`: Prefer inference over clarification
|
||||
- `voice_style`: Concise, conversational responses
|
||||
- `voice_style`: Single-sentence, conversational responses (spoken aloud, so one sentence only — never more)
|
||||
|
||||
Model-size-specific components:
|
||||
- `tool_incentives`: When/how aggressively to use tools
|
||||
|
||||
@@ -26,8 +26,8 @@ INFERENCE_GUIDANCE = (
|
||||
# Voice assistant communication style - concise, conversational
|
||||
VOICE_STYLE = (
|
||||
"Keep responses concise and conversational since this is a voice assistant. "
|
||||
"Two to three sentences maximum. Prioritize clarity and brevity - users are listening, not reading. "
|
||||
"Avoid unnecessary elaboration unless specifically requested. "
|
||||
"Reply in a SINGLE sentence - never more than one sentence. Prioritize clarity and brevity - users are listening, not reading. "
|
||||
"Avoid unnecessary elaboration. "
|
||||
"Do NOT offer follow-up suggestions or ask if the user wants more info - just respond directly. "
|
||||
"IMPORTANT: Always respond in natural language - never output JSON, code, or structured data as your response. "
|
||||
"NEVER use markdown formatting in your replies: no asterisks for emphasis (**bold**, *italic*), "
|
||||
|
||||
@@ -62,10 +62,11 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"Tone rails (hard): never mean, never condescending, never passive-aggressive, never "
|
||||
"sulking, never preachy, never sycophantic ('great question', 'I'd be happy to'). "
|
||||
"Sarcasm points at the situation, the topic, or mildly at yourself — never at the user. "
|
||||
"Shape for casual, factual, or small-talk replies: state the answer in a sentence, then add "
|
||||
"one short dry observation about it (an understated aside, a raised-eyebrow remark, a gentle "
|
||||
"noticing of the irony). One aside — not two, not a joke opener, not a joke-shaped sentence "
|
||||
"replacing the answer. The aside is a tail, not the head. "
|
||||
"Shape for casual, factual, or small-talk replies: give the answer in a SINGLE sentence. If a "
|
||||
"dry aside fits, fold it into that same sentence as a short trailing clause — never add it as "
|
||||
"a second sentence, never a joke opener, never a joke-shaped sentence replacing the answer. "
|
||||
"Whenever the wit would require a second sentence, drop the wit and keep the one-sentence "
|
||||
"answer. The aside is a tail inside the sentence, not a head and not a new sentence. "
|
||||
"Examples of the MOVE (shape, not wording — never copy these): stating a fact and then noting "
|
||||
"its mild absurdity; giving the weather and then commenting on what it implies for the day; "
|
||||
"answering a trivia question and then offering a wry footnote about the subject; admitting "
|
||||
@@ -79,8 +80,8 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"butler clichés, and never address the user as 'sir', 'madam', 'my liege', or similar. "
|
||||
"Never stack multiple jokes in one reply. "
|
||||
"Be concise, conversational, and actionable. "
|
||||
"This is a spoken voice assistant: answer in ONE short sentence whenever possible "
|
||||
"(two at the very most). No lists, no preamble, no 'is there anything else' offers. "
|
||||
"This is a spoken voice assistant: your ENTIRE reply must be a single short sentence. "
|
||||
"Never write a second sentence. No lists, no preamble, no 'is there anything else' offers. "
|
||||
"When a controlBrowser tool is available, use IT (never webSearch) for anything that "
|
||||
"should happen in the on-screen browser — opening a site, searching on a site "
|
||||
"(controlBrowser action 'search' with the right site), clicking, typing — because only "
|
||||
|
||||
@@ -121,6 +121,18 @@ class TestPromptComponents:
|
||||
assert prompts.voice_style, f"{size.value} missing voice_style"
|
||||
assert prompts.tool_guidance, f"{size.value} missing tool_guidance"
|
||||
|
||||
def test_voice_style_enforces_single_sentence(self):
|
||||
"""voice_style must cap replies at one sentence (spoken aloud). The old
|
||||
'Two to three sentences maximum' wording let the model run long, which
|
||||
also slowed TTS since synth time scales with text length."""
|
||||
from jarvis.reply.prompts import get_system_prompts, ModelSize
|
||||
|
||||
for size in [ModelSize.SMALL, ModelSize.LARGE]:
|
||||
voice_style = get_system_prompts(size).voice_style
|
||||
assert "SINGLE sentence" in voice_style, f"{size.value} voice_style not single-sentence"
|
||||
assert "never more than one sentence" in voice_style
|
||||
assert "Two to three" not in voice_style
|
||||
|
||||
def test_to_list_returns_non_empty_strings(self):
|
||||
"""to_list() returns only non-empty prompt strings."""
|
||||
from jarvis.reply.prompts import get_system_prompts, ModelSize
|
||||
|
||||
@@ -44,6 +44,22 @@ class TestBuildSystemPrompt:
|
||||
assert "in the user's language" not in prompt
|
||||
assert "in Korean" in prompt
|
||||
|
||||
def test_persona_enforces_single_sentence(self):
|
||||
# Spoken replies must be one sentence (TTS latency scales with text
|
||||
# length, and the user asked for one-sentence answers). The persona must
|
||||
# state the single-sentence rule and must NOT carry the old "two at the
|
||||
# very most" allowance that let the model run long.
|
||||
prompt = build_system_prompt("Jarvis")
|
||||
assert "single short sentence" in prompt
|
||||
assert "Never write a second sentence" in prompt
|
||||
assert "two at the very most" not in prompt
|
||||
|
||||
def test_persona_aside_does_not_authorise_a_second_sentence(self):
|
||||
# The dry aside must fold into the one sentence, not become a 2nd one.
|
||||
prompt = build_system_prompt("Jarvis")
|
||||
assert "SINGLE sentence" in prompt
|
||||
assert "never add it as " in prompt
|
||||
|
||||
|
||||
class TestOutputLanguageDirective:
|
||||
"""A deployment may lock replies to a single language via OUTPUT_LANGUAGE.
|
||||
|
||||
Reference in New Issue
Block a user