Skip to content

Commit

Permalink
Merge branch 'main' into bundling
Browse files Browse the repository at this point in the history
  • Loading branch information
aabounegm committed Mar 8, 2021
2 parents bf681f3 + 31198be commit 70c6bd3
Show file tree
Hide file tree
Showing 20 changed files with 174 additions and 174 deletions.
57 changes: 4 additions & 53 deletions documents/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,13 @@ duration (in seconds).

## Data collection

To start recording the data, use the `record` option:
To launch the application, simply start the executable without any parameters:

```
$ vpt record
$ vpt
```

This will start a recording session. You can minimize the command line and start working.

To stop recording, use `Ctrl+C`. Your data will be saved and you can continue recording more data at any time.
This will open a window with live graphs of data and buttons to start/stop recording. You may minimize the window and start working now.

If the recording process unexpectedly terminates, the data will still be saved.

Expand All @@ -59,25 +57,9 @@ To select an audio source for recording, use `--audio <NAME>` for specifying the
specifying the video source:

```
$ vpt record --audio AudioSource2 --video "Video source 2"
```

## Generating reports

Once you have collected some data, you may generate a PDF report with the `report` option:

```
$ vpt report
$ vpt --audio AudioSource2 --video "Video source 2"
```

If you wish to only analyze a part of the data, you can specify the `--start <TIME>` and `--end <TIME>` with a time
point in the format `YYYY.MM.DD HH:MM:SS`:

```
$ vpt report --start "2021-01-01 00:00:00" --end "2021-01-07 23:59:59"
```

Omitting the `--start` will select the all the data up to `--end` and vice versa.

## Obtaining raw data for analysis

Expand All @@ -95,34 +77,3 @@ To dump the data into a different file, use the `-o` parameter:
```
$ vpt dump -o for-analysis.sql
```

## Clearing old data

To clear the data collected by the program, use the `clear` option:

```
$ vpt clear
Warning! You are about to delete the data from 2021-01-01 13:37:00 to 2021-02-01 13:38:00.
Are you sure (y/N)?
```

Type `y` and hit `Enter` to confirm deletion.

If you only wish to delete a part of the data, you may constrain the range of deletion with `--start <TIME>`
and `--end <TIME>` options with a time point in the format `YYYY.MM.DD HH:MM:SS`:

```
$ vpt clear --start "2021-01-01 13:37:00" --end "2021-01-02 13:37:00"
Warning! You are about to delete the data from 2021-01-01 13:37:00 to 2021-01-02 13:37:00.
Are you sure (y/N)?
```










