feat(asr): implement ASR subtitle generation features#223
Conversation
- Added new IPC channels for ASR subtitle generation, progress tracking, cancellation, and API key validation. - Introduced ASRSubtitleService to handle subtitle generation logic, including audio extraction and transcription using Deepgram API. - Created ASRProgressModal and ASRSubtitlePrompt components for user interaction during subtitle generation. - Updated settings to include configuration options for Deepgram API key, default language, and transcription model. - Enhanced player page to support ASR functionality, including prompts and progress indicators. This update significantly improves the subtitle generation experience by leveraging AI technology for automatic subtitle creation.
|
Note Other AI code review bot(s) detectedCodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review. Walkthrough本次提交新增并集成完整 ASR 功能:共享类型与 IPC 通道、主进程 ASR 服务(音频提取、Deepgram 转录、字幕构建与格式化)、并发转录器、预处理模块、渲染端 API/钩子/UI、配置项及大量单元测试,主/渲染通过新增 IPC 通道交换进度与结果。 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant User as 用户
participant UI as 播放器 UI
participant Preload as 预加载 (window.api)
participant Main as 主进程 (ipc)
participant ASR as ASRSubtitleService
participant Audio as AudioPreprocessor
participant Trans as DeepgramTranscriber
participant DB as 字幕库
User->>UI: 点击「生成 AI 字幕」
UI->>Preload: window.api.asr.generate(options)
Preload->>Main: IpcChannel.ASR_Generate
Main->>ASR: generateSubtitle(opts, progressCallback)
ASR->>Audio: extractAudioTrack(videoPath)
Audio-->>ASR: { audioPath, duration }
ASR->>Trans: transcribeSegments(segments, deepgramOpts)
Trans-->>ASR: 分段结果 & 进度回调
ASR->>ASR: 构建/格式化字幕,导出文件
ASR->>DB: 保存字幕元数据
ASR-->>Main: 返回 ASRResult
Main->>Preload: 通过 IpcChannel.ASR_Progress 发送进度事件
Preload->>UI: 通知进度/完成
Estimated code review effort🎯 4 (复杂) | ⏱️ ~60 分钟 Possibly related PRs
诗
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
- Added .cursor to the .gitignore file to prevent tracking of cursor files in the repository.
There was a problem hiding this comment.
Actionable comments posted: 36
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
src/renderer/src/pages/settings/SettingsPage.tsx (1)
97-101: 语法错误:多余的 ']' 破坏 styled-components 模板字符串Line 101 的
]会导致编译失败。请移除。应用以下修复:
const ContentContainer = styled.div` display: flex; flex: 1; flex-direction: row; -]` +`src/preload/index.ts (1)
609-612: 补充 LogSourceWithContext 类型定义,修复参数结构原始建议的第一参数缺少必需的
process字段。根据 LogSourceWithContext 的定义,preload 中应包含process: 'main'。- console.error('[Preload]Failed to expose APIs:', error as Error) + ipcRenderer.invoke( + IpcChannel.App_LogToMain, + { process: 'main', module: 'preload' }, + 'error', + '[Preload]Failed to expose APIs', + [error] + )src/renderer/src/pages/player/components/SubtitleListPanel.tsx (1)
253-271: AI 选项卡可用性与可达性微调
- 当
asrEnabled为真但未传onOpenASRGenerator时,onClick仍可能为undefined;建议双重守卫。- 为禁用卡片补充
aria-disabled以提升可达性。最小修复如下:
- <OptionCard $disabled={!asrEnabled}> + <OptionCard $disabled={!asrEnabled} aria-disabled={!asrEnabled}> ... - <Button + <Button type="primary" disabled={!asrEnabled} - onClick={asrEnabled ? onOpenASRGenerator : undefined} + onClick={asrEnabled && onOpenASRGenerator ? onOpenASRGenerator : undefined} >
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (22)
packages/shared/IpcChannel.ts(1 hunks)packages/shared/types/asr.ts(1 hunks)packages/shared/types/index.ts(1 hunks)src/main/ipc.ts(4 hunks)src/main/services/ASRSubtitleService.ts(1 hunks)src/main/services/ConfigManager.ts(3 hunks)src/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.ts(1 hunks)src/main/services/asr/DeepgramTranscriber.ts(1 hunks)src/main/services/asr/SubtitleFormatter.ts(1 hunks)src/main/services/audio/AudioPreprocessor.ts(1 hunks)src/preload/index.ts(2 hunks)src/renderer/src/i18n/locales/en-us.json(3 hunks)src/renderer/src/i18n/locales/zh-cn.json(3 hunks)src/renderer/src/infrastructure/types/subtitle.ts(1 hunks)src/renderer/src/pages/player/PlayerPage.tsx(5 hunks)src/renderer/src/pages/player/components/ASRProgressModal.tsx(1 hunks)src/renderer/src/pages/player/components/ASRSubtitlePrompt.tsx(1 hunks)src/renderer/src/pages/player/components/SubtitleListPanel.tsx(4 hunks)src/renderer/src/pages/player/components/index.ts(1 hunks)src/renderer/src/pages/player/hooks/useASRSubtitle.ts(1 hunks)src/renderer/src/pages/settings/ASRSettings.tsx(1 hunks)src/renderer/src/pages/settings/SettingsPage.tsx(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-)、var(--ant-box-shadow-))
避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Zustand 必须在组件/Hook 顶层通过 selector 使用(useStore(selector)),禁止在 useMemo/useEffect 等内部调用 store Hook,避免 Hooks 顺序问题
避免使用返回对象的 Zustand selector(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器避免不必要重渲染与更新循环
React 副作用与状态更新规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定(默认 TS + React 18)
Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Files:
src/renderer/src/pages/player/components/index.tssrc/renderer/src/infrastructure/types/subtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.tssrc/renderer/src/pages/player/components/SubtitleListPanel.tsxsrc/renderer/src/pages/player/components/ASRSubtitlePrompt.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxpackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/main/services/ASRSubtitleService.tssrc/main/services/asr/DeepgramTranscriber.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/ConfigManager.tspackages/shared/IpcChannel.tspackages/shared/types/index.tssrc/preload/index.tssrc/main/ipc.tssrc/main/services/asr/SubtitleFormatter.tssrc/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/SettingsPage.tsx
**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
布局优先使用 flex,除必要场景外避免将 grid 作为默认方案
Files:
src/renderer/src/pages/player/components/index.tssrc/renderer/src/infrastructure/types/subtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.tssrc/renderer/src/pages/player/components/SubtitleListPanel.tsxsrc/renderer/src/pages/player/components/ASRSubtitlePrompt.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxpackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/main/services/ASRSubtitleService.tssrc/main/services/asr/DeepgramTranscriber.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/ConfigManager.tspackages/shared/IpcChannel.tspackages/shared/types/index.tssrc/preload/index.tssrc/main/ipc.tssrc/main/services/asr/SubtitleFormatter.tssrc/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/SettingsPage.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志而不是 console
logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
任何组件或页面都不得写入 currentTime(播放器控制权由编排器统一管理)
Files:
src/renderer/src/pages/player/components/index.tssrc/renderer/src/infrastructure/types/subtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.tssrc/renderer/src/pages/player/components/SubtitleListPanel.tsxsrc/renderer/src/pages/player/components/ASRSubtitlePrompt.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxpackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/main/services/ASRSubtitleService.tssrc/main/services/asr/DeepgramTranscriber.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/ConfigManager.tspackages/shared/IpcChannel.tspackages/shared/types/index.tssrc/preload/index.tssrc/main/ipc.tssrc/main/services/asr/SubtitleFormatter.tssrc/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/SettingsPage.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
所有图标统一使用 lucide-react,不使用 emoji
Files:
src/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/player/components/SubtitleListPanel.tsxsrc/renderer/src/pages/player/components/ASRSubtitlePrompt.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/renderer/src/pages/player/PlayerPage.tsxsrc/renderer/src/pages/settings/SettingsPage.tsx
🧠 Learnings (1)
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Applied to files:
src/renderer/src/pages/player/components/SubtitleListPanel.tsxsrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/renderer/src/pages/player/PlayerPage.tsx
🧬 Code graph analysis (11)
src/renderer/src/pages/settings/ASRSettings.tsx (4)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/main/services/ConfigManager.ts (1)
setDeepgramApiKey(225-227)src/renderer/src/pages/settings/AboutSettings.tsx (1)
SettingRowTitle(412-424)
src/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.ts (1)
packages/shared/types/asr.ts (1)
DeepgramWord(125-136)
src/renderer/src/pages/player/components/ASRSubtitlePrompt.tsx (1)
src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (1)
packages/shared/types/asr.ts (1)
ASRProgress(60-75)
src/renderer/src/pages/player/hooks/useASRSubtitle.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (2)
ASRProgress(60-75)ASRResult(80-104)
src/main/services/ASRSubtitleService.ts (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (7)
ASRProgress(60-75)ASRGenerateOptions(24-35)ASRResult(80-104)DeepgramResponse(157-184)ASRSubtitleItem(8-19)DeepgramUtterance(141-152)DeepgramWord(125-136)src/main/services/ConfigManager.ts (1)
configManager(246-246)
src/main/services/asr/DeepgramTranscriber.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (3)
AudioSegment(109-120)TranscriptSegment(189-198)DeepgramResponse(157-184)
src/main/services/audio/AudioPreprocessor.ts (1)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)
src/main/ipc.ts (1)
packages/shared/types/asr.ts (1)
ASRGenerateOptions(24-35)
src/main/services/asr/SubtitleFormatter.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (1)
ASRSubtitleItem(8-19)
src/renderer/src/pages/player/PlayerPage.tsx (1)
src/renderer/src/pages/player/hooks/useASRSubtitle.ts (1)
useASRSubtitle(12-163)
🪛 Biome (2.1.2)
src/renderer/src/pages/player/components/ASRSubtitlePrompt.tsx
[error] 57-59: A form label must be associated with an input.
Consider adding a for or htmlFor attribute to the label element or moving the input element to inside the label element.
(lint/a11y/noLabelWithoutControl)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
🔇 Additional comments (15)
src/renderer/src/i18n/locales/en-us.json (1)
149-151: Ensure the UI switches to actionEnabled when ASR is availableKeep “Coming Soon” only for gated states. Use actionEnabled: "Generate" when the feature is enabled.
同样建议在组件中检查对 actionEnabled 的引用与条件切换逻辑。
src/main/services/asr/DeepgramTranscriber.ts (1)
12-15: 导入路径正确,main 侧已存在相应导出,无需调整根据验证结果,
src/main/services/LoggerService.ts确实存在且在第417行导出loggerService常量。从src/main/services/asr/DeepgramTranscriber.ts相对导入'../LoggerService'的路径指向正确,不会导致构建失败。代码已遵循编码指南的 logger 使用规范。Likely an incorrect or invalid review comment.
src/renderer/src/i18n/locales/zh-cn.json (1)
274-278: 已验证文案切换逻辑正确实现——无需修改组件在 SubtitleListPanel.tsx 第 268-270 行已完整实现条件逻辑:当
asrEnabled为 true 时显示 actionEnabled("生成"),为 false 时显示 action("即将推出")。同时按钮的 disabled 状态(第 265 行)和点击处理(第 266 行)也由 asrEnabled 完整控制。i18n 配置正确。src/renderer/src/pages/player/components/index.ts (1)
3-4: 导出验证通过,无需后续调整两个新增组件已正确声明为 default 导出:
ASRProgressModal.tsx(第 86 行)ASRSubtitlePrompt.tsx(第 85 行)barrel 导出模式一致,无未定义引用风险。代码变更可直接合并。
packages/shared/IpcChannel.ts (1)
143-148: ASR IPC 通道实现完整,无遗漏验证确认四个通道均已在主进程与 renderer 层完成实现:
- ASR_Generate/Cancel/ValidateApiKey:主进程通过
ipcMain.handle注册,preload 通过ipcRenderer.invoke调用(命令模式)✓- ASR_Progress:主进程通过
mainWindow.webContents.send发送,renderer 通过window.api.on/off订阅(事件流模式)✓架构规范,通道分组清晰,无重复或遗漏实现。
src/renderer/src/pages/player/components/ASRProgressModal.tsx (1)
15-32: Now I need to search for the actual translation files in the codebase to understand the structure:I need to search the codebase for translation/i18n file locations to understand the implementation better:
改进建议不完整,提议使用的翻译键不存在于代码库中
根据代码搜索结果,所有 ASRProgressStage 枚举值(initializing、extracting、transcribing、formatting、saving、complete)都已在 switch 语句中覆盖。default 分支触发的可能性极小。
更重要的是,建议的翻译键
player.asr.progress.stages.unknown在代码库中不存在。直接实现该改动不会产生预期效果,因为 i18next 会返回未翻译的键名而非真正的兜底文案。若要添加兜底处理,需要:
- 在所有语言的翻译文件中创建该键值对
- 在此基础上修改代码
建议先确认是否真有未覆盖的状态场景,再考虑添加翻译键。
Likely an incorrect or invalid review comment.
src/renderer/src/pages/settings/SettingsPage.tsx (1)
56-61: ASR 设置入口与路由接入验证完毕已确认所有 i18n 键值在翻译文件中完整定义:
settings.asr.title、settings.asr.description及其他相关文案键值在src/renderer/src/i18n/locales/en-us.json和zh-cn.json中均已正确定义。菜单使用的 Mic 图标符合 lucide-react 规范,菜单与路由结构合理。src/renderer/src/pages/player/PlayerPage.tsx (4)
37-48: ASR 组件引入与导出路径整合良好命名、分层与路径一致,符合组件目录约定。无阻断问题。
741-743: 向 SubtitleListPanel 透出的 ASR 能力开关与回调属性命名清晰、含义准确,能有效门控触发入口。LGTM。
780-793: ASR 弹窗装配正确且职责清晰
- 提示弹窗与进度弹窗的开关、回调与进度数据传递合理。
- 回调引用均为 useCallback,稳定性良好。
LGTM。
123-134: useASRSubtitle 的 videoId 防御建议确认有效经验证,该建议合理且必要:
videoId在 PlayerPage 可为0(第 86-87 行),但 useASRSubtitle 内的检查if (!videoId)将 0 视为无效值(第 73 行)- 传递
videoId || null将 0 正确转换为null,与 Hook 声明的参数类型number | null对齐- 此修改区分了"无有效视频"(应为 null)和"有效 ID"(应为 number > 0)的意图
建议应用,符合防御性编程规范。
建议的最小改动:
- } = useASRSubtitle(videoId, originalFilePathRef.current) + } = useASRSubtitle(videoId || null, originalFilePathRef.current)src/main/ipc.ts (1)
727-735: 取消与 API Key 校验处理看起来正确语义清晰,未泄露敏感数据到日志。LGTM。
src/main/services/__tests__/ASRSubtitleService.shouldBreakSentence.test.ts (1)
83-94: 测试覆盖充分且针对关键边界场景
- 覆盖标点/停顿/时长分段、最后词、零停顿与中文标点。
- 动态导入与私有方法访问处理合理。
- 用例可读性强,断言明确。
LGTM。
Also applies to: 125-131, 133-168, 170-184, 186-214, 216-242, 244-429
src/renderer/src/pages/player/components/SubtitleListPanel.tsx (1)
41-44: ASR 入口属性扩展清晰
onOpenASRGenerator与asrEnabled的职责划分明确,默认值妥当。LGTM。Also applies to: 67-70
src/renderer/src/pages/player/hooks/useASRSubtitle.ts (1)
51-56: 自动关闭进度依赖阶段常量,需与主进程枚举对齐主服务当前上报为字符串(如 'complete'),本地比较的是 ASRProgressStage.Complete,可能导致不关闭。
建议优先修复主服务按 ASRProgressStage 枚举上报;如需前端兜底,可暂加兼容:
- if (progress.stage === ASRProgressStage.Complete) { + if (progress.stage === ASRProgressStage.Complete || (progress as any).stage === 'complete') { setTimeout(() => setShowAsrProgress(false), 2000) }
| for (let i = 0; i < segments.length; i++) { | ||
| const segment = segments[i] | ||
|
|
||
| // 添加到队列 | ||
| const promise = this.queue.add(async () => { | ||
| try { | ||
| // 携带上一段末尾文本作为提示 | ||
| const promptText = | ||
| previousTranscript.length > 200 ? previousTranscript.slice(-200) : previousTranscript | ||
|
|
||
| const response = await this.transcribeSingleSegment(segment.filePath, { | ||
| ...options, | ||
| prompt: promptText | ||
| }) | ||
|
|
||
| completed++ | ||
| onProgress?.({ completed, total: segments.length, current: i }) | ||
|
|
||
| // 更新上一段文本 | ||
| if (response.results.channels[0]?.alternatives[0]?.transcript) { | ||
| const fullTranscript = response.results.channels[0].alternatives[0].transcript | ||
| previousTranscript = fullTranscript | ||
| } | ||
|
|
||
| return { | ||
| audioSegment: segment, | ||
| response, | ||
| success: true | ||
| } as TranscriptSegment | ||
| } catch (error) { | ||
| completed++ | ||
| onProgress?.({ completed, total: segments.length, current: i }) | ||
|
|
||
| logger.error('段转写失败', { | ||
| index: segment.index, | ||
| error: error instanceof Error ? error.message : String(error) | ||
| }) | ||
|
|
||
| return { | ||
| audioSegment: segment, | ||
| response: {} as DeepgramResponse, | ||
| success: false, | ||
| error: error instanceof Error ? error.message : String(error) | ||
| } as TranscriptSegment | ||
| } | ||
| }) | ||
|
|
||
| if (promise) { | ||
| results.push((await promise) as TranscriptSegment) | ||
| } | ||
| } |
There was a problem hiding this comment.
并发与“前文提示链”语义冲突;当前用法实际串行且浪费队列
for 循环里对每个 queue.add(... ) 立即 await,实际不会并发;同时 previousTranscript 需要严格顺序依赖。应明确两种模式:需要提示链则串行;不需要则并发。
最小修复(强制串行,确保提示链正确;可去除队列):
- for (let i = 0; i < segments.length; i++) {
- const segment = segments[i]
- const promise = this.queue.add(async () => {
- ...
- })
- if (promise) {
- results.push((await promise) as TranscriptSegment)
- }
- }
- await this.queue.onIdle()
+ for (let i = 0; i < segments.length; i++) {
+ const segment = segments[i]
+ try {
+ const promptText =
+ previousTranscript.length > 200 ? previousTranscript.slice(-200) : previousTranscript
+ const response = await this.transcribeSingleSegment(segment.filePath, {
+ ...options,
+ prompt: promptText
+ })
+ completed++
+ onProgress?.({ completed, total: segments.length, current: i })
+ const fullTranscript = response.results.channels?.[0]?.alternatives?.[0]?.transcript
+ if (fullTranscript) previousTranscript = fullTranscript
+ results.push({ audioSegment: segment, response, success: true })
+ } catch (error) {
+ completed++
+ onProgress?.({ completed, total: segments.length, current: i })
+ logger.error('段转写失败', {
+ index: segment.index,
+ error: error instanceof Error ? error.message : String(error)
+ })
+ results.push({
+ audioSegment: segment,
+ success: false,
+ error: error instanceof Error ? error.message : String(error)
+ })
+ }
+ }若未来需要并发,可新增 usePromptChaining: boolean 选项,按需切换模式。
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/services/asr/DeepgramTranscriber.ts around lines 66–116, the code
calls queue.add(...) and immediately awaits each returned promise inside the for
loop which both defeats concurrency and mixes up the required strict ordering
for previousTranscript; implement two clear modes: if prompt chaining is
required (add a new boolean option usePromptChaining), run segments strictly
serially (do not use the queue — await each transcribeSequentially call, update
previousTranscript after each response, report progress), otherwise run in
concurrent mode (use queue.add for each segment without awaiting immediately,
collect promises into an array and await Promise.all, and do not read/write
previousTranscript across tasks). Ensure error handling and onProgress updates
remain correct for both modes.
- Replaced inline description spans with HelpText component for better styling and consistency. - Updated API key validation status handling to simplify logic. - Added ExternalLink icon to the "Get Key" button for improved visual clarity. - Introduced HelpText styled component for uniform text styling across settings descriptions.
- Added early return in API key validation to prevent further processing when the key is invalid, enhancing user feedback and preventing unnecessary state updates.
…dling - Change error handling to inspect result.errorCode first - Map known error codes to specific user-friendly messages using switch statement - Log error code and raw error payload for debugging - Attach error code to Error object for upstream handling - Fall back to string matching only when errorCode is absent - Handle TASK_CANCELLED silently (no error shown to user) - Add SUBTITLE_EXTRACTION_FAILED mapping to transcriptionFailed message
…dle empty responses - Updated TranscriptSegment interface to make the response property optional. - Modified extractSubtitlesFromResponse method to handle cases where the response is undefined, logging a warning and returning an empty subtitles array. - Removed the default empty object assignment for response in DeepgramTranscriber to prevent unnecessary object creation.
- update `ASRGenerateOptions.language` to accept `'auto'` for automatic language detection - clarify JSDoc for `DeepgramWord.punctuated_word` to "Punctuated word form"
- Add checks to ensure `mainWindow` and `mainWindow.webContents` are not destroyed before sending ASR progress updates. - Implement a `try-catch` block to gracefully handle potential errors during progress event emission. - Log a warning if sending the ASR progress event fails, improving robustness.
- replace fs.readFileSync with fs.createReadStream for memory efficiency - add proper stream error handling and cleanup - use fsPromises.stat to get file size for Content-Length header - implement stream piping to HTTP request - add comprehensive test coverage for streaming implementation - handle stream destruction on network errors and timeouts
- replace placeholder validation with actual API endpoint call - add comprehensive error handling for various HTTP status codes - implement timeout handling for network requests - add detailed error messages for different failure scenarios - include unit tests covering all validation cases
…age enum - Update all reportProgress calls to use ASRProgressStage enum values instead of hardcoded strings - Change ASRProgressStage import from type-only to runtime import - Update reportProgress method signature to enforce ASRProgressStage type - Fixes frontend recognition issues caused by mismatched stage values - Ensures compile-time correctness for progress reporting Changes made: - Lines 77, 80, 100, 123, 136, 147, 180, 210: Replace string literals with enum values - Import: Move ASRProgressStage from type-only import to runtime import - Method signature: Strengthen type safety by removing string union type
…eprocessing - add createPersistentSubtitlePath method to store subtitles in userData directory - change subtitle export to use persistent paths instead of temp directory - enable immediate temp directory cleanup after subtitle generation - update extractAudioTrack to properly transcode audio for ASR processing - improve FFmpeg command documentation for clarity
…stency - replace ternary operator with nullish coalescing operator in get method -【ASR Config】update setters to use setAndNotify for consistent behavior - ensure ASR configuration changes trigger subscriber notifications
- add ASRGenerateOptions, ASRProgress, and ASRResult type imports - enhance ASR generate method with proper return type ASRResult - implement onProgress listener for real-time ASR progress updates - add proper cleanup for progress event listeners
…overage - extract duration parsing logic into dedicated parseFFmpegDuration function - support multiple fractional second formats (1-3 decimal places) - add null handling for malformed duration strings - implement comprehensive test suite covering edge cases - improve regex pattern to handle optional fractional parts
…ess API - remove generic on/off IPC listeners from preload API - add dedicated asr.offProgress() method for cleanup - update useASRSubtitle hook to use whitelist-based progress subscription - improve type safety by removing untyped channel string parameters
…line - add cancellation checks at multiple stages during subtitle generation - implement proper cleanup for cancelled requests in DeepgramTranscriber - enhance cancel button UX with confirmation mode and auto-reset - improve error logging to distinguish user cancellation from failures - add REQUEST_CANCELLED error code mapping to TASK_CANCELLED
…tring matching Move error handling logic from try-catch block to main response handling, eliminating unnecessary exception throwing and catching. Simplify catch block to handle only truly unexpected errors like network failures.
There was a problem hiding this comment.
Actionable comments posted: 12
♻️ Duplicate comments (11)
src/main/services/ConfigManager.ts (1)
65-68: API Key 明文存储风险仍未解决历史评论中已指出 Deepgram API Key 通过 electron-conf 明文存储到用户目录存在安全和合规风险。建议:
- 使用系统级安全存储(如 keytar 或 Electron safeStorage API)
- 仅在 electron-conf 中保存"是否已配置"的布尔标识
- 在 getDeepgramApiKey 时从安全存储读取
Also applies to: 221-243
src/main/services/ASRSubtitleService.ts (2)
254-254: segmentCount 统计不准确历史评论中已指出该字段固定为 1 不准确。当前实现中:
- 如果使用 utterances:应为
utterances.length- 如果使用 words 分组:应为分组后的句子数量
建议修改:
stats: { duration: audioDuration, processingTime, - segmentCount: 1, + segmentCount: formattedSubtitles.length, subtitleCount: formattedSubtitles.length }
529-538: 错误码映射仍依赖字符串匹配,鲁棒性不足历史评论中已指出通过
message.includes匹配中文字符串的方式脆弱。当前实现问题:
- 依赖中文字符串"配额"、"网络"等进行判断
- 多语言环境下可能失效
- DeepgramTranscriber 抛出的错误缺乏标准化错误码
建议:
- 在 DeepgramTranscriber 中定义错误码枚举
- 抛出结构化错误对象(包含 code 字段)
- getErrorCode 优先读取错误对象的 code 属性
src/renderer/src/pages/player/components/ASRProgressModal.tsx (1)
126-135: 完成态用户体验可优化历史评论中已指出,当
progress.stage === Complete时:
- 取消按钮被禁用但仍显示"取消"文案,用户体验不佳
- 建议切换为"关闭"按钮并允许用户关闭 Modal
优化建议:
<Flex justify="flex-end"> + {progress.stage === ASRProgressStage.Complete ? ( + <Button onClick={onCancel}>{t('common.close')}</Button> + ) : ( <CancelButton $confirmMode={confirmMode} onClick={handleCancel} onMouseLeave={handleCancelMouseLeave} - disabled={progress.stage === ASRProgressStage.Complete} > {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} </CancelButton> + )} </Flex>src/renderer/src/i18n/locales/zh-cn.json (1)
76-84: 缺少 API 调用频率限制的专门错误文案历史评论中已指出,当 Deepgram API 返回 429 状态码时,应提供专门的
rateLimited文案键以精确提示用户。当前 errors 部分包含多种错误类型,但缺少针对频率限制的翻译。建议补充:
"errors": { "noApiKey": "请先在设置中配置 Deepgram API Key", "invalidApiKey": "API Key 无效,请检查设置", "apiQuotaExceeded": "API 配额已用尽,请检查您的 Deepgram 账户", + "rateLimited": "请求过于频繁,请稍后重试", "networkError": "网络错误,请检查连接后重试", "audioExtractionFailed": "音频提取失败,请检查视频文件", "transcriptionFailed": "转写失败,请重试", "unknown": "生成失败:{{message}}" },同时需要在
src/main/services/ASRSubtitleService.ts的getErrorCode方法中添加对应的错误码映射。src/renderer/src/pages/settings/ASRSettings.tsx (1)
87-97: 校验结果被当作布尔使用,导致无效 Key 也会显示“有效”并被保存
window.api.asr.validateApiKey很可能返回{ valid, error }对象(见主进程/类型定义),当前将其当作布尔值使用,任何非空对象都会进入成功分支,造成误判与错误持久化。请兼容两种返回形态或统一为对象。应用修复示例:
- const isValid = await window.api.asr.validateApiKey(deepgramApiKey) - setApiKeyValid(isValid) - if (isValid) { + const resp = await window.api.asr.validateApiKey(deepgramApiKey) + const isValid = typeof resp === 'boolean' ? resp : !!resp?.valid + setApiKeyValid(isValid) + if (isValid) { message.success(t('settings.asr.apiKey.valid') || 'API Key 有效') // Save the validated key await window.api.config.set('deepgramApiKey', deepgramApiKey) - } else { - message.error(t('settings.asr.apiKey.invalid') || 'API Key 无效') + } else { + const errMsg = typeof resp === 'object' ? resp?.error : undefined + message.error(errMsg || t('settings.asr.apiKey.invalid') || 'API Key 无效') }src/renderer/src/pages/player/hooks/useASRSubtitle.ts (1)
71-74: videoId 判空错误:videoId=0 会被误判为缺失
!videoId将合法0视为 falsy。改为仅对 null/undefined 判空。此前已有相同反馈。- if (!videoPath || !videoId) { + if (!videoPath || videoId == null) { message.error(t('player.asr.errors.unknown', { message: 'No video path' })) return }src/main/services/audio/AudioPreprocessor.ts (1)
40-59: 时长解析实现已对齐此前建议,LGTM放宽了正则到 1–3 位小数并按位数计算分母,能覆盖无小数/1–3 位小数场景,避免 NaN。
src/main/services/asr/DeepgramTranscriber.ts (2)
68-117: 并发语义与提示链冲突:queue.add 后立即 await 导致实际串行且浪费队列当前 for 循环里对 queue.add(...) 的 Promise 逐个 await,既无法并发,也让“前文提示链”语义混乱(此前已提醒)。若确需提示链(previousTranscript),应显式串行并去掉不必要的队列;若不需提示链,则批量入队并 Promise.all。
最小修复(强制串行,保证提示链正确;可移除队列引用):
- for (let i = 0; i < segments.length; i++) { - const segment = segments[i] - const promise = this.queue.add(async () => { - try { - const promptText = - previousTranscript.length > 200 ? previousTranscript.slice(-200) : previousTranscript - const response = await this.transcribeSingleSegment(segment.filePath, { - ...options, - prompt: promptText - }) - completed++ - onProgress?.({ completed, total: segments.length, current: i }) - if (response.results.channels[0]?.alternatives[0]?.transcript) { - const fullTranscript = response.results.channels[0].alternatives[0].transcript - previousTranscript = fullTranscript - } - return { audioSegment: segment, response, success: true } as TranscriptSegment - } catch (error) { - completed++ - onProgress?.({ completed, total: segments.length, current: i }) - logger.error('段转写失败', { index: segment.index, error: error instanceof Error ? error.message : String(error) }) - return { audioSegment: segment, success: false, error: error instanceof Error ? error.message : String(error) } as TranscriptSegment - } - }) - if (promise) { - results.push((await promise) as TranscriptSegment) - } - } - await this.queue.onIdle() + for (let i = 0; i < segments.length; i++) { + const segment = segments[i] + try { + const promptText = previousTranscript.length > 200 ? previousTranscript.slice(-200) : previousTranscript + const response = await this.transcribeSingleSegment(segment.filePath, { ...options, prompt: promptText }) + completed++ + onProgress?.({ completed, total: segments.length, current: i }) + const fullTranscript = response.results.channels?.[0]?.alternatives?.[0]?.transcript + if (fullTranscript) previousTranscript = fullTranscript + results.push({ audioSegment: segment, response, success: true }) + } catch (error) { + completed++ + onProgress?.({ completed, total: segments.length, current: i }) + logger.error('段转写失败', { index: segment.index, error: error instanceof Error ? error.message : String(error) }) + results.push({ audioSegment: segment, success: false, error: error instanceof Error ? error.message : String(error) }) + } + }若未来需要并发,建议新增选项 usePromptChaining: boolean,按需切换两种模式。
257-327: 为 429(限流)提供标准化错误码,便于 UI/i18n 精准提示建议在 429 分支附带 code='rate_limited',上层可直接映射到专用文案键:
- } else if (res.statusCode === 429) { - reject(new Error('API 调用频率超限')) + } else if (res.statusCode === 429) { + const err = new Error('API 调用频率超限') + ;(err as any).code = 'rate_limited' + reject(err)src/renderer/src/i18n/locales/en-us.json (1)
16-25: 补充 429(限流)专用文案,匹配后端错误码映射建议在 errors 下新增 rateLimited,配合后端抛出的 code='rate_limited':
"errors": { "apiQuotaExceeded": "API quota exceeded, please check your Deepgram account", "audioExtractionFailed": "Audio extraction failed, please check the video file", "invalidApiKey": "Invalid API Key, please check settings", "networkError": "Network error, please check your connection and retry", "noApiKey": "Please configure Deepgram API Key in settings first", "transcriptionFailed": "Transcription failed, please retry", + "rateLimited": "Too many requests, please try again later", "unknown": "Generation failed: {{message}}" },
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (16)
packages/shared/types/asr.ts(1 hunks)src/main/ipc.ts(4 hunks)src/main/services/ASRSubtitleService.ts(1 hunks)src/main/services/ConfigManager.ts(3 hunks)src/main/services/__tests__/AudioPreprocessor.test.ts(1 hunks)src/main/services/asr/DeepgramTranscriber.ts(1 hunks)src/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.ts(1 hunks)src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts(1 hunks)src/main/services/audio/AudioPreprocessor.ts(1 hunks)src/preload/index.ts(2 hunks)src/renderer/src/i18n/locales/en-us.json(6 hunks)src/renderer/src/i18n/locales/zh-cn.json(4 hunks)src/renderer/src/pages/player/components/ASRProgressModal.tsx(1 hunks)src/renderer/src/pages/player/hooks/useASRSubtitle.ts(1 hunks)src/renderer/src/pages/settings/ASRSettings.tsx(1 hunks)src/renderer/src/pages/settings/index.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-)、var(--ant-box-shadow-))
避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Zustand 必须在组件/Hook 顶层通过 selector 使用(useStore(selector)),禁止在 useMemo/useEffect 等内部调用 store Hook,避免 Hooks 顺序问题
避免使用返回对象的 Zustand selector(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器避免不必要重渲染与更新循环
React 副作用与状态更新规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定(默认 TS + React 18)
Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Files:
src/main/services/ASRSubtitleService.tssrc/main/services/__tests__/AudioPreprocessor.test.tssrc/main/ipc.tssrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/settings/index.tsxsrc/preload/index.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/ConfigManager.tssrc/main/services/asr/DeepgramTranscriber.ts
**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
布局优先使用 flex,除必要场景外避免将 grid 作为默认方案
Files:
src/main/services/ASRSubtitleService.tssrc/main/services/__tests__/AudioPreprocessor.test.tssrc/main/ipc.tssrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/settings/index.tsxsrc/preload/index.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/ConfigManager.tssrc/main/services/asr/DeepgramTranscriber.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志而不是 console
logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
任何组件或页面都不得写入 currentTime(播放器控制权由编排器统一管理)
Files:
src/main/services/ASRSubtitleService.tssrc/main/services/__tests__/AudioPreprocessor.test.tssrc/main/ipc.tssrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/renderer/src/pages/player/hooks/useASRSubtitle.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/settings/index.tsxsrc/preload/index.tssrc/main/services/audio/AudioPreprocessor.tssrc/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/ConfigManager.tssrc/main/services/asr/DeepgramTranscriber.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
所有图标统一使用 lucide-react,不使用 emoji
Files:
src/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/settings/index.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsx
🧠 Learnings (9)
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Applied to files:
src/renderer/src/pages/player/hooks/useASRSubtitle.ts
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx,js,jsx} : logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
Applied to files:
src/renderer/src/pages/player/hooks/useASRSubtitle.ts
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/DictionaryPopover.{tsx,jsx} : DictionaryPopover 组件需使用 Ant Design CSS 变量替代硬编码颜色,完整适配明暗主题(文字、背景、边框、滚动条、交互状态)
Applied to files:
src/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
Applied to files:
src/renderer/src/pages/settings/ASRSettings.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-*)、var(--ant-box-shadow-*))
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
🧬 Code graph analysis (10)
src/main/services/ASRSubtitleService.ts (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (7)
ASRProgress(60-75)ASRGenerateOptions(24-35)ASRResult(80-104)DeepgramResponse(157-184)ASRSubtitleItem(8-19)DeepgramUtterance(141-152)DeepgramWord(125-136)src/main/services/ConfigManager.ts (1)
configManager(246-246)
src/main/services/__tests__/AudioPreprocessor.test.ts (1)
src/main/services/audio/AudioPreprocessor.ts (1)
parseFFmpegDuration(40-59)
src/main/ipc.ts (1)
packages/shared/types/asr.ts (1)
ASRGenerateOptions(24-35)
src/renderer/src/pages/player/hooks/useASRSubtitle.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (2)
ASRProgress(60-75)ASRResult(80-104)
src/renderer/src/pages/settings/ASRSettings.tsx (4)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/main/services/ConfigManager.ts (1)
setDeepgramApiKey(225-227)src/renderer/src/pages/settings/index.tsx (8)
SettingContainer(14-29)SettingGroup(95-101)SettingTitle(31-39)SettingDescription(49-53)SettingDivider(55-58)SettingRow(60-66)SettingRowTitle(68-75)HelpText(102-107)
src/preload/index.ts (1)
packages/shared/types/asr.ts (3)
ASRGenerateOptions(24-35)ASRResult(80-104)ASRProgress(60-75)
src/main/services/audio/AudioPreprocessor.ts (1)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)
src/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.ts (1)
packages/shared/types/asr.ts (1)
DeepgramResponse(157-184)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (2)
packages/shared/types/asr.ts (1)
ASRProgress(60-75)src/renderer/src/infrastructure/styles/theme.ts (4)
FONT_SIZES(25-40)FONT_WEIGHTS(11-22)SPACING(43-58)ANIMATION_DURATION(91-100)
src/main/services/asr/DeepgramTranscriber.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (3)
AudioSegment(109-120)TranscriptSegment(189-198)DeepgramResponse(157-184)
🪛 GitHub Check: test (macos-latest, 20)
src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts
[failure] 286-286:
'options' is declared but its value is never read.
[failure] 256-256:
'options' is declared but its value is never read.
[failure] 226-226:
'options' is declared but its value is never read.
[failure] 200-200:
'options' is declared but its value is never read.
[failure] 174-174:
'options' is declared but its value is never read.
[failure] 144-144:
'options' is declared but its value is never read.
[failure] 114-114:
'options' is declared but its value is never read.
🪛 GitHub Check: test (ubuntu-latest, 20)
src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts
[failure] 286-286:
'options' is declared but its value is never read.
[failure] 256-256:
'options' is declared but its value is never read.
[failure] 226-226:
'options' is declared but its value is never read.
[failure] 200-200:
'options' is declared but its value is never read.
[failure] 174-174:
'options' is declared but its value is never read.
[failure] 144-144:
'options' is declared but its value is never read.
[failure] 114-114:
'options' is declared but its value is never read.
🔇 Additional comments (2)
src/main/services/asr/__tests__/DeepgramTranscriber.callDeepgramAPI.test.ts (1)
115-191: 用例覆盖与实现细节校验充分,读写流与请求生命周期模拟合理参数断言、流管道、错误与 JSON 解析分支均被良好覆盖,当前实现 LGTM。
src/renderer/src/pages/player/hooks/useASRSubtitle.ts (1)
58-61: 确认:ASR 进度订阅返回取消函数 — 无需修改
已验证:src/preload/index.ts 的 onProgress 实现返回一个 unsubscribe(() => ipcRenderer.removeListener(IpcChannel.ASR_Progress, handler)),在 useASRSubtitle 中直接使用返回值作为取消函数是正确的。
src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts
Outdated
Show resolved
Hide resolved
| <Flex vertical gap={8} style={{ flex: 1, maxWidth: '400px' }}> | ||
| <Flex gap={8}> | ||
| <Input.Password | ||
| value={deepgramApiKey} | ||
| onChange={handleApiKeyChange} | ||
| placeholder={t('settings.asr.apiKey.placeholder')} | ||
| onBlur={handleApiKeySave} | ||
| status={apiKeyValid === false ? 'error' : undefined} | ||
| /> | ||
| <Button onClick={handleValidateApiKey} loading={validatingApiKey}> | ||
| {t('settings.asr.apiKey.validate')} | ||
| </Button> | ||
| </Flex> | ||
| <Button | ||
| type="link" | ||
| onClick={openDeepgramWebsite} | ||
| style={{ alignSelf: 'flex-start', padding: 0 }} | ||
| > | ||
| {t('settings.asr.apiKey.getKey')} <ExternalLink size={14} style={{ marginLeft: 4 }} /> | ||
| </Button> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
移除行内样式与硬编码尺寸,改用 styled-components 与设计 Token
style={{ flex: 1, maxWidth: '400px' }}、style={{ alignSelf: 'flex-start', padding: 0 }}与size={14}为硬编码;建议抽到 styled 组件并采用主题 Token/集中常量,避免魔数并统一暗/明主题表现。As per coding guidelines
示例要点:
- 提取 RightCol/LinkButton styled 组件替代行内样式;
- 将 14/400 抽为常量或从主题 token 获取(如 icon 尺寸/内容区宽度)。
Also applies to: 174-179, 191-196
🤖 Prompt for AI Agents
In src/renderer/src/pages/settings/ASRSettings.tsx around lines 142-161 (and
also apply same changes at 174-179 and 191-196): remove the inline styles and
hardcoded numeric literals by creating styled-components (e.g., RightCol and
LinkButton) that consume theme tokens or centralized constants for layout and
sizes; move style={{ flex: 1, maxWidth: '400px' }} into RightCol with maxWidth
read from a token/constant (or theme), replace style={{ alignSelf: 'flex-start',
padding: 0 }} with LinkButton styled-component that applies theme spacing, and
replace size={14} with an ICON_SIZE constant or theme.iconSize; update JSX to
use the new styled components and import the tokens/constants from the theme or
a constants file, ensuring no inline styles or magic numbers remain.
Fix TypeScript TS6133 warnings by renaming unused 'options' parameters to '_options' in mock implementations. Preserved 'options' parameter name in the first test case as it's used for assertions.
- introduce ASRErrorCode enum for standardized error handling - replace generic string errorCode with typed ASRErrorCode -【Error Handling】provide specific error codes for common failure scenarios -【Type Safety】ensure consistent error reporting across ASR pipeline
…bortSignal Replace flawed cancellation logic that used activeRequests.size === 0 as a cancellation indicator with explicit AbortSignal-based cancellation. This fixes incorrect retry abortion when activeRequests is legitimately 0 between attempts. - Add abortController and currentRequestAbortController for explicit cancellation state - Replace activeRequests.size === 0 checks with abortController.signal.aborted - Update cancelAll() to set proper cancellation signals - Add resetCancellationState() for clean state on new transcription tasks - Improve type safety by using Set<ClientRequest> instead of Set<any>
- replace hardcoded font sizes with FONT_SIZES constants in ASRProgressModal and settings - use SPACING.XXS for consistent margin values in HelpText component - apply FONT_WEIGHTS.REGULAR instead of numeric font-weight values - add missing theme import to settings index file
Add 'auto' option to ASR language dropdown to enable automatic language detection for subtitle generation. The option appears first in the list and uses existing translation key for proper localization.
- remove unused offProgress method from ASR module - cleanup listener management by removing removeAllListeners call - simplify ASR progress event handling interface
Fix the critical issue where buildFFmpegArgs was always using pcm_s16le encoding regardless of the format parameter. This caused MP3 outputs to fail or generate incompatible files, breaking the ASR transcription pipeline. Changes: - Pass format parameter from extractAudioTrack to buildFFmpegArgs - Add proper codec mapping: wav=pcm_s16le, mp3=libmp3lame with 128k bitrate - Fix getErrorCode return type to ASRErrorCode - Add comprehensive tests for format handling - Include format parameter in logging for better debugging This ensures audio preprocessing works correctly for both WAV and MP3 formats, restoring the full ASR subtitle generation functionality.
There was a problem hiding this comment.
Actionable comments posted: 10
♻️ Duplicate comments (8)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (1)
103-111: 完成态不可关闭,需切换为“关闭”并允许点遮罩/右上角关闭完成阶段仍禁用按钮且 Modal 不可关闭,易造成“卡住”。建议完成态改为“关闭”按钮,并允许关闭遮罩/右上角。
应用此最小改动:
<Modal open={open} title={t('player.asr.progress.title')} footer={null} - closable={false} - maskClosable={false} + closable={progress.stage === ASRProgressStage.Complete} + maskClosable={progress.stage === ASRProgressStage.Complete} width={500} centered > @@ - <Flex justify="flex-end"> - <CancelButton - $confirmMode={confirmMode} - onClick={handleCancel} - onMouseLeave={handleCancelMouseLeave} - disabled={progress.stage === ASRProgressStage.Complete} - > - {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} - </CancelButton> - </Flex> + <Flex justify="flex-end"> + {progress.stage === ASRProgressStage.Complete ? ( + <Button onClick={onCancel}>{t('common.close')}</Button> + ) : ( + <CancelButton + $confirmMode={confirmMode} + onClick={handleCancel} + onMouseLeave={handleCancelMouseLeave} + > + {confirmMode + ? t('player.asr.progress.confirmCancel') + : t('player.asr.progress.cancel')} + </CancelButton> + )} + </Flex>As per coding guidelines
Also applies to: 126-135
src/renderer/src/pages/settings/ASRSettings.tsx (1)
143-164: 移除行内样式与魔数,使用 styled-components + 主题常量存在内联样式和硬编码尺寸(400、padding:0、size=14、marginLeft:4)。请抽为样式组件并用 FONT_SIZES/SPACING 常量。
应用示例(节选):
+import styled from 'styled-components' +import { FONT_SIZES, SPACING } from '@renderer/infrastructure/styles/theme' @@ +const INPUT_MAX_WIDTH = 400 +const ICON_SIZE = FONT_SIZES.SM +const RightCol = styled(Flex)` + flex: 1; + max-width: ${INPUT_MAX_WIDTH}px; +` +const LinkButton = styled(Button)` + align-self: flex-start; + padding: 0; +` +const LinkIcon = styled(ExternalLink)` + margin-left: ${SPACING.XXS}px; +` @@ - <Flex vertical gap={8} style={{ flex: 1, maxWidth: '400px' }}> + <RightCol vertical gap={8}> @@ - <Button - type="link" - onClick={openDeepgramWebsite} - style={{ alignSelf: 'flex-start', padding: 0 }} - > - {t('settings.asr.apiKey.getKey')} <ExternalLink size={14} style={{ marginLeft: 4 }} /> - </Button> + <LinkButton type="link" onClick={openDeepgramWebsite}> + {t('settings.asr.apiKey.getKey')} <LinkIcon size={ICON_SIZE} /> + </LinkButton> - </Flex> + </RightCol> @@ - <Selector - size={14} + <Selector + size={FONT_SIZES.SM} value={asrDefaultLanguage} onChange={handleAsrLanguageChange} options={asrLanguageOptions} /> @@ - <Selector - size={14} + <Selector + size={FONT_SIZES.SM} value={asrModel} onChange={handleAsrModelChange} options={asrModelOptions} />As per coding guidelines
Also applies to: 156-163, 175-180, 192-197, 136-142
src/main/services/ASRSubtitleService.ts (2)
252-257: 统计字段 segmentCount 恒为 1,不准确应使用实际条目数或分段数。
- segmentCount: 1, + segmentCount: formattedSubtitles.length,As per coding guidelines
530-539: 错误码映射缺少 SUBTITLE_EXTRACTION_FAILED,前端无法区分补充对应分支,避免落入 UNKNOWN。
private getErrorCode(errorMessage: string): ASRErrorCode { if (errorMessage.includes('NO_API_KEY')) return 'NO_API_KEY' if (errorMessage.includes('API Key 无效')) return 'INVALID_API_KEY' if (errorMessage.includes('配额')) return 'QUOTA_EXCEEDED' if (errorMessage.includes('网络')) return 'NETWORK_ERROR' if (errorMessage.includes('AUDIO_EXTRACTION_FAILED')) return 'AUDIO_EXTRACTION_FAILED' + if (errorMessage.includes('SUBTITLE_EXTRACTION_FAILED')) return 'SUBTITLE_EXTRACTION_FAILED' if (errorMessage.includes('TASK_CANCELLED')) return 'TASK_CANCELLED' if (errorMessage.includes('REQUEST_CANCELLED')) return 'TASK_CANCELLED' return 'UNKNOWN_ERROR' }As per coding guidelines
src/main/services/audio/AudioPreprocessor.ts (1)
186-239: Promise 可能被重复 resolve/重复记录日志(超时与 close 并发)
setTimeout分支 resolve 后,close事件仍会触发并再次 resolve/记录日志。需要幂等保护。应用如下最小修复(加入 settled 标志):
private async runFFmpegExtract( @@ - return new Promise((resolve) => { + return new Promise((resolve) => { + let settled = false const ffmpeg = spawn(ffmpegPath, args) @@ - const timeoutHandle = setTimeout( + const timeoutHandle = setTimeout( () => { - if (ffmpeg && !ffmpeg.killed) { - ffmpeg.kill('SIGKILL') - } - logger.error('FFmpeg 提取超时') - resolve({ success: false, error: 'FFmpeg 提取超时' }) + if (settled) return + if (ffmpeg && !ffmpeg.killed) ffmpeg.kill('SIGKILL') + logger.error('FFmpeg 提取超时') + settled = true + resolve({ success: false, error: 'FFmpeg 提取超时' }) }, 5 * 60 * 1000 ) @@ - ffmpeg.on('close', (code) => { + ffmpeg.on('close', (code) => { clearTimeout(timeoutHandle) - - if (code === 0) { + if (settled) return + settled = true + if (code === 0) { logger.debug('FFmpeg 提取成功', { code, duration }) resolve({ success: true, duration }) } else { logger.error('FFmpeg 提取失败', { code, error: stderrOutput.slice(-500) }) resolve({ success: false, error: `FFmpeg 退出码: ${code}` }) } })src/main/services/asr/DeepgramTranscriber.ts (3)
84-137: 当前逐个await队列任务,实际串行;且与“前文提示链”语义混淆并浪费队列for 循环里
const promise = this.queue.add(...); await promise导致无并发;如需严格前文提示链,应完全串行并去掉队列;如需并发,则不要跨任务读写previousTranscript。最小修复(显式串行,移除队列依赖):
- for (let i = 0; i < segments.length; i++) { - const segment = segments[i] - // 添加到队列 - const promise = this.queue.add(async () => { + for (let i = 0; i < segments.length; i++) { + const segment = segments[i] try { const promptText = previousTranscript.length > 200 ? previousTranscript.slice(-200) : previousTranscript - const response = await this.transcribeSingleSegment(segment.filePath, { + const response = await this.transcribeSingleSegment(segment.filePath, { ...options, prompt: promptText }) completed++ onProgress?.({ completed, total: segments.length, current: i }) - // 更新上一段文本 - if (response.results.channels[0]?.alternatives[0]?.transcript) { - const fullTranscript = response.results.channels[0].alternatives[0].transcript + const fullTranscript = response.results.channels?.[0]?.alternatives?.[0]?.transcript + if (fullTranscript) { previousTranscript = fullTranscript } - return { - audioSegment: segment, - response, - success: true - } as TranscriptSegment + results.push({ audioSegment: segment, response, success: true }) } catch (error) { completed++ onProgress?.({ completed, total: segments.length, current: i }) logger.error('段转写失败', { index: segment.index, error: error instanceof Error ? error.message : String(error) }) - return { - audioSegment: segment, - success: false, - error: error instanceof Error ? error.message : String(error) - } as TranscriptSegment + results.push({ + audioSegment: segment, + success: false, + error: error instanceof Error ? error.message : String(error) + }) } - }) - - if (promise) { - results.push((await promise) as TranscriptSegment) - } } - // 等待所有任务完成 - await this.queue.onIdle()若未来支持并发,建议新增
usePromptChaining选项区分两种模式。
303-311: 为 429(限流)返回标准化错误码,便于 UI/i18n 精准映射- } else if (res.statusCode === 429) { - reject(new Error('API 调用频率超限')) + } else if (res.statusCode === 429) { + const err = new Error('API 调用频率超限') + ;(err as any).code = 'rate_limited' + reject(err)
401-475: 验证请求建议补充req.setTimeout(8000)与Accept: application/json,增强兼容性仅设置
options.timeout在部分 Node 版本不触发 timeout 事件;同时补充 Accept 头更稳妥拿到 JSON。const requestOptions = { hostname: 'api.deepgram.com', port: 443, path: '/v1/auth/token', method: 'GET', headers: { Authorization: `Token ${apiKey}`, - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + Accept: 'application/json' }, timeout: 8000 // 8秒超时 } @@ - req.on('timeout', () => { + req.setTimeout(8000) + req.on('timeout', () => { req.destroy() logger.error('API Key 验证请求超时') resolve({ valid: false, error: '验证请求超时,请稍后重试' }) })
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (10)
packages/shared/types/asr.ts(1 hunks)src/main/services/ASRSubtitleService.ts(1 hunks)src/main/services/__tests__/AudioPreprocessor.test.ts(1 hunks)src/main/services/asr/DeepgramTranscriber.ts(1 hunks)src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts(1 hunks)src/main/services/audio/AudioPreprocessor.ts(1 hunks)src/preload/index.ts(2 hunks)src/renderer/src/pages/player/components/ASRProgressModal.tsx(1 hunks)src/renderer/src/pages/settings/ASRSettings.tsx(1 hunks)src/renderer/src/pages/settings/index.tsx(2 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-)、var(--ant-box-shadow-))
避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Zustand 必须在组件/Hook 顶层通过 selector 使用(useStore(selector)),禁止在 useMemo/useEffect 等内部调用 store Hook,避免 Hooks 顺序问题
避免使用返回对象的 Zustand selector(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器避免不必要重渲染与更新循环
React 副作用与状态更新规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定(默认 TS + React 18)
Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Files:
src/renderer/src/pages/settings/index.tsxsrc/main/services/ASRSubtitleService.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/main/services/asr/DeepgramTranscriber.tssrc/preload/index.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/audio/AudioPreprocessor.tssrc/main/services/__tests__/AudioPreprocessor.test.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
所有图标统一使用 lucide-react,不使用 emoji
Files:
src/renderer/src/pages/settings/index.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/renderer/src/pages/settings/ASRSettings.tsx
**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
布局优先使用 flex,除必要场景外避免将 grid 作为默认方案
Files:
src/renderer/src/pages/settings/index.tsxsrc/main/services/ASRSubtitleService.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/main/services/asr/DeepgramTranscriber.tssrc/preload/index.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/audio/AudioPreprocessor.tssrc/main/services/__tests__/AudioPreprocessor.test.ts
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志而不是 console
logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
任何组件或页面都不得写入 currentTime(播放器控制权由编排器统一管理)
Files:
src/renderer/src/pages/settings/index.tsxsrc/main/services/ASRSubtitleService.tssrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.tspackages/shared/types/asr.tssrc/main/services/asr/DeepgramTranscriber.tssrc/preload/index.tssrc/renderer/src/pages/settings/ASRSettings.tsxsrc/main/services/audio/AudioPreprocessor.tssrc/main/services/__tests__/AudioPreprocessor.test.ts
🧠 Learnings (7)
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
Applied to files:
src/renderer/src/pages/settings/index.tsxsrc/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/renderer/src/pages/settings/ASRSettings.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-*)、var(--ant-box-shadow-*))
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/DictionaryPopover.{tsx,jsx} : DictionaryPopover 组件需使用 Ant Design CSS 变量替代硬编码颜色,完整适配明暗主题(文字、背景、边框、滚动条、交互状态)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsxsrc/renderer/src/pages/settings/ASRSettings.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
🧬 Code graph analysis (8)
src/renderer/src/pages/settings/index.tsx (1)
src/renderer/src/infrastructure/styles/theme.ts (3)
FONT_SIZES(25-40)SPACING(43-58)FONT_WEIGHTS(11-22)
src/main/services/ASRSubtitleService.ts (3)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (8)
ASRProgress(70-85)ASRGenerateOptions(34-45)ASRResult(90-114)DeepgramResponse(167-194)ASRSubtitleItem(18-29)DeepgramUtterance(151-162)DeepgramWord(135-146)ASRErrorCode(5-13)src/main/services/ConfigManager.ts (1)
configManager(246-246)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (2)
packages/shared/types/asr.ts (1)
ASRProgress(70-85)src/renderer/src/infrastructure/styles/theme.ts (4)
FONT_SIZES(25-40)FONT_WEIGHTS(11-22)SPACING(43-58)ANIMATION_DURATION(91-100)
src/main/services/asr/DeepgramTranscriber.ts (2)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)packages/shared/types/asr.ts (3)
AudioSegment(119-130)TranscriptSegment(199-208)DeepgramResponse(167-194)
src/preload/index.ts (1)
packages/shared/types/asr.ts (3)
ASRGenerateOptions(34-45)ASRResult(90-114)ASRProgress(70-85)
src/renderer/src/pages/settings/ASRSettings.tsx (4)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)src/renderer/src/contexts/theme.context.tsx (1)
useTheme(124-124)src/main/services/ConfigManager.ts (1)
setDeepgramApiKey(225-227)src/renderer/src/pages/settings/index.tsx (8)
SettingContainer(16-31)SettingGroup(97-103)SettingTitle(33-41)SettingDescription(51-55)SettingDivider(57-60)SettingRow(62-68)SettingRowTitle(70-77)HelpText(104-109)
src/main/services/audio/AudioPreprocessor.ts (1)
src/renderer/src/services/Logger.ts (2)
loggerService(817-817)error(422-424)
src/main/services/__tests__/AudioPreprocessor.test.ts (1)
src/main/services/audio/AudioPreprocessor.ts (1)
parseFFmpegDuration(40-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
- GitHub Check: test (windows-latest, 20)
🔇 Additional comments (5)
src/renderer/src/pages/settings/index.tsx (2)
14-14: 引入必要的主题常量,实现正确导入语句正确引入了 HelpText 组件所需的主题 token,符合项目规范要求。
104-109: 实现正确,已修复之前的硬编码问题HelpText 组件的实现完全符合项目编码规范:
- ✓ 使用
FONT_SIZES.XS替代硬编码的12px- ✓ 使用
SPACING.XXS替代硬编码的4px- ✓ 使用
FONT_WEIGHTS.REGULAR作为字重值- ✓ 颜色使用 CSS 变量
var(--color-text-3)正确响应了之前的审查意见,现在所有尺寸都使用了集中定义的主题 token。
As per coding guidelines
src/preload/index.ts (1)
272-285: Now I need to search the actual codebase for the asrSubtitleService implementation:Based on the provided script output, I can now analyze the IPC handler implementation. The handler at
src/main/ipc.ts:740-742shows:ipcMain.handle(IpcChannel.ASR_ValidateApiKey, async (_, apiKey: string) => { logger.info('验证 Deepgram API Key') return await asrSubtitleService.validateApiKey(apiKey) })The handler returns the result directly from
asrSubtitleService.validateApiKey(apiKey)without any transformation. According to the preload signature insrc/preload/index.ts, the expected return type isPromise<boolean>.根据现有信息和代码检查结果,预加载层的 IPC 方法声明与主进程处理程序的返回值契约一致。IPC 处理程序(src/main/ipc.ts:740-742)直接返回
asrSubtitleService.validateApiKey(apiKey)的结果,没有任何中间变换。预加载已正确声明Promise<boolean>类型签名。若主进程
asrSubtitleService.validateApiKey()方法实现返回布尔值(而非对象如{ valid, error }),则签名契约匹配无误。确认 validateApiKey 签名与实现一致
IPC 处理程序直接透传
asrSubtitleService.validateApiKey(apiKey)结果。预加载已声明Promise<boolean>类型。若服务方法返回布尔值,则契约正确;若返回结构体(如{ valid: boolean }),则需要在处理程序中提取布尔值字段。请验证主进程服务实现是否与签名匹配。src/renderer/src/pages/settings/ASRSettings.tsx (1)
88-105: 基于脚本输出,需要进一步检查asrSubtitleService.validateApiKey方法的实现以确认返回类型。需要在本地代码库中查找
asrSubtitleService.validateApiKey的具体实现。我已经从脚本输出中看到 IPC handler 的位置,现在需要深入检查该方法的返回类型。确认 validateApiKey IPC 返回类型为 boolean,否则渲染逻辑将误判
根据脚本输出验证,IPC handler(
src/main/ipc.ts行 740-742)直接返回asrSubtitleService.validateApiKey(apiKey)的结果,未进行类型转换。渲染层代码(src/renderer/src/pages/settings/ASRSettings.tsx行 88-105)直接使用if (isValid)判断,这意味着:
- 若服务方法返回
boolean类型(true/false),逻辑正确- 若服务方法返回对象(如
{ valid: boolean, error?: string }),该对象会被视为 truthy,导致if (isValid)始终为true,即使验证失败也会显示成功消息需要查阅代码库确认
asrSubtitleService.validateApiKey的实际返回类型并确保与 IPC handler 和渲染层逻辑一致。建议验证服务层实现,并在必要时在 IPC handler 中添加显式的布尔转换(如!!result.valid),或统一将服务返回值标准化为boolean。src/main/services/asr/DeepgramTranscriber.ts (1)
235-243: 现在需要检查代码实现以确认 options.prompt 的用途和正确的映射方式:现在让我查看代码实现,以确认 options.prompt 的实际使用情况:
根据 Deepgram 官方文档的查询结果,我已确认以下关键事实:
核心发现:
Keyterm Prompting 仅适用于使用 Nova-3 模型的单语转录,而Keywords 仅适用于 Nova-2、Nova-1、Enhanced 和 Base 模型,Nova-3 应使用 Keyterm Prompting。
关键的是,Deepgram /v1/listen API 不支持名为 "prompt" 的参数。
要启用 Keyterm Prompting,需在查询字符串中添加 keyterm 参数,而要启用 Keywords,需在查询字符串中添加 keywords 参数并设置所选关键字和强度值。
评论评估结果:
原始评审建议"将
prompt透传给 Deepgram"基于对 API 的错误理解。Deepgram 的官方 API 和 SDK 都不支持 "prompt" 参数。如果代码目的是实现关键词提示功能,应根据所用模型使用keyterm(Nova-3)或keywords(其他模型)参数,而非prompt。
关键词提示机制不适用于文本 Prompt,仅支持术语/关键字列表
重要区别:Deepgram 的 keyterm 和 keywords 功能减少了对模型重新训练或定制的需求,使企业能够立即改进,无需等待资源密集型流程完成,但这些是术语增强机制,而非通用的提示文本传递。Keywords 应使用单个单词(无空格或标点),多词短语应改用 keyterm。
建议: 此评审需要与代码实现一起核实,以确认
options.prompt的实际用途。如果目的是增强特定术语识别,应映射到相应的keyterm或keywords参数;如果prompt是客户端内部参数(与 Deepgram API 无关),则原评审本身就是不正确的。Likely an incorrect or invalid review comment.
| export interface ASRGenerateOptions { | ||
| /** 视频文件路径 */ | ||
| videoPath: string | ||
| /** 视频 ID(用于保存字幕记录) */ | ||
| videoId: number | ||
| /** 目标语言(ISO 639-1 代码,如 'en', 'zh', 'ja',或 'auto' 进行自动语言检测) */ | ||
| language?: string | 'auto' | ||
| /** Deepgram 模型选择 */ | ||
| model?: 'nova-2' | 'nova-3' | ||
| /** 输出格式 */ | ||
| outputFormat?: 'srt' | 'vtt' | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
language?: string | 'auto' 等同于 string,类型约束没有实际收紧
若希望在类型层面强调 'auto',建议引入受控字面量集合或品牌类型;否则可以仅留注释避免造成误导。
可选修正示例(不必枚举全部语言,先引入有限集合并退回到宽松字符串):
+export type ASRLanguage =
+ | 'auto'
+ | 'en'
+ | 'zh'
+ | 'ja'
+ | (string & {}) // 允许其它合法代码
export interface ASRGenerateOptions {
@@
- language?: string | 'auto'
+ language?: ASRLanguage
}🤖 Prompt for AI Agents
In packages/shared/types/asr.ts around lines 34 to 45, the declared type
language?: string | 'auto' is redundant because 'auto' is already included by
string; fix by either (A) making the field a plain optional string (remove the
literal union) or (B) introduce a controlled union type that lists the allowed
language literals including 'auto' and also a permissive fallback (e.g., a union
of specific ISO codes plus 'auto' and a fallback string type), then use that
alias here; update the interface to reference the new alias and adjust any
callers/types accordingly.
| describe('buildFFmpegArgs', () => { | ||
| // 由于 buildFFmpegArgs 是私有方法,我们通过测试 extractAudioTrack 的行为来间接测试它 | ||
| // 这里我们主要关注格式参数是否正确传递和处理 | ||
|
|
||
| it('should handle MP3 format correctly', async () => { | ||
| // 由于这是一个集成测试,需要真实的文件和 FFmpeg,我们主要测试逻辑是否正确 | ||
| // 创建一个临时目录用于测试 | ||
| const tempDir = audioPreprocessor.createTempDir('test-') | ||
|
|
||
| try { | ||
| // 验证 options 参数能够正确传递 format | ||
| // 由于 buildFFmpegArgs 是私有的,我们通过反射来测试它 | ||
| const buildFFmpegArgsMethod = (audioPreprocessor as any).buildFFmpegArgs | ||
|
|
||
| if (buildFFmpegArgsMethod) { | ||
| // 测试 MP3 格式 | ||
| const mp3Args = buildFFmpegArgsMethod.call( | ||
| audioPreprocessor, | ||
| '/test/input.mp4', | ||
| '/test/output.mp3', | ||
| 16000, | ||
| 1, | ||
| 'mp3' | ||
| ) | ||
|
|
||
| // 验证 MP3 格式使用了正确的编解码器 | ||
| expect(mp3Args).toContain('libmp3lame') | ||
| expect(mp3Args).toContain('-b:a') | ||
| expect(mp3Args).toContain('128k') | ||
| expect(mp3Args).not.toContain('pcm_s16le') | ||
| } | ||
| } finally { | ||
| // 清理临时目录 | ||
| await audioPreprocessor.cleanupTempDir(tempDir) | ||
| } | ||
| }) |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
测试通过反射调用私有方法,脆弱且与实现耦合
建议改为黑盒测试:对外部 API 行为断言(如构建参数可通过 stub runFFmpeg/spy buildFFmpegArgs 间接验证),降低重构成本。
As per coding guidelines
Also applies to: 110-135, 137-160
| vi.mock('../../LoggerService', () => ({ | ||
| loggerService: { | ||
| withContext: () => mockLogger | ||
| } | ||
| })) |
There was a problem hiding this comment.
mock 路径与被测模块的导入符号需保持一致,避免偶发未命中 mock
被测模块使用 import { loggerService } from '../LoggerService',而此处为 ../../LoggerService。在某些打包器/路径别名场景下可能导致未命中同一模块实例。建议与被测文件保持相同的导入符号或使用路径别名统一之(例如 @main/services/LoggerService)。
🤖 Prompt for AI Agents
In
src/main/services/asr/__tests__/DeepgramTranscriber.makeValidationRequest.test.ts
around lines 13 to 17, the vi.mock path uses '../../LoggerService' which differs
from the tested module's import ('../LoggerService') and can cause the mock to
miss in some bundler/alias scenarios; change the mock call to use the exact same
import path/symbol as the module under test (e.g., mock '../LoggerService'
instead of '../../LoggerService'), or switch both the test and the
implementation to a consistent path alias (such as @main/services/LoggerService)
so the mocked module instance is identical to the one imported by the code under
test.
| private abortController: AbortController = new AbortController() | ||
| private currentRequestAbortController: AbortController | null = null | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
未使用的字段:currentRequestAbortController 从未赋非空值,建议删除或落地使用
避免误导与死代码累积。如需支持 per-request abort,应在发起请求处创建并传递 signal;否则删除该字段及相关逻辑。
🤖 Prompt for AI Agents
In src/main/services/asr/DeepgramTranscriber.ts around lines 47 to 49, the field
currentRequestAbortController is declared but never assigned a non-null value,
which is dead/misleading code; either remove the field and any references to it,
or implement per-request abort behavior by creating a new AbortController at
each request (assign it to currentRequestAbortController), pass its signal to
the Deepgram request, and ensure you clear/reset currentRequestAbortController
after the request completes or on abort; update types and null checks
accordingly.
| // 获取配置 | ||
| const language = options.language || configManager.getASRDefaultLanguage() | ||
| const model = (options.model || configManager.getASRModel()) as 'nova-2' | 'nova-3' | ||
| const outputFormat = options.outputFormat || 'srt' | ||
|
|
||
| // 阶段 1: 初始化 |
There was a problem hiding this comment.
未正确处理语言 'auto',Deepgram 需 detect_language 而非传 'auto'
当前直接将 'auto' 作为 language 传入,可能被 Deepgram 拒绝。应在语言自动检测时设置 detectLanguage/移除 language。
- const language = options.language || configManager.getASRDefaultLanguage()
+ const languageConfig = (options.language || configManager.getASRDefaultLanguage()) as
+ | string
+ | 'auto'
+ const detectLanguage = languageConfig === 'auto'
+ const language = detectLanguage ? undefined : (languageConfig as string)
const model = (options.model || configManager.getASRModel()) as 'nova-2' | 'nova-3'
const outputFormat = options.outputFormat || 'srt'
@@
- const deepgramResponse = await transcriber.transcribeFile(extractResult.audioPath, {
- apiKey,
- model,
- language,
- smartFormat: true,
- utterances: true,
- utteranceEndMs: 1000
- })
+ const deepgramResponse = await transcriber.transcribeFile(extractResult.audioPath, {
+ apiKey,
+ model,
+ ...(language ? { language } : {}),
+ ...(detectLanguage ? { detectLanguage: true } : {}),
+ smartFormat: true,
+ utterances: true,
+ utteranceEndMs: 1000
+ })As per coding guidelines
Also applies to: 131-138
🤖 Prompt for AI Agents
In src/main/services/ASRSubtitleService.ts around lines 96-101 (and similarly
131-138), the code passes the literal 'auto' to Deepgram as language which can
be rejected; detect_language must be used or the language field omitted when
auto-detection is desired. Update the logic so that when options.language ===
'auto' (or resolved config yields 'auto') you do not set the language parameter
and instead set detect_language: true (or remove the language key entirely
depending on Deepgram client API), otherwise set language to the explicit value;
apply the same change to the analogous block at lines 131-138 and ensure typing
reflects that language may be undefined when detect_language is used.
| logger.info('智能分段完成', { | ||
| totalWords: words.length, | ||
| sentenceCount: sentences.length, | ||
| avgWordsPerSentence: (words.length / sentences.length).toFixed(1) | ||
| }) |
There was a problem hiding this comment.
日志均值计算缺少 0 保护
当 sentences 为空时可能除以 0(极端降级路径)。
- avgWordsPerSentence: (words.length / sentences.length).toFixed(1)
+ avgWordsPerSentence: sentences.length
+ ? (words.length / sentences.length).toFixed(1)
+ : '0.0'As per coding guidelines
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.info('智能分段完成', { | |
| totalWords: words.length, | |
| sentenceCount: sentences.length, | |
| avgWordsPerSentence: (words.length / sentences.length).toFixed(1) | |
| }) | |
| logger.info('智能分段完成', { | |
| totalWords: words.length, | |
| sentenceCount: sentences.length, | |
| avgWordsPerSentence: sentences.length | |
| ? (words.length / sentences.length).toFixed(1) | |
| : '0.0' | |
| }) |
🤖 Prompt for AI Agents
In src/main/services/ASRSubtitleService.ts around lines 388 to 392, the
avgWordsPerSentence calculation can divide by zero when sentences.length is 0;
change the computation to guard against zero (e.g., use a conditional or
Math.max to avoid division by 0), produce a safe numeric default (like 0) or a
clearly formatted fallback, and only call toFixed on a finite number to avoid
NaN or exceptions; update the logged property to use this guarded value.
| public async cancelTask(taskId: string): Promise<boolean> { | ||
| const task = this.activeTasks.get(taskId) | ||
| if (!task) { | ||
| return false | ||
| } | ||
|
|
||
| logger.info('取消 ASR 任务', { taskId }) | ||
| task.cancelled = true | ||
| await task.transcriber.cancelAll() | ||
| this.activeTasks.delete(taskId) | ||
|
|
||
| return true | ||
| } |
There was a problem hiding this comment.
取消逻辑存在竞态:过早删除 activeTasks 导致后续检查失效
cancelTask 立即从 Map 删除,generateSubtitle 中的取消检查将读不到标记,依赖 transcriber 抛错,存在漏网风险。
public async cancelTask(taskId: string): Promise<boolean> {
const task = this.activeTasks.get(taskId)
if (!task) {
return false
}
logger.info('取消 ASR 任务', { taskId })
task.cancelled = true
await task.transcriber.cancelAll()
- this.activeTasks.delete(taskId)
return true
}说明:由 generateSubtitle 的成功/失败分支统一 delete,确保各阶段的取消检查可读取到 cancelled 标记。
As per coding guidelines
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In src/main/services/ASRSubtitleService.ts around lines 473-485, cancelTask
currently sets task.cancelled and immediately deletes the entry from
this.activeTasks, which races with generateSubtitle's cancellation checks;
change cancelTask to only mark cancelled and call task.transcriber.cancelAll()
(do NOT delete from this.activeTasks), and move the
this.activeTasks.delete(taskId) into generateSubtitle so the task is removed
only in its unified success/failure/cleanup paths where cancelled flag is still
readable; ensure both success and error/finally branches in generateSubtitle
perform the delete so cancel checks can observe task.cancelled.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (3)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (3)
37-50: 将 transition: all 缩小为具体属性以提升性能避免使用
transition: all,仅对实际变化的属性(border-color、color)添加过渡效果。- transition: all ${ANIMATION_DURATION.SLOW} ease-in-out; + transition: + border-color ${ANIMATION_DURATION.SLOW} ease-in-out, + color ${ANIMATION_DURATION.SLOW} ease-in-out;根据编码规范
56-73: 补充 Failed 阶段的文案处理switch 语句缺少
ASRProgressStage.Failed的 case,导致失败时标题为空。const getStageText = () => { switch (progress.stage) { case ASRProgressStage.Initializing: return t('player.asr.progress.stages.initializing') case ASRProgressStage.ExtractingAudio: return t('player.asr.progress.stages.extracting') case ASRProgressStage.Transcribing: return t('player.asr.progress.stages.transcribing') case ASRProgressStage.Formatting: return t('player.asr.progress.stages.formatting') case ASRProgressStage.Saving: return t('player.asr.progress.stages.saving') case ASRProgressStage.Complete: return t('player.asr.progress.stages.complete') + case ASRProgressStage.Failed: + return t('player.asr.progress.stages.failed') default: return '' } }根据编码规范
125-134: 完成状态应允许关闭弹窗当进度完成时,按钮被禁用且仍显示"取消"文案,用户无法关闭弹窗。建议完成时切换为"关闭"按钮并允许操作。
- <CancelButton - $confirmMode={confirmMode} - onClick={handleCancel} - onMouseLeave={handleCancelMouseLeave} - disabled={progress.stage === ASRProgressStage.Complete} - > - {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} - </CancelButton> + {progress.stage === ASRProgressStage.Complete ? ( + <Button onClick={onCancel}>{t('common.close')}</Button> + ) : ( + <CancelButton + $confirmMode={confirmMode} + onClick={handleCancel} + onMouseLeave={handleCancelMouseLeave} + > + {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} + </CancelButton> + )}根据编码规范
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (1)
src/renderer/src/pages/player/components/ASRProgressModal.tsx(1 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-)、var(--ant-box-shadow-))
避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Zustand 必须在组件/Hook 顶层通过 selector 使用(useStore(selector)),禁止在 useMemo/useEffect 等内部调用 store Hook,避免 Hooks 顺序问题
避免使用返回对象的 Zustand selector(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器避免不必要重渲染与更新循环
React 副作用与状态更新规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定(默认 TS + React 18)
Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
所有图标统一使用 lucide-react,不使用 emoji
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
布局优先使用 flex,除必要场景外避免将 grid 作为默认方案
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志而不是 console
logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
任何组件或页面都不得写入 currentTime(播放器控制权由编排器统一管理)
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
🧠 Learnings (7)
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-*)、var(--ant-box-shadow-*))
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/DictionaryPopover.{tsx,jsx} : DictionaryPopover 组件需使用 Ant Design CSS 变量替代硬编码颜色,完整适配明暗主题(文字、背景、边框、滚动条、交互状态)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
🧬 Code graph analysis (1)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (2)
packages/shared/types/asr.ts (1)
ASRProgress(70-85)src/renderer/src/infrastructure/styles/theme.ts (4)
FONT_SIZES(25-40)FONT_WEIGHTS(11-22)SPACING(43-58)ANIMATION_DURATION(91-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (windows-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
- refine button transition to target specific properties for better performance - add missing Failed stage text for ASRProgressStage.Failed - replace hardcoded gap value with theme constant SPACING.LG
…Popover helper - add waitForPopover helper function to handle async popover rendering - add data-testid attributes for better test targeting - increase timeout to 3000ms for more stable async tests - fix flaky tests by ensuring popover content is fully loaded before assertions
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (3)
src/renderer/src/i18n/locales/en-us.json (1)
16-24: 缺少 HTTP 429 限速错误的专用 i18n 密钥当前错误键中缺少对 Deepgram API 限速(429 Too Many Requests)的专用支持。虽然
apiQuotaExceeded存在,但它应指配额耗尽(402),而非限速。根据前次审查意见和 Deepgram 文档,超出限速时返回 429 Too Many Requests 错误,应添加专用密钥以向用户显示清晰的限速消息。请添加专用限速错误密钥:
"errors": { "apiQuotaExceeded": "API quota exceeded, please check your Deepgram account", "audioExtractionFailed": "Audio extraction failed, please check the video file", "invalidApiKey": "Invalid API Key, please check settings", "networkError": "Network error, please check your connection and retry", "noApiKey": "Please configure Deepgram API Key in settings first", + "rateLimited": "Too many requests, please try again later", "transcriptionFailed": "Transcription failed, please retry", "unknown": "Generation failed: {{message}}" },并确保代码中对 HTTP 429 错误的处理映射到此新密钥。
src/renderer/src/i18n/locales/zh-cn.json (1)
48-56: 请补充"频率限制"错误码的专门文案前次审查建议为 HTTP 429(API 调用频率超限)场景新增
rateLimited文案键,但当前代码仅有apiQuotaExceeded(配额已用尽)。这两种错误属于不同场景:
- 429 超限:临时限流,稍后重试可恢复
- 配额用尽:账户额度问题,需检查账户
建议新增
rateLimited键以提供精确的用户提示。"errors": { "apiQuotaExceeded": "API 配额已用尽,请检查您的 Deepgram 账户", "audioExtractionFailed": "音频提取失败,请检查视频文件", "invalidApiKey": "API Key 无效,请检查设置", + "rateLimited": "请求过于频繁,请稍后重试", "networkError": "网络错误,请检查连接后重试", "noApiKey": "请先在设置中配置 Deepgram API Key", "transcriptionFailed": "转写失败,请重试", "unknown": "生成失败:{{message}}" }src/renderer/src/pages/player/components/ASRProgressModal.tsx (1)
129-138: 完成阶段应显示"关闭"按钮而非禁用"取消"当进度完成时,按钮被禁用且仍显示"取消"文案,用户无法关闭 Modal。应切换为可用的"关闭"按钮以改善用户体验。
应用此改动:
<Flex justify="flex-end"> - <CancelButton - $confirmMode={confirmMode} - onClick={handleCancel} - onMouseLeave={handleCancelMouseLeave} - disabled={progress.stage === ASRProgressStage.Complete} - > - {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} - </CancelButton> + {progress.stage === ASRProgressStage.Complete ? ( + <Button onClick={onCancel}>{t('common.close')}</Button> + ) : ( + <CancelButton + $confirmMode={confirmMode} + onClick={handleCancel} + onMouseLeave={handleCancelMouseLeave} + > + {confirmMode ? t('player.asr.progress.confirmCancel') : t('player.asr.progress.cancel')} + </CancelButton> + )} </Flex>
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
📒 Files selected for processing (4)
src/renderer/src/i18n/locales/en-us.json(6 hunks)src/renderer/src/i18n/locales/zh-cn.json(5 hunks)src/renderer/src/pages/player/components/ASRProgressModal.tsx(1 hunks)tests/SubtitleDictionaryLookup.test.tsx(6 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx}: 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-)、var(--ant-box-shadow-))
避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Zustand 必须在组件/Hook 顶层通过 selector 使用(useStore(selector)),禁止在 useMemo/useEffect 等内部调用 store Hook,避免 Hooks 顺序问题
避免使用返回对象的 Zustand selector(如 useStore(s => ({ a: s.a, b: s.b }))),应使用单字段选择器或配合 shallow 比较器避免不必要重渲染与更新循环
React 副作用与状态更新规范:渲染纯函数、Effect 三分法、幂等更新、稳定引用、严格清理、禁止写回自身依赖、Provider 值 memo、外部状态 selector 稳定(默认 TS + React 18)
Player 相关实现需遵循:Zustand selector 顶层调用,禁止在 useMemo/useEffect 中调用 store Hook;为 useSubtitleEngine 增加 subtitles 入参防御等(已在 SubtitleOverlay、SubtitleListPanel、usePlayerControls、useVideoEvents、VideoSurface、useSubtitleSync 落地)
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsxtests/SubtitleDictionaryLookup.test.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
所有图标统一使用 lucide-react,不使用 emoji
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsxtests/SubtitleDictionaryLookup.test.tsx
**/*.{ts,tsx,scss,css}
📄 CodeRabbit inference engine (CLAUDE.md)
布局优先使用 flex,除必要场景外避免将 grid 作为默认方案
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsxtests/SubtitleDictionaryLookup.test.tsx
**/*.{ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.{ts,tsx,js,jsx}: 统一使用 loggerService 记录日志而不是 console
logger 使用约定:如 logger.error('msg', { error }),第二个参数必须是对象字面量({})承载上下文
任何组件或页面都不得写入 currentTime(播放器控制权由编排器统一管理)
Files:
src/renderer/src/pages/player/components/ASRProgressModal.tsxtests/SubtitleDictionaryLookup.test.tsx
🧠 Learnings (7)
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 CSS 变量而不是硬编码样式值;主题相关属性在 styled-components 中使用 Ant Design CSS 变量(如 var(--ant-color-*)、var(--ant-box-shadow-*))
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 在 styled-components 中采用“主题相关用 CSS 变量、设计系统常量用 JS 变量”的混合模式(如颜色/阴影用 CSS var,间距/圆角/字号/动效/ZIndex 用 JS 常量)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/DictionaryPopover.{tsx,jsx} : DictionaryPopover 组件需使用 Ant Design CSS 变量替代硬编码颜色,完整适配明暗主题(文字、背景、边框、滚动条、交互状态)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 避免硬编码尺寸与时长,优先使用 useTheme() 的 token 或集中定义的 JS 样式变量(如 motionDurationMid、borderRadiusSM/MD 等)
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 定制 antd 组件样式时优先使用 styled-components(styled(Component) 包装)而非全局 SCSS/classNames,避免样式污染并保持架构一致性
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 项目样式应使用 styled-components,v1 的 style 构建机制已废弃,不再引入或编写相关实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
📚 Learning: 2025-10-16T15:20:02.493Z
Learnt from: CR
PR: mkdir700/EchoPlayer#0
File: CLAUDE.md:0-0
Timestamp: 2025-10-16T15:20:02.493Z
Learning: Applies to **/*.{ts,tsx} : 优先使用 antd 组件库;若已有可复用的 antd 组件则不要重复自定义实现
Applied to files:
src/renderer/src/pages/player/components/ASRProgressModal.tsx
🧬 Code graph analysis (1)
src/renderer/src/pages/player/components/ASRProgressModal.tsx (2)
packages/shared/types/asr.ts (1)
ASRProgress(70-85)src/renderer/src/infrastructure/styles/theme.ts (4)
FONT_SIZES(25-40)FONT_WEIGHTS(11-22)SPACING(43-58)ANIMATION_DURATION(91-100)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
- GitHub Check: test (macos-latest, 20)
- GitHub Check: test (ubuntu-latest, 20)
- GitHub Check: test (windows-latest, 20)
🔇 Additional comments (12)
src/renderer/src/i18n/locales/en-us.json (5)
43-58: ASR 提示和成功消息结构完整
player.asr节点的prompt和success子部分包含了所有必需的用户交互文案,包括估计时间、语言选择和生成成功反馈。结构与 UI 流程一致。
139-144: AI 字幕选项配置完整且一致
player.subtitleList.empty.options.ai新增项提供了清晰的操作状态区分(action: "Coming Soon"和actionEnabled: "Generate"),支持渐进式功能发布。描述准确传达了 AI 字幕功能的特点。
168-197: 内嵌字幕导入界面文案完善
player.subtitleTrackSelector新增actions和messages结构提供了完整的用户交互覆盖,包括导入结果反馈(成功、失败、多个选区)和验证提示。字幕分类(文本/图像 PGS)的措辞清晰。
238-274: ASR 设置页面密钥完整且用户友好
settings.asr新增分区涵盖所有必需配置:API 密钥验证(包括验证进度状态),语言选择,模型选项(Nova 2/3),以及明确的功能描述。包含了从获取 API 密钥到验证状态的完整用户流程支持。
3-3: common.confirm 新增密钥用途确认新增
"confirm": "Yes"响应前确认对话,用于 ASR 生成取消确认流程(player.asr.progress.cancelConfirm)。命名简洁且符合现有约定。Also applies to: 3-3
src/renderer/src/i18n/locales/zh-cn.json (2)
261-266: 验证"AI 生成字幕"选项的双状态设计该选项同时配置了
action: "即将推出"和actionEnabled: "生成",这表示两种 UI 状态。请确认:
- 这是有意为之的分阶段发布设计(已禁用 → 已启用)
- 前端代码正确地根据 API 密钥有效性切换这两个状态
- 用户交互流程符合预期
385-421: 设置部分 ASR 配置翻译完善且实用新增的
settings.asr部分组织结构清晰,包含:
- API 密钥配置与验证提示
- 默认语言选择(含自动检测)
- 模型选择(Nova 2 标记为"推荐",Nova 3 标记为"最新")
- 8 种语言支持加自动检测
所有译文表达自然,用户引导清晰。
tests/SubtitleDictionaryLookup.test.tsx (3)
70-71: 改进合理,增强了测试的可见性。为 Popover mock 的 title 和 content 添加 testid 是良好的测试实践,便于精确定位和断言弹窗内容。
88-107: 辅助函数设计良好,提升测试可靠性。
waitForPopover采用两阶段等待策略(先等弹窗,再等内容容器),确保异步渲染完成后再进行断言,有效减少测试不稳定性。timeout 参数化设计也增强了灵活性。
193-204: 原始审查意见不正确,testid 已存在于源码中。验证结果表明,
data-testid="dictionary-popover-content"确实存在于src/renderer/src/pages/player/components/DictionaryPopover.tsx(第135行和第148行),而非如原审查意见所假设的那样不存在。测试代码的逻辑是正确的:
- Popover mock 中的
dictionary-popover-content-wrapper是对 Popover 组件 content prop 的包装- 实际传入的 content 来自 DictionaryPopover.tsx,其内部元素已包含
data-testid="dictionary-popover-content"- 因此第197-200行的测试等待完全符合实现
Likely an incorrect or invalid review comment.
src/renderer/src/pages/player/components/ASRProgressModal.tsx (2)
1-17: 导入和接口定义清晰正确导入组织合理,接口类型定义完整,符合 TypeScript 最佳实践。
54-104: 组件逻辑实现正确取消确认流程、自动重置定时器及 Effect 清理都实现得当,符合 React 最佳实践。
| onCancel: () => void | ||
| } | ||
|
|
||
| const Section = styled.div`` |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
移除无用的空样式组件
Section 组件未定义任何样式,建议直接移除,或者如果是为语义化保留请添加注释说明。
应用此改动移除空组件:
-const Section = styled.div``
-
const StageTitle = styled.div`同时更新 JSX 中的使用:
- <Section>
+ <div>
<StageTitle>{getStageText()}</StageTitle>
<Progress percent={progress.percent} status="active" />
- </Section>
+ </div>🤖 Prompt for AI Agents
In src/renderer/src/pages/player/components/ASRProgressModal.tsx around line 19,
the Section styled-component is defined with an empty template (const Section =
styled.div``) and is unused/non-semantic; remove this empty component definition
and replace any JSX usages of <Section>...</Section> with a plain div (or
another appropriate semantic element) or delete the wrapper if unnecessary; if
you want to keep it for semantics, add a brief comment explaining why and add at
least a minimal style or aria attributes.
| const CancelButton = styled(Button)<{ $confirmMode: boolean }>` | ||
| transition: | ||
| color ${ANIMATION_DURATION.SLOW} ease-in-out, | ||
| border-color ${ANIMATION_DURATION.SLOW} ease-in-out; | ||
|
|
||
| ${(props) => | ||
| props.$confirmMode && | ||
| ` | ||
| border-color: var(--ant-color-error) !important; | ||
| color: var(--color-error-text) !important; | ||
|
|
||
| &:hover { | ||
| border-color: var(--ant-color-error) !important; | ||
| } | ||
| `} | ||
| ` |
There was a problem hiding this comment.
使用标准的 Ant Design CSS 变量
第 46 行使用了非标准的 CSS 变量 --color-error-text,应使用 Ant Design 的标准变量命名。
As per coding guidelines
应用此改动:
props.$confirmMode &&
`
border-color: var(--ant-color-error) !important;
- color: var(--color-error-text) !important;
+ color: var(--ant-color-error) !important;
&:hover {
border-color: var(--ant-color-error) !important;
}
`}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const CancelButton = styled(Button)<{ $confirmMode: boolean }>` | |
| transition: | |
| color ${ANIMATION_DURATION.SLOW} ease-in-out, | |
| border-color ${ANIMATION_DURATION.SLOW} ease-in-out; | |
| ${(props) => | |
| props.$confirmMode && | |
| ` | |
| border-color: var(--ant-color-error) !important; | |
| color: var(--color-error-text) !important; | |
| &:hover { | |
| border-color: var(--ant-color-error) !important; | |
| } | |
| `} | |
| ` | |
| const CancelButton = styled(Button)<{ $confirmMode: boolean }>` | |
| transition: | |
| color ${ANIMATION_DURATION.SLOW} ease-in-out, | |
| border-color ${ANIMATION_DURATION.SLOW} ease-in-out; | |
| ${(props) => | |
| props.$confirmMode && | |
| ` | |
| border-color: var(--ant-color-error) !important; | |
| color: var(--ant-color-error) !important; | |
| &:hover { | |
| border-color: var(--ant-color-error) !important; | |
| } | |
| `} | |
| ` |
🤖 Prompt for AI Agents
In src/renderer/src/pages/player/components/ASRProgressModal.tsx around lines 37
to 52, the styled CancelButton uses a non-standard CSS variable
`--color-error-text` on line 46; replace that with the standard Ant Design
variable (use `var(--ant-color-error)` to match the existing `border-color`
usage) so the color comes from Ant's design tokens and remains consistent;
ensure the replacement keeps the `!important` and hover rules intact.
# [1.3.0](v1.2.0...v1.3.0) (2025-10-19) ### Bug Fixes * Fix system theme toggle in Sidebar ([#224](#224)) ([2d2de6c](2d2de6c)) ### Features * **asr:** implement ASR subtitle generation features ([#223](#223)) ([d87a723](d87a723)) * **player:** enhance video interaction area and progress bar behavior ([20fbcc5](20fbcc5)) * **player:** 优化进度条 seek 交互和性能 ([600734d](600734d))
This update significantly improves the subtitle generation experience by leveraging AI technology for automatic subtitle creation.
Summary by CodeRabbit
closes: #189