@gradio/client 0.0.1

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/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # `@gradio/client`
2
+
3
+ A javascript client to call Gradio APIs.
4
+
5
+ **usage**
6
+
7
+ ```ts
8
+ import { client } from "@gradio/client";
9
+
10
+ const app = client();
11
+
12
+ const prediction = app.predict(endpoint, payload);
13
+
14
+ // listen for predictions
15
+ prediction.on("data", (event: { data: Array<unknown>; type: "data" }) => {});
16
+
17
+ // listen for status updates
18
+ prediction.on("status", (event: { data: Status; type: "data" }) => {});
19
+
20
+ interface Status {
21
+ status: "pending" | "error" | "complete" | "generating";
22
+ size: number;
23
+ position?: number;
24
+ eta?: number;
25
+ message?: string;
26
+ progress?: Array<{
27
+ progress: number | null;
28
+ index: number | null;
29
+ length: number | null;
30
+ unit: string | null;
31
+ desc: string | null;
32
+ }>;
33
+ }
34
+
35
+ // stop listening
36
+ prediction.off("data");
37
+
38
+ // cancel a prediction if it is a generator
39
+ prediction.cancel();
40
+
41
+ // chainable
42
+ const prediction_two = app
43
+ .predict(endpoint, payload)
44
+ .on("data", data_callback)
45
+ .on("status", status_callback);
46
+ ```
package/package.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@gradio/client",
3
+ "version": "0.0.1",
4
+ "description": "Gradio UI packages",
5
+ "type": "module",
6
+ "main": "src/index.ts",
7
+ "author": "",
8
+ "license": "ISC"
9
+ }
package/src/client.ts ADDED
@@ -0,0 +1,540 @@
1
+ import {
2
+ process_endpoint,
3
+ RE_SPACE_NAME,
4
+ map_names_to_ids,
5
+ discussions_enabled
6
+ } from "./utils";
7
+
8
+ import type {
9
+ EventType,
10
+ EventListener,
11
+ ListenerMap,
12
+ Event,
13
+ Config,
14
+ Payload,
15
+ PostResponse,
16
+ UploadResponse,
17
+ Status,
18
+ SpaceStatus,
19
+ SpaceStatusCallback
20
+ } from "./types";
21
+
22
+ type event = <K extends EventType>(
23
+ eventType: K,
24
+ listener: EventListener<K>
25
+ ) => ReturnType<predict>;
26
+ type predict = (endpoint: string, payload: Payload) => {};
27
+
28
+ type client_return = {
29
+ predict: predict;
30
+ config: Config;
31
+ on: event;
32
+ off: event;
33
+ cancel: (endpoint: string, fn_index?: number) => void;
34
+ };
35
+
36
+ const QUEUE_FULL_MSG = "This application is too busy. Keep trying!";
37
+ const BROKEN_CONNECTION_MSG = "Connection errored out.";
38
+
39
+ export async function post_data(
40
+ url: string,
41
+ body: unknown
42
+ ): Promise<[PostResponse, number]> {
43
+ try {
44
+ var response = await fetch(url, {
45
+ method: "POST",
46
+ body: JSON.stringify(body),
47
+ headers: { "Content-Type": "application/json" }
48
+ });
49
+ } catch (e) {
50
+ return [{ error: BROKEN_CONNECTION_MSG }, 500];
51
+ }
52
+ const output: PostResponse = await response.json();
53
+ return [output, response.status];
54
+ }
55
+
56
+ export async function upload_files(
57
+ root: string,
58
+ files: Array<File>
59
+ ): Promise<UploadResponse> {
60
+ const formData = new FormData();
61
+ files.forEach((file) => {
62
+ formData.append("files", file);
63
+ });
64
+ try {
65
+ var response = await fetch(`${root}/upload`, {
66
+ method: "POST",
67
+ body: formData
68
+ });
69
+ } catch (e) {
70
+ return { error: BROKEN_CONNECTION_MSG };
71
+ }
72
+ const output: UploadResponse["files"] = await response.json();
73
+ return { files: output };
74
+ }
75
+
76
+ export async function client(
77
+ app_reference: string,
78
+ space_status_callback?: SpaceStatusCallback
79
+ ): Promise<client_return> {
80
+ return new Promise(async (res, rej) => {
81
+ const return_obj = {
82
+ predict,
83
+ on,
84
+ off,
85
+ cancel
86
+ };
87
+
88
+ const listener_map: ListenerMap<EventType> = {};
89
+ const { ws_protocol, http_protocol, host, space_id } =
90
+ await process_endpoint(app_reference);
91
+ const session_hash = Math.random().toString(36).substring(2);
92
+ const ws_map = new Map<number, WebSocket>();
93
+ const last_status: Record<string, Status["status"]> = {};
94
+ let config: Config;
95
+ let api_map: Record<string, number> = {};
96
+
97
+ function config_success(_config: Config) {
98
+ config = _config;
99
+ api_map = map_names_to_ids(_config?.dependencies || []);
100
+ return {
101
+ config,
102
+ ...return_obj
103
+ };
104
+ }
105
+
106
+ function on<K extends EventType>(eventType: K, listener: EventListener<K>) {
107
+ const narrowed_listener_map: ListenerMap<K> = listener_map;
108
+ let listeners = narrowed_listener_map[eventType] || [];
109
+ narrowed_listener_map[eventType] = listeners;
110
+ listeners?.push(listener);
111
+
112
+ return { ...return_obj, config };
113
+ }
114
+
115
+ function off<K extends EventType>(
116
+ eventType: K,
117
+ listener: EventListener<K>
118
+ ) {
119
+ const narrowed_listener_map: ListenerMap<K> = listener_map;
120
+ let listeners = narrowed_listener_map[eventType] || [];
121
+ listeners = listeners?.filter((l) => l !== listener);
122
+ narrowed_listener_map[eventType] = listeners;
123
+
124
+ return { ...return_obj, config };
125
+ }
126
+
127
+ function cancel(endpoint: string, fn_index?: number) {
128
+ const _index =
129
+ typeof fn_index === "number" ? fn_index : api_map[endpoint];
130
+
131
+ fire_event({
132
+ type: "status",
133
+ endpoint,
134
+ fn_index: _index,
135
+ status: "complete",
136
+ queue: false
137
+ });
138
+
139
+ ws_map.get(_index)?.close();
140
+ }
141
+
142
+ function fire_event<K extends EventType>(event: Event<K>) {
143
+ const narrowed_listener_map: ListenerMap<K> = listener_map;
144
+ let listeners = narrowed_listener_map[event.type] || [];
145
+ listeners?.forEach((l) => l(event));
146
+ }
147
+
148
+ async function handle_space_sucess(status: SpaceStatus) {
149
+ if (space_status_callback) space_status_callback(status);
150
+ if (status.status === "running")
151
+ try {
152
+ console.log(host);
153
+ config = await resolve_config(`${http_protocol}//${host}`);
154
+ res(config_success(config));
155
+ } catch (e) {
156
+ if (space_status_callback) {
157
+ space_status_callback({
158
+ status: "error",
159
+ message: "Could not load this space.",
160
+ load_status: "error",
161
+ detail: "NOT_FOUND"
162
+ });
163
+ }
164
+ }
165
+ }
166
+
167
+ try {
168
+ config = await resolve_config(`${http_protocol}//${host}`);
169
+ res(config_success(config));
170
+ } catch (e) {
171
+ if (space_id) {
172
+ check_space_status(
173
+ space_id,
174
+ RE_SPACE_NAME.test(space_id) ? "space_name" : "subdomain",
175
+ handle_space_sucess
176
+ );
177
+ } else {
178
+ if (space_status_callback)
179
+ space_status_callback({
180
+ status: "error",
181
+ message: "Could not load this space.",
182
+ load_status: "error",
183
+ detail: "NOT_FOUND"
184
+ });
185
+ }
186
+ }
187
+ function make_predict(endpoint: string, payload: Payload) {
188
+ return new Promise((res, rej) => {
189
+ const trimmed_endpoint = endpoint.replace(/^\//, "");
190
+ let fn_index =
191
+ typeof payload.fn_index === "number"
192
+ ? payload.fn_index
193
+ : api_map[trimmed_endpoint];
194
+
195
+ if (skip_queue(fn_index, config)) {
196
+ fire_event({
197
+ type: "status",
198
+ endpoint,
199
+ status: "pending",
200
+ queue: false,
201
+ fn_index
202
+ });
203
+
204
+ post_data(
205
+ `${http_protocol}//${host + config.path}/run${
206
+ endpoint.startsWith("/") ? endpoint : `/${endpoint}`
207
+ }`,
208
+ {
209
+ ...payload,
210
+ session_hash
211
+ }
212
+ )
213
+ .then(([output, status_code]) => {
214
+ if (status_code == 200) {
215
+ fire_event({
216
+ type: "status",
217
+ endpoint,
218
+ fn_index,
219
+ status: "complete",
220
+ eta: output.average_duration,
221
+ queue: false
222
+ });
223
+
224
+ fire_event({
225
+ type: "data",
226
+ endpoint,
227
+ fn_index,
228
+ data: output.data
229
+ });
230
+ } else {
231
+ fire_event({
232
+ type: "status",
233
+ status: "error",
234
+ endpoint,
235
+ fn_index,
236
+ message: output.error,
237
+ queue: false
238
+ });
239
+ }
240
+ })
241
+ .catch((e) => {
242
+ fire_event({
243
+ type: "status",
244
+ status: "error",
245
+ message: e.message,
246
+ endpoint,
247
+ fn_index,
248
+ queue: false
249
+ });
250
+ throw new Error(e.message);
251
+ });
252
+ } else {
253
+ fire_event({
254
+ type: "status",
255
+ status: "pending",
256
+ queue: true,
257
+ endpoint,
258
+ fn_index
259
+ });
260
+
261
+ const ws_endpoint = `${ws_protocol}://${
262
+ host + config.path
263
+ }/queue/join`;
264
+
265
+ const websocket = new WebSocket(ws_endpoint);
266
+
267
+ ws_map.set(fn_index, websocket);
268
+ websocket.onclose = (evt) => {
269
+ if (!evt.wasClean) {
270
+ fire_event({
271
+ type: "status",
272
+ status: "error",
273
+ message: BROKEN_CONNECTION_MSG,
274
+ queue: true,
275
+ endpoint,
276
+ fn_index
277
+ });
278
+ }
279
+ };
280
+
281
+ websocket.onmessage = function (event) {
282
+ const _data = JSON.parse(event.data);
283
+ const { type, status, data } = handle_message(
284
+ _data,
285
+ last_status[fn_index]
286
+ );
287
+
288
+ if (type === "update" && status) {
289
+ // call 'status' listeners
290
+ fire_event({ type: "status", endpoint, fn_index, ...status });
291
+ if (status.status === "error") {
292
+ websocket.close();
293
+ rej(status);
294
+ }
295
+ } else if (type === "hash") {
296
+ websocket.send(JSON.stringify({ fn_index, session_hash }));
297
+ return;
298
+ } else if (type === "data") {
299
+ websocket.send(JSON.stringify({ ...payload, session_hash }));
300
+ } else if (type === "complete") {
301
+ fire_event({
302
+ type: "status",
303
+ ...status,
304
+ status: status?.status!,
305
+ queue: true,
306
+ endpoint,
307
+ fn_index
308
+ });
309
+ websocket.close();
310
+ } else if (type === "generating") {
311
+ fire_event({
312
+ type: "status",
313
+ ...status,
314
+ status: status?.status!,
315
+ queue: true,
316
+ endpoint,
317
+ fn_index
318
+ });
319
+ }
320
+ if (data) {
321
+ fire_event({
322
+ type: "data",
323
+ data: data.data,
324
+ endpoint,
325
+ fn_index
326
+ });
327
+ res({ data: data.data });
328
+ }
329
+ };
330
+ }
331
+ });
332
+ }
333
+
334
+ /**
335
+ * Run a prediction.
336
+ * @param endpoint - The prediction endpoint to use.
337
+ * @param status_callback - A function that is called with the current status of the prediction immediately and every time it updates.
338
+ * @return Returns the data for the prediction or an error message.
339
+ */
340
+ function predict(endpoint: string, payload: Payload) {
341
+ return make_predict(endpoint, payload);
342
+ }
343
+ });
344
+ }
345
+
346
+ function skip_queue(id: number, config: Config) {
347
+ return (
348
+ !(config?.dependencies?.[id].queue === null
349
+ ? config.enable_queue
350
+ : config?.dependencies?.[id].queue) || false
351
+ );
352
+ }
353
+
354
+ async function resolve_config(endpoint?: string): Promise<Config> {
355
+ if (window.gradio_config && location.origin !== "http://localhost:9876") {
356
+ const path = window.gradio_config.root;
357
+ const config = window.gradio_config;
358
+ config.root = endpoint + config.root;
359
+ return { ...config, path: path };
360
+ } else if (endpoint) {
361
+ let response = await fetch(`${endpoint}/config`);
362
+
363
+ if (response.status === 200) {
364
+ const config = await response.json();
365
+ config.path = config.path ?? "";
366
+ config.root = endpoint;
367
+ return config;
368
+ } else {
369
+ throw new Error("Could not get config.");
370
+ }
371
+ }
372
+
373
+ throw new Error("No config or app endpoint found");
374
+ }
375
+
376
+ async function check_space_status(
377
+ id: string,
378
+ type: "subdomain" | "space_name",
379
+ space_status_callback: SpaceStatusCallback
380
+ ) {
381
+ let endpoint =
382
+ type === "subdomain"
383
+ ? `https://huggingface.co/api/spaces/by-subdomain/${id}`
384
+ : `https://huggingface.co/api/spaces/${id}`;
385
+ let response;
386
+ let _status;
387
+ try {
388
+ response = await fetch(endpoint);
389
+ _status = response.status;
390
+ if (_status !== 200) {
391
+ throw new Error();
392
+ }
393
+ response = await response.json();
394
+ } catch (e) {
395
+ space_status_callback({
396
+ status: "error",
397
+ load_status: "error",
398
+ message: "Could not get space status",
399
+ detail: "NOT_FOUND"
400
+ });
401
+ return;
402
+ }
403
+
404
+ if (!response || _status !== 200) return;
405
+ const {
406
+ runtime: { stage },
407
+ id: space_name
408
+ } = response;
409
+
410
+ switch (stage) {
411
+ case "STOPPED":
412
+ case "SLEEPING":
413
+ space_status_callback({
414
+ status: "sleeping",
415
+ load_status: "pending",
416
+ message: "Space is asleep. Waking it up...",
417
+ detail: stage
418
+ });
419
+
420
+ setTimeout(() => {
421
+ check_space_status(id, type, space_status_callback);
422
+ }, 1000);
423
+ break;
424
+ // poll for status
425
+ case "RUNNING":
426
+ case "RUNNING_BUILDING":
427
+ space_status_callback({
428
+ status: "running",
429
+ load_status: "complete",
430
+ message: "",
431
+ detail: stage
432
+ });
433
+ // load_config(source);
434
+ // launch
435
+ break;
436
+ case "BUILDING":
437
+ space_status_callback({
438
+ status: "building",
439
+ load_status: "pending",
440
+ message: "Space is building...",
441
+ detail: stage
442
+ });
443
+
444
+ setTimeout(() => {
445
+ check_space_status(id, type, space_status_callback);
446
+ }, 1000);
447
+ break;
448
+ default:
449
+ space_status_callback({
450
+ status: "space_error",
451
+ load_status: "error",
452
+ message: "This space is experiencing an issue.",
453
+ detail: stage,
454
+ discussions_enabled: await discussions_enabled(space_name)
455
+ });
456
+ break;
457
+ }
458
+ }
459
+
460
+ function handle_message(
461
+ data: any,
462
+ last_status: Status["status"]
463
+ ): {
464
+ type: "hash" | "data" | "update" | "complete" | "generating" | "none";
465
+ data?: any;
466
+ status?: Status;
467
+ } {
468
+ const queue = true;
469
+ switch (data.msg) {
470
+ case "send_data":
471
+ return { type: "data" };
472
+ case "send_hash":
473
+ return { type: "hash" };
474
+ case "queue_full":
475
+ return {
476
+ type: "update",
477
+ status: {
478
+ queue,
479
+ message: QUEUE_FULL_MSG,
480
+ status: "error"
481
+ }
482
+ };
483
+ case "estimation":
484
+ return {
485
+ type: "update",
486
+ status: {
487
+ queue,
488
+ status: last_status || "pending",
489
+ size: data.queue_size,
490
+ position: data.rank,
491
+ eta: data.rank_eta
492
+ }
493
+ };
494
+ case "progress":
495
+ return {
496
+ type: "update",
497
+ status: {
498
+ queue,
499
+ status: "pending",
500
+ progress: data.progress_data
501
+ }
502
+ };
503
+ case "process_generating":
504
+ return {
505
+ type: "generating",
506
+ status: {
507
+ queue,
508
+ message: !data.success ? data.output.error : null,
509
+ status: data.success ? "generating" : "error",
510
+ progress: data.progress_data,
511
+ eta: data.average_duration
512
+ },
513
+ data: data.success ? data.output : null
514
+ };
515
+ case "process_completed":
516
+ return {
517
+ type: "complete",
518
+ status: {
519
+ queue,
520
+ message: !data.success ? data.output.error : undefined,
521
+ status: data.success ? "complete" : "error",
522
+ progress: data.progress_data,
523
+ eta: data.output.average_duration
524
+ },
525
+ data: data.success ? data.output : null
526
+ };
527
+ case "process_starts":
528
+ return {
529
+ type: "update",
530
+ status: {
531
+ queue,
532
+ status: "pending",
533
+ size: data.rank,
534
+ position: 0
535
+ }
536
+ };
537
+ }
538
+
539
+ return { type: "none", status: { status: "error", queue } };
540
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { client, post_data, upload_files } from "./client";
2
+ export type { SpaceStatus } from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,88 @@
1
+ export interface Config {
2
+ auth_required: boolean | undefined;
3
+ auth_message: string;
4
+ components: any[];
5
+ css: string | null;
6
+ dependencies: any[];
7
+ dev_mode: boolean;
8
+ enable_queue: boolean;
9
+ layout: any;
10
+ mode: "blocks" | "interface";
11
+ root: string;
12
+ theme: string;
13
+ title: string;
14
+ version: string;
15
+ is_space: boolean;
16
+ is_colab: boolean;
17
+ show_api: boolean;
18
+ stylesheets: string[];
19
+ path: string;
20
+ }
21
+
22
+ export interface Payload {
23
+ data: Array<unknown>;
24
+ fn_index: number;
25
+ }
26
+
27
+ export interface PostResponse {
28
+ error?: string;
29
+ [x: string]: any;
30
+ }
31
+ export interface UploadResponse {
32
+ error?: string;
33
+ files?: Array<string>;
34
+ }
35
+
36
+ export interface Status {
37
+ queue: boolean;
38
+ status: "pending" | "error" | "complete" | "generating";
39
+ size?: number;
40
+ position?: number;
41
+ eta?: number;
42
+ message?: string;
43
+ progress?: Array<{
44
+ progress: number | null;
45
+ index: number | null;
46
+ length: number | null;
47
+ unit: string | null;
48
+ desc: string | null;
49
+ }>;
50
+ }
51
+
52
+ export interface SpaceStatusNormal {
53
+ status: "sleeping" | "running" | "building" | "error" | "stopped";
54
+ detail:
55
+ | "SLEEPING"
56
+ | "RUNNING"
57
+ | "RUNNING_BUILDING"
58
+ | "BUILDING"
59
+ | "NOT_FOUND";
60
+ load_status: "pending" | "error" | "complete" | "generating";
61
+ message: string;
62
+ }
63
+ export interface SpaceStatusError {
64
+ status: "space_error";
65
+ detail: "NO_APP_FILE" | "CONFIG_ERROR" | "BUILD_ERROR" | "RUNTIME_ERROR";
66
+ load_status: "error";
67
+ message: string;
68
+ discussions_enabled: boolean;
69
+ }
70
+ export type SpaceStatus = SpaceStatusNormal | SpaceStatusError;
71
+
72
+ export type status_callback_function = (a: Status) => void;
73
+ export type SpaceStatusCallback = (a: SpaceStatus) => void;
74
+
75
+ export type EventType = "data" | "status";
76
+
77
+ export interface EventMap {
78
+ data: Record<string, any>;
79
+ status: Status;
80
+ }
81
+
82
+ export type Event<K extends EventType> = {
83
+ [P in K]: EventMap[P] & { type: P; endpoint: string; fn_index: number };
84
+ }[K];
85
+ export type EventListener<K extends EventType> = (event: Event<K>) => void;
86
+ export type ListenerMap<K extends EventType> = {
87
+ [P in K]?: EventListener<K>[];
88
+ };
package/src/utils.ts ADDED
@@ -0,0 +1,101 @@
1
+ import type { Config } from "./types";
2
+
3
+ export function determine_protocol(endpoint: string): {
4
+ ws_protocol: "ws" | "wss";
5
+ http_protocol: "http:" | "https:";
6
+ host: string;
7
+ } {
8
+ if (endpoint.startsWith("http")) {
9
+ const { protocol, host } = new URL(endpoint);
10
+
11
+ if (host.endsWith("hf.space")) {
12
+ return {
13
+ ws_protocol: "wss",
14
+ host: host,
15
+ http_protocol: protocol as "http:" | "https:"
16
+ };
17
+ } else {
18
+ return {
19
+ ws_protocol: protocol === "https:" ? "wss" : "ws",
20
+ http_protocol: protocol as "http:" | "https:",
21
+ host
22
+ };
23
+ }
24
+ }
25
+
26
+ // default to secure if no protocol is provided
27
+ return {
28
+ ws_protocol: "wss",
29
+ http_protocol: "https:",
30
+ host: endpoint
31
+ };
32
+ }
33
+
34
+ export const RE_SPACE_NAME = /^[^\/]*\/[^\/]*$/;
35
+ export const RE_SPACE_DOMAIN = /.*hf\.space\/{0,1}$/;
36
+ export async function process_endpoint(app_reference: string): Promise<{
37
+ space_id: string | false;
38
+ host: string;
39
+ ws_protocol: "ws" | "wss";
40
+ http_protocol: "http:" | "https:";
41
+ }> {
42
+ const _app_reference = app_reference.trim();
43
+
44
+ if (RE_SPACE_NAME.test(_app_reference)) {
45
+ const _host = (
46
+ await (
47
+ await fetch(`https://huggingface.co/api/spaces/${_app_reference}/host`)
48
+ ).json()
49
+ ).host;
50
+ return {
51
+ space_id: app_reference,
52
+ ...determine_protocol(_host)
53
+ };
54
+ }
55
+
56
+ if (RE_SPACE_DOMAIN.test(_app_reference)) {
57
+ const { ws_protocol, http_protocol, host } =
58
+ determine_protocol(_app_reference);
59
+
60
+ return {
61
+ space_id: host.replace(".hf.space", ""),
62
+ ws_protocol,
63
+ http_protocol,
64
+ host
65
+ };
66
+ }
67
+
68
+ return {
69
+ space_id: false,
70
+ ...determine_protocol(_app_reference)
71
+ };
72
+ }
73
+
74
+ export function map_names_to_ids(fns: Config["dependencies"]) {
75
+ let apis: Record<string, number> = {};
76
+
77
+ fns.forEach(({ api_name }, i) => {
78
+ if (api_name) apis[api_name] = i;
79
+ });
80
+
81
+ return apis;
82
+ }
83
+
84
+ const RE_DISABLED_DISCUSSION =
85
+ /^(?=[^]*\b[dD]iscussions{0,1}\b)(?=[^]*\b[dD]isabled\b)[^]*$/;
86
+ export async function discussions_enabled(space_id: string) {
87
+ try {
88
+ const r = await fetch(
89
+ `https://huggingface.co/api/spaces/${space_id}/discussions`,
90
+ {
91
+ method: "HEAD"
92
+ }
93
+ );
94
+ const error = r.headers.get("x-error-message");
95
+
96
+ if (error && RE_DISABLED_DISCUSSION.test(error)) return false;
97
+ else return true;
98
+ } catch (e) {
99
+ return false;
100
+ }
101
+ }