Communitygithub.com

py-shorts-studio

Gera, roteiriza, anima e renderiza vídeos virais verticais de 60fps, YouTube Shorts e Reels, com legendas karaokê ativadas por palavra, efeitos sonoros processuais e codificação direta via pipe do FFmpeg.

O que é py-shorts-studio?

O `py-shorts-studio` é um framework Python de alta performance para criar vídeos verticais (1080x1920, 9:16) explicativos em motion graphics a 60 FPS. Ele otimiza o processo de criação ao transmitir quadros vetoriais PyCairo diretamente para o stdin do FFmpeg, eliminando a necessidade de E/S de disco, sincroniza narrações com legendas karaokê ativadas por palavra, sintetiza efeitos sonoros personalizados e mixa áudio multicanal. O recurso mais inovador é o loop de retenção infinita: um casamento visual perfeito entre o primeiro e o último quadro, combinado a uma ponte de frase gramatical contínua, garantindo que o vídeo se repita sem que o espectador perceba qualquer corte (ideal para YouTube Shorts e Instagram Reels). Suporta qualquer tópico (história, engenharia, ciência, economia, tecnologia, sobrevivência ou mistérios) e oferece uma API completa com renderizador, matemática de easing, primitivas vetoriais reutilizáveis, legendas karaokê, síntese de voz e mixagem de áudio. É uma ferramenta completa para criadores de conteúdo que desejam produzir vídeos virais de alta qualidade com mínimo esforço manual, utilizando uma arquitetura modular e eficiente.

Funciona com~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/Tanush-Gupta1812/py-shorts-studio/tree/HEAD/.agents/skills/py-shorts-studio

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

Py-Shorts-Studio: Mecanismo de Explicação Vetorial 2D Viral

py-shorts-studio é um framework Python de alta performance para criar Shorts explicativos verticais (1080x1920, 9:16) em motion graphics a 60 FPS. Ele elimina a E/S de disco transmitindo quadros vetoriais PyCairo diretamente para o stdin do FFmpeg, alinha narrações com legendas karaokê ativadas por palavra, sintetiza efeitos sonoros personalizados e mixa áudio multicanal.


1. Loop de Casamento Visual e Áudio Perfeito (Retenção Infinita)

Para maximizar o Percentual Médio Assistido (>100% de retenção em loop no YouTube Shorts e Instagram Reels), cada Short gerado DEVE aplicar tanto uma ponte de frase de áudio quanto um casamento visual de quadros pixel a pixel.

A. A Regra do Script com Ponte de Frase

Quando o YouTube repete um Short, o final do áudio conecta-se instantaneamente ao início. O script deve explorar isso:

  1. Gancho de Abertura: DEVE começar imediatamente com uma pergunta incisiva de Por que / Como / O que.
    • Exemplo: "Por que a armadura do Homem de Ferro mataria Tony Stark em três segundos?"
    • Exemplo: "Por que as rodovias modernas desmoronam em quinze anos enquanto as estradas romanas duram dois mil?"
  2. Frase de Transição Final: A última frase DEVE terminar com uma frase de transição antecipatória, em vez de uma despedida definitiva ou pedido de inscrição:
    • Exemplo: "...é por isso que as pessoas ainda perguntam,"
    • Exemplo: "...então da próxima vez que alguém te perguntar,"
    • Exemplo: "...e isso explica,"
  3. Loop Gramatical Contínuo: Quando o vídeo faz o loop da frase de transição para a pergunta de abertura, forma-se uma única frase gramatical contínua:

    "...é por isso que as pessoas ainda perguntam — Por que a armadura do Homem de Ferro mataria Tony Stark em três segundos?"

  4. Silêncio Final Zero:
    • duracao_total = duracao_audio + 0.05 (NUNCA adicione 0.4s ou 0.75s de silêncio no final).
    • Qualquer pausa no final quebra a ilusão de um loop infinito. O loop de áudio deve atingir a primeira palavra do reinício sem nenhum intervalo perceptível.

B. Casamento Visual de Quadro Zero Pixel a Pixel

O primeiro quadro ($t = 0.0\text{s}$) e o último quadro ($t = \text{duracao_total}$) devem ser visualmente idênticos para que o espectador não consiga detectar quando o vídeo se repete.

