import os
import sys
import uuid
import re
import shutil
import subprocess
import logging
import wave
import cv2
import numpy as np
from concurrent.futures import ProcessPoolExecutor, as_completed
import tqdm
import multiprocessing
import time

from talkingface.audio_model import AudioModel
from talkingface.render_model import RenderModel
from piper.voice import PiperVoice

# =============================================================
#                      Setup Logging
# =============================================================
logging.basicConfig(
    level=logging.INFO,  # Set to INFO for general logs
    format='%(asctime)s [%(levelname)s] %(message)s',
    handlers=[
        logging.FileHandler("processing_optimized.log"),
        logging.StreamHandler(sys.stdout)
    ]
)

# =============================================================
#                      Helper Functions
# =============================================================

def preprocess_text(text):
    """
    Preprocess the input text by removing specific patterns and non-ASCII characters.
    """
    logging.info("Original Text: %s", text)
    text = re.sub(r'~+', '!', text)
    text = re.sub(r"\(.*?\)", "", text)
    text = re.sub(r"(\*[^*]+\*)|(_[^_]+_)", "", text).strip()
    text = re.sub(r'[^\x00-\x7F]+', '', text)
    logging.info("Preprocessed Text: %s", text)
    return text

def synthesize_audio(voice, text, output_wav_path):
    """
    Synthesize speech from text and save it to a WAV file.
    """
    try:
        with wave.open(output_wav_path, 'w') as wav_file:
            voice.synthesize(preprocess_text(text), wav_file)
        logging.info(f"Audio synthesized and saved to {output_wav_path}")
    except Exception as e:
        logging.error(f"Audio synthesis failed: {e}")
        sys.exit(1)

def convert_audio(input_wav, output_wav):
    """
    Convert audio to the desired format using FFmpeg.
    """
    try:
        subprocess.run([
            "ffmpeg", "-y",  # Overwrite output files without asking
            "-i", input_wav,  # Input file
            "-ar", "16000",    # Set audio sampling rate to 16000 Hz
            output_wav          # Output file
        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
        logging.info(f"Audio converted and saved to {output_wav}")
    except subprocess.CalledProcessError as e:
        logging.error(f"Audio conversion failed: {e.stderr.decode().strip()}")
        sys.exit(1)

def merge_audio_video(save_path, wavpath, output_video_name):
    """
    Merge processed video and audio into the final output video using FFmpeg.
    """
    try:
        subprocess.run([
            "ffmpeg", "-y",  # Overwrite output files without asking
            "-i", save_path,  # Input video
            "-i", wavpath,    # Input audio
            "-c:v", "libx264",  # Video codec
            "-pix_fmt", "yuv420p",  # Pixel format
            "-c:a", "aac",         # Audio codec
            "-strict", "experimental",  # Allow experimental codecs
            output_video_name      # Output file
        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
        logging.info(f"Audio and video merged into {output_video_name}")
    except subprocess.CalledProcessError as e:
        logging.error(f"Merging audio and video failed: {e.stderr.decode().strip()}")
        sys.exit(1)

def process_single_frame(args):
    """
    Process a single video frame using the RenderModel.
    This function is intended to be used with multiprocessing.
    """
    frame, render_model_path = args
    try:
        # Initialize RenderModel within the subprocess
        renderModel = RenderModel()
        renderModel.loadModel(render_model_path)
        processed_frame = renderModel.interface(frame)
        if processed_frame is None:
            logging.warning("Processed frame is None. Using a blank frame.")
            processed_frame = np.ones_like(frame) * 255  # White frame as placeholder
        return processed_frame
    except Exception as e:
        logging.error(f"Error processing frame: {e}")
        return np.ones_like(frame) * 255  # White frame as placeholder

def read_video_properties(video_path):
    """
    Read video properties such as width and height.
    """
    cap = cv2.VideoCapture(video_path)
    if not cap.isOpened():
        logging.error(f"Unable to open video: {video_path}")
        sys.exit(1)
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    cap.release()
    logging.info(f"Video properties - Width: {width}, Height: {height}")
    return width, height

# =============================================================
#                          Main Function
# =============================================================

def main():
    # Set the multiprocessing start method to 'spawn' to avoid CUDA errors
    try:
        multiprocessing.set_start_method('spawn', force=True)
        logging.info("Multiprocessing start method set to 'spawn'.")
    except RuntimeError as e:
        # Start method has already been set
        logging.warning(f"Could not set start method to 'spawn': {e}")

    # Initialize Models within the main process
    try:
        model = "en_US-john-medium.onnx"
        voice = PiperVoice.load(model)
        logging.info("PiperVoice model loaded successfully.")
    except Exception as e:
        logging.error(f"Failed to load PiperVoice model: {e}")
        sys.exit(1)

    try:
        audioModel = AudioModel()
        audioModel.loadModel("checkpoint/audio.pkl")
        logging.info("AudioModel loaded successfully.")
    except Exception as e:
        logging.error(f"Failed to load AudioModel: {e}")
        sys.exit(1)

    try:
        renderModel = RenderModel()
        renderModel.loadModel("checkpoint/render.pth")
        logging.info("RenderModel loaded successfully.")
    except Exception as e:
        logging.error(f"Failed to load RenderModel: {e}")
        sys.exit(1)

    print("=====================")

    # Define paths and filenames
    filename = 'temp.wav'
    text = '''Gunman lurked for hours before Trump's last-minute game of golf
4 hours ago
Madeline Halpert
BBC News
Reporting from
West Palm Beach, Florida
Bodycam footage shows arrest of suspected Trump gunman

”'''

    # Synthesize audio
    synthesize_audio(voice, text, filename)

    # Get command-line arguments
    try:
        video_path_input = sys.argv[1]
        print(f"Video path is set to: {video_path_input}")
        audio_path = filename
        print(f"Audio path is set to: {audio_path}")
        output_video_name = sys.argv[3]
        print(f"Output video name is set to: {output_video_name}")
    except IndexError:
        logging.error("Insufficient command-line arguments provided.")
        logging.error("Usage: python demo.py <video_input_path> <unused_arg> <output_video_name>")
        sys.exit(1)

    # Define paths
    pkl_path = os.path.join(video_path_input, "keypoint_rotate.pkl")
    video_path_processed = os.path.join(video_path_input, "circle.mp4")

    if not os.path.exists(pkl_path):
        logging.error(f"Keypoint pickle file not found at {pkl_path}")
        sys.exit(1)

    if not os.path.exists(video_path_processed):
        logging.error(f"Processed video file not found at {video_path_processed}")
        sys.exit(1)

    # Reset character in RenderModel within the main process
    try:
        renderModel.reset_charactor(video_path_processed, pkl_path)
        logging.info("RenderModel.reset_charactor completed successfully.")
    except Exception as e:
        logging.error(f"Failed to reset character: {e}")
        sys.exit(1)

    # Convert audio using subprocess.run
    wavpath = 'output_audio1.wav'
    try:
        subprocess.run([
            "ffmpeg", "-y",  # Overwrite output files without asking
            "-i", audio_path,  # Input file
            "-ar", "16000",    # Set audio sampling rate to 16000 Hz
            wavpath            # Output file
        ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
        logging.info(f"Audio converted and saved to {wavpath}")
    except subprocess.CalledProcessError as e:
        logging.error(f"Audio conversion failed: {e.stderr.decode().strip()}")
        sys.exit(1)

    # Generate mouth frames from audio
    try:
        mouth_frame = audioModel.interface_wav(wavpath)

        # Proper validation to avoid ambiguous truth value error
        if isinstance(mouth_frame, np.ndarray):
            if mouth_frame.size == 0:
                logging.error("Mouth frame generation returned no frames.")
                sys.exit(1)
        elif isinstance(mouth_frame, (list, tuple)):
            if len(mouth_frame) == 0:
                logging.error("Mouth frame generation returned no frames.")
                sys.exit(1)
        else:
            logging.error("Unexpected type for mouth_frame. It should be a list, tuple, or NumPy array.")
            sys.exit(1)

        logging.info(f"Generated {len(mouth_frame)} mouth frames from audio.")
    except Exception as e:
        logging.error(f"Failed to generate mouth frames: {e}")
        sys.exit(1)

    # Read video properties
    vid_width, vid_height = read_video_properties(video_path_processed)

    # Prepare output directory and VideoWriter
    task_id = str(uuid.uuid1())
    output_dir = os.path.join("output", task_id)
    os.makedirs(output_dir, exist_ok=True)
    save_path = os.path.join(output_dir, "silence.mp4")
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')

    try:
        videoWriter = cv2.VideoWriter(save_path, fourcc, 25, (vid_width, vid_height))
        if not videoWriter.isOpened():
            logging.error(f"Failed to initialize VideoWriter with path: {save_path}")
            sys.exit(1)
        logging.info(f"VideoWriter initialized with save path: {save_path}")
    except Exception as e:
        logging.error(f"Failed to initialize VideoWriter: {e}")
        sys.exit(1)

    # Prepare arguments for each frame
    frames_with_path = [(frame, "checkpoint/render.pth") for frame in mouth_frame]

    # Process frames in parallel using ProcessPoolExecutor
    start_time = time.time()

    try:
        max_workers = min(8, os.cpu_count() or 4)  # Limit to 8 workers or number of CPUs
        logging.info(f"Starting frame processing with {max_workers} workers.")

        with ProcessPoolExecutor(max_workers=max_workers) as executor:
            # Submit all frame processing tasks
            futures = {executor.submit(process_single_frame, args): idx for idx, args in enumerate(frames_with_path)}

            for future in tqdm.tqdm(as_completed(futures), total=len(futures), desc="Processing Frames"):
                idx = futures[future]
                try:
                    processed_frame = future.result()
                    videoWriter.write(processed_frame)
                except Exception as e:
                    logging.error(f"Error processing frame {idx}: {e}")
                    # Write a blank frame in case of error to maintain video consistency
                    blank_frame = np.ones((vid_height, vid_width, 3), dtype=np.uint8) * 255
                    videoWriter.write(blank_frame)
    except Exception as e:
        logging.error(f"Failed during frame processing: {e}")
        videoWriter.release()
        shutil.rmtree(output_dir)
        sys.exit(1)

    videoWriter.release()
    elapsed_time = time.time() - start_time
    logging.info(f"Frame processing completed in {elapsed_time:.2f} seconds.")

    # Merge audio and video
    try:
        merge_audio_video(save_path, wavpath, output_video_name)
    except Exception as e:
        logging.error(f"Failed to merge audio and video: {e}")
        shutil.rmtree(output_dir)
        sys.exit(1)

    # Clean up temporary files and directories
    try:
        shutil.rmtree(output_dir)
        logging.info(f"Cleaned up temporary directory: {output_dir}")
    except Exception as e:
        logging.warning(f"Failed to remove temporary directory {output_dir}: {e}")

    # Remove temporary audio files
    try:
        os.remove(wavpath)
        os.remove(audio_path)
        logging.info("Temporary audio files removed.")
    except Exception as e:
        logging.warning(f"Failed to remove temporary audio files: {e}")

    logging.info(f"Output video saved to {os.path.abspath(output_video_name)}")

# =============================================================
#                        Entry Point
# =============================================================

if __name__ == "__main__":
    main()  Processing Frames:  11%|██████████▎                                                                                   | 37/336 [00:03<00:10, 27.20it/s]2024-10-07 10:58:09,074 [ERROR] Error processing frame: 'NoneType' object has no attribute 'get'
2024-10-07 10:58:09,107 [ERROR] Error processing frame: 'NoneType' object has no attribute 'get'
2024-10-07 10:58:09,115 [ERROR] Error processing frame: 'NoneType' object has no attribute 'get'
2024-10-07 10:58:09,127 [ERROR] Error processing frame: 'NoneType' object has no attribute 'get'
2024-10-07 10:58:09,133 [ERROR] Error processing frame: 'NoneType' object has no attribute 'get'  不要视频多线程