feat: improve generation analytics and maintenance

This commit is contained in:
2026-04-19 09:03:40 +08:00
parent d5a173aa0d
commit 5318de670f
21 changed files with 1155 additions and 57 deletions

View File

@@ -2,6 +2,7 @@
import asyncio
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from typing import Literal
from fastapi import HTTPException
@@ -9,6 +10,7 @@ from sqlalchemy import desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from app.core.config import settings
from app.core.logging import get_logger
from app.db.models import ChildProfile, Story, StoryUniverse
from app.schemas.story_schemas import (
@@ -32,6 +34,7 @@ from app.services.audio_storage import (
)
from app.services.generation_jobs import (
create_generation_job,
ensure_no_active_story_generation_job,
finish_generation_job,
record_generation_event,
)
@@ -1369,6 +1372,7 @@ async def retry_story_assets(
db: AsyncSession,
) -> Story:
"""Retry selected assets through one workflow-level endpoint."""
await ensure_no_active_story_generation_job(db, story_id=story_id, user_id=user_id)
requested_assets = list(dict.fromkeys(assets))
job = await create_generation_job(
db,
@@ -1443,6 +1447,7 @@ async def generate_story_cover(
db: AsyncSession,
) -> str:
"""Generate cover image for an existing story."""
await ensure_no_active_story_generation_job(db, story_id=story_id, user_id=user_id)
job = await create_generation_job(
db,
user_id=user_id,
@@ -1495,6 +1500,7 @@ async def generate_story_audio(
db: AsyncSession,
) -> bytes:
"""Generate audio for a story."""
await ensure_no_active_story_generation_job(db, story_id=story_id, user_id=user_id)
job = await create_generation_job(
db,
user_id=user_id,
@@ -1597,6 +1603,50 @@ async def clear_story_audio_cache(
return await get_story_audio_status(story_id, user_id, db)
async def prune_story_audio_cache(db: AsyncSession) -> dict[str, int]:
"""Prune expired audio cache files and repair story metadata."""
ttl_days = max(1, settings.story_audio_cache_ttl_days)
cutoff = datetime.now(timezone.utc) - timedelta(days=ttl_days)
result = await db.execute(select(Story).where(Story.audio_path.is_not(None)))
stories = result.scalars().all()
scanned = 0
pruned = 0
repaired = 0
for story in stories:
scanned += 1
metadata = get_audio_cache_metadata(story.audio_path)
if not metadata.exists:
story.audio_path = None
if story.audio_status == StoryAssetStatus.READY.value:
sync_story_status(story, audio_status=StoryAssetStatus.NOT_REQUESTED)
repaired += 1
continue
if metadata.updated_at and metadata.updated_at < cutoff:
delete_audio_cache(story.audio_path)
story.audio_path = None
sync_story_status(
story,
audio_status=StoryAssetStatus.NOT_REQUESTED,
last_error=None,
)
pruned += 1
await db.commit()
logger.info(
"story_audio_cache_pruned",
scanned=scanned,
pruned=pruned,
repaired=repaired,
ttl_days=ttl_days,
)
return {"scanned": scanned, "pruned": pruned, "repaired": repaired}
async def get_story_achievements(
story_id: int,
user_id: str,