studio — full tool reference
Studio projects, compositions, slides, versions, comments, templates and themes.
| Endpoint | https://studio.mcp.devfellowship.com/mcp |
| Package | packages/dfl-mcp-studio |
| Tools | 72 |
| Tool | Description |
|---|---|
create_studio_project | Create a new Lesson Studio project in the lesson_studio schema, plus a “Default” composition. Returns the project_id and the default composition_id (attach slides to a composition via create_slide). |
list_studio_projects | List Lesson Studio projects owned by the caller (RLS-scoped), most recently updated first. |
delete_studio_project | Delete a Lesson Studio project. Its compositions, slides, slide_elements and slide_media cascade. RLS-scoped: only the caller’s own projects can be deleted. |
update_studio_project | Update a project’s title, description, thumbnail, orientation, active theme (settings.theme_id), burned-in captions (settings.captions_*) or export watermark (settings.watermark). For a 9:16 reel, pass captions {enabled: true, position: “top”} and watermark {enabled: true} before start_composition_export. Camera defaults are set_project_camera’s job, not this tool’s. theme_id is checked against list_themes when the registry is reachable; an unknown id is rejected. A settings write (theme_id, captions, watermark) is guarded on updated_at (it reads settings first to merge; keys you leave out keep their stored value); a change to the other fields overwrites only the columns passed. Changing orientation does NOT reshape camera boxes — it warns and points at set_project_camera. |
create_composition | Create a composition (lesson-level grouping / “frame”) under a Lesson Studio project. Slides attach to a composition. |
list_compositions | List the compositions of a Lesson Studio project, ordered by order_index. |
update_composition | Update a composition title and/or order_index. |
delete_composition | Delete a composition. Its slides must be removed or reassigned first (slides.composition_id is NOT NULL). |
create_slide | Create a slide under a composition (mirrors the Lesson Studio editor insert shape so it is valid + editable in the UI). Optional template_data.animation animates the slide. Preset ‘steps-reveal’ shows list items one at a time; use it when the narration walks through steps, on a template whose items carry data-anim-target=“step”. Example: {preset:‘steps-reveal’,version:1,beat_source:‘manual’,beats:[{kind:‘reveal’,target:‘step’,index:0,start_ms:600,duration_ms:300}]}. Sort beats by start_ms. Every beat must end before the slide duration_ms. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects. |
create_image_slide | Create a slide that displays a single image filling the entire slide canvas, using the fullscreen-image template. Use this to bring your own finished slide images (e.g. an image hosted on S3, or a pre-rendered slide exported from another tool) instead of reusing the built-in content templates. The fit option controls how the image fills the canvas: cover (default) crops to fill, contain letterboxes to show the whole image. |
list_slides | List the slides of a composition, ordered by order_index. Soft-deleted slides are excluded by default; pass include_deleted:true to return all (including soft-deleted). |
update_slide | Update a slide. template_data replaces the slots; an omitted slot is removed. Studio keys (not slots): narration_notes, animation, background_override, camera, camera_box, capture, camera_box_custom, watermark, webcam_layout, pointer_track, is_cover, sfx, framing, caption_cues, clips, style_overrides, slide_name, caption_cues_source, caption_language, camera_segments, content_over_camera, caption_position, camera_focus, insert. An omitted Studio key is kept, except animation: a template_id change removes it unless you send it. Remove one via clear_template_data_keys, only when the user asks. narration_notes and pointer_track are user work and cannot be restored. The response lists removed_template_data_keys. A new background sets background_override = true. To show the template background again, send clear_template_data_keys: [‘background_override’] and no background field. A sent animation is checked as the create_slide description says, against the slide duration_ms. A new duration_ms is checked against the stored animation. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects. |
delete_slide | Delete a slide. By default this is a SOFT delete (recoverable via restore_slide; slide_elements/slide_media are kept). Pass hard:true for a permanent delete (slide_elements + slide_media cascade). |
restore_slide | Restore a soft-deleted slide (clears deleted_at/deleted_by so it reappears in list_slides). Only works on slides that were soft-deleted via delete_slide; a hard-deleted slide is gone permanently. |
list_animation_presets | The slide animation presets set_slide_animation can apply, with the template_data slots each one reads. Pass template_id to also learn which of them that template can actually run: the answer is read from the live template markup (data-anim-target), so it cannot go stale. A preset whose slots the slide does not carry produces no beats — that is what requires describes. |
set_slide_animation | Animate a slide with one of the presets from list_animation_presets. With no beats, the timings are computed from the slide’s own template_data and duration_ms — the same defaults the Lesson Studio editor applies, already fitted to the slide length and valid at 24, 30 and 60 fps. Pass beats only for a timing the defaults do not express. “zoom-focus” is the one preset with no template slot — it works on any slide, pushing in and holding until a later zoom beat frames back out. A “zoom” beat also COMPOSES with every other preset (add one to the “beats” array of a steps-reveal/chat-typing/highlight/image-enter spec): it drives the synthetic “slide” target no template markup ever declares, so it never collides with that preset’s own beats. Writes template_data.animation and leaves every other template_data key untouched. To remove an animation, call update_slide with clear_template_data_keys: [‘animation’]. |
search_slide_images | Search the stock photo library for an image to put on a slide, and get back candidate URLs. Use it to fill an image-type template slot without a human opening the editor: pick a result and write its url with update_slide (template_data.image) or create_image_slide (image_url). This tool only reads — it never touches a slide. Match orientation to the project canvas so the crop is not fighting the layout. Results carry photographer and pexels_url: the licence asks for that credit, so keep it with the image. |
set_cover_slide | Mark a slide as the deck’s cover (template_data.is_cover), or unmark it (is_cover: false). The cover is used for the video thumbnail via capture_cover_slide, but dfl-render EXCLUDES it from the rendered video, its slide count, and the 01/09 deck index. Setting is_cover: true clears the flag from every other slide in the same composition first — a deck has at most one cover. Preserves every other template_data key. To set the composition’s thumbnail after capturing it, chain the result into whichever tool writes projects.thumbnail_url (this tool never writes it). |
list_slide_beats | List the beats of a slide’s template_data.animation, each tagged with the beat_index add_slide_beat/update_slide_beat/remove_slide_beat address. Returns an empty beats array (not an error) when the slide carries no animation. |
add_slide_beat | Insert one beat into a slide’s template_data.animation.beats. The slide must already carry an animation (create one with set_slide_animation first). Beats must stay sorted by start_ms; a beat or at_index that breaks the order is rejected with the same validator set_slide_animation uses. Marks beat_source “manual”. Returns the new beat_index. |
update_slide_beat | Replace one beat of a slide’s template_data.animation.beats, addressed by beat_index (from list_slide_beats). beat replaces the ENTIRE beat, not just the fields you pass — send every field the beat needs, same shape set_slide_animation’s “beats” entries take. Beats must stay sorted by start_ms after the replacement. Marks beat_source “manual”. |
remove_slide_beat | Remove one beat from a slide’s template_data.animation.beats, addressed by beat_index (from list_slide_beats). A spec needs at least one beat, so removing the only beat is rejected — use update_slide with clear_template_data_keys: [‘animation’] to drop the animation entirely. Marks beat_source “manual”. |
create_slide_element | Create a free-canvas box on a slide: a text box, an image, or a shape drawn on top of the slide’s template. position_x/position_y/width/height/rotation are DESIGN pixels — the canvas is 1280x720 for a landscape project and 720x1280 for a portrait one (the parent project’s own orientation, not an argument here). A box entirely outside that canvas is rejected; a box that only partly overlaps it is allowed (that is a normal partial-off-canvas placement, not an error). content is validated by type: text.text is at most 2000 characters; image.url must be an http(s) URL; every color (text.color, shape.fill, shape.stroke, …) must be a hex color (#rgb, #rrggbb, #rrggbbaa) or an rgba()/rgb() function; an unknown content key is rejected. A ‘text’ box may omit content fields — missing ones are filled from the Studio’s own default text style, so { type: ‘text’, content: { text: ‘Hello’ } } is enough for a fully-styled box. ‘image’ and ‘shape’ boxes must supply every field their type needs. The response carries animation_index: the box’s position among the slide’s elements, ordered by creation time — THIS is the index a set_slide_animation “element-reveal” beat ({ kind: “reveal”, target: “element”, index }) must use to animate THIS box, because that index is the box’s position in the stored array, never its z_index or the order it was drawn on screen. Example: create_slide_element({ slide_id, type: “text”, content: { text: “Welcome” }, position_x: 100, position_y: 80, width: 600, height: 120 }) → { element: {…}, animation_index: 0 }. |
list_slide_elements | List a slide’s free-canvas boxes (text/image/shape), ordered by creation time. Each element carries animation_index: its position in this order, and the exact index a reveal beat with target: "element" must reference to animate it — never the box’s z_index or draw order. Example response: { elements: [{ id, type: “text”, content: {…}, position_x, position_y, width, height, rotation, opacity, z_index, animation_index: 0 }, …] }. |
update_slide_element | Update a free-canvas box’s content and/or geometry (position_x/position_y/width/height/rotation/opacity/z_index). Every field is optional — send only what changes. content is a PARTIAL patch: it is merged over the box’s stored content and the merged object is re-validated against its type’s rules (same rules as create_slide_element — text max 2000 chars, image url must be http(s), colors must be hex or rgba()/rgb(), unknown keys rejected). Geometry is design pixels, same canvas rule as create_slide_element: a box moved/resized fully outside the canvas is rejected. The box’s type and its animation_index (set by create order) cannot be changed here. Example: update_slide_element({ id, content: { text: “Updated headline” }, position_x: 140 }). |
delete_slide_element | Delete a free-canvas box and repair the slide’s animation in the same call. A reveal beat (target: "element") addresses a box by its position among the slide’s elements (animation_index), so removing a box shifts every later box’s beat index down by one — this tool does that shift automatically: the removed box’s own beat is dropped, later beats are reindexed keeping their effect/focus, and if no element beat survives template_data.animation is removed entirely (an animation with zero beats is not valid). The response reports removed_beat_count, shifted_beat_count and animation_cleared so the caller knows exactly what changed. |
get_camera | Read the camera box + visibility for a project (project_id) and optionally one of its slides (slide_id). Reports the RAW override at each level (project default, slide override) plus the RESOLVED value that actually applies, following the same precedence dfl-render uses (slide, then project, then the built-in default). Pass slide_id alone to also resolve the project it belongs to. |
set_project_camera | Set the project-wide camera default: aspect (16:9 or 1:1), box (preset size+corner or custom fractions), and default visibility (off/overlay). Applies to every slide that does not override its own camera via set_slide_camera. Changing aspect with no box in the same call reshapes the current project box (and every slide’s own camera_box override) to fit the new aspect, mirroring the Lesson Studio editor. Guarded on updated_at. focus {x, y} (0..1 of the camera frame) sets the project default crop point (settings.camera_focus); a slide focus (set_slide_camera) wins. |
set_slide_camera | Override this slide’s camera box and/or visibility, on top of the project default set_project_camera sets. box resolves against the parent project’s orientation and camera aspect. clear_box removes the box override (the slide follows the project box again); clear_visibility removes the visibility override. segments limits WHEN the corner (picture-in-picture) camera is drawn — refused on a fullscreen-webcam slide: a list of { start_ms, end_ms } windows in slide time (whole ms, sorted, non-overlapping, within the slide duration, 1-50 of them); the voice keeps playing outside them. clear_segments shows the camera for the whole slide again. To hide it on the whole slide use visibility: ‘off’. focus {x, y} (0..1 of the camera frame) moves the crop of the camera off the centre, e.g. {x: 0.3, y: 0.5} for a face left of centre in a 16:9 take on a portrait slide; clear_focus goes back to the project focus / the centre. Preserves every other template_data key. Guarded on updated_at. |
list_sound_effects | The sound-effect catalog available to template_data.sfx / set_slide_sfx: every valid sound key, its category, use-case hint and duration. Read-only, no database access. Pass category to filter. |
set_slide_sfx | Write this slide’s sound effects (template_data.sfx). Each entry is {sound, at_ms, volume?} — sound is a catalog key from list_sound_effects, at_ms must start before the slide duration_ms. mode “replace” (default) discards the stored list; “append” adds to it. An empty array with “replace” removes the key. Preserves every other template_data key. Guarded on updated_at. |
set_slide_captions | Replace a slide’s caption cues (template_data.caption_cues). Cues use the recording’s SOURCE clock in ms (the file, not the slide), sorted and non-overlapping, max 2000. source_stamp ‘current’ (default) stamps caption_cues_source with the slide’s recording URL, so the Studio editor does not re-transcribe and overwrite them; ‘none’ removes the stamp (the editor re-transcribes). Keeps every other template_data key. Guarded on updated_at. |
edit_caption_cue | Edit ONE cue of a slide’s caption_cues by its 0-based index: new text and/or startMs/endMs (source clock, ms). The whole list must stay sorted and non-overlapping. Keeps the caption_cues_source stamp and every other key. Guarded on updated_at. |
set_slide_layout | Apply a camera/content layout preset to one slide. ‘fullscreen-camera’: the webcam fills the frame, no slide content. ‘overlay-top’: webcam fullscreen, slide content drawn OVER it in the top half (content_over_camera). ‘overlay-full’: webcam fullscreen, slide content drawn OVER it on the WHOLE canvas (no safe band, scale 1) — for a full-canvas image. ‘split-top-image’: slide content on top, full-width camera band at the bottom (portrait only). The band crop keeps the FACE: it finds the face in the slide’s recording and writes camera_focus there (else it keeps the upper part of the frame); an existing camera_focus is kept. On an image-only slide (fullscreen-image, no free elements) it trims the image’s flat margins and contains the drawn area in the top band (content_zoom/offset; the trim is recorded in image_trim); a slide with text keeps the plain band fit. ‘background-pip’: slide content fullscreen, small camera bottom-right. Resolves boxes against the project orientation and camera aspect. One write, guarded on updated_at; returns the written and removed keys. Keeps every other template_data key. caption_position (with or without a preset) sets this slide’s caption placement, e.g. ‘over-camera’ on a split slide so the captions do not cover the image. |
transcribe_media | Transcribe a video/audio (URL or media_id) with dfl-render, optionally translated. Returns the text and caption cues {text, startMs, endMs} on the source clock. Waits up to ~45 s; if still running it returns job_id — then call get_media_transcription. Writes nothing; attach_slide_recording transcribes on its own. |
get_media_transcription | Read a transcription job that transcribe_media started: its status while it runs, else the text, cues and translation. |
attach_slide_recording | Attach a raw recording (URL or media_id) to a slide: writes its slide_media row (one per slide), sets the slide duration to the trim window, starts the server transcode, and writes captions — transcribed by dfl-render (optionally translated) or the cues you pass — stamped so the editor keeps them. Waits up to ~45 s; if the transcription still runs, it returns transcription_job_id: then call get_slide_recording. WARNING: an open Lesson Studio tab on this project can overwrite the row on autosave; close it first. |
get_slide_recording | Read a slide’s recording: source URL, trim window, transcode_status (export needs ‘done’; ‘processing’ still runs) and captions. With transcription_job_id (from attach_slide_recording) it finishes the attach once the job is done: fills an unknown length and writes the stamped captions, unless the slide already has captions for this recording. |
split_recording_across_slides | Spread ONE raw take over several slides. mode window (default): every slide points at the same file with its own trim window [start_ms, end_ms), keep ranges become template_data.clips, and ONE transcription of the whole file gives each slide ONLY its own cues (a cue goes to the slide whose window, or keep range, holds its midpoint, clamped to it). mode cut: dfl-render cuts real files (/media/cut, optional fit) and each slide gets its part, with its own cues shifted onto it. create_slides appends new slides for parts without slide_id. Waits ~45 s; if a job still runs it returns job ids to resume with. An open Studio tab on the project can overwrite the rows on autosave. |
import_media_from_url | Import a clip from a public video URL (e.g. a YouTube/X post) through dfl-render: download, optional [start_s, end_s) cut, optional portrait fit, transcription and translation. register_media makes it a public.media row; attach_to_slide_id attaches it to a slide with its (translated) cues, with no second transcription. as_insert (with attach_to_slide_id) makes it an INSERT slide: the clip on its own slide, contained, no camera treatment, optional insert_label. Waits ~45 s; else returns job_id — then call get_media_import. Use only media you may reuse. |
get_media_import | Read an import_media_from_url job. While it runs, its status. When done, it does the same finish: register_media and/or attach_to_slide_id (pass them again). Safe to call again. |
set_slide_insert | Make a slide an INSERT slide: an inserted video on its own slide (the video cuts from the camera to the clip, then back). The slide recording plays as a screen recording — contained, no camera box, no camera focus, no face crop — and the camera keys go. label (e.g. “@rohanpaul_ai”) adds a name label box under the clip, drawn above the video in the export; label null removes it. The slide needs a recording (import_media_from_url with as_insert does all of this in one call). Captions stay as they are. |
create_project_from_video | Turn one raw take into a new Lesson Studio project in one call: creates the project (portrait by default, no project camera box) with burned-in caption settings, a fullscreen-camera slide per part (default: one slide, the whole take), attaches the recording, transcribes (optionally translates) and writes stamped captions. parts has the split_recording_across_slides shape. Returns project, composition and slide ids; if a job still runs it returns the ids to finish with get_slide_recording or split_recording_across_slides. |
set_project_glossary | Set a Lesson Studio project’s caption glossary: the canonical spellings (names, tickers — ‘Bessent’, ‘USDC’) that the server transcription applies to the captions (a whole-word near match becomes the term; timings never change). transcribe_media (with project_id), attach_slide_recording, split_recording_across_slides and create_project_from_video use it by default. Replaces the list; terms: [] clears it. Stored in projects.settings.caption_glossary; guarded on updated_at. |
upload_slide_image | Upload an image (base64 PNG, JPEG, WebP or GIF, at most 6 MB; the type is read from the bytes, SVG is refused) to DFL media as the caller, PUBLIC (the editor and the export load slide images by URL). Returns the url. With slide_id it also places the image as an image box on that slide: by default the WHOLE canvas (720x1280 portrait, 1280x720 landscape) with fit ‘contain’, above the other boxes — pair it with set_slide_layout ‘overlay-full’ for a full-canvas overlay over a fullscreen camera. box {x,y,width,height} in design pixels places it elsewhere. Use it in place of an upload outside the Studio. |
list_project_versions | List the versions of a Lesson Studio project, ordered by version_number descending (current/highest first). |
bump_project_version | Create a new version of a Lesson Studio project (version_number = max existing + 1, starting at 1). Subsequent comments are stamped to this new version. Returns the created version row. |
create_slide_comment | Create a review comment anchored to a slide in a Lesson Studio project (Course Canvas comments). Stamped with the project current version; version 1 is auto-created if none exists. |
list_slide_comments | List Course Canvas comments of a project (newest first), optionally filtered by version and/or resolved state. Enriched with composition title + slide order for reference. |
update_slide_comment | Update a Course Canvas comment body and/or its resolved state. |
delete_slide_comment | Delete a Course Canvas comment by id. |
get_version_comments | Get all comments for a project version as a clipboard-ready text block, one comment per line prefixed with project/composition/slide references. Defaults to the project current version. Use for the “copy all comments → paste to Claude” flow. |
expire_stale_studio_exports | Find Lesson Studio video exports stuck in a non-terminal state (pending/processing) older than N hours and mark them failed with an explanatory error_message, so an abandoned export stops looking like it is still running. Defaults to a DRY RUN — pass dry_run: false to actually write. RLS-scoped to the caller’s own projects. |
start_composition_export | Render a whole composition into an MP4, server-side. Returns as soon as the job is accepted — rendering takes minutes, so this does NOT wait: poll get_studio_export with the returned export_id until it is completed, then pass that export to register_export_as_media to get a media_id the post calendar accepts. Recording a webcam over the deck is still done in the Lesson Studio UI; this exports what is there. Set variant to mobile for a 9:16 reel/short/story cut; omit it to follow the project’s own orientation. |
get_studio_export | Read one export and, while it is still rendering, ask the render service where it is and write the answer back to the row. Poll this after start_composition_export until completed, then pass the export id to register_export_as_media. A failed export carries the real reason, not a generic one. |
list_studio_exports | List the rendered video exports (MP4) of the caller’s Lesson Studio projects, newest first. Defaults to completed exports only. Use the returned export id with register_export_as_media to turn a video into a media_id the post calendar accepts. |
register_export_as_media | Make a completed Lesson Studio export (MP4) usable by the post calendar: creates a public.media row pointing at the exported file and returns the media_id plus the stable media.devfellowship.com/<id> link. Pass that media_id to draft_post_for_review on the campaigns MCP. Idempotent — the same export returns the same media_id. Only exports stored in the DevFellowship bucket can be registered. |
split_export_for_stories | Cut a completed export (MP4) into ordered Story parts of at most 60 s each, and register each part as media. The cuts follow the slide changes when the render service knows them, else scene changes. Returns the parts in order, each with a media_id — pass those media ids, in order, to the campaigns Story sequence. Waits up to ~45 s; if the split is still running, it returns split_job_id — then poll get_story_split with it. |
check_studio_export | A machine review of a COMPLETED export (dfl-render /media/export-check): the duration (and the gap to expected_duration_s), the frame size, black-frame ranges, JPEG frames just after each slide starts, at its middle and just before it ends (slides from the render job), and a caption-over-face flag per mid-slide frame (one vision-LLM pass; face_check false skips it). flags lists what a reviewer must look at first. Waits ~50 s; if the check still runs it returns check_job_id — then call get_export_check. |
get_export_check | Read an export check that check_studio_export started (the same answer when it is done). |
get_story_split | Read a Story split job that split_export_for_stories started. While it runs, this returns its status — call it again. When it is done, it registers each part as media and returns the parts in order with their media ids (the same answer split_export_for_stories gives). Safe to call again: a part keeps its media_id. |
list_templates | List all slide templates available in dfl-slide-templates (reads registry.json from GitHub). |
search_templates | Find the best slide templates for an intent. Returns a RANKED shortlist (top-N) instead of the full list — give a natural-language intent (e.g. “compare two options head-to-head”, “mostrar uma captura de tela”, “one big hero number”) and get back the templates whose registry “when_to_use”/tags/media_profile best match, each with a relevance score and a one-line reason. Use this to PICK a template, then call get_template(id) for its slots. Deterministic + read-only (no auth needed). |
get_template | Fetch all source files (HTML landscape, HTML portrait, CSS landscape, CSS portrait, config YAML) for a specific template. |
update_template | Open a ready-to-merge PR in dfl-slide-templates with updated HTML/CSS and optional config YAML for a template. All provided fields are written in a single commit. Returns the PR URL. registry.json is edited as a MERGE: any registry_* field you omit keeps its current value, so a version bump can no longer erase the discoverability metadata search_templates ranks on. Pass null to delete a field. CREATING a template (an id not yet in registry.json) requires registry_version, registry_name, registry_when_to_use, registry_media_profile and registry_tags, so the new template is findable from the moment it merges. |
list_themes | List the CSS themes that actually exist in dfl-slide-templates at the pinned revision. The set is DERIVED from the repo (registry.json themes[]), so a theme added upstream appears here with no code change. Each entry reports whether its stylesheet resolved, plus its display name and light/dark mode when declared. The response names the commit it was read from and flags a degraded read. |
get_theme | Fetch the CSS source for a specific theme. |
update_theme | Open a ready-to-merge PR in dfl-slide-templates with updated CSS for a theme. Returns the PR URL. The PR also REGISTERS the theme in registry.json’s themes[] — the array themes_doc calls the source of truth and list_themes reads — so a theme published here is discoverable instead of being a stylesheet nothing lists. Registering a NEW theme requires theme_name and theme_mode: neither a brand’s display name nor its light/dark mode can be inferred, and a guess would become the source of truth. A new theme also needs its forbidden-colour contract in scripts/theme.config.json, or lint:css fails — that file is human-gated, so a brand-new theme cannot merge unattended. |
render_slide_image | Render one stored slide to a PNG and return its URL. Wraps the deterministic headless capture in dfl-render with the CALLING user’s own credentials. Returns the dfl-slide-templates commit revision the template was read at AND a composition fingerprint of the deck state the derived slide index came from — together those two reproduce the render. canvas is optional; a canvas the template does not declare is REFUSED and no image is produced (never a fallback to the nearest canvas). |
render_composition_images | Render every surviving slide of a composition to a PNG and return the URLs in order_index order — the batch form of render_slide_image, and the tool a carousel needs. Soft-deleted slides are skipped. Captures run with bounded concurrency. Returns the dfl-slide-templates commit revision and ONE deck-level composition fingerprint shared by every image. canvas is optional; a canvas any template in the deck does not declare is REFUSED before anything is captured. |
capture_cover_slide | Render the composition’s cover slide (template_data.is_cover, set by set_cover_slide) to a PNG for the deck’s video thumbnail, and return its image_url. Errors clearly when the composition has no cover slide. This does NOT write projects.thumbnail_url — no tool in dfl-mcp-studio does yet — so persist the returned image_url yourself once such a tool exists. |
generate_lesson_from_profile | Author a NEW lesson topic in a specific tutor’s proven pedagogical style. Loads that tutor’s stored pedagogy profile (lms.tutor_profiles) and returns a Studio-importable GENERATION SPEC: a typology-biased slide plan (template picks ranked by the professor’s dominant lesson-type via search_templates’ ranker), the professor’s SIGNATURE as few-shot exemplars (verbatim metaphors/openers/cotidiano anchors), and amplify/reduce directives (amplify high-consistency moves, reduce vícios). Deterministic — the tool does NOT call an LLM; the calling agent expands each suggested slide into content, few-shotting the exemplars and honoring the directives. The rubric (HALF A) is never used for generation (de-circularization). |
create_studio_project
Section titled “create_studio_project”Create Lesson Studio Project
Create a new Lesson Studio project in the lesson_studio schema, plus a “Default” composition. Returns the project_id and the default composition_id (attach slides to a composition via create_slide).
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Project title |
owner_id | string | no | UUID of the owner (auth.users.id). Defaults to the authenticated caller. |
orientation | enum | no | Canvas orientation (default: landscape). social-portrait = Feed/carousel post, 1080×1350 (4:5): static slides, no camera/captions/video export; only templates declaring the social-portrait canvas. One of: landscape, portrait, social-portrait. |
description | string | no | Optional project description |
settings | object | no | Optional settings JSON, merged OVER the defaults (resolution, fps, backgroundColor, theme_id, and portrait’s 1:1 camera) rather than replacing them. Pass theme_id here to start on a theme other than the DFL one; update_studio_project changes it later. |
list_studio_projects
Section titled “list_studio_projects”List Lesson Studio Projects
List Lesson Studio projects owned by the caller (RLS-scoped), most recently updated first.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | number | no | Max projects to return (default: 50, max: 100) |
offset | number | no | Number of projects to skip (pagination) |
search | string | no | Search by title (ilike) |
delete_studio_project
Section titled “delete_studio_project”Delete Lesson Studio Project
Delete a Lesson Studio project. Its compositions, slides, slide_elements and slide_media cascade. RLS-scoped: only the caller’s own projects can be deleted.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project to delete |
update_studio_project
Section titled “update_studio_project”Update Lesson Studio Project
Update a project’s title, description, thumbnail, orientation, active theme (settings.theme_id), burned-in captions (settings.captions_*) or export watermark (settings.watermark). For a 9:16 reel, pass captions {enabled: true, position: “top”} and watermark {enabled: true} before start_composition_export. Camera defaults are set_project_camera’s job, not this tool’s. theme_id is checked against list_themes when the registry is reachable; an unknown id is rejected. A settings write (theme_id, captions, watermark) is guarded on updated_at (it reads settings first to merge; keys you leave out keep their stored value); a change to the other fields overwrites only the columns passed. Changing orientation does NOT reshape camera boxes — it warns and points at set_project_camera.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
title | string | no | New title |
description | string | no | New description, or null to clear it |
thumbnail_url | string | no | New thumbnail URL, or null to clear it |
orientation | enum | no | Canvas orientation. social-portrait = Feed/carousel post, 1080×1350 (4:5): static slides, no camera/captions/video export; only templates declaring the social-portrait canvas. Changing an existing project to or from social-portrait is refused unless it has no slides (soft-deleted included); an empty project switching resets safe_area_top and camera settings to the target orientation defaults. One of: landscape, portrait, social-portrait. |
theme_id | string | no | Theme id to apply project-wide, as reported by list_themes (e.g. “default”, “devfellowship”). |
captions | object | no | Burned-in caption settings, merged key by key into settings.captions_*. |
watermark | object | no | Export watermark settings, merged key by key into settings.watermark. The logo comes from the theme. |
create_composition
Section titled “create_composition”Create Composition
Create a composition (lesson-level grouping / “frame”) under a Lesson Studio project. Slides attach to a composition.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the parent project |
title | string | no | Composition title (default: “Untitled Composition”) |
order_index | number | no | Display order within the project. If omitted, appended after existing compositions. |
list_compositions
Section titled “list_compositions”List Compositions
List the compositions of a Lesson Studio project, ordered by order_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the parent project |
update_composition
Section titled “update_composition”Update Composition
Update a composition title and/or order_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the composition |
title | string | no | New title |
order_index | number | no | New display order |
delete_composition
Section titled “delete_composition”Delete Composition
Delete a composition. Its slides must be removed or reassigned first (slides.composition_id is NOT NULL).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the composition to delete |
create_slide
Section titled “create_slide”Create Slide
Create a slide under a composition (mirrors the Lesson Studio editor insert shape so it is valid + editable in the UI). Optional template_data.animation animates the slide. Preset ‘steps-reveal’ shows list items one at a time; use it when the narration walks through steps, on a template whose items carry data-anim-target=“step”. Example: {preset:‘steps-reveal’,version:1,beat_source:‘manual’,beats:[{kind:‘reveal’,target:‘step’,index:0,start_ms:600,duration_ms:300}]}. Sort beats by start_ms. Every beat must end before the slide duration_ms. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition |
project_id | string | no | UUID of the parent project. Resolved from the composition if omitted. |
order_index | number | no | Display order within the composition. If omitted, appended at the end. |
template_id | string | no | Slide template id, as published in the dfl-slide-templates registry. Call search_templates(“<what the slide should do>”) or list_templates to pick one, and get_template(“<id>”) for its exact slots — this list is NOT enumerated here on purpose, so it can never go stale. An id that is not in the registry is rejected. Required slots must carry real content (see template_data). For a single image filling the whole canvas, prefer the dedicated create_image_slide tool. |
template_data | object | no | Template data JSON (text/props consumed by the template renderer). Validated against the slots the template declares in the registry (templates/<id>/config.yaml) — call get_template(“<id>”) to see each slot’s type and a valid sample. Required slots must be non-empty and correctly shaped: text fields need real text (not "" or whitespace); array fields need at least one entry matching the sample shape (e.g. bullet-list items: [{ “text”: “Agents run tools autonomously” }], table rows: [{ “cells”: [{ “value”: “Revenue” }] }]). Use template_id “blank” for an intentionally empty slide. |
duration_ms | number | no | Slide duration in ms, a positive integer (default: 5000) |
background_color | string | no | Hex background color (default: “#1a1a2e”). Only a color other than the default sets template_data.background_override = true, so the Studio shows it over the template background. |
background_image_url | string | no | Optional background image URL. A non-empty URL also sets template_data.background_override = true. |
transition_type | string | no | Transition type (default: “fade”) |
transition_duration_ms | number | no | Transition duration in ms (default: 500) |
create_image_slide
Section titled “create_image_slide”Create Full-screen Image Slide
Create a slide that displays a single image filling the entire slide canvas, using the fullscreen-image template. Use this to bring your own finished slide images (e.g. an image hosted on S3, or a pre-rendered slide exported from another tool) instead of reusing the built-in content templates. The fit option controls how the image fills the canvas: cover (default) crops to fill, contain letterboxes to show the whole image.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition |
project_id | string | no | UUID of the parent project. Resolved from the composition if omitted. |
image_url | string | yes | URL of the image to display full-screen (e.g. an S3 URL) |
fit | enum | no | How the image fills the canvas: “cover” (crop to fill, default) or “contain” (letterbox the whole image) One of: cover, contain. |
bg | string | no | Letterbox/background color (hex) used behind the image in “contain” mode. Defaults handled by the template CSS. |
image_alt | string | no | Accessibility alt text for the image |
order_index | number | no | Display order within the composition. If omitted, appended at the end. |
duration_ms | number | no | Slide duration in ms (default: 5000) |
background_color | string | no | Hex background color (default: “#1a1a2e”) |
transition_type | string | no | Transition type (default: “fade”) |
transition_duration_ms | number | no | Transition duration in ms (default: 500) |
list_slides
Section titled “list_slides”List Slides
List the slides of a composition, ordered by order_index. Soft-deleted slides are excluded by default; pass include_deleted:true to return all (including soft-deleted).
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the parent composition |
include_deleted | boolean | no | When true, also return soft-deleted slides (deleted_at IS NOT NULL). Default false → only live slides. |
update_slide
Section titled “update_slide”Update Slide
Update a slide. template_data replaces the slots; an omitted slot is removed. Studio keys (not slots): narration_notes, animation, background_override, camera, camera_box, capture, camera_box_custom, watermark, webcam_layout, pointer_track, is_cover, sfx, framing, caption_cues, clips, style_overrides, slide_name, caption_cues_source, caption_language, camera_segments, content_over_camera, caption_position, camera_focus, insert. An omitted Studio key is kept, except animation: a template_id change removes it unless you send it. Remove one via clear_template_data_keys, only when the user asks. narration_notes and pointer_track are user work and cannot be restored. The response lists removed_template_data_keys. A new background sets background_override = true. To show the template background again, send clear_template_data_keys: [‘background_override’] and no background field. A sent animation is checked as the create_slide description says, against the slide duration_ms. A new duration_ms is checked against the stored animation. The response can list warnings. sfx: [{sound, at_ms, volume?}] — see list_sound_effects.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide |
order_index | number | no | New display order in the composition |
composition_id | string | no | Move the slide to another composition |
template_id | string | no | Registry template id. An unknown id is rejected. |
template_data | object | no | Template slot values, validated against get_template(“<id>”). |
clear_template_data_keys | enum[] | no | Studio keys to remove; do not also send in template_data. |
duration_ms | number | no | Slide duration in ms (positive int) |
background_color | string | no | Hex background color |
background_image_url | string | no | Background image URL |
transition_type | string | no | Transition type |
transition_duration_ms | number | no | Transition duration, ms |
delete_slide
Section titled “delete_slide”Delete Slide
Delete a slide. By default this is a SOFT delete (recoverable via restore_slide; slide_elements/slide_media are kept). Pass hard:true for a permanent delete (slide_elements + slide_media cascade).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide to delete |
hard | boolean | no | When true, permanently delete the slide (slide_elements + slide_media cascade). Default false → soft delete (recoverable via restore_slide). |
restore_slide
Section titled “restore_slide”Restore Slide
Restore a soft-deleted slide (clears deleted_at/deleted_by so it reappears in list_slides). Only works on slides that were soft-deleted via delete_slide; a hard-deleted slide is gone permanently.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide to restore |
list_animation_presets
Section titled “list_animation_presets”List Animation Presets
The slide animation presets set_slide_animation can apply, with the template_data slots each one reads. Pass template_id to also learn which of them that template can actually run: the answer is read from the live template markup (data-anim-target), so it cannot go stale. A preset whose slots the slide does not carry produces no beats — that is what requires describes.
| Parameter | Type | Required | Description |
|---|---|---|---|
template_id | string | no | Slide template id, as published in the dfl-slide-templates registry. When given, each preset also reports runs_on_template and the targets the template declares. |
set_slide_animation
Section titled “set_slide_animation”Set Slide Animation
Animate a slide with one of the presets from list_animation_presets. With no beats, the timings are computed from the slide’s own template_data and duration_ms — the same defaults the Lesson Studio editor applies, already fitted to the slide length and valid at 24, 30 and 60 fps. Pass beats only for a timing the defaults do not express. “zoom-focus” is the one preset with no template slot — it works on any slide, pushing in and holding until a later zoom beat frames back out. A “zoom” beat also COMPOSES with every other preset (add one to the “beats” array of a steps-reveal/chat-typing/highlight/image-enter spec): it drives the synthetic “slide” target no template markup ever declares, so it never collides with that preset’s own beats. Writes template_data.animation and leaves every other template_data key untouched. To remove an animation, call update_slide with clear_template_data_keys: [‘animation’].
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to animate |
preset | enum | yes | Animation preset. A preset the slide’s template cannot drive is REFUSED here, not silently stored: call list_animation_presets(“<template_id>”) to see which ones run. A preset whose targets the template does not declare drives nothing, and one whose slots the slide does not carry is rejected here. “zoom-focus” always runs_on_template: true — it reads no slot and needs no declared target. “element-reveal” is the same kind of preset for the free-canvas elements of the slide (slide_elements rows, never a template_data slot): it needs at least one element on the slide. One of: steps-reveal, chat-typing, highlight, image-enter, zoom-focus, element-reveal. |
beats | object[] | no | Explicit beats, sorted by start_ms, every one ending before the slide duration_ms. Omit this to get the preset defaults fitted to the slide — that is the recommended path. Sending beats marks the spec beat_source “manual”, so the editor will not silently re-time them. A “zoom” beat ({ kind: “zoom”, target: “slide”, start_ms, duration_ms?, rect: { x, y, width } }) may be mixed into ANY preset’s beats to push in on part of the canvas; rect is fractions of the canvas (same shape as camera_box), width floored at 0.1 (10x zoom); a rect of { x:0, y:0, width:1 } zooms back out to the full canvas. A “reveal” beat on target “element” (one of a slide’s free-canvas boxes, see create_slide_element/list_slide_elements for the index) may also carry: effect, one of “fade”, “rise”, “drop”, “slide-left”, “slide-right”, “zoom-in”, “shrink-in”, “spin-shrink”, “pop”, “blur-in”, “swing-in”, “bounce-in”, “scramble-in”, “glitch-in”, “slide-up”, “slide-down”, “flip-in”, “wipe-right”, “wipe-up”, “typewriter-in”, “count-up”, “pulse”, “shake”, “wobble”, “tada”, “heartbeat”, “spin”, “ken-burns”, “float”, “breathe”, “fade-out”, “drop-out”, “shrink-out”, “spin-out”, “blur-out”, “glitch-out”, “rise-out”, “slide-left-out”, “slide-right-out”, “zoom-through-out”, “wipe-left-out” — plays instead of the default fade + rise; and focus (boolean) — dims and blurs the rest of the slide while this box is the subject, for a camera-lens feel with no separate zoom beat. Example: { kind: “reveal”, target: “element”, index: 0, start_ms: 600, duration_ms: 400, effect: “pop”, focus: true }. |
tracks | object[] | no | Keyframe tracks for free-canvas boxes (spec version 2). Each track is { index, property, keyframes: [{ t_ms, value, easing }] }: property is one of “x”, “y”, “scale”, “rotate”, “opacity”, “blur”; easing one of “linear”, “in”, “out”, “inOut”, “hold” and shapes the segment that STARTS at that keyframe (“hold” jumps at the next one). Values are relative to the box rest pose: x/y are design-pixel offsets, rotate a degree offset, blur added px, scale and opacity (0..1) multipliers. Keyframes strictly increasing in t_ms and before the slide end; one track per box and property; a box revealed by a beat with no effect cannot take tracks. With tracks and no beats the spec animates the boxes by tracks alone (no default entrances are added). Example: { index: 0, property: “x”, keyframes: [{ t_ms: 0, value: 0, easing: “inOut” }, { t_ms: 1200, value: 300, easing: “linear” }] }. |
formulas | object[] | no | Formula-driven box properties (spec version 2): { index, property, source, start_ms, end_ms }. source is an expression (max 200 chars) over t (seconds), p (0..1 across [start_ms, end_ms]), i (box index), w/h (the canvas) with + - * / % ^, unary minus, pi and sin cos abs min max clamp lerp step smoothstep noise(x, seed?). Same units as a track value; outside its window the value holds its end values; a non-finite result is identity. One driver per box and property: a property with a track cannot also take a formula. A parse error names the character position. Example: { index: 0, property: “y”, source: “sin(t * 2 * pi) * 20”, start_ms: 0, end_ms: 3000 }. |
canvas | object | no | Design canvas a formula reads as w/h. Defaults to the project’s design canvas: 1280x720 landscape, 720x1280 portrait. |
behaviours | object[] | no | Optional metadata naming the behaviour that wrote a box’s formulas, so the Lesson Studio picker re-opens it: { index, id, params: { name: number }, owned: [properties] }, one per box; every owned property needs a formula on that box. It is NOT expanded here: send the formulas it stands for (the Studio catalog: float → y, orbit → x/y, shake → x/y, pulse → scale, drift → x/y, typewriter-cursor → opacity). |
search_slide_images
Section titled “search_slide_images”Search Slide Images
Search the stock photo library for an image to put on a slide, and get back candidate URLs. Use it to fill an image-type template slot without a human opening the editor: pick a result and write its url with update_slide (template_data.image) or create_image_slide (image_url). This tool only reads — it never touches a slide. Match orientation to the project canvas so the crop is not fighting the layout. Results carry photographer and pexels_url: the licence asks for that credit, so keep it with the image.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | yes | What to look for, in English — e.g. “developer at a laptop”, “solar panels”. Stock search is keyword-based, so a short concrete noun phrase beats a sentence. |
orientation | enum | no | Crop to ask the provider for. Defaults to landscape; pass portrait for a 9:16 project. social-portrait (Feed, 1080×1350 4:5) searches as portrait. One of: landscape, portrait, social-portrait. |
per_page | number | no | How many candidates to return (default 8, max 20). |
set_cover_slide
Section titled “set_cover_slide”Set Cover Slide
Mark a slide as the deck’s cover (template_data.is_cover), or unmark it (is_cover: false). The cover is used for the video thumbnail via capture_cover_slide, but dfl-render EXCLUDES it from the rendered video, its slide count, and the 01/09 deck index. Setting is_cover: true clears the flag from every other slide in the same composition first — a deck has at most one cover. Preserves every other template_data key. To set the composition’s thumbnail after capturing it, chain the result into whichever tool writes projects.thumbnail_url (this tool never writes it).
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to mark or unmark as the cover |
is_cover | boolean | no | true (the default) to make this slide the cover; false to unmark it |
list_slide_beats
Section titled “list_slide_beats”List Slide Beats
List the beats of a slide’s template_data.animation, each tagged with the beat_index add_slide_beat/update_slide_beat/remove_slide_beat address. Returns an empty beats array (not an error) when the slide carries no animation.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
add_slide_beat
Section titled “add_slide_beat”Add Slide Beat
Insert one beat into a slide’s template_data.animation.beats. The slide must already carry an animation (create one with set_slide_animation first). Beats must stay sorted by start_ms; a beat or at_index that breaks the order is rejected with the same validator set_slide_animation uses. Marks beat_source “manual”. Returns the new beat_index.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
beat | object | yes | The beat to insert — same shape set_slide_animation’s “beats” entries take, e.g. {kind:“zoom”, target:“slide”, start_ms, rect:{x,y,width}} or {kind:“reveal”, target:“step”, index, start_ms, duration_ms}. |
at_index | number | no | Position to insert at (0 = first). Omit to append at the end. |
update_slide_beat
Section titled “update_slide_beat”Update Slide Beat
Replace one beat of a slide’s template_data.animation.beats, addressed by beat_index (from list_slide_beats). beat replaces the ENTIRE beat, not just the fields you pass — send every field the beat needs, same shape set_slide_animation’s “beats” entries take. Beats must stay sorted by start_ms after the replacement. Marks beat_source “manual”.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
beat_index | number | yes | Index of the beat to replace (from list_slide_beats) |
beat | object | yes | The full replacement beat, including kind |
remove_slide_beat
Section titled “remove_slide_beat”Remove Slide Beat
Remove one beat from a slide’s template_data.animation.beats, addressed by beat_index (from list_slide_beats). A spec needs at least one beat, so removing the only beat is rejected — use update_slide with clear_template_data_keys: [‘animation’] to drop the animation entirely. Marks beat_source “manual”.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
beat_index | number | yes | Index of the beat to remove (from list_slide_beats) |
create_slide_element
Section titled “create_slide_element”Create Slide Element
Create a free-canvas box on a slide: a text box, an image, or a shape drawn on top of the slide’s template. position_x/position_y/width/height/rotation are DESIGN pixels — the canvas is 1280x720 for a landscape project and 720x1280 for a portrait one (the parent project’s own orientation, not an argument here). A box entirely outside that canvas is rejected; a box that only partly overlaps it is allowed (that is a normal partial-off-canvas placement, not an error). content is validated by type: text.text is at most 2000 characters; image.url must be an http(s) URL; every color (text.color, shape.fill, shape.stroke, …) must be a hex color (#rgb, #rrggbb, #rrggbbaa) or an rgba()/rgb() function; an unknown content key is rejected. A ‘text’ box may omit content fields — missing ones are filled from the Studio’s own default text style, so { type: ‘text’, content: { text: ‘Hello’ } } is enough for a fully-styled box. ‘image’ and ‘shape’ boxes must supply every field their type needs. The response carries animation_index: the box’s position among the slide’s elements, ordered by creation time — THIS is the index a set_slide_animation “element-reveal” beat ({ kind: “reveal”, target: “element”, index }) must use to animate THIS box, because that index is the box’s position in the stored array, never its z_index or the order it was drawn on screen. Example: create_slide_element({ slide_id, type: “text”, content: { text: “Welcome” }, position_x: 100, position_y: 80, width: 600, height: 120 }) → { element: {…}, animation_index: 0 }.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to add the box to |
type | enum | yes | Box kind: text, image, or shape One of: text, image, shape. |
content | object | no | Type-specific content. text: { text, fontFamily?, fontSize?, fontWeight?, color?, textAlign?, lineHeight?, letterSpacing?, fontStyle?, textTransform?, backgroundColor?, verticalAlign? } — missing fields fall back to the Studio default text style. image: { url, alt, objectFit, crop?, enter?, growFrom? } — url, alt and objectFit are required, no defaults. shape: { shapeType, fill, stroke, strokeWidth, borderRadius? } — all of shapeType/fill/stroke/strokeWidth are required. |
position_x | number | yes | Left edge, design pixels |
position_y | number | yes | Top edge, design pixels |
width | number | yes | Box width, design pixels |
height | number | yes | Box height, design pixels |
rotation | number | no | Degrees, clockwise (default 0) |
opacity | number | no | 0 (invisible) to 1 (opaque); default 1 |
z_index | number | no | Stacking order among the slide’s boxes. Default: one above the highest existing box. |
list_slide_elements
Section titled “list_slide_elements”List Slide Elements
List a slide’s free-canvas boxes (text/image/shape), ordered by creation time. Each element carries animation_index: its position in this order, and the exact index a reveal beat with target: "element" must reference to animate it — never the box’s z_index or draw order. Example response: { elements: [{ id, type: “text”, content: {…}, position_x, position_y, width, height, rotation, opacity, z_index, animation_index: 0 }, …] }.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
update_slide_element
Section titled “update_slide_element”Update Slide Element
Update a free-canvas box’s content and/or geometry (position_x/position_y/width/height/rotation/opacity/z_index). Every field is optional — send only what changes. content is a PARTIAL patch: it is merged over the box’s stored content and the merged object is re-validated against its type’s rules (same rules as create_slide_element — text max 2000 chars, image url must be http(s), colors must be hex or rgba()/rgb(), unknown keys rejected). Geometry is design pixels, same canvas rule as create_slide_element: a box moved/resized fully outside the canvas is rejected. The box’s type and its animation_index (set by create order) cannot be changed here. Example: update_slide_element({ id, content: { text: “Updated headline” }, position_x: 140 }).
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide element |
content | object | no | Partial content patch, merged then re-validated |
position_x | number | no | Left edge, design pixels |
position_y | number | no | Top edge, design pixels |
width | number | no | Box width, design pixels |
height | number | no | Box height, design pixels |
rotation | number | no | Degrees, clockwise |
opacity | number | no | 0 (invisible) to 1 (opaque) |
z_index | number | no | Stacking order among the slide’s boxes |
delete_slide_element
Section titled “delete_slide_element”Delete Slide Element
Delete a free-canvas box and repair the slide’s animation in the same call. A reveal beat (target: "element") addresses a box by its position among the slide’s elements (animation_index), so removing a box shifts every later box’s beat index down by one — this tool does that shift automatically: the removed box’s own beat is dropped, later beats are reindexed keeping their effect/focus, and if no element beat survives template_data.animation is removed entirely (an animation with zero beats is not valid). The response reports removed_beat_count, shifted_beat_count and animation_cleared so the caller knows exactly what changed.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the slide element to delete |
get_camera
Section titled “get_camera”Get Camera
Read the camera box + visibility for a project (project_id) and optionally one of its slides (slide_id). Reports the RAW override at each level (project default, slide override) plus the RESOLVED value that actually applies, following the same precedence dfl-render uses (slide, then project, then the built-in default). Pass slide_id alone to also resolve the project it belongs to.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | no | UUID of the project. Required unless slide_id is given. |
slide_id | string | no | UUID of a slide, to also report its resolved/override camera. |
set_project_camera
Section titled “set_project_camera”Set Project Camera
Set the project-wide camera default: aspect (16:9 or 1:1), box (preset size+corner or custom fractions), and default visibility (off/overlay). Applies to every slide that does not override its own camera via set_slide_camera. Changing aspect with no box in the same call reshapes the current project box (and every slide’s own camera_box override) to fit the new aspect, mirroring the Lesson Studio editor. Guarded on updated_at. focus {x, y} (0..1 of the camera frame) sets the project default crop point (settings.camera_focus); a slide focus (set_slide_camera) wins.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
aspect | enum | no | Camera shape. Omit to leave it unchanged. One of: 16:9, 1:1. |
box | object | object | no | New project-default camera box. |
clear_box | boolean | no | Remove settings.camera_box, so the project falls back to the built-in default. |
default_visibility | enum | no | Camera visibility for a slide with no override (default: overlay). One of: off, overlay. |
clear_default_visibility | boolean | no | Remove settings.camera_default. |
focus | object | no | Project default camera crop point (settings.camera_focus). |
clear_focus | boolean | no | Remove settings.camera_focus (the centre). |
set_slide_camera
Section titled “set_slide_camera”Set Slide Camera
Override this slide’s camera box and/or visibility, on top of the project default set_project_camera sets. box resolves against the parent project’s orientation and camera aspect. clear_box removes the box override (the slide follows the project box again); clear_visibility removes the visibility override. segments limits WHEN the corner (picture-in-picture) camera is drawn — refused on a fullscreen-webcam slide: a list of { start_ms, end_ms } windows in slide time (whole ms, sorted, non-overlapping, within the slide duration, 1-50 of them); the voice keeps playing outside them. clear_segments shows the camera for the whole slide again. To hide it on the whole slide use visibility: ‘off’. focus {x, y} (0..1 of the camera frame) moves the crop of the camera off the centre, e.g. {x: 0.3, y: 0.5} for a face left of centre in a 16:9 take on a portrait slide; clear_focus goes back to the project focus / the centre. Preserves every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
box | object | object | no | New camera box override for this slide. |
clear_box | boolean | no | Remove this slide’s camera_box override. |
visibility | enum | no | Camera visibility override for this slide. One of: off, overlay. |
clear_visibility | boolean | no | Remove this slide’s camera visibility override. |
segments | object[] | no | Windows (slide ms) in which the camera is drawn; stored as template_data.camera_segments. |
clear_segments | boolean | no | Remove this slide’s camera_segments (camera on the whole slide). |
focus | object | no | The point of the camera frame the crop keeps (template_data.camera_focus). |
clear_focus | boolean | no | Remove this slide’s camera_focus (the project focus, else the centre). |
list_sound_effects
Section titled “list_sound_effects”List Sound Effects
The sound-effect catalog available to template_data.sfx / set_slide_sfx: every valid sound key, its category, use-case hint and duration. Read-only, no database access. Pass category to filter.
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | no | Restrict to one category id (see the returned categories list). |
set_slide_sfx
Section titled “set_slide_sfx”Set Slide Sound Effects
Write this slide’s sound effects (template_data.sfx). Each entry is {sound, at_ms, volume?} — sound is a catalog key from list_sound_effects, at_ms must start before the slide duration_ms. mode “replace” (default) discards the stored list; “append” adds to it. An empty array with “replace” removes the key. Preserves every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
sfx | object[] | yes | The sound effects to write (or add, in append mode). |
mode | enum | no | Default “replace”. One of: replace, append. |
set_slide_captions
Section titled “set_slide_captions”Set Slide Captions
Replace a slide’s caption cues (template_data.caption_cues). Cues use the recording’s SOURCE clock in ms (the file, not the slide), sorted and non-overlapping, max 2000. source_stamp ‘current’ (default) stamps caption_cues_source with the slide’s recording URL, so the Studio editor does not re-transcribe and overwrite them; ‘none’ removes the stamp (the editor re-transcribes). Keeps every other template_data key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
cues | object[] | yes | Cues on the recording (SOURCE) clock, ms: sorted, non-overlapping, endMs > startMs, text non-empty. |
language | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
source_stamp | enum | no | ’current’ (default): stamp with the slide recording URL. ‘none’: remove the stamp. One of: current, none. |
edit_caption_cue
Section titled “edit_caption_cue”Edit Caption Cue
Edit ONE cue of a slide’s caption_cues by its 0-based index: new text and/or startMs/endMs (source clock, ms). The whole list must stay sorted and non-overlapping. Keeps the caption_cues_source stamp and every other key. Guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
index | number | yes | 0-based index of the cue in caption_cues |
text | string | no | New text (non-empty) |
startMs | number | no | New start, source ms |
endMs | number | no | New end, source ms |
set_slide_layout
Section titled “set_slide_layout”Set Slide Layout
Apply a camera/content layout preset to one slide. ‘fullscreen-camera’: the webcam fills the frame, no slide content. ‘overlay-top’: webcam fullscreen, slide content drawn OVER it in the top half (content_over_camera). ‘overlay-full’: webcam fullscreen, slide content drawn OVER it on the WHOLE canvas (no safe band, scale 1) — for a full-canvas image. ‘split-top-image’: slide content on top, full-width camera band at the bottom (portrait only). The band crop keeps the FACE: it finds the face in the slide’s recording and writes camera_focus there (else it keeps the upper part of the frame); an existing camera_focus is kept. On an image-only slide (fullscreen-image, no free elements) it trims the image’s flat margins and contains the drawn area in the top band (content_zoom/offset; the trim is recorded in image_trim); a slide with text keeps the plain band fit. ‘background-pip’: slide content fullscreen, small camera bottom-right. Resolves boxes against the project orientation and camera aspect. One write, guarded on updated_at; returns the written and removed keys. Keeps every other template_data key. caption_position (with or without a preset) sets this slide’s caption placement, e.g. ‘over-camera’ on a split slide so the captions do not cover the image.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
preset | enum | no | The layout preset. Optional when caption_position is set. One of: fullscreen-camera, overlay-top, overlay-full, split-top-image, background-pip. |
caption_position | enum | no | Where THIS slide’s burned-in captions sit, over the project setting: top | middle | bottom (project semantics, the camera box is avoided), over-camera (bottom edge, NOT avoiding the camera: on a split slide the captions sit on the camera band, off the image), project (remove the override). One of: top, middle, bottom, over-camera, project. |
transcribe_media
Section titled “transcribe_media”Transcribe Media
Transcribe a video/audio (URL or media_id) with dfl-render, optionally translated. Returns the text and caption cues {text, startMs, endMs} on the source clock. Waits up to ~45 s; if still running it returns job_id — then call get_media_transcription. Writes nothing; attach_slide_recording transcribes on its own.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | object | object | yes | The source: {url} or {media_id}. |
language | enum | no | Spoken language; default ‘auto’. One of: auto, pt, en. |
translate_to | string | no | Also return the cues translated to this language. |
project_id | string | no | Use this project’s caption glossary (set_project_glossary). |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
get_media_transcription
Section titled “get_media_transcription”Get Media Transcription
Read a transcription job that transcribe_media started: its status while it runs, else the text, cues and translation.
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | The job_id transcribe_media returned. |
attach_slide_recording
Section titled “attach_slide_recording”Attach Slide Recording
Attach a raw recording (URL or media_id) to a slide: writes its slide_media row (one per slide), sets the slide duration to the trim window, starts the server transcode, and writes captions — transcribed by dfl-render (optionally translated) or the cues you pass — stamped so the editor keeps them. Waits up to ~45 s; if the transcription still runs, it returns transcription_job_id: then call get_slide_recording. WARNING: an open Lesson Studio tab on this project can overwrite the row on autosave; close it first.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
source | object | object | yes | The source: {url} or {media_id}. |
media_type | enum | no | Default ‘webcam_video’. One of: webcam_video, screen_recording, audio. |
webcam_layout | enum | no | Also set the slide webcam_layout. One of: fullscreen, pip. |
trim | object | no | Window of the recording that plays on the slide, SOURCE ms [start, end). |
duration_ms | number | no | Source length, ms. Else estimated from the transcription. |
transcribe | boolean | no | Default true. Ignored when captions is given. |
language | enum | no | Spoken language; default ‘auto’. One of: auto, pt, en. |
translate_to | string | no | Write the captions translated to this language. |
captions | object[] | no | Write these cues (source clock) and skip transcription. |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
get_slide_recording
Section titled “get_slide_recording”Get Slide Recording
Read a slide’s recording: source URL, trim window, transcode_status (export needs ‘done’; ‘processing’ still runs) and captions. With transcription_job_id (from attach_slide_recording) it finishes the attach once the job is done: fills an unknown length and writes the stamped captions, unless the slide already has captions for this recording.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide |
transcription_job_id | string | no | The job attach_slide_recording returned. |
split_recording_across_slides
Section titled “split_recording_across_slides”Split Recording Across Slides
Spread ONE raw take over several slides. mode window (default): every slide points at the same file with its own trim window [start_ms, end_ms), keep ranges become template_data.clips, and ONE transcription of the whole file gives each slide ONLY its own cues (a cue goes to the slide whose window, or keep range, holds its midpoint, clamped to it). mode cut: dfl-render cuts real files (/media/cut, optional fit) and each slide gets its part, with its own cues shifted onto it. create_slides appends new slides for parts without slide_id. Waits ~45 s; if a job still runs it returns job ids to resume with. An open Studio tab on the project can overwrite the rows on autosave.
| Parameter | Type | Required | Description |
|---|---|---|---|
source | object | object | yes | The source: {url} or {media_id}. |
parts | object[] | yes | One entry per slide, in slide order. |
create_slides | object | no | — |
mode | enum | no | Default ‘window’. One of: window, cut. |
fit | enum | no | cut mode only. One of: none, portrait-crop, portrait-letterbox. |
media_type | enum | no | Default ‘webcam_video’. One of: webcam_video, screen_recording, audio. |
duration_ms | number | no | Source length, ms, when known. |
transcribe | boolean | no | Default true. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
transcription_job_id | string | no | Resume: the job a previous call returned. |
cut_job_id | string | no | Resume: the cut job a previous call returned. |
glossary | string[] | no | Canonical terms (names, tickers) the captions must spell right. Default: the project glossary. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
import_media_from_url
Section titled “import_media_from_url”Import Media From URL
Import a clip from a public video URL (e.g. a YouTube/X post) through dfl-render: download, optional [start_s, end_s) cut, optional portrait fit, transcription and translation. register_media makes it a public.media row; attach_to_slide_id attaches it to a slide with its (translated) cues, with no second transcription. as_insert (with attach_to_slide_id) makes it an INSERT slide: the clip on its own slide, contained, no camera treatment, optional insert_label. Waits ~45 s; else returns job_id — then call get_media_import. Use only media you may reuse.
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | yes | Public page or file URL of the video. |
start_s | number | no | Clip start, seconds. |
end_s | number | no | Clip end, seconds. |
fit | enum | no | One of: none, portrait-crop, portrait-letterbox. |
transcribe | boolean | no | Default true. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Translate the cues to this language. |
attach_to_slide_id | string | no | Attach the clip to this slide. |
webcam_layout | enum | no | With attach_to_slide_id. One of: fullscreen, pip. |
as_insert | boolean | no | With attach_to_slide_id: make it an INSERT slide (its own slide, contained, no camera treatment). |
insert_label | string | no | With as_insert: a name label under the clip. |
register_media | boolean | no | Also create a public.media row (default false). |
get_media_import
Section titled “get_media_import”Get Media Import
Read an import_media_from_url job. While it runs, its status. When done, it does the same finish: register_media and/or attach_to_slide_id (pass them again). Safe to call again.
| Parameter | Type | Required | Description |
|---|---|---|---|
job_id | string | yes | The job_id import_media_from_url returned. |
attach_to_slide_id | string | no | — |
webcam_layout | enum | no | One of: fullscreen, pip. |
as_insert | boolean | no | — |
insert_label | string | no | — |
register_media | boolean | no | — |
set_slide_insert
Section titled “set_slide_insert”Set Slide Insert
Make a slide an INSERT slide: an inserted video on its own slide (the video cuts from the camera to the clip, then back). The slide recording plays as a screen recording — contained, no camera box, no camera focus, no face crop — and the camera keys go. label (e.g. “@rohanpaul_ai”) adds a name label box under the clip, drawn above the video in the export; label null removes it. The slide needs a recording (import_media_from_url with as_insert does all of this in one call). Captions stay as they are.
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide with the clip. |
label | string | no | Name label text; null removes it; omit to keep it. |
create_project_from_video
Section titled “create_project_from_video”Create Project From Video
Turn one raw take into a new Lesson Studio project in one call: creates the project (portrait by default, no project camera box) with burned-in caption settings, a fullscreen-camera slide per part (default: one slide, the whole take), attaches the recording, transcribes (optionally translates) and writes stamped captions. parts has the split_recording_across_slides shape. Returns project, composition and slide ids; if a job still runs it returns the ids to finish with get_slide_recording or split_recording_across_slides.
| Parameter | Type | Required | Description |
|---|---|---|---|
title | string | yes | Project title |
source | object | object | yes | The source: {url} or {media_id}. |
orientation | enum | no | Default ‘portrait’. One of: portrait, landscape. |
captions | object | no | — |
parts | object[] | no | Default: one slide, the whole take. |
mode | enum | no | With parts. Default ‘window’. One of: window, cut. |
webcam_layout | enum | no | Default ‘fullscreen’. One of: fullscreen, pip. |
duration_ms | number | no | Source length, ms, when known. |
language | enum | no | One of: auto, pt, en. |
translate_to | string | no | Language of the cues, BCP-47 (pt, en, pt-BR). Stored as caption_language. |
glossary | string[] | no | Caption glossary: stored on the new project and applied now. |
fix_cues | boolean | no | Also run the LLM cue-fix pass (word order, misheard words, glossary). Default false. |
set_project_glossary
Section titled “set_project_glossary”Set Project Caption Glossary
Set a Lesson Studio project’s caption glossary: the canonical spellings (names, tickers — ‘Bessent’, ‘USDC’) that the server transcription applies to the captions (a whole-word near match becomes the term; timings never change). transcribe_media (with project_id), attach_slide_recording, split_recording_across_slides and create_project_from_video use it by default. Replaces the list; terms: [] clears it. Stored in projects.settings.caption_glossary; guarded on updated_at.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | The project. |
terms | string[] | yes | The full list (replaces the old one). |
upload_slide_image
Section titled “upload_slide_image”Upload Slide Image
Upload an image (base64 PNG, JPEG, WebP or GIF, at most 6 MB; the type is read from the bytes, SVG is refused) to DFL media as the caller, PUBLIC (the editor and the export load slide images by URL). Returns the url. With slide_id it also places the image as an image box on that slide: by default the WHOLE canvas (720x1280 portrait, 1280x720 landscape) with fit ‘contain’, above the other boxes — pair it with set_slide_layout ‘overlay-full’ for a full-canvas overlay over a fullscreen camera. box {x,y,width,height} in design pixels places it elsewhere. Use it in place of an upload outside the Studio.
| Parameter | Type | Required | Description |
|---|---|---|---|
file_content | string | yes | The image, base64. |
file_name | string | yes | A name for the file; the extension comes from the bytes. |
slide_id | string | no | Place the image on this slide. |
alt | string | no | Alt text for the box (default: the file name). |
fit | enum | no | Default ‘contain’. One of: contain, cover, fill. |
box | object | no | Design pixels. Default: the whole canvas. |
list_project_versions
Section titled “list_project_versions”List Project Versions
List the versions of a Lesson Studio project, ordered by version_number descending (current/highest first).
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
bump_project_version
Section titled “bump_project_version”Bump Project Version
Create a new version of a Lesson Studio project (version_number = max existing + 1, starting at 1). Subsequent comments are stamped to this new version. Returns the created version row.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project to bump |
label | string | no | Optional human label for this version (e.g. “after canvas review pass 1”) |
create_slide_comment
Section titled “create_slide_comment”Create Slide Comment
Create a review comment anchored to a slide in a Lesson Studio project (Course Canvas comments). Stamped with the project current version; version 1 is auto-created if none exists.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
body | string | yes | The comment text (change request / note) |
slide_id | string | no | UUID of the slide the comment targets |
composition_id | string | no | UUID of the composition (for reference / canvas grouping) |
anchor_x | number | no | Optional Figma-style pin X coordinate on the canvas |
anchor_y | number | no | Optional Figma-style pin Y coordinate on the canvas |
list_slide_comments
Section titled “list_slide_comments”List Slide Comments
List Course Canvas comments of a project (newest first), optionally filtered by version and/or resolved state. Enriched with composition title + slide order for reference.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
project_version_id | string | no | Filter to comments stamped with this version |
resolved | boolean | no | Filter by resolved state (true/false) |
update_slide_comment
Section titled “update_slide_comment”Update Slide Comment
Update a Course Canvas comment body and/or its resolved state.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the comment |
body | string | no | New comment text |
resolved | boolean | no | Mark resolved (true) or reopen (false) |
delete_slide_comment
Section titled “delete_slide_comment”Delete Slide Comment
Delete a Course Canvas comment by id.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | UUID of the comment to delete |
get_version_comments
Section titled “get_version_comments”Get Version Comments (clipboard text)
Get all comments for a project version as a clipboard-ready text block, one comment per line prefixed with project/composition/slide references. Defaults to the project current version. Use for the “copy all comments → paste to Claude” flow.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | yes | UUID of the project |
project_version_id | string | no | UUID of the version. If omitted, the current (max version_number) is used. |
expire_stale_studio_exports
Section titled “expire_stale_studio_exports”Expire Stale Lesson Studio Exports
Find Lesson Studio video exports stuck in a non-terminal state (pending/processing) older than N hours and mark them failed with an explanatory error_message, so an abandoned export stops looking like it is still running. Defaults to a DRY RUN — pass dry_run: false to actually write. RLS-scoped to the caller’s own projects.
| Parameter | Type | Required | Description |
|---|---|---|---|
older_than_hours | number | no | Only expire exports whose started_at is at least this many hours ago (default 6, minimum 1). Keep this comfortably above your longest legitimate render. |
project_id | string | no | Restrict the sweep to a single project UUID. Omit to sweep all of the caller’s projects. |
statuses | string[] | no | Which non-terminal statuses to sweep (default [“pending”,“processing”]). |
reason | string | no | Message written to error_message. Defaults to a user-facing explanation. |
limit | number | no | Maximum number of rows to expire in one call (safety cap). |
dry_run | boolean | no | When true (the DEFAULT), only report what WOULD be expired without writing. |
start_composition_export
Section titled “start_composition_export”Export a Lesson Studio composition to MP4
Render a whole composition into an MP4, server-side. Returns as soon as the job is accepted — rendering takes minutes, so this does NOT wait: poll get_studio_export with the returned export_id until it is completed, then pass that export to register_export_as_media to get a media_id the post calendar accepts. Recording a webcam over the deck is still done in the Lesson Studio UI; this exports what is there. Set variant to mobile for a 9:16 reel/short/story cut; omit it to follow the project’s own orientation.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | The composition to render (from list_compositions). |
variant | enum | no | Aspect to cut to: desktop is landscape 16:9 (YouTube), mobile is portrait 9:16 (Instagram reels, YouTube shorts, stories). Omit to follow the project orientation (set at create_studio_project, changeable with update_studio_project). Use mobile to cut a portrait video out of a landscape project without changing it. One of: desktop, mobile. |
resolution | enum | no | Output size. Omit for the render service default (1080p). One of: 720p, 1080p, 4k. |
fps | number | no | Output frame rate. Omit for the render service default. |
get_studio_export
Section titled “get_studio_export”Get one Lesson Studio export
Read one export and, while it is still rendering, ask the render service where it is and write the answer back to the row. Poll this after start_composition_export until completed, then pass the export id to register_export_as_media. A failed export carries the real reason, not a generic one.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | The export to read (from start_composition_export). |
list_studio_exports
Section titled “list_studio_exports”List Lesson Studio Exports
List the rendered video exports (MP4) of the caller’s Lesson Studio projects, newest first. Defaults to completed exports only. Use the returned export id with register_export_as_media to turn a video into a media_id the post calendar accepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
project_id | string | no | Restrict to one project. |
status | enum | no | Export status to match (default completed). all lists every state. One of: completed, failed, pending, processing, all. |
limit | number | no | Maximum rows (default 20, max 100). |
register_export_as_media
Section titled “register_export_as_media”Register a Lesson Studio Export as Post Media
Make a completed Lesson Studio export (MP4) usable by the post calendar: creates a public.media row pointing at the exported file and returns the media_id plus the stable media.devfellowship.com/<id> link. Pass that media_id to draft_post_for_review on the campaigns MCP. Idempotent — the same export returns the same media_id. Only exports stored in the DevFellowship bucket can be registered.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | An export id from list_studio_exports (status must be completed). |
name | string | no | Display name for the media row. Defaults to the export file name. |
split_export_for_stories
Section titled “split_export_for_stories”Split a Lesson Studio export into Instagram Story parts
Cut a completed export (MP4) into ordered Story parts of at most 60 s each, and register each part as media. The cuts follow the slide changes when the render service knows them, else scene changes. Returns the parts in order, each with a media_id — pass those media ids, in order, to the campaigns Story sequence. Waits up to ~45 s; if the split is still running, it returns split_job_id — then poll get_story_split with it.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | A completed export (from get_studio_export or list_studio_exports). |
max_segment_s | number | no | Longest part in seconds (6–60). Omit for the render service default (59). |
check_studio_export
Section titled “check_studio_export”Check a Lesson Studio export
A machine review of a COMPLETED export (dfl-render /media/export-check): the duration (and the gap to expected_duration_s), the frame size, black-frame ranges, JPEG frames just after each slide starts, at its middle and just before it ends (slides from the render job), and a caption-over-face flag per mid-slide frame (one vision-LLM pass; face_check false skips it). flags lists what a reviewer must look at first. Waits ~50 s; if the check still runs it returns check_job_id — then call get_export_check.
| Parameter | Type | Required | Description |
|---|---|---|---|
export_id | string | yes | A completed export (get_studio_export / list_studio_exports). |
expected_duration_s | number | no | The length you expect, seconds. |
face_check | boolean | no | Default true: the caption/face overlap pass. |
get_export_check
Section titled “get_export_check”Get an export check
Read an export check that check_studio_export started (the same answer when it is done).
| Parameter | Type | Required | Description |
|---|---|---|---|
check_job_id | string | yes | The check_job_id check_studio_export returned. |
get_story_split
Section titled “get_story_split”Get a Story split job
Read a Story split job that split_export_for_stories started. While it runs, this returns its status — call it again. When it is done, it registers each part as media and returns the parts in order with their media ids (the same answer split_export_for_stories gives). Safe to call again: a part keeps its media_id.
| Parameter | Type | Required | Description |
|---|---|---|---|
split_job_id | string | yes | The split_job_id that split_export_for_stories returned. |
list_templates
Section titled “list_templates”List Templates
List all slide templates available in dfl-slide-templates (reads registry.json from GitHub).
Takes no parameters.
search_templates
Section titled “search_templates”Search Templates
Find the best slide templates for an intent. Returns a RANKED shortlist (top-N) instead of the full list — give a natural-language intent (e.g. “compare two options head-to-head”, “mostrar uma captura de tela”, “one big hero number”) and get back the templates whose registry “when_to_use”/tags/media_profile best match, each with a relevance score and a one-line reason. Use this to PICK a template, then call get_template(id) for its slots. Deterministic + read-only (no auth needed).
| Parameter | Type | Required | Description |
|---|---|---|---|
intent | string | yes | Natural-language description of what the slide should do (English or Portuguese), e.g. “comparar duas opções”, “show a screenshot”, “single big statistic”. |
limit | number | no | Max templates to return in the shortlist (default 5). |
canvas | string | no | Only templates that declare this design canvas (e.g. “social-portrait”). Omit for no canvas filter. |
project_id | string | no | Filter by this project’s canvas (its orientation). Ignored when canvas is given. |
get_template
Section titled “get_template”Get Template
Fetch all source files (HTML landscape, HTML portrait, CSS landscape, CSS portrait, config YAML) for a specific template.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Template ID (e.g. “title”, “bullet-list”, “two-column”) |
update_template
Section titled “update_template”Update Template
Open a ready-to-merge PR in dfl-slide-templates with updated HTML/CSS and optional config YAML for a template. All provided fields are written in a single commit. Returns the PR URL. registry.json is edited as a MERGE: any registry_* field you omit keeps its current value, so a version bump can no longer erase the discoverability metadata search_templates ranks on. Pass null to delete a field. CREATING a template (an id not yet in registry.json) requires registry_version, registry_name, registry_when_to_use, registry_media_profile and registry_tags, so the new template is findable from the moment it merges.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Template ID to update (e.g. “title”) |
description | string | yes | Human-readable description of the change (used as PR/commit title suffix) |
html_landscape | string | yes | New landscape.html content |
css_landscape | string | yes | New landscape.css content |
html_portrait | string | yes | New portrait.html content |
css_portrait | string | yes | New portrait.css content |
config_yaml | string | no | New config.yaml content (optional — skip to leave unchanged) |
registry_version | string | no | Bump the registry version (e.g. “1.1.0”) — omit to leave unchanged. REQUIRED when creating a new template. |
registry_category | string | no | Update the registry category (content | layout | data | …) — omit to leave unchanged. Defaults to “content” on create only. |
registry_name | string | no | Human display name, e.g. “Title Slide”. search_templates ranks on it. Omit to leave unchanged; null to delete. REQUIRED when creating. |
registry_when_to_use | string | no | When an authoring agent SHOULD reach for this template — the highest-signal ranking field. Omit to leave unchanged; null to delete. REQUIRED when creating. |
registry_avoid_when | string | no | Anti-patterns: when NOT to use this template. Omit to leave unchanged; null to delete. |
registry_media_profile | enum | no | What kind of content this template is shaped for. Omit to leave unchanged; null to delete. REQUIRED when creating. One of: text-heavy, balanced, image-first, image-only, video, data, code, diagram. |
registry_text_density | enum | no | How much copy the layout carries. Omit to leave unchanged; null to delete. One of: none, low, medium, high. |
registry_layout | string | no | One-line shape description, e.g. “centered big headline + subtitle, no media”. Omit to leave unchanged; null to delete. |
registry_tags | string[] | no | Free keywords for matching — the heaviest-weighted ranking field. Replaces the whole list (never merged element-wise). Omit to leave unchanged; null to delete. REQUIRED when creating. |
list_themes
Section titled “list_themes”List Themes
List the CSS themes that actually exist in dfl-slide-templates at the pinned revision. The set is DERIVED from the repo (registry.json themes[]), so a theme added upstream appears here with no code change. Each entry reports whether its stylesheet resolved, plus its display name and light/dark mode when declared. The response names the commit it was read from and flags a degraded read.
Takes no parameters.
get_theme
Section titled “get_theme”Get Theme
Fetch the CSS source for a specific theme.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Theme ID as reported by list_themes (e.g. “default”, “devfellowship”, “itera”, “revera”) |
update_theme
Section titled “update_theme”Update Theme
Open a ready-to-merge PR in dfl-slide-templates with updated CSS for a theme. Returns the PR URL. The PR also REGISTERS the theme in registry.json’s themes[] — the array themes_doc calls the source of truth and list_themes reads — so a theme published here is discoverable instead of being a stylesheet nothing lists. Registering a NEW theme requires theme_name and theme_mode: neither a brand’s display name nor its light/dark mode can be inferred, and a guess would become the source of truth. A new theme also needs its forbidden-colour contract in scripts/theme.config.json, or lint:css fails — that file is human-gated, so a brand-new theme cannot merge unattended.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Theme ID to update, as reported by list_themes (e.g. “devfellowship”, “itera”) |
description | string | yes | Human-readable description of the change (used as PR/commit title suffix) |
css | string | yes | New CSS content for the theme file |
theme_name | string | no | Display name for the registry themes[] entry, e.g. “Itera”. Omit to leave an existing theme unchanged. REQUIRED when registering a new theme. |
theme_mode | enum | no | Advisory light/dark hint for the host chrome around the slide (the slide always paints its own surface). Omit to leave an existing theme unchanged. REQUIRED when registering a new theme. One of: light, dark. |
render_slide_image
Section titled “render_slide_image”Render Slide Image
Render one stored slide to a PNG and return its URL. Wraps the deterministic headless capture in dfl-render with the CALLING user’s own credentials. Returns the dfl-slide-templates commit revision the template was read at AND a composition fingerprint of the deck state the derived slide index came from — together those two reproduce the render. canvas is optional; a canvas the template does not declare is REFUSED and no image is produced (never a fallback to the nearest canvas).
| Parameter | Type | Required | Description |
|---|---|---|---|
slide_id | string | yes | UUID of the slide to render |
canvas | string | no | Design canvas id, e.g. “landscape”, “portrait”, “social-portrait”. Omit to use the parent project’s orientation. Validated against the declared canvas contract AND against the canvases the slide’s template ships; an undeclared canvas errors and produces no image. |
render_composition_images
Section titled “render_composition_images”Render Composition Images
Render every surviving slide of a composition to a PNG and return the URLs in order_index order — the batch form of render_slide_image, and the tool a carousel needs. Soft-deleted slides are skipped. Captures run with bounded concurrency. Returns the dfl-slide-templates commit revision and ONE deck-level composition fingerprint shared by every image. canvas is optional; a canvas any template in the deck does not declare is REFUSED before anything is captured.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the composition to render |
canvas | string | no | Design canvas id, e.g. “landscape”, “portrait”, “social-portrait”. Omit to use each slide’s parent project orientation. Every distinct template in the deck must declare it, or the whole call errors and no image is produced. |
capture_cover_slide
Section titled “capture_cover_slide”Capture Cover Slide
Render the composition’s cover slide (template_data.is_cover, set by set_cover_slide) to a PNG for the deck’s video thumbnail, and return its image_url. Errors clearly when the composition has no cover slide. This does NOT write projects.thumbnail_url — no tool in dfl-mcp-studio does yet — so persist the returned image_url yourself once such a tool exists.
| Parameter | Type | Required | Description |
|---|---|---|---|
composition_id | string | yes | UUID of the composition whose cover slide to capture |
generate_lesson_from_profile
Section titled “generate_lesson_from_profile”Generate Lesson From Tutor Profile
Author a NEW lesson topic in a specific tutor’s proven pedagogical style. Loads that tutor’s stored pedagogy profile (lms.tutor_profiles) and returns a Studio-importable GENERATION SPEC: a typology-biased slide plan (template picks ranked by the professor’s dominant lesson-type via search_templates’ ranker), the professor’s SIGNATURE as few-shot exemplars (verbatim metaphors/openers/cotidiano anchors), and amplify/reduce directives (amplify high-consistency moves, reduce vícios). Deterministic — the tool does NOT call an LLM; the calling agent expands each suggested slide into content, few-shotting the exemplars and honoring the directives. The rubric (HALF A) is never used for generation (de-circularization).
| Parameter | Type | Required | Description |
|---|---|---|---|
tutor_id | string | yes | work.members.id of the tutor whose stored pedagogy profile to author in (matches lms.tutor_profiles.member_id). |
topic | string | yes | The new lesson topic to author in that tutor’s style, e.g. “Introdução a APIs REST”. |
version | string | no | Profile version to load. Defaults to the newest (most recently updated) profile for the tutor. |
n_slides | number | no | Target slide count / number of template picks (default 8). |
canvas | string | no | Only pick templates that declare this design canvas (e.g. “social-portrait”). |
project_id | string | no | Pick only templates that fit this project’s canvas. Ignored when canvas is given. |