Atualmente é feito assim: - Quando o gradiente ch...
Criado em: 9 de maio de 2025
Criado em: 9 de maio de 2025
Atualmente é feito assim:
Faça essa mesma coisa também para as linhas 3 e 4:
pythonimport 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()
Segue um “mini-diff” focado apenas no bloco for k in range(NUM_FRASES_GRUPO) dentro de process_frame
.
Ele estende a lógica que você já tinha para os slots 0 e 1 (linhas 1 e 2) para os slots 2 e 3 (linhas 3 e 4):
pythonfor k in range(NUM_FRASES_GRUPO): # 0-based: 0,1,2,3 trigger_sentence_for_fade_current = None trigger_sentence_for_appear_next = None trigger_is_from_current_pair = False # → de onde vem o gatilho # ──────────── SLOTS 0 e 1 ──────────── if k == 0: # linha-1 some na linha-3 if len(current_pair_sentences) >= 3: trigger_sentence_for_fade_current = current_pair_sentences[2] # linha-3 trigger_sentence_for_appear_next = current_pair_sentences[2] trigger_is_from_current_pair = True elif k == 1: # linha-2 some na linha-4 if len(current_pair_sentences) >= 4: trigger_sentence_for_fade_current = current_pair_sentences[3] # linha-4 trigger_sentence_for_appear_next = current_pair_sentences[3] trigger_is_from_current_pair = True # ──────────── ★ NOVO: SLOTS 2 e 3 ★ ──────────── elif k == 2: # linha-3 some na linha-5 if next_pair_sentences_list: # precisa existir o próximo grupo trigger_sentence_for_fade_current = next_pair_sentences_list[0] # linha-5 trigger_sentence_for_appear_next = next_pair_sentences_list[0] # idem trigger_is_from_current_pair = False elif k == 3: # linha-4 some na linha-6 if next_pair_sentences_list and len(next_pair_sentences_list) > 1: trigger_sentence_for_fade_current = next_pair_sentences_list[1] # linha-6 trigger_sentence_for_appear_next = next_pair_sentences_list[1] trigger_is_from_current_pair = False
Fade-out da linha 3 (slot 2) e linha 4 (slot 3)
O gatilho é o início das linhas 5 e 6 (primeiras do próximo grupo), exatamente como já acontece para as linhas 1 e 2 em relação às linhas 3 e 4.
Fade-in das linhas 7 e 8
No momento em que o código decide qual frase desenhar no display-slot k
, ele usa:
pythonnext_pair_sentences_list[display_slot_idx]
Portanto, enquanto o gatilho é a sentença 0 ou 1 do próximo grupo, a frase realmente desenhada para o slot 2 e 3 é, respectivamente, o índice 2 e 3 (linhas 7 e 8). Isso atende ao requisito de “mostrar a próxima linha (7/8) na metade do gradiente da linha 5/6”.
Não é preciso alterar o resto (do cálculo de alpha, etc.), pois a lógica genérica de fade-out/fade-in já usa trigger_sentence_for_fade_current
e trigger_sentence_for_appear_next
configurados acima.
Com essa pequena extensão, o comportamento fica:
Evento | Some | Aparece no lugar |
---|---|---|
Gradiente atinge linha 3 | 1 | 5 |
Metade do gradiente da linha 3 | – | 5 (fade-in) |
Gradiente atinge linha 4 | 2 | 6 |
Metade do gradiente da linha 4 | – | 6 (fade-in) |
Gradiente atinge linha 5 | 3 | 7 |
Metade do gradiente da linha 5 | – | 7 (fade-in) |
Gradiente atinge linha 6 | 4 | 8 |
Metade do gradiente da linha 6 | – | 8 (fade-in) |
Basta colar essas condições adicionais e recompilar; o restante do fluxo permanece inalterado.
Fiz o teste mas continua a mesma coisa, quando o gradiente chega no final da linha 4, e vai para a posição da linha 1 (agora linha 5), simplesmente as linhas 3 e linha 4 são substituídas imediatamente, sem seguir o que eu pedi.
O “sumi‑troca” imediato acontece porque, no laço de desenho, a linha do próximo grupo passa a ter prioridade assim que o seu α (opacidade) fica > 0. Como o α da linha antiga já está caindo, o resultado visual é um “pulo”.
Você não precisa mexer de novo nos gatilhos (k == 2 e k == 3) – eles estão corretos.
Basta alterar a regra que decide qual linha desenhar dentro do bloco
python# Loop de DESENHO: iterar sobre cada slot de display for display_slot_idx in range(NUM_FRASES_GRUPO): ...
Substitua apenas a condição que escolhe a linha do próximo grupo:
diff- # PRIORIDADE ATUAL (qualquer α > 0 já troca): - if should_draw_next_pair_line[display_slot_idx] and alpha_next_pair_line[display_slot_idx] > 0: + # NOVA PRIORIDADE – só troca quando a nova linha + # estiver (pelo menos) tão visível quanto a antiga. + if (should_draw_next_pair_line[display_slot_idx] + and alpha_next_pair_line[display_slot_idx] > alpha_line_current[display_slot_idx]):
Durante o fade‑in da nova linha (α subindo de 0 → 255) e o fade‑out da antiga (α descendo de 255 → 0), o código compara os dois valores:
alpha_next_pair_line
≤ alpha_line_current
, continua mostrando a linha antiga (com o gradiente ativo).Isso garante que:
Recompile e rode novamente; você deverá ver uma sobreposição suave, sem o “salto” instantâneo.
fiz o teste e não, o problema ainda não foi resolvido, continua a mesma coisa, quando o gradiente chega no final da linha 4, e vai para a linha seguinte, imediatamente as linhas 3 e 4 são substituidas ao mesmo tempo..
O “pulo” continua acontecendo porque, no instante em que o current_pair_index
muda (ou seja, quando o vídeo entra no grupo 5-8), as linhas 3-4 pertencentes ao grupo anterior simplesmente deixam de ser consideradas.
Para que elas persistam dentro do grupo seguinte e só desapareçam na hora certa, você precisa trazer explicitamente as “sobras” do par anterior para o laço de desenho dos slots 2 e 3.
A correção exige só duas pequenas adições dentro de process_frame
:
Logo depois de obter pair_data
, coloque:
python# Sentenças & posições do GRUPO ANTERIOR – necessárias p/ slots 2-3 prev_pair_sentences_list = None prev_pair_positions_list = None if current_pair_index > 0: prev_pair_sentences_list = paired_sentences[current_pair_index - 1]["sentences"] prev_pair_positions_list = sentence_positions[current_pair_index - 1]
Substitua apenas o bloco que decide o que desenhar para display_slot_idx in (2, 3)
pelo trecho abaixo (o resto do laço fica igual):
python# ───────────── SLOTS 2 e 3 ───────────── if display_slot_idx in (2, 3): old_idx = display_slot_idx # 2→linha-3, 3→linha-4 # ------------- FASE 1 – linha antiga (grupo anterior) ------------- if (prev_pair_sentences_list and old_idx < len(prev_pair_sentences_list) and prev_pair_positions_list and old_idx < len(prev_pair_positions_list)): # gatilhos para fade-out da linha antiga trigger_idx_in_curr = display_slot_idx - 2 # 0 p/ slot-2 (linha-5), 1 p/ slot-3 (linha-6) if trigger_idx_in_curr < len(current_pair_sentences): fade_out_start = current_pair_sentences[trigger_idx_in_curr]["start_time"] # metade do “gradiente” da sentença-gatilho next_trigger_start = (current_pair_sentences[trigger_idx_in_curr + 1]["start_time"] if trigger_idx_in_curr + 1 < len(current_pair_sentences) else pair_data["end_time"]) fade_out_end = fade_out_start + (next_trigger_start - fade_out_start) / 2 if current_time < fade_out_end: # ainda estamos na transição ⇒ desenhe a LINHA ANTIGA progress = (current_time - fade_out_start).total_seconds() / ( fade_out_end - fade_out_start).total_seconds() alpha_old = int(group_alpha * max(0.0, 1.0 - progress)) old_sentence = prev_pair_sentences_list[old_idx] old_line_pos = prev_pair_positions_list[old_idx] for line_info in old_line_pos: x, y, font, _w, words, idxs = line_info draw_current_line_text( image, old_sentence, x, y, font, current_time, subtitles, words, idxs, alpha=alpha_old ) continue # slot resolvido – passa p/ próximo display_slot_idx # ------------- FASE 2 – linha nova (grupo atual) ------------- # se chegou aqui, já terminou o fade-out da linha antiga # → aplique a regra normal (linha 7/8 com fade-in)
(Os slots 0-1 continuam como estão.)
Slots 2 e 3 olham primeiro para o grupo anterior.
Enquanto o relógio estiver entre fade_out_start
(início da linha 5 ou 6) e fade_out_end
(meio da linha 5 ou 6) eles continuam desenhando linha 3 ou 4, com α decrescendo suavemente de 255 → 0.
Ao ultrapassar fade_out_end
, o bloco continue
não é executado; o código cai na lógica já existente, que aplica o fade-in da linha 7 ou 8 no slot correspondente.
Dessa forma:
Se o fade-out continuar rápido demais para o seu gosto, aumente
FADE_OUT_DURATION_LINES
(0.5 – 0.7 s costuma ficar bem) ou simplesmente substitua a equação deprogress
por algo comoprogress ** 0.7
para desacelerar visualmente o início do desvanecimento.
Compile novamente: você verá que as linhas 3 e 4 permanecem vivas até o ponto exato solicitado, sem sumir abruptamente.