Skip to content

Commit 3967dd1

Browse files
committed
feat(video): add motion graphics examples and tools
Introduced several Python scripts demonstrating motion graphics rendering and GitTools safety features. Includes examples for basic rendering, Git repository validation, and agent factories. These enhancements aim to provide a comprehensive toolkit for creating and managing motion graphics projects efficiently.
1 parent 4737f86 commit 3967dd1

9 files changed

Lines changed: 349 additions & 12 deletions

File tree

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
"""Motion Graphics — Basic HTML/GSAP → MP4 render (no LLM required).
2+
3+
This example drives the `HtmlRenderBackend` directly with a hand-authored
4+
HTML/GSAP composition. It renders to a real MP4 via headless Chromium +
5+
bundled ffmpeg, with no API keys needed.
6+
7+
Requirements:
8+
pip install praisonai-tools[video-motion]
9+
playwright install chromium
10+
11+
Verified end-to-end: produces 1920x1080 @ 30fps H.264 MP4 in ~5–10 seconds.
12+
"""
13+
14+
import asyncio
15+
from pathlib import Path
16+
17+
from praisonai_tools.video.motion_graphics import HtmlRenderBackend, RenderOpts
18+
19+
20+
HTML = """
21+
<!DOCTYPE html>
22+
<html>
23+
<head>
24+
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
25+
<style>
26+
body { margin:0; padding:0; background:#0f172a; overflow:hidden;
27+
font-family: -apple-system, Arial, sans-serif; }
28+
#stage { width:1920px; height:1080px; position:relative; }
29+
.title { font-size:96px; font-weight:800; color:#f8fafc;
30+
position:absolute; top:50%; left:50%;
31+
transform:translate(-50%,-50%); opacity:0; }
32+
.subtitle { font-size:44px; color:#94a3b8;
33+
position:absolute; top:62%; left:50%;
34+
transform:translate(-50%,-50%); opacity:0; }
35+
.dot { width:32px; height:32px; border-radius:50%; background:#38bdf8;
36+
position:absolute; top:78%; left:50%;
37+
transform:translate(-50%,-50%); opacity:0; }
38+
</style>
39+
</head>
40+
<body>
41+
<div id="stage" data-duration="4.0">
42+
<div class="title">PraisonAI</div>
43+
<div class="subtitle">Motion Graphics Pipeline</div>
44+
<div class="dot"></div>
45+
</div>
46+
<script>
47+
const tl = gsap.timeline({ paused: true });
48+
tl.to(".title", { duration: 0.8, opacity: 1, y: -30, ease: "power3.out" })
49+
.to(".subtitle", { duration: 0.7, opacity: 1, y: -20, ease: "power3.out" }, "-=0.3")
50+
.to(".dot", { duration: 0.5, opacity: 1, scale: 2, ease: "back.out(2)" }, "-=0.2")
51+
.to(".dot", { duration: 0.4, x: 200, ease: "power2.inOut" })
52+
.to(".dot", { duration: 0.4, x: -200, ease: "power2.inOut" })
53+
.to(".dot", { duration: 0.4, x: 0, ease: "power2.inOut" })
54+
.to([".title",".subtitle",".dot"],
55+
{ duration: 0.5, opacity: 0, ease: "power2.in" });
56+
// Required: export timeline(s) for the render backend to drive.
57+
window.__timelines = [tl];
58+
</script>
59+
</body>
60+
</html>
61+
"""
62+
63+
64+
async def main() -> None:
65+
out = Path("/tmp/motion_graphics_demo")
66+
out.mkdir(exist_ok=True)
67+
(out / "index.html").write_text(HTML)
68+
69+
backend = HtmlRenderBackend(base_dir=out)
70+
71+
lint = await backend.lint(out)
72+
print(f"Lint: ok={lint.ok} messages={lint.messages}")
73+
74+
opts = RenderOpts(output_name="praisonai_demo.mp4", fps=30, quality="standard")
75+
print("Render: ...", flush=True)
76+
result = await backend.render(out, opts)
77+
78+
if result.ok:
79+
print(f"Render: OK path={result.output_path} size={result.size_kb} KB")
80+
else:
81+
print(f"Render: FAIL stderr={result.stderr}")
82+
83+
84+
if __name__ == "__main__":
85+
asyncio.run(main())
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Motion Graphics — GitTools safety demo (no network required for this demo).
2+
3+
GitTools is the read-only toolkit used by the code-exploration specialist in
4+
`motion_graphics_team`. It clones repos on demand, validates paths against
5+
traversal, and restricts to GitHub URLs or `owner/repo` format.
6+
7+
This example exercises the pure-Python safety helpers (URL parsing + file
8+
path validation). Cloning is network-bound and demonstrated separately.
9+
10+
Requirements:
11+
pip install praisonai-tools
12+
"""
13+
14+
from praisonai_tools.tools.git_tools import GitTools
15+
16+
17+
def main() -> None:
18+
tools = GitTools(base_dir="/tmp/praison_git_repos_demo")
19+
20+
print("URL parsing")
21+
print("-----------")
22+
for repo_input in [
23+
"octocat/Hello-World", # owner/repo
24+
"https://github.com/octocat/Hello-World.git", # https URL
25+
"git@github.com:octocat/Hello-World.git", # ssh URL
26+
]:
27+
try:
28+
url, name = tools._parse_repo_input(repo_input)
29+
print(f" {repo_input:<50} -> name={name}")
30+
except ValueError as exc:
31+
print(f" {repo_input:<50} -> REJECTED: {exc}")
32+
33+
print("\nFile-path validation")
34+
print("--------------------")
35+
for path in [
36+
"README.md", # safe
37+
"src/main.py", # safe
38+
"../etc/passwd", # traversal
39+
"../../secret.txt", # traversal
40+
"/etc/passwd", # absolute
41+
]:
42+
try:
43+
safe = tools._validate_file_path(path)
44+
print(f" {path:<30} -> SAFE: {safe}")
45+
except ValueError as exc:
46+
print(f" {path:<30} -> REJECT: {exc}")
47+
48+
print("\nListing any previously cloned repos:", tools.list_repos())
49+
50+
51+
if __name__ == "__main__":
52+
main()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Motion Graphics — single-agent factory (requires praisonaiagents + LLM key).
2+
3+
Creates a single motion-graphics authoring agent that writes HTML/GSAP and
4+
renders to MP4 via the HTML backend. Good for quick, one-shot prompts.
5+
6+
Requirements:
7+
pip install praisonai-tools[video-motion] praisonaiagents[llm]
8+
playwright install chromium
9+
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, etc.
10+
11+
Set a working model via env (LiteLLM routing):
12+
export MOTION_LLM=anthropic/claude-sonnet-4-20250514
13+
"""
14+
15+
import os
16+
from pathlib import Path
17+
18+
from praisonai_tools.video.motion_graphics import create_motion_graphics_agent
19+
20+
21+
def main() -> None:
22+
workspace = Path("/tmp/motion_graphics_agent_demo")
23+
workspace.mkdir(exist_ok=True)
24+
25+
llm = os.getenv("MOTION_LLM", "anthropic/claude-sonnet-4-20250514")
26+
27+
agent = create_motion_graphics_agent(
28+
backend="html",
29+
workspace=workspace,
30+
max_retries=3,
31+
llm=llm,
32+
)
33+
34+
print(f"Agent: {agent.name}")
35+
print(f"Backend: {agent._motion_graphics_backend.__class__.__name__}")
36+
print(f"Workspace: {agent._motion_graphics_workspace}")
37+
print(f"Retries: {agent._motion_graphics_max_retries}")
38+
print(f"LLM: {llm}")
39+
print()
40+
print("Starting agent...\n")
41+
42+
agent.start(
43+
"Create a 6-second title-card animation with the text 'Hello Motion' "
44+
"fading in and a cyan underline drawing across. Save as intro.mp4."
45+
)
46+
47+
print("\nArtifacts in workspace:")
48+
for p in sorted(workspace.rglob("*")):
49+
if p.is_file():
50+
print(f" {p.relative_to(workspace)} ({p.stat().st_size} bytes)")
51+
52+
53+
if __name__ == "__main__":
54+
main()
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""Motion Graphics — full team pipeline (requires praisonaiagents + LLM key).
2+
3+
Runs the coordinator + animator team (optionally with researcher and
4+
code-explorer specialists) under a hierarchical process, so the coordinator
5+
validates outputs before marking a render as successful.
6+
7+
Requirements:
8+
pip install praisonai-tools[video-motion] praisonaiagents[llm]
9+
playwright install chromium
10+
export ANTHROPIC_API_KEY=...
11+
export MOTION_LLM=anthropic/claude-sonnet-4-20250514
12+
"""
13+
14+
import os
15+
import time
16+
from pathlib import Path
17+
18+
from praisonai_tools.video.motion_graphics import motion_graphics_team
19+
20+
21+
def main() -> None:
22+
workspace = Path("/tmp/motion_graphics_team_demo")
23+
workspace.mkdir(exist_ok=True)
24+
25+
llm = os.getenv("MOTION_LLM", "anthropic/claude-sonnet-4-20250514")
26+
27+
print(f"[setup] workspace={workspace} llm={llm}")
28+
29+
team = motion_graphics_team(
30+
research=False, # set True to add a web-search researcher
31+
code_exploration=False, # set True to add a GitTools code-explorer
32+
workspace=workspace,
33+
llm=llm,
34+
)
35+
36+
print(f"[setup] {len(team.agents)} agents: {[a.name for a in team.agents]}")
37+
38+
prompt = "Animate Dijkstra's algorithm on a small weighted graph, 30s."
39+
print(f"[run] prompt: {prompt}")
40+
41+
t0 = time.time()
42+
result = team.start(prompt)
43+
elapsed = time.time() - t0
44+
45+
print(f"[done] elapsed={elapsed:.1f}s")
46+
print(f"[result] {result!r}")
47+
48+
print("\n[artifacts]")
49+
for p in sorted(workspace.rglob("*.mp4")):
50+
print(f" mp4 {p} ({p.stat().st_size} bytes)")
51+
for p in sorted(workspace.rglob("*.html")):
52+
print(f" html {p} ({p.stat().st_size} bytes)")
53+
54+
55+
if __name__ == "__main__":
56+
main()

src/praisonai-agents/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "praisonaiagents"
7-
version = "1.6.13"
7+
version = "1.6.14"
88
description = "Praison AI agents for completing complex tasks with Self Reflection Agents"
99
readme = "README.md"
1010
requires-python = ">=3.10"

src/praisonai-agents/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/praisonai/praisonai/cli/features/gateway.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,55 @@
88

99
import asyncio
1010
import logging
11-
from typing import Optional
11+
import os
12+
from pathlib import Path
13+
from typing import Dict, Optional
1214

1315
logger = logging.getLogger(__name__)
1416

1517

18+
def _load_praisonai_env_file() -> Dict[str, str]:
19+
"""Load ``~/.praisonai/.env`` into ``os.environ`` (without overwriting).
20+
21+
Daemons launched by ``launchd`` / ``systemd`` don't inherit the user's
22+
shell env and don't auto-source dotfiles, so secrets written by
23+
``praisonai onboard`` (e.g. ``TELEGRAM_BOT_TOKEN``) are missing when
24+
the gateway starts in the background. We load them here so the
25+
YAML ``${VAR}`` substitution in ``GatewayServer.load_gateway_config``
26+
resolves correctly.
27+
28+
Existing ``os.environ`` values take precedence (so user-set shell
29+
vars always win). Returns the dict of keys we loaded (for logging).
30+
"""
31+
env_path = Path(os.environ.get("PRAISONAI_ENV_FILE")
32+
or (Path.home() / ".praisonai" / ".env"))
33+
loaded: Dict[str, str] = {}
34+
if not env_path.exists():
35+
return loaded
36+
try:
37+
for raw in env_path.read_text().splitlines():
38+
s = raw.strip()
39+
if not s or s.startswith("#") or "=" not in s:
40+
continue
41+
k, v = s.split("=", 1)
42+
k = k.strip()
43+
v = v.strip().strip('"').strip("'")
44+
if not k:
45+
continue
46+
if k in os.environ:
47+
continue # don't clobber existing env
48+
os.environ[k] = v
49+
loaded[k] = v
50+
except OSError as exc:
51+
logger.warning("Could not read %s: %s", env_path, exc)
52+
if loaded:
53+
logger.info(
54+
"Loaded %d env var(s) from %s: %s",
55+
len(loaded), env_path, ", ".join(sorted(loaded.keys())),
56+
)
57+
return loaded
58+
59+
1660
class GatewayHandler:
1761
"""Handler for gateway CLI commands."""
1862

@@ -34,6 +78,28 @@ def start(
3478
agent_file: Optional path to agent configuration file
3579
config_file: Optional path to gateway.yaml for multi-bot mode
3680
"""
81+
# Ensure INFO-level logs surface to bot-stdout.log / bot-stderr.log
82+
# when running under launchd / systemd. Many key lifecycle events
83+
# (bot start, channel routing, scheduler tick, retries) are already
84+
# emitted via `logger.info()` — they just weren't visible with the
85+
# default WARNING root level. Only configure if nothing is set yet,
86+
# so users/embedders keep control.
87+
_root = logging.getLogger()
88+
if not _root.handlers:
89+
logging.basicConfig(
90+
level=logging.INFO,
91+
format="%(asctime)s %(name)s %(levelname)s %(message)s",
92+
)
93+
if _root.level > logging.INFO or _root.level == logging.NOTSET:
94+
_root.setLevel(logging.INFO)
95+
96+
# Load ~/.praisonai/.env BEFORE any config parsing or ${VAR}
97+
# substitution — daemons don't inherit shell env.
98+
_load_praisonai_env_file()
99+
logger.info(
100+
"Gateway starting (host=%s port=%s config=%s agents=%s)",
101+
host, port, config_file or "-", agent_file or "-",
102+
)
37103
try:
38104
from praisonai.gateway import WebSocketGateway
39105
from praisonaiagents.gateway import GatewayConfig

