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
+ ![](https://www.phonexia.com/wp-content/uploads/phonexia-logo-transparent-500px.png)
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
+ ![](https://www.phonexia.com/wp-content/uploads/phonexia-logo-transparent-500px.png)
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
+ ]