mirror of
https://github.com/aidygus/LinVAM.git
synced 2024-11-23 01:08:06 +11:00
Merge pull request #3 from smirgol/master
Fixes & implementation of playing audio files. See changes.md for det…
This commit is contained in:
commit
a229683c57
3
.gitignore
vendored
3
.gitignore
vendored
@ -127,5 +127,8 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# Ignore voicepack files
|
||||
voicepacks/*
|
||||
|
||||
# End of https://www.gitignore.io/api/python
|
||||
|
||||
|
33
README.md
33
README.md
@ -1,5 +1,6 @@
|
||||
# LinVAM
|
||||
Linux Voice Activated Macro
|
||||
|
||||
## Status
|
||||
This project is currently a work-in-progress and is minimally functional only for english.
|
||||
|
||||
@ -10,6 +11,7 @@ Known bugs and planned additions
|
||||
- Remember last loaded profile and load on start
|
||||
- Log window showing spoken words the V2T recognises with ability to right click and assign voice command and actions to current profile
|
||||
- Support for joysticks and gaming devices
|
||||
|
||||
## Requirements
|
||||
- python3
|
||||
- PyQt5
|
||||
@ -17,20 +19,33 @@ Known bugs and planned additions
|
||||
- pyaudio
|
||||
- pocketsphinx
|
||||
- swig3.0
|
||||
|
||||
## Optional requirements
|
||||
- xdotool
|
||||
- ffplay (part of ffmpeg, usually already installed)
|
||||
- HCS voicepacks
|
||||
|
||||
## Install
|
||||
- $ pip3 install PyQt5
|
||||
- $ pip3 install python3-xlib
|
||||
- $ pip3 install pyaudio
|
||||
- $ pip3 install pocketsphinx
|
||||
- $ sudo apt-get install swig3.0
|
||||
- $ (optional) sudo apt install xdotool
|
||||
- $ (optional) sudo apt install ffmpeg
|
||||
- $ sudo ln -s /usr/bin/swig3.0 /usr/bin/swig
|
||||
- $ git clone https://github.com/aidygus/LinVAM.git
|
||||
|
||||
## Usage
|
||||
This script must be run with root privilege because it must hook and simulate input devices such as keyboard, mouse etc.
|
||||
- $ cd LinVAM
|
||||
- $ xhost +
|
||||
- $ sudo ./main.py
|
||||
|
||||
As an alternative, if you use the X window manager and have xdotool installed, you can run the script like this:
|
||||
- $ cd LinVAM
|
||||
- $ ./main.py -noroot
|
||||
|
||||
### Profiles
|
||||
Multiple profiles are supported. To create a new profile for a specific task/game click new and the main profile editor window will be displayed
|
||||
|
||||
@ -46,3 +61,21 @@ You can also assign mouse movements and system commands if you require (eg openi
|
||||
![Main GUI](https://raw.githubusercontent.com/aidygus/LinVAM/master/.img/complex.png)
|
||||
### Threshold
|
||||
As a rough guide use a value of 10 for each syllable of a word then tweak it down for better accuracy.
|
||||
|
||||
### Output audio
|
||||
In the Command Edit Dialog, chose 'Other' and then 'Play sound'. Pick the sound you would like to play.
|
||||
For this to work you need to copy any audio file you would like to use to the folder 'voicepacks'.
|
||||
You are required to create a subfolder to hold all your audio files (voicepack folder), then within that subfolder, create as many folders as you like to group your audio files (category folders).
|
||||
Place the audio file into these category folders or in any subfolder within a category folder.
|
||||
In theory any audio file should work, but tested only with MP3 files.
|
||||
|
||||
Example:
|
||||
/voicepacks/my voicepack/custom commands/hello.mp3
|
||||
/voicepacks/my voicepack/other/thank you.mp3
|
||||
|
||||
If you own a HCS voicepack, copy the whole voicepack folder (like 'hcspack', 'hcspack-eden', ...) to the 'voicepacks' folder, so it reads like this:
|
||||
/voicepacks/hcspack/...
|
||||
|
||||
### Improve voice recognition accuracy
|
||||
Please see this resource on how to train the acoustic model of pocketsphinx to match your voice:
|
||||
https://cmusphinx.github.io/wiki/tutorialadapt/
|
66
changes.md
Normal file
66
changes.md
Normal file
@ -0,0 +1,66 @@
|
||||
# Changelog smirgol
|
||||
|
||||
### Issues:
|
||||
- (fixed) voice recognition autostarts, there is no way to disable it. the flag "self.m_listening" does nothing
|
||||
- (fixed) when profile is changed, the new commands will not be loaded, as the voice recognition is already running - by default with the Elite Dangerous settings. You cannot use any other profile.
|
||||
- (fixed) "When I say" commands need to be all lower case, otherwise it won't be recognized.
|
||||
|
||||
### Known/New Issues:
|
||||
- When using external speakers it will pick up a playing audio sample and might trigger commands. Use headphones! :)
|
||||
- On a Valve Index headset, the earphones, while being good, will feed the Index microphone causing troubles with the voice recognition.
|
||||
There's probably nothing that can be done about it, maybe some sort of audio filter could be introduced to cut off low-volume sounds?
|
||||
Lower voice audio for now to minimize the issue.
|
||||
- There sometimes is a window error in the console when clicking on a 'cancel' button, does not seem to do harm, but should fix that if possible.
|
||||
- Sometimes keypress signals are not recognized by game, this is the case with xdotool, don't know if this also happens with standard keyboard library.
|
||||
It seems to solve the issue if you at least once manually press any key while the game window is focused
|
||||
|
||||
### Added:
|
||||
- hacked profileexcutor to reload command list when changing it. it did not do that,
|
||||
so changing the profile had no effect at all.
|
||||
- hacked profileexecutor to somewhat make use of "Enable listening".
|
||||
It was an option without functionality at all until now.
|
||||
- created new directory 'voicepacks', copy HCS voicepacks here
|
||||
- new command to play a sound
|
||||
- new gui to select sounds from voicepacks
|
||||
- created playsound class that reads voicepack files and plays audio files with ffplay
|
||||
- added alternate keypress handling using xdotool, which won't require root privileges.
|
||||
might not work for any key existing in profiles, as some need to be remapped. added some remappings to profileexecutor.py pressKey()
|
||||
- added basic command line argument reading to set some configs. right now there:
|
||||
-noroot - will enable xdotool usage for keypresses
|
||||
-xdowindowid <windowid> - will send keypresses to this window only, makes it more relyable when window is not focused for any reason
|
||||
- added auto-detection of Elite Dangerous client window id if -noroot is used and no -xdowindowid is supplied.
|
||||
for this to work, start this script AFTER the client is already running.
|
||||
- monkey-wrenched a volume slider to the main window
|
||||
- added confirmation dialog for removing a profile (!)
|
||||
- copy existing profile. added a copy button to the main menu
|
||||
- properly shutdown audio recording stream
|
||||
- updated gitignore to ignore audio files
|
||||
|
||||
### Dependencies
|
||||
- ffplay
|
||||
- xdotool (optional, won't need root privileges for keypresses)
|
||||
|
||||
Not sure about these, I had them installed already:
|
||||
- import subprocess
|
||||
- import shlex
|
||||
- import signal
|
||||
|
||||
### TODO
|
||||
- select multiple files (from same category) to play one of them randomly
|
||||
|
||||
### FUTURE IDEAS
|
||||
- remember settings and load on start (nice to have)
|
||||
- change key assignment logic to record a keypress / buttonpress combo
|
||||
- will definitely break existing profiles, as we need to record keycodes and stuff
|
||||
- maybe add a state machine like voice attack does
|
||||
- look into reading log files from elite dangerous to trigger certain actions.
|
||||
this would be a very game specific addition though...
|
||||
- make connection to a specific voice pack more loose, so you can easily change the voice pack,
|
||||
without re-editing all the commands. maybe even a different voice pack for certain actions,
|
||||
like in voice attack (crewmen)
|
||||
- reorganize main window to hold more settings and a log window for displaying actions
|
||||
- look into other text2speech engines
|
||||
i gave julius-speech a quick try. its pretty easy to setup and looks promising, but it
|
||||
also has its problems with detecting some "exotic" words. as not being a native
|
||||
english speaker, my pronounciation is not the best sometimes, which doesn't help. :)
|
||||
- look into make recognized words using regex expressions
|
@ -5,6 +5,7 @@ from ui_commandeditwnd import Ui_CommandEditDialog
|
||||
from keyactioneditwnd import KeyActionEditWnd
|
||||
from mouseactioneditwnd import MouseActionEditWnd
|
||||
from pauseactioneditwnd import PauseActionEditWnd
|
||||
from soundactioneditwnd import SoundActionEditWnd
|
||||
import json
|
||||
|
||||
class CommandEditWnd(QDialog):
|
||||
@ -12,6 +13,7 @@ class CommandEditWnd(QDialog):
|
||||
super().__init__(p_parent)
|
||||
self.ui = Ui_CommandEditDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.m_parent = p_parent
|
||||
|
||||
self.ui.deleteBut.clicked.connect(self.slotDelete)
|
||||
self.ui.ok.clicked.connect(self.slotOK)
|
||||
@ -27,6 +29,7 @@ class CommandEditWnd(QDialog):
|
||||
w_otherMenu = QMenu()
|
||||
w_otherMenu.addAction('Stop Another Command', self.slotStopAnotherCommand)
|
||||
w_otherMenu.addAction('Execute Another Command', self.slotDoAnotherCommand)
|
||||
w_otherMenu.addAction('Play Sound', self.slotNewSoundEdit)
|
||||
self.ui.otherBut.setMenu(w_otherMenu)
|
||||
|
||||
self.m_command = {}
|
||||
@ -73,6 +76,14 @@ class CommandEditWnd(QDialog):
|
||||
w_commandDoAction['command name'] = text
|
||||
self.addAction(w_commandDoAction)
|
||||
|
||||
def slotDoPlaySound(self):
|
||||
text, okPressed = QInputDialog.getItem(self, "Set sound to play", "Enter sound file:", list(self.m_parent.m_parent.m_sound.m_sounds), 0, False)
|
||||
if okPressed and text != '':
|
||||
w_commandDoAction = {}
|
||||
w_commandDoAction['name'] = 'command play sound'
|
||||
w_commandDoAction['command name'] = text
|
||||
self.addAction(w_commandDoAction)
|
||||
|
||||
def slotNewKeyEdit(self):
|
||||
w_keyEditWnd = KeyActionEditWnd(None, self)
|
||||
if w_keyEditWnd.exec() == QDialog.Accepted:
|
||||
@ -88,6 +99,11 @@ class CommandEditWnd(QDialog):
|
||||
if w_pauseEditWnd.exec() == QDialog.Accepted:
|
||||
self.addAction(w_pauseEditWnd.m_pauseAction)
|
||||
|
||||
def slotNewSoundEdit(self):
|
||||
w_soundEditWnd = SoundActionEditWnd(self.m_parent.m_parent.m_sound, None, self)
|
||||
if w_soundEditWnd.exec() == QDialog.Accepted:
|
||||
self.addAction(w_soundEditWnd.m_soundAction)
|
||||
|
||||
def slotActionUp(self):
|
||||
currentIndex = self.ui.actionsListWidget.currentRow()
|
||||
currentItem = self.ui.actionsListWidget.takeItem(currentIndex);
|
||||
@ -132,6 +148,10 @@ class CommandEditWnd(QDialog):
|
||||
if okPressed and text != '':
|
||||
w_action['command name'] = text
|
||||
w_jsonAction = json.dumps(w_action)
|
||||
elif w_action['name'] == 'play sound':
|
||||
w_soundEditWnd = SoundActionEditWnd(self.m_parent.m_parent.m_sound, w_action, self)
|
||||
if w_soundEditWnd.exec() == QDialog.Accepted:
|
||||
w_jsonAction = json.dumps(w_soundEditWnd.m_soundAction)
|
||||
|
||||
w_item.setText(w_jsonAction)
|
||||
w_item.setData(Qt.UserRole, w_jsonAction)
|
||||
|
82
main.py
82
main.py
@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python3
|
||||
#!/usr/bin/python3
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
@ -11,21 +11,32 @@ import pickle
|
||||
import signal
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import shlex
|
||||
from soundfiles import SoundFiles
|
||||
|
||||
class MainWnd(QWidget):
|
||||
def __init__(self, p_parent = None):
|
||||
super().__init__(p_parent)
|
||||
|
||||
self.ui = Ui_MainWidget()
|
||||
self.ui.setupUi(self)
|
||||
self.m_profileExecutor = ProfileExecutor()
|
||||
self.handleArgs()
|
||||
self.m_sound = SoundFiles()
|
||||
self.m_profileExecutor = ProfileExecutor(None, self)
|
||||
|
||||
if not os.geteuid() == 0 and not self.m_config['noroot'] == 1:
|
||||
print("\033[93m\nWARNING: no root privileges, unable to send key strokes to the system.\nConsider running this as root.\nFor that you might need to install some python modules for the root user. \n\033[0m")
|
||||
|
||||
self.ui.profileCbx.currentIndexChanged.connect(self.slotProfileChanged)
|
||||
self.ui.addBut.clicked.connect(self.slotAddNewProfile)
|
||||
self.ui.editBut.clicked.connect(self.slotEditProfile)
|
||||
self.ui.copyBut.clicked.connect(self.slotCopyProfile)
|
||||
self.ui.removeBut.clicked.connect(self.slotRemoveProfile)
|
||||
self.ui.listeningChk.stateChanged.connect(self.slotListeningEnabled)
|
||||
self.ui.ok.clicked.connect(self.slotOK)
|
||||
self.ui.cancel.clicked.connect(self.slotCancel)
|
||||
self.ui.sliderVolume.valueChanged.connect(lambda: self.m_sound.setVolume(self.ui.sliderVolume.value()))
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
if self.loadFromDatabase() > 0 :
|
||||
@ -34,7 +45,8 @@ class MainWnd(QWidget):
|
||||
if w_jsonProfile != None:
|
||||
self.m_activeProfile = json.loads(w_jsonProfile)
|
||||
self.m_profileExecutor.setProfile(self.m_activeProfile)
|
||||
self.m_profileExecutor.start()
|
||||
#self.m_profileExecutor.start()
|
||||
|
||||
|
||||
def saveToDatabase(self):
|
||||
w_profiles = []
|
||||
@ -228,7 +240,25 @@ class MainWnd(QWidget):
|
||||
w_jsonProfile = json.dumps(w_profile)
|
||||
self.ui.profileCbx.setItemData(w_idx, w_jsonProfile)
|
||||
|
||||
def slotCopyProfile(self):
|
||||
text, okPressed = QInputDialog.getText(self, "Copy profile", "Enter new profile name:", QLineEdit.Normal, "")
|
||||
if okPressed and text != '':
|
||||
# todo: check if name not already in use
|
||||
w_idx = self.ui.profileCbx.currentIndex()
|
||||
w_jsonProfile = self.ui.profileCbx.itemData(w_idx)
|
||||
w_profile = json.loads(w_jsonProfile)
|
||||
w_profile_copy = w_profile
|
||||
w_profile_copy['name'] = text
|
||||
self.ui.profileCbx.addItem(w_profile_copy['name'])
|
||||
w_jsonProfile = json.dumps(w_profile_copy)
|
||||
self.ui.profileCbx.setItemData(self.ui.profileCbx.count()-1, w_jsonProfile)
|
||||
|
||||
def slotRemoveProfile(self):
|
||||
|
||||
buttonReply = QMessageBox.question(self, 'Remove Profile', "Do you really want to delete this profile?", QMessageBox.No | QMessageBox.Yes, QMessageBox.No)
|
||||
if buttonReply == QMessageBox.No:
|
||||
return
|
||||
|
||||
w_curIdx = self.ui.profileCbx.currentIndex()
|
||||
if w_curIdx >= 0:
|
||||
self.ui.profileCbx.removeItem(w_curIdx)
|
||||
@ -247,14 +277,56 @@ class MainWnd(QWidget):
|
||||
|
||||
def slotOK(self):
|
||||
self.saveToDatabase()
|
||||
self.m_profileExecutor.stop()
|
||||
self.m_profileExecutor.shutdown()
|
||||
self.close()
|
||||
|
||||
def slotCancel(self):
|
||||
self.m_profileExecutor.stop()
|
||||
self.m_profileExecutor.shutdown()
|
||||
self.close()
|
||||
exit()
|
||||
|
||||
def handleArgs(self):
|
||||
self.m_config = {
|
||||
'noroot' : 0,
|
||||
'xdowindowid' : None
|
||||
}
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
return
|
||||
|
||||
for i in range(1,len(sys.argv)):
|
||||
if sys.argv[i] == '-noroot':
|
||||
self.m_config['noroot'] = 1
|
||||
elif sys.argv[i] == '-xdowindowid' and (i+1 < len(sys.argv)):
|
||||
self.m_config['xdowindowid'] = sys.argv[i+1]
|
||||
i = i+1
|
||||
|
||||
# try to help: if -noroot is supplied, but no xdowindowid, try to determine the id
|
||||
# Elite Dangerous only
|
||||
try:
|
||||
args = shlex.split('xdotool search --name "\(CLIENT\)"')
|
||||
window_id = str(subprocess.check_output(args))
|
||||
window_id = window_id.replace('b\'', '')
|
||||
window_id = window_id.replace('\\n\'','')
|
||||
except subprocess.CalledProcessError:
|
||||
window_id = None
|
||||
pass
|
||||
|
||||
if not window_id == None:
|
||||
# check if it's really the Client we want
|
||||
try:
|
||||
window_name = subprocess.check_output(['xdotool', 'getwindowname', str(window_id)])
|
||||
except subprocess.CalledProcessError:
|
||||
window_name = None
|
||||
pass
|
||||
if not window_name == None:
|
||||
if not str(window_name).find('Elite - Dangerous') == -1:
|
||||
print("Window ID: ", str(window_id), ", window name: ", window_name)
|
||||
print("Auto-detected window id of ED Client: ", window_id)
|
||||
self.m_config['xdowindowid'] = window_id
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
mainWnd = MainWnd()
|
||||
|
100
mainwnd.ui
100
mainwnd.ui
@ -25,7 +25,7 @@
|
||||
<property name="verticalSpacing">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>20</number>
|
||||
@ -38,7 +38,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
@ -50,6 +50,57 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Voice Volume</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
<widget class="QSlider" name="sliderVolume">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>20</y>
|
||||
<width>181</width>
|
||||
<height>21</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Change voice volume</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ok">
|
||||
<property name="minimumSize">
|
||||
@ -78,13 +129,19 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<item row="1" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>20</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</width>
|
||||
@ -100,7 +157,7 @@
|
||||
<widget class="QComboBox" name="profileCbx">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>250</width>
|
||||
<width>300</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -110,7 +167,7 @@
|
||||
<widget class="QPushButton" name="addBut">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<width>75</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -123,7 +180,7 @@
|
||||
<widget class="QPushButton" name="editBut">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<width>75</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -132,11 +189,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="copyBut">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>75</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Copy currently selected profile</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Copy</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="removeBut">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>130</width>
|
||||
<width>75</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
@ -145,19 +218,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
|
@ -13,6 +13,7 @@ class ProfileEditWnd(QDialog):
|
||||
self.ui = Ui_ProfileEditDialog()
|
||||
self.ui.setupUi(self)
|
||||
self.m_profile = {}
|
||||
self.m_parent = p_parent
|
||||
|
||||
self.ui.cmdTable.setHorizontalHeaderLabels(('Spoken command', 'Actions'))
|
||||
self.ui.cmdTable.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
|
@ -4,31 +4,44 @@ import time
|
||||
import threading
|
||||
import os, pyaudio
|
||||
import shutil
|
||||
import re
|
||||
from pocketsphinx.pocketsphinx import *
|
||||
from sphinxbase.sphinxbase import *
|
||||
from soundfiles import SoundFiles
|
||||
|
||||
|
||||
class ProfileExecutor(threading.Thread):
|
||||
mouse = Controller()
|
||||
|
||||
def __init__(self, p_profile = None):
|
||||
threading.Thread.__init__(self)
|
||||
def __init__(self, p_profile = None, p_parent = None):
|
||||
# threading.Thread.__init__(self)
|
||||
|
||||
# does nothing?
|
||||
self.setProfile(p_profile)
|
||||
self.m_stop = False
|
||||
self.m_listening = True
|
||||
self.m_listening = False
|
||||
self.m_cmdThreads = {}
|
||||
|
||||
self.m_config = Decoder.default_config()
|
||||
self.m_config.set_string('-hmm', os.path.join('model', 'en-us/en-us'))
|
||||
self.m_config.set_string('-dict', os.path.join('model', 'en-us/cmudict-en-us.dict'))
|
||||
self.m_config.set_string('-kws', 'command.list')
|
||||
# you usually don't want all this info stuff as a regular user. it just covers up init messages
|
||||
self.m_config.set_string('-logfn', '/dev/null')
|
||||
|
||||
p = pyaudio.PyAudio()
|
||||
self.m_stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
|
||||
self.m_stream.start_stream()
|
||||
self.m_pyaudio = pyaudio.PyAudio()
|
||||
self.m_stream = self.m_pyaudio.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
|
||||
|
||||
# Process audio chunk by chunk. On keyword detected perform action and restart search
|
||||
self.m_decoder = Decoder(self.m_config)
|
||||
|
||||
self.m_thread = False
|
||||
|
||||
self.p_parent = p_parent
|
||||
if not self.p_parent == None:
|
||||
self.m_sound = self.p_parent.m_sound
|
||||
|
||||
|
||||
def getSettingsPath(self, setting):
|
||||
home = os.path.expanduser("~") + '/.linvam/'
|
||||
if not os.path.exists(home):
|
||||
@ -39,25 +52,46 @@ class ProfileExecutor(threading.Thread):
|
||||
return home + setting
|
||||
|
||||
def setProfile(self, p_profile):
|
||||
#print("setProfile")
|
||||
self.m_profile = p_profile
|
||||
if self.m_profile == None:
|
||||
return
|
||||
|
||||
#print ("writing command list")
|
||||
w_commandWordFile = open(self.getSettingsPath('command.list'), 'w')
|
||||
w_commands = self.m_profile['commands']
|
||||
i = 0
|
||||
for w_command in w_commands:
|
||||
if i != 0:
|
||||
w_commandWordFile.write('\n')
|
||||
w_commandWordFile.write(w_command['name'] + ' /1e-%d/' % w_command['threshold'])
|
||||
w_commandWordFile.write(w_command['name'].lower() + ' /1e-%d/' % w_command['threshold'])
|
||||
i = i + 1
|
||||
w_commandWordFile.close()
|
||||
self.m_config.set_string('-kws', self.getSettingsPath('command.list'))
|
||||
# load new command list into decoder and restart it
|
||||
if self.m_listening == True:
|
||||
self.stop()
|
||||
self.m_stream.start_stream()
|
||||
# a self.m_decoder.reinit(self.config) will segfault?
|
||||
self.m_decoder = Decoder(self.m_config)
|
||||
self.m_stop = False
|
||||
self.m_thread = threading.Thread(target=self.doListen, args=())
|
||||
self.m_thread.start()
|
||||
else:
|
||||
self.m_decoder.reinit(self.m_config)
|
||||
|
||||
def setEnableListening(self, p_enable):
|
||||
if self.m_listening == False and p_enable == True:
|
||||
self.m_stream.start_stream()
|
||||
self.m_listening = p_enable
|
||||
self.m_stop = False
|
||||
self.m_thread = threading.Thread(target=self.doListen, args=())
|
||||
self.m_thread.start()
|
||||
elif self.m_listening == True and p_enable == False:
|
||||
self.stop()
|
||||
|
||||
def run(self):
|
||||
def doListen(self):
|
||||
print("Detection started")
|
||||
self.m_listening = True
|
||||
self.m_decoder.start_utt()
|
||||
while self.m_stop != True:
|
||||
buf = self.m_stream.read(1024)
|
||||
@ -65,8 +99,14 @@ class ProfileExecutor(threading.Thread):
|
||||
self.m_decoder.process_raw(buf, False, False)
|
||||
|
||||
if self.m_decoder.hyp() != None:
|
||||
# print([(seg.word, seg.prob, seg.start_frame, seg.end_frame) for seg in decoder.seg()])
|
||||
# print("Detected keyword, restarting search")
|
||||
#print([(seg.word, seg.prob, seg.start_frame, seg.end_frame) for seg in self.m_decoder.seg()])
|
||||
#print("Detected keyword, restarting search")
|
||||
|
||||
# hack :)
|
||||
for seg in self.m_decoder.seg():
|
||||
print("Detected: ",seg.word)
|
||||
break
|
||||
|
||||
#
|
||||
# Here you run the code you want based on keyword
|
||||
#
|
||||
@ -76,9 +116,21 @@ class ProfileExecutor(threading.Thread):
|
||||
self.m_decoder.end_utt()
|
||||
self.m_decoder.start_utt()
|
||||
|
||||
# def run(self):
|
||||
|
||||
def stop(self):
|
||||
if self.m_listening == True:
|
||||
self.m_stop = True
|
||||
threading.Thread.join(self)
|
||||
self.m_listening = False
|
||||
self.m_decoder.end_utt()
|
||||
self.m_thread.join()
|
||||
self.m_stream.stop_stream()
|
||||
|
||||
def shutdown(self):
|
||||
self.stop()
|
||||
self.m_stream.close()
|
||||
self.m_pyaudio.terminate()
|
||||
|
||||
|
||||
def doAction(self, p_action):
|
||||
# {'name': 'key action', 'key': 'left', 'type': 0}
|
||||
@ -91,20 +143,14 @@ class ProfileExecutor(threading.Thread):
|
||||
if w_actionName == 'key action':
|
||||
w_key = p_action['key']
|
||||
w_type = p_action['type']
|
||||
if w_type == 1:
|
||||
keyboard.press(w_key)
|
||||
print("pressed key: ", w_key)
|
||||
elif w_type == 0:
|
||||
keyboard.release(w_key)
|
||||
print("released key: ", w_key)
|
||||
elif w_type == 10:
|
||||
keyboard.press(w_key)
|
||||
keyboard.release(w_key)
|
||||
print("pressed and released key: ", w_key)
|
||||
self.pressKey(w_key, w_type)
|
||||
elif w_actionName == 'pause action':
|
||||
print("Sleep ", p_action['time'])
|
||||
time.sleep(p_action['time'])
|
||||
elif w_actionName == 'command stop action':
|
||||
self.stopCommand(p_action['command name'])
|
||||
elif w_actionName == 'command play sound' or w_actionName == 'play sound':
|
||||
self.playSound(p_action)
|
||||
elif w_actionName == 'mouse move action':
|
||||
if p_action['absolute']:
|
||||
ProfileExecutor.mouse.position([p_action['x'], p_action['y']])
|
||||
@ -167,7 +213,7 @@ class ProfileExecutor(threading.Thread):
|
||||
w_commands = self.m_profile['commands']
|
||||
flag = False
|
||||
for w_command in w_commands:
|
||||
if w_command['name'] == p_cmdName:
|
||||
if w_command['name'].lower() == p_cmdName:
|
||||
flag = True
|
||||
break
|
||||
if flag == False:
|
||||
@ -195,3 +241,66 @@ class ProfileExecutor(threading.Thread):
|
||||
if p_cmdName in self.m_cmdThreads.keys():
|
||||
self.m_cmdThreads[p_cmdName].stop()
|
||||
del self.m_cmdThreads[p_cmdName]
|
||||
|
||||
def playSound(self, p_cmdName):
|
||||
sound_file = './voicepacks/' + p_cmdName['pack'] + '/' + p_cmdName['cat'] + '/' + p_cmdName['file']
|
||||
self.m_sound.play(sound_file)
|
||||
|
||||
|
||||
def pressKey(self, w_key, w_type):
|
||||
if self.p_parent.m_config['noroot'] == 1:
|
||||
# xdotool has a different key mapping. translate old existing mappings of special keys
|
||||
# use this to find key name: xev -event keyboard
|
||||
w_key = re.sub('left ctrl', 'Control_L', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('right ctrl', 'Control_R', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('left shift', 'Shift_L', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('right shift', 'Shift_R', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('left alt', 'Alt_L', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('right alt', 'Alt_R', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('left windows', 'Super_L', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('right windows', 'Super_R', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('tab', 'Tab', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('esc', 'Escape', w_key, flags=re.IGNORECASE)
|
||||
|
||||
w_key = re.sub('left', 'Left', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('right', 'Right', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('up', 'Up', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('down', 'Down', w_key, flags=re.IGNORECASE)
|
||||
|
||||
w_key = re.sub('ins$', 'Insert', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('del$', 'Delete', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('home', 'Home', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('end', 'End', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('Page\s?up', 'Prior', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('Page\s?down', 'Next', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('return', 'Return', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('enter', 'Return', w_key, flags=re.IGNORECASE)
|
||||
w_key = re.sub('backspace', 'BackSpace', w_key, flags=re.IGNORECASE)
|
||||
|
||||
w_key = w_key.replace('insert', 'Insert')
|
||||
w_key = w_key.replace('delete', 'Delete')
|
||||
|
||||
window_cmd = ""
|
||||
if not self.p_parent.m_config['xdowindowid'] == None:
|
||||
window_cmd = " windowactivate --sync " + str(self.p_parent.m_config['xdowindowid'])
|
||||
|
||||
if w_type == 1:
|
||||
os.system('xdotool ' + window_cmd + ' keydown ' + str(w_key) )
|
||||
print("pressed key: ", w_key)
|
||||
elif w_type == 0:
|
||||
os.system('xdotool' + window_cmd + ' keyup ' + str(w_key))
|
||||
print("released key: ", w_key)
|
||||
elif w_type == 10:
|
||||
os.system('xdotool' + window_cmd + ' key ' + str(w_key))
|
||||
print("pressed and released key: ", w_key)
|
||||
else:
|
||||
if w_type == 1:
|
||||
keyboard.press(w_key)
|
||||
print("pressed key: ", w_key)
|
||||
elif w_type == 0:
|
||||
keyboard.release(w_key)
|
||||
print("released key: ", w_key)
|
||||
elif w_type == 10:
|
||||
keyboard.press(w_key)
|
||||
keyboard.release(w_key)
|
||||
print("pressed and released key: ", w_key)
|
164
soundactioneditwnd.py
Normal file
164
soundactioneditwnd.py
Normal file
@ -0,0 +1,164 @@
|
||||
from PyQt5.QtCore import *
|
||||
from PyQt5.QtGui import *
|
||||
from PyQt5.QtWidgets import *
|
||||
import re
|
||||
import os
|
||||
from ui_soundactioneditwnd import Ui_SoundSelect
|
||||
|
||||
class SoundActionEditWnd(QDialog):
|
||||
def __init__(self, p_sounds, p_soundAction = None, p_parent = None):
|
||||
super().__init__(p_parent)
|
||||
self.ui = Ui_SoundSelect()
|
||||
self.ui.setupUi(self)
|
||||
|
||||
if p_sounds == None:
|
||||
return
|
||||
|
||||
self.p_sounds = p_sounds
|
||||
self.selectedVoicepack = False
|
||||
self.selectedCategory = False
|
||||
self.selectedFile = False
|
||||
|
||||
self.ui.buttonOkay.clicked.connect(self.slotOK)
|
||||
self.ui.buttonCancel.clicked.connect(super().reject)
|
||||
self.ui.buttonPlaySound.clicked.connect(self.playSound)
|
||||
self.ui.buttonStopSound.clicked.connect(self.stopSound)
|
||||
self.ui.buttonOkay.setEnabled(False)
|
||||
|
||||
# restore stuff when editing
|
||||
if not p_soundAction == None:
|
||||
self.selectedVoicepack = p_soundAction['pack']
|
||||
self.selectedCategory = p_soundAction['cat']
|
||||
self.selectedFile = p_soundAction['file']
|
||||
self.ui.buttonOkay.setEnabled(True)
|
||||
|
||||
self.listVoicepacks_model = QStandardItemModel()
|
||||
self.ui.listVoicepacks.setModel(self.listVoicepacks_model)
|
||||
self.ui.listVoicepacks.clicked.connect(self.onVoicepackSelect)
|
||||
|
||||
self.listCategories_model = QStandardItemModel()
|
||||
self.ui.listCategories.setModel(self.listCategories_model)
|
||||
self.ui.listCategories.clicked.connect(self.onCategorySelect)
|
||||
|
||||
self.listFiles_model = QStandardItemModel()
|
||||
self.ui.listFiles.setModel(self.listFiles_model)
|
||||
self.ui.listFiles.clicked.connect(self.onFileSelect)
|
||||
self.ui.listFiles.doubleClicked.connect(self.selectAndPlay)
|
||||
|
||||
s = sorted(p_sounds.m_sounds)
|
||||
for v in s:
|
||||
item = QStandardItem(v)
|
||||
self.listVoicepacks_model.appendRow(item)
|
||||
|
||||
self.ui.filterCategories.textChanged.connect(self.populateCategories)
|
||||
self.ui.filterFiles.textChanged.connect(self.populateFiles)
|
||||
|
||||
|
||||
self.populateCategories(False)
|
||||
self.populateFiles(False)
|
||||
|
||||
# when editing, select old entries
|
||||
if not self.selectedVoicepack == False:
|
||||
item = self.listVoicepacks_model.findItems(self.selectedVoicepack)
|
||||
if len(item) > 0:
|
||||
index = self.listVoicepacks_model.indexFromItem(item[0])
|
||||
self.ui.listVoicepacks.setCurrentIndex(index)
|
||||
|
||||
if not self.selectedCategory == False:
|
||||
item = self.listCategories_model.findItems(self.selectedCategory)
|
||||
if len(item) > 0:
|
||||
index = self.listCategories_model.indexFromItem(item[0])
|
||||
self.ui.listCategories.setCurrentIndex(index)
|
||||
|
||||
if not self.selectedFile == False:
|
||||
item = self.listFiles_model.findItems(self.selectedFile)
|
||||
if len(item) > 0:
|
||||
index = self.listFiles_model.indexFromItem(item[0])
|
||||
self.ui.listFiles.setCurrentIndex(index)
|
||||
|
||||
|
||||
|
||||
|
||||
def slotOK(self):
|
||||
self.m_soundAction = {'name': 'play sound', 'pack': self.selectedVoicepack, 'cat' : self.selectedCategory, 'file' : self.selectedFile}
|
||||
super().accept()
|
||||
|
||||
def slotCancel(self):
|
||||
super().reject()
|
||||
|
||||
def onVoicepackSelect(self):
|
||||
index = self.ui.listVoicepacks.currentIndex()
|
||||
itemText = index.data()
|
||||
self.selectedVoicepack = itemText
|
||||
self.populateCategories()
|
||||
self.ui.buttonOkay.setEnabled(False)
|
||||
self.ui.buttonPlaySound.setEnabled(False)
|
||||
|
||||
def onCategorySelect(self):
|
||||
index = self.ui.listCategories.currentIndex()
|
||||
itemText = index.data()
|
||||
self.selectedCategory = itemText
|
||||
self.populateFiles()
|
||||
self.ui.buttonOkay.setEnabled(False)
|
||||
self.ui.buttonPlaySound.setEnabled(False)
|
||||
|
||||
|
||||
def onFileSelect(self):
|
||||
index = self.ui.listFiles.currentIndex()
|
||||
itemText = index.data()
|
||||
self.selectedFile = itemText
|
||||
self.ui.buttonOkay.setEnabled(True)
|
||||
self.ui.buttonPlaySound.setEnabled(True)
|
||||
|
||||
def selectAndPlay(self):
|
||||
self.onFileSelect()
|
||||
self.playSound()
|
||||
|
||||
def populateCategories(self, reset = True):
|
||||
if self.selectedVoicepack == False:
|
||||
return
|
||||
|
||||
if reset == True:
|
||||
self.listCategories_model.removeRows( 0, self.listCategories_model.rowCount() )
|
||||
self.listFiles_model.removeRows( 0, self.listFiles_model.rowCount() )
|
||||
self.selectedCategory = False
|
||||
self.selectedFile = False
|
||||
|
||||
filter_categories = self.ui.filterCategories.toPlainText()
|
||||
if len(filter_categories) == 0:
|
||||
filter_categories = None
|
||||
|
||||
s = sorted(self.p_sounds.m_sounds[self.selectedVoicepack])
|
||||
for v in s:
|
||||
if not filter_categories == None:
|
||||
if not re.search(filter_categories, v, re.IGNORECASE):
|
||||
continue
|
||||
item = QStandardItem(v)
|
||||
self.listCategories_model.appendRow(item)
|
||||
|
||||
def populateFiles(self, reset = True):
|
||||
if self.selectedVoicepack == False or self.selectedCategory == False:
|
||||
return
|
||||
|
||||
if reset == True:
|
||||
self.listFiles_model.removeRows( 0, self.listFiles_model.rowCount() )
|
||||
self.selectedFile = False
|
||||
|
||||
filter_files = self.ui.filterFiles.toPlainText()
|
||||
if len(filter_files) == 0:
|
||||
filter_files = None
|
||||
|
||||
s = sorted(self.p_sounds.m_sounds[self.selectedVoicepack][self.selectedCategory])
|
||||
for v in s:
|
||||
if not filter_files == None:
|
||||
if not re.search(filter_files, v, re.IGNORECASE):
|
||||
continue
|
||||
item = QStandardItem(v)
|
||||
self.listFiles_model.appendRow(item)
|
||||
|
||||
def playSound(self):
|
||||
sound_file = './voicepacks/' + self.selectedVoicepack + '/' + self.selectedCategory + '/' + self.selectedFile
|
||||
self.p_sounds.play(sound_file)
|
||||
|
||||
def stopSound(self):
|
||||
self.p_sounds.stop()
|
241
soundactioneditwnd.ui
Normal file
241
soundactioneditwnd.ui
Normal file
@ -0,0 +1,241 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SoundSelect</class>
|
||||
<widget class="QGroupBox" name="SoundSelect">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1119</width>
|
||||
<height>362</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Sound selection</string>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<widget class="QLabel" name="labelVoicepacks">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>30</y>
|
||||
<width>111</width>
|
||||
<height>17</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>VoicePacks:</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelCategories">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>160</x>
|
||||
<y>30</y>
|
||||
<width>111</width>
|
||||
<height>17</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Categories:</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QListView" name="listFiles">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>660</x>
|
||||
<y>90</y>
|
||||
<width>451</width>
|
||||
<height>231</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QListView" name="listCategories">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>160</x>
|
||||
<y>90</y>
|
||||
<width>491</width>
|
||||
<height>231</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QListView" name="listVoicepacks">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>10</x>
|
||||
<y>50</y>
|
||||
<width>141</width>
|
||||
<height>271</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="labelFiles">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>660</x>
|
||||
<y>30</y>
|
||||
<width>111</width>
|
||||
<height>17</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Voice files:</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="buttonOkay">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>940</x>
|
||||
<y>330</y>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Ok</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="buttonCancel">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>1030</x>
|
||||
<y>330</y>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="locale">
|
||||
<locale language="English" country="UnitedStates"/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPlainTextEdit" name="filterCategories">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>160</x>
|
||||
<y>50</y>
|
||||
<width>491</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Filter categories</string>
|
||||
</property>
|
||||
<property name="statusTip">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPlainTextEdit" name="filterFiles">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>660</x>
|
||||
<y>50</y>
|
||||
<width>451</width>
|
||||
<height>31</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Filter voice files</string>
|
||||
</property>
|
||||
<property name="statusTip">
|
||||
<string>Filter</string>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
<string/>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="buttonPlaySound">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>410</x>
|
||||
<y>330</y>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Play Sound</string>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="buttonStopSound">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>500</x>
|
||||
<y>330</y>
|
||||
<width>80</width>
|
||||
<height>25</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Stop Sound</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
87
soundfiles.py
Normal file
87
soundfiles.py
Normal file
@ -0,0 +1,87 @@
|
||||
import time
|
||||
import threading
|
||||
import os
|
||||
import shutil
|
||||
from os import path
|
||||
import subprocess
|
||||
import shlex
|
||||
import signal
|
||||
|
||||
# I've tried a couple of libs that are capable of playing mp3:
|
||||
# pysound - offers no way to stop sounds. can't play files with whitespace in path
|
||||
# pygame - has problems with certain mp3 files from voice packs
|
||||
# mpg123 - does not offer volume control. need to be threaded as it's a system binary
|
||||
#
|
||||
# ffplay - need to be threaded as it's a system binary. i picked this one as it uses ffmpeg which
|
||||
# is an excellent tool and should be installed on any system already
|
||||
# I needed some process stuff to be able to stop an already playing sound (kill ffplay subprocess)
|
||||
|
||||
|
||||
class SoundFiles():
|
||||
def __init__(self):
|
||||
print("SoundFiles: init")
|
||||
self.m_sounds = {}
|
||||
self.scanSoundFiles()
|
||||
self.thread_play = False
|
||||
self.volume = 100
|
||||
|
||||
def scanSoundFiles(self):
|
||||
print("SoundFiles: scanning")
|
||||
if not path.exists('./voicepacks'):
|
||||
print("No folder 'voicepacks' found. Please create one and copy all your voicepacks in there.")
|
||||
return
|
||||
|
||||
for root, dirs, files in os.walk("./voicepacks"):
|
||||
for file in files:
|
||||
if file.endswith(".mp3"):
|
||||
# we expect a path like this:
|
||||
# voicepacks/VOICEPACKNAME/COMMANDGROUP/(FURTHER_OPTIONAL_FOLDERS/)FILE
|
||||
path_parts = root.split('/')
|
||||
|
||||
if len(path_parts) < 4:
|
||||
continue
|
||||
|
||||
if not path_parts[2] in self.m_sounds:
|
||||
self.m_sounds[path_parts[2]] = {}
|
||||
|
||||
category = path_parts[3]
|
||||
# there might be subfolders, so we have more than just 4 split results...
|
||||
# for the ease of my mind, we concat the voicepack subfolders to 1 category name
|
||||
# like voicepacks/hcspack/Characters/Astra/blah.mp3 will become:
|
||||
#
|
||||
# voicepack = hcspack
|
||||
# category = Characters/Astra
|
||||
# file = blah.mp4
|
||||
|
||||
if len(path_parts) > 4:
|
||||
for i in range(4, len(path_parts)):
|
||||
category = category + '/' + path_parts[i]
|
||||
|
||||
if not category in self.m_sounds[path_parts[2]]:
|
||||
self.m_sounds[path_parts[2]][category] = []
|
||||
|
||||
self.m_sounds[path_parts[2]][category].append(file)
|
||||
|
||||
def play(self, sound_file):
|
||||
if not os.path.isfile(sound_file):
|
||||
print("ERROR - Sound file not found: ", sound_file)
|
||||
return
|
||||
|
||||
self.stop()
|
||||
|
||||
# construct shell command. use shlex to split it up into valid args for Popen.
|
||||
cmd = "ffplay -nodisp -autoexit -loglevel quiet -volume " + str(self.volume) + " \"" + sound_file + "\"";
|
||||
args = shlex.split(cmd)
|
||||
self.thread_play = subprocess.Popen(args)
|
||||
|
||||
def stop(self):
|
||||
if not self.thread_play == False:
|
||||
# that aint no nice, but it's the only way i got the subprocess reliably killed.
|
||||
# self.thread_play.terminate() or kill() should do the trick, but it won't
|
||||
try:
|
||||
os.kill(self.thread_play.pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def setVolume(self, volume):
|
||||
self.volume = volume;
|
@ -2,10 +2,11 @@
|
||||
|
||||
# Form implementation generated from reading ui file 'mainwnd.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.12.1
|
||||
# Created by: PyQt5 UI code generator 5.14.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
@ -28,6 +29,19 @@ class Ui_MainWidget(object):
|
||||
self.horizontalLayout_2.addWidget(self.listeningChk)
|
||||
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem)
|
||||
self.groupBox = QtWidgets.QGroupBox(MainWidget)
|
||||
self.groupBox.setMinimumSize(QtCore.QSize(200, 42))
|
||||
self.groupBox.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.groupBox.setObjectName("groupBox")
|
||||
self.sliderVolume = QtWidgets.QSlider(self.groupBox)
|
||||
self.sliderVolume.setGeometry(QtCore.QRect(10, 20, 181, 21))
|
||||
self.sliderVolume.setMaximum(100)
|
||||
self.sliderVolume.setProperty("value", 100)
|
||||
self.sliderVolume.setOrientation(QtCore.Qt.Horizontal)
|
||||
self.sliderVolume.setObjectName("sliderVolume")
|
||||
self.horizontalLayout_2.addWidget(self.groupBox)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout_2.addItem(spacerItem1)
|
||||
self.ok = QtWidgets.QPushButton(MainWidget)
|
||||
self.ok.setMinimumSize(QtCore.QSize(130, 0))
|
||||
self.ok.setObjectName("ok")
|
||||
@ -36,33 +50,40 @@ class Ui_MainWidget(object):
|
||||
self.cancel.setMinimumSize(QtCore.QSize(130, 0))
|
||||
self.cancel.setObjectName("cancel")
|
||||
self.horizontalLayout_2.addWidget(self.cancel)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout_2, 1, 0, 1, 1)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout_2, 2, 0, 1, 1)
|
||||
self.horizontalLayout = QtWidgets.QHBoxLayout()
|
||||
self.horizontalLayout.setSpacing(20)
|
||||
self.horizontalLayout.setObjectName("horizontalLayout")
|
||||
self.label = QtWidgets.QLabel(MainWidget)
|
||||
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(0)
|
||||
sizePolicy.setHeightForWidth(self.label.sizePolicy().hasHeightForWidth())
|
||||
self.label.setSizePolicy(sizePolicy)
|
||||
self.label.setMinimumSize(QtCore.QSize(60, 0))
|
||||
self.label.setObjectName("label")
|
||||
self.horizontalLayout.addWidget(self.label)
|
||||
self.profileCbx = QtWidgets.QComboBox(MainWidget)
|
||||
self.profileCbx.setMinimumSize(QtCore.QSize(250, 0))
|
||||
self.profileCbx.setMinimumSize(QtCore.QSize(300, 0))
|
||||
self.profileCbx.setObjectName("profileCbx")
|
||||
self.horizontalLayout.addWidget(self.profileCbx)
|
||||
self.addBut = QtWidgets.QPushButton(MainWidget)
|
||||
self.addBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||
self.addBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||
self.addBut.setObjectName("addBut")
|
||||
self.horizontalLayout.addWidget(self.addBut)
|
||||
self.editBut = QtWidgets.QPushButton(MainWidget)
|
||||
self.editBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||
self.editBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||
self.editBut.setObjectName("editBut")
|
||||
self.horizontalLayout.addWidget(self.editBut)
|
||||
self.copyBut = QtWidgets.QPushButton(MainWidget)
|
||||
self.copyBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||
self.copyBut.setObjectName("copyBut")
|
||||
self.horizontalLayout.addWidget(self.copyBut)
|
||||
self.removeBut = QtWidgets.QPushButton(MainWidget)
|
||||
self.removeBut.setMinimumSize(QtCore.QSize(130, 0))
|
||||
self.removeBut.setMinimumSize(QtCore.QSize(75, 0))
|
||||
self.removeBut.setObjectName("removeBut")
|
||||
self.horizontalLayout.addWidget(self.removeBut)
|
||||
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||
self.horizontalLayout.addItem(spacerItem1)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 1)
|
||||
self.gridLayout_2.addLayout(self.horizontalLayout, 1, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(MainWidget)
|
||||
QtCore.QMetaObject.connectSlotsByName(MainWidget)
|
||||
@ -71,21 +92,13 @@ class Ui_MainWidget(object):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
MainWidget.setWindowTitle(_translate("MainWidget", "LinVAM"))
|
||||
self.listeningChk.setText(_translate("MainWidget", "Enable Listening"))
|
||||
self.groupBox.setTitle(_translate("MainWidget", "Voice Volume"))
|
||||
self.sliderVolume.setToolTip(_translate("MainWidget", "Change voice volume"))
|
||||
self.ok.setText(_translate("MainWidget", "OK"))
|
||||
self.cancel.setText(_translate("MainWidget", "Cancel"))
|
||||
self.label.setText(_translate("MainWidget", "Profile:"))
|
||||
self.addBut.setText(_translate("MainWidget", "Add"))
|
||||
self.editBut.setText(_translate("MainWidget", "Edit"))
|
||||
self.copyBut.setToolTip(_translate("MainWidget", "Copy currently selected profile"))
|
||||
self.copyBut.setText(_translate("MainWidget", "Copy"))
|
||||
self.removeBut.setText(_translate("MainWidget", "Remove"))
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
MainWidget = QtWidgets.QWidget()
|
||||
ui = Ui_MainWidget()
|
||||
ui.setupUi(MainWidget)
|
||||
MainWidget.show()
|
||||
sys.exit(app.exec_())
|
||||
|
93
ui_soundactioneditwnd.py
Normal file
93
ui_soundactioneditwnd.py
Normal file
@ -0,0 +1,93 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file 'soundactioneditwnd.ui'
|
||||
#
|
||||
# Created by: PyQt5 UI code generator 5.14.2
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
|
||||
|
||||
class Ui_SoundSelect(object):
|
||||
def setupUi(self, SoundSelect):
|
||||
SoundSelect.setObjectName("SoundSelect")
|
||||
SoundSelect.resize(1119, 362)
|
||||
SoundSelect.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.labelVoicepacks = QtWidgets.QLabel(SoundSelect)
|
||||
self.labelVoicepacks.setGeometry(QtCore.QRect(10, 30, 111, 17))
|
||||
self.labelVoicepacks.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.labelVoicepacks.setObjectName("labelVoicepacks")
|
||||
self.labelCategories = QtWidgets.QLabel(SoundSelect)
|
||||
self.labelCategories.setGeometry(QtCore.QRect(160, 30, 111, 17))
|
||||
self.labelCategories.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.labelCategories.setObjectName("labelCategories")
|
||||
self.listFiles = QtWidgets.QListView(SoundSelect)
|
||||
self.listFiles.setGeometry(QtCore.QRect(660, 90, 451, 231))
|
||||
self.listFiles.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.listFiles.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listFiles.setProperty("showDropIndicator", False)
|
||||
self.listFiles.setAlternatingRowColors(True)
|
||||
self.listFiles.setObjectName("listFiles")
|
||||
self.listCategories = QtWidgets.QListView(SoundSelect)
|
||||
self.listCategories.setEnabled(True)
|
||||
self.listCategories.setGeometry(QtCore.QRect(160, 90, 491, 231))
|
||||
self.listCategories.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.listCategories.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listCategories.setProperty("showDropIndicator", False)
|
||||
self.listCategories.setAlternatingRowColors(True)
|
||||
self.listCategories.setObjectName("listCategories")
|
||||
self.listVoicepacks = QtWidgets.QListView(SoundSelect)
|
||||
self.listVoicepacks.setGeometry(QtCore.QRect(10, 50, 141, 271))
|
||||
self.listVoicepacks.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.listVoicepacks.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
|
||||
self.listVoicepacks.setProperty("showDropIndicator", False)
|
||||
self.listVoicepacks.setAlternatingRowColors(True)
|
||||
self.listVoicepacks.setObjectName("listVoicepacks")
|
||||
self.labelFiles = QtWidgets.QLabel(SoundSelect)
|
||||
self.labelFiles.setGeometry(QtCore.QRect(660, 30, 111, 17))
|
||||
self.labelFiles.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.labelFiles.setObjectName("labelFiles")
|
||||
self.buttonOkay = QtWidgets.QPushButton(SoundSelect)
|
||||
self.buttonOkay.setEnabled(True)
|
||||
self.buttonOkay.setGeometry(QtCore.QRect(940, 330, 80, 25))
|
||||
self.buttonOkay.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.buttonOkay.setObjectName("buttonOkay")
|
||||
self.buttonCancel = QtWidgets.QPushButton(SoundSelect)
|
||||
self.buttonCancel.setGeometry(QtCore.QRect(1030, 330, 80, 25))
|
||||
self.buttonCancel.setLocale(QtCore.QLocale(QtCore.QLocale.English, QtCore.QLocale.UnitedStates))
|
||||
self.buttonCancel.setObjectName("buttonCancel")
|
||||
self.filterCategories = QtWidgets.QPlainTextEdit(SoundSelect)
|
||||
self.filterCategories.setGeometry(QtCore.QRect(160, 50, 491, 31))
|
||||
self.filterCategories.setObjectName("filterCategories")
|
||||
self.filterFiles = QtWidgets.QPlainTextEdit(SoundSelect)
|
||||
self.filterFiles.setGeometry(QtCore.QRect(660, 50, 451, 31))
|
||||
self.filterFiles.setPlainText("")
|
||||
self.filterFiles.setObjectName("filterFiles")
|
||||
self.buttonPlaySound = QtWidgets.QPushButton(SoundSelect)
|
||||
self.buttonPlaySound.setGeometry(QtCore.QRect(410, 330, 80, 25))
|
||||
self.buttonPlaySound.setToolTip("")
|
||||
self.buttonPlaySound.setObjectName("buttonPlaySound")
|
||||
self.buttonStopSound = QtWidgets.QPushButton(SoundSelect)
|
||||
self.buttonStopSound.setGeometry(QtCore.QRect(500, 330, 80, 25))
|
||||
self.buttonStopSound.setToolTip("")
|
||||
self.buttonStopSound.setObjectName("buttonStopSound")
|
||||
|
||||
self.retranslateUi(SoundSelect)
|
||||
QtCore.QMetaObject.connectSlotsByName(SoundSelect)
|
||||
|
||||
def retranslateUi(self, SoundSelect):
|
||||
_translate = QtCore.QCoreApplication.translate
|
||||
SoundSelect.setWindowTitle(_translate("SoundSelect", "Sound selection"))
|
||||
self.labelVoicepacks.setText(_translate("SoundSelect", "VoicePacks:"))
|
||||
self.labelCategories.setText(_translate("SoundSelect", "Categories:"))
|
||||
self.labelFiles.setText(_translate("SoundSelect", "Voice files:"))
|
||||
self.buttonOkay.setText(_translate("SoundSelect", "Ok"))
|
||||
self.buttonCancel.setText(_translate("SoundSelect", "Cancel"))
|
||||
self.filterCategories.setToolTip(_translate("SoundSelect", "Filter categories"))
|
||||
self.filterCategories.setStatusTip(_translate("SoundSelect", "Filter"))
|
||||
self.filterFiles.setToolTip(_translate("SoundSelect", "Filter voice files"))
|
||||
self.filterFiles.setStatusTip(_translate("SoundSelect", "Filter"))
|
||||
self.buttonPlaySound.setText(_translate("SoundSelect", "Play Sound"))
|
||||
self.buttonStopSound.setText(_translate("SoundSelect", "Stop Sound"))
|
Loading…
Reference in New Issue
Block a user