Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yaoai-video
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
姚珂
yaoai-video
Commits
2ce18b88
Commit
2ce18b88
authored
May 06, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Agent制作fix6
parent
b29011ef
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
270 additions
and
8 deletions
+270
-8
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+81
-4
ai.ts
doc/html/src/lib/api/ai.ts
+2
-0
2026-04-28-agent-storyboard-enrichment.md
...perpowers/plans/2026-04-28-agent-storyboard-enrichment.md
+0
-0
AgentRunServiceImpl.java
...ava/com/yaoai/agent/service/impl/AgentRunServiceImpl.java
+10
-1
AgentRunServiceImplTest.java
...com/yaoai/agent/service/impl/AgentRunServiceImplTest.java
+139
-0
StoryboardPipelineServiceImplTest.java
...eline/service/impl/StoryboardPipelineServiceImplTest.java
+1
-1
StoryboardPipelineServiceImpl.java
.../pipeline/service/impl/StoryboardPipelineServiceImpl.java
+37
-2
No files found.
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
2ce18b88
...
...
@@ -95,6 +95,21 @@ function characterFrontUrl(c: Character): string | null {
return
c
.
frontImageUrl
??
c
.
imageUrl
??
null
;
}
function
refTokens
(
value
?:
string
|
null
):
string
[]
{
if
(
!
value
)
return
[];
return
value
.
split
(
","
).
map
((
token
)
=>
token
.
trim
()).
filter
(
Boolean
);
}
function
inferSceneRefFromCharacters
(
characters
:
string
|
null
|
undefined
,
scenes
:
Scene
[]):
string
|
null
{
const
sceneRefs
=
new
Set
(
scenes
.
map
((
scene
)
=>
scene
.
name
?.
trim
())
.
filter
(
Boolean
)
.
map
((
name
)
=>
`@
${
name
}
`
)
);
return
refTokens
(
characters
).
find
((
token
)
=>
sceneRefs
.
has
(
token
))
??
null
;
}
export
function
StoryboardWorkspace
()
{
const
{
episodeId
:
urlEpisodeId
,
projectId
}
=
useParams
<
{
projectId
:
string
;
episodeId
?:
string
}
>
();
const
navigate
=
useNavigate
();
...
...
@@ -238,7 +253,7 @@ export function StoryboardWorkspace() {
setCharacterKeys
(
initialChars
);
setSceneKey
(
lastTask
?.
sceneImageKey
??
""
);
setPropKeys
(
lastTask
?.
propImageKeys
??
[]);
setFreeformPrompt
(
lastTask
?.
userPrompt
?.
freeformPrompt
??
""
);
setFreeformPrompt
(
expanded
.
videoPrompt
?.
trim
()
||
(
lastTask
?.
userPrompt
?.
freeformPrompt
??
""
)
);
if
(
typeof
lastTask
?.
videoDuration
===
"number"
)
setSelectedDuration
(
lastTask
.
videoDuration
);
if
(
lastTask
?.
videoRatio
)
setSelectedRatio
(
lastTask
.
videoRatio
);
if
(
typeof
lastTask
?.
generateAudio
===
"boolean"
)
setAudioOn
(
lastTask
.
generateAudio
);
...
...
@@ -373,6 +388,20 @@ export function StoryboardWorkspace() {
const
previewTask
=
previewSb
?
getTaskForSb
(
previewSb
)
:
null
;
const
promptPreviewText
=
freeformPrompt
.
trim
();
const
sceneRefDisplay
=
expanded
?
expanded
.
sceneRef
?.
trim
()
||
inferSceneRefFromCharacters
(
expanded
.
characters
,
scenes
)
||
"未标注"
:
""
;
const
charactersDisplay
=
expanded
?.
characters
?.
trim
()
||
"未标注"
;
const
startFrameDisplay
=
expanded
?.
startFramePrompt
?.
trim
()
||
"未生成"
;
const
motionScriptDisplay
=
expanded
?.
motionScript
?.
trim
()
||
"未生成"
;
const
videoPromptDisplay
=
expanded
?.
videoPrompt
?.
trim
()
||
"未生成"
;
const
storyboardInfoFields
=
expanded
?
[
{
label
:
"出镜角色"
,
value
:
charactersDisplay
},
{
label
:
"分镜场景"
,
value
:
sceneRefDisplay
},
{
label
:
"首帧提示词"
,
value
:
startFrameDisplay
},
{
label
:
"镜头运动"
,
value
:
motionScriptDisplay
},
{
label
:
"视频提示词"
,
value
:
videoPromptDisplay
,
multiline
:
true
},
]
:
[];
return
(
<
div
className=
"h-full flex overflow-hidden bg-[#f4f7fb] text-foreground"
>
...
...
@@ -431,7 +460,7 @@ export function StoryboardWorkspace() {
<
span
className=
"absolute left-0 top-3 bottom-3 w-[3px] bg-[#1c64ff]/55 rounded-r-full"
/>
<
div
className=
"flex items-center justify-between mb-1.5"
>
<
span
className=
"text-[11px] font-semibold tracking-[0.12em] uppercase text-muted-foreground"
>
分镜备注
</
span
>
{
promptPreviewText
&&
(
{
(
promptPreviewText
||
expanded
)
&&
(
<
button
onClick=
{
()
=>
setShowPromptDetails
(
true
)
}
className=
"text-[10px] text-[#1c64ff] hover:text-[#164fd0] flex items-center gap-0.5 font-semibold transition-colors"
...
...
@@ -789,6 +818,7 @@ export function StoryboardWorkspace() {
{
showPromptDetails
&&
(
<
PromptDetailsModal
text=
{
freeformPrompt
}
agentInfo=
{
storyboardInfoFields
}
onClose=
{
()
=>
setShowPromptDetails
(
false
)
}
/>
)
}
...
...
@@ -799,6 +829,36 @@ export function StoryboardWorkspace() {
// ────────────────────────────────────────────────────────────────────────────
// StoryboardCard — accordion item with self-contained editor when expanded
// ────────────────────────────────────────────────────────────────────────────
function
StoryboardInfoField
({
label
,
value
,
multiline
=
false
,
compact
=
false
,
}:
{
label
:
string
;
value
:
string
;
multiline
?:
boolean
;
compact
?:
boolean
;
})
{
return
(
<
div
className=
{
compact
?
"px-3 py-2"
:
"px-3 py-2.5"
}
>
<
div
className=
"flex items-start gap-2"
>
<
span
className=
"w-16 flex-shrink-0 text-[11px] font-medium text-muted-foreground leading-relaxed"
>
{
label
}
</
span
>
<
span
className=
{
`min-w-0 flex-1 text-[11px] leading-relaxed text-foreground/85 ${
multiline ? "line-clamp-3 break-words" : "truncate"
}`
}
title=
{
value
}
>
{
value
}
</
span
>
</
div
>
</
div
>
);
}
interface
StoryboardCardProps
{
sb
:
Storyboard
;
isExpanded
:
boolean
;
...
...
@@ -1483,9 +1543,10 @@ function AddCharacterModal({ projectId, onClose, onAdded }: AddCharacterModalPro
// ────────────────────────────────────────────────────────────────────────────
interface
PromptDetailsModalProps
{
text
:
string
;
agentInfo
:
Array
<
{
label
:
string
;
value
:
string
;
multiline
?:
boolean
}
>
;
onClose
:
()
=>
void
;
}
function
PromptDetailsModal
({
text
,
onClose
}:
PromptDetailsModalProps
)
{
function
PromptDetailsModal
({
text
,
agentInfo
,
onClose
}:
PromptDetailsModalProps
)
{
const
trimmed
=
text
.
trim
();
return
(
<
div
className=
"fixed inset-0 bg-slate-950/55 backdrop-blur-sm z-50 flex items-center justify-center p-4"
onClick=
{
onClose
}
>
...
...
@@ -1499,7 +1560,7 @@ function PromptDetailsModal({ text, onClose }: PromptDetailsModalProps) {
<
X
className=
"w-4 h-4 text-muted-foreground"
/>
</
button
>
</
div
>
<
div
className=
"flex-1 overflow-y-auto p-5"
>
<
div
className=
"flex-1 overflow-y-auto p-5
space-y-4
"
>
{
trimmed
?
(
<
pre
className=
"text-sm text-foreground leading-relaxed whitespace-pre-wrap font-mono rounded-xl border border-slate-200/80 bg-[#f8fafc] p-4"
>
{
trimmed
}
...
...
@@ -1507,6 +1568,22 @@ function PromptDetailsModal({ text, onClose }: PromptDetailsModalProps) {
)
:
(
<
p
className=
"text-sm text-muted-foreground italic"
>
(未填写提示词)
</
p
>
)
}
{
agentInfo
.
length
>
0
&&
(
<
div
className=
"rounded-xl border border-slate-200/80 bg-white divide-y divide-slate-100 overflow-hidden"
>
<
div
className=
"px-3 py-2 flex items-center justify-between bg-[#f8fafc]"
>
<
span
className=
"text-xs font-medium text-foreground"
>
Agent 分镜信息
</
span
>
<
span
className=
"text-[10px] text-muted-foreground"
>
仅作参考
</
span
>
</
div
>
{
agentInfo
.
map
((
item
)
=>
(
<
StoryboardInfoField
key=
{
item
.
label
}
label=
{
item
.
label
}
value=
{
item
.
value
}
multiline=
{
item
.
multiline
}
/>
))
}
</
div
>
)
}
</
div
>
</
div
>
</
div
>
...
...
doc/html/src/lib/api/ai.ts
View file @
2ce18b88
...
...
@@ -31,6 +31,8 @@ export interface Storyboard {
shortDescription
:
string
;
detailedDescription
:
string
;
characters
:
string
;
sceneRef
?:
string
|
null
;
videoPrompt
?:
string
|
null
;
dialogues
:
string
;
cameraDirection
:
string
;
compositionGuide
:
string
;
...
...
docs/superpowers/plans/2026-04-28-agent-storyboard-enrichment.md
0 → 100644
View file @
2ce18b88
This diff is collapsed.
Click to expand it.
yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/service/impl/AgentRunServiceImpl.java
View file @
2ce18b88
...
...
@@ -187,14 +187,23 @@ public class AgentRunServiceImpl implements AgentRunService {
executeStep
(
runId
,
"storyboard_gen"
,
()
->
{
sseManager
.
emitLog
(
runId
,
"agent"
,
"[执行层] 生成分镜..."
);
int
total
=
0
;
int
promptTotal
=
0
;
for
(
Episode
ep
:
episodes
)
{
List
<
Storyboard
>
boards
=
storyboardPipeline
.
generateStoryboards
(
ep
.
getId
(),
projectId
,
tenantId
);
total
+=
boards
.
size
();
List
<
Storyboard
>
enriched
=
storyboardPipeline
.
populateMissingVideoPromptsByEpisode
(
ep
.
getId
(),
tenantId
);
long
promptCount
=
enriched
.
stream
()
.
filter
(
sb
->
sb
.
getVideoPrompt
()
!=
null
&&
!
sb
.
getVideoPrompt
().
isBlank
())
.
count
();
promptTotal
+=
(
int
)
promptCount
;
sseManager
.
emitLog
(
runId
,
"info"
,
"第 "
+
ep
.
getEpisodeNumber
()
+
" 集视频提示词:"
+
promptCount
+
" 条已生成"
);
sseManager
.
emitLog
(
runId
,
"info"
,
"第 "
+
ep
.
getEpisodeNumber
()
+
" 集分镜:"
+
boards
.
size
()
+
" 个镜头"
);
}
return
"共生成 "
+
total
+
" 个分镜"
;
return
"共生成 "
+
total
+
" 个分镜
,并补全 "
+
promptTotal
+
" 条视频提示词
"
;
});
checkPaused
(
runId
);
...
...
yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/agent/service/impl/AgentRunServiceImplTest.java
0 → 100644
View file @
2ce18b88
package
com
.
yaoai
.
agent
.
service
.
impl
;
import
com.yaoai.agent.sse.AgentSseManager
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.domain.entity.Episode
;
import
com.yaoai.domain.entity.GraphRun
;
import
com.yaoai.domain.entity.GraphStep
;
import
com.yaoai.domain.entity.Outline
;
import
com.yaoai.domain.entity.Storyboard
;
import
com.yaoai.domain.mapper.EpisodeMapper
;
import
com.yaoai.domain.mapper.GraphRunMapper
;
import
com.yaoai.domain.mapper.GraphStepMapper
;
import
com.yaoai.pipeline.service.AssetGenPipelineService
;
import
com.yaoai.pipeline.service.OutlinePipelineService
;
import
com.yaoai.pipeline.service.StoryboardPipelineService
;
import
org.junit.jupiter.api.Test
;
import
org.junit.jupiter.api.extension.ExtendWith
;
import
org.mockito.ArgumentCaptor
;
import
org.mockito.Mock
;
import
org.mockito.junit.jupiter.MockitoExtension
;
import
org.springframework.test.util.ReflectionTestUtils
;
import
java.util.List
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertTrue
;
import
static
org
.
mockito
.
ArgumentMatchers
.
any
;
import
static
org
.
mockito
.
Mockito
.
atLeastOnce
;
import
static
org
.
mockito
.
Mockito
.
verify
;
import
static
org
.
mockito
.
Mockito
.
when
;
@ExtendWith
(
MockitoExtension
.
class
)
class
AgentRunServiceImplTest
{
@Mock
private
GraphRunMapper
graphRunMapper
;
@Mock
private
GraphStepMapper
graphStepMapper
;
@Mock
private
EpisodeMapper
episodeMapper
;
@Mock
private
AgentSseManager
sseManager
;
@Mock
private
LlmService
llmService
;
@Mock
private
OutlinePipelineService
outlinePipeline
;
@Mock
private
AssetGenPipelineService
assetGenPipeline
;
@Mock
private
StoryboardPipelineService
storyboardPipeline
;
@Test
void
storyboardGen_shouldPopulateMissingVideoPromptsAfterGeneratingStoryboards
()
{
AgentRunServiceImpl
service
=
new
AgentRunServiceImpl
(
graphRunMapper
,
graphStepMapper
,
episodeMapper
,
sseManager
,
llmService
,
outlinePipeline
,
assetGenPipeline
,
storyboardPipeline
);
Long
runId
=
88L
;
Long
projectId
=
22L
;
Long
tenantId
=
33L
;
GraphRun
runningRun
=
new
GraphRun
();
runningRun
.
setId
(
runId
);
runningRun
.
setStatus
(
"RUNNING"
);
when
(
graphRunMapper
.
selectById
(
runId
)).
thenReturn
(
runningRun
);
when
(
graphRunMapper
.
updateById
(
any
(
GraphRun
.
class
))).
thenReturn
(
1
);
List
<
GraphStep
>
steps
=
List
.
of
(
step
(
runId
,
"decision"
,
0
),
step
(
runId
,
"outline_gen"
,
1
),
step
(
runId
,
"episode_gen"
,
2
),
step
(
runId
,
"supervision_1"
,
3
),
step
(
runId
,
"asset_gen"
,
4
),
step
(
runId
,
"storyboard_gen"
,
5
),
step
(
runId
,
"supervision_2"
,
6
)
);
when
(
graphStepMapper
.
findByRunId
(
runId
)).
thenReturn
(
steps
);
when
(
graphStepMapper
.
updateById
(
any
(
GraphStep
.
class
))).
thenReturn
(
1
);
when
(
llmService
.
chat
(
any
())).
thenReturn
(
"SCORE: A"
);
Outline
outline
=
new
Outline
();
outline
.
setId
(
77L
);
outline
.
setTitle
(
"outline"
);
outline
.
setEpisodeCount
(
1
);
when
(
outlinePipeline
.
generateOutline
(
projectId
,
tenantId
,
"goal"
)).
thenReturn
(
outline
);
Episode
episode
=
new
Episode
();
episode
.
setId
(
101L
);
episode
.
setEpisodeNumber
(
1
);
when
(
outlinePipeline
.
generateEpisodes
(
77L
,
projectId
,
tenantId
)).
thenReturn
(
List
.
of
(
episode
));
when
(
assetGenPipeline
.
extractCharacters
(
projectId
,
tenantId
)).
thenReturn
(
List
.
of
());
when
(
assetGenPipeline
.
extractScenes
(
projectId
,
tenantId
)).
thenReturn
(
List
.
of
());
when
(
storyboardPipeline
.
generateStoryboards
(
101L
,
projectId
,
tenantId
))
.
thenReturn
(
List
.
of
(
new
Storyboard
(),
new
Storyboard
()));
when
(
storyboardPipeline
.
populateMissingVideoPromptsByEpisode
(
101L
,
tenantId
))
.
thenReturn
(
List
.
of
(
storyboardWithPrompt
(),
storyboardWithPrompt
()));
ReflectionTestUtils
.
invokeMethod
(
service
,
"executeRun"
,
runId
,
projectId
,
tenantId
,
1001L
,
"goal"
);
verify
(
storyboardPipeline
).
populateMissingVideoPromptsByEpisode
(
101L
,
tenantId
);
ArgumentCaptor
<
GraphStep
>
stepCaptor
=
ArgumentCaptor
.
forClass
(
GraphStep
.
class
);
verify
(
graphStepMapper
,
atLeastOnce
()).
updateById
(
stepCaptor
.
capture
());
boolean
storyboardOutputMentionedPrompts
=
stepCaptor
.
getAllValues
().
stream
()
.
anyMatch
(
step
->
"storyboard_gen"
.
equals
(
step
.
getStepKey
())
&&
"DONE"
.
equals
(
step
.
getStatus
())
&&
step
.
getOutput
()
!=
null
&&
step
.
getOutput
().
contains
(
"视频提示词"
));
assertTrue
(
storyboardOutputMentionedPrompts
);
}
private
static
GraphStep
step
(
Long
runId
,
String
stepKey
,
int
seq
)
{
GraphStep
step
=
new
GraphStep
();
step
.
setRunId
(
runId
);
step
.
setStepKey
(
stepKey
);
step
.
setStatus
(
"PENDING"
);
step
.
setSeq
(
seq
);
return
step
;
}
private
static
Storyboard
storyboardWithPrompt
()
{
Storyboard
storyboard
=
new
Storyboard
();
storyboard
.
setVideoPrompt
(
"prompt"
);
return
storyboard
;
}
}
yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImplTest.java
View file @
2ce18b88
...
...
@@ -315,7 +315,7 @@ class StoryboardPipelineServiceImplTest {
verify
(
storyboardMapper
,
never
()).
selectById
(
3L
);
ArgumentCaptor
<
Storyboard
>
updateCaptor
=
ArgumentCaptor
.
forClass
(
Storyboard
.
class
);
verify
(
storyboardMapper
,
times
(
2
)).
updateById
(
updateCaptor
.
capture
());
assert
Equals
(
List
.
of
(
1L
,
2L
),
updateCaptor
.
getAllValues
().
stream
().
map
(
Storyboard:
:
getId
).
toList
(
));
assert
True
(
updateCaptor
.
getAllValues
().
stream
().
map
(
Storyboard:
:
getId
).
toList
().
containsAll
(
List
.
of
(
1L
,
2L
)
));
}
private
static
Scene
scene
(
String
name
,
String
sceneType
)
{
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/StoryboardPipelineServiceImpl.java
View file @
2ce18b88
...
...
@@ -32,12 +32,18 @@ import java.util.List;
import
java.util.Map
;
import
java.util.Objects
;
import
java.util.Set
;
import
java.util.concurrent.CompletableFuture
;
import
java.util.concurrent.CompletionException
;
import
java.util.concurrent.ExecutorService
;
import
java.util.concurrent.Executors
;
import
java.util.stream.Collectors
;
@Slf4j
@Service
@RequiredArgsConstructor
public
class
StoryboardPipelineServiceImpl
implements
StoryboardPipelineService
{
private
static
final
int
STORYBOARD_PROMPT_CONCURRENCY
=
3
;
private
static
final
String
SYSTEM_PROMPT_BASE
=
"""
You are a professional storyboard artist.
...
...
@@ -363,8 +369,37 @@ public class StoryboardPipelineServiceImpl implements StoryboardPipelineService
@Override
public
List
<
Storyboard
>
populateMissingVideoPromptsByEpisode
(
Long
episodeId
,
Long
tenantId
)
{
List
<
Storyboard
>
pending
=
storyboardMapper
.
findMissingVideoPromptByEpisode
(
episodeId
,
tenantId
);
for
(
Storyboard
storyboard
:
pending
)
{
generatePrompt
(
storyboard
.
getId
(),
tenantId
);
if
(
pending
.
isEmpty
())
{
return
storyboardMapper
.
findByEpisode
(
episodeId
,
tenantId
);
}
Long
userId
=
UserContext
.
get
();
int
poolSize
=
Math
.
min
(
STORYBOARD_PROMPT_CONCURRENCY
,
pending
.
size
());
ExecutorService
executor
=
Executors
.
newFixedThreadPool
(
poolSize
);
try
{
List
<
CompletableFuture
<
Void
>>
futures
=
pending
.
stream
()
.
map
(
storyboard
->
CompletableFuture
.
runAsync
(()
->
{
if
(
userId
!=
null
)
{
UserContext
.
set
(
userId
);
}
try
{
generatePrompt
(
storyboard
.
getId
(),
tenantId
);
}
finally
{
UserContext
.
clear
();
}
},
executor
))
.
toList
();
for
(
CompletableFuture
<
Void
>
future
:
futures
)
{
future
.
join
();
}
}
catch
(
CompletionException
e
)
{
if
(
e
.
getCause
()
instanceof
RuntimeException
runtimeException
)
{
throw
runtimeException
;
}
throw
e
;
}
finally
{
executor
.
shutdown
();
}
return
storyboardMapper
.
findByEpisode
(
episodeId
,
tenantId
);
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment