improve script berikut ini agar lebih interaktif dalam mengirim update ke telegram agar user tau update dari progress yang sedang berjalan, tambahkan juga saat video selesai di buat kirimkan pesan ke telegram jika process selesai dan video sedang di kirim, lalu untuk upload video pada bagian title dan desc kirim pesan ke telegram agar user mengiisi nya sendiri, dan menunggu balasan nya, buatkan dengan benar lengkap dan tepat : import random
import os
import sys
import re
import time
import requests
import json
import math
import difflib
import uuid
import logging
import colorsys
import azure.cognitiveservices.speech as speechsdk
import numpy as np
from pydub import AudioSegment
from pydub.silence import detect_silence
from PIL import Image
from gradio_client import Client
from moviepy.editor import VideoFileClip, concatenate_audioclips, AudioFileClip, TextClip, CompositeVideoClip, CompositeAudioClip, ImageClip
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from google.auth.transport.requests import Request
from telegram import (
    Bot, Update, ReplyKeyboardMarkup, KeyboardButton,
    InlineKeyboardMarkup, InlineKeyboardButton,
)
from telegram.ext import (
    ApplicationBuilder,
    ContextTypes,
    CommandHandler,
    MessageHandler,
    CallbackQueryHandler,
    filters,
    ConversationHandler,
)

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')

SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))

TELEGRAM_BOT_TOKEN = '<TRUFFLEHOG_REDACTED_TELEGRAMBOTTOKEN>'  # Replace with your Telegram bot token
TELEGRAM_CHAT_ID = '882283026'      # Replace with your Telegram chat ID
YOUTUBE_CLIENT_SECRETS_FILE = os.path.join(SCRIPT_DIR, 'client_secrets.json')  # Replace with your YouTube client secrets JSON file path

# Initialize Telegram bot
bot = Bot(token=TELEGRAM_BOT_TOKEN)

# Define states for ConversationHandler
(
    ASK_VIDEO_ABOUT,
    CONFIRM_DIALOGUE,
    REGENERATE_OR_UPLOAD,
    EDIT_DIALOGUE,
    HANDLE_BRACKET_ERROR,
) = range(5)

async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    await update.message.reply_text("What is the video about?")
    return ASK_VIDEO_ABOUT

