sederhanakan cara untuk variabel character agar untuk pembuatan nya satu saja untuk semua script lalu sederhanakan juga untuk character directory nya: import random
import asyncio
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, ColorClip

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

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):
    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 [], 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 the example: {text}"
        )

        user_input = input("What is the video about: ")
        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)

            brackets = re.findall(r'\[(.*?)\]', cleaned_response)
            if len(brackets) != 12:
                raise ValueError(f"Validation failed: Expected 12 brackets, but found {len(brackets)}. Aborting the process.")

            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}")

            user_decision = input("Do you want to generate again or continue? (type 'again' to regenerate, 'continue' to finish): ").lower()
            if user_decision != 'again':
                break

        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'
        elif "Cristiano Ronaldo:" in line:
            character = "Cristiano Ronaldo"
            url = 'weight_ren3ng1bvpktsvg58gcrv2s52'
        elif "Mr. Beast:" in line:
            character = "Mr. Beast"
            url = 'weight_qqqtmnrpjmq2dc40ex5rr8t95'
        elif "IShowSpeed:" in line:
            character = "IShowSpeed"
            url = 'weight_msq6440ch8hj862nz5y255n8j'
        elif "Donald Trump:" in line:
            character = "Donald Trump"
            url = 'weight_ppqs5038bvkm6wc29w0xfebzy'
        elif "Carl Johnson:" in line:
            character = "Carl Johnson"
            url = 'weight_3796yzf18mr3nq0qaccb1t265'
        elif "Elon Musk:" in line:
            character = "Elon Musk"
            url = 'weight_q5xbb54g2vmeeq2722zzfs897'
        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 design_model_1(
        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()

    matched_alignment = match_alignment(text_words, alignment)

    combined_alignment = handle_brackets(matched_alignment)

    found_offsets = find_image_offsets(text, matched_alignment)

    logging.info("Preparing to display images based on text and alignment")
    clips = []
    sfx_clips = []
    font = 'Luckiest-Guy'
    bracket_index = 0
    video_duration = video.duration

    character_dirs = {
        "spongebob": spongebob_dir,
        "patrick": patrick_dir,
        "sandy": sandy_dir,
        "squidward": squid_dir,
        "mr. krabs": krabs_dir,
        "plankton": plankton_dir,
        "mr. beast": plankton_dir,
        "cristiano ronaldo": plankton_dir,
        "ishowspeed": plankton_dir,
        "donald trump": plankton_dir,
        "carl johnson": plankton_dir,
        "elon musk":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_np = load_image(image_path)
        image_clip = (
            ImageClip(image_np, 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]

    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="7000k",
        fps=30,
    )
    logging.info(f"Video with captions saved at: {output_file}")

async def design_model_2(
    text, video, audio_file, output_file, alignment_file
    # Add any additional parameters if needed
):
    # Design function for Model 2 (currently empty)
    pass

async def design_model_3(
    text, video, audio_file, output_file, alignment_file
    # Add any additional parameters if needed
):
    # Design function for Model 3 (currently empty)
    pass

def match_alignment(text_words, alignment):
    logging.info("Matching cleaned text with alignment data")
    def normalize(word):
        return re.sub(r"^\W*|\W+$", '', word).lower()
    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

def handle_brackets(matched_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))
    return combined_alignment

def find_image_offsets(text, matched_alignment):
    logging.info("Preparing to display images based on text and alignment")
    characters = ['SpongeBob',
                  'Patrick',
                  'Sandy', 
                  'Mr. Krabs', 
                  'Plankton', 
                  'Squidward', 
                  'Mr. Beast',
                  'Cristiano Ronaldo',
                  'IShowSpeed',
                  'Donald Trump',
                  'Carl Johnson',
                  'Elon Musk',
                  ]
    
    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
    return found_offsets

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

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}")
        
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 design_model_1(text, video_clip, combined_audio_file, output_video, alignment_file, spongebob_dir, patrick_dir, squid_dir, sandy_dir, plankton_dir, krabs_dir, sfx_file)

async def main():
    script_dir = os.path.dirname(os.path.abspath(__file__))
    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)

    while True:
        logging.info("Starting main process")
        timestamp = time.strftime("%Y%m%d-%H%M%S")
        output_video = os.path.join(output_dir, f"output_{timestamp}.mp4")
        # if os.path.exists(audio_dir):
        #     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}")
        # else:
        #     os.makedirs(audio_dir)
        try:
            # await generate(text_file, script_dir)
            # 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("Main process completed successfully")
            break
        except ValueError as ve:
            logging.error(f"Validation error: {ve}. Restarting the process.")
            continue
        except Exception as e:
            logging.error(f"An error occurred: {e}")
            time.sleep(5)
            continue

try:
    asyncio.run(main())
except KeyboardInterrupt:
    logging.warning("Script stopped by user. Exiting gracefully.")
    sys.exit(0)