Skip to content

Commit

Permalink
feat(Dictation): dictation tool using wavesurfer
Browse files Browse the repository at this point in the history
  • Loading branch information
adarshpastakia committed Oct 18, 2024
1 parent 21167df commit e6ff12b
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/core/src/types/icons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export module CoreIcons {
"M5.541 2.159c-0.153-0.1-0.34-0.159-0.541-0.159-0.552 0-1 0.448-1 1v18c-0.001 0.182 0.050 0.372 0.159 0.541 0.299 0.465 0.917 0.599 1.382 0.3l14-9c0.114-0.072 0.219-0.174 0.3-0.3 0.299-0.465 0.164-1.083-0.3-1.382zM6 4.832l11.151 7.168-11.151 7.168z";
export const pause =
"M6 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM7 5h2v14h-2zM14 3c-0.552 0-1 0.448-1 1v16c0 0.552 0.448 1 1 1h4c0.552 0 1-0.448 1-1v-16c0-0.552-0.448-1-1-1zM15 5h2v14h-2z";
export const stop = "M18,18H6V6H18V18Z";
export const volumeOn =
"M10 7.081v9.839l-3.375-2.7c-0.17-0.137-0.388-0.22-0.625-0.22h-3v-4h3c0.218 0.001 0.439-0.071 0.625-0.219zM10.375 4.219l-4.726 3.781h-3.649c-0.552 0-1 0.448-1 1v6c0 0.552 0.448 1 1 1h3.649l4.726 3.781c0.431 0.345 1.061 0.275 1.406-0.156 0.148-0.185 0.22-0.407 0.219-0.625v-14c0-0.552-0.448-1-1-1-0.237 0-0.455 0.083-0.625 0.219zM18.363 5.637c1.757 1.758 2.635 4.059 2.635 6.364 0 2.304-0.878 4.605-2.635 6.362-0.39 0.391-0.39 1.024 0 1.414s1.024 0.39 1.414 0c2.147-2.147 3.22-4.963 3.221-7.776s-1.074-5.63-3.221-7.778c-0.39-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414zM14.833 9.167c0.781 0.781 1.171 1.803 1.171 2.828s-0.39 2.047-1.171 2.828c-0.39 0.391-0.39 1.024 0 1.414s1.024 0.39 1.414 0c1.171-1.171 1.757-2.708 1.757-4.242s-0.586-3.071-1.757-4.242c-0.39-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z";
export const volumeOff =
Expand Down
159 changes: 159 additions & 0 deletions packages/media/src/dictation/Dictation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
/*
* React Fabric
* @version: 1.0.0
*
*
* The MIT License (MIT)
* Copyright (c) 2024 Adarsh Pastakia
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { Button, CoreIcons, HotKey } from "@react-fabric/core";
import { Format } from "@react-fabric/utilities";
import classNames from "classnames";
import { Fragment, useEffect, useRef, useState } from "react";
import WaveSurfer from "wavesurfer.js";
import RecordPlugin from "wavesurfer.js/dist/plugins/record";

export interface DictationProps {
size?: "sm";
variant?: "solid" | "outline";
hotkey?: string;
onRecord?: (blob: Blob) => void;
}

export const Dictation = ({
hotkey = "alt+t",
size,
variant,
onRecord,
}: DictationProps) => {
const [error, setError] = useState("");
const [progress, setProgress] = useState(0);
const [recording, setRecording] = useState(false);
const wavesurfer = useRef<WaveSurfer>();
const record = useRef<RecordPlugin>();
const container = useRef<HTMLDivElement>(null);

useEffect(() => {
if (container.current) {
wavesurfer.current = new WaveSurfer({
container: container.current,
waveColor: "#0190ff",
cursorWidth: 0,
width: 96,
height: size === "sm" ? 28 : 32,
barWidth: 1,
barRadius: 3,
barGap: 1,
barHeight: 1.5,
minPxPerSec: 16,
});

record.current = wavesurfer.current.registerPlugin(
RecordPlugin.create({
scrollingWaveform: true,
renderRecordedAudio: false,
}),
);

record.current.on("record-progress", (time) => {
setProgress(time);
});

return () => {
wavesurfer.current?.destroy();
};
}
}, [size]);

useEffect(() => {
record.current?.on("record-end", (blob) => {
setRecording(false);
onRecord?.(blob);
});
}, [onRecord]);

const startDictation = useRef(() => {
record.current
?.startRecording()
.then(() => {
setRecording(true);
})
.catch(() => {
setError("Unable to access microphone");
});
});

const stopDictation = useRef(() => {
record.current?.stopRecording();
});

return (
<div
className={classNames(
"inline-flex rounded-full overflow-hidden items-center outline outline-tint-100 bg-dimmed -outline-offset-1 self-center",
size === "sm" ? "h-7" : "h-8",
)}
>
<div
ref={container}
className={classNames(
"overflow-hidden w-24 bg-base rounded-full shadow-inner shadow-base",
size === "sm" ? "h-7" : "h-8",
!recording && "hidden",
)}
style={{
boxShadow: "inset 0 1px 2px 0 var(--tw-shadow-color)",
}}
/>
{error && (
<span className="text-xs font-medium text-danger-500 px-2">
{error}
</span>
)}
{!error && !recording && (
<Fragment>
<HotKey global keyCombo={hotkey} handler={startDictation.current} />
<Button
size={size}
icon={CoreIcons.mic}
aria-label="Start dictation"
rounded
variant={variant}
onClick={startDictation.current}
/>
</Fragment>
)}
{!error && recording && (
<Fragment>
<HotKey global keyCombo="esc" handler={stopDictation.current} />
<span className="text-xs px-2">{Format.duration(progress)}</span>
<Button
size={size}
icon={CoreIcons.stop}
aria-label="Stop dictation"
rounded
iconAlign="end"
variant="solid"
color="danger"
onClick={stopDictation.current}
/>
</Fragment>
)}
</div>
);
};
4 changes: 2 additions & 2 deletions packages/media/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@

export { AudioPlayer, type AudioPlayerRef } from "./audio/AudioPlayer";
export { type CanvasRef as ImageViewerRef } from "./canvas/Context";
export { Dictation } from "./dictation/Dictation";
export { ImageViewer } from "./image/ImageViewer";
export { Thumbnail } from "./thumbnail/Thumbnail";
export { type VideoPlayerRef } from "./video/types";
export { VideoPlayer } from "./video/VideoPlayer";

export { Thumbnail } from "./thumbnail/Thumbnail";
52 changes: 52 additions & 0 deletions packages/media/stories/Dictation.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* React Fabric
* @version: 1.0.0
*
*
* The MIT License (MIT)
* Copyright (c) 2024 Adarsh Pastakia
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction,
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial
* portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
* TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

import { SearchBar } from "@react-fabric/searchbar";
import { action } from "@storybook/addon-actions";
import type { Meta, StoryObj } from "@storybook/react";
import { Dictation } from "../src";

const meta: Meta = {
component: Dictation,
title: "@media/Dictation",
parameters: {
layout: "centered",
jest: ["media/tests/Dictation.test.tsx"],
},
};

export default meta;
type DictationStory = StoryObj<typeof Dictation>;

export const _Dictation: DictationStory = {
render: (args) => {
return (
<div className="">
<Dictation {...args} />
</div>
);
},
args: {
onRecord: action("onRecord"),
},
};

0 comments on commit e6ff12b

Please sign in to comment.