Skip to content

“Import your own transcript — skip Whisper and edit with an SRT, VTT, or JSON caption file” doesn‘t work #105

Description

@reed-lv

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.tsmodels.ts
我来检查一下 GitHub 仓库中关于此功能的相关 issue,并查看解析测试(parse test)和国际化(i18n)消息。
目前没有可用的 GitHub 认证。让我通过 WebFetch 获取 issues,并检查解析测试 (parse test) 和 i18n 消息。
我来运行解析测试以确认其是否有效,并查看导入流程的 i18n 消息。
还有一个“替换字幕”的入口点。让我追踪一下 importWordstranscript.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 中的 loadVideoskipTranscription)是完全正确的——通过解析测试验证了流程;错误仅在于文件输入处理程序丢弃了文件。这就是为什么该功能看起来虽然“配置正确”却无法生效的原因。

修复方案

读取文件(或对 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 无法被跳过的原因。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions