Refactor backend configuration management and update environment settings

- Replace the `config.py` module with a new `settings.py` to streamline environment variable management, focusing on database, CORS, and TURN settings.
- Update references throughout the backend codebase to use the new `settings` module instead of the deprecated `config`.
- Modify the `.env.example` file to reflect the new configuration approach, indicating that model provider credentials should be maintained separately.
- Enhance the `AssistantConfig` model to clarify the source of runtime connection information, ensuring it is injected from model resources rather than relying on defaults from the environment.
- Introduce new user scripts for audio and video management in the Tampermonkey environment, enhancing WebRTC capabilities.
This commit is contained in:
Xin Wang
2026-07-09 21:40:29 +08:00
parent 195579c5b7
commit 35d24acf40
19 changed files with 106 additions and 139 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -1,28 +1,6 @@
# 复制为 .env 并填入真实值。 # 复制为 .env 并填入真实值。
# 国产栈走 OpenAI 兼容协议:每类服务给一个 base_url + api_key + 模型名即可 # 模型接入配置不再放在 .env;请在 model_resources 中维护 apiUrl/modelId/apiKey 等字段
# 开发环境可先执行 `make db-seed`,再到前端「组件 / 模型」里替换真实密钥。
# ---- LLM:DeepSeek(OpenAI 兼容,直连云端,只需 key) ----
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_API_KEY=sk-your-deepseek-key
LLM_MODEL=deepseek-chat
# ---- STT:SenseVoice / FunASR ----
# 需要本地起一个 OpenAI 兼容的语音转写服务(/v1/audio/transcriptions),
# 例如用 funasr / sherpa-onnx / speaches 包一层。下面填那个服务地址。
STT_BASE_URL=http://localhost:8001/v1
STT_API_KEY=local
STT_MODEL=sensevoice
# ---- TTS:CosyVoice ----
# 同样需要本地起一个 OpenAI 兼容的 TTS 服务(/v1/audio/speech)。
TTS_BASE_URL=http://localhost:8002/v1
TTS_API_KEY=local
TTS_MODEL=cosyvoice
TTS_VOICE=中文女
# ---- Realtime 模式(可选,先不接也行) ----
REALTIME_API_KEY=
REALTIME_MODEL=gpt-realtime
# ---- 数据库(Postgres) ---- # ---- 数据库(Postgres) ----
# 本地直连;docker compose 里则用 postgres:5432 # 本地直连;docker compose 里则用 postgres:5432

View File

@@ -25,7 +25,7 @@ pipecat 把"管线"和"输出方式"解耦:同一条 `STT→LLM→TTS` 管线可
``` ```
ai-video-backend/ ai-video-backend/
├── app.py # FastAPI 入口,挂路由 + CORS ├── app.py # FastAPI 入口,挂路由 + CORS
├── config.py # 读 .env,模型接口环境变量兜底 ├── settings.py # 读 .env,仅存数据库/CORS/TURN 等运行设置
├── models.py # AssistantConfig(对齐前端 AssistantForm) ├── models.py # AssistantConfig(对齐前端 AssistantForm)
├── routes/ # 一个文件一组端点(对齐 dograh routes/) ├── routes/ # 一个文件一组端点(对齐 dograh routes/)
│ ├── health.py │ ├── health.py
@@ -96,7 +96,7 @@ uv pip install fastapi "uvicorn[standard]" sqlalchemy asyncpg greenlet python-do
# 阶段 B:做语音时再装全量(含 pipecat,需 3.10+) # 阶段 B:做语音时再装全量(含 pipecat,需 3.10+)
# uv pip install -r requirements.txt # uv pip install -r requirements.txt
cp .env.example .env # CRUD 阶段只需 DATABASE_URL;语音再填模型 key cp .env.example .env # 只放数据库/CORS/TURN;模型 key 在模型资源里维护
# 起 Postgres:在 ai-video/ 下 docker compose up -d postgres # 起 Postgres:在 ai-video/ 下 docker compose up -d postgres
cd .. cd ..
make db-migrate # 首次或表结构变更后执行 Alembic 迁移 make db-migrate # 首次或表结构变更后执行 Alembic 迁移

View File

