phonexia-denoiser-client 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: phonexia-denoiser-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Client for communicating with Phonexia denoiser microservice
|
|
5
|
+
Keywords: grpc,voice,speech,noise,denoise,denoising,noise-reduction,noise-removal,voice-isolation,noise-cancellation
|
|
6
|
+
Author: Phonexia
|
|
7
|
+
Author-email: info@phonexia.com
|
|
8
|
+
Requires-Python: >=3.8,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Dist: grpcio (>=1.54.0,<2.0.0)
|
|
17
|
+
Requires-Dist: numpy (<2.0.0) ; python_version < "3.12"
|
|
18
|
+
Requires-Dist: numpy (>=2.0.0) ; python_version >= "3.12"
|
|
19
|
+
Requires-Dist: phonexia-grpc (>=2.0.0,<3.0.0)
|
|
20
|
+
Requires-Dist: soundfile (>=0.12.1,<0.13.0)
|
|
21
|
+
Project-URL: Homepage, https://phonexia.com
|
|
22
|
+
Project-URL: Issues, https://phonexia.atlassian.net/servicedesk/customer/portal/15/group/20/create/40
|
|
23
|
+
Project-URL: protofiles, https://github.com/phonexia/protofiles
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
# Phonexia denoiser client
|
|
29
|
+
|
|
30
|
+
This module contains client for communication with [denoiser](https://hub.docker.com/repository/docker/phonexia/denoiser/general) developed by [Phonexia](https://phonexia.com).
|
|
31
|
+
|
|
32
|
+
To use this client you will first need a running instance of any *Phonexia denoiser*. If you don't yet have any running instance, don't hesitate to [contact our sales department](mailto:info@phonexia.com).
|
|
33
|
+
|
|
34
|
+
You can learn more about the denoiser technology [here](https://docs.cloud.phonexia.com/docs/technologies/denoiser/).
|
|
35
|
+
|
|
36
|
+
On [this page](https://docs.cloud.phonexia.com/docs/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/denoiser/v1/denoiser.proto), you will find a *gRPC API* reference for *denoiser microservice*.
|
|
37
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import pathlib
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
from typing import Iterator, Optional
|
|
7
|
+
|
|
8
|
+
import grpc
|
|
9
|
+
import soundfile as sf
|
|
10
|
+
from google.protobuf.duration_pb2 import Duration
|
|
11
|
+
from phonexia.grpc.common.core_pb2 import Audio, RawAudioConfig, TimeRange
|
|
12
|
+
from phonexia.grpc.technologies.denoiser.v1.denoiser_pb2 import DenoiseRequest, DenoiseResponse
|
|
13
|
+
from phonexia.grpc.technologies.denoiser.v1.denoiser_pb2_grpc import DenoiserStub
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def time_to_duration(time: float) -> Optional[Duration]:
|
|
17
|
+
if time is None:
|
|
18
|
+
return None
|
|
19
|
+
duration = Duration()
|
|
20
|
+
duration.seconds = int(time)
|
|
21
|
+
duration.nanos = int((time - duration.seconds) * 1e9)
|
|
22
|
+
return duration
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def make_request(
|
|
26
|
+
file: str,
|
|
27
|
+
start: Optional[float],
|
|
28
|
+
end: Optional[float],
|
|
29
|
+
use_raw_audio: bool,
|
|
30
|
+
) -> Iterator[DenoiseRequest]:
|
|
31
|
+
time_range = TimeRange(start=time_to_duration(start), end=time_to_duration(end))
|
|
32
|
+
chunk_size = 1024 * 100
|
|
33
|
+
if use_raw_audio:
|
|
34
|
+
with sf.SoundFile(file) as r:
|
|
35
|
+
raw_audio_config = RawAudioConfig(
|
|
36
|
+
channels=r.channels,
|
|
37
|
+
sample_rate_hertz=r.samplerate,
|
|
38
|
+
encoding=RawAudioConfig.AudioEncoding.PCM16,
|
|
39
|
+
)
|
|
40
|
+
for data in r.blocks(blocksize=r.samplerate, dtype="int16"):
|
|
41
|
+
yield DenoiseRequest(
|
|
42
|
+
audio=Audio(
|
|
43
|
+
content=data.flatten().tobytes(),
|
|
44
|
+
raw_audio_config=raw_audio_config,
|
|
45
|
+
time_range=time_range,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
time_range = None
|
|
49
|
+
raw_audio_config = None
|
|
50
|
+
|
|
51
|
+
else:
|
|
52
|
+
with open(file, mode="rb") as fd:
|
|
53
|
+
while chunk := fd.read(chunk_size):
|
|
54
|
+
yield DenoiseRequest(audio=Audio(content=chunk, time_range=time_range))
|
|
55
|
+
time_range = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_result(
|
|
59
|
+
audio_path: str,
|
|
60
|
+
output_file: pathlib.Path,
|
|
61
|
+
billed_time: timedelta,
|
|
62
|
+
audio_data: bytearray,
|
|
63
|
+
raw_audio_config: Optional[RawAudioConfig] = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
logging.info(f"Writing denoised audio to '{output_file}'")
|
|
66
|
+
if raw_audio_config is None:
|
|
67
|
+
with open(output_file, "wb") as f:
|
|
68
|
+
f.write(audio_data)
|
|
69
|
+
else:
|
|
70
|
+
with sf.SoundFile(
|
|
71
|
+
output_file,
|
|
72
|
+
mode="w",
|
|
73
|
+
samplerate=raw_audio_config.sample_rate_hertz,
|
|
74
|
+
channels=1,
|
|
75
|
+
subtype="PCM_16",
|
|
76
|
+
format="wav",
|
|
77
|
+
) as file:
|
|
78
|
+
file.buffer_write(audio_data, dtype="int16")
|
|
79
|
+
|
|
80
|
+
result = {
|
|
81
|
+
"audio": audio_path,
|
|
82
|
+
"total_billed_time": str(billed_time),
|
|
83
|
+
"file_path": str(output_file),
|
|
84
|
+
}
|
|
85
|
+
print(json.dumps(result, indent=2))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def denoise(
|
|
89
|
+
channel: grpc.Channel,
|
|
90
|
+
file: str,
|
|
91
|
+
output_file: pathlib.Path,
|
|
92
|
+
start: Optional[float],
|
|
93
|
+
end: Optional[float],
|
|
94
|
+
metadata: Optional[list],
|
|
95
|
+
use_raw_audio: bool,
|
|
96
|
+
) -> None:
|
|
97
|
+
logging.info(f"Denoising '{file}'")
|
|
98
|
+
stub = DenoiserStub(channel)
|
|
99
|
+
response_it: Iterator[DenoiseResponse] = stub.Denoise(
|
|
100
|
+
make_request(file, start, end, use_raw_audio),
|
|
101
|
+
metadata=metadata,
|
|
102
|
+
)
|
|
103
|
+
billed_time = timedelta()
|
|
104
|
+
audio_data = bytearray()
|
|
105
|
+
raw_audio_config = None
|
|
106
|
+
for response in response_it:
|
|
107
|
+
if response.HasField("processed_audio_length"):
|
|
108
|
+
billed_time = response.processed_audio_length.ToTimedelta()
|
|
109
|
+
if response.result.audio.HasField("raw_audio_config"):
|
|
110
|
+
raw_audio_config = response.result.audio.raw_audio_config
|
|
111
|
+
audio_data += response.result.audio.content
|
|
112
|
+
|
|
113
|
+
write_result(file, output_file, billed_time, audio_data, raw_audio_config)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def existing_file(file: str) -> str:
|
|
117
|
+
if not pathlib.Path(file).exists():
|
|
118
|
+
raise argparse.ArgumentError(argument=None, message=f"File {file} does not exist")
|
|
119
|
+
return file
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def main():
|
|
123
|
+
parser = argparse.ArgumentParser(
|
|
124
|
+
description="Denoiser gRPC client. Removing noises and other disturbing elements from audio recordings."
|
|
125
|
+
)
|
|
126
|
+
parser.add_argument(
|
|
127
|
+
"-H",
|
|
128
|
+
"--host",
|
|
129
|
+
type=str,
|
|
130
|
+
default="localhost:8080",
|
|
131
|
+
help="Server address, default: localhost:8080",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"-l",
|
|
135
|
+
"--log_level",
|
|
136
|
+
type=str,
|
|
137
|
+
default="error",
|
|
138
|
+
choices=["critical", "error", "warning", "info", "debug"],
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
"--metadata",
|
|
142
|
+
metavar="key=value",
|
|
143
|
+
nargs="+",
|
|
144
|
+
type=lambda x: tuple(x.split("=")),
|
|
145
|
+
help="Custom client metadata",
|
|
146
|
+
)
|
|
147
|
+
parser.add_argument(
|
|
148
|
+
"-o",
|
|
149
|
+
"--output",
|
|
150
|
+
type=pathlib.Path,
|
|
151
|
+
required=True,
|
|
152
|
+
help="Output audio file in 'wav' format. The samplerate will be the same as of the input file",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
parser.add_argument("--use_ssl", action="store_true", help="Use SSL connection")
|
|
156
|
+
parser.add_argument("--start", type=float, help="Audio start time")
|
|
157
|
+
parser.add_argument("--end", type=float, help="Audio end time")
|
|
158
|
+
parser.add_argument(
|
|
159
|
+
"--use_raw_audio", action="store_true", help="Send the input in a raw format"
|
|
160
|
+
)
|
|
161
|
+
parser.add_argument("file", type=existing_file, help="Input audio file")
|
|
162
|
+
|
|
163
|
+
args = parser.parse_args()
|
|
164
|
+
|
|
165
|
+
if args.start is not None and args.start < 0:
|
|
166
|
+
raise ValueError("Parameter 'start' must be a non-negative float.")
|
|
167
|
+
|
|
168
|
+
if args.end is not None and args.end <= 0:
|
|
169
|
+
raise ValueError("Parameter 'end' must be a positive float.")
|
|
170
|
+
|
|
171
|
+
if args.start is not None and args.end is not None and args.start >= args.end:
|
|
172
|
+
raise ValueError("Parameter 'end' must be larger than 'start'.")
|
|
173
|
+
|
|
174
|
+
logging.basicConfig(
|
|
175
|
+
level=args.log_level.upper(),
|
|
176
|
+
format="[%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s",
|
|
177
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
logging.info(f"Connecting to {args.host}")
|
|
182
|
+
if args.use_ssl:
|
|
183
|
+
with grpc.secure_channel(
|
|
184
|
+
target=args.host, credentials=grpc.ssl_channel_credentials()
|
|
185
|
+
) as channel:
|
|
186
|
+
denoise(
|
|
187
|
+
channel=channel,
|
|
188
|
+
file=args.file,
|
|
189
|
+
output_file=args.output,
|
|
190
|
+
start=args.start,
|
|
191
|
+
end=args.end,
|
|
192
|
+
metadata=args.metadata,
|
|
193
|
+
use_raw_audio=args.use_raw_audio,
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
with grpc.insecure_channel(target=args.host) as channel:
|
|
197
|
+
denoise(
|
|
198
|
+
channel=channel,
|
|
199
|
+
file=args.file,
|
|
200
|
+
output_file=args.output,
|
|
201
|
+
start=args.start,
|
|
202
|
+
end=args.end,
|
|
203
|
+
metadata=args.metadata,
|
|
204
|
+
use_raw_audio=args.use_raw_audio,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except grpc.RpcError as e:
|
|
208
|
+
logging.exception(f"RPC failed: {e}") # noqa: TRY401
|
|
209
|
+
exit(1)
|
|
210
|
+
except Exception:
|
|
211
|
+
logging.exception("Unknown error")
|
|
212
|
+
exit(1)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Phonexia denoiser client
|
|
4
|
+
|
|
5
|
+
This module contains client for communication with [denoiser](https://hub.docker.com/repository/docker/phonexia/denoiser/general) developed by [Phonexia](https://phonexia.com).
|
|
6
|
+
|
|
7
|
+
To use this client you will first need a running instance of any *Phonexia denoiser*. If you don't yet have any running instance, don't hesitate to [contact our sales department](mailto:info@phonexia.com).
|
|
8
|
+
|
|
9
|
+
You can learn more about the denoiser technology [here](https://docs.cloud.phonexia.com/docs/technologies/denoiser/).
|
|
10
|
+
|
|
11
|
+
On [this page](https://docs.cloud.phonexia.com/docs/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/denoiser/v1/denoiser.proto), you will find a *gRPC API* reference for *denoiser microservice*.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "phonexia-denoiser-client"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Client for communicating with Phonexia denoiser microservice"
|
|
5
|
+
readme = "pypi-README.md"
|
|
6
|
+
keywords = ["grpc", "voice", "speech", "noise", "denoise", "denoising", "noise-reduction", "noise-removal", "voice-isolation", "noise-cancellation"]
|
|
7
|
+
authors = ["Phonexia <info@phonexia.com>"]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.urls]
|
|
10
|
+
Homepage = "https://phonexia.com"
|
|
11
|
+
Issues = "https://phonexia.atlassian.net/servicedesk/customer/portal/15/group/20/create/40"
|
|
12
|
+
protofiles = "https://github.com/phonexia/protofiles"
|
|
13
|
+
|
|
14
|
+
[tool.poetry.scripts]
|
|
15
|
+
denoiser_client = 'phonexia_denoiser_client:main'
|
|
16
|
+
|
|
17
|
+
[tool.poetry.dependencies]
|
|
18
|
+
python = ">=3.8,<4.0"
|
|
19
|
+
grpcio = "^1.54.0"
|
|
20
|
+
phonexia-grpc = {version="^2.0.0", source="pypi"}
|
|
21
|
+
soundfile = "^0.12.1"
|
|
22
|
+
numpy = [
|
|
23
|
+
{ version = "<2.0.0", markers = "python_version < '3.12'" },
|
|
24
|
+
{ version = ">=2.0.0", markers = "python_version >= '3.12'" }
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.poetry.group.dev.dependencies]
|
|
28
|
+
pytest = "^8.0.0"
|
|
29
|
+
pytest-cov = "^5.0.0"
|
|
30
|
+
pytest-env = "^1.0.0"
|
|
31
|
+
pytest-random-order = "^1.1.0"
|
|
32
|
+
black = "^24.0.0"
|
|
33
|
+
ruff = "^0.6.0"
|
|
34
|
+
|
|
35
|
+
[[tool.poetry.source]]
|
|
36
|
+
name = "PyPI"
|
|
37
|
+
priority = "primary"
|
|
38
|
+
|
|
39
|
+
[[tool.poetry.source]]
|
|
40
|
+
name = "gitlab"
|
|
41
|
+
url = "https://gitlab.cloud.phonexia.com/api/v4/groups/39/-/packages/pypi/simple"
|
|
42
|
+
|
|
43
|
+
[build-system]
|
|
44
|
+
requires = ["poetry-core>=1.0.0"]
|
|
45
|
+
build-backend = "poetry.core.masonry.api"
|
|
46
|
+
|
|
47
|
+
[tool.black]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = ['py38']
|
|
50
|
+
preview = true
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py38"
|
|
54
|
+
line-length = 100
|
|
55
|
+
fix = true
|
|
56
|
+
lint.select = [
|
|
57
|
+
# flake8-2020
|
|
58
|
+
"YTT",
|
|
59
|
+
# flake8-bandit
|
|
60
|
+
"S",
|
|
61
|
+
# flake8-bugbear
|
|
62
|
+
"B",
|
|
63
|
+
# flake8-builtins
|
|
64
|
+
"A",
|
|
65
|
+
# flake8-comprehensions
|
|
66
|
+
"C4",
|
|
67
|
+
# flake8-debugger
|
|
68
|
+
"T10",
|
|
69
|
+
# flake8-simplify
|
|
70
|
+
"SIM",
|
|
71
|
+
# isort
|
|
72
|
+
"I",
|
|
73
|
+
# mccabe
|
|
74
|
+
"C90",
|
|
75
|
+
# pycodestyle
|
|
76
|
+
"E", "W",
|
|
77
|
+
# pyflakes
|
|
78
|
+
"F",
|
|
79
|
+
# pygrep-hooks
|
|
80
|
+
"PGH",
|
|
81
|
+
# pyupgrade
|
|
82
|
+
"UP",
|
|
83
|
+
# ruff
|
|
84
|
+
"RUF",
|
|
85
|
+
# tryceratops
|
|
86
|
+
"TRY",
|
|
87
|
+
]
|
|
88
|
+
lint.ignore = [
|
|
89
|
+
# LineTooLong
|
|
90
|
+
"E501",
|
|
91
|
+
# DoNotAssignLambda
|
|
92
|
+
"E731",
|
|
93
|
+
# RaiseVanillaArgs aka Avoid specifying long messages outside the exception class
|
|
94
|
+
"TRY003",
|
|
95
|
+
]
|