@ohmygame/fal 0.0.0-stage → 0.1.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.
@@ -0,0 +1,207 @@
1
+ {
2
+ "$schema": "https://www.ohmygame.ai/schema/plugin-2.json",
3
+ "id": "ohmygame/fal",
4
+ "version": "0.1.0",
5
+ "engines": {
6
+ "ohmygame-api": "^2.5.0"
7
+ },
8
+ "displayName": "%plugin.name%",
9
+ "description": "%plugin.description%",
10
+ "longDescription": "%plugin.longDescription%",
11
+ "icon": "./assets/icon.svg",
12
+ "categories": [
13
+ "provider"
14
+ ],
15
+ "capabilities": {
16
+ "network": [
17
+ "queue.fal.run",
18
+ "fal.media",
19
+ "v3.fal.media",
20
+ "v3b.fal.media"
21
+ ]
22
+ },
23
+ "configuration": {
24
+ "apiKey": {
25
+ "type": "secret",
26
+ "title": "%config.apiKey.title%",
27
+ "description": "%config.apiKey.description%"
28
+ }
29
+ },
30
+ "contributes": {
31
+ "providers": [
32
+ {
33
+ "id": "fal",
34
+ "title": "%plugin.name%",
35
+ "icon": "./assets/icon.svg",
36
+ "apiKey": "apiKey",
37
+ "media": {
38
+ "adapter": "./media/adapter.js",
39
+ "auth": {
40
+ "header": "authorization",
41
+ "scheme": "Key"
42
+ },
43
+ "image": {
44
+ "models": [
45
+ {
46
+ "id": "fal-ai/flux/schnell",
47
+ "title": "FLUX.1 [schnell]",
48
+ "resolutions": [
49
+ "1K"
50
+ ],
51
+ "aspectRatios": [
52
+ "1:1",
53
+ "4:3",
54
+ "3:4",
55
+ "16:9",
56
+ "9:16"
57
+ ],
58
+ "maxReferenceImages": 0
59
+ }
60
+ ]
61
+ },
62
+ "video": {
63
+ "maxMinutes": 30,
64
+ "models": [
65
+ {
66
+ "id": "fal-ai/kling-video/o3/standard/image-to-video",
67
+ "title": "Kling O3",
68
+ "durations": [
69
+ 3,
70
+ 5,
71
+ 10,
72
+ 15
73
+ ],
74
+ "resolutions": [
75
+ "720p"
76
+ ],
77
+ "aspectRatios": [
78
+ "adaptive"
79
+ ],
80
+ "maxReferenceImages": 2,
81
+ "referenceModes": [
82
+ "frame"
83
+ ]
84
+ }
85
+ ]
86
+ },
87
+ "audio": {
88
+ "models": [
89
+ {
90
+ "id": "fal-ai/minimax/speech-02-hd",
91
+ "title": "MiniMax Speech-02 HD",
92
+ "kinds": [
93
+ "speech"
94
+ ],
95
+ "readsText": true,
96
+ "voices": [
97
+ {
98
+ "id": "Wise_Woman",
99
+ "name": "Wise Woman"
100
+ },
101
+ {
102
+ "id": "Friendly_Person",
103
+ "name": "Friendly Person"
104
+ },
105
+ {
106
+ "id": "Inspirational_girl",
107
+ "name": "Inspirational girl"
108
+ },
109
+ {
110
+ "id": "Deep_Voice_Man",
111
+ "name": "Deep Voice Man"
112
+ },
113
+ {
114
+ "id": "Calm_Woman",
115
+ "name": "Calm Woman"
116
+ },
117
+ {
118
+ "id": "Casual_Guy",
119
+ "name": "Casual Guy"
120
+ },
121
+ {
122
+ "id": "Lively_Girl",
123
+ "name": "Lively Girl"
124
+ },
125
+ {
126
+ "id": "Patient_Man",
127
+ "name": "Patient Man"
128
+ },
129
+ {
130
+ "id": "Young_Knight",
131
+ "name": "Young Knight"
132
+ },
133
+ {
134
+ "id": "Determined_Man",
135
+ "name": "Determined Man"
136
+ },
137
+ {
138
+ "id": "Lovely_Girl",
139
+ "name": "Lovely Girl"
140
+ },
141
+ {
142
+ "id": "Decent_Boy",
143
+ "name": "Decent Boy"
144
+ },
145
+ {
146
+ "id": "Imposing_Manner",
147
+ "name": "Imposing Manner"
148
+ },
149
+ {
150
+ "id": "Elegant_Man",
151
+ "name": "Elegant Man"
152
+ },
153
+ {
154
+ "id": "Abbess",
155
+ "name": "Abbess"
156
+ },
157
+ {
158
+ "id": "Sweet_Girl_2",
159
+ "name": "Sweet Girl 2"
160
+ },
161
+ {
162
+ "id": "Exuberant_Girl",
163
+ "name": "Exuberant Girl"
164
+ }
165
+ ],
166
+ "formats": [
167
+ "mp3"
168
+ ],
169
+ "maxTextCharacters": 5000
170
+ },
171
+ {
172
+ "id": "fal-ai/stable-audio",
173
+ "title": "Stable Audio",
174
+ "kinds": [
175
+ "sound-effect",
176
+ "music"
177
+ ],
178
+ "formats": [
179
+ "wav"
180
+ ],
181
+ "minDurationSeconds": 1,
182
+ "maxDurationSeconds": 47
183
+ }
184
+ ]
185
+ },
186
+ "3d": {
187
+ "models": [
188
+ {
189
+ "id": "fal-ai/trellis",
190
+ "title": "TRELLIS",
191
+ "operations": [
192
+ "image"
193
+ ],
194
+ "maxReferenceImages": 1,
195
+ "polycount": {
196
+ "min": 20000,
197
+ "max": 20000,
198
+ "default": 20000
199
+ }
200
+ }
201
+ ]
202
+ }
203
+ }
204
+ }
205
+ ]
206
+ }
207
+ }
package/README.md CHANGED
@@ -1,3 +1,25 @@
1
- # Temporary Holding Version
1
+ # fal.ai
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Adds fal.ai to OhMyGame's Settings → Providers, with your own API key, for
4
+ pictures, videos, audio and 3D models. It has no chat models.
5
+
6
+ - API key: From fal.ai → Dashboard → Keys.
7
+
8
+ | Kind | Model | What it takes |
9
+ | --- | --- | --- |
10
+ | Image | FLUX.1 [schnell] (`fal-ai/flux/schnell`) | A prompt, in 1:1, 4:3, 3:4, 16:9 or 9:16 |
11
+ | Video | Kling O3 (`fal-ai/kling-video/o3/standard/image-to-video`) | A first frame, an optional last frame, and 3, 5, 10 or 15 seconds |
12
+ | Speech | MiniMax Speech-02 HD (`fal-ai/minimax/speech-02-hd`) | Up to 5,000 characters, in one of MiniMax's voices, as MP3 |
13
+ | Sound effects, music | Stable Audio (`fal-ai/stable-audio`) | A description and up to 47 seconds, as WAV |
14
+ | 3D | TRELLIS (`fal-ai/trellis`) | One picture of the object, as GLB |
15
+
16
+ Every request goes through fal.ai's queue, in
17
+ [`media/adapter.js`](media/adapter.js). OhMyGame runs that script in a sandbox:
18
+ it reaches only `queue.fal.run` and the hosts fal.ai's files are on, and
19
+ OhMyGame adds the key to its requests. A video, audio file or 3D model a
20
+ restart cut off is checked again on **Retry**, not paid for twice. fal.ai bills
21
+ each request to your account.
22
+
23
+ Needs OhMyGame with plugin API 2.5.0 or later.
24
+
25
+ Published as `@ohmygame/fal` from [ohmygame-plugins](https://github.com/WhiteTowerAI/ohmygame-plugins).
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><rect width="24" height="24" rx="6" fill="#111827"/><path d="M12 4.5a7.5 7.5 0 1 0 0 15 7.5 7.5 0 0 0 0-15zm0 3.75a3.75 3.75 0 1 1 0 7.5 3.75 3.75 0 0 1 0-7.5z" fill="#A78BFA" fill-rule="evenodd"/></svg>
@@ -0,0 +1,7 @@
1
+ {
2
+ "plugin.name": "fal.ai",
3
+ "plugin.description": "fal.ai's image, video, audio and 3D models, with your own API key.",
4
+ "plugin.longDescription": "Adds fal.ai to Settings → Providers. Enter your API key here or in Settings: FLUX.1 [schnell] makes pictures, Kling O3 makes videos of 3 to 15 seconds from a first frame (and a last one if you give it), MiniMax Speech-02 HD reads text aloud, Stable Audio makes sound effects and music, and TRELLIS makes a 3D model from a picture. fal.ai bills each request to your account.",
5
+ "config.apiKey.title": "fal.ai API key",
6
+ "config.apiKey.description": "From fal.ai → Dashboard → Keys."
7
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "plugin.name": "fal.ai",
3
+ "plugin.description": "用你自己的 API key,使用 fal.ai 的图片、视频、音频和 3D 模型。",
4
+ "plugin.longDescription": "把 fal.ai 加入 设置 → 服务商。在这里或设置里填好 API key:FLUX.1 [schnell] 生成图片,Kling O3 根据首帧(可加尾帧)生成 3 到 15 秒的视频,MiniMax Speech-02 HD 朗读文本,Stable Audio 生成音效和音乐,TRELLIS 根据图片生成 3D 模型。每次请求由 fal.ai 向你的账户计费。",
5
+ "config.apiKey.title": "fal.ai API key",
6
+ "config.apiKey.description": "在 fal.ai → Dashboard → Keys 创建。"
7
+ }
@@ -0,0 +1,136 @@
1
+ // fal.ai's queue API for OhMyGame (docs.fal.ai: model endpoints, queue).
2
+ // Runs in OhMyGame's sandbox: host.fetch reaches only the hosts in
3
+ // capabilities.network, and `auth: true` adds the key ("Key <key>"), which
4
+ // this script never sees.
5
+ //
6
+ // Every model goes through the same queue: `start` submits the request, `check`
7
+ // reads its status and, once it's done, its result, and `cancel` withdraws it.
8
+ // The queue answers with the addresses to use, so they're kept as the task.
9
+
10
+ const QUEUE = "https://queue.fal.run/";
11
+
12
+ // FLUX's named sizes for the aspect ratios it's offered with.
13
+ const IMAGE_SIZES = {
14
+ "1:1": "square_hd", "4:3": "landscape_4_3", "3:4": "portrait_4_3", "16:9": "landscape_16_9", "9:16": "portrait_16_9",
15
+ };
16
+
17
+ /** The answer's JSON, and its message when fal.ai refused the request. */
18
+ async function read(response) {
19
+ const text = await response.text();
20
+ let answer;
21
+ try { answer = JSON.parse(text); } catch { answer = undefined; }
22
+ const detail = answer && answer.detail;
23
+ const message = (typeof detail === "string" && detail)
24
+ // A validation error lists what was wrong with each field.
25
+ || (Array.isArray(detail) && detail.map((item) => item && item.msg).filter(Boolean).join("; "))
26
+ || text.slice(0, 300)
27
+ || `fal.ai answered ${response.status}`;
28
+ return { answer, message };
29
+ }
30
+
31
+ function dataUri(picture) {
32
+ return `data:${picture.mediaType};base64,${picture.base64}`;
33
+ }
34
+
35
+ function onQueue(url) {
36
+ return typeof url === "string" && url.startsWith(QUEUE);
37
+ }
38
+
39
+ /** Submits a request to a model's queue. */
40
+ async function submit(model, body, host) {
41
+ const response = await host.fetch(QUEUE + model, { method: "POST", auth: true, json: body });
42
+ const { answer, message } = await read(response);
43
+ // A server error may come after fal.ai took the request: throwing has OhMyGame
44
+ // treat it as possibly sent, so a retry doesn't pay again. A refusal can be tried again.
45
+ if (response.status >= 500) throw new Error(message);
46
+ if (!response.ok) return { error: message };
47
+ const task = answer && { status: answer.status_url, result: answer.response_url, cancel: answer.cancel_url };
48
+ // Taken but unreadable: thrown for the same reason as a server error.
49
+ if (!task || !onQueue(task.status) || !onQueue(task.result)) throw new Error("fal.ai returned no request to follow");
50
+ return { task };
51
+ }
52
+
53
+ /** Reads a request's status, then its result with `pick` once it's done. */
54
+ async function follow(task, host, retryAfter, pick) {
55
+ const response = await host.fetch(task.status, { method: "GET", auth: true });
56
+ const { answer, message } = await read(response);
57
+ // A busy service is checked again (OhMyGame retries a failed check); a refusal ends the task.
58
+ if (response.status === 429 || response.status >= 500) throw new Error(message);
59
+ if (!response.ok) return { error: message };
60
+ const status = answer && answer.status;
61
+ if (status === "IN_QUEUE" || status === "IN_PROGRESS") return { retryAfter };
62
+ if (status !== "COMPLETED") throw new Error(`fal.ai answered with status ${String(status)}`);
63
+ if (answer.error) return { error: String(answer.error) };
64
+ const result = await host.fetch(task.result, { method: "GET", auth: true });
65
+ const output = await read(result);
66
+ if (result.status === 429 || result.status >= 500) throw new Error(output.message);
67
+ if (!result.ok) return { error: output.message };
68
+ // fal.ai keeps the file for a while; OhMyGame downloads it at once.
69
+ const url = output.answer && pick(output.answer);
70
+ return typeof url === "string" && url ? { outputs: [{ url }] } : { error: "fal.ai returned no file" };
71
+ }
72
+
73
+ async function cancel(task, host) {
74
+ if (onQueue(task.cancel)) await host.fetch(task.cancel, { method: "PUT", auth: true });
75
+ }
76
+
77
+ export default {
78
+ image: {
79
+ start(input, host) {
80
+ return submit(input.model, {
81
+ prompt: input.prompt,
82
+ image_size: IMAGE_SIZES[input.aspectRatio] || "square_hd",
83
+ num_images: 1,
84
+ output_format: "png",
85
+ }, host);
86
+ },
87
+ check: (task, host) => follow(task, host, 1, (answer) => answer.images && answer.images[0] && answer.images[0].url),
88
+ cancel,
89
+ },
90
+
91
+ video: {
92
+ start(input, host) {
93
+ const [first, last] = input.references;
94
+ if (!first) return { error: "Kling O3 on fal.ai makes a video from a first frame: add a picture." };
95
+ const body = { image_url: dataUri(first), duration: String(input.duration) };
96
+ if (input.prompt) body.prompt = input.prompt;
97
+ if (last) body.end_image_url = dataUri(last);
98
+ return submit(input.model, body, host);
99
+ },
100
+ check: (task, host) => follow(task, host, 5, (answer) => answer.video && answer.video.url),
101
+ cancel,
102
+ },
103
+
104
+ audio: {
105
+ start(input, host) {
106
+ if (input.model === "fal-ai/minimax/speech-02-hd") {
107
+ // OhMyGame's speed runs from -50 (half as fast) to 100 (twice as fast); MiniMax's from 0.5 to 2.
108
+ const speed = Math.min(2, Math.max(0.5, 1 + (input.speed || 0) / 100));
109
+ return submit(input.model, {
110
+ text: input.prompt,
111
+ voice_setting: { voice_id: input.voiceId || "Wise_Woman", speed },
112
+ audio_setting: { format: "mp3" },
113
+ output_format: "url",
114
+ }, host);
115
+ }
116
+ if (input.model === "fal-ai/stable-audio") {
117
+ const body = { prompt: input.prompt };
118
+ if (input.durationSeconds) body.seconds_total = input.durationSeconds;
119
+ return submit(input.model, body, host);
120
+ }
121
+ return { error: `${input.model} isn't offered here` };
122
+ },
123
+ check: (task, host) => follow(task, host, 2, (answer) => (answer.audio && answer.audio.url) || (answer.audio_file && answer.audio_file.url)),
124
+ cancel,
125
+ },
126
+
127
+ "3d": {
128
+ start(input, host) {
129
+ const front = input.references && input.references[0];
130
+ if (input.operation !== "image" || !front) return { error: "TRELLIS on fal.ai makes a 3D model from a picture: add one." };
131
+ return submit(input.model, { image_url: dataUri(front) }, host);
132
+ },
133
+ check: (task, host) => follow(task, host, 5, (answer) => answer.model_mesh && answer.model_mesh.url),
134
+ cancel,
135
+ },
136
+ };
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@ohmygame/fal",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "fal.ai's image, video, audio and 3D models, with your own API key.",
5
+ "keywords": [
6
+ "ohmygame-plugin",
7
+ "model-provider",
8
+ "fal",
9
+ "media"
10
+ ],
11
+ "license": "Apache-2.0",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/WhiteTowerAI/ohmygame-plugins.git",
15
+ "directory": "plugins/fal"
16
+ },
17
+ "homepage": "https://github.com/WhiteTowerAI/ohmygame-plugins/tree/main/plugins/fal#readme",
18
+ "files": [
19
+ ".ohmygame-plugin",
20
+ "assets",
21
+ "locales",
22
+ "media"
23
+ ],
24
+ "scripts": {
25
+ "check": "ohmygame-plugin check",
26
+ "pack": "ohmygame-plugin pack"
27
+ },
28
+ "devDependencies": {
29
+ "@ohmygame/plugin-cli": "^2.5.0"
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ }
34
+ }