From 220e4fab27b7f419b3e89e2f2d28ae789aac09d7 Mon Sep 17 00:00:00 2001
From: wangzhibo <wangzhibo@shsening.com>
Date: 星期四, 08 十月 2026 16:29:55 +0800
Subject: [PATCH] 5种场景模式家长端ok
---
xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/runtime.py | 394 +++++++++++++++++++++++++++++++++++++++++++++++++------
1 files changed, 347 insertions(+), 47 deletions(-)
diff --git a/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/runtime.py b/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/runtime.py
index 71dc2af..d4604fa 100644
--- a/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/runtime.py
+++ b/xiaozhi-esp32-server/main/xiaozhi-server/core/app_runtime/runtime.py
@@ -30,8 +30,10 @@
default_shadowing_playlist,
expand_playlist_plays,
lesson_day_index,
+ load_last_lesson_params,
mark_lesson_played_today,
- parse_playlist,
+ playlist_from_params,
+ remember_last_lesson_params,
resolve_play_speed,
speeds_for_repeats,
)
@@ -43,7 +45,7 @@
match_registry_voice,
voice_menu_text,
)
-from .session import begin_session, current_session, end_session, parse_skill_event
+from .session import begin_session, current_session, end_session, parse_skill_event, schedule_close_parent_except
if TYPE_CHECKING:
from core.connection import ConnectionHandler
@@ -70,6 +72,26 @@
"鐢ㄦ埛璇撮��鍑轰腑鑻卞璇濄�佸洖鍒拌亰澶╂椂璋冪敤 switch_app锛宎pp_id=chat銆�"
)
+BILINGUAL_PAIR_PROMPTS = {
+ "zh-en": BILINGUAL_PROMPT,
+ "zh-fr": (
+ "浣犳鍦ㄨ繘琛屼腑娉曞鐓у璇濈粌涔狅紝涓嶆槸缈昏瘧鏈猴紝涓嶈鎶婄敤鎴风殑璇濆師鏍疯瘧鍥炲幓浜ゅ樊銆�"
+ "鐢ㄦ埛璇翠腑鏂囨椂浣犵敤娉曡瀹屾暣鍥炵瓟锛涚敤鎴疯娉曡鏃朵綘鐢ㄤ腑鏂囧畬鏁村洖绛斻��"
+ "涓嶈涓ょ璇█鍚屾椂杈撳嚭銆傜敤鎴疯閫�鍑哄璇濄�佸洖鍒拌亰澶╂椂璋冪敤 switch_app锛宎pp_id=chat銆�"
+ ),
+ "zh-ja": (
+ "浣犳鍦ㄨ繘琛屼腑鏃ュ鐓у璇濈粌涔狅紝涓嶆槸缈昏瘧鏈猴紝涓嶈鎶婄敤鎴风殑璇濆師鏍疯瘧鍥炲幓浜ゅ樊銆�"
+ "鐢ㄦ埛璇翠腑鏂囨椂浣犵敤鏃ヨ瀹屾暣鍥炵瓟锛涚敤鎴疯鏃ヨ鏃朵綘鐢ㄤ腑鏂囧畬鏁村洖绛斻��"
+ "涓嶈涓ょ璇█鍚屾椂杈撳嚭銆傜敤鎴疯閫�鍑哄璇濄�佸洖鍒拌亰澶╂椂璋冪敤 switch_app锛宎pp_id=chat銆�"
+ ),
+}
+
+TRANSLATOR_PAIR_PROMPTS = {
+ "zh-en": TRANSLATOR_PROMPT,
+ "zh-fr": "浣犳槸涓�鍙扮炕璇戞満銆傚彧杈撳嚭璇戞枃銆備腑鏂囪瘧鎴愭硶璇紝娉曡璇戞垚涓枃锛涘叾瀹冭瑷�榛樿璇戞垚涓枃銆�",
+ "zh-ja": "浣犳槸涓�鍙扮炕璇戞満銆傚彧杈撳嚭璇戞枃銆備腑鏂囪瘧鎴愭棩璇紝鏃ヨ璇戞垚涓枃锛涘叾瀹冭瑷�榛樿璇戞垚涓枃銆�",
+}
+
CHAT_PROACTIVE_HINT = (
"浣犳槸鐢ㄦ埛鐨勮闊充紮浼淬�傝繘鍏ュ璇濆悗鍙互鍏堜富鍔ㄧ煭鍙ラ個璇疯亰澶╋紝渚嬪銆屾垜浠潵鑱婂ぉ鍚с�嶃��"
"鐢ㄦ埛寮�鍙e悗鎸夋甯稿璇濆洖绛旓紝涓嶈鍙嶅鍌績銆�"
@@ -92,8 +114,13 @@
SD_LAYOUT_MODES = ("PLAY_AUDIO", "SHADOWING")
SHADOWING_LISTEN_RATIO = 1.2
SHADOWING_LISTEN_MIN_MS = 800
-REPEATER_END_PHRASE = "澶嶈缁撴潫"
+REPEATER_END_PHRASE = "娉涘惉缁撴潫"
SHADOWING_END_PHRASE = "璺熻缁撴潫"
+REPEATER_START_PHRASE = "寮�濮嬫硾鍚换鍔�"
+SHADOWING_START_PHRASE = "寮�濮嬭窡璇讳换鍔�"
+CHAT_START_PHRASE = "寮�濮嬪璇濅换鍔�"
+TRANSLATION_START_PHRASE = "寮�濮嬬炕璇戜换鍔�"
+BILINGUAL_START_PHRASE = "寮�濮嬩腑鑻卞璇濅换鍔�"
def source_priority(source: str) -> int:
@@ -282,17 +309,30 @@
handler.upload_functions_desc()
-def _apply_prompt(conn: "ConnectionHandler", app: Dict[str, Any]) -> None:
+def _normalize_language_pair(raw: Any) -> str:
+ pair = str(raw or "zh-en").strip().lower().replace("_", "-")
+ if pair in ("cn-en", "zh-cn-en", "zhcn-en"):
+ return "zh-en"
+ return pair or "zh-en"
+
+
+def _apply_prompt(conn: "ConnectionHandler", app: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> None:
mode = app.get("system_prompt_mode") or "none"
if conn.original_system_prompt is None:
conn.original_system_prompt = conn.prompt
+ pair = _normalize_language_pair((params or {}).get("language_pair"))
+ conn.language_pair = pair
+ conn.language_pair_fallback = pair not in BILINGUAL_PAIR_PROMPTS and pair not in TRANSLATOR_PAIR_PROMPTS
+ prompt_pair = "zh-en" if conn.language_pair_fallback else pair
if mode == "translator":
+ pair_prompt = TRANSLATOR_PAIR_PROMPTS.get(prompt_pair, TRANSLATOR_PROMPT)
conn.change_system_prompt(
- "浣犵幇鍦ㄦ槸鑻辫缈昏瘧鏈恒�傜敤鎴疯璇濆悗搴旇皟鐢� translate锛涚敤鎴疯閫�鍑虹炕璇戙�佸洖鍒拌亰澶╂椂璋冪敤 switch_app锛宎pp_id=chat銆�"
+ pair_prompt
+ + "鐢ㄦ埛璇磋瘽鍚庡簲璋冪敤 translate锛涚敤鎴疯閫�鍑虹炕璇戙�佸洖鍒拌亰澶╂椂璋冪敤 switch_app锛宎pp_id=chat銆�"
"涓嶈璁茬瑧璇濄�佷笉瑕佹煡澶╂皵銆佷笉瑕侀棽鑱娿��"
)
elif mode == "bilingual":
- conn.change_system_prompt(BILINGUAL_PROMPT)
+ conn.change_system_prompt(BILINGUAL_PAIR_PROMPTS.get(prompt_pair, BILINGUAL_PROMPT))
elif mode == "agent_proactive":
base = conn.original_system_prompt or conn.prompt or ""
if CHAT_PROACTIVE_HINT not in (base or ""):
@@ -378,6 +418,28 @@
speak_txt(conn, text)
except Exception as e:
logger.bind(tag=TAG).warning(f"涓诲姩鎾姤澶辫触: {e}")
+
+
+async def _play_bind_prompt(conn: "ConnectionHandler") -> None:
+ from core.handle.receiveAudioHandle import check_bind_device
+
+ await check_bind_device(conn)
+
+
+async def prompt_bind_if_needed(conn: "ConnectionHandler") -> bool:
+ """鏈粦瀹氳澶囨椂鎾� 6 浣嶇粦瀹氱爜锛屽苟闃绘娆㈣繋璇�/杩涘満璇嶃�傚凡缁戝畾鍒欒繑鍥� False銆�"""
+ event = getattr(conn, "bind_completed_event", None)
+ if event is not None and not event.is_set():
+ try:
+ await asyncio.wait_for(event.wait(), timeout=8)
+ except asyncio.TimeoutError:
+ pass
+ if not getattr(conn, "need_bind", False):
+ return False
+ if not await wait_for_tts_channels(conn, timeout=8):
+ logger.bind(tag=TAG).warning("TTS閫氶亾鏈氨缁紝浠嶅皾璇曟挱鏀剧粦瀹氱爜")
+ await _play_bind_prompt(conn)
+ return True
def _greeting_text(conn: "ConnectionHandler") -> str:
@@ -575,7 +637,15 @@
step_index,
wav_bytes,
conn=conn,
+ text=asr,
)
+ elif audio_id and asr:
+ try:
+ from core.audio_archive import write_sidecar_for_relative
+
+ write_sidecar_for_relative(conn, audio_id, asr)
+ except Exception:
+ pass
try:
from core.handle.reportHandle import enqueue_asr_report
@@ -829,6 +899,112 @@
return _parse_mp3_list_names(result)
+async def _list_sd_mp3_names_safe(conn: "ConnectionHandler"):
+ try:
+ names = await _list_sd_mp3_names(conn)
+ return names if isinstance(names, list) else []
+ except Exception as exc:
+ logger.bind(tag=TAG).warning(f"鍒椾妇 SD 璇炬枃澶辫触: {exc}")
+ return None
+
+
+def _play_lesson_names(play_params: Optional[Dict[str, Any]]) -> list:
+ from .lesson_audio import unique_lesson_names
+
+ if not isinstance(play_params, dict):
+ return []
+ items = play_params.get("items")
+ if isinstance(items, list) and items:
+ return unique_lesson_names(items)
+ return unique_lesson_names([play_params])
+
+
+def _lesson_sync_timeout_sec(conn: "ConnectionHandler") -> float:
+ runtime = {}
+ cfg = getattr(conn, "config", None)
+ if isinstance(cfg, dict) and isinstance(cfg.get("app_runtime"), dict):
+ runtime = cfg.get("app_runtime") or {}
+ raw = runtime.get("lesson_sync_timeout_sec", 90)
+ try:
+ return max(0.0, float(raw))
+ except (TypeError, ValueError):
+ return 90.0
+
+
+async def _request_lesson_sync(
+ conn: "ConnectionHandler",
+ missing: list,
+ task_id: str,
+ params: Optional[Dict[str, Any]],
+) -> None:
+ from config.manage_api_client import sync_lesson_audio
+ from .lesson_audio import conn_org_id, course_id_for, lesson_audio_base_url, unique_lesson_names
+
+ files = unique_lesson_names(missing)
+ if not files:
+ return
+ params = params or {}
+ lesson_id = str(params.get("lesson_id") or params.get("lesson") or "l1.001").strip() or "l1.001"
+ org_id = conn_org_id(conn, params)
+ payload = {
+ "macAddress": getattr(conn, "device_id", None) or "",
+ "courseId": course_id_for(task_id, lesson_id),
+ "orgId": org_id,
+ "baseUrl": lesson_audio_base_url(getattr(conn, "config", None), org_id),
+ "files": files,
+ }
+ result = await sync_lesson_audio(payload)
+ if result is None:
+ logger.bind(tag=TAG).warning(f"璇炬枃 MQTT 鍚屾澶辫触 files={len(files)}")
+ else:
+ logger.bind(tag=TAG).info(f"璇炬枃 MQTT 鍚屾宸蹭笅鍙� org={org_id} files={len(files)}")
+
+
+async def _wait_lesson_files(conn: "ConnectionHandler", needed: list, timeout_sec: float) -> bool:
+ from .lesson_audio import missing_lesson_names
+
+ if not needed:
+ return False
+ deadline = asyncio.get_running_loop().time() + max(0.0, timeout_sec)
+ while True:
+ present = await _list_sd_mp3_names_safe(conn)
+ if present is not None and not missing_lesson_names(needed, present):
+ return True
+ remaining = deadline - asyncio.get_running_loop().time()
+ if remaining <= 0:
+ return False
+ await asyncio.sleep(min(2.0, remaining))
+
+
+async def _ensure_lesson_mp3s(
+ conn: "ConnectionHandler",
+ names: list,
+ task_id: str,
+ params: Optional[Dict[str, Any]],
+) -> bool:
+ """READY if SD already has the package. Otherwise PRELOAD via MQTT then wait. Device paths have no org."""
+ from .lesson_audio import missing_lesson_names, unique_lesson_names
+
+ needed = unique_lesson_names(names)
+ if not needed:
+ return False
+ present = await _list_sd_mp3_names_safe(conn)
+ if present is None:
+ logger.bind(tag=TAG).warning("鏃犳硶鍒椾妇 SD 璇炬枃锛岃烦杩囧悓姝ヤ互鍏嶈鎶�")
+ return True
+ missing = missing_lesson_names(needed, present)
+ if not missing:
+ return True
+ params = params or {}
+ conn.lesson_syncing = True
+ _speak(conn, "姝e湪鍚屾闊抽锛岃绋嶇瓑銆�")
+ await _request_lesson_sync(conn, missing, task_id, params)
+ timeout = _lesson_sync_timeout_sec(conn)
+ ready = await _wait_lesson_files(conn, needed, timeout)
+ conn.lesson_syncing = False
+ return ready
+
+
def _is_full_lesson_mp3(name: str) -> bool:
base = str(name or "").replace("\\", "/").rsplit("/", 1)[-1].lower()
return base == "full.mp3" or base.endswith("_full.mp3")
@@ -839,9 +1015,7 @@
params: Optional[Dict[str, Any]],
) -> List[Dict[str, Any]]:
"""Keep {name, repeat, speed}. Device expands repeats locally with a listen window each pass."""
- items = parse_playlist(params)
- if not items:
- items = default_shadowing_playlist(_task_for(conn))
+ items = playlist_from_params(_task_for(conn), params, "shadowing", default_shadowing_playlist)
compact: List[Dict[str, Any]] = []
for raw in items:
if not isinstance(raw, dict):
@@ -919,13 +1093,17 @@
items = _compact_shadowing_playlist(conn, params)
clips = expand_playlist_plays(items)
if not clips:
- names = await _list_sd_mp3_names(conn)
- lesson = str(params.get("lesson") or "").strip()
+ lesson = str(params.get("lesson_id") or params.get("lesson") or "").strip()
single = str(params.get("name") or params.get("path") or params.get("url") or "")
if not lesson and single and not single.lower().endswith(".mp3"):
lesson = single
if lesson.lower().endswith(".mp3"):
lesson = ""
+ if not lesson:
+ conn.shadowing_clips = []
+ conn.shadowing_prompt = ""
+ return False
+ names = await _list_sd_mp3_names(conn)
clips = lesson_mp3_clips(
names,
getattr(conn, "active_app_id", "english_learning"),
@@ -948,6 +1126,9 @@
if not await _prepare_shadowing_sd(conn, params):
return False
play = _begin_shadowing_playlist(conn, params)
+ names = _play_lesson_names(play)
+ if not await _ensure_lesson_mp3s(conn, names, getattr(conn, "current_task_id", "shadowing") or "shadowing", params):
+ return False
conn.shadowing_clip_gen = int(getattr(conn, "shadowing_clip_gen", 0) or 0) + 1
notice = await _execute_named_tool_play(conn, play)
if notice and notice.action in (Action.ERROR, Action.NOTFOUND):
@@ -972,20 +1153,6 @@
if play.get("speed") is None and first.get("speed") is not None:
play["speed"] = first.get("speed")
conn.repeater_play_name = play["name"]
- return play
- try:
- names = await _list_sd_mp3_names(conn)
- except Exception as exc:
- logger.bind(tag=TAG).warning(f"鍒椾妇澶嶈璇炬枃澶辫触: {exc}")
- return play
- picked = repeater_lesson_mp3(
- names,
- getattr(conn, "active_app_id", "") or "english_learning",
- getattr(conn, "current_task_id", "") or "repeater",
- )
- if picked:
- play["name"] = picked
- conn.repeater_play_name = picked
return play
@@ -1010,6 +1177,32 @@
await _execute_named_tool_play(conn, play_params)
except Exception as exc:
logger.bind(tag=TAG).warning(f"寤跺悗鎾斁璇炬枃澶辫触: {exc}")
+
+
+def _lesson_start_phrase(task_id: str) -> str:
+ if task_id == "repeater":
+ return REPEATER_START_PHRASE
+ if task_id == "shadowing":
+ return SHADOWING_START_PHRASE
+ return ""
+
+
+def _schedule_talk_start_phrase(task_id: str) -> str:
+ if task_id == "conversation":
+ return CHAT_START_PHRASE
+ if task_id == "live_translate":
+ return TRANSLATION_START_PHRASE
+ if task_id == "bilingual_chat":
+ return BILINGUAL_START_PHRASE
+ return ""
+
+
+def _speak_lesson_start(conn: "ConnectionHandler", task_id: str) -> None:
+ phrase = _lesson_start_phrase(task_id)
+ if not phrase:
+ return
+ conn.sentence_id = uuid.uuid4().hex
+ _speak(conn, phrase)
def _lesson_end_speech(conn: "ConnectionHandler", phrase: str) -> str:
@@ -1065,7 +1258,7 @@
getattr(conn, "repeater_play_name", "") or "",
)
except Exception as exc:
- logger.bind(tag=TAG).warning(f"璁板綍澶嶈澶╂暟澶辫触: {exc}")
+ logger.bind(tag=TAG).warning(f"璁板綍娉涘惉澶╂暟澶辫触: {exc}")
def _apply_playback_speed(
@@ -1102,7 +1295,7 @@
conn.repeater_speed_queue = []
play["speed"] = speeds[0]
play["repeat"] = repeat_n
- # 澶嶈娓呭崟缁撴潫鐢� Server 鏀� audio.completed 鍚庡垏鍥炲璇濄�傝澶囦笉瑕� finish_task 鍐� NVS銆�
+ # 娉涘惉娓呭崟缁撴潫鐢� Server 鏀� audio.completed 鍚庡垏鍥炲璇濄�傝澶囦笉瑕� finish_task 鍐� NVS銆�
play["finish_task"] = False
return play
@@ -1113,9 +1306,7 @@
params: Optional[Dict[str, Any]],
) -> Dict[str, Any]:
"""One shot: send the whole playlist. Repeat is just the same clip again, no mode switch."""
- items = parse_playlist(params)
- if not items:
- items = default_repeater_playlist(task)
+ items = playlist_from_params(task, params, "repeater", default_repeater_playlist)
normalized = []
for raw in items:
if not isinstance(raw, dict):
@@ -1170,7 +1361,7 @@
)
await _execute_named_tool_play(conn, play_params)
except Exception as exc:
- logger.bind(tag=TAG).warning(f"澶嶈涓嬩竴閬嶅け璐�: {exc}")
+ logger.bind(tag=TAG).warning(f"娉涘惉涓嬩竴閬嶅け璐�: {exc}")
conn.repeater_speed_queue = []
return False
return True
@@ -1185,7 +1376,7 @@
play_params = await _fill_repeater_play_name(conn, play_params)
await _execute_named_tool_play(conn, play_params)
except Exception as exc:
- logger.bind(tag=TAG).warning(f"澶嶈涓嬩竴棣栧け璐�: {exc}")
+ logger.bind(tag=TAG).warning(f"娉涘惉涓嬩竴棣栧け璐�: {exc}")
conn.repeater_playlist = []
return False
return True
@@ -1246,13 +1437,45 @@
if not play_args["name"]:
return ActionResponse(
action=Action.RESPONSE,
- response="澶嶈鏈哄凡鎵撳紑锛屼絾 SD 鍗′笂杩樻病鏈夊彲鎾斁鐨勮鏂囥��",
+ response="娉涘惉宸叉墦寮�锛屼絾 SD 鍗′笂杩樻病鏈夊彲鎾斁鐨勮鏂囥��",
)
return ActionResponse(
action=Action.RESPONSE,
- response=(result.response if result else None) or "杩欏彴璁惧娌℃湁鏈湴澶嶈鎾斁鑳藉姏",
+ response=(result.response if result else None) or "杩欏彴璁惧娌℃湁鏈湴娉涘惉鎾斁鑳藉姏",
)
return None
+
+
+def _apply_last_lesson_params(
+ conn: "ConnectionHandler",
+ task_id: str,
+ params: Optional[Dict[str, Any]],
+ source: str,
+) -> Dict[str, Any]:
+ params = dict(params or {})
+ if task_id not in ("repeater", "shadowing"):
+ return params
+ device_id = str(getattr(conn, "device_id", "") or "")
+ has_lesson = str(params.get("lesson_id") or params.get("lesson") or "").strip()
+ if has_lesson:
+ remember_last_lesson_params(device_id, task_id, params)
+ return params
+ saved = load_last_lesson_params(device_id, task_id)
+ if saved:
+ merged = dict(saved)
+ for key, value in params.items():
+ if value not in (None, "", []):
+ merged[key] = value
+ return merged
+ return params
+
+
+def _lesson_missing_speech(task_id: str, params: Optional[Dict[str, Any]], fallback: str) -> str:
+ kind = "娉涘惉" if task_id == "repeater" else "璺熻"
+ lesson = str((params or {}).get("lesson_id") or (params or {}).get("lesson") or "").strip()
+ if not lesson:
+ return f"杩樻病鏈変笅鍙戣繃{kind}璇炬枃锛岃鍏堝湪瀹堕暱绔�変竴璇俱��"
+ return fallback
async def start_app_task(
@@ -1267,7 +1490,15 @@
) -> ActionResponse:
"""Single entry for MQTT / schedule / voice / default chat."""
_ensure_runtime_fields(conn)
+ if await prompt_bind_if_needed(conn):
+ return ActionResponse(action=Action.NONE, response="璁惧鏈粦瀹�")
+ conn.client_abort = False
+ tts = getattr(conn, "tts", None)
+ if tts is not None:
+ tts.tts_stop_request = False
params = params or {}
+ if params.get("announce") is False:
+ speak_switch = False
app, task = get_task(app_id, task_id)
if not app:
return ActionResponse(action=Action.ERROR, response=f"鏈煡鐨� App: {app_id}")
@@ -1278,13 +1509,18 @@
return ActionResponse(action=Action.ERROR, response=f"鏈煡鐨� Task: {app_id}/{task_id}")
resolved_task_id = task_id or app.get("default_task") or "conversation"
+ params = _apply_last_lesson_params(conn, resolved_task_id, params, source)
new_p = source_priority(source)
cur_p = source_priority(getattr(conn, "current_source", "default_chat"))
same_task = (
getattr(conn, "active_app_id", "") == app_id
and getattr(conn, "current_task_id", "") == resolved_task_id
)
- if new_p < cur_p and not same_task:
+ # 鍙d护鍥炲璇濊鑳介��鍑哄闀垮嵆鏃�/璁″垝浠诲姟銆傚叾瀹冧綆浼樺厛绾у垏鎹粛鐒舵嫤浣忋��
+ voice_exit_to_chat = (
+ source == "voice" and app_id == "chat" and resolved_task_id == "conversation"
+ )
+ if new_p < cur_p and not same_task and not voice_exit_to_chat:
logger.bind(tag=TAG).info(
f"鎷掔粷浣庝紭鍏堢骇浠诲姟 {source}({new_p}) < {conn.current_source}({cur_p})"
)
@@ -1292,6 +1528,9 @@
action=Action.NONE,
response="褰撳墠鏈夋洿楂樹紭鍏堢骇鐨勪换鍔★紝鍏堝畬鎴愬悗鍐嶅垏鎹�",
)
+
+ if source in ("voice", "mqtt_immediate", "schedule"):
+ schedule_close_parent_except(conn, source, app_id, resolved_task_id)
prev_app_id = getattr(conn, "active_app_id", "chat")
prev_task_id = getattr(conn, "current_task_id", "conversation")
@@ -1310,29 +1549,56 @@
conn.current_source = source
conn.tool_policy = app.get("tool_policy") or "inherit_agent"
conn.tool_allowlist = list(app.get("tools") or [])
- _apply_prompt(conn, app)
+ _apply_prompt(conn, app, params)
_refresh_tools(conn)
logger.bind(tag=TAG).info(
f"start_app_task app={app_id} task={resolved_task_id} mode={conn.interaction_mode} source={source}"
)
+ device_ready = bool(params.get("sd_ready"))
+ if device_ready:
+ speak_switch = False
+ logger.bind(tag=TAG).info("璇炬枃宸插湪 SD锛屽厛鎾紑濮嬭锛屽啀鐢辫澶囨湰鍦板紑鎾�")
+ _speak_lesson_start(conn, resolved_task_id)
+
if app_id == "chat":
end_session(conn, "user_exit")
+ # 瀹堕暱涓嬪彂鐨勫璇濊璁颁竴鏉℃墽琛岃褰曪紱寮�鏈烘杩庤瘝 / 鍙d护鍥炲璇濅笉瑕佸彟寮� session銆�
+ if source in ("mqtt_immediate", "schedule"):
+ begin_session(conn, app_id, resolved_task_id, source, conn.interaction_mode)
elif switching:
begin_session(conn, app_id, resolved_task_id, source, conn.interaction_mode)
pending_play = None
- if execute_skills:
+ lesson_missing = False
+ if execute_skills and not device_ready:
if app_sd_layout_paths(app_id, app):
await ensure_app_sd_dirs(conn, app_id)
for skill in task.get("skills") or []:
skill_name = (skill or {}).get("name") or ""
if skill_name in ("self.mp3.play", DEVICE_PLAY_TOOL):
play_params = _begin_repeater_playlist(conn, task, params)
- pending_play = {"kind": "repeater", "params": play_params}
+ items = play_params.get("items") if isinstance(play_params, dict) else None
+ if not items and not (isinstance(play_params, dict) and play_params.get("name")):
+ lesson_missing = True
+ else:
+ names = _play_lesson_names(play_params)
+ if not await _ensure_lesson_mp3s(conn, names, resolved_task_id, params):
+ lesson_missing = True
+ else:
+ pending_play = {"kind": "repeater", "params": play_params}
elif skill_name == "self.power_off":
await _execute_named_tool(conn, "self_power_off", {})
+
+ if source == "schedule":
+ talk_phrase = _schedule_talk_start_phrase(resolved_task_id)
+ if talk_phrase:
+ conn.sentence_id = uuid.uuid4().hex
+ _speak(conn, talk_phrase)
+ speak_switch = False
+ if resolved_task_id == "conversation":
+ conn.chat_greeted = True
announce = ""
boot_chat = app_id == "chat" and source == "default_chat"
@@ -1348,17 +1614,48 @@
_speak(conn, _greeting_text(conn))
elif switching:
announce = _switch_announcement(prev_app, prev_task, app, task, app_id)
- if conn.interaction_mode == "SHADOWING":
- started = await _prepare_shadowing_sd(conn, params)
- if not started:
+ if lesson_missing:
+ prefix = announce or "鐜板湪宸茬粡鍒囨崲鑷虫硾鍚ā寮忋��"
+ announce = _lesson_missing_speech(
+ resolved_task_id,
+ params,
+ f"{prefix}杩欒杩樻病鏈夋斁鍒癝D鍗°��",
+ )
+ if getattr(conn, "language_pair_fallback", False) and conn.interaction_mode in (
+ "BILINGUAL",
+ "TRANSLATION",
+ ):
+ announce = f"{announce or ''}杩欏璇█鍏堢敤涓嫳鏉ョ粌銆�"
+ if conn.interaction_mode == "SHADOWING" and not device_ready:
+ play_params = _begin_shadowing_playlist(conn, params)
+ names = _play_lesson_names(play_params)
+ if not names:
+ if await _prepare_shadowing_sd(conn, params):
+ play_params = _begin_shadowing_playlist(conn, params)
+ names = _play_lesson_names(play_params)
+ if not names:
prefix = announce or "鐜板湪宸茬粡鍒囨崲鑷宠窡璇绘ā寮忋��"
- announce = f"{prefix}璺熻璇炬枃杩樻病鏈夋斁鍒癝D鍗°��"
+ announce = _lesson_missing_speech(
+ resolved_task_id,
+ params,
+ f"{prefix}璺熻璇炬枃杩樻病鏈夋斁鍒癝D鍗°��",
+ )
+ elif not await _ensure_lesson_mp3s(conn, names, resolved_task_id, params):
+ prefix = announce or "鐜板湪宸茬粡鍒囨崲鑷宠窡璇绘ā寮忋��"
+ announce = _lesson_missing_speech(
+ resolved_task_id,
+ params,
+ f"{prefix}璺熻璇炬枃杩樻病鏈夋斁鍒癝D鍗°��",
+ )
else:
- announce = announce or "鐜板湪宸茬粡鍒囨崲鑷宠窡璇绘ā寮忥紝璇疯窡鐫�璇炬枃璇汇��"
+ announce = _lesson_start_phrase(resolved_task_id) or announce or "寮�濮嬭窡璇讳换鍔�"
pending_play = {
"kind": "shadowing",
- "params": _begin_shadowing_playlist(conn, params),
+ "params": play_params,
}
+ start_phrase = _lesson_start_phrase(resolved_task_id)
+ if start_phrase and pending_play and not lesson_missing:
+ announce = start_phrase
if speak_switch and announce:
conn.sentence_id = uuid.uuid4().hex
conn.pending_play_after_tts = pending_play
@@ -1375,11 +1672,11 @@
conn.chat_greeted = True
conn.sentence_id = uuid.uuid4().hex
_speak(conn, _greeting_text(conn))
- elif conn.interaction_mode == "SHADOWING" and not getattr(conn, "shadowing_clips", None):
+ elif conn.interaction_mode == "SHADOWING" and not device_ready and not getattr(conn, "shadowing_clips", None):
started = await _start_shadowing_sd(conn, params)
if not started and speak_switch:
conn.sentence_id = uuid.uuid4().hex
- _speak(conn, "璺熻璇炬枃杩樻病鏈夋斁鍒癝D鍗°��")
+ _speak(conn, _lesson_missing_speech(resolved_task_id, params, "璺熻璇炬枃杩樻病鏈夋斁鍒癝D鍗°��"))
elif pending_play:
conn.pending_play_after_tts = pending_play
await maybe_flush_pending_play_after_tts(conn)
@@ -1634,7 +1931,10 @@
)
return True
- if mode == "PLAY_AUDIO" and name == "audio.completed":
+ repeater_done = name == "audio.completed" and (
+ mode == "PLAY_AUDIO" or task_id == "repeater"
+ )
+ if repeater_done:
continued = await _continue_repeater_if_needed(conn)
session = getattr(conn, "task_session", None)
if session:
--
Gitblit v1.9.1