Skip to content

feat: Support voice_state/model_type for audio.voices.list #269

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion cozepy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
UpdateVoicePrintGroupFeatureResp,
VoicePrintGroupFeature,
)
from .audio.voices import Voice
from .audio.voices import Voice, VoiceModelType, VoiceState
from .auth import (
AsyncAuth,
AsyncDeviceOAuthApp,
Expand Down Expand Up @@ -223,6 +223,8 @@
# audio.rooms
"CreateRoomResp",
# audio.voices
"VoiceState",
"VoiceModelType",
"Voice",
"AudioFormat",
# audio.transcriptions
Expand Down
65 changes: 53 additions & 12 deletions cozepy/audio/voices/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,22 @@

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


class VoiceState(DynamicStrEnum):
INIT = "init" # 初始化
CLONED = "cloned" # 已克隆
ALL = "all" # 所有, 只有查询的时候有效


class VoiceModelType(DynamicStrEnum):
BIG = "big" # 大模型音色
SMALL = "small" # 小模型音色


class Voice(CozeModel):
# The id of voice
voice_id: str
Expand Down Expand Up @@ -38,6 +49,12 @@ class Voice(CozeModel):
# Voice last update timestamp
update_time: int

# Voice model type
model_type: VoiceModelType

# Voice state
state: VoiceState


class _PrivateListVoiceData(CozeModel, NumberPagedResponse[Voice]):
voice_list: List[Voice]
Expand Down Expand Up @@ -113,30 +130,41 @@ def list(
self,
*,
filter_system_voice: bool = False,
model_type: Optional[VoiceModelType] = None,
voice_state: Optional[VoiceState] = None,
page_num: int = 1,
page_size: int = 100,
**kwargs,
) -> NumberPaged[Voice]:
"""
Get available voices, including system voices + user cloned voices
Tips: Voices cloned under each Volcano account can be reused within the team

:param filter_system_voice: If True, system voices will not be returned.
:param model_type: The type of the voice.
:param voice_state: The state of the voice.
:param page_num: The page number for paginated queries. Default is 1, meaning the data return starts from the
first page.
:param page_size: The size of pagination. Default is 100, meaning that 100 data entries are returned per page.
:return: list of Voice
"""
url = f"{self._base_url}/v1/audio/voices"
headers: Optional[dict] = kwargs.get("headers")

def request_maker(i_page_num: int, i_page_size: int) -> HTTPRequest:
return self._requester.make_request(
"GET",
url,
params={
"filter_system_voice": filter_system_voice,
"page_num": i_page_num,
"page_size": i_page_size,
},
params=remove_none_values(
{
"filter_system_voice": filter_system_voice,
"voice_state": voice_state.value if voice_state else None,
"model_type": model_type.value if model_type else None,
"page_num": i_page_num,
"page_size": i_page_size,
}
),
headers=headers,
cast=_PrivateListVoiceData,
stream=False,
)
Expand Down Expand Up @@ -206,13 +234,22 @@ async def clone(
return await self._requester.arequest("post", url, False, Voice, headers=headers, body=body, files=files)

async def list(
self, *, filter_system_voice: bool = False, page_num: int = 1, page_size: int = 100, **kwargs
self,
*,
filter_system_voice: bool = False,
model_type: Optional[VoiceModelType] = None,
voice_state: Optional[VoiceState] = None,
page_num: int = 1,
page_size: int = 100,
**kwargs,
) -> AsyncNumberPaged[Voice]:
"""
Get available voices, including system voices + user cloned voices
Tips: Voices cloned under each Volcano account can be reused within the team

:param filter_system_voice: If True, system voices will not be returned.
:param model_type: The type of the voice.
:param voice_state: The state of the voice.
:param page_num: The page number for paginated queries. Default is 1, meaning the data return starts from the
first page.
:param page_size: The size of pagination. Default is 100, meaning that 100 data entries are returned per page.
Expand All @@ -225,11 +262,15 @@ async def request_maker(i_page_num: int, i_page_size: int) -> HTTPRequest:
return await self._requester.amake_request(
"GET",
url,
params={
"filter_system_voice": filter_system_voice,
"page_num": i_page_num,
"page_size": i_page_size,
},
params=remove_none_values(
{
"filter_system_voice": filter_system_voice,
"voice_state": voice_state.value if voice_state else None,
"model_type": model_type.value if model_type else None,
"page_num": i_page_num,
"page_size": i_page_size,
}
),
headers=headers,
cast=_PrivateListVoiceData,
stream=False,
Expand Down
47 changes: 20 additions & 27 deletions tests/test_audio_voices.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,35 @@
import httpx
import pytest

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


def mock_voice() -> Voice:
return 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()),
model_type=VoiceModelType.BIG,
state=VoiceState.INIT,
)


def mock_list_voices(respx_mock) -> str:
logid = random_hex(10)
raw_response = httpx.Response(
200,
json={
"data": {
"voice_list": [
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()),
).model_dump()
],
"voice_list": [mock_voice().model_dump()],
"has_more": False,
}
},
Expand All @@ -40,18 +44,7 @@ def mock_list_voices(respx_mock) -> str:


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 = mock_voice()
voice._raw_response = httpx.Response(
200,
json={"data": voice.model_dump()},
Expand Down
Loading