diff --git a/CHANGELOG.md b/CHANGELOG.md index 547129f..52008f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/examples/advanced_mix_example.py b/examples/advanced_mix_example.py index 626206f..53a6dd3 100644 --- a/examples/advanced_mix_example.py +++ b/examples/advanced_mix_example.py @@ -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)): @@ -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)): diff --git a/examples/analysis_example.py b/examples/analysis_example.py index f2f2a73..8dfc17e 100644 --- a/examples/analysis_example.py +++ b/examples/analysis_example.py @@ -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") @@ -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}") diff --git a/examples/enhance_example.py b/examples/enhance_example.py index 2b25a4b..da8f740 100644 --- a/examples/enhance_example.py +++ b/examples/enhance_example.py @@ -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 @@ -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(): @@ -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 @@ -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(): diff --git a/examples/mastering_example.py b/examples/mastering_example.py index 7baeb39..93dc0e8 100644 --- a/examples/mastering_example.py +++ b/examples/mastering_example.py @@ -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 @@ -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: diff --git a/examples/mix_example.py b/examples/mix_example.py index c54e118..d1f3593 100644 --- a/examples/mix_example.py +++ b/examples/mix_example.py @@ -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}") diff --git a/pyproject.toml b/pyproject.toml index 73f4c23..508fef5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/roex_python/__init__.py b/roex_python/__init__.py index 4f9d1d3..d6ce489 100644 --- a/roex_python/__init__.py +++ b/roex_python/__init__.py @@ -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" diff --git a/roex_python/controllers/analysis_controller.py b/roex_python/controllers/analysis_controller.py index 00f5e78..dff8061 100644 --- a/roex_python/controllers/analysis_controller.py +++ b/roex_python/controllers/analysis_controller.py @@ -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 @@ -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 = { @@ -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)}") @@ -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( @@ -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), @@ -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 = [ @@ -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) diff --git a/roex_python/controllers/enhance_controller.py b/roex_python/controllers/enhance_controller.py index 9e0cea5..b93b1a6 100644 --- a/roex_python/controllers/enhance_controller.py +++ b/roex_python/controllers/enhance_controller.py @@ -9,7 +9,7 @@ import requests -from roex_python.models.enhance import MixEnhanceRequest, MixEnhanceResponse +from roex_python.models.enhance import EnhancedTrackResult, MixEnhanceRequest, MixEnhanceResponse from roex_python.providers.api_provider import ApiProvider # Initialize logger for this module @@ -172,56 +172,38 @@ def create_mix_enhance(self, request: MixEnhanceRequest) -> MixEnhanceResponse: logger.exception(f"Unexpected error creating mix enhance: {e}") raise - def retrieve_enhanced_track(self, task_id: str, poll_interval: int = 5, timeout: int = 600) -> dict: + def retrieve_enhanced_track(self, task_id: str, poll_interval: int = 5, timeout: int = 600) -> EnhancedTrackResult: """ Retrieve the results of a mix enhancement task (preview or full). - This method polls the RoEx API's task status endpoint using the provided - `task_id`. It waits for the task to complete or until the `timeout` is reached. + Polls the ``/retrieveenhancedtrack`` endpoint until results are ready + or the *timeout* is reached. Args: task_id (str): The unique ID of the enhancement task (obtained from - `create_mix_enhance_preview` or `create_mix_enhance`). + ``create_mix_enhance_preview`` or ``create_mix_enhance``). poll_interval (int): Seconds to wait between status checks. Defaults to 5. timeout (int): Maximum seconds to wait for task completion. Defaults to 600 (10 minutes). Returns: - dict: A dictionary containing the task results. The structure includes: - - `status` (str): Final status ('completed', 'failed', etc.). - - `results` (dict): Contains details upon completion: - - `preview_audio_file_location` (Optional[str]): URL for preview audio. - - `enhanced_audio_file_location` (Optional[str]): URL for full enhanced audio. - - `stems` (Optional[dict]): If `stem_processing` was True in the request, - this dictionary contains URLs for individual stems (e.g., `vocals`, `bass`, `drums`, `other`). - - `processing_time` (float): Time taken for processing. - - `error_message` (Optional[str]): Error details if the task failed. - - Other task metadata. + EnhancedTrackResult: A typed result containing: + - ``download_url_preview_revived`` (Optional[str]): Signed URL for the MP3 preview. + - ``download_url_revived`` (Optional[str]): Signed URL for the full WAV file. + - ``stems`` (Optional[Dict[str, str]]): URLs keyed by stem name + (``vocal``, ``bass``, ``drums``, ``other``) when stem processing was requested. + - ``preview_start_time`` (Optional[float]): Offset in seconds where + the preview clip begins in the original track. Raises: - TimeoutError: If the task does not complete within the specified `timeout`. - requests.exceptions.RequestException: Network or API endpoint errors during polling. - Exception: If the API returns an error status code (4xx, 5xx) during polling - or if the task itself failed. + Exception: If the task does not complete within the specified *timeout*, + or the API returns an error during polling. Example: - >>> # Assume 'client' is an initialized RoExClient and 'task_id' was obtained previously - >>> try: - >>> results = client.enhance.retrieve_enhanced_track(task_id) - >>> if results.get('status') == 'completed': - >>> enhanced_url = results.get('results', {}).get('enhanced_audio_file_location') - >>> print(f"Enhancement complete! Audio URL: {enhanced_url}") - >>> stems = results.get('results', {}).get('stems') - >>> if stems: - >>> print(f"Stems generated:") - >>> for stem_name, stem_url in stems.items(): - >>> print(f" - {stem_name}: {stem_url}") - >>> else: - >>> error_msg = results.get('results', {}).get('error_message', 'Unknown error') - >>> print(f"Task failed or timed out. Status: {results.get('status')}. Error: {error_msg}") - >>> except TimeoutError: - >>> print("Task timed out.") - >>> except Exception as e: - >>> print(f"An error occurred while retrieving results: {e}") + >>> result = client.enhance.retrieve_enhanced_track(task_id) + >>> print(result.download_url_preview_revived) + >>> if result.stems: + ... for name, url in result.stems.items(): + ... print(f"{name}: {url}") """ logger.info(f"Attempting to retrieve results for task ID: {task_id}") payload = { @@ -230,32 +212,30 @@ def retrieve_enhanced_track(self, task_id: str, poll_interval: int = 5, timeout: } } - # Poll for results for attempt in range(timeout // poll_interval): try: logger.debug(f"Polling attempt {attempt + 1}/{timeout // poll_interval}...") response = self.api_provider.post("/retrieveenhancedtrack", payload) if not response.get("error", False): - results = response.get("revived_track_tasks_results", {}) - if results: + results = response.get("revivedTrackTaskResults", {}) + has_url = ( + results.get("download_url_preview_revived") + or results.get("download_url_revived") + ) + if results and has_url: logger.info(f"Enhanced track retrieved successfully for task ID: {task_id}") - return results - - # Some API versions might return a different format - for key in response: - if isinstance(response[key], dict) and ( - "download_url_revived" in response[key] or - "download_url_preview_revived" in response[key] - ): - logger.info(f"Enhanced track retrieved successfully for task ID: {task_id}") - return response[key] + return EnhancedTrackResult( + download_url_preview_revived=results.get("download_url_preview_revived"), + download_url_revived=results.get("download_url_revived"), + stems=results.get("stems"), + preview_start_time=results.get("preview_start_time"), + ) except requests.HTTPError as e: logger.error(f"Error during polling: {str(e)}") except Exception as e: logger.exception(f"Unexpected error during polling for task ID: {task_id}: {e}") - # Wait before next attempt time.sleep(poll_interval) logger.error(f"Enhanced track was not available after polling for task ID: {task_id}.") diff --git a/roex_python/controllers/mastering_controller.py b/roex_python/controllers/mastering_controller.py index 178fcea..de044a7 100644 --- a/roex_python/controllers/mastering_controller.py +++ b/roex_python/controllers/mastering_controller.py @@ -10,9 +10,11 @@ import requests from roex_python.models.mastering import ( + AlbumMasteringRequest, + FinalMasterResult, MasteringRequest, MasteringTaskResponse, - AlbumMasteringRequest + PreviewMasterResult ) from roex_python.providers.api_provider import ApiProvider @@ -107,45 +109,30 @@ def create_mastering_preview(self, request: MasteringRequest) -> MasteringTaskRe raise def retrieve_preview_master(self, task_id: str, max_attempts: int = 30, - poll_interval: int = 5) -> Dict[str, Any]: + poll_interval: int = 5) -> PreviewMasterResult: """ Retrieve the results of a mastering preview task, polling until complete. - Checks the status of a mastering task initiated by `create_mastering_preview`. - Polls the API periodically until the task completes or the maximum number - of attempts is reached. + Polls the ``/retrievepreviewmaster`` endpoint until + ``previewMasterTaskResults`` is present or *max_attempts* is exhausted. Args: - task_id (str): The `mastering_task_id` obtained from the - `create_mastering_preview` response. - max_attempts (int, optional): Maximum number of polling attempts before - timing out. Defaults to 30. - poll_interval (int, optional): Seconds to wait between polling attempts. - Defaults to 5. + task_id (str): The ``mastering_task_id`` from ``create_mastering_preview``. + max_attempts (int): Maximum polling iterations. Defaults to 30. + poll_interval (int): Seconds between polls. Defaults to 5. Returns: - Dict[str, Any]: A dictionary containing the results of the preview master. - Key fields typically include: - - 'status': Final status (e.g., 'MASTERING_TASK_PREVIEW_COMPLETED'). - - 'download_url_mastered_preview': URL to download the preview audio. - Check the official RoEx API documentation for the exact structure. + PreviewMasterResult: A typed result containing: + - ``download_url_mastered_preview`` (Optional[str]): Signed URL for the mastered preview. + - ``preview_start_time`` (Optional[float]): Offset in seconds where the + preview clip starts in the original track. Raises: - requests.exceptions.RequestException: If an API request fails during polling. - Exception: If the task does not complete successfully within `max_attempts`, - if the API returns an error during polling, or for other - API errors (4xx/5xx). + Exception: If the task does not complete within *max_attempts* polls. Example: - >>> # Assume 'client' is an initialized RoExClient - >>> # Assume 'task_id' was obtained from create_mastering_preview - >>> try: - >>> master_results = client.mastering.retrieve_preview_master(task_id) - >>> print(f"Mastering Preview Status: {master_results.get('status')}") - >>> print(f"Preview Download URL: {master_results.get('download_url_mastered_preview')}") - >>> # Further process the results (e.g., download the file) - >>> except Exception as e: - >>> print(f"Error retrieving mastering preview: {e}") + >>> result = client.mastering.retrieve_preview_master(task_id) + >>> print(result.download_url_mastered_preview) """ logger.info(f"Retrieving preview master for task ID: {task_id}") payload = { @@ -154,85 +141,65 @@ def retrieve_preview_master(self, task_id: str, max_attempts: int = 30, } } - # Try initial request + def _parse(raw: Dict[str, Any]) -> PreviewMasterResult: + return PreviewMasterResult( + download_url_mastered_preview=raw.get("download_url_mastered_preview"), + preview_start_time=raw.get("preview_start_time"), + ) + try: response = self.api_provider.post("/retrievepreviewmaster", payload) if "previewMasterTaskResults" in response: logger.info(f"Preview master ready for task ID: {task_id}") - return response["previewMasterTaskResults"] + return _parse(response["previewMasterTaskResults"]) except requests.HTTPError: - # Initial request failed, let's try polling logger.warning(f"Initial request failed for task ID: {task_id}. Starting polling...") pass - # Poll for results for attempt in range(max_attempts): try: logger.debug(f"Polling attempt {attempt + 1}/{max_attempts} for task ID: {task_id}") response = self.api_provider.post("/retrievepreviewmaster", payload) - # If we get a 200 response with results if "previewMasterTaskResults" in response: logger.info(f"Preview master ready for task ID: {task_id}") - return response["previewMasterTaskResults"] + return _parse(response["previewMasterTaskResults"]) - # Check for specific status codes status_code = response.get("status", 0) if status_code == 202: logger.info(f"Task still processing for task ID: {task_id}...") - elif status_code == 200: - # If we get a 200 but no results, try to parse the response differently - if isinstance(response, dict): - for key, value in response.items(): - if isinstance(value, dict) and "download_url_mastered_preview" in value: - logger.info(f"Preview master ready for task ID: {task_id}") - return value except requests.HTTPError as e: logger.error(f"Error during polling for task ID: {task_id}: {e}") except Exception as e: logger.exception(f"Unexpected error during polling for task ID: {task_id}: {e}") - # Wait before next attempt time.sleep(poll_interval) logger.error(f"Timeout waiting for preview master for task ID: {task_id} after {max_attempts} attempts.") raise Exception(f"Preview master task {task_id} did not complete after polling for {max_attempts * poll_interval} seconds.") - def retrieve_final_master(self, task_id: str) -> Dict[str, Any]: + def retrieve_final_master(self, task_id: str) -> FinalMasterResult: """ Retrieve the final mastered audio file. - This method fetches the final output of a completed mastering task. - It's typically called after `create_mastering_preview` and potentially - `retrieve_preview_master` confirm the task is done, although polling - is not built into this specific retrieval method. + Fetches the result from ``/retrievefinalmaster``. Call after + ``create_mastering_preview`` and ``retrieve_preview_master`` have + confirmed the task is done. Args: - task_id (str): The `mastering_task_id` obtained from the - `create_mastering_preview` response. + task_id (str): The ``mastering_task_id`` from ``create_mastering_preview``. Returns: - Dict[str, Any]: A dictionary containing the results of the final master. - Key fields typically include: - - 'status': Final status (e.g., 'MASTERING_TASK_FINAL_COMPLETED'). - - 'download_url_mastered_final': URL to download the final audio. - Check the official RoEx API documentation for the exact structure. + FinalMasterResult: A typed result containing: + - ``download_url_mastered`` (Optional[str]): Signed URL for the final + mastered audio file. Raises: - requests.exceptions.RequestException: If the API request fails. - Exception: If the API returns an error response (e.g., 4xx, 5xx status codes), - indicating issues like task not found, task not complete, or server errors. + Exception: If the API returns an error response. Example: - >>> # Assume 'client' is an initialized RoExClient - >>> # Assume 'task_id' was obtained from create_mastering_preview and preview is complete - >>> try: - >>> final_master_results = client.mastering.retrieve_final_master(task_id) - >>> print(f"Final Master Status: {final_master_results.get('status')}") - >>> print(f"Final Download URL: {final_master_results.get('download_url_mastered_final')}") - >>> # Further process the results - >>> except Exception as e: - >>> print(f"Error retrieving final master: {e}") + >>> result = client.mastering.retrieve_final_master(task_id) + >>> print(result.download_url_mastered) """ logger.info(f"Retrieving final master for task ID: {task_id}") payload = { @@ -244,23 +211,20 @@ def retrieve_final_master(self, task_id: str) -> Dict[str, Any]: try: response = self.api_provider.post("/retrievefinalmaster", payload) - # Handle different response formats if "finalMasterTaskResults" in response: - # If it's a structured response + raw = response["finalMasterTaskResults"] logger.info(f"Final master ready for task ID: {task_id}") - return response["finalMasterTaskResults"] + return FinalMasterResult( + download_url_mastered=raw.get("download_url_mastered"), + ) elif isinstance(response, dict) and "download_url_mastered" in response: - # If the URL is directly in the response - logger.info(f"Final master ready for task ID: {task_id}") - return response["download_url_mastered"] - elif isinstance(response, str) and (response.startswith("http://") or response.startswith("https://")): - # If the response is just the URL as a string logger.info(f"Final master ready for task ID: {task_id}") - return response + return FinalMasterResult( + download_url_mastered=response.get("download_url_mastered"), + ) - # Default fallback - logger.warning(f"Unknown response format for task ID: {task_id}. Returning raw response.") - return response + logger.warning(f"Unknown response format for task ID: {task_id}. Returning empty result.") + return FinalMasterResult() except requests.HTTPError as e: logger.exception(f"HTTP error retrieving final master for task ID: {task_id}: {e}") raise Exception(f"Failed to retrieve final master: {e}") @@ -303,7 +267,7 @@ def process_album(self, album_request: AlbumMasteringRequest, output_dir: str = # Get final master try: - final_url = self.retrieve_final_master(task_id)['download_url_mastered'] + final_url = self.retrieve_final_master(task_id).download_url_mastered results[idx] = final_url # Download the file diff --git a/roex_python/controllers/mix_controller.py b/roex_python/controllers/mix_controller.py index a5b9037..3aab530 100644 --- a/roex_python/controllers/mix_controller.py +++ b/roex_python/controllers/mix_controller.py @@ -9,10 +9,12 @@ import requests from roex_python.models.mixing import ( - MultitrackMixRequest, - MultitrackTaskResponse, FinalMixRequest, FinalMixRequestAdvanced, + FinalMixResult, + MultitrackMixRequest, + MultitrackTaskResponse, + PreviewMixResult, TrackData, TrackGainData, TrackEffectsData @@ -106,53 +108,32 @@ def create_mix_preview(self, request: MultitrackMixRequest) -> MultitrackTaskRes raise def retrieve_preview_mix(self, task_id: str, retrieve_fx_settings: bool = False, - max_attempts: int = 30, poll_interval: int = 5) -> Dict[str, Any]: + max_attempts: int = 30, poll_interval: int = 5) -> PreviewMixResult: """ Retrieve the results of a multitrack mix preview task, polling until complete. - This method checks the status of a mix preview task initiated by - `create_mix_preview`. It polls the API endpoint periodically until the task - status indicates completion (or failure) or the maximum number of attempts - is reached. + Polls the ``/retrievepreviewmix`` endpoint until the task reaches + ``MIX_TASK_PREVIEW_COMPLETED`` or *max_attempts* is exhausted. Args: - task_id (str): The `multitrack_task_id` obtained from the - `create_mix_preview` response. - retrieve_fx_settings (bool, optional): Whether to retrieve detailed FX settings - applied during the mix. Note: This might incur additional charges - depending on the API plan. Defaults to False. - max_attempts (int, optional): The maximum number of times to poll the API - before timing out. Defaults to 30. - poll_interval (int, optional): The number of seconds to wait between - polling attempts. Defaults to 5. + task_id (str): The ``multitrack_task_id`` from ``create_mix_preview``. + retrieve_fx_settings (bool): Request detailed FX settings. Defaults to False. + max_attempts (int): Maximum polling iterations. Defaults to 30. + poll_interval (int): Seconds between polls. Defaults to 5. Returns: - Dict[str, Any]: A dictionary containing the results of the preview mix task. - The structure typically includes: - - 'status': The final status of the task (e.g., 'MIX_TASK_PREVIEW_COMPLETED'). - - 'previewMixDownloadUrl': URL to download the preview mix audio file. - - 'stemsDownloadUrl': URL to download stems (if requested). - - 'settings': Applied settings (gain, pan, etc.). - - 'fxSettings': Detailed FX settings (if requested and available). - Check the official RoEx API documentation for the exact structure. + PreviewMixResult: A typed result containing: + - ``download_url_preview_mixed`` (Optional[str]): Signed URL for the preview mix. + - ``stems`` (Optional[Dict[str, str]]): Per-stem download URLs (if requested). + - ``mix_output_settings`` (Optional[Dict]): Gain, pan, and other applied settings. + - ``status`` (Optional[str]): Task status string. Raises: - requests.exceptions.RequestException: If an API request fails during polling. - Exception: If the task does not complete successfully within the - `max_attempts` or if the API returns an error during polling. - Also raised for other API errors (4xx/5xx). + Exception: If the task does not complete within *max_attempts* polls. Example: - >>> # Assume 'client' is an initialized RoExClient - >>> # Assume 'task_id' was obtained from create_mix_preview - >>> try: - >>> preview_results = client.mix.retrieve_preview_mix(task_id) - >>> print(f"Preview Status: {preview_results.get('status')}") - >>> print(f"Preview Download URL: {preview_results.get('previewMixDownloadUrl')}") - >>> # Further process the results (e.g., download the file) - >>> except Exception as e: - >>> print(f"Error retrieving mix preview: {e}") - + >>> result = client.mix.retrieve_preview_mix(task_id) + >>> print(result.download_url_preview_mixed) """ logger.info(f"Retrieving preview mix for task ID: {task_id}") payload = { @@ -162,133 +143,74 @@ def retrieve_preview_mix(self, task_id: str, retrieve_fx_settings: bool = False, } } - # Initial request + def _parse_preview_result(raw: Dict[str, Any]) -> PreviewMixResult: + return PreviewMixResult( + download_url_preview_mixed=raw.get("download_url_preview_mixed"), + stems=raw.get("stems"), + mix_output_settings=raw.get("mix_output_settings"), + status=raw.get("status"), + ) + try: response = self.api_provider.post("/retrievepreviewmix", payload) - # Check if the mix is already complete if "previewMixTaskResults" in response and response.get("status") == "MIX_TASK_PREVIEW_COMPLETED": logger.info(f"Preview mix for task {task_id} is ready.") - return response["previewMixTaskResults"] + return _parse_preview_result(response["previewMixTaskResults"]) - # If status code is 200 but no results, it might still be processing if "status" in response and response.get("status") != "MIX_TASK_PREVIEW_COMPLETED": logger.info(f"Mix preview is pending. Starting polling...") else: - # If the response doesn't indicate it's processing, return it as is - return response + return _parse_preview_result(response) except requests.HTTPError: - # Initial request failed, let's try polling logger.error("Initial request failed. Starting polling...") pass - # Poll for results for attempt in range(max_attempts): try: logger.debug(f"Polling attempt {attempt + 1}/{max_attempts}...") response = self.api_provider.post("/retrievepreviewmix", payload) - # Check if the mix is complete if "previewMixTaskResults" in response: results = response["previewMixTaskResults"] status = results.get("status", "") if status == "MIX_TASK_PREVIEW_COMPLETED": logger.info(f"Preview mix for task {task_id} is ready.") - return results + return _parse_preview_result(results) - # Check if it's still processing if "status" in response: logger.info(f"Current status: {response.get('status')}") except requests.HTTPError as e: logger.error(f"Error during polling: {str(e)}") - # Wait before next attempt time.sleep(poll_interval) logger.error(f"Polling timed out for preview mix task {task_id} after {max_attempts} attempts.") raise Exception(f"Preview mix task {task_id} did not complete after polling for {max_attempts * poll_interval} seconds.") - def retrieve_final_mix_advanced(self, request: FinalMixRequestAdvanced) -> Dict[str, Any]: + def retrieve_final_mix_advanced(self, request: FinalMixRequestAdvanced) -> FinalMixResult: """ Retrieve the final multitrack mix with advanced audio effects (EQ, compression, panning). - This method generates the final mix output with comprehensive audio processing - capabilities including parametric EQ, dynamic compression, and stereo panning. - It typically follows a `create_mix_preview` and `retrieve_preview_mix` sequence. + Posts to ``/retrievefinalmix`` with per-track EQ, compression, and panning + settings. Typically follows ``create_mix_preview`` / ``retrieve_preview_mix``. Args: - request (FinalMixRequestAdvanced): An object containing: - - multitrack_task_id (str): The task ID from the original preview. - - track_data (List[TrackEffectsData]): A list of advanced track settings - including gain, EQ, compression, and panning for each track. - - return_stems (bool, optional): Whether to return individual track stems - along with the final mix. - - create_master (bool, optional): Whether to create a mastered version of - the final mix. - - desired_loudness (DesiredLoudness, optional): Target loudness level - (LOW, MEDIUM, HIGH). Only applicable when not creating stems. - - sample_rate (str, optional): Sample rate for output ("44100" or "48000"). - - webhook_url (str, optional): URL for completion notifications. + request (FinalMixRequestAdvanced): Advanced final mix parameters including + per-track EQ, compression, panning, gain, and global options. Returns: - Dict[str, Any]: A dictionary containing the results of the final mix task. - The structure typically includes: - - 'status': The status of the final mix generation. - - 'download_url_mixed': URL to download the final mix audio file. - - 'stems': URLs to download stems (if requested). - - 'mix_output_settings': Applied settings. - Check the official RoEx API documentation for the exact structure. + FinalMixResult: A typed result containing: + - ``download_url_mixed`` (Optional[str]): Signed URL for the final mix. + - ``stems`` (Optional[Dict[str, str]]): Per-stem download URLs (if requested). + - ``mix_output_settings`` (Optional[Dict]): Applied settings. Raises: - requests.exceptions.RequestException: If the API request fails. - Exception: If the API returns an error response (e.g., 4xx, 5xx status codes). - ValueError: If audio effects parameters are out of valid ranges. + Exception: If the API returns an error response. Example: - >>> from roex_python.models import ( - ... FinalMixRequestAdvanced, TrackEffectsData, - ... EQSettings, EQBandSettings, CompressionSettings, PanningSettings - ... ) - >>> # Assume 'client' is an initialized RoExClient - >>> # Assume 'task_id' was obtained from create_mix_preview - >>> - >>> # Define advanced track effects - >>> bass_track = TrackEffectsData( - ... track_url="https://example.com/bass.wav", - ... gain_db=2.0, - ... eq_settings=EQSettings.preset_bass_boost(), - ... compression_settings=CompressionSettings.preset_bass(), - ... panning_settings=PanningSettings.center() - ... ) - >>> - >>> vocal_track = TrackEffectsData( - ... track_url="https://example.com/vocals.wav", - ... gain_db=-0.5, - ... eq_settings=EQSettings.preset_vocal_clarity(), - ... compression_settings=CompressionSettings.preset_vocal() - ... ) - >>> - >>> final_mix_request = FinalMixRequestAdvanced( - ... multitrack_task_id=task_id, - ... track_data=[bass_track, vocal_track], - ... return_stems=True, - ... create_master=False - ... ) - >>> - >>> try: - >>> final_mix_results = client.mix.retrieve_final_mix_advanced(final_mix_request) - >>> print(f"Final Mix Status: {final_mix_results.get('status')}") - >>> print(f"Final Mix URL: {final_mix_results.get('download_url_mixed')}") - >>> except Exception as e: - >>> print(f"Error retrieving final mix: {e}") - + >>> result = client.mix.retrieve_final_mix_advanced(request) + >>> print(result.download_url_mixed) """ logger.info(f"Retrieving advanced final mix for task ID: {request.multitrack_task_id}") logger.debug(f"Advanced final mix request data: {request}") @@ -297,77 +219,43 @@ def retrieve_final_mix_advanced(self, request: FinalMixRequestAdvanced) -> Dict[ try: response = self.api_provider.post("/retrievefinalmix", payload) logger.info("Advanced final mix retrieved successfully.") - if "applyAudioEffectsResults" in response: - return response["applyAudioEffectsResults"] - return response + raw = response.get("applyAudioEffectsResults", response) + return FinalMixResult( + download_url_mixed=raw.get("download_url_mixed"), + stems=raw.get("stems"), + mix_output_settings=raw.get("mix_output_settings"), + ) except requests.HTTPError as e: - # Log specific HTTP errors error_detail = f"{e.response.status_code} - {e.response.text}" if hasattr(e, 'response') and e.response else str(e) logger.error(f"HTTP error retrieving advanced final mix: {error_detail}") raise Exception(f"Failed to retrieve advanced final mix: {error_detail}") except Exception as e: - # Catch other potential exceptions logger.exception(f"Unexpected error retrieving advanced final mix: {e}") raise - def retrieve_final_mix(self, request: FinalMixRequest) -> Dict[str, Any]: + def retrieve_final_mix(self, request: FinalMixRequest) -> FinalMixResult: """ Retrieve the final multitrack mix, potentially with gain adjustments. - This method is used to generate the final mix output. It typically follows - a `create_mix_preview` and `retrieve_preview_mix` sequence, allowing users - to apply gain adjustments based on the preview before generating the final - audio file(s). + Posts to ``/retrievefinalmix`` with optional per-track gain changes applied + on top of the original preview. Args: - request (FinalMixRequest): An object containing: - - multitrack_task_id (str): The task ID from the original preview. - - gain_adjustments (List[TrackGainData], optional): A list of gain - adjustments to apply to specific tracks before final mixing. - - return_stems (bool, optional): Whether to return individual track stems - along with the final mix. Defaults according to original preview request if omitted. + request (FinalMixRequest): Final mix parameters including the preview + task ID, optional gain adjustments, and stem / sample-rate options. Returns: - Dict[str, Any]: A dictionary containing the results of the final mix task. - The structure typically includes: - - 'status': The status of the final mix generation (e.g., 'FINAL_MIX_COMPLETE'). - - 'finalMixDownloadUrl': URL to download the final mix audio file. - - 'stemsDownloadUrl': URL to download stems (if requested). - Check the official RoEx API documentation for the exact structure. + FinalMixResult: A typed result containing: + - ``download_url_mixed`` (Optional[str]): Signed URL for the final mix. + - ``stems`` (Optional[Dict[str, str]]): Per-stem download URLs (if requested). + - ``mix_output_settings`` (Optional[Dict]): Applied settings. Raises: - requests.exceptions.RequestException: If the API request fails. - Exception: If the API returns an error response (e.g., 4xx, 5xx status codes) - indicating issues like invalid input, task not found, or server errors. + Exception: If the API returns an error response. Example: - >>> from roex_python.models import FinalMixRequest, TrackGainData, InstrumentGroup - >>> # Assume 'client' is an initialized RoExClient - >>> # Assume 'task_id' was obtained from create_mix_preview - >>> - >>> # Optional: Define gain adjustments based on preview analysis - >>> adjustments = [ - ... TrackGainData(instrument_group=InstrumentGroup.BASS_GROUP, gain_db=1.5), - ... TrackGainData(instrument_group=InstrumentGroup.VOCAL_GROUP, gain_db=-0.5) - ... ] - >>> - >>> final_mix_request = FinalMixRequest( - ... multitrack_task_id=task_id, - ... gain_adjustments=adjustments, # Can be None or empty list if no adjustments - ... return_stems=True # Optional: Override stem generation - ... ) - >>> - >>> try: - >>> final_mix_results = client.mix.retrieve_final_mix(final_mix_request) - >>> print(f"Final Mix Status: {final_mix_results.get('status')}") - >>> print(f"Final Mix URL: {final_mix_results.get('finalMixDownloadUrl')}") - >>> # Further process the results - >>> except Exception as e: - >>> print(f"Error retrieving final mix: {e}") - + >>> result = client.mix.retrieve_final_mix(request) + >>> print(result.download_url_mixed) """ logger.info(f"Retrieving final mix for task ID: {request.multitrack_task_id}") logger.debug(f"Final mix request data: {request}") @@ -376,16 +264,17 @@ def retrieve_final_mix(self, request: FinalMixRequest) -> Dict[str, Any]: try: response = self.api_provider.post("/retrievefinalmix", payload) logger.info("Final mix retrieved successfully.") - if "applyAudioEffectsResults" in response: - return response["applyAudioEffectsResults"] - return response + raw = response.get("applyAudioEffectsResults", response) + return FinalMixResult( + download_url_mixed=raw.get("download_url_mixed"), + stems=raw.get("stems"), + mix_output_settings=raw.get("mix_output_settings"), + ) except requests.HTTPError as e: - # Log specific HTTP errors error_detail = f"{e.response.status_code} - {e.response.text}" if hasattr(e, 'response') and e.response else str(e) logger.error(f"HTTP error retrieving final mix: {error_detail}") raise Exception(f"Failed to retrieve final mix: {error_detail}") except Exception as e: - # Catch other potential exceptions logger.exception(f"Unexpected error retrieving final mix: {e}") raise diff --git a/roex_python/models/__init__.py b/roex_python/models/__init__.py index 928afd8..f071f26 100644 --- a/roex_python/models/__init__.py +++ b/roex_python/models/__init__.py @@ -17,8 +17,10 @@ from roex_python.models.mixing import ( FinalMixRequest, FinalMixRequestAdvanced, + FinalMixResult, MultitrackMixRequest, MultitrackTaskResponse, + PreviewMixResult, TrackData, TrackGainData, TrackEffectsData, @@ -31,18 +33,22 @@ # Import mastering models from roex_python.models.mastering import ( AlbumMasteringRequest, + FinalMasterResult, MasteringRequest, - MasteringTaskResponse + MasteringTaskResponse, + PreviewMasterResult ) # Import analysis models from roex_python.models.analysis import ( AnalysisMusicalStyle, + AnalysisResult, MixAnalysisRequest ) # Import enhance models from roex_python.models.enhance import ( + EnhancedTrackResult, EnhanceMusicalStyle, MixEnhanceRequest, MixEnhanceResponse @@ -75,8 +81,10 @@ # Mixing models "FinalMixRequest", "FinalMixRequestAdvanced", + "FinalMixResult", "MultitrackMixRequest", "MultitrackTaskResponse", + "PreviewMixResult", "TrackData", "TrackGainData", "TrackEffectsData", @@ -87,14 +95,18 @@ # Mastering models "AlbumMasteringRequest", + "FinalMasterResult", "MasteringRequest", "MasteringTaskResponse", + "PreviewMasterResult", # Analysis models "AnalysisMusicalStyle", + "AnalysisResult", "MixAnalysisRequest", # Enhance models + "EnhancedTrackResult", "EnhanceMusicalStyle", "MixEnhanceRequest", "MixEnhanceResponse", diff --git a/roex_python/models/analysis.py b/roex_python/models/analysis.py index c48b30f..5bd6f83 100644 --- a/roex_python/models/analysis.py +++ b/roex_python/models/analysis.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from enum import Enum +from typing import Any, Dict, Optional class AnalysisMusicalStyle(Enum): @@ -61,4 +62,22 @@ class MixAnalysisRequest: musical_style: AnalysisMusicalStyle """AnalysisMusicalStyle: The musical style of the track, used as a reference for the analysis (e.g., ROCK, POP, ELECTRONIC).""" is_master: bool - """bool: Indicates whether the provided audio file is a mastered track (True) or a mix (False).""" \ No newline at end of file + """bool: Indicates whether the provided audio file is a mastered track (True) or a mix (False).""" + + +@dataclass +class AnalysisResult: + """Result of a mix/master analysis from the ``/mixanalysis`` endpoint. + + The ``payload`` dict contains the detailed diagnosis metrics such as + ``integrated_loudness_lufs``, ``peak_loudness_dbfs``, ``tonal_profile``, + ``clipping``, ``stereo_field``, etc. + """ + payload: Optional[Dict[str, Any]] = None + """Optional[Dict[str, Any]]: The diagnosis metrics dictionary.""" + error: bool = False + """bool: Whether the analysis encountered an error.""" + info: str = "" + """str: Additional information from the API.""" + completion_time: str = "" + """str: Timestamp when the analysis completed.""" \ No newline at end of file diff --git a/roex_python/models/enhance.py b/roex_python/models/enhance.py index f91c082..43c0f12 100644 --- a/roex_python/models/enhance.py +++ b/roex_python/models/enhance.py @@ -4,7 +4,7 @@ from dataclasses import dataclass from enum import Enum -from typing import Optional +from typing import Dict, Optional from roex_python.models.common import LoudnessPreference @@ -57,4 +57,21 @@ class MixEnhanceResponse: """Response model for mix enhancement task creation""" mixrevive_task_id: str error: bool - message: str \ No newline at end of file + message: str + + +@dataclass +class EnhancedTrackResult: + """Result of a completed mix enhancement (preview or full). + + Returned by ``EnhanceController.retrieve_enhanced_track`` after polling + the ``/retrieveenhancedtrack`` endpoint. + """ + download_url_preview_revived: Optional[str] = None + """Optional[str]: Signed URL for the MP3 preview of the enhanced track.""" + download_url_revived: Optional[str] = None + """Optional[str]: Signed URL for the full WAV enhanced track.""" + stems: Optional[Dict[str, str]] = None + """Optional[Dict[str, str]]: URLs keyed by stem name (``vocal``, ``bass``, ``drums``, ``other``).""" + preview_start_time: Optional[float] = None + """Optional[float]: Offset in seconds where the preview clip starts in the original track.""" \ No newline at end of file diff --git a/roex_python/models/mastering.py b/roex_python/models/mastering.py index a65281a..b5ac02c 100644 --- a/roex_python/models/mastering.py +++ b/roex_python/models/mastering.py @@ -5,6 +5,7 @@ from dataclasses import dataclass, field from typing import List, Optional + from roex_python.models.common import DesiredLoudness, MusicalStyle @@ -50,4 +51,20 @@ class AlbumMasteringRequest: This model might be reserved for future use or internal API structures. """ tracks: List[MasteringRequest] - """List[MasteringRequest]: A list of individual MasteringRequest objects, one for each track to be mastered.""" \ No newline at end of file + """List[MasteringRequest]: A list of individual MasteringRequest objects, one for each track to be mastered.""" + + +@dataclass +class PreviewMasterResult: + """Result of a completed mastering preview from the ``/retrievepreviewmaster`` endpoint.""" + download_url_mastered_preview: Optional[str] = None + """Optional[str]: Signed URL for the mastered preview audio file.""" + preview_start_time: Optional[float] = None + """Optional[float]: Offset in seconds where the preview clip starts in the original track.""" + + +@dataclass +class FinalMasterResult: + """Result of a completed final master from the ``/retrievefinalmaster`` endpoint.""" + download_url_mastered: Optional[str] = None + """Optional[str]: Signed URL for the final mastered audio file.""" \ No newline at end of file diff --git a/roex_python/models/mixing.py b/roex_python/models/mixing.py index fa0f45d..214b174 100644 --- a/roex_python/models/mixing.py +++ b/roex_python/models/mixing.py @@ -3,7 +3,7 @@ """ from dataclasses import dataclass, field -from typing import List, Optional +from typing import Any, Dict, List, Optional from roex_python.models.common import ( DesiredLoudness, @@ -407,4 +407,28 @@ class FinalMixRequestAdvanced: sample_rate: str = "44100" """str: The desired sample rate for the output. "44100" for 44.1kHz (16-bit) or "48000" for 48kHz (24-bit). Defaults to "44100".""" webhook_url: Optional[str] = None - """Optional[str]: A URL to which a notification will be sent upon task completion.""" \ No newline at end of file + """Optional[str]: A URL to which a notification will be sent upon task completion.""" + + +@dataclass +class PreviewMixResult: + """Result of a completed mix preview from the ``/retrievepreviewmix`` endpoint.""" + download_url_preview_mixed: Optional[str] = None + """Optional[str]: Signed URL for the preview mix audio file.""" + stems: Optional[Dict[str, str]] = None + """Optional[Dict[str, str]]: URLs keyed by stem name, if stems were requested.""" + mix_output_settings: Optional[Dict[str, Any]] = None + """Optional[Dict[str, Any]]: The mixing settings applied (gain, pan, etc.).""" + status: Optional[str] = None + """Optional[str]: Task status (e.g. ``MIX_TASK_PREVIEW_COMPLETED``).""" + + +@dataclass +class FinalMixResult: + """Result of a completed final mix from the ``/retrievefinalmix`` endpoint.""" + download_url_mixed: Optional[str] = None + """Optional[str]: Signed URL for the final mix audio file.""" + stems: Optional[Dict[str, str]] = None + """Optional[Dict[str, str]]: URLs keyed by stem name, if stems were requested.""" + mix_output_settings: Optional[Dict[str, Any]] = None + """Optional[Dict[str, Any]]: The mixing settings applied.""" \ No newline at end of file diff --git a/setup.py b/setup.py index e938224..916dd22 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="roex_python", - version="1.3.1", + version="1.3.2", author="RoEx Audio", author_email="support@roexaudio.com", description="Pip package for the RoEx Tonn API", diff --git a/tests/integration/test_analysis_integration.py b/tests/integration/test_analysis_integration.py index a114ea3..b0f1ab5 100644 --- a/tests/integration/test_analysis_integration.py +++ b/tests/integration/test_analysis_integration.py @@ -31,14 +31,12 @@ def test_analyze_mix(self, requires_api_key, integration_audio_file): results = client.analysis.analyze_mix(request) - # Verify results contain expected metrics - assert "payload" in results or isinstance(results, dict) - print(f"Analysis results keys: {results.keys()}") + assert results is not None + assert results.payload is not None + print(f"Analysis payload keys: {list(results.payload.keys())}") - # Check for common analysis fields - payload = results.get("payload", results) - if "integrated_loudness_lufs" in payload: - print(f"Integrated loudness: {payload['integrated_loudness_lufs']} LUFS") + if "integrated_loudness_lufs" in results.payload: + print(f"Integrated loudness: {results.payload['integrated_loudness_lufs']} LUFS") def test_compare_two_mixes(self, requires_api_key, integration_audio_file): """Test comparing two mixes""" diff --git a/tests/integration/test_mastering_integration.py b/tests/integration/test_mastering_integration.py index eabc62d..50d53e4 100644 --- a/tests/integration/test_mastering_integration.py +++ b/tests/integration/test_mastering_integration.py @@ -52,13 +52,13 @@ def test_complete_mastering_workflow(self, requires_api_key, integration_audio_f max_attempts=30, poll_interval=5 ) - assert "download_url_mastered_preview" in preview - print(f"Preview ready: {preview.get('download_url_mastered_preview')}") + assert preview.download_url_mastered_preview is not None + print(f"Preview ready: {preview.download_url_mastered_preview}") # Retrieve final master - final_url = client.mastering.retrieve_final_master(task.mastering_task_id) - assert final_url is not None - print(f"Final master URL: {final_url}") + final_result = client.mastering.retrieve_final_master(task.mastering_task_id) + assert final_result.download_url_mastered is not None + print(f"Final master URL: {final_result.download_url_mastered}") def test_mastering_different_styles(self, requires_api_key, integration_audio_file): """Test mastering with different musical styles""" diff --git a/tests/smoke_test.py b/tests/smoke_test.py index 16d6416..08de900 100644 --- a/tests/smoke_test.py +++ b/tests/smoke_test.py @@ -1,5 +1,5 @@ """ -Smoke test for roex-python SDK v1.3.1 +Smoke test for roex-python SDK v1.3.2 Validates that models, enums, and payloads are accepted by the live API. Run: ROEX_API_KEY= python -m tests.smoke_test """ @@ -28,7 +28,7 @@ def main(): ) from roex_python.utils import upload_file import roex_python - assert roex_python.__version__ == "1.3.1" + assert roex_python.__version__ == "1.3.2" print(f"OK (v{roex_python.__version__})") except Exception as e: print(f"FAIL: {e}") @@ -106,7 +106,8 @@ def main(): ) results = client.analysis.analyze_mix(request) assert results is not None - print(f"OK (keys: {list(results.keys()) if isinstance(results, dict) else 'non-dict'})") + assert results.payload is not None + print(f"OK (payload keys: {list(results.payload.keys())})") except Exception as e: print(f"FAIL: {e}") failures.append(("Analysis", e)) diff --git a/tests/unit/test_controllers/test_analysis_controller.py b/tests/unit/test_controllers/test_analysis_controller.py index 2b1217b..1209781 100644 --- a/tests/unit/test_controllers/test_analysis_controller.py +++ b/tests/unit/test_controllers/test_analysis_controller.py @@ -6,7 +6,7 @@ from unittest.mock import Mock import requests from roex_python.controllers.analysis_controller import AnalysisController -from roex_python.models import MixAnalysisRequest, AnalysisMusicalStyle +from roex_python.models import MixAnalysisRequest, AnalysisMusicalStyle, AnalysisResult @pytest.mark.unit @@ -48,13 +48,12 @@ def test_successful_analysis(self, mock_api_provider): is_master=False ) - # Execute result = controller.analyze_mix(request) - # Assert - assert "payload" in result - assert result["payload"]["integrated_loudness_lufs"] == -14.5 - assert result["payload"]["clipping"] == "NO" + assert isinstance(result, AnalysisResult) + assert result.payload["integrated_loudness_lufs"] == -14.5 + assert result.payload["clipping"] == "NO" + assert result.error is False # Verify correct payload was sent call_args = mock_api_provider.post.call_args @@ -93,7 +92,6 @@ def test_master_analysis(self, mock_api_provider): def test_response_without_expected_format(self, mock_api_provider): """Test handling of response without expected format""" - # Setup mock_api_provider.post.return_value = { "some_other_key": "value" } @@ -106,11 +104,10 @@ def test_response_without_expected_format(self, mock_api_provider): is_master=False ) - # Execute result = controller.analyze_mix(request) - # Assert - should return raw response - assert result == {"some_other_key": "value"} + assert isinstance(result, AnalysisResult) + assert result.payload is None def test_http_error_handling(self, mock_api_provider): """Test error handling when API returns HTTP error""" @@ -236,8 +233,8 @@ def test_extract_all_metrics(self, mock_api_provider): """Test extraction of all available metrics""" controller = AnalysisController(mock_api_provider) - diagnosis = { - "payload": { + diagnosis = AnalysisResult( + payload={ "bit_depth": 24, "clipping": "NO", "integrated_loudness_lufs": -14.5, @@ -250,12 +247,10 @@ def test_extract_all_metrics(self, mock_api_provider): "high_frequency": "BRIGHT" } } - } + ) - # Execute metrics = controller._extract_metrics(diagnosis) - # Assert assert metrics["bit_depth"] == 24 assert metrics["clipping"] == "NO" assert metrics["integrated_loudness_lufs"] == -14.5 @@ -267,17 +262,14 @@ def test_extract_with_missing_fields(self, mock_api_provider): """Test extraction when some fields are missing""" controller = AnalysisController(mock_api_provider) - diagnosis = { - "payload": { + diagnosis = AnalysisResult( + payload={ "integrated_loudness_lufs": -14.5, - # Missing many fields } - } + ) - # Execute metrics = controller._extract_metrics(diagnosis) - # Assert - missing fields should be "N/A" assert metrics["integrated_loudness_lufs"] == -14.5 assert metrics["clipping"] == "N/A" assert metrics["bit_depth"] == "N/A" @@ -286,12 +278,10 @@ def test_extract_with_empty_payload(self, mock_api_provider): """Test extraction with empty payload""" controller = AnalysisController(mock_api_provider) - diagnosis = {} + diagnosis = AnalysisResult() - # Execute metrics = controller._extract_metrics(diagnosis) - # Assert - all fields should be "N/A" assert all(value == "N/A" or value == {} for value in metrics.values()) @@ -303,28 +293,22 @@ def test_numeric_comparison(self, mock_api_provider): """Test comparison of numeric values""" controller = AnalysisController(mock_api_provider) - results_a = { - "payload": { - "integrated_loudness_lufs": -14.0, - "peak_loudness_dbfs": -1.0, - "bit_depth": 24, - "sample_rate": 44100 - } - } + results_a = AnalysisResult(payload={ + "integrated_loudness_lufs": -14.0, + "peak_loudness_dbfs": -1.0, + "bit_depth": 24, + "sample_rate": 44100 + }) - results_b = { - "payload": { - "integrated_loudness_lufs": -10.0, - "peak_loudness_dbfs": -0.5, - "bit_depth": 16, - "sample_rate": 48000 - } - } + results_b = AnalysisResult(payload={ + "integrated_loudness_lufs": -10.0, + "peak_loudness_dbfs": -0.5, + "bit_depth": 16, + "sample_rate": 48000 + }) - # Execute differences = controller._compare_metrics(results_a, results_b) - # Assert assert differences["integrated_loudness_lufs"]["difference"] == 4.0 assert differences["peak_loudness_dbfs"]["difference"] == 0.5 assert differences["bit_depth"]["difference"] == 8.0 @@ -334,24 +318,11 @@ def test_categorical_comparison(self, mock_api_provider): """Test comparison of categorical values""" controller = AnalysisController(mock_api_provider) - results_a = { - "payload": { - "clipping": "NO", - "stereo_field": "GOOD" - } - } + results_a = AnalysisResult(payload={"clipping": "NO", "stereo_field": "GOOD"}) + results_b = AnalysisResult(payload={"clipping": "YES", "stereo_field": "GOOD"}) - results_b = { - "payload": { - "clipping": "YES", - "stereo_field": "GOOD" - } - } - - # Execute differences = controller._compare_metrics(results_a, results_b) - # Assert assert differences["clipping"]["status"] == "DIFFERENT" assert differences["stereo_field"]["status"] == "SAME" @@ -359,27 +330,14 @@ def test_tonal_profile_comparison(self, mock_api_provider): """Test comparison of tonal profiles""" controller = AnalysisController(mock_api_provider) - results_a = { - "payload": { - "tonal_profile": { - "bass_frequency": "GOOD", - "high_frequency": "BRIGHT" - } - } - } - - results_b = { - "payload": { - "tonal_profile": { - "bass_frequency": "WEAK", - "high_frequency": "BRIGHT" - } - } - } + results_a = AnalysisResult(payload={ + "tonal_profile": {"bass_frequency": "GOOD", "high_frequency": "BRIGHT"} + }) + results_b = AnalysisResult(payload={ + "tonal_profile": {"bass_frequency": "WEAK", "high_frequency": "BRIGHT"} + }) - # Execute differences = controller._compare_metrics(results_a, results_b) - # Assert assert differences["tonal_profile"]["bass_frequency"]["status"] == "DIFFERENT" assert differences["tonal_profile"]["high_frequency"]["status"] == "SAME" diff --git a/tests/unit/test_controllers/test_enhance_controller.py b/tests/unit/test_controllers/test_enhance_controller.py index 6818406..37b8f95 100644 --- a/tests/unit/test_controllers/test_enhance_controller.py +++ b/tests/unit/test_controllers/test_enhance_controller.py @@ -7,7 +7,8 @@ import requests from roex_python.controllers.enhance_controller import EnhanceController from roex_python.models import ( - MixEnhanceRequest, MixEnhanceResponse, MusicalStyle, LoudnessPreference + MixEnhanceRequest, MixEnhanceResponse, EnhancedTrackResult, + MusicalStyle, LoudnessPreference ) @@ -164,74 +165,53 @@ class TestRetrieveEnhancedTrack: @patch('roex_python.controllers.enhance_controller.time.sleep') def test_successful_retrieval(self, mock_sleep, mock_api_provider): """Test successful enhanced track retrieval""" - # Setup mock_api_provider.post.return_value = { "error": False, - "revived_track_tasks_results": { - "download_url_revived": "https://example.com/enhanced.wav" + "revivedTrackTaskResults": { + "download_url_revived": "https://example.com/enhanced.wav", + "download_url_preview_revived": "https://example.com/preview.mp3", + "stems": {"vocal": "https://example.com/vocal.wav"}, + "preview_start_time": 30.0 } } controller = EnhanceController(mock_api_provider) - - # Execute result = controller.retrieve_enhanced_track("enhance_task_123") - # Assert - assert result["download_url_revived"] == "https://example.com/enhanced.wav" + assert isinstance(result, EnhancedTrackResult) + assert result.download_url_revived == "https://example.com/enhanced.wav" + assert result.download_url_preview_revived == "https://example.com/preview.mp3" + assert result.stems == {"vocal": "https://example.com/vocal.wav"} + assert result.preview_start_time == 30.0 @patch('roex_python.controllers.enhance_controller.time.sleep') def test_polling_until_ready(self, mock_sleep, mock_api_provider): """Test polling until enhanced track is ready""" - # Setup - first calls return error, last returns result mock_api_provider.post.side_effect = [ {"error": True}, {"error": True}, { "error": False, - "revived_track_tasks_results": { + "revivedTrackTaskResults": { "download_url_revived": "https://example.com/enhanced.wav" } } ] controller = EnhanceController(mock_api_provider) - - # Execute result = controller.retrieve_enhanced_track("enhance_task_123", poll_interval=1) - # Assert - assert result["download_url_revived"] == "https://example.com/enhanced.wav" + assert isinstance(result, EnhancedTrackResult) + assert result.download_url_revived == "https://example.com/enhanced.wav" assert mock_api_provider.post.call_count == 3 - @patch('roex_python.controllers.enhance_controller.time.sleep') - def test_alternate_response_format(self, mock_sleep, mock_api_provider): - """Test handling of alternate response format""" - # Setup - response in different format - mock_api_provider.post.return_value = { - "error": False, - "track_results": { - "download_url_preview_revived": "https://example.com/preview_enhanced.wav" - } - } - - controller = EnhanceController(mock_api_provider) - - # Execute - result = controller.retrieve_enhanced_track("enhance_task_123") - - # Assert - should find the download URL in nested structure - assert "download_url_preview_revived" in result - @patch('roex_python.controllers.enhance_controller.time.sleep') def test_polling_timeout(self, mock_sleep, mock_api_provider): """Test polling timeout after max attempts""" - # Setup - always return error mock_api_provider.post.return_value = {"error": True} controller = EnhanceController(mock_api_provider) - # timeout=3, poll_interval=1 -> 3 attempts with pytest.raises(Exception, match="did not complete after polling"): controller.retrieve_enhanced_track("enhance_task_123", timeout=3, poll_interval=1) @@ -240,24 +220,21 @@ def test_polling_timeout(self, mock_sleep, mock_api_provider): @patch('roex_python.controllers.enhance_controller.time.sleep') def test_http_error_continues_polling(self, mock_sleep, mock_api_provider): """Test that HTTP errors don't stop polling""" - # Setup - first call errors, second succeeds mock_api_provider.post.side_effect = [ requests.HTTPError("Temporary error"), { "error": False, - "revived_track_tasks_results": { + "revivedTrackTaskResults": { "download_url_revived": "https://example.com/enhanced.wav" } } ] controller = EnhanceController(mock_api_provider) - - # timeout=10, poll_interval=1 -> up to 10 attempts result = controller.retrieve_enhanced_track("enhance_task_123", timeout=10, poll_interval=1) - # Assert - should eventually succeed despite initial error - assert result["download_url_revived"] == "https://example.com/enhanced.wav" + assert isinstance(result, EnhancedTrackResult) + assert result.download_url_revived == "https://example.com/enhanced.wav" @pytest.mark.unit diff --git a/tests/unit/test_controllers/test_mastering_controller.py b/tests/unit/test_controllers/test_mastering_controller.py index 8e80243..bc08088 100644 --- a/tests/unit/test_controllers/test_mastering_controller.py +++ b/tests/unit/test_controllers/test_mastering_controller.py @@ -9,7 +9,7 @@ from roex_python.controllers.mastering_controller import MasteringController from roex_python.models import ( MasteringRequest, MusicalStyle, DesiredLoudness, - MasteringTaskResponse + MasteringTaskResponse, PreviewMasterResult, FinalMasterResult ) @@ -102,27 +102,23 @@ class TestRetrievePreviewMaster: def test_immediate_success(self, mock_api_provider): """Test when preview is immediately available""" - # Setup mock_api_provider.post.return_value = { "previewMasterTaskResults": { "download_url_mastered_preview": "https://example.com/preview.wav", - "status": "completed" + "preview_start_time": 15.0 } } controller = MasteringController(mock_api_provider) - - # Execute result = controller.retrieve_preview_master("task_123") - # Assert - assert result["download_url_mastered_preview"] == "https://example.com/preview.wav" - assert result["status"] == "completed" + assert isinstance(result, PreviewMasterResult) + assert result.download_url_mastered_preview == "https://example.com/preview.wav" + assert result.preview_start_time == 15.0 @patch('roex_python.controllers.mastering_controller.time.sleep') def test_polling_until_ready(self, mock_sleep, mock_api_provider): """Test polling until preview is ready""" - # Setup - first call returns pending, second returns result mock_api_provider.post.side_effect = [ requests.HTTPError("Not ready"), {"status": 202}, @@ -134,12 +130,10 @@ def test_polling_until_ready(self, mock_sleep, mock_api_provider): ] controller = MasteringController(mock_api_provider) - - # Execute result = controller.retrieve_preview_master("task_123", poll_interval=1) - # Assert - assert result["download_url_mastered_preview"] == "https://example.com/preview.wav" + assert isinstance(result, PreviewMasterResult) + assert result.download_url_mastered_preview == "https://example.com/preview.wav" assert mock_api_provider.post.call_count == 3 @patch('roex_python.controllers.mastering_controller.time.sleep') @@ -169,7 +163,6 @@ class TestRetrieveFinalMaster: def test_structured_response(self, mock_api_provider): """Test with structured response format""" - # Setup mock_api_provider.post.return_value = { "finalMasterTaskResults": { "download_url_mastered": "https://example.com/final.wav" @@ -177,25 +170,22 @@ def test_structured_response(self, mock_api_provider): } controller = MasteringController(mock_api_provider) - - # Execute result = controller.retrieve_final_master("task_123") - # Assert - assert result["download_url_mastered"] == "https://example.com/final.wav" + assert isinstance(result, FinalMasterResult) + assert result.download_url_mastered == "https://example.com/final.wav" - def test_direct_url_response(self, mock_api_provider): - """Test when response is direct URL""" - # Setup - mock_api_provider.post.return_value = "https://example.com/final.wav" + def test_direct_url_in_response(self, mock_api_provider): + """Test when download_url_mastered is directly in response dict""" + mock_api_provider.post.return_value = { + "download_url_mastered": "https://example.com/final.wav" + } controller = MasteringController(mock_api_provider) - - # Execute result = controller.retrieve_final_master("task_123") - # Assert - assert result == "https://example.com/final.wav" + assert isinstance(result, FinalMasterResult) + assert result.download_url_mastered == "https://example.com/final.wav" def test_http_error(self, mock_api_provider): """Test error handling""" diff --git a/tests/unit/test_controllers/test_mix_controller.py b/tests/unit/test_controllers/test_mix_controller.py index 8f0e23c..f18f7e9 100644 --- a/tests/unit/test_controllers/test_mix_controller.py +++ b/tests/unit/test_controllers/test_mix_controller.py @@ -9,8 +9,8 @@ from roex_python.models import ( MultitrackMixRequest, TrackData, InstrumentGroup, PresenceSetting, PanPreference, ReverbPreference, - MusicalStyle, FinalMixRequest, TrackGainData, - MultitrackTaskResponse, FinalMixRequestAdvanced, + MusicalStyle, FinalMixRequest, FinalMixResult, TrackGainData, + MultitrackTaskResponse, PreviewMixResult, FinalMixRequestAdvanced, TrackEffectsData, EQSettings, EQBandSettings, CompressionSettings, PanningSettings, DesiredLoudness ) @@ -161,46 +161,42 @@ class TestRetrievePreviewMix: def test_immediate_success(self, mock_api_provider): """Test when preview is immediately available""" - # Setup mock_api_provider.post.return_value = { "previewMixTaskResults": { - "download_url_preview_mix": "https://example.com/preview.wav", - "status": "MIX_TASK_PREVIEW_COMPLETED" + "download_url_preview_mixed": "https://example.com/preview.wav", + "status": "MIX_TASK_PREVIEW_COMPLETED", + "stems": {"vocal": "https://example.com/vocal.wav"}, + "mix_output_settings": {"gain": 1.0} }, "status": "MIX_TASK_PREVIEW_COMPLETED" } controller = MixController(mock_api_provider) - - # Execute result = controller.retrieve_preview_mix("mix_task_123") - # Assert - assert result["download_url_preview_mix"] == "https://example.com/preview.wav" - assert result["status"] == "MIX_TASK_PREVIEW_COMPLETED" + assert isinstance(result, PreviewMixResult) + assert result.download_url_preview_mixed == "https://example.com/preview.wav" + assert result.status == "MIX_TASK_PREVIEW_COMPLETED" @patch('roex_python.controllers.mix_controller.time.sleep') def test_polling_until_ready(self, mock_sleep, mock_api_provider): """Test polling until preview is ready""" - # Setup - first calls return pending, last returns result mock_api_provider.post.side_effect = [ requests.HTTPError("Not ready"), {"status": "PROCESSING"}, { "previewMixTaskResults": { - "download_url_preview_mix": "https://example.com/preview.wav", + "download_url_preview_mixed": "https://example.com/preview.wav", "status": "MIX_TASK_PREVIEW_COMPLETED" } } ] controller = MixController(mock_api_provider) - - # Execute result = controller.retrieve_preview_mix("mix_task_123", poll_interval=1) - # Assert - assert result["download_url_preview_mix"] == "https://example.com/preview.wav" + assert isinstance(result, PreviewMixResult) + assert result.download_url_preview_mixed == "https://example.com/preview.wav" assert mock_api_provider.post.call_count == 3 @patch('roex_python.controllers.mix_controller.time.sleep') @@ -225,25 +221,21 @@ def test_polling_timeout(self, mock_sleep, mock_api_provider): def test_with_fx_settings(self, mock_api_provider): """Test retrieving preview with FX settings""" - # Setup mock_api_provider.post.return_value = { "previewMixTaskResults": { - "download_url_preview_mix": "https://example.com/preview.wav", - "fx_settings": {"compression": "moderate"}, + "download_url_preview_mixed": "https://example.com/preview.wav", + "mix_output_settings": {"compression": "moderate"}, "status": "MIX_TASK_PREVIEW_COMPLETED" }, "status": "MIX_TASK_PREVIEW_COMPLETED" } controller = MixController(mock_api_provider) - - # Execute result = controller.retrieve_preview_mix("mix_task_123", retrieve_fx_settings=True) - # Assert - assert "fx_settings" in result + assert isinstance(result, PreviewMixResult) + assert result.mix_output_settings == {"compression": "moderate"} - # Verify retrieveFXSettings was passed payload = mock_api_provider.post.call_args[0][1] assert payload["multitrackData"]["retrieveFXSettings"] is True @@ -254,10 +246,11 @@ class TestRetrieveFinalMix: def test_successful_retrieval(self, mock_api_provider): """Test successful final mix retrieval""" - # Setup mock_api_provider.post.return_value = { "applyAudioEffectsResults": { - "download_url_final_mix": "https://example.com/final.wav" + "download_url_mixed": "https://example.com/final.wav", + "stems": {"vocal": "https://example.com/vocal.wav"}, + "mix_output_settings": {"gain": 2.5} } } @@ -275,11 +268,10 @@ def test_successful_retrieval(self, mock_api_provider): track_data=track_data ) - # Execute result = controller.retrieve_final_mix(request) - # Assert - assert result["download_url_final_mix"] == "https://example.com/final.wav" + assert isinstance(result, FinalMixResult) + assert result.download_url_mixed == "https://example.com/final.wav" def test_with_multiple_tracks_and_gains(self, mock_api_provider): """Test final mix with multiple tracks and gain adjustments""" @@ -703,11 +695,10 @@ def test_successful_advanced_final_mix(self, mock_api_provider): webhook_url="https://example.com/webhook" ) - # Execute result = controller.retrieve_final_mix_advanced(request) - # Assert - assert result["download_url_mixed"] == "https://example.com/final_advanced.wav" + assert isinstance(result, FinalMixResult) + assert result.download_url_mixed == "https://example.com/final_advanced.wav" # Verify payload structure payload = mock_api_provider.post.call_args[0][1]