| | |
| | | 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, |
| | | ) |
| | |
| | | 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 |
| | |
| | | "用户说退出中英对话、回到聊天时调用 switch_app,app_id=chat。" |
| | | ) |
| | | |
| | | BILINGUAL_PAIR_PROMPTS = { |
| | | "zh-en": BILINGUAL_PROMPT, |
| | | "zh-fr": ( |
| | | "你正在进行中法对照对话练习,不是翻译机,不要把用户的话原样译回去交差。" |
| | | "用户说中文时你用法语完整回答;用户说法语时你用中文完整回答。" |
| | | "不要两种语言同时输出。用户说退出对话、回到聊天时调用 switch_app,app_id=chat。" |
| | | ), |
| | | "zh-ja": ( |
| | | "你正在进行中日对照对话练习,不是翻译机,不要把用户的话原样译回去交差。" |
| | | "用户说中文时你用日语完整回答;用户说日语时你用中文完整回答。" |
| | | "不要两种语言同时输出。用户说退出对话、回到聊天时调用 switch_app,app_id=chat。" |
| | | ), |
| | | } |
| | | |
| | | TRANSLATOR_PAIR_PROMPTS = { |
| | | "zh-en": TRANSLATOR_PROMPT, |
| | | "zh-fr": "你是一台翻译机。只输出译文。中文译成法语,法语译成中文;其它语言默认译成中文。", |
| | | "zh-ja": "你是一台翻译机。只输出译文。中文译成日语,日语译成中文;其它语言默认译成中文。", |
| | | } |
| | | |
| | | CHAT_PROACTIVE_HINT = ( |
| | | "你是用户的语音伙伴。进入对话后可以先主动短句邀请聊天,例如「我们来聊天吧」。" |
| | | "用户开口后按正常对话回答,不要反复催促。" |
| | |
| | | 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: |
| | |
| | | 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,app_id=chat。" |
| | | pair_prompt |
| | | + "用户说话后应调用 translate;用户说退出翻译、回到聊天时调用 switch_app,app_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 ""): |
| | |
| | | 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: |
| | |
| | | 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 |
| | | |
| | |
| | | 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, "正在同步音频,请稍等。") |
| | | 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") |
| | |
| | | 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): |
| | |
| | | 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"), |
| | |
| | | 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): |
| | |
| | | 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 |
| | | |
| | | |
| | |
| | | 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: |
| | |
| | | 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( |
| | |
| | | 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 |
| | | |
| | |
| | | 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): |
| | |
| | | ) |
| | | 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 |
| | |
| | | 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 |
| | |
| | | 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( |
| | |
| | | ) -> 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}") |
| | |
| | | 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: |
| | | # 口令回对话要能退出家长即时/计划任务。其它低优先级切换仍然拦住。 |
| | | 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})" |
| | | ) |
| | |
| | | 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") |
| | |
| | | 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") |
| | | # 家长下发的对话要记一条执行记录;开机欢迎词 / 口令回对话不要另开 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" |
| | |
| | | _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}这课还没有放到SD卡。", |
| | | ) |
| | | 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}跟读课文还没有放到SD卡。" |
| | | announce = _lesson_missing_speech( |
| | | resolved_task_id, |
| | | params, |
| | | f"{prefix}跟读课文还没有放到SD卡。", |
| | | ) |
| | | 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}跟读课文还没有放到SD卡。", |
| | | ) |
| | | 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 |
| | |
| | | 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, "跟读课文还没有放到SD卡。") |
| | | _speak(conn, _lesson_missing_speech(resolved_task_id, params, "跟读课文还没有放到SD卡。")) |
| | | elif pending_play: |
| | | conn.pending_play_after_tts = pending_play |
| | | await maybe_flush_pending_play_after_tts(conn) |
| | |
| | | ) |
| | | 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: |