karaoke-gen 0.57.0__py3-none-any.whl → 0.71.23__py3-none-any.whl
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.
- karaoke_gen/audio_fetcher.py +461 -0
- karaoke_gen/audio_processor.py +407 -30
- karaoke_gen/config.py +62 -113
- karaoke_gen/file_handler.py +32 -59
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +148 -67
- karaoke_gen/karaoke_gen.py +270 -61
- karaoke_gen/lyrics_processor.py +13 -1
- karaoke_gen/metadata.py +78 -73
- karaoke_gen/pipeline/__init__.py +87 -0
- karaoke_gen/pipeline/base.py +215 -0
- karaoke_gen/pipeline/context.py +230 -0
- karaoke_gen/pipeline/executors/__init__.py +21 -0
- karaoke_gen/pipeline/executors/local.py +159 -0
- karaoke_gen/pipeline/executors/remote.py +257 -0
- karaoke_gen/pipeline/stages/__init__.py +27 -0
- karaoke_gen/pipeline/stages/finalize.py +202 -0
- karaoke_gen/pipeline/stages/render.py +165 -0
- karaoke_gen/pipeline/stages/screens.py +139 -0
- karaoke_gen/pipeline/stages/separation.py +191 -0
- karaoke_gen/pipeline/stages/transcription.py +191 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/bulk_cli.py +6 -0
- karaoke_gen/utils/cli_args.py +424 -0
- karaoke_gen/utils/gen_cli.py +26 -261
- karaoke_gen/utils/remote_cli.py +1815 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.23.dist-info/METADATA +610 -0
- karaoke_gen-0.71.23.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info}/entry_points.txt +1 -0
- lyrics_transcriber/__init__.py +10 -0
- lyrics_transcriber/cli/__init__.py +0 -0
- lyrics_transcriber/cli/cli_main.py +285 -0
- lyrics_transcriber/core/__init__.py +0 -0
- lyrics_transcriber/core/config.py +50 -0
- lyrics_transcriber/core/controller.py +520 -0
- lyrics_transcriber/correction/__init__.py +0 -0
- lyrics_transcriber/correction/agentic/__init__.py +9 -0
- lyrics_transcriber/correction/agentic/adapter.py +71 -0
- lyrics_transcriber/correction/agentic/agent.py +313 -0
- lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
- lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
- lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
- lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
- lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
- lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
- lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
- lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
- lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
- lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
- lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
- lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
- lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
- lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
- lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
- lyrics_transcriber/correction/agentic/models/enums.py +38 -0
- lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
- lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
- lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
- lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
- lyrics_transcriber/correction/agentic/models/utils.py +19 -0
- lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
- lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
- lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
- lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
- lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
- lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
- lyrics_transcriber/correction/agentic/providers/base.py +36 -0
- lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
- lyrics_transcriber/correction/agentic/providers/config.py +73 -0
- lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
- lyrics_transcriber/correction/agentic/providers/health.py +28 -0
- lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
- lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
- lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
- lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
- lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
- lyrics_transcriber/correction/agentic/router.py +35 -0
- lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
- lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
- lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
- lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
- lyrics_transcriber/correction/anchor_sequence.py +1043 -0
- lyrics_transcriber/correction/corrector.py +760 -0
- lyrics_transcriber/correction/feedback/__init__.py +2 -0
- lyrics_transcriber/correction/feedback/schemas.py +107 -0
- lyrics_transcriber/correction/feedback/store.py +236 -0
- lyrics_transcriber/correction/handlers/__init__.py +0 -0
- lyrics_transcriber/correction/handlers/base.py +52 -0
- lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
- lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
- lyrics_transcriber/correction/handlers/llm.py +293 -0
- lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
- lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
- lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
- lyrics_transcriber/correction/handlers/repeat.py +88 -0
- lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
- lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
- lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
- lyrics_transcriber/correction/handlers/word_operations.py +187 -0
- lyrics_transcriber/correction/operations.py +352 -0
- lyrics_transcriber/correction/phrase_analyzer.py +435 -0
- lyrics_transcriber/correction/text_utils.py +30 -0
- lyrics_transcriber/frontend/.gitignore +23 -0
- lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
- lyrics_transcriber/frontend/.yarnrc.yml +3 -0
- lyrics_transcriber/frontend/README.md +50 -0
- lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
- lyrics_transcriber/frontend/__init__.py +25 -0
- lyrics_transcriber/frontend/eslint.config.js +28 -0
- lyrics_transcriber/frontend/index.html +18 -0
- lyrics_transcriber/frontend/package.json +42 -0
- lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/public/favicon.ico +0 -0
- lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/src/App.tsx +212 -0
- lyrics_transcriber/frontend/src/api.ts +239 -0
- lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
- lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
- lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
- lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
- lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
- lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
- lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
- lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
- lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
- lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
- lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
- lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
- lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
- lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
- lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
- lyrics_transcriber/frontend/src/components/Header.tsx +387 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1373 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
- lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
- lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
- lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
- lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +688 -0
- lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
- lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
- lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
- lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
- lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
- lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
- lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
- lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
- lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
- lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
- lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
- lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
- lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
- lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
- lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
- lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
- lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
- lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
- lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
- lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
- lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
- lyrics_transcriber/frontend/src/main.tsx +17 -0
- lyrics_transcriber/frontend/src/theme.ts +177 -0
- lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
- lyrics_transcriber/frontend/src/types.js +2 -0
- lyrics_transcriber/frontend/src/types.ts +199 -0
- lyrics_transcriber/frontend/src/validation.ts +132 -0
- lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
- lyrics_transcriber/frontend/tsconfig.app.json +26 -0
- lyrics_transcriber/frontend/tsconfig.json +25 -0
- lyrics_transcriber/frontend/tsconfig.node.json +23 -0
- lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
- lyrics_transcriber/frontend/update_version.js +11 -0
- lyrics_transcriber/frontend/vite.config.d.ts +2 -0
- lyrics_transcriber/frontend/vite.config.js +10 -0
- lyrics_transcriber/frontend/vite.config.ts +11 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
- lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
- lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js +42039 -0
- lyrics_transcriber/frontend/web_assets/assets/index-DdJTDWH3.js.map +1 -0
- lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
- lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
- lyrics_transcriber/frontend/web_assets/index.html +18 -0
- lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
- lyrics_transcriber/frontend/yarn.lock +3752 -0
- lyrics_transcriber/lyrics/__init__.py +0 -0
- lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
- lyrics_transcriber/lyrics/file_provider.py +95 -0
- lyrics_transcriber/lyrics/genius.py +384 -0
- lyrics_transcriber/lyrics/lrclib.py +231 -0
- lyrics_transcriber/lyrics/musixmatch.py +156 -0
- lyrics_transcriber/lyrics/spotify.py +290 -0
- lyrics_transcriber/lyrics/user_input_provider.py +44 -0
- lyrics_transcriber/output/__init__.py +0 -0
- lyrics_transcriber/output/ass/__init__.py +21 -0
- lyrics_transcriber/output/ass/ass.py +2088 -0
- lyrics_transcriber/output/ass/ass_specs.txt +732 -0
- lyrics_transcriber/output/ass/config.py +180 -0
- lyrics_transcriber/output/ass/constants.py +23 -0
- lyrics_transcriber/output/ass/event.py +94 -0
- lyrics_transcriber/output/ass/formatters.py +132 -0
- lyrics_transcriber/output/ass/lyrics_line.py +265 -0
- lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
- lyrics_transcriber/output/ass/section_detector.py +89 -0
- lyrics_transcriber/output/ass/section_screen.py +106 -0
- lyrics_transcriber/output/ass/style.py +187 -0
- lyrics_transcriber/output/cdg.py +619 -0
- lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
- lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
- lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
- lyrics_transcriber/output/cdgmaker/config.py +151 -0
- lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
- lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
- lyrics_transcriber/output/cdgmaker/pack.py +507 -0
- lyrics_transcriber/output/cdgmaker/render.py +346 -0
- lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
- lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
- lyrics_transcriber/output/cdgmaker/utils.py +132 -0
- lyrics_transcriber/output/countdown_processor.py +267 -0
- lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
- lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
- lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
- lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
- lyrics_transcriber/output/fonts/arial.ttf +0 -0
- lyrics_transcriber/output/fonts/georgia.ttf +0 -0
- lyrics_transcriber/output/fonts/verdana.ttf +0 -0
- lyrics_transcriber/output/generator.py +257 -0
- lyrics_transcriber/output/lrc_to_cdg.py +61 -0
- lyrics_transcriber/output/lyrics_file.py +102 -0
- lyrics_transcriber/output/plain_text.py +96 -0
- lyrics_transcriber/output/segment_resizer.py +431 -0
- lyrics_transcriber/output/subtitles.py +397 -0
- lyrics_transcriber/output/video.py +544 -0
- lyrics_transcriber/review/__init__.py +0 -0
- lyrics_transcriber/review/server.py +676 -0
- lyrics_transcriber/storage/__init__.py +0 -0
- lyrics_transcriber/storage/dropbox.py +225 -0
- lyrics_transcriber/transcribers/__init__.py +0 -0
- lyrics_transcriber/transcribers/audioshake.py +290 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +648 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
- karaoke_gen-0.57.0.dist-info/METADATA +0 -167
- karaoke_gen-0.57.0.dist-info/RECORD +0 -23
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.23.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { CorrectionData, CorrectionAnnotation } from './types';
|
|
2
|
+
import { validateCorrectionData } from './validation';
|
|
3
|
+
|
|
4
|
+
// New file to handle API communication
|
|
5
|
+
export interface ApiClient {
|
|
6
|
+
getCorrectionData: () => Promise<CorrectionData>;
|
|
7
|
+
submitCorrections: (data: CorrectionData) => Promise<void>;
|
|
8
|
+
getAudioUrl: (audioHash: string) => string;
|
|
9
|
+
generatePreviewVideo: (data: CorrectionData) => Promise<PreviewVideoResponse>;
|
|
10
|
+
getPreviewVideoUrl: (previewHash: string) => string;
|
|
11
|
+
updateHandlers: (enabledHandlers: string[]) => Promise<CorrectionData>;
|
|
12
|
+
isUpdatingHandlers?: boolean;
|
|
13
|
+
addLyrics: (source: string, lyrics: string) => Promise<CorrectionData>;
|
|
14
|
+
submitAnnotations: (annotations: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]) => Promise<void>;
|
|
15
|
+
getAnnotationStats: () => Promise<any>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Add new interface for the minimal update payload
|
|
19
|
+
interface CorrectionUpdate {
|
|
20
|
+
corrections: CorrectionData['corrections'];
|
|
21
|
+
corrected_segments: CorrectionData['corrected_segments'];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Add new interface for preview response
|
|
25
|
+
interface PreviewVideoResponse {
|
|
26
|
+
status: "success" | "error";
|
|
27
|
+
preview_hash?: string;
|
|
28
|
+
message?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add new interface for adding lyrics
|
|
32
|
+
interface AddLyricsRequest {
|
|
33
|
+
source: string;
|
|
34
|
+
lyrics: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class LiveApiClient implements ApiClient {
|
|
38
|
+
constructor(private baseUrl: string) {
|
|
39
|
+
this.baseUrl = baseUrl.replace(/\/$/, '')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public isUpdatingHandlers = false;
|
|
43
|
+
|
|
44
|
+
async getCorrectionData(): Promise<CorrectionData> {
|
|
45
|
+
const response = await fetch(`${this.baseUrl}/correction-data`);
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
const rawData = await response.json();
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// This will throw if validation fails
|
|
53
|
+
return validateCorrectionData(rawData);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error('Data validation failed:', error);
|
|
56
|
+
throw new Error('Invalid data received from server: missing or incorrect fields');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async submitCorrections(data: CorrectionData): Promise<void> {
|
|
61
|
+
// Extract only the needed fields
|
|
62
|
+
const updatePayload: CorrectionUpdate = {
|
|
63
|
+
corrections: data.corrections,
|
|
64
|
+
corrected_segments: data.corrected_segments
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const response = await fetch(`${this.baseUrl}/complete`, {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: {
|
|
70
|
+
'Content-Type': 'application/json',
|
|
71
|
+
},
|
|
72
|
+
body: JSON.stringify(updatePayload)
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getAudioUrl(audioHash: string): string {
|
|
81
|
+
return `${this.baseUrl}/audio/${audioHash}`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async generatePreviewVideo(data: CorrectionData): Promise<PreviewVideoResponse> {
|
|
85
|
+
// Extract only the needed fields, just like in submitCorrections
|
|
86
|
+
const updatePayload: CorrectionUpdate = {
|
|
87
|
+
corrections: data.corrections,
|
|
88
|
+
corrected_segments: data.corrected_segments
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const response = await fetch(`${this.baseUrl}/preview-video`, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify(updatePayload)
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
return {
|
|
101
|
+
status: 'error',
|
|
102
|
+
message: `API error: ${response.statusText}`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return await response.json();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
getPreviewVideoUrl(previewHash: string): string {
|
|
110
|
+
return `${this.baseUrl}/preview-video/${previewHash}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async updateHandlers(enabledHandlers: string[]): Promise<CorrectionData> {
|
|
114
|
+
console.log('API: Starting handler update...');
|
|
115
|
+
this.isUpdatingHandlers = true;
|
|
116
|
+
console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}/handlers`, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: {
|
|
122
|
+
'Content-Type': 'application/json',
|
|
123
|
+
},
|
|
124
|
+
body: JSON.stringify(enabledHandlers)
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (!response.ok) {
|
|
128
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const data = await response.json();
|
|
132
|
+
if (data.status === 'error') {
|
|
133
|
+
throw new Error(data.message || 'Failed to update handlers');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log('API: Handler update successful');
|
|
137
|
+
return validateCorrectionData(data.data);
|
|
138
|
+
} finally {
|
|
139
|
+
this.isUpdatingHandlers = false;
|
|
140
|
+
console.log('API: Set isUpdatingHandlers to', this.isUpdatingHandlers);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async addLyrics(source: string, lyrics: string): Promise<CorrectionData> {
|
|
145
|
+
const payload: AddLyricsRequest = {
|
|
146
|
+
source,
|
|
147
|
+
lyrics
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const response = await fetch(`${this.baseUrl}/add-lyrics`, {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify(payload)
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
if (!response.ok) {
|
|
159
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const data = await response.json();
|
|
163
|
+
if (data.status === 'error') {
|
|
164
|
+
throw new Error(data.message || 'Failed to add lyrics');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return validateCorrectionData(data.data);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async submitAnnotations(annotations: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]): Promise<void> {
|
|
171
|
+
// Submit each annotation to the backend
|
|
172
|
+
for (const annotation of annotations) {
|
|
173
|
+
const response = await fetch(`${this.baseUrl}/v1/annotations`, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'Content-Type': 'application/json',
|
|
177
|
+
},
|
|
178
|
+
body: JSON.stringify(annotation)
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
console.error(`Failed to submit annotation:`, annotation);
|
|
183
|
+
// Continue with other annotations even if one fails
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getAnnotationStats(): Promise<any> {
|
|
189
|
+
const response = await fetch(`${this.baseUrl}/v1/annotations/stats`);
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error(`API error: ${response.statusText}`);
|
|
192
|
+
}
|
|
193
|
+
return await response.json();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export class FileOnlyClient implements ApiClient {
|
|
198
|
+
async getCorrectionData(): Promise<CorrectionData> {
|
|
199
|
+
throw new Error('Not supported in file-only mode');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
203
|
+
async submitCorrections(_data: CorrectionData): Promise<void> {
|
|
204
|
+
throw new Error('Not supported in file-only mode');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
208
|
+
getAudioUrl(_audioHash: string): string {
|
|
209
|
+
throw new Error('Not supported in file-only mode');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async generatePreviewVideo(): Promise<PreviewVideoResponse> {
|
|
213
|
+
throw new Error('Not supported in file-only mode');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
217
|
+
getPreviewVideoUrl(_previewHash: string): string {
|
|
218
|
+
throw new Error('Not supported in file-only mode');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
222
|
+
async updateHandlers(_enabledHandlers: string[]): Promise<CorrectionData> {
|
|
223
|
+
throw new Error('Not supported in file-only mode');
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async addLyrics(): Promise<CorrectionData> {
|
|
227
|
+
throw new Error('Not supported in file-only mode');
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
231
|
+
async submitAnnotations(_annotations: Omit<CorrectionAnnotation, 'annotation_id' | 'timestamp'>[]): Promise<void> {
|
|
232
|
+
throw new Error('Not supported in file-only mode');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async getAnnotationStats(): Promise<any> {
|
|
236
|
+
throw new Error('Not supported in file-only mode');
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
isOpen: boolean;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
onSubmit: (payload: { reviewerAction: string; finalText?: string; reasonCategory: string; reasonDetail?: string }) => void;
|
|
7
|
+
suggestion?: { text: string; reasoning?: string; confidence?: number };
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const AIFeedbackModal: React.FC<Props> = ({ isOpen, onClose, onSubmit, suggestion }) => {
|
|
11
|
+
const [reviewerAction, setAction] = React.useState("ACCEPT");
|
|
12
|
+
const [finalText, setFinalText] = React.useState("");
|
|
13
|
+
const [reasonCategory, setReason] = React.useState("AI_CORRECT");
|
|
14
|
+
const [reasonDetail, setDetail] = React.useState("");
|
|
15
|
+
|
|
16
|
+
if (!isOpen) return null;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,0.5)", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
20
|
+
<div style={{ background: "#fff", padding: 16, width: 480, borderRadius: 8 }}>
|
|
21
|
+
<h3>AI Suggestion</h3>
|
|
22
|
+
<p style={{ marginTop: 8 }}>
|
|
23
|
+
{suggestion?.text ?? "No suggestion"}
|
|
24
|
+
{suggestion?.confidence != null ? ` (confidence ${Math.round((suggestion.confidence || 0) * 100)}%)` : null}
|
|
25
|
+
</p>
|
|
26
|
+
{suggestion?.reasoning ? <small>{suggestion.reasoning}</small> : null}
|
|
27
|
+
|
|
28
|
+
<div style={{ marginTop: 12 }}>
|
|
29
|
+
<label>Action</label>
|
|
30
|
+
<select value={reviewerAction} onChange={(e) => setAction(e.target.value)} style={{ marginLeft: 8 }}>
|
|
31
|
+
<option value="ACCEPT">Accept</option>
|
|
32
|
+
<option value="REJECT">Reject</option>
|
|
33
|
+
<option value="MODIFY">Modify</option>
|
|
34
|
+
</select>
|
|
35
|
+
</div>
|
|
36
|
+
|
|
37
|
+
{reviewerAction === "MODIFY" ? (
|
|
38
|
+
<div style={{ marginTop: 12 }}>
|
|
39
|
+
<label>Final Text</label>
|
|
40
|
+
<input value={finalText} onChange={(e) => setFinalText(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
|
|
41
|
+
</div>
|
|
42
|
+
) : null}
|
|
43
|
+
|
|
44
|
+
<div style={{ marginTop: 12 }}>
|
|
45
|
+
<label>Reason</label>
|
|
46
|
+
<select value={reasonCategory} onChange={(e) => setReason(e.target.value)} style={{ marginLeft: 8 }}>
|
|
47
|
+
<option value="AI_CORRECT">AI_CORRECT</option>
|
|
48
|
+
<option value="AI_INCORRECT">AI_INCORRECT</option>
|
|
49
|
+
<option value="AI_SUBOPTIMAL">AI_SUBOPTIMAL</option>
|
|
50
|
+
<option value="CONTEXT_NEEDED">CONTEXT_NEEDED</option>
|
|
51
|
+
<option value="SUBJECTIVE_PREFERENCE">SUBJECTIVE_PREFERENCE</option>
|
|
52
|
+
</select>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div style={{ marginTop: 12 }}>
|
|
56
|
+
<label>Details</label>
|
|
57
|
+
<textarea value={reasonDetail} onChange={(e) => setDetail(e.target.value)} style={{ marginLeft: 8, width: "100%" }} />
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div style={{ display: "flex", gap: 8, justifyContent: "flex-end", marginTop: 16 }}>
|
|
61
|
+
<button onClick={onClose}>Cancel</button>
|
|
62
|
+
<button
|
|
63
|
+
onClick={() =>
|
|
64
|
+
onSubmit({ reviewerAction, finalText: finalText || undefined, reasonCategory, reasonDetail: reasonDetail || undefined })
|
|
65
|
+
}
|
|
66
|
+
>
|
|
67
|
+
Submit
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export default AIFeedbackModal;
|
|
76
|
+
|
|
77
|
+
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Dialog,
|
|
4
|
+
DialogTitle,
|
|
5
|
+
DialogContent,
|
|
6
|
+
DialogActions,
|
|
7
|
+
Button,
|
|
8
|
+
TextField,
|
|
9
|
+
Box,
|
|
10
|
+
CircularProgress,
|
|
11
|
+
Typography
|
|
12
|
+
} from '@mui/material'
|
|
13
|
+
|
|
14
|
+
interface AddLyricsModalProps {
|
|
15
|
+
open: boolean
|
|
16
|
+
onClose: () => void
|
|
17
|
+
onSubmit: (source: string, lyrics: string) => Promise<void>
|
|
18
|
+
isSubmitting: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default function AddLyricsModal({
|
|
22
|
+
open,
|
|
23
|
+
onClose,
|
|
24
|
+
onSubmit,
|
|
25
|
+
isSubmitting
|
|
26
|
+
}: AddLyricsModalProps) {
|
|
27
|
+
const [source, setSource] = useState('')
|
|
28
|
+
const [lyrics, setLyrics] = useState('')
|
|
29
|
+
const [error, setError] = useState<string | null>(null)
|
|
30
|
+
|
|
31
|
+
const handleSubmit = async () => {
|
|
32
|
+
if (!source.trim()) {
|
|
33
|
+
setError('Please enter a source name')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
if (!lyrics.trim()) {
|
|
37
|
+
setError('Please enter lyrics text')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await onSubmit(source.trim(), lyrics.trim())
|
|
43
|
+
// Reset form on success
|
|
44
|
+
setSource('')
|
|
45
|
+
setLyrics('')
|
|
46
|
+
setError(null)
|
|
47
|
+
onClose()
|
|
48
|
+
} catch (err) {
|
|
49
|
+
setError(err instanceof Error ? err.message : 'Failed to add lyrics')
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleClose = () => {
|
|
54
|
+
// Don't allow closing if currently submitting
|
|
55
|
+
if (isSubmitting) return
|
|
56
|
+
|
|
57
|
+
setSource('')
|
|
58
|
+
setLyrics('')
|
|
59
|
+
setError(null)
|
|
60
|
+
onClose()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<Dialog
|
|
65
|
+
open={open}
|
|
66
|
+
onClose={handleClose}
|
|
67
|
+
maxWidth="md"
|
|
68
|
+
fullWidth
|
|
69
|
+
disableEscapeKeyDown={isSubmitting}
|
|
70
|
+
>
|
|
71
|
+
<DialogTitle>Add Reference Lyrics</DialogTitle>
|
|
72
|
+
<DialogContent>
|
|
73
|
+
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2, mt: 1 }}>
|
|
74
|
+
{error && (
|
|
75
|
+
<Typography color="error" variant="body2">
|
|
76
|
+
{error}
|
|
77
|
+
</Typography>
|
|
78
|
+
)}
|
|
79
|
+
<TextField
|
|
80
|
+
label="Source Name"
|
|
81
|
+
value={source}
|
|
82
|
+
onChange={(e) => setSource(e.target.value)}
|
|
83
|
+
disabled={isSubmitting}
|
|
84
|
+
fullWidth
|
|
85
|
+
placeholder="e.g., Official Lyrics, Album Booklet"
|
|
86
|
+
/>
|
|
87
|
+
<TextField
|
|
88
|
+
label="Lyrics"
|
|
89
|
+
value={lyrics}
|
|
90
|
+
onChange={(e) => setLyrics(e.target.value)}
|
|
91
|
+
disabled={isSubmitting}
|
|
92
|
+
fullWidth
|
|
93
|
+
multiline
|
|
94
|
+
rows={10}
|
|
95
|
+
placeholder="Paste lyrics text here (one line per segment)"
|
|
96
|
+
/>
|
|
97
|
+
</Box>
|
|
98
|
+
</DialogContent>
|
|
99
|
+
<DialogActions>
|
|
100
|
+
<Button onClick={handleClose} disabled={isSubmitting}>
|
|
101
|
+
Cancel
|
|
102
|
+
</Button>
|
|
103
|
+
<Button
|
|
104
|
+
onClick={handleSubmit}
|
|
105
|
+
variant="contained"
|
|
106
|
+
disabled={isSubmitting}
|
|
107
|
+
startIcon={isSubmitting ? <CircularProgress size={20} /> : undefined}
|
|
108
|
+
>
|
|
109
|
+
{isSubmitting ? 'Adding...' : 'Add Lyrics'}
|
|
110
|
+
</Button>
|
|
111
|
+
</DialogActions>
|
|
112
|
+
</Dialog>
|
|
113
|
+
)
|
|
114
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { Paper, Box, Typography, Chip, Tooltip } from '@mui/material'
|
|
2
|
+
import { WordCorrection } from '../types'
|
|
3
|
+
import { useMemo } from 'react'
|
|
4
|
+
|
|
5
|
+
interface GapCategoryMetric {
|
|
6
|
+
category: string
|
|
7
|
+
count: number
|
|
8
|
+
avgConfidence: number
|
|
9
|
+
corrections: WordCorrection[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AgenticCorrectionMetricsProps {
|
|
13
|
+
corrections: WordCorrection[]
|
|
14
|
+
onCategoryClick?: (category: string) => void
|
|
15
|
+
onConfidenceFilterClick?: (filter: 'low' | 'high') => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function AgenticCorrectionMetrics({
|
|
19
|
+
corrections,
|
|
20
|
+
onCategoryClick,
|
|
21
|
+
onConfidenceFilterClick
|
|
22
|
+
}: AgenticCorrectionMetricsProps) {
|
|
23
|
+
|
|
24
|
+
const metrics = useMemo(() => {
|
|
25
|
+
// Filter only agentic corrections
|
|
26
|
+
const agenticCorrections = corrections.filter(c => c.handler === 'AgenticCorrector')
|
|
27
|
+
|
|
28
|
+
// Parse category from reason string (format: "reason [CATEGORY] (confidence: XX%)")
|
|
29
|
+
const categoryMap = new Map<string, GapCategoryMetric>()
|
|
30
|
+
|
|
31
|
+
agenticCorrections.forEach(correction => {
|
|
32
|
+
const categoryMatch = correction.reason?.match(/\[([A-Z_]+)\]/)
|
|
33
|
+
const category = categoryMatch ? categoryMatch[1] : 'UNKNOWN'
|
|
34
|
+
|
|
35
|
+
if (!categoryMap.has(category)) {
|
|
36
|
+
categoryMap.set(category, {
|
|
37
|
+
category,
|
|
38
|
+
count: 0,
|
|
39
|
+
avgConfidence: 0,
|
|
40
|
+
corrections: []
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const metric = categoryMap.get(category)!
|
|
45
|
+
metric.count++
|
|
46
|
+
metric.corrections.push(correction)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Calculate average confidence for each category
|
|
50
|
+
categoryMap.forEach((metric) => {
|
|
51
|
+
const totalConfidence = metric.corrections.reduce((sum, c) => sum + c.confidence, 0)
|
|
52
|
+
metric.avgConfidence = totalConfidence / metric.count
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Convert to array and sort by count descending
|
|
56
|
+
const sortedMetrics = Array.from(categoryMap.values()).sort((a, b) => b.count - a.count)
|
|
57
|
+
|
|
58
|
+
// Calculate overall stats
|
|
59
|
+
const totalCorrections = agenticCorrections.length
|
|
60
|
+
const avgConfidence = totalCorrections > 0
|
|
61
|
+
? agenticCorrections.reduce((sum, c) => sum + c.confidence, 0) / totalCorrections
|
|
62
|
+
: 0
|
|
63
|
+
|
|
64
|
+
const lowConfidenceCount = agenticCorrections.filter(c => c.confidence < 0.6).length
|
|
65
|
+
const highConfidenceCount = agenticCorrections.filter(c => c.confidence >= 0.8).length
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
categories: sortedMetrics,
|
|
69
|
+
totalCorrections,
|
|
70
|
+
avgConfidence,
|
|
71
|
+
lowConfidenceCount,
|
|
72
|
+
highConfidenceCount
|
|
73
|
+
}
|
|
74
|
+
}, [corrections])
|
|
75
|
+
|
|
76
|
+
// Format category name for display
|
|
77
|
+
const formatCategory = (category: string): string => {
|
|
78
|
+
return category
|
|
79
|
+
.split('_')
|
|
80
|
+
.map(word => word.charAt(0) + word.slice(1).toLowerCase())
|
|
81
|
+
.join(' ')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Get emoji/icon for category
|
|
85
|
+
const getCategoryIcon = (category: string): string => {
|
|
86
|
+
const icons: Record<string, string> = {
|
|
87
|
+
'SOUND_ALIKE': '🎵',
|
|
88
|
+
'PUNCTUATION_ONLY': '✏️',
|
|
89
|
+
'BACKGROUND_VOCALS': '🎤',
|
|
90
|
+
'EXTRA_WORDS': '➕',
|
|
91
|
+
'REPEATED_SECTION': '🔁',
|
|
92
|
+
'COMPLEX_MULTI_ERROR': '🔧',
|
|
93
|
+
'AMBIGUOUS': '❓',
|
|
94
|
+
'NO_ERROR': '✅'
|
|
95
|
+
}
|
|
96
|
+
return icons[category] || '📝'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Paper
|
|
101
|
+
sx={{
|
|
102
|
+
p: 0.8,
|
|
103
|
+
height: '100%',
|
|
104
|
+
display: 'flex',
|
|
105
|
+
flexDirection: 'column'
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 0.5, fontSize: '0.7rem' }}>
|
|
109
|
+
Agentic AI Corrections
|
|
110
|
+
</Typography>
|
|
111
|
+
|
|
112
|
+
{/* Overall stats */}
|
|
113
|
+
<Box sx={{ mb: 1 }}>
|
|
114
|
+
<Typography variant="body2" sx={{ fontSize: '0.75rem', mb: 0.3 }}>
|
|
115
|
+
Total: <strong>{metrics.totalCorrections}</strong>
|
|
116
|
+
</Typography>
|
|
117
|
+
<Typography variant="body2" sx={{ fontSize: '0.75rem', mb: 0.5 }}>
|
|
118
|
+
Avg Confidence: <strong>{(metrics.avgConfidence * 100).toFixed(0)}%</strong>
|
|
119
|
+
</Typography>
|
|
120
|
+
|
|
121
|
+
{/* Quick filters */}
|
|
122
|
+
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
|
123
|
+
<Chip
|
|
124
|
+
label={`Low (<60%): ${metrics.lowConfidenceCount}`}
|
|
125
|
+
size="small"
|
|
126
|
+
variant="outlined"
|
|
127
|
+
color="warning"
|
|
128
|
+
onClick={() => onConfidenceFilterClick?.('low')}
|
|
129
|
+
sx={{ fontSize: '0.65rem', height: '20px', cursor: 'pointer' }}
|
|
130
|
+
/>
|
|
131
|
+
<Chip
|
|
132
|
+
label={`High (≥80%): ${metrics.highConfidenceCount}`}
|
|
133
|
+
size="small"
|
|
134
|
+
variant="outlined"
|
|
135
|
+
color="success"
|
|
136
|
+
onClick={() => onConfidenceFilterClick?.('high')}
|
|
137
|
+
sx={{ fontSize: '0.65rem', height: '20px', cursor: 'pointer' }}
|
|
138
|
+
/>
|
|
139
|
+
</Box>
|
|
140
|
+
</Box>
|
|
141
|
+
|
|
142
|
+
{/* Category breakdown */}
|
|
143
|
+
<Typography variant="subtitle2" color="text.secondary" sx={{ mb: 0.5, fontSize: '0.7rem' }}>
|
|
144
|
+
By Category
|
|
145
|
+
</Typography>
|
|
146
|
+
<Box sx={{ flex: 1, overflow: 'auto' }}>
|
|
147
|
+
{metrics.categories.map((metric) => (
|
|
148
|
+
<Box
|
|
149
|
+
key={metric.category}
|
|
150
|
+
sx={{
|
|
151
|
+
mb: 0.5,
|
|
152
|
+
p: 0.5,
|
|
153
|
+
borderRadius: 1,
|
|
154
|
+
cursor: 'pointer',
|
|
155
|
+
'&:hover': {
|
|
156
|
+
bgcolor: 'action.hover'
|
|
157
|
+
}
|
|
158
|
+
}}
|
|
159
|
+
onClick={() => onCategoryClick?.(metric.category)}
|
|
160
|
+
>
|
|
161
|
+
<Tooltip
|
|
162
|
+
title={`${metric.count} correction${metric.count !== 1 ? 's' : ''} • Avg confidence: ${(metric.avgConfidence * 100).toFixed(0)}%`}
|
|
163
|
+
placement="right"
|
|
164
|
+
>
|
|
165
|
+
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
166
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
|
167
|
+
<span style={{ fontSize: '0.85rem' }}>{getCategoryIcon(metric.category)}</span>
|
|
168
|
+
<Typography variant="body2" sx={{ fontSize: '0.7rem' }}>
|
|
169
|
+
{formatCategory(metric.category)}
|
|
170
|
+
</Typography>
|
|
171
|
+
</Box>
|
|
172
|
+
<Box sx={{ display: 'flex', gap: 0.5, alignItems: 'center' }}>
|
|
173
|
+
<Typography variant="body2" sx={{ fontSize: '0.65rem', color: 'text.secondary' }}>
|
|
174
|
+
{(metric.avgConfidence * 100).toFixed(0)}%
|
|
175
|
+
</Typography>
|
|
176
|
+
<Typography
|
|
177
|
+
variant="body2"
|
|
178
|
+
sx={{
|
|
179
|
+
fontSize: '0.7rem',
|
|
180
|
+
fontWeight: 600,
|
|
181
|
+
bgcolor: 'action.selected',
|
|
182
|
+
px: 0.5,
|
|
183
|
+
borderRadius: 0.5,
|
|
184
|
+
minWidth: '24px',
|
|
185
|
+
textAlign: 'center'
|
|
186
|
+
}}
|
|
187
|
+
>
|
|
188
|
+
{metric.count}
|
|
189
|
+
</Typography>
|
|
190
|
+
</Box>
|
|
191
|
+
</Box>
|
|
192
|
+
</Tooltip>
|
|
193
|
+
</Box>
|
|
194
|
+
))}
|
|
195
|
+
{metrics.categories.length === 0 && (
|
|
196
|
+
<Typography variant="body2" color="text.secondary" sx={{ fontSize: '0.7rem', fontStyle: 'italic' }}>
|
|
197
|
+
No agentic corrections
|
|
198
|
+
</Typography>
|
|
199
|
+
)}
|
|
200
|
+
</Box>
|
|
201
|
+
</Paper>
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
|