I like this program, when i try to use a SRT, i meet this issu. it cannot skip whisper.
workbuddy resault:
我来分析一下这个问题。让我先克隆代码仓库并探索一下其结构。
让我读取 README 并搜索 transcript 导入相关的代码。
现在让我查看一下用于字幕导入功能的关键文件。
现在让我检查一下 store、transcriber hook 和 upload screen,以追踪导入流程。
我来查看一下 store 以及转录流程的启动位置。
现在是关键部分——上传屏幕如何处理 pendingTranscript 以及是否将其传递给 loadVideo。让我来读取一下上传屏幕和编辑器的代码。
UploadScreen 仅在 !isElectron 时渲染 ModelSelector(及其 ImportTranscriptOption)。我来检查一下 Editor 以了解 transcription 是如何触发的,以及 skipTranscription 是如何交互的,并查看 ModelSelector。
我检查一下 ModelSelector 以了解菜单机制,并检查 git log 和 issues,看看是否有相关的修复或 bug 报告。
ModelSelector/Import 机制错综复杂,但看起来是一致的。我来检查一下 git 历史记录和 GitHub issues,看看有没有关于此功能的具体 bug 报告,并查看 source.ts 和 models.ts。
我来检查一下 GitHub 仓库中关于此功能的相关 issue,并查看解析测试(parse test)和国际化(i18n)消息。
目前没有可用的 GitHub 认证。让我通过 WebFetch 获取 issues,并检查解析测试 (parse test) 和 i18n 消息。
我来运行解析测试以确认其是否有效,并查看导入流程的 i18n 消息。
还有一个“替换字幕”的入口点。让我追踪一下 importWords 和 transcript.replace,并仔细研究一下 Popover 组件。
让我查看一下 TranscriptPanel 的替换流程,以及 Popover 组件。
替换流程使用了原生的 <label>,这是健壮的。上传界面的流程使用了 ctx.closeMenu(),随后是 input?.click() —— 这是最脆弱的部分。让我检查一下 Popover,看看关闭操作是否会卸载该输入框。
我来查看一下修改 import/parse 代码的提交记录,并查看 ROADMAP/PLAN 以获取上下文。
导入功能的历史记录显示了多次修复(取消 stuck 状态、菜单重新打开、web picker 无法打开)。让我查看一下最近关于 web picker 的修复,以了解是否还有遗留问题,并验证在显示选择器(picker)时输入框是否没有被隐藏。
导入代码有一个针对未打开的选择器(picker)的修复记录。让我验证一下 main 分支上是否还有未解决的 issue,并仔细检查最脆弱的交互部分——即 onChange 中的 e.target.value = "" 时机和 files 捕获。
没有关于导入失败的未解决问题——功能追踪显示其工作正常。但我注意到 onChange 处理程序中有一个可疑的模式:它捕获了 const files = e.target.files,然后清除了 e.target.value = "",最后读取了 files?.[0]。由于 HTMLInputElement.files 是一个实时的 [SameObject] FileList,清空该值可能会在读取之前将其置空。让我通过实验验证一下浏览器的行为——这是关键点。
我有一个 Playwright Chromium。让我设置一个针对性的测试,以检查在清空 value 后,捕获到的 files 引用是否仍然有效。
确认。 这是问题的根源。在设置 e.target.value = "" 后,捕获的 files 引用(同一个 live [SameObject] FileList)长度变为 0,因此 files?.[0] 为 undefined。让我通过测试“正确”模式(先读取,后清除)和 Array.from 快照来验证修复方案是否有效,并确认面板内的 transcript-import 输入端是否也存在同样的缺陷。
模式 A(先读取,后清除)可以正常工作;而有问题的模式(先清除,后读取引用)会返回 undefined。让我确认一下 Array.from 快照是否能作为一种修复方案,并检查 git blame,看看这个清除顺序是在什么时候引入的。
Array.from 快照修复方案有效。现在让我确认一下引入该 clear-before-read 顺序的具体 commit,并验证这两个存在缺陷的调用点是否都存在相同的 bug。
我已经找到了根本原因,并利用沙盒中的 Chromium 对其进行了验证。以下是分析结果。
结论
该功能之所以“不生效”,是因为导入字幕的 <input onChange> 处理程序在读取所选文件之前就清空了输入的 value。 由于 HTMLInputElement.files 是一个实时的 [SameObject] FileList,调用 e.target.value = "" 会立即清空该列表,导致之前捕获的引用指向了空列表,因此 files?.[0] 的结果为 undefined。随后处理程序将此视为“未选择文件”并直接退出,从而静默丢弃了 SRT/VTT/JSON 文件——导致 pendingTranscript 未被设置,source 从未携带字词变为 "import",最终 Whisper 依然在运行。
验证(沙盒中的 Chromium)
我使用 Playwright + 系统自带的 Chromium 复现了这三种模式:
| 模式 |
代码 |
结果 |
| A. 先读取,后清空 |
const f = e.target.files?.[0]; e.target.value=""; |
✅ f = a.srt |
B. 使用 Array.from 快照,后清空 |
const a = Array.from(e.target.files); e.target.value=""; |
✅ a[0] = fix.srt |
| C. 先捕获引用,后清空,再读取(本项目使用的模式) |
const files = e.target.files; e.target.value=""; files?.[0] |
❌ undefined |
模式 C 报告 BEFORE_CLEAR_LENGTH=1,但随后的 SYNC_FILE=UNDEFINED / AFTER_CLEAR_LENGTH=0 / ASYNC_FILE=UNDEFINED —— 这证实了捕获的 files 引用被 value="" 清空操作置空了,即便是在同一个同步任务(sync tick)中也是如此。
问题代码
两处导入字幕的入口点都使用了这种有问题的模式:
1. components/ImportTranscriptOption.tsx(上传界面“跳过 Whisper”路径,第 121–131 行):
onChange={(e) => {
const files = e.target.files; // live FileList ref
e.target.value = ""; // ← empties that same FileList
void (async () => {
const file = files?.[0]; // ← undefined!
...
if (!file) { finishCancel(); return; } // treated as "user cancelled"
2. components/TranscriptPanel.tsx(编辑器内的“替换字幕”路径,第 378–382 行 → handleImportTranscript 第 224 行):
onChange={(e) => {
const files = e.target.files;
e.target.value = "";
void handleImportTranscript(files); // → const file = files?.[0]; → undefined → return
}}
为什么媒体上传功能正常
Editor 的媒体文件输入(components/Editor.tsx 第 419–423 行)在清空之前就读取了文件,因此它避开了这个 Bug:
onChange={(e) => {
handleMenuFile(e.target.files?.[0]); // read FIRST
e.target.value = ""; // clear AFTER
}}
为什么在 issue 追踪器中没有体现
这个清空顺序是在 2113b47(2026-07-27,“Make ModelSelector a generic option-component shell”)中引入的。解析逻辑(lib/parseTranscript.ts)和存储逻辑(lib/store.ts 中的 loadVideo → skipTranscription)是完全正确的——通过解析测试验证了流程;错误仅在于文件输入处理程序丢弃了文件。这就是为什么该功能看起来虽然“配置正确”却无法生效的原因。
修复方案
读取文件(或对 FileList 进行快照),然后再清空 value。最小化的修改方案如下:
ImportTranscriptOption.tsx:
onChange={(e) => {
const file = e.target.files?.[0]; // read first
e.target.value = ""; // then allow re-pick
void (async () => {
const menu = menuRef.current;
const gen = ++pickGenRef.current;
if (!file) { finishCancel(); return; }
...
const parsed = await parseTranscriptFile(file);
TranscriptPanel.tsx(使用快照,以便异步处理程序仍能接收到有效的列表):
onChange={(e) => {
const files = Array.from(e.target.files ?? []); // snapshot
e.target.value = "";
void handleImportTranscript(files as unknown as FileList);
}}
(或者将 handleImportTranscript 的签名修改为 File[] | File | null。)
Array.from(e.target.files) 这种快照形式是我已经在 Chromium 中验证过的,它能在不改变异步流程的情况下正确返回所选文件。
所以:README 中关于导入的描述并没有说谎——功能本身是存在的——但 value="" 这一行代码在所选文件被读取之前就将其抹除了,这就是为什么选中的字幕从未被使用,导致 Whisper 无法被跳过的原因。
I like this program, when i try to use a SRT, i meet this issu. it cannot skip whisper.
workbuddy resault:
我来分析一下这个问题。让我先克隆代码仓库并探索一下其结构。
让我读取 README 并搜索 transcript 导入相关的代码。
现在让我查看一下用于字幕导入功能的关键文件。
现在让我检查一下 store、transcriber hook 和 upload screen,以追踪导入流程。
我来查看一下 store 以及转录流程的启动位置。
现在是关键部分——上传屏幕如何处理
pendingTranscript以及是否将其传递给loadVideo。让我来读取一下上传屏幕和编辑器的代码。UploadScreen仅在!isElectron时渲染ModelSelector(及其ImportTranscriptOption)。我来检查一下Editor以了解transcription是如何触发的,以及skipTranscription是如何交互的,并查看ModelSelector。我检查一下
ModelSelector以了解菜单机制,并检查 git log 和 issues,看看是否有相关的修复或 bug 报告。ModelSelector/Import 机制错综复杂,但看起来是一致的。我来检查一下 git 历史记录和 GitHub issues,看看有没有关于此功能的具体 bug 报告,并查看
source.ts和models.ts。我来检查一下 GitHub 仓库中关于此功能的相关 issue,并查看解析测试(parse test)和国际化(i18n)消息。
目前没有可用的 GitHub 认证。让我通过 WebFetch 获取 issues,并检查解析测试 (parse test) 和 i18n 消息。
我来运行解析测试以确认其是否有效,并查看导入流程的 i18n 消息。
还有一个“替换字幕”的入口点。让我追踪一下
importWords和transcript.replace,并仔细研究一下 Popover 组件。让我查看一下 TranscriptPanel 的替换流程,以及 Popover 组件。
替换流程使用了原生的
<label>,这是健壮的。上传界面的流程使用了ctx.closeMenu(),随后是input?.click()—— 这是最脆弱的部分。让我检查一下 Popover,看看关闭操作是否会卸载该输入框。我来查看一下修改 import/parse 代码的提交记录,并查看 ROADMAP/PLAN 以获取上下文。
导入功能的历史记录显示了多次修复(取消 stuck 状态、菜单重新打开、web picker 无法打开)。让我查看一下最近关于 web picker 的修复,以了解是否还有遗留问题,并验证在显示选择器(picker)时输入框是否没有被隐藏。
导入代码有一个针对未打开的选择器(picker)的修复记录。让我验证一下
main分支上是否还有未解决的 issue,并仔细检查最脆弱的交互部分——即onChange中的e.target.value = ""时机和files捕获。没有关于导入失败的未解决问题——功能追踪显示其工作正常。但我注意到
onChange处理程序中有一个可疑的模式:它捕获了const files = e.target.files,然后清除了e.target.value = "",最后读取了files?.[0]。由于HTMLInputElement.files是一个实时的[SameObject]FileList,清空该值可能会在读取之前将其置空。让我通过实验验证一下浏览器的行为——这是关键点。我有一个 Playwright Chromium。让我设置一个针对性的测试,以检查在清空
value后,捕获到的files引用是否仍然有效。确认。 这是问题的根源。在设置
e.target.value = ""后,捕获的files引用(同一个 live[SameObject]FileList)长度变为 0,因此files?.[0]为undefined。让我通过测试“正确”模式(先读取,后清除)和Array.from快照来验证修复方案是否有效,并确认面板内的 transcript-import 输入端是否也存在同样的缺陷。模式 A(先读取,后清除)可以正常工作;而有问题的模式(先清除,后读取引用)会返回
undefined。让我确认一下Array.from快照是否能作为一种修复方案,并检查 git blame,看看这个清除顺序是在什么时候引入的。Array.from快照修复方案有效。现在让我确认一下引入该clear-before-read顺序的具体 commit,并验证这两个存在缺陷的调用点是否都存在相同的 bug。我已经找到了根本原因,并利用沙盒中的 Chromium 对其进行了验证。以下是分析结果。
结论
该功能之所以“不生效”,是因为导入字幕的
<input onChange>处理程序在读取所选文件之前就清空了输入的value。 由于HTMLInputElement.files是一个实时的[SameObject]FileList,调用e.target.value = ""会立即清空该列表,导致之前捕获的引用指向了空列表,因此files?.[0]的结果为undefined。随后处理程序将此视为“未选择文件”并直接退出,从而静默丢弃了 SRT/VTT/JSON 文件——导致pendingTranscript未被设置,source从未携带字词变为"import",最终 Whisper 依然在运行。验证(沙盒中的 Chromium)
我使用 Playwright + 系统自带的 Chromium 复现了这三种模式:
const f = e.target.files?.[0]; e.target.value="";f = a.srtArray.from快照,后清空const a = Array.from(e.target.files); e.target.value="";a[0] = fix.srtconst files = e.target.files; e.target.value=""; files?.[0]undefined模式 C 报告
BEFORE_CLEAR_LENGTH=1,但随后的SYNC_FILE=UNDEFINED/AFTER_CLEAR_LENGTH=0/ASYNC_FILE=UNDEFINED—— 这证实了捕获的files引用被value=""清空操作置空了,即便是在同一个同步任务(sync tick)中也是如此。问题代码
两处导入字幕的入口点都使用了这种有问题的模式:
1.
components/ImportTranscriptOption.tsx(上传界面“跳过 Whisper”路径,第 121–131 行):2.
components/TranscriptPanel.tsx(编辑器内的“替换字幕”路径,第 378–382 行 →handleImportTranscript第 224 行):为什么媒体上传功能正常
Editor 的媒体文件输入(
components/Editor.tsx第 419–423 行)在清空之前就读取了文件,因此它避开了这个 Bug:为什么在 issue 追踪器中没有体现
这个清空顺序是在
2113b47(2026-07-27,“Make ModelSelector a generic option-component shell”)中引入的。解析逻辑(lib/parseTranscript.ts)和存储逻辑(lib/store.ts中的loadVideo→skipTranscription)是完全正确的——通过解析测试验证了流程;错误仅在于文件输入处理程序丢弃了文件。这就是为什么该功能看起来虽然“配置正确”却无法生效的原因。修复方案
读取文件(或对 FileList 进行快照),然后再清空
value。最小化的修改方案如下:ImportTranscriptOption.tsx:TranscriptPanel.tsx(使用快照,以便异步处理程序仍能接收到有效的列表):(或者将
handleImportTranscript的签名修改为File[] | File | null。)Array.from(e.target.files)这种快照形式是我已经在 Chromium 中验证过的,它能在不改变异步流程的情况下正确返回所选文件。所以:README 中关于导入的描述并没有说谎——功能本身是存在的——但
value=""这一行代码在所选文件被读取之前就将其抹除了,这就是为什么选中的字幕从未被使用,导致 Whisper 无法被跳过的原因。