corect the syntax only: import sys
import os
import re
import math
import json
import hashlib
import logging
import datetime
import traceback
import subprocess
import requests
from openai import OpenAI
from openai._exceptions import OpenAIError, RateLimitError
from dotenv import load_dotenv
from pydub import AudioSegment
from PIL import Image

# Fix for PIL.Image.ANTIALIAS issues in newer Pillow versions
import PIL.Image
if not hasattr(PIL.Image, 'ANTIALIAS'):
    # For Pillow 9.0.0+
    try:
        PIL.Image.ANTIALIAS = PIL.Image.Resampling.LANCZOS
    except:
        try:
            # Fallback for other versions
            PIL.Image.ANTIALIAS = PIL.Image.LANCZOS
        except:
            # Last resort
            pass

import numpy as np
import srt
from srt import Subtitle
from typing import List, Optional
from _utils import perf_monitor, show_running_message_decorator, hm_sz, hm_time

# Load environment variables from the .env file
load_dotenv()

# Access the OpenAI API key
api_key = os.getenv("OPENAI_API_KEY")
if not api_key:
    raise ValueError("No OpenAI API key found. Please set OPENAI_API_KEY in your .env file.")

# Initialize the OpenAI client
client = OpenAI(api_key=api_key)

# Constants
story_filename = "story.txt"
num_ChatGPT_images = 1  # Number of images to generate
# Changed image size for DALL-E 2 compatibility - must be 1024x1024
size_ChatGPT_image = "1024x1024"  # DALL-E 2 requires square dimensions
frame_rate = 30
output_dir = 'output'

# YouTube video dimensions (Full HD 1080p)
video_width = 1920
video_height = 1080

# OpenAI TTS voice options
TTS_VOICE_OPTIONS = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]
TTS_DEFAULT_VOICE = "nova"  # More natural storytelling voice

# Define a collection of artistic styles to choose from
ART_STYLES = {
    "storybook": "animated storybook illustration with vibrant colors and clean lines",
    "watercolor": "soft watercolor painting with gentle brush strokes and pastel colors",
    "3d_animation": "3D animated movie render with detailed textures and soft lighting",
    "pixel_art": "detailed pixel art with vibrant colors like a retro video game",
    "comic_book": "comic book style with bold outlines, vibrant colors and dynamic composition",
    "claymation": "claymation style with textured clay-like materials and warm lighting",
    "anime": "anime style with bright colors, large expressive eyes, and dynamic composition",
    "disney": "Disney animation style with rounded shapes, expressive characters and vibrant colors",
    "studio_ghibli": "Studio Ghibli style with painterly backgrounds, soft colors and detailed environments",
    "noir": "film noir style with dramatic shadows, high contrast and moody atmosphere",
    "impressionist": "impressionist painting style with visible brush strokes and vibrant light effects"
}

# Create output directory if it doesn't exist
if not os.path.exists(output_dir):
    os.makedirs(output_dir)