@@ -14,7 +14,7 @@
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import config import settings
import uvicorn import uvicorn
from db.session import sync_interface_definitions from db.session import sync_interface_definitions
from fastapi import FastAPI from fastapi import FastAPI
@@ -41,7 +41,7 @@ app = FastAPI(title="AI Video Assistant 平台 - 后端", lifespan=lifespan)
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=config.CORS_ORIGINS, allow_origins=settings.CORS_ORIGINS,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["*"],
allow_headers=["*"], allow_headers=["*"],
@@ -57,4 +57,4 @@ app.include_router(voice_ws.router)
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("app:app", host=config.HOST, port=config.PORT, reload=True) uvicorn.run("app:app", host=settings.HOST, port=settings.PORT, reload=True)

View File

@@ -1,55 +0,0 @@
"""集中读取环境变量。所有模型接口的环境变量兜底都在这里。"""
import os
from dotenv import load_dotenv
load_dotenv()
def _split(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
# ---- LLM(DeepSeek 等,OpenAI 兼容) ----
LLM_BASE_URL = os.getenv("LLM_BASE_URL", "https://api.deepseek.com/v1")
LLM_API_KEY = os.getenv("LLM_API_KEY", "")
LLM_MODEL = os.getenv("LLM_MODEL", "deepseek-chat")
# ---- STT(SenseVoice / FunASR,OpenAI 兼容) ----
STT_BASE_URL = os.getenv("STT_BASE_URL", "http://localhost:8001/v1")
STT_API_KEY = os.getenv("STT_API_KEY", "local")
STT_MODEL = os.getenv("STT_MODEL", "sensevoice")
# ---- TTS(CosyVoice,OpenAI 兼容) ----
TTS_BASE_URL = os.getenv("TTS_BASE_URL", "http://localhost:8002/v1")
TTS_API_KEY = os.getenv("TTS_API_KEY", "local")
TTS_MODEL = os.getenv("TTS_MODEL", "cosyvoice")
TTS_VOICE = os.getenv("TTS_VOICE", "中文女")
# ---- Realtime(可选) ----
REALTIME_API_KEY = os.getenv("REALTIME_API_KEY", "")
REALTIME_MODEL = os.getenv("REALTIME_MODEL", "gpt-realtime")
# ---- 数据库(Postgres) ----
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/postgres",
)
# ---- 服务 ----
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "8000"))
CORS_ORIGINS = _split(
os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000")
)
# ---- WebRTC TURN(公网跨网语音预览;本地开发留空即可) ----
# TURN_URLS 示例:turn:182.92.86.220:3478?transport=udp,turn:182.92.86.220:3478?transport=tcp
TURN_URLS = _split(os.getenv("TURN_URLS", ""))
# coturn --use-auth-secret 时与 --static-auth-secret 一致;后端据此签发短时凭证
TURN_SECRET = os.getenv("TURN_SECRET", "")
# 不用 secret 时可改静态账号(需 coturn --user=...)
TURN_USERNAME = os.getenv("TURN_USERNAME", "")
TURN_PASSWORD = os.getenv("TURN_PASSWORD", "")
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))

View File

@@ -8,7 +8,7 @@
from collections.abc import AsyncGenerator from collections.abc import AsyncGenerator
import json import json
import config import settings
from services.interface_catalog import INTERFACE_DEFINITIONS from services.interface_catalog import INTERFACE_DEFINITIONS
from sqlalchemy import text from sqlalchemy import text
from sqlalchemy.ext.asyncio import ( from sqlalchemy.ext.asyncio import (
@@ -17,7 +17,7 @@ from sqlalchemy.ext.asyncio import (
create_async_engine, create_async_engine,
) )
engine = create_async_engine(config.DATABASE_URL, echo=False, pool_pre_ping=True) engine = create_async_engine(settings.DATABASE_URL, echo=False, pool_pre_ping=True)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False) SessionLocal = async_sessionmaker(engine, expire_on_commit=False)

View File

@@ -7,7 +7,7 @@ from sqlalchemy import pool
from sqlalchemy.engine import Connection from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config from sqlalchemy.ext.asyncio import async_engine_from_config
import config as app_config import settings as app_settings
from db.models import Base from db.models import Base
config = context.config config = context.config
@@ -19,7 +19,7 @@ target_metadata = Base.metadata
def get_url() -> str: def get_url() -> str:
return app_config.DATABASE_URL return app_settings.DATABASE_URL
def run_migrations_offline() -> None: def run_migrations_offline() -> None:

View File

@@ -70,7 +70,7 @@ class AssistantConfig(BaseModel):
fastgpt_app_id: str = "" fastgpt_app_id: str = ""
# ---- 运行时连接信息(服务端注入,不来自浏览器) ---- # ---- 运行时连接信息(服务端注入,不来自浏览器) ----
# 为空时,service_factory 会回退到 config.py 的 .env 默认值 # 由模型资源注入;调试 inline_config 也必须显式提供
llm_api_key: str = "" llm_api_key: str = ""
llm_base_url: str = "" llm_base_url: str = ""
stt_api_key: str = "" stt_api_key: str = ""