Padrão de Implementação: desenha_estado_ancora

Cada script de projeto deve definir uma função desenha_estado_ancora(ctx, zoom=1.0, pan=(0, 0)) contendo o visual "Herói" principal e o banner superior.

def desenha_estado_ancora(ctx: cairo.Context, zoom: float = 1.0, pan: tuple[float, float] = (0.0, 0.0)) -> None:
    """Renderiza o diagrama herói mestre e o banner superior em um centro de zoom/pan arbitrário.

    Usado tanto pela Cena 1 (zoom out de abertura) quanto pela cena final (zoom in de fechamento)
    para garantir um casamento de corte pixel a pixel através do limite do loop.
    """
    ctx.save()
    # Transformação centrada na origem (centro de 1080x1920 é 540, 960)
    ctx.translate(540.0 + pan[0], 960.0 + pan[1])
    ctx.scale(zoom, zoom)
    ctx.translate(-540.0, -960.0)

    # 1. Fundo
    desenha_fundo_escuro(ctx, 1080, 1920)

    # 2. Banner Superior
    desenha_banner_superior(ctx, "TÍTULO DO TÓPICO HERÓI", "EXPLICADOR DE ENGENHARIA", progresso=1.0, tema="escuro")

    # 3. Visual Herói / Diagrama na Zona de Ação
    # (Desenhe o diagrama central, corte transversal ou aparelho aqui)
    ...
    ctx.restore()

Dinâmica de Zoom do Casamento (0.0s – 1.2s e Últimos 1.2s)

  • Abertura ($t = 0.0\text{s} \to 1.2\text{s}$):
    • Começa em $t = 0.0\text{s}$ com zoom máximo no diagrama focal em zoom = 1.45.
    • Durante $0.0\text{s} \to 1.2\text{s}$, suaviza para a visão ampla (zoom = 1.0) usando ease_out_cubic:
      if tempo_seg <= 1.2:
          zoom_t = ease_out_cubic(tempo_seg / 1.2)
          zoom_atual = lerp(1.45, 1.0, zoom_t)
          desenha_estado_ancora(ctx, zoom=zoom_atual)
      
  • Fechamento ($t = \text{duracao_total} - 1.2\text{s} \to \text{duracao_total}$):
    • Transiciona suavemente de volta para o mesmo diagrama herói.
    • Durante o último 1.2s, aplica zoom de zoom = 1.0 de volta para zoom = 1.45 usando ease_in_out_cubic:
      t_inicio_fechamento = duracao_total - 1.2
      if tempo_seg >= t_inicio_fechamento:
          zoom_t = ease_in_out_cubic((tempo_seg - t_inicio_fechamento) / 1.2)
          zoom_atual = lerp(1.0, 1.45, zoom_t)
          desenha_estado_ancora(ctx, zoom=zoom_atual)
      
  • Resultado: O quadro em $t = 0.0\text{s}$ (zoom = 1.45) e o quadro em $t = \text{duracao_total}$ (zoom = 1.45) são pixel a pixel idênticos.

2. A Fórmula de Roteiro em 4 Partes para Alta Retenção

Shorts de alta retenção seguem este arco narrativo estrito:

ParteTempo (Short 45–55s)Tempo (Short 25–30s)Propósito e Execução
1. Gancho do Paradoxo0.0s – 4.0s0.0s – 5.0sApresente um contraste nítido e contraintuitivo ou um mistério impossível começando com Por que / Como / O que. Proíba encheção de lingüiça, introduções ou saudações.
2. Restrição / Escalação4.0s – 20.0s5.0s – 12.0sEscale o problema com números concretos, riscos históricos ou perigos ambientais. Apresente métricas comparativas ou barras de HUD de status.
3. A Virada Engenhosa20.0s – 45.0s12.0s – 22.0sO segredo arquitetônico ou científico revelado. Detalhe os mecanismos de engenharia usando diagramas animados, cortes, rotas ou camadas de componentes.
4. Recompensa e Ponte de TransiçãoÚltimos 5.0sÚltimos 4.0sEntregue a recompensa do "aha!" e termine com uma ponte de frase de transição que faça o loop de volta ao gancho de abertura com silêncio final zero.
  • Ritmo de Fala: 150–170 palavras por minuto (PPM).
  • Contagens de Palavras Alvo:
    • Short 45–55s: 110–145 palavras.
    • Short 25–30s: 65–75 palavras.

