diff --git a/core/src/main/java/ai/z/openapi/AbstractAiClient.java b/core/src/main/java/ai/z/openapi/AbstractAiClient.java index 045e1d1..445ca58 100644 --- a/core/src/main/java/ai/z/openapi/AbstractAiClient.java +++ b/core/src/main/java/ai/z/openapi/AbstractAiClient.java @@ -23,6 +23,8 @@ import ai.z.openapi.service.videos.VideosService; import ai.z.openapi.service.videos.VideosServiceImpl; import ai.z.openapi.service.assistant.AssistantService; import ai.z.openapi.service.assistant.AssistantServiceImpl; +import ai.z.openapi.service.voiceclone.VoiceCloneService; +import ai.z.openapi.service.voiceclone.VoiceCloneServiceImpl; import ai.z.openapi.core.config.ZaiConfig; import ai.z.openapi.core.model.BiFlowableClientResponse; import ai.z.openapi.core.model.ClientRequest; @@ -98,6 +100,9 @@ public abstract class AbstractAiClient extends AbstractClientBaseService { /** Assistant service for AI assistant functionality */ private AssistantService assistantService; + /** Voice clone service for voice cloning operations */ + private VoiceCloneService voiceCloneService; + /** * Constructs a new AbstractAiClient with the specified configuration. * @param config the configuration object containing API keys, timeouts, and other @@ -238,6 +243,18 @@ public abstract class AbstractAiClient extends AbstractClientBaseService { return assistantService; } + /** + * Returns the voice clone service for voice cloning operations. This service handles + * voice cloning creation, deletion, and listing functionality. + * @return the VoiceCloneService instance (lazily initialized) + */ + public synchronized VoiceCloneService voiceClone() { + if (voiceCloneService == null) { + this.voiceCloneService = new VoiceCloneServiceImpl(this); + } + return voiceCloneService; + } + // ==================== Utility Methods ==================== /** diff --git a/core/src/main/java/ai/z/openapi/api/voiceclone/VoiceCloneApi.java b/core/src/main/java/ai/z/openapi/api/voiceclone/VoiceCloneApi.java new file mode 100644 index 0000000..4a2d8d9 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/api/voiceclone/VoiceCloneApi.java @@ -0,0 +1,66 @@ +package ai.z.openapi.api.voiceclone; + +import ai.z.openapi.service.voiceclone.VoiceCloneRequest; +import ai.z.openapi.service.voiceclone.VoiceCloneResult; +import ai.z.openapi.service.voiceclone.VoiceDeleteRequest; +import ai.z.openapi.service.voiceclone.VoiceDeleteResult; +import ai.z.openapi.service.voiceclone.VoiceListResult; +import io.reactivex.rxjava3.core.Single; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.Header; +import retrofit2.http.POST; +import retrofit2.http.Query; + +/** + * Voice Clone API for voice cloning and management operations. Provides endpoints for + * creating voice clones from audio samples, deleting existing voice clones, and listing + * available voice clones. + */ +public interface VoiceCloneApi { + + /** + * Creates a new voice clone from provided audio sample and parameters. Uses advanced + * neural voice cloning technology to generate a custom voice model that can + * synthesize speech matching the characteristics of the provided sample. + * @param request voice clone creation parameters including voice name, sample text, + * target text, and audio file reference + * @return voice clone creation result with voice ID and preview file information + */ + @POST("voice/clone") + Single cloneVoice(@Body VoiceCloneRequest request); + + /** + * Deletes an existing voice clone by voice ID. Permanently removes the voice model + * and associated data from the system. This operation cannot be undone. + * @param request voice deletion parameters containing the voice ID to delete + * @return voice deletion result with confirmation and deletion timestamp + */ + @POST("voice/delete") + Single deleteVoice(@Body VoiceDeleteRequest request); + + /** + * Retrieves a list of available voice clones with optional filtering. Returns + * metadata and details for voice clones including voice IDs, names, types, download + * URLs, and creation timestamps. + * @param voiceType optional voice type filter + * @param voiceName optional voice name filter + * @return list of voice clone data with comprehensive metadata + */ + @GET("voice/list") + Single listVoices(@Query("voiceType") String voiceType, @Query("voiceName") String voiceName); + + /** + * Retrieves a list of available voice clones with optional filtering and Request-Id + * header. Returns metadata and details for voice clones including voice IDs, names, + * types, download URLs, and creation timestamps. + * @param voiceType optional voice type filter + * @param voiceName optional voice name filter + * @param requestId unique request identifier for tracking and monitoring + * @return list of voice clone data with comprehensive metadata + */ + @GET("voice/list") + Single listVoices(@Query("voiceType") String voiceType, @Query("voiceName") String voiceName, + @Header("Request-Id") String requestId); + +} \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java b/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java index c674394..5b7766b 100644 --- a/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java +++ b/core/src/main/java/ai/z/openapi/service/file/UploadFilePurpose.java @@ -2,7 +2,8 @@ package ai.z.openapi.service.file; public enum UploadFilePurpose { - BATCH("batch"), FILE_EXTRACT("file-extract"), CODE_INTERPRETER("code-interpreter"), AGENT("agent"); + BATCH("batch"), FILE_EXTRACT("file-extract"), CODE_INTERPRETER("code-interpreter"), AGENT("agent"), + VOICE_CLONE_INPUT("voice-clone-input"); private final String value; diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneRequest.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneRequest.java new file mode 100644 index 0000000..05dc421 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneRequest.java @@ -0,0 +1,34 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientRequest; +import ai.z.openapi.service.CommonRequest; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Request parameters for voice cloning API. This class contains all the necessary + * parameters for voice cloning operations, including voice name, sample audio text, + * target preview text, and audio file information. + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class VoiceCloneRequest extends CommonRequest implements ClientRequest { + + /** Voice name */ + @JsonProperty("voice_name") + private String voiceName; + + /** Text content corresponding to the sample audio */ + @JsonProperty("voice_text_input") + private String voiceTextInput; + + /** Target text for preview audio */ + @JsonProperty("voice_text_output") + private String voiceTextOutput; + + /** File ID of the audio file */ + @JsonProperty("file_id") + private String fileId; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResponse.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResponse.java new file mode 100644 index 0000000..896d777 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResponse.java @@ -0,0 +1,35 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientResponse; +import ai.z.openapi.service.batches.BatchPage; +import ai.z.openapi.service.model.ChatError; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Response wrapper for voice cloning API operations. This class contains the standard + * response structure including status code, message, success flag, result data, and error + * information. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceCloneResponse implements ClientResponse { + + private int code; + + private String msg; + + private boolean success; + + private VoiceCloneResult data; + + private ChatError error; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResult.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResult.java new file mode 100644 index 0000000..231bfaf --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneResult.java @@ -0,0 +1,31 @@ +package ai.z.openapi.service.voiceclone; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Result data for voice cloning operations. This class contains the voice cloning result + * information including voice ID, audio file ID, and file purpose. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VoiceCloneResult { + + /** Voice ID */ + @JsonProperty("voice_id") + private String voiceId; + + /** Audio preview file ID */ + @JsonProperty("file_id") + private String fileId; + + /** File purpose */ + @JsonProperty("file_purpose") + private String filePurpose; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneService.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneService.java new file mode 100644 index 0000000..1d741f1 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneService.java @@ -0,0 +1,32 @@ +package ai.z.openapi.service.voiceclone; + +/** + * Voice clone service interface for voice cloning operations. This service provides + * methods for creating, deleting, and listing voice clones. + */ +public interface VoiceCloneService { + + /** + * Creates a new voice clone based on the provided audio sample and parameters. + * @param request the voice clone creation request containing voice name, sample audio + * text, target text, and audio file information + * @return VoiceCloneResponse containing the voice clone result and metadata + */ + VoiceCloneResponse cloneVoice(VoiceCloneRequest request); + + /** + * Deletes an existing voice clone by voice ID. + * @param request the voice deletion request containing the voice ID to delete + * @return VoiceDeleteResponse containing the deletion result and timestamp + */ + VoiceDeleteResponse deleteVoice(VoiceDeleteRequest request); + + /** + * Retrieves a list of available voice clones with optional filtering. + * @param request the voice list request containing optional voice type and name + * filters + * @return VoiceListResponse containing the filtered list of voice data + */ + VoiceListResponse listVoice(VoiceListRequest request); + +} \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceImpl.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceImpl.java new file mode 100644 index 0000000..451cedb --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceImpl.java @@ -0,0 +1,78 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.AbstractAiClient; +import ai.z.openapi.api.voiceclone.VoiceCloneApi; +import ai.z.openapi.service.deserialize.MessageDeserializeFactory; +import ai.z.openapi.utils.RequestSupplier; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; + +/** + * Voice clone service implementation for voice cloning operations. This service provides + * methods for creating, deleting, and listing voice clones using the VoiceClone API + * endpoints. + */ +public class VoiceCloneServiceImpl implements VoiceCloneService { + + protected static final ObjectMapper mapper = MessageDeserializeFactory.defaultObjectMapper(); + + private final AbstractAiClient zAiClient; + + private final VoiceCloneApi voiceCloneApi; + + public VoiceCloneServiceImpl(AbstractAiClient zAiClient) { + this.zAiClient = zAiClient; + this.voiceCloneApi = zAiClient.retrofit().create(VoiceCloneApi.class); + } + + @Override + public VoiceCloneResponse cloneVoice(VoiceCloneRequest request) { + validateCreateVoiceParams(request); + RequestSupplier supplier = voiceCloneApi::cloneVoice; + return this.zAiClient.executeRequest(request, supplier, VoiceCloneResponse.class); + } + + @Override + public VoiceDeleteResponse deleteVoice(VoiceDeleteRequest request) { + validateDeleteVoiceParams(request); + RequestSupplier supplier = voiceCloneApi::deleteVoice; + return this.zAiClient.executeRequest(request, supplier, VoiceDeleteResponse.class); + } + + @Override + public VoiceListResponse listVoice(VoiceListRequest request) { + RequestSupplier supplier = (params) -> { + String voiceType = params != null ? params.getVoiceType() : null; + String voiceName = params != null ? params.getVoiceName() : null; + String requestId = params != null ? params.getRequestId() : ""; + + return voiceCloneApi.listVoices(voiceType, voiceName, requestId); + }; + return this.zAiClient.executeRequest(request, supplier, VoiceListResponse.class); + } + + private void validateCreateVoiceParams(VoiceCloneRequest request) { + if (request == null) { + throw new IllegalArgumentException("request cannot be null"); + } + if (request.getVoiceName() == null || request.getVoiceName().trim().isEmpty()) { + throw new IllegalArgumentException("voice name cannot be null or empty"); + } + if (request.getVoiceTextInput() == null || request.getVoiceTextInput().trim().isEmpty()) { + throw new IllegalArgumentException("voice text input cannot be null or empty"); + } + if (request.getFileId() == null || request.getFileId().trim().isEmpty()) { + throw new IllegalArgumentException("file ID cannot be null or empty"); + } + } + + private void validateDeleteVoiceParams(VoiceDeleteRequest request) { + if (request == null) { + throw new IllegalArgumentException("request cannot be null"); + } + if (request.getVoiceId() == null || request.getVoiceId().trim().isEmpty()) { + throw new IllegalArgumentException("voice ID cannot be null or empty"); + } + } + +} \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceData.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceData.java new file mode 100644 index 0000000..a8130a0 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceData.java @@ -0,0 +1,32 @@ +package ai.z.openapi.service.voiceclone; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.util.Date; + +/** + * Voice data entity containing voice information. This class contains voice details + * including voice ID, name, type, download URL, and creation time. + */ +@Data +public class VoiceData { + + @JsonProperty("voice_id") + private String voiceId; + + @JsonProperty("voice_name") + private String voiceName; + + @JsonProperty("voice_type") + private String voiceType; + + @JsonProperty("download_url") + private String downloadUrl; + + @JsonProperty("create_time") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteRequest.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteRequest.java new file mode 100644 index 0000000..96230ec --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteRequest.java @@ -0,0 +1,20 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientRequest; +import ai.z.openapi.service.CommonRequest; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Request parameters for voice deletion API. This class contains the necessary parameters + * for deleting a voice, specifically the voice ID. + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class VoiceDeleteRequest extends CommonRequest implements ClientRequest { + + @JsonProperty("voice_id") + private String voiceId; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResponse.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResponse.java new file mode 100644 index 0000000..e876915 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResponse.java @@ -0,0 +1,31 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientResponse; +import ai.z.openapi.service.model.ChatError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response wrapper for voice deletion API operations. This class contains the standard + * response structure including status code, message, success flag, result data, and error + * information. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceDeleteResponse implements ClientResponse { + + private int code; + + private String msg; + + private boolean success; + + private VoiceDeleteResult data; + + private ChatError error; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResult.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResult.java new file mode 100644 index 0000000..bb5a276 --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceDeleteResult.java @@ -0,0 +1,30 @@ +package ai.z.openapi.service.voiceclone; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.Date; + +/** + * Result data for voice deletion operations. This class contains the deletion result + * information including voice ID and deletion timestamp. + */ +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class VoiceDeleteResult { + + @JsonProperty("voice_id") + private String voiceId; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonProperty("delete_time") + private Date deleteTime; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListRequest.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListRequest.java new file mode 100644 index 0000000..19bdc5c --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListRequest.java @@ -0,0 +1,25 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientRequest; +import ai.z.openapi.service.CommonRequest; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * Request parameters for voice list API. This class contains optional filter parameters + * for retrieving voice clones by type and name. + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class VoiceListRequest extends CommonRequest implements ClientRequest { + + /** Voice type filter,"PRIVATE" or "OFFICIAL" or null */ + @JsonProperty("voice_type") + private String voiceType; + + /** Voice name filter */ + @JsonProperty("voice_name") + private String voiceName; + +} \ No newline at end of file diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResponse.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResponse.java new file mode 100644 index 0000000..f480a6f --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResponse.java @@ -0,0 +1,31 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.core.model.ClientResponse; +import ai.z.openapi.service.model.ChatError; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Response wrapper for voice list API operations. This class contains the standard + * response structure including status code, message, success flag, result data, and error + * information. + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class VoiceListResponse implements ClientResponse { + + private int code; + + private String msg; + + private boolean success; + + private VoiceListResult data; + + private ChatError error; + +} diff --git a/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResult.java b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResult.java new file mode 100644 index 0000000..b05620d --- /dev/null +++ b/core/src/main/java/ai/z/openapi/service/voiceclone/VoiceListResult.java @@ -0,0 +1,19 @@ +package ai.z.openapi.service.voiceclone; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * Result data for voice list operations. This class contains the list of voice data + * returned from voice listing API calls. + */ +@Data +public class VoiceListResult { + + @JsonProperty("voice_list") + private List voiceList; + +} diff --git a/core/src/test/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceTest.java b/core/src/test/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceTest.java new file mode 100644 index 0000000..647a503 --- /dev/null +++ b/core/src/test/java/ai/z/openapi/service/voiceclone/VoiceCloneServiceTest.java @@ -0,0 +1,130 @@ +package ai.z.openapi.service.voiceclone; + +import ai.z.openapi.ZaiClient; +import ai.z.openapi.core.config.ZaiConfig; +import ai.z.openapi.service.file.FileApiResponse; +import ai.z.openapi.service.file.FileService; +import ai.z.openapi.service.file.FileUploadParams; +import ai.z.openapi.service.file.UploadFilePurpose; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.file.Paths; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test class for VoiceCloneService functionality. Tests voice cloning operations + * including file upload, voice creation, deletion, and listing. Requires ZAI_API_KEY + * environment variable to be set for integration tests. + */ +@DisplayName("VoiceCloneService Tests") +public class VoiceCloneServiceTest { + + private static final Logger logger = LoggerFactory.getLogger(VoiceCloneServiceTest.class); + + private static final ObjectMapper mapper = new ObjectMapper(); + + private VoiceCloneService voiceCloneService; + + private FileService fileService; + + @BeforeEach + void setUp() { + ZaiConfig zaiConfig = new ZaiConfig(); + String apiKey = zaiConfig.getApiKey(); + if (apiKey == null) { + zaiConfig.setApiKey("id.test-api-key"); + } + ZaiClient client = new ZaiClient(zaiConfig); + voiceCloneService = client.voiceClone(); + fileService = client.files(); + } + + @Test + @DisplayName("Test voice clone creation with file upload") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testCloneVoice() throws JsonProcessingException { + // Step 1: Upload the voice input audio file + // First, we need to upload the voice sample audio file to get a file ID + String voiceInputFilePath = "src/test/resources/voice_clone_input.mp3"; + + FileUploadParams uploadRequest = FileUploadParams.builder() + .filePath(voiceInputFilePath) + .purpose(UploadFilePurpose.VOICE_CLONE_INPUT.value()) + .requestId("voice-clone-test-" + System.currentTimeMillis()) + .build(); + + FileApiResponse uploadResponse = fileService.uploadFile(uploadRequest); + assertNotNull(uploadResponse, "File upload response should not be null"); + assertTrue(uploadResponse.isSuccess(), "File upload should be successful"); + assertNotNull(uploadResponse.getData(), "Uploaded file data should not be null"); + + String fileId = uploadResponse.getData().getId(); + logger.info("Voice input file uploaded successfully with ID: {}", fileId); + + // Step 2: Create voice clone using the uploaded file + // Now we can use the uploaded file ID to create a voice clone + VoiceCloneRequest request = new VoiceCloneRequest(); + request.setVoiceName("Test"); + request.setVoiceTextInput("This is sample text for voice cloning training"); + request.setVoiceTextOutput("This is target text for voice preview generation"); + request.setFileId(fileId); // Use the actual file ID from upload + + // Execute voice cloning + VoiceCloneResponse response = voiceCloneService.cloneVoice(request); + + // Verify the voice cloning response + assertNotNull(response, "Voice clone response should not be null"); + assertTrue(response.isSuccess(), "Voice cloning should be successful"); + assertNotNull(response.getData(), "Voice clone data should not be null"); + logger.info("Voice clone created successfully: {}", mapper.writeValueAsString(response)); + } + + @Test + @DisplayName("Test voice list") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testListVoice() throws JsonProcessingException { + // Create a voice list request with filtering parameters + // This will retrieve voices filtered by type and name pattern + VoiceListRequest request = new VoiceListRequest(); + request.setVoiceType("PRIVATE"); // Filter for custom voice clones + request.setVoiceName("Test"); // Filter by voice name pattern + + // Execute the voice listing + VoiceListResponse response = voiceCloneService.listVoice(request); + + // Verify the listing response + logger.info("Voice list response: {}", mapper.writeValueAsString(response)); + assertNotNull(response, "Voice list response should not be null"); + assertTrue(response.isSuccess(), "Voice listing should be successful"); + assertNotNull(response.getData(), "Voice list data should not be null"); + logger.info("Voice list retrieved successfully: {}", mapper.writeValueAsString(response)); + } + + @Test + @DisplayName("Test voice deletion") + @EnabledIfEnvironmentVariable(named = "ZAI_API_KEY", matches = "^[^.]+\\.[^.]+$") + void testDeleteVoice() throws JsonProcessingException { + // Create a voice deletion request with a test voice ID + // In a real scenario, this would be an ID from a previously created voice clone + VoiceDeleteRequest request = new VoiceDeleteRequest(); + request.setVoiceId("Test voice Id"); + + // Execute the voice deletion + VoiceDeleteResponse response = voiceCloneService.deleteVoice(request); + + // Verify the deletion response + assertNotNull(response, "Voice deletion response should not be null"); + assertTrue(response.isSuccess(), "Voice deletion should be successful"); + assertNotNull(response.getData(), "Voice deletion data should not be null"); + logger.info("Voice deleted successfully: {}", mapper.writeValueAsString(response)); + } + +} \ No newline at end of file diff --git a/core/src/test/resources/voice_clone_input.mp3 b/core/src/test/resources/voice_clone_input.mp3 new file mode 100644 index 0000000..0eb5ef7 Binary files /dev/null and b/core/src/test/resources/voice_clone_input.mp3 differ diff --git a/samples/src/main/ai.z.openapi.samples/VoiceCloneExample.java b/samples/src/main/ai.z.openapi.samples/VoiceCloneExample.java new file mode 100644 index 0000000..bdd51c2 --- /dev/null +++ b/samples/src/main/ai.z.openapi.samples/VoiceCloneExample.java @@ -0,0 +1,176 @@ +package ai.z.openapi.samples; + +import ai.z.openapi.ZaiClient; +import ai.z.openapi.service.file.FileApiResponse; +import ai.z.openapi.service.file.FileService; +import ai.z.openapi.service.file.FileUploadParams; +import ai.z.openapi.service.file.UploadFilePurpose; +import ai.z.openapi.service.voiceclone.*; + +import java.nio.file.Paths; + +/** + * Voice Clone Example + * Demonstrates how to use ZaiClient for voice cloning operations including: + * - Uploading voice samples + * - Creating voice clones + * - Listing existing voices + * - Deleting voice clones + */ +public class VoiceCloneExample { + + public static void main(String[] args) { + // Create client, recommended to set API Key via environment variable + // export ZAI_API_KEY=your.api_key + ZaiClient client = ZaiClient.builder().build(); + + // Or set API Key via code + // ZaiClient client = ZaiClient.builder() + // .apiKey("your.api_key") + // .build(); + + VoiceCloneService voiceCloneService = client.voiceClone(); + FileService fileService = client.files(); + + try { + // Example 1: Create a voice clone + System.out.println("=== Voice Clone Creation Example ==="); + createVoiceCloneExample(voiceCloneService, fileService); + + // Example 2: List existing voices + System.out.println("\n=== Voice List Example ==="); + listVoicesExample(voiceCloneService); + + // Example 3: Delete a voice clone + System.out.println("\n=== Voice Deletion Example ==="); + deleteVoiceExample(voiceCloneService); + + } catch (Exception e) { + System.err.println("Exception occurred: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * Example of creating a voice clone with file upload + */ + private static void createVoiceCloneExample(VoiceCloneService voiceCloneService, FileService fileService) { + try { + // Step 1: Upload the voice input audio file + String voiceInputFilePath = Paths.get("samples", "resources", "voice_clone_input.mp3").toString(); + + FileUploadParams uploadRequest = FileUploadParams.builder() + .filePath(voiceInputFilePath) + .purpose(UploadFilePurpose.VOICE_CLONE_INPUT.value()) + .requestId("voice-clone-example-" + System.currentTimeMillis()) + .build(); + + System.out.println("Uploading voice input file..."); + FileApiResponse uploadResponse = fileService.uploadFile(uploadRequest); + + if (uploadResponse.isSuccess()) { + String fileId = uploadResponse.getData().getId(); + System.out.println("Voice input file uploaded successfully with ID: " + fileId); + + // Step 2: Create voice clone using the uploaded file + VoiceCloneRequest request = new VoiceCloneRequest(); + request.setVoiceName("My Custom Voice"); + request.setVoiceTextInput("Hello, this is a sample text for voice cloning training"); + request.setVoiceTextOutput("Welcome to our voice synthesis system"); + request.setFileId(fileId); + request.setRequestId("clone-request-" + System.currentTimeMillis()); + + System.out.println("Creating voice clone..."); + VoiceCloneResponse response = voiceCloneService.cloneVoice(request); + + if (response.isSuccess()) { + System.out.println("Voice clone created successfully!"); + System.out.println("Voice ID: " + response.getData().getVoiceId()); + } else { + System.err.println("Voice clone creation failed: " + response.getMsg()); + if (response.getError() != null) { + System.err.println("Error details: " + response.getError().getMessage()); + } + } + } else { + System.err.println("File upload failed: " + uploadResponse.getMsg()); + } + } catch (Exception e) { + System.err.println("Error in voice clone creation: " + e.getMessage()); + } + } + + /** + * Example of listing existing voice clones + */ + private static void listVoicesExample(VoiceCloneService voiceCloneService) { + try { + // List all private voices + VoiceListRequest request = new VoiceListRequest(); + request.setVoiceType("PRIVATE"); // Filter for custom voice clones + request.setRequestId("list-request-" + System.currentTimeMillis()); + + System.out.println("Retrieving voice list..."); + VoiceListResponse response = voiceCloneService.listVoice(request); + + if (response.isSuccess()) { + System.out.println("Voice list retrieved successfully!"); + if (response.getData().getVoiceList() != null && !response.getData().getVoiceList().isEmpty()) { + System.out.println("Found " + response.getData().getVoiceList().size() + " voices:"); + for (VoiceData voice : response.getData().getVoiceList()) { + System.out.println("- Voice ID: " + voice.getVoiceId()); + System.out.println(" Name: " + voice.getVoiceName()); + System.out.println(" Type: " + voice.getVoiceType()); + if (voice.getDownloadUrl() != null) { + System.out.println(" Download URL: " + voice.getDownloadUrl()); + } + if (voice.getCreateTime() != null) { + System.out.println(" Created: " + voice.getCreateTime()); + } + System.out.println(); + } + } else { + System.out.println("No voices found."); + } + } else { + System.err.println("Voice list retrieval failed: " + response.getMsg()); + } + } catch (Exception e) { + System.err.println("Error in voice list retrieval: " + e.getMessage()); + } + } + + /** + * Example of deleting a voice clone + * Note: Replace "your-voice-id" with an actual voice ID from your account + */ + private static void deleteVoiceExample(VoiceCloneService voiceCloneService) { + try { + // Note: This is just an example - replace with actual voice ID + String voiceIdToDelete = "your-voice-id-here"; + + VoiceDeleteRequest request = new VoiceDeleteRequest(); + request.setVoiceId(voiceIdToDelete); + request.setRequestId("delete-request-" + System.currentTimeMillis()); + + System.out.println("Deleting voice clone with ID: " + voiceIdToDelete); + VoiceDeleteResponse response = voiceCloneService.deleteVoice(request); + + if (response.isSuccess()) { + System.out.println("Voice clone deleted successfully!"); + if (response.getData().getDeleteTime() != null) { + System.out.println("Deletion time: " + response.getData().getDeleteTime()); + } + } else { + System.err.println("Voice deletion failed: " + response.getMsg()); + if (response.getError() != null) { + System.err.println("Error details: " + response.getError().getMessage()); + } + } + } catch (Exception e) { + // Expected to fail with example voice ID + System.out.println("Note: This example uses a placeholder voice ID and is expected to fail."); + System.out.println("Replace 'your-voice-id-here' with an actual voice ID to test deletion."); + } + } +} \ No newline at end of file