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
69bfd17b
Commit
69bfd17b
authored
Apr 28, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Agent制作优化上下文错误和剧本集数可修改
parent
824a0e6d
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
158 additions
and
4 deletions
+158
-4
ProjectDetail.tsx
doc/html/src/app/pages/ProjectDetail.tsx
+0
-0
useAi.ts
doc/html/src/hooks/useAi.ts
+18
-1
ai.ts
doc/html/src/lib/api/ai.ts
+8
-0
Dockerfile
yaoai-comic-studio/Dockerfile
+5
-1
AgentSseManager.java
...nt/src/main/java/com/yaoai/agent/sse/AgentSseManager.java
+43
-2
OutlineController.java
...main/java/com/yaoai/api/controller/OutlineController.java
+20
-0
GlobalExceptionHandler.java
...in/java/com/yaoai/api/handler/GlobalExceptionHandler.java
+7
-0
logback-spring.xml
...dio/yaoai-bootstrap/src/main/resources/logback-spring.xml
+1
-0
OutlinePipelineService.java
...va/com/yaoai/pipeline/service/OutlinePipelineService.java
+10
-0
OutlinePipelineServiceImpl.java
...oai/pipeline/service/impl/OutlinePipelineServiceImpl.java
+46
-0
No files found.
doc/html/src/app/pages/ProjectDetail.tsx
View file @
69bfd17b
This diff is collapsed.
Click to expand it.
doc/html/src/hooks/useAi.ts
View file @
69bfd17b
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
useMutation
,
useQuery
,
useQueryClient
}
from
"@tanstack/react-query"
;
import
{
aiApi
}
from
"../lib/api/ai"
;
import
{
aiApi
}
from
"../lib/api/ai"
;
import
type
{
Character
,
Scene
,
Storyboard
,
StructuredVideoRequest
}
from
"../lib/api/ai"
;
import
type
{
Character
,
Episode
,
Scene
,
Storyboard
,
StructuredVideoRequest
}
from
"../lib/api/ai"
;
// ---- Characters ----
// ---- Characters ----
const
charactersKey
=
(
pid
:
string
)
=>
[
"characters"
,
pid
];
const
charactersKey
=
(
pid
:
string
)
=>
[
"characters"
,
pid
];
...
@@ -102,6 +102,23 @@ export function useGenerateEpisodes(projectId: string) {
...
@@ -102,6 +102,23 @@ export function useGenerateEpisodes(projectId: string) {
});
});
}
}
export
function
useCreateEpisode
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
(
req
:
Partial
<
Episode
>
)
=>
aiApi
.
createEpisode
(
projectId
,
req
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
episodesKey
(
projectId
)
}),
});
}
export
function
useUpdateEpisode
(
projectId
:
string
)
{
const
qc
=
useQueryClient
();
return
useMutation
({
mutationFn
:
({
id
,
patch
}:
{
id
:
string
;
patch
:
Partial
<
Episode
>
})
=>
aiApi
.
updateEpisode
(
projectId
,
id
,
patch
),
onSuccess
:
()
=>
qc
.
invalidateQueries
({
queryKey
:
episodesKey
(
projectId
)
}),
});
}
// ---- Storyboards ----
// ---- Storyboards ----
export
function
useStoryboards
(
projectId
:
string
,
episodeId
:
string
)
{
export
function
useStoryboards
(
projectId
:
string
,
episodeId
:
string
)
{
return
useQuery
({
return
useQuery
({
...
...
doc/html/src/lib/api/ai.ts
View file @
69bfd17b
...
@@ -160,6 +160,14 @@ export const aiApi = {
...
@@ -160,6 +160,14 @@ export const aiApi = {
const
r
=
await
apiClient
.
get
(
`/projects/
${
projectId
}
/episodes`
);
const
r
=
await
apiClient
.
get
(
`/projects/
${
projectId
}
/episodes`
);
return
r
.
data
.
data
;
return
r
.
data
.
data
;
},
},
createEpisode
:
async
(
projectId
:
string
,
req
:
Partial
<
Episode
>
):
Promise
<
Episode
>
=>
{
const
r
=
await
apiClient
.
post
(
`/projects/
${
projectId
}
/episodes`
,
req
);
return
r
.
data
.
data
;
},
updateEpisode
:
async
(
projectId
:
string
,
episodeId
:
string
,
patch
:
Partial
<
Episode
>
):
Promise
<
Episode
>
=>
{
const
r
=
await
apiClient
.
put
(
`/projects/
${
projectId
}
/episodes/
${
episodeId
}
`
,
patch
);
return
r
.
data
.
data
;
},
// ---- Storyboards ----
// ---- Storyboards ----
createStoryboard
:
async
(
projectId
:
string
,
episodeId
:
string
,
req
:
Partial
<
Storyboard
>
):
Promise
<
Storyboard
>
=>
{
createStoryboard
:
async
(
projectId
:
string
,
episodeId
:
string
,
req
:
Partial
<
Storyboard
>
):
Promise
<
Storyboard
>
=>
{
...
...
yaoai-comic-studio/Dockerfile
View file @
69bfd17b
...
@@ -35,6 +35,10 @@ LABEL maintainer="YaoAI Team <yaoke251@gmail.com>"
...
@@ -35,6 +35,10 @@ LABEL maintainer="YaoAI Team <yaoke251@gmail.com>"
LABEL
org.opencontainers.image.title="YaoAI Comic Studio"
LABEL
org.opencontainers.image.title="YaoAI Comic Studio"
LABEL
org.opencontainers.image.version="0.1.0"
LABEL
org.opencontainers.image.version="0.1.0"
ENV
LANG=C.UTF-8 \
LANGUAGE=C.UTF-8 \
LC_ALL=C.UTF-8
RUN
apt-get update
\
RUN
apt-get update
\
&&
apt-get
install
-y
--no-install-recommends
ffmpeg
\
&&
apt-get
install
-y
--no-install-recommends
ffmpeg
\
&&
rm
-rf
/var/lib/apt/lists/
*
&&
rm
-rf
/var/lib/apt/lists/
*
...
@@ -52,6 +56,6 @@ USER yaoai
...
@@ -52,6 +56,6 @@ USER yaoai
EXPOSE
8080
EXPOSE
8080
ENV
JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom"
ENV
JAVA_OPTS="-Xms256m -Xmx512m -XX:+UseG1GC -Djava.security.egd=file:/dev/./urandom
-Dfile.encoding=UTF-8 -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8
"
ENTRYPOINT
["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
ENTRYPOINT
["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
yaoai-comic-studio/yaoai-agent/src/main/java/com/yaoai/agent/sse/AgentSseManager.java
View file @
69bfd17b
package
com
.
yaoai
.
agent
.
sse
;
package
com
.
yaoai
.
agent
.
sse
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
jakarta.annotation.PostConstruct
;
import
jakarta.annotation.PreDestroy
;
import
lombok.RequiredArgsConstructor
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.stereotype.Component
;
import
org.springframework.stereotype.Component
;
...
@@ -10,21 +12,48 @@ import java.util.List;
...
@@ -10,21 +12,48 @@ import java.util.List;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.ConcurrentHashMap
;
import
java.util.concurrent.CopyOnWriteArrayList
;
import
java.util.concurrent.CopyOnWriteArrayList
;
import
java.util.concurrent.Executors
;
import
java.util.concurrent.ScheduledExecutorService
;
import
java.util.concurrent.TimeUnit
;
@Slf4j
@Slf4j
@Component
@Component
@RequiredArgsConstructor
@RequiredArgsConstructor
public
class
AgentSseManager
{
public
class
AgentSseManager
{
private
static
final
long
HEARTBEAT_INTERVAL_SECONDS
=
15
;
private
final
ObjectMapper
objectMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
Map
<
Long
,
List
<
SseEmitter
>>
emitters
=
new
ConcurrentHashMap
<>();
private
final
Map
<
Long
,
List
<
SseEmitter
>>
emitters
=
new
ConcurrentHashMap
<>();
private
final
ScheduledExecutorService
heartbeatExecutor
=
Executors
.
newSingleThreadScheduledExecutor
(
r
->
{
Thread
t
=
new
Thread
(
r
,
"agent-sse-heartbeat"
);
t
.
setDaemon
(
true
);
return
t
;
});
@PostConstruct
void
startHeartbeat
()
{
heartbeatExecutor
.
scheduleAtFixedRate
(
this
::
heartbeatAll
,
HEARTBEAT_INTERVAL_SECONDS
,
HEARTBEAT_INTERVAL_SECONDS
,
TimeUnit
.
SECONDS
);
}
@PreDestroy
void
shutdownHeartbeat
()
{
heartbeatExecutor
.
shutdownNow
();
}
public
SseEmitter
subscribe
(
Long
runId
)
{
public
SseEmitter
subscribe
(
Long
runId
)
{
SseEmitter
emitter
=
new
SseEmitter
(
300_00
0L
);
SseEmitter
emitter
=
new
SseEmitter
(
0L
);
List
<
SseEmitter
>
list
=
emitters
.
computeIfAbsent
(
runId
,
k
->
new
CopyOnWriteArrayList
<>());
List
<
SseEmitter
>
list
=
emitters
.
computeIfAbsent
(
runId
,
k
->
new
CopyOnWriteArrayList
<>());
list
.
add
(
emitter
);
list
.
add
(
emitter
);
emitter
.
onCompletion
(()
->
remove
(
runId
,
emitter
));
emitter
.
onCompletion
(()
->
remove
(
runId
,
emitter
));
emitter
.
onTimeout
(()
->
remove
(
runId
,
emitter
));
emitter
.
onTimeout
(()
->
{
log
.
debug
(
"SSE timeout: runId={}"
,
runId
);
remove
(
runId
,
emitter
);
emitter
.
complete
();
});
emitter
.
onError
(
e
->
remove
(
runId
,
emitter
));
emitter
.
onError
(
e
->
remove
(
runId
,
emitter
));
log
.
debug
(
"SSE subscribed: runId={}, total={}"
,
runId
,
list
.
size
());
log
.
debug
(
"SSE subscribed: runId={}, total={}"
,
runId
,
list
.
size
());
return
emitter
;
return
emitter
;
...
@@ -57,6 +86,18 @@ public class AgentSseManager {
...
@@ -57,6 +86,18 @@ public class AgentSseManager {
"time"
,
java
.
time
.
LocalTime
.
now
().
toString
().
substring
(
0
,
8
)));
"time"
,
java
.
time
.
LocalTime
.
now
().
toString
().
substring
(
0
,
8
)));
}
}
private
void
heartbeatAll
()
{
emitters
.
forEach
((
runId
,
list
)
->
list
.
removeIf
(
emitter
->
{
try
{
emitter
.
send
(
SseEmitter
.
event
().
comment
(
"ping"
));
return
false
;
}
catch
(
Exception
e
)
{
log
.
debug
(
"Removed heartbeat-broken SSE emitter for runId={}"
,
runId
);
return
true
;
}
}));
}
private
void
remove
(
Long
runId
,
SseEmitter
emitter
)
{
private
void
remove
(
Long
runId
,
SseEmitter
emitter
)
{
List
<
SseEmitter
>
list
=
emitters
.
get
(
runId
);
List
<
SseEmitter
>
list
=
emitters
.
get
(
runId
);
if
(
list
!=
null
)
list
.
remove
(
emitter
);
if
(
list
!=
null
)
list
.
remove
(
emitter
);
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/OutlineController.java
View file @
69bfd17b
...
@@ -5,6 +5,7 @@ import com.yaoai.api.dto.ai.OutlineDTO;
...
@@ -5,6 +5,7 @@ import com.yaoai.api.dto.ai.OutlineDTO;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.common.response.ApiResponse
;
import
com.yaoai.domain.entity.Episode
;
import
com.yaoai.domain.entity.Outline
;
import
com.yaoai.domain.entity.Outline
;
import
com.yaoai.domain.mapper.StoryboardMapper
;
import
com.yaoai.domain.mapper.StoryboardMapper
;
import
com.yaoai.pipeline.service.OutlinePipelineService
;
import
com.yaoai.pipeline.service.OutlinePipelineService
;
...
@@ -87,4 +88,23 @@ public class OutlineController {
...
@@ -87,4 +88,23 @@ public class OutlineController {
.
collect
(
Collectors
.
toList
())
.
collect
(
Collectors
.
toList
())
);
);
}
}
@Operation
(
summary
=
"手动新增一集"
)
@PostMapping
(
"/episodes"
)
public
ApiResponse
<
EpisodeDTO
>
createEpisode
(
@PathVariable
Long
projectId
,
@RequestBody
Episode
req
)
{
Long
tenantId
=
TenantContext
.
get
();
Episode
ep
=
outlinePipelineService
.
createEpisode
(
projectId
,
tenantId
,
req
);
return
ApiResponse
.
success
(
EpisodeDTO
.
from
(
ep
,
0
));
}
@Operation
(
summary
=
"编辑单集(episodeNumber/title/summary/script)"
)
@PutMapping
(
"/episodes/{episodeId}"
)
public
ApiResponse
<
EpisodeDTO
>
updateEpisode
(
@PathVariable
Long
projectId
,
@PathVariable
Long
episodeId
,
@RequestBody
Episode
req
)
{
Long
tenantId
=
TenantContext
.
get
();
Episode
ep
=
outlinePipelineService
.
updateEpisode
(
projectId
,
tenantId
,
episodeId
,
req
);
return
ApiResponse
.
success
(
EpisodeDTO
.
from
(
ep
,
storyboardMapper
.
countByEpisode
(
ep
.
getId
(),
tenantId
)));
}
}
}
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/handler/GlobalExceptionHandler.java
View file @
69bfd17b
...
@@ -12,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
...
@@ -12,6 +12,7 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
org.springframework.web.bind.annotation.ExceptionHandler
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
import
org.springframework.web.bind.annotation.ResponseStatus
;
import
org.springframework.web.bind.annotation.RestControllerAdvice
;
import
org.springframework.web.bind.annotation.RestControllerAdvice
;
import
org.springframework.web.context.request.async.AsyncRequestTimeoutException
;
import
java.util.stream.Collectors
;
import
java.util.stream.Collectors
;
...
@@ -56,6 +57,12 @@ public class GlobalExceptionHandler {
...
@@ -56,6 +57,12 @@ public class GlobalExceptionHandler {
return
ApiResponse
.
error
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
e
.
getMessage
());
return
ApiResponse
.
error
(
ErrorCode
.
INVALID_PARAM
.
getCode
(),
e
.
getMessage
());
}
}
@ExceptionHandler
(
AsyncRequestTimeoutException
.
class
)
@ResponseStatus
(
HttpStatus
.
NO_CONTENT
)
public
void
handleAsyncTimeout
(
AsyncRequestTimeoutException
e
)
{
log
.
debug
(
"Async request timeout"
,
e
);
}
@ExceptionHandler
(
Exception
.
class
)
@ExceptionHandler
(
Exception
.
class
)
@ResponseStatus
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
@ResponseStatus
(
HttpStatus
.
INTERNAL_SERVER_ERROR
)
public
ApiResponse
<?>
handleException
(
Exception
e
)
{
public
ApiResponse
<?>
handleException
(
Exception
e
)
{
...
...
yaoai-comic-studio/yaoai-bootstrap/src/main/resources/logback-spring.xml
View file @
69bfd17b
...
@@ -43,6 +43,7 @@
...
@@ -43,6 +43,7 @@
<!-- ===== non-local: file only ===== -->
<!-- ===== non-local: file only ===== -->
<springProfile
name=
"!local"
>
<springProfile
name=
"!local"
>
<root
level=
"INFO"
>
<root
level=
"INFO"
>
<appender-ref
ref=
"CONSOLE"
/>
<appender-ref
ref=
"ASYNC_FILE"
/>
<appender-ref
ref=
"ASYNC_FILE"
/>
</root>
</root>
</springProfile>
</springProfile>
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/OutlinePipelineService.java
View file @
69bfd17b
...
@@ -20,4 +20,14 @@ public interface OutlinePipelineService {
...
@@ -20,4 +20,14 @@ public interface OutlinePipelineService {
Outline
getOutline
(
Long
projectId
,
Long
tenantId
);
Outline
getOutline
(
Long
projectId
,
Long
tenantId
);
List
<
Episode
>
getEpisodes
(
Long
projectId
,
Long
tenantId
);
List
<
Episode
>
getEpisodes
(
Long
projectId
,
Long
tenantId
);
/**
* 手动新增一集(不调 LLM)。若 req.episodeNumber 为空,自动取当前最大 +1。
*/
Episode
createEpisode
(
Long
projectId
,
Long
tenantId
,
Episode
req
);
/**
* 手动编辑一集(episodeNumber/title/summary/script,非 null 字段才更新)。
*/
Episode
updateEpisode
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
Episode
req
);
}
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/OutlinePipelineServiceImpl.java
View file @
69bfd17b
...
@@ -209,6 +209,52 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
...
@@ -209,6 +209,52 @@ public class OutlinePipelineServiceImpl implements OutlinePipelineService {
return
episodeMapper
.
findByProject
(
projectId
,
tenantId
);
return
episodeMapper
.
findByProject
(
projectId
,
tenantId
);
}
}
@Override
public
Episode
createEpisode
(
Long
projectId
,
Long
tenantId
,
Episode
req
)
{
Outline
outline
=
outlineMapper
.
findLatestByProject
(
projectId
,
tenantId
);
if
(
outline
==
null
)
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"请先生成或保存大纲"
);
}
Integer
epNum
=
req
.
getEpisodeNumber
();
if
(
epNum
==
null
||
epNum
<=
0
)
{
int
max
=
episodeMapper
.
findByProject
(
projectId
,
tenantId
).
stream
()
.
mapToInt
(
e
->
e
.
getEpisodeNumber
()
==
null
?
0
:
e
.
getEpisodeNumber
())
.
max
().
orElse
(
0
);
epNum
=
max
+
1
;
}
Episode
ep
=
new
Episode
();
ep
.
setOutlineId
(
outline
.
getId
());
ep
.
setProjectId
(
projectId
);
ep
.
setTenantId
(
tenantId
);
ep
.
setEpisodeNumber
(
epNum
);
ep
.
setTitle
(
req
.
getTitle
()
==
null
?
""
:
req
.
getTitle
());
ep
.
setSummary
(
req
.
getSummary
()
==
null
?
""
:
req
.
getSummary
());
ep
.
setScript
(
req
.
getScript
()
==
null
?
""
:
req
.
getScript
());
ep
.
setStatus
(
"ready"
);
episodeMapper
.
insert
(
ep
);
log
.
info
(
"Episode manually created: id={}, episodeNumber={}"
,
ep
.
getId
(),
epNum
);
return
ep
;
}
@Override
public
Episode
updateEpisode
(
Long
projectId
,
Long
tenantId
,
Long
episodeId
,
Episode
req
)
{
Episode
existing
=
episodeMapper
.
selectById
(
episodeId
);
if
(
existing
==
null
||
!
tenantId
.
equals
(
existing
.
getTenantId
())
||
!
projectId
.
equals
(
existing
.
getProjectId
()))
{
throw
new
BizException
(
ErrorCode
.
NOT_FOUND
,
"分集不存在"
);
}
if
(
req
.
getEpisodeNumber
()
!=
null
&&
req
.
getEpisodeNumber
()
>
0
)
{
existing
.
setEpisodeNumber
(
req
.
getEpisodeNumber
());
}
if
(
req
.
getTitle
()
!=
null
)
existing
.
setTitle
(
req
.
getTitle
());
if
(
req
.
getSummary
()
!=
null
)
existing
.
setSummary
(
req
.
getSummary
());
if
(
req
.
getScript
()
!=
null
)
existing
.
setScript
(
req
.
getScript
());
episodeMapper
.
updateById
(
existing
);
log
.
info
(
"Episode updated: id={}"
,
episodeId
);
return
existing
;
}
private
String
extractJson
(
String
raw
)
{
private
String
extractJson
(
String
raw
)
{
// strip markdown code fences if present
// strip markdown code fences if present
String
s
=
raw
.
strip
();
String
s
=
raw
.
strip
();
...
...
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