Patrick Rathje commited on
Commit
57c34dd
·
1 Parent(s): e5509f7
Files changed (8) hide show
  1. Dockerfile +1 -44
  2. README.md +6 -6
  3. app.py +23 -132
  4. extract_examples.py +0 -23
  5. gradio_mcp_server.py +113 -0
  6. llm.py +0 -0
  7. modal_run.py +0 -0
  8. modal_setup.py +0 -0
Dockerfile CHANGED
@@ -1,57 +1,14 @@
1
  FROM python:3.10-slim
2
 
3
-
4
- # Install Node to run Motion Canvas
5
- WORKDIR /tmp/node
6
- RUN apt-get update && apt-get install -y nodejs npm
7
-
8
-
9
- # without v prefix!
10
- ARG MC_VERSION="3.17.2"
11
- ENV NODE_ENV=$MC_VERSION
12
-
13
- WORKDIR /motion-canvas
14
-
15
- RUN yes '' | npm init -y @motion-canvas@$MC_VERSION
16
-
17
- WORKDIR /motion-canvas/my-animation
18
- ENV MC_PROJECT_DIR=/motion-canvas/my-animation
19
-
20
- # copy the vite.config.ts file to the project to get deterministic output filenames under dist/project.js
21
- COPY docker/vite.config.ts ./vite.config.ts
22
-
23
- # if we build here, we have to clean up the project every time before building a new one...
24
- RUN npm install
25
- RUN npm run build
26
-
27
- # clone examples
28
- #ENV MC_EXAMPLE_PROJECTS_DIR=/motion-canvas/examples
29
- #RUN git clone https://github.com/motion-canvas/examples /motion-canvas/example-projects
30
-
31
- ENV MC_DIR=/motion-canvas/motion-canvas
32
- ENV MC_DOCS_DIR=/motion-canvas/motion-canvas/packages/docs
33
- RUN git clone https://github.com/motion-canvas/motion-canvas /motion-canvas/motion-canvas
34
-
35
- ENV MC_EXAMPLE_SCENES_DIR=/motion-canvas/motion-canvas/packages/examples/src/scenes
36
-
37
- # docs/docs and docs/api should be interesting resources
38
-
39
- RUN git clone https://github.com/motion-canvas/motion-canvas.github.io /motion-canvas/docs
40
-
41
-
42
  # Install Gradio and copy local files
43
  WORKDIR /app
44
- RUN pip install --no-cache-dir gradio[mcp] gradio_motioncanvasplayer
45
  COPY . .
46
 
47
  RUN mkdir -p /app/public
48
- ENV MC_EXAMPLES_MD_FILE=/app/public/examples.md
49
- RUN python extract_examples.py $MC_EXAMPLES_MD_FILE
50
-
51
 
52
  RUN useradd -m -u 1000 user
53
 
54
- RUN chown -R user:user /motion-canvas
55
  RUN chown -R user:user /app
56
  USER user
57
 
 
1
  FROM python:3.10-slim
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  # Install Gradio and copy local files
4
  WORKDIR /app
5
+ RUN pip install --no-cache-dir gradio[mcp] gradio_motioncanvasplayer modal
6
  COPY . .
7
 
8
  RUN mkdir -p /app/public
 
 
 
9
 
10
  RUN useradd -m -u 1000 user
11
 
 
12
  RUN chown -R user:user /app
13
  USER user
14
 
README.md CHANGED
@@ -1,22 +1,22 @@
1
  ---
2
- title: Motion Canvas MCP Server
3
  emoji: 📹
4
  colorFrom: red
5
  colorTo: gray
6
  sdk: docker
7
  app_port: 7860
8
  pinned: true
9
- short_description: Gradio MCP Server to build Motion Canvas projects
10
  tags:
11
- - mcp-server-track
12
  ---
13
 
14
  ## Run
15
 
16
 
17
  ```bash
18
- docker build -t mc-gradio .
19
- docker run -p 7860:7860 mc-gradio
20
  ```
21
 
22
- The Motion Canvas Gradio MCP server will be available under *http://localhost:7860*
 
1
  ---
2
+ title: Motion Canvas Agent
3
  emoji: 📹
4
  colorFrom: red
5
  colorTo: gray
6
  sdk: docker
7
  app_port: 7860
8
  pinned: true
9
+ short_description: Agent to create Motion Canvas animations
10
  tags:
