@writepanda/mcp 1.285.0 → 1.287.0

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.
Files changed (2) hide show
  1. package/bin/server.mjs +132 -19
  2. package/package.json +1 -1
package/bin/server.mjs CHANGED
@@ -608,7 +608,7 @@ const MOTION_ELEMENT_PROPS = {
608
608
  content: {
609
609
  type: ["object", "string", "array"],
610
610
  description:
611
- 'keyword/chip/stamp/slam/behind/highlight { text } (or a bare string); count { value, to?, prefix?, suffix?, label?, decimals? } (value "20-30" = range); steps { items[], cues? (ms offsets from start) }; lowerThird { title, subtitle? (<=320 chars, wraps), look?: plate|plain|typewriter, typeSpeed? (typewriter pace, 5-80 chars/s, default ~22) }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path or "@speaker" (the speaker cut out of her shot), label?, twin? (mirrored pair, one each side) }; ticker { text (one story per line, <=2000 chars), label?, speed? (40-600 px/s) }; credits { title?, entries: [{ role?, name }] | "Role: Name" strings (<=200) }; crawl { title?, text (<=6000 chars), tilt? (0-80), turn? (-60..60) }. Creator effects (picture slots take a path or "@speaker" = the speaker\'s own live shot, the default): reelWall { images?, count? 4-24 } (tiles of her shot behind her); cardFan { images?, count? 3-9 } (card reveal: cards fan in an arc behind her head); orbit { images?, count? 2-6 } (cards circle her, layer around); polaroids { images?, count? 2-8, captions? } (scattered around her, layer around); wordWall { text, rows? 3-14, background? } (word repeated in rows behind her); neon { text, flicker? }; bloom { text } (colour bloom); scrapbook { text } (ransom-note letters); keycaps { text <=24 } (keys falling, pile at the bottom); productCard { title, subtitle?, price?, image?, button? ("Add to cart"), done? ("Added") }; scrollList { title?, items: (string | { title, subtitle?, image? })[] <=30, look?: playlist|feed } (phone list scrolling); splitScreen { panels? 2|3, images?, zooms? 1-4 } (three scenes). Any script, symbols and emoji draw.',
611
+ 'keyword/chip/stamp/slam/behind/highlight { text } (or a bare string); count { value, to?, prefix?, suffix?, label?, decimals? } (value "20-30" = range); steps { items[], cues? (ms offsets from start; omit = equal time per item, the last too; a cue in the last ~1.5 s is pulled in so its item still holds) }; lowerThird { title, subtitle? (<=320 chars, wraps), look?: plate|plain|typewriter, typeSpeed? (typewriter pace, 5-80 chars/s, default ~22) }; endCard { lines: [a, b] }; progress { label?, fromMs?, toMs? }; iconPop { emoji } | { icon }; frame { look: viewfinder | selection | corners, label? }; sticker { image: transparent PNG path or "@speaker" (the speaker cut out of her shot), label?, twin? (mirrored pair, one each side) }; ticker { text (one story per line, <=2000 chars), label?, speed? (40-600 px/s) }; credits { title?, entries: [{ role?, name }] | "Role: Name" strings (<=200) }; crawl { title?, text (<=6000 chars), tilt? (0-80), turn? (-60..60) }. Creator effects (picture slots take a path or "@speaker" = the speaker\'s own live shot, the default): reelWall { images?, count? 4-24 } (tiles of her shot behind her); cardFan { images?, count? 3-9 } (card reveal: cards fan in an arc behind her head); orbit { images?, count? 2-6 } (cards circle her, layer around); polaroids { images?, count? 2-8, captions? } (scattered around her, layer around); wordWall { text, rows? 3-14, background? } (word repeated in rows behind her); neon { text, flicker? }; bloom { text } (colour bloom); scrapbook { text } (ransom-note letters); keycaps { text <=24 } (keys falling, pile at the bottom); productCard { title, subtitle?, price?, image?, button? ("Add to cart"), done? ("Added") }; scrollList { title?, items: (string | { title, subtitle?, image? })[] <=30, look?: playlist|feed } (phone list scrolling); splitScreen { panels? 2|3, images?, zooms? 1-4 } (three scenes). Any script, symbols and emoji draw.',
612
612
  },
613
613
  atMs: { type: "number", description: "Start, edited ms. Or wordId." },
614
614
  wordId: {
@@ -621,7 +621,8 @@ const MOTION_ELEMENT_PROPS = {
621
621
  cueWordIds: {
622
622
  type: "array",
623
623
  items: { type: "string" },
624
- description: "steps: one word id per item.",
624
+ description:
625
+ "steps: one word id per item. End the element >= 1.5 s after the last one (a later cue is pulled in).",
625
626
  },
626
627
  style: {
627
628
  type: "object",
@@ -998,6 +999,7 @@ const TOOLS = [
998
999
  "marathi",
999
1000
  "gujarati",
1000
1001
  "punjabi",
1002
+ "turkish",
1001
1003
  ],
1002
1004
  },
1003
1005
  },
@@ -1416,7 +1418,7 @@ const TOOLS = [
1416
1418
  {
1417
1419
  name: "youtube_list_accounts",
1418
1420
  description:
1419
- "Connected YouTube accounts and channels (pick before export_publish_youtube). issues[] = accounts missing a Google permission (code youtube_permission_missing): relay message, follow howToFix (the user reconnects in Settings > YouTube, leaving every box ticked; you can't do the sign-in).",
1421
+ "Connected YouTube accounts and channels (pick before export_publish_youtube). Each channel's userLastPrivacy = what the user last picked in the app's Publish window (use only when they ask for their usual setting). issues[] = accounts missing a Google permission (code youtube_permission_missing): relay message, follow howToFix (the user reconnects in Settings > YouTube, leaving every box ticked; you can't do the sign-in).",
1420
1422
  inputSchema: { type: "object", properties: {} },
1421
1423
  command: "youtube.list-accounts",
1422
1424
  },
@@ -1434,7 +1436,7 @@ const TOOLS = [
1434
1436
  {
1435
1437
  name: "export_publish_youtube",
1436
1438
  description:
1437
- "Publish an export entry to YouTube. Slow. Check the entry's youtubeVideoId before republishing. channelId optional (uploads go to the account's own channel; omit when channels can't be listed). Error code youtube_permission_missing: relay message, follow howToFix. Error code daily_limit or youtube_quota_exceeded (with resetsAt): tell the user when it resets; never retry before resetsAt. Returns { videoUrl, channelId }.",
1439
+ "Publish an export entry to YouTube. Slow. Check the entry's youtubeVideoId before republishing. channelId optional (uploads go to the account's own channel; omit when channels can't be listed). Error code youtube_permission_missing: relay message, follow howToFix. Error code daily_limit or youtube_quota_exceeded (with resetsAt): tell the user when it resets; never retry before resetsAt. privacyStatus defaults to unlisted: public only when the user asked. A vertical or square export of 3 min or less becomes a YouTube Short (result short: true); pass addShortsTag to add #Shorts. Returns { videoUrl, channelId, short }.",
1438
1440
  inputSchema: {
1439
1441
  type: "object",
1440
1442
  properties: {
@@ -1447,6 +1449,10 @@ const TOOLS = [
1447
1449
  categoryId: { type: "string" },
1448
1450
  privacyStatus: { type: "string", enum: ["public", "unlisted", "private"] },
1449
1451
  setThumbnail: { type: "boolean" },
1452
+ addShortsTag: {
1453
+ type: "boolean",
1454
+ description: "Add #Shorts to the description when the export is a Short. Default false.",
1455
+ },
1450
1456
  subtitles: {
1451
1457
  type: "boolean",
1452
1458
  description:
@@ -1463,7 +1469,7 @@ const TOOLS = [
1463
1469
  {
1464
1470
  name: "social_channels",
1465
1471
  description:
1466
- "Social accounts connected in the active workspace (Instagram, TikTok, Facebook, LinkedIn, X) with ids, limits and per-post settings, plus each network's count.",
1472
+ "Social accounts connected in the active workspace (Instagram, TikTok, Facebook, LinkedIn, X) with ids, limits and per-post settings, plus each network's count. X accounts carry xPremium (25,000-char limit when true, else 280). YouTube and YouTube Shorts aren't here: use youtube_list_accounts + export_publish_youtube.",
1467
1473
  inputSchema: { type: "object", properties: {} },
1468
1474
  command: "social.channels",
1469
1475
  },
@@ -1490,6 +1496,20 @@ const TOOLS = [
1490
1496
  },
1491
1497
  command: "social.connect-status",
1492
1498
  },
1499
+ {
1500
+ name: "social_set_x_premium",
1501
+ description:
1502
+ "Mark a connected X account as X Premium (posts up to 25,000 chars) or standard (280). Only when the user says the account has Premium: X rejects long posts from other accounts. Returns the updated channel.",
1503
+ inputSchema: {
1504
+ type: "object",
1505
+ properties: {
1506
+ channelId: { type: "string", description: "The X account's id from social_channels." },
1507
+ premium: { type: "boolean" },
1508
+ },
1509
+ required: ["channelId", "premium"],
1510
+ },
1511
+ command: "social.set-x-premium",
1512
+ },
1493
1513
  {
1494
1514
  name: "social_disconnect",
1495
1515
  description:
@@ -1803,7 +1823,7 @@ const TOOLS = [
1803
1823
  {
1804
1824
  name: "project_edit_summary",
1805
1825
  description:
1806
- "What the edit contains now: lengths (editedDurationMs, sourceDurationMs, removedMs cut or sped up), counts (cuts, zooms, motionElements, graphics, transitions, brollBeats, mediaOverlays, captions { on, template }, audio { narration, music, soundEffects }, voiceEnhancedClips vs loudnessNormalizedClips, ...) and highlights[] (plain lines). Each recording counts once, so splits change neither source length nor cuts. Cheap; use it to report an editing pass instead of project_read.",
1826
+ "What the edit contains now: lengths (editedDurationMs, sourceDurationMs, removedMs cut or sped up), counts (cuts, zooms, motionElements, graphics, transitions, brollBeats, mediaOverlays, captions { on, template }, audio { narration, music, soundEffects }, audioTracks[{id, kind voice|music|sfx, basis}] (the same kind the export mix and audio_preview_mix use), voiceEnhancedClips vs loudnessNormalizedClips, ...) and highlights[] (plain lines). Each recording counts once, so splits change neither source length nor cuts. Cheap; use it to report an editing pass instead of project_read.",
1807
1827
  inputSchema: {
1808
1828
  type: "object",
1809
1829
  properties: {
@@ -2092,14 +2112,17 @@ const TOOLS = [
2092
2112
  ],
2093
2113
  [
2094
2114
  "stop-recording",
2095
- "Stop recording and finalize podcast media. Check podcast_status for finalization and guest uploads.",
2115
+ "Stop recording. The take goes into the episode project by itself (desktop track once finalized, each guest's as their upload lands, even after the studio is left); returns savedTo and takeId. podcast_status shows finalization and guest uploads.",
2096
2116
  ],
2097
2117
  [
2098
2118
  "start-screen-share",
2099
2119
  "Open the desktop picker to share the host screen with everyone in the call (any time, backstage included; it's recorded while recording). One sharer at a time. Waits for the host to choose; fails if the picker is cancelled or capture fails.",
2100
2120
  ],
2101
2121
  ["stop-screen-share", "Stop and finalize the host's podcast screen share."],
2102
- ["open-in-editor", "Open the finished podcast in the editor when guest media has arrived."],
2122
+ [
2123
+ "open-in-editor",
2124
+ "Open the finished podcast's episode in the editor when guest media has arrived (the take is already in the episode project).",
2125
+ ],
2103
2126
  ].map(([noun, description]) => ({
2104
2127
  name: `podcast_${noun.replaceAll("-", "_")}`,
2105
2128
  description,
@@ -2623,6 +2646,34 @@ const TOOLS = [
2623
2646
  },
2624
2647
  command: "podcast.list-upcoming",
2625
2648
  },
2649
+ {
2650
+ name: "podcast_found_recordings",
2651
+ description:
2652
+ "Recordings that belong to a podcast episode but never reached it (older takes left by Back before Open in editor): guest tracks matched by session code plus the nearest desktop camera recording, each with a key. No path / id / sessionCode = every episode.",
2653
+ inputSchema: {
2654
+ type: "object",
2655
+ properties: {
2656
+ path: { type: "string" },
2657
+ id: { type: "string" },
2658
+ sessionCode: { type: "string" },
2659
+ },
2660
+ },
2661
+ command: "podcast.found-recordings",
2662
+ },
2663
+ {
2664
+ name: "podcast_add_found_recordings",
2665
+ description:
2666
+ "Add a podcast_found_recordings take (path + key) to its episode: it becomes the episode's podcast clip, files move into the project folder. Matched by time, so ask the user first; check the sync afterwards (project_auto_sync_podcast).",
2667
+ inputSchema: {
2668
+ type: "object",
2669
+ properties: {
2670
+ path: { type: "string", description: "The episode's project file." },
2671
+ key: { type: "string", description: "The found take's key." },
2672
+ },
2673
+ required: ["path", "key"],
2674
+ },
2675
+ command: "podcast.add-found-recordings",
2676
+ },
2626
2677
  {
2627
2678
  name: "podcast_get_episode",
2628
2679
  description:
@@ -3698,6 +3749,39 @@ const TOOLS = [
3698
3749
  },
3699
3750
  command: "project.remove-mute-region",
3700
3751
  },
3752
+ {
3753
+ name: "project_add_original_voice",
3754
+ description:
3755
+ 'Turn Enhance voice off for an edited-time span ("Original voice"): the main track plays its original audio there, enhanced elsewhere. Only enhanced clips change. Returns regionId (editor.enhanceOffRegions).',
3756
+ inputSchema: {
3757
+ type: "object",
3758
+ properties: {
3759
+ id: { type: "string" },
3760
+ path: { type: "string" },
3761
+ atMs: { type: "number" },
3762
+ durationMs: { type: "number" },
3763
+ expectedRevision: { type: "number" },
3764
+ },
3765
+ required: ["atMs", "durationMs"],
3766
+ },
3767
+ command: "project.add-original-voice",
3768
+ },
3769
+ {
3770
+ name: "project_remove_original_voice",
3771
+ description:
3772
+ "Remove an Original voice region so Enhance voice applies there again (ids: editor.enhanceOffRegions[].id).",
3773
+ inputSchema: {
3774
+ type: "object",
3775
+ properties: {
3776
+ id: { type: "string" },
3777
+ path: { type: "string" },
3778
+ regionId: { type: "string" },
3779
+ expectedRevision: { type: "number" },
3780
+ },
3781
+ required: ["regionId"],
3782
+ },
3783
+ command: "project.remove-original-voice",
3784
+ },
3701
3785
  {
3702
3786
  name: "project_add_bleep",
3703
3787
  description:
@@ -4143,7 +4227,7 @@ const TOOLS = [
4143
4227
  {
4144
4228
  name: "project_review_moment",
4145
4229
  description:
4146
- "LOOK AND LISTEN to a moment of the edit as the viewer gets it: a contact sheet of the frames around atMs (edited ms) or wordId, the EXPORTED mix of that stretch transcribed and compared with what the edit should play (extra = leftover of a cut word or line, missing = clipped word), whether each cut lands in quiet or speech, nearby edits and stutters, plus a plain verdict. Use after every cut / pause / insert fix BEFORE telling the user it's fixed, and when the user names a moment ('at 16 seconds'). ASYNC: { jobId }; job_wait.",
4230
+ "LOOK AND LISTEN to a moment of the edit as the viewer gets it: a contact sheet of the frames around atMs (edited ms) or wordId, the EXPORTED mix of that stretch transcribed and compared with what the edit should play (the clips' words AND narration / voiceover track words; extra = leftover of a cut word or line, missing = clipped word), whether each cut that interrupts the main track's sound lands in quiet or speech (judged on the main track alone, so narration or music playing across it doesn't count; a join of silent clips such as images is never a cut in the speech), nearby edits and stutters, plus a plain verdict. Use after every cut / pause / insert fix BEFORE telling the user it's fixed, and when the user names a moment ('at 16 seconds'). ASYNC: { jobId }; job_wait.",
4147
4231
  inputSchema: {
4148
4232
  type: "object",
4149
4233
  properties: {
@@ -4198,7 +4282,7 @@ const TOOLS = [
4198
4282
  {
4199
4283
  name: "audio_preview_mix",
4200
4284
  description:
4201
- "Render final export audio only to temporary .m4a plus description and voice/music/SFX stem RMS/masking flags. Edited-time range fromMs..toMs inside 0..editedDurationMs (project_read), default whole project, max 10 minutes. Async: job.wait.",
4285
+ "Render final export audio only to temporary .m4a plus description and voice/music/SFX stem RMS/masking flags; tracks[{id, kind, basis}] = which stem each audio track went to (same as project_edit_summary audioTracks). Edited-time range fromMs..toMs inside 0..editedDurationMs (project_read), default whole project, max 10 minutes. Async: job.wait.",
4202
4286
  inputSchema: {
4203
4287
  type: "object",
4204
4288
  properties: {
@@ -5126,7 +5210,7 @@ const TOOLS = [
5126
5210
  {
5127
5211
  name: "project_auto_reframe",
5128
5212
  description:
5129
- "Active-speaker auto-reframe: frames whoever talks, shot by shot (face tracking, no speaker IDs needed). Landscape footage on a vertical / square canvas gets a crop that follows the speaker; footage that already has the canvas shape (a 16:9 wide of two hosts in a 16:9 video) gets a punch-in on the speaker inside the frame, which is also how to get face-tracked zooms on the main video. zoom = fixed punch-in. split=true stacks two people (podcast / interview wide) one above the other on a 9:16 canvas. Slow (minutes). clear reverts. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs).",
5213
+ "Active-speaker auto-reframe: frames whoever talks, shot by shot (face tracking, no speaker IDs needed). Landscape footage on a vertical / square canvas gets a crop that follows the speaker; footage that already has the canvas shape (a 16:9 wide of two hosts in a 16:9 video) gets a punch-in on the speaker inside the frame, which is also how to get face-tracked zooms on the main video. zoom = fixed punch-in. split=true stacks two people (podcast / interview wide) one above the other on a 9:16 canvas (not on a clip with its own camera video). In the vertical-stack camera layout the crop is computed for the main video's top half. Slow (minutes). clear reverts. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs).",
5130
5214
  inputSchema: {
5131
5215
  type: "object",
5132
5216
  properties: {
@@ -5396,6 +5480,12 @@ const TOOLS = [
5396
5480
  type: "boolean",
5397
5481
  description: "Repeat the file to endMs (a ~30 s Lyria clip under a longer video).",
5398
5482
  },
5483
+ audioKind: {
5484
+ type: "string",
5485
+ enum: ["voice", "music", "sfx"],
5486
+ description:
5487
+ "What the track is (its mix bus, preview-mix stem and edit-summary count). Omit for bundled library files and generated narration / music / SFX (known); pass it for other files, e.g. music for an uploaded song. Change later: project_update_region regionType=audio-overlay patch {audioKind}.",
5488
+ },
5399
5489
  transcribe: {
5400
5490
  type: "boolean",
5401
5491
  description:
@@ -5807,7 +5897,7 @@ const TOOLS = [
5807
5897
  {
5808
5898
  name: "project_set_clip_webcam",
5809
5899
  description:
5810
- "Attach, replace or remove a clip's camera video (Screen + camera), in sync from its first frame. Not for podcast composites.",
5900
+ "Attach, replace or remove a clip's camera video, in sync from its first frame: a presenter camera over a screen recording, or a REACTION video (main clip = the video reacted to, camera = the reactor; audio=mix plays both). The main video keeps its kind. In 9:16 use set-webcam-layout vertical-stack (main on top, camera below), then auto-reframe frames the main video inside its half. Not for podcast composites.",
5811
5901
  inputSchema: {
5812
5902
  type: "object",
5813
5903
  properties: {
@@ -5818,8 +5908,9 @@ const TOOLS = [
5818
5908
  webcam: { type: "string", description: "Absolute path, or '' to remove." },
5819
5909
  audio: {
5820
5910
  type: "string",
5821
- enum: ["auto", "camera", "keep"],
5822
- description: "auto: camera audio if the main video is silent (re-transcribe after).",
5911
+ enum: ["auto", "mix", "camera", "keep"],
5912
+ description:
5913
+ "auto: camera audio if the main video is silent; mix: both sounds (reaction video); camera: camera's sound only; keep: unchanged. Re-transcribe after a change.",
5823
5914
  },
5824
5915
  expectedRevision: { type: "number" },
5825
5916
  },
@@ -5895,13 +5986,14 @@ const TOOLS = [
5895
5986
  {
5896
5987
  name: "system_set_transcription_preferences",
5897
5988
  description:
5898
- "Save workspace transcription language, provider and/or on-device engine atomically (pass any). engine: auto (Parakeet for English/auto-detect, Whisper LOCKED to any other named language), parakeet (keeps fillers, detects the language, can't be locked), whisper (locks to the language). Confirm consent to upload audio for cloud. Cloud and apple cannot use auto.",
5989
+ "Save workspace transcription language, provider and/or on-device engine atomically (pass any). engine: auto (Parakeet for English/auto-detect, Whisper LOCKED to any other named language), parakeet (keeps fillers, detects the language, can't be locked), whisper (locks to the language). Confirm consent to upload audio for cloud. Cloud and apple cannot use auto. fallback: when the cloud provider / Apple speech still fails after retries, local (default: this computer transcribes only the failed parts, reported) or none (fail, never transcribe on this computer: use when the user wants the cloud provider only).",
5899
5990
  inputSchema: {
5900
5991
  type: "object",
5901
5992
  properties: {
5902
5993
  language: { type: "string" },
5903
5994
  provider: { type: "string", enum: ["local", "apple", "deepgram", "elevenlabs"] },
5904
5995
  engine: { type: "string", enum: ["auto", "parakeet", "whisper"] },
5996
+ fallback: { type: "string", enum: ["local", "none"] },
5905
5997
  },
5906
5998
  },
5907
5999
  command: "system.set-transcription-preferences",
@@ -5910,13 +6002,19 @@ const TOOLS = [
5910
6002
  {
5911
6003
  name: "transcript_transcribe",
5912
6004
  description:
5913
- "Transcribe clips with word timestamps (default: only clips without words). Asking again while the same transcription is still running returns that job (alreadyRunning: true), never a second run: keep waiting on it. Engine: language auto/english = Parakeet; any other named language = local Whisper LOCKED to it (engine override). Returns { jobId, plan } (plan = engine + reason, ranges, totalAudioMs: the audio that will be processed, estimate/estimatedMs: how long on this computer, advice: faster options for a long Whisper run; tell the user for long jobs). Long Whisper runs are saved chunk by chunk: stalls restart from the last chunk automatically; after job_cancel, a crash or an app restart, transcribing again resumes there. Progress is real (audio decoded); time left in progress.message and progress.data.etaMs. To REDO a transcript (wrong language, bad run) confirm language, pass per-run language and provider overrides, then force: true (all clips) or clipId. Word fixes are re-applied; tell the user about result droppedWordEdits[] (fixes that may need redoing). Each clip is transcribed only over the part it plays (head trim to tail trim, e.g. one half of a split; only the kept parts when cuts hide most of a long recording; result transcribedRanges[]). Clips with no audio track are skipped (result noAudioClips), not failed. Result warnings[] flags words that can't cover the audio (sound with no words, too few words: music, noise, another language) and a re-run with the same words as before: surface it, and get a second reading with engine whisper or a cloud provider (Parakeet isn't locked by language english, so retrying the same way repeats it). ASYNC: { jobId }, job_wait. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs).",
6005
+ "Transcribe clips with word timestamps (default: only clips without words). Asking again while the same transcription is still running returns that job (alreadyRunning: true), never a second run: keep waiting on it. Engine: language auto/english = Parakeet; any other named language = local Whisper LOCKED to it (engine override). Returns { jobId, plan } (plan = engine + reason, ranges, totalAudioMs: the audio that will be processed, estimate/estimatedMs: how long on this computer, advice: faster options for a long Whisper run; tell the user for long jobs). Long Whisper runs are saved chunk by chunk: stalls restart from the last chunk automatically; after job_cancel, a crash or an app restart, transcribing again resumes there. Progress is real (audio decoded); time left in progress.message and progress.data.etaMs. To REDO a transcript (wrong language, bad run) confirm language, pass per-run language and provider overrides, then force: true (all clips) or clipId. Word fixes are re-applied; tell the user about result droppedWordEdits[] (fixes that may need redoing). Each clip is transcribed only over the part it plays (head trim to tail trim, e.g. one half of a split; only the kept parts when cuts hide most of a long recording; result transcribedRanges[]). Clips with no audio track are skipped (result noAudioClips), not failed. Result warnings[] flags words that can't cover the audio (sound with no words, too few words: music, noise, another language) and a re-run with the same words as before: surface it, and get a second reading with engine whisper or a cloud provider (Parakeet isn't locked by language english, so retrying the same way repeats it). ASYNC: { jobId }, job_wait. Concurrent saves are merged and retried (three attempts). Exhausted conflicts report code revision_conflict / status 409 (job.errorDetails for async jobs). CLOUD (deepgram / elevenlabs): long audio goes up in ~5 min parts, each retried on network drops, timeouts, 5xx and 429. fallback: none = parts that still fail FAIL the job (job.errorDetails.code cloud_failed, failedRanges; nothing runs on this computer; parts that worked are kept for a re-run), local = this computer transcribes only the failed parts. Default none when you pass a cloud/apple provider, else the workspace setting. The job says which engine really ran: progress.data.engine / fallbackFrom while running, result clips[].engine / engines / fallback. job_cancel stops every upload, retry and on-device helper of the job.",
5914
6006
  inputSchema: {
5915
6007
  type: "object",
5916
6008
  properties: {
5917
6009
  id: { type: "string" },
5918
6010
  path: { type: "string" },
5919
6011
  clipId: { type: "string", description: "Only this clip, even if already transcribed." },
6012
+ fallback: {
6013
+ type: "string",
6014
+ enum: ["local", "none"],
6015
+ description:
6016
+ "When the cloud provider / Apple speech still fails after retries: none = fail the job (never transcribe on this computer), local = this computer transcribes only the failed parts. Default: none when provider is passed, else the workspace setting.",
6017
+ },
5920
6018
  language: {
5921
6019
  type: "string",
5922
6020
  description:
@@ -8676,13 +8774,18 @@ const TOOLS = [
8676
8774
  {
8677
8775
  name: "llm_generate_title",
8678
8776
  description:
8679
- "Generate a YouTube title from what the EDITED video says (cut words left out) with the local LLM. Returns { title }: one complete phrase within maxChars, never cut mid-word.",
8777
+ "Generate a YouTube title from what the EDITED video says (cut words left out) with the local LLM. Names the people speaking when the project knows them (podcast participants); instruction steers it. Returns { title }: one complete phrase within maxChars, never cut mid-word.",
8680
8778
  inputSchema: {
8681
8779
  type: "object",
8682
8780
  properties: {
8683
8781
  id: { type: "string" },
8684
8782
  path: { type: "string" },
8685
8783
  maxChars: { type: "number" },
8784
+ instruction: {
8785
+ type: "string",
8786
+ description:
8787
+ "Optional steer from the user (what to focus on), max 500 chars. Speaker names the project knows are used automatically.",
8788
+ },
8686
8789
  },
8687
8790
  },
8688
8791
  command: "llm.generate-title",
@@ -8690,13 +8793,18 @@ const TOOLS = [
8690
8793
  {
8691
8794
  name: "llm_generate_description",
8692
8795
  description:
8693
- "Generate a 3-5 sentence YouTube description from what the EDITED video says, with the local LLM. Returns { description, warnings? }: a warning means it repeated the transcript, so write the description yourself.",
8796
+ "Generate a 3-5 sentence YouTube description from what the EDITED video says, with the local LLM. Names the people speaking when the project knows them (podcast participants); instruction steers what it focuses on. Returns { description, warnings? }: a warning means it repeated the transcript, so write the description yourself.",
8694
8797
  inputSchema: {
8695
8798
  type: "object",
8696
8799
  properties: {
8697
8800
  id: { type: "string" },
8698
8801
  path: { type: "string" },
8699
8802
  maxChars: { type: "number" },
8803
+ instruction: {
8804
+ type: "string",
8805
+ description:
8806
+ "Optional steer from the user (what to focus on), max 500 chars. Speaker names the project knows are used automatically.",
8807
+ },
8700
8808
  },
8701
8809
  },
8702
8810
  command: "llm.generate-description",
@@ -8725,6 +8833,11 @@ const TOOLS = [
8725
8833
  id: { type: "string" },
8726
8834
  path: { type: "string" },
8727
8835
  maxChapters: { type: "number" },
8836
+ instruction: {
8837
+ type: "string",
8838
+ description:
8839
+ "Optional steer from the user (what to focus on), max 500 chars. Speaker names the project knows are used automatically.",
8840
+ },
8728
8841
  },
8729
8842
  },
8730
8843
  command: "llm.generate-timestamps",
@@ -8939,7 +9052,7 @@ const TOOLS = [
8939
9052
  {
8940
9053
  name: "clips_export",
8941
9054
  description:
8942
- "Export a clip set's finished clips to MP4 (all, or clipIds) into Movies/PandaStudio Clips/<video>/, at standard quality (1080p; 720p on a free trial). Async: returns { jobId, outputDir }; job_wait for { exported[] }.",
9055
+ "Export a clip set's finished clips to MP4 (all, or clipIds) into Movies/PandaStudio Clips/<video>/, at standard quality (1080p; 720p on a free trial). Async: returns { jobId, outputDir }; job_wait for { exported[{ clipId, outputPath, exportId }] }. To publish a clip, export it here, then pass exportId to export_publish_youtube (9:16 = Short) or export_publish_social.",
8943
9056
  inputSchema: {
8944
9057
  type: "object",
8945
9058
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@writepanda/mcp",
3
- "version": "1.285.0",
3
+ "version": "1.287.0",
4
4
  "description": "Model Context Protocol server for PandaStudio. Exposes the desktop video editor's automation surface to Cursor, Continue, Cline, Claude Desktop, and any MCP-compliant client.",
5
5
  "keywords": [
6
6
  "pandastudio",