Compare commits

..

No commits in common. "master" and "speech_detector" have entirely different histories.

25 changed files with 279 additions and 654 deletions

View File

@ -1,21 +0,0 @@
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

BIN
bin/linux/aqua-wb Executable file

Binary file not shown.

BIN
bin/linux/pjsua Executable file

Binary file not shown.

BIN
bin/linux/pvqa Executable file

Binary file not shown.

BIN
bin/linux/silence_eraser Executable file

Binary file not shown.

BIN
bin/linux/speech_detector Executable file

Binary file not shown.

1
bin/macos/README.txt Normal file
View File

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

BIN
bin/macos/aqua-wb Executable file

Binary file not shown.

BIN
bin/macos/pjsua Executable file

Binary file not shown.

BIN
bin/macos/pvqa Executable file

Binary file not shown.

View File

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

View File

@ -1,23 +0,0 @@
# 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=""

View File

@ -1,64 +0,0 @@
# 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

@ -1,5 +0,0 @@
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

@ -1,8 +0,0 @@
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

@ -1,31 +0,0 @@
[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

@ -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 dnsmasq hostapd screen sudo apt install --assume-yes git mc python3 sox vim libffi-dev screen python3-pip python3-numpy
sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect requests rabbitpy bottle sudo pip3 install pyyaml sox pyrabbit soundfile dbus_python pexpect pydub requests rabbitpy pydub
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 - finish preparing the agent configuration file # Replace the values
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,45 +56,11 @@ 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 .`
# Update systemD unit file sed -i "s|ABSOLUTE_INSTALL_DIR|$ABSOLUTE_INSTALL_DIR|" config/systemd/agent_gsm.service
# 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

@ -24,7 +24,6 @@ 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()
@ -69,23 +68,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 = SignalBoundaries()
if CONFIG.UseSpeechDetector: if CONFIG.UseSpeechDetector:
r = bt_signal.find_reference_signal_via_speechdetector(file_test) r = bt_signal.find_reference_signal_via_speechdetector(file_test)
else:
r = bt_signal.find_reference_signal(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 = SignalBoundaries()
if CONFIG.UseSpeechDetector: if CONFIG.UseSpeechDetector:
result = bt_signal.find_reference_signal_via_speechdetector(file_reference) result = bt_signal.find_reference_signal_via_speechdetector(file_reference)
else:
result = bt_signal.find_reference_signal(file_reference)
return result return result
@ -153,20 +152,17 @@ def run_analyze(file_test: str, file_reference: str, number: str) -> bool:
return False return False
try: try:
bounds_signal = SignalBoundaries() bounds_signal : SignalBoundaries = detect_degraded_signal(Path(file_test), Path(file_reference))
if is_caller: # bounds_signal.offset_start = 0
bounds_signal.offset_start = 10.0 # Skip ringtones # bounds_signal.offset_finish = 0
bounds_signal.offset_finish = 1.0 # Eat possible end tone print(f'Found signal bounds: {bounds_signal}')
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 = SignalBoundaries() bounds_reference : SignalBoundaries = detect_reference_signal(Path(file_reference))
bounds_reference.offset_start = 0 bounds_reference.offset_start = 0
bounds_reference.offset_finish = 0 bounds_reference.offset_finish = 0
@ -262,7 +258,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.PreparedReferenceAudio, target) run_analyze(CONFIG.RecordFile, CONFIG.ReferenceAudio, 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)}')
@ -298,7 +294,7 @@ def perform_answerer():
break break
# Call analyzer script # Call analyzer script
run_analyze(CONFIG.RecordFile, CONFIG.PreparedReferenceAudio, '') run_analyze(CONFIG.RecordFile, CONFIG.ReferenceAudio, '')
# Increase counter of attempts # Increase counter of attempts
attempt_idx += 1 attempt_idx += 1
@ -353,12 +349,12 @@ def run_caller_task(t):
# Start call. It will analyse audio as well and upload results # Start call. It will analyse audio as well and upload results
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
@ -367,7 +363,6 @@ 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:
@ -388,7 +383,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
@ -403,10 +398,8 @@ def run_probe():
# Remove sheduled time # Remove sheduled time
del t['scheduled_time'] del t['scheduled_time']
# Run task if we are online # Run task
# Otherwise tasks run from the API point - via helper .apk run_caller_task(t)
if not offline_mode:
run_caller_task(t)
utils.log_verbose(f'Call #{CALL_COUNTER.value} finished') utils.log_verbose(f'Call #{CALL_COUNTER.value} finished')
if CALL_COUNTER.value >= CONFIG.TaskLimit and CONFIG.TaskLimit > 0: if CALL_COUNTER.value >= CONFIG.TaskLimit and CONFIG.TaskLimit > 0:
@ -417,35 +410,15 @@ 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:
timeout_time = 60 - spent_time time.sleep(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():
@ -460,9 +433,6 @@ 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}')
@ -474,6 +444,7 @@ 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)
@ -497,14 +468,6 @@ 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
@ -516,7 +479,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 - here we wait for it # Init BT modem
bt_call_controller.init() bt_call_controller.init()
# Logging settings # Logging settings
@ -525,12 +488,18 @@ 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}')
@ -557,7 +526,7 @@ if __name__ == '__main__':
if BACKEND.phone is None: if BACKEND.phone is None:
utils.log_error(f'Failed to obtain information about {BACKEND.instance}. Exiting.') utils.log_error(f'Failed to obtain information about {BACKEND.instance}. Exiting.')
exit(EXIT_ERROR) exit(EXIT_ERROR)
# Cache phone information # Cache phone information
CACHE.put_phone(BACKEND.phone) CACHE.put_phone(BACKEND.phone)
@ -598,7 +567,4 @@ 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)

View File

@ -1,133 +0,0 @@
#!/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

@ -6,36 +6,36 @@ import pathlib
from utils_types import SignalBoundaries from utils_types import SignalBoundaries
from utils_sevana import speech_detector from utils_sevana import speech_detector
# from pydub import silence, AudioSegment from pydub import silence, AudioSegment
SILENCE_DELTA = 16 SILENCE_DELTA = 16
# def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries: def find_reference_signal(input_file: pathlib.Path, output_file: pathlib.Path = None, use_end_offset: bool = True) -> SignalBoundaries:
# myaudio = AudioSegment.from_wav(str(input_file)) myaudio = AudioSegment.from_wav(str(input_file))
# dBFS = myaudio.dBFS dBFS = myaudio.dBFS
# # Find silence intervals # Find silence intervals
# intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50) intervals = silence.detect_nonsilent(myaudio, min_silence_len=1000, silence_thresh=dBFS-SILENCE_DELTA, seek_step=50)
# # Translate to seconds # Translate to seconds
# intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec intervals = [((start/1000),(stop/1000)) for start,stop in intervals] # in sec
# # print(intervals) # print(intervals)
# # Example of intervals: [(5.4, 6.4), (18.7, 37.05)] # Example of intervals: [(5.4, 6.4), (18.7, 37.05)]
# for p in intervals: for p in intervals:
# if p[1] - p[0] > 17: if p[1] - p[0] > 17:
# bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1]) bounds = SignalBoundaries(offset_start=p[0], offset_finish=p[1])
# if output_file is not None: if output_file is not None:
# signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000] signal = myaudio[bounds.offset_start * 1000 : bounds.offset_finish * 1000]
# signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16']) signal.export(str(output_file), format='wav', parameters=['-ar', '44100', '-sample_fmt', 's16'])
# if use_end_offset: if use_end_offset:
# bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish bounds.offset_finish = myaudio.duration_seconds - bounds.offset_finish
# return bounds return bounds
# return SignalBoundaries() return SignalBoundaries()
def find_reference_signal_via_speechdetector(input_file: pathlib.Path) -> SignalBoundaries: def find_reference_signal_via_speechdetector(input_file: pathlib.Path) -> SignalBoundaries:

View File

@ -19,6 +19,8 @@ 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

View File

@ -66,13 +66,12 @@ 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:
@ -191,17 +190,8 @@ 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,6 +12,8 @@ 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}"
@ -19,8 +21,10 @@ 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}\" -config {aqua_config} " AQUA_CMD = ("{aqua} {aqua_lic} -mode files -src file \"{reference}\" -tstf \"{input}\" -avlp off -smtnrm on "
"-specp 32 {spectrum} -fau {faults} -cut-tst {cut_begin} {cut_end} -cut-src {cut_begin_src} {cut_end_src}") "-decor off -mprio off -acr auto -npnt auto -voip on -enorm rms -g711 off "
"-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"
@ -28,7 +32,6 @@ 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 = ""
@ -75,16 +78,12 @@ 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, AQUA_CFG_PATH 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_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()
@ -95,9 +94,8 @@ 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}...')
@ -111,23 +109,16 @@ 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 file.') utils.log_error(f'Failed to find pvqa config.')
return False 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
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 + '"'
@ -247,7 +238,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, aqua_config = AQUA_CFG_PATH, cmd = AQUA_CMD.format(aqua=AQUA_PATH, aqua_lic=AQUA_LIC_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),