@sushanglewis/lincoln 1.5.0 → 1.5.1
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 +1231 -906
- 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,24 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "lincoln-record"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2024"
|
|
5
|
+
|
|
6
|
+
[dependencies]
|
|
7
|
+
anyhow = "1.0.103"
|
|
8
|
+
chrono = { version = "0.4.45", features = ["serde"] }
|
|
9
|
+
clap = { version = "4.6.1", features = ["derive"] }
|
|
10
|
+
cpal = "0.18.1"
|
|
11
|
+
crossbeam-channel = "0.5.16"
|
|
12
|
+
dirs = "6.0.0"
|
|
13
|
+
env_logger = "0.11.11"
|
|
14
|
+
hound = "3.5.1"
|
|
15
|
+
log = "0.4.33"
|
|
16
|
+
serde = { version = "1.0.228", features = ["derive"] }
|
|
17
|
+
serde_json = "1.0.150"
|
|
18
|
+
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "process", "signal", "fs"] }
|
|
19
|
+
toml = "0.8"
|
|
20
|
+
ureq = "3.0"
|
|
21
|
+
whisper-rs = { version = "0.14", features = ["metal"] }
|
|
22
|
+
|
|
23
|
+
[dev-dependencies]
|
|
24
|
+
tempfile = "3.27.0"
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lightweight local speaker diarization for testing and fallback.
|
|
3
|
+
|
|
4
|
+
This implementation uses zero-crossing rate heuristics and is intentionally
|
|
5
|
+
simple. In production it should be replaced with whisperx + pyannote.audio
|
|
6
|
+
once models and HuggingFace tokens are available.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import struct
|
|
13
|
+
import wave
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def read_mono_16bit(path: str) -> tuple[int, bytes]:
|
|
17
|
+
with wave.open(path, "rb") as wf:
|
|
18
|
+
channels = wf.getnchannels()
|
|
19
|
+
rate = wf.getframerate()
|
|
20
|
+
frames = wf.getnframes()
|
|
21
|
+
data = wf.readframes(frames)
|
|
22
|
+
if channels != 1:
|
|
23
|
+
raise ValueError("only mono WAV files are supported")
|
|
24
|
+
return rate, data
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def samples_from_bytes(data: bytes) -> list[float]:
|
|
28
|
+
count = len(data) // 2
|
|
29
|
+
ints = struct.unpack(f"<{count}h", data)
|
|
30
|
+
return [s / 32768.0 for s in ints]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def zero_crossing_rate(samples: list[float]) -> float:
|
|
34
|
+
if len(samples) < 2:
|
|
35
|
+
return 0.0
|
|
36
|
+
crossings = sum(1 for i in range(1, len(samples)) if samples[i - 1] * samples[i] < 0)
|
|
37
|
+
return crossings / len(samples)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def rms(samples: list[float]) -> float:
|
|
41
|
+
if not samples:
|
|
42
|
+
return 0.0
|
|
43
|
+
return math.sqrt(sum(s * s for s in samples) / len(samples))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def diarize(path: str, window_seconds: float = 0.5) -> list[dict]:
|
|
47
|
+
rate, data = read_mono_16bit(path)
|
|
48
|
+
samples = samples_from_bytes(data)
|
|
49
|
+
window_size = int(rate * window_seconds)
|
|
50
|
+
|
|
51
|
+
features = []
|
|
52
|
+
for start in range(0, len(samples), window_size):
|
|
53
|
+
chunk = samples[start : start + window_size]
|
|
54
|
+
if rms(chunk) < 0.01:
|
|
55
|
+
continue
|
|
56
|
+
features.append(
|
|
57
|
+
{
|
|
58
|
+
"start": start / rate,
|
|
59
|
+
"end": min(start + window_size, len(samples)) / rate,
|
|
60
|
+
"zcr": zero_crossing_rate(chunk),
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if not features:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
# Classify into two speakers by median zero-crossing rate.
|
|
68
|
+
zcrs = [f["zcr"] for f in features]
|
|
69
|
+
threshold = sum(zcrs) / len(zcrs)
|
|
70
|
+
|
|
71
|
+
segments = []
|
|
72
|
+
current_speaker = "Speaker 1" if features[0]["zcr"] <= threshold else "Speaker 2"
|
|
73
|
+
current_start = features[0]["start"]
|
|
74
|
+
current_end = features[0]["end"]
|
|
75
|
+
|
|
76
|
+
for feature in features[1:]:
|
|
77
|
+
speaker = "Speaker 1" if feature["zcr"] <= threshold else "Speaker 2"
|
|
78
|
+
if speaker == current_speaker:
|
|
79
|
+
current_end = feature["end"]
|
|
80
|
+
else:
|
|
81
|
+
segments.append(
|
|
82
|
+
{
|
|
83
|
+
"start": current_start,
|
|
84
|
+
"end": current_end,
|
|
85
|
+
"speaker": current_speaker,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
current_speaker = speaker
|
|
89
|
+
current_start = feature["start"]
|
|
90
|
+
current_end = feature["end"]
|
|
91
|
+
|
|
92
|
+
segments.append(
|
|
93
|
+
{
|
|
94
|
+
"start": current_start,
|
|
95
|
+
"end": current_end,
|
|
96
|
+
"speaker": current_speaker,
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
return segments
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main() -> None:
|
|
103
|
+
parser = argparse.ArgumentParser(description="Local speaker diarization")
|
|
104
|
+
parser.add_argument("audio", help="input WAV file")
|
|
105
|
+
parser.add_argument("--output", required=True, help="output JSON file")
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"--window", type=float, default=0.5, help="analysis window in seconds"
|
|
108
|
+
)
|
|
109
|
+
args = parser.parse_args()
|
|
110
|
+
|
|
111
|
+
segments = diarize(args.audio, args.window)
|
|
112
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
113
|
+
json.dump(segments, f, indent=2)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if __name__ == "__main__":
|
|
117
|
+
main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Merge diarization output with transcript segments."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def overlap(a_start: float, a_end: float, b_start: float, b_end: float) -> float:
|
|
9
|
+
return max(0.0, min(a_end, b_end) - max(a_start, b_start))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge(transcript: list[dict], diarization: list[dict]) -> list[dict]:
|
|
13
|
+
merged = []
|
|
14
|
+
for t in transcript:
|
|
15
|
+
t_start = t.get("start", 0.0)
|
|
16
|
+
t_end = t.get("end", t_start)
|
|
17
|
+
best_speaker = None
|
|
18
|
+
best_overlap = 0.0
|
|
19
|
+
for d in diarization:
|
|
20
|
+
d_start = d.get("start", 0.0)
|
|
21
|
+
d_end = d.get("end", d_start)
|
|
22
|
+
ov = overlap(t_start, t_end, d_start, d_end)
|
|
23
|
+
if ov > best_overlap:
|
|
24
|
+
best_overlap = ov
|
|
25
|
+
best_speaker = d.get("speaker")
|
|
26
|
+
merged.append(
|
|
27
|
+
{
|
|
28
|
+
"start": t_start,
|
|
29
|
+
"end": t_end,
|
|
30
|
+
"speaker": best_speaker,
|
|
31
|
+
"text": t.get("text", ""),
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
return merged
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
parser = argparse.ArgumentParser(description="Merge diarization with transcript")
|
|
39
|
+
parser.add_argument("transcript", help="transcript JSON file")
|
|
40
|
+
parser.add_argument("diarization", help="diarization JSON file")
|
|
41
|
+
parser.add_argument("--output", required=True, help="output JSON file")
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
with open(args.transcript, encoding="utf-8") as f:
|
|
45
|
+
transcript = json.load(f)
|
|
46
|
+
with open(args.diarization, encoding="utf-8") as f:
|
|
47
|
+
diarization = json.load(f)
|
|
48
|
+
|
|
49
|
+
merged = merge(transcript, diarization)
|
|
50
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
51
|
+
json.dump(merged, f, indent=2)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
main()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
use crate::audio::source::AudioSource;
|
|
2
|
+
use anyhow::{Context, Result};
|
|
3
|
+
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
|
4
|
+
use cpal::{SampleFormat, Stream, StreamConfig};
|
|
5
|
+
use crossbeam_channel::{Receiver, Select, Sender, bounded};
|
|
6
|
+
use std::sync::Arc;
|
|
7
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
8
|
+
|
|
9
|
+
const AUDIO_CHANNEL_CAPACITY: usize = 50;
|
|
10
|
+
|
|
11
|
+
/// An [AudioSource] that captures live microphone input until stopped or the
|
|
12
|
+
/// maximum duration is reached.
|
|
13
|
+
pub struct MicrophoneSource {
|
|
14
|
+
sample_rate: u32,
|
|
15
|
+
channels: u16,
|
|
16
|
+
audio_sender: Sender<Option<Vec<f32>>>,
|
|
17
|
+
audio_receiver: Receiver<Option<Vec<f32>>>,
|
|
18
|
+
/// Kept alive so the OS continues to deliver samples while we iterate.
|
|
19
|
+
#[allow(dead_code)]
|
|
20
|
+
stream: Stream,
|
|
21
|
+
target_samples: usize,
|
|
22
|
+
stop_flag: Arc<AtomicBool>,
|
|
23
|
+
stop_sender: Sender<()>,
|
|
24
|
+
stop_receiver: Receiver<()>,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Handle that can request an early stop of a [MicrophoneSource].
|
|
28
|
+
#[derive(Clone)]
|
|
29
|
+
pub struct StopHandle {
|
|
30
|
+
sender: Sender<()>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
impl StopHandle {
|
|
34
|
+
/// Request a stop. The associated iterator will terminate after draining
|
|
35
|
+
/// any buffered audio chunks.
|
|
36
|
+
pub fn stop(&self) {
|
|
37
|
+
let _ = self.sender.try_send(());
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
impl MicrophoneSource {
|
|
42
|
+
/// Open the requested microphone (or the system default) and start capturing.
|
|
43
|
+
pub fn new(device_name: Option<&str>, duration_seconds: f32) -> Result<Self> {
|
|
44
|
+
anyhow::ensure!(duration_seconds > 0.0, "duration must be greater than zero");
|
|
45
|
+
|
|
46
|
+
let host = cpal::default_host();
|
|
47
|
+
let device = match device_name {
|
|
48
|
+
Some(name) => host
|
|
49
|
+
.input_devices()?
|
|
50
|
+
.find(|d| d.to_string() == name)
|
|
51
|
+
.with_context(|| format!("microphone not found: {}", name))?,
|
|
52
|
+
None => host
|
|
53
|
+
.default_input_device()
|
|
54
|
+
.context("no default input device")?,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
let config = device.default_input_config()?;
|
|
58
|
+
let sample_rate = config.sample_rate();
|
|
59
|
+
let channels = config.channels();
|
|
60
|
+
let sample_format = config.sample_format();
|
|
61
|
+
|
|
62
|
+
let stream_config = StreamConfig {
|
|
63
|
+
channels,
|
|
64
|
+
sample_rate: config.sample_rate(),
|
|
65
|
+
buffer_size: cpal::BufferSize::Default,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let (audio_sender, audio_receiver) = bounded::<Option<Vec<f32>>>(AUDIO_CHANNEL_CAPACITY);
|
|
69
|
+
let callback_sender = audio_sender.clone();
|
|
70
|
+
let stop_flag = Arc::new(AtomicBool::new(false));
|
|
71
|
+
let callback_stop_flag = Arc::clone(&stop_flag);
|
|
72
|
+
|
|
73
|
+
let stream = match sample_format {
|
|
74
|
+
SampleFormat::F32 => device.build_input_stream(
|
|
75
|
+
stream_config,
|
|
76
|
+
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
|
77
|
+
if callback_stop_flag.load(Ordering::Relaxed) {
|
|
78
|
+
let _ = callback_sender.try_send(None);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if let Err(error) = callback_sender.try_send(Some(data.to_vec())) {
|
|
82
|
+
log::warn!("dropping audio chunk: {}", error);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
|error| log::error!("microphone stream error: {}", error),
|
|
86
|
+
None,
|
|
87
|
+
),
|
|
88
|
+
other => anyhow::bail!("unsupported sample format: {:?}", other),
|
|
89
|
+
}
|
|
90
|
+
.context("failed to build microphone stream")?;
|
|
91
|
+
|
|
92
|
+
stream.play().context("failed to start microphone stream")?;
|
|
93
|
+
|
|
94
|
+
let frames = (sample_rate as f32 * duration_seconds).ceil() as usize;
|
|
95
|
+
let target_samples = frames.saturating_mul(channels as usize);
|
|
96
|
+
let (stop_sender, stop_receiver) = bounded::<()>(1);
|
|
97
|
+
|
|
98
|
+
Ok(Self {
|
|
99
|
+
sample_rate,
|
|
100
|
+
channels,
|
|
101
|
+
audio_sender,
|
|
102
|
+
audio_receiver,
|
|
103
|
+
stream,
|
|
104
|
+
target_samples,
|
|
105
|
+
stop_flag,
|
|
106
|
+
stop_sender,
|
|
107
|
+
stop_receiver,
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Return a handle that can be used to request an early stop.
|
|
112
|
+
pub fn stop_handle(&self) -> StopHandle {
|
|
113
|
+
StopHandle {
|
|
114
|
+
sender: self.stop_sender.clone(),
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Signal the capture to stop. The iterator will terminate after draining
|
|
119
|
+
/// any buffered chunks.
|
|
120
|
+
pub fn stop(&self) {
|
|
121
|
+
self.stop_flag.store(true, Ordering::Relaxed);
|
|
122
|
+
let _ = self.audio_sender.try_send(None);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
impl AudioSource for MicrophoneSource {
|
|
127
|
+
fn sample_rate(&self) -> u32 {
|
|
128
|
+
self.sample_rate
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn channels(&self) -> u16 {
|
|
132
|
+
self.channels
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn duration_seconds(&self) -> f32 {
|
|
136
|
+
(self.target_samples / (self.channels as usize).max(1)) as f32 / (self.sample_rate as f32)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
fn iter(&self) -> Box<dyn Iterator<Item = f32> + Send + '_> {
|
|
140
|
+
Box::new(MicrophoneIter {
|
|
141
|
+
audio_receiver: &self.audio_receiver,
|
|
142
|
+
stop_receiver: &self.stop_receiver,
|
|
143
|
+
chunk: Vec::new(),
|
|
144
|
+
position: 0,
|
|
145
|
+
remaining: self.target_samples,
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
struct MicrophoneIter<'a> {
|
|
151
|
+
audio_receiver: &'a Receiver<Option<Vec<f32>>>,
|
|
152
|
+
stop_receiver: &'a Receiver<()>,
|
|
153
|
+
chunk: Vec<f32>,
|
|
154
|
+
position: usize,
|
|
155
|
+
remaining: usize,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
impl Iterator for MicrophoneIter<'_> {
|
|
159
|
+
type Item = f32;
|
|
160
|
+
|
|
161
|
+
fn next(&mut self) -> Option<f32> {
|
|
162
|
+
loop {
|
|
163
|
+
if self.remaining == 0 {
|
|
164
|
+
return None;
|
|
165
|
+
}
|
|
166
|
+
if self.position < self.chunk.len() {
|
|
167
|
+
let sample = self.chunk[self.position];
|
|
168
|
+
self.position += 1;
|
|
169
|
+
self.remaining -= 1;
|
|
170
|
+
return Some(sample);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let mut select = Select::new();
|
|
174
|
+
let audio_handle = select.recv(self.audio_receiver);
|
|
175
|
+
let stop_handle = select.recv(self.stop_receiver);
|
|
176
|
+
let oper = select.select();
|
|
177
|
+
|
|
178
|
+
match oper.index() {
|
|
179
|
+
index if index == audio_handle => match oper.recv(self.audio_receiver) {
|
|
180
|
+
Ok(Some(chunk)) => {
|
|
181
|
+
self.chunk = chunk;
|
|
182
|
+
self.position = 0;
|
|
183
|
+
}
|
|
184
|
+
Ok(None) | Err(_) => return None,
|
|
185
|
+
},
|
|
186
|
+
index if index == stop_handle => {
|
|
187
|
+
let _ = oper.recv(self.stop_receiver);
|
|
188
|
+
return None;
|
|
189
|
+
}
|
|
190
|
+
_ => return None,
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pub mod microphone;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
use cpal::traits::HostTrait;
|
|
2
|
+
use log::warn;
|
|
3
|
+
|
|
4
|
+
/// Enumerate the names of all available audio input devices.
|
|
5
|
+
pub fn list_input_devices() -> Result<Vec<String>, cpal::Error> {
|
|
6
|
+
let host = cpal::default_host();
|
|
7
|
+
let devices = host.input_devices()?;
|
|
8
|
+
Ok(devices.map(|device| device.to_string()).collect())
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/// Return the name of the default audio input device, if any.
|
|
12
|
+
pub fn default_input_device() -> Option<String> {
|
|
13
|
+
let host = cpal::default_host();
|
|
14
|
+
host.default_input_device().map(|device| device.to_string())
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// Log a warning when device enumeration fails so callers can still fall back.
|
|
18
|
+
pub fn log_device_enumeration_error(error: &cpal::Error) {
|
|
19
|
+
warn!("failed to enumerate audio input devices: {}", error);
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
use anyhow::{Result, ensure};
|
|
2
|
+
|
|
3
|
+
/// Naive linear-interpolation resampling for interleaved `f32` PCM.
|
|
4
|
+
///
|
|
5
|
+
/// This is intentionally simple for Phase 2; it will be replaced with a
|
|
6
|
+
/// higher-quality resampler once the pipeline stabilizes.
|
|
7
|
+
pub fn resample_interleaved(
|
|
8
|
+
input: &[f32],
|
|
9
|
+
input_rate: u32,
|
|
10
|
+
output_rate: u32,
|
|
11
|
+
channels: u16,
|
|
12
|
+
) -> Result<Vec<f32>> {
|
|
13
|
+
ensure!(input_rate > 0, "input_rate must be greater than zero");
|
|
14
|
+
ensure!(output_rate > 0, "output_rate must be greater than zero");
|
|
15
|
+
let channels = channels as usize;
|
|
16
|
+
ensure!(channels > 0, "channels must be greater than zero");
|
|
17
|
+
ensure!(
|
|
18
|
+
input.len().is_multiple_of(channels),
|
|
19
|
+
"input length must be a multiple of channels"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
let input_frames = input.len() / channels;
|
|
23
|
+
let output_frames =
|
|
24
|
+
(input_frames as f64 * output_rate as f64 / input_rate as f64).ceil() as usize;
|
|
25
|
+
let ratio = input_rate as f64 / output_rate as f64;
|
|
26
|
+
|
|
27
|
+
let mut output = Vec::with_capacity(output_frames.saturating_mul(channels));
|
|
28
|
+
|
|
29
|
+
for out_frame in 0..output_frames {
|
|
30
|
+
let in_pos = out_frame as f64 * ratio;
|
|
31
|
+
let index = in_pos as usize;
|
|
32
|
+
let frac = in_pos - index as f64;
|
|
33
|
+
let next_index = (index + 1).min(input_frames.saturating_sub(1));
|
|
34
|
+
|
|
35
|
+
for channel in 0..channels {
|
|
36
|
+
let current = input[index * channels + channel];
|
|
37
|
+
let next = input[next_index * channels + channel];
|
|
38
|
+
let sample = current + (next - current) * frac as f32;
|
|
39
|
+
output.push(sample);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
Ok(output)
|
|
44
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
use crate::audio::resampler::resample_interleaved;
|
|
2
|
+
use crate::audio::source::AudioSource;
|
|
3
|
+
use anyhow::{Context, Result, bail};
|
|
4
|
+
use hound::{WavSpec, WavWriter};
|
|
5
|
+
use std::path::Path;
|
|
6
|
+
|
|
7
|
+
const MAX_AMPLITUDE: f32 = i16::MAX as f32;
|
|
8
|
+
|
|
9
|
+
/// Write all samples from `source` to a 16-bit PCM WAV file at `output`.
|
|
10
|
+
///
|
|
11
|
+
/// If `target_sample_rate` or `target_channels` differ from the source, the
|
|
12
|
+
/// samples are resampled and duplicated/mixed down using simple rules.
|
|
13
|
+
pub fn write_source_to_wav(
|
|
14
|
+
source: &dyn AudioSource,
|
|
15
|
+
output: &Path,
|
|
16
|
+
target_sample_rate: u32,
|
|
17
|
+
target_channels: u16,
|
|
18
|
+
) -> Result<()> {
|
|
19
|
+
anyhow::ensure!(
|
|
20
|
+
target_sample_rate > 0,
|
|
21
|
+
"target_sample_rate must be greater than zero"
|
|
22
|
+
);
|
|
23
|
+
anyhow::ensure!(
|
|
24
|
+
target_channels > 0,
|
|
25
|
+
"target_channels must be greater than zero"
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
let spec = WavSpec {
|
|
29
|
+
channels: target_channels,
|
|
30
|
+
sample_rate: target_sample_rate,
|
|
31
|
+
bits_per_sample: 16,
|
|
32
|
+
sample_format: hound::SampleFormat::Int,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
let mut writer = WavWriter::create(output, spec)
|
|
36
|
+
.with_context(|| format!("failed to create wav writer: {}", output.display()))?;
|
|
37
|
+
|
|
38
|
+
let frames = (source.sample_rate() as f32 * source.duration_seconds()).max(0.0) as usize;
|
|
39
|
+
let expected_samples = frames.saturating_mul(source.channels() as usize);
|
|
40
|
+
let samples: Vec<f32> = source.iter().take(expected_samples).collect();
|
|
41
|
+
|
|
42
|
+
let final_samples = if source.sample_rate() != target_sample_rate {
|
|
43
|
+
resample_interleaved(
|
|
44
|
+
&samples,
|
|
45
|
+
source.sample_rate(),
|
|
46
|
+
target_sample_rate,
|
|
47
|
+
source.channels(),
|
|
48
|
+
)?
|
|
49
|
+
} else {
|
|
50
|
+
samples
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let final_channels = target_channels as usize;
|
|
54
|
+
let source_channels = source.channels() as usize;
|
|
55
|
+
|
|
56
|
+
for frame in final_samples.chunks(source_channels) {
|
|
57
|
+
match (source_channels, final_channels) {
|
|
58
|
+
(1, 2) => {
|
|
59
|
+
let value = frame.first().copied().unwrap_or(0.0);
|
|
60
|
+
writer.write_sample(to_i16(value))?;
|
|
61
|
+
writer.write_sample(to_i16(value))?;
|
|
62
|
+
}
|
|
63
|
+
(2, 1) => {
|
|
64
|
+
let left = frame.first().copied().unwrap_or(0.0);
|
|
65
|
+
let right = frame.get(1).copied().unwrap_or(0.0);
|
|
66
|
+
writer.write_sample(to_i16((left + right) * 0.5))?;
|
|
67
|
+
}
|
|
68
|
+
(s, t) if s == t => {
|
|
69
|
+
for value in frame {
|
|
70
|
+
writer.write_sample(to_i16(*value))?;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
_ => bail!(
|
|
74
|
+
"unsupported channel conversion from {} to {}",
|
|
75
|
+
source_channels,
|
|
76
|
+
final_channels
|
|
77
|
+
),
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writer.finalize()?;
|
|
82
|
+
Ok(())
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
fn to_i16(sample: f32) -> i16 {
|
|
86
|
+
(sample.clamp(-1.0, 1.0) * MAX_AMPLITUDE) as i16
|
|
87
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
use std::f32::consts::PI;
|
|
2
|
+
|
|
3
|
+
use anyhow::Result;
|
|
4
|
+
|
|
5
|
+
/// Metadata describing an audio source.
|
|
6
|
+
pub trait AudioSource: Send {
|
|
7
|
+
/// Sample rate in Hz.
|
|
8
|
+
fn sample_rate(&self) -> u32;
|
|
9
|
+
/// Number of interleaved channels.
|
|
10
|
+
fn channels(&self) -> u16;
|
|
11
|
+
/// Total duration in seconds.
|
|
12
|
+
fn duration_seconds(&self) -> f32;
|
|
13
|
+
/// Return an iterator yielding interleaved `f32` PCM samples.
|
|
14
|
+
fn iter(&self) -> Box<dyn Iterator<Item = f32> + Send + '_>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/// A finite sine-wave source for tests and calibration.
|
|
18
|
+
pub struct SineWaveSource {
|
|
19
|
+
sample_rate: f32,
|
|
20
|
+
channels: u16,
|
|
21
|
+
frequency: f32,
|
|
22
|
+
duration: f32,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
impl SineWaveSource {
|
|
26
|
+
pub fn new(sample_rate: f32, channels: u16, frequency: f32, duration: f32) -> Result<Self> {
|
|
27
|
+
anyhow::ensure!(sample_rate > 0.0, "sample_rate must be greater than zero");
|
|
28
|
+
anyhow::ensure!(channels > 0, "channels must be greater than zero");
|
|
29
|
+
anyhow::ensure!(duration >= 0.0, "duration must be non-negative");
|
|
30
|
+
|
|
31
|
+
Ok(Self {
|
|
32
|
+
sample_rate,
|
|
33
|
+
channels,
|
|
34
|
+
frequency,
|
|
35
|
+
duration,
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
impl AudioSource for SineWaveSource {
|
|
41
|
+
fn sample_rate(&self) -> u32 {
|
|
42
|
+
self.sample_rate as u32
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn channels(&self) -> u16 {
|
|
46
|
+
self.channels
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
fn duration_seconds(&self) -> f32 {
|
|
50
|
+
self.duration
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn iter(&self) -> Box<dyn Iterator<Item = f32> + Send + '_> {
|
|
54
|
+
let sample_rate = self.sample_rate;
|
|
55
|
+
let frequency = self.frequency;
|
|
56
|
+
let channels = self.channels as usize;
|
|
57
|
+
let total_samples =
|
|
58
|
+
((sample_rate * self.duration).ceil().max(0.0) as usize).saturating_mul(channels);
|
|
59
|
+
Box::new((0..total_samples).map(move |index| {
|
|
60
|
+
let sample_index = index / channels;
|
|
61
|
+
let t = sample_index as f32 / sample_rate;
|
|
62
|
+
(2.0 * PI * frequency * t).sin()
|
|
63
|
+
}))
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// An in-memory audio source for tests and offline processing.
|
|
68
|
+
pub struct InMemorySource {
|
|
69
|
+
samples: Vec<f32>,
|
|
70
|
+
sample_rate: u32,
|
|
71
|
+
channels: u16,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
impl InMemorySource {
|
|
75
|
+
pub fn new(samples: Vec<f32>, sample_rate: u32, channels: u16, duration: f32) -> Self {
|
|
76
|
+
let expected = ((sample_rate as f32 * duration.max(0.0)).ceil() as usize)
|
|
77
|
+
.saturating_mul(channels.max(1) as usize)
|
|
78
|
+
.min(samples.len());
|
|
79
|
+
Self {
|
|
80
|
+
samples: samples.into_iter().take(expected).collect(),
|
|
81
|
+
sample_rate,
|
|
82
|
+
channels,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
impl AudioSource for InMemorySource {
|
|
88
|
+
fn sample_rate(&self) -> u32 {
|
|
89
|
+
self.sample_rate
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn channels(&self) -> u16 {
|
|
93
|
+
self.channels
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn duration_seconds(&self) -> f32 {
|
|
97
|
+
let frames = self.samples.len() / (self.channels as usize).max(1);
|
|
98
|
+
frames as f32 / self.sample_rate as f32
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn iter(&self) -> Box<dyn Iterator<Item = f32> + Send + '_> {
|
|
102
|
+
Box::new(self.samples.iter().copied())
|
|
103
|
+
}
|
|
104
|
+
}
|