新增海螺视频

This commit is contained in:
lq1405 2025-09-25 17:21:45 +08:00
parent 2ce5409ec7
commit 3d5307c8e4
26 changed files with 3840 additions and 102 deletions

View File

@ -64,6 +64,9 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
lumaOptions!: string | null // 生成视频的一些设置 lumaOptions!: string | null // 生成视频的一些设置
klingOptions!: string | null // 生成视频的一些设置 klingOptions!: string | null // 生成视频的一些设置
mjVideoOptions!: string | null // MJ生成视频的一些设置 mjVideoOptions!: string | null // MJ生成视频的一些设置
hailuoTextToVideoOptions?: string
hailuoFirstFrameOnlyOptions?: string
hailuoFirstLastFrameOptions?: string
messageData!: string | null messageData!: string | null
static schema: ObjectSchema = { static schema: ObjectSchema = {
name: 'VideoMessage', name: 'VideoMessage',
@ -83,6 +86,9 @@ export class VideoMessage extends Realm.Object<VideoMessage> {
lumaOptions: 'string?', lumaOptions: 'string?',
klingOptions: 'string?', klingOptions: 'string?',
mjVideoOptions: 'string?', mjVideoOptions: 'string?',
hailuoTextToVideoOptions: "string?",
hailuoFirstFrameOnlyOptions: "string?",
hailuoFirstLastFrameOptions: "string?",
messageData: 'string?' messageData: 'string?'
}, },
primaryKey: 'id' primaryKey: 'id'

View File

@ -80,7 +80,7 @@ export class RealmBaseService extends BaseService {
PresetModel PresetModel
], ],
path: this.dbpath, path: this.dbpath,
schemaVersion: 21, // 数据库版本号,修改时需要增加 schemaVersion: 22, // 数据库版本号,修改时需要增加
migration: migration migration: migration
} }
this.realm = await Realm.open(config) this.realm = await Realm.open(config)

View File

@ -120,7 +120,13 @@ export enum BookBackTaskType {
// MJ Video // MJ Video
MJ_VIDEO = 'mj_video', MJ_VIDEO = 'mj_video',
// MJ VIDEO EXTEND 视频拓展 // MJ VIDEO EXTEND 视频拓展
MJ_VIDEO_EXTEND = 'mj_video_extend' MJ_VIDEO_EXTEND = 'mj_video_extend',
// 海螺文生视频
HAILUO_TEXT_TO_VIDEO = 'hailuo_text_to_video',
// 海螺图生视频
HAILUO_IMAGE_TO_VIDEO = 'hailuo_image_to_video',
// 海螺视频首尾帧
HAILUO_FIRST_LAST_FRAME = 'hailuo_first_last_frame'
} }
export enum BookBackTaskStatus { export enum BookBackTaskStatus {

View File

@ -71,6 +71,7 @@ export enum ResponseMessageType {
KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND', // Kling生成视频拓展 KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND', // Kling生成视频拓展
MJ_VIDEO = 'MJ_VIDEO', // MJ生成视频 MJ_VIDEO = 'MJ_VIDEO', // MJ生成视频
MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND', // MJ生成视频拓展 MJ_VIDEO_EXTEND = 'MJ_VIDEO_EXTEND', // MJ生成视频拓展
HAI_LUO_VIDEO = 'HAI_LUO_VIDEO', // 海螺生成视频
VIDEO_SUCESS = 'VIDEO_SUCESS' //视频生成成功 VIDEO_SUCESS = 'VIDEO_SUCESS' //视频生成成功
} }

View File

@ -3,6 +3,8 @@
import { t } from '@/i18n' import { t } from '@/i18n'
import { BookBackTaskType } from './bookEnum' import { BookBackTaskType } from './bookEnum'
export type ToVIdeoType = 'textToVideo' | 'imageToVideo' | "firstLastFrame"
/** 图片转视频的方式 */ /** 图片转视频的方式 */
export enum ImageToVideoModels { export enum ImageToVideoModels {
/** runway 生成视频 */ /** runway 生成视频 */
@ -15,6 +17,8 @@ export enum ImageToVideoModels {
KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND', KLING_VIDEO_EXTEND = 'KLING_VIDEO_EXTEND',
/** Pika 生成视频 */ /** Pika 生成视频 */
PIKA = 'PIKA', PIKA = 'PIKA',
/** 海螺生成视频 */
HAILUO = 'HAILUO',
/** MJ 图转视频 */ /** MJ 图转视频 */
MJ_VIDEO = 'MJ_VIDEO', MJ_VIDEO = 'MJ_VIDEO',
/** MJ 视频拓展 */ /** MJ 视频拓展 */
@ -22,10 +26,36 @@ export enum ImageToVideoModels {
} }
/**
*
*
* BookBackTaskType
* 使 ImageToVideoModels
* 便
*
* @param type BookBackTaskType
* @returns ImageToVideoModels 'UNKNOWN'
*
* @description
* -
* - 'UNKNOWN'便
* -
*
* @example
* ```typescript
* const model = MappingTaskTypeToVideoModel(BookBackTaskType.LUMA_VIDEO);
* // model === ImageToVideoModels.LUMA
* ```
*
* @see BookBackTaskType -
* @see ImageToVideoModels -
*/
export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => { export const MappingTaskTypeToVideoModel = (type: BookBackTaskType | string) => {
switch (type) { switch (type) {
case BookBackTaskType.LUMA_VIDEO: case BookBackTaskType.LUMA_VIDEO:
return ImageToVideoModels.LUMA return ImageToVideoModels.LUMA
case BookBackTaskType.HAILUO_TEXT_TO_VIDEO:
return ImageToVideoModels.HAILUO
case BookBackTaskType.RUNWAY_VIDEO: case BookBackTaskType.RUNWAY_VIDEO:
return ImageToVideoModels.RUNWAY return ImageToVideoModels.RUNWAY
case BookBackTaskType.KLING_VIDEO: case BookBackTaskType.KLING_VIDEO:
@ -56,6 +86,8 @@ export const GetImageToVideoModelsLabel = (model: ImageToVideoModels | string) =
return t('可灵') return t('可灵')
case ImageToVideoModels.PIKA: case ImageToVideoModels.PIKA:
return 'Pika' return 'Pika'
case ImageToVideoModels.HAILUO:
return t('海螺')
case ImageToVideoModels.MJ_VIDEO: case ImageToVideoModels.MJ_VIDEO:
case ImageToVideoModels.MJ_VIDEO_EXTEND: case ImageToVideoModels.MJ_VIDEO_EXTEND:
return t('MJ视频') return t('MJ视频')
@ -78,6 +110,10 @@ export const GetImageToVideoModelsOptions = () => {
{ {
label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO), label: GetImageToVideoModelsLabel(ImageToVideoModels.MJ_VIDEO),
value: ImageToVideoModels.MJ_VIDEO value: ImageToVideoModels.MJ_VIDEO
}, {
label: GetImageToVideoModelsLabel(ImageToVideoModels.HAILUO),
value: ImageToVideoModels.HAILUO
}, },
{ {
label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY), label: GetImageToVideoModelsLabel(ImageToVideoModels.RUNWAY),
@ -492,3 +528,458 @@ export function GetMJVideoBatchSizeOptions() {
} }
//#endregion //#endregion
//#region 海螺视频相关
/**
*
*/
export enum HailuoModel {
/** MiniMax-Hailuo-02 模型 */
MINIMAX_HAILUO_02 = 'MiniMax-Hailuo-02',
/** I2V-01-Director 模型 */
I2V_01_DIRECTOR = 'I2V-01-Director',
/** I2V-01-live 模型 */
I2V_01_LIVE = 'I2V-01-live',
/** I2V-01 模型 */
I2V_01 = 'I2V-01',
/** T2V-01-Director 模型 */
T2V_01_DIRECTOR = "T2V-01-Director",
/** T2V-01 模型 */
T2V_01 = 'T2V-01'
}
/**
*
*/
export enum HailuoResolution {
/** 512P 分辨率 */
P512 = '512P',
/** 720P 分辨率 */
P720 = '720P',
/** 768P 分辨率 */
P768 = '768P',
/** 1080P 分辨率 */
P1080 = '1080P'
}
/**
*
*/
export enum HailuoDuration {
/** 6秒 */
SIX = 6,
/** 10秒 */
TEN = 10
}
/**
*
*
* @param model
* @returns
*/
export function GetHailuoModelLabel(model: HailuoModel | string) {
switch (model) {
case HailuoModel.MINIMAX_HAILUO_02:
return t('MiniMax-Hailuo-02')
case HailuoModel.I2V_01_DIRECTOR:
return t('I2V-01-Director')
case HailuoModel.I2V_01_LIVE:
return t('I2V-01-live')
case HailuoModel.I2V_01:
return t('I2V-01')
case HailuoModel.T2V_01_DIRECTOR:
return t('T2V-01-Director')
case HailuoModel.T2V_01:
return t('T2V-01')
default:
return t('未知')
}
}
/**
*
*
* @returns UI组件
*/
export function GetHailuoModelOptions(type: ToVIdeoType) {
if (type == "textToVideo") {
return [
{
label: GetHailuoModelLabel(HailuoModel.MINIMAX_HAILUO_02),
value: HailuoModel.MINIMAX_HAILUO_02
},
{
label: GetHailuoModelLabel(HailuoModel.T2V_01_DIRECTOR),
value: HailuoModel.T2V_01_DIRECTOR
},
{
label: GetHailuoModelLabel(HailuoModel.T2V_01),
value: HailuoModel.T2V_01
}
]
} else if (type == "imageToVideo") {
return [
{
label: GetHailuoModelLabel(HailuoModel.MINIMAX_HAILUO_02),
value: HailuoModel.MINIMAX_HAILUO_02
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01_DIRECTOR),
value: HailuoModel.I2V_01_DIRECTOR
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01_LIVE),
value: HailuoModel.I2V_01_LIVE
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01),
value: HailuoModel.I2V_01
}
]
} else if (type == "firstLastFrame") {
return [
{
label: GetHailuoModelLabel(HailuoModel.MINIMAX_HAILUO_02),
value: HailuoModel.MINIMAX_HAILUO_02
}]
}
return [
[
{
label: GetHailuoModelLabel(HailuoModel.MINIMAX_HAILUO_02),
value: HailuoModel.MINIMAX_HAILUO_02
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01_DIRECTOR),
value: HailuoModel.I2V_01_DIRECTOR
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01_LIVE),
value: HailuoModel.I2V_01_LIVE
},
{
label: GetHailuoModelLabel(HailuoModel.I2V_01),
value: HailuoModel.I2V_01
},
{
label: GetHailuoModelLabel(HailuoModel.T2V_01_DIRECTOR),
value: HailuoModel.T2V_01_DIRECTOR
},
{
label: GetHailuoModelLabel(HailuoModel.T2V_01),
value: HailuoModel.T2V_01
}
]
]
}
/**
*
*
* @param model
* @returns
*/
export function IsHailuoModelSupportDirectorCommands(model: HailuoModel | string): boolean {
return model === HailuoModel.MINIMAX_HAILUO_02 || model === HailuoModel.I2V_01_DIRECTOR
}
/**
*
*
* @param type : 'textToVideo' (), 'imageToVideo' (), 'firstLastFrame' ()
* @param model
* @param duration
* @returns
*/
export function GetHailuoModelSupportedResolutions(type: ToVIdeoType, model: HailuoModel | string, duration: HailuoDuration | number = HailuoDuration.SIX) {
if (model === HailuoModel.MINIMAX_HAILUO_02) {
// MiniMax-Hailuo-02 模型根据类型和时长的不同支持
if (type === 'textToVideo') {
// 文生视频
if (duration === HailuoDuration.SIX) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 }, // 默认
{ label: GetHailuoResolutionLabel(HailuoResolution.P1080), value: HailuoResolution.P1080 }
]
} else if (duration === HailuoDuration.TEN) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 } // 默认
]
}
} else if (type === 'imageToVideo') {
// 图生视频
if (duration === HailuoDuration.SIX) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P512), value: HailuoResolution.P512 },
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 }, // 默认
{ label: GetHailuoResolutionLabel(HailuoResolution.P1080), value: HailuoResolution.P1080 }
]
} else if (duration === HailuoDuration.TEN) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P512), value: HailuoResolution.P512 },
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 } // 默认
]
}
} else if (type === 'firstLastFrame') {
// 首尾帧视频
if (duration === HailuoDuration.SIX) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 }, // 默认
{ label: GetHailuoResolutionLabel(HailuoResolution.P1080), value: HailuoResolution.P1080 }
]
} else if (duration === HailuoDuration.TEN) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P768), value: HailuoResolution.P768 }
]
}
}
} else {
// 其他模型 (I2V-01-Director, I2V-01-live, I2V-01)
if (duration === HailuoDuration.SIX) {
return [
{ label: GetHailuoResolutionLabel(HailuoResolution.P720), value: HailuoResolution.P720 } // 默认
]
}
// 其他模型不支持10秒时长
}
return []
}
/**
*
*
*
*
*
* @param type - 'textToVideo' (), 'imageToVideo' (), 'firstLastFrame' ()
* @param model -
* @param duration - 610
* @param resolution - 512P, 720P, 768P, 1080P
*
* @returns {boolean} truefalse
*
* @description
* - MiniMax-Hailuo-02
* - I2V系列模型通常只支持720P分辨率
* - 1080P分辨率通常只在6秒时长下被支持
*
* @example
* ```typescript
* // 验证文生视频在MiniMax-Hailuo-02模型下6秒时长是否支持768P
* const isValid = IsValidResolution('textToVideo', HailuoModel.MINIMAX_HAILUO_02, HailuoDuration.SIX, HailuoResolution.P768);
* // isValid === true
*
* // 验证图生视频在I2V-01模型下6秒时长是否支持1080P
* const isValid2 = IsValidResolution('imageToVideo', HailuoModel.I2V_01, HailuoDuration.SIX, HailuoResolution.P1080);
* // isValid2 === false
* ```
*
* @see GetHailuoModelSupportedResolutions -
*/
export function IsValidResolution(type: ToVIdeoType, model: HailuoModel, duration: HailuoDuration, resolution: HailuoResolution): boolean {
let spportedResolutions = GetHailuoModelSupportedResolutions(type, model, duration)
// 检查传入的分辨率是否在支持的列表中
return spportedResolutions.some(r => r.value === resolution)
}
/**
*
*
* @param type : 'textToVideo' (), 'imageToVideo' (), 'firstLastFrame' ()
* @param model
* @param resolution
* @returns
*/
export function GetHailuoModelSupportedDurations(type: ToVIdeoType, model: HailuoModel | string, resolution?: HailuoResolution | string) {
if (model === HailuoModel.MINIMAX_HAILUO_02) {
if (type === 'textToVideo') {
// 文生视频 - MiniMax-Hailuo-02 支持 6s 和 10s但 1080P 只支持 6s
if (resolution === HailuoResolution.P1080) {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX }
]
} else {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX },
{ label: GetHailuoDurationLabel(HailuoDuration.TEN), value: HailuoDuration.TEN }
]
}
} else if (type === 'imageToVideo') {
// 图生视频 - MiniMax-Hailuo-02 支持 6s 和 10s1080P 只支持 6s
if (resolution === HailuoResolution.P1080) {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX }
]
} else {
// 512P, 768P 都支持 6s 和 10s
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX },
{ label: GetHailuoDurationLabel(HailuoDuration.TEN), value: HailuoDuration.TEN }
]
}
} else if (type === 'firstLastFrame') {
// 首尾帧视频 - MiniMax-Hailuo-02 支持 6s 和 10s但 1080P 只支持 6s
if (resolution === HailuoResolution.P1080) {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX }
]
} else {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX },
{ label: GetHailuoDurationLabel(HailuoDuration.TEN), value: HailuoDuration.TEN }
]
}
} else {
// 兼容旧版本调用,默认逻辑
if (resolution === HailuoResolution.P1080) {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX }
]
} else {
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX },
{ label: GetHailuoDurationLabel(HailuoDuration.TEN), value: HailuoDuration.TEN }
]
}
}
} else {
// 其他模型 (I2V-01-Director, I2V-01-live, I2V-01) 只支持6秒
return [
{ label: GetHailuoDurationLabel(HailuoDuration.SIX), value: HailuoDuration.SIX }
]
}
}
/**
*
*
*
*
*
* @param type - 'textToVideo' (), 'imageToVideo' (), 'firstLastFrame' ()
* @param model -
* @param resolution - 512P, 720P, 768P, 1080P
* @param duration - 610
*
* @returns {boolean} truefalse
*
* @description
* - MiniMax-Hailuo-02 6101080P分辨率只支持6秒
* - I2V系列模型 (I2V-01-Director, I2V-01-live, I2V-01) 6
* - 1080P
*
* @example
* ```typescript
* // 验证文生视频在MiniMax-Hailuo-02模型下768P分辨率是否支持10秒
* const isValid = IsValidDuratio('textToVideo', HailuoModel.MINIMAX_HAILUO_02, HailuoResolution.P768, HailuoDuration.TEN);
* // isValid === true
*
* // 验证图生视频在MiniMax-Hailuo-02模型下1080P分辨率是否支持10秒
* const isValid2 = IsValidDuratio('imageToVideo', HailuoModel.MINIMAX_HAILUO_02, HailuoResolution.P1080, HailuoDuration.TEN);
* // isValid2 === false (1080P只支持6秒)
*
* // 验证I2V-01模型是否支持10秒时长
* const isValid3 = IsValidDuratio('imageToVideo', HailuoModel.I2V_01, HailuoResolution.P720, HailuoDuration.TEN);
* // isValid3 === false (I2V系列只支持6秒)
* ```
*
* @see GetHailuoModelSupportedDurations -
*/
export function IsValidDuratio(type: ToVIdeoType, model: HailuoModel, resolution: HailuoResolution, duration: HailuoDuration): boolean {
// 获取当前模型、类型和分辨率组合下支持的所有时长选项
let spportedResolutions = GetHailuoModelSupportedDurations(type, model, resolution)
// 检查传入的时长是否在支持的列表中
return spportedResolutions.some(r => r.value === duration)
}
/**
*
*
* @param resolution
* @returns
*/
export function GetHailuoResolutionLabel(resolution: HailuoResolution | string) {
switch (resolution) {
case HailuoResolution.P512:
return t('512P')
case HailuoResolution.P720:
return t('720P')
case HailuoResolution.P768:
return t('768P')
case HailuoResolution.P1080:
return t('1080P')
default:
return t('未知')
}
}
/**
*
*
* @returns UI组件
*/
export function GetHailuoResolutionOptions() {
return [
{
label: GetHailuoResolutionLabel(HailuoResolution.P512),
value: HailuoResolution.P512
},
{
label: GetHailuoResolutionLabel(HailuoResolution.P720),
value: HailuoResolution.P720
},
{
label: GetHailuoResolutionLabel(HailuoResolution.P768),
value: HailuoResolution.P768
},
{
label: GetHailuoResolutionLabel(HailuoResolution.P1080),
value: HailuoResolution.P1080
}
]
}
/**
*
*
* @param duration
* @returns
*/
export function GetHailuoDurationLabel(duration: HailuoDuration | number) {
switch (duration) {
case HailuoDuration.SIX:
return t('6秒')
case HailuoDuration.TEN:
return t('10秒')
default:
return t('未知')
}
}
/**
*
*
* @returns UI组件
*/
export function GetHailuoDurationOptions() {
return [
{
label: GetHailuoDurationLabel(HailuoDuration.SIX),
value: HailuoDuration.SIX
},
{
label: GetHailuoDurationLabel(HailuoDuration.TEN),
value: HailuoDuration.TEN
}
]
}
//#endregion

