indoxrouter 0.1.27__py3-none-any.whl → 0.1.29__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.
indoxrouter/client.py CHANGED
@@ -38,6 +38,9 @@ Usage example:
38
38
  # Generate text-to-speech audio
39
39
  audio = client.text_to_speech("Hello, welcome to IndoxRouter!", model="openai/tts-1", voice="alloy")
40
40
 
41
+ # Transcribe audio to text using speech-to-text
42
+ transcription = client.speech_to_text("path/to/audio.mp3", model="openai/whisper-1")
43
+
41
44
  # Using BYOK (Bring Your Own Key)
42
45
  response = client.chat([
43
46
  {"role": "user", "content": "Hello!"}
@@ -97,11 +100,14 @@ from .constants import (
97
100
  DEFAULT_EMBEDDING_MODEL,
98
101
  DEFAULT_IMAGE_MODEL,
99
102
  DEFAULT_TTS_MODEL,
103
+ DEFAULT_STT_MODEL,
100
104
  CHAT_ENDPOINT,
101
105
  COMPLETION_ENDPOINT,
102
106
  EMBEDDING_ENDPOINT,
103
107
  IMAGE_ENDPOINT,
104
108
  TTS_ENDPOINT,
109
+ STT_ENDPOINT,
110
+ STT_TRANSLATION_ENDPOINT,
105
111
  MODEL_ENDPOINT,
106
112
  USAGE_ENDPOINT,
107
113
  USE_COOKIES,
@@ -257,6 +263,7 @@ class Client:
257
263
  endpoint: str,
258
264
  data: Optional[Dict[str, Any]] = None,
259
265
  stream: bool = False,
266
+ files: Optional[Dict[str, Any]] = None,
260
267
  ) -> Any:
261
268
  """
262
269
  Make a request to the API.
@@ -266,6 +273,7 @@ class Client:
266
273
  endpoint: API endpoint
267
274
  data: Request data
268
275
  stream: Whether to stream the response
276
+ files: Files to upload (for multipart/form-data requests)
269
277
 
270
278
  Returns:
271
279
  Response data
@@ -279,7 +287,14 @@ class Client:
279
287
  endpoint = endpoint[1:]
280
288
 
281
289
  url = f"{self.base_url}/{endpoint}"
282
- headers = {"Content-Type": "application/json"}
290
+
291
+ # Set headers based on whether we're uploading files
292
+ if files:
293
+ # For multipart/form-data, don't set Content-Type header
294
+ # requests will set it automatically with boundary
295
+ headers = {}
296
+ else:
297
+ headers = {"Content-Type": "application/json"}
283
298
 
284
299
  # Add Authorization header if we have an access token
285
300
  if hasattr(self, "access_token") and self.access_token:
@@ -289,8 +304,8 @@ class Client:
289
304
  # if data:
290
305
  # logger.debug(f"Request data: {json.dumps(data, indent=2)}")
291
306
 
292
- # Diagnose potential issues with the request
293
- if method == "POST" and data:
307
+ # Diagnose potential issues with the request (only for non-file uploads)
308
+ if method == "POST" and data and not files:
294
309
  diagnosis = self.diagnose_request(endpoint, data)
295
310
  if not diagnosis["is_valid"]:
296
311
  issues_str = "\n".join([f"- {issue}" for issue in diagnosis["issues"]])
@@ -298,14 +313,25 @@ class Client:
298
313
  # We'll still send the request, but log the issues
299
314
 
300
315
  try:
301
- response = self.session.request(
302
- method,
303
- url,
304
- headers=headers,
305
- json=data,
306
- timeout=self.timeout,
307
- stream=stream,
308
- )
316
+ # Prepare request parameters
317
+ request_params = {
318
+ "method": method,
319
+ "url": url,
320
+ "headers": headers,
321
+ "timeout": self.timeout,
322
+ "stream": stream,
323
+ }
324
+
325
+ # Add data based on request type
326
+ if files:
327
+ # For file uploads, use form data
328
+ request_params["data"] = data
329
+ request_params["files"] = files
330
+ else:
331
+ # For regular requests, use JSON
332
+ request_params["json"] = data
333
+
334
+ response = self.session.request(**request_params)
309
335
 
310
336
  if stream:
311
337
  return response
@@ -318,16 +344,10 @@ class Client:
318
344
  # Update Authorization header with new token if available
319
345
  if hasattr(self, "access_token") and self.access_token:
320
346
  headers["Authorization"] = f"Bearer {self.access_token}"
347
+ request_params["headers"] = headers
321
348
 
322
349
  # Retry the request after reauthentication
323
- response = self.session.request(
324
- method,
325
- url,
326
- headers=headers,
327
- json=data,
328
- timeout=self.timeout,
329
- stream=stream,
330
- )
350
+ response = self.session.request(**request_params)
331
351
 
332
352
  if stream:
333
353
  return response
@@ -908,6 +928,231 @@ class Client:
908
928
 
909
929
  return self._request("POST", TTS_ENDPOINT, data)
910
930
 
931
+ def speech_to_text(
932
+ self,
933
+ file: Union[str, bytes],
934
+ model: str = DEFAULT_STT_MODEL,
935
+ language: Optional[str] = None,
936
+ prompt: Optional[str] = None,
937
+ response_format: Optional[str] = "json",
938
+ temperature: Optional[float] = 0.0,
939
+ timestamp_granularities: Optional[List[str]] = None,
940
+ byok_api_key: Optional[str] = None,
941
+ **kwargs,
942
+ ) -> Dict[str, Any]:
943
+ """
944
+ Transcribe audio to text using speech-to-text models.
945
+
946
+ Args:
947
+ file: Audio file path (str) or audio file data (bytes)
948
+ model: Model to use in the format "provider/model" (e.g., "openai/whisper-1")
949
+ language: Language code for the audio (e.g., "en", "es", "fr")
950
+ prompt: Optional text to guide the model's style
951
+ response_format: Format of the response ("json", "text", "srt", "verbose_json", "vtt")
952
+ temperature: Temperature for transcription (0.0 to 1.0)
953
+ timestamp_granularities: List of timestamp granularities (["word", "segment"])
954
+ byok_api_key: Your own API key for the provider (BYOK - Bring Your Own Key)
955
+ **kwargs: Additional parameters to pass to the API
956
+
957
+ Returns:
958
+ Response data with transcription text
959
+
960
+ Examples:
961
+ Basic usage with file path:
962
+ response = client.speech_to_text("path/to/audio.mp3")
963
+
964
+ Basic usage with file bytes:
965
+ with open("audio.mp3", "rb") as f:
966
+ audio_data = f.read()
967
+ response = client.speech_to_text(audio_data)
968
+
969
+ With specific model and language:
970
+ response = client.speech_to_text(
971
+ "path/to/audio.wav",
972
+ model="openai/whisper-1",
973
+ language="en",
974
+ response_format="json"
975
+ )
976
+
977
+ With timestamps for detailed analysis:
978
+ response = client.speech_to_text(
979
+ "path/to/audio.mp3",
980
+ model="openai/whisper-1",
981
+ response_format="verbose_json",
982
+ timestamp_granularities=["word", "segment"]
983
+ )
984
+
985
+ Using BYOK (Bring Your Own Key):
986
+ response = client.speech_to_text(
987
+ "path/to/audio.mp3",
988
+ model="openai/whisper-1",
989
+ byok_api_key="sk-your-openai-key-here"
990
+ )
991
+ """
992
+ # Format the model string
993
+ formatted_model = self._format_model_string(model)
994
+
995
+ # Handle file input - can be a file path (str) or file data (bytes)
996
+ if isinstance(file, str):
997
+ # It's a file path, read the file
998
+ try:
999
+ with open(file, "rb") as f:
1000
+ file_data = f.read()
1001
+ filename = os.path.basename(file)
1002
+ except FileNotFoundError:
1003
+ raise InvalidParametersError(f"File not found: {file}")
1004
+ except Exception as e:
1005
+ raise InvalidParametersError(f"Error reading file {file}: {str(e)}")
1006
+ elif isinstance(file, bytes):
1007
+ # It's file data
1008
+ file_data = file
1009
+ filename = kwargs.get("filename", "audio_file")
1010
+ else:
1011
+ raise InvalidParametersError(
1012
+ "File must be either a file path (str) or file data (bytes)"
1013
+ )
1014
+
1015
+ # Prepare form data for multipart upload
1016
+ files = {"file": (filename, file_data, "audio/*")}
1017
+
1018
+ # Create the form data with required parameters
1019
+ data = {
1020
+ "model": formatted_model,
1021
+ }
1022
+
1023
+ # Add optional parameters only if they are provided
1024
+ if language is not None:
1025
+ data["language"] = language
1026
+ if prompt is not None:
1027
+ data["prompt"] = prompt
1028
+ if response_format is not None:
1029
+ data["response_format"] = response_format
1030
+ if temperature is not None:
1031
+ data["temperature"] = temperature
1032
+ if timestamp_granularities is not None:
1033
+ # Convert to JSON string as expected by the API
1034
+ data["timestamp_granularities"] = json.dumps(timestamp_granularities)
1035
+
1036
+ # Add BYOK API key if provided
1037
+ if byok_api_key:
1038
+ data["byok_api_key"] = byok_api_key
1039
+
1040
+ # Filter out problematic parameters from kwargs
1041
+ filtered_kwargs = {}
1042
+ for key, value in kwargs.items():
1043
+ if key not in [
1044
+ "filename",
1045
+ "return_generator",
1046
+ ]: # List of parameters to exclude
1047
+ filtered_kwargs[key] = value
1048
+
1049
+ # Add any additional parameters from kwargs
1050
+ if filtered_kwargs:
1051
+ data.update(filtered_kwargs)
1052
+
1053
+ return self._request("POST", STT_ENDPOINT, data, files=files)
1054
+
1055
+ def translate_audio(
1056
+ self,
1057
+ file: Union[str, bytes],
1058
+ model: str = DEFAULT_STT_MODEL,
1059
+ prompt: Optional[str] = None,
1060
+ response_format: Optional[str] = "json",
1061
+ temperature: Optional[float] = 0.0,
1062
+ byok_api_key: Optional[str] = None,
1063
+ **kwargs,
1064
+ ) -> Dict[str, Any]:
1065
+ """
1066
+ Translate audio to English text using speech-to-text models.
1067
+
1068
+ Args:
1069
+ file: Audio file path (str) or audio file data (bytes)
1070
+ model: Model to use in the format "provider/model" (e.g., "openai/whisper-1")
1071
+ prompt: Optional text to guide the model's style
1072
+ response_format: Format of the response ("json", "text", "srt", "verbose_json", "vtt")
1073
+ temperature: Temperature for translation (0.0 to 1.0)
1074
+ byok_api_key: Your own API key for the provider (BYOK - Bring Your Own Key)
1075
+ **kwargs: Additional parameters to pass to the API
1076
+
1077
+ Returns:
1078
+ Response data with translated text in English
1079
+
1080
+ Examples:
1081
+ Basic usage with file path:
1082
+ response = client.translate_audio("path/to/spanish_audio.mp3")
1083
+
1084
+ With specific response format:
1085
+ response = client.translate_audio(
1086
+ "path/to/french_audio.wav",
1087
+ model="openai/whisper-1",
1088
+ response_format="text"
1089
+ )
1090
+
1091
+ Using BYOK (Bring Your Own Key):
1092
+ response = client.translate_audio(
1093
+ "path/to/audio.mp3",
1094
+ model="openai/whisper-1",
1095
+ byok_api_key="sk-your-openai-key-here"
1096
+ )
1097
+ """
1098
+ # Format the model string
1099
+ formatted_model = self._format_model_string(model)
1100
+
1101
+ # Handle file input - can be a file path (str) or file data (bytes)
1102
+ if isinstance(file, str):
1103
+ # It's a file path, read the file
1104
+ try:
1105
+ with open(file, "rb") as f:
1106
+ file_data = f.read()
1107
+ filename = os.path.basename(file)
1108
+ except FileNotFoundError:
1109
+ raise InvalidParametersError(f"File not found: {file}")
1110
+ except Exception as e:
1111
+ raise InvalidParametersError(f"Error reading file {file}: {str(e)}")
1112
+ elif isinstance(file, bytes):
1113
+ # It's file data
1114
+ file_data = file
1115
+ filename = kwargs.get("filename", "audio_file")
1116
+ else:
1117
+ raise InvalidParametersError(
1118
+ "File must be either a file path (str) or file data (bytes)"
1119
+ )
1120
+
1121
+ # Prepare form data for multipart upload
1122
+ files = {"file": (filename, file_data, "audio/*")}
1123
+
1124
+ # Create the form data with required parameters
1125
+ data = {
1126
+ "model": formatted_model,
1127
+ }
1128
+
1129
+ # Add optional parameters only if they are provided
1130
+ if prompt is not None:
1131
+ data["prompt"] = prompt
1132
+ if response_format is not None:
1133
+ data["response_format"] = response_format
1134
+ if temperature is not None:
1135
+ data["temperature"] = temperature
1136
+
1137
+ # Add BYOK API key if provided
1138
+ if byok_api_key:
1139
+ data["byok_api_key"] = byok_api_key
1140
+
1141
+ # Filter out problematic parameters from kwargs
1142
+ filtered_kwargs = {}
1143
+ for key, value in kwargs.items():
1144
+ if key not in [
1145
+ "filename",
1146
+ "return_generator",
1147
+ ]: # List of parameters to exclude
1148
+ filtered_kwargs[key] = value
1149
+
1150
+ # Add any additional parameters from kwargs
1151
+ if filtered_kwargs:
1152
+ data.update(filtered_kwargs)
1153
+
1154
+ return self._request("POST", STT_TRANSLATION_ENDPOINT, data, files=files)
1155
+
911
1156
  def _get_supported_parameters_for_model(
912
1157
  self, provider: str, model_name: str
913
1158
  ) -> List[str]:
indoxrouter/constants.py CHANGED
@@ -5,7 +5,7 @@ Constants for the IndoxRouter client.
5
5
  # API settings
6
6
  DEFAULT_API_VERSION = "v1"
7
7
  DEFAULT_BASE_URL = "https://api.indoxrouter.com" # Production server URL with HTTPS
8
- # DEFAULT_BASE_URL = "http://localhost:8000" # Local development server
8
+ # DEFAULT_BASE_URL = "http://localhost:9050" # Local development server
9
9
  DEFAULT_TIMEOUT = 60
10
10
  USE_COOKIES = True # Always use cookie-based authentication
11
11
 
@@ -14,6 +14,7 @@ DEFAULT_MODEL = "openai/gpt-4o-mini"
14
14
  DEFAULT_EMBEDDING_MODEL = "openai/text-embedding-3-small"
15
15
  DEFAULT_IMAGE_MODEL = "openai/dall-e-3"
16
16
  DEFAULT_TTS_MODEL = "openai/tts-1"
17
+ DEFAULT_STT_MODEL = "openai/whisper-1"
17
18
  GOOGLE_IMAGE_MODEL = "google/imagen-3.0-generate-002"
18
19
  XAI_IMAGE_MODEL = "xai/grok-2-image"
19
20
  XAI_IMAGE_LATEST_MODEL = "xai/grok-2-image-latest"
@@ -24,7 +25,9 @@ CHAT_ENDPOINT = "chat/completions"
24
25
  COMPLETION_ENDPOINT = "completions"
25
26
  EMBEDDING_ENDPOINT = "embeddings"
26
27
  IMAGE_ENDPOINT = "images/generations"
27
- TTS_ENDPOINT = "tts/generations"
28
+ TTS_ENDPOINT = "audio/tts/generations"
29
+ STT_ENDPOINT = "audio/stt/transcriptions"
30
+ STT_TRANSLATION_ENDPOINT = "audio/stt/translations"
28
31
  MODEL_ENDPOINT = "models"
29
32
  USAGE_ENDPOINT = "user/usage"
30
33
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: indoxrouter
3
- Version: 0.1.27
3
+ Version: 0.1.29
4
4
  Summary: A unified client for various AI providers
5
5
  Author-email: indoxRouter Team <ashkan.eskandari.dev@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,9 @@
1
+ indoxrouter/__init__.py,sha256=P_2eiiAi-DrkkeeJndQdEMqnVheWzOZIjWkDayrwxuk,1561
2
+ indoxrouter/client.py,sha256=dzeLN-DKHiHORhXA0zI_fFg5_Rvrd98uNXqpIFKobBQ,55234
3
+ indoxrouter/constants.py,sha256=rBTeOojwiokuN4F5DEt3DAmryXiCCtT7BBq9Gtc0arg,1590
4
+ indoxrouter/exceptions.py,sha256=cGlXNF8dd4X8UBWgxTA099nRhEkIjcqE_4iGOlIVVp8,1632
5
+ indoxrouter-0.1.29.dist-info/licenses/LICENSE,sha256=5n28CfoynFakg-QJIHnecEXcveN8gq-ZwhC0h7ATse0,24232
6
+ indoxrouter-0.1.29.dist-info/METADATA,sha256=2nYiD1nqnzRfjSqNWi6txEp075hE9XpnIoN1f7POUBk,5740
7
+ indoxrouter-0.1.29.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
+ indoxrouter-0.1.29.dist-info/top_level.txt,sha256=v6FGWkw0QAnXhyYtnXLI1cxzna0iveNvZUotVzCWabM,12
9
+ indoxrouter-0.1.29.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- indoxrouter/__init__.py,sha256=P_2eiiAi-DrkkeeJndQdEMqnVheWzOZIjWkDayrwxuk,1561
2
- indoxrouter/client.py,sha256=AirPauY2kFqpR46LWOtDBKs--_9ilFTc2RQHb7vsLnM,45694
3
- indoxrouter/constants.py,sha256=HqTXfNpL_UvWsR5mod8YZgWOs1W6x6itr1BSbmyYEHY,1451
4
- indoxrouter/exceptions.py,sha256=cGlXNF8dd4X8UBWgxTA099nRhEkIjcqE_4iGOlIVVp8,1632
5
- indoxrouter-0.1.27.dist-info/licenses/LICENSE,sha256=5n28CfoynFakg-QJIHnecEXcveN8gq-ZwhC0h7ATse0,24232
6
- indoxrouter-0.1.27.dist-info/METADATA,sha256=5bZ0cAhmLFMuRP8h_5uNOEPwu39xjIqUaT_6CEjyi_k,5740
7
- indoxrouter-0.1.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- indoxrouter-0.1.27.dist-info/top_level.txt,sha256=v6FGWkw0QAnXhyYtnXLI1cxzna0iveNvZUotVzCWabM,12
9
- indoxrouter-0.1.27.dist-info/RECORD,,