feat: add unified generation entrypoint
This commit is contained in:
@@ -137,9 +137,9 @@ See `backend/.env.example` for required variables:
|
|||||||
| GET | `/auth/{provider}/signin` | OAuth login |
|
| GET | `/auth/{provider}/signin` | OAuth login |
|
||||||
| GET | `/auth/dev/signin` | Local dev login |
|
| GET | `/auth/dev/signin` | Local dev login |
|
||||||
| GET | `/auth/session` | Get current user |
|
| GET | `/auth/session` | Get current user |
|
||||||
| POST | `/api/stories/generate/full` | Generate story + assets |
|
| POST | `/api/generations` | Unified story/storybook generation |
|
||||||
| POST | `/api/storybook/generate` | Generate storybook |
|
| GET | `/api/generations/{id}` | Get generated result |
|
||||||
| POST | `/api/stories/{id}/assets/retry` | Retry cover/audio assets |
|
| POST | `/api/generations/{id}/retry-assets` | Retry generated assets |
|
||||||
| GET | `/api/stories` | List stories (paginated) |
|
| GET | `/api/stories` | List stories (paginated) |
|
||||||
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
|
| GET/DELETE | `/api/stories/{id}` | Story CRUD |
|
||||||
| CRUD | `/api/profiles` | User profiles |
|
| CRUD | `/api/profiles` | User profiles |
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ cd ../admin-frontend
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
当前已知情况:完整后端测试可通过;全量 ruff 仍有少量历史 lint 债,优先处理核心演示链路与新增代码。
|
当前已知情况:完整后端测试可通过;全量 ruff 可通过。前端生产构建建议优先通过 Docker 验证,确保与本地演示环境一致。
|
||||||
|
|
||||||
## 核心接口
|
## 核心接口
|
||||||
|
|
||||||
@@ -124,9 +124,9 @@ npm run build
|
|||||||
| GET | `/auth/github/signin` | GitHub OAuth 登录 |
|
| GET | `/auth/github/signin` | GitHub OAuth 登录 |
|
||||||
| GET | `/auth/google/signin` | Google OAuth 登录 |
|
| GET | `/auth/google/signin` | Google OAuth 登录 |
|
||||||
| GET | `/auth/session` | 当前会话 |
|
| GET | `/auth/session` | 当前会话 |
|
||||||
| POST | `/api/stories/generate/full` | 生成故事并尝试生成封面 |
|
| POST | `/api/generations` | 统一生成故事或绘本 |
|
||||||
| POST | `/api/storybook/generate` | 生成绘本 |
|
| GET | `/api/generations/{story_id}` | 统一读取生成结果 |
|
||||||
| POST | `/api/stories/{story_id}/assets/retry` | 统一重试封面/语音资源 |
|
| POST | `/api/generations/{story_id}/retry-assets` | 统一重试封面/语音资源 |
|
||||||
| GET | `/api/stories` | 故事列表 |
|
| GET | `/api/stories` | 故事列表 |
|
||||||
| GET | `/api/stories/{story_id}` | 故事详情 |
|
| GET | `/api/stories/{story_id}` | 故事详情 |
|
||||||
| DELETE | `/api/stories/{story_id}` | 删除故事 |
|
| DELETE | `/api/stories/{story_id}` | 删除故事 |
|
||||||
|
|||||||
@@ -133,37 +133,35 @@ async function generateStory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload: Record<string, unknown> = {
|
const requestedOutputMode =
|
||||||
type: inputType.value,
|
inputType.value === 'full_story' ? 'story' : outputMode.value === 'storybook' ? 'storybook' : 'story'
|
||||||
data: inputData.value,
|
const payload: Record<string, unknown> = {
|
||||||
education_theme: educationTheme.value || undefined,
|
output_mode: requestedOutputMode,
|
||||||
}
|
type: inputType.value,
|
||||||
if (selectedProfileId.value) payload.child_profile_id = selectedProfileId.value
|
data: inputData.value,
|
||||||
if (selectedUniverseId.value) payload.universe_id = selectedUniverseId.value
|
education_theme: educationTheme.value || undefined,
|
||||||
|
generate_images: true,
|
||||||
if (outputMode.value === 'storybook') {
|
page_count: 6,
|
||||||
const response = await api.post<any>('/api/storybook/generate', {
|
}
|
||||||
keywords: inputData.value,
|
if (selectedProfileId.value) payload.child_profile_id = selectedProfileId.value
|
||||||
education_theme: educationTheme.value || undefined,
|
if (selectedUniverseId.value) payload.universe_id = selectedUniverseId.value
|
||||||
generate_images: true,
|
|
||||||
page_count: 6,
|
if (requestedOutputMode === 'storybook') {
|
||||||
child_profile_id: selectedProfileId.value || undefined,
|
const response = await api.post<any>('/api/generations', payload)
|
||||||
universe_id: selectedUniverseId.value || undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
storybookStore.setStorybook(response)
|
storybookStore.setStorybook(response)
|
||||||
close()
|
close()
|
||||||
const storybookPath = response.id ? `/storybook/view/${response.id}` : '/storybook/view'
|
const storybookPath = response.id ? `/storybook/view/${response.id}` : '/storybook/view'
|
||||||
router.push(storybookPath)
|
router.push(storybookPath)
|
||||||
} else {
|
} else {
|
||||||
const result = await api.post<any>('/api/stories/generate/full', payload)
|
const result = await api.post<any>('/api/generations', payload)
|
||||||
const query: Record<string, string> = {}
|
const query: Record<string, string> = {}
|
||||||
if (result.errors && Object.keys(result.errors).length > 0) {
|
if (result.errors && Object.keys(result.errors).length > 0) {
|
||||||
if (result.errors.image) query.imageError = '1'
|
if (result.errors.image) query.imageError = '1'
|
||||||
}
|
}
|
||||||
close()
|
close()
|
||||||
router.push({ path: `/story/${result.id}`, query })
|
router.push({ path: `/story/${result.id}`, query })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from app.schemas.story_schemas import (
|
|||||||
AchievementItem,
|
AchievementItem,
|
||||||
FullStoryResponse,
|
FullStoryResponse,
|
||||||
GenerateRequest,
|
GenerateRequest,
|
||||||
|
GenerationRequest,
|
||||||
|
GenerationResponse,
|
||||||
StoryAssetRetryRequest,
|
StoryAssetRetryRequest,
|
||||||
StorybookRequest,
|
StorybookRequest,
|
||||||
StorybookResponse,
|
StorybookResponse,
|
||||||
@@ -37,13 +39,45 @@ logger = get_logger(__name__)
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
RATE_LIMIT_WINDOW = 60 # seconds
|
RATE_LIMIT_WINDOW = 60 # seconds
|
||||||
RATE_LIMIT_REQUESTS = 10
|
RATE_LIMIT_REQUESTS = 10
|
||||||
|
|
||||||
|
|
||||||
@router.post("/stories/generate", response_model=StoryResponse)
|
@router.post("/generations", response_model=GenerationResponse)
|
||||||
async def generate_story(
|
async def create_generation(
|
||||||
request: GenerateRequest,
|
request: GenerationRequest,
|
||||||
user: User = Depends(require_user),
|
user: User = Depends(require_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Create a story or storybook through the unified generation workflow."""
|
||||||
|
await check_rate_limit(f"story:{user.id}", RATE_LIMIT_REQUESTS, RATE_LIMIT_WINDOW)
|
||||||
|
return await story_service.generate_generation_service(request, user.id, db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/generations/{story_id}", response_model=StoryDetailResponse)
|
||||||
|
async def get_generation(
|
||||||
|
story_id: int,
|
||||||
|
user: User = Depends(require_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Get a generated story/storybook through the unified generation API."""
|
||||||
|
return await story_service.get_story_detail(story_id, user.id, db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/generations/{story_id}/retry-assets", response_model=StoryDetailResponse)
|
||||||
|
async def retry_generation_assets(
|
||||||
|
story_id: int,
|
||||||
|
payload: StoryAssetRetryRequest,
|
||||||
|
user: User = Depends(require_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Retry generated assets through the unified generation API."""
|
||||||
|
return await story_service.retry_story_assets(story_id, user.id, payload.assets, db)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/stories/generate", response_model=StoryResponse)
|
||||||
|
async def generate_story(
|
||||||
|
request: GenerateRequest,
|
||||||
|
user: User = Depends(require_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Generate or enhance a story."""
|
"""Generate or enhance a story."""
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ class GenerateRequest(BaseModel):
|
|||||||
universe_id: str | None = None
|
universe_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationRequest(BaseModel):
|
||||||
|
"""Unified generation request for story and storybook outputs."""
|
||||||
|
|
||||||
|
output_mode: Literal["story", "storybook"] = Field(default="story")
|
||||||
|
type: Literal["keywords", "full_story"] = Field(default="keywords")
|
||||||
|
data: str = Field(..., min_length=1, max_length=MAX_DATA_LENGTH)
|
||||||
|
education_theme: str | None = Field(default=None, max_length=MAX_EDU_THEME_LENGTH)
|
||||||
|
generate_images: bool = Field(default=True)
|
||||||
|
page_count: int = Field(default=6, ge=4, le=12)
|
||||||
|
child_profile_id: str | None = None
|
||||||
|
universe_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class StoryResponse(StoryStatusMixin):
|
class StoryResponse(StoryStatusMixin):
|
||||||
"""Story generation response."""
|
"""Story generation response."""
|
||||||
|
|
||||||
@@ -99,6 +112,25 @@ class StorybookResponse(StoryStatusMixin):
|
|||||||
cover_url: str | None = None
|
cover_url: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class GenerationResponse(StoryStatusMixin):
|
||||||
|
"""Unified generation response for the target workflow API."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
title: str
|
||||||
|
mode: str
|
||||||
|
story_text: str | None = None
|
||||||
|
pages: list[StorybookPageResponse] | None = None
|
||||||
|
cover_prompt: str | None = None
|
||||||
|
image_url: str | None = None
|
||||||
|
cover_url: str | None = None
|
||||||
|
audio_ready: bool = False
|
||||||
|
errors: dict[str, str | None] = Field(default_factory=dict)
|
||||||
|
main_character: str | None = None
|
||||||
|
art_style: str | None = None
|
||||||
|
child_profile_id: str | None = None
|
||||||
|
universe_id: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class StoryDetailResponse(StoryStatusMixin):
|
class StoryDetailResponse(StoryStatusMixin):
|
||||||
"""Story detail response for both stories and storybooks."""
|
"""Story detail response for both stories and storybooks."""
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from app.schemas.story_schemas import (
|
|||||||
AchievementItem,
|
AchievementItem,
|
||||||
FullStoryResponse,
|
FullStoryResponse,
|
||||||
GenerateRequest,
|
GenerateRequest,
|
||||||
|
GenerationRequest,
|
||||||
|
GenerationResponse,
|
||||||
StorybookPageResponse,
|
StorybookPageResponse,
|
||||||
StorybookRequest,
|
StorybookRequest,
|
||||||
StorybookResponse,
|
StorybookResponse,
|
||||||
@@ -385,6 +387,94 @@ async def generate_storybook_service(
|
|||||||
audio_status=story.audio_status,
|
audio_status=story.audio_status,
|
||||||
last_error=story.last_error,
|
last_error=story.last_error,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def generate_generation_service(
|
||||||
|
request: GenerationRequest,
|
||||||
|
user_id: str,
|
||||||
|
db: AsyncSession,
|
||||||
|
) -> GenerationResponse:
|
||||||
|
"""Unified generation workflow entry point for stories and storybooks."""
|
||||||
|
|
||||||
|
if request.output_mode == "storybook":
|
||||||
|
storybook = await generate_storybook_service(
|
||||||
|
StorybookRequest(
|
||||||
|
keywords=request.data,
|
||||||
|
page_count=request.page_count,
|
||||||
|
education_theme=request.education_theme,
|
||||||
|
generate_images=request.generate_images,
|
||||||
|
child_profile_id=request.child_profile_id,
|
||||||
|
universe_id=request.universe_id,
|
||||||
|
),
|
||||||
|
user_id,
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
if storybook.id is None:
|
||||||
|
raise HTTPException(status_code=500, detail="Storybook generation did not persist.")
|
||||||
|
|
||||||
|
saved_story = await get_story_detail(storybook.id, user_id, db)
|
||||||
|
return GenerationResponse(
|
||||||
|
id=storybook.id,
|
||||||
|
title=storybook.title,
|
||||||
|
mode="storybook",
|
||||||
|
pages=storybook.pages,
|
||||||
|
cover_prompt=storybook.cover_prompt,
|
||||||
|
image_url=storybook.cover_url,
|
||||||
|
cover_url=storybook.cover_url,
|
||||||
|
main_character=storybook.main_character,
|
||||||
|
art_style=storybook.art_style,
|
||||||
|
generation_status=storybook.generation_status,
|
||||||
|
image_status=storybook.image_status,
|
||||||
|
audio_status=storybook.audio_status,
|
||||||
|
last_error=storybook.last_error,
|
||||||
|
child_profile_id=saved_story.child_profile_id,
|
||||||
|
universe_id=saved_story.universe_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
generate_request = GenerateRequest(
|
||||||
|
type=request.type,
|
||||||
|
data=request.data,
|
||||||
|
education_theme=request.education_theme,
|
||||||
|
child_profile_id=request.child_profile_id,
|
||||||
|
universe_id=request.universe_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if request.generate_images:
|
||||||
|
story = await generate_full_story_service(generate_request, user_id, db)
|
||||||
|
return GenerationResponse(
|
||||||
|
id=story.id,
|
||||||
|
title=story.title,
|
||||||
|
mode=story.mode,
|
||||||
|
story_text=story.story_text,
|
||||||
|
cover_prompt=story.cover_prompt,
|
||||||
|
image_url=story.image_url,
|
||||||
|
cover_url=story.image_url,
|
||||||
|
audio_ready=story.audio_ready,
|
||||||
|
errors=story.errors,
|
||||||
|
generation_status=story.generation_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,
|
||||||
|
)
|
||||||
|
|
||||||
|
story = await generate_and_save_story(generate_request, user_id, db)
|
||||||
|
return GenerationResponse(
|
||||||
|
id=story.id,
|
||||||
|
title=story.title,
|
||||||
|
mode=story.mode,
|
||||||
|
story_text=story.story_text,
|
||||||
|
cover_prompt=story.cover_prompt,
|
||||||
|
image_url=story.image_url,
|
||||||
|
cover_url=story.image_url,
|
||||||
|
generation_status=story.generation_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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ==================== Missing Endpoints Logic (for Issue #5) ====================
|
# ==================== Missing Endpoints Logic (for Issue #5) ====================
|
||||||
|
|||||||
@@ -367,6 +367,135 @@ class TestGenerateFull:
|
|||||||
assert call_kwargs["education_theme"] == "勇气与友谊"
|
assert call_kwargs["education_theme"] == "勇气与友谊"
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnifiedGenerations:
|
||||||
|
"""Tests for the target unified generation API."""
|
||||||
|
|
||||||
|
def test_create_generation_without_auth(self, client: TestClient):
|
||||||
|
response = client.post(
|
||||||
|
"/api/generations",
|
||||||
|
json={"output_mode": "story", "type": "keywords", "data": "小兔子, 森林"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
def test_create_story_generation_success(
|
||||||
|
self,
|
||||||
|
auth_client: TestClient,
|
||||||
|
mock_text_provider,
|
||||||
|
mock_image_provider,
|
||||||
|
):
|
||||||
|
response = auth_client.post(
|
||||||
|
"/api/generations",
|
||||||
|
json={
|
||||||
|
"output_mode": "story",
|
||||||
|
"type": "keywords",
|
||||||
|
"data": "小兔子, 森林, 勇气",
|
||||||
|
"generate_images": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] is not None
|
||||||
|
assert data["mode"] == "generated"
|
||||||
|
assert data["story_text"] == "从前有一只小兔子。"
|
||||||
|
assert data["image_url"] == "https://example.com/image.png"
|
||||||
|
assert data["cover_url"] == "https://example.com/image.png"
|
||||||
|
assert data["pages"] is None
|
||||||
|
assert data["generation_status"] == "completed"
|
||||||
|
assert data["image_status"] == "ready"
|
||||||
|
assert data["audio_status"] == "not_requested"
|
||||||
|
assert data["errors"] == {}
|
||||||
|
|
||||||
|
def test_create_story_generation_without_assets(
|
||||||
|
self,
|
||||||
|
auth_client: TestClient,
|
||||||
|
mock_text_provider,
|
||||||
|
):
|
||||||
|
response = auth_client.post(
|
||||||
|
"/api/generations",
|
||||||
|
json={
|
||||||
|
"output_mode": "story",
|
||||||
|
"type": "keywords",
|
||||||
|
"data": "小兔子, 森林",
|
||||||
|
"generate_images": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["mode"] == "generated"
|
||||||
|
assert data["image_url"] is None
|
||||||
|
assert data["generation_status"] == "narrative_ready"
|
||||||
|
assert data["image_status"] == "not_requested"
|
||||||
|
|
||||||
|
def test_create_storybook_generation_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:
|
||||||
|
mock_storybook.return_value = build_storybook_output()
|
||||||
|
mock_image.side_effect = [
|
||||||
|
"https://example.com/storybook-cover.png",
|
||||||
|
"https://example.com/storybook-page-1.png",
|
||||||
|
"https://example.com/storybook-page-2.png",
|
||||||
|
]
|
||||||
|
|
||||||
|
response = auth_client.post(
|
||||||
|
"/api/generations",
|
||||||
|
json={
|
||||||
|
"output_mode": "storybook",
|
||||||
|
"type": "keywords",
|
||||||
|
"data": "森林, 发光, 友情",
|
||||||
|
"page_count": 6,
|
||||||
|
"generate_images": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] is not None
|
||||||
|
assert data["mode"] == "storybook"
|
||||||
|
assert data["story_text"] is None
|
||||||
|
assert len(data["pages"]) == 2
|
||||||
|
assert data["cover_url"] == "https://example.com/storybook-cover.png"
|
||||||
|
assert data["image_url"] == "https://example.com/storybook-cover.png"
|
||||||
|
assert data["main_character"] == "小兔子露露"
|
||||||
|
assert data["art_style"] == "温暖水彩"
|
||||||
|
assert data["generation_status"] == "completed"
|
||||||
|
assert data["image_status"] == "ready"
|
||||||
|
assert data["audio_status"] == "not_requested"
|
||||||
|
|
||||||
|
def test_get_generation_alias(self, auth_client: TestClient, test_story):
|
||||||
|
response = auth_client.get(f"/api/generations/{test_story.id}")
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["id"] == test_story.id
|
||||||
|
assert data["title"] == test_story.title
|
||||||
|
assert data["mode"] == "generated"
|
||||||
|
|
||||||
|
def test_retry_generation_assets_alias(
|
||||||
|
self,
|
||||||
|
auth_client: TestClient,
|
||||||
|
degraded_story_with_text,
|
||||||
|
mock_image_provider,
|
||||||
|
):
|
||||||
|
response = auth_client.post(
|
||||||
|
f"/api/generations/{degraded_story_with_text.id}/retry-assets",
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
class TestImageGenerateSuccess:
|
class TestImageGenerateSuccess:
|
||||||
"""Tests for successful cover generation."""
|
"""Tests for successful cover generation."""
|
||||||
|
|
||||||
|
|||||||
@@ -53,14 +53,20 @@
|
|||||||
- 已新增数据库迁移:
|
- 已新增数据库迁移:
|
||||||
- `0009_add_story_generation_statuses.py`
|
- `0009_add_story_generation_statuses.py`
|
||||||
- `0010_add_story_audio_cache_path.py`
|
- `0010_add_story_audio_cache_path.py`
|
||||||
- 已完成一轮后端回归验证:`backend/` 下 `pytest -q` 结果为 `53 passed`
|
- 已完成一轮后端回归验证:`backend/` 下 `pytest -q` 结果为 `63 passed`
|
||||||
|
- 已完成全量后端 lint 清理:`ruff check app tests` 可通过
|
||||||
- 已修复 admin-frontend 构建阻塞,主前端与管理端前端均可生产构建
|
- 已修复 admin-frontend 构建阻塞,主前端与管理端前端均可生产构建
|
||||||
- 已落地首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
- 已落地首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
||||||
|
- 已建立目标态统一生成入口:
|
||||||
|
- `POST /api/generations`
|
||||||
|
- `GET /api/generations/{story_id}`
|
||||||
|
- `POST /api/generations/{story_id}/retry-assets`
|
||||||
|
- 用户前端与 admin 前端创建弹窗已切换到统一生成入口
|
||||||
|
|
||||||
### What Is In Progress
|
### What Is In Progress
|
||||||
|
|
||||||
- 统一状态模型已落地,但统一 service workflow 仍未真正收束成单一路径
|
- 统一状态模型与统一外部 API 已落地,但内部 service workflow 仍未真正收束成单一路径
|
||||||
- 普通故事、完整生成、绘本生成仍存在多条 service / API 路径
|
- 旧生成 API 仍保留为兼容层,后续需要继续降低重复实现
|
||||||
- 资产补全已经具备统一重试入口首版,但仍需要继续抽象统一补全过程和 generation job 边界
|
- 资产补全已经具备统一重试入口首版,但仍需要继续抽象统一补全过程和 generation job 边界
|
||||||
|
|
||||||
### What Is Still Pending
|
### What Is Still Pending
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
|
|||||||
|
|
||||||
## Implementation Snapshot
|
## Implementation Snapshot
|
||||||
|
|
||||||
**Updated**: 2026-04-18 morning
|
**Updated**: 2026-04-18 afternoon
|
||||||
|
|
||||||
当前代码已经从“纯目标态设计”进入“部分落地”阶段,主要进展如下:
|
当前代码已经从“纯目标态设计”进入“部分落地”阶段,主要进展如下:
|
||||||
|
|
||||||
@@ -34,13 +34,17 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
|
|||||||
- 故事音频已支持首次生成后缓存复用
|
- 故事音频已支持首次生成后缓存复用
|
||||||
- `degraded_completed` 已在服务层和前端语义中落地
|
- `degraded_completed` 已在服务层和前端语义中落地
|
||||||
- 已新增首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
- 已新增首版统一资产重试入口:`POST /api/stories/{story_id}/assets/retry`
|
||||||
|
- 已新增目标态统一生成 API:
|
||||||
|
- `POST /api/generations`
|
||||||
|
- `GET /api/generations/{story_id}`
|
||||||
|
- `POST /api/generations/{story_id}/retry-assets`
|
||||||
|
- 用户前端与 admin 前端创建弹窗已切换到 `POST /api/generations`
|
||||||
- 故事详情页封面补全已切换到统一资产重试入口
|
- 故事详情页封面补全已切换到统一资产重试入口
|
||||||
- 管理端前端构建阻塞已修复,主前端与 admin 前端均可完成生产构建
|
- 管理端前端构建阻塞已修复,主前端与 admin 前端均可完成生产构建
|
||||||
|
|
||||||
### Still Missing
|
### Still Missing
|
||||||
|
|
||||||
- 统一的 `POST /api/generations` 风格入口尚未建立
|
- 普通故事、完整生成、绘本生成已有统一外部入口,但内部仍通过兼容 service 路径编排
|
||||||
- 普通故事、完整生成、绘本生成仍通过多条 service 路径实现
|
|
||||||
- 统一资产重试入口仍是首版:已覆盖普通故事封面、绘本缺失插图、故事音频,但尚未抽象成完整 generation job 模型
|
- 统一资产重试入口仍是首版:已覆盖普通故事封面、绘本缺失插图、故事音频,但尚未抽象成完整 generation job 模型
|
||||||
- `partial_ready`、`retryable_assets` 等更细粒度状态仍停留在目标态
|
- `partial_ready`、`retryable_assets` 等更细粒度状态仍停留在目标态
|
||||||
|
|
||||||
@@ -56,8 +60,8 @@ DreamWeaver 当前同时支持普通故事生成、完整故事生成和绘本
|
|||||||
|
|
||||||
DreamWeaver 当前存在以下工作流层面问题:
|
DreamWeaver 当前存在以下工作流层面问题:
|
||||||
|
|
||||||
1. **生成入口不统一**
|
1. **生成入口正在统一中**
|
||||||
普通故事走 `/api/stories/generate`,完整故事走 `/api/stories/generate/full`,绘本走 `/api/storybook/generate`,前端对结果的处理也不同。
|
当前前端已切到 `/api/generations`,旧的 `/api/stories/generate`、`/api/stories/generate/full`、`/api/storybook/generate` 仍作为兼容入口保留。下一步重点是继续收束 service 内部路径。
|
||||||
|
|
||||||
2. **保存与资产补全过程不统一**
|
2. **保存与资产补全过程不统一**
|
||||||
有的流程先存文本再补图,有的流程只返回绘本对象并依赖前端 store,有的流程不考虑音频状态。
|
有的流程先存文本再补图,有的流程只返回绘本对象并依赖前端 store,有的流程不考虑音频状态。
|
||||||
|
|||||||
@@ -134,33 +134,31 @@ async function generateStory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
error.value = ''
|
error.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const payload: Record<string, unknown> = {
|
const requestedOutputMode =
|
||||||
type: inputType.value,
|
inputType.value === 'full_story' ? 'story' : outputMode.value === 'storybook' ? 'storybook' : 'story'
|
||||||
data: inputData.value,
|
const payload: Record<string, unknown> = {
|
||||||
education_theme: educationTheme.value || undefined,
|
output_mode: requestedOutputMode,
|
||||||
}
|
type: inputType.value,
|
||||||
if (selectedProfileId.value) payload.child_profile_id = selectedProfileId.value
|
data: inputData.value,
|
||||||
if (selectedUniverseId.value) payload.universe_id = selectedUniverseId.value
|
education_theme: educationTheme.value || undefined,
|
||||||
|
generate_images: true,
|
||||||
if (outputMode.value === 'storybook') {
|
page_count: 6,
|
||||||
const response = await api.post<any>('/api/storybook/generate', {
|
}
|
||||||
keywords: inputData.value,
|
if (selectedProfileId.value) payload.child_profile_id = selectedProfileId.value
|
||||||
education_theme: educationTheme.value || undefined,
|
if (selectedUniverseId.value) payload.universe_id = selectedUniverseId.value
|
||||||
generate_images: true,
|
|
||||||
page_count: 6,
|
if (requestedOutputMode === 'storybook') {
|
||||||
child_profile_id: selectedProfileId.value || undefined,
|
const response = await api.post<any>('/api/generations', payload)
|
||||||
universe_id: selectedUniverseId.value || undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
storybookStore.setStorybook(response)
|
storybookStore.setStorybook(response)
|
||||||
close()
|
close()
|
||||||
const storybookPath = response.id ? `/storybook/view/${response.id}` : '/storybook/view'
|
const storybookPath = response.id ? `/storybook/view/${response.id}` : '/storybook/view'
|
||||||
router.push(storybookPath)
|
router.push(storybookPath)
|
||||||
} else {
|
} else {
|
||||||
const result = await api.post<any>('/api/stories/generate/full', payload)
|
const result = await api.post<any>('/api/generations', payload)
|
||||||
close()
|
close()
|
||||||
router.push(`/story/${result.id}`)
|
router.push(`/story/${result.id}`)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user