Compare commits

...

20 Commits

Author SHA1 Message Date
Dmytro Bogovych 85181b7d1e - improved setup script 2023-09-21 07:57:10 +03:00
Dmytro Bogovych 598456b830 - no more scheduled runs in offline mode - only interactive calls 2023-09-21 05:18:07 +01:00
Dmytro Bogovych cd9c250c95 - first working prototype 2023-09-20 18:16:37 +01:00
Dmytro Bogovych d0032364ee - first prototype of API enabled agent_gsm 2023-09-19 10:59:40 +01:00
Dmytro Bogovych 8ed8e5f255 - initial implementation of standalone hotspot 2023-09-19 09:01:45 +03:00
Dmytro Bogovych e06636132b - fix None reference 2023-09-17 18:23:38 +01:00
Dmytro Bogovych 3efc3d076c - fix the utils_sevana.py 2023-09-17 19:40:06 +03:00
Dmytro Bogovych e512f7a643 - attempt to make MOSes better 2023-09-17 19:37:15 +03:00
Dmytro Bogovych 4696b0e690 - remove non-used binaries 2023-09-17 19:37:03 +03:00
Dmytro Bogovych 0bf8134feb - use native speech detector 2023-09-14 15:44:55 +03:00
Dmytro Bogovych 48743574ad - non-used files removed 2023-09-14 10:20:02 +03:00
Dmytro Bogovych 19c9881784 Merge branch 'master' of https://git.sevana.biz/public/agent_gsm 2023-09-14 08:17:09 +01:00
Dmytro Bogovych 5e2390d9a5 - refresh speech detector 2023-09-14 08:16:49 +01:00
Dmytro Bogovych cc5cec6cd2 - fix global variable access 2023-09-11 12:21:33 +03:00
Dmytro Bogovych 7d21de2dd0 - fix error on name 2023-09-11 12:19:32 +03:00
Dmytro Bogovych ef9ac651f9 - fix error on None value 2023-09-11 12:18:08 +03:00
Dmytro Bogovych ed3b91d8c1 - fix remaining audio results uploading 2023-09-11 12:15:11 +03:00
Dmytro Bogovych c186badb43 - fix false alarm when checking recorded audio is good 2023-09-11 11:46:35 +03:00
Dmytro Bogovych 59d38975e3 - avoid 2nd copy of agent 2023-09-08 15:25:05 +01:00
Dmytro Bogovych ace93a7c51 - fix conf template 2023-09-08 13:46:53 +01:00
32 changed files with 724 additions and 344 deletions

View File

@ -1,7 +0,0 @@
#!/bin/bash
# Oneliner to find script's directory. Please note - last path component should NOT be symlink.
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
/usr/bin/python3 ${SCRIPT_DIR}/src/utils_network_impairment.py --start

View File

@ -1,6 +0,0 @@
#!/bin/bash
# Oneliner to find script's directory. Please note - last path component should NOT be symlink.
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
/usr/bin/python3 ${SCRIPT_DIR}/src/utils_network_impairment.py --stop

21
bin/aqua.cfg Normal file
View File

