feat(i18n): persist language preference and thread through chat, memoir, TTS

- Add users.language_preference (Alembic 0018, default zh); capture at signup/SMS
  only; expose on auth and profile APIs
- Lite English prompts for chat and memoir; localized stage labels and agent
  names (Life Echo / 岁月知己)
- Tencent TTS: language-aware synthesis, ModelType=1 for 501004, English chunking
- WebSocket pipeline: emit all AGENT_RESPONSE segments when TTS cancels; INFO logs
  for tts_this_turn and TTS decisions; on-demand TTS logging
- Expo: device language on auth, i18n tiers/agent name, [SPLIT] streaming UX fixes
- Tests for migration, prompts, pipeline, router tts_this_turn, reply segments

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-11 16:16:49 +08:00
parent 5ce29aad64
commit ccdc4e4277
64 changed files with 3233 additions and 208 deletions

View File

@@ -51,8 +51,19 @@ def generate_story_title_after_create(
ms,
)
return {"status": "skip_not_found"}
expected_ph = _placeholder_title(chapter_category)
if (st.title or "").strip() and (st.title or "").strip() != expected_ph:
user_obj_pre = db.get(User, user_id)
user_language = (
"en"
if user_obj_pre is not None
and str(getattr(user_obj_pre, "language_preference", "zh") or "zh").lower()
== "en"
else "zh"
)
expected_ph_zh = _placeholder_title(chapter_category, language="zh")
expected_ph_en = _placeholder_title(chapter_category, language="en")
expected_ph = _placeholder_title(chapter_category, language=user_language)
current = (st.title or "").strip()
if current and current not in (expected_ph_zh, expected_ph_en):
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=user_modified duration_ms={:.1f} "
@@ -73,7 +84,7 @@ def generate_story_title_after_create(
)
return {"status": "skip_no_llm"}
user_obj = db.get(User, user_id)
user_obj = user_obj_pre
user_profile = ""
birth_year = None
if user_obj:
@@ -83,6 +94,7 @@ def generate_story_title_after_create(
birth_place=user_obj.birth_place,
grew_up_place=user_obj.grew_up_place,
occupation=user_obj.occupation,
language=user_language,
)
state = get_or_create_state_sync(user_id, db)
@@ -101,8 +113,13 @@ def generate_story_title_after_create(
user_birth_year=birth_year,
llm=llm,
oral_scope=oral_scope or "",
language=user_language,
)
if not new_title.strip() or new_title.strip() == expected_ph:
if not new_title.strip() or new_title.strip() in (
expected_ph_zh,
expected_ph_en,
expected_ph,
):
ms = (time.perf_counter() - t0) * 1000
logger.info(
"event=story_title_task_skip story_id={} reason=placeholder duration_ms={:.1f} "