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.
@@ -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")
@@ -0,0 +1,3 @@
1
+ """Version information for langchain-camb."""
2
+
3
+ __version__ = "0.1.0"