* feat(client): endpoint type definitions * fix(client): endpoint generation * fix(demo): streaming page type * chore: update reference docs * chore: prepare client release * chore: update reference docs
213 lines
6.5 KiB
TypeScript
213 lines
6.5 KiB
TypeScript
"use client";
|
|
|
|
import { createFalClient } from "@fal-ai/client";
|
|
import { useCallback, useMemo, useState } from "react";
|
|
|
|
const fal = createFalClient({
|
|
// credentials: 'FAL_KEY_ID:FAL_KEY_SECRET',
|
|
proxyUrl: "/api/fal/proxy",
|
|
});
|
|
|
|
type ErrorProps = {
|
|
error: any;
|
|
};
|
|
|
|
function Error(props: ErrorProps) {
|
|
if (!props.error) {
|
|
return null;
|
|
}
|
|
return (
|
|
<div
|
|
className="mb-4 rounded bg-red-50 p-4 text-sm text-red-800 dark:bg-gray-800 dark:text-red-400"
|
|
role="alert"
|
|
>
|
|
<span className="font-medium">Error</span> {props.error.message}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
type RecorderOptions = {
|
|
maxDuration?: number;
|
|
};
|
|
|
|
function useMediaRecorder({ maxDuration = 10000 }: RecorderOptions = {}) {
|
|
const [isRecording, setIsRecording] = useState(false);
|
|
const [mediaRecorder, setMediaRecorder] = useState<MediaRecorder | null>(
|
|
null,
|
|
);
|
|
|
|
const record = useCallback(async () => {
|
|
setIsRecording(true);
|
|
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
const audioChunks: BlobPart[] = [];
|
|
const recorder = new MediaRecorder(stream);
|
|
setMediaRecorder(recorder);
|
|
return new Promise<File>((resolve, reject) => {
|
|
try {
|
|
recorder.addEventListener("dataavailable", (event) => {
|
|
audioChunks.push(event.data);
|
|
});
|
|
recorder.addEventListener("stop", async () => {
|
|
const fileOptions = { type: "audio/wav" };
|
|
const audioBlob = new Blob(audioChunks, fileOptions);
|
|
const audioFile = new File(
|
|
[audioBlob],
|
|
`recording_${Date.now()}.wav`,
|
|
fileOptions,
|
|
);
|
|
setIsRecording(false);
|
|
resolve(audioFile);
|
|
});
|
|
setTimeout(() => {
|
|
recorder.stop();
|
|
recorder.stream.getTracks().forEach((track) => track.stop());
|
|
}, maxDuration);
|
|
recorder.start();
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}, [maxDuration]);
|
|
|
|
const stopRecording = useCallback(() => {
|
|
setIsRecording(false);
|
|
mediaRecorder?.stop();
|
|
mediaRecorder?.stream.getTracks().forEach((track) => track.stop());
|
|
}, [mediaRecorder]);
|
|
|
|
return { record, stopRecording, isRecording };
|
|
}
|
|
|
|
export default function WhisperDemo() {
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<Error | null>(null);
|
|
const [logs, setLogs] = useState<string[]>([]);
|
|
const [audioFile, setAudioFile] = useState<File | null>(null);
|
|
const [result, setResult] = useState<any>(null); // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
const [elapsedTime, setElapsedTime] = useState<number>(0);
|
|
|
|
const { record, stopRecording, isRecording } = useMediaRecorder();
|
|
|
|
const reset = () => {
|
|
setLoading(false);
|
|
setError(null);
|
|
setLogs([]);
|
|
setElapsedTime(0);
|
|
setResult(null);
|
|
};
|
|
|
|
const audioFileLocalUrl = useMemo(() => {
|
|
if (!audioFile) {
|
|
return null;
|
|
}
|
|
return URL.createObjectURL(audioFile);
|
|
}, [audioFile]);
|
|
|
|
const transcribeAudio = async (audioFile: File) => {
|
|
reset();
|
|
setLoading(true);
|
|
const start = Date.now();
|
|
try {
|
|
const result = await fal.subscribe("fal-ai/wizper", {
|
|
input: {
|
|
audio_url: audioFile,
|
|
version: "3",
|
|
},
|
|
logs: true,
|
|
onQueueUpdate(update) {
|
|
setElapsedTime(Date.now() - start);
|
|
if (
|
|
update.status === "IN_PROGRESS" ||
|
|
update.status === "COMPLETED"
|
|
) {
|
|
setLogs((update.logs || []).map((log) => log.message));
|
|
}
|
|
},
|
|
});
|
|
setResult(result);
|
|
} catch (error: any) {
|
|
setError(error);
|
|
} finally {
|
|
setLoading(false);
|
|
setElapsedTime(Date.now() - start);
|
|
}
|
|
};
|
|
return (
|
|
<div className="min-h-screen bg-gray-100 dark:bg-gray-900">
|
|
<main className="container flex w-full flex-1 flex-col items-center justify-center space-y-8 py-10 text-gray-900 dark:text-gray-50">
|
|
<h1 className="mb-8 text-4xl font-bold">
|
|
Hello <code className="text-pink-600">fal</code> and{" "}
|
|
<code className="text-indigo-500">whisper</code>
|
|
</h1>
|
|
|
|
<div className="flex flex-row space-x-2">
|
|
<button
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
try {
|
|
if (isRecording) {
|
|
stopRecording();
|
|
} else {
|
|
const recordedAudio = await record();
|
|
setAudioFile(recordedAudio);
|
|
}
|
|
} catch (e: any) {
|
|
setError(e);
|
|
}
|
|
}}
|
|
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none disabled:opacity-70"
|
|
disabled={loading}
|
|
>
|
|
{isRecording ? "Stop Recording" : "Record"}
|
|
</button>
|
|
<button
|
|
onClick={async (e) => {
|
|
e.preventDefault();
|
|
if (audioFile) {
|
|
try {
|
|
await transcribeAudio(audioFile);
|
|
} catch (e: any) {
|
|
setError(e);
|
|
}
|
|
}
|
|
}}
|
|
className="focus:shadow-outline mx-auto rounded bg-indigo-600 py-3 px-6 text-lg font-bold text-white hover:bg-indigo-700 focus:outline-none disabled:opacity-70"
|
|
disabled={loading || !audioFile}
|
|
>
|
|
{loading ? "Transcribing..." : "Transcribe"}
|
|
</button>
|
|
</div>
|
|
|
|
{audioFileLocalUrl && (
|
|
<div className="my-2 w-full text-lg">
|
|
<audio className="mx-auto" controls src={audioFileLocalUrl} />
|
|
</div>
|
|
)}
|
|
|
|
<Error error={error} />
|
|
|
|
<div className="flex w-full flex-col space-y-4">
|
|
<div className="space-y-2">
|
|
<h3 className="text-xl font-light">JSON Result</h3>
|
|
<p className="text-current/80 text-sm">
|
|
{`Elapsed Time (seconds): ${(elapsedTime / 1000).toFixed(2)}`}
|
|
</p>
|
|
<pre className="h-96 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
|
{result
|
|
? JSON.stringify(result, null, 2)
|
|
: "// result pending..."}
|
|
</pre>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<h3 className="text-xl font-light">Logs</h3>
|
|
<pre className="h-60 w-full overflow-auto whitespace-pre rounded bg-black/70 font-mono text-sm text-white/80">
|
|
{logs.filter(Boolean).join("\n")}
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|