@ -0,0 +1,21 @@
AQuA:
mode: files
# src: file test_audio/jane_8k.wav
# tstf: test_audio/jane_8k_40.wav
avlp: off
smtnrm: off
decor: off
mprio: off
acr: auto
npnt: auto
voip: on
enorm: rms
g711: off
spfrcor: on
grad: off
tmc: on
miter: 1
ratem: "%%m"
trim: "r 15"
output: json

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1 +0,0 @@
This pjsua requires 10.14 at least!

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,226 +1,232 @@
BOF Common Common:
IntervalLength = 0.68 IntervalLength: 0.68
IsUseUncertain = false IsUseUncertain: no
IsUseMixMode = true IsUseMixMode: yes
IsUseDistance = false IsUseDistance: no
AllWeight = 1.0 AllWeight: 1.0
SilWeight = 1 SilWeight: 1.0
VoiWeight = 1 VoiWeight: 1.0
AllCoefficient = 1.0 AllCoefficient: 1.0
SilCoefficient = 1.0 SilCoefficient: 1.0
VoiCoefficient = 1.0 VoiCoefficient: 1.0
SilThreshold = -37.50 SilThreshold: -37.50
IsOnePointSil = false IsOnePointSil: no
IsNormResult = true IsNormResult: yes
IsMapScore = true IsMapScore: yes
EOF Common NormalizeByRms: yes
BOF Detector SilenceEraser:
Name = SNR Enabled: no
DetectorType = SNR Options:
IntThresh = 0.10
FrameThresh = 14
DetThresh = 0.10
PVQA-Flag = true
PVQA-Weight = 1.0
DetMode = Both
EOF Detector
BOF Detector Detector:
Name = DeadAir-00 - Name: SNR
DetectorType = DeadAir DetectorType: SNR
IntThresh = 0.60 IntThresh: 0.10
DetThresh = 0.60 FrameThresh: 14
PVQA-Flag = true DetThresh: 0.10
PVQA-Weight = 1.0 PVQA-Flag: yes
DetMode = Both PVQA-Weight: 1.0
EOF Detector DetMode: both
BOF Detector - Name: Noise
Name = DeadAir-01 DetectorType: Noise
DetectorType = DeadAir IntThresh: 0.99
IntThresh = 0.5 DetThresh: 0.99
DetThresh = 0.5 # This is still experimental detector so its values are not participating in MOS calculation
PVQA-Flag = true PVQA-Flag: false
PVQA-Weight = 1.0 PVQA-Weight: 1.0
DetMode = Both DetMode: both
EOF Detector
BOF Detector - Name: DTMF
Name = Click DetectorType: DTMF
DetectorType = Clicking IntThresh: 0.99
IntThresh = 0.10 DetThresh: 0.99
DetThresh = 0.10
PVQA-Flag = true
PVQA-Weight = 1.0
DetMode = Both
EOF Detector
BOF Detector # There is no sense to use detected DTMF signal in MOS calculation in the current config
Name = VAD-Clipping PVQA-Flag: no
DetectorType = VADClipping PVQA-Weight: 0.0
IntThresh = 0.0 DetMode: both
FrameThresh = 0.0
DetThresh = 0.0
PVQA-Flag = true
PVQA-Weight = 1.0
DetMode = Both
EOF Detector
BOF Detector - Name: DeadAir-00
Name = Amplitude-Clipping DetectorType: DeadAir
DetectorType = AmpClipping IntThresh: 0.60
IntThresh = 0.00 DetThresh: 0.60
FrameThresh = 1.00 PVQA-Flag: true
DetThresh = 0.00 PVQA-Weight: 1.0
PVQA-Flag = true DetMode: both
PVQA-Weight = 1.00
DetMode = Both
EOF Detector
BOF Detector - Name: DeadAir-01
Name = Dynamic-Clipping DetectorType: DeadAir
DetectorType = AmpClipping IntThresh: 0.5
IntThresh = 0.05 DetThresh: 0.5
FrameThresh = 1.50 PVQA-Flag: yes
DetThresh = 0 PVQA-Weight: 1.0
PVQA-Flag = true DetMode: both
PVQA-Weight = 0.0 Override:
DetMode = Voice MinLevelThreshold: 0
EOF Detector
BOF Base EchoMono - Name: Click
SamplesType = UnKnownCodec DetectorType: Clicking
StepLengthSec = 0.5 IntThresh: 0.10
MinDelayMs = 50 DetThresh: 0.10
MaxLengthMs = 2800 PVQA-Flag: true
WindowFunckID = 0 PVQA-Weight: 1.0
SpanLengthMs = 50 DetMode: both
EOF Base EchoMono
BOF Detector - Name: VAD-Clipping
Name = ECHO DetectorType: VADClipping
DetectorType = EchoMono IntThresh: 0.0
IntThresh = 0.00 FrameThresh: 0.0
FrameThresh = -40.0 DetThresh: 0.0
DetThresh = 0.00 PVQA-Flag: true
PVQA-Flag = true PVQA-Weight: 1.0
PVQA-Weight = 1.0 DetMode: both
DetMode = Voice
STAT-Flag = true
SpanLengthMs = 50
EOF Detector
BOF Detector - Name: AmpClipping
Name = Silent-Call-Detection DetectorType: AmpClipping
DetectorType = DeadAir IntThresh: 0.00
IntThresh = 0.99 FrameThresh: 1.00
DetThresh = 0.99 DetThresh: 0.00
PVQA-Flag = false PVQA-Flag: true
PVQA-Weight = 1.0 PVQA-Weight: 1.00
EOF Detector DetMode: both
BOF Base SNR - Name: DynClipping
MinPowerThresh = 1.0000 DetectorType: AmpClipping
LogEnergyCoefficient = 10.0000 IntThresh: 0.05
MinSignalLevel = 40.0000 FrameThresh: 1.50
MinSNRDelta = 0.0001 DetThresh: 0
MinEnergyDisp = 3.0000 PVQA-Flag: true
MinEnergyDelta = 1.0000 PVQA-Weight: 0.0
SamplesType = UnKnownCodec DetMode: voice
EOF Base SNR Override:
FlyAddingCoefficient: 0.1000
SamplesType: UnKnownCodec
IsUseDynamicClipping: yes
BOF Base AmpClipping - Name: Echo
FlyAddingCoefficient = 0.1000 DetectorType: EchoMono
IsUseDynamicClipping = false IntThresh: 0.00
SamplesType = UnKnownCodec FrameThresh: -40.0
EOF Base AmpClipping DetThresh: 0.00
PVQA-Flag: true
PVQA-Weight: 1.0
DetMode: voice
STAT-Flag: true
SpanLengthMs: 50
BOF Base Clicking - Name: SilentCall
SamplesType = UnKnownCodec DetectorType: DeadAir
EOF Base Clicking IntThresh: 0.99
DetThresh: 0.99
PVQA-Flag: false
PVQA-Weight: 1.0
Override:
MinLevelThreshold: 0
IsUseRMSPower: yes
MinRMSThreshold: -70
BOF Base DeadAir
StuckDeltaThreshold = 6
MinNonStuckTime = 80
MinStuckTime = 80
MinStartNonStuckTime = 1920
MinLevelThreshold = 256
SamplesType = UnKnownCodec
EOF Base DeadAir
BOF Base VADClipping
SamplesType = UnKnownCodec
EOF Base VADClipping
BOF DeadAir-01 Base EchoMono:
MinLevelThreshold = 0 SamplesType: UnKnownCodec
EOF DeadAir-01 StepLengthSec: 0.5
MinDelayMs: 50
MaxLengthMs: 2800
WindowFunckID: 0
SpanLengthMs: 50
BOF Silent-Call-Detection Base SNR:
MinLevelThreshold = 0 MinPowerThresh: 1.0000
IsUseRMSPower = true LogEnergyCoefficient: 10.0000
MinRMSThreshold = -70 MinSignalLevel: 40.0000
EOF Silent-Call-Detection MinSNRDelta: 0.0001
MinEnergyDisp: 3.0000
MinEnergyDelta: 1.0000
SamplesType: UnKnownCodec
BOF Dynamic-Clipping Base DTMF:
FlyAddingCoefficient = 0.1000 SamplesType: UnKnownCodec
SamplesType = UnKnownCodec
IsUseDynamicClipping = true
EOF Dynamic-Clipping
BOF Correction Base AmpClipping:
IntStart = 5.0 FlyAddingCoefficient: 0.1000
IntEnd = 4.2 IsUseDynamicClipping: no
Mult = 1.0 SamplesType: UnKnownCodec
#Shift = -1.7
Shift = 0
EOF Correction
BOF Correction Base Clicking:
IntStart = 4.2 SamplesType: UnKnownCodec
IntEnd = 3.5
Mult = 1.0
#Shift = -0.85
Shift = 0
EOF Correction
BOF SR Correction Base DeadAir:
SampleRate = 11000.0 StuckDeltaThreshold: 6
Shift = 0.05 MinNonStuckTime: 80
EOF SR Correction MinStuckTime: 80
MinStartNonStuckTime: 1920
MinLevelThreshold: 256
SamplesType: UnKnownCodec
BOF SR Correction Base VADClipping:
SampleRate = 16000.0 SamplesType: UnKnownCodec
Shift = 0.1
EOF SR Correction
BOF SR Correction Base Noise:
SampleRate = 22000.0 Interval: 0.1 # Seconds
Shift = 0.2 DetectorType: RMS # This can be FFT as well
EOF SR Correction NoiseThreshold: 20
SignalThreshold: 80
Normalize: no
RemoveBias: no
ResultDb: yes
WindowType: Hann
WindowWidth: 3
BOF SR Correction # Moved to Override: sections
SampleRate = 32000.0 # DeadAir-01:
Shift = 0.3 # MinLevelThreshold: 0
EOF SR Correction
BOF SR Correction # SilentCall:
SampleRate = 48000.0 # MinLevelThreshold: 0
Shift = 0.45 # IsUseRMSPower: yes
EOF SR Correction # MinRMSThreshold: -70
BOF SR Correction # Dynamic-Clipping:
SampleRate = 96000.0 # FlyAddingCoefficient: 0.1000
Shift = 0.5 # SamplesType: UnKnownCodec
EOF SR Correction # IsUseDynamicClipping: yes
BOF SR Correction Correction:
SampleRate = 192000.0 - IntStart: 5.0
Shift = 0.6 IntEnd: 4.2
EOF SR Correction Mult: 1.0
Shift: 0
BOF Scores Map - IntStart: 4.2
ScoresLine = 4;3.027000;2.935000;2.905000;2.818000;2.590000;2.432000;2.310000;1.665000;1.000000; IntEnd: 3.5
EOF Scores Map Mult: 1.0
Shift: 0
SR Correction:
- SampleRate: 11000.0
Shift: 0.05
- SampleRate: 16000.0
Shift: 0.1
- SampleRate: 22000.0
Shift: 0.2
- SampleRate: 32000.0
Shift: 0.3
- SampleRate: 48000.0
Shift: 0.45
- SampleRate: 96000.0
Shift: 0.5
- SampleRate: 192000.0
Shift: 0.6
Scores Map:
ScoresLine: 4;3.027000;2.935000;2.905000;2.818000;2.590000;2.432000;2.310000;1.665000;1.000000;

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -25,8 +25,8 @@ rabbitmq:
cache_dir: cache cache_dir: cache
audio: audio:
# Silence prefix & suffix lengths (in seconds) # Silence prefix & suffix lengths (in seconds)
silence_prefix: 30 silence_prefix: 10
silence_suffix: 30 silence_suffix: 10
bluetooth_mac: "MAC_ADDRESS" bluetooth_mac: "MAC_ADDRESS"