src/praisonai/praisonai/gateway/server.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1030,15 +1030,26 @@ def _resolve_agent(agent_id):
10301030

10311031
@staticmethod
10321032
def _substitute_env_vars(value: str) -> str:
1033-
"""Replace ${VAR_NAME} patterns with environment variable values."""
1033+
"""Replace ``${VAR_NAME}`` patterns with environment variable values.
1034+
1035+
Previously, missing env vars returned the literal ``${VAR_NAME}`` which
1036+
caused downstream APIs (e.g. Telegram) to receive a broken token
1037+
string and fail with opaque 404s. We now substitute missing vars with
1038+
an **empty string** so the schema validator's required-field checks
1039+
(e.g. ``token`` must be truthy) trip cleanly instead.
1040+
"""
10341041
if not isinstance(value, str):
10351042
return value
10361043
def _replacer(match):
10371044
var_name = match.group(1)
10381045
env_val = os.environ.get(var_name)
10391046
if env_val is None:
1040-
logger.warning(f"Environment variable {var_name} not set")
1041-
return match.group(0)
1047+
logger.warning(
1048+
f"Environment variable {var_name} not set "
1049+
f"— substituting empty string. "
1050+
f"Run `praisonai onboard` or set it in ~/.praisonai/.env."
1051+
)
1052+
return ""
10421053
return env_val
10431054
return re.sub(r'\$\{([^}]+)\}', _replacer, value)
10441055

0 commit comments

Comments
 (0)