Skip to content

Releases: Xenoah/stackchan-codex

v1.1.5 - Gaming RGB Edition

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 12:48

Gaming RGB Edition. 設定画面からのローカルLLMチャットUIと、顔・本体LEDの虹色循環演出を追加したリリースです。

Added

  • 設定ページに Local LLM Chat カードを追加。ブラウザの fetch から直接 Android Gateway の http://<TTS Host>:<TTS Port>/ask へ質問をPOSTし、LLM回答(question / answer / stackchan)を画面表示しつつStackChanに発話させる(ESP32側に中継APIは作らず、JSON処理はブラウザ側で実施)。
  • Gaming RGB: 顔(画面)と本体LEDを虹色にゆっくり循環させる演出(約6秒で1周、顔とLEDの色相を同期、colorDepth=1の2トーン描画では背景と前景を補色関係で循環)。
  • Gaming RGB の有効/無効を設定ページのチェックボックスで切り替え(NVS gaming_rgb に保存、初期値ON)。
  • /status ページの App カードに Gaming RGB の On/Off 表示を追加。

Changed

  • ステータスLED表示を showStatusLed() 経由へ変更し、Gaming RGB有効時はイベント色(TTS=青/エラー=赤/通常=緑 等)を一定時間保持してから虹色サイクルへ復帰(状態表示の意味を維持)。

Build

  • m5stack-cores3: RAM 18.2% / Flash 18.4%

🤖 Generated with Claude Code

update chat ui

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 02:53
v1.1.0

update chatUI

StackChan × Android Termux Local LLM Gateway

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 01:52

StackChan × Android Termux Local LLM Gateway

Android Termux 上でローカル LLM と TTS Gateway を起動し、StackChan に喋らせるためのセットアップ手順です。

この構成では、PC またはスマホのブラウザ/curl から Android Gateway に質問を送り、Android 内のローカル LLM で回答を生成し、TTS で WAV を生成して StackChan に発話指示します。

1. 目的

StackChan を Android Termux 上のローカル LLM Gateway と連携させます。

最終フローは以下です。

Windows / Android Browser / curl
↓
Android Termux Gateway  : http://Android_IP:50021/ask
↓
Android 内 llama-server : http://127.0.0.1:8080/v1/chat/completions
↓
LLM回答生成
↓
pykakasi で漢字混じり日本語をかな寄りに変換
↓
espeak / espeak-ng で WAV 生成
↓
Gateway が StackChan /api/speak へ発話命令
↓
StackChan が Gateway /synthesis から WAV を取得
↓
StackChan が喋る

音声認識/STT は使用しません。
質問入力はブラウザまたは curl から行います。


2. リポジトリ

GitHub:

https://github.com/Xenoah/stackchan-codex

ベース:

release tag β3.5.0

方針:

β3.5.0 simple_wav 構成をベースにする
main ブランチの ConversationController 系は使わない
StackChan 側は simple_wav TTS と /api/speak を使う
Android 側 Gateway が LLM/TTS を担当する

3. 現在の動作確認済み環境

StackChan

StackChan IP : 192.168.0.190
TTS Engine   : simple_wav
TTS Host     : Android Termux Gateway の LAN IP
TTS Port     : 50021
Mode         : LOCAL LLM

例:

StackChan IP       : 192.168.0.190
Android Gateway IP : 192.168.0.106
Gateway Port       : 50021
LLM Port           : 8080

Android Termux

使用する主なもの:

Termux
python
python-pip
tmux
curl
espeak または espeak-ng
pykakasi
llama-server
GGUF model

確認済みモデル例:

~/stackchan-qwen/models/qwen2.5-1.5b-instruct-q4_k_m.gguf

Gateway が標準で探すモデルリンク:

~/models/model.gguf

4. 仕様

4.1 Android Gateway 側 API

GET /

簡易 Web UI を表示します。

http://Android_IP:50021/

GET /status

Gateway の状態を返します。

例:

curl http://192.168.0.106:50021/status

期待レスポンス例:

{
  "ok": true,
  "name": "stackchan-android-llm-gateway-fixed",
  "llm_ok": true,
  "ask": "/ask",
  "chat": "/chat",
  "synthesis": "/synthesis",
  "current_wav": "/data/data/com.termux/files/home/stackchan_gateway/current.wav",
  "stackchan_host": "192.168.0.190"
}

POST /ask

