This commit is contained in:
2026-07-21 19:22:50 +08:00
parent b374a541ea
commit 7683ebb186
65 changed files with 5067 additions and 1741 deletions
+158 -44
View File
@@ -55,6 +55,16 @@ from app.services.generation.billing_service import (
get_next_credit_attempt_no,
)
from app.services.generation.refund_service import mark_generation_record_failed_and_refund_once
from app.services.generation.ai.engine_service import (
get_image_engine,
get_video_engine,
image_supported_sizes,
parse_json_list,
)
from app.services.generation.media_reference_service import (
calculate_media_reference_usage,
validate_media_reference_usage_for_engine,
)
from app.services.media_token_usage_snapshot_service import sync_generation_record_media_token_snapshot
from app.services.credit_record_meta_service import build_generation_record_prompt_meta
from app.services.video_cover_service import async_create_video_cover_for_local_video
@@ -70,6 +80,36 @@ router = APIRouter(prefix="/generation-records", tags=["generation"])
logger = logging.getLogger("videogen")
def _engine_snapshot(record: GenerationRecord) -> dict | None:
if not record.engine_snapshot_json:
return None
try:
value = json.loads(record.engine_snapshot_json)
except (TypeError, json.JSONDecodeError):
return None
return value if isinstance(value, dict) else None
def _validate_video_engine_selection(engine, *, aspect_ratio: str, resolution: str, duration: int) -> None:
ratios = [str(item) for item in parse_json_list(engine.supported_ratios, [])]
resolutions = [str(item) for item in parse_json_list(engine.supported_resolutions, [])]
durations = [int(item) for item in parse_json_list(engine.supported_durations, []) if str(item).isdigit()]
if ratios and aspect_ratio not in ratios:
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选画面比例")
if resolutions and resolution not in resolutions:
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选分辨率")
if durations and duration not in durations:
raise HTTPException(status_code=400, detail="当前视频引擎不支持所选时长")
if int(engine.max_duration or 0) > 0 and duration > int(engine.max_duration):
raise HTTPException(status_code=400, detail="生成时长超过当前视频引擎上限")
def _validate_image_engine_selection(engine, *, image_size: str) -> None:
sizes = image_supported_sizes(engine)
if sizes and image_size not in sizes:
raise HTTPException(status_code=400, detail="当前图片引擎不支持所选画面分辨率")
def _record_to_out(record: GenerationRecord, project_name: str, refs_override: list[dict] | None = None) -> GenerationRecordOut:
refs = refs_override
if refs is None and record.media_references:
@@ -112,6 +152,10 @@ def _record_to_out(record: GenerationRecord, project_name: str, refs_override: l
video_cover_url=build_resource_signed_url(record.video_cover_url) if record.video_cover_url else '',
image_url=build_resource_signed_url(record.image_url) if record.image_url else '',
references=refs,
engine_id=record.engine_id,
engine_name=(_engine_snapshot(record) or {}).get("name"),
engine_snapshot=_engine_snapshot(record),
include_media_references=bool(record.include_media_references),
text_credits_cost=round(record.text_credits_cost or 0.00, 2),
# text_tokens_used=record.text_tokens_used or 0,
credits_cost=round(record.credits_cost or 0.00, 2),
@@ -436,7 +480,11 @@ async def generate_record_resource(
raise InvalidStatusError("该任务生成失败,请联系客服进行修复")
await assert_user_resource_capacity_available(db, current_user.id)
attempt_no = await get_next_credit_attempt_no(db, owner_type=OWNER_GENERATION_RECORD, owner_id=record.id)
attempt_no = await get_next_credit_attempt_no(
db, owner_type=OWNER_GENERATION_RECORD, owner_id=record.id
)
selected_engine_id = req.engine_id or record.engine_id
record.include_media_references = bool(req.include_media_references)
from app.services.generation.pipeline.generation_record_service import (
commit_and_enqueue_generation_record,
@@ -444,56 +492,83 @@ async def generate_record_resource(
)
if record.gen_type == GenerationType.video:
if req.aspect_ratio not in ASPECT_RATIOS:
aspect_ratio = req.aspect_ratio or record.aspect_ratio
resolution = req.resolution or record.resolution
if aspect_ratio not in ASPECT_RATIOS:
raise HTTPException(status_code=400, detail="不支持的画面比例")
if req.resolution not in RESOLUTIONS:
if resolution not in RESOLUTIONS:
raise HTTPException(status_code=400, detail="不支持的分辨率")
from app.services.video_gen import get_active_engine
engine = await get_video_engine(db, selected_engine_id)
_validate_video_engine_selection(
engine,
aspect_ratio=aspect_ratio,
resolution=resolution,
duration=int(record.duration or 5),
)
from app.services.video_upscale.snapshot_service import build_video_upscale_snapshot
engine = await get_active_engine(db)
try:
supported_provider_resolutions = json.loads(engine.supported_resolutions or "[]")
except (TypeError, json.JSONDecodeError):
supported_provider_resolutions = []
supported_provider_resolutions = parse_json_list(engine.supported_resolutions, [])
provider_resolution, upscale_enabled, upscale_snapshot_json = await build_video_upscale_snapshot(
db,
target_resolution=req.resolution,
aspect_ratio=req.aspect_ratio,
target_resolution=resolution,
aspect_ratio=aspect_ratio,
supported_provider_resolutions=supported_provider_resolutions,
)
billing = await charge_generation_media_by_params(
db, user_id=current_user.id, record_id=record.id, gen_type="video",
duration=record.duration or 5, resolution=req.resolution, engine_id=engine.id,
project_name=project_name, description_prefix=project_name + "-",
owner_type=OWNER_GENERATION_RECORD, attempt_no=attempt_no,
)
record.aspect_ratio = req.aspect_ratio
record.resolution = req.resolution
record.aspect_ratio = aspect_ratio
record.resolution = resolution
record.provider_generation_resolution = provider_resolution
record.video_upscale_enabled_snapshot = upscale_enabled
record.video_upscale_snapshot_json = upscale_snapshot_json
else:
from app.services.image_gen import get_active_image_engine
engine = await get_active_image_engine(db)
engine = await get_image_engine(db, selected_engine_id)
image_size = req.image_size or record.image_size or engine.default_size or "2K"
billing = await charge_generation_media_by_params(
db, user_id=current_user.id, record_id=record.id, gen_type="image",
image_size=image_size, engine_id=engine.id, project_name=project_name,
description_prefix=project_name + "-", owner_type=OWNER_GENERATION_RECORD, attempt_no=attempt_no,
)
_validate_image_engine_selection(engine, image_size=image_size)
record.image_size = image_size
record.provider_generation_resolution = None
record.video_upscale_enabled_snapshot = False
record.video_upscale_snapshot_json = None
record.credits_cost = round(float(record.credits_cost or 0) + float(billing.total_charged or 0), 2)
reference_usage = calculate_media_reference_usage(
record.media_references,
include=bool(record.include_media_references),
)
validate_media_reference_usage_for_engine(
reference_usage,
gen_type=record.gen_type,
engine=engine,
)
billing = await charge_generation_media_for_record(
db,
record=record,
project_name=project_name,
description_prefix=project_name + "-",
attempt_no=attempt_no,
engine_id=engine.id,
)
record.credits_cost = round(
float(record.credits_cost or 0) + float(billing.total_charged or 0), 2
)
prepare_generation_record_execution(record, engine=engine, attempt_no=attempt_no)
await db.flush()
await commit_and_enqueue_generation_record(db, record, reason="generation_record_api_generate")
record_id_snapshot = str(record.id)
await commit_and_enqueue_generation_record(
db, record, reason="generation_record_api_generate"
)
refreshed = await db.execute(
select(GenerationRecord, Project.name)
.join(Project, GenerationRecord.project_id == Project.id)
.where(GenerationRecord.id == record_id_snapshot)
.limit(1)
)
refreshed_row = refreshed.first()
if not refreshed_row:
raise RecordNotFoundError()
record, project_name = refreshed_row
refs = await resolve_private_portrait_reference_display_urls(
db, json.loads(record.media_references) if record.media_references else None, user_id=current_user.id
db,
json.loads(record.media_references) if record.media_references else None,
user_id=current_user.id,
)
return _record_to_out(record, project_name, refs_override=refs)
@@ -529,43 +604,82 @@ async def retry_generation(
raise InvalidStatusError("该任务生成失败,请联系客服进行修复")
await assert_user_resource_capacity_available(db, current_user.id)
attempt_no = await get_next_credit_attempt_no(db, owner_type=OWNER_GENERATION_RECORD, owner_id=record.id)
attempt_no = await get_next_credit_attempt_no(
db, owner_type=OWNER_GENERATION_RECORD, owner_id=record.id
)
from app.services.generation.pipeline.generation_record_service import (
commit_and_enqueue_generation_record,
prepare_generation_record_execution,
)
if record.gen_type == GenerationType.video:
from app.services.video_gen import get_active_engine
engine = await get_video_engine(db, record.engine_id)
_validate_video_engine_selection(
engine,
aspect_ratio=record.aspect_ratio or "16:9",
resolution=record.resolution or "480p",
duration=int(record.duration or 5),
)
from app.services.video_upscale.snapshot_service import build_video_upscale_snapshot
engine = await get_active_engine(db)
try:
supported_provider_resolutions = json.loads(engine.supported_resolutions or "[]")
except (TypeError, json.JSONDecodeError):
supported_provider_resolutions = []
provider_resolution, upscale_enabled, upscale_snapshot_json = await build_video_upscale_snapshot(
db, target_resolution=record.resolution or "480p", aspect_ratio=record.aspect_ratio or "16:9",
supported_provider_resolutions=supported_provider_resolutions,
db,
target_resolution=record.resolution or "480p",
aspect_ratio=record.aspect_ratio or "16:9",
supported_provider_resolutions=parse_json_list(engine.supported_resolutions, []),
)
record.provider_generation_resolution = provider_resolution
record.video_upscale_enabled_snapshot = upscale_enabled
record.video_upscale_snapshot_json = upscale_snapshot_json
else:
from app.services.image_gen import get_active_image_engine
engine = await get_active_image_engine(db)
engine = await get_image_engine(db, record.engine_id)
_validate_image_engine_selection(
engine, image_size=record.image_size or engine.default_size or "2K"
)
reference_usage = calculate_media_reference_usage(
record.media_references,
include=bool(record.include_media_references),
)
validate_media_reference_usage_for_engine(
reference_usage,
gen_type=record.gen_type,
engine=engine,
)
billing = await charge_generation_media_for_record(
db, record=record, project_name=project_name, description_prefix="资源生成重试-", attempt_no=attempt_no, engine_id=engine.id
db,
record=record,
project_name=project_name,
description_prefix="资源生成重试-",
attempt_no=attempt_no,
engine_id=engine.id,
)
record.credits_cost = round(
float(record.credits_cost or 0) + float(billing.total_charged or 0), 2
)
record.credits_cost = round(float(record.credits_cost or 0) + float(billing.total_charged or 0), 2)
record.manual_retry_count = int(record.manual_retry_count or 0) + 1
record.retry_count = int(record.manual_retry_count or 0)
prepare_generation_record_execution(record, engine=engine, attempt_no=attempt_no)
await db.flush()
await commit_and_enqueue_generation_record(db, record, reason="generation_record_api_retry")
record_id_snapshot = str(record.id)
await commit_and_enqueue_generation_record(
db, record, reason="generation_record_api_retry"
)
refreshed = await db.execute(
select(GenerationRecord, Project.name)
.join(Project, GenerationRecord.project_id == Project.id)
.where(GenerationRecord.id == record_id_snapshot)
.limit(1)
)
refreshed_row = refreshed.first()
if not refreshed_row:
raise RecordNotFoundError()
record, project_name = refreshed_row
refs = await resolve_private_portrait_reference_display_urls(
db, json.loads(record.media_references) if record.media_references else None, user_id=current_user.id
db,
json.loads(record.media_references) if record.media_references else None,
user_id=current_user.id,
)
return _record_to_out(record, project_name, refs_override=refs)