phonexia-time-analysis-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,35 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: phonexia-time-analysis-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Client for communication with Phonexia time analysis microservice.
|
|
5
|
+
Keywords: grpc,voice,analysis
|
|
6
|
+
Author: Phonexia
|
|
7
|
+
Author-email: info@phonexia.com
|
|
8
|
+
Requires-Python: >=3.9,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Dist: grpcio (>=1.54.0,<2.0.0)
|
|
16
|
+
Requires-Dist: numpy (<2.0.0) ; python_version < "3.12"
|
|
17
|
+
Requires-Dist: numpy (>=2.0.0) ; python_version >= "3.12"
|
|
18
|
+
Requires-Dist: phonexia-grpc (>=2.0.0,<3.0.0)
|
|
19
|
+
Requires-Dist: soundfile (>=0.12.1,<0.13.0)
|
|
20
|
+
Project-URL: Homepage, https://phonexia.com
|
|
21
|
+
Project-URL: Issues, https://phonexia.atlassian.net/servicedesk/customer/portal/15/group/20/create/40
|
|
22
|
+
Project-URL: protofiles, https://github.com/phonexia/protofiles
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
# Phonexia time analysis client
|
|
28
|
+
|
|
29
|
+
This module contains client for communication with [time analysis microservice](https://hub.docker.com/repository/docker/phonexia/time-analysis/general) developed by [Phonexia](https://phonexia.com).
|
|
30
|
+
|
|
31
|
+
To use this client you will first need a running instance of any *Phonexia time analysis microservice*. If you don't yet have any running instance, don't hesitate to [contact our sales department](mailto:info@phonexia.com).
|
|
32
|
+
|
|
33
|
+
You can learn more about the time analysis technology [here](https://docs.phonexia.com/products/speech-platform-4/technologies/time-analysis/).
|
|
34
|
+
|
|
35
|
+
On [this page](https://docs.phonexia.com/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/time_analysis/v1/time_analysis.proto), you will find a *gRPC API* reference for *time analysis microservice*.
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Iterator, Optional
|
|
11
|
+
|
|
12
|
+
import grpc
|
|
13
|
+
import numpy as np
|
|
14
|
+
import soundfile
|
|
15
|
+
from google.protobuf.duration_pb2 import Duration
|
|
16
|
+
from google.protobuf.json_format import MessageToDict
|
|
17
|
+
from phonexia.grpc.common.core_pb2 import (
|
|
18
|
+
Audio,
|
|
19
|
+
RawAudioConfig,
|
|
20
|
+
TimeRange,
|
|
21
|
+
)
|
|
22
|
+
from phonexia.grpc.technologies.time_analysis.v1.time_analysis_pb2 import (
|
|
23
|
+
AnalyzeRequest,
|
|
24
|
+
AnalyzeResponse,
|
|
25
|
+
)
|
|
26
|
+
from phonexia.grpc.technologies.time_analysis.v1.time_analysis_pb2_grpc import (
|
|
27
|
+
TimeAnalysisStub,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def print_json(message, output_file: Optional[Path] = None) -> None:
|
|
32
|
+
json.dump(
|
|
33
|
+
message, output_file.open("w") if output_file else sys.stdout, indent=2, ensure_ascii=False
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def message_to_dict(message):
|
|
38
|
+
return MessageToDict(
|
|
39
|
+
message,
|
|
40
|
+
always_print_fields_with_no_presence=True,
|
|
41
|
+
preserving_proto_field_name=True,
|
|
42
|
+
use_integers_for_enums=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def time_to_duration(time: float) -> Optional[Duration]:
|
|
47
|
+
if time is None:
|
|
48
|
+
return None
|
|
49
|
+
duration = Duration()
|
|
50
|
+
duration.seconds = int(time)
|
|
51
|
+
duration.nanos = int((time - duration.seconds) * 1e9)
|
|
52
|
+
return duration
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def make_request(
|
|
56
|
+
file: str,
|
|
57
|
+
start: Optional[float],
|
|
58
|
+
end: Optional[float],
|
|
59
|
+
use_raw_audio: bool,
|
|
60
|
+
) -> Iterator[AnalyzeRequest]:
|
|
61
|
+
time_range = TimeRange(start=time_to_duration(start), end=time_to_duration(end))
|
|
62
|
+
chunk_size = 1024 * 100
|
|
63
|
+
if use_raw_audio:
|
|
64
|
+
with soundfile.SoundFile(file) as r:
|
|
65
|
+
raw_audio_config = RawAudioConfig(
|
|
66
|
+
channels=r.channels,
|
|
67
|
+
sample_rate_hertz=r.samplerate,
|
|
68
|
+
encoding=RawAudioConfig.AudioEncoding.PCM16,
|
|
69
|
+
)
|
|
70
|
+
for data in r.blocks(blocksize=r.samplerate, dtype="float32"):
|
|
71
|
+
int16_info = np.iinfo(np.int16)
|
|
72
|
+
data_scaled = np.clip(
|
|
73
|
+
data * (int16_info.max + 1), int16_info.min, int16_info.max
|
|
74
|
+
).astype("int16")
|
|
75
|
+
yield AnalyzeRequest(
|
|
76
|
+
audio=Audio(
|
|
77
|
+
content=data_scaled.flatten().tobytes(),
|
|
78
|
+
raw_audio_config=raw_audio_config,
|
|
79
|
+
time_range=time_range,
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
time_range = None
|
|
83
|
+
raw_audio_config = None
|
|
84
|
+
|
|
85
|
+
else:
|
|
86
|
+
with open(file, mode="rb") as fd:
|
|
87
|
+
while chunk := fd.read(chunk_size):
|
|
88
|
+
yield AnalyzeRequest(audio=Audio(content=chunk, time_range=time_range))
|
|
89
|
+
time_range = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def write_result(response: AnalyzeResponse, output_file: Optional[str] = None) -> None:
|
|
93
|
+
logging.debug("Writing matches to {}".format(output_file if output_file else "'stdout'"))
|
|
94
|
+
message = message_to_dict(message=response)
|
|
95
|
+
print_json(message, output_file)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def analyze(
|
|
99
|
+
channel: grpc.Channel,
|
|
100
|
+
input_file: Path,
|
|
101
|
+
output_file: Optional[Path],
|
|
102
|
+
start: Optional[float],
|
|
103
|
+
end: Optional[float],
|
|
104
|
+
metadata: Optional[list],
|
|
105
|
+
use_raw_audio: bool,
|
|
106
|
+
) -> None:
|
|
107
|
+
logging.info(
|
|
108
|
+
"Analyzing {input} -> {output}".format(
|
|
109
|
+
input=input_file, output=(output_file if output_file else "'stdout'")
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
response_it = make_request(
|
|
114
|
+
file=input_file,
|
|
115
|
+
start=start,
|
|
116
|
+
end=end,
|
|
117
|
+
use_raw_audio=use_raw_audio,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
stub = TimeAnalysisStub(channel)
|
|
121
|
+
for response in stub.Analyze(response_it, metadata=metadata):
|
|
122
|
+
write_result(response, output_file)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# Main program
|
|
126
|
+
def handle_grpc_error(e: grpc.RpcError):
|
|
127
|
+
logging.error(f"gRPC call failed with status code: {e.code()}")
|
|
128
|
+
logging.error(f"Error details: {e.details()}")
|
|
129
|
+
|
|
130
|
+
if e.code() == grpc.StatusCode.UNAVAILABLE:
|
|
131
|
+
logging.error("Service is unavailable. Please try again later.")
|
|
132
|
+
elif e.code() == grpc.StatusCode.INVALID_ARGUMENT:
|
|
133
|
+
logging.error("Invalid arguments were provided to the RPC.")
|
|
134
|
+
elif e.code() == grpc.StatusCode.DEADLINE_EXCEEDED:
|
|
135
|
+
logging.error("The RPC deadline was exceeded.")
|
|
136
|
+
else:
|
|
137
|
+
logging.error(f"An unexpected error occurred: {e.code()} - {e.details()}")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def check_file_exists(path: Path) -> Path:
|
|
141
|
+
if not os.path.isfile(path):
|
|
142
|
+
raise argparse.ArgumentTypeError(f"File '{path}' does not exist.")
|
|
143
|
+
return Path(path)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main():
|
|
147
|
+
parser = argparse.ArgumentParser(
|
|
148
|
+
description=("Analyze time based traits in a multichannel audio file"),
|
|
149
|
+
)
|
|
150
|
+
parser.add_argument(
|
|
151
|
+
"-H",
|
|
152
|
+
"--host",
|
|
153
|
+
type=str,
|
|
154
|
+
default="localhost:8080",
|
|
155
|
+
)
|
|
156
|
+
parser.add_argument(
|
|
157
|
+
"-l",
|
|
158
|
+
"--log_level",
|
|
159
|
+
type=str,
|
|
160
|
+
default="error",
|
|
161
|
+
choices=["critical", "error", "warning", "info", "debug"],
|
|
162
|
+
)
|
|
163
|
+
parser.add_argument(
|
|
164
|
+
"--metadata",
|
|
165
|
+
metavar="key=value",
|
|
166
|
+
nargs="+",
|
|
167
|
+
type=lambda x: tuple(x.split("=")),
|
|
168
|
+
help="Custom client metadata",
|
|
169
|
+
)
|
|
170
|
+
parser.add_argument(
|
|
171
|
+
"-i",
|
|
172
|
+
"--input",
|
|
173
|
+
type=check_file_exists,
|
|
174
|
+
help="Multichannel input audio file.",
|
|
175
|
+
required=True,
|
|
176
|
+
)
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
"-o",
|
|
179
|
+
"--output",
|
|
180
|
+
type=Path,
|
|
181
|
+
default=None,
|
|
182
|
+
help="Output result to a file. If omitted, output to stdout.",
|
|
183
|
+
)
|
|
184
|
+
parser.add_argument("--use_ssl", action="store_true", help="Use SSL connection")
|
|
185
|
+
parser.add_argument("--start", type=float, help="Audio start time")
|
|
186
|
+
parser.add_argument("--end", type=float, help="Audio end time")
|
|
187
|
+
parser.add_argument("--use_raw_audio", action="store_true", help="Send a raw audio")
|
|
188
|
+
|
|
189
|
+
args = parser.parse_args()
|
|
190
|
+
|
|
191
|
+
output_file: Optional[Path] = Optional
|
|
192
|
+
output_file = args.output
|
|
193
|
+
|
|
194
|
+
if args.start is not None and args.start < 0:
|
|
195
|
+
raise ValueError("Parameter 'start' must be a non-negative float.")
|
|
196
|
+
|
|
197
|
+
if args.end is not None and args.end <= 0:
|
|
198
|
+
raise ValueError("Parameter 'end' must be a positive float.")
|
|
199
|
+
|
|
200
|
+
logging.basicConfig(
|
|
201
|
+
level=args.log_level.upper(),
|
|
202
|
+
format="[%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s",
|
|
203
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
logging.info(f"Connecting to {args.host}")
|
|
208
|
+
channel = (
|
|
209
|
+
grpc.secure_channel(target=args.host, credentials=grpc.ssl_channel_credentials())
|
|
210
|
+
if args.use_ssl
|
|
211
|
+
else grpc.insecure_channel(target=args.host)
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
start_time = datetime.now()
|
|
215
|
+
|
|
216
|
+
analyze(
|
|
217
|
+
channel=channel,
|
|
218
|
+
input_file=args.input,
|
|
219
|
+
output_file=output_file,
|
|
220
|
+
start=args.start,
|
|
221
|
+
end=args.end,
|
|
222
|
+
metadata=args.metadata,
|
|
223
|
+
use_raw_audio=args.use_raw_audio,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
logging.debug(f"Elapsed time {(datetime.now() - start_time)}")
|
|
227
|
+
|
|
228
|
+
except grpc.RpcError as e:
|
|
229
|
+
handle_grpc_error(e)
|
|
230
|
+
exit(1)
|
|
231
|
+
except Exception:
|
|
232
|
+
logging.exception("Unknown error")
|
|
233
|
+
exit(1)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+

|
|
2
|
+
|
|
3
|
+
# Phonexia time analysis client
|
|
4
|
+
|
|
5
|
+
This module contains client for communication with [time analysis microservice](https://hub.docker.com/repository/docker/phonexia/time-analysis/general) developed by [Phonexia](https://phonexia.com).
|
|
6
|
+
|
|
7
|
+
To use this client you will first need a running instance of any *Phonexia time analysis microservice*. 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 time analysis technology [here](https://docs.phonexia.com/products/speech-platform-4/technologies/time-analysis/).
|
|
10
|
+
|
|
11
|
+
On [this page](https://docs.phonexia.com/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/time_analysis/v1/time_analysis.proto), you will find a *gRPC API* reference for *time analysis microservice*.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "phonexia-time-analysis-client"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Client for communication with Phonexia time analysis microservice."
|
|
5
|
+
readme = "pypi-README.md"
|
|
6
|
+
keywords = ["grpc", "voice", "analysis"]
|
|
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
|
+
time_analysis_client = 'phonexia_time_analysis_client:main'
|
|
16
|
+
|
|
17
|
+
[tool.poetry.dependencies]
|
|
18
|
+
python = ">=3.9,<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 = "^6.0.0"
|
|
30
|
+
pytest-env = "^1.0.0"
|
|
31
|
+
pytest-random-order = "^1.1.0"
|
|
32
|
+
black = "^24.0.0"
|
|
33
|
+
ruff = "^0.7.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 = ['py39']
|
|
50
|
+
preview = true
|
|
51
|
+
|
|
52
|
+
[tool.ruff]
|
|
53
|
+
target-version = "py39"
|
|
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
|
+
# Use X | Y for type annotations instead of Optional
|
|
96
|
+
"UP007",
|
|
97
|
+
# Import Iterator from 'collections.abc' instead of typing
|
|
98
|
+
"UP035",
|
|
99
|
+
]
|