View File

@ -0,0 +1,23 @@
# Defaults for hostapd initscript
#
# WARNING: The DAEMON_CONF setting has been deprecated and will be removed
# in future package releases.
#
# See /usr/share/doc/hostapd/README.Debian for information about alternative
# methods of managing hostapd.
#
# Uncomment and set DAEMON_CONF to the absolute path of a hostapd configuration
# file and hostapd will be started during system boot. An example configuration
# file can be found at /usr/share/doc/hostapd/examples/hostapd.conf.gz
#
DAEMON_CONF="/etc/hostapd/hostapd.conf"
# Additional daemon options to be appended to hostapd command:-
# -d show more debug messages (-dd for even more)
# -K include key data in debug messages
# -t include timestamps in some debug messages
#
# Note that -B (daemon mode) and -P (pidfile) options are automatically
# configured by the init.d script and must not be added to DAEMON_OPTS.
#
#DAEMON_OPTS=""

64
config/ap/etc/dhcpcd.conf Normal file
View File

@ -0,0 +1,64 @@
# A sample configuration for dhcpcd.
# See dhcpcd.conf(5) for details.
# Allow users of this group to interact with dhcpcd via the control socket.
#controlgroup wheel
# Inform the DHCP server of our hostname for DDNS.
hostname
# Use the hardware address of the interface for the Client ID.
clientid
# or
# Use the same DUID + IAID as set in DHCPv6 for DHCPv4 ClientID as per RFC4361.
# Some non-RFC compliant DHCP servers do not reply with this set.
# In this case, comment out duid and enable clientid above.
#duid
# Persist interface configuration when dhcpcd exits.
persistent
# Rapid commit support.
# Safe to enable by default because it requires the equivalent option set
# on the server to actually work.
option rapid_commit
# A list of options to request from the DHCP server.
option domain_name_servers, domain_name, domain_search, host_name
option classless_static_routes
# Respect the network MTU. This is applied to DHCP routes.
option interface_mtu
# Most distributions have NTP support.
#option ntp_servers
# A ServerID is required by RFC2131.
require dhcp_server_identifier
# Generate SLAAC address using the Hardware Address of the interface
#slaac hwaddr
# OR generate Stable Private IPv6 Addresses based from the DUID
slaac private
# Example static IP configuration:
#interface eth0
#static ip_address=192.168.0.10/24
#static ip6_address=fd51:42f8:caae:d92e::ff/64
#static routers=192.168.0.1
#static domain_name_servers=192.168.0.1 8.8.8.8 fd51:42f8:caae:d92e::1
# It is possible to fall back to a static IP if DHCP fails:
# define static profile
#profile static_eth0
#static ip_address=192.168.1.23/24
#static routers=192.168.1.1
#static domain_name_servers=192.168.1.1
# fallback to static profile on eth0
#interface eth0
#fallback static_eth0
#
#
interface wlan0
static ip_address=192.168.45.1/24
static routers=192.168.45.1

View File

@ -0,0 +1,5 @@
interface=wlan0 # Listening interface
dhcp-range=192.168.45.10,192.168.45.20,255.255.255.0,24h # Pool of IP addresses for wireless clients
domain=wlan # Domain
address=/gw.wlan/192.168.45.1 # Alias for router

View File

@ -0,0 +1,8 @@
country_code=GB
interface=wlan0
ssid=AGENT_GSM
hw_mode=g
channel=7
macaddr_acl=0
auth_algs=1
ignore_broadcast_ssid=0

View File

@ -0,0 +1,31 @@
[Unit]
Description=dnsmasq - A lightweight DHCP and caching DNS server
Requires=network.target
Wants=network-online.target
Before=nss-lookup.target
After=network-online.target
[Service]
Type=forking
PIDFile=/run/dnsmasq/dnsmasq.pid
# Test the config file and refuse starting if it is not valid.
ExecStartPre=/usr/sbin/dnsmasq --test
# We run dnsmasq via the /etc/init.d/dnsmasq script which acts as a
# wrapper picking up extra configuration files and then execs dnsmasq
# itself, when called with the "systemd-exec" function.
ExecStart=/etc/init.d/dnsmasq systemd-exec
# The systemd-*-resolvconf functions configure (and deconfigure)
# resolvconf to work with the dnsmasq DNS server. They're called like
# this to get correct error handling (ie don't start-resolvconf if the
# dnsmasq daemon fails to start.
ExecStartPost=/etc/init.d/dnsmasq systemd-start-resolvconf
ExecStop=/etc/init.d/dnsmasq systemd-stop-resolvconf
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target

View File

@ -3,6 +3,12 @@
# Oneliner to find script's directory. Please note - last path component should NOT be symlink. # Oneliner to find script's directory. Please note - last path component should NOT be symlink.
SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )"; SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )";
for pid in $(pidof -x run_agent.sh); do
if [ $pid != $$ ]; then
echo "[$(date)] : run_agent.sh : Process is already running with PID $pid. Exiting."
exit 1
fi
done
while : while :
do do

View File

