seaislee1209 6c364f4c3f feat: v0.11.0 素材库功能 + 生成页面 UI 优化
素材库(虚拟人像):
- 后端:AssetGroup/Asset 模型 + 火山 Assets API 客户端 + 7 个 API 端点
- 前端:素材库管理弹窗(上传/浏览/追加/改名/状态轮询)
- PromptInput:@ 搜索素材库 + mention 标签(缩略图+名字)
- 提交生成时提取 asset:// 引用并去重
- 打开素材详情时自动检查云端状态,已删除的自动清理
- 后端 reference_snapshots 存储 thumb_url,刷新后标签缩略图和 hover 预览正常

生成页面 UI:
- 提示词 hover 即梦风格:原位展开玻璃底覆盖视频,不弹浮层
- 标签(AirDrama/时长/比例)inline 排列,溢出时 canvas 截断
- 详细信息弹窗支持鼠标移上去不消失(延迟关闭),增加 token/费用信息
- 任务卡片/视频详情页提示词标签化(renderPromptWithMentions)
- 视频详情页底部去掉重复按钮,信息栏 flex-wrap 自动换行

mention 标签:
- 输入框内剪切/复制粘贴保留标签(handlePaste 检测 text/html)
- 拖拽标签跟手(caretRangeFromPoint + drop 位置精确插入)
- 拖拽时 hover 预览自动关闭,InputBar 蓝边仅外部文件拖入时触发

其他:
- 联网搜索按钮(暂禁用,等火山确认 API)
- card max-width 800→1024,参考图缩略图 48→56px 居中对齐
- 导航箭头禁用时不触发关闭(去掉 pointer-events:none)
- API 错误信息附带原始报错便于排查

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-22 03:11:05 +08:00

223 lines
8.5 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.

