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
787ae772
Commit
787ae772
authored
Jun 03, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
客户端加入首未帧和道具修改
parent
232de8ef
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
12 changed files
with
189 additions
and
10 deletions
+189
-10
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+0
-0
useAi.ts
doc/html/src/hooks/useAi.ts
+9
-0
ai.ts
doc/html/src/lib/api/ai.ts
+21
-0
SeedancePromptBuilder.java
...va/com/yaoai/ai/providers/util/SeedancePromptBuilder.java
+46
-3
StoryboardController.java
...n/java/com/yaoai/api/controller/StoryboardController.java
+50
-0
VideoTaskController.java
...in/java/com/yaoai/api/controller/VideoTaskController.java
+6
-2
StoryboardDTO.java
...api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java
+8
-0
StructuredVideoGenerateRequest.java
.../com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
+6
-0
V21__storyboard_frame_reference_images.sql
...s/db/migration/V21__storyboard_frame_reference_images.sql
+11
-0
Storyboard.java
...ain/src/main/java/com/yaoai/domain/entity/Storyboard.java
+4
-0
StructuredVideoSubmission.java
...ava/com/yaoai/pipeline/dto/StructuredVideoSubmission.java
+4
-0
VideoTaskPipelineServiceImpl.java
...i/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
+24
-5
No files found.
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
787ae772
This diff is collapsed.
Click to expand it.
doc/html/src/hooks/useAi.ts
View file @
787ae772
...
@@ -340,6 +340,15 @@ export function useUpdateStoryboard(projectId: string, episodeId: string) {
...
@@ -340,6 +340,15 @@ export function useUpdateStoryboard(projectId: string, episodeId: string) {
});
});
}
}
export
function
useUploadStoryboardFrameImage
(
projectId
:
string
,
episodeId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
({
id
,
frameType
,
file
}:
{
id
:
string
;
frameType
:
"first"
|
"last"
;
file
:
File
})
=>
aiApi
.
uploadStoryboardFrameImage
(
projectId
,
id
,
frameType
,
file
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
storyboardsKey
(
projectId
,
episodeId
)
}),
});
}
export
function
useDeleteStoryboard
(
projectId
:
string
,
episodeId
:
string
)
{
export
function
useDeleteStoryboard
(
projectId
:
string
,
episodeId
:
string
)
{
const
qc
=
useQueryClient
();
const
qc
=
useQueryClient
();
return
useMutation
({
return
useMutation
({
...
...
doc/html/src/lib/api/ai.ts
View file @
787ae772
...
@@ -47,6 +47,10 @@ export interface Storyboard {
...
@@ -47,6 +47,10 @@ export interface Storyboard {
sceneImageKey
?:
string
|
null
;
sceneImageKey
?:
string
|
null
;
propImageKeys
?:
string
[]
|
null
;
propImageKeys
?:
string
[]
|
null
;
styleImageKey
?:
string
|
null
;
styleImageKey
?:
string
|
null
;
firstFrameImageKey
?:
string
|
null
;
firstFrameImageUrl
?:
string
|
null
;
lastFrameImageKey
?:
string
|
null
;
lastFrameImageUrl
?:
string
|
null
;
}
}
export
interface
ShotAsset
{
export
interface
ShotAsset
{
...
@@ -157,6 +161,8 @@ export interface StructuredVideoRequest {
...
@@ -157,6 +161,8 @@ export interface StructuredVideoRequest {
sceneImageKey
?:
string
|
null
;
sceneImageKey
?:
string
|
null
;
propImageKeys
?:
string
[]
|
null
;
propImageKeys
?:
string
[]
|
null
;
styleImageKey
?:
string
|
null
;
styleImageKey
?:
string
|
null
;
firstFrameImageKey
?:
string
|
null
;
lastFrameImageKey
?:
string
|
null
;
/** V17:单段 freeform 提示词(与 userPrompt 二选一,优先使用) */
/** V17:单段 freeform 提示词(与 userPrompt 二选一,优先使用) */
freeformPrompt
?:
string
|
null
;
freeformPrompt
?:
string
|
null
;
userPrompt
?:
{
userPrompt
?:
{
...
@@ -234,6 +240,21 @@ export const aiApi = {
...
@@ -234,6 +240,21 @@ export const aiApi = {
const
r
=
await
apiClient
.
put
(
`/projects/
${
projectId
}
/storyboards/
${
storyboardId
}
`
,
patch
);
const
r
=
await
apiClient
.
put
(
`/projects/
${
projectId
}
/storyboards/
${
storyboardId
}
`
,
patch
);
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
uploadStoryboardFrameImage
:
async
(
projectId
:
string
,
storyboardId
:
string
,
frameType
:
"first"
|
"last"
,
file
:
File
):
Promise
<
Storyboard
>
=>
{
const
form
=
new
FormData
();
form
.
append
(
"file"
,
file
);
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/storyboards/
${
storyboardId
}
/frame-image?frameType=
${
frameType
}
`
,
form
,
{
headers
:
{
"Content-Type"
:
"multipart/form-data"
},
timeout
:
600
_000
}
);
return
r
.
data
.
data
;
},
deleteStoryboard
:
async
(
projectId
:
string
,
storyboardId
:
string
):
Promise
<
void
>
=>
{
deleteStoryboard
:
async
(
projectId
:
string
,
storyboardId
:
string
):
Promise
<
void
>
=>
{
await
apiClient
.
delete
(
`/projects/
${
projectId
}
/storyboards/
${
storyboardId
}
`
);
await
apiClient
.
delete
(
`/projects/
${
projectId
}
/storyboards/
${
storyboardId
}
`
);
},
},
...
...
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/util/SeedancePromptBuilder.java
View file @
787ae772
...
@@ -8,7 +8,7 @@ import java.util.List;
...
@@ -8,7 +8,7 @@ import java.util.List;
*
*
* 图片顺序固定(即 @图N 标号):
* 图片顺序固定(即 @图N 标号):
* @图1..@图K = 角色(K = 角色数量),@图K+1 = 场景,
* @图1..@图K = 角色(K = 角色数量),@图K+1 = 场景,
* @图K+2.. = 道具(按顺序),最后一张 = 风格图(如有)
* @图K+2.. = 道具(按顺序),
之后是首/尾帧参考图,
最后一张 = 风格图(如有)
*/
*/
public
final
class
SeedancePromptBuilder
{
public
final
class
SeedancePromptBuilder
{
...
@@ -24,8 +24,14 @@ public final class SeedancePromptBuilder {
...
@@ -24,8 +24,14 @@ public final class SeedancePromptBuilder {
/** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */
/** V17:单段 freeform 提示词模式(前端单输入框后的主要入口)。 */
public
static
String
buildPromptFreeform
(
String
freeformBody
,
int
characterCount
,
public
static
String
buildPromptFreeform
(
String
freeformBody
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
return
buildPromptFreeform
(
freeformBody
,
characterCount
,
hasSceneImage
,
propCount
,
0
,
hasStyleImage
);
}
public
static
String
buildPromptFreeform
(
String
freeformBody
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
int
frameCount
,
boolean
hasStyleImage
)
{
StringBuilder
sb
=
new
StringBuilder
();
StringBuilder
sb
=
new
StringBuilder
();
appendImageHeader
(
sb
,
characterCount
,
hasSceneImage
,
propCount
,
hasStyleImage
);
appendImageHeader
(
sb
,
characterCount
,
hasSceneImage
,
propCount
,
frameCount
,
hasStyleImage
);
sb
.
append
(
"\n分镜内容:\n"
);
sb
.
append
(
"\n分镜内容:\n"
);
if
(
freeformBody
!=
null
&&
!
freeformBody
.
isBlank
())
{
if
(
freeformBody
!=
null
&&
!
freeformBody
.
isBlank
())
{
sb
.
append
(
freeformBody
.
trim
()).
append
(
'\n'
);
sb
.
append
(
freeformBody
.
trim
()).
append
(
'\n'
);
...
@@ -36,8 +42,14 @@ public final class SeedancePromptBuilder {
...
@@ -36,8 +42,14 @@ public final class SeedancePromptBuilder {
public
static
String
buildPrompt
(
UserPromptParts
parts
,
int
characterCount
,
public
static
String
buildPrompt
(
UserPromptParts
parts
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
return
buildPrompt
(
parts
,
characterCount
,
hasSceneImage
,
propCount
,
0
,
hasStyleImage
);
}
public
static
String
buildPrompt
(
UserPromptParts
parts
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
int
frameCount
,
boolean
hasStyleImage
)
{
StringBuilder
sb
=
new
StringBuilder
();
StringBuilder
sb
=
new
StringBuilder
();
appendImageHeader
(
sb
,
characterCount
,
hasSceneImage
,
propCount
,
hasStyleImage
);
appendImageHeader
(
sb
,
characterCount
,
hasSceneImage
,
propCount
,
frameCount
,
hasStyleImage
);
sb
.
append
(
"\n分镜内容:\n"
);
sb
.
append
(
"\n分镜内容:\n"
);
appendIfPresent
(
sb
,
"人物动作"
,
parts
.
characterAction
());
appendIfPresent
(
sb
,
"人物动作"
,
parts
.
characterAction
());
...
@@ -56,6 +68,12 @@ public final class SeedancePromptBuilder {
...
@@ -56,6 +68,12 @@ public final class SeedancePromptBuilder {
*/
*/
private
static
void
appendImageHeader
(
StringBuilder
sb
,
int
characterCount
,
private
static
void
appendImageHeader
(
StringBuilder
sb
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
boolean
hasSceneImage
,
int
propCount
,
boolean
hasStyleImage
)
{
appendImageHeader
(
sb
,
characterCount
,
hasSceneImage
,
propCount
,
0
,
hasStyleImage
);
}
private
static
void
appendImageHeader
(
StringBuilder
sb
,
int
characterCount
,
boolean
hasSceneImage
,
int
propCount
,
int
frameCount
,
boolean
hasStyleImage
)
{
int
idx
=
0
;
int
idx
=
0
;
if
(
characterCount
==
1
)
{
if
(
characterCount
==
1
)
{
sb
.
append
(
"@图"
).
append
(
idx
+
1
)
sb
.
append
(
"@图"
).
append
(
idx
+
1
)
...
@@ -83,6 +101,16 @@ public final class SeedancePromptBuilder {
...
@@ -83,6 +101,16 @@ public final class SeedancePromptBuilder {
}
}
idx
+=
propCount
;
idx
+=
propCount
;
}
}
if
(
frameCount
>
0
)
{
if
(
frameCount
==
1
)
{
sb
.
append
(
"@图"
).
append
(
idx
+
1
)
.
append
(
" 是分镜首帧或尾帧参考图,请作为画面起止状态参考,不要生硬复制成静态画面。\n"
);
}
else
{
sb
.
append
(
"@图"
).
append
(
idx
+
1
).
append
(
" 是首帧参考图,@图"
).
append
(
idx
+
2
)
.
append
(
" 是尾帧参考图,请参考起始和结束画面状态,让视频动作自然衔接。\n"
);
}
idx
+=
frameCount
;
}
if
(
hasStyleImage
)
{
if
(
hasStyleImage
)
{
sb
.
append
(
"@图"
).
append
(
idx
+
1
)
sb
.
append
(
"@图"
).
append
(
idx
+
1
)
.
append
(
" 是风格参考图,请参考整体美术风格、色彩和质感。\n"
);
.
append
(
" 是风格参考图,请参考整体美术风格、色彩和质感。\n"
);
...
@@ -111,6 +139,14 @@ public final class SeedancePromptBuilder {
...
@@ -111,6 +139,14 @@ public final class SeedancePromptBuilder {
String
sceneImageKey
,
String
sceneImageKey
,
List
<
String
>
propImageKeys
,
List
<
String
>
propImageKeys
,
String
styleImageKey
)
{
String
styleImageKey
)
{
return
buildOrderedImageKeys
(
characterImageKeys
,
sceneImageKey
,
propImageKeys
,
null
,
styleImageKey
);
}
public
static
List
<
String
>
buildOrderedImageKeys
(
List
<
String
>
characterImageKeys
,
String
sceneImageKey
,
List
<
String
>
propImageKeys
,
List
<
String
>
frameImageKeys
,
String
styleImageKey
)
{
List
<
String
>
keys
=
new
ArrayList
<>();
List
<
String
>
keys
=
new
ArrayList
<>();
if
(
characterImageKeys
!=
null
)
{
if
(
characterImageKeys
!=
null
)
{
for
(
String
k
:
characterImageKeys
)
{
for
(
String
k
:
characterImageKeys
)
{
...
@@ -129,6 +165,13 @@ public final class SeedancePromptBuilder {
...
@@ -129,6 +165,13 @@ public final class SeedancePromptBuilder {
}
}
}
}
}
}
if
(
frameImageKeys
!=
null
)
{
for
(
String
k
:
frameImageKeys
)
{
if
(
k
!=
null
&&
!
k
.
isBlank
())
{
keys
.
add
(
k
);
}
}
}
if
(
styleImageKey
!=
null
&&
!
styleImageKey
.
isBlank
())
{
if
(
styleImageKey
!=
null
&&
!
styleImageKey
.
isBlank
())
{
keys
.
add
(
styleImageKey
);
keys
.
add
(
styleImageKey
);
}
}
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/StoryboardController.java
View file @
787ae772
...
@@ -2,16 +2,23 @@ package com.yaoai.api.controller;
...
@@ -2,16 +2,23 @@ package com.yaoai.api.controller;
import
com.yaoai.api.dto.ai.StoryboardDTO
;
import
com.yaoai.api.dto.ai.StoryboardDTO
;
import
com.yaoai.api.dto.ai.ShotAssetDTO
;
import
com.yaoai.api.dto.ai.ShotAssetDTO
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.domain.entity.Storyboard
;
import
com.yaoai.domain.entity.Storyboard
;
import
com.yaoai.domain.mapper.StoryboardMapper
;
import
com.yaoai.pipeline.service.ShotAssetService
;
import
com.yaoai.pipeline.service.ShotAssetService
;
import
com.yaoai.pipeline.service.StoryboardPipelineService
;
import
com.yaoai.pipeline.service.StoryboardPipelineService
;
import
com.yaoai.security.context.TenantContext
;
import
com.yaoai.security.context.TenantContext
;
import
com.yaoai.storage.service.TosService
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.Operation
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
io.swagger.v3.oas.annotations.tags.Tag
;
import
lombok.RequiredArgsConstructor
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.http.MediaType
;
import
org.springframework.web.bind.annotation.*
;
import
org.springframework.web.bind.annotation.*
;
import
org.springframework.web.multipart.MultipartFile
;
import
java.io.IOException
;
import
java.util.List
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.stream.Collectors
;
import
java.util.stream.Collectors
;
...
@@ -24,6 +31,8 @@ public class StoryboardController {
...
@@ -24,6 +31,8 @@ public class StoryboardController {
private
final
StoryboardPipelineService
storyboardService
;
private
final
StoryboardPipelineService
storyboardService
;
private
final
ShotAssetService
shotAssetService
;
private
final
ShotAssetService
shotAssetService
;
private
final
StoryboardMapper
storyboardMapper
;
private
final
TosService
tosService
;
@Operation
(
summary
=
"AI 批量生成分集分镜"
)
@Operation
(
summary
=
"AI 批量生成分集分镜"
)
@PostMapping
(
"/episodes/{episodeId}/storyboards/generate"
)
@PostMapping
(
"/episodes/{episodeId}/storyboards/generate"
)
...
@@ -79,6 +88,47 @@ public class StoryboardController {
...
@@ -79,6 +88,47 @@ public class StoryboardController {
return
ApiResponse
.
success
(
StoryboardDTO
.
from
(
storyboardService
.
update
(
storyboardId
,
tenantId
,
req
)));
return
ApiResponse
.
success
(
StoryboardDTO
.
from
(
storyboardService
.
update
(
storyboardId
,
tenantId
,
req
)));
}
}
@Operation
(
summary
=
"上传分镜首尾帧参考图"
)
@PostMapping
(
value
=
"/storyboards/{storyboardId}/frame-image"
,
consumes
=
MediaType
.
MULTIPART_FORM_DATA_VALUE
)
public
ApiResponse
<
StoryboardDTO
>
uploadFrameImage
(
@PathVariable
Long
projectId
,
@PathVariable
Long
storyboardId
,
@RequestParam
(
"frameType"
)
String
frameType
,
@RequestPart
(
"file"
)
MultipartFile
file
)
throws
IOException
{
Long
tenantId
=
TenantContext
.
get
();
Storyboard
storyboard
=
storyboardMapper
.
selectById
(
storyboardId
);
if
(
storyboard
==
null
||
!
tenantId
.
equals
(
storyboard
.
getTenantId
())
||
!
projectId
.
equals
(
storyboard
.
getProjectId
()))
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"分镜不存在"
);
}
if
(!
"first"
.
equals
(
frameType
)
&&
!
"last"
.
equals
(
frameType
))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"frameType 仅支持 first / last"
);
}
if
(
file
==
null
||
file
.
isEmpty
())
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"图片文件不能为空"
);
}
String
contentType
=
file
.
getContentType
()
!=
null
?
file
.
getContentType
()
:
"application/octet-stream"
;
if
(!
contentType
.
startsWith
(
"image/"
))
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"请上传图片文件"
);
}
String
key
=
TosService
.
buildKey
(
tenantId
,
projectId
,
"storyboard-frames/"
+
frameType
,
file
.
getOriginalFilename
());
tosService
.
upload
(
key
,
file
.
getInputStream
(),
file
.
getSize
(),
contentType
);
String
url
=
tosService
.
publicUrl
(
key
);
Storyboard
patch
=
new
Storyboard
();
patch
.
setId
(
storyboardId
);
if
(
"first"
.
equals
(
frameType
))
{
patch
.
setFirstFrameImageKey
(
key
);
patch
.
setFirstFrameImageUrl
(
url
);
}
else
{
patch
.
setLastFrameImageKey
(
key
);
patch
.
setLastFrameImageUrl
(
url
);
}
storyboardMapper
.
updateById
(
patch
);
return
ApiResponse
.
success
(
StoryboardDTO
.
from
(
storyboardMapper
.
selectById
(
storyboardId
)));
}
@Operation
(
summary
=
"删除分镜"
)
@Operation
(
summary
=
"删除分镜"
)
@DeleteMapping
(
"/storyboards/{storyboardId}"
)
@DeleteMapping
(
"/storyboards/{storyboardId}"
)
public
ApiResponse
<
Void
>
delete
(
@PathVariable
Long
projectId
,
public
ApiResponse
<
Void
>
delete
(
@PathVariable
Long
projectId
,
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/VideoTaskController.java
View file @
787ae772
...
@@ -46,8 +46,10 @@ public class VideoTaskController {
...
@@ -46,8 +46,10 @@ public class VideoTaskController {
||
(
req
.
getCharacterImageKey
()
!=
null
&&
!
req
.
getCharacterImageKey
().
isBlank
());
||
(
req
.
getCharacterImageKey
()
!=
null
&&
!
req
.
getCharacterImageKey
().
isBlank
());
boolean
hasScene
=
req
.
getSceneImageKey
()
!=
null
&&
!
req
.
getSceneImageKey
().
isBlank
();
boolean
hasScene
=
req
.
getSceneImageKey
()
!=
null
&&
!
req
.
getSceneImageKey
().
isBlank
();
boolean
hasProp
=
req
.
getPropImageKeys
()
!=
null
&&
!
req
.
getPropImageKeys
().
isEmpty
();
boolean
hasProp
=
req
.
getPropImageKeys
()
!=
null
&&
!
req
.
getPropImageKeys
().
isEmpty
();
if
(!
hasCharacter
&&
!
hasScene
&&
!
hasProp
)
{
boolean
hasFrame
=
(
req
.
getFirstFrameImageKey
()
!=
null
&&
!
req
.
getFirstFrameImageKey
().
isBlank
())
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"请至少选择一张参考图(角色/场景/道具任一)"
);
||
(
req
.
getLastFrameImageKey
()
!=
null
&&
!
req
.
getLastFrameImageKey
().
isBlank
());
if
(!
hasCharacter
&&
!
hasScene
&&
!
hasProp
&&
!
hasFrame
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"请至少选择一张参考图(角色/场景/道具/首尾帧任一)"
);
}
}
Long
tenantId
=
TenantContext
.
get
();
Long
tenantId
=
TenantContext
.
get
();
...
@@ -60,6 +62,8 @@ public class VideoTaskController {
...
@@ -60,6 +62,8 @@ public class VideoTaskController {
.
sceneImageKey
(
req
.
getSceneImageKey
())
.
sceneImageKey
(
req
.
getSceneImageKey
())
.
propImageKeys
(
req
.
getPropImageKeys
())
.
propImageKeys
(
req
.
getPropImageKeys
())
.
styleImageKey
(
req
.
getStyleImageKey
())
.
styleImageKey
(
req
.
getStyleImageKey
())
.
firstFrameImageKey
(
req
.
getFirstFrameImageKey
())
.
lastFrameImageKey
(
req
.
getLastFrameImageKey
())
.
characterAction
(
up
!=
null
?
up
.
getCharacterAction
()
:
null
)
.
characterAction
(
up
!=
null
?
up
.
getCharacterAction
()
:
null
)
.
sceneEvent
(
up
!=
null
?
up
.
getSceneEvent
()
:
null
)
.
sceneEvent
(
up
!=
null
?
up
.
getSceneEvent
()
:
null
)
.
cameraMovement
(
up
!=
null
?
up
.
getCameraMovement
()
:
null
)
.
cameraMovement
(
up
!=
null
?
up
.
getCameraMovement
()
:
null
)
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StoryboardDTO.java
View file @
787ae772
...
@@ -41,6 +41,10 @@ public class StoryboardDTO {
...
@@ -41,6 +41,10 @@ public class StoryboardDTO {
private
String
sceneImageKey
;
private
String
sceneImageKey
;
private
List
<
String
>
propImageKeys
;
private
List
<
String
>
propImageKeys
;
private
String
styleImageKey
;
private
String
styleImageKey
;
private
String
firstFrameImageKey
;
private
String
firstFrameImageUrl
;
private
String
lastFrameImageKey
;
private
String
lastFrameImageUrl
;
public
static
StoryboardDTO
from
(
Storyboard
s
)
{
public
static
StoryboardDTO
from
(
Storyboard
s
)
{
StoryboardDTO
dto
=
new
StoryboardDTO
();
StoryboardDTO
dto
=
new
StoryboardDTO
();
...
@@ -68,6 +72,10 @@ public class StoryboardDTO {
...
@@ -68,6 +72,10 @@ public class StoryboardDTO {
dto
.
setSceneImageKey
(
s
.
getSceneImageKey
());
dto
.
setSceneImageKey
(
s
.
getSceneImageKey
());
dto
.
setPropImageKeys
(
parseList
(
s
.
getPropImageKeys
(),
"propImageKeys"
,
s
.
getId
()));
dto
.
setPropImageKeys
(
parseList
(
s
.
getPropImageKeys
(),
"propImageKeys"
,
s
.
getId
()));
dto
.
setStyleImageKey
(
s
.
getStyleImageKey
());
dto
.
setStyleImageKey
(
s
.
getStyleImageKey
());
dto
.
setFirstFrameImageKey
(
s
.
getFirstFrameImageKey
());
dto
.
setFirstFrameImageUrl
(
s
.
getFirstFrameImageUrl
());
dto
.
setLastFrameImageKey
(
s
.
getLastFrameImageKey
());
dto
.
setLastFrameImageUrl
(
s
.
getLastFrameImageUrl
());
return
dto
;
return
dto
;
}
}
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
View file @
787ae772
...
@@ -46,6 +46,12 @@ public class StructuredVideoGenerateRequest {
...
@@ -46,6 +46,12 @@ public class StructuredVideoGenerateRequest {
/** 风格图(可选,固定为最后一张) */
/** 风格图(可选,固定为最后一张) */
private
String
styleImageKey
;
private
String
styleImageKey
;
/** 首帧参考图(可选,作为视频生成参考图参与,不强制模型首帧锁定) */
private
String
firstFrameImageKey
;
/** 尾帧参考图(可选,作为视频生成参考图参与,不强制模型尾帧锁定) */
private
String
lastFrameImageKey
;
/** 视频时长(秒) */
/** 视频时长(秒) */
@Min
(
value
=
5
,
message
=
"视频时长最短 5 秒"
)
@Min
(
value
=
5
,
message
=
"视频时长最短 5 秒"
)
@Max
(
value
=
15
,
message
=
"视频时长最长 15 秒"
)
@Max
(
value
=
15
,
message
=
"视频时长最长 15 秒"
)
...
...
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/db/migration/V21__storyboard_frame_reference_images.sql
0 → 100644
View file @
787ae772
-- V21 分镜首尾帧参考图
ALTER
TABLE
storyboards
ADD
COLUMN
first_frame_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'该分镜首帧参考图 TOS key'
,
ADD
COLUMN
first_frame_image_url
VARCHAR
(
1024
)
DEFAULT
NULL
COMMENT
'该分镜首帧参考图公开 URL'
,
ADD
COLUMN
last_frame_image_key
VARCHAR
(
512
)
DEFAULT
NULL
COMMENT
'该分镜尾帧参考图 TOS key'
,
ADD
COLUMN
last_frame_image_url
VARCHAR
(
1024
)
DEFAULT
NULL
COMMENT
'该分镜尾帧参考图公开 URL'
;
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/entity/Storyboard.java
View file @
787ae772
...
@@ -43,6 +43,10 @@ public class Storyboard {
...
@@ -43,6 +43,10 @@ public class Storyboard {
private
String
sceneImageKey
;
private
String
sceneImageKey
;
private
String
propImageKeys
;
private
String
propImageKeys
;
private
String
styleImageKey
;
private
String
styleImageKey
;
private
String
firstFrameImageKey
;
private
String
firstFrameImageUrl
;
private
String
lastFrameImageKey
;
private
String
lastFrameImageUrl
;
/** draft / review / approved / generating / generated / failed */
/** draft / review / approved / generating / generated / failed */
private
String
status
;
private
String
status
;
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/dto/StructuredVideoSubmission.java
View file @
787ae772
...
@@ -29,6 +29,10 @@ public class StructuredVideoSubmission {
...
@@ -29,6 +29,10 @@ public class StructuredVideoSubmission {
private
List
<
String
>
propImageKeys
;
private
List
<
String
>
propImageKeys
;
/** 风格图 TOS key(可空,永远是最后一张) */
/** 风格图 TOS key(可空,永远是最后一张) */
private
String
styleImageKey
;
private
String
styleImageKey
;
/** 首帧参考图 TOS key(可空,作为额外参考图参与) */
private
String
firstFrameImageKey
;
/** 尾帧参考图 TOS key(可空,作为额外参考图参与) */
private
String
lastFrameImageKey
;
/** 用户原始结构化输入(5 段或单段 freeform),用于落库存证 */
/** 用户原始结构化输入(5 段或单段 freeform),用于落库存证 */
private
Map
<
String
,
String
>
userPromptRaw
;
private
Map
<
String
,
String
>
userPromptRaw
;
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
View file @
787ae772
...
@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
...
@@ -23,6 +23,7 @@ import lombok.RequiredArgsConstructor;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.Collections
;
import
java.util.List
;
import
java.util.List
;
...
@@ -188,18 +189,20 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -188,18 +189,20 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
}
}
boolean
hasScene
=
s
.
getSceneImageKey
()
!=
null
&&
!
s
.
getSceneImageKey
().
isBlank
();
boolean
hasScene
=
s
.
getSceneImageKey
()
!=
null
&&
!
s
.
getSceneImageKey
().
isBlank
();
int
propCount
=
s
.
getPropImageKeys
()
!=
null
?
s
.
getPropImageKeys
().
size
()
:
0
;
int
propCount
=
s
.
getPropImageKeys
()
!=
null
?
s
.
getPropImageKeys
().
size
()
:
0
;
List
<
String
>
frameImageKeys
=
frameImageKeys
(
s
);
int
frameCount
=
frameImageKeys
.
size
();
boolean
hasStyle
=
s
.
getStyleImageKey
()
!=
null
&&
!
s
.
getStyleImageKey
().
isBlank
();
boolean
hasStyle
=
s
.
getStyleImageKey
()
!=
null
&&
!
s
.
getStyleImageKey
().
isBlank
();
// 至少要有一张参考图(角色/场景/道具任一),否则前端不应允许提交
// 至少要有一张参考图(角色/场景/道具
/首尾帧
任一),否则前端不应允许提交
if
(
characterKeys
.
isEmpty
()
&&
!
hasScene
&&
propCount
==
0
)
{
if
(
characterKeys
.
isEmpty
()
&&
!
hasScene
&&
propCount
==
0
&&
frameCount
==
0
)
{
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"至少需要一张参考图(角色/场景/道具任一)"
);
throw
new
BizException
(
ErrorCode
.
INVALID_PARAM
,
"至少需要一张参考图(角色/场景/道具
/首尾帧
任一)"
);
}
}
// 1. 拼装 final prompt + 图片顺序
// 1. 拼装 final prompt + 图片顺序
String
finalPrompt
;
String
finalPrompt
;
if
(
s
.
getFreeformPrompt
()
!=
null
&&
!
s
.
getFreeformPrompt
().
isBlank
())
{
if
(
s
.
getFreeformPrompt
()
!=
null
&&
!
s
.
getFreeformPrompt
().
isBlank
())
{
finalPrompt
=
SeedancePromptBuilder
.
buildPromptFreeform
(
finalPrompt
=
SeedancePromptBuilder
.
buildPromptFreeform
(
s
.
getFreeformPrompt
(),
characterKeys
.
size
(),
hasScene
,
propCount
,
hasStyle
);
s
.
getFreeformPrompt
(),
characterKeys
.
size
(),
hasScene
,
propCount
,
frameCount
,
hasStyle
);
}
else
{
}
else
{
finalPrompt
=
SeedancePromptBuilder
.
buildPrompt
(
finalPrompt
=
SeedancePromptBuilder
.
buildPrompt
(
new
SeedancePromptBuilder
.
UserPromptParts
(
new
SeedancePromptBuilder
.
UserPromptParts
(
...
@@ -212,6 +215,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -212,6 +215,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
characterKeys
.
size
(),
characterKeys
.
size
(),
hasScene
,
hasScene
,
propCount
,
propCount
,
frameCount
,
hasStyle
hasStyle
);
);
}
}
...
@@ -219,6 +223,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -219,6 +223,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
characterKeys
,
characterKeys
,
s
.
getSceneImageKey
(),
s
.
getSceneImageKey
(),
s
.
getPropImageKeys
(),
s
.
getPropImageKeys
(),
frameImageKeys
,
s
.
getStyleImageKey
()
s
.
getStyleImageKey
()
);
);
...
@@ -260,7 +265,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -260,7 +265,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
java
.
util
.
Map
.
of
(
java
.
util
.
Map
.
of
(
"durationSeconds"
,
durationOrDefault
(
s
.
getDurationSeconds
()),
"durationSeconds"
,
durationOrDefault
(
s
.
getDurationSeconds
()),
"ratio"
,
s
.
getRatio
()
!=
null
?
s
.
getRatio
()
:
"16:9"
,
"ratio"
,
s
.
getRatio
()
!=
null
?
s
.
getRatio
()
:
"16:9"
,
"generateAudio"
,
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
())
"generateAudio"
,
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
()),
"frameImageKeys"
,
frameImageKeys
)
)
);
);
log
.
info
(
"Structured video task created: id={}, characters={}, images={}, duration={}s, ratio={}"
,
log
.
info
(
"Structured video task created: id={}, characters={}, images={}, duration={}s, ratio={}"
,
...
@@ -287,6 +293,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -287,6 +293,8 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
patch
.
setSceneImageKey
(
s
.
getSceneImageKey
());
patch
.
setSceneImageKey
(
s
.
getSceneImageKey
());
patch
.
setPropImageKeys
(
toJson
(
s
.
getPropImageKeys
()));
patch
.
setPropImageKeys
(
toJson
(
s
.
getPropImageKeys
()));
patch
.
setStyleImageKey
(
s
.
getStyleImageKey
());
patch
.
setStyleImageKey
(
s
.
getStyleImageKey
());
patch
.
setFirstFrameImageKey
(
s
.
getFirstFrameImageKey
());
patch
.
setLastFrameImageKey
(
s
.
getLastFrameImageKey
());
patch
.
setDurationSeconds
(
durationOrDefault
(
s
.
getDurationSeconds
()));
patch
.
setDurationSeconds
(
durationOrDefault
(
s
.
getDurationSeconds
()));
String
editablePrompt
=
s
.
getFreeformPrompt
()
!=
null
&&
!
s
.
getFreeformPrompt
().
isBlank
()
String
editablePrompt
=
s
.
getFreeformPrompt
()
!=
null
&&
!
s
.
getFreeformPrompt
().
isBlank
()
?
s
.
getFreeformPrompt
()
?
s
.
getFreeformPrompt
()
...
@@ -295,6 +303,17 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -295,6 +303,17 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
storyboardMapper
.
updateById
(
patch
);
storyboardMapper
.
updateById
(
patch
);
}
}
private
List
<
String
>
frameImageKeys
(
StructuredVideoSubmission
s
)
{
List
<
String
>
keys
=
new
ArrayList
<>(
2
);
if
(
s
.
getFirstFrameImageKey
()
!=
null
&&
!
s
.
getFirstFrameImageKey
().
isBlank
())
{
keys
.
add
(
s
.
getFirstFrameImageKey
());
}
if
(
s
.
getLastFrameImageKey
()
!=
null
&&
!
s
.
getLastFrameImageKey
().
isBlank
())
{
keys
.
add
(
s
.
getLastFrameImageKey
());
}
return
keys
;
}
private
Integer
durationOrDefault
(
Integer
durationSeconds
)
{
private
Integer
durationOrDefault
(
Integer
durationSeconds
)
{
if
(
durationSeconds
==
null
||
durationSeconds
<=
0
)
{
if
(
durationSeconds
==
null
||
durationSeconds
<=
0
)
{
return
5
;
return
5
;
...
...
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