Ava2lon commited on
Commit
6c2642f
·
verified ·
1 Parent(s): 99d5f49

Upload 214 files

Browse files
Files changed (2) hide show
  1. renderer_app.py +97 -46
  2. tests/test_mcp_metadata.py +56 -0
renderer_app.py CHANGED
@@ -15,6 +15,11 @@ from renderer.studio import capability_catalog
15
  from renderer.templates import apply_creative_style, apply_preset, list_creative_styles, list_platform_profiles, list_templates
16
 
17
 
 
 
 
 
 
18
  def create_dashboard() -> gr.Blocks:
19
  with gr.Blocks(title="Ava2lon Studio AI") as dashboard:
20
  gr.Markdown(
@@ -27,8 +32,8 @@ def create_dashboard() -> gr.Blocks:
27
  project_create = gr.Button("Create Project", variant="primary")
28
  project_list = gr.Button("Refresh Projects")
29
  project_output = gr.JSON(label="Projects")
30
- project_create.click(fn=_create_project, inputs=[project_name, project_metadata], outputs=project_output)
31
- project_list.click(fn=_list_projects, outputs=project_output)
32
 
33
  with gr.Tab("Assets"):
34
  gr.Markdown("Use `/upload` or `/assets/upload` for multipart assets, then attach them to a project with `/project/assets/add`.")
@@ -36,7 +41,7 @@ def create_dashboard() -> gr.Blocks:
36
  asset_json = gr.Textbox(label="Asset JSON", lines=6, value=json.dumps({"path": "clip.mp4", "kind": "video"}, indent=2))
37
  asset_button = gr.Button("Attach Asset", variant="primary")
38
  asset_output = gr.JSON(label="Project")
39
- asset_button.click(fn=_add_project_asset, inputs=[asset_project_id, asset_json], outputs=asset_output)
40
 
41
  with gr.Tab("Timeline"):
42
  timeline_project_id = gr.Textbox(label="Project ID")
@@ -48,7 +53,7 @@ def create_dashboard() -> gr.Blocks:
48
  )
49
  timeline_add_button = gr.Button("Add To Timeline", variant="primary")
50
  timeline_output = gr.JSON(label="Project")
51
- timeline_add_button.click(fn=_timeline_add, inputs=[timeline_project_id, timeline_track_type, timeline_item], outputs=timeline_output)
52
 
53
  timeline_operation_json = gr.Textbox(
54
  label="Operation JSON",
@@ -56,40 +61,40 @@ def create_dashboard() -> gr.Blocks:
56
  value=json.dumps({"operation": "split", "item_id": "clip_123", "params": {"offset": 2.5}}, indent=2),
57
  )
58
  timeline_operation_button = gr.Button("Apply Operation")
59
- timeline_operation_button.click(fn=_timeline_operation, inputs=[timeline_project_id, timeline_operation_json], outputs=timeline_output)
60
 
61
  with gr.Tab("Templates"):
62
  template_button = gr.Button("Load Template Catalog")
63
  template_output = gr.JSON(label="Templates")
64
- template_button.click(fn=lambda: _catalog_section("templates"), outputs=template_output)
65
 
66
  with gr.Tab("Effects"):
67
  effect_button = gr.Button("Load Effect Catalog")
68
  effect_output = gr.JSON(label="Effects")
69
- effect_button.click(fn=lambda: _catalog_section("effects"), outputs=effect_output)
70
 
71
  with gr.Tab("Filters"):
72
  filter_button = gr.Button("Load Filter Catalog")
73
  filter_output = gr.JSON(label="Filters")
74
- filter_button.click(fn=lambda: _catalog_section("filters"), outputs=filter_output)
75
 
76
  with gr.Tab("Captions"):
77
  caption_text = gr.Textbox(label="Caption source text", lines=6)
78
  caption_button = gr.Button("Submit Caption Job", variant="primary")
79
  caption_output = gr.JSON(label="Caption Job")
80
- caption_button.click(fn=_submit_caption_generation, inputs=caption_text, outputs=caption_output)
81
 
82
  with gr.Tab("Audio"):
83
  audio_button = gr.Button("Load Audio Catalog")
84
  audio_output = gr.JSON(label="Audio")
85
- audio_button.click(fn=lambda: {"audio": capability_catalog()["audio"], "music": capability_catalog()["music_generator"]}, outputs=audio_output)
86
 
87
  with gr.Tab("AI Tools"):
88
  ai_tool = gr.Dropdown(choices=capability_catalog()["ai_editing"], value="auto_viral_score", label="AI tool")
89
  ai_payload = gr.Textbox(label="AI payload JSON", lines=8, value=json.dumps({"platform": "tiktok", "text": "A strong opening hook"}, indent=2))
90
  ai_button = gr.Button("Submit AI Tool", variant="primary")
91
  ai_output = gr.JSON(label="AI Job")
92
- ai_button.click(fn=_submit_ai_tool, inputs=[ai_tool, ai_payload], outputs=ai_output)
93
 
94
  with gr.Tab("Rendering"):
95
  render_json = gr.Textbox(
@@ -100,7 +105,7 @@ def create_dashboard() -> gr.Blocks:
100
  )
101
  render_button = gr.Button("Submit Render", variant="primary")
102
  render_output = gr.JSON(label="Submission")
103
- render_button.click(fn=_submit_render_json, inputs=render_json, outputs=render_output)
104
 
105
  with gr.Tab("AI Reels"):
106
  script = gr.Textbox(label="Script", lines=6)
@@ -112,31 +117,31 @@ def create_dashboard() -> gr.Blocks:
112
  music = gr.File(label="Background Music", file_types=["audio"], type="filepath")
113
  ai_button = gr.Button("Submit AI Reel", variant="primary")
114
  ai_output = gr.JSON(label="Submission")
115
- ai_button.click(fn=_submit_ai_reel, inputs=[script, voiceover, assets, template, creative_style, platform, music], outputs=ai_output)
116
 
117
  with gr.Tab("Batch Render"):
118
  batch_json = gr.Textbox(label="Batch JSON", lines=14, value=json.dumps({"jobs": []}, indent=2))
119
  batch_button = gr.Button("Submit Batch", variant="primary")
120
  batch_output = gr.JSON(label="Batch Submission")
121
- batch_button.click(fn=_submit_batch_json, inputs=batch_json, outputs=batch_output)
122
 
123
  with gr.Tab("Job Status"):
124
  status_job_id = gr.Textbox(label="Job ID")
125
  status_button = gr.Button("Refresh")
126
  status_output = gr.JSON(label="Status")
127
- status_button.click(fn=_job_status, inputs=status_job_id, outputs=status_output)
128
 
129
  with gr.Tab("Logs"):
130
  logs_job_id = gr.Textbox(label="Job ID")
131
  logs_button = gr.Button("Load Logs")
132
  logs_output = gr.Textbox(label="Logs", lines=20)
133
- logs_button.click(fn=_job_logs, inputs=logs_job_id, outputs=logs_output)
134
 
135
  with gr.Tab("Downloads"):
136
  download_job_id = gr.Textbox(label="Job ID")
137
  download_button = gr.Button("Get Output")
138
  download_output = gr.File(label="Rendered Video")
139
- download_button.click(fn=_download_path, inputs=download_job_id, outputs=download_output)
140
 
141
  with gr.Tab("Transcribe"):
142
  transcribe_audio = gr.File(label="Audio or Video", file_types=["audio", "video"], type="filepath")
@@ -149,16 +154,18 @@ def create_dashboard() -> gr.Blocks:
149
  transcribe_button = gr.Button("Transcribe", variant="primary")
150
  transcribe_output = gr.JSON(label="Transcript")
151
  transcribe_button.click(
152
- fn=_transcribe_file,
153
  inputs=[transcribe_audio, transcribe_model, transcribe_language],
154
  outputs=transcribe_output,
 
 
155
  )
156
 
157
  with gr.Tab("Asset Inspector"):
158
  asset_path = gr.Textbox(label="Asset path")
159
  inspect_button = gr.Button("Inspect")
160
  inspect_output = gr.JSON(label="Metadata")
161
- inspect_button.click(fn=_inspect_asset, inputs=asset_path, outputs=inspect_output)
162
 
163
  with gr.Tab("AI Analysis"):
164
  analysis_media = gr.Textbox(label="Media URL or path")
@@ -167,9 +174,11 @@ def create_dashboard() -> gr.Blocks:
167
  analysis_button = gr.Button("Submit Analysis", variant="primary")
168
  analysis_output = gr.JSON(label="Analysis Job")
169
  analysis_button.click(
170
- fn=_submit_analysis,
171
  inputs=[analysis_media, analysis_transcript, analysis_platform],
172
  outputs=analysis_output,
 
 
173
  )
174
 
175
  with gr.Tab("Clip Generator"):
@@ -177,14 +186,14 @@ def create_dashboard() -> gr.Blocks:
177
  clip_json = gr.Textbox(label="Clip JSON", lines=6, value=json.dumps([{"start": 0, "end": 8}], indent=2))
178
  clip_button = gr.Button("Generate Clips", variant="primary")
179
  clip_output = gr.JSON(label="Clip Job")
180
- clip_button.click(fn=_submit_clips, inputs=[clip_media, clip_json], outputs=clip_output)
181
 
182
  with gr.Tab("Metadata"):
183
  metadata_topic = gr.Textbox(label="Topic or transcript", lines=5)
184
  metadata_platform = gr.Dropdown(choices=list_platform_profiles(), value="tiktok", label="Platform")
185
  metadata_button = gr.Button("Generate Metadata", variant="primary")
186
  metadata_output = gr.JSON(label="Metadata Job")
187
- metadata_button.click(fn=_submit_metadata, inputs=[metadata_topic, metadata_platform], outputs=metadata_output)
188
 
189
  with gr.Tab("Publishing"):
190
  publish_media = gr.Textbox(label="Media URL or rendered output path")
@@ -192,46 +201,52 @@ def create_dashboard() -> gr.Blocks:
192
  publish_platforms = gr.Textbox(label="Platforms", value="youtube,tiktok,instagram")
193
  publish_button = gr.Button("Create Publish Draft", variant="primary")