@ -10,8 +10,8 @@ INSTALL_DIR=agent_gsm
GIT_SOURCE=https://git.sevana.biz/public/agent_gsm GIT_SOURCE=https://git.sevana.biz/public/agent_gsm
# Install prerequisites # Install prerequisites
sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy dnsmasq hostapd screen
sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect pydub requests rabbitpy pydub sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect requests rabbitpy bottle
if [ -f "$INSTALL_DIR" ]; then if [ -f "$INSTALL_DIR" ]; then
rm -rf "$INSTALL_DIR" rm -rf "$INSTALL_DIR"
@ -45,7 +45,7 @@ cp config/agent.in.yaml config/agent.yaml
mkdir -p ~/.config/mc mkdir -p ~/.config/mc
cp config/mc/ini ~/.config/mc cp config/mc/ini ~/.config/mc
# Replace the values # Replace the values - finish preparing the agent configuration file
if [[ $BACKEND_URL != "" ]]; then if [[ $BACKEND_URL != "" ]]; then
sed -i "s|BACKEND|$BACKEND|" config/agent.yaml sed -i "s|BACKEND|$BACKEND|" config/agent.yaml
fi fi
@ -56,11 +56,45 @@ fi
sed -i "s|TASK_NAME|$TASK_NAME|" config/agent.yaml sed -i "s|TASK_NAME|$TASK_NAME|" config/agent.yaml
# Update systemD unit file
cp config/systemd/agent_gsm.in.service config/systemd/agent_gsm.service
ABSOLUTE_INSTALL_DIR=`realpath .` ABSOLUTE_INSTALL_DIR=`realpath .`
sed -i "s|ABSOLUTE_INSTALL_DIR|$ABSOLUTE_INSTALL_DIR|" config/systemd/agent_gsm.service # Update systemD unit file
# cp config/systemd/agent_gsm.in.service config/systemd/agent_gsm.service
# sed -i "s|ABSOLUTE_INSTALL_DIR|$ABSOLUTE_INSTALL_DIR|" config/systemd/agent_gsm.service
install_ap() {
# $1 is AP name
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/dhcpcd.conf /etc
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/dnsmasq.conf /etc
sudo mkdir -p /etc/hostapd
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/hostapd.conf /etc/hostapd
sudo sed -i "s|AGENT_GSM|$1|" /etc/hostapt/hostapd.conf
sudo cp $ABSOLUTE_INSTALL_DIR/config/ap/etc/default/hostapt /etd/default
sudo systemctl enable dnsmasq
sudo systemctl enable hostapd
sudo systemctl start dnsmasq
sudo systemctl start hostapd
}
function enable_autologin() {
sudo systemctl --quiet set-default multi-user.target
sudo cat > /etc/systemd/system/getty@tty1.service.d/autologin.conf << EOF
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin $USER --noclear %I \$TERM
EOF
}
# ToDo:
# - allow autologin in console mode for 'pi' user
enable_autologin
# - add $ABSOLUTE_INSTALL_DIR/run_agent_screen.sh to ~/.bashrc
echo "$ABSOLUTE_INSTALL_DIR/run_agent_screen.sh" >> ~/.bashrc
# - install wifi AP with name $PHONE_NAME
install_ap $PHONE_NAME
echo "Now the remaining prerequisites will be installed and system will reboot." echo "Now the remaining prerequisites will be installed and system will reboot."
echo "You can connect the phone via Bluetooth after the reboot." echo "You can connect the phone via Bluetooth after the reboot."

View File

@ -36,8 +36,8 @@ class AgentConfig:
# Should the first task run immediately ? # Should the first task run immediately ?
ForceRun = False ForceRun = False
# Use silence eraser or not (speech detector is used in this case) # Use external speech detector if needed
UseSilenceEraser = True UseSpeechDetector = False
# Path to log file # Path to log file
LogPath : Path = None LogPath : Path = None
@ -90,7 +90,7 @@ class AgentConfig:
if 'speech_detector' in config: if 'speech_detector' in config:
if config['speech_detector']: if config['speech_detector']:
self.UseSilenceEraser = False self.UseSpeechDetector = True
if 'audio' in config: if 'audio' in config:
audio = config['audio'] audio = config['audio']

View File

