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
ee496fd0
Commit
ee496fd0
authored
May 28, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
导出视频合成问题fix8
parent
52c75644
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
79 additions
and
26 deletions
+79
-26
VideoGeneration.tsx
doc/html/src/app/pages/VideoGeneration.tsx
+18
-13
useAi.ts
doc/html/src/hooks/useAi.ts
+1
-1
ai.ts
doc/html/src/lib/api/ai.ts
+4
-2
AssemblyController.java
...ain/java/com/yaoai/api/controller/AssemblyController.java
+13
-2
AssemblyPipelineService.java
...a/com/yaoai/pipeline/service/AssemblyPipelineService.java
+11
-4
AssemblyPipelineServiceImpl.java
...ai/pipeline/service/impl/AssemblyPipelineServiceImpl.java
+32
-4
No files found.
doc/html/src/app/pages/VideoGeneration.tsx
View file @
ee496fd0
...
...
@@ -62,6 +62,7 @@ export function VideoGeneration() {
const
orderedShots
=
storyboards
.
map
((
sb
)
=>
({
sb
,
task
:
getTaskForSb
(
sb
)
}))
.
filter
(({
task
})
=>
task
?.
status
===
"succeeded"
&&
task
.
resultVideoUrl
);
const
orderedShotTaskIds
=
orderedShots
.
map
(({
task
})
=>
task
!
.
id
);
// Stats
const
totalShots
=
storyboards
.
length
;
...
...
@@ -116,6 +117,10 @@ export function VideoGeneration() {
await deleteTask.mutateAsync(taskId);
};
const handleStartAssembly = () => {
startAssembly.mutate(orderedShotTaskIds);
};
// Episode stats helper
const epShotCount = (ep: Episode) => ep.storyboardCount ?? 0;
const epDoneCount = (ep: Episode) =>
...
...
@@ -194,32 +199,32 @@ export function VideoGeneration() {
整集
</button>
)}
{(!assemblyTask || assemblyTask.status === "failed") && (
<button
onClick={() => startAssembly.mutate()}
disabled={startAssembly.isPending || doneShots === 0}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-border text-xs text-foreground hover:bg-muted transition disabled:opacity-50"
>
{startAssembly.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Film className="w-3.5 h-3.5" />}
合成整集
</button>
)}
{(assemblyTask?.status === "running" || assemblyTask?.status === "pending") && (
<span className="flex items-center gap-1.5 px-3 py-1.5 text-xs text-muted-foreground">
<Loader2 className="w-3.5 h-3.5 animate-spin" />合成中...
</span>
)}
{hasAssembly && (
{hasAssembly &&
assemblyTask?.status !== "running" && assemblyTask?.status !== "pending" &&
(
<a
href={assemblyTask!.downloadUrl!}
target="_blank"
rel="noreferrer"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg b
g-green-600 text-white text-xs hover:bg-green-700
transition"
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg b
order border-border text-xs text-foreground hover:bg-muted
transition"
>
<Download className="w-3.5 h-3.5" />
导出整集
下载已合成
</a>
)}
{assemblyTask?.status !== "running" && assemblyTask?.status !== "pending" && (
<button
onClick={handleStartAssembly}
disabled={startAssembly.isPending || doneShots === 0}
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-green-600 text-white text-xs hover:bg-green-700 transition disabled:opacity-50"
>
{startAssembly.isPending ? <Loader2 className="w-3.5 h-3.5 animate-spin" /> : <Download className="w-3.5 h-3.5" />}
导出整集
</button>
)}
</div>
</div>
...
...
doc/html/src/hooks/useAi.ts
View file @
ee496fd0
...
...
@@ -446,7 +446,7 @@ export function useAssemblyTask(projectId: string, episodeId: string) {
export
function
useStartAssembly
(
projectId
:
string
,
episodeId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
(
)
=>
aiApi
.
startAssembly
(
projectId
,
episodeId
),
mutationFn
:
(
videoTaskIds
?:
string
[])
=>
aiApi
.
startAssembly
(
projectId
,
episodeId
,
videoTaskIds
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
assemblyKey
(
projectId
,
episodeId
)
}),
});
}
...
...
doc/html/src/lib/api/ai.ts
View file @
ee496fd0
...
...
@@ -406,8 +406,10 @@ export const aiApi = {
},
// ---- Assembly ----
startAssembly
:
async
(
projectId
:
string
,
episodeId
:
string
):
Promise
<
AssemblyTask
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/episodes/
${
episodeId
}
/assembly`
);
startAssembly
:
async
(
projectId
:
string
,
episodeId
:
string
,
videoTaskIds
?:
string
[]):
Promise
<
AssemblyTask
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/episodes/
${
episodeId
}
/assembly`
,
{
videoTaskIds
,
});
return
r
.
data
.
data
;
},
getAssemblyTask
:
async
(
projectId
:
string
,
episodeId
:
string
):
Promise
<
AssemblyTask
|
null
>
=>
{
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/AssemblyController.java
View file @
ee496fd0
...
...
@@ -10,6 +10,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.*
;
import
java.util.List
;
@Tag
(
name
=
"视频合成"
)
@RestController
@RequestMapping
(
"/projects/{projectId}/episodes/{episodeId}/assembly"
)
...
...
@@ -21,9 +23,15 @@ public class AssemblyController {
@Operation
(
summary
=
"启动合成任务"
)
@PostMapping
public
ApiResponse
<
AssemblyTaskDTO
>
start
(
@PathVariable
Long
projectId
,
@PathVariable
Long
episodeId
)
{
@PathVariable
Long
episodeId
,
@RequestBody
(
required
=
false
)
AssemblyStartRequest
req
)
{
Long
tenantId
=
TenantContext
.
get
();
AssemblyTask
task
=
assemblyPipelineService
.
startAssembly
(
projectId
,
episodeId
,
tenantId
);
AssemblyTask
task
=
assemblyPipelineService
.
startAssembly
(
projectId
,
episodeId
,
tenantId
,
req
==
null
?
null
:
req
.
videoTaskIds
()
);
return
ApiResponse
.
success
(
AssemblyTaskDTO
.
from
(
task
,
null
));
}
...
...
@@ -39,4 +47,7 @@ public class AssemblyController {
String
downloadUrl
=
assemblyPipelineService
.
signedDownloadUrl
(
task
);
return
ApiResponse
.
success
(
AssemblyTaskDTO
.
from
(
task
,
downloadUrl
));
}
public
record
AssemblyStartRequest
(
List
<
Long
>
videoTaskIds
)
{
}
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/AssemblyPipelineService.java
View file @
ee496fd0
...
...
@@ -2,14 +2,21 @@ package com.yaoai.pipeline.service;
import
com.yaoai.domain.entity.AssemblyTask
;
import
java.util.List
;
public
interface
AssemblyPipelineService
{
/** 创建合成任务并异步执行 */
AssemblyTask
startAssembly
(
Long
projectId
,
Long
episodeId
,
Long
tenantId
);
default
AssemblyTask
startAssembly
(
Long
projectId
,
Long
episodeId
,
Long
tenantId
)
{
return
startAssembly
(
projectId
,
episodeId
,
tenantId
,
null
);
}
/**
* Creates an async assembly task. When videoTaskIds is provided, the task
* uses exactly those succeeded video tasks in the given order.
*/
AssemblyTask
startAssembly
(
Long
projectId
,
Long
episodeId
,
Long
tenantId
,
List
<
Long
>
videoTaskIds
);
/** 查询最新合成任务 */
AssemblyTask
getLatest
(
Long
episodeId
,
Long
tenantId
);
/** 生成签名下载 URL(1小时有效) */
String
signedDownloadUrl
(
AssemblyTask
task
);
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/AssemblyPipelineServiceImpl.java
View file @
ee496fd0
...
...
@@ -24,9 +24,12 @@ import java.io.IOException;
import
java.nio.file.Files
;
import
java.nio.file.Path
;
import
java.util.ArrayList
;
import
java.util.LinkedHashMap
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.concurrent.CompletableFuture
;
import
java.util.concurrent.Executor
;
import
java.util.stream.Collectors
;
@Slf4j
@Service
...
...
@@ -61,7 +64,7 @@ public class AssemblyPipelineServiceImpl implements AssemblyPipelineService {
}
@Override
public
AssemblyTask
startAssembly
(
Long
projectId
,
Long
episodeId
,
Long
tenantId
)
{
public
AssemblyTask
startAssembly
(
Long
projectId
,
Long
episodeId
,
Long
tenantId
,
List
<
Long
>
videoTaskIds
)
{
Long
userId
=
resolveUserId
();
AssemblyTask
task
=
new
AssemblyTask
();
task
.
setTenantId
(
tenantId
);
...
...
@@ -75,7 +78,7 @@ public class AssemblyPipelineServiceImpl implements AssemblyPipelineService {
CompletableFuture
.
runAsync
(()
->
{
UserContext
.
set
(
userId
);
try
{
doAssemble
(
taskId
,
episodeId
,
userId
);
doAssemble
(
taskId
,
episodeId
,
userId
,
videoTaskIds
);
}
finally
{
UserContext
.
clear
();
}
...
...
@@ -94,14 +97,16 @@ public class AssemblyPipelineServiceImpl implements AssemblyPipelineService {
return
tosService
.
publicUrl
(
task
.
getResultTosKey
());
}
private
void
doAssemble
(
Long
assemblyTaskId
,
Long
episodeId
,
Long
userId
)
{
private
void
doAssemble
(
Long
assemblyTaskId
,
Long
episodeId
,
Long
userId
,
List
<
Long
>
videoTaskIds
)
{
AssemblyTask
task
=
assemblyTaskMapper
.
selectById
(
assemblyTaskId
);
task
.
setStatus
(
"running"
);
assemblyTaskMapper
.
updateById
(
task
);
Path
outputFile
=
null
;
try
{
List
<
String
>
urls
=
buildAssemblyUrls
(
episodeId
,
task
.
getTenantId
());
List
<
String
>
urls
=
videoTaskIds
!=
null
&&
!
videoTaskIds
.
isEmpty
()
?
buildAssemblyUrlsFromTaskIds
(
videoTaskIds
,
episodeId
,
task
.
getTenantId
())
:
buildAssemblyUrls
(
episodeId
,
task
.
getTenantId
());
if
(
urls
.
isEmpty
())
{
fail
(
task
,
"没有可用的视频片段(需先生成每个镜头的视频)"
);
return
;
...
...
@@ -143,6 +148,29 @@ public class AssemblyPipelineServiceImpl implements AssemblyPipelineService {
}
}
private
List
<
String
>
buildAssemblyUrlsFromTaskIds
(
List
<
Long
>
videoTaskIds
,
Long
episodeId
,
Long
tenantId
)
{
List
<
AiTask
>
tasks
=
aiTaskMapper
.
selectBatchIds
(
videoTaskIds
);
Map
<
Long
,
AiTask
>
taskById
=
tasks
.
stream
()
.
collect
(
Collectors
.
toMap
(
AiTask:
:
getId
,
task
->
task
,
(
left
,
right
)
->
left
,
LinkedHashMap:
:
new
));
List
<
String
>
urls
=
new
ArrayList
<>();
for
(
Long
taskId
:
videoTaskIds
)
{
AiTask
task
=
taskById
.
get
(
taskId
);
if
(
task
==
null
||
!
tenantId
.
equals
(
task
.
getTenantId
())
||
!
episodeId
.
equals
(
task
.
getEpisodeId
())
||
!
"succeeded"
.
equals
(
task
.
getStatus
())
||
task
.
getResultVideoUrl
()
==
null
||
task
.
getResultVideoUrl
().
isBlank
())
{
continue
;
}
urls
.
add
(
task
.
getResultVideoUrl
());
}
if
(
urls
.
size
()
!=
videoTaskIds
.
size
())
{
throw
new
IllegalStateException
(
"合成视频任务数量不一致:请求 "
+
videoTaskIds
.
size
()
+
" 个,实际可用 "
+
urls
.
size
()
+
" 个"
);
}
return
urls
;
}
private
List
<
String
>
buildAssemblyUrls
(
Long
episodeId
,
Long
tenantId
)
{
List
<
Storyboard
>
storyboards
=
storyboardMapper
.
findByEpisode
(
episodeId
,
tenantId
);
List
<
String
>
urls
=
new
ArrayList
<>();
...
...
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