194
  publish_output = gr.JSON(label="Publish Job")
195
- publish_button.click(fn=_submit_publish, inputs=[publish_media, publish_title, publish_platforms], outputs=publish_output)
196
 
197
  with gr.Tab("Settings"):
198
  settings_button = gr.Button("Load Settings")
199
  settings_output = gr.JSON(label="Settings")
200
- settings_button.click(fn=_settings_payload, outputs=settings_output)
201
 
202
  with gr.Tab("Queue Monitor"):
203
  queue_button = gr.Button("Refresh Queue")
204
  queue_output = gr.JSON(label="Queue")
205
- queue_button.click(fn=_queue_status, outputs=queue_output)
206
 
207
  return dashboard
208
 
209
 
210
- def _submit_render_json(payload: str) -> dict[str, Any]:
 
211
  data = apply_creative_style(apply_preset(json.loads(payload)))
212
  request = Timeline.request_from_payload(data)
213
  job_id = job_manager.submit_render(request)
214
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
215
 
216
 
217
- def _create_project(name: str, metadata_json: str) -> dict[str, Any]:
 
218
  metadata = json.loads(metadata_json or "{}")
219
  return {"project": project_manager.create(name, metadata=metadata)}
220
 
221
 
222
- def _list_projects() -> dict[str, Any]:
 
223
  return {"projects": project_manager.list()}
224
 
225
 
226
- def _add_project_asset(project_id: str, asset_json: str) -> dict[str, Any]:
 
227
  return {"project": project_manager.add_asset(project_id, json.loads(asset_json or "{}"))}
228
 
229
 
230
- def _timeline_add(project_id: str, track_type: str, item_json: str) -> dict[str, Any]:
 
231
  return {"project": project_manager.add_to_timeline(project_id, json.loads(item_json or "{}"), track_type=track_type)}
232
 
233
 
234
- def _timeline_operation(project_id: str, operation_json: str) -> dict[str, Any]:
 
235
  data = json.loads(operation_json or "{}")
