From 9ec20afbaf87166b26ddc2956c03a5a4d3b562ea Mon Sep 17 00:00:00 2001 From: majmongoose Date: Thu, 20 Aug 2026 14:39:06 -0400 Subject: [PATCH] add better crash detection --- README.md | 2 +- pagebot.py | 141 +++++++++++++++++++++++------------------ sample-secrets_file.py | 4 +- 3 files changed, 85 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index f856666..911a59a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ PageBot is a discord bot written in python using the discord.py library, which w 1. Install the latest version of python3 2. Download and Decompress PageBot 3. Download [ffmpeg.exe](https://www.ffmpeg.org/download.html) and drop it into the PageBot directory -4. Run the following from the PageBot directory ```pip install requirements.txt``` +4. Run the following from the PageBot directory ```pip install -r requirements.txt``` 5. Rename ```sample-secrets_file.py``` to ```secrets_file.py``` and fill in required information 6. Run the bot with ```python pagebot.py``` diff --git a/pagebot.py b/pagebot.py index 5fcc557..346126f 100644 --- a/pagebot.py +++ b/pagebot.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +import asyncio import discord import os import subprocess @@ -7,17 +8,40 @@ import secrets_file import threading from watchdog.observers import Observer from watchdog.events import FileSystemEventHandler -import speech_recognition as sr +import speech_recognition as sr + +def wait_for_file_ready(filepath, stable_seconds=2, timeout=60): + elapsed = 0 + last_size = -1 + stable_time = 0 + while elapsed < timeout: + try: + size = os.path.getsize(filepath) + except OSError: + return False + if size == last_size: + stable_time += 1 + if stable_time >= stable_seconds: + return True + else: + stable_time = 0 + last_size = size + time.sleep(1) + elapsed += 1 + return False + +last_file_time = time.time() -## New File Handler class MyHandler(FileSystemEventHandler): def on_created(self, event): + global last_file_time if event.is_directory: return + last_file_time = time.time() filepath = event.src_path filename, file_extension = os.path.splitext(filepath) if file_extension.lower() == '.amr': - time.sleep(10) + wait_for_file_ready(filepath) os.remove(filepath) print("Removing AMR.") if file_extension.lower() == '.mp3': @@ -25,64 +49,58 @@ class MyHandler(FileSystemEventHandler): text = "" if secrets_file.speech_to_text: print("Converting To Text") - text = convert_to_text(filepath,filename) + text = convert_to_text(filepath, filename) print("Converting to MP4") mp4_file = convert_to_mp4(filepath) print("Sending to Discord") - client.loop.create_task(upload_to_discord(mp4_file,text)) + asyncio.run_coroutine_threadsafe(upload_to_discord(mp4_file, text), client.loop) -## Convert MP3 to MP4 def convert_to_mp4(mp3_file): try: - time.sleep(10) + wait_for_file_ready(mp3_file) mp4_file = os.path.splitext(mp3_file)[0] + '.mp4' - command = f'ffmpeg -loop 1 -i "{secrets_file.image_path}" -i "{mp3_file}" -c:a aac -b:a 192k -c:v libx264 -pix_fmt yuv420p -shortest "{mp4_file}"' - subprocess.run(command, shell=True) + subprocess.run([ + 'ffmpeg', '-loop', '1', + '-i', secrets_file.image_path, + '-i', mp3_file, + '-c:a', 'aac', '-b:a', '192k', + '-c:v', 'libx264', '-pix_fmt', 'yuv420p', + '-shortest', mp4_file + ]) os.remove(mp3_file) return mp4_file except Exception as e: print(f"Error during conversion: {e}") return None -## Send audio to interpreter -def convert_to_text(mp3_path,mp3_name): +def convert_to_text(mp3_path, mp3_name): print(mp3_path) try: - command = f'ffmpeg -i "{mp3_path}" "{mp3_name}".wav' - subprocess.run(command, shell=True) - r = sr.Recognizer() - # Load the audio file - with sr.AudioFile(f"{mp3_name}.wav") as source: - data = r.record(source) - # Convert speech to text - text = r.recognize_google(data) - os.remove(f"{mp3_name}.wav") - return (text) + wav_file = f"{mp3_name}.wav" + subprocess.run(['ffmpeg', '-i', mp3_path, wav_file]) + r = sr.Recognizer() + with sr.AudioFile(wav_file) as source: + data = r.record(source) + text = r.recognize_google(data) + os.remove(wav_file) + return text except Exception as e: print(f"Error during conversion: {e}") return "" - - -## upload to discord -async def upload_to_discord(mp4_file,text): - ## Check to make sure conversion worked. +async def upload_to_discord(mp4_file, text): if mp4_file is None: print("Conversion failed. Skipping upload.") return - ## Get channel to send in channel = client.get_channel(secrets_file.channel_id) if channel: filename = os.path.basename(mp4_file) - ## Send Video with name with open(mp4_file, 'rb') as f: - await channel.send(filename,file=discord.File(f)) + await channel.send(filename, file=discord.File(f)) if secrets_file.delete_after_upload: os.remove(mp4_file) - ## Send transcribed voice if present. - if (text != ""): + if text != "": await channel.send(f"The following text was transcoded from the recording: \n{text}") - ## Ping users with the appropriate number role_name = filename.split('-', 1)[0].strip() role = discord.utils.get(channel.guild.roles, name=role_name) if role: @@ -90,52 +108,55 @@ async def upload_to_discord(mp4_file,text): else: print(f"Role '{role_name}' not found.") else: - print(f"Could not find channel with ID {channel}") + print(f"Could not find channel with ID {secrets_file.channel_id}") + +TTD_TIMEOUT = getattr(secrets_file, 'ttd_timeout', 900) def launch_and_watch(program_path): + global last_file_time program_directory = os.path.dirname(program_path) while True: - process = subprocess.Popen(program_path,cwd=program_directory) - process.wait() + last_file_time = time.time() + process = subprocess.Popen(program_path, cwd=program_directory) + while True: + try: + process.wait(timeout=30) + break + except subprocess.TimeoutExpired: + if time.time() - last_file_time > TTD_TIMEOUT: + print("TTD appears to have locked up (no new files in 15 minutes). Restarting...") + process.kill() + process.wait() + break if process.returncode != 0: print("TTD has crashed. Relaunching...") else: print("TTD has exited normally.") break - - # Wait for a few seconds before relaunching time.sleep(2) if __name__ == "__main__": - - ## initialize watchdogs - event_handler = MyHandler() observer = Observer() - observer.schedule(event_handler, secrets_file.watch_folder, recursive=False) - observer.start() - ## Launch TTD - if (secrets_file.ttd_path != ""): - watchdog_thread = threading.Thread(target=launch_and_watch, args=(secrets_file.ttd_path,)) - watchdog_thread.start() - - ## initialize discord intents = discord.Intents.default() intents.message_content = True client = discord.Client(intents=intents) - client.run(secrets_file.key) - + + @client.event + async def on_ready(): + print(f'We have logged in as {client.user}') + + event_handler = MyHandler() + observer.schedule(event_handler, secrets_file.watch_folder, recursive=False) + observer.start() + + if secrets_file.ttd_path != "": + watchdog_thread = threading.Thread(target=launch_and_watch, args=(secrets_file.ttd_path,), daemon=True) + watchdog_thread.start() try: - while True: - time.sleep(1) - except KeyboardInterrupt: + client.run(secrets_file.key) + finally: observer.stop() - observer.join() - -##discord stuffs -@client.event -async def on_ready(): - print(f'We have logged in as {client.user}') - + observer.join() diff --git a/sample-secrets_file.py b/sample-secrets_file.py index 541ff8d..ab0d7f7 100644 --- a/sample-secrets_file.py +++ b/sample-secrets_file.py @@ -13,4 +13,6 @@ delete_after_upload=True ## This will slow down pages and is unreliable. speech_to_text = True ## The background image for the video. -image_path = "img/blacksmall.jpg" \ No newline at end of file +image_path = "img/blacksmall.jpg" +## Seconds with no new files before TTD is considered locked up (default 900 = 15 min). +ttd_timeout = 900 \ No newline at end of file