@mevdragon/vidfarm-devcli 0.20.6 → 0.20.8

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 (154) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +15 -1
  2. package/.agents/skills/vidfarm-director/SKILL.md +106 -0
  3. package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
  4. package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
  5. package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
  6. package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
  7. package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
  9. package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
  10. package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
  11. package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
  12. package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
  13. package/SKILL.director.md +476 -260
  14. package/dist/src/cli.js +362 -96
  15. package/dist/src/devcli/clips.js +15 -10
  16. package/dist/src/devcli/doctor.js +2 -2
  17. package/dist/src/devcli/local-backend.js +46 -2
  18. package/dist/src/services/clip-curation/cost.js +5 -1
  19. package/dist/src/services/clip-curation/gemini.js +10 -0
  20. package/package.json +30 -10
  21. package/SKILL.platform.md +0 -432
  22. package/demo/README.md +0 -28
  23. package/demo/dist/app.css +0 -1
  24. package/demo/dist/app.js +0 -1850
  25. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  26. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  27. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  28. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  29. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  30. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  31. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  32. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  33. package/demo/dist/favicon.ico +0 -0
  34. package/demo/dist/icons/timeline/audio.svg +0 -7
  35. package/demo/dist/icons/timeline/captions.svg +0 -5
  36. package/demo/dist/icons/timeline/composition.svg +0 -12
  37. package/demo/dist/icons/timeline/image.svg +0 -18
  38. package/demo/dist/icons/timeline/music.svg +0 -10
  39. package/demo/dist/icons/timeline/text.svg +0 -3
  40. package/demo/dist/index.html +0 -15
  41. package/dist/src/account-pages-legacy.js +0 -9396
  42. package/dist/src/account-pages.js +0 -61
  43. package/dist/src/app.js +0 -21603
  44. package/dist/src/composition-runtime.js +0 -1053
  45. package/dist/src/config.js +0 -217
  46. package/dist/src/context.js +0 -447
  47. package/dist/src/dev-app-legacy.js +0 -739
  48. package/dist/src/dev-app.js +0 -6
  49. package/dist/src/devcli/migrate-local.js +0 -140
  50. package/dist/src/devcli/sync.js +0 -368
  51. package/dist/src/domain.js +0 -5
  52. package/dist/src/editor-chat-history.js +0 -82
  53. package/dist/src/editor-chat.js +0 -828
  54. package/dist/src/editor-dark-theme.js +0 -1128
  55. package/dist/src/frontend/debug.js +0 -71
  56. package/dist/src/frontend/discover-client.js +0 -130
  57. package/dist/src/frontend/discover-store.js +0 -23
  58. package/dist/src/frontend/file-directory.js +0 -1018
  59. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  60. package/dist/src/frontend/homepage-client.js +0 -446
  61. package/dist/src/frontend/homepage-shared.js +0 -201
  62. package/dist/src/frontend/homepage-store.js +0 -66
  63. package/dist/src/frontend/homepage-view.js +0 -705
  64. package/dist/src/frontend/page-runtime-client.js +0 -132
  65. package/dist/src/frontend/page-runtime-store.js +0 -9
  66. package/dist/src/frontend/sentry.js +0 -42
  67. package/dist/src/frontend/template-editor-chat.js +0 -4181
  68. package/dist/src/help-page.js +0 -346
  69. package/dist/src/homepage.js +0 -1458
  70. package/dist/src/index.js +0 -16
  71. package/dist/src/instrument.js +0 -30
  72. package/dist/src/landing-page.js +0 -384
  73. package/dist/src/page-runtime.js +0 -2
  74. package/dist/src/page-shell.js +0 -1452
  75. package/dist/src/primitive-context.js +0 -416
  76. package/dist/src/primitive-registry.js +0 -3940
  77. package/dist/src/primitive-sdk.js +0 -4
  78. package/dist/src/primitives/hyperframes-media.js +0 -108
  79. package/dist/src/react-page-shell.js +0 -35
  80. package/dist/src/ready-post-schedule-component.js +0 -1540
  81. package/dist/src/registry.js +0 -296
  82. package/dist/src/reskin/agency-page.js +0 -299
  83. package/dist/src/reskin/calendar-page.js +0 -568
  84. package/dist/src/reskin/chat-page.js +0 -942
  85. package/dist/src/reskin/discover-page.js +0 -1788
  86. package/dist/src/reskin/document.js +0 -1587
  87. package/dist/src/reskin/help-page.js +0 -357
  88. package/dist/src/reskin/index-page.js +0 -62
  89. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  90. package/dist/src/reskin/inpaint-page.js +0 -2554
  91. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  92. package/dist/src/reskin/job-runs-page.js +0 -477
  93. package/dist/src/reskin/library-page.js +0 -1634
  94. package/dist/src/reskin/login-page.js +0 -262
  95. package/dist/src/reskin/portfolio-page.js +0 -687
  96. package/dist/src/reskin/pricing-page.js +0 -390
  97. package/dist/src/reskin/settings-page.js +0 -732
  98. package/dist/src/reskin/theme.js +0 -711
  99. package/dist/src/runtime.js +0 -35
  100. package/dist/src/services/api-call-history.js +0 -249
  101. package/dist/src/services/auth.js +0 -152
  102. package/dist/src/services/billing-pricing.js +0 -39
  103. package/dist/src/services/billing.js +0 -241
  104. package/dist/src/services/cast.js +0 -127
  105. package/dist/src/services/chat-threads.js +0 -92
  106. package/dist/src/services/clip-records.js +0 -250
  107. package/dist/src/services/clip-search.js +0 -77
  108. package/dist/src/services/clip-vectors.js +0 -125
  109. package/dist/src/services/composition-sanitize.js +0 -124
  110. package/dist/src/services/composition-watch.js +0 -79
  111. package/dist/src/services/elevenlabs.js +0 -222
  112. package/dist/src/services/file-directory.js +0 -117
  113. package/dist/src/services/fork-access.js +0 -93
  114. package/dist/src/services/fork-manifest.js +0 -42
  115. package/dist/src/services/ghostcut.js +0 -179
  116. package/dist/src/services/hyperframes.js +0 -3654
  117. package/dist/src/services/job-capacity.js +0 -14
  118. package/dist/src/services/job-logs.js +0 -197
  119. package/dist/src/services/jobs.js +0 -136
  120. package/dist/src/services/local-dynamo.js +0 -0
  121. package/dist/src/services/media-processing.js +0 -766
  122. package/dist/src/services/primitive-media-lambda.js +0 -280
  123. package/dist/src/services/providers.js +0 -2748
  124. package/dist/src/services/rate-limits.js +0 -262
  125. package/dist/src/services/scene-annotations.js +0 -32
  126. package/dist/src/services/serverless-auth.js +0 -382
  127. package/dist/src/services/serverless-jobs.js +0 -1084
  128. package/dist/src/services/serverless-provider-keys.js +0 -409
  129. package/dist/src/services/serverless-records.js +0 -1515
  130. package/dist/src/services/serverless-template-configs.js +0 -75
  131. package/dist/src/services/storage.js +0 -461
  132. package/dist/src/services/swipe-customize.js +0 -437
  133. package/dist/src/services/template-certification.js +0 -413
  134. package/dist/src/services/template-loader.js +0 -99
  135. package/dist/src/services/template-runtime-bundles.js +0 -217
  136. package/dist/src/services/template-sources.js +0 -1017
  137. package/dist/src/services/upstream.js +0 -248
  138. package/dist/src/services/video-normalization.js +0 -2
  139. package/dist/src/services/webhooks.js +0 -62
  140. package/dist/src/template-editor-pages.js +0 -2576
  141. package/dist/src/template-editor-shell.js +0 -2893
  142. package/dist/src/template-sdk.js +0 -4
  143. package/dist/src/worker.js +0 -17
  144. package/public/assets/discover-client-app.js +0 -1
  145. package/public/assets/file-directory-app.js +0 -3
  146. package/public/assets/homepage-app.js +0 -54
  147. package/public/assets/homepage-client-app.js +0 -80
  148. package/public/assets/page-runtime-client-app.js +0 -94
  149. package/public/assets/placeholders/scene-placeholder.png +0 -0
  150. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  151. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  152. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  153. package/src/assets/favicon.ico +0 -0
  154. package/src/assets/logo-vidfarm.png +0 -0
