Skip to content

Commit

Permalink
feat: Add description and space_id parameters to voices.clone api (#152)
Browse files Browse the repository at this point in the history
- Update file type to FileTypes in voices.clone
- Add optional space_id and description parameters in voices.clone
- Add Unit Test
  • Loading branch information
chyroc authored Dec 26, 2024
1 parent 565a0f2 commit b6d5261
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 24 deletions.
61 changes: 39 additions & 22 deletions cozepy/audio/voices/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from typing import List, Optional, Union
from typing import List, Optional

from cozepy import AudioFormat
from cozepy.auth import Auth
from cozepy.files import FileTypes, _try_fix_file
from cozepy.model import AsyncNumberPaged, CozeModel, HTTPRequest, NumberPaged, NumberPagedResponse
from cozepy.request import Requester
from cozepy.util import remove_url_trailing_slash
from cozepy.util import remove_none_values, remove_url_trailing_slash


class Voice(CozeModel):
Expand Down Expand Up @@ -63,12 +64,14 @@ def clone(
self,
*,
voice_name: str,
file: Union[str],
file: FileTypes,
audio_format: AudioFormat,
language: Optional[str] = None,
voice_id: Optional[str] = None,
preview_text: Optional[str] = None,
text: Optional[str] = None,
space_id: Optional[str] = None,
description: Optional[str] = None,
**kwargs,
) -> Voice:
"""
Expand All @@ -86,19 +89,25 @@ def clone(
Otherwise, the default text "你好,我是你的专属AI克隆声音,希望未来可以一起好好相处哦".
:param text: Users can recite the text, and the service will compare the audio with the text.
If the difference is too large, an error will be returned.
:param space_id: The space id of the voice.
:param description: The description of the voice.
:return: Voice of the cloned.
"""
url = f"{self._base_url}/v1/audio/voices/clone"
headers: Optional[dict] = kwargs.get("headers")
body = {
"voice_name": voice_name,
"audio_format": audio_format,
"language": language,
"voice_id": voice_id,
"preview_text": preview_text,
"text": text,
}
files = {"file": file}
body = remove_none_values(
{
"voice_name": voice_name,
"audio_format": audio_format,
"language": language,
"voice_id": voice_id,
"preview_text": preview_text,
"text": text,
"space_id": space_id,
"description": description,
}
)
files = {"file": _try_fix_file(file)}

return self._requester.request("post", url, False, Voice, headers=headers, body=body, files=files)

Expand Down Expand Up @@ -153,12 +162,14 @@ async def clone(
self,
*,
voice_name: str,
file: Union[str],
file: FileTypes,
audio_format: AudioFormat,
language: Optional[str] = None,
voice_id: Optional[str] = None,
preview_text: Optional[str] = None,
text: Optional[str] = None,
space_id: Optional[str] = None,
description: Optional[str] = None,
**kwargs,
) -> Voice:
"""
Expand All @@ -176,19 +187,25 @@ async def clone(
Otherwise, the default text "你好,我是你的专属AI克隆声音,希望未来可以一起好好相处哦".
:param text: Users can recite the text, and the service will compare the audio with the text.
If the difference is too large, an error will be returned.
:param space_id: The space id of the voice.
:param description: The description of the voice.
:return: Voice of the cloned.
"""
url = f"{self._base_url}/v1/audio/voices/clone"
headers: Optional[dict] = kwargs.get("headers")
body = {
"voice_name": voice_name,
"audio_format": audio_format,
"language": language,
"voice_id": voice_id,
"preview_text": preview_text,
"text": text,
}
files = {"file": file}
body = remove_none_values(
{
"voice_name": voice_name,
"audio_format": audio_format,
"language": language,
"voice_id": voice_id,
"preview_text": preview_text,
"text": text,
"space_id": space_id,
"description": description,
}
)
files = {"file": _try_fix_file(file)}

return await self._requester.arequest("post", url, False, Voice, headers=headers, body=body, files=files)

Expand Down
44 changes: 42 additions & 2 deletions tests/test_audio_voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
import httpx
import pytest

from cozepy import AsyncCoze, Coze, TokenAuth, Voice
from cozepy import AsyncCoze, AudioFormat, Coze, TokenAuth, Voice
from cozepy.util import random_hex
from tests.test_util import logid_key


def mock_list_voices(respx_mock):
def mock_list_voices(respx_mock) -> str:
logid = random_hex(10)
raw_response = httpx.Response(
200,
Expand Down Expand Up @@ -39,6 +39,28 @@ def mock_list_voices(respx_mock):
return logid


def mock_clone_voice(respx_mock) -> Voice:
voice = Voice(
voice_id="voice_id",
name="name",
is_system_voice=False,
language_code="language_code",
language_name="language_name",
preview_text="preview_text",
preview_audio="preview_audio",
available_training_times=1,
create_time=int(time.time()),
update_time=int(time.time()),
)
voice._raw_response = httpx.Response(
200,
json={"data": voice.model_dump()},
headers={logid_key(): random_hex(10)},
)
respx_mock.post("/v1/audio/voices/clone").mock(voice._raw_response)
return voice


@pytest.mark.respx(base_url="https://api.coze.com")
class TestSyncAudioVoices:
def test_sync_voices_list(self, respx_mock):
Expand All @@ -53,6 +75,14 @@ def test_sync_voices_list(self, respx_mock):
assert voices
assert len(voices) == 1

def test_clone_voice(self, respx_mock):
coze = Coze(auth=TokenAuth(token="token"))

mock_voice = mock_clone_voice(respx_mock)

voice = coze.audio.voices.clone(voice_name="voice_name", file=("name", "content"), audio_format=AudioFormat.MP3)
assert voice.response.logid == mock_voice.response.logid


@pytest.mark.respx(base_url="https://api.coze.com")
@pytest.mark.asyncio
Expand All @@ -68,3 +98,13 @@ async def test_async_voices_list(self, respx_mock):
voices = [i async for i in voices]
assert voices
assert len(voices) == 1

async def test_async_clone_voice(self, respx_mock):
coze = AsyncCoze(auth=TokenAuth(token="token"))

mock_voice = mock_clone_voice(respx_mock)

voice = await coze.audio.voices.clone(
voice_name="voice_name", file=("name", "content"), audio_format=AudioFormat.MP3
)
assert voice.response.logid == mock_voice.response.logid

0 comments on commit b6d5261

Please sign in to comment.