feat: 🎸 add transcription export (#814)

This commit is contained in:
divisey
2024-07-16 18:28:47 +08:00
committed by GitHub
parent ea48a28ccb
commit 1899f26a83
8 changed files with 179 additions and 1 deletions

View File

@@ -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);
});
}
}

View File

@@ -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);
},

View File

@@ -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";

View File

@@ -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>
);
};

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>;

View File

@@ -18,6 +18,7 @@
"@main/*": ["./src/main/*"],
"@commands": ["./src/commands"]
},
"types": ["vite/client"],
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"useDefineForClassFields": false