@ -24,6 +24,7 @@ from bt_signal import SignalBoundaries
from bt_call_controller import INTERRUPT_SIGNAL from bt_call_controller import INTERRUPT_SIGNAL
import bt_call_controller import bt_call_controller
import agent_point
CONFIG = AgentConfig() CONFIG = AgentConfig()
@ -68,17 +69,23 @@ def detect_degraded_signal(file_test: Path, file_reference: Path) -> SignalBound
# Seems some problem with recording, return zero boundaries # Seems some problem with recording, return zero boundaries
return SignalBoundaries() return SignalBoundaries()
r = bt_signal.find_reference_signal(file_test) r = SignalBoundaries()
if CONFIG.UseSpeechDetector:
r = bt_signal.find_reference_signal_via_speechdetector(file_test)
if r.offset_start == 0.0 and is_caller: if r.offset_start == 0.0 and is_caller:
r.offset_start = 5.0 # Skip ringing tones r.offset_start = 5.0 # Skip ringing tones
return r return r
def detect_reference_signal(file_reference: Path) -> SignalBoundaries: def detect_reference_signal(file_reference: Path) -> SignalBoundaries:
# Run silence eraser on reference file as well # Run silence eraser on reference file as well
result = bt_signal.find_reference_signal(file_reference) result = SignalBoundaries()
if CONFIG.UseSpeechDetector:
result = bt_signal.find_reference_signal_via_speechdetector(file_reference)
return result return result
@ -92,7 +99,8 @@ def upload_results():
# Path to audio # Path to audio
path_audio = t[1] path_audio = t[1]
utils.log(f'Found {path_report.name} and {path_audio.name} files.') utils.log(f'Found {t} report pair.')
if path_report is not None and path_report.exists():
try: try:
with open(path_report, 'rt') as f: with open(path_report, 'rt') as f:
report = json.loads(f.read()) report = json.loads(f.read())
@ -104,26 +112,16 @@ def upload_results():
if success: if success:
utils.log(f'Report {upload_id} is uploaded ok.') utils.log(f'Report {upload_id} is uploaded ok.')
# Rename files to make sync audio filename with reported ones
# path_report_fixed = CACHE.dir / f'{upload_id}.json'
# path_report = path_report.rename(path_report_fixed)
# path_audio_fixed = CACHE.dir / f'{upload_id}.wav'
# path_audio = path_audio.rename(path_audio_fixed)
if path_audio.exists():
utils.log(f'Uploading {path_audio.name} file...')
# Upload recorded audio
upload_result = BACKEND.upload_audio(upload_id, path_audio)
if upload_result:
utils.log(f' Recorded audio {upload_id}.wav is uploaded ok.')
os.remove(path_audio)
else:
utils.log(f'No recorded audio file found, skipping audio upload.')
os.remove(path_report) os.remove(path_report)
else: if path_audio is not None and path_audio.exists():
utils.log(f'Failed to upload report {path_report.name}') utils.log(f'Uploading {path_audio.name} file...')
break # Upload recorded audio
upload_result = BACKEND.upload_audio(path_audio.stem, path_audio)
if upload_result:
utils.log(f' Recorded audio {path_audio.stem}.wav is uploaded ok.')
os.remove(path_audio)
def run_analyze(file_test: str, file_reference: str, number: str) -> bool: def run_analyze(file_test: str, file_reference: str, number: str) -> bool:
global CALL_COUNTER global CALL_COUNTER
@ -147,25 +145,28 @@ def run_analyze(file_test: str, file_reference: str, number: str) -> bool:
utils.log(f'Recorded audio call duration: {test_audio_length}s, reference audio length: {ref_audio_length}s') utils.log(f'Recorded audio call duration: {test_audio_length}s, reference audio length: {ref_audio_length}s')
# Check if audio length is strange - skip such calls. Usually this is missed call. # Check if audio length is strange - skip such calls. Usually this is missed call.
is_caller_audio_big = is_caller and test_audio_length > ref_audio_length * 1.5 is_caller_audio_big = is_caller and test_audio_length > ref_audio_length * 3
is_answerer_audio_big = is_answerer and test_audio_length > ref_audio_length * 1.5 is_answerer_audio_big = is_answerer and test_audio_length > ref_audio_length * 3
if is_caller_audio_big or is_answerer_audio_big: if is_caller_audio_big or is_answerer_audio_big:
utils.log_error(f'Recorded call is too big - looks like mobile operator prompt, skipping analysis') utils.log_error(f'Recorded call is too big - looks like mobile operator prompt, skipping analysis')
return False return False
try: try:
bounds_signal : SignalBoundaries = detect_degraded_signal(Path(file_test), Path(file_reference)) bounds_signal = SignalBoundaries()
# bounds_signal.offset_start = 0 if is_caller:
# bounds_signal.offset_finish = 0 bounds_signal.offset_start = 10.0 # Skip ringtones
print(f'Found signal bounds: {bounds_signal}') bounds_signal.offset_finish = 1.0 # Eat possible end tone
elif is_answerer:
bounds_signal.offset_start = 0.0
bounds_signal.offset_finish = 1.0 # Eat possible end tone
# PVQA report # PVQA report
pvqa_mos, pvqa_report, pvqa_rfactor = utils_sevana.find_pvqa_mos(file_test, bounds_signal.offset_start, bounds_signal.offset_finish) pvqa_mos, pvqa_report, pvqa_rfactor = utils_sevana.find_pvqa_mos(file_test, bounds_signal.offset_start, bounds_signal.offset_finish)
utils.log(f'PVQA MOS: {pvqa_mos}, PVQA R-factor: {pvqa_rfactor}') utils.log(f'PVQA MOS: {pvqa_mos}, PVQA R-factor: {pvqa_rfactor}')
# AQuA report # AQuA report
bounds_reference : SignalBoundaries = detect_reference_signal(Path(file_reference)) bounds_reference : SignalBoundaries = SignalBoundaries()
bounds_reference.offset_start = 0 bounds_reference.offset_start = 0
bounds_reference.offset_finish = 0 bounds_reference.offset_finish = 0
@ -261,7 +262,7 @@ def make_call(target: str):
timelimit_seconds=ref_time_length, timelimit_seconds=ref_time_length,
target=target) target=target)
run_analyze(CONFIG.RecordFile, CONFIG.ReferenceAudio, target) run_analyze(CONFIG.RecordFile, CONFIG.PreparedReferenceAudio, target)
except Exception as e: except Exception as e:
utils.log_error(f'BT I/O failed finally. Error: {str(e)}') utils.log_error(f'BT I/O failed finally. Error: {str(e)}')
@ -297,7 +298,7 @@ def perform_answerer():
break break
# Call analyzer script # Call analyzer script
run_analyze(CONFIG.RecordFile, CONFIG.ReferenceAudio, '') run_analyze(CONFIG.RecordFile, CONFIG.PreparedReferenceAudio, '')
# Increase counter of attempts # Increase counter of attempts
attempt_idx += 1 attempt_idx += 1
@ -354,10 +355,10 @@ def run_caller_task(t):
make_call(target_addr) make_call(target_addr)
# Runs caller probe - load task list and perform calls # Runs caller probe - load task list and perform calls
def run_probe(): def run_probe():
global TASK_LIST, CURRENT_TASK global TASK_LIST, CURRENT_TASK
offline_mode : bool = False
while True: while True:
# Get task list update # Get task list update
@ -366,6 +367,7 @@ def run_probe():
# Check in cache # Check in cache
utils.log('Checking for task list in cache...') utils.log('Checking for task list in cache...')
new_tasks = CACHE.get_tasks(BACKEND.phone.name) new_tasks = CACHE.get_tasks(BACKEND.phone.name)
offline_mode = True
# Did we fetch anything ? # Did we fetch anything ?
if new_tasks: if new_tasks:
@ -386,7 +388,7 @@ def run_probe():
if TASK_LIST.tasks is not None: if TASK_LIST.tasks is not None:
utils.log_verbose(f"Resulting task list: {TASK_LIST.tasks}") utils.log_verbose(f"Resulting task list: {TASK_LIST.tasks}")
# Run test immediately if specified
if CONFIG.ForceRun and len(TASK_LIST.tasks) > 0: if CONFIG.ForceRun and len(TASK_LIST.tasks) > 0:
run_caller_task(TASK_LIST.tasks[0]) run_caller_task(TASK_LIST.tasks[0])
break break
@ -401,7 +403,9 @@ def run_probe():
# Remove sheduled time # Remove sheduled time
del t['scheduled_time'] del t['scheduled_time']
# Run task # Run task if we are online
# Otherwise tasks run from the API point - via helper .apk
if not offline_mode:
run_caller_task(t) run_caller_task(t)
utils.log_verbose(f'Call #{CALL_COUNTER.value} finished') utils.log_verbose(f'Call #{CALL_COUNTER.value} finished')
@ -413,15 +417,35 @@ def run_probe():
except Exception as err: except Exception as err:
utils.log_error(message="Unexpected error.", err=err) utils.log_error(message="Unexpected error.", err=err)
# Sleep for
spent_time = utils.get_monotonic_time() - start_time spent_time = utils.get_monotonic_time() - start_time
# Wait 1 minute # Wait 1 minute
if spent_time < 60: if spent_time < 60:
time.sleep(60 - spent_time) timeout_time = 60 - spent_time
else:
timeout_time = 0
# Try to get next task
try:
if agent_point.WEB_QUEUE is None:
utils.log('Web task queue is None')
task = agent_point.WEB_QUEUE.get(block=True, timeout=timeout_time)
if task is not None:
run_caller_task(task)
except multiprocessing.Queue.empty:
# Ignore this exception, this is normal
pass
except Exception as err:
utils.log_error(message='Error when running t')
# In case of empty task list wait 1 minute before refresh # In case of empty task list wait 1 minute before refresh
if len(TASK_LIST.tasks) == 0: # if len(TASK_LIST.tasks) == 0:
time.sleep(60) # time.sleep(60)
def remove_pid_on_exit(): def remove_pid_on_exit():
@ -436,6 +460,9 @@ def receive_signal(signal_number, frame):
# Delete PID file # Delete PID file
remove_pid_on_exit() remove_pid_on_exit()
# Stop optional access point
agent_point.stop()
# Debugging info # Debugging info
print(f'Got signal {signal_number} from {frame}') print(f'Got signal {signal_number} from {frame}')
@ -447,7 +474,6 @@ def receive_signal(signal_number, frame):
return return
# Check if Python version is ok # Check if Python version is ok
assert sys.version_info >= (3, 6) assert sys.version_info >= (3, 6)
@ -471,6 +497,14 @@ if __name__ == '__main__':
signal.signal(signal.SIGINT, receive_signal) signal.signal(signal.SIGINT, receive_signal)
signal.signal(signal.SIGQUIT, receive_signal) signal.signal(signal.SIGQUIT, receive_signal)
if CONFIG.CacheDir:
CACHE = utils_cache.InfoCache(dir=CONFIG.CacheDir)
# Start own hotspot and API server
agent_point.CONFIG = CONFIG
agent_point.CACHE = CACHE
agent_point.start()
# Preconnect the phone # Preconnect the phone
if CONFIG.BT_MAC: if CONFIG.BT_MAC:
# Connect to phone before # Connect to phone before
@ -482,7 +516,7 @@ if __name__ == '__main__':
utils.log_error(f'No BT MAC specified, cannot connect. Exiting.') utils.log_error(f'No BT MAC specified, cannot connect. Exiting.')
raise SystemExit(EXIT_ERROR) raise SystemExit(EXIT_ERROR)
# Init BT modem # Init BT modem - here we wait for it
bt_call_controller.init() bt_call_controller.init()
# Logging settings # Logging settings
@ -491,18 +525,12 @@ if __name__ == '__main__':
if CONFIG.LogPath: if CONFIG.LogPath:
utils.open_log_file(CONFIG.LogPath, 'at') utils.open_log_file(CONFIG.LogPath, 'at')
if CONFIG.CacheDir:
CACHE = utils_cache.InfoCache(dir=CONFIG.CacheDir)
# Update path to pvqa/aqua-wb # Update path to pvqa/aqua-wb
VOICE_QUALITY_AVAILABLE = utils_sevana.find_binaries(DIR_PROJECT / 'bin') VOICE_QUALITY_AVAILABLE = utils_sevana.find_binaries(DIR_PROJECT / 'bin')
# Load latest licenses & configs - this requires utils_sevana.find_binaries() to be called before # Load latest licenses & configs - this requires utils_sevana.find_binaries() to be called before
# utils_sevana.load_config_and_licenses(config['backend']) # utils_sevana.load_config_and_licenses(config['backend'])
# Limit number of calls # Limit number of calls
if CONFIG.TaskLimit: if CONFIG.TaskLimit:
utils.log(f'Limiting number of calls to {CONFIG.TaskLimit}') utils.log(f'Limiting number of calls to {CONFIG.TaskLimit}')
@ -570,4 +598,7 @@ if __name__ == '__main__':
# Close log file # Close log file
utils.close_log_file() utils.close_log_file()
# Stop optional access point
agent_point.stop()
sys.exit(EXIT_OK) sys.exit(EXIT_OK)

