File size: 25,105 Bytes
e990dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c2642f
 
a137ca3
 
6c2642f
 
e990dfa
 
 
 
 
 
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
 
 
 
 
6c2642f
e990dfa
 
6c2642f
 
e990dfa
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
6c2642f
e990dfa
 
6c2642f
 
e990dfa
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
e990dfa
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6c2642f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e990dfa
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
6c2642f
e990dfa
 
 
 
 
 
 
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
 
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
6c2642f
 
e990dfa
 
 
6c2642f
 
e990dfa
 
 
 
 
 
 
 
 
 
 
 
 
a4e6ce3
 
 
 
 
99d5f49
a4e6ce3
e990dfa
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
from __future__ import annotations

import json
from pathlib import Path
from typing import Any

import gradio as gr
import uvicorn

from api import api, job_manager, project_manager, settings
from renderer import RenderEngine
from renderer.core.models import AIReelsRequest
from renderer.scenes import Timeline
from renderer.studio import capability_catalog
from renderer.templates import apply_creative_style, apply_preset, list_creative_styles, list_platform_profiles, list_templates


def _mcp_description(summary: str, example: dict[str, Any]) -> str:
    """Build a concise MCP description with a machine-readable JSON example."""
    payload = json.dumps(example, indent=2, ensure_ascii=True)
    return f"{summary}\n\nExample JSON:\n```json\n{payload}\n```"


