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