diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0e82a37..f4c0e5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,36 @@ All notable changes to `audiostack` will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
+## [2.10.1] - 2025-02-24
+- Fixed logic that removed 0 float values from payload
+
+
+## [2.10.0] - 2025-02-13
+
+### Added
+
+- Customer trace id header can now be set per production using the `use_trace` context manager.
+- Can pass through custom headers into `send_request`.
+
+## [2.9.0] - 2025-01-23
+
+### Fixed
+
+- Removed emojis from PyPi document
+- Made example asset public
+
+## [2.8.2] - 2025-01-22
+
+### Improvement
+
+- Adding custom loudness presets support.
+
+## [2.8.0] - 2025-01-20
+
+### Added
+
+- Added Voice query endpoint to SDK.
+
## [2.7.1] - 2024-12-18
### Improvement
@@ -98,4 +128,4 @@ Improved error messaging for Media files
### Fixes
- Fixed missing argument in `content.list_modules`.
-- Fixed inclusion of missing `x-assume-org` header in `request_interface.download_url`.
+- Fixed inclusion of missing `x-assume-org` header in `request_interface.download_url`.
\ No newline at end of file
diff --git a/README.md b/README.md
index 2ea3f6e..6f3251f 100644
--- a/README.md
+++ b/README.md
@@ -11,15 +11,15 @@
-## 🧐 About
+## About
This repository is actively maintained by [Audiostack](https://audiostack.ai/). For examples, recipes and api reference see the [api.audio docs](https://docs.audiostack.ai/reference/quick-start). Feel free to get in touch with any questions or feedback!
-## :book: Changelog
+## Changelog
You can view [here](https://docs.audiostack.ai/changelog) our updated Changelog.
-## 🏁 Getting Started
+## Getting Started
### Installation
@@ -51,7 +51,7 @@ audiostack.assume_org_id = "your-org-id"
### Create your first audio asset
-#### ✍️ First, create a Script.
+#### First, create a Script.
Audiostack Scripts are the first step in creating audio assets. Not only do they contain the text to be spoken, but also determine the final structure of our audio asset using the [Script Syntax](https://docs.audiostack.ai/docs/script-syntax).
@@ -71,7 +71,7 @@ We are excited to see what you'll create with our product!
""")
```
-#### 🎤 Now, let's read it out load.
+#### Now, let's read it out load.
We integrate all the major TTS voices in the market. You can browse them in our [voice library](https://library.audiostack.ai/).
@@ -88,7 +88,7 @@ When you listen to these files, you'll notice each of them has a certain silence
tts = audiostack.Speech.TTS.remove_padding(speechId=tts.speechId)
```
-#### 🎛️ Now let's mix the speech we just created with a [sound template](https://library.audiostack.ai/sound).
+#### Now let's mix the speech we just created with a [sound template](https://library.audiostack.ai/sound).
```python
mix = audiostack.Production.Mix.create(speechItem=tts, soundTemplate="chill_vibes")
@@ -101,18 +101,18 @@ You can list all the sound templates to see what segments are available or even
Mixing comes with a lot of options to tune your audio to sound just right.
[More on this here.](https://docs.audiostack.ai/docs/advance-timing-parameters)
-#### 🎧 At this point, we can download the mix as a wave file, or convert it to another format.
+#### At this point, we can download the mix as a wave file, or convert it to another format.
```python
enc = audiostack.Delivery.Encoder.encode_mix(productionItem=mix, preset="mp3_high")
enc.download(fileName="example")
```
-Easy right? 🔮 This is the final result:
+Easy right? This is the final result:
-https://github.com/aflorithmic/audiostack-python/assets/64603095/6948cddb-4132-40a7-b84d-457f3fc0803d
+https://file.api.audio/pypi_example.mp3
-## :speedboat: More quickstarts
+## More quickstarts
Get started with our [quickstart recipes](https://docs.audiostack.ai/docs/introduction).
diff --git a/audiostack/delivery/encoder.py b/audiostack/delivery/encoder.py
index 838462e..1713b2a 100644
--- a/audiostack/delivery/encoder.py
+++ b/audiostack/delivery/encoder.py
@@ -44,6 +44,10 @@ def encode_mix(
format: str = "",
bitDepth: Optional[int] = None,
channels: Optional[int] = None,
+ loudnessSettings: str = "",
+ loudnessTarget: Optional[float] = None,
+ dynamicRange: Optional[float] = None,
+ truePeak: Optional[float] = None,
) -> Item:
if productionId and productionItem:
raise Exception(
@@ -65,7 +69,7 @@ def encode_mix(
if not preset:
raise Exception(
- "Either a an encoding preset (preset) or a loudness preset (loudnessPreset) should be supplied"
+ "Either an encoding preset (preset) or a loudness preset (loudnessPreset) should be supplied"
)
body = {
@@ -79,6 +83,10 @@ def encode_mix(
"bitDepth": bitDepth,
"channels": channels,
"loudnessPreset": loudnessPreset,
+ "loudnessSettings": loudnessSettings,
+ "loudnessTarget": loudnessTarget,
+ "dynamicRange": dynamicRange,
+ "truePeak": truePeak,
}
r = Encoder.interface.send_request(
rtype=RequestTypes.POST, route="encoder", json=body
diff --git a/audiostack/helpers/request_interface.py b/audiostack/helpers/request_interface.py
index 3ff8a6a..114f3ac 100644
--- a/audiostack/helpers/request_interface.py
+++ b/audiostack/helpers/request_interface.py
@@ -1,12 +1,18 @@
+import contextlib
import json
import shutil
-from typing import Any, Callable, Dict, Optional, Union
+from contextvars import ContextVar
+from typing import Any, Callable, Dict, Generator, Optional, Union
import requests
import audiostack
from audiostack.helpers.request_types import RequestTypes
+_current_trace_id: ContextVar[Optional[str]] = ContextVar(
+ "current_trace_id", default=None
+)
+
def remove_empty(data: Any) -> Any:
if not (isinstance(data, dict) or isinstance(data, list)):
@@ -14,7 +20,9 @@ def remove_empty(data: Any) -> Any:
final_dict = {}
for key, val in data.items(): # type: ignore
- if val or isinstance(val, int): # val = int(0) shoud not be removed
+ if (
+ val or isinstance(val, int) or isinstance(val, float)
+ ): # val = int(0), float(0) should not be removed
if isinstance(val, dict):
final_dict[key] = remove_empty(val)
elif isinstance(val, list):
@@ -32,14 +40,19 @@ def __init__(self, family: str) -> None:
self.family = family
@staticmethod
- def make_header() -> dict:
- header = {
+ def make_header(headers: Optional[dict] = None) -> dict:
+ new_headers = {
"x-api-key": audiostack.api_key,
"x-python-sdk-version": audiostack.sdk_version,
}
+ current_trace_id = _current_trace_id.get()
+ if current_trace_id is not None:
+ new_headers["x-customer-trace-id"] = current_trace_id
if audiostack.assume_org_id:
- header["x-assume-org"] = audiostack.assume_org_id
- return header
+ new_headers["x-assume-org"] = audiostack.assume_org_id
+ if headers:
+ new_headers.update(headers)
+ return new_headers
def resolve_response(self, r: Any) -> dict:
if self.DEBUG_PRINT:
@@ -82,6 +95,7 @@ def send_request(
path_parameters: Optional[Union[dict, str]] = None,
query_parameters: Optional[Union[dict, str]] = None,
overwrite_base_url: Optional[str] = None,
+ headers: Optional[dict] = None,
) -> Any:
if overwrite_base_url:
url = overwrite_base_url
@@ -111,7 +125,7 @@ def send_request(
}
return self.resolve_response(
- FUNC_MAP[rtype](url=url, json=json, headers=self.make_header())
+ FUNC_MAP[rtype](url=url, json=json, headers=self.make_header(headers))
)
elif rtype == RequestTypes.GET:
if path_parameters:
@@ -119,7 +133,7 @@ def send_request(
return self.resolve_response(
requests.get(
- url=url, params=query_parameters, headers=self.make_header()
+ url=url, params=query_parameters, headers=self.make_header(headers)
)
)
elif rtype == RequestTypes.DELETE:
@@ -128,7 +142,7 @@ def send_request(
return self.resolve_response(
requests.delete(
- url=url, params=query_parameters, headers=self.make_header()
+ url=url, params=query_parameters, headers=self.make_header(headers)
)
)
@@ -142,3 +156,12 @@ def download_url(cls, url: str, name: str, destination: str) -> None:
local_filename = f"{destination}/{name}"
with open(local_filename, "wb") as f:
shutil.copyfileobj(r.raw, f)
+
+
+@contextlib.contextmanager
+def use_trace(trace_id: str) -> Generator[None, None, None]:
+ token = _current_trace_id.set(trace_id)
+ try:
+ yield
+ finally:
+ _current_trace_id.reset(token)
diff --git a/audiostack/speech/voice.py b/audiostack/speech/voice.py
index 257f952..5586e24 100644
--- a/audiostack/speech/voice.py
+++ b/audiostack/speech/voice.py
@@ -1,4 +1,5 @@
-from typing import Any
+from typing import Any, Dict
+from typing import List as ListType
from audiostack.helpers.api_item import APIResponseItem
from audiostack.helpers.api_list import APIResponseList
@@ -25,6 +26,27 @@ def resolve_item(self, list_type: str, item: Any) -> "Voice.Item":
else:
raise Exception()
+ @staticmethod
+ def query(
+ filters: ListType[Dict] = [],
+ minimumNumberOfResults: int = 3,
+ forceApplyFilters: bool = True,
+ page: int = 1,
+ pageLimit: int = 1000,
+ ) -> "Voice.List":
+ body = {
+ "filters": filters,
+ "minimumNumberOfResults": minimumNumberOfResults,
+ "forceApplyFilters": forceApplyFilters,
+ "page": page,
+ "pageLimit": pageLimit,
+ }
+
+ r = Voice.interface.send_request(
+ rtype=RequestTypes.POST, route="query", json=body
+ )
+ return Voice.List(r, list_type="voices")
+
@staticmethod
def select_for_script(
scriptId: str = "", scriptItem: Any = "", tone: str = "", targetLength: int = 20
diff --git a/audiostack/tests/helpers/test_request_interface.py b/audiostack/tests/helpers/test_request_interface.py
index 48a46fd..2d9cad0 100644
--- a/audiostack/tests/helpers/test_request_interface.py
+++ b/audiostack/tests/helpers/test_request_interface.py
@@ -3,7 +3,8 @@
import pytest
import audiostack
-from audiostack.helpers.request_interface import RequestInterface
+from audiostack.helpers.request_interface import RequestInterface, use_trace
+from audiostack.helpers.request_types import RequestTypes
@patch("audiostack.helpers.request_interface.open")
@@ -59,3 +60,49 @@ def test_RequestInterface_download_url_4XX(
mock_requests.get.return_value.status_code = 400
with pytest.raises(Exception):
RequestInterface.download_url(url="foo", name="bar", destination="baz")
+
+
+@patch("audiostack.helpers.request_interface.requests")
+def test_RequestInterface_with_trace_id(mock_requests: Mock) -> None:
+ body = {
+ "scriptText": "scriptText",
+ "projectName": "projectName",
+ "moduleName": "moduleName",
+ "scriptName": "scriptName",
+ "metadata": "metadata",
+ }
+ with use_trace("trace_id"):
+ mock_requests.post.return_value.status_code = 200
+ interface = RequestInterface(family="content")
+ interface.send_request(rtype=RequestTypes.POST, route="script", json=body)
+ mock_requests.post.assert_called_once_with(
+ url=f"{audiostack.api_base}/content/script",
+ headers={
+ "x-api-key": audiostack.api_key,
+ "x-python-sdk-version": audiostack.sdk_version,
+ "x-customer-trace-id": "trace_id",
+ },
+ json=body,
+ )
+
+
+@patch("audiostack.helpers.request_interface.requests")
+def test_RequestInterface_with_no_trace_id(mock_requests: Mock) -> None:
+ body = {
+ "scriptText": "scriptText",
+ "projectName": "projectName",
+ "moduleName": "moduleName",
+ "scriptName": "scriptName",
+ "metadata": "metadata",
+ }
+ mock_requests.post.return_value.status_code = 200
+ interface = RequestInterface(family="content")
+ interface.send_request(rtype=RequestTypes.POST, route="script", json=body)
+ mock_requests.post.assert_called_once_with(
+ url=f"{audiostack.api_base}/content/script",
+ headers={
+ "x-api-key": audiostack.api_key,
+ "x-python-sdk-version": audiostack.sdk_version,
+ },
+ json=body,
+ )
diff --git a/audiostack/tests/tts/test_voice.py b/audiostack/tests/tts/test_voice.py
index 9666b2c..8bb17d0 100644
--- a/audiostack/tests/tts/test_voice.py
+++ b/audiostack/tests/tts/test_voice.py
@@ -1,12 +1,40 @@
import os
+from typing import Any
+from unittest.mock import Mock, patch
+
+from pytest import fixture
import audiostack
+from audiostack.helpers.api_item import APIResponseItem
+from audiostack.helpers.request_types import RequestTypes
from audiostack.speech.voice import Voice
audiostack.api_base = os.environ.get("AUDIO_STACK_DEV_URL", "https://v2.api.audio")
audiostack.api_key = os.environ["AUDIO_STACK_DEV_KEY"] # type: ignore
+@fixture
+def voice_data() -> dict:
+ return {
+ "response": "response",
+ "provider": "provider",
+ "alias": "alias",
+ }
+
+
+@fixture
+def mock_send_request() -> Any:
+ with patch("audiostack.speech.Voice.interface.send_request") as mock_:
+ yield mock_
+
+
+def test_item(voice_data: dict) -> None:
+ v = Voice.Item({"data": voice_data})
+ assert v.provider
+ assert v.alias
+ assert v.response
+
+
def test_list() -> None:
voices = Voice.list()
for v in voices:
@@ -19,3 +47,57 @@ def test_list() -> None:
def test_parmaters() -> None:
r = Voice.Parameter.get()
assert r
+
+
+def test_Voice_query(mock_send_request: Mock, voice_data: dict) -> None:
+ mock_send_request.return_value = {"data": {"voices": [voice_data] * 10}}
+ voices = Voice.query()
+ mock_send_request.assert_called_once_with(
+ rtype=RequestTypes.POST,
+ route="query",
+ json={
+ "filters": [],
+ "minimumNumberOfResults": 3,
+ "forceApplyFilters": True,
+ "page": 1,
+ "pageLimit": 1000,
+ },
+ )
+ assert isinstance(voices, Voice.List)
+ assert voices.data == {"voices": [voice_data] * 10}
+
+
+def test_Voice_query_with_parameters(mock_send_request: Mock, voice_data: dict) -> None:
+ mock_send_request.return_value = {"data": {"voices": [voice_data] * 10}}
+ filters = [{"in": {"language": ["dutch"]}}]
+ voices = Voice.query(
+ filters=filters,
+ minimumNumberOfResults=1,
+ forceApplyFilters=False,
+ pageLimit=100,
+ )
+ mock_send_request.assert_called_once_with(
+ rtype=RequestTypes.POST,
+ route="query",
+ json={
+ "filters": filters,
+ "minimumNumberOfResults": 1,
+ "forceApplyFilters": False,
+ "page": 1,
+ "pageLimit": 100,
+ },
+ )
+ assert isinstance(voices, Voice.List)
+ assert voices.data == {"voices": [voice_data] * 10}
+
+
+def test_Voice_select_for_script(mock_send_request: Mock, voice_data: dict) -> None:
+ mock_send_request.return_value = voice_data
+ r = Voice.select_for_script(scriptId="1")
+ mock_send_request.assert_called_once_with(
+ rtype=RequestTypes.POST,
+ route="select",
+ json={"scriptId": "1", "tone": "", "targetLength": 20},
+ )
+ assert isinstance(r, APIResponseItem)
+ assert r.response == voice_data
diff --git a/pyproject.toml b/pyproject.toml
index dee7090..1a75309 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,9 +1,10 @@
[tool.poetry]
name = "audiostack"
-version = "2.7.1"
+version = "2.10.1"
description = "Python SDK for Audiostack API"
authors = ["Aflorithmic "]
repository = "https://github.com/aflorithmic/audiostack-python"
+
readme = "README.md"
classifiers = [
"Programming Language :: Python :: 3",
@@ -12,6 +13,9 @@ classifiers = [
]
exclude = ["audiostack/tests"]
+[tool.poetry.urls]
+Changelog = "https://github.com/aflorithmic/audiostack-python/blob/main/CHANGELOG.md"
+
[tool.poetry.dependencies]
python = "^3.8.1"
requests = "^2.31.0"