@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9

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 (157) 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 +117 -0
  8. package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -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 +308 -0
  13. package/SKILL.director.md +517 -250
  14. package/dist/src/cli.js +563 -99
  15. package/dist/src/devcli/clips.js +29 -12
  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 +14 -2
  20. package/dist/src/services/clip-curation/hunt.js +56 -0
  21. package/dist/src/services/clip-curation/index.js +1 -1
  22. package/dist/src/services/clip-curation/scan.js +5 -3
  23. package/package.json +30 -10
  24. package/SKILL.platform.md +0 -432
  25. package/demo/README.md +0 -28
  26. package/demo/dist/app.css +0 -1
  27. package/demo/dist/app.js +0 -1850
  28. package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
  29. package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
  30. package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
  31. package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
  32. package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
  33. package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
  34. package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
  35. package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
  36. package/demo/dist/favicon.ico +0 -0
  37. package/demo/dist/icons/timeline/audio.svg +0 -7
  38. package/demo/dist/icons/timeline/captions.svg +0 -5
  39. package/demo/dist/icons/timeline/composition.svg +0 -12
  40. package/demo/dist/icons/timeline/image.svg +0 -18
  41. package/demo/dist/icons/timeline/music.svg +0 -10
  42. package/demo/dist/icons/timeline/text.svg +0 -3
  43. package/demo/dist/index.html +0 -15
  44. package/dist/src/account-pages-legacy.js +0 -9396
  45. package/dist/src/account-pages.js +0 -61
  46. package/dist/src/app.js +0 -21603
  47. package/dist/src/composition-runtime.js +0 -1053
  48. package/dist/src/config.js +0 -217
  49. package/dist/src/context.js +0 -447
  50. package/dist/src/dev-app-legacy.js +0 -739
  51. package/dist/src/dev-app.js +0 -6
  52. package/dist/src/devcli/migrate-local.js +0 -140
  53. package/dist/src/devcli/sync.js +0 -368
  54. package/dist/src/domain.js +0 -5
  55. package/dist/src/editor-chat-history.js +0 -82
  56. package/dist/src/editor-chat.js +0 -828
  57. package/dist/src/editor-dark-theme.js +0 -1128
  58. package/dist/src/frontend/debug.js +0 -71
  59. package/dist/src/frontend/discover-client.js +0 -130
  60. package/dist/src/frontend/discover-store.js +0 -23
  61. package/dist/src/frontend/file-directory.js +0 -1018
  62. package/dist/src/frontend/flockposter-cache-store.js +0 -124
  63. package/dist/src/frontend/homepage-client.js +0 -446
  64. package/dist/src/frontend/homepage-shared.js +0 -201
  65. package/dist/src/frontend/homepage-store.js +0 -66
  66. package/dist/src/frontend/homepage-view.js +0 -705
  67. package/dist/src/frontend/page-runtime-client.js +0 -132
  68. package/dist/src/frontend/page-runtime-store.js +0 -9
  69. package/dist/src/frontend/sentry.js +0 -42
  70. package/dist/src/frontend/template-editor-chat.js +0 -4181
  71. package/dist/src/help-page.js +0 -346
  72. package/dist/src/homepage.js +0 -1458
  73. package/dist/src/index.js +0 -16
  74. package/dist/src/instrument.js +0 -30
  75. package/dist/src/landing-page.js +0 -384
  76. package/dist/src/page-runtime.js +0 -2
  77. package/dist/src/page-shell.js +0 -1452
  78. package/dist/src/primitive-context.js +0 -416
  79. package/dist/src/primitive-registry.js +0 -3940
  80. package/dist/src/primitive-sdk.js +0 -4
  81. package/dist/src/primitives/hyperframes-media.js +0 -108
  82. package/dist/src/react-page-shell.js +0 -35
  83. package/dist/src/ready-post-schedule-component.js +0 -1540
  84. package/dist/src/registry.js +0 -296
  85. package/dist/src/reskin/agency-page.js +0 -299
  86. package/dist/src/reskin/calendar-page.js +0 -568
  87. package/dist/src/reskin/chat-page.js +0 -942
  88. package/dist/src/reskin/discover-page.js +0 -1788
  89. package/dist/src/reskin/document.js +0 -1587
  90. package/dist/src/reskin/help-page.js +0 -357
  91. package/dist/src/reskin/index-page.js +0 -62
  92. package/dist/src/reskin/inpaint-clipper-page.js +0 -890
  93. package/dist/src/reskin/inpaint-page.js +0 -2554
  94. package/dist/src/reskin/inpaint-video-page.js +0 -1339
  95. package/dist/src/reskin/job-runs-page.js +0 -477
  96. package/dist/src/reskin/library-page.js +0 -1634
  97. package/dist/src/reskin/login-page.js +0 -262
  98. package/dist/src/reskin/portfolio-page.js +0 -687
  99. package/dist/src/reskin/pricing-page.js +0 -390
  100. package/dist/src/reskin/settings-page.js +0 -732
  101. package/dist/src/reskin/theme.js +0 -711
  102. package/dist/src/runtime.js +0 -35
  103. package/dist/src/services/api-call-history.js +0 -249
  104. package/dist/src/services/auth.js +0 -152
  105. package/dist/src/services/billing-pricing.js +0 -39
  106. package/dist/src/services/billing.js +0 -241
  107. package/dist/src/services/cast.js +0 -127
  108. package/dist/src/services/chat-threads.js +0 -92
  109. package/dist/src/services/clip-records.js +0 -250
  110. package/dist/src/services/clip-search.js +0 -77
  111. package/dist/src/services/clip-vectors.js +0 -125
  112. package/dist/src/services/composition-sanitize.js +0 -124
  113. package/dist/src/services/composition-watch.js +0 -79
  114. package/dist/src/services/elevenlabs.js +0 -222
  115. package/dist/src/services/file-directory.js +0 -117
  116. package/dist/src/services/fork-access.js +0 -93
  117. package/dist/src/services/fork-manifest.js +0 -42
  118. package/dist/src/services/ghostcut.js +0 -179
  119. package/dist/src/services/hyperframes.js +0 -3654
  120. package/dist/src/services/job-capacity.js +0 -14
  121. package/dist/src/services/job-logs.js +0 -197
  122. package/dist/src/services/jobs.js +0 -136
  123. package/dist/src/services/local-dynamo.js +0 -0
  124. package/dist/src/services/media-processing.js +0 -766
  125. package/dist/src/services/primitive-media-lambda.js +0 -280
  126. package/dist/src/services/providers.js +0 -2748
  127. package/dist/src/services/rate-limits.js +0 -262
  128. package/dist/src/services/scene-annotations.js +0 -32
  129. package/dist/src/services/serverless-auth.js +0 -382
  130. package/dist/src/services/serverless-jobs.js +0 -1084
  131. package/dist/src/services/serverless-provider-keys.js +0 -409
  132. package/dist/src/services/serverless-records.js +0 -1515
  133. package/dist/src/services/serverless-template-configs.js +0 -75
  134. package/dist/src/services/storage.js +0 -461
  135. package/dist/src/services/swipe-customize.js +0 -437
  136. package/dist/src/services/template-certification.js +0 -413
  137. package/dist/src/services/template-loader.js +0 -99
  138. package/dist/src/services/template-runtime-bundles.js +0 -217
  139. package/dist/src/services/template-sources.js +0 -1017
  140. package/dist/src/services/upstream.js +0 -248
  141. package/dist/src/services/video-normalization.js +0 -2
  142. package/dist/src/services/webhooks.js +0 -62
  143. package/dist/src/template-editor-pages.js +0 -2576
  144. package/dist/src/template-editor-shell.js +0 -2893
  145. package/dist/src/template-sdk.js +0 -4
  146. package/dist/src/worker.js +0 -17
  147. package/public/assets/discover-client-app.js +0 -1
  148. package/public/assets/file-directory-app.js +0 -3
  149. package/public/assets/homepage-app.js +0 -54
  150. package/public/assets/homepage-client-app.js +0 -80
  151. package/public/assets/page-runtime-client-app.js +0 -94
  152. package/public/assets/placeholders/scene-placeholder.png +0 -0
  153. package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
  154. package/src/assets/SELLING_WITH_HOOKS.md +0 -377
  155. package/src/assets/SELLING_WITH_VSLS.md +0 -606
  156. package/src/assets/favicon.ico +0 -0
  157. 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