6 changes: 3 additions & 3 deletions vpt/__main__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Main entry point for the package"""
from vpt.cli.record import record
from vpt.cli.check import check
from vpt.cli.cli import parse_args
from vpt.cli.sources import print_audio_inputs, print_video_inputs, get_default_audio_input_name
Expand All @@ -14,5 +13,6 @@
elif args['cmd'] == 'check':
check()
elif args['cmd'] is None:
# If not argument, fall back to recording
record()
# If no argument, fall back to recording
# record(args['audio'], args['video'])
pass
87 changes: 4 additions & 83 deletions vpt/cli/check.py
Original file line number Diff line number Diff line change
@@ -1,100 +1,21 @@
'''A group of functions to make sure the hardware is correctly functioning
and recognized by the program.'''
import json
import time

import keyboard
import mouse
import sounddevice as sd
import soundfile as sf

from vpt.processors.gaze_detector import GazeDetector
from vpt.sinks import VideoDisplay, FileStore
from vpt.sources import DeviceVideoSource, KeyboardSource, MouseSource, DeviceAudioSource


def record_audio(device=None, duration=5, filename='vpt-audio.wav'):
'''Records audio from the given device and saves it to disk.
Note that it currently blocks the main thread for the given duration.'''
if device is None:
device = sd.query_devices(kind='input')
else:
device = sd.query_devices(device)

samplerate = int(device['default_samplerate'])
channels = min(2, device['max_input_channels'])

sd.default.samplerate = samplerate
sd.default.channels = channels

data = sd.rec(int(samplerate * duration))
sd.wait()
with sf.SoundFile(filename, mode='w', samplerate=samplerate, channels=channels) as file:
file.write(data)


def record_mouse(duration=5, filename='vpt-mouse.json'):
'''Records all mouse events and saves them to a JSON file.
Note that it currently blocks the main thread for the given duration.'''
events = []

def callback(event):
if isinstance(event, mouse.ButtonEvent):
events.append({
'type': 'button',
'button': event.button,
'action': event.event_type,
'time': event.time,
})
elif isinstance(event, mouse.WheelEvent):
events.append({
'type': 'wheel',
'delta': event.delta,
'time': event.time,
})
elif isinstance(event, mouse.MoveEvent):
events.append({
'type': 'move',
'position': {
'x': event.x,
'y': event.y,
},
'time': event.time,
})

mouse.hook(callback)
time.sleep(duration)
mouse.unhook(callback)
with open(filename, 'w') as file:
json.dump(events, file, indent=4)


def record_keyboard(duration=5, filename='vpt-keyboard.json'):
'''Record all keyboard events and save them to a json file
Note that it currently blocks the main thread for the given duration.'''
events = []

def callback(event: keyboard.KeyboardEvent):
events.append({
'character': event.name,
'key_code': event.scan_code,
'time': event.time,
})

keyboard.hook(callback)
time.sleep(duration)
keyboard.unhook(callback)
with open(filename, 'w') as file:
json.dump(events, file, indent=4)
from vpt.cli.cli import parse_args


def check():
'''Runs all of the recorders to check that everything works correctly.'''
args = parse_args(audio_default=sd.query_devices(kind='input')['name'])
print('Checking the devices for 5s...')

# Create capture nodes
video_source = DeviceVideoSource()
audio_source = DeviceAudioSource()
video_source = DeviceVideoSource(int(args['video']))
audio_source = DeviceAudioSource(args['audio'])
keyboard_source = KeyboardSource()
mouse_source = MouseSource()

Expand Down
41 changes: 14 additions & 27 deletions vpt/cli/cli.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,42 @@
'''CLI'''
from argparse import ArgumentParser
from datetime import datetime
from typing import Sequence


def create_parser(audio_default=None):
'''Creates and returns a parser for command line arguments.'''
# pylint: disable=unused-variable; They are created for consistency and clarity

device_parser = ArgumentParser(add_help=False)
device_parser.add_argument('--audio',
default=audio_default,
help='The name of the audio source, as reported by the '
'"sources audio" subcommand')
device_parser.add_argument('--video',
default=0,
help='The name of the audio source, as reported by the '
'"sources video" subcommand')

parser = ArgumentParser(prog='vpt',
parents=[device_parser],
description="A research tool that allows collecting audio/video data "
"to predict a person's state of work engagement and "
"correlate that data with that person's keyboard/mouse "
"activity")
subparsers = parser.add_subparsers(dest='cmd')
subparsers = parser.add_subparsers(dest='cmd', required=False)

check_parser = subparsers.add_parser('check',
parents=[device_parser],
description='Make sure your hardware is correctly '
'functioning and recognized by the program')
check_parser.add_argument('--audio',
default=audio_default,
help='The name of the audio source, as reported by the '
'"sources audio" subcommand')
check_parser.add_argument('--video',
help='The name of the audio source, as reported by the '
'"sources video" subcommand')
check_parser.add_argument('--duration',
default=5,
type=float,
help='Duration of the recording (in seconds)')

record_parser = subparsers.add_parser('record', description='Start recording the data')
record_parser.add_argument('--audio',
default=audio_default,
help='The name of the audio source, as reported by the '
'"sources audio" subcommand')
record_parser.add_argument('--video',
help='The name of the audio source, as reported by the '
'"sources video" subcommand')

sources_parser = subparsers.add_parser('sources', description='List all available sources')
sources_parser.add_argument('source', choices=['audio', 'video'])

report_parser = subparsers.add_parser('report', description='Generate a PDF report')

dump_parser = subparsers.add_parser('dump',
description='Get a dataset of keyboard/mouse activity '
'along with the predicted work state '
Expand All @@ -51,13 +45,6 @@ def create_parser(audio_default=None):
help='SQLite database file path',
default='vpt-data.sql')

clear_parser = subparsers.add_parser('clear',
description='Clear the data collected by the program')
clear_parser.add_argument('-s', '--start', type=datetime.fromisoformat,
help='Start date of the range whose data to delete')
clear_parser.add_argument('-e', '--end', type=datetime.fromisoformat,
help='Start date of the range whose data to delete')

return parser


Expand Down
17 changes: 12 additions & 5 deletions vpt/processors/gaze_engagement_estimator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,25 @@
from rx import Observable
from rx.subject import Subject

from vpt.processors.base import SourceBase
from vpt.processors.base import ProcessorBase


class GazeEngagementEstimator(ProcessorBase[np.ndarray]):
'''Transforms the gaze estimation to engagement estimation.'''
_subj: Subject

def __init__(self, rotation_vector_source: ProcessorBase[np.ndarray]):
self._subj = Subject()
rotation_vector_source.get_data_stream().subscribe(
lambda v: self._subj.on_next(np.linalg.norm(v))
)
def __init__(self, rotation_vector_source: SourceBase[np.ndarray], threshold=0.5):
self.threshold = threshold
rotation_vector_source.get_data_stream().subscribe(lambda v:
self.subj.on_next(np.linalg.norm(v) < self.threshold))

def start(self):
pass

def stop(self):
pass


def get_data_stream(self) -> Observable:
return self._subj
2 changes: 1 addition & 1 deletion vpt/sinks/file_store/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def __init__(self, dir_name: str, mouse_source: SourceBase,
with contextlib.suppress(FileNotFoundError):
os.remove(self.dir / 'audio.wav')
self.audio_file = sf.SoundFile(self.dir / 'audio.wav',
mode='w', samplerate=44100, channels=1)
mode='w', samplerate=44100, channels=2)
audio_source.get_data_stream().subscribe(self.store_audio_frame)

def store_audio_frame(self, frame: np.ndarray):
Expand Down
19 changes: 17 additions & 2 deletions vpt/sources/device_audio_source/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'''Gets the audio from the device.'''
import threading
from typing import Union

import numpy as np
import sounddevice as sd
Expand All @@ -19,6 +20,11 @@ class DeviceAudioSource(SourceBase[np.ndarray]):
def __init__(self):
self._subj = Subject()

def __init__(self, device: Union[str, int] = None):
super().__init__()
if device is not None:
sd.default.device = device

def get_data_stream(self) -> Observable:
return self._subj

Expand All @@ -27,10 +33,19 @@ def run(self):
while not self.stopped:
rec = sd.rec(int(self.sample_duration * self.sample_rate),
samplerate=self.sample_rate,
channels=1)
sd.wait()
channels=2,
blocking=True)

rec = self.trim_corruption_lol(rec)
self._subj.on_next(rec)

def trim_corruption_lol(self, chunk):
eps = 1e-4
for sample_idx in range(len(chunk)):
if np.any(chunk[sample_idx] > eps):
break
return chunk[int(sample_idx * 1.1):, :]

def start(self):
self.stopped = False
threading.Thread(target=self.run).start()
Expand Down
Loading

0 comments on commit 70c6bd3

Please sign in to comment.