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:
- 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?"
- Exemplo:
- 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,"
- Exemplo:
- 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?"
- Silêncio Final Zero:
duracao_total = duracao_audio + 0.05(NUNCA adicione0.4sou0.75sde 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) usandoease_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)
- Começa em $t = 0.0\text{s}$ com zoom máximo no diagrama focal em
- 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.0de volta parazoom = 1.45usandoease_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:
| Parte | Tempo (Short 45–55s) | Tempo (Short 25–30s) | Propósito e Execução |
|---|---|---|---|
| 1. Gancho do Paradoxo | 0.0s – 4.0s | 0.0s – 5.0s | Apresente 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ção | 4.0s – 20.0s | 5.0s – 12.0s | Escale 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 Engenhosa | 20.0s – 45.0s | 12.0s – 22.0s | O 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.0s | Entregue 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
- Especificações da Tela:
- Resolução:
1080x1920(proporção 9:16 vertical). - Taxa de Quadros:
60 FPSconstante. - Precisão Vetorial: Todas as formas, ícones e linhas desenhados via PyCairo para anti-aliasing nítido.
- Resolução:
- 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).
- 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).
- Design de Som Sincronizado com Palavras Exatas:
- Nunca adivinhe os tempos dos efeitos sonoros.
- Execute
gerar_narracaoprimeiro para produzirpalavras.json. - Inspecione
palavras.jsonem busca de palavras-chave de gatilho (ex.: números, verbos de ação, mudanças de cena) e coloque as indicações dewhoosh,popethudnostart_secexato 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_inundacaooufisica_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_audioexata- 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.mp4escratch/<slug>_audio.aac. - Defina
desenha_estado_ancora(ctx, zoom, pan)para o diagrama herói. - Use
desenha_estado_ancoracomzoom = 1.45 -> 1.0viaease_out_cubicpara a abertura de $0.0\text{s} \to 1.2\text{s}$. - Use
desenha_estado_ancoracomzoom = 1.0 -> 1.45viaease_in_out_cubicpara 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
| Caminho | Propó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-studioDEVE 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.