@mevdragon/vidfarm-devcli 0.17.0 → 0.18.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.
- package/.agents/skills/vidfarm-media/SKILL.md +3 -3
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/app.js +301 -45
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +502 -187
- package/dist/src/reskin/chat-page.js +470 -277
- package/dist/src/reskin/discover-page.js +988 -272
- package/dist/src/reskin/document.js +602 -13
- package/dist/src/reskin/help-page.js +138 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +435 -262
- package/dist/src/reskin/login-page.js +118 -81
- package/dist/src/reskin/pricing-page.js +195 -166
- package/dist/src/reskin/settings-page.js +520 -186
- package/dist/src/reskin/theme.js +106 -9
- package/package.json +1 -1
package/dist/src/app.js
CHANGED
|
@@ -30,7 +30,7 @@ import { renderReskinLibrary } from "./reskin/library-page.js";
|
|
|
30
30
|
import { renderReskinDiscover } from "./reskin/discover-page.js";
|
|
31
31
|
import { renderReskinChat } from "./reskin/chat-page.js";
|
|
32
32
|
import { renderReskinCalendar } from "./reskin/calendar-page.js";
|
|
33
|
-
import {
|
|
33
|
+
import { renderReskinInpaint } from "./reskin/inpaint-page.js";
|
|
34
34
|
import { renderReskinAgency } from "./reskin/agency-page.js";
|
|
35
35
|
import { renderReskinPricing } from "./reskin/pricing-page.js";
|
|
36
36
|
import { renderReskinLogin } from "./reskin/login-page.js";
|
|
@@ -101,7 +101,10 @@ const COMPOSITIONS_PREFIX = `${API_PREFIX}/compositions`;
|
|
|
101
101
|
const INSPIRATIONS_PREFIX = `${API_PREFIX}/inspirations`;
|
|
102
102
|
const VIDEOS_PREFIX = `${API_PREFIX}/videos`;
|
|
103
103
|
const PRIMITIVES_PREFIX = `${API_PREFIX}/primitives`;
|
|
104
|
-
const
|
|
104
|
+
const RAWS_PREFIX = "/raws";
|
|
105
|
+
// Back-compat: the clip library was renamed "raws". Old /clips callers are
|
|
106
|
+
// transparently redispatched to /raws (see the alias registered below).
|
|
107
|
+
const CLIPS_COMPAT_PREFIX = "/clips";
|
|
105
108
|
// Temp folder ("My Files → temporary") hard-TTL. Keep in sync with the
|
|
106
109
|
// tag-scoped S3 lifecycle rule (30 days) in the CDK stacks and the clip-scan
|
|
107
110
|
// Lambda's TEMP_SOURCE_TTL_DAYS — records, sweeps, and object lifecycle must
|
|
@@ -3259,11 +3262,11 @@ async function buildClipsEditorChatBoot(input) {
|
|
|
3259
3262
|
const directorSkill = readRootSkillFile("SKILL.director.md", "SKILL.user.md") || null;
|
|
3260
3263
|
const availableProviders = (await normalizeEditorChatProviderRows(input.customer.id)).map((entry) => entry.provider);
|
|
3261
3264
|
const docsRoutes = [
|
|
3262
|
-
{ method: "GET", path: `${
|
|
3263
|
-
{ method: "POST", path: `${
|
|
3264
|
-
{ method: "POST", path: `${
|
|
3265
|
-
{ method: "GET", path: `${
|
|
3266
|
-
{ method: "GET", path: `${
|
|
3265
|
+
{ method: "GET", path: `${RAWS_PREFIX}/feed`, summary: "List the caller's reusable clip library (optional ?q, ?source, ?limit)." },
|
|
3266
|
+
{ method: "POST", path: `${RAWS_PREFIX}/search`, summary: "Hybrid structured + semantic clip search. Body is { query } (natural language) or { criteria } (structured filter)." },
|
|
3267
|
+
{ method: "POST", path: `${RAWS_PREFIX}/scan`, summary: "Import a source video and mine it into clips. Body accepts source_url (a public/durable video URL), raw_s3_uri, or s3_key, plus an optional prompt describing which scenes to keep (e.g. 'people with food, no on-screen text, vertical, 5-10s')." },
|
|
3268
|
+
{ method: "GET", path: `${RAWS_PREFIX}/scan/:scanId`, summary: "Poll clip-scan status for an in-progress import." },
|
|
3269
|
+
{ method: "GET", path: `${RAWS_PREFIX}/presets`, summary: "List the caller's saved clip-search presets." }
|
|
3267
3270
|
];
|
|
3268
3271
|
return {
|
|
3269
3272
|
apiUrl: resolveEditorChatApiUrl(),
|
|
@@ -3627,7 +3630,7 @@ function normalizeEditorChatPath(input) {
|
|
|
3627
3630
|
// and NEVER runs GhostCut on the long-form source — "no text" is a
|
|
3628
3631
|
// scene-selection filter; caption removal stays a per-finished-clip
|
|
3629
3632
|
// primitive (/api/v1/primitives/videos/remove-captions).
|
|
3630
|
-
|
|
3633
|
+
RAWS_PREFIX,
|
|
3631
3634
|
// Public skill knowledge: the vendored skill packs (load_skill fetches
|
|
3632
3635
|
// these too) and per-template SKILL.md manifests. Read-only markdown.
|
|
3633
3636
|
"/skill/",
|
|
@@ -4911,27 +4914,268 @@ app.get("/", async (c) => {
|
|
|
4911
4914
|
// Each page is a standalone document with its own CSS; renders sample data so
|
|
4912
4915
|
// it's viewable without auth. Ported pages are wired here one at a time.
|
|
4913
4916
|
app.get("/reskin", (c) => c.html(renderReskinIndex()));
|
|
4914
|
-
app.get("/
|
|
4917
|
+
app.get("/settings", async (c) => {
|
|
4918
|
+
const customer = await requireBrowserCustomer(c);
|
|
4919
|
+
if (!customer) {
|
|
4920
|
+
setLoginReturnPath(c, currentPathWithSearch(c));
|
|
4921
|
+
return redirect(c, "/login");
|
|
4922
|
+
}
|
|
4923
|
+
const flash = consumeSettingsFlash(c);
|
|
4924
|
+
const flockPosterApiKey = customer.flockposterApiKey?.trim() || null;
|
|
4925
|
+
const activeTab = flash?.tab === "wallet" || flash?.tab === "channels" || flash?.tab === "developer"
|
|
4926
|
+
? flash.tab
|
|
4927
|
+
: c.req.query("tab") === "wallet" || c.req.query("tab") === "channels" || c.req.query("tab") === "developer"
|
|
4928
|
+
? c.req.query("tab")
|
|
4929
|
+
: "profile";
|
|
4930
|
+
const directorSkill = readRootSkillFile("SKILL.director.md", "SKILL.user.md");
|
|
4931
|
+
const emailChannels = await getEmailPseudoChannels(customer.id, c);
|
|
4932
|
+
const [wallet, walletEvents] = await Promise.all([
|
|
4933
|
+
fetchBillingWalletSummary(customer.id).catch((error) => ({
|
|
4934
|
+
balanceUsd: 0, totalFundsAddedUsd: 0, totalChargeUsd: 0, totalBaseCostUsd: 0,
|
|
4935
|
+
totalEventCount: 0, updatedAtMs: 0,
|
|
4936
|
+
unavailableReason: error instanceof Error ? error.message : "Unable to load wallet."
|
|
4937
|
+
})),
|
|
4938
|
+
fetchBillingWalletEvents({ customerId: customer.id, limit: 50 }).catch((error) => ({
|
|
4939
|
+
events: [],
|
|
4940
|
+
nextCursor: null,
|
|
4941
|
+
unavailableReason: error instanceof Error ? error.message : "Unable to load billing history."
|
|
4942
|
+
}))
|
|
4943
|
+
]);
|
|
4944
|
+
return c.html(renderReskinSettings({
|
|
4945
|
+
userId: customer.id,
|
|
4946
|
+
currentAccountId: customer.id,
|
|
4947
|
+
name: customer.name,
|
|
4948
|
+
notice: flash?.notice ?? null,
|
|
4949
|
+
error: flash?.error ?? null,
|
|
4950
|
+
activeTab,
|
|
4951
|
+
openFlockPosterDialog: c.req.query("connect") === "flockposter" || (activeTab === "channels" && !flockPosterApiKey),
|
|
4952
|
+
email: customer.email,
|
|
4953
|
+
isPaidPlan: customer.isPaidPlan,
|
|
4954
|
+
isDeveloper: customer.isDeveloper,
|
|
4955
|
+
about: customer.about,
|
|
4956
|
+
groupchatUrl: customer.groupchatUrl,
|
|
4957
|
+
flockposterApiKey: flockPosterApiKey,
|
|
4958
|
+
emailChannelIconUrl: await getEmailChannelIconUrl(c),
|
|
4959
|
+
emailChannels,
|
|
4960
|
+
vidfarmApiKey: await getVisibleApiKey(customer.id),
|
|
4961
|
+
billingUrl: config.VIDFARM_BILLING_URL?.trim() || null,
|
|
4962
|
+
directorSkill,
|
|
4963
|
+
wallet,
|
|
4964
|
+
walletEvents,
|
|
4965
|
+
flockposterChannels: [],
|
|
4966
|
+
flockposterChannelsError: null,
|
|
4967
|
+
providerKeys: (await listProviderKeysWithSecretsForCustomer(customer.id)).map((entry) => ({
|
|
4968
|
+
id: String(entry.id),
|
|
4969
|
+
provider: String(entry.provider),
|
|
4970
|
+
label: entry.label ? String(entry.label) : null,
|
|
4971
|
+
secret: readStoredSecret(String(entry.secret)),
|
|
4972
|
+
status: String(entry.status),
|
|
4973
|
+
created_at: String(entry.created_at),
|
|
4974
|
+
last_used_at: entry.last_used_at ? String(entry.last_used_at) : null
|
|
4975
|
+
})),
|
|
4976
|
+
attachments: (await serverlessRecords.listUserAttachments(customer.id)).map((attachment) => serializeUserAttachment(c, attachment))
|
|
4977
|
+
}));
|
|
4978
|
+
});
|
|
4915
4979
|
// Library hosts two tabs at distinct URLs (approved projects + mined clips).
|
|
4916
|
-
app.get("/
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4980
|
+
app.get("/library", (c) => redirect(c, "/library/approved"));
|
|
4981
|
+
const reskinLibraryHandler = async (c, tab) => {
|
|
4982
|
+
const customer = await getPreferredBrowserCustomer(c);
|
|
4983
|
+
if (!customer) {
|
|
4984
|
+
setLoginReturnPath(c, currentPathWithSearch(c));
|
|
4985
|
+
return redirect(c, "/login");
|
|
4986
|
+
}
|
|
4987
|
+
const emailChannels = await getEmailPseudoChannels(customer.id, c);
|
|
4988
|
+
const flockPoster = await getFlockPosterChannels(customer.flockposterApiKey?.trim() || null);
|
|
4989
|
+
const scheduleChannels = [
|
|
4990
|
+
...emailChannels.map((channel) => ({ id: channel.id, name: channel.name, title: channel.title, handle: channel.handle, platform: channel.platform, status: channel.status, avatarUrl: channel.avatarUrl })),
|
|
4991
|
+
...flockPoster.channels.map((channel) => ({ id: channel.id, name: channel.title, title: channel.title, handle: channel.handle, platform: channel.platform, status: channel.status, avatarUrl: channel.avatarUrl }))
|
|
4992
|
+
];
|
|
4993
|
+
const localPosts = await Promise.all((await serverlessRecords.listReadyPosts(customer.id))
|
|
4994
|
+
.filter((post) => (post.source ?? "legacy") === "hyperframes")
|
|
4995
|
+
.filter((post) => post.status === "ready" || post.status === "scheduled")
|
|
4996
|
+
.map(async (post) => ({ ...serializeReadyPost(c, post, { includePrivate: true }), template_id: post.compositionId ?? null })));
|
|
4997
|
+
const posts = await mergeUpstreamReadyPosts(localPosts);
|
|
4998
|
+
return c.html(renderReskinLibrary(tab, {
|
|
4999
|
+
userId: customer.id,
|
|
5000
|
+
currentAccountId: customer.id,
|
|
5001
|
+
name: customer.name,
|
|
5002
|
+
email: customer.email,
|
|
5003
|
+
posts,
|
|
5004
|
+
schedule: { channels: scheduleChannels, connectHref: "/settings?tab=channels&connect=flockposter" },
|
|
5005
|
+
editorChat: null
|
|
5006
|
+
}));
|
|
5007
|
+
};
|
|
5008
|
+
app.get("/library/approved", (c) => reskinLibraryHandler(c, "approved"));
|
|
5009
|
+
app.get("/library/raws", (c) => reskinLibraryHandler(c, "raws"));
|
|
5010
|
+
app.get("/library/logs", (c) => reskinLibraryHandler(c, "logs")); // job-runs, folded in
|
|
5011
|
+
app.get("/library/clips", (c) => redirect(c, "/library/raws")); // renamed Clips → Raws
|
|
5012
|
+
// Discover has three modes selected via ?view= (feed default / swipe / categories).
|
|
5013
|
+
// Path segments are avoided because /discover/feed, /discover/swipe/* are JSON APIs.
|
|
5014
|
+
app.get("/discover", async (c) => {
|
|
5015
|
+
const view = c.req.query("view");
|
|
5016
|
+
const mode = view === "swipe" || view === "categories" ? view : "feed";
|
|
5017
|
+
const customer = await getOptionalPreferredBrowserCustomer(c);
|
|
5018
|
+
return c.html(renderReskinDiscover(mode, { accountId: customer?.id, isLoggedIn: Boolean(customer) }));
|
|
5019
|
+
});
|
|
5020
|
+
app.get("/chat", async (c) => {
|
|
5021
|
+
const customer = await getPreferredBrowserCustomer(c);
|
|
5022
|
+
if (!customer) {
|
|
5023
|
+
setLoginReturnPath(c, currentPathWithSearch(c));
|
|
5024
|
+
return redirect(c, "/login");
|
|
5025
|
+
}
|
|
5026
|
+
return c.html(renderReskinChat({
|
|
5027
|
+
userId: customer.id,
|
|
5028
|
+
currentAccountId: customer.id,
|
|
5029
|
+
name: customer.name,
|
|
5030
|
+
email: customer.email,
|
|
5031
|
+
editorChat: await buildChatBrainstormEditorChatBoot({ customer })
|
|
5032
|
+
}));
|
|
5033
|
+
});
|
|
5034
|
+
// /reskin/inpaint — the "Create Image" masked-image editor (reached from the
|
|
5035
|
+
// chat page's studio-mode select). Auth-gated like the other tools.
|
|
5036
|
+
app.get("/inpaint", async (c) => {
|
|
5037
|
+
const customer = await getOptionalPreferredBrowserCustomer(c);
|
|
5038
|
+
if (!customer) {
|
|
5039
|
+
setLoginReturnPath(c, currentPathWithSearch(c));
|
|
5040
|
+
return redirect(c, "/login");
|
|
5041
|
+
}
|
|
5042
|
+
return c.html(renderReskinInpaint({ accountId: customer.id, isLoggedIn: true }));
|
|
5043
|
+
});
|
|
5044
|
+
app.get("/calendar", async (c) => {
|
|
5045
|
+
const customer = await requireBrowserCustomer(c);
|
|
5046
|
+
if (!customer) {
|
|
5047
|
+
setLoginReturnPath(c, currentPathWithSearch(c));
|
|
5048
|
+
return redirect(c, "/login");
|
|
5049
|
+
}
|
|
5050
|
+
const flockPosterApiKey = customer.flockposterApiKey?.trim() || null;
|
|
5051
|
+
const emailChannels = await getEmailPseudoChannels(customer.id, c);
|
|
5052
|
+
return c.html(renderReskinCalendar({
|
|
5053
|
+
userId: customer.id,
|
|
5054
|
+
currentAccountId: customer.id,
|
|
5055
|
+
name: customer.name,
|
|
5056
|
+
email: customer.email,
|
|
5057
|
+
channels: emailChannels.map((channel) => ({
|
|
5058
|
+
id: channel.id,
|
|
5059
|
+
name: channel.title,
|
|
5060
|
+
handle: channel.handle,
|
|
5061
|
+
platform: channel.platform,
|
|
5062
|
+
avatarUrl: channel.avatarUrl,
|
|
5063
|
+
platformIconUrl: channel.platformIconUrl
|
|
5064
|
+
})),
|
|
5065
|
+
posts: [],
|
|
5066
|
+
loadEndpoint: withAccountQuery("/calendar/feed", customer.id),
|
|
5067
|
+
hasFlockPosterKey: Boolean(flockPosterApiKey || emailChannels.length),
|
|
5068
|
+
connectChannelsHref: "/settings?tab=channels&connect=flockposter"
|
|
5069
|
+
}));
|
|
5070
|
+
});
|
|
5071
|
+
// Job runs were renamed and folded into the Library "Logs" tab.
|
|
5072
|
+
app.get("/job-runs", (c) => redirect(c, "/library/logs"));
|
|
5073
|
+
app.get("/agency", async (c) => {
|
|
5074
|
+
const customer = await requireBrowserCustomer(c);
|
|
5075
|
+
return c.html(renderReskinAgency({
|
|
5076
|
+
userId: customer?.id ?? null,
|
|
5077
|
+
currentAccountId: customer?.id ?? null,
|
|
5078
|
+
name: customer?.name ?? null,
|
|
5079
|
+
email: customer?.email ?? null,
|
|
5080
|
+
loggedIn: Boolean(customer)
|
|
5081
|
+
}));
|
|
5082
|
+
});
|
|
5083
|
+
app.get("/pricing", async (c) => {
|
|
5084
|
+
const customer = await getOptionalPreferredBrowserCustomer(c);
|
|
5085
|
+
return c.html(renderReskinPricing({ email: customer?.email ?? "" }));
|
|
5086
|
+
});
|
|
5087
|
+
app.get("/login", (c) => c.html(renderReskinLogin({ mode: getLoginMode(c.req.query("mode")), routePrefix: "/login" })));
|
|
5088
|
+
app.post("/login/password", async (c) => {
|
|
5089
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
5090
|
+
const routePrefix = "/login";
|
|
5091
|
+
const parsed = passwordLoginSchema.safeParse(await parseFormBody(c));
|
|
5092
|
+
if (!parsed.success)
|
|
5093
|
+
return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter a valid email and password." }), 400);
|
|
5094
|
+
try {
|
|
5095
|
+
const result = await auth.authenticateWithPassword(parsed.data.email, parsed.data.password);
|
|
5096
|
+
if (result.status === "pricing")
|
|
5097
|
+
return redirect(c, "/pricing");
|
|
5098
|
+
setBrowserSession(c, result.customer);
|
|
5099
|
+
return redirect(c, resolvePostLoginRedirect(c, result.customer));
|
|
5100
|
+
}
|
|
5101
|
+
catch (error) {
|
|
5102
|
+
return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, error: error instanceof Error ? error.message : "Unable to login." }), 401);
|
|
5103
|
+
}
|
|
5104
|
+
});
|
|
5105
|
+
app.post("/login/otp/request", async (c) => {
|
|
5106
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
5107
|
+
const routePrefix = "/login";
|
|
5108
|
+
const parsed = otpRequestSchema.safeParse(await parseFormBody(c));
|
|
5109
|
+
if (!parsed.success)
|
|
5110
|
+
return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter a valid email address." }), 400);
|
|
5111
|
+
try {
|
|
5112
|
+
const result = await auth.requestOtp(parsed.data.email);
|
|
5113
|
+
const message = result.delivery === "console" && result.code
|
|
5114
|
+
? `Local dev mode: OTP is ${result.code}.`
|
|
5115
|
+
: "OTP sent. Enter the code to continue.";
|
|
5116
|
+
return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, otpSent: true, message }));
|
|
5117
|
+
}
|
|
5118
|
+
catch (error) {
|
|
5119
|
+
return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, error: error instanceof Error ? error.message : "Unable to send OTP." }), 502);
|
|
5120
|
+
}
|
|
5121
|
+
});
|
|
5122
|
+
app.post("/login/otp/verify", async (c) => {
|
|
5123
|
+
const mode = getLoginMode(c.req.query("mode"));
|
|
5124
|
+
const routePrefix = "/login";
|
|
5125
|
+
const parsed = otpVerifySchema.safeParse(await parseFormBody(c));
|
|
5126
|
+
if (!parsed.success)
|
|
5127
|
+
return c.html(renderReskinLogin({ mode, routePrefix, error: "Enter the email and 6-digit OTP." }), 400);
|
|
5128
|
+
try {
|
|
5129
|
+
const result = await auth.verifyOtpForBrowserLogin(parsed.data.email, parsed.data.code, parsed.data.name);
|
|
5130
|
+
if (result.status === "pricing") {
|
|
5131
|
+
clearBrowserSession(c, result.customer.id);
|
|
5132
|
+
return redirect(c, "/pricing");
|
|
5133
|
+
}
|
|
5134
|
+
setBrowserSession(c, result.customer);
|
|
5135
|
+
return redirect(c, resolvePostLoginRedirect(c, result.customer));
|
|
5136
|
+
}
|
|
5137
|
+
catch (error) {
|
|
5138
|
+
return c.html(renderReskinLogin({ mode, routePrefix, email: parsed.data.email, otpSent: true, error: error instanceof Error ? error.message : "Unable to verify OTP." }), 401);
|
|
5139
|
+
}
|
|
5140
|
+
});
|
|
5141
|
+
app.post("/logout", async (c) => {
|
|
5142
|
+
const formBody = await parseFormBody(c);
|
|
5143
|
+
const customer = await requireBrowserCustomer(c, formBody);
|
|
5144
|
+
if (customer) {
|
|
5145
|
+
clearBrowserSession(c, customer.id);
|
|
5146
|
+
}
|
|
5147
|
+
return redirect(c, "/login");
|
|
5148
|
+
});
|
|
5149
|
+
app.get("/help", async (c) => {
|
|
5150
|
+
const customer = await getOptionalPreferredBrowserCustomer(c);
|
|
5151
|
+
return c.html(renderReskinHelp({
|
|
5152
|
+
account: { isLoggedIn: Boolean(customer), userId: customer?.id ?? null, displayName: customer?.name ?? null, email: customer?.email ?? null },
|
|
5153
|
+
expand: c.req.query("expand") ?? null
|
|
5154
|
+
}));
|
|
5155
|
+
});
|
|
5156
|
+
// The reskin IS the app now — every old /reskin/* URL 301/302s to its promoted
|
|
5157
|
+
// canonical route so existing bookmarks/links keep working. The /reskin gallery
|
|
5158
|
+
// (above) is kept as a dev reference; its cards link to the canonical routes.
|
|
5159
|
+
app.get("/reskin/settings", (c) => redirect(c, "/settings"));
|
|
5160
|
+
app.get("/reskin/library", (c) => redirect(c, "/library/approved"));
|
|
5161
|
+
app.get("/reskin/library/approved", (c) => redirect(c, "/library/approved"));
|
|
5162
|
+
app.get("/reskin/library/raws", (c) => redirect(c, "/library/raws"));
|
|
5163
|
+
app.get("/reskin/library/logs", (c) => redirect(c, "/library/logs"));
|
|
5164
|
+
app.get("/reskin/library/clips", (c) => redirect(c, "/library/raws"));
|
|
5165
|
+
app.get("/reskin/clips", (c) => redirect(c, "/library/raws"));
|
|
5166
|
+
app.get("/reskin/discover", (c) => redirect(c, "/discover"));
|
|
4922
5167
|
app.get("/reskin/discover/:mode", (c) => {
|
|
4923
|
-
const
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
app.get("/reskin/
|
|
4929
|
-
app.get("/reskin/
|
|
4930
|
-
app.get("/reskin/
|
|
4931
|
-
app.get("/reskin/
|
|
4932
|
-
app.get("/reskin/
|
|
4933
|
-
app.get("/reskin/login", (c) => c
|
|
4934
|
-
app.get("/reskin/help", (c) => c.html(renderReskinHelp()));
|
|
5168
|
+
const m = c.req.param("mode");
|
|
5169
|
+
return redirect(c, m === "swipe" || m === "categories" ? `/discover?view=${m}` : "/discover");
|
|
5170
|
+
});
|
|
5171
|
+
app.get("/reskin/chat", (c) => redirect(c, "/chat"));
|
|
5172
|
+
app.get("/reskin/inpaint", (c) => redirect(c, "/inpaint"));
|
|
5173
|
+
app.get("/reskin/calendar", (c) => redirect(c, "/calendar"));
|
|
5174
|
+
app.get("/reskin/job-runs", (c) => redirect(c, "/library/logs"));
|
|
5175
|
+
app.get("/reskin/agency", (c) => redirect(c, "/agency"));
|
|
5176
|
+
app.get("/reskin/pricing", (c) => redirect(c, "/pricing"));
|
|
5177
|
+
app.get("/reskin/help", (c) => redirect(c, "/help"));
|
|
5178
|
+
app.get("/reskin/login", (c) => redirect(c, "/login"));
|
|
4935
5179
|
app.get("/discover", async (c) => renderApprovedHomepage(c));
|
|
4936
5180
|
app.get("/editor/:templateId", async (c) => {
|
|
4937
5181
|
const templateId = c.req.param("templateId");
|
|
@@ -12134,9 +12378,21 @@ app.use(`${USER_PREFIX}/me/*`, requireAuth);
|
|
|
12134
12378
|
// is the public HTML gallery page (like /discover vs /discover/feed). Hono's
|
|
12135
12379
|
// `/clips/*` also matches the bare `/clips`, so let that one path through
|
|
12136
12380
|
// unauthenticated.
|
|
12137
|
-
const isClipsGalleryPage = (c) => c.req.path ===
|
|
12138
|
-
app.use(`${
|
|
12139
|
-
app.use(`${
|
|
12381
|
+
const isClipsGalleryPage = (c) => c.req.path === RAWS_PREFIX || c.req.path === `${RAWS_PREFIX}/`;
|
|
12382
|
+
app.use(`${RAWS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : requireAuth(c, next)));
|
|
12383
|
+
app.use(`${RAWS_PREFIX}/*`, (c, next) => (isClipsGalleryPage(c) ? next() : captureApiCallHistory(c, next)));
|
|
12384
|
+
// Deprecated /clips/* → /raws/* alias: transparently redispatch legacy paths so
|
|
12385
|
+
// existing skills, editor-chat prompts, and the devcli keep working during the
|
|
12386
|
+
// clips→raws rename. Remove once all callers emit /raws.
|
|
12387
|
+
const rawsCompatAlias = (c) => {
|
|
12388
|
+
const url = new URL(c.req.url);
|
|
12389
|
+
url.pathname = RAWS_PREFIX + url.pathname.slice(CLIPS_COMPAT_PREFIX.length);
|
|
12390
|
+
// Redispatch through the app with just the rewritten request — do NOT forward
|
|
12391
|
+
// c.executionCtx (its getter throws in the Node adapter when absent).
|
|
12392
|
+
return app.fetch(new Request(url.toString(), c.req.raw));
|
|
12393
|
+
};
|
|
12394
|
+
app.all(CLIPS_COMPAT_PREFIX, rawsCompatAlias);
|
|
12395
|
+
app.all(`${CLIPS_COMPAT_PREFIX}/*`, rawsCompatAlias);
|
|
12140
12396
|
app.use(AGENCY_PREFIX, requireAuth);
|
|
12141
12397
|
app.use(`${AGENCY_PREFIX}/*`, requireAuth);
|
|
12142
12398
|
app.use(`${API_PREFIX}/rate-limit-status`, requireAuth);
|
|
@@ -17045,7 +17301,7 @@ function clipKeywordMatch(clip, q) {
|
|
|
17045
17301
|
// GET /clips — the Clips gallery, rendered in the same shell as the rest of the
|
|
17046
17302
|
// frontend and reached via the Library page's "Finished / Clips" toggle. The
|
|
17047
17303
|
// client fetches /clips/feed with the session cookie (mirrors /discover vs /feed).
|
|
17048
|
-
app.get(
|
|
17304
|
+
app.get(RAWS_PREFIX, async (c) => {
|
|
17049
17305
|
const customer = await getPreferredBrowserCustomer(c);
|
|
17050
17306
|
if (!customer) {
|
|
17051
17307
|
return redirect(c, "/login");
|
|
@@ -17059,7 +17315,7 @@ app.get(CLIPS_PREFIX, async (c) => {
|
|
|
17059
17315
|
}));
|
|
17060
17316
|
});
|
|
17061
17317
|
// GET /clips/feed — the caller's clip library as JSON (optional ?source, ?q, ?limit).
|
|
17062
|
-
app.get(`${
|
|
17318
|
+
app.get(`${RAWS_PREFIX}/feed`, async (c) => {
|
|
17063
17319
|
const customer = requireCustomer(c);
|
|
17064
17320
|
const limit = clampPageLimit(Number(c.req.query("limit") || "60"), 200);
|
|
17065
17321
|
const q = c.req.query("q")?.trim();
|
|
@@ -17072,7 +17328,7 @@ app.get(`${CLIPS_PREFIX}/feed`, async (c) => {
|
|
|
17072
17328
|
return c.json({ clips: await Promise.all(clips.map(serializeClip)) });
|
|
17073
17329
|
});
|
|
17074
17330
|
// POST /clips/search — hybrid structured + semantic search.
|
|
17075
|
-
app.post(`${
|
|
17331
|
+
app.post(`${RAWS_PREFIX}/search`, async (c) => {
|
|
17076
17332
|
const customer = requireCustomer(c);
|
|
17077
17333
|
try {
|
|
17078
17334
|
const body = (await c.req.json().catch(() => ({})));
|
|
@@ -17120,7 +17376,7 @@ function normalizeMatchScene(s, index) {
|
|
|
17120
17376
|
}
|
|
17121
17377
|
// POST /clips/match — find the top library clips that could REPLACE one scene.
|
|
17122
17378
|
// Body: a scene annotation, or {criteria, embedding_text|text, limit}.
|
|
17123
|
-
app.post(`${
|
|
17379
|
+
app.post(`${RAWS_PREFIX}/match`, async (c) => {
|
|
17124
17380
|
const customer = requireCustomer(c);
|
|
17125
17381
|
try {
|
|
17126
17382
|
const body = (await c.req.json().catch(() => ({})));
|
|
@@ -17143,7 +17399,7 @@ app.post(`${CLIPS_PREFIX}/match`, async (c) => {
|
|
|
17143
17399
|
// POST /clips/match-scenes — batch: take a decomposed video's scene annotations
|
|
17144
17400
|
// and return the top replacement-clip matches per scene (one library read).
|
|
17145
17401
|
// Body: { scenes | annotations: SceneAnnotation[], limit? }.
|
|
17146
|
-
app.post(`${
|
|
17402
|
+
app.post(`${RAWS_PREFIX}/match-scenes`, async (c) => {
|
|
17147
17403
|
const customer = requireCustomer(c);
|
|
17148
17404
|
try {
|
|
17149
17405
|
const body = (await c.req.json());
|
|
@@ -17184,7 +17440,7 @@ app.post(`${CLIPS_PREFIX}/match-scenes`, async (c) => {
|
|
|
17184
17440
|
// pipeline runs the hunt and GET /clips/scan/:scanId polls it. Billing is AWS
|
|
17185
17441
|
// compute only (clip_scan_lambda + step_functions_standard) — the AI spend is
|
|
17186
17442
|
// the caller's own provider key (BYOK) and is never wallet-billed.
|
|
17187
|
-
app.post(`${
|
|
17443
|
+
app.post(`${RAWS_PREFIX}/scan`, async (c) => {
|
|
17188
17444
|
const customer = requireCustomer(c);
|
|
17189
17445
|
try {
|
|
17190
17446
|
const body = (await c.req.json());
|
|
@@ -17503,7 +17759,7 @@ app.post(`${CLIPS_PREFIX}/scan`, async (c) => {
|
|
|
17503
17759
|
// GET /clips/scan-options — what the Import Source modal can run a hunt with:
|
|
17504
17760
|
// which BYOK providers have saved keys, whether a local agent CLI is available
|
|
17505
17761
|
// (vidfarm serve boxes only), what "auto" resolves to, and where the hunt runs.
|
|
17506
|
-
app.get(`${
|
|
17762
|
+
app.get(`${RAWS_PREFIX}/scan-options`, async (c) => {
|
|
17507
17763
|
const customer = requireCustomer(c);
|
|
17508
17764
|
const keys = await loadCustomerVisionKeys(customer.id);
|
|
17509
17765
|
const cloudPipeline = Boolean(config.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN);
|
|
@@ -17748,7 +18004,7 @@ async function mirrorUpstreamClipsToLocal(input) {
|
|
|
17748
18004
|
await clipRecords.putScan({ ...(current ?? scan), status: "complete", updated_at: nowIso() });
|
|
17749
18005
|
}
|
|
17750
18006
|
// GET /clips/scan/:scanId — poll scan status.
|
|
17751
|
-
app.get(`${
|
|
18007
|
+
app.get(`${RAWS_PREFIX}/scan/:scanId`, async (c) => {
|
|
17752
18008
|
const customer = requireCustomer(c);
|
|
17753
18009
|
let scan = await clipRecords.getScan(customer.id, c.req.param("scanId"));
|
|
17754
18010
|
if (!scan)
|
|
@@ -17786,17 +18042,17 @@ app.get(`${CLIPS_PREFIX}/scan/:scanId`, async (c) => {
|
|
|
17786
18042
|
return c.json({ scan, source });
|
|
17787
18043
|
});
|
|
17788
18044
|
// GET /clips/sources — scanned source videos.
|
|
17789
|
-
app.get(`${
|
|
18045
|
+
app.get(`${RAWS_PREFIX}/sources`, async (c) => {
|
|
17790
18046
|
const customer = requireCustomer(c);
|
|
17791
18047
|
return c.json({ sources: await clipRecords.listSources(customer.id) });
|
|
17792
18048
|
});
|
|
17793
18049
|
// GET /clips/presets — built-in + saved presets.
|
|
17794
|
-
app.get(`${
|
|
18050
|
+
app.get(`${RAWS_PREFIX}/presets`, async (c) => {
|
|
17795
18051
|
const customer = requireCustomer(c);
|
|
17796
18052
|
return c.json({ presets: await clipRecords.listPresets(customer.id) });
|
|
17797
18053
|
});
|
|
17798
18054
|
// POST /clips/presets — save a custom preset from criteria or a NL query.
|
|
17799
|
-
app.post(`${
|
|
18055
|
+
app.post(`${RAWS_PREFIX}/presets`, async (c) => {
|
|
17800
18056
|
const customer = requireCustomer(c);
|
|
17801
18057
|
try {
|
|
17802
18058
|
const body = (await c.req.json());
|
|
@@ -17824,7 +18080,7 @@ app.post(`${CLIPS_PREFIX}/presets`, async (c) => {
|
|
|
17824
18080
|
}
|
|
17825
18081
|
});
|
|
17826
18082
|
// DELETE /clips/presets/:presetId — delete a saved (non-builtin) preset.
|
|
17827
|
-
app.delete(`${
|
|
18083
|
+
app.delete(`${RAWS_PREFIX}/presets/:presetId`, async (c) => {
|
|
17828
18084
|
const customer = requireCustomer(c);
|
|
17829
18085
|
const id = c.req.param("presetId");
|
|
17830
18086
|
if (id.startsWith("preset_") && BUILTIN_CLIP_PRESET_IDS.has(id)) {
|
|
@@ -17834,7 +18090,7 @@ app.delete(`${CLIPS_PREFIX}/presets/:presetId`, async (c) => {
|
|
|
17834
18090
|
return c.json({ ok: true });
|
|
17835
18091
|
});
|
|
17836
18092
|
// GET /clips/:clipId/download — presigned redirect to the clip mp4.
|
|
17837
|
-
app.get(`${
|
|
18093
|
+
app.get(`${RAWS_PREFIX}/:clipId/download`, async (c) => {
|
|
17838
18094
|
const customer = requireCustomer(c);
|
|
17839
18095
|
const clip = await clipRecords.getClip(customer.id, c.req.param("clipId"));
|
|
17840
18096
|
if (!clip)
|
|
@@ -17845,7 +18101,7 @@ app.get(`${CLIPS_PREFIX}/:clipId/download`, async (c) => {
|
|
|
17845
18101
|
return redirect(c, url, 302);
|
|
17846
18102
|
});
|
|
17847
18103
|
// GET /clips/:clipId — one clip (+ presigned view/thumbnail urls).
|
|
17848
|
-
app.get(`${
|
|
18104
|
+
app.get(`${RAWS_PREFIX}/:clipId`, async (c) => {
|
|
17849
18105
|
const customer = requireCustomer(c);
|
|
17850
18106
|
const clip = await clipRecords.getClip(customer.id, c.req.param("clipId"));
|
|
17851
18107
|
if (!clip)
|
|
@@ -17853,7 +18109,7 @@ app.get(`${CLIPS_PREFIX}/:clipId`, async (c) => {
|
|
|
17853
18109
|
return c.json({ clip: await serializeClip(clip) });
|
|
17854
18110
|
});
|
|
17855
18111
|
// DELETE /clips/:clipId — remove a clip record (+ vector).
|
|
17856
|
-
app.delete(`${
|
|
18112
|
+
app.delete(`${RAWS_PREFIX}/:clipId`, async (c) => {
|
|
17857
18113
|
const customer = requireCustomer(c);
|
|
17858
18114
|
await clipRecords.deleteClip(customer.id, c.req.param("clipId"));
|
|
17859
18115
|
return c.json({ ok: true });
|
package/dist/src/cli.js
CHANGED
|
@@ -168,22 +168,22 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
168
168
|
original vs decomposed (caption-free),
|
|
169
169
|
non-billing (alias: ghostcut)
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
Raws (the third library — mine long-form video into a reusable raws store):
|
|
172
|
+
raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
|
|
173
173
|
Detect → tag/transcribe → embed → persist your local claude/codex CLI
|
|
174
174
|
to ~/.vidfarm/clips.db. (keys are fallback)
|
|
175
175
|
--range "MM:SS-MM:SS" Only hunt these source windows (repeatable; big cost saver)
|
|
176
176
|
--duration <sec> Target clip length as a SOFT band (10→5-20s, 30→20-40s)
|
|
177
177
|
--aspect 9:16|16:9|4:3|1:1 Crop clips (also: vertical/horizontal/square) [--crop-focus <f>]
|
|
178
178
|
--no-text Prefer scenes WITHOUT captions/on-screen text (selection only)
|
|
179
|
-
--cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /
|
|
179
|
+
--cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /raws/scan (+ poll)
|
|
180
180
|
(uploads to your temp folder — 30-day TTL — bills AWS compute only)
|
|
181
181
|
--dry-run Show the estimated cost and stop (over $1 needs --yes/confirm)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(run 'vidfarm
|
|
182
|
+
raws list List the local raws library → GET /raws
|
|
183
|
+
raws search "<text>" Hybrid structured + semantic search → POST /raws/search
|
|
184
|
+
raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
|
|
185
|
+
raws export <ids...> --to <dir> Copy raw mp4s + metadata out
|
|
186
|
+
(run 'vidfarm raws' for the full raws help)
|
|
187
187
|
|
|
188
188
|
Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
|
|
189
189
|
tts "<text>" Text → narration audio file. LOCAL: your OPENAI/GEMINI/
|
|
@@ -528,6 +528,7 @@ async function main() {
|
|
|
528
528
|
case "annotate-file":
|
|
529
529
|
await runAnnotateFileCommand(rest);
|
|
530
530
|
return;
|
|
531
|
+
case "raws":
|
|
531
532
|
case "clips":
|
|
532
533
|
await runClipsCommand(rest);
|
|
533
534
|
return;
|
|
@@ -722,7 +723,7 @@ async function runServeCommand(argv) {
|
|
|
722
723
|
// render_target="cloud" upstream handoff (the editor's "Render in Cloud").
|
|
723
724
|
process.env.VIDFARM_JOB_STATE_MACHINE_ARN = "";
|
|
724
725
|
// Same trap for clip hunts: a dev-shell .env carrying the staging clip-scan
|
|
725
|
-
// ARN would route this box's /
|
|
726
|
+
// ARN would route this box's /raws/scan to the cloud Step Functions. serve
|
|
726
727
|
// hunts are LOCAL-FIRST (local agent CLI / BYOK keys); the upstream cloud is
|
|
727
728
|
// the explicit backup inside the scan route itself.
|
|
728
729
|
process.env.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN = "";
|
|
@@ -2256,7 +2257,7 @@ async function runGenerateCommand(argv) {
|
|
|
2256
2257
|
}
|
|
2257
2258
|
}
|
|
2258
2259
|
// ── Speech: tts / stt ────────────────────────────────────────────────────────
|
|
2259
|
-
// LOCAL-FIRST like `
|
|
2260
|
+
// LOCAL-FIRST like `raws scan`: both commands default to running on the
|
|
2260
2261
|
// user's OWN AI key (GEMINI_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY or
|
|
2261
2262
|
// the --*-key flags) via src/services/speech.ts — local ffmpeg demux, direct
|
|
2262
2263
|
// provider call, no cloud job, no wallet. The REST primitive routes
|