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:
@@ -254,7 +254,11 @@ def _slot_snippets_for_narrative(
|
||||
return slot_snippets
|
||||
|
||||
|
||||
def _placeholder_title(chapter_category: str) -> str:
|
||||
def _placeholder_title(chapter_category: str, language: str = "zh") -> str:
|
||||
if language == "en":
|
||||
from app.agents.stage_constants import chapter_category_display
|
||||
|
||||
return chapter_category_display(chapter_category, language="en")
|
||||
return CHAPTER_CATEGORIES.get(chapter_category, chapter_category)
|
||||
|
||||
|
||||
@@ -301,16 +305,19 @@ def _strip_ungrounded_title_segments(
|
||||
hay: str,
|
||||
*,
|
||||
chapter_category: str,
|
||||
language: str = "zh",
|
||||
) -> str:
|
||||
"""
|
||||
按 · / • 分节丢弃含未落地履历短语的小节;全部丢弃则占位。
|
||||
"""
|
||||
if not settings.memoir_title_hay_grounding_strict_phrases_enabled:
|
||||
return (title or "").strip() or _placeholder_title(chapter_category)
|
||||
return (title or "").strip() or _placeholder_title(
|
||||
chapter_category, language=language
|
||||
)
|
||||
t = (title or "").strip()
|
||||
h = (hay or "").strip()
|
||||
if not t:
|
||||
return _placeholder_title(chapter_category)
|
||||
return _placeholder_title(chapter_category, language=language)
|
||||
segments = [s.strip() for s in re.split(r"\s*[·•]\s*", t) if s.strip()]
|
||||
if not segments:
|
||||
segments = [t]
|
||||
@@ -329,7 +336,7 @@ def _strip_ungrounded_title_segments(
|
||||
continue
|
||||
kept.append(seg)
|
||||
if not kept:
|
||||
return _placeholder_title(chapter_category)
|
||||
return _placeholder_title(chapter_category, language=language)
|
||||
if len(kept) == 1:
|
||||
return kept[0]
|
||||
return " · ".join(kept)
|
||||
@@ -346,11 +353,12 @@ def _maybe_generate_title(
|
||||
llm: Any,
|
||||
oral_scope: str = "",
|
||||
narrow_profile_for_title: bool = True,
|
||||
language: str = "zh",
|
||||
) -> str:
|
||||
"""Generate a title only when body is long enough; otherwise return placeholder."""
|
||||
body_len = len((md or "").strip())
|
||||
if body_len < settings.story_title_min_body_chars:
|
||||
return _placeholder_title(chapter_category)
|
||||
return _placeholder_title(chapter_category, language=language)
|
||||
content_excerpt = (md or "").strip()[:300]
|
||||
merged_slots = _title_slots_filtered_for_generation(
|
||||
slot_snippets, md=md, oral_scope=oral_scope
|
||||
@@ -366,10 +374,14 @@ def _maybe_generate_title(
|
||||
user_profile=profile_for_title,
|
||||
birth_year=user_birth_year,
|
||||
llm=llm,
|
||||
language=language,
|
||||
)
|
||||
hay = _title_hay_for_grounding(merged_slots, md, oral_scope)
|
||||
return _strip_ungrounded_title_segments(
|
||||
raw_title, hay, chapter_category=chapter_category
|
||||
raw_title,
|
||||
hay,
|
||||
chapter_category=chapter_category,
|
||||
language=language,
|
||||
)
|
||||
|
||||
|
||||
@@ -733,6 +745,7 @@ def _execute_narrative_unit(
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
fidelity_llm: Any | None = None,
|
||||
language: str = "zh",
|
||||
) -> tuple[str | None, bool]:
|
||||
"""
|
||||
Unified narrative unit executor: generate narrative, apply fidelity/safety,
|
||||
@@ -740,7 +753,9 @@ def _execute_narrative_unit(
|
||||
"""
|
||||
t0 = time.perf_counter()
|
||||
oral_norm = (oral_text or "").strip()
|
||||
new_content_input = format_narrative_user_content(oral_text, evidence_text)
|
||||
new_content_input = format_narrative_user_content(
|
||||
oral_text, evidence_text, language=language
|
||||
)
|
||||
|
||||
raw_gen = narrative_agent.generate_narrative(
|
||||
stage=chapter_category,
|
||||
@@ -753,6 +768,7 @@ def _execute_narrative_unit(
|
||||
background_voice=background_voice,
|
||||
occupation=occupation,
|
||||
fallback_plain_oral=oral_norm,
|
||||
language=language,
|
||||
)
|
||||
json_invalid = False
|
||||
s0 = (raw_gen or "").strip()
|
||||
@@ -816,7 +832,7 @@ def _execute_narrative_unit(
|
||||
sid_log = target_story_id
|
||||
is_append = True
|
||||
else:
|
||||
story_title = _placeholder_title(chapter_category)
|
||||
story_title = _placeholder_title(chapter_category, language=language)
|
||||
st = create_story_with_version_sync(
|
||||
session,
|
||||
user_id=user_id,
|
||||
@@ -905,6 +921,7 @@ def _run_batch_plan_writes(
|
||||
occupation: str = "",
|
||||
memoir_correlation_id: str | None = None,
|
||||
fidelity_llm: Any | None = None,
|
||||
language: str = "zh",
|
||||
) -> set[str]:
|
||||
dispatch_ids: set[str] = set()
|
||||
for unit in plan.units:
|
||||
@@ -951,6 +968,7 @@ def _run_batch_plan_writes(
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=fidelity_llm,
|
||||
language=language,
|
||||
)
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
@@ -972,6 +990,7 @@ def run_story_pipeline_for_category_batch(
|
||||
memoir_correlation_id: str | None = None,
|
||||
llm_fast: Any | None = None,
|
||||
memory_evidence: dict | None = None,
|
||||
language: str = "zh",
|
||||
) -> StoryPipelineResult:
|
||||
"""运行某 chapter_category 的 Phase2 写入管线。
|
||||
|
||||
@@ -1064,7 +1083,9 @@ def run_story_pipeline_for_category_batch(
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
title = chapter.title if chapter else _placeholder_title(chapter_category)
|
||||
title = chapter.title if chapter else _placeholder_title(
|
||||
chapter_category, language=language
|
||||
)
|
||||
|
||||
# 仅同 chapter_category(story.stage)的 Story 可作为 append 候选,避免跨章节链接导致多章内容相同
|
||||
all_stories = list_active_stories_for_user_sync(session, user_id)
|
||||
@@ -1167,6 +1188,7 @@ def run_story_pipeline_for_category_batch(
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=llm_fidelity,
|
||||
language=language,
|
||||
)
|
||||
else:
|
||||
route = single_route
|
||||
@@ -1215,6 +1237,7 @@ def run_story_pipeline_for_category_batch(
|
||||
occupation=occupation,
|
||||
memoir_correlation_id=memoir_correlation_id,
|
||||
fidelity_llm=llm_fidelity,
|
||||
language=language,
|
||||
)
|
||||
if sid:
|
||||
dispatch_ids.add(sid)
|
||||
|
||||
Reference in New Issue
Block a user