feat: polish generation demo workflow
This commit is contained in:
@@ -53,6 +53,27 @@ const storyParagraphs = computed(() => story.value?.story_text?.split('\n\n') ??
|
||||
const generationMeta = computed(() => getGenerationStatusMeta(story.value?.generation_status))
|
||||
const imageMeta = computed(() => getAssetStatusMeta(story.value?.image_status))
|
||||
const audioMeta = computed(() => getAssetStatusMeta(story.value?.audio_status))
|
||||
const canRetryImage = computed(() =>
|
||||
Boolean(story.value?.cover_prompt)
|
||||
&& story.value?.image_status !== 'ready'
|
||||
&& story.value?.image_status !== 'generating',
|
||||
)
|
||||
const canRetryAudio = computed(() =>
|
||||
Boolean(story.value?.story_text)
|
||||
&& story.value?.audio_status !== 'ready'
|
||||
&& story.value?.audio_status !== 'generating',
|
||||
)
|
||||
const assetGuidance = computed(() => {
|
||||
if (story.value?.generation_status === 'degraded_completed') {
|
||||
return '正文已经可读,失败的资源可以单独重试,不会覆盖当前故事。'
|
||||
}
|
||||
|
||||
if (story.value?.generation_status === 'assets_generating') {
|
||||
return '资源正在处理中,可以稍后刷新查看最新状态。'
|
||||
}
|
||||
|
||||
return '封面和音频都是可补全资产,首次生成后会保存状态并复用结果。'
|
||||
})
|
||||
|
||||
async function refreshStorySnapshot() {
|
||||
const data = await api.get<Story>(`/api/stories/${route.params.id}`)
|
||||
@@ -85,7 +106,7 @@ async function generateImage() {
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
story.value = await api.post<Story>(`/api/stories/${story.value.id}/assets/retry`, {
|
||||
story.value = await api.post<Story>(`/api/generations/${story.value.id}/retry-assets`, {
|
||||
assets: ['image'],
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -122,6 +143,28 @@ async function loadAudio() {
|
||||
}
|
||||
}
|
||||
|
||||
async function retryAudio() {
|
||||
if (!story.value) return
|
||||
|
||||
audioLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
story.value = await api.post<Story>(`/api/generations/${story.value.id}/retry-assets`, {
|
||||
assets: ['audio'],
|
||||
})
|
||||
if (story.value.audio_status === 'ready') {
|
||||
audioUrl.value = null
|
||||
await loadAudio()
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '音频生成失败'
|
||||
await refreshStorySnapshot().catch(() => undefined)
|
||||
} finally {
|
||||
audioLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function togglePlay() {
|
||||
if (!audioRef.value) return
|
||||
|
||||
@@ -266,6 +309,16 @@ onUnmounted(() => {
|
||||
<div class="text-sm text-gray-500 mb-2">封面资源</div>
|
||||
<div class="font-semibold text-gray-800 mb-2">{{ imageMeta.label }}</div>
|
||||
<p class="text-sm text-gray-500 leading-6">{{ imageMeta.description }}</p>
|
||||
<BaseButton
|
||||
v-if="canRetryImage"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:loading="imageLoading"
|
||||
class="mt-4 w-full"
|
||||
@click="generateImage"
|
||||
>
|
||||
{{ story.image_status === 'failed' ? '重试封面' : '补全封面' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-gray-100 bg-white/80 p-5">
|
||||
<div class="text-sm text-gray-500 mb-2">音频资源</div>
|
||||
@@ -273,9 +326,24 @@ onUnmounted(() => {
|
||||
<p class="text-sm text-gray-500 leading-6">
|
||||
{{ audioMeta.description }} 音频首次生成后会缓存复用,状态记录的是当前可播放结果。
|
||||
</p>
|
||||
<BaseButton
|
||||
v-if="canRetryAudio"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:loading="audioLoading"
|
||||
class="mt-4 w-full"
|
||||
@click="retryAudio"
|
||||
>
|
||||
{{ story.audio_status === 'failed' ? '重试音频' : '生成音频' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-10 rounded-lg border border-emerald-100 bg-emerald-50/80 p-4 text-sm text-emerald-800 leading-6">
|
||||
<div class="font-semibold mb-1">资源补全策略</div>
|
||||
<p>{{ assetGuidance }}</p>
|
||||
</div>
|
||||
|
||||
<div class="prose prose-lg max-w-none mb-10">
|
||||
<p
|
||||
v-for="(paragraph, index) in storyParagraphs"
|
||||
@@ -290,13 +358,13 @@ onUnmounted(() => {
|
||||
<div v-if="!audioUrl" class="text-center">
|
||||
<BaseButton
|
||||
:loading="audioLoading"
|
||||
@click="loadAudio"
|
||||
@click="story.audio_status === 'ready' ? loadAudio() : retryAudio()"
|
||||
class="mx-auto"
|
||||
>
|
||||
<template v-if="audioLoading">正在准备音频...</template>
|
||||
<template v-else>
|
||||
<SpeakerWaveIcon class="h-5 w-5" />
|
||||
试听故事
|
||||
{{ story.audio_status === 'ready' ? '试听故事' : '生成并试听故事' }}
|
||||
</template>
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ const store = useStorybookStore()
|
||||
|
||||
const storybook = computed(() => store.currentStorybook)
|
||||
const loading = ref(true)
|
||||
const imageLoading = ref(false)
|
||||
const error = ref('')
|
||||
const currentPageIndex = ref(-1)
|
||||
|
||||
@@ -50,6 +51,11 @@ const isLastPage = computed(() => currentPageIndex.value === totalPages.value -
|
||||
const generationMeta = computed(() => getGenerationStatusMeta(storybook.value?.generation_status))
|
||||
const imageMeta = computed(() => getAssetStatusMeta(storybook.value?.image_status))
|
||||
const audioMeta = computed(() => getAssetStatusMeta(storybook.value?.audio_status))
|
||||
const canRetryImages = computed(() =>
|
||||
Boolean(storybook.value?.id)
|
||||
&& storybook.value?.image_status !== 'ready'
|
||||
&& storybook.value?.image_status !== 'generating',
|
||||
)
|
||||
const currentPage = computed(() => {
|
||||
if (!storybook.value || isCover.value) return null
|
||||
return storybook.value.pages[currentPageIndex.value]
|
||||
@@ -153,6 +159,42 @@ async function loadStorybook() {
|
||||
}
|
||||
}
|
||||
|
||||
async function retryStorybookImages() {
|
||||
if (!storybook.value?.id) return
|
||||
|
||||
imageLoading.value = true
|
||||
error.value = ''
|
||||
|
||||
try {
|
||||
const detail = await api.post<StoryDetailResponse>(
|
||||
`/api/generations/${storybook.value.id}/retry-assets`,
|
||||
{ assets: ['image'] },
|
||||
)
|
||||
|
||||
store.setStorybook({
|
||||
id: detail.id,
|
||||
title: detail.title,
|
||||
main_character: storybook.value.main_character || '故事主角',
|
||||
art_style: storybook.value.art_style || 'AI 绘本风格',
|
||||
pages: (detail.pages ?? []).map((page) => ({
|
||||
...page,
|
||||
image_url: page.image_url ?? undefined,
|
||||
})),
|
||||
cover_prompt: detail.cover_prompt ?? '',
|
||||
cover_url: detail.image_url ?? undefined,
|
||||
generation_status: detail.generation_status,
|
||||
image_status: detail.image_status,
|
||||
audio_status: detail.audio_status,
|
||||
last_error: detail.last_error,
|
||||
})
|
||||
} catch (e) {
|
||||
error.value = e instanceof Error ? e.message : '插图补全失败'
|
||||
await loadStorybook().catch(() => undefined)
|
||||
} finally {
|
||||
imageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
() => {
|
||||
@@ -206,6 +248,16 @@ watch(
|
||||
<div v-else class="w-full h-full flex flex-col items-center justify-center p-8 text-center bg-gradient-to-br from-indigo-900 to-purple-900 text-white">
|
||||
<SparklesIcon class="w-20 h-20 mb-4 opacity-50" />
|
||||
<p class="text-white/70 text-sm max-w-xs leading-6">{{ imageMeta.description }}</p>
|
||||
<BaseButton
|
||||
v-if="canRetryImages"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:loading="imageLoading"
|
||||
class="mt-5"
|
||||
@click="retryStorybookImages"
|
||||
>
|
||||
{{ storybook.image_status === 'failed' ? '重试插图' : '补全插图' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black/60 via-transparent to-transparent md:bg-gradient-to-r"></div>
|
||||
<div class="absolute bottom-6 left-6 text-white md:hidden">
|
||||
@@ -249,6 +301,24 @@ watch(
|
||||
<p class="leading-6">{{ storybook.last_error }}</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="canRetryImages"
|
||||
class="mb-8 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900"
|
||||
>
|
||||
<div class="font-semibold mb-1">插图可稍后补全</div>
|
||||
<p class="leading-6 mb-3">
|
||||
绘本文字已经保存,可以先阅读;补全插图会更新封面和缺失页面。
|
||||
</p>
|
||||
<BaseButton
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:loading="imageLoading"
|
||||
@click="retryStorybookImages"
|
||||
>
|
||||
{{ storybook.image_status === 'failed' ? '重试全部插图' : '补全全部插图' }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
|
||||
<BaseButton size="lg" @click="nextPage" class="self-start shadow-xl hover:shadow-2xl hover:-translate-y-1 transition-all">
|
||||
开始阅读
|
||||
<BookOpenIcon class="w-5 h-5 ml-2" />
|
||||
@@ -273,6 +343,16 @@ watch(
|
||||
<p class="text-amber-900/50 text-sm max-w-xs mx-auto italic leading-6">
|
||||
{{ pageImageMessage }}
|
||||
</p>
|
||||
<BaseButton
|
||||
v-if="canRetryImages"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
:loading="imageLoading"
|
||||
class="mt-4"
|
||||
@click="retryStorybookImages"
|
||||
>
|
||||
补全插图
|
||||
</BaseButton>
|
||||
<p
|
||||
v-if="storybook.last_error && storybook.image_status === 'failed'"
|
||||
class="mt-3 text-xs font-medium text-amber-700"
|
||||
|
||||
Reference in New Issue
Block a user