Atualmente o processo funciona assim: * **Linha 3...

Criado em: 9 de maio de 2025

Respondido usando O3 por Chat01

Pergunta

Atualmente o processo funciona assim:

  • Linha 3

    • Quando o gradiente atinge o início da linha 3, a linha 1 desaparece.
    • Ao chegar na metade da linha 3, a linha 5 aparece no lugar da linha 1.
  • Linha 4

    • Quando o gradiente atinge o início da linha 4, a linha 2 desaparece.
    • Ao chegar na metade da linha 4, a linha 6 aparece no lugar da linha 2.

Quero que a mesma lógica seja aplicada às linhas 3 e 4:

  • Linha 5

    • Quando o gradiente atinge o início da linha 5, a linha 3 desaparece.
    • Ao chegar na metade da linha 5, a linha 7 aparece no lugar da linha 3.
  • Linha 6

    • Quando o gradiente atinge o início da linha 6, a linha 4 desaparece.
    • Ao chegar na metade da linha 6, a linha 8 aparece no lugar da linha 4.
python
import os import shutil import traceback import re import subprocess import time import multiprocessing from datetime import timedelta from functools import lru_cache from PIL import Image, ImageDraw, ImageFont, ImageFilter from tqdm import tqdm # # CONFIGURAÇÕES DE VÍDEO E IMAGEM IMAGE_SIZE = (1280, 720) # Tamanho base das imagens/frames FINAL_SIZE = (1280, 720) # Tamanho final do vídeo (pode ser diferente se houver redimensionamento final) FPS = 30 # Quadros por segundo do vídeo # CONFIGURAÇÕES DE LAYOUT E POSICIONAMENTO DE TEXTO AJUSTE_HORIZONTAL = 0 # Ajuste fino horizontal para o texto AJUSTE_VERTICAL = 73 # Ajuste fino vertical para o texto CENTRALIZAR = True # Se o texto principal deve ser centralizado horizontalmente POSICAO_VERTICAL = 2 # 1: Topo, 2: Meio, 3: Baixo, 4: Alternado (Topo/Baixo) MAX_WIDTH = IMAGE_SIZE[0] - 40 # Largura máxima para o texto nas legendas ESPACO_ENTRE_FRASES = 30 # Espaçamento vertical entre frases dentro de um mesmo grupo # CONFIGURAÇÕES DE FONTE E TEXTO FONT_SIZE_COMPLETE = 80 # Tamanho da fonte para as legendas UPPER_CASE = True # Converter todo o texto da legenda para maiúsculas FONT_PATH = r"C:\Users\lucas\LilitaOne-Regular.ttf" # Caminho para o arquivo da fonte # CONFIGURAÇÕES DE CORES FONT_COLOR = "#ffffff" # Cor principal da fonte das legendas HIGHLIGHT_COLOR = "#ff3131" # Cor de destaque para a palavra atual PROGRESS_BAR_COLOR = "#ff3131" # Cor da barra de progresso BORDER_COLOR = "#000000" # Cor da borda do texto # CONFIGURAÇÕES DA BARRA DE PROGRESSO E TEXTOS ADICIONAIS PROGRESS_BAR_SIZE = (1000, 53) # Tamanho da barra de progresso (largura, altura) SUBSCRIBE_TEXT_SIZE = 23 # Tamanho da fonte para o texto "Inscreva-se" # CONFIGURAÇÕES DE TRANSIÇÃO E TIMING TRANSITION_DURATION_GAPS = 0.1 # Duração da transição para o aparecimento de texto durante gaps FADE_OUT_DURATION_LINES = 0.3 # Duração do fade-out para as linhas de legenda durante a transição de grupo FADE_IN_DURATION_NEXT_LINES = 0.3 # Duração do fade-in para as novas linhas do próximo grupo MIN_GAP_DURATION = 4.0 # Duração mínima de um "gap" para exibir a barra de progresso e texto adicional (em segundos) # CONFIGURAÇÕES DE AGRUPAMENTO DE LEGENDAS NUM_FRASES_GRUPO = 4 # Número de sentenças a serem exibidas juntas na tela # CAMINHOS E DIRETÓRIOS BACKGROUND_SOURCE = r"C:\Users\lucas\Downloads\Scripts\Karaoke-Creator\#shorts\capa-fs.mp4" # Caminho para imagem/vídeo de fundo BACKGROUND_FRAMES_DIR = "background_frames" # Diretório para armazenar frames extraídos do vídeo de fundo def hex_to_rgb(hex_color): """Converte cor hexadecimal para RGB.""" return tuple(int(hex_color.lstrip("#")[i: i + 2], 16) for i in (0, 2, 4)) def parse_time(time_str): """Converte string de tempo para timedelta.""" match = re.match(r"(\d+):(\d{2}):(\d)#", time_str) if match: minutes, seconds, deciseconds = map(int, match.groups()) if minutes == 0: if hasattr(parse_time, 'last_minutes') and parse_time.last_minutes > 0: minutes = parse_time.last_minutes parse_time.last_minutes = minutes total_seconds = minutes * 60 + seconds + deciseconds / 10.0 return timedelta(seconds=total_seconds) return timedelta(0) parse_time.last_minutes = 0 def get_audio_duration(audio_file): """Obtém a duração de um arquivo de áudio usando ffprobe.""" command = [ "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", audio_file, ] result = subprocess.run(command, capture_output=True, text=True) return float(result.stdout) def read_subtitles(filename, audio_duration): """ Lê e processa o arquivo de legendas, criando estruturas para gerenciar as frases e intervalos. """ raw_subtitle_entries = [] with open(filename, "r", encoding="utf-8") as file: content = file.read().strip() lines = content.split("\n") if lines[-1].endswith('#P'): lines = lines[:-1] for line in lines: match = re.match(r"(\d+:\d{2}:\d)#(.+)", line.strip()) if match: time_str, word_from_file = match.groups() word_for_display = word_from_file.upper() if UPPER_CASE else word_from_file raw_subtitle_entries.append((parse_time(time_str + "#"), word_for_display, word_from_file)) if not raw_subtitle_entries: raise ValueError("Nenhuma legenda foi encontrada no arquivo.") sentences = _extract_sentences(raw_subtitle_entries) final_timestamp = timedelta(seconds=audio_duration) _set_sentence_end_times(sentences, final_timestamp) processed_subtitle_words = [(entry[0], entry[1]) for entry in raw_subtitle_entries] gaps = _identify_gaps(sentences, processed_subtitle_words, final_timestamp) paired_sentences = _group_sentences(sentences, final_timestamp) return processed_subtitle_words, paired_sentences, gaps def _extract_sentences(raw_subtitle_entries): """ Extrai 'linhas' baseadas na capitalização da palavra original do arquivo .srt. Cada 'sentence' no resultado é uma 'linha' completa. """ sentences = [] current_line_words_for_display = [] current_line_start_time = None current_line_start_index = -1 for i, (time, word_for_display, original_word_from_file) in enumerate(raw_subtitle_entries): if not original_word_from_file: continue starts_with_capital = original_word_from_file[0].isupper() if starts_with_capital: if current_line_words_for_display: sentences.append({ "start_time": current_line_start_time, "end_time": None, "words": list(current_line_words_for_display), "start_index": current_line_start_index, "end_index": i, }) current_line_words_for_display.clear() current_line_start_time = time current_line_start_index = i current_line_words_for_display.append(word_for_display) else: if not current_line_words_for_display: current_line_start_time = time current_line_start_index = i current_line_words_for_display.append(word_for_display) if current_line_words_for_display: sentences.append({ "start_time": current_line_start_time, "end_time": None, "words": list(current_line_words_for_display), "start_index": current_line_start_index, "end_index": len(raw_subtitle_entries), }) return sentences def _set_sentence_end_times(sentences, final_timestamp): """Define os tempos finais para cada sentença.""" for i in range(len(sentences) - 1): sentences[i]["end_time"] = sentences[i + 1]["start_time"] if sentences: sentences[-1]["end_time"] = final_timestamp def _identify_gaps(sentences, subtitles, final_timestamp): """Identifica os intervalos entre sentenças e palavras.""" gaps = [] if sentences and sentences[0]["start_time"] > timedelta(0): gaps.append({"start_time": timedelta(0), "end_time": sentences[0]["start_time"]}) for i in range(len(sentences) - 1): gap_start = sentences[i]["end_time"] gap_end = sentences[i + 1]["start_time"] if gap_end > gap_start: gaps.append({"start_time": gap_start, "end_time": gap_end}) if sentences and sentences[-1]["end_time"] < final_timestamp: gaps.append({"start_time": sentences[-1]["end_time"], "end_time": final_timestamp}) additional_gaps = _identify_word_gaps(subtitles, final_timestamp) all_gaps = gaps + additional_gaps merged_gaps = _merge_intervals(all_gaps) return [gap for gap in merged_gaps if (gap['end_time'] - gap['start_time']).total_seconds() >= MIN_GAP_DURATION] def _identify_word_gaps(subtitles, final_timestamp): """Identifica intervalos longos entre palavras individuais.""" additional_gaps = [] for i in range(len(subtitles) - 1): current_time = subtitles[i][0] next_time = subtitles[i + 1][0] time_diff = (next_time - current_time).total_seconds() word_duration = time_diff effective_word_duration = min(word_duration, 3.0) effective_end_time = current_time + timedelta(seconds=effective_word_duration) gap_length = (next_time - effective_end_time).total_seconds() if gap_length >= MIN_GAP_DURATION: additional_gaps.append({'start_time': effective_end_time, 'end_time': next_time}) if subtitles: last_word_time = subtitles[-1][0] last_word_duration = (final_timestamp - last_word_time).total_seconds() effective_last_word_duration = min(last_word_duration, 3.0) effective_end_time = last_word_time + timedelta(seconds=effective_last_word_duration) gap_length = (final_timestamp - effective_end_time).total_seconds() if gap_length >= MIN_GAP_DURATION: additional_gaps.append({'start_time': effective_end_time, 'end_time': final_timestamp}) return additional_gaps def _merge_intervals(intervals): """Combina intervalos que se sobrepõem.""" if not intervals: return [] intervals.sort(key=lambda x: x['start_time']) merged = [intervals[0]] for current in intervals[1:]: last = merged[-1] if current['start_time'] <= last['end_time']: last['end_time'] = max(last['end_time'], current['end_time']) else: merged.append(current) return merged def _group_sentences(sentences, final_timestamp): """Agrupa sentenças em conjuntos para exibição.""" paired_sentences = [] num_sentences = len(sentences) for i in range(0, num_sentences, NUM_FRASES_GRUPO): group_sentences = sentences[i: i + NUM_FRASES_GRUPO] if not group_sentences: continue start_time = group_sentences[0]["start_time"] if i + NUM_FRASES_GRUPO < num_sentences: end_time = sentences[i + NUM_FRASES_GRUPO]["start_time"] else: end_time = final_timestamp group = { "start_time": start_time, "end_time": end_time, "sentences": group_sentences, } paired_sentences.append(group) return paired_sentences def adjust_font_size_to_fit(words, max_width, max_font_size): """Ajusta o tamanho da fonte para caber dentro da largura máxima.""" font_size = max_font_size font = ImageFont.truetype(FONT_PATH, font_size) total_width = sum(font.getlength(word) for word in words) + font.getlength(' ') * (len(words) - 1) while total_width > max_width and font_size > 10: font_size -= 1 font = ImageFont.truetype(FONT_PATH, font_size) total_width = sum(font.getlength(word) for word in words) + font.getlength(' ') * (len(words) - 1) return font_size, font def calcular_posicao_texto(largura_texto, altura_texto, posicao_idx=0): """ Calcula a posição ideal do texto baseado nas configurações globais e no índice de posição (para posicionamento alternado) """ global IMAGE_SIZE, CENTRALIZAR, AJUSTE_HORIZONTAL, POSICAO_VERTICAL, AJUSTE_VERTICAL if CENTRALIZAR: x = (IMAGE_SIZE[0] - largura_texto) // 2 else: x = 20 x += AJUSTE_HORIZONTAL posicao_v = POSICAO_VERTICAL if posicao_v == 4: posicao_v = 1 if posicao_idx % 2 == 0 else 3 if posicao_v == 1: y = IMAGE_SIZE[1] // 8 elif posicao_v == 2: y = (IMAGE_SIZE[1] - altura_texto) // 2 else: y = (IMAGE_SIZE[1] * 7) // 8 - altura_texto y += AJUSTE_VERTICAL return x, y def calculate_sentence_positions(paired_sentences): """Calcula a posição de cada sentença na tela.""" positions = [] max_width = MAX_WIDTH for i, group in enumerate(paired_sentences): fonts = [] sentences_lines = [] for sentence in group["sentences"]: font_size, font = adjust_font_size_to_fit(sentence["words"], max_width, FONT_SIZE_COMPLETE) fonts.append((font_size, font)) lines = [(sentence["words"], list(range(sentence["start_index"], sentence["end_index"])))] sentences_lines.append(lines) total_height = 0 for (font_size, _), lines in zip(fonts, sentences_lines): total_height += font_size * len(lines) if len(sentences_lines) > 1: total_height += ESPACO_ENTRE_FRASES * (len(sentences_lines) - 1) x_base, y_base = calcular_posicao_texto(max_width, total_height, i) y = y_base sentence_positions = [] for idx, ((sentence, lines), (font_size, font)) in enumerate(zip(zip(group["sentences"], sentences_lines), fonts)): line_height = font_size line_positions = [] if idx > 0: y += ESPACO_ENTRE_FRASES for line_words, line_word_indices in lines: total_width = sum(font.getlength(word) for word in line_words) + font.getlength(' ') * (len(line_words) - 1) if CENTRALIZAR: x = (IMAGE_SIZE[0] - total_width) // 2 else: x = x_base line_positions.append((x, y, font, total_width, line_words, line_word_indices)) y += line_height sentence_positions.append(line_positions) positions.append(sentence_positions) return positions @lru_cache(maxsize=1000) def create_gradient_word(word, width, start_color, end_color, progress, font_size): """ Cria uma imagem de palavra com efeito de gradiente. Cacheia resultados para melhor performance. """ font = ImageFont.truetype(FONT_PATH, font_size) padding_x = 20 padding_y = 20 ascent, descent = font.getmetrics() text_height = ascent + descent image_height = text_height + (padding_y * 2) image_width = int(width) + (padding_x * 2) base_image = Image.new("RGBA", (image_width, image_height), (0, 0, 0, 0)) draw = ImageDraw.Draw(base_image) x = padding_x y = padding_y + (image_height - text_height) // 2 - descent border_width = 3 border_color = hex_to_rgb(BORDER_COLOR) for offset_x in range(-border_width, border_width + 1): for offset_y in range(-border_width, border_width + 1): if offset_x != 0 or offset_y != 0: draw.text((x + offset_x, y + offset_y), word, font=font, fill=(*border_color, 255), anchor="la") shadow_offset = 2 shadow_color = (0, 0, 0, 180) draw.text((x + shadow_offset, y + shadow_offset), word, font=font, fill=shadow_color, anchor="la") draw.text((x, y), word, font=font, fill=(255, 255, 255, 255), anchor="la") mask = Image.new("L", base_image.size, 0) mask_draw = ImageDraw.Draw(mask) mask_draw.text((x, y), word, font=font, fill=255, anchor="la") gradient = Image.new("RGBA", base_image.size, (0, 0, 0, 0)) start_rgb = hex_to_rgb(start_color) end_rgb = hex_to_rgb(end_color) cut_point = int(image_width * progress) transition_width = 35 for i in range(image_width): if i <= cut_point - transition_width: color = end_rgb elif i >= cut_point + transition_width: color = start_rgb else: progress_in_transition = (i - (cut_point - transition_width)) / (2 * transition_width) color = tuple( int(end_rgb[j] + (start_rgb[j] - end_rgb[j]) * progress_in_transition) for j in range(3) ) ImageDraw.Draw(gradient).line([(i, 0), (i, image_height)], fill=(*color, 255)) gradient.putalpha(mask) result = Image.alpha_composite(base_image, gradient) return result def draw_text_with_alpha(image, text, x, y, font, color, alpha=255): """Desenha texto com transparência especificada.""" display_text = text.rstrip(".") word_width = font.getlength(display_text) word_image = create_gradient_word(display_text, word_width, color, color, 1.0, font.size) if alpha != 255: word_image = word_image.convert("RGBA") data = word_image.getdata() new_data = [(r, g, b, int(a * alpha / 255)) for r, g, b, a in data] word_image.putdata(new_data) padding_x_compensation = 20 image.paste(word_image, (int(x) - padding_x_compensation, int(y)), word_image) def draw_current_line_text(image, sentence, x, y, font, current_time, subtitles, line_words, line_word_indices, alpha=255): """Desenha uma linha de texto com animação de progresso.""" x_current = x space_width = font.getlength(" ") word_positions = [] for word in line_words: display_word = word.rstrip(".") word_width = font.getlength(display_word) word_positions.append((x_current, word_width)) x_current += word_width + space_width padding_x_compensation = 20 for (word_x, word_width), word, word_index in zip(word_positions, line_words, line_word_indices): display_word = word.rstrip(".") word_time = subtitles[word_index][0] if word_index + 1 < len(subtitles): next_word_time = subtitles[word_index + 1][0] else: next_word_time = word_time + timedelta(seconds=2) word_duration = (next_word_time - word_time).total_seconds() effective_word_duration = min(word_duration, 3.0) time_since_word_start = (current_time - word_time).total_seconds() if time_since_word_start >= effective_word_duration: progress = 1.0 elif time_since_word_start >= 0: progress = min(max(time_since_word_start / effective_word_duration, 0.0), 1.0) else: progress = 0.0 word_image = create_gradient_word( display_word, word_width, FONT_COLOR, HIGHLIGHT_COLOR, progress, font.size ) if alpha != 255: word_image = word_image.convert("RGBA") data = word_image.getdata() new_data = [(r, g, b, int(a * alpha / 255)) for r, g, b, a in data] word_image.putdata(new_data) image.paste(word_image, (int(word_x) - padding_x_compensation, int(y)), word_image) def draw_progress_bar(image, progress, bar_position=None, bar_size=None, number_to_display=None, alpha=255): """Desenha uma barra de progresso animada.""" if bar_position is None: bar_position = (IMAGE_SIZE[0] // 2, IMAGE_SIZE[1] - 200) if bar_size is None: bar_size = PROGRESS_BAR_SIZE x_center, y_center = bar_position bar_width, bar_height = bar_size x_start = x_center - bar_width // 2 y_start = y_center - bar_height // 2 overlay = Image.new("RGBA", image.size, (255, 255, 255, 0)) draw = ImageDraw.Draw(overlay) draw.rectangle([x_start, y_start, x_start + bar_width, y_start + bar_height], fill=(200, 200, 200, alpha)) progress_color = hex_to_rgb(PROGRESS_BAR_COLOR) progress_width = int(bar_width * progress) draw.rectangle( [x_start, y_start, x_start + progress_width, y_start + bar_height], fill=(*progress_color, alpha) ) if number_to_display is not None: font_size = int(bar_height * 2.2) font = ImageFont.truetype(FONT_PATH, font_size) text = str(number_to_display) text_width, text_height = draw.textsize(text, font=font) text_x = x_center - text_width // 2 text_y = y_center - text_height // 2 - int(bar_height * 0.3) draw.text((text_x, text_y), text, font=font, fill=(255, 255, 255, alpha)) image.alpha_composite(overlay) def draw_animated_text(image, current_time, gap, text, font_path, alpha=255): """Desenha texto animado com efeito de movimento.""" bar_position = (IMAGE_SIZE[0] // 2, IMAGE_SIZE[1] - 200) bar_width, bar_height = PROGRESS_BAR_SIZE base_text_y = bar_position[1] + bar_height // 2 + 20 draw = ImageDraw.Draw(image) font_size = SUBSCRIBE_TEXT_SIZE font = ImageFont.truetype(font_path, font_size) text_width, text_height = draw.textsize(text, font=font) text_x = (IMAGE_SIZE[0] - text_width) // 2 gap_duration = (gap["end_time"] - gap["start_time"]).total_seconds() time_since_gap_start = (current_time - gap["start_time"]).total_seconds() time_to_gap_end = (gap["end_time"] - current_time).total_seconds() fade_duration = 1 if time_since_gap_start < fade_duration: movement_progress = time_since_gap_start / fade_duration elif time_to_gap_end < fade_duration: movement_progress = time_to_gap_end / fade_duration else: movement_progress = 1.0 max_movement = 20 y_offset = max_movement * (1 - movement_progress) text_y = base_text_y - y_offset text_image = Image.new('RGBA', (text_width, text_height), (0, 0, 0, 0)) text_draw = ImageDraw.Draw(text_image) text_draw.text((0, 0), text, font=font, fill=(255, 255, 255, alpha)) image.paste(text_image, (text_x, int(text_y)), text_image) @lru_cache(maxsize=None) def load_and_resize_background(image_path, size): """Carrega e redimensiona a imagem de fundo, cacheando o resultado.""" try: img = Image.open(image_path).convert("RGBA") img = img.resize(size, Image.LANCZOS) return img except FileNotFoundError: print(f"Erro: Imagem de fundo não encontrada em {image_path}") return Image.new("RGBA", size, (0, 0, 0, 255)) except Exception as e: print(f"Erro ao carregar ou redimensionar a imagem de fundo: {e}") return Image.new("RGBA", size, (0, 0, 0, 255)) def extract_background_frames(video_path, output_dir, size, fps): """Extrai frames de um vídeo de fundo usando ffmpeg, redimensionando-os.""" os.makedirs(output_dir, exist_ok=True) frame_pattern = os.path.join(output_dir, "bg_frame_%04d.jpg") width, height = size try: full_command = [ "ffmpeg", "-y", "-hide_banner", "-i", video_path, "-vf", f"fps={fps},scale={width}:{height}", "-q:v", "2", "-start_number", "0", frame_pattern ] print(f"Extraindo frames de {video_path} para {output_dir}...") result = subprocess.run(full_command, check=True, capture_output=True, text=True, encoding='utf-8', errors='replace') extracted_files = [f for f in os.listdir(output_dir) if f.startswith("bg_frame_") and f.endswith(".jpg")] if not extracted_files: print(f"AVISO CRÍTICO: Nenhum frame (bg_frame_*.jpg) encontrado em {output_dir} após a execução do ffmpeg.") else: print(f"{len(extracted_files)} frames de fundo foram extraídos para {output_dir}.") except subprocess.CalledProcessError as e: print(f"Erro CRÍTICO ao extrair frames do vídeo de fundo: {e}") print(f"Comando: {' '.join(e.cmd)}") if e.stdout: print(f"Stdout do FFMPEG (erro): {e.stdout}") if e.stderr: print(f"Stderr do FFMPEG (erro): {e.stderr}") raise except FileNotFoundError: print("Erro: ffmpeg não encontrado. Certifique-se de que está instalado e no PATH do sistema.") raise def process_frame(frame_index, subtitles, paired_sentences, gaps, num_frames, sentence_positions, audio_duration, fps, is_background_video, current_background_frames_dir, num_actual_background_frames): """ Processa um único frame do vídeo, aplicando legendas e efeitos sobre um fundo (estático ou de vídeo). """ global TRANSITION_DURATION_GAPS, BACKGROUND_SOURCE, IMAGE_SIZE, FADE_OUT_DURATION_LINES, NUM_FRASES_GRUPO, FADE_IN_DURATION_NEXT_LINES, FONT_COLOR current_time = timedelta(seconds=frame_index / fps) if is_background_video: if num_actual_background_frames > 0: bg_frame_to_load_idx = frame_index % num_actual_background_frames bg_frame_path = os.path.join(current_background_frames_dir, f"bg_frame_{bg_frame_to_load_idx:04d}.jpg") try: background_image = Image.open(bg_frame_path).convert("RGBA") except FileNotFoundError: print(f"Aviso: Frame de fundo {bg_frame_path} (índice {bg_frame_to_load_idx} de {num_actual_background_frames} frames) não encontrado. Usando fundo preto.") background_image = Image.new("RGBA", IMAGE_SIZE, (0, 0, 0, 255)) except Exception as e: print(f"Erro ao carregar frame de fundo {bg_frame_path}: {e}. Usando fundo preto.") background_image = Image.new("RGBA", IMAGE_SIZE, (0, 0, 0, 255)) else: background_image = Image.new("RGBA", IMAGE_SIZE, (0, 0, 0, 255)) else: background_image = load_and_resize_background(BACKGROUND_SOURCE, IMAGE_SIZE) image = background_image.copy() subtitles_visible = True # alpha = 255 # alpha é agora tratado por linha/slot current_pair_index = None for i, pair in enumerate(paired_sentences): if pair["start_time"] <= current_time < pair["end_time"]: current_pair_index = i break in_gap = False for gap_index, gap in enumerate(gaps): if gap["start_time"] <= current_time < gap["end_time"]: in_gap = True # alpha_gap = 255 # Alpha para elementos do gap gap_duration = (gap["end_time"] - gap["start_time"]).total_seconds() gap_progress = (current_time - gap["start_time"]).total_seconds() / gap_duration is_start_gap = gap_index == 0 and gap["start_time"] == timedelta(0) if is_start_gap: total_countdown_duration = 3.0 countdown_start_time = gap["end_time"] - timedelta(seconds=total_countdown_duration) if countdown_start_time <= current_time < gap["end_time"]: gap_remaining_time = (gap["end_time"] - current_time).total_seconds() number_to_display = int(gap_remaining_time) + 1 if 1 <= number_to_display <= 3: draw_progress_bar(image, gap_progress, number_to_display=number_to_display, alpha=255) else: draw_progress_bar(image, gap_progress, alpha=255) draw_animated_text(image, current_time, gap, "Curta o vídeo e inscreva-se no canal", FONT_PATH, alpha=255) else: draw_progress_bar(image, gap_progress, alpha=255) draw_animated_text(image, current_time, gap, "Curta o vídeo e inscreva-se no canal", FONT_PATH, alpha=255) else: draw_progress_bar(image, gap_progress, alpha=255) draw_animated_text(image, current_time, gap, "Curta o vídeo e inscreva-se no canal", FONT_PATH, alpha=255) subtitles_visible = False break if subtitles_visible and current_pair_index is not None: pair_data = paired_sentences[current_pair_index] current_pair_sentences = pair_data["sentences"] # Lista de sentenças no grupo atual current_positions_for_pair = sentence_positions[current_pair_index] # Lista de posições para o grupo atual group_alpha = 255 # Alpha base para sentenças totalmente visíveis # Inicialização dos arrays de alpha e visibilidade para cada slot de display alpha_line_current = [group_alpha] * NUM_FRASES_GRUPO should_draw_line_current = [False] * NUM_FRASES_GRUPO alpha_next_pair_line = [0] * NUM_FRASES_GRUPO # Alpha para a linha correspondente do *próximo* grupo should_draw_next_pair_line = [False] * NUM_FRASES_GRUPO # Se a linha do *próximo* grupo deve ser desenhada # Define quais linhas do grupo atual existem e devem ser inicialmente consideradas para desenho for i in range(min(len(current_pair_sentences), NUM_FRASES_GRUPO)): should_draw_line_current[i] = True # Obtém as sentenças do próximo grupo, se existir next_pair_sentences_list = None if current_pair_index + 1 < len(paired_sentences): next_pair_sentences_list = paired_sentences[current_pair_index + 1]["sentences"] # Calcula as transições (fade out da linha atual, fade in da linha do próximo grupo) para cada slot for k in range(NUM_FRASES_GRUPO): # k é o índice do slot de display (0 a NUM_FRASES_GRUPO-1) trigger_sentence_for_fade_current = None # Sentença que dispara o fade out da linha atual no slot k trigger_sentence_for_appear_next = None # Sentença que dispara o fade in da linha do próximo grupo no slot k trigger_is_from_current_pair = False # True se a sentença gatilho pertence ao grupo atual # Determinar as sentenças gatilho com base no slot k if k == 0: # Slot 0 (originalmente linha 1 do grupo atual) if NUM_FRASES_GRUPO >= 3 and len(current_pair_sentences) >= 3: trigger_sentence_for_fade_current = current_pair_sentences[2] # Gatilho é a 3ª sentença do grupo atual trigger_sentence_for_appear_next = current_pair_sentences[2] trigger_is_from_current_pair = True elif k == 1: # Slot 1 (originalmente linha 2 do grupo atual) if NUM_FRASES_GRUPO >= 4 and len(current_pair_sentences) >= 4: trigger_sentence_for_fade_current = current_pair_sentences[3] # Gatilho é a 4ª sentença do grupo atual trigger_sentence_for_appear_next = current_pair_sentences[3] trigger_is_from_current_pair = True elif k == 2: # Slot 2 (originalmente linha 3 do grupo atual) - NOVA LÓGICA if NUM_FRASES_GRUPO >= 3: # Verifica se o slot k conceitualmente existe if next_pair_sentences_list and len(next_pair_sentences_list) > 0: trigger_sentence_for_fade_current = next_pair_sentences_list[0] # Gatilho é a 1ª sentença do PRÓXIMO grupo trigger_sentence_for_appear_next = next_pair_sentences_list[0] trigger_is_from_current_pair = False elif k == 3: # Slot 3 (originalmente linha 4 do grupo atual) - NOVA LÓGICA if NUM_FRASES_GRUPO >= 4: # Verifica se o slot k conceitualmente existe if next_pair_sentences_list and len(next_pair_sentences_list) > 1: trigger_sentence_for_fade_current = next_pair_sentences_list[1] # Gatilho é a 2ª sentença do PRÓXIMO grupo trigger_sentence_for_appear_next = next_pair_sentences_list[1] trigger_is_from_current_pair = False # Aplicar lógica de FADE OUT para current_pair_sentences[k] if trigger_sentence_for_fade_current and k < len(current_pair_sentences) and should_draw_line_current[k]: time_trigger_fade = trigger_sentence_for_fade_current["start_time"] time_when_fade_finishes = time_trigger_fade + timedelta(seconds=FADE_OUT_DURATION_LINES) if current_time >= time_trigger_fade: if current_time < time_when_fade_finishes and FADE_OUT_DURATION_LINES > 0: time_elapsed_in_fade = (current_time - time_trigger_fade).total_seconds() progress_fade = min(time_elapsed_in_fade / FADE_OUT_DURATION_LINES, 1.0) alpha_line_current[k] = int(group_alpha * (1 - progress_fade)) else: should_draw_line_current[k] = False # Linha atual desapareceu completamente alpha_line_current[k] = 0 # Aplicar lógica de FADE IN para next_pair_sentences_list[k] (a linha k do próximo grupo) if trigger_sentence_for_appear_next: # Verificar se a linha k do próximo grupo realmente existe if next_pair_sentences_list and k < len(next_pair_sentences_list): start_of_trigger_sentence = trigger_sentence_for_appear_next["start_time"] # Determinar o fim da duração efetiva da sentença gatilho para calcular o ponto médio end_of_trigger_sentence_for_gradient = None source_sentences_for_trigger_duration = current_pair_sentences if trigger_is_from_current_pair else next_pair_sentences_list group_end_time_for_trigger_source = None if trigger_is_from_current_pair: group_end_time_for_trigger_source = pair_data["end_time"] elif current_pair_index + 1 < len(paired_sentences): # Implica que next_pair_sentences_list existe group_end_time_for_trigger_source = paired_sentences[current_pair_index + 1]["end_time"] idx_of_trigger_in_its_source = -1 if source_sentences_for_trigger_duration: # Encontrar o índice da sentença gatilho em sua lista de origem for idx, sentence_obj in enumerate(source_sentences_for_trigger_duration): if sentence_obj is trigger_sentence_for_appear_next: # Comparação de identidade de objeto idx_of_trigger_in_its_source = idx break if source_sentences_for_trigger_duration and idx_of_trigger_in_its_source != -1: if idx_of_trigger_in_its_source + 1 < len(source_sentences_for_trigger_duration): # Há uma próxima sentença no mesmo grupo da sentença gatilho end_of_trigger_sentence_for_gradient = source_sentences_for_trigger_duration[idx_of_trigger_in_its_source + 1]["start_time"] elif group_end_time_for_trigger_source: # É a última sentença no grupo da sentença gatilho, usar o end_time desse grupo end_of_trigger_sentence_for_gradient = group_end_time_for_trigger_source if end_of_trigger_sentence_for_gradient is None: # Fallback se não conseguiu determinar end_of_trigger_sentence_for_gradient = start_of_trigger_sentence + timedelta(seconds=2) # Duração padrão de 2s # Calcular tempos para o fade in da linha do próximo grupo mid_time_trigger_sentence = start_of_trigger_sentence + (end_of_trigger_sentence_for_gradient - start_of_trigger_sentence) / 2 time_when_next_line_starts_appearing = mid_time_trigger_sentence time_when_next_line_fully_appears = time_when_next_line_starts_appearing + timedelta(seconds=FADE_IN_DURATION_NEXT_LINES) if current_time >= time_when_next_line_starts_appearing: should_draw_next_pair_line[k] = True if current_time < time_when_next_line_fully_appears and FADE_IN_DURATION_NEXT_LINES > 0: time_elapsed_in_fade_in = (current_time - time_when_next_line_starts_appearing).total_seconds() progress_fade_in = min(time_elapsed_in_fade_in / FADE_IN_DURATION_NEXT_LINES, 1.0) alpha_next_pair_line[k] = int(group_alpha * progress_fade_in) else: alpha_next_pair_line[k] = group_alpha # Totalmente visível # Loop de DESENHO: iterar sobre cada slot de display for display_slot_idx in range(NUM_FRASES_GRUPO): sentence_to_draw = None line_positions_to_use = None current_alpha_to_use = 0 # Default to transparent is_drawing_from_next_pair = False force_static_color_for_next_pair_line = None # Tentar desenhar a linha do PRÓXIMO grupo se ela deve aparecer e tem maior prioridade (já está em fade in) if should_draw_next_pair_line[display_slot_idx] and alpha_next_pair_line[display_slot_idx] > 0: if next_pair_sentences_list and display_slot_idx < len(next_pair_sentences_list): # Precisamos das posições do próximo par também if current_pair_index + 1 < len(sentence_positions): next_pair_positions_list = sentence_positions[current_pair_index + 1] if display_slot_idx < len(next_pair_positions_list): # Checa se o slot existe nas posições do próximo par sentence_to_draw = next_pair_sentences_list[display_slot_idx] line_positions_to_use = next_pair_positions_list[display_slot_idx] is_drawing_from_next_pair = True force_static_color_for_next_pair_line = FONT_COLOR # Linhas do próximo par entram com cor estática current_alpha_to_use = alpha_next_pair_line[display_slot_idx] # Se não for desenhar o próximo par (ou ele não deve ser desenhado ainda/mais), tentar desenhar a linha do par ATUAL if not sentence_to_draw and should_draw_line_current[display_slot_idx] and alpha_line_current[display_slot_idx] > 0: if display_slot_idx < len(current_pair_sentences) and display_slot_idx < len(current_positions_for_pair): sentence_to_draw = current_pair_sentences[display_slot_idx] line_positions_to_use = current_positions_for_pair[display_slot_idx] current_alpha_to_use = alpha_line_current[display_slot_idx] is_drawing_from_next_pair = False # Garantir que está correto force_static_color_for_next_pair_line = None # Garantir que está correto if sentence_to_draw and line_positions_to_use and current_alpha_to_use > 0: for line_info in line_positions_to_use: x, y, font, _width, line_words, line_word_indices = line_info # _width não é usado aqui if is_drawing_from_next_pair and force_static_color_for_next_pair_line: # Desenha a linha do próximo par (que está entrando) com cor estática e alpha de fade-in text_to_draw_static = ' '.join(line_words) draw_text_with_alpha(image, text_to_draw_static, x, y, font, force_static_color_for_next_pair_line, alpha=current_alpha_to_use) else: # Desenha a linha do par atual (com gradiente de palavra) e alpha de fade-out (se aplicável) draw_current_line_text( image, sentence_to_draw, x, y, font, current_time, subtitles, line_words, line_word_indices, alpha=current_alpha_to_use ) elif subtitles_visible and paired_sentences and current_time < paired_sentences[0]["start_time"]: next_pair_sents = paired_sentences[0]["sentences"] next_positions_list = sentence_positions[0] next_pair_display_start_time = paired_sentences[0]["start_time"] - timedelta(seconds=TRANSITION_DURATION_GAPS) if next_pair_display_start_time <= current_time: opacity = 255 # Opcional: Adicionar um fade-in gradual para o primeiro grupo se TRANSITION_DURATION_GAPS > 0 if TRANSITION_DURATION_GAPS > 0: time_into_transition = (current_time - next_pair_display_start_time).total_seconds() opacity_progress = min(time_into_transition / TRANSITION_DURATION_GAPS, 1.0) opacity = int(255 * opacity_progress) for idx, (sentence_data, line_positions_for_sentence) in enumerate(zip(next_pair_sents, next_positions_list)): if idx >= NUM_FRASES_GRUPO: break # Não desenhar mais linhas do que o grupo permite for line_info in line_positions_for_sentence: x, y, font, _width, line_words, _line_word_indices = line_info text = ' '.join(line_words) draw_text_with_alpha(image, text, x, y, font, FONT_COLOR, alpha=opacity) return image.convert("RGB") def create_video(frame_folder, output_video, audio_file): """Cria um vídeo a partir dos frames gerados e adiciona áudio.""" frame_pattern = os.path.join(frame_folder, "frame_%04d.jpg") duration = get_audio_duration(audio_file) command = [ "ffmpeg", "-y", "-framerate", str(FPS), "-i", frame_pattern, "-i", audio_file, "-c:v", "h264_nvenc", "-preset", "fast", "-b:v", "8M", "-profile:v", "high", "-c:a", "aac", "-profile:a", "aac_low", "-b:a", "448k", "-vf", f"scale=1920:1080", "-shortest", output_video, ] subprocess.run(command, check=True) print(f"Vídeo criado e redimensionado para 1920x1080 com sucesso.") def generate_frames_chunk(args): """ Processa um conjunto de frames do vídeo. Função usada para processamento paralelo. """ ( start_frame, end_frame, subtitles, paired_sentences, gaps, output_dir, num_frames, sentence_positions, progress_queue, audio_duration, fps, is_background_video, current_background_frames_dir, num_actual_background_frames ) = args for frame_index in range(start_frame, end_frame): try: image = process_frame( frame_index, subtitles, paired_sentences, gaps, num_frames, sentence_positions, audio_duration, fps, is_background_video, current_background_frames_dir, num_actual_background_frames ) frame_filename = os.path.join(output_dir, f"frame_{frame_index:04d}.jpg") image.save(frame_filename, quality=95) progress_queue.put(1) except Exception as e: print(f"Erro ao processar o frame {frame_index}: {str(e)}") progress_queue.put(1) def generate_frames_parallel(subtitles, paired_sentences, gaps, output_dir, sentence_positions, audio_duration, is_background_video, current_background_frames_dir, num_actual_background_frames): """ Gera todos os frames do vídeo em paralelo, usando múltiplos núcleos da CPU. """ os.makedirs(output_dir, exist_ok=True) total_duration = audio_duration num_frames = int(total_duration * FPS) + 1 num_processes = multiprocessing.cpu_count() chunk_size = num_frames // num_processes + 1 manager = multiprocessing.Manager() progress_queue = manager.Queue() args_list = [ ( i * chunk_size, min((i + 1) * chunk_size, num_frames), subtitles, paired_sentences, gaps, output_dir, num_frames, sentence_positions, progress_queue, audio_duration, FPS, is_background_video, current_background_frames_dir, num_actual_background_frames ) for i in range(num_processes) ] with multiprocessing.Pool(processes=num_processes) as pool: with tqdm(total=num_frames, unit="frame", desc="Gerando frames") as pbar: results = pool.map_async(generate_frames_chunk, args_list) while not results.ready(): frames_processed = 0 while not progress_queue.empty(): progress_queue.get() frames_processed += 1 if frames_processed > 0: pbar.update(frames_processed) time.sleep(0.1) frames_processed = 0 while not progress_queue.empty(): progress_queue.get() frames_processed += 1 if frames_processed > 0: pbar.update(frames_processed) expected_frames = num_frames generated_frames = len([f for f in os.listdir(output_dir) if f.endswith(".jpg")]) if generated_frames < expected_frames: print(f"Aviso: Apenas {generated_frames} de {expected_frames} frames foram gerados.") else: print(f"Todos os {expected_frames} frames foram gerados com sucesso.") def main(): """Função principal que coordena todo o processo de criação do vídeo.""" try: audio_file = "audio.wav" subtitle_file = "legenda.srt" frames_dir_name = "frames" abs_frames_dir = os.path.abspath(frames_dir_name) abs_background_frames_dir = os.path.abspath(BACKGROUND_FRAMES_DIR) video_extensions = ('.mp4', '.avi', '.mov', '.mkv', '.webm') is_background_video = BACKGROUND_SOURCE.lower().endswith(video_extensions) num_actual_background_frames = 0 print("Processando áudio e legendas...") audio_duration = get_audio_duration(audio_file) subtitles, paired_sentences, gaps = read_subtitles(subtitle_file, audio_duration) if not subtitles or not paired_sentences: raise ValueError("Nenhuma legenda ou sentença foi processada.") if os.path.exists(abs_frames_dir): shutil.rmtree(abs_frames_dir) os.makedirs(abs_frames_dir, exist_ok=True) if is_background_video: if os.path.exists(abs_background_frames_dir): shutil.rmtree(abs_background_frames_dir) extract_background_frames(BACKGROUND_SOURCE, abs_background_frames_dir, IMAGE_SIZE, FPS) try: bg_files = [name for name in os.listdir(abs_background_frames_dir) if os.path.isfile(os.path.join(abs_background_frames_dir, name)) and name.startswith("bg_frame_") and name.endswith(".jpg")] num_actual_background_frames = len(bg_files) if num_actual_background_frames == 0: print(f"Aviso: Nenhum frame de fundo (bg_frame_*.jpg) foi encontrado em '{abs_background_frames_dir}' após a extração. O vídeo usará um fundo preto se o fundo for um vídeo.") else: print(f"Contagem: {num_actual_background_frames} frames de fundo utilizáveis em '{abs_background_frames_dir}'.") except FileNotFoundError: print(f"Aviso: Diretório de frames de fundo '{abs_background_frames_dir}' não encontrado após tentativa de extração. O vídeo usará um fundo preto se o fundo for um vídeo.") num_actual_background_frames = 0 print("Calculando posições das legendas...") sentence_positions = calculate_sentence_positions(paired_sentences) print("Gerando frames do vídeo...") generate_frames_parallel( subtitles, paired_sentences, gaps, abs_frames_dir, sentence_positions, audio_duration, is_background_video, abs_background_frames_dir, num_actual_background_frames ) print("Criando vídeo final...") create_video(abs_frames_dir, "output_video.mp4", audio_file) print("Limpando arquivos temporários...") shutil.rmtree(abs_frames_dir) if is_background_video and os.path.exists(abs_background_frames_dir): shutil.rmtree(abs_background_frames_dir) print("Processamento concluído com sucesso!") except Exception as e: print(f"Erro durante a execução do programa: {str(e)}") traceback.print_exc() if __name__ == "__main__": main()