async def ask_video_about(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user_input = update.message.text
    context.user_data['video_about'] = user_input
    await update.message.reply_text("Generating dialogue, please wait...")
    # Start the dialogue generation process
    await generate_dialogue(context)
    return CONFIRM_DIALOGUE

async def generate_dialogue(context: ContextTypes.DEFAULT_TYPE):
    script_dir = SCRIPT_DIR
    text_file = os.path.join(script_dir, 'generated_text.txt')
    user_input = context.user_data['video_about']
    try:
        generated_text = await generate(text_file, script_dir, user_input)
        context.user_data['generated_text'] = generated_text

        # Send the generated text to the user
        await bot.send_message(
            chat_id=TELEGRAM_CHAT_ID,
            text=f"Generated Dialogue:\n\n{generated_text}"
        )

        # Send options as buttons
        keyboard = [
            [KeyboardButton('Generate Again'), KeyboardButton('Continue'), KeyboardButton('Edit')]
        ]
        reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
        await bot.send_message(
            chat_id=TELEGRAM_CHAT_ID,
            text="Please choose an option:",
            reply_markup=reply_markup
        )
    except Exception as e:
        logging.error(f"An error occurred during dialogue generation: {e}")
        await bot.send_message(
            chat_id=TELEGRAM_CHAT_ID,
            text="An error occurred during dialogue generation. Please try again."
        )
        return ConversationHandler.END

async def confirm_dialogue(update: Update, context: ContextTypes.DEFAULT_TYPE):
    decision = update.message.text.lower()
    if decision == 'generate again':
        await update.message.reply_text("Regenerating dialogue, please wait...")
        # Regenerate the dialogue
        await generate_dialogue(context)
        return CONFIRM_DIALOGUE
    elif decision == 'continue':
        await update.message.reply_text("Proceeding with video generation, please wait...")
        # Proceed with the video generation process
        result = await generate_video(context)
        if result == HANDLE_BRACKET_ERROR:
            return HANDLE_BRACKET_ERROR
        # Send options as buttons
        keyboard = [
            [KeyboardButton('Regenerate'), KeyboardButton('Upload')]
        ]
        reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
        await update.message.reply_text(
            "Do you want to regenerate or upload to YouTube?",
            reply_markup=reply_markup
        )
        return REGENERATE_OR_UPLOAD
    elif decision == 'edit':
        await update.message.reply_text("Please send the edited dialogue text.")
        return EDIT_DIALOGUE
    else:
        await update.message.reply_text("Invalid input. Please select an option from the buttons.")
        return CONFIRM_DIALOGUE

async def edit_dialogue(update: Update, context: ContextTypes.DEFAULT_TYPE):
    edited_text = update.message.text
    context.user_data['generated_text'] = edited_text
    text_file = os.path.join(SCRIPT_DIR, 'generated_text.txt')
    # Save the edited text to the file
    with open(text_file, 'w', encoding='utf-8') as f:
        f.write(edited_text)
    await update.message.reply_text("Edited dialogue saved. Proceeding with video generation, please wait...")
    # Proceed with the video generation process
    result = await generate_video(context)
    if result == HANDLE_BRACKET_ERROR:
        return HANDLE_BRACKET_ERROR
    # Send options as buttons
    keyboard = [
        [KeyboardButton('Regenerate'), KeyboardButton('Upload')]
    ]
    reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
    await update.message.reply_text(
        "Do you want to regenerate or upload to YouTube?",
        reply_markup=reply_markup
    )
    return REGENERATE_OR_UPLOAD

async def generate_video(context: ContextTypes.DEFAULT_TYPE):
    audio_dir = os.path.join(SCRIPT_DIR, 'audio')
    char_dir = os.path.join(SCRIPT_DIR, 'char')
    spongebob_dir = os.path.join(char_dir, 'spongebob')
    patrick_dir = os.path.join(char_dir, 'patrick')
    squid_dir = os.path.join(char_dir, 'squid')
    sandy_dir = os.path.join(char_dir, 'sandy')
    plankton_dir = os.path.join(char_dir, 'plankton')
    krabs_dir = os.path.join(char_dir, 'krabs')
    output_dir = os.path.join(SCRIPT_DIR, 'output')

    video_file = os.path.join(SCRIPT_DIR, 'input.mp4')
    text_file = os.path.join(SCRIPT_DIR, 'generated_text.txt')
    sfx_file = os.path.join(SCRIPT_DIR, 'whoosh.mp3')
    alignment_file = os.path.join(SCRIPT_DIR, 'alignment.json')

    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    if not os.path.exists(audio_dir):
        os.makedirs(audio_dir)
    else:
        # Clean audio directory
        for file in os.listdir(audio_dir):
            file_path = os.path.join(audio_dir, file)
            if os.path.isfile(file_path):
                os.remove(file_path)
        logging.info(f"Audio directory cleaned: {audio_dir}")

    timestamp = time.strftime("%Y%m%d-%H%M%S")
    output_video = os.path.join(output_dir, f"output_{timestamp}.mp4")
    context.user_data['output_video'] = output_video

    try:
        # Use the generated text from context
        generated_text = context.user_data['generated_text']
        # Save the generated text to the text file
        with open(text_file, 'w', encoding='utf-8') as f:
            f.write(generated_text)

        await process_dialog(text_file, audio_dir)
        await combine_audio(audio_dir)
        await process_video(
            video_file, audio_dir, text_file, output_video, alignment_file,
            spongebob_dir, patrick_dir, squid_dir, sandy_dir, plankton_dir, krabs_dir, sfx_file
        )
        logging.info("Video generation completed successfully")

        # Send generated video to user
        await send_video(output_video)
        return None

    except ValueError as e:
        logging.error(f"Bracket validation failed: {e}")
        # Send options as buttons
        keyboard = [
            [KeyboardButton('Generate Again'), KeyboardButton('Edit')]
        ]
        reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
        await bot.send_message(
            chat_id=TELEGRAM_CHAT_ID,
            text="Validation failed: Expected 12 brackets in the dialogue. Please choose an option:",
            reply_markup=reply_markup
        )
        return HANDLE_BRACKET_ERROR
    except Exception as e:
        logging.error(f"An error occurred during video generation: {e}")
        await bot.send_message(
            chat_id=TELEGRAM_CHAT_ID,
            text="An error occurred during video generation. Please try again."
        )
        return ConversationHandler.END

async def handle_bracket_error(update: Update, context: ContextTypes.DEFAULT_TYPE):
    decision = update.message.text.lower()
    if decision == 'generate again':
        await update.message.reply_text("Regenerating dialogue, please wait...")
        # Regenerate the dialogue
        await generate_dialogue(context)
        return CONFIRM_DIALOGUE
    elif decision == 'edit':
        await update.message.reply_text("Please send the edited dialogue text.")
        return EDIT_DIALOGUE
    else:
        await update.message.reply_text("Invalid input. Please select an option from the buttons.")
        return HANDLE_BRACKET_ERROR

async def regenerate_or_upload(update: Update, context: ContextTypes.DEFAULT_TYPE):
    decision = update.message.text.lower()
    if decision == 'regenerate':
        await update.message.reply_text("Regenerating video, please wait...")
        # Start the video generation process again
        result = await generate_video(context)
        if result == HANDLE_BRACKET_ERROR:
            return HANDLE_BRACKET_ERROR
        # Send options as buttons
        keyboard = [
            [KeyboardButton('Regenerate'), KeyboardButton('Upload')]
        ]
        reply_markup = ReplyKeyboardMarkup(keyboard, resize_keyboard=True, one_time_keyboard=True)
        await update.message.reply_text(
            "Do you want to regenerate or upload to YouTube?",
            reply_markup=reply_markup
        )
        return REGENERATE_OR_UPLOAD
    elif decision == 'upload':
        # Upload to YouTube
        output_video = context.user_data.get('output_video')
        if output_video:
            await update.message.reply_text("Uploading video to YouTube, please wait...")
            await upload_to_youtube(
                output_video,
                title="SpongeBob and Patrick Funny Video",
                description="A funny video generated by a bot."
            )
            await update.message.reply_text("Video uploaded to YouTube!")
        else:
            await update.message.reply_text("No video found to upload.")
        # End conversation
        return ConversationHandler.END
    else:
        await update.message.reply_text("Invalid input. Please select an option from the buttons.")
        return REGENERATE_OR_UPLOAD

async def send_video(video_file_path):
    # Before sending the video, check if the size is within Telegram's limit (50MB for normal accounts)
    max_file_size = 50 * 1024 * 1024  # 50MB
    file_size = os.path.getsize(video_file_path)
    if file_size > max_file_size:
        # Try to compress or resize the video
        compressed_video_path = video_file_path.replace('.mp4', '_compressed.mp4')
        await compress_video(video_file_path, compressed_video_path)
        video_file_path = compressed_video_path

    with open(video_file_path, 'rb') as video_file:
        await bot.send_video(chat_id=TELEGRAM_CHAT_ID, video=video_file, supports_streaming=True)
        logging.info(f"Video sent to Telegram chat {TELEGRAM_CHAT_ID}")

async def compress_video(input_path, output_path):
    # Compress the video to reduce file size
    from moviepy.video.io.ffmpeg_tools import ffmpeg_extract_subclip
    # Adjust bitrate to reduce file size
    clip = VideoFileClip(input_path)
    clip.write_videofile(output_path, codec="libx264", audio_codec="aac", bitrate="1000k", fps=24)
    clip.close()

async def upload_to_youtube(video_file_path, title, description):
    logging.info("Uploading video to YouTube")

    scopes = ["https://www.googleapis.com/auth/youtube.upload"]

    # Authenticate using OAuth 2.0
    credentials = None
    cred_file = os.path.join(SCRIPT_DIR, 'credentials.json')
    if os.path.exists(cred_file):
        credentials = Credentials.from_authorized_user_file(cred_file, scopes)
    # Refresh token if expired
    if credentials and credentials.expired and credentials.refresh_token:
        credentials.refresh(Request())
    else:
        flow = InstalledAppFlow.from_client_secrets_file(YOUTUBE_CLIENT_SECRETS_FILE, scopes)
        # Use run_local_server to handle the redirect_uri_mismatch error
        credentials = flow.run_local_server(port=8080)
        # Save the credentials for future use
        with open(cred_file, 'w') as cred_file_obj:
            cred_file_obj.write(credentials.to_json())

    youtube = build('youtube', 'v3', credentials=credentials)

    body = {
        'snippet': {
            'title': title,
            'description': description,
            'tags': ['SpongeBob', 'Patrick', 'Funny', 'Animation'],
            'categoryId': '24'  # 24 corresponds to 'Entertainment'
        },
        'status': {
            'privacyStatus': 'private',
        }
    }

    media = MediaFileUpload(video_file_path, chunksize=-1, resumable=True)

    request = youtube.videos().insert(
        part=','.join(body.keys()),
        body=body,
        media_body=media
    )

    response = None
    while response is None:
        status, response = request.next_chunk()
        if status:
            logging.info(f"Uploaded {int(status.progress() * 100)}%")

    logging.info(f"Video uploaded to YouTube: {response.get('id')}")

async def text_to_speech(text_to_speech, character, tts_model_token, count, audio_dir):
    def request_tts(tts_model_token, text):
        url = 'https://api.fakeyou.com/tts/inference'
        headers = {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }
        payload = {
            "uuid_idempotency_token": str(uuid.uuid4()),
            "tts_model_token": tts_model_token,
            "inference_text": text
        }
        response = requests.post(url, json=payload, headers=headers)
        if response.status_code == 200:
            result = response.json()
            if result.get('success'):
                return result['inference_job_token']
            else:
                raise Exception("Failed to request TTS: " + result.get('error', 'Unknown error'))
        else:
            raise Exception(f"Failed to request TTS with status code {response.status_code}")

    def check_tts_status(job_token):
        url = f'https://api.fakeyou.com/tts/job/{job_token}'
        headers = {
            'Accept': 'application/json'
        }
        response = requests.get(url, headers=headers)
        if response.status_code == 200:
            result = response.json()
            if result.get('success'):
                status = result['state']['status']
                if status == 'complete_success':
                    return result['state']['maybe_public_bucket_wav_audio_path']
                elif status == 'pending' or status == 'started':
                    return None
                else:
                    raise Exception("TTS request failed: " + status)
            else:
                raise Exception("Failed to check TTS status: " + result.get('error', 'Unknown error'))
        else:
            raise Exception(f"Failed to check TTS status with status code {response.status_code}")

    def download_audio(file_path):
        base_url = 'https://storage.googleapis.com/vocodes-public'
        download_url = f'{base_url}{file_path}'
        response = requests.get(download_url)
        download_directory = audio_dir
        file_name = os.path.join(download_directory, f'{count}_{character}.wav')
        if response.status_code == 200:
            with open(file_name, 'wb') as audio_file:
                audio_file.write(response.content)
            logging.info(f"Audio downloaded successfully as {file_name}")
            return file_name
        else:
            raise Exception(f"Failed to download audio with status code {response.status_code}")
    
    def check_audio(file_name):
        audio = AudioSegment.from_wav(file_name)
        silence_threshold = -40
        silent_ranges = detect_silence(audio, min_silence_len=1000, silence_thresh=silence_threshold)
        if silent_ranges:
            start_silence, end_silence = silent_ranges[0][0], silent_ranges[-1][-1]
            if start_silence == 0 or end_silence >= len(audio) - 1000:
                return False
        return True

    def main():
        try:
            logging.info("Requesting TTS...")
            job_token = request_tts(tts_model_token, text_to_speech)
            logging.info(f"TTS requested. Job Token: {job_token}")
            
            audio_path = None
            while audio_path is None:
                logging.info("Checking status...")
                time.sleep(5)
                audio_path = check_tts_status(job_token)
            
            logging.info(f"Audio available at: {audio_path}")
            file_name = download_audio(audio_path)
            if not check_audio(file_name):
                logging.info("Audio has too much silence, retrying TTS request...")
                main()
            else:
                logging.info("Audio passed the silence check.")
        except Exception as e:
            logging.error(f"An error occurred: {e}")
    if __name__ == "__main__":
        main()

async def generate(text_file, script_dir, user_input):
    while True:
        logging.info("Generating content for dialogue...")

        file = os.path.join(script_dir, 'content.json')
        with open(file, 'r') as f:
            data = json.load(f)
        category = random.choice(['Dad', 'Battery', 'Birthday', 'Name', 'Mom'])
        logging.info(f"Selected category: {category}")
        text = data[category]
        prompt = (
            "I want you to act as a dynamic dialogue generator to create a humorous dialogue between SpongeBob and Patrick with occasional appearances by Sandy, Mr. Krabs, Plankton, or Squidward that lasts at least 50 seconds. "
            "The conversation should have 10 turns, with SpongeBob starting first, followed by Patrick and others. "
            "Each character will guess something fun about the viewers, making it silly and engaging. "
            "You should generate new content each time, using this pattern: "
            "For the first []: integrate it into a sentence like this example: '[IF I GUESS YOUR ----] YOU ARE ELIMINATED' as a title and (limit to 5 words in brackets). "
            "For the second [], it should end with the word 'edition' and use exactly one word (e.g., '[funny word] edition'). "
            "For the remaining 3-12 [], only use exactly one word in the brackets. "
            "Ensure the total bracketed count in text is exactly 12. "
            "Make sure the dialogue is creative, funny, lighthearted, and corrected for grammar."
            f"Here is an example: {text}"
        )

        client = Client("Qwen/Qwen2.5-72B-Instruct")
        try:
            result = client.predict(
                query=user_input,
                history=[],
                system=prompt,
                api_name="/model_chat"
            )
            response = result[1][0][1]
            cleaned_response = re.sub(r'<.*?>|\*+', '', response)

            characters = ['SpongeBob', 'Patrick', 'Sandy', 'Mr. Krabs', 'Plankton', 'Squidward']
            for character in characters:
                cleaned_response = re.sub(rf'({character}:)', rf'\n{character}:', cleaned_response)

            with open(text_file, 'w', encoding='utf-8') as f:
                f.write(cleaned_response)
            logging.info(f"Generated content: {cleaned_response}")
            logging.info(f"Generated content saved to {text_file}")
            return cleaned_response

        except Exception as e:
            logging.info(f"Failed to generate content: {e}")
            raise e
    
async def speech_to_text(text, audio_file, alignment_file):
    logging.info(f"Running Azure Speech SDK for {audio_file}")
    speech_config = speechsdk.SpeechConfig(subscription="874b3eb84cf749fe90eb4443dd9ff0a5", region="eastasia")
    speech_config.output_format = speechsdk.OutputFormat.Detailed
    speech_config.request_word_level_timestamps()
    audio_config = speechsdk.AudioConfig(filename=audio_file)
    reference_text = text
    pronunciation_config = speechsdk.PronunciationAssessmentConfig(
        reference_text=reference_text,
        grading_system=speechsdk.PronunciationAssessmentGradingSystem.HundredMark,
        granularity=speechsdk.PronunciationAssessmentGranularity.Phoneme)
    speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, language='en-US', audio_config=audio_config)
    pronunciation_config.apply_to(speech_recognizer)
    done = False
    word_details = []
    def stop_cb(evt: speechsdk.SessionEventArgs):
        nonlocal done
        done = True
    def recognized(evt: speechsdk.SpeechRecognitionEventArgs):
        json_result = evt.result.properties.get(speechsdk.PropertyId.SpeechServiceResponse_JsonResult)
        try:
            jo = json.loads(json_result)
            nb = jo.get("NBest", [{}])[0]
            words = nb.get("Words", [])
            nonlocal word_details
            word_details.extend([
                (
                    w.get("Word", ""),
                    int(w.get("Offset", 0)),
                    int(w.get("Duration", 0))
                )
                for w in words
            ])
        except json.JSONDecodeError as e:
            logging.error(f"JSON decode error: {e}")
        except Exception as e:
            logging.error(f"Unexpected error: {e}")

    speech_recognizer.recognized.connect(recognized)
    speech_recognizer.session_stopped.connect(stop_cb)
    speech_recognizer.canceled.connect(stop_cb)
    speech_recognizer.start_continuous_recognition()
    while not done:
        time.sleep(.5)
    speech_recognizer.stop_continuous_recognition()
    with open(alignment_file, 'w') as f:
        json.dump(word_details, f)
    logging.info(f"Word details saved to: {alignment_file}")