133
src/agent_point.py Executable file
View File

@ -0,0 +1,133 @@
#!/usr/bin/python3
import bottle
import multiprocessing
import time
import os
import json
import utils_cache
from agent_config import AgentConfig
class AccessPoint:
active: bool = False
def __init__(self) -> None:
pass
def start(self):
pass
def stop(self):
pass
# Just a stub for now
ACCESS_POINT = AccessPoint()
# Web server process
SERVER_PROCESS = None
# Good status response
RESPONSE_OK = {'status': 'ok'}
# Available information in cache
CACHE : utils_cache.InfoCache = None
CONFIG: AgentConfig = None
# Web queue
WEB_QUEUE = multiprocessing.Manager().Queue()
@bottle.route('/status')
def web_status():
print(f'Serving /status request...')
r = RESPONSE_OK
if CONFIG is not None:
r['name'] = CONFIG.Name
r['backend'] = CONFIG.Backend
r['bt_mac'] = CONFIG.BT_MAC
if CACHE is not None:
print('Cache is found...')
# Phone information
phone = CACHE.get_phone(CONFIG.Name)
if phone is not None:
print('Phone information is found...')
r['phone'] = phone.to_dict()
# Task list information
task_list = CACHE.get_tasks(CONFIG.Name)
if task_list is not None and task_list.tasks is not None:
r['task_list'] = task_list.tasks
else:
print('Cache not found.')
return r
@bottle.route('/reboot')
def web_reboot():
os.system('sudo reboot')
return RESPONSE_OK
@bottle.route('/halt')
def web_reboot():
os.system('sudo halt')
return RESPONSE_OK
@bottle.route('/cache')
def web_list_cache():
result = []
if CACHE is None:
return result
# Iterate cache and return available files list
for f in os.listdir(CACHE.dir):
result.append(f)
return result
@bottle.route('/call', method=['POST'])
def web_call():
global WEB_QUEUE
try:
data = bottle.request.json
# Send task definition
print('Sending data to ougoing queue...')
WEB_QUEUE.put_nowait(data)
print('Returning OK response.')
return RESPONSE_OK
except Exception as e:
print(f'{str(e)}')
return RESPONSE_OK
def web_process(mp_queue: multiprocessing.Queue):
#global WEB_QUEUE
#WEB_QUEUE = mp_queue
print(f'Run web process...')
bottle.run(host='0.0.0.0', port=8080)
def start():
global ACCESS_POINT, SERVER_PROCESS
ACCESS_POINT.start()
SERVER_PROCESS = multiprocessing.Process(target=web_process, args=(None,), name='agent_gsm_web')
SERVER_PROCESS.start()
def stop():
global ACCESS_POINT, SERVER_PROCESS
ACCESS_POINT.stop()
SERVER_PROCESS.kill()
if __name__ == '__main__':
# Start test stuff
start()
# Wait 120 seconds for tests
time.sleep(120.0)
# Stop test
stop()

View File