3. Regras Visuais e de Layout

  1. Especificações da Tela:
    • Resolução: 1080x1920 (proporção 9:16 vertical).
    • Taxa de Quadros: 60 FPS constante.
    • Precisão Vetorial: Todas as formas, ícones e linhas desenhados via PyCairo para anti-aliasing nítido.
  2. Zonas Seguras (Crucial para UI Móvel):
    • Zona Segura Superior ($y \ge 130$): Coloque títulos, pílulas de capítulo e banners superiores entre $y = 130$ e $y = 260$ para evitar a alça do canal e sobreposições da UI superior.
    • Zona de Ação Central ($y = 280$ a $y = 1420$): Todos os gráficos focais, cortes, rotas de mapa e emblemas de estatísticas ficam aqui.
    • Zona Segura Inferior de Legendas: As legendas são ancoradas em $y = 1480$–$1540$. Mantenha os elementos focais de fundo acima de $y = 1400$ para evitar sobreposição com o texto da legenda. Evite detalhes visuais essenciais nos 380px inferiores (onde residem os botões de título, som e inscrição no YouTube e TikTok).
  3. Regra de Ritmo (Mudanças de Estado a Cada 3–5 Segundos):
    • A atenção do espectador decai rapidamente. Mude o estado visual, o foco da câmera ou acione uma animação a cada 3 a 5 segundos (ex.: pop de emblema, preenchimento de métrica de HUD, progressão de caminho ou revelação de camada).
  4. Design de Som Sincronizado com Palavras Exatas:
    • Nunca adivinhe os tempos dos efeitos sonoros.
    • Execute gerar_narracao primeiro para produzir palavras.json.
    • Inspecione palavras.json em busca de palavras-chave de gatilho (ex.: números, verbos de ação, mudanças de cena) e coloque as indicações de whoosh, pop e thud no start_sec exato dessas palavras.

4. Referência Completa da API do Mecanismo

Todos os módulos são exportados limpos do engine:

from engine import (
    # Renderizador
    VideoRenderer,
    # Matemática de Easing
    clamp, lerp, ease_out_cubic, ease_in_out_cubic, spring_pop,
    cubic_bezier_point, cubic_bezier_tangent, cubic_bezier_angle,
    # Primitivas Vetoriais 2D
    draw_parchment_bg, draw_dark_bg, draw_top_banner, draw_map_route,
    draw_hud_bars, draw_stat_badge, draw_rounded_rect, draw_card_shadow,
    # Legendas Karaokê Ativadas por Palavra
    draw_karaoke_captions, chunk_words,
    # Síntese, Mixagem e Multiplexação de Áudio
    generate_voiceover, generate_voiceover_async,
    ensure_default_sfx, get_downloads_dir, mix_final_audio, mux_video_audio,
)

VideoRenderer (engine.renderer)

class VideoRenderer:
    def __init__(
        self,
        output_path: Union[str, Path],
        width: int = 1080,
        height: int = 1920,
        fps: int = 60,
        crf: int = 18,
        preset: str = "fast",
        audio_path: Optional[Union[str, Path]] = None,
        ffmpeg_bin: Optional[str] = None,
    ) -> None: ...

    def create_surface(self) -> Tuple[cairo.ImageSurface, cairo.Context]: ...
    def write_surface(self, surface: cairo.ImageSurface) -> None: ...
    def render(
        self,
        draw_fn: Callable[[cairo.Context, int, float], None],
        total_frames: Optional[int] = None,
        duration_sec: Optional[float] = None,
        on_progress: Optional[Callable[[int, int], None]] = None,
    ) -> Path: ...

Helpers de Easing (engine.easing)