View File

@@ -1,10 +1,9 @@
"""assistant_id → 运行时配置(把真 key 在服务端组装好)。 """assistant_id → 运行时配置(把真 key 在服务端组装好)。
浏览器只传 assistant_id;真 key 在这里从 model_resources 取出注入。 浏览器只传 assistant_id;真 key 在这里从 model_resources 取出注入。
助手按 capability binding 引用资源;取不到则回退该能力默认资源,再回退 .env 助手按 capability binding 引用资源;取不到则回退该能力默认资源。
""" """
import config
from db.models import Assistant, AssistantModelBinding, ModelResource from db.models import Assistant, AssistantModelBinding, ModelResource
from models import AssistantConfig from models import AssistantConfig
from sqlalchemy import select from sqlalchemy import select
@@ -63,14 +62,14 @@ async def _agent_resource_for(
).scalar_one_or_none() ).scalar_one_or_none()
def _value(resource: ModelResource | None, key: str, default): def _value(resource: ModelResource | None, key: str, default=""):
if not resource: if not resource:
return default return default
value = (resource.values or {}).get(key, default) value = (resource.values or {}).get(key, default)
return default if value is None else value return default if value is None else value
def _secret(resource: ModelResource | None, key: str, default: str) -> str: def _secret(resource: ModelResource | None, key: str, default: str = "") -> str:
if not resource: if not resource:
return default return default
return str((resource.secrets or {}).get(key) or default) return str((resource.secrets or {}).get(key) or default)
@@ -148,19 +147,15 @@ async def resolve_runtime_config(
agent_interface_type=(agent_resource.interface_type if agent_resource else ""), agent_interface_type=(agent_resource.interface_type if agent_resource else ""),
agent_values=(agent_resource.values or {}) if agent_resource else {}, agent_values=(agent_resource.values or {}) if agent_resource else {},
agent_secrets=(agent_resource.secrets or {}) if agent_resource else {}, agent_secrets=(agent_resource.secrets or {}) if agent_resource else {},
# 运行时连接信息(真 key + url):模型资源优先,否则 .env 兜底 # 运行时连接信息(真 key + url):来自模型资源,不再从 .env 兜底
llm_api_key=_secret(llm_resource, "apiKey", config.LLM_API_KEY), llm_api_key=_secret(llm_resource, "apiKey"),
llm_base_url=str(_value(llm_resource, "apiUrl", config.LLM_BASE_URL)), llm_base_url=str(_value(llm_resource, "apiUrl")),
vision_llm_api_key=_secret( vision_llm_api_key=_secret(vision_resource, "apiKey"),
vision_resource, "apiKey", config.LLM_API_KEY vision_llm_base_url=str(_value(vision_resource, "apiUrl")),
), stt_api_key=_secret(stt_resource, "apiKey"),
vision_llm_base_url=str( stt_base_url=str(_value(stt_resource, "apiUrl")),
_value(vision_resource, "apiUrl", config.LLM_BASE_URL) tts_api_key=_secret(tts_resource, "apiKey"),
), tts_base_url=str(_value(tts_resource, "apiUrl")),
stt_api_key=_secret(stt_resource, "apiKey", config.STT_API_KEY),
stt_base_url=str(_value(stt_resource, "apiUrl", config.STT_BASE_URL)),
tts_api_key=_secret(tts_resource, "apiKey", config.TTS_API_KEY),
tts_base_url=str(_value(tts_resource, "apiUrl", config.TTS_BASE_URL)),
realtime_api_key=_secret(realtime_resource, "apiKey", ""), realtime_api_key=_secret(realtime_resource, "apiKey", ""),
realtime_base_url=str(_value(realtime_resource, "apiUrl", "")), realtime_base_url=str(_value(realtime_resource, "apiUrl", "")),
) )

View File

