Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.3.2] - 2026-04-21

### Fixed
- Fixed `retrieve_enhanced_track` using wrong response key `revived_track_tasks_results` (snake_case) — now correctly reads `revivedTrackTaskResults` (camelCase) from the server response
- Rewrote inaccurate docstrings across all retrieval methods to reflect actual server response shapes

### Added
- Typed response dataclasses for all retrieval endpoints, replacing raw `dict` returns:
- `EnhancedTrackResult` — fields: `download_url_preview_revived`, `download_url_revived`, `stems`, `preview_start_time`
- `PreviewMixResult` — fields: `download_url_preview_mixed`, `stems`, `mix_output_settings`, `status`
- `FinalMixResult` — fields: `download_url_mixed`, `stems`, `mix_output_settings`
- `PreviewMasterResult` — fields: `download_url_mastered_preview`, `preview_start_time`
- `FinalMasterResult` — fields: `download_url_mastered`
- `AnalysisResult` — fields: `payload`, `error`, `info`, `completion_time`
- All new response types are exported from `roex_python.models`

### Changed
- `retrieve_enhanced_track` now returns `EnhancedTrackResult` instead of `dict`
- `retrieve_preview_mix` now returns `PreviewMixResult` instead of `Dict`
- `retrieve_final_mix` and `retrieve_final_mix_advanced` now return `FinalMixResult` instead of `Dict`
- `retrieve_preview_master` now returns `PreviewMasterResult` instead of `Dict`
- `retrieve_final_master` now returns `FinalMasterResult` instead of `Dict`
- `analyze_mix` now returns `AnalysisResult` instead of `Dict`

## [1.3.1] - 2026-04-21

### Fixed
Expand Down Expand Up @@ -103,6 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Asynchronous task polling
- Secure file upload/download