async def process_dialog(text, audio_dir):
    logging.info("Processing dialogue and scraping audio")
    with open(text, 'r', encoding='utf-8') as f:
        text_content = f.read()
    brackets = re.findall(r'\[(.*?)\]', text_content)
    if len(brackets) != 12:
        raise ValueError(f"Validation failed: Expected 12 brackets, but found {len(brackets)}. Aborting the process.")
    count = 1
    lines = text_content.splitlines()
    for line in lines: 
        logging.info(f"Processing line: {line}")
        if "SpongeBob:" in line:
            character = "SpongeBob"
            url = 'weight_qdtx8h0mvn945md9tm2zaq422'
        elif "Patrick:" in line:
            character = "Patrick"
            url = 'weight_17enezbhvjmzxkcp87v46zv8n'
        elif "Sandy:" in line:
            character = "Sandy"
            url = 'weight_89asn9bw0xghm9be0ths6pmxq'
        elif "Squidward:" in line:
            character = "Squidward"
            url = 'weight_pdf4b4pknzq89d011cd95029e'
        elif "Mr. Krabs:" in line:
            character = "Mr. Krabs"
            url = 'weight_zsqq6mtdcr0c5n0shxcpy96tt'
        elif "Plankton:" in line:
            character = "Plankton"
            url = 'weight_vdpxnv3frpkk0mrpvs8jz62xa'
        else:
            continue
        dialog_text = line.split(": ", 1)[1].replace("[", "").replace("]", "").lower()
        await text_to_speech(dialog_text, character, url, count, audio_dir)
        count += 1

