Overhaul of how scripts are written to allow programmatic access

This introduces a way for scripts to be easily called from within Python with command line arguments as function parameters
To support this, prettyparse has been upgraded to the latest version
pull/102/head
Matthew D. Scholefield 2019-10-25 00:09:58 -07:00
parent fb452ca1eb
commit 4cec9b0767
20 changed files with 924 additions and 866 deletions

View File

@ -12,16 +12,18 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from prettyparse import create_parser
from random import randint
from precise_runner import PreciseRunner
from precise_runner.runner import ListenerEngine
from prettyparse import Usage
from threading import Event
from precise.pocketsphinx.listener import PocketsphinxListener
from precise.scripts.base_script import BaseScript
from precise.util import activate_notify
from precise_runner import PreciseRunner
from precise_runner.runner import ListenerEngine
usage = '''
class PocketsphinxListenScript(BaseScript):
usage = Usage('''
Run Pocketsphinx on microphone audio input
:key_phrase str
@ -38,20 +40,16 @@ usage = '''
:-c --chunk-size int 2048
Samples between inferences
'''
session_id, chunk_num = '%09d' % randint(0, 999999999), 0
def main():
args = create_parser(usage).parse_args()
''')
def run(self):
def on_activation():
activate_notify()
def on_prediction(conf):
print('!' if conf > 0.5 else '.', end='', flush=True)
args = self.args
runner = PreciseRunner(
ListenerEngine(
PocketsphinxListener(
@ -63,5 +61,7 @@ def main():
Event().wait() # Wait forever
main = PocketsphinxListenScript.run_main
if __name__ == '__main__':
main()

View File

@ -13,14 +13,17 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import wave
from prettyparse import create_parser
from prettyparse import Usage
from subprocess import check_output, PIPE
from precise.pocketsphinx.listener import PocketsphinxListener
from precise.scripts.test import show_stats, Stats
from precise.scripts.base_script import BaseScript
from precise.scripts.test import Stats
from precise.train_data import TrainData
usage = '''
class PocketsphinxTestScript(BaseScript):
usage = Usage('''
Test a dataset using Pocketsphinx
:key_phrase str
@ -42,51 +45,69 @@ usage = '''
Don't show the names of files that failed
...
'''
''') | TrainData.usage
def __init__(self, args):
super().__init__(args)
self.listener = PocketsphinxListener(
args.key_phrase, args.dict_file, args.hmm_folder, args.threshold
)
def eval_file(filename) -> float:
self.outputs = []
self.targets = []
self.filenames = []
def get_stats(self):
return Stats(self.outputs, self.targets, self.filenames)
def run(self):
args = self.args
data = TrainData.from_both(args.tags_file, args.tags_folder, args.folder)
print('Data:', data)
ww_files, nww_files = data.train_files if args.use_train else data.test_files
self.run_test(ww_files, 'Wake Word', 1.0)
self.run_test(nww_files, 'Not Wake Word', 0.0)
stats = self.get_stats()
if not self.args.no_filenames:
fp_files = stats.calc_filenames(False, True, 0.5)
fn_files = stats.calc_filenames(False, False, 0.5)
print('=== False Positives ===')
print('\n'.join(fp_files))
print()
print('=== False Negatives ===')
print('\n'.join(fn_files))
print()
print(stats.counts_str(0.5))
print()
print(stats.summary_str(0.5))
def eval_file(self, filename) -> float:
transcription = check_output(
['pocketsphinx_continuous', '-kws_threshold', '1e-20', '-keyphrase', 'hey my craft',
'-infile', filename], stderr=PIPE)
return float(bool(transcription) and not transcription.isspace())
def test_pocketsphinx(listener: PocketsphinxListener, data_files) -> Stats:
def run_test(filenames, name):
def run_test(self, test_files, label_name, label):
print()
print('===', name, '===')
negatives, positives = [], []
for filename in filenames:
print('===', label_name, '===')
for test_file in test_files:
try:
with wave.open(filename) as wf:
with wave.open(test_file) as wf:
frames = wf.readframes(wf.getnframes())
except (OSError, EOFError):
print('?', end='', flush=True)
continue
out = listener.found_wake_word(frames)
{False: negatives, True: positives}[out].append(filename)
out = int(self.listener.found_wake_word(frames))
self.outputs.append(out)
self.targets.append(label)
self.filenames.append(test_file)
print('!' if out else '.', end='', flush=True)
print()
return negatives, positives
false_neg, true_pos = run_test(data_files[0], 'Wake Word')
true_neg, false_pos = run_test(data_files[1], 'Not Wake Word')
return Stats(false_pos, false_neg, true_pos, true_neg)
def main():
args = TrainData.parse_args(create_parser(usage))
data = TrainData.from_both(args.tags_file, args.tags_folder, args.folder)
data_files = data.train_files if args.use_train else data.test_files
listener = PocketsphinxListener(
args.key_phrase, args.dict_file, args.hmm_folder, args.threshold
)
print('Data:', data)
stats = test_pocketsphinx(listener, data_files)
show_stats(stats, not args.no_filenames)
main = PocketsphinxTestScript.run_main
if __name__ == '__main__':
main()

View File

