diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index ff4ced2..550135d 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -4,7 +4,7 @@ name: Deploy GitHub Pages # site. Refreshed on doc edits, on each release publish, and on demand. on: push: - branches: [main] + branches: [main, gemini] paths: - 'docs/**' # tools/settings.html is the canonical source; pages.yml copies it @@ -15,6 +15,9 @@ on: # + + diff --git a/docs/index.html b/docs/index.html index 455f6a1..4b1f7cf 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,5 +1,5 @@ - + @@ -33,18 +33,18 @@

🤖 Stack-chan Web Flasher

- ブラウザから M5Stack CoreS3 にスタックチャン用ファームウェアを書き込みます。 -
初回 / リカバリ時用。普段のアップデートは - BLE 設定ページ から OTA でどうぞ。 -
Chrome / Edge など WebSerial 対応ブラウザが必要です。 + Flash Stack-chan firmware (Gemini Live edition) to an M5Stack board from the browser. +
For first-time / recovery use. Everyday updates go through the + BLE settings page OTA. +
Needs Chrome / Edge or another WebSerial browser.

@@ -122,7 +122,7 @@

🤖 Stack-chan Web Flasher

Bundled with esptool-js. - Source: ciniml/stackchan-idf. + Source: m5stack/stackchan-idf-gemini.

diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index eedb961..3cd01dd 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -41,6 +41,7 @@ set(_main_srcs "atom_status.cpp" "audio_codec.cpp" "battery.cpp" + "boot_splash.cpp" "camera_service.cpp" "dance.cpp" "dance_storage.cpp" @@ -92,6 +93,12 @@ idf_component_register( ) target_compile_features(${COMPONENT_LIB} PRIVATE cxx_std_20) +if(CONFIG_STACKCHAN_BOOT_SPLASH) + target_add_binary_data(${COMPONENT_LIB} "${PROJECT_DIR}/assets/splash/splash_cores3.png" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${PROJECT_DIR}/assets/splash/splash_atom.png" BINARY) + target_add_binary_data(${COMPONENT_LIB} "${PROJECT_DIR}/assets/splash/splash_stopwatch.png" BINARY) +endif() + # --- ADR-001: exttab / bootctl の生成と書き込み (partitions_main_*.csv のとき) -- # 開発時の `make flash` で Main と一緒に拡張テーブルと「起動先=Main (確認済み)」の # bootctl を書く。Recovery は recovery/ プロジェクトから別途書く。 diff --git a/main/Kconfig.projbuild b/main/Kconfig.projbuild index dbbcf84..4ee9c4e 100644 --- a/main/Kconfig.projbuild +++ b/main/Kconfig.projbuild @@ -100,3 +100,47 @@ config STACKCHAN_MIC_TAP_HOST 同じ時計 (機体 ms) で突き合わせるための診断用。通常は空 (無効)。 endmenu + +menu "Stack-chan product profile (Gemini customer build)" + + config STACKCHAN_GEMINI_ONLY + bool "Lock AI provider to Gemini Live" + default n + help + Hide OpenAI / XiaoZhi from every user-facing config surface, + ignore attempts to select them over BLE / HTTP, and force the + effective provider to Gemini even if NVS still holds an old + value. OpenAI / XiaoZhi source stays in the tree. Turn off to + restore upstream multi-provider behaviour. + + config STACKCHAN_SETTINGS_ENGLISH + bool "Ship the device settings pages in English" + default y if STACKCHAN_GEMINI_ONLY + default n + help + Build the self-contained Wi-Fi settings page from the upstream + Japanese source, then translate the complete injected HTML and + both inlined JavaScript modules before embedding it. Japanese + sources remain untouched for upstream / non-Gemini builds. The + build fails rather than shipping a partially translated page when + a new Japanese string needs an English catalog entry. + + config STACKCHAN_BOOT_SPLASH + bool "Show Google Cloud logo at boot" + default n + help + After Board::begin(), draw a Google Cloud logo on the LCD until + the face render task takes over. Per-board PNGs are embedded + (CoreS3 320x240, Atom 128x128, StopWatch 466x466). + + config STACKCHAN_BOOT_SPLASH_MS + int "Minimum boot-splash display time (ms)" + default 2000 + range 500 5000 + depends on STACKCHAN_BOOT_SPLASH + help + Floor on how long the Google Cloud logo stays visible, even if + the rest of boot finishes earlier. 1500–3000 ms is the intended + range. + +endmenu diff --git a/main/agent_delegate.cpp b/main/agent_delegate.cpp index 6693d7d..d1a1f93 100644 --- a/main/agent_delegate.cpp +++ b/main/agent_delegate.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: BSL-1.0 #include "agent_delegate.hpp" +#include #include #include @@ -44,14 +45,19 @@ conversation::ToolDefinition Delegate::tool_definition() { return conversation::ToolDefinition{ .name = "ask_agent", - .description = + .description = config::tr( + "Ask a smarter home-server agent about things you cannot answer (search, math, " + "looking things up). It takes tens of seconds; say you will check and wait. " + "One request at a time.", "自分では分からないこと (最新情報の検索、計算、文章の下調べ、家のパソコンにある情報など) を、" "家のサーバーで動いている賢いエージェントに頼む。回答には数十秒かかるので、" "呼んだら「聞いてみるね」とだけ伝えて待つ。回答は後で自動的に届くので、届いたら要点を話す。" - "同時に頼めるのは 1 件。", - .parameters_json = - R"({"type":"object","properties":{"question":{"type":"string","maxLength":1000,)" - R"("description":"エージェントへの依頼内容。必要な前提も含めて日本語で具体的に"}},"required":["question"]})", + "同時に頼めるのは 1 件。"), + .parameters_json = config::ui_japanese() + ? R"({"type":"object","properties":{"question":{"type":"string","maxLength":1000,)" + R"("description":"エージェントへの依頼内容。必要な前提も含めて日本語で具体的に"}},"required":["question"]})" + : R"({"type":"object","properties":{"question":{"type":"string","maxLength":1000,)" + R"("description":"The request for the agent, including needed context"}},"required":["question"]})", .non_blocking = true, }; } @@ -154,13 +160,17 @@ std::optional Delegate::poll(std::uint32_t now_ms) if (now_ms - started_ms_ > kJobTimeoutMs) { ESP_LOGW(kTag, "job %s timed out", job_id_.c_str()); - return finish(Result{.ok = false, .error = "エージェントからの回答が時間内に届きませんでした"}); + return finish(Result{.ok = false, + .error = config::tr("The agent did not answer in time", + "エージェントからの回答が時間内に届きませんでした")}); } int status = 0; const auto resp = http("GET", "/jobs/" + job_id_, "", status); if (!resp) return std::nullopt; // transient; retry next interval - if (status == 404) return finish(Result{.ok = false, .error = "エージェント サーバーが依頼を見失いました"}); + if (status == 404) return finish(Result{.ok = false, + .error = config::tr("The agent server lost the request", + "エージェント サーバーが依頼を見失いました")}); if (status != 200) return std::nullopt; cJSON* root = cJSON_Parse(resp->c_str()); diff --git a/main/agent_tools.cpp b/main/agent_tools.cpp index 7f291dd..663efd2 100644 --- a/main/agent_tools.cpp +++ b/main/agent_tools.cpp @@ -17,6 +17,7 @@ #include "settings_sinks.hpp" #include "utf8.hpp" +#include namespace stackchan::app::agent_tools { @@ -103,9 +104,13 @@ std::string tool_get_time() std::tm tm{}; localtime_r(&now, &tm); if (tm.tm_year + 1900 < 2020) { - return R"json({"ok":false,"error":"時計がまだ合っていません (Wi-Fi 未接続か NTP 未同期)"})json"; + return config::ui_japanese() + ? R"json({"ok":false,"error":"時計がまだ合っていません (Wi-Fi 未接続か NTP 未同期)"})json" + : R"json({"ok":false,"error":"clock not set yet (Wi-Fi down or NTP not synced)"})json"; } - static const char* kWeekday[] = {"日", "月", "火", "水", "木", "金", "土"}; + static const char* kWeekdayJa[] = {"日", "月", "火", "水", "木", "金", "土"}; + static const char* kWeekdayEn[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; + const char* const* kWeekday = config::ui_japanese() ? kWeekdayJa : kWeekdayEn; char buf[160]; std::snprintf(buf, sizeof(buf), R"({"ok":true,"date":"%04d-%02d-%02d","time":"%02d:%02d","weekday":"%s","timezone":"JST"})", @@ -158,9 +163,9 @@ std::string tool_remember(const std::string& args) if (deserializeJson(in, args)) return error_json("bad arguments"); std::string key = in["key"] | ""; std::string text = in["text"] | ""; - if (key.empty() || text.empty()) return error_json("key と text が必要です"); + if (key.empty() || text.empty()) return error_json(config::tr("key and text are required", "key と text が必要です")); if (key.size() > kMaxKeyBytes) key.resize(kMaxKeyBytes); - if (text.size() > kMaxValueBytes) return error_json("text が長すぎます (500 バイトまで)"); + if (text.size() > kMaxValueBytes) return error_json(config::tr("text is too long (500 bytes max)", "text が長すぎます (500 バイトまで)")); auto store = make_json_doc(); JsonArray arr = load_items(store); @@ -178,7 +183,7 @@ std::string tool_remember(const std::string& args) item["v"] = text; item["t"] = static_cast(std::time(nullptr)); const std::size_t n = arr.size(); - if (!save_items(store)) return error_json("保存に失敗しました"); + if (!save_items(store)) return error_json(config::tr("save failed", "保存に失敗しました")); ESP_LOGI(kTag, "remember: %s (%u items)", key.c_str(), static_cast(n)); char b[64]; std::snprintf(b, sizeof(b), R"({"ok":true,"stored":%u})", static_cast(n)); @@ -212,14 +217,14 @@ std::string tool_forget(const std::string& args) auto in = make_json_doc(); if (deserializeJson(in, args)) return error_json("bad arguments"); const std::string key = in["key"] | ""; - if (key.empty()) return error_json("key が必要です"); + if (key.empty()) return error_json(config::tr("key is required", "key が必要です")); auto store = make_json_doc(); JsonArray arr = load_items(store); int removed = 0; for (std::size_t i = arr.size(); i-- > 0;) { if (key == (arr[i]["k"] | "")) { arr.remove(i); ++removed; } } - if (!save_items(store)) return error_json("保存に失敗しました"); + if (!save_items(store)) return error_json(config::tr("save failed", "保存に失敗しました")); char b[64]; std::snprintf(b, sizeof(b), R"({"ok":true,"removed":%d})", removed); return b; @@ -234,9 +239,15 @@ std::string tool_set_timer(const std::string& args, SharedState& state) const double seconds = in["seconds"] | -1.0; std::string display = in["message"] | ""; std::string reading = in["reading"] | ""; - if (seconds <= 0 || seconds > 12 * 3600) return error_json("seconds は 1〜43200 の範囲で指定してください"); - if (display.empty()) display = "じかんだよ"; - if (reading.empty()) reading = display; + if (seconds <= 0 || seconds > 12 * 3600) + return error_json(config::tr("seconds must be between 1 and 43200", + "seconds は 1〜43200 の範囲で指定してください")); + if (display.empty()) display = config::tr("It's time", "じかんだよ"); + if (reading.empty()) { + // Japanese formant TTS cannot speak English; leave empty so demo_loop + // falls back to the balloon only. + reading = config::ui_japanese() ? display : std::string{}; + } if (display.size() > 120) display.resize(120); if (reading.size() > 120) reading.resize(120); const std::uint32_t now_ms = static_cast(esp_timer_get_time() / 1000); @@ -274,11 +285,15 @@ std::string tool_set_volume(const std::string& args, SharedState& state) } else if (action == "down") { pct = std::max(kMin, pct - kStep); } else if (action == "set") { - if (level < 0 || level > kMax) return R"({"ok":false,"error":"level は 0〜200 で指定"})"; + if (level < 0 || level > kMax) + return config::ui_japanese() ? R"({"ok":false,"error":"level は 0〜200 で指定"})" + : R"({"ok":false,"error":"level must be 0..200"})"; pct = std::max(kMin, level); muted = false; } else { - return R"({"ok":false,"error":"action は up / down / mute / unmute / set のいずれか"})"; + return config::ui_japanese() + ? R"({"ok":false,"error":"action は up / down / mute / unmute / set のいずれか"})" + : R"({"ok":false,"error":"action must be up / down / mute / unmute / set"})"; } state.speaker.muted.store(muted, std::memory_order_relaxed); settings_sinks::apply_speaker_volume_persist(static_cast(pct)); @@ -300,7 +315,9 @@ std::string tool_lt_timer(const std::string& args, SharedState& state) const double minutes = in["minutes"] | 0.0; char buf[160]; if (action == "start") { - if (minutes < 0.5 || minutes > 180) return R"({"ok":false,"error":"minutes は 0.5〜180 で指定"})"; + if (minutes < 0.5 || minutes > 180) + return config::ui_japanese() ? R"({"ok":false,"error":"minutes は 0.5〜180 で指定"})" + : R"({"ok":false,"error":"minutes must be 0.5..180"})"; const auto total_s = static_cast(minutes * 60.0 + 0.5); state.lt.total_s.store(total_s, std::memory_order_relaxed); state.lt.command.store(1, std::memory_order_release); @@ -320,84 +337,108 @@ std::string tool_lt_timer(const std::string& args, SharedState& state) static_cast(state.lt.total_s.load(std::memory_order_relaxed))); return buf; } - return R"({"ok":false,"error":"action は start / stop / status のいずれか"})"; + return config::ui_japanese() ? R"({"ok":false,"error":"action は start / stop / status のいずれか"})" + : R"({"ok":false,"error":"action must be start / stop / status"})"; } std::vector definitions() { + const bool ja = config::ui_japanese(); return { conv::ToolDefinition{ .name = "lt_timer", - .description = - "発表 (LT) 用のカウントダウン タイマー。action: start (minutes で長さを指定、例 1 や 5)、" - "stop (停止・リセット)、status (残り時間)。画面に残り時間を表示し、残り 1 分・終了・超過を声で知らせる。" - "「3 分のタイマーをセット」「タイマー止めて」「あと何分?」などで使う。" - "単純に N 分後に一言知らせてほしいだけなら set_timer を使う。", + .description = ja + ? "発表 (LT) 用のカウントダウン タイマー。action: start (minutes で長さを指定、例 1 や 5)、" + "stop (停止・リセット)、status (残り時間)。画面に残り時間を表示し、残り 1 分・終了・超過を声で知らせる。" + "「3 分のタイマーをセット」「タイマー止めて」「あと何分?」などで使う。" + "単純に N 分後に一言知らせてほしいだけなら set_timer を使う。" + : "Lightning-talk countdown timer. action: start (minutes, e.g. 1 or 5), " + "stop, status. Shows remaining time on screen. In English mode announcements " + "are on-screen only (no Japanese TTS). Use set_timer for a one-shot reminder.", .parameters_json = R"({"type":"object","properties":{"action":{"type":"string","enum":["start","stop","status"]},)" R"("minutes":{"type":"number","minimum":0.5,"maximum":180}},"required":["action"]})", }, conv::ToolDefinition{ .name = "set_volume", - .description = - "スピーカーの音量を変える / ミュートする。action: up (少し上げる), down (少し下げる), " - "mute (消音), unmute (消音解除), set (level で 0〜200% を直接指定、100 が標準)。" - "「音量上げて」「うるさい」「静かにして」「ミュート」「音出して」などで使う。", + .description = ja + ? "スピーカーの音量を変える / ミュートする。action: up (少し上げる), down (少し下げる), " + "mute (消音), unmute (消音解除), set (level で 0〜200% を直接指定、100 が標準)。" + "「音量上げて」「うるさい」「静かにして」「ミュート」「音出して」などで使う。" + : "Change speaker volume or mute. action: up, down, mute, unmute, set " + "(level 0..200 percent, 100 = default).", .parameters_json = R"({"type":"object","properties":{"action":{"type":"string","enum":["up","down","mute","unmute","set"]},)" R"("level":{"type":"integer","minimum":0,"maximum":200}},"required":["action"]})", }, conv::ToolDefinition{ .name = "get_time", - .description = "現在の日付・時刻・曜日 (日本時間) を取得する。時間や日付を聞かれたら必ずこれで確認する。", + .description = ja ? "現在の日付・時刻・曜日 (日本時間) を取得する。時間や日付を聞かれたら必ずこれで確認する。" + : "Get the current date, time, and weekday (JST). Always call this instead of guessing.", .parameters_json = R"({"type":"object","properties":{}})", }, conv::ToolDefinition{ .name = "get_status", - .description = - "スタックチャン自身の状態を取得する: 近接センサー (手が近くにあるか)、バッテリー残量、" - "起動からの時間、LT タイマーの残り。体調や電池、近くに何かあるか聞かれたときに使う。", + .description = ja + ? "スタックチャン自身の状態を取得する: 近接センサー (手が近くにあるか)、バッテリー残量、" + "起動からの時間、LT タイマーの残り。体調や電池、近くに何かあるか聞かれたときに使う。" + : "Read Stack-chan's own status: proximity (hand nearby), battery, uptime, LT timer remaining.", .parameters_json = R"({"type":"object","properties":{}})", }, conv::ToolDefinition{ .name = "remember", - .description = - "ユーザーに頼まれたことや大事な情報を、電源を切っても残るメモとして保存する。" - "key は短い見出し (例: \"ユーザーの名前\")、text は内容。同じ key は上書き。", + .description = ja + ? "ユーザーに頼まれたことや大事な情報を、電源を切っても残るメモとして保存する。" + "key は短い見出し (例: \"ユーザーの名前\")、text は内容。同じ key は上書き。" + : "Persist a short note across reboots. key is a heading, text is the content. Same key overwrites.", .parameters_json = R"({"type":"object","properties":{"key":{"type":"string","maxLength":48},)" R"("text":{"type":"string","maxLength":500}},"required":["key","text"]})", }, conv::ToolDefinition{ .name = "recall", - .description = "保存してあるメモを読み出す。key を省略すると全部返す。以前の話や約束を聞かれたら使う。", + .description = ja ? "保存してあるメモを読み出す。key を省略すると全部返す。以前の話や約束を聞かれたら使う。" + : "Recall stored notes. Omit key to list all.", .parameters_json = R"({"type":"object","properties":{"key":{"type":"string"}}})", }, conv::ToolDefinition{ .name = "forget", - .description = "保存してあるメモを key を指定して消す。", + .description = ja ? "保存してあるメモを key を指定して消す。" : "Delete a stored note by key.", .parameters_json = R"({"type":"object","properties":{"key":{"type":"string"}},"required":["key"]})", }, conv::ToolDefinition{ .name = "set_timer", - .description = - "指定秒数後にスタックチャンが自分から声をかける (タイマー / リマインダー)。" - "message は画面に出す文、reading はその読み (ひらがな・カタカナのみ、漢字不可)。" - "設定できるのは 1 件だけで、再起動すると消える。", - .parameters_json = - R"({"type":"object","properties":{"seconds":{"type":"integer","minimum":1,"maximum":43200},)" - R"("message":{"type":"string","maxLength":60},"reading":{"type":"string","maxLength":80}},)" - R"("required":["seconds","message","reading"]})", + .description = ja + ? "指定秒数後にスタックチャンが自分から声をかける (タイマー / リマインダー)。" + "message は画面に出す文、reading はその読み (ひらがな・カタカナのみ、漢字不可)。" + "設定できるのは 1 件だけで、再起動すると消える。" + : "One-shot reminder after N seconds. message is shown on screen. " + "reading (hiragana) is optional and ignored in English mode (text balloon only). " + "One reminder at a time; lost on reboot.", + .parameters_json = ja + ? R"({"type":"object","properties":{"seconds":{"type":"integer","minimum":1,"maximum":43200},)" + R"("message":{"type":"string","maxLength":60},"reading":{"type":"string","maxLength":80}},)" + R"("required":["seconds","message","reading"]})" + : R"({"type":"object","properties":{"seconds":{"type":"integer","minimum":1,"maximum":43200},)" + R"("message":{"type":"string","maxLength":60},"reading":{"type":"string","maxLength":80}},)" + R"("required":["seconds","message"]})", }, }; } const char* guidance() { - return "\n\n[ツール] 時刻や日付を聞かれたら get_time、電池や近くの様子を聞かれたら get_status を使って" - "確かめてから答えてください (推測で答えない)。覚えておいてと言われたら remember、" - "前に話したことを聞かれたら recall を使います。「N 分後に教えて」は set_timer を使います " - "(reading はひらがなで)。音量やミュートの指示は set_volume、「N 分のタイマーをセット」は lt_timer を使います。ツールは必ず関数呼び出し (function calling) の仕組みで呼び、" - "「tool_code」や関数名を文章・音声として話さないでください。ツールの結果は要点だけ短く話してください。"; + return config::ui_japanese() + ? "\n\n[ツール] 時刻や日付を聞かれたら get_time、電池や近くの様子を聞かれたら get_status を使って" + "確かめてから答えてください (推測で答えない)。覚えておいてと言われたら remember、" + "前に話したことを聞かれたら recall を使います。「N 分後に教えて」は set_timer を使います " + "(reading はひらがなで)。音量やミュートの指示は set_volume、「N 分のタイマーをセット」は lt_timer を使います。ツールは必ず関数呼び出し (function calling) の仕組みで呼び、" + "「tool_code」や関数名を文章・音声として話さないでください。ツールの結果は要点だけ短く話してください。" + : "\n\n[Tools] Use get_time for the date or time, and get_status for battery / " + "proximity / uptime — do not guess. Use remember / recall / forget for notes. " + "Use set_timer for a one-shot reminder (message only; do not rely on reading). " + "Use set_volume for loudness, lt_timer for a countdown on screen. Always invoke " + "tools via function calling; never speak tool names or tool_code. Summarise tool " + "results briefly."; } std::optional dispatch(const std::string& name, const std::string& args, SharedState& state) diff --git a/main/ap_screen.cpp b/main/ap_screen.cpp index 4947046..e3d0f10 100644 --- a/main/ap_screen.cpp +++ b/main/ap_screen.cpp @@ -7,6 +7,7 @@ #include #include +#include #include "wifi_sta.hpp" namespace stackchan::app::ap_screen { @@ -160,7 +161,7 @@ bool draw(M5GFX& display) : &fonts::lgfxJapanGothic_16); display.setTextColor(TFT_BLACK, TFT_WHITE); display.setTextDatum(lgfx::textdatum_t::top_center); - display.drawString("Wi-Fi 設定モード", W / 2, top_margin); + display.drawString(config::tr("Wi-Fi setup", "Wi-Fi 設定モード"), W / 2, top_margin); } // QR. Version 0 = auto-pick by payload length. The payload is ≤ 80 @@ -203,7 +204,7 @@ bool draw(M5GFX& display) : &fonts::lgfxJapanGothic_16); display.setTextColor(TFT_WHITE, red); display.setTextDatum(lgfx::textdatum_t::middle_center); - display.drawString("終了 (AP モード停止)", + display.drawString(config::tr("Exit (stop AP)", "終了 (AP モード停止)"), btn_x + btn_w / 2, btn_band_y + btn_h / 2); } display.endWrite(); diff --git a/main/app_main.cpp b/main/app_main.cpp index 5a04924..49a89b7 100644 --- a/main/app_main.cpp +++ b/main/app_main.cpp @@ -71,6 +71,8 @@ #include "wifi_audio.hpp" #endif #include "wifi_sta.hpp" +#include "boot_splash.hpp" +#include #include #ifdef CONFIG_TELEGRAM_PHASE1_ENABLED @@ -282,6 +284,7 @@ extern "C" void app_main() // kind (the Module Audio probe below) are folded into this local copy in // one place; everything downstream reads the profile, not board.kind(). stackchan::board::BoardProfile profile = stackchan::board::profile_for(board.kind()); + stackchan::app::boot_splash::show(board.display(), profile.circular_display); // Reserve the conversation task's internal-RAM speaker ring *now*, before // anything else has a chance to chew up DRAM. The conversation task itself @@ -727,6 +730,7 @@ extern "C" void app_main() case StartError::BadTag: return "bad tag"; case StartError::UnknownBoard: return "unknown board"; case StartError::WorkerSpawnFailed: return "spawn failed"; + case StartError::NoCatalog: return "update catalog unset"; } return "fetch failed"; }); @@ -912,7 +916,7 @@ extern "C" void app_main() const char* xiaozhi_token = ""; if (!cfg.openai_enabled) { ESP_LOGI(kTag, "Conversation disabled by configuration"); - } else if (cfg.provider == stackchan::config::Provider::Gemini) { + } else if (stackchan::config::effective_provider(cfg) == stackchan::config::Provider::Gemini) { if (!cfg.gemini_api_key.empty()) { api_key = cfg.gemini_api_key.c_str(); } @@ -936,12 +940,15 @@ extern "C" void app_main() #if CONFIG_STACKCHAN_CONVERSATION_ENABLED g_conversation_args = new stackchan::app::ConversationTaskArgs{ - .state = g_state, .api_key = api_key, .provider = cfg.provider, .touch = head_touch, + .state = g_state, .api_key = api_key, + .provider = stackchan::config::effective_provider(cfg), .touch = head_touch, .xiaozhi_url = xiaozhi_url, .xiaozhi_token = xiaozhi_token, .system_prompt = cfg.system_prompt.c_str(), .extra_headers = cfg.conv_extra_headers.c_str(), .agent_url = cfg.agent_url.c_str(), .agent_token = cfg.agent_token.c_str(), + .gemini_model = cfg.gemini_model.c_str(), + .gemini_voice = cfg.gemini_voice.c_str(), .seg_buf = g_conv_seg_buf}; #else (void)api_key; (void)xiaozhi_url; (void)xiaozhi_token; @@ -952,6 +959,7 @@ extern "C" void app_main() // profile.button_overlay_ui decides). render_task and demo_loop // dispatch through screens:: from here on. stackchan::app::diag_heap("pre-screens/render"); + stackchan::app::boot_splash::wait_min_time(); stackchan::app::screens::init(board.display(), *g_state, profile); stackchan::app::start_render_task(*g_render_args); if (!no_servo_bus && cfg.servo_enabled && g_servo_args != nullptr) { diff --git a/main/boot_splash.cpp b/main/boot_splash.cpp new file mode 100644 index 0000000..08a0f22 --- /dev/null +++ b/main/boot_splash.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2026 Kenta IDA +// SPDX-License-Identifier: BSL-1.0 + +#include "boot_splash.hpp" + +#include "sdkconfig.h" + +#include + +#include +#include +#include +#include +#include + +namespace stackchan::app::boot_splash { + +namespace { +constexpr const char* kTag = "boot-splash"; + +#if CONFIG_STACKCHAN_BOOT_SPLASH +extern "C" { +extern const std::uint8_t splash_cores3_png_start[] asm("_binary_splash_cores3_png_start"); +extern const std::uint8_t splash_cores3_png_end[] asm("_binary_splash_cores3_png_end"); +extern const std::uint8_t splash_atom_png_start[] asm("_binary_splash_atom_png_start"); +extern const std::uint8_t splash_atom_png_end[] asm("_binary_splash_atom_png_end"); +extern const std::uint8_t splash_stopwatch_png_start[] asm("_binary_splash_stopwatch_png_start"); +extern const std::uint8_t splash_stopwatch_png_end[] asm("_binary_splash_stopwatch_png_end"); +} + +std::int64_t g_shown_us = 0; + +void blit(M5GFX& display, const std::uint8_t* start, const std::uint8_t* end) +{ + const std::size_t len = static_cast(end - start); + display.startWrite(); + display.fillScreen(TFT_WHITE); + display.drawPng(start, len, 0, 0); + display.endWrite(); + display.display(); +} +#endif +} // namespace + +void show(M5GFX& display, bool /*circular_display*/) +{ +#if CONFIG_STACKCHAN_BOOT_SPLASH + const int w = display.width(); + if (w >= 400) { + blit(display, splash_stopwatch_png_start, splash_stopwatch_png_end); + } else if (w >= 240) { + blit(display, splash_cores3_png_start, splash_cores3_png_end); + } else { + blit(display, splash_atom_png_start, splash_atom_png_end); + } + g_shown_us = esp_timer_get_time(); + ESP_LOGI(kTag, "Google Cloud splash %dx%d", display.width(), display.height()); +#else + (void)display; +#endif +} + +void wait_min_time() +{ +#if CONFIG_STACKCHAN_BOOT_SPLASH + if (g_shown_us == 0) return; +#ifdef CONFIG_STACKCHAN_BOOT_SPLASH_MS + const std::int64_t min_us = static_cast(CONFIG_STACKCHAN_BOOT_SPLASH_MS) * 1000; +#else + const std::int64_t min_us = 2000 * 1000; +#endif + const std::int64_t elapsed = esp_timer_get_time() - g_shown_us; + if (elapsed < min_us) { + const std::uint32_t remain_ms = + static_cast((min_us - elapsed + 999) / 1000); + vTaskDelay(pdMS_TO_TICKS(remain_ms)); + } +#endif +} + +} // namespace stackchan::app::boot_splash diff --git a/main/boot_splash.hpp b/main/boot_splash.hpp new file mode 100644 index 0000000..af8932c --- /dev/null +++ b/main/boot_splash.hpp @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 Kenta IDA +// SPDX-License-Identifier: BSL-1.0 + +#pragma once + +#include + +namespace stackchan::app::boot_splash { + +// Draw the Google Cloud startup logo on `display` (board-native resolution) +// and record the show timestamp. No-op when CONFIG_STACKCHAN_BOOT_SPLASH is +// unset. Safe to call immediately after Board::begin(). +void show(M5GFX& display, bool circular_display); + +// Block until the minimum splash time has elapsed (CONFIG_STACKCHAN_BOOT_SPLASH_MS, +// default 2000). Call just before the face render task takes over the panel. +void wait_min_time(); + +} // namespace stackchan::app::boot_splash diff --git a/main/conversation_task.cpp b/main/conversation_task.cpp index ca5f51b..48a4310 100644 --- a/main/conversation_task.cpp +++ b/main/conversation_task.cpp @@ -29,6 +29,7 @@ #include "conversation/metrics.hpp" #include "conversation/openai_realtime_client.hpp" #include "conversation/xiaozhi_client.hpp" +#include #include "utf8.hpp" #include "wifi_sta.hpp" @@ -94,18 +95,31 @@ enum class Local : std::uint8_t { Yielded, // I2S handed off to BLE audio streamer (mic + speaker ended) }; -const char* kInstructions = +const char* kInstructionsJa = "あなたは「スタックチャン」という小さな卓上ロボットです。M5Stack CoreS3 で動いています。" "フレンドリーで元気いっぱい、少し子供っぽい口調で、短く返事をします。ユーザーとは日本語で会話してください。ただし、ユーザーが日本語以外の言語(英語)などで話したいと要望したときは、ユーザーが要望する言語に切り替えてください。" "顔の表情と首の向きを変えられます。気持ちに合わせて set_expression や set_head_pose ツールを使ってください。" "また speak_katakoto ツールでロボット風のカタコト声を出すこともできます。" "ものまね・効果音・繰り返しなど演出的に使ってください(ツール呼び出しのターンでは普通の声で続けて喋らなくて構いません)。"; +const char* kInstructionsEn = + "You are Stack-chan, a small desktop robot running on M5Stack. Be friendly, " + "cheerful, and a little childlike. Keep replies short. Speak English with the " + "user (switch only if they clearly ask for another language). You can change " + "your facial expression and head pose; use set_expression and set_head_pose " + "to match the mood."; + +const char* kInstructions() +{ + return config::ui_japanese() ? kInstructionsJa : kInstructionsEn; +} + conv::ToolDefinition make_set_expression_tool() { return conv::ToolDefinition{ .name = "set_expression", - .description = "スタックチャンの顔の表情を変える。感情を表現したいときに使う。", + .description = config::tr("Change Stack-chan's facial expression to match the mood.", + "スタックチャンの顔の表情を変える。感情を表現したいときに使う。"), .parameters_json = R"({"type":"object","properties":{"expression":{"type":"string",)" R"("enum":["neutral","happy","sad","angry","doubt","sleepy"]}},"required":["expression"]})", @@ -116,7 +130,9 @@ conv::ToolDefinition make_set_head_pose_tool() { return conv::ToolDefinition{ .name = "set_head_pose", - .description = "スタックチャンの首の向きを変える。yaw は左右(-40〜40度)、pitch は上下(-10〜25度)。", + .description = config::tr( + "Change Stack-chan's head pose. yaw is left/right (-40 to 40 deg), pitch is up/down (-10 to 25 deg).", + "スタックチャンの首の向きを変える。yaw は左右(-40〜40度)、pitch は上下(-10〜25度)。"), .parameters_json = R"({"type":"object","properties":{)" R"("yaw_deg":{"type":"number"},"pitch_deg":{"type":"number"}},"required":["yaw_deg","pitch_deg"]})", @@ -204,6 +220,7 @@ class Coordinator { board::Si12tTouch* touch, const char* xiaozhi_url, const char* xiaozhi_token, const char* system_prompt, const char* extra_headers, const char* agent_url, const char* agent_token, + const char* gemini_model, const char* gemini_voice, const std::array& seg_buf) : state_{state}, api_key_{api_key != nullptr ? api_key : ""}, provider_{provider}, touch_{touch}, @@ -211,6 +228,8 @@ class Coordinator { xiaozhi_token_{xiaozhi_token != nullptr ? xiaozhi_token : ""}, system_prompt_{system_prompt != nullptr ? system_prompt : ""}, extra_headers_{extra_headers != nullptr ? extra_headers : ""}, + gemini_model_{gemini_model != nullptr ? gemini_model : ""}, + gemini_voice_{gemini_voice != nullptr ? gemini_voice : ""}, delegate_{agent_url != nullptr ? agent_url : "", agent_token != nullptr ? agent_token : ""}, seg_buf_{seg_buf} { @@ -381,7 +400,7 @@ class Coordinator { conv::ConversationConfig cfg{}; // User-set system prompt (over Wi-Fi) wins; empty falls back to the // firmware's built-in persona. - cfg.instructions = system_prompt_.empty() ? kInstructions : system_prompt_; + cfg.instructions = system_prompt_.empty() ? kInstructions() : system_prompt_; // Tool guidance applies whichever persona is in use (a user prompt // replaces kInstructions wholesale, so append rather than embed). cfg.instructions += agent_tools::guidance(); @@ -390,10 +409,10 @@ class Coordinator { if (provider_ == config::Provider::Gemini) { // Gemini Live model + voice. The model is namespaced as // "models/..."; the client prepends that for us when missing. - // gemini-2.0-flash-live-001 was deprecated; the native-audio - // preview is what's actually live on the Developer API right now. - cfg.model = "gemini-2.5-flash-native-audio-preview-12-2025"; - cfg.voice = "Aoede"; // pre-built voice name; OK to leave empty + // Default is the current AI Studio GA native-audio live model + // (Kconfig / NVS); empty voice is also accepted by the API. + cfg.model = gemini_model_.empty() ? config::default_gemini_model() : gemini_model_; + cfg.voice = gemini_voice_.empty() ? config::default_gemini_voice() : gemini_voice_; } else { cfg.model = CONFIG_STACKCHAN_OPENAI_REALTIME_MODEL; cfg.voice = CONFIG_STACKCHAN_OPENAI_VOICE; @@ -405,7 +424,11 @@ class Coordinator { // device is the MCP server) so the xiaozhi server's LLM can call them. cfg.tools.push_back(make_set_expression_tool()); cfg.tools.push_back(make_set_head_pose_tool()); - cfg.tools.push_back(make_speak_katakoto_tool()); + // speak_katakoto drives the Japanese-only formant TTS engine. Hide + // it from the model in English mode; the handler stays in the tree. + if (config::ui_japanese()) { + cfg.tools.push_back(make_speak_katakoto_tool()); + } // XiaoZhi: the server runs ask_agent itself (it sits on the same LAN as // the agent server, whereas the device may be talking through a // tunnel), so don't publish ours over MCP — two tools of the same @@ -655,7 +678,7 @@ class Coordinator { ESP_LOGI(kTag, "barge-in: user touched the head, interrupting reply"); M5.Speaker.stop(); (void)client_->cancel_response(); - state_.set_balloon_text("はいはい?", /*hold_ms=*/1500); + state_.set_balloon_text(config::tr("Yes?", "はいはい?"), /*hold_ms=*/1500); log_playback_metrics(); enter_listening(); } @@ -792,7 +815,9 @@ class Coordinator { ESP_LOGI(kTag, "notice (balloon only on Gemini): %s", text.c_str()); return; } - const std::string turn = "[お知らせ] " + text + " — これをユーザーにそのまま短く日本語で伝えてください"; + const std::string turn = config::ui_japanese() + ? ("[お知らせ] " + text + " — これをユーザーにそのまま短く日本語で伝えてください") + : ("[Notice] " + text + " — Tell the user this briefly, in their language."); if (auto r = client_->push_text(turn); r) { set_local(Local::Thinking); thinking_since_ms_ = now_ms(); @@ -809,10 +834,18 @@ class Coordinator { if (!res) return; std::string text; if (res->ok) { - text = "[エージェントからの回答が届きました。ユーザーの質問「" + delegate_.question() + - "」への答えです。要点を短く話してください]\n" + res->answer; + text = std::string(config::tr( + "[Answer from the agent for the user's question \"", + "[エージェントからの回答が届きました。ユーザーの質問「")) + + delegate_.question() + + config::tr("\". Summarise the key points briefly.]\n", + "」への答えです。要点を短く話してください]\n") + + res->answer; } else { - text = "[エージェントへの依頼は失敗しました: " + res->error + "。ユーザーにそう伝えてください]"; + text = std::string(config::tr("[The agent request failed: ", + "[エージェントへの依頼は失敗しました: ")) + + res->error + + config::tr(". Please tell the user.]", "。ユーザーにそう伝えてください]"); } if (text.size() > 3000) text.resize(3000); // Show the answer itself right away: if the session drops while the @@ -828,7 +861,9 @@ class Coordinator { // Deferred functionResponse for the NON_BLOCKING call. std::string payload = res->ok ? R"({"ok":true,"answer":)" + json_quote(res->answer) : R"({"ok":false,"error":)" + json_quote(res->error); - payload += R"(,"note":"ユーザーに要点を短く話してください","scheduling":"INTERRUPT"})"; + payload += config::ui_japanese() + ? R"(,"note":"ユーザーに要点を短く話してください","scheduling":"INTERRUPT"})" + : R"(,"note":"Summarise the key points briefly for the user.","scheduling":"INTERRUPT"})"; r = client_->submit_tool_result(agent_call_id_, payload); agent_call_id_.clear(); } @@ -845,7 +880,8 @@ class Coordinator { ESP_LOGW(kTag, "push_text failed (%d); announcing via reminder slot", static_cast(r.error())); std::string display = res->ok ? res->answer : res->error; if (display.size() > 120) display.resize(120); - std::string reading = res->ok && !res->reading.empty() ? res->reading : "えーじぇんとからへんじがきたよ"; + std::string reading = res->ok && !res->reading.empty() ? res->reading + : (config::ui_japanese() ? "えーじぇんとからへんじがきたよ" : std::string{}); state_.set_reminder(now_ms(), display, reading); } } @@ -1146,7 +1182,7 @@ class Coordinator { ev.error == conv::ConversationError::TransportInit; if (transport_error) { ESP_LOGE(kTag, "transport error: %s — reconnecting", ev.text.c_str()); - state_.set_balloon_text("接続エラー", /*hold_ms=*/3000); + state_.set_balloon_text(config::tr("Connection error", "接続エラー"), /*hold_ms=*/3000); recover_after_error(); } else { ESP_LOGW(kTag, "server error (non-fatal): %s", ev.text.c_str()); @@ -1383,6 +1419,8 @@ class Coordinator { std::string xiaozhi_token_; std::string system_prompt_; std::string extra_headers_; + std::string gemini_model_; + std::string gemini_voice_; agent_delegate::Delegate delegate_; conv::ConversationConfig config_{}; std::unique_ptr client_; @@ -1450,7 +1488,8 @@ void conversation_task_entry(void* arg) auto& args = *static_cast(arg); auto* coordinator = new Coordinator(*args.state, args.api_key, args.provider, args.touch, args.xiaozhi_url, args.xiaozhi_token, args.system_prompt, - args.extra_headers, args.agent_url, args.agent_token, args.seg_buf); + args.extra_headers, args.agent_url, args.agent_token, + args.gemini_model, args.gemini_voice, args.seg_buf); coordinator->run(); // run() only returns by deleting the task; keep the object alive regardless. vTaskDelete(nullptr); diff --git a/main/conversation_task.hpp b/main/conversation_task.hpp index 24695b4..b83659a 100644 --- a/main/conversation_task.hpp +++ b/main/conversation_task.hpp @@ -39,6 +39,10 @@ struct ConversationTaskArgs { // Empty url hides the tool. const char* agent_url = ""; const char* agent_token = ""; + // Gemini Live model + prebuilt voice. Empty → firmware / Kconfig default. + // Ignored by OpenAI / XiaoZhi. + const char* gemini_model = ""; + const char* gemini_voice = ""; // Internal-RAM playback ring, pre-allocated by app_main in the boot-time // low-fragmentation window. We can't allocate this lazily from the // conversation task: by the time it runs (post Wi-Fi association, ~12 s diff --git a/main/demo_loop.cpp b/main/demo_loop.cpp index 72e6e1b..75c2ec1 100644 --- a/main/demo_loop.cpp +++ b/main/demo_loop.cpp @@ -20,6 +20,7 @@ #include #include #include "config_service/config_service.hpp" +#include "config_service/product.hpp" #include "avatar/expression.hpp" #include "battery.hpp" @@ -407,11 +408,11 @@ constexpr const char* kTag = "stackchan"; g_state->face.expression.store(static_cast(avatar::Expression::Happy), std::memory_order_relaxed); g_state->set_balloon_text(rem_display, /*hold_ms=*/6000); - if (allow_full_demo) { + if (allow_full_demo && config::ui_japanese() && !rem_reading.empty()) { speech.stop(); (void)speech.say(decode_utf8(rem_reading)); next_speech_ms = std::max(next_speech_ms, now_ms + 8000); - } else { + } else if (!allow_full_demo) { // Conversation owns the speaker: let the assistant say it. g_state->post_notice(rem_display); } @@ -449,7 +450,8 @@ constexpr const char* kTag = "stackchan"; if (!wifi_ok && !wifi_warning_active) { speech.stop(); // hold_ms = UINT32_MAX so the balloon stays put until we clear it. - g_state->set_balloon_text("Wi-Fi: 切断中", /*hold_ms=*/UINT32_MAX); + g_state->set_balloon_text(config::tr("Wi-Fi: disconnected", "Wi-Fi: 切断中"), + /*hold_ms=*/UINT32_MAX); balloon_in_flight.store(false, std::memory_order_release); wifi_warning_active = true; } else if (wifi_ok && wifi_warning_active) { @@ -471,7 +473,9 @@ constexpr const char* kTag = "stackchan"; // whose display text (発話内容) the subtitle sink above shows // chunk by chunk, in step with the sound (or all at once if // nothing could be synthesised), so screen and voice always match. - (void)speech.babble(esp_random()); + if (config::ui_japanese()) { + (void)speech.babble(esp_random()); + } next_speech_ms = now_ms + rand_range_ms(kSpeechMinMs, kSpeechMaxMs); } } else { @@ -554,7 +558,7 @@ constexpr const char* kTag = "stackchan"; // subtitle sink; no phrase configured → short balloon only. if (speech.speak_proximity(esp_random()).empty()) { balloon_in_flight.store(true, std::memory_order_release); - g_state->set_balloon_text("なに?", /*hold_ms=*/1500, [] { + g_state->set_balloon_text(config::tr("Huh?", "なに?"), /*hold_ms=*/1500, [] { balloon_in_flight.store(false, std::memory_order_release); }); } @@ -643,7 +647,7 @@ constexpr const char* kTag = "stackchan"; g_state->face.expression.store(static_cast(avatar::Expression::Happy), std::memory_order_relaxed); balloon_in_flight.store(true, std::memory_order_release); - g_state->set_balloon_text("なでなで♡", /*hold_ms=*/2200, [] { + g_state->set_balloon_text(config::tr("Nuzzle ♡", "なでなで♡"), /*hold_ms=*/2200, [] { balloon_in_flight.store(false, std::memory_order_release); }); diff --git a/main/device_ui.cpp b/main/device_ui.cpp index 85e401b..7d8e22a 100644 --- a/main/device_ui.cpp +++ b/main/device_ui.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "servo_limits.hpp" #include "wifi_sta.hpp" @@ -53,7 +54,19 @@ enum Page : int { kLtTimer = 6, kTabCount }; -const char* const kTabLabels[kTabCount] = {"情報", "設定1", "設定2", "操作", "範囲", "会話", "LT"}; +const char* tab_label(int i) +{ + switch (i) { + case kInfo: return config::tr("Info", "情報"); + case kSettings: return config::tr("Set 1", "設定1"); + case kSettings2: return config::tr("Set 2", "設定2"); + case kControl: return config::tr("Ctrl", "操作"); + case kRange: return config::tr("Range", "範囲"); + case kConversation: return config::tr("Chat", "会話"); + case kLtTimer: return "LT"; + } + return "?"; +} int num_tab_pages() { return (kTabCount + kTabsPerPage - 1) / kTabsPerPage; } const auto* const kFontTitle = &fonts::lgfxJapanGothic_24; @@ -285,7 +298,7 @@ void draw_topbar(int page) const TabSlot& s = b.slots[i]; if (s.index == page) g_cv->fillRect(s.x, 0, s.w, kBarH, sel); g_cv->setTextColor(fg); - g_cv->drawString(kTabLabels[s.index], s.x + s.w / 2, kBarH / 2); + g_cv->drawString(tab_label(s.index), s.x + s.w / 2, kBarH / 2); } // Close button. g_cv->fillRect(b.close_x, 0, kCloseW, kBarH, g_cv->color565(120, 60, 60)); @@ -349,15 +362,15 @@ void draw_info() // squeezing every pixel. const int dy = 18; draw_kv(y, "FW", app ? app->version : "?", fg); y += dy; - draw_kv(y, "SSID", g_ssid.empty() ? "(未設定)" : g_ssid.c_str(), fg); y += dy; + draw_kv(y, "SSID", g_ssid.empty() ? config::tr("(unset)", "(未設定)") : g_ssid.c_str(), fg); y += dy; draw_kv(y, "mDNS", g_host.c_str(), fg); y += dy; draw_kv(y, "IP", ip.c_str(), wifi ? fg : off); y += dy; - draw_kv(y, "Wi-Fi", wifi ? "接続中" : "未接続", wifi ? ok : off); y += dy; - draw_kv(y, "BLE", ble ? "接続中" : "待受中", ble ? ok : fg); y += dy; - draw_kv(y, "稼働", uptime, fg); y += dy; - draw_kv(y, "空きRAM", heap, fg); y += dy; - draw_kv(y, "電池", battery, bat_color); y += dy; - draw_kv(y, "MCP", g_has_mcp_token ? "設定済" : "未設定", + draw_kv(y, "Wi-Fi", wifi ? config::tr("up", "接続中") : config::tr("down", "未接続"), wifi ? ok : off); y += dy; + draw_kv(y, "BLE", ble ? config::tr("connected", "接続中") : config::tr("idle", "待受中"), ble ? ok : fg); y += dy; + draw_kv(y, config::tr("Up", "稼働"), uptime, fg); y += dy; + draw_kv(y, config::tr("RAM", "空きRAM"), heap, fg); y += dy; + draw_kv(y, config::tr("Batt", "電池"), battery, bat_color); y += dy; + draw_kv(y, "MCP", g_has_mcp_token ? config::tr("set", "設定済") : config::tr("unset", "未設定"), g_has_mcp_token ? ok : dim); y += dy; } @@ -385,8 +398,8 @@ const char* op_mode_label(std::uint8_t m) const char* audio_output_label(std::uint8_t m) { switch (m) { - case 0: return "自動"; - case 1: return "内蔵スピーカ"; + case 0: return config::tr("Auto", "自動"); + case 1: return config::tr("Internal speaker", "内蔵スピーカ"); case 2: return "Module Audio"; } return "?"; @@ -431,40 +444,40 @@ void draw_button(int i, const char* label, std::uint16_t color, int row_h = kRow // 設定2 — サーボ恒久 / 起動アルペジオ / 適用 (4 rows w/ caption) void draw_settings() { - draw_value_row(0, "動作モード", + draw_value_row(0, config::tr("Mode", "動作モード"), op_mode_label(g_stage_op_mode.load(std::memory_order_relaxed)), kSettingsRowH); - draw_value_row(1, "音声出力", + draw_value_row(1, config::tr("Audio out", "音声出力"), audio_output_label(g_stage_audio_output.load(std::memory_order_relaxed)), kSettingsRowH); - draw_toggle_row(2, "RTP 音声受信", g_stage_rtp.load(std::memory_order_relaxed), kSettingsRowH); - draw_toggle_row(3, "電池ゲージ", g_stage_bat_gauge.load(std::memory_order_relaxed), kSettingsRowH); + draw_toggle_row(2, config::tr("RTP audio", "RTP 音声受信"), g_stage_rtp.load(std::memory_order_relaxed), kSettingsRowH); + draw_toggle_row(3, config::tr("Batt gauge", "電池ゲージ"), g_stage_bat_gauge.load(std::memory_order_relaxed), kSettingsRowH); g_cv->setFont(kFontBody); g_cv->setTextDatum(lgfx::textdatum_t::top_left); g_cv->setTextColor(g_cv->color565(150, 150, 150)); - g_cv->drawString("→ スワイプで 設定2 / 操作 へ", + g_cv->drawString(config::tr("-> swipe to Set 2 / Ctrl", "→ スワイプで 設定2 / 操作 へ"), 12, kContentY + 4 * kSettingsRowH + 4); } void draw_settings2() { - draw_toggle_row(0, "サーボ (恒久)", g_stage_servo_master.load(std::memory_order_relaxed), + draw_toggle_row(0, config::tr("Servo (persist)", "サーボ (恒久)"), g_stage_servo_master.load(std::memory_order_relaxed), kSettingsRowH); - draw_toggle_row(1, "起動アルペジオ", g_stage_boot_arp.load(std::memory_order_relaxed), + draw_toggle_row(1, config::tr("Boot arpeggio", "起動アルペジオ"), g_stage_boot_arp.load(std::memory_order_relaxed), kSettingsRowH); - draw_button(2, "適用(保存して再起動)", g_cv->color565(60, 120, 200), kSettingsRowH); + draw_button(2, config::tr("Apply (save + reboot)", "適用(保存して再起動)"), g_cv->color565(60, 120, 200), kSettingsRowH); g_cv->setFont(kFontBody); g_cv->setTextDatum(lgfx::textdatum_t::top_left); g_cv->setTextColor(g_cv->color565(150, 150, 150)); - g_cv->drawString("設定1+2 の変更を「適用」で保存・再起動", + g_cv->drawString(config::tr("Set 1+2 changes apply on reboot", "設定1+2 の変更を「適用」で保存・再起動"), 12, kContentY + 3 * kSettingsRowH + 4); } void draw_control() { const bool servo_on = g_state->servo.enabled.load(std::memory_order_relaxed); - draw_toggle_row(0, "サーボ(脱力/復帰)", servo_on); - draw_button(1, "姿勢をリセット", g_cv->color565(60, 120, 200)); + draw_toggle_row(0, config::tr("Servo (hold/free)", "サーボ(脱力/復帰)"), servo_on); + draw_button(1, config::tr("Reset pose", "姿勢をリセット"), g_cv->color565(60, 120, 200)); // Speaker volume row: 3 hit zones — left third = -10 %, right third = // +10 %, center shows the current value and toggles mute. Live (no @@ -476,7 +489,7 @@ void draw_control() g_cv->setFont(kFontBody); g_cv->setTextDatum(lgfx::textdatum_t::middle_left); g_cv->setTextColor(g_cv->color565(245, 245, 245)); - g_cv->drawString("音量", rx + 12, ry + rh / 2); + g_cv->drawString(config::tr("Vol", "音量"), rx + 12, ry + rh / 2); // − / + visual hints on the row edges so the user knows they can tap // there to nudge the value. g_cv->setTextDatum(lgfx::textdatum_t::middle_center); @@ -489,7 +502,7 @@ void draw_control() // it back (same zone as the mute tap). if (g_state->speaker.muted.load(std::memory_order_relaxed)) { g_cv->setTextColor(g_cv->color565(230, 110, 110)); - g_cv->drawString("ミュート中", rx + rw / 2, ry + rh / 2); + g_cv->drawString(config::tr("MUTED", "ミュート中"), rx + rw / 2, ry + rh / 2); } else { char buf[16]; std::snprintf(buf, sizeof(buf), "%u %%", static_cast(pct)); @@ -503,7 +516,8 @@ void draw_control() // reflects the live state so the same row dismisses it. const bool ap_on = wifi_ap_active(); draw_button(3, - ap_on ? "AP モード(停止)" : "AP モード(Wi-Fi 再設定)", + ap_on ? config::tr("Stop AP mode", "AP モード(停止)") + : config::tr("AP mode (Wi-Fi setup)", "AP モード(Wi-Fi 再設定)"), ap_on ? g_cv->color565(200, 90, 60) : g_cv->color565(60, 120, 200)); } @@ -603,7 +617,8 @@ void draw_range() draw_range_btn(x0 + bw + gap, L.y_pitch_btns, bw, kRangeBtnRowH - 4, "P-", min_c); draw_range_btn(x0 + 2 * (bw + gap), L.y_pitch_btns, bw, kRangeBtnRowH - 4, "P+", max_c); - draw_range_btn(12, L.y_save, kW - 24, kRangeSaveH, "保存(再起動)", + draw_range_btn(12, L.y_save, kW - 24, kRangeSaveH, + config::tr("Save (reboot)", "保存(再起動)"), g_cv->color565(60, 140, 80)); } @@ -619,31 +634,33 @@ void draw_conversation() const char* status_text = "?"; std::uint16_t status_color = dim; switch (st) { - case ConvStatus::Disabled: status_text = "無効"; status_color = dim; break; - case ConvStatus::WaitingWifi: status_text = "Wi-Fi 待ち"; status_color = warn; break; - case ConvStatus::Connecting: status_text = "接続中…"; status_color = warn; break; - case ConvStatus::Listening: status_text = "接続(待受)"; status_color = ok; break; - case ConvStatus::Talking: status_text = "通話中"; status_color = ok; break; - case ConvStatus::Yielded: status_text = "音声再生中"; status_color = dim; break; - case ConvStatus::Reconnecting: status_text = "再接続中…"; status_color = warn; break; - case ConvStatus::Error: status_text = "接続エラー"; status_color = err; break; - case ConvStatus::UserSpeaking: status_text = "聞き取り中"; status_color = ok; break; - case ConvStatus::Thinking: status_text = "応答待ち"; status_color = ok; break; - case ConvStatus::ToolRunning: status_text = "ツール実行中"; status_color = ok; break; - case ConvStatus::AgentWaiting: status_text = "エージェント待ち"; status_color = ok; break; + case ConvStatus::Disabled: status_text = config::tr("Off", "無効"); status_color = dim; break; + case ConvStatus::WaitingWifi: status_text = config::tr("Waiting for Wi-Fi", "Wi-Fi 待ち"); status_color = warn; break; + case ConvStatus::Connecting: status_text = config::tr("Connecting…", "接続中…"); status_color = warn; break; + case ConvStatus::Listening: status_text = config::tr("Ready", "接続(待受)"); status_color = ok; break; + case ConvStatus::Talking: status_text = config::tr("Speaking", "通話中"); status_color = ok; break; + case ConvStatus::Yielded: status_text = config::tr("Audio out", "音声再生中"); status_color = dim; break; + case ConvStatus::Reconnecting: status_text = config::tr("Reconnecting…", "再接続中…"); status_color = warn; break; + case ConvStatus::Error: status_text = config::tr("Error", "接続エラー"); status_color = err; break; + case ConvStatus::UserSpeaking: status_text = config::tr("Listening", "聞き取り中"); status_color = ok; break; + case ConvStatus::Thinking: status_text = config::tr("Thinking", "応答待ち"); status_color = ok; break; + case ConvStatus::ToolRunning: status_text = config::tr("Running tool", "ツール実行中"); status_color = ok; break; + case ConvStatus::AgentWaiting: status_text = config::tr("Waiting on agent", "エージェント待ち"); status_color = ok; break; } const std::uint32_t reconnects = g_state->conv.reconnects.load(std::memory_order_relaxed); char rc[16]; - std::snprintf(rc, sizeof(rc), "%u 回", static_cast(reconnects)); + std::snprintf(rc, sizeof(rc), config::ui_japanese() ? "%u 回" : "%u", + static_cast(reconnects)); int y = kContentY; const int dy = 26; const char* provider_name = g_provider == 1 ? "Gemini Live" : g_provider == 2 ? "XiaoZhi" : "OpenAI Realtime"; - draw_kv(y, "サービス", provider_name, fg); y += dy; - draw_kv(y, "状態", status_text, status_color); y += dy; - draw_kv(y, "再接続", rc, reconnects > 0 ? warn : fg); y += dy; + if (config::gemini_only()) provider_name = "Gemini Live"; + draw_kv(y, config::tr("Service", "サービス"), provider_name, fg); y += dy; + draw_kv(y, config::tr("Status", "状態"), status_text, status_color); y += dy; + draw_kv(y, config::tr("Retries", "再接続"), rc, reconnects > 0 ? warn : fg); y += dy; } // --- LT (lightning talk) timekeeper page ---------------------------------- @@ -654,7 +671,8 @@ void draw_conversation() // a talk can't change the deadline. constexpr std::uint16_t kLtPresetsS[] = {180, 300, 600}; -constexpr const char* kLtPresetLabels[] = {"3分", "5分", "10分"}; +constexpr const char* kLtPresetLabelsJa[] = {"3分", "5分", "10分"}; +constexpr const char* kLtPresetLabelsEn[] = {"3 min", "5 min", "10 min"}; constexpr int kLtPresetY = 150; constexpr int kLtPresetH = 36; constexpr int kLtStartY = 196; @@ -673,7 +691,9 @@ void draw_lt_timer() const std::uint16_t err = g_cv->color565(235, 100, 100); // Status line. - const char* status = !active ? "待機中" : (remaining < 0 ? "時間超過!" : "計測中"); + const char* status = !active ? config::tr("Idle", "待機中") + : (remaining < 0 ? config::tr("Overtime!", "時間超過!") + : config::tr("Running", "計測中")); const std::uint16_t status_color = !active ? dim : (remaining < 0 ? err : ok); g_cv->setFont(kFontBody); g_cv->setTextDatum(lgfx::textdatum_t::top_center); @@ -708,7 +728,8 @@ void draw_lt_timer() : g_cv->color565(50, 56, 66)); g_cv->fillRoundRect(bx, kLtPresetY, bw, kLtPresetH, 6, bg); g_cv->setTextColor(active ? dim : fg); - g_cv->drawString(kLtPresetLabels[i], bx + bw / 2, kLtPresetY + kLtPresetH / 2); + g_cv->drawString((config::ui_japanese() ? kLtPresetLabelsJa : kLtPresetLabelsEn)[i], + bx + bw / 2, kLtPresetY + kLtPresetH / 2); } // Start / stop. @@ -716,7 +737,8 @@ void draw_lt_timer() : g_cv->color565(60, 160, 90); g_cv->fillRoundRect(12, kLtStartY, kW - 24, kLtStartH, 6, btn_color); g_cv->setTextColor(g_cv->color565(245, 245, 245)); - g_cv->drawString(active ? "ストップ" : "スタート", kW / 2, kLtStartY + kLtStartH / 2); + g_cv->drawString(active ? config::tr("Stop", "ストップ") : config::tr("Start", "スタート"), + kW / 2, kLtStartY + kLtStartH / 2); } void render_page() diff --git a/main/lt_timer.cpp b/main/lt_timer.cpp index 0be1e44..f27712a 100644 --- a/main/lt_timer.cpp +++ b/main/lt_timer.cpp @@ -6,6 +6,7 @@ #include #include +#include #include "shared_state.hpp" #include "speech.hpp" #include "utf8.hpp" @@ -19,6 +20,17 @@ constexpr std::uint32_t kBalloonHoldMs = 4000; void LtTimer::configure(const std::string& json, SharedState* state) { + if (!config::ui_japanese() && json.empty()) { + // English stock phrases when NVS has no custom LT config. Voice + // readings stay empty so announce() skips Japanese-only TTS. + cfg_.warn_display = "1 minute left"; + cfg_.warn_reading.clear(); + cfg_.soon_display = "15 seconds left"; + cfg_.soon_reading.clear(); + cfg_.over_display = "Time's up!"; + cfg_.over_reading.clear(); + return; + } if (json.empty()) return; cJSON* root = cJSON_Parse(json.c_str()); if (root == nullptr) { @@ -80,6 +92,11 @@ void LtTimer::announce(SharedState& state, Speech& speech, state.post_notice(display); return; } + // Local Japanese formant TTS cannot speak English. Balloon-only in + // English mode, or when the reading was left empty. + if (!config::ui_japanese() || reading.empty()) { + return; + } if (!speech.say(decode_utf8(reading))) { ESP_LOGW(kTag, "announcement synthesis failed: %s", reading.c_str()); } diff --git a/main/render_task.cpp b/main/render_task.cpp index 16428b9..fd52dd8 100644 --- a/main/render_task.cpp +++ b/main/render_task.cpp @@ -3,6 +3,7 @@ #include #include "render_task.hpp" +#include #include #include @@ -106,17 +107,17 @@ void draw_conv_status(RichCanvas& canvas, ConvStatus st, int canvas_w, int right const std::uint16_t err = canvas.color565(230, 110, 110); switch (st) { case ConvStatus::Disabled: return; - case ConvStatus::WaitingWifi: text = "Wi-Fi待ち"; dot = busy; break; - case ConvStatus::Connecting: text = "接続中"; dot = busy; break; - case ConvStatus::Listening: text = "待受中"; dot = ok; break; - case ConvStatus::UserSpeaking: text = "聞き取り中"; dot = live; break; - case ConvStatus::Thinking: text = "応答待ち"; dot = busy; break; - case ConvStatus::ToolRunning: text = "ツール実行中"; dot = busy; break; - case ConvStatus::AgentWaiting: text = "エージェント待ち"; dot = busy; break; - case ConvStatus::Talking: text = "応答中"; dot = live; break; - case ConvStatus::Yielded: text = "音声再生中"; dot = canvas.color565(150, 150, 150); break; - case ConvStatus::Reconnecting: text = "再接続中"; dot = busy; break; - case ConvStatus::Error: text = "接続エラー"; dot = err; break; + case ConvStatus::WaitingWifi: text = config::tr("Wi-Fi…", "Wi-Fi待ち"); dot = busy; break; + case ConvStatus::Connecting: text = config::tr("Connecting", "接続中"); dot = busy; break; + case ConvStatus::Listening: text = config::tr("Ready", "待受中"); dot = ok; break; + case ConvStatus::UserSpeaking: text = config::tr("Listening", "聞き取り中"); dot = live; break; + case ConvStatus::Thinking: text = config::tr("Thinking", "応答待ち"); dot = busy; break; + case ConvStatus::ToolRunning: text = config::tr("Tool", "ツール実行中"); dot = busy; break; + case ConvStatus::AgentWaiting: text = config::tr("Agent…", "エージェント待ち"); dot = busy; break; + case ConvStatus::Talking: text = config::tr("Speaking", "応答中"); dot = live; break; + case ConvStatus::Yielded: text = config::tr("Audio", "音声再生中"); dot = canvas.color565(150, 150, 150); break; + case ConvStatus::Reconnecting: text = config::tr("Reconnect", "再接続中"); dot = busy; break; + case ConvStatus::Error: text = config::tr("Error", "接続エラー"); dot = err; break; } if (text == nullptr) return; diff --git a/recovery/main/http.cpp b/recovery/main/http.cpp index 1302ceb..030e549 100644 --- a/recovery/main/http.cpp +++ b/recovery/main/http.cpp @@ -175,6 +175,7 @@ esp_err_t handle_ota_release_post(httpd_req_t* req) case StartError::BadTag: msg = "bad tag"; break; case StartError::UnknownBoard: msg = "unknown board"; break; case StartError::WorkerSpawnFailed: msg = "spawn failed"; break; + case StartError::NoCatalog: msg = "update catalog unset"; break; } return send_error(req, "400 Bad Request", msg); } diff --git a/recovery/sdkconfig.defaults b/recovery/sdkconfig.defaults index 02c0c50..319e6e0 100644 --- a/recovery/sdkconfig.defaults +++ b/recovery/sdkconfig.defaults @@ -65,6 +65,10 @@ CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN=y CONFIG_MBEDTLS_HKDF_C=y CONFIG_MBEDTLS_HARDWARE_AES=y +# Same catalog the main firmware uses so Recovery cannot fetch upstream +# ciniml images onto a Gemini customer device. +CONFIG_STACKCHAN_UPDATE_BASE_URL="https://m5stack.github.io/stackchan-idf-gemini" + # --- HTTP server ------------------------------------------------------------- # Chrome のヘッダは 1 KiB を超える (Main と同じ)。 CONFIG_HTTPD_MAX_REQ_HDR_LEN=4096 diff --git a/sdkconfig.defaults b/sdkconfig.defaults index 3f57dbe..7e480f7 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -243,3 +243,17 @@ CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y CONFIG_ESP_COREDUMP_CHECKSUM_CRC32=y CONFIG_ESP_COREDUMP_CHECK_BOOT=n CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=40 + +# --- Gemini customer build (Google) --------------------------------------- +# Additive product profile: lock the runtime provider to Gemini Live, show +# a Google Cloud boot splash, and point in-app OTA / voice-fetch at this +# fork's GitHub Pages instead of upstream ciniml/stackchan-idf. Kconfig +# defaults stay off / upstream so a tree without these lines (or with +# them commented) builds the original multi-provider firmware. +CONFIG_STACKCHAN_GEMINI_ONLY=y +CONFIG_STACKCHAN_SETTINGS_ENGLISH=y +CONFIG_STACKCHAN_BOOT_SPLASH=y +CONFIG_STACKCHAN_BOOT_SPLASH_MS=2000 +CONFIG_STACKCHAN_GEMINI_DEFAULT_MODEL="gemini-3.8-live" +CONFIG_STACKCHAN_GEMINI_DEFAULT_VOICE="Aoede" +CONFIG_STACKCHAN_UPDATE_BASE_URL="https://m5stack.github.io/stackchan-idf-gemini" diff --git a/tools/avatar_dsl/inject.mjs b/tools/avatar_dsl/inject.mjs index 1ee7cdb..8b22264 100644 --- a/tools/avatar_dsl/inject.mjs +++ b/tools/avatar_dsl/inject.mjs @@ -90,6 +90,12 @@ html = html.replace(/\/\*\{\{SETTINGS_COMMON\}\}\*\//g, () => { return readFileSync(resolve(SCRIPT_DIR, '..', 'settings_common.js'), 'utf8'); }); +let geminiHits = 0; +html = html.replace(/\/\*\{\{GEMINI_UI\}\}\*\//g, () => { + ++geminiHits; + return readFileSync(resolve(SCRIPT_DIR, '..', 'gemini_ui.js'), 'utf8'); +}); + if (bundleHits === 0) { process.stderr.write(`warning: ${templatePath}: no '/*{{AVATAR_DSL_BUNDLE}}*/' placeholder found\n`); } @@ -102,4 +108,5 @@ process.stdout.write( `[avatar_dsl] injected bundle (${bundle.length}B) + ${presets.length} preset(s) ` + `(${presets.map((p) => p.name).join(', ')})` + (commonHits > 0 ? ' + settings_common.js' : '') + + (geminiHits > 0 ? ' + gemini_ui.js' : '') + ` into ${templatePath} -> ${outPath}\n`); diff --git a/tools/gemini_ui.js b/tools/gemini_ui.js new file mode 100644 index 0000000..f4e8628 --- /dev/null +++ b/tools/gemini_ui.js @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2026 Kenta IDA +// SPDX-License-Identifier: BSL-1.0 +// +// Gemini customer-build behaviour for the BLE / Wi-Fi settings pages. +// Loaded as a sibling on Pages (settings.html) and inlined into the +// firmware's settings_wifi.html. The Gemini-only build translates the full +// page at build time; this overlay only adds product fields and hides +// non-Gemini providers after the device reports gemini_only. Keeping the +// translation out of runtime DOM traversal prevents late or repeated +// rewrites from missing dynamically generated strings. + +(function () { + 'use strict'; + + const MODELS = [ + { id: 'gemini-3.8-live', label: 'gemini-3.8-live (recommended, GA)' }, + { id: 'gemini-2.5-flash-native-audio-preview-12-2025', + label: 'gemini-2.5-flash-native-audio-preview-12-2025 (legacy; shutdown 2026-11-17)' }, + ]; + const VOICES = ['Aoede', 'Puck', 'Charon', 'Kore', 'Fenrir', 'Leda', 'Orus', 'Zephyr']; + + // Translation is a build-time transform now. Keep this compatibility entry + // point for settings.html, which calls it after a Gemini-only device is + // identified, but never rewrite dynamic text from a partial runtime table. + function tr(value) { + return value == null ? value : String(value); + } + + function walk(node, fn) { + if (!node) return; + fn(node); + for (const c of Array.from(node.childNodes || [])) walk(c, fn); + } + + function translateTree(root) { + walk(root || document.body, (n) => { + if (n.nodeType === 3) { + const next = tr(n.nodeValue); + if (next !== n.nodeValue) n.nodeValue = next; + } else if (n.nodeType === 1) { + for (const attr of ['placeholder', 'title', 'aria-label']) { + if (n.hasAttribute && n.hasAttribute(attr)) { + const v = n.getAttribute(attr); + const nv = tr(v); + if (nv !== v) n.setAttribute(attr, nv); + } + } + } + }); + document.documentElement.lang = 'en'; + } + + function setVisible(selector, visible) { + document.querySelectorAll(selector).forEach((el) => { + el.style.display = visible ? '' : 'none'; + }); + } + + function hideNonGemini() { + setVisible('label[for="provider"], #provider', false); + setVisible('#provider + p.hint', false); + setVisible('label[for="apikey"], #apikey', false); + setVisible('label[for="xiaozhi-url"], #xiaozhi-url', false); + setVisible('label[for="xiaozhi-token"], #xiaozhi-token', false); + const sel = document.getElementById('provider'); + if (sel) { + sel.value = '1'; + for (const opt of sel.options) { + opt.hidden = opt.value !== '1'; + } + } + const fk = document.getElementById('flag-key'); + const fe = document.getElementById('flag-en'); + if (fk) fk.style.display = 'none'; + if (fe) fe.style.display = 'none'; + const title = document.querySelector('#chat-section h2.section-title'); + if (title) title.textContent = 'Gemini Live'; + } + + function ensureSelect(el, items, current) { + if (!el) return; + const wanted = current || el.value || ''; + const have = new Set(Array.from(el.options).map((o) => o.value)); + for (const it of items) { + const id = typeof it === 'string' ? it : it.id; + const label = typeof it === 'string' ? it : it.label; + if (!have.has(id)) { + const opt = document.createElement('option'); + opt.value = id; + opt.textContent = label; + el.appendChild(opt); + have.add(id); + } + } + if (wanted && !have.has(wanted)) { + const opt = document.createElement('option'); + opt.value = wanted; + opt.textContent = wanted; + el.appendChild(opt); + } + if (wanted) el.value = wanted; + } + + function ensureFields() { + const key = document.getElementById('gemini-apikey'); + if (!key) return; + if (!document.getElementById('gemini-model')) { + const lab = document.createElement('label'); + lab.htmlFor = 'gemini-model'; + lab.textContent = 'Gemini model'; + const sel = document.createElement('select'); + sel.id = 'gemini-model'; + const hint = document.createElement('p'); + hint.className = 'hint'; + hint.textContent = + 'Native-audio Live model. gemini-3.8-live is the current GA default; the 2.5 preview shuts down 2026-11-17.'; + key.insertAdjacentElement('afterend', lab); + lab.insertAdjacentElement('afterend', sel); + sel.insertAdjacentElement('afterend', hint); + } + if (!document.getElementById('gemini-voice')) { + const after = document.getElementById('gemini-model') || key; + const lab = document.createElement('label'); + lab.htmlFor = 'gemini-voice'; + lab.textContent = 'Gemini voice'; + const sel = document.createElement('select'); + sel.id = 'gemini-voice'; + const hint = document.createElement('p'); + hint.className = 'hint'; + hint.textContent = 'Prebuilt Gemini Live voice. Default is Aoede.'; + const anchor = after.nextElementSibling && after.nextElementSibling.classList && + after.nextElementSibling.classList.contains('hint') + ? after.nextElementSibling + : after; + anchor.insertAdjacentElement('afterend', lab); + lab.insertAdjacentElement('afterend', sel); + sel.insertAdjacentElement('afterend', hint); + } + ensureSelect(document.getElementById('gemini-model'), MODELS); + ensureSelect(document.getElementById('gemini-voice'), VOICES, 'Aoede'); + } + + function hideReleaseIfUnset(updateCatalog) { + const btn = document.getElementById('btn-ota-release'); + const sel = document.getElementById('ota-release-select'); + const ch = document.getElementById('ota-release-channel'); + const tag = document.getElementById('ota-release-tag'); + if (updateCatalog === false) { + [btn, sel, ch, tag].forEach((el) => { + if (!el) return; + el.disabled = true; + }); + if (btn) btn.title = 'In-app update catalog is not configured for this build'; + } + } + + function rewritePagesLinks() { + document.querySelectorAll('a[href*="ciniml.github.io/stackchan-idf"]').forEach((a) => { + a.href = a.href.replace('ciniml.github.io/stackchan-idf', 'm5stack.github.io/stackchan-idf-gemini'); + if (a.textContent && a.textContent.indexOf('ciniml.github.io') >= 0) { + a.textContent = a.textContent.replace('ciniml.github.io/stackchan-idf', 'm5stack.github.io/stackchan-idf-gemini'); + } + }); + document.querySelectorAll('a[href*="github.com/ciniml/stackchan-idf"]').forEach((a) => { + a.href = a.href.replace('github.com/ciniml/stackchan-idf', 'github.com/m5stack/stackchan-idf-gemini'); + }); + } + + function applyGeminiOnlyUi(opts) { + const o = opts || {}; + const hide = o.gemini_only !== false; // product default: hide + ensureFields(); + if (hide) hideNonGemini(); + if (o.gemini_model) ensureSelect(document.getElementById('gemini-model'), MODELS, o.gemini_model); + if (o.gemini_voice) ensureSelect(document.getElementById('gemini-voice'), VOICES, o.gemini_voice); + if ('update_catalog' in o) hideReleaseIfUnset(!!o.update_catalog); + translateTree(document.body); + rewritePagesLinks(); + } + + async function afterWifiRefresh() { + try { + const r = await fetch('/api/status', { cache: 'no-store' }); + if (!r.ok) return; + const s = await r.json(); + if (!s.gemini_only) return; + applyGeminiOnlyUi({ + gemini_only: !!s.gemini_only, + gemini_model: s.gemini_model, + gemini_voice: s.gemini_voice, + update_catalog: s.update_catalog, + }); + } catch (_) { /* Keep the translated page usable if status is unavailable. */ } + } + + function wrapWifi() { + if (typeof refresh === 'function') { + const orig = refresh; + window.refresh = async function () { + await orig.apply(this, arguments); + await afterWifiRefresh(); + }; + } + if (typeof apply === 'function') { + const orig = apply; + window.apply = async function () { + const model = document.getElementById('gemini-model'); + const voice = document.getElementById('gemini-voice'); + try { + if (model && model.value && typeof postText === 'function') { + await postText('/api/gemini-model', model.value); + } + if (voice && voice.value && typeof postText === 'function') { + await postText('/api/gemini-voice', voice.value); + } + } catch (e) { + if (typeof log === 'function') log('Gemini model/voice: ' + e.message, 'err'); + } + return orig.apply(this, arguments); + }; + } + if (typeof log === 'function') { + const origLog = log; + window.log = function (msg, cls) { + return origLog(tr(String(msg)), cls); + }; + } + } + + const api = { + applyGeminiOnlyUi, + afterWifiRefresh, + hideNonGemini, + ensureFields, + translateTree, + tr, + MODELS, + VOICES, + CHR_GEMINI_MODEL: 'e3f0a032-7b1c-4d2a-9e6f-2c5a8d4b1f00', + CHR_GEMINI_VOICE: 'e3f0a033-7b1c-4d2a-9e6f-2c5a8d4b1f00', + }; + window.StackchanGeminiUi = api; + + function boot() { + wrapWifi(); + // The device status decides whether this is the Gemini-only product. + // Do not hide providers or inject product fields for upstream firmware. + if (typeof fetch === 'function' && location.protocol !== 'file:') { + afterWifiRefresh(); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', boot); + } else { + boot(); + } +})(); diff --git a/tools/gen_google_cloud_splash.py b/tools/gen_google_cloud_splash.py new file mode 100644 index 0000000..1375c41 --- /dev/null +++ b/tools/gen_google_cloud_splash.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Kenta IDA +# SPDX-License-Identifier: BSL-1.0 +"""Generate per-board Google Cloud boot-splash PNGs from Wikimedia artwork. + +Source (CC BY 4.0): + https://commons.wikimedia.org/wiki/File:Google_Cloud_logo.svg + https://upload.wikimedia.org/wikipedia/commons/5/51/Google_Cloud_logo.svg + +Renders a white canvas at each board's native resolution with the logo +centred. StopWatch (466x466 round) keeps the artwork inside the inscribed +circle so the square panel corners don't clip it. +""" +from __future__ import annotations + +import io +import pathlib +import sys +import urllib.request + +REPO = pathlib.Path(__file__).resolve().parent.parent +OUT = REPO / "assets" / "splash" +WIKI = "https://commons.wikimedia.org/wiki/File:Google_Cloud_logo.svg" +SVG_URL = "https://upload.wikimedia.org/wikipedia/commons/5/51/Google_Cloud_logo.svg" +PNG_FALLBACK = ( + "https://upload.wikimedia.org/wikipedia/commons/thumb/5/51/" + "Google_Cloud_logo.svg/1280px-Google_Cloud_logo.svg.png" +) + +# Board canvases: (filename, width, height, max_logo_w, max_logo_h) +# StopWatch inscribed-square side is 466/sqrt(2) ≈ 329; keep a margin. +TARGETS = [ + ("splash_cores3.png", 320, 240, 280, 120), + ("splash_atom.png", 128, 128, 112, 56), + ("splash_stopwatch.png", 466, 466, 300, 140), +] + + +def fetch(url: str) -> bytes: + req = urllib.request.Request(url, headers={"User-Agent": "stackchan-idf-gemini-splash/1.0"}) + with urllib.request.urlopen(req, timeout=60) as r: + return r.read() + + +def load_logo(): + from PIL import Image + + # Prefer a rasterized PNG (no cairo required). Fall back to the SVG only + # if a renderer is present. + try: + data = fetch(PNG_FALLBACK) + im = Image.open(io.BytesIO(data)).convert("RGBA") + print(f"fetched PNG fallback {im.size} from {PNG_FALLBACK}", file=sys.stderr) + return im + except Exception as e: + print(f"PNG fallback failed: {e}", file=sys.stderr) + + svg = fetch(SVG_URL) + print(f"fetched SVG {len(svg)} B from {SVG_URL}", file=sys.stderr) + try: + import cairosvg # type: ignore + + png = cairosvg.svg2png(bytestring=svg, output_width=1280) + return Image.open(io.BytesIO(png)).convert("RGBA") + except Exception as e: + print(f"cairosvg unavailable ({e}); cannot rasterize SVG", file=sys.stderr) + raise SystemExit(1) from e + + +def fit(im, max_w: int, max_h: int): + from PIL import Image + + w, h = im.size + scale = min(max_w / w, max_h / h) + nw, nh = max(1, int(w * scale)), max(1, int(h * scale)) + return im.resize((nw, nh), Image.Resampling.LANCZOS) + + +def main() -> int: + from PIL import Image + + OUT.mkdir(parents=True, exist_ok=True) + logo = load_logo() + # Drop near-white background so compositing onto our white canvas is clean. + # The Wikimedia PNG is already transparent around the wordmark. + + source_note = OUT / "SOURCE.txt" + source_note.write_text( + "Google Cloud logo (full color wordmark)\n" + f"Page: {WIKI}\n" + f"File: {SVG_URL}\n" + f"Raster used: {PNG_FALLBACK}\n" + "License: Creative Commons Attribution 4.0 International (CC BY 4.0)\n" + "Customer (Google) approved use of this trademark artwork in firmware.\n", + encoding="utf-8", + ) + + for name, cw, ch, max_w, max_h in TARGETS: + canvas = Image.new("RGBA", (cw, ch), (255, 255, 255, 255)) + mark = fit(logo, max_w, max_h) + x = (cw - mark.size[0]) // 2 + y = (ch - mark.size[1]) // 2 + canvas.alpha_composite(mark, (x, y)) + out = canvas.convert("RGB") + path = OUT / name + out.save(path, format="PNG", optimize=True) + print(f"wrote {path} {out.size} {path.stat().st_size} B", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + try: + raise SystemExit(main()) + except ImportError: + print("Pillow is required: pip install pillow", file=sys.stderr) + raise SystemExit(1) diff --git a/tools/settings.html b/tools/settings.html index 77fc604..4b69c0b 100644 --- a/tools/settings.html +++ b/tools/settings.html @@ -938,6 +938,25 @@