async def combine_audio(directory):
    logging.info(f"Combining audio files in directory: {directory}")
    audio_files = sorted(
        [os.path.join(directory, file) for file in os.listdir(directory) if file.endswith(".wav")],
        key=lambda f: int(re.match(r'(\d+)_', os.path.basename(f)).group(1))
    )
    if not audio_files:
        logging.error("No audio files found to combine")
        return
    combined_audio = concatenate_audioclips([AudioFileClip(file) for file in audio_files])
    combined_audio_file = os.path.join(directory, "combined_audio.wav")
    combined_audio.write_audiofile(combined_audio_file)
    logging.info(f"Combined audio saved at: {combined_audio_file}")

async def resize(video_clip):
    logging.info("Resizing video")
    target_width, target_height = 1080, 1920
    target_aspect_ratio = target_width / target_height
    video_aspect_ratio = video_clip.w / video_clip.h
    if video_aspect_ratio > target_aspect_ratio:
        new_width = int(target_aspect_ratio * video_clip.h)
        video_clip = video_clip.crop(x_center=video_clip.w // 2, width=new_width)
    elif video_aspect_ratio < target_aspect_ratio:
        new_height = int(video_clip.w / target_aspect_ratio)
        video_clip = video_clip.crop(y_center=video_clip.h // 2, height=new_height)
    return video_clip.resize((target_width, target_height))

async def designing(text, video, audio_file, output_file, alignment_file, spongebob_dir, patrick_dir, squid_dir, sandy_dir, plankton_dir, krabs_dir, sfx_file):
    logging.info(f"Creating design for {audio_file}")
    logging.info("Cleaning and preparing the text")
    characters = ['SpongeBob', 'Patrick', 'Sandy', 'Mr. Krabs', 'Plankton', 'Squidward']
    cleaned_text = re.sub(rf"({'|'.join(characters)}):", '', text)
    cleaned_text = re.sub(r'[^\w\s!\'?[\]]', '', cleaned_text)
    cleaned_text = re.sub(r'\[.*?\]', '', cleaned_text, count=2)

    await speech_to_text(cleaned_text, audio_file, alignment_file)
    logging.info(f"Loading alignment data from {alignment_file}")
    with open(alignment_file, 'r') as f:
        alignment = json.load(f)

    logging.info("Matching cleaned text with alignment data")
    text_words = cleaned_text.split()
    def normalize(word):
        return re.sub(r"^\W*|\W+$", '', word).lower()
    def align_words(text_words, alignment):
        normalized_text_words = [normalize(w) for w in text_words]
        alignment_words = [w[0] for w in alignment]
        normalized_alignment_words = [normalize(w) for w in alignment_words]
        matcher = difflib.SequenceMatcher(None, normalized_alignment_words, normalized_text_words)
        opcodes = matcher.get_opcodes()
        align_idx = 0
        matched_alignment = []
        for tag, i1, i2, j1, j2 in opcodes:
            if tag == 'equal':
                for idx in range(i1, i2):
                    text_word = text_words[j1 + (idx - i1)]
                    offset, duration = alignment[align_idx][1], alignment[align_idx][2]
                    matched_alignment.append([text_word, offset, duration])
                    align_idx += 1
            elif tag == 'replace':
                for idx in range(i1, i2):
                    text_word = text_words[j1 + (idx - i1)]
                    offset, duration = alignment[align_idx][1], alignment[align_idx][2]
                    matched_alignment.append([text_word, offset, duration])
                    align_idx += 1
            elif tag == 'insert':
                for idx in range(j1, j2):
                    text_word = text_words[idx]
                    prev_offset = matched_alignment[-1][1] + matched_alignment[-1][2] if matched_alignment else 0
                    next_offset = alignment[align_idx][1] if align_idx < len(alignment) else prev_offset + 1000000
                    estimated_duration = (next_offset - prev_offset) // (j2 - j1)
                    matched_alignment.append([text_word, prev_offset, estimated_duration])
                    prev_offset += estimated_duration
            elif tag == 'delete':
                align_idx += (i2 - i1)
        return matched_alignment
    matched_alignment = align_words(text_words, alignment)

    logging.info("Handling bracketed words for combining text")
    combined_alignment = []
    combining = False
    combined_word = ""
    combined_offset = 0
    combined_duration = 0
    for word, offset, duration in matched_alignment:
        if '[' in word and ']' in word:
            combined_alignment.append((word, offset, duration))
        elif '[' in word:
            combining = True
            combined_word = word
            combined_offset = offset
            combined_duration = duration
        elif combining:
            combined_word += " " + word
            combined_duration += duration
            if ']' in word:
                combining = False
                combined_alignment.append((combined_word, combined_offset, combined_duration))
        else:
            combined_alignment.append((word, offset, duration))
    
    logging.info("Preparing to display images based on text and alignment")
    clips = []
    sfx_clips = []
    font = 'Luckiest-Guy'
    bracket_index = 0
    video_duration = video.duration
    lines = text.strip().split('\n')
    dialogues = []
    for line in lines:
        if any(line.startswith(f"{character}:") for character in characters):
            character, dialogue = line.split(": ", 1)
            words = dialogue.split()[:2] if len(dialogue.split()) >= 2 else dialogue.split()
            dialogues.append((character, words))

    comb_words = [(word.lower().replace(',', '').replace('!', ''), offset, duration) for word, offset, duration in matched_alignment]
    dialogues = [(character.lower(), [w.replace('*', '').replace(',', '').replace('!', '').lower() for w in dialogue_words]) for character, dialogue_words in dialogues]
    SIMILARITY_THRESHOLD = 0.5
    found_offsets = []
    last_matched_offset = 0

    for character, words in dialogues:
        for i in range(len(comb_words) - 1):
            word_1 = comb_words[i][0]
            word_2 = comb_words[i + 1][0]
            offset_1 = comb_words[i][1]
            if offset_1 <= last_matched_offset:
                continue
            match_first_word = difflib.SequenceMatcher(None, word_1, words[0]).ratio() > SIMILARITY_THRESHOLD
            match_second_word = difflib.SequenceMatcher(None, word_2, words[1]).ratio() > SIMILARITY_THRESHOLD
            if match_first_word and match_second_word:
                found_offsets.append((character, comb_words[i][1]))
                last_matched_offset = comb_words[i][1]
                break

    def load_image(image_path, target_size=(800, 800), resample=Image.LANCZOS):
        try:
            logging.info(f"Loading image from: {image_path}")
            img = Image.open(image_path)
            logging.info(f"Image mode: {img.mode}, Image size: {img.size}")
            if img.mode != 'RGBA':
                img = img.convert('RGBA')
            if target_size is not None:
                original_size = img.size
                original_ratio = original_size[0] / original_size[1]
                target_ratio = target_size[0] / target_size[1]
                if original_ratio > target_ratio:
                    new_width = target_size[0]
                    new_height = int(new_width / original_ratio)
                else:
                    new_height = target_size[1]
                    new_width = int(new_height * original_ratio)
                img = img.resize((new_width, new_height), resample=resample)
                logging.info(f"Resized image to: {target_size}")
            img_np = np.array(img)
            return img_np
        except Exception as e:
            logging.error(f"An error occurred while loading the image: {e}")
            return None
    
    logging.info(f"Adding Image")
    character_dirs = {
        "spongebob": spongebob_dir,
        "patrick": patrick_dir,
        "sandy": sandy_dir,
        "squidward": squid_dir,
        "mr. krabs": krabs_dir,
        "plankton": plankton_dir
    }

    for i in range(len(found_offsets)):
        character, start_offset = found_offsets[i]
        start_time = start_offset / 1e7
        if i + 1 < len(found_offsets):
            next_offset = found_offsets[i + 1][1]
        else:
            next_offset = video.duration * 1e7

        duration = (next_offset - start_offset) / 1e7

        image_dir = character_dirs[character.lower()]
        image_path = os.path.join(image_dir, random.choice(os.listdir(image_dir)))
        logging.info(f"Adding {character} image from {start_time:.2f} to {start_time + duration:.2f}")
        image_path = load_image(image_path)
        image_clip = (
            ImageClip(image_path, transparent=True)
            .with_start(start_time)
            .with_duration(duration)
            .with_position(('center', 1200))
            .resize(lambda t: 
                1.5 + (0.8 - 1.5) * (t / 0.05) if t < 0.05 else
                0.8 + (1.2 - 0.8) * ((t - 0.05) / 0.05) if t < 0.1 else
                1.2 + (0.9 - 1.2) * ((t - 0.1) / 0.05) if t < 0.15 else
                0.9 + (1.0 - 0.9) * ((t - 0.15) / 0.05) if t < 0.2 else
                1.0 if t < (duration - 0.3) else
                (1.0 + (0.1 - 1.0) * ((t - (duration - 0.3)) / 0.3)) if t < duration else
                0.1
            )
            .rotate(lambda t: 5 * math.sin(2 * math.pi * t * 0.3))
        )
        clips.append(image_clip)
        sound_effect = AudioFileClip(sfx_file).with_start(start_time)
        sfx_clips.append(sound_effect)

    matches = re.findall(r'\[(.*?)\]', text)
    text1 = matches[0]
    text2 = matches[1]

    def color_hunt():
      def is_bright(hex_color):
        hex_color = hex_color.lstrip('#')
        rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
        luminance = 0.299 * rgb[0] + 0.587 * rgb[1] + 0.114 * rgb[2]
        r, g, b = [x / 255.0 for x in rgb]
        h, s, v = colorsys.rgb_to_hsv(r, g, b)
        return luminance >= 130 and s >= 0.3
      logging.info(f"Selecting Colors")
      while True:
          url = 'https://colorhunt.co/php/feed.php'
          headers = {
              'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
          }
          data = {
              'step': 0,
              'sort': 'random',
              'tags': ''
          }
          response = requests.post(url, headers=headers, data=data)
          if response.status_code == 200:
              palettes = response.json()
              color_pairs = []
              for palette in palettes:
                  code = palette['code']
                  hex_colors = [f'#{code[i:i+6].upper()}' for i in range(0, len(code), 6)]
                  if len(hex_colors) == 4 and all(is_bright(hex_color) for hex_color in hex_colors):
                      color_pairs.append(tuple(hex_colors))
              if color_pairs:
                  selected_color_pair = random.choice(color_pairs)
                  logging.info(f"Selected Color Pair: {selected_color_pair}")
                  return selected_color_pair
          else:
            raise Exception(f"Failed to retrieve data. Status code: {response.status_code}")

    selected_colors = color_hunt()
    color1, color2, color3, color4 = selected_colors
    logging.info(f"Adding Top Text")
    max_text_width = 1080 * 0.9
    top_text = (
        TextClip(f"{text1}, YOU'ARE ELIMINATED", font_size=90, color=color1, font=font, stroke_color='black', stroke_width=6, align='center', method='caption', size=(max_text_width, None))
        .with_duration(video_duration)
        .with_position(('center', 100))
    )
    clips.append(top_text)

    top_text = (
        TextClip(text2, font_size=90, color=color2, font=font, stroke_color='black', stroke_width=6, align='center')
        .with_duration(video_duration)
        .with_position(('center', 400))
    )
    clips.append(top_text)

    logging.info(f"Adding Number")
    positions = [
        (150, 510), (150, 610), (150, 710), (150, 810), (150, 910),
        (620, 510), (620, 610), (620, 710), (620, 810), (620, 910)
    ]

    for i, position in enumerate(positions):
      number_clip = (
          TextClip(f"{i + 1}.", font_size=65, color=color3, font=font, stroke_color='black', method='caption', stroke_width=5)
          .with_duration(video_duration - i)
          .with_position((position[0]-80, position[1]))
          .with_start(i)
          .resize(lambda t:
              1.5 + (0.8 - 1.5) * (t / 0.05) if t < 0.05 else
              0.8 + (1.2 - 0.8) * ((t - 0.05) / 0.05) if t < 0.1 else
              1.2 + (0.9 - 1.2) * ((t - 0.1) / 0.05) if t < 0.15 else
              0.9 + (1.0 - 0.9) * ((t - 0.15) / 0.05) if t < 0.2 else
              1.0
          )
      )
      clips.append(number_clip)

    logging.info(f"Adding Caption")
    bracketed_words = set()
    for word, offset, duration in combined_alignment:
        offset_sec = offset / 1e7
        duration_sec = duration / 1e7
        is_bracketed = '[' in word and ']' in word
        color = color4 if is_bracketed else 'white'
        word = word.replace('[', '').replace(']', '') if is_bracketed else word
        if is_bracketed and word in bracketed_words:
          continue
        subtitle_clip = (
            TextClip(word, font_size=75, color=color, font=font, stroke_color='black', method='caption', stroke_width=6)
            .with_duration(duration_sec)
            .with_position(('center', 1100))
            .with_start(offset_sec)
            .resize(lambda t:
                0.6 + (1.2 - 0.6) * (t / 0.05) if t < 0.05 else
                1.2 + (0.9 - 1.2) * ((t - 0.05) / 0.05) if t < 0.1 else
                0.9 + (1.1 - 0.9) * ((t - 0.1) / 0.05) if t < 0.15 else
                1.1 + (1.0 - 1.1) * ((t - 0.15) / 0.05) if t < 0.2 else
                1.0
            )
        )
        clips.append(subtitle_clip)
        month_abbr = {
            "January": "Jan",
            "February": "Feb",
            "March": "Mar",
            "April": "Apr",
            "May": "May",
            "June": "Jun",
            "July": "Jul",
            "August": "Aug",
            "September": "Sep",
            "October": "Oct",
            "November": "Nov",
            "December": "Dec"
        }
        def replace_month_abbr(text):
            for full_month, abbr in month_abbr.items():
                if full_month in text:
                    return text.replace(full_month, abbr)
            return text
        if is_bracketed:
            word = replace_month_abbr(word)
            bracketed_words.add(word)
            temp_clip = TextClip(word, font_size=65, font=font)
            text_width, _ = temp_clip.size
            max_column_width = 385
            if text_width > max_column_width:
                scale_factor = max_column_width / text_width
                new_font_size = int(65 * scale_factor)
                logging.info(f"Resizing text '{word}' to fit within column. New font size: {new_font_size}")
            else:
                new_font_size = 65
            position = positions[bracket_index % len(positions)]
            bracket_index += 1
            logging.info(f"Creating bracketed word clip for '{word}' at position {position}")
            bracket_word_clip = (
                TextClip(word, font_size=new_font_size, color='white', font=font, stroke_color='black', stroke_width=5, align='West', method='caption', size=(max_text_width, None))
                .with_duration(video_duration - offset_sec)
                .with_position(position)
                .with_start(offset_sec)
                .resize(lambda t:
                    1.5 + (0.8 - 1.5) * (t / 0.05) if t < 0.05 else
                    0.8 + (1.2 - 0.8) * ((t - 0.05) / 0.05) if t < 0.1 else
                    1.2 + (0.9 - 1.2) * ((t - 0.1) / 0.05) if t < 0.15 else
                    0.9 + (1.0 - 0.9) * ((t - 0.15) / 0.05) if t < 0.2 else
                    1.0
                )
            )
            clips.append(bracket_word_clip)
    audio = video.audio
    final_audio = CompositeAudioClip([audio] + sfx_clips)
    logging.info(f"Writing final video to {output_file}")
    video = CompositeVideoClip([video] + clips)
    video.audio = final_audio
    video.write_videofile(
        output_file,
        codec="libx264",
        audio_codec="aac",
        bitrate="2000k",
        fps=30,
        # Ensure the file size is within Telegram's limit by adjusting parameters as needed
    )
    logging.info(f"Video with captions saved at: {output_file}")

async def process_video(video_file, audio_dir, text_file, output_video, alignment_file, spongebob_dir, patrick_dir, squid_dir, sandy_dir, plankton_dir, krabs_dir, sfx_file):
    logging.info("Processing video with combined audio")
    with open(text_file, 'r', encoding='utf-8') as f:
        text = f.read()
    combined_audio_file = os.path.join(audio_dir, "combined_audio.wav")
    video = VideoFileClip(video_file)
    audio = AudioFileClip(combined_audio_file)
    video_duration = video.duration
    audio_duration = audio.duration
    start_time = random.uniform(0, video_duration - audio_duration)
    end_time = start_time + audio_duration
    video_clip = video.subclip(start_time, end_time)
    video_clip = await resize(video_clip)
    video_clip = video_clip.with_audio(audio)
    await designing(text, video_clip, combined_audio_file, output_video, alignment_file, spongebob_dir, patrick_dir, squid_dir, sandy_dir, plankton_dir, krabs_dir, sfx_file)

def main():
    application = ApplicationBuilder().token(TELEGRAM_BOT_TOKEN).build()

    # Define the conversation handler
    conv_handler = ConversationHandler(
        entry_points=[CommandHandler('start', start)],
        states={
            ASK_VIDEO_ABOUT: [MessageHandler(filters.TEXT & ~filters.COMMAND, ask_video_about)],
            CONFIRM_DIALOGUE: [MessageHandler(filters.TEXT & ~filters.COMMAND, confirm_dialogue)],
            EDIT_DIALOGUE: [MessageHandler(filters.TEXT & ~filters.COMMAND, edit_dialogue)],
            REGENERATE_OR_UPLOAD: [MessageHandler(filters.TEXT & ~filters.COMMAND, regenerate_or_upload)],
        },
        fallbacks=[],
    )

    application.add_handler(conv_handler)

    # Start the application
    application.run_polling()

if __name__ == '__main__':
    try:
        main()
    except KeyboardInterrupt:
        logging.warning("Script stopped by user. Exiting gracefully.")
        sys.exit(0)