11
+ - agent-demo-track
12
  ---
13
 
14
  ## Run
15
 
16
 
17
  ```bash
18
+ docker build -t mc_agent .
19
+ docker run -p 7860:7860 mc_agent
20
  ```
21
 
22
+ The Gradio Interface will be available under *http://localhost:7860*
app.py CHANGED
@@ -6,152 +6,43 @@ import uuid
6
  import subprocess
7
  from threading import Timer
8
  from functools import partial
 
9
 
10
  from gradio_motioncanvasplayer import MotionCanvasPlayer
11
 
 
12
  example_project_path = "https://prathje-gradio-motioncanvasplayer.hf.space/gradio_api/file=/home/user/app/public/project-3.17.2.js"
13
 
14
- loading_project_path = ""
15
- failed_project_path = ""
 
 
16
 
17
- BUILD_TIMEOUT = 60
18
-
19
- gr.set_static_paths(paths=[os.path.join(os.path.dirname(__file__), "public")])
20
-
21
- def get_local_path(project_id):
22
- return os.path.join(os.path.dirname(__file__), "public", "project-" + project_id + ".js")
23
-
24
- def get_public_path(project_id):
25
- return "/gradio_api/file=" + get_local_path(project_id)
26
-
27
-
28
- def build_project(code):
29
- # TODO: as soon as gradio supports states, we should keep the project_id for some time...
30
- # TODO: In the best case, we start a separate container for each project!
31
- yield loading_project_path, "Preparing project...",
32
-
33
- # generate a random uuid for the project
34
- project_id = str(uuid.uuid4())
35
-
36
- tmp_dir = os.path.join("/tmp/", project_id)
37
-
38
- shutil.copytree(os.environ['MC_PROJECT_DIR'], tmp_dir, dirs_exist_ok=False, symlinks=True)
39
- acc_logs = ""
40
-
41
- try:
42
- yield loading_project_path, "Building project...",
43
-
44
- # write code to scene.ts
45
- with open(os.path.join(tmp_dir, "src", "scenes", "example.tsx"), "w") as f:
46
- f.write(code)
47
-
48
- process = subprocess.Popen(
49
- "npm run build",
50
- stdout=subprocess.PIPE,
51
- stderr=subprocess.PIPE,
52
- text=True,
53
- shell=True,
54
- cwd=tmp_dir
55
- )
56
-
57
- timer = Timer(BUILD_TIMEOUT, process.kill)
58
- timer.start()
59
-
60
- while True:
61
- line = process.stdout.readline()
62
- if line:
63
- acc_logs += line.rstrip() + "\n"
64
- yield loading_project_path, acc_logs
65
- elif process.poll() is not None:
66
- break
67
- timer.cancel()
68
-
69
- # Check for errors
70
- stderr_output = process.stderr.read()
71
- if stderr_output:
72
- acc_logs += "\n" + stderr_output
73
-
74
- # check if the build was successful
75
- if process.returncode != 0:
76
- yield failed_project_path, acc_logs
77
- else:
78
- # copy dist/project.js to get_local_path(id)
79
- shutil.copy(os.path.join(tmp_dir, "dist", "project.js"), get_local_path(project_id))
80
- yield get_public_path(project_id), acc_logs
81
-
82
- except Exception as e:
83
- yield failed_project_path, acc_logs + "\n" + "Error building project: " + str(e)
84
-
85
- finally:
86
- # cleanup tmp dir
87
- shutil.rmtree(tmp_dir)
88
-
89
- def load_example_code(example_name):
90
- return open(os.path.join(os.path.dirname(__file__), "examples", example_name + ".tsx")).read()
91
-
92
- def build_example(example_name):
93
- iterator = build_project(load_example_code(example_name))
94
- last = next(iterator)
95
- for last in iterator:
96
- continue
97
-
98
- path = last[0]
99
- assert path, "Failed to build example: " + last[1]
100
- return path
101
-
102
- print("Building examples...")
103
- EXAMPLES = [
104
- {
105
- "name": "Gradio + Motion Canvas",
106
- "code": load_example_code("gradio-motion-canvas-example"),
107
- "project_path": build_example("gradio-motion-canvas-example")
108
- },
109
- {
110
- "name": "Logo",
111
- "code": load_example_code("logo"),
112
- "project_path": build_example("logo")
113
- },
114
- {
115
- "name": "Latex",
116
- "code": load_example_code("latex"),
117
- "project_path": build_example("latex")
118
- },
119
- {
120
- "name": "Code",
121
- "code": load_example_code("code"),
122
- "project_path": build_example("code")
123
- },
124
-
125
- ]
126
- print("Examples built!")
127
 
