@sushanglewis/lincoln 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.d.ts +3 -0
- package/dist/main.js +1286 -916
- package/dist/main.js.map +1 -1
- package/dist/postinstall.d.ts +3 -0
- package/dist/postinstall.js +18 -0
- package/dist/postinstall.js.map +1 -0
- package/framework/.claude/agents/_contract.md +79 -0
- package/framework/.claude/agents/architect.md +31 -0
- package/framework/.claude/agents/default.md +151 -0
- package/framework/.claude/agents/designer.md +33 -0
- package/framework/.claude/agents/engineer.md +42 -0
- package/framework/.claude/agents/external/NOTICES.md +12 -0
- package/framework/.claude/agents/external/everything-claude-code.manifest.yaml +23 -0
- package/framework/.claude/agents/external/oh-my-claudecode.manifest.yaml +26 -0
- package/framework/.claude/agents/external/wshobson-agents.manifest.yaml +23 -0
- package/framework/.claude/agents/frontend-engineer.md +32 -0
- package/framework/.claude/agents/knowledge-curator.md +28 -0
- package/framework/.claude/agents/pm.md +38 -0
- package/framework/.claude/agents/qa.md +28 -0
- package/framework/.claude/agents/release-coordinator.md +25 -0
- package/framework/.claude/agents/researcher.md +41 -0
- package/framework/.claude/harnesses/claude-code.yaml +15 -0
- package/framework/.claude/harnesses/codex.yaml +40 -0
- package/framework/.claude/harnesses/command-map.yaml +255 -0
- package/framework/.claude/harnesses/opencode.yaml +32 -0
- package/framework/.claude/harnesses/scenarios.yaml +45 -0
- package/framework/.claude/hooks/on-pr-event.sh +69 -0
- package/framework/.claude/hooks/on-session-start.sh +427 -0
- package/framework/.claude/hooks/on-stop.sh +104 -0
- package/framework/.claude/hooks/post-tool-use.sh +161 -0
- package/framework/.claude/hooks/pre-tool-use.sh +246 -0
- package/framework/.claude/schemas/agent-definition.schema.json +35 -0
- package/framework/.claude/schemas/benchmark-report.schema.json +148 -0
- package/framework/.claude/schemas/pm-to-ux-handoff.schema.yaml +128 -0
- package/framework/.claude/schemas/stage-definition.schema.json +233 -0
- package/framework/.claude/schemas/workflow-stage.schema.json +86 -0
- package/framework/.claude/schemas/workflow-template.schema.json +88 -0
- package/framework/.claude/settings.json +14 -0
- package/framework/.claude/skills/build-product-prototype/SKILL.md +35 -0
- package/framework/.claude/skills/build-product-prototype/prompts/main.md +78 -0
- package/framework/.claude/skills/clarify-requirements/SKILL.md +32 -0
- package/framework/.claude/skills/clarify-requirements/prompts/main.md +81 -0
- package/framework/.claude/skills/dependencies.yaml +139 -0
- package/framework/.claude/skills/draft-product-design/SKILL.md +40 -0
- package/framework/.claude/skills/draft-product-design/prompts/main.md +47 -0
- package/framework/.claude/skills/lc-analyze-frameworks/SKILL.md +43 -0
- package/framework/.claude/skills/lc-analyze-frameworks/prompts/main.md +95 -0
- package/framework/.claude/skills/lc-benchmark/SKILL.md +61 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/SKILL.md +38 -0
- package/framework/.claude/skills/lc-build-codebase-knowledge/prompts/build-codebase-knowledge.md +61 -0
- package/framework/.claude/skills/lc-collect-intelligence/SKILL.md +43 -0
- package/framework/.claude/skills/lc-collect-intelligence/prompts/main.md +72 -0
- package/framework/.claude/skills/lc-competitive-analysis/SKILL.md +43 -0
- package/framework/.claude/skills/lc-competitive-analysis/prompts/main.md +70 -0
- package/framework/.claude/skills/lc-explore-opensource/SKILL.md +36 -0
- package/framework/.claude/skills/lc-explore-opensource/prompts/explore-opensource.md +77 -0
- package/framework/.claude/skills/lc-first-principles/SKILL.md +43 -0
- package/framework/.claude/skills/lc-first-principles/prompts/main.md +73 -0
- package/framework/.claude/skills/lc-handoff/SKILL.md +34 -0
- package/framework/.claude/skills/lc-init-branch/SKILL.md +52 -0
- package/framework/.claude/skills/lc-market-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-market-research/prompts/main.md +79 -0
- package/framework/.claude/skills/lc-product-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-product-research/prompts/main.md +80 -0
- package/framework/.claude/skills/lc-research-report/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-report/prompts/main.md +90 -0
- package/framework/.claude/skills/lc-research-scope/SKILL.md +43 -0
- package/framework/.claude/skills/lc-research-scope/prompts/main.md +82 -0
- package/framework/.claude/skills/lc-setup/SKILL.md +68 -0
- package/framework/.claude/skills/lc-setup/prompts/main.md +88 -0
- package/framework/.claude/skills/lc-stage/SKILL.md +45 -0
- package/framework/.claude/skills/lc-stakeholder-research/SKILL.md +43 -0
- package/framework/.claude/skills/lc-stakeholder-research/prompts/main.md +65 -0
- package/framework/.claude/skills/lc-status/SKILL.md +25 -0
- package/framework/.claude/skills/lc-storytelling/SKILL.md +43 -0
- package/framework/.claude/skills/lc-storytelling/prompts/main.md +77 -0
- package/framework/.claude/skills/lc-wf/SKILL.md +54 -0
- package/framework/.claude/skills/lc-workflow/SKILL.md +48 -0
- package/framework/.claude/skills/lc-workflow/validators/validate.py +755 -0
- package/framework/.claude/skills/lc-workflow-router/SKILL.md +41 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/intake-prompt.md +91 -0
- package/framework/.claude/skills/lc-workflow-router/prompts/router-prompt.md +44 -0
- package/framework/.claude/skills/plan-tdd-development/SKILL.md +31 -0
- package/framework/.claude/skills/plan-tdd-development/prompts/main.md +44 -0
- package/framework/.claude/skills/process-interview/SKILL.md +32 -0
- package/framework/.claude/skills/process-interview/prompts/main.md +47 -0
- package/framework/.claude/skills/propose-with-openspec/SKILL.md +37 -0
- package/framework/.claude/skills/propose-with-openspec/prompts/main.md +53 -0
- package/framework/.claude/skills/split-to-github/SKILL.md +32 -0
- package/framework/.claude/skills/split-to-github/prompts/main.md +42 -0
- package/framework/.claude/skills/sync-to-knowledge/SKILL.md +35 -0
- package/framework/.claude/skills/sync-to-knowledge/prompts/main.md +54 -0
- package/framework/.claude/skills/workflow-continue/SKILL.md +24 -0
- package/framework/.claude/skills/workflow-continue/prompts/main.md +25 -0
- package/framework/.claude/stages/build-codebase-knowledge.yaml +42 -0
- package/framework/.claude/stages/clarify.yaml +59 -0
- package/framework/.claude/stages/explore-opensource.yaml +40 -0
- package/framework/.claude/stages/implement.yaml +60 -0
- package/framework/.claude/stages/ingest.yaml +40 -0
- package/framework/.claude/stages/lc-analyze-frameworks.yaml +37 -0
- package/framework/.claude/stages/lc-collect-intelligence.yaml +37 -0
- package/framework/.claude/stages/lc-competitive-analysis.yaml +37 -0
- package/framework/.claude/stages/lc-first-principles.yaml +37 -0
- package/framework/.claude/stages/lc-market-research.yaml +37 -0
- package/framework/.claude/stages/lc-product-research.yaml +37 -0
- package/framework/.claude/stages/lc-research-report.yaml +38 -0
- package/framework/.claude/stages/lc-research-scope.yaml +38 -0
- package/framework/.claude/stages/lc-stakeholder-research.yaml +37 -0
- package/framework/.claude/stages/lc-storytelling.yaml +37 -0
- package/framework/.claude/stages/product-design-docs.yaml +66 -0
- package/framework/.claude/stages/product-prototype.yaml +53 -0
- package/framework/.claude/stages/propose.yaml +45 -0
- package/framework/.claude/stages/split.yaml +41 -0
- package/framework/.claude/stages/sync-knowledge.yaml +49 -0
- package/framework/.claude/stages/tdd-development-plan.yaml +43 -0
- package/framework/.claude/stages/workflow-router.yaml +45 -0
- package/framework/.claude/templates/solo-workflow-context.yaml +23 -0
- package/framework/.claude/workflows/README.md +93 -0
- package/framework/.claude/workflows/bug-fix.yaml +81 -0
- package/framework/.claude/workflows/design-spike.yaml +55 -0
- package/framework/.claude/workflows/existing-project-iteration.yaml +106 -0
- package/framework/.claude/workflows/interview-to-knowledge.yaml +189 -0
- package/framework/.claude/workflows/oss-first-design.yaml +100 -0
- package/framework/.claude/workflows/pm-research.yaml +115 -0
- package/framework/.claude-plugin/hooks.json +6 -0
- package/framework/.claude-plugin/marketplace.json +32 -0
- package/framework/.claude-plugin/plugin.json +57 -0
- package/framework/.version-bump.json +43 -0
- package/framework/CLAUDE.md +140 -0
- package/framework/CONTRIBUTING.md +219 -0
- package/framework/LICENSE +21 -0
- package/framework/README.en.md +139 -0
- package/framework/README.md +139 -0
- package/framework/RELEASE.md +334 -0
- package/framework/SKILL.md +24 -0
- package/framework/USAGE.md +356 -0
- package/framework/manifest.json +1214 -0
- package/framework/requirements.txt +4 -0
- package/framework/scripts/__init__.py +0 -0
- package/framework/scripts/build_codebase_knowledge.py +76 -0
- package/framework/scripts/bump_version.py +181 -0
- package/framework/scripts/check-dependency-drift.py +69 -0
- package/framework/scripts/check-harness-drift.sh +57 -0
- package/framework/scripts/check-main-merge-hygiene.py +128 -0
- package/framework/scripts/check-skill-dependencies.sh +124 -0
- package/framework/scripts/init-lincoln-branch.sh +300 -0
- package/framework/scripts/init-project.sh +184 -0
- package/framework/scripts/lc-benchmark-cli.py +66 -0
- package/framework/scripts/lincoln-audit.py +288 -0
- package/framework/scripts/lincoln-setup.py +651 -0
- package/framework/scripts/lincoln-status.py +448 -0
- package/framework/scripts/lincoln_benchmark.py +224 -0
- package/framework/scripts/lincoln_benchmark_eval.py +104 -0
- package/framework/scripts/lincoln_benchmark_metrics.py +818 -0
- package/framework/scripts/lincoln_benchmark_report.py +141 -0
- package/framework/scripts/lincoln_command_map.py +289 -0
- package/framework/scripts/lincoln_dependency_manager.py +335 -0
- package/framework/scripts/lincoln_documents.py +175 -0
- package/framework/scripts/lincoln_harness_adapter.py +319 -0
- package/framework/scripts/lincoln_index.py +255 -0
- package/framework/scripts/lincoln_paths.py +198 -0
- package/framework/scripts/lincoln_prd.py +227 -0
- package/framework/scripts/lincoln_role.py +46 -0
- package/framework/scripts/lincoln_scenario.py +59 -0
- package/framework/scripts/lincoln_session_start_metrics.py +71 -0
- package/framework/scripts/lincoln_skill_prompt.py +48 -0
- package/framework/scripts/lincoln_trace.py +335 -0
- package/framework/scripts/lincoln_workflow.py +199 -0
- package/framework/scripts/list-active-lincoln-branches.sh +87 -0
- package/framework/scripts/package-lincoln-plugin.py +340 -0
- package/framework/scripts/run-infrastructure-tests.py +84 -0
- package/framework/scripts/stage_loader.py +1071 -0
- package/framework/scripts/static-check.sh +356 -0
- package/framework/scripts/sync-external-agents.sh +210 -0
- package/framework/scripts/sync-framework-package.mjs +218 -0
- package/framework/scripts/task_tool_guard.py +213 -0
- package/framework/scripts/track-artifacts.py +140 -0
- package/framework/scripts/validate_stage.py +350 -0
- package/{README.md → framework/tools/lincoln/README.md} +4 -4
- package/framework/tools/lincoln/package-lock.json +3349 -0
- package/framework/tools/lincoln/package.json +48 -0
- package/framework/tools/lincoln/src/cli.tsx +60 -0
- package/framework/tools/lincoln/src/components/AudioMeter.tsx +34 -0
- package/framework/tools/lincoln/src/components/CancelledScreen.tsx +11 -0
- package/framework/tools/lincoln/src/components/DevicesMenu.tsx +79 -0
- package/framework/tools/lincoln/src/components/HintBar.tsx +25 -0
- package/framework/tools/lincoln/src/components/LogView.tsx +36 -0
- package/framework/tools/lincoln/src/components/ModelMenu.tsx +122 -0
- package/framework/tools/lincoln/src/components/ReadyScreen.tsx +54 -0
- package/framework/tools/lincoln/src/components/RecordingApp.tsx +211 -0
- package/framework/tools/lincoln/src/components/RecordingScreen.tsx +41 -0
- package/framework/tools/lincoln/src/components/SelectMenu.tsx +53 -0
- package/framework/tools/lincoln/src/components/StatusPanel.tsx +52 -0
- package/framework/tools/lincoln/src/components/StopConfirmation.tsx +32 -0
- package/framework/tools/lincoln/src/config/args.ts +35 -0
- package/framework/tools/lincoln/src/config/loadConfig.ts +81 -0
- package/framework/tools/lincoln/src/config/resolveConfig.ts +85 -0
- package/framework/tools/lincoln/src/devices/listDevices.ts +11 -0
- package/framework/tools/lincoln/src/devices/parseDevices.ts +25 -0
- package/framework/tools/lincoln/src/hooks/useKeyHandler.ts +63 -0
- package/framework/tools/lincoln/src/main.tsx +4 -0
- package/framework/tools/lincoln/src/models/models.ts +34 -0
- package/framework/tools/lincoln/src/models/runWarmup.ts +46 -0
- package/framework/tools/lincoln/src/models/warmupProgress.ts +30 -0
- package/framework/tools/lincoln/src/recording/spawnRecorder.ts +127 -0
- package/framework/tools/lincoln/src/recording/useRecorder.ts +146 -0
- package/framework/tools/lincoln/src/utils/amplitudeToMeter.ts +4 -0
- package/framework/tools/lincoln/src/utils/formatDuration.ts +6 -0
- package/framework/tools/lincoln/src/utils/sessionId.ts +25 -0
- package/framework/tools/lincoln/tests/cli.test.tsx +52 -0
- package/framework/tools/lincoln/tests/components/AudioMeter.test.tsx +31 -0
- package/framework/tools/lincoln/tests/components/CancelledScreen.test.tsx +12 -0
- package/framework/tools/lincoln/tests/components/ReadyScreen.test.tsx +68 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.menus.test.tsx +158 -0
- package/framework/tools/lincoln/tests/components/RecordingApp.test.tsx +248 -0
- package/framework/tools/lincoln/tests/components/RecordingScreen.test.tsx +37 -0
- package/framework/tools/lincoln/tests/components/SelectMenu.test.tsx +59 -0
- package/framework/tools/lincoln/tests/components/StopConfirmation.test.tsx +38 -0
- package/framework/tools/lincoln/tests/config/args.test.ts +49 -0
- package/framework/tools/lincoln/tests/config/loadConfig.test.ts +59 -0
- package/framework/tools/lincoln/tests/config/resolveConfig.test.ts +72 -0
- package/framework/tools/lincoln/tests/devices/parseDevices.test.ts +21 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.enabled.test.tsx +44 -0
- package/framework/tools/lincoln/tests/hooks/useKeyHandler.test.tsx +130 -0
- package/framework/tools/lincoln/tests/models/models.test.ts +27 -0
- package/framework/tools/lincoln/tests/models/warmupProgress.test.ts +25 -0
- package/framework/tools/lincoln/tests/recording/spawnRecorder.test.ts +85 -0
- package/framework/tools/lincoln/tests/recording/useRecorder.test.tsx +172 -0
- package/framework/tools/lincoln/tests/utils/amplitudeToMeter.test.ts +22 -0
- package/framework/tools/lincoln/tests/utils/formatDuration.test.ts +11 -0
- package/framework/tools/lincoln/tests/utils/sessionId.test.ts +30 -0
- package/framework/tools/lincoln/tsconfig.json +22 -0
- package/framework/tools/lincoln/tsconfig.tests.json +10 -0
- package/framework/tools/lincoln/tsup.config.ts +17 -0
- package/framework/tools/lincoln/vitest.config.ts +20 -0
- package/framework/tools/lincoln-installer/package-lock.json +3611 -0
- package/framework/tools/lincoln-installer/package.json +53 -0
- package/framework/tools/lincoln-installer/src/cli.tsx +63 -0
- package/framework/tools/lincoln-installer/src/components/HarnessSelectScreen.tsx +43 -0
- package/framework/tools/lincoln-installer/src/components/InstallerApp.tsx +98 -0
- package/framework/tools/lincoln-installer/src/components/OptionsScreen.tsx +56 -0
- package/framework/tools/lincoln-installer/src/components/ProgressScreen.tsx +25 -0
- package/framework/tools/lincoln-installer/src/components/ResultScreen.tsx +40 -0
- package/framework/tools/lincoln-installer/src/components/SelectMenu.tsx +73 -0
- package/framework/tools/lincoln-installer/src/components/SummaryScreen.tsx +49 -0
- package/framework/tools/lincoln-installer/src/components/UpdateApp.tsx +135 -0
- package/framework/tools/lincoln-installer/src/components/WelcomeScreen.tsx +22 -0
- package/framework/tools/lincoln-installer/src/config/args.ts +76 -0
- package/framework/tools/lincoln-installer/src/config/resolveConfig.ts +33 -0
- package/framework/tools/lincoln-installer/src/config/updateArgs.ts +67 -0
- package/framework/tools/lincoln-installer/src/hooks/useSetup.ts +149 -0
- package/framework/tools/lincoln-installer/src/main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/types.ts +19 -0
- package/framework/tools/lincoln-installer/src/update-main.tsx +11 -0
- package/framework/tools/lincoln-installer/src/update.tsx +90 -0
- package/framework/tools/lincoln-installer/src/utils/detectHarness.ts +51 -0
- package/framework/tools/lincoln-installer/src/utils/runSetup.ts +62 -0
- package/framework/tools/lincoln-installer/src/utils/updateRelease.ts +454 -0
- package/framework/tools/lincoln-installer/tests/components/InstallerApp.test.tsx +88 -0
- package/framework/tools/lincoln-installer/tests/components/SelectMenu.test.tsx +26 -0
- package/framework/tools/lincoln-installer/tests/components/UpdateApp.test.tsx +170 -0
- package/framework/tools/lincoln-installer/tests/components/screens.test.tsx +310 -0
- package/framework/tools/lincoln-installer/tests/config/args.test.ts +66 -0
- package/framework/tools/lincoln-installer/tests/config/resolveConfig.test.ts +75 -0
- package/framework/tools/lincoln-installer/tests/config/updateArgs.test.ts +52 -0
- package/framework/tools/lincoln-installer/tests/hooks/useSetup.test.tsx +213 -0
- package/framework/tools/lincoln-installer/tests/update.test.tsx +108 -0
- package/framework/tools/lincoln-installer/tests/utils/detectHarness.test.ts +40 -0
- package/framework/tools/lincoln-installer/tests/utils/runSetup.test.ts +45 -0
- package/framework/tools/lincoln-installer/tests/utils/updateRelease.test.ts +559 -0
- package/framework/tools/lincoln-installer/tsconfig.json +22 -0
- package/framework/tools/lincoln-installer/tsconfig.tests.json +8 -0
- package/framework/tools/lincoln-installer/tsup.config.ts +17 -0
- package/framework/tools/lincoln-installer/vitest.config.ts +20 -0
- package/framework/tools/lincoln-record/Cargo.lock +1896 -0
- package/framework/tools/lincoln-record/Cargo.toml +24 -0
- package/framework/tools/lincoln-record/diarize/diarize.py +117 -0
- package/framework/tools/lincoln-record/diarize/merge.py +55 -0
- package/framework/tools/lincoln-record/src/audio/capture/microphone.rs +194 -0
- package/framework/tools/lincoln-record/src/audio/capture/mod.rs +1 -0
- package/framework/tools/lincoln-record/src/audio/devices.rs +20 -0
- package/framework/tools/lincoln-record/src/audio/mod.rs +5 -0
- package/framework/tools/lincoln-record/src/audio/resampler.rs +44 -0
- package/framework/tools/lincoln-record/src/audio/saver.rs +87 -0
- package/framework/tools/lincoln-record/src/audio/source.rs +104 -0
- package/framework/tools/lincoln-record/src/cli.rs +96 -0
- package/framework/tools/lincoln-record/src/config.rs +135 -0
- package/framework/tools/lincoln-record/src/lib.rs +6 -0
- package/framework/tools/lincoln-record/src/main.rs +242 -0
- package/framework/tools/lincoln-record/src/model/mod.rs +248 -0
- package/framework/tools/lincoln-record/src/output/metadata.rs +30 -0
- package/framework/tools/lincoln-record/src/output/mod.rs +2 -0
- package/framework/tools/lincoln-record/src/output/transcript.rs +42 -0
- package/framework/tools/lincoln-record/src/transcription/diarization.rs +64 -0
- package/framework/tools/lincoln-record/src/transcription/mod.rs +3 -0
- package/framework/tools/lincoln-record/src/transcription/provider.rs +43 -0
- package/framework/tools/lincoln-record/src/transcription/whisper.rs +176 -0
- package/framework/tools/lincoln-record/tests/cli_args.rs +103 -0
- package/framework/tools/lincoln-record/tests/config_test.rs +70 -0
- package/framework/tools/lincoln-record/tests/devices_test.rs +23 -0
- package/framework/tools/lincoln-record/tests/diarization_tests.rs +101 -0
- package/framework/tools/lincoln-record/tests/integration_tests.rs +44 -0
- package/framework/tools/lincoln-record/tests/model_test.rs +68 -0
- package/framework/tools/lincoln-record/tests/transcription_tests.rs +73 -0
- package/package.json +19 -13
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
use crate::config::Config;
|
|
2
|
+
use anyhow::{Context, Result};
|
|
3
|
+
use std::io::{Read, Write};
|
|
4
|
+
use std::path::{Path, PathBuf};
|
|
5
|
+
|
|
6
|
+
const HUGGINGFACE_BASE: &str = "https://huggingface.co/ggerganov/whisper.cpp/resolve/main";
|
|
7
|
+
const HF_MIRROR_BASE: &str = "https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main";
|
|
8
|
+
|
|
9
|
+
/// Known Whisper model names and their published filenames.
|
|
10
|
+
fn model_filename(name: &str) -> Option<&str> {
|
|
11
|
+
match name {
|
|
12
|
+
"tiny" => Some("ggml-tiny.bin"),
|
|
13
|
+
"base" => Some("ggml-base.bin"),
|
|
14
|
+
"small" => Some("ggml-small.bin"),
|
|
15
|
+
"medium" => Some("ggml-medium.bin"),
|
|
16
|
+
"large-v1" => Some("ggml-large-v1.bin"),
|
|
17
|
+
"large-v2" => Some("ggml-large-v2.bin"),
|
|
18
|
+
"large-v3" => Some("ggml-large-v3.bin"),
|
|
19
|
+
"large-v3-turbo" => Some("ggml-large-v3-turbo.bin"),
|
|
20
|
+
_ => None,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fn model_url(name: &str) -> Option<String> {
|
|
25
|
+
model_filename(name).map(|file| format!("{}/{}", HUGGINGFACE_BASE, file))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn model_name_from_arg(model_arg: Option<&Path>) -> String {
|
|
29
|
+
model_arg
|
|
30
|
+
.and_then(|path| {
|
|
31
|
+
if path.is_file() || looks_like_file_path(path) {
|
|
32
|
+
// File paths are handled separately; do not treat them as model names.
|
|
33
|
+
None
|
|
34
|
+
} else {
|
|
35
|
+
path.file_name().and_then(|name| name.to_str()).map(|s| s.to_string())
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.unwrap_or_default()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn looks_like_file_path(path: &Path) -> bool {
|
|
42
|
+
path.extension().is_some()
|
|
43
|
+
|| path
|
|
44
|
+
.parent()
|
|
45
|
+
.map(|parent| !parent.as_os_str().is_empty())
|
|
46
|
+
.unwrap_or(false)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Return the default model-cache directory (`~/.cache/lincoln/models`).
|
|
50
|
+
pub fn default_cache_dir() -> PathBuf {
|
|
51
|
+
dirs::home_dir()
|
|
52
|
+
.map(|home| home.join(".cache").join("lincoln").join("models"))
|
|
53
|
+
.unwrap_or_else(|| PathBuf::from(".lincoln").join("cache").join("models"))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Resolve the absolute path to the Whisper model.
|
|
57
|
+
///
|
|
58
|
+
/// Priority:
|
|
59
|
+
/// 1. `model_arg` if it points to an existing file.
|
|
60
|
+
/// 2. `model_arg` as a model name inside the cache directory.
|
|
61
|
+
/// 3. `config.transcription.model` as a model name inside the cache directory.
|
|
62
|
+
///
|
|
63
|
+
/// Returns an error with download instructions if the file is missing.
|
|
64
|
+
pub fn resolve_model_path(model_arg: Option<&Path>, config: &Config) -> Result<PathBuf> {
|
|
65
|
+
let cache_dir = config
|
|
66
|
+
.model_cache_dir
|
|
67
|
+
.clone()
|
|
68
|
+
.unwrap_or_else(default_cache_dir);
|
|
69
|
+
let engine = config.transcription.engine.clone();
|
|
70
|
+
|
|
71
|
+
if let Some(path) = model_arg.filter(|p| p.is_file() || looks_like_file_path(p)) {
|
|
72
|
+
return Ok(path.to_path_buf());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let model_name = model_name_from_arg(model_arg);
|
|
76
|
+
let model_name = if model_name.is_empty() {
|
|
77
|
+
config.transcription.model.as_str()
|
|
78
|
+
} else {
|
|
79
|
+
model_name.as_str()
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
let path = cache_path_for_model(model_name, &engine, &cache_dir);
|
|
83
|
+
|
|
84
|
+
if model_filename(model_name).is_some() {
|
|
85
|
+
return Ok(path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let url = manual_download_url(model_name);
|
|
89
|
+
|
|
90
|
+
anyhow::bail!(
|
|
91
|
+
"Whisper model not found: {}\n\n\
|
|
92
|
+
You can either:\n\
|
|
93
|
+
1. Download it automatically: lincoln-record warmup --model {}\n\
|
|
94
|
+
2. Download manually: {}\n\
|
|
95
|
+
3. Place it at: {}\n\n\
|
|
96
|
+
To use a different model, pass --model <name-or-path> or set it in ~/.lincolnrc.",
|
|
97
|
+
model_name,
|
|
98
|
+
model_name,
|
|
99
|
+
url,
|
|
100
|
+
path.display()
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Resolve the cache path for a model name without checking existence.
|
|
105
|
+
pub fn cache_path_for_model(name: &str, engine: &str, cache_dir: &Path) -> PathBuf {
|
|
106
|
+
cache_dir.join(engine).join(format!("ggml-{}.bin", name))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/// Return the manual download URL for a known model name.
|
|
110
|
+
pub fn manual_download_url(name: &str) -> String {
|
|
111
|
+
model_url(name).unwrap_or_else(|| format!("{}/ggml-{}.bin", HUGGINGFACE_BASE, name))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn mirror_download_url(name: &str) -> String {
|
|
115
|
+
model_filename(name)
|
|
116
|
+
.map(|file| format!("{}/{}", HF_MIRROR_BASE, file))
|
|
117
|
+
.unwrap_or_else(|| format!("{}/ggml-{}.bin", HF_MIRROR_BASE, name))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Validate that a model name is known.
|
|
121
|
+
pub fn validate_model_name(name: &str) -> Result<()> {
|
|
122
|
+
if model_filename(name).is_some() {
|
|
123
|
+
Ok(())
|
|
124
|
+
} else {
|
|
125
|
+
anyhow::bail!(
|
|
126
|
+
"unknown model '{}'. Known models: tiny, base, small, medium, large-v1, large-v2, large-v3, large-v3-turbo",
|
|
127
|
+
name
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Download a Whisper model into the cache directory.
|
|
133
|
+
///
|
|
134
|
+
/// Progress is reported via `progress(downloaded_bytes, total_bytes_hint)`.
|
|
135
|
+
/// If the model file already exists, it is returned immediately.
|
|
136
|
+
/// If the primary HuggingFace host is unreachable, falls back to hf-mirror.com.
|
|
137
|
+
pub fn download_model<F>(
|
|
138
|
+
name: &str,
|
|
139
|
+
engine: &str,
|
|
140
|
+
cache_dir: &Path,
|
|
141
|
+
mut progress: F,
|
|
142
|
+
) -> Result<PathBuf>
|
|
143
|
+
where
|
|
144
|
+
F: FnMut(usize, Option<u64>),
|
|
145
|
+
{
|
|
146
|
+
validate_model_name(name)?;
|
|
147
|
+
let dest = cache_path_for_model(name, engine, cache_dir);
|
|
148
|
+
if dest.is_file() {
|
|
149
|
+
return Ok(dest);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
let parent = dest
|
|
153
|
+
.parent()
|
|
154
|
+
.context("model cache path has no parent directory")?;
|
|
155
|
+
std::fs::create_dir_all(parent)
|
|
156
|
+
.with_context(|| format!("failed to create cache directory: {}", parent.display()))?;
|
|
157
|
+
|
|
158
|
+
let urls = [manual_download_url(name), mirror_download_url(name)];
|
|
159
|
+
let mut last_error: Option<anyhow::Error> = None;
|
|
160
|
+
|
|
161
|
+
for url in urls {
|
|
162
|
+
match download_from_url(&url, &dest, &mut progress) {
|
|
163
|
+
Ok(()) => return Ok(dest),
|
|
164
|
+
Err(error) => {
|
|
165
|
+
last_error = Some(error.context(format!("failed to download from: {}", url)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Err(last_error.unwrap_or_else(|| anyhow::anyhow!("download failed")))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
fn download_from_url<F>(url: &str, dest: &Path, progress: &mut F) -> Result<()>
|
|
174
|
+
where
|
|
175
|
+
F: FnMut(usize, Option<u64>),
|
|
176
|
+
{
|
|
177
|
+
let mut response = ureq::get(url)
|
|
178
|
+
.call()
|
|
179
|
+
.with_context(|| format!("failed to start download from: {}", url))?;
|
|
180
|
+
|
|
181
|
+
let total = response.body().content_length();
|
|
182
|
+
|
|
183
|
+
let temp = dest.with_extension("bin.tmp");
|
|
184
|
+
let mut file = std::fs::File::create(&temp)
|
|
185
|
+
.with_context(|| format!("failed to create temporary file: {}", temp.display()))?;
|
|
186
|
+
|
|
187
|
+
let mut reader = response.body_mut().as_reader();
|
|
188
|
+
let mut buffer = [0u8; 8192];
|
|
189
|
+
let mut downloaded = 0usize;
|
|
190
|
+
|
|
191
|
+
loop {
|
|
192
|
+
let n = reader
|
|
193
|
+
.read(&mut buffer)
|
|
194
|
+
.context("failed to read download chunk")?;
|
|
195
|
+
if n == 0 {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
file.write_all(&buffer[..n])
|
|
199
|
+
.with_context(|| format!("failed to write to: {}", temp.display()))?;
|
|
200
|
+
downloaded += n;
|
|
201
|
+
progress(downloaded, total);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
file.flush().context("failed to flush downloaded model")?;
|
|
205
|
+
drop(file);
|
|
206
|
+
|
|
207
|
+
std::fs::rename(&temp, dest)
|
|
208
|
+
.with_context(|| format!("failed to move downloaded model to: {}", dest.display()))?;
|
|
209
|
+
|
|
210
|
+
Ok(())
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[cfg(test)]
|
|
214
|
+
mod tests {
|
|
215
|
+
use super::*;
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn default_cache_dir_under_home() {
|
|
219
|
+
let dir = default_cache_dir();
|
|
220
|
+
assert!(dir.to_string_lossy().contains(".cache/lincoln/models"));
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#[test]
|
|
224
|
+
fn download_model_returns_existing_file_without_download() {
|
|
225
|
+
let tmp = std::env::temp_dir().join(format!(
|
|
226
|
+
"lincoln-model-test-{}",
|
|
227
|
+
std::process::id()
|
|
228
|
+
));
|
|
229
|
+
std::fs::create_dir_all(&tmp).unwrap();
|
|
230
|
+
let cache_dir = tmp.join("cache");
|
|
231
|
+
let dest = cache_path_for_model("tiny", "whisper", &cache_dir);
|
|
232
|
+
std::fs::create_dir_all(dest.parent().unwrap()).unwrap();
|
|
233
|
+
std::fs::write(&dest, b"fake model").unwrap();
|
|
234
|
+
|
|
235
|
+
let result = download_model("tiny", "whisper", &cache_dir, |_downloaded, _total| {});
|
|
236
|
+
assert_eq!(result.unwrap(), dest);
|
|
237
|
+
|
|
238
|
+
std::fs::remove_dir_all(&tmp).ok();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
#[test]
|
|
242
|
+
fn mirror_url_matches_hf_mirror_host() {
|
|
243
|
+
assert_eq!(
|
|
244
|
+
mirror_download_url("base"),
|
|
245
|
+
"https://hf-mirror.com/ggerganov/whisper.cpp/resolve/main/ggml-base.bin"
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
|
|
3
|
+
/// Recording session metadata written alongside `transcript.md`.
|
|
4
|
+
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
5
|
+
pub struct SessionMetadata {
|
|
6
|
+
pub session_id: String,
|
|
7
|
+
pub process_slug: String,
|
|
8
|
+
pub started_at: String,
|
|
9
|
+
pub ended_at: Option<String>,
|
|
10
|
+
pub engine: String,
|
|
11
|
+
pub model: String,
|
|
12
|
+
pub diarization: bool,
|
|
13
|
+
pub devices: DeviceInfo,
|
|
14
|
+
pub files: FileInfo,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Audio devices used for the session.
|
|
18
|
+
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
19
|
+
pub struct DeviceInfo {
|
|
20
|
+
pub microphone: String,
|
|
21
|
+
pub system: Option<String>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Output file names relative to the session directory.
|
|
25
|
+
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
26
|
+
pub struct FileInfo {
|
|
27
|
+
pub transcript: String,
|
|
28
|
+
pub audio: String,
|
|
29
|
+
pub metadata: String,
|
|
30
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
pub use crate::transcription::provider::TranscriptSegment;
|
|
2
|
+
|
|
3
|
+
/// Format segments as a Lincoln-standard `transcript.md` document.
|
|
4
|
+
pub fn format_transcript(session_id: &str, segments: &[TranscriptSegment]) -> String {
|
|
5
|
+
let mut speakers: Vec<String> = segments
|
|
6
|
+
.iter()
|
|
7
|
+
.filter_map(|segment| segment.speaker.clone())
|
|
8
|
+
.fold(Vec::new(), |mut acc, speaker| {
|
|
9
|
+
if !acc.contains(&speaker) {
|
|
10
|
+
acc.push(speaker);
|
|
11
|
+
}
|
|
12
|
+
acc
|
|
13
|
+
});
|
|
14
|
+
speakers.sort();
|
|
15
|
+
|
|
16
|
+
let mut output = String::new();
|
|
17
|
+
output.push_str(&format!("# Interview: {}\n\n", session_id));
|
|
18
|
+
output.push_str("## Speakers\n\n");
|
|
19
|
+
for speaker in &speakers {
|
|
20
|
+
output.push_str(&format!("- {}\n", speaker));
|
|
21
|
+
}
|
|
22
|
+
output.push_str("\n## Transcript\n\n");
|
|
23
|
+
|
|
24
|
+
for segment in segments {
|
|
25
|
+
let speaker = segment.speaker.as_deref().unwrap_or("Unknown");
|
|
26
|
+
let timestamp = format_timestamp(segment.start);
|
|
27
|
+
output.push_str(&format!(
|
|
28
|
+
"**[{}] {}**: {}\n\n",
|
|
29
|
+
timestamp, speaker, segment.text
|
|
30
|
+
));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
output
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn format_timestamp(seconds: f64) -> String {
|
|
37
|
+
let total_seconds = seconds as u64;
|
|
38
|
+
let hours = total_seconds / 3600;
|
|
39
|
+
let minutes = (total_seconds % 3600) / 60;
|
|
40
|
+
let secs = total_seconds % 60;
|
|
41
|
+
format!("{:02}:{:02}:{:02}", hours, minutes, secs)
|
|
42
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
use crate::transcription::provider::TranscriptSegment;
|
|
2
|
+
use anyhow::{Context, Result};
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
use std::process::Command;
|
|
5
|
+
|
|
6
|
+
/// Run the Python diarization sidecar on `audio_path` and merge the result with
|
|
7
|
+
/// `transcript_segments`. Returns a new segment list with speaker labels.
|
|
8
|
+
pub fn diarize_and_merge(
|
|
9
|
+
audio_path: &Path,
|
|
10
|
+
transcript_segments: &[TranscriptSegment],
|
|
11
|
+
work_dir: &Path,
|
|
12
|
+
) -> Result<Vec<TranscriptSegment>> {
|
|
13
|
+
let transcript_json = work_dir.join("transcripts.json");
|
|
14
|
+
let diarization_json = work_dir.join("diarization.json");
|
|
15
|
+
let merged_json = work_dir.join("merged.json");
|
|
16
|
+
|
|
17
|
+
std::fs::write(
|
|
18
|
+
&transcript_json,
|
|
19
|
+
serde_json::to_string_pretty(transcript_segments)?,
|
|
20
|
+
)
|
|
21
|
+
.with_context(|| "failed to write transcripts.json")?;
|
|
22
|
+
|
|
23
|
+
let project_root = Path::new(env!("CARGO_MANIFEST_DIR"));
|
|
24
|
+
let diarize_script = project_root.join("diarize/diarize.py");
|
|
25
|
+
let merge_script = project_root.join("diarize/merge.py");
|
|
26
|
+
|
|
27
|
+
let diarize_output = Command::new("python3")
|
|
28
|
+
.arg(&diarize_script)
|
|
29
|
+
.arg(audio_path)
|
|
30
|
+
.arg("--output")
|
|
31
|
+
.arg(&diarization_json)
|
|
32
|
+
.output()
|
|
33
|
+
.with_context(|| format!("failed to execute {}", diarize_script.display()))?;
|
|
34
|
+
if !diarize_output.status.success() {
|
|
35
|
+
anyhow::bail!(
|
|
36
|
+
"{} failed: {}",
|
|
37
|
+
diarize_script.display(),
|
|
38
|
+
String::from_utf8_lossy(&diarize_output.stderr)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let merge_output = Command::new("python3")
|
|
43
|
+
.arg(&merge_script)
|
|
44
|
+
.arg(&transcript_json)
|
|
45
|
+
.arg(&diarization_json)
|
|
46
|
+
.arg("--output")
|
|
47
|
+
.arg(&merged_json)
|
|
48
|
+
.output()
|
|
49
|
+
.with_context(|| format!("failed to execute {}", merge_script.display()))?;
|
|
50
|
+
if !merge_output.status.success() {
|
|
51
|
+
anyhow::bail!(
|
|
52
|
+
"{} failed: {}",
|
|
53
|
+
merge_script.display(),
|
|
54
|
+
String::from_utf8_lossy(&merge_output.stderr)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let raw = std::fs::read_to_string(&merged_json)
|
|
59
|
+
.with_context(|| format!("failed to read {}", merged_json.display()))?;
|
|
60
|
+
let segments: Vec<TranscriptSegment> =
|
|
61
|
+
serde_json::from_str(&raw).with_context(|| "failed to parse merged diarization json")?;
|
|
62
|
+
|
|
63
|
+
Ok(segments)
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
use anyhow::Result;
|
|
2
|
+
use serde::{Deserialize, Serialize};
|
|
3
|
+
use std::path::Path;
|
|
4
|
+
|
|
5
|
+
/// A single transcript segment with timing and optional speaker label.
|
|
6
|
+
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
|
|
7
|
+
pub struct TranscriptSegment {
|
|
8
|
+
pub start: f64,
|
|
9
|
+
pub end: f64,
|
|
10
|
+
pub speaker: Option<String>,
|
|
11
|
+
pub text: String,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Abstraction over local transcription engines.
|
|
15
|
+
pub trait TranscriptionProvider: Send + Sync {
|
|
16
|
+
/// Transcribe `audio_path` and return segments ordered by time.
|
|
17
|
+
fn transcribe(
|
|
18
|
+
&self,
|
|
19
|
+
audio_path: &Path,
|
|
20
|
+
language: Option<&str>,
|
|
21
|
+
) -> Result<Vec<TranscriptSegment>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// A deterministic provider for tests and development.
|
|
25
|
+
pub struct MockProvider {
|
|
26
|
+
segments: Vec<TranscriptSegment>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl MockProvider {
|
|
30
|
+
pub fn new(segments: Vec<TranscriptSegment>) -> Self {
|
|
31
|
+
Self { segments }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
impl TranscriptionProvider for MockProvider {
|
|
36
|
+
fn transcribe(
|
|
37
|
+
&self,
|
|
38
|
+
_audio_path: &Path,
|
|
39
|
+
_language: Option<&str>,
|
|
40
|
+
) -> Result<Vec<TranscriptSegment>> {
|
|
41
|
+
Ok(self.segments.clone())
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
use crate::audio::resampler::resample_interleaved;
|
|
2
|
+
use crate::transcription::provider::{TranscriptSegment, TranscriptionProvider};
|
|
3
|
+
use anyhow::{Context, Result};
|
|
4
|
+
use hound::WavReader;
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters};
|
|
7
|
+
|
|
8
|
+
const WHISPER_SAMPLE_RATE: u32 = 16000;
|
|
9
|
+
const WHISPER_CHANNELS: u16 = 1;
|
|
10
|
+
|
|
11
|
+
/// A local Whisper transcription provider using whisper.cpp via `whisper-rs`.
|
|
12
|
+
pub struct WhisperProvider {
|
|
13
|
+
model_path: std::path::PathBuf,
|
|
14
|
+
language: Option<String>,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
impl WhisperProvider {
|
|
18
|
+
pub fn new(model_path: impl Into<std::path::PathBuf>, language: Option<String>) -> Self {
|
|
19
|
+
Self {
|
|
20
|
+
model_path: model_path.into(),
|
|
21
|
+
language,
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
impl TranscriptionProvider for WhisperProvider {
|
|
27
|
+
fn transcribe(&self,
|
|
28
|
+
audio_path: &Path,
|
|
29
|
+
language: Option<&str>,
|
|
30
|
+
) -> Result<Vec<TranscriptSegment>> {
|
|
31
|
+
anyhow::ensure!(
|
|
32
|
+
self.model_path.is_file(),
|
|
33
|
+
"Whisper model not found: {}",
|
|
34
|
+
self.model_path.display()
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
let samples = load_whisper_audio(audio_path)
|
|
38
|
+
.with_context(|| format!("failed to load audio: {}", audio_path.display()))?;
|
|
39
|
+
|
|
40
|
+
let ctx = WhisperContext::new_with_params(
|
|
41
|
+
self.model_path.to_string_lossy().as_ref(),
|
|
42
|
+
WhisperContextParameters::default(),
|
|
43
|
+
)
|
|
44
|
+
.with_context(|| format!("failed to load Whisper model: {}", self.model_path.display()))?;
|
|
45
|
+
|
|
46
|
+
let mut params = FullParams::new(SamplingStrategy::Greedy { best_of: 1 });
|
|
47
|
+
|
|
48
|
+
let lang = language
|
|
49
|
+
.filter(|l| *l != "auto")
|
|
50
|
+
.or_else(|| self.language.as_deref().filter(|l| *l != "auto"));
|
|
51
|
+
params.set_language(lang);
|
|
52
|
+
params.set_print_special(false);
|
|
53
|
+
params.set_print_progress(false);
|
|
54
|
+
params.set_print_realtime(false);
|
|
55
|
+
params.set_print_timestamps(false);
|
|
56
|
+
|
|
57
|
+
let mut state = ctx.create_state().context("failed to create Whisper state")?;
|
|
58
|
+
state
|
|
59
|
+
.full(params, &samples)
|
|
60
|
+
.context("Whisper transcription failed")?;
|
|
61
|
+
|
|
62
|
+
let num_segments = state.full_n_segments().context("failed to get segment count")?;
|
|
63
|
+
let mut segments = Vec::with_capacity(num_segments as usize);
|
|
64
|
+
for index in 0..num_segments {
|
|
65
|
+
let text = state
|
|
66
|
+
.full_get_segment_text(index)
|
|
67
|
+
.context("failed to get segment text")?;
|
|
68
|
+
let start = state
|
|
69
|
+
.full_get_segment_t0(index)
|
|
70
|
+
.context("failed to get segment start")? as f64 / 1000.0;
|
|
71
|
+
let end = state
|
|
72
|
+
.full_get_segment_t1(index)
|
|
73
|
+
.context("failed to get segment end")? as f64 / 1000.0;
|
|
74
|
+
segments.push(TranscriptSegment {
|
|
75
|
+
start,
|
|
76
|
+
end,
|
|
77
|
+
speaker: None,
|
|
78
|
+
text: text.trim().to_string(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Ok(segments)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// Load a WAV file and convert it to 16 kHz mono f32 samples for Whisper.
|
|
87
|
+
fn load_whisper_audio(path: &Path) -> Result<Vec<f32>> {
|
|
88
|
+
let mut reader = WavReader::open(path)
|
|
89
|
+
.with_context(|| format!("failed to open wav: {}", path.display()))?;
|
|
90
|
+
let spec = reader.spec();
|
|
91
|
+
|
|
92
|
+
anyhow::ensure!(
|
|
93
|
+
spec.sample_rate > 0,
|
|
94
|
+
"invalid sample rate: {}",
|
|
95
|
+
spec.sample_rate
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
let samples: Vec<f32> = match spec.sample_format {
|
|
99
|
+
hound::SampleFormat::Int => reader
|
|
100
|
+
.samples::<i16>()
|
|
101
|
+
.map(|sample| sample.map(|s| s as f32 / i16::MAX as f32))
|
|
102
|
+
.collect::<Result<_, _>>()
|
|
103
|
+
.context("failed to read i16 samples")?,
|
|
104
|
+
hound::SampleFormat::Float => reader
|
|
105
|
+
.samples::<f32>()
|
|
106
|
+
.collect::<Result<_, _>>()
|
|
107
|
+
.context("failed to read f32 samples")?,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
let channels = spec.channels;
|
|
111
|
+
let mono = if channels == 1 {
|
|
112
|
+
samples
|
|
113
|
+
} else {
|
|
114
|
+
downmix_to_mono(&samples, channels)
|
|
115
|
+
.context("failed to downmix audio to mono")?
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
if spec.sample_rate == WHISPER_SAMPLE_RATE {
|
|
119
|
+
return Ok(mono);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
resample_interleaved(
|
|
123
|
+
&mono,
|
|
124
|
+
spec.sample_rate,
|
|
125
|
+
WHISPER_SAMPLE_RATE,
|
|
126
|
+
WHISPER_CHANNELS,
|
|
127
|
+
)
|
|
128
|
+
.context("failed to resample audio to 16 kHz")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn downmix_to_mono(samples: &[f32], channels: u16) -> Result<Vec<f32>> {
|
|
132
|
+
let channels = channels as usize;
|
|
133
|
+
anyhow::ensure!(channels > 0, "channels must be greater than zero");
|
|
134
|
+
anyhow::ensure!(
|
|
135
|
+
samples.len().is_multiple_of(channels),
|
|
136
|
+
"sample length is not a multiple of channels"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let divisor = channels as f32;
|
|
140
|
+
Ok(samples
|
|
141
|
+
.chunks_exact(channels)
|
|
142
|
+
.map(|frame| frame.iter().sum::<f32>() / divisor)
|
|
143
|
+
.collect())
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[cfg(test)]
|
|
147
|
+
mod tests {
|
|
148
|
+
use super::*;
|
|
149
|
+
use std::path::PathBuf;
|
|
150
|
+
|
|
151
|
+
#[test]
|
|
152
|
+
fn test_downmix_stereo_to_mono() {
|
|
153
|
+
let stereo = vec![1.0f32, 1.0f32, -1.0f32, -1.0f32, 0.5f32, 0.5f32];
|
|
154
|
+
let mono = downmix_to_mono(&stereo, 2).unwrap();
|
|
155
|
+
assert_eq!(mono, vec![1.0f32, -1.0f32, 0.5f32]);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
#[test]
|
|
159
|
+
fn test_whisper_provider_missing_model_errors() {
|
|
160
|
+
let provider = WhisperProvider::new("/nonexistent/model.bin", None);
|
|
161
|
+
let result = provider.transcribe(Path::new("/nonexistent/audio.wav"), None);
|
|
162
|
+
assert!(result.is_err());
|
|
163
|
+
let msg = format!("{}", result.unwrap_err());
|
|
164
|
+
assert!(msg.contains("Whisper model not found"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
#[ignore = "requires a local Whisper ggml model"]
|
|
169
|
+
fn test_whisper_provider_transcribes_fixture() {
|
|
170
|
+
let model_path = PathBuf::from(std::env::var("WHISPER_MODEL").unwrap());
|
|
171
|
+
let audio_path = PathBuf::from(std::env::var("WHISPER_AUDIO").unwrap());
|
|
172
|
+
let provider = WhisperProvider::new(&model_path, None);
|
|
173
|
+
let segments = provider.transcribe(&audio_path, None).unwrap();
|
|
174
|
+
assert!(!segments.is_empty());
|
|
175
|
+
}
|
|
176
|
+
}
|