feat: add week 3 audio and timeline enhancements
This commit is contained in:
@@ -2,11 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from app.core.config import settings
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AudioCacheMetadata:
|
||||
"""Metadata about one cached story audio file."""
|
||||
|
||||
exists: bool
|
||||
path: str | None = None
|
||||
size_bytes: int | None = None
|
||||
updated_at: datetime | None = None
|
||||
|
||||
|
||||
def build_story_audio_path(story_id: int) -> str:
|
||||
"""Build the cache path for a story audio file."""
|
||||
|
||||
@@ -25,6 +37,39 @@ def read_audio_cache(audio_path: str) -> bytes:
|
||||
return Path(audio_path).read_bytes()
|
||||
|
||||
|
||||
def get_audio_cache_metadata(audio_path: str | None) -> AudioCacheMetadata:
|
||||
"""Return cache metadata without reading the audio bytes."""
|
||||
|
||||
if not audio_path:
|
||||
return AudioCacheMetadata(exists=False)
|
||||
|
||||
path = Path(audio_path)
|
||||
if not path.is_file():
|
||||
return AudioCacheMetadata(exists=False, path=str(path))
|
||||
|
||||
stat = path.stat()
|
||||
return AudioCacheMetadata(
|
||||
exists=True,
|
||||
path=str(path),
|
||||
size_bytes=stat.st_size,
|
||||
updated_at=datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc),
|
||||
)
|
||||
|
||||
|
||||
def delete_audio_cache(audio_path: str | None) -> bool:
|
||||
"""Delete cached story audio if it exists."""
|
||||
|
||||
if not audio_path:
|
||||
return False
|
||||
|
||||
path = Path(audio_path)
|
||||
if not path.is_file():
|
||||
return False
|
||||
|
||||
path.unlink()
|
||||
return True
|
||||
|
||||
|
||||
def write_story_audio_cache(story_id: int, audio_data: bytes) -> str:
|
||||
"""Persist story audio and return the saved file path."""
|
||||
|
||||
|
||||
@@ -25,6 +25,8 @@ from app.services.adapters.storybook.primary import Storybook
|
||||
from app.services.adapters.text.models import StoryOutput
|
||||
from app.services.audio_storage import (
|
||||
audio_cache_exists,
|
||||
delete_audio_cache,
|
||||
get_audio_cache_metadata,
|
||||
read_audio_cache,
|
||||
write_story_audio_cache,
|
||||
)
|
||||
@@ -1536,9 +1538,66 @@ async def generate_story_audio(
|
||||
raise
|
||||
|
||||
raise HTTPException(status_code=500, detail="Audio generation failed")
|
||||
|
||||
|
||||
async def get_story_achievements(
|
||||
|
||||
|
||||
async def get_story_audio_status(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
"""Return audio cache status without generating audio."""
|
||||
|
||||
story = await get_story_detail(story_id, user_id, db)
|
||||
metadata = get_audio_cache_metadata(story.audio_path)
|
||||
|
||||
if story.audio_path and not metadata.exists:
|
||||
logger.warning(
|
||||
"story_audio_cache_missing_on_status",
|
||||
story_id=story.id,
|
||||
audio_path=story.audio_path,
|
||||
)
|
||||
story.audio_path = None
|
||||
if story.audio_status == StoryAssetStatus.READY.value:
|
||||
sync_story_status(story, audio_status=StoryAssetStatus.NOT_REQUESTED)
|
||||
await db.commit()
|
||||
metadata = get_audio_cache_metadata(story.audio_path)
|
||||
|
||||
return {
|
||||
"story_id": story.id,
|
||||
"audio_ready": story.audio_status == StoryAssetStatus.READY.value
|
||||
and metadata.exists,
|
||||
"cache_exists": metadata.exists,
|
||||
"cache_size_bytes": metadata.size_bytes,
|
||||
"cache_updated_at": metadata.updated_at,
|
||||
"generation_status": story.generation_status,
|
||||
"text_status": story.text_status,
|
||||
"image_status": story.image_status,
|
||||
"audio_status": story.audio_status,
|
||||
"last_error": story.last_error,
|
||||
"retryable_assets": story.retryable_assets,
|
||||
}
|
||||
|
||||
|
||||
async def clear_story_audio_cache(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
"""Delete cached audio and mark audio as retryable again."""
|
||||
|
||||
story = await get_story_detail(story_id, user_id, db)
|
||||
delete_audio_cache(story.audio_path)
|
||||
story.audio_path = None
|
||||
sync_story_status(
|
||||
story,
|
||||
audio_status=StoryAssetStatus.NOT_REQUESTED,
|
||||
last_error=None,
|
||||
)
|
||||
await db.commit()
|
||||
return await get_story_audio_status(story_id, user_id, db)
|
||||
|
||||
|
||||
async def get_story_achievements(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
db: AsyncSession,
|
||||
|
||||
Reference in New Issue
Block a user