@omg-dev/media 0.4.24

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/dist/index.mjs ADDED
@@ -0,0 +1,91 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/index.ts
3
+ const endUserStore = new AsyncLocalStorage();
4
+ /**
5
+ * Run `fn` with the given end-user id in scope. Any generation submitted
6
+ * (transitively) inside `fn` stamps `X-OMG-User: <userId>` on the proxy call
7
+ * unless an explicit `endUser` option overrides it. Pass a falsy id to opt out.
8
+ */
9
+ function runWithEndUser(userId, fn) {
10
+ if (!userId) return fn();
11
+ return endUserStore.run(userId, fn);
12
+ }
13
+ /** The end-user id currently in scope, or undefined. */
14
+ function getEndUser() {
15
+ return endUserStore.getStore();
16
+ }
17
+ function mediaBase() {
18
+ return process.env.OMG_MEDIA_URL || "http://localhost:9090/media";
19
+ }
20
+ function attributedHeaders(explicit) {
21
+ const h = { "Content-Type": "application/json" };
22
+ const user = explicit ?? endUserStore.getStore();
23
+ if (user) h["X-OMG-User"] = user;
24
+ return h;
25
+ }
26
+ /** Enqueue a generation. Returns immediately with a queued job. */
27
+ async function submit(opts) {
28
+ const res = await fetch(`${mediaBase()}/submit`, {
29
+ method: "POST",
30
+ headers: attributedHeaders(opts.endUser),
31
+ body: JSON.stringify({
32
+ provider: opts.provider,
33
+ model: opts.model,
34
+ input: opts.input ?? {}
35
+ })
36
+ });
37
+ if (!res.ok) throw new Error(`media submit failed: ${res.status} ${await res.text()}`);
38
+ return await res.json();
39
+ }
40
+ /** Fetch the current state of a job. */
41
+ async function getJob(jobId) {
42
+ const res = await fetch(`${mediaBase()}/jobs/${encodeURIComponent(jobId)}`);
43
+ if (!res.ok) throw new Error(`media getJob failed: ${res.status} ${await res.text()}`);
44
+ return await res.json();
45
+ }
46
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
47
+ /** Submit then poll until the job reaches a terminal state (or times out). */
48
+ async function generate(opts) {
49
+ const interval = opts.pollIntervalMs ?? 1500;
50
+ const timeout = opts.timeoutMs ?? 3e5;
51
+ const endUser = opts.endUser ?? endUserStore.getStore();
52
+ const job = await submit({
53
+ ...opts,
54
+ endUser
55
+ });
56
+ const deadline = Date.now() + timeout;
57
+ let current = job;
58
+ while (current.status === "queued" || current.status === "running") {
59
+ if (Date.now() > deadline) throw new Error(`media generate timed out after ${timeout}ms (job ${job.jobId})`);
60
+ await sleep(interval);
61
+ current = await getJob(job.jobId);
62
+ }
63
+ if (current.status === "failed") throw new Error(`media generate failed: ${current.error || "unknown error"}`);
64
+ return current;
65
+ }
66
+ /** Generate one image and return its first result URL plus the full job. */
67
+ async function generateImage(opts) {
68
+ const job = await generate(opts);
69
+ const url = job.results?.[0]?.url;
70
+ if (!url) throw new Error(`media generateImage: job ${job.jobId} produced no result`);
71
+ return {
72
+ url,
73
+ job
74
+ };
75
+ }
76
+ /** Generate one video and return its first result URL plus the full job. */
77
+ async function generateVideo(opts) {
78
+ const job = await generate({
79
+ timeoutMs: 6e5,
80
+ pollIntervalMs: 3e3,
81
+ ...opts
82
+ });
83
+ const url = job.results?.[0]?.url;
84
+ if (!url) throw new Error(`media generateVideo: job ${job.jobId} produced no result`);
85
+ return {
86
+ url,
87
+ job
88
+ };
89
+ }
90
+ //#endregion
91
+ export { generate, generateImage, generateVideo, getEndUser, getJob, runWithEndUser, submit };
package/dist/react.mjs ADDED
@@ -0,0 +1,53 @@
1
+ import { useEffect, useState } from "react";
2
+ //#region src/react.ts
3
+ /**
4
+ * Poll `pollUrl` (a route on your own app that proxies getJob) until the job
5
+ * is terminal. Pass `null` to stay idle (e.g. before a job exists). The route
6
+ * is expected to return a `Job` shape: `{ status, results?, error? }`.
7
+ */
8
+ function useMediaJob(pollUrl, opts = {}) {
9
+ const interval = opts.intervalMs ?? 1500;
10
+ const [state, setState] = useState({
11
+ status: "idle",
12
+ results: []
13
+ });
14
+ useEffect(() => {
15
+ if (!pollUrl) {
16
+ setState({
17
+ status: "idle",
18
+ results: []
19
+ });
20
+ return;
21
+ }
22
+ let cancelled = false;
23
+ let timer;
24
+ const tick = async () => {
25
+ try {
26
+ const job = await (await fetch(pollUrl)).json();
27
+ if (cancelled) return;
28
+ setState({
29
+ status: job.status,
30
+ results: job.results ?? [],
31
+ error: job.error,
32
+ job
33
+ });
34
+ if (job.status === "queued" || job.status === "running") timer = setTimeout(tick, interval);
35
+ } catch (e) {
36
+ if (cancelled) return;
37
+ setState((s) => ({
38
+ ...s,
39
+ error: String(e)
40
+ }));
41
+ timer = setTimeout(tick, interval);
42
+ }
43
+ };
44
+ tick();
45
+ return () => {
46
+ cancelled = true;
47
+ clearTimeout(timer);
48
+ };
49
+ }, [pollUrl, interval]);
50
+ return state;
51
+ }
52
+ //#endregion
53
+ export { useMediaJob };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@omg-dev/media",
3
+ "version": "0.4.24",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./src/index.ts",
8
+ "default": "./dist/index.mjs"
9
+ },
10
+ "./react": {
11
+ "types": "./src/react.ts",
12
+ "default": "./dist/react.mjs"
13
+ }
14
+ },
15
+ "peerDependencies": {
16
+ "react": "^18 || ^19"
17
+ },
18
+ "peerDependenciesMeta": {
19
+ "react": {
20
+ "optional": true
21
+ }
22
+ },
23
+ "license": "MIT",
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/BennyKok/vibes.git"
27
+ },
28
+ "homepage": "https://docs.omg.dev",
29
+ "files": [
30
+ "dist",
31
+ "src"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ }
37
+ }
package/src/index.ts ADDED
@@ -0,0 +1,167 @@
1
+ // @omg-dev/media — zero-config media generation (image / video / audio) for
2
+ // omg-deployed apps. Server-only entry.
3
+ //
4
+ // What this gives you:
5
+ // - `submit` / `getJob` — the raw async job API: enqueue a generation on a
6
+ // curated model, then poll for the result. Video can take minutes, so the
7
+ // model is fundamentally async (submit returns a jobId immediately).
8
+ // - `generateImage` / `generateVideo` — sugar that submits then polls to
9
+ // completion, so the simple case reads synchronously.
10
+ // - `runWithEndUser(userId, fn)` — wraps a handler so generations inside are
11
+ // attributed to the end-user (`X-OMG-User`), same pattern as @omg-dev/ai.
12
+ //
13
+ // No baseURL or key from your code: the omg infra orchestrator injects
14
+ // `OMG_MEDIA_URL` into the sandbox, the host-side media proxy holds the
15
+ // provider key, bills your omg credits (pass-through, no margin), and uploads
16
+ // the result to a durable CDN. You just pick a model and pass params.
17
+ //
18
+ // import { generateImage } from "@omg-dev/media"
19
+ // const { url } = await generateImage({
20
+ // model: "wavespeed-ai/flux-dev",
21
+ // input: { prompt: "a corgi astronaut, studio lighting" },
22
+ // })
23
+
24
+ import { AsyncLocalStorage } from "node:async_hooks"
25
+
26
+ // ── End-user attribution ─────────────────────────────────────────────────────
27
+
28
+ const endUserStore = new AsyncLocalStorage<string>()
29
+
30
+ /**
31
+ * Run `fn` with the given end-user id in scope. Any generation submitted
32
+ * (transitively) inside `fn` stamps `X-OMG-User: <userId>` on the proxy call
33
+ * unless an explicit `endUser` option overrides it. Pass a falsy id to opt out.
34
+ */
35
+ export function runWithEndUser<T>(
36
+ userId: string | undefined | null,
37
+ fn: () => T | Promise<T>,
38
+ ): T | Promise<T> {
39
+ if (!userId) return fn()
40
+ return endUserStore.run(userId, fn)
41
+ }
42
+
43
+ /** The end-user id currently in scope, or undefined. */
44
+ export function getEndUser(): string | undefined {
45
+ return endUserStore.getStore()
46
+ }
47
+
48
+ // ── Types ────────────────────────────────────────────────────────────────────
49
+
50
+ export type JobStatus = "queued" | "running" | "succeeded" | "failed"
51
+
52
+ export interface ResultRef {
53
+ url: string
54
+ contentType?: string
55
+ }
56
+
57
+ export interface Job {
58
+ jobId: string
59
+ status: JobStatus
60
+ model?: string
61
+ results?: ResultRef[]
62
+ error?: string
63
+ }
64
+
65
+ export interface SubmitOptions {
66
+ /** Curated model id, e.g. "wavespeed-ai/flux-dev". */
67
+ model: string
68
+ /** Model-specific params (prompt, image, duration, num_images, …). */
69
+ input?: Record<string, unknown>
70
+ /** Provider override; inferred from the model when omitted. */
71
+ provider?: string
72
+ /** Attribution override; defaults to the runWithEndUser context. */
73
+ endUser?: string
74
+ }
75
+
76
+ export interface GenerateOptions extends SubmitOptions {
77
+ /** Poll interval in ms (default 1500). */
78
+ pollIntervalMs?: number
79
+ /** Give up after this many ms (default 300_000 = 5 min). */
80
+ timeoutMs?: number
81
+ }
82
+
83
+ // ── Transport ────────────────────────────────────────────────────────────────
84
+
85
+ // Base injected by the orchestrator. Falls back to a localhost guess so the
86
+ // package is importable outside a sandbox (calls will just fail to connect).
87
+ function mediaBase(): string {
88
+ return process.env.OMG_MEDIA_URL || "http://localhost:9090/media"
89
+ }
90
+
91
+ function attributedHeaders(explicit?: string): Record<string, string> {
92
+ const h: Record<string, string> = { "Content-Type": "application/json" }
93
+ const user = explicit ?? endUserStore.getStore()
94
+ if (user) h["X-OMG-User"] = user
95
+ return h
96
+ }
97
+
98
+ /** Enqueue a generation. Returns immediately with a queued job. */
99
+ export async function submit(opts: SubmitOptions): Promise<Job> {
100
+ const res = await fetch(`${mediaBase()}/submit`, {
101
+ method: "POST",
102
+ headers: attributedHeaders(opts.endUser),
103
+ body: JSON.stringify({
104
+ provider: opts.provider,
105
+ model: opts.model,
106
+ input: opts.input ?? {},
107
+ }),
108
+ })
109
+ if (!res.ok) {
110
+ throw new Error(`media submit failed: ${res.status} ${await res.text()}`)
111
+ }
112
+ return (await res.json()) as Job
113
+ }
114
+
115
+ /** Fetch the current state of a job. */
116
+ export async function getJob(jobId: string): Promise<Job> {
117
+ const res = await fetch(`${mediaBase()}/jobs/${encodeURIComponent(jobId)}`)
118
+ if (!res.ok) {
119
+ throw new Error(`media getJob failed: ${res.status} ${await res.text()}`)
120
+ }
121
+ return (await res.json()) as Job
122
+ }
123
+
124
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
125
+
126
+ /** Submit then poll until the job reaches a terminal state (or times out). */
127
+ export async function generate(opts: GenerateOptions): Promise<Job> {
128
+ const interval = opts.pollIntervalMs ?? 1500
129
+ const timeout = opts.timeoutMs ?? 300_000
130
+ // Capture attribution now so polling outside the ALS scope still attributes.
131
+ const endUser = opts.endUser ?? endUserStore.getStore()
132
+ const job = await submit({ ...opts, endUser })
133
+ const deadline = Date.now() + timeout
134
+ let current = job
135
+ while (current.status === "queued" || current.status === "running") {
136
+ if (Date.now() > deadline) {
137
+ throw new Error(`media generate timed out after ${timeout}ms (job ${job.jobId})`)
138
+ }
139
+ await sleep(interval)
140
+ current = await getJob(job.jobId)
141
+ }
142
+ if (current.status === "failed") {
143
+ throw new Error(`media generate failed: ${current.error || "unknown error"}`)
144
+ }
145
+ return current
146
+ }
147
+
148
+ /** Generate one image and return its first result URL plus the full job. */
149
+ export async function generateImage(
150
+ opts: GenerateOptions,
151
+ ): Promise<{ url: string; job: Job }> {
152
+ const job = await generate(opts)
153
+ const url = job.results?.[0]?.url
154
+ if (!url) throw new Error(`media generateImage: job ${job.jobId} produced no result`)
155
+ return { url, job }
156
+ }
157
+
158
+ /** Generate one video and return its first result URL plus the full job. */
159
+ export async function generateVideo(
160
+ opts: GenerateOptions,
161
+ ): Promise<{ url: string; job: Job }> {
162
+ // Video is slower; default the timeout up if the caller didn't set one.
163
+ const job = await generate({ timeoutMs: 600_000, pollIntervalMs: 3000, ...opts })
164
+ const url = job.results?.[0]?.url
165
+ if (!url) throw new Error(`media generateVideo: job ${job.jobId} produced no result`)
166
+ return { url, job }
167
+ }
package/src/react.ts ADDED
@@ -0,0 +1,92 @@
1
+ // Browser-safe entry — a polling hook for media jobs.
2
+ //
3
+ // `@omg-dev/media` (the root entry) is server-only: it imports
4
+ // `node:async_hooks` and talks to the in-VM media proxy (OMG_MEDIA_URL), which
5
+ // is not reachable from the browser. So the client flow is:
6
+ //
7
+ // 1. Your `functions/api/<name>.ts` route calls `submit()` (server) and
8
+ // returns the `{ jobId }` to the browser.
9
+ // 2. The browser hook below polls YOUR route (which calls `getJob()` server
10
+ // side) until the job is terminal, then hands you the result URLs.
11
+ //
12
+ // import { useMediaJob } from "@omg-dev/media/react"
13
+ // const { status, results } = useMediaJob(jobId ? `/api/media-status?id=${jobId}` : null)
14
+
15
+ import { useEffect, useState } from "react"
16
+
17
+ export type JobStatus = "queued" | "running" | "succeeded" | "failed"
18
+
19
+ export interface ResultRef {
20
+ url: string
21
+ contentType?: string
22
+ }
23
+
24
+ export interface Job {
25
+ jobId: string
26
+ status: JobStatus
27
+ results?: ResultRef[]
28
+ error?: string
29
+ }
30
+
31
+ export interface UseMediaJobOptions {
32
+ /** Poll interval in ms (default 1500). */
33
+ intervalMs?: number
34
+ }
35
+
36
+ export interface UseMediaJobState {
37
+ status: JobStatus | "idle"
38
+ results: ResultRef[]
39
+ error?: string
40
+ job?: Job
41
+ }
42
+
43
+ /**
44
+ * Poll `pollUrl` (a route on your own app that proxies getJob) until the job
45
+ * is terminal. Pass `null` to stay idle (e.g. before a job exists). The route
46
+ * is expected to return a `Job` shape: `{ status, results?, error? }`.
47
+ */
48
+ export function useMediaJob(
49
+ pollUrl: string | null,
50
+ opts: UseMediaJobOptions = {},
51
+ ): UseMediaJobState {
52
+ const interval = opts.intervalMs ?? 1500
53
+ const [state, setState] = useState<UseMediaJobState>({ status: "idle", results: [] })
54
+
55
+ useEffect(() => {
56
+ if (!pollUrl) {
57
+ setState({ status: "idle", results: [] })
58
+ return
59
+ }
60
+ let cancelled = false
61
+ let timer: ReturnType<typeof setTimeout>
62
+
63
+ const tick = async () => {
64
+ try {
65
+ const res = await fetch(pollUrl)
66
+ const job = (await res.json()) as Job
67
+ if (cancelled) return
68
+ setState({
69
+ status: job.status,
70
+ results: job.results ?? [],
71
+ error: job.error,
72
+ job,
73
+ })
74
+ if (job.status === "queued" || job.status === "running") {
75
+ timer = setTimeout(tick, interval)
76
+ }
77
+ } catch (e) {
78
+ if (cancelled) return
79
+ setState((s) => ({ ...s, error: String(e) }))
80
+ timer = setTimeout(tick, interval)
81
+ }
82
+ }
83
+ tick()
84
+
85
+ return () => {
86
+ cancelled = true
87
+ clearTimeout(timer)
88
+ }
89
+ }, [pollUrl, interval])
90
+
91
+ return state
92
+ }