236
  return {
237
  "project": project_manager.timeline_operation(
@@ -248,14 +263,37 @@ def _catalog_section(section: str) -> dict[str, Any]:
248
  return {section: catalog.get(section)}
249
 
250
 
251
- def _submit_caption_generation(text: str) -> dict[str, Any]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  from renderer.studio import StudioTaskProcessor
253
 
254
  job_id = job_manager.submit_task(lambda task_id, log: StudioTaskProcessor(settings, log=log).caption_generate({"text": text}, task_id))
255
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
256
 
257
 
258
- def _submit_ai_tool(tool: str, payload: str) -> dict[str, Any]:
 
259
  from renderer.studio import StudioTaskProcessor
260
 
261
  data = json.loads(payload or "{}")
@@ -263,13 +301,14 @@ def _submit_ai_tool(tool: str, payload: str) -> dict[str, Any]:
263
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
264
 
265
 
266
- def _submit_batch_json(payload: str) -> dict[str, Any]:
 
267
  data = json.loads(payload)
268
  requests = [Timeline.request_from_payload(apply_creative_style(apply_preset(job))) for job in data.get("jobs", [])]
269
  return {"job_ids": job_manager.submit_batch(requests)}
270
 
271
 
272
- def _submit_ai_reel(
273
  script: str,
274
  voiceover: str,
275
  assets: list[str],
@@ -278,6 +317,7 @@ def _submit_ai_reel(
278
  platform: str,
279
  music: str | None,
280
  ) -> dict[str, Any]:
 
281
  request = AIReelsRequest(
282
  script=script,
283
  voiceover=voiceover,
@@ -291,26 +331,31 @@ def _submit_ai_reel(
291
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
292
 
293
 
294
- def _job_status(job_id: str) -> dict[str, Any]:
 
295
  return job_manager.get(job_id).__dict__
296
 
297
 
298
- def _job_logs(job_id: str) -> str:
 
299
  return "\n\n".join(job_manager.get(job_id).logs)
300
 
301
 
302
- def _download_path(job_id: str) -> str | None:
 
303
  record = job_manager.get(job_id)
304
  if record.state != "COMPLETED":
305
  return None
306
  return record.output_path
307
 
308
 
309
- def _inspect_asset(path: str) -> dict[str, Any]:
 
310
  return RenderEngine(settings).inspect_asset(path)
311
 
312
 
313
- def _transcribe_file(path: str, model_size: str, language: str) -> dict[str, Any]:
 
314
  return RenderEngine(settings).transcribe(
315
  path,
316
  model_size=model_size,
@@ -319,7 +364,8 @@ def _transcribe_file(path: str, model_size: str, language: str) -> dict[str, Any
319
  )
320
 
321
 
322
- def _submit_analysis(media: str, transcript: str, platform: str) -> dict[str, Any]:
 
323
  from renderer.platform import PlatformProcessor
324
 
325
  job_id = job_manager.submit_task(
@@ -328,7 +374,8 @@ def _submit_analysis(media: str, transcript: str, platform: str) -> dict[str, An
328
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
329
 
330
 
331
- def _submit_clips(media: str, clips_json: str) -> dict[str, Any]:
 
332
  from renderer.platform import PlatformProcessor
333
 
334
  clips = json.loads(clips_json)
@@ -336,14 +383,16 @@ def _submit_clips(media: str, clips_json: str) -> dict[str, Any]:
336
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
337
 
338
 
339
- def _submit_metadata(topic: str, platform: str) -> dict[str, Any]:
 
340
  from renderer.platform import PlatformProcessor
341
 
342
  job_id = job_manager.submit_task(lambda task_id, log: PlatformProcessor(settings, log=log).metadata(task_id, topic=topic, platform=platform))
343
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
344
 
345
 
346
- def _submit_publish(media: str, title: str, platforms: str) -> dict[str, Any]:
 
347
  from renderer.platform import PlatformProcessor
348
 
349
  payload = {"media": media, "title": title, "platforms": [item.strip() for item in platforms.split(",") if item.strip()], "draft": True}
@@ -351,11 +400,13 @@ def _submit_publish(media: str, title: str, platforms: str) -> dict[str, Any]:
351
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
352
 
353
 
354
- def _queue_status() -> dict[str, Any]:
 
355
  return job_manager.summary()
356
 
357
 
358
- def _settings_payload() -> dict[str, Any]:
 
359
  return {
360
  "product": "Ava2lon Studio AI",
361
  "base_dir": str(settings.base_dir),
 
15
  from renderer.templates import apply_creative_style, apply_preset, list_creative_styles, list_platform_profiles, list_templates
16
 
17
 
18
+ def _mcp_description(summary: str, example: dict[str, Any]) -> str:
19
+ """Build a concise MCP description with a machine-readable JSON example."""
20
+ return f"{summary} Example JSON: {json.dumps(example, separators=(',', ':'), ensure_ascii=True)}"
21
+
22
+
23
  def create_dashboard() -> gr.Blocks:
24
  with gr.Blocks(title="Ava2lon Studio AI") as dashboard:
25
  gr.Markdown(
 
32
  project_create = gr.Button("Create Project", variant="primary")
33
  project_list = gr.Button("Refresh Projects")
34
  project_output = gr.JSON(label="Projects")
35
+ project_create.click(fn=create_project, inputs=[project_name, project_metadata], outputs=project_output, api_name="create_project", api_description=_mcp_description("Create a project from a name and JSON metadata object.", {"name": "Campaign 01", "metadata_json": '{"platform":"tiktok"}'}))
36
+ project_list.click(fn=list_projects, outputs=project_output, api_name="list_projects", api_description=_mcp_description("List saved studio projects.", {}))
37
 
38
  with gr.Tab("Assets"):
39
  gr.Markdown("Use `/upload` or `/assets/upload` for multipart assets, then attach them to a project with `/project/assets/add`.")
 
41
  asset_json = gr.Textbox(label="Asset JSON", lines=6, value=json.dumps({"path": "clip.mp4", "kind": "video"}, indent=2))
42
  asset_button = gr.Button("Attach Asset", variant="primary")
43
  asset_output = gr.JSON(label="Project")
44
+ asset_button.click(fn=add_project_asset, inputs=[asset_project_id, asset_json], outputs=asset_output, api_name="add_project_asset", api_description=_mcp_description("Attach a media asset JSON object to an existing project.", {"project_id": "project_123", "asset_json": '{"path":"clip.mp4","kind":"video"}'}))
45
 
46
  with gr.Tab("Timeline"):
47
  timeline_project_id = gr.Textbox(label="Project ID")
 
53
  )
54
  timeline_add_button = gr.Button("Add To Timeline", variant="primary")
55
  timeline_output = gr.JSON(label="Project")
56
+ timeline_add_button.click(fn=add_timeline_item, inputs=[timeline_project_id, timeline_track_type, timeline_item], outputs=timeline_output, api_name="add_timeline_item", api_description=_mcp_description("Add a typed item to a project timeline.", {"project_id": "project_123", "track_type": "video", "item_json": '{"media":"clip.mp4","start":0,"duration":5,"caption":"Hook"}'}))
57
 
58
  timeline_operation_json = gr.Textbox(
59
  label="Operation JSON",
 
61
  value=json.dumps({"operation": "split", "item_id": "clip_123", "params": {"offset": 2.5}}, indent=2),
62
  )
63
  timeline_operation_button = gr.Button("Apply Operation")
64
+ timeline_operation_button.click(fn=apply_timeline_operation, inputs=[timeline_project_id, timeline_operation_json], outputs=timeline_output, api_name="apply_timeline_operation", api_description=_mcp_description("Apply a timeline operation such as split, trim, insert, or ripple delete.", {"project_id": "project_123", "operation_json": '{"operation":"split","item_id":"clip_123","params":{"offset":2.5}}'}))
65
 
66
  with gr.Tab("Templates"):
67
  template_button = gr.Button("Load Template Catalog")
68
  template_output = gr.JSON(label="Templates")
69
+ template_button.click(fn=load_template_catalog, outputs=template_output, api_name="load_template_catalog", api_description=_mcp_description("Load available render and caption templates.", {}))
70
 
71
  with gr.Tab("Effects"):
72
  effect_button = gr.Button("Load Effect Catalog")
73
  effect_output = gr.JSON(label="Effects")
74
+ effect_button.click(fn=load_effect_catalog, outputs=effect_output, api_name="load_effect_catalog", api_description=_mcp_description("Load available video effects.", {}))
75
 
76
  with gr.Tab("Filters"):
77
  filter_button = gr.Button("Load Filter Catalog")
78
  filter_output = gr.JSON(label="Filters")
79
+ filter_button.click(fn=load_filter_catalog, outputs=filter_output, api_name="load_filter_catalog", api_description=_mcp_description("Load available video filters.", {}))
80
 
81
  with gr.Tab("Captions"):
82
  caption_text = gr.Textbox(label="Caption source text", lines=6)
83
  caption_button = gr.Button("Submit Caption Job", variant="primary")
84
  caption_output = gr.JSON(label="Caption Job")
85
+ caption_button.click(fn=submit_caption_job, inputs=caption_text, outputs=caption_output, api_name="submit_caption_job", api_description=_mcp_description("Submit a caption-generation job from source text.", {"text": "Launch faster with automated rendering."}))
86
 
87
  with gr.Tab("Audio"):
88
  audio_button = gr.Button("Load Audio Catalog")
89
  audio_output = gr.JSON(label="Audio")
90
+ audio_button.click(fn=load_audio_catalog, outputs=audio_output, api_name="load_audio_catalog", api_description=_mcp_description("Load audio-processing and music-generation capabilities.", {}))
91
 
92
  with gr.Tab("AI Tools"):
93
  ai_tool = gr.Dropdown(choices=capability_catalog()["ai_editing"], value="auto_viral_score", label="AI tool")
94
  ai_payload = gr.Textbox(label="AI payload JSON", lines=8, value=json.dumps({"platform": "tiktok", "text": "A strong opening hook"}, indent=2))
95
  ai_button = gr.Button("Submit AI Tool", variant="primary")
96
  ai_output = gr.JSON(label="AI Job")
97
+ ai_button.click(fn=submit_ai_tool, inputs=[ai_tool, ai_payload], outputs=ai_output, api_name="submit_ai_tool", api_description=_mcp_description("Submit a configured AI editing tool job.", {"tool": "auto_viral_score", "payload": '{"platform":"tiktok","text":"A strong opening hook"}'}))
98
 
99
  with gr.Tab("Rendering"):
100
  render_json = gr.Textbox(
 
105
  )
106
  render_button = gr.Button("Submit Render", variant="primary")
107
  render_output = gr.JSON(label="Submission")
108
+ render_button.click(fn=submit_render_job, inputs=render_json, outputs=render_output, api_name="submit_render_job", api_description=_mcp_description("Submit a validated JSON video render job.", {"payload": '{"scenes":[{"start":0,"duration":5,"media":"clip.mp4","caption":"Opening hook"}],"platform":"tiktok","output_name":"render.mp4"}'}))
109
 
110
  with gr.Tab("AI Reels"):
111
  script = gr.Textbox(label="Script", lines=6)
 
117
  music = gr.File(label="Background Music", file_types=["audio"], type="filepath")
118
  ai_button = gr.Button("Submit AI Reel", variant="primary")
119
  ai_output = gr.JSON(label="Submission")
120
+ ai_button.click(fn=submit_ai_reel, inputs=[script, voiceover, assets, template, creative_style, platform, music], outputs=ai_output, api_name="submit_ai_reel", api_description=_mcp_description("Create an AI reel from a script, voiceover, and visual assets. Upload files first and use the returned paths.", {"script": "Three automation tips.", "voiceover": "/tmp/voice.wav", "assets": ["/tmp/a.jpg", "/tmp/b.mp4"], "template": "tiktok_classic", "creative_style": "viral_shorts", "platform": "tiktok", "music": None}))
121
 
122
  with gr.Tab("Batch Render"):
123
  batch_json = gr.Textbox(label="Batch JSON", lines=14, value=json.dumps({"jobs": []}, indent=2))
124
  batch_button = gr.Button("Submit Batch", variant="primary")
125
  batch_output = gr.JSON(label="Batch Submission")
126
+ batch_button.click(fn=submit_batch_render, inputs=batch_json, outputs=batch_output, api_name="submit_batch_render", api_description=_mcp_description("Submit multiple render jobs from a JSON batch.", {"payload": '{"jobs":[{"scenes":[{"start":0,"duration":3,"media":"clip.mp4"}],"output_name":"clip-a.mp4"}]}'}))
127
 
128
  with gr.Tab("Job Status"):
129
  status_job_id = gr.Textbox(label="Job ID")
130
  status_button = gr.Button("Refresh")
131
  status_output = gr.JSON(label="Status")
132
+ status_button.click(fn=get_job_status, inputs=status_job_id, outputs=status_output, api_name="get_job_status", api_description=_mcp_description("Retrieve the current state and metrics for a job.", {"job_id": "job_abc123"}))
133
 
134
  with gr.Tab("Logs"):
135
  logs_job_id = gr.Textbox(label="Job ID")
136
  logs_button = gr.Button("Load Logs")
137
  logs_output = gr.Textbox(label="Logs", lines=20)
138
+ logs_button.click(fn=get_job_logs, inputs=logs_job_id, outputs=logs_output, api_name="get_job_logs", api_description=_mcp_description("Retrieve execution logs for a job.", {"job_id": "job_abc123"}))
139
 
140
  with gr.Tab("Downloads"):
141
  download_job_id = gr.Textbox(label="Job ID")
142
  download_button = gr.Button("Get Output")
143
  download_output = gr.File(label="Rendered Video")
144
+ download_button.click(fn=get_download_path, inputs=download_job_id, outputs=download_output, api_name="get_download_path", api_description=_mcp_description("Return the completed artifact path for a job.", {"job_id": "job_abc123"}))
145
 
146
  with gr.Tab("Transcribe"):
147
  transcribe_audio = gr.File(label="Audio or Video", file_types=["audio", "video"], type="filepath")
 
154
  transcribe_button = gr.Button("Transcribe", variant="primary")
155
  transcribe_output = gr.JSON(label="Transcript")
156
  transcribe_button.click(
157
+ fn=transcribe_audio_file,
158
  inputs=[transcribe_audio, transcribe_model, transcribe_language],
159
  outputs=transcribe_output,
160
+ api_name="transcribe_audio_file",
161
+ api_description=_mcp_description("Transcribe an uploaded audio or video file with word timestamps. Upload the file first and use its returned path.", {"path": "/tmp/interview.mp3", "model_size": "tiny", "language": "en"}),
162
  )
163
 
164
  with gr.Tab("Asset Inspector"):
165
  asset_path = gr.Textbox(label="Asset path")
166
  inspect_button = gr.Button("Inspect")
167
  inspect_output = gr.JSON(label="Metadata")
168
+ inspect_button.click(fn=inspect_media_asset, inputs=asset_path, outputs=inspect_output, api_name="inspect_media_asset", api_description=_mcp_description("Inspect media codecs, streams, duration, and dimensions.", {"path": "/app/storage/video.mp4"}))
169
 
170
  with gr.Tab("AI Analysis"):
171
  analysis_media = gr.Textbox(label="Media URL or path")
 
174
  analysis_button = gr.Button("Submit Analysis", variant="primary")
175
  analysis_output = gr.JSON(label="Analysis Job")
176
  analysis_button.click(
177
+ fn=submit_media_analysis,
178
  inputs=[analysis_media, analysis_transcript, analysis_platform],
179
  outputs=analysis_output,
180
+ api_name="submit_media_analysis",
181
+ api_description=_mcp_description("Analyze media for highlights, pacing, platform fit, and engagement signals.", {"media": "https://example.com/video.mp4", "transcript": "A strong opening hook and useful explanation.", "platform": "tiktok"}),
182
  )
183
 
184
  with gr.Tab("Clip Generator"):
 
186
  clip_json = gr.Textbox(label="Clip JSON", lines=6, value=json.dumps([{"start": 0, "end": 8}], indent=2))
187
  clip_button = gr.Button("Generate Clips", variant="primary")
188
  clip_output = gr.JSON(label="Clip Job")
189
+ clip_button.click(fn=submit_clip_job, inputs=[clip_media, clip_json], outputs=clip_output, api_name="submit_clip_job", api_description=_mcp_description("Generate one or more clips from media and timed clip JSON.", {"media": "https://example.com/video.mp4", "clips_json": '[{"start":0,"end":8},{"start":20,"end":32}]'}))
190
 
191
  with gr.Tab("Metadata"):
192
  metadata_topic = gr.Textbox(label="Topic or transcript", lines=5)
193
  metadata_platform = gr.Dropdown(choices=list_platform_profiles(), value="tiktok", label="Platform")
194
  metadata_button = gr.Button("Generate Metadata", variant="primary")
195
  metadata_output = gr.JSON(label="Metadata Job")
196
+ metadata_button.click(fn=submit_metadata_job, inputs=[metadata_topic, metadata_platform], outputs=metadata_output, api_name="submit_metadata_job", api_description=_mcp_description("Generate platform-aware title, description, hashtags, and chapters.", {"topic": "Five n8n video automation mistakes", "platform": "youtube_shorts"}))
197
 
198
  with gr.Tab("Publishing"):
199
  publish_media = gr.Textbox(label="Media URL or rendered output path")
 
201
  publish_platforms = gr.Textbox(label="Platforms", value="youtube,tiktok,instagram")
202
  publish_button = gr.Button("Create Publish Draft", variant="primary")
203
  publish_output = gr.JSON(label="Publish Job")
204
+ publish_button.click(fn=create_publish_draft, inputs=[publish_media, publish_title, publish_platforms], outputs=publish_output, api_name="create_publish_draft", api_description=_mcp_description("Create a publishing draft for one or more platforms.", {"media": "/app/exports/render.mp4", "title": "Automation Tips", "platforms": "youtube,tiktok,instagram"}))
205
 
206
  with gr.Tab("Settings"):
207
  settings_button = gr.Button("Load Settings")
208
  settings_output = gr.JSON(label="Settings")
209
+ settings_button.click(fn=get_runtime_settings, outputs=settings_output, api_name="get_runtime_settings", api_description=_mcp_description("Return renderer configuration and runtime capability settings.", {}))
210
 
211
  with gr.Tab("Queue Monitor"):
212
  queue_button = gr.Button("Refresh Queue")
213
  queue_output = gr.JSON(label="Queue")
214
+ queue_button.click(fn=get_queue_status, outputs=queue_output, api_name="get_queue_status", api_description=_mcp_description("Return queue counts and active render jobs.", {}))
215
 
216
  return dashboard
217
 
218
 
219
+ def submit_render_job(payload: str) -> dict[str, Any]:
220
+ """Submit a validated JSON timeline for asynchronous video rendering."""
221
  data = apply_creative_style(apply_preset(json.loads(payload)))
222
  request = Timeline.request_from_payload(data)
223
  job_id = job_manager.submit_render(request)
224
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
225
 
226
 
227
+ def create_project(name: str, metadata_json: str) -> dict[str, Any]:
228
+ """Create a persistent studio project with optional JSON metadata."""
229
  metadata = json.loads(metadata_json or "{}")
230
  return {"project": project_manager.create(name, metadata=metadata)}
231
 
232
 
233
+ def list_projects() -> dict[str, Any]:
234
+ """List all persistent studio projects."""
235
  return {"projects": project_manager.list()}
236
 
237
 
238
+ def add_project_asset(project_id: str, asset_json: str) -> dict[str, Any]:
239
+ """Attach a media asset JSON object to an existing project."""
240
  return {"project": project_manager.add_asset(project_id, json.loads(asset_json or "{}"))}
241
 
242
 
243
+ def add_timeline_item(project_id: str, track_type: str, item_json: str) -> dict[str, Any]:
244
+ """Add a JSON item to a video, audio, text, overlay, sticker, or subtitle track."""
245
  return {"project": project_manager.add_to_timeline(project_id, json.loads(item_json or "{}"), track_type=track_type)}
246
 
247
 
248
+ def apply_timeline_operation(project_id: str, operation_json: str) -> dict[str, Any]:
249
+ """Apply a JSON timeline operation such as split, trim, insert, or ripple delete."""
250
  data = json.loads(operation_json or "{}")
251
  return {
252
  "project": project_manager.timeline_operation(
 
263
  return {section: catalog.get(section)}
264
 
265
 
266
+ def load_template_catalog() -> dict[str, Any]:
267
+ """Return available render and caption template capabilities."""
268
+ return _catalog_section("templates")
269
+
270
+
271
+ def load_effect_catalog() -> dict[str, Any]:
272
+ """Return available video effect capabilities."""
273
+ return _catalog_section("effects")
274
+
275
+
276
+ def load_filter_catalog() -> dict[str, Any]:
277
+ """Return available video filter capabilities."""
278
+ return _catalog_section("filters")
279
+
280
+
281
+ def load_audio_catalog() -> dict[str, Any]:
282
+ """Return audio-processing and music-generation capabilities."""
283
+ catalog = capability_catalog()
284
+ return {"audio": catalog["audio"], "music": catalog["music_generator"]}
285
+
286
+
287
+ def submit_caption_job(text: str) -> dict[str, Any]:
288
+ """Submit an asynchronous caption-generation job from source text."""
289
  from renderer.studio import StudioTaskProcessor
290
 
291
  job_id = job_manager.submit_task(lambda task_id, log: StudioTaskProcessor(settings, log=log).caption_generate({"text": text}, task_id))
292
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
293
 
294
 
295
+ def submit_ai_tool(tool: str, payload: str) -> dict[str, Any]:
296
+ """Submit an AI editing tool using its name and a JSON payload."""
297
  from renderer.studio import StudioTaskProcessor
298
 
299
  data = json.loads(payload or "{}")
 
301
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
302
 
303
 
304
+ def submit_batch_render(payload: str) -> dict[str, Any]:
305
+ """Submit multiple asynchronous render jobs from a JSON jobs array."""
306
  data = json.loads(payload)
307
  requests = [Timeline.request_from_payload(apply_creative_style(apply_preset(job))) for job in data.get("jobs", [])]
308
  return {"job_ids": job_manager.submit_batch(requests)}
309
 
310
 
311
+ def submit_ai_reel(
312
  script: str,
313
  voiceover: str,
314
  assets: list[str],
 
317
  platform: str,
318
  music: str | None,
319
  ) -> dict[str, Any]:
320
+ """Create an AI reel from a script, voiceover, visual assets, style, and music."""
321
  request = AIReelsRequest(
322
  script=script,
323
  voiceover=voiceover,
 
331
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
332
 
333
 
334
+ def get_job_status(job_id: str) -> dict[str, Any]:
335
+ """Return state, metrics, artifact metadata, and errors for a job ID."""
336
  return job_manager.get(job_id).__dict__
337
 
338
 
339
+ def get_job_logs(job_id: str) -> str:
340
+ """Return execution log messages for a job ID."""
341
  return "\n\n".join(job_manager.get(job_id).logs)
342
 
343
 
344
+ def get_download_path(job_id: str) -> str | None:
345
+ """Return the local artifact path when a job has completed."""
346
  record = job_manager.get(job_id)
347
  if record.state != "COMPLETED":
348
  return None
349
  return record.output_path
350
 
351
 
352
+ def inspect_media_asset(path: str) -> dict[str, Any]:
353
+ """Inspect a local media asset for streams, codecs, duration, and dimensions."""
354
  return RenderEngine(settings).inspect_asset(path)
355
 
356
 
357
+ def transcribe_audio_file(path: str, model_size: str, language: str) -> dict[str, Any]:
358
+ """Transcribe an uploaded audio or video file with optional language selection."""
359
  return RenderEngine(settings).transcribe(
360
  path,
361
  model_size=model_size,
 
364
  )
365
 
366
 
367
+ def submit_media_analysis(media: str, transcript: str, platform: str) -> dict[str, Any]:
368
+ """Analyze media for highlights, pacing, retention, engagement, and platform fit."""
369
  from renderer.platform import PlatformProcessor
370
 
371
  job_id = job_manager.submit_task(
 
374
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
375
 
376
 
377
+ def submit_clip_job(media: str, clips_json: str) -> dict[str, Any]:
378
+ """Generate timed clips from a media path or URL and a JSON clip specification."""
379
  from renderer.platform import PlatformProcessor
380
 
381
  clips = json.loads(clips_json)
 
383
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
384
 
385
 
386
+ def submit_metadata_job(topic: str, platform: str) -> dict[str, Any]:
387
+ """Generate platform-aware title, description, hashtags, keywords, and chapters."""
388
  from renderer.platform import PlatformProcessor
389
 
390
  job_id = job_manager.submit_task(lambda task_id, log: PlatformProcessor(settings, log=log).metadata(task_id, topic=topic, platform=platform))
391
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
392
 
393
 
394
+ def create_publish_draft(media: str, title: str, platforms: str) -> dict[str, Any]:
395
+ """Create an asynchronous publishing draft for comma-separated platforms."""
396
  from renderer.platform import PlatformProcessor
397
 
398
  payload = {"media": media, "title": title, "platforms": [item.strip() for item in platforms.split(",") if item.strip()], "draft": True}
 
400
  return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}
401
 
402
 
403
+ def get_queue_status() -> dict[str, Any]:
404
+ """Return queue totals, state counts, worker limits, and active jobs."""
405
  return job_manager.summary()
406
 
407
 
408
+ def get_runtime_settings() -> dict[str, Any]:
409
+ """Return renderer paths, worker settings, Whisper settings, and capabilities."""
410
  return {
411
  "product": "Ava2lon Studio AI",
412
  "base_dir": str(settings.base_dir),
tests/test_mcp_metadata.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+
9
+ class MCPMetadataTests(unittest.TestCase):
10
+ def test_every_dashboard_tool_has_public_name_and_description(self) -> None:
11
+ tree = ast.parse(Path("renderer_app.py").read_text(encoding="utf-8-sig"))
12
+ functions = {
13
+ node.name: node
14
+ for node in tree.body
15
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
16
+ }
17
+ events = [
18
+ node
19
+ for node in ast.walk(tree)
20
+ if isinstance(node, ast.Call)
21
+ and isinstance(node.func, ast.Attribute)
22
+ and node.func.attr == "click"
23
+ ]
24
+
25
+ self.assertEqual(len(events), 25)
26
+ for event in events:
27
+ keywords = {item.arg: item.value for item in event.keywords if item.arg}
28
+ callback = keywords.get("fn")
29
+ self.assertIsInstance(callback, ast.Name, "MCP callbacks must be named functions")
30
+ callback_name = callback.id
31
+ self.assertFalse(callback_name.startswith("_"), f"Private MCP callback: {callback_name}")
32
+ self.assertIn(callback_name, functions)
33
+ self.assertTrue(ast.get_docstring(functions[callback_name]), f"Missing docstring: {callback_name}")
34
+
35
+ api_name = keywords.get("api_name")
36
+ description = keywords.get("api_description")
37
+ self.assertIsInstance(api_name, ast.Constant, f"Missing api_name: {callback_name}")
38
+ self.assertTrue(api_name.value, f"Empty api_name: {callback_name}")
39
+ self.assertIsInstance(description, ast.Call, f"Missing structured api_description: {callback_name}")
40
+ self.assertIsInstance(description.func, ast.Name)
41
+ self.assertEqual(description.func.id, "_mcp_description")
42
+ self.assertEqual(len(description.args), 2)
43
+
44
+ summary = ast.literal_eval(description.args[0])
45
+ example = ast.literal_eval(description.args[1])
46
+ self.assertTrue(summary, f"Empty description: {callback_name}")
47
+ self.assertIsInstance(example, dict, f"Example must be an object: {callback_name}")
48
+ encoded = json.dumps(example, separators=(",", ":"), ensure_ascii=True)
49
+ self.assertEqual(json.loads(encoded), example)
50
+
51
+ parameters = [argument.arg for argument in functions[callback_name].args.args]
52
+ self.assertEqual(set(example), set(parameters), f"Example keys do not match: {callback_name}")
53
+
54
+
55
+ if __name__ == "__main__":
56
+ unittest.main()