@contenthero/mcp 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/format.d.ts +16 -3
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +71 -11
- package/dist/format.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +149 -10
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
package/dist/server.js
CHANGED
|
@@ -37,7 +37,7 @@ import { z } from 'zod';
|
|
|
37
37
|
import { GenerationTimeoutError, pendingOutputId, } from '@contenthero/sdk';
|
|
38
38
|
import { getClient as defaultGetClient } from './client.js';
|
|
39
39
|
import { resolveModelEnums, BOARD_TYPES, BOARD_TYPE_GUIDANCE, IMAGE_MODEL_GUIDANCE, VIDEO_MODEL_GUIDANCE, AUDIO_MODEL_GUIDANCE, EDIT_AUDIO_MODEL_GUIDANCE, UPSCALE_MODEL_GUIDANCE, LIP_SYNC_MODEL_GUIDANCE, } from './models.js';
|
|
40
|
-
import { audioResult, avatarListResult, avatarResult, avatarPendingResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
|
|
40
|
+
import { audioResult, avatarListResult, avatarResult, avatarPendingResult, balanceResult, brandKitListResult, brandKitResult, brandKnowledgeListResult, brandKnowledgeDetailResult, brandKnowledgeSearchResult, brandKnowledgeItemResult, completedResult, connectedAccountListResult, connectedAccountResult, costResult, accountDetailResult, inspirationContentResult, mediaListResult, mediaSearchResult, folderListResult, folderContentsResult, mediaBatchResult, mediaUploadResult, importedMediaResult, uploadedMediaResult, tagListResult, tagResult, tagDeletedResult, modelListResult, modelResult, platformListResult, platformResult, elementListResult, elementResult, elementDeletedResult, errorResult, generationBatchResult, outlierListResult, enhanceClipsResult, pendingResult, stageListResult, stageResult, stageDeletedResult, spaceDeletedResult, spaceListResult, spaceResult, cardListResult, cardResult, postSummaryResult, publishResult, statusActionResult, editorOpsResult, text, projectDetailResult, liveContextResult, projectListResult, projectCreatedResult, projectDeletedResult, layerTypesResult, timelineTypesResult, editorTranscriptResult, exportJobResult, exportFormatsResult, trackedAccountListResult, transcriptResult, voiceListResult, voiceResult, } from './format.js';
|
|
41
41
|
/** Platforms a card or one of its posts may target. */
|
|
42
42
|
const POST_PLATFORMS = [
|
|
43
43
|
'youtube',
|
|
@@ -207,6 +207,36 @@ function buildReferences(parts) {
|
|
|
207
207
|
*/
|
|
208
208
|
export function registerTools(server, opts) {
|
|
209
209
|
const { getClient, models } = opts;
|
|
210
|
+
/**
|
|
211
|
+
* 🚨🚨 **EVERY TOOL'S INPUT IS STRICT. AN UNDECLARED PARAMETER IS A 400, NEVER A SILENT DROP.**
|
|
212
|
+
*
|
|
213
|
+
* Zod object schemas STRIP unknown keys by default, and the SDK builds one from each `inputSchema` shape.
|
|
214
|
+
* So before this wrapper, all 85 tools accepted any parameter they did not declare, discarded it, and ran
|
|
215
|
+
* on whatever survived. The failure is invisible by construction: the caller gets a success.
|
|
216
|
+
*
|
|
217
|
+
* ⚠️ **THE MEASURED CASE, 2026-09-08.** `update_card` does not declare `ops`. A call passing `ops` had it
|
|
218
|
+
* stripped, leaving only `cardId`, and returned **"Updated: 01.3 Design Your Character"** having written
|
|
219
|
+
* nothing. A second call passing the nonsense op `__probe__` did the same. An agent running a batch of
|
|
220
|
+
* partial edits would collect a full set of success messages and zero writes.
|
|
221
|
+
*
|
|
222
|
+
* ⭐⭐ **WRAPPED HERE RATHER THAN AT 85 CALL SITES, AND THAT IS THE POINT.** A rule every registration must
|
|
223
|
+
* remember is a rule that holds until someone adds the 86th tool. Same move as `withSpineRegistration`
|
|
224
|
+
* wrapping the Supabase client instead of asking 73 upload sites to declare an owner.
|
|
225
|
+
*
|
|
226
|
+
* ⚠️ **NESTED `.passthrough()` SURVIVES, DELIBERATELY.** `update_timeline` and `update_canvas` declare
|
|
227
|
+
* `ops: z.array(z.object({ op: z.string() }).passthrough())` because a timeline op carries a different
|
|
228
|
+
* shape per op type. Strictness here applies to the TOP-LEVEL argument object only, so those keep taking
|
|
229
|
+
* varied op payloads while still rejecting an undeclared top-level parameter.
|
|
230
|
+
*/
|
|
231
|
+
const rawRegisterTool = server.registerTool.bind(server);
|
|
232
|
+
server.registerTool = ((name, config, cb) => {
|
|
233
|
+
const shape = config.inputSchema;
|
|
234
|
+
// A tool with no inputs, or one that already passed a built schema, is left exactly as it was.
|
|
235
|
+
const strict = shape && typeof shape === 'object' && !(shape instanceof z.ZodType)
|
|
236
|
+
? z.object(shape).strict()
|
|
237
|
+
: shape;
|
|
238
|
+
return rawRegisterTool(name, { ...config, inputSchema: strict }, cb);
|
|
239
|
+
});
|
|
210
240
|
/**
|
|
211
241
|
* ⚠️ THESE SHAPES ARE DECLARED, NOT LEFT AS `z.unknown()`. An array of unknown serialises to
|
|
212
242
|
* `{"type":"array","items":{}}`, which tells a client NOTHING about what may go inside it. The server
|
|
@@ -816,6 +846,10 @@ export function registerTools(server, opts) {
|
|
|
816
846
|
imageUrls: z
|
|
817
847
|
.array(z.string())
|
|
818
848
|
.describe('Images the account already owns, as URLs: an upload, a creation, an editor export, or another avatar look. Anything not owned by this account is skipped rather than failing the call.'),
|
|
849
|
+
name: z
|
|
850
|
+
.string()
|
|
851
|
+
.optional()
|
|
852
|
+
.describe("What to call the look. Applied to every image in this op. Omit and it stays unnamed, displaying by its source label ('from_media'), which is rarely what you want for a look you will pick from a list later."),
|
|
819
853
|
}),
|
|
820
854
|
z.object({
|
|
821
855
|
op: z.literal('remove_look'),
|
|
@@ -1710,7 +1744,10 @@ export function registerTools(server, opts) {
|
|
|
1710
1744
|
.string()
|
|
1711
1745
|
.optional()
|
|
1712
1746
|
.describe("Which space's board to list, from list_spaces. Omit ONLY when you mean the account's default space; omitting it does not search every board."),
|
|
1713
|
-
|
|
1747
|
+
archived: z
|
|
1748
|
+
.boolean()
|
|
1749
|
+
.optional()
|
|
1750
|
+
.describe('Only ARCHIVED cards. Archived cards are excluded by default, matching the board.'),
|
|
1714
1751
|
platform: z.enum(POST_PLATFORMS).optional().describe('Filter by the post platform.'),
|
|
1715
1752
|
stage: z.string().optional().describe('Filter by a stage id, slug, or name. Resolved within the chosen space.'),
|
|
1716
1753
|
search: z.string().optional().describe('Case-insensitive title search, scoped to the chosen space.'),
|
|
@@ -1722,7 +1759,7 @@ export function registerTools(server, opts) {
|
|
|
1722
1759
|
const client = await getClient(extra);
|
|
1723
1760
|
return cardListResult(await client.listCards({
|
|
1724
1761
|
spaceId: args.spaceId,
|
|
1725
|
-
|
|
1762
|
+
archived: args.archived,
|
|
1726
1763
|
platform: args.platform,
|
|
1727
1764
|
stage: args.stage,
|
|
1728
1765
|
search: args.search,
|
|
@@ -1873,14 +1910,116 @@ export function registerTools(server, opts) {
|
|
|
1873
1910
|
return errorResult(err);
|
|
1874
1911
|
}
|
|
1875
1912
|
});
|
|
1913
|
+
// -- create_stage ---------------------------------------------------------
|
|
1914
|
+
server.registerTool('create_stage', {
|
|
1915
|
+
title: 'Create Stage',
|
|
1916
|
+
annotations: WRITE,
|
|
1917
|
+
description: "Create a stage (a column on one board). ⚠️ STAGES ARE PER-SPACE: without spaceId this creates on the account's DEFAULT board, which is rarely what you want once more than one space exists, so call list_spaces first. The slug is DERIVED from the name and is not settable; a board cannot hold two columns whose names produce the same slug and the server refuses the second rather than renaming it for you. Place the column with afterId/beforeId, or omit both to put it at the end. Requires the planner:write scope.",
|
|
1918
|
+
inputSchema: {
|
|
1919
|
+
name: z.string().describe('The column name, for example "In Review". Must contain a letter or number.'),
|
|
1920
|
+
spaceId: z
|
|
1921
|
+
.string()
|
|
1922
|
+
.optional()
|
|
1923
|
+
.describe("Which board, from list_spaces. Omit only when you mean the account's default space."),
|
|
1924
|
+
color: z.string().optional().describe('A hex color such as "#3B82F6".'),
|
|
1925
|
+
afterId: z.string().optional().describe('Put the new column immediately after this stage id.'),
|
|
1926
|
+
beforeId: z.string().optional().describe('Put the new column immediately before this stage id.'),
|
|
1927
|
+
},
|
|
1928
|
+
}, async (args, extra) => {
|
|
1929
|
+
try {
|
|
1930
|
+
const client = await getClient(extra);
|
|
1931
|
+
return stageResult(await client.createStage({
|
|
1932
|
+
name: args.name,
|
|
1933
|
+
spaceId: args.spaceId,
|
|
1934
|
+
color: args.color,
|
|
1935
|
+
afterId: args.afterId,
|
|
1936
|
+
beforeId: args.beforeId,
|
|
1937
|
+
}));
|
|
1938
|
+
}
|
|
1939
|
+
catch (err) {
|
|
1940
|
+
return errorResult(err);
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
// -- update_stage ---------------------------------------------------------
|
|
1944
|
+
server.registerTool('update_stage', {
|
|
1945
|
+
title: 'Update Stage',
|
|
1946
|
+
annotations: WRITE,
|
|
1947
|
+
description: "Rename, recolor or move one stage. This is a PATCH: a field you omit is left alone. ⚠️ spaceId is REQUIRED, because a stage id alone does not tell the server which board you mean and guessing the wrong one silently changes nothing. Renaming re-derives the slug, so renaming a column away from 'Published' also stops publishing auto-moving cards into it; a rename that collides with another column on the same board is refused. Moving names NEIGHBORS, not a position: pass afterId or beforeId. Requires the planner:write scope.",
|
|
1948
|
+
inputSchema: {
|
|
1949
|
+
stageId: z.string().describe('The stage id to update, from list_stages.'),
|
|
1950
|
+
spaceId: z.string().describe('The board this stage is on, from list_stages or list_spaces.'),
|
|
1951
|
+
name: z.string().optional().describe('A new name. The slug follows it automatically.'),
|
|
1952
|
+
color: z.string().optional().describe('A new hex color such as "#3B82F6".'),
|
|
1953
|
+
afterId: z
|
|
1954
|
+
.string()
|
|
1955
|
+
.optional()
|
|
1956
|
+
.describe('Move it immediately after this stage id. Use the empty string to move it to the far left.'),
|
|
1957
|
+
beforeId: z
|
|
1958
|
+
.string()
|
|
1959
|
+
.optional()
|
|
1960
|
+
.describe('Move it immediately before this stage id. Use the empty string to move it to the far right.'),
|
|
1961
|
+
},
|
|
1962
|
+
}, async (args, extra) => {
|
|
1963
|
+
try {
|
|
1964
|
+
const client = await getClient(extra);
|
|
1965
|
+
/*
|
|
1966
|
+
An empty string is how a tool caller says "the edge": JSON Schema cannot distinguish an
|
|
1967
|
+
omitted string from an explicit null in a plain string field, and the two mean opposite
|
|
1968
|
+
things here. Omitted means "do not move it"; null means "move it to the end of the board".
|
|
1969
|
+
`update_space` resolves the same ambiguity the same way for coverUrl.
|
|
1970
|
+
*/
|
|
1971
|
+
const edge = (v) => (v === undefined ? undefined : v === '' ? null : v);
|
|
1972
|
+
const { stage, respaced } = await client.updateStage(args.stageId, {
|
|
1973
|
+
spaceId: args.spaceId,
|
|
1974
|
+
name: args.name,
|
|
1975
|
+
color: args.color,
|
|
1976
|
+
afterId: edge(args.afterId),
|
|
1977
|
+
beforeId: edge(args.beforeId),
|
|
1978
|
+
});
|
|
1979
|
+
return stageResult(stage, respaced);
|
|
1980
|
+
}
|
|
1981
|
+
catch (err) {
|
|
1982
|
+
return errorResult(err);
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
// -- delete_stage ---------------------------------------------------------
|
|
1986
|
+
server.registerTool('delete_stage', {
|
|
1987
|
+
title: 'Delete Stage',
|
|
1988
|
+
annotations: WRITE,
|
|
1989
|
+
description: 'Delete a stage and move its cards to another column. The server REFUSES a column that still holds cards when you name no targetStageId, and tells you how many there are: cards are never destroyed by deleting a column, and the delete and the reassignment happen in one transaction. Returns the board that is left. Requires the planner:write scope.',
|
|
1990
|
+
inputSchema: {
|
|
1991
|
+
stageId: z.string().describe('The stage id to delete, from list_stages.'),
|
|
1992
|
+
spaceId: z.string().describe('The board this stage is on.'),
|
|
1993
|
+
targetStageId: z
|
|
1994
|
+
.string()
|
|
1995
|
+
.optional()
|
|
1996
|
+
.describe('Where this column\'s cards should go. Required unless the column is empty.'),
|
|
1997
|
+
},
|
|
1998
|
+
}, async (args, extra) => {
|
|
1999
|
+
try {
|
|
2000
|
+
const client = await getClient(extra);
|
|
2001
|
+
const result = await client.deleteStage(args.stageId, {
|
|
2002
|
+
spaceId: args.spaceId,
|
|
2003
|
+
targetStageId: args.targetStageId ?? null,
|
|
2004
|
+
});
|
|
2005
|
+
return stageDeletedResult(result.id, result.movedCards, result.stages);
|
|
2006
|
+
}
|
|
2007
|
+
catch (err) {
|
|
2008
|
+
return errorResult(err);
|
|
2009
|
+
}
|
|
2010
|
+
});
|
|
1876
2011
|
// -- create_card ----------------------------------------------------------
|
|
1877
2012
|
server.registerTool('create_card', {
|
|
1878
2013
|
title: 'Create Card',
|
|
1879
2014
|
annotations: WRITE,
|
|
1880
|
-
description: "Create a card, the container in the content pipeline. A card holds the work (title, script, notes, cover) and the posts that publish it. Attach posts and media by passing `posts` and `assets` to update_card, then publish with publish_post. `stage` accepts a stage id/slug/name
|
|
2015
|
+
description: "Create a card, the container in the content pipeline. A card holds the work (title, script, notes, cover) and the posts that publish it. Attach posts and media by passing `posts` and `assets` to update_card, then publish with publish_post. ⚠️ WITHOUT spaceId THIS LANDS ON THE ACCOUNT'S DEFAULT BOARD, which is rarely what you want once more than one space exists, so call list_spaces first. `stage` accepts a stage id/slug/name and DECIDES the space when it is an id; a spaceId that disagrees with it is rejected rather than guessed. Requires a key with the planner:write scope.",
|
|
1881
2016
|
inputSchema: {
|
|
1882
2017
|
title: z.string().describe('Post title (required).'),
|
|
1883
2018
|
platform: z.enum(POST_PLATFORMS).describe('Primary platform for the post.'),
|
|
2019
|
+
spaceId: z
|
|
2020
|
+
.string()
|
|
2021
|
+
.optional()
|
|
2022
|
+
.describe("Which board to create the card on, from list_spaces. Omit only when you mean the account's default space."),
|
|
1884
2023
|
stage: z.string().optional().describe('Pipeline stage id, slug, or name. Defaults to the first stage.'),
|
|
1885
2024
|
coverUrl: z.string().optional().describe('Public URL for the post cover (the card thumbnail).'),
|
|
1886
2025
|
coverOutputId: z
|
|
@@ -1898,6 +2037,7 @@ export function registerTools(server, opts) {
|
|
|
1898
2037
|
return postSummaryResult(await client.createCard({
|
|
1899
2038
|
title: args.title,
|
|
1900
2039
|
platform: args.platform,
|
|
2040
|
+
spaceId: args.spaceId,
|
|
1901
2041
|
stage: args.stage,
|
|
1902
2042
|
coverUrl: args.coverUrl,
|
|
1903
2043
|
coverOutputId: args.coverOutputId,
|
|
@@ -1912,12 +2052,11 @@ export function registerTools(server, opts) {
|
|
|
1912
2052
|
server.registerTool('update_card', {
|
|
1913
2053
|
title: 'Update Card',
|
|
1914
2054
|
annotations: WRITE,
|
|
1915
|
-
description: "Update a card: its fields (title, description, script, notes,
|
|
2055
|
+
description: "Update a card: its fields (title, description, script, notes, platform, cover, stage), its POSTS (one per platform, which is how it publishes), its ASSETS (the media on it, in order), and its SCHEDULE. posts and assets are DECLARATIVE: pass the WHOLE set, because anything you leave out is removed. Posts key on platform. Assets key on id, and THE ARRAY ORDER IS THE carousel ORDER, so reordering is just sending the same ids in a different order; keep an existing asset by id, add a new one by assetUrl or outputId. scheduledAt sets the time on the card AND every post (pass null to clear); give a post its own scheduledAt to override it for that platform. To publish NOW, use publish_post. Pass spaceId to MOVE the card to another space; without a stage it lands in the target space's stage whose slug matches its current one, or that space's first stage. Pass cardIds to update several cards at once, which crossed with spaceId is how a selection moves in one call; fields that describe ONE card (title, notes, script, cover) still need exactly one. Requires the planner:write scope.",
|
|
1916
2056
|
inputSchema: {
|
|
1917
2057
|
cardId: z.string().describe('The card id.'),
|
|
1918
2058
|
title: z.string().optional(),
|
|
1919
2059
|
platform: z.enum(POST_PLATFORMS).optional(),
|
|
1920
|
-
status: z.enum(['draft', 'active', 'completed', 'archived']).optional(),
|
|
1921
2060
|
stage: z.string().optional().describe('Move the post to this stage (id, slug, or name).'),
|
|
1922
2061
|
spaceId: z
|
|
1923
2062
|
.string()
|
|
@@ -2256,7 +2395,7 @@ export function registerTools(server, opts) {
|
|
|
2256
2395
|
server.registerTool('archive', {
|
|
2257
2396
|
title: 'Archive',
|
|
2258
2397
|
annotations: WRITE,
|
|
2259
|
-
description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (card, brand_kit, brand_kit_section, project, space). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving a
|
|
2398
|
+
description: "Archive or UNarchive an asset: pass archived:false to restore it (default true). ContentHero never hard-deletes, so this is always reversible. For a top-level asset, pass assetType + id (card, brand_kit, brand_kit_section, project, space). To archive a single studio media variation, pass the output id + variationIndex (1-based) and omit assetType. Archiving is a timestamp and nothing else is touched, so a scheduled card restores as scheduled. Requires the favorites:write scope. Idempotent in both directions.",
|
|
2260
2399
|
inputSchema: {
|
|
2261
2400
|
assetType: z
|
|
2262
2401
|
.enum(['card', 'brand_kit', 'brand_kit_section', 'project', 'space'])
|
|
@@ -2518,12 +2657,12 @@ export function registerTools(server, opts) {
|
|
|
2518
2657
|
server.registerTool('export_project', {
|
|
2519
2658
|
title: 'Export Project',
|
|
2520
2659
|
annotations: WRITE,
|
|
2521
|
-
description: "Export (render) a project's saved composition to a downloadable file the user KEEPS: a permanent deliverable that counts against the user's storage. To preview or verify a frame or slide while editing, do NOT export; use get_context with render (ephemeral, stored nowhere). format 'mp4' works for both editor and canvas (a video render; may take a while). 'png' / 'jpg' work for both surfaces too: a canvas project renders one image per slide (multiple slides come back as a zip), while an editor project renders a single composited frame of the timeline (pick which frame with `frame`; defaults to frame 0). Canvas projects additionally support 'pdf' and 'pptx'.
|
|
2660
|
+
description: "Export (render) a project's saved composition to a downloadable file the user KEEPS: a permanent deliverable that counts against the user's storage. To preview or verify a frame or slide while editing, do NOT export; use get_context with render (ephemeral, stored nowhere). format 'mp4' works for both editor and canvas (a video render; may take a while). 'png' / 'jpg' work for both surfaces too: a canvas project renders one image per slide (multiple slides come back as a zip), while an editor project renders a single composited frame of the timeline (pick which frame with `frame`; defaults to frame 0). Canvas projects additionally support 'pdf' and 'pptx'. Resolution and watermark apply to EVERY format, not just mp4: a free account never exports above 720p and never removes the watermark, on any format or surface. `quality` is mp4 only. Returns the download URL when the render finishes in time, otherwise an exportId to poll with get_export. Requires the editor:write scope.",
|
|
2522
2661
|
inputSchema: {
|
|
2523
2662
|
projectId: z.string().describe('The project to export.'),
|
|
2524
2663
|
format: z.enum(['mp4', 'png', 'jpg', 'pdf', 'pptx']).optional().describe("Output format. Defaults to 'mp4'. mp4/png/jpg work for both surfaces (png/jpg on an editor project render one timeline frame); pdf/pptx are canvas-only."),
|
|
2525
|
-
resolution: z.enum(['480p', '720p', '1080p', '2k', '4k']).optional().describe("
|
|
2526
|
-
quality: z.enum(['low', 'recommended', 'high']).optional().describe('mp4 video
|
|
2664
|
+
resolution: z.enum(['480p', '720p', '1080p', '2k', '4k']).optional().describe("Output resolution, for EVERY format including stills. Defaults '720p' for an editor mp4 and the project's NATIVE size for a still or canvas mp4, in both cases clamped to your plan. 1080p and above are plan-gated: NAMING one above your plan is a 403, omitting one clamps instead."),
|
|
2665
|
+
quality: z.enum(['low', 'recommended', 'high']).optional().describe('mp4 ONLY: video bitrate. Meaningless for a still, which has no duration to spread bits over. Defaults recommended.'),
|
|
2527
2666
|
watermark: z.boolean().optional().describe('Keep the watermark. Defaults true; removing it is plan-gated.'),
|
|
2528
2667
|
frame: z.number().int().min(0).optional().describe('Editor still (png/jpg) only: which timeline frame to render. Clamped to the composition length. Defaults 0. Use the playhead frame from get_context to render exactly the frame the user is viewing.'),
|
|
2529
2668
|
},
|