@ -12,7 +12,6 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from itertools import chain
from math import sqrt
import numpy as np
@ -20,40 +19,15 @@ import os
from glob import glob
from os import makedirs
from os.path import join, dirname, abspath, splitext
from pip._vendor.distlib._backport import shutil
from prettyparse import create_parser
import shutil
from prettyparse import Usage
from random import random
from precise.scripts.base_script import BaseScript
from precise.train_data import TrainData
from precise.util import load_audio
from precise.util import save_audio
usage = '''
Create a duplicate dataset with added noise
:folder str
Folder containing source dataset
:-tg --tags-file str -
Tags file to optionally load from
:noise_folder str
Folder with wav files containing noise to be added
:output_folder str
Folder to write the duplicate generated dataset
:-if --inflation-factor int 1
The number of noisy samples generated per single source sample
:-nl --noise-ratio-low float 0.0
Minimum random ratio of noise to sample. 1.0 is all noise, no sample sound
:-nh --noise-ratio-high float 0.4
Maximum random ratio of noise to sample. 1.0 is all noise, no sample sound
...
'''
class NoiseData:
def __init__(self, noise_folder: str):
@ -92,11 +66,39 @@ class NoiseData:
return noise_ratio * adjusted_noise + (1.0 - noise_ratio) * audio
def main():
args = create_parser(usage).parse_args()
args.tags_file = abspath(args.tags_file) if args.tags_file else None
args.folder = abspath(args.folder)
args.output_folder = abspath(args.output_folder)
class AddNoiseScript(BaseScript):
usage = Usage(
"""
Create a duplicate dataset with added noise
:folder str
Folder containing source dataset
:-tg --tags-file str -
Tags file to optionally load from
:noise_folder str
Folder with wav files containing noise to be added
:output_folder str
Folder to write the duplicate generated dataset
:-if --inflation-factor int 1
The number of noisy samples generated per single source sample
:-nl --noise-ratio-low float 0.0
Minimum random ratio of noise to sample. 1.0 is all noise, no sample sound
:-nh --noise-ratio-high float 0.4
Maximum random ratio of noise to sample. 1.0 is all noise, no sample sound
""",
tags_file=lambda args: abspath(args.tags_file) if args.tags_file else None,
folder=lambda args: abspath(args.folder),
output_folder=lambda args: abspath(args.output_folder)
)
def run(self):
args = self.args
noise_min, noise_max = args.noise_ratio_low, args.noise_ratio_high
data = TrainData.from_both(args.tags_file, args.folder, args.folder)
@ -129,5 +131,7 @@ def main():
shutil.copy2(args.tags_file, translate_filename(args.tags_file))
main = AddNoiseScript.run_main
if __name__ == '__main__':
main()

View File

@ -0,0 +1,51 @@
from abc import abstractmethod
from argparse import ArgumentParser, Namespace
from prettyparse import Usage
class BaseScript:
"""A class to standardize the way scripts are defined"""
usage = Usage()
def __init__(self, args):
self.args = args
@classmethod
def create(cls, **args):
values = {}
for arg_name, arg_data in cls.usage.arguments.items():
if arg_name in args:
values[arg_name] = args.pop(arg_name)
else:
if 'default' not in arg_data and arg_name and not arg_data['_0'].startswith('-'):
raise TypeError('Calling script without required "{}" argument.'.format(arg_name))
typ = arg_data.get('type')
if arg_data.get('action', '').startswith('store_') and not typ:
typ = bool
if not typ:
typ = lambda x: x
values[arg_name] = typ(arg_data.get('default'))
args = Namespace(**values)
cls.usage.render_args(args)
return cls(args)
@abstractmethod
def run(self):
pass
@classmethod
def run_main(cls):
parser = ArgumentParser()
cls.usage.apply(parser)
args = cls.usage.render_args(parser.parse_args())
try:
script = cls(args)
except ValueError as e:
parser.error('Error parsing args: ' + str(e))
raise SystemExit(1)
try:
script.run()
except KeyboardInterrupt:
print()

View File