@ -3,51 +3,46 @@
import sys import sys
import os import os
import pathlib import pathlib
from utils_types import SignalBoundaries
from utils_sevana import speech_detector
from pydub import silence, AudioSegment # from pydub import silence, AudioSegment
class SignalBoundaries: SILENCE_DELTA = 16
# Offset from start (in seconds)
offset_start: float
# Offset from finish (in seconds) # def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries:
offset_finish: float # myaudio = AudioSegment.from_wav(str(input_file))
# dBFS = myaudio.dBFS
def __init__(self, offset_start = 0.0, offset_finish = 0.0) -> None: # # Find silence intervals
self.offset_start = offset_start # intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50)
self.offset_finish = offset_finish
def __repr__(self) -> str: # # Translate to seconds
return f'[offset_start: {round(self.offset_start, 3)}, offset_finish : {round(self.offset_finish, 3)}]' # intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec
# # print(intervals)
# # Example of intervals: [(5.4, 6.4), (18.7, 37.05)]
# for p in intervals:
# if p[1] - p[0] > 17:
# bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
# if output_file is not None:
# signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
# signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
# if use_end_offset:
# bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish
# return bounds
# return SignalBoundaries()
def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries: def find_reference_signal_via_speechdetector(input_file: pathlib.Path) -> SignalBoundaries:
myaudio = AudioSegment.from_wav(str(input_file)) bounds = speech_detector(str(input_file))
dBFS = myaudio.dBFS r = SignalBoundaries(bounds[0], bounds[1])
# Find silence intervals
intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-17, seek_step=50)
# Translate to seconds
intervals = [((start/1000),(stop/1000)) for start,stop in intervals] #in sec
# print(intervals)
# Example of intervals: [(5.4, 6.4), (18.7, 37.05)]
for p in intervals:
if p[1] - p[0] > 17:
bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
if output_file is not None:
signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
if use_end_offset:
bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish
return bounds return bounds
return SignalBoundaries()
if __name__ == '__main__': if __name__ == '__main__':
if len(sys.argv) < 2: if len(sys.argv) < 2:
print(f'Please specify input filename.') print(f'Please specify input filename.')

View File

@ -19,8 +19,6 @@ class InfoCache:
utils.log_error(str(e)) utils.log_error(str(e))
self.dir = None self.dir = None
def is_active(self) -> bool: def is_active(self) -> bool:
return self.dir is not None return self.dir is not None
@ -80,11 +78,16 @@ class InfoCache:
lst = os.listdir(self.dir) lst = os.listdir(self.dir)
for n in lst: for n in lst:
p = self.dir / n p = self.dir / n
if self.is_valid_uuid(p.stem) and n.endswith('.json'): if self.is_valid_uuid(p.stem) and (n.endswith('.json') or n.endswith(".wav")):
# Probe found # Probe found
p_json = p.with_suffix('.json')
p_audio = p.with_suffix('.wav') p_audio = p.with_suffix('.wav')
if p_audio.exists(): if p_json.exists() and p_audio.exists():
r.append((p, p.with_suffix('.wav'))) r.append((p_json, p_audio))
elif p_json.exists():
r.append((p_json, None))
elif p_audio.exists():
r.append((None, p_audio))
return r return r

View File

@ -58,6 +58,7 @@ TRACE_TOTAL_TIMEOUT = 30
# a webpage is mostly I/O bound, it's not going to be significant. # a webpage is mostly I/O bound, it's not going to be significant.
def trace_function(frame, event, arg): def trace_function(frame, event, arg):
global TRACE_START_TIME
if time.time() - TRACE_START_TIME > TRACE_TOTAL_TIMEOUT: if time.time() - TRACE_START_TIME > TRACE_TOTAL_TIMEOUT:
raise Exception('Timed out!') # Use whatever exception you consider appropriate. raise Exception('Timed out!') # Use whatever exception you consider appropriate.
@ -65,12 +66,13 @@ def trace_function(frame, event, arg):
class QualtestBackend: class QualtestBackend:
address: str address: str
instance: str instance: str
online: bool
def __init__(self): def __init__(self):
self.address = "" self.address = ""
self.instance = "" self.instance = ""
self.__phone = None self.__phone = None
self.online = False
@property @property
def phone(self) -> Phone: def phone(self) -> Phone:
@ -114,6 +116,7 @@ class QualtestBackend:
def upload_audio(self, probe_id, path_recorded: Path): def upload_audio(self, probe_id, path_recorded: Path):
global TRACE_START_TIME
result = False result = False
# Log about upload attempt # Log about upload attempt
@ -188,8 +191,17 @@ class QualtestBackend:
response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT) response = urllib.request.urlopen(url, timeout=utils.NETWORK_TIMEOUT)
if response.getcode() != 200: if response.getcode() != 200:
raise RuntimeError(f'Failed to load phone definition from server. Error code: {response.getcode()}') raise RuntimeError(f'Failed to load phone definition from server. Error code: {response.getcode()}')
# Consider backend as working one
self.online = True
except Exception as e: except Exception as e:
utils.log_error(f'Problem when loading the phone definition from backend. Error: {str(e)}') utils.log_error(f'Problem when loading the phone definition from backend. Error: {str(e)}')
# Consider backend as non-working
self.online = False
# Try to get data from the cache
r = cache.get_phone(self.instance) r = cache.get_phone(self.instance)
if r is None: if r is None:
raise RuntimeError(f'No cached phone definition.') raise RuntimeError(f'No cached phone definition.')

View File

