Fullstack integration
The JavaScript Client library simplify the integration of workflows in web applications by providing utilities to stream deployments results to the browser, while preserving your credentials and workflow informations private.
We recommend implementing the following flow:
To stream progress and results, you can use the Client#runStream
method which takes the same parameters as Client#run
but
return progress state as an AsyncGenerator
.
import {Client} from "@ikomia/ikclient";
const client = new Client({
url: "https://your.scale.endpoint.url",
});
const stream = await client.runStream({
image: "https://production-media.paperswithcode.com/datasets/Foggy_Cityscapes-0000003414-fb7dc023.jpg",
taskName: "infer_mmlab_segmentation",
});
for await (const progress of stream) {
console.log(progress);
if (progress.results) {
// Do something with the results
console.log(progress.results.getOutputs());
}
}
You can then serialize and deserialize the generator using one of our provided utilities to stream the results to the browser:
toStreamingResponse
andprocessStreamingResponse
for serializing into a standard Response object that sends Server-sent events.toReadableStream
andprocessReadableStream
for serializing into a ReadableStream formatted as a Server-sent event.- By JSON serializing the generator events and deserializing them using
processJsonStream
.
Using these utilities, you can easily integrate your deployment into a fullstack web application with many modern frameworks and technologies:
Implementing the backend endpoint
- Express
- Next.js
- Nuxt
- SvelteKit
- tRPC
Implement a route that calls your deployment and streams the results using Server-sent events.
We can use the toReadableStream
utility to serialize the generator into a readable stream and pipe it to the response:
import {Readable} from 'node:stream';
import express from 'express';
import {Client} from '@ikomia/ikclient';
import {toReadableStream} from '@ikomia/ikclient/proxy/server';
const app = express();
const imageGenerator = new Client({
url: process.env.ENDPOINT_URL,
});
app.get('/api/generate-image', async (req, res) => {
// Add your own logic here (e.g. authentication, rate limiting, billing, etc.)
const prompt = req.query.prompt;
if (!prompt) {
res.status(400).send('Missing prompt');
return;
}
const stream = imageGenerator.runStream({
parameters: {prompt}
});
// Set status and headers for Server-sent events
res.status(200);
res.setHeader('Content-Type', 'text/event-stream');
// Stream the results
Readable.fromWeb(toReadableStream(stream)).pipe(res);
});
app.listen(3000, () => {
console.log('Express server initialized');
});
If you are using Next.js Page Router for your project, note that you'll also need to use the App Router for this endpoint as API Routes does not support streaming. Since Next.js 13+, you can use both router in the same project.
Implement a Route Handler to call your deployment and stream the results to the browser
using toStreamingResponse
:
import {Client} from '@ikomia/ikclient';
import {toStreamingResponse} from '@ikomia/ikclient/proxy/server';
const imageGenerator = new Client({
url: process.env.ENDPOINT_URL!,
});
export async function GET(request: Request) {
// Add your own logic here (e.g. authentication, rate limiting, billing, etc.)
const url = new URL(request.url);
const prompt = url.searchParams.get('prompt');
if (!prompt) {
return new Response('Missing prompt', {status: 400});
}
const stream = imageGenerator.runStream({
parameters: {prompt},
});
// Return as a streaming response
return toStreamingResponse(stream);
}
Implement an event handler to call your deployment and stream the results to the browser using toStreamingResponse
:
import {Client} from '@ikomia/ikclient';
import {toStreamingResponse} from '@ikomia/ikclient/proxy/server';
const imageGenerator = new Client({
url: process.env.ENDPOINT_URL!,
});
export default defineEventHandler(async event => {
// Add your own logic here (e.g. authentication, rate limiting, billing, etc.)
const {prompt} = getQuery(event);
if (!prompt) {
throw createError({statusCode: 400});
}
const stream = imageGenerator.runStream({
parameters: {prompt},
});
// Return as a streaming response
return toStreamingResponse(stream);
});
Implement a request handler to call your deployment and stream the results to the browser using toStreamingResponse
:
import {Client} from '@ikomia/ikclient';
import {toStreamingResponse} from '@ikomia/ikclient/proxy/server';
import type {RequestHandler} from './$types';
const imageGenerator = new Client({
url: process.env.ENDPOINT_URL!,
});
export const GET = (async ({request}) => {
// Add your own logic here (e.g. authentication, rate limiting, billing, etc.)
const url = new URL(request.url);
const prompt = url.searchParams.get('prompt');
if (!prompt) {
return new Response('Missing prompt', {status: 400});
}
const stream = imageGenerator.runStream({
parameters: {prompt},
});
// Return as a streaming response
return toStreamingResponse(stream);
}) satisfies RequestHandler;
Implement a tRPC procedure to call your deployment and stream back the results to the client:
import {createHTTPServer} from '@trpc/server/adapters/standalone';
import {z} from 'zod';
import {Client} from '@ikomia/ikclient';
import {publicProcedure, router} from './trpc.js';
const imageGenerator = new Client({
url: process.env.ENDPOINT_URL!,
});
const appRouter = router({
stableDiffusion: publicProcedure.input(z.string()).query(async function* ({input}) {
// Directly stream the progress to the client
yield* imageGenerator.runStream({
parameters: {
prompt: input,
},
});
}),
});
// Export type router type signature, this is used by the client.
export type AppRouter = typeof appRouter;
const server = createHTTPServer({
router: appRouter,
});
server.listen(3000);
Since you are responsible of implementing your backend endpoint, you can easily incorporate your own logic to authenticate users, rate limit requests, add billing or any other custom constraints.
Implementing the client-side code
- TypeScript
- React
- Vue
- Svelte
- tRPC
Just fetch your endpoint and process the results using the processStreamingResponse
utility:
import {processStreamingResponse} from '@ikomia/ikclient/proxy/browser';
import {ImageIO} from '@ikomia/ikclient/io';
const prompt = 'A puppy playing in the snow';
const response = await fetch(`/api/generate-image?prompt=${prompt}`);
const results = await processStreamingResponse(response);
// Get the output image
const resultImage = results.getOutput<ImageIO>(0);
Implement a React component that call your endpoint and display the result:
'use client';
import {ImageIO} from '@ikomia/ikclient/io';
import {processStreamingResponse} from '@ikomia/ikclient/proxy/browser';
import {useState} from 'react';
import {Submit} from './Submit';
export function ImageGenerator() {
const [eta, setEta] = useState<number | undefined>(undefined);
const [error, setError] = useState<Error | undefined>(undefined);
const [resultImage, setResultImage] = useState<ImageIO | undefined>(
undefined
);
return (
<>
<form
action={async (formData: FormData) => {
const prompt = formData.get('prompt');
if (!prompt) {
return;
}
setError(undefined);
setResultImage(undefined);
try {
const response = await fetch(`/api/generate-image?prompt=${prompt}`);
// Process the Server-sent events
const results = await processStreamingResponse(response, progress =>
setEta(progress.eta[1] ?? undefined) // Update the ETA
);
// Get the output image
setResultImage(results.getOutput<ImageIO>(0));
} catch (error) {
setError(error as Error);
}
}}
>
<input type="text" name="prompt" placeholder="Enter a prompt..." />
<Submit eta={eta} />
</form>
{error && <p>Error: {error.message}</p>}
{resultImage && <img src={resultImage.dataUrl} alt="Generated image" />}
</>
);
}
'use client';
import {useFormStatus} from 'react-dom';
export function Submit(props: {eta?: number}) {
const {eta} = props;
const {pending} = useFormStatus();
return (
<div>
<button type="submit" disabled={pending}>
{pending ? (
<>
Generating...
{eta != null && ` (ETA: ${Math.round(eta / 1000)}s)`}
</>
) : (
'Generate'
)}
</button>
</div>
);
}
Implement a Vue component that call your endpoint and display the result:
<template>
<div>
<form @submit.prevent="handleSubmit">
<input type="text" v-model="prompt" placeholder="Enter a prompt..." />
<button type="submit" :disabled="pending">
{{pending ? `Generating... ${eta > 0 ? ` (ETA: ${Math.round(eta / 1000)}s)` : ''}` : 'Generate'}}
</button>
</form>
<p v-if="error">Error: {{error.message}}</p>
<img v-if="resultImage" :src="resultImage" alt="Generated image" />
</div>
</template>
<script setup lang="ts">
import {ImageIO} from '@ikomia/ikclient/io';
import {processStreamingResponse} from '@ikomia/ikclient/proxy/browser';
const prompt = ref('');
const pending = ref(false);
const eta = ref(0);
const error = ref(null);
const resultImage = ref('');
const handleSubmit = async () => {
if (!prompt.value) return;
error.value = undefined;
resultImage.value = undefined;
pending.value = true;
try {
const response = await fetch(`/api/generate-image?prompt=${prompt.value}`);
// Process the Server-sent events
const results = await processStreamingResponse(response, progress => {
eta.value = progress.eta[1] ?? 0; // Update the ETA
});
// Get the output image
resultImage.value = results.getOutput<ImageIO>(0).dataUrl;
} catch (err) {
error.value = err;
} finally {
pending.value = false;
}
};
</script>
Implement a Svelte component that call your endpoint and display the result:
<script lang="ts">
import {writable} from 'svelte/store';
import {ImageIO} from '@ikomia/ikclient/io';
import {processStreamingResponse} from '@ikomia/ikclient/proxy/browser';
let prompt = '';
let pending = writable(false);
let eta = writable(0);
let error = writable(null);
let resultImage = writable('');
const handleSubmit = async event => {
event.preventDefault();
if (!prompt) {
return;
}
$error = null;
$pending = true;
$eta = 0;
try {
const response = await fetch(`/api/generate-image?prompt=${prompt}`);
// Process the Server-sent events
const results = await processStreamingResponse(response, progress => {
$eta = progress.eta[1] ?? 0; // Update the ETA
});
// Get the output image
$resultImage = results.getOutput<ImageIO>(0).dataUrl;
} catch (err) {
$error = err;
} finally {
$pending = false;
}
};
</script>
<div>
<form on:submit={handleSubmit}>
<input type="text" bind:value={prompt} placeholder="Enter a prompt..." />
<button type="submit" disabled={$pending}>
{$pending ? `Generating... ${$eta > 0 ? ` (ETA: ${Math.round($eta / 1000)}s)` : ''}` : 'Generate'}
</button>
</form>
{#if $error}
<p>Error: {$error.message}</p>
{/if}
{#if $resultImage}
<img src={$resultImage} alt="Generated image" />
{/if}
</div>
Just call your procedure and use processJsonStream
to easily process the results:
import {
createTRPCClient,
splitLink,
unstable_httpBatchStreamLink,
unstable_httpSubscriptionLink,
} from '@trpc/client';
import {processJsonStream} from '@ikomia/ikclient/proxy/browser';
import {ImageIO} from '@ikomia/ikclient/io';
// Import typings from server code
import type { AppRouter } from '../server/index.js';
// Initialize the tRPC client
const trpc = createTRPCClient<AppRouter>({
links: [
splitLink({
condition: (op) => op.type === 'subscription',
true: unstable_httpSubscriptionLink({
url: 'http://localhost:3000',
}),
false: unstable_httpBatchStreamLink({
url: 'http://localhost:3000',
}),
}),
],
});
// Fetch the stream and process it
const stream = await trpc.stableDiffusion.query('A puppy that fly like a superhero');
const results = await processJsonStream(stream, progress => {
const [_, maxEta] = progress.eta;
if (maxEta) {
console.log(`ETA: ${Math.round(maxEta / 1000)}s`);
}
});
// Get the output image
const image = results.getOutput<ImageIO>(0);
console.log('Image:', image.dataUrl);