@ -12,19 +12,18 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License
from math import atan, tan, log, exp, sqrt, pi
from math import sqrt
import math
from functools import partial
from os.path import basename, splitext
from prettyparse import create_parser
from prettyparse import Usage
from precise.params import inject_params, save_params
from precise.scripts.base_script import BaseScript
from precise.stats import Stats
usage = '''
class CalcThresholdScript(BaseScript):
usage = Usage('''
Update the threshold values of a model for a dataset.
This makes the sensitivity more accurate and linear
@ -43,11 +42,13 @@ usage = '''
:-c --center float 0.2
Decoded threshold that is mapped to 0.5. Proportion of
false negatives at sensitivity=0.5
'''
''')
def __init__(self, args):
super().__init__(args)
def main():
args = create_parser(usage).parse_args()
def run(self):
args = self.args
import numpy as np
model_data = {
@ -83,5 +84,7 @@ def main():
print('Saved params to {}.params'.format(args.model))
main = CalcThresholdScript.run_main
if __name__ == '__main__':
main()

View File

@ -16,52 +16,13 @@ from select import select
from sys import stdin
from termios import tcsetattr, tcgetattr, TCSADRAIN
import pyaudio
import tty
import wave
from os.path import isfile
from prettyparse import create_parser
from prettyparse import Usage
from pyaudio import PyAudio
usage = '''
Record audio samples for use with precise
:-w --width int 2
Sample width of audio
:-r --rate int 16000
Sample rate of audio
:-c --channels int 1
Number of audio channels
'''
def key_pressed():
return select([stdin], [], [], 0) == ([stdin], [], [])
def termios_wrapper(main):
global orig_settings
orig_settings = tcgetattr(stdin)
try:
hide_input()
main()
finally:
tcsetattr(stdin, TCSADRAIN, orig_settings)
def show_input():
tcsetattr(stdin, TCSADRAIN, orig_settings)
def hide_input():
tty.setcbreak(stdin.fileno())
orig_settings = None
RECORD_KEY = ' '
EXIT_KEY_CODE = 27
from precise.scripts.base_script import BaseScript
def record_until(p, should_return, args):
@ -88,7 +49,39 @@ def save_audio(name, data, args):
wf.close()
def next_name(name):
class CollectScript(BaseScript):
RECORD_KEY = ' '
EXIT_KEY_CODE = 27
usage = Usage('''
Record audio samples for use with precise
:-w --width int 2
Sample width of audio
:-r --rate int 16000
Sample rate of audio
:-c --channels int 1
Number of audio channels
''')
usage.add_argument('file_label', nargs='?', help='File label (Ex. recording-##)')
def __init__(self, args):
super().__init__(args)
self.orig_settings = tcgetattr(stdin)
self.p = PyAudio()
def key_pressed(self):
return select([stdin], [], [], 0) == ([stdin], [], [])
def show_input(self):
tcsetattr(stdin, TCSADRAIN, self.orig_settings)
def hide_input(self):
tty.setcbreak(stdin.fileno())
def next_name(self, name):
name += '.wav'
pos, num_digits = None, None
try:
@ -110,52 +103,49 @@ def next_name(name):
return get_name(i)
def wait_to_continue():
def wait_to_continue(self):
while True:
c = stdin.read(1)
if c == RECORD_KEY:
if c == self.RECORD_KEY:
return True
elif ord(c) == EXIT_KEY_CODE:
elif ord(c) == self.EXIT_KEY_CODE:
return False
def record_until_key(p, args):
def record_until_key(self):
def should_return():
return key_pressed() and stdin.read(1) == RECORD_KEY
return self.key_pressed() and stdin.read(1) == self.RECORD_KEY
return record_until(p, should_return, args)
return record_until(self.p, should_return, self.args)
def _main():
parser = create_parser(usage)
parser.add_argument('file_label', nargs='?', help='File label (Ex. recording-##)')
args = parser.parse_args()
show_input()
def _run(self):
args = self.args
self.show_input()
args.file_label = args.file_label or input("File label (Ex. recording-##): ")
args.file_label = args.file_label + ('' if '#' in args.file_label else '-##')
hide_input()
p = pyaudio.PyAudio()
self.hide_input()
while True:
print('Press space to record (esc to exit)...')
if not wait_to_continue():
if not self.wait_to_continue():
break
print('Recording...')
d = record_until_key(p, args)
name = next_name(args.file_label)
d = self.record_until_key()
name = self.next_name(args.file_label)
save_audio(name, d, args)
print('Saved as ' + name)
p.terminate()
def run(self):
try:
self.hide_input()
self._run()
finally:
tcsetattr(stdin, TCSADRAIN, self.orig_settings)
self.p.terminate()
def main():
termios_wrapper(_main)
main = CollectScript.run_main
if __name__ == '__main__':
main()

View File

@ -15,10 +15,14 @@
# limitations under the License.
import os
from os.path import split, isfile
from prettyparse import create_parser
from prettyparse import Usage
from shutil import copyfile
usage = '''
from precise.scripts.base_script import BaseScript
class ConvertScript(BaseScript):
usage = Usage('''
Convert wake word model from Keras to TensorFlow
:model str
@ -26,10 +30,14 @@ usage = '''
:-o --out str {model}.pb
Custom output TensorFlow protobuf filename
'''
''')
def run(self):
args = self.args
model_name = args.model.replace('.net', '')
self.convert(args.model, args.out.format(model=model_name))
def convert(model_path: str, out_file: str):
def convert(self, model_path: str, out_file: str):
"""
Converts an HD5F file from Keras to a .pb for use with TensorFlow
@ -76,12 +84,7 @@ def convert(model_path: str, out_file: str):
del sess
def main():
args = create_parser(usage).parse_args()
model_name = args.model.replace('.net', '')
convert(args.model, args.out.format(model=model_name))
main = ConvertScript.run_main
if __name__ == '__main__':
main()

View File

@ -15,12 +15,19 @@
import sys
import os
from prettyparse import create_parser
from prettyparse import Usage
from precise import __version__
from precise.network_runner import Listener
from precise.scripts.base_script import BaseScript
usage = '''
def add_audio_pipe_to_parser(parser):
parser.usage = parser.format_usage().strip().replace('usage: ', '') + ' < audio.wav'
class EngineScript(BaseScript):
usage = Usage('''
stdin should be a stream of raw int16 audio, written in
groups of CHUNK_SIZE samples. If no CHUNK_SIZE is given
it will read until EOF. For every chunk, an inference
@ -30,26 +37,23 @@ usage = '''
Keras or TensorFlow model to read from
...
'''
''')
usage.add_argument('-v', '--version', action='version', version=__version__)
usage.add_argument('chunk_size', type=int, nargs='?', default=-1,
help='Number of bytes to read before making a prediction. '
'Higher values are less computationally expensive')
usage.add_customizer(add_audio_pipe_to_parser)
def __init__(self, args):
super().__init__(args)
if sys.stdin.isatty():
raise ValueError('Please pipe audio via stdin using < audio.wav')
def main():
def run(self):
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
stdout = sys.stdout
sys.stdout = sys.stderr
parser = create_parser(usage)
parser.add_argument('-v', '--version', action='version', version=__version__)
parser.add_argument('chunk_size', type=int, nargs='?', default=-1,
help='Number of bytes to read before making a prediction.'
'Higher values are less computationally expensive')
parser.usage = parser.format_usage().strip().replace('usage: ', '') + ' < audio.wav'
args = parser.parse_args()
if sys.stdin.isatty():
parser.error('Please pipe audio via stdin using < audio.wav')
listener = Listener(args.model_name, args.chunk_size)
listener = Listener(self.args.model_name, self.args.chunk_size)
try:
while True:
@ -60,5 +64,7 @@ def main():
pass
main = EngineScript.run_main
if __name__ == '__main__':
main()

View File

@ -14,22 +14,24 @@
# limitations under the License.
import json
from os.path import isfile, isdir
from prettyparse import create_parser
from prettyparse import Usage
from precise.network_runner import Listener
from precise.params import inject_params
from precise.pocketsphinx.listener import PocketsphinxListener
from precise.pocketsphinx.scripts.test import test_pocketsphinx
from precise.pocketsphinx.scripts.test import PocketsphinxTestScript
from precise.scripts.base_script import BaseScript
from precise.stats import Stats
from precise.train_data import TrainData
usage = '''
class EvalScript(BaseScript):
usage = Usage('''
Evaluate a list of models on a dataset
:-u --use-train
Evaluate training data instead of test data
:-t --threshold floaat 0.5
:-t --threshold float 0.5
Network output to be considered an activation
:-pw --pocketsphinx-wake-word str -
@ -39,7 +41,7 @@ usage = '''
:-pd --pocketsphinx-dict str -
Optional word dictionary used to
generate a Pocketsphinx data point
Format: wake-word.yy-mm-dd.dict
Format = wake-word.yy-mm-dd.dict
:-pf --pocketsphinx-folder str -
Optional hmm folder used to
@ -53,38 +55,45 @@ usage = '''
Output json file
...
'''
def main():
parser = create_parser(usage)
parser.add_argument('models', nargs='*',
''')
usage.add_argument('models', nargs='*',
help='List of model filenames in format: wake-word.yy-mm-dd.net')
args = TrainData.parse_args(parser)
usage |= TrainData.usage
def __init__(self, args):
super().__init__(args)
if not (
bool(args.pocketsphinx_dict) ==
bool(args.pocketsphinx_folder) ==
bool(args.pocketsphinx_wake_word)
):
parser.error('Must pass all or no Pocketsphinx arguments')
raise ValueError('Must pass all or no Pocketsphinx arguments')
self.is_pocketsphinx = bool(args.pocketsphinx_dict)
if self.is_pocketsphinx:
if not isfile(args.pocketsphinx_dict):
raise ValueError('No such file: ' + args.pocketsphinx_dict)
if not isdir(args.pocketsphinx_folder):
raise ValueError('No such folder: ' + args.pocketsphinx_folder)
def run(self):
args = self.args
data = TrainData.from_both(args.tags_file, args.tags_folder, args.folder)
data_files = data.train_files if args.use_train else data.test_files
print('Data:', data)
metrics = {}
if args.pocketsphinx_dict and args.pocketsphinx_folder and args.pocketsphinx_wake_word:
if not isfile(args.pocketsphinx_dict):
parser.error('No such file: ' + args.pocketsphinx_dict)
if not isdir(args.pocketsphinx_folder):
parser.error('No such folder: ' + args.pocketsphinx_folder)
listener = PocketsphinxListener(
args.pocketsphinx_wake_word, args.pocketsphinx_dict,
args.pocketsphinx_folder, args.pocketsphinx_threshold
if self.is_pocketsphinx:
script = PocketsphinxTestScript.create(
key_phrase=args.pocketsphinx_wake_word, dict_file=args.pocketsphinx_dict,
hmm_folder=args.pocketsphinx_folder, threshold=args.pocketsphinx_threshold
)
stats = test_pocketsphinx(listener, data_files)
metrics[args.pocketsphinx_dict] = stats_to_dict(stats)
ww_files, nww_files = data_files
script.run_test(ww_files, 'Wake Word', 1.0)
script.run_test(nww_files, 'Not Wake Word', 0.0)
stats = script.get_stats()
metrics[args.pocketsphinx_dict] = stats.to_dict(args.threshold)
for model_name in args.models:
print('Calculating', model_name + '...')
@ -108,5 +117,7 @@ def main():
json.dump(metrics, f)
main = EvalScript.run_main
if __name__ == '__main__':
main()

View File

@ -12,47 +12,19 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License
import numpy as np
from functools import partial
from os.path import basename, splitext
from prettyparse import create_parser
from prettyparse import Usage
from typing import Callable, Tuple
from precise.network_runner import Listener
from precise.params import inject_params, pr
from precise.scripts.base_script import BaseScript
from precise.stats import Stats
from precise.threshold_decoder import ThresholdDecoder
from precise.train_data import TrainData
usage = '''
Show ROC curves for a series of models
...
:-t --use-train
Evaluate training data instead of test data
:-nf --no-filenames
Don't print out the names of files that failed
:-r --resolution int 100
Number of points to generate
:-p --power float 3.0
Power of point distribution
:-l --labels
Print labels attached to each point
:-o --output-file str -
File to write data instead of displaying it
:-i --input-file str -
File to read data from and visualize
...
'''
def get_thresholds(points=100, power=3) -> list:
"""Run a function with a series of thresholds between 0 and 1"""
@ -108,19 +80,50 @@ def calc_stats(model_files, loader, use_train, filenames):
return model_data
def main():
parser = create_parser(usage)
parser.add_argument('models', nargs='*', help='Either Keras (.net) or TensorFlow (.pb) models to test')
args = TrainData.parse_args(parser)
class GraphScript(BaseScript):
usage = Usage('''
Show ROC curves for a series of models
...
:-t --use-train
Evaluate training data instead of test data
:-nf --no-filenames
Don't print out the names of files that failed
:-r --resolution int 100
Number of points to generate
:-p --power float 3.0
Power of point distribution
:-l --labels
Print labels attached to each point
:-o --output-file str -
File to write data instead of displaying it
:-i --input-file str -
File to read data from and visualize
...
''')
usage.add_argument('models', nargs='*', help='Either Keras (.net) or TensorFlow (.pb) models to test')
usage |= TrainData.usage
def __init__(self, args):
super().__init__(args)
if not args.models and not args.input_file and args.folder:
args.input_file = args.folder
if bool(args.models) == bool(args.input_file):
parser.error('Please specify either a list of models or an input file')
raise ValueError('Please specify either a list of models or an input file')
if not args.output_file:
load_plt() # Error early if matplotlib not installed
import numpy as np
def run(self):
args = self.args
if args.models:
data = TrainData.from_both(args.tags_file, args.tags_folder, args.folder)
print('Data:', data)
@ -156,5 +159,7 @@ def main():
plt.show()
main = GraphScript.run_main
if __name__ == '__main__':
main()

View File

@ -14,17 +14,20 @@
# limitations under the License.
import numpy as np
from os.path import join
from prettyparse import create_parser
from precise_runner import PreciseRunner
from precise_runner.runner import ListenerEngine
from prettyparse import Usage
from random import randint
from shutil import get_terminal_size
from threading import Event
from precise.network_runner import Listener
from precise.scripts.base_script import BaseScript
from precise.util import save_audio, buffer_to_audio, activate_notify
from precise_runner import PreciseRunner
from precise_runner.runner import ListenerEngine
usage = '''
class ListenScript(BaseScript):
usage = Usage('''
Run a model on microphone audio input
:model str
@ -47,52 +50,50 @@ usage = '''
:-p --save-prefix str -
Prefix for saved filenames
'''
''')
session_id, chunk_num = '%09d' % randint(0, 999999999), 0
def __init__(self, args):
super().__init__(args)
self.listener = Listener(args.model, args.chunk_size)
self.audio_buffer = np.zeros(self.listener.pr.buffer_samples, dtype=float)
self.engine = ListenerEngine(self.listener, args.chunk_size)
self.engine.get_prediction = self.get_prediction
self.runner = PreciseRunner(self.engine, args.trigger_level, sensitivity=args.sensitivity,
on_activation=self.on_activation, on_prediction=self.on_prediction)
self.session_id, self.chunk_num = '%09d' % randint(0, 999999999), 0
def main():
args = create_parser(usage).parse_args()
def on_activation():
def on_activation(self):
activate_notify()
if args.save_dir:
global chunk_num
nm = join(args.save_dir, args.save_prefix + session_id + '.' + str(chunk_num) + '.wav')
save_audio(nm, audio_buffer)
if self.args.save_dir:
nm = join(self.args.save_dir, self.args.save_prefix + self.session_id + '.' + str(self.chunk_num) + '.wav')
save_audio(nm, self.audio_buffer)
print()
print('Saved to ' + nm + '.')
chunk_num += 1
self.chunk_num += 1
def on_prediction(conf):
if args.basic_mode:
def on_prediction(self, conf):
if self.args.basic_mode:
print('!' if conf > 0.7 else '.', end='', flush=True)
else:
max_width = 80
width = min(get_terminal_size()[0], max_width)
units = int(round(conf * width))
bar = 'X' * units + '-' * (width - units)
cutoff = round((1.0 - args.sensitivity) * width)
cutoff = round((1.0 - self.args.sensitivity) * width)
print(bar[:cutoff] + bar[cutoff:].replace('X', 'x'))
listener = Listener(args.model, args.chunk_size)
audio_buffer = np.zeros(listener.pr.buffer_samples, dtype=float)
def get_prediction(chunk):
nonlocal audio_buffer
def get_prediction(self, chunk):
audio = buffer_to_audio(chunk)
audio_buffer = np.concatenate((audio_buffer[len(audio):], audio))
return listener.update(chunk)
self.audio_buffer = np.concatenate((self.audio_buffer[len(audio):], audio))
return self.listener.update(chunk)
engine = ListenerEngine(listener, args.chunk_size)
engine.get_prediction = get_prediction
runner = PreciseRunner(engine, args.trigger_level, sensitivity=args.sensitivity,
on_activation=on_activation, on_prediction=on_prediction)
runner.start()
def run(self):
self.runner.start()
Event().wait() # Wait forever
main = ListenScript.run_main
if __name__ == '__main__':
main()

View File

@ -16,30 +16,14 @@ import attr
import numpy as np
from glob import glob
from os.path import join, basename
from prettyparse import create_parser
from precise_runner.runner import TriggerDetector
from prettyparse import Usage
from precise.network_runner import Listener
from precise.params import pr, inject_params
from precise.scripts.base_script import BaseScript
from precise.util import load_audio
from precise.vectorization import vectorize_raw
from precise_runner.runner import TriggerDetector
usage = '''
Simulate listening to long chunks of audio to find
unbiased false positive metrics
:model str
Either Keras (.net) or TensorFlow (.pb) model to test
:folder str
Folder with a set of long wav files to test against
:-c --chunk_size int 4096
Number of samples between tests
:-t --threshold float 0.5
Network output required to be considered an activation
'''
@attr.s()
@ -80,9 +64,26 @@ class Metric:
)
class Simulator:
def __init__(self):
self.args = create_parser(usage).parse_args()
class SimulateScript(BaseScript):
usage = Usage('''
Simulate listening to long chunks of audio to find
unbiased false positive metrics
:model str
Either Keras (.net) or TensorFlow (.pb) model to test
:folder str
Folder with a set of long wav files to test against
:-c --chunk_size int 4096
Number of samples between tests
:-t --threshold float 0.5
Network output required to be considered an activation
''')
def __init__(self, args):
super().__init__(args)
inject_params(self.args.model)
self.runner = Listener.find_runner(self.args.model)(self.args.model)
self.audio_buffer = np.zeros(pr.buffer_samples, dtype=float)
@ -127,9 +128,7 @@ class Simulator:
print(total.info_string('Total'))
def main():
Simulator().run()
main = SimulateScript.run_main
if __name__ == '__main__':
main()

View File

@ -12,14 +12,17 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from prettyparse import create_parser
from prettyparse import Usage
from precise.network_runner import Listener
from precise.params import inject_params
from precise.scripts.base_script import BaseScript
from precise.stats import Stats
from precise.train_data import TrainData
usage = '''
class TestScript(BaseScript):
usage = Usage('''
Test a model against a dataset
:model str
@ -35,14 +38,11 @@ usage = '''
Network output required to be considered an activation
...
'''
def main():
args = TrainData.parse_args(create_parser(usage))
''') | TrainData.usage
def run(self):
args = self.args
inject_params(args.model)
data = TrainData.from_both(args.tags_file, args.tags_folder, args.folder)
train, test = data.load(args.use_train, not args.use_train, shuffle=False)
inputs, targets = train if args.use_train else test
@ -67,5 +67,7 @@ def main():
print(stats.summary_str(args.threshold))
main = TestScript.run_main
if __name__ == '__main__':
main()

View File

@ -12,21 +12,21 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from argparse import ArgumentParser
from fitipy import Fitipy
from keras.callbacks import LambdaCallback
from os.path import splitext, isfile
from prettyparse import add_to_parser
from prettyparse import Usage
from typing import Any, Tuple
from precise.model import create_model, ModelParams
from precise.params import inject_params, save_params
from precise.scripts.base_script import BaseScript
from precise.train_data import TrainData
from precise.util import calc_sample_hash
class Trainer:
usage = '''
class TrainScript(BaseScript):
usage = Usage('''
Train a new model on a dataset
:model str
@ -66,20 +66,17 @@ class Trainer:
Can be negative to wrap from end
...
'''
''') | TrainData.usage
def __init__(self, parser=None):
parser = parser or ArgumentParser()
add_to_parser(parser, self.usage, True)
args = TrainData.parse_args(parser)
self.args = args = self.process_args(args) or args
def __init__(self, args):
super().__init__(args)
if args.invert_samples and not args.samples_file:
parser.error('You must specify --samples-file when using --invert-samples')
raise ValueError('You must specify --samples-file when using --invert-samples')
if args.samples_file and not isfile(args.samples_file):
parser.error('No such file: ' + (args.invert_samples or args.samples_file))
raise ValueError('No such file: ' + (args.invert_samples or args.samples_file))
if not 0.0 <= args.sensitivity <= 1.0:
parser.error('sensitivity must be between 0.0 and 1.0')
raise ValueError('sensitivity must be between 0.0 and 1.0')
inject_params(args.model)
save_params(args.model)
@ -94,7 +91,7 @@ class Trainer:
epoch_fiti = Fitipy(splitext(args.model)[0] + '.epoch')
self.epoch = epoch_fiti.read().read(0, int)
def on_epoch_end(a, b):
def on_epoch_end(_a, _b):
self.epoch += 1
epoch_fiti.write().write(self.epoch, str)
@ -112,10 +109,6 @@ class Trainer:
), LambdaCallback(on_epoch_end=on_epoch_end)
]
def process_args(self, args: Any) -> Any:
"""Override to modify args"""
pass
@staticmethod
def load_sample_data(filename, train_data) -> Tuple[set, dict]:
samples = Fitipy(filename).read().set()
@ -163,20 +156,15 @@ class Trainer:
def run(self):
self.model.summary()
try:
train_inputs, train_outputs = self.sampled_data
self.model.fit(
train_inputs, train_outputs, self.args.batch_size,
self.epoch + self.args.epochs, validation_data=self.test,
initial_epoch=self.epoch, callbacks=self.callbacks
)
except KeyboardInterrupt:
print()
def main():
Trainer().run()
main = TrainScript.run_main
if __name__ == '__main__':
main()

View File

@ -20,17 +20,20 @@ from contextlib import suppress
from fitipy import Fitipy
from keras.callbacks import LambdaCallback
from os.path import splitext, join, basename
from prettyparse import create_parser
from prettyparse import Usage
from random import random, shuffle
from typing import *
from precise.model import create_model, ModelParams
from precise.network_runner import Listener
from precise.params import pr, save_params
from precise.scripts.base_script import BaseScript
from precise.train_data import TrainData
from precise.util import load_audio, glob_all, save_audio, chunk_audio
usage = '''
class TrainGeneratedScript(BaseScript):
usage = Usage('''
Train a model on infinitely generated batches
:model str
@ -72,14 +75,11 @@ usage = '''
Probability of saving audio into debug/ww and debug/nww folders
...
'''
class GeneratedTrainer:
''') | TrainData.usage
"""A trainer the runs on generated data by overlaying wakewords on background audio"""
def __init__(self):
parser = create_parser(usage)
self.args = args = TrainData.parse_args(parser)
def __init__(self, args):
super().__init__(args)
self.audio_buffer = np.zeros(pr.buffer_samples, dtype=float)
self.vals_buffer = np.zeros(pr.buffer_samples, dtype=float)
@ -96,7 +96,7 @@ class GeneratedTrainer:
epoch_fiti = Fitipy(splitext(args.model)[0] + '.epoch')
self.epoch = epoch_fiti.read().read(0, int)
def on_epoch_end(a, b):
def on_epoch_end(_a, _b):
self.epoch += 1
epoch_fiti.write().write(self.epoch, str)
@ -228,7 +228,8 @@ class GeneratedTrainer:
_, test_data = self.data.load(train=False, test=True)
try:
self.model.fit_generator(
self.samples_to_batches(self.generate_samples(), self.args.batch_size), steps_per_epoch=self.args.steps_per_epoch,
self.samples_to_batches(self.generate_samples(), self.args.batch_size),
steps_per_epoch=self.args.steps_per_epoch,
epochs=self.epoch + self.args.epochs, validation_data=test_data,
callbacks=self.callbacks, initial_epoch=self.epoch
)
@ -237,12 +238,7 @@ class GeneratedTrainer:
save_params(self.args.model)
def main():
try:
GeneratedTrainer().run()
except KeyboardInterrupt:
print()
main = TrainGeneratedScript.run_main
if __name__ == '__main__':
main()

View File

@ -15,18 +15,34 @@
import numpy as np
from os import makedirs
from os.path import basename, splitext, isfile, join
from prettyparse import create_parser
from prettyparse import Usage
from random import random
from typing import *
from precise.model import create_model, ModelParams
from precise.network_runner import Listener, KerasRunner
from precise.params import pr
from precise.scripts.train import TrainScript
from precise.train_data import TrainData
from precise.scripts.train import Trainer
from precise.util import load_audio, save_audio, glob_all, chunk_audio
usage = '''
def load_trained_fns(model_name: str) -> list:
progress_file = model_name.replace('.net', '') + '.trained.txt'
if isfile(progress_file):
print('Starting from saved position in', progress_file)
with open(progress_file, 'rb') as f:
return f.read().decode('utf8', 'surrogatepass').split('\n')
return []
def save_trained_fns(trained_fns: list, model_name: str):
with open(model_name.replace('.net', '') + '.trained.txt', 'wb') as f:
f.write('\n'.join(trained_fns).encode('utf8', 'surrogatepass'))
class TrainIncrementalScript(TrainScript):
usage = Usage('''
Train a model to inhibit activation by
marking false activations and retraining
@ -47,26 +63,10 @@ usage = '''
Network output to be considered activated
...
'''
''') | TrainScript.usage
def load_trained_fns(model_name: str) -> list:
progress_file = model_name.replace('.net', '') + '.trained.txt'
if isfile(progress_file):
print('Starting from saved position in', progress_file)
with open(progress_file, 'rb') as f:
return f.read().decode('utf8', 'surrogatepass').split('\n')
return []
def save_trained_fns(trained_fns: list, model_name: str):
with open(model_name.replace('.net', '') + '.trained.txt', 'wb') as f:
f.write('\n'.join(trained_fns).encode('utf8', 'surrogatepass'))
class IncrementalTrainer(Trainer):
def __init__(self):
super().__init__(create_parser(usage))
def __init__(self, args):
super().__init__(args)
for i in (
join(self.args.folder, 'not-wake-word', 'generated'),
@ -153,12 +153,7 @@ class IncrementalTrainer(Trainer):
save_trained_fns(self.trained_fns, self.args.model)
def main():
try:
IncrementalTrainer().run()
except KeyboardInterrupt:
print()
main = TrainIncrementalScript.run_main
if __name__ == '__main__':
main()

View File

@ -12,25 +12,24 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import re
from glob import glob
from os import remove
from os.path import isfile, splitext, join
import numpy
# Optimizer blackhat
from bbopt import BlackBoxOptimizer
from glob import glob
from os import remove
from os.path import isfile, splitext, join
from pprint import pprint
from prettyparse import create_parser
from prettyparse import Usage
from shutil import rmtree
from typing import Any
from precise.model import ModelParams, create_model
from precise.scripts.train import TrainScript
from precise.train_data import TrainData
from precise.scripts.train import Trainer
usage = '''
class TrainOptimizeScript(TrainScript):
Usage('''
Use black box optimization to tune model hyperparameters
:-t --trials-name str -
@ -42,15 +41,12 @@ usage = '''
:-m --model str .cache/optimized.net
Model to load from
...
'''
''') | TrainScript.usage
class OptimizeTrainer(Trainer):
usage = re.sub(r'.*:model str.*\n.*\n', '', Trainer.usage)
def __init__(self):
super().__init__(create_parser(usage))
def __init__(self, args):
super().__init__(args)
self.bb = BlackBoxOptimizer(file=self.args.trials_name)
if not self.test:
data = TrainData.from_both(self.args.tags_file, self.args.tags_folder, self.args.folder)
@ -128,9 +124,7 @@ class OptimizeTrainer(Trainer):
pprint(best_example)
def main():
OptimizeTrainer().run()
main = TrainOptimizeScript.run_main
if __name__ == '__main__':
main()

View File

@ -15,12 +15,14 @@
from itertools import islice
from fitipy import Fitipy
from prettyparse import create_parser
from prettyparse import Usage
from precise.scripts.train import Trainer
from precise.scripts.train import TrainScript
from precise.util import calc_sample_hash
usage = '''
class TrainSampledScript(TrainScript):
usage = Usage('''
Train a model, sampling data points with the highest loss from a larger dataset
:-c --cycles int 200
@ -31,20 +33,17 @@ usage = '''
:-sf --samples-file str -
Json file to write selected samples to.
Default: {model_base}.samples.json
Default = {model_base}.samples.json
:-is --invert-samples
Unused parameter
...
'''
''') | TrainScript.usage
class SampledTrainer(Trainer):
def __init__(self):
parser = create_parser(usage)
super().__init__(parser)
def __init__(self, args):
super().__init__(args)
if self.args.invert_samples:
parser.error('--invert-samples should be left blank')
raise ValueError('--invert-samples should be left blank')
self.args.samples_file = (self.args.samples_file or '{model_base}.samples.json').format(
model_base=self.model_base
)
@ -90,9 +89,7 @@ class SampledTrainer(Trainer):
)
def main():
SampledTrainer().run()
main = TrainSampledScript.run_main
if __name__ == '__main__':
main()

View File

@ -13,11 +13,10 @@
# limitations under the License.
import json
import numpy as np
from argparse import ArgumentParser
from glob import glob
from hashlib import md5
from os.path import join, isfile
from prettyparse import add_to_parser
from prettyparse import Usage
from pyache import Pyache
from typing import *
@ -27,6 +26,20 @@ from precise.vectorization import vectorize_delta, vectorize
class TrainData:
"""Class to handle loading of wave data from categorized folders and tagged text files"""
usage = Usage('''
:folder str
Folder to load wav files from
:-tf --tags-folder str {folder}
Specify a different folder to load file ids
in tags file from
:-tg --tags-file str -
Text file to load tags from where each line is
<file_id> TAB (wake-word|not-wake-word) and
{folder}/<file_id>.wav exists
''', tags_folder=lambda args: args.tags_folder.format(folder=args.folder))
def __init__(self, train_files: Tuple[List[str], List[str]],
test_files: Tuple[List[str], List[str]]):
@ -144,28 +157,6 @@ class TrainData:
def merge(data_a: tuple, data_b: tuple) -> tuple:
return np.concatenate((data_a[0], data_b[0])), np.concatenate((data_a[1], data_b[1]))
@staticmethod
def parse_args(parser: ArgumentParser) -> Any:
"""Return parsed args from parser, adding options for train data inputs"""
extra_usage = '''
:folder str
Folder to load wav files from
:-tf --tags-folder str {folder}
Specify a different folder to load file ids
in tags file from
:-tg --tags-file str -
Text file to load tags from where each line is
<file_id> TAB (wake-word|not-wake-word) and
{folder}/<file_id>.wav exists
'''
add_to_parser(parser, extra_usage)
args = parser.parse_args()
args.tags_folder = args.tags_folder.format(folder=args.folder)
return args
def __repr__(self) -> str:
string = '<TrainData wake_words={kws} not_wake_words={nkws}' \
' test_wake_words={test_kws} test_not_wake_words={test_nkws}>'
@ -203,6 +194,7 @@ class TrainData:
def on_loop():
on_loop.i += 1
print('\r{0:.2%} '.format(on_loop.i / len(filenames)), end='', flush=True)
on_loop.i = 0
new_inputs = cache.load(filenames, on_loop=on_loop)

View File

@ -78,7 +78,7 @@ setup(
'h5py',
'wavio',
'typing',
'prettyparse<1.0',
'prettyparse>=1.1.0',
'precise-runner',
'attrs',
'fitipy<1.0',