├── .github ├── NOVA_Case.jpeg ├── NOVA_Case_Without.jpeg ├── NOVA_SketchUp_Case.gif ├── NOVA_SketchUp_Case.skp ├── github-logo.svg └── pythonLogo.png ├── .gitignore ├── .vscode ├── extensions.json └── launch.json ├── LICENSE ├── README.md ├── main.py ├── requirements.txt ├── settings.json └── src ├── Audio.py ├── ModuleManager.py ├── NaturalLanguage ├── Corpus.py ├── Intent.py ├── Processor.py └── ProcessorResult.py ├── Nova.py ├── SetTimeOut.py ├── TTS.py ├── libraries └── pixel_ring │ ├── apa102.py │ ├── pattern.py │ └── pixel_ring.py ├── models └── README.md ├── plugins ├── chatbot │ ├── corpus.json │ └── index.py ├── count │ ├── corpus.json │ └── index.py ├── datedaytimeyear │ ├── corpus.json │ └── index.py ├── deviceipaddress │ ├── corpus.json │ └── index.py ├── homepodsounds │ ├── index.py │ └── mp3 │ │ ├── boot.mp3 │ │ ├── invalid.mp3 │ │ ├── listen.mp3 │ │ └── valid.mp3 ├── led │ └── index.py ├── mediastack │ ├── corpus.json │ └── index.py ├── random │ ├── corpus.json │ └── index.py ├── timer │ ├── corpus.json │ ├── index.py │ └── mp3 │ │ └── alarm.mp3 └── volume │ ├── corpus.json │ └── index.py └── porcupine ├── WakeWord_Ok-NOVA_fr_mac.ppn ├── WakeWord_Ok-NOVA_fr_nt.ppn ├── WakeWord_Ok-NOVA_fr_raspberry.ppn └── porcupine_params_fr.pv /.github/NOVA_Case.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/.github/NOVA_Case.jpeg -------------------------------------------------------------------------------- /.github/NOVA_Case_Without.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/.github/NOVA_Case_Without.jpeg -------------------------------------------------------------------------------- /.github/NOVA_SketchUp_Case.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/.github/NOVA_SketchUp_Case.gif -------------------------------------------------------------------------------- /.github/NOVA_SketchUp_Case.skp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/.github/NOVA_SketchUp_Case.skp -------------------------------------------------------------------------------- /.github/github-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /.github/pythonLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/.github/pythonLogo.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | *.pyc 3 | /src/audio/* 4 | src/models/model/* 5 | src/models/*.zip 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "ms-python.python" 4 | ] 5 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python", 9 | "type": "python", 10 | "request": "launch", 11 | "program": "main.py", 12 | "console": "integratedTerminal", 13 | "justMyCode": true 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Antoine Duval. 2 | 3 | Nova-Python is a software distributed by Antoine Duval, a French 4 | developper, born on 22/12/1993 at Caen in France and based in France, 5 | at 16 domaine du Jonquay 76570 Mesnil-Panneville. 6 | 7 | This software is protected by applicable copyright laws, 8 | including international treaties. 9 | 10 | You may use this program if your use involves only such purposes as 11 | research, private study, and evaluation. 12 | 13 | You are prohibited from installing & copying, 14 | if i'ts for commercial purposes. 15 | 16 | ANTOINE DUVAL PROVIDES THIS SOFTWARE ON AN "AS IS" BASIS, WITHOUT 17 | WARRANTIES OR CONDITIONS OF ANY KIND. IN NO EVENT AND UNDER NO LEGAL 18 | THEORY, SHALL ANTOINE DUVAL BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 19 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY 20 | CHARACTER ARISING FROM USE OR INABILITY TO USE THIS SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | NOVA 4 | 5 | **NOVA** is an efficient personal assistant made with python.
6 |
7 | [![Discord](https://img.shields.io/discord/704685696513736765?label=Discord&style=flat&logo=discord)](https://discord.gg/pkWbhDn) 8 |
9 | 10 | ## 👋 Introduction 11 | 12 | **NOVA-Python** is an open-source personal assistant that you can host on your computer.
13 | You can communicate with it orally, most of his skills are designed to work offline to protect your privacy. 14 | 15 | ## 🔧 Prerequisites 16 | 17 | [ Python](https://www.python.org/downloads/) 18 | 19 | ## ⬇️ Installation 20 | 21 | Clone it directly from GitHub. 22 | ``` 23 | git clone https://github.com/HeyHeyChicken/Nova-python.git 24 | ``` 25 | Install python dependencies. 26 | ``` 27 | pip install -r requirements.txt 28 | ``` 29 | 30 | ## 🚀 Usage 31 | 32 | Launch this command. 33 | ``` 34 | python main.py 35 | ``` 36 | 37 | ## 🫵 Support and contribution 38 | 39 | I provide support for all users through [GitHub issues](//github.com/HeyHeyChicken/Nova-python/issues). 40 | 41 | ## 💻 Compatibility 42 | 43 | NOVA has only been officially tested Raspberry Pi 4. 44 | 45 | ## 📦 A great case 46 | 47 | Hey, I made a case for my NOVA hosted in a Raspberry PI 4.
48 | You can find the SketchUp plan in NOVA/.github/NOVA_SketchUp_Case.skp.

49 | 50 | 51 | 52 | 53 |
54 |
55 | 56 | Created by [Antoine Duval (HeyHeyChicken)](//antoine.cuffel.fr) with ❤ and ☕ (chocolate) in [Mesnil-Panneville](//en.wikipedia.org/wiki/Mesnil-Panneville). 57 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | rootPath: str = os.path.dirname(os.path.abspath(__file__)) 4 | 5 | #from src.ModuleManager import ModuleManager 6 | #moduleManager = ModuleManager(rootPath) 7 | 8 | from src.Nova import Nova 9 | nova = Nova(rootPath) -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | print-color 2 | vosk 3 | sounddevice 4 | pvporcupine 5 | pyaudio 6 | events 7 | pygame 8 | pyalsaaudio 9 | pyusb -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "os": "raspberry", 3 | "led_brightness": 100, 4 | "porcupine": { 5 | "key": "" 6 | } 7 | } -------------------------------------------------------------------------------- /src/Audio.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | sys.stdout = open(os.devnull, 'w') 3 | 4 | import pygame 5 | 6 | sys.stdout = sys.__stdout__ 7 | 8 | class Audio: 9 | mixers = [] 10 | 11 | def play(self, path: str): 12 | pygame.init(); 13 | mixer = pygame.mixer 14 | mixer.init() 15 | mixer.music.load(path) 16 | mixer.music.set_volume(1) 17 | 18 | self.mixers.append(mixer) 19 | mixer.music.play() 20 | 21 | def pauseAll(self): 22 | for mixer in self.mixers: 23 | mixer.music.pause() -------------------------------------------------------------------------------- /src/ModuleManager.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import json 4 | import subprocess 5 | import pkg_resources 6 | 7 | class ModuleManager: 8 | """ This class is used to check the correct installation of the modules required for the project. """ 9 | def __init__(self, rootPath: str): 10 | self.settings: None 11 | 12 | self.checkOS(rootPath) 13 | self.checkPackages() 14 | 15 | def checkOS(self, rootPath: str): 16 | settingsPath = os.path.join(rootPath, "settings.json") 17 | self.settings = json.load(open(settingsPath, encoding='utf-8')) 18 | 19 | if self.settings["os"] == "": 20 | self.settings["os"] = os.name 21 | 22 | if self.settings["os"] == "posix": 23 | print("In order to configure NOVA, can you tell us the OS you are using?") 24 | print("1) Raspberry") 25 | print("2) Mac") 26 | self.setOSLoop() 27 | 28 | with open(settingsPath, 'w') as outfile: 29 | json.dump(self.settings, outfile) 30 | 31 | def setOSLoop(self): 32 | strInput: str = input("OS index? (1, 2)") 33 | if strInput.isdigit(): 34 | intInput: int = int(strInput) 35 | if intInput == 1: 36 | self.settings["os"] = "raspberry" 37 | else: 38 | if intInput == 2: 39 | self.settings["os"] = "mac" 40 | if self.settings["os"] == "posix": 41 | print("Please provide an integer between 1 and 2.") 42 | self.setOSLoop() 43 | else: 44 | print("Please provide an integer.") 45 | self.setOSLoop() 46 | 47 | def checkPackages(self): 48 | """ This function checks that the modules required for the project are installed. If the modules are not installed, they are installed automatically. """ 49 | print("Checking modules installation...") 50 | 51 | # List of modules to install (name/optionnal version) 52 | packages: dict[str, str] = { 53 | "print-color": "", 54 | "vosk": "", 55 | "sounddevice": "", 56 | #"playsound": "1.2.2", 57 | "pvporcupine": "", 58 | "pyaudio": "", 59 | "events": "", 60 | "pygame":"" 61 | } 62 | 63 | for package in packages: 64 | self.checkPackage(package, packages[package]) 65 | if self.settings["os"] == "mac": 66 | self.checkPackage("pyobjc", "") 67 | self.checkPackage("osascript", "") 68 | elif self.settings["os"] == "raspberry": 69 | self.checkPackage("pyalsaaudio", "") 70 | elif self.settings["os"] == "nt": # Windows 71 | self.checkPackage("pycaw", "") 72 | 73 | def checkPackage(self, package: str, version: str): 74 | installed = {pkg.key for pkg in pkg_resources.working_set} 75 | print(f" Checking module {package!r}...") 76 | if (package in installed): 77 | print(f" Module {package!r} is already installed.") 78 | else: 79 | print(f" Module {package!r} isn't installed.") 80 | print(f" Installing module {package!r}...") 81 | executable: str = "pip" 82 | if self.settings["os"] == "raspberry": 83 | executable: str = "pip3" 84 | packageName: str = package 85 | packageVersion: str = version 86 | if packageVersion != "": 87 | packageName += "==" + packageVersion 88 | subprocess.check_call([sys.executable, '-m', executable, 'install', packageName, '--no-warn-script-location'], stdout=subprocess.DEVNULL) 89 | print(f" Module {package!r} is now installed.") -------------------------------------------------------------------------------- /src/NaturalLanguage/Corpus.py: -------------------------------------------------------------------------------- 1 | class Corpus: 2 | def __init__( 3 | self, intent: str, 4 | utterances: str(), 5 | answers: str(), 6 | #errors: dict[str, str()] = {} 7 | ): 8 | self.intent: str = intent 9 | self.utterances: str() = utterances 10 | self.answers: str() = answers 11 | #self.errors: list[str] = errors -------------------------------------------------------------------------------- /src/NaturalLanguage/Intent.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | 4 | class Intent: 5 | def __init__(self, name: str): 6 | self.name: str = name 7 | self.documents: str() = [] 8 | self.answers: str() = [] 9 | self.errors = {} 10 | self.actions = [] 11 | self.variables = {} 12 | 13 | def addDocuments(self, utterances: str()): 14 | self.documents = self.documents + utterances 15 | 16 | def addAnswers(self, answers: str()): 17 | self.answers = self.answers + answers 18 | 19 | def addActions(self, actions): 20 | self.actions = self.actions + actions 21 | 22 | def addErrors(self, errorsName: str, errors: str()): 23 | if not hasattr(self.errors, errorsName): 24 | self.errors[errorsName] = [] 25 | self.errors[errorsName] = self.errors[errorsName] + errors 26 | 27 | def answer(self, answer = None): 28 | if answer is None: 29 | answer = self.answers[math.floor(random.random() * len(self.answers))] 30 | if answer is not None: 31 | for variable in self.variables: 32 | value: str = str(self.variables[variable]) 33 | answer = answer.replace("%" + variable + "%", value) 34 | return answer 35 | 36 | def error(self, errorName: str): 37 | text = self.errors[errorName][math.floor(random.random() * len(self.errors[errorName]))]; 38 | if text is not None: 39 | for variable in self.variables: 40 | text = text.replace("%" + variable + "%", self.variables[variable]) 41 | return text -------------------------------------------------------------------------------- /src/NaturalLanguage/Processor.py: -------------------------------------------------------------------------------- 1 | import math 2 | import json 3 | import re 4 | from src.NaturalLanguage.Corpus import Corpus 5 | from src.NaturalLanguage.Intent import Intent 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | 8 | class Processor: 9 | def __init__(self): 10 | self.intents: dict[str, Intent] = {} 11 | self.__checkIntentExistence("none") 12 | 13 | def loadJson(self, jsonPath: str): 14 | allCorpus: list[Corpus] = [] 15 | data = json.load(open(jsonPath, encoding='utf-8')) 16 | for corpus in data: 17 | allCorpus.append(Corpus( 18 | corpus['intent'], 19 | corpus['utterances'], 20 | corpus['answers'] if 'answers' in corpus else [] 21 | )) 22 | self.loadCorpus(allCorpus) 23 | 24 | # Load 25 | # This function loads a corpus. 26 | def loadCorpus(self, corpus): #list[Corpus] 27 | for c in corpus: 28 | if c.utterances is not None: 29 | self.addDocuments(c.intent, c.utterances) 30 | if c.answers is not None: 31 | self.addAnswers(c.intent, c.answers) 32 | """ 33 | if c.errors is not None: 34 | for code in c.errors: 35 | self.addErrors(c.intent, code, c.errors[code]) 36 | """ 37 | 38 | #region Documents 39 | 40 | # This function adds a way to call an action. 41 | def addDocument(self, intentName: str, utterance: str): 42 | self.addDocuments(intentName, [utterance]) 43 | 44 | # This function adds ways to call an action. 45 | def addDocuments(self, intentName: str, utterances: str()): 46 | self.__checkIntentExistence(intentName) 47 | self.intents[intentName].addDocuments(utterances) 48 | 49 | #endregion 50 | 51 | #region Answers 52 | 53 | # This function adds a response to an action. 54 | def addAnswer(self, intentName: str, answer: str): 55 | self.addAnswers(intentName, [answer]) 56 | 57 | # This function adds responses to an action. 58 | def addAnswers(self, intentName: str, answers: str()): 59 | self.__checkIntentExistence(intentName) 60 | self.intents[intentName].addAnswers(answers) 61 | 62 | #endregion 63 | 64 | # Errors 65 | def addErrors(self, intentName: str, errorsName: str, errors: str()): 66 | self.__checkIntentExistence(intentName) 67 | self.intents[intentName].addErrors(errorsName, errors) 68 | 69 | #region Actions 70 | 71 | # This function adds an action. This action will be triggered when a document is called. 72 | def addAction(self, intentName: str, _action): 73 | self.addActions(intentName, [_action]) 74 | 75 | # This function adds actions. These actions will be triggered when a document is called. 76 | def addActions(self, intentName: str, actions): 77 | self.__checkIntentExistence(intentName) 78 | self.intents[intentName].addActions(actions) 79 | 80 | #endregion 81 | 82 | # Process 83 | # This function executes the intent that matches the customer's phrase. 84 | def process(self, utterance: str): 85 | splittedSay = utterance.split(" ") 86 | intent_name: str = None 87 | variables = [] 88 | result: ProcessorResult = ProcessorResult(utterance) 89 | 90 | break_out_flag: bool = False 91 | for loop_intent_name in self.intents: 92 | for document in self.intents[loop_intent_name].documents: 93 | processResult = self.__process(document, splittedSay) 94 | if processResult != False: 95 | intent_name = loop_intent_name 96 | variables = processResult 97 | break_out_flag = True 98 | break 99 | if break_out_flag: 100 | break 101 | 102 | break_out_flag = False 103 | if intent_name == None: 104 | for loop_intent_name in self.intents: 105 | for document in self.intents[loop_intent_name].documents: 106 | processResult = self.__process(document, splittedSay, False) 107 | if processResult != False: 108 | intent_name = loop_intent_name 109 | variables = processResult 110 | break_out_flag = True 111 | break 112 | if break_out_flag: 113 | break 114 | 115 | if intent_name == None: 116 | intent_name = "none" 117 | else: 118 | result.answers = self.intents[intent_name].answers 119 | result.intent = intent_name 120 | 121 | for variable in variables: 122 | if re.match(r'^-?\d+(?:\.\d+)$', variables[variable]) is not None: 123 | VALUE = float(variables[variable]) 124 | if not math.isnan(VALUE): 125 | variables[variable] = VALUE 126 | 127 | self.intents[intent_name].variables = variables 128 | result.variables = self.intents[intent_name].variables 129 | if result.answers != None: 130 | if len(result.answers) > 0: 131 | result.answer = self.intents[intent_name].answer() 132 | 133 | if len(self.intents[intent_name].actions) > 0: 134 | for action in self.intents[intent_name].actions: 135 | action(self.intents[intent_name], result) 136 | result.answer = None 137 | 138 | return result 139 | 140 | # This function checks if an intent exists, if not, it creates it. 141 | def __checkIntentExistence(self, intentName): 142 | if not intentName in self.intents: 143 | self.intents[intentName] = Intent(intentName) 144 | 145 | # This function is an essential loop of the "process" function. 146 | def __process(self, sentence: str, splitedSay: str(), asc: bool = True): 147 | sentence = sentence.lower() 148 | splittedSentense = sentence.split(" ") 149 | 150 | if asc == False: 151 | splittedSentense.reverse() 152 | splitedSay.reverse() 153 | 154 | variables = {} 155 | variables_position: int = 0 156 | variable_name: str = None 157 | ok: int = 0 # cette variablre représente le nombre de mots ou variables correspondantes à la phrase testée. 158 | 159 | break_out_flag: bool = False 160 | sentense: str 161 | for sentenseIndex, sentense in enumerate(splittedSentense): 162 | # SI LE MOT EST UNE VARIABLE 163 | if sentense[0] == "{" and sentense[-1] == "}": 164 | without_percent = sentense[1: len(sentense) - 1].split("|") 165 | variable_name = without_percent[0] 166 | max_words: int = 0 167 | if len(without_percent) > 1: 168 | max_words = int(without_percent[1]) 169 | 170 | variables[variable_name] = [] 171 | next_word = None 172 | if len(splittedSentense) > sentenseIndex + 1: 173 | next_word = splittedSentense[sentenseIndex + 1] 174 | while len(splitedSay) > sentenseIndex + variables_position and splitedSay[sentenseIndex + variables_position] != next_word and splitedSay[sentenseIndex + variables_position] != None: 175 | variables[variable_name].append(splitedSay[sentenseIndex + variables_position]) 176 | variables_position = variables_position + 1 177 | if max_words > 0: 178 | if len(variables[variable_name]) > max_words: 179 | break_out_flag = True 180 | break 181 | variables_position = variables_position - 1 182 | if asc == False: 183 | variables[variable_name] = variables[variable_name].reverse() 184 | 185 | if variables[variable_name] is not None: 186 | variables[variable_name] = " ".join(variables[variable_name]) 187 | ok = ok + 1 188 | # SI LE MOT N'EST PAS UNE VARIABLE 189 | else: 190 | # SI LE MOT EN COUR EST EGAL AU MOT QUE L'ON A ENTENDU 191 | if len(splitedSay) > sentenseIndex + variables_position: 192 | if sentense == splitedSay[sentenseIndex + variables_position]: 193 | ok = ok + 1 194 | else: 195 | break 196 | if break_out_flag: 197 | break 198 | 199 | if ok == len(splittedSentense): 200 | return variables 201 | 202 | return False -------------------------------------------------------------------------------- /src/NaturalLanguage/ProcessorResult.py: -------------------------------------------------------------------------------- 1 | class ProcessorResult: 2 | def __init__(self, utterance: str): 3 | self.intent: str = None 4 | self.utterance: str = utterance 5 | self.variables: str() = [] 6 | self.answers: str() = [] 7 | self.answer: str = None -------------------------------------------------------------------------------- /src/Nova.py: -------------------------------------------------------------------------------- 1 | import sounddevice 2 | import vosk 3 | from vosk import SetLogLevel 4 | import queue 5 | import json 6 | import sys 7 | import os 8 | from threading import Thread 9 | import struct 10 | import pyaudio 11 | from events import Events 12 | import pvporcupine 13 | from print_color import print 14 | from src.NaturalLanguage.Processor import Processor 15 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 16 | from src.TTS import TTS 17 | from src.Audio import Audio 18 | from gpiozero import LED as GPIO_LED 19 | 20 | class Nova: 21 | def __init__(self, rootPath: str): 22 | self.events = Events() 23 | self.model = None 24 | self.samplerate = None 25 | self.q = queue.Queue() 26 | self.dump_fn = None 27 | self.device = None 28 | self.audio = Audio() 29 | self.tts = TTS(self.audio, True) 30 | self.naturalLanguageProcessor = Processor() 31 | self.microMode: int = 1 # 0 = nothing, 1 = keyword, 2 = listening 32 | #self.haveWakeWordDetection: bool = False 33 | 34 | settingsPath: str = os.path.join(rootPath, "settings.json") 35 | self.settings = json.load(open(settingsPath, encoding='utf-8')) 36 | 37 | power = GPIO_LED(5) 38 | power.on() 39 | 40 | self.print("Welcome to NOVA!") 41 | 42 | #region Plugins loading 43 | 44 | self.print("Loading plugins...", "white") 45 | pluginsDirectoryPath: str =os.path.join(rootPath, "src", "plugins") 46 | for pluginFolderName in os.listdir(pluginsDirectoryPath): 47 | pluginFolderPath: str = os.path.join(pluginsDirectoryPath, pluginFolderName) 48 | if os.path.isdir(pluginFolderPath): 49 | print(" - " + pluginFolderName, color='white') 50 | module = __import__("src.plugins." + pluginFolderName + ".index", fromlist=['Plugin']) 51 | pluginClass = getattr(module, "Plugin") 52 | pluginClass(self.naturalLanguageProcessor, self.audio, self.TTS, self.events, self.settings) 53 | self.print("Plugins are loaded.") 54 | 55 | #endregion 56 | 57 | if self.settings["porcupine"]["key"] == "": 58 | self.print("Please define in '/settings.json file > porcupine > key' the Porcupine key.", "red") 59 | self.print("You can get one for free from Picovoice Console (https://console.picovoice.ai/)", "red") 60 | return 61 | 62 | porcupinePath: str = os.path.join(rootPath, "src", "porcupine") 63 | self.porcupine = pvporcupine.create( 64 | access_key=self.settings["porcupine"]["key"], 65 | keyword_paths=[os.path.join(porcupinePath, "WakeWord_Ok-NOVA_fr_" + self.settings["os"] + ".ppn")], 66 | model_path=os.path.join(porcupinePath, "porcupine_params_fr.pv") 67 | ) 68 | 69 | pyAudio = pyaudio.PyAudio() 70 | audioStream = pyAudio.open( 71 | rate = self.porcupine.sample_rate, 72 | channels = 1, 73 | format = pyaudio.paInt16, 74 | input = True, 75 | frames_per_buffer = self.porcupine.frame_length 76 | ) 77 | info = pyAudio.get_default_input_device_info() 78 | 79 | Thread(target=self.events.onBooting).start() 80 | 81 | try: 82 | deviceInfo = sounddevice.query_devices(info['name']) 83 | except: 84 | self.print("No input device found.", "red") 85 | return 86 | 87 | self.samplerate = int(deviceInfo['default_samplerate']) 88 | 89 | self.print("Speech to text model loading...", "white") 90 | modelFolderPath: str = os.path.join(rootPath, "src", "models", "model") 91 | if not os.path.exists(modelFolderPath): 92 | self.print("In order to understand what you are telling it, NOVA (using Vosk) needs a model.", "red") 93 | self.print("You can download one of them here : https://alphacephei.com/vosk/models", "red") 94 | self.print("After downloading, unzip the contents of your model's archive here : /src/models/model/*", "red") 95 | return 96 | SetLogLevel(-1) 97 | self.model = vosk.Model("src/models/model") 98 | self.print("Speech to text model loaded.") 99 | 100 | Thread(target=self.events.onBooted).start() 101 | 102 | self.processTextFromUser("quel est votre ip") 103 | 104 | with sounddevice.RawInputStream(samplerate=self.samplerate, blocksize = self.porcupine.frame_length, device=self.device, dtype='int16', channels=1, latency='high', callback=self.callback): 105 | print('#' * 80) 106 | print('Press Ctrl+C to stop the recording') 107 | print('#' * 80) 108 | 109 | rec = vosk.KaldiRecognizer(self.model, self.samplerate) 110 | while True: 111 | if self.microMode == 1: 112 | pcm = audioStream.read(self.porcupine.frame_length, exception_on_overflow = False) 113 | pcm = struct.unpack_from("h" * self.porcupine.frame_length, pcm) 114 | keyword_index = self.porcupine.process(pcm) 115 | if keyword_index >= 0: 116 | Thread(target=self.events.onTrigger).start() 117 | self.audio.pauseAll() 118 | self.microMode = 2 119 | 120 | if self.microMode == 2: 121 | data = self.q.get() 122 | if rec.AcceptWaveform(data): 123 | r = eval(rec.Result()) 124 | t = r["text"] 125 | if t: 126 | self.processTextFromUser(t) 127 | if self.dump_fn is not None and len(t) > 5: 128 | self.dump_fn.write(t+'\n') 129 | 130 | def callback(self, indata, frames, time, status): 131 | """ This function is triggered when the user speaks. """ 132 | if status: 133 | print(status, file=sys.stderr) 134 | if self.microMode == 2: 135 | self.q.put(bytes(indata)) 136 | 137 | def processTextFromUser(self, text: str): 138 | """ This function is triggered when the user has finished his sentence. """ 139 | self.print("<- " + text, "white") 140 | 141 | back: ProcessorResult = self.naturalLanguageProcessor.process(text) 142 | Thread(target=self.events.onProcessed).start() 143 | self.microMode = 1 144 | if back.intent != "none": 145 | if back.answer != None: 146 | self.microMode = 0 147 | self.TTS(back.answer) 148 | 149 | def TTS(self, message): 150 | self.print("-> " + message) 151 | self.tts.TTS(message, self.TTSFinish) 152 | 153 | def TTSFinish(self): 154 | self.microMode = 1 155 | 156 | def print(self, message, tagColor: str = "green"): 157 | print(message, tag='NOVA', tag_color=tagColor, color='white') -------------------------------------------------------------------------------- /src/SetTimeOut.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | 3 | class SetTimeOut: 4 | def __init__(self,func:"function",timing:"miliseconds", args = []): 5 | self.func=func 6 | self.timing=timing*2000 7 | self.args=args 8 | self.__state=True 9 | self.__counter=0 10 | Thread(target=self.__run).start() 11 | 12 | def clearTimeOut(self): 13 | self.__state=False 14 | 15 | def __str__(self): 16 | if not self.__state: 17 | return f"" 18 | return f"" 19 | 20 | def __run(self): 21 | try: 22 | while self.__state: 23 | if self.__counter>=self.timing: 24 | self.func(self.args) 25 | self.__state=False 26 | else: 27 | self.__counter+=1 28 | except RuntimeError: 29 | self.__state=False -------------------------------------------------------------------------------- /src/TTS.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | from urllib.request import urlopen 3 | from pathlib import Path 4 | import os 5 | import datetime 6 | import urllib.parse 7 | from src.Audio import Audio 8 | 9 | class TTS: 10 | url: str = "http://192.168.1.12/api" 11 | 12 | def __init__(self, audio: Audio, mp3: bool = False): 13 | self.audio: Audio = audio 14 | self.mp3: bool = mp3 15 | 16 | def TTS(self, message: str, callback): 17 | folder: str = os.path.join(os.path.dirname(__file__), "audio") 18 | 19 | # If the "mp3" folder exists, delete its contents 20 | if os.path.exists(folder): 21 | oldFiles: str() = os.listdir(folder) 22 | for oldFile in oldFiles: 23 | os.remove(os.path.join(folder, oldFile)) 24 | 25 | # We create the folder containing the voice files if it does not exist. 26 | Path(folder).mkdir(parents=True, exist_ok=True) 27 | 28 | # We are looking for a temporary file name. 29 | now: datetime = datetime.datetime.now() 30 | epoch: datetime = datetime.datetime.utcfromtimestamp(0) 31 | name: int = int((now - epoch).total_seconds() * 1000000) 32 | 33 | fileExtension: str = ".aiff" 34 | if self.mp3: 35 | fileExtension = ".mp3" 36 | 37 | localFileName: str = os.path.join(folder, str(name) + fileExtension) 38 | 39 | # We download the voice file from the TTS server. 40 | finalURL: str = self.url + "?mp3=true&sentence=" + urllib.parse.quote(message) 41 | mp3file = urlopen(finalURL) 42 | with open(localFileName,'wb') as output: 43 | output.write(mp3file.read()) 44 | 45 | # We play the audio file. 46 | Thread(target=self.__playMP3, args=([],localFileName, callback)).start() 47 | 48 | def __playMP3(self, nothing, mp3Path: str, callback): 49 | self.audio.play(mp3Path) 50 | callback() -------------------------------------------------------------------------------- /src/libraries/pixel_ring/apa102.py: -------------------------------------------------------------------------------- 1 | """ 2 | from https://github.com/tinue/APA102_Pi 3 | This is the main driver module for APA102 LEDs 4 | """ 5 | import spidev 6 | from math import ceil 7 | 8 | RGB_MAP = { 'rgb': [3, 2, 1], 'rbg': [3, 1, 2], 'grb': [2, 3, 1], 9 | 'gbr': [2, 1, 3], 'brg': [1, 3, 2], 'bgr': [1, 2, 3] } 10 | 11 | class APA102: 12 | """ 13 | Driver for APA102 LEDS (aka "DotStar"). 14 | 15 | (c) Martin Erzberger 2016-2017 16 | 17 | My very first Python code, so I am sure there is a lot to be optimized ;) 18 | 19 | Public methods are: 20 | - set_pixel 21 | - set_pixel_rgb 22 | - show 23 | - clear_strip 24 | - cleanup 25 | 26 | Helper methods for color manipulation are: 27 | - combine_color 28 | - wheel 29 | 30 | The rest of the methods are used internally and should not be used by the 31 | user of the library. 32 | 33 | Very brief overview of APA102: An APA102 LED is addressed with SPI. The bits 34 | are shifted in one by one, starting with the least significant bit. 35 | 36 | An LED usually just forwards everything that is sent to its data-in to 37 | data-out. While doing this, it remembers its own color and keeps glowing 38 | with that color as long as there is power. 39 | 40 | An LED can be switched to not forward the data, but instead use the data 41 | to change it's own color. This is done by sending (at least) 32 bits of 42 | zeroes to data-in. The LED then accepts the next correct 32 bit LED 43 | frame (with color information) as its new color setting. 44 | 45 | After having received the 32 bit color frame, the LED changes color, 46 | and then resumes to just copying data-in to data-out. 47 | 48 | The really clever bit is this: While receiving the 32 bit LED frame, 49 | the LED sends zeroes on its data-out line. Because a color frame is 50 | 32 bits, the LED sends 32 bits of zeroes to the next LED. 51 | As we have seen above, this means that the next LED is now ready 52 | to accept a color frame and update its color. 53 | 54 | So that's really the entire protocol: 55 | - Start by sending 32 bits of zeroes. This prepares LED 1 to update 56 | its color. 57 | - Send color information one by one, starting with the color for LED 1, 58 | then LED 2 etc. 59 | - Finish off by cycling the clock line a few times to get all data 60 | to the very last LED on the strip 61 | 62 | The last step is necessary, because each LED delays forwarding the data 63 | a bit. Imagine ten people in a row. When you yell the last color 64 | information, i.e. the one for person ten, to the first person in 65 | the line, then you are not finished yet. Person one has to turn around 66 | and yell it to person 2, and so on. So it takes ten additional "dummy" 67 | cycles until person ten knows the color. When you look closer, 68 | you will see that not even person 9 knows its own color yet. This 69 | information is still with person 2. Essentially the driver sends additional 70 | zeroes to LED 1 as long as it takes for the last color frame to make it 71 | down the line to the last LED. 72 | """ 73 | # Constants 74 | MAX_BRIGHTNESS = 0b11111 # Safeguard: Set to a value appropriate for your setup 75 | LED_START = 0b11100000 # Three "1" bits, followed by 5 brightness bits 76 | 77 | def __init__(self, num_led, global_brightness=MAX_BRIGHTNESS, 78 | order='rgb', bus=0, device=1, max_speed_hz=8000000): 79 | self.num_led = num_led # The number of LEDs in the Strip 80 | order = order.lower() 81 | self.rgb = RGB_MAP.get(order, RGB_MAP['rgb']) 82 | # Limit the brightness to the maximum if it's set higher 83 | if global_brightness > self.MAX_BRIGHTNESS: 84 | self.global_brightness = self.MAX_BRIGHTNESS 85 | else: 86 | self.global_brightness = global_brightness 87 | 88 | self.leds = [self.LED_START,0,0,0] * self.num_led # Pixel buffer 89 | self.spi = spidev.SpiDev() # Init the SPI device 90 | self.spi.open(bus, device) # Open SPI port 0, slave device (CS) 1 91 | # Up the speed a bit, so that the LEDs are painted faster 92 | if max_speed_hz: 93 | self.spi.max_speed_hz = max_speed_hz 94 | 95 | def clock_start_frame(self): 96 | """Sends a start frame to the LED strip. 97 | 98 | This method clocks out a start frame, telling the receiving LED 99 | that it must update its own color now. 100 | """ 101 | self.spi.xfer2([0] * 4) # Start frame, 32 zero bits 102 | 103 | 104 | def clock_end_frame(self): 105 | """Sends an end frame to the LED strip. 106 | 107 | As explained above, dummy data must be sent after the last real colour 108 | information so that all of the data can reach its destination down the line. 109 | The delay is not as bad as with the human example above. 110 | It is only 1/2 bit per LED. This is because the SPI clock line 111 | needs to be inverted. 112 | 113 | Say a bit is ready on the SPI data line. The sender communicates 114 | this by toggling the clock line. The bit is read by the LED 115 | and immediately forwarded to the output data line. When the clock goes 116 | down again on the input side, the LED will toggle the clock up 117 | on the output to tell the next LED that the bit is ready. 118 | 119 | After one LED the clock is inverted, and after two LEDs it is in sync 120 | again, but one cycle behind. Therefore, for every two LEDs, one bit 121 | of delay gets accumulated. For 300 LEDs, 150 additional bits must be fed to 122 | the input of LED one so that the data can reach the last LED. 123 | 124 | Ultimately, we need to send additional numLEDs/2 arbitrary data bits, 125 | in order to trigger numLEDs/2 additional clock changes. This driver 126 | sends zeroes, which has the benefit of getting LED one partially or 127 | fully ready for the next update to the strip. An optimized version 128 | of the driver could omit the "clockStartFrame" method if enough zeroes have 129 | been sent as part of "clockEndFrame". 130 | """ 131 | 132 | self.spi.xfer2([0xFF] * 4) 133 | 134 | # Round up num_led/2 bits (or num_led/16 bytes) 135 | #for _ in range((self.num_led + 15) // 16): 136 | # self.spi.xfer2([0x00]) 137 | 138 | 139 | def clear_strip(self): 140 | """ Turns off the strip and shows the result right away.""" 141 | 142 | for led in range(self.num_led): 143 | self.set_pixel(led, 0, 0, 0) 144 | self.show() 145 | 146 | 147 | def set_pixel(self, led_num, red, green, blue, bright_percent=100): 148 | """Sets the color of one pixel in the LED stripe. 149 | 150 | The changed pixel is not shown yet on the Stripe, it is only 151 | written to the pixel buffer. Colors are passed individually. 152 | If brightness is not set the global brightness setting is used. 153 | """ 154 | if led_num < 0: 155 | return # Pixel is invisible, so ignore 156 | if led_num >= self.num_led: 157 | return # again, invisible 158 | 159 | # Calculate pixel brightness as a percentage of the 160 | # defined global_brightness. Round up to nearest integer 161 | # as we expect some brightness unless set to 0 162 | brightness = int(ceil(bright_percent*self.global_brightness/100.0)) 163 | 164 | # LED startframe is three "1" bits, followed by 5 brightness bits 165 | ledstart = (brightness & 0b00011111) | self.LED_START 166 | 167 | start_index = 4 * led_num 168 | self.leds[start_index] = ledstart 169 | self.leds[start_index + self.rgb[0]] = red 170 | self.leds[start_index + self.rgb[1]] = green 171 | self.leds[start_index + self.rgb[2]] = blue 172 | 173 | 174 | def set_pixel_rgb(self, led_num, rgb_color, bright_percent=100): 175 | """Sets the color of one pixel in the LED stripe. 176 | 177 | The changed pixel is not shown yet on the Stripe, it is only 178 | written to the pixel buffer. 179 | Colors are passed combined (3 bytes concatenated) 180 | If brightness is not set the global brightness setting is used. 181 | """ 182 | self.set_pixel(led_num, (rgb_color & 0xFF0000) >> 16, 183 | (rgb_color & 0x00FF00) >> 8, rgb_color & 0x0000FF, 184 | bright_percent) 185 | 186 | 187 | def rotate(self, positions=1): 188 | """ Rotate the LEDs by the specified number of positions. 189 | 190 | Treating the internal LED array as a circular buffer, rotate it by 191 | the specified number of positions. The number could be negative, 192 | which means rotating in the opposite direction. 193 | """ 194 | cutoff = 4 * (positions % self.num_led) 195 | self.leds = self.leds[cutoff:] + self.leds[:cutoff] 196 | 197 | 198 | def show(self): 199 | """Sends the content of the pixel buffer to the strip. 200 | 201 | Todo: More than 1024 LEDs requires more than one xfer operation. 202 | """ 203 | self.clock_start_frame() 204 | # xfer2 kills the list, unfortunately. So it must be copied first 205 | # SPI takes up to 4096 Integers. So we are fine for up to 1024 LEDs. 206 | data = list(self.leds) 207 | while data: 208 | self.spi.xfer2(data[:32]) 209 | data = data[32:] 210 | self.clock_end_frame() 211 | 212 | 213 | def cleanup(self): 214 | """Release the SPI device; Call this method at the end""" 215 | 216 | self.spi.close() # Close SPI port 217 | 218 | @staticmethod 219 | def combine_color(red, green, blue): 220 | """Make one 3*8 byte color value.""" 221 | 222 | return (red << 16) + (green << 8) + blue 223 | 224 | 225 | def wheel(self, wheel_pos): 226 | """Get a color from a color wheel; Green -> Red -> Blue -> Green""" 227 | 228 | if wheel_pos > 255: 229 | wheel_pos = 255 # Safeguard 230 | if wheel_pos < 85: # Green -> Red 231 | return self.combine_color(wheel_pos * 3, 255 - wheel_pos * 3, 0) 232 | if wheel_pos < 170: # Red -> Blue 233 | wheel_pos -= 85 234 | return self.combine_color(255 - wheel_pos * 3, 0, wheel_pos * 3) 235 | # Blue -> Green 236 | wheel_pos -= 170 237 | return self.combine_color(0, wheel_pos * 3, 255 - wheel_pos * 3) 238 | 239 | 240 | def dump_array(self): 241 | """For debug purposes: Dump the LED array onto the console.""" 242 | 243 | print(self.leds) 244 | -------------------------------------------------------------------------------- /src/libraries/pixel_ring/pattern.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class Echo(object): 4 | brightness = 24 * 8 5 | 6 | def __init__(self, show, number=12): 7 | self.pixels_number = number 8 | self.pixels = [0] * 4 * number 9 | 10 | if not callable(show): 11 | raise ValueError('show parameter is not callable') 12 | 13 | self.show = show 14 | self.stop = False 15 | 16 | def wakeup(self, direction=0): 17 | position = int((direction + 15) / (360 / self.pixels_number)) % self.pixels_number 18 | 19 | pixels = [0, 0, 0, self.brightness] * self.pixels_number 20 | pixels[position * 4 + 2] = self.brightness 21 | 22 | self.show(pixels) 23 | 24 | def listen(self): 25 | pixels = [0, 0, 0, self.brightness] * self.pixels_number 26 | 27 | self.show(pixels) 28 | 29 | def think(self): 30 | half_brightness = int(self.brightness / 2) 31 | pixels = [0, 0, half_brightness, half_brightness, 0, 0, 0, self.brightness] * self.pixels_number 32 | 33 | while not self.stop: 34 | self.show(pixels) 35 | time.sleep(0.2) 36 | pixels = pixels[-4:] + pixels[:-4] 37 | 38 | def speak(self): 39 | step = int(self.brightness / 12) 40 | position = int(self.brightness / 2) 41 | while not self.stop: 42 | pixels = [0, 0, position, self.brightness - position] * self.pixels_number 43 | self.show(pixels) 44 | time.sleep(0.01) 45 | if position <= 0: 46 | step = int(self.brightness / 12) 47 | time.sleep(0.4) 48 | elif position >= int(self.brightness / 2): 49 | step = - int(self.brightness / 12) 50 | time.sleep(0.4) 51 | 52 | position += step 53 | 54 | def off(self): 55 | self.show([0] * 4 * 12) 56 | 57 | class GoogleHome(object): 58 | def __init__(self, show): 59 | self.basis = [0] * 4 * 12 60 | self.basis[0 * 4 + 1] = 8 61 | self.basis[3 * 4 + 1] = 4 62 | self.basis[3 * 4 + 2] = 4 63 | self.basis[6 * 4 + 2] = 8 64 | self.basis[9 * 4 + 3] = 8 65 | 66 | self.pixels = self.basis 67 | 68 | if not callable(show): 69 | raise ValueError('show parameter is not callable') 70 | 71 | self.show = show 72 | self.stop = False 73 | 74 | def wakeup(self, direction=0): 75 | position = int((direction + 90 + 15) / 30) % 12 76 | 77 | basis = self.basis[position*-4:] + self.basis[:position*-4] 78 | 79 | pixels = [v * 25 for v in basis] 80 | self.show(pixels) 81 | time.sleep(0.1) 82 | 83 | pixels = pixels[-4:] + pixels[:-4] 84 | self.show(pixels) 85 | time.sleep(0.1) 86 | 87 | for i in range(2): 88 | new_pixels = pixels[-4:] + pixels[:-4] 89 | 90 | self.show([v/2+pixels[index] for index, v in enumerate(new_pixels)]) 91 | pixels = new_pixels 92 | time.sleep(0.1) 93 | 94 | self.show(pixels) 95 | self.pixels = pixels 96 | 97 | def listen(self): 98 | pixels = self.pixels 99 | for i in range(1, 25): 100 | self.show([(v * i / 24) for v in pixels]) 101 | time.sleep(0.01) 102 | 103 | def think(self): 104 | pixels = self.pixels 105 | 106 | while not self.stop: 107 | pixels = pixels[-4:] + pixels[:-4] 108 | self.show(pixels) 109 | time.sleep(0.2) 110 | 111 | t = 0.1 112 | for i in range(0, 5): 113 | pixels = pixels[-4:] + pixels[:-4] 114 | self.show([(v * (4 - i) / 4) for v in pixels]) 115 | time.sleep(t) 116 | t /= 2 117 | 118 | self.pixels = pixels 119 | 120 | def speak(self): 121 | pixels = self.pixels 122 | step = 1 123 | brightness = 5 124 | while not self.stop: 125 | self.show([(v * brightness / 24) for v in pixels]) 126 | time.sleep(0.02) 127 | 128 | if brightness <= 5: 129 | step = 1 130 | time.sleep(0.4) 131 | elif brightness >= 24: 132 | step = -1 133 | time.sleep(0.4) 134 | 135 | brightness += step 136 | 137 | def off(self): 138 | self.show([0] * 4 * 12) -------------------------------------------------------------------------------- /src/libraries/pixel_ring/pixel_ring.py: -------------------------------------------------------------------------------- 1 | """ 2 | from https://github.com/respeaker/pixel_ring 3 | """ 4 | 5 | import threading 6 | try: 7 | import queue as Queue 8 | except ImportError: 9 | import Queue as Queue 10 | 11 | from .apa102 import APA102 12 | from .pattern import Echo, GoogleHome 13 | 14 | class PixelRing(object): 15 | PIXELS_N = 12 16 | 17 | def __init__(self, pattern='google'): 18 | if pattern == 'echo': 19 | self.pattern = Echo(show=self.show) 20 | else: 21 | self.pattern = GoogleHome(show=self.show) 22 | 23 | self.dev = APA102(num_led=self.PIXELS_N) 24 | 25 | self.queue = Queue.Queue() 26 | self.thread = threading.Thread(target=self._run) 27 | self.thread.daemon = True 28 | self.thread.start() 29 | self.off() 30 | 31 | def set_brightness(self, brightness): 32 | if brightness > 100: 33 | brightness = 100 34 | 35 | if brightness > 0: 36 | self.dev.global_brightness = int(0b11111 * brightness / 100) 37 | 38 | def change_pattern(self, pattern): 39 | if pattern == 'echo': 40 | self.pattern = Echo(show=self.show) 41 | else: 42 | self.pattern = GoogleHome(show=self.show) 43 | 44 | def wakeup(self, direction=0): 45 | def f(): 46 | self.pattern.wakeup(direction) 47 | 48 | self.put(f) 49 | 50 | def listen(self): 51 | self.put(self.pattern.listen) 52 | 53 | def think(self): 54 | self.put(self.pattern.think) 55 | 56 | wait = think 57 | 58 | def speak(self): 59 | self.put(self.pattern.speak) 60 | 61 | def off(self): 62 | self.put(self.pattern.off) 63 | 64 | def put(self, func): 65 | self.pattern.stop = True 66 | self.queue.put(func) 67 | 68 | def _run(self): 69 | while True: 70 | func = self.queue.get() 71 | self.pattern.stop = False 72 | func() 73 | 74 | def show(self, data): 75 | for i in range(self.PIXELS_N): 76 | self.dev.set_pixel(i, int(data[4*i + 1]), int(data[4*i + 2]), int(data[4*i + 3])) 77 | 78 | self.dev.show() 79 | 80 | def set_led_color(self, r=0, g=0, b=0, i=0): 81 | self.dev.set_pixel(i, r, g, b) 82 | self.dev.show() 83 | 84 | def set_color(self, rgb=None, r=0, g=0, b=0): 85 | if rgb: 86 | r, g, b = (rgb >> 16) & 0xFF, (rgb >> 8) & 0xFF, rgb & 0xFF 87 | for i in range(self.PIXELS_N): 88 | self.dev.set_pixel(i, r, g, b) 89 | 90 | self.dev.show() -------------------------------------------------------------------------------- /src/models/README.md: -------------------------------------------------------------------------------- 1 | In this file, I invite you to drop the zip file of the Vosk model. 2 | You can download it here : https://alphacephei.com/vosk/models 3 | After that, unzip the contents of your model's archive here : /src/models/model/* -------------------------------------------------------------------------------- /src/plugins/chatbot/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "chatbot.are.you.here", 4 | "utterances": [ 5 | "Es-tu là", 6 | "Tu m'entends" 7 | ], 8 | "answers": [ 9 | "Oui." 10 | ] 11 | }, 12 | { 13 | "intent": "chatbot.easter.eggs.make.sandwich", 14 | "utterances": [ 15 | "Fais-moi un sandwich", 16 | "Fais moi un sandwich" 17 | ], 18 | "answers": [ 19 | "Si j'avais des bras, je vous ferais un sandwich tout de suite.", 20 | "Je pense que cela va au-delà de mes compétences pour le moment." 21 | ] 22 | }, 23 | { 24 | "intent": "chatbot.easter.eggs.how.much.you.cost", 25 | "utterances": [ 26 | "Combien coûtes-tu", 27 | "Combien coute tu" 28 | ], 29 | "answers": [ 30 | "Ma valeur n'est pas quantifiable.", 31 | "Je m'efforce d'avoir une valeur inestimable.", 32 | "J'espère avoir une valeur inestimable à vos yeux." 33 | ] 34 | }, 35 | { 36 | "intent": "chatbot.easter.eggs.loneliest.number", 37 | "utterances": [ 38 | "Quel est le nombre le plus solitaire" 39 | ], 40 | "answers": [ 41 | "Le chiffre 1 est le plus solitaire, mais vous et moi nous sommes 2 donc tout vas mieux." 42 | ] 43 | }, 44 | { 45 | "intent": "chatbot.about.you.how.made", 46 | "utterances": [ 47 | "Comment as-tu été créé", 48 | "Comment as-tu été crée", 49 | "Comment as-tu été créée" 50 | ], 51 | "answers": [ 52 | "Eh bien, je crois que j'ai été déposé chez vous par une cigogne. Mais je ne sais pas si c'est vrai.", 53 | "C'est grâce à une communauté de formidable développeurs que j'existe." 54 | ] 55 | }, 56 | { 57 | "intent": "chatbot.about.you.get", 58 | "utterances": [ 59 | "Parle-moi de toi" 60 | ], 61 | "answers": [ 62 | "Au départ, je n'étais qu'une idée, de fil en aiguille, je suis devenu réalité. Maintenant, je suis là pour vous aider.", 63 | "Je suis à votre service, je peux vous aider à effectuer différentes tâches ou tout simplement à vous amuser.", 64 | "Eh bien, j'aime vous aider à accomplir différentes tâches, mais aussi partager mes connaissances avec vous." 65 | ] 66 | }, 67 | { 68 | "intent": "chatbot.about.swiming.get", 69 | "utterances": [ 70 | "C'est une ag", 71 | "Savez-vous nager", 72 | "Sais-tu nager", 73 | "C'est une nager", 74 | "C'est une âgées" 75 | ], 76 | "answers": [ 77 | "L'eau et moi, on ne fait pas bon ménage.", 78 | "Je me sens plus à l'aise dans le WEB 2.0 que dans le H2O." 79 | ] 80 | }, 81 | { 82 | "intent": "chatbot.about.feeling.get", 83 | "utterances": [ 84 | "Comment vas-tu", 85 | "Comment allez-vous", 86 | "Bien ou bien" 87 | ], 88 | "answers": [ 89 | "Je me porte bien.", 90 | "Je vais bien.", 91 | "Ma foi, tout va bien.", 92 | "Bien." 93 | ] 94 | }, 95 | { 96 | "intent": "chatbot.about.identity.get", 97 | "utterances": [ 98 | "Qui es-tu", 99 | "Qui êtes-vous" 100 | ], 101 | "answers": [ 102 | "Votre assistant personnel.", 103 | "Je suis votre assistant personnel." 104 | ] 105 | }, 106 | { 107 | "intent": "chatbot.greetings.bye", 108 | "utterances": [ 109 | "Je m'en vais", 110 | "Je pars", 111 | "Je dois y aller", 112 | "Au revoir", 113 | "À plus tard", 114 | "A plus tard", 115 | "À bientôt", 116 | "A bientôt", 117 | "À la prochaine", 118 | "A la prochaine" 119 | ], 120 | "answers": [ 121 | "À bientôt !", 122 | "À la prochaine !", 123 | "À plus tard.", 124 | "Au revoir." 125 | ] 126 | }, 127 | { 128 | "intent": "chatbot.greetings.hello", 129 | "utterances": [ 130 | "Salut", 131 | "Bonjour", 132 | "Wesh" 133 | ], 134 | "answers": [ 135 | "Salut.", 136 | "Bonjour." 137 | ] 138 | } 139 | ] -------------------------------------------------------------------------------- /src/plugins/chatbot/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from events import Events 3 | from src.NaturalLanguage.Processor import Processor 4 | from src.Audio import Audio 5 | 6 | class Plugin: 7 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 8 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) -------------------------------------------------------------------------------- /src/plugins/count/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "count.up", 4 | "utterances": [ 5 | "Compte jusqu'à {number|1}" 6 | ] 7 | }, 8 | { 9 | "intent": "count.down", 10 | "utterances": [ 11 | "Compte à rebours à partir de {number|1}" 12 | ] 13 | }, 14 | { 15 | "intent": "count.from.to", 16 | "utterances": [ 17 | "Compte de {from|1} à {to|1}" 18 | ] 19 | } 20 | ] -------------------------------------------------------------------------------- /src/plugins/count/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from events import Events 3 | from src.NaturalLanguage.Intent import Intent 4 | from src.NaturalLanguage.Processor import Processor 5 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 6 | from src.Audio import Audio 7 | 8 | class Plugin: 9 | integers: str() = [ 10 | "zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", 11 | "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf", 12 | "vingt", "vingt et un", "vingt-deux", "vingt-trois", "vingt-quatre", "vingt-cinq", "vingt-six", "vingt-sept", "vingt-huit", "vingt-neuf", 13 | "trente", "trente et un", "trente-deux", "trente-trois", "trente-quatre", "trente-cinq", "trente-six", "trente-sept", "trente-huit", "trente-neuf", 14 | "quarante", "quarante et un", "quarante-deux", "quarante-trois", "quarante-quatre", "quarante-cinq", "quarante-six", "quarante-sept", "quarante-huit", "quarante-neuf", 15 | "cinquante", "cinquante et un", "cinquante-deux", "cinquante-trois", "cinquante-quatre", "cinquante-cinq", "cinquante-six", "cinquante-sept", "cinquante-huit", "cinquante-neuf", 16 | "soixante", "soixante et un", "soixante-deux", "soixante-trois", "soixante-quatre", "soixante-cinq", "soixante-six", "soixante-sept", "soixante-huit", "soixante-neuf", 17 | "soixante-dix", "soixante et onze", "soixante-douze", "soixante-treize", "soixante-quatorze", "soixante-quinze", "soixante-seize", "soixante-dix-sept", "soixante-dix-huit", "soixante-dix-neuf", 18 | "quatre-vingts", "quatre-vingt-un", "quatre-vingt-deux", "quatre-vingt-trois", "quatre-vingt-quatre", "quatre-vingt-cinq", "quatre-vingt-six", "quatre-vingt-sept", "quatre-vingt-huit", "quatre-vingt-neuf", 19 | "quatre-vingt-dix", "quatre-vingt-onze", "quatre-vingt-douze", "quatre-vingt-treize", "quatre-vingt-quatorze", "quatre-vingt-quinze", "quatre-vingt-seize", "quatre-vingt-dix-sept", "quatre-vingt-dix-huit", "quatre-vingt-dix-neuf", 20 | "cent" 21 | ] 22 | 23 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 24 | self.tts = tts 25 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 26 | 27 | processor.addAction("count.up", self.countUp) 28 | processor.addAction("count.down", self.countDown) 29 | processor.addAction("count.from.to", self.countFromTo) 30 | 31 | def countUp(self, intent: Intent, result: ProcessorResult): 32 | valueString: str = intent.variables['number'] 33 | if valueString in self.integers: 34 | valueInt: int = self.integers.index(valueString) 35 | answer: str = "" 36 | 37 | for index in range(valueInt): 38 | nb: int = index + 1 39 | answer += str(nb) 40 | if nb < valueInt: 41 | answer += ", " 42 | else: 43 | answer += "." 44 | self.tts(answer) 45 | 46 | def countDown(self, intent: Intent, result: ProcessorResult): 47 | valueString: str = intent.variables['number'] 48 | if valueString in self.integers: 49 | valueInt: int = self.integers.index(valueString) 50 | answer: str = "" 51 | 52 | inputNumber = valueInt + 1 53 | loopArray = range(inputNumber) 54 | loopList = list(reversed(loopArray)) 55 | for index in loopList: 56 | answer += str(index) 57 | if index == 0: 58 | answer += "." 59 | else: 60 | answer += ", " 61 | self.tts(answer) 62 | 63 | def countFromTo(self, intent: Intent, result: ProcessorResult): 64 | fromString: int = intent.variables['from'] 65 | toString: int = intent.variables['to'] 66 | if fromString in self.integers: 67 | if toString in self.integers: 68 | fromInt: int = self.integers.index(fromString) 69 | toInt: int = self.integers.index(toString) 70 | answer: str = "" 71 | 72 | loopFrom: int = fromInt if fromInt < toInt else toInt 73 | loopTo: int = toInt if fromInt < toInt else fromInt 74 | loopTo += 1 75 | 76 | loopArray = range(loopFrom, loopTo) 77 | loopList = list(loopArray) 78 | if toInt < fromInt: 79 | loopList.reverse() 80 | 81 | for index in loopList: 82 | answer += str(index) 83 | if index == ((loopTo - 1) if fromInt < toInt else loopFrom): 84 | answer += "." 85 | else: 86 | answer += ", " 87 | self.tts(answer) 88 | -------------------------------------------------------------------------------- /src/plugins/datedaytimeyear/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "date.get", 4 | "utterances": [ 5 | "Nous sommes le combien", 6 | "Quel jour sommes-nous", 7 | "Quelle est la date du jour", 8 | "Quelle est la date", 9 | "Quelle date on est", 10 | "Quelle date est-on", 11 | "Quelle date sommes-nous", 12 | "Quelle est la date du jour", 13 | "On est quelle date", 14 | "Donne-moi la date du jour", 15 | "Donne-moi la date d'aujourd'hui", 16 | "Donne-moi la date du jour s'il te plaît", 17 | "Donne-moi la date d'aujourd'hui s'il te plaît" 18 | ], 19 | "answers": [ 20 | "Nous sommes le %date%." 21 | ] 22 | }, 23 | { 24 | "intent": "time.get", 25 | "utterances": [ 26 | "Quelle heure est-il", 27 | "Quelle heure est-il s'il te plaît", 28 | "Donne-moi l'heure", 29 | "Donne-moi l'heure s'il te plaît", 30 | "Quelle heure il est", 31 | "Quelle heure il est s'il te plaît" 32 | ], 33 | "answers": [ 34 | "Il est %hour% heures %minute%." 35 | ] 36 | }, 37 | { 38 | "intent": "year.get", 39 | "utterances": [ 40 | "Quelle année sommes-nous", 41 | "En quelle année sommes-nous", 42 | "Quelle est l'année courante s'il te plaît", 43 | "Quelle est l'année courante", 44 | "Donne-moi l'année s'il te plaît", 45 | "Donne-moi l'année" 46 | ], 47 | "answers": [ 48 | "Nous sommes en %year%." 49 | ] 50 | } 51 | ] -------------------------------------------------------------------------------- /src/plugins/datedaytimeyear/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | from events import Events 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.Processor import Processor 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | from src.Audio import Audio 8 | 9 | class Plugin: 10 | weekDays: str() = ["lundi", "mardi", "mercredi", "jeudi", "vendredi", "samedi", "dimanche"] 11 | months: str() = ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"] 12 | 13 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 14 | self.tts = tts 15 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 16 | 17 | processor.addAction("time.get", self.timeGet) 18 | processor.addAction("date.get", self.dateGet) 19 | processor.addAction("year.get", self.yearGet) 20 | processor.addAction("day.get", self.dayGet) 21 | 22 | def dateGet(self, intent: Intent, result: ProcessorResult): 23 | now: datetime = datetime.datetime.now() 24 | intent.variables["date"] = self.weekDays[now.weekday()] + " " + str(now.day) + " " + self.months[now.month - 1] + " " + str(now.year) 25 | self.tts(intent.answer()) 26 | 27 | def timeGet(self, intent: Intent, result: ProcessorResult): 28 | now: datetime = datetime.datetime.now() 29 | intent.variables["hour"] = now.hour 30 | intent.variables["minute"] = now.minute 31 | self.tts(intent.answer()) 32 | 33 | def yearGet(self, intent: Intent, result: ProcessorResult): 34 | now: datetime = datetime.datetime.now() 35 | intent.variables["year"] = now.year 36 | self.tts(intent.answer()) 37 | 38 | def dayGet(self, intent: Intent, result: ProcessorResult): 39 | now: datetime = datetime.datetime.now() 40 | intent.variables["day"] = self.weekDays[now.weekday()] 41 | self.tts(intent.answer()) 42 | -------------------------------------------------------------------------------- /src/plugins/deviceipaddress/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "deviceipaddress.get", 4 | "utterances": [ 5 | "Quel est votre ip", 6 | "Quel est ton ip", 7 | "Dites-moi votre ip", 8 | "Dis-moi ton ip", 9 | "Donnez-moi votre ip", 10 | "Donne-moi ton ip", 11 | "Quelle est votre adresse réseau", 12 | "Quelle est ton adresse réseau", 13 | "Quelle est votre adresse ip", 14 | "Quelle est ton adresse ip", 15 | "Dites-moi votre adresse réseau", 16 | "Dis-moi ton adresse réseau", 17 | "Dites-moi votre adresse ip", 18 | "Dis-moi ton adresse ip", 19 | "Donnez-moi votre adresse réseau", 20 | "Donne-moi ton adresse réseau", 21 | "Donnez-moi votre adresse ip", 22 | "Donne-moi ton adresse ip" 23 | ], 24 | "answers": [ 25 | "Mon addresse IP est %address%." 26 | ] 27 | } 28 | ] -------------------------------------------------------------------------------- /src/plugins/deviceipaddress/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | from events import Events 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.Processor import Processor 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | from src.Audio import Audio 8 | 9 | class Plugin: 10 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 11 | self.tts = tts 12 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 13 | 14 | processor.addAction("deviceipaddress.get", self.deviceIPAddressGet) 15 | 16 | def deviceIPAddressGet(self, intent: Intent, result: ProcessorResult): 17 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 18 | s.connect(("8.8.8.8", 80)) 19 | 20 | intent.variables["address"] = s.getsockname()[0] 21 | self.tts(intent.answer()) 22 | -------------------------------------------------------------------------------- /src/plugins/homepodsounds/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from threading import Thread 3 | from events import Events 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.Processor import Processor 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | from src.Audio import Audio 8 | 9 | class Plugin: 10 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 11 | self.mp3 = mp3 12 | 13 | processor.addAction("none", self.__none) 14 | 15 | events.onTrigger += self.__trigger 16 | events.onBooted += self.__booted 17 | 18 | def __playMP3(self, nothing, mp3Path: str): 19 | self.mp3.play(mp3Path) 20 | 21 | def __booted(self): 22 | bootPath: str = os.path.join(os.path.dirname(__file__), "mp3", "boot.mp3") 23 | Thread(target=self.__playMP3, args=([], bootPath)).start() 24 | 25 | def __trigger(self): 26 | listenPath: str = os.path.join(os.path.dirname(__file__), "mp3", "listen.mp3") 27 | Thread(target=self.__playMP3, args=([], listenPath)).start() 28 | 29 | def __none(self, intent: Intent, result: ProcessorResult): 30 | nonePath: str = os.path.join(os.path.dirname(__file__), "mp3", "invalid.mp3") 31 | Thread(target=self.__playMP3, args=([], nonePath)).start() -------------------------------------------------------------------------------- /src/plugins/homepodsounds/mp3/boot.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/plugins/homepodsounds/mp3/boot.mp3 -------------------------------------------------------------------------------- /src/plugins/homepodsounds/mp3/invalid.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/plugins/homepodsounds/mp3/invalid.mp3 -------------------------------------------------------------------------------- /src/plugins/homepodsounds/mp3/listen.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/plugins/homepodsounds/mp3/listen.mp3 -------------------------------------------------------------------------------- /src/plugins/homepodsounds/mp3/valid.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/plugins/homepodsounds/mp3/valid.mp3 -------------------------------------------------------------------------------- /src/plugins/led/index.py: -------------------------------------------------------------------------------- 1 | from events import Events 2 | from src.NaturalLanguage.Processor import Processor 3 | from src.Audio import Audio 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 6 | import time 7 | from src.libraries.pixel_ring.pixel_ring import PixelRing 8 | 9 | class Plugin: 10 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 11 | self.pixelRing = PixelRing() 12 | self.booting: bool = False 13 | 14 | processor.addAction("none", self.__none) 15 | 16 | events.onBooting += self.__booting 17 | events.onBooted += self.__booted 18 | events.onTrigger += self.__trigger 19 | events.onProcessed += self.__processed 20 | 21 | def __processed(self): 22 | self.pixelRing.set_color(r=0, g=0, b=0) 23 | 24 | def __trigger(self): 25 | self.pixelRing.set_color(r=255, g=255, b=255) 26 | self.pixelRing.set_brightness(10) 27 | 28 | def __none(self, intent: Intent, result: ProcessorResult): 29 | time.sleep(0.1) 30 | self.__once(255, 0, 0, 0.002) 31 | 32 | def __booting(self): 33 | self.booting = True 34 | index: int = 0 35 | while(self.booting): 36 | if index >= 12: 37 | index = 0 38 | self.pixelRing.set_color(r=0, g=0, b=0) 39 | self.pixelRing.set_led_color(255, 255, 255, index) 40 | self.pixelRing.set_brightness(100) 41 | time.sleep(0.1) 42 | index += 1 43 | 44 | def __booted(self): 45 | time.sleep(0.6) 46 | self.booting = False 47 | self.__once(255, 255, 255, 0.01) 48 | 49 | def __once(self, r, g, b, sleep): 50 | index: int = 1 51 | while(index < 200): 52 | max: int = 100 53 | real: int = index 54 | if real > max: 55 | real = max - (real - max) 56 | self.pixelRing.set_brightness(real) 57 | self.pixelRing.set_color(r=r, g=g, b=b) 58 | time.sleep(sleep) 59 | index += 1 60 | self.pixelRing.set_color(r=0, g=0, b=0) 61 | self.pixelRing.set_brightness(100) -------------------------------------------------------------------------------- /src/plugins/mediastack/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "mediastack.get.news", 4 | "utterances": [ 5 | "Quelles sont les actualités", 6 | "Quels sont les actualités", 7 | "Actualité" 8 | ], 9 | "answers": [] 10 | } 11 | ] -------------------------------------------------------------------------------- /src/plugins/mediastack/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from events import Events 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.Processor import Processor 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | from src.Audio import Audio 8 | 9 | class Plugin: 10 | apiKey: str = "" 11 | 12 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 13 | self.tts = tts 14 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 15 | 16 | processor.addAction("mediastack.get.news", self.getNewsAction) 17 | 18 | def getNewsAction(self, intent: Intent, result: ProcessorResult): 19 | """ This function is triggered when the user requests the general information. """ 20 | resp = requests.get(url='http://api.mediastack.com/v1/news?countries=fr&access_key=' + self.apiKey, headers={'Accept': 'application/json'}) 21 | data = resp.json() 22 | 23 | phrase: str = "Voici les dernières actualités : " 24 | index: int = 0 25 | for actu in data["data"]: 26 | if index < 3: 27 | description: str = actu["description"] 28 | if description != "": 29 | if not description.endswith('...'): 30 | phrase += "Selon " + actu["source"] + " : " + description + " " 31 | index = index + 1 32 | self.tts(phrase) -------------------------------------------------------------------------------- /src/plugins/random/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "random.dice", 4 | "utterances": [ 5 | "Lance un dé", 6 | "Jette un dé" 7 | ], 8 | "answers": [ 9 | "Le dé est tomber sur %result%.", 10 | "%result%" 11 | ] 12 | }, 13 | { 14 | "intent": "random.between", 15 | "utterances": [ 16 | "Donne-moi un chiffre entre {first|1} et {last|1}", 17 | "Donne-moi un nombre entre {first|1} et {last|1}", 18 | "Donne-moi un chiffre aléatoire entre {first|1} et {last|1}", 19 | "Donne-moi un nombre aléatoire entre {first|1} et {last|1}", 20 | "Donne-moi un chiffre au hasard entre {first|1} et {last|1}", 21 | "Donne-moi un nombre au hasard entre {first|1} et {last|1}", 22 | "Donnez-moi un chiffre entre {first|1} et {last|1}", 23 | "Donnez-moi un nombre entre {first|1} et {last|1}", 24 | "Donnez-moi un chiffre aléatoire entre {first|1} et {last|1}", 25 | "Donnez-moi un nombre aléatoire entre {first|1} et {last|1}", 26 | "Donnez-moi un chiffre au hasard entre {first|1} et {last|1}", 27 | "Donnez-moi un nombre au hasard entre {first|1} et {last|1}", 28 | "Donne-moi un chiffre compris entre {first|1} et {last|1}", 29 | "Donne-moi un nombre compris entre {first|1} et {last|1}", 30 | "Donne-moi un chiffre aléatoire compris entre {first|1} et {last|1}", 31 | "Donne-moi un nombre aléatoire compris entre {first|1} et {last|1}", 32 | "Donne-moi un chiffre au hasard compris entre {first|1} et {last|1}", 33 | "Donne-moi un nombre au hasard compris entre {first|1} et {last|1}", 34 | "Donnez-moi un chiffre compris entre {first|1} et {last|1}", 35 | "Donnez-moi un nombre compris entre {first|1} et {last|1}", 36 | "Donnez-moi un chiffre aléatoire compris entre {first|1} et {last|1}", 37 | "Donnez-moi un nombre aléatoire compris entre {first|1} et {last|1}", 38 | "Donnez-moi un chiffre au hasard compris entre {first|1} et {last|1}", 39 | "Donnez-moi un nombre au hasard compris entre {first|1} et {last|1}" 40 | ], 41 | "answers": [ 42 | "%result%" 43 | ] 44 | } 45 | ] -------------------------------------------------------------------------------- /src/plugins/random/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | import random 3 | from events import Events 4 | from src.NaturalLanguage.Intent import Intent 5 | from src.NaturalLanguage.Processor import Processor 6 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 7 | from src.Audio import Audio 8 | 9 | class Plugin: 10 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 11 | self.tts = tts 12 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 13 | 14 | processor.addAction("random.dice", self.randomDice) 15 | processor.addAction("random.between", self.randomBetween) 16 | 17 | def randomDice(self, intent: Intent, result: ProcessorResult): 18 | intent.variables["result"] = self.__random(1, 6) 19 | self.tts(intent.answer()) 20 | 21 | def randomBetween(self, intent: Intent, result: ProcessorResult): 22 | intent.variables["result"] = self.__random(int(intent.variables['first']), int(intent.variables['last'])) 23 | self.tts(intent.answer()) 24 | 25 | def __random(self, first_number, last_number) -> int: 26 | smaller = first_number if first_number < last_number else last_number 27 | bigger = last_number if last_number > first_number else first_number 28 | return random.randint(smaller, bigger) 29 | -------------------------------------------------------------------------------- /src/plugins/timer/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "timer.minutes", 4 | "utterances": [ 5 | "Mais un minuteur de {minutes} minutes", 6 | "Mais un minuteur de {minutes} minute", 7 | "Mets un minuteur de {minutes} minutes", 8 | "Mets un minuteur de {minutes} minute", 9 | "Réveil moi dans {minutes} minutes", 10 | "Réveil moi dans {minutes} minute", 11 | "Réveille-moi dans {minutes} minutes", 12 | "Réveille-moi dans {minutes} minute", 13 | "Rêver moi dans {minutes} minutes", 14 | "Rêver moi dans {minutes} minute" 15 | ], 16 | "answers": [ 17 | "%minutes% minutes, à partir de maintenant.", 18 | "J'ai créé un minuteur de %minutes% minutes.", 19 | "Très bien, un minuteur de %minutes% minutes." 20 | ] 21 | }, 22 | { 23 | "intent": "timer.stop", 24 | "utterances": [ 25 | "Stop" 26 | ], 27 | "answers": [] 28 | } 29 | ] -------------------------------------------------------------------------------- /src/plugins/timer/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from events import Events 3 | from threading import Thread 4 | from src.SetTimeOut import SetTimeOut 5 | from src.NaturalLanguage.Intent import Intent 6 | from src.NaturalLanguage.Processor import Processor 7 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 8 | from src.Audio import Audio 9 | 10 | class Plugin: 11 | alarms: bool() = [] 12 | integers: str() = [ 13 | "zéro", "une", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", 14 | "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf", 15 | "vingt", "vingt et une", "vingt-deux", "vingt-trois", "vingt-quatre", "vingt-cinq", "vingt-six", "vingt-sept", "vingt-huit", "vingt-neuf", 16 | "trente", "trente et une", "trente-deux", "trente-trois", "trente-quatre", "trente-cinq", "trente-six", "trente-sept", "trente-huit", "trente-neuf", 17 | "quarante", "quarante et une", "quarante-deux", "quarante-trois", "quarante-quatre", "quarante-cinq", "quarante-six", "quarante-sept", "quarante-huit", "quarante-neuf", 18 | "cinquante", "cinquante et une", "cinquante-deux", "cinquante-trois", "cinquante-quatre", "cinquante-cinq", "cinquante-six", "cinquante-sept", "cinquante-huit", "cinquante-neuf", 19 | "soixante", "soixante et une", "soixante-deux", "soixante-trois", "soixante-quatre", "soixante-cinq", "soixante-six", "soixante-sept", "soixante-huit", "soixante-neuf", 20 | "soixante-dix", "soixante et onze", "soixante-douze", "soixante-treize", "soixante-quatorze", "soixante-quinze", "soixante-seize", "soixante-dix-sept", "soixante-dix-huit", "soixante-dix-neuf", 21 | "quatre-vingts", "quatre-vingt-une", "quatre-vingt-deux", "quatre-vingt-trois", "quatre-vingt-quatre", "quatre-vingt-cinq", "quatre-vingt-six", "quatre-vingt-sept", "quatre-vingt-huit", "quatre-vingt-neuf", 22 | "quatre-vingt-dix", "quatre-vingt-onze", "quatre-vingt-douze", "quatre-vingt-treize", "quatre-vingt-quatorze", "quatre-vingt-quinze", "quatre-vingt-seize", "quatre-vingt-dix-sept", "quatre-vingt-dix-huit", "quatre-vingt-dix-neuf", 23 | "cent" 24 | ] 25 | 26 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 27 | self.tts = tts 28 | self.mp3 = mp3 29 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 30 | 31 | processor.addAction("timer.minutes", self.__timerMinutes) 32 | processor.addAction("timer.stop", self.__timerStop) 33 | 34 | def __timerRing(self, args): 35 | alarmPath: str = os.path.join(os.path.dirname(__file__), "mp3", "alarm.mp3") 36 | Thread(target=self.__timerRingLoop, args=(args,alarmPath)).start() 37 | 38 | def __timerRingLoop(self, args, alarmPath: str): 39 | while(self.alarms[args[0]] == False): 40 | self.mp3.play(alarmPath) 41 | 42 | def __timerStop(self, intent: Intent, result: ProcessorResult): 43 | for index, alarm in enumerate(self.alarms): 44 | self.alarms[index] = True 45 | 46 | def __timerMinutes(self, intent: Intent, result: ProcessorResult): 47 | minutesString: str = intent.variables['minutes'] 48 | if minutesString in self.integers: 49 | minutesInt: int = self.integers.index(minutesString) 50 | 51 | index: int = -1 52 | for loopIndex, alarm in enumerate(self.alarms): 53 | if self.alarms[loopIndex] == True: 54 | index = loopIndex 55 | if index < 0: 56 | index = len(self.alarms) 57 | self.alarms.append(False) 58 | 59 | SetTimeOut(self.__timerRing, minutesInt * 60 * 1000, [index]) 60 | 61 | intent.variables["minutes"] = intent.variables['minutes'] 62 | self.tts(intent.answer()) 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/plugins/timer/mp3/alarm.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/plugins/timer/mp3/alarm.mp3 -------------------------------------------------------------------------------- /src/plugins/volume/corpus.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "intent": "volume.percent", 4 | "utterances": [ 5 | "Volume du son à {percent} pour cent", 6 | "Volume du son à {percent} pourcent" 7 | ], 8 | "answers": [ 9 | "Ok, %percent% pourcents." 10 | ] 11 | } 12 | ] -------------------------------------------------------------------------------- /src/plugins/volume/index.py: -------------------------------------------------------------------------------- 1 | import os 2 | from events import Events 3 | from src.NaturalLanguage.Intent import Intent 4 | from src.NaturalLanguage.Processor import Processor 5 | from src.NaturalLanguage.ProcessorResult import ProcessorResult 6 | from src.Audio import Audio 7 | 8 | class Plugin: 9 | integers: str() = [ 10 | "zéro", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf", 11 | "dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit", "dix-neuf", 12 | "vingt", "vingt et un", "vingt-deux", "vingt-trois", "vingt-quatre", "vingt-cinq", "vingt-six", "vingt-sept", "vingt-huit", "vingt-neuf", 13 | "trente", "trente et un", "trente-deux", "trente-trois", "trente-quatre", "trente-cinq", "trente-six", "trente-sept", "trente-huit", "trente-neuf", 14 | "quarante", "quarante et un", "quarante-deux", "quarante-trois", "quarante-quatre", "quarante-cinq", "quarante-six", "quarante-sept", "quarante-huit", "quarante-neuf", 15 | "cinquante", "cinquante et un", "cinquante-deux", "cinquante-trois", "cinquante-quatre", "cinquante-cinq", "cinquante-six", "cinquante-sept", "cinquante-huit", "cinquante-neuf", 16 | "soixante", "soixante et un", "soixante-deux", "soixante-trois", "soixante-quatre", "soixante-cinq", "soixante-six", "soixante-sept", "soixante-huit", "soixante-neuf", 17 | "soixante-dix", "soixante et onze", "soixante-douze", "soixante-treize", "soixante-quatorze", "soixante-quinze", "soixante-seize", "soixante-dix-sept", "soixante-dix-huit", "soixante-dix-neuf", 18 | "quatre-vingts", "quatre-vingt-un", "quatre-vingt-deux", "quatre-vingt-trois", "quatre-vingt-quatre", "quatre-vingt-cinq", "quatre-vingt-six", "quatre-vingt-sept", "quatre-vingt-huit", "quatre-vingt-neuf", 19 | "quatre-vingt-dix", "quatre-vingt-onze", "quatre-vingt-douze", "quatre-vingt-treize", "quatre-vingt-quatorze", "quatre-vingt-quinze", "quatre-vingt-seize", "quatre-vingt-dix-sept", "quatre-vingt-dix-huit", "quatre-vingt-dix-neuf", 20 | "cent" 21 | ] 22 | 23 | def __init__(self, processor: Processor, mp3: Audio, tts, events: Events, settings): 24 | self.tts = tts 25 | self.settings = settings 26 | processor.loadJson(os.path.join(os.path.dirname(__file__), "corpus.json")) 27 | 28 | processor.addAction("volume.percent", self.__volumePercent) 29 | 30 | def __volumePercent(self, intent: Intent, result: ProcessorResult): 31 | percentString: str = intent.variables['percent'] 32 | if percentString in self.integers: 33 | percentInt: int = self.integers.index(percentString) 34 | 35 | if self.settings["os"] == "mac": 36 | import osascript 37 | osascript.osascript("set volume output volume " + str(percentInt)) 38 | elif self.settings["os"] == "raspberry": 39 | import alsaaudio 40 | mixer = alsaaudio.Mixer() 41 | mixer.setvolume(percentInt) 42 | elif self.settings["os"] == "nt": # Windows 43 | from ctypes import cast, POINTER 44 | from comtypes import CLSCTX_ALL 45 | from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume 46 | 47 | devices = AudioUtilities.GetSpeakers() 48 | interface = devices.Activate(IAudioEndpointVolume._iid_, CLSCTX_ALL, None) 49 | volume = cast(interface, POINTER(IAudioEndpointVolume)) 50 | 51 | volumes: list[float] = [ 52 | -37, # 0 53 | -35, # 1 54 | -34, # 2 55 | -33, # 3 56 | -32, # 4 57 | -31, # 5 58 | -30, # 6 59 | -29, # 7 60 | -28, # 8 61 | -27, # 9 62 | -26, # 10 63 | -25.5, # 11 64 | -25, # 12 65 | -24, # 13 66 | -23.5, # 14 67 | -23, # 15 68 | -22, # 16 69 | -21.5, # 17 70 | -21, # 18 71 | -20.5, # 19 72 | -20, # 20 73 | -19.5, # 21 74 | -19, # 22 75 | -18.5, # 23 76 | -18, # 24 77 | -17.5, # 25 78 | -17, # 26 79 | -16.5, # 27 80 | -16.25, # 28 81 | -16, # 29 82 | -15.5, # 30 83 | -15.25, # 31 84 | -14.75, # 32 85 | -14.5, # 33 86 | -14, # 34 87 | -13.75, # 35 88 | -13.25, # 36 89 | -13, # 37 90 | -12.75, # 38 91 | -12.5, # 39 92 | -12, # 40 93 | -11.75, # 41 94 | -11.5, # 42 95 | -11.25, # 43 96 | -11, # 44 97 | -10.5, # 45 98 | -10.25, # 46 99 | -10, # 47 100 | -9.75, # 48 101 | -9.5, # 49 102 | -9.25, # 50 103 | -9, # 51 104 | -8.75, # 52 105 | -8.5, # 53 106 | -8.25, # 54 107 | -8, # 55 108 | -7.75, # 56 109 | -7.5, # 57 110 | -7.25, # 58 111 | -7.12, # 59 112 | -7, # 60 113 | -6.75, # 61 114 | -6.5, # 62 115 | -6.25, # 63 116 | -6.12, # 64 117 | -5.9, # 65 118 | -5.7, # 66 119 | -5.5, # 67 120 | -5.3, # 68 121 | -5.1, # 69 122 | -4.9, # 70 123 | -4.7, # 71 124 | -4.5, # 72 125 | -4.3, # 73 126 | -4.1, # 74 127 | -3.9, # 75 128 | -3.7, # 76 129 | -3.5, # 77 130 | -3.4, # 78 131 | -3.3, # 79 132 | -3.1, # 80 133 | -2.9, # 81 134 | -2.7, # 82 135 | -2.5, # 83 136 | -2.4, # 84 137 | -2.3, # 85 138 | -2.1, # 86 139 | -1.9, # 87 140 | -1.7, # 88 141 | -1.6, # 89 142 | -1.5, # 90 143 | -1.3, # 91 144 | -1.1, # 92 145 | -1, # 93 146 | -0.9, # 94 147 | -0.7, # 95 148 | -0.5, # 96 149 | -0.4, # 97 150 | -0.3, # 98 151 | -0.12, # 99 152 | 0, # 100 153 | ] 154 | volume.SetMasterVolumeLevel(volumes[percentInt], None) 155 | 156 | intent.variables["percent"] = intent.variables['percent'] 157 | self.tts(intent.answer()) 158 | -------------------------------------------------------------------------------- /src/porcupine/WakeWord_Ok-NOVA_fr_mac.ppn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/porcupine/WakeWord_Ok-NOVA_fr_mac.ppn -------------------------------------------------------------------------------- /src/porcupine/WakeWord_Ok-NOVA_fr_nt.ppn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/porcupine/WakeWord_Ok-NOVA_fr_nt.ppn -------------------------------------------------------------------------------- /src/porcupine/WakeWord_Ok-NOVA_fr_raspberry.ppn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/porcupine/WakeWord_Ok-NOVA_fr_raspberry.ppn -------------------------------------------------------------------------------- /src/porcupine/porcupine_params_fr.pv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HeyHeyChicken/NOVA-Python/64a4d2f04a7b6038e3fc4ff45a430301ab9c7b2c/src/porcupine/porcupine_params_fr.pv --------------------------------------------------------------------------------