128
  def load_example(example):
129
  return example['project_path'], example['code'], ""
130
 
131
- with gr.Blocks(theme=gr.themes.Monochrome()) as demo:
132
- gr.Markdown("# Motion Canvas MCP Server")
133
- gr.Markdown("Leverage the power of Motion Canvas to create your own animations using TypeScript.")
134
  with gr.Row():
135
  with gr.Column():
136
- gr.Markdown("## TypeScript Input for Your Scene")
137
- code = gr.Code(value=EXAMPLES[0]['code'], language="typescript")
138
- submit = gr.Button("Build", variant="primary")
 
 
 
139
  logs = gr.Textbox(value="", label="Build Logs", interactive=False)
 
140
  with gr.Column():
141
  gr.Markdown("## Preview")
142
- player = MotionCanvasPlayer(EXAMPLES[0]['project_path'], auto=True, quality=0.5, width=1920, height=1080, variables="{}")
143
-
144
- for ex in EXAMPLES:
145
- btn = gr.Button("Load Example: " + ex["name"], variant="secondary")
146
- btn.click(partial(load_example, ex), outputs=[player, code, logs], api_name=False)
147
-
148
- with gr.Accordion("Show Example Scenes"):
149
- example_md_content = open(os.path.join(os.path.dirname(__file__), "public", "examples.md")).read()
150
- example_md = gr.Markdown(example_md_content, render=True)
151
- load_example_md_btn = gr.Button("Load Example", variant="secondary", render=False)
152
- load_example_md_btn.click(lambda: example_md_content, outputs=[example_md], api_name="examples")
153
-
154
- submit.click(build_project, inputs=[code], outputs=[player, logs], api_name="build")
155
 
156
  if __name__ == "__main__":
157
- demo.launch(mcp_server=True, strict_cors=False)
 
 
6
  import subprocess
7
  from threading import Timer
8
  from functools import partial
9
+ import time
10
 
11
  from gradio_motioncanvasplayer import MotionCanvasPlayer
12
 
13
+ # Just some example project that servers as a placholder in the beginning
14
  example_project_path = "https://prathje-gradio-motioncanvasplayer.hf.space/gradio_api/file=/home/user/app/public/project-3.17.2.js"
15
 
16
+ def slow_echo(message, history):
17
+ for i in range(len(message)):
18
+ time.sleep(0.3)
19
+ yield "You typed: " + message[: i+1]
20
 
21
+ gr.ChatInterface(
22
+ fn=slow_echo,
23
+ type="messages"
24
+ ).launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
  def load_example(example):
27
  return example['project_path'], example['code'], ""
28
 
29
+ with gr.Blocks(theme=gr.themes.Monochrome()) as app:
30
+ gr.Markdown("# Motion Canvas Agent")
31
+ gr.Markdown("Leverage the power of AI and Motion Canvas to create animations using TypeScript.")
32
  with gr.Row():
33
  with gr.Column():
34
+ gr.Markdown("## Chat")
35
+ chat = gr.ChatInterface(fn=slow_echo, type="messages")
36
+
37
+
38
+ gr.Markdown("### TypeScript Code for Your Scene")
39
+ code = gr.Code(value="", language="typescript")
40
  logs = gr.Textbox(value="", label="Build Logs", interactive=False)
41
+
42
  with gr.Column():
43
  gr.Markdown("## Preview")
44
+ player = MotionCanvasPlayer(example_project_path, auto=True, quality=0.5, width=1920, height=1080, variables="{}")
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
  if __name__ == "__main__":