Pensamento

Understanding user request

The user provided a detailed description of behavior they want to change, particularly involving gradient transitions for lines 3, 4, 5, and 6. The current pattern is that certain lines disappear and others appear at specific points of the gradient. They want to apply this same logic to lines 3 and 4, making them disappear and replace with lines 7 and 8. It seems like they’re asking for an update to the Python code handling these fade transitions. I need to carefully implement this new pattern.

Planning code modifications

The user wants to modify how lines fade and appear based on a cascading sliding group behavior. The pattern they've provided for lines 3 and 4 needs to follow the same logic as the first two lines. This means lines 3 and 4 should be replaced gradually, with new lines fading in when triggers occur. To implement this, I'll need to adjust the logic for slots 2 and 3 and ensure they fade out lines 3 and 4 as the new lines (line 7 and 8) appear. The requested changes will be reflected in the code accordingly.

Clarifying fading logic

The user is asking for a cascading fade effect with lines disappearing and appearing across groups. They want lines 3 and 4 (original group A) to fade in/out based on sentences from the next group (group B). Specifically, they want line 3 to fade when sentence 5 starts and line 7 to appear by the time sentence 5 reaches its middle. Similarly, line 4 should fade at sentence 6, with line 8 appearing in its place. They want this fade logic applied consistently across groups and within specific sentence transitions.

