diff --git a/commons/viewer-common-ai-model/pom.xml b/commons/viewer-common-ai-model/pom.xml index 5d3e4a20e37891393e0e8fea9e872690c01dd713..8856109e1e88594c57f1207305262682e4b12748 100644 --- a/commons/viewer-common-ai-model/pom.xml +++ b/commons/viewer-common-ai-model/pom.xml @@ -32,6 +32,12 @@ spring-ai-client-chat ${springai.version} + + + org.springframework.ai + spring-ai-retry + ${springai.version} + \ No newline at end of file diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java index aca8aaf5c53bbd700506510f4976885b26f9c1c8..ff3a2c6ed198729af711d91eadfacd81ff2af77a 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/ModelParam.java @@ -27,7 +27,7 @@ public class ModelParam extends BaseMeta { protected String setting; protected String paramJson; protected Integer maxMemory = 8; - + //Options Value Map protected transient Map paramMap; protected transient BaseModelSetting modelArgs; diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/AudioModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/AudioModelSetting.java new file mode 100644 index 0000000000000000000000000000000000000000..5dc92e8a3ab2f0c6389067e6bd25b885bffebab5 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/AudioModelSetting.java @@ -0,0 +1,12 @@ +package xyz.thoughtset.viewer.common.ai.model.entity.purpose; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +//@NoArgsConstructor +@AllArgsConstructor +public class AudioModelSetting extends BaseModelSetting { + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java index 62ad41b478acda08017fe8f8d56f25fa6cac1d53..a7cd2f814d01dc6bb8ced43f53d5ad1006ff62c3 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/BaseModelSetting.java @@ -4,8 +4,13 @@ import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; -//@Data -//@NoArgsConstructor -//@AllArgsConstructor +@Data +@NoArgsConstructor +@AllArgsConstructor +/** + * Base class for model settings. + * select specific model setting class according to different option classes. + */ public class BaseModelSetting { + protected String workWay;//select ModelOptions class } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ImageModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ImageModelSetting.java index 18e0bfbfcbd37a97993851dc9b4901204d9bb357..a4b5ee7ec3811c313ea19f8324e8d81cd4b23e0d 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ImageModelSetting.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ImageModelSetting.java @@ -5,8 +5,8 @@ import lombok.Data; import lombok.NoArgsConstructor; @Data -@NoArgsConstructor +//@NoArgsConstructor @AllArgsConstructor public class ImageModelSetting extends BaseModelSetting { - private String size; +// private String size; } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ModelPurposeEnum.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ModelPurposeEnum.java index dafd0f622f56d54f9be650f85de584f841892f51..f1658decbda3c83c7c71721639ca2163537bd86e 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ModelPurposeEnum.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/ModelPurposeEnum.java @@ -6,15 +6,14 @@ import com.fasterxml.jackson.annotation.JsonValue; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.util.StringUtils; -import xyz.thoughtset.viewer.common.core.enums.EnumValue; @Getter @AllArgsConstructor public enum ModelPurposeEnum implements IEnum { CHAT("CHAT", ChatModelSetting.class) ,IMAGE("IMAGE", ImageModelSetting.class) -// ,EMBEDDING("EMBEDDING") -// ,EMBEDDING("EMBEDDING") + ,Audio("Audio", AudioModelSetting.class) + ,Video("Video", VideoModelSetting.class) ; diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/VideoModelSetting.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/VideoModelSetting.java new file mode 100644 index 0000000000000000000000000000000000000000..78cbb8e34b010d6d01fe2bf81f5c0cb1302ee10c --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/entity/purpose/VideoModelSetting.java @@ -0,0 +1,12 @@ +package xyz.thoughtset.viewer.common.ai.model.entity.purpose; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +//@NoArgsConstructor +@AllArgsConstructor +public class VideoModelSetting extends BaseModelSetting { +// private String size; +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/AudioModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/AudioModelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..00146e03189e6feea0200428161ad6639acadf61 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/AudioModelBuilder.java @@ -0,0 +1,23 @@ +package xyz.thoughtset.viewer.common.ai.model.factory; + + +import lombok.Getter; +import lombok.NonNull; +import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; + +import java.util.List; + +@Getter +public abstract class AudioModelBuilder extends ModelBuilder{ + + + protected AudioModelBuilder(@NonNull String provider) { + super(provider, ModelPurposeEnum.Audio); + } + + protected AudioModelBuilder(@NonNull String provider, List limitModels) { + super(provider, ModelPurposeEnum.Audio, limitModels); + } + + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java index 9de05a98096a9d1ef67d416e784d906d827dd075..3a3a1020385f9304d153febff58ad43dff11f8a2 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelBuilder.java @@ -5,21 +5,35 @@ import com.fasterxml.jackson.databind.ObjectMapper; import lombok.Getter; import lombok.NonNull; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; import org.springframework.ai.model.Model; import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.retry.TransientAiException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.retry.RetryCallback; +import org.springframework.retry.RetryContext; +import org.springframework.retry.RetryListener; +import org.springframework.retry.support.RetryTemplate; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClient; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; import xyz.thoughtset.viewer.common.core.util.MapMergeUtil; +import java.time.Duration; import java.util.List; import java.util.Map; import java.util.Optional; +@Slf4j @Getter public abstract class ModelBuilder { protected String provider; @@ -60,6 +74,49 @@ public abstract class ModelBuilder { return this.provider.equals(node.getProvider()) && checkFlag; } + protected RetryTemplate buildDefaultLongRetryTemplate(){ +// RetryTemplate retryTemplate = new RetryTemplate(); +// if (modelParam.getMaxRetries() != null && modelParam.getMaxRetries() > 0){ +// retryTemplate.setMaxAttempts(modelParam.getMaxRetries()+1); +// } +// if (modelParam.getRetryDelayMs() != null && modelParam.getRetryDelayMs() > 0){ +// retryTemplate.setBackOffPeriod(modelParam.getRetryDelayMs()); +// } +// return retryTemplate; + return buildSimpleRetryTemplate(3,9000L,5.0,180000L); + } + + protected RetryTemplate buildSimpleRetryTemplate(int maxAttempts, long initialIntervalMs, double multiplier, long maxIntervalMs){ + return RetryTemplate.builder() + .maxAttempts(maxAttempts) + .retryOn(TransientAiException.class) + .retryOn(ResourceAccessException.class) + .exponentialBackoff(Duration.ofMillis(initialIntervalMs), multiplier, Duration.ofMillis(maxIntervalMs)) + .withListener(new RetryListener() { + public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { + log.warn("Retry error. Retry count:{}", context.getRetryCount(), throwable); + } + }).build(); + } + + protected RestClient.Builder createLoggingRestClient() { + // 创建支持缓冲的请求工厂(允许多次读取请求体和响应体) + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(30000); + requestFactory.setReadTimeout(120000); + + ClientHttpRequestFactory bufferingFactory = + new BufferingClientHttpRequestFactory(requestFactory); + + // 创建RestClient并添加拦截器 + return RestClient.builder() + .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) + .requestFactory(bufferingFactory) + // 使用美化版拦截器 + .requestInterceptor(new PrettyRestClientLoggingInterceptor()) + ; + } + public Model buildMode(AiNode node, ModelParam modelParam){ return buildMode(node,modelParam,null); } @@ -88,4 +145,6 @@ public abstract class ModelBuilder { return (T) targetOptions; } + + } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java index af60a0d126e5ff243ae04eed1443a16997b95941..1d0181aa9ddf0b9bf2fac3d2f411452edeec6853 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/ModelFactory.java @@ -42,9 +42,15 @@ public class ModelFactory { public Model buildModel(ModelParam modelParam, ModelOptions modelOptions){ AiNode aiNode = aiNodeService.selectDetail(modelParam.getPid()); ModelBuilder builder = ModelsRegistry.loadBuilder(aiNode, modelParam); - return builder.buildMode(aiNode, modelParam); + return builder.buildMode(aiNode, modelParam,modelOptions); } +// public ModelPkg buildModel(ModelParam modelParam, ModelOptions modelOptions){ +// AiNode aiNode = aiNodeService.selectDetail(modelParam.getPid()); +// ModelBuilder builder = ModelsRegistry.loadBuilder(aiNode, modelParam); +// return builder.buildMode(aiNode, modelParam,modelOptions); +// } + public ChatClient.Builder clientBuilder(@NonNull ModelParam modelParam){ return clientBuilder(modelParam,null); } @@ -54,6 +60,7 @@ public class ModelFactory { return ChatClient.builder(chatModel); } + public record ModelPkg(String modelPkey, Map paramMap){} } diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/PrettyRestClientLoggingInterceptor.java similarity index 98% rename from models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java rename to commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/PrettyRestClientLoggingInterceptor.java index d0a81f4be8d732a1fa2fd18f64dc95263eb3add0..898edf7c4f6f1c9c003096aa5a9260ae135162f6 100644 --- a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/PrettyRestClientLoggingInterceptor.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/PrettyRestClientLoggingInterceptor.java @@ -1,4 +1,4 @@ -package xyz.thoughtset.viewer.models.zhipuai; +package xyz.thoughtset.viewer.common.ai.model.factory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/VideoModelBuilder.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/VideoModelBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..746b4ccf21251397feaedd30459cd1679838ac11 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/factory/VideoModelBuilder.java @@ -0,0 +1,23 @@ +package xyz.thoughtset.viewer.common.ai.model.factory; + + +import lombok.Getter; +import lombok.NonNull; +import xyz.thoughtset.viewer.common.ai.model.entity.purpose.ModelPurposeEnum; + +import java.util.List; + +@Getter +public abstract class VideoModelBuilder extends ModelBuilder{ + + + protected VideoModelBuilder(@NonNull String provider) { + super(provider, ModelPurposeEnum.Video); + } + + protected VideoModelBuilder(@NonNull String provider, List limitModels) { + super(provider, ModelPurposeEnum.Video, limitModels); + } + + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/AiNodeServiceImpl.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/AiNodeServiceImpl.java index 9d4906b652c15982c52f268b92feb08bcc619bba..5bcf3a4c7e40fcf663adda7ab03f0d3a17acda0b 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/AiNodeServiceImpl.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/AiNodeServiceImpl.java @@ -1,26 +1,14 @@ package xyz.thoughtset.viewer.common.ai.model.service; -import cn.zhxu.bs.BeanSearcher; -import cn.zhxu.bs.util.MapUtils; -import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; -import com.fasterxml.jackson.databind.JavaType; -import jakarta.annotation.PostConstruct; import lombok.SneakyThrows; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import xyz.thoughtset.viewer.common.ai.model.dao.AiNodeDao; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.crud.core.service.BaseServiceImpl; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @Service public class AiNodeServiceImpl extends BaseServiceImpl implements AiNodeService { @@ -28,10 +16,10 @@ public class AiNodeServiceImpl extends BaseServiceImpl implem @Override public AiNode saveData(AiNode data) { if (!ObjectUtils.isEmpty(data.getSettingMap())){ - data.setSettingStr(mapper.writeValueAsString(data.getSettingMap())); + data.setSettingStr(objectMapper.writeValueAsString(data.getSettingMap())); } if (!ObjectUtils.isEmpty(data.getHeaderMap())){ - data.setHeadersStr(mapper.writeValueAsString(data.getHeaderMap())); + data.setHeadersStr(objectMapper.writeValueAsString(data.getHeaderMap())); } return super.saveData(data); } @@ -41,10 +29,10 @@ public class AiNodeServiceImpl extends BaseServiceImpl implem public AiNode selectDetail(String pkey) { AiNode data = super.selectDetail(pkey); if (StringUtils.hasText(data.getSettingStr())){ - data.setSettingMap(mapper.readValue(data.getSettingStr(), Map.class)); + data.setSettingMap(objectMapper.readValue(data.getSettingStr(), Map.class)); } if (StringUtils.hasText(data.getHeadersStr())){ - data.setHeaderMap(mapper.readValue(data.getHeadersStr(), Map.class)); + data.setHeaderMap(objectMapper.readValue(data.getHeadersStr(), Map.class)); } return data; } diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/ModelParamServiceImpl.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/ModelParamServiceImpl.java index f54fce1799f1467144c810c5c7c36791e646b216..7211c0fe6c0e4f87a73423bf26a7762f6989f699 100644 --- a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/ModelParamServiceImpl.java +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/service/ModelParamServiceImpl.java @@ -2,12 +2,10 @@ package xyz.thoughtset.viewer.common.ai.model.service; import lombok.SneakyThrows; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import xyz.thoughtset.viewer.common.ai.model.dao.ModelParamDao; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; -import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; import xyz.thoughtset.viewer.common.ai.model.entity.purpose.BaseModelSetting; import xyz.thoughtset.viewer.common.crud.core.service.BaseServiceImpl; @@ -19,10 +17,10 @@ public class ModelParamServiceImpl extends BaseServiceImpl { + private final String missionId; + + public AsyncVideo(@JsonProperty("request_id") String missionId){ + this.missionId = missionId; + } + + @Override + public String getOutput() { + return this.missionId; + } + + @Override + public ResultMetadata getMetadata() { + return null; + } +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/ReqInfo.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/ReqInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..085a657fd2ceb5d838fa4939889f50b566d55669 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/ReqInfo.java @@ -0,0 +1,5 @@ +package xyz.thoughtset.viewer.common.ai.model.video; + +public interface ReqInfo { + String reqUrl(); +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoPrompt.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoPrompt.java new file mode 100644 index 0000000000000000000000000000000000000000..f1a89628f85345c3e61413bfdc861c84a626bf2d --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoPrompt.java @@ -0,0 +1,25 @@ +package xyz.thoughtset.viewer.common.ai.model.video; + + +import org.springframework.ai.audio.tts.TextToSpeechOptions; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.model.ModelRequest; + +public class VideoPrompt implements ModelRequest { + private final String message; + private _BaseVideoOptions options; + + public VideoPrompt(String message) { + this.message = message; + } + + @Override + public String getInstructions() { + return message; + } + + @Override + public ModelOptions getOptions() { + return options; + } +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoResponseMetadata.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoResponseMetadata.java new file mode 100644 index 0000000000000000000000000000000000000000..4b320d6477b176ce52c2b7b95f23ef1fda296ce6 --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/VideoResponseMetadata.java @@ -0,0 +1,38 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.common.ai.model.video; + +import org.springframework.ai.model.MutableResponseMetadata; + + +public class VideoResponseMetadata extends MutableResponseMetadata { + + private final Long created; + + public VideoResponseMetadata() { + this(System.currentTimeMillis()); + } + + public VideoResponseMetadata(Long created) { + this.created = created; + } + + public Long getCreated() { + return this.created; + } + +} diff --git a/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/_BaseVideoOptions.java b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/_BaseVideoOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..54b6c6d714b5fcd940466cb28f95dc8b87b569ab --- /dev/null +++ b/commons/viewer-common-ai-model/src/main/java/xyz/thoughtset/viewer/common/ai/model/video/_BaseVideoOptions.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.common.ai.model.video; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.springframework.ai.audio.tts.TextToSpeechOptions; +import org.springframework.ai.model.ModelOptions; +import org.springframework.lang.Nullable; + + +public interface _BaseVideoOptions extends ModelOptions { + + @Nullable + String getModel(); + + + +} diff --git a/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/config/ObjectMapperConfig.java b/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/config/ObjectMapperConfig.java index 4ed63902e9411d368278c27ca49298d8e255be64..417c6c7687e0621f4d5bd1b148c2d5cd56c0ba8b 100644 --- a/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/config/ObjectMapperConfig.java +++ b/commons/viewer-common-core/src/main/java/xyz/thoughtset/viewer/common/core/config/ObjectMapperConfig.java @@ -19,6 +19,7 @@ public class ObjectMapperConfig { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true); diff --git a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java index 00390cd39cddf0e0e3563cc2789873ecb97595a7..1592d75ec120018f05da1bec108b7ecc49c4c5d7 100644 --- a/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java +++ b/commons/viewer-common-crud/viewer-common-crud-core/src/main/java/xyz/thoughtset/viewer/common/crud/core/service/BaseServiceImpl.java @@ -16,7 +16,7 @@ import java.util.List; @SuppressWarnings({"unchecked","SpringJavaInjectionPointsAutowiringInspection"}) public class BaseServiceImpl , T extends IdMeta> extends ServiceImpl implements BaseService { @Autowired - protected ObjectMapper mapper; + protected ObjectMapper objectMapper; @Override public Object createData(LinkedHashMap baseMap){ @@ -89,7 +89,7 @@ public class BaseServiceImpl , T extends IdMeta> extends @SneakyThrows public T convertValue(LinkedHashMap baseMap) { - return mapper.convertValue(baseMap,this.getEntityClass()); + return objectMapper.convertValue(baseMap,this.getEntityClass()); } protected QueryWrapper pidQueryWrapper(String pid){ diff --git a/commons/viewer-common-envvar/src/main/java/xyz/thoughtset/viewer/common/envvar/service/EnvVarsServiceImpl.java b/commons/viewer-common-envvar/src/main/java/xyz/thoughtset/viewer/common/envvar/service/EnvVarsServiceImpl.java index 62dc95752588899df025acd7906d2509c7636530..206fd712b30352c598064bdc666781b5c42e05a3 100644 --- a/commons/viewer-common-envvar/src/main/java/xyz/thoughtset/viewer/common/envvar/service/EnvVarsServiceImpl.java +++ b/commons/viewer-common-envvar/src/main/java/xyz/thoughtset/viewer/common/envvar/service/EnvVarsServiceImpl.java @@ -4,16 +4,13 @@ import cn.hutool.core.collection.ConcurrentHashSet; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import jakarta.annotation.PostConstruct; import lombok.SneakyThrows; -import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; import xyz.thoughtset.viewer.common.crud.core.service.BaseServiceImpl; import xyz.thoughtset.viewer.common.envvar.annotation.EnvPropSign; import xyz.thoughtset.viewer.common.envvar.constants.EnvVarDataTypeConstant; import xyz.thoughtset.viewer.common.envvar.dao.EnvVarsDao; import xyz.thoughtset.viewer.common.envvar.entity.EnvVars; -import xyz.thoughtset.viewer.common.envvar.entity.EnvVars; import xyz.thoughtset.viewer.common.envvar.factory.EnvPropSignFactory; import java.util.*; @@ -48,7 +45,7 @@ public class EnvVarsServiceImpl extends BaseServiceImpl imp if (EnvVarDataTypeConstant.OBJ.equals(data.getType())) { targetData = envSignFactory.getTargetData(topic, targetData); } - data.setPayload(mapper.writeValueAsString(targetData)); + data.setPayload(objectMapper.writeValueAsString(targetData)); if (envSignFactory.getSignInfo(topic).single()){ data.setId(topic); } diff --git a/commons/viewer-common-exc/src/main/java/xyz/thoughtset/viewer/common/exc/service/ExcInfoServiceImpl.java b/commons/viewer-common-exc/src/main/java/xyz/thoughtset/viewer/common/exc/service/ExcInfoServiceImpl.java index aa730587652910f37a1fb4c6e849dee945d3ace5..0a4ff69de300b041939e15d95744fcf293fca534 100644 --- a/commons/viewer-common-exc/src/main/java/xyz/thoughtset/viewer/common/exc/service/ExcInfoServiceImpl.java +++ b/commons/viewer-common-exc/src/main/java/xyz/thoughtset/viewer/common/exc/service/ExcInfoServiceImpl.java @@ -1,19 +1,12 @@ package xyz.thoughtset.viewer.common.exc.service; import com.fasterxml.jackson.core.JsonProcessingException; -import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.ObjectUtils; import xyz.thoughtset.viewer.common.crud.core.service.BaseServiceImpl; import xyz.thoughtset.viewer.common.exc.dao.ExcInfoDao; import xyz.thoughtset.viewer.common.exc.entity.ExcInfo; -import java.util.LinkedHashMap; -import java.util.Objects; - @Slf4j @Service public class ExcInfoServiceImpl extends BaseServiceImpl implements ExcInfoService { @@ -21,7 +14,7 @@ public class ExcInfoServiceImpl extends BaseServiceImpl imp @Override public boolean save(ExcInfo entity) { try { - entity.setParamStr(mapper.writeValueAsString(entity.getParams())); + entity.setParamStr(objectMapper.writeValueAsString(entity.getParams())); } catch (JsonProcessingException e) { e.printStackTrace(); } diff --git a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java index 3979e9c3abc47acc6a7f12110e5ac396f244d757..94cb709839f93c2d3a66c1e26daffd4ab7e07446 100644 --- a/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java +++ b/executor/viewer-executor-blocks/src/main/java/xyz/thoughtset/viewer/executor/blocks/executor/ExecAIBlockExecutor.java @@ -1,6 +1,8 @@ package xyz.thoughtset.viewer.executor.blocks.executor; import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.audio.tts.TextToSpeechModel; +import org.springframework.ai.audio.tts.TextToSpeechPrompt; import org.springframework.ai.image.*; import org.springframework.ai.model.Model; import org.springframework.expression.ExpressionParser; @@ -9,6 +11,7 @@ import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.video.VideoPrompt; import xyz.thoughtset.viewer.common.exc.exceptions.ExecException; import xyz.thoughtset.viewer.modules.step.entity.ExecAIBody; import xyz.thoughtset.viewer.modules.step.entity.block.BlockBodyEle; @@ -32,22 +35,22 @@ public class ExecAIBlockExecutor extends AbstractAISupportBlockExecutor { + result = aiModel.call(new TextToSpeechPrompt(userPrompt)).getResult().getOutput(); + } + case IMAGE -> { + result = ((ImageModel) aiModel).call( + new ImagePrompt(userPrompt) + ).getResult().getOutput(); + } + case Video -> { + result = aiModel.call(new VideoPrompt(userPrompt)).getResult().getOutput(); + } + default -> { + throw new RuntimeException("Unsupported AI model purpose: " + modelParam.getPurpose()); + } } -// aiModel. -// if (StringUtils.hasText(systemPrompt)){ -// builder.defaultSystem(systemPrompt); -// } -// ChatClient client = builder.build(); - return result; } } diff --git a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java index 907d2b10b57b98091423a261aef425504814de11..b17f1b5e8691ed2fecfe2a3a87444aff489a60e2 100644 --- a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java +++ b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAIImageBuilder.java @@ -3,6 +3,7 @@ package xyz.thoughtset.viewer.models.openai; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.model.Model; import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.openai.OpenAiAudioSpeechOptions; import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.api.OpenAiImageApi; @@ -43,46 +44,18 @@ public class OpenAIImageBuilder extends ImageModelBuilder { @Override public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { - OpenAiImageOptions.Builder builder = OpenAiImageOptions.builder() - .model(modelParam.getModel()); - OpenAiImageOptions options = builder.build(); +// OpenAiImageOptions.Builder builder = OpenAiImageOptions.builder() +// .model(modelParam.getModel()); + OpenAiImageOptions options = loadAndMergeModelOptions(modelParam, OpenAiImageOptions.class, modelOptions); + options.setModel(modelParam.getModel()); OpenAiImageApi api = OpenAiImageApi.builder() .baseUrl(node.getBaseUrl()) .apiKey(node.getApiKey()) .build(); - OpenAiImageModel model = new OpenAiImageModel(api, options, - RetryTemplate.builder().maxAttempts(3) - .retryOn(TransientAiException.class) - .retryOn(ResourceAccessException.class) - .exponentialBackoff(Duration.ofMillis(11000L), 5.0, Duration.ofMillis(180000L)) - .withListener(new RetryListener() { - public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { - log.warn("Retry error. Retry count:{}", context.getRetryCount(), throwable); - } - }).build() - ); + OpenAiImageModel model = new OpenAiImageModel(api, options,buildDefaultLongRetryTemplate()); return model; } - private RestClient.Builder createLoggingRestClient() { - // 创建支持缓冲的请求工厂(允许多次读取请求体和响应体) - SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); - requestFactory.setConnectTimeout(30000); - requestFactory.setReadTimeout(120000); - - ClientHttpRequestFactory bufferingFactory = - new BufferingClientHttpRequestFactory(requestFactory); - - // 创建RestClient并添加拦截器 - return RestClient.builder() - .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) - .requestFactory(bufferingFactory) - // 使用美化版拦截器 -// .requestInterceptor(new PrettyRestClientLoggingInterceptor()) - // 或使用简单版拦截器 - // .requestInterceptor(new RestClientLoggingInterceptor()) - ; - } } diff --git a/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAITTSBuilder.java b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAITTSBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..1e01bb3c1f57e59e119de90e5fc574234ac9e043 --- /dev/null +++ b/models/viewer-models-openai/src/main/java/xyz/thoughtset/viewer/models/openai/OpenAITTSBuilder.java @@ -0,0 +1,38 @@ +package xyz.thoughtset.viewer.models.openai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.openai.*; +import org.springframework.ai.openai.api.OpenAiAudioApi; +import org.springframework.stereotype.Component; +import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; +import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.factory.AudioModelBuilder; + +@Slf4j +@Component +public class OpenAITTSBuilder extends AudioModelBuilder { + public OpenAITTSBuilder() { + super("OpenAI"); + } + + + @Override + public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { +// OpenAiAudioSpeechOptions.Builder builder = OpenAiAudioSpeechOptions.builder() +// .model(modelParam.getModel()); + OpenAiAudioSpeechOptions options = loadAndMergeModelOptions(modelParam, OpenAiAudioSpeechOptions.class, modelOptions); + options.setModel(modelParam.getModel()); + OpenAiAudioApi api = OpenAiAudioApi.builder() + .baseUrl(node.getBaseUrl()) + .apiKey(node.getApiKey()) + .restClientBuilder(createLoggingRestClient()) + .build(); + OpenAiAudioSpeechModel model = new OpenAiAudioSpeechModel(api, options,buildDefaultLongRetryTemplate()); + return model; + } + + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuAudioBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuAudioBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..90705913084732bdc3b8895a892abaafa40aeb34 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuAudioBuilder.java @@ -0,0 +1,41 @@ +package xyz.thoughtset.viewer.models.zhipuai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; +import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.stereotype.Component; +import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; +import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.factory.AudioModelBuilder; +import xyz.thoughtset.viewer.models.zhipuai.api.audio.AudioApi; +import xyz.thoughtset.viewer.models.zhipuai.api.audio.AudioModel; +import xyz.thoughtset.viewer.models.zhipuai.api.audio.AudioOptions; + +@Slf4j +@Component +public class ZhipuAudioBuilder extends AudioModelBuilder { + public ZhipuAudioBuilder() { + super("ZhipuAI"); + } + + + @Override + public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { + AudioOptions options = loadAndMergeModelOptions(modelParam, AudioOptions.class, modelOptions); + options.setModel(modelParam.getModel()); + AudioApi api = AudioApi.builder() + .baseUrl(node.getBaseUrl()) + .apiKey(node.getApiKey()) + .restClientBuilder(createLoggingRestClient()) + .build(); +// new AudioApi(node.getBaseUrl(), node.getApiKey(), createLoggingRestClient()); + AudioModel model = new AudioModel(api, options,buildDefaultLongRetryTemplate()); + return model; + } + + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java index 273ddd13ceb09dabe235e681907886c7f7874152..362a107fc1d51807e7610f151151e32deb3648d5 100644 --- a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuImageBuilder.java @@ -3,32 +3,17 @@ package xyz.thoughtset.viewer.models.zhipuai; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.model.Model; import org.springframework.ai.model.ModelOptions; -import org.springframework.ai.retry.TransientAiException; import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; import org.springframework.http.client.*; -import org.springframework.retry.RetryCallback; -import org.springframework.retry.RetryContext; -import org.springframework.retry.RetryListener; -import org.springframework.retry.support.RetryTemplate; import org.springframework.stereotype.Component; -import org.springframework.web.client.ResourceAccessException; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestTemplate; import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; import xyz.thoughtset.viewer.common.ai.model.factory.ImageModelBuilder; -import xyz.thoughtset.viewer.common.ai.model.factory.ModelBuilder; - -import java.time.Duration; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import xyz.thoughtset.viewer.common.ai.model.factory.PrettyRestClientLoggingInterceptor; @Slf4j @Component @@ -40,40 +25,15 @@ public class ZhipuImageBuilder extends ImageModelBuilder { @Override public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { - ZhiPuAiImageOptions.Builder builder = ZhiPuAiImageOptions.builder() - .model(modelParam.getModel()); - ZhiPuAiImageOptions options = builder.build(); + ZhiPuAiImageOptions options = loadAndMergeModelOptions(modelParam, ZhiPuAiImageOptions.class, modelOptions); + options.setModel(modelParam.getModel()); ZhiPuAiImageApi api = new ZhiPuAiImageApi(node.getBaseUrl(), node.getApiKey(), createLoggingRestClient()); - ZhiPuAiImageModel model = new ZhiPuAiImageModel(api, options, - RetryTemplate.builder().maxAttempts(3).retryOn(TransientAiException.class).retryOn(ResourceAccessException.class).exponentialBackoff(Duration.ofMillis(9000L), 5.0, Duration.ofMillis(180000L)).withListener(new RetryListener() { - public void onError(RetryContext context, RetryCallback callback, Throwable throwable) { - log.warn("Retry error. Retry count:{}", context.getRetryCount(), throwable); - } - }).build() - ); + ZhiPuAiImageModel model = new ZhiPuAiImageModel(api, options,buildDefaultLongRetryTemplate()); return model; } - private RestClient.Builder createLoggingRestClient() { - // 创建支持缓冲的请求工厂(允许多次读取请求体和响应体) - SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); - requestFactory.setConnectTimeout(30000); - requestFactory.setReadTimeout(120000); - - ClientHttpRequestFactory bufferingFactory = - new BufferingClientHttpRequestFactory(requestFactory); - // 创建RestClient并添加拦截器 - return RestClient.builder() - .defaultHeaders(h -> h.setContentType(MediaType.APPLICATION_JSON)) - .requestFactory(bufferingFactory) - // 使用美化版拦截器 - .requestInterceptor(new PrettyRestClientLoggingInterceptor()) - // 或使用简单版拦截器 - // .requestInterceptor(new RestClientLoggingInterceptor()) - ; - } } diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuVideoBuilder.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuVideoBuilder.java new file mode 100644 index 0000000000000000000000000000000000000000..eb651982082e028df2844c195f8711aba1ad82f7 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/ZhipuVideoBuilder.java @@ -0,0 +1,50 @@ +package xyz.thoughtset.viewer.models.zhipuai; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.ModelOptions; +import org.springframework.ai.zhipuai.ZhiPuAiImageModel; +import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; +import org.springframework.ai.zhipuai.api.ZhiPuAiApi; +import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; +import org.springframework.http.MediaType; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; +import xyz.thoughtset.viewer.common.ai.model.entity.AiNode; +import xyz.thoughtset.viewer.common.ai.model.entity.ModelParam; +import xyz.thoughtset.viewer.common.ai.model.factory.ImageModelBuilder; +import xyz.thoughtset.viewer.common.ai.model.factory.PrettyRestClientLoggingInterceptor; +import xyz.thoughtset.viewer.common.ai.model.factory.VideoModelBuilder; +import xyz.thoughtset.viewer.models.zhipuai.api.video.VideoApi; +import xyz.thoughtset.viewer.models.zhipuai.api.video.VideoModel; +import xyz.thoughtset.viewer.models.zhipuai.api.video.VideoOptions; + +@Slf4j +@Component +public class ZhipuVideoBuilder extends VideoModelBuilder { + public ZhipuVideoBuilder() { + super("ZhipuAI"); + } + + + @Override + public Model buildMode(AiNode node, ModelParam modelParam, ModelOptions modelOptions) { + VideoOptions options = loadAndMergeModelOptions(modelParam, VideoOptions.class, modelOptions); + options.setModel(modelParam.getModel()); +// VideoApi api = new VideoApi(node.getBaseUrl(), node.getApiKey(), createLoggingRestClient()); + VideoApi api = VideoApi.builder() + .baseUrl(node.getBaseUrl()) + .apiKey(node.getApiKey()) + .restClientBuilder(createLoggingRestClient()) + .build(); + VideoModel model = new VideoModel(api, options,buildDefaultLongRetryTemplate()); + return model; + } + + + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/ApiConstants.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/ApiConstants.java new file mode 100644 index 0000000000000000000000000000000000000000..6e8a17e01cbef3c892c7d74c576a4d53f23eee1d --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/ApiConstants.java @@ -0,0 +1,10 @@ +package xyz.thoughtset.viewer.models.zhipuai.api; + +public class ApiConstants { + public static final String DEFAULT_BASE_URL = "https://open.bigmodel.cn/api/paas"; + public static final String TTS_URL = "/v4/audio/speech"; + public static final String ASR_URL = "/v4/audio/transcriptions"; + public static final String CLONE_URL = "/v4/voice/clone"; + public static final String VIDEO_URL = "/v4/videos/generations"; + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioApi.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioApi.java new file mode 100644 index 0000000000000000000000000000000000000000..c055d3a3d17ea6fb28b78c194068dedf4655ff6c --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioApi.java @@ -0,0 +1,415 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.audio; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.springframework.ai.model.ApiKey; +import org.springframework.ai.model.NoopApiKey; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.core.io.ByteArrayResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import xyz.thoughtset.viewer.models.zhipuai.api.ApiConstants; + +import java.util.List; +import java.util.function.Consumer; + +public class AudioApi { + + private final RestClient restClient; + + private final WebClient webClient; + + /** + * Create a new audio api. + * @param baseUrl api base URL. + * @param apiKey OpenAI apiKey. + * @param headers the http headers to use. + * @param restClientBuilder RestClient builder. + * @param webClientBuilder WebClient builder. + * @param responseErrorHandler Response error handler. + */ + public AudioApi(String baseUrl, ApiKey apiKey, HttpHeaders headers, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + if (!StringUtils.hasText(baseUrl)) { + baseUrl = ApiConstants.DEFAULT_BASE_URL; + } + Consumer authHeaders = h -> h.addAll(HttpHeaders.readOnlyHttpHeaders(headers)); + + // @formatter:off + this.restClient = restClientBuilder.clone() + .baseUrl(baseUrl) + .defaultHeaders(authHeaders) + .defaultStatusHandler(responseErrorHandler) + .defaultRequest(requestHeadersSpec -> { + if (!(apiKey instanceof NoopApiKey)) { + requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue()); + } + }) + .build(); + + this.webClient = webClientBuilder.clone() + .baseUrl(baseUrl) + .defaultHeaders(authHeaders) + .defaultRequest(requestHeadersSpec -> { + if (!(apiKey instanceof NoopApiKey)) { + requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue()); + } + }) + .build(); // @formatter:on + } + + + public static Builder builder() { + return new Builder(); + } + + /** + * Request to generates audio from the input text. + * @param requestBody The request body. + * @return Response entity containing the audio binary. + */ + public ResponseEntity createSpeech(SpeechRequest requestBody) { + return this.restClient.post().uri(ApiConstants.TTS_URL).body(requestBody).retrieve().toEntity(byte[].class); + } + + /** + * Streams audio generated from the input text. + *

+ * This method sends a POST request to the OpenAI API to generate audio from the + * provided text. The audio is streamed back as a Flux of ResponseEntity objects, each + * containing a byte array of the audio data. + * @param requestBody The request body containing the details for the audio + * generation, such as the input text, model, voice, and response format. + * @return A Flux of ResponseEntity objects, each containing a byte array of the audio + * data. + */ + public Flux> stream(SpeechRequest requestBody) { + + return this.webClient.post() + .uri(ApiConstants.TTS_URL) + .body(Mono.just(requestBody), SpeechRequest.class) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .exchangeToFlux(clientResponse -> { + HttpHeaders headers = clientResponse.headers().asHttpHeaders(); + return clientResponse.bodyToFlux(byte[].class) + .map(bytes -> ResponseEntity.ok().headers(headers).body(bytes)); + }); + } + + + public ResponseEntity createTranscription(TranscriptionRequest requestBody) { + + MultiValueMap multipartBody = new LinkedMultiValueMap<>(); + multipartBody.add("file", new ByteArrayResource(requestBody.file()) { + + @Override + public String getFilename() { + return requestBody.fileName(); + } + }); + multipartBody.add("model", requestBody.model()); + multipartBody.add("temperature", requestBody.temperature()); + + return this.restClient.post() + .uri(ApiConstants.ASR_URL) + .body(multipartBody) + .retrieve() + .toEntity(StructuredResponse.class); + } + + + /** + * Request to generates audio from the input text. Reference: + * Create + * Speech + * + * @param model The model to use for generating the audio. One of the available Audio + * models: audio-1, audio-1-hd, or gpt-4o-mini-audio. + * @param input The input text to synthesize. Must be at most 4096 tokens long. + * @param voice The voice to use for synthesis. One of the available voices for the + * chosen model: 'alloy', 'ash', 'ballad', 'coral', 'echo', 'fable', 'onyx', 'nova', + * 'sage', 'shimmer', and 'verse'. + * @param responseFormat The format to audio in. Supported formats are mp3, opus, aac, + * flac, wav, and pcm. Defaults to mp3. + * @param speed The speed of the voice synthesis. The acceptable range is from 0.25 + * (slowest) to 4.0 (fastest). Does not work with gpt-4o-mini-audio. + */ + @JsonInclude(Include.NON_NULL) + public record SpeechRequest( + @JsonProperty("model") String model, + @JsonProperty("input") String input, + @JsonProperty("voice") String voice, + @JsonProperty("response_format") String responseFormat, + @JsonProperty("speed") Double speed) { + // @formatter:on + + public static Builder builder() { + return new Builder(); + } + + + + /** + * Builder for the SpeechRequest. + */ + public static final class Builder { + + private String model = "cogtts"; + + private String input; + + private String voice; + + private String responseFormat = "pcm"; + + private Double speed; + + public Builder model(String model) { + this.model = model; + return this; + } + + public Builder input(String input) { + this.input = input; + return this; + } + + public Builder voice(String voice) { + this.voice = voice; + return this; + } + + public Builder responseFormat(String responseFormat) { + this.responseFormat = responseFormat; + return this; + } + + public Builder speed(Double speed) { + this.speed = speed; + return this; + } + + public SpeechRequest build() { + + return new SpeechRequest(this.model, this.input, this.voice, this.responseFormat, this.speed); + } + + } + + } + + @JsonInclude(Include.NON_NULL) + public record TranscriptionRequest( + // @formatter:off + @JsonProperty("file") byte[] file, + String fileName, + @JsonProperty("model") String model, + @JsonProperty("temperature") Float temperature + ) { + public static Builder builder() { + return new Builder(); + } + + + public static final class Builder { + + private byte[] file; + + private String fileName; + + private String model; + + private Float temperature; + + public Builder file(byte[] file) { + this.file = file; + return this; + } + + public Builder fileName(String fileName) { + this.fileName = fileName; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + + public Builder temperature(Float temperature) { + this.temperature = temperature; + return this; + } + + public TranscriptionRequest build() { + Assert.notNull(this.file, "file must not be null"); + Assert.hasText(this.model, "model must not be empty"); + + return new TranscriptionRequest(this.file, this.fileName, this.model, this.temperature); + } + + } + + } + + @JsonInclude(Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public record StructuredResponse( + // @formatter:off + @JsonProperty("language") String language, + @JsonProperty("duration") Float duration, + @JsonProperty("text") String text, + @JsonProperty("words") List words, + @JsonProperty("segments") List segments) { + // @formatter:on + + /** + * Extracted word and it corresponding timestamps. + * + * @param word The text content of the word. + * @param start The start time of the word in seconds. + * @param end The end time of the word in seconds. + */ + @JsonInclude(Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public record Word( + // @formatter:off + @JsonProperty("word") String word, + @JsonProperty("start") Float start, + @JsonProperty("end") Float end) { + // @formatter:on + } + + /** + * Segment of the transcribed text and its corresponding details. + * + * @param id Unique identifier of the segment. + * @param seek Seek offset of the segment. + * @param start Start time of the segment in seconds. + * @param end End time of the segment in seconds. + * @param text The text content of the segment. + * @param tokens Array of token IDs for the text content. + * @param temperature Temperature parameter used for generating the segment. + * @param avgLogprob Average logprob of the segment. If the value is lower than + * -1, consider the logprobs failed. + * @param compressionRatio Compression ratio of the segment. If the value is + * greater than 2.4, consider the compression failed. + * @param noSpeechProb Probability of no speech in the segment. If the value is + * higher than 1.0 and the avg_logprob is below -1, consider this segment silent. + */ + @JsonInclude(Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public record Segment( + // @formatter:off + @JsonProperty("id") Integer id, + @JsonProperty("seek") Integer seek, + @JsonProperty("start") Float start, + @JsonProperty("end") Float end, + @JsonProperty("text") String text, + @JsonProperty("tokens") List tokens, + @JsonProperty("temperature") Float temperature, + @JsonProperty("avg_logprob") Float avgLogprob, + @JsonProperty("compression_ratio") Float compressionRatio, + @JsonProperty("no_speech_prob") Float noSpeechProb) { + // @formatter:on + } + + } + + + + public static final class Builder { + + private String baseUrl = ApiConstants.DEFAULT_BASE_URL; + + private ApiKey apiKey; + + private HttpHeaders headers = new HttpHeaders(); + + private RestClient.Builder restClientBuilder = RestClient.builder(); + + private WebClient.Builder webClientBuilder = WebClient.builder(); + + private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER; + + public Builder baseUrl(String baseUrl) { + Assert.hasText(baseUrl, "baseUrl cannot be null or empty"); + this.baseUrl = baseUrl; + return this; + } + + public Builder apiKey(ApiKey apiKey) { + Assert.notNull(apiKey, "apiKey cannot be null"); + this.apiKey = apiKey; + return this; + } + + public Builder apiKey(String simpleApiKey) { + Assert.notNull(simpleApiKey, "simpleApiKey cannot be null"); + this.apiKey = new SimpleApiKey(simpleApiKey); + return this; + } + + public Builder headers(HttpHeaders headers) { + Assert.notNull(headers, "headers cannot be null"); + this.headers = headers; + return this; + } + + public Builder restClientBuilder(RestClient.Builder restClientBuilder) { + Assert.notNull(restClientBuilder, "restClientBuilder cannot be null"); + this.restClientBuilder = restClientBuilder; + return this; + } + + public Builder webClientBuilder(WebClient.Builder webClientBuilder) { + Assert.notNull(webClientBuilder, "webClientBuilder cannot be null"); + this.webClientBuilder = webClientBuilder; + return this; + } + + public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) { + Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null"); + this.responseErrorHandler = responseErrorHandler; + return this; + } + + public AudioApi build() { + Assert.notNull(this.apiKey, "apiKey must be set"); + return new AudioApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder, + this.webClientBuilder, this.responseErrorHandler); + } + + } + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioModel.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioModel.java new file mode 100644 index 0000000000000000000000000000000000000000..92b64a515a53019c70bfdf500cd8cca4018e04df --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioModel.java @@ -0,0 +1,137 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.audio; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.audio.tts.*; +import org.springframework.http.ResponseEntity; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; + +import java.util.List; + +@Slf4j +public class AudioModel implements TextToSpeechModel { + + /** + * The default options used for the audio completion requests. + */ + private final AudioOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Audio API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Low-level access to the OpenAI Audio API. + */ + private final AudioApi audioApi; + + + /** + * Initializes a new instance of the AudioModel class with the provided + * OpenAiAudioApi and options. + * @param audioApi The OpenAiAudioApi to use for speech synthesis. + * @param options The AudioOptions containing the speech synthesis + * options. + * @param retryTemplate The retry template. + */ + public AudioModel(AudioApi audioApi, AudioOptions options, + RetryTemplate retryTemplate) { + Assert.notNull(audioApi, "OpenAiAudioApi must not be null"); + Assert.notNull(options, "OpenAiSpeechOptions must not be null"); + Assert.notNull(options, "RetryTemplate must not be null"); + this.audioApi = audioApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + } + + @Override + public byte[] call(String text) { + TextToSpeechPrompt prompt = new TextToSpeechPrompt(text); + return call(prompt).getResult().getOutput(); + } + + @Override + public TextToSpeechResponse call(TextToSpeechPrompt prompt) { + + AudioApi.SpeechRequest speechRequest = createRequest(prompt); + + ResponseEntity speechEntity = this.retryTemplate.execute( + (ctx) -> this.audioApi.createSpeech(speechRequest)); + + var speech = speechEntity.getBody(); + + return new TextToSpeechResponse(List.of(new Speech(speech))); + } + + /** + * Streams the audio response for the given speech prompt. + * @param prompt The speech prompt containing the text and options for speech + * synthesis. + * @return A Flux of TextToSpeechResponse objects containing the streamed audio and + * metadata. + */ + @Override + public Flux stream(TextToSpeechPrompt prompt) { + + AudioApi.SpeechRequest speechRequest = createRequest(prompt); + + Flux> speechEntity = this.retryTemplate.execute( + (ctx) -> this.audioApi.stream(speechRequest)); + + return speechEntity.map(entity -> new TextToSpeechResponse(List.of(new Speech(entity.getBody())))); + } + + private AudioApi.SpeechRequest createRequest(TextToSpeechPrompt prompt) { + AudioOptions options = this.defaultOptions; + + String input = StringUtils.hasText(options.getInput()) ? options.getInput() + : prompt.getInstructions().getText(); + + AudioApi.SpeechRequest.Builder requestBuilder = AudioApi.SpeechRequest.builder() + .model(options.getModel()) + .input(input) + .voice(options.getVoice()) + .responseFormat(options.getResponseFormat()) + .speed(options.getSpeed()); + + return requestBuilder.build(); + } + + @Override + public TextToSpeechOptions getDefaultOptions() { + return this.defaultOptions; + } + + private AudioOptions merge(AudioOptions source, AudioOptions target) { + AudioOptions.Builder mergedBuilder = AudioOptions.builder(); + + mergedBuilder.model(source.getModel() != null ? source.getModel() : target.getModel()); + mergedBuilder.input(source.getInput() != null ? source.getInput() : target.getInput()); + mergedBuilder.voice(source.getVoice() != null ? source.getVoice() : target.getVoice()); + mergedBuilder.responseFormat( + source.getResponseFormat() != null ? source.getResponseFormat() : target.getResponseFormat()); + mergedBuilder.speed(source.getSpeed() != null ? source.getSpeed() : target.getSpeed()); + + return mergedBuilder.build(); + } + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioOptions.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..27e50243ffe509c9dab402c3d38beb81d99531c5 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/audio/AudioOptions.java @@ -0,0 +1,187 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.audio; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.springframework.ai.audio.tts.TextToSpeechOptions; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class AudioOptions implements TextToSpeechOptions { + + @JsonProperty("model") + private String model; + + @JsonProperty("input") + private String input; + + @JsonProperty("voice") + private String voice; + + /** + * The format of the audio output. Supported formats are mp3, opus, aac, and flac. + * Defaults to mp3. + */ + @JsonProperty("response_format") + private String responseFormat; + + /** + * The speed of the voice synthesis. The acceptable range is from 0.25 (slowest) to + * 4.0 (fastest). Defaults to 1 (normal) + */ + @JsonProperty("speed") + private Double speed; + @JsonProperty("watermark_enabled") + private boolean watermark; + + public static Builder builder() { + return new Builder(); + } + + @Override + public String getFormat() { + return null; + } + + + @Override + @SuppressWarnings("unchecked") + public AudioOptions copy() { + return AudioOptions.builder() + .model(this.model) + .input(this.input) + .voice(this.voice) + .responseFormat(this.responseFormat) + .speed(this.speed) + .build(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((this.model == null) ? 0 : this.model.hashCode()); + result = prime * result + ((this.input == null) ? 0 : this.input.hashCode()); + result = prime * result + ((this.voice == null) ? 0 : this.voice.hashCode()); + result = prime * result + ((this.responseFormat == null) ? 0 : this.responseFormat.hashCode()); + result = prime * result + ((this.speed == null) ? 0 : this.speed.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AudioOptions other = (AudioOptions) obj; + if (this.model == null) { + if (other.model != null) { + return false; + } + } + else if (!this.model.equals(other.model)) { + return false; + } + if (this.input == null) { + if (other.input != null) { + return false; + } + } + else if (!this.input.equals(other.input)) { + return false; + } + if (this.voice == null) { + if (other.voice != null) { + return false; + } + } + else if (!this.voice.equals(other.voice)) { + return false; + } + if (this.responseFormat == null) { + if (other.responseFormat != null) { + return false; + } + } + else if (!this.responseFormat.equals(other.responseFormat)) { + return false; + } + if (this.speed == null) { + return other.speed == null; + } + else { + return this.speed.equals(other.speed); + } + } + + @Override + public String toString() { + return "AudioOptions{" + "model='" + this.model + '\'' + ", input='" + this.input + '\'' + + ", voice='" + this.voice + '\'' + ", responseFormat='" + this.responseFormat + '\'' + ", speed=" + + this.speed + '}'; + } + + public static final class Builder { + + private final AudioOptions options = new AudioOptions(); + + public Builder model(String model) { + this.options.model = model; + return this; + } + + public Builder input(String input) { + this.options.input = input; + return this; + } + + + public Builder voice(String voice) { + this.options.voice = voice; + return this; + } + + public Builder responseFormat(String responseFormat) { + this.options.responseFormat = responseFormat; + return this; + } + + public Builder speed(Double speed) { + this.options.speed = speed; + return this; + } + + public Builder watermark(boolean watermark) { + this.options.watermark = watermark; + return this; + } + + public AudioOptions build() { + return this.options; + } + + } + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoApi.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoApi.java new file mode 100644 index 0000000000000000000000000000000000000000..1f2e17f7adc947c999d668569ddcd2ef3b28d005 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoApi.java @@ -0,0 +1,225 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.video; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Data; +import org.springframework.ai.model.ApiKey; +import org.springframework.ai.model.NoopApiKey; +import org.springframework.ai.model.SimpleApiKey; +import org.springframework.ai.retry.RetryUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClient; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import xyz.thoughtset.viewer.common.ai.model.video.ReqInfo; +import xyz.thoughtset.viewer.models.zhipuai.api.ApiConstants; + +import java.util.function.Consumer; + +public class VideoApi { + + private final RestClient restClient; + + private final WebClient webClient; + + /** + * Create a new audio api. + * @param baseUrl api base URL. + * @param apiKey OpenAI apiKey. + * @param headers the http headers to use. + * @param restClientBuilder RestClient builder. + * @param webClientBuilder WebClient builder. + * @param responseErrorHandler Response error handler. + */ + public VideoApi(String baseUrl, ApiKey apiKey, HttpHeaders headers, RestClient.Builder restClientBuilder, + WebClient.Builder webClientBuilder, ResponseErrorHandler responseErrorHandler) { + if (!StringUtils.hasText(baseUrl)) { + baseUrl = ApiConstants.DEFAULT_BASE_URL; + } + Consumer authHeaders = h -> h.addAll(HttpHeaders.readOnlyHttpHeaders(headers)); + + // @formatter:off + this.restClient = restClientBuilder.clone() + .baseUrl(baseUrl) + .defaultHeaders(authHeaders) + .defaultStatusHandler(responseErrorHandler) + .defaultRequest(requestHeadersSpec -> { + if (!(apiKey instanceof NoopApiKey)) { + requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue()); + } + }) + .build(); + + this.webClient = webClientBuilder.clone() + .baseUrl(baseUrl) + .defaultHeaders(authHeaders) + .defaultRequest(requestHeadersSpec -> { + if (!(apiKey instanceof NoopApiKey)) { + requestHeadersSpec.header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey.getValue()); + } + }) + .build(); // @formatter:on + } + + + public static Builder builder() { + return new Builder(); + } + + /** + * Request to generates audio from the input text. + * @param requestBody The request body. + * @return Response entity containing the audio binary. + */ + public ResponseEntity createReq(VideoRequest requestBody) { + return this.restClient.post().uri(requestBody.reqUrl()).body(requestBody).retrieve().toEntity(VideoResp.class); + } + + /** + * Streams audio generated from the input text. + *

+ * This method sends a POST request to the OpenAI API to generate audio from the + * provided text. The audio is streamed back as a Flux of ResponseEntity objects, each + * containing a byte array of the audio data. + * @param requestBody The request body containing the details for the audio + * generation, such as the input text, model, voice, and response format. + * @return A Flux of ResponseEntity objects, each containing a byte array of the audio + * data. + */ + public Flux> stream(VideoRequest requestBody) { + + return this.webClient.post() + .uri(requestBody.reqUrl()) + .body(Mono.just(requestBody), VideoRequest.class) + .accept(MediaType.APPLICATION_OCTET_STREAM) + .exchangeToFlux(clientResponse -> { + HttpHeaders headers = clientResponse.headers().asHttpHeaders(); + return clientResponse.bodyToFlux(VideoResp.class) + .map(bytes -> ResponseEntity.ok().headers(headers).body(bytes)); + }); + } + + + + @lombok.Builder + @JsonInclude(Include.NON_NULL) + public record VideoRequest( + @JsonProperty("model") String model, + @JsonProperty("prompt") String prompt, + @JsonProperty("quality") String quality, + @JsonProperty("with_audio") Boolean withAudio, + @JsonProperty("watermark_enabled") Boolean watermarkEnabled, + @JsonProperty("size") String size, + @JsonProperty("fps") Integer fps, + @JsonProperty("duration") Integer duration) implements ReqInfo { + + @Override + public String reqUrl() { + return ApiConstants.VIDEO_URL; + } + + } + + + @JsonInclude(Include.NON_NULL) + @JsonIgnoreProperties(ignoreUnknown = true) + public record VideoResp( + @JsonProperty("model") String model, + @JsonProperty("id") String id, + @JsonProperty("request_id") String requestId, + @JsonProperty("task_status") String taskStatus) { + + } + + + + public static final class Builder { + + private String baseUrl = ApiConstants.DEFAULT_BASE_URL; + + private ApiKey apiKey; + + private HttpHeaders headers = new HttpHeaders(); + + private RestClient.Builder restClientBuilder = RestClient.builder(); + + private WebClient.Builder webClientBuilder = WebClient.builder(); + + private ResponseErrorHandler responseErrorHandler = RetryUtils.DEFAULT_RESPONSE_ERROR_HANDLER; + + public Builder baseUrl(String baseUrl) { + Assert.hasText(baseUrl, "baseUrl cannot be null or empty"); + this.baseUrl = baseUrl; + return this; + } + + public Builder apiKey(ApiKey apiKey) { + Assert.notNull(apiKey, "apiKey cannot be null"); + this.apiKey = apiKey; + return this; + } + + public Builder apiKey(String simpleApiKey) { + Assert.notNull(simpleApiKey, "simpleApiKey cannot be null"); + this.apiKey = new SimpleApiKey(simpleApiKey); + return this; + } + + public Builder headers(HttpHeaders headers) { + Assert.notNull(headers, "headers cannot be null"); + this.headers = headers; + return this; + } + + public Builder restClientBuilder(RestClient.Builder restClientBuilder) { + Assert.notNull(restClientBuilder, "restClientBuilder cannot be null"); + this.restClientBuilder = restClientBuilder; + return this; + } + + public Builder webClientBuilder(WebClient.Builder webClientBuilder) { + Assert.notNull(webClientBuilder, "webClientBuilder cannot be null"); + this.webClientBuilder = webClientBuilder; + return this; + } + + public Builder responseErrorHandler(ResponseErrorHandler responseErrorHandler) { + Assert.notNull(responseErrorHandler, "responseErrorHandler cannot be null"); + this.responseErrorHandler = responseErrorHandler; + return this; + } + + public VideoApi build() { + Assert.notNull(this.apiKey, "apiKey must be set"); + return new VideoApi(this.baseUrl, this.apiKey, this.headers, this.restClientBuilder, + this.webClientBuilder, this.responseErrorHandler); + } + + } + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoModel.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoModel.java new file mode 100644 index 0000000000000000000000000000000000000000..c3c2b41a27b3b679783512f324ca8b819b37b3f6 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoModel.java @@ -0,0 +1,116 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.video; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.audio.tts.*; +import org.springframework.ai.model.Model; +import org.springframework.ai.model.StreamingModel; +import org.springframework.beans.BeanUtils; +import org.springframework.http.ResponseEntity; +import org.springframework.retry.support.RetryTemplate; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import xyz.thoughtset.viewer.common.ai.model.video.AsyncVideo; +import xyz.thoughtset.viewer.common.ai.model.video.VideoPrompt; + +@Slf4j +public class VideoModel implements Model, StreamingModel { + + /** + * The default options used for the audio completion requests. + */ + private final VideoOptions defaultOptions; + + /** + * The retry template used to retry the OpenAI Audio API calls. + */ + private final RetryTemplate retryTemplate; + + /** + * Low-level access to the OpenAI Audio API. + */ + private final VideoApi videoApi; + + + + public VideoModel(VideoApi videoApi, VideoOptions options, + RetryTemplate retryTemplate) { + Assert.notNull(videoApi, "OpenAiAudioApi must not be null"); + Assert.notNull(options, "OpenAiSpeechOptions must not be null"); + Assert.notNull(options, "RetryTemplate must not be null"); + this.videoApi = videoApi; + this.defaultOptions = options; + this.retryTemplate = retryTemplate; + } + + public String call(String text) { + VideoPrompt prompt = new VideoPrompt(text); + return call(prompt).getResult().getOutput(); + } + + + @Override + public VideoResponse call(VideoPrompt prompt) { + + VideoApi.VideoRequest speechRequest = createRequest(prompt); + + ResponseEntity respEntity = this.retryTemplate.execute( + (ctx) -> this.videoApi.createReq(speechRequest)); + + return new VideoResponse(new AsyncVideo(respEntity.getBody().id())); + } + + /** + * Streams the audio response for the given speech prompt. + * @param prompt The speech prompt containing the text and options for speech + * synthesis. + * @return A Flux of TextToSpeechResponse objects containing the streamed audio and + * metadata. + */ + @Override + public Flux stream(VideoPrompt prompt) { + + VideoApi.VideoRequest speechRequest = createRequest(prompt); + + Flux> entityFlux = this.retryTemplate.execute( + (ctx) -> this.videoApi.stream(speechRequest)); + + return entityFlux.map(entity -> new VideoResponse(new AsyncVideo(entity.getBody().id()))); + } + + private VideoApi.VideoRequest createRequest(VideoPrompt prompt) { + VideoOptions options = this.defaultOptions; + + String input = StringUtils.hasText(options.getPrompt()) ? options.getPrompt() + : prompt.getInstructions(); + + return VideoApi.VideoRequest.builder() + .model(options.getModel()) + .prompt(input) + .quality(options.getQuality()) + .withAudio(options.getWithAudio()) + .watermarkEnabled(options.getWatermarkEnabled()) + .size(options.getSize()) + .fps(options.getFps()) + .duration(options.getDuration()) + .build(); + } + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoOptions.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoOptions.java new file mode 100644 index 0000000000000000000000000000000000000000..5815db6ee87ddba6a7c478607f71d27b44e921f7 --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoOptions.java @@ -0,0 +1,40 @@ +/* + * Copyright 2023-2025 the original author or authors. + * + * Licensed under the Apache License; Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing; software + * distributed under the License is distributed on an "AS IS" BASIS; + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND; either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.video; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import org.springframework.ai.audio.tts.TextToSpeechOptions; +import org.springframework.ai.model.ModelOptions; +import xyz.thoughtset.viewer.common.ai.model.video._BaseVideoOptions; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class VideoOptions implements _BaseVideoOptions { + + protected String model; + protected String prompt; + protected String quality; + protected Boolean withAudio; + protected Boolean watermarkEnabled; + protected String size; + protected Integer fps; + protected Integer duration; + + +} diff --git a/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoResponse.java b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..315e18d87e65fce294d285a8b530b14518441d2b --- /dev/null +++ b/models/viewer-models-zhipuai/src/main/java/xyz/thoughtset/viewer/models/zhipuai/api/video/VideoResponse.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package xyz.thoughtset.viewer.models.zhipuai.api.video; + +import org.springframework.ai.audio.tts.TextToSpeechResponse; +import org.springframework.ai.model.ModelResponse; +import xyz.thoughtset.viewer.common.ai.model.video.AsyncVideo; +import xyz.thoughtset.viewer.common.ai.model.video.VideoResponseMetadata; + +import java.util.List; +import java.util.Objects; + + +public class VideoResponse implements ModelResponse { + + private final AsyncVideo result; + + private final VideoResponseMetadata videoResponseMetadata; + + public VideoResponse(AsyncVideo result) { + this(result, null); + } + + public VideoResponse(AsyncVideo result, VideoResponseMetadata videoResponseMetadata) { + this.result = result; + this.videoResponseMetadata = videoResponseMetadata; + } + + @Override + public List getResults() { + return List.of(this.result); + } + + public AsyncVideo getResult() { + return this.result; + } + + @Override + public VideoResponseMetadata getMetadata() { + return this.videoResponseMetadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof VideoResponse that)) { + return false; + } + return Objects.equals(this.result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash(this.result); + } + + @Override + public String toString() { + return "VideoResponseMetadata{" + "results=" + this.result + '}'; + } + +} diff --git a/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/DsConfigServiceImpl.java b/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/DsConfigServiceImpl.java index 7651486dfd8a0e04f3355387c3695de88bc5766f..e2d5678f0fa9b62f1b43d56cb59dada85a5b2c33 100644 --- a/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/DsConfigServiceImpl.java +++ b/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/DsConfigServiceImpl.java @@ -12,7 +12,6 @@ import xyz.thoughtset.viewer.modules.ds.core.factory.ConnectFactory; import javax.sql.DataSource; import java.util.LinkedHashMap; import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; @Service @Transactional @@ -27,7 +26,7 @@ public class DsConfigServiceImpl extends BaseServiceImpl Object settings = baseMap.remove(settingsKey); DsConfig o = convertValue(baseMap); if (Objects.nonNull(settings)){ - o.setOtherSettings(mapper.writeValueAsString(settings)); + o.setOtherSettings(objectMapper.writeValueAsString(settings)); } saveOrUpdate(o); return o; @@ -51,7 +50,7 @@ public class DsConfigServiceImpl extends BaseServiceImpl // if(StringUtils.hasText(settingsStr)){ // LinkedHashMap settings = null; // try { -// settings = mapper.readValue(settingsStr, LinkedHashMap.class); +// settings = objectMapper.readValue(settingsStr, LinkedHashMap.class); // } catch (JsonProcessingException e) { // throw new RuntimeException(e); // } diff --git a/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/LinkerConfigServiceImpl.java b/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/LinkerConfigServiceImpl.java index 50e2c24f771c9d3a03487c1f1b59d344840dad3d..0c03e6f89a90126fe489688585e3831331ffc676 100644 --- a/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/LinkerConfigServiceImpl.java +++ b/modules/viewer-modules-ds/viewer-modules-ds-core/src/main/java/xyz/thoughtset/viewer/modules/ds/core/service/LinkerConfigServiceImpl.java @@ -21,7 +21,7 @@ public class LinkerConfigServiceImpl extends BaseServiceImpl params; @TableField(exist = false) private Map> groupParams; diff --git a/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/service/BlockInfoServiceImpl.java b/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/service/BlockInfoServiceImpl.java index fb43667cd4a3c418d19654479b21f79ffcc997d1..c1e945289d624a92eaf9f4a8a301a4a13175cf9a 100644 --- a/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/service/BlockInfoServiceImpl.java +++ b/modules/viewer-modules-step/src/main/java/xyz/thoughtset/viewer/modules/step/service/BlockInfoServiceImpl.java @@ -1,7 +1,6 @@ package xyz.thoughtset.viewer.modules.step.service; import cn.zhxu.bs.BeanSearcher; -import cn.zhxu.bs.MapSearcher; import cn.zhxu.bs.util.MapUtils; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.databind.JavaType; @@ -18,7 +17,6 @@ import xyz.thoughtset.viewer.modules.step.entity.BlockTypeEnum; import xyz.thoughtset.viewer.modules.step.entity.block.BlockBodyEle; import xyz.thoughtset.viewer.modules.step.entity.block.BlockInfo; import xyz.thoughtset.viewer.modules.step.entity.block.EleParam; -import xyz.thoughtset.viewer.modules.step.entity.vo.BodyEleView; import java.io.Serializable; import java.util.*; @@ -36,7 +34,7 @@ public class BlockInfoServiceImpl extends BaseServiceImpl4.11.0 0.2.25 3.2.0 - 1.1.0 - 0.16.0 + 1.1.2 + 0.17.0