@appeeky/spec 0.0.0-stage → 0.2.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/.github/assets/cover.gif +0 -0
- package/LICENSE +110 -0
- package/README.md +636 -2
- package/agents/appeeky-ads.md +50 -0
- package/agents/appeeky-aso.md +58 -0
- package/agents/appeeky-releases.md +47 -0
- package/agents/appeeky-reviews.md +53 -0
- package/agents/appeeky-social.md +54 -0
- package/agents/appeeky-ugc.md +56 -0
- package/agents/cofounder.md +36 -0
- package/dist/ads.d.ts +42 -0
- package/dist/ads.js +71 -0
- package/dist/agents.d.ts +47 -0
- package/dist/agents.js +88 -0
- package/dist/appeeky-api.d.ts +62 -0
- package/dist/appeeky-api.js +240 -0
- package/dist/attribution-setup.d.ts +28 -0
- package/dist/attribution-setup.js +65 -0
- package/dist/attribution.d.ts +121 -0
- package/dist/attribution.js +170 -0
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +4 -0
- package/dist/category.d.ts +71 -0
- package/dist/category.js +198 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +1960 -0
- package/dist/competitors.d.ts +74 -0
- package/dist/competitors.js +143 -0
- package/dist/control.d.ts +21 -0
- package/dist/control.js +53 -0
- package/dist/core.d.ts +15 -0
- package/dist/core.js +15 -0
- package/dist/coverage.d.ts +46 -0
- package/dist/coverage.js +115 -0
- package/dist/data.d.ts +48 -0
- package/dist/data.js +298 -0
- package/dist/dataforseo.d.ts +139 -0
- package/dist/dataforseo.js +354 -0
- package/dist/diff.d.ts +11 -0
- package/dist/diff.js +33 -0
- package/dist/experiments.d.ts +115 -0
- package/dist/experiments.js +85 -0
- package/dist/files.d.ts +27 -0
- package/dist/files.js +129 -0
- package/dist/history.d.ts +43 -0
- package/dist/history.js +55 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +24 -0
- package/dist/limits.d.ts +15 -0
- package/dist/limits.js +30 -0
- package/dist/lint.d.ts +16 -0
- package/dist/lint.js +159 -0
- package/dist/loops.d.ts +37 -0
- package/dist/loops.js +197 -0
- package/dist/measure.d.ts +40 -0
- package/dist/measure.js +159 -0
- package/dist/metrics.d.ts +290 -0
- package/dist/metrics.js +254 -0
- package/dist/output.d.ts +21 -0
- package/dist/output.js +79 -0
- package/dist/project.d.ts +19 -0
- package/dist/project.js +35 -0
- package/dist/report.d.ts +10 -0
- package/dist/report.js +113 -0
- package/dist/reviews.d.ts +130 -0
- package/dist/reviews.js +287 -0
- package/dist/schema.d.ts +612 -0
- package/dist/schema.js +253 -0
- package/dist/screenshots.d.ts +23 -0
- package/dist/screenshots.js +116 -0
- package/dist/search-ads.d.ts +137 -0
- package/dist/search-ads.js +354 -0
- package/dist/seo.d.ts +87 -0
- package/dist/seo.js +391 -0
- package/dist/setup.d.ts +45 -0
- package/dist/setup.js +259 -0
- package/dist/social.d.ts +129 -0
- package/dist/social.js +189 -0
- package/dist/storefronts.d.ts +15 -0
- package/dist/storefronts.js +89 -0
- package/dist/stores/app-store.d.ts +33 -0
- package/dist/stores/app-store.js +159 -0
- package/dist/stores/google-play.d.ts +13 -0
- package/dist/stores/google-play.js +61 -0
- package/dist/stores/types.d.ts +34 -0
- package/dist/stores/types.js +2 -0
- package/dist/suggest.d.ts +73 -0
- package/dist/suggest.js +135 -0
- package/dist/text.d.ts +18 -0
- package/dist/text.js +55 -0
- package/dist/transport.d.ts +99 -0
- package/dist/transport.js +170 -0
- package/dist/ugc-core.js +1 -0
- package/dist/ugc-local.d.ts +116 -0
- package/dist/ugc-local.js +174 -0
- package/dist/ugc.d.ts +149 -0
- package/dist/ugc.js +111 -0
- package/dist/verify.d.ts +70 -0
- package/dist/verify.js +134 -0
- package/dist/visibility-core.js +96 -0
- package/dist/visibility-local.d.ts +159 -0
- package/dist/visibility-local.js +246 -0
- package/dist/visibility.d.ts +152 -0
- package/dist/visibility.js +239 -0
- package/package.json +80 -4
- package/skills/appeeky-spec/SKILL.md +142 -0
- package/skills/appeeky-spec-experiments/SKILL.md +93 -0
- package/skills/appeeky-spec-localize/SKILL.md +52 -0
- package/skills/appeeky-spec-review/SKILL.md +75 -0
- package/skills/appeeky-spec-targets/SKILL.md +66 -0
- package/skills/appeeky-spec-visibility/SKILL.md +106 -0
package/dist/text.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Text helpers shared by the lint rules and the coverage report. */
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
/** UTF-8 byte length: the unit Apple uses for the 100-byte keyword field. */
|
|
4
|
+
export function byteLength(value) {
|
|
5
|
+
return encoder.encode(value).length;
|
|
6
|
+
}
|
|
7
|
+
/** Character count as the stores count it (user-perceived characters, not UTF-16 units). */
|
|
8
|
+
export function charLength(value) {
|
|
9
|
+
return [...value].length;
|
|
10
|
+
}
|
|
11
|
+
export function normalize(value) {
|
|
12
|
+
return value.normalize("NFKC").toLocaleLowerCase();
|
|
13
|
+
}
|
|
14
|
+
/** Lowercased words; punctuation and symbols split words, apostrophes inside a word are kept. */
|
|
15
|
+
export function words(value) {
|
|
16
|
+
if (!value)
|
|
17
|
+
return [];
|
|
18
|
+
return (normalize(value)
|
|
19
|
+
.replace(/[’']/g, "'")
|
|
20
|
+
.match(/[\p{L}\p{N}]+(?:'[\p{L}\p{N}]+)*/gu) ?? []);
|
|
21
|
+
}
|
|
22
|
+
/** Entries of an App Store keyword field, as written (trimmed, empty ones dropped). */
|
|
23
|
+
export function keywordEntries(field) {
|
|
24
|
+
if (!field)
|
|
25
|
+
return [];
|
|
26
|
+
return field
|
|
27
|
+
.split(",")
|
|
28
|
+
.map((s) => s.trim())
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
/** Naive English singular form, only used to flag likely plural pairs. */
|
|
32
|
+
export function singular(word) {
|
|
33
|
+
if (word.length > 4 && word.endsWith("ies"))
|
|
34
|
+
return `${word.slice(0, -3)}y`;
|
|
35
|
+
if (word.length > 3 && word.endsWith("es") && /(s|x|z|ch|sh)es$/.test(word))
|
|
36
|
+
return word.slice(0, -2);
|
|
37
|
+
if (word.length > 3 && word.endsWith("s") && !word.endsWith("ss"))
|
|
38
|
+
return word.slice(0, -1);
|
|
39
|
+
return word;
|
|
40
|
+
}
|
|
41
|
+
export function isEnglish(locale) {
|
|
42
|
+
return locale.toLowerCase().startsWith("en");
|
|
43
|
+
}
|
|
44
|
+
export function unique(items) {
|
|
45
|
+
return [...new Set(items)];
|
|
46
|
+
}
|
|
47
|
+
/** Short English words that do not decide a search; ignored when a phrase has other words. */
|
|
48
|
+
export const STOPWORDS = new Set(["a", "an", "the", "and", "or", "for", "to", "of", "in", "on", "with", "my", "your", "by", "at", "app", "apps"]);
|
|
49
|
+
/** Words of a target that matter for coverage: stopwords drop out unless they are all there is. */
|
|
50
|
+
export function keyWords(value) {
|
|
51
|
+
const all = words(value);
|
|
52
|
+
const kept = all.filter((w) => !STOPWORDS.has(w));
|
|
53
|
+
return kept.length > 0 ? kept : all;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { StoreId, TransportMode } from "./schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Both store CLIs expose the same named operations locally (`appstore ops call`,
|
|
4
|
+
* `gps call`) and on Appeeky's hosted gateways (`POST /v1/ops/<name>`), so a
|
|
5
|
+
* store adapter only needs `call(op, args)`.
|
|
6
|
+
*/
|
|
7
|
+
export interface OpsTransport {
|
|
8
|
+
readonly kind: "local" | "hosted";
|
|
9
|
+
readonly label: string;
|
|
10
|
+
call<T = unknown>(op: string, args: Record<string, unknown>, options?: {
|
|
11
|
+
write?: boolean;
|
|
12
|
+
}): Promise<T>;
|
|
13
|
+
}
|
|
14
|
+
export declare class TransportError extends Error {
|
|
15
|
+
readonly op?: string | undefined;
|
|
16
|
+
constructor(message: string, op?: string | undefined);
|
|
17
|
+
}
|
|
18
|
+
interface StoreCli {
|
|
19
|
+
bin: string;
|
|
20
|
+
npmPackage: string;
|
|
21
|
+
/** argv before the op name, e.g. `ops call` vs `call`. */
|
|
22
|
+
callArgs: string[];
|
|
23
|
+
/** Flag that picks a named account or profile. */
|
|
24
|
+
accountFlag: string;
|
|
25
|
+
hostedUrl: string;
|
|
26
|
+
writeHeader: string;
|
|
27
|
+
setupHint: string;
|
|
28
|
+
}
|
|
29
|
+
export declare const STORE_CLIS: Record<StoreId, StoreCli>;
|
|
30
|
+
export declare function onPath(bin: string, env?: NodeJS.ProcessEnv): boolean;
|
|
31
|
+
export type Runner = (command: string, args: string[], env?: Record<string, string>) => Promise<{
|
|
32
|
+
code: number;
|
|
33
|
+
stdout: string;
|
|
34
|
+
stderr: string;
|
|
35
|
+
}>;
|
|
36
|
+
export declare const defaultRunner: Runner;
|
|
37
|
+
export interface LocalCliOptions {
|
|
38
|
+
/** Named account (`appstore --account`) or profile (`gps --profile`). */
|
|
39
|
+
account?: string;
|
|
40
|
+
/** Run the CLI through `npx` when it is not installed. */
|
|
41
|
+
useNpx?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Run a specific executable instead of the CLI on PATH, e.g. an app that
|
|
44
|
+
* bundles the store CLI: `{ file: process.execPath, args: ["/path/to/bin.js"] }`.
|
|
45
|
+
*/
|
|
46
|
+
command?: {
|
|
47
|
+
file: string;
|
|
48
|
+
args: string[];
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Extra environment for the CLI, e.g. credentials:
|
|
52
|
+
* `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_PRIVATE_KEY` or `GPS_CREDENTIALS` (inline JSON).
|
|
53
|
+
*/
|
|
54
|
+
env?: Record<string, string>;
|
|
55
|
+
label?: string;
|
|
56
|
+
runner?: Runner;
|
|
57
|
+
}
|
|
58
|
+
/** Runs operations through the store CLI on this machine, with the user's own key. */
|
|
59
|
+
export declare class LocalCliTransport implements OpsTransport {
|
|
60
|
+
private readonly store;
|
|
61
|
+
private readonly options;
|
|
62
|
+
readonly kind: "local";
|
|
63
|
+
readonly label: string;
|
|
64
|
+
constructor(store: StoreId, options?: LocalCliOptions);
|
|
65
|
+
call<T>(op: string, args: Record<string, unknown>, options?: {
|
|
66
|
+
write?: boolean;
|
|
67
|
+
}): Promise<T>;
|
|
68
|
+
}
|
|
69
|
+
export type Fetcher = typeof fetch;
|
|
70
|
+
/** Runs operations on Appeeky's hosted gateway with an Appeeky API key; the store key never leaves Appeeky. */
|
|
71
|
+
export declare class HostedTransport implements OpsTransport {
|
|
72
|
+
private readonly store;
|
|
73
|
+
private readonly apiKey;
|
|
74
|
+
private readonly options;
|
|
75
|
+
readonly kind: "hosted";
|
|
76
|
+
readonly label: string;
|
|
77
|
+
constructor(store: StoreId, apiKey: string, options?: {
|
|
78
|
+
baseUrl?: string;
|
|
79
|
+
fetcher?: Fetcher;
|
|
80
|
+
});
|
|
81
|
+
call<T>(op: string, args: Record<string, unknown>, options?: {
|
|
82
|
+
write?: boolean;
|
|
83
|
+
}): Promise<T>;
|
|
84
|
+
}
|
|
85
|
+
export interface TransportOptions {
|
|
86
|
+
via: TransportMode;
|
|
87
|
+
account?: string;
|
|
88
|
+
apiKey?: string;
|
|
89
|
+
env?: NodeJS.ProcessEnv;
|
|
90
|
+
runner?: Runner;
|
|
91
|
+
fetcher?: Fetcher;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* `auto` prefers the local CLI when it is installed (your key, nothing leaves
|
|
95
|
+
* your machine), then the hosted gateway when an Appeeky API key is set, and
|
|
96
|
+
* finally the CLI through npx.
|
|
97
|
+
*/
|
|
98
|
+
export declare function createTransport(store: StoreId, options: TransportOptions): OpsTransport;
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
export class TransportError extends Error {
|
|
5
|
+
op;
|
|
6
|
+
constructor(message, op) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.op = op;
|
|
9
|
+
this.name = "TransportError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const STORE_CLIS = {
|
|
13
|
+
"app-store": {
|
|
14
|
+
bin: "appstore",
|
|
15
|
+
npmPackage: "@appeeky/app-store-connect-cli",
|
|
16
|
+
callArgs: ["ops", "call"],
|
|
17
|
+
accountFlag: "--account",
|
|
18
|
+
hostedUrl: "https://asc-api.appeeky.com",
|
|
19
|
+
writeHeader: "X-ASC-Allow-Writes",
|
|
20
|
+
setupHint: "Save your App Store Connect API key once with the App Store Connect CLI: `npx -p @appeeky/app-store-connect-cli appstore accounts add my-team --key-id … --issuer-id … --private-key-path AuthKey.p8 --default` (github.com/appeeky/app-store-connect-cli). Or set APPEEKY_API_KEY to use your connection in Appeeky.",
|
|
21
|
+
},
|
|
22
|
+
"google-play": {
|
|
23
|
+
bin: "gps",
|
|
24
|
+
npmPackage: "@appeeky/google-play-store-cli",
|
|
25
|
+
callArgs: ["call"],
|
|
26
|
+
accountFlag: "--account",
|
|
27
|
+
hostedUrl: "https://gps-api.appeeky.com",
|
|
28
|
+
writeHeader: "X-GPS-Allow-Writes",
|
|
29
|
+
setupHint: "Save your Google Play service account once with the Google Play CLI: `npx -p @appeeky/google-play-store-cli gps accounts add my-co --credentials service-account.json --default` (github.com/appeeky/google-play-console-cli). Or set APPEEKY_API_KEY to use your connection in Appeeky.",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export function onPath(bin, env = process.env) {
|
|
33
|
+
const exts = process.platform === "win32" ? (env.PATHEXT ?? ".EXE;.CMD").split(";") : [""];
|
|
34
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
35
|
+
if (!dir)
|
|
36
|
+
continue;
|
|
37
|
+
for (const ext of exts)
|
|
38
|
+
if (existsSync(join(dir, bin + ext)))
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
export const defaultRunner = (command, args, env) => new Promise((resolve, reject) => {
|
|
44
|
+
const child = spawn(command, args, {
|
|
45
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
46
|
+
shell: process.platform === "win32",
|
|
47
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
48
|
+
});
|
|
49
|
+
let stdout = "";
|
|
50
|
+
let stderr = "";
|
|
51
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
52
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
53
|
+
child.on("error", reject);
|
|
54
|
+
child.on("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
55
|
+
});
|
|
56
|
+
/** Runs operations through the store CLI on this machine, with the user's own key. */
|
|
57
|
+
export class LocalCliTransport {
|
|
58
|
+
store;
|
|
59
|
+
options;
|
|
60
|
+
kind = "local";
|
|
61
|
+
label;
|
|
62
|
+
constructor(store, options = {}) {
|
|
63
|
+
this.store = store;
|
|
64
|
+
this.options = options;
|
|
65
|
+
const cli = STORE_CLIS[store];
|
|
66
|
+
this.label = options.label ?? (options.useNpx ? `npx ${cli.npmPackage}` : `local ${cli.bin} CLI`);
|
|
67
|
+
}
|
|
68
|
+
async call(op, args, options = {}) {
|
|
69
|
+
const cli = STORE_CLIS[this.store];
|
|
70
|
+
const argv = ["--json"];
|
|
71
|
+
if (this.options.account)
|
|
72
|
+
argv.push(cli.accountFlag, this.options.account);
|
|
73
|
+
if (options.write)
|
|
74
|
+
argv.push("--confirm");
|
|
75
|
+
argv.push(...cli.callArgs, op, JSON.stringify(args));
|
|
76
|
+
const [command, full] = this.options.command
|
|
77
|
+
? [this.options.command.file, [...this.options.command.args, ...argv]]
|
|
78
|
+
: this.options.useNpx
|
|
79
|
+
? ["npx", ["-y", "-p", cli.npmPackage, cli.bin, ...argv]]
|
|
80
|
+
: [cli.bin, argv];
|
|
81
|
+
const run = this.options.runner ?? defaultRunner;
|
|
82
|
+
let result;
|
|
83
|
+
try {
|
|
84
|
+
result = await run(command, full, this.options.env);
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
throw new TransportError(`Could not start ${command}: ${err.message}. ${cli.setupHint}`, op);
|
|
88
|
+
}
|
|
89
|
+
if (result.code !== 0) {
|
|
90
|
+
const detail = (result.stderr || result.stdout).trim().split("\n").slice(-3).join(" ");
|
|
91
|
+
throw new TransportError(`${cli.bin} ${op} failed: ${detail || `exit ${result.code}`}`, op);
|
|
92
|
+
}
|
|
93
|
+
const text = result.stdout.trim();
|
|
94
|
+
if (!text)
|
|
95
|
+
return undefined;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
throw new TransportError(`${cli.bin} ${op} returned output that is not JSON: ${text.slice(0, 200)}`, op);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Runs operations on Appeeky's hosted gateway with an Appeeky API key; the store key never leaves Appeeky. */
|
|
105
|
+
export class HostedTransport {
|
|
106
|
+
store;
|
|
107
|
+
apiKey;
|
|
108
|
+
options;
|
|
109
|
+
kind = "hosted";
|
|
110
|
+
label;
|
|
111
|
+
constructor(store, apiKey, options = {}) {
|
|
112
|
+
this.store = store;
|
|
113
|
+
this.apiKey = apiKey;
|
|
114
|
+
this.options = options;
|
|
115
|
+
this.label = `hosted ${new URL(options.baseUrl ?? STORE_CLIS[store].hostedUrl).host}`;
|
|
116
|
+
}
|
|
117
|
+
async call(op, args, options = {}) {
|
|
118
|
+
const cli = STORE_CLIS[this.store];
|
|
119
|
+
const base = (this.options.baseUrl ?? cli.hostedUrl).replace(/\/$/, "");
|
|
120
|
+
const headers = {
|
|
121
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"User-Agent": "appeeky-spec",
|
|
124
|
+
};
|
|
125
|
+
if (options.write)
|
|
126
|
+
headers[cli.writeHeader] = "true";
|
|
127
|
+
const doFetch = this.options.fetcher ?? fetch;
|
|
128
|
+
let res;
|
|
129
|
+
try {
|
|
130
|
+
res = await doFetch(`${base}/v1/ops/${op}`, { method: "POST", headers, body: JSON.stringify(args) });
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw new TransportError(`Could not reach ${base}: ${err.message}`, op);
|
|
134
|
+
}
|
|
135
|
+
const body = (await res.json().catch(() => null));
|
|
136
|
+
if (!res.ok) {
|
|
137
|
+
const message = body?.error?.message ?? `HTTP ${res.status}`;
|
|
138
|
+
throw new TransportError(`${op} failed on ${new URL(base).host}: ${message}`, op);
|
|
139
|
+
}
|
|
140
|
+
return body?.data;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `auto` prefers the local CLI when it is installed (your key, nothing leaves
|
|
145
|
+
* your machine), then the hosted gateway when an Appeeky API key is set, and
|
|
146
|
+
* finally the CLI through npx.
|
|
147
|
+
*/
|
|
148
|
+
export function createTransport(store, options) {
|
|
149
|
+
const env = options.env ?? process.env;
|
|
150
|
+
const apiKey = options.apiKey ?? env.APPEEKY_API_KEY;
|
|
151
|
+
const cli = STORE_CLIS[store];
|
|
152
|
+
const local = (useNpx) => new LocalCliTransport(store, { account: options.account, useNpx, runner: options.runner });
|
|
153
|
+
const hostedBase = store === "app-store" ? env.APPEEKY_ASC_URL : env.APPEEKY_GPS_URL;
|
|
154
|
+
const hosted = (key) => new HostedTransport(store, key, { baseUrl: hostedBase, fetcher: options.fetcher });
|
|
155
|
+
switch (options.via) {
|
|
156
|
+
case "local":
|
|
157
|
+
return local(!onPath(cli.bin, env));
|
|
158
|
+
case "hosted":
|
|
159
|
+
if (!apiKey)
|
|
160
|
+
throw new TransportError(`Set APPEEKY_API_KEY to use the hosted ${store} gateway.`);
|
|
161
|
+
return hosted(apiKey);
|
|
162
|
+
case "auto":
|
|
163
|
+
if (onPath(cli.bin, env))
|
|
164
|
+
return local(false);
|
|
165
|
+
if (apiKey)
|
|
166
|
+
return hosted(apiKey);
|
|
167
|
+
return local(true);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
//# sourceMappingURL=transport.js.map
|
package/dist/ugc-core.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var r=class extends Error{constructor(o,n,i){super(o);this.code=n;this.status=i;this.name="MediaJobError"}code;status};var C=280;var v=e=>({prompt:e.prompt,duration:`${e.durationSec}s`,resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio,...e.negativePrompt?{negative_prompt:e.negativePrompt}:{},...e.seed!==void 0?{seed:e.seed}:{}}),p=(e,t,o=1)=>Array.from({length:Math.floor((t-e)/o)+1},(n,i)=>e+i*o),s=e=>({silent:e,audio:e}),l=e=>({prompt:e.prompt,image_url:e.imageUrl,duration:String(e.durationSec)}),y=(e,t=!1)=>({prompt:e.prompt,...t?{image_urls:e.imageUrls??[]}:{},num_images:e.count,aspect_ratio:e.aspectRatio,output_format:"png",...e.seed!==void 0?{seed:e.seed}:{}}),Y={"9:16":"portrait_16_9","16:9":"landscape_16_9","1:1":"square_hd","4:5":"portrait_4_3"},V=(e,t=!1)=>({prompt:e.prompt,...t?{image_urls:e.imageUrls??[]}:{},num_images:e.count,image_size:Y[e.aspectRatio],...e.seed!==void 0?{seed:e.seed}:{}}),Q=[{kind:"video",id:"veo-3.1-lite",family:"Veo",nativeAudio:!0,label:"Veo 3.1 Lite",provider:"fal",endpoint:"fal-ai/veo3.1/lite/image-to-video",mode:"image-to-video",description:"Google's cheapest Veo: animates a first frame with native speech and sound.",durations:[4,6,8],resolutions:["720p","1080p"],defaultResolution:"720p",pricePerSecond:{silent:.03,audio:.05},pricePerSecondByResolution:{"1080p":{silent:.05,audio:.08}},mapInput:e=>({...v(e),image_url:e.imageUrl})},{kind:"video",id:"veo-3.1-fast",family:"Veo",nativeAudio:!0,label:"Veo 3.1 Fast",provider:"fal",endpoint:"fal-ai/veo3.1/fast/image-to-video",mode:"image-to-video",description:"Animates a first frame; native speech + lip-sync. Cheapest talking-head option.",durations:[4,6,8],resolutions:["720p","1080p"],defaultResolution:"720p",pricePerSecond:{silent:.1,audio:.15},mapInput:e=>({...v(e),image_url:e.imageUrl})},{kind:"video",id:"veo-3.1",family:"Veo",nativeAudio:!0,label:"Veo 3.1",provider:"fal",endpoint:"fal-ai/veo3.1/image-to-video",mode:"image-to-video",description:"Highest quality image-to-video with native dialogue.",durations:[4,6,8],resolutions:["720p","1080p"],defaultResolution:"720p",pricePerSecond:{silent:.2,audio:.4},mapInput:e=>({...v(e),image_url:e.imageUrl})},{kind:"video",id:"veo-3.1-reference",family:"Veo",nativeAudio:!0,label:"Veo 3.1 Reference",provider:"fal",endpoint:"fal-ai/veo3.1/reference-to-video",mode:"reference-to-video",description:"Keeps a persona consistent from reference images (face, outfit, product).",durations:[8],resolutions:["720p","1080p"],defaultResolution:"720p",pricePerSecond:{silent:.2,audio:.4},mapInput:e=>({...v(e),image_urls:e.referenceImageUrls??[]})},{kind:"video",id:"seedance-2.0",family:"Seedance",nativeAudio:!0,label:"Seedance 2.0",provider:"fal",endpoint:"bytedance/seedance-2.0/image-to-video",mode:"image-to-video",description:"ByteDance's flagship: cinematic motion with native audio, up to 15s.",durations:p(4,15),resolutions:["480p","720p","1080p"],defaultResolution:"720p",pricePerSecond:s(.3034),pricePerSecondByResolution:{"480p":s(.135),"1080p":s(.682)},mapInput:e=>({...l(e),resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio})},{kind:"video",id:"seedance-2.0-fast",family:"Seedance",nativeAudio:!0,label:"Seedance 2.0 Fast",provider:"fal",endpoint:"bytedance/seedance-2.0/fast/image-to-video",mode:"image-to-video",description:"Faster, cheaper Seedance 2.0 with native audio.",durations:p(4,15),resolutions:["480p","720p"],defaultResolution:"720p",pricePerSecond:s(.2419),pricePerSecondByResolution:{"480p":s(.108)},mapInput:e=>({...l(e),resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio})},{kind:"video",id:"seedance-2.0-mini",family:"Seedance",nativeAudio:!0,label:"Seedance 2.0 Mini",provider:"fal",endpoint:"bytedance/seedance-2.0/mini/image-to-video",mode:"image-to-video",description:"The budget Seedance 2.0, still with native audio.",durations:p(4,15),resolutions:["480p","720p"],defaultResolution:"720p",pricePerSecond:s(.1547),pricePerSecondByResolution:{"480p":s(.0721)},mapInput:e=>({...l(e),resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio})},{kind:"video",id:"seedance-2.5",family:"Seedance",nativeAudio:!0,label:"Seedance 2.5",provider:"fal",endpoint:"bytedance/seedance-2.5/image-to-video",mode:"image-to-video",description:"Newest Seedance: highest fidelity and up to 30s. Follows the first frame's shape.",durations:p(4,30),resolutions:["480p","720p","1080p"],defaultResolution:"720p",pricePerSecond:s(.473),pricePerSecondByResolution:{"480p":s(.2205),"1080p":s(1.164)},mapInput:e=>({...l(e),resolution:e.resolution,generate_audio:e.audio})},{kind:"video",id:"seedance-1.5-pro",family:"Seedance",nativeAudio:!0,label:"Seedance 1.5 Pro",provider:"fal",endpoint:"fal-ai/bytedance/seedance/v1.5/pro/image-to-video",mode:"image-to-video",description:"Cheap Seedance with native audio; good for quick drafts.",durations:p(4,12),resolutions:["480p","720p","1080p"],defaultResolution:"720p",pricePerSecond:{silent:.026,audio:.0519},pricePerSecondByResolution:{"480p":{silent:.0116,audio:.0231},"1080p":{silent:.0584,audio:.1167}},mapInput:e=>({...l(e),resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio,...e.seed!==void 0?{seed:e.seed}:{}})},{kind:"video",id:"seedance-1.0-pro-fast",family:"Seedance",nativeAudio:!1,label:"Seedance 1.0 Pro Fast",provider:"fal",endpoint:"fal-ai/bytedance/seedance/v1/pro/fast/image-to-video",mode:"image-to-video",description:"Very cheap silent motion \u2014 b-roll, product shots, ElevenLabs lip-sync bases.",durations:p(2,12),resolutions:["480p","720p","1080p"],defaultResolution:"720p",pricePerSecond:s(.0216),pricePerSecondByResolution:{"480p":s(.0096),"1080p":s(.0486)},mapInput:e=>({...l(e),resolution:e.resolution,aspect_ratio:e.aspectRatio,...e.seed!==void 0?{seed:e.seed}:{}})},{kind:"video",id:"kling-v3-pro",family:"Kling",nativeAudio:!0,label:"Kling 3.0 Pro",provider:"fal",endpoint:"fal-ai/kling-video/v3/pro/image-to-video",mode:"image-to-video",description:"Kling's best: strong motion and faces, native audio, 3\u201315s. Follows the first frame's shape.",durations:p(3,15),resolutions:["1080p"],defaultResolution:"1080p",pricePerSecond:{silent:.112,audio:.168},mapInput:e=>({prompt:e.prompt,start_image_url:e.imageUrl,duration:String(e.durationSec),generate_audio:e.audio,...e.negativePrompt?{negative_prompt:e.negativePrompt}:{}})},{kind:"video",id:"kling-v3-standard",family:"Kling",nativeAudio:!0,label:"Kling 3.0 Standard",provider:"fal",endpoint:"fal-ai/kling-video/v3/standard/image-to-video",mode:"image-to-video",description:"Cheaper Kling 3.0 with native audio.",durations:p(3,15),resolutions:["720p"],defaultResolution:"720p",pricePerSecond:{silent:.084,audio:.126},mapInput:e=>({prompt:e.prompt,start_image_url:e.imageUrl,duration:String(e.durationSec),generate_audio:e.audio,...e.negativePrompt?{negative_prompt:e.negativePrompt}:{}})},{kind:"video",id:"kling-v2.5-turbo-pro",family:"Kling",nativeAudio:!1,label:"Kling 2.5 Turbo Pro",provider:"fal",endpoint:"fal-ai/kling-video/v2.5-turbo/pro/image-to-video",mode:"image-to-video",description:"Fast, silent Kling with very good motion; 5 or 10s.",durations:[5,10],resolutions:["1080p"],defaultResolution:"1080p",pricePerSecond:s(.07),mapInput:e=>({...l(e),...e.negativePrompt?{negative_prompt:e.negativePrompt}:{}})},{kind:"video",id:"hailuo-2.3",family:"Hailuo",nativeAudio:!1,label:"Hailuo 2.3",provider:"fal",endpoint:"fal-ai/minimax/hailuo-2.3/standard/image-to-video",mode:"image-to-video",description:"MiniMax Hailuo: lively, physical motion; silent, 6 or 10s.",durations:[6,10],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:s(.056),mapInput:e=>l(e)},{kind:"video",id:"wan-3.0",family:"Wan",nativeAudio:!0,label:"Wan 3.0",provider:"fal",endpoint:"alibaba/wan-3.0/image-to-video",mode:"image-to-video",description:"Alibaba Wan 3.0 with audio, 2\u201315s.",durations:p(2,15),resolutions:["480p","720p","1080p"],defaultResolution:"720p",pricePerSecond:s(.1),pricePerSecondByResolution:{"480p":s(.05),"1080p":s(.2)},mapInput:e=>({prompt:e.prompt,start_image_url:e.imageUrl,duration:e.durationSec,resolution:e.resolution,aspect_ratio:e.aspectRatio,audio:e.audio,...e.seed!==void 0?{seed:e.seed}:{}})},{kind:"video",id:"ltx-2.3-fast",family:"LTX",nativeAudio:!0,label:"LTX 2.3 Fast",provider:"fal",endpoint:"fal-ai/ltx-2.3/image-to-video/fast",mode:"image-to-video",description:"Lightricks LTX: cheap 1080p with audio, 6\u201320s.",durations:p(6,20,2),resolutions:["1080p"],defaultResolution:"1080p",pricePerSecond:s(.06),mapInput:e=>({prompt:e.prompt,image_url:e.imageUrl,duration:e.durationSec,resolution:e.resolution,aspect_ratio:e.aspectRatio,generate_audio:e.audio})},{kind:"video",id:"sora-2",family:"Sora",nativeAudio:!0,label:"Sora 2",provider:"fal",endpoint:"fal-ai/sora-2/image-to-video",mode:"image-to-video",description:"OpenAI Sora 2: natural scenes and dialogue; always renders audio.",durations:[4,8,12],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:s(.1),mapInput:e=>({prompt:e.prompt,image_url:e.imageUrl,duration:e.durationSec,resolution:"720p",aspect_ratio:e.aspectRatio})},{kind:"video",id:"sora-2-pro",family:"Sora",nativeAudio:!0,label:"Sora 2 Pro",provider:"fal",endpoint:"fal-ai/sora-2/image-to-video/pro",mode:"image-to-video",description:"Higher-fidelity Sora 2, up to 1080p; always renders audio.",durations:[4,8,12],resolutions:["720p","1080p"],defaultResolution:"720p",pricePerSecond:s(.3),pricePerSecondByResolution:{"1080p":s(.5)},mapInput:e=>({prompt:e.prompt,image_url:e.imageUrl,duration:e.durationSec,resolution:e.resolution,aspect_ratio:e.aspectRatio})},{kind:"video",id:"kling-avatar-v2",family:"Kling",nativeAudio:!0,label:"Kling AI Avatar v2",provider:"fal",endpoint:"fal-ai/kling-video/ai-avatar/v2/standard",mode:"audio-to-video",description:"Animates a still from a voice track with lip-sync. Cheapest talking clip; the default for ElevenLabs voices.",durations:[],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:{silent:.0562,audio:.0562},mapInput:e=>({image_url:e.imageUrl,audio_url:e.audioUrl,prompt:e.prompt||"."})},{kind:"video",id:"kling-avatar-v2-pro",family:"Kling",nativeAudio:!0,label:"Kling AI Avatar v2 Pro",provider:"fal",endpoint:"fal-ai/kling-video/ai-avatar/v2/pro",mode:"audio-to-video",description:"Higher-fidelity Kling avatar from a still and a voice track.",durations:[],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:{silent:.115,audio:.115},mapInput:e=>({image_url:e.imageUrl,audio_url:e.audioUrl,prompt:e.prompt||"."})},{kind:"video",id:"omnihuman-v1.5",family:"OmniHuman",nativeAudio:!0,label:"OmniHuman 1.5",provider:"fal",endpoint:"fal-ai/bytedance/omnihuman/v1.5",mode:"audio-to-video",description:"ByteDance OmniHuman: expressive body and face motion that follows the voice.",durations:[],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:{silent:.16,audio:.16},mapInput:e=>({image_url:e.imageUrl,audio_url:e.audioUrl,prompt:e.prompt,resolution:"720p"})},{kind:"video",id:"sync-lipsync-v2",family:"Sync",nativeAudio:!0,label:"Sync Lipsync 2",provider:"fal",endpoint:"fal-ai/sync-lipsync/v2",mode:"lipsync",description:"Re-times a video's mouth to a voice track ($3/min).",durations:[],resolutions:["720p"],defaultResolution:"720p",pricePerSecond:{silent:.05,audio:.05},mapInput:e=>({video_url:e.videoUrl,audio_url:e.audioUrl,model:"lipsync-2",sync_mode:"cut_off"})},{kind:"audio",id:"elevenlabs-tts",label:"ElevenLabs Multilingual v2",provider:"fal",endpoint:"fal-ai/elevenlabs/tts/multilingual-v2",mode:"tts",description:"Natural voiceover in a preset ElevenLabs voice, with word timings for captions.",pricePerKChars:.1,maxChars:2500,mapInput:e=>({text:e.text,voice:e.voice||F,...e.speed?{speed:e.speed}:{},timestamps:!0})},{kind:"audio",id:"lyria2",label:"Lyria 2",provider:"fal",endpoint:"fal-ai/lyria2",mode:"music",description:"Google Lyria 2: a 30-second instrumental bed from a prompt.",pricePerTrack:.1,fixedSeconds:30,mapInput:e=>({prompt:e.prompt,negative_prompt:"vocals, singing, low quality"})},{kind:"audio",id:"stable-audio-25",label:"Stable Audio 2.5",provider:"fal",endpoint:"fal-ai/stable-audio-25/text-to-audio",mode:"music",description:"Stability AI Stable Audio 2.5: music of a chosen length.",pricePerTrack:.2,maxSeconds:190,mapInput:e=>({prompt:e.prompt,seconds_total:e.seconds??30})},{kind:"image",id:"nano-banana",label:"Nano Banana",family:"Nano Banana",provider:"fal",endpoint:"fal-ai/nano-banana",description:"Google Gemini 2.5 Flash Image. Fast, cheap and strong at keeping a creator's face across shots.",maxImages:4,maxReferenceImages:0,pricePerImage:.039,mapInput:e=>({prompt:e.prompt,num_images:e.count,aspect_ratio:e.aspectRatio,output_format:"png",...e.seed!==void 0?{seed:e.seed}:{}}),edit:{endpoint:"fal-ai/nano-banana/edit",maxReferenceImages:6,mapInput:e=>({prompt:e.prompt,image_urls:e.imageUrls??[],num_images:e.count,aspect_ratio:e.aspectRatio,output_format:"png",...e.seed!==void 0?{seed:e.seed}:{}})}},{kind:"image",id:"nano-banana-2",label:"Nano Banana 2",family:"Nano Banana",provider:"fal",endpoint:"fal-ai/nano-banana-2",description:"Newer Gemini image model: sharper detail and text, still keeps faces consistent.",maxImages:4,maxReferenceImages:0,pricePerImage:.08,mapInput:e=>y(e),edit:{endpoint:"fal-ai/nano-banana-2/edit",maxReferenceImages:6,mapInput:e=>y(e,!0)}},{kind:"image",id:"nano-banana-pro",label:"Nano Banana Pro",family:"Nano Banana",provider:"fal",endpoint:"fal-ai/nano-banana-pro",description:"Gemini 3 Pro Image: the most realistic stills and the best text in the frame.",maxImages:4,maxReferenceImages:0,pricePerImage:.15,mapInput:e=>y(e),edit:{endpoint:"fal-ai/nano-banana-pro/edit",maxReferenceImages:6,mapInput:e=>y(e,!0)}},{kind:"image",id:"seedream-4.5",label:"Seedream 4.5",family:"Seedream",provider:"fal",endpoint:"fal-ai/bytedance/seedream/v4.5/text-to-image",description:"ByteDance Seedream: photoreal 2K stills, up to 10 references.",maxImages:4,maxReferenceImages:0,pricePerImage:.04,mapInput:e=>V(e),edit:{endpoint:"fal-ai/bytedance/seedream/v4.5/edit",maxReferenceImages:10,mapInput:e=>V(e,!0)}},{kind:"image",id:"nano-banana-edit",label:"Nano Banana",family:"Nano Banana",hidden:!0,provider:"fal",endpoint:"fal-ai/nano-banana/edit",description:"Consistent-character stills from reference images (persona looks, scenes, product shots).",maxImages:4,maxReferenceImages:6,pricePerImage:.039,mapInput:e=>({prompt:e.prompt,image_urls:e.imageUrls??[],num_images:e.count,aspect_ratio:e.aspectRatio,output_format:"png",...e.seed!==void 0?{seed:e.seed}:{}})}];var F="Rachel",Z=new Map(Q.map(e=>[e.id,e]));function u(e){return Z.get(e)}function q(){let e=Number(process.env.MEDIA_CREDIT_MARKUP);return Number.isFinite(e)&&e>=1?e:1.5}function h(e){return e<=0?0:Math.max(1,Math.ceil(e*100*q()))}function T(e,t){let o=[...e.durations].sort((n,i)=>n-i);return o.find(n=>n>=t)??o[o.length-1]}function ee(e,t){return t&&e.pricePerSecondByResolution?.[t]||e.pricePerSecond}function I(e,t){let o=ee(e,t.resolution)[t.audio&&e.nativeAudio?"audio":"silent"];return Math.round(o*t.durationSec*1e4)/1e4}function N(e,t){return Math.round(e.pricePerImage*t*1e4)/1e4}function f(e,t){let o;try{o=new URL(e)}catch{throw new r(`${t} must be a valid URL`,"INVALID_URL",400)}if(o.protocol!=="https:")throw new r(`${t} must be an https URL`,"INVALID_URL",400);let n=o.hostname.toLowerCase();if(n==="localhost"||n.endsWith(".local")||/^(10|127|169\.254|192\.168|172\.(1[6-9]|2\d|3[01]))\./.test(n))throw new r(`${t} must be a public URL`,"INVALID_URL",400)}function $(e){let t=u(e.modelId);if(!t||t.kind!=="video")throw new r(`Unknown video model "${e.modelId}"`,"INVALID_MODEL",400);if(t.mode==="audio-to-video"||t.mode==="lipsync")return te(t,e);let o=e.prompt?.trim();if(!o)throw new r("prompt is required","MISSING_PROMPT",400);if(o.length>4e3)throw new r("prompt is too long (max 4000 chars)","PROMPT_TOO_LONG",400);let n=e.durationSec??t.durations[t.durations.length-1];if(!t.durations.includes(n))throw new r(`${t.label} takes ${t.durations.join(", ")} second clips`,"INVALID_DURATION",400);let i=e.resolution??t.defaultResolution;if(!t.resolutions.includes(i))throw new r(`${t.label} renders ${t.resolutions.join(", ")}`,"INVALID_RESOLUTION",400);let a=e.aspectRatio??"9:16";if(a!=="9:16"&&a!=="16:9")throw new r("aspectRatio must be 9:16 or 16:9","INVALID_ASPECT_RATIO",400);if(t.mode==="image-to-video"){if(!e.imageUrl)throw new r("imageUrl is required for this model","MISSING_IMAGE",400);f(e.imageUrl,"imageUrl")}else{let g=e.referenceImageUrls??[];if(g.length<1||g.length>3)throw new r("referenceImageUrls needs 1\u20133 images","INVALID_REFERENCES",400);g.forEach((A,U)=>f(A,`referenceImageUrls[${U}]`))}let d={prompt:o,imageUrl:e.imageUrl,referenceImageUrls:e.referenceImageUrls,durationSec:n,resolution:i,aspectRatio:a,audio:t.nativeAudio?e.audio??!0:!1,negativePrompt:e.negativePrompt?.trim()||void 0,seed:e.seed},b=I(t,d);return{kind:"video",model:t,request:d,costUsd:b,credits:h(b)}}function te(e,t){if(!t.audioUrl)throw new r("audioUrl is required for this model","MISSING_AUDIO",400);if(f(t.audioUrl,"audioUrl"),e.mode==="audio-to-video"){if(!t.imageUrl)throw new r("imageUrl is required for this model","MISSING_IMAGE",400);f(t.imageUrl,"imageUrl")}else{if(!t.videoUrl)throw new r("videoUrl is required for lip-sync","MISSING_VIDEO",400);f(t.videoUrl,"videoUrl")}let o=Math.ceil(t.durationSec??8);if(!Number.isFinite(o)||o<1||o>45)throw new r(`Speech must be 1\u2013${45} seconds`,"INVALID_DURATION",400);let n={prompt:(t.prompt??"").trim().slice(0,2e3),imageUrl:t.imageUrl,audioUrl:t.audioUrl,videoUrl:t.videoUrl,durationSec:o,resolution:e.defaultResolution,aspectRatio:t.aspectRatio==="16:9"?"16:9":"9:16",audio:!0},i=I(e,{durationSec:o,audio:!0});return{kind:"video",model:e,request:n,costUsd:i,credits:h(i)}}function oe(e,t){if(!t||!e.edit)return e;let{edit:o}=e;return{...e,endpoint:o.endpoint,maxReferenceImages:o.maxReferenceImages,pricePerImage:o.pricePerImage??e.pricePerImage,mapInput:o.mapInput}}function G(e){let t=u(e.modelId);if(!t||t.kind!=="image")throw new r(`Unknown image model "${e.modelId}"`,"INVALID_MODEL",400);if((e.imageUrls?.length??0)>0&&!t.edit&&t.maxReferenceImages===0)throw new r(`${t.label} can't use reference photos \u2014 pick a model that edits from references`,"INVALID_REFERENCES",400);let o=oe(t,(e.imageUrls?.length??0)>0),n=e.prompt?.trim();if(!n)throw new r("prompt is required","MISSING_PROMPT",400);let i=e.count??1;if(!Number.isInteger(i)||i<1||i>o.maxImages)throw new r(`count must be 1\u2013${o.maxImages}`,"INVALID_COUNT",400);let a=e.imageUrls??[];if(o.maxReferenceImages>0&&a.length<1)throw new r("imageUrls needs at least one reference image","MISSING_IMAGE",400);if(a.length>o.maxReferenceImages)throw new r(`imageUrls allows up to ${o.maxReferenceImages} images`,"INVALID_REFERENCES",400);a.forEach((A,U)=>f(A,`imageUrls[${U}]`));let d=e.aspectRatio??"9:16";if(!["9:16","16:9","1:1","4:5"].includes(d))throw new r("aspectRatio must be 9:16, 16:9, 1:1 or 4:5","INVALID_ASPECT_RATIO",400);let b={prompt:n,imageUrls:a,count:i,aspectRatio:d,seed:e.seed},g=N(o,i);return{kind:"image",model:o,request:b,costUsd:g,credits:h(g)}}var ne="https://queue.fal.run",m=class extends Error{constructor(o,n,i){super(o);this.status=n;this.retryable=i;this.name="FalError"}status;retryable};function ie(e){let t=(typeof e=="function"?e():e)?.trim();if(!t)throw new m("fal API key is not configured",500,!1);return t}async function w(e,t,o={}){let n=await fetch(t,{...o,headers:{Authorization:`Key ${ie(e)}`,"Content-Type":"application/json",...o.headers??{}},signal:AbortSignal.timeout(3e4)}),i=await n.text(),a=null;try{a=i?JSON.parse(i):null}catch{a=i}if(!n.ok){let d=(a&&typeof a=="object"&&"detail"in a?JSON.stringify(a.detail):typeof a=="string"?a:JSON.stringify(a))??"";throw new m(`fal ${n.status}: ${d.slice(0,300)}`,n.status,n.status===429||n.status>=500)}return a}async function re(e,t,o){let n=await w(e,`${ne}/${t}`,{method:"POST",body:JSON.stringify(o)});if(!n?.request_id||!n.status_url||!n.response_url)throw new m("fal submit returned no request id",502,!0);return{requestId:n.request_id,statusUrl:n.status_url,responseUrl:n.response_url,cancelUrl:n.cancel_url}}async function ae(e,t){let o=await w(e,t);if(o.error)return{state:"failed",error:`${o.error_type??"error"}: ${o.error}`};switch(o.status){case"IN_QUEUE":return{state:"queued",position:o.queue_position};case"IN_PROGRESS":return{state:"running"};case"COMPLETED":return{state:"completed"};default:return{state:"running"}}}async function se(e,t){return w(e,t)}var de="https://rest.fal.ai/storage/upload/initiate?storage_type=fal-cdn-v3";async function ce(e,t){let o=await w(e,de,{method:"POST",body:JSON.stringify({content_type:t.contentType,file_name:t.fileName})});if(!o?.upload_url||!o.file_url)throw new m("fal storage returned no upload URL",502,!0);let n=await fetch(o.upload_url,{method:"PUT",headers:{"Content-Type":t.contentType||"application/octet-stream"},body:new Blob([t.data],{type:t.contentType||"application/octet-stream"}),signal:AbortSignal.timeout(12e4)});if(!n.ok)throw new m(`fal storage upload failed (${n.status})`,n.status,n.status===429||n.status>=500);return o.file_url}function pe(e){return{submit:(t,o)=>re(e,t,o),status:t=>ae(e,t),result:t=>se(e,t),upload:t=>ce(e,t)}}function le(e){let t=[],o=e??{},n=o.video;n?.url&&t.push({url:n.url,contentType:n.content_type});let i=o.audio;i?.url&&t.push({url:i.url,contentType:i.content_type});let a=Array.isArray(o.images)?o.images:[];for(let d of a)d?.url&&t.push({url:d.url,contentType:d.content_type});return t}var ue=["ageAppearance","face","eyes","brows","skin","hair","bodyBuild"],me=["device","lens","distance","height","processing"],ge={device:"front-facing smartphone camera",lens:"24-28mm equivalent",distance:"arm's length",height:"slightly above eye level",processing:"subtle HDR, mild phone sharpening, no beauty filter"},fe=["face","hair","signature details","skin texture"],he=300,Se=12,c=(e,t=he)=>typeof e=="string"?e.replace(/\s+/g," ").trim().slice(0,t):"";function k(e,t=Se,o=120){return Array.isArray(e)?[...new Set(e.map(n=>c(n,o)).filter(Boolean))].slice(0,t):[]}var be={ageAppearance:"age_appearance",bodyBuild:"body_build"};function P(e){let t={};for(let i of ue){let a=c(e.identity?.[i]);a&&(t[be[i]??i]=a)}let o=c(e.appearance,600);o&&Object.keys(t).length===0&&(t.appearance=o);let n={};for(let i of me)n[i]=c(e.cameraHabits?.[i])||ge[i];return{creator:c(e.name,60)||"the creator",identity_lock:t,signature_details:k(e.signatureDetails),imperfections:k(e.imperfections),camera_habits:n,never_change:e.neverChange?.length?k(e.neverChange):fe}}function B(e){let t={},o=e.voiceProfile??{};return c(o.tone)&&(t.tone=c(o.tone)),c(o.accent)&&(t.accent=c(o.accent)),o.verbalHabits?.length&&(t.verbal_habits=o.verbalHabits),c(e.personality)&&(t.personality=c(e.personality)),t}var ve=["different face","shorter hair","missing jewelry","plastic skin","studio lighting","perfectly tidy room","extra fingers","warped label","text","watermark","UI overlays"],ye=["morphing face","melting hands","label changing between frames","camera cuts","slow motion"];function Ie(e){return(e?.signature_details??[]).map(t=>`missing ${t}`)}function S(e,t={}){return[...new Set([...ve,...t.video?ye:[],...Ie(e)])]}var xe=["identity from the creator lock (face, hair, signature details)","same camera habits","product and app screen readable","requested wardrobe only","requested location only"],we={style:"authentic user-generated content filmed on a phone",framing:"handheld, slightly imperfect framing, vertical 9:16",light:"natural light, casual real-life setting",avoid:"studio look, cinematic grading, text or logos on screen"},E={image:3800,video:3800,avatar:1200};function ke(e){let t={task:e.task};return e.creator&&(t.priority_order=xe,t.creator=e.creator),e.shot&&Object.keys(e.shot).length&&(t.shot=e.shot),e.direction&&(t.direction=e.direction),e.dialogue&&(t.dialogue=e.dialogue),e.delivery&&Object.keys(e.delivery).length&&(t.delivery=e.delivery),e.notes?.length&&(t.notes=e.notes),t.look=we,t.negative_prompt=e.negatives,t}function _(e,t){return typeof e=="string"?e.length>t?`${e.slice(0,t-1)}\u2026`:e:Array.isArray(e)?e.map(o=>_(o,t)):e&&typeof e=="object"?Object.fromEntries(Object.entries(e).map(([o,n])=>[o,_(n,t)])):e}function O(e,t){let o=ke(e),n=JSON.stringify(o);for(let a of[400,240,160,100]){if(n.length<=t)return n;let d=_(o,a);e.dialogue&&(d.dialogue=e.dialogue),n=JSON.stringify(d)}if(n.length<=t)return n;let i={...o,look:void 0,delivery:void 0};return e.creator&&(i.creator={...e.creator,imperfections:void 0,camera_habits:void 0}),JSON.stringify(_(i,100)).slice(0,t)}var Pe=(e,t=300)=>typeof e=="string"?e.replace(/\s+/g," ").trim().slice(0,t):"";function L(e,t){let o={},n=e.wardrobe||Pe(t);if(n&&(o.wardrobe=n),e.location&&(o.location=e.location),e.action&&(o.action=e.action),e.productPlacement){let i=e.productPlacement;o.product_placement={...i.visible!==void 0?{visible:i.visible}:{},...i.placement?{placement:i.placement}:{},...i.sizeInFrame?{size_in_frame:i.sizeInFrame}:{}}}return e.motionBeats?.length&&(o.motion=e.motionBeats),o}var j=e=>e?P(e):null;function J({persona:e,shot:t,direction:o}){let n=j(e);return O({task:n?`A photo of ${n.creator}: the same person as in the reference images, face, hair and build identical.`:"A candid phone photo.",creator:n,shot:L(t,e?.style?.wardrobe),direction:o||(t.action?"":"A candid everyday moment at home."),negatives:S(n)},E.image)}function K(e){let t=j(e.persona),o=e.script.replace(/\s+/g," ").trim();return O({task:o?`${t?.creator??"The person"} talks straight to the phone camera and says the dialogue line.`:`${t?.creator??"The person"} on camera, no dialogue.`,creator:t,shot:L(e.shot,e.persona?.style?.wardrobe),direction:e.direction,dialogue:o,delivery:e.persona?B(e.persona):void 0,notes:["Only describe what moves; the start frame fixes the face, room and light.","Subtle handheld shake, no background music, no subtitles."],negatives:S(t,{video:!0})},E.video)}var Ue="https://placeholder.appeeky.com/frame.png";var io=675;export{C as MAX_SCRIPT_CHARS,Ue as PLACEHOLDER_IMAGE,P as buildCreatorLock,pe as createFalClient,le as extractOutputUrls,T as fitDuration,u as getMediaModel,S as negativesFor,G as prepareImageJob,$ as prepareVideoJob,J as stillPrompt,K as talkingVideoPrompt};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { AppeekyApi } from "./appeeky-api.js";
|
|
2
|
+
import type { Brief } from "./ugc.js";
|
|
3
|
+
/**
|
|
4
|
+
* Clips made on this machine with the user's own fal key (FAL_KEY), the same pipeline as Appeeky's
|
|
5
|
+
* hosted UGC and the desktop app's local mode: the persona's look → a still in the scene → a talking
|
|
6
|
+
* video with the script in the persona's voice. Planning, prompts and prices come from Appeeky's
|
|
7
|
+
* UGC core, bundled into dist/ugc-core.js and loaded only when this runs; fal bills the user directly.
|
|
8
|
+
*
|
|
9
|
+
* Personas come from either:
|
|
10
|
+
* appeeky/ugc/personas/<name>.yaml a persona kept in the repo, `looks:` pointing at its images
|
|
11
|
+
* Appeeky UGC a persona made in Appeeky (APPEEKY_API_KEY), its cover as the look
|
|
12
|
+
*/
|
|
13
|
+
export declare const localPersonasDir: (root: string) => string;
|
|
14
|
+
export declare const mediaDir: (root: string) => string;
|
|
15
|
+
/** The UgcPersona fields the prompts read; everything else stays at its default. */
|
|
16
|
+
export interface LocalPersona {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
appearance?: string;
|
|
20
|
+
personality?: string;
|
|
21
|
+
bio?: string;
|
|
22
|
+
voice?: string;
|
|
23
|
+
lane?: string;
|
|
24
|
+
identity?: Record<string, unknown>;
|
|
25
|
+
signatureDetails?: string[];
|
|
26
|
+
imperfections?: string[];
|
|
27
|
+
cameraHabits?: Record<string, unknown>;
|
|
28
|
+
voiceProfile?: Record<string, unknown>;
|
|
29
|
+
neverChange?: string[];
|
|
30
|
+
style?: Record<string, unknown>;
|
|
31
|
+
/** Image files (relative to the persona file) or URLs that show the persona. */
|
|
32
|
+
looks: string[];
|
|
33
|
+
}
|
|
34
|
+
interface PreparedJob {
|
|
35
|
+
costUsd: number;
|
|
36
|
+
model: {
|
|
37
|
+
endpoint: string;
|
|
38
|
+
mapInput(request: unknown): Record<string, unknown>;
|
|
39
|
+
};
|
|
40
|
+
request: unknown;
|
|
41
|
+
}
|
|
42
|
+
interface Core {
|
|
43
|
+
MAX_SCRIPT_CHARS: number;
|
|
44
|
+
getMediaModel(id: string): {
|
|
45
|
+
id: string;
|
|
46
|
+
kind: string;
|
|
47
|
+
mode: string;
|
|
48
|
+
label: string;
|
|
49
|
+
nativeAudio?: boolean;
|
|
50
|
+
} | undefined;
|
|
51
|
+
fitDuration(model: unknown, seconds: number): number;
|
|
52
|
+
prepareImageJob(input: Record<string, unknown>): PreparedJob;
|
|
53
|
+
prepareVideoJob(input: Record<string, unknown>): PreparedJob;
|
|
54
|
+
createFalClient(key: string): {
|
|
55
|
+
submit(endpoint: string, input: Record<string, unknown>): Promise<{
|
|
56
|
+
statusUrl: string;
|
|
57
|
+
responseUrl: string;
|
|
58
|
+
}>;
|
|
59
|
+
status(url: string): Promise<{
|
|
60
|
+
state: string;
|
|
61
|
+
error?: string;
|
|
62
|
+
}>;
|
|
63
|
+
result(url: string): Promise<unknown>;
|
|
64
|
+
upload(file: {
|
|
65
|
+
data: Uint8Array;
|
|
66
|
+
contentType: string;
|
|
67
|
+
fileName: string;
|
|
68
|
+
}): Promise<string>;
|
|
69
|
+
};
|
|
70
|
+
extractOutputUrls(result: unknown): Array<{
|
|
71
|
+
url: string;
|
|
72
|
+
contentType?: string;
|
|
73
|
+
}>;
|
|
74
|
+
buildCreatorLock(persona: unknown): unknown;
|
|
75
|
+
negativesFor(lock: unknown, opts: {
|
|
76
|
+
video: boolean;
|
|
77
|
+
}): string[];
|
|
78
|
+
stillPrompt(input: Record<string, unknown>): string;
|
|
79
|
+
talkingVideoPrompt(input: Record<string, unknown>): string;
|
|
80
|
+
PLACEHOLDER_IMAGE: string;
|
|
81
|
+
}
|
|
82
|
+
/** The UGC pipeline ships inside Spec as one minified file (dist/ugc-core.js, built by scripts/vendor-ugc-core.mjs). */
|
|
83
|
+
export declare const CORE_FILE = "./ugc-core.js";
|
|
84
|
+
export declare function loadCore(load?: (specifier: string) => Promise<Record<string, unknown>>): Promise<Core>;
|
|
85
|
+
/** A persona kept in the repo (appeeky/ugc/personas/<name>.yaml), with its looks resolved to paths or URLs. */
|
|
86
|
+
export declare function readLocalPersona(root: string, name: string): Promise<LocalPersona | undefined>;
|
|
87
|
+
/** A persona made in Appeeky UGC: its fields, with the cover image as its look. */
|
|
88
|
+
export declare function readAppeekyPersona(api: AppeekyApi, name: string): Promise<LocalPersona | undefined>;
|
|
89
|
+
interface Plan {
|
|
90
|
+
still: PreparedJob;
|
|
91
|
+
video: (stillUrl: string) => PreparedJob;
|
|
92
|
+
costUsd: number;
|
|
93
|
+
clip: {
|
|
94
|
+
durationSec: number;
|
|
95
|
+
aspectRatio: string;
|
|
96
|
+
videoModelId: string;
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** The same two steps as the hosted clip render: a still of the persona in the scene, then a talking video. */
|
|
100
|
+
export declare function planLocalClip(core: Core, persona: LocalPersona, brief: Brief, refs: string[]): Plan;
|
|
101
|
+
export interface LocalClipResult {
|
|
102
|
+
file: string;
|
|
103
|
+
videoUrl: string;
|
|
104
|
+
stillUrl: string;
|
|
105
|
+
costUsd: number;
|
|
106
|
+
}
|
|
107
|
+
/** Make the clip with the user's fal key, save it under .appeeky/media/ and record it in the brief. */
|
|
108
|
+
export declare function generateLocal(core: Core, root: string, brief: Brief & {
|
|
109
|
+
file: string;
|
|
110
|
+
}, persona: LocalPersona, falKey: string, options?: {
|
|
111
|
+
fetcher?: typeof fetch;
|
|
112
|
+
sleep?: (ms: number) => Promise<void>;
|
|
113
|
+
now?: Date;
|
|
114
|
+
log?: (line: string) => void;
|
|
115
|
+
}): Promise<LocalClipResult>;
|
|
116
|
+
export {};
|