Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f6805b7
version poetry
guillem-211 Jan 16, 2025
f433cba
feat: add query method to Voice with tests
harry97uk Jan 21, 2025
c4d99c9
updoate
guillem-211 Jan 22, 2025
5eb08ec
Merge branch 'main' into adding-loudness-params-to-sdk
guillem-211 Jan 22, 2025
afa2cdd
Merge pull request #66 from aflorithmic/adding-loudness-params-to-sdk
guillem-211 Jan 22, 2025
a3ba2e1
fix comit
guillem-211 Jan 22, 2025
387bd3e
Fix - remove emojis from section headers and add public asset link
harry97uk Jan 23, 2025
8b9c357
Include changelog
CodeBooster97 Jan 30, 2025
b87b0ba
bump version
CodeBooster97 Jan 30, 2025
b56829d
Merge pull request #71 from aflorithmic/fix/changelog
CodeBooster97 Jan 30, 2025
a8c1b0a
feat: add customer trace id header
harry97uk Feb 13, 2025
b343bcb
feat: enhance header creation to accept custom headers
harry97uk Feb 13, 2025
6dc2bc9
docs: update changelog to include custom headers feature
harry97uk Feb 13, 2025
083aeb1
feat: implement context manager for setting customer trace id
harry97uk Feb 14, 2025
018b319
chore: reorganize imports in request_interface.py
harry97uk Feb 14, 2025
e79a5ae
feat: update use_trace context manager to specify return type and add…
harry97uk Feb 14, 2025
5f79d57
fix: update use_trace context manager to improve type hinting for tra…
harry97uk Feb 14, 2025
feabc63
fix: improve type hinting for current_trace_id in request_interface.py
harry97uk Feb 18, 2025
d24c926
chore: reorder imports in request_interface.py for consistency
harry97uk Feb 18, 2025
e692e34
Merge pull request #72 from aflorithmic/feat/tracing-headers
harry97uk Feb 19, 2025
d72102b
fix: pass 0 float values (#73)
martinezpl Feb 25, 2025
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
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`.
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
<br>
</p>

## 🧐 About <a name = "about"></a>
## About <a name = "about"></a>

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 <a name = "getting_started"></a>
## Getting Started <a name = "getting_started"></a>

### Installation

Expand Down Expand Up @@ -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).

Expand All @@ -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/).

Expand All @@ -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")
Expand All @@ -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 <a name = "quickstarts"></a>
## More quickstarts <a name = "quickstarts"></a>

Get started with our [quickstart recipes](https://docs.audiostack.ai/docs/introduction).

Expand Down
10 changes: 9 additions & 1 deletion audiostack/delivery/encoder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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 = {
Expand All @@ -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
Expand Down
41 changes: 32 additions & 9 deletions audiostack/helpers/request_interface.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,28 @@
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)):
return data

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):
Expand All @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -111,15 +125,15 @@ 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:
url = f"{url}/{path_parameters}"

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:
Expand All @@ -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)
)
)

Expand All @@ -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)
24 changes: 23 additions & 1 deletion audiostack/speech/voice.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand Down
49 changes: 48 additions & 1 deletion audiostack/tests/helpers/test_request_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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,
)
Loading
Loading