seaislee1209 da9a1413c3 v0.18.0 素材库多类型支持 + @ 引用改为单素材
对齐火山 API 文档(Asset URI 小写、HEIC/HEIF、DeleteAsset)
素材库支持视频/音频上传(按类型分三区显示、前端校验、拖拽上传)
@ 引用从素材组改为单个素材(搜索返回具体素材、即时数量/时长检查)
ffmpeg 视频封面帧提取 + 音频时长读取(Celery 异步)
生产级安全修复(跨团队校验、异常信息脱敏、下载大小限制)
2026-04-04 17:36:35 +08:00

173 lines
9.6 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.

import uuid
from django.db import models
from django.conf import settings
class GenerationRecord(models.Model):
"""Video generation call record."""
MODE_CHOICES = [
('universal', '全能参考'),
('keyframe', '首尾帧'),
]
MODEL_CHOICES = [
('seedance_2.0', 'AirDrama'),
('seedance_2.0_fast', 'AirDrama Fast'),
]
STATUS_CHOICES = [
('queued', '排队中'),
('processing', '生成中'),
('completed', '已完成'),
('failed', '失败'),
]
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name='generation_records',
verbose_name='用户',
)
task_id = models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='任务ID')
ark_task_id = models.CharField(max_length=100, blank=True, default='', verbose_name='火山ARK任务ID')
prompt = models.TextField(blank=True, verbose_name='提示词')
mode = models.CharField(max_length=20, choices=MODE_CHOICES, verbose_name='创作模式')
model = models.CharField(max_length=30, choices=MODEL_CHOICES, verbose_name='模型')
aspect_ratio = models.CharField(max_length=10, verbose_name='宽高比')
duration = models.IntegerField(verbose_name='视频时长(秒)')
seconds_consumed = models.FloatField(default=0, verbose_name='消费秒数')
# ── 金额计费字段v0.10.0 新增) ──
tokens_consumed = models.IntegerField(default=0, verbose_name='消耗tokens')
cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='用户费用(元)')
base_cost_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='平台成本(元)')
frozen_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0, verbose_name='冻结金额(元)')
resolution = models.CharField(max_length=10, blank=True, default='', verbose_name='分辨率')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='queued', verbose_name='状态')
result_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='生成结果URL')
thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='视频缩略图URL')
error_message = models.TextField(blank=True, default='', verbose_name='错误信息')
raw_error = models.TextField(blank=True, default='', verbose_name='原始错误信息')
reference_urls = models.JSONField(default=list, blank=True, verbose_name='参考素材信息')
is_favorited = models.BooleanField(default=False, verbose_name='已收藏')
is_deleted = models.BooleanField(default=False, verbose_name='用户已删除')
seed = models.BigIntegerField(default=-1, verbose_name='种子值')
created_at = models.DateTimeField(auto_now_add=True, db_index=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间')
class Meta:
verbose_name = '生成记录'
verbose_name_plural = '生成记录'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'created_at']),
]
def __str__(self):
return f'{self.user.username} - {self.task_id}'
class QuotaConfig(models.Model):
"""Global quota configuration (singleton) — Phase 3: seconds + announcement + anomaly detection."""
default_daily_seconds_limit = models.IntegerField(default=600, verbose_name='默认每日秒数上限')
default_monthly_seconds_limit = models.IntegerField(default=6000, verbose_name='默认每月秒数上限')
announcement = models.TextField(blank=True, default='', verbose_name='系统公告')
announcement_enabled = models.BooleanField(default=False, verbose_name='启用公告')
max_desktop_sessions = models.IntegerField(default=1, verbose_name='每用户最大桌面端会话数')
max_mobile_sessions = models.IntegerField(default=0, verbose_name='每用户最大移动端会话数')
# ── 异常检测全局默认配置 ──
anomaly_detection_enabled = models.BooleanField(default=False, verbose_name='异常检测总开关')
r1_enabled_default = models.BooleanField(default=True, verbose_name='R1 默认开关')
r2_enabled_default = models.BooleanField(default=True, verbose_name='R2 默认开关')
r2_window_seconds = models.IntegerField(default=3600, verbose_name='R2 默认时间窗口(秒)')
r3_enabled_default = models.BooleanField(default=True, verbose_name='R3 默认开关')
r3_window_seconds = models.IntegerField(default=3600, verbose_name='R3 默认时间窗口(秒)')
r3_max_count = models.IntegerField(default=10, verbose_name='R3 默认最大登录次数')
r4_enabled_default = models.BooleanField(default=True, verbose_name='R4 默认开关')
r4_window_seconds = models.IntegerField(default=3600, verbose_name='R4 默认时间窗口(秒)')
r4_city_count = models.IntegerField(default=5, verbose_name='R4 默认预期外城市数')
r5_enabled_default = models.BooleanField(default=True, verbose_name='R5 默认开关')
r5_days = models.IntegerField(default=7, verbose_name='R5 默认统计天数')
r5_country_count = models.IntegerField(default=10, verbose_name='R5 默认海外国家数')
feishu_alert_mobiles = models.CharField(max_length=500, blank=True, default='', verbose_name='飞书告警接收人手机号')
sms_alert_mobiles = models.CharField(max_length=500, blank=True, default='', verbose_name='短信告警手机号(预留)')
alert_cooldown_seconds = models.IntegerField(default=1800, verbose_name='告警冷却时间(秒)')
# ── 计费全局配置v0.10.0 新增) ──
default_daily_generation_limit = models.IntegerField(default=50, verbose_name='默认每日生成次数')
default_monthly_generation_limit = models.IntegerField(default=1500, verbose_name='默认每月生成次数')
base_token_price = models.DecimalField(max_digits=10, decimal_places=2, default=46, verbose_name='基础token单价-不含视频(元/百万tokens)')
base_token_price_video = models.DecimalField(max_digits=10, decimal_places=2, default=28, verbose_name='基础token单价-含视频(元/百万tokens)')
base_token_price_fast = models.DecimalField(max_digits=10, decimal_places=2, default=37, verbose_name='Fast单价-不含视频(元/百万tokens)')
base_token_price_fast_video = models.DecimalField(max_digits=10, decimal_places=2, default=22, verbose_name='Fast单价-含视频(元/百万tokens)')
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = '系统配置'
verbose_name_plural = '系统配置'
def save(self, *args, **kwargs):
self.pk = 1
super().save(*args, **kwargs)
def __str__(self):
return f'全局配额: {self.default_daily_seconds_limit}s/日, {self.default_monthly_seconds_limit}s/月'
class AssetGroup(models.Model):
"""虚拟人像素材组 — 一个角色对应一个组。"""
team = models.ForeignKey(
'accounts.Team', on_delete=models.CASCADE,
related_name='asset_groups', verbose_name='所属团队',
)
remote_group_id = models.CharField(max_length=100, default='', verbose_name='火山Group ID')
name = models.CharField(max_length=100, default='', verbose_name='角色名')
description = models.CharField(max_length=300, blank=True, default='', verbose_name='描述')
thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='缩略图URL')
created_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
null=True, blank=True, related_name='created_asset_groups', verbose_name='创建人',
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '素材组'
verbose_name_plural = '素材组'
ordering = ['-created_at']
def __str__(self):
return f'{self.team.name} - {self.name}'
class Asset(models.Model):
"""虚拟人像素材 — 图片/视频/音频。"""
STATUS_CHOICES = [
('processing', '处理中'),
('active', '可用'),
('failed', '失败'),
]
ASSET_TYPE_CHOICES = [
('Image', '图像'),
('Video', '视频'),
('Audio', '音频'),
]
group = models.ForeignKey(
AssetGroup, on_delete=models.CASCADE,
related_name='assets', verbose_name='所属素材组',
)
remote_asset_id = models.CharField(max_length=100, default='', verbose_name='火山Asset ID')
name = models.CharField(max_length=100, default='', verbose_name='素材名称')
url = models.CharField(max_length=1000, blank=True, default='', verbose_name='素材URL')
asset_type = models.CharField(max_length=10, choices=ASSET_TYPE_CHOICES, default='Image', verbose_name='素材类型')
thumbnail_url = models.CharField(max_length=1000, blank=True, default='', verbose_name='缩略图URL')
duration = models.FloatField(default=0, verbose_name='时长(秒)')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='processing', verbose_name='状态')
error_message = models.CharField(max_length=500, blank=True, default='', verbose_name='错误信息')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
verbose_name = '素材'
verbose_name_plural = '素材'
ordering = ['-created_at']
def __str__(self):
return f'{self.group.name} - {self.name}'