47
+ # Todo: In the future we could allow to use this as an MCP server, but right now, we need the preview to be available.
48
+ app.launch(mcp_server=False, strict_cors=False)
extract_examples.py DELETED
@@ -1,23 +0,0 @@
1
- import os
2
- import glob
3
- from pathlib import Path
4
-
5
- assert os.environ['MC_EXAMPLE_SCENES_DIR']
6
- assert os.environ['MC_EXAMPLES_MD_FILE']
7
-
8
- with open(os.environ['MC_EXAMPLES_MD_FILE'], "w") as f:
9
-
10
- f.write(f"# Examples for Motion Canvas Scenes in TypeScript\n")
11
-
12
- for filepath in glob.iglob(os.environ['MC_EXAMPLE_SCENES_DIR'] + '/**/*.tsx', recursive=True):
13
- name = Path(filepath).stem
14
-
15
- f.write(f"\n\n## {name}\n")
16
-
17
- # read the file
18
- with open(filepath, 'r') as scene_file:
19
- content = scene_file.read()
20
-
21
- f.write(f"```tsx\n")
22
- f.write(content)
23
- f.write(f"```\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
gradio_mcp_server.py ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from mcp.server.fastmcp import FastMCP
2
+ import json
3
+ import sys
4
+ import io
5
+ import time
6
+ from gradio_client import Client
7
+
8
+ sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
9
+ sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
10
+
11
+ mcp = FastMCP("Gradio MCP Server")
12
+
13
+ clients = {}
14
+
15
+ BUILD_SERVER_SPACE_ID = "https://agents-mcp-hackathon-gradio-motioncanvas-mcp-server.hf.space/"
16
+ DOCS_SERVER_SPACE_ID = "https://agents-mcp-hackathon-gradio-motioncanvas-docs-mcp-server.hf.space/"
17
+
18
+ def get_client(space_id: str) -> Client:
19
+ """Get or create a Gradio client for the specified space."""
20
+ if space_id not in clients:
21
+ clients[space_id] = Client(space_id)
22
+ return clients[space_id]
23
+
24
+ @mcp.tool()
25
+ async def build_project(code: str) -> str:
26
+ """Build a Motion Canvas project using the Gradio Motion MCP server.
27
+
28
+ Args:
29
+ code: TypeScript code for the scene to build
30
+ """
31
+
32
+ try:
33
+ result = get_client(BUILD_SERVER_SPACE_ID).predict(
34
+ code,
35
+ api_name="/build"
36
+ )
37
+
38
+ if isinstance(result, list) and len(result) >= 2:
39
+ project_path = result[0]
40
+ logs = result[1]
41
+
42
+ if project_path:
43
+ return json.dumps({
44
+ "type": "mc_project",
45
+ "project_path": project_path,
46
+ "message": f"Built project at {project_path}",
47
+ "logs": result[1]
48
+ })
49
+ else:
50
+ return json.dumps({
51
+ "type": "error",
52
+ "message": f"Error building project:\n" + logs
53
+ })
54
+ else:
55
+ return json.dumps({
56
+ "type": "error",
57
+ "message": f"Error building project"
58
+ })
59
+
60
+ except Exception as e:
61
+ return json.dumps({
62
+ "type": "error",
63
+ "message": f"Error building project: {str(e)}"
64
+ })
65
+
66
+ @mcp.tool()
67
+ async def build_project(code: str) -> str:
68
+ """Build a Motion Canvas project using the Gradio Motion MCP server.
69
+
70
+ Args:
71
+ code: TypeScript code for the scene to build
72
+ """
73
+ client = Client("https://agents-mcp-hackathon-gradio-motioncanvas-mcp-server.hf.space/")
74
+
75
+
76
+
77
+
78
+ try:
79
+ result = client.predict(
80
+ code,
81
+ api_name="/build"
82
+ )
83
+
84
+ if isinstance(result, list) and len(result) >= 2:
85
+ project_path = result[0]
86
+ logs = result[1]
87
+
88
+ if project_path:
89
+ return json.dumps({
90
+ "type": "mc_project",
91
+ "project_path": project_path,
92
+ "message": f"Built project at {project_path}",
93
+ "logs": result[1]
94
+ })
95
+ else:
96
+ return json.dumps({
97
+ "type": "error",
98
+ "message": f"Error building project:\n" + logs
99
+ })
100
+ else:
101
+ return json.dumps({
102
+ "type": "error",
103
+ "message": f"Error building project"
104
+ })
105
+
106
+ except Exception as e:
107
+ return json.dumps({
108
+ "type": "error",
109
+ "message": f"Error building project: {str(e)}"
110
+ })
111
+
112
+ if __name__ == "__main__":
113
+ mcp.run(transport='stdio')
llm.py ADDED
File without changes
modal_run.py ADDED
File without changes
modal_setup.py ADDED
File without changes