def create_dashboard() -> gr.Blocks:
    with gr.Blocks(title="Ava2lon Studio AI") as dashboard:
        gr.Markdown(
            "# Ava2lon Studio AI\n"
            "CPU-first CapCut-class video automation studio with REST API parity, async jobs, webhooks, and project JSON."
        )
        with gr.Tab("Projects"):
            project_name = gr.Textbox(label="Project name", value="Untitled Ava2lon Project")
            project_metadata = gr.Textbox(label="Metadata JSON", lines=5, value=json.dumps({"platform": "tiktok"}, indent=2))
            project_create = gr.Button("Create Project", variant="primary")
            project_list = gr.Button("Refresh Projects")
            project_output = gr.JSON(label="Projects")
            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"}'}))
            project_list.click(fn=list_projects, outputs=project_output, api_name="list_projects", api_description=_mcp_description("List saved studio projects.", {}))

        with gr.Tab("Assets"):
            gr.Markdown("Use `/upload` or `/assets/upload` for multipart assets, then attach them to a project with `/project/assets/add`.")
            asset_project_id = gr.Textbox(label="Project ID")
            asset_json = gr.Textbox(label="Asset JSON", lines=6, value=json.dumps({"path": "clip.mp4", "kind": "video"}, indent=2))
            asset_button = gr.Button("Attach Asset", variant="primary")
            asset_output = gr.JSON(label="Project")
            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"}'}))

        with gr.Tab("Timeline"):
            timeline_project_id = gr.Textbox(label="Project ID")
            timeline_track_type = gr.Dropdown(choices=["video", "audio", "text", "overlay", "sticker", "subtitle"], value="video", label="Track type")
            timeline_item = gr.Textbox(
                label="Timeline item JSON",
                lines=8,
                value=json.dumps({"media": "clip.mp4", "start": 0, "duration": 5, "caption": "Hook"}, indent=2),
            )
            timeline_add_button = gr.Button("Add To Timeline", variant="primary")
            timeline_output = gr.JSON(label="Project")
            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"}'}))

            timeline_operation_json = gr.Textbox(
                label="Operation JSON",
                lines=8,
                value=json.dumps({"operation": "split", "item_id": "clip_123", "params": {"offset": 2.5}}, indent=2),
            )
            timeline_operation_button = gr.Button("Apply Operation")
            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}}'}))

        with gr.Tab("Templates"):
            template_button = gr.Button("Load Template Catalog")
            template_output = gr.JSON(label="Templates")
            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.", {}))

        with gr.Tab("Effects"):
            effect_button = gr.Button("Load Effect Catalog")
            effect_output = gr.JSON(label="Effects")
            effect_button.click(fn=load_effect_catalog, outputs=effect_output, api_name="load_effect_catalog", api_description=_mcp_description("Load available video effects.", {}))

        with gr.Tab("Filters"):
            filter_button = gr.Button("Load Filter Catalog")
            filter_output = gr.JSON(label="Filters")
            filter_button.click(fn=load_filter_catalog, outputs=filter_output, api_name="load_filter_catalog", api_description=_mcp_description("Load available video filters.", {}))

        with gr.Tab("Captions"):
            caption_text = gr.Textbox(label="Caption source text", lines=6)
            caption_button = gr.Button("Submit Caption Job", variant="primary")
            caption_output = gr.JSON(label="Caption Job")
            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."}))

        with gr.Tab("Audio"):
            audio_button = gr.Button("Load Audio Catalog")
            audio_output = gr.JSON(label="Audio")
            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.", {}))

        with gr.Tab("AI Tools"):
            ai_tool = gr.Dropdown(choices=capability_catalog()["ai_editing"], value="auto_viral_score", label="AI tool")
            ai_payload = gr.Textbox(label="AI payload JSON", lines=8, value=json.dumps({"platform": "tiktok", "text": "A strong opening hook"}, indent=2))
            ai_button = gr.Button("Submit AI Tool", variant="primary")
            ai_output = gr.JSON(label="AI Job")
            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"}'}))

        with gr.Tab("Rendering"):
            render_json = gr.Textbox(
                label="Render JSON",
                lines=14,
                value="",
                placeholder="Paste a production render request JSON object with absolute or uploaded asset paths.",
            )
            render_button = gr.Button("Submit Render", variant="primary")
            render_output = gr.JSON(label="Submission")
            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"}'}))

        with gr.Tab("AI Reels"):
            script = gr.Textbox(label="Script", lines=6)
            voiceover = gr.File(label="Voiceover", file_types=["audio"], type="filepath")
            assets = gr.File(label="Assets", file_count="multiple", type="filepath")
            template = gr.Dropdown(choices=list_templates(), value="tiktok_classic", label="Caption Template")
            creative_style = gr.Dropdown(choices=list_creative_styles(), value="viral_shorts", label="Creative Style")
            platform = gr.Dropdown(choices=list_platform_profiles(), value="tiktok", label="Platform")
            music = gr.File(label="Background Music", file_types=["audio"], type="filepath")
            ai_button = gr.Button("Submit AI Reel", variant="primary")
            ai_output = gr.JSON(label="Submission")
            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}))

        with gr.Tab("Batch Render"):
            batch_json = gr.Textbox(label="Batch JSON", lines=14, value=json.dumps({"jobs": []}, indent=2))
            batch_button = gr.Button("Submit Batch", variant="primary")
            batch_output = gr.JSON(label="Batch Submission")
            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"}]}'}))

        with gr.Tab("Job Status"):
            status_job_id = gr.Textbox(label="Job ID")
            status_button = gr.Button("Refresh")
            status_output = gr.JSON(label="Status")
            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"}))

        with gr.Tab("Logs"):
            logs_job_id = gr.Textbox(label="Job ID")
            logs_button = gr.Button("Load Logs")
            logs_output = gr.Textbox(label="Logs", lines=20)
            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"}))

        with gr.Tab("Downloads"):
            download_job_id = gr.Textbox(label="Job ID")
            download_button = gr.Button("Get Output")
            download_output = gr.File(label="Rendered Video")
            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"}))

        with gr.Tab("Transcribe"):
            transcribe_audio = gr.File(label="Audio or Video", file_types=["audio", "video"], type="filepath")
            transcribe_model = gr.Dropdown(
                choices=["tiny", "base", "small", "medium", "large-v3"],
                value=settings.whisper_model_size,
                label="Whisper Model",
            )
            transcribe_language = gr.Textbox(label="Language", placeholder="Optional ISO code, e.g. en")
            transcribe_button = gr.Button("Transcribe", variant="primary")
            transcribe_output = gr.JSON(label="Transcript")
            transcribe_button.click(
                fn=transcribe_audio_file,
                inputs=[transcribe_audio, transcribe_model, transcribe_language],
                outputs=transcribe_output,
                api_name="transcribe_audio_file",
                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"}),
            )

        with gr.Tab("Asset Inspector"):
            asset_path = gr.Textbox(label="Asset path")
            inspect_button = gr.Button("Inspect")
            inspect_output = gr.JSON(label="Metadata")
            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"}))

        with gr.Tab("AI Analysis"):
            analysis_media = gr.Textbox(label="Media URL or path")
            analysis_transcript = gr.Textbox(label="Transcript", lines=5)
            analysis_platform = gr.Dropdown(choices=list_platform_profiles(), value="tiktok", label="Target Platform")
            analysis_button = gr.Button("Submit Analysis", variant="primary")
            analysis_output = gr.JSON(label="Analysis Job")
            analysis_button.click(
                fn=submit_media_analysis,
                inputs=[analysis_media, analysis_transcript, analysis_platform],
                outputs=analysis_output,
                api_name="submit_media_analysis",
                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"}),
            )

        with gr.Tab("Clip Generator"):
            clip_media = gr.Textbox(label="Media URL or path")
            clip_json = gr.Textbox(label="Clip JSON", lines=6, value=json.dumps([{"start": 0, "end": 8}], indent=2))
            clip_button = gr.Button("Generate Clips", variant="primary")
            clip_output = gr.JSON(label="Clip Job")
            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}]'}))

        with gr.Tab("Metadata"):
            metadata_topic = gr.Textbox(label="Topic or transcript", lines=5)
            metadata_platform = gr.Dropdown(choices=list_platform_profiles(), value="tiktok", label="Platform")
            metadata_button = gr.Button("Generate Metadata", variant="primary")
            metadata_output = gr.JSON(label="Metadata Job")
            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"}))

        with gr.Tab("Publishing"):
            publish_media = gr.Textbox(label="Media URL or rendered output path")
            publish_title = gr.Textbox(label="Title")
            publish_platforms = gr.Textbox(label="Platforms", value="youtube,tiktok,instagram")
            publish_button = gr.Button("Create Publish Draft", variant="primary")
            publish_output = gr.JSON(label="Publish Job")
            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"}))

        with gr.Tab("Settings"):
            settings_button = gr.Button("Load Settings")
            settings_output = gr.JSON(label="Settings")
            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.", {}))

        with gr.Tab("Queue Monitor"):
            queue_button = gr.Button("Refresh Queue")
            queue_output = gr.JSON(label="Queue")
            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.", {}))

    return dashboard