LLM に質問し、回答を WAV 化し、StackChan に喋らせます。

curl -X POST http://192.168.0.106:50021/ask --data-urlencode "text=自己紹介して"

処理内容:

質問を受け取る
↓
ローカル LLM に投げる
↓
回答の文字間スペースを除去
↓
WAV 生成
↓
StackChan /api/speak に回答本文を送る
↓
StackChan が /synthesis から WAV を取得して発話

POST /chat

/ask の別名です。

curl -X POST http://192.168.0.106:50021/chat --data-urlencode "text=なぜ空は青いの?"

POST /synthesis

StackChan の simple_wav 用エンドポイントです。
受け取ったテキストを WAV にして返します。

curl -X POST http://192.168.0.106:50021/synthesis \
  --data-urlencode "text=今日は良い天気です" \
  --output tts_test.wav

GET /current.wav

最後に生成した WAV を返します。

curl http://192.168.0.106:50021/current.wav --output current.wav

4.2 StackChan 側 API

GET /api/status

StackChan の状態確認。

curl http://192.168.0.190/api/status

見るべき項目:

{
  "tts_host": "192.168.0.106",
  "tts_port": 50021,
  "tts_engine": "simple_wav"
}

POST /api/speak

StackChan に発話命令を送ります。

curl -X POST http://192.168.0.190/api/speak --data-urlencode "text=今日は良い天気です"

注意:

これは LLM を通りません。
StackChan に直接「この文字列を喋れ」と命令するだけです。

LLM を介したい場合は、Android Gateway の /ask に送ります。

curl -X POST http://192.168.0.106:50021/ask --data-urlencode "text=自己紹介して"

5. StackChan 本体側セットアップ

StackChan Setup 画面を開きます。

http://192.168.0.190/

以下のように設定します。

TTS Engine Type : simple_wav
TTS Host        : Android Termux Gateway の LAN IP
TTS Port        : 50021
Speaker/Style ID: 3
Text to speak   : __REASK_LAST__

例:

TTS Engine Type : simple_wav
TTS Host        : 192.168.0.106
TTS Port        : 50021
Speaker/Style ID: 3
Text to speak   : __REASK_LAST__

重要:

__REASK_LAST__

前後にアンダースコア2本ずつ必要です。

間違い例:

REASK_LAST
CURRENT
__CURRENT

5.1 Text to speak の意味

__REASK_LAST__

Aボタンなどで最後の質問を再処理したい場合に使います。

流れ:

StackChan Aボタン
↓
Gateway /synthesis に "__REASK_LAST__" を送る
↓
Gateway が最後の質問を LLM に再送
↓
新しい回答を WAV 化
↓
StackChan が喋る

__CURRENT__

最後に生成した current.wav をそのまま喋らせる用途です。

TTS復旧テスト中には便利ですが、運用時は __REASK_LAST__ 推奨です。


6. Android Termux セットアップ

6.1 Termux の準備

Termux を起動して、以下を実行します。

pkg update -y
pkg upgrade -y
pkg install -y git python python-pip tmux curl jq procps coreutils findutils iproute2 lsof clang make cmake

TTS 用に espeak を入れます。

pkg install -y espeak

環境によっては espeak-ng という名前で入る場合があります。
以下で確認します。

command -v espeak
command -v espeak-ng
ls -l $PREFIX/bin/espeak*

どちらかが存在していれば OK です。


6.2 Python パッケージ

python -m pip install --upgrade pip setuptools wheel
python -m pip install --no-cache-dir pykakasi==2.3.0

pykakasi は、漢字混じり日本語を読み上げ用のかな寄りテキストへ変換するために使います。

例:

今日は良い天気です
↓
きょうはよいてんきです

6.3 モデル配置

推奨モデル例:

qwen2.5-1.5b-instruct-q4_k_m.gguf

配置例:

~/stackchan-qwen/models/qwen2.5-1.5b-instruct-q4_k_m.gguf

Gateway/llama-server 用に以下のリンクを作ります。

mkdir -p ~/models

ln -sf ~/stackchan-qwen/models/qwen2.5-1.5b-instruct-q4_k_m.gguf \
  ~/models/model.gguf

ls -lh ~/models/model.gguf

注意:

以下のようなファイルは本体モデルではありません。

ggml-vocab-qwen2.gguf
ggml-vocab-llama-spm.gguf
ggml-vocab-gpt-neox.gguf

