markupr 2.1.8
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/.claude/commands/review-feedback.md +47 -0
- package/.eslintrc.json +35 -0
- package/.github/CODEOWNERS +16 -0
- package/.github/FUNDING.yml +1 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +56 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +54 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +89 -0
- package/.github/dependabot.yml +70 -0
- package/.github/workflows/ci.yml +184 -0
- package/.github/workflows/deploy-landing.yml +134 -0
- package/.github/workflows/nightly.yml +288 -0
- package/.github/workflows/release.yml +318 -0
- package/CHANGELOG.md +127 -0
- package/CLAUDE.md +137 -0
- package/CODE_OF_CONDUCT.md +9 -0
- package/CONTRIBUTING.md +390 -0
- package/LICENSE +21 -0
- package/PRODUCT_VISION.md +277 -0
- package/README.md +517 -0
- package/SECURITY.md +51 -0
- package/SIGNING_INSTRUCTIONS.md +284 -0
- package/assets/DMG_BACKGROUND_INSTRUCTIONS.md +130 -0
- package/assets/svg-source/dmg-background.svg +70 -0
- package/assets/svg-source/icon.svg +20 -0
- package/assets/svg-source/tray-icon-processing.svg +7 -0
- package/assets/svg-source/tray-icon-recording.svg +7 -0
- package/assets/svg-source/tray-icon.svg +6 -0
- package/assets/tray-complete.png +0 -0
- package/assets/tray-complete@2x.png +0 -0
- package/assets/tray-completeTemplate.png +0 -0
- package/assets/tray-completeTemplate@2x.png +0 -0
- package/assets/tray-error.png +0 -0
- package/assets/tray-error@2x.png +0 -0
- package/assets/tray-errorTemplate.png +0 -0
- package/assets/tray-errorTemplate@2x.png +0 -0
- package/assets/tray-icon-processing.png +0 -0
- package/assets/tray-icon-processing@2x.png +0 -0
- package/assets/tray-icon-processingTemplate.png +0 -0
- package/assets/tray-icon-processingTemplate@2x.png +0 -0
- package/assets/tray-icon-recording.png +0 -0
- package/assets/tray-icon-recording@2x.png +0 -0
- package/assets/tray-icon-recordingTemplate.png +0 -0
- package/assets/tray-icon-recordingTemplate@2x.png +0 -0
- package/assets/tray-icon.png +0 -0
- package/assets/tray-icon@2x.png +0 -0
- package/assets/tray-iconTemplate.png +0 -0
- package/assets/tray-iconTemplate@2x.png +0 -0
- package/assets/tray-idle.png +0 -0
- package/assets/tray-idle@2x.png +0 -0
- package/assets/tray-idleTemplate.png +0 -0
- package/assets/tray-idleTemplate@2x.png +0 -0
- package/assets/tray-processing-0.png +0 -0
- package/assets/tray-processing-0@2x.png +0 -0
- package/assets/tray-processing-0Template.png +0 -0
- package/assets/tray-processing-0Template@2x.png +0 -0
- package/assets/tray-processing-1.png +0 -0
- package/assets/tray-processing-1@2x.png +0 -0
- package/assets/tray-processing-1Template.png +0 -0
- package/assets/tray-processing-1Template@2x.png +0 -0
- package/assets/tray-processing-2.png +0 -0
- package/assets/tray-processing-2@2x.png +0 -0
- package/assets/tray-processing-2Template.png +0 -0
- package/assets/tray-processing-2Template@2x.png +0 -0
- package/assets/tray-processing-3.png +0 -0
- package/assets/tray-processing-3@2x.png +0 -0
- package/assets/tray-processing-3Template.png +0 -0
- package/assets/tray-processing-3Template@2x.png +0 -0
- package/assets/tray-processing.png +0 -0
- package/assets/tray-processing@2x.png +0 -0
- package/assets/tray-processingTemplate.png +0 -0
- package/assets/tray-processingTemplate@2x.png +0 -0
- package/assets/tray-recording.png +0 -0
- package/assets/tray-recording@2x.png +0 -0
- package/assets/tray-recordingTemplate.png +0 -0
- package/assets/tray-recordingTemplate@2x.png +0 -0
- package/build/DMG_BACKGROUND_SPEC.md +50 -0
- package/build/dmg-background.png +0 -0
- package/build/dmg-background@2x.png +0 -0
- package/build/entitlements.mac.inherit.plist +27 -0
- package/build/entitlements.mac.plist +41 -0
- package/build/favicon-16.png +0 -0
- package/build/favicon-180.png +0 -0
- package/build/favicon-192.png +0 -0
- package/build/favicon-32.png +0 -0
- package/build/favicon-48.png +0 -0
- package/build/favicon-512.png +0 -0
- package/build/favicon-64.png +0 -0
- package/build/icon-128.png +0 -0
- package/build/icon-16.png +0 -0
- package/build/icon-24.png +0 -0
- package/build/icon-256.png +0 -0
- package/build/icon-32.png +0 -0
- package/build/icon-48.png +0 -0
- package/build/icon-64.png +0 -0
- package/build/icon.icns +0 -0
- package/build/icon.ico +0 -0
- package/build/icon.iconset/icon_128x128.png +0 -0
- package/build/icon.iconset/icon_128x128@2x.png +0 -0
- package/build/icon.iconset/icon_16x16.png +0 -0
- package/build/icon.iconset/icon_16x16@2x.png +0 -0
- package/build/icon.iconset/icon_256x256.png +0 -0
- package/build/icon.iconset/icon_256x256@2x.png +0 -0
- package/build/icon.iconset/icon_32x32.png +0 -0
- package/build/icon.iconset/icon_32x32@2x.png +0 -0
- package/build/icon.iconset/icon_512x512.png +0 -0
- package/build/icon.iconset/icon_512x512@2x.png +0 -0
- package/build/icon.png +0 -0
- package/build/installer-header.bmp +0 -0
- package/build/installer-header.png +0 -0
- package/build/installer-sidebar.bmp +0 -0
- package/build/installer-sidebar.png +0 -0
- package/build/installer.nsh +45 -0
- package/build/overlay-processing.png +0 -0
- package/build/overlay-recording.png +0 -0
- package/build/toolbar-record.png +0 -0
- package/build/toolbar-screenshot.png +0 -0
- package/build/toolbar-settings.png +0 -0
- package/build/toolbar-stop.png +0 -0
- package/dist/main/index.mjs +12612 -0
- package/dist/preload/index.mjs +907 -0
- package/dist/renderer/assets/index-CCmUjl9K.js +19495 -0
- package/dist/renderer/assets/index-CUqz_Gs6.css +2270 -0
- package/dist/renderer/index.html +27 -0
- package/docs/AI_AGENT_QUICKSTART.md +42 -0
- package/docs/AI_PIPELINE_DESIGN.md +595 -0
- package/docs/API.md +514 -0
- package/docs/ARCHITECTURE.md +460 -0
- package/docs/CONFIGURATION.md +336 -0
- package/docs/DEVELOPMENT.md +508 -0
- package/docs/EXPORT_FORMATS.md +451 -0
- package/docs/GETTING_STARTED.md +236 -0
- package/docs/KEYBOARD_SHORTCUTS.md +334 -0
- package/docs/TROUBLESHOOTING.md +418 -0
- package/docs/landing/index.html +672 -0
- package/docs/landing/script.js +342 -0
- package/docs/landing/styles.css +1543 -0
- package/electron-builder.yml +140 -0
- package/electron.vite.config.ts +63 -0
- package/package.json +108 -0
- package/railway.json +12 -0
- package/scripts/build.mjs +51 -0
- package/scripts/generate-icons.mjs +314 -0
- package/scripts/generate-installer-images.cjs +253 -0
- package/scripts/generate-tray-icons.mjs +258 -0
- package/scripts/notarize.cjs +180 -0
- package/scripts/one-click-clean-test.sh +147 -0
- package/scripts/postinstall.mjs +36 -0
- package/scripts/setup-markupr.sh +55 -0
- package/setup +17 -0
- package/site/index.html +1835 -0
- package/site/package.json +11 -0
- package/site/railway.json +12 -0
- package/site/server.js +31 -0
- package/src/main/AutoUpdater.ts +392 -0
- package/src/main/CrashRecovery.ts +655 -0
- package/src/main/ErrorHandler.ts +703 -0
- package/src/main/HotkeyManager.ts +399 -0
- package/src/main/MenuManager.ts +529 -0
- package/src/main/PermissionManager.ts +420 -0
- package/src/main/SessionController.ts +1465 -0
- package/src/main/TrayManager.ts +540 -0
- package/src/main/ai/AIPipelineManager.ts +199 -0
- package/src/main/ai/ClaudeAnalyzer.ts +339 -0
- package/src/main/ai/ImageOptimizer.ts +176 -0
- package/src/main/ai/StructuredMarkdownBuilder.ts +379 -0
- package/src/main/ai/index.ts +16 -0
- package/src/main/ai/types.ts +258 -0
- package/src/main/analysis/ClarificationGenerator.ts +385 -0
- package/src/main/analysis/FeedbackAnalyzer.ts +531 -0
- package/src/main/analysis/index.ts +19 -0
- package/src/main/audio/AudioCapture.ts +978 -0
- package/src/main/audio/audioUtils.ts +100 -0
- package/src/main/audio/index.ts +20 -0
- package/src/main/capture/index.ts +1 -0
- package/src/main/index.ts +1693 -0
- package/src/main/ipc/captureHandlers.ts +272 -0
- package/src/main/ipc/index.ts +45 -0
- package/src/main/ipc/outputHandlers.ts +302 -0
- package/src/main/ipc/sessionHandlers.ts +56 -0
- package/src/main/ipc/settingsHandlers.ts +471 -0
- package/src/main/ipc/types.ts +56 -0
- package/src/main/ipc/windowHandlers.ts +277 -0
- package/src/main/output/ClipboardService.ts +369 -0
- package/src/main/output/ExportService.ts +539 -0
- package/src/main/output/FileManager.ts +416 -0
- package/src/main/output/MarkdownGenerator.ts +791 -0
- package/src/main/output/MarkdownPatcher.ts +299 -0
- package/src/main/output/index.ts +186 -0
- package/src/main/output/sessionAdapter.ts +207 -0
- package/src/main/output/templates/html-template.ts +553 -0
- package/src/main/pipeline/FrameExtractor.ts +330 -0
- package/src/main/pipeline/PostProcessor.ts +399 -0
- package/src/main/pipeline/TranscriptAnalyzer.ts +226 -0
- package/src/main/pipeline/index.ts +36 -0
- package/src/main/platform/WindowsTaskbar.ts +600 -0
- package/src/main/platform/index.ts +16 -0
- package/src/main/settings/SettingsManager.ts +730 -0
- package/src/main/settings/index.ts +19 -0
- package/src/main/transcription/ModelDownloadManager.ts +494 -0
- package/src/main/transcription/TierManager.ts +219 -0
- package/src/main/transcription/TranscriptionRecoveryService.ts +340 -0
- package/src/main/transcription/WhisperService.ts +748 -0
- package/src/main/transcription/index.ts +56 -0
- package/src/main/transcription/types.ts +135 -0
- package/src/main/windows/PopoverManager.ts +284 -0
- package/src/main/windows/TaskbarIntegration.ts +452 -0
- package/src/main/windows/index.ts +23 -0
- package/src/preload/index.ts +1047 -0
- package/src/renderer/App.tsx +515 -0
- package/src/renderer/AppWrapper.tsx +28 -0
- package/src/renderer/assets/logo-dark.svg +7 -0
- package/src/renderer/assets/logo.svg +7 -0
- package/src/renderer/audio/AudioCaptureRenderer.ts +454 -0
- package/src/renderer/capture/ScreenRecordingRenderer.ts +492 -0
- package/src/renderer/components/AnnotationOverlay.tsx +836 -0
- package/src/renderer/components/AudioWaveform.tsx +811 -0
- package/src/renderer/components/ClarificationQuestions.tsx +656 -0
- package/src/renderer/components/CountdownTimer.tsx +495 -0
- package/src/renderer/components/CrashRecoveryDialog.tsx +632 -0
- package/src/renderer/components/DonateButton.tsx +127 -0
- package/src/renderer/components/ErrorBoundary.tsx +308 -0
- package/src/renderer/components/ExportDialog.tsx +872 -0
- package/src/renderer/components/HotkeyHint.tsx +261 -0
- package/src/renderer/components/KeyboardShortcuts.tsx +787 -0
- package/src/renderer/components/ModelDownloadDialog.tsx +844 -0
- package/src/renderer/components/Onboarding.tsx +1830 -0
- package/src/renderer/components/ProcessingOverlay.tsx +157 -0
- package/src/renderer/components/RecordingOverlay.tsx +423 -0
- package/src/renderer/components/SessionHistory.tsx +1746 -0
- package/src/renderer/components/SessionReview.tsx +1321 -0
- package/src/renderer/components/SettingsPanel.tsx +217 -0
- package/src/renderer/components/Skeleton.tsx +347 -0
- package/src/renderer/components/StatusIndicator.tsx +86 -0
- package/src/renderer/components/ThemeProvider.tsx +429 -0
- package/src/renderer/components/Tooltip.tsx +370 -0
- package/src/renderer/components/TranscriptionPreview.tsx +183 -0
- package/src/renderer/components/TranscriptionTierSelector.tsx +640 -0
- package/src/renderer/components/UpdateNotification.tsx +377 -0
- package/src/renderer/components/WindowSelector.tsx +947 -0
- package/src/renderer/components/index.ts +99 -0
- package/src/renderer/components/primitives/ApiKeyInput.tsx +98 -0
- package/src/renderer/components/primitives/ColorPicker.tsx +65 -0
- package/src/renderer/components/primitives/DangerButton.tsx +45 -0
- package/src/renderer/components/primitives/DirectoryPicker.tsx +41 -0
- package/src/renderer/components/primitives/Dropdown.tsx +34 -0
- package/src/renderer/components/primitives/KeyRecorder.tsx +117 -0
- package/src/renderer/components/primitives/SettingsSection.tsx +32 -0
- package/src/renderer/components/primitives/Slider.tsx +43 -0
- package/src/renderer/components/primitives/Toggle.tsx +36 -0
- package/src/renderer/components/primitives/index.ts +10 -0
- package/src/renderer/components/settings/AdvancedTab.tsx +174 -0
- package/src/renderer/components/settings/AppearanceTab.tsx +77 -0
- package/src/renderer/components/settings/GeneralTab.tsx +40 -0
- package/src/renderer/components/settings/HotkeysTab.tsx +79 -0
- package/src/renderer/components/settings/RecordingTab.tsx +84 -0
- package/src/renderer/components/settings/index.ts +9 -0
- package/src/renderer/components/settings/settingsStyles.ts +673 -0
- package/src/renderer/components/settings/tabConfig.tsx +85 -0
- package/src/renderer/components/settings/useSettingsPanel.ts +447 -0
- package/src/renderer/contexts/ProcessingContext.tsx +227 -0
- package/src/renderer/contexts/RecordingContext.tsx +683 -0
- package/src/renderer/contexts/UIContext.tsx +326 -0
- package/src/renderer/contexts/index.ts +24 -0
- package/src/renderer/donateMessages.ts +69 -0
- package/src/renderer/hooks/index.ts +75 -0
- package/src/renderer/hooks/useAnimation.tsx +544 -0
- package/src/renderer/hooks/useTheme.ts +313 -0
- package/src/renderer/index.html +26 -0
- package/src/renderer/main.tsx +52 -0
- package/src/renderer/styles/animations.css +1093 -0
- package/src/renderer/styles/app-shell.css +662 -0
- package/src/renderer/styles/globals.css +515 -0
- package/src/renderer/styles/theme.ts +578 -0
- package/src/renderer/types/electron.d.ts +385 -0
- package/src/shared/hotkeys.ts +283 -0
- package/src/shared/types.ts +809 -0
- package/tests/clipboard.test.ts +228 -0
- package/tests/e2e/criticalPaths.test.ts +594 -0
- package/tests/feedbackAnalyzer.test.ts +303 -0
- package/tests/integration/sessionFlow.test.ts +583 -0
- package/tests/markdownGenerator.test.ts +418 -0
- package/tests/output.test.ts +96 -0
- package/tests/setup.ts +486 -0
- package/tests/unit/appIntegration.test.ts +676 -0
- package/tests/unit/appViewState.test.ts +281 -0
- package/tests/unit/audioIpcChannels.test.ts +17 -0
- package/tests/unit/exportService.test.ts +492 -0
- package/tests/unit/hotkeys.test.ts +92 -0
- package/tests/unit/navigationPreload.test.ts +94 -0
- package/tests/unit/onboardingFlow.test.ts +345 -0
- package/tests/unit/permissionManager.test.ts +175 -0
- package/tests/unit/permissionManagerExpanded.test.ts +296 -0
- package/tests/unit/screenRecordingRenderer.test.ts +368 -0
- package/tests/unit/sessionController.test.ts +515 -0
- package/tests/unit/tierManager.test.ts +61 -0
- package/tests/unit/tierManagerExpanded.test.ts +142 -0
- package/tests/unit/transcriptAnalyzer.test.ts +64 -0
- package/tsconfig.json +25 -0
- package/vitest.config.ts +46 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostProcessor.ts - Post-Recording Pipeline Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Runs the full post-processing pipeline after a recording session stops:
|
|
5
|
+
* 1. Transcribe audio via WhisperService batch mode
|
|
6
|
+
* 2. Analyze transcript to find key moments (heuristic-based)
|
|
7
|
+
* 3. Extract video frames at those timestamps via ffmpeg
|
|
8
|
+
* 4. Return combined result for markdown report generation
|
|
9
|
+
*
|
|
10
|
+
* Designed to degrade gracefully: if frame extraction fails (e.g. ffmpeg
|
|
11
|
+
* not installed), the pipeline still returns a transcript-only result.
|
|
12
|
+
*
|
|
13
|
+
* Part of the post-processing pipeline that runs after recording stops.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { whisperService } from '../transcription/WhisperService';
|
|
17
|
+
import { TranscriptAnalyzer, transcriptAnalyzer } from './TranscriptAnalyzer';
|
|
18
|
+
import { FrameExtractor, frameExtractor } from './FrameExtractor';
|
|
19
|
+
import type { KeyMoment } from './TranscriptAnalyzer';
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Types
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
export interface TranscriptSegment {
|
|
26
|
+
text: string;
|
|
27
|
+
startTime: number; // seconds from start of recording
|
|
28
|
+
endTime: number;
|
|
29
|
+
confidence: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ExtractedFrame {
|
|
33
|
+
path: string; // path to extracted PNG
|
|
34
|
+
timestamp: number; // seconds from start of recording
|
|
35
|
+
reason: string; // why this frame was selected
|
|
36
|
+
transcriptSegment?: TranscriptSegment; // associated transcript segment
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PostProcessResult {
|
|
40
|
+
transcriptSegments: TranscriptSegment[];
|
|
41
|
+
extractedFrames: ExtractedFrame[];
|
|
42
|
+
reportPath: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface PostProcessProgress {
|
|
46
|
+
step: 'transcribing' | 'analyzing' | 'extracting-frames' | 'generating-report';
|
|
47
|
+
percent: number;
|
|
48
|
+
message: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PostProcessOptions {
|
|
52
|
+
videoPath: string;
|
|
53
|
+
audioPath: string;
|
|
54
|
+
sessionDir: string;
|
|
55
|
+
transcriptSegments?: TranscriptSegment[];
|
|
56
|
+
aiMomentHints?: KeyMoment[];
|
|
57
|
+
onProgress?: (progress: PostProcessProgress) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// PostProcessor Class
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
export class PostProcessor {
|
|
65
|
+
private analyzer: TranscriptAnalyzer;
|
|
66
|
+
private extractor: FrameExtractor;
|
|
67
|
+
|
|
68
|
+
constructor(
|
|
69
|
+
analyzer: TranscriptAnalyzer = transcriptAnalyzer,
|
|
70
|
+
extractor: FrameExtractor = frameExtractor
|
|
71
|
+
) {
|
|
72
|
+
this.analyzer = analyzer;
|
|
73
|
+
this.extractor = extractor;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run the full post-processing pipeline.
|
|
78
|
+
*
|
|
79
|
+
* @param options - Pipeline configuration (paths, progress callback)
|
|
80
|
+
* @returns Combined result with transcript, frames, and report path
|
|
81
|
+
*/
|
|
82
|
+
async process(options: PostProcessOptions): Promise<PostProcessResult> {
|
|
83
|
+
const { videoPath, audioPath, sessionDir, transcriptSegments, aiMomentHints, onProgress } = options;
|
|
84
|
+
|
|
85
|
+
const emitProgress = (progress: PostProcessProgress): void => {
|
|
86
|
+
if (onProgress) {
|
|
87
|
+
onProgress(progress);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// -----------------------------------------------------------------------
|
|
92
|
+
// Step 1: Transcribe audio (0-40%)
|
|
93
|
+
// -----------------------------------------------------------------------
|
|
94
|
+
emitProgress({
|
|
95
|
+
step: 'transcribing',
|
|
96
|
+
percent: 0,
|
|
97
|
+
message: 'Transcribing audio...',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
let segments: TranscriptSegment[] = [];
|
|
101
|
+
const providedSegments = (transcriptSegments || [])
|
|
102
|
+
.map((segment) => ({
|
|
103
|
+
text: segment.text?.trim() || '',
|
|
104
|
+
startTime: Number.isFinite(segment.startTime) ? Math.max(0, segment.startTime) : 0,
|
|
105
|
+
endTime: Number.isFinite(segment.endTime)
|
|
106
|
+
? Math.max(0, segment.endTime)
|
|
107
|
+
: (Number.isFinite(segment.startTime) ? Math.max(0, segment.startTime) + 1.5 : 1.5),
|
|
108
|
+
confidence: Number.isFinite(segment.confidence) ? segment.confidence : 0.8,
|
|
109
|
+
}))
|
|
110
|
+
.filter((segment) => segment.text.length > 0);
|
|
111
|
+
|
|
112
|
+
// Strategy: prefer Whisper file-based transcription when available because
|
|
113
|
+
// it produces higher quality output than the live-streamed segments captured
|
|
114
|
+
// during recording. Fall back to pre-provided segments only when Whisper is
|
|
115
|
+
// unavailable or fails.
|
|
116
|
+
const whisperAvailable = audioPath && whisperService.isModelAvailable();
|
|
117
|
+
|
|
118
|
+
if (whisperAvailable) {
|
|
119
|
+
this.log(
|
|
120
|
+
`Whisper model available, attempting file-based transcription` +
|
|
121
|
+
(providedSegments.length > 0
|
|
122
|
+
? ` (${providedSegments.length} pre-provided segments available as fallback)`
|
|
123
|
+
: '')
|
|
124
|
+
);
|
|
125
|
+
try {
|
|
126
|
+
emitProgress({
|
|
127
|
+
step: 'transcribing',
|
|
128
|
+
percent: 5,
|
|
129
|
+
message: 'Loading Whisper model...',
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const whisperResults = await whisperService.transcribeFile(audioPath);
|
|
133
|
+
|
|
134
|
+
segments = whisperResults.map((result) => ({
|
|
135
|
+
text: result.text,
|
|
136
|
+
startTime: result.startTime,
|
|
137
|
+
endTime: result.endTime,
|
|
138
|
+
confidence: result.confidence,
|
|
139
|
+
}));
|
|
140
|
+
|
|
141
|
+
if (segments.length > 0) {
|
|
142
|
+
emitProgress({
|
|
143
|
+
step: 'transcribing',
|
|
144
|
+
percent: 40,
|
|
145
|
+
message: `Whisper transcription complete: ${segments.length} segments`,
|
|
146
|
+
});
|
|
147
|
+
this.log(`Whisper file transcription complete: ${segments.length} segments`);
|
|
148
|
+
} else {
|
|
149
|
+
this.log('Whisper returned 0 segments, will try pre-provided segments as fallback');
|
|
150
|
+
}
|
|
151
|
+
} catch (error) {
|
|
152
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
153
|
+
this.log(`Whisper file transcription failed: ${message}`);
|
|
154
|
+
// Fall through to use pre-provided segments below
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
this.log(
|
|
158
|
+
`Whisper file transcription skipped: ${!audioPath ? 'no audio file path' : 'model not available'}`
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fall back to pre-provided segments if Whisper did not produce results
|
|
163
|
+
if (segments.length === 0 && providedSegments.length > 0) {
|
|
164
|
+
segments = providedSegments.sort((a, b) => a.startTime - b.startTime);
|
|
165
|
+
emitProgress({
|
|
166
|
+
step: 'transcribing',
|
|
167
|
+
percent: 40,
|
|
168
|
+
message: `Using captured transcript (${segments.length} segments)`,
|
|
169
|
+
});
|
|
170
|
+
this.log(`Using pre-provided transcript segments as fallback: ${segments.length}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// If neither Whisper nor pre-provided segments produced anything, and we
|
|
174
|
+
// have an audio path but Whisper was not available, attempt Whisper anyway
|
|
175
|
+
// as a last resort (it will throw if the model truly cannot load).
|
|
176
|
+
if (segments.length === 0 && audioPath && !whisperAvailable) {
|
|
177
|
+
try {
|
|
178
|
+
emitProgress({
|
|
179
|
+
step: 'transcribing',
|
|
180
|
+
percent: 5,
|
|
181
|
+
message: 'Attempting transcription...',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const whisperResults = await whisperService.transcribeFile(audioPath);
|
|
185
|
+
segments = whisperResults.map((result) => ({
|
|
186
|
+
text: result.text,
|
|
187
|
+
startTime: result.startTime,
|
|
188
|
+
endTime: result.endTime,
|
|
189
|
+
confidence: result.confidence,
|
|
190
|
+
}));
|
|
191
|
+
|
|
192
|
+
if (segments.length > 0) {
|
|
193
|
+
emitProgress({
|
|
194
|
+
step: 'transcribing',
|
|
195
|
+
percent: 40,
|
|
196
|
+
message: `Transcription complete: ${segments.length} segments`,
|
|
197
|
+
});
|
|
198
|
+
this.log(`Whisper last-resort transcription complete: ${segments.length} segments`);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
202
|
+
this.log(`Whisper last-resort transcription failed: ${message}`);
|
|
203
|
+
|
|
204
|
+
// No segments from any source - return empty result
|
|
205
|
+
return {
|
|
206
|
+
transcriptSegments: [],
|
|
207
|
+
extractedFrames: [],
|
|
208
|
+
reportPath: sessionDir,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (segments.length === 0) {
|
|
214
|
+
this.log('No transcript segments found, returning empty result');
|
|
215
|
+
emitProgress({
|
|
216
|
+
step: 'generating-report',
|
|
217
|
+
percent: 100,
|
|
218
|
+
message: 'No speech detected in recording',
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
transcriptSegments: [],
|
|
223
|
+
extractedFrames: [],
|
|
224
|
+
reportPath: sessionDir,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// -----------------------------------------------------------------------
|
|
229
|
+
// Step 2: Analyze transcript for key moments (40-50%)
|
|
230
|
+
// -----------------------------------------------------------------------
|
|
231
|
+
emitProgress({
|
|
232
|
+
step: 'analyzing',
|
|
233
|
+
percent: 40,
|
|
234
|
+
message: 'Analyzing transcript for key moments...',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const keyMoments = this.analyzer.analyze(segments, aiMomentHints || []);
|
|
238
|
+
|
|
239
|
+
emitProgress({
|
|
240
|
+
step: 'analyzing',
|
|
241
|
+
percent: 50,
|
|
242
|
+
message: `Found ${keyMoments.length} key moments`,
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
this.log(`Analysis complete: ${keyMoments.length} key moments identified`);
|
|
246
|
+
|
|
247
|
+
// -----------------------------------------------------------------------
|
|
248
|
+
// Step 3: Extract frames from video (50-90%)
|
|
249
|
+
// -----------------------------------------------------------------------
|
|
250
|
+
emitProgress({
|
|
251
|
+
step: 'extracting-frames',
|
|
252
|
+
percent: 50,
|
|
253
|
+
message: 'Extracting video frames...',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
let extractedFrames: ExtractedFrame[] = [];
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
const timestamps = keyMoments.map((m) => m.timestamp);
|
|
260
|
+
|
|
261
|
+
const extractionResult = await this.extractor.extract({
|
|
262
|
+
videoPath,
|
|
263
|
+
timestamps,
|
|
264
|
+
outputDir: sessionDir,
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
if (!extractionResult.ffmpegAvailable) {
|
|
268
|
+
this.log('ffmpeg not available, skipping frame extraction');
|
|
269
|
+
emitProgress({
|
|
270
|
+
step: 'extracting-frames',
|
|
271
|
+
percent: 90,
|
|
272
|
+
message: 'Frame extraction skipped (ffmpeg not installed)',
|
|
273
|
+
});
|
|
274
|
+
} else {
|
|
275
|
+
// Map extraction results to ExtractedFrame with associated transcript segments
|
|
276
|
+
extractedFrames = extractionResult.frames
|
|
277
|
+
.filter((f) => f.success)
|
|
278
|
+
.map((frame) => {
|
|
279
|
+
// Find the key moment that corresponds to this frame
|
|
280
|
+
const moment = keyMoments.find(
|
|
281
|
+
(m) => Math.abs(m.timestamp - frame.timestamp) < 0.5
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Find the closest transcript segment
|
|
285
|
+
const closestSegment = this.findClosestSegment(
|
|
286
|
+
frame.timestamp,
|
|
287
|
+
segments
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
path: frame.path,
|
|
292
|
+
timestamp: frame.timestamp,
|
|
293
|
+
reason: moment?.reason ?? 'Extracted frame',
|
|
294
|
+
transcriptSegment: closestSegment,
|
|
295
|
+
};
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
emitProgress({
|
|
299
|
+
step: 'extracting-frames',
|
|
300
|
+
percent: 90,
|
|
301
|
+
message: `Extracted ${extractedFrames.length} frames`,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
this.log(`Frame extraction complete: ${extractedFrames.length} frames`);
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
this.log(`Frame extraction failed: ${message} - continuing with transcript only`);
|
|
309
|
+
|
|
310
|
+
emitProgress({
|
|
311
|
+
step: 'extracting-frames',
|
|
312
|
+
percent: 90,
|
|
313
|
+
message: 'Frame extraction failed, continuing with transcript only',
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Continue with transcript-only result - do not throw
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// -----------------------------------------------------------------------
|
|
320
|
+
// Step 4: Return result (90-100%)
|
|
321
|
+
// -----------------------------------------------------------------------
|
|
322
|
+
emitProgress({
|
|
323
|
+
step: 'generating-report',
|
|
324
|
+
percent: 90,
|
|
325
|
+
message: 'Preparing results...',
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const result: PostProcessResult = {
|
|
329
|
+
transcriptSegments: segments,
|
|
330
|
+
extractedFrames,
|
|
331
|
+
reportPath: sessionDir,
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
emitProgress({
|
|
335
|
+
step: 'generating-report',
|
|
336
|
+
percent: 100,
|
|
337
|
+
message: 'Post-processing complete',
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
this.log(
|
|
341
|
+
`Pipeline complete: ${segments.length} segments, ${extractedFrames.length} frames`
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ============================================================================
|
|
348
|
+
// Private Methods
|
|
349
|
+
// ============================================================================
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Find the transcript segment closest to a given timestamp.
|
|
353
|
+
* Prefers segments that contain the timestamp; falls back to nearest start time.
|
|
354
|
+
*/
|
|
355
|
+
private findClosestSegment(
|
|
356
|
+
timestamp: number,
|
|
357
|
+
segments: TranscriptSegment[]
|
|
358
|
+
): TranscriptSegment | undefined {
|
|
359
|
+
if (segments.length === 0) {
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// First, check if timestamp falls within any segment
|
|
364
|
+
for (const segment of segments) {
|
|
365
|
+
if (timestamp >= segment.startTime && timestamp <= segment.endTime) {
|
|
366
|
+
return segment;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Otherwise, find the closest segment by start time
|
|
371
|
+
let closest = segments[0];
|
|
372
|
+
let minDistance = Math.abs(timestamp - closest.startTime);
|
|
373
|
+
|
|
374
|
+
for (let i = 1; i < segments.length; i++) {
|
|
375
|
+
const distance = Math.abs(timestamp - segments[i].startTime);
|
|
376
|
+
if (distance < minDistance) {
|
|
377
|
+
minDistance = distance;
|
|
378
|
+
closest = segments[i];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return closest;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Log helper with consistent prefix.
|
|
387
|
+
*/
|
|
388
|
+
private log(message: string): void {
|
|
389
|
+
const timestamp = new Date().toISOString();
|
|
390
|
+
console.log(`[PostProcessor ${timestamp}] ${message}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ============================================================================
|
|
395
|
+
// Singleton Export
|
|
396
|
+
// ============================================================================
|
|
397
|
+
|
|
398
|
+
export const postProcessor = new PostProcessor();
|
|
399
|
+
export default PostProcessor;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TranscriptAnalyzer.ts - Heuristic Key Moment Detection
|
|
3
|
+
*
|
|
4
|
+
* Analyzes transcript segments to identify moments where video frames
|
|
5
|
+
* should be extracted. Uses local heuristics (no AI required):
|
|
6
|
+
*
|
|
7
|
+
* - Natural pauses > 1.5s between segments
|
|
8
|
+
* - Periodic baseline captures every 15-20 seconds
|
|
9
|
+
* - Session start and end always included
|
|
10
|
+
*
|
|
11
|
+
* Part of the post-processing pipeline that runs after recording stops.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { TranscriptSegment } from './PostProcessor';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export interface KeyMoment {
|
|
21
|
+
timestamp: number; // seconds from start of recording
|
|
22
|
+
reason: string; // human-readable reason for selection
|
|
23
|
+
confidence: number; // 0-1
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ============================================================================
|
|
27
|
+
// Constants
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
/** Minimum gap between segments to count as a natural pause */
|
|
31
|
+
const PAUSE_THRESHOLD_SECONDS = 1.5;
|
|
32
|
+
|
|
33
|
+
/** Baseline periodic capture interval when no pauses are found */
|
|
34
|
+
const PERIODIC_INTERVAL_SECONDS = 15;
|
|
35
|
+
|
|
36
|
+
/** Maximum periodic interval to avoid sparse captures */
|
|
37
|
+
const MAX_PERIODIC_INTERVAL_SECONDS = 20;
|
|
38
|
+
|
|
39
|
+
/** Hard cap on returned key moments */
|
|
40
|
+
const MAX_KEY_MOMENTS = 20;
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// TranscriptAnalyzer Class
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
export class TranscriptAnalyzer {
|
|
47
|
+
/**
|
|
48
|
+
* Analyze transcript segments and return key moments where frames
|
|
49
|
+
* should be extracted from the video recording.
|
|
50
|
+
*
|
|
51
|
+
* @param segments - Array of transcript segments with timing info
|
|
52
|
+
* @param aiHints - Optional AI-informed key-moment hints to merge
|
|
53
|
+
* @returns Array of key moments sorted by timestamp, capped at 20
|
|
54
|
+
*/
|
|
55
|
+
analyze(segments: TranscriptSegment[], aiHints: KeyMoment[] = []): KeyMoment[] {
|
|
56
|
+
if (segments.length === 0) {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const moments: KeyMoment[] = [];
|
|
61
|
+
|
|
62
|
+
// Always include session start
|
|
63
|
+
const firstSegment = segments[0];
|
|
64
|
+
moments.push({
|
|
65
|
+
timestamp: firstSegment.startTime,
|
|
66
|
+
reason: 'Session start',
|
|
67
|
+
confidence: 1.0,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Detect natural pauses between segments
|
|
71
|
+
for (let i = 1; i < segments.length; i++) {
|
|
72
|
+
const prev = segments[i - 1];
|
|
73
|
+
const curr = segments[i];
|
|
74
|
+
const gap = curr.startTime - prev.endTime;
|
|
75
|
+
|
|
76
|
+
if (gap >= PAUSE_THRESHOLD_SECONDS) {
|
|
77
|
+
// Place the key moment at the start of the pause (end of previous segment)
|
|
78
|
+
moments.push({
|
|
79
|
+
timestamp: prev.endTime,
|
|
80
|
+
reason: 'Natural pause in narration',
|
|
81
|
+
confidence: Math.min(1.0, gap / 3.0), // Longer pauses = higher confidence
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Always include session end
|
|
87
|
+
const lastSegment = segments[segments.length - 1];
|
|
88
|
+
if (lastSegment.endTime !== firstSegment.startTime) {
|
|
89
|
+
moments.push({
|
|
90
|
+
timestamp: lastSegment.endTime,
|
|
91
|
+
reason: 'Session end',
|
|
92
|
+
confidence: 1.0,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If we found fewer than 3 moments (just start/end), add periodic baseline captures.
|
|
97
|
+
// When AI hints exist, prefer them over periodic filler captures.
|
|
98
|
+
if (moments.length < 3 && aiHints.length === 0) {
|
|
99
|
+
const sessionStart = firstSegment.startTime;
|
|
100
|
+
const sessionEnd = lastSegment.endTime;
|
|
101
|
+
const sessionDuration = sessionEnd - sessionStart;
|
|
102
|
+
|
|
103
|
+
if (sessionDuration > PERIODIC_INTERVAL_SECONDS) {
|
|
104
|
+
// Calculate interval: target 15s but stretch up to 20s to avoid one extra capture
|
|
105
|
+
const rawCount = Math.floor(sessionDuration / PERIODIC_INTERVAL_SECONDS);
|
|
106
|
+
const interval = Math.min(
|
|
107
|
+
sessionDuration / rawCount,
|
|
108
|
+
MAX_PERIODIC_INTERVAL_SECONDS
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
for (
|
|
112
|
+
let t = sessionStart + interval;
|
|
113
|
+
t < sessionEnd;
|
|
114
|
+
t += interval
|
|
115
|
+
) {
|
|
116
|
+
moments.push({
|
|
117
|
+
timestamp: t,
|
|
118
|
+
reason: 'Periodic capture',
|
|
119
|
+
confidence: 0.5,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Merge AI hints (when available) so frame extraction can prioritize
|
|
126
|
+
// narration moments that the analysis pipeline considered important.
|
|
127
|
+
for (const hint of aiHints) {
|
|
128
|
+
if (!Number.isFinite(hint.timestamp)) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
moments.push({
|
|
132
|
+
timestamp: Math.max(0, hint.timestamp),
|
|
133
|
+
reason: hint.reason?.trim() || 'AI-highlighted context',
|
|
134
|
+
confidence: Math.max(0, Math.min(1, Number.isFinite(hint.confidence) ? hint.confidence : 0.8)),
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Deduplicate moments that are very close together (within 1 second)
|
|
139
|
+
const deduped = this.deduplicateMoments(moments);
|
|
140
|
+
|
|
141
|
+
// Sort by timestamp
|
|
142
|
+
deduped.sort((a, b) => a.timestamp - b.timestamp);
|
|
143
|
+
|
|
144
|
+
// Cap at MAX_KEY_MOMENTS, keeping highest confidence ones
|
|
145
|
+
if (deduped.length > MAX_KEY_MOMENTS) {
|
|
146
|
+
// Always keep first and last; rank the rest by confidence
|
|
147
|
+
const first = deduped[0];
|
|
148
|
+
const last = deduped[deduped.length - 1];
|
|
149
|
+
const middle = deduped
|
|
150
|
+
.slice(1, -1)
|
|
151
|
+
.sort((a, b) => {
|
|
152
|
+
const priorityDelta = this.momentPriority(b) - this.momentPriority(a);
|
|
153
|
+
if (priorityDelta !== 0) {
|
|
154
|
+
return priorityDelta;
|
|
155
|
+
}
|
|
156
|
+
return b.confidence - a.confidence;
|
|
157
|
+
})
|
|
158
|
+
.slice(0, MAX_KEY_MOMENTS - 2);
|
|
159
|
+
|
|
160
|
+
const capped = [first, ...middle, last];
|
|
161
|
+
capped.sort((a, b) => a.timestamp - b.timestamp);
|
|
162
|
+
return capped;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return deduped;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Remove moments that are within 1 second of each other,
|
|
170
|
+
* keeping the one with higher confidence.
|
|
171
|
+
*/
|
|
172
|
+
private deduplicateMoments(moments: KeyMoment[]): KeyMoment[] {
|
|
173
|
+
if (moments.length <= 1) {
|
|
174
|
+
return moments;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Sort by timestamp first for grouping
|
|
178
|
+
const sorted = [...moments].sort((a, b) => a.timestamp - b.timestamp);
|
|
179
|
+
const result: KeyMoment[] = [sorted[0]];
|
|
180
|
+
|
|
181
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
182
|
+
const prev = result[result.length - 1];
|
|
183
|
+
const curr = sorted[i];
|
|
184
|
+
|
|
185
|
+
if (curr.timestamp - prev.timestamp < 1.0) {
|
|
186
|
+
// Prefer higher-priority moments (AI / semantic captures over periodic).
|
|
187
|
+
const currPriority = this.momentPriority(curr);
|
|
188
|
+
const prevPriority = this.momentPriority(prev);
|
|
189
|
+
if (
|
|
190
|
+
currPriority > prevPriority ||
|
|
191
|
+
(currPriority === prevPriority && curr.confidence > prev.confidence)
|
|
192
|
+
) {
|
|
193
|
+
result[result.length - 1] = curr;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
result.push(curr);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return result;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
private momentPriority(moment: KeyMoment): number {
|
|
204
|
+
const reason = (moment.reason || '').toLowerCase();
|
|
205
|
+
if (reason.includes('session start') || reason.includes('session end')) {
|
|
206
|
+
return 4;
|
|
207
|
+
}
|
|
208
|
+
if (reason.includes('ai-') || reason.includes('ai ')) {
|
|
209
|
+
return 3;
|
|
210
|
+
}
|
|
211
|
+
if (reason.includes('natural pause')) {
|
|
212
|
+
return 2;
|
|
213
|
+
}
|
|
214
|
+
if (reason.includes('periodic')) {
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
return 1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ============================================================================
|
|
222
|
+
// Singleton Export
|
|
223
|
+
// ============================================================================
|
|
224
|
+
|
|
225
|
+
export const transcriptAnalyzer = new TranscriptAnalyzer();
|
|
226
|
+
export default TranscriptAnalyzer;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Module - Post-Processing Pipeline
|
|
3
|
+
*
|
|
4
|
+
* After recording stops, this pipeline:
|
|
5
|
+
* 1. Transcribes audio via WhisperService (batch mode)
|
|
6
|
+
* 2. Analyzes transcript for key moments (heuristic-based)
|
|
7
|
+
* 3. Extracts video frames at those timestamps (via ffmpeg)
|
|
8
|
+
* 4. Returns structured data for markdown report generation
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Classes & Singletons
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
export { PostProcessor, postProcessor } from './PostProcessor';
|
|
16
|
+
export { FrameExtractor, frameExtractor } from './FrameExtractor';
|
|
17
|
+
export { TranscriptAnalyzer, transcriptAnalyzer } from './TranscriptAnalyzer';
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export type {
|
|
24
|
+
PostProcessResult,
|
|
25
|
+
TranscriptSegment,
|
|
26
|
+
ExtractedFrame,
|
|
27
|
+
PostProcessProgress,
|
|
28
|
+
PostProcessOptions,
|
|
29
|
+
} from './PostProcessor';
|
|
30
|
+
|
|
31
|
+
export type {
|
|
32
|
+
FrameExtractionRequest,
|
|
33
|
+
FrameExtractionResult,
|
|
34
|
+
} from './FrameExtractor';
|
|
35
|
+
|
|
36
|
+
export type { KeyMoment } from './TranscriptAnalyzer';
|