def submit_render_job(payload: str) -> dict[str, Any]:
    """Submit a validated JSON timeline for asynchronous video rendering."""
    data = apply_creative_style(apply_preset(json.loads(payload)))
    request = Timeline.request_from_payload(data)
    job_id = job_manager.submit_render(request)
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def create_project(name: str, metadata_json: str) -> dict[str, Any]:
    """Create a persistent studio project with optional JSON metadata."""
    metadata = json.loads(metadata_json or "{}")
    return {"project": project_manager.create(name, metadata=metadata)}


def list_projects() -> dict[str, Any]:
    """List all persistent studio projects."""
    return {"projects": project_manager.list()}


def add_project_asset(project_id: str, asset_json: str) -> dict[str, Any]:
    """Attach a media asset JSON object to an existing project."""
    return {"project": project_manager.add_asset(project_id, json.loads(asset_json or "{}"))}


def add_timeline_item(project_id: str, track_type: str, item_json: str) -> dict[str, Any]:
    """Add a JSON item to a video, audio, text, overlay, sticker, or subtitle track."""
    return {"project": project_manager.add_to_timeline(project_id, json.loads(item_json or "{}"), track_type=track_type)}


def apply_timeline_operation(project_id: str, operation_json: str) -> dict[str, Any]:
    """Apply a JSON timeline operation such as split, trim, insert, or ripple delete."""
    data = json.loads(operation_json or "{}")
    return {
        "project": project_manager.timeline_operation(
            project_id,
            data.get("operation", "drag"),
            item_id=data.get("item_id"),
            params=data.get("params", {}),
        )
    }


def _catalog_section(section: str) -> dict[str, Any]:
    catalog = capability_catalog()
    return {section: catalog.get(section)}


def load_template_catalog() -> dict[str, Any]:
    """Return available render and caption template capabilities."""
    return _catalog_section("templates")


def load_effect_catalog() -> dict[str, Any]:
    """Return available video effect capabilities."""
    return _catalog_section("effects")


def load_filter_catalog() -> dict[str, Any]:
    """Return available video filter capabilities."""
    return _catalog_section("filters")


def load_audio_catalog() -> dict[str, Any]:
    """Return audio-processing and music-generation capabilities."""
    catalog = capability_catalog()
    return {"audio": catalog["audio"], "music": catalog["music_generator"]}


def submit_caption_job(text: str) -> dict[str, Any]:
    """Submit an asynchronous caption-generation job from source text."""
    from renderer.studio import StudioTaskProcessor

    job_id = job_manager.submit_task(lambda task_id, log: StudioTaskProcessor(settings, log=log).caption_generate({"text": text}, task_id))
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def submit_ai_tool(tool: str, payload: str) -> dict[str, Any]:
    """Submit an AI editing tool using its name and a JSON payload."""
    from renderer.studio import StudioTaskProcessor

    data = json.loads(payload or "{}")
    job_id = job_manager.submit_task(lambda task_id, log: StudioTaskProcessor(settings, log=log).ai_tool(tool, data, task_id))
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def submit_batch_render(payload: str) -> dict[str, Any]:
    """Submit multiple asynchronous render jobs from a JSON jobs array."""
    data = json.loads(payload)
    requests = [Timeline.request_from_payload(apply_creative_style(apply_preset(job))) for job in data.get("jobs", [])]
    return {"job_ids": job_manager.submit_batch(requests)}


