From d16f0f0ad06aed2f5f620a47aaa5526712a4a2d3 Mon Sep 17 00:00:00 2001 From: rerender2021 Date: Sat, 1 Apr 2023 07:46:10 +0800 Subject: [PATCH 1/4] fix typo --- src/whisper/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/whisper/index.ts b/src/whisper/index.ts index 14effad..19dc311 100644 --- a/src/whisper/index.ts +++ b/src/whisper/index.ts @@ -56,7 +56,7 @@ export class Whisper { const exePath = path.resolve(dir, "./Whisper-API.exe"); if (fs.existsSync(dir) && fs.existsSync(exePath)) { return new Promise((resolve, reject) => { - console.log("asrDir exists, start asr server", dir); + console.log("whisper dir exists, start whisper server", dir); const name = getModel(); console.log("start whisper server with model: ", name); From 82e55fc22f95f3e8f35465515e2e003c1ebb1c8a Mon Sep 17 00:00:00 2001 From: rerender2021 Date: Sat, 1 Apr 2023 08:15:22 +0800 Subject: [PATCH 2/4] impl batch --- src/app.tsx | 43 ++++++++++++++++++++++++++++------------ src/hooks/useDragDrop.ts | 31 +++++++++++++++++------------ src/layout/index.ts | 2 +- 3 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/app.tsx b/src/app.tsx index 91688cd..daeb595 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,7 +3,7 @@ import { AveRenderer, Grid, Window, getAppContext, IIconResource, IWindowCompone import { App, ThemePredefined_Dark } from "ave-ui"; import { containerLayout, controlLayout } from "./layout"; import { iconResource } from "./resource"; -import { safe } from "./common"; +import { safe, sleep } from "./common"; import axios from "axios"; import { ProgressType, Whisper } from "./whisper"; import { useDragDrop } from "./hooks"; @@ -23,9 +23,10 @@ function initTheme() { themeDark.SetStyle(themeImage, 0); } -enum ButtonText { +enum Text { GenerateSubtitle = "生成字幕", - OpenFile = "选择文件" + OpenFile = "选择文件", + ProgressLabel = "进度", } const modelOptions = getModelList().map((each, index) => { @@ -47,11 +48,18 @@ export function Heard() { const [title, setTitle] = useState("Heard"); const [whisperReady, setwhisperReady] = useState(false); const [src, setSrc] = useState(""); + const [srcDesc, setSrcDesc] = useState(""); + const [srcList, setSrcList] = useState>([""]); const promptRef = useRef(""); - useDragDrop((path) => setSrc(path)); + useDragDrop((pathList) => { + setSrcList(pathList); + setSrcDesc(pathList.length === 1 ? pathList[0] : `${pathList.length} files`); + setProgressLabelText(`${Text.ProgressLabel}: 0/${pathList.length}`); + }); const [progress, setProgress] = useState(ProgressType.None); const [progressText, setProgressText] = useState(""); + const [progressLabelText, setProgressLabelText] = useState(Text.ProgressLabel); const [defaultSelectedKey, setDefaultSelectedKey] = useState("1"); const [hintText, setHintText] = useState("初始化中..."); @@ -65,6 +73,7 @@ export function Heard() { if (filePath) { setSrc(filePath); + setProgressLabelText(Text.ProgressLabel); } }), [] @@ -75,8 +84,13 @@ export function Heard() { if (progress !== ProgressType.None) { return; } - whisper.transcribe(src, promptRef.current).then( - safe((response) => { + + for (let i = 0; i < srcList.length; ++i) { + try { + const src = srcList[i]; + setSrc(src); + setProgressLabelText(`${Text.ProgressLabel}: ${i + 1}/${srcList.length}: ${src}`); + const response = await whisper.transcribe(src, promptRef.current); const data = response.data; const dirName = path.dirname(src); const fileName = path.basename(src); @@ -84,10 +98,13 @@ export function Heard() { const outPath = path.resolve(dirName, `${fileName}.subtitle.json`); fs.writeFileSync(outPath, JSON.stringify(data, null, 4), "utf8"); - }) - ); + await sleep(1500); + } catch (error) { + console.error(error?.message); + } + } }), - [src] + [srcList] ); const startWhisper = safe(() => { @@ -174,10 +191,10 @@ export function Heard() { - + - + {whisperReady ? ( <> @@ -191,10 +208,10 @@ export function Heard() { - + - + diff --git a/src/hooks/useDragDrop.ts b/src/hooks/useDragDrop.ts index 6411361..75fedcc 100644 --- a/src/hooks/useDragDrop.ts +++ b/src/hooks/useDragDrop.ts @@ -1,27 +1,32 @@ import { getAppContext } from "ave-react"; import { useEffect, useState } from "react"; import { DragDropImage, DropBehavior } from "ave-ui"; +import { safe } from "../common"; -export function useDragDrop(onPathChange?: (path: string) => void) { - const [path, setPath] = useState(""); +export function useDragDrop(onPathChange?: (pathList: Array) => void) { + const [pathList, setPathList] = useState([""]); useEffect(() => { const context = getAppContext(); const window = context.getWindow(); - window.OnDragMove((sender, dc) => { - if (1 === dc.FileGetCount()) { - dc.SetDropTip(DragDropImage.Copy, "打开此文件"); + window.OnDragMove( + safe((sender, dc) => { + const fileCount = dc.FileGetCount(); + dc.SetDropTip(DragDropImage.Copy, fileCount === 1 ? "选择此文件" : `选择文件x${fileCount}`); dc.SetDropBehavior(DropBehavior.Copy); - } - }); + }) + ); - window.OnDragDrop((sender, dc) => { - const filePath = dc.FileGet()[0]; - setPath(path); - onPathChange?.(filePath ?? ""); - }); + window.OnDragDrop( + safe((sender, dc) => { + const filePaths = dc.FileGet(); + console.log(`on drap drop, file paths:`, { filePaths }); + setPathList(filePaths); + onPathChange?.(filePaths ?? [""]); + }) + ); }, []); - return { path }; + return { pathList }; } diff --git a/src/layout/index.ts b/src/layout/index.ts index 906af38..4dddbfb 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -27,7 +27,7 @@ export const controlLayout = { selectModel: { row: 5, column: 0, columnSpan: 3 }, generate: { row: 5, column: 3, columnSpan: 5 }, - progressLabel: { row: 7, column: 0, columnSpan: 4 }, + progressLabel: { row: 7, column: 0, columnSpan: 8 }, progress: { row: 9, column: 0, columnSpan: 8 } }, }; From f770cd0f61f05289d4600a28f59729ead86b37a5 Mon Sep 17 00:00:00 2001 From: rerender2021 Date: Sat, 1 Apr 2023 08:22:53 +0800 Subject: [PATCH 3/4] fix open file --- src/app.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app.tsx b/src/app.tsx index daeb595..d91aa1b 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -72,7 +72,8 @@ export function Heard() { console.log(`open file: ${filePath}`); if (filePath) { - setSrc(filePath); + setSrcList([filePath]); + setSrcDesc(filePath); setProgressLabelText(Text.ProgressLabel); } }), From 03cd1d570c790237e0995479dc177efe9b986f64 Mon Sep 17 00:00:00 2001 From: rerender2021 Date: Sat, 22 Apr 2023 22:40:52 +0800 Subject: [PATCH 4/4] impl custom subtitle format --- .gitignore | 3 ++- src/app.tsx | 10 +++----- src/common/index.ts | 7 +++--- src/config/index.ts | 34 +++++++++++++++++++++++++++ src/utils.ts/index.ts | 54 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 src/utils.ts/index.ts diff --git a/.gitignore b/.gitignore index 706d5ce..3c07133 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ subtitle perf log *.exe -error.log \ No newline at end of file +error.log +heard.config.json \ No newline at end of file diff --git a/src/app.tsx b/src/app.tsx index d91aa1b..bbd2972 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -8,8 +8,8 @@ import axios from "axios"; import { ProgressType, Whisper } from "./whisper"; import { useDragDrop } from "./hooks"; import path from "path"; -import fs from "fs"; import { getModelList, useModel } from "./config"; +import { writeSubtitleFile } from "./utils.ts"; function onInit(app: App) { const context = getAppContext(); @@ -93,12 +93,8 @@ export function Heard() { setProgressLabelText(`${Text.ProgressLabel}: ${i + 1}/${srcList.length}: ${src}`); const response = await whisper.transcribe(src, promptRef.current); const data = response.data; - const dirName = path.dirname(src); - const fileName = path.basename(src); - console.log(`save subtitle json`, { dirName, fileName }); - - const outPath = path.resolve(dirName, `${fileName}.subtitle.json`); - fs.writeFileSync(outPath, JSON.stringify(data, null, 4), "utf8"); + + writeSubtitleFile(data, src); await sleep(1500); } catch (error) { console.error(error?.message); diff --git a/src/common/index.ts b/src/common/index.ts index 95d8e3b..c4cf197 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -5,14 +5,15 @@ export function assetsPath(name: string) { return path.resolve(root, `./${name}`); } -export function safe(callback: Function) { - return (...args: any[]) => { +export function safe(callback: T): T { + const f = (...args: any[]) => { try { return callback(...args); } catch (error) { console.error(error); } - }; + } + return f as unknown as T; } export async function sleep(time: number) { diff --git a/src/config/index.ts b/src/config/index.ts index dcea2dd..5ea29c6 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -28,3 +28,37 @@ export function getModelList() { const globalConfig = { model: "", }; + +// --- user config +export function getSubtitleFormat() { + try { + const config = getConfig(); + const format = config?.format ?? defaultConfig.format; + console.log(`current subtitle format: ${format}`); + return format; + } catch (error) { + console.error("get subtitle format failed", { error }); + return defaultConfig.format; + } +} + +const defaultConfig = { + format: "srt", +}; + +function getConfig() { + const configPath = path.resolve(process.cwd(), "./heard.config.json"); + if (!fs.existsSync(configPath)) { + console.log(`config not exist at ${configPath}, create it!`); + fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 4), "utf-8"); + } + + try { + const configJson = JSON.parse(fs.readFileSync(configPath, "utf-8")); + console.log(`parse config succeed, use it`); + return configJson; + } catch (error) { + console.log(`parse config failed, ${error?.message}, use default config`); + return defaultConfig; + } +} diff --git a/src/utils.ts/index.ts b/src/utils.ts/index.ts new file mode 100644 index 0000000..f010143 --- /dev/null +++ b/src/utils.ts/index.ts @@ -0,0 +1,54 @@ +import path from "path"; +import fs from "fs"; +import { getSubtitleFormat } from "../config"; +import { safe } from "../common"; + +interface ISubtitleData { + result: { + text: string; + language: string; + segments: ISubtitleSegment[]; + }; +} + +interface ISubtitleSegment { + id: number; + seek: number; + start: number; + end: number; + text: string; + tokens: number[]; + temperature: number; + avg_logprob: number; + compression_ratio: number; + no_speech_prob: number; +} + +export const writeSubtitleFile = safe((data: ISubtitleData, src: string) => { + const dirName = path.dirname(src); + const fileName = path.basename(src); + const outPath = path.resolve(dirName, `${fileName}.subtitle.json`); + fs.writeFileSync(outPath, JSON.stringify(data, null, 4), "utf8"); + + const parsed = path.parse(fileName); // en.wav + const baseName = parsed.name; // en + const extension = parsed.ext; // .wav + console.log(`save subtitle json`, { dirName, fileName, baseName, extension }); + + const format = getSubtitleFormat(); + if (format === "txt") { + const txtPath = path.resolve(dirName, `${baseName}.txt`); + const txtContent = getTxtContent(data); + fs.writeFileSync(txtPath, txtContent, "utf8"); + console.log(`write txt subtitle succeed`); + + const srtPath = path.resolve(dirName, `${baseName}.srt`); + if (fs.existsSync(srtPath)) { + fs.unlinkSync(srtPath); + } + } +}); + +function getTxtContent(data: ISubtitleData) { + return data.result.segments.map((each) => each.text).join("\n"); +}