refactor: support biflowable & audio refactor (#9)
* refactor: update knowledge service and response structure * feat: Enhance audio service with bi-streaming support and response models - Introduced BiFlowableClientResponse interface to handle responses with separate body and stream item types. - Updated ZaiClient to support bi-stream requests, allowing for more flexible streaming API interactions. - Refactored AudioApi to rename methods for consistency and clarity, including audio transcription methods. - Created new response classes: AudioCustomizationResponse, AudioSpeechResponse, and AudioTranscriptionResponse to encapsulate response data. - Implemented AudioTranscriptionChunk and AudioTranscriptionResult classes for structured transcription results. - Removed obsolete classes: AudioCustomizationApiResponse and AudioSpeechApiResponse. - Updated AudioService and AudioServiceImpl to utilize new response models and handle streaming transcriptions. - Enhanced unit tests for AudioService to reflect changes in method signatures and response types. --------- Co-authored-by: tomsun28 <tomsun28@outlook.com>
This commit is contained in:
parent
a4de5d46e3
commit
e3c878b90e
19 changed files with 347 additions and 189 deletions
|
|
@ -30,6 +30,7 @@ import ai.z.openapi.service.document.DocumentServiceImpl;
|
|||
import ai.z.openapi.service.assistant.AssistantService;
|
||||
import ai.z.openapi.service.assistant.AssistantServiceImpl;
|
||||
import ai.z.openapi.core.config.ZaiConfig;
|
||||
import ai.z.openapi.core.model.BiFlowableClientResponse;
|
||||
import ai.z.openapi.core.model.ClientRequest;
|
||||
import ai.z.openapi.core.model.ClientResponse;
|
||||
import ai.z.openapi.core.model.FlowableClientResponse;
|
||||
|
|
@ -376,19 +377,55 @@ public class ZaiClient extends AbstractClientBaseService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Executes a streaming API request and returns a response containing a Flowable
|
||||
* stream. This method is used for requests that return data as a continuous stream,
|
||||
* such as chat completions with streaming enabled.
|
||||
* @param <Data> the type of data expected in each stream element
|
||||
* @param <Param> the type of parameters for the request
|
||||
* @param <TReq> the type of client request
|
||||
* @param <TResp> the type of flowable client response
|
||||
* @param request the client request containing parameters
|
||||
* @param requestSupplier the supplier that creates the actual streaming API call
|
||||
* @param tRespClass the class of the response type
|
||||
* @param tDataClass the class of the data type for stream elements
|
||||
* @return the wrapped response containing either a success stream or error
|
||||
* information
|
||||
* Executes a streaming API request and returns a BiFlowableClientResponse where
|
||||
* response body and stream element type can differ.
|
||||
* @param request the request object
|
||||
* @param requestSupplier the streaming API supplier
|
||||
* @param tRespClass the client response class (must implement
|
||||
* BiFlowableClientResponse<Data, F>)
|
||||
* @param tStreamDataClass the stream data element class
|
||||
* @return a response containing a Flowable<F> stream
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public <Data, F, Param, TReq extends ClientRequest<Param>, TResp extends BiFlowableClientResponse<Data, F>> TResp biStreamRequest(
|
||||
TReq request, FlowableRequestSupplier<Param, retrofit2.Call<ResponseBody>> requestSupplier,
|
||||
Class<TResp> tRespClass, Class<F> tStreamDataClass) {
|
||||
retrofit2.Call<ResponseBody> apiCall = requestSupplier.get((Param) request);
|
||||
TResp tResp = convertToClientResponse(tRespClass);
|
||||
|
||||
try {
|
||||
Flowable<F> stream = stream(apiCall, tStreamDataClass);
|
||||
tResp.setCode(200);
|
||||
tResp.setMsg("Stream initialized successfully");
|
||||
tResp.setSuccess(true);
|
||||
tResp.setFlowable(stream);
|
||||
}
|
||||
catch (ZAiHttpException e) {
|
||||
handleStreamError(tResp, e);
|
||||
}
|
||||
return tResp;
|
||||
}
|
||||
|
||||
private void handleStreamError(ClientResponse<?> response, ZAiHttpException e) {
|
||||
logger.error("Streaming API request failed with business error", e);
|
||||
response.setCode(e.statusCode);
|
||||
response.setMsg("Business error");
|
||||
response.setSuccess(false);
|
||||
ChatError chatError = new ChatError();
|
||||
chatError.setCode(Integer.parseInt(e.code));
|
||||
chatError.setMessage(e.getMessage());
|
||||
response.setError(chatError);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a streaming API request and returns a FlowableClientResponse with stream
|
||||
* elements of type Data.
|
||||
* @param request the request object
|
||||
* @param requestSupplier the streaming API supplier
|
||||
* @param tRespClass the client response class (must implement
|
||||
* FlowableClientResponse<Data>)
|
||||
* @param tDataClass the class representing stream data type
|
||||
* @return a response containing a Flowable<Data> stream
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
@ -396,10 +433,9 @@ public class ZaiClient extends AbstractClientBaseService {
|
|||
TReq request, FlowableRequestSupplier<Param, retrofit2.Call<ResponseBody>> requestSupplier,
|
||||
Class<TResp> tRespClass, Class<Data> tDataClass) {
|
||||
retrofit2.Call<ResponseBody> apiCall = requestSupplier.get((Param) request);
|
||||
|
||||
TResp tResp = convertToClientResponse(tRespClass);
|
||||
|
||||
try {
|
||||
// Create a streaming response using the provided API call
|
||||
Flowable<Data> stream = stream(apiCall, tDataClass);
|
||||
tResp.setCode(200);
|
||||
tResp.setMsg("Stream initialized successfully");
|
||||
|
|
@ -407,14 +443,7 @@ public class ZaiClient extends AbstractClientBaseService {
|
|||
tResp.setFlowable(stream);
|
||||
}
|
||||
catch (ZAiHttpException e) {
|
||||
logger.error("Streaming API request failed with business error", e);
|
||||
tResp.setCode(e.statusCode);
|
||||
tResp.setMsg("Business error");
|
||||
tResp.setSuccess(false);
|
||||
ChatError chatError = new ChatError();
|
||||
chatError.setCode(Integer.parseInt(e.code));
|
||||
chatError.setMessage(e.getMessage());
|
||||
tResp.setError(chatError);
|
||||
handleStreamError(tResp, e);
|
||||
}
|
||||
return tResp;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
package ai.z.openapi.api.audio;
|
||||
|
||||
import ai.z.openapi.service.audio.AudioSpeechRequest;
|
||||
import ai.z.openapi.service.model.ModelData;
|
||||
import ai.z.openapi.service.audio.AudioTranscriptionResult;
|
||||
import io.reactivex.Single;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
|
|
@ -70,7 +70,7 @@ public interface AudioApi {
|
|||
@Streaming
|
||||
@POST("audio/transcriptions")
|
||||
@Multipart
|
||||
Call<ResponseBody> audioTranscriptionsStream(@PartMap Map<String, RequestBody> request,
|
||||
Call<ResponseBody> audioTranscriptionStream(@PartMap Map<String, RequestBody> request,
|
||||
@Part MultipartBody.Part file);
|
||||
|
||||
/**
|
||||
|
|
@ -87,6 +87,7 @@ public interface AudioApi {
|
|||
*/
|
||||
@POST("audio/transcriptions")
|
||||
@Multipart
|
||||
Single<ModelData> audioTranscriptions(@PartMap Map<String, RequestBody> request, @Part MultipartBody.Part file);
|
||||
Single<AudioTranscriptionResult> audioTranscription(@PartMap Map<String, RequestBody> request,
|
||||
@Part MultipartBody.Part file);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,11 +66,13 @@ public class ZaiConfig {
|
|||
/**
|
||||
* JWT token expiration time in milliseconds (default: 30 minutes).
|
||||
*/
|
||||
@Builder.Default
|
||||
private int expireMillis = 30 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* JWT encryption algorithm (default: HS256).
|
||||
*/
|
||||
@Builder.Default
|
||||
private String alg = "HS256";
|
||||
|
||||
/**
|
||||
|
|
@ -81,46 +83,55 @@ public class ZaiConfig {
|
|||
/**
|
||||
* Maximum number of idle connections in the connection pool.
|
||||
*/
|
||||
@Builder.Default
|
||||
private int connectionPoolMaxIdleConnections = 5;
|
||||
|
||||
/**
|
||||
* Keep alive duration for connections in the pool (in seconds).
|
||||
*/
|
||||
@Builder.Default
|
||||
private long connectionPoolKeepAliveDuration = 1;
|
||||
|
||||
/**
|
||||
* Time unit for connection pool keep alive duration.
|
||||
*/
|
||||
@Builder.Default
|
||||
private TimeUnit connectionPoolTimeUnit = TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* Request timeout in specified time unit.
|
||||
*/
|
||||
@Builder.Default
|
||||
private int requestTimeOut = 300;
|
||||
|
||||
/**
|
||||
* Connection timeout in specified time unit.
|
||||
*/
|
||||
@Builder.Default
|
||||
private int connectTimeout = 100;
|
||||
|
||||
/**
|
||||
* Read timeout in specified time unit.
|
||||
*/
|
||||
@Builder.Default
|
||||
private int readTimeout = 100;
|
||||
|
||||
/**
|
||||
* Write timeout in specified time unit.
|
||||
*/
|
||||
@Builder.Default
|
||||
private int writeTimeout = 100;
|
||||
|
||||
/**
|
||||
* Time unit for timeout configurations.
|
||||
*/
|
||||
@Builder.Default
|
||||
private TimeUnit timeOutTimeUnit = TimeUnit.SECONDS;
|
||||
|
||||
/**
|
||||
* Source channel identifier for request tracking.
|
||||
*/
|
||||
@Builder.Default
|
||||
private String source_channel = "java-sdk";
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package ai.z.openapi.core.model;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
/**
|
||||
* A client response that supports a reactive Flowable stream with separate body type and
|
||||
* stream item type.
|
||||
*
|
||||
* @param <T> Response body type
|
||||
* @param <F> Flowable stream item type
|
||||
*/
|
||||
public interface BiFlowableClientResponse<T, F> extends ClientResponse<T> {
|
||||
|
||||
void setFlowable(Flowable<F> stream);
|
||||
|
||||
Flowable<F> getFlowable();
|
||||
|
||||
}
|
||||
|
|
@ -1,19 +1,13 @@
|
|||
package ai.z.openapi.core.model;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
|
||||
/**
|
||||
* Client response interface with reactive stream support. Extends ClientResponse to
|
||||
* provide Flowable stream functionality.
|
||||
* Simplified client response with a Flowable stream where the body type and stream item
|
||||
* type are the same.
|
||||
*
|
||||
* @param <T> response data type
|
||||
*/
|
||||
public interface FlowableClientResponse<T> extends ClientResponse<T> {
|
||||
public interface FlowableClientResponse<T> extends BiFlowableClientResponse<T, T> {
|
||||
|
||||
/**
|
||||
* Sets the reactive stream for this response.
|
||||
* @param stream Flowable stream containing response data
|
||||
*/
|
||||
void setFlowable(Flowable<T> stream);
|
||||
// No additional methods needed
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package ai.z.openapi.service;
|
||||
|
||||
import ai.z.openapi.core.model.BiFlowableClientResponse;
|
||||
import ai.z.openapi.core.model.ClientRequest;
|
||||
import ai.z.openapi.core.model.ClientResponse;
|
||||
import ai.z.openapi.core.model.FlowableClientResponse;
|
||||
|
|
@ -51,20 +52,38 @@ public abstract class AbstractClientBaseService {
|
|||
TReq request, RequestSupplier<Param, Data> requestSupplier, Class<TResp> tRespClass);
|
||||
|
||||
/**
|
||||
* Executes a streaming API request that returns a continuous stream of data.
|
||||
* @param <Data> the type of data returned by the API stream
|
||||
* @param <Param> the type of parameters sent to the API
|
||||
* @param <TReq> the type of the request object
|
||||
* @param <TResp> the type of the streaming response object
|
||||
* @param request the request object containing parameters
|
||||
* @param requestSupplier the supplier that creates the streaming API call
|
||||
* @param tRespClass the class of the response type
|
||||
* @param tDataClass the class of the data type in the stream
|
||||
* @return the streaming response object containing the API result stream
|
||||
* Executes a streaming API request and returns a Flowable-based response. This
|
||||
* unified version supports both: - simplified response where Data == stream element
|
||||
* type (FlowableClientResponse<Data>) - and bi-type response where Data ≠ stream
|
||||
* element type (BiFlowableClientResponse<Data, F>)
|
||||
* @param <Data> type of response body
|
||||
* @param <F> type of each element in the stream (can be same as Data)
|
||||
* @param <Param> request param type
|
||||
* @param <TReq> request type
|
||||
* @param <TResp> response type (must extend BiFlowableClientResponse<Data, F>)
|
||||
* @param request the request to send
|
||||
* @param requestSupplier factory that creates the Retrofit call
|
||||
* @param tRespClass the response class type
|
||||
* @param tStreamClass the class of the stream element
|
||||
* @return streaming client response
|
||||
*/
|
||||
public abstract <Data, Param, TReq extends ClientRequest<Param>, TResp extends FlowableClientResponse<Data>> TResp streamRequest(
|
||||
public abstract <Data, F, Param, TReq extends ClientRequest<Param>, TResp extends BiFlowableClientResponse<Data, F>> TResp biStreamRequest(
|
||||
TReq request, FlowableRequestSupplier<Param, Call<ResponseBody>> requestSupplier, Class<TResp> tRespClass,
|
||||
Class<Data> tDataClass);
|
||||
Class<F> tStreamClass);
|
||||
|
||||
/**
|
||||
* Executes a streaming API request with the same type for response body and stream
|
||||
* element.
|
||||
* @param <T> data type for both response and stream
|
||||
* @param <Param> request param type
|
||||
* @param <TReq> request type
|
||||
* @param <TResp> response type (must extend FlowableClientResponse<T>)
|
||||
*/
|
||||
public <T, Param, TReq extends ClientRequest<Param>, TResp extends FlowableClientResponse<T>> TResp streamRequest(
|
||||
TReq request, FlowableRequestSupplier<Param, Call<ResponseBody>> requestSupplier, Class<TResp> tRespClass,
|
||||
Class<T> tClass) {
|
||||
return this.<T, T, Param, TReq, TResp>biStreamRequest(request, requestSupplier, tRespClass, tClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a Single API call synchronously and handles errors.
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ import lombok.NoArgsConstructor;
|
|||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Request parameters for audio customization API calls. This class contains all the
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import java.io.File;
|
|||
* information.
|
||||
*/
|
||||
@Data
|
||||
public class AudioCustomizationApiResponse implements ClientResponse<File> {
|
||||
public class AudioCustomizationResponse implements ClientResponse<File> {
|
||||
|
||||
/**
|
||||
* Response status code.
|
||||
|
|
@ -1,7 +1,5 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import ai.z.openapi.service.model.ChatCompletionResponse;
|
||||
|
||||
/**
|
||||
* Audio service interface
|
||||
*/
|
||||
|
|
@ -12,20 +10,20 @@ public interface AudioService {
|
|||
* @param request the speech generation request
|
||||
* @return AudioSpeechApiResponse containing the generated speech
|
||||
*/
|
||||
AudioSpeechApiResponse createSpeech(AudioSpeechRequest request);
|
||||
AudioSpeechResponse createSpeech(AudioSpeechRequest request);
|
||||
|
||||
/**
|
||||
* Creates customized speech with specific voice characteristics.
|
||||
* @param request the speech customization request
|
||||
* @return AudioCustomizationApiResponse containing the customized speech result
|
||||
* @return AudioCustomizationResponse containing the customized speech result
|
||||
*/
|
||||
AudioCustomizationApiResponse createCustomSpeech(AudioCustomizationRequest request);
|
||||
AudioCustomizationResponse createCustomSpeech(AudioCustomizationRequest request);
|
||||
|
||||
/**
|
||||
* Creates audio transcriptions from audio files.
|
||||
* Creates audio transcription from audio files.
|
||||
* @param request the transcription request
|
||||
* @return ChatCompletionResponse containing the transcription result
|
||||
* @return AudioTranscriptionResponse containing the transcription result
|
||||
*/
|
||||
ChatCompletionResponse createTranscription(AudioTranscriptionsRequest request);
|
||||
AudioTranscriptionResponse createTranscription(AudioTranscriptionRequest request);
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package ai.z.openapi.service.audio;
|
|||
import ai.z.openapi.ZaiClient;
|
||||
import ai.z.openapi.api.audio.AudioApi;
|
||||
import ai.z.openapi.service.deserialize.MessageDeserializeFactory;
|
||||
import ai.z.openapi.service.model.ChatCompletionResponse;
|
||||
import ai.z.openapi.service.model.Audio;
|
||||
import ai.z.openapi.service.model.ModelData;
|
||||
import ai.z.openapi.utils.FlowableRequestSupplier;
|
||||
import ai.z.openapi.utils.RequestSupplier;
|
||||
|
|
@ -43,7 +43,7 @@ public class AudioServiceImpl implements AudioService {
|
|||
}
|
||||
|
||||
@Override
|
||||
public AudioSpeechApiResponse createSpeech(AudioSpeechRequest request) {
|
||||
public AudioSpeechResponse createSpeech(AudioSpeechRequest request) {
|
||||
validateSpeechParams(request);
|
||||
RequestSupplier<AudioSpeechRequest, java.io.File> supplier = (params) -> {
|
||||
try {
|
||||
|
|
@ -57,11 +57,11 @@ public class AudioServiceImpl implements AudioService {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
return this.zAiClient.executeRequest(request, supplier, AudioSpeechApiResponse.class);
|
||||
return this.zAiClient.executeRequest(request, supplier, AudioSpeechResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public AudioCustomizationApiResponse createCustomSpeech(AudioCustomizationRequest request) {
|
||||
public AudioCustomizationResponse createCustomSpeech(AudioCustomizationRequest request) {
|
||||
validateCustomSpeechParams(request);
|
||||
RequestSupplier<AudioCustomizationRequest, java.io.File> supplier = (params) -> {
|
||||
try {
|
||||
|
|
@ -114,22 +114,22 @@ public class AudioServiceImpl implements AudioService {
|
|||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
return this.zAiClient.executeRequest(request, supplier, AudioCustomizationApiResponse.class);
|
||||
return this.zAiClient.executeRequest(request, supplier, AudioCustomizationResponse.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChatCompletionResponse createTranscription(AudioTranscriptionsRequest request) {
|
||||
public AudioTranscriptionResponse createTranscription(AudioTranscriptionRequest request) {
|
||||
validateTranscriptionParams(request);
|
||||
if (request.getStream()) {
|
||||
return createTranscriptionStream(request);
|
||||
}
|
||||
else {
|
||||
return createTranscriptionSync(request);
|
||||
return createTranscriptionBlock(request);
|
||||
}
|
||||
}
|
||||
|
||||
private ChatCompletionResponse createTranscriptionStream(AudioTranscriptionsRequest request) {
|
||||
FlowableRequestSupplier<AudioTranscriptionsRequest, retrofit2.Call<ResponseBody>> supplier = params -> {
|
||||
private AudioTranscriptionResponse createTranscriptionStream(AudioTranscriptionRequest request) {
|
||||
FlowableRequestSupplier<AudioTranscriptionRequest, retrofit2.Call<ResponseBody>> supplier = params -> {
|
||||
try {
|
||||
java.io.File file = params.getFile();
|
||||
Tika tika = new Tika();
|
||||
|
|
@ -153,18 +153,19 @@ public class AudioServiceImpl implements AudioService {
|
|||
requestMap.put("user_id", RequestBody.create(MediaType.parse("text/plain"), params.getUserId()));
|
||||
}
|
||||
|
||||
return audioApi.audioTranscriptionsStream(requestMap, fileData);
|
||||
return audioApi.audioTranscriptionStream(requestMap, fileData);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Error create transcription: {}", e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
return this.zAiClient.streamRequest(request, supplier, ChatCompletionResponse.class, ModelData.class);
|
||||
return this.zAiClient.biStreamRequest(request, supplier, AudioTranscriptionResponse.class,
|
||||
AudioTranscriptionChunk.class);
|
||||
}
|
||||
|
||||
private ChatCompletionResponse createTranscriptionSync(AudioTranscriptionsRequest request) {
|
||||
RequestSupplier<AudioTranscriptionsRequest, ModelData> supplier = (params) -> {
|
||||
private AudioTranscriptionResponse createTranscriptionBlock(AudioTranscriptionRequest request) {
|
||||
RequestSupplier<AudioTranscriptionRequest, AudioTranscriptionResult> supplier = (params) -> {
|
||||
try {
|
||||
java.io.File file = params.getFile();
|
||||
Tika tika = new Tika();
|
||||
|
|
@ -188,14 +189,14 @@ public class AudioServiceImpl implements AudioService {
|
|||
requestMap.put("user_id", RequestBody.create(MediaType.parse("text/plain"), params.getUserId()));
|
||||
}
|
||||
|
||||
return audioApi.audioTranscriptions(requestMap, fileData);
|
||||
return audioApi.audioTranscription(requestMap, fileData);
|
||||
}
|
||||
catch (IOException e) {
|
||||
log.error("Error create transcription: {}", e.getMessage(), e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
};
|
||||
return this.zAiClient.executeRequest(request, supplier, ChatCompletionResponse.class);
|
||||
return this.zAiClient.executeRequest(request, supplier, AudioTranscriptionResponse.class);
|
||||
}
|
||||
|
||||
private void validateSpeechParams(AudioSpeechRequest request) {
|
||||
|
|
@ -225,7 +226,7 @@ public class AudioServiceImpl implements AudioService {
|
|||
}
|
||||
}
|
||||
|
||||
private void validateTranscriptionParams(AudioTranscriptionsRequest request) {
|
||||
private void validateTranscriptionParams(AudioTranscriptionRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("request cannot be null");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,9 +10,6 @@ import lombok.EqualsAndHashCode;
|
|||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import lombok.Data;
|
|||
import java.io.File;
|
||||
|
||||
@Data
|
||||
public class AudioSpeechApiResponse implements ClientResponse<File> {
|
||||
public class AudioSpeechResponse implements ClientResponse<File> {
|
||||
|
||||
private int code;
|
||||
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ai.z.openapi.service.model.Choice;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public final class AudioTranscriptionChunk {
|
||||
|
||||
@JsonProperty("choices")
|
||||
private List<Choice> choices;
|
||||
|
||||
private Long created;
|
||||
|
||||
private String model;
|
||||
|
||||
private String id;
|
||||
|
||||
private String type;
|
||||
|
||||
private String delta;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import ai.z.openapi.core.model.ClientRequest;
|
||||
import ai.z.openapi.service.CommonRequest;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class AudioTranscriptionRequest extends CommonRequest implements ClientRequest<AudioTranscriptionRequest> {
|
||||
|
||||
/**
|
||||
* Model code to call (Required)
|
||||
*/
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* Stream parameter for synchronous/asynchronous calls (Optional) Set to false or omit
|
||||
* for synchronous calls. The model returns all content at once after generation is
|
||||
* complete. Default is false. If set to true, the model will return generated content
|
||||
* in chunks via standard Event Stream. When Event Stream ends, a data: [DONE] message
|
||||
* will be returned.
|
||||
*/
|
||||
private Boolean stream;
|
||||
|
||||
/**
|
||||
* Audio file to be transcribed (Required) Supported audio file formats: .wav / .mp3
|
||||
* Specification limits: file size ≤ 25 MB, audio duration ≤ 60 seconds
|
||||
*/
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* Sampling temperature, controls output randomness, must be positive (Optional)
|
||||
* Range: [0.0, 1.0], default value is 0.95 Higher values make output more random and
|
||||
* creative; lower values make output more stable or deterministic It's recommended to
|
||||
* adjust either top_p or temperature parameter based on your use case, but not both
|
||||
* simultaneously
|
||||
*/
|
||||
private Float temperature;
|
||||
|
||||
/**
|
||||
* Unique identifier for each request (Optional) Passed by the client, must be unique.
|
||||
* Used to distinguish each request. If not provided by the client, the platform will
|
||||
* generate one by default.
|
||||
*/
|
||||
@JsonProperty("request_id")
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* Unique ID of the end user (Optional) Helps the platform intervene in illegal
|
||||
* activities, generation of illegal inappropriate information, or other abusive
|
||||
* behaviors by end users. ID length requirement: at least 6 characters, maximum 128
|
||||
* characters.
|
||||
*/
|
||||
@JsonProperty("user_id")
|
||||
private String userId;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import ai.z.openapi.core.model.BiFlowableClientResponse;
|
||||
import ai.z.openapi.service.model.ChatError;
|
||||
import io.reactivex.Flowable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AudioTranscriptionResponse
|
||||
implements BiFlowableClientResponse<AudioTranscriptionResult, AudioTranscriptionChunk> {
|
||||
|
||||
private int code;
|
||||
|
||||
private String msg;
|
||||
|
||||
private boolean success;
|
||||
|
||||
private AudioTranscriptionResult data;
|
||||
|
||||
private Flowable<AudioTranscriptionChunk> flowable;
|
||||
|
||||
private ChatError error;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import ai.z.openapi.service.model.Segment;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public final class AudioTranscriptionResult {
|
||||
|
||||
@JsonProperty("request_id")
|
||||
private String requestId;
|
||||
|
||||
private Long created;
|
||||
|
||||
private String model;
|
||||
|
||||
private String id;
|
||||
|
||||
private String text;
|
||||
|
||||
private List<Segment> segments;
|
||||
|
||||
}
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
package ai.z.openapi.service.audio;
|
||||
|
||||
import ai.z.openapi.core.model.ClientRequest;
|
||||
import ai.z.openapi.service.CommonRequest;
|
||||
import ai.z.openapi.service.model.SensitiveWordCheckRequest;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
public class AudioTranscriptionsRequest extends CommonRequest implements ClientRequest<AudioTranscriptionsRequest> {
|
||||
|
||||
/**
|
||||
* Model code to call
|
||||
*/
|
||||
private String model;
|
||||
|
||||
/**
|
||||
* Synchronous call: false, SSE call: true
|
||||
*/
|
||||
private Boolean stream;
|
||||
|
||||
private File file;
|
||||
|
||||
/**
|
||||
* Sampling temperature, controls output randomness, must be positive Range:
|
||||
* (0.0,1.0], cannot equal 0, default value is 0.95 Higher values make output more
|
||||
* random and creative; lower values make output more stable or deterministic It's
|
||||
* recommended to adjust either top_p or temperature parameter based on your use case,
|
||||
* but not both simultaneously
|
||||
*/
|
||||
private Float temperature;
|
||||
|
||||
/**
|
||||
* Sensitive word detection control
|
||||
*/
|
||||
@JsonProperty("sensitive_word_check")
|
||||
private SensitiveWordCheckRequest sensitiveWordCheck;
|
||||
|
||||
}
|
||||
|
|
@ -3,7 +3,6 @@ package ai.z.openapi.service.audio;
|
|||
import ai.z.openapi.ZaiClient;
|
||||
import ai.z.openapi.core.Constants;
|
||||
import ai.z.openapi.core.config.ZaiConfig;
|
||||
import ai.z.openapi.service.model.ChatCompletionResponse;
|
||||
import ai.z.openapi.service.model.Choice;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
|
@ -48,17 +47,17 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test AudioService Instantiation")
|
||||
void testAudioServiceInstantiation() {
|
||||
@DisplayName("Should instantiate AudioService successfully")
|
||||
void shouldInstantiateAudioServiceSuccessfully() {
|
||||
assertNotNull(audioService, "AudioService should be properly instantiated");
|
||||
assertInstanceOf(AudioServiceImpl.class, audioService,
|
||||
"AudioService should be an instance of AudioServiceImpl");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Text-to-Speech Generation")
|
||||
@DisplayName("Should generate speech from text successfully")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testCreateSpeech() throws JsonProcessingException {
|
||||
void shouldGenerateSpeechFromTextSuccessfully() throws JsonProcessingException {
|
||||
// Prepare test data
|
||||
String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis());
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ public class AudioServiceTest {
|
|||
.build();
|
||||
|
||||
// Execute test
|
||||
AudioSpeechApiResponse response = audioService.createSpeech(request);
|
||||
AudioSpeechResponse response = audioService.createSpeech(request);
|
||||
|
||||
// Verify results
|
||||
assertNotNull(response, "Speech response should not be null");
|
||||
|
|
@ -83,9 +82,9 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Custom Speech Generation with Voice Cloning")
|
||||
@DisplayName("Should generate custom speech with voice cloning successfully")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testCreateCustomSpeech() throws JsonProcessingException {
|
||||
void shouldGenerateCustomSpeechWithVoiceCloningSuccessfully() throws JsonProcessingException {
|
||||
// Prepare test data
|
||||
String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis());
|
||||
File voiceFile = new File("src/test/resources/asr.wav");
|
||||
|
|
@ -100,7 +99,7 @@ public class AudioServiceTest {
|
|||
.build();
|
||||
|
||||
// Execute test
|
||||
AudioCustomizationApiResponse response = audioService.createCustomSpeech(request);
|
||||
AudioCustomizationResponse response = audioService.createCustomSpeech(request);
|
||||
|
||||
// Verify results
|
||||
assertNotNull(response, "Custom speech response should not be null");
|
||||
|
|
@ -113,14 +112,14 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Synchronous Audio Transcription")
|
||||
@DisplayName("Should transcribe audio with blocking")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testSyncAudioTranscription() throws JsonProcessingException {
|
||||
void shouldTranscribeAudioWithBlocking() throws JsonProcessingException {
|
||||
// Prepare test data
|
||||
String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis());
|
||||
File audioFile = new File("src/test/resources/asr.wav");
|
||||
|
||||
AudioTranscriptionsRequest request = AudioTranscriptionsRequest.builder()
|
||||
AudioTranscriptionRequest request = AudioTranscriptionRequest.builder()
|
||||
.model(Constants.ModelGLMASR)
|
||||
.file(audioFile)
|
||||
.stream(false)
|
||||
|
|
@ -128,27 +127,26 @@ public class AudioServiceTest {
|
|||
.build();
|
||||
|
||||
// Execute test
|
||||
ChatCompletionResponse response = audioService.createTranscription(request);
|
||||
AudioTranscriptionResponse response = audioService.createTranscription(request);
|
||||
|
||||
// Verify results
|
||||
assertNotNull(response, "Transcription response should not be null");
|
||||
assertTrue(response.isSuccess(), "Transcription response should be successful");
|
||||
assertNotNull(response.getData(), "Transcription response data should not be null");
|
||||
assertNotNull(response.getData().getChoices(), "Response choices should not be null");
|
||||
assertFalse(response.getData().getChoices().isEmpty(), "Response choices should not be empty");
|
||||
assertNotNull(response.getData().getText(), "Transcription text should not be null");
|
||||
assertNull(response.getError(), "Response error should be null");
|
||||
logger.info("Synchronous transcription response: {}", mapper.writeValueAsString(response));
|
||||
logger.info("Blocking transcription response: {}", mapper.writeValueAsString(response));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Stream Audio Transcription")
|
||||
@DisplayName("Should transcribe audio with streaming")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testStreamAudioTranscription() throws JsonProcessingException {
|
||||
void shouldTranscribeAudioWithStreaming() throws JsonProcessingException {
|
||||
// Prepare test data
|
||||
String requestId = String.format(REQUEST_ID_TEMPLATE, System.currentTimeMillis());
|
||||
File audioFile = new File("src/test/resources/asr.wav");
|
||||
|
||||
AudioTranscriptionsRequest request = AudioTranscriptionsRequest.builder()
|
||||
AudioTranscriptionRequest request = AudioTranscriptionRequest.builder()
|
||||
.model(Constants.ModelGLMASR)
|
||||
.file(audioFile)
|
||||
.stream(true)
|
||||
|
|
@ -156,7 +154,7 @@ public class AudioServiceTest {
|
|||
.build();
|
||||
|
||||
// Execute test
|
||||
ChatCompletionResponse response = audioService.createTranscription(request);
|
||||
AudioTranscriptionResponse response = audioService.createTranscription(request);
|
||||
|
||||
// Verify results
|
||||
assertNotNull(response, "Stream transcription response should not be null");
|
||||
|
|
@ -188,33 +186,33 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null Speech Request")
|
||||
void testValidation_NullSpeechRequest() {
|
||||
@DisplayName("Should throw exception when speech request is null")
|
||||
void shouldThrowExceptionWhenSpeechRequestIsNull() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
audioService.createSpeech(null);
|
||||
}, "Null speech request should throw IllegalArgumentException");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null Custom Speech Request")
|
||||
void testValidation_NullCustomSpeechRequest() {
|
||||
@DisplayName("Should throw exception when custom speech request is null")
|
||||
void shouldThrowExceptionWhenCustomSpeechRequestIsNull() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
audioService.createCustomSpeech(null);
|
||||
}, "Null custom speech request should throw IllegalArgumentException");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null Transcription Request")
|
||||
void testValidation_NullTranscriptionRequest() {
|
||||
@DisplayName("Should throw exception when transcription request is null")
|
||||
void shouldThrowExceptionWhenTranscriptionRequestIsNull() {
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
audioService.createTranscription(null);
|
||||
}, "Null transcription request should throw IllegalArgumentException");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Invalid Audio File")
|
||||
void testValidation_InvalidAudioFile() {
|
||||
AudioTranscriptionsRequest request = AudioTranscriptionsRequest.builder()
|
||||
@DisplayName("Should throw exception when audio file does not exist")
|
||||
void shouldThrowExceptionWhenAudioFileDoesNotExist() {
|
||||
AudioTranscriptionRequest request = AudioTranscriptionRequest.builder()
|
||||
.model(Constants.ModelGLMASR)
|
||||
.file(new File("non-existent-file.wav"))
|
||||
.stream(false)
|
||||
|
|
@ -226,8 +224,8 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null Model in Speech Request")
|
||||
void testValidation_NullModelInSpeechRequest() {
|
||||
@DisplayName("Should throw exception when model is null in speech request")
|
||||
void shouldThrowExceptionWhenModelIsNullInSpeechRequest() {
|
||||
AudioSpeechRequest request = AudioSpeechRequest.builder().input("Test input").voice("tongtong").build();
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
|
|
@ -236,8 +234,8 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Empty Input in Speech Request")
|
||||
void testValidation_EmptyInputInSpeechRequest() {
|
||||
@DisplayName("Should throw exception when input is empty in speech request")
|
||||
void shouldThrowExceptionWhenInputIsEmptyInSpeechRequest() {
|
||||
AudioSpeechRequest request = AudioSpeechRequest.builder()
|
||||
.model(Constants.ModelTTS)
|
||||
.input("")
|
||||
|
|
@ -250,8 +248,8 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null Voice Data in Custom Speech Request")
|
||||
void testValidation_NullVoiceDataInCustomSpeechRequest() {
|
||||
@DisplayName("Should throw exception when voice data is null in custom speech request")
|
||||
void shouldThrowExceptionWhenVoiceDataIsNullInCustomSpeechRequest() {
|
||||
AudioCustomizationRequest request = AudioCustomizationRequest.builder()
|
||||
.model(Constants.ModelTTS)
|
||||
.input("Test input")
|
||||
|
|
@ -264,9 +262,9 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Parameter Validation - Null File in Transcription Request")
|
||||
void testValidation_NullFileInTranscriptionRequest() {
|
||||
AudioTranscriptionsRequest request = AudioTranscriptionsRequest.builder()
|
||||
@DisplayName("Should throw exception when file is null in transcription request")
|
||||
void shouldThrowExceptionWhenFileIsNullInTranscriptionRequest() {
|
||||
AudioTranscriptionRequest request = AudioTranscriptionRequest.builder()
|
||||
.model(Constants.ModelGLMASR)
|
||||
.stream(false)
|
||||
.build();
|
||||
|
|
@ -277,9 +275,9 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Speech Generation with Different Voice Options")
|
||||
@DisplayName("Should generate speech successfully with different voice options")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testSpeechGenerationWithDifferentVoices() throws JsonProcessingException {
|
||||
void shouldGenerateSpeechSuccessfullyWithDifferentVoiceOptions() throws JsonProcessingException {
|
||||
// Test with available voice options (currently limited to 'tongtong' as of
|
||||
// 2025-01-14)
|
||||
String[] voices = { "tongtong" };
|
||||
|
|
@ -294,7 +292,7 @@ public class AudioServiceTest {
|
|||
.requestId(requestId)
|
||||
.build();
|
||||
|
||||
AudioSpeechApiResponse response = audioService.createSpeech(request);
|
||||
AudioSpeechResponse response = audioService.createSpeech(request);
|
||||
|
||||
assertNotNull(response, "Speech response should not be null for voice: " + voice);
|
||||
logger.info("Voice {} response: {}", voice, mapper.writeValueAsString(response));
|
||||
|
|
@ -302,23 +300,23 @@ public class AudioServiceTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Test Transcription with Different Audio Formats")
|
||||
@DisplayName("Should transcribe different audio formats successfully")
|
||||
@EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$")
|
||||
void testTranscriptionWithDifferentFormats() throws JsonProcessingException {
|
||||
void shouldTranscribeDifferentAudioFormatsSuccessfully() throws JsonProcessingException {
|
||||
String[] audioFiles = { "asr.wav", "asr.webm" };
|
||||
|
||||
for (String audioFile : audioFiles) {
|
||||
String requestId = String.format(REQUEST_ID_TEMPLATE + "-%s", System.currentTimeMillis(), audioFile);
|
||||
File file = new File("src/test/resources/" + audioFile);
|
||||
|
||||
AudioTranscriptionsRequest request = AudioTranscriptionsRequest.builder()
|
||||
AudioTranscriptionRequest request = AudioTranscriptionRequest.builder()
|
||||
.model(Constants.ModelGLMASR)
|
||||
.file(file)
|
||||
.stream(false)
|
||||
.requestId(requestId)
|
||||
.build();
|
||||
|
||||
ChatCompletionResponse response = audioService.createTranscription(request);
|
||||
AudioTranscriptionResponse response = audioService.createTranscription(request);
|
||||
|
||||
assertNotNull(response, "Transcription response should not be null for file: " + audioFile);
|
||||
logger.info("Audio file {} transcription response: {}", audioFile, mapper.writeValueAsString(response));
|
||||
|
|
|
|||
|
|
@ -13,20 +13,6 @@ import org.slf4j.LoggerFactory;
|
|||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
// Import all required request and response classes
|
||||
import ai.z.openapi.service.fine_turning.FineTuningService;
|
||||
import ai.z.openapi.service.fine_turning.FineTuningServiceImpl;
|
||||
import ai.z.openapi.service.fine_turning.FineTuningJobRequest;
|
||||
import ai.z.openapi.service.fine_turning.CreateFineTuningJobApiResponse;
|
||||
import ai.z.openapi.service.fine_turning.QueryFineTuningJobRequest;
|
||||
import ai.z.openapi.service.fine_turning.QueryFineTuningJobApiResponse;
|
||||
import ai.z.openapi.service.fine_turning.QueryFineTuningEventApiResponse;
|
||||
import ai.z.openapi.service.fine_turning.QueryPersonalFineTuningJobRequest;
|
||||
import ai.z.openapi.service.fine_turning.QueryPersonalFineTuningJobApiResponse;
|
||||
import ai.z.openapi.service.fine_turning.FineTuningJobIdRequest;
|
||||
import ai.z.openapi.service.fine_turning.FineTuningJobModelRequest;
|
||||
import ai.z.openapi.service.fine_turning.FineTunedModelsStatusResponse;
|
||||
|
||||
/**
|
||||
* FineTuningService test class for testing various functionalities of FineTuningService
|
||||
* and FineTuningServiceImpl
|
||||
|
|
|
|||
Loading…
Reference in a new issue