[1.3.2]: https://github.com/roexaudio/roex-python/compare/v1.3.1...v1.3.2
[1.3.1]: https://github.com/roexaudio/roex-python/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/roexaudio/roex-python/compare/v1.2.1...v1.3.0
[1.2.1]: https://github.com/roexaudio/roex-python/compare/v1.2.0...v1.2.1
Expand Down
4 changes: 2 additions & 2 deletions examples/advanced_mix_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ def advanced_mix_workflow(bass_path: str, vocals_path: str, drums_path: str = No
logger.info("? Mix preview ready!")

# Download preview
preview_url = preview_results.get('download_url_preview_mixed')
preview_url = preview_results.download_url_preview_mixed
if preview_url:
preview_file = output_path / "mix_preview.wav"
if client.api_provider.download_file(preview_url, str(preview_file)):
Expand Down Expand Up @@ -273,7 +273,7 @@ def advanced_mix_workflow(bass_path: str, vocals_path: str, drums_path: str = No
logger.info("? Advanced final mix complete!")

# Download final mix
final_url = final_mix_results.get('download_url_mixed') or final_mix_results.get('download_url_final_mix')
final_url = final_mix_results.download_url_mixed
if final_url:
final_file = output_path / "mix_final_advanced.wav"
if client.api_provider.download_file(final_url, str(final_file)):
Expand Down
14 changes: 7 additions & 7 deletions examples/analysis_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ def print_analysis_results(results):
"""Pretty print the analysis results from the mix analysis endpoint.

Args:
results: Dictionary containing the analysis results from the API
results: AnalysisResult from the API
"""
if not results or "payload" not in results:
if not results or not results.payload:
logger.error("No payload data found in analysis results.")
return

payload = results["payload"]
payload = results.payload

print("\n==== Mix Analysis Results ====")
print(f"Integrated Loudness: {payload.get('integrated_loudness_lufs', 'N/A')} LUFS")
Expand Down Expand Up @@ -205,15 +205,15 @@ def analysis_workflow(input_file: str, compare_file: str = None):
)
try:
analysis_results = client.analysis.analyze_mix(analysis_request)
if not analysis_results or analysis_results.get("error"): # Basic error check
logger.error(f"Error analyzing mix: {analysis_results.get('message', 'Unknown API error')}")
# Continue to comparison if possible, but don't save analysis results
if not analysis_results or analysis_results.error:
logger.error(f"Error analyzing mix: {analysis_results.info if analysis_results else 'Unknown API error'}")
else:
print_analysis_results(analysis_results)
analysis_output_path = output_dir / f"analysis_{file_path_a.stem}.json"
try:
from dataclasses import asdict
with open(analysis_output_path, "w") as f:
json.dump(analysis_results, f, indent=2)
json.dump(asdict(analysis_results), f, indent=2)
logger.info(f"\nAnalysis results saved to {analysis_output_path}")
except IOError as e:
logger.error(f"Error saving analysis results: {e}")
Expand Down
8 changes: 4 additions & 4 deletions examples/enhance_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def enhance_workflow(input_file: str = None):
logger.error(f"Error retrieving preview: {e}")
return

preview_url = preview_results.get("download_url_preview_revived")
preview_url = preview_results.download_url_preview_revived
if not preview_url:
logger.error("No preview URL received")
return
Expand All @@ -148,7 +148,7 @@ def enhance_workflow(input_file: str = None):
logger.error("Failed to download preview")
return

stems = preview_results.get('stems', {})
stems = preview_results.stems or {}
if stems:
logger.info("\nDownloading preview stems...")
for stem_name, stem_url in stems.items():
Expand Down Expand Up @@ -177,7 +177,7 @@ def enhance_workflow(input_file: str = None):
logger.error(f"Error retrieving full enhancement: {e}")
return None

final_url = full_results.get('download_url_revived')
final_url = full_results.download_url_revived
if not final_url:
logger.error("No final enhancement URL received")
return
Expand All @@ -191,7 +191,7 @@ def enhance_workflow(input_file: str = None):
return
logger.info(f"Downloaded full enhancement to {full_path}")

stems = full_results.get('stems', {})
stems = full_results.stems or {}
if stems:
logger.info("\nDownloading final stems...")
for stem_name, stem_url in stems.items():
Expand Down
9 changes: 4 additions & 5 deletions examples/mastering_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ def master_single_track(client: RoExClient, file_path: Path, output_dir: Path):

logger.info("Retrieving preview master (this may take some time)...")
preview_results = client.mastering.retrieve_preview_master(mastering_task_id)
preview_url = preview_results.get('download_url_mastered_preview')
preview_url = preview_results.download_url_mastered_preview
if preview_url:
logger.info("Preview master ready.")
# Optional: Download preview
Expand All @@ -130,11 +130,10 @@ def master_single_track(client: RoExClient, file_path: Path, output_dir: Path):
try:
logger.info("Retrieving final master URL (this may take some time)...")
# This retrieves the download URL for the final mastered file
final_url_response = client.mastering.retrieve_final_master(mastering_task_id)
final_url = final_url_response.get('download_url_mastered')
final_result = client.mastering.retrieve_final_master(mastering_task_id)
final_url = final_result.download_url_mastered
if not final_url:
error_msg = final_url_response.get('message', 'No URL found')
logger.error(f"Could not retrieve final master URL. Error: {error_msg}")
logger.error("Could not retrieve final master URL.")
return False
logger.info("Final master URL ready.")
except Exception as e:
Expand Down
4 changes: 2 additions & 2 deletions examples/mix_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,8 +247,8 @@ def add_track_config(track_name: str, path_str: Optional[str], instrument: Instr
logger.error(f"Error retrieving mix preview: {e}")
return

preview_download_url = preview_results.get('download_url_preview_mixed')
fx_settings = preview_results.get('mix_output_settings') # Capture FX settings
preview_download_url = preview_results.download_url_preview_mixed
fx_settings = preview_results.mix_output_settings

if preview_download_url:
logger.info(f"Preview mix ready: {preview_download_url}")
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "roex_python"
version = "1.3.1"
version = "1.3.2"
description = "Pip package for the RoEx Tonn API"
readme = "README.md"
authors = [
Expand Down
2 changes: 1 addition & 1 deletion roex_python/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
for audio mixing, mastering, analysis, and enhancement.
"""

__version__ = "1.3.1"
__version__ = "1.3.2"
__author__ = "RoEx Audio"
__email__ = "support@roexaudio.com"
__license__ = "MIT"
Expand Down
127 changes: 39 additions & 88 deletions roex_python/controllers/analysis_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import requests
import logging

from roex_python.models.analysis import MixAnalysisRequest, AnalysisMusicalStyle
from roex_python.models.analysis import AnalysisMusicalStyle, AnalysisResult, MixAnalysisRequest
from roex_python.providers.api_provider import ApiProvider

# Initialize logger for this module
Expand All @@ -30,47 +30,33 @@ def __init__(self, api_provider: ApiProvider):
self.api_provider = api_provider
logger.info("AnalysisController initialized.")

def analyze_mix(self, request: MixAnalysisRequest) -> Dict[str, Any]:
def analyze_mix(self, request: MixAnalysisRequest) -> AnalysisResult:
"""
Analyze a single mix or master track to retrieve detailed metrics.

This method sends the track URL and analysis parameters to the API
and returns the analysis results synchronously.
Sends the track URL and analysis parameters to ``/mixanalysis`` and
returns the results synchronously.

Args:
request (MixAnalysisRequest): An object containing the track URL
(`audio_file_location`), the musical style for reference
(`musical_style`), and whether the track is mastered (`is_master`).
The URL must point to an accessible WAV or FLAC file.
request (MixAnalysisRequest): The track URL, musical style reference,
and ``is_master`` flag.

Returns:
Dict[str, Any]: A dictionary containing the analysis results.
The structure typically includes detailed metrics under a 'payload' key,
covering loudness, dynamics, stereo field, phase, tonal balance, etc.
Check the official RoEx API documentation for the full structure.
AnalysisResult: A typed result containing:
- ``payload`` (Optional[Dict[str, Any]]): Diagnosis metrics including
``integrated_loudness_lufs``, ``peak_loudness_dbfs``,
``tonal_profile``, ``clipping``, ``stereo_field``, ``phase_issues``,
``bit_depth``, ``sample_rate``, etc.
- ``error`` (bool): Whether the analysis encountered an error.
- ``info`` (str): Additional information from the API.
- ``completion_time`` (str): When the analysis finished.

Raises:
requests.exceptions.RequestException: If the API request fails due to network
issues or invalid endpoint.
Exception: If the API returns an error response (e.g., 4xx, 5xx status codes)
indicating issues like invalid input, file access problems, or
server errors.
Exception: If the API returns an error response.

Example:
>>> from roex_python.models import MixAnalysisRequest, AnalysisMusicalStyle
>>> # Assume 'client' is an initialized RoExClient
>>> # Assume 'track_url' is a URL obtained after uploading a local file
>>> analysis_request = MixAnalysisRequest(
... audio_file_location=track_url,
... musical_style=AnalysisMusicalStyle.ROCK_PUNK,
... is_master=False
... )
>>> try:
>>> analysis_results = client.analysis.analyze_mix(analysis_request)
>>> print(f"Analysis Loudness (LUFS): {analysis_results.get('payload', {}).get('integrated_loudness_lufs')}")
>>> # Explore other metrics in analysis_results['payload']
>>> except Exception as e:
>>> print(f"Error analyzing mix: {e}")
>>> result = client.analysis.analyze_mix(request)
>>> print(result.payload.get("integrated_loudness_lufs"))
"""
logger.info(f"Analyzing mix with parameters: {request}")
payload = {
Expand All @@ -84,11 +70,14 @@ def analyze_mix(self, request: MixAnalysisRequest) -> Dict[str, Any]:
try:
logger.debug(f"Sending analysis request to API: {payload}")
response = self.api_provider.post("/mixanalysis", payload)
if "mixDiagnosisResults" in response:
logger.info("Analysis results received successfully.")
return response["mixDiagnosisResults"]
logger.info("Analysis results received without expected format.")
return response
raw = response.get("mixDiagnosisResults", response)
logger.info("Analysis results received successfully.")
return AnalysisResult(
payload=raw.get("payload"),
error=raw.get("error", False),
info=raw.get("info", ""),
completion_time=raw.get("completion_time", ""),
)
except requests.HTTPError as e:
logger.error(f"Failed to analyze mix: {str(e)}")
raise Exception(f"Failed to analyze mix: {str(e)}")
Expand All @@ -101,43 +90,23 @@ def compare_mixes(self, mix_a_url: str, mix_b_url: str,
"""
Analyze two mixes and provide a comparison of their key metrics.

This method internally calls `analyze_mix` for both provided track URLs
and then computes differences between key metrics.
Calls ``analyze_mix`` for both URLs and computes per-metric differences.

Args:
mix_a_url (str): URL of the first mix (must be accessible WAV/FLAC).
mix_b_url (str): URL of the second mix (must be accessible WAV/FLAC).
musical_style (AnalysisMusicalStyle): The musical style reference for analysis.
is_master (bool, optional): Whether the tracks should be analyzed as
mastered tracks. Defaults to False.
mix_a_url (str): URL of the first mix (accessible WAV/FLAC).
mix_b_url (str): URL of the second mix (accessible WAV/FLAC).
musical_style (AnalysisMusicalStyle): Musical style reference for analysis.
is_master (bool): Whether to analyze as mastered tracks. Defaults to False.

Returns:
Dict[str, Any]: A dictionary containing 'mix_a' results, 'mix_b' results,
and calculated 'differences' between key metrics (loudness, etc.).
Dict[str, Any]: ``{"mix_a": {...}, "mix_b": {...}, "differences": {...}}``.

Raises:
requests.exceptions.RequestException: If either underlying `analyze_mix` call fails
due to network issues.
Exception: If either underlying `analyze_mix` call returns an API error,
or if there's an issue during metric extraction/comparison.
Exception: If either underlying ``analyze_mix`` call fails.

Example:
>>> from roex_python.models import AnalysisMusicalStyle
>>> # Assume 'client' is an initialized RoExClient
>>> # Assume 'track_url_a', 'track_url_b' are URLs for two mixes
>>> try:
>>> comparison = client.analysis.compare_mixes(
... mix_a_url=track_url_a,
... mix_b_url=track_url_b,
... musical_style=AnalysisMusicalStyle.POP,
... is_master=True
... )
>>> print(f"Mix A LUFS: {comparison['mix_a'].get('integrated_loudness_lufs')}")
>>> print(f"Mix B LUFS: {comparison['mix_b'].get('integrated_loudness_lufs')}")
>>> print(f"LUFS Difference: {comparison['differences'].get('integrated_loudness_lufs')}")
>>> # Explore other comparison metrics
>>> except Exception as e:
>>> print(f"Error comparing mixes: {e}")
>>> comparison = client.analysis.compare_mixes(url_a, url_b, AnalysisMusicalStyle.POP)
>>> print(comparison["differences"]["integrated_loudness_lufs"])
"""
logger.info(f"Comparing mixes: {mix_a_url} and {mix_b_url} with musical style: {musical_style}")
request_a = MixAnalysisRequest(
Expand All @@ -155,7 +124,6 @@ def compare_mixes(self, mix_a_url: str, mix_b_url: str,
results_a = self.analyze_mix(request_a)
results_b = self.analyze_mix(request_b)

# Extract key metrics for comparison
comparison = {
"mix_a": self._extract_metrics(results_a),
"mix_b": self._extract_metrics(results_b),
Expand All @@ -165,18 +133,10 @@ def compare_mixes(self, mix_a_url: str, mix_b_url: str,
logger.info("Comparison results generated successfully.")
return comparison

def _extract_metrics(self, diagnosis: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract key metrics from diagnosis results

Args:
diagnosis: Mix diagnosis results

Returns:
Dictionary of extracted metrics
"""
def _extract_metrics(self, diagnosis: AnalysisResult) -> Dict[str, Any]:
"""Extract key metrics from an AnalysisResult."""
logger.debug(f"Extracting metrics from diagnosis results: {diagnosis}")
payload = diagnosis.get("payload", {})
payload = diagnosis.payload or {}

# Extract production metrics
production_keys = [
Expand All @@ -193,17 +153,8 @@ def _extract_metrics(self, diagnosis: Dict[str, Any]) -> Dict[str, Any]:
logger.info("Metrics extracted successfully.")
return metrics

def _compare_metrics(self, results_a: Dict[str, Any], results_b: Dict[str, Any]) -> Dict[str, Any]:
"""
Compare metrics between two analysis results

Args:
results_a: First mix diagnosis results
results_b: Second mix diagnosis results

Returns:
Dictionary of differences
"""
def _compare_metrics(self, results_a: AnalysisResult, results_b: AnalysisResult) -> Dict[str, Any]:
"""Compare metrics between two AnalysisResult objects."""
logger.info("Comparing metrics between two analysis results.")
metrics_a = self._extract_metrics(results_a)
metrics_b = self._extract_metrics(results_b)
Expand Down
Loading