| 1 | # Copyright 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| 2 | # |
| 3 | # Redistribution and use in source and binary forms, with or without |
| 4 | # modification, are permitted provided that the following conditions |
| 5 | # are met: |
| 6 | # * Redistributions of source code must retain the above copyright |
| 7 | # notice, this list of conditions and the following disclaimer. |
| 8 | # * Redistributions in binary form must reproduce the above copyright |
| 9 | # notice, this list of conditions and the following disclaimer in the |
| 10 | # documentation and/or other materials provided with the distribution. |
| 11 | # * Neither the name of NVIDIA CORPORATION nor the names of its |
| 12 | # contributors may be used to endorse or promote products derived |
| 13 | # from this software without specific prior written permission. |
| 14 | # |
| 15 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY |
| 16 | # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| 17 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| 18 | # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR |
| 19 | # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| 20 | # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| 21 | # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| 22 | # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| 23 | # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| 25 | # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | import argparse |
| 27 | import os |
| 28 | |
| 29 | import numpy as np |
| 30 | import requests |
| 31 | import soundfile as sf |
| 32 | |
| 33 | |
| 34 | def get_args(): |
| 35 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) |
| 36 | |
| 37 | parser.add_argument( |
| 38 | "--server-url", |
| 39 | type=str, |
| 40 | default="localhost:8000", |
| 41 | help="Address of the server", |
| 42 | ) |
| 43 | |
| 44 | parser.add_argument( |
| 45 | "--reference-audio", |
| 46 | type=str, |
| 47 | default="../../infer/examples/basic/basic_ref_en.wav", |
| 48 | help="Path to a single audio file. It can't be specified at the same time with --manifest-dir", |
| 49 | ) |
| 50 | |
| 51 | parser.add_argument( |
| 52 | "--reference-text", |
| 53 | type=str, |
| 54 | default="Some call me nature, others call me mother nature.", |
| 55 | help="", |
| 56 | ) |
| 57 | |
| 58 | parser.add_argument( |
| 59 | "--target-text", |
| 60 | type=str, |
| 61 | default="I don't really care what you call me. I've been a silent spectator, watching species evolve, empires rise and fall. But always remember, I am mighty and enduring.", |
| 62 | help="", |
| 63 | ) |
| 64 | |
| 65 | parser.add_argument( |
| 66 | "--model-name", |
| 67 | type=str, |
| 68 | default="f5_tts", |
| 69 | help="triton model_repo module name to request", |
| 70 | ) |
| 71 | |
| 72 | parser.add_argument( |
| 73 | "--output-audio", |
| 74 | type=str, |
| 75 | default="tests/client_http.wav", |
| 76 | help="Path to save the output audio", |
| 77 | ) |
| 78 | return parser.parse_args() |
| 79 | |
| 80 | |
| 81 | def prepare_request( |
| 82 | waveform, |
| 83 | reference_text, |
| 84 | target_text, |
| 85 | sample_rate=24000, |
| 86 | audio_save_dir: str = "./", |
| 87 | ): |
| 88 | assert len(waveform.shape) == 1, "waveform should be 1D" |
| 89 | lengths = np.array([[len(waveform)]], dtype=np.int32) |
| 90 | waveform = waveform.reshape(1, -1).astype(np.float32) |
| 91 | |
| 92 | data = { |
| 93 | "inputs": [ |
| 94 | {"name": "reference_wav", "shape": waveform.shape, "datatype": "FP32", "data": waveform.tolist()}, |
| 95 | { |
| 96 | "name": "reference_wav_len", |
| 97 | "shape": lengths.shape, |
| 98 | "datatype": "INT32", |
| 99 | "data": lengths.tolist(), |
| 100 | }, |
| 101 | {"name": "reference_text", "shape": [1, 1], "datatype": "BYTES", "data": [reference_text]}, |
| 102 | {"name": "target_text", "shape": [1, 1], "datatype": "BYTES", "data": [target_text]}, |
| 103 | ] |
| 104 | } |
| 105 | |
| 106 | return data |
| 107 | |
| 108 | |
| 109 | def load_audio(wav_path, target_sample_rate=24000): |
| 110 | assert target_sample_rate == 24000, "hard coding in server" |
| 111 | if isinstance(wav_path, dict): |
| 112 | waveform = wav_path["array"] |
| 113 | sample_rate = wav_path["sampling_rate"] |
| 114 | else: |
| 115 | waveform, sample_rate = sf.read(wav_path) |
| 116 | if sample_rate != target_sample_rate: |
| 117 | from scipy.signal import resample |
| 118 | |
| 119 | waveform = resample(waveform, int(len(waveform) * (target_sample_rate / sample_rate))) |
| 120 | return waveform, target_sample_rate |
| 121 | |
| 122 | |
| 123 | if __name__ == "__main__": |
| 124 | args = get_args() |
| 125 | server_url = args.server_url |
| 126 | if not server_url.startswith(("http://", "https://")): |
| 127 | server_url = f"http://{server_url}" |
| 128 | |
| 129 | url = f"{server_url}/v2/models/{args.model_name}/infer" |
| 130 | waveform, sr = load_audio(args.reference_audio) |
| 131 | assert sr == 24000, "sample rate hardcoded in server" |
| 132 | |
| 133 | waveform = np.array(waveform, dtype=np.float32) |
| 134 | data = prepare_request(waveform, args.reference_text, args.target_text) |
| 135 | |
| 136 | rsp = requests.post( |
| 137 | url, headers={"Content-Type": "application/json"}, json=data, verify=False, params={"request_id": "0"} |
| 138 | ) |
| 139 | result = rsp.json() |
| 140 | audio = result["outputs"][0]["data"] |
| 141 | audio = np.array(audio, dtype=np.float32) |
| 142 | os.makedirs(os.path.dirname(args.output_audio), exist_ok=True) |
| 143 | sf.write(args.output_audio, audio, 24000, "PCM_16") |
| 144 |