@@ -12,10 +12,10 @@ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
import httpx import httpx
from websockets.asyncio.client import connect as websocket_connect from websockets.asyncio.client import connect as websocket_connect
import config
from schemas import ModelResourceTestResult from schemas import ModelResourceTestResult
TEST_TIMEOUT_SECONDS = 10.0 TEST_TIMEOUT_SECONDS = 10.0
DEFAULT_TEST_TTS_VOICE = "alloy"
def _endpoint(base_url: str, path: str) -> str: def _endpoint(base_url: str, path: str) -> str:
@@ -116,7 +116,7 @@ async def test_model_resource(
json={ json={
"model": model_id, "model": model_id,
"input": "测试", "input": "测试",
"voice": str(values.get("voice") or config.TTS_VOICE), "voice": str(values.get("voice") or DEFAULT_TEST_TTS_VOICE),
"response_format": "pcm", "response_format": "pcm",
"speed": float(values.get("speed") or 1), "speed": float(values.get("speed") or 1),
}, },

View File

@@ -11,7 +11,6 @@ import base64
from io import BytesIO from io import BytesIO
from uuid import uuid4 from uuid import uuid4
import config
from loguru import logger from loguru import logger
from models import AssistantConfig from models import AssistantConfig
from openai import AsyncOpenAI from openai import AsyncOpenAI
@@ -77,6 +76,12 @@ VISION_ANALYSIS_SYSTEM_PROMPT = (
) )
def _require(value: str, label: str) -> str:
if value:
return value
raise ValueError(f"缺少模型资源配置: {label}")
def _vision_uses_main_llm(cfg: AssistantConfig) -> bool: def _vision_uses_main_llm(cfg: AssistantConfig) -> bool:
"""模型自己支持图片时,沿用 Pipecat 的同上下文视觉工具路径。""" """模型自己支持图片时,沿用 Pipecat 的同上下文视觉工具路径。"""
return not cfg.vision_model_resource_id and cfg.llm_support_image_input return not cfg.vision_model_resource_id and cfg.llm_support_image_input
@@ -107,12 +112,12 @@ async def _analyze_image_with_vision_model(
extra_body = cfg.vision_llm_values.get("extraBody") extra_body = cfg.vision_llm_values.get("extraBody")
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {} extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
client = AsyncOpenAI( client = AsyncOpenAI(
api_key=cfg.vision_llm_api_key or config.LLM_API_KEY, api_key=_require(cfg.vision_llm_api_key, "Vision LLM apiKey"),
base_url=cfg.vision_llm_base_url or config.LLM_BASE_URL, base_url=_require(cfg.vision_llm_base_url, "Vision LLM apiUrl"),
) )
try: try:
response = await client.chat.completions.create( response = await client.chat.completions.create(
model=cfg.vision_model or config.LLM_MODEL, model=_require(cfg.vision_model, "Vision LLM modelId"),
messages=[ messages=[
{"role": "system", "content": VISION_ANALYSIS_SYSTEM_PROMPT}, {"role": "system", "content": VISION_ANALYSIS_SYSTEM_PROMPT},
{ {
@@ -665,9 +670,9 @@ async def run_pipeline(
target = await engine.route( target = await engine.route(
wf_state["current"], wf_state["current"],
history, history,
api_key=cfg.llm_api_key or config.LLM_API_KEY, api_key=_require(cfg.llm_api_key, "LLM apiKey"),
base_url=cfg.llm_base_url or config.LLM_BASE_URL, base_url=_require(cfg.llm_base_url, "LLM apiUrl"),
model=cfg.model or config.LLM_MODEL, model=_require(cfg.model, "LLM modelId"),
) )
if target and target != wf_state["current"]: if target and target != wf_state["current"]:
logger.info(f"文本兜底触发转移 → {engine.name(target)}") logger.info(f"文本兜底触发转移 → {engine.name(target)}")

View File

@@ -4,7 +4,6 @@
按 interface_type 扩展时在这里加分支即可——这是未来接更多模型的唯一入口。 按 interface_type 扩展时在这里加分支即可——这是未来接更多模型的唯一入口。
""" """
import config
from loguru import logger from loguru import logger
from models import AssistantConfig from models import AssistantConfig
@@ -27,6 +26,12 @@ from services.pipecat.xfyun_tts import DEFAULT_XFYUN_TTS_URL, XfyunTTSService
TTS_STOP_FRAME_TIMEOUT_S = 1.0 TTS_STOP_FRAME_TIMEOUT_S = 1.0
def _require(value: str, label: str) -> str:
if value:
return value
raise ValueError(f"缺少模型资源配置: {label}")
def _language(value: str) -> Language | None: def _language(value: str) -> Language | None:
if not value: if not value:
return None return None
@@ -40,7 +45,7 @@ def _language(value: str) -> Language | None:
def create_stt(cfg: AssistantConfig): def create_stt(cfg: AssistantConfig):
"""SenseVoice / FunASR 等,走 OpenAI 兼容的 /v1/audio/transcriptions。 """SenseVoice / FunASR 等,走 OpenAI 兼容的 /v1/audio/transcriptions。
连接信息优先用 cfg(由 config_resolver 从 DB 注入),为空回退 .env 默认 连接信息来自 cfg(由 config_resolver 从 DB 注入,或调试时 inline_config 传入)
""" """
if cfg.stt_interface_type == "xfyun-asr": if cfg.stt_interface_type == "xfyun-asr":
return XfyunASRService( return XfyunASRService(
@@ -59,10 +64,10 @@ def create_stt(cfg: AssistantConfig):
raise ValueError(f"不支持的 ASR 接口类型: {cfg.stt_interface_type}") raise ValueError(f"不支持的 ASR 接口类型: {cfg.stt_interface_type}")
return OpenAISTTService( return OpenAISTTService(
api_key=cfg.stt_api_key or config.STT_API_KEY, api_key=_require(cfg.stt_api_key, "ASR apiKey"),
base_url=cfg.stt_base_url or config.STT_BASE_URL, base_url=_require(cfg.stt_base_url, "ASR apiUrl"),
settings=OpenAISTTService.Settings( settings=OpenAISTTService.Settings(
model=cfg.asr or config.STT_MODEL, model=_require(cfg.asr, "ASR modelId"),
language=_language(cfg.stt_language), language=_language(cfg.stt_language),
), ),
) )
@@ -75,10 +80,10 @@ def create_llm(cfg: AssistantConfig):
extra_body = cfg.llm_values.get("extraBody") extra_body = cfg.llm_values.get("extraBody")
extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {} extra = {"extra_body": extra_body} if isinstance(extra_body, dict) else {}
return OpenAILLMService( return OpenAILLMService(
api_key=cfg.llm_api_key or config.LLM_API_KEY, api_key=_require(cfg.llm_api_key, "LLM apiKey"),
base_url=cfg.llm_base_url or config.LLM_BASE_URL, base_url=_require(cfg.llm_base_url, "LLM apiUrl"),
settings=OpenAILLMService.Settings( settings=OpenAILLMService.Settings(
model=cfg.model or config.LLM_MODEL, model=_require(cfg.model, "LLM modelId"),
extra=extra, extra=extra,
), ),
) )
@@ -86,7 +91,7 @@ def create_llm(cfg: AssistantConfig):
def create_tts(cfg: AssistantConfig): def create_tts(cfg: AssistantConfig):
"""CosyVoice 等,走 OpenAI 兼容的 /v1/audio/speech。""" """CosyVoice 等,走 OpenAI 兼容的 /v1/audio/speech。"""
voice = cfg.voice or config.TTS_VOICE voice = _require(cfg.voice, "TTS voice")
if cfg.tts_interface_type == "xfyun-super-tts": if cfg.tts_interface_type == "xfyun-super-tts":
return XfyunSuperTTSService( return XfyunSuperTTSService(
app_id=str(cfg.tts_secrets.get("appId") or ""), app_id=str(cfg.tts_secrets.get("appId") or ""),
@@ -126,11 +131,11 @@ def create_tts(cfg: AssistantConfig):
# 注册为原样映射后仍由 OpenAI SDK 按字符串透传给供应商。 # 注册为原样映射后仍由 OpenAI SDK 按字符串透传给供应商。
VALID_VOICES.setdefault(voice, voice) VALID_VOICES.setdefault(voice, voice)
return OpenAITTSService( return OpenAITTSService(
api_key=cfg.tts_api_key or config.TTS_API_KEY, api_key=_require(cfg.tts_api_key, "TTS apiKey"),
base_url=cfg.tts_base_url or config.TTS_BASE_URL, base_url=_require(cfg.tts_base_url, "TTS apiUrl"),
stop_frame_timeout_s=TTS_STOP_FRAME_TIMEOUT_S, stop_frame_timeout_s=TTS_STOP_FRAME_TIMEOUT_S,
settings=OpenAITTSService.Settings( settings=OpenAITTSService.Settings(
model=cfg.tts_model or config.TTS_MODEL, model=_require(cfg.tts_model, "TTS modelId"),
voice=voice, voice=voice,
speed=cfg.tts_speed, speed=cfg.tts_speed,
), ),
@@ -143,9 +148,9 @@ def create_realtime_service(cfg: AssistantConfig):
from services.pipecat.stepfun_realtime import StepFunRealtimeService from services.pipecat.stepfun_realtime import StepFunRealtimeService
return StepFunRealtimeService( return StepFunRealtimeService(
api_key=cfg.realtime_api_key, api_key=_require(cfg.realtime_api_key, "Realtime apiKey"),
model=cfg.realtimeModel, model=_require(cfg.realtimeModel, "Realtime modelId"),
base_url=cfg.realtime_base_url, base_url=_require(cfg.realtime_base_url, "Realtime apiUrl"),
instructions=cfg.prompt, instructions=cfg.prompt,
voice=str(cfg.realtime_values.get("voice") or "linjiajiejie"), voice=str(cfg.realtime_values.get("voice") or "linjiajiejie"),
input_sample_rate=int( input_sample_rate=int(

View File

@@ -7,28 +7,28 @@ import hashlib
import hmac import hmac
import time import time
import config import settings
STUN_URL = "stun:stun.l.google.com:19302" STUN_URL = "stun:stun.l.google.com:19302"
def _turn_credentials() -> tuple[str, str] | None: def _turn_credentials() -> tuple[str, str] | None:
"""Return (username, credential) for TURN, or None when TURN is not configured.""" """Return (username, credential) for TURN, or None when TURN is not configured."""
if not config.TURN_URLS: if not settings.TURN_URLS:
return None return None
if config.TURN_SECRET: if settings.TURN_SECRET:
expiry = int(time.time()) + config.TURN_CREDENTIAL_TTL expiry = int(time.time()) + settings.TURN_CREDENTIAL_TTL
username = f"{expiry}:ai-video" username = f"{expiry}:ai-video"
credential = base64.b64encode( credential = base64.b64encode(
hmac.new( hmac.new(
config.TURN_SECRET.encode("utf-8"), settings.TURN_SECRET.encode("utf-8"),
username.encode("utf-8"), username.encode("utf-8"),
hashlib.sha1, hashlib.sha1,
).digest() ).digest()
).decode("utf-8") ).decode("utf-8")
return username, credential return username, credential
if config.TURN_USERNAME and config.TURN_PASSWORD: if settings.TURN_USERNAME and settings.TURN_PASSWORD:
return config.TURN_USERNAME, config.TURN_PASSWORD return settings.TURN_USERNAME, settings.TURN_PASSWORD
return None return None
@@ -39,7 +39,7 @@ def client_ice_servers() -> list[dict]:
if not creds: if not creds:
return servers return servers
username, credential = creds username, credential = creds
for url in config.TURN_URLS: for url in settings.TURN_URLS:
servers.append({"urls": url, "username": username, "credential": credential}) servers.append({"urls": url, "username": username, "credential": credential})
return servers return servers
@@ -53,7 +53,7 @@ def aiortc_ice_servers() -> list:
if not creds: if not creds:
return servers return servers
username, credential = creds username, credential = creds
for url in config.TURN_URLS: for url in settings.TURN_URLS:
servers.append( servers.append(
RTCIceServer(urls=url, username=username, credential=credential) RTCIceServer(urls=url, username=username, credential=credential)
) )

39
backend/settings.py Normal file
View File

@@ -0,0 +1,39 @@
"""Runtime settings for backend infrastructure.
Model provider credentials live in ``model_resources`` and are resolved per
assistant. Keep this module limited to process-level settings such as database,
CORS, server bind address, and WebRTC ICE/TURN configuration.
"""
import os
from dotenv import load_dotenv
load_dotenv()
def _split(value: str) -> list[str]:
return [item.strip() for item in value.split(",") if item.strip()]
# ---- Database ----
DATABASE_URL = os.getenv(
"DATABASE_URL",
"postgresql+asyncpg://postgres:postgres@localhost:5432/postgres",
)
# ---- Service ----
HOST = os.getenv("HOST", "0.0.0.0")
PORT = int(os.getenv("PORT", "8000"))
CORS_ORIGINS = _split(
os.getenv("CORS_ORIGINS", "http://localhost:3000,http://127.0.0.1:3000")
)
# ---- WebRTC TURN ----
# TURN_URLS example:
# turn:182.92.86.220:3478?transport=udp,turn:182.92.86.220:3478?transport=tcp
TURN_URLS = _split(os.getenv("TURN_URLS", ""))
TURN_SECRET = os.getenv("TURN_SECRET", "")
TURN_USERNAME = os.getenv("TURN_USERNAME", "")
TURN_PASSWORD = os.getenv("TURN_PASSWORD", "")
TURN_CREDENTIAL_TTL = int(os.getenv("TURN_CREDENTIAL_TTL", "86400"))