test/app/core/config.py

537 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# -*- coding: utf-8 -*-
"""
统一配置管理
ASR语音识别配置选项
"""
import os
from typing import Optional
from pathlib import Path
class Settings:
"""统一应用配置类"""
# 应用信息
APP_NAME: str = "Qwen3-ASR Server"
APP_VERSION: str = "1.0.1"
APP_DESCRIPTION: str = "Qwen3-ASR speech recognition API service"
# 服务器配置
HOST: str = "0.0.0.0"
PORT: int = 8000
DEBUG: bool = False
# 鉴权配置
API_KEY: Optional[str] = None # 从环境变量API_KEY读取,如果为None则鉴权可选
# 设备配置
ACCELERATOR: str = "auto" # auto, cpu, nvidia, metax, iluvatar, mthreads
DEVICE: str = "auto" # auto, cpu, cuda:0
ASR_DEPLOY_TOPOLOGY: str = "isolated" # isolated, sharded, auto
# 路径配置
BASE_DIR: Path = Path(__file__).parent.parent.parent
DATA_DIR: str = str(BASE_DIR / "data")
TEMP_DIR: str = str(BASE_DIR / "data" / "temp")
# 项目总模型目录。实际模型目录直接扁平化到:
# /models/{Qwen,iic,damo,...}
MODELS_DIR: str = str(BASE_DIR / "models")
# ModelScope 会在 MODELSCOPE_CACHE 下创建 models/{publisher}/{model_name}。
# 因此 cache 根目录应指向 models 的上一级,实际运行模型根目录仍由 MODELSCOPE_PATH 指定。
MODELSCOPE_CACHE: str = str(BASE_DIR)
MODELSCOPE_PATH: str = str(BASE_DIR / "models")
# 日志配置
LOG_LEVEL: str = "INFO"
LOG_FILE: Optional[str] = str(BASE_DIR / "data" / "logs" / "qwen3-asr.log")
LOG_MAX_BYTES: int = 20 * 1024 * 1024 # 20MB
LOG_BACKUP_COUNT: int = 50 # 保留50个备份文件
# ASR模型配置
WS_MAX_BUFFER_SIZE: int = 10 * 16000 # WebSocket音频缓冲区最大大小(10秒@16kHz)
FUNASR_AUTOMODEL_KWARGS = {
"trust_remote_code": False,
"disable_update": True,
"disable_pbar": True,
"disable_log": True, # 禁用FunASR的tables输出
"local_files_only": True, # 强制使用本地模型,禁止联网下载
}
ASR_MODELS_CONFIG: str = str(BASE_DIR / "app/services/asr/models.json")
ASR_ENABLE_REALTIME_PUNC: bool = True # 是否启用实时标点模型(用于中间结果展示)
VAD_MODEL: str = "damo/speech_fsmn_vad_zh-cn-16k-common-pytorch"
PUNC_MODEL: str = "iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch"
PUNC_REALTIME_MODEL: str = (
"iic/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727"
)
# 流式ASR远场过滤配置
ASR_ENABLE_NEARFIELD_FILTER: bool = True # 是否启用远场声音过滤
ASR_NEARFIELD_RMS_THRESHOLD: float = 0.01 # RMS能量阈值(宽松模式,适合大多数场景)
# 音频处理配置
MAX_AUDIO_SIZE: int = 2048 * 1024 * 1024 # 2GB
# 批处理推理配置(GPU 真并行)
ASR_BATCH_SIZE: int = 4 # ASR 批处理大小(同时推理的片段数),建议 2-8
ASR_ENABLE_WORD_TIMESTAMPS: bool = False # 全局字词级时间戳开关;关闭时不预热 forced aligner,接口参数也默认隐藏
# 音频分段配置
MAX_SEGMENT_SEC: float = 60.0 # Max offline ASR segment duration in seconds.
# Runtime 并发配置(按 backend 独立控制)
QWEN_VLLM_SHARED_CONCURRENCY: int = 8
QWEN_VLLM_ENFORCE_EAGER: bool = True
QWEN_RUST_CPU_WORKERS: int = 4
QWEN_RUST_ASR_CONCURRENCY: int = 0
QWEN_RUST_ALIGN_CONCURRENCY: int = 0
FUNASR_WORKERS: int = 1
# 声纹数据库配置(与 Model-Test-New 使用同一套 PostgreSQL/pgvector 表结构)
SPEAKER_DB_ENABLED: bool = True
DB_USER: str = "postgres"
DB_PASSWORD: str = "postgres"
DB_NAME: str = "asr_db"
DB_HOST: str = "127.0.0.1"
DB_PORT: int = 5432
DB_POOL_MAX_SIZE: int = 5
SV_MODEL: str = "iic/speech_campplus_sv_zh-cn_16k-common"
SV_MODEL_REVISION: str = "v2.0.2"
REALTIME_SV_MODEL: str = "iic/speech_eres2netv2_sv_zh-cn_16k-common"
REALTIME_SV_MODEL_REVISION: str = ""
SV_THRESHOLD: float = 0.6
REALTIME_MAX_SEGMENT_SEC: float = 12.0
REALTIME_MAX_SEGMENT_TAIL_SEC: float = 1.6
REALTIME_FORCE_STABLE_SEGMENT_SEC: float = 8.0
REALTIME_FORCE_STABLE_MIN_CHARS: int = 24
REALTIME_MIN_PARTIAL_SEC: float = 0.45
REALTIME_PARTIAL_EMIT_INTERVAL_SEC: float = 0.25
REALTIME_PARTIAL_WINDOW_SEC: float = 8.0
REALTIME_STREAM_CHUNK_SEC: float = 1.2
REALTIME_STREAM_MAX_PENDING_CHUNKS: int = 3
REALTIME_STREAM_WINDOW_SEC: float = 8.0
REALTIME_STREAM_STABLE_TAIL_CHARS: int = 8
REALTIME_STREAM_STABLE_MIN_GROW_CHARS: int = 2
REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS: int = 2
REALTIME_PARTIAL_HOLDBACK_CHARS: int = 6
REALTIME_LONGFORM_MIN_SEC: float = 8.0
REALTIME_LONGFORM_CHUNK_SEC: float = 6.0
REALTIME_LONGFORM_OVERLAP_SEC: float = 1.2
REALTIME_VAD_CHECK_INTERVAL_SEC: float = 0.8
REALTIME_VAD_FINALIZE_SILENCE_SEC: float = 0.6
REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC: float = 8.0
REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC: float = 12.0
REALTIME_ENABLE_DIARIZATION: bool = True
REALTIME_ENABLE_SEGMENT_REFINE: bool = False
REALTIME_DIARIZATION_MIN_SEC: float = 3.0
REALTIME_DIARIZATION_LOOKBACK_SEC: float = 3.0
REALTIME_DIARIZATION_WINDOW_SEC: float = 15.0
REALTIME_SPEAKER_MIN_SEC: float = 1.2
REALTIME_SPEAKER_CLUSTER_THRESHOLD: float = 0.75
REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD: float = 0.58
REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD: float = 0.50
REALTIME_SPEAKER_CONFIRM_THRESHOLD: float = 0.62
REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE: float = 0.72
REALTIME_SPEAKER_MAX_SLOTS: int = 8
REALTIME_SESSION_RESUME_TTL_SEC: int = 120 # WebSocket 断线后保留会话上下文的秒数;TTL 内同 session_id 可恢复
API_PREFIX: str = "/api/v1"
TASK_STATE_DIR: str = str(BASE_DIR / "data" / "tasks")
TASK_RETENTION_HOURS: int = 24
def __init__(self):
"""从环境变量读取配置"""
self._load_from_env()
self._ensure_directories()
def _load_from_env(self):
"""从环境变量加载配置"""
# 服务器配置
self.HOST = os.getenv("HOST", self.HOST)
self.PORT = int(os.getenv("PORT", str(self.PORT)))
self.DEBUG = os.getenv("DEBUG", "false").lower() == "true"
self.DATA_DIR = os.getenv("DATA_DIR", self.DATA_DIR)
self.TEMP_DIR = os.getenv("TEMP_DIR", self.TEMP_DIR)
# 日志配置
self.LOG_LEVEL = os.getenv("LOG_LEVEL", self.LOG_LEVEL)
self.LOG_FILE = os.getenv("LOG_FILE", self.LOG_FILE)
self.LOG_MAX_BYTES = int(os.getenv("LOG_MAX_BYTES", str(self.LOG_MAX_BYTES)))
self.LOG_BACKUP_COUNT = int(
os.getenv("LOG_BACKUP_COUNT", str(self.LOG_BACKUP_COUNT))
)
# 鉴权配置:空值/空白统一视为未配置
self.API_KEY = (os.getenv("API_KEY") or "").strip() or None
# 设备配置
self.ACCELERATOR = os.getenv("ACCELERATOR", self.ACCELERATOR)
self.DEVICE = os.getenv("DEVICE", self.DEVICE)
self.ASR_DEPLOY_TOPOLOGY = os.getenv(
"ASR_DEPLOY_TOPOLOGY",
self.ASR_DEPLOY_TOPOLOGY,
).strip().lower()
# 模型缓存路径
self.MODELS_DIR = os.getenv("MODELS_DIR", self.MODELS_DIR)
self.MODELSCOPE_CACHE = os.getenv("MODELSCOPE_CACHE", self.MODELSCOPE_CACHE)
self.MODELSCOPE_PATH = os.getenv("MODELSCOPE_PATH", self.MODELSCOPE_PATH)
# 给第三方库补齐默认缓存环境变量,允许用户自行覆盖
os.environ.setdefault("MODELS_DIR", self.MODELS_DIR)
os.environ.setdefault("MODELSCOPE_CACHE", self.MODELSCOPE_CACHE)
os.environ.setdefault("MODELSCOPE_PATH", self.MODELSCOPE_PATH)
# ASR模型配置
self.ASR_ENABLE_REALTIME_PUNC = (
os.getenv("ASR_ENABLE_REALTIME_PUNC", "true").lower() == "true"
)
# WebSocket缓冲区配置
self.WS_MAX_BUFFER_SIZE = int(
os.getenv("WS_MAX_BUFFER_SIZE", str(self.WS_MAX_BUFFER_SIZE))
)
# 远场过滤配置
self.ASR_ENABLE_NEARFIELD_FILTER = (
os.getenv("ASR_ENABLE_NEARFIELD_FILTER", "true").lower() == "true"
)
self.ASR_NEARFIELD_RMS_THRESHOLD = float(
os.getenv(
"ASR_NEARFIELD_RMS_THRESHOLD", str(self.ASR_NEARFIELD_RMS_THRESHOLD)
)
)
# 音频处理配置
# 支持简化格式:纯数字表示MB,或带单位(如 2048MB, 2GB)
max_audio_size_str = os.getenv("MAX_AUDIO_SIZE")
if max_audio_size_str:
self.MAX_AUDIO_SIZE = self._parse_size(max_audio_size_str)
self.ASR_BATCH_SIZE = int(
os.getenv("ASR_BATCH_SIZE", str(self.ASR_BATCH_SIZE))
)
self.ASR_ENABLE_WORD_TIMESTAMPS = (
os.getenv(
"ASR_ENABLE_WORD_TIMESTAMPS",
str(self.ASR_ENABLE_WORD_TIMESTAMPS),
).lower()
== "true"
)
self.MAX_SEGMENT_SEC = float(
os.getenv("MAX_SEGMENT_SEC", str(self.MAX_SEGMENT_SEC))
)
self.QWEN_VLLM_SHARED_CONCURRENCY = int(
os.getenv(
"QWEN_VLLM_SHARED_CONCURRENCY",
str(self.QWEN_VLLM_SHARED_CONCURRENCY),
)
)
self.QWEN_VLLM_ENFORCE_EAGER = (
os.getenv(
"QWEN_VLLM_ENFORCE_EAGER",
str(self.QWEN_VLLM_ENFORCE_EAGER),
).lower()
== "true"
)
self.QWEN_RUST_CPU_WORKERS = int(
os.getenv("QWEN_RUST_CPU_WORKERS", str(self.QWEN_RUST_CPU_WORKERS))
)
self.QWEN_RUST_ASR_CONCURRENCY = int(
os.getenv("QWEN_RUST_ASR_CONCURRENCY", str(self.QWEN_RUST_ASR_CONCURRENCY))
)
self.QWEN_RUST_ALIGN_CONCURRENCY = int(
os.getenv("QWEN_RUST_ALIGN_CONCURRENCY", str(self.QWEN_RUST_ALIGN_CONCURRENCY))
)
self.FUNASR_WORKERS = int(
os.getenv("FUNASR_WORKERS", str(self.FUNASR_WORKERS))
)
self.SPEAKER_DB_ENABLED = (
os.getenv("SPEAKER_DB_ENABLED", str(self.SPEAKER_DB_ENABLED)).lower()
== "true"
)
self.DB_USER = os.getenv("DB_USER", self.DB_USER)
self.DB_PASSWORD = os.getenv("DB_PASSWORD", self.DB_PASSWORD)
self.DB_NAME = os.getenv("DB_NAME", self.DB_NAME)
self.DB_HOST = os.getenv("DB_HOST", self.DB_HOST)
self.DB_PORT = int(os.getenv("DB_PORT", str(self.DB_PORT)))
self.DB_POOL_MAX_SIZE = int(
os.getenv("DB_POOL_MAX_SIZE", str(self.DB_POOL_MAX_SIZE))
)
self.SV_MODEL = os.getenv("SV_MODEL", self.SV_MODEL)
self.SV_MODEL_REVISION = os.getenv("SV_MODEL_REVISION", self.SV_MODEL_REVISION)
self.REALTIME_SV_MODEL = os.getenv("REALTIME_SV_MODEL", self.REALTIME_SV_MODEL)
self.REALTIME_SV_MODEL_REVISION = os.getenv(
"REALTIME_SV_MODEL_REVISION",
self.REALTIME_SV_MODEL_REVISION,
)
self.SV_THRESHOLD = float(os.getenv("SV_THRESHOLD", str(self.SV_THRESHOLD)))
self.REALTIME_MAX_SEGMENT_SEC = float(
os.getenv(
"REALTIME_MAX_SEGMENT_SEC",
str(self.REALTIME_MAX_SEGMENT_SEC),
)
)
self.REALTIME_MAX_SEGMENT_TAIL_SEC = float(
os.getenv(
"REALTIME_MAX_SEGMENT_TAIL_SEC",
str(self.REALTIME_MAX_SEGMENT_TAIL_SEC),
)
)
self.REALTIME_FORCE_STABLE_SEGMENT_SEC = float(
os.getenv(
"REALTIME_FORCE_STABLE_SEGMENT_SEC",
str(self.REALTIME_FORCE_STABLE_SEGMENT_SEC),
)
)
self.REALTIME_FORCE_STABLE_MIN_CHARS = int(
os.getenv(
"REALTIME_FORCE_STABLE_MIN_CHARS",
str(self.REALTIME_FORCE_STABLE_MIN_CHARS),
)
)
self.REALTIME_MIN_PARTIAL_SEC = float(
os.getenv(
"REALTIME_MIN_PARTIAL_SEC",
str(self.REALTIME_MIN_PARTIAL_SEC),
)
)
self.REALTIME_PARTIAL_EMIT_INTERVAL_SEC = float(
os.getenv(
"REALTIME_PARTIAL_EMIT_INTERVAL_SEC",
str(self.REALTIME_PARTIAL_EMIT_INTERVAL_SEC),
)
)
self.REALTIME_PARTIAL_WINDOW_SEC = float(
os.getenv(
"REALTIME_PARTIAL_WINDOW_SEC",
str(self.REALTIME_PARTIAL_WINDOW_SEC),
)
)
self.REALTIME_STREAM_CHUNK_SEC = float(
os.getenv(
"REALTIME_STREAM_CHUNK_SEC",
str(self.REALTIME_STREAM_CHUNK_SEC),
)
)
self.REALTIME_STREAM_MAX_PENDING_CHUNKS = int(
os.getenv(
"REALTIME_STREAM_MAX_PENDING_CHUNKS",
str(self.REALTIME_STREAM_MAX_PENDING_CHUNKS),
)
)
self.REALTIME_STREAM_WINDOW_SEC = float(
os.getenv(
"REALTIME_STREAM_WINDOW_SEC",
str(self.REALTIME_STREAM_WINDOW_SEC),
)
)
self.REALTIME_STREAM_STABLE_TAIL_CHARS = int(
os.getenv(
"REALTIME_STREAM_STABLE_TAIL_CHARS",
str(self.REALTIME_STREAM_STABLE_TAIL_CHARS),
)
)
self.REALTIME_STREAM_STABLE_MIN_GROW_CHARS = int(
os.getenv(
"REALTIME_STREAM_STABLE_MIN_GROW_CHARS",
str(self.REALTIME_STREAM_STABLE_MIN_GROW_CHARS),
)
)
self.REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS = int(
os.getenv(
"REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS",
str(self.REALTIME_STREAM_DIVERGENCE_TOLERANCE_CHARS),
)
)
self.REALTIME_PARTIAL_HOLDBACK_CHARS = int(
os.getenv(
"REALTIME_PARTIAL_HOLDBACK_CHARS",
str(self.REALTIME_PARTIAL_HOLDBACK_CHARS),
)
)
self.REALTIME_LONGFORM_MIN_SEC = float(
os.getenv(
"REALTIME_LONGFORM_MIN_SEC",
str(self.REALTIME_LONGFORM_MIN_SEC),
)
)
self.REALTIME_LONGFORM_CHUNK_SEC = float(
os.getenv(
"REALTIME_LONGFORM_CHUNK_SEC",
str(self.REALTIME_LONGFORM_CHUNK_SEC),
)
)
self.REALTIME_LONGFORM_OVERLAP_SEC = float(
os.getenv(
"REALTIME_LONGFORM_OVERLAP_SEC",
str(self.REALTIME_LONGFORM_OVERLAP_SEC),
)
)
self.REALTIME_VAD_CHECK_INTERVAL_SEC = float(
os.getenv(
"REALTIME_VAD_CHECK_INTERVAL_SEC",
str(self.REALTIME_VAD_CHECK_INTERVAL_SEC),
)
)
self.REALTIME_VAD_FINALIZE_SILENCE_SEC = float(
os.getenv(
"REALTIME_VAD_FINALIZE_SILENCE_SEC",
str(self.REALTIME_VAD_FINALIZE_SILENCE_SEC),
)
)
self.REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC = float(
os.getenv(
"REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC",
str(self.REALTIME_FINAL_SEGMENT_SOFT_LIMIT_SEC),
)
)
self.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC = float(
os.getenv(
"REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC",
str(self.REALTIME_FINAL_SEGMENT_HARD_LIMIT_SEC),
)
)
self.REALTIME_ENABLE_DIARIZATION = (
os.getenv(
"REALTIME_ENABLE_DIARIZATION",
str(self.REALTIME_ENABLE_DIARIZATION),
).lower()
== "true"
)
self.REALTIME_ENABLE_SEGMENT_REFINE = (
os.getenv(
"REALTIME_ENABLE_SEGMENT_REFINE",
str(self.REALTIME_ENABLE_SEGMENT_REFINE),
).lower()
== "true"
)
self.REALTIME_DIARIZATION_MIN_SEC = float(
os.getenv(
"REALTIME_DIARIZATION_MIN_SEC",
str(self.REALTIME_DIARIZATION_MIN_SEC),
)
)
self.REALTIME_DIARIZATION_LOOKBACK_SEC = float(
os.getenv(
"REALTIME_DIARIZATION_LOOKBACK_SEC",
str(self.REALTIME_DIARIZATION_LOOKBACK_SEC),
)
)
self.REALTIME_DIARIZATION_WINDOW_SEC = float(
os.getenv(
"REALTIME_DIARIZATION_WINDOW_SEC",
str(self.REALTIME_DIARIZATION_WINDOW_SEC),
)
)
self.REALTIME_SPEAKER_MIN_SEC = float(
os.getenv(
"REALTIME_SPEAKER_MIN_SEC",
str(self.REALTIME_SPEAKER_MIN_SEC),
)
)
self.REALTIME_SPEAKER_CLUSTER_THRESHOLD = float(
os.getenv(
"REALTIME_SPEAKER_CLUSTER_THRESHOLD",
str(self.REALTIME_SPEAKER_CLUSTER_THRESHOLD),
)
)
self.REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD = float(
os.getenv(
"REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD",
str(self.REALTIME_UNKNOWN_SPK_CLUSTER_THRESHOLD),
)
)
self.REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD = float(
os.getenv(
"REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD",
str(self.REALTIME_RECENT_UNKNOWN_SPK_THRESHOLD),
)
)
self.REALTIME_SPEAKER_CONFIRM_THRESHOLD = float(
os.getenv(
"REALTIME_SPEAKER_CONFIRM_THRESHOLD",
str(self.REALTIME_SPEAKER_CONFIRM_THRESHOLD),
)
)
self.REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE = float(
os.getenv(
"REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE",
str(self.REALTIME_REGISTRY_MIN_CLUSTER_CONFIDENCE),
)
)
self.REALTIME_SPEAKER_MAX_SLOTS = int(
os.getenv(
"REALTIME_SPEAKER_MAX_SLOTS",
str(self.REALTIME_SPEAKER_MAX_SLOTS),
)
)
self.REALTIME_SESSION_RESUME_TTL_SEC = int(
os.getenv(
"REALTIME_SESSION_RESUME_TTL_SEC",
str(self.REALTIME_SESSION_RESUME_TTL_SEC),
)
)
self.API_PREFIX = os.getenv("API_PREFIX", self.API_PREFIX)
self.TASK_STATE_DIR = os.getenv("TASK_STATE_DIR", self.TASK_STATE_DIR)
self.TASK_RETENTION_HOURS = int(
os.getenv("TASK_RETENTION_HOURS", str(self.TASK_RETENTION_HOURS))
)
def _parse_size(self, size_str: str) -> int:
"""解析带单位的大小字符串
支持格式:
- 纯数字:视为 MB(如 2048 = 2048MB = 2147483648 bytes)
- 带单位:如 2GB, 2048MB, 1.5GB
"""
size_str = size_str.strip().upper()
# 如果纯数字,视为 MB
if size_str.isdigit():
return int(size_str) * 1024 * 1024
# 带单位的处理
if size_str.endswith('GB'):
return int(float(size_str[:-2]) * 1024 * 1024 * 1024)
elif size_str.endswith('MB'):
return int(float(size_str[:-2]) * 1024 * 1024)
elif size_str.endswith('KB'):
return int(float(size_str[:-2]) * 1024)
else:
# 默认视为字节
return int(size_str)
def _ensure_directories(self):
"""确保必需的目录存在"""
os.makedirs(self.TEMP_DIR, exist_ok=True)
if self.LOG_FILE:
os.makedirs(os.path.dirname(self.LOG_FILE), exist_ok=True)
os.makedirs(self.MODELS_DIR, exist_ok=True)
os.makedirs(self.MODELSCOPE_CACHE, exist_ok=True)
os.makedirs(self.MODELSCOPE_PATH, exist_ok=True)
os.makedirs(self.DATA_DIR, exist_ok=True)
os.makedirs(self.TASK_STATE_DIR, exist_ok=True)
@property
def models_config_path(self) -> str:
"""获取模型配置文件的完整路径"""
return str(self.BASE_DIR / self.ASR_MODELS_CONFIG)
@property
def docs_url(self) -> Optional[str]:
"""获取文档URL"""
return "/docs"
@property
def redoc_url(self) -> Optional[str]:
"""获取ReDoc URL"""
return "/redoc"
# 全局配置实例
settings = Settings()