feat: 🎸 add transcription export (#814)
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ipcMain, app } from "electron";
|
||||
import { ipcMain, app, BrowserWindow } from "electron";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import mainWin from "@main/window";
|
||||
@@ -77,6 +77,44 @@ class Downloader {
|
||||
});
|
||||
}
|
||||
|
||||
prinfAsPDF(content: string, savePath: string) {
|
||||
let pdfWin: BrowserWindow | null = null;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
pdfWin = new BrowserWindow({
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
webSecurity: false,
|
||||
},
|
||||
show: false,
|
||||
width: 800,
|
||||
height: 600,
|
||||
fullscreenable: false,
|
||||
minimizable: false,
|
||||
});
|
||||
|
||||
pdfWin.loadURL(`data:text/html;charset=utf-8,${encodeURI(content)}`);
|
||||
|
||||
pdfWin.webContents.on("did-finish-load", () => {
|
||||
pdfWin.webContents
|
||||
.printToPDF({ printBackground: true })
|
||||
.then((data) => {
|
||||
fs.writeFile(savePath, data, (error) => {
|
||||
if (error) throw error;
|
||||
|
||||
resolve(savePath);
|
||||
|
||||
pdfWin.close();
|
||||
pdfWin = null;
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cancel(filename: string) {
|
||||
logger.debug("dashboard", this.dashboard());
|
||||
this.tasks
|
||||
@@ -125,6 +163,9 @@ class Downloader {
|
||||
ipcMain.handle("download-dashboard", () => {
|
||||
return this.dashboard();
|
||||
});
|
||||
ipcMain.handle("print-as-pdf", (_event, content, savePath) => {
|
||||
return this.prinfAsPDF(content, savePath);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -497,6 +497,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
|
||||
start: (url: string, savePath?: string) => {
|
||||
return ipcRenderer.invoke("download-start", url, savePath);
|
||||
},
|
||||
printAsPdf: (content: string, savePath: string) => {
|
||||
return ipcRenderer.invoke("print-as-pdf", content, savePath);
|
||||
},
|
||||
cancel: (filename: string) => {
|
||||
ipcRenderer.invoke("download-cancel", filename);
|
||||
},
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from "./media-provider";
|
||||
export * from "./media-tabs";
|
||||
export * from "./media-loading-modal";
|
||||
export * from "./add-media-button";
|
||||
export * from "./media-transcription-download";
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useContext } from "react";
|
||||
import { Button, toast } from "@renderer/components/ui";
|
||||
import { t } from "i18next";
|
||||
import {
|
||||
MediaPlayerProviderContext,
|
||||
AppSettingsProviderContext,
|
||||
} from "@/renderer/context";
|
||||
import { AlignmentResult } from "echogarden/dist/api/API.d.js";
|
||||
import { convertWordIpaToNormal } from "@/utils";
|
||||
import template from "./transcription.template.html?raw";
|
||||
|
||||
export const MediaTranscriptionDownload = () => {
|
||||
const { media, transcription } = useContext(MediaPlayerProviderContext);
|
||||
const { EnjoyApp, learningLanguage, ipaMappings } = useContext(
|
||||
AppSettingsProviderContext
|
||||
);
|
||||
|
||||
function generateContent() {
|
||||
const language = transcription.language || learningLanguage;
|
||||
const sentences = transcription.result as AlignmentResult;
|
||||
|
||||
const contents = sentences.timeline.map((sentence) => {
|
||||
let words = sentence.text.split(" ");
|
||||
|
||||
const ipas = sentence.timeline.map((w) =>
|
||||
w.timeline.map((t) =>
|
||||
language.startsWith("en")
|
||||
? convertWordIpaToNormal(
|
||||
t.timeline.map((s) => s.text),
|
||||
{ mappings: ipaMappings }
|
||||
).join("")
|
||||
: t.text
|
||||
)
|
||||
);
|
||||
|
||||
if (words.length !== sentence.timeline.length) {
|
||||
words = sentence.timeline.map((w) => w.text);
|
||||
}
|
||||
|
||||
return `
|
||||
<div class='sentence'>
|
||||
${words
|
||||
.map(
|
||||
(word, index) => `
|
||||
<div class='word-wrap'>
|
||||
<div class='text'>${word}</div>
|
||||
<div class='ipa'>${ipas[index]}</div>
|
||||
</div>`
|
||||
)
|
||||
.join("")}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
return template
|
||||
.replace("$title", media.name)
|
||||
.replace("$content", contents.join(""));
|
||||
}
|
||||
|
||||
async function download() {
|
||||
try {
|
||||
const savePath = await EnjoyApp.dialog.showSaveDialog({
|
||||
title: t("download"),
|
||||
defaultPath: `${media.name}.pdf`,
|
||||
});
|
||||
|
||||
if (!savePath) return;
|
||||
|
||||
await EnjoyApp.download.printAsPdf(generateContent(), savePath);
|
||||
|
||||
toast.success(t("downloadedSuccessfully"));
|
||||
} catch (err) {
|
||||
toast.error(`${t("downloadFailed")}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button variant="ghost" className="block w-full" onClick={download}>
|
||||
{t("download")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import { formatDuration } from "@renderer/lib/utils";
|
||||
import {
|
||||
MediaTranscriptionReadButton,
|
||||
MediaTranscriptionGenerateButton,
|
||||
MediaTranscriptionDownload,
|
||||
TranscriptionEditButton,
|
||||
} from "@renderer/components";
|
||||
|
||||
@@ -163,6 +164,9 @@ export const MediaTranscription = (props: { display?: boolean }) => {
|
||||
</Button>
|
||||
</TranscriptionEditButton>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<MediaTranscriptionDownload />
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width initial-scale=1.0" />
|
||||
<title>Transcription</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: ui-serif, Georgia, Cambria, "Times New Roman", Times, serif;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.sentence {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.sentence .word-wrap {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sentence .word-wrap .text {
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.sentence .word-wrap .ipa {
|
||||
font-size: 13px;
|
||||
padding: 0 4px;
|
||||
color: rgb(160, 160, 160);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">$title</div>
|
||||
<div class="contents">$content</div>
|
||||
</body>
|
||||
</html>
|
||||
1
enjoy/src/types/enjoy-app.d.ts
vendored
1
enjoy/src/types/enjoy-app.d.ts
vendored
@@ -291,6 +291,7 @@ type EnjoyAppType = {
|
||||
cancelAll: () => void;
|
||||
dashboard: () => Promise<DownloadStateType[]>;
|
||||
removeAllListeners: () => void;
|
||||
printAsPdf: (content: string, savePath?: string) => Promise<void>;
|
||||
};
|
||||
cacheObjects: {
|
||||
get: (key: string) => Promise<any>;
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"@main/*": ["./src/main/*"],
|
||||
"@commands": ["./src/commands"]
|
||||
},
|
||||
"types": ["vite/client"],
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false
|
||||
|
||||
Reference in New Issue
Block a user