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
|
+

|
|
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
|
+

|
|
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
|
+
]
|