langchain-camb 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- langchain_camb/__init__.py +81 -0
- langchain_camb/toolkits/__init__.py +5 -0
- langchain_camb/toolkits/camb_toolkit.py +148 -0
- langchain_camb/tools/__init__.py +40 -0
- langchain_camb/tools/audio_separation.py +189 -0
- langchain_camb/tools/base.py +161 -0
- langchain_camb/tools/text_to_sound.py +156 -0
- langchain_camb/tools/transcription.py +189 -0
- langchain_camb/tools/translated_tts.py +340 -0
- langchain_camb/tools/translation.py +150 -0
- langchain_camb/tools/tts.py +182 -0
- langchain_camb/tools/voice_clone.py +152 -0
- langchain_camb/tools/voice_list.py +108 -0
- langchain_camb/version.py +3 -0
- langchain_camb-0.1.0.dist-info/METADATA +307 -0
- langchain_camb-0.1.0.dist-info/RECORD +17 -0
- langchain_camb-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Translation tool for CAMB AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal, Optional, Type
|
|
6
|
+
|
|
7
|
+
from langchain_core.callbacks import (
|
|
8
|
+
AsyncCallbackManagerForToolRun,
|
|
9
|
+
CallbackManagerForToolRun,
|
|
10
|
+
)
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
from langchain_camb.tools.base import CambBaseTool
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TranslationInput(BaseModel):
|
|
17
|
+
"""Input schema for Translation tool."""
|
|
18
|
+
|
|
19
|
+
text: str = Field(
|
|
20
|
+
...,
|
|
21
|
+
description="Text to translate.",
|
|
22
|
+
)
|
|
23
|
+
source_language: int = Field(
|
|
24
|
+
...,
|
|
25
|
+
description="Source language code (integer). Common codes: 1=English, 2=Spanish, 3=French, 4=German, 5=Italian, 6=Portuguese, 7=Dutch, 8=Russian, 9=Japanese, 10=Korean, 11=Chinese.",
|
|
26
|
+
)
|
|
27
|
+
target_language: int = Field(
|
|
28
|
+
...,
|
|
29
|
+
description="Target language code (integer). Use same language codes as source_language.",
|
|
30
|
+
)
|
|
31
|
+
formality: Optional[int] = Field(
|
|
32
|
+
default=None,
|
|
33
|
+
description="Formality level: 1=formal, 2=informal. Optional.",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CambTranslationTool(CambBaseTool):
|
|
38
|
+
"""Tool for translating text using CAMB AI.
|
|
39
|
+
|
|
40
|
+
This tool provides high-quality machine translation supporting 140+ languages.
|
|
41
|
+
It uses CAMB AI's streaming translation API for fast responses.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
```python
|
|
45
|
+
from langchain_camb import CambTranslationTool
|
|
46
|
+
|
|
47
|
+
translator = CambTranslationTool()
|
|
48
|
+
result = translator.invoke({
|
|
49
|
+
"text": "Hello, how are you?",
|
|
50
|
+
"source_language": 1, # English
|
|
51
|
+
"target_language": 2, # Spanish
|
|
52
|
+
})
|
|
53
|
+
print(result) # "Hola, ¿cómo estás?"
|
|
54
|
+
```
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
name: str = "camb_translation"
|
|
58
|
+
description: str = (
|
|
59
|
+
"Translate text between 140+ languages using CAMB AI. "
|
|
60
|
+
"Provide source and target language codes (integers) and the text to translate. "
|
|
61
|
+
"Common codes: 1=English, 2=Spanish, 3=French, 4=German, 5=Italian."
|
|
62
|
+
)
|
|
63
|
+
args_schema: Type[BaseModel] = TranslationInput
|
|
64
|
+
|
|
65
|
+
def _run(
|
|
66
|
+
self,
|
|
67
|
+
text: str,
|
|
68
|
+
source_language: int,
|
|
69
|
+
target_language: int,
|
|
70
|
+
formality: Optional[int] = None,
|
|
71
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
72
|
+
) -> str:
|
|
73
|
+
"""Translate text synchronously.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Translated text string.
|
|
77
|
+
"""
|
|
78
|
+
from camb.core.api_error import ApiError
|
|
79
|
+
|
|
80
|
+
kwargs = {
|
|
81
|
+
"text": text,
|
|
82
|
+
"source_language": source_language,
|
|
83
|
+
"target_language": target_language,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if formality:
|
|
87
|
+
kwargs["formality"] = formality
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
result = self.sync_client.translation.translation_stream(**kwargs)
|
|
91
|
+
return self._extract_text(result)
|
|
92
|
+
except ApiError as e:
|
|
93
|
+
# SDK bug: translation_stream returns plain text but SDK tries to parse as JSON
|
|
94
|
+
# If status is 200, the body contains the translated text
|
|
95
|
+
if e.status_code == 200 and e.body:
|
|
96
|
+
return str(e.body)
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
async def _arun(
|
|
100
|
+
self,
|
|
101
|
+
text: str,
|
|
102
|
+
source_language: int,
|
|
103
|
+
target_language: int,
|
|
104
|
+
formality: Optional[int] = None,
|
|
105
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
106
|
+
) -> str:
|
|
107
|
+
"""Translate text asynchronously.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Translated text string.
|
|
111
|
+
"""
|
|
112
|
+
from camb.core.api_error import ApiError
|
|
113
|
+
|
|
114
|
+
kwargs = {
|
|
115
|
+
"text": text,
|
|
116
|
+
"source_language": source_language,
|
|
117
|
+
"target_language": target_language,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if formality:
|
|
121
|
+
kwargs["formality"] = formality
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
result = await self.async_client.translation.translation_stream(**kwargs)
|
|
125
|
+
return self._extract_text(result)
|
|
126
|
+
except ApiError as e:
|
|
127
|
+
# SDK bug: translation_stream returns plain text but SDK tries to parse as JSON
|
|
128
|
+
# If status is 200, the body contains the translated text
|
|
129
|
+
if e.status_code == 200 and e.body:
|
|
130
|
+
return str(e.body)
|
|
131
|
+
raise
|
|
132
|
+
|
|
133
|
+
def _extract_text(self, result) -> str:
|
|
134
|
+
"""Extract text from various result types."""
|
|
135
|
+
# Handle streaming response - collect all chunks
|
|
136
|
+
if hasattr(result, "__iter__") and not isinstance(result, (str, bytes)):
|
|
137
|
+
chunks = []
|
|
138
|
+
for chunk in result:
|
|
139
|
+
if hasattr(chunk, "text"):
|
|
140
|
+
chunks.append(chunk.text)
|
|
141
|
+
elif isinstance(chunk, str):
|
|
142
|
+
chunks.append(chunk)
|
|
143
|
+
return "".join(chunks)
|
|
144
|
+
|
|
145
|
+
# Direct result
|
|
146
|
+
if hasattr(result, "text"):
|
|
147
|
+
return result.text
|
|
148
|
+
if isinstance(result, str):
|
|
149
|
+
return result
|
|
150
|
+
return str(result)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Text-to-Speech tool for CAMB AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import tempfile
|
|
7
|
+
from typing import Any, Literal, Optional, Type, Union
|
|
8
|
+
|
|
9
|
+
from langchain_core.callbacks import (
|
|
10
|
+
AsyncCallbackManagerForToolRun,
|
|
11
|
+
CallbackManagerForToolRun,
|
|
12
|
+
)
|
|
13
|
+
from pydantic import BaseModel, Field
|
|
14
|
+
|
|
15
|
+
from langchain_camb.tools.base import CambBaseTool
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TTSInput(BaseModel):
|
|
19
|
+
"""Input schema for Text-to-Speech tool."""
|
|
20
|
+
|
|
21
|
+
text: str = Field(
|
|
22
|
+
...,
|
|
23
|
+
min_length=3,
|
|
24
|
+
max_length=3000,
|
|
25
|
+
description="Text to convert to speech (3-3000 characters).",
|
|
26
|
+
)
|
|
27
|
+
language: str = Field(
|
|
28
|
+
default="en-us",
|
|
29
|
+
description="BCP-47 language code (e.g., 'en-us', 'es-es', 'fr-fr').",
|
|
30
|
+
)
|
|
31
|
+
voice_id: int = Field(
|
|
32
|
+
default=147320,
|
|
33
|
+
description="Voice ID to use. Get available voices with CambVoiceListTool.",
|
|
34
|
+
)
|
|
35
|
+
speech_model: str = Field(
|
|
36
|
+
default="mars-flash",
|
|
37
|
+
description="Speech model: 'mars-flash' (fast), 'mars-pro' (quality), 'mars-instruct' (with instructions).",
|
|
38
|
+
)
|
|
39
|
+
output_format: Literal["file_path", "base64", "bytes"] = Field(
|
|
40
|
+
default="file_path",
|
|
41
|
+
description="Output format: 'file_path' (save to file), 'base64' (encoded string), 'bytes' (raw bytes).",
|
|
42
|
+
)
|
|
43
|
+
speed: float = Field(
|
|
44
|
+
default=1.0,
|
|
45
|
+
ge=0.5,
|
|
46
|
+
le=2.0,
|
|
47
|
+
description="Speech speed multiplier (0.5-2.0).",
|
|
48
|
+
)
|
|
49
|
+
user_instructions: Optional[str] = Field(
|
|
50
|
+
default=None,
|
|
51
|
+
description="Instructions for mars-instruct model (e.g., 'Speak with excitement').",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CambTTSTool(CambBaseTool):
|
|
56
|
+
"""Tool for converting text to speech using CAMB AI.
|
|
57
|
+
|
|
58
|
+
This tool uses CAMB AI's streaming TTS API to convert text into natural
|
|
59
|
+
speech in 140+ languages. Supports multiple voice models and output formats.
|
|
60
|
+
|
|
61
|
+
Example:
|
|
62
|
+
```python
|
|
63
|
+
from langchain_camb import CambTTSTool
|
|
64
|
+
|
|
65
|
+
tts = CambTTSTool()
|
|
66
|
+
result = tts.invoke({
|
|
67
|
+
"text": "Hello, world!",
|
|
68
|
+
"language": "en-us",
|
|
69
|
+
"voice_id": 147320
|
|
70
|
+
})
|
|
71
|
+
print(result) # Returns file path to audio
|
|
72
|
+
```
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
name: str = "camb_tts"
|
|
76
|
+
description: str = (
|
|
77
|
+
"Convert text to speech using CAMB AI. "
|
|
78
|
+
"Supports 140+ languages and multiple voice models. "
|
|
79
|
+
"Returns audio as file path, base64, or raw bytes."
|
|
80
|
+
)
|
|
81
|
+
args_schema: Type[BaseModel] = TTSInput
|
|
82
|
+
|
|
83
|
+
def _run(
|
|
84
|
+
self,
|
|
85
|
+
text: str,
|
|
86
|
+
language: str = "en-us",
|
|
87
|
+
voice_id: int = 147320,
|
|
88
|
+
speech_model: str = "mars-flash",
|
|
89
|
+
output_format: str = "file_path",
|
|
90
|
+
speed: float = 1.0,
|
|
91
|
+
user_instructions: Optional[str] = None,
|
|
92
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
93
|
+
) -> Union[str, bytes]:
|
|
94
|
+
"""Run text-to-speech conversion synchronously.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
File path, base64 string, or raw bytes depending on output_format.
|
|
98
|
+
"""
|
|
99
|
+
from camb import (
|
|
100
|
+
StreamTtsOutputConfiguration,
|
|
101
|
+
StreamTtsVoiceSettings,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Build request parameters
|
|
105
|
+
kwargs: dict[str, Any] = {
|
|
106
|
+
"text": text,
|
|
107
|
+
"language": language,
|
|
108
|
+
"voice_id": voice_id,
|
|
109
|
+
"speech_model": speech_model,
|
|
110
|
+
"output_configuration": StreamTtsOutputConfiguration(format="wav"),
|
|
111
|
+
"voice_settings": StreamTtsVoiceSettings(speed=speed),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if user_instructions and speech_model == "mars-instruct":
|
|
115
|
+
kwargs["user_instructions"] = user_instructions
|
|
116
|
+
|
|
117
|
+
# Stream audio chunks
|
|
118
|
+
audio_chunks: list[bytes] = []
|
|
119
|
+
for chunk in self.sync_client.text_to_speech.tts(**kwargs):
|
|
120
|
+
audio_chunks.append(chunk)
|
|
121
|
+
|
|
122
|
+
audio_data = b"".join(audio_chunks)
|
|
123
|
+
|
|
124
|
+
return self._format_output(audio_data, output_format)
|
|
125
|
+
|
|
126
|
+
async def _arun(
|
|
127
|
+
self,
|
|
128
|
+
text: str,
|
|
129
|
+
language: str = "en-us",
|
|
130
|
+
voice_id: int = 147320,
|
|
131
|
+
speech_model: str = "mars-flash",
|
|
132
|
+
output_format: str = "file_path",
|
|
133
|
+
speed: float = 1.0,
|
|
134
|
+
user_instructions: Optional[str] = None,
|
|
135
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
136
|
+
) -> Union[str, bytes]:
|
|
137
|
+
"""Run text-to-speech conversion asynchronously.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
File path, base64 string, or raw bytes depending on output_format.
|
|
141
|
+
"""
|
|
142
|
+
from camb import (
|
|
143
|
+
StreamTtsOutputConfiguration,
|
|
144
|
+
StreamTtsVoiceSettings,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Build request parameters
|
|
148
|
+
kwargs: dict[str, Any] = {
|
|
149
|
+
"text": text,
|
|
150
|
+
"language": language,
|
|
151
|
+
"voice_id": voice_id,
|
|
152
|
+
"speech_model": speech_model,
|
|
153
|
+
"output_configuration": StreamTtsOutputConfiguration(format="wav"),
|
|
154
|
+
"voice_settings": StreamTtsVoiceSettings(speed=speed),
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if user_instructions and speech_model == "mars-instruct":
|
|
158
|
+
kwargs["user_instructions"] = user_instructions
|
|
159
|
+
|
|
160
|
+
# Stream audio chunks
|
|
161
|
+
audio_chunks: list[bytes] = []
|
|
162
|
+
async for chunk in self.async_client.text_to_speech.tts(**kwargs):
|
|
163
|
+
audio_chunks.append(chunk)
|
|
164
|
+
|
|
165
|
+
audio_data = b"".join(audio_chunks)
|
|
166
|
+
|
|
167
|
+
return self._format_output(audio_data, output_format)
|
|
168
|
+
|
|
169
|
+
def _format_output(
|
|
170
|
+
self, audio_data: bytes, output_format: str
|
|
171
|
+
) -> Union[str, bytes]:
|
|
172
|
+
"""Format audio data according to output_format."""
|
|
173
|
+
if output_format == "bytes":
|
|
174
|
+
return audio_data
|
|
175
|
+
elif output_format == "base64":
|
|
176
|
+
return base64.b64encode(audio_data).decode("utf-8")
|
|
177
|
+
else: # file_path
|
|
178
|
+
with tempfile.NamedTemporaryFile(
|
|
179
|
+
suffix=".wav", delete=False
|
|
180
|
+
) as f:
|
|
181
|
+
f.write(audio_data)
|
|
182
|
+
return f.name
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Voice clone tool for CAMB AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, Type
|
|
7
|
+
|
|
8
|
+
from langchain_core.callbacks import (
|
|
9
|
+
AsyncCallbackManagerForToolRun,
|
|
10
|
+
CallbackManagerForToolRun,
|
|
11
|
+
)
|
|
12
|
+
from pydantic import BaseModel, Field
|
|
13
|
+
|
|
14
|
+
from langchain_camb.tools.base import CambBaseTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VoiceCloneInput(BaseModel):
|
|
18
|
+
"""Input schema for Voice Clone tool."""
|
|
19
|
+
|
|
20
|
+
voice_name: str = Field(
|
|
21
|
+
...,
|
|
22
|
+
description="Name for the new cloned voice.",
|
|
23
|
+
)
|
|
24
|
+
audio_file_path: str = Field(
|
|
25
|
+
...,
|
|
26
|
+
description="Path to audio file (2+ seconds) to clone voice from.",
|
|
27
|
+
)
|
|
28
|
+
gender: int = Field(
|
|
29
|
+
...,
|
|
30
|
+
description="Gender: 1=Male, 2=Female, 0=Not Specified, 9=Not Applicable.",
|
|
31
|
+
)
|
|
32
|
+
description: Optional[str] = Field(
|
|
33
|
+
default=None,
|
|
34
|
+
description="Optional description of the voice.",
|
|
35
|
+
)
|
|
36
|
+
age: Optional[int] = Field(
|
|
37
|
+
default=None,
|
|
38
|
+
description="Optional age of the voice.",
|
|
39
|
+
)
|
|
40
|
+
language: Optional[int] = Field(
|
|
41
|
+
default=None,
|
|
42
|
+
description="Optional language code for the voice.",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CambVoiceCloneTool(CambBaseTool):
|
|
47
|
+
"""Tool for cloning voices using CAMB AI.
|
|
48
|
+
|
|
49
|
+
This tool creates a new voice from a 2+ second audio sample.
|
|
50
|
+
The cloned voice can then be used with TTS tools.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
```python
|
|
54
|
+
from langchain_camb import CambVoiceCloneTool
|
|
55
|
+
|
|
56
|
+
clone = CambVoiceCloneTool()
|
|
57
|
+
result = clone.invoke({
|
|
58
|
+
"voice_name": "My Custom Voice",
|
|
59
|
+
"audio_file_path": "/path/to/audio.wav",
|
|
60
|
+
"gender": 2 # Female
|
|
61
|
+
})
|
|
62
|
+
print(result) # JSON with new voice_id
|
|
63
|
+
```
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
name: str = "camb_voice_clone"
|
|
67
|
+
description: str = (
|
|
68
|
+
"Clone a voice from an audio sample using CAMB AI. "
|
|
69
|
+
"Requires 2+ seconds of audio. "
|
|
70
|
+
"Returns the new voice ID that can be used with TTS tools. "
|
|
71
|
+
"Gender: 1=Male, 2=Female, 0=Not Specified."
|
|
72
|
+
)
|
|
73
|
+
args_schema: Type[BaseModel] = VoiceCloneInput
|
|
74
|
+
|
|
75
|
+
def _run(
|
|
76
|
+
self,
|
|
77
|
+
voice_name: str,
|
|
78
|
+
audio_file_path: str,
|
|
79
|
+
gender: int,
|
|
80
|
+
description: Optional[str] = None,
|
|
81
|
+
age: Optional[int] = None,
|
|
82
|
+
language: Optional[int] = None,
|
|
83
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""Clone a voice synchronously.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
JSON string with new voice_id and details.
|
|
89
|
+
"""
|
|
90
|
+
with open(audio_file_path, "rb") as f:
|
|
91
|
+
kwargs = {
|
|
92
|
+
"voice_name": voice_name,
|
|
93
|
+
"gender": gender,
|
|
94
|
+
"file": f,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if description:
|
|
98
|
+
kwargs["description"] = description
|
|
99
|
+
if age:
|
|
100
|
+
kwargs["age"] = age
|
|
101
|
+
if language:
|
|
102
|
+
kwargs["language"] = language
|
|
103
|
+
|
|
104
|
+
result = self.sync_client.voice_cloning.create_custom_voice(**kwargs)
|
|
105
|
+
|
|
106
|
+
return self._format_result(result, voice_name)
|
|
107
|
+
|
|
108
|
+
async def _arun(
|
|
109
|
+
self,
|
|
110
|
+
voice_name: str,
|
|
111
|
+
audio_file_path: str,
|
|
112
|
+
gender: int,
|
|
113
|
+
description: Optional[str] = None,
|
|
114
|
+
age: Optional[int] = None,
|
|
115
|
+
language: Optional[int] = None,
|
|
116
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Clone a voice asynchronously.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
JSON string with new voice_id and details.
|
|
122
|
+
"""
|
|
123
|
+
with open(audio_file_path, "rb") as f:
|
|
124
|
+
kwargs = {
|
|
125
|
+
"voice_name": voice_name,
|
|
126
|
+
"gender": gender,
|
|
127
|
+
"file": f,
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if description:
|
|
131
|
+
kwargs["description"] = description
|
|
132
|
+
if age:
|
|
133
|
+
kwargs["age"] = age
|
|
134
|
+
if language:
|
|
135
|
+
kwargs["language"] = language
|
|
136
|
+
|
|
137
|
+
result = await self.async_client.voice_cloning.create_custom_voice(**kwargs)
|
|
138
|
+
|
|
139
|
+
return self._format_result(result, voice_name)
|
|
140
|
+
|
|
141
|
+
def _format_result(self, result, voice_name: str) -> str:
|
|
142
|
+
"""Format the voice clone result as JSON."""
|
|
143
|
+
output = {
|
|
144
|
+
"voice_id": getattr(result, "voice_id", getattr(result, "id", None)),
|
|
145
|
+
"voice_name": voice_name,
|
|
146
|
+
"status": "created",
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if hasattr(result, "message"):
|
|
150
|
+
output["message"] = result.message
|
|
151
|
+
|
|
152
|
+
return json.dumps(output, indent=2)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Voice list tool for CAMB AI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any, Optional, Type
|
|
7
|
+
|
|
8
|
+
from langchain_core.callbacks import (
|
|
9
|
+
AsyncCallbackManagerForToolRun,
|
|
10
|
+
CallbackManagerForToolRun,
|
|
11
|
+
)
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
from langchain_camb.tools.base import CambBaseTool
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class VoiceListInput(BaseModel):
|
|
18
|
+
"""Input schema for Voice List tool (no parameters required)."""
|
|
19
|
+
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CambVoiceListTool(CambBaseTool):
|
|
24
|
+
"""Tool for listing available voices from CAMB AI.
|
|
25
|
+
|
|
26
|
+
This tool retrieves all available voices that can be used with TTS tools.
|
|
27
|
+
Returns voice ID, name, gender, age, and language information.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
```python
|
|
31
|
+
from langchain_camb import CambVoiceListTool
|
|
32
|
+
|
|
33
|
+
voice_list = CambVoiceListTool()
|
|
34
|
+
voices = voice_list.invoke({})
|
|
35
|
+
print(voices) # JSON list of available voices
|
|
36
|
+
```
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
name: str = "camb_voice_list"
|
|
40
|
+
description: str = (
|
|
41
|
+
"List all available voices from CAMB AI. "
|
|
42
|
+
"Returns voice IDs, names, genders, ages, and languages. "
|
|
43
|
+
"Use this to find the right voice_id for TTS tools."
|
|
44
|
+
)
|
|
45
|
+
args_schema: Type[BaseModel] = VoiceListInput
|
|
46
|
+
|
|
47
|
+
def _run(
|
|
48
|
+
self,
|
|
49
|
+
run_manager: Optional[CallbackManagerForToolRun] = None,
|
|
50
|
+
) -> str:
|
|
51
|
+
"""Get list of available voices synchronously.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
JSON string containing list of voices with id, name, gender, age, language.
|
|
55
|
+
"""
|
|
56
|
+
voices = self.sync_client.voice_cloning.list_voices()
|
|
57
|
+
return self._format_voices(voices)
|
|
58
|
+
|
|
59
|
+
async def _arun(
|
|
60
|
+
self,
|
|
61
|
+
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
|
62
|
+
) -> str:
|
|
63
|
+
"""Get list of available voices asynchronously.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
JSON string containing list of voices with id, name, gender, age, language.
|
|
67
|
+
"""
|
|
68
|
+
voices = await self.async_client.voice_cloning.list_voices()
|
|
69
|
+
return self._format_voices(voices)
|
|
70
|
+
|
|
71
|
+
def _format_voices(self, voices: list[Any]) -> str:
|
|
72
|
+
"""Format voice list as JSON."""
|
|
73
|
+
voice_list = []
|
|
74
|
+
for voice in voices:
|
|
75
|
+
# Handle both dict and object responses
|
|
76
|
+
if isinstance(voice, dict):
|
|
77
|
+
voice_list.append(
|
|
78
|
+
{
|
|
79
|
+
"id": voice.get("id"),
|
|
80
|
+
"name": voice.get("voice_name", voice.get("name", "Unknown")),
|
|
81
|
+
"gender": self._gender_to_string(voice.get("gender", 0)),
|
|
82
|
+
"age": voice.get("age"),
|
|
83
|
+
"language": voice.get("language"),
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
voice_list.append(
|
|
88
|
+
{
|
|
89
|
+
"id": getattr(voice, "id", None),
|
|
90
|
+
"name": getattr(voice, "voice_name", getattr(voice, "name", "Unknown")),
|
|
91
|
+
"gender": self._gender_to_string(getattr(voice, "gender", 0)),
|
|
92
|
+
"age": getattr(voice, "age", None),
|
|
93
|
+
"language": getattr(voice, "language", None),
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return json.dumps(voice_list, indent=2)
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def _gender_to_string(gender: int) -> str:
|
|
101
|
+
"""Convert gender integer to string."""
|
|
102
|
+
gender_map = {
|
|
103
|
+
0: "not_specified",
|
|
104
|
+
1: "male",
|
|
105
|
+
2: "female",
|
|
106
|
+
9: "not_applicable",
|
|
107
|
+
}
|
|
108
|
+
return gender_map.get(gender, "unknown")
|