ggml-vocab-*.gguf は語彙テスト用であり、LLM 本体として使えません。


6.4 llama-server

Gateway は OpenAI互換APIとして llama-server を呼びます。

期待URL:

http://127.0.0.1:8080/v1/chat/completions

手動起動例:

llama-server \
  -m ~/models/model.gguf \
  --host 127.0.0.1 \
  --port 8080 \
  -c 1024 \
  -ngl 0

確認:

curl http://127.0.0.1:8080/v1/models

llama-server は Android 内部だけで使うため、127.0.0.1 で起動します。
LAN公開する必要はありません。


7. ワンショット起動スクリプト

以下は、Android Termux 側で Gateway を起動するためのスクリプトです。

役割:

既存 Gateway 停止
espeak / espeak-ng 検出
モデルリンク確認
llama-server 起動
LLM対応 Gateway 起動
/status テスト
/ask テスト

作成:

cat > ~/stackchan_android_llm_start.sh <<'EOF'
#!/data/data/com.termux/files/usr/bin/bash
set -Eeuo pipefail

PORT=50021
LLM_PORT=8080
STACKCHAN_HOST="${STACKCHAN_HOST:-192.168.0.190}"

PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
BIN="$PREFIX/bin"
DATA="$HOME/stackchan_gateway"
SERVER="$HOME/stackchan_android_llm_gateway.py"
MODEL_LINK="$HOME/models/model.gguf"

log() {
  echo
  echo "=== $* ==="
}

log "1. stop old gateway"

tmux kill-session -t stackchan_gateway 2>/dev/null || true
tmux kill-session -t stackchan_llama 2>/dev/null || true
pkill -f "stackchan_android_llm_gateway.py" 2>/dev/null || true
pkill -f "llama-server" 2>/dev/null || true
sleep 1

log "2. packages"

pkg update -y
pkg install -y python python-pip tmux curl jq procps coreutils findutils iproute2 lsof git espeak 2>/dev/null || true
python -m pip install --upgrade pip setuptools wheel >/dev/null 2>&1 || true
python -m pip install --no-cache-dir pykakasi==2.3.0 >/dev/null 2>&1 || true

mkdir -p "$DATA/logs" "$DATA/history" "$HOME/models"

log "3. detect espeak"

ESPEAK_BIN=""

# 壊れた開発中ラッパー対策。espeak-ng.real.bak がある場合は優先して復元する。
if [ -x "$BIN/espeak-ng.real.bak" ]; then
  cp -f "$BIN/espeak-ng.real.bak" "$BIN/espeak-ng"
  chmod +x "$BIN/espeak-ng"
  ln -sf "$BIN/espeak-ng" "$BIN/espeak"
fi

if command -v espeak-ng >/dev/null 2>&1; then
  ESPEAK_BIN="$(command -v espeak-ng)"
elif command -v espeak >/dev/null 2>&1; then
  ESPEAK_BIN="$(command -v espeak)"
fi

if [ -z "$ESPEAK_BIN" ]; then
  echo "ERROR: espeak / espeak-ng not found"
  echo "Try:"
  echo "  termux-change-repo"
  echo "  pkg update"
  echo "  pkg install espeak"
  exit 1
fi

# espeak という名前でも呼べるようにする
if ! command -v espeak >/dev/null 2>&1; then
  ln -sf "$ESPEAK_BIN" "$BIN/espeak"
fi

echo "ESPEAK_BIN=$ESPEAK_BIN"
"$ESPEAK_BIN" --version 2>&1 | head -n 3 || true

log "4. model link"

PREFERRED="$HOME/stackchan-qwen/models/qwen2.5-1.5b-instruct-q4_k_m.gguf"
MODEL_SRC=""

if [ -f "$PREFERRED" ]; then
  MODEL_SRC="$PREFERRED"
else
  MODEL_SRC="$(
    find "$HOME" /sdcard/Download -type f -iname "*.gguf" 2>/dev/null \
      | grep -vi "ggml-vocab" \
      | grep -vi "/vocab" \
      | while read -r f; do du -k "$f" 2>/dev/null || true; done \
      | sort -nr \
      | head -n 1 \
      | cut -f2-
  )"
fi

if [ -z "$MODEL_SRC" ] || [ ! -f "$MODEL_SRC" ]; then
  echo "WARNING: model gguf not found. TTS will work, but LLM will be NG."