Analyzing code logic for fading

The user seems to be describing a fading effect where certain lines should disappear and new ones appear at specific points within a set of sentences. It looks like the current code is almost correct, but there are edge cases and minor modifications that may be needed. Specifically, triggers for fading and appearing need to be adjusted for certain slots, like slot2 and slot3. For instance, the fade for line3 should start when sentence5 begins and the corresponding appearance of a new line (line7) should occur.

Thought for 1m 48s

Resposta

A lógica de transição dos “slots” 2 e 3 (linhas 3 e 4 na tela) precisa ser disparada pelo próprio grupo que acabou de entrar, e não - como está hoje - pelo próximo grupo. Em outras palavras:

Momento - sentença em execuçãoLinha que someLinha que aparece
início da sentença 5linha 3
meio da sentença 5linha 7
início da sentença 6linha 4
meio da sentença 6linha 8

Para conseguir isso basta identificar também o par anterior e usar o par atual como gatilho dos “fade-out” / “fade-in” para os slots 2 e 3.

python
# (logo depois de current_pair_sentences = pair_data["sentences"]) previous_pair_sentences = ( paired_sentences[current_pair_index - 1]["sentences"] if current_pair_index > 0 else None )

1. Ajuste na escolha das sentenças-gatilho

Substitua o bloco que define trigger_sentence_for_fade_current / trigger_sentence_for_appear_next apenas para k==2 e k==3 pelo seguinte:

