Seria possível modificar o script para que a frase atual e a próxima vai passando igual "créditos finais",  que vai subindo até sumirem no topo do video?

Após ter pensado a fundo passo a passo, envie o código completo com as atualizações necessárias.


```python
import re
from datetime import timedelta
from PIL import Image, ImageDraw, ImageFont, ImageFilter
import subprocess
import os
import multiprocessing
from functools import lru_cache
import shutil
from tqdm import tqdm
import time

# Configurações
IMAGE_SIZE = (1920, 1080)
FONT_COLOR = "#FBF04A"
HIGHLIGHT_COLOR = "#FF6156"
NEXT_SENTENCE_COLOR = "#FF6156"  # Cor para o próximo verso
OUTLINE_COLOR = "#000000"
UPPER_CASE = False
FPS = 10
FADE_DURATION = 3
FONT_SIZE_INCOMPLETE = 65
FONT_SIZE_COMPLETE = 65
POSICAO = 1  # 1: centralizado, 2: parte inferior, 3: parte superior
FONT_PATH = r"C:\Users\lucas\Downloads\LilitaOne-Regular.ttf"


def hex_to_rgb(hex_color):
    return tuple(int(hex_color.lstrip('#')[i:i+2], 16) for i in (0, 2, 4))


def parse_time(time_str):
    match = re.match(r'(\d+):(\d+):(\d+)', time_str)
    if match:
        minutes, seconds, milliseconds = map(int, match.groups())
        return timedelta(minutes=minutes, seconds=seconds, milliseconds=milliseconds)
    return timedelta(0)


def get_audio_duration(audio_file):
    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):
    subtitles = []
    with open(filename, 'r', encoding='utf-8') as file:
        for line in file:
            match = re.match(r'\[(\d+:\d+:\d+)\] (.+)', line.strip())
            if match:
                time_str, word = match.groups()
                subtitles.append((parse_time(time_str), word.upper() if UPPER_CASE else word))
    
    audio_duration = get_audio_duration('audio.wav')
    final_timestamp = timedelta(seconds=audio_duration)
    subtitles.append((final_timestamp, ""))

    # Processar legendas em frases completas
    sentences = []
    sentence_words = []
    sentence_start_time = None
    sentence_start_index = None

    for i, (time, word) in enumerate(subtitles):
        if word == "":
            continue  # Ignora palavras vazias
        if word[0].isupper() and (i == 0 or subtitles[i - 1][1].endswith('.')):
            # Início de uma nova frase
            if sentence_words:
                # Adiciona a frase anterior à lista
                sentences.append({
                    'start_time': sentence_start_time,
                    'end_time': subtitles[i - 1][0],  # Tempo final é o tempo da última palavra
                    'words': sentence_words,
                    'start_index': sentence_start_index,
                    'end_index': i,  # Exclusivo
                })
            # Inicia nova frase
            sentence_words = [word]
            sentence_start_time = time
            sentence_start_index = i
        else:
            sentence_words.append(word)
    # Adiciona a última frase
    if sentence_words:
        sentences.append({
            'start_time': sentence_start_time,
            'end_time': subtitles[-1][0],
            'words': sentence_words,
            'start_index': sentence_start_index,
            'end_index': len(subtitles),
        })

    return subtitles, sentences


@lru_cache(maxsize=1000)
def create_gradient_word(word, width, height, start_color, end_color, progress, font_size):
    font = ImageFont.truetype(FONT_PATH, font_size)
    ascent, descent = font.getmetrics()
    total_height = ascent + descent

    padding = 20
    word_image = Image.new('RGBA', (int(width) + padding, total_height + padding), (0, 0, 0, 0))
    draw = ImageDraw.Draw(word_image)
    
    x, y = padding // 2, ascent

    # Desenhar o texto principal sem sombra
    draw.text((x, y), word, font=font, fill=hex_to_rgb(end_color), anchor="ls")
    
    mask = Image.new('L', word_image.size, 0)
    mask_draw = ImageDraw.Draw(mask)
    mask_draw.text((x, y), word, font=font, fill=255, anchor="ls")
    mask = mask.filter(ImageFilter.GaussianBlur(radius=1.0))
    
    gradient = Image.new('RGBA', word_image.size, (0, 0, 0, 0))
    start_color_rgb = hex_to_rgb(start_color)
    end_color_rgb = hex_to_rgb(end_color)
    
    for x_coord in range(word_image.width):
        ratio = max(0, min(1, (x_coord / (width + padding) - progress) / 0.1))
        r = int(start_color_rgb[0] + (end_color_rgb[0] - start_color_rgb[0]) * ratio)
        g = int(start_color_rgb[1] + (end_color_rgb[1] - start_color_rgb[1]) * ratio)
        b = int(start_color_rgb[2] + (end_color_rgb[2] - start_color_rgb[2]) * ratio)
        color = (r, g, b, 255)
        ImageDraw.Draw(gradient).line([(x_coord, 0), (x_coord, word_image.height)], fill=color)
    
    gradient.putalpha(mask)
    
    return Image.alpha_composite(word_image, gradient)


def arrange_sentence_into_lines(sentence_words, font, max_width, space_width):
    lines = []
    current_line = []
    current_line_width = 0
    for word in sentence_words:
        word_width = font.getlength(word)
        if current_line_width + word_width + (space_width if current_line else 0) > max_width:
            lines.append(current_line)
            current_line = [word]
            current_line_width = word_width + space_width
        else:
            if current_line:
                current_line_width += word_width + space_width
            else:
                current_line_width = word_width
            current_line.append(word)
    if current_line:
        lines.append(current_line)
    return lines


def process_frame(frame_index, subtitles, sentences, background_image, num_frames):
    current_time = timedelta(seconds=frame_index / FPS)
    image = background_image.copy()

    # Inicializar fonte
    current_font_size = FONT_SIZE_COMPLETE
    font = ImageFont.truetype(FONT_PATH, current_font_size)
    space_width = font.getlength(' ')
    line_spacing = 10
    max_width = IMAGE_SIZE[0] - 40

    # Encontrar o índice da frase atual
    current_sentence_index = 0
    for i, s in enumerate(sentences):
        if s['start_time'] <= current_time < s['end_time']:
            current_sentence_index = i
            break
        elif current_time >= s['end_time']:
            current_sentence_index = i
        else:
            break

    # Obter frases atual e seguinte
    current_sentence = sentences[current_sentence_index]
    next_sentence = sentences[current_sentence_index + 1] if current_sentence_index + 1 < len(sentences) else None

    # Organizar frases em linhas
    # Preparar variáveis para calcular altura total
    total_height = 0
    sentence_spacing = 20

    current_lines = arrange_sentence_into_lines(current_sentence['words'], font, max_width, space_width)
    current_total_height = len(current_lines) * (current_font_size + line_spacing)
    total_height += current_total_height + sentence_spacing

    next_lines = []
    next_total_height = 0
    if next_sentence is not None:
        next_lines = arrange_sentence_into_lines(next_sentence['words'], font, max_width, space_width)
        next_total_height = len(next_lines) * (current_font_size + line_spacing)
        total_height += next_total_height + sentence_spacing

    total_height -= sentence_spacing  # Remove espaçamento extra após a última frase

    # Determinar posição inicial y
    if POSICAO == 1:  # Centralizado
        y = (IMAGE_SIZE[1] - total_height) // 2
    elif POSICAO == 2:  # Parte inferior
        y = IMAGE_SIZE[1] - total_height - 50
    else:  # Parte superior
        y = 50

    # Desenhar frase atual
    word_index = current_sentence['start_index']
    for line in current_lines:
        line_width = sum(font.getlength(word) for word in line) + space_width * (len(line) -1)
        x = (IMAGE_SIZE[0] - line_width) // 2

        for word in line:
            word_time = subtitles[word_index][0]
            next_word_time = subtitles[word_index + 1][0] if word_index + 1 < len(subtitles) else word_time + timedelta(seconds=2)

            ascent_word, descent_word = font.getmetrics()
            text_width = font.getlength(word)
            y_centered = y + ascent_word

            if current_time >= next_word_time:
                color = FONT_COLOR
                progress = 1.0
            elif current_time >= word_time:
                word_duration = (next_word_time - word_time).total_seconds()
                progress = min(max((current_time - word_time).total_seconds() / word_duration, 0.0), 1.0)
                color = FONT_COLOR
            else:
                color = HIGHLIGHT_COLOR
                progress = 1.0

            gradient_image = create_gradient_word(word, text_width, current_font_size, color, HIGHLIGHT_COLOR, progress, current_font_size)
            image.paste(gradient_image, (int(x - 5), int(y_centered - ascent_word - 5)), gradient_image)

            x += text_width + space_width
            word_index += 1

        y += current_font_size + line_spacing
    y += sentence_spacing  # Espaço entre frases

    # Desenhar próxima frase
    if next_sentence is not None:
        word_index = next_sentence['start_index']
        for line in next_lines:
            line_width = sum(font.getlength(word) for word in line) + space_width * (len(line) -1)
            x = (IMAGE_SIZE[0] - line_width) // 2

            for word in line:
                ascent_word, descent_word = font.getmetrics()
                text_width = font.getlength(word)
                y_centered = y + ascent_word

                color = NEXT_SENTENCE_COLOR
                progress = 1.0

                gradient_image = create_gradient_word(word, text_width, current_font_size, color, color, progress, current_font_size)
                image.paste(gradient_image, (int(x -5), int(y_centered - ascent_word -5)), gradient_image)

                x += text_width + space_width
                word_index += 1

            y += current_font_size + line_spacing

    # Aplicar fade in e fade out
    if frame_index < FADE_DURATION * FPS:
        fade_opacity = frame_index / (FADE_DURATION * FPS)
        fade_overlay = Image.new('RGBA', image.size, (0, 0, 0, int(255 * (1 - fade_opacity))))
        image = Image.alpha_composite(image.convert('RGBA'), fade_overlay).convert('RGB')
    elif frame_index > num_frames - FADE_DURATION * FPS:
        fade_opacity = (num_frames - frame_index) / (FADE_DURATION * FPS)
        fade_overlay = Image.new('RGBA', image.size, (0, 0, 0, int(255 * (1 - fade_opacity))))
        image = Image.alpha_composite(image.convert('RGBA'), fade_overlay).convert('RGB')

    # Converter para RGB para salvar como JPEG
    return image.convert('RGB')


def generate_frames_chunk(args):
    start_frame, end_frame, subtitles, sentences, output_dir, background_image, num_frames, progress_queue = args

    for frame_index in range(start_frame, end_frame):
        try:
            image = process_frame(frame_index, subtitles, sentences, background_image, num_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)  # Ainda incrementamos o progresso para evitar travamentos

    return end_frame - start_frame


def generate_frames_parallel(subtitles, sentences, output_dir, background_image):
    total_duration = (subtitles[-1][0] + timedelta(seconds=2)).total_seconds()
    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, sentences, output_dir, background_image, num_frames, progress_queue)
        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 = progress_queue.qsize()
                if frames_processed > 0:
                    pbar.update(frames_processed)
                    for _ in range(frames_processed):
                        progress_queue.get()
                time.sleep(0.1)

            frames_processed = progress_queue.qsize()
            if frames_processed > 0:
                pbar.update(frames_processed)

    # Verificar se todos os frames foram gerados
    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 create_video(frame_folder, output_video, audio_file):
    frame_pattern = os.path.join(frame_folder, 'frame_%04d.jpg')
    duration = get_audio_duration(audio_file)
    fade_out_start = duration - FADE_DURATION

    command = [
        'ffmpeg',
        '-y',
        '-framerate', str(FPS),
        '-i', frame_pattern,
        '-i', audio_file,
        '-c:v', 'h264_nvenc',
        '-preset', 'medium',
        '-rc', 'vbr_hq',
        '-cq', '23',
        '-b:v', '10M',
        '-maxrate', '20M',
        '-bufsize', '20M',
        '-profile:v', 'high',
        '-c:a', 'aac',
        '-profile:a', 'aac_low',
        '-b:a', '448k',
        '-vf', f"fade=t=in:st=0:d={FADE_DURATION},fade=t=out:st={fade_out_start}:d={FADE_DURATION}",
        '-shortest',
        output_video
    ]
    subprocess.run(command, check=True)


def main():
    try:
        subtitles, sentences = read_subtitles('legenda.srt')
        output_dir = 'frames'
        os.makedirs(output_dir, exist_ok=True)

        background_image_path = r"C:\Users\lucas\OneDrive\Documentos\capa.jpg"
        background_image = Image.open(background_image_path).convert("RGBA").resize(IMAGE_SIZE)

        generate_frames_parallel(subtitles, sentences, output_dir, background_image)
        create_video(output_dir, 'output_video.mp4', 'audio.wav')
        shutil.rmtree(output_dir)
    except Exception as e:
        print(f"Erro durante a execução do programa: {str(e)}")


if __name__ == "__main__":
    main()
```