diff --git a/README.md b/README.md index 05a9ccf..78583c3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,23 @@ # sentimeter -Reading the Emotions From Text / Youtube video or live speech +Sentimeter is an app written in python to understand the emotions in audio / text. Currently it has the below modules +* Audio Module + Process the Emotions in a given audio file. Currently it supports only wav file . In future we will add more formats + +* Live Speech +Process the Emotions in a live speech + +* Telegram Mode +Sentimeter has a Telegram Bot, it can process the emotions in a Telegram chat + +* Text File +Process a text file and identifies the emotions + +* Youtube Video +Under Development + + ## Usage diff --git a/__init__.py b/__init__.py index e69de29..e6d79e0 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Application Main +""" diff --git a/core/__init__.py b/core/__init__.py index 1d1aa52..8af0497 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Application Main +""" from core.emotions_engine import EmotionsEngine from core.sttengine import STTEngine -from core.user_interfaces import SentimeterSimpleUI \ No newline at end of file +from core.user_interfaces import SentimeterSimpleUI diff --git a/core/emotions_engine.py b/core/emotions_engine.py index e079318..2521ae6 100644 --- a/core/emotions_engine.py +++ b/core/emotions_engine.py @@ -1,5 +1,8 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ -Class Description +The Core Engine """ import logging @@ -9,14 +12,14 @@ class SentiMeterModule: def __init__(self) -> None: - self.active=False + self.active = False def start(self): - self.active=True + self.active = True return self.active def stop(self): - self.active=False + self.active = False return self.active def is_active(self): @@ -25,45 +28,45 @@ def is_active(self): class EngineObserver: def __init__(self, name) -> None: - ''' + """ A Name is given for observer - ''' + """ self.name = name def on_event(self, result, emotions): - ''' + """ Observer Abstract Method - ''' + """ pass -class EmotionsEngine(): - ''' +class EmotionsEngine: + """ The class responsible to identify the emotions of a given text - ''' + """ def __init__(self) -> None: - ''' + """ constructor , the object acts as singleton - ''' + """ self._listeners = {} def add_observer(self, observer): - ''' + """ The Engine events are propogated via an observer mechanism, Objects can register via this method - ''' + """ self._listeners[observer.name] = observer def remove_observer(self, observer): - ''' + """ Remove an observer ,Todo : Check if observer is not registered to avoid exceptions - ''' + """ self._listeners.pop(observer.name) def process_text(self, result): - ''' + """ The Functions to start the processing - ''' + """ emotions = te.get_emotion(result) # Lets inform all the listerners for obj in self._listeners: diff --git a/core/sttengine.py b/core/sttengine.py index f72e9fc..6e9f949 100644 --- a/core/sttengine.py +++ b/core/sttengine.py @@ -1,31 +1,36 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Speech to Text Module, Currently uses Google STT, Future we will add more offline Engines +""" import logging import speech_recognition as sr + class STTEngine: - ''' + """ Speech to Text Engine, Currently using Google Service, Options to be added for Sphinix - ''' + """ def __init__(self, recognizer) -> None: - ''' + """ constructor - ''' + """ self.recognizer = recognizer self.lang = "en-IN" pass def speech_to_text(self, audiodata): - ''' + """ function to get the text for inputed audio data - ''' + """ try: actual_result = self.recognizer.recognize_google( audiodata, language=self.lang, show_all=True ) except sr.UnknownValueError: - logging.fatal( - "Google Speech Recognition could not understand audio") + logging.fatal("Google Speech Recognition could not understand audio") exit(-1) except sr.RequestError as e: logging.fatal("Error service; {0}".format(e)) diff --git a/core/user_interfaces.py b/core/user_interfaces.py index 9e50c1e..b20fef2 100644 --- a/core/user_interfaces.py +++ b/core/user_interfaces.py @@ -1,6 +1,13 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +File which can handle various user interfaces , Currently only basic commandlines +""" import core.emotions_engine as emotions_engine from core.emotions_engine import EngineObserver + class SentimeterSimpleUI(EngineObserver): def __init__(self, name) -> None: super().__init__(name) diff --git a/modules/__init__.py b/modules/__init__.py index b2c87c3..4b79804 100644 --- a/modules/__init__.py +++ b/modules/__init__.py @@ -1,3 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Application Main +""" + from modules.livespeech_module import LiveSpeechModule from modules.telegram_module import TelegramModule -from modules.audio_module import AudioModule \ No newline at end of file +from modules.audio_module import AudioModule diff --git a/modules/audio_module.py b/modules/audio_module.py index 4d92c3a..5aedf8d 100644 --- a/modules/audio_module.py +++ b/modules/audio_module.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Module for processing offline Audiofile +""" import core.emotions_engine as emotions_engine from core.emotions_engine import SentiMeterModule from pydub import AudioSegment @@ -12,21 +18,22 @@ import speech_recognition as sr from core.user_interfaces import SentimeterSimpleUI -class AudioModule (SentiMeterModule): - ''' + +class AudioModule(SentiMeterModule): + """ Classs Abstracts Audio Related features - ''' + """ - def __init__(self,path) -> None: - ''' + def __init__(self, path) -> None: + """ Constructor path : location to a .wav file , in future we can add support for mp3 etc - ''' + """ super().__init__() self.recognizer = sr.Recognizer() self.backend = STTEngine(self.recognizer) - self.path=path - self.ui=SentimeterSimpleUI("Audio UI") + self.path = path + self.ui = SentimeterSimpleUI("Audio UI") def start(self): super().start() @@ -35,14 +42,15 @@ def start(self): # open the audio file using pydub sound = AudioSegment.from_wav(self.path) # split audio sound where silence is 1000 miliseconds or more and get chunks - chunks = split_on_silence(sound, - # experiment with this value for your target audio file - min_silence_len=1000, - # adjust this per requirement - silence_thresh=sound.dBFS-14, - # keep the silence for 1 second, adjustable as well - keep_silence=1000, - ) + chunks = split_on_silence( + sound, + # experiment with this value for your target audio file + min_silence_len=1000, + # adjust this per requirement + silence_thresh=sound.dBFS - 14, + # keep the silence for 1 second, adjustable as well + keep_silence=1000, + ) folder_name = "audio-chunks" # create a directory to store the audio chunks if not os.path.isdir(folder_name): @@ -61,9 +69,9 @@ def start(self): return True def stop(self): - ''' + """ Routines to stop the Engine - ''' + """ super().stop() logging.debug("Stopping IntelliAudio") if not self.sttrunner.is_alive: diff --git a/modules/livespeech_module.py b/modules/livespeech_module.py index f061584..09cbbe6 100644 --- a/modules/livespeech_module.py +++ b/modules/livespeech_module.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + """ IntelliAudio Class who proces live speech Audio Data file , Todo : May be split the class? """ @@ -19,29 +22,29 @@ class STTLiveTranscoder(Thread): - ''' + """ Threaded implementation to handle live audio with a processing FIFO - ''' + """ def __init__(self, backend): - ''' + """ Constructor - ''' + """ Thread.__init__(self) self.backend = backend self.queue = Queue() - self.ui=SentimeterSimpleUI("Live UI") + self.ui = SentimeterSimpleUI("Live UI") def put_data(self, audio_data): - ''' + """ Adds the audio data to processing queue - ''' + """ self.queue.put(audio_data) def run(self): - ''' + """ Runner - ''' + """ while True: audiodata = self.queue.get() result = self.backend.speech_to_text(audiodata) @@ -49,26 +52,25 @@ def run(self): return True -class LiveSpeechModule (SentiMeterModule): - ''' +class LiveSpeechModule(SentiMeterModule): + """ Classs Abstracts Audio Related features - ''' + """ def __init__(self) -> None: - ''' + """ Constructor - ''' + """ super().__init__() self.recognizer = sr.Recognizer() self.entries = [] self.backend = STTEngine(self.recognizer) self.sttrunner = STTLiveTranscoder(self.backend) - def start(self): - ''' + """ Process Microphone - ''' + """ super().start() self.sttrunner.start() while True: @@ -77,11 +79,10 @@ def start(self): audio_data = self.recognizer.record(source, duration=5) self.sttrunner.put_data(audio_data) - def stop(self): - ''' + """ Routines to stop the Engine - ''' + """ super().stop() logging.debug("Stopping IntelliAudio") if not self.sttrunner.is_alive: diff --git a/modules/telegram_module.py b/modules/telegram_module.py index 8489bc9..87af1ea 100644 --- a/modules/telegram_module.py +++ b/modules/telegram_module.py @@ -1,3 +1,9 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Module to run as telegram bot +""" import logging import speech_recognition as sr from threading import Thread @@ -14,7 +20,7 @@ import telebot import os -BOT_TOKEN = os.environ.get('SENTI_TOKEN') +BOT_TOKEN = os.environ.get("SENTI_TOKEN") bot = telebot.TeleBot(BOT_TOKEN) EMOJI_HAPPY = "\U0001F600 " @@ -28,7 +34,7 @@ class TelebotResponder(EngineObserver): def __init__(self, bot, message) -> None: self.message = message super().__init__("Telegrambot") - emotions_engine.engine.add_observer(self) + emotions_engine.engine.add_observer(self) def on_event(self, message_text, emotion_map): global bot @@ -37,21 +43,33 @@ def on_event(self, message_text, emotion_map): if total_emotions == 0: text = "I do not find any emotions :)" else: - text += EMOJI_ANGRY + \ - str(((emotion_map["Angry"]/total_emotions)*100)) + " % | " - text += EMOJI_FEAR + \ - str(((emotion_map["Fear"]/total_emotions)*100)) + " % | " - text += EMOJI_HAPPY + \ - str(((emotion_map["Happy"]/total_emotions)*100)) + " % | " - text += EMOJI_SAD + \ - str(((emotion_map["Sad"]/total_emotions)*100)) + " % | " - text += EMOJI_SURPRISE + \ - str(((emotion_map["Surprise"]/total_emotions)*100)) + " % |" + text += ( + EMOJI_ANGRY + + str(((emotion_map["Angry"] / total_emotions) * 100)) + + " % | " + ) + text += ( + EMOJI_FEAR + + str(((emotion_map["Fear"] / total_emotions) * 100)) + + " % | " + ) + text += ( + EMOJI_HAPPY + + str(((emotion_map["Happy"] / total_emotions) * 100)) + + " % | " + ) + text += ( + EMOJI_SAD + str(((emotion_map["Sad"] / total_emotions) * 100)) + " % | " + ) + text += ( + EMOJI_SURPRISE + + str(((emotion_map["Surprise"] / total_emotions) * 100)) + + " % |" + ) bot.reply_to(self.message, text) class TelegramModule(SentiMeterModule): - def __init__(self) -> None: super().__init__() pass diff --git a/modules/youtube.py b/modules/youtube.py index 175354c..9522d78 100644 --- a/modules/youtube.py +++ b/modules/youtube.py @@ -1,4 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +To do : Youtube Module +""" from pytube import YouTube -YouTube('https://youtu.be/9bZkp7q19f0').streams.first().download() -#Yet to be developed \ No newline at end of file +YouTube("https://youtu.be/9bZkp7q19f0").streams.first().download() + +# Yet to be developed diff --git a/sentimeter.py b/sentimeter.py index e1e2e35..f40d9ef 100644 --- a/sentimeter.py +++ b/sentimeter.py @@ -1,59 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Application Main +""" + import logging -from modules import LiveSpeechModule , TelegramModule , AudioModule +from modules import LiveSpeechModule, TelegramModule, AudioModule import signal import sys import click +# Module Global Instance +module = None +logging.basicConfig( + filename="app.log", + filemode="w", + level=logging.DEBUG, + format="%(name)s - %(levelname)s - %(message)s", +) -#Module Global Instance -module=None -logging.basicConfig(filename='app.log', filemode='w', level=logging.DEBUG, format='%(name)s - %(levelname)s - %(message)s') @click.command() @click.option( "--live", is_flag=True, default=False, help="Application in Live Listening Mode" ) @click.option( - "--telegrambot", is_flag=True, default=False, help="Application in Acting as a Telegram Bot" + "--telegrambot", + is_flag=True, + default=False, + help="Application in Acting as a Telegram Bot", ) -@click.option("--text", help="Text to Process") @click.option("--audio_file", help="Process Audio File") @click.option("--text_file", help="File to Process") - -def main(live,telegrambot, text, audio_file, text_file): - '''The Main function which process the user commands to start the respective modules''' +def main(live, telegrambot, text, audio_file, text_file): + """The Main function which process the user commands to start the respective modules""" global app global module if telegrambot == True: - logging.info('Started Telegram Mode') - module=TelegramModule() + logging.info("Started Telegram Mode") + module = TelegramModule() if live == True: - logging.info('Started Listening Mode') - module=LiveSpeechModule() - elif text != None: - pass + logging.info("Started Listening Mode") + module = LiveSpeechModule() elif audio_file != None: logging.info("Process audio file") - module=AudioModule(audio_file) + module = AudioModule(audio_file) elif text_file != None: logging.info("Process text file") else: logging.fatal("Unable to Recognize") - if module !=None: + if module != None: module.start() return + def signal_handler(sig, frame): - ''' Need to capture the Control-C especially when any module is in live processing mode - ''' + """Need to capture the Control-C especially when any module is in live processing mode""" global module logging.info("Please wait, Let me cleanup") if module.is_active: module.stop() sys.exit(0) -if __name__ == '__main__': + +if __name__ == "__main__": signal.signal(signal.SIGINT, signal_handler) - #Below line is required , Related to PY bug https://bugs.python.org/issue35935 + # Below line is required , Related to PY bug https://bugs.python.org/issue35935 signal.signal(signal.SIGINT, signal.SIG_DFL) main() diff --git a/test.py b/test.py deleted file mode 100644 index 4358124..0000000 --- a/test.py +++ /dev/null @@ -1,32 +0,0 @@ - -from multiprocessing import Process, BoundedSemaphore -from time import sleep - -def do_work(A, B): - sleep(.4) - print(A, B) - -def worker(sema, *args): - try: - do_work(*args) - finally: - sema.release() #allow a new process to be started now that this one is exiting - -def main(): - tasks = zip(range(65,91), bytes(range(65,91)).decode()) - sema = BoundedSemaphore(4) #only every 4 workers at a time - procs = [] - for arglist in tasks: - sema.acquire() #wait to start until another process is finished - procs.append(Process(target=worker, args=(sema, *arglist))) - procs[-1].start() - - #cleanup completed processes - while not procs[0].is_alive(): - procs.pop(0) - for p in procs: - p.join() #wait for any remaining tasks - print("done") - -if __name__ == "__main__": - main() \ No newline at end of file