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:
Junhui Huang 2025-07-15 18:40:42 +08:00 committed by GitHub
parent a4de5d46e3
commit e3c878b90e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 347 additions and 189 deletions

View file

@ -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;
}

View file

@ -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);
}

View 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";
/**

View file

@ -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();
}

View file

@ -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
}

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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);
}

View file

@ -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");
}

View file

@ -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

View file

@ -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;

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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));

View file

@ -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