@@ -1,222 +0,0 @@
1
- // ElevenLabs audio provider (music, TTS, STT) — shared by the ProviderService
2
- // wallet/BYOK router and the primitive handlers. ElevenLabs is a SPECIAL
3
- // audio-only provider: it never counts as a qualified text/image/video key.
4
- // Two ways a call reaches here:
5
- // 1. Platform key (config.ELEVENLABS_API_KEY) + wallet billing — the default
6
- // "use_wallet_credits" path, so good speech/music work with zero setup.
7
- // 2. The customer's own saved ElevenLabs key (use_wallet_credits: false) — no
8
- // wallet charge; identity/voices are scoped to THEIR ElevenLabs account.
9
- //
10
- // Keep the raw request/response shapes here — one implementation for cloud and
11
- // devcli, mirroring services/speech.ts for the openai/gemini/openrouter family.
12
- import { ProviderAuthError, rateLimitOrQuotaError } from "./provider-errors.js";
13
- import { pcm16MonoToWav } from "./speech.js";
14
- const ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
15
- // Sensible defaults so a bare `music`/`tts` call just works.
16
- export const DEFAULT_ELEVENLABS_TTS_MODEL = "eleven_multilingual_v2";
17
- export const DEFAULT_ELEVENLABS_STT_MODEL = "scribe_v1";
18
- export const DEFAULT_ELEVENLABS_MUSIC_MODEL = "music_v2";
19
- // George — a warm, versatile narrative voice. Callers can override with any
20
- // voice_id from `elevenLabsListVoices` (platform or their own account).
21
- export const DEFAULT_ELEVENLABS_VOICE_ID = "JBFqnCBsd6RMkjVDRZzb";
22
- export const ELEVENLABS_VOICE_LIBRARY_URL = "https://elevenlabs.io/app/voice-library";
23
- // A tiny friendly-name → voice_id map so agents/users can say "sarah" instead of
24
- // an opaque id. Unknown values are passed through untouched (treated as a
25
- // voice_id), and the real catalog is always `elevenLabsListVoices`.
26
- const ELEVENLABS_VOICE_PRESETS = {
27
- george: "JBFqnCBsd6RMkjVDRZzb",
28
- sarah: "EXAVITQu4vr4xnSDxMaL",
29
- daniel: "onwK4e9ZLuTAKqWW03F9",
30
- charlotte: "XB0fDUnXU5powFXDhCwa",
31
- rachel: "21m00Tcm4TlvDq8ikWAM"
32
- };
33
- /** Resolve a caller-supplied voice (friendly preset name or raw id) to a voice_id. */
34
- export function resolveElevenLabsVoiceId(voice) {
35
- const raw = voice?.trim();
36
- if (!raw) {
37
- return DEFAULT_ELEVENLABS_VOICE_ID;
38
- }
39
- return ELEVENLABS_VOICE_PRESETS[raw.toLowerCase()] ?? raw;
40
- }
41
- function authHeaders(apiKey) {
42
- return { "xi-api-key": apiKey };
43
- }
44
- async function throwForStatus(provider, operation, response) {
45
- if (response.status === 401 || response.status === 403) {
46
- throw new ProviderAuthError("elevenlabs authentication failed");
47
- }
48
- if (response.status === 429) {
49
- throw await rateLimitOrQuotaError(provider, response);
50
- }
51
- const details = await response.text().catch(() => "");
52
- throw new Error(`elevenlabs ${operation} returned ${response.status}${details ? `: ${details.slice(0, 400)}` : ""}`);
53
- }
54
- /** Cheap credential probe — the ElevenLabs equivalent of a chat-completion validate. */
55
- export async function validateElevenLabsKey(apiKey) {
56
- const response = await fetch(`${ELEVENLABS_BASE_URL}/user`, { headers: authHeaders(apiKey) });
57
- if (response.ok) {
58
- return;
59
- }
60
- await throwForStatus("elevenlabs", "key validation", response);
61
- }
62
- /** List voices available to whichever account owns `apiKey` (platform or the user's). */
63
- export async function elevenLabsListVoices(apiKey) {
64
- const response = await fetch(`${ELEVENLABS_BASE_URL}/voices`, { headers: authHeaders(apiKey) });
65
- if (!response.ok) {
66
- await throwForStatus("elevenlabs", "list voices", response);
67
- }
68
- const data = (await response.json());
69
- return (data.voices ?? []).map((voice) => ({
70
- voice_id: String(voice.voice_id ?? ""),
71
- name: String(voice.name ?? "Unnamed"),
72
- category: typeof voice.category === "string" ? voice.category : null,
73
- description: typeof voice.description === "string" && voice.description.trim() ? voice.description.trim() : null,
74
- labels: voice.labels && typeof voice.labels === "object"
75
- ? Object.fromEntries(Object.entries(voice.labels).map(([key, value]) => [key, String(value)]))
76
- : {},
77
- preview_url: typeof voice.preview_url === "string" ? voice.preview_url : null
78
- })).filter((voice) => voice.voice_id);
79
- }
80
- /** ElevenLabs text-to-speech. `voice` is a voice_id (or a friendly preset name). */
81
- export async function elevenLabsTextToSpeech(input) {
82
- const voiceId = resolveElevenLabsVoiceId(input.voice);
83
- const model = input.model?.trim() || DEFAULT_ELEVENLABS_TTS_MODEL;
84
- const wantWav = input.responseFormat === "wav";
85
- // ElevenLabs has no direct "wav" output — request raw PCM and wrap it in a WAV
86
- // header, mirroring the gemini/openrouter PCM bridge in services/speech.ts.
87
- const outputFormat = wantWav ? "pcm_44100" : "mp3_44100_128";
88
- const voiceSettings = {};
89
- if (input.stability != null)
90
- voiceSettings.stability = input.stability;
91
- if (input.similarityBoost != null)
92
- voiceSettings.similarity_boost = input.similarityBoost;
93
- if (input.style != null)
94
- voiceSettings.style = input.style;
95
- if (input.speed != null)
96
- voiceSettings.speed = input.speed;
97
- const response = await fetch(`${ELEVENLABS_BASE_URL}/text-to-speech/${encodeURIComponent(voiceId)}?output_format=${outputFormat}`, {
98
- method: "POST",
99
- headers: { ...authHeaders(input.apiKey), "Content-Type": "application/json" },
100
- body: JSON.stringify({
101
- text: input.text,
102
- model_id: model,
103
- ...(input.languageCode ? { language_code: input.languageCode } : {}),
104
- ...(Object.keys(voiceSettings).length ? { voice_settings: voiceSettings } : {})
105
- })
106
- });
107
- if (!response.ok) {
108
- await throwForStatus("elevenlabs", "text-to-speech", response);
109
- }
110
- const raw = Buffer.from(await response.arrayBuffer());
111
- const bytes = wantWav ? pcm16MonoToWav(raw, 44100) : raw;
112
- return {
113
- bytes,
114
- contentType: wantWav ? "audio/wav" : "audio/mpeg",
115
- characterCount: input.text.length,
116
- usage: { inputTokens: input.text.length, outputTokens: 0, costUsd: 0 }
117
- };
118
- }
119
- /** ElevenLabs music generation from a prompt or a hand-built composition plan. */
120
- export async function elevenLabsMusicCompose(input) {
121
- const model = input.model?.trim() || DEFAULT_ELEVENLABS_MUSIC_MODEL;
122
- const outputFormat = input.outputFormat?.trim() || "mp3_44100_128";
123
- const body = { model_id: model };
124
- if (input.compositionPlan) {
125
- body.composition_plan = input.compositionPlan;
126
- }
127
- else {
128
- body.prompt = input.prompt;
129
- body.music_length_ms = input.musicLengthMs ?? 30000;
130
- }
131
- const response = await fetch(`${ELEVENLABS_BASE_URL}/music?output_format=${encodeURIComponent(outputFormat)}`, {
132
- method: "POST",
133
- headers: { ...authHeaders(input.apiKey), "Content-Type": "application/json" },
134
- body: JSON.stringify(body)
135
- });
136
- if (!response.ok) {
137
- await throwForStatus("elevenlabs", "music generation", response);
138
- }
139
- return {
140
- bytes: Buffer.from(await response.arrayBuffer()),
141
- contentType: "audio/mpeg",
142
- musicLengthMs: input.musicLengthMs ?? 30000
143
- };
144
- }
145
- /** ElevenLabs speech-to-text (Scribe) — native diarization + word timestamps. */
146
- export async function elevenLabsSpeechToText(input) {
147
- const model = input.model?.trim() || DEFAULT_ELEVENLABS_STT_MODEL;
148
- const form = new FormData();
149
- form.set("file", new Blob([Buffer.from(input.audio)], { type: input.contentType ?? "audio/mpeg" }), "audio");
150
- form.set("model_id", model);
151
- form.set("diarize", String(input.diarize !== false));
152
- form.set("timestamps_granularity", "word");
153
- if (input.languageCode) {
154
- form.set("language_code", input.languageCode);
155
- }
156
- if (input.numSpeakers != null) {
157
- form.set("num_speakers", String(input.numSpeakers));
158
- }
159
- const response = await fetch(`${ELEVENLABS_BASE_URL}/speech-to-text`, {
160
- method: "POST",
161
- headers: authHeaders(input.apiKey),
162
- body: form
163
- });
164
- if (!response.ok) {
165
- await throwForStatus("elevenlabs", "speech-to-text", response);
166
- }
167
- const data = (await response.json());
168
- const words = Array.isArray(data.words) ? data.words : [];
169
- const segments = groupElevenLabsWordsIntoSegments(words);
170
- const speakers = new Set(segments.map((segment) => segment.speaker));
171
- const audioDurationSec = words.reduce((max, word) => Math.max(max, Number(word.end ?? 0)), 0) || null;
172
- return {
173
- text: String(data.text ?? "").trim(),
174
- language: typeof data.language_code === "string" && data.language_code.trim() ? data.language_code.trim() : null,
175
- segments,
176
- diarization: speakers.size > 1 ? "diarized" : "none",
177
- usage: { inputTokens: 0, outputTokens: 0, costUsd: 0 },
178
- audioDurationSec
179
- };
180
- }
181
- // Fold ElevenLabs' flat word stream (each tagged with a speaker_id) into
182
- // speaker-turn segments carrying word-level timings — the shape STT callers and
183
- // the caption builder already consume.
184
- function groupElevenLabsWordsIntoSegments(words) {
185
- const segments = [];
186
- let current = null;
187
- let currentWords = [];
188
- const flush = () => {
189
- if (current) {
190
- current.text = current.text.trim();
191
- if (currentWords.length) {
192
- current.words = currentWords;
193
- }
194
- if (current.text) {
195
- segments.push(current);
196
- }
197
- }
198
- current = null;
199
- currentWords = [];
200
- };
201
- for (const word of words) {
202
- if (word.type && word.type !== "word" && word.type !== "spacing") {
203
- continue;
204
- }
205
- const speaker = word.speaker_id?.trim() || "speaker_1";
206
- const text = String(word.text ?? "");
207
- if (!current || current.speaker !== speaker) {
208
- flush();
209
- current = { speaker, start_sec: Number(word.start ?? 0), end_sec: Number(word.end ?? 0), text: "" };
210
- currentWords = [];
211
- }
212
- current.text += text;
213
- current.end_sec = Number(word.end ?? current.end_sec ?? 0);
214
- const trimmed = text.trim();
215
- if (trimmed && Number.isFinite(Number(word.start)) && Number.isFinite(Number(word.end))) {
216
- currentWords.push({ text: trimmed, start_sec: Number(word.start), end_sec: Number(word.end) });
217
- }
218
- }
219
- flush();
220
- return segments;
221
- }
222
- //# sourceMappingURL=elevenlabs.js.map
@@ -1,117 +0,0 @@
1
- // Shared canonical file-directory path model.
2
- //
3
- // The user's files live in three *separate* backends — My Files attachments,
4
- // Temp files, and the Raws (clip) library — each with its own DynamoDB records
5
- // and S3 prefix. This module is the ONE place that defines the unified virtual
6
- // directory that presents them as three navigable roots:
7
- //
8
- // /files/<subfolders…>/<name> → My Files attachments (users/<id>/files/…)
9
- // /temp/<YYYY-MM-DD>/<name> → Temp files (users/<id>/temporary/…)
10
- // /raws/<subfolders…>/<name> → Raws / clip library (clips/<owner>/…)
11
- // /projects/<forkId>/<subpath…> → Composition forks (compositions/forks/<forkId>/…)
12
- //
13
- // It's a *virtual* layer: physical S3 keys are untouched. The canonical path is
14
- // what the UI copies, what the chat agent navigates, and what search annotates.
15
- // A file's real handle is still its backend id (attachment/temp/clip id) — the
16
- // path is the human-readable navigation + display layer on top.
17
- //
18
- // Pure logic, no runtime deps, so both `src/app.ts` (serve) and
19
- // `infra/lambda/editor-chat.ts` (cloud) import it, and the frontend explorer
20
- // client mirrors the same constants.
21
- export const DIRECTORY_ROOT_KEYS = ["files", "temp", "raws", "projects", "approved"];
22
- export const DIRECTORY_ROOTS = [
23
- { key: "files", label: "My Files", description: "Durable personal files — brand assets, characters, saved context. Vector-searchable via notes." },
24
- { key: "temp", label: "Temp", description: "Scratch space, auto-organized by date (YYYY-MM-DD) and auto-deleted after 30 days." },
25
- { key: "raws", label: "Raws", description: "The reusable clip / source-footage library, organized into folders by source." },
26
- { key: "projects", label: "Projects", description: "Your video-editing projects — one folder per fork, holding that composition's working files (composition.html/json, cast, media) and saved versions. Read-only: browse to reference or study another fork." },
27
- { key: "approved", label: "Approved", description: "Finished, ready-to-post cuts, organized into folders (defaults to a folder per tracer/campaign)." }
28
- ];
29
- // The attachments backend historically scopes its folders under the internal
30
- // key "my_files"; the canonical public root name is "files". These two helpers
31
- // bridge the canonical name and that backend scope so callers never hardcode it.
32
- export const ATTACHMENTS_FOLDER_SCOPE = "my_files";
33
- export function isDirectoryRoot(value) {
34
- return value === "files" || value === "temp" || value === "raws" || value === "projects" || value === "approved";
35
- }
36
- export function directoryRootLabel(root) {
37
- const info = DIRECTORY_ROOTS.find((r) => r.key === root);
38
- return info ? info.label : root;
39
- }
40
- /**
41
- * Normalize a relative folder path: trim each segment, drop empties and any
42
- * leading/trailing slashes, collapse repeats. `"/a//b/ "` → `"a/b"`.
43
- */
44
- export function normalizeFolderPath(value) {
45
- return String(value ?? "")
46
- .split("/")
47
- .map((s) => s.trim())
48
- .filter(Boolean)
49
- .join("/");
50
- }
51
- /**
52
- * Parse a canonical directory path into its root + relative folder path.
53
- *
54
- * A path is always interpreted as a FOLDER location (what to list / scope to):
55
- * every segment after the root is treated as folder path. Files are addressed
56
- * by backend id, not resolved from the trailing segment, so there's no
57
- * file-vs-folder ambiguity to guess at here.
58
- *
59
- * "/raws/product-demos" → { root:"raws", folderPath:"product-demos" }
60
- * "/" or "" → { root:null, folderPath:"" } (virtual root)
61
- */
62
- export function parseDirectoryPath(input) {
63
- const segments = String(input ?? "")
64
- .split("/")
65
- .map((s) => s.trim())
66
- .filter(Boolean);
67
- if (segments.length === 0)
68
- return { root: null, folderPath: "", segments: [] };
69
- const [head, ...rest] = segments;
70
- if (!isDirectoryRoot(head))
71
- return { root: null, folderPath: "", segments };
72
- return { root: head, folderPath: rest.join("/"), segments: rest };
73
- }
74
- /**
75
- * Build a canonical directory path from parts. `name` omitted → a folder path.
76
- *
77
- * buildDirectoryPath("files", "logos", "logo.png") → "/files/logos/logo.png"
78
- * buildDirectoryPath("raws", "demos") → "/raws/demos"
79
- * buildDirectoryPath("temp") → "/temp"
80
- */
81
- export function buildDirectoryPath(root, folderPath, name) {
82
- const parts = [root, normalizeFolderPath(folderPath)];
83
- if (name)
84
- parts.push(String(name).trim());
85
- return "/" + parts.filter(Boolean).join("/");
86
- }
87
- /** Build a folder DirectoryItem for a child folder name under a root/parent. */
88
- export function folderItem(root, parentFolder, childName) {
89
- const folderPath = normalizeFolderPath(parentFolder ? `${parentFolder}/${childName}` : childName);
90
- return {
91
- path: buildDirectoryPath(root, folderPath),
92
- root,
93
- folderPath,
94
- name: childName,
95
- kind: "folder"
96
- };
97
- }
98
- /**
99
- * Given a flat list of items' folderPaths (+ any explicit empty-folder records)
100
- * and the current folder, return the immediate CHILD folder names. Mirrors the
101
- * client explorer's `immediateFolders`, kept here so server and client agree.
102
- */
103
- export function immediateChildFolders(folderPaths, currentFolder) {
104
- const cur = normalizeFolderPath(currentFolder);
105
- const prefix = cur ? cur + "/" : "";
106
- const names = new Set();
107
- for (const raw of folderPaths) {
108
- const n = normalizeFolderPath(raw);
109
- if (!n || (cur && n === cur) || n.indexOf(prefix) !== 0)
110
- continue;
111
- const child = n.slice(prefix.length).split("/")[0];
112
- if (child)
113
- names.add(child);
114
- }
115
- return Array.from(names).sort();
116
- }
117
- //# sourceMappingURL=file-directory.js.map
@@ -1,93 +0,0 @@
1
- import { serverlessRecords } from "./serverless-records.js";
2
- export class ForkNotFoundError extends Error {
3
- constructor() { super("Fork not found"); this.name = "ForkNotFoundError"; }
4
- }
5
- export class ForkAccessDeniedError extends Error {
6
- needed;
7
- constructor(needed) {
8
- super(`Missing required capability: ${needed}`);
9
- this.needed = needed;
10
- this.name = "ForkAccessDeniedError";
11
- }
12
- }
13
- const capabilitiesByRole = {
14
- viewer: { canView: true, canEdit: false, canPublish: false },
15
- editor: { canView: true, canEdit: true, canPublish: false },
16
- publisher: { canView: true, canEdit: true, canPublish: true }
17
- };
18
- function ownerCaps() {
19
- return { isOwner: true, canView: true, canEdit: true, canPublish: true, canShare: true, canDelete: true, role: "owner" };
20
- }
21
- function fromRole(role) {
22
- return {
23
- isOwner: false,
24
- ...capabilitiesByRole[role],
25
- canShare: false,
26
- canDelete: false,
27
- role
28
- };
29
- }
30
- function mergeCaps(a, b) {
31
- const stronger = roleStrength(a.role) >= roleStrength(b.role) ? a : b;
32
- return {
33
- isOwner: a.isOwner || b.isOwner,
34
- canView: a.canView || b.canView,
35
- canEdit: a.canEdit || b.canEdit,
36
- canPublish: a.canPublish || b.canPublish,
37
- canShare: a.canShare || b.canShare,
38
- canDelete: a.canDelete || b.canDelete,
39
- role: stronger.role
40
- };
41
- }
42
- function roleStrength(role) {
43
- switch (role) {
44
- case "owner": return 4;
45
- case "publisher": return 3;
46
- case "editor": return 2;
47
- case "viewer": return 1;
48
- default: return 0;
49
- }
50
- }
51
- function isExpired(expiresAt) {
52
- if (!expiresAt)
53
- return false;
54
- return new Date(expiresAt).getTime() < Date.now();
55
- }
56
- export async function resolveForkAccess(input) {
57
- const fork = await serverlessRecords.getCompositionFork(input.forkId);
58
- if (!fork || fork.deletedAt) {
59
- throw new ForkNotFoundError();
60
- }
61
- // Fork content lives under a public-read S3 prefix — any client with the
62
- // fork_id can already fetch composition.html/json/manifest.json directly
63
- // from S3. Reflect that reality in the API so read endpoints don't lie
64
- // about protecting content that isn't actually protected. Edit/publish/
65
- // share/delete stay gated below.
66
- let caps = fromRole("viewer");
67
- if (input.customerId && fork.customerId === input.customerId) {
68
- caps = mergeCaps(caps, ownerCaps());
69
- }
70
- if (input.customerId) {
71
- const allPerms = await serverlessRecords.listForkPermissions(fork.id);
72
- const granted = allPerms.filter((perm) => perm.granteeType === "customer" && perm.granteeId === input.customerId && !isExpired(perm.expiresAt));
73
- for (const perm of granted) {
74
- caps = mergeCaps(caps, fromRole(perm.role));
75
- }
76
- }
77
- if (input.shareToken) {
78
- const link = await serverlessRecords.getForkShareLinkByToken(input.shareToken);
79
- if (link && link.forkId === fork.id && !link.revokedAt && !isExpired(link.expiresAt)) {
80
- caps = mergeCaps(caps, fromRole(link.role));
81
- }
82
- }
83
- return { fork, capabilities: caps };
84
- }
85
- export async function assertForkAccess(input, required) {
86
- const result = await resolveForkAccess(input);
87
- const need = `can${required[0].toUpperCase()}${required.slice(1)}`;
88
- if (!result.capabilities[need]) {
89
- throw new ForkAccessDeniedError(need);
90
- }
91
- return result;
92
- }
93
- //# sourceMappingURL=fork-access.js.map
@@ -1,42 +0,0 @@
1
- function buildManifest(storage, fork, target) {
2
- const isWorking = target.kind === "working";
3
- const htmlKey = isWorking
4
- ? storage.compositionForkWorkingKey(fork.id, "composition.html")
5
- : storage.compositionForkVersionKey(fork.id, target.version, "composition.html");
6
- const jsonKey = isWorking
7
- ? storage.compositionForkWorkingKey(fork.id, "composition.json")
8
- : storage.compositionForkVersionKey(fork.id, target.version, "composition.json");
9
- const manifestKey = isWorking
10
- ? storage.compositionForkWorkingKey(fork.id, "manifest.json")
11
- : storage.compositionForkVersionKey(fork.id, target.version, "manifest.json");
12
- const parentManifestKey = fork.parentForkId && fork.parentVersion
13
- ? storage.compositionForkVersionKey(fork.parentForkId, fork.parentVersion, "manifest.json")
14
- : null;
15
- return {
16
- fork_id: fork.id,
17
- customer_id: fork.customerId,
18
- is_working: isWorking,
19
- version: isWorking ? null : target.version,
20
- latest_version: fork.latestVersion,
21
- parent_fork_id: fork.parentForkId,
22
- parent_version: fork.parentVersion,
23
- template_id: fork.templateId,
24
- visibility: fork.visibility,
25
- title: fork.title,
26
- composition_url: storage.getPublicUrl(htmlKey),
27
- composition_json_url: storage.getPublicUrl(jsonKey),
28
- manifest_url: storage.getPublicUrl(manifestKey),
29
- parent_manifest_url: parentManifestKey ? storage.getPublicUrl(parentManifestKey) : null,
30
- created_at: !isWorking && target.kind === "version" && target.createdAt ? target.createdAt : fork.createdAt,
31
- updated_at: fork.updatedAt
32
- };
33
- }
34
- export async function writeForkManifest(storage, fork, target) {
35
- const manifest = buildManifest(storage, fork, target);
36
- const key = target.kind === "working"
37
- ? storage.compositionForkWorkingKey(fork.id, "manifest.json")
38
- : storage.compositionForkVersionKey(fork.id, target.version, "manifest.json");
39
- await storage.putJson(key, manifest);
40
- return manifest;
41
- }
42
- //# sourceMappingURL=fork-manifest.js.map
@@ -1,179 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { config } from "../config.js";
3
- const BASE_URL = "https://api.zhaoli.com";
4
- export function isGhostcutConfigured() {
5
- return Boolean(config.GHOSTCUT_KEY && config.GHOSTCUT_SECRET);
6
- }
7
- // GhostCut bills per 30-second chunk with a 1-chunk minimum. A 45s clip is
8
- // 2 chunks, a 90s clip is 3. Rate defaults to $0.10 per chunk but can be
9
- // overridden via GHOSTCUT_USD_PER_30S.
10
- export function estimateGhostcutCostUsd(durationSeconds) {
11
- const usdPerChunk = config.GHOSTCUT_USD_PER_30S;
12
- const positiveSeconds = Number.isFinite(durationSeconds) && durationSeconds > 0 ? durationSeconds : 30;
13
- const chunks = Math.max(1, Math.ceil(positiveSeconds / 30));
14
- const usd = Number((chunks * usdPerChunk).toFixed(4));
15
- return { chunks, usd, usdPerChunk };
16
- }
17
- function md5Hex(input) {
18
- return createHash("md5").update(input).digest("hex");
19
- }
20
- async function ghostcutPost(path, payload, signal) {
21
- if (!config.GHOSTCUT_KEY || !config.GHOSTCUT_SECRET) {
22
- throw new Error("GhostCut credentials not configured");
23
- }
24
- const body = JSON.stringify(payload);
25
- const sign = md5Hex(md5Hex(body) + config.GHOSTCUT_SECRET);
26
- const response = await fetch(`${BASE_URL}${path}`, {
27
- method: "POST",
28
- headers: {
29
- "Content-Type": "application/json",
30
- AppKey: config.GHOSTCUT_KEY,
31
- AppSign: sign
32
- },
33
- body,
34
- signal
35
- });
36
- if (!response.ok) {
37
- throw new Error(`GhostCut HTTP ${response.status} ${response.statusText}`);
38
- }
39
- return (await response.json());
40
- }
41
- // Submits a subtitle-removal task using GhostCut's Basic text-removal model.
42
- // We deliberately do NOT pass `extra_inpaint_config.model` — that would opt
43
- // into one of the Advanced tiers (Lite/Pro/Full), which are:
44
- // 1. More expensive per 30s chunk
45
- // 2. Require `videoInpaintMasks` to be supplied — advanced_lite in particular
46
- // rejects with processStatus=26 / ValidationError
47
- // ("video_inpaint_masks is not set") when no mask is passed
48
- // Basic is the cheapest tier and handles hardcoded TikTok-style captions fine
49
- // without requiring us to hand-crop a mask region for every video.
50
- //
51
- // needChineseOcclude=3 = "Automatically remove moving text". Per the docs this
52
- // is the smart mode that only touches AI-detected text and leaves backgrounds
53
- // alone — exactly what TikTok-style dynamic burned-in captions need. Mode 1
54
- // (full-screen inpainting) is more aggressive; mode 3 sidesteps that class of
55
- // failure while still catching the burned-in subtitles.
56
- // Returns quickly (<2s). Throws on non-1000 response codes (auth, credits, etc).
57
- export async function submitSubtitleRemoval(sourceUrl, signal) {
58
- const payload = {
59
- urls: [sourceUrl],
60
- needChineseOcclude: 3,
61
- videoInpaintLang: "all"
62
- };
63
- const res = await ghostcutPost("/v-w-c/gateway/ve/work/free", payload, signal);
64
- if (res.code !== 1000) {
65
- throw new Error(`GhostCut submit failed: code=${res.code} msg=${res.msg ?? "unknown"}`);
66
- }
67
- const taskId = res.body?.idProject ??
68
- res.body?.dataList?.[0]?.idProject ??
69
- res.body?.dataList?.[0]?.id;
70
- if (typeof taskId !== "number") {
71
- throw new Error(`GhostCut response missing task id: ${JSON.stringify(res)}`);
72
- }
73
- return { taskId };
74
- }
75
- export async function pollStatus(taskId, signal) {
76
- const res = await ghostcutPost("/v-w-c/gateway/ve/work/status", { idProjects: [taskId] }, signal);
77
- if (res.code !== 1000) {
78
- return { state: "failed", reason: `status code=${res.code}` };
79
- }
80
- const entry = res.body?.content?.[0];
81
- if (!entry) {
82
- return { state: "queued", progress: 0 };
83
- }
84
- const processStatus = Number(entry.processStatus ?? 0);
85
- const progress = Number(entry.processProgress ?? 0);
86
- // GhostCut's canonical live states: 1 = done, 0 = processing, -2 = queued.
87
- // Everything else is a terminal error condition. Notable codes we've hit
88
- // in the wild: -1/2/3 (generic failure), 73 (insufficient points — account
89
- // out of credits, task will NEVER progress). Treat all non-live codes as
90
- // failed so we stop polling and fall back to the original video instead
91
- // of spinning forever.
92
- if (processStatus === 1) {
93
- const rawUrl = entry.videoUrl ?? entry.videoUrls;
94
- const videoUrl = Array.isArray(rawUrl) ? String(rawUrl[0] ?? "") : String(rawUrl ?? "");
95
- if (!videoUrl)
96
- return { state: "failed", reason: "processStatus=1 but no videoUrl" };
97
- return { state: "done", videoUrl, progress: 100 };
98
- }
99
- if (processStatus === 0) {
100
- return { state: "processing", progress };
101
- }
102
- if (processStatus === -2) {
103
- return { state: "queued", progress };
104
- }
105
- // Any other code is terminal-failure. Assemble the most helpful reason
106
- // string GhostCut gives us, in priority order:
107
- // 1. processStatusEnum.descriptionEn — official English label for the code
108
- // 2. errorDetail — long-form failure message ("Video too short", etc.)
109
- // 3. errorMsg — short label
110
- // 4. compact raw entry dump — fallback so unknown codes still capture
111
- // whatever fields GhostCut populated, letting us reverse-engineer
112
- // new failure modes without a second API round-trip.
113
- // The Appendix A enum table (Feishu-only) isn't public, so relying on
114
- // GhostCut's own free-text fields is the only way to name codes like 26.
115
- const enumObj = entry.processStatusEnum;
116
- const enumEn = typeof enumObj?.descriptionEn === "string" ? enumObj.descriptionEn.trim() : "";
117
- const errorDetail = typeof entry.errorDetail === "string" ? entry.errorDetail.trim() : "";
118
- const errorMsg = typeof entry.errorMsg === "string" ? entry.errorMsg.trim() : "";
119
- const parts = [];
120
- if (enumEn)
121
- parts.push(enumEn);
122
- if (errorDetail && errorDetail !== enumEn)
123
- parts.push(`detail=${errorDetail}`);
124
- if (errorMsg && errorMsg !== enumEn && errorMsg !== errorDetail)
125
- parts.push(`msg=${errorMsg}`);
126
- if (parts.length === 0) {
127
- // Nothing human-readable — dump the raw payload so operators can still see
128
- // whatever GhostCut *did* send back.
129
- return { state: "failed", reason: `processStatus=${processStatus} raw=${safeCompactJson(entry, 500)}` };
130
- }
131
- return { state: "failed", reason: `processStatus=${processStatus} ${parts.join(" ")}` };
132
- }
133
- // Best-effort compact stringifier — clamps the output so a huge GhostCut
134
- // entry doesn't blow up the log line, and falls back to a marker string
135
- // when serialization itself fails (circular refs shouldn't happen but
136
- // defense-in-depth for JSON.stringify).
137
- function safeCompactJson(value, maxLen) {
138
- try {
139
- const raw = JSON.stringify(value);
140
- if (raw.length <= maxLen)
141
- return raw;
142
- return `${raw.slice(0, maxLen)}…(+${raw.length - maxLen}b)`;
143
- }
144
- catch {
145
- return "<unserializable>";
146
- }
147
- }
148
- // Polls GhostCut until the task is done, fails, or the timeout expires.
149
- // Throws on failure/timeout — callers use that to fall back to the original video.
150
- export async function waitForCompletion(taskId, opts) {
151
- const started = Date.now();
152
- const pollIntervalMs = opts.pollIntervalMs ?? 5000;
153
- while (true) {
154
- if (opts.signal?.aborted)
155
- throw new Error("GhostCut wait aborted");
156
- const remaining = opts.timeoutMs - (Date.now() - started);
157
- if (remaining <= 0)
158
- throw new Error(`GhostCut task ${taskId} timed out`);
159
- const status = await pollStatus(taskId, opts.signal);
160
- if (status.state === "done")
161
- return { videoUrl: status.videoUrl };
162
- if (status.state === "failed")
163
- throw new Error(`GhostCut task ${taskId} failed: ${status.reason}`);
164
- await new Promise((resolve) => setTimeout(resolve, Math.min(pollIntervalMs, remaining)));
165
- }
166
- }
167
- // Downloads the GhostCut-hosted result and mirrors it to our S3 bucket so
168
- // composition HTML can point at a stable URL under our control (rather than a
169
- // short-lived third-party CDN link).
170
- export async function mirrorGhostcutVideoToStorage(input) {
171
- const response = await fetch(input.videoUrl, { signal: input.signal });
172
- if (!response.ok) {
173
- throw new Error(`Failed to fetch GhostCut result: HTTP ${response.status}`);
174
- }
175
- const arrayBuffer = await response.arrayBuffer();
176
- const result = await input.storage.putBuffer(input.key, Buffer.from(arrayBuffer), "video/mp4");
177
- return { key: result.key, publicUrl: result.url };
178
- }
179
- //# sourceMappingURL=ghostcut.js.map