├── .docker └── Dockerfile ├── .gitignore ├── README.md ├── database ├── __init__.py ├── db_functions.py └── time_processing.py ├── deploy_database.sh ├── deploy_diary.sh ├── requirements.txt ├── tg_bot ├── __init__.py ├── bot.py └── voice_module.py └── voice_diary ├── __init__.py └── __main__.py /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # TODO 2 | # запуск бота внутри контейнера 3 | # установить в контейнер ffmpeg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # custom 132 | .DS_Store 133 | vosk-model-ru-0.10/ 134 | .idea/ 135 | *.ipynb 136 | user_logs/ 137 | credentials* -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python_voice_diary 2 | voice diary based on telegram bot 3 | 4 | 5 | do this steps to run this telegram diary bot: 6 | 1. place vosk-model folder to /models 7 | 2. change model name in tg_bot init file (model name is folder name from step 1) 8 | 3. add credentials.json to /voice_diary with next keys: ["api_key", "special_chat_id"] 9 | 4. run command sh deploy_diary.sh 10 | 5. enjoy ! :) 11 | -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | import pymongo 2 | 3 | client = pymongo.MongoClient("localhost", 27017) 4 | DIARY_DB = client.database.diary 5 | 6 | 7 | __all__ = [ 8 | 'DIARY_DB', 9 | ] 10 | -------------------------------------------------------------------------------- /database/db_functions.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | import datetime 3 | from pymongo.errors import DuplicateKeyError 4 | from pymongo.collection import Collection 5 | 6 | 7 | def add_value(database: Collection, value: str): 8 | """ 9 | Добавить одну запись в базу данных 10 | """ 11 | 12 | database_index = int(datetime.datetime.now().timestamp()) 13 | try: 14 | database.insert_one({'_id': database_index, 'text': value}) 15 | return True 16 | 17 | except DuplicateKeyError: 18 | return False 19 | 20 | 21 | def del_value(database: Collection, index: int, index_end: int = None): 22 | """ 23 | Удалить одну или несколько записей по индексу 24 | """ 25 | 26 | if index_end is None: 27 | database.delete_one( 28 | { 29 | "_id": { 30 | "$gte": index, 31 | "$lt": index + 1 32 | } 33 | } 34 | ) 35 | 36 | else: 37 | database.delete_many( 38 | { 39 | "_id": { 40 | "$gte": index, 41 | "$lt": index_end + 1 42 | } 43 | } 44 | ) 45 | 46 | return True 47 | 48 | 49 | def find_value(database: Collection, index: int, index_end: int = None) -> List: 50 | """ 51 | Получить одну или несколько записей по индексу 52 | """ 53 | return [ 54 | value for value in database.find( 55 | { 56 | "_id": { 57 | "$gte": index, 58 | "$lt": index + 1 if index_end is None else index_end + 1 59 | } 60 | } 61 | ) 62 | ] 63 | 64 | 65 | def find_last_values(database: Collection, last_n_values: int = 5) -> List: 66 | """ 67 | Получить последние N записей 68 | """ 69 | return [value for value in database.find().limit(last_n_values).sort([('$natural', -1)])] 70 | -------------------------------------------------------------------------------- /database/time_processing.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from typing import Tuple, Union 3 | 4 | 5 | def timestamp_to_date(timestamp: int) -> str: 6 | """ 7 | Конвертирует timestamp в читаемый формат даты 8 | """ 9 | 10 | return datetime.datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S') 11 | 12 | 13 | def date_to_timestamp(date: str) -> int: 14 | """ 15 | Конвертирует дату в timestamp 16 | """ 17 | 18 | return int(datetime.datetime.strptime(date, '%Y-%m-%d %H:%M:%S').timestamp()) 19 | 20 | 21 | def get_time_interval(end_date: Union[str, int], minutes_delta: int) -> Tuple[int, int]: 22 | """ 23 | Возвращает интервал из двух дат: начальная (end_date - minutes_back) и конечная (end_date) 24 | в формате timestamp 25 | """ 26 | 27 | if isinstance(end_date, str): 28 | end_date = int(datetime.datetime.strptime(end_date, '%Y-%m-%d %H:%M:%S').timestamp()) 29 | start_date = int((end_date - minutes_delta * 60)) 30 | return start_date, end_date 31 | 32 | 33 | elif isinstance(end_date, int) or isinstance(end_date, float): 34 | start_date = int((end_date - minutes_delta * 60)) 35 | return start_date, int(end_date) 36 | -------------------------------------------------------------------------------- /deploy_database.sh: -------------------------------------------------------------------------------- 1 | if docker inspect --format '{{json .State.Running}}' mongo_database 2 | then 3 | echo "container is already running" 4 | exit 5 | fi 6 | docker pull mongo 7 | docker run -d --name mongo_database -p 27017:27017 -v mongodb_data:/data/db mongo 8 | echo 'database launched successfully' -------------------------------------------------------------------------------- /deploy_diary.sh: -------------------------------------------------------------------------------- 1 | pip3 install --no-cache-dir -r requirements.txt 2 | sh './deploy_database.sh' 3 | python3 -m voice_diary -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyTelegramBotAPI==3.7.9 2 | vosk==0.3.29 3 | pyttsx3==2.90 4 | pymongo==3.11.4 5 | mongoengine==0.23.1 -------------------------------------------------------------------------------- /tg_bot/__init__.py: -------------------------------------------------------------------------------- 1 | from vosk import Model 2 | VOICE_MODEL = Model("models/vosk-model-ru-0.10") 3 | 4 | __all__ = [ 5 | 'VOICE_MODEL' 6 | ] 7 | -------------------------------------------------------------------------------- /tg_bot/bot.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | from subprocess import check_call 4 | 5 | import pymongo 6 | import vosk 7 | import telebot 8 | from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton 9 | 10 | from tg_bot.voice_module import recognize_phrase 11 | from database.db_functions import add_value, find_value, find_last_values, del_value 12 | from database.time_processing import get_time_interval, timestamp_to_date, date_to_timestamp 13 | 14 | 15 | class TGBot: 16 | 17 | def __init__( 18 | self, 19 | token: str, 20 | database: pymongo.collection.Collection, 21 | voice_model: vosk.Model, 22 | special_chat_id: int = None 23 | ): 24 | 25 | self.token = token 26 | self.database = database 27 | self.special_chat_id = special_chat_id 28 | self.bot = telebot.TeleBot(token, threaded=False) 29 | self.voice_model = voice_model 30 | 31 | def add_handlers(self): 32 | 33 | @self.bot.message_handler(content_types=['voice']) 34 | def process_voice_message(message): 35 | 36 | path_user_logs = os.path.join('user_logs', str(message.chat.id)) 37 | if not os.path.exists(path_user_logs): 38 | os.makedirs(path_user_logs) 39 | 40 | file_info = self.bot.get_file(message.voice.file_id) 41 | downloaded_file = self.bot.download_file(file_info.file_path) 42 | 43 | with open(os.path.join(path_user_logs, 'my_phrase.ogg'), 'wb') as new_file: 44 | new_file.write(downloaded_file) 45 | 46 | # convert oog to wav 47 | command = f"ffmpeg -i {path_user_logs}/my_phrase.ogg -ar 16000 -ac 2 -ab 192K -f wav {path_user_logs}/my_phrase_to_translite.wav" 48 | _ = check_call(command.split()) 49 | 50 | user_phrase = recognize_phrase(self.voice_model, f'{path_user_logs}/my_phrase_to_translite.wav') 51 | add_value(self.database, user_phrase) 52 | self.bot.reply_to(message, 53 | f"Твое сообщение сохранено! Дата: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") 54 | 55 | os.remove(f'{path_user_logs}/my_phrase.ogg') 56 | os.remove(f'{path_user_logs}/my_phrase_to_translite.wav') 57 | 58 | @self.bot.message_handler(commands=['menu']) 59 | def main_menu(message): 60 | 61 | keyboard = InlineKeyboardMarkup() 62 | button_1 = InlineKeyboardButton(text="получить записи", callback_data='find') 63 | button_2 = InlineKeyboardButton(text="удалить записи", callback_data='delete') 64 | button_3 = InlineKeyboardButton(text="ничего", callback_data='pass') 65 | keyboard.add(button_1, button_2, button_3) 66 | 67 | self.bot.send_message(message.chat.id, "Выбери что хочешь сделать", reply_markup=keyboard) 68 | 69 | @self.bot.message_handler(func=lambda message: True, content_types=['text']) 70 | def process_text_message(message): 71 | template_text_message = """ 72 | Привет! Если ты хочешь сохранить сообщение, просто отправь голосовое прямо сюда. Для всех остальных случаев воспользуйся командой /menu. 73 | """ 74 | self.bot.send_message(message.chat.id, template_text_message) 75 | 76 | @self.bot.callback_query_handler(func=lambda call: True) 77 | def callback_menu_inline(call): 78 | if call.data == "back_to_main_menu": 79 | keyboard = InlineKeyboardMarkup() 80 | button_1 = InlineKeyboardButton(text="получить записи", callback_data='find') 81 | button_2 = InlineKeyboardButton(text="удалить записи", callback_data='delete') 82 | button_3 = InlineKeyboardButton(text="ничего", callback_data='pass') 83 | keyboard.add(button_1, button_2, button_3) 84 | self.bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, reply_markup=keyboard) 85 | 86 | elif call.data == "find": 87 | find_menu = InlineKeyboardMarkup() 88 | button_1 = InlineKeyboardButton(text='[find] последние N', callback_data='find_last_n') 89 | button_2 = InlineKeyboardButton(text='[find] на интервале', callback_data='find_interval') 90 | button_3 = InlineKeyboardButton(text='назад', callback_data='back_to_main_menu') 91 | find_menu.add(button_1, button_2, button_3) 92 | self.bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, 93 | reply_markup=find_menu) 94 | 95 | elif call.data == "delete": 96 | delete_menu = InlineKeyboardMarkup() 97 | button_1 = InlineKeyboardButton(text='[del] последние N', callback_data='delete_last_n') 98 | button_2 = InlineKeyboardButton(text='[del] на интервале', callback_data='delete_interval') 99 | button_3 = InlineKeyboardButton(text='назад', callback_data='back_to_main_menu') 100 | delete_menu.add(button_1, button_2, button_3) 101 | self.bot.edit_message_reply_markup(call.message.chat.id, call.message.message_id, 102 | reply_markup=delete_menu) 103 | 104 | elif call.data == "pass": 105 | self.bot.answer_callback_query(callback_query_id=call.id, show_alert=False, text="Ну и отлично!") 106 | 107 | elif call.data == "delete_last_n": 108 | msg = self.bot.send_message(call.message.chat.id, 'Введи число записей, которые хочешь удалить') 109 | self.bot.register_next_step_handler(msg, delete_last_n) 110 | 111 | elif call.data == "find_last_n": 112 | msg = self.bot.send_message(call.message.chat.id, 'Введи число записей, которые хочешь вывести') 113 | self.bot.register_next_step_handler(msg, find_last_n) 114 | 115 | elif call.data == "delete_interval": 116 | msg = self.bot.send_message(call.message.chat.id, 117 | 'Удалить последние\n\n1) 10 минут\n2) 1 час\n3) 1 день\n\nвведи число') 118 | self.bot.register_next_step_handler(msg, delete_interval) 119 | 120 | elif call.data == "find_interval": 121 | msg = self.bot.send_message(call.message.chat.id, 122 | 'Найти последние\n\n1) 10 минут\n2) 1 час\n3) 1 день\n\nвведи число') 123 | self.bot.register_next_step_handler(msg, find_interval) 124 | 125 | # main handlers 126 | def delete_last_n(message): 127 | user_answer = message.text 128 | 129 | if not user_answer.isdigit(): 130 | self.bot.send_message(message.chat.id, 'необходимо ввести число') 131 | return 132 | 133 | last_values = find_last_values(self.database, int(user_answer)) 134 | last_idx, first_idx = last_values[0]['_id'], last_values[-1]['_id'] 135 | del_value(self.database, first_idx, last_idx) 136 | self.bot.send_message(message.chat.id, 'удалено!') 137 | 138 | def delete_interval(message): 139 | user_answer = message.text 140 | 141 | if user_answer == '1': # 10 минут 142 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 10) 143 | 144 | elif user_answer == '2': # 60 минут 145 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 60) 146 | 147 | elif user_answer == '3': # 1440 минут 148 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 1440) 149 | 150 | else: 151 | self.bot.send_message(message.chat.id, 'введено некорректное значение') 152 | return 153 | 154 | del_value(self.database, use_interval[0], use_interval[1]) 155 | self.bot.send_message(message.chat.id, 'удалено!') 156 | 157 | def find_last_n(message): 158 | user_answer = message.text 159 | 160 | if not user_answer.isdigit(): 161 | self.bot.send_message(message.chat.id, 'необходимо ввести число') 162 | return 163 | 164 | for result in find_last_values(self.database, int(user_answer)): 165 | self.bot.send_message(message.chat.id, timestamp_to_date(result['_id']) + '\n' + result['text']) 166 | 167 | def find_interval(message): 168 | user_answer = message.text 169 | 170 | if user_answer == '1': # 10 минут 171 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 10) 172 | 173 | elif user_answer == '2': # 60 минут 174 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 60) 175 | 176 | elif user_answer == '3': # 1440 минут 177 | use_interval = get_time_interval(int(datetime.datetime.now().timestamp()), 1440) 178 | 179 | else: 180 | self.bot.send_message(message.chat.id, 'введено некорректное значение') 181 | return 182 | 183 | self.bot.send_message( 184 | message.chat.id, 185 | 'будут выведены сообщения\nс ' + timestamp_to_date(use_interval[0]) + ' по ' + timestamp_to_date( 186 | use_interval[1]) 187 | ) 188 | 189 | for result in find_value(self.database, use_interval[0], use_interval[1]): 190 | self.bot.send_message(message.chat.id, timestamp_to_date(result['_id']) + '\n' + result['text']) 191 | 192 | def run_bot(self, **kwargs): 193 | self.bot.polling(**kwargs) 194 | -------------------------------------------------------------------------------- /tg_bot/voice_module.py: -------------------------------------------------------------------------------- 1 | import wave 2 | import json 3 | import vosk 4 | from vosk import KaldiRecognizer 5 | 6 | 7 | def recognize_phrase(model: vosk.Model, phrase_wav_path: str) -> str: 8 | """ 9 | Recognize Russian voice in wav 10 | """ 11 | 12 | wave_audio_file = wave.open(phrase_wav_path, "rb") 13 | offline_recognizer = KaldiRecognizer(model, 24000) 14 | data = wave_audio_file.readframes(wave_audio_file.getnframes()) 15 | 16 | offline_recognizer.AcceptWaveform(data) 17 | recognized_data = json.loads(offline_recognizer.Result())["text"] 18 | return recognized_data 19 | -------------------------------------------------------------------------------- /voice_diary/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MaximShalankin/python_voice_diary/0f177884765bf5a409ace03ca9570eab36c19cb2/voice_diary/__init__.py -------------------------------------------------------------------------------- /voice_diary/__main__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from tg_bot.bot import TGBot 4 | from database import DIARY_DB 5 | from tg_bot import VOICE_MODEL 6 | 7 | with open('voice_diary/credentials.json', 'r') as file: 8 | credentials = json.load(file) 9 | 10 | 11 | def main(): 12 | my_diary_bot = TGBot(credentials['api_key'], DIARY_DB, VOICE_MODEL, credentials['special_chat_id']) 13 | my_diary_bot.add_handlers() 14 | my_diary_bot.run_bot(**{'none_stop': True}) 15 | 16 | 17 | if __name__ == '__main__': 18 | main() 19 | --------------------------------------------------------------------------------