feat: queue voice session cover generation jobs
This commit is contained in:
@@ -1241,13 +1241,16 @@ async def execute_generation_job_service(
|
||||
"""Execute one previously accepted generation job inside the worker."""
|
||||
|
||||
try:
|
||||
request = GenerationRequest.model_validate(job.request_payload or {})
|
||||
response = await _generate_generation_service_with_job(
|
||||
request,
|
||||
job.user_id,
|
||||
db,
|
||||
job=job,
|
||||
)
|
||||
if job.output_mode == "asset_generation":
|
||||
response = await _generate_asset_generation_service_with_job(job, db)
|
||||
else:
|
||||
request = GenerationRequest.model_validate(job.request_payload or {})
|
||||
response = await _generate_generation_service_with_job(
|
||||
request,
|
||||
job.user_id,
|
||||
db,
|
||||
job=job,
|
||||
)
|
||||
except GenerationJobCanceledError:
|
||||
return _build_canceled_generation_response(job)
|
||||
except HTTPException as exc:
|
||||
@@ -1294,6 +1297,39 @@ def _build_canceled_generation_response(job: GenerationJob) -> GenerationRespons
|
||||
)
|
||||
|
||||
|
||||
def _build_generation_response_from_story(
|
||||
story: Story,
|
||||
*,
|
||||
job_id: str,
|
||||
) -> GenerationResponse:
|
||||
"""Build a unified generation response from one persisted story record."""
|
||||
|
||||
pages = None
|
||||
if story.mode == "storybook":
|
||||
pages = _storybook_pages_to_response(story.pages or [])
|
||||
|
||||
return GenerationResponse(
|
||||
id=story.id,
|
||||
generation_job_id=job_id,
|
||||
title=story.title,
|
||||
mode=story.mode,
|
||||
story_text=story.story_text,
|
||||
pages=pages,
|
||||
cover_prompt=story.cover_prompt,
|
||||
image_url=story.image_url,
|
||||
cover_url=story.image_url,
|
||||
audio_ready=story.audio_status == StoryAssetStatus.READY.value,
|
||||
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,
|
||||
child_profile_id=story.child_profile_id,
|
||||
universe_id=story.universe_id,
|
||||
retryable_assets=story.retryable_assets,
|
||||
)
|
||||
|
||||
|
||||
async def run_generation_job_service(
|
||||
job_id: str,
|
||||
db: AsyncSession,
|
||||
@@ -1309,6 +1345,54 @@ async def run_generation_job_service(
|
||||
return job
|
||||
|
||||
|
||||
async def _generate_asset_generation_service_with_job(
|
||||
job: GenerationJob,
|
||||
db: AsyncSession,
|
||||
) -> GenerationResponse:
|
||||
"""Run queued asset generation in the background worker."""
|
||||
|
||||
payload = job.request_payload or {}
|
||||
story_id = payload.get("story_id") or job.story_id
|
||||
requested_assets = list(dict.fromkeys(payload.get("assets") or []))
|
||||
if story_id is None:
|
||||
raise HTTPException(status_code=400, detail="资源任务缺少 story_id。")
|
||||
if not requested_assets:
|
||||
raise HTTPException(status_code=400, detail="资源任务缺少 assets。")
|
||||
|
||||
story = await get_story_detail(int(story_id), job.user_id, db)
|
||||
|
||||
if "image" in requested_assets:
|
||||
if story.mode == "storybook":
|
||||
await _complete_storybook_image_assets(story, db, job=job)
|
||||
else:
|
||||
await _complete_cover_image_asset(
|
||||
story,
|
||||
db,
|
||||
raise_on_failure=True,
|
||||
log_event="cover_generation_failed",
|
||||
job=job,
|
||||
)
|
||||
|
||||
if "audio" in requested_assets:
|
||||
await _complete_audio_asset(
|
||||
story,
|
||||
db,
|
||||
raise_on_failure=True,
|
||||
job=job,
|
||||
)
|
||||
|
||||
story = await get_story_detail(story.id, job.user_id, db)
|
||||
await finish_generation_job(
|
||||
db,
|
||||
job=job,
|
||||
story=story,
|
||||
current_step="asset_generation_completed",
|
||||
message="Asset generation completed in the background worker.",
|
||||
metadata={"assets": requested_assets},
|
||||
)
|
||||
return _build_generation_response_from_story(story, job_id=job.id)
|
||||
|
||||
|
||||
async def retry_generation_job_service(
|
||||
job_id: str,
|
||||
user_id: str,
|
||||
@@ -1534,6 +1618,46 @@ async def create_story_from_result(
|
||||
)
|
||||
|
||||
|
||||
async def queue_story_asset_generation(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
assets: list[str],
|
||||
db: AsyncSession,
|
||||
) -> dict:
|
||||
"""Queue one asset generation job for an already persisted story."""
|
||||
|
||||
await ensure_no_active_story_generation_job(db, story_id=story_id, user_id=user_id)
|
||||
requested_assets = list(dict.fromkeys(assets))
|
||||
if not requested_assets:
|
||||
raise HTTPException(status_code=400, detail="至少需要一个待生成资源")
|
||||
|
||||
story = await get_story_detail(story_id, user_id, db)
|
||||
if "image" in requested_assets:
|
||||
has_image_prompt = bool(story.cover_prompt)
|
||||
if story.mode == "storybook":
|
||||
has_image_prompt = has_image_prompt or any(
|
||||
isinstance(page, dict) and page.get("image_prompt")
|
||||
for page in story.pages or []
|
||||
)
|
||||
if not has_image_prompt:
|
||||
raise HTTPException(status_code=400, detail="当前故事没有可生成的图片提示词")
|
||||
|
||||
if "audio" in requested_assets and not story.story_text:
|
||||
raise HTTPException(status_code=400, detail="当前故事没有可生成音频的正文")
|
||||
|
||||
job = await create_generation_job(
|
||||
db,
|
||||
user_id=user_id,
|
||||
output_mode="asset_generation",
|
||||
input_type=",".join(requested_assets),
|
||||
request_payload={"story_id": story_id, "assets": requested_assets},
|
||||
story_id=story_id,
|
||||
)
|
||||
await _dispatch_generation_job(db, job=job)
|
||||
await db.refresh(job)
|
||||
return generation_job_to_summary(job)
|
||||
|
||||
|
||||
async def _retry_cover_image_asset(story: Story, db: AsyncSession, *, job=None) -> None:
|
||||
"""Retry cover generation for a text story."""
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ from app.services.memory_service import build_enhanced_memory_context
|
||||
from app.services.provider_router import generate_story_content, text_to_speech
|
||||
from app.services.story_service import (
|
||||
create_story_from_result,
|
||||
generate_story_cover,
|
||||
queue_story_asset_generation,
|
||||
validate_profile_and_universe,
|
||||
)
|
||||
from app.services.voice_session_safety import (
|
||||
@@ -1710,15 +1710,21 @@ async def finalize_voice_session_service(
|
||||
generation_job_id: str | None = None
|
||||
if request.generate_cover and story.cover_prompt:
|
||||
try:
|
||||
await generate_story_cover(story.id, user_id, db)
|
||||
cover_job = await queue_story_asset_generation(
|
||||
story.id,
|
||||
user_id,
|
||||
["image"],
|
||||
db,
|
||||
)
|
||||
generation_job_id = str(cover_job["id"])
|
||||
await _record_session_event(
|
||||
db,
|
||||
session_id=session.id,
|
||||
turn_id=None,
|
||||
event_type="session_cover_generation_succeeded",
|
||||
event_type="session_cover_generation_queued",
|
||||
status="succeeded",
|
||||
message="Finalized story cover was generated after session save.",
|
||||
metadata={"story_id": story.id},
|
||||
message="Finalized story cover generation was queued after session save.",
|
||||
metadata={"story_id": story.id, "generation_job_id": generation_job_id},
|
||||
)
|
||||
except HTTPException as exc:
|
||||
await _record_session_event(
|
||||
@@ -1727,11 +1733,11 @@ async def finalize_voice_session_service(
|
||||
turn_id=None,
|
||||
event_type="session_cover_generation_failed",
|
||||
status="failed",
|
||||
message="Finalized story cover generation failed after session save.",
|
||||
message="Finalized story cover generation failed before the worker could start.",
|
||||
metadata={"story_id": story.id, "error": str(exc.detail)},
|
||||
)
|
||||
logger.warning(
|
||||
"voice_session_finalize_cover_failed",
|
||||
"voice_session_finalize_cover_queue_failed",
|
||||
session_id=session.id,
|
||||
story_id=story.id,
|
||||
error=str(exc.detail),
|
||||
|
||||
Reference in New Issue
Block a user