@ -12,8 +12,6 @@ import time
import urllib import urllib
from pathlib import Path from pathlib import Path
from colorama import Fore, Style
from utils_cache import InfoCache
PVQA_CMD = "{pvqa} --license {pvqa_lic} --config {pvqa_cfg} --mode analysis --channel 0 " \ PVQA_CMD = "{pvqa} --license {pvqa_lic} --config {pvqa_cfg} --mode analysis --channel 0 " \
"--report {output} --input {input} --cut-begin {cut_begin} --cut-end {cut_end}" "--report {output} --input {input} --cut-begin {cut_begin} --cut-end {cut_end}"
@ -21,10 +19,8 @@ PVQA_CMD = "{pvqa} --license {pvqa_lic} --config {pvqa_cfg} --mode analysis --ch
PVQA_CMD_LIC_SERVER = "{pvqa} --license-server {pvqa_lic} --config {pvqa_cfg} --mode analysis --channel 0 " \ PVQA_CMD_LIC_SERVER = "{pvqa} --license-server {pvqa_lic} --config {pvqa_cfg} --mode analysis --channel 0 " \
"--report {output} --input {input}" "--report {output} --input {input}"
AQUA_CMD = ("{aqua} {aqua_lic} -mode files -src file \"{reference}\" -tstf \"{input}\" -avlp off -smtnrm on " AQUA_CMD = ("{aqua} {aqua_lic} -mode files -src file \"{reference}\" -tstf \"{input}\" -config {aqua_config} "
"-decor off -mprio off -acr auto -npnt auto -voip on -enorm rms -g711 off " "-specp 32 {spectrum} -fau {faults} -cut-tst {cut_begin} {cut_end} -cut-src {cut_begin_src} {cut_end_src}")
"-spfrcor on -grad off -tmc on -hist-pitch on on -hist-levels on on on -miter 1 -specp 32 {spectrum} "
"-ratem %%m -fau {faults} -output json -trim r 15 -cut-tst {cut_begin} {cut_end} -cut-src {cut_begin_src} {cut_end_src}")
PVQA_PATH = "" PVQA_PATH = ""
PVQA_LIC_PATH = "pvqa.lic" PVQA_LIC_PATH = "pvqa.lic"
@ -32,6 +28,7 @@ PVQA_CFG_PATH = "pvqa.cfg"
AQUA_PATH = "" AQUA_PATH = ""
AQUA_LIC_PATH = "aqua-wb.lic" AQUA_LIC_PATH = "aqua-wb.lic"
AQUA_CFG_PATH = "aqua.cfg"
SILER_PATH = "" SILER_PATH = ""
@ -78,12 +75,16 @@ def load_config_and_licenses(server: str):
load_file(utils.join_host_and_path(server, '/deploy/pvqa.cfg'), PVQA_CFG_PATH) load_file(utils.join_host_and_path(server, '/deploy/pvqa.cfg'), PVQA_CFG_PATH)
load_file(utils.join_host_and_path(server, '/deploy/pvqa.lic'), PVQA_LIC_PATH) load_file(utils.join_host_and_path(server, '/deploy/pvqa.lic'), PVQA_LIC_PATH)
load_file(utils.join_host_and_path(server, '/deploy/aqua-wb.lic'), AQUA_LIC_PATH) load_file(utils.join_host_and_path(server, '/deploy/aqua-wb.lic'), AQUA_LIC_PATH)
load_file(utils.join_host_and_path(server, '/deploy/aqua.cfg'), AQUA_CFG_PATH)
except Exception as e: except Exception as e:
utils.log_error(f'Failed to fetch new licenses and config. Skipping it.') utils.log_error(f'Failed to fetch new licenses and config. Skipping it.')
def find_binaries(bin_directory: Path, license_server: str = None) -> bool: def find_binaries(bin_directory: Path, license_server: str = None) -> bool:
# Update path to pvqa/aqua-wb # Update path to pvqa/aqua-wb
global PVQA_CFG_PATH, PVQA_LIC_PATH, AQUA_LIC_PATH, PVQA_PATH, AQUA_PATH, PVQA_CMD, AQUA_CMD, SILER_PATH, SPEECH_DETECTOR_PATH global PVQA_CFG_PATH, PVQA_LIC_PATH, AQUA_LIC_PATH, AQUA_CFG_PATH
global PVQA_PATH, AQUA_PATH, PVQA_CMD, AQUA_CMD
global SILER_PATH, SPEECH_DETECTOR_PATH
# Find platform prefix # Find platform prefix
platform_prefix = platform.system().lower() platform_prefix = platform.system().lower()
@ -94,8 +95,9 @@ def find_binaries(bin_directory: Path, license_server: str = None) -> bool:
PVQA_LIC_PATH = bin_directory / PVQA_LIC_PATH PVQA_LIC_PATH = bin_directory / PVQA_LIC_PATH
PVQA_CFG_PATH = bin_directory / PVQA_CFG_PATH PVQA_CFG_PATH = bin_directory / PVQA_CFG_PATH
AQUA_PATH = bin_directory / platform_prefix / AQUA_PATH AQUA_PATH = bin_directory / platform_prefix / AQUA_PATH
AQUA_CFG_PATH = bin_directory / AQUA_CFG_PATH
AQUA_LIC_PATH = bin_directory / AQUA_LIC_PATH AQUA_LIC_PATH = bin_directory / AQUA_LIC_PATH
SILER_PATH = bin_directory / platform_prefix / SILER_PATH # SILER_PATH = bin_directory / platform_prefix / SILER_PATH
SPEECH_DETECTOR_PATH = bin_directory / platform_prefix / SPEECH_DETECTOR_PATH SPEECH_DETECTOR_PATH = bin_directory / platform_prefix / SPEECH_DETECTOR_PATH
utils.log(f'Looking for binaries/licenses/configs at {bin_directory}...') utils.log(f'Looking for binaries/licenses/configs at {bin_directory}...')
@ -109,16 +111,23 @@ def find_binaries(bin_directory: Path, license_server: str = None) -> bool:
PVQA_CFG_PATH = Path(utils.get_script_path()) / 'pvqa.cfg' PVQA_CFG_PATH = Path(utils.get_script_path()) / 'pvqa.cfg'
if not PVQA_CFG_PATH.exists(): if not PVQA_CFG_PATH.exists():
utils.log_error(f'Failed to find pvqa config.') utils.log_error(f'Failed to find PVQA config file.')
return False
if not AQUA_CFG_PATH.exists():
AQUA_CFG_PATH = Path(utils.get_script_path()) / 'aqua.cfg'
if not AQUA_CFG_PATH.exists():
utils.log_error(f'Failed to find AQuA config file.')
return False return False
if not AQUA_PATH.exists(): if not AQUA_PATH.exists():
utils.log_error(f'Failed to find aqua-wb binary.') utils.log_error(f'Failed to find aqua-wb binary.')
return False return False
if not SILER_PATH.exists(): # if not SILER_PATH.exists():
utils.log_error(f'Failed to find silence_eraser binary..') # utils.log_error(f'Failed to find silence_eraser binary..')
return False # return False
if license_server is not None: if license_server is not None:
AQUA_LIC_PATH = '"license://' + license_server + '"' AQUA_LIC_PATH = '"license://' + license_server + '"'
@ -238,7 +247,7 @@ def find_aqua_mos(good_path, test_path, test_file_offset_begin: float = 0.0, tes
good_file_offset_begin: float = 0.0, good_file_offset_end: float = 0.0): good_file_offset_begin: float = 0.0, good_file_offset_end: float = 0.0):
try: try:
out_data = "" out_data = ""
cmd = AQUA_CMD.format(aqua=AQUA_PATH, aqua_lic=AQUA_LIC_PATH, cmd = AQUA_CMD.format(aqua=AQUA_PATH, aqua_lic=AQUA_LIC_PATH, aqua_config = AQUA_CFG_PATH,
reference=good_path, input=test_path, spectrum=AQUA_SPECTRUM, reference=good_path, input=test_path, spectrum=AQUA_SPECTRUM,
faults=AQUA_FAULTS, faults=AQUA_FAULTS,
cut_begin=int(test_file_offset_begin * 1000), cut_end=int(test_file_offset_end * 1000), cut_begin=int(test_file_offset_begin * 1000), cut_end=int(test_file_offset_end * 1000),

View File

@ -6,12 +6,25 @@ import utils
import json import json
from crontab import CronTab from crontab import CronTab
# Exit codes # Exit codes
EXIT_OK = 0 EXIT_OK = 0
EXIT_ERROR = 1 EXIT_ERROR = 1
class SignalBoundaries:
# Offset from start (in seconds)
offset_start: float
# Offset from finish (in seconds)
offset_finish: float
def __init__(self, offset_start = 0.0, offset_finish = 0.0) -> None:
self.offset_start = offset_start
self.offset_finish = offset_finish
def __repr__(self) -> str:
return f'[offset_start: {round(self.offset_start, 3)}, offset_finish : {round(self.offset_finish, 3)}]'
class Phone: class Phone:
identifier: int = 0 identifier: int = 0
name: str = "" name: str = ""