Add multi-account OAuth, Obsidian integration, product assets, and test tooling
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
71
product/animaGen/README_local_render.md
Normal file
71
product/animaGen/README_local_render.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# Render the SquareMCP launch video locally
|
||||
|
||||
## Files
|
||||
- `render_squaremcp_video.py` renders the video
|
||||
- `squaremcp_shotlist.json` drives scene timing and copy
|
||||
- `squaremcp_launch_captions.srt` provides burned-in captions
|
||||
- `squaremcp_visual_prompts.md` can be used to create optional scene background images
|
||||
|
||||
## 1. Install dependencies
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 2. Basic render
|
||||
|
||||
Place the script in the same folder as the shotlist and captions, then run:
|
||||
|
||||
```bash
|
||||
python render_squaremcp_video.py \
|
||||
--shotlist squaremcp_shotlist.json \
|
||||
--captions squaremcp_launch_captions.srt \
|
||||
--output squaremcp_launch.mp4
|
||||
```
|
||||
|
||||
## 3. Faster draft render
|
||||
|
||||
```bash
|
||||
python render_squaremcp_video.py \
|
||||
--shotlist squaremcp_shotlist.json \
|
||||
--captions squaremcp_launch_captions.srt \
|
||||
--output squaremcp_launch_draft.mp4 \
|
||||
--draft
|
||||
```
|
||||
|
||||
## 4. Add your own voiceover
|
||||
|
||||
```bash
|
||||
python render_squaremcp_video.py \
|
||||
--shotlist squaremcp_shotlist.json \
|
||||
--captions squaremcp_launch_captions.srt \
|
||||
--voiceover founder_voiceover.wav \
|
||||
--output squaremcp_launch_with_vo.mp4
|
||||
```
|
||||
|
||||
## 5. Optional richer visuals
|
||||
|
||||
If you generate background stills for each scene from the prompts, save them in an `assets/` folder like this:
|
||||
|
||||
- `assets/scene1.png`
|
||||
- `assets/scene2.png`
|
||||
- ...
|
||||
- `assets/scene7.png`
|
||||
|
||||
Then run:
|
||||
|
||||
```bash
|
||||
python render_squaremcp_video.py \
|
||||
--shotlist squaremcp_shotlist.json \
|
||||
--captions squaremcp_launch_captions.srt \
|
||||
--assets-dir assets \
|
||||
--output squaremcp_launch_with_assets.mp4
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Default output is vertical 1080x1920
|
||||
- `--draft` renders 720x1280 and is much faster
|
||||
- The renderer creates a clean product-style motion graphic even without scene images
|
||||
- If you already have a polished voiceover, attach it with `--voiceover`
|
||||
481
product/animaGen/render_squaremcp_video.py
Normal file
481
product/animaGen/render_squaremcp_video.py
Normal file
@@ -0,0 +1,481 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import textwrap
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Optional
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
||||
|
||||
try:
|
||||
from moviepy.editor import VideoClip, concatenate_videoclips, AudioFileClip
|
||||
except Exception as exc: # pragma: no cover
|
||||
raise SystemExit(
|
||||
"MoviePy is required. Install dependencies with: pip install -r requirements.txt\n"
|
||||
f"Import error: {exc}"
|
||||
)
|
||||
|
||||
|
||||
BG = (10, 14, 22)
|
||||
BG2 = (16, 22, 34)
|
||||
TEXT = (244, 247, 251)
|
||||
MUTED = (144, 156, 178)
|
||||
ACCENT = (112, 166, 255)
|
||||
ACCENT_2 = (107, 228, 183)
|
||||
CARD = (20, 28, 44)
|
||||
CARD_2 = (28, 38, 58)
|
||||
STROKE = (44, 58, 86)
|
||||
SHADOW = (0, 0, 0, 110)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Scene:
|
||||
scene: int
|
||||
start_s: float
|
||||
end_s: float
|
||||
on_screen_text: str
|
||||
voiceover: str
|
||||
visual: str
|
||||
|
||||
@property
|
||||
def duration(self) -> float:
|
||||
return self.end_s - self.start_s
|
||||
|
||||
|
||||
@dataclass
|
||||
class Subtitle:
|
||||
start_s: float
|
||||
end_s: float
|
||||
text: str
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Render the SquareMCP launch video locally from the generated shot list and captions."
|
||||
)
|
||||
parser.add_argument("--shotlist", default="squaremcp_shotlist.json", help="Path to shot list JSON")
|
||||
parser.add_argument("--captions", default="squaremcp_launch_captions.srt", help="Path to captions SRT")
|
||||
parser.add_argument("--output", default="squaremcp_launch.mp4", help="Output MP4 path")
|
||||
parser.add_argument("--voiceover", default=None, help="Optional voiceover audio file")
|
||||
parser.add_argument("--assets-dir", default=None, help="Optional directory containing scene1.png ... scene7.png")
|
||||
parser.add_argument("--width", type=int, default=1080, help="Video width")
|
||||
parser.add_argument("--height", type=int, default=1920, help="Video height")
|
||||
parser.add_argument("--fps", type=int, default=24, help="Frames per second")
|
||||
parser.add_argument("--no-captions", action="store_true", help="Disable burned-in captions")
|
||||
parser.add_argument("--draft", action="store_true", help="Lower-resolution faster render")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def load_scenes(path: Path) -> List[Scene]:
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
return [Scene(**item) for item in data]
|
||||
|
||||
|
||||
_TIME_RE = re.compile(
|
||||
r"(?P<h>\d\d):(?P<m>\d\d):(?P<s>\d\d),(?P<ms>\d\d\d)\s+-->\s+"
|
||||
r"(?P<h2>\d\d):(?P<m2>\d\d):(?P<s2>\d\d),(?P<ms2>\d\d\d)"
|
||||
)
|
||||
|
||||
|
||||
def ts_to_seconds(h: str, m: str, s: str, ms: str) -> float:
|
||||
return int(h) * 3600 + int(m) * 60 + int(s) + int(ms) / 1000.0
|
||||
|
||||
|
||||
def parse_srt(path: Optional[Path]) -> List[Subtitle]:
|
||||
if not path or not path.exists():
|
||||
return []
|
||||
blocks = re.split(r"\n\s*\n", path.read_text(encoding="utf-8").strip())
|
||||
subtitles: List[Subtitle] = []
|
||||
for block in blocks:
|
||||
lines = [ln.rstrip() for ln in block.splitlines() if ln.strip()]
|
||||
if len(lines) < 2:
|
||||
continue
|
||||
timing_line = lines[1] if lines[0].strip().isdigit() else lines[0]
|
||||
match = _TIME_RE.match(timing_line)
|
||||
if not match:
|
||||
continue
|
||||
start = ts_to_seconds(match["h"], match["m"], match["s"], match["ms"])
|
||||
end = ts_to_seconds(match["h2"], match["m2"], match["s2"], match["ms2"])
|
||||
text_lines = lines[2:] if lines[0].strip().isdigit() else lines[1:]
|
||||
subtitles.append(Subtitle(start, end, "\n".join(text_lines)))
|
||||
return subtitles
|
||||
|
||||
|
||||
def ease_in_out(x: float) -> float:
|
||||
x = max(0.0, min(1.0, x))
|
||||
return x * x * (3 - 2 * x)
|
||||
|
||||
|
||||
def lerp(a: float, b: float, x: float) -> float:
|
||||
return a + (b - a) * x
|
||||
|
||||
|
||||
def get_font(size: int, bold: bool = False) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
|
||||
candidates = []
|
||||
if bold:
|
||||
candidates.extend([
|
||||
"DejaVuSans-Bold.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||
"Arial Bold.ttf",
|
||||
"Arial.ttf",
|
||||
])
|
||||
else:
|
||||
candidates.extend([
|
||||
"DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
"Arial.ttf",
|
||||
])
|
||||
for cand in candidates:
|
||||
try:
|
||||
return ImageFont.truetype(cand, size=size)
|
||||
except Exception:
|
||||
pass
|
||||
return ImageFont.load_default()
|
||||
|
||||
|
||||
def text_size(draw: ImageDraw.ImageDraw, text: str, font) -> Tuple[int, int]:
|
||||
bbox = draw.multiline_textbbox((0, 0), text, font=font, spacing=10)
|
||||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
|
||||
|
||||
def wrap_text(draw: ImageDraw.ImageDraw, text: str, font, max_width: int) -> str:
|
||||
parts = re.split(r"\n+", text)
|
||||
wrapped_parts = []
|
||||
for part in parts:
|
||||
words = part.split()
|
||||
if not words:
|
||||
wrapped_parts.append("")
|
||||
continue
|
||||
lines = []
|
||||
current = words[0]
|
||||
for word in words[1:]:
|
||||
candidate = f"{current} {word}"
|
||||
w = draw.textbbox((0, 0), candidate, font=font)[2]
|
||||
if w <= max_width:
|
||||
current = candidate
|
||||
else:
|
||||
lines.append(current)
|
||||
current = word
|
||||
lines.append(current)
|
||||
wrapped_parts.append("\n".join(lines))
|
||||
return "\n".join(wrapped_parts)
|
||||
|
||||
|
||||
def rounded_box(draw: ImageDraw.ImageDraw, box, fill, outline=None, radius=24, width=2):
|
||||
draw.rounded_rectangle(box, radius=radius, fill=fill, outline=outline, width=width)
|
||||
|
||||
|
||||
def add_shadow(base: Image.Image, box, radius=28):
|
||||
shadow = Image.new("RGBA", base.size, (0, 0, 0, 0))
|
||||
sd = ImageDraw.Draw(shadow)
|
||||
sd.rounded_rectangle(box, radius=28, fill=SHADOW)
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(radius=radius))
|
||||
base.alpha_composite(shadow)
|
||||
|
||||
|
||||
def draw_gradient_background(img: Image.Image, t: float):
|
||||
arr = np.zeros((img.height, img.width, 3), dtype=np.uint8)
|
||||
y = np.linspace(0, 1, img.height)[:, None]
|
||||
x = np.linspace(0, 1, img.width)[None, :]
|
||||
pulse = 0.5 + 0.5 * math.sin(t * 0.55)
|
||||
r = (BG[0] * (1 - y) + BG2[0] * y + 8 * pulse * x).clip(0, 255)
|
||||
g = (BG[1] * (1 - y) + BG2[1] * y + 10 * pulse * x).clip(0, 255)
|
||||
b = (BG[2] * (1 - y) + BG2[2] * y + 20 * pulse * x).clip(0, 255)
|
||||
arr[:, :, 0] = r.astype(np.uint8)
|
||||
arr[:, :, 1] = g.astype(np.uint8)
|
||||
arr[:, :, 2] = b.astype(np.uint8)
|
||||
bg = Image.fromarray(arr, mode="RGB").convert("RGBA")
|
||||
img.alpha_composite(bg)
|
||||
|
||||
draw = ImageDraw.Draw(img)
|
||||
step = max(40, img.width // 18)
|
||||
grid_alpha = 30
|
||||
for xx in range(0, img.width, step):
|
||||
draw.line((xx, 0, xx, img.height), fill=(255, 255, 255, grid_alpha), width=1)
|
||||
for yy in range(0, img.height, step):
|
||||
draw.line((0, yy, img.width, yy), fill=(255, 255, 255, grid_alpha), width=1)
|
||||
|
||||
|
||||
def paste_asset_background(img: Image.Image, asset_path: Optional[Path], t: float):
|
||||
if not asset_path or not asset_path.exists():
|
||||
return
|
||||
asset = Image.open(asset_path).convert("RGBA")
|
||||
scale = max(img.width / asset.width, img.height / asset.height) * 1.08
|
||||
new_size = (int(asset.width * scale), int(asset.height * scale))
|
||||
asset = asset.resize(new_size, Image.LANCZOS)
|
||||
pan_x = int((asset.width - img.width) * (0.04 + 0.02 * math.sin(t * 0.3)))
|
||||
pan_y = int((asset.height - img.height) * (0.04 + 0.02 * math.cos(t * 0.2)))
|
||||
asset = asset.crop((pan_x, pan_y, pan_x + img.width, pan_y + img.height))
|
||||
overlay = Image.new("RGBA", img.size, (5, 8, 14, 150))
|
||||
img.alpha_composite(asset)
|
||||
img.alpha_composite(overlay)
|
||||
|
||||
|
||||
def draw_top_label(draw, width, height):
|
||||
font = get_font(max(18, width // 42), bold=True)
|
||||
label = "SQUAREMCP"
|
||||
pad_x = width * 0.08
|
||||
pad_y = height * 0.06
|
||||
tw = draw.textbbox((0, 0), label, font=font)[2]
|
||||
rounded_box(draw, (pad_x - 18, pad_y - 12, pad_x + tw + 18, pad_y + 42), fill=(18, 24, 38, 215), outline=(55, 75, 110), radius=18)
|
||||
draw.text((pad_x, pad_y), label, font=font, fill=TEXT)
|
||||
|
||||
|
||||
def draw_main_text(draw, width, height, text, progress):
|
||||
font = get_font(max(34, width // 16), bold=True)
|
||||
body_font = get_font(max(22, width // 32), bold=False)
|
||||
max_width = int(width * 0.78)
|
||||
wrapped = wrap_text(draw, text.replace(" | ", "\n"), font, max_width)
|
||||
tw, th = text_size(draw, wrapped, font)
|
||||
x = int(width * 0.08)
|
||||
y = int(height * 0.18 + (1 - ease_in_out(min(progress * 1.6, 1.0))) * 40)
|
||||
box = (x - 26, y - 22, x + tw + 28, y + th + 26)
|
||||
draw.rounded_rectangle(box, radius=30, fill=(14, 18, 30, 185), outline=(52, 68, 98), width=2)
|
||||
draw.multiline_text((x, y), wrapped, font=font, fill=TEXT, spacing=12)
|
||||
return y + th + 30, body_font
|
||||
|
||||
|
||||
def draw_footer_caption(draw, width, height, subtitles: List[Subtitle], current_t: float):
|
||||
active = None
|
||||
for sub in subtitles:
|
||||
if sub.start_s <= current_t < sub.end_s:
|
||||
active = sub.text
|
||||
break
|
||||
if not active:
|
||||
return
|
||||
font = get_font(max(24, width // 28), bold=True)
|
||||
max_width = int(width * 0.78)
|
||||
active = wrap_text(draw, active, font, max_width)
|
||||
tw, th = text_size(draw, active, font)
|
||||
x = (width - tw) // 2
|
||||
y = int(height * 0.82)
|
||||
pad = 22
|
||||
draw.rounded_rectangle((x - pad, y - pad, x + tw + pad, y + th + pad), radius=26, fill=(8, 10, 16, 205), outline=(56, 72, 110), width=2)
|
||||
draw.multiline_text((x, y), active, font=font, fill=TEXT, spacing=8, align="center")
|
||||
|
||||
|
||||
def draw_node(draw, cx, cy, r, label, progress, active=False):
|
||||
fill = ACCENT if active else CARD_2
|
||||
outline = (160, 208, 255) if active else STROKE
|
||||
draw.ellipse((cx - r, cy - r, cx + r, cy + r), fill=fill, outline=outline, width=4)
|
||||
font = get_font(max(22, int(r * 0.33)), bold=True)
|
||||
bbox = draw.textbbox((0, 0), label, font=font)
|
||||
draw.text((cx - (bbox[2] - bbox[0]) / 2, cy - (bbox[3] - bbox[1]) / 2), label, font=font, fill=TEXT)
|
||||
|
||||
|
||||
def draw_scene_visual(scene: Scene, draw: ImageDraw.ImageDraw, img: Image.Image, local_t: float, progress: float, width: int, height: int):
|
||||
if scene.scene == 1:
|
||||
cy = int(height * 0.58)
|
||||
x1, x2, x3 = int(width * 0.18), int(width * 0.5), int(width * 0.82)
|
||||
draw_node(draw, x1, cy, int(width * 0.07), "Agent", progress, active=True)
|
||||
rounded_box(draw, (x2 - 120, cy - 90, x2 + 120, cy + 90), fill=CARD, outline=(90, 116, 168), radius=30, width=3)
|
||||
mid_font = get_font(max(28, width // 28), bold=True)
|
||||
label = "Gateway"
|
||||
bbox = draw.textbbox((0, 0), label, font=mid_font)
|
||||
draw.text((x2 - (bbox[2] - bbox[0]) / 2, cy - 15), label, font=mid_font, fill=TEXT)
|
||||
draw_node(draw, x3, cy, int(width * 0.07), "Tools", progress, active=False)
|
||||
p = ease_in_out(min(local_t / max(scene.duration * 0.7, 0.001), 1.0))
|
||||
stop_x = lerp(x1 + 76, x2 - 124, p)
|
||||
draw.line((x1 + 78, cy, stop_x, cy), fill=ACCENT_2, width=10)
|
||||
if p > 0.96:
|
||||
draw.ellipse((stop_x - 10, cy - 10, stop_x + 10, cy + 10), fill=ACCENT_2)
|
||||
elif scene.scene == 2:
|
||||
cards = ["APIs", "Databases", "Workflows", "Ops Tools"]
|
||||
positions = [(0.08, 0.56), (0.54, 0.56), (0.08, 0.75), (0.54, 0.75)]
|
||||
for idx, (label, pos) in enumerate(zip(cards, positions), start=1):
|
||||
delay = (idx - 1) * 0.12
|
||||
p = ease_in_out((progress - delay) / 0.65)
|
||||
if p <= 0:
|
||||
continue
|
||||
x = int(width * pos[0])
|
||||
y = int(height * pos[1] + (1 - p) * 80)
|
||||
w = int(width * 0.32)
|
||||
h = int(height * 0.11)
|
||||
add_shadow(img, (x, y, x + w, y + h), radius=18)
|
||||
rounded_box(draw, (x, y, x + w, y + h), fill=CARD, outline=STROKE, radius=28)
|
||||
f = get_font(max(26, width // 30), bold=True)
|
||||
draw.text((x + 28, y + 26), label, font=f, fill=TEXT)
|
||||
# Fake interface lines
|
||||
for line_idx in range(3):
|
||||
yy = y + 64 + line_idx * 14
|
||||
draw.line((x + 28, yy, x + w - 28 - line_idx * 40, yy), fill=MUTED, width=4)
|
||||
elif scene.scene == 3:
|
||||
left = (int(width * 0.08), int(height * 0.52), int(width * 0.46), int(height * 0.76))
|
||||
right = (int(width * 0.52), int(height * 0.52), int(width * 0.92), int(height * 0.76))
|
||||
add_shadow(img, left, radius=20)
|
||||
add_shadow(img, right, radius=20)
|
||||
rounded_box(draw, left, fill=CARD, outline=STROKE, radius=28)
|
||||
rounded_box(draw, right, fill=CARD, outline=STROKE, radius=28)
|
||||
title_font = get_font(max(26, width // 28), bold=True)
|
||||
small = get_font(max(20, width // 36), bold=False)
|
||||
draw.text((left[0] + 26, left[1] + 22), "Policy Controls", font=title_font, fill=TEXT)
|
||||
for i, label in enumerate(["OAuth required", "Scoped tools", "Write actions blocked"]):
|
||||
yy = left[1] + 84 + i * 54
|
||||
draw.rounded_rectangle((left[0] + 26, yy, left[0] + 74, yy + 28), radius=14, fill=(36, 56, 86), outline=(98, 132, 188))
|
||||
knob_x = left[0] + 50 + (18 if i != 2 else 0)
|
||||
draw.ellipse((knob_x, yy + 4, knob_x + 20, yy + 24), fill=ACCENT_2 if i != 2 else MUTED)
|
||||
draw.text((left[0] + 92, yy - 2), label, font=small, fill=TEXT)
|
||||
draw.text((right[0] + 26, right[1] + 22), "Audit Trail", font=title_font, fill=TEXT)
|
||||
for i, item in enumerate(["tool.call github.list_prs", "auth.ok service_account", "policy.allow read_only"]):
|
||||
yy = right[1] + 84 + i * 52
|
||||
draw.rounded_rectangle((right[0] + 22, yy, right[2] - 22, yy + 36), radius=16, fill=CARD_2, outline=(60, 78, 116))
|
||||
draw.text((right[0] + 36, yy + 6), item, font=small, fill=TEXT)
|
||||
elif scene.scene == 4:
|
||||
title = "SquareMCP"
|
||||
subtitle = "Managed MCP gateway for internal tools"
|
||||
title_font = get_font(max(56, width // 10), bold=True)
|
||||
subtitle_font = get_font(max(28, width // 28), bold=False)
|
||||
bbox = draw.textbbox((0, 0), title, font=title_font)
|
||||
tw = bbox[2] - bbox[0]
|
||||
draw.text(((width - tw) / 2, height * 0.52), title, font=title_font, fill=TEXT)
|
||||
bbox2 = draw.textbbox((0, 0), subtitle, font=subtitle_font)
|
||||
sw = bbox2[2] - bbox2[0]
|
||||
draw.text(((width - sw) / 2, height * 0.52 + 120), subtitle, font=subtitle_font, fill=MUTED)
|
||||
cy = int(height * 0.74)
|
||||
x1, x2, x3 = int(width * 0.16), int(width * 0.5), int(width * 0.84)
|
||||
draw_node(draw, x1, cy, int(width * 0.05), "Agent", progress, active=True)
|
||||
rounded_box(draw, (x2 - 130, cy - 74, x2 + 130, cy + 74), fill=CARD, outline=(90, 116, 168), radius=26, width=3)
|
||||
mid_font = get_font(max(24, width // 32), bold=True)
|
||||
label = "SquareMCP"
|
||||
bbox = draw.textbbox((0, 0), label, font=mid_font)
|
||||
draw.text((x2 - (bbox[2] - bbox[0]) / 2, cy - 12), label, font=mid_font, fill=TEXT)
|
||||
draw_node(draw, x3, cy, int(width * 0.05), "Tools", progress, active=False)
|
||||
draw.line((x1 + 58, cy, x2 - 132, cy), fill=ACCENT_2, width=8)
|
||||
draw.line((x2 + 132, cy, x3 - 58, cy), fill=ACCENT, width=8)
|
||||
elif scene.scene == 5:
|
||||
shell = (int(width * 0.08), int(height * 0.5), int(width * 0.92), int(height * 0.8))
|
||||
add_shadow(img, shell, radius=24)
|
||||
rounded_box(draw, shell, fill=(15, 21, 34), outline=(64, 85, 126), radius=34)
|
||||
title_font = get_font(max(26, width // 28), bold=True)
|
||||
small = get_font(max(20, width // 38), bold=False)
|
||||
draw.text((shell[0] + 28, shell[1] + 22), "Gateway Runtime", font=title_font, fill=TEXT)
|
||||
left_panel = (shell[0] + 28, shell[1] + 72, shell[0] + int(width * 0.28), shell[3] - 28)
|
||||
mid_panel = (left_panel[2] + 22, shell[1] + 72, left_panel[2] + 22 + int(width * 0.22), shell[3] - 28)
|
||||
right_panel = (mid_panel[2] + 22, shell[1] + 72, shell[2] - 28, shell[3] - 28)
|
||||
for box, label in [(left_panel, "Connected Tools"), (mid_panel, "Access"), (right_panel, "Audit Log")]:
|
||||
rounded_box(draw, box, fill=CARD, outline=STROKE, radius=24)
|
||||
draw.text((box[0] + 18, box[1] + 14), label, font=small, fill=MUTED)
|
||||
for i, tool in enumerate(["postgres.query", "github.repos", "zendesk.ticket"]):
|
||||
yy = left_panel[1] + 52 + i * 58
|
||||
draw.rounded_rectangle((left_panel[0] + 14, yy, left_panel[2] - 14, yy + 42), radius=16, fill=CARD_2, outline=(58, 76, 110))
|
||||
draw.text((left_panel[0] + 28, yy + 10), tool, font=small, fill=TEXT)
|
||||
for i, item in enumerate(["Auth OK", "Read only", "Tenant scoped"]):
|
||||
yy = mid_panel[1] + 56 + i * 70
|
||||
draw.rounded_rectangle((mid_panel[0] + 16, yy, mid_panel[2] - 16, yy + 48), radius=18, fill=(25, 37, 56), outline=(74, 96, 138))
|
||||
draw.text((mid_panel[0] + 28, yy + 13), item, font=small, fill=ACCENT_2 if i == 0 else TEXT)
|
||||
logs = [
|
||||
"12:01 auth.ok",
|
||||
"12:01 tool.call postgres.query",
|
||||
"12:01 policy.allow",
|
||||
"12:02 result.ok",
|
||||
]
|
||||
for i, log in enumerate(logs):
|
||||
yy = right_panel[1] + 48 + i * 44
|
||||
draw.text((right_panel[0] + 18, yy), log, font=small, fill=TEXT if i % 2 == 0 else MUTED)
|
||||
draw.line((right_panel[0] + 18, yy + 28, right_panel[2] - 18, yy + 28), fill=(50, 66, 96), width=2)
|
||||
elif scene.scene == 6:
|
||||
titles = ["AI startups", "Copilot teams", "Regulated teams"]
|
||||
subs = ["Move fast with guardrails", "Connect real internal systems", "Keep logs and policy boundaries"]
|
||||
widths = [0.26, 0.26, 0.26]
|
||||
start_x = 0.08
|
||||
gap = 0.03
|
||||
top = int(height * 0.56)
|
||||
title_font = get_font(max(24, width // 34), bold=True)
|
||||
body_font = get_font(max(20, width // 42), bold=False)
|
||||
x = start_x
|
||||
for idx, (title, sub, w_pct) in enumerate(zip(titles, subs, widths)):
|
||||
p = ease_in_out((progress - idx * 0.12) / 0.7)
|
||||
if p <= 0:
|
||||
x += w_pct + gap
|
||||
continue
|
||||
box_w = int(width * w_pct)
|
||||
box_h = int(height * 0.18)
|
||||
xx = int(width * x)
|
||||
yy = int(top + (1 - p) * 70)
|
||||
add_shadow(img, (xx, yy, xx + box_w, yy + box_h), radius=18)
|
||||
rounded_box(draw, (xx, yy, xx + box_w, yy + box_h), fill=CARD, outline=STROKE, radius=26)
|
||||
draw.text((xx + 20, yy + 18), title, font=title_font, fill=TEXT)
|
||||
wrapped = wrap_text(draw, sub, body_font, box_w - 40)
|
||||
draw.multiline_text((xx + 20, yy + 66), wrapped, font=body_font, fill=MUTED, spacing=8)
|
||||
x += w_pct + gap
|
||||
elif scene.scene == 7:
|
||||
title = "squaremcp.com"
|
||||
font = get_font(max(52, width // 12), bold=True)
|
||||
bbox = draw.textbbox((0, 0), title, font=font)
|
||||
tw = bbox[2] - bbox[0]
|
||||
th = bbox[3] - bbox[1]
|
||||
x = (width - tw) / 2
|
||||
y = height * 0.60
|
||||
draw.rounded_rectangle((x - 30, y - 24, x + tw + 30, y + th + 24), radius=30, fill=(14, 18, 30, 190), outline=(70, 94, 140), width=2)
|
||||
draw.text((x, y), title, font=font, fill=TEXT)
|
||||
small = get_font(max(24, width // 32), bold=False)
|
||||
msg = "Managed MCP gateway for internal tools"
|
||||
bbox2 = draw.textbbox((0, 0), msg, font=small)
|
||||
draw.text(((width - (bbox2[2] - bbox2[0])) / 2, y - 80), msg, font=small, fill=MUTED)
|
||||
|
||||
|
||||
def make_scene_clip(scene: Scene, subtitles: List[Subtitle], width: int, height: int, assets_dir: Optional[Path], show_captions: bool) -> VideoClip:
|
||||
asset_path = assets_dir / f"scene{scene.scene}.png" if assets_dir else None
|
||||
|
||||
def frame_fn(t: float):
|
||||
local_t = max(0.0, min(scene.duration, t))
|
||||
progress = local_t / max(scene.duration, 0.001)
|
||||
img = Image.new("RGBA", (width, height), (0, 0, 0, 255))
|
||||
draw_gradient_background(img, scene.start_s + local_t)
|
||||
paste_asset_background(img, asset_path, scene.start_s + local_t)
|
||||
draw = ImageDraw.Draw(img)
|
||||
draw_top_label(draw, width, height)
|
||||
draw_main_text(draw, width, height, scene.on_screen_text, progress)
|
||||
draw_scene_visual(scene, draw, img, local_t, progress, width, height)
|
||||
if show_captions:
|
||||
draw_footer_caption(draw, width, height, subtitles, scene.start_s + local_t)
|
||||
return np.array(img.convert("RGB"))
|
||||
|
||||
return VideoClip(frame_fn, duration=scene.duration)
|
||||
|
||||
|
||||
def attach_audio(clip: VideoClip, voiceover_path: Optional[Path]) -> VideoClip:
|
||||
if not voiceover_path:
|
||||
return clip
|
||||
audio = AudioFileClip(str(voiceover_path))
|
||||
if audio.duration > clip.duration:
|
||||
audio = audio.subclip(0, clip.duration)
|
||||
return clip.set_audio(audio)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
width = 720 if args.draft else args.width
|
||||
height = 1280 if args.draft else args.height
|
||||
|
||||
shotlist_path = Path(args.shotlist)
|
||||
captions_path = Path(args.captions) if args.captions else None
|
||||
output_path = Path(args.output)
|
||||
assets_dir = Path(args.assets_dir) if args.assets_dir else None
|
||||
voiceover_path = Path(args.voiceover) if args.voiceover else None
|
||||
|
||||
scenes = load_scenes(shotlist_path)
|
||||
subtitles = [] if args.no_captions else parse_srt(captions_path)
|
||||
|
||||
clips = [make_scene_clip(scene, subtitles, width, height, assets_dir, not args.no_captions) for scene in scenes]
|
||||
final = concatenate_videoclips(clips, method="compose")
|
||||
final = attach_audio(final, voiceover_path)
|
||||
|
||||
final.write_videofile(
|
||||
str(output_path),
|
||||
fps=args.fps,
|
||||
codec="libx264",
|
||||
audio_codec="aac" if voiceover_path else None,
|
||||
preset="medium" if not args.draft else "ultrafast",
|
||||
threads=4,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
4
product/animaGen/requirements.txt
Normal file
4
product/animaGen/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
moviepy==1.0.3
|
||||
Pillow>=10,<12
|
||||
numpy>=1.26,<3
|
||||
imageio-ffmpeg>=0.4.9
|
||||
33
product/animaGen/squaremcp_launch_captions (1).srt
Normal file
33
product/animaGen/squaremcp_launch_captions (1).srt
Normal file
@@ -0,0 +1,33 @@
|
||||
1
|
||||
00:00:00,000 --> 00:00:04,000
|
||||
Agents need access to internal tools.
|
||||
Teams need control.
|
||||
|
||||
2
|
||||
00:00:04,000 --> 00:00:09,000
|
||||
The real value in agents starts when they can work inside real systems:
|
||||
APIs, databases, workflows, and operational tools.
|
||||
|
||||
3
|
||||
00:00:09,000 --> 00:00:15,000
|
||||
But production access is not just connectivity.
|
||||
You need authentication, scoped permissions, audit logs, and observability.
|
||||
|
||||
4
|
||||
00:00:15,000 --> 00:00:21,000
|
||||
That is why we built SquareMCP,
|
||||
a managed MCP gateway for internal tools.
|
||||
|
||||
5
|
||||
00:00:21,000 --> 00:00:31,000
|
||||
SquareMCP gives teams a practical path to connect agents to internal systems
|
||||
without stitching the security and control layer together by hand.
|
||||
|
||||
6
|
||||
00:00:31,000 --> 00:00:38,000
|
||||
We built it for AI startups, internal copilot teams,
|
||||
and regulated teams that need more control than a quick demo stack can provide.
|
||||
|
||||
7
|
||||
00:00:38,000 --> 00:00:42,000
|
||||
Learn more at squaremcp.com.
|
||||
33
product/animaGen/squaremcp_launch_captions.srt
Normal file
33
product/animaGen/squaremcp_launch_captions.srt
Normal file
@@ -0,0 +1,33 @@
|
||||
1
|
||||
00:00:00,000 --> 00:00:04,000
|
||||
Agents need access to internal tools.
|
||||
Teams need control.
|
||||
|
||||
2
|
||||
00:00:04,000 --> 00:00:09,000
|
||||
The real value in agents starts when they can work inside real systems:
|
||||
APIs, databases, workflows, and operational tools.
|
||||
|
||||
3
|
||||
00:00:09,000 --> 00:00:15,000
|
||||
But production access is not just connectivity.
|
||||
You need authentication, scoped permissions, audit logs, and observability.
|
||||
|
||||
4
|
||||
00:00:15,000 --> 00:00:21,000
|
||||
That is why we built SquareMCP,
|
||||
a managed MCP gateway for internal tools.
|
||||
|
||||
5
|
||||
00:00:21,000 --> 00:00:31,000
|
||||
SquareMCP gives teams a practical path to connect agents to internal systems
|
||||
without stitching the security and control layer together by hand.
|
||||
|
||||
6
|
||||
00:00:31,000 --> 00:00:38,000
|
||||
We built it for AI startups, internal copilot teams,
|
||||
and regulated teams that need more control than a quick demo stack can provide.
|
||||
|
||||
7
|
||||
00:00:38,000 --> 00:00:42,000
|
||||
Learn more at squaremcp.com.
|
||||
58
product/animaGen/squaremcp_shotlist (1).json
Normal file
58
product/animaGen/squaremcp_shotlist (1).json
Normal file
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"scene": 1,
|
||||
"start_s": 0.0,
|
||||
"end_s": 4.0,
|
||||
"on_screen_text": "Agents need access to internal tools. Teams need control.",
|
||||
"voiceover": "Agents need access to internal tools. Teams need control.",
|
||||
"visual": "Animated path between Agent and Internal Tools with connection paused at gateway boundary."
|
||||
},
|
||||
{
|
||||
"scene": 2,
|
||||
"start_s": 4.0,
|
||||
"end_s": 9.0,
|
||||
"on_screen_text": "APIs | Databases | Workflows | Operational systems",
|
||||
"voiceover": "The real value in agents starts when they can work inside real systems: APIs, databases, workflows, and operational tools.",
|
||||
"visual": "Quick UI sequence of API payload, SQL table, support dashboard, admin panel."
|
||||
},
|
||||
{
|
||||
"scene": 3,
|
||||
"start_s": 9.0,
|
||||
"end_s": 15.0,
|
||||
"on_screen_text": "Authentication | Permissions | Audit logs | Observability",
|
||||
"voiceover": "But production access is not just connectivity. You need authentication, scoped permissions, audit logs, and observability.",
|
||||
"visual": "Policy panel, auth controls, audit entries, request traces."
|
||||
},
|
||||
{
|
||||
"scene": 4,
|
||||
"start_s": 15.0,
|
||||
"end_s": 21.0,
|
||||
"on_screen_text": "SquareMCP \u2014 Managed MCP gateway for internal tools",
|
||||
"voiceover": "That is why we built SquareMCP, a managed MCP gateway for internal tools.",
|
||||
"visual": "Title card into gateway diagram."
|
||||
},
|
||||
{
|
||||
"scene": 5,
|
||||
"start_s": 21.0,
|
||||
"end_s": 31.0,
|
||||
"on_screen_text": "Secure agent access without stitching the control layer by hand",
|
||||
"voiceover": "SquareMCP gives teams a practical path to connect agents to internal systems without stitching the security and control layer together by hand.",
|
||||
"visual": "Gateway UI with connected tools, auth status, audit trail."
|
||||
},
|
||||
{
|
||||
"scene": 6,
|
||||
"start_s": 31.0,
|
||||
"end_s": 38.0,
|
||||
"on_screen_text": "Built for AI startups | Internal copilot teams | Regulated and fintech teams",
|
||||
"voiceover": "We built it for AI startups, internal copilot teams, and regulated teams that need more control than a quick demo stack can provide.",
|
||||
"visual": "Three use-case panels."
|
||||
},
|
||||
{
|
||||
"scene": 7,
|
||||
"start_s": 38.0,
|
||||
"end_s": 42.0,
|
||||
"on_screen_text": "squaremcp.com",
|
||||
"voiceover": "Learn more at squaremcp.com.",
|
||||
"visual": "Minimal end card."
|
||||
}
|
||||
]
|
||||
58
product/animaGen/squaremcp_shotlist (2).json
Normal file
58
product/animaGen/squaremcp_shotlist (2).json
Normal file
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"scene": 1,
|
||||
"start_s": 0.0,
|
||||
"end_s": 4.0,
|
||||
"on_screen_text": "Agents need access to internal tools. Teams need control.",
|
||||
"voiceover": "Agents need access to internal tools. Teams need control.",
|
||||
"visual": "Animated path between Agent and Internal Tools with connection paused at gateway boundary."
|
||||
},
|
||||
{
|
||||
"scene": 2,
|
||||
"start_s": 4.0,
|
||||
"end_s": 9.0,
|
||||
"on_screen_text": "APIs | Databases | Workflows | Operational systems",
|
||||
"voiceover": "The real value in agents starts when they can work inside real systems: APIs, databases, workflows, and operational tools.",
|
||||
"visual": "Quick UI sequence of API payload, SQL table, support dashboard, admin panel."
|
||||
},
|
||||
{
|
||||
"scene": 3,
|
||||
"start_s": 9.0,
|
||||
"end_s": 15.0,
|
||||
"on_screen_text": "Authentication | Permissions | Audit logs | Observability",
|
||||
"voiceover": "But production access is not just connectivity. You need authentication, scoped permissions, audit logs, and observability.",
|
||||
"visual": "Policy panel, auth controls, audit entries, request traces."
|
||||
},
|
||||
{
|
||||
"scene": 4,
|
||||
"start_s": 15.0,
|
||||
"end_s": 21.0,
|
||||
"on_screen_text": "SquareMCP \u2014 Managed MCP gateway for internal tools",
|
||||
"voiceover": "That is why we built SquareMCP, a managed MCP gateway for internal tools.",
|
||||
"visual": "Title card into gateway diagram."
|
||||
},
|
||||
{
|
||||
"scene": 5,
|
||||
"start_s": 21.0,
|
||||
"end_s": 31.0,
|
||||
"on_screen_text": "Secure agent access without stitching the control layer by hand",
|
||||
"voiceover": "SquareMCP gives teams a practical path to connect agents to internal systems without stitching the security and control layer together by hand.",
|
||||
"visual": "Gateway UI with connected tools, auth status, audit trail."
|
||||
},
|
||||
{
|
||||
"scene": 6,
|
||||
"start_s": 31.0,
|
||||
"end_s": 38.0,
|
||||
"on_screen_text": "Built for AI startups | Internal copilot teams | Regulated and fintech teams",
|
||||
"voiceover": "We built it for AI startups, internal copilot teams, and regulated teams that need more control than a quick demo stack can provide.",
|
||||
"visual": "Three use-case panels."
|
||||
},
|
||||
{
|
||||
"scene": 7,
|
||||
"start_s": 38.0,
|
||||
"end_s": 42.0,
|
||||
"on_screen_text": "squaremcp.com",
|
||||
"voiceover": "Learn more at squaremcp.com.",
|
||||
"visual": "Minimal end card."
|
||||
}
|
||||
]
|
||||
58
product/animaGen/squaremcp_shotlist.json
Normal file
58
product/animaGen/squaremcp_shotlist.json
Normal file
@@ -0,0 +1,58 @@
|
||||
[
|
||||
{
|
||||
"scene": 1,
|
||||
"start_s": 0.0,
|
||||
"end_s": 4.0,
|
||||
"on_screen_text": "Agents need access to internal tools. Teams need control.",
|
||||
"voiceover": "Agents need access to internal tools. Teams need control.",
|
||||
"visual": "Animated path between Agent and Internal Tools with connection paused at gateway boundary."
|
||||
},
|
||||
{
|
||||
"scene": 2,
|
||||
"start_s": 4.0,
|
||||
"end_s": 9.0,
|
||||
"on_screen_text": "APIs | Databases | Workflows | Operational systems",
|
||||
"voiceover": "The real value in agents starts when they can work inside real systems: APIs, databases, workflows, and operational tools.",
|
||||
"visual": "Quick UI sequence of API payload, SQL table, support dashboard, admin panel."
|
||||
},
|
||||
{
|
||||
"scene": 3,
|
||||
"start_s": 9.0,
|
||||
"end_s": 15.0,
|
||||
"on_screen_text": "Authentication | Permissions | Audit logs | Observability",
|
||||
"voiceover": "But production access is not just connectivity. You need authentication, scoped permissions, audit logs, and observability.",
|
||||
"visual": "Policy panel, auth controls, audit entries, request traces."
|
||||
},
|
||||
{
|
||||
"scene": 4,
|
||||
"start_s": 15.0,
|
||||
"end_s": 21.0,
|
||||
"on_screen_text": "SquareMCP \u2014 Managed MCP gateway for internal tools",
|
||||
"voiceover": "That is why we built SquareMCP, a managed MCP gateway for internal tools.",
|
||||
"visual": "Title card into gateway diagram."
|
||||
},
|
||||
{
|
||||
"scene": 5,
|
||||
"start_s": 21.0,
|
||||
"end_s": 31.0,
|
||||
"on_screen_text": "Secure agent access without stitching the control layer by hand",
|
||||
"voiceover": "SquareMCP gives teams a practical path to connect agents to internal systems without stitching the security and control layer together by hand.",
|
||||
"visual": "Gateway UI with connected tools, auth status, audit trail."
|
||||
},
|
||||
{
|
||||
"scene": 6,
|
||||
"start_s": 31.0,
|
||||
"end_s": 38.0,
|
||||
"on_screen_text": "Built for AI startups | Internal copilot teams | Regulated and fintech teams",
|
||||
"voiceover": "We built it for AI startups, internal copilot teams, and regulated teams that need more control than a quick demo stack can provide.",
|
||||
"visual": "Three use-case panels."
|
||||
},
|
||||
{
|
||||
"scene": 7,
|
||||
"start_s": 38.0,
|
||||
"end_s": 42.0,
|
||||
"on_screen_text": "squaremcp.com",
|
||||
"voiceover": "Learn more at squaremcp.com.",
|
||||
"visual": "Minimal end card."
|
||||
}
|
||||
]
|
||||
18
product/animaGen/squaremcp_visual_prompts (1).md
Normal file
18
product/animaGen/squaremcp_visual_prompts (1).md
Normal file
@@ -0,0 +1,18 @@
|
||||
# SquareMCP Visual Prompts
|
||||
|
||||
Use these prompts in your preferred video or image tool to generate background clips or keyframes.
|
||||
|
||||
## Prompt 1
|
||||
Minimal dark UI animation showing an AI agent connecting to internal company tools through a secure gateway, modern enterprise software aesthetic, subtle motion, clean typography areas, terminal plus dashboard fragments, no neon, no hype, realistic product style
|
||||
|
||||
## Prompt 2
|
||||
Enterprise admin panel with authentication status, scoped permissions, audit logs, and observability charts, tasteful dark interface, believable SaaS product design, technical and restrained
|
||||
|
||||
## Prompt 3
|
||||
Abstract network diagram showing agent, gateway, internal APIs, database, workflow engine, and support tool, thin lines, soft motion, clean modern UI, suitable for a product launch video
|
||||
|
||||
## Prompt 4
|
||||
Close up product dashboard with tool call logs and policy controls, realistic trace view, developer infrastructure product, crisp interface, understated and credible
|
||||
|
||||
## Prompt 5
|
||||
Simple branded end card for SquareMCP with just the product name and squaremcp.com, minimal dark background, refined motion design, founder-led launch feel
|
||||
18
product/animaGen/squaremcp_visual_prompts.md
Normal file
18
product/animaGen/squaremcp_visual_prompts.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# SquareMCP Visual Prompts
|
||||
|
||||
Use these prompts in your preferred video or image tool to generate background clips or keyframes.
|
||||
|
||||
## Prompt 1
|
||||
Minimal dark UI animation showing an AI agent connecting to internal company tools through a secure gateway, modern enterprise software aesthetic, subtle motion, clean typography areas, terminal plus dashboard fragments, no neon, no hype, realistic product style
|
||||
|
||||
## Prompt 2
|
||||
Enterprise admin panel with authentication status, scoped permissions, audit logs, and observability charts, tasteful dark interface, believable SaaS product design, technical and restrained
|
||||
|
||||
## Prompt 3
|
||||
Abstract network diagram showing agent, gateway, internal APIs, database, workflow engine, and support tool, thin lines, soft motion, clean modern UI, suitable for a product launch video
|
||||
|
||||
## Prompt 4
|
||||
Close up product dashboard with tool call logs and policy controls, realistic trace view, developer infrastructure product, crisp interface, understated and credible
|
||||
|
||||
## Prompt 5
|
||||
Simple branded end card for SquareMCP with just the product name and squaremcp.com, minimal dark background, refined motion design, founder-led launch feel
|
||||
Reference in New Issue
Block a user