def analyze_story_for_characters(story_lines):
    """
    Automatically analyze a story to detect characters and generate consistent descriptions.
    This completely automated approach requires no user input.
    """
    # Step 1: Identify potential characters (using NLP patterns)
    potential_characters = set()
    character_contexts = {}  # Store contextual information for each character
    
    # Common adjectives that might describe characters
    appearance_adjectives = [
        "tall", "short", "young", "old", "beautiful", "handsome", "pretty", 
        "ugly", "fat", "thin", "skinny", "plump", "slim", "stout", "large", 
        "small", "huge", "tiny", "big", "little", "giant", "miniature",
        "blonde", "brunette", "redhead", "gray-haired", "bald", "curly-haired", 
        "straight-haired", "dark-haired", "light-haired", "fair-haired"
    ]
    
    # Scan the story for capitalized nouns (potential characters)
    for line in story_lines:
        words = line.split()
        for i, word in enumerate(words):
            # Look for capitalized words that might be character names (not at start of sentence)
            if ((i > 0 and word[0].isupper() and len(word) > 1) or 
                (i == 0 and len(words) > 1 and word[0].isupper() and not any(word.lower().endswith(p) for p in ['.', '?', '!']))):
                
                # Clean the word (remove punctuation)
                clean_word = ''.join(c for c in word if c.isalnum())
                if clean_word and len(clean_word) > 1:  # Avoid single letters
                    # Check if it's a common non-character capitalized word
                    if clean_word.lower() not in ["the", "a", "an", "and", "but", "or", "so", "because", 
                                                "however", "therefore", "thus", "then", "there", "these", 
                                                "those", "this", "that", "i", "my", "mine", "me"]:
                        potential_characters.add(clean_word)
                        
                        # Collect contextual information around this character mention
                        start_idx = max(0, i - 3)
                        end_idx = min(len(words), i + 4)
                        context = ' '.join(words[start_idx:end_idx])
                        
                        if clean_word not in character_contexts:
                            character_contexts[clean_word] = []
                        character_contexts[clean_word].append(context)
    
    print(f"Detected potential characters: {', '.join(potential_characters)}")
    
    # Step 2: Generate descriptions based on context
    character_descriptions = {}
    for character in potential_characters:
        # Analyze contexts to extract descriptive information
        all_contexts = ' '.join(character_contexts[character]).lower()
        
        # Look for descriptive adjectives in context
        found_adjectives = [adj for adj in appearance_adjectives if f" {adj} {character.lower()}" in all_contexts or f"{character.lower()} is {adj}" in all_contexts]
        
        # Check for animal or person
        is_animal = any(animal in all_contexts for animal in ["cat", "dog", "bird", "fox", "wolf", "bear", "lion", "tiger", "rabbit", "mouse", "animal", "creature", "pet"])
        is_child = any(child_term in all_contexts for child_term in ["child", "boy", "girl", "baby", "infant", "toddler", "kid", "teenager", "young"])
        is_male = any(male_term in all_contexts for male_term in [" he ", " his ", " him ", " man ", " male ", " boy ", " guy "])
        is_female = any(female_term in all_contexts for female_term in [" she ", " her ", " hers ", " woman ", " female ", " girl ", " lady "])
        is_elder = any(elder_term in all_contexts for elder_term in [" elder ", " elderly ", " old ", " ancient ", " grandpa ", " grandma ", " grandmother ", " grandfather "])
        
        # Generate a description based on the gathered information
        description = ""
        
        if is_animal:
            # For animals
            if "cat" in all_contexts:
                description = f"a {' '.join(found_adjectives)} cat"
            elif "dog" in all_contexts:
                description = f"a {' '.join(found_adjectives)} dog"
            elif "bird" in all_contexts:
                description = f"a {' '.join(found_adjectives)} bird"
            elif "fox" in all_contexts:
                description = f"a {' '.join(found_adjectives)} fox" 
            else:
                description = f"an {' '.join(found_adjectives)} animal"
        elif is_child:
            # For children
            if is_male:
                description = f"a {' '.join(found_adjectives)} young boy"
            elif is_female:
                description = f"a {' '.join(found_adjectives)} young girl"
            else:
                description = f"a {' '.join(found_adjectives)} child"
        elif is_elder:
            # For elderly characters
            if is_male:
                description = f"an {' '.join(found_adjectives)} elderly man"
            elif is_female:
                description = f"an {' '.join(found_adjectives)} elderly woman"
            else:
                description = f"an {' '.join(found_adjectives)} elderly person"
        else:
            # For adults
            if is_male:
                description = f"a {' '.join(found_adjectives)} man"
            elif is_female:
                description = f"a {' '.join(found_adjectives)} woman"
            else:
                description = f"a {' '.join(found_adjectives)} person"
        
        # If no specifics were found, provide a generic but detailed description for consistency
        if not description or description == "a  person" or description == "a  man" or description == "a  woman":
            # Use character name to generate a consistent description (hash function to ensure consistency)
            hash_val = int(hashlib.md5(character.encode()).hexdigest(), 16)
            
            # Generate consistent characteristics based on hash
            hair_colors = ["blonde", "brown", "black", "red", "gray", "white"]
            eye_colors = ["blue", "green", "brown", "hazel", "gray"]
            builds = ["slender", "average", "athletic", "muscular", "heavy-set"]
            
            hair_color = hair_colors[hash_val % len(hair_colors)]
            eye_color = eye_colors[(hash_val // 10) % len(eye_colors)]
            build = builds[(hash_val // 100) % len(builds)]
            
            if is_animal:
                colors = ["white", "black", "brown", "gray", "orange", "spotted", "striped"]
                color = colors[hash_val % len(colors)]
                animal_type = "creature" if not any(a in all_contexts for a in ["cat", "dog", "bird", "fox"]) else next((a for a in ["cat", "dog", "bird", "fox"] if a in all_contexts), "animal")
                description = f"a {color} {animal_type} with distinctive features"
            elif is_male:
                description = f"a {build} man with {hair_color} hair and {eye_color} eyes"
            elif is_female:
                description = f"a {build} woman with {hair_color} hair and {eye_color} eyes"
            else:
                description = f"a person with {hair_color} hair, {eye_color} eyes, and a {build} build"
        
        # Add character-specific consistency details
        description += f". This character should be visually consistent across all images."
        
        character_descriptions[character] = description
    
    print(f"Generated automated descriptions for {len(character_descriptions)} characters:")
    for char, desc in character_descriptions.items():
        print(f"  - {char}: {desc}")
    
    return character_descriptions

def select_art_style():
    '''Let the user select an artistic style for the images.'''
    print("\nAvailable artistic styles:")
    styles = list(ART_STYLES.keys())
    for i, style in enumerate(styles, 1):
        print(f"{i}. {style.replace('_', ' ').title()}")
    
    try:
        choice = input(f"\nSelect a style (1-{len(styles)}) or press Enter for default (storybook): ")
        if not choice.strip():
            selected_style = "storybook"
            print(f"Selected default style: Storybook")
            return ART_STYLES[selected_style]
        
        choice_idx = int(choice) - 1
        if 0 <= choice_idx < len(styles):
            selected_style = styles[choice_idx]
            print(f"Selected style: {selected_style.replace('_', ' ').title()}")
            return ART_STYLES[selected_style]
        else:
            print(f"Invalid selection, using default style: Storybook")
            return ART_STYLES["storybook"]
    except ValueError:
        print(f"Invalid input, using default style: Storybook")
        return ART_STYLES["storybook"]

@perf_monitor
@show_running_message_decorator
def load_story_from_file(filename: str) -> Optional[List[str]]:
    '''Load text lines from a filename.'''
    if not os.path.isfile(filename):
        print(f"Story file '{filename}' not found. Please provide a valid story file.")
        filename = input("Enter the path to the story file: ")
        if not os.path.isfile(filename):
            print("Invalid file path. Please make sure the file exists and try again.")
            return None

    with open(filename, "r") as f:
        story = [line.strip() for line in f if line.strip()]  # Ignore empty lines or lines with only whitespace

    return story

@perf_monitor
@show_running_message_decorator
def generate_and_download_image(prompt: str, output_dir: str, index: int, character_descriptions=None, art_style=None) -> Optional[str]:
    '''Generate an image using OpenAI's DALL-E 2 API with character consistency and selected art style.'''
    try:
        # Extract which characters appear in this line
        characters_in_line = []
        if character_descriptions:
            for character in character_descriptions.keys():
                if re.search(r'\b' + re.escape(character) + r'\b', prompt, re.IGNORECASE):
                    characters_in_line.append(character)
        
        # Create character descriptions for this line
        character_directive = ""
        if characters_in_line:
            character_directive = "The image must include these specific characters with exact descriptions: "
            for character in characters_in_line:
                character_directive += f"{character} as {character_descriptions[character]}, "
            character_directive = character_directive.rstrip(", ") + "."
            
            # Add stronger consistency directive when characters are present
            character_directive += " CRITICALLY IMPORTANT: Maintain EXACT visual consistency for all characters across all scenes. Characters must have identical appearances, proportions, and colors in every image."
        
        # Style directive (use the selected art style or default to storybook)
        style_directive = f"Create the image in {art_style or ART_STYLES['storybook']} style"
        
        # Format directive - DALL-E 2 only supports square images, so we modify our directive
        format_directive = "Use a cinematic composition suitable for storytelling."
        
        # Combine all directives
        enhanced_prompt = f'{prompt} {style_directive}. {character_directive} {format_directive}'
        
        print(f"Enhanced prompt: {enhanced_prompt}")
        
        # Generate an image using DALL-E 2
        response = client.images.generate(
            model="dall-e-2",  # Using DALL-E 2
            prompt=enhanced_prompt,
            size=size_ChatGPT_image,
            n=num_ChatGPT_images,
        )

        # Get image URL from response
        image_url = response.data[0].url if response.data else None
        
        if not image_url:
            logging.error(f"No image URL returned from DALL-E for prompt: {enhanced_prompt}")
            return None

        # Download the image
        img_response = requests.get(image_url)
        if img_response.status_code != 200:
            logging.error(f"Failed to download image. Status Code: {img_response.status_code}")
            return None

        # Adjust the file numbering to start from 1 instead of 0
        adjusted_index = index + 1
        image_file = f"{output_dir}/image_{adjusted_index:04d}.png"
        
        # Save the image
        with open(image_file, "wb") as f:
            f.write(img_response.content)

        logging.info(f"Image successfully saved to: {image_file}")
        return image_file

    except Exception as e:
        logging.error(f"An error occurred while generating an image for '{prompt}': {e}\n{traceback.format_exc()}")
        return None
    @perf_monitor
def select_tts_voice():
    '''Let the user select a TTS voice.'''
    print("\nAvailable OpenAI TTS voices:")
    for i, voice in enumerate(TTS_VOICE_OPTIONS, 1):
        print(f"{i}. {voice}")
    
    try:
        choice = input(f"\nSelect a voice (1-{len(TTS_VOICE_OPTIONS)}) or press Enter for default ({TTS_DEFAULT_VOICE}): ")
        if not choice.strip():
            return TTS_DEFAULT_VOICE
        
        choice_idx = int(choice) - 1
        if 0 <= choice_idx < len(TTS_VOICE_OPTIONS):
            selected_voice = TTS_VOICE_OPTIONS[choice_idx]
            print(f"Selected voice: {selected_voice}")
            return selected_voice
        else:
            print(f"Invalid selection, using default voice: {TTS_DEFAULT_VOICE}")
            return TTS_DEFAULT_VOICE
    except ValueError:
        print(f"Invalid input, using default voice: {TTS_DEFAULT_VOICE}")
        return TTS_DEFAULT_VOICE

@perf_monitor
@show_running_message_decorator
def generate_audio_with_openai_tts(text: str, out_file: str, voice: str = TTS_DEFAULT_VOICE):
    '''Generate audio from text using OpenAI's TTS API.'''
    try:
        # Check if voice is valid
        if voice not in TTS_VOICE_OPTIONS:
            logging.warning(f"Voice '{voice}' not in available options. Using default voice '{TTS_DEFAULT_VOICE}'")
            voice = TTS_DEFAULT_VOICE
        
        # Generate audio using OpenAI TTS
        response = client.audio.speech.create(
            model="tts-1-hd",  # High quality TTS model
            voice=voice,
            input=text
        )
        
        # Save the audio file
        response.stream_to_file(out_file)
        
        # Get the duration using pydub
        audio = AudioSegment.from_file(out_file)
        au_len = math.ceil(audio.duration_seconds)
        
        logging.info(f"Generated audio for: {text} saved as '{out_file}'")
        return out_file, au_len
    
    except Exception as e:
        logging.error(f"An error occurred while generating audio from '{text}': {e}\n{traceback.format_exc()}")
        return None, 0

@perf_monitor
@show_running_message_decorator
def combine_all_audio(audio_files: List[str], output_dir: str) -> str:
    '''Combine audio files into one.'''
    merged_audio_file = f"{output_dir}/merged_audio.mp3"
    merged_audio = AudioSegment.empty()
    for audio_file in audio_files:
        audio_segment = AudioSegment.from_file(audio_file)
        merged_audio += audio_segment
    merged_audio.export(merged_audio_file, format="mp3")
    logging.info(f"Merged audio file: {merged_audio_file}")
    return merged_audio_file

@perf_monitor
def combine_all_subtitles(subtitle_entries: List[dict], output_file: str):
    '''Create subtitle file from subtitles.'''
    subtitles = []
    start_time = datetime.timedelta()
    for index, entry in enumerate(subtitle_entries):
        duration = datetime.timedelta(seconds=entry["duration"])
        end_time = start_time + duration
        content = sanitize_txt(entry['text'])
        subtitle = Subtitle(index, start_time, end_time, content)
        subtitles.append(subtitle)
        start_time = end_time
    with open(output_file, "w") as f:
        f.write(srt.compose(subtitles))
    logging.info(f"Subtitles saved to: {output_file}")
    return output_file

@perf_monitor
def sanitize_txt(content: str) -> str:
    '''Replace illegal characters with spaces and remove extra spaces.'''
    return re.sub(r'[^\w\s]', ' ', content).strip()

@perf_monitor
@show_running_message_decorator
def create_video_with_ffmpeg(images: List[str], merged_audio_path: str, subtitle_file: str, output_file: str):
    '''Create video using FFmpeg directly.'''
    try:
        # Path to FFmpeg executable - try both possible locations
        ffmpeg_exe = r"C:\Users\buggy\Desktop\Story-to-Video-main\ffmpeg\ffmpeg\ffmpeg.exe"
        if not os.path.exists(ffmpeg_exe):
            ffmpeg_exe = "ffmpeg"  # Try using system PATH
        
        print(f"Using FFmpeg from: {ffmpeg_exe}")
        
        # Create a text file listing the images with their durations
        images_list = f"{output_dir}/images_list.txt"
        with open(images_list, 'w') as f:
            for img in images:
                # Make sure path is absolute and uses correct slashes
                abs_path = os.path.abspath(img).replace('\\', '/')
                # Each image gets shown for 3 seconds by default
                f.write(f"file '{abs_path}'\nduration 3\n")
            
            # Add the last image again (required by FFmpeg)
            if images:
                abs_path = os.path.abspath(images[-1]).replace('\\', '/')
                f.write(f"file '{abs_path}'\n")
        
        # First attempt: use concat demuxer with image list
        try:
            print("Attempting video creation with concat demuxer...")
            cmd = [
                ffmpeg_exe,
                "-y",  # Overwrite output if exists
                "-f", "concat",
                "-safe", "0",
                "-i", images_list,
                "-i", merged_audio_path,
                "-c:v", "libx264",
                "-pix_fmt", "yuv420p",
                "-preset", "medium",
                "-crf", "23",
                "-c:a", "aac",
                "-b:a", "192k",
                "-shortest",  # End when shortest input ends
                output_file
            ]
            
            print(f"Running command: {' '.join(cmd)}")
            subprocess.run(cmd, check=True)
            print(f"Successfully created video: {output_file}")
            return True
        except Exception as e:
            print(f"First method failed: {e}. Trying alternative approach...")
            
            # Second attempt: use pattern_type glob
            try:
                print("Attempting with glob pattern...")
                
                # Sort the images alphanumerically to ensure correct order
                pattern = f"{output_dir}/image_*.png"
                
                alt_output = f"{output_dir}/video_alt.mp4"
                cmd_str = f'{ffmpeg_exe} -y -framerate {frame_rate} -pattern_type glob -i "{pattern}" -i "{merged_audio_path}" -c:v libx264 -pix_fmt yuv420p -c:a aac -shortest {alt_output}'
                
                print(f"Running command: {cmd_str}")
                subprocess.run(cmd_str, shell=True)
                print(f"Successfully created video with alternative method: {alt_output}")
                return True
            except Exception as alt_e:
                print(f"Alternative method failed: {alt_e}. Trying final approach...")
                
                # Final attempt: create a basic slideshow with direct image inputs
                try:
                    print("Attempting basic slideshow...")
                    
                    # Join the first 5 images directly (for simplicity)
                    inputs = []
                    for i, img in enumerate(images[:5]):  # Limit to first 5 to avoid command line length issues
                        inputs.extend(["-loop", "1", "-t", "3", "-i", img])
                    
                    final_output = f"{output_dir}/video_final.mp4"
                    cmd = [ffmpeg_exe, "-y"]
                    cmd.extend(inputs)
                    cmd.extend([
                        "-i", merged_audio_path,
                        "-filter_complex", f"concat=n={min(5, len(images))}:v=1:a=0",
                        "-c:v", "libx264", "-pix_fmt", "yuv420p",
                        "-c:a", "aac", "-shortest",
                        final_output
                    ])
                    
                    print(f"Running final command: {' '.join(cmd)}")
                    subprocess.run(cmd)
                    print(f"Created basic slideshow: {final_output}")
                    return True
                except Exception as final_e:
                    print(f"All video creation methods failed: {final_e}")
                    return False
    
    except Exception as e:
        logging.error(f"Error in create_video_with_ffmpeg: {e}\n{traceback.format_exc()}")
        print(f"Error creating video: {e}")
        return False

@perf_monitor
def main(story_file: List[str]):
    # Let user select a TTS voice
    selected_voice = select_tts_voice()
    
    # Let user select an art style
    selected_art_style = select_art_style()
    
    # Automatically analyze story for characters and generate descriptions
    character_descriptions = analyze_story_for_characters(story_file)
    if character_descriptions:
        print(f"Using {len(character_descriptions)} automatically detected characters for consistency.")
    
    images = []
    audio_files = []
    durations = []
    subtitle_entries = []
    start_time = 0
    
    for cnt, line in enumerate(story_file):
        prompt = f'"{line}"'  # Enclose line in quotes for clarity in prompting
        
        # Use the adjusted index for display
        adjusted_cnt = cnt + 1
        print(f"\n{adjusted_cnt} Line {prompt} -> output/image_{adjusted_cnt:04d}.png")
        
        # Generate and download image using DALL-E 2 with character consistency and selected style
        image_file = generate_and_download_image(prompt, output_dir, cnt, character_descriptions, selected_art_style)
        
        if image_file:
            images.append(image_file)
            logging.info(f"Image generated and saved: {image_file}")
        else:
            logging.error(f"Failed to generate image for line: {line}")
            # Use previous image as fallback if available
            if cnt > 0 and len(images) > 0:
                fallback_image = images[-1]
                images.append(fallback_image)
                logging.info(f"Using fallback image: {fallback_image}")
            else:
                # Create a blank image as a last resort
                blank_img = Image.new('RGB', (1024, 1024), color='black')
                fallback_image = f"{output_dir}/blank_{adjusted_cnt:04d}.png"
                blank_img.save(fallback_image)
                images.append(fallback_image)
                logging.info(f"Using blank fallback image: {fallback_image}")
        
        # Adjust audio file numbering
        adjusted_cnt = cnt + 1
        audio_file_name = f"{output_dir}/{adjusted_cnt:04d}_audio.mp3"
        
        # Generate audio using OpenAI TTS
        audio_file, duration = generate_audio_with_openai_tts(line, audio_file_name, selected_voice)
        
        if audio_file:
            audio_files.append(audio_file)
            durations.append(duration)
        else:
            logging.error(f"Failed to generate audio for line: {line}")
            continue  # Skip this iteration if audio generation failed
        
        # Store subtitle entries
        end_time = start_time + duration
        subtitle_entries.append({
            "start": f"{start_time:.3f}",
            "end": f"{end_time:.3f}",
            "text": line,
            "duration": duration
        })
        start_time = end_time
    
    print(f"Total images generated: {len(images)}")
    
    # Create video if we have at least 2 images
    if len(images) >= 2:
        # Create subtitle file
        srt_file = f"{output_dir}/subtitles.srt"
        combine_all_subtitles(subtitle_entries, srt_file)
        
        # Merge audio files
        merged_audio = combine_all_audio(audio_files, output_dir)
        
        # Create video with FFmpeg directly
        youtube_video_path = f"{output_dir}/youtube_video.mp4"
        success = create_video_with_ffmpeg(images, merged_audio, srt_file, youtube_video_path)
        
        if success:
            print(f"Video created successfully in {output_dir} directory.")
        else:
            print("Failed to create video with all methods. Check logs for details.")
    else:
        logging.error("Cannot create video with fewer than 2 images.")

if __name__ == "__main__":
    # Setup the logging file
    name, _ = os.path.splitext(sys.argv[0])
    name += '_.log'
    
    logging.basicConfig(
        level=logging.INFO,
        filename=name,
        filemode='w',
        format='%(asctime)s %(levelname)s %(message)s',
        datefmt='%d-%H:%M:%S'
    )
    
    story_file = load_story_from_file(story_filename)
    if story_file:  # Proceed only if the story file is loaded successfully
        main(story_file)