Files
life-echo/api/app/features/memoir/oral_normalize.py
Kevin 69a673e6c6 feat(api): 访谈人格/回复长度策略、口述归一、背景语气与输入净稿全链路
Chat 访谈
- 新增 persona 系统(default / warm_listener / curious_guide)与 background_voice 语气层
- 回复长度由 compute_reply_plan 统一决策(brief / standard / expanded),融合信息密度启发式
- 输入净稿(input_normalize):编排层可选 rules/llm 归一用户口语后再喂模型与记忆检索
- 记忆证据注入:按用户话检索 memory evidence 并注入 prompt

Memoir 回忆录
- 口述归一(oral_normalize):segment 原文保留,story 管线取派生净稿作叙事输入
- segment 入队批次门闸:累计字数 + 最长等待秒数,减少零碎提交
- fidelity_check / prompts / narrative_agent 微调
- Alembic 0005:清理跨章节 story 外键

Infra
- Dockerfile 加入 ffmpeg
- pyproject.toml 新增依赖并同步 uv.lock
- .env.example / .env.production 补全新配置项

Tests
- 新增 test_background_voice、test_chat_input_normalize、test_experience_regressions
- 扩展 test_interview_prompts、test_interview_reply_length、test_story_route_oral_invariant

Made-with: Cursor
2026-03-31 23:55:26 +08:00

93 lines
3.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
口述归一:在进入叙事与忠实度校验前,对同一段文本做可控预处理(规则 / 可选 LLM
不改变 segment 落库原文;仅作为 memoir story 生成路径的派生输入。
规则层与聊天侧共用 `apply_conversation_input_rules`(见 conversation.input_normalize
"""
from __future__ import annotations
import json
from typing import Any
from app.core.config import settings
from app.core.langchain_llm import invoke_json_object
from app.core.logging import get_logger
from app.features.conversation.input_normalize import apply_conversation_input_rules
from app.features.memoir.memoir_images.json_payload import extract_json_payload
logger = get_logger(__name__)
def apply_oral_normalization_rules(text: str) -> str:
"""确定性规则;与 `apply_conversation_input_rules` 等价memoir 历史名保留)。"""
return apply_conversation_input_rules(text)
def _llm_normalize_oral(text: str, llm: Any) -> str | None:
"""仅修正明显错字与同音字,不增事实;失败返回 None。"""
if not llm or not (text or "").strip():
return None
max_in = int(settings.memoir_oral_normalize_llm_max_input_chars)
t = (text or "").strip()
if len(t) > max_in:
logger.debug(
"event=oral_normalize_llm_skip reason=input_too_long len={} max={}",
len(t),
max_in,
)
return None
prompt = f"""你是口述转写纠错助手。只修正明显的同音错别字、别字与标点,使句子通顺可读。
禁止增加事实、不补充细节、不摘要、不改写句式风格;不得新增人名、地名、数字、事件。
若原文已通顺或无法确定错误,则照抄输入。
【用户口述】
{t}
**JSON 输出**:只输出一个合法 JSON 对象。
{{"normalized_text": "纠错后的完整文本(与输入等意,仅修错字与标点)"}}
只输出 JSON不要其它文字。"""
try:
raw = invoke_json_object(
llm,
prompt,
max_tokens=int(settings.memoir_oral_normalize_llm_max_tokens),
agent="oral_normalize.llm",
)
data = json.loads(extract_json_payload(raw))
if not isinstance(data, dict):
return None
out = (data.get("normalized_text") or "").strip()
if not out:
return None
return out
except Exception as e:
logger.warning("oral_normalize LLM 失败,回退规则结果: {}", e)
return None
def normalize_oral_for_memoir(text: str, *, llm: Any | None = None) -> str:
"""
供 story pipeline 单一出口:叙事与忠实度使用同一返回值。
- off / 全局关闭:原文
- rules仅规则
- rules + LLM 分支先规则可选LLMLLM 失败则保留规则结果
"""
if not settings.memoir_oral_normalize_enabled:
return text or ""
mode = (settings.memoir_oral_normalize_mode or "rules").strip().lower()
if mode == "off":
return text or ""
base = apply_oral_normalization_rules(text or "")
if mode != "llm":
return base
refined = _llm_normalize_oral(base, llm)
if refined is not None:
return refined
return base