def submit_ai_reel(
    script: str,
    voiceover: str,
    assets: list[str],
    template: str,
    creative_style: str,
    platform: str,
    music: str | None,
) -> dict[str, Any]:
    """Create an AI reel from a script, voiceover, visual assets, style, and music."""
    request = AIReelsRequest(
        script=script,
        voiceover=voiceover,
        assets=assets or [],
        template=template,
        creative_style=creative_style,
        platform=platform,
        background_music=music,
    )
    job_id = job_manager.submit_ai_reels(request)
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def get_job_status(job_id: str) -> dict[str, Any]:
    """Return state, metrics, artifact metadata, and errors for a job ID."""
    return job_manager.get(job_id).__dict__


def get_job_logs(job_id: str) -> str:
    """Return execution log messages for a job ID."""
    return "\n\n".join(job_manager.get(job_id).logs)


def get_download_path(job_id: str) -> str | None:
    """Return the local artifact path when a job has completed."""
    record = job_manager.get(job_id)
    if record.state != "COMPLETED":
        return None
    return record.output_path


def inspect_media_asset(path: str) -> dict[str, Any]:
    """Inspect a local media asset for streams, codecs, duration, and dimensions."""
    return RenderEngine(settings).inspect_asset(path)


def transcribe_audio_file(path: str, model_size: str, language: str) -> dict[str, Any]:
    """Transcribe an uploaded audio or video file with optional language selection."""
    return RenderEngine(settings).transcribe(
        path,
        model_size=model_size,
        language=language.strip() or None,
        word_timestamps=True,
    )


def submit_media_analysis(media: str, transcript: str, platform: str) -> dict[str, Any]:
    """Analyze media for highlights, pacing, retention, engagement, and platform fit."""
    from renderer.platform import PlatformProcessor

    job_id = job_manager.submit_task(
        lambda task_id, log: PlatformProcessor(settings, log=log).analyze(media, task_id, transcript=transcript, platform=platform)
    )
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def submit_clip_job(media: str, clips_json: str) -> dict[str, Any]:
    """Generate timed clips from a media path or URL and a JSON clip specification."""
    from renderer.platform import PlatformProcessor

    clips = json.loads(clips_json)
    job_id = job_manager.submit_task(lambda task_id, log: PlatformProcessor(settings, log=log).clips(media, task_id, clips))
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def submit_metadata_job(topic: str, platform: str) -> dict[str, Any]:
    """Generate platform-aware title, description, hashtags, keywords, and chapters."""
    from renderer.platform import PlatformProcessor

    job_id = job_manager.submit_task(lambda task_id, log: PlatformProcessor(settings, log=log).metadata(task_id, topic=topic, platform=platform))
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def create_publish_draft(media: str, title: str, platforms: str) -> dict[str, Any]:
    """Create an asynchronous publishing draft for comma-separated platforms."""
    from renderer.platform import PlatformProcessor

    payload = {"media": media, "title": title, "platforms": [item.strip() for item in platforms.split(",") if item.strip()], "draft": True}
    job_id = job_manager.submit_task(lambda task_id, log: PlatformProcessor(settings, log=log).publish(payload, task_id))
    return {"job_id": job_id, "status": f"/status/{job_id}", "download": f"/download/{job_id}"}


def get_queue_status() -> dict[str, Any]:
    """Return queue totals, state counts, worker limits, and active jobs."""
    return job_manager.summary()


def get_runtime_settings() -> dict[str, Any]:
    """Return renderer paths, worker settings, Whisper settings, and capabilities."""
    return {
        "product": "Ava2lon Studio AI",
        "base_dir": str(settings.base_dir),
        "temp_dir": str(settings.temp_dir),
        "exports_dir": str(settings.exports_dir),
        "storage_dir": str(settings.storage_dir),
        "max_workers": settings.max_workers,
        "whisper_model_size": settings.whisper_model_size,
        "whisper_device": settings.whisper_device,
        "capabilities": capability_catalog()["principles"],
    }


app = gr.mount_gradio_app(
    api,
    create_dashboard(),
    path="/dashboard",
    root_path="/dashboard",
    mcp_server=True,
)


if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=7860)