"""Django settings for AirDrama backend."""
import os
from pathlib import Path
from datetime import timedelta
BASE_DIR = Path(__file__).resolve().parent.parent
# 自动加载 .env.local本地开发用不进 git
_env_local = BASE_DIR / '.env.local'
if _env_local.exists():
with open(_env_local, encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
# 去掉 export 前缀
if line.startswith('export '):
line = line[7:]
key, _, value = line.partition('=')
if key and _ == '=':
# 去掉引号
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key.strip(), value)
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', '')
if not SECRET_KEY:
import warnings
warnings.warn('DJANGO_SECRET_KEY not set — using insecure dev key. Do NOT deploy like this.')
SECRET_KEY = 'django-insecure-dev-only-do-not-deploy'
DEBUG = os.environ.get('DJANGO_DEBUG', 'False').lower() in ('true', '1', 'yes')
ALLOWED_HOSTS = os.environ.get('DJANGO_ALLOWED_HOSTS', 'localhost,127.0.0.1').split(',')
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party
'rest_framework',
'corsheaders',
# Local apps
'apps.accounts',
'apps.generation',
]
MIDDLEWARE = [
'utils.log_center.LogCenterMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'config.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'config.wsgi.application'
# Database configuration
# TESTING=true → isolated test_db.sqlite3 (never pollutes dev data)
# USE_MYSQL=true → Aliyun RDS MySQL (production)
# Otherwise → SQLite db.sqlite3 (local dev)
TESTING = os.environ.get('TESTING', 'false').lower() in ('true', '1', 'yes')
if TESTING:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'test_db.sqlite3',
}
}
elif os.environ.get('USE_MYSQL', 'false').lower() in ('true', '1', 'yes'):
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get('DB_NAME', 'video_auto'),
'USER': os.environ.get('DB_USER', 'ai_video'),
'PASSWORD': os.environ.get('DB_PASSWORD', ''),
'HOST': os.environ.get('DB_HOST', 'localhost'),
'PORT': os.environ.get('DB_PORT', '3306'),
'OPTIONS': {
'charset': 'utf8mb4',
'init_command': "SET sql_mode='STRICT_TRANS_TABLES'",
},
}
}
else:
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
AUTH_USER_MODEL = 'accounts.User'
AUTH_PASSWORD_VALIDATORS = [
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 'OPTIONS': {'min_length': 8}},
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
]
# REST Framework
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'apps.accounts.authentication.SessionJWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_RENDERER_CLASSES': (
'rest_framework.renderers.JSONRenderer',
),
'EXCEPTION_HANDLER': 'utils.log_center.custom_exception_handler',
'DEFAULT_THROTTLE_CLASSES': [
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
],
'DEFAULT_THROTTLE_RATES': {
'anon': '30/minute',
'user': '120/minute',
'login': '5/minute',
},
}
# JWT settings
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'AUTH_HEADER_TYPES': ('Bearer',),
}
# CORS
CORS_ALLOWED_ORIGINS = [
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:3000',
]
_extra_cors = os.environ.get('CORS_ALLOWED_ORIGINS', '')
if _extra_cors:
CORS_ALLOWED_ORIGINS += [o.strip() for o in _extra_cors.split(',') if o.strip()]
CORS_ALLOW_CREDENTIALS = True
CSRF_TRUSTED_ORIGINS = [o for o in CORS_ALLOWED_ORIGINS if o.startswith('https://')]
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
USE_TZ = True
STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ──────────────────────────────────────────────
# IP Geolocation
# ──────────────────────────────────────────────
ALIYUN_IP_GEO_APPCODE = os.environ.get('ALIYUN_IP_GEO_APPCODE', '93a86e9dfc9e4c71bcd44baa4008e662')
IP2REGION_DB_PATH = BASE_DIR / 'data' / 'ip2region.xdb'
# ──────────────────────────────────────────────
# Security headers (production)
# ──────────────────────────────────────────────
if not DEBUG:
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# ──────────────────────────────────────────────
# TOS (Volcano Engine Object Storage)
# ──────────────────────────────────────────────
TOS_ACCESS_KEY = os.environ.get('TOS_ACCESS_KEY', '')
TOS_SECRET_KEY = os.environ.get('TOS_SECRET_KEY', '')
TOS_ENDPOINT = os.environ.get('TOS_ENDPOINT', 'https://tos-cn-beijing.volces.com')
TOS_BUCKET = os.environ.get('TOS_BUCKET', 'airdrama-media')
TOS_REGION = os.environ.get('TOS_REGION', 'cn-beijing')
TOS_CDN_DOMAIN = os.environ.get('TOS_CDN_DOMAIN', 'https://airdrama-media.tos-cn-beijing.volces.com')
# ──────────────────────────────────────────────
# Seedance API (Volcano Engine ARK)
# ──────────────────────────────────────────────
ARK_API_KEY = os.environ.get('ARK_API_KEY', '')
ARK_BASE_URL = os.environ.get('ARK_BASE_URL', 'https://ark.cn-beijing.volces.com/api/v3')
# Set to True when Seedance model is activated on ARK platform
SEEDANCE_ENABLED = os.environ.get('SEEDANCE_ENABLED', 'false').lower() == 'true'
# Set to True to enable the Assets API (virtual avatar library)
ASSETS_API_ENABLED = os.environ.get('ASSETS_API_ENABLED', 'false').lower() == 'true'
# ──────────────────────────────────────────────
# Aliyun SMS (短信告警)
# ──────────────────────────────────────────────
ALIYUN_SMS_ACCESS_KEY = os.environ.get('ALIYUN_SMS_ACCESS_KEY', '')
ALIYUN_SMS_ACCESS_SECRET = os.environ.get('ALIYUN_SMS_ACCESS_SECRET', '')
ALIYUN_SMS_SIGN_NAME = os.environ.get('ALIYUN_SMS_SIGN_NAME', '广州气元科技')
ALIYUN_SMS_TEMPLATE_CODE = os.environ.get('ALIYUN_SMS_TEMPLATE_CODE', 'SMS_503445109')