else
  ln -sf "$MODEL_SRC" "$MODEL_LINK"
  echo "MODEL_SRC=$MODEL_SRC"
  ls -lh "$MODEL_LINK"
fi

log "5. find llama-server"

LLAMA_BIN=""

for d in "$HOME/stackchan-qwen" "$HOME/llama.cpp" "$HOME/stackchan-codex" "$HOME"; do
  if [ -d "$d" ]; then
    found="$(find "$d" -type f -name "llama-server" 2>/dev/null | head -n 1 || true)"
    if [ -n "$found" ]; then
      LLAMA_BIN="$found"
      break
    fi
  fi
done

if [ -n "$LLAMA_BIN" ] && [ -x "$LLAMA_BIN" ] && [ -f "$MODEL_LINK" ]; then
  log "6. start llama-server"
  echo "LLAMA_BIN=$LLAMA_BIN"

  tmux new-session -d -s stackchan_llama \
    "'$LLAMA_BIN' -m '$MODEL_LINK' --host 127.0.0.1 --port $LLM_PORT -c 1024 -ngl 0"

  sleep 5
  curl -s --max-time 10 "http://127.0.0.1:$LLM_PORT/v1/models" | head -c 500 || true
  echo
else
  echo "WARNING: llama-server or model not found. LLM will be NG."
fi

log "7. create gateway"

cat > "$SERVER" <<PY
#!/data/data/com.termux/files/usr/bin/python
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathl...
Read more

success

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 01:31

Termuxでこれを丸ごと実行してください。いま動いているLLMとTTSはなるべく壊さず、Gatewayだけ差し替えます。

cat > ~/stackchan_fix_spacing_and_current.sh <<'EOF'
#!/data/data/com.termux/files/usr/bin/bash
set -Eeuo pipefail

PORT=50021
LLM_PORT=8080
STACKCHAN_HOST="192.168.0.190"

PREFIX="${PREFIX:-/data/data/com.termux/files/usr}"
BIN="$PREFIX/bin"
DATA="$HOME/stackchan_gateway"
SERVER="$HOME/stackchan_android_llm_gateway.py"

echo
echo "=== 1. Gateway停止 LLMは止めない ==="
tmux kill-session -t stackchan_gateway 2>/dev/null || true
pkill -f "stackchan_android_llm_gateway.py" 2>/dev/null || true

mkdir -p "$DATA/logs" "$DATA/history"

echo
echo "=== 2. espeak確認 ==="

if [ ! -x "$BIN/espeak" ] && [ -x "$BIN/espeak-ng.real.bak" ]; then
cp -f "$BIN/espeak-ng.real.bak" "$BIN/espeak-ng"
chmod +x "$BIN/espeak-ng"
ln -sf "$BIN/espeak-ng" "$BIN/espeak"
fi

if [ ! -x "$BIN/espeak" ] && [ -x "$BIN/espeak-ng" ]; then
ln -sf "$BIN/espeak-ng" "$BIN/espeak"
fi

if [ ! -x "$BIN/espeak" ]; then
echo "ERROR: espeak がありません"
ls -l "$BIN"/espeak* 2>/dev/null || true
exit 1
fi

ls -l "$BIN"/espeak*

echo
echo "=== 3. pykakasi確認 ==="
python -m pip install --no-cache-dir pykakasi==2.3.0 >/dev/null 2>&1 || true

echo
echo "=== 4. Gatewayを書き換え ==="

if [ -f "$SERVER" ]; then
cp "$SERVER" "$SERVER.bak.$(date +%Y%m%d_%H%M%S)"
fi

cat > "$SERVER" <<PY
#!/data/data/com.termux/files/usr/bin/python
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
from urllib.parse import parse_qs, urlencode
import urllib.request
import json
import subprocess
import tempfile
import re
import time
import html

PORT = $PORT
LLM_PORT = $LLM_PORT
STACKCHAN_HOST = "$STACKCHAN_HOST"

BASE = Path.home() / "stackchan_gateway"
BASE.mkdir(parents=True, exist_ok=True)
(BASE / "logs").mkdir(exist_ok=True)
(BASE / "history").mkdir(exist_ok=True)

CURRENT_WAV = BASE / "current.wav"
CURRENT_JSON = BASE / "current.json"
LAST_TXT = BASE / "tts_last.txt"
LAST_QUESTION = BASE / "last_question.txt"
LAST_ANSWER = BASE / "last_answer.txt"
LOG_FILE = BASE / "logs" / "llm_gateway.log"

