From b545ea236268daacd038deadfedc9bd05eef99b0 Mon Sep 17 00:00:00 2001 From: an-lee Date: Mon, 15 Jan 2024 16:57:44 +0800 Subject: [PATCH] Feat: save waveform as file (#118) * package rpm * cache waveform data as file in library * clear waveform data in db * fix some css --- enjoy/components.json | 2 +- enjoy/src/main/db/index.ts | 20 ++++++++-- enjoy/src/main/waveform.ts | 38 +++++++++++++++++++ enjoy/src/main/window.ts | 5 +++ enjoy/src/preload.ts | 8 ++++ enjoy/src/renderer/app.tsx | 2 +- .../components/audios/audio-detail.tsx | 4 +- .../conversations/conversations-shortcut.tsx | 2 +- .../conversations/speech-player.tsx | 2 +- .../components/medias/media-player.tsx | 23 ++++------- .../components/messages/assistant-message.tsx | 6 +-- .../components/messages/user-message.tsx | 17 ++++++++- .../renderer/components/posts/post-audio.tsx | 2 +- .../renderer/components/posts/post-card.tsx | 2 +- .../pronunciation-assessment-score-result.tsx | 2 +- .../components/recordings/recording-card.tsx | 2 +- .../components/stories/story-viewer.tsx | 8 ++-- .../components/ui/floating-toolbar.tsx | 4 +- enjoy/src/renderer/components/ui/sonner.tsx | 2 +- .../components/videos/video-detail.tsx | 8 ++-- enjoy/src/renderer/pages/conversation.tsx | 4 +- enjoy/src/renderer/pages/conversations.tsx | 2 +- enjoy/src/renderer/pages/vocabulary.tsx | 6 ++- enjoy/src/types/enjoy-app.d.ts | 4 ++ enjoy/src/types/waveform.d.ts | 6 +++ 25 files changed, 133 insertions(+), 48 deletions(-) create mode 100644 enjoy/src/main/waveform.ts create mode 100644 enjoy/src/types/waveform.d.ts diff --git a/enjoy/components.json b/enjoy/components.json index 84987e9b..8b573d4a 100644 --- a/enjoy/components.json +++ b/enjoy/components.json @@ -7,7 +7,7 @@ "config": "tailwind.config.js", "css": "src/index.css", "baseColor": "zinc", - "cssVariables": true + "cssVariables": false }, "aliases": { "components": "src/renderer/components", diff --git a/enjoy/src/main/db/index.ts b/enjoy/src/main/db/index.ts index a42883a4..e183c70f 100644 --- a/enjoy/src/main/db/index.ts +++ b/enjoy/src/main/db/index.ts @@ -51,9 +51,6 @@ db.connect = async () => { db.connection = sequelize; - // vacuum the database - await sequelize.query("VACUUM"); - const umzug = new Umzug({ migrations: { glob: __dirname + "/migrations/*.js" }, context: sequelize.getQueryInterface(), @@ -68,6 +65,23 @@ db.connect = async () => { await sequelize.sync(); await sequelize.authenticate(); + // TODO: + // clear the large waveform data in DB. + // Remove this in next release + const caches = await CacheObject.findAll({ + attributes: ["id", "key"], + }); + const cacheIds: string[] = []; + caches.forEach((cache) => { + if (cache.key.startsWith("waveform")) { + cacheIds.push(cache.id); + } + }); + await CacheObject.destroy({ where: { id: cacheIds } }); + + // vacuum the database + await sequelize.query("VACUUM"); + // register handlers audiosHandler.register(); cacheObjectsHandler.register(); diff --git a/enjoy/src/main/waveform.ts b/enjoy/src/main/waveform.ts new file mode 100644 index 00000000..e4b6a6a9 --- /dev/null +++ b/enjoy/src/main/waveform.ts @@ -0,0 +1,38 @@ +import { ipcMain } from "electron"; +import settings from "@main/settings"; +import path from "path"; +import fs from "fs-extra"; + +export class Waveform { + public dir = path.join(settings.libraryPath(), "waveforms"); + + constructor() { + fs.ensureDirSync(this.dir); + } + + find(id: string) { + const file = path.join(this.dir, id + ".waveform.json"); + + if (fs.existsSync(file)) { + return fs.readJsonSync(file); + } else { + return null; + } + } + + save(id: string, data: WaveFormDataType) { + const file = path.join(this.dir, id + ".waveform.json"); + + fs.writeJsonSync(file, data); + } + + registerIpcHandlers() { + ipcMain.handle("waveforms-find", async (_event, id) => { + return this.find(id); + }); + + ipcMain.handle("waveforms-save", (_event, id, data) => { + return this.save(id, data); + }); + } +} diff --git a/enjoy/src/main/window.ts b/enjoy/src/main/window.ts index 5c3b69c2..804e8170 100644 --- a/enjoy/src/main/window.ts +++ b/enjoy/src/main/window.ts @@ -18,6 +18,7 @@ import log from "electron-log/main"; import { WEB_API_URL } from "@/constants"; import { AudibleProvider, TedProvider } from "@main/providers"; import { FfmpegDownloader } from "@main/ffmpeg"; +import { Waveform } from "./waveform"; log.initialize({ preload: true }); const logger = log.scope("window"); @@ -25,6 +26,7 @@ const logger = log.scope("window"); const audibleProvider = new AudibleProvider(); const tedProvider = new TedProvider(); const ffmpegDownloader = new FfmpegDownloader(); +const waveform = new Waveform(); const main = { win: null as BrowserWindow | null, @@ -46,6 +48,9 @@ main.init = () => { // Whisper whisper.registerIpcHandlers(); + // Waveform + waveform.registerIpcHandlers(); + // Downloader downloader.registerIpcHandlers(); diff --git a/enjoy/src/preload.ts b/enjoy/src/preload.ts index 4fbfabc2..9d53e6f2 100644 --- a/enjoy/src/preload.ts +++ b/enjoy/src/preload.ts @@ -399,4 +399,12 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", { return ipcRenderer.invoke("transcriptions-update", id, params); }, }, + waveforms: { + find: (id: string) => { + return ipcRenderer.invoke("waveforms-find", id); + }, + save: (id: string, data: WaveFormDataType) => { + return ipcRenderer.invoke("waveforms-save", id, data); + }, + } }); diff --git a/enjoy/src/renderer/app.tsx b/enjoy/src/renderer/app.tsx index 5c919c40..baba26ba 100644 --- a/enjoy/src/renderer/app.tsx +++ b/enjoy/src/renderer/app.tsx @@ -53,7 +53,7 @@ function App() { - + diff --git a/enjoy/src/renderer/components/audios/audio-detail.tsx b/enjoy/src/renderer/components/audios/audio-detail.tsx index 96da1e2c..bb03a39c 100644 --- a/enjoy/src/renderer/components/audios/audio-detail.tsx +++ b/enjoy/src/renderer/components/audios/audio-detail.tsx @@ -132,7 +132,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => { mediaId={audio.id} mediaType="Audio" mediaUrl={audio.src} - waveformCacheKey={`waveform-audio-${audio.md5}`} + mediaMd5={audio.md5} transcription={transcription} currentTime={currentTime} setCurrentTime={setCurrentTime} @@ -207,7 +207,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => { {!initialized && ( -
+
)} diff --git a/enjoy/src/renderer/components/conversations/conversations-shortcut.tsx b/enjoy/src/renderer/components/conversations/conversations-shortcut.tsx index 081871b6..2d570928 100644 --- a/enjoy/src/renderer/components/conversations/conversations-shortcut.tsx +++ b/enjoy/src/renderer/components/conversations/conversations-shortcut.tsx @@ -48,7 +48,7 @@ export const ConversationsShortcut = (props: {
ask(conversation)} - className="bg-white text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-primary hover:text-white cursor-pointer flex items-center border" + className="bg-background text-primary rounded-full w-full mb-2 py-2 px-4 hover:bg-primary hover:text-white cursor-pointer flex items-center border" style={{ borderLeftColor: `#${conversation.id .replaceAll("-", "") diff --git a/enjoy/src/renderer/components/conversations/speech-player.tsx b/enjoy/src/renderer/components/conversations/speech-player.tsx index af646183..76e0881a 100644 --- a/enjoy/src/renderer/components/conversations/speech-player.tsx +++ b/enjoy/src/renderer/components/conversations/speech-player.tsx @@ -89,7 +89,7 @@ export const SpeechPlayer = (props: {
{!initialized && (
diff --git a/enjoy/src/renderer/components/medias/media-player.tsx b/enjoy/src/renderer/components/medias/media-player.tsx index 788917c2..28ef95cc 100644 --- a/enjoy/src/renderer/components/medias/media-player.tsx +++ b/enjoy/src/renderer/components/medias/media-player.tsx @@ -34,7 +34,7 @@ export const MediaPlayer = (props: { mediaId: string; mediaType: "Audio" | "Video"; mediaUrl: string; - waveformCacheKey: string; + mediaMd5?: string; transcription: TranscriptionType; // player controls currentTime: number; @@ -67,7 +67,7 @@ export const MediaPlayer = (props: { mediaId, mediaType, mediaUrl, - waveformCacheKey, + mediaMd5, transcription, height = 200, currentTime, @@ -94,12 +94,7 @@ export const MediaPlayer = (props: { if (!mediaUrl) return; const [wavesurfer, setWavesurfer] = useState(null); - const [waveform, setWaveForm] = useState<{ - peaks: number[]; - duration: number; - frequencies: number[]; - sampleRate: number; - }>(null); + const [waveform, setWaveForm] = useState(null); const containerRef = useRef(); const [mediaProvider, setMediaProvider] = useState< HTMLAudioElement | HTMLVideoElement @@ -181,7 +176,7 @@ export const MediaPlayer = (props: { const renderPitchContour = (region: RegionType) => { if (!region) return; - if (!waveform.frequencies.length) return; + if (!waveform?.frequencies?.length) return; if (!wavesurfer) return; const duration = wavesurfer.getDuration(); @@ -280,7 +275,6 @@ export const MediaPlayer = (props: { const ws = WaveSurfer.create({ container: containerRef.current, height, - url: mediaUrl, waveColor: "#ddd", progressColor: "rgba(0, 0, 0, 0.25)", cursorColor: "#dc143c", @@ -324,6 +318,7 @@ export const MediaPlayer = (props: { const subscriptions = [ wavesurfer.on("play", () => setIsPlaying(true)), wavesurfer.on("pause", () => setIsPlaying(false)), + wavesurfer.on("loading", (percent: number) => console.log(percent)), wavesurfer.on("timeupdate", (time: number) => setCurrentTime(time)), wavesurfer.on("decode", () => { if (waveform?.frequencies) return; @@ -340,7 +335,7 @@ export const MediaPlayer = (props: { sampleRate, frequencies: _frequencies, }; - EnjoyApp.cacheObjects.set(waveformCacheKey, _waveform); + EnjoyApp.waveforms.save(mediaMd5, _waveform); setWaveForm(_waveform); }), wavesurfer.on("ready", () => { @@ -479,10 +474,8 @@ export const MediaPlayer = (props: { }, [wavesurfer, isPlaying]); useEffect(() => { - EnjoyApp.cacheObjects.get(waveformCacheKey).then((cached) => { - if (!cached) return; - - setWaveForm(cached); + EnjoyApp.waveforms.find(mediaMd5).then((waveform) => { + setWaveForm(waveform); }); }, []); diff --git a/enjoy/src/renderer/components/messages/assistant-message.tsx b/enjoy/src/renderer/components/messages/assistant-message.tsx index da61bbf9..712b2051 100644 --- a/enjoy/src/renderer/components/messages/assistant-message.tsx +++ b/enjoy/src/renderer/components/messages/assistant-message.tsx @@ -90,11 +90,11 @@ export const AssistantMessageComponent = (props: { id={`message-${message.id}`} className="flex items-end space-x-2 pr-10" > - + - AI + AI -
+
{configuration?.autoSpeech && speeching ? (
diff --git a/enjoy/src/renderer/components/messages/user-message.tsx b/enjoy/src/renderer/components/messages/user-message.tsx index c2cbf637..7b3b3a45 100644 --- a/enjoy/src/renderer/components/messages/user-message.tsx +++ b/enjoy/src/renderer/components/messages/user-message.tsx @@ -32,6 +32,7 @@ import { } from "lucide-react"; import { useCopyToClipboard } from "@uidotdev/usehooks"; import { t } from "i18next"; +import { useNavigate } from "react-router-dom"; import Markdown from "react-markdown"; export const UserMessageComponent = (props: { @@ -45,6 +46,7 @@ export const UserMessageComponent = (props: { const { user, webApi } = useContext(AppSettingsProviderContext); const [_, copyToClipboard] = useCopyToClipboard(); const [copied, setCopied] = useState(false); + const navigate = useNavigate(); const handleShare = async () => { if (message.role === "user") { @@ -57,7 +59,18 @@ export const UserMessageComponent = (props: { }, }) .then(() => { - toast(t("sharedSuccessfully"), { description: t("sharedPrompt") }); + toast.success(t("sharedSuccessfully"), { + description: t("sharedPrompt"), + action: { + label: t("view"), + onClick: () => { + navigate("/community"); + }, + }, + actionButtonStyle: { + backgroundColor: "var(--primary)", + }, + }); }) .catch((err) => { toast.error(t("shareFailed"), { description: err.message }); @@ -155,7 +168,7 @@ export const UserMessageComponent = (props: { - + {user.name[0]} diff --git a/enjoy/src/renderer/components/posts/post-audio.tsx b/enjoy/src/renderer/components/posts/post-audio.tsx index f2bee1e3..3fb165e0 100644 --- a/enjoy/src/renderer/components/posts/post-audio.tsx +++ b/enjoy/src/renderer/components/posts/post-audio.tsx @@ -165,7 +165,7 @@ const WavesurferPlayer = (props: {
{!initialized && (
diff --git a/enjoy/src/renderer/components/posts/post-card.tsx b/enjoy/src/renderer/components/posts/post-card.tsx index 50da0fb2..3e44427b 100644 --- a/enjoy/src/renderer/components/posts/post-card.tsx +++ b/enjoy/src/renderer/components/posts/post-card.tsx @@ -20,7 +20,7 @@ export const PostCard = (props: { const { user } = useContext(AppSettingsProviderContext); return ( -
+
diff --git a/enjoy/src/renderer/components/pronunciation-assessments/pronunciation-assessment-score-result.tsx b/enjoy/src/renderer/components/pronunciation-assessments/pronunciation-assessment-score-result.tsx index b7a45538..23d2e3f6 100644 --- a/enjoy/src/renderer/components/pronunciation-assessments/pronunciation-assessment-score-result.tsx +++ b/enjoy/src/renderer/components/pronunciation-assessments/pronunciation-assessment-score-result.tsx @@ -96,7 +96,7 @@ export const PronunciationAssessmentScoreResult = (props: {
{!pronunciationScore && ( -
+
-
+

{story.title.split(" ").map((word, i) => ( diff --git a/enjoy/src/renderer/components/ui/floating-toolbar.tsx b/enjoy/src/renderer/components/ui/floating-toolbar.tsx index 7b1e8500..3daaf5ec 100644 --- a/enjoy/src/renderer/components/ui/floating-toolbar.tsx +++ b/enjoy/src/renderer/components/ui/floating-toolbar.tsx @@ -41,8 +41,8 @@ export const ToolbarButton = (props: { className={cn( `rounded-full p-3 h-12 w-12 ${ toggled - ? "bg-primary text-white" - : "bg-white text-muted-foreground hover:text-white " + ? "bg-primary dark:bg-background text-background dark:text-foreground" + : "bg-background dark:bg-muted text-muted-foreground hover:text-background " }`, className )} diff --git a/enjoy/src/renderer/components/ui/sonner.tsx b/enjoy/src/renderer/components/ui/sonner.tsx index 2958b9ae..9c6c0e0c 100644 --- a/enjoy/src/renderer/components/ui/sonner.tsx +++ b/enjoy/src/renderer/components/ui/sonner.tsx @@ -6,7 +6,7 @@ import { Toaster as Sonner, toast } from "sonner"; type ToasterProps = React.ComponentProps; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); + const { theme = "light" } = useTheme(); return ( { targetId: video.id, }) .then(() => { - toast.success(t("sharedSuccessfully"), { description: t("sharedVideo") }); + toast.success(t("sharedSuccessfully"), { + description: t("sharedVideo"), + }); }) .catch((err) => { toast.error(t("shareFailed"), { description: err.message }); @@ -136,7 +138,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => { mediaId={video.id} mediaType="Video" mediaUrl={video.src} - waveformCacheKey={`waveform-video-${video.md5}`} + mediaMd5={video.md5} transcription={transcription} currentTime={currentTime} setCurrentTime={setCurrentTime} @@ -216,7 +218,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => { {!initialized && ( -
+
)} diff --git a/enjoy/src/renderer/pages/conversation.tsx b/enjoy/src/renderer/pages/conversation.tsx index e427224d..0f8c8f25 100644 --- a/enjoy/src/renderer/pages/conversation.tsx +++ b/enjoy/src/renderer/pages/conversation.tsx @@ -271,7 +271,7 @@ export default () => {
-
+