chat quero que quando executa esse programa ele faça um host html para fazer uma interface com varios dados e aonde eu vou controlar tudo e tudo que eu clicar pode ser executado no script python no pc normalmente o html será somente a interface por onde vou controlar tudo como pode ser feito. import os
import time
import threading
import random
import subprocess
import re
import shutil
import pickle
import sqlite3
from datetime import datetime, timedelta, timezone
from tkinter import (
    Tk, Frame, Button, Text, Entry, Label, messagebox, END, filedialog, simpledialog,
    Scrollbar, Listbox, VERTICAL, SINGLE, RIGHT, Y, LEFT, BOTH, StringVar, OptionMenu
)
import tkinter as tk
from tkinter.ttk import Notebook, Progressbar, Combobox, Style
import pyperclip
import cv2
from pydub import AudioSegment
from pydub.silence import detect_nonsilent
import numpy as np
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
import pytz


# ==== CONFIGURAÇÕES ====
DB_PATH = r"C:\Users\jonathan\Desktop\FINANCE CHANEEL\monitorador_juntador.db"
HORARIOS_DIA = [5, 6, 7, 8]
DEFAULT_DESCRIPTION = """
In this video, we reveal the shocking truth behind Warren Buffett’s boldest move ever — and what it could mean for the future of the market. Is a crash on the horizon? Don’t miss these crucial insights!
⏱️ Chapters
0:00 - Introduction
0:45 - What Just Happened
2:10 - Deep Dive into the Situation
3:00 - Final Thoughts and Predictions
8:45 - Outro

📢 Subscribe now for more eye-opening financial insights!
#finance #investing #stockmarket #breakingnews #wealth
"""
DEFAULT_TAGS = [
    "Warren Buffett sells biggest stock",
    "Buffett stock sale 2025",
    "Wall Street reaction Buffett",
    "Warren Buffett stock market news",
    "Buffett biggest investment sold",
    "Buffett stock dump explained",
    "Warren Buffett investing",
    "stock market news 2025",
    "Buffett sell off impact",
    "Warren Buffett market strategy",
    "investment advice Buffett",
    "Buffett latest moves",
    "Wall Street freak out Buffett",
    "trump",
    "trump tarrifs"
]

PASTA_CATEGORIAS = r"C:\Users\jonathan\Desktop\AUTLAND\CATEGORIAS DE IMAGENS VIDEO"
AUDIO_FUNDO = r"C:\Users\jonathan\Desktop\AUTLAND\TRILHA SONORA\soundfund.mp3"
PASTA_DESTINO_VIDEOS = r"C:\Users\jonathan\Desktop\FINANCE CHANEEL\VIDEO PARA POSTAR PRONTOS"


# ======== BANCO DE DADOS ========
def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS videos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            filename TEXT UNIQUE,
            title TEXT,
            channel TEXT,
            schedule_time TEXT
        )
    ''')
    c.execute('''
        CREATE TABLE IF NOT EXISTS perfis (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nome TEXT UNIQUE,
            credentials BLOB,
            canal TEXT
        )
    ''')
    conn.commit()
    conn.close()


def get_perfis():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT id, nome, canal FROM perfis")
    perfis = c.fetchall()
    conn.close()
    return perfis


def add_perfil(nome, credentials_blob, canal):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("INSERT OR REPLACE INTO perfis (nome, credentials, canal) VALUES (?, ?, ?)", (nome, credentials_blob, canal))
    conn.commit()
    conn.close()


def delete_perfil(nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("DELETE FROM perfis WHERE nome=?", (nome,))
    conn.commit()
    conn.close()


def get_canal_por_perfil(nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT canal FROM perfis WHERE nome=?", (nome,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None


def get_title_from_db(filename):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT title FROM videos WHERE filename=?", (filename,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None


def set_video_entry(filename, title, channel, schedule_time):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        INSERT OR REPLACE INTO videos (filename, title, channel, schedule_time)
        VALUES (?, ?, ?, ?)
    ''', (filename, title, channel, schedule_time))
    conn.commit()
    conn.close()


def get_scheduled_times_for_channel(channel):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT schedule_time FROM videos WHERE channel=?", (channel,))
    horarios = [row[0] for row in c.fetchall()]
    conn.close()
    return horarios


def load_credentials_from_blob(blob):
    filename_temp = "temp_creds.pickle"
    with open(filename_temp, "wb") as f:
        f.write(blob)
    with open(filename_temp, "rb") as f:
        creds = pickle.load(f)
    os.remove(filename_temp)
    return creds