python
elif k == 2: # SLOT 2 (linha 3 &rarr; linha 7) if previous_pair_sentences and len(previous_pair_sentences) >= 3 \ and len(current_pair_sentences) >= 3: trigger_sentence_for_fade_current = current_pair_sentences[0] # sent. 5 trigger_sentence_for_appear_next = current_pair_sentences[0] # mesmo tempo sentence_to_fade_index = 2 # linha 3 do par anterior sentence_to_appear_index = 2 # linha 7 do par atual trigger_is_from_current_pair = True elif k == 3: # SLOT 3 (linha 4 &rarr; linha 8) if previous_pair_sentences and len(previous_pair_sentences) >= 4 \ and len(current_pair_sentences) >= 4: trigger_sentence_for_fade_current = current_pair_sentences[1] # sent. 6 trigger_sentence_for_appear_next = current_pair_sentences[1] sentence_to_fade_index = 3 # linha 4 do par anterior sentence_to_appear_index = 3 # linha 8 do par atual trigger_is_from_current_pair = True

2. Linhas que serão realmente desenhadas

Ainda dentro do laço for display_slot_idx in range(NUM_FRASES_GRUPO): troque a parte que decide qual sentença desenhar para os slots 2 e 3 por esta lógica condensada:

python
if display_slot_idx in (2, 3): # --- FADE-IN: linha do par **atual** -------------------- if should_draw_next_pair_line[display_slot_idx]: sentence_to_draw = current_pair_sentences[sentence_to_appear_index] line_positions_to_use = current_positions_for_pair[display_slot_idx] current_alpha_to_use = alpha_next_pair_line[display_slot_idx] is_drawing_from_next_pair = True force_static_color_for_next_pair_line = FONT_COLOR # --- FADE-OUT: linha do **par anterior** ---------------- elif should_draw_line_current[display_slot_idx]: sentence_to_draw = previous_pair_sentences[sentence_to_fade_index] line_positions_to_use = sentence_positions[current_pair_index-1][display_slot_idx] current_alpha_to_use = alpha_line_current[display_slot_idx] is_drawing_from_next_pair = False

(Para os slots 0 e 1 mantenha o código que já existe.)

3. Pequenos retoques

  • Garanta que previous_pair_sentences e sentence_positions[current_pair_index-1] existam antes de usá-los (já mostrado no if acima).
  • Não esqueça de declarar sentence_to_fade_index e sentence_to_appear_index antes de chegar ao bloco de desenho (pode iniciar ambos como None fora do for k in ...).

Com essas três alterações:

  • linha 3 some no exato início da sentença 5 e linha 7 aparece no meio dela;
  • linha 4 some no início da sentença 6 e linha 8 aparece no meio dela;

reproduzindo exatamente a mesma dinâmica que você já tem para os pares (linha 1 → 5, linha 2 → 6).

Compartilhar esta Q&A