phonexia-replay-attack-detection-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,34 @@
1
+ Metadata-Version: 2.3
2
+ Name: phonexia-replay-attack-detection-client
3
+ Version: 1.0.0
4
+ Summary: Client for communication with Phonexia Replay Attack Detection microservice.
5
+ Keywords: grpc,voice
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
+ # Phonexia Replay Attack Detection client
27
+
28
+ This module contains client for communication with [Replay attack detection microservice](https://hub.docker.com/repository/docker/phonexia/replay-attack-detection/general) developed by [Phonexia](https://phonexia.com).
29
+
30
+ To use this client you will first need a running instance of any *Phonexia Replay Attack Detection microservice*. If you don't yet have any running instance, don't hesitate to [contact our sales department](mailto:info@phonexia.com).
31
+
32
+ You can learn more about the speaker identification technology [here](https://docs.phonexia.com/products/speech-platform-4/technologies/speaker-identification/).
33
+
34
+ On [this page](https://docs.phonexia.com/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/replay_attack_detection/experimental/replay_attack_detection.proto), you will find a *gRPC API* reference for *Replay Attack Detection microservice*.
@@ -0,0 +1,222 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import csv
5
+ import json
6
+ import logging
7
+ import os
8
+ import sys
9
+ from collections.abc import Iterator
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ import google.protobuf.duration_pb2
14
+ import grpc
15
+ import numpy as np
16
+ import soundfile
17
+ from phonexia.grpc.common.core_pb2 import Audio, RawAudioConfig, TimeRange
18
+ from phonexia.grpc.technologies.replay_attack_detection.experimental.replay_attack_detection_pb2 import (
19
+ DetectRequest,
20
+ DetectResponse,
21
+ )
22
+ from phonexia.grpc.technologies.replay_attack_detection.experimental.replay_attack_detection_pb2_grpc import (
23
+ ReplayAttackDetectionStub,
24
+ )
25
+
26
+
27
+ def time_to_duration(time: float) -> Optional[google.protobuf.duration_pb2.Duration]:
28
+ if time is None:
29
+ return None
30
+ duration = google.protobuf.duration_pb2.Duration()
31
+ duration.seconds = int(time)
32
+ duration.nanos = int((time - duration.seconds) * 1e9)
33
+ return duration
34
+
35
+
36
+ def make_request(
37
+ file: Path,
38
+ start: Optional[float],
39
+ end: Optional[float],
40
+ use_raw_audio: bool,
41
+ ) -> Iterator[DetectRequest]:
42
+ time_range: Optional[TimeRange] = TimeRange(
43
+ start=time_to_duration(start), end=time_to_duration(end)
44
+ )
45
+ chunk_size = 1024 * 100
46
+ if use_raw_audio:
47
+ with soundfile.SoundFile(file) as r:
48
+ raw_audio_config: Optional[RawAudioConfig] = RawAudioConfig(
49
+ channels=r.channels,
50
+ sample_rate_hertz=r.samplerate,
51
+ encoding=RawAudioConfig.AudioEncoding.PCM16,
52
+ )
53
+ for data in r.blocks(blocksize=r.samplerate, dtype="float32"):
54
+ int16_info = np.iinfo(np.int16)
55
+ data_scaled = np.clip(
56
+ data * (int16_info.max + 1), int16_info.min, int16_info.max
57
+ ).astype("int16")
58
+ yield DetectRequest(
59
+ audio=Audio(
60
+ content=data_scaled.flatten().tobytes(),
61
+ raw_audio_config=raw_audio_config,
62
+ time_range=time_range,
63
+ )
64
+ )
65
+ time_range = None
66
+ raw_audio_config = None
67
+
68
+ else:
69
+ with open(file, mode="rb") as fd:
70
+ while chunk := fd.read(chunk_size):
71
+ yield DetectRequest(audio=Audio(content=chunk, time_range=time_range))
72
+ time_range = None
73
+
74
+
75
+ def write_result(audio_path: str, response: DetectResponse, output_file: Optional[str] = None):
76
+ with sys.stdout if output_file is None else open(output_file, "w") as f:
77
+ logging.info(f"{audio_path!s} -> {output_file!s}")
78
+ output = {
79
+ "audio": str(audio_path),
80
+ "total_billed_time": str(response.processed_audio_length.ToTimedelta()),
81
+ }
82
+ if response.HasField("result"):
83
+ output["score"] = response.result.score
84
+ json.dump(output, f, indent=2)
85
+
86
+
87
+ def detect(
88
+ channel: grpc.Channel,
89
+ file: Path,
90
+ output_file: Optional[Path],
91
+ start: Optional[float],
92
+ end: Optional[float],
93
+ metadata: Optional[list[Any]],
94
+ use_raw_audio: bool,
95
+ ):
96
+ logging.info(f"Detecting replay attacks in {file}")
97
+ stub = ReplayAttackDetectionStub(channel)
98
+ response = stub.Detect(make_request(file, start, end, use_raw_audio), metadata=metadata)
99
+ write_result(file, response, output_file)
100
+
101
+
102
+ # Main program
103
+ def check_file_exists(path: str) -> Path:
104
+ if not os.path.isfile(path):
105
+ raise argparse.ArgumentTypeError(f"File '{path}' does not exist.")
106
+ return Path(path)
107
+
108
+
109
+ def parse_list(path: Path) -> list[list[str]]:
110
+ with open(path, newline="") as csvfile:
111
+ reader = csv.reader(csvfile, delimiter=" ", skipinitialspace=True)
112
+ rows = list(reader)
113
+
114
+ for row in rows:
115
+ num_cols = len(row)
116
+ if num_cols < 1 or num_cols > 2:
117
+ raise ValueError(
118
+ f"File '{path}' must contain one or two columns. Problematic row: '{row}'"
119
+ )
120
+ if num_cols == 1:
121
+ row.append(str(Path(row[0]).with_suffix(".json")))
122
+
123
+ return rows
124
+
125
+
126
+ def main(): # noqa: C901
127
+ parser = argparse.ArgumentParser(description="Replay Attack Detection Client")
128
+ parser.add_argument(
129
+ "-H",
130
+ "--host",
131
+ type=str,
132
+ default="localhost:8080",
133
+ help="Server address, default: localhost:8080",
134
+ )
135
+ parser.add_argument(
136
+ "-l",
137
+ "--log_level",
138
+ type=str,
139
+ default="error",
140
+ choices=["critical", "error", "warning", "info", "debug"],
141
+ )
142
+ parser.add_argument(
143
+ "--metadata",
144
+ metavar="key=value",
145
+ nargs="+",
146
+ type=lambda x: tuple(x.split("=")),
147
+ help="Custom client metadata",
148
+ )
149
+ parser.add_argument("-i", "--input", type=check_file_exists, help="Input audio file")
150
+ parser.add_argument("-o", "--output", type=Path, help="Output file path")
151
+ parser.add_argument(
152
+ "-L",
153
+ "--list",
154
+ type=check_file_exists,
155
+ help="List of files and optional output locations (csv file, delimiter is space character ' ')",
156
+ )
157
+ parser.add_argument("--use_ssl", action="store_true", help="Use SSL connection")
158
+ parser.add_argument("--start", type=float, default=None, help="Start time (seconds)")
159
+ parser.add_argument("--end", type=float, default=None, help="End time (seconds)")
160
+ parser.add_argument("--use_raw_audio", action="store_true", help="Send raw audio chunks")
161
+
162
+ args = parser.parse_args()
163
+
164
+ if not (args.input or args.list):
165
+ raise ValueError("Either 'input' or 'list' parameter must be set.")
166
+
167
+ if args.start is not None and args.start < 0:
168
+ raise ValueError("Parameter 'start' must be a non-negative float.")
169
+
170
+ if args.end is not None and args.end <= 0:
171
+ raise ValueError("Parameter 'end' must be a positive float.")
172
+
173
+ if args.start is not None and args.end is not None and args.start >= args.end:
174
+ raise ValueError("Parameter 'end' must be larger than 'start'.")
175
+
176
+ logging.basicConfig(
177
+ level=args.log_level.upper(),
178
+ format="[%(asctime)s.%(msecs)03d] [%(levelname)s] %(message)s",
179
+ datefmt="%Y-%m-%d %H:%M:%S",
180
+ )
181
+
182
+ try:
183
+ logging.info(f"Connecting to {args.host}")
184
+ channel = (
185
+ grpc.secure_channel(target=args.host, credentials=grpc.ssl_channel_credentials())
186
+ if args.use_ssl
187
+ else grpc.insecure_channel(target=args.host)
188
+ )
189
+
190
+ def execute(input_file: Path, output_file: Path) -> None:
191
+ detect(
192
+ channel,
193
+ input_file,
194
+ output_file,
195
+ args.start,
196
+ args.end,
197
+ args.metadata,
198
+ args.use_raw_audio,
199
+ )
200
+
201
+ if args.input is not None:
202
+ execute(args.input, args.output)
203
+
204
+ elif args.list is not None:
205
+ for input_file, output_file in parse_list(args.list):
206
+ execute(Path(input_file), Path(output_file))
207
+
208
+ except grpc.RpcError:
209
+ logging.exception("RPC failed")
210
+ exit(1)
211
+ except ValueError as e:
212
+ logging.exception(e) # noqa: TRY401
213
+ exit(1)
214
+ except Exception:
215
+ logging.exception("Unknown error")
216
+ exit(1)
217
+ finally:
218
+ channel.close()
219
+
220
+
221
+ if __name__ == "__main__":
222
+ main()
@@ -0,0 +1,10 @@
1
+ ![](https://www.phonexia.com/wp-content/uploads/phonexia-logo-transparent-500px.png)
2
+ # Phonexia Replay Attack Detection client
3
+
4
+ This module contains client for communication with [Replay attack detection microservice](https://hub.docker.com/repository/docker/phonexia/replay-attack-detection/general) developed by [Phonexia](https://phonexia.com).
5
+
6
+ To use this client you will first need a running instance of any *Phonexia Replay Attack Detection microservice*. If you don't yet have any running instance, don't hesitate to [contact our sales department](mailto:info@phonexia.com).
7
+
8
+ You can learn more about the speaker identification technology [here](https://docs.phonexia.com/products/speech-platform-4/technologies/speaker-identification/).
9
+
10
+ On [this page](https://docs.phonexia.com/products/speech-platform-4/grpc/api/phonexia/grpc/technologies/replay_attack_detection/experimental/replay_attack_detection.proto), you will find a *gRPC API* reference for *Replay Attack Detection microservice*.
@@ -0,0 +1,99 @@
1
+ [tool.poetry]
2
+ name = "phonexia-replay-attack-detection-client"
3
+ version = "1.0.0"
4
+ description = "Client for communication with Phonexia Replay Attack Detection microservice."
5
+ readme = "pypi-README.md"
6
+ keywords = ["grpc", "voice"]
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
+ replay_attack_detection_client = 'phonexia_replay_attack_detection_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
+ ]