-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat: 集成 SenseVoice 字幕引擎,支持多模型选择 #775
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # cc-Recordly-cn — 开发日志 | ||
| > Build in public 系列 | ||
|
|
||
| ## 文章 | ||
| | 日期 | 标题 | 类型 | | ||
| |------|------|------| | ||
|
|
||
| ## 命令 | ||
| ```bash | ||
| journal push # 生成草稿 | ||
| journal status # 查看状态 | ||
| journal sync # 同步到全局 | ||
| journal search # 全局搜索 | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -36,6 +36,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "from": "public/wallpapers", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "to": "assets/wallpapers" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "from": "models", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "to": "assets/models" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "publish": [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -53,15 +57,23 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "entitlements": "build/entitlements.mac.plist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "entitlementsInherit": "build/entitlements.mac.inherit.plist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": "dmg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "arch": ["x64", "arm64"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": "zip", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "arch": ["x64", "arm64"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": "dmg", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "arch": ["x64", "arm64"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": "zip", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "arch": ["x64", "arm64"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "entitlements": "build/entitlements.mac.plist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "entitlementsInherit": "build/entitlements.mac.inherit.plist", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "target": "dir", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "arch": ["arm64"] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
57
to
+76
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Duplicate
🛠️ Proposed fix "mac": {
"hardenedRuntime": true,
"entitlements": "build/entitlements.mac.plist",
"entitlementsInherit": "build/entitlements.mac.inherit.plist",
"target": [
- {
- "target": "dmg",
- "arch": ["x64", "arm64"]
- },
- {
- "target": "zip",
- "arch": ["x64", "arm64"]
- }
- ],
- "entitlements": "build/entitlements.mac.plist",
- "entitlementsInherit": "build/entitlements.mac.inherit.plist",
- "target": [
- {
- "target": "dir",
- "arch": ["arm64"]
- }
- ],
+ { "target": "dmg", "arch": ["x64", "arm64"] },
+ { "target": "zip", "arch": ["x64", "arm64"] },
+ { "target": "dir", "arch": ["arm64"] }
+ ],📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "icon": "icons/icons/mac/icon.icns", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "artifactName": "${productName}-${arch}.${ext}", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "extendInfo": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { CaptionModel } from "./models"; | ||
|
|
||
| /** | ||
| * Result from caption generation. | ||
| */ | ||
| export interface CaptionWordPayload { | ||
| text: string; | ||
| startMs: number; | ||
| endMs: number; | ||
| } | ||
|
|
||
| export interface CaptionCuePayload { | ||
| id: string; | ||
| text: string; | ||
| startMs: number; | ||
| endMs: number; | ||
| words?: CaptionWordPayload[]; | ||
| } | ||
|
|
||
| export interface GenerateCaptionResult { | ||
| success: boolean; | ||
| cues: CaptionCuePayload[]; | ||
| message?: string; | ||
| error?: string; | ||
| } | ||
|
|
||
| /** | ||
| * Options passed to an engine for caption generation. | ||
| */ | ||
| export interface GenerateCaptionOptions { | ||
| /** Path to the pre-extracted 16kHz mono WAV file */ | ||
| audioPath: string; | ||
| /** Absolute path to the downloaded model file */ | ||
| modelPath: string; | ||
| /** The model metadata */ | ||
| model: CaptionModel; | ||
| /** Language hint (BCP-47 code or "auto") */ | ||
| language?: string; | ||
| /** Temp directory for intermediate files */ | ||
| tempDir: string; | ||
| } | ||
|
|
||
| /** | ||
| * Abstract engine interface. | ||
| * Each engine (whisper, sensevoice) implements this. | ||
| */ | ||
| export interface CaptionEngine { | ||
| readonly engineType: string; | ||
|
|
||
| /** | ||
| * Run caption generation. | ||
| * Receives fully resolved paths — no model resolution needed. | ||
| */ | ||
| generate(options: GenerateCaptionOptions): Promise<GenerateCaptionResult>; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -96,6 +96,13 @@ export async function resolveCaptionAudioCandidates(videoPath: string) { | |
| candidates.push({ path: normalizedCandidatePath, label }); | ||
| }; | ||
|
|
||
| // Companion audio (mic/system) should be tried FIRST - they have the best audio quality | ||
| const videoDir = path.dirname(videoPath); | ||
| const videoExt = path.extname(videoPath); | ||
| const videoBase = path.basename(videoPath, videoExt); | ||
| pushCandidate(path.join(videoDir, `${videoBase}.mic.wav`), "microphone"); | ||
| pushCandidate(path.join(videoDir, `${videoBase}.system.wav`), "system audio"); | ||
|
|
||
| pushCandidate(videoPath, "recording"); | ||
|
|
||
| const requestedRecordingSession = await resolveRecordingSession(videoPath); | ||
|
|
@@ -190,8 +197,47 @@ export async function generateAutoCaptionsFromVideo(options: { | |
| videoPath: string; | ||
| whisperExecutablePath?: string; | ||
| whisperModelPath: string; | ||
| modelId?: string; | ||
| language?: string; | ||
| }) { | ||
| // ── Engine dispatch: SenseVoice ────────────────────────────────────── | ||
| if (options.modelId) { | ||
| const { getModelById } = await import("./models"); | ||
| const model = getModelById(options.modelId); | ||
| if (model?.engine === "sensevoice") { | ||
| const { SenseVoiceEngine } = await import("./sensevoice"); | ||
| const engine = new SenseVoiceEngine(); | ||
| const ffmpegPath = getFfmpegBinaryPath(); | ||
| const tempDir = app.getPath("temp"); | ||
| const wavPath = path.join(tempDir, `sensevoice-${Date.now()}.wav`); | ||
|
|
||
| // Use the same audio extraction as whisper path | ||
| const audioSource = await extractCaptionAudioSource({ | ||
| videoPath: options.videoPath, | ||
| ffmpegPath, | ||
| wavPath, | ||
| }); | ||
|
|
||
| const result = await engine.generate({ | ||
| audioPath: wavPath, | ||
| modelPath: options.whisperModelPath, | ||
| model, | ||
| language: options.language, | ||
| tempDir, | ||
| }); | ||
|
|
||
| if (!result.success) { | ||
| await fs.rm(wavPath, { force: true }).catch(() => undefined); | ||
| throw new Error(result.error || "SenseVoice caption generation failed."); | ||
| } | ||
|
|
||
| await fs.rm(wavPath, { force: true }).catch(() => undefined); | ||
|
|
||
| return { success: true, cues: result.cues, audioSourceLabel: audioSource.label }; | ||
|
Comment on lines
+210
to
+236
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win SenseVoice temp WAV can leak on unexpected throw. Cleanup of 🧰 Tools🪛 ast-grep (0.44.1)[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec. (detect-child-process-typescript) 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| // ── Engine: whisper.cpp (default) ──────────────────────────────────── | ||
| const ffmpegPath = getFfmpegBinaryPath(); | ||
| const normalizedVideoPath = normalizeVideoSourcePath(options.videoPath); | ||
| if (!normalizedVideoPath) { | ||
|
|
@@ -292,6 +338,7 @@ export async function generateAutoCaptionsFromVideo(options: { | |
| } | ||
|
|
||
| return { | ||
| success: true, | ||
| cues: cuesToReturn, | ||
| audioSourceLabel: audioSource.label, | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Surround the table with blank lines.
Add a blank line before and after the table to satisfy Markdown linting rule MD058.
Proposed fix
📝 Committable suggestion
🧰 Tools
🪛 markdownlint-cli2 (0.23.0)
[warning] 5-5: Tables should be surrounded by blank lines
(MD058, blanks-around-tables)
🤖 Prompt for AI Agents
Source: Linters/SAST tools