feat: refine UI, improve UX, optimize the analysis pipeline, and add character standing positions

This commit is contained in:
saturn
2026-04-02 17:39:16 +08:00
parent c3e74c228a
commit 9703714b69
153 changed files with 4472 additions and 1088 deletions

View File

@@ -0,0 +1,33 @@
import { describe, expect, it } from 'vitest'
import { buildInsertPanelLocationsDescription } from '@/lib/novel-promotion/insert-panel-prompt-context'
describe('insert panel prompt context', () => {
it('injects available slots for related selected location images', () => {
const text = buildInsertPanelLocationsDescription(
[
{
name: '餐厅',
images: [
{
isSelected: true,
description: '长方形饭桌位于画面中央',
availableSlots: JSON.stringify([
'饭桌左侧靠桌边的位置',
]),
},
],
},
{
name: '客厅',
images: [{ isSelected: true, description: '不会被选中' }],
},
],
['餐厅'],
)
expect(text).toContain('餐厅: 长方形饭桌位于画面中央')
expect(text).toContain('可站位置:')
expect(text).toContain('饭桌左侧靠桌边的位置')
expect(text).not.toContain('客厅')
})
})

View File

@@ -17,6 +17,7 @@ vi.mock('@/components/story-input/StoryInputComposer', () => ({
default: ({
minRows,
maxHeightViewportRatio,
textareaClassName,
topRight,
footer,
secondaryActions,
@@ -24,6 +25,7 @@ vi.mock('@/components/story-input/StoryInputComposer', () => ({
}: {
minRows: number
maxHeightViewportRatio: number
textareaClassName?: string
topRight?: React.ReactNode
footer?: React.ReactNode
secondaryActions?: React.ReactNode
@@ -33,6 +35,7 @@ vi.mock('@/components/story-input/StoryInputComposer', () => ({
{
'data-min-rows': String(minRows),
'data-max-height-ratio': String(maxHeightViewportRatio),
'data-textarea-class': textareaClassName,
},
topRight,
footer,
@@ -79,6 +82,7 @@ describe('NovelInputStage', () => {
expect(html).toContain('StoryInputComposer')
expect(html).toContain('data-min-rows="8"')
expect(html).toContain('data-max-height-ratio="0.5"')
expect(html).toContain('data-textarea-class="px-0 pt-0 pb-3 align-top"')
expect(html).toContain('aiWrite.trigger')
expect(html).toContain('AiWriteModal')
expect(html).not.toContain('storyInput.wordCount 0')

View File

@@ -0,0 +1,81 @@
import { describe, expect, it } from 'vitest'
import {
hasScriptArtifacts,
hasStoryboardArtifacts,
hasVideoArtifacts,
resolveEpisodeStageArtifacts,
} from '@/lib/novel-promotion/stage-readiness'
describe('stage readiness', () => {
it('treats script as ready only when at least one clip has non-empty screenplay', () => {
expect(hasScriptArtifacts([])).toBe(false)
expect(hasScriptArtifacts([
{ id: 'clip-1', summary: '', location: null, characters: null, props: null, content: 'a', screenplay: '' },
])).toBe(false)
expect(hasScriptArtifacts([
{ id: 'clip-1', summary: '', location: null, characters: null, props: null, content: 'a', screenplay: ' {"scenes":[]}' },
])).toBe(true)
})
it('treats storyboard as ready only when at least one storyboard has panels', () => {
expect(hasStoryboardArtifacts([])).toBe(false)
expect(hasStoryboardArtifacts([{ panels: [] }])).toBe(false)
expect(hasStoryboardArtifacts([{ panels: [{ id: 'panel-1' }] }])).toBe(true)
})
it('treats video as ready only when at least one panel has videoUrl', () => {
expect(hasVideoArtifacts([{ panels: [{ id: 'panel-1', videoUrl: '' }] }])).toBe(false)
expect(hasVideoArtifacts([{ panels: [{ id: 'panel-1', videoUrl: 'https://example.com/video.mp4' }] }])).toBe(true)
})
it('derives full episode stage artifacts from persisted outputs', () => {
const readiness = resolveEpisodeStageArtifacts({
novelText: 'story',
clips: [
{ id: 'clip-1', summary: '', location: null, characters: null, props: null, content: 'a', screenplay: '{"scenes":[]}' },
],
storyboards: [
{
id: 'sb-1',
episodeId: 'ep-1',
clipId: 'clip-1',
storyboardTextJson: null,
panelCount: 1,
storyboardImageUrl: null,
panels: [{
id: 'panel-1',
storyboardId: 'sb-1',
panelIndex: 0,
panelNumber: 1,
shotType: null,
cameraMove: null,
description: null,
location: null,
characters: null,
props: null,
srtSegment: null,
srtStart: null,
srtEnd: null,
duration: null,
imagePrompt: null,
imageUrl: null,
imageHistory: null,
videoPrompt: null,
videoUrl: 'https://example.com/video.mp4',
photographyRules: null,
actingNotes: null,
}],
},
],
voiceLines: [{ id: 'voice-1' }],
})
expect(readiness).toEqual({
hasStory: true,
hasScript: true,
hasStoryboard: true,
hasVideo: true,
hasVoice: true,
})
})
})