View File

@ -167,6 +167,9 @@ const BOOK = {
/** Kling 图转视频返回前端数据任务 */ /** Kling 图转视频返回前端数据任务 */
KLING_IMAGE_TO_VIDEO_RETURN: 'KLING_IMAGE_TO_VIDEO_RETURN', KLING_IMAGE_TO_VIDEO_RETURN: 'KLING_IMAGE_TO_VIDEO_RETURN',
/** 海螺图转视频返回前端数据任务 */
HAILUO_TO_VIDEO_RETURN: 'HAILUO_TO_VIDEO_RETURN',
//#endregion //#endregion
} }

View File

@ -5,10 +5,15 @@ import {
KlingModelName, KlingModelName,
MJVideoBatchSize, MJVideoBatchSize,
MJVideoType, MJVideoType,
MJVideoAction,
MJVideoMotion,
RunawayModel, RunawayModel,
RunwaySeconds, RunwaySeconds,
VideoModel, VideoModel,
VideoStatus VideoStatus,
HailuoModel,
HailuoResolution,
HailuoDuration
} from '@/define/enum/video' } from '@/define/enum/video'
declare namespace BookTaskDetail { declare namespace BookTaskDetail {
@ -31,6 +36,9 @@ declare namespace BookTaskDetail {
lumaOptions?: string lumaOptions?: string
klingOptions?: string klingOptions?: string
mjVideoOptions?: string mjVideoOptions?: string
hailuoTextToVideoOptions?: string
hailuoFirstFrameOnlyOptions?: string
hailuoFirstLastFrameOptions?: string
messageData?: string messageData?: string
videoUrls?: string[] // 视频地址数组 videoUrls?: string[] // 视频地址数组
messageData?: string messageData?: string
@ -191,6 +199,141 @@ declare namespace BookTaskDetail {
loop?: boolean loop?: boolean
} }
/**
* -
*/
interface HailuoBaseOptions {
/**
*
* MiniMax-Hailuo-02, I2V-01-Director, I2V-01-live, I2V-01
*/
model: HailuoModel
/**
* 2000
* MiniMax-Hailuo-02 I2V-01-Director 使 []
*
* 15
* - : [], []
* - : [], []
* - : [], []
* - : [], []
* - : [], []
* - : [], []
* - : [], [], []
*
* 使
* - 组合运镜: 同一组 [] [,] 3
* - 顺序运镜: prompt "...[推进], 然后...[拉远]"
* - 自然语言: 也支持通过自然语言描述运镜使
*/
prompt?: string
/**
* prompt true
* false
*/
prompt_optimizer?: boolean
/**
* 6
*
*
* MiniMax-Hailuo-02
* - 512P6 10
* - 768P6 10
* - 1080P6
*
*
* - 720P6
*/
duration?: HailuoDuration
/**
*
*
*
* MiniMax-Hailuo-02
* - 6512P, 768P (), 1080P
* - 10512P, 768P ()
*
*
* - 6720P ()
* - 10
*/
resolution?: HailuoResolution
}
/**
* -
*
*/
interface HailuoFirstFrameOnlyOptions extends HailuoBaseOptions {
/**
*
* URL Base64 Data URL (data:image/jpeg;base64,...)
*
*
* - model I2V-01, I2V-01-Director, I2V-01-live
* - model MiniMax-Hailuo-02 resolution 512P
*
*
* - JPG, JPEG, PNG, WebP
* - 20MB
* - 300px 2:5 5:2
*/
first_frame_image: string
/**
* prompt_optimizer false
* MiniMax-Hailuo-02
*/
fast_pretreatment?: boolean
}
/**
* -
*
*/
interface HailuoFirstLastFrameOptions extends HailuoFirstFrameOnlyOptions {
/**
*
* URL Base64 Data URL (data:image/jpeg;base64,...)
*
*
* - JPG, JPEG, PNG, WebP
* - 20MB
* - 300px 2:5 5:2
*
* 5
*/
last_frame_image: string
/**
* 使5
*/
duration: HailuoDuration
}
/**
* -
*
*/
interface HailuoTextToVideoOptions extends HailuoBaseOptions {
/**
* prompt_optimizer false
* MiniMax-Hailuo-02
*/
fast_pretreatment?: boolean
}
/**
*
* 使
*/
type HailuoOptions = HailuoTextToVideoOptions | HailuoFirstFrameOnlyOptions | HailuoFirstLastFrameOptions
//#endregion //#endregion
//#region 小说文案相关 //#region 小说文案相关

View File

@ -1660,9 +1660,55 @@ export default {
"高性能 (std)": "High Performance (std)", "高性能 (std)": "High Performance (std)",
"高表现 (pro)": "High Performance (pro)", "高表现 (pro)": "High Performance (pro)",
"选择Video": "Select Video", "选择Video": "Select Video",
"<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间": "<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间", "<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间": "<strong>Required</strong><br/><br/>• Supported formats: <strong>.jpg/.jpeg/.png</strong><br/>• File size: <strong>not exceeding 10MB</strong><br/>• Resolution: <strong>not less than 300*300px</strong><br/>• Aspect ratio: <strong>between 1:2.5 ~ 2.5:1</strong>",
"参考图像 - 尾帧控制": "参考图像 - 尾帧控制", "参考图像 - 尾帧控制": "Reference Image - End Frame Control",
'<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间': '<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间', '<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间': '<strong>Optional</strong><br/><br/>• Supported formats: <strong>.jpg/.jpeg/.png</strong><br/>• File size: <strong>not exceeding 10MB</strong><br/>• Resolution: <strong>not less than 300*300px</strong><br/>• Aspect ratio: <strong>between 1:2.5 ~ 2.5:1</strong>',
"海螺": "HaiLuo",
"海螺首尾帧视频任务完成!": 'HaiLuo first-last frame video task completed!',
"海螺首尾帧视频任务失败,失败信息:{error}": "HaiLuo first-last frame video task failed, error details: {error}",
"海螺图转视频任务完成!": 'HaiLuo image-to-video task completed!',
'海螺图转视频任务失败,失败信息:{error}': 'HaiLuo image-to-video task failed, error details: {error}',
"当前分镜数据的海螺视频参数的提示词为空,请检查": "The prompt for the HaiLuo video parameters in the current storyboard data is empty, please check",
"海螺文生视频任务完成!": 'HaiLuo text-to-video task completed!',
"海螺文生视频任务失败,失败信息:{error}": 'HaiLuo text-to-video task failed, error details: {error}',
"海螺视频任务正在执行中...": "HaiLuo video task in progress...",
"海螺视频任务已完成!": "HaiLuo video task completed!",
"海螺视频任务失败,失败信息:{error}": "HaiLuo video task failed, error details: {error}",
"获取海螺视频下载地址失败": "Failed to get HaiLuo video download address",
"获取海螺视频下载地址失败,下载地址为空": "Failed to get HaiLuo video download address, download address is empty",
"已成功提交海螺视频任务任务ID{taskId}": "HaiLuo video task submitted successfully, task ID: {taskId}",
"当前分镜数据的海螺视频参数的模型参数为空,请检查": "The model parameter for the HaiLuo video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺视频参数的模型参数不合法,请检查": "The model parameter for the HaiLuo video parameters in the current storyboard data is invalid, please check",
"当前分镜数据的海螺视频参数的分辨率参数为空,请检查": "The resolution parameter for the HaiLuo video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺视频参数的时长参数为空,请检查": "The duration parameter for the HaiLuo video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺视频参数的分辨率参数不合法,请检查": "The resolution parameter for the HaiLuo video parameters in the current storyboard data is invalid, please check",
"当前分镜数据的海螺视频参数的时长参数不合法,请检查": "The duration parameter for the HaiLuo video parameters in the current storyboard data is invalid, please check",
"非MiniMax-Hailuo-02不支持启用fast_pretreatment参数请检查": "Non-MiniMax-Hailuo-02 does not support enabling the fast_pretreatment parameter, please check",
"当前分镜数据的海螺图生视频参数的首帧图片参数为空,请检查": "The first frame image parameter for the HaiLuo image-to-video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺首尾帧视频参数的首帧图片参数为空,请检查": "The first frame image parameter for the HaiLuo first-last frame video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺首尾帧视频参数的尾帧图片参数为空,请检查": "The last frame image parameter for the HaiLuo first-last frame video parameters in the current storyboard data is empty, please check",
"当前分镜数据的海螺文生视频参数为空或参数校验失败,请检查": 'The HaiLuo text-to-video parameters for the current storyboard data are empty or validation failed, please check',
"当前分镜数据的海螺图生视频参数为空或参数校验失败,请检查": "The HaiLuo image-to-video parameters for the current storyboard data are empty or validation failed, please check",
"当前分镜数据的海螺首尾帧视频参数为空或参数校验失败,请检查": "The HaiLuo first-last frame video parameters for the current storyboard data are empty or validation failed, please check",
"不支持的海螺视频类型:{type},请检查": "Unsupported HaiLuo video type: {type}, please check",
"将当前首尾帧视频的基础设置批量应用到所有的分镜中": "Apply the current first-last frame video's basic settings to all storyboards in batch",
"首帧图片": "First Frame Image",
"尾帧图片": "Last Frame Image",
"视频分辨率": "Video Resolution",
"自动优化提示词": "Auto Optimize Prompt",
"模型变更后已自动调整相关参数以确保兼容性": 'Model change has automatically adjusted related parameters to ensure compatibility',
"分辨率变更后已自动调整时长以确保兼容性": "Resolution change has automatically adjusted duration to ensure compatibility",
"已自动设置默认值并调整参数以确保兼容性": "Default values have been automatically set and parameters adjusted to ensure compatibility",
"时长变更后已自动调整分辨率以确保兼容性": "Duration change has automatically adjusted resolution to ensure compatibility",
"将当前图生视频的基础设置批量应用到所有的分镜中": "Apply the current image-to-video basic settings to all storyboards in batch",
"快速预处理": "Fast Pretreatment",
"将当前文生视频的基础设置批量应用到所有的分镜中": "Apply the current text-to-video basic settings to all storyboards in batch",
"首尾帧视频": "First-Last Frame Video",
"文生视频": "Text-to-Video",
"是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置模型名称、分辨率、时长、提示词优化等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续": "Do you want to apply the current storyboard settings to all other storyboards in batch?\n\nSynchronized settings: Model Name, Resolution, Duration, Prompt Optimization and other basic settings\n\nAfter batch application, the above basic settings of other storyboards will be replaced with the current storyboard data. Continue?",
"请输入提示词和选择模型": "Please enter prompt and select model",
"请上传首帧图片": "Please upload first frame image",
"请上传首帧和尾帧图片": "Please upload first and last frame images",
//#endregion //#endregion
//#region MJ //#region MJ

View File

@ -1663,6 +1663,52 @@ export default {
"<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间": "<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间", "<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间": "<strong>必须</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间",
"参考图像 - 尾帧控制": "参考图像 - 尾帧控制", "参考图像 - 尾帧控制": "参考图像 - 尾帧控制",
'<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间': '<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间', '<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间': '<strong>可选</strong><br/><br/>• 支持格式:<strong>.jpg/.jpeg/.png</strong><br/>• 文件大小:<strong>不超过10MB</strong><br/>• 分辨率:<strong>不小于300*300px</strong><br/>• 宽高比:<strong>1:2.5 ~ 2.5:1</strong>之间',
"海螺": "海螺",
"海螺首尾帧视频任务完成!": "海螺首尾帧视频任务完成!",
"海螺首尾帧视频任务失败,失败信息:{error}": "海螺首尾帧视频任务失败,失败信息:{error}",
"海螺图转视频任务完成!": '海螺图转视频任务完成!',
'海螺图转视频任务失败,失败信息:{error}': '海螺图转视频任务失败,失败信息:{error}',
"当前分镜数据的海螺视频参数的提示词为空,请检查": "当前分镜数据的海螺视频参数的提示词为空,请检查",
"海螺文生视频任务完成!": '海螺文生视频任务完成!',
"海螺文生视频任务失败,失败信息:{error}": '海螺文生视频任务失败,失败信息:{error}',
"海螺视频任务正在执行中...": "海螺视频任务正在执行中...",
"海螺视频任务已完成!": "海螺视频任务已完成!",
"海螺视频任务失败,失败信息:{error}": "海螺视频任务失败,失败信息:{error}",
"获取海螺视频下载地址失败": "获取海螺视频下载地址失败",
"获取海螺视频下载地址失败,下载地址为空": "获取海螺视频下载地址失败,下载地址为空",
"已成功提交海螺视频任务任务ID{taskId}": "已成功提交海螺视频任务任务ID{taskId}",
"当前分镜数据的海螺视频参数的模型参数为空,请检查": "当前分镜数据的海螺视频参数的模型参数为空,请检查",
"当前分镜数据的海螺视频参数的模型参数不合法,请检查": "当前分镜数据的海螺视频参数的模型参数不合法,请检查",
"当前分镜数据的海螺视频参数的分辨率参数为空,请检查": "当前分镜数据的海螺视频参数的分辨率参数为空,请检查",
"当前分镜数据的海螺视频参数的时长参数为空,请检查": "当前分镜数据的海螺视频参数的时长参数为空,请检查",
"当前分镜数据的海螺视频参数的分辨率参数不合法,请检查": "当前分镜数据的海螺视频参数的分辨率参数不合法,请检查",
"当前分镜数据的海螺视频参数的时长参数不合法,请检查": "当前分镜数据的海螺视频参数的时长参数不合法,请检查",
"非MiniMax-Hailuo-02不支持启用fast_pretreatment参数请检查": "非MiniMax-Hailuo-02不支持启用fast_pretreatment参数请检查",
"当前分镜数据的海螺图生视频参数的首帧图片参数为空,请检查": "当前分镜数据的海螺图生视频参数的首帧图片参数为空,请检查",
"当前分镜数据的海螺首尾帧视频参数的首帧图片参数为空,请检查": "当前分镜数据的海螺首尾帧视频参数的首帧图片参数为空,请检查",
"当前分镜数据的海螺首尾帧视频参数的尾帧图片参数为空,请检查": "当前分镜数据的海螺首尾帧视频参数的尾帧图片参数为空,请检查",
"当前分镜数据的海螺文生视频参数为空或参数校验失败,请检查": "当前分镜数据的海螺文生视频参数为空或参数校验失败,请检查",
"当前分镜数据的海螺图生视频参数为空或参数校验失败,请检查": "当前分镜数据的海螺图生视频参数为空或参数校验失败,请检查",
"当前分镜数据的海螺首尾帧视频参数为空或参数校验失败,请检查": "当前分镜数据的海螺首尾帧视频参数为空或参数校验失败,请检查",
"不支持的海螺视频类型:{type},请检查": "不支持的海螺视频类型:{type},请检查",
"将当前首尾帧视频的基础设置批量应用到所有的分镜中": "将当前首尾帧视频的基础设置批量应用到所有的分镜中",
"首帧图片": "首帧图片",
"尾帧图片": "尾帧图片",
"视频分辨率": "视频分辨率",
"自动优化提示词": "自动优化提示词",
"模型变更后已自动调整相关参数以确保兼容性": "模型变更后已自动调整相关参数以确保兼容性",
"分辨率变更后已自动调整时长以确保兼容性": "分辨率变更后已自动调整时长以确保兼容性",
"已自动设置默认值并调整参数以确保兼容性": "已自动设置默认值并调整参数以确保兼容性",
"时长变更后已自动调整分辨率以确保兼容性": "时长变更后已自动调整分辨率以确保兼容性",
"将当前图生视频的基础设置批量应用到所有的分镜中": "将当前图生视频的基础设置批量应用到所有的分镜中",
"快速预处理": "快速预处理",
"将当前文生视频的基础设置批量应用到所有的分镜中": "将当前文生视频的基础设置批量应用到所有的分镜中",
"首尾帧视频": "首尾帧视频",
"文生视频": "文生视频",
"是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置模型名称、分辨率、时长、提示词优化等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续": "是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置模型名称、分辨率、时长、提示词优化等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续",
"请输入提示词和选择模型": "请输入提示词和选择模型",
"请上传首帧图片": "请上传首帧图片",
"请上传首帧和尾帧图片": "请上传首帧和尾帧图片",
//#endregion //#endregion
//#region MJ //#region MJ

View File

@ -15,6 +15,7 @@ import { DownloadFile } from '@/define/Tools/common'
import { MappingTaskTypeToVideoModel } from '@/define/enum/video' import { MappingTaskTypeToVideoModel } from '@/define/enum/video'
import { BookBackTaskType } from '@/define/enum/bookEnum' import { BookBackTaskType } from '@/define/enum/bookEnum'
import { t } from '@/i18n' import { t } from '@/i18n'
import { PresetRealmService } from '@/define/db/service/presetService'
export class BookBasicHandle { export class BookBasicHandle {
bookTaskDetailService!: BookTaskDetailService bookTaskDetailService!: BookTaskDetailService
@ -22,6 +23,7 @@ export class BookBasicHandle {
optionRealmService!: OptionRealmService optionRealmService!: OptionRealmService
bookService!: BookService bookService!: BookService
taskListService!: TaskListService taskListService!: TaskListService
presetRealmService!: PresetRealmService
constructor() { constructor() {
// 初始化 // 初始化
@ -44,6 +46,9 @@ export class BookBasicHandle {
if (!this.taskListService) { if (!this.taskListService) {
this.taskListService = await TaskListService.getInstance() this.taskListService = await TaskListService.getInstance()
} }
if (!this.presetRealmService) {
this.presetRealmService = await PresetRealmService.getInstance()
}
} }
/** /**
@ -151,6 +156,9 @@ export class BookBasicHandle {
&& !videoUrl.startsWith('https://cdn.midjourney.com') && !videoUrl.startsWith('https://cdn.midjourney.com')
&& task.type != BookBackTaskType.KLING_VIDEO && task.type != BookBackTaskType.KLING_VIDEO
&& task.type != BookBackTaskType.KLING_VIDEO_EXTEND && task.type != BookBackTaskType.KLING_VIDEO_EXTEND
&& task.type != BookBackTaskType.HAILUO_TEXT_TO_VIDEO
&& task.type != BookBackTaskType.HAILUO_IMAGE_TO_VIDEO
&& task.type != BookBackTaskType.HAILUO_FIRST_LAST_FRAME
) { ) {
// 转存一下视频文件 // 转存一下视频文件
// 获取当前url的文件名 // 获取当前url的文件名

View File

@ -2,6 +2,9 @@ import { errorMessage, SendReturnMessage, successMessage } from '@/public/genera
import { BookBasicHandle } from './bookBasicHandle' import { BookBasicHandle } from './bookBasicHandle'
import { Book } from '@/define/model/book/book' import { Book } from '@/define/model/book/book'
import { import {
HailuoDuration,
HailuoModel,
HailuoResolution,
ImageToVideoModels, ImageToVideoModels,
KlingDuration, KlingDuration,
KlingMode, KlingMode,
@ -239,15 +242,45 @@ export class BookVideoServiceHandle extends BookBasicHandle {
index: undefined, index: undefined,
motion: MJVideoMotion.High, // 根据 Motion 类型的定义提供默认值 motion: MJVideoMotion.High, // 根据 Motion 类型的定义提供默认值
noStorage: false, noStorage: false,
notifyHook: undefined, notifyHook: "",
prompt: null, prompt: null,
state: undefined, state: "",
taskId: undefined, taskId: "",
raw: false, raw: false,
batchSize: MJVideoBatchSize.ONE, batchSize: MJVideoBatchSize.ONE,
videoType: MJVideoType.HD videoType: MJVideoType.HD
} }
let hailuoTextToVideoOptions: BookTaskDetail.HailuoTextToVideoOptions = {
model: HailuoModel.MINIMAX_HAILUO_02,
prompt: '',
prompt_optimizer: true,
fast_pretreatment: false,
duration: HailuoDuration.SIX,
resolution: HailuoResolution.P768
}
// 初始化海螺设置
let hailuoFirstFrameOnlyOptions: BookTaskDetail.HailuoFirstFrameOnlyOptions = {
model: HailuoModel.MINIMAX_HAILUO_02,
prompt: '',
prompt_optimizer: true,
fast_pretreatment: false,
duration: HailuoDuration.SIX,
resolution: HailuoResolution.P768,
first_frame_image: outImage
}
let hailuoFirstLastFrameOptions: BookTaskDetail.HailuoFirstLastFrameOptions = {
model: HailuoModel.MINIMAX_HAILUO_02,
prompt: '',
first_frame_image: outImage,
last_frame_image: "",
prompt_optimizer: true,
duration: HailuoDuration.SIX,
resolution: HailuoResolution.P768,
}
let videoMessage: BookTaskDetail.VideoMessage = { let videoMessage: BookTaskDetail.VideoMessage = {
id: bookTaskDetail.id, id: bookTaskDetail.id,
msg: '', msg: '',
@ -264,7 +297,7 @@ export class BookVideoServiceHandle extends BookBasicHandle {
model: VideoModel.IMAGE_TO_VIDEO model: VideoModel.IMAGE_TO_VIDEO
} }
return { optionObject, lumaOptions, klingOptions, mjVideoOptions, videoMessage } return { optionObject, lumaOptions, klingOptions, mjVideoOptions, hailuoTextToVideoOptions, hailuoFirstFrameOnlyOptions, hailuoFirstLastFrameOptions, videoMessage }
} catch (error) { } catch (error) {
throw error throw error
} }
@ -324,6 +357,15 @@ export class BookVideoServiceHandle extends BookBasicHandle {
case BookBackTaskType.KLING_VIDEO_EXTEND: case BookBackTaskType.KLING_VIDEO_EXTEND:
res = await videoHandle.KlingVideoExtend(task) res = await videoHandle.KlingVideoExtend(task)
break break
case BookBackTaskType.HAILUO_TEXT_TO_VIDEO:
res = await videoHandle.HailuoTextToVideo(task)
break
case BookBackTaskType.HAILUO_IMAGE_TO_VIDEO:
res = await videoHandle.HailuoImageToVideo(task)
break
case BookBackTaskType.HAILUO_FIRST_LAST_FRAME:
res = await videoHandle.HailuoFirstLastFrameToVideo(task)
break
default: default:
throw new Error(t('未知的视频生成方式,请检查')) throw new Error(t('未知的视频生成方式,请检查'))
} }

View File

@ -1,60 +1,16 @@
import { OptionRealmService } from '@/define/db/service/optionService'
import { OptionKeyName } from '@/define/enum/option' import { OptionKeyName } from '@/define/enum/option'
import { optionSerialization } from '../option/optionSerialization' import { optionSerialization } from '../option/optionSerialization'
import { SettingModal } from '@/define/model/setting' import { SettingModal } from '@/define/model/setting'
import { BookTaskDetailService } from '@/define/db/service/book/bookTaskDetailService'
import { BookTaskService } from '@/define/db/service/book/bookTaskService'
import { BookService } from '@/define/db/service/book/bookService'
import { PresetRealmService } from '@/define/db/service/presetService'
import { TaskListService } from '@/define/db/service/book/taskListService'
import { t } from '@/i18n' import { t } from '@/i18n'
import { BookBasicHandle } from '../book/subBookHandle/bookBasicHandle'
export class MJBasic { export class MJBasic extends BookBasicHandle {
optionRealmService!: OptionRealmService
mjGeneralSetting?: SettingModal.MJGeneralSettings mjGeneralSetting?: SettingModal.MJGeneralSettings
mjApiSetting?: SettingModal.MJApiSettings mjApiSetting?: SettingModal.MJApiSettings
mjPackageSetting?: SettingModal.MJPackageSetting mjPackageSetting?: SettingModal.MJPackageSetting
mjRemoteSetting?: SettingModal.MJRemoteSetting mjRemoteSetting?: SettingModal.MJRemoteSetting
mjLocalSetting?: SettingModal.MJLocalSetting mjLocalSetting?: SettingModal.MJLocalSetting
bookTaskDetailService!: BookTaskDetailService
bookTaskService!: BookTaskService
bookService!: BookService
presetRealmService!: PresetRealmService
taskListService!: TaskListService
/**
* MJBasic类基础服务
*
* MJBasic类所需的选项服务(optionRealmService,bookTaskDetailService,bookTaskService)
*
* 访MJ相关操作的前提条件
*
* @returns {Promise<void>} Promise对象
* @throws {Error} OptionRealmService.getInstance()
*/
async InitMJBasic(): Promise<void> {
// 如果 mjServiceHandle 已经初始化,则直接返回
if (!this.optionRealmService) {
this.optionRealmService = await OptionRealmService.getInstance()
}
if (!this.bookTaskDetailService) {
this.bookTaskDetailService = await BookTaskDetailService.getInstance()
}
if (!this.bookTaskService) {
this.bookTaskService = await BookTaskService.getInstance()
}
if (!this.bookService) {
this.bookService = await BookService.getInstance()
}
if (!this.presetRealmService) {
this.presetRealmService = await PresetRealmService.getInstance()
}
if (!this.taskListService) {
this.taskListService = await TaskListService.getInstance()
}
}
/** /**
* Midjourney通用设置 * Midjourney通用设置
@ -68,7 +24,7 @@ export class MJBasic {
* @throws {Error} optionSerialization可能会抛出错误 * @throws {Error} optionSerialization可能会抛出错误
*/ */
async GetMJGeneralSetting(): Promise<void> { async GetMJGeneralSetting(): Promise<void> {
await this.InitMJBasic() await this.InitBookBasicHandle()
let generalSetting = this.optionRealmService.GetOptionByKey( let generalSetting = this.optionRealmService.GetOptionByKey(
OptionKeyName.Midjourney.GeneralSetting OptionKeyName.Midjourney.GeneralSetting
) )
@ -111,7 +67,7 @@ export class MJBasic {
* @throws {Error} optionSerialization可能会抛出错误 * @throws {Error} optionSerialization可能会抛出错误
*/ */
async GetMJPackageSetting(): Promise<void> { async GetMJPackageSetting(): Promise<void> {
await this.InitMJBasic() await this.InitBookBasicHandle()
let packageSetting = this.optionRealmService.GetOptionByKey( let packageSetting = this.optionRealmService.GetOptionByKey(
OptionKeyName.Midjourney.PackageSetting OptionKeyName.Midjourney.PackageSetting
) )
@ -135,7 +91,7 @@ export class MJBasic {
* @throws {Error} optionSerialization可能会抛出错误 * @throws {Error} optionSerialization可能会抛出错误
*/ */
async GetMjRemoteSetting(): Promise<void> { async GetMjRemoteSetting(): Promise<void> {
await this.InitMJBasic() await this.InitBookBasicHandle()
let remoteSetting = this.optionRealmService.GetOptionByKey( let remoteSetting = this.optionRealmService.GetOptionByKey(
OptionKeyName.Midjourney.RemoteSetting OptionKeyName.Midjourney.RemoteSetting
) )
@ -159,7 +115,7 @@ export class MJBasic {
* @throws {Error} optionSerialization可能会抛出错误 * @throws {Error} optionSerialization可能会抛出错误
*/ */
async GetMjLocalSetting(): Promise<void> { async GetMjLocalSetting(): Promise<void> {
await this.InitMJBasic() await this.InitBookBasicHandle()
let localSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.LocalSetting) let localSetting = this.optionRealmService.GetOptionByKey(OptionKeyName.Midjourney.LocalSetting)
this.mjLocalSetting = optionSerialization<SettingModal.MJLocalSetting>( this.mjLocalSetting = optionSerialization<SettingModal.MJLocalSetting>(
localSetting, localSetting,

View File

@ -440,6 +440,9 @@ export class TaskManager {
case BookBackTaskType.KLING_VIDEO_EXTEND: case BookBackTaskType.KLING_VIDEO_EXTEND:
case BookBackTaskType.MJ_VIDEO: case BookBackTaskType.MJ_VIDEO:
case BookBackTaskType.MJ_VIDEO_EXTEND: case BookBackTaskType.MJ_VIDEO_EXTEND:
case BookBackTaskType.HAILUO_TEXT_TO_VIDEO:
case BookBackTaskType.HAILUO_IMAGE_TO_VIDEO:
case BookBackTaskType.HAILUO_FIRST_LAST_FRAME:
this.AddImageToVideo(task) this.AddImageToVideo(task)
break break

View File

@ -0,0 +1,886 @@
import { TaskModal } from "@/define/model/task";
import { VideoBasicHandle } from "./videoBasic";
import { t } from "@/i18n";
import { Book } from "@/define/model/book/book";
import { BookTaskDetail } from "@/define/model/book/bookTaskDetail";
import { ValidateJson } from "@/define/Tools/validate";
import { GetHailuoModelOptions, HailuoModel, IsValidDuratio, IsValidResolution, ToVIdeoType, VideoStatus } from "@/define/enum/video";
import { cloneDeep, isEmpty } from "lodash";
import axios from "axios";
import { SendReturnMessage, successMessage } from "@/public/generalTools";
import { ResponseMessageType } from "@/define/enum/softwareEnum";
import { BookBackTaskStatus, BookTaskStatus } from "@/define/enum/bookEnum";
import { GetImageBase64 } from "@/define/Tools/image";
//#region private interface
/**
*
*
*
*
*/
interface HaiLuoOptionsResult {
/** 分镜任务详情对象,包含任务的基本信息和状态 */
bookTaskDetail: Book.SelectBookTaskDetail,
/** 视频消息配置对象,包含视频生成相关的所有配置信息 */
videoMessage: BookTaskDetail.VideoMessage,
}
/**
*
*
* HaiLuoOptionsResult
*
*/
interface HaiLuoTextToVideoOptionsResult extends HaiLuoOptionsResult {
/** 解析后的海螺文生视频配置参数对象 */
hailuoOption: BookTaskDetail.HailuoTextToVideoOptions
}
/**
*
*
* HaiLuoOptionsResult
*
*/
interface HaiLuoFirstFrameOnlyOptionsResult extends HaiLuoOptionsResult {
/** 解析后的海螺图生视频配置参数对象 */
hailuoOption: BookTaskDetail.HailuoFirstFrameOnlyOptions
}
/**
*
*
* HaiLuoOptionsResult
*
*/
interface HaiLuoFirstLastFrameOptionsResult extends HaiLuoOptionsResult {
/** 解析后的海螺首尾帧视频配置参数对象 */
hailuoOption: BookTaskDetail.HailuoFirstLastFrameOptions
}
//#endregion
export class HaiLuoVideoService extends VideoBasicHandle {
/**
*
*
* VideoBasicHandle基类
* AI视频生成相关的完整功能支持
*
* @description
*
* -
* -
* -
* -
* -
*/
constructor() {
super();
}
//#region 首尾帧
/**
*
*
* AI首尾帧视频任务的完整流程
* MiniMax-Hailuo-02
*
* @param task
*
* @description
*
* 1. API设置
* 2.
* 3.
* 4. base64URL
* 5. API请求体并调用海螺视频生成接口
* 6.
*
*
* - 使
* -
* - 6
* -
*
* @throws {Error}
* @throws {Error}
* @throws {Error}
* @throws {Error} API调用失败时抛出异常
*
* @returns
*
* @example
* ```typescript
* const task = {
* bookTaskDetailId: "task-123",
* messageName: "HAILUO_FIRST_LAST_FRAME_VIDEO_RETURN"
* };
* await hailuoService.HailuoFirstLastFrameToVideo(task);
* ```
*/
async HailuoFirstLastFrameToVideo(task: TaskModal.Task) {
try {
// 初始化基础句柄和API设置
await this.InitBookBasicHandle();
await this.InitApiSetting();
let { bookTaskDetail, hailuoOption, videoMessage } = await this.GetHailuoOptions(task.bookTaskDetailId as string, 'firstLastFrame');
this.CheckHaiLuoParams("firstLastFrame", hailuoOption);
let model = hailuoOption.model;
let prompt = hailuoOption.prompt;
let first_frame_image = hailuoOption.first_frame_image;
let last_frame_image = hailuoOption.last_frame_image;
let prompt_optimizer = hailuoOption.prompt_optimizer == null ? true : hailuoOption.prompt_optimizer;
let duration = hailuoOption.duration;
let resolution = hailuoOption.resolution;
if (!first_frame_image.startsWith("http")) {
// 不是网络图片转为base64
first_frame_image = await GetImageBase64(first_frame_image, false)
}
if (!last_frame_image.startsWith("http")) {
// 不是网络图片转为base64
last_frame_image = await GetImageBase64(last_frame_image, false)
}
let body: BookTaskDetail.HailuoFirstLastFrameOptions = {
model: model,
prompt: prompt,
first_frame_image: first_frame_image,
last_frame_image: last_frame_image,
prompt_optimizer: prompt_optimizer,
duration: duration,
resolution: resolution
}
let url = this.inferenceSetting.apiProviderItem.base_url + "/v1/video_generation";
let res = await axios.post(url, body, {
headers: {
'Authorization': `Bearer ${this.inferenceSetting.apiToken}`,
'Content-Type': 'application/json'
}
});
let resData = res.data;
// 海螺转视频任务提交成功
await this.HailuoSubmitSuccess(resData, task, bookTaskDetail, videoMessage)
return successMessage(
t('海螺首尾帧视频任务完成!'),
'HaiLuoOptionsResult_HailuoFirstLastFrameToVideo'
)
} catch (error) {
throw new Error(t('海螺首尾帧视频任务失败,失败信息:{error}', { error: (error as Error).message }));
}
}
//#endregion
//#region 图转视频
/**
*
*
* AI图生视频任务的完整流程
* MiniMax-Hailuo-02
*
* @param task
*
* @description
*
* 1. API设置
* 2.
* 3.
* 4. base64URL
* 5. API请求体并调用海螺视频生成接口
* 6.
*
*
* - 512P720P768P1080P
* - 610
* -
* - MiniMax-Hailuo-02
* -
*
* @throws {Error}
* @throws {Error}
* @throws {Error}
* @throws {Error} API调用失败时抛出异常
*
* @returns
*
* @example
* ```typescript
* const task = {
* bookTaskDetailId: "task-123",
* messageName: "HAILUO_IMAGE_TO_VIDEO_RETURN"
* };
* await hailuoService.HailuoImageToVideo(task);
* ```
*/
async HailuoImageToVideo(task: TaskModal.Task) {
try {
// 初始化基础句柄和API设置
await this.InitBookBasicHandle();
await this.InitApiSetting();
let { bookTaskDetail, hailuoOption, videoMessage } = await this.GetHailuoOptions(task.bookTaskDetailId as string, 'imageToVideo');
this.CheckHaiLuoParams("imageToVideo", hailuoOption);
let prompt = hailuoOption.prompt;
let prompt_optimizer = hailuoOption.prompt_optimizer == null ? true : hailuoOption.prompt_optimizer;
let fast_pretreatment = hailuoOption.fast_pretreatment == null ? false : hailuoOption.fast_pretreatment;
let duration = hailuoOption.duration;
let model = hailuoOption.model;
let resolution = hailuoOption.resolution;
let first_frame_image = hailuoOption.first_frame_image;
if (!first_frame_image.startsWith("http")) {
// 不是网络图片转为base64
first_frame_image = await GetImageBase64(first_frame_image, false)
}
let body: BookTaskDetail.HailuoFirstFrameOnlyOptions = {
model: model,
prompt: prompt,
prompt_optimizer: prompt_optimizer,
duration: duration,
resolution: resolution,
first_frame_image: first_frame_image
}
if (model == HailuoModel.MINIMAX_HAILUO_02) {
body.fast_pretreatment = fast_pretreatment
}
let url = this.inferenceSetting.apiProviderItem.base_url + "/v1/video_generation";
let res = await axios.post(url, body, {
headers: {
'Authorization': `Bearer ${this.inferenceSetting.apiToken}`,
'Content-Type': 'application/json'
}
});
let resData = res.data;
// 海螺转视频任务提交成功
await this.HailuoSubmitSuccess(resData, task, bookTaskDetail, videoMessage)
return successMessage(
t('海螺图转视频任务完成!'),
'HaiLuoOptionsResult_HailuoImageToVideo'
)
} catch (error) {
throw new Error(t('海螺图转视频任务失败,失败信息:{error}', { error: (error as Error).message }));
}
}
//#endregion
//#region 文生视频
/**
*
*
* AI文生视频任务的完整流程API调用和结果处理
* MiniMax-Hailuo-02
*
* @param task
*
* @description
*
* 1. API设置
* 2.
* 3.
* 4. API请求体并调用海螺视频生成接口
* 5.
*
* @throws {Error}
* @throws {Error}
* @throws {Error} API调用失败时抛出异常
*
* @returns
*
* @example
* ```typescript
* const task = {
* bookTaskDetailId: "task-123",
* messageName: "HAILUO_TEXT_TO_VIDEO_RETURN"
* };
* await hailuoService.HailuoTextToVideo(task);
* ```
*/
async HailuoTextToVideo(task: TaskModal.Task) {
try {
// 初始化基础句柄和API设置
await this.InitBookBasicHandle();
await this.InitApiSetting();
let { bookTaskDetail, hailuoOption, videoMessage } = await this.GetHailuoOptions(task.bookTaskDetailId as string, 'textToVideo');
let prompt = hailuoOption.prompt;
if (prompt == null || isEmpty(prompt)) {
throw new Error(t("当前分镜数据的海螺视频参数的提示词为空,请检查"))
}
// 检查参数
this.CheckHaiLuoParams("textToVideo", hailuoOption);
let model = hailuoOption.model;
let body: BookTaskDetail.HailuoTextToVideoOptions = {
model: model,
prompt: prompt,
prompt_optimizer: hailuoOption.prompt_optimizer == null ? true : hailuoOption.prompt_optimizer,
duration: hailuoOption.duration,
resolution: hailuoOption.resolution,
}
if (model == HailuoModel.MINIMAX_HAILUO_02) {
body.fast_pretreatment = hailuoOption.fast_pretreatment == null ? false : hailuoOption.fast_pretreatment
}
let url = this.inferenceSetting.apiProviderItem.base_url + "/v1/video_generation"
let res = await axios.post(url, body, {
headers: {
'Authorization': `Bearer ${this.inferenceSetting.apiToken}`,
'Content-Type': 'application/json'
}
})
let resData = res.data;
// 海螺转视频任务提交成功
await this.HailuoSubmitSuccess(resData, task, bookTaskDetail, videoMessage)
return successMessage(
t('海螺文生视频任务完成!'),
'HaiLuoOptionsResult_HailuoTextToVideo'
)
} catch (error) {
throw new Error(t('海螺文生视频任务失败,失败信息:{error}', { error: (error as Error).message }));
}
}
//#endregion
//#region fetch task status
/**
*
*
*
*
*
* @param bookTaskDetail
* @param task
* @param hailuoId ID
* @param isTransfer 使
*
* @description
*
* 1.
* 2.
* 3.
* 4.
*
*
* - Preparing/Queueing/Processing: 更新为处理中状态20
* - Success: 获取视频下载地址
* - 其他: 视为失败
*
* @throws {Error} API调用失败时抛出异常
* @throws {Error}
*
* @example
* ```typescript
* await hailuoService.FecthHailuoToVideoResult(
* bookTaskDetail,
* task,
* "hailuo-task-123",
* false
* );
* ```
*/
async FecthHailuoToVideoResult(bookTaskDetail: Book.SelectBookTaskDetail, task: TaskModal.Task, hailuoId: string, isTransfer: boolean) {
console.log(isTransfer);
while (true) {
let fetchUrl = this.inferenceSetting.apiProviderItem.base_url + "/v1/query/video_generation" + `?task_id=${hailuoId}`;
let res = await axios.get(fetchUrl, {
headers: {
'Authorization': `Bearer ${this.inferenceSetting.apiToken}`,
'Content-Type': 'application/json'
}
});
let resData = res.data;
console.log(resData);
if (resData.status == undefined && resData.base_resp && resData.base_resp.status_code == 0) {
resData.status = "Processing"
}
// 判断不同的状态
if (resData.status == "Preparing" || resData.status == "Queueing" || resData.status == "Processing") {
// 任务正在执行中
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.PROCESSING
videoMessage.taskId = hailuoId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('海螺视频任务正在执行中...'),
type: ResponseMessageType.HAI_LUO_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
// 没有成功 等待二十秒后继续执行
await new Promise((resolve) => setTimeout(resolve, 20000))
} else if (resData.status == "Success") {
// 成功
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.SUCCESS
videoMessage.taskId = hailuoId
let fileId = resData.file_id
// 获取海螺的视频下载地址
let { videoUrls, fileIds } = await this.GetHailuoVideoUrl(fileId, hailuoId);
if (videoUrls.length > 0) {
videoMessage.videoUrls = []
videoUrls.forEach((item: any) => {
videoMessage.videoUrls?.push(item.url || item)
})
}
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
// 修改小说分镜状态
this.bookTaskDetailService.ModifyBookTaskDetailById(task.bookTaskDetailId as string, {
status: BookTaskStatus.IMAGE_TO_VIDEO_SUCCESS
})
// 修改任务状态
this.taskListService.UpdateBackTaskData(task.id as string, {
status: BookBackTaskStatus.DONE,
taskId: hailuoId,
taskMessage: JSON.stringify(resData)
})
// 下载 视频
await this.DownloadVideoUrls(videoMessage.videoUrls || [], task, bookTaskDetail, hailuoId, fileIds)
SendReturnMessage(
{
code: 1,
id: bookTaskDetail.id as string,
message: t('海螺视频任务已完成!'),
type: ResponseMessageType.HAI_LUO_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
break;
} else {
// 失败
// 修改小说分镜的 videoMessage
let videoMessage = cloneDeep(bookTaskDetail.videoMessage) ?? {}
videoMessage.status = VideoStatus.FAIL
videoMessage.msg = resData.task_status_msg
videoMessage.taskId = hailuoId
videoMessage.messageData = JSON.stringify(resData)
delete videoMessage.imageUrl
// 修改 videoMessage数据
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
bookTaskDetail.id as string,
videoMessage
)
// 修改TASK
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: hailuoId,
taskMessage: JSON.stringify(resData)
})
// 返回前端数据
SendReturnMessage(
{
code: 0,
id: bookTaskDetail.id as string,
message: t('海螺视频任务失败,失败信息:{error}', {
error: resData.base_resp.status_msg || t("未知错误")
}),
type: ResponseMessageType.HAI_LUO_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
throw new Error(resData.base_resp.status_msg)
}
}
}
//#endregion
//#region get video url
/**
*
*
* ID和任务ID从海螺API获取生成视频的下载地址
*
*
* @param fileId ID
* @param hailuoId ID
*
* @returns Promise<{videoUrls: string[], fileIds: string[]}> ID的对象
* - videoUrls: 视频下载地址数组
* - fileIds: 文件ID数组
*
* @description
*
* 1. API的请求URL
* 2. GET请求获取文件信息
* 3.
* 4.
*
* @throws {Error} API请求失败时抛出异常
* @throws {Error} 0
*
* @example
* ```typescript
* const { videoUrls, fileIds } = await hailuoService.GetHailuoVideoUrl(
* "file-123",
* "hailuo-task-456"
* );
* console.log("视频下载地址:", videoUrls[0]);
* ```
*/
async GetHailuoVideoUrl(fileId: string, hailuoId: string): Promise<{ videoUrls: string[], fileIds: string[] }> {
let fetchUrl = this.inferenceSetting.apiProviderItem.base_url + `/v1/files/retrieve?file_id=${fileId}&task_id=${hailuoId}`;
let res = await axios.get(fetchUrl, {
headers: {
'Authorization': `Bearer ${this.inferenceSetting.apiToken}`,
'Content-Type': 'application/json'
}
});
let resData = res.data;
let statusCode = resData.base_resp.status_code;
if (statusCode != 0) {
throw new Error(resData.base_resp.status_msg || t("获取海螺视频下载地址失败"));
}
let videoUrl = resData.file.download_url;
if (videoUrl == null || isEmpty(videoUrl)) {
throw new Error(t("获取海螺视频下载地址失败,下载地址为空"));
}
return {
videoUrls: [videoUrl],
fileIds: [fileId]
}
}
//#endregion
//#region submit success
/**
*
*
* API后执行
* ID和响应信息
*
* @param resData API响应数据ID和其他相关信息
* @param task
* @param bookTaskDetail
* @param videoMessage
*
* @description
*
* 1. ID并更新后端任务表
* 2. SUBMITTED
* 3. API响应数据用于后续查询
* 4.
* 5.
*
*
* - SUBMITTED
* -
* - API响应数据
*
* @example
* ```typescript
* const resData = { task_id: "hailuo-123", status: "submitted" };
* await hailuoService.HailuoSubmitSuccess(resData, task, bookTaskDetail, videoMessage);
* ```
*/
async HailuoSubmitSuccess(resData: any, task: TaskModal.Task, bookTaskDetail: Book.SelectBookTaskDetail, videoMessage: BookTaskDetail.VideoMessage) {
// 修改Task, 将数据写入
let hailuoId = resData.task_id
this.taskListService.UpdateBackTaskData(task.id as string, {
taskId: hailuoId as string,
taskMessage: JSON.stringify(resData)
})
// 修改videoMessage
videoMessage.taskId = hailuoId
videoMessage.status = VideoStatus.SUBMITTED
videoMessage.messageData = JSON.stringify(resData)
videoMessage.msg = ''
delete videoMessage.imageUrl // 不要修改原本的图片地址
this.bookTaskDetailService.UpdateBookTaskDetailVideoMessage(
task.bookTaskDetailId as string,
videoMessage
)
// 添加任务成功 返回前端任务事件
SendReturnMessage(
{
code: 1,
id: task.bookTaskDetailId as string,
message: t("已成功提交海螺视频任务任务ID{taskId}", { taskId: hailuoId }),
type: ResponseMessageType.HAI_LUO_VIDEO,
data: JSON.stringify(videoMessage)
},
task.messageName as string
)
await this.FecthHailuoToVideoResult(bookTaskDetail, task, hailuoId, false)
}
//#endregion
//#region hailuo check params
/**
*
*
* API要求和模型限制
*
*
* @param type - 'textToVideo' (), 'imageToVideo' (), 'firstLastFrame' ()
* @param hailuoOption
*
* @returns {boolean} true
*
* @description
*
* 1.
* 2.
* 3.
* -
* -
* -
* 4.
*
* @throws {Error}
* @throws {Error}
* @throws {Error} /
* @throws {Error} /
* @throws {Error}
*
* @example
* ```typescript
* const options = {
* model: HailuoModel.MINIMAX_HAILUO_02,
* resolution: HailuoResolution.P768,
* duration: HailuoDuration.SIX,
* prompt: "生成一个美丽的风景视频"
* };
*
* const isValid = hailuoService.CheckHaiLuoParams('textToVideo', options);
* // isValid === true
* ```
*/
CheckHaiLuoParams(type: ToVIdeoType, hailuoOption: BookTaskDetail.HailuoTextToVideoOptions | BookTaskDetail.HailuoFirstFrameOnlyOptions | BookTaskDetail.HailuoFirstLastFrameOptions): boolean {
if (hailuoOption.model == null || isEmpty(hailuoOption.model)) {
throw new Error(t("当前分镜数据的海螺视频参数的模型参数为空,请检查"))
}
// 检验类型
let models = GetHailuoModelOptions(type);
if (models.some(m => m.value == hailuoOption.model) == false) {
throw new Error(t("当前分镜数据的海螺视频参数的模型参数不合法,请检查"))
}
if (hailuoOption.resolution == undefined || hailuoOption.resolution == null) {
throw new Error(t("当前分镜数据的海螺视频参数的分辨率参数为空,请检查"))
}
if (hailuoOption.duration == undefined || hailuoOption.duration == null) {
throw new Error(t("当前分镜数据的海螺视频参数的时长参数为空,请检查"))
}
// 检查分辨率和时长
let checkResolution = IsValidResolution(type, hailuoOption.model, hailuoOption.duration, hailuoOption.resolution);
let checkDuration = IsValidDuratio(type, hailuoOption.model, hailuoOption.resolution, hailuoOption.duration);
if (!checkResolution) {
throw new Error(t("当前分镜数据的海螺视频参数的分辨率参数不合法,请检查"))
}
if (!checkDuration) {
throw new Error(t("当前分镜数据的海螺视频参数的时长参数不合法,请检查"))
}
if (type == "textToVideo" || type == "imageToVideo") {
if (hailuoOption.model != HailuoModel.MINIMAX_HAILUO_02 && hailuoOption.fast_pretreatment == true) {
throw new Error(t("非MiniMax-Hailuo-02不支持启用fast_pretreatment参数请检查"))
}
}
if (type == 'imageToVideo') {
let first_frame_image = (hailuoOption as BookTaskDetail.HailuoFirstFrameOnlyOptions).first_frame_image;
if (first_frame_image == null || isEmpty(first_frame_image)) {
throw new Error(t("当前分镜数据的海螺图生视频参数的首帧图片参数为空,请检查"))
}
}
if (type == 'firstLastFrame') {
let first_frame_image = (hailuoOption as BookTaskDetail.HailuoFirstLastFrameOptions).first_frame_image;
if (first_frame_image == null || isEmpty(first_frame_image)) {
throw new Error(t("当前分镜数据的海螺首尾帧视频参数的首帧图片参数为空,请检查"))
}
let last_frame_image = (hailuoOption as BookTaskDetail.HailuoFirstLastFrameOptions).last_frame_image;
if (last_frame_image == null || isEmpty(last_frame_image)) {
throw new Error(t("当前分镜数据的海螺首尾帧视频参数的尾帧图片参数为空,请检查"))
}
}
return true;
}
//#endregion
//#region get hailuo options
/**
*
* @param bookTaskDetailId ID
* @param type "textToVideo"
* @returns
*/
async GetHailuoOptions(bookTaskDetailId: string, type: "textToVideo"): Promise<HaiLuoTextToVideoOptionsResult>
/**
*
* @param bookTaskDetailId ID
* @param type "imageToVideo"
* @returns
*/
async GetHailuoOptions(bookTaskDetailId: string, type: "imageToVideo"): Promise<HaiLuoFirstFrameOnlyOptionsResult>
/**
*
* @param bookTaskDetailId ID
* @param type "firstLastFrame"
* @returns
*/
async GetHailuoOptions(bookTaskDetailId: string, type: "firstLastFrame"): Promise<HaiLuoFirstLastFrameOptionsResult>
/**
*
*
*
* @param bookTaskDetailId ID -
* @param type -
* - "textToVideo":
* - "imageToVideo":
* - "firstLastFrame":
*
* @returns
* - bookTaskDetail: 分镜任务详情数据
* - videoMessage: 视频消息配置信息
* - hailuoOption: 解析后的海螺视频配置参数
*
* @throws {Error}
* @throws {Error} JSON格式错误时抛出异常
* @throws {Error}
*
* @example
* ```typescript
* // 获取文生视频配置
* const textOptions = await hailuoService.GetHailuoOptions(taskId, "textToVideo");
*
* // 获取图生视频配置
* const imageOptions = await hailuoService.GetHailuoOptions(taskId, "imageToVideo");
*
* // 获取首尾帧视频配置
* const frameOptions = await hailuoService.GetHailuoOptions(taskId, "firstLastFrame");
* ```
*/
async GetHailuoOptions(bookTaskDetailId: string, type: ToVIdeoType = "textToVideo"): Promise<HaiLuoTextToVideoOptionsResult | HaiLuoFirstFrameOnlyOptionsResult | HaiLuoFirstLastFrameOptionsResult> {
// 1. 根据分镜任务详情ID查询分镜数据包含关联的视频消息配置
let bookTaskDetail = await this.bookTaskDetailService.GetBookTaskDetailDataById(bookTaskDetailId, true);
// 2. 验证视频配置信息是否存在
let videoMessage = bookTaskDetail.videoMessage
if (videoMessage == null || videoMessage == undefined) {
throw new Error(t("小说批次任务的分镜数据的转视频配置为空,请检查"))
}
// 3. 根据视频类型获取对应的配置字段名和错误提示信息
let hailuoOptionsString: string
let errorMessage: string
switch (type) {
case "textToVideo":
// 文生视频:获取文本转视频的配置参数
hailuoOptionsString = bookTaskDetail.videoMessage?.hailuoTextToVideoOptions as string
errorMessage = t("当前分镜数据的海螺文生视频参数为空或参数校验失败,请检查")
break
case "imageToVideo":
// 图生视频:获取图片转视频的配置参数(首帧图片)
hailuoOptionsString = bookTaskDetail.videoMessage?.hailuoFirstFrameOnlyOptions as string
errorMessage = t("当前分镜数据的海螺图生视频参数为空或参数校验失败,请检查")
break
case "firstLastFrame":
// 首尾帧视频:获取首尾帧图片转视频的配置参数
hailuoOptionsString = bookTaskDetail.videoMessage?.hailuoFirstLastFrameOptions as string
errorMessage = t("当前分镜数据的海螺首尾帧视频参数为空或参数校验失败,请检查")
break
default:
// 不支持的视频类型,抛出异常
throw new Error(t("不支持的海螺视频类型:{type}", { type }))
}
// 4. 验证配置参数是否为有效的JSON格式
if (!ValidateJson(hailuoOptionsString)) {
throw new Error(errorMessage)
}
// 5. 解析JSON配置参数为对象
let hailuoOptions = JSON.parse(hailuoOptionsString)
// 6. 返回包含分镜详情、视频消息和海螺配置的完整结果对象
return { bookTaskDetail: bookTaskDetail, hailuoOption: hailuoOptions, videoMessage: videoMessage }
}
//#endregion
}

View File

@ -1,14 +1,17 @@
import { TaskModal } from '@/define/model/task' import { TaskModal } from '@/define/model/task'
import { MJVideoService } from './mjVideo' import { MJVideoService } from './mjVideo'
import { KlingVideoService } from './klingVideo' import { KlingVideoService } from './klingVideo'
import { HaiLuoVideoService } from './hailuoVideo'
export class VideoHandle { export class VideoHandle {
mjVideoService: MJVideoService mjVideoService: MJVideoService
klingVideoService: KlingVideoService klingVideoService: KlingVideoService
hailuoVideoService: HaiLuoVideoService
// 这里可以添加 VideoHandle 特有的方法 // 这里可以添加 VideoHandle 特有的方法
constructor() { constructor() {
// mixin 装饰器会处理初始化 // mixin 装饰器会处理初始化
this.mjVideoService = new MJVideoService() this.mjVideoService = new MJVideoService()
this.klingVideoService = new KlingVideoService() this.klingVideoService = new KlingVideoService()
this.hailuoVideoService = new HaiLuoVideoService()
} }
/** MJ图片转视频处理方法 将指定的图片通过Midjourney API转换为视频 */ /** MJ图片转视频处理方法 将指定的图片通过Midjourney API转换为视频 */
@ -26,7 +29,20 @@ export class VideoHandle {
return this.klingVideoService.KlingImageToVideo(task) return this.klingVideoService.KlingImageToVideo(task)
} }
/** 可灵视频延长服务 */
KlingVideoExtend(task: TaskModal.Task) { KlingVideoExtend(task: TaskModal.Task) {
return this.klingVideoService.KlingVideoExtend(task) return this.klingVideoService.KlingVideoExtend(task)
} }
HailuoTextToVideo(task: TaskModal.Task) {
return this.hailuoVideoService.HailuoTextToVideo(task)
}
HailuoImageToVideo(task: TaskModal.Task) {
return this.hailuoVideoService.HailuoImageToVideo(task)
}
HailuoFirstLastFrameToVideo(task: TaskModal.Task) {
return this.hailuoVideoService.HailuoFirstLastFrameToVideo(task)
}
} }

View File

@ -1,9 +1,6 @@
import { TaskModal } from "@/define/model/task"; import { TaskModal } from "@/define/model/task";
import { BookBasicHandle } from "../book/subBookHandle/bookBasicHandle";
import { getInferenceSetting } from "../option/optionCommonService";
import { t } from "@/i18n"; import { t } from "@/i18n";
import { cloneDeep, isEmpty } from "lodash"; import { cloneDeep, isEmpty } from "lodash";
import { SettingModal } from "@/define/model/setting";
import { ValidateJson } from "@/define/Tools/validate"; import { ValidateJson } from "@/define/Tools/validate";
import { BookTaskDetail } from "@/define/model/book/bookTaskDetail"; import { BookTaskDetail } from "@/define/model/book/bookTaskDetail";
import { KlingDuration, KlingMode, KlingModelName, VideoStatus } from "@/define/enum/video"; import { KlingDuration, KlingMode, KlingModelName, VideoStatus } from "@/define/enum/video";
@ -15,28 +12,13 @@ import { ResponseMessageType } from "@/define/enum/softwareEnum";
import { GeneralResponse } from "@/define/model/generalResponse"; import { GeneralResponse } from "@/define/model/generalResponse";
import { Book } from "@/define/model/book/book"; import { Book } from "@/define/model/book/book";
import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus } from "@/define/enum/bookEnum"; import { BookBackTaskStatus, BookBackTaskType, BookTaskStatus } from "@/define/enum/bookEnum";
import { VideoBasicHandle } from "./videoBasic";
export class KlingVideoService extends BookBasicHandle { export class KlingVideoService extends VideoBasicHandle {
inferenceSetting!: SettingModal.InferenceAISettingAndProvider
constructor() { constructor() {
super(); super();
} }
private async InitApiSetting() {
// 加载推理设置中的数据
const inferenceSetting = await getInferenceSetting();
this.inferenceSetting = inferenceSetting;
// 判断一些数据是不是存在
if (isEmpty(this.inferenceSetting.apiProviderItem.base_url)) {
throw new Error(t('未找到有效的API地址'));
}
if (this.inferenceSetting.apiToken == null || isEmpty(this.inferenceSetting.apiToken)) {
throw new Error(t('请先配置AI推理的API密钥'));
}
}
//#region KlingImageToVideo //#region KlingImageToVideo
/** /**
* *

View File

@ -39,7 +39,7 @@ export class MJVideoService extends MJApiService {
*/ */
async MJImageToVideo(task: TaskModal.Task) { async MJImageToVideo(task: TaskModal.Task) {
try { try {
await this.InitMJBasic() await this.InitBookBasicHandle()
// 加载设置 // 加载设置
await this.InitMJSetting(ImageGenerateMode.MJ_API) await this.InitMJSetting(ImageGenerateMode.MJ_API)
@ -177,7 +177,7 @@ export class MJVideoService extends MJApiService {
async MJVideoExtendToVideo(task: TaskModal.Task) { async MJVideoExtendToVideo(task: TaskModal.Task) {
try { try {
await this.InitMJBasic() await this.InitBookBasicHandle()
await this.InitMJSetting(ImageGenerateMode.MJ_API) await this.InitMJSetting(ImageGenerateMode.MJ_API)
// 检查是否支持视频功能 // 检查是否支持视频功能

View File

@ -0,0 +1,110 @@
import { SettingModal } from "@/define/model/setting";
import { BookBasicHandle } from "../book/subBookHandle/bookBasicHandle";
import { getInferenceSetting } from "../option/optionCommonService";
import { isEmpty } from "lodash";
import { t } from "@/i18n";
/**
*
*
* BookBasicHandleAPI配置管理
* AI推理设置的初始化和验证逻辑
*
* @extends BookBasicHandle -
*
* @description
*
* - AI推理API配置的加载和验证
* -
* - API设置基础
* - AI服务提供商的配置管理
*
*
* -
* - API配置和验证逻辑
* -
* -
*
* @note
* - 使
* - inferenceSetting属性用于存储AI推理配置
* - InitApiSetting()
* - Midjourney等多种视频生成服务
*
* @example
* ```typescript
* // 通常作为基类被继承使用
* class KlingVideoService extends VideoBasicHandle {
* async processVideo() {
* await this.InitBookBasicHandle();
* await this.InitApiSetting(); // 使用基类的API初始化
* // 具体的视频处理逻辑...
* }
* }
*
* // 或者直接使用
* const videoHandler = new VideoBasicHandle();
* await videoHandler.InitApiSetting();
* ```
*
* @see BookBasicHandle -
* @see SettingModal.InferenceAISettingAndProvider - AI推理配置数据结构
* @see getInferenceSetting -
*/
export class VideoBasicHandle extends BookBasicHandle {
inferenceSetting!: SettingModal.InferenceAISettingAndProvider;
constructor() {
super()
}
/**
* API设置
*
* AI推理相关的API配置设置
* API提供商信息API地址和密钥的有效性
*
* @throws {Error} API地址为空或无效时抛出错误
* @throws {Error} API密钥未配置或为空时抛出错误
*
* @description
*
* 1. getInferenceSetting()
* 2. inferenceSetting属性
* 3. API提供商的base_url是否存在且有效
* 4. API访问令牌(apiToken)
* 5.
*
* @note
* -
* - API之前执行
* - API调用
* - API提供商的统一配置管理
*
* @example
* ```typescript
* const videoService = new VideoBasicHandle();
* try {
* await videoService.InitApiSetting();
* console.log('API设置初始化成功');
* } catch (error) {
* console.error('API配置错误:', error.message);
* }
* ```
*
* @see getInferenceSetting -
* @see SettingModal.InferenceAISettingAndProvider -
*/
async InitApiSetting() {
// 加载推理设置中的数据
const inferenceSetting = await getInferenceSetting();
this.inferenceSetting = inferenceSetting;
// 判断一些数据是不是存在
if (isEmpty(this.inferenceSetting.apiProviderItem.base_url)) {
throw new Error(t('未找到有效的API地址'));
}
if (this.inferenceSetting.apiToken == null || isEmpty(this.inferenceSetting.apiToken)) {
throw new Error(t('请先配置AI推理的API密钥'));
}
}
}

View File

@ -44,6 +44,9 @@ declare module 'vue' {
EditWord: typeof import('./src/components/Original/Copywriter/EditWord.vue')['default'] EditWord: typeof import('./src/components/Original/Copywriter/EditWord.vue')['default']
FindReplaceRound: typeof import('./src/components/common/Icon/FindReplaceRound.vue')['default'] FindReplaceRound: typeof import('./src/components/common/Icon/FindReplaceRound.vue')['default']
GeneralSettings: typeof import('./src/components/Setting/GeneralSettings.vue')['default'] GeneralSettings: typeof import('./src/components/Setting/GeneralSettings.vue')['default']
HailuoFirstLastFrameInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoHaiLuo/HailuoFirstLastFrameInfo.vue')['default']
HailuoImageToVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoHaiLuo/HailuoImageToVideoInfo.vue')['default']
HailuoTextToVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoHaiLuo/HailuoTextToVideoInfo.vue')['default']
HandGroup: typeof import('./src/components/Original/Copywriter/HandGroup.vue')['default'] HandGroup: typeof import('./src/components/Original/Copywriter/HandGroup.vue')['default']
ImageCompressHome: typeof import('./src/components/ToolBox/ImageCompress/ImageCompressHome.vue')['default'] ImageCompressHome: typeof import('./src/components/ToolBox/ImageCompress/ImageCompressHome.vue')['default']
ImageDisplay: typeof import('./src/components/ToolBox/ImageUpload/ImageDisplay.vue')['default'] ImageDisplay: typeof import('./src/components/ToolBox/ImageUpload/ImageDisplay.vue')['default']
@ -60,6 +63,7 @@ declare module 'vue' {
MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default'] MediaToVideoInfoBasicInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoBasicInfo.vue')['default']
MediaToVideoInfoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue')['default'] MediaToVideoInfoConfig: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoConfig.vue')['default']
MediaToVideoInfoEmptyState: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue')['default'] MediaToVideoInfoEmptyState: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoEmptyState.vue')['default']
MediaToVideoInfoHaiLuoVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoHaiLuo/MediaToVideoInfoHaiLuoVideoInfo.vue')['default']
MediaToVideoInfoHome: typeof import('./src/components/MediaToVideo/MediaToVideoInfoHome.vue')['default'] MediaToVideoInfoHome: typeof import('./src/components/MediaToVideo/MediaToVideoInfoHome.vue')['default']
MediaToVideoInfoKlingVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue')['default'] MediaToVideoInfoKlingVideoInfo: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoKling/MediaToVideoInfoKlingVideoInfo.vue')['default']
MediaToVideoInfoMJVideoExtend: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue')['default'] MediaToVideoInfoMJVideoExtend: typeof import('./src/components/MediaToVideo/MediaToVideoInfo/MediaToVideoInfoMJVideo/MediaToVideoInfoMJVideoExtend.vue')['default']

View File

@ -137,6 +137,11 @@
</div> </div>
</div> </div>
<!-- Kling 类型 -->
<div v-else-if="selectedVideoType === ImageToVideoModels.HAILUO" class="info-content">
<MediaToVideoInfoHaiLuoVideoInfo :task="task" />
</div>
<!-- Kling 类型 --> <!-- Kling 类型 -->
<div v-else-if="selectedVideoType === 'KLING'" class="info-content"> <div v-else-if="selectedVideoType === 'KLING'" class="info-content">
<MediaToVideoInfoKlingVideoInfo :task="task" /> <MediaToVideoInfoKlingVideoInfo :task="task" />

View File

@ -0,0 +1,522 @@
<template>
<n-space vertical :size="20" style="width: 100%">
<ConfigOptionGroup
v-model:value="videoMessage.hailuoFirstLastFrameOptionsObject"
:options="firstLastFrameOptions"
@change="handleConfigChange"
/>
<div style="display: flex; gap: 12px; width: 100%">
<TooltipButton
:tooltip="t('将当前首尾帧视频的基础设置批量应用到所有的分镜中')"
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M3,6V8H21V6H3M3,11V13H21V11H3M3,16V18H21V16H3Z" />
</svg>
</n-icon>
</template>
{{ t('应用设置') }}
</TooltipButton>
<n-button type="primary" size="small" @click="handleFirstLastFrameVideo" style="flex: 1">
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
</svg>
</n-icon>
</template>
{{ t('生成视频') }}
</n-button>
</div>
</n-space>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import ConfigOptionGroup from '@/renderer/src/components/common/ConfigOptionGroup.vue'
import TooltipButton from '@/renderer/src/components/common/TooltipButton.vue'
import {
GetHailuoModelOptions,
HailuoModel,
HailuoResolution,
HailuoDuration,
GetHailuoModelSupportedDurations,
GetHailuoModelSupportedResolutions
} from '@/define/enum/video'
import { t } from '@/i18n'
import { useFile } from '@/renderer/src/hooks/useFile'
const { UploadImageToLaiTool } = useFile()
const message = useMessage()
const props = defineProps({
task: {
type: Object,
required: true
},
videoMessage: {
type: Object,
required: true
}
})
const emit = defineEmits(['update-hailuo-options', 'batch-settings', 'first-last-frame-video'])
//
const validateAndFixFirstLastFrameOptions = (currentData) => {
//
const model = currentData.model || HailuoModel.MINIMAX_HAILUO_02
const resolution = currentData.resolution || HailuoResolution.P768
const duration = currentData.duration || HailuoDuration.SIX
//
const supportedDurations = GetHailuoModelSupportedDurations(
model,
resolution,
'firstLastFrame'
)
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'firstLastFrame',
model,
duration
)
let needsUpdate = false
const updates = {}
//
if (!currentData.model) {
updates.model = HailuoModel.MINIMAX_HAILUO_02
needsUpdate = true
}
//
if (!currentData.resolution) {
updates.resolution = HailuoResolution.P768
needsUpdate = true
}
//
if (!currentData.duration) {
updates.duration = HailuoDuration.SIX
needsUpdate = true
}
//
if (currentData.duration && !supportedDurations.some((d) => d.value === currentData.duration)) {
updates.duration = supportedDurations[0]?.value || HailuoDuration.SIX
needsUpdate = true
}
//
if (
currentData.resolution &&
!supportedResolutions.some((r) => r.value === currentData.resolution)
) {
updates.resolution = supportedResolutions[0]?.value || HailuoResolution.P768
needsUpdate = true
}
//
const finalModel = updates.model || currentData.model || HailuoModel.MINIMAX_HAILUO_02
const finalResolution = updates.resolution || currentData.resolution || HailuoResolution.P768
const finalDuration = updates.duration || currentData.duration || HailuoDuration.SIX
//
const finalSupportedDurations = GetHailuoModelSupportedDurations(
finalModel,
finalResolution,
'firstLastFrame'
)
const finalSupportedResolutions = GetHailuoModelSupportedResolutions(
'firstLastFrame',
finalModel,
finalDuration
)
return {
needsUpdate,
updates,
supportedDurations: finalSupportedDurations,
supportedResolutions: finalSupportedResolutions
}
}
//
const firstLastFrameOptions = computed(() => {
const currentData = props.videoMessage.hailuoFirstLastFrameOptionsObject || {}
const { supportedDurations, supportedResolutions } =
validateAndFixFirstLastFrameOptions(currentData)
return [
{
label: t('模型名称'),
key: 'model',
type: 'select',
width: '200px',
options: GetHailuoModelOptions('firstLastFrame'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px;">模型名称MiniMax-Hailuo-02</div>
<div>首尾帧模式专用的视频生成模型</div>
</div>`
},
{
label: t('首帧图片'),
key: 'first_frame_image',
type: 'image',
fullWidth: true, //
onUpload: handleImageUpload,
placeholder: t('上传或输入图片URL作为视频的起始帧'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px;">将指定图片作为视频的起始帧</div>
<div style="margin-bottom: 12px;">支持公网 URL Base64 编码的 Data URL</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">支持格式</div>
<div> 格式JPGJPEGPNGWebP</div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">图片要求</div>
<div> 体积小于 20MB</div>
<div> 尺寸短边像素大于 300px</div>
<div> 长宽比在 2:5 5:2 之间</div>
</div>
<div style="font-size: 12px; color: #f56c6c; margin-top: 8px;">
<strong> 生成视频尺寸遵循首帧图片</strong>
</div>
</div>`,
required: true,
accept: 'image/jpeg,image/jpg,image/png,image/webp'
},
{
label: t('尾帧图片'),
key: 'last_frame_image',
fullWidth: true, //
type: 'image',
onUpload: handleImageUpload,
placeholder: t('上传或输入图片URL作为视频的结束帧'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px;">将指定图片作为视频的结束帧</div>
<div style="margin-bottom: 12px;">支持公网 URL Base64 编码的 Data URL</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">支持格式</div>
<div> 格式JPGJPEGPNGWebP</div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">图片要求</div>
<div> 体积小于 20MB</div>
<div> 尺寸短边像素大于 300px</div>
<div> 长宽比在 2:5 5:2 之间</div>
</div>
<div style="font-size: 12px; color: #f56c6c; margin-top: 8px;">
<strong> 当首尾帧尺寸不一致时模型将参考首帧对尾帧图片进行裁剪</strong>
</div>
</div>`,
required: true,
accept: 'image/jpeg,image/jpg,image/png,image/webp'
},
{
label: t('提示词'),
key: 'prompt',
type: 'input',
fullWidth: true, //
inputType: 'textarea',
autosize: { minRows: 3, maxRows: 3 },
placeholder: t('描述从首帧到尾帧的过渡过程最大2000字符。支持运镜指令如[推进]、[左摇]等'),
tooltip: `<div style="max-width: 450px;">
<div style="font-weight: bold; margin-bottom: 8px;">视频的文本描述最大 2000 字符</div>
<div style="margin-bottom: 12px;">对于 MiniMax-Hailuo-02 支持使用 [指令] 语法进行运镜控制</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 6px;">支持 15 种运镜指令</div>
<div style="font-size: 12px; line-height: 1.6;">
<div>左右移: [左移], [右移]</div>
<div>左右摇: [左摇], [右摇]</div>
<div>推拉: [推进], [拉远]</div>
<div>升降: [上升], [下降]</div>
<div>上下摇: [上摇], [下摇]</div>
<div>变焦: [变焦推近], [变焦拉远]</div>
<div>其他: [晃动], [跟随], [固定]</div>
</div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">使用规则</div>
<div style="font-size: 12px;">
<div> <strong>组合运镜</strong>同一组 [] 内的多个指令会同时生效 [左摇,上升]建议组合不超过 3 </div>
<div> <strong>顺序运镜</strong>prompt 中前后出现的指令会依次生效 "...[推进], 然后...[拉远]"</div>
<div> <strong>自然语言</strong>也支持通过自然语言描述运镜但使用标准指令能获得更准确的响应</div>
</div>
</div>
</div>`,
maxLength: 2000,
rows: 4
},
{
label: t('视频时长'),
key: 'duration',
type: 'select',
options: supportedDurations,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px;">视频时长默认值为 6</div>
<div style="margin-bottom: 12px;">可用值与模型和分辨率相关</div>
<div style="padding: 8px; border-radius: 6px;">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">1080P</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">MiniMax-Hailuo-02</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6 10</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6</td>
</tr>
</tbody>
</table>
</div>
</div>`,
disabled: false // APItrue
},
{
label: t('视频分辨率'),
key: 'resolution',
type: 'select',
options: supportedResolutions,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px;">视频分辨率首尾帧功能支持 768P1080P</div>
<div style="margin-bottom: 12px;">可用值与模型相关</div>
<div style="padding: 8px; border-radius: 6px;">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">10s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">MiniMax-Hailuo-02</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P (默认), 1080P</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P</td>
</tr>
</tbody>
</table>
</div>
</div>`
},
{
label: t('自动优化提示词'),
key: 'prompt_optimizer',
type: 'switch',
tooltip: `<div style="max-width: 350px;">
<div style="font-weight: bold; margin-bottom: 8px;">是否自动优化 prompt默认为 true</div>
<div style="margin-bottom: 12px;">设为 false 可进行更精确的控制</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">功能说明</div>
<div style="font-size: 12px;">
<div> <strong>启用(true)</strong>系统会自动优化和增强你的prompt描述</div>
<div> <strong>禁用(false)</strong>严格按照原始prompt生成适合精确控制</div>
</div>
</div>
</div>`
}
]
})
//
onMounted(() => {
const currentData = props.videoMessage.hailuoFirstLastFrameOptionsObject || {}
const { needsUpdate, updates } = validateAndFixFirstLastFrameOptions(currentData)
if (needsUpdate) {
const updatedData = { ...currentData, ...updates }
emit('update-hailuo-options', 'firstLastFrame', '', '', updatedData)
//
if (Object.keys(updates).length > 1) {
message.info(t('已自动设置默认值并调整参数以确保兼容性'))
}
}
})
//
async function handleImageUpload(key, imagePath) {
const url = await UploadImageToLaiTool(imagePath, 'video')
if (url) {
const newValue = {
...props.videoMessage.hailuoFirstLastFrameOptionsObject,
[key]: url
}
//
emit('update-hailuo-options', 'firstLastFrame', key, url, newValue)
}
}
//
const handleConfigChange = (key, value, newValue) => {
// console.log('HailuoFirstLastFrameInfo:', { key, value, currentData: newValue })
//
if (key === 'model') {
const newModel = value
let adjustments = {}
let hasChanges = false
//
if (newValue.resolution) {
const supportedResolutions = GetHailuoModelSupportedResolutions('firstLastFrame', newModel)
const supportedResolutionValues = supportedResolutions.map((r) => r.value)
if (!supportedResolutionValues.includes(newValue.resolution)) {
//
const defaultResolution =
supportedResolutions.find((r) => r.value) || supportedResolutions[0]
if (defaultResolution) {
adjustments.resolution = defaultResolution.value
hasChanges = true
}
}
}
//
if (newValue.duration) {
const currentResolution = adjustments.resolution || newValue.resolution
const supportedDurations = GetHailuoModelSupportedDurations(
newModel,
currentResolution,
'firstLastFrame'
)
const supportedDurationValues = supportedDurations.map((d) => d.value)
if (!supportedDurationValues.includes(newValue.duration)) {
//
const defaultDuration = supportedDurations.find((d) => d.value) || supportedDurations[0]
if (defaultDuration) {
adjustments.duration = defaultDuration.value
hasChanges = true
// console.log(` ${defaultDuration.label}`)
}
}
}
//
if (hasChanges) {
const mergedData = { ...newValue, ...adjustments }
emit('update-hailuo-options', 'firstLastFrame', key, value, mergedData)
//
message.info(t('模型变更后已自动调整相关参数以确保兼容性'))
return
}
}
//
if (key === 'resolution') {
let adjustments = {}
let hasChanges = false
//
if (newValue.duration && newValue.model) {
const supportedDurations = GetHailuoModelSupportedDurations(
newValue.model,
value,
'firstLastFrame'
)
const supportedDurationValues = supportedDurations.map((d) => d.value)
if (!supportedDurationValues.includes(newValue.duration)) {
//
const defaultDuration = supportedDurations.find((d) => d.value) || supportedDurations[0]
if (defaultDuration) {
adjustments.duration = defaultDuration.value
hasChanges = true
}
}
}
//
if (hasChanges) {
const mergedData = { ...newValue, ...adjustments }
emit('update-hailuo-options', 'firstLastFrame', key, value, mergedData)
message.info('分辨率变更后已自动调整时长以确保兼容性')
return
}
}
//
if (key === 'duration') {
let adjustments = {}
let hasChanges = false
//
if (newValue.model && newValue.resolution) {
const supportedResolutions = GetHailuoModelSupportedResolutions(
'firstLastFrame',
newValue.model,
value //
)
const supportedResolutionValues = supportedResolutions.map((r) => r.value)
if (!supportedResolutionValues.includes(newValue.resolution)) {
//
const defaultResolution = supportedResolutions.find((r) => r.value) || supportedResolutions[0]
if (defaultResolution) {
adjustments.resolution = defaultResolution.value
hasChanges = true
}
}
}
//
if (hasChanges) {
const mergedData = { ...newValue, ...adjustments }
emit('update-hailuo-options', 'firstLastFrame', key, value, mergedData)
message.info('时长变更后已自动调整分辨率以确保兼容性')
return
}
}
//
emit('update-hailuo-options', 'firstLastFrame', key, value, newValue)
}
//
async function handleBatchSettings() {
emit('batch-settings')
}
//
async function handleFirstLastFrameVideo() {
emit('first-last-frame-video')
}
</script>
<style scoped>
/* 组件样式 */
</style>

View File

@ -0,0 +1,460 @@
<template>
<n-space vertical :size="20" style="width: 100%">
<ConfigOptionGroup
v-model:value="videoMessage.hailuoFirstFrameOptionsObject"
:options="imageToVideoOptions"
@change="handleConfigChange"
/>
<div style="display: flex; gap: 12px; width: 100%">
<TooltipButton
:tooltip="t('将当前图生视频的基础设置批量应用到所有的分镜中')"
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M3,6V8H21V6H3M3,11V13H21V11H3M3,16V18H21V16H3Z" />
</svg>
</n-icon>
</template>
{{ t('应用设置') }}
</TooltipButton>
<n-button type="primary" size="small" @click="handleImageToVideo" style="flex: 1">
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
</svg>
</n-icon>
</template>
{{ t('生成视频') }}
</n-button>
</div>
</n-space>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import ConfigOptionGroup from '@/renderer/src/components/common/ConfigOptionGroup.vue'
import TooltipButton from '@/renderer/src/components/common/TooltipButton.vue'
import {
GetHailuoModelOptions,
HailuoModel,
HailuoResolution,
HailuoDuration,
GetHailuoModelSupportedDurations,
GetHailuoModelSupportedResolutions
} from '@/define/enum/video'
import { t } from '@/i18n'
import { useFile } from '@/renderer/src/hooks/useFile'
const { UploadImageToLaiTool } = useFile()
const props = defineProps({
task: {
type: Object,
required: true
},
videoMessage: {
type: Object,
required: true
}
})
const emit = defineEmits(['update-hailuo-options', 'batch-settings', 'image-to-video'])
//
const validateAndFixImageToVideoOptions = (currentData) => {
//
const supportedDurations = GetHailuoModelSupportedDurations(
'imageToVideo',
currentData.model || HailuoModel.MINIMAX_HAILUO_02,
currentData.resolution
)
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'imageToVideo',
currentData.model || HailuoModel.MINIMAX_HAILUO_02,
currentData.duration
)
let needsUpdate = false
const updates = {}
//
if (currentData.duration && !supportedDurations.some((d) => d.value === currentData.duration)) {
updates.duration = supportedDurations[0]?.value || HailuoDuration.SIX
needsUpdate = true
}
//
if (
currentData.resolution &&
!supportedResolutions.some((r) => r.value === currentData.resolution)
) {
updates.resolution = supportedResolutions[0]?.value || HailuoResolution.P768
needsUpdate = true
}
return {
needsUpdate,
updates,
supportedDurations,
supportedResolutions
}
}
//
const imageToVideoOptions = computed(() => {
const currentData = props.videoMessage.hailuoFirstFrameOptionsObject || {}
const { supportedDurations, supportedResolutions } =
validateAndFixImageToVideoOptions(currentData)
return [
{
label: t('模型名称'),
key: 'model',
type: 'select',
width: '200px',
options: GetHailuoModelOptions('imageToVideo'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px; ">海螺图生视频模型</div>
<div style="margin-bottom: 12px;">选择图像转视频生成模型不同模型支持的功能和约束条件不同</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">可用模型</div>
<div> <strong>MiniMax-Hailuo-02</strong>支持多种分辨率和时长高质量生成</div>
<div> <strong>I2V-01-Director</strong>导演级图生视频支持运镜控制</div>
<div> <strong>I2V-01-live</strong>实时风格图生视频转换</div>
<div> <strong>I2V-01</strong>基础图生视频模型</div>
</div>
</div>`
},
{
label: t('首帧图片'),
key: 'first_frame_image',
type: 'image',
fullWidth: true, //
onUpload: handleImageUpload,
placeholder: t('上传或输入图片URL作为视频的起始帧'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px; ">首帧图片设置</div>
<div style="margin-bottom: 12px;">指定图片作为视频的起始帧支持公网URL或Base64编码</div>
<div style=" padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">必填条件</div>
<div> I2V-01, I2V-01-Director, I2V-01-live 模型时</div>
<div> MiniMax-Hailuo-02 且分辨率为 512P </div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">图片要求</div>
<div> <strong>格式</strong>JPG, JPEG, PNG, WebP</div>
<div> <strong>体积</strong>小于 20MB</div>
<div> <strong>尺寸</strong>短边像素大于 300px</div>
<div> <strong>比例</strong>长宽比在 2:5 5:2 之间</div>
</div>
</div>`,
accept: 'image/jpeg,image/jpg,image/png,image/webp'
},
{
label: t('提示词'),
key: 'prompt',
type: 'input',
inputType: 'textarea',
fullWidth: true, //
autosize: { minRows: 3, maxRows: 3 },
placeholder: t(
'描述你想要基于图片生成的视频内容最大2000字符。支持运镜指令如[推进]、[左摇]等'
),
tooltip: `<div style="max-width: 450px;">
<div style="font-weight: bold; margin-bottom: 8px; ">视频提示词</div>
<div style="margin-bottom: 12px;">基于首帧图片的视频内容描述最大2000字符MiniMax-Hailuo-02和I2V-01-Director支持运镜控制</div>
<div style=" padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 6px;">15种运镜指令</div>
<div style="font-size: 12px; line-height: 1.6;">
<div>左右移: [左移], [右移]</div>
<div>左右摇: [左摇], [右摇]</div>
<div>推拉: [推进], [拉远]</div>
<div>升降: [上升], [下降]</div>
<div>上下摇: [上摇], [下摇]</div>
<div>变焦: [变焦推近], [变焦拉远]</div>
<div>其他: [晃动], [跟随], [固定]</div>
</div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">使用规则</div>
<div style="font-size: 12px;">
<div> <strong>组合运镜</strong>同一组[]内多个指令同时生效[左摇,上升]建议不超过3个</div>
<div> <strong>顺序运镜</strong>prompt中前后出现的指令依次生效"...[推进],然后...[拉远]"</div>
<div> <strong>自然语言</strong>也可用自然语言描述但标准指令更准确</div>
</div>
</div>
<div style="font-size: 12px;">
支持中文描述 + 运镜指令的混合使用方式
</div>
</div>`,
maxLength: 2000,
rows: 4
},
{
label: t('视频时长'),
key: 'duration',
type: 'select',
options: supportedDurations,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px; ">视频时长设置</div>
<div style="margin-bottom: 12px;">生成视频的时长默认值为6可用值与模型和分辨率相关</div>
<div style=" padding: 8px; border-radius: 6px;">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<tr>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">512P</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">1080P</th>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;"><strong>MiniMax-Hailuo-02</strong></td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6 10</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6 10</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6</td>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;"><strong>其他模型</strong></td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6</td>
</tr>
</table>
</div>
<div style="font-size: 12px; margin-top: 8px;">
系统会根据所选模型和分辨率自动调整可选时长
</div>
</div>`
},
{
label: t('视频分辨率'),
key: 'resolution',
type: 'select',
options: supportedResolutions,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px; ">视频分辨率设置</div>
<div style="margin-bottom: 12px;">生成视频的分辨率可用值与模型和时长相关</div>
<div style=" padding: 8px; border-radius: 6px; ">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<tr style="">
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">10s</th>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;"><strong>MiniMax-Hailuo-02</strong></td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">512P, 768P(默认), 1080P</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">512P, 768P(默认)</td>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;"><strong>其他模型</strong></td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">720P(默认)</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">不支持</td>
</tr>
</table>
</div>
<div style="font-size: 12px; margin-top: 8px;">
系统会根据所选模型和时长自动调整可选分辨率
</div>
</div>`
},
{
label: t('自动优化提示词'),
key: 'prompt_optimizer',
type: 'switch',
tooltip: `<div style="max-width: 350px;">
<div style="font-weight: bold; margin-bottom: 8px; ">提示词自动优化</div>
<div style="margin-bottom: 12px;">是否自动优化prompt以获得更好的视频生成效果</div>
<div style=" padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">功能说明</div>
<div style="font-size: 12px;">
<div> <strong>启用(true)</strong>系统会自动优化和增强你的prompt描述</div>
<div> <strong>禁用(false)</strong>严格按照原始prompt生成适合精确控制</div>
</div>
</div>
</div>`
},
{
label: t('快速预处理'),
key: 'fast_pretreatment',
type: 'switch',
tooltip: `<div style="max-width: 350px;">
<div style="font-weight: bold; margin-bottom: 8px;">快速预处理模式</div>
<div style="margin-bottom: 12px;">是否缩短prompt_optimizer的优化耗时提升生成速度</div>
<div style=" padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">模型限制</div>
<div style="font-size: 12px; ">
仅对 <strong>MiniMax-Hailuo-02</strong> 模型生效其他模型忽略此设置
</div>
</div>
</div>`,
show: (data) => data.model === HailuoModel.MINIMAX_HAILUO_02
}
]
})
//
onMounted(() => {
const currentData = props.videoMessage.hailuoFirstFrameOptionsObject || {}
const { needsUpdate, updates } = validateAndFixImageToVideoOptions(currentData)
if (needsUpdate) {
const updatedData = { ...currentData, ...updates }
emit('update-hailuo-options', 'imageToVideo', '', '', updatedData)
}
})
//
async function handleImageUpload(key, imagePath) {
const url = await UploadImageToLaiTool(imagePath, 'video')
if (url) {
const newValue = {
...props.videoMessage.hailuoFirstFrameOptionsObject,
[key]: url
}
//
emit('update-hailuo-options', 'imageToVideo', key, url, newValue)
}
}
//
const handleConfigChange = (key, value, newValue) => {
//
if (key === 'model') {
const updatedValue = { ...newValue }
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'imageToVideo',
value,
updatedValue.duration
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
)
if (!currentResolutionSupported) {
//
if (value === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
}
}
//
const supportedDurations = GetHailuoModelSupportedDurations(
value,
updatedValue.resolution,
'imageToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
}
// MiniMax-Hailuo-02
if (value !== HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.fast_pretreatment = false
}
emit('update-hailuo-options', 'firstFrameOnly', key, value, updatedValue)
return
}
//
if (key === 'resolution') {
const updatedValue = { ...newValue }
//
const supportedDurations = GetHailuoModelSupportedDurations(
updatedValue.model,
value,
'imageToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
}
emit('update-hailuo-options', 'firstFrameOnly', key, value, updatedValue)
return
}
//
if (key === 'duration') {
const updatedValue = { ...newValue }
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'imageToVideo',
updatedValue.model,
value
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
)
if (!currentResolutionSupported) {
//
if (updatedValue.model === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
}
}
emit('update-hailuo-options', 'firstFrameOnly', key, value, updatedValue)
return
}
//
emit('update-hailuo-options', 'firstFrameOnly', key, value, newValue)
console.log('Hailuo image-to-video options changed:', key, value, newValue)
}
//
async function handleBatchSettings() {
emit('batch-settings')
}
//
async function handleImageToVideo() {
emit('image-to-video')
}
</script>
<style scoped>
/* 组件样式 */
</style>

View File

@ -0,0 +1,419 @@
<template>
<n-space vertical :size="20" style="width: 100%">
<ConfigOptionGroup
v-model:value="videoMessage.hailuoTextToVideoOptionsObject"
:options="textToVideoOptions"
@change="handleConfigChange"
/>
<div style="display: flex; gap: 12px; width: 100%">
<TooltipButton
:tooltip="t('将当前文生视频的基础设置批量应用到所有的分镜中')"
type="default"
size="small"
@click="handleBatchSettings"
style="width: 100px"
>
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M3,6V8H21V6H3M3,11V13H21V11H3M3,16V18H21V16H3Z" />
</svg>
</n-icon>
</template>
{{ t('应用设置') }}
</TooltipButton>
<n-button type="primary" size="small" @click="handleTextToVideo" style="flex: 1">
<template #icon>
<n-icon>
<svg viewBox="0 0 24 24">
<path fill="currentColor" d="M8,5.14V19.14L19,12.14L8,5.14Z" />
</svg>
</n-icon>
</template>
{{ t('生成视频') }}
</n-button>
</div>
</n-space>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import ConfigOptionGroup from '@/renderer/src/components/common/ConfigOptionGroup.vue'
import TooltipButton from '@/renderer/src/components/common/TooltipButton.vue'
import {
GetHailuoModelOptions,
GetHailuoModelSupportedDurations,
GetHailuoModelSupportedResolutions,
HailuoModel,
HailuoResolution,
HailuoDuration
} from '@/define/enum/video'
import { t } from '@/i18n'
const props = defineProps({
task: {
type: Object,
required: true
},
videoMessage: {
type: Object,
required: true
}
})
const emit = defineEmits(['update-hailuo-options', 'batch-settings', 'text-to-video'])
//
const validateAndFixTextToVideoOptions = (currentData) => {
//
const supportedDurations = GetHailuoModelSupportedDurations(
'textToVideo',
currentData.model || HailuoModel.MINIMAX_HAILUO_02,
currentData.resolution
)
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'textToVideo',
currentData.model || HailuoModel.MINIMAX_HAILUO_02,
currentData.duration
)
let needsUpdate = false
const updates = {}
//
if (currentData.duration && !supportedDurations.some((d) => d.value === currentData.duration)) {
updates.duration = supportedDurations[0]?.value || HailuoDuration.SIX
needsUpdate = true
}
//
if (
currentData.resolution &&
!supportedResolutions.some((r) => r.value === currentData.resolution)
) {
updates.resolution = supportedResolutions[0]?.value || HailuoResolution.P768
needsUpdate = true
}
return {
needsUpdate,
updates,
supportedDurations,
supportedResolutions
}
}
//
const textToVideoOptions = computed(() => {
const currentData = props.videoMessage.hailuoTextToVideoOptionsObject || {}
const { supportedDurations, supportedResolutions } = validateAndFixTextToVideoOptions(currentData)
return [
{
label: t('模型名称'),
key: 'model',
type: 'select',
width: '200px',
options: GetHailuoModelOptions('textToVideo'),
tooltip: `<div style="max-width: 400px;">
<div style="font-weight: bold; margin-bottom: 8px;">模型选择说明</div>
<div style="margin-bottom: 12px;">选择图像转视频生成模型不同模型支持的功能和约束条件不同</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">可用模型</div>
<div> <strong>MiniMax-Hailuo-02</strong>最新模型支持运镜指令</div>
<div> <strong>T2V-01-Director</strong>支持运镜控制的导演版模型</div>
<div> <strong>T2V-01</strong>基础文生视频模型</div>
</div>
<div style="font-size: 12px; margin-top: 8px;">
运镜指令功能仅对 MiniMax-Hailuo-02 Director 系列模型生效
</div>
</div>`
},
{
label: t('提示词'),
key: 'prompt',
type: 'input',
inputType: 'textarea',
autosize: { minRows: 3, maxRows: 3 },
fullWidth: true,
placeholder: t('描述你想要生成的视频内容最大2000字符。支持运镜指令如[推进]、[左摇]等'),
tooltip: `<div style="max-width: 450px;">
<div style="font-weight: bold; margin-bottom: 8px;">视频文本描述最大2000字符</div>
<div style="margin-bottom: 12px;">对于 MiniMax-Hailuo-02 *-Director 系列模型支持运镜指令</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 6px;">支持 15 种运镜指令</div>
<div style="font-size: 12px; line-height: 1.6;">
<div>左右移: [左移], [右移]</div>
<div>左右摇: [左摇], [右摇]</div>
<div>推拉: [推进], [拉远]</div>
<div>升降: [上升], [下降]</div>
<div>上下摇: [上摇], [下摇]</div>
<div>变焦: [变焦推近], [变焦拉远]</div>
<div>其他: [晃动], [跟随], [固定]</div>
</div>
</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">使用规则</div>
<div style="font-size: 12px;">
<div> <strong>组合运镜</strong>[左摇,上升] 同时生效建议不超过3个</div>
<div> <strong>顺序运镜</strong>前后指令依次生效"[推进],然后[拉远]"</div>
<div> <strong>自然语言</strong>也支持描述运镜但标准指令更准确</div>
</div>
</div>
</div>`,
maxLength: 2000,
rows: 4
},
{
label: t('视频时长'),
key: 'duration',
type: 'select',
options: supportedDurations,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px;">视频时长限制</div>
<div style="margin-bottom: 12px;">生成视频的时长默认值为6可用值与模型和分辨率相关</div>
<div style="padding: 8px; border-radius: 6px;">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">1080P</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">MiniMax-Hailuo-02</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s 10s</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s</td>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">其他模型</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">不支持</td>
</tr>
</tbody>
</table>
</div>
</div>`
},
{
label: t('视频分辨率'),
key: 'resolution',
type: 'select',
options: supportedResolutions,
tooltip: `<div style="max-width: 380px;">
<div style="font-weight: bold; margin-bottom: 8px;">分辨率支持情况</div>
<div style="margin-bottom: 12px;">生成视频的分辨率可用值与模型和时长相关</div>
<div style="padding: 8px; border-radius: 6px;">
<div style="font-weight: bold; margin-bottom: 6px;">模型支持矩阵</div>
<table style="width: 100%; border-collapse: collapse; font-size: 12px;">
<thead>
<tr>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: left;">模型</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">6s</th>
<th style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">10s</th>
</tr>
</thead>
<tbody>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">MiniMax-Hailuo-02</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P(默认), 1080P</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">768P(默认)</td>
</tr>
<tr>
<td style="border: 1px solid #d1d5db; padding: 4px;">其他模型</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">720P(默认)</td>
<td style="border: 1px solid #d1d5db; padding: 4px; text-align: center;">不支持</td>
</tr>
</tbody>
</table>
</div>
<div style="font-size: 12px; margin-top: 8px;">
系统会根据所选模型和时长自动调整可选分辨率
</div>
</div>`
},
{
label: t('自动优化提示词'),
key: 'prompt_optimizer',
type: 'switch',
tooltip: `<div style="max-width: 350px;">
<div style="font-weight: bold; margin-bottom: 8px;">提示词自动优化</div>
<div style="margin-bottom: 12px;">是否自动优化prompt以获得更好的视频生成效果</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">功能说明</div>
<div style="font-size: 12px;">
<div> <strong>开启(默认)</strong>系统自动优化prompt以获得更好效果</div>
<div> <strong>关闭</strong>使用原始prompt进行更精确的控制</div>
</div>
</div>
<div style="font-size: 12px; margin-top: 8px;">
建议保持开启以获得最佳生成效果
</div>
</div>`
},
{
label: t('快速预处理'),
key: 'fast_pretreatment',
type: 'switch',
tooltip: `<div style="max-width: 350px;">
<div style="font-weight: bold; margin-bottom: 8px;">快速预处理模式</div>
<div style="margin-bottom: 12px;">是否缩短prompt_optimizer的优化耗时提升生成速度</div>
<div style="padding: 8px; border-radius: 6px; margin-bottom: 8px;">
<div style="font-weight: bold; margin-bottom: 4px;">模型限制</div>
<div style="font-size: 12px;">
仅对 <strong>MiniMax-Hailuo-02</strong> 模型生效其他模型忽略此设置
</div>
</div>
</div>`,
show: (data) => data.model === HailuoModel.MINIMAX_HAILUO_02
}
]
})
//
onMounted(() => {
const currentData = props.videoMessage.hailuoTextToVideoOptionsObject || {}
const { needsUpdate, updates } = validateAndFixTextToVideoOptions(currentData)
if (needsUpdate) {
const updatedData = { ...currentData, ...updates }
emit('update-hailuo-options', 'textToVideo', '', '', updatedData)
}
})
//
const handleConfigChange = (key, value, newValue) => {
//
if (key === 'model') {
const updatedValue = { ...newValue }
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'textToVideo',
value,
updatedValue.duration
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
)
if (!currentResolutionSupported) {
//
if (value === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
}
}
//
const supportedDurations = GetHailuoModelSupportedDurations(
value,
updatedValue.resolution,
'textToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
}
// MiniMax-Hailuo-02
if (value !== HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.fast_pretreatment = false
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
if (key === 'resolution') {
const updatedValue = { ...newValue }
//
const supportedDurations = GetHailuoModelSupportedDurations(
updatedValue.model,
value,
'textToVideo'
)
const currentDurationSupported = supportedDurations.some(
(option) => option.value === updatedValue.duration
)
if (!currentDurationSupported) {
// 6
updatedValue.duration = HailuoDuration.SIX
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
if (key === 'duration') {
const updatedValue = { ...newValue }
//
const supportedResolutions = GetHailuoModelSupportedResolutions(
'textToVideo',
updatedValue.model,
value
)
const currentResolutionSupported = supportedResolutions.some(
(option) => option.value === updatedValue.resolution
)
if (!currentResolutionSupported) {
//
if (updatedValue.model === HailuoModel.MINIMAX_HAILUO_02) {
updatedValue.resolution = HailuoResolution.P768 // MiniMax-Hailuo-02 768P
} else {
updatedValue.resolution = HailuoResolution.P720 // 720P
}
}
emit('update-hailuo-options', 'textToVideo', key, value, updatedValue)
return
}
//
emit('update-hailuo-options', 'textToVideo', key, value, newValue)
console.log('Hailuo text-to-video options changed:', key, value, newValue)
}
//
async function handleBatchSettings() {
emit('batch-settings')
}
//
async function handleTextToVideo() {
emit('text-to-video')
}
</script>
<style scoped>
/* 组件样式 */
</style>

View File

@ -0,0 +1,387 @@
<template>
<div class="hailuo-video-container">
<n-tabs v-model:value="activeTab" type="segment" size="small">
<!-- 首尾帧视频 Tab -->
<n-tab-pane name="first-last-frame" :tab="t('首尾帧视频')">
<HailuoFirstLastFrameInfo
:task="props.task"
:video-message="videoMessage"
@update-hailuo-options="handleHailuoOptionsUpdate"
@batch-settings="handleBatchSettings"
@first-last-frame-video="handleFirstLastFrameVideo"
/>
</n-tab-pane>
<!-- 图生视频 Tab -->
<n-tab-pane name="image-to-video" :tab="t('图生视频')">
<HailuoImageToVideoInfo
:task="props.task"
:video-message="videoMessage"
@update-hailuo-options="handleHailuoOptionsUpdate"
@batch-settings="handleBatchSettings"
@image-to-video="handleImageToVideo"
/>
</n-tab-pane>
<!-- 文生视频 Tab -->
<n-tab-pane name="text-to-video" :tab="t('文生视频')">
<HailuoTextToVideoInfo
:task="props.task"
:video-message="videoMessage"
@update-hailuo-options="handleHailuoOptionsUpdate"
@batch-settings="handleBatchSettings"
@text-to-video="handleTextToVideo"
/>
</n-tab-pane>
</n-tabs>
</div>
</template>
<script setup>
import { ref, computed, h } from 'vue'
import { useMessage, useDialog } from 'naive-ui'
import HailuoTextToVideoInfo from './HailuoTextToVideoInfo.vue'
import HailuoImageToVideoInfo from './HailuoImageToVideoInfo.vue'
import HailuoFirstLastFrameInfo from './HailuoFirstLastFrameInfo.vue'
import { t } from '@/i18n'
import { ValidateJsonAndParse } from '@/define/Tools/validate'
import { HailuoModel, HailuoResolution, HailuoDuration } from '@/define/enum/video'
import { useSoftwareStore, useBookStore } from '@/renderer/src/stores'
import { BookBackTaskType, TaskExecuteType } from '@/define/enum/bookEnum'
import { DEFINE_STRING } from '@/define/ipcDefineString'
import { AddOneTask } from '@/renderer/src/common/task'
const message = useMessage()
const dialog = useDialog()
const softwareStore = useSoftwareStore()
const bookStore = useBookStore()
const props = defineProps({
task: {
type: Object,
required: true
}
})
//
const activeTab = ref('first-last-frame')
//
const videoMessage = computed(() => {
console.log('MediaToVideoInfoHaiLuoVideoInfo props.task', props.task, props.task?.videoMessage)
let videoMessage = props.task?.videoMessage || {}
//
let hailuoTextToVideoOptionsString = videoMessage.hailuoTextToVideoOptions || '{}'
let hailuoTextToVideoOptions = ValidateJsonAndParse(hailuoTextToVideoOptionsString)
//
let hailuoFirstFrameOptionsString = videoMessage.hailuoFirstFrameOnlyOptions || '{}'
let hailuoFirstFrameOptions = ValidateJsonAndParse(hailuoFirstFrameOptionsString)
//
let hailuoFirstLastFrameOptionsString = videoMessage.hailuoFirstLastFrameOptions || '{}'
let hailuoFirstLastFrameOptions = ValidateJsonAndParse(hailuoFirstLastFrameOptionsString)
//
const cleanTextToVideoOptions = {
model: hailuoTextToVideoOptions.model || HailuoModel.MINIMAX_HAILUO_02,
prompt: hailuoTextToVideoOptions.prompt || '',
duration: hailuoTextToVideoOptions.duration ?? HailuoDuration.SIX,
resolution: hailuoTextToVideoOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoTextToVideoOptions.prompt_optimizer ?? true,
fast_pretreatment: hailuoTextToVideoOptions.fast_pretreatment ?? false
}
//
const cleanFirstFrameOptions = {
model: hailuoFirstFrameOptions.model || HailuoModel.MINIMAX_HAILUO_02,
first_frame_image: videoMessage.imageUrl || hailuoFirstFrameOptions.first_frame_image || '',
prompt: hailuoFirstFrameOptions.prompt || '',
duration: hailuoFirstFrameOptions.duration ?? HailuoDuration.SIX,
resolution: hailuoFirstFrameOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoFirstFrameOptions.prompt_optimizer ?? true,
fast_pretreatment: hailuoFirstFrameOptions.fast_pretreatment ?? false
}
//
const cleanFirstLastFrameOptions = {
model: hailuoFirstLastFrameOptions.model || HailuoModel.MINIMAX_HAILUO_02,
first_frame_image: videoMessage.imageUrl || hailuoFirstLastFrameOptions.first_frame_image || '',
last_frame_image: hailuoFirstLastFrameOptions.last_frame_image || '',
prompt: hailuoFirstLastFrameOptions.prompt || '',
duration: HailuoDuration.SIX, // 6API
resolution: hailuoFirstLastFrameOptions.resolution || HailuoResolution.P768,
prompt_optimizer: hailuoFirstLastFrameOptions.prompt_optimizer ?? true,
fast_pretreatment: hailuoFirstLastFrameOptions.fast_pretreatment ?? false
}
videoMessage.hailuoTextToVideoOptionsObject = cleanTextToVideoOptions
videoMessage.hailuoFirstFrameOptionsObject = cleanFirstFrameOptions
videoMessage.hailuoFirstLastFrameOptionsObject = cleanFirstLastFrameOptions
console.log('MediaToVideoInfoHaiLuoVideoInfo videoMessage', videoMessage)
return videoMessage
})
//
async function handleHailuoOptionsUpdate(optionsType, key, value, newOptions) {
//
let updateData = {
imageUrl: newOptions.first_frame_image || videoMessage.value.imageUrl
}
//
switch (optionsType) {
case 'textToVideo':
updateData.hailuoTextToVideoOptions = JSON.stringify(newOptions)
break
case 'firstFrameOnly':
updateData.hailuoFirstFrameOnlyOptions = JSON.stringify(newOptions)
break
case 'firstLastFrame':
updateData.hailuoFirstLastFrameOptions = JSON.stringify(newOptions)
break
}
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(props.task.id, updateData)
if (res.code !== 1) {
message.error(
t('保存失败:{error}', {
error: res.message
}) + `, Key: ${key}`
)
return
}
//
if (!props.task.videoMessage) {
props.task.videoMessage = {}
}
Object.assign(props.task.videoMessage, updateData)
//
switch (optionsType) {
case 'textToVideo':
videoMessage.value.hailuoTextToVideoOptionsObject = { ...newOptions }
break
case 'firstFrameOnly':
videoMessage.value.hailuoFirstFrameOptionsObject = { ...newOptions }
break
case 'firstLastFrame':
videoMessage.value.hailuoFirstLastFrameOptionsObject = { ...newOptions }
break
}
}
//
async function handleBatchSettings() {
const currentOptions = getCurrentTabOptions()
let da = dialog.warning({
title: t('操作确认'),
content: () =>
h(
'div',
{
style: {
whiteSpace: 'pre-line'
}
},
{
default: () =>
t(
'是否将当前分镜的设置批量应用到其余所有分镜?\n\n同步的设置模型名称、分辨率、时长、提示词优化等基础设置\n\n批量应用后其余分镜的上述基础设置会被替换为当前分镜的数据是否继续'
)
}
),
positiveText: t('确认'),
negativeText: t('取消'),
closable: true,
onPositiveClick: async () => {
da.destroy()
try {
softwareStore.spin.spinning = true
softwareStore.spin.tip = t('正在批量应用当前设置...')
//
for (let i = 0; i < bookStore.selectBookTaskDetail.length; i++) {
const element = bookStore.selectBookTaskDetail[i]
let updateObject = {}
let elementVideoMessage = element?.videoMessage || {}
// tab
switch (activeTab.value) {
case 'text-to-video':
let textOptions = ValidateJsonAndParse(
elementVideoMessage.hailuoTextToVideoOptions || '{}'
)
textOptions.model = currentOptions.model
textOptions.duration = currentOptions.duration
textOptions.resolution = currentOptions.resolution
textOptions.prompt_optimizer = currentOptions.prompt_optimizer
textOptions.fast_pretreatment = currentOptions.fast_pretreatment
updateObject.hailuoTextToVideoOptions = JSON.stringify(textOptions)
elementVideoMessage.hailuoTextToVideoOptions = updateObject.hailuoTextToVideoOptions
break
case 'image-to-video':
let frameOptions = ValidateJsonAndParse(
elementVideoMessage.hailuoFirstFrameOnlyOptions || '{}'
)
frameOptions.model = currentOptions.model
frameOptions.duration = currentOptions.duration
frameOptions.resolution = currentOptions.resolution
frameOptions.prompt_optimizer = currentOptions.prompt_optimizer
frameOptions.fast_pretreatment = currentOptions.fast_pretreatment
updateObject.hailuoFirstFrameOnlyOptions = JSON.stringify(frameOptions)
elementVideoMessage.hailuoFirstFrameOnlyOptions =
updateObject.hailuoFirstFrameOnlyOptions
break
case 'first-last-frame':
let lastFrameOptions = ValidateJsonAndParse(
elementVideoMessage.hailuoFirstLastFrameOptions || '{}'
)
lastFrameOptions.model = currentOptions.model
lastFrameOptions.duration = currentOptions.duration
lastFrameOptions.resolution = currentOptions.resolution
lastFrameOptions.prompt_optimizer = currentOptions.prompt_optimizer
lastFrameOptions.fast_pretreatment = currentOptions.fast_pretreatment
updateObject.hailuoFirstLastFrameOptions = JSON.stringify(lastFrameOptions)
elementVideoMessage.hailuoFirstLastFrameOptions =
updateObject.hailuoFirstLastFrameOptions
break
}
//
let res = await window.book.video.UpdateBookTaskDetailVideoMessage(
element.id,
updateObject
)
if (res.code !== 1) {
message.error(
t('批量应用当前设置失败,{error}', {
error: res.message
})
)
return
}
}
message.success(t('批量应用当前设置成功!'))
} catch (error) {
message.error(
t('批量应用当前设置失败,{error}', {
error: error.message
})
)
} finally {
softwareStore.spin.spinning = false
}
},
onNegativeClick: () => {
da.destroy()
message.info(t('取消操作'))
}
})
}
//
function getCurrentTabOptions() {
switch (activeTab.value) {
case 'text-to-video':
return videoMessage.value.hailuoTextToVideoOptionsObject
case 'image-to-video':
return videoMessage.value.hailuoFirstFrameOptionsObject
case 'first-last-frame':
return videoMessage.value.hailuoFirstLastFrameOptionsObject
default:
return {}
}
}
//
async function handleTextToVideo() {
const options = videoMessage.value.hailuoTextToVideoOptionsObject
if (!options.prompt && !options.model) {
message.error(t('请输入提示词和选择模型'))
return
}
let res = await AddOneTask({
bookId: props.task.bookId,
type: BookBackTaskType.HAILUO_TEXT_TO_VIDEO,
executeType: TaskExecuteType.AUTO,
bookTaskId: props.task.bookTaskId,
bookTaskDetailId: props.task.id,
messageName: DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN
})
if (res.code != 1) {
message.error(res.message)
return
}
message.success(res.message)
}
//
async function handleImageToVideo() {
const options = videoMessage.value.hailuoFirstFrameOptionsObject
if (!options.first_frame_image) {
message.error(t('请上传首帧图片'))
return
}
let res = await AddOneTask({
bookId: props.task.bookId,
type: BookBackTaskType.HAILUO_IMAGE_TO_VIDEO,
executeType: TaskExecuteType.AUTO,
bookTaskId: props.task.bookTaskId,
bookTaskDetailId: props.task.id,
messageName: DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN
})
if (res.code != 1) {
message.error(res.message)
return
}
message.success(res.message)
}
//
async function handleFirstLastFrameVideo() {
const options = videoMessage.value.hailuoFirstLastFrameOptionsObject
if (!options.first_frame_image || !options.last_frame_image) {
message.error(t('请上传首帧和尾帧图片'))
return
}
let res = await AddOneTask({
bookId: props.task.bookId,
type: BookBackTaskType.HAILUO_FIRST_LAST_FRAME,
executeType: TaskExecuteType.AUTO,
bookTaskId: props.task.bookTaskId,
bookTaskDetailId: props.task.id,
messageName: DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN
})
if (res.code != 1) {
message.error(res.message)
return
}
message.success(res.message)
}
</script>
<style scoped>
.hailuo-video-container {
width: 100%;
}
</style>

View File

@ -0,0 +1,185 @@
# 海螺视频合成组件
## 📋 概述
基于海螺AI的视频生成组件支持三种不同的视频生成模式
- **文生视频**:纯文本描述生成视频
- **图生视频**:基于首帧图片生成视频
- **首尾帧视频**:指定首帧和尾帧生成过渡视频
## 🏗️ 组件架构
```
MediaToVideoInfoHaiLuoVideoInfo.vue (主容器)
├── HailuoTextToVideoInfo.vue (文生视频)
├── HailuoImageToVideoInfo.vue (图生视频)
└── HailuoFirstLastFrameInfo.vue (首尾帧视频)
```
## 📊 数据结构分析
### VideoMessage 数据结构
```typescript
type VideoMessage = {
// ... 其他字段
hailuoTextToVideoOptions?: string // 文生视频配置JSON
hailuoFirstFrameOnlyOptions?: string // 图生视频配置JSON
hailuoFirstLastFrameOptions?: string // 首尾帧配置JSON
imageUrl?: string // 图片URL兼容字段
}
```
### 三种配置类型
#### 1. 文生视频配置 (HailuoTextToVideoOptions)
```typescript
interface HailuoTextToVideoOptions extends HailuoBaseOptions {
fast_pretreatment?: boolean // 快速预处理
// 可选的首帧图片
}
```
#### 2. 图生视频配置 (HailuoFirstFrameOnlyOptions)
```typescript
interface HailuoFirstFrameOnlyOptions extends HailuoBaseOptions {
first_frame_image: string // 必填的首帧图片
fast_pretreatment?: boolean // 快速预处理
}
```
#### 3. 首尾帧配置 (HailuoFirstLastFrameOptions)
```typescript
interface HailuoFirstLastFrameOptions extends HailuoFirstFrameOnlyOptions {
last_frame_image: string // 必填的尾帧图片
duration: HailuoDuration // 根据API限制的固定时长
}
```
### 基础配置 (HailuoBaseOptions)
```typescript
interface HailuoBaseOptions {
model: HailuoModel // 模型名称
prompt?: string // 提示词 (最大2000字符)
prompt_optimizer?: boolean // 自动优化提示词
duration?: HailuoDuration // 视频时长
resolution?: HailuoResolution // 视频分辨率
}
```
## 🎯 核心功能
### 1. 智能配置验证
- 根据模型类型自动验证首帧图片是否必填
- 动态调整支持的分辨率和时长选项
- 运镜指令语法支持检查
### 2. 响应式数据绑定
- 三种配置类型独立存储和管理
- 实时保存到数据库
- 支持批量应用设置到所有分镜
### 3. 用户交互优化
- Tab切换不丢失数据
- 智能提示和错误处理
- 配置项动态显示/隐藏
## 🔧 使用方法
### 在父组件中使用
```vue
<template>
<MediaToVideoInfoHaiLuoVideoInfo :task="currentTask" />
</template>
<script setup>
import MediaToVideoInfoHaiLuoVideoInfo from './MediaToVideoInfoHaiLuo/MediaToVideoInfoHaiLuoVideoInfo.vue'
const currentTask = ref({
id: 'task-id',
bookId: 'book-id',
videoMessage: {
hailuoTextToVideoOptions: '{}',
hailuoFirstFrameOnlyOptions: '{}',
hailuoFirstLastFrameOptions: '{}',
imageUrl: ''
}
})
</script>
```
### 数据更新流程
1. 用户在UI中修改配置
2. 触发 `handleConfigChange` 事件
3. 调用 `handleHailuoOptionsUpdate` 更新数据库
4. 同步更新本地数据状态
5. 重新计算响应式数据
## 🎨 配置选项详解
### 模型选择 (HailuoModel)
- `MiniMax-Hailuo-02`: 支持运镜指令和快速预处理
- `I2V-01-Director`: 支持运镜指令,必须提供首帧
- `I2V-01-live`: 必须提供首帧
- `I2V-01`: 必须提供首帧
### 运镜指令支持
支持15种运镜指令语法
- 左右移: `[左移]`, `[右移]`
- 左右摇: `[左摇]`, `[右摇]`
- 推拉: `[推进]`, `[拉远]`
- 升降: `[上升]`, `[下降]`
- 上下摇: `[上摇]`, `[下摇]`
- 变焦: `[变焦推近]`, `[变焦拉远]`
- 其他: `[晃动]`, `[跟随]`, `[固定]`
### 分辨率和时长限制
根据模型不同有以下限制:
**MiniMax-Hailuo-02:**
- 6秒: 支持 512P, 768P, 1080P
- 10秒: 支持 512P, 768P
**其他模型:**
- 6秒: 支持 720P
- 10秒: 不支持
## 🚀 任务执行
### 任务类型映射
- 文生视频 → `BookBackTaskType.HAILUO_VIDEO`
- 图生视频 → `BookBackTaskType.HAILUO_VIDEO`
- 首尾帧视频 → `BookBackTaskType.HAILUO_VIDEO`
### 消息名称
- 文生视频: `HAILUO_TEXT_TO_VIDEO_RETURN`
- 图生视频: `HAILUO_IMAGE_TO_VIDEO_RETURN`
- 首尾帧视频: `HAILUO_FIRST_LAST_FRAME_VIDEO_RETURN`
## 🛠️ 扩展性
### 添加新的配置项
1. 在 `HailuoBaseOptions` 或对应接口中添加字段
2. 在对应的 `*Options` 计算属性中添加配置项
3. 更新 `handleConfigChange` 处理逻辑
### 支持新的模型
1. 在 `HailuoModel` 枚举中添加新模型
2. 更新相关的工具函数 (`IsHailuoModelRequireFirstFrame` 等)
3. 在配置选项中添加对应的显示/隐藏逻辑
## 📝 注意事项
1. **图片格式要求**: JPG、JPEG、PNG、WebP小于20MB
2. **提示词长度**: 最大2000字符
3. **首尾帧限制**: 根据API文档可能有特殊的时长限制
4. **数据同步**: 确保配置变更及时保存到数据库
5. **类型安全**: 利用TypeScript接口确保数据结构正确
## 🔍 调试建议
1. 检查控制台日志中的配置变更信息
2. 验证数据库更新是否成功
3. 确认模型和参数的兼容性
4. 测试Tab切换时的数据保持
这个组件架构提供了完整的海螺视频生成功能,具有良好的扩展性和维护性。

View File

@ -85,6 +85,7 @@ import { useBookStore, useSoftwareStore } from '@/renderer/src/stores'
import { ResponseMessageType } from '@/define/enum/softwareEnum' import { ResponseMessageType } from '@/define/enum/softwareEnum'
import { VideoStatus } from '@/define/enum/video' import { VideoStatus } from '@/define/enum/video'
import { DEFINE_STRING } from '@/define/ipcDefineString' import { DEFINE_STRING } from '@/define/ipcDefineString'
import { t } from '@/i18n'
const bookStore = useBookStore() const bookStore = useBookStore()
const softwareStore = useSoftwareStore() const softwareStore = useSoftwareStore()
@ -135,6 +136,7 @@ onUnmounted(() => {
// //
window.system.removeEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN]) window.system.removeEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN])
window.system.removeEventListen(DEFINE_STRING.BOOK.KLING_IMAGE_TO_VIDEO_RETURN) window.system.removeEventListen(DEFINE_STRING.BOOK.KLING_IMAGE_TO_VIDEO_RETURN)
window.system.removeEventListen(DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN)
}) })
// //
@ -148,6 +150,22 @@ function handleMessageChange(videoMessage, id) {
} }
} }
//
function handleIpcTaskListChange() {
// SD
window.system.setEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN], (value) => {
handleEventReceive(value)
})
window.system.setEventListen(DEFINE_STRING.BOOK.KLING_IMAGE_TO_VIDEO_RETURN, (value) => {
handleEventReceive(value)
})
window.system.setEventListen(DEFINE_STRING.BOOK.HAILUO_TO_VIDEO_RETURN, (value) => {
handleEventReceive(value)
})
}
function handleEventReceive(value) { function handleEventReceive(value) {
try { try {
if (value.type == ResponseMessageType.MJ_VIDEO) { if (value.type == ResponseMessageType.MJ_VIDEO) {
@ -168,6 +186,10 @@ function handleEventReceive(value) {
let videoMessage = JSON.parse(value.data) let videoMessage = JSON.parse(value.data)
console.log('收到 Kling video extend 视频处理进度', videoMessage) console.log('收到 Kling video extend 视频处理进度', videoMessage)
handleMessageChange(videoMessage, value.id) handleMessageChange(videoMessage, value.id)
} else if (value.type == ResponseMessageType.HAI_LUO_VIDEO) {
let videoMessage = JSON.parse(value.data)
console.log('收到 海螺 video extend 视频处理进度', videoMessage)
handleMessageChange(videoMessage, value.id)
} else if (value.type == ResponseMessageType.VIDEO_SUCESS) { } else if (value.type == ResponseMessageType.VIDEO_SUCESS) {
// //
let bookTaskDetail = JSON.parse(value.data) let bookTaskDetail = JSON.parse(value.data)
@ -191,17 +213,6 @@ function handleEventReceive(value) {
} }
} }
function handleIpcTaskListChange() {
// SD
window.system.setEventListen([DEFINE_STRING.BOOK.MJ_VIDEO_TO_VIDEO_RETURN], (value) => {
handleEventReceive(value)
})
window.system.setEventListen(DEFINE_STRING.BOOK.KLING_IMAGE_TO_VIDEO_RETURN, (value) => {
handleEventReceive(value)
})
}
// //
function handleViewDetail(row) { function handleViewDetail(row) {
selectedTask.value = { ...row } selectedTask.value = { ...row }