Files
life-echo/api/app/core/app_config_models.py
Kevin 22d282dc01 feat(api): use Tencent 16k_zh_large ASR and remove local Whisper
Standardize ASR on Tencent's dialect-capable engine across all environments,
drop faster-whisper from dependencies and deployment images, and add an
expo-sqlite iOS vendor sync plus pod install in prebuild to prevent native
build failures after npm install.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-25 10:21:41 +08:00

317 lines
11 KiB
Python

"""Pydantic models for TOML-backed application configuration (non-secret SSOT)."""
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
class DeployConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
alembic_startup_fail_fast: bool = False
access_token_expire_minutes: int = 120
refresh_token_expire_days: int = 30
refresh_token_reuse_grace_seconds: int = Field(default=30, ge=0, le=300)
mock_sms_login_enabled: bool = False
tencent_sms_sdk_app_id: str = ""
tencent_sms_sign_name: str = ""
tencent_sms_template_id: str = ""
tencent_cos_bucket: str = ""
tencent_cos_base_url: str = ""
enable_tts: bool = True
memoir_image_enabled: bool = False
enable_docs: bool = True
wechat_pay_app_id: str = ""
wechat_pay_mch_id: str = ""
wechat_pay_private_key_path: str = "certs/apiclient_key.pem"
wechat_pay_cert_serial_no: str = ""
wechat_pay_notify_url: str = ""
wechat_pay_platform_public_key_path: str = ""
wechat_pay_platform_public_key_id: str = ""
alipay_app_id: str = ""
alipay_notify_url: str = ""
liblib_template_uuid: str = ""
log_level: str = "INFO"
otel_enabled: bool = False
otel_exporter_otlp_endpoint: str = "http://localhost:48317"
api_cors_origins: str = ""
class ChatConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
interview_max_tokens: int = 512
interview_max_segments: int = 2
interview_max_chars_per_segment: int = 380
opening_max_tokens: int = 380
profile_followup_max_tokens: int = 280
history_max_pairs: int = 15
history_max_chars: int = 6000
era_context_enabled: bool = True
stage_detection_enabled: bool = True
stage_detection_max_tokens: int = 128
interview_persona: str = "default"
interview_temperature: float = 0.93
memory_retrieval_enabled: bool = True
memory_top_k: int = 8
memory_evidence_max_chars: int = 4096
memory_safe_evidence_format_enabled: bool = True
reply_planner_llm_enabled: bool = False
reply_planner_max_tokens: int = 256
reply_planner_temperature: float = 0.2
re_greeting_enabled: bool = True
re_greeting_idle_hours: float = 6.0
topic_chips_enabled: bool = True
topic_chips_max: int = 4
input_normalize_enabled: bool = True
input_normalize_mode: str = "rules"
input_normalize_llm_max_tokens: int = 512
input_normalize_llm_max_input_chars: int = 8000
input_normalize_llm_voice_only: bool = True
profile_max_turns: int = 8
profile_extract_max_tokens: int = 512
class MemoirConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
fidelity_check_enabled: bool = True
fidelity_check_max_tokens: int = 512
oral_normalize_enabled: bool = True
oral_normalize_mode: str = "rules"
oral_normalize_llm_max_tokens: int = 512
oral_normalize_llm_max_input_chars: int = 8000
phase1_batch_llm_enabled: bool = True
phase1_batch_llm_max_tokens: int = 4096
phase1_batch_llm_chunk_size: int = 24
pipeline_run_ttl_seconds: int = 172_800
extraction_max_tokens: int = 1024
classification_max_tokens: int = 256
narrative_max_tokens: int = 4096
narrative_merge_max_tokens: int = 8192
title_max_tokens: int = 256
story_route_max_tokens: int = 1024
story_batch_plan_max_tokens: int = 4096
segment_batch_min_chars: int = 50
segment_batch_max_wait_seconds: float = 60.0
narrative_immediate_char_threshold: int = 50
narrative_batch_min_segments: int = 3
narrative_batch_min_chars: int = 80
narrative_batch_max_wait_seconds: float = 120.0
extraction_updates_current_stage: bool = False
fidelity_fail_open_on_parse_error: bool = False
narrative_evidence_overlap_min_chars: int = 14
evidence_scene_anchor_check_enabled: bool = True
title_slots_require_body_or_oral_match: bool = True
title_hay_grounding_strict_phrases_enabled: bool = True
recompose_retry_on_lock_contention: bool = True
phase2_singleflight_immediate: bool = True
route_defer_enabled: bool = True
route_defer_seconds: float = 120.0
route_defer_max_attempts: int = 3
quality_pass_enabled: bool = True
quality_pass_delay_seconds: int = 5
story_route_append_guardrail_oral_chars: int = 1800
min_inline_images_for_chapter_cover: int = 1
image_poll_interval: int = 3
image_max_attempts: int = 20
image_provider: str = "liblib"
image_style_default: str = "watercolor"
image_size_default: str = "1280x720"
image_download_hosts: str = ""
image_prompt_fallback_disabled: bool = False
class MemoryConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
enrichment_enabled: bool = True
enrichment_max_chars: int = 12000
compaction_enabled: bool = True
compaction_debounce_seconds: int = 105
compaction_lock_ttl_seconds: int = 600
compaction_chunk_similarity_threshold: float = 0.92
compaction_min_layers_for_exclude: int = 2
compaction_max_chunks_per_run: int = 200
compaction_max_excludes_per_run: int = 50
compaction_max_neighbors_per_chunk: int = 25
compaction_text_jaccard_min: float = 0.55
compaction_metadata_event_year_window: int = 1
compaction_sweep_recent_hours: int = 24
class StoryConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
image_min_body_chars: int = 400
image_enqueue_dedup_ttl: int = 300
recompose_chapter_delay_seconds: int = 8
chapter_pipeline_lock_ttl_seconds: int = 360
append_max_canonical_chars: int = 12000
append_max_versions: int = 20
route_candidate_body_max_chars: int = 2200
route_candidate_total_max_chars: int = 20_000
route_long_body_head_chars: int = 700
route_long_body_tail_chars: int = 700
route_summary_min_chars: int = 30
route_index_preview_chars: int = 140
title_min_body_chars: int = 60
evidence_top_k_default: int = 10
evidence_top_k_large_batch: int = 5
evidence_large_batch_threshold: int = 3
class EvalConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
judge_base_url: str = "https://open.bigmodel.cn/api/paas/v4"
judge_model: str = "glm-5"
judge_temperature: float = 0.3
judge_deepseek_model: str = "deepseek-v4-flash"
judge_deepseek_thinking_enabled: bool = False
judge_deepseek_context_window_tokens: int = 64_000
judge_context_window_tokens: int = 200_000
judge_completion_reserve_tokens: int = 4096
judge_prompt_budget_safety_tokens: int = 2048
judge_approx_tokens_per_char: float = 1.0
judge_max_transcript_chars: int = 0
judge_max_compare_transcript_chars_each: int = 0
judge_compare_prompt_overhead_chars: int = 10_000
judge_memoir_chapter_concurrency: int = 4
judge_memoir_body_max_chars: int = 36_000
judge_memoir_evidence_max_chars: int = 32_000
judge_memoir_completion_max_tokens: int = 3072
candidate_temperature: float = 0.7
gate_protected_regression_threshold: float = 2.0
execution_enabled: bool = True
internal_enable_docs: bool = False
internal_cors_origins: str = ""
class LlmConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
deepseek_base_url: str = "https://api.deepseek.com"
deepseek_model: str = "deepseek-v4-flash"
deepseek_thinking_enabled: bool = False
temperature: float = 0.7
fast_model: str = ""
embedding_base_url: str = "https://open.bigmodel.cn/api/paas/v4"
embedding_model: str = "embedding-3"
class AsrConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
provider: Literal["tencent"] = "tencent"
engine_type: Literal["16k_zh_large"] = "16k_zh_large"
class TtsConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
provider: str = "tencent"
voice_type: int = 501004
voice_type_en: int = 501004
codec: str = "mp3"
class RedisConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
socket_timeout_seconds: float = 5.0
socket_connect_timeout_seconds: float = 2.0
health_check_interval_seconds: int = 30
task_tracker_ttl_seconds: int = 86400
class CeleryConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
memory_enrichment_queue: str = "memory_idle"
broker_pool_limit: int = 10
broker_connection_retry_on_startup: bool = True
memoir_soft_time_limit: int = 1800
memoir_hard_time_limit: int = 2400
image_soft_time_limit: int = 600
image_hard_time_limit: int = 900
compaction_sweep_soft_time_limit: int = 300
compaction_sweep_hard_time_limit: int = 600
enrichment_soft_time_limit: int = 660
enrichment_hard_time_limit: int = 960
class AlembicConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
run_on_startup: bool = True
max_retries: int = 3
retry_base_seconds: float = 1.0
class AgentLogConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
agent_verbose: bool = False
max_chars: int = 4096
omit_system_message_body: bool = True
json_prompt_prefix_chars: int = 0
json_prompt_prefix_only_if_len_gt: int = 4000
prompt_mode: str = "preview"
prompt_dedup: bool = False
celery_log_level: str = ""
httpx_log_level: str = ""
log_json_file: str = ""
class OtelConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
exporter_insecure: bool = True
service_name: str = "life-echo-api"
metric_export_interval_ms: int = 10_000
def traces_sampler(self, app_environment: str) -> str:
env = (app_environment or "").strip().lower()
if env in ("production", "staging"):
return "parentbased_traceidratio"
return "always_on"
def traces_sampler_arg(self, app_environment: str) -> float | None:
env = (app_environment or "").strip().lower()
if env in ("production", "staging"):
return 0.1
return None
class MiscConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
algorithm: str = "HS256"
redis_session_ttl: int = 86400
tencent_sms_template_param_count: int = 2
tencent_cos_region: str = "ap-shanghai"
liblib_base_url: str = "https://openapi.liblibai.cloud"
alipay_sign_type: str = "RSA2"
alipay_under_development: str = "true"
class AppConfig(BaseModel):
model_config = ConfigDict(extra="forbid")
deploy: DeployConfig = Field(default_factory=DeployConfig)
chat: ChatConfig = Field(default_factory=ChatConfig)
memoir: MemoirConfig = Field(default_factory=MemoirConfig)
memory: MemoryConfig = Field(default_factory=MemoryConfig)
story: StoryConfig = Field(default_factory=StoryConfig)
eval: EvalConfig = Field(default_factory=EvalConfig)
llm: LlmConfig = Field(default_factory=LlmConfig)
asr: AsrConfig = Field(default_factory=AsrConfig)
tts: TtsConfig = Field(default_factory=TtsConfig)
celery: CeleryConfig = Field(default_factory=CeleryConfig)
redis: RedisConfig = Field(default_factory=RedisConfig)
alembic: AlembicConfig = Field(default_factory=AlembicConfig)
agent_log: AgentLogConfig = Field(default_factory=AgentLogConfig)
otel: OtelConfig = Field(default_factory=OtelConfig)
misc: MiscConfig = Field(default_factory=MiscConfig)