会話プロバイダ・API キー

+ + +

Native-audio Live model. gemini-3.8-live is current GA; the 2.5 preview shuts down 2026-11-17.

+ + +

Prebuilt Gemini Live voice. Default is Aoede.

+ @@ -1283,6 +1302,8 @@

ファームウェア更新 (OTA)

const CHR_OTA_DATA = 'e3f0a00a-7b1c-4d2a-9e6f-2c5a8d4b1f00'; const CHR_PROVIDER = 'e3f0a00b-7b1c-4d2a-9e6f-2c5a8d4b1f00'; const CHR_GEMINI_KEY = 'e3f0a00c-7b1c-4d2a-9e6f-2c5a8d4b1f00'; +const CHR_GEMINI_MODEL = 'e3f0a032-7b1c-4d2a-9e6f-2c5a8d4b1f00'; +const CHR_GEMINI_VOICE = 'e3f0a033-7b1c-4d2a-9e6f-2c5a8d4b1f00'; const CHR_XIAOZHI_URL = 'e3f0a013-7b1c-4d2a-9e6f-2c5a8d4b1f00'; const CHR_XIAOZHI_TOKEN = 'e3f0a014-7b1c-4d2a-9e6f-2c5a8d4b1f00'; // FaceConfig — encrypted compact JSON of the avatar face tuning. WRITE applies @@ -1323,7 +1344,7 @@

ファームウェア更新 (OTA)

// --- State --- let bleDevice = null, bleServer = null, svc = null; -let chrSsid = null, chrPass = null, chrKey = null, chrApply = null, chrStatus = null, chrKx = null, chrEn = null, chrRtpEn = null, chrJttsIdleEn = null, chrJtts = null, chrOtaCtrl = null, chrOtaData = null, chrProvider = null, chrGeminiKey = null, chrXiaozhiUrl = null, chrXiaozhiToken = null, chrFaceConfig = null, chrBattery = null, chrBatGauge = null, chrStartupArp = null, chrServoEnabled = null, chrMcpToken = null, chrLtConfig = null, chrServoLimits = null, chrServoRangeMode = null, chrServoPositions = null, chrBoardKind = null, chrWifiIp = null, chrWifiMac = null, chrAudioCtrl = null, chrAudioData = null, chrAudioCredit = null, chrAudioMetrics = null, chrLedState = null, chrMicLipGain = null, chrLedMouthSync = null, chrLipSyncMode = null, chrMicLipAgc = null, chrSpeakerVolume = null, chrJttsSay = null, chrSanoTts = null, chrOperationMode = null, chrAudioOutput = null, chrBargeIn = null, chrProximity = null, chrProximityTuning = null, chrDeviceName = null, chrAuthPassword = null, chrAvatarBc = null; +let chrSsid = null, chrPass = null, chrKey = null, chrApply = null, chrStatus = null, chrKx = null, chrEn = null, chrRtpEn = null, chrJttsIdleEn = null, chrJtts = null, chrOtaCtrl = null, chrOtaData = null, chrProvider = null, chrGeminiKey = null, chrGeminiModel = null, chrGeminiVoice = null, chrXiaozhiUrl = null, chrXiaozhiToken = null, chrFaceConfig = null, chrBattery = null, chrBatGauge = null, chrStartupArp = null, chrServoEnabled = null, chrMcpToken = null, chrLtConfig = null, chrServoLimits = null, chrServoRangeMode = null, chrServoPositions = null, chrBoardKind = null, chrWifiIp = null, chrWifiMac = null, chrAudioCtrl = null, chrAudioData = null, chrAudioCredit = null, chrAudioMetrics = null, chrLedState = null, chrMicLipGain = null, chrLedMouthSync = null, chrLipSyncMode = null, chrMicLipAgc = null, chrSpeakerVolume = null, chrJttsSay = null, chrSanoTts = null, chrOperationMode = null, chrAudioOutput = null, chrBargeIn = null, chrProximity = null, chrProximityTuning = null, chrDeviceName = null, chrAuthPassword = null, chrAvatarBc = null; let boardKind = null; // null = unknown (older firmware), else 0/1/2 from BOARD_* let batteryPollTimer = null; // setInterval handle while connected let jttsInitialJson = ''; // last read JSON, used to detect Apply-time changes @@ -1412,9 +1433,11 @@

ファームウェア更新 (OTA)

} let deviceWifiConnected = false; +let lastStatusFlags = 0; function updateStatus(data) { const flags = data.getUint8(0); const wifi = data.getUint8(1); + lastStatusFlags = flags; deviceWifiConnected = !!wifi; document.getElementById('flag-ssid').classList.toggle('set', !!(flags & 0x01)); document.getElementById('flag-pass').classList.toggle('set', !!(flags & 0x02)); @@ -1905,6 +1928,7 @@

ファームウェア更新 (OTA)

const hasOpenAiKey = !!chrKey; const hasGemini = !!(chrProvider && chrGeminiKey); const hasXiaozhi = !!(chrProvider && chrXiaozhiUrl && chrXiaozhiToken); + const geminiOnly = !!(typeof lastStatusFlags === 'number' && (lastStatusFlags & 0x40)); const hasEnable = !!chrEn; const hasRtpEnable = !!chrRtpEn; const hasBatGauge = !!chrBatGauge; @@ -1920,15 +1944,21 @@

ファームウェア更新 (OTA)

el.style.display = visible ? '' : 'none'; } }; - setVisible('label[for="provider"], #provider', hasGemini); - setVisible('#provider + p.hint', hasGemini); + setVisible('label[for="provider"], #provider', hasGemini && !geminiOnly); + setVisible('#provider + p.hint', hasGemini && !geminiOnly); setVisible('label[for="gemini-apikey"], #gemini-apikey', hasGemini); - setVisible('label[for="xiaozhi-url"], #xiaozhi-url', hasXiaozhi); - setVisible('label[for="xiaozhi-token"], #xiaozhi-token', hasXiaozhi); + setVisible('label[for="gemini-model"], #gemini-model', hasGemini); + setVisible('#gemini-model + p.hint', hasGemini); + setVisible('label[for="gemini-voice"], #gemini-voice', hasGemini); + setVisible('#gemini-voice + p.hint', hasGemini); + setVisible('label[for="xiaozhi-url"], #xiaozhi-url', hasXiaozhi && !geminiOnly); + setVisible('label[for="xiaozhi-token"], #xiaozhi-token', hasXiaozhi && !geminiOnly); // Hide the XiaoZhi provider choice entirely on firmware that lacks its chrs. const xzOption = document.querySelector('#provider option[value="2"]'); - if (xzOption) xzOption.hidden = !hasXiaozhi; - setVisible('label[for="apikey"], #apikey', hasOpenAiKey); + if (xzOption) xzOption.hidden = !hasXiaozhi || geminiOnly; + const oaOption = document.querySelector('#provider option[value="0"]'); + if (oaOption) oaOption.hidden = geminiOnly; + setVisible('label[for="apikey"], #apikey', hasOpenAiKey && !geminiOnly); setVisible('label.toggle-label[for="openai-enabled"]', hasEnable); setVisible('#openai-enabled + span', hasEnable); setVisible('label.toggle-label[for="rtp-enabled"]', hasRtpEnable); @@ -2013,6 +2043,11 @@

ファームウェア更新 (OTA)

if (headerInfo) headerInfo.hidden = false; applyFirmwareChannel(fwEl.textContent); } + if (geminiOnly && window.StackchanGeminiUi) { + StackchanGeminiUi.ensureFields(); + StackchanGeminiUi.hideNonGemini(); + StackchanGeminiUi.translateTree(document.body); + } } // --- ZIP extraction (deflate-raw via DecompressionStream, no deps) --- @@ -2777,6 +2812,8 @@

ファームウェア更新 (OTA)

['otaData', CHR_OTA_DATA], ['provider', CHR_PROVIDER], ['geminiKey', CHR_GEMINI_KEY], + ['geminiModel', CHR_GEMINI_MODEL], + ['geminiVoice', CHR_GEMINI_VOICE], ['xiaozhiUrl', CHR_XIAOZHI_URL], ['xiaozhiToken', CHR_XIAOZHI_TOKEN], ['faceConfig', CHR_FACE_CONFIG], @@ -2836,6 +2873,8 @@

ファームウェア更新 (OTA)

chrOtaData = found.otaData ?? null; chrProvider = found.provider ?? null; chrGeminiKey= found.geminiKey?? null; + chrGeminiModel = found.geminiModel ?? null; + chrGeminiVoice = found.geminiVoice ?? null; chrXiaozhiUrl = found.xiaozhiUrl ?? null; chrXiaozhiToken = found.xiaozhiToken ?? null; chrFaceConfig = found.faceConfig ?? null; @@ -3165,6 +3204,40 @@

ファームウェア更新 (OTA)

