feat: add unified asset retry endpoint
Some checks failed
Build and Push Docker Images / changes (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-admin-frontend (push) Has been cancelled
Some checks failed
Build and Push Docker Images / changes (push) Has been cancelled
Build and Push Docker Images / build-backend (push) Has been cancelled
Build and Push Docker Images / build-frontend (push) Has been cancelled
Build and Push Docker Images / build-admin-frontend (push) Has been cancelled
This commit is contained in:
@@ -1,19 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { api } from '../api/client'
|
||||
import BaseButton from '../components/ui/BaseButton.vue'
|
||||
import LoginDialog from '../components/ui/LoginDialog.vue'
|
||||
import {
|
||||
SparklesIcon,
|
||||
ArrowRightIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
UserGroupIcon
|
||||
} from '@heroicons/vue/24/outline'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const { locale } = useI18n()
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
|
||||
@@ -43,11 +40,6 @@ function scrollToFeatures() {
|
||||
document.getElementById('features')?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
// ========== 统计数据 (静态模拟) ==========
|
||||
const storiesCount = 10000
|
||||
const familiesCount = 5000
|
||||
const satisfactionCount = 99
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -5,8 +5,8 @@ import uuid
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import APIRouter, Depends, Response
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sse_starlette.sse import EventSourceResponse
|
||||
|
||||
from app.core.deps import require_user
|
||||
from app.core.logging import get_logger
|
||||
@@ -17,18 +17,19 @@ from app.schemas.story_schemas import (
|
||||
AchievementItem,
|
||||
FullStoryResponse,
|
||||
GenerateRequest,
|
||||
StoryAssetRetryRequest,
|
||||
StorybookRequest,
|
||||
StorybookResponse,
|
||||
StoryDetailResponse,
|
||||
StoryImageResponse,
|
||||
StoryListItem,
|
||||
StoryResponse,
|
||||
StorybookRequest,
|
||||
StorybookResponse,
|
||||
)
|
||||
from app.services import story_service
|
||||
from app.services.memory_service import build_enhanced_memory_context
|
||||
from app.services.provider_router import (
|
||||
generate_story_content,
|
||||
generate_image,
|
||||
generate_story_content,
|
||||
)
|
||||
from app.services.story_status import StoryAssetStatus, sync_story_status
|
||||
|
||||
@@ -243,6 +244,17 @@ async def generate_story_image(
|
||||
}
|
||||
|
||||
|
||||
@router.post("/stories/{story_id}/assets/retry", response_model=StoryDetailResponse)
|
||||
async def retry_story_assets(
|
||||
story_id: int,
|
||||
payload: StoryAssetRetryRequest,
|
||||
user: User = Depends(require_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""Retry selected generated assets for a story."""
|
||||
return await story_service.retry_story_assets(story_id, user.id, payload.assets, db)
|
||||
|
||||
|
||||
@router.get("/audio/{story_id}")
|
||||
async def get_story_audio(
|
||||
story_id: int,
|
||||
|
||||
@@ -5,7 +5,6 @@ from typing import Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
MAX_DATA_LENGTH = 2000
|
||||
MAX_EDU_THEME_LENGTH = 200
|
||||
MAX_TTS_LENGTH = 4000
|
||||
@@ -120,6 +119,12 @@ class StoryImageResponse(StoryStatusMixin):
|
||||
image_url: str | None
|
||||
|
||||
|
||||
class StoryAssetRetryRequest(BaseModel):
|
||||
"""Retry selected generated assets for a story."""
|
||||
|
||||
assets: list[Literal["image", "audio"]] = Field(..., min_length=1)
|
||||
|
||||
|
||||
class AchievementItem(BaseModel):
|
||||
"""Achievement item returned for a story."""
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ from sqlalchemy.orm import joinedload
|
||||
from app.core.logging import get_logger
|
||||
from app.db.models import ChildProfile, Story, StoryUniverse
|
||||
from app.schemas.story_schemas import (
|
||||
GenerateRequest,
|
||||
StorybookRequest,
|
||||
FullStoryResponse,
|
||||
StorybookResponse,
|
||||
StorybookPageResponse,
|
||||
AchievementItem,
|
||||
FullStoryResponse,
|
||||
GenerateRequest,
|
||||
StorybookPageResponse,
|
||||
StorybookRequest,
|
||||
StorybookResponse,
|
||||
)
|
||||
from app.services.audio_storage import (
|
||||
audio_cache_exists,
|
||||
@@ -24,8 +24,8 @@ from app.services.audio_storage import (
|
||||
)
|
||||
from app.services.memory_service import build_enhanced_memory_context
|
||||
from app.services.provider_router import (
|
||||
generate_story_content,
|
||||
generate_image,
|
||||
generate_story_content,
|
||||
generate_storybook,
|
||||
)
|
||||
from app.services.story_status import (
|
||||
@@ -154,7 +154,10 @@ async def generate_and_save_story(
|
||||
db=db,
|
||||
)
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=502, detail="Story generation failed, please try again.") from exc
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail="Story generation failed, please try again.",
|
||||
) from exc
|
||||
|
||||
# 4. Save
|
||||
story = Story(
|
||||
@@ -462,6 +465,125 @@ async def create_story_from_result(
|
||||
return story
|
||||
|
||||
|
||||
async def _retry_cover_image_asset(story: Story, db: AsyncSession) -> None:
|
||||
"""Retry cover generation for a text story."""
|
||||
|
||||
if not story.cover_prompt:
|
||||
raise HTTPException(status_code=400, detail="Story has no cover prompt")
|
||||
|
||||
sync_story_status(story, image_status=StoryAssetStatus.GENERATING)
|
||||
await db.commit()
|
||||
|
||||
try:
|
||||
story.image_url = await generate_image(story.cover_prompt, db=db)
|
||||
sync_story_status(story, image_status=StoryAssetStatus.READY)
|
||||
except Exception as exc:
|
||||
sync_story_status(
|
||||
story,
|
||||
image_status=StoryAssetStatus.FAILED,
|
||||
last_error=f"封面生成失败: {exc}",
|
||||
)
|
||||
logger.error("cover_asset_retry_failed", story_id=story.id, error=str(exc))
|
||||
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def _retry_storybook_image_assets(story: Story, db: AsyncSession) -> None:
|
||||
"""Retry missing storybook cover/page images."""
|
||||
|
||||
pages_data = [dict(page) for page in story.pages or [] if isinstance(page, dict)]
|
||||
has_image_prompt = bool(story.cover_prompt) or any(
|
||||
page.get("image_prompt") for page in pages_data
|
||||
)
|
||||
if not has_image_prompt:
|
||||
raise HTTPException(status_code=400, detail="Storybook has no image prompts")
|
||||
|
||||
sync_story_status(story, image_status=StoryAssetStatus.GENERATING)
|
||||
await db.commit()
|
||||
|
||||
cover_failed = False
|
||||
failed_pages: list[int] = []
|
||||
|
||||
if story.cover_prompt and not story.image_url:
|
||||
try:
|
||||
story.image_url = await generate_image(story.cover_prompt, db=db)
|
||||
except Exception as exc:
|
||||
cover_failed = True
|
||||
logger.warning(
|
||||
"storybook_cover_asset_retry_failed",
|
||||
story_id=story.id,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
for page in pages_data:
|
||||
if not page.get("image_prompt") or page.get("image_url"):
|
||||
continue
|
||||
|
||||
try:
|
||||
page["image_url"] = await generate_image(page["image_prompt"], db=db)
|
||||
except Exception as exc:
|
||||
page_number = page.get("page_number")
|
||||
if isinstance(page_number, int):
|
||||
failed_pages.append(page_number)
|
||||
logger.warning(
|
||||
"storybook_page_asset_retry_failed",
|
||||
story_id=story.id,
|
||||
page=page_number,
|
||||
error=str(exc),
|
||||
)
|
||||
|
||||
story.pages = pages_data
|
||||
sync_story_status(
|
||||
story,
|
||||
image_status=_resolve_storybook_image_status(
|
||||
generate_images=True,
|
||||
cover_prompt=story.cover_prompt,
|
||||
cover_url=story.image_url,
|
||||
pages_data=pages_data,
|
||||
),
|
||||
last_error=_build_storybook_error_message(
|
||||
cover_failed=cover_failed,
|
||||
failed_pages=failed_pages,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def _retry_audio_asset(story_id: int, user_id: str, db: AsyncSession) -> None:
|
||||
"""Retry audio generation while preserving persisted status on provider failure."""
|
||||
|
||||
try:
|
||||
await generate_story_audio(story_id, user_id, db)
|
||||
except HTTPException as exc:
|
||||
if exc.status_code >= 500:
|
||||
logger.warning("audio_asset_retry_failed", story_id=story_id, error=exc.detail)
|
||||
return
|
||||
raise
|
||||
|
||||
|
||||
async def retry_story_assets(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
assets: list[str],
|
||||
db: AsyncSession,
|
||||
) -> Story:
|
||||
"""Retry selected assets through one workflow-level endpoint."""
|
||||
|
||||
story = await get_story_detail(story_id, user_id, db)
|
||||
requested_assets = list(dict.fromkeys(assets))
|
||||
|
||||
if "image" in requested_assets:
|
||||
if story.mode == "storybook":
|
||||
await _retry_storybook_image_assets(story, db)
|
||||
else:
|
||||
await _retry_cover_image_asset(story, db)
|
||||
|
||||
if "audio" in requested_assets:
|
||||
await _retry_audio_asset(story_id, user_id, db)
|
||||
|
||||
return await get_story_detail(story_id, user_id, db)
|
||||
|
||||
|
||||
async def generate_story_cover(
|
||||
story_id: int,
|
||||
user_id: str,
|
||||
|
||||
@@ -264,7 +264,10 @@ class TestAudio:
|
||||
mock_tts_provider.assert_awaited_once()
|
||||
|
||||
def test_get_audio_failure_updates_status(self, auth_client: TestClient, test_story):
|
||||
with patch("app.services.provider_router.text_to_speech", new_callable=AsyncMock) as mock_tts:
|
||||
with patch(
|
||||
"app.services.provider_router.text_to_speech",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_tts:
|
||||
mock_tts.side_effect = Exception("TTS provider timeout")
|
||||
response = auth_client.get(f"/api/audio/{test_story.id}")
|
||||
assert response.status_code == 500
|
||||
@@ -383,12 +386,83 @@ class TestImageGenerateSuccess:
|
||||
assert data["last_error"] is None
|
||||
|
||||
|
||||
class TestAssetRetry:
|
||||
"""Tests for unified asset retry endpoint."""
|
||||
|
||||
def test_retry_cover_image_success(
|
||||
self,
|
||||
auth_client: TestClient,
|
||||
degraded_story_with_text,
|
||||
mock_image_provider,
|
||||
):
|
||||
response = auth_client.post(
|
||||
f"/api/stories/{degraded_story_with_text.id}/assets/retry",
|
||||
json={"assets": ["image"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["image_url"] == "https://example.com/image.png"
|
||||
assert data["generation_status"] == "completed"
|
||||
assert data["image_status"] == "ready"
|
||||
assert data["audio_status"] == "not_requested"
|
||||
assert data["last_error"] is None
|
||||
mock_image_provider.assert_awaited_once()
|
||||
|
||||
def test_retry_storybook_missing_page_image_success(
|
||||
self,
|
||||
auth_client: TestClient,
|
||||
storybook_story,
|
||||
):
|
||||
async def image_side_effect(prompt: str, **kwargs):
|
||||
return "https://example.com/retried-page.png"
|
||||
|
||||
with patch(
|
||||
"app.services.story_service.generate_image",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_image:
|
||||
mock_image.side_effect = image_side_effect
|
||||
|
||||
response = auth_client.post(
|
||||
f"/api/stories/{storybook_story.id}/assets/retry",
|
||||
json={"assets": ["image"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["generation_status"] == "completed"
|
||||
assert data["image_status"] == "ready"
|
||||
assert data["audio_status"] == "not_requested"
|
||||
assert data["last_error"] is None
|
||||
assert data["pages"][1]["image_url"] == "https://example.com/retried-page.png"
|
||||
mock_image.assert_awaited_once()
|
||||
|
||||
def test_retry_audio_on_storybook_is_rejected(
|
||||
self,
|
||||
auth_client: TestClient,
|
||||
storybook_story,
|
||||
):
|
||||
response = auth_client.post(
|
||||
f"/api/stories/{storybook_story.id}/assets/retry",
|
||||
json={"assets": ["audio"]},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert response.json()["detail"] == "Story has no text"
|
||||
|
||||
|
||||
class TestStorybookGenerate:
|
||||
"""Tests for storybook generation status handling."""
|
||||
|
||||
def test_generate_storybook_success(self, auth_client: TestClient):
|
||||
with patch("app.services.story_service.generate_storybook", new_callable=AsyncMock) as mock_storybook:
|
||||
with patch("app.services.story_service.generate_image", new_callable=AsyncMock) as mock_image:
|
||||
with patch(
|
||||
"app.services.story_service.generate_storybook",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_storybook:
|
||||
with patch(
|
||||
"app.services.story_service.generate_image",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_image:
|
||||
mock_storybook.return_value = build_storybook_output()
|
||||
mock_image.side_effect = [
|
||||
"https://example.com/storybook-cover.png",
|
||||
@@ -422,8 +496,14 @@ class TestStorybookGenerate:
|
||||
slug = prompt.split()[0].lower()
|
||||
return f"https://example.com/{slug}.png"
|
||||
|
||||
with patch("app.services.story_service.generate_storybook", new_callable=AsyncMock) as mock_storybook:
|
||||
with patch("app.services.story_service.generate_image", new_callable=AsyncMock) as mock_image:
|
||||
with patch(
|
||||
"app.services.story_service.generate_storybook",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_storybook:
|
||||
with patch(
|
||||
"app.services.story_service.generate_image",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_image:
|
||||
mock_storybook.return_value = build_storybook_output()
|
||||
mock_image.side_effect = image_side_effect
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
## 2.1 Current Progress Snapshot
|
||||
|
||||
**Updated**: 2026-04-17 evening
|
||||
**Updated**: 2026-04-18 morning
|
||||
|
||||
### What Has Been Completed
|
||||
|
||||
@@ -54,35 +54,43 @@
|
||||
- `0009_add_story_generation_statuses.py`
|
||||
- `0010_add_story_audio_cache_path.py`
|
||||
- 已完成一轮后端回归验证:`backend/` 下 `pytest -q` 结果为 `53 passed`
|
||||
- 已修复 admin-frontend 构建阻塞,主前端与管理端前端均可生产构建
|
||||
- 已落地首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
||||
|
||||
### What Is In Progress
|
||||
|
||||
- 统一状态模型已落地,但统一 service workflow 仍未真正收束成单一路径
|
||||
- 普通故事、完整生成、绘本生成仍存在多条 service / API 路径
|
||||
- 资产补全虽然已经支持图片与音频状态,但“统一重试入口”尚未实现
|
||||
- 资产补全已经具备统一重试入口首版,但仍需要继续抽象统一补全过程和 generation job 边界
|
||||
|
||||
### What Is Still Pending
|
||||
|
||||
- admin-frontend 的处理决策与演示范围收敛
|
||||
- Provider 的 Capability / Provider / Routing Policy 边界整理
|
||||
- Week 2 可直接执行的开发任务表
|
||||
- 演示 checklist 与最终收尾策略
|
||||
|
||||
### Remote Checkpoint Scope
|
||||
|
||||
当前远端已同步一个阶段性 checkpoint:
|
||||
当前远端已同步到 2026-04-17 晚间 WIP 快照:
|
||||
|
||||
- Commit: `a97a2fe`
|
||||
- Message: `feat: persist story generation states and cache audio`
|
||||
- Commit: `b8d3cb4`
|
||||
- Message: `wip: snapshot full local workspace state`
|
||||
|
||||
这个 checkpoint **不是今天下午所有本地修改的全集**。它只覆盖以下主线:
|
||||
这个 checkpoint 包含 `a97a2fe` 的统一状态模型主线,也包含后续文档治理和接手机制整理:
|
||||
|
||||
- 统一生成状态模型
|
||||
- Storybook 按 ID 恢复
|
||||
- 故事列表/详情/绘本页状态展示
|
||||
- 音频缓存与状态语义修正
|
||||
- `docs/` 信息架构整理
|
||||
- 求职版 PRD、统一生成工作流 PRD、Week 1 backlog、文档状态盘点
|
||||
- `AGENTS.md` 接手说明
|
||||
|
||||
当前工作区里仍存在其他未提交、本机独有的改动,周末换电脑后不会自动带过去。
|
||||
2026-04-18 在个人电脑接手时已确认:
|
||||
|
||||
- 本地 `main` 与 `origin/main` 对齐
|
||||
- 工作树起始状态干净
|
||||
- Gitea HTTPS 推送凭据已配置并通过 dry-run 验证
|
||||
|
||||
---
|
||||
|
||||
@@ -171,7 +179,7 @@
|
||||
| W1-05 | Product / Backend | 定义统一工作流下的 API / 数据结构影响 | 接口与模型变更清单 | P0 | 0.5d | In Progress |
|
||||
| W1-06 | Product / Backend | 梳理 Provider 概念层:Capability / Provider / Routing Policy | 分层图与术语表 | P1 | 0.5d | Pending |
|
||||
| W1-07 | Product / Frontend | 梳理 Storybook 当前问题与恢复方案 | 恢复方案说明 | P0 | 0.5d | Done |
|
||||
| W1-08 | Product / Frontend | 确认 admin 前端是修复、裁剪还是暂时降级 | 决策记录 | P0 | 0.5d | Pending |
|
||||
| W1-08 | Product / Frontend | 确认 admin 前端是修复、裁剪还是暂时降级 | 决策记录 | P0 | 0.5d | Done |
|
||||
| W1-09 | Planning | 产出 Week 2 开发任务清单 | 下周 backlog | P1 | 0.5d | In Progress |
|
||||
| W1-10 | Review | 形成求职演示版检查清单 | 演示清单 | P1 | 0.5d | Pending |
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## What Is Already On Remote
|
||||
|
||||
当前远端已经包含一个阶段性 checkpoint:
|
||||
当前远端 `main` 已经包含两个连续 checkpoint:
|
||||
|
||||
- Commit: `a97a2fe`
|
||||
- Message: `feat: persist story generation states and cache audio`
|
||||
@@ -25,16 +25,30 @@
|
||||
- 音频首次生成后缓存落盘并可复用
|
||||
- 统一状态语义中 `degraded_completed` 已和错误展示保持一致
|
||||
|
||||
- Commit: `b8d3cb4`
|
||||
- Message: `wip: snapshot full local workspace state`
|
||||
|
||||
这个 checkpoint 已把 2026-04-17 晚间的工作区快照同步到远端,包括:
|
||||
|
||||
- 新增 `AGENTS.md`
|
||||
- 整理 `docs/` 文档信息架构
|
||||
- 新增求职版 PRD、统一生成工作流 PRD、Week 1 backlog 与文档盘点
|
||||
- 归档旧 backend docs 到 `docs/archive/`、`docs/technical/`、`docs/operations/`
|
||||
- 补齐 Storybook 带 ID 路由恢复相关前端改动
|
||||
|
||||
注意:`b8d3cb4` 是一次 WIP 快照提交,原始 diff 中包含大量行尾/格式噪音。继续开发时应以当前 `main` 代码与 `docs/` 中 Active 文档为准,不需要再回到 `a97a2fe` 重新整理。
|
||||
|
||||
---
|
||||
|
||||
## What Is Not Yet On Remote
|
||||
## Current Local Status On 2026-04-18
|
||||
|
||||
当前这台机器的工作区里仍存在大量未提交改动,它们 **不属于上面的 checkpoint**,也不会自动出现在另一台电脑上。
|
||||
2026-04-18 在个人电脑接手后已确认:
|
||||
|
||||
因此,周末接手时应该默认:
|
||||
|
||||
- 远端 `main` 只包含“统一状态模型 + Storybook 恢复 + 音频缓存”这一条主线
|
||||
- 其他本机未提交内容需要后续再整理,不应假设它们已经同步
|
||||
- 本地 `main` 与 `origin/main` 对齐到 `b8d3cb4`
|
||||
- 工作树起始状态干净
|
||||
- 已配置 Gitea HTTPS 推送凭据,并通过 `git push --dry-run origin HEAD:main`
|
||||
- 后端本地虚拟环境可用:`backend/.venv`
|
||||
- 主前端与管理端依赖已安装到各自 `node_modules`
|
||||
|
||||
---
|
||||
|
||||
@@ -55,7 +69,7 @@
|
||||
|
||||
1. `git pull`
|
||||
2. `cd backend && alembic upgrade head`
|
||||
3. `cd backend && ./.venv/Scripts/python.exe -m pytest -q`
|
||||
3. `cd backend && .venv/bin/python -m pytest -q`(macOS/Linux)
|
||||
4. `cd frontend && npm install`
|
||||
5. `cd frontend && ./node_modules/.bin/vue-tsc --noEmit`
|
||||
|
||||
@@ -120,6 +134,6 @@
|
||||
|
||||
如果周末是在另一台电脑上继续,不要默认“今天下午所有本地修改”都已经上远端。当前最可靠的 source of truth 是:
|
||||
|
||||
- 远端代码:以 commit `a97a2fe` 为准
|
||||
- 远端代码:以 commit `b8d3cb4` 为准
|
||||
- 产品目标:以 `docs/product/job-search-relaunch-prd.md` 为准
|
||||
- 当前执行主线:以 `docs/product/unified-generation-workflow-prd.md` 与 `docs/planning/week-1-execution-backlog.md` 为准
|
||||
|
||||
@@ -17,7 +17,7 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
|
||||
|
||||
## Implementation Snapshot
|
||||
|
||||
**Updated**: 2026-04-17 evening
|
||||
**Updated**: 2026-04-18 morning
|
||||
|
||||
当前代码已经从“纯目标态设计”进入“部分落地”阶段,主要进展如下:
|
||||
|
||||
@@ -33,12 +33,15 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
|
||||
- 故事列表页、故事详情页、绘本阅读页已接入统一状态展示
|
||||
- 故事音频已支持首次生成后缓存复用
|
||||
- `degraded_completed` 已在服务层和前端语义中落地
|
||||
- 已新增首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
||||
- 故事详情页封面补全已切换到统一资产重试入口
|
||||
- 管理端前端构建阻塞已修复,主前端与 admin 前端均可完成生产构建
|
||||
|
||||
### Still Missing
|
||||
|
||||
- 统一的 `POST /api/generations` 风格入口尚未建立
|
||||
- 普通故事、完整生成、绘本生成仍通过多条 service 路径实现
|
||||
- “统一资产重试入口”尚未落地
|
||||
- 统一资产重试入口仍是首版:已覆盖普通故事封面、绘本缺失插图、故事音频,但尚未抽象成完整 generation job 模型
|
||||
- `partial_ready`、`retryable_assets` 等更细粒度状态仍停留在目标态
|
||||
|
||||
### What This Means
|
||||
|
||||
@@ -34,14 +34,6 @@ interface Story {
|
||||
}> | null
|
||||
}
|
||||
|
||||
interface ImageGenerationResponse {
|
||||
image_url: string | null
|
||||
generation_status: string
|
||||
image_status: string
|
||||
audio_status: string
|
||||
last_error: string | null
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
@@ -93,12 +85,9 @@ async function generateImage() {
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const result = await api.post<ImageGenerationResponse>(`/api/image/generate/${story.value.id}`)
|
||||
story.value.image_url = result.image_url
|
||||
story.value.generation_status = result.generation_status
|
||||
story.value.image_status = result.image_status
|
||||
story.value.audio_status = result.audio_status
|
||||
story.value.last_error = result.last_error
|
||||
story.value = await api.post<Story>(`/api/stories/${story.value.id}/assets/retry`, {
|
||||
assets: ['image'],
|
||||
})
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '图片生成失败'
|
||||
await refreshStorySnapshot().catch(() => undefined)
|
||||
|
||||
Reference in New Issue
Block a user