def get_cred_blob(profile_nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT credentials FROM perfis WHERE nome=?", (profile_nome,))
    row = c.fetchone()
    conn.close()
    if row:
        return row[0]
    else:
        raise Exception("Perfil não encontrado")


def upload_video_to_youtube(credentials_blob, title, description, tags, video_path, thumbnail_path, publish_now, publish_at=None, category_id="22"):
    creds = load_credentials_from_blob(credentials_blob)
    youtube = build("youtube", "v3", credentials=creds)

    if not os.path.exists(video_path):
        raise FileNotFoundError(f"Arquivo não encontrado: {video_path}")

    if publish_now:
        privacy_status = "public"
        publish_at_utc = None
    else:
        if not publish_at:
            raise ValueError("publishAt deve ser informado para agendamento")
        dt_local = datetime.fromisoformat(publish_at)
        now_local = datetime.now(dt_local.tzinfo)
        if dt_local <= now_local:
            raise ValueError(f"publishAt ({dt_local.isoformat()}) deve ser no futuro em relação a agora ({now_local.isoformat()})")
        publish_at_utc = dt_local.astimezone(timezone.utc).isoformat()
        privacy_status = "private"

    body = {
        "snippet": {
            "title": title,
            "description": description,
            "tags": tags,
            "categoryId": category_id
        },
        "status": {
            "privacyStatus": privacy_status,
            "publishAt": publish_at_utc
        }
    }

    media = MediaFileUpload(video_path, mimetype="video/mp4", resumable=True, chunksize=10 * 1024 * 1024)

    request = youtube.videos().insert(
        part="snippet,status",
        body=body,
        media_body=media
    )

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

    if thumbnail_path and os.path.exists(thumbnail_path):
        youtube.thumbnails().set(
            videoId=response["id"],
            media_body=MediaFileUpload(thumbnail_path)
        ).execute()

    print(f"[INFO] Upload finalizado, videoId: {response['id']}")
    return response["id"]


def get_next_available_time(profile_name, start_dt=None, num_slots=1):
    canal = get_canal_por_perfil(profile_name)
    horarios_ocupados = get_scheduled_times_for_channel(canal)

    br_tz = pytz.timezone("America/Sao_Paulo")
    agora = datetime.now(br_tz)
    if start_dt:
        if isinstance(start_dt, str):
            try:
                dt = datetime.fromisoformat(start_dt)
                if dt.tzinfo is None:
                    dt = br_tz.localize(dt)
                start_dt = dt
            except Exception:
                start_dt = None
        elif isinstance(start_dt, datetime):
            if start_dt.tzinfo is None:
                start_dt = br_tz.localize(start_dt)
    if start_dt is None or start_dt <= agora:
        start_dt = agora + timedelta(minutes=1)
        start_dt = start_dt.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)

    parsed_ocupados = []
    for h in horarios_ocupados:
        try:
            dt = datetime.fromisoformat(h)
            if dt.tzinfo is None:
                dt = br_tz.localize(dt)
        except Exception:
            continue
        parsed_ocupados.append(dt)

    scheduled_slots = []
    candidate_time = start_dt
    limit_date = agora + timedelta(days=30)

    while len(scheduled_slots) < num_slots and candidate_time <= limit_date:
        conflict = False
        for ocupado in parsed_ocupados:
            if abs((candidate_time - ocupado).total_seconds()) < 60:
                conflict = True
                break
        if not conflict:
            scheduled_slots.append(candidate_time)
            candidate_time += timedelta(hours=1)
        else:
            candidate_time += timedelta(hours=1)

    if len(scheduled_slots) < num_slots:
        raise Exception("Não foi possível agendar todos os vídeos nos próximos 30 dias devido a conflitos de horário.")

    return [slot.isoformat() for slot in scheduled_slots]


