From b6d52610bf8c0272432f5c7834978a59c4957eb1 Mon Sep 17 00:00:00 2001 From: chyroc Date: Thu, 26 Dec 2024 19:23:08 +0800 Subject: [PATCH] feat: Add description and space_id parameters to voices.clone api (#152) - Update file type to FileTypes in voices.clone - Add optional space_id and description parameters in voices.clone - Add Unit Test --- cozepy/audio/voices/__init__.py | 61 +++++++++++++++++++++------------ tests/test_audio_voices.py | 44 ++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/cozepy/audio/voices/__init__.py b/cozepy/audio/voices/__init__.py index 442f5cc..17787c9 100644 --- a/cozepy/audio/voices/__init__.py +++ b/cozepy/audio/voices/__init__.py @@ -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): @@ -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: """ @@ -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) @@ -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: """ @@ -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) diff --git a/tests/test_audio_voices.py b/tests/test_audio_voices.py index 0aed1ef..3746b71 100644 --- a/tests/test_audio_voices.py +++ b/tests/test_audio_voices.py @@ -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, @@ -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): @@ -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 @@ -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