feat: voiceclone implement comprehensive voice cloning service with full API support (#29)
Co-authored-by: tomsun28 <tomsun28@outlook.com>
This commit is contained in:
parent
67639ae83e
commit
7d30990e53
12 changed files with 479 additions and 1 deletions
69
examples/voice_clone.py
Normal file
69
examples/voice_clone.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from zai import ZaiClient, ZhipuAiClient
|
||||
import time
|
||||
import os
|
||||
|
||||
def voice_clone():
|
||||
# Initialize client
|
||||
client = ZhipuAiClient()
|
||||
|
||||
# Step 1: Upload the voice input audio file
|
||||
# First, we need to upload the voice sample audio file to get a file ID
|
||||
voice_input_file_path = "tests/integration_tests/voice_clone_input.mp3"
|
||||
|
||||
try:
|
||||
with open(voice_input_file_path, 'rb') as f:
|
||||
upload_response = client.files.create(
|
||||
file=f,
|
||||
purpose='voice-clone-input',
|
||||
)
|
||||
|
||||
print(f"Voice input file uploaded successfully with ID: {upload_response.id}")
|
||||
file_id = upload_response.id
|
||||
|
||||
except FileNotFoundError:
|
||||
print(f"File not found: {voice_input_file_path}")
|
||||
return
|
||||
except Exception as e:
|
||||
print(f"File upload failed: {e}")
|
||||
return
|
||||
|
||||
# Step 2: Clone voice using the uploaded file ID
|
||||
response = client.voice.clone(
|
||||
voice_name="My Test Voice!",
|
||||
text="This is sample text for voice cloning training",
|
||||
input="This is target text for voice preview generation",
|
||||
file_id=file_id,
|
||||
request_id=f"voice_clone_request_{int(time.time() * 1000)}",
|
||||
model="cogtts-clone"
|
||||
)
|
||||
print(f"Voice clone response: {response}")
|
||||
|
||||
def voice_delete():
|
||||
# Initialize client
|
||||
client = ZhipuAiClient()
|
||||
|
||||
# Delete voice
|
||||
response = client.voice.delete(
|
||||
voice="Your voice",
|
||||
request_id=f"voice_delete_request_{int(time.time() * 1000)}"
|
||||
)
|
||||
print(response)
|
||||
|
||||
def voice_list():
|
||||
# Initialize client
|
||||
client = ZhipuAiClient()
|
||||
|
||||
# List voices with filter
|
||||
response = client.voice.list(
|
||||
voice_type="PRIVATE",
|
||||
voice_name="Test",
|
||||
request_id=f"voice_list_request_{int(time.time() * 1000)}"
|
||||
)
|
||||
print(response)
|
||||
|
||||
if __name__ == "__main__":
|
||||
# voice_clone()
|
||||
|
||||
# voice_delete()
|
||||
|
||||
voice_list()
|
||||
|
|
@ -20,6 +20,7 @@ if TYPE_CHECKING:
|
|||
from zai.api_resource.moderations import Moderations
|
||||
from zai.api_resource.tools import Tools
|
||||
from zai.api_resource.videos import Videos
|
||||
from zai.api_resource.voice import Voice
|
||||
from zai.api_resource.web_search import WebSearchApi
|
||||
|
||||
from .core import (
|
||||
|
|
@ -180,6 +181,12 @@ class BaseClient(HttpClient):
|
|||
|
||||
return Moderations(self)
|
||||
|
||||
@cached_property
|
||||
def voice(self) -> Voice:
|
||||
from zai.api_resource.voice import Voice
|
||||
|
||||
return Voice(self)
|
||||
|
||||
@property
|
||||
@override
|
||||
def auth_headers(self) -> dict[str, str]:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class Files(BaseAPI):
|
|||
*,
|
||||
file: FileTypes = None,
|
||||
upload_detail: List[UploadDetail] = None,
|
||||
purpose: Literal['fine-tune', 'retrieval', 'batch'],
|
||||
purpose: Literal['fine-tune', 'retrieval', 'batch', 'voice-clone-input'],
|
||||
knowledge_id: str = None,
|
||||
sentence_size: int = None,
|
||||
extra_headers: Headers | None = None,
|
||||
|
|
|
|||
3
src/zai/api_resource/voice/__init__.py
Normal file
3
src/zai/api_resource/voice/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from .voice import Voice
|
||||
|
||||
__all__ = ['Voice']
|
||||
171
src/zai/api_resource/voice/voice.py
Normal file
171
src/zai/api_resource/voice/voice.py
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import httpx
|
||||
from httpx import stream
|
||||
|
||||
from zai.core import (
|
||||
NOT_GIVEN,
|
||||
BaseAPI,
|
||||
Body,
|
||||
Headers,
|
||||
NotGiven,
|
||||
make_request_options,
|
||||
maybe_transform,
|
||||
)
|
||||
from zai.types.voiceclone import (
|
||||
VoiceCloneParams,
|
||||
VoiceCloneResult,
|
||||
VoiceDeleteParams,
|
||||
VoiceDeleteResult,
|
||||
VoiceListParams,
|
||||
VoiceListResult,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from zai._client import ZaiClient
|
||||
|
||||
|
||||
class Voice(BaseAPI):
|
||||
"""
|
||||
Voice API resource for handling voice cloning operations
|
||||
"""
|
||||
|
||||
def __init__(self, client: ZaiClient) -> None:
|
||||
super().__init__(client)
|
||||
|
||||
def clone(
|
||||
self,
|
||||
*,
|
||||
voice_name: str,
|
||||
text: str,
|
||||
input: str,
|
||||
file_id: str,
|
||||
request_id: Optional[str] = None,
|
||||
model: str,
|
||||
extra_headers: Headers | None = None,
|
||||
extra_body: Body | None = None,
|
||||
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
|
||||
) -> VoiceCloneResult:
|
||||
"""
|
||||
Clone a voice with the provided audio sample and parameters
|
||||
|
||||
Args:
|
||||
voice_name: Name for the cloned voice
|
||||
text: Text content corresponding to the sample audio
|
||||
input: Target text for preview audio
|
||||
file_id: File ID of the uploaded audio file
|
||||
request_id: Optional request ID for tracking
|
||||
model: Model
|
||||
extra_headers: Additional headers to include in the request
|
||||
extra_body: Additional body parameters
|
||||
timeout: Request timeout
|
||||
|
||||
Returns:
|
||||
Voice clone response
|
||||
"""
|
||||
|
||||
return self._post(
|
||||
"/voice/clone",
|
||||
body=maybe_transform(
|
||||
{
|
||||
"voice_name": voice_name,
|
||||
"text": text,
|
||||
"input": input,
|
||||
"file_id": file_id,
|
||||
"request_id": request_id,
|
||||
"model": model,
|
||||
},
|
||||
VoiceCloneParams,
|
||||
),
|
||||
options=make_request_options(
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
timeout=timeout,
|
||||
),
|
||||
cast_type=VoiceCloneResult,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
def delete(
|
||||
self,
|
||||
*,
|
||||
voice: str,
|
||||
request_id: Optional[str] = None,
|
||||
extra_headers: Headers | None = None,
|
||||
extra_body: Body | None = None,
|
||||
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
|
||||
) -> VoiceDeleteResult:
|
||||
"""
|
||||
Delete a cloned voice by voice ID
|
||||
|
||||
Args:
|
||||
voice: The voice to delete
|
||||
request_id: Optional request ID for tracking
|
||||
extra_headers: Additional headers to include in the request
|
||||
extra_body: Additional body parameters
|
||||
timeout: Request timeout
|
||||
|
||||
Returns:
|
||||
Voice deletion response
|
||||
"""
|
||||
return self._post(
|
||||
"/voice/delete",
|
||||
body=maybe_transform(
|
||||
{
|
||||
"voice": voice,
|
||||
"request_id": request_id,
|
||||
},
|
||||
VoiceDeleteParams,
|
||||
),
|
||||
options=make_request_options(
|
||||
extra_headers=extra_headers,
|
||||
extra_body=extra_body,
|
||||
timeout=timeout,
|
||||
),
|
||||
cast_type=VoiceDeleteResult,
|
||||
stream=False,
|
||||
)
|
||||
|
||||
def list(
|
||||
self,
|
||||
*,
|
||||
voice_type: Optional[str] = None,
|
||||
voice_name: Optional[str] = None,
|
||||
request_id: Optional[str] = None,
|
||||
extra_headers: Headers | None = None,
|
||||
timeout: float | httpx.Timeout | None | NotGiven = NOT_GIVEN,
|
||||
) -> VoiceListResult:
|
||||
"""
|
||||
List voices with optional filtering
|
||||
|
||||
Args:
|
||||
voice_type: Type of voice to filter by
|
||||
voice_name: Name of voice to filter by
|
||||
request_id: Optional request ID for tracking
|
||||
extra_headers: Additional headers to include in the request
|
||||
timeout: Request timeout
|
||||
|
||||
Returns:
|
||||
List of voices response
|
||||
"""
|
||||
return self._get(
|
||||
"/voice/list",
|
||||
options=make_request_options(
|
||||
extra_headers={
|
||||
**({} if request_id is None else {"Request-Id": request_id}),
|
||||
**(extra_headers or {}),
|
||||
},
|
||||
extra_query=maybe_transform(
|
||||
{
|
||||
"voiceType": voice_type,
|
||||
"voiceName": voice_name,
|
||||
"request_id": request_id,
|
||||
},
|
||||
VoiceListParams,
|
||||
),
|
||||
timeout=timeout,
|
||||
),
|
||||
cast_type=VoiceListResult,
|
||||
)
|
||||
19
src/zai/types/voiceclone/__init__.py
Normal file
19
src/zai/types/voiceclone/__init__.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from .voice_clone_params import VoiceCloneParams
|
||||
from .voice_delete_params import VoiceDeleteParams
|
||||
from .voice_list_params import VoiceListParams
|
||||
from .voice_object import (
|
||||
VoiceCloneResult,
|
||||
VoiceDeleteResult,
|
||||
VoiceData,
|
||||
VoiceListResult,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
'VoiceCloneParams',
|
||||
'VoiceDeleteParams',
|
||||
'VoiceListParams',
|
||||
'VoiceCloneResult',
|
||||
'VoiceDeleteResult',
|
||||
'VoiceData',
|
||||
'VoiceListResult',
|
||||
]
|
||||
24
src/zai/types/voiceclone/voice_clone_params.py
Normal file
24
src/zai/types/voiceclone/voice_clone_params.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
|
||||
class VoiceCloneParams(TypedDict, total=False):
|
||||
"""
|
||||
Parameters for voice cloning
|
||||
|
||||
Attributes:
|
||||
voice_name (str): Name for the cloned voice
|
||||
voice_text_input (str): Text content corresponding to the sample audio
|
||||
voice_text_output (str): Target text for preview audio
|
||||
file_id (str): File ID of the uploaded audio file
|
||||
request_id (Optional[str]): Optional request ID for tracking
|
||||
"""
|
||||
|
||||
voice_name: Required[str]
|
||||
voice_text_input: Required[str]
|
||||
voice_text_output: Required[str]
|
||||
file_id: Required[str]
|
||||
request_id: Optional[str]
|
||||
18
src/zai/types/voiceclone/voice_delete_params.py
Normal file
18
src/zai/types/voiceclone/voice_delete_params.py
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import Required, TypedDict
|
||||
|
||||
|
||||
class VoiceDeleteParams(TypedDict, total=False):
|
||||
"""
|
||||
Parameters for voice deletion
|
||||
|
||||
Attributes:
|
||||
voice (str): The voice to delete
|
||||
request_id (Optional[str]): Optional request ID for tracking
|
||||
"""
|
||||
|
||||
voice: Required[str]
|
||||
request_id: Optional[str]
|
||||
20
src/zai/types/voiceclone/voice_list_params.py
Normal file
20
src/zai/types/voiceclone/voice_list_params.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class VoiceListParams(TypedDict, total=False):
|
||||
"""
|
||||
Parameters for listing voices
|
||||
|
||||
Attributes:
|
||||
voice_type (Optional[str]): Type of voice to filter by
|
||||
voice_name (Optional[str]): Name of voice to filter by
|
||||
request_id (Optional[str]): Optional request ID for tracking
|
||||
"""
|
||||
|
||||
voice_type: Optional[str]
|
||||
voice_name: Optional[str]
|
||||
request_id: Optional[str]
|
||||
61
src/zai/types/voiceclone/voice_object.py
Normal file
61
src/zai/types/voiceclone/voice_object.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from typing import List
|
||||
|
||||
from zai.core import BaseModel
|
||||
|
||||
|
||||
class VoiceCloneResult(BaseModel):
|
||||
"""
|
||||
Voice cloning result
|
||||
|
||||
Attributes:
|
||||
voice (str): Voice
|
||||
file_id (str): Audio preview file ID
|
||||
file_purpose (str): File purpose
|
||||
"""
|
||||
|
||||
voice: str
|
||||
file_id: str
|
||||
file_purpose: str
|
||||
|
||||
|
||||
class VoiceDeleteResult(BaseModel):
|
||||
"""
|
||||
Voice deletion result
|
||||
|
||||
Attributes:
|
||||
voice (str): Voice
|
||||
update_time (str): Delete time (format: yyyy-MM-dd HH:mm:ss)
|
||||
"""
|
||||
|
||||
voice: str
|
||||
update_time: str
|
||||
|
||||
|
||||
class VoiceData(BaseModel):
|
||||
"""
|
||||
Voice data information
|
||||
|
||||
Attributes:
|
||||
voice (str): Voice
|
||||
voice_name (str): Voice name
|
||||
voice_type (str): Voice type
|
||||
download_url (str): Download URL
|
||||
create_time (str): Create time (format: yyyy-MM-dd HH:mm:ss)
|
||||
"""
|
||||
|
||||
voice: str
|
||||
voice_name: str
|
||||
voice_type: str
|
||||
download_url: str
|
||||
create_time: str
|
||||
|
||||
|
||||
class VoiceListResult(BaseModel):
|
||||
"""
|
||||
Voice list result
|
||||
|
||||
Attributes:
|
||||
voice_list (List[VoiceData]): List of voices
|
||||
"""
|
||||
|
||||
voice_list: List[VoiceData]
|
||||
86
tests/integration_tests/test_voice_clone.py
Normal file
86
tests/integration_tests/test_voice_clone.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import logging
|
||||
import logging.config
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import zai
|
||||
from zai import ZaiClient
|
||||
|
||||
def test_voice_clone(logging_conf):
|
||||
"""Test voice cloning with existing file ID"""
|
||||
logging.config.dictConfig(logging_conf) # type: ignore
|
||||
client = ZaiClient() # Fill in your own API Key
|
||||
|
||||
try:
|
||||
# Upload file first
|
||||
voice_input_file_path = Path(__file__).parent / 'voice_clone_input.mp3'
|
||||
with open(voice_input_file_path, 'rb') as f:
|
||||
upload_response = client.files.create(
|
||||
file=f,
|
||||
purpose='voice-clone-input',
|
||||
)
|
||||
|
||||
# Clone voice
|
||||
request_id = f"voice_clone_only_test_{int(time.time() * 1000)}"
|
||||
response = client.voice.clone(
|
||||
voice_name="Test Voice Clone Only",
|
||||
text="This is sample text for voice cloning training",
|
||||
input="This is target text for voice preview generation",
|
||||
file_id=upload_response.id,
|
||||
request_id=request_id,
|
||||
model="cogtts-clone"
|
||||
)
|
||||
print(f"Voice clone response: {response}")
|
||||
|
||||
except FileNotFoundError:
|
||||
print("Voice input file not found: voice_clone_input.mp3")
|
||||
except zai.core._errors.APIRequestFailedError as err:
|
||||
print(f"API Request Failed: {err}")
|
||||
except zai.core._errors.APIInternalError as err:
|
||||
print(f"API Internal Error: {err}")
|
||||
except zai.core._errors.APIStatusError as err:
|
||||
print(f"API Status Error: {err}")
|
||||
|
||||
|
||||
def test_voice_list(logging_conf):
|
||||
"""Test voice listing functionality"""
|
||||
logging.config.dictConfig(logging_conf) # type: ignore
|
||||
client = ZaiClient() # Fill in your own API Key
|
||||
|
||||
try:
|
||||
request_id = f"voice_list_test_{int(time.time() * 1000)}"
|
||||
response = client.voice.list(
|
||||
voice_type="PRIVATE",
|
||||
request_id=request_id
|
||||
)
|
||||
print(f"Voice list response: {response}")
|
||||
|
||||
except zai.core._errors.APIRequestFailedError as err:
|
||||
print(f"API Request Failed: {err}")
|
||||
except zai.core._errors.APIInternalError as err:
|
||||
print(f"API Internal Error: {err}")
|
||||
except zai.core._errors.APIStatusError as err:
|
||||
print(f"API Status Error: {err}")
|
||||
|
||||
|
||||
def test_voice_delete(logging_conf):
|
||||
"""Test voice deletion functionality"""
|
||||
logging.config.dictConfig(logging_conf) # type: ignore
|
||||
client = ZaiClient() # Fill in your own API Key
|
||||
|
||||
try:
|
||||
# Note: Replace with actual voice from a previous clone operation
|
||||
voice = "test_voice_placeholder"
|
||||
request_id = f"voice_delete_test_{int(time.time() * 1000)}"
|
||||
response = client.voice.delete(
|
||||
voice=voice,
|
||||
request_id=request_id
|
||||
)
|
||||
print(f"Voice delete response: {response}")
|
||||
|
||||
except zai.core._errors.APIRequestFailedError as err:
|
||||
print(f"API Request Failed: {err}")
|
||||
except zai.core._errors.APIInternalError as err:
|
||||
print(f"API Internal Error: {err}")
|
||||
except zai.core._errors.APIStatusError as err:
|
||||
print(f"API Status Error: {err}")
|
||||
BIN
tests/integration_tests/voice_clone_input.mp3
Normal file
BIN
tests/integration_tests/voice_clone_input.mp3
Normal file
Binary file not shown.
Loading…
Reference in a new issue