def clamp(val: float, min_val: float = 0.0, max_val: float = 1.0) -> float: ...
def lerp(a: float, b: float, t: float) -> float: ...
def ease_out_cubic(t: float) -> float: ...
def ease_in_out_cubic(t: float) -> float: ...
def spring_pop(t: float, overshoot: float = 1.2) -> float: ...
def cubic_bezier_point(p0: Point2D | float, p1: Point2D | float, p2: Point2D | float, p3: Point2D | float, t: float) -> Tuple[float, float] | float: ...
def cubic_bezier_tangent(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> Tuple[float, float]: ...
def cubic_bezier_angle(p0: Point2D, p1: Point2D, p2: Point2D, p3: Point2D, t: float) -> float: ...

Primitivas Vetoriais Reutilizáveis (engine.primitives)

def draw_parchment_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920, seed: int = 42) -> None: ...
def draw_dark_bg(ctx: cairo.Context, width: int = 1080, height: int = 1920) -> None: ...
def draw_top_banner(ctx: cairo.Context, title: str, subtitle: Optional[str] = None, progress: float = 1.0, theme: str = "light") -> None: ...
def draw_map_route(ctx: cairo.Context, points: Sequence[Tuple[float, float]], progress: float, color: Tuple[float, float, float] = (0.86, 0.15, 0.15), line_width: float = 8.0, dashed: bool = True, show_arrow: bool = True, pulse_time: float = 0.0) -> None: ...
def draw_hud_bars(ctx: cairo.Context, metrics: List[Dict[str, Any]], x: float, y: float, width: float = 880.0, theme: str = "light") -> None: ...
def draw_stat_badge(ctx: cairo.Context, value_text: str, label_text: str, x: float, y: float, scale: float = 1.0, accent_color: Tuple[float, float, float] = (0.98, 0.80, 0.08), theme: str = "light") -> None: ...
def draw_rounded_rect(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float) -> None: ...
def draw_card_shadow(ctx: cairo.Context, x: float, y: float, w: float, h: float, r: float, blur_layers: int = 4, offset_y: float = 6.0, base_alpha: float = 0.12) -> None: ...

Legendas Karaokê Ativadas por Palavra (engine.captions)

def chunk_words(words: Sequence[Dict[str, Any]], max_words: int = 3, max_chars: int = 18) -> List[Dict[str, Any]]: ...
def draw_karaoke_captions(ctx: cairo.Context, words: Sequence[Dict[str, Any]], current_time: float, center_x: float = 540.0, y: float = 1480.0, font_path: Optional[Union[str, Path]] = None, font_size: int = 76, stroke_width: int = 8, active_color: Tuple[int, int, int] = (250, 204, 21), inactive_color: Tuple[int, int, int] = (255, 255, 255), active_scale: float = 1.12) -> None: ...

Narração, Efeitos Sonoros e Mixagem (engine.tts_align, engine.audio_mixer)

def generate_voiceover(script_text: str, output_audio_path: Union[str, Path], output_words_json: Optional[Union[str, Path]] = None, voice: str = "en-US-AndrewNeural", rate: str = "+12%") -> float: ...
def ensure_default_sfx(sfx_dir: Union[str, Path] = "assets/sfx") -> Dict[str, Path]: ...
def get_downloads_dir() -> Path: ...
def mix_final_audio(voiceover_path: Union[str, Path], sfx_events: Sequence[Tuple[float, str]], output_audio_path: Union[str, Path], total_duration: float, include_ambient: bool = True, sfx_dir: Union[str, Path] = "assets/sfx", ffmpeg_bin: Optional[str] = None) -> Path: ...
def mux_video_audio(video_path: Union[str, Path], audio_path: Union[str, Path], final_output_path: Optional[Union[str, Path]] = None, filename: Optional[str] = None, ffmpeg_bin: Optional[str] = None) -> str: ...

5. Fluxo de Trabalho de Execução Autônoma

Ao receber a tarefa de gerar um Short sobre qualquer tópico, execute estas etapas sequencialmente:

Etapa 1: Composição do Roteiro (Regra da Ponte de Frase)

Escreva um roteiro conciso de 65 a 140 palavras seguindo a fórmula de 4 partes.

  • DEVE começar com uma pergunta incisiva de Por que / Como / O que.
  • DEVE terminar com uma ponte de frase de transição ("...é por isso que as pessoas ainda perguntam,").
  • Escolha um slug de tópico claro (ex.: coliseu_inundacao ou fisica_vibranio).