ESPEAK = "/data/data/com.termux/files/usr/bin/espeak"

PITCH = "70"
SPEED = "145"
VOLUME = "105"

JP = r"[\u3040-\u30ff\u3400-\u9fff々〆ヶー]"

def log(msg):
s = f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n"
print(s, end="")
try:
with LOG_FILE.open("a", encoding="utf-8") as f:
f.write(s)
except Exception:
pass

def remove_japanese_char_spaces(text: str) -> str:
# 「こ ん に ち は」「ス タ ッ ク チ ャ ン」を「こんにちは」「スタックチャン」に戻す
old = None
while old != text:
old = text
text = re.sub(f"({JP})\\s+(?={JP})", r"\1", text)
return text

def clean_text(text: str) -> str:
text = text.replace("\x00", "")
text = re.sub(r".*?", " ", text, flags=re.S)
text = re.sub(r"https?://\S+", " ", text)
text = re.sub(r"[\r\n\t]+", " ", text)
text = remove_japanese_char_spaces(text)
text = re.sub(r"\s+", " ", text).strip()
if len(text) > 300:
text = text[:300] + "。"
return text

def token_kind(text: str) -> str:
t = text.strip()
t = t.replace("_", "_")
folded = re.sub(r"[^A-Za-z_]", "", t).upper()
if folded in ("CURRENT", "CURRENT"):
return "current"
if folded in ("REASKLAST", "REASK_LAST", "REASK_LAST"):
return "reask"
return ""

def to_kana(text: str) -> str:
text = remove_japanese_char_spaces(text)
try:
import pykakasi
kks = pykakasi.kakasi()
kana = "".join(x.get("hira") or x.get("orig") or "" for x in kks.convert(text))
return remove_japanese_char_spaces(kana)
except Exception as e:
log("pykakasi error: " + repr(e))
return text

def llm_health():
try:
with urllib.request.urlopen(f"http://127.0.0.1:{LLM_PORT}/v1/models", timeout=3) as res:
return True, res.read().decode("utf-8", errors="ignore")[:300]
except Exception as e:
return False, repr(e)

def ask_llm(question: str) -> str:
question = clean_text(question)
LAST_QUESTION.write_text(question, encoding="utf-8")

payload = {
    "model": "local",
    "messages": [
        {
            "role": "system",
            "content": "あなたは手のひらサイズのコミュニケーションロボット、スタックチャンです。日本語で短く自然に答えてください。読み上げやすい文章にしてください。文字と文字の間に空白を入れないでください。"
        },
        {
            "role": "user",
            "content": question
        }
    ],
    "temperature": 0.7,
    "max_tokens": 160,
    "stream": False
}

data = json.dumps(payload, ensure_ascii=False).encode("utf-8")

req = urllib.request.Request(
    f"http://127.0.0.1:{LLM_PORT}/v1/chat/completions",
    data=data,
    headers={"Content-Type": "application/json"},
    method="POST"
)

try:
    with urllib.request.urlopen(req, timeout=90) as res:
        body = json.loads(res.read().decode("utf-8", errors="ignore"))

    ans = body["choices"][0]["message"]["content"]
    ans = clean_text(ans)

    if not ans:
        ans = "うまく返事を作れませんでした。"

except Exception as e:
    log("LLM ERROR: " + repr(e))
    ans = "えるえるえむの呼び出しに失敗しました。"

LAST_ANSWER.write_text(ans, encoding="utf-8")
return ans

def synth_wav(text: str) -> bytes:
raw = text
kind = token_kind(raw)

if kind == "current":
    if CURRENT_WAV.exists():
        return CURRENT_WAV.read_bytes()
    original = "まだ音声がありません。"
elif kind == "reask":
    if LAST_QUESTION.exists():
        q = LAST_QUESTION.read_text(encoding="utf-8", errors="ignore").strip()
        original = ask_llm(q) if q else "さいごの質問がありません。"
    else:
        original = "さいごの質問がありません。"
else:
    original = clean_text(raw)

kana = clean_text(to_kana(original))

LAST_TXT.write_text(
    "ORIG:\\n" + original + "\\n\\nKANA:\\n" + kana + "\\n",
    encoding="utf-8"
)

