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
fde0aba7
Commit
fde0aba7
authored
Jun 06, 2026
by
yaoke.yk
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
测试尝试用Agnes测demo
parent
0aa3bd46
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
929 additions
and
170 deletions
+929
-170
StoryboardWorkspace.tsx
doc/html/src/app/pages/StoryboardWorkspace.tsx
+1
-25
ai.ts
doc/html/src/lib/api/ai.ts
+0
-2
router.ts
yaoai-admin-web/src/router.ts
+6
-0
ModelConfigView.vue
yaoai-admin-web/src/views/ModelConfigView.vue
+0
-0
tsconfig.tsbuildinfo
yaoai-admin-web/tsconfig.tsbuildinfo
+2
-2
vite.config.ts
yaoai-admin-web/vite.config.ts
+0
-1
pom.xml
yaoai-comic-studio/yaoai-admin/pom.xml
+4
-0
ModelConfigAdminController.java
...m/yaoai/admin/modelconfig/ModelConfigAdminController.java
+36
-0
pom.xml
yaoai-comic-studio/yaoai-ai-providers/pom.xml
+8
-0
ModelCapability.java
...n/java/com/yaoai/ai/providers/config/ModelCapability.java
+29
-0
PlatformModelConfigService.java
...yaoai/ai/providers/config/PlatformModelConfigService.java
+215
-0
PlatformModelItem.java
...java/com/yaoai/ai/providers/config/PlatformModelItem.java
+18
-0
PlatformModelSettings.java
.../com/yaoai/ai/providers/config/PlatformModelSettings.java
+26
-0
ProviderRestClientFactory.java
.../yaoai/ai/providers/config/ProviderRestClientFactory.java
+82
-0
ResolvedModelConfig.java
...va/com/yaoai/ai/providers/config/ResolvedModelConfig.java
+26
-0
ArkLlmService.java
...va/com/yaoai/ai/providers/service/impl/ArkLlmService.java
+61
-30
SeedanceServiceImpl.java
.../yaoai/ai/providers/service/impl/SeedanceServiceImpl.java
+0
-0
SeedreamServiceImpl.java
.../yaoai/ai/providers/service/impl/SeedreamServiceImpl.java
+71
-36
VideoTaskController.java
...in/java/com/yaoai/api/controller/VideoTaskController.java
+0
-1
StructuredVideoGenerateRequest.java
.../com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
+2
-3
PlatformModelConfigServiceTest.java
...i/ai/providers/config/PlatformModelConfigServiceTest.java
+104
-0
AiTaskMapper.java
...n/src/main/java/com/yaoai/domain/mapper/AiTaskMapper.java
+4
-1
VideoTaskCompletionService.java
.../com/yaoai/pipeline/async/VideoTaskCompletionService.java
+21
-61
VideoTransferService.java
...n/java/com/yaoai/pipeline/async/VideoTransferService.java
+83
-0
AssetGenPipelineServiceImpl.java
...ai/pipeline/service/impl/AssetGenPipelineServiceImpl.java
+5
-7
SceneImageTransferService.java
...aoai/pipeline/service/impl/SceneImageTransferService.java
+124
-0
VideoTaskPipelineServiceImpl.java
...i/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
+1
-1
No files found.
doc/html/src/app/pages/StoryboardWorkspace.tsx
View file @
fde0aba7
...
@@ -41,11 +41,6 @@ const RATIO_OPTIONS = [
...
@@ -41,11 +41,6 @@ const RATIO_OPTIONS = [
{
value
:
"9:16"
,
label
:
"9:16 竖屏"
},
{
value
:
"9:16"
,
label
:
"9:16 竖屏"
},
{
value
:
"1:1"
,
label
:
"1:1 方形"
},
{
value
:
"1:1"
,
label
:
"1:1 方形"
},
];
];
const
VIDEO_MODEL_OPTIONS
=
[
{
value
:
"doubao-seedance-2-0-fast-260128"
,
label
:
"Doubao-Seedance-2-0 fast"
},
{
value
:
"doubao-seedance-2-0-260128"
,
label
:
"Doubao-Seedance-2-0"
},
];
const
DEFAULT_VIDEO_MODEL
=
VIDEO_MODEL_OPTIONS
[
0
].
value
;
const
MAX_CHARACTERS
=
5
;
const
MAX_CHARACTERS
=
5
;
const
MAX_PROPS
=
5
;
const
MAX_PROPS
=
5
;
const
PROMPT_PREVIEW_LIMIT
=
80
;
const
PROMPT_PREVIEW_LIMIT
=
80
;
...
@@ -71,7 +66,6 @@ interface SbDraft {
...
@@ -71,7 +66,6 @@ interface SbDraft {
freeformPrompt
:
string
;
freeformPrompt
:
string
;
selectedDuration
:
number
;
selectedDuration
:
number
;
selectedRatio
:
string
;
selectedRatio
:
string
;
selectedModel
:
string
;
audioOn
:
boolean
;
audioOn
:
boolean
;
}
}
...
@@ -328,7 +322,6 @@ export function StoryboardWorkspace() {
...
@@ -328,7 +322,6 @@ export function StoryboardWorkspace() {
const [freeformPrompt, setFreeformPrompt] = useState<string>("");
const [freeformPrompt, setFreeformPrompt] = useState<string>("");
const [selectedDuration, setSelectedDuration] = useState(DEFAULT_VIDEO_DURATION);
const [selectedDuration, setSelectedDuration] = useState(DEFAULT_VIDEO_DURATION);
const [selectedRatio, setSelectedRatio] = useState<string>("16:9");
const [selectedRatio, setSelectedRatio] = useState<string>("16:9");
const [selectedModel, setSelectedModel] = useState<string>(DEFAULT_VIDEO_MODEL);
const [audioOn, setAudioOn] = useState(true);
const [audioOn, setAudioOn] = useState(true);
const [generatingVideoId, setGeneratingVideoId] = useState<string | null>(null);
const [generatingVideoId, setGeneratingVideoId] = useState<string | null>(null);
...
@@ -437,7 +430,6 @@ export function StoryboardWorkspace() {
...
@@ -437,7 +430,6 @@ export function StoryboardWorkspace() {
setFreeformPrompt(draft.freeformPrompt);
setFreeformPrompt(draft.freeformPrompt);
setSelectedDuration(normalizeVideoDuration(draft.selectedDuration));
setSelectedDuration(normalizeVideoDuration(draft.selectedDuration));
setSelectedRatio(draft.selectedRatio);
setSelectedRatio(draft.selectedRatio);
setSelectedModel(draft.selectedModel);
setAudioOn(draft.audioOn);
setAudioOn(draft.audioOn);
if (nextSceneKey && draft.sceneKey !== nextSceneKey) {
if (nextSceneKey && draft.sceneKey !== nextSceneKey) {
updateSb.mutate({ id: expanded.id, patch: { sceneImageKey: nextSceneKey } });
updateSb.mutate({ id: expanded.id, patch: { sceneImageKey: nextSceneKey } });
...
@@ -503,7 +495,6 @@ export function StoryboardWorkspace() {
...
@@ -503,7 +495,6 @@ export function StoryboardWorkspace() {
freeformPrompt,
freeformPrompt,
selectedDuration,
selectedDuration,
selectedRatio,
selectedRatio,
selectedModel,
audioOn,
audioOn,
});
});
}, [
}, [
...
@@ -511,7 +502,7 @@ export function StoryboardWorkspace() {
...
@@ -511,7 +502,7 @@ export function StoryboardWorkspace() {
generationMode, shortDescription, characterKeys, sceneKey, propKeys,
generationMode, shortDescription, characterKeys, sceneKey, propKeys,
firstFrameImageKey, firstFrameImageUrl, lastFrameImageKey, lastFrameImageUrl,
firstFrameImageKey, firstFrameImageUrl, lastFrameImageKey, lastFrameImageUrl,
freeformPrompt,
freeformPrompt,
selectedDuration, selectedRatio,
selectedModel,
audioOn,
selectedDuration, selectedRatio, audioOn,
]);
]);
useEffect(() => {
useEffect(() => {
...
@@ -680,7 +671,6 @@ export function StoryboardWorkspace() {
...
@@ -680,7 +671,6 @@ export function StoryboardWorkspace() {
freeformPrompt: freeformPrompt.trim(),
freeformPrompt: freeformPrompt.trim(),
duration: selectedDuration,
duration: selectedDuration,
ratio: selectedRatio,
ratio: selectedRatio,
model: selectedModel,
generateAudio: audioOn,
generateAudio: audioOn,
});
});
setActiveVideoSbId(expanded.id);
setActiveVideoSbId(expanded.id);
...
@@ -950,20 +940,6 @@ export function StoryboardWorkspace() {
...
@@ -950,20 +940,6 @@ export function StoryboardWorkspace() {
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"flex items-center gap-2"
>
<
div
className=
"relative flex-1"
>
<
div
className=
"relative flex-1"
>
<
select
<
select
value=
{
selectedModel
}
onChange=
{
(
e
)
=>
setSelectedModel
(
e
.
target
.
value
)
}
disabled=
{
!
expanded
}
title=
"视频生成模型"
className=
{
`w-full appearance-none pl-3 pr-7 py-2 rounded-md bg-white text-xs text-[#111827] cursor-pointer disabled:opacity-50 transition-colors ${CONTROL_SURFACE} ${FOCUS_RING}`
}
>
{
VIDEO_MODEL_OPTIONS
.
map
((
m
)
=>
(
<
option
key=
{
m
.
value
}
value=
{
m
.
value
}
>
{
m
.
label
}
</
option
>
))
}
</
select
>
<
ChevronDown
className=
"absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 text-muted-foreground pointer-events-none"
/>
</
div
>
<
div
className=
"relative flex-1"
>
<
select
value=
{
selectedRatio
}
value=
{
selectedRatio
}
onChange=
{
(
e
)
=>
setSelectedRatio
(
e
.
target
.
value
)
}
onChange=
{
(
e
)
=>
setSelectedRatio
(
e
.
target
.
value
)
}
disabled=
{
!
expanded
}
disabled=
{
!
expanded
}
...
...
doc/html/src/lib/api/ai.ts
View file @
fde0aba7
...
@@ -174,8 +174,6 @@ export interface StructuredVideoRequest {
...
@@ -174,8 +174,6 @@ export interface StructuredVideoRequest {
}
|
null
;
}
|
null
;
duration
?:
number
|
null
;
duration
?:
number
|
null
;
ratio
?:
string
|
null
;
ratio
?:
string
|
null
;
/** Seedance 模型 ID(如 doubao-seedance-2-0-fast-260128 / doubao-seedance-2-0-260128),缺省由后端 ArkProperties 兜底 */
model
?:
string
|
null
;
generateAudio
?:
boolean
|
null
;
generateAudio
?:
boolean
|
null
;
}
}
...
...
yaoai-admin-web/src/router.ts
View file @
fde0aba7
...
@@ -6,6 +6,7 @@ import {
...
@@ -6,6 +6,7 @@ import {
OfficeBuilding
,
OfficeBuilding
,
Operation
,
Operation
,
PriceTag
,
PriceTag
,
Setting
,
Tickets
,
Tickets
,
UserFilled
,
UserFilled
,
}
from
'@element-plus/icons-vue'
;
}
from
'@element-plus/icons-vue'
;
...
@@ -50,6 +51,11 @@ export const routes: RouteRecordRaw[] = [
...
@@ -50,6 +51,11 @@ export const routes: RouteRecordRaw[] = [
meta
:
{
title
:
'计费价格'
,
icon
:
PriceTag
,
authority
:
[
'SUPER_ADMIN'
]
},
meta
:
{
title
:
'计费价格'
,
icon
:
PriceTag
,
authority
:
[
'SUPER_ADMIN'
]
},
},
},
{
{
path
:
'/model-config'
,
component
:
()
=>
import
(
'./views/ModelConfigView.vue'
),
meta
:
{
title
:
'模型配置'
,
icon
:
Setting
,
authority
:
[
'SUPER_ADMIN'
,
'OPERATOR'
]
},
},
{
path
:
'/plans'
,
path
:
'/plans'
,
component
:
()
=>
import
(
'./views/PlanManagementView.vue'
),
component
:
()
=>
import
(
'./views/PlanManagementView.vue'
),
meta
:
{
title
:
'套餐管理'
,
icon
:
PriceTag
,
authority
:
[
'SUPER_ADMIN'
]
},
meta
:
{
title
:
'套餐管理'
,
icon
:
PriceTag
,
authority
:
[
'SUPER_ADMIN'
]
},
...
...
yaoai-admin-web/src/views/ModelConfigView.vue
0 → 100644
View file @
fde0aba7
This diff is collapsed.
Click to expand it.
yaoai-admin-web/tsconfig.tsbuildinfo
View file @
fde0aba7
{"root":["./src/env.d.ts","./src/main.ts","./src/router.ts","./src/api/http.ts","./src/api/types.ts","./src/stores/auth.ts","./src/app.vue","./src/views/adminusermanagementview.vue","./src/views/auditlogsview.vue","./src/views/billingcostsview.vue","./src/views/billingrecordsview.vue","./src/views/dashboardview.vue","./src/views/loginview.vue","./src/views/notfoundview.vue","./src/views/planmanagementview.vue","./src/views/tenantdetailview.vue","./src/views/tenantlistview.vue","./src/views/usermanagementview.vue"],"version":"5.9.3"}
{"root":["./src/env.d.ts","./src/main.ts","./src/router.ts","./src/api/http.ts","./src/api/types.ts","./src/stores/auth.ts","./src/app.vue","./src/views/adminusermanagementview.vue","./src/views/auditlogsview.vue","./src/views/billingcostsview.vue","./src/views/billingrecordsview.vue","./src/views/dashboardview.vue","./src/views/loginview.vue","./src/views/modelconfigview.vue","./src/views/notfoundview.vue","./src/views/planmanagementview.vue","./src/views/tenantdetailview.vue","./src/views/tenantlistview.vue","./src/views/usermanagementview.vue"],"version":"5.9.3"}
\ No newline at end of file
\ No newline at end of file
yaoai-admin-web/vite.config.ts
View file @
fde0aba7
...
@@ -35,7 +35,6 @@ export default defineConfig(({ mode }) => {
...
@@ -35,7 +35,6 @@ export default defineConfig(({ mode }) => {
'/admin-api'
:
{
'/admin-api'
:
{
target
:
resolveBackendTarget
(
env
),
target
:
resolveBackendTarget
(
env
),
changeOrigin
:
true
,
changeOrigin
:
true
,
rewrite
:
(
path
)
=>
path
.
replace
(
/^
\/
admin-api/
,
'/admin'
),
},
},
},
},
},
},
...
...
yaoai-comic-studio/yaoai-admin/pom.xml
View file @
fde0aba7
...
@@ -25,6 +25,10 @@
...
@@ -25,6 +25,10 @@
</dependency>
</dependency>
<dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-ai-providers
</artifactId>
</dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-security
</artifactId>
<artifactId>
yaoai-security
</artifactId>
</dependency>
</dependency>
<dependency>
<dependency>
...
...
yaoai-comic-studio/yaoai-admin/src/main/java/com/yaoai/admin/modelconfig/ModelConfigAdminController.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
admin
.
modelconfig
;
import
cn.dev33.satoken.annotation.SaCheckLogin
;
import
cn.dev33.satoken.annotation.SaCheckRole
;
import
cn.dev33.satoken.annotation.SaMode
;
import
com.yaoai.admin.audit.annotation.AdminAudited
;
import
com.yaoai.admin.auth.StpAdminUtil
;
import
com.yaoai.ai.providers.config.PlatformModelConfigService
;
import
com.yaoai.ai.providers.config.PlatformModelSettings
;
import
com.yaoai.common.result.Result
;
import
lombok.RequiredArgsConstructor
;
import
org.springframework.web.bind.annotation.GetMapping
;
import
org.springframework.web.bind.annotation.PutMapping
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RestController
;
@RestController
@RequiredArgsConstructor
@SaCheckLogin
(
type
=
"admin"
)
public
class
ModelConfigAdminController
{
private
final
PlatformModelConfigService
platformModelConfigService
;
@GetMapping
(
"/admin-api/model-config"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
,
"OPERATOR"
},
mode
=
SaMode
.
OR
)
public
Result
<
PlatformModelSettings
>
getConfig
()
{
return
Result
.
ok
(
platformModelConfigService
.
getAdminSettings
());
}
@PutMapping
(
"/admin-api/model-config"
)
@AdminAudited
(
action
=
"MODEL_CONFIG_UPDATE"
,
resource
=
"MODEL_CONFIG"
)
@SaCheckRole
(
type
=
"admin"
,
value
=
{
"SUPER_ADMIN"
})
public
Result
<
PlatformModelSettings
>
saveConfig
(
@RequestBody
PlatformModelSettings
settings
)
{
return
Result
.
ok
(
platformModelConfigService
.
saveAdminSettings
(
settings
,
StpAdminUtil
.
getLoginIdAsLong
()));
}
}
yaoai-comic-studio/yaoai-ai-providers/pom.xml
View file @
fde0aba7
...
@@ -20,8 +20,16 @@
...
@@ -20,8 +20,16 @@
<artifactId>
yaoai-ai-core
</artifactId>
<artifactId>
yaoai-ai-core
</artifactId>
</dependency>
</dependency>
<dependency>
<dependency>
<groupId>
com.yaoai
</groupId>
<artifactId>
yaoai-domain
</artifactId>
</dependency>
<dependency>
<groupId>
org.springframework.boot
</groupId>
<groupId>
org.springframework.boot
</groupId>
<artifactId>
spring-boot-starter-web
</artifactId>
<artifactId>
spring-boot-starter-web
</artifactId>
</dependency>
</dependency>
<dependency>
<groupId>
org.apache.httpcomponents.client5
</groupId>
<artifactId>
httpclient5
</artifactId>
</dependency>
</dependencies>
</dependencies>
</project>
</project>
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/ModelCapability.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
public
enum
ModelCapability
{
TEXT
(
"text"
),
IMAGE
(
"image"
),
VIDEO
(
"video"
);
private
final
String
key
;
ModelCapability
(
String
key
)
{
this
.
key
=
key
;
}
public
String
key
()
{
return
key
;
}
public
static
ModelCapability
fromKey
(
String
value
)
{
if
(
value
==
null
)
{
throw
new
IllegalArgumentException
(
"capability is required"
);
}
for
(
ModelCapability
capability
:
values
())
{
if
(
capability
.
key
.
equalsIgnoreCase
(
value
)
||
capability
.
name
().
equalsIgnoreCase
(
value
))
{
return
capability
;
}
}
throw
new
IllegalArgumentException
(
"unsupported capability: "
+
value
);
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/PlatformModelConfigService.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.SystemSetting
;
import
com.yaoai.domain.mapper.SystemSettingMapper
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.stereotype.Service
;
import
org.springframework.util.StringUtils
;
import
java.time.LocalDateTime
;
@Slf4j
@Service
public
class
PlatformModelConfigService
{
public
static
final
String
SETTING_KEY
=
"ai.model.config"
;
public
static
final
String
MASKED_API_KEY
=
"******"
;
private
final
SystemSettingMapper
systemSettingMapper
;
private
final
ArkProperties
arkProperties
;
private
final
ObjectMapper
objectMapper
;
@Autowired
public
PlatformModelConfigService
(
SystemSettingMapper
systemSettingMapper
,
ArkProperties
arkProperties
)
{
this
(
systemSettingMapper
,
arkProperties
,
new
ObjectMapper
());
}
PlatformModelConfigService
(
SystemSettingMapper
systemSettingMapper
,
ArkProperties
arkProperties
,
ObjectMapper
objectMapper
)
{
this
.
systemSettingMapper
=
systemSettingMapper
;
this
.
arkProperties
=
arkProperties
;
this
.
objectMapper
=
objectMapper
;
}
public
ResolvedModelConfig
resolve
(
ModelCapability
capability
)
{
PlatformModelItem
item
=
loadStoredSettings
().
get
(
capability
);
if
(
isUsable
(
item
))
{
return
toResolved
(
item
,
fallback
(
capability
));
}
return
fallback
(
capability
);
}
public
PlatformModelSettings
getAdminSettings
()
{
PlatformModelSettings
settings
=
withFallbacks
(
loadStoredSettings
());
maskApiKey
(
settings
.
getText
());
maskApiKey
(
settings
.
getImage
());
maskApiKey
(
settings
.
getVideo
());
return
settings
;
}
public
PlatformModelSettings
saveAdminSettings
(
PlatformModelSettings
input
,
Long
adminId
)
{
PlatformModelSettings
current
=
loadStoredSettings
();
PlatformModelSettings
next
=
withFallbacks
(
input
==
null
?
new
PlatformModelSettings
()
:
input
);
preserveMaskedApiKey
(
next
.
getText
(),
current
.
getText
());
preserveMaskedApiKey
(
next
.
getImage
(),
current
.
getImage
());
preserveMaskedApiKey
(
next
.
getVideo
(),
current
.
getVideo
());
try
{
String
json
=
objectMapper
.
writeValueAsString
(
next
);
SystemSetting
setting
=
systemSettingMapper
.
findByKey
(
SETTING_KEY
).
orElseGet
(
SystemSetting:
:
new
);
setting
.
setSettingKey
(
SETTING_KEY
);
setting
.
setSettingValue
(
json
);
setting
.
setUpdatedAt
(
LocalDateTime
.
now
());
setting
.
setUpdatedBy
(
adminId
);
if
(
setting
.
getId
()
==
null
)
{
systemSettingMapper
.
insert
(
setting
);
}
else
{
systemSettingMapper
.
updateById
(
setting
);
}
return
getAdminSettings
();
}
catch
(
Exception
e
)
{
log
.
error
(
"save platform model config failed"
,
e
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"保存模型配置失败: "
+
e
.
getMessage
());
}
}
private
PlatformModelSettings
loadStoredSettings
()
{
return
systemSettingMapper
.
findByKey
(
SETTING_KEY
)
.
map
(
SystemSetting:
:
getSettingValue
)
.
filter
(
StringUtils:
:
hasText
)
.
map
(
this
::
parseSettings
)
.
orElseGet
(
PlatformModelSettings:
:
new
);
}
private
PlatformModelSettings
parseSettings
(
String
json
)
{
try
{
PlatformModelSettings
settings
=
objectMapper
.
readValue
(
json
,
PlatformModelSettings
.
class
);
return
settings
==
null
?
new
PlatformModelSettings
()
:
settings
;
}
catch
(
Exception
e
)
{
log
.
warn
(
"ignore invalid platform model config json"
,
e
);
return
new
PlatformModelSettings
();
}
}
private
PlatformModelSettings
withFallbacks
(
PlatformModelSettings
settings
)
{
PlatformModelSettings
next
=
new
PlatformModelSettings
();
next
.
setText
(
merge
(
settings
.
getText
(),
fallback
(
ModelCapability
.
TEXT
)));
next
.
setImage
(
merge
(
settings
.
getImage
(),
fallback
(
ModelCapability
.
IMAGE
)));
next
.
setVideo
(
merge
(
settings
.
getVideo
(),
fallback
(
ModelCapability
.
VIDEO
)));
return
next
;
}
private
PlatformModelItem
merge
(
PlatformModelItem
item
,
ResolvedModelConfig
fallback
)
{
PlatformModelItem
next
=
item
==
null
?
new
PlatformModelItem
()
:
item
;
if
(!
StringUtils
.
hasText
(
next
.
getProvider
()))
next
.
setProvider
(
fallback
.
provider
());
if
(!
StringUtils
.
hasText
(
next
.
getProviderName
()))
next
.
setProviderName
(
fallback
.
providerName
());
if
(!
StringUtils
.
hasText
(
next
.
getProtocol
()))
next
.
setProtocol
(
fallback
.
protocol
());
if
(!
StringUtils
.
hasText
(
next
.
getBaseUrl
()))
next
.
setBaseUrl
(
fallback
.
baseUrl
());
if
(!
StringUtils
.
hasText
(
next
.
getApiKey
()))
next
.
setApiKey
(
fallback
.
apiKey
());
if
(!
StringUtils
.
hasText
(
next
.
getModelId
()))
next
.
setModelId
(
fallback
.
modelId
());
if
(!
StringUtils
.
hasText
(
next
.
getModelName
()))
next
.
setModelName
(
fallback
.
modelName
());
if
(!
StringUtils
.
hasText
(
next
.
getEndpoint
()))
next
.
setEndpoint
(
fallback
.
endpoint
());
if
(!
StringUtils
.
hasText
(
next
.
getStatusEndpoint
()))
next
.
setStatusEndpoint
(
fallback
.
statusEndpoint
());
if
(
next
.
getEnabled
()
==
null
)
next
.
setEnabled
(
false
);
return
next
;
}
private
ResolvedModelConfig
toResolved
(
PlatformModelItem
item
,
ResolvedModelConfig
fallback
)
{
return
new
ResolvedModelConfig
(
valueOr
(
item
.
getProvider
(),
fallback
.
provider
()),
valueOr
(
item
.
getProviderName
(),
fallback
.
providerName
()),
valueOr
(
item
.
getProtocol
(),
fallback
.
protocol
()),
normalizeBaseUrl
(
valueOr
(
item
.
getBaseUrl
(),
fallback
.
baseUrl
())),
valueOr
(
item
.
getApiKey
(),
fallback
.
apiKey
()),
valueOr
(
item
.
getModelId
(),
fallback
.
modelId
()),
valueOr
(
item
.
getModelName
(),
fallback
.
modelName
()),
normalizeEndpoint
(
valueOr
(
item
.
getEndpoint
(),
fallback
.
endpoint
())),
normalizeEndpoint
(
valueOr
(
item
.
getStatusEndpoint
(),
fallback
.
statusEndpoint
())),
valueOr
(
item
.
getResolveIp
(),
fallback
.
resolveIp
())
);
}
private
ResolvedModelConfig
fallback
(
ModelCapability
capability
)
{
return
switch
(
capability
)
{
case
TEXT
->
new
ResolvedModelConfig
(
"volcengine"
,
"Volcengine ARK"
,
"openai"
,
normalizeBaseUrl
(
arkProperties
.
getBaseUrl
()),
arkProperties
.
getApiKey
(),
arkProperties
.
getTextModel
(),
"Doubao Text"
,
"/chat/completions"
,
null
,
null
);
case
IMAGE
->
new
ResolvedModelConfig
(
"volcengine"
,
"Volcengine ARK"
,
"seedream"
,
normalizeBaseUrl
(
arkProperties
.
getBaseUrl
()),
arkProperties
.
getApiKey
(),
arkProperties
.
getImageModel
(),
"Seedream Image"
,
"/images/generations"
,
null
,
null
);
case
VIDEO
->
new
ResolvedModelConfig
(
"volcengine"
,
"Volcengine ARK"
,
"seedance"
,
normalizeBaseUrl
(
arkProperties
.
getBaseUrl
()),
arkProperties
.
getApiKey
(),
arkProperties
.
getVideoModel
(),
"Seedance Image-to-Video"
,
"/contents/generations/tasks"
,
"/contents/generations/tasks/{id}"
,
null
);
};
}
private
boolean
isUsable
(
PlatformModelItem
item
)
{
return
item
!=
null
&&
Boolean
.
TRUE
.
equals
(
item
.
getEnabled
())
&&
StringUtils
.
hasText
(
item
.
getBaseUrl
())
&&
StringUtils
.
hasText
(
item
.
getApiKey
())
&&
StringUtils
.
hasText
(
item
.
getModelId
())
&&
!
MASKED_API_KEY
.
equals
(
item
.
getApiKey
());
}
private
void
preserveMaskedApiKey
(
PlatformModelItem
next
,
PlatformModelItem
current
)
{
if
(
next
==
null
)
return
;
String
apiKey
=
next
.
getApiKey
();
if
(!
StringUtils
.
hasText
(
apiKey
)
||
MASKED_API_KEY
.
equals
(
apiKey
))
{
next
.
setApiKey
(
current
==
null
?
""
:
current
.
getApiKey
());
}
}
private
void
maskApiKey
(
PlatformModelItem
item
)
{
if
(
item
!=
null
&&
StringUtils
.
hasText
(
item
.
getApiKey
()))
{
item
.
setApiKey
(
MASKED_API_KEY
);
}
}
private
String
valueOr
(
String
value
,
String
fallback
)
{
return
StringUtils
.
hasText
(
value
)
?
value
.
trim
()
:
fallback
;
}
private
String
normalizeBaseUrl
(
String
value
)
{
if
(!
StringUtils
.
hasText
(
value
))
return
value
;
return
value
.
trim
().
replaceAll
(
"/+$"
,
""
);
}
private
String
normalizeEndpoint
(
String
value
)
{
if
(!
StringUtils
.
hasText
(
value
))
return
value
;
String
trimmed
=
value
.
trim
();
return
trimmed
.
startsWith
(
"/"
)
?
trimmed
:
"/"
+
trimmed
;
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/PlatformModelItem.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
import
lombok.Data
;
@Data
public
class
PlatformModelItem
{
private
String
provider
;
private
String
providerName
;
private
String
protocol
;
private
String
baseUrl
;
private
String
apiKey
;
private
String
modelId
;
private
String
modelName
;
private
String
endpoint
;
private
String
statusEndpoint
;
private
String
resolveIp
;
private
Boolean
enabled
;
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/PlatformModelSettings.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
import
lombok.Data
;
@Data
public
class
PlatformModelSettings
{
private
PlatformModelItem
text
;
private
PlatformModelItem
image
;
private
PlatformModelItem
video
;
public
PlatformModelItem
get
(
ModelCapability
capability
)
{
return
switch
(
capability
)
{
case
TEXT
->
text
;
case
IMAGE
->
image
;
case
VIDEO
->
video
;
};
}
public
void
set
(
ModelCapability
capability
,
PlatformModelItem
item
)
{
switch
(
capability
)
{
case
TEXT
->
text
=
item
;
case
IMAGE
->
image
=
item
;
case
VIDEO
->
video
=
item
;
}
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/ProviderRestClientFactory.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
import
org.apache.hc.client5.http.DnsResolver
;
import
org.apache.hc.client5.http.impl.classic.CloseableHttpClient
;
import
org.apache.hc.client5.http.impl.classic.HttpClients
;
import
org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder
;
import
org.springframework.http.client.HttpComponentsClientHttpRequestFactory
;
import
org.springframework.stereotype.Component
;
import
org.springframework.util.StringUtils
;
import
org.springframework.web.client.RestClient
;
import
java.net.InetAddress
;
import
java.net.URI
;
import
java.net.UnknownHostException
;
import
java.time.Duration
;
@Component
public
class
ProviderRestClientFactory
{
private
static
final
String
AGNES_API_HOST
=
"apihub.agnes-ai.com"
;
private
static
final
String
AGNES_API_RESOLVE_IP
=
"104.18.18.62"
;
public
RestClient
create
(
ResolvedModelConfig
config
)
{
return
create
(
config
,
Duration
.
ofSeconds
(
10
),
Duration
.
ofMinutes
(
2
));
}
public
RestClient
create
(
ResolvedModelConfig
config
,
Duration
connectTimeout
,
Duration
readTimeout
)
{
RestClient
.
Builder
builder
=
RestClient
.
builder
()
.
baseUrl
(
config
.
baseUrl
())
.
defaultHeader
(
"Content-Type"
,
"application/json"
);
String
resolveIp
=
resolveIp
(
config
);
if
(
StringUtils
.
hasText
(
resolveIp
))
{
builder
.
requestFactory
(
requestFactory
(
config
,
resolveIp
,
connectTimeout
,
readTimeout
));
}
return
builder
.
build
();
}
private
HttpComponentsClientHttpRequestFactory
requestFactory
(
ResolvedModelConfig
config
,
String
resolveIp
,
Duration
connectTimeout
,
Duration
readTimeout
)
{
String
host
=
URI
.
create
(
config
.
baseUrl
()).
getHost
();
DnsResolver
resolver
=
new
FixedHostDnsResolver
(
host
,
resolveIp
.
trim
());
CloseableHttpClient
httpClient
=
HttpClients
.
custom
()
.
setConnectionManager
(
PoolingHttpClientConnectionManagerBuilder
.
create
()
.
setDnsResolver
(
resolver
)
.
build
())
.
build
();
HttpComponentsClientHttpRequestFactory
factory
=
new
HttpComponentsClientHttpRequestFactory
(
httpClient
);
factory
.
setConnectTimeout
(
connectTimeout
);
factory
.
setConnectionRequestTimeout
(
connectTimeout
);
return
factory
;
}
private
String
resolveIp
(
ResolvedModelConfig
config
)
{
if
(
StringUtils
.
hasText
(
config
.
resolveIp
()))
{
return
config
.
resolveIp
();
}
String
host
=
URI
.
create
(
config
.
baseUrl
()).
getHost
();
if
(
AGNES_API_HOST
.
equalsIgnoreCase
(
host
))
{
return
AGNES_API_RESOLVE_IP
;
}
return
null
;
}
private
record
FixedHostDnsResolver
(
String
host
,
String
resolveIp
)
implements
DnsResolver
{
@Override
public
InetAddress
[]
resolve
(
String
requestedHost
)
throws
UnknownHostException
{
if
(
host
!=
null
&&
host
.
equalsIgnoreCase
(
requestedHost
))
{
return
new
InetAddress
[]{
InetAddress
.
getByName
(
resolveIp
)};
}
return
InetAddress
.
getAllByName
(
requestedHost
);
}
@Override
public
String
resolveCanonicalHostname
(
String
requestedHost
)
throws
UnknownHostException
{
return
requestedHost
;
}
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/config/ResolvedModelConfig.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
public
record
ResolvedModelConfig
(
String
provider
,
String
providerName
,
String
protocol
,
String
baseUrl
,
String
apiKey
,
String
modelId
,
String
modelName
,
String
endpoint
,
String
statusEndpoint
,
String
resolveIp
)
{
public
boolean
isProtocol
(
String
value
)
{
return
protocol
!=
null
&&
protocol
.
equalsIgnoreCase
(
value
);
}
public
boolean
isProvider
(
String
value
)
{
return
provider
!=
null
&&
provider
.
equalsIgnoreCase
(
value
);
}
public
boolean
isAgnes
()
{
return
isProvider
(
"agnes"
)
||
isProtocol
(
"agnes"
);
}
}
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/ArkLlmService.java
View file @
fde0aba7
...
@@ -4,9 +4,14 @@ import com.yaoai.ai.core.model.ChatRequest;
...
@@ -4,9 +4,14 @@ import com.yaoai.ai.core.model.ChatRequest;
import
com.yaoai.ai.core.model.ChatResponse
;
import
com.yaoai.ai.core.model.ChatResponse
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.ai.core.service.LlmService
;
import
com.yaoai.ai.providers.config.ArkProperties
;
import
com.yaoai.ai.providers.config.ArkProperties
;
import
com.yaoai.ai.providers.config.ModelCapability
;
import
com.yaoai.ai.providers.config.PlatformModelConfigService
;
import
com.yaoai.ai.providers.config.ProviderRestClientFactory
;
import
com.yaoai.ai.providers.config.ResolvedModelConfig
;
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
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
org.springframework.web.client.RestClient
;
import
org.springframework.web.client.RestClient
;
...
@@ -17,32 +22,39 @@ import org.springframework.web.client.RestClientResponseException;
...
@@ -17,32 +22,39 @@ import org.springframework.web.client.RestClientResponseException;
@ConditionalOnProperty
(
name
=
"ai.local-stub.enabled"
,
havingValue
=
"false"
,
matchIfMissing
=
true
)
@ConditionalOnProperty
(
name
=
"ai.local-stub.enabled"
,
havingValue
=
"false"
,
matchIfMissing
=
true
)
public
class
ArkLlmService
implements
LlmService
{
public
class
ArkLlmService
implements
LlmService
{
private
static
final
String
A
RK_A
PI_KEY_HINT
=
private
static
final
String
API_KEY_HINT
=
"未配置
火山方舟 API Key,请设置环境变量 VOLCENGINE_ARK_API_KEY,或在 application-local.yml 中配置 volcengine.ark.api-key
"
;
"未配置
文本模型 API Key,请在运营端模型配置中填写,或设置 VOLCENGINE_ARK_API_KEY
"
;
private
final
ArkProperties
properties
;
private
final
ArkProperties
properties
;
private
final
RestClient
restClient
;
private
final
PlatformModelConfigService
platformModelConfigService
;
private
final
ProviderRestClientFactory
restClientFactory
;
public
ArkLlmService
(
ArkProperties
properties
)
{
public
ArkLlmService
(
ArkProperties
properties
)
{
this
(
properties
,
null
,
new
ProviderRestClientFactory
());
}
@Autowired
public
ArkLlmService
(
ArkProperties
properties
,
PlatformModelConfigService
platformModelConfigService
,
ProviderRestClientFactory
restClientFactory
)
{
this
.
properties
=
properties
;
this
.
properties
=
properties
;
this
.
restClient
=
RestClient
.
builder
()
this
.
platformModelConfigService
=
platformModelConfigService
;
.
baseUrl
(
properties
.
getBaseUrl
())
this
.
restClientFactory
=
restClientFactory
;
.
defaultHeader
(
"Content-Type"
,
"application/json"
)
.
build
();
}
}
@Override
@Override
public
String
chat
(
ChatRequest
request
)
{
public
String
chat
(
ChatRequest
request
)
{
String
apiKey
=
resolveApiKey
();
ResolvedModelConfig
config
=
resolveConfig
();
// 如果未指定 model,使用默认文本模型
String
apiKey
=
resolveApiKey
(
config
);
if
(
request
.
getModel
()
==
null
)
{
if
(
request
.
getModel
()
==
null
||
request
.
getModel
().
isBlank
()
)
{
request
.
setModel
(
properties
.
getTextModel
());
request
.
setModel
(
config
.
modelId
());
}
}
log
.
debug
(
"ARK LLM call: model={}, messages={}"
,
request
.
getModel
(),
request
.
getMessages
().
size
());
log
.
debug
(
"LLM call: provider={}, model={}, messages={}"
,
config
.
provider
(),
request
.
getModel
(),
request
.
getMessages
().
size
());
try
{
try
{
ChatResponse
response
=
restClient
.
post
()
ChatResponse
response
=
restClient
Factory
.
create
(
config
)
.
post
()
.
uri
(
"/chat/completions"
)
.
uri
(
config
.
endpoint
()
)
.
header
(
"Authorization"
,
"Bearer "
+
apiKey
)
.
header
(
"Authorization"
,
"Bearer "
+
apiKey
)
.
body
(
request
)
.
body
(
request
)
.
retrieve
()
.
retrieve
()
...
@@ -52,53 +64,72 @@ public class ArkLlmService implements LlmService {
...
@@ -52,53 +64,72 @@ public class ArkLlmService implements LlmService {
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"AI 服务返回空结果"
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"AI 服务返回空结果"
);
}
}
String
content
=
response
.
firstContent
();
String
content
=
response
.
firstContent
();
log
.
debug
(
"
ARK
LLM response: {} chars"
,
content
.
length
());
log
.
debug
(
"LLM response: {} chars"
,
content
.
length
());
return
content
;
return
content
;
}
catch
(
RestClientResponseException
e
)
{
}
catch
(
RestClientResponseException
e
)
{
if
(
e
.
getStatusCode
().
value
()
==
400
&&
request
.
getResponseFormat
()
!=
null
)
{
if
(
e
.
getStatusCode
().
value
()
==
400
&&
request
.
getResponseFormat
()
!=
null
)
{
log
.
warn
(
"
ARK
LLM response_format not accepted, retrying without response_format: body={}"
,
log
.
warn
(
"LLM response_format not accepted, retrying without response_format: body={}"
,
e
.
getResponseBodyAsString
());
e
.
getResponseBodyAsString
());
request
.
setResponseFormat
(
null
);
request
.
setResponseFormat
(
null
);
return
chat
(
request
);
return
chat
(
request
);
}
}
if
(
e
.
getStatusCode
().
value
()
==
401
)
{
if
(
e
.
getStatusCode
().
value
()
==
401
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"文本模型鉴权失败,请检查 API Key 是否有效"
);
"AI 服务鉴权失败,请检查 VOLCENGINE_ARK_API_KEY 是否正确且仍有效"
);
}
}
log
.
error
(
"
ARK
LLM call failed: status={}, body={}"
,
e
.
getStatusCode
(),
e
.
getResponseBodyAsString
(),
e
);
log
.
error
(
"LLM call failed: status={}, body={}"
,
e
.
getStatusCode
(),
e
.
getResponseBodyAsString
(),
e
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"
AI 服务
调用失败: HTTP "
+
e
.
getStatusCode
().
value
());
"
文本模型
调用失败: HTTP "
+
e
.
getStatusCode
().
value
());
}
catch
(
BizException
e
)
{
}
catch
(
BizException
e
)
{
throw
e
;
throw
e
;
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
log
.
error
(
"
ARK
LLM call failed"
,
e
);
log
.
error
(
"LLM call failed"
,
e
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"
AI 服务
调用失败: "
+
e
.
getMessage
());
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"
文本模型
调用失败: "
+
e
.
getMessage
());
}
}
}
}
@Override
@Override
public
String
getModelId
()
{
public
String
getModelId
()
{
return
properties
.
getTextModel
();
return
resolveConfig
().
modelId
();
}
}
@Override
@Override
public
String
getModelName
()
{
public
String
getModelName
()
{
return
"豆包 Pro 文本生成"
;
return
resolveConfig
().
modelName
()
;
}
}
@Override
@Override
public
String
getProvider
()
{
public
String
getProvider
()
{
return
"volcengine"
;
return
resolveConfig
().
provider
();
}
private
ResolvedModelConfig
resolveConfig
()
{
if
(
platformModelConfigService
!=
null
)
{
return
platformModelConfigService
.
resolve
(
ModelCapability
.
TEXT
);
}
return
new
ResolvedModelConfig
(
"volcengine"
,
"Volcengine ARK"
,
"openai"
,
properties
.
getBaseUrl
(),
properties
.
getApiKey
(),
properties
.
getTextModel
(),
"Doubao Text"
,
"/chat/completions"
,
null
,
null
);
}
}
private
String
resolveApiKey
()
{
private
String
resolveApiKey
(
ResolvedModelConfig
config
)
{
String
apiKey
=
properties
.
getA
piKey
();
String
apiKey
=
config
.
a
piKey
();
if
(
apiKey
==
null
||
apiKey
.
isBlank
())
{
if
(
apiKey
==
null
||
apiKey
.
isBlank
())
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
A
RK_A
PI_KEY_HINT
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
API_KEY_HINT
);
}
}
String
normalized
=
apiKey
.
trim
();
String
normalized
=
apiKey
.
trim
();
if
(
"your-ark-api-key"
.
equalsIgnoreCase
(
normalized
)
||
"your_ark_api_key"
.
equalsIgnoreCase
(
normalized
))
{
if
(
"your-ark-api-key"
.
equalsIgnoreCase
(
normalized
)
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
ARK_API_KEY_HINT
);
||
"your_ark_api_key"
.
equalsIgnoreCase
(
normalized
)
||
PlatformModelConfigService
.
MASKED_API_KEY
.
equals
(
normalized
))
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
API_KEY_HINT
);
}
}
return
normalized
;
return
normalized
;
}
}
...
...
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/SeedanceServiceImpl.java
View file @
fde0aba7
This diff is collapsed.
Click to expand it.
yaoai-comic-studio/yaoai-ai-providers/src/main/java/com/yaoai/ai/providers/service/impl/SeedreamServiceImpl.java
View file @
fde0aba7
...
@@ -3,15 +3,18 @@ package com.yaoai.ai.providers.service.impl;
...
@@ -3,15 +3,18 @@ package com.yaoai.ai.providers.service.impl;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.JsonNode
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.fasterxml.jackson.databind.ObjectMapper
;
import
com.yaoai.ai.providers.config.ArkProperties
;
import
com.yaoai.ai.providers.config.ArkProperties
;
import
com.yaoai.ai.providers.config.ModelCapability
;
import
com.yaoai.ai.providers.config.PlatformModelConfigService
;
import
com.yaoai.ai.providers.config.ProviderRestClientFactory
;
import
com.yaoai.ai.providers.config.ResolvedModelConfig
;
import
com.yaoai.ai.providers.service.SeedreamService
;
import
com.yaoai.ai.providers.service.SeedreamService
;
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
lombok.Data
;
import
lombok.Data
;
import
lombok.extern.slf4j.Slf4j
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.http.client.SimpleClientHttpRequestFactory
;
import
org.springframework.stereotype.Service
;
import
org.springframework.stereotype.Service
;
import
org.springframework.web.client.RestClient
;
import
org.springframework.web.client.RestClientResponseException
;
import
org.springframework.web.client.RestClientResponseException
;
import
java.time.Duration
;
import
java.time.Duration
;
...
@@ -24,23 +27,25 @@ import java.util.Map;
...
@@ -24,23 +27,25 @@ import java.util.Map;
@ConditionalOnProperty
(
name
=
"ai.local-stub.enabled"
,
havingValue
=
"false"
,
matchIfMissing
=
true
)
@ConditionalOnProperty
(
name
=
"ai.local-stub.enabled"
,
havingValue
=
"false"
,
matchIfMissing
=
true
)
public
class
SeedreamServiceImpl
implements
SeedreamService
{
public
class
SeedreamServiceImpl
implements
SeedreamService
{
private
static
final
String
A
RK_A
PI_KEY_HINT
=
private
static
final
String
API_KEY_HINT
=
"未配置
火山方舟 API Key,请设置环境变量 VOLCENGINE_ARK_API_KEY,或在 application-local.yml 中配置 volcengine.ark.api-key
"
;
"未配置
图片模型 API Key,请在运营端模型配置中填写,或设置 VOLCENGINE_ARK_API_KEY
"
;
private
static
final
ObjectMapper
JSON
=
new
ObjectMapper
();
private
static
final
ObjectMapper
JSON
=
new
ObjectMapper
();
private
final
ArkProperties
properties
;
private
final
ArkProperties
properties
;
private
final
RestClient
restClient
;
private
final
PlatformModelConfigService
platformModelConfigService
;
private
final
ProviderRestClientFactory
restClientFactory
;
public
SeedreamServiceImpl
(
ArkProperties
properties
)
{
public
SeedreamServiceImpl
(
ArkProperties
properties
)
{
this
(
properties
,
null
,
new
ProviderRestClientFactory
());
}
@Autowired
public
SeedreamServiceImpl
(
ArkProperties
properties
,
PlatformModelConfigService
platformModelConfigService
,
ProviderRestClientFactory
restClientFactory
)
{
this
.
properties
=
properties
;
this
.
properties
=
properties
;
SimpleClientHttpRequestFactory
rf
=
new
SimpleClientHttpRequestFactory
();
this
.
platformModelConfigService
=
platformModelConfigService
;
rf
.
setConnectTimeout
(
Duration
.
ofSeconds
(
10
));
this
.
restClientFactory
=
restClientFactory
;
rf
.
setReadTimeout
(
Duration
.
ofMinutes
(
2
));
this
.
restClient
=
RestClient
.
builder
()
.
baseUrl
(
properties
.
getBaseUrl
())
.
defaultHeader
(
"Content-Type"
,
"application/json"
)
.
requestFactory
(
rf
)
.
build
();
}
}
@Override
@Override
...
@@ -55,13 +60,17 @@ public class SeedreamServiceImpl implements SeedreamService {
...
@@ -55,13 +60,17 @@ public class SeedreamServiceImpl implements SeedreamService {
@Override
@Override
public
String
generateImage
(
String
prompt
,
String
size
,
List
<
String
>
referenceImageUrls
)
{
public
String
generateImage
(
String
prompt
,
String
size
,
List
<
String
>
referenceImageUrls
)
{
String
apiKey
=
resolveApiKey
();
ResolvedModelConfig
config
=
resolveConfig
();
String
apiKey
=
resolveApiKey
(
config
);
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<>();
Map
<
String
,
Object
>
body
=
new
LinkedHashMap
<>();
body
.
put
(
"model"
,
properties
.
getImageModel
());
body
.
put
(
"model"
,
config
.
modelId
());
body
.
put
(
"prompt"
,
prompt
);
body
.
put
(
"prompt"
,
prompt
);
body
.
put
(
"n"
,
1
);
body
.
put
(
"n"
,
1
);
body
.
put
(
"size"
,
size
);
body
.
put
(
"size"
,
size
);
body
.
put
(
"response_format"
,
"url"
);
if
(!
config
.
isAgnes
())
{
body
.
put
(
"response_format"
,
"url"
);
}
List
<
String
>
cleanReferences
=
referenceImageUrls
==
null
List
<
String
>
cleanReferences
=
referenceImageUrls
==
null
?
List
.
of
()
?
List
.
of
()
:
referenceImageUrls
.
stream
()
:
referenceImageUrls
.
stream
()
...
@@ -69,31 +78,34 @@ public class SeedreamServiceImpl implements SeedreamService {
...
@@ -69,31 +78,34 @@ public class SeedreamServiceImpl implements SeedreamService {
.
map
(
String:
:
trim
)
.
map
(
String:
:
trim
)
.
toList
();
.
toList
();
if
(!
cleanReferences
.
isEmpty
())
{
if
(!
cleanReferences
.
isEmpty
())
{
body
.
put
(
"image"
,
cleanReferences
.
size
()
==
1
?
cleanReferences
.
get
(
0
)
:
cleanReferences
);
if
(
config
.
isProtocol
(
"openai"
)
||
config
.
isProtocol
(
"agnes"
))
{
body
.
put
(
"image_urls"
,
cleanReferences
);
}
else
{
body
.
put
(
"image"
,
cleanReferences
.
size
()
==
1
?
cleanReferences
.
get
(
0
)
:
cleanReferences
);
}
}
}
log
.
info
(
"
Seedream generate image:
model={}, size={}, references={}"
,
log
.
info
(
"
Image generate: provider={},
model={}, size={}, references={}"
,
properties
.
getImageModel
(),
size
,
cleanReferences
.
size
());
config
.
provider
(),
config
.
modelId
(),
size
,
cleanReferences
.
size
());
try
{
try
{
ImageResponse
resp
=
restClient
.
post
()
ImageResponse
resp
=
restClient
Factory
.
create
(
config
,
Duration
.
ofSeconds
(
10
),
Duration
.
ofMinutes
(
2
))
.
post
()
.
uri
(
"/images/generations"
)
.
uri
(
config
.
endpoint
()
)
.
header
(
"Authorization"
,
"Bearer "
+
apiKey
)
.
header
(
"Authorization"
,
"Bearer "
+
apiKey
)
.
body
(
body
)
.
body
(
body
)
.
retrieve
()
.
retrieve
()
.
body
(
ImageResponse
.
class
);
.
body
(
ImageResponse
.
class
);
if
(
resp
==
null
||
resp
.
getData
()
==
null
||
resp
.
getData
().
isEmpty
())
{
if
(
resp
==
null
||
resp
.
getData
()
==
null
||
resp
.
getData
().
isEmpty
())
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"
Seedream
返回空结果"
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"
图片模型
返回空结果"
);
}
}
String
url
=
resp
.
getData
().
get
(
0
).
getUrl
();
String
url
=
resp
.
getData
().
get
(
0
).
getUrl
();
log
.
info
(
"
Seedream i
mage generated: url={}"
,
url
);
log
.
info
(
"
I
mage generated: url={}"
,
url
);
return
url
;
return
url
;
}
catch
(
RestClientResponseException
e
)
{
}
catch
(
RestClientResponseException
e
)
{
if
(
e
.
getStatusCode
().
value
()
==
401
)
{
if
(
e
.
getStatusCode
().
value
()
==
401
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"图片模型鉴权失败,请检查 API Key 是否有效"
);
"图片服务鉴权失败,请检查 VOLCENGINE_ARK_API_KEY 是否正确且仍有效"
);
}
}
log
.
error
(
"
Seedream
generate failed: status={}, body={}"
,
e
.
getStatusCode
(),
e
.
getResponseBodyAsString
(),
e
);
log
.
error
(
"
Image
generate failed: status={}, body={}"
,
e
.
getStatusCode
(),
e
.
getResponseBodyAsString
(),
e
);
String
providerMessage
=
extractProviderError
(
e
.
getResponseBodyAsString
());
String
providerMessage
=
extractProviderError
(
e
.
getResponseBodyAsString
());
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"图片生成失败: HTTP "
+
e
.
getStatusCode
().
value
()
"图片生成失败: HTTP "
+
e
.
getStatusCode
().
value
()
...
@@ -101,29 +113,54 @@ public class SeedreamServiceImpl implements SeedreamService {
...
@@ -101,29 +113,54 @@ public class SeedreamServiceImpl implements SeedreamService {
}
catch
(
BizException
e
)
{
}
catch
(
BizException
e
)
{
throw
e
;
throw
e
;
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
log
.
error
(
"
Seedream
generate failed"
,
e
);
log
.
error
(
"
Image
generate failed"
,
e
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"图片生成失败: "
+
e
.
getMessage
());
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"图片生成失败: "
+
e
.
getMessage
());
}
}
}
}
@Override
@Override
public
String
getModelId
()
{
public
String
getModelId
()
{
return
properties
.
getImageModel
();
return
resolveConfig
().
modelId
();
}
}
@Override
@Override
public
String
getModelName
()
{
public
String
getModelName
()
{
return
"Seedream 文生图"
;
return
resolveConfig
().
modelName
();
}
@Override
public
String
getProvider
()
{
return
resolveConfig
().
provider
();
}
private
ResolvedModelConfig
resolveConfig
()
{
if
(
platformModelConfigService
!=
null
)
{
return
platformModelConfigService
.
resolve
(
ModelCapability
.
IMAGE
);
}
return
new
ResolvedModelConfig
(
"volcengine"
,
"Volcengine ARK"
,
"seedream"
,
properties
.
getBaseUrl
(),
properties
.
getApiKey
(),
properties
.
getImageModel
(),
"Seedream Image"
,
"/images/generations"
,
null
,
null
);
}
}
private
String
resolveApiKey
()
{
private
String
resolveApiKey
(
ResolvedModelConfig
config
)
{
String
apiKey
=
properties
.
getA
piKey
();
String
apiKey
=
config
.
a
piKey
();
if
(
apiKey
==
null
||
apiKey
.
isBlank
())
{
if
(
apiKey
==
null
||
apiKey
.
isBlank
())
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
A
RK_A
PI_KEY_HINT
);
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
API_KEY_HINT
);
}
}
String
normalized
=
apiKey
.
trim
();
String
normalized
=
apiKey
.
trim
();
if
(
"your-ark-api-key"
.
equalsIgnoreCase
(
normalized
)
||
"your_ark_api_key"
.
equalsIgnoreCase
(
normalized
))
{
if
(
"your-ark-api-key"
.
equalsIgnoreCase
(
normalized
)
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
ARK_API_KEY_HINT
);
||
"your_ark_api_key"
.
equalsIgnoreCase
(
normalized
)
||
PlatformModelConfigService
.
MASKED_API_KEY
.
equals
(
normalized
))
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
API_KEY_HINT
);
}
}
return
normalized
;
return
normalized
;
}
}
...
@@ -145,8 +182,6 @@ public class SeedreamServiceImpl implements SeedreamService {
...
@@ -145,8 +182,6 @@ public class SeedreamServiceImpl implements SeedreamService {
return
normalized
.
length
()
>
300
?
normalized
.
substring
(
0
,
300
)
+
"..."
:
normalized
;
return
normalized
.
length
()
>
300
?
normalized
.
substring
(
0
,
300
)
+
"..."
:
normalized
;
}
}
// ---- internal response models ----
@Data
@Data
private
static
class
ImageResponse
{
private
static
class
ImageResponse
{
private
Long
created
;
private
Long
created
;
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/controller/VideoTaskController.java
View file @
fde0aba7
...
@@ -73,7 +73,6 @@ public class VideoTaskController {
...
@@ -73,7 +73,6 @@ public class VideoTaskController {
.
userPromptRaw
(
buildUserPromptMap
(
up
,
req
.
getFreeformPrompt
()))
.
userPromptRaw
(
buildUserPromptMap
(
up
,
req
.
getFreeformPrompt
()))
.
durationSeconds
(
req
.
getDuration
())
.
durationSeconds
(
req
.
getDuration
())
.
ratio
(
req
.
getRatio
())
.
ratio
(
req
.
getRatio
())
.
model
(
req
.
getModel
())
.
generateAudio
(
req
.
getGenerateAudio
())
.
generateAudio
(
req
.
getGenerateAudio
())
.
build
();
.
build
();
...
...
yaoai-comic-studio/yaoai-api/src/main/java/com/yaoai/api/dto/ai/StructuredVideoGenerateRequest.java
View file @
fde0aba7
...
@@ -61,9 +61,8 @@ public class StructuredVideoGenerateRequest {
...
@@ -61,9 +61,8 @@ public class StructuredVideoGenerateRequest {
@Pattern
(
regexp
=
"16:9|9:16|1:1"
,
message
=
"宽高比仅支持 16:9 / 9:16 / 1:1"
)
@Pattern
(
regexp
=
"16:9|9:16|1:1"
,
message
=
"宽高比仅支持 16:9 / 9:16 / 1:1"
)
private
String
ratio
=
"16:9"
;
private
String
ratio
=
"16:9"
;
/** Seedance 模型 ID(doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128),为空走 ArkProperties 默认值 */
/** @deprecated 模型统一由运营端视频模型配置决定,客户端传入值会被忽略。 */
@Pattern
(
regexp
=
"doubao-seedance-2-0-260128|doubao-seedance-2-0-fast-260128"
,
@Deprecated
message
=
"模型仅支持 Seedance 2.0 标准 / 极速"
)
private
String
model
;
private
String
model
;
/** 是否生成配音 */
/** 是否生成配音 */
...
...
yaoai-comic-studio/yaoai-bootstrap/src/test/java/com/yaoai/ai/providers/config/PlatformModelConfigServiceTest.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
ai
.
providers
.
config
;
import
com.yaoai.domain.entity.SystemSetting
;
import
com.yaoai.domain.mapper.SystemSettingMapper
;
import
org.junit.jupiter.api.Test
;
import
java.util.Optional
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertEquals
;
import
static
org
.
junit
.
jupiter
.
api
.
Assertions
.
assertTrue
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
static
org
.
mockito
.
Mockito
.
when
;
class
PlatformModelConfigServiceTest
{
@Test
void
resolveUsesEnabledPlatformConfigBeforeEnvironmentDefaults
()
{
SystemSettingMapper
mapper
=
mock
(
SystemSettingMapper
.
class
);
SystemSetting
setting
=
new
SystemSetting
();
setting
.
setSettingKey
(
PlatformModelConfigService
.
SETTING_KEY
);
setting
.
setSettingValue
(
"""
{
"
text
": {
"
provider
": "
agnes
",
"
providerName
": "
Agnes
",
"
protocol
": "
openai
",
"
baseUrl
": "
https:
//apihub.agnes-ai.com/v1",
"apiKey"
:
"agnes-secret"
,
"modelId"
:
"agnes-2.0-flash"
,
"modelName"
:
"Agnes 2.0 Flash"
,
"endpoint"
:
"/chat/completions"
,
"resolveIp"
:
"104.18.18.62"
,
"enabled"
:
true
}
}
""");
when(mapper.findByKey(PlatformModelConfigService.SETTING_KEY)).thenReturn(Optional.of(setting));
PlatformModelConfigService service = new PlatformModelConfigService(mapper, buildArkProperties());
ResolvedModelConfig config = service.resolve(ModelCapability.TEXT);
assertEquals("
agnes
", config.provider());
assertEquals("
openai
", config.protocol());
assertEquals("
https:
//apihub.agnes-ai.com/v1", config.baseUrl());
assertEquals
(
"agnes-secret"
,
config
.
apiKey
());
assertEquals
(
"agnes-2.0-flash"
,
config
.
modelId
());
assertEquals
(
"Agnes 2.0 Flash"
,
config
.
modelName
());
assertEquals
(
"/chat/completions"
,
config
.
endpoint
());
assertEquals
(
"104.18.18.62"
,
config
.
resolveIp
());
}
@Test
void
resolveFallsBackToVolcenginePropertiesWhenPlatformConfigIsMissing
()
{
SystemSettingMapper
mapper
=
mock
(
SystemSettingMapper
.
class
);
when
(
mapper
.
findByKey
(
PlatformModelConfigService
.
SETTING_KEY
)).
thenReturn
(
Optional
.
empty
());
PlatformModelConfigService
service
=
new
PlatformModelConfigService
(
mapper
,
buildArkProperties
());
ResolvedModelConfig
config
=
service
.
resolve
(
ModelCapability
.
IMAGE
);
assertEquals
(
"volcengine"
,
config
.
provider
());
assertEquals
(
"seedream"
,
config
.
protocol
());
assertEquals
(
"https://ark.cn-beijing.volces.com/api/v3"
,
config
.
baseUrl
());
assertEquals
(
"ark-secret"
,
config
.
apiKey
());
assertEquals
(
"doubao-seedream-test"
,
config
.
modelId
());
assertTrue
(
config
.
modelName
().
contains
(
"Seedream"
));
assertEquals
(
"/images/generations"
,
config
.
endpoint
());
}
@Test
void
adminSettingsMasksStoredApiKeys
()
{
SystemSettingMapper
mapper
=
mock
(
SystemSettingMapper
.
class
);
SystemSetting
setting
=
new
SystemSetting
();
setting
.
setSettingKey
(
PlatformModelConfigService
.
SETTING_KEY
);
setting
.
setSettingValue
(
"""
{
"
video
": {
"
provider
": "
agnes
",
"
apiKey
": "
agnes
-
secret
",
"
modelId
": "
agnes
-
video
-
v2
.
0
",
"
enabled
": true
}
}
"""
);
when
(
mapper
.
findByKey
(
PlatformModelConfigService
.
SETTING_KEY
)).
thenReturn
(
Optional
.
of
(
setting
));
PlatformModelConfigService
service
=
new
PlatformModelConfigService
(
mapper
,
buildArkProperties
());
PlatformModelSettings
settings
=
service
.
getAdminSettings
();
assertEquals
(
"******"
,
settings
.
getVideo
().
getApiKey
());
}
private
static
ArkProperties
buildArkProperties
()
{
ArkProperties
properties
=
new
ArkProperties
();
properties
.
setApiKey
(
"ark-secret"
);
properties
.
setBaseUrl
(
"https://ark.cn-beijing.volces.com/api/v3"
);
properties
.
setTextModel
(
"doubao-text-test"
);
properties
.
setImageModel
(
"doubao-seedream-test"
);
properties
.
setVideoModel
(
"doubao-seedance-test"
);
return
properties
;
}
}
yaoai-comic-studio/yaoai-domain/src/main/java/com/yaoai/domain/mapper/AiTaskMapper.java
View file @
fde0aba7
...
@@ -30,7 +30,10 @@ public interface AiTaskMapper extends BaseMapper<AiTask> {
...
@@ -30,7 +30,10 @@ public interface AiTaskMapper extends BaseMapper<AiTask> {
/** 待轮询的视频任务:已提交 Ark 且未到终态。limit 控制单轮处理上限,避免单次扫描挤占线程。 */
/** 待轮询的视频任务:已提交 Ark 且未到终态。limit 控制单轮处理上限,避免单次扫描挤占线程。 */
@Select
(
"""
@Select
(
"""
SELECT * FROM ai_tasks
SELECT * FROM ai_tasks
WHERE status IN ('submitted','running')
WHERE (
status IN ('submitted','running')
OR (status = 'succeeded' AND (result_video_url IS NULL OR result_video_url = ''))
)
AND external_task_id IS NOT NULL
AND external_task_id IS NOT NULL
ORDER BY created_at ASC
ORDER BY created_at ASC
LIMIT #{limit}
LIMIT #{limit}
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/async/VideoTaskCompletionService.java
View file @
fde0aba7
...
@@ -5,22 +5,10 @@ import com.yaoai.ai.providers.service.SeedanceService;
...
@@ -5,22 +5,10 @@ import com.yaoai.ai.providers.service.SeedanceService;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
import
com.yaoai.pipeline.service.ShotAssetService
;
import
com.yaoai.pipeline.service.ShotAssetService
;
import
com.yaoai.storage.service.TosService
;
import
lombok.RequiredArgsConstructor
;
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.io.ByteArrayInputStream
;
import
java.net.URI
;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpResponse
;
import
java.time.Duration
;
/**
* 单条视频任务的查询与完成处理:查 Ark → 更新 DB → 转存视频到自家 TOS。
* 给 {@link VideoTaskPoller} 调用,未来若加管理后台补救接口也可复用。
*/
@Slf4j
@Slf4j
@Service
@Service
@RequiredArgsConstructor
@RequiredArgsConstructor
...
@@ -28,15 +16,13 @@ public class VideoTaskCompletionService {
...
@@ -28,15 +16,13 @@ public class VideoTaskCompletionService {
private
final
AiTaskMapper
aiTaskMapper
;
private
final
AiTaskMapper
aiTaskMapper
;
private
final
SeedanceService
seedanceService
;
private
final
SeedanceService
seedanceService
;
private
final
TosService
tosService
;
private
final
ShotAssetService
shotAssetService
;
private
final
ShotAssetService
shotAssetService
;
private
final
HttpClient
httpClient
=
HttpClient
.
newBuilder
()
private
final
VideoTransferService
videoTransferService
;
.
connectTimeout
(
Duration
.
ofSeconds
(
10
))
.
build
();
/**
/**
* 单次轮询一个任务:查 Ark 状态并按状态更新 DB;succeeded 时把视频转存到我们桶。
* Poll one video task and update local state.
* 返回此任务在本轮处理后是否进入终态(succeeded/failed)。
* When provider video is ready, write the provider URL immediately so the UI can play it,
* then transfer it to TOS asynchronously and replace the URL later.
*/
*/
public
boolean
pollOnce
(
AiTask
task
)
{
public
boolean
pollOnce
(
AiTask
task
)
{
Long
taskId
=
task
.
getId
();
Long
taskId
=
task
.
getId
();
...
@@ -44,34 +30,38 @@ public class VideoTaskCompletionService {
...
@@ -44,34 +30,38 @@ public class VideoTaskCompletionService {
try
{
try
{
VideoTaskResult
result
=
seedanceService
.
getTaskStatus
(
externalTaskId
);
VideoTaskResult
result
=
seedanceService
.
getTaskStatus
(
externalTaskId
);
String
status
=
result
.
getStatus
();
String
status
=
result
.
getStatus
();
log
.
debug
(
"Polling
Ark
: taskId={}, externalId={}, status={}"
,
taskId
,
externalTaskId
,
status
);
log
.
debug
(
"Polling
video provider
: taskId={}, externalId={}, status={}"
,
taskId
,
externalTaskId
,
status
);
if
(
"succeeded"
.
equals
(
status
)
||
"failed"
.
equals
(
status
))
{
if
(
"succeeded"
.
equals
(
status
)
||
"failed"
.
equals
(
status
))
{
String
videoUrl
=
result
.
getVideoUrl
();
if
(
"succeeded"
.
equals
(
status
)
&&
(
videoUrl
==
null
||
videoUrl
.
isBlank
()))
{
log
.
warn
(
"Video task succeeded but URL is empty, will retry: taskId={}, externalId={}"
,
taskId
,
externalTaskId
);
return
false
;
}
AiTask
done
=
new
AiTask
();
AiTask
done
=
new
AiTask
();
done
.
setId
(
taskId
);
done
.
setId
(
taskId
);
done
.
setStatus
(
status
);
done
.
setStatus
(
status
);
if
(
result
.
getVideoUrl
()
!=
null
)
{
if
(
"succeeded"
.
equals
(
status
))
{
String
persistentUrl
=
persistVideoToTos
(
done
.
setResultVideoUrl
(
videoUrl
);
task
.
getTenantId
(),
task
.
getProjectId
(),
taskId
,
result
.
getVideoUrl
());
done
.
setResultVideoUrl
(
persistentUrl
);
}
}
if
(
result
.
getErrorMessage
()
!=
null
)
{
if
(
result
.
getErrorMessage
()
!=
null
)
{
done
.
setErrorMessage
(
result
.
getErrorMessage
());
done
.
setErrorMessage
(
result
.
getErrorMessage
());
}
}
aiTaskMapper
.
updateById
(
done
);
aiTaskMapper
.
updateById
(
done
);
if
(
"succeeded"
.
equals
(
status
))
{
if
(
"succeeded"
.
equals
(
status
))
{
shotAssetService
.
markVideoAssetSucceeded
(
shotAssetService
.
markVideoAssetSucceeded
(
task
.
getTenantId
(),
taskId
,
videoUrl
);
task
.
getTenantId
(),
taskId
,
done
.
getResultVideoUrl
()
);
videoTransferService
.
transferVideoToTos
(
task
.
getTenantId
(),
task
.
getProjectId
(),
taskId
,
videoUrl
);
}
else
{
}
else
{
shotAssetService
.
markVideoAssetFailed
(
shotAssetService
.
markVideoAssetFailed
(
task
.
getTenantId
(),
taskId
,
done
.
getErrorMessage
());
task
.
getTenantId
(),
taskId
,
done
.
getErrorMessage
());
}
}
log
.
info
(
"Video task completed: taskId={}, status={}, url={}"
,
taskId
,
status
,
done
.
getResultVideoUrl
()
);
log
.
info
(
"Video task completed: taskId={}, status={}, url={}"
,
taskId
,
status
,
videoUrl
);
return
true
;
return
true
;
}
}
// running 时同步状态,便于前端区分 submitted/running
if
(
"running"
.
equals
(
status
)
&&
!
"running"
.
equals
(
task
.
getStatus
()))
{
if
(
"running"
.
equals
(
status
)
&&
!
"running"
.
equals
(
task
.
getStatus
()))
{
AiTask
running
=
new
AiTask
();
AiTask
running
=
new
AiTask
();
running
.
setId
(
taskId
);
running
.
setId
(
taskId
);
...
@@ -80,38 +70,8 @@ public class VideoTaskCompletionService {
...
@@ -80,38 +70,8 @@ public class VideoTaskCompletionService {
}
}
return
false
;
return
false
;
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
log
.
warn
(
"
Ark
poll error (will retry): taskId={}, error={}"
,
taskId
,
e
.
getMessage
());
log
.
warn
(
"
Video
poll error (will retry): taskId={}, error={}"
,
taskId
,
e
.
getMessage
());
return
false
;
return
false
;
}
}
}
}
/**
* 把 Ark 临时视频 URL 下载下来上传到我们 public-read TOS bucket,返回永不过期的公开 URL。
* 失败时退回原始 URL(24h 内仍可用)+ 错误日志,不阻塞任务完成流程。
*/
public
String
persistVideoToTos
(
Long
tenantId
,
Long
projectId
,
Long
taskId
,
String
arkVideoUrl
)
{
try
{
HttpRequest
request
=
HttpRequest
.
newBuilder
()
.
uri
(
URI
.
create
(
arkVideoUrl
))
.
timeout
(
Duration
.
ofMinutes
(
2
))
.
GET
()
.
build
();
HttpResponse
<
byte
[]>
response
=
httpClient
.
send
(
request
,
HttpResponse
.
BodyHandlers
.
ofByteArray
());
if
(
response
.
statusCode
()
!=
200
)
{
log
.
warn
(
"Download Ark video failed (status={}), keep raw URL: taskId={}"
,
response
.
statusCode
(),
taskId
);
return
arkVideoUrl
;
}
byte
[]
bytes
=
response
.
body
();
String
key
=
String
.
format
(
"%d/%d/video/%d.mp4"
,
tenantId
,
projectId
,
taskId
);
try
(
var
is
=
new
ByteArrayInputStream
(
bytes
))
{
tosService
.
upload
(
key
,
is
,
bytes
.
length
,
"video/mp4"
);
}
String
publicUrl
=
tosService
.
publicUrl
(
key
);
log
.
info
(
"Video persisted to TOS: taskId={}, key={}, size={}"
,
taskId
,
key
,
bytes
.
length
);
return
publicUrl
;
}
catch
(
Exception
e
)
{
log
.
error
(
"Persist Ark video to TOS failed, fallback to raw URL: taskId={}"
,
taskId
,
e
);
return
arkVideoUrl
;
}
}
}
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/async/VideoTransferService.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
pipeline
.
async
;
import
com.yaoai.domain.entity.AiTask
;
import
com.yaoai.domain.mapper.AiTaskMapper
;
import
com.yaoai.pipeline.service.ShotAssetService
;
import
com.yaoai.storage.service.TosService
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Service
;
import
java.io.ByteArrayInputStream
;
import
java.net.URI
;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpResponse
;
import
java.time.Duration
;
@Slf4j
@Service
@RequiredArgsConstructor
public
class
VideoTransferService
{
private
final
AiTaskMapper
aiTaskMapper
;
private
final
TosService
tosService
;
private
final
ShotAssetService
shotAssetService
;
private
final
HttpClient
httpClient
=
HttpClient
.
newBuilder
()
.
connectTimeout
(
Duration
.
ofSeconds
(
10
))
.
build
();
@Async
public
void
transferVideoToTos
(
Long
tenantId
,
Long
projectId
,
Long
taskId
,
String
temporaryVideoUrl
)
{
try
{
if
(
temporaryVideoUrl
==
null
||
temporaryVideoUrl
.
isBlank
())
{
return
;
}
AiTask
current
=
aiTaskMapper
.
selectById
(
taskId
);
if
(
current
==
null
||
!
tenantId
.
equals
(
current
.
getTenantId
()))
{
log
.
warn
(
"Skip video transfer: task not found or tenant mismatch, taskId={}"
,
taskId
);
return
;
}
if
(!
temporaryVideoUrl
.
equals
(
current
.
getResultVideoUrl
()))
{
log
.
info
(
"Skip stale video transfer: taskId={}"
,
taskId
);
return
;
}
log
.
info
(
"Video transfer started: taskId={}, temporaryUrl={}"
,
taskId
,
temporaryVideoUrl
);
HttpRequest
request
=
HttpRequest
.
newBuilder
()
.
uri
(
URI
.
create
(
temporaryVideoUrl
))
.
timeout
(
Duration
.
ofMinutes
(
2
))
.
GET
()
.
build
();
HttpResponse
<
byte
[]>
response
=
httpClient
.
send
(
request
,
HttpResponse
.
BodyHandlers
.
ofByteArray
());
if
(
response
.
statusCode
()
!=
200
)
{
log
.
warn
(
"Download provider video failed (status={}), keep temporary URL: taskId={}"
,
response
.
statusCode
(),
taskId
);
return
;
}
byte
[]
bytes
=
response
.
body
();
String
key
=
String
.
format
(
"%d/%d/video/%d.mp4"
,
tenantId
,
projectId
,
taskId
);
try
(
var
input
=
new
ByteArrayInputStream
(
bytes
))
{
tosService
.
upload
(
key
,
input
,
bytes
.
length
,
"video/mp4"
);
}
AiTask
latest
=
aiTaskMapper
.
selectById
(
taskId
);
if
(
latest
==
null
||
!
tenantId
.
equals
(
latest
.
getTenantId
())
||
!
temporaryVideoUrl
.
equals
(
latest
.
getResultVideoUrl
()))
{
log
.
info
(
"Skip stale video replace after upload: taskId={}"
,
taskId
);
return
;
}
String
publicUrl
=
tosService
.
publicUrl
(
key
);
AiTask
patch
=
new
AiTask
();
patch
.
setId
(
taskId
);
patch
.
setResultVideoUrl
(
publicUrl
);
patch
.
setResultTosKey
(
key
);
aiTaskMapper
.
updateById
(
patch
);
shotAssetService
.
markVideoAssetSucceeded
(
tenantId
,
taskId
,
publicUrl
);
log
.
info
(
"Video transfer completed: taskId={}, key={}, size={}"
,
taskId
,
key
,
bytes
.
length
);
}
catch
(
Exception
e
)
{
log
.
error
(
"Video transfer failed, keep temporary URL: taskId={}"
,
taskId
,
e
);
}
}
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/AssetGenPipelineServiceImpl.java
View file @
fde0aba7
...
@@ -118,6 +118,7 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
...
@@ -118,6 +118,7 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
private
final
ProjectConsistencyBibleMapper
projectConsistencyBibleMapper
;
private
final
ProjectConsistencyBibleMapper
projectConsistencyBibleMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
ObjectMapper
objectMapper
;
private
final
BillingService
billingService
;
private
final
BillingService
billingService
;
private
final
SceneImageTransferService
sceneImageTransferService
;
private
final
HttpClient
httpClient
=
HttpClient
.
newBuilder
()
private
final
HttpClient
httpClient
=
HttpClient
.
newBuilder
()
.
connectTimeout
(
Duration
.
ofSeconds
(
10
))
.
connectTimeout
(
Duration
.
ofSeconds
(
10
))
.
build
();
.
build
();
...
@@ -365,13 +366,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
...
@@ -365,13 +366,9 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
try
{
try
{
String
prompt
=
limitImagePrompt
(
buildSceneRenderPrompt
(
scene
.
getImagePrompt
(),
visualStyle
,
renderSpec
));
String
prompt
=
limitImagePrompt
(
buildSceneRenderPrompt
(
scene
.
getImagePrompt
(),
visualStyle
,
renderSpec
));
String
imageUrl
=
seedreamService
.
generateImage
(
prompt
,
renderSpec
.
size
());
String
imageUrl
=
seedreamService
.
generateImage
(
prompt
,
renderSpec
.
size
());
byte
[]
bytes
=
downloadBytes
(
imageUrl
);
scene
.
setImageUrl
(
imageUrl
);
String
key
=
TosService
.
buildKey
(
scene
.
getTenantId
(),
scene
.
getProjectId
(),
"scenes"
,
scene
.
getName
()
+
".jpg"
);
scene
.
setImageTosKey
(
null
);
tosService
.
upload
(
key
,
new
ByteArrayInputStream
(
bytes
),
bytes
.
length
,
"image/jpeg"
);
scene
.
setStatus
(
"generating"
);
scene
.
setImageUrl
(
tosService
.
publicUrl
(
key
));
scene
.
setImageTosKey
(
key
);
scene
.
setStatus
(
"ready"
);
sceneMapper
.
updateById
(
scene
);
sceneMapper
.
updateById
(
scene
);
billingService
.
charge
(
BillingChargeRequest
.
builder
()
billingService
.
charge
(
BillingChargeRequest
.
builder
()
.
tenantId
(
tenantId
)
.
tenantId
(
tenantId
)
...
@@ -385,6 +382,7 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
...
@@ -385,6 +382,7 @@ public class AssetGenPipelineServiceImpl implements AssetGenPipelineService {
.
unitCount
(
1
)
.
unitCount
(
1
)
.
refId
(
String
.
valueOf
(
sceneId
))
.
refId
(
String
.
valueOf
(
sceneId
))
.
build
());
.
build
());
sceneImageTransferService
.
transferSceneImageToTos
(
sceneId
,
tenantId
,
imageUrl
);
return
scene
;
return
scene
;
}
catch
(
BizException
e
)
{
}
catch
(
BizException
e
)
{
scene
.
setStatus
(
"failed"
);
scene
.
setStatus
(
"failed"
);
...
...
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/SceneImageTransferService.java
0 → 100644
View file @
fde0aba7
package
com
.
yaoai
.
pipeline
.
service
.
impl
;
import
com.yaoai.common.exception.BizException
;
import
com.yaoai.common.exception.ErrorCode
;
import
com.yaoai.domain.entity.Scene
;
import
com.yaoai.domain.mapper.SceneMapper
;
import
com.yaoai.storage.service.TosService
;
import
lombok.RequiredArgsConstructor
;
import
lombok.extern.slf4j.Slf4j
;
import
org.springframework.scheduling.annotation.Async
;
import
org.springframework.stereotype.Service
;
import
java.io.ByteArrayInputStream
;
import
java.net.URI
;
import
java.net.http.HttpClient
;
import
java.net.http.HttpRequest
;
import
java.net.http.HttpResponse
;
import
java.time.Duration
;
import
java.util.Base64
;
@Slf4j
@Service
@RequiredArgsConstructor
public
class
SceneImageTransferService
{
private
final
SceneMapper
sceneMapper
;
private
final
TosService
tosService
;
private
final
HttpClient
httpClient
=
HttpClient
.
newBuilder
()
.
connectTimeout
(
Duration
.
ofSeconds
(
10
))
.
build
();
@Async
public
void
transferSceneImageToTos
(
Long
sceneId
,
Long
tenantId
,
String
temporaryImageUrl
)
{
try
{
Scene
scene
=
sceneMapper
.
selectById
(
sceneId
);
if
(
scene
==
null
||
!
tenantId
.
equals
(
scene
.
getTenantId
()))
{
log
.
warn
(
"Skip scene image transfer: scene not found or tenant mismatch, sceneId={}"
,
sceneId
);
return
;
}
if
(!
temporaryImageUrl
.
equals
(
scene
.
getImageUrl
()))
{
log
.
info
(
"Skip stale scene image transfer: sceneId={}"
,
sceneId
);
return
;
}
log
.
info
(
"Scene image transfer started: sceneId={}, temporaryUrl={}"
,
sceneId
,
temporaryImageUrl
);
DownloadedImage
image
=
downloadImage
(
temporaryImageUrl
);
String
key
=
TosService
.
buildKey
(
scene
.
getTenantId
(),
scene
.
getProjectId
(),
"scenes"
,
scene
.
getName
()
+
"."
+
image
.
extension
()
);
tosService
.
upload
(
key
,
new
ByteArrayInputStream
(
image
.
bytes
()),
image
.
bytes
().
length
,
image
.
contentType
());
Scene
latest
=
sceneMapper
.
selectById
(
sceneId
);
if
(
latest
==
null
||
!
tenantId
.
equals
(
latest
.
getTenantId
())
||
!
temporaryImageUrl
.
equals
(
latest
.
getImageUrl
()))
{
log
.
info
(
"Skip stale scene image replace after upload: sceneId={}"
,
sceneId
);
return
;
}
latest
.
setImageUrl
(
tosService
.
publicUrl
(
key
));
latest
.
setImageTosKey
(
key
);
latest
.
setStatus
(
"ready"
);
sceneMapper
.
updateById
(
latest
);
log
.
info
(
"Scene image transfer completed: sceneId={}, key={}"
,
sceneId
,
key
);
}
catch
(
Exception
e
)
{
log
.
warn
(
"Scene image transfer failed: sceneId={}, temporaryUrl={}"
,
sceneId
,
temporaryImageUrl
,
e
);
markFailedIfStillCurrent
(
sceneId
,
tenantId
,
temporaryImageUrl
);
}
}
private
void
markFailedIfStillCurrent
(
Long
sceneId
,
Long
tenantId
,
String
temporaryImageUrl
)
{
Scene
scene
=
sceneMapper
.
selectById
(
sceneId
);
if
(
scene
==
null
||
!
tenantId
.
equals
(
scene
.
getTenantId
())
||
!
temporaryImageUrl
.
equals
(
scene
.
getImageUrl
()))
{
return
;
}
scene
.
setStatus
(
"failed"
);
sceneMapper
.
updateById
(
scene
);
}
private
DownloadedImage
downloadImage
(
String
url
)
throws
Exception
{
if
(
url
!=
null
&&
url
.
startsWith
(
"data:"
))
{
int
comma
=
url
.
indexOf
(
','
);
int
semicolon
=
url
.
indexOf
(
';'
);
if
(
comma
<=
0
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"图片 data URL 格式无效"
);
}
String
contentType
=
semicolon
>
5
?
url
.
substring
(
5
,
semicolon
)
:
"image/png"
;
return
new
DownloadedImage
(
Base64
.
getDecoder
().
decode
(
url
.
substring
(
comma
+
1
)),
contentType
,
extensionFor
(
contentType
));
}
HttpRequest
request
=
HttpRequest
.
newBuilder
()
.
uri
(
URI
.
create
(
url
))
.
timeout
(
Duration
.
ofMinutes
(
2
))
.
GET
()
.
build
();
HttpResponse
<
byte
[]>
response
=
httpClient
.
send
(
request
,
HttpResponse
.
BodyHandlers
.
ofByteArray
());
if
(
response
.
statusCode
()
!=
200
)
{
throw
new
BizException
(
ErrorCode
.
INTERNAL_ERROR
,
"下载图片失败 HTTP "
+
response
.
statusCode
());
}
String
contentType
=
response
.
headers
().
firstValue
(
"content-type"
)
.
map
(
value
->
value
.
split
(
";"
)[
0
].
trim
())
.
filter
(
value
->
value
.
startsWith
(
"image/"
))
.
orElseGet
(()
->
contentTypeFromUrl
(
url
));
return
new
DownloadedImage
(
response
.
body
(),
contentType
,
extensionFor
(
contentType
));
}
private
String
contentTypeFromUrl
(
String
url
)
{
String
lower
=
url
==
null
?
""
:
url
.
toLowerCase
();
if
(
lower
.
contains
(
".jpg"
)
||
lower
.
contains
(
".jpeg"
))
return
"image/jpeg"
;
if
(
lower
.
contains
(
".webp"
))
return
"image/webp"
;
if
(
lower
.
contains
(
".gif"
))
return
"image/gif"
;
return
"image/png"
;
}
private
String
extensionFor
(
String
contentType
)
{
if
(
"image/jpeg"
.
equalsIgnoreCase
(
contentType
))
return
"jpg"
;
if
(
"image/webp"
.
equalsIgnoreCase
(
contentType
))
return
"webp"
;
if
(
"image/gif"
.
equalsIgnoreCase
(
contentType
))
return
"gif"
;
return
"png"
;
}
private
record
DownloadedImage
(
byte
[]
bytes
,
String
contentType
,
String
extension
)
{
}
}
yaoai-comic-studio/yaoai-pipeline/src/main/java/com/yaoai/pipeline/service/impl/VideoTaskPipelineServiceImpl.java
View file @
fde0aba7
...
@@ -277,7 +277,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
...
@@ -277,7 +277,7 @@ public class VideoTaskPipelineServiceImpl implements VideoTaskPipelineService {
String
ratio
=
s
.
getRatio
()
!=
null
&&
!
s
.
getRatio
().
isBlank
()
?
s
.
getRatio
()
:
"16:9"
;
String
ratio
=
s
.
getRatio
()
!=
null
&&
!
s
.
getRatio
().
isBlank
()
?
s
.
getRatio
()
:
"16:9"
;
boolean
generateAudio
=
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
());
boolean
generateAudio
=
Boolean
.
TRUE
.
equals
(
s
.
getGenerateAudio
());
asyncProcessor
.
processTextToVideo
(
task
.
getId
(),
tenantId
,
s
.
getUserId
(),
projectId
,
asyncProcessor
.
processTextToVideo
(
task
.
getId
(),
tenantId
,
s
.
getUserId
(),
projectId
,
finalPrompt
,
finalPrompt
,
orderedKeys
,
duration
,
ratio
,
generateAudio
,
s
.
getModel
()
);
finalPrompt
,
finalPrompt
,
orderedKeys
,
duration
,
ratio
,
generateAudio
,
null
);
return
task
;
return
task
;
}
}
...
...
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