log(`プロバイダ取得失敗: ${e.message}`, 'err'); } } + if (chrGeminiModel) { + try { + const b = await readEncrypted(chrGeminiModel); + const v = dec.decode(b); + const sel = document.getElementById('gemini-model'); + if (sel && v) { + if (![...sel.options].some((o) => o.value === v)) { + const opt = document.createElement('option'); + opt.value = v; opt.textContent = v; sel.appendChild(opt); + } + sel.value = v; + } + log(`Gemini model: ${v || '(default)'}`); + } catch (e) { + log(`Gemini model read failed: ${e.message}`, 'err'); + } + } + if (chrGeminiVoice) { + try { + const b = await readEncrypted(chrGeminiVoice); + const v = dec.decode(b); + const sel = document.getElementById('gemini-voice'); + if (sel && v) { + if (![...sel.options].some((o) => o.value === v)) { + const opt = document.createElement('option'); + opt.value = v; opt.textContent = v; sel.appendChild(opt); + } + sel.value = v; + } + log(`Gemini voice: ${v || '(default)'}`); + } catch (e) { + log(`Gemini voice read failed: ${e.message}`, 'err'); + } + } if (chrJtts) { try { @@ -3260,7 +3333,7 @@

ファームウェア更新 (OTA)

ledStateLoaded = false; clearTimeout(micLipWriteTimer); micLipWriteTimer = null; micLipLoaded = false; - chrSsid = chrPass = chrKey = chrApply = chrStatus = chrKx = chrEn = chrRtpEn = chrJttsIdleEn = chrJtts = chrOtaCtrl = chrOtaData = chrProvider = chrGeminiKey = chrXiaozhiUrl = chrXiaozhiToken = chrFaceConfig = chrBattery = chrBatGauge = chrServoEnabled = chrMcpToken = chrLtConfig = chrServoLimits = chrServoRangeMode = chrServoPositions = chrBoardKind = chrWifiIp = chrWifiMac = chrAudioCtrl = chrAudioData = chrAudioCredit = chrAudioMetrics = chrLedState = chrMicLipGain = chrLedMouthSync = chrLipSyncMode = chrMicLipAgc = chrSpeakerVolume = chrJttsSay = chrOperationMode = chrAudioOutput = chrBargeIn = chrAvatarBc = null; + chrSsid = chrPass = chrKey = chrApply = chrStatus = chrKx = chrEn = chrRtpEn = chrJttsIdleEn = chrJtts = chrOtaCtrl = chrOtaData = chrProvider = chrGeminiKey = chrGeminiModel = chrGeminiVoice = chrXiaozhiUrl = chrXiaozhiToken = chrFaceConfig = chrBattery = chrBatGauge = chrServoEnabled = chrMcpToken = chrLtConfig = chrServoLimits = chrServoRangeMode = chrServoPositions = chrBoardKind = chrWifiIp = chrWifiMac = chrAudioCtrl = chrAudioData = chrAudioCredit = chrAudioMetrics = chrLedState = chrMicLipGain = chrLedMouthSync = chrLipSyncMode = chrMicLipAgc = chrSpeakerVolume = chrJttsSay = chrOperationMode = chrAudioOutput = chrBargeIn = chrAvatarBc = null; if (typeof refreshAvbcButton === 'function') refreshAvbcButton(); boardKind = null; aesKey = null; @@ -3336,6 +3409,16 @@

ファームウェア更新 (OTA)

await writeEncrypted(chrGeminiKey, strToBytes(gkey)); log('Gemini キー 書き込み完了', 'ok'); } + const gmodel = document.getElementById('gemini-model'); + if (chrGeminiModel && gmodel && gmodel.value) { + await writeEncrypted(chrGeminiModel, strToBytes(gmodel.value)); + log('Gemini model write complete', 'ok'); + } + const gvoice = document.getElementById('gemini-voice'); + if (chrGeminiVoice && gvoice && gvoice.value) { + await writeEncrypted(chrGeminiVoice, strToBytes(gvoice.value)); + log('Gemini voice write complete', 'ok'); + } if (xzUrl && chrXiaozhiUrl) { await writeEncrypted(chrXiaozhiUrl, strToBytes(xzUrl)); log('XiaoZhi URL 書き込み完了', 'ok'); @@ -4340,5 +4423,6 @@

ファームウェア更新 (OTA)

// no event listener or chr-probing code needed to change. StackchanSettings.setupTabs('stackchan.settings.ble.activeTab'); + diff --git a/tools/settings_i18n/catalog.json b/tools/settings_i18n/catalog.json new file mode 100644 index 0000000..c45125b --- /dev/null +++ b/tools/settings_i18n/catalog.json @@ -0,0 +1,1983 @@ +{ + "replacements": { + "診断: 書き込み ${diag.writes} 回 平均 ${(diag.writeMs / Math.max(1, diag.writes)).toFixed(1)} ms/回 (最大 ${diag.writeMaxMs.toFixed(0)} ms, 合計 ${(diag.writeMs / 1000).toFixed(1)} 秒), 状態読み取り ${diag.flushes} 回 平均 ${(diag.flushMs / Math.max(1, diag.flushes)).toFixed(1)} ms/回 (合計 ${(diag.flushMs / 1000).toFixed(1)} 秒), navigator: ${navigator.userAgent.match(/Chrome/[d.]+/)?.[0] || '?'} on ${navigator.platform}": "Diagnostics: Write ${diag.writes} times average ${(diag.writeMs / Math.max(1, diag.writes)).toFixed(1)} ms/times (maximum ${diag.writeMaxMs.toFixed(0)} ms, total ${(diag.writeMs / 1000).toFixed(1)} seconds), read status ${diag.flushes} times average ${(diag.flushMs / Math.max(1, diag.flushes)).toFixed(1)} ms/times (total ${(diag.flushMs / 1000).toFixed(1)} seconds), navigator: ${navigator.userAgent.match(/Chrome/[d.]+/)?.[0] || '?'} on ${navigator.platform}", + "本体前面の GC0308 カメラで 1 枚撮影して表示します (640×480 カラー)。\n 露出安定化のため撮影に 1 秒ほどかかります。QR スキャン実行中は使えません。\n 「RAW 撮影」は ISP (AWB・色変換) をバイパスした Bayer モザイク (RGGB) を取得し、\n 「カラーバー」はセンサー内蔵テストパターンで転送経路を検証します。": "Take one 640×480 color photo with the GC0308 camera on the front. Exposure stabilization takes about one second. Unavailable while a QR scan is running. RAW capture bypasses the ISP (AWB and color conversion) for a Bayer RGGB mosaic. Color bars use the sensor's built-in test pattern to verify the data path.", + "

画面上辺のセンサーに手を近づけると、うれしい顔になって近接フレーズ (音声タブで設定、無ければ吹き出しのみ) を話します。会話中は反応しません。値は反射光量 (0〜2047) で、既定の前段 (ゲイン x64・15 パルス) では何もないとき 200 前後、既定の閾値は near 335 / far 240 です。すべて即時反映。

": "

Bring your hand near the sensor above the screen to show a happy face and speak a proximity phrase (configured on the Audio tab; if none is set, only the speech bubble appears). It does not react during a conversation. The reading is reflected light (0–2047): with the default front end (gain x64, 15 pulses), an empty scene reads around 200. The default thresholds are near 335 / far 240. All changes apply immediately.

", + "機体が自分の Wi-Fi で公式 GitHub Releases (ayutaz/sanoTTS-jp) から重みを取得して保存します。\n モデルの利用条件 (LICENSE-MODEL.md) は利用者が確認してください。\n 取得済みなら sanoTTS が優先して使われます。ファイルからの書き込みは Wi-Fi 設定画面で行えます。": "The device downloads and saves the weights from the official GitHub Releases (ayutaz/sanoTTS-jp) using its own Wi-Fi. Review the model terms (LICENSE-MODEL.md) before use. When installed, sanoTTS takes priority. Upload from a local file on the Wi-Fi settings page.", + "音声の出力先を選びます。「自動」は M5 Module Audio が装着されていれば\n そちらを優先、未装着なら内蔵スピーカ。「内蔵」と「Module Audio」は\n 強制指定 (Module Audio 未装着で「Module Audio」を選ぶと内蔵に\n フォールバックします)。保存後の再起動で切替わります。": "Select the audio output. Auto prefers M5 Module Audio when connected, otherwise uses the built-in speaker. Built-in and Module Audio force the selected output; selecting Module Audio without one connected falls back to the built-in speaker. Takes effect after saving and rebooting.", + "診断: 最初の 64 書き込み 平均 ${(diag.writeMs / diag.writes).toFixed(1)} ms/回 (最大 ${diag.writeMaxMs.toFixed(0)} ms), 状態読み取り 平均 ${(diag.flushMs / Math.max(1, diag.flushes)).toFixed(1)} ms/回": "Diagnostics: First 64 writes average ${(diag.writeMs / diag.writes).toFixed(1)} ms/times (maximum ${diag.writeMaxMs.toFixed(0)} ms), status reads average ${(diag.flushMs / Math.max(1, diag.flushes)).toFixed(1)} ms/times", + "本体前面の GC0308 カメラで 1 枚撮影して表示します (640×480 カラー)。 露出安定化のため撮影に 1 秒ほどかかります。QR スキャン実行中は使えません。 「RAW 撮影」は ISP (AWB・色変換) をバイパスした Bayer モザイク (RGGB) を取得し、 「カラーバー」はセンサー内蔵テストパターンで転送経路を検証します。": "Take one 640×480 color photo with the GC0308 camera on the front. Exposure stabilization takes about one second. Unavailable while a QR scan is running. RAW capture bypasses the ISP (AWB and color conversion) for a Bayer RGGB mosaic. Color bars use the sensor's built-in test pattern to verify the data path.", + "設定すると、BLE 設定ページ (これ) と WiFi 設定ページの両方で認証が必要に\n なります。BLE は暗号鍵にパスワードハッシュが混入するので、間違ったパスワード\n では全てのチャネルが復号失敗で動かなくなります。HTTP は Basic Auth。\n クリアで認証なし (現状互換) に戻ります。保存後の再起動で反映。": "Once set, both this BLE settings page and the Wi-Fi settings page require authentication. BLE mixes a password hash into its encryption key, so a wrong password makes every channel fail to decrypt. HTTP uses Basic Auth. Clear returns to no authentication (current behavior). Takes effect after saving and rebooting.", + "ローカルの音声ファイル (.mp3 / .wav / .m4a / .aac など) を選んで「再生」を押すと、\n ブラウザ側で 24 kHz mono AAC-LC に再エンコードし、BLE 経由でストリーミング送信します。\n スタックチャン側で AAC をデコードしながら再生し、音量に合わせて口を動かします。": "Choose a local audio file (.mp3, .wav, .m4a, .aac, and similar) and press Play. The browser re-encodes it to 24 kHz mono AAC-LC and streams it over BLE. Stack-chan decodes AAC while playing and animates the mouth to the volume.", + "されています (物理ボタンで AP を有効化\n した ≒ 本体アクセス済み)。まず「接続」タブで家庭 Wi-Fi の SSID / パスワードを\n 設定 → 保存 & 再起動してください。ファームを更新する場合は、STA 未接続では\n リリース取得 (device side fetch) が使えないため、「メンテ」タブの": "(AP was enabled with the physical button, so this device is considered trusted). First, set the home Wi-Fi SSID and password on the Connect tab, then select Save & reboot. To update firmware while STA is disconnected, in-app release download is unavailable; use local file upload on the Maintenance tab instead.", + "機体が自分の Wi-Fi で公式 GitHub Releases (ayutaz/sanoTTS-jp) から重みを取得して保存します。 モデルの利用条件 (LICENSE-MODEL.md) は利用者が確認してください。 取得済みなら sanoTTS が優先して使われます。ファイルからの書き込みは Wi-Fi 設定画面で行えます。": "The device downloads and saves the weights from the official GitHub Releases (ayutaz/sanoTTS-jp) using its own Wi-Fi. Review the model terms (LICENSE-MODEL.md) before use. When installed, sanoTTS takes priority. Upload from a local file on the Wi-Fi settings page.", + "設定すると、BLE 設定ページ (これ) と WiFi 設定ページの両方で認証が必要に なります。BLE は暗号鍵にパスワードハッシュが混入するので、間違ったパスワード では全てのチャネルが復号失敗で動かなくなります。HTTP は Basic Auth。 クリアで認証なし (現状互換) に戻ります。保存後の再起動で反映。": "Once set, both this BLE settings page and the Wi-Fi settings page require authentication. BLE mixes a password hash into its encryption key, so a wrong password makes every channel fail to decrypt. HTTP uses Basic Auth. Clear returns to no authentication (current behavior). Takes effect after saving and rebooting.", + "Stack-chan の主な振る舞いを 3 つから選びます。「会話応答」は OpenAI / Gemini /\n XiaoZhi のいずれかで会話、「JTTS ランダム発話」は音声のアイドル発話のみ、\n 「マイクでリップシンク」はマイク入力に合わせて口だけ動かします。\n 保存後の再起動で切替わります。": "Choose one of three main behaviors. Conversation talks with OpenAI, Gemini, or XiaoZhi. JTTS random speech only makes idle spoken phrases. Mic lip sync moves the mouth in response to microphone input. Changes take effect after saving and rebooting.", + "範囲設定モードを ON にするとサーボの力が抜け、手で動かした位置を取り込めます。\n 中央位置で「Yaw 原点取込」、左端で「Yaw 最小取込」、右端で「Yaw 最大取込」を順にタップしてください。\n 取り込んだ値は上の入力欄に反映されます。最後に「保存 & 再起動」で確定します。": "Turn on range-setting mode to release servo power and capture hand-positioned endpoints. Tap Yaw center capture at the center, Yaw minimum capture at the left endpoint, and Yaw maximum capture at the right endpoint. Captured values appear in the fields above. Select Save & reboot to apply them.", + "されています (物理ボタンで AP を有効化 した ≒ 本体アクセス済み)。まず「接続」タブで家庭 Wi-Fi の SSID / パスワードを 設定 → 保存 & 再起動してください。ファームを更新する場合は、STA 未接続では リリース取得 (device side fetch) が使えないため、「メンテ」タブの": "(AP was enabled with the physical button, so this device is considered trusted). First, set the home Wi-Fi SSID and password on the Connect tab, then select Save & reboot. To update firmware while STA is disconnected, in-app release download is unavailable; use local file upload on the Maintenance tab instead.", + "音声の出力先を選びます。「自動」は M5 Module Audio が装着されていれば そちらを優先、未装着なら内蔵スピーカ。「内蔵」と「Module Audio」は 強制指定 (Module Audio 未装着で「Module Audio」を選ぶと内蔵に フォールバックします)。保存後の再起動で切替わります。": "Select the audio output. Auto prefers M5 Module Audio when connected, otherwise uses the built-in speaker. Built-in and Module Audio force the selected output; selecting Module Audio without one connected falls back to the built-in speaker. Takes effect after saving and rebooting.", + "ローカルの音声ファイル (.mp3 / .wav / .m4a / .aac など) を選んで「再生」を押すと、 ブラウザ側で 24 kHz mono AAC-LC に再エンコードし、BLE 経由でストリーミング送信します。 スタックチャン側で AAC をデコードしながら再生し、音量に合わせて口を動かします。": "Choose a local audio file (.mp3, .wav, .m4a, .aac, and similar) and press Play. The browser re-encodes it to 24 kHz mono AAC-LC and streams it over BLE. Stack-chan decodes AAC while playing and animates the mouth to the volume.", + "ドロップダウンで選ぶと右のテキスト欄に反映されます。手入力も可 (未来の\n tag / 特殊 build)。ボード判定 (cores3 / atoms3r / atoms3 / stopwatch) は\n device 側で自動。進捗は下の status バーに反映されます。": "Selecting a tag copies it to the text field. Manual entry is also supported for future tags or special builds. The device detects the board automatically (cores3, atoms3r, atoms3, stopwatch). Progress appears in the status bar below.", + "Stack-chan の主な振る舞いを 3 つから選びます。「会話応答」は OpenAI / Gemini / XiaoZhi のいずれかで会話、「JTTS ランダム発話」は音声のアイドル発話のみ、 「マイクでリップシンク」はマイク入力に合わせて口だけ動かします。 保存後の再起動で切替わります。": "Choose one of three main behaviors. Conversation talks with OpenAI, Gemini, or XiaoZhi. JTTS random speech only makes idle spoken phrases. Mic lip sync moves the mouth in response to microphone input. Changes take effect after saving and rebooting.", + "会話中に「調べて」「〇〇に聞いて」と頼むと、家のパソコンで動くエージェント (tools/stackchan-agent) に依頼して回答を話します。LAN か Tailscale で届く http:// の URL を指定。OpenAI / Gemini のみ。保存後の再起動で反映されます。": "When you ask the device to look something up or ask another assistant, it can delegate to an agent running on your home computer (tools/stackchan-agent) and speak the answer. Enter an http:// URL reachable over your LAN or Tailscale. OpenAI and Gemini only. Takes effect after saving and rebooting.", + "ON のときの制御方式を選びます。「輝度」は口が開くほど LED 全体が明るくなる\n 従来動作。「レベル メーター」はネコミミの三角形 (LED1 = 底辺、LED5 = 頂点)\n を 5 段階のバー グラフとして点灯させます。保存後の再起動で反映されます。": "Select the control mode used when enabled. Brightness makes the whole LED brighter as the mouth opens. Level meter lights the cat-ear triangle (LED1 at the base, LED5 at the apex) as a five-step bar graph. Takes effect after saving and rebooting.", + "範囲設定モードを ON にするとサーボの力が抜け、手で動かした位置を取り込めます。 中央位置で「Yaw 原点取込」、左端で「Yaw 最小取込」、右端で「Yaw 最大取込」を順にタップしてください。 取り込んだ値は上の入力欄に反映されます。最後に「保存 & 再起動」で確定します。": "Turn on range-setting mode to release servo power and capture hand-positioned endpoints. Tap Yaw center capture at the center, Yaw minimum capture at the left endpoint, and Yaw maximum capture at the right endpoint. Captured values appear in the fields above. Select Save & reboot to apply them.", + "ドロップダウンで選ぶと右のテキスト欄に反映されます。手入力も可 (未来の tag / 特殊 build)。ボード判定 (cores3 / atoms3r / atoms3 / stopwatch) は device 側で自動。進捗は下の status バーに反映されます。": "Selecting a tag copies it to the text field. Manual entry is also supported for future tags or special builds. The device detects the board automatically (cores3, atoms3r, atoms3, stopwatch). Progress appears in the status bar below.", + "経由で GitHub Pages から自分宛のビルドを取得し\n OTA 適用します。AP モード中でも STA がアップであれば動作 (APSTA)。STA が\n ダウンの場合はエラーになるので、上の「ローカル ファイル」を使ってください。": "The device fetches the matching build from GitHub Pages and applies it over OTA. This also works in AP mode when STA is connected (APSTA). If STA is down, use Local file above instead.", + "設定すると、この設定ページ (WiFi) と BLE の両方で認証が必要になります。\n 空欄を保存すると認証なし (現状互換) に戻ります。HTTP は Basic Auth(即時反映・\n 再起動不要)、BLE は暗号鍵に混入されるため次回接続時に反映。": "Once set, both this Wi-Fi settings page and BLE require authentication. Save an empty field to return to no authentication (current behavior). HTTP uses Basic Auth and applies immediately; BLE mixes the password into its encryption key, so changes apply on the next connection.", + "近接: ${v.available ? (v.enabled ? '有効' : '無効') : 'センサーなし'} (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s)": "Proximity: ${v.available ? (v.enabled ? '有効' : '無効') : 'センサーなし'} (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s)", + "ネコミミ部 NeoPixel (GPIO9 / 18 LED) のモード・色・明るさを設定します。\n 変更は即時反映 + 自動で NVS に保存されるので再起動後も復元されます。\n gradient (虹) モードでは色は無視されます。": "Configure the mode, color, and brightness of the cat-ear NeoPixels (GPIO9 / 18 LEDs). Changes apply immediately and are saved to NVS automatically. Color is ignored in gradient (rainbow) mode.", + "※ Android が「インターネット接続がありません」と表示しても、\n 接続は維持したままにしてください。元の Wi-Fi に自動で戻って\n しまう場合は、Wi-Fi 設定で Stackchan AP を選び直してください。": "If Android says the network has no internet connection, keep the connection anyway. If it switches back to the original Wi-Fi automatically, reconnect to the Stackchan AP in Wi-Fi settings.", + "近接センサー調整を適用 (gain=${['x16','x32','x64'][v.gain]} LED=${[5,10,20,50,100][v.led_current]}mA pulses=${v.pulses} offset=${v.offset})": "Apply proximity sensor adjustment (gain=${['x16','x32','x64'][v.gain]} LED=${[5,10,20,50,100][v.led_current]}mA pulses=${v.pulses} offset=${v.offset})", + "ON のときの制御方式を選びます。「輝度」は口が開くほど LED 全体が明るくなる 従来動作。「レベル メーター」はネコミミの三角形 (LED1 = 底辺、LED5 = 頂点) を 5 段階のバー グラフとして点灯させます。保存後の再起動で反映されます。": "Select the control mode used when enabled. Brightness makes the whole LED brighter as the mouth opens. Level meter lights the cat-ear triangle (LED1 at the base, LED5 at the apex) as a five-step bar graph. Takes effect after saving and rebooting.", + "設定すると、この設定ページ (WiFi) と BLE の両方で認証が必要になります。 空欄を保存すると認証なし (現状互換) に戻ります。HTTP は Basic Auth(即時反映・ 再起動不要)、BLE は暗号鍵に混入されるため次回接続時に反映。": "Once set, both this Wi-Fi settings page and BLE require authentication. Save an empty field to return to no authentication (current behavior). HTTP uses Basic Auth and applies immediately; BLE mixes the password into its encryption key, so changes apply on the next connection.", + "経由で GitHub Pages から自分宛のビルドを取得し OTA 適用します。AP モード中でも STA がアップであれば動作 (APSTA)。STA が ダウンの場合はエラーになるので、上の「ローカル ファイル」を使ってください。": "The device fetches the matching build from GitHub Pages and applies it over OTA. This also works in AP mode when STA is connected (APSTA). If STA is down, use Local file above instead.", + "ネコミミ部 NeoPixel (GPIO9 / 18 LED) のモード・色・明るさを設定します。 変更は即時反映 + 自動で NVS に保存されるので再起動後も復元されます。 gradient (虹) モードでは色は無視されます。": "Configure the mode, color, and brightness of the cat-ear NeoPixels (GPIO9 / 18 LEDs). Changes apply immediately and are saved to NVS automatically. Color is ignored in gradient (rainbow) mode.", + "応答再生中にアバター画面 (頭) をタップで AI 応答を中断します。タッチ センサが敏感で誤動作しやすいので、初期値はオフです。なでなで (前→中→後の連続タッチ) は本トグルとは独立で常に有効です。保存後の再起動で反映されます。": "Tap the avatar screen (head) while a response is playing to interrupt the AI. The default is off because the touch sensor is sensitive. Stroking (continuous touch from front to middle to back) is independent of this toggle and always enabled. Takes effect after saving and rebooting.", + "応答再生中にアバター画面 (頭) をタップで AI 応答を中断します。タッチ センサが敏感で誤動作しやすいので、初期値はオフです。なでなで (前→中→後の連続タッチ) は本トグルとは独立で常に有効です。即時反映(再起動不要)。": "Tap the avatar screen (head) while a response is playing to interrupt the AI. The default is off because the touch sensor is sensitive. Stroking (continuous touch from front to middle to back) is independent of this toggle and always enabled. Applies immediately; no reboot needed.", + "※ Android が「インターネット接続がありません」と表示しても、 接続は維持したままにしてください。元の Wi-Fi に自動で戻って しまう場合は、Wi-Fi 設定で Stackchan AP を選び直してください。": "If Android says the network has no internet connection, keep the connection anyway. If it switches back to the original Wi-Fi automatically, reconnect to the Stackchan AP in Wi-Fi settings.", + "を\n 選んで「書き込み」を押すと、暗号化された BLE 経由で新ファームを別パーティションに書き込み、\n 再起動します。成功後はブラウザ側で BLE が切断されるので、手動で再接続してください。": "and press Write to flash the new firmware to the other partition over encrypted BLE and reboot. BLE disconnects after a successful update; reconnect manually.", + "ダウンロード中 ${Math.round(received * 100 / total)}% (${Math.round(received/1024)} / ${Math.round(total/1024)} KiB)": "Downloading ${Math.round(received * 100 / total)}% (${Math.round(received/1024)} / ${Math.round(total/1024)} KiB)", + "ダウンロード中 ${Math.round(received * 100 / total)}% (${Math.round(received/1024)} / ${Math.round(total/1024)} KiB)": "Downloading ${Math.round(received * 100 / total)}% (${Math.round(received/1024)} / ${Math.round(total/1024)} KiB)", + "(AP モードで Wi-Fi がインターネットに繋がっていなくても、iOS は\n セルラーへ自動 fallback します。繋がらない場合は一度 Wi-Fi を切ってからやり直してください。)": "(iOS may fall back to cellular even when AP mode has no internet access. If it does not load, temporarily turn Wi-Fi off and try again.)", + "やわらかめの目安: 開大比 0.65–0.75 / tilt 6–12 dB / 帯域幅 1.2–1.5\n (+息成分 0.1–0.25)。いずれも v2 方式で有効 (帯域幅はクラシックでも有効)。": "For a softer voice, try open quotient 0.65–0.75, tilt 6–12 dB, and bandwidth 1.2–1.5 (plus breathiness 0.1–0.25). All apply to v2; bandwidth also applies to Classic.", + "会話 API への WebSocket 接続に付与する追加ヘッダ。1 行 1 つ「ヘッダ名: 値」。Cloudflare Access のサービストークン等に使用。全プロバイダ共通。保存後の再起動で反映されます。": "Extra headers added to conversation API WebSocket connections. Use one \"Header-Name: value\" per line. Useful for Cloudflare Access service tokens and similar. Shared by all providers. Takes effect after saving and rebooting.", + "会話モードで画面右上のステータスに合わせて LED の光り方を変えます。待受中は上の通常設定どおり。各状態で「通常どおり」を選ぶとその状態では切り替えません。明るさは上の「明るさ」が上限。即時反映(再起動不要)。": "In conversation mode, change the LED pattern to match the status shown in the top-right corner. Idle uses the normal settings above. Selecting Normal for a state leaves it unchanged. The brightness above is the upper limit. Applies immediately; no reboot needed.", + "送信中 ${pct}% / ${(bytesSent/1024).toFixed(1)} KiB / ${rate} KiB/s / credit ${(credit/1024).toFixed(1)} KiB": "Sending ${pct}% / ${(bytesSent/1024).toFixed(1)} KiB / ${rate} KiB/s / credit ${(credit/1024).toFixed(1)} KiB", + "ひらがな / カタカナで入力して「話す」ボタンを押すと jtts で読み上げます。\n 漢字や英字は変換されないので、フリガナを直接入力してください。\n 音量や音声出力の確認に便利。": "Enter hiragana or katakana and select Speak to hear JTTS read it aloud. Kanji and Latin letters are not converted, so enter the pronunciation directly. Useful for checking volume and audio output.", + "を確認してください。合成音声はつくよみちゃんコーパスの規約により、個人・団体への攻撃や批判、\n 政治的・宗教的な主張、注意書きのない過激な内容、他者が再利用できる素材としての配布には使えません。": "the license. Under the Tsukuyomi-chan Corpus terms, synthesized audio may not be used to attack or criticize individuals or groups, promote political or religious views, include extreme content without a warning, or be distributed as reusable material.", + "(アクセント句切り) を\n 書けます。CC BY のボイス例: Mei / tohoku-f01 / nitech m001。\n 反映は即時、flash 保存で再起動後も有効。": "(accent phrase break) can be written. CC BY voice examples: Mei, tohoku-f01, and nitech m001. Applies immediately and persists across reboots once saved to flash.", + "⚠ 会話機能が有効なため使用できません。無線/CPU を奪い合って再生が\n 途切れるため排他です。「会話機能を有効にする」を OFF にして\n 保存・再起動すると利用できます。": "⚠ Unavailable while Conversation is enabled because both compete for radio and CPU. Turn off Enable conversation, save, and reboot to use audio streaming.", + "アバターの口が開くと LED が反応します (会話再生・jtts 発話・マイク リップシンク いずれの口元アニメーションでも有効)。OFF のときは設定した明るさ で固定。保存後の再起動で反映されます。": "The LEDs respond when the avatar's mouth opens during conversation playback, JTTS speech, mic lip sync, or any mouth animation. When off, brightness stays fixed at the configured level. Takes effect after saving and rebooting.", + "+ 自動保存 (再起動不要)。\n 100% = ボード ごとの工場既定値、200% でブースト (内部 master volume\n を 2 倍にして 255 で飽和)。": "and saves automatically (no reboot). 100% is the board's factory default; 200% doubles the internal master volume and saturates at 255.", + "やわらかめの目安: 開大比 0.65–0.75 / tilt 6–12 dB / 帯域幅 1.2–1.5 (+息成分 0.1–0.25)。いずれも v2 方式で有効 (帯域幅はクラシックでも有効)。": "For a softer voice, try open quotient 0.65–0.75, tilt 6–12 dB, and bandwidth 1.2–1.5 (plus breathiness 0.1–0.25). All apply to v2; bandwidth also applies to Classic.", + "サーボの取り付け向きや可動範囲はベース基板で異なります。原点(0..1023)と可動範囲(度)を\n 設定して「保存 & 再起動」で反映してください。既定は M5 base の値です。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and range in degrees, then select Save & reboot. Defaults match the M5 base.", + "BLE アドバタイズ名と mDNS ホスト名のもとになります。空欄なら自動生成 (Stackchan-XXXXXX)。\n mDNS は英数字とハイフンに丸められます。保存後の再起動で反映。": "Used for the BLE advertising name and mDNS hostname. Leave blank to generate one automatically (Stackchan-XXXXXX). mDNS names are limited to letters, numbers, and hyphens. Takes effect after saving and rebooting.", + "アバターの口が開くと LED が反応します (会話再生・jtts 発話・マイク リップシンク いずれの口元アニメーションでも有効)。OFF のときは設定した明るさで固定。即時反映(再起動不要)。": "The LEDs respond when the avatar's mouth opens during conversation playback, JTTS speech, mic lip sync, or any mouth animation. When off, brightness stays fixed at the configured level. Applies immediately; no reboot needed.", + "サーボの取り付け向きや可動範囲はベース基板で異なります。原点(0..1023)と可動範囲(度)を\n 設定して「保存して再起動」で反映してください。既定は M5 base の値です。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and range in degrees, then select Save & reboot. Defaults match the M5 base.", + "機体が GitHub Releases (ayutaz/sanoTTS-jp ${release}/${file}) から取得します。LICENSE-MODEL.md の条件に同意していますか?": "The aircraft is obtained from GitHub Releases (ayutaz/sanoTTS-jp ${release}/${file}). Do you agree to the terms of LICENSE-MODEL.md?", + "BLE アドバタイズ名と mDNS ホスト名のもとになります。空欄なら自動生成\n (Stackchan-XXXXXX)。mDNS は英数字とハイフンに丸められます。保存後の再起動で反映。": "Used for the BLE advertising name and mDNS hostname. Leave blank to generate one automatically (Stackchan-XXXXXX). mDNS names are limited to letters, numbers, and hyphens. Takes effect after saving and rebooting.", + "を確認してください。合成音声はつくよみちゃんコーパスの規約により、個人・団体への攻撃や批判、 政治的・宗教的な主張、注意書きのない過激な内容、他者が再利用できる素材としての配布には使えません。": "the license. Under the Tsukuyomi-chan Corpus terms, synthesized audio may not be used to attack or criticize individuals or groups, promote political or religious views, include extreme content without a warning, or be distributed as reusable material.", + "アバターの顔の描画ロジックを Lua 風 DSL で書いて、ブラウザ内でバイトコード化\n → BLE 経由で実機に送信して即時反映します。NVS に保存されるので再起動後も維持。": "Write the avatar's drawing logic in a Lua-style DSL, compile it to bytecode in the browser, and send it to the device over BLE for immediate effect. It is stored in NVS and survives reboots.", + "(AP モードで Wi-Fi がインターネットに繋がっていなくても、iOS は セルラーへ自動 fallback します。繋がらない場合は一度 Wi-Fi を切ってからやり直してください。)": "(iOS may fall back to cellular even when AP mode has no internet access. If it does not load, temporarily turn Wi-Fi off and try again.)", + "の重み (654 KB) を入れると、jtts の発話がニューラル合成 (22.05 kHz) になります\n (PSRAM のあるボード)。読みは HTS ボイスと同じ記法 (": "Installing the weights (654 KB) enables neural synthesis at 22.05 kHz for JTTS (boards with PSRAM). Pronunciation uses the same notation as HTS voices (", + "を 選んで「書き込み」を押すと、暗号化された BLE 経由で新ファームを別パーティションに書き込み、 再起動します。成功後はブラウザ側で BLE が切断されるので、手動で再接続してください。": "and press Write to flash the new firmware to the other partition over encrypted BLE and reboot. BLE disconnects after a successful update; reconnect manually.", + "近接の設定を送信 (near=${pv.near} far=${pv.far} hold=${pv.hold_ms}ms cooldown=${pv.cooldown_s}s) — 即時反映": "Send proximity settings (near=${pv.near} far=${pv.far} hold=${pv.hold_ms}ms cooldown=${pv.cooldown_s}s) — Immediately reflected", + "AAC エンコード開始: ${(file.size/1024).toFixed(1)} KiB → ${durationMs} ms @ ${AAC_SAMPLE_RATE}Hz mono": "Start AAC encoding: ${(file.size/1024).toFixed(1)} KiB → ${durationMs} ms @ ${AAC_SAMPLE_RATE}Hz mono", + "BLE アドバタイズ名と mDNS ホスト名のもとになります。空欄なら自動生成 (Stackchan-XXXXXX)。 mDNS は英数字とハイフンに丸められます。保存後の再起動で反映。": "Used for the BLE advertising name and mDNS hostname. Leave blank to generate one automatically (Stackchan-XXXXXX). mDNS names are limited to letters, numbers, and hyphens. Takes effect after saving and rebooting.", + "周辺の雑音レベルを自動追跡して、静かな部屋でも騒音下でも声に反応するよう感度を調整します。OFF にすると固定閾値で判定 (環境変化に弱いが予測しやすい)。保存後の再起動で反映されます。": "Tracks ambient noise automatically so the device responds to your voice in quiet and noisy rooms. When off, a fixed threshold is used (less adaptable but more predictable). Takes effect after saving and rebooting.", + "BLE アドバタイズ名と mDNS ホスト名のもとになります。空欄なら自動生成 (Stackchan-XXXXXX)。mDNS は英数字とハイフンに丸められます。保存後の再起動で反映。": "Used for the BLE advertising name and mDNS hostname. Leave blank to generate one automatically (Stackchan-XXXXXX). mDNS names are limited to letters, numbers, and hyphens. Takes effect after saving and rebooting.", + "HTS ボイス (.htsvoice、~1-2 MB) をアップロードすると jtts の発話が\n HMM 合成になります (16 MB flash ボードのみ)。読みに": "Upload an HTS voice (.htsvoice, about 1–2 MB) to use HMM synthesis for JTTS (16 MB flash boards only). In the pronunciation,", + "状態: ロード済み (${who}${Math.round(st.stored/1024)} KiB / 上限 ${Math.round(st.capacity/1024)} KiB)": "Status: Loaded (${who}${Math.round(st.stored/1024)} KiB / Upper limit ${Math.round(st.capacity/1024)} KiB)", + "近接の閾値を適用 (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s) — 即時反映": "Apply proximity threshold (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s) — Immediate effect", + "サーボの取り付け向きや可動範囲はベース基板で異なります。原点(0..1023)と可動範囲(度)を 設定して「保存 & 再起動」で反映してください。既定は M5 base の値です。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and range in degrees, then select Save & reboot. Defaults match the M5 base.", + "アバターの顔の描画ロジックを Lua 風 DSL で書いて、ブラウザ内でバイトコード化\n → 実機に送信して即時反映します。NVS に保存されるので再起動後も維持。": "Write the avatar's drawing logic in a Lua-style DSL, compile it to bytecode in the browser, and send it to the device for immediate effect. It is stored in NVS and survives reboots.", + "サーボの取り付け向きや可動範囲はベース基板で異なります。原点(0..1023)と可動範囲(度)を 設定して「保存して再起動」で反映してください。既定は M5 base の値です。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and range in degrees, then select Save & reboot. Defaults match the M5 base.", + "ひらがな / カタカナで入力して「話す」ボタンを押すと jtts で読み上げます。 漢字や英字は変換されないので、フリガナを直接入力してください。 音量や音声出力の確認に便利。": "Enter hiragana or katakana and select Speak to hear JTTS read it aloud. Kanji and Latin letters are not converted, so enter the pronunciation directly. Useful for checking volume and audio output.", + "アバターの顔の描画ロジックを Lua 風 DSL で書いて、ブラウザ内でバイトコード化 → BLE 経由で実機に送信して即時反映します。NVS に保存されるので再起動後も維持。": "Write the avatar's drawing logic in a Lua-style DSL, compile it to bytecode in the browser, and send it to the device over BLE for immediate effect. It is stored in NVS and survives reboots.", + "Wi-Fi 経由設定。家庭 LAN 上の他端末から API キーを盗聴される可能性があるので、\n 信頼できないネットワークでは BLE 版の設定ツールを使ってください。": "Wi-Fi settings. Other devices on your home LAN could intercept API keys, so use the BLE settings tool on untrusted networks.", + "の重み (654 KB) を入れると、jtts の発話がニューラル合成 (22.05 kHz) になります (PSRAM のあるボード)。読みは HTS ボイスと同じ記法 (": "Installing the weights (654 KB) enables neural synthesis at 22.05 kHz for JTTS (boards with PSRAM). Pronunciation uses the same notation as HTS voices (", + "(アクセント句切り) を 書けます。CC BY のボイス例: Mei / tohoku-f01 / nitech m001。 反映は即時、flash 保存で再起動後も有効。": "(accent phrase break) can be written. CC BY voice examples: Mei, tohoku-f01, and nitech m001. Applies immediately and persists across reboots once saved to flash.", + "⚠ 会話機能が有効なため使用できません。無線/CPU を奪い合って再生が 途切れるため排他です。「会話機能を有効にする」を OFF にして 保存・再起動すると利用できます。": "⚠ Unavailable while Conversation is enabled because both compete for radio and CPU. Turn off Enable conversation, save, and reboot to use audio streaming.", + "本体の「LT」タブで開始/停止。ここでは既定時間と通知ワードを設定します(保存しなくても送信で即反映、保存で再起動後も保持)。読みはひらがな(jtts は漢字を読めません)": "Start and stop with the LT tab on the device. Set the default duration and notification phrases here. Send applies immediately without saving; Save preserves them across reboots. Pronunciations must use hiragana because JTTS cannot read kanji.", + "状態: ロード済み (${Math.round(st.stored/1024)} KiB / 上限 ${Math.round(st.capacity/1024)} KiB)": "Status: Loaded (${Math.round(st.stored/1024)} KiB / Upper limit ${Math.round(st.capacity/1024)} KiB)", + "HTS ボイス (.htsvoice、~1-2 MB) をアップロードすると jtts の発話が HMM 合成になります (16 MB flash ボードのみ)。読みに": "Upload an HTS voice (.htsvoice, about 1–2 MB) to use HMM synthesis for JTTS (16 MB flash boards only). In the pronunciation,", + "配布ボイスをブラウザがダウンロードしてデバイスへ転送・インストールします\n (PC にファイルを用意する必要なし。ブラウザ側にインターネット接続が必要)。": "The browser downloads a distributed voice and transfers it to the device for installation. No local file is needed, but the browser must have internet access.", + "+ 自動保存 (再起動不要)。 100% = ボード ごとの工場既定値、200% でブースト (内部 master volume を 2 倍にして 255 で飽和)。": "and saves automatically (no reboot). 100% is the board's factory default; 200% doubles the internal master volume and saturates at 255.", + "Wi-Fi 経由設定。家庭 LAN 上の他端末から API キーを盗聴される可能性があるので、 信頼できないネットワークでは BLE 版の設定ツールを使ってください。": "Wi-Fi settings. Other devices on your home LAN could intercept API keys, so use the BLE settings tool on untrusted networks.", + "適用しました (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s)": "Applied (near=${v.near} far=${v.far} hold=${v.hold_ms}ms cooldown=${v.cooldown_s}s)", + "この画面が「ネットワークにログイン」ミニブラウザ\n (CaptivePortalLogin) で開いている場合、ファイル選択は動作しません。": "File selection does not work when this page is open in Android's CaptivePortalLogin mini-browser.", + "アバターの顔の描画ロジックを Lua 風 DSL で書いて、ブラウザ内でバイトコード化 → 実機に送信して即時反映します。NVS に保存されるので再起動後も維持。": "Write the avatar's drawing logic in a Lua-style DSL, compile it to bytecode in the browser, and send it to the device for immediate effect. It is stored in NVS and survives reboots.", + "sanoTTS ${cmd.op} 失敗: ${e?.message ?? e} (機体側が拒否 — Wi-Fi 未接続 / 取得中 など。状態を更新して確認)": "sanoTTS ${cmd.op} Failure: ${e?.message ?? e} (Refused by the aircraft side — Wi-Fi not connected/obtaining, etc. Update the status and check)", + "Characteristics 取得完了 (missing: ${missing.length ? missing.join(', ') : 'none'})": "Characteristics acquisition completed (missing: ${missing.length ? missing.join(', ') : 'none'})", + "の方を選んでください。前者はブートローダ+\n パーティション+アプリの結合イメージ (USB 初回書き込み用) で OTA には使えません。": "the latter is the combined bootloader, partition table, and app image for initial USB flashing and cannot be used for OTA.", + "無効にするとサーボ電源を入れず、サーボタスクも起動しません(保存後の再起動で反映)。操作画面の脱力/復帰トグルとは別物で、こちらは NVS に永続化されます。": "When disabled, servo power and the servo task remain off. Takes effect after saving and rebooting. This is separate from the release/return toggle on the operation screen and is persisted in NVS.", + "release tag ${arr.length} 件取得 (表示 ${$('ota-release-select').options.length} 件)": "Get release tag ${arr.length} items (Display ${$('ota-release-select').options.length} items)", + "本体の「LT」タブで開始/停止。ここでは既定時間と通知ワードを設定(書込で即反映、保存で再起動後も保持)。読みはひらがな(jtts は漢字を読めません)": "Start and stop with the LT tab on the device. Set the default duration and notification phrases here. Write applies immediately; Save preserves them across reboots. Pronunciations must use hiragana because JTTS cannot read kanji.", + "配布ボイスをブラウザがダウンロードしてデバイスへ転送・インストールします (PC にファイルを用意する必要なし。ブラウザ側にインターネット接続が必要)。": "The browser downloads a distributed voice and transfers it to the device for installation. No local file is needed, but the browser must have internet access.", + "に\n 変化します(再起動不要)。気に入った設定を残すには「保存 & 再起動」を押してください\n (再起動して確定します)。": "(no reboot required). Select Save & reboot to keep the settings.", + "を選んで「書き込み」を押すと、新ファームを\n 別パーティションに書き込んで再起動します。Wi-Fi 経由は BLE より大幅に高速です。": "and press Write to flash the new firmware to the other partition and reboot. Wi-Fi is much faster than BLE.", + "⚠ AP モード (STA 未接続) 中はリリース取得できません。上の「ローカル\n ファイル アップロード」を使用してください。": "⚠ Release download is unavailable in AP mode while STA is disconnected. Use Local file upload above.", + "データ送信完了 (${elapsed.toFixed(1)} 秒, 平均 ${fmtBytes(received / elapsed)}/s)": "Data transmission completed (${elapsed.toFixed(1)} seconds, average ${fmtBytes(received / elapsed)}/s)", + "OpenAI / Gemini の会話に使う指示文。XiaoZhi はサーバー側の人格を使うため無視されます。保存後の再起動で反映されます。": "Instructions used for OpenAI and Gemini conversations. XiaoZhi uses a server-side persona and ignores this. Takes effect after saving and rebooting.", + "Wi-Fi 経由の RTP 音声受信(L16/μ-law=udp/6970, AAC=udp/6972)。会話機能オン時は自動的に無効です": "Receive RTP audio over Wi-Fi (L16/μ-law on UDP 6970, AAC on UDP 6972). Automatically disabled while Conversation is on.", + "この画面が「ネットワークにログイン」ミニブラウザ (CaptivePortalLogin) で開いている場合、ファイル選択は動作しません。": "File selection does not work when this page is open in Android's CaptivePortalLogin mini-browser.", + "の方を選んでください。前者はブートローダ+ パーティション+アプリの結合イメージ (USB 初回書き込み用) で OTA には使えません。": "the latter is the combined bootloader, partition table, and app image for initial USB flashing and cannot be used for OTA.", + "内蔵 / Module Audio / Atomic Echo Base すべての出力に効きます。\n スライダーを動かすと": "Applies to the built-in speaker, Module Audio, and Atomic Echo Base.", + "句切り)。\n 重みは本ファームウェアには同梱していません。機体が公式 GitHub Releases から直接取得します。": "phrase break). The weights are not bundled with this firmware; the device downloads them directly from the official GitHub Releases.", + "HMM ボイス (.htsvoice) / 単位連結 音声 DB (.jvox) のアップロードは WiFi 設定ページから行えます。": "Upload HMM voices (.htsvoice) and unit-concatenation voice databases (.jvox) from the Wi-Fi settings page.", + "を選んで「書き込み」を押すと、新ファームを 別パーティションに書き込んで再起動します。Wi-Fi 経由は BLE より大幅に高速です。": "and press Write to flash the new firmware to the other partition and reboot. Wi-Fi is much faster than BLE.", + "マイク入力の音量に合わせて口を開閉します。CoreS3 はマイク感度が低めなので入力ゲインを上げると見栄えします。スライダーを動かすと": "Opens and closes the mouth based on microphone volume. CoreS3 microphone sensitivity is low, so a higher input gain usually looks better.", + "実音声由来のモーラ単位 DB をアップロードすると、jtts の発話が単位連結\n (TD-PSOLA) 合成になります。": "Upload a real-voice mora unit database to use TD-PSOLA unit-concatenation synthesis for JTTS.", + "起動直後の C5–E5–G5(スピーカー動作確認音)を鳴らすかどうか。OFF にすると無音起動(設定は即時保存・音自体は次回起動時)": "Play C5–E5–G5 immediately after startup as a speaker check. When off, startup is silent. The setting is saved immediately; the sound changes on the next boot.", + "のツールで生成した\n .jvox (ADPCM 版推奨、~230 KB)。反映は即時、NVS 保存で再起動後も有効。": "A .jvox generated by this tool (ADPCM recommended, about 230 KB). Applies immediately and persists across reboots once saved to NVS.", + "⚠ AP モード (STA 未接続) 中はリリース取得できません。上の「ローカル ファイル アップロード」を使用してください。": "⚠ Release download is unavailable in AP mode while STA is disconnected. Use Local file upload above.", + "状態: ロード済み (${st.units} 単位、保存 ${Math.round(st.stored/1024)} KiB)": "Status: Loaded (${st.units} units, saved ${Math.round(st.stored/1024)} KiB)", + "OK — ${buf.byteLength} bytes, コンパイル ${compileMs.toFixed(1)} ms": "OK — ${buf.byteLength} bytes, compile ${compileMs.toFixed(1)} ms", + "OTA 完了 — デバイスを再起動します。新ファームの起動確認は自動で行われます。BLE が切断されたら再接続してください。": "OTA Complete — Reboot the device. The startup confirmation of the new firmware will be performed automatically. If BLE is disconnected, please reconnect.", + "Recovery に再接続できません (${lastErr ? lastErr.message : 'timeout'})": "Unable to reconnect to Recovery (${lastErr ? lastErr.message : 'timeout'})", + "プロバイダ: ${provider === 1 ? 'Gemini Live' : 'OpenAI Realtime'}": "Provider: ${provider === 1 ? 'Gemini Live' : 'OpenAI Realtime'}", + "内蔵 / Module Audio / Atomic Echo Base すべての出力に効きます。 スライダーを動かすと": "Applies to the built-in speaker, Module Audio, and Atomic Echo Base.", + "句切り)。 重みは本ファームウェアには同梱していません。機体が公式 GitHub Releases から直接取得します。": "phrase break). The weights are not bundled with this firmware; the device downloads them directly from the official GitHub Releases.", + "sanoTTS 重みの取得を開始: ${release}/${file} (機体の Wi-Fi 経由、10〜30 秒)": "Start retrieving sanoTTS weights: ${release}/${file} (via aircraft Wi-Fi, 10-30 seconds)", + "に 変化します(再起動不要)。気に入った設定を残すには「保存 & 再起動」を押してください (再起動して確定します)。": "(no reboot required). Select Save & reboot to keep the settings.", + "実音声由来のモーラ単位 DB をアップロードすると、jtts の発話が単位連結 (TD-PSOLA) 合成になります。": "Upload a real-voice mora unit database to use TD-PSOLA unit-concatenation synthesis for JTTS.", + "起動直後の C5–E5–G5(スピーカー動作確認音)を鳴らすかどうか。OFF にすると無音起動(保存後の再起動で反映)": "Play C5–E5–G5 immediately after startup as a speaker check. When off, startup is silent. Takes effect after saving and rebooting.", + "音声 DB 登録 (${st.units} 単位、${Math.round(st.stored/1024)} KiB)": "Audio DB registration (${st.units} units, ${Math.round(st.stored/1024)} KiB)", + "実機の顔を firmware 内蔵 デフォルトに戻します (NVS の override を削除)。よろしいですか?": "Return the face of the actual machine to the default built-in firmware (delete the NVS override). Are you sure?", + "のツールで生成した .jvox (ADPCM 版推奨、~230 KB)。反映は即時、NVS 保存で再起動後も有効。": "A .jvox generated by this tool (ADPCM recommended, about 230 KB). Applies immediately and persists across reboots once saved to NVS.", + "プロバイダを ${provSel.options[provSel.selectedIndex].text} に切替": "Switch provider to ${provSel.options[provSel.selectedIndex].text}", + "オフセット ${Math.min(1023, cur + raw)} を入れました — 「センサーに適用」で反映": "I put the offset ${Math.min(1023, cur + raw)} — reflected in “Apply to sensor”", + "プレビュー モジュール (avatar_module.js) が見つかりません — BLE 調整のみ利用できます": "Preview module (avatar_module.js) not found — only BLE adjustments available", + "+ 自動保存 (再起動不要)。\n 100% = ボード ごとの工場既定値、200% でブースト。": "and saves automatically (no reboot). 100% is the board's factory default; 200% boosts it.", + "MCP API トークン (空欄なら変更しない / 値が入っているなら上書き、空文字列に揃えるには「クリア」)": "MCP API token (leave blank to keep the current value; enter a value to replace it, or select Clear to store an empty string)", + "新ファーム ${name} (${fmtBytes(blob.size)}) を書き込みます。よろしいですか?": "Write new farm ${name} (${fmtBytes(blob.size)}). Are you sure?", + "LED 口の動き連動を ${lmsCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch the LED mouth movement link to ${lmsCheckbox.checked ? '有効' : '無効'}", + "OpenAI 会話機能を ${enCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch OpenAI conversation feature to ${enCheckbox.checked ? '有効' : '無効'}", + "jtts アイドル発話を ${jiCheckbox.checked ? '有効' : '無効'} に切り替え": "jtts switch idle utterance to ${jiCheckbox.checked ? '有効' : '無効'}", + "高速パス: writeValueWithoutResponse + 16-chunk status sync": "Fast path: writeValueWithoutResponse + 16-chunk status sync", + "WebCodecs AudioEncoder が使えません (Chrome / Edge 94+ が必要)": "WebCodecs AudioEncoder cannot be used (requires Chrome / Edge 94+)", + "flush 中に session が消失 (${st.state}: ${st.error || ''})": "session disappeared during flush (${st.state}: ${st.error || ''})", + "バッテリーゲージ表示を ${bgCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch battery gauge display to ${bgCheckbox.checked ? '有効' : '無効'}", + "${label}送信完了 — NVS に ${r.saved} バイト保存${r.note || ''}": "${label} Transmission complete — ${r.saved} bytes saved to NVS${r.note || ''}", + "RTP 音声受信を ${rtpCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch RTP audio reception to ${rtpCheckbox.checked ? '有効' : '無効'}", + "マイク AGC を ${agcCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch microphone AGC to ${agcCheckbox.checked ? '有効' : '無効'}", + "周辺の雑音レベルを自動追跡して感度を調整します。OFF にすると固定閾値で判定。即時反映(再起動不要)。": "Tracks ambient noise and adjusts sensitivity automatically. When off, a fixed threshold is used. Applies immediately; no reboot needed.", + "で確定。\n 接続後、即反映する項目 (アバター DSL / LT) は個別ボタンが付いています。": "select Save & reboot. Settings that apply immediately (Avatar DSL and LT) have their own buttons.", + "保存済み・ロード済み (${kb(s.stored)} / 領域 ${kb(s.capacity)})": "Saved/Loaded (${kb(s.stored)} / Area ${kb(s.capacity)})", + "通信は接続ごとに X25519 + AES-256-GCM で暗号化されます(OS ペアリング不要)。": "Each connection is encrypted with X25519 + AES-256-GCM (no OS pairing required).", + "Web Bluetooth が使えません。Chrome / Edge デスクトップで開いてください。": "Web Bluetooth cannot be used. Please open in Chrome/Edge desktop.", + "デバイスへ転送中… (${Math.round(buf.byteLength/1024)} KiB)": "Transferring to device... (${Math.round(buf.byteLength/1024)} KiB)", + "保存済みの sanoTTS 重みを削除しますか? (以後は HMM / 他エンジンにフォールバック)": "Do you want to delete saved sanoTTS weights? (Fallback to HMM/other engines from now on)", + "最新の release をローカル ファイルで書き込む手順 (iPhone / AP モードでも可)": "Manual update from the latest release using a local file (works on iPhone and in AP mode)", + "起動アルペジオを ${baCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch startup arpeggio to ${baCheckbox.checked ? '有効' : '無効'}", + "MCP API トークン 書き込み完了 (${mcpTokenEl.value.length}B)": "MCP API token write completed (${mcpTokenEl.value.length}B)", + "で動作します。\n Firefox・Safari は Web Bluetooth 非対応です。": ". Firefox and Safari do not support Web Bluetooth.", + "インストール完了 (${Math.round((st.stored||0)/1024)} KiB)": "Installation completed (${Math.round((st.stored||0)/1024)} KiB)", + "KeyExchange characteristic がありません。古すぎるファームウェアです。": "KeyExchange characteristic is missing. The firmware is too old.", + "バージインを ${biCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch barge-in to ${biCheckbox.checked ? '有効' : '無効'}", + "互換パス: writeValueWithResponse (firmware < v0.0.4)": "Compatible path: writeValueWithResponse (firmware < v0.0.4)", + "機体が Wi-Fi 経由で ${releaseTag} を取得して書き込みます。よろしいですか?": "The aircraft will retrieve and write ${releaseTag} via Wi-Fi. Are you sure?", + "状態: このファームウェアには sanoTTS エンジンが入っていません (取得・書き込み不可)": "Status: This firmware does not contain the sanoTTS engine (cannot be retrieved or written to)", + "+ 自動保存 (再起動不要)。 100% = ボード ごとの工場既定値、200% でブースト。": "and saves automatically (no reboot). 100% is the board's factory default; 200% boosts it.", + "ws://192.168.1.10:8000/xiaozhi/v1/ (変更しない場合は空欄)": "ws://192.168.1.10:8000/xiaozhi/v1/ (leave blank if not changed)", + "で確定。 接続後、即反映する項目 (アバター DSL / LT) は個別ボタンが付いています。": "select Save & reboot. Settings that apply immediately (Avatar DSL and LT) have their own buttons.", + "(AP モードの場合) device の AP に Wi-Fi 接続し、このページを再度開く": "(In AP mode) connect to the device's AP and reopen this page", + "MCP API トークン (空欄なら変更しない / 「クリア」で空文字列を NVS に書く)": "MCP API token (leave blank to keep; Clear writes an empty string to NVS)", + "end に失敗 (${final.state}: ${final.error || ''})": "end failed (${final.state}: ${final.error || ''})", + "の右側 (読み) はひらがな/カタカナのみ\n (漢字は発声時に無視されます)。": "The right side (pronunciation) must use hiragana or katakana only; kanji are ignored during speech.", + "サーボを ${seCheckbox.checked ? '有効' : '無効'} に切り替え": "Switch servo to ${seCheckbox.checked ? '有効' : '無効'}", + "基板: ${StackchanSettings.boardLabel(boardKind)}": "Board: ${StackchanSettings.boardLabel(boardKind)}", + "音声エンジン (hts_engine, Modified BSD) と配布ボイスの帰属表示:": "Voice engine (hts_engine, Modified BSD) and distributed voice attribution:", + ") で更新されるので、\n 下の「読み込み」を押すと最新ターンの値が見えます。": "). Select Load below to view the latest turn.", + "HMM ボイス登録 (${Math.round(st.stored/1024)} KiB)": "HMM voice registration (${Math.round(st.stored/1024)} KiB)", + "で動作します。 Firefox・Safari は Web Bluetooth 非対応です。": ". Firefox and Safari do not support Web Bluetooth.", + "で確定 (再起動が必要な項目のみ)。\n LED や顔の DSL はリアルタイム反映。": "to apply reboot-required settings. LEDs and face DSL apply immediately.", + "にアクセスし、そちらで\n ファイル選択・OTA を実行してください。": "and perform file selection and OTA there.", + "リップシンク モードを ${labels[v] || `mode ${v}`} に切り替え": "Switch lip sync mode to ${labels[v] || `mode ${v}`}", + "手を近づけたときに 1 つ選んで話します (形式は上と同じ)。空なら吹き出しだけ出します。": "Choose and speak one phrase when a hand approaches (same format as above). If empty, only the speech bubble appears.", + "直近の会話 1 ターンぶんの音声パイプライン統計 (BLE chr 0x1f / HTTP": "Audio pipeline statistics for the most recent conversation turn (the same data as BLE chr 0x1f / HTTP", + "の形式。読みはひらがな/カタカナのみ (漢字は発声時に\n 無視されます)。": "Pronunciation must use hiragana or katakana only; kanji are ignored during speech.", + "実機をデフォルト顔に戻しました${r.raw ? ` (${r.raw})` : ''}": "Returned the actual machine to the default face ${r.raw ? ` (${r.raw})` : ''}", + "X25519 非対応のブラウザです (Chrome / Edge 133 以降が必要)": "Browser does not support X25519 (requires Chrome / Edge 133 or later)", + "audio metrics: JSON parse 失敗 (${e.message})": "audio metrics: JSON parse failed (${e.message})", + "どの会話バックエンドを使うかを切り替えます。キー / 設定は下記すべてに保存できます。": "Select the conversation backend. Keys and settings for every provider are stored below.", + "バージイン (頭タッチで会話中断): ${enabled ? '有効' : '無効'}": "Barge-in (conversation interrupted by touching the head): ${enabled ? '有効' : '無効'}", + "会話機能 ON 時は排他のため再生できません。OFF にして保存・再起動してください。": "When the conversation function is ON, it cannot be played because it is exclusive. Please turn it off, save and restart.", + "DSL コンパイラ バンドルが読込み失敗 (firmware ビルド不一致の可能性)": "DSL compiler bundle fails to load (possible firmware build mismatch)", + "DSL コンパイラ バンドルが読込み失敗 (オフライン file:// では未注入)": "DSL compiler bundle fails to load (not injected in offline file://)", + "HTTP API が有効化(Channel adapter から接続可)。空にすると": "When configured, the HTTP API is enabled and the Channel adapter can connect. When empty,", + "begin に失敗 (${st.state}: ${st.error || ''})": "begin failed (${st.state}: ${st.error || ''})", + "デバイスへ転送中… (${Math.round(f.size/1024)} KiB)": "Transferring to device... (${Math.round(f.size/1024)} KiB)", + "状態: voice パーティションなし (8 MB ボード / 旧パーティション表)": "Condition: No voice partition (8 MB board / old partition table)", + "OTA 開始: ${file.name} (${fmtBytes(total)})": "OTA start: ${file.name} (${fmtBytes(total)})", + "で確定 (再起動が必要な項目のみ)。 LED や顔の DSL はリアルタイム反映。": "to apply reboot-required settings. LEDs and face DSL apply immediately.", + "デバイスは更新モード (Recovery) で再起動し、Wi-Fi で取得します。": "The device will restart into update mode (Recovery) and retrieve over Wi-Fi.", + ") から取得します。LICENSE-MODEL.md の条件に同意していますか?": "). Do you agree to the terms of LICENSE-MODEL.md?", + "sanoTTS 重みを取得+インストール: ${release}/${file}": "Get sanoTTS weights + install: ${release}/${file}", + "こんにちは | こんにちわ\nスタックチャンです | すたっくちゃんです\nやっほー": "Hello | Hello\nThis is Stack Chan | This is Stack Chan\nYahoo", + "こんにちは | こんにちわ スタックチャンです | すたっくちゃんです やっほー": "Hello | Hello, this is Stack-chan | This is Stack-chan Yahoo", + "動作モード: ${labels[mode] || `mode ${mode}`}": "Operating mode: ${labels[mode] || `mode ${mode}`}", + "動作モードを ${labels[v] || `mode ${v}`} に切り替え": "Switch operation mode to ${labels[v] || `mode ${v}`}", + "準備完了 — Ctrl/Cmd+Enter または「コンパイル & 送信」で反映": "Ready — Apply with Ctrl/Cmd+Enter or \"Compile & Send\"", + "認証なし (空欄) に戻します。保存 & 再起動で反映されます。よろしいですか?": "Return to no authentication (blank). It will be reflected after saving & restarting. Are you sure?", + "(機体側が拒否 — Wi-Fi 未接続 / 取得中 など。状態を更新して確認)": "(Rejected by the aircraft - Wi-Fi not connected/obtaining, etc. Update the status and check)", + "このファームウェアには sanoTTS エンジンが入っていません (取得不可)": "This firmware does not include the sanoTTS engine (not available)", + "マイク リップシンク 調整 (動作モード = マイクリップシンク のとき有効)": "Microphone lip-sync tuning (active when Operation mode is MicLipSync)", + "マイクリップシンク 設定の書き込み失敗: ${e?.message ?? e}": "Failed to write My Clip Sync settings: ${e?.message ?? e}", + "リップシンク モード: ${labels[m] || `mode ${m}`}": "Lip sync mode: ${labels[m] || `mode ${m}`}", + "自動 (sanoTTS > HMM ボイス > 音声 DB > フォルマント)": "Auto (sanoTTS > HMM voice > voice DB > formant)", + "音声出力を ${labels[v] || `mode ${v}`} に切り替え": "Switch audio output to ${labels[v] || `mode ${v}`}", + "${label}送信失敗 — ${r.error || 'unknown'}": "${label} Transmission failure — ${r.error || 'unknown'}", + "Status 読み取り失敗 → デバイス側で認証パスワードが設定済の可能性。": "Status Read failure → Authentication password may have already been set on the device side.", + "Web Bluetooth 経由で Wi-Fi と API キーを設定します": "Configure Wi-Fi and the Gemini API key over Web Bluetooth", + "の右側 (読み) はひらがな/カタカナのみ (漢字は発声時に無視されます)。": "The right side (pronunciation) must use hiragana or katakana only; kanji are ignored during speech.", + "機体が GitHub Releases (ayutaz/sanoTTS-jp": "The aircraft is GitHub Releases (ayutaz/sanoTTS-jp", + "近接の閾値を適用 (near=${v.near} far=${v.far})": "Apply proximity threshold (near=${v.near} far=${v.far})", + ") で更新されるので、 下の「読み込み」を押すと最新ターンの値が見えます。": "). Select Load below to view the latest turn.", + "0x プレフィクス可。書込はセンサー再起動まで有効 (NVS 保存なし)。": "0x prefix is optional. Writes remain active until the sensor restarts and are not saved to NVS.", + "LED 口の動きと連動: ${enabled ? '有効' : '無効'}": "LED linked to mouth movements: ${enabled ? '有効' : '無効'}", + "Wi-Fi 音声 RTP (L16/AAC/μ-law, 会話機能オフ時)": "Wi-Fi audio RTP (L16/AAC/μ-law; Conversation off)", + "jtts アイドル発話: ${enabled ? '有効' : '無効'}": "jtts idle utterance: ${enabled ? '有効' : '無効'}", + "を選択してください (Stackchan AP への接続が維持されます)。": "to keep the connection to the Stackchan AP.", + "サイズ範囲外 (${buf.length} B、1..32768 が許容)": "Out of size range (${buf.length} B, 1..32768 is acceptable)", + "サーボ設定 書き込み完了 (${slCanonical.length}B)": "Servo setting writing completed (${slCanonical.length}B)", + "スピーカー音量 設定の書き込み失敗: ${e?.message ?? e}": "Failed to write speaker volume settings: ${e?.message ?? e}", + "マイクリップシンク 設定の取得失敗: ${e?.message ?? e}": "Failed to get My Clip Sync settings: ${e?.message ?? e}", + "完了 ${(bytesSent/1024).toFixed(1)} KiB": "Completed ${(bytesSent/1024).toFixed(1)} KiB", + "LED state 書き込み失敗: ${e?.message ?? e}": "LED state write failure: ${e?.message ?? e}", + "LED state 読み出し失敗: ${e?.message ?? e}": "LED state read failure: ${e?.message ?? e}", + "OTA 完了 — 機体が新ファームを起動し、起動確認まで自動で進みます。": "OTA Complete — The aircraft will boot the new firmware and automatically proceed to boot confirmation.", + "と同じデータ)。会話ターンが終わるたびに\n ファーム側 (": "). The firmware updates them when each turn ends (", + "の形式。読みはひらがな/カタカナのみ (漢字は発声時に 無視されます)。": "Pronunciation must use hiragana or katakana only; kanji are ignored during speech.", + "バッテリーゲージ表示: ${enabled ? '有効' : '無効'}": "Battery gauge display: ${enabled ? '有効' : '無効'}", + "配布ボイスをブラウザがダウンロードしてデバイスへ転送・インストールします": "The browser will download the distributed voice, transfer it to the device, and install it.", + "(このファームウェアは BLE からの sanoTTS 操作に未対応)": "(This firmware does not support sanoTTS operation from BLE)", + "sanoTTS 状態取得 失敗: ${e?.message ?? e}": "sanoTTS status acquisition failure: ${e?.message ?? e}", + "speaker-volume 書き込み失敗: ${e.message}": "speaker-volume write failure: ${e.message}", + "speaker-volume 読み出し失敗: ${e.message}": "speaker-volume read failure: ${e.message}", + "スピーカー音量 設定の取得失敗: ${e?.message ?? e}": "Failed to get speaker volume settings: ${e?.message ?? e}", + "デバイス: ${bleDevice.name || '(名称不明)'}": "Device: ${bleDevice.name || '(名称不明)'}", + "デバイス名を更新: ${dnEl.value || '(自動生成)'}": "Updated device name: ${dnEl.value || '(自動生成)'}", + "マイク リップシンク 調整 (会話・jtts どちらもオフのとき有効)": "Microphone lip-sync tuning (active when Conversation and JTTS are both off)", + "任意の HTS ボイス (.htsvoice、~1-2 MB)。読みに": "Any HTS voice (.htsvoice, about 1–2 MB). In the pronunciation,", + "認証パスワード送信 (${authEl.value.length}B)": "Send authentication password (${authEl.value.length}B)", + "送信例 (ffmpeg)。ブラウザは UDP を送れないのでコピー用:": "Example ffmpeg command. Browsers cannot send UDP, so copy this command:", + "Claude Code Channel (MCP) — トークン管理": "Claude Code Channel (MCP) — token management", + "RTP 音声受信: ${enabled ? '有効' : '無効'}": "RTP audio reception: ${enabled ? '有効' : '無効'}", + "http://192.168.1.10:8787 (空欄 = 無効)": "http://192.168.1.10:8787 (blank = invalid)", + "release から直接取得 (device side fetch)": "Download directly from a release (device-side fetch)", + "デバイスは更新モード (Recovery) で再起動して受信します。": "The device will reboot into update mode (Recovery) to receive it.", + "プリセット「${name}」を読込み — 実機を内蔵デフォルトに復帰": "Load preset “${name}” — restore the actual machine to the built-in default", + "${f.name} (${buf.length} B) を送信中…": "Sending ${f.name} (${buf.length} B)...", + "${f.name} 送信中… ${sent}/${total} B": "${f.name} Sending... ${sent}/${total} B", + "90 秒待っても取得が始まりません (Wi-Fi / タグを確認)": "Retrieval does not start even after waiting 90 seconds (Check Wi-Fi/tag)", + "OTA 開始: ${fmtBytes(bytes.length)}": "OTA start: ${fmtBytes(bytes.length)}", + "jtts アイドル発話 状態の取得失敗: ${e.message}": "jtts idle utterance status acquisition failure: ${e.message}", + "jtts 設定読み込み: ${jttsBytes.length}B": "Load jtts settings: ${jttsBytes.length}B", + "mic-lip-gain 書き込み失敗: ${e.message}": "mic-lip-gain write failure: ${e.message}", + "mic-lip-gain 読み出し失敗: ${e.message}": "mic-lip-gain read failure: ${e.message}", + "sanoTTS: release と file を入力してください": "sanoTTS: Please enter release and file", + "で Web Bluetooth 接続 → タブを切り替えて設定 →": "via Web Bluetooth, switch tabs, configure settings, then select", + "にアクセスし、そちらで ファイル選択・OTA を実行してください。": "and perform file selection and OTA there.", + "アバター画面の左上に残量ゲージを表示します(保存後の再起動で反映)": "Shows the battery gauge in the top-left corner of the avatar screen. Takes effect after saving and rebooting.", + "アバター調整を読み込み: ${faceBytes.length}B": "Load avatar adjustments: ${faceBytes.length}B", + "インストール HTTP ${up.status}: ${body}": "Install HTTP ${up.status}: ${body}", + "マイク AGC: ${enabled ? '有効' : '無効'}": "Mike AGC: ${enabled ? '有効' : '無効'}", + "読込みました — 「コンパイル & 送信」で実機に反映してください": "Loaded — Please use “Compile & Send” to reflect on the actual device", + "起動アルペジオ: ${enabled ? '有効' : '無効'}": "Starting arpeggio: ${enabled ? '有効' : '無効'}", + "音声 DB を削除してフォルマント合成に戻します。よろしいですか?": "Delete the audio DB and revert to formant synthesis. Are you sure?", + "音声出力: ${labels[v] || `mode ${v}`}": "Audio output: ${labels[v] || `mode ${v}`}", + "Apply 完了 — デバイスは ~200ms 後に再起動します": "Apply completed — device will reboot after ~200ms", + "Avatar DSL プリセット ${name} (reset)": "Avatar DSL preset ${name} (reset)", + "LT 設定 書き込み完了 (${ltJson.length}B)": "LT setting write complete (${ltJson.length}B)", + "jtts 試し発話 失敗: ${e?.message ?? e}": "jtts trial utterance failure: ${e?.message ?? e}", + "リップシンク モード 状態の取得失敗: ${e.message}": "Failed to get lip sync mode status: ${e.message}", + "会話ステータスの LED 設定に失敗: ${e.message}": "Failed to set conversation status LED: ${e.message}", + "正式版以外 (alpha / beta / rc) も一覧に出す": "Also list non-official versions (alpha / beta / rc)", + "現在値がありません (接続してリアルタイム更新を待ってください)": "No current value (please connect and wait for real-time updates)", + "Apply chr が無いので適用はスキップ(古いファーム?)": "Skip applying because there is no Apply chr (old farm?)", + "Avatar DSL 送信 OK (${r.saved} B)": "Avatar DSL transmission OK (${r.saved} B)", + "Status 通知の復号エラー: ${err.message}": "Error decoding Status notification: ${err.message}", + "firmware.bin を選んでください (ZIP 非対応)": "Please select firmware.bin (ZIP not supported)", + "jtts 設定を更新 (${payload.length}B)": "Update jtts configuration (${payload.length}B)", + "サーボ原点・可動範囲 (Takao base 等の自作版向け)": "Servo center and range (custom bases such as Takao base)", + "Characteristics 取得完了 (missing:": "Characteristics acquisition completed (missing:", + "DSL コンパイラ が読込めていないためコンパイルできません": "Cannot compile because DSL compiler cannot be loaded.", + "LT 設定を読み込み: ${ltBytes.length}B": "Load LT settings: ${ltBytes.length}B", + "audio metrics: JSON parse 失敗 (": "audio metrics: JSON parse failed (", + "jtts 試し発話 失敗: HTTP ${r.status}": "jtts trial utterance failure: HTTP ${r.status}", + "release ${tag} を device 側で取得中…": "Obtaining release ${tag} on device side...", + "sanoTTS アップロード失敗: ${e.message}": "sanoTTS upload failure: ${e.message}", + "— 取得完了: ${f.release}/${f.file}": "— Acquisition completed: ${f.release}/${f.file}", + "サーボ設定を読み込み: ${slBytes.length}B": "Load servo settings: ${slBytes.length}B", + "プレビュー描画中 (現行 C++ コードを WASM 実行)": "Preview drawing (WASM execution of current C++ code)", + "会話機能: ${enabled ? '有効' : '無効'}": "Conversation function: ${enabled ? '有効' : '無効'}", + "適用しました — near / far を決め直してください": "Applied — please redefine near/far", + "ESP-NOW リモコン受信 (EspNowRemote)": "ESP-NOW remote input (EspNowRemote)", + "HTTP 経由で Wi-Fi と API キーを設定します": "Configure Wi-Fi and the Gemini API key over HTTP", + "fetch に失敗 (${st.error || ''})": "fetch failed (${st.error || ''})", + "sanoTTS-jp (重み必須、無ければフォールバック)": "sanoTTS-jp (weights required; falls back if unavailable)", + "— 取得中: ${f.release}/${f.file}": "— Obtaining: ${f.release}/${f.file}", + "「ファイル」App で ZIP をタップして展開 → 中の": "Tap the ZIP in the Files app to extract it. The", + "を省くと表示と読みが同じになります。\n 例:": "If you omit it, the screen text and pronunciation are the same. Example:", + "サーボ: ${enabled ? '有効' : '無効'}": "Servo: ${enabled ? '有効' : '無効'}", + "バッテリーゲージ設定の取得失敗: ${e.message}": "Failed to get battery gauge settings: ${e.message}", + "マイク AGC 状態の取得失敗: ${e.message}": "Failed to get microphone AGC status: ${e.message}", + "保存済み (${kb(s.stored)}) だが未ロード": "Saved (${kb(s.stored)}) but not loaded", + "未接続か、この firmware は近接センサー非対応です": "Not connected or this firmware does not support proximity sensor", + "空欄や変更なしで「保存」を押すと現在の設定は維持されます。": "Leave the field empty or unchanged and select Save to keep the current setting.", + "LED 連動 状態の取得失敗: ${e.message}": "LED interlocking status acquisition failure: ${e.message}", + "Status 読み取りエラー: ${e.message}": "Status read error: ${e.message}", + "です。\n このページは device 自身の AP": "This page is being served through the device's own AP.", + "と同じデータ)。会話ターンが終わるたびに ファーム側 (": "). The firmware updates them when each turn ends (", + "はマイク感度が低めなので入力ゲインを上げると見栄えします": "Since the microphone sensitivity is low, increasing the input gain will improve the appearance.", + "未接続か、この firmware は調整モード非対応です": "Not connected or this firmware does not support adjustment mode", + "状態: sanotts 領域なし (旧パーティション表)": "Status: sanotts no space (old partitioned table)", + "現在値が無いので「現在値を表示」を先に有効にしてください": "Since there is no current value, please enable \"Show current value\" first.", + "起動アルペジオ設定の取得失敗: ${e.message}": "Failed to get startup arpeggio settings: ${e.message}", + "近接センサー調整 の取得失敗: ${e.message}": "Failed to get proximity sensor adjustment: ${e.message}", + "鍵交換完了 (X25519 + AES-256-GCM)": "Key exchange completed (X25519 + AES-256-GCM)", + "静かな部屋でも騒音下でも声に反応するよう感度を調整します": "Adjust the sensitivity to respond to your voice in a quiet room or in noisy conditions", + "Avatar DSL 送信失敗: ${r.error}": "Avatar DSL transmission failure: ${r.error}", + "ESP-NOW 姿勢配信 (EspNowSender)": "ESP-NOW pose output (EspNowSender)", + "RTP 有効状態の取得失敗: ${e.message}": "Failed to get RTP enable state: ${e.message}", + "エージェント サーバー URL (ask_agent)": "Agent server URL (ask_agent)", + "サーボの取り付け向きや可動範囲はベース基板で異なります": "The mounting direction and movable range of the servo vary depending on the base board.", + "バージイン 状態の取得失敗: ${e.message}": "Failed to get barge-in status: ${e.message}", + "動作モード 状態の取得失敗: ${e.message}": "Operation mode Status acquisition failure: ${e.message}", + "ESP-NOWリモコン (EspNowRemote)": "ESP-NOW remote", + "ESP-NOW姿勢配信 (EspNowSender)": "ESP-NOW pose send", + "GitHub Release から選ぶか、ローカルの": "Choose a GitHub Release or select a local", + "HMM 合成 (ボイス必須、無ければフォールバック)": "HMM synthesis (voice required; falls back if unavailable)", + "SSID 読み取りエラー: ${e.message}": "SSID read error: ${e.message}", + "jtts 設定の取得失敗: ${e.message}": "Failed to get jtts settings: ${e.message}", + "jtts 試し発話 失敗: ${e.message}": "jtts trial utterance failure: ${e.message}", + "release OTA 完了 — 新ファームに再起動": "release OTA completed — reboot to new farm", + "sanoTTS 取得失敗: ${e.message}": "sanoTTS acquisition failure: ${e.message}", + "は 404 を返します。保存後の再起動で反映されます": "returns 404. Takes effect after saving and rebooting.", + "未保存 (領域 ${kb(s.capacity)})": "Unsaved (area ${kb(s.capacity)})", + "近接時の発話フレーズ (一行 1 つ、CoreS3)": "Proximity speech phrases (one per line, CoreS3)", + "重みのライセンスは MIT ではありません。取得前に": "The weights are not MIT-licensed. Before downloading, review", + "音声出力 状態の取得失敗: ${e.message}": "Failed to get audio output status: ${e.message}", + "(空欄なら変更しない / クリアで認証なしに戻す)": "(If blank, do not change / Clear to return to no authentication)", + "Android で「ファイル選択」が反応しない場合": "If File selection does not respond on Android", + "jtts 試し発話: 192 バイトを超えています": "jtts trial utterance: exceeds 192 bytes", + "tag を入力してください (例: v0.7.3)": "Please enter tag (e.g. v0.7.3)", + "— 取得失敗: ${f.error || '?'}": "— Failed to get: ${f.error || '?'}", + "⋮ メニュー →「このネットワークをこのまま使用」": "⋮ menu and select Use this network as is", + "が有効化(adapter から接続可)。空にすると": "is enabled and the Channel adapter can connect. When empty,", + "はセンサー内蔵テストパターンで転送経路を検証します": "verifies the transfer path with a sensor built-in test pattern", + "アバター調整の取得失敗: ${e.message}": "Failed to obtain avatar adjustment: ${e.message}", + "アバター調整の送信失敗: ${e.message}": "Failed to send avatar adjustment: ${e.message}", + "デバイス情報の取得失敗: ${e.message}": "Failed to get device information: ${e.message}", + "ドロップダウンで選ぶと右のテキスト欄に反映されます": "Select from the dropdown and it will be reflected in the text field on the right.", + "取得に失敗 (${st.error || ''})": "Failed to get (${st.error || ''})", + "状態: 保存済み・未ロード (PSRAM 不足?)": "Status: saved/not loaded (insufficient PSRAM?)", + "JTTS ランダム発話 (JttsRandom)": "JTTS random speech (JttsRandom)", + "LT 設定の取得失敗: ${e.message}": "Failed to get LT settings: ${e.message}", + "release fetch 開始: ${tag}": "release fetch start: ${tag}", + "このデバイスは認証パスワードが設定されています。": "An authentication password is set on this device.", + "です。 このページは device 自身の AP": "This page is being served through the device's own AP.", + "アクセサリ 2〜8 (DSL 用の予備スロット)": "Accessories 2–8 (reserved DSL slots)", + "オフセットは機体ごとに違うので既定値にはしません": "The offset is different for each aircraft, so do not set it as the default value.", + "サーボ設定の取得失敗: ${e.message}": "Failed to get servo settings: ${e.message}", + "ダウンロード HTTP ${dl.status}": "Download HTTP ${dl.status}", + "デバイス名 取得失敗: ${e.message}": "Failed to get device name: ${e.message}", + "デバイス名: ${dn || '(自動生成)'}": "Device name: ${dn || '(自動生成)'}", + "他者が再利用できる素材としての配布には使えません": "Cannot be distributed as material that can be reused by others", + "空欄にすると組み込みの既定プロンプトを使用します": "Leave blank to use built-in default prompt", + "範囲設定モード: ON (サーボの力が抜けます)": "Range setting mode: ON (servo power is released)", + "近接 状態の取得失敗: ${e.message}": "Failed to get proximity state: ${e.message}", + "連続 (上の行から順に、最後まで行ったら先頭へ)": "Sequential (top to bottom, then restart at the first line)", + "Apply エラー: ${e.message}": "Apply error: ${e.message}", + "Recovery の広告を待って再接続します…": "Wait for the Recovery ad and reconnect...", + "このデバイスは認証パスワードが設定されています": "This device has an authentication password set", + "では全てのチャネルが復号失敗で動かなくなります": "Then all channels will stop working due to decoding failure.", + "は 404 を返します(即時反映・再起動不要)": "returns 404. Applies immediately; no reboot needed.", + "スライダーを動かすと下のプレビューと、接続中は": "Moving a slider updates the preview below and, while connected, the", + "プロバイダ取得失敗: ${e.message}": "Failed to obtain provider: ${e.message}", + "マイクでリップシンク (MicLipSync)": "Mic lip sync (MicLipSync)", + "単位連結 (DB 必須、無ければフォルマント)": "Unit concatenation (database required; falls back to formant)", + "周辺の雑音レベルを自動追跡して感度を調整します": "Automatically track ambient noise level and adjust sensitivity", + "基板種別の取得失敗: ${e.message}": "Failed to get board type: ${e.message}", + "有効状態の取得失敗: ${e.message}": "Failed to get valid state: ${e.message}", + "望む距離で十分な差が出る組み合わせを探してから": "After searching for a combination that provides a sufficient difference at the desired distance,", + "認証パスワードがキャンセルされたため接続を中断": "Connection interrupted because authentication password was canceled", + "送信中… ${sent}/${total} B": "Sending... ${sent}/${total} B", + "顔の調整 (スライダー & ライブプレビュー)": "Face tuning (sliders and live preview)", + "🤖 Stack-chan 設定 (Wi-Fi)": "🤖 Stack-chan settings (Wi-Fi)", + "# AAC (圧縮) udp/6972": "# AAC (compressed) udp/6972", + "(機体の Wi-Fi 経由、10〜30 秒)": "(via aircraft Wi-Fi, 10-30 seconds)", + "Apply 完了 — デバイスが再起動します": "Apply Done — Device will restart", + "AvatarBytecode chr 未接続": "AvatarBytecode chr Not connected", + "Chrome / Edge (デスクトップ)": "Works in desktop Chrome / Edge", + "HMM ボイス: ファイルを選択してください": "HMM Voice: Please select file", + "HMM ボイスを削除します。よろしいですか?": "Delete the HMM voice. Are you sure?", + "flush 中に session が消失 (": "session disappears during flush (", + "recv_lag (event 渡し) μs": "recv_lag (event passing) μs", + "sanoTTS-jp (ニューラル TTS)": "sanoTTS-jp (neural TTS)", + "speaker-volume 書き込み失敗:": "speaker-volume write failure:", + "speaker-volume 読み出し失敗:": "speaker-volume read failure:", + "こんにちは | こんにちわ\nおはよー\n...": "Hello | Hello\nGood morning\n...", + "こんにちは | こんにちわ おはよー ...": "Hello | Hello, good morning...", + "の前段を変えると生の値のスケールが変わります": "Changing the first half of will change the scale of the raw value.", + "プリセットを選ぶとソースを読込んで実機に送信": "When you select a preset, the source is loaded and sent to the actual device.", + "リリース タグとファイル名を入力してください": "Enter release tag and filename", + "リリース取得失敗: ${e.message}": "Failed to get release: ${e.message}", + "合成音声はつくよみちゃんコーパスの規約により": "The synthesized speech is based on the Tsukuyomi-chan corpus rules.", + "状態: 保存済み・未ロード (メモリ不足?)": "Status: Saved/Not loaded (Insufficient memory?)", + "経由で新ファームを別パーティションに書き込み": "Write the new firmware to another partition via", + "音声ストリーミング非対応のファームウェアです": "The firmware does not support audio streaming.", + "音声ファイル再生 (BLE ストリーミング)": "Audio file playback (BLE streaming)", + "# L16 (非圧縮) udp/6970": "# L16 (uncompressed) udp/6970", + "${label}送信完了 — レスポンス:": "${label} Transmission completed — Response:", + "(NVS のみ書込、live 反映は失敗)": "(Writing only to NVS, live reflection failed)", + "MCP API トークン 書き込み完了 (": "MCP API token write completed (", + "OTA エラー: ${e.message}": "OTA error: ${e.message}", + "Stack-chan 設定 (Wi-Fi)": "Stack-chan settings (Wi-Fi)", + "recv→queued (再生投入) ms": "recv→queued (reproduction input) ms", + "sanoTTS 重みを取得+インストール:": "Get sanoTTS weights + install:", + "⚠ SoftAP プロビジョニング モード": "⚠ SoftAP provisioning mode", + "」を読込み — 実機を内蔵デフォルトに復帰": "” — restores the actual device to the built-in defaults", + "どの会話バックエンドを使うかを切り替えます": "Switch which conversation backend to use", + "は暗号鍵にパスワードハッシュが混入するので": "Since the password hash is mixed into the encryption key,", + "を入れました — 「センサーに適用」で反映": "— reflected in “Apply to sensor”", + "を省くと表示と読みが同じになります。 例:": "If you omit it, the screen text and pronunciation are the same. Example:", + "アイドル時の発話フレーズ (一行 1 つ)": "Idle speech phrases (one per line)", + "アバター画面の左上に残量ゲージを表示します": "Displays the remaining fuel gauge at the top left of the avatar screen", + "デバイス側で認証パスワードが設定済の可能性": "Possibility that an authentication password has already been set on the device side", + "ボイス取得+インストール完了: ${id}": "Voice acquisition + installation completed: ${id}", + "レベル メーター (LevelMeter)": "Level meter (LevelMeter)", + "会話のステータスをネコミミ LED で表す": "Show conversation status on cat-ear LEDs", + "会話モードで画面右上のステータスに合わせて": "In conversation mode, according to the status at the top right of the screen", + "標準 (v2 — 声門波 + カスケード)": "Standard (v2 — glottal pulse + cascade)", + "機体に ${tag} の取得を指示します…": "Instruct the aircraft to obtain ${tag}...", + "重みは本ファームウェアには同梱していません": "Weights are not included in this firmware.", + "# L16 (非圧縮) udp/6970": "# L16 (uncompressed) udp/6970", + "Recovery に再接続できません (": "Unable to reconnect to Recovery (", + "jtts アイドル発話 状態の取得失敗:": "jtts idle utterance state acquisition failure:", + "jtts 試し発話: 「${text}」": "jtts trial utterance: “${text}”", + "mic-lip-gain 書き込み失敗:": "mic-lip-gain write failure:", + "mic-lip-gain 読み出し失敗:": "mic-lip-gain read failure:", + "sanoTTS 取得: ${state}": "sanoTTS acquisition: ${state}", + "sanoTTS 重みを取得+保存しました": "sanoTTS weights retrieved + saved", + "が読込めていないためコンパイルできません": "Cannot be compiled because it cannot be read.", + "ここでは既定時間と通知ワードを設定します": "Set the default time and notification word here", + "はサーバー側の人格を使うため無視されます": "is ignored because it uses server-side personality", + "は暗号鍵に混入されるため次回接続時に反映": "is mixed into the encryption key, so it will be reflected the next time you connect.", + "を取得して書き込みます。よろしいですか?": "Get and write. Are you sure?", + "ネコミミ LED を口の動きと連動させる": "Link cat-ear LEDs to mouth movement", + "マイクリップシンク 設定の書き込み失敗:": "Failed to write My Clip Sync settings:", + "マイク入力の音量に合わせて口を開閉します": "Open and close your mouth according to the volume of the microphone input", + "別パーティションに書き込んで再起動します": "Write to another partition and reboot", + "接続してリアルタイム更新を待ってください": "Connect and wait for real-time updates", + "機体がダウンロード中… (30 秒程度)": "The aircraft is downloading... (about 30 seconds)", + "現在の SSID: ${ssidStr}": "Current SSID: ${ssidStr}", + "空欄のままにすると現在の値を変更しません": "Leave blank to keep the current value", + "自動ゲイン制御 (AGC) を有効にする": "Enable automatic gain control (AGC)", + "追加 HTTP ヘッダ (会話 API)": "Additional HTTP headers (conversation API)", + "音声 DB: ファイルを選択してください": "Audio DB: Please select file", + "頭タッチで会話を中断する (バージイン)": "Interrupt conversation with a head touch (barge-in)", + "# AAC (圧縮) udp/6972": "# AAC (compressed) udp/6972", + "HMM ボイス (.htsvoice)": "HMM voice (.htsvoice)", + "Recovery に再接続、鍵交換完了": "Reconnect to Recovery, key exchange completed", + "XiaoZhi トークン 書き込み完了": "XiaoZhi token writing completed", + "pcm_lag 未再生 samples": "pcm_lag unplayed samples", + "sanoTTS の重みを削除しますか?": "Do you want to remove sanoTTS weights?", + "spk_queue 深度 (0..2)": "spk_queue depth (0..2)", + "なあに? | なあに\nちかいよ\n...": "What? | Hey!\nIt's close\n...", + "なあに? | なあに ちかいよ ...": "What? | Hey, come on...", + "に保存されるので再起動後も復元されます": "It will be saved even after rebooting.", + "はマイク入力に合わせて口だけ動かします": "moves only the mouth according to the microphone input", + "を押した時点で機体に反映・保存されます": "It will be reflected and saved on the aircraft as soon as you press .", + "ビブラート depth (cents)": "Vibrato depth (cents)", + "リップシンク モード 状態の取得失敗:": "Failed to get lip sync mode status:", + "ローカル音声認識 (AsrLocal)": "On-device speech recognition (AsrLocal)", + "会話ステータスの LED 設定に失敗:": "Failed to set conversation status LED:", + "会話応答 (Conversation)": "Conversation", + "取り込んだ値は上の入力欄に反映されます": "The imported value will be reflected in the input field above.", + "口の動きに合わせて LED を制御する": "Control LEDs with mouth movement", + "応答待ち / ツール / エージェント": "Waiting / tool / agent", + "新ファームの起動確認は自動で行われます": "The startup confirmation of the new firmware will be performed automatically.", + "超過の繰返し間隔 (秒、0=1回のみ)": "Overtime repeat interval (seconds; 0 = once)", + "(例)こんにちは すたっくちゃん です": "(Example) Hello, this is Stuck-chan.", + "(設定済み / 変更しない場合は空欄)": "(Leave blank if set/no change)", + "(クリア予約 — 保存で空欄を送信)": "(Clear Reservation — Save and Submit Blank)", + "(設定済 — 変更しない場合は空欄)": "(Already set — leave blank if not changed)", + ") を書き込みます。よろしいですか?": "). Are you sure?", + "AIza… (変更しない場合は空欄)": "AIza… (Leave blank if not changed)", + "Avatar DSL リセット失敗:": "Avatar DSL reset failure:", + "Avatar DSL 送信 OK (": "Avatar DSL transmission OK (", + "BLE 接続中 (Recovery)": "Connecting to BLE (Recovery)", + "OTA 完了 — 新ファームに再起動": "OTA completed — reboot to new farm", + "XiaoZhi URL 書き込み完了": "XiaoZhi URL writing completed", + "jtts 試し発話 失敗: HTTP": "jtts trial utterance failure: HTTP", + "がインターネットに繋がっていなくても": "even if you are not connected to the internet", + "このチャンネルにリリースはありません": "There are no releases on this channel", + "は 0..255 で指定してください": "Please specify as 0..255.", + "を削除してフォルマント合成に戻します": "Delete and return to formant synthesis", + "クラシック (旧方式 — ブザー声)": "Classic (legacy buzzer voice)", + "ゲインを上げたときは先にこれをやると": "When increasing the gain, do this first.", + "スピーカー音量 設定の書き込み失敗:": "Failed to write speaker volume settings:", + "スライダーを動かすと下のプレビューと": "Move the slider to see the preview below.", + "デバイス名 (BLE / mDNS)": "Device name (BLE / mDNS)", + "バージイン (頭タッチで会話中断):": "Barge-in (conversation interrupted by touching head):", + "ブラウザ側にインターネット接続が必要": "Internet connection required on browser side", + "マイクリップシンク 設定の取得失敗:": "Failed to get My Clip Sync settings:", + "会話ステータスの LED 設定を適用": "Applied conversation LED settings", + "削除失敗: ${e.message}": "Delete failed: ${e.message}", + "単位連結 音声 DB (.jvox)": "Unit-concatenation voice DB (.jvox)", + "接続失敗: ${e.message}": "Connection failure: ${e.message}", + "状態: 未登録 (フォルマント合成)": "Status: Unregistered (formant synthesis)", + "空のパスワード — 認証に進めません": "Empty password — unable to proceed with authentication", + "色 (solid / breath)": "Color (solid / breathing)", + "診断: 最初の 64 書き込み 平均": "Diagnosis: first 64 writes average", + "近接センサー調整を適用 (gain=": "Apply proximity sensor adjustment (gain=", + "顔描画 DSL (構造ごと差し替え)": "Face drawing DSL (replace the whole structure)", + "(アクセント句切り) を書けます。": "(accent phrase break) can be written.", + "Avatar DSL 送信 失敗:": "Avatar DSL transmission failure:", + "LED state 書き込み失敗:": "LED state write failure:", + "LED state 読み出し失敗:": "LED state read failure:", + "Save & reboot": "Save & reboot", + "release fetch 開始:": "release fetch start:", + "sanoTTS アップロード失敗:": "sanoTTS upload failure:", + "sanoTTS 重みの取得を開始:": "Start retrieving sanoTTS weights:", + "sanoTTS 重みをアップロード": "Upload sanoTTS weights", + "sanoTTS 重みを削除しました": "Removed sanoTTS weights", + "sk-… (変更しない場合は空欄)": "sk-… (leave blank if not changed)", + "いずれの口元アニメーションでも有効": "Valid for any mouth animation", + "ここでは既定時間と通知ワードを設定": "Here you can set the default time and notification word.", + "まだサーボ位置を取得できていません": "Not able to get servo position yet", + "を省くと表示と読みが同じになります": "If you omit , the display and reading will be the same.", + "を選ぶとその状態では切り替えません": "If you select , you will not be able to switch in that state.", + "システムプロンプト (会話の人格)": "System prompt (conversation persona)", + "タグとファイル名を入力してください": "Please enter tag and file name", + "パスワード マネージャから自動入力": "Autofill from password manager", + "会話機能が有効なため使用できません": "Cannot be used because conversation function is enabled", + "内蔵スピーカ (Internal)": "Built-in speaker (Internal)", + "息成分 (breathiness)": "Breathiness", + "画面上辺のセンサーに手を近づけると": "When you bring your hand close to the sensor at the top of the screen", + "経由で実機に送信して即時反映します": "Send it to the actual device via", + "設定は即時保存・音自体は次回起動時": "Settings are saved instantly and the sound itself is saved the next time you start up.", + "進捗停止 (STA リンク不調?)": "Progress stopped (STA link problem?)", + "(任意 / 変更しない場合は空欄)": "(Optional/blank if no change)", + "(未設定 — 空欄なら認証なし)": "(Not set — no authentication if blank)", + "Avatar DSL プリセット": "Avatar DSL presets", + "Avatar DSL 送信 OK": "Avatar DSL transmission OK", + "Avatar DSL 送信失敗:": "Avatar DSL transmission failure:", + "Gemini キー 書き込み完了": "Gemini key writing completed", + "OpenAI キー 書き込み完了": "OpenAI key writing completed", + "Status 通知の復号エラー:": "Error decoding Status notification:", + "Status 通知を登録しました": "Status notification registered", + "XiaoZhi サーバー URL": "XiaoZhi server URL", + "jtts 設定: ファーム既定値": "jtts settings: farm defaults", + "sanoTTS 状態取得 失敗:": "sanoTTS status acquisition failure:", + "sanoTTS-jp 音声モデル": "sanoTTS-jp voice model", + "が切断されたら再接続してください": "Please reconnect if disconnected", + "のモード・色・明るさを設定します": "Set the mode, color, and brightness of", + "は本トグルとは独立で常に有効です": "is independent of this toggle and is always valid", + "は英数字とハイフンに丸められます": "is rounded to alphanumeric characters and hyphens", + "を押すと最新ターンの値が見えます": "Press to see the value of the latest turn", + "を押すと現在の設定は維持されます": "Press to keep the current settings", + "エージェント サーバー トークン": "Agent server token", + "キーを盗聴される可能性があるので": "The key may be intercepted, so", + "スピーカー音量 設定の取得失敗:": "Failed to get speaker volume settings:", + "センサが敏感で誤動作しやすいので": "The sensor is sensitive and prone to malfunction.", + "センサー レジスタ (色調整用)": "Sensor registers (color tuning)", + "ターンぶんの音声パイプライン統計": "Turnbun audio pipeline statistics", + "バッテリーゲージ設定の取得失敗:": "Failed to get battery gauge settings:", + "パスワードが違うため接続を中断:": "Connection aborted due to wrong password:", + "マイク AGC 状態の取得失敗:": "Failed to get microphone AGC status:", + "ローカル ファイル アップロード": "local file upload", + "会話機能オン時は自動的に無効です": "Automatically disabled when conversation function is on", + "公式 Releases から取得": "Download from official Releases", + "取得するボイスを選択してください": "Please select the voice you want to get", + "失敗: ${e.message}": "Failed: ${e.message}", + "完了 — device 再起動中": "Completed — device rebooting", + "接続すると自動で更新が始まります": "Once connected, the update will start automatically.", + "接続は維持したままにしてください": "Stay Connected", + "時間ちょうど 読み (ひらがな)": "On-time pronunciation (hiragana)", + "設定済 — 変更しない場合は空欄": "set — leave blank to keep", + "顔描画 DSL (Avatar)": "Face drawing DSL (Avatar)", + "${id} をダウンロード中…": "Downloading ${id}...", + "Apply を送信しています…": "Sending Apply...", + "Avatar DSL リセット": "Avatar DSL Reset", + "B、1..32768 が許容)": "B, 1..32768 is acceptable)", + "Gemini key 設定済み": "Gemini key already set", + "HMM ピッチシフト [半音]": "HMM pitch shift [semitones]", + "LED 連動 状態の取得失敗:": "Failed to get LED interlock status:", + "OpenAI key 設定済み": "OpenAI key set", + "Recovery に再接続中…": "Reconnecting to Recovery...", + "Recovery へ再起動中…": "Rebooting to Recovery...", + "Status 読み取りエラー:": "Status read error:", + "Wi-Fi 設定 (mDNS)": "Wi-Fi settings (mDNS)", + "release OTA 失敗:": "release OTA failure:", + "ℹ️ タブを切り替えて設定 →": "ℹ️ Switch tabs, configure settings, then", + "うれしい顔になって近接フレーズ": "Proximity phrase with happy face", + "このネットワークをこのまま使用": "Use this network as is", + "にして保存・再起動してください": "Please save and restart", + "に保存されるので再起動後も維持": "will be saved even after restarting.", + "を device 側で取得中…": "Retrieving on device side...", + "を上げるか連続時間を伸ばします": "increase or extend continuous time", + "を切ってからやり直してください": "Please turn off and try again.", + "アバター調整: ファーム既定値": "Avatar Adjustment: Farm Defaults", + "インターネット接続がありません": "no internet connection", + "ダウンの場合はエラーになるので": "If it is down, an error will occur.", + "バッテリーゲージ設定の取得失敗": "Failed to get battery gauge settings", + "バッテリー残量ゲージを表示する": "Show battery gauge", + "パスワードが違うため接続を中断": "Connection interrupted due to wrong password", + "ビブラート rate (Hz)": "Vibrato rate (Hz)", + "ファームウェア更新 (OTA)": "Firmware update (OTA)", + "フリガナを直接入力してください": "Please enter the furigana directly", + "ページ下部の保存でも送られます": "It can also be sent by saving at the bottom of the page.", + "ボイス取得+インストール完了:": "Voice acquisition + installation completed:", + "ランダム (毎回どれかを選ぶ)": "Random (choose one each time)", + "保存・再起動すると利用できます": "You can use it by saving and restarting.", + "周辺の雑音レベルを自動追跡して": "Automatically tracks ambient noise levels", + "実機をデフォルト顔に戻しました": "I returned the actual device to its default face.", + "家のパソコンで動くエージェント": "agent that runs on your home computer", + "手で動かした位置を取り込めます": "You can import the position moved by hand.", + "既定値を入れました (未適用)": "Defaults loaded (not applied)", + "無効にするとサーボ電源を入れず": "If disabled, the servo power will not be turned on.", + "版の設定ツールを使ってください": "Please use the version settings tool", + "経由でストリーミング送信します": "Stream it via", + "虹 (gradient) 周期": "Rainbow period", + "設定は下記すべてに保存できます": "Settings can be saved to all", + "設定ページの両方で認証が必要に": "Authentication is now required on both settings pages", + "認証パスワードは一時的に無効化": "The authentication password is temporarily disabled", + "起動アルペジオ設定の取得失敗:": "Failed to get startup arpeggio settings:", + "輝度 (Brightness)": "Brightness", + "近接の設定を送信 (near=": "Send proximity settings (near=", + "近接の閾値を適用 (near=": "Apply a proximity threshold (near=", + "近接センサー調整 の取得失敗:": "Failed to obtain proximity sensor calibration:", + "重みファイルを選択してください": "Please select a weight file", + "音声ファイルを選択してください": "Please select an audio file", + "🤖 Stack-chan 設定": "🤖 Stack-chan settings", + "${label} (現在)": "${label} (current)", + "Female (子供っぽい)": "Female (childlike)", + "LT 設定 書き込み完了 (": "LT Settings Writing completed (", + "LT 設定: ファーム既定値": "LT Settings: Farm Defaults", + "LT 設定を送信 (即反映)": "Send LT settings (apply immediately)", + "MCP API トークン送信": "MCP API Token Send", + "Pitch 原点 (raw)": "Pitch center (raw)", + "RTP 有効状態の取得失敗:": "Failed to get RTP enable state:", + "RTP 音声受信を有効にする": "Enable RTP audio input", + "Wi-Fi RTP 音声受信": "Wi-Fi RTP audio input", + "XiaoZhi URL 送信": "XiaoZhi URL Send", + "XiaoZhi トークン送信": "XiaoZhi token sending", + "avatar_init 失敗": "avatar_init failed", + "ms), 状態読み取り 平均": "ms), status reading average", + "— tag 一覧を読込中 —": "— Loading release tags —", + "— テキストだけ反映しました": "— Only the text is reflected", + "から重みを取得して保存します": "Get the weights from and save them", + "にファイルを用意する必要なし": "No need to prepare files for", + "のこり じゅうごびょう です": "The rest is Jyugobyo.", + "のときは設定した明るさで固定": "When , the brightness is fixed at the set brightness.", + "の両方で認証が必要になります": "Authentication is required for both", + "アイドル時の声 (jtts)": "Idle voice (JTTS)", + "アイドル時の発話を有効にする": "Enable idle speech", + "アクセサリ 1 (首輪と鈴)": "Accessory 1 (collar and bell)", + "アバターの顔の描画ロジックを": "Avatar face drawing logic", + "サーボ設定 書き込み完了 (": "Servo setting writing completed (", + "サーボ設定: ファーム既定値": "Servo settings: Farm default values", + "デスクトップで開いてください": "Please open it on your desktop", + "バージイン 状態の取得失敗:": "Failed to get barge-in status:", + "会話プロバイダ・API キー": "Conversation provider and API key", + "保存後の再起動で切替わります": "It will change when you restart after saving.", + "保存後の再起動で反映されます": "It will be reflected after rebooting after saving", + "信頼できないネットワークでは": "on an untrusted network", + "動作モード 状態の取得失敗:": "Operation mode Status acquisition failure:", + "実機に送信して即時反映します": "Send it to the actual device and reflect it immediately", + "時は排他のため再生できません": "Time cannot be played because it is exclusive.", + "書込はセンサー再起動まで有効": "Writing is valid until the sensor is restarted.", + "未設定 — 空欄なら認証なし": "unset — blank means no auth", + "残り通知 読み (ひらがな)": "Remaining-time pronunciation (hiragana)", + "漢字や英字は変換されないので": "Kanji and alphabetic characters are not converted.", + "環境変化に弱いが予測しやすい": "Weak against environmental changes, but easy to predict", + "直前通知 (秒前、0=無効)": "Final notice (seconds before end; 0 = off)", + "直前通知 読み (ひらがな)": "Final notice pronunciation (hiragana)", + "秒), navigator:": "seconds), navigator:", + "秒待っても取得が始まりません": "Even after waiting a few seconds, the acquisition does not start", + "起動アルペジオ設定の取得失敗": "Failed to get startup arpeggio settings", + "起動時のアルペジオを再生する": "Play startup arpeggio", + "超過繰返し (秒、0=1回)": "Overtime repeat (seconds; 0 = once)", + "適用しました — 再起動中…": "Applied — Rebooting…", + "音量に合わせて口を動かします": "Move your mouth according to the volume", + "高域減衰 tilt (dB)": "High-frequency tilt (dB)", + "${label} (現在)": "${label} (current)", + ".avdsl ダウンロード": "Download .avdsl", + "Gemini API キー": "Gemini API key", + "LT 設定送信 (即反映)": "LT Send settings (immediately reflected)", + "OpenAI API キー": "OpenAI API key", + "Password 設定済み": "Password already set", + "RAW 保存 (.pgm)": "Save RAW (.pgm)", + "SSID 読み取りエラー:": "SSID read error:", + "Save & reboot": "Save & reboot", + "Stack-chan 設定": "Stack-chan settings", + "Wi-Fi 設定 (IP)": "Wi-Fi settings (IP)", + "jtts 設定の取得失敗:": "Failed to get jtts configuration:", + "jtts 試し発話 失敗:": "jtts trial utterance failure:", + "sanoTTS 取得失敗:": "sanoTTS acquisition failure:", + "sanoTTS 重みを削除": "Remove sanoTTS weights", + "から自分宛のビルドを取得し": "Get the build for yourself from", + "が OTA ペイロード本体": "inside is the OTA payload", + "こんにちは | こんにちわ": "Hello | hello", + "のときの制御方式を選びます": "Select the control method when", + "のサービストークン等に使用": "Used for service tokens, etc.", + "の広告を待って再接続します": "wait for advertisement and reconnect", + "は個別ボタンが付いています": "has a separate button", + "は利用者が確認してください": "Please confirm by the user", + "を有効にしたまま値を変えて": "Change the value while keeping it enabled.", + "エージェント URL 送信": "Agent URL Send", + "エージェント トークン送信": "Agent Token Send", + "オフセットは何もない時の値": "Offset is the value when there is nothing", + "サーボタスクも起動しません": "Servo task doesn't start either", + "スキャン実行中は使えません": "Cannot be used while scanning is in progress", + "ファイルを選択してください": "Please select file", + "ファイル選択は動作しません": "File selection doesn't work", + "ブラウザ内でバイトコード化": "Bytecode in browser", + "モードでは色は無視されます": "Color is ignored in mode", + "中はリリース取得できません": "Unable to obtain release during", + "他エンジンにフォールバック": "Fallback to other engines", + "会話のステータスをネコミミ": "Nekomimi the conversation status", + "保存しなくても送信で即反映": "Immediately reflected after sending without saving", + "古すぎるファームウェアです": "Firmware too old", + "声門開大比 (やわらかさ)": "Glottal open quotient (softness)", + "実機を内蔵デフォルトに復帰": "Restoring the actual device to the built-in default", + "帯域幅はクラシックでも有効": "Bandwidth is valid even in classic", + "待受中は上の通常設定どおり": "During standby, the normal settings above are used.", + "時間ちょうど 表示テキスト": "On-time text", + "機体が Wi-Fi 経由で": "the aircraft via Wi-Fi.", + "残り通知 (終了の何秒前)": "Remaining-time notice (seconds before end)", + "漢字は発声時に無視されます": "Kanji are ignored when speaking", + "状態: 保存済み・未ロード": "Status: saved/unloaded", + "空なら吹き出しだけ出します": "If it's empty, just pop out a speech bubble", + "起動確認まで自動で進みます": "It will automatically proceed to the startup confirmation.", + "追加 HTTP ヘッダ送信": "Send additional HTTP headers", + "送信完了 — レスポンス:": "Transmission completed — Response:", + "適用しました (near=": "Applied (near=", + "音声出力 状態の取得失敗:": "Failed to get audio output status:", + "音量や音声出力の確認に便利": "Convenient for checking volume and audio output", + ".avbc ダウンロード": "Download .avbc", + "AAC エンコード開始:": "Start AAC encoding:", + "DSL コンパイラ未起動": "DSL compiler not loaded", + "HMM ボイス削除失敗:": "HMM voice deletion failure:", + "HMM ボイス登録失敗:": "HMM voice registration failure:", + "LED 口の動きと連動:": "LED linked to mouth movements:", + "OpenAI 会話機能を": "OpenAI conversation function", + "Pitch 最大 (°)": "Pitch maximum (°)", + "Pitch 最小 (°)": "Pitch minimum (°)", + "XiaoZhi サーバー": "XiaoZhi server", + "XiaoZhi トークン": "XiaoZhi token", + "Yaw 原点 (raw)": "Yaw center (raw)", + "bytes, コンパイル": "bytes, compilation", + "jtts アイドル発話:": "jtts idle utterances:", + "jtts アイドル発話を": "jtts idle utterance", + "jtts 設定を更新 (": "Update jtts configuration (", + "jtts 設定読み込み:": "Load jtts configuration:", + "jtts 試し発話: 「": "jtts trial utterance: “", + "が無いので適用はスキップ": "Skip application because there is no", + "で実機に反映してください": "Please reflect it on the actual machine with", + "にするとサーボの力が抜け": "If you do this, the servo will lose its power.", + "に依頼して回答を話します": "ask someone and tell them the answer", + "の条件に同意していますか": "Do you agree to the terms of", + "は近接センサー非対応です": "is not compatible with proximity sensor", + "は音声のアイドル発話のみ": "is the voice idle utterance only", + "をデコードしながら再生し": "play while decoding", + "を先に有効にしてください": "Please enable it first.", + "を順にタップしてください": "Please tap in order", + "アイドル時の発話フレーズ": "Utterance phrases when idle", + "アバター調整の取得失敗:": "Failed to obtain avatar adjustment:", + "アバター調整の送信失敗:": "Failed to send avatar adjustment:", + "アバター調整を読み込み:": "Load avatar adjustments:", + "エンジンが入っていません": "no engine in", + "グラフとして点灯させます": "light up as a graph", + "テキストだけ反映しました": "Only the text is reflected", + "デバイス情報の取得失敗:": "Failed to get device information:", + "デバイス機能の有効/無効": "Enable or disable device features", + "パスワード 書き込み完了": "Password writing completed", + "ファイルからの書き込みは": "Writing from a file", + "ファイルからアップロード": "Upload from file", + "ファームを更新する場合は": "If you want to update the firmware", + "プレビュー初期化エラー:": "Preview initialization error:", + "ホスト名のもとになります": "This is the basis of the host name", + "ライセンス / 帰属表示": "Licenses / attribution", + "レジスタ アクセス失敗:": "Register access failure:", + "今の値をオフセットにする": "Offset the current value", + "会話ターンが終わるたびに": "At the end of each conversation turn", + "何もかざしていない状態で": "without holding anything up", + "保存済み・ロード済み (": "Saved/loaded (", + "個人・団体への攻撃や批判": "Attacks and criticism of individuals and groups", + "値が入っているなら上書き": "Overwrite if there is a value", + "帯域幅倍率 (鳴き抑え)": "Bandwidth scale (squeal suppression)", + "応答再生中にアバター画面": "Avatar screen during response playback", + "手動で再接続してください": "Please reconnect manually", + "接続に付与する追加ヘッダ": "Additional headers to add to the connection", + "機体が新ファームを起動し": "The aircraft starts the new firmware.", + "気に入った設定を残すには": "To save your favorite settings", + "注意書きのない過激な内容": "Extreme content without warnings", + "空欄を保存すると認証なし": "If you leave the field blank, there will be no authentication.", + "範囲設定モード: OFF": "Range-setting mode: OFF", + "範囲設定モード切替失敗:": "Range setting mode switching failure:", + "虹 (gradient)": "Rainbow (gradient)", + "超過 読み (ひらがな)": "Overtime pronunciation (hiragana)", + "送信完了 — NVS に": "Send completed — to NVS", + "頭タッチで会話を中断する": "Interrupt the conversation by touching the head", + "(変更しない場合は空欄)": "(Leave blank if not changed)", + "(変更しないなら空欄)": "(Leave blank if not changed)", + "(自前ファイルを使う)": "(Use local file)", + "BLE 接続パスワード": "BLE connection password", + "Gemini キー送信": "Gemini key send", + "HMM ボイス登録 (": "HMM voice registration (", + "JTTS ランダム発話": "JTTS random utterance", + "LED 口の動き連動を": "LED mouth movement linked", + "LT 設定: 変更なし": "LT settings: unchanged", + "LT 設定の取得失敗:": "Failed to get LT settings:", + "LT 設定を読み込み:": "Load LT settings:", + "OpenAI キー送信": "OpenAI key transmission", + "SSID 書き込み完了": "SSID writing completed", + "Wi-Fi パスワード": "Wi-Fi password", + "begin に失敗 (": "begin failed (", + "fetch に失敗 (": "fetch failed (", + "release を適用": "Apply release", + "sanoTTS 取得:": "Get sanoTTS:", + "このファームウェアには": "This firmware has", + "すべての出力に効きます": "Works on all outputs", + "で再起動して受信します": "Restart and receive", + "と配布ボイスの帰属表示": "and attribution of distributed voices", + "どちらもオフのとき有効": "Valid when both are off", + "にすると固定閾値で判定": "When set to , judgment is made using a fixed threshold value.", + "のこり いっぷん です": "It's all left.", + "のときは設定した明るさ": "When , the set brightness", + "の発話がニューラル合成": "utterances are neurally synthesized", + "は調整モード非対応です": "does not support adjustment mode", + "への接続が維持されます": "will remain connected to", + "より小さくしてください": "Please make it smaller", + "をチップ内で引き算して": "subtract in the chip", + "を取得して書き込みます": "get and write", + "を口の動きと連動させる": "synchronize with mouth movements", + "を小さいままにできます": "can be kept small", + "を送れないのでコピー用": "For copying as I cannot send", + "アバター調整の取得失敗": "Failed to obtain avatar adjustment", + "アバター調整の送信失敗": "Failed to send avatar adjustment", + "アバター調整を読み込み": "Load avatar adjustments", + "インストール HTTP": "Install HTTP", + "カメラ (テスト撮影)": "Camera (test capture)", + "クリアで認証なしに戻す": "Clear to return to no authentication", + "サーボ設定の取得失敗:": "Failed to get servo settings:", + "サーボ設定を読み込み:": "Load servo settings:", + "システムプロンプト送信": "Send system prompt", + "ダウンロード HTTP": "Download HTTP", + "デバイスが再起動します": "device will restart", + "デバイスへ転送中… (": "Transferring to device... (", + "デバイスを再起動します": "restart your device", + "デバイス名 取得失敗:": "Failed to get device name:", + "デバイス情報の取得失敗": "Failed to obtain device information", + "ネットワークにログイン": "Login to network", + "バッテリーゲージ表示:": "Battery gauge display:", + "バッテリーゲージ表示を": "battery gauge display", + "ピッチ F0 (Hz)": "Pitch F0 (Hz)", + "ファイルで書き込む手順": "Steps to write as a file", + "プレビュー初期化エラー": "Preview initialization error", + "プレビュー読み込み中…": "Loading preview…", + "マネージャから自動入力": "Autofill from manager", + "リップシンク モード:": "Lip sync mode:", + "リップシンク モードを": "lip sync mode", + "ローカルの音声ファイル": "local audio file", + "上の「ファイル選択」で": "Under File selection above, choose", + "再起動が必要な項目のみ": "Only items that require a restart", + "呼吸 (breath)": "Breathing", + "完了 — 再起動します": "Done — reboot", + "実機の顔がリアルタイム": "device face in real time", + "実機をデフォルトに戻す": "Reset device to default face", + "実音声由来のモーラ単位": "Mora unit derived from real speech", + "手を近づけると反応する": "Reacts when your hand approaches", + "撮影中… (~1 秒)": "Shooting... (~1 second)", + "最後まで行ったら先頭へ": "When you reach the end, go to the beginning", + "未装着なら内蔵スピーカ": "Built-in speaker if not installed", + "残り通知 表示テキスト": "Remaining-time notice text", + "無ければフォールバック": "If not, fallback", + "状態: ロード済み (": "Status: Loaded (", + "直前通知 表示テキスト": "Final notice text", + "直近の会話ターン未完了": "Latest conversation turn incomplete", + "範囲設定モード切替失敗": "Range setting mode switching failure", + "経由で表示されており、": "through the device's own AP", + "設定ページから行えます": "You can do this from the settings page", + "認証パスワード クリア": "Authentication password clear", + "認証パスワード送信 (": "Send authentication password (", + "近接 状態の取得失敗:": "Failed to get proximity status:", + "近接センサー調整を適用": "Apply proximity sensor adjustment", + "露出安定化のため撮影に": "When shooting to stabilize exposure", + "音声 DB 削除失敗:": "Voice DB deletion failed:", + "音声 DB 登録失敗:": "Voice DB registration failure:", + "音声の出力先を選びます": "Select the audio output destination", + "音声送信を中断しました": "Audio transmission has been interrupted", + "(空欄なら超過と同じ)": "(If blank, same as excess)", + "(アクセント核) と": "(accent nucleus) and", + "+ 自動保存します。": "and saves automatically.", + ".avbc 直接送信": "Send .avbc directly", + "Apply エラー:": "Apply error:", + "LT タイムキーパー": "LT timekeeper", + "LT 設定送信失敗:": "LT configuration sending failure:", + "Pitch 原点取込": "Capture Pitch center", + "Pitch 最大取込": "Capture Pitch maximum", + "Pitch 最小取込": "Capture Pitch minimum", + "Wi-Fi: 切断中": "Wi-Fi: Disconnected", + "Wi-Fi: 接続中": "Wi-Fi: Connecting", + "Yaw 最大 (°)": "Yaw maximum (°)", + "Yaw 最小 (°)": "Yaw minimum (°)", + "がアップであれば動作": "works if is up", + "が優先して使われます": "will be used with priority", + "このファームウェアは": "This firmware is", + "このページを再度開く": "Reopen this page", + "の取得を指示します…": "Instructs to obtain...", + "の方を選んでください": "Please select", + "の重みを削除しますか": "Do you want to remove the weight of", + "ほっぺ 横オフセット": "Cheek horizontal offset", + "ほっぺ 縦オフセット": "Cheek vertical offset", + "をアップロードすると": "When you upload", + "を使用してください。": "instead.", + "を決め直してください": "Please reconsider", + "を選び直してください": "Please reselect", + "アバターの口が開くと": "When the avatar's mouth opens", + "アプリの結合イメージ": "App combination image", + "インストール完了 (": "Installation completed (", + "カメラ撮影 OK (": "Camera shooting OK (", + "コンパイル & 送信": "Compile & send", + "コンパイル エラー:": "Compilation error:", + "サーボの力が抜けます": "The servo loses its power.", + "サーボ原点・可動範囲": "Servo origin/movement range", + "サーボ設定の取得失敗": "Failed to get servo settings", + "サーボ設定を読み込み": "Load servo settings", + "スピーカー動作確認音": "Speaker operation confirmation sound", + "スライダーを動かすと": "When you move the slider", + "タブを切り替えて設定": "Switch tabs to configure settings", + "デバイスは更新モード": "Device is in update mode", + "デフォルトに戻します": "Revert to default", + "バイトを超えています": "More than a part-time job", + "バッテリーゲージ表示": "battery gauge display", + "バンドルが読込み失敗": "Bundle failed to load", + "ビルド不一致の可能性": "Possible build mismatch", + "フォルマント合成のみ": "Formant synthesis only", + "フォールバックします": "fall back", + "プレビュー読み込み中": "Loading preview", + "プロバイダ取得失敗:": "Failed to get provider:", + "マイクでリップシンク": "lip sync with microphone", + "リップシンク モード": "Lip-sync mode", + "リリース一覧取得失敗": "Failed to get release list", + "会話中は反応しません": "Does not respond during conversation", + "会話機能を有効にする": "Enable conversation", + "保存で再起動後も保持": "Save to keep after reboot", + "保存で再起動後も有効": "Save to remain valid after reboot", + "保存後の再起動で反映": "It will be reflected after rebooting after saving.", + "保存済み・ロード済み": "Saved/loaded", + "再起動して確定します": "Reboot and confirm", + "再起動で反映されます": "It will take effect after rebooting", + "到達距離を伸ばすなら": "If you want to extend your reach", + "単色 (solid)": "Solid", + "即時反映・再起動不要": "Immediate reflection / No need to restart", + "反応が敏感すぎるなら": "If you are too sensitive", + "取得してインストール": "Download and install", + "基板種別の取得失敗:": "Failed to get board type:", + "変更しない場合は空欄": "Leave blank if no change", + "復帰トグルとは別物で": "It is different from the return toggle.", + "成功後はブラウザ側で": "After success, on the browser side", + "政治的・宗教的な主張": "political/religious claims", + "有効状態の取得失敗:": "Failed to get valid state:", + "枚撮影して表示します": "Take a photo and display it", + "機体がダウンロード中": "Aircraft is downloading", + "無ければフォルマント": "If not, formant", + "無ければ吹き出しのみ": "If not, just a speech bubble", + "秒), 状態読み取り": "seconds), read status", + "空文字列に揃えるには": "To align to empty string", + "経由で表示されており": "It is displayed via", + "繋がらない場合は一度": "If you can't connect, try", + "自動更新 (2 秒)": "Auto-refresh (2 s)", + "調整のみ利用できます": "Adjustment only available", + "近接時の発話フレーズ": "Phrases to speak when approaching", + "途切れるため排他です": "It is exclusive because it is interrupted.", + "非対応のブラウザです": "This browser is not compatible.", + "音声 DB 登録 (": "Audio DB registration (", + "音声パイプライン診断": "Audio pipeline diagnostics", + "音声受信を有効にする": "Enabling audio reception", + ".avdsl 読込": "Load .avdsl", + "Apply 失敗:": "Apply failed:", + "BLE 接続エラー": "BLE connection error", + "BLE 認証 OK": "BLE authentication OK", + "GATT 接続完了": "GATT connection completed", + "HMM ボイス削除": "HMM voice deletion", + "Male (低め)": "Male (lower)", + "OpenAI 有効": "OpenAI enabled", + "RTP 音声受信:": "RTP audio reception:", + "RTP 音声受信を": "RTP audio reception", + "SSID 設定済み": "SSID already set", + "end に失敗 (": "end failed (", + "jtts 設定送信": "jtts settings sending", + "jtts 試し発話": "Test JTTS speech", + "s) — 即時反映": "s) — Immediate reflection", + "から直接取得します": "Get directly from", + "が装着されていれば": "If the is installed", + "すたっくちゃんです": "I'm Stuck-chan", + "で反映してください": "Please reflect in", + "で指定してください": "Please specify with", + "に再接続できません": "cannot reconnect to", + "のツールで生成した": "generated with the tool", + "の会話に使う指示文": "directives used in conversations about", + "の光り方を変えます": "changes the way the light shines", + "の取得を指示します": "instructs the acquisition of", + "はネコミミの三角形": "is a cat-eared triangle", + "はリアルタイム反映": "is reflected in real time", + "は漢字を読めません": "cannot read kanji", + "より大幅に高速です": "is significantly faster", + "をダウンロード中…": "Downloading...", + "を使用してください": "Please use", + "を入力してください": "Please enter", + "を奪い合って再生が": "playback by competing for", + "を実行してください": "Please run", + "を確認してください": "Please check", + "を選び「書き込み」": "and press Write", + "を選択してください": "Please select", + "カタカナで入力して": "Enter in katakana", + "サーボを有効にする": "Enable servos", + "システムプロンプト": "system prompt", + "スタックチャンです": "This is Stack Chan", + "センサー調整モード": "Sensor adjustment mode", + "デバイスを検索中…": "Searching for devices...", + "デバイス名を更新:": "Update device name:", + "デバイス機能の有効": "Enabling device features", + "デフォルト顔ソース": "Default face source", + "データ送信完了 (": "Data transmission completed (", + "バーに反映されます": "reflected in the bar", + "パーティションなし": "no partition", + "ファームウェア更新": "Firmware update", + "プロバイダ取得失敗": "Provider acquisition failure", + "ボイスを削除します": "delete voice", + "マイク AGC を": "microphone agc", + "マイクリップシンク": "my lip sync", + "モーラ長 (ms)": "Mora length (ms)", + "リリース取得失敗:": "Failed to get release:", + "保存済み・未ロード": "Saved/unloaded", + "再起動してください": "Please restart", + "前者はブートローダ": "The former is the bootloader", + "取得・書き込み不可": "Cannot be acquired or written", + "口の動きに合わせて": "according to the movement of the mouth", + "基板種別の取得失敗": "Failed to get board type", + "変更しないなら空欄": "Leave blank if not changed", + "手を近づけたときに": "when you bring your hands close", + "新ファームに再起動": "Reboot to new firmware", + "既定値を入れました": "I entered the default value", + "旧パーティション表": "old partition table", + "有効状態の取得失敗": "Failed to get valid status", + "残り通知 (秒前)": "Remaining-time notice (seconds before end)", + "状態を更新して確認": "Update and check status", + "現在の SSID:": "Current SSID:", + "現在値がありません": "No current value", + "画面表示 | 読み": "Screen text | pronunciation", + "空欄なら変更しない": "If left blank, do not change", + "空欄なら超過と同じ": "If it is blank, it is the same as exceeding", + "自前ファイルを使う": "use your own files", + "自動 (Auto)": "Auto", + "設定の書き込み失敗": "Failed to write settings", + "設定画面で行えます": "This can be done on the settings screen", + "認証パスワード送信": "Send authentication password", + "超過 表示テキスト": "Overtime text", + "通知を登録しました": "Registered for notification", + "遠いのちらつき防止": "Prevents flickering in the distance", + "重みのライセンスは": "The weight license is", + "重みをアップロード": "upload weights", + "重みを削除しました": "removed weights", + "重みを削除しますか": "Do you want to remove weights?", + "開口 最大 (開)": "Maximum opening (open)", + "開口 最小 (閉)": "Minimum opening (closed)", + "間違ったパスワード": "wrong password", + "音量 (gain)": "Gain", + "頭タッチで会話中断": "Conversation interrupted by head touch", + "スタックチャン側で": "On the stacker side", + ") だが未ロード": ") but not loaded", + "B) を送信中…": "B) Sending...", + "HTTP 接続中": "HTTP connecting", + "KiB / 上限": "KiB / upper limit", + "OTA エラー:": "OTA error:", + "STA が未接続": "STA is not connected", + "Safari で": "Open", + "Yaw 原点取込": "Capture Yaw center", + "Yaw 最大取込": "Capture Yaw maximum", + "Yaw 最小取込": "Capture Yaw minimum", + "`} に切り替え": "switch to `}", + "device が": "The device downloads", + "jtts ボイス": "JTTS voice", + "ms/回 (合計": "ms/times (total", + "ms/回 (最大": "ms/times (max.", + "— 取得中… —": "— Downloading… —", + "⟳ 再起動が必要": "⟳ Restart required", + "が切断されるので": "is disconnected, so", + "が反応しない場合": "does not respond", + "が見つかりません": "not found", + "ごとの工場既定値": "Factory default value per", + "つ選んで話します": "choose one and talk about it", + "では何もないとき": "Then when there's nothing", + "で暗号化されます": "is encrypted with", + "で更新されるので": "It will be updated with", + "で開いている場合": "If opened in", + "にすると無音起動": "When set to silent startup", + "に再エンコードし": "re-encode to", + "に永続化されます": "will be persisted to", + "のいずれかで会話": "Conversation with either", + "のこり15秒です": "15 seconds left", + "のアップロードは": "The upload of", + "の主な振る舞いを": "The main behavior of", + "の発話が単位連結": "utterances are unit concatenation", + "やわらかめの目安": "A guideline for softness", + "をタップして展開": "Tap to expand", + "をダウンロード中": "Downloading", + "を使ってください": "Please use", + "を押してください": "Please press", + "を送信しています": "is sending", + "を選んでください": "Please select", + "を鳴らすかどうか": "whether to sound", + "アクセント句切り": "accent punctuation", + "アップロード失敗": "Upload failed", + "アドバタイズ名と": "advertising name and", + "インストール完了": "Installation completed", + "カメラ撮影失敗:": "Camera shooting failure:", + "キーを設定します": "set the key", + "クリアで認証なし": "Clear and no authentication", + "コンパイラ未起動": "Compiler not started", + "サイズ範囲外 (": "Out of size range (", + "サーバーから取得": "Download from server", + "デバイスへ転送中": "Transferring to device", + "デバイスを検索中": "Searching for device", + "デバイス名を更新": "Update device name", + "デフォルトに戻す": "Restore defaults", + "ネコミミ LED": "Cat-ear LED", + "パスワードを表示": "Show password", + "フォルマント倍率": "Formant scale", + "フォルマント合成": "formant synthesis", + "プレビュー描画中": "Drawing preview", + "プロビジョニング": "provisioning", + "ボイスと同じ記法": "Same notation as voice", + "ボイス取得失敗:": "Voice acquisition failure:", + "マイク AGC:": "Mike AGC:", + "モデルの利用条件": "Model usage conditions", + "ライブプレビュー": "live preview", + "リアルタイム更新": "Real-time updates", + "リリース取得失敗": "Release acquisition failure", + "レベル メーター": "level meter", + "ローカル音声認識": "local speech recognition", + "会話ステータスの": "conversation status", + "会話プロバイダ・": "Conversation provider/", + "保存 & 再起動": "Save & reboot", + "保存で空欄を送信": "Save and send blank", + "入力を求めます。": "Prompt for input.", + "全プロバイダ共通": "Common to all providers", + "全体が明るくなる": "the whole thing becomes brighter", + "初期値はオフです": "Default is off", + "即時・再起動なし": "Immediate/no restart", + "口 横オフセット": "Mouth horizontal offset", + "口 縦オフセット": "Mouth vertical offset", + "口の開き ゲイン": "Mouth opening gain", + "後に再起動します": "reboot after", + "応答を中断します": "interrupt the response", + "本体アクセス済み": "Main unit accessed", + "構造ごと差し替え": "Replace entire structure", + "毎回どれかを選ぶ": "choose one each time", + "状態: 取得失敗": "Status: Acquisition failed", + "現在値が無いので": "Since there is no current value", + "用の予備スロット": "spare slot for", + "発表時間 (秒)": "Presentation time (seconds)", + "目 横オフセット": "Eye horizontal offset", + "目 縦オフセット": "Eye vertical offset", + "眉 横オフセット": "Eyebrow horizontal offset", + "眉 縦オフセット": "Eyebrow vertical offset", + "秒ほどかかります": "It takes about seconds", + "空欄なら自動生成": "Automatically generated if blank", + "空欄なら認証なし": "If left blank, no authentication", + "空欄や変更なしで": "blank or unchanged", + "範囲設定モード:": "Range setting mode:", + "範囲設定モードを": "Range setting mode", + "設定されていれば": "When configured,", + "診断: 書き込み": "Diagnosis: Write", + "認証に進めません": "Can't proceed to authentication", + "起動アルペジオ:": "Start arpeggio:", + "起動アルペジオを": "start arpeggio", + "超過の繰返し間隔": "Excess repeat interval", + "近接の設定を送信": "Send proximity settings", + "近接の閾値を適用": "Applied proximity thresholds", + "近接センサー調整": "proximity sensor adjustment", + "通信は接続ごとに": "Communication is per connection", + "通知の復号エラー": "Notification decryption error", + "重みの取得を開始": "Start getting weights", + "音声 DB 削除": "Voice DB deletion", + "音声ファイル再生": "Audio file playback", + "音声送信エラー:": "Audio transmission error:", + "BLE 接続中": "Connecting to BLE", + "BLE 未接続": "BLE not connected", + "OTA 失敗:": "OTA failure:", + "OTA 開始:": "OTA start:", + "SSID 送信": "SSID transmission", + "— 取得失敗:": "— Failed to get:", + "— 取得完了:": "— Obtained:", + "おじかん です": "My name is Ojikan.", + "から取得します": "get from", + "が使えないため": "Because it cannot be used", + "この設定ページ": "This settings page", + "じゅうごびょう": "full time", + "すたっくちゃん": "Stuck-chan", + "すべて即時反映": "All reflected immediately", + "つから選びます": "choose from", + "ではありません": "is not", + "で読み上げます": "Read aloud with", + "には使えません": "cannot be used for", + "に自動で戻って": "automatically return to", + "のこり1分です": "1 minute left", + "の順に効きます": "It takes effect in the order of", + "は口が開くほど": "The more my mouth opens", + "よろしいですか": "Are you sure?", + "をダウンロード": "for your board", + "をバイパスした": "bypassed", + "を引き算します": "subtract", + "を書き込みます": "write", + "を選ぶと内蔵に": "If you select , it will be built-in.", + "アイドル時の声": "Voice when Idol", + "アイドル発話を": "idle speech", + "エンコード開始": "Start encoding", + "カメラ撮影失敗": "Camera shooting failure", + "サーボ設定送信": "Send servo settings", + "ストリーミング": "streaming", + "スピーカー音量": "Speaker volume", + "セルラーへ自動": "automatic to cellular", + "センサーに適用": "Applies to sensors", + "タイムキーパー": "time keeper", + "ダウンロード中": "Downloading", + "デバイス名送信": "Send device name", + "データ送信完了": "Data transmission completed", + "パスワード認証": "Password authentication", + "パスワード送信": "Send password", + "パーティション": "partition", + "ファイル未選択": "File not selected", + "ファイル選択・": "File selection/", + "ファーム既定値": "Farm default value", + "プレフィクス可": "Prefix allowed", + "ペアリング不要": "No pairing required", + "ペイロード本体": "payload body", + "ボイス削除失敗": "Voice deletion failed", + "ボイス取得失敗": "Voice acquisition failure", + "ボイス登録失敗": "Voice registration failure", + "ボタンを押すと": "when you press the button", + "リセット失敗:": "Reset failure:", + "上の他端末から": "From other terminals above", + "上の行から順に": "Starting from the top row", + "付近にできます": "You can do it nearby", + "件取得 (表示": "Get (display)", + "会話プロバイダ": "Conversation provider", + "会話機能オフ時": "When conversation function is off", + "保存して再起動": "save and restart", + "入力を求めます": "prompt for input", + "切断されました": "disconnected", + "初回書き込み用": "For first writing", + "判定の連続時間": "Continuous judgment time", + "即反映する項目": "Items to be reflected immediately", + "取得に失敗 (": "Failed to get (", + "口の動きと連動": "linked to mouth movements", + "口の動きに連動": "Linked to mouth movements", + "口の動き連動を": "Linked mouth movements", + "合成になります": "It will be a composite", + "変更は即時反映": "Changes are reflected immediately", + "形式は上と同じ": "The format is the same as above", + "後の連続タッチ": "Continuous touch after", + "接続パスワード": "connection password", + "操作画面の脱力": "Weakness of the operation screen", + "未保存 (領域": "unsaved (area", + "漢字は発声時に": "Kanji when pronounced", + "状態: 未登録": "Status: Unregistered", + "状態の取得失敗": "Failed to get status", + "状態取得 OK": "Status OK", + "状態取得失敗:": "Failed to get status:", + "空のパスワード": "empty password", + "等の自作版向け": "For self-made versions such as", + "範囲設定モード": "Range setting mode", + "自動ゲイン制御": "automatic gain control", + "自動保存します": "Auto save", + "設定の取得失敗": "Failed to get settings", + "設定を読み込み": "Load settings", + "認証なしに戻す": "Revert to no authentication", + "認証パスワード": "Authentication password", + "読みはひらがな": "Read in hiragana", + "読み取りエラー": "read error", + "起動アルペジオ": "launch arpeggio", + "配布ボイスなし": "No distributed voice", + "音声タブで設定": "Set in audio tab", + "音声送信エラー": "Audio transmission error", + "(空欄で自動)": "(Automatically if blank)", + "(名称不明)": "(unknown)", + "(現在の値)": "(current value)", + "(自動生成)": "(auto-generated)", + "133 以降": "133 or later", + "BLE 接続": "Connect BLE", + "PNG 保存": "Save PNG", + "RAW 撮影": "RAW capture", + "ms, 合計": "ms, total", + "— 取得中:": "— Retrieving:", + "⚡ 即時反映": "⚡ Immediate reflection", + "から直接取得": "Retrieved directly from", + "がありません": "There is no", + "が使えません": "cannot be used", + "が反応します": "will react", + "このページは": "This page", + "されています": "has been", + "しまう場合は": "If you want to put it away", + "そちらを優先": "give priority to that", + "だが未ロード": "But not loaded", + "で動作します": "works with", + "で取得します": "Get it with", + "で確定します": "Confirm with", + "で空文字列を": "empty string with", + "と同じデータ": "same data as", + "と表示しても": "Even if it is displayed as", + "にアクセスし": "access", + "のあるボード": "board with", + "ほっぺ 半径": "Cheek radius", + "ほっぺを表示": "Show cheeks", + "も一覧に出す": "also list", + "を入れました": "I put", + "を有効にする": "enable", + "アイドル発話": "idle speech", + "アクセス失敗": "Access failure", + "アクセント核": "accent nucleus", + "アップロード": "Upload", + "アバター調整": "avatar adjustment", + "インストール": "installation", + "エージェント": "agent", + "カタカナのみ": "Katakana only", + "クロストーク": "crosstalk", + "サイズ範囲外": "Out of size range", + "センサーなし": "No sensor", + "ダウンロード": "download", + "デコード中…": "Decoding...", + "デスクトップ": "desktop", + "デバイス名:": "Device name:", + "トークン管理": "token management", + "トークン送信": "Send token", + "ネット未接続": "No internet connection", + "バージインを": "barge in", + "パスワードを": "password", + "パルス周波数": "pulse frequency", + "ピッチシフト": "pitch shift", + "ファイル選択": "Choose file", + "フォルマント": "formant", + "ブラウザ側で": "on the browser side", + "プリセット「": "Preset “", + "プロバイダ:": "Provider:", + "プロバイダを": "provider", + "ミニブラウザ": "mini browser", + "モードでも可": "mode is also possible", + "モードの場合": "For mode", + "モード中でも": "even in mode", + "ランダム生成": "Generate", + "ランダム発話": "random utterance", + "リセット失敗": "Reset failure", + "リップシンク": "lip sync", + "リモコン受信": "remote control reception", + "リリース取得": "Get release", + "一覧を読込中": "Loading list", + "一覧取得失敗": "List acquisition failure", + "保存しました": "Saved", + "保存済み (": "Saved (", + "値は反射光量": "The value is the amount of reflected light", + "内蔵スピーカ": "built-in speaker", + "再起動が必要": "Restart required", + "再起動します": "reboot", + "動作モード:": "Operating mode:", + "動作モードを": "operating mode", + "取得して保存": "Download and save", + "取得済みなら": "If you have already obtained", + "古いファーム": "old farm", + "呼吸 (速)": "breathing (fast)", + "操作に未対応": "Not compatible with operation", + "新ファームを": "New farm", + "既定の閾値は": "The default threshold is", + "既定値に戻す": "Restore default values", + "明るさは上の": "Brightness is above", + "時間ちょうど": "just in time", + "暗号化された": "encrypted", + "書き込み失敗": "Write failure", + "書き込み完了": "Writing completed", + "書込で即反映": "Immediately reflected by writing", + "横オフセット": "Lateral offset", + "機体が自分の": "the aircraft is mine", + "機体側が拒否": "The aircraft side refuses", + "無視されます": "will be ignored", + "物理ボタンで": "with physical button", + "状態を再取得": "Refresh", + "状態取得中…": "Loading status…", + "状態取得失敗": "Status failed", + "状態読み取り": "Read status", + "現在値を表示": "Display current value", + "発話・マイク": "Speech/mic", + "終了の何秒前": "seconds before the end", + "縦オフセット": "vertical offset", + "表示テキスト": "display text", + "設定読み込み": "Load settings", + "設定送信失敗": "Settings sending failure", + "該当ボードの": "Download the", + "読み出し失敗": "Read failure", + "読み取り失敗": "Read failure", + "読込みました": "Loaded", + "近接センサー": "proximity sensor", + "送信エラー:": "Sending error:", + "送信失敗 —": "Send failure —", + "適用しました": "Applied", + "音声エンジン": "voice engine", + "音声送信完了": "Audio transmission completed", + "から接続可": "Can be connected from", + "から選ぶか": "Choose from", + "この画面が": "This screen", + "こんにちは": "Hello", + "こんにちわ": "Hello", + "では未注入": "Not injected", + "でブースト": "boost with", + "で再起動し": "Restart with", + "と可動範囲": "and range of motion", + "になります": "becomes", + "に再接続中": "reconnecting to", + "に切り替え": "switch to", + "に戻します": "return to", + "に戻ります": "return to", + "のとき有効": "Valid when", + "のボイス例": "voice example", + "の取得失敗": "Failed to get", + "はひらがな": "Hiragana", + "へ再起動中": "Rebooting to", + "やわらかさ": "Softness", + "をタップで": "by tapping", + "をローカル": "the local", + "を入れると": "If you put", + "を制御する": "control", + "を書けます": "can write", + "を話します": "speak", + "を返します": "returns", + "アクセサリ": "accessories", + "オフセット": "offset", + "オフライン": "offline", + "カスケード": "cascade", + "カメラ撮影": "camera shooting", + "カラーバー": "Color bars", + "キャンセル": "Cancel", + "クラシック": "classic", + "クリア予約": "Clear reservation", + "コンパイラ": "compiler", + "コンパイル": "compilation", + "サーボ設定": "Servo settings", + "スライダー": "slider", + "タグを確認": "Check tag", + "タッチ操作": "Touch controls", + "タブで家庭": "home in tabs", + "タブで開始": "start with tab", + "チャンネル": "Channel", + "テスト撮影": "test shooting", + "デコード中": "Decoding", + "デバイス:": "Device:", + "デバイスは": "The device is", + "デバイス名": "Device name", + "トークン:": "Token:", + "ニューラル": "neural", + "ネコミミ部": "cat ears club", + "バイト保存": "Byte save", + "バッテリー": "Battery", + "バージイン": "barge in", + "パスワード": "Password", + "ビブラート": "vibrato", + "ファーム側": "Farm side", + "ブラウザは": "The browser is", + "プリセット": "preset", + "プレビュー": "preview", + "プロバイダ": "provider", + "ヘッダ送信": "header sending", + "ボイス削除": "Delete voice", + "ボイス取得": "voice acquisition", + "ボイス必須": "Voice required", + "ボイス登録": "Voice registration", + "ボードのみ": "board only", + "ボード判定": "Board judgment", + "メモリ不足": "Out of memory", + "モジュール": "module", + "ライセンス": "license", + "リンク不調": "Bad link", + "レスポンス": "response", + "ローカルの": "Select a local", + "ロード済み": "loaded", + "・パルス数": "Pulses", + "中央位置で": "in the center position", + "以上で近い": "That's close", + "以降が必要": "Requires the following", + "会話の人格": "conversation personality", + "会話再生・": "Conversation playback/", + "会話機能:": "Conversation function:", + "会話機能を": "conversation function", + "保存済みの": "saved", + "側で取得中": "Obtaining on the side", + "入力ゲイン": "Input gain", + "再起動不要": "No reboot required", + "再起動中…": "Rebooting...", + "削除失敗:": "Delete failed:", + "動作モード": "Operation mode", + "単位、保存": "unit, save", + "即時反映・": "Immediate reflection/", + "反応の間隔": "reaction interval", + "反映は即時": "Reflection is immediate", + "反映は失敗": "Reflection failed", + "取得に失敗": "Failed to get", + "取得失敗:": "Retrieval failure:", + "受光ゲイン": "Light receiving gain", + "口 最大幅": "Mouth maximum width", + "口 最小幅": "Mouth minimum width", + "声門開大比": "glottal opening ratio", + "変化します": "will change", + "子供っぽい": "childish", + "実機の顔を": "The face of the actual machine", + "帯域幅倍率": "Bandwidth multiplier", + "手入力も可": "Manual input is also possible.", + "接続エラー": "connection error", + "接続失敗:": "Connection failure:", + "文法ガイド": "Grammar guide", + "新ファーム": "new farm", + "方式で有効": "valid in method", + "既定の前段": "Default first stage", + "時間です!": "It's time!", + "未接続では": "When not connected", + "未満で遠い": "far less than", + "本体前面の": "on the front of the main unit", + "機体が公式": "The aircraft is official", + "正式版以外": "Non-official version", + "段階のバー": "stage bar", + "状態: —": "Status: —", + "状態を更新": "Refresh status", + "状態取得中": "Obtaining status", + "発話の順序": "Phrase order", + "直近の会話": "recent conversation", + "秒, 平均": "seconds, average", + "空にすると": "When empty", + "空欄で自動": "Auto if blank", + "聞き取り中": "Listening", + "設定すると": "When set", + "設定に失敗": "Configuration failed", + "設定を更新": "Update settings", + "設定を送信": "Submit settings", + "設定を適用": "Apply settings", + "設定ページ": "Settings page", + "起動直後の": "Immediately after startup", + "超過繰返し": "Excessive repetition", + "送信エラー": "Sending error", + "通常どおり": "as usual", + "進捗は下の": "Progress is below", + "適用します": "apply", + "重みを削除": "remove weight", + "重みを取得": "get weights", + "鍵交換完了": "Key exchange completed", + "閾値を適用": "Apply threshold", + "非対応です": "Not compatible", + "音声モデル": "voice model", + "音声出力:": "Audio output:", + "音声出力を": "audio output", + "音声受信を": "audio reception", + "飽和したら": "When it gets saturated", + "(未設定)": "(Not set)", + "(現在)": "(currently)", + "(飽和)": "(saturated)", + "/ 領域": "/ area", + "ms/回": "ms/times", + "いずれも": "Both", + "いっぷん": "Ippun", + "おじかん": "Old man", + "おはよー": "Good morning", + "から取得": "Retrieved from", + "が有効化": "is enabled", + "が未接続": "is not connected", + "こちらは": "This is", + "そちらで": "Over there", + "ちかいよ": "It's close", + "ではなく": "Do not select", + "で書いて": "write it in", + "と出ます": "appears.", + "と頼むと": "When I asked", + "なでなで": "Stroke", + "なります": "It will be", + "に再接続": "reconnect to", + "に聞いて": "ask", + "のみ書込": "write only", + "の値です": "is the value of", + "の形式。": "format.", + "の発話が": "The utterance of", + "ひらがな": "Hiragana", + "やっほー": "Yahoo", + "を取得し": "get", + "を押すと": "When you press", + "を有効化": "enable", + "を読込み": "load", + "を送信中": "Sending", + "を選んで": "choose", + "を開いて": "Open", + "を開く。": "in Safari.", + "アバター": "Avatar", + "アルファ": "alpha", + "エンジン": "Engine", + "カメラで": "with camera", + "キー送信": "key transmission", + "コードを": "code", + "サーバー": "server", + "サーボ:": "Servo:", + "サーボを": "the servo", + "スタート": "Start", + "ストップ": "Stop", + "センサー": "sensor", + "デバイス": "Device", + "トークン": "token", + "ネコミミ": "cat ears", + "パルス数": "Number of pulses", + "ファイル": "file", + "ブザー声": "buzzer voice", + "ヘッダ名": "header name", + "メニュー": "menu", + "メーター": "meter", + "モザイク": "mosaic", + "モードで": "in mode", + "モードを": "mode", + "モーラ長": "Mora length", + "ランダム": "random", + "リセット": "reset", + "リリース": "Release", + "レジスタ": "register", + "ローカル": "local", + "・色変換": "Color conversion", + "互換パス": "Compatibility path", + "今の値を": "current value", + "会話中に": "during a conversation", + "会話応答": "Conversation response", + "会話機能": "Conversation function", + "保存なし": "No storage", + "保存中…": "Saving...", + "保存済み": "saved", + "倍にして": "double it", + "側で自動": "automatic on the side", + "再生投入": "Recycle input", + "再起動中": "Rebooting", + "削除失敗": "Delete failed", + "単位連結": "unit consolidation", + "即時反映": "Applies immediately", + "原点取込": "Origin capture", + "取得不可": "Not available", + "取得中…": "Obtaining...", + "取得前に": "before acquisition", + "取得失敗": "Acquisition failure", + "取得完了": "Acquisition completed", + "口の開き": "mouth opening", + "各状態で": "in each state", + "合成方式": "Synthesis method", + "名称不明": "Unknown name", + "回 平均": "times average", + "声タイプ": "Voice type", + "変更なし": "No change", + "姿勢配信": "posture delivery", + "帰属表示": "Attribution", + "強制指定": "Forced specification", + "従来動作": "Conventional operation", + "応答待ち": "Waiting for response", + "接続中…": "Connecting...", + "接続中は": "While connected", + "接続失敗": "Connection failure", + "接続完了": "Connection completed", + "時間です": "It's time", + "書き込み": "Write", + "書けます": "I can write", + "最大取込": "Maximum intake", + "最小取込": "Minimum intake", + "未接続か": "Not connected?", + "未装着で": "Not installed", + "残り通知": "Remaining notification", + "測定周期": "Measurement period", + "準備中…": "Preparing...", + "準備完了": "Ready", + "状態取得": "Get status", + "現在の値": "current value", + "現状互換": "Current compatibility", + "画面表示": "Screen display", + "発表時間": "Presentation time", + "登録失敗": "Registration failure", + "目の半径": "Eye radius", + "直前通知": "Last minute notice", + "直接送信": "direct send", + "眉を表示": "Show eyebrows", + "経由設定": "Route settings", + "自動保存": "auto save", + "自動更新": "automatic update", + "自動生成": "automatic generation", + "色調整用": "For color adjustment", + "設定して": "Set it", + "設定済み": "Already set", + "設定送信": "Send settings", + "試し発話": "trial utterance", + "認証なし": "No authentication", + "読み込み": "Load", + "赤の点線": "red dotted line", + "送信中…": "Sending...", + "送信失敗": "Send failure", + "送信完了": "Transmission completed", + "進捗停止": "progress stopped", + "適用中…": "Applying…", + "重み必須": "Weight required", + "音声出力": "Audio output", + "音声受信": "voice reception", + "領域なし": "no area", + "顔の調整": "face adjustment", + "首輪と鈴": "collar and bell", + "高域減衰": "High frequency attenuation", + "高速パス": "fast path", + "鳴き抑え": "Squeal suppression", + "からの": "from", + "が上限": "is the upper limit", + "が必要": "is required", + "が消失": "disappears", + "が許容": "is acceptable", + "します": "I will", + "すると": "Then", + "で公式": "official in", + "で反映": "reflected in", + "で固定": "fixed with", + "で届く": "Arrive at", + "で確定": "Confirmed with", + "で表す": "expressed as", + "で飽和": "saturated with", + "なあに": "Hey there", + "にして": "to", + "にする": "to make", + "に切替": "switch to", + "に失敗": "failed to", + "に書く": "write to", + "のこり": "What remains", + "の右側": "right side of", + "の形式": "format of", + "の重み": "weight of", + "ほっぺ": "cheeks", + "または": "or", + "や顔の": "and face", + "を削除": "delete", + "を押し": "press", + "を指定": "specify", + "を適用": "apply", + "を選び": "choose", + "を開く": "open", + "エラー": "Error", + "カメラ": "camera", + "カラー": "color", + "クリア": "Clear", + "ゲイン": "gain", + "サーボ": "servo", + "タッチ": "touch", + "タブの": "tab's", + "ツール": "tools", + "パルス": "pulse", + "ピッチ": "pitch", + "ヘッダ": "header", + "ベータ": "beta", + "ボイス": "voice", + "ボード": "board", + "マイク": "microphone", + "メンテ": "Maintenance", + "モード": "Mode", + "レベル": "level", + "・電流": "Current", + "以後は": "From then on", + "件取得": "Get items", + "任意の": "any", + "会話・": "Conversation/", + "保存中": "Saving", + "再起動": "restart", + "分です": "It's a minute", + "切断中": "Cutting", + "単位、": "unit,", + "即反映": "Immediately reflected", + "取得中": "Obtaining", + "句切り": "punctuation", + "右上の": "Open the", + "右端で": "at the right end", + "回のみ": "times only", + "基板:": "Substrate:", + "声門波": "glottal wave", + "失敗:": "Failure:", + "左端で": "at the left end", + "帯域幅": "bandwidth", + "床の値": "floor value", + "応答中": "Speaking", + "息成分": "breath components", + "接続し": "connect", + "接続中": "Connecting", + "接続後": "After connection", + "撮影中": "Shooting in progress", + "既定は": "The default is", + "旧方式": "Old method", + "明るさ": "Brightness", + "最初の": "first", + "最大幅": "maximum width", + "最小幅": "minimum width", + "最後に": "Finally", + "最新の": "latest", + "未保存": "Unsaved", + "未再生": "Not played", + "未取得": "Not captured", + "未接続": "Disconnected", + "未来の": "future", + "未登録": "Not registered", + "未設定": "Not set", + "未適用": "Not applicable", + "未開始": "Not started", + "本体の": "of the main body", + "機体が": "The aircraft", + "機体に": "to the fuselage", + "機体の": "of the aircraft", + "準備中": "Under preparation", + "版推奨": "Recommended version", + "現在の": "current", + "現在値": "Current value", + "秒ごと": "every second", + "秒です": "seconds", + "秒程度": "About seconds", + "経由で": "via", + "経由の": "via", + "経由は": "Via", + "背景色": "Background color", + "自動で": "automatically", + "自身の": "own", + "設定で": "in settings", + "設定済": "Already set", + "読みに": "To read", + "読みは": "The reading is", + "調べて": "Check it out", + "近接:": "Proximity:", + "送信中": "Sending", + "送信例": "Sending example", + "適用中": "Applying", + "選んで": "choose", + "開大比": "Opening ratio", + "非圧縮": "uncompressed", + "非対応": "Not compatible", + "項目は": "The items are", + "顔の色": "Face color", + "顔描画": "face drawing", + "この": "This", + "これ": "This", + "した": "did", + "です": "It is", + "など": "etc.", + "のみ": "only", + "への": "to", + "まず": "First", + "キー": "key", + "一行": "one line", + "上の": "above", + "上限": "upper limit", + "下の": "below", + "不足": "shortage", + "中に": "inside", + "中の": "inside", + "中断": "interruption", + "以降": "From then on", + "件)": ")", + "任意": "Any", + "会話": "Conversation", + "低め": "Low", + "保存": "save", + "停止": "Stop", + "元の": "original", + "公式": "official", + "内蔵": "Built-in", + "内部": "inside", + "再生": "Play", + "切断": "Disconnect", + "削除": "Delete", + "前後": "Before and after", + "半径": "radius", + "半音": "semitone", + "単位": "unit", + "単色": "Single color", + "原点": "origin", + "取得": "acquisition", + "合成": "synthesis", + "合計": "total", + "周期": "period", + "呼吸": "breathing", + "回転": "rotation", + "圧縮": "compression", + "基板": "Board", + "失敗": "failure", + "完了": "Completed", + "実行": "execution", + "家庭": "home", + "平均": "average", + "底辺": "bottom", + "必須": "Required", + "指標": "Metric", + "接続": "Connect", + "撮影": "Capture", + "書込": "Write", + "最大": "maximum", + "最小": "minimum", + "有効": "On", + "核、": "nucleus,", + "標準": "standard", + "正式": "formal", + "深度": "depth", + "渡し": "delivery", + "点滅": "blinking", + "点灯": "lit", + "無効": "Off", + "無線": "wireless", + "特殊": "special", + "状態": "condition", + "現在": "Currently", + "現行": "current", + "登録": "Registration", + "秒前": "seconds ago", + "空欄": "Blank field", + "経由": "Via", + "自動": "automatic", + "表示": "Show", + "設定": "settings", + "診断": "diagnosis", + "話す": "Speak", + "認証": "Authentication", + "読み": "reading", + "読取": "Read", + "読込": "Load", + "調整": "adjustment", + "超過": "excess", + "輝度": "brightness", + "近い": "close", + "近接": "proximity", + "追加": "Add", + "送信": "send", + "連動": "interlocking", + "連続": "continuous", + "遠い": "far", + "適用": "Apply", + "開口": "opening", + "開始": "start", + "電流": "current", + "音声": "Audio", + "音量": "Volume", + "頂点": "apex", + "領域": "area", + "飽和": "saturation", + "か": "Or", + "が": "But", + "つ": "Two", + "で": "In", + "と": "and", + "に": "to", + "の": "of", + "は": "Ha", + "を": "The", + "中": "Medium", + "件": "matter", + "例": "example", + "値": "value", + "前": "Previous", + "口": "mouth", + "回": "times", + "度": "degree", + "核": "nuclear", + "目": "eyes", + "眉": "eyebrows", + "秒": "seconds", + "空": "sky", + "緑": "green", + "色": "color", + "虹": "rainbow", + "行": "line", + "速": "speed", + "閉": "closed", + "開": "Open", + "頭": "head", + "風": "wind" + } +} diff --git a/tools/settings_i18n/overrides.json b/tools/settings_i18n/overrides.json new file mode 100644 index 0000000..6ddace0 --- /dev/null +++ b/tools/settings_i18n/overrides.json @@ -0,0 +1,17 @@ +{ + "有効": "On", + "無効": "Off", + "名称不明": "Unknown device", + "自動生成": "auto-generated", + "センサーなし": "No sensor", + " ": " ", + "

\n ℹ️ タブを切り替えて設定 → 保存 & 再起動 で確定。\n 接続後、即反映する項目 (アバター DSL / LT) は個別ボタンが付いています。\n

": "

\n ℹ️ Switch tabs and configure settings, then select Save & reboot.\n Once connected, settings that apply immediately (Avatar DSL and LT) have their own buttons.\n

", + "

\n device が STA 経由で GitHub Pages から自分宛のビルドを取得し\n OTA 適用します。AP モード中でも STA がアップであれば動作 (APSTA)。STA が\n ダウンの場合はエラーになるので、上の「ローカル ファイル」を使ってください。\n

": "

\n The device downloads the matching build from GitHub Pages through its STA link and\n applies it over OTA. This also works in AP mode when STA is connected (APSTA). If STA\n is down, use Local file above instead.\n

", + "

\n サーボの取り付け向きや可動範囲はベース基板で異なります。原点(0..1023)と可動範囲(度)を\n 設定して「保存して再起動」で反映してください。既定は M5 base の値です。\n

": "

\n Servo orientation and travel range vary by base board. Set the center (0..1023) and\n range in degrees, then select Save & reboot. Defaults match the M5 base.\n

", + "

\n 範囲設定モードを ON にするとサーボの力が抜け、手で動かした位置を取り込めます。\n 中央位置で「Yaw 原点取込」、左端で「Yaw 最小取込」、右端で「Yaw 最大取込」を順にタップしてください。\n 取り込んだ値は上の入力欄に反映されます。最後に「保存 & 再起動」で確定します。\n

": "

\n Turning on range-setting mode releases the servos so you can move them by hand.\n Tap Capture Yaw center at the center position, Capture Yaw minimum\n at the left limit, and Capture Yaw maximum at the right limit. Important: use the\n corresponding Yaw and Pitch capture buttons in the order shown. Captured values appear in\n the fields above. Finally, select Save & reboot to apply them.\n

", + " const PROX_SECTION_HTML = `\n

近接センサー (CoreS3)

\n \n

画面上辺のセンサーに手を近づけると、うれしい顔になって近接フレーズ (音声タブで設定、無ければ吹き出しのみ) を話します。会話中は反応しません。値は反射光量 (0〜2047) で、既定の前段 (ゲイン x64・15 パルス) では何もないとき 200 前後、既定の閾値は near 335 / far 240 です。すべて即時反映。

\n
\n
\n 現在値\n —\n \n \n \n
\n \n
\n \n 赤の点線 = near、緑 = far。接続すると自動で更新が始まります (0.5 秒ごと)。\n
\n
\n
\n \n \n \n \n
\n
\n \n \n
\n

far は near より小さくしてください (近い→遠いのちらつき防止)。反応が敏感すぎるなら near を上げるか連続時間を伸ばします。上の 4 項目は「閾値を適用」を押した時点で機体に反映・保存されます (ページ下部の保存でも送られます)。

\n
\n センサー調整モード (受光ゲイン / LED / 測定周期 / オフセット)\n

LTR-553 の前段を変えると生の値のスケールが変わります。「現在値を表示」を有効にしたまま値を変えて「センサーに適用」を押し、望む距離で十分な差が出る組み合わせを探してから near / far を決め直してください。到達距離を伸ばすなら ゲイン↑・電流↑・パルス数↑ の順に効きます (飽和したら「飽和」と出ます)。オフセットは何もない時の値 (クロストーク) を引き算します。

\n
\n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n \n \n \n \n
\n

オフセット: 何もかざしていない状態で「今の値をオフセットにする」→「センサーに適用」すると、床の値 (クロストーク) をチップ内で引き算して 0 付近にできます。ゲインを上げたときは先にこれをやると near / far を小さいままにできます (オフセットは機体ごとに違うので既定値にはしません)。

\n
`;": " const PROX_SECTION_HTML = `\n

Proximity sensor (CoreS3)

\n \n

Bring your hand near the sensor above the screen to show a happy face and speak a proximity phrase (configured on the Audio tab; if none is set, only the speech bubble appears). It does not react during a conversation. The reading is reflected light (0–2047): with the default front end (gain x64, 15 pulses), an empty scene reads around 200. The default thresholds are near 335 / far 240. All changes apply immediately.

\n
\n
\n Current value\n —\n \n \n \n
\n \n
\n \n Red dotted line = near; green = far. Updates start automatically when connected (every 0.5 seconds).\n
\n
\n
\n \n \n \n \n
\n
\n \n \n
\n

Keep far below near to prevent close/distant flicker. If the sensor is too sensitive, increase near or the hold time. These four values are sent and saved to the device when you select Apply thresholds (the Save button at the bottom also sends them).

\n
\n Sensor tuning (receiver gain / LED / measurement rate / offset)\n

Changing the LTR-553 front end changes the scale of the raw reading. Keep Live updates enabled, adjust the values, and select Apply to sensor. Find a combination that gives enough separation at the distance you want, then set near / far again. To increase range, try higher gain first, then higher LED current, then more pulses. The state shows Saturated when the receiver clips. Offset subtracts the no-target baseline (crosstalk).

\n
\n \n \n \n \n \n \n \n \n \n \n \n \n
\n
\n \n \n \n \n
\n

To compensate for crosstalk, hold nothing in front of the sensor, select Use current value as offset, then Apply to sensor. The chip subtracts that baseline and the reading returns near zero. Do this before increasing gain to keep near / far small. Offsets vary by device, so this is not a fixed default.

\n
`;", + "コンパイル & 送信": "Compile & send", + "\n /\n": "\n /\n", + "範囲設定モード: OFF": "Range-setting mode: OFF", + "範囲設定モード: ON": "Range-setting mode: ON" +} diff --git a/tools/settings_i18n/polish.json b/tools/settings_i18n/polish.json new file mode 100644 index 0000000..e6e4ca0 --- /dev/null +++ b/tools/settings_i18n/polish.json @@ -0,0 +1,237 @@ +{ + "Stuck-chan": "Stack-chan", + "Stack Chan": "Stack-chan", + "The rest is Jyugobyo.": "Fifteen seconds remaining.", + "My name is Ojikan.": "Time's up!", + "It's all left.": "One minute remaining.", + "It's time!": "Time's up!", + "(If blank, same as excess)": "(Blank uses overtime copy)", + "Hey there? | Hey there It's close ...": "What is it? | what is it You're close ...", + "Hello | hello Good morning ...": "Hello | hello This is Stack-chan | this is Stack-chan Hi there", + "Hello | hello This is Stack Chan | I'm Stuck-chan Yahoo": "Hello | hello This is Stack-chan | this is Stack-chan Hi there", + "(Example) Hello, this is Stuck-chan.": "(Example) Hello, this is Stack-chan.", + "save & restart": "Save & reboot", + "Save & restart": "Save & reboot", + "Save & restart": "Save & reboot", + "save and restart": "save and reboot", + "Save and restart": "Save and reboot", + "saving & restarting": "saving and rebooting", + "saving and restarting": "saving and rebooting", + "When configured, /mcp/* When configured, the HTTP API is enabled and the Channel adapter can connect. When empty, /mcp/* returns 404. Takes effect after saving and rebooting.": "When configured, the HTTP API is enabled at /mcp/* so the Channel adapter can connect. When empty, /mcp/* returns 404. Takes effect after saving and rebooting.", + "When configured, /mcp/* When configured, the HTTP API is enabled and the Channel adapter can connect. When empty, /mcp/* returns 404.": "When configured, the HTTP API is enabled at /mcp/* so the Channel adapter can connect. When empty, /mcp/* returns 404.", + "Moving a slider updates the preview below and, while connected, thedevice face in real timeto\n will change(No reboot required)。To save your favorite settings「save & restart」Please press\n (Reboot and confirm)。": "Move a slider to update the preview below and, while connected, the\n device face in real time. No reboot is required. To keep your settings, select\n Save & reboot (the device will restart to apply them).", + "The mounting direction and movable range of the servo vary depending on the base board.。origin(0..1023)and range of motion(degree)The\n Set it「save & restart」Please reflect in。The default is M5 base is the value of。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and\n range in degrees, then select Save & reboot. Defaults match the M5 base.", + "The mounting direction and movable range of the servo vary depending on the base board.。origin(0..1023)and range of motion(degree)The Set it「save & restart」Please reflect in。The default is M5 base is the value of。": "Servo orientation and travel range vary by base board. Set the center (0..1023) and range in degrees, then select Save & reboot. Defaults match the M5 base.", + "Upload a real-voice mora unit database to use TD-PSOLA unit-concatenation synthesis for JTTS.tools/jvox/ A .jvox generated by this tool (ADPCM recommended, about 230 KB). Applies immediately and persists across reboots once saved to NVS.": "Upload a real-voice mora unit database to use TD-PSOLA unit-concatenation synthesis for JTTS. Generate the .jvox file with the tools/jvox/ tool (ADPCM recommended, about 230 KB). Applies immediately and persists across reboots once saved to NVS.", + "The weights are not MIT-licensed. Before downloading, review\n LICENSE-MODEL.md\n the license. Under the Tsukuyomi-chan Corpus terms, synthesized audio may not be used to attack or criticize individuals or groups, promote political or religious views, include extreme content without a warning, or be distributed as reusable material.": "The model weights are not MIT-licensed. Before downloading, review the\n LICENSE-MODEL.md\n terms. Under the Tsukuyomi-chan Corpus terms, synthesized audio may not be used to attack or criticize individuals or groups, promote political or religious views, include extreme content without a warning, or be distributed as reusable material.", + "※ firmware-vX.Y.Z-<board>.bin Do not select\n stackchan_idf.bin the latter is the combined bootloader, partition table, and app image for initial USB flashing and cannot be used for OTA.": "Note: select firmware-vX.Y.Z-<board>.bin, not\n stackchan_idf.bin. The latter is the combined bootloader, partition table, and app image for initial USB flashing and cannot be used for OTA.", + "Applies to the built-in speaker, Module Audio, and Atomic Echo Base. Applies immediately and saves automatically (no reboot). 100% is the board's factory default; 200% boosts it.": "Applies to the built-in speaker, Module Audio, and Atomic Echo Base. Applies immediately and is saved automatically (no reboot). 100% is the board's factory default; 200% boosts it.", + "Applies to the built-in speaker, Module Audio, and Atomic Echo Base. Applies immediately and saves automatically (no reboot). 100% is the board's factory default; 200% doubles the internal master volume and saturates at 255.": "Applies to the built-in speaker, Module Audio, and Atomic Echo Base. Applies immediately and is saved automatically (no reboot). 100% is the board's factory default; 200% doubles the internal master volume and saturates at 255.", + "Opens and closes the mouth based on microphone volume. CoreS3 microphone sensitivity is low, so a higher input gain usually looks better.\n and saves automatically.": "Opens and closes the mouth based on microphone volume. CoreS3 microphone sensitivity is low, so a higher input gain usually looks better. Changes are saved automatically.", + "Opens and closes the mouth based on microphone volume. CoreS3 microphone sensitivity is low, so a higher input gain usually looks better. and saves automatically.": "Opens and closes the mouth based on microphone volume. CoreS3 microphone sensitivity is low, so a higher input gain usually looks better. Changes are saved automatically.", + "${v.label} (": "${v.label_en || v.label} (", + "return hit ? hit.label : null;": "return hit ? (hit.label_en || hit.label) : null;", + "stable: 'formal'": "stable: 'Stable'", + "for every Provider": "for every provider", + "Shared by all Providers.": "Shared by all providers.", + "Status: Acquisition failed": "Status: Download failed", + "Status: This firmware does not contain the sanoTTS engine (cannot be retrieved or written to)": "Status: this firmware does not include the sanoTTS engine", + "List acquisition failure": "Failed to load list", + "tag Acquisition failure": "Failed to load tags", + "Acquisition failure": "Download failed", + "acquisition failure": "failed to load", + "Retrieval failure": "Download failed", + "retrieval failure": "failed to load", + "Connection failure": "Connection failed", + "connection failure": "connection failed", + "Reflection failed": "Failed to apply", + "reflection failed": "failed to apply", + "Transmission failure": "Send failed", + "transmission failure": "transmission failed", + "Reset failure": "Reset failed", + "reset failure": "reset failed", + "Range setting mode switching failure": "Failed to switch range-setting mode", + "Camera shooting failure": "Capture failed", + "camera shooting failure": "capture failed", + "Voice acquisition + installation completed": "Voice installed", + "Voice acquisition failure": "Failed to install voice", + "voice acquisition failure": "failed to download voice", + "Voice DB registration failure": "Failed to register voice database", + "Voice DB deletion failed": "Failed to delete voice database", + "HMM voice registration failure": "Failed to register HMM voice", + "HMM voice deletion failure": "Failed to delete HMM voice", + "sanoTTS acquisition failure": "sanoTTS failed", + "sanoTTS upload failure": "Failed to upload sanoTTS model", + "sanoTTS status acquisition failure": "Failed to get sanoTTS status", + "LT configuration sending failure": "Failed to send LT settings", + "Avatar DSL transmission failure": "Avatar DSL send failed", + "Avatar DSL reset failure": "Failed to reset avatar DSL", + "Status Read failure": "Failed to read status", + "Register access failure": "Register access failed", + "idle utterance status acquisition failure": "Failed to read idle speech status", + "LED interlocking status acquisition failure": "Failed to read LED sync status", + "Operation mode Status acquisition failure": "Failed to get operation mode", + "microphone lip-sync read failure": "Failed to read mic lip-sync setting", + "microphone lip-sync write failure": "Failed to write mic lip-sync setting", + "mic-lip-gain read failure": "Failed to read mic lip-sync gain", + "mic-lip-gain write failure": "Failed to write mic lip-sync gain", + "speaker-volume read failure": "Failed to read speaker volume", + "speaker-volume write failure": "Failed to write speaker volume", + "jtts trial utterance failure": "JTTS test speech failed", + "sanoTTS ${cmd.op} Failure": "sanoTTS ${cmd.op} failed", + "Refused by the aircraft side": "Rejected by the device", + "refused by the aircraft side": "rejected by the device", + "Failure:": "Failed:", + "The aircraft is obtained from GitHub Releases (ayutaz/sanoTTS-jp ${release}/${file}). Do you agree to the terms of LICENSE-MODEL.md?": "Download the sanoTTS model from GitHub Releases (ayutaz/sanoTTS-jp ${release}/${file})? By continuing, you agree to the LICENSE-MODEL.md terms.", + "The aircraft is downloading... (about 30 seconds)": "Downloading on the device... (about 30 seconds)", + "The aircraft will retrieve and write ${releaseTag} via Wi-Fi. Are you sure?": "The device will download and install ${releaseTag} over Wi-Fi. Continue?", + "Instruct the aircraft to obtain ${tag}...": "Downloading ${tag} on the device...", + "OTA Complete — The aircraft will boot the new firmware and automatically proceed to boot confirmation.": "Update complete — the device will reboot into the new firmware and verify the boot automatically.", + "Write new farm ${name}": "Install firmware ${name}", + "Skip applying because there is no Apply chr (old farm?)": "Skipped apply: the Apply characteristic is missing (old firmware?)", + "Apply Done — Device will restart": "Apply complete — device will reboot", + "Done — reboot": "Update complete — reboot required", + "OTA completed — reboot to new farm": "OTA complete — reboot into the new firmware", + "release OTA completed — reboot to new farm": "Release update complete — reboot to use the new firmware", + "Release OTA completed — reboot to new farm": "Release update complete — reboot to use the new firmware", + "jtts settings sending": "Sending JTTS settings", + "jtts settings transmission completed": "JTTS settings sent", + "MCP API Token Send": "Sending MCP API token", + "Obtaining...": "Downloading...", + "Obtaining…": "Downloading…", + "Getting...": "Downloading...", + "Getting…": "Downloading…", + "sanoTTS weights retrieved + saved": "sanoTTS model downloaded and saved", + "Start retrieving sanoTTS weights": "Downloading sanoTTS model", + "via aircraft Wi-Fi": "over the device's Wi-Fi", + "The aircraft": "The device", + "the aircraft": "the device", + "Aircraft": "Device", + "aircraft": "device", + "old farm": "old firmware", + "new farm": "new firmware", + "Not applicable": "Not applied", + "save & restart": "Save & reboot", + "save & reboot": "Save & reboot", + "HTTP connecting": "HTTP: Connected", + "Wi-Fi: Connecting": "Wi-Fi: Connected", + "⚡ Immediate reflection": "⚡ Applies immediately", + "Immediate reflection / No need to restart": "Applies immediately; no reboot required", + "Immediate reflection/": "Applies immediately; ", + "s) — Immediate reflection": "s) — Applies immediately", + "Status: saved/not loaded": "Status: Saved, not loaded", + "Status: Saved/Not loaded": "Status: Saved, not loaded", + "Status: saved/unloaded": "Status: Saved, not loaded", + "Status: Saved/Not loaded (Insufficient memory?)": "Status: Saved, not loaded (insufficient memory)", + "Status: Unregistered (formant synthesis)": "Status: Not installed (formant synthesis)", + "Status: Unregistered": "Status: Not installed", + "LT Settings: Farm Defaults": "LT settings: Firmware defaults", + "Servo settings: Farm default values": "Servo settings: Firmware defaults", + "Avatar Adjustment: Farm Defaults": "Avatar adjustments: Firmware defaults", + "jtts settings: farm defaults": "JTTS settings: Firmware defaults", + "Farm Defaults": "Firmware defaults", + "Farm default values": "Firmware defaults", + "Farm default value": "Firmware default", + "${label} Transmission complete — ${r.saved} bytes saved to NVS${r.note || ''}": "${label} sent — ${r.saved} bytes saved to NVS${r.note || ''}", + "${label} Transmission completed — Response:": "${label} sent — Response:", + "${label} Transmission failure — ${r.error || 'unknown'}": "${label} send failed — ${r.error || 'unknown'}", + "Avatar DSL transmission OK": "Avatar DSL sent", + "Audio transmission has been interrupted": "Audio sending was interrupted", + "Audio transmission completed": "Audio sent", + "Audio transmission error": "Audio send error", + "Data transmission completed": "Data transfer complete", + "Transmission completed — Response:": "Sent — Response:", + "Transmission completed": "Sent", + "Transmission failed": "Send failed", + "SSID transmission": "SSID sent", + "key transmission": "key sent", + "Characteristics acquisition completed": "Characteristics loaded", + "jtts idle utterance status acquisition failure": "Failed to get JTTS idle-speech status", + "jtts idle utterance state acquisition failure": "Failed to get JTTS idle-speech status", + "Provider acquisition failure": "Failed to load provider", + "Release acquisition failure": "Failed to load releases", + "sanoTTS acquisition:": "sanoTTS status:", + "voice acquisition": "voice install", + "Acquisition completed": "Download complete", + "Retrieval does not start even after waiting 90 seconds (Check Wi-Fi/tag)": "Download did not start after 90 seconds. Check Wi-Fi and the release tag.", + "Even after waiting a few seconds, the acquisition does not start": "The download did not start. Check Wi-Fi and the release tag.", + "Retrieved directly from": "Downloaded directly from", + "Retrieved from": "Downloaded from", + "Obtaining release ${tag} on device side...": "Downloading release ${tag} on the device...", + "Obtaining release ${tag} on device side…": "Downloading release ${tag} on the device…", + "Obtaining on the side": "Downloading on the device", + "Obtaining status": "Getting status", + "Obtaining:": "Downloading:", + "Obtained:": "Downloaded:", + "obtaining, etc. Update the status and check": "downloading, etc. Refresh the status and try again", + "ms/times": "ms", + "Status Read failure → Authentication password may have already been set on the device side.": "Failed to read status → The device may already have an authentication password.", + "Return to no authentication (blank). It will be reflected after saving and rebooting. Are you sure?": "Return to no authentication? Leave the password blank, save, and reboot to apply.", + "When the conversation function is ON, it cannot be played because it is exclusive. Please turn it off, save and restart.": "Audio playback is unavailable while Conversation is on. Turn off Enable conversation, save, and reboot.", + "Cannot be used because conversation function is enabled": "Unavailable while Conversation is on", + "Automatically disabled when conversation function is on": "Automatically disabled while Conversation is on", + "When conversation function is off": "When Conversation is off", + "OpenAI conversation function": "OpenAI conversation", + "conversation function": "Conversation", + "It is exclusive because it is interrupted.": "Audio playback is unavailable while Conversation is on.", + "Time cannot be played because it is exclusive.": "Audio playback is unavailable while Conversation is on.", + "It will be reflected and saved on the aircraft as soon as you press .": "Press Apply to save the changes to the device.", + "It will be reflected and saved on the device as soon as you press .": "Press Apply to save the changes to the device.", + "(Writing only to NVS, live reflection failed)": "(Saved to NVS, but applying the live change failed)", + "live reflection failed": "applying the live change failed", + "The offset is different for each aircraft, so do not set it as the default value.": "Sensor offset varies by device; do not use it as a fixed default.", + "The offset is different for each device, so do not set it as the default value.": "Sensor offset varies by device; do not use it as a fixed default.", + "Loaded — Please use “Compile & Send” to reflect on the actual device": "Loaded. Select Compile & send to apply it to the device.", + "Apply completed — device will reboot after ~200ms": "Applied. The device will reboot in about 200 ms.", + "OTA Complete — The device will boot the new firmware and automatically proceed to boot confirmation.": "Update complete — the device will reboot into the new firmware and verify the boot automatically.", + "Saved/Loaded": "Saved and loaded", + "Saved/unloaded": "Saved, not loaded", + "Unregistered": "Not installed", + "The aircraft is official": "Official device", + "The aircraft side refuses": "The device rejected the request", + "of the aircraft": "of the device", + "New farm": "New firmware", + "farm defaults": "firmware defaults", + "farm default values": "firmware defaults", + "farm default value": "firmware default", + "/": "/", + "Choose one of three main behaviors. Conversation talks with OpenAI, Gemini, or XiaoZhi. JTTS random speech only makes idle spoken phrases. Mic lip sync moves the mouth in response to microphone input. Changes take effect after saving and rebooting.": "Choose an operation mode. Conversation talks with the configured AI service. JTTS random speech plays spoken phrases while idle. Mic lip sync moves the mouth in response to microphone input. Additional modes provide on-device speech recognition or ESP-NOW control. Changes take effect after saving and rebooting.", + "(Automatically if blank)": "Auto if blank", + "(Leave blank if not changed)": "Leave blank to keep current", + "AIza… (Leave blank if not changed)": "AIza… (leave blank to keep)", + "sk-… (leave blank if not changed)": "sk-… (leave blank to keep)", + "ws://192.168.1.10:8000/xiaozhi/v1/ (leave blank if not changed)": "ws://192.168.1.10:8000/xiaozhi/v1/ (leave blank to keep)", + "(Optional/blank if no change)": "(Optional; leave blank to keep)", + "(If blank, do not change / Clear to return to no authentication)": "(Leave blank to keep; Clear to disable authentication)", + "Failed to obtain provider:": "Failed to read provider:", + "Gemini model read failed:": "Failed to read Gemini model:", + "Gemini voice read failed:": "Failed to read Gemini voice:", + "Failed to obtain avatar adjustment:": "Failed to read face settings:", + "SSID writing completed": "SSID saved", + "Password writing completed": "Password saved", + "OpenAI key writing completed": "OpenAI key saved", + "Gemini key writing completed": "Gemini key saved", + "XiaoZhi URL writing completed": "XiaoZhi URL saved", + "XiaoZhi token writing completed": "XiaoZhi token saved", + "Servo setting writing completed": "Servo settings saved", + "LED state read failure:": "Failed to read LED state:", + "LED state write failure:": "Failed to write LED state:", + "My Clip Sync": "mic lip-sync", + "Failed to get mic lip-sync settings:": "Failed to read mic lip-sync settings:", + "Failed to write mic lip-sync settings:": "Failed to write mic lip-sync settings:", + "Since there is no current value, please enable \"Show current value\" first.": "Enable \"Show current value\" first.", + "Applied — please redefine near/far": "Applied. Recheck the near/far thresholds.", + "No current value (please connect and wait for real-time updates)": "No current reading. Connect the device and wait for live updates.", + "OTA Complete — Reboot the device. The startup confirmation of the new firmware will be performed automatically. If BLE is disconnected, please reconnect.": "Update complete. Reboot the device; the new firmware will confirm its startup automatically. Reconnect BLE if it disconnects.", + "Web Bluetooth cannot be used. Please open in Chrome/Edge desktop.": "Web Bluetooth is unavailable. Open this page in desktop Chrome or Edge.", + "Do you want to delete saved sanoTTS weights? (Fallback to HMM/other engines from now on)": "Delete the saved sanoTTS weights? The device will fall back to HMM or another engine.", + "This firmware does not support sanoTTS operation from BLE": "This firmware does not support sanoTTS over BLE", + "This firmware does not support sanoTTS over BLE": "This firmware does not support sanoTTS over BLE", + "sanoTTS: Please enter release and file": "sanoTTS: enter a release and file", + "Rejected by the device — Wi-Fi not connected/downloading, etc. Refresh the status and try again": "The device rejected the request (for example, Wi-Fi is disconnected or another download is in progress). Refresh the status and try again.", + "Failed to obtain provider: ${e.message}": "Failed to read provider: ${e.message}", + "Gemini model read failed: ${e.message}": "Failed to read Gemini model: ${e.message}", + "Gemini voice read failed: ${e.message}": "Failed to read Gemini voice: ${e.message}", + "Failed to obtain avatar adjustment: ${e.message}": "Failed to read face settings: ${e.message}", + "「current value near to make」": "\"current value near to make\"" +} diff --git a/tools/settings_i18n/translate.py b/tools/settings_i18n/translate.py new file mode 100755 index 0000000..562d9a2 --- /dev/null +++ b/tools/settings_i18n/translate.py @@ -0,0 +1,388 @@ +#!/usr/bin/env python3 +# SPDX-FileCopyrightText: 2026 Kenta IDA +# SPDX-License-Identifier: BSL-1.0 +"""Build-time English settings-page generator. + +The Japanese pages remain the upstream-maintained sources. The Gemini product +build runs this deterministic transformer and embeds/serves the result. The +catalog is checked in so a firmware build needs no network access. + +Usage: + translate.py CATALOG INPUT OUTPUT [... INPUT OUTPUT ...] + translate.py --check CATALOG FILE [...] + +If optional overrides.json and polish.json files sit next to CATALOG, the +overrides replace generated translations and the polish entries run as a +second, exact-string copyediting pass over the resulting English. All files are +checked in; builds never call a translation service. + +A source string that is still Japanese after all catalog replacements is a +hard error: adding a new Japanese UI string upstream must also add its English +translation here rather than silently shipping a mixed-language page. +""" + +from __future__ import annotations + +import argparse +import json +import re +import sys +from pathlib import Path +from urllib.parse import quote + +JA = re.compile( + r"[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f]" +) + +# Google Cloud icon geometry from the official multicolour cloud mark in +# https://commons.wikimedia.org/wiki/File:Google_Cloud_logo.svg. Keeping the +# vector inline makes the firmware-served SoftAP page fully offline while +# adding only a few kilobytes to the already-injected HTML. +GOOGLE_CLOUD_ICON_SVG = ( + '' + '' + '' + '' + '' + '' +) +GOOGLE_CLOUD_FAVICON = 'data:image/svg+xml,' + quote(GOOGLE_CLOUD_ICON_SVG, safe='') +GOOGLE_CLOUD_HEADER_ICON = GOOGLE_CLOUD_ICON_SVG.replace( + '

\s*(?:🤖\s*)?Stack-chan settings(?: \(Wi-Fi\))?\s*

", + re.IGNORECASE, + ) + favicon = f'\n' + heading = ( + '

' + f'{GOOGLE_CLOUD_HEADER_ICON}{GOOGLE_CLOUD_TITLE}

' + ) + text, title_count = title_pattern.subn(f'{GOOGLE_CLOUD_TITLE}{favicon}', text, count=1) + text, heading_count = heading_pattern.subn(heading, text, count=1) + if title_count != 1 or heading_count != 1: + raise SystemExit( + "error: settings branding markers not found; update apply_google_branding() " + "for the changed upstream title/header markup" + ) + return text + + +def translate(text: str, pairs: list[tuple[str, str]], polish: list[tuple[str, str]]) -> str: + # Some catalog entries intentionally retain interpolated fragments, such as + # `${enabled ? '有効' : '無効'}`. Repeat only the Japanese-to-English scan so + # those fragments are translated without ever feeding English output + # through English copyediting rules. + japanese_pairs = [(source, target) for source, target in pairs if JA.search(source)] + for _ in range(16): + translated = replace_all(text, japanese_pairs) + if translated == text or not JA.search(translated): + text = translated + break + text = translated + text = replace_all(text, polish) + text = text.replace('', '') + text = text.replace("", "") + return strip_japanese_comments(text) + + +def strip_script_comments(segment: str) -> str: + """Remove Japanese JS/CSS comments without treating comment text as code. + + A naive ``/\\*.*?\\*/`` pass corrupts real pages: ``// ... /mcp/*`` is a + line comment, not the beginning of a block comment. Scan strings and line + comments first so only actual comment spans containing Japanese are + removed. Comment-looking text inside regex literals is uncommon and is + left intact unless it looks like a complete block comment; this is safe + because we only remove spans that contain Japanese, and user-visible + strings have already been translated. + """ + output: list[str] = [] + i = 0 + quote: str | None = None + while i < len(segment): + c = segment[i] + if quote is not None: + output.append(c) + if c == "\\" and i + 1 < len(segment): + output.append(segment[i + 1]) + i += 2 + continue + if c == quote: + quote = None + i += 1 + continue + + if c in ("'", '"', "`"): + quote = c + output.append(c) + i += 1 + continue + + if c == "/" and i + 1 < len(segment) and segment[i + 1] == "/": + end = segment.find("\n", i) + if end < 0: + end = len(segment) + comment = segment[i:end] + output.append(" " if JA.search(comment) else comment) + i = end + continue + + if c == "/" and i + 1 < len(segment) and segment[i + 1] == "*": + end = segment.find("*/", i + 2) + if end < 0: + end = len(segment) - 2 + end += 2 + comment = segment[i:end] + if JA.search(comment): + # Keep line count stable while ensuring adjacent tokens do not + # become one token on an inline comment removal. + output.append("".join(ch if ch == "\n" else "" for ch in comment) or " ") + else: + output.append(comment) + i = end + continue + + output.append(c) + i += 1 + return "".join(output) + + +def strip_japanese_comments(text: str) -> str: + """Remove Japanese comments left in the generated artifact. + + The source pages keep their Japanese comments for upstream maintenance. + The shipped Gemini artifacts do not need them, and removing them keeps a + simple residual-character audit meaningful. Only comments containing + Japanese are touched; English comments (including license headers) stay. + """ + + # HTML comments can span lines. A comment is not user-visible, but ships in + # the page source, so drop it when it still contains Japanese. + def html_comment(match: re.Match[str]) -> str: + return "" if JA.search(match.group(0)) else match.group(0) + + text = re.sub(r"", html_comment, text, flags=re.DOTALL) + + # Preset DSL sources are serialized into a JSON string, so their comments + # appear as literal "\n-- ..." sequences rather than physical lines. + def escaped_dsl_comment(match: re.Match[str]) -> str: + return "\\n" if JA.search(match.group(0)) else match.group(0) + + text = re.sub(r'[ \t]*--[^\\\n]*(?=\\n|")', escaped_dsl_comment, text) + + # Process script/style bodies as code. Matching the whole HTML with a block + # comment regex is unsafe: '/mcp/*' inside a JavaScript line comment would + # swallow code through the next unrelated '*/'. + def script_body(match: re.Match[str]) -> str: + return match.group(1) + strip_script_comments(match.group(2)) + match.group(3) + + for tag in ("script", "style"): + text = re.sub( + rf"(<{tag}\b[^>]*>)(.*?)()", + script_body, + text, + flags=re.DOTALL | re.IGNORECASE, + ) + return text + + +def residual_lines(text: str) -> list[tuple[int, str]]: + return [(i, line.rstrip()) for i, line in enumerate(text.splitlines(), 1) if JA.search(line)] + + +def copyedit_lines(text: str) -> list[tuple[int, str]]: + return [(i, line.rstrip()) for i, line in enumerate(text.splitlines(), 1) if BAD_ENGLISH.search(line)] + + +def branding_lines(text: str) -> list[tuple[int, str]]: + issues: list[tuple[int, str]] = [] + if f"{GOOGLE_CLOUD_TITLE}" not in text: + issues.append((0, "missing branded document title")) + if "data:image/svg+xml," not in text: + issues.append((0, "missing inline SVG favicon")) + if f"{GOOGLE_CLOUD_TITLE}" not in text: + issues.append((0, "missing branded page heading")) + return issues + + +def report(path: Path, text: str, require_branding: bool = False) -> bool: + failed = False + checks = [("Japanese", residual_lines(text)), ("banned English", copyedit_lines(text))] + if require_branding: + checks.append(("missing Google Cloud branding", branding_lines(text))) + for kind, lines in checks: + if not lines: + continue + failed = True + print(f"error: {path}: {kind} remains in {len(lines)} line(s)", file=sys.stderr) + for line_no, line in lines[:40]: + print(f" {line_no}: {line}", file=sys.stderr) + if len(lines) > 40: + print(f" ... {len(lines) - 40} more", file=sys.stderr) + return failed + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("catalog", type=Path) + parser.add_argument("--check", action="store_true", help="check files without writing") + parser.add_argument("paths", nargs="+", type=Path) + args = parser.parse_args() + + pairs = load_catalog(args.catalog) + polish = load_polish(args.catalog) + if args.check: + failed = False + for path in args.paths: + is_html = path.suffix.lower() in (".html", ".htm") + if report(path, path.read_text(encoding="utf-8"), require_branding=is_html): + failed = True + return 1 if failed else 0 + + if len(args.paths) % 2: + parser.error("translation mode needs INPUT OUTPUT pairs") + failed = False + for input_name, output_name in zip(args.paths[::2], args.paths[1::2]): + source = input_name.read_text(encoding="utf-8") + result = translate(source, pairs, polish) + is_html = input_name.suffix.lower() in (".html", ".htm") + if is_html: + result = apply_google_branding(result) + if report(input_name, result, require_branding=is_html): + failed = True + continue + output_name.parent.mkdir(parents=True, exist_ok=True) + output_name.write_text(result, encoding="utf-8") + print(f"[settings-i18n] {input_name} -> {output_name}") + return 1 if failed else 0 + + +if __name__ == "__main__": + raise SystemExit(main())