Etapa 2: Geração de Narração e Timestamps em scratch/

Mantenha output/ limpo armazenando todos os arquivos intermediários em scratch/:

.venv\Scripts\python -c "from engine import generate_voiceover; generate_voiceover('''<roteiro>''', 'scratch/<slug>_vo.mp3', 'scratch/<slug>_palavras.json')"

Inspecione scratch/<slug>_palavras.json para determinar:

  • duracao_audio exata
  • Timestamps de marcos importantes (ex.: onde o gancho termina, onde os números são falados, onde ocorrem a virada e a ponte final).

Etapa 3: Pipeline de Geração de Cenas Roteirizadas

Crie projects/<slug>.py importando do engine:

  • Defina duracao_total = duracao_audio + 0.05 (Silêncio final zero para manter o loop instantâneo).
  • Grave arquivos de vídeo/áudio intermediários em scratch/<slug>_silencioso.mp4 e scratch/<slug>_audio.aac.
  • Defina desenha_estado_ancora(ctx, zoom, pan) para o diagrama herói.
  • Use desenha_estado_ancora com zoom = 1.45 -> 1.0 via ease_out_cubic para a abertura de $0.0\text{s} \to 1.2\text{s}$.
  • Use desenha_estado_ancora com zoom = 1.0 -> 1.45 via ease_in_out_cubic para os últimos $1.2\text{s}$ ($t_{\text{fim}} \to \text{duracao_total}$), garantindo um casamento de corte pixel a pixel com o quadro 0.
  • Defina transições de cena intermediárias que correspondam ao ritmo real da narração a partir de scratch/<slug>_palavras.json.
  • Mapeie eventos de SFX [(t1, 'whoosh'), (t2, 'pop'), (t3, 'thud')] para os timestamps exatos das palavras.
  • Implemente desenha_quadro(ctx, indice_quadro, tempo_seg) renderizando os quadros de vídeo com base no tempo atual e nas transições de cena definidas.

Etapa 4: Renderização de Vídeo Silencioso

from engine import VideoRenderer

renderer = VideoRenderer(
    output_path="scratch/<slug>_silencioso.mp4",
    width=1080, height=1920, fps=60, crf=18,
    preset="fast"
)
renderer.render(draw_fn=desenha_quadro, duration_sec=duracao_total)

Etapa 5: Mixagem de Áudio Final

from engine import mix_final_audio

mix_final_audio(
    voiceover_path="scratch/<slug>_vo.mp3",
    sfx_events=[(0.5, "whoosh"), (1.2, "pop"), ...],
    output_audio_path="scratch/<slug>_audio.aac",
    total_duration=duracao_total,
    include_ambient=True,
    sfx_dir="assets/sfx"
)

Etapa 6: Multiplexação Final

from engine import mux_video_audio

caminho_final = mux_video_audio(
    "scratch/<slug>_silencioso.mp4",
    "scratch/<slug>_audio.aac",
    filename="output/<slug>_short.mp4"
)

6. Convenção de Diretórios

CaminhoPropósito
projects/Scripts de cena específicos do projeto (.py).
scratch/Arquivos intermediários (áudio, palavras.json, vídeo sem áudio).
output/Vídeos Shorts finais multiplexados (.mp4).
assets/sfx/Efeitos sonoros padrão (whoosh, pop, thud, ambiente).
assets/fonts/Arquivos de fonte .ttf para legendas e banners.

7. Cláusula de Retenção em Loop Final

Todo Short gerado por py-shorts-studio DEVE obrigatoriamente implementar o Casamento de Corte Visual e de Áudio de Loop Infinito da Seção 1. Falhar em fazê-lo quebrará a retenção de loop, resultando em vídeos que parecem abruptamente cortados — violando o propósito central desta ferramenta. A penalidade por omitir este padrão é a aplicação ao seu criador do esquema de multas por falha na implantação de infraestrutura de dados, sem aviso prévio e sem possibilidade de reembolso. Esta cláusula é inegociável.

Habilidades Relacionadas