Py-Shorts-Studio: バイラル2Dベクター解説動画エンジン は何をしますか?
py-shorts-studio は、縦型(1080x1920、9:16)60 FPS のモーショングラフィックス解説 Shorts を作成するための高性能 Python フレームワークです。PyCairo ベクターフレームを FFmpeg stdin に直接ストリーミングすることでディスク I/O を回避し、ボイスオーバーをアクティブワードカラオケキャプションと同期させ、カスタム SFX を合成し、マルチトラックオーディオをミキシングします。
1. シームレスなオーディオ + ビジュアル マッチカット ループ(無限リテンション)
平均視聴率(YouTube Shorts と Instagram Reels での 100% 以上のループリテンション)を最大化するために、生成されるすべての Short は、オーディオのセンテンスブリッジとピクセル単位で同一のビジュアルマッチカットの両方を適用しなければなりません。
A. センテンスブリッジスクリプトルール
YouTube が Short をループするとき、オーディオの終わりは瞬時に始まりに接続されます。スクリプトはこれを活用しなければなりません:
- オープニングフック:直ちに鋭い なぜ / どのように / 何 の質問で始めなければなりません。
- 例:
「なぜアイアンマンのスーツはトニー・スタークを3秒で殺してしまうのか?」 - 例:
「なぜ現代の高速道路は15年で崩壊するのに、ローマの道は2000年も持つのか?」
- 例:
- エンディングリードインフレーズ:最後の文は、明確な締めくくりやチャンネル登録の呼びかけではなく、予感させるリードインフレーズで終わらなければなりません:
- 例:
「...だからこそ、今でも人々はこう尋ねるのだ、」 - 例:
「...だから次に誰かがあなたに尋ねたら、」 - 例:
「...そしてそれが説明している、」
- 例:
- 連続する文法的ループ:ビデオがリードインフレーズからオープニング質問にループするとき、それは一つのシームレスな文を形成します:
「...だからこそ、今でも人々はこう尋ねるのだ — なぜアイアンマンのスーツはトニー・スタークを3秒で殺してしまうのか?」
- ゼロテーリングサイレンス:
total_duration = audio_duration + 0.05(決して0.4sや0.75sの無音を追加しないでください)。- 最後のポーズは無限ループの錯覚を壊します。オーディオループは、知覚可能なギャップゼロで再スタートの最初の単語にヒットしなければなりません。
B. ピクセル単位で同一のフレーム0マッチカット
最初のフレーム($t = 0.0\text{s}$)と最後のフレーム($t = \text{total_duration}$)は視覚的に同一でなければならず、視聴者はビデオが繰り返されていることを検出できません。
実装パターン: draw_anchor_state
すべてのプロジェクトスクリプトは、主要な「ヒーロー」ビジュアルとトップバナーを含む共有の draw_anchor_state(ctx, zoom=1.0, pan=(0, 0)) 関数を定義しなければなりません。
def draw_anchor_state(ctx: cairo.Context, zoom: float = 1.0, pan: tuple[float, float] = (0.0, 0.0)) -> None:
"""任意のズーム/パンセンターでマスターヒーロー図とトップバナーをレンダリングします。
シーン1(オープニングイーズアウトズーム)と最終シーン(クロージングイーズインズーム)の両方で使用され、
ループ境界全体にわたってピクセル単位で同一のマッチカットを保証します。
"""
ctx.save()
# センター起点変換(1080x1920の中心は540, 960)
ctx.translate(540.0 + pan[0], 960.0 + pan[1])
ctx.scale(zoom, zoom)
ctx.translate(-540.0, -960.0)
# 1. 背景
draw_dark_bg(ctx, 1080, 1920)
# 2. トップバナー
draw_top_banner(ctx, "HERO TOPIC TITLE", "ENGINEERING EXPLAINER", progress=1.0, theme="dark")
# 3. アクションゾーンのヒーロービジュアル/図
# (ここに中央図、断面図、または装置を描画)
...
ctx.restore()
マッチカットズームダイナミクス(0.0s – 1.2s & 最後の1.2s)
- オープニング($t = 0.0\text{s} \to 1.2\text{s}$):
- $t = 0.0\text{s}$ で、焦点図に
zoom = 1.45でタイトにズームインして開始します。 - $0.0\text{s} \to 1.2\text{s}$ の間、
ease_out_cubicを使用してワイドビュー(zoom = 1.0)にスムーズにイーズアウトします:if time_sec <= 1.2: zoom_t = ease_out_cubic(time_sec / 1.2) cur_zoom = lerp(1.45, 1.0, zoom_t) draw_anchor_state(ctx, zoom=cur_zoom)
- $t = 0.0\text{s}$ で、焦点図に
- クロージング($t = \text{total_duration} - 1.2\text{s} \to \text{total_duration}$):
- そのまったく同じヒーロー図にスムーズに遷移します。
- 最後の1.2秒間で、
ease_in_out_cubicを使用してzoom = 1.0からzoom = 1.45にズームインします:t_closing_start = total_duration - 1.2 if time_sec >= t_closing_start: zoom_t = ease_in_out_cubic((time_sec - t_closing_start) / 1.2) cur_zoom = lerp(1.0, 1.45, zoom_t) draw_anchor_state(ctx, zoom=cur_zoom)
- 結果:$t = 0.0\text{s}$(
zoom = 1.45)のフレームと $t = \text{total_duration}$(zoom = 1.45)のフレームはピクセル単位で同一です。
2. 高リテンションのための4部構成スクリプト公式
高リテンション Shorts は、この厳格なナラティブアークに従います:
| パート | タイミング(45~55秒 Short) | タイミング(25~30秒 Short) | 目的と実行 |
|---|---|---|---|
| 1. パラドックスフック | 0.0s – 4.0s | 0.0s – 5.0s | なぜ / どのように / 何 で始まる、 stark な反直感的なコントラストや不可能な謎を述べます。無駄な前置き、イントロ、挨拶は禁止。 |
| 2. 制約 / エスカレーション | 4.0s – 20.0s | 5.0s – 12.0s | 具体的な数字、歴史的な利害、環境的危険性で問題をエスカレートさせます。比較メトリクスやステータス HUD バーを導入します。 |
| 3. 巧妙なピボット | 20.0s – 45.0s | 12.0s – 22.0s | 建築的または科学的秘密が明らかになります。アニメーション図、カットアウェイ、ルート、コンポーネントレイヤーを使用してエンジニアリングメカニズムを分解します。 |
| 4. ペイオフ & トレーリングブリッジ | 最後の5.0s | 最後の4.0s | 「ああ!」というペイオフを届け、ゼロテーリングサイレンスでオープニングフックにループバックするトレーリングセンテンスブリッジで終了します。 |
- スピーキングペーシング:毎分150~170語(WPM)。
- ターゲットワードカウント:
- 45~55秒 Short:110~145語。
- 25~30秒 Short:65~75語。
3. ビジュアル & レイアウトルール
- キャンバス仕様:
- 解像度:
1080x1920(9:16 縦長アスペクト比)。 - フレームレート:
60 FPS固定。 - ベクター精度: すべての図形、アイコン、線は鮮明なアンチエイリアシングのために PyCairo を介して描画されます。
- 解像度:
- セーフゾーン(モバイル UI に重要):
- トップセーフゾーン($y \ge 130$):タイトル、チャプターピル、トップバナーは、チャンネルハンドルと上部 UI オーバーレイを避けるために $y = 130$ から $y = 260$ の間に配置します。
- センターアクションゾーン($y = 280$ から $y = 1420$):すべての焦点グラフィック、カットアウェイ、マップルート、統計バッジはここに配置します。
- ボトムキャプションセーフゾーン:キャプションは $y = 1480$–$1540$ に固定されます。背景の焦点要素は $y = 1400$ より上に保ち、キャプションテキストとの重なりを防ぎます。下部380px(YouTube と TikTok でタイトル、サウンド、チャンネル登録ボタンが配置される場所)には、重要な視覚的詳細を避けてください。
- ペーシングルール(3~5秒ごとの状態変更):
- 視聴者の注意は急速に衰えます。3~5秒 ごとに視覚的状態、カメラフォーカスを変更するか、アニメーションをトリガーします(例:バッジのスプリングポップ、HUD メトリックのフィル、パスの進行、レイヤーの表示)。
- 正確なワードシンクサウンドデザイン:
- 決して SFX タイミングを推測しないでください。
- 最初に
generate_voiceoverを実行してwords.jsonを生成します。 words.jsonを検査してキートリガーワード(例:数字、アクション動詞、シーン遷移)を見つけ、それらの単語の正確なstart_secにwhoosh、pop、thudのキューを配置します。
4. 完全なエンジン API リファレンス
すべてのモジュールは engine からきれいにエクスポートされています:
from engine import (
# レンダラー
VideoRenderer,
# イージング数学
clamp, lerp, ease_out_cubic, ease_in_out_cubic, spring_pop,
cubic_bezier_point, cubic_bezier_tangent, cubic_bezier_angle,
# 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,
# アクティブワードカラオケキャプション
draw_karaoke_captions, chunk_words,
# オーディオ合成、ミキシング & マルチプレクサ
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: ...
イージングヘルパー(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: ...
再利用可能なベクタープリミティブ(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: ...
アクティブワードカラオケキャプション(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: ...
ボイスオーバー、効果音 & ミキシング(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. 自律実行ワークフロー
任意のトピックで Short を生成するタスクが与えられた場合、これらの手順を順次実行します:
ステップ 1: スクリプト構成(センテンスブリッジルール)
4部構成の公式に従って、タイトな65~140語のスクリプトを作成します。
- 必須:鋭い Why / How / What の質問で始める。
- 必須:トレーリングセンテンスブリッジ(
「...だからこそ、今でも人々はこう尋ねるのだ、」)で終わる。 - 明確なトピックスラッグを選択します(例:
colosseum_floodやvibranium_physics)。
ステップ 2: scratch/ でのボイスオーバー & タイムスタンプ生成
すべての中間ファイルを scratch/ に保存して、output/ をクリーンに保ちます:
.venv\Scripts\python -c "from engine import generate_voiceover; generate_voiceover('''<script>''', 'scratch/<slug>_vo.mp3', 'scratch/<slug>_words.json')"
scratch/<slug>_words.json を検査して、以下を決定します:
- 正確な
audio_duration - キーマイルストーンワードのタイムスタンプ(例:フックが終わる場所、数字が話される場所、ピボットと最終ブリッジが発生する場所)。
ステップ 3: スクリプト化されたシーンパイプライン生成
projects/<slug>.py を作成し、engine からインポートします:
total_duration = audio_dur + 0.05を設定します(インスタントループを維持するためにゼロテーリングサイレンス)。- 中間ビデオ/オーディオファイルを
scratch/<slug>_silent.mp4とscratch/<slug>_audio.aacに書き込みます。 - ヒーロー図のための
draw_anchor_state(ctx, zoom, pan)を定義します。 - オープニングの $0.0\text{s} \to 1.2\text{s}$ では、
ease_out_cubicを介してzoom = 1.45 -> 1.0でdraw_anchor_stateを使用します。 - 最後の $1.2\text{s}$($t_{\text{close}} \to \text{total_duration}$)では、
ease_in_out_cubicを介してzoom = 1.0 -> 1.45でdraw_anchor_stateを使用し、フレーム0とのピクセル単位で同一のマッチカットを保証します。 scratch/<slug>_words.jsonから実際のナレーションペーシングに一致する中間シーン遷移を定義します。- SFX イベント
[(t1, 'whoosh'), (t2, 'pop'), (t3, 'thud')]を正確なワードタイムスタンプにマッピングします。 draw_frame(ctx, frame_idx, time_sec)レンダリング関数を実装...
[以下、翻訳のため切り詰められました]