with tempfile.TemporaryDirectory() as td:
    td = Path(td)
    txt = td / "in.txt"
    wav = td / "out.wav"
    txt.write_text(kana, encoding="utf-8")

    cmd = [
        ESPEAK,
        "-v", "ja",
        "-p", PITCH,
        "-s", SPEED,
        "-a", VOLUME,
        "-w", str(wav),
        "-f", str(txt)
    ]

    r = subprocess.run(
        cmd,
        stdout=subprocess.PIPE,
        stderr=subprocess.PIPE,
        timeout=30
    )

    if r.returncode != 0:
        raise RuntimeError("espeak failed: " + r.stderr.decode("utf-8", errors="ignore"))

    data = wav.read_bytes()

    if not data.startswith(b"RIFF") or b"WAVE" not in data[:16]:
        raise RuntimeError("not RIFF/WAVE")

    CURRENT_WAV.write_bytes(data)
    CURRENT_JSON.write_text(json.dumps({
        "text": original,
        "kana": kana,
        "time": time.time()
    }, ensure_ascii=False, indent=2), encoding="utf-8")

    return data

def call_stackchan_text(text: str):
# CURRENT ではなく本文を渡す。current読み上げ化を回避する
text = clean_text(text)
data = urlencode({"text": text}).encode("utf-8")
req = urllib.request.Request(
f"http://{STACKCHAN_HOST}/api/speak",
data=data,
headers={"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=8) as res:
return res.read().decode("utf-8", errors="ignore")
except Exception as e:
log("StackChan call error: " + repr(e))
return "stackchan call error: " + repr(e)

class Handler(BaseHTTPRequestHandler):
server_version = "StackChanLLMGateway/1.1"

def log_message(self, fmt, *args):
    log(fmt % args)

def send_body(self, code, body, ctype="text/plain; charset=utf-8"):
    if isinstance(body, str):
        body = body.encode("utf-8")
    self.send_response(code)
    self.send_header("Content-Type", ctype)
    self.send_header("Content-Length", str(len(body)))
    self.send_header("Access-Control-Allow-Origin", "*")
    self.end_headers()
    self.wfile.write(body)

def read_form(self):
    n = int(self.headers.get("Content-Length", "0") or "0")
    raw = self.rfile.read(n)
    s = raw.decode("utf-8", errors="ignore")
    qs = parse_qs(s)
    return qs, s

def do_GET(self):
    path = self.path.split("?", 1)[0]

    if path == "/current.wav":
        if CURRENT_WAV.exists():
            return self.send_body(200, CURRENT_WAV.read_bytes(), "audio/wav")
        return self.send_body(404, "no current.wav")

    if path in ("/status", "/health"):
        ok, detail = llm_health()
        body = {
            "ok": True,
            "name": "stackchan-android-llm-gateway-fixed",
            "llm_ok": ok,
            "llm_detail": detail,
            "ask": "/ask",
            "chat": "/chat",
            "synthesis": "/synthesis",
            "current_wav": str(CURRENT_WAV),
            "espeak": ESPEAK,
            "stackchan_host": STACKCHAN_HOST
        }
        return self.send_body(200, json.dumps(body, ensure_ascii=False, indent=2), "application/json; charset=utf-8")

    last_q = LAST_QUESTION.read_text(encoding="utf-8", errors="ignore") if LAST_QUESTION.exists() else ""
    last_a = LAST_ANSWER.read_text(encoding="utf-8", errors="ignore") if LAST_ANSWER.exists() else ""
    last_tts = LAST_TXT.read_text(encoding="utf-8", errors="ignore") if LAST_TXT.exists() else ""
    llm_ok, _ = llm_health()

...

Read more

curl

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 01:14
β0.4.1

change center position

add motion

Choose a tag to compare

@Xenoah Xenoah released this 28 Jun 00:33
β0.4.0

add motion

root on fix bug

Choose a tag to compare

@Xenoah Xenoah released this 27 Jun 14:53
β0.3.7

add motion

Success to speak

Choose a tag to compare

@Xenoah Xenoah released this 27 Jun 08:30
β0.3.5

add motion

add japanese discription on code

Choose a tag to compare

@Xenoah Xenoah released this 27 Jun 07:55
β0.3.0

add comment

set UI only which YES or NO on select set origin mode

Choose a tag to compare

@Xenoah Xenoah released this 24 Jun 14:45
β0.2.8

ui with only yes or no to select calibration set origin mode