def processar_envio(profile_nome, video_path, description, tags, thumbnail_path, publicar_agora=False,
                   notify=True, category_id="22", start_schedule_dt=None, qtd_videos=1, progress_callback=None):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT credentials, canal FROM perfis WHERE nome=?", (profile_nome,))
    row = c.fetchone()
    conn.close()

    if not row:
        if notify:
            messagebox.showerror("Erro", "Perfil não encontrado")
        return False

    credentials_blob, canal = row

    video_paths = []
    thumbnail_paths = []

    if isinstance(video_path, str):
        video_paths = [video_path] * qtd_videos
    elif isinstance(video_path, (list, tuple)):
        if len(video_path) < qtd_videos:
            if notify:
                messagebox.showerror("Erro", "Quantidade de vídeos insuficiente para a quantidade solicitada")
            return False
        video_paths = video_path[:qtd_videos]
    else:
        if notify:
            messagebox.showerror("Erro", "video_path inválido")
        return False

    if isinstance(thumbnail_path, str):
        thumbnail_paths = [thumbnail_path] * qtd_videos
    elif isinstance(thumbnail_path, (list, tuple)):
        if len(thumbnail_path) < qtd_videos:
            extended = thumbnail_path * ((qtd_videos // len(thumbnail_path)) + 1)
            thumbnail_paths = extended[:qtd_videos]
        else:
            thumbnail_paths = thumbnail_path[:qtd_videos]
    else:
        thumbnail_paths = [None] * qtd_videos

    if publicar_agora:
        schedule_times = [None] * qtd_videos
        publish_now_flags = [True] * qtd_videos
    else:
        try:
            schedule_times = get_next_available_time(profile_nome, start_dt=start_schedule_dt, num_slots=qtd_videos)
        except Exception as e:
            if notify:
                messagebox.showerror("Erro", f"Falha ao calcular horários de agendamento: {e}")
            return False
        publish_now_flags = [False] * qtd_videos

    enviados = 0
    total = qtd_videos

    br_tz = pytz.timezone("America/Sao_Paulo")

    for idx in range(qtd_videos):
        video_file = video_paths[idx]
        thumb_file = thumbnail_paths[idx]
        publish_now = publish_now_flags[idx]
        publish_at = schedule_times[idx]

        filename_base = os.path.basename(video_file)
        filename_sem_ext = os.path.splitext(filename_base)[0]

        titulo_bd = get_title_from_db(filename_sem_ext)
        titulo_final = titulo_bd or filename_sem_ext

        try:
            vid_id = upload_video_to_youtube(
                credentials_blob,
                titulo_final,
                description,
                tags,
                video_file,
                thumb_file,
                publish_now,
                publish_at,
                category_id=category_id
            )
            set_video_entry(filename_sem_ext, titulo_final, canal,
                            publish_at if publish_at else datetime.now(br_tz).isoformat())
            enviados += 1
            if progress_callback:
                progress_callback(enviados, total)
        except Exception as e:
            if notify:
                messagebox.showerror("Erro", f"Falha no upload do vídeo {filename_base}: {e}")
            return False

    if notify:
        if publicar_agora:
            messagebox.showinfo("Sucesso", f"{enviados} vídeo(s) enviado(s) com sucesso.")
        else:
            preview_text = "Vídeos agendados:\n"
            for i in range(enviados):
                hum_time = datetime.fromisoformat(schedule_times[i]).strftime("%d/%m %H:%M")
                preview_text += f"Vídeo {i + 1}: {hum_time}\n"
            messagebox.showinfo("Sucesso", preview_text)
    return True


# ===== CRIADOR DE VÍDEO COM OPENCV E Pydub =====

def zoom_effect(image, progress, zoom_in=True, direction="center"):
    h, w = image.shape[:2]
    zoom_strength = 0.05
    scale = 1 + (zoom_strength * progress) if zoom_in else 1 + (zoom_strength * (1 - progress))
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    if direction == "top":
        x, y = (new_w - w) // 2, 0
    elif direction == "bottom":
        x, y = (new_w - w) // 2, new_h - h
    elif direction == "left":
        x, y = 0, (new_h - h) // 2
    elif direction == "right":
        x, y = new_w - w, (new_h - h) // 2
    elif direction == "top_left":
        x, y = 0, 0
    elif direction == "top_right":
        x, y = new_w - w, 0
    elif direction == "bottom_left":
        x, y = 0, new_h - h
    elif direction == "bottom_right":
        x, y = new_w - w, new_h - h
    else:
        x, y = (new_w - w) // 2, (new_h - h) // 2
    x = max(min(x, new_w - w), 0)
    y = max(min(y, new_h - h), 0)
    cropped = resized[y:y + h, x:x + w]
    return cropped

def move_effect(image, progress, direction="left_to_right"):
    h, w = image.shape[:2]
    dx = int(w * 0.05 * progress)
    dy = int(h * 0.05 * progress)
    if direction == "left_to_right":
        M = np.float32([[1, 0, dx], [0, 1, 0]])
    elif direction == "right_to_left":
        M = np.float32([[1, 0, -dx], [0, 1, 0]])
    elif direction == "top_to_bottom":
        M = np.float32([[1, 0, 0], [0, 1, dy]])
    elif direction == "bottom_to_top":
        M = np.float32([[1, 0, 0], [0, 1, -dy]])
    else:
        return image
    moved = cv2.warpAffine(image, M, (w, h))
    return moved

def ken_burns_effect(image, progress, zoom_in=True, direction="left_to_right"):
    h, w = image.shape[:2]
    zoom_strength = 0.1
    scale = 1 + (zoom_strength * progress) if zoom_in else 1 + (zoom_strength * (1 - progress))
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    dx = int((new_w - w) * progress)
    dy = int((new_h - h) * progress)
    if direction == "left_to_right":
        x, y = dx, 0
    elif direction == "right_to_left":
        x, y = (new_w - w) - dx, 0
    elif direction == "top_to_bottom":
        x, y = 0, dy
    elif direction == "bottom_to_top":
        x, y = 0, (new_h - h) - dy
    elif direction == "top_left_to_bottom_right":
        x, y = dx, dy
    elif direction == "bottom_right_to_top_left":
        x, y = (new_w - w) - dx, (new_h - h) - dy
    else:
        x, y = (new_w - w) // 2, (new_h - h) // 2
    x = max(min(x, new_w - w), 0)
    y = max(min(y, new_h - h), 0)
    cropped = resized[y:y + h, x:x + w]
    return cropped

def fast_zoom_blur_effect(image, progress):
    h, w = image.shape[:2]
    scale = 1 + 0.4 * progress
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    x = (new_w - w) // 2
    y = (new_h - h) // 2
    zoomed = resized[y:y + h, x:x + w]
    blur_amount = int(progress * 7) * 2 + 1
    if blur_amount % 2 == 0:
        blur_amount += 1
    if blur_amount > 1:
        zoomed = cv2.GaussianBlur(zoomed, (blur_amount, blur_amount), 0)
    return zoomed

def morph_transition_effect(img1, img2, alpha):
    h, w = img1.shape[:2]
    img1 = cv2.resize(img1, (w, h))
    img2 = cv2.resize(img2, (w, h))
    blended = cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
    return blended

def slide_effect(image, progress, direction="left_to_right"):
    h, w = image.shape[:2]
    dx = int(w * progress)
    dy = int(h * progress)
    if direction == "left_to_right":
        M = np.float32([[1, 0, dx], [0, 1, 0]])
    elif direction == "right_to_left":
        M = np.float32([[1, 0, -dx], [0, 1, 0]])
    elif direction == "top_to_bottom":
        M = np.float32([[1, 0, 0], [0, 1, dy]])
    elif direction == "bottom_to_top":
        M = np.float32([[1, 0, 0], [0, 1, -dy]])
    else:
        return image
    moved = cv2.warpAffine(image, M, (w, h))
    return moved

def gallery_slide_effect(image, progress):
    h, w = image.shape[:2]
    dx = int(w * 0.3 * progress)
    M = np.float32([[1, 0, dx], [0, 1, 0]])
    shifted = cv2.warpAffine(image, M, (w, h))
    fade_width = int(w * 0.1)
    mask = np.ones((h, w), dtype=np.float32)
    for x_ in range(fade_width):
        alpha = x_ / fade_width
        mask[:, x_] *= alpha
        mask[:, w - 1 - x_] *= alpha
    if shifted.ndim == 3:
        for c in range(3):
            shifted[:, :, c] = shifted[:, :, c].astype(np.float32) * mask
        shifted = shifted.astype(np.uint8)
    else:
        shifted = (shifted.astype(np.float32) * mask).astype(np.uint8)
    return shifted

def load_images_from_folder(folder):
    images = []
    for filename in os.listdir(folder):
        if filename.lower().endswith((".png", ".jpg", ".jpeg")):
            path = os.path.join(folder, filename)
            img = cv2.imread(path)
            if img is not None and img.shape[0] > 0 and img.shape[1] > 0:
                images.append(img)
    random.shuffle(images)
    return images

def get_beat_times(audio_path, frame_ms=50, energy_threshold_factor=1.3):
    audio = AudioSegment.from_file(audio_path).set_channels(1)
    frame_length = frame_ms
    n_frames = len(audio) // frame_length
    energies = []
    for i in range(n_frames):
        frame = audio[i * frame_length: (i + 1) * frame_length]
        energies.append(frame.rms)
    energies = np.array(energies)
    avg_energy = np.mean(energies)
    beat_indices = []
    for i in range(1, len(energies) - 1):
        if energies[i] > energy_threshold_factor * avg_energy and energies[i] > energies[i - 1] and energies[i] > energies[i + 1]:
            beat_indices.append(i)
    beat_times = [i * frame_length / 1000.0 for i in beat_indices]
    filtered_beats = []
    min_separation = 1.0
    for t in beat_times:
        if len(filtered_beats) == 0 or (t - filtered_beats[-1]) > min_separation:
            filtered_beats.append(t)
    return filtered_beats

def remover_silencio(audio_segment, silence_thresh=-50, min_silence_len=500, padding=300):
    nao_silencioso = detect_nonsilent(audio_segment, min_silence_len=min_silence_len, silence_thresh=silence_thresh)
    if not nao_silencioso:
        return audio_segment
    partes = []
    for start, end in nao_silencioso:
        start_pad = max(0, start - padding)
        end_pad = min(len(audio_segment), end + padding)
        partes.append(audio_segment[start_pad:end_pad])
    audio_sem_silencio = partes[0]
    for part in partes[1:]:
        audio_sem_silencio += AudioSegment.silent(duration=150) + part
    return audio_sem_silencio

def create_video_only(video_path, duration, images, fps=30, progress_callback=None, change_times=None):
    if not images:
        raise ValueError("Lista de imagens vazia.")
    height, width, _ = images[0].shape
    total_frames = int(duration * fps)
    out = cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
    img_index = 0
    efeitos = {
        "zoom": 0.15, "move": 0.15, "ken_burns": 0.15, "fast_zoom_blur": 0.10,
        "morph_transition": 0.10, "slide": 0.10, "gallery_slide": 0.10, "static": 0.15,
    }
    efeito_usado_count = {key: 0 for key in efeitos.keys()}
    def escolher_efeito(efeitos, efeito_usado_count):
        chances_ajustadas = {}
        for e, chance in efeitos.items():
            mult = 0.8 ** efeito_usado_count[e]
            chances_ajustadas[e] = chance * mult
        total = sum(chances_ajustadas.values())
        probs = [v / total for v in chances_ajustadas.values()]
        escolha = random.choices(list(chances_ajustadas.keys()), weights=probs, k=1)[0]
        efeito_usado_count[escolha] += 1
        return escolha
    morph_duration_frames = int(fps * 0.5)
    rapid_change_count = 0
    rapid_change_max = 7
    rapid_change_interval_frames = fps
    morph_active = False
    morph_frame = 0
    morph_from_img = None
    morph_to_img = None
    frames_per_image = int(3 * fps)
    if change_times is None or len(change_times) == 0:
        def random_interval_frames():
            return int(random.uniform(2.5, 4.0) * fps)
        frames_per_image = random_interval_frames()
        next_change_frame = frames_per_image
    else:
        change_frames = [int(t * fps) for t in change_times if t * fps < total_frames]
        if change_frames[-1] < total_frames - 1:
            change_frames.append(total_frames - 1)
        change_index = 0
        next_change_frame = change_frames[change_index]
    start_time = time.time()
    current_effect = None
    current_direction_zoom = None
    current_direction_move = None
    current_direction_ken = None
    current_direction_slide = None
    current_direction_static = None
    for i in range(total_frames):
        if rapid_change_count < rapid_change_max:
            if i % rapid_change_interval_frames == 0 and i != 0:
                img_index = (img_index + 1) % len(images)
                rapid_change_count += 1
                current_effect = None
                current_direction_zoom = None
                current_direction_move = None
                current_direction_ken = None
                current_direction_slide = None
                current_direction_static = None
        else:
            if change_times is None or len(change_times) == 0:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    frames_per_image = random_interval_frames()
                    next_change_frame = i + frames_per_image
                    current_effect = None
                    current_direction_zoom = None
                    current_direction_move = None
                    current_direction_ken = None
                    current_direction_slide = None
                    current_direction_static = None
            else:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    change_index += 1
                    if change_index < len(change_frames):
                        next_change_frame = change_frames[change_index]
                    else:
                        next_change_frame = total_frames + 1
                    current_effect = None
                    current_direction_zoom = None
                    current_direction_move = None
                    current_direction_ken = None
                    current_direction_slide = None
                    current_direction_static = None
        img = images[img_index]
        if change_times is not None and len(change_times) > 0 and i <= next_change_frame:
            prev_frame_change = change_frames[change_index - 1] if change_index > 0 else 0
            frame_in_image = i - prev_frame_change
            total_frames_for_image = next_change_frame - prev_frame_change
        else:
            total_frames_for_image = frames_per_image
            frame_in_image = i % frames_per_image
        progress = frame_in_image / total_frames_for_image if total_frames_for_image > 0 else 0
        progress = min(max(progress, 0), 1)
        if morph_active:
            if morph_frame < morph_duration_frames:
                frame = morph_transition_effect(morph_from_img, morph_to_img, morph_frame / morph_duration_frames)
                morph_frame += 1
                frame = move_effect(frame, progress, direction="left_to_right")
                frame = cv2.resize(frame, (width, height))
                out.write(frame)
                if progress_callback:
                    elapsed = time.time() - start_time
                    frames_done = i + 1
                    frames_left = total_frames - frames_done
                    eta = (elapsed / frames_done) * frames_left if frames_done > 0 else 0
                    progress_callback(frames_done, total_frames, eta)
                continue
            else:
                morph_active = False
                morph_frame = 0
                current_effect = None
        aplicar_morph = False
        if frame_in_image == 0 and random.random() < 0.2 and len(images) > 1:
            morph_active = True
            morph_frame = 0
            morph_from_img = img
            prox_img_index = (img_index + 1) % len(images)
            morph_to_img = images[prox_img_index]
            aplicar_morph = True
        if aplicar_morph:
            continue
        if current_effect is None:
            current_effect = escolher_efeito(efeitos, efeito_usado_count)
        if current_effect == "zoom":
            if current_direction_zoom is None:
                current_direction_zoom = random.choice(["center", "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right"])
            frame_effect = zoom_effect(img, progress, zoom_in=True, direction=current_direction_zoom)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "move":
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(img, progress, direction=current_direction_move)
        elif current_effect == "ken_burns":
            if current_direction_ken is None:
                current_direction_ken = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top", "top_left_to_bottom_right", "bottom_right_to_top_left"])
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame_effect = ken_burns_effect(img, progress, zoom_in=True, direction=current_direction_ken)
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "fast_zoom_blur":
            frame_effect = fast_zoom_blur_effect(img, progress)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "slide":
            if current_direction_slide is None:
                current_direction_slide = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame_effect = slide_effect(img, progress, direction=current_direction_slide)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "gallery_slide":
            frame_effect = gallery_slide_effect(img, progress)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "static":
            if current_direction_move is None:
                current_direction_move = "left_to_right"
            frame = move_effect(img, progress, direction=current_direction_move)
        else:
            frame = img.copy()
        frame = cv2.resize(frame, (width, height))
        out.write(frame)
        if progress_callback:
            elapsed = time.time() - start_time
            frames_done = i + 1
            frames_left = total_frames - frames_done
            eta = (elapsed / frames_done) * frames_left if frames_done > 0 else 0
            progress_callback(frames_done, total_frames, eta)
    out.release()

def merge_audio_with_video(video_path, audio_path, output_path):
    audio1 = AudioSegment.from_file(audio_path).set_channels(2)
    audio2 = AudioSegment.from_file(AUDIO_FUNDO).set_channels(2)
    audio2 = audio2 - 1
    if len(audio2) < len(audio1):
        audio2 = audio2 * (len(audio1) // len(audio2) + 1)
    audio2 = audio2[:len(audio1)]
    combined = audio1.overlay(audio2)
    temp_audio_path = "combined_temp_audio.mp3"
    combined.export(temp_audio_path, format="mp3")
    command = [
        "ffmpeg",
        "-y",
        "-i", video_path,
        "-i", temp_audio_path,
        "-c:v", "copy",
        "-c:a", "aac",
        "-shortest",
        output_path
    ]
    subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    os.remove(temp_audio_path)

def create_video_with_audio(audio_path, images_folder, progress_callback=None):
    try:
        audio = AudioSegment.from_file(audio_path)
    except Exception as e:
        print(f"[ERRO] Falha ao carregar áudio: {e}")
        return
    silence_threshold = audio.dBFS - 14
    audio_sem_silencio = remover_silencio(audio, silence_thresh=silence_threshold, min_silence_len=400, padding=300)
    temp_audio_no_silencio = "temp_audio_no_silencio.mp3"
    audio_sem_silencio.export(temp_audio_no_silencio, format="mp3")
    duration_seconds = audio_sem_silencio.duration_seconds
    audio_name = os.path.splitext(os.path.basename(audio_path))[0]
    temp_video_path = f"{audio_name}_temp.mp4"
    final_video_path = os.path.join(PASTA_DESTINO_VIDEOS, f"{audio_name}.mp4")
    images = load_images_from_folder(images_folder)
    if not images:
        print("[ERRO] Nenhuma imagem válida encontrada.")
        return
    try:
        change_times = get_beat_times(temp_audio_no_silencio)
        def internal_progress(current_frame, total_frames, eta):
            if progress_callback:
                percent = int((current_frame / total_frames) * 100)
                eta_str = time.strftime("%M:%S", time.gmtime(max(0, eta)))
                progress_callback(percent, eta_str)
        create_video_only(temp_video_path, duration_seconds, images, fps=30, progress_callback=internal_progress, change_times=change_times)
        merge_audio_with_video(temp_video_path, temp_audio_no_silencio, final_video_path)
        os.remove(temp_video_path)
        os.remove(temp_audio_no_silencio)
        if progress_callback:
            progress_callback(100, "00:00")
    except Exception as e:
        print(f"[ERRO FATAL] Falha ao criar vídeo: {e}")

class VideoEditorFrame(Frame):
    COR_FUNDO = "#3e2f1c"
    COR_TEXTO = "#e2c290"
    COR_BOTAO = "#a68c53"
    COR_BOTAO_HOVER = "#c4b07e"
    COR_TEXTO_BOTAO = "#3e2f1c"
    FONT_TITULO = ("Segoe UI Semibold", 16)
    FONT_DEFAULT = ("Segoe UI", 12)
    def __init__(self, parent):
        super().__init__(parent, bg=self.COR_FUNDO)
        self.audio_paths = []
        label_style = {"bg": self.COR_FUNDO, "fg": self.COR_TEXTO, "font": self.FONT_DEFAULT}
        btn_style = {"bg": self.COR_BOTAO, "fg": self.COR_TEXTO_BOTAO, "activebackground": self.COR_BOTAO_HOVER,
                     "font": self.FONT_DEFAULT, "relief": "flat", "borderwidth": 0,
                     "highlightthickness": 0, "highlightbackground": self.COR_BOTAO,
                     "padx": 12, "pady": 8}
        Label(self, text="Carl Jung - Editor Automático de Vídeo", font=self.FONT_TITULO,
              bg=self.COR_FUNDO, fg=self.COR_TEXTO).pack(pady=(20, 15))
        self.btn_select_audio = Button(self, text="Selecionar Áudios", command=self.select_audios, **btn_style)
        self.btn_select_audio.pack(pady=10)
        Label(self, text="Escolha a Categoria de Imagens:", **label_style).pack(pady=(20, 10))
        self.category_combo = Combobox(self, state="readonly", font=self.FONT_DEFAULT, width=50)
        self.category_combo.pack(pady=10)
        style = Style()
        style.theme_use('default')
        style.configure("TCombobox",
                        fieldbackground=self.COR_BOTAO,
                        background=self.COR_BOTAO,
                        foreground=self.COR_TEXTO,
                        arrowcolor=self.COR_TEXTO)
        self.btn_start = Button(self, text="Começar Edição", command=self.start_editing, **btn_style)
        self.btn_start.pack(pady=20)
        self.progress = Progressbar(self, orient='horizontal', length=600, mode='determinate')
        self.progress.pack(pady=(5, 3))
        self.progress_label = Label(self, text="0%", **label_style)
        self.progress_label.pack()
        self.eta_label = Label(self, text="", **label_style)
        self.eta_label.pack()
        self.status_label = Label(self, text="", **label_style)
        self.status_label.pack(pady=(15, 20))

    def load_categories(self):
        if not os.path.isdir(PASTA_CATEGORIAS):
            messagebox.showerror("Erro", "Pasta de categorias não encontrada!")
            return
        subpastas = [nome for nome in os.listdir(PASTA_CATEGORIAS) if os.path.isdir(os.path.join(PASTA_CATEGORIAS, nome))]
        if not subpastas:
            messagebox.showwarning("Aviso", "Nenhuma subpasta encontrada em categorias.")
            return
        self.category_combo['values'] = subpastas
        self.category_combo.current(0)

    def select_audios(self):
        files = filedialog.askopenfilenames(title="Selecione um ou mais arquivos de áudio", filetypes=[("Áudios", "*.mp3 *.wav")])
        if files:
            self.audio_paths = list(files)
            messagebox.showinfo("Sucesso", f"{len(files)} áudio(s) selecionado(s).")
            self.status_label.config(text=f"{len(files)} áudio(s) selecionado(s).")

    def update_progress(self, percent, eta):
        self.progress['value'] = percent
        self.progress_label.config(text=f"{percent}%")
        self.eta_label.config(text=f"Tempo estimado restante: {eta}")
        self.update_idletasks()

    def run_editing(self):
        selected_category = self.category_combo.get()
        if not selected_category:
            messagebox.showwarning("Aviso", "Selecione uma categoria de imagens!")
            self.status_label.config(text="Selecione uma categoria de imagens!")
            return
        if not self.audio_paths:
            messagebox.showwarning("Aviso", "Nenhum áudio selecionado!")
            self.status_label.config(text="Nenhum áudio selecionado!")
            return
        images_folder = os.path.join(PASTA_CATEGORIAS, selected_category)
        self.btn_select_audio.config(state='disabled')
        self.btn_start.config(state='disabled')
        self.status_label.config(text="Processando vídeos...")
        try:
            total_audios = len(self.audio_paths)
            for idx, audio_path in enumerate(self.audio_paths, start=1):
                self.status_label.config(text=f"Processando áudio {idx} de {total_audios}: {os.path.basename(audio_path)}")
                create_video_with_audio(audio_path, images_folder, progress_callback=self.update_progress)
            self.status_label.config(text="Todos os vídeos foram gerados com sucesso!")
            messagebox.showinfo("Concluído", "Todos os vídeos foram gerados com sucesso!")
        except Exception as e:
            messagebox.showerror("Erro", f"Ocorreu um erro: {e}")
            self.status_label.config(text="Erro durante processamento.")
        self.progress['value'] = 0
        self.progress_label.config(text="0%")
        self.eta_label.config(text="")
        self.btn_select_audio.config(state='normal')
        self.btn_start.config(state='normal')

    def start_editing(self):
        thread = threading.Thread(target=self.run_editing, daemon=True)
        thread.start()


# === GUI COMPLETO COM ABAS ===

class MainApp:
    def __init__(self, root):
        self.root = root
        root.title("Sistema Integrado - Carl Jung")
        root.geometry("1000x700")
        root.configure(bg="#3e2f1c")

        style = Style()
        style.theme_use('default')
        style.configure('TNotebook', background="#3e2f1c", borderwidth=0)
        style.configure('TNotebook.Tab', background="#a68c53", foreground="#e2c290", font=("Segoe UI Semibold", 13))
        style.map('TNotebook.Tab', background=[('selected', '#c4b07e')], foreground=[('selected', '#3e2f1c')])

        self.notebook = Notebook(root)
        self.notebook.pack(expand=True, fill='both', padx=10, pady=10)

        # Aba 1: Editor Automático de Vídeo
        self.frame_video = VideoEditorFrame(self.notebook)
        self.frame_video.load_categories()
        self.notebook.add(self.frame_video, text="Editor Automático de Vídeo")

       # Aba 2: Agendador e Upload para YouTube
class AgendadorFrame(Frame):
    def __init__(self, parent):
        super().__init__(parent, bg="#3e2f1c")
        # Configurações visuais
        self.bg_color = "#3e2f1c"
        self.fg_color = "#e2c290"
        self.btn_color = "#a68c53"
        self.btn_hover_color = "#c4b07e"
        self.font_default = ("Segoe UI", 12)
        self.font_title = ("Segoe UI Semibold", 14)

        Label(self, text="Agendador e Upload YouTube", bg=self.bg_color, fg=self.fg_color, font=self.font_title).pack(pady=(15, 10))

        # Vídeo
        lbl_video = Label(self, text="Arquivo de Vídeo (.mp4):", bg=self.bg_color, fg=self.fg_color, font=self.font_default)
        lbl_video.pack()
        self.ent_video = Entry(self, width=70)
        self.ent_video.pack(pady=(0,10))
        btn_video = Button(self, text="Selecionar Vídeo", bg=self.btn_color, fg=self.fg_color, command=self.escolher_video)
        btn_video.pack(pady=(0, 10))

        # Thumbnail
        lbl_thumb = Label(self, text="Thumbnail (opcional):", bg=self.bg_color, fg=self.fg_color, font=self.font_default)
        lbl_thumb.pack()
        self.ent_thumb = Entry(self, width=70)
        self.ent_thumb.pack(pady=(0,10))
        btn_thumb = Button(self, text="Selecionar Thumbnail", bg=self.btn_color, fg=self.fg_color, command=self.escolher_thumb)
        btn_thumb.pack(pady=(0, 10))

        # Perfil de upload
        lbl_perfil = Label(self, text="Perfil para Upload:", bg=self.bg_color, fg=self.fg_color, font=self.font_default)
        lbl_perfil.pack()
        self.var_perfil = StringVar()
        self.opt_perfis = OptionMenu(self, self.var_perfil, "")
        self.opt_perfis.pack(pady=(0,10))
        self.atualizar_perfis()

        btn_gerenciar = Button(self, text="Gerenciar Perfis", bg=self.btn_color, fg=self.fg_color, command=self.abrir_gestao_perfis)
        btn_gerenciar.pack(pady=(0, 20))

        # Descrição e tags
        lbl_desc = Label(self, text="Descrição:", bg=self.bg_color, fg=self.fg_color, font=self.font_default)
        lbl_desc.pack()
        self.txt_desc = Text(self, width=80, height=10)
        self.txt_desc.insert(END, DEFAULT_DESCRIPTION)
        self.txt_desc.pack(pady=(0, 10))

        lbl_tags = Label(self, text="Tags (separadas por vírgula):", bg=self.bg_color, fg=self.fg_color, font=self.font_default)
        lbl_tags.pack()
        self.ent_tags = Entry(self, width=80)
        self.ent_tags.insert(0, ",".join(DEFAULT_TAGS))
        self.ent_tags.pack(pady=(0, 10))

        # Agendamento manual
        frame_schedule = Frame(self, bg=self.bg_color)
        frame_schedule.pack(pady=(10, 20), fill="x")

        lbl_num_videos = Label(frame_schedule, text="Número de vídeos para enviar:", bg=self.bg_color, fg=self.fg_color)
        lbl_num_videos.pack(side="left")
        self.ent_num_videos = Entry(frame_schedule, width=5)
        self.ent_num_videos.insert(0, "1")
        self.ent_num_videos.pack(side="left", padx=(5,20))

        lbl_data = Label(frame_schedule, text="Data (AAAA-MM-DD):", bg=self.bg_color, fg=self.fg_color)
        lbl_data.pack(side="left")
        self.ent_data = Entry(frame_schedule, width=12)
        self.ent_data.pack(side="left", padx=(5, 20))
        today_str = datetime.now(pytz.timezone("America/Sao_Paulo")).strftime("%Y-%m-%d")
        self.ent_data.insert(0, today_str)

        lbl_hora = Label(frame_schedule, text="Hora (HH:MM 24h):", bg=self.bg_color, fg=self.fg_color)
        lbl_hora.pack(side="left")
        self.ent_hora = Entry(frame_schedule, width=6)
        self.ent_hora.pack(side="left", padx=(5,20))
        self.ent_hora.insert(0, "06:00")

        # Progresso e preview
        progress_frame = Frame(self, bg=self.bg_color)
        progress_frame.pack(pady=(0, 10), fill="x")
        lbl_prog = Label(progress_frame, text="Progresso:", bg=self.bg_color, fg=self.fg_color)
        lbl_prog.pack(side="left")
        self.lbl_progress = Label(progress_frame, text="0/0", width=10, anchor="w", bg=self.bg_color, fg=self.fg_color)
        self.lbl_progress.pack(side="left")

        lbl_preview = Label(self, text="Fila de agendamento:", bg=self.bg_color, fg=self.fg_color)
        lbl_preview.pack()
        self.txt_preview = Text(self, height=10, width=80, state="disabled")
        self.txt_preview.pack(pady=(0, 10))

        btn_enviar = Button(self, text="Enviar Vídeos", bg=self.btn_color, fg=self.fg_color, command=self.enviar_videos)
        btn_enviar.pack(pady=(5, 20))

    def atualizar_perfis(self):
        perfis = get_perfis()
        menu = self.opt_perfis["menu"]
        menu.delete(0, "end")
        for p in perfis:
            menu.add_command(label=p[1], command=lambda v=p[1]: self.var_perfil.set(v))
        if perfis:
            self.var_perfil.set(perfis[0][1])
        else:
            self.var_perfil.set("")

    def abrir_gestao_perfis(self):
        mostrar_gestao_perfis(self)
        self.atualizar_perfis()

    def escolher_video(self):
        path = filedialog.askopenfilename(title="Selecione o arquivo de vídeo", filetypes=[("MP4 files", "*.mp4")])
        if path:
            self.ent_video.delete(0, END)
            self.ent_video.insert(0, path)

    def escolher_thumb(self):
        path = filedialog.askopenfilename(title="Selecione o thumbnail", filetypes=[("Image files", "*.jpg *.png *.jpeg")])
        if path:
            self.ent_thumb.delete(0, END)
            self.ent_thumb.insert(0, path)

    def atualizar_preview(self, filas):
        self.txt_preview.config(state="normal")
        self.txt_preview.delete("1.0", END)
        for idx, (video, datahora) in enumerate(filas, 1):
            dt = datetime.fromisoformat(datahora)
            data_fmt = dt.strftime("%d/%m %Y %H:%M")
            self.txt_preview.insert(END, f"{idx}. {os.path.basename(video)} - {data_fmt}\n")
        self.txt_preview.config(state="disabled")

    def progress_callback(self, sent, total):
        self.lbl_progress.config(text=f"{sent}/{total}")

    def enviar_videos(self):
        video_path = self.ent_video.get().strip()
        thumbnail_path = self.ent_thumb.get().strip()
        profile_nome = self.var_perfil.get()
        description = self.txt_desc.get("1.0", END).strip()
        tags = [t.strip() for t in self.ent_tags.get().split(",") if t.strip()]

        if not video_path:
            messagebox.showwarning("Aviso", "Selecione um vídeo")
            return
        if not profile_nome:
            messagebox.showwarning("Aviso", "Selecione um perfil")
            return
        try:
            qtd = int(self.ent_num_videos.get())
            if qtd < 1:
                messagebox.showwarning("Aviso", "Número de vídeos inválido")
                return
        except Exception:
            messagebox.showwarning("Aviso", "Número de vídeos inválido")
            return

        data_str = self.ent_data.get().strip()
        hora_str = self.ent_hora.get().strip()
        try:
            dt_str = f"{data_str}T{hora_str}:00"
            dt_parsed = datetime.fromisoformat(dt_str)
            br_tz = pytz.timezone("America/Sao_Paulo")
            if dt_parsed.tzinfo is None:
                dt_parsed = br_tz.localize(dt_parsed)
            else:
                dt_parsed = dt_parsed.astimezone(br_tz)
            agora = datetime.now(br_tz)
            if dt_parsed <= agora:
                messagebox.showwarning("Aviso", "Data e hora devem ser no futuro")
                return
        except Exception:
            messagebox.showwarning("Aviso", "Data ou hora inválida")
            return

        videos_list = [video_path] * qtd
        thumbs_list = [thumbnail_path] * qtd if thumbnail_path else [None] * qtd

        try:
            horarios_agendados = get_next_available_time(profile_nome, start_dt=dt_parsed, num_slots=qtd)
        except Exception as e:
            messagebox.showerror("Erro", f"Erro ao calcular horários: {e}")
            return

        fila_preview = list(zip(videos_list, horarios_agendados))
        self.atualizar_preview(fila_preview)
        self.progress_callback(0, qtd)

        def worker():
            processar_envio(profile_nome, videos_list, description, tags, thumbs_list, publicar_agora=False,
                           notify=True, category_id="22", start_schedule_dt=dt_parsed, qtd_videos=qtd,
                           progress_callback=self.progress_callback)
        threading.Thread(target=worker, daemon=True).start()


# === INTEGRAÇÃO FINAL NA MAINAPP ===

class MainApp:
    def __init__(self, root):
        self.root = root
        root.title("Sistema Integrado - Carl Jung")
        root.geometry("1000x700")
        root.configure(bg="#3e2f1c")

        style = Style()
        style.theme_use('default')
        style.configure('TNotebook', background="#3e2f1c", borderwidth=0)
        style.configure('TNotebook.Tab', background="#a68c53", foreground="#e2c290", font=("Segoe UI Semibold", 13))
        style.map('TNotebook.Tab', background=[('selected', '#c4b07e')], foreground=[('selected', '#3e2f1c')])

        self.notebook = Notebook(root)
        self.notebook.pack(expand=True, fill='both', padx=10, pady=10)

        # Aba 1: Editor Automático de Vídeo
        self.frame_video = VideoEditorFrame(self.notebook)
        self.frame_video.load_categories()
        self.notebook.add(self.frame_video, text="Editor Automático de Vídeo")

        # Aba 2: Agendador e Upload
        self.frame_agendador = AgendadorFrame(self.notebook)
        self.notebook.add(self.frame_agendador, text="Agendador YouTube")


def main():
    init_db()
    root = Tk()
    app = MainApp(root)
    root.mainloop()

if __name__ == "__main__":
    main()