Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat use ffmpeg static #327

Merged
merged 2 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion enjoy/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@ protocol.registerSchemesAsPrivileged([
app.on("ready", async () => {
protocol.handle("enjoy", (request) => {
let url = request.url.replace("enjoy://", "");
if (url.startsWith("library")) {
if (url.match(/library\/(audios|videos|recordings)/g)) {
url = url.replace("library/", "");
url = path.join(settings.userDataPath(), url);
} else if (url.startsWith("library")) {
url = url.replace("library/", "");
url = path.join(settings.libraryPath(), url);
}

return net.fetch(`file:///${url}`);
Expand Down
102 changes: 85 additions & 17 deletions enjoy/src/main/ffmpeg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,18 @@ import Ffmpeg from "fluent-ffmpeg";
import log from "electron-log/main";
import path from "path";
import fs from "fs-extra";
import settings from "./settings";

Ffmpeg.setFfmpegPath(ffmpegPath);
Ffmpeg.setFfprobePath(ffprobePath);

const logger = log.scope("ffmpeg");
export default class FfmpegWrapper {
public ffmpeg: Ffmpeg.FfmpegCommand;

constructor() {
const ff = Ffmpeg();
logger.debug("Using ffmpeg path:", ffmpegPath);
logger.debug("Using ffprobe path:", ffprobePath);
ff.setFfmpegPath(ffmpegPath);
ff.setFfprobePath(ffprobePath);
this.ffmpeg = ff;
}

checkCommand(): Promise<boolean> {
const ffmpeg = Ffmpeg();
const sampleFile = path.join(__dirname, "samples", "jfk.wav");
return new Promise((resolve, _reject) => {
this.ffmpeg.input(sampleFile).getAvailableFormats((err, _formats) => {
ffmpeg.input(sampleFile).getAvailableFormats((err, _formats) => {
if (err) {
logger.error("Command not valid:", err);
resolve(false);
Expand All @@ -35,8 +29,9 @@ export default class FfmpegWrapper {
}

generateMetadata(input: string): Promise<Ffmpeg.FfprobeData> {
const ffmpeg = Ffmpeg();
return new Promise((resolve, reject) => {
this.ffmpeg
ffmpeg
.input(input)
.on("start", (commandLine) => {
logger.info("Spawned FFmpeg with command: " + commandLine);
Expand All @@ -57,8 +52,9 @@ export default class FfmpegWrapper {
}

generateCover(input: string, output: string): Promise<string> {
const ffmpeg = Ffmpeg();
return new Promise((resolve, reject) => {
this.ffmpeg
ffmpeg
.input(input)
.thumbnail({
count: 1,
Expand Down Expand Up @@ -91,8 +87,9 @@ export default class FfmpegWrapper {
fs.removeSync(output);
}

const ffmpeg = Ffmpeg();
return new Promise((resolve, reject) => {
this.ffmpeg
ffmpeg
.input(input)
.outputOptions("-ar", `${sampleRate}`)
.on("error", (err) => {
Expand All @@ -112,8 +109,9 @@ export default class FfmpegWrapper {
output: string,
options: string[] = []
): Promise<string> {
const ffmpeg = Ffmpeg();
return new Promise((resolve, reject) => {
this.ffmpeg
ffmpeg
.input(input)
.outputOptions(
"-ar",
Expand All @@ -135,7 +133,7 @@ export default class FfmpegWrapper {
}

if (stderr) {
logger.error(stderr);
logger.info(stderr);
}

if (fs.existsSync(output)) {
Expand Down Expand Up @@ -176,9 +174,79 @@ export default class FfmpegWrapper {
return this.convertToWav(input, output);
}

async transcode(
input: string,
output?: string,
options?: string[]
): Promise<string> {
if (input.match(/enjoy:\/\/library\/(audios|videos|recordings)/g)) {
input = path.join(
settings.userDataPath(),
input.replace("enjoy://library/", "")
);
} else if (input.startsWith("enjoy://library/")) {
input = path.join(
settings.libraryPath(),
input.replace("enjoy://library/", "")
);
}

if (!output) {
output = path.join(settings.cachePath(), `${path.basename(input)}.wav`);
}

if (output.startsWith("enjoy://library/")) {
output = path.join(
settings.libraryPath(),
output.replace("enjoy://library/", "")
);
}

options = options || ["-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le"];

const ffmpeg = Ffmpeg();
return new Promise((resolve, reject) => {
ffmpeg
.input(input)
.outputOptions(...options)
.on("start", (commandLine) => {
logger.debug(`Trying to convert ${input} to ${output}`);
logger.info("Spawned FFmpeg with command: " + commandLine);
fs.ensureDirSync(path.dirname(output));
})
.on("end", (stdout, stderr) => {
if (stdout) {
logger.debug(stdout);
}

if (stderr) {
logger.info(stderr);
}

if (fs.existsSync(output)) {
resolve(output);
} else {
reject(new Error("FFmpeg command failed"));
}
})
.on("error", (err: Error) => {
logger.error(err);
reject(err);
})
.save(output);
});
}

registerIpcHandlers() {
ipcMain.handle("ffmpeg-check-command", async (_event) => {
return await this.checkCommand();
});

ipcMain.handle(
"ffmpeg-transcode",
async (_event, input, output, options) => {
return await this.transcode(input, output, options);
}
);
}
}
4 changes: 2 additions & 2 deletions enjoy/src/main/whisper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class Whipser {
}

if (stderr) {
logger.error("stderr", stderr);
logger.info("stderr", stderr);
}

if (stdout) {
Expand Down Expand Up @@ -199,7 +199,7 @@ class Whipser {

command.stderr.on("data", (data) => {
const output = data.toString();
logger.error(`stderr: ${output}`);
logger.info(`stderr: ${output}`);
if (output.startsWith("whisper_print_progress_callback")) {
const progress = parseInt(output.match(/\d+%/)?.[0] || "0");
if (typeof progress === "number") onProgress(progress);
Expand Down
3 changes: 3 additions & 0 deletions enjoy/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,9 @@ contextBridge.exposeInMainWorld("__ENJOY_APP__", {
check: () => {
return ipcRenderer.invoke("ffmpeg-check-command");
},
transcode: (input: string, output: string, options: string[]) => {
return ipcRenderer.invoke("ffmpeg-transcode", input, output, options);
},
},
download: {
onState: (
Expand Down
5 changes: 3 additions & 2 deletions enjoy/src/renderer/components/audios/audio-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
}, [audio]);

useEffect(() => {
if (!initialized) return;
if (!transcription) return;

addDblistener(onTransactionUpdate);
Expand All @@ -192,7 +193,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {
removeDbListener(onTransactionUpdate);
EnjoyApp.whisper.removeProgressListeners();
};
}, [md5, transcription]);
}, [md5, transcription, initialized]);

if (!audio) {
return <LoaderSpin />;
Expand Down Expand Up @@ -324,7 +325,7 @@ export const AudioDetail = (props: { id?: string; md5?: string }) => {

{!transcription ? (
<div className="flex items-center space-x-4">
<PingPoint colorClassName="bg-muted" />
<LoaderIcon className="w-4 h-4 animate-spin" />
<span>{t("loadingTranscription")}</span>
</div>
) : transcription.result ? (
Expand Down
5 changes: 3 additions & 2 deletions enjoy/src/renderer/components/videos/video-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
}, [video]);

useEffect(() => {
if (!initialized) return;
if (!transcription) return;

addDblistener(onTransactionUpdate);
Expand All @@ -198,7 +199,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {
removeDbListener(onTransactionUpdate);
EnjoyApp.whisper.removeProgressListeners();
};
}, [md5, transcription]);
}, [md5, transcription, initialized]);

if (!video) {
return <LoaderSpin />;
Expand Down Expand Up @@ -337,7 +338,7 @@ export const VideoDetail = (props: { id?: string; md5?: string }) => {

{!transcription ? (
<div className="flex items-center space-x-4">
<PingPoint colorClassName="bg-muted" />
<LoaderIcon className="w-4 h-4 animate-spin" />
<span>{t("loadingTranscription")}</span>
</div>
) : transcription.result ? (
Expand Down
21 changes: 16 additions & 5 deletions enjoy/src/renderer/context/app-settings-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ type AppSettingsProviderState = {
login?: (user: UserType) => void;
logout?: () => void;
setLibraryPath?: (path: string) => Promise<void>;
ffmpeg?: FFmpeg;
ffmpegWasm?: FFmpeg;
ffmpegValid?: boolean;
EnjoyApp?: EnjoyAppType;
language?: "en" | "zh-CN";
switchLanguage?: (language: "en" | "zh-CN") => void;
Expand Down Expand Up @@ -44,7 +45,8 @@ export const AppSettingsProvider = ({
const [webApi, setWebApi] = useState<Client>(null);
const [user, setUser] = useState<UserType | null>(null);
const [libraryPath, setLibraryPath] = useState("");
const [ffmpeg, setFfmpeg] = useState<FFmpeg>(null);
const [ffmpegWasm, setFfmpegWasm] = useState<FFmpeg>(null);
const [ffmpegValid, setFfmpegValid] = useState<boolean>(false);
const [language, setLanguage] = useState<"en" | "zh-CN">();
const [proxy, setProxy] = useState<ProxyConfigType>();
const EnjoyApp = window.__ENJOY_APP__;
Expand All @@ -56,7 +58,7 @@ export const AppSettingsProvider = ({
fetchUser();
fetchLibraryPath();
fetchLanguage();
loadFfmpegWASM();
prepareFfmpeg();
fetchProxyConfig();
}, []);

Expand All @@ -76,6 +78,14 @@ export const AppSettingsProvider = ({
);
}, [user, apiUrl, language]);

const prepareFfmpeg = async () => {
const valid = await EnjoyApp.ffmpeg.check();
setFfmpegValid(valid);
if (!valid) {
loadFfmpegWASM();
}
};

const loadFfmpegWASM = async () => {
const baseURL = "assets/libs";
ffmpegRef.current.on("log", ({ message }) => {
Expand All @@ -101,7 +111,7 @@ export const AppSettingsProvider = ({
wasmURL,
workerURL,
});
setFfmpeg(ffmpegRef.current);
setFfmpegWasm(ffmpegRef.current);
} catch (err) {
toast.error(err.message);
}
Expand Down Expand Up @@ -195,7 +205,8 @@ export const AppSettingsProvider = ({
logout,
libraryPath,
setLibraryPath: setLibraryPathHandler,
ffmpeg,
ffmpegValid,
ffmpegWasm,
proxy,
setProxy: setProxyConfigHandler,
initialized,
Expand Down
28 changes: 22 additions & 6 deletions enjoy/src/renderer/hooks/use-transcribe.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,42 @@ import * as sdk from "microsoft-cognitiveservices-speech-sdk";
import axios from "axios";
import take from "lodash/take";
import sortedUniqBy from "lodash/sortedUniqBy";
import { groupTranscription, END_OF_WORD_REGEX, milisecondsToTimestamp } from "@/utils";
import {
groupTranscription,
END_OF_WORD_REGEX,
milisecondsToTimestamp,
} from "@/utils";

export const useTranscribe = () => {
const { EnjoyApp, ffmpeg, user, webApi } = useContext(
const { EnjoyApp, ffmpegWasm, ffmpegValid, user, webApi } = useContext(
AppSettingsProviderContext
);
const { whisperConfig, openai } = useContext(AISettingsProviderContext);

const transcode = async (src: string, options?: string[]) => {
if (!ffmpeg?.loaded) return;
if (ffmpegValid) {
const output = `enjoy://library/cache/${src.split("/").pop()}.wav`;
const res = await EnjoyApp.ffmpeg.transcode(src, output, options);
console.log(res);
const data = await fetchFile(output);
return new Blob([data], { type: "audio/wav" });
} else {
return transcodeUsingWasm(src, options);
}
};

const transcodeUsingWasm = async (src: string, options?: string[]) => {
if (!ffmpegWasm?.loaded) return;

options = options || ["-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le"];

try {
const uri = new URL(src);
const input = uri.pathname.split("/").pop();
const output = input.replace(/\.[^/.]+$/, ".wav");
await ffmpeg.writeFile(input, await fetchFile(src));
await ffmpeg.exec(["-i", input, ...options, output]);
const data = await ffmpeg.readFile(output);
await ffmpegWasm.writeFile(input, await fetchFile(src));
await ffmpegWasm.exec(["-i", input, ...options, output]);
const data = await ffmpegWasm.readFile(output);
return new Blob([data], { type: "audio/wav" });
} catch (e) {
toast.error(t("transcodeError"));
Expand Down
5 changes: 5 additions & 0 deletions enjoy/src/types/enjoy-app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ type EnjoyAppType = {
};
ffmpeg: {
check: () => Promise<boolean>;
transcode: (
input: string,
output: string,
options?: string[]
) => Promise<string>;
};
download: {
onState: (callback: (event, state) => void) => void;
Expand Down
10 changes: 0 additions & 10 deletions enjoy/src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,6 @@ type TransactionStateType = {
record?: AudioType | UserType | RecordingType;
};

type FfmpegConfigType = {
os: string;
arch: string;
commandExists: boolean;
ffmpegPath?: string;
ffprobePath?: string;
scanDirs: string[];
ready: boolean;
};

type LookupType = {
id: string;
word: string;
Expand Down
Loading