karaoke-gen 0.57.0__py3-none-any.whl → 0.71.27__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 +1965 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen-0.71.27.dist-info/METADATA +610 -0
- karaoke_gen-0.71.27.dist-info/RECORD +275 -0
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.dist-info}/WHEEL +1 -1
- {karaoke_gen-0.57.0.dist-info → karaoke_gen-0.71.27.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.27.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Optional, Dict, Any
|
|
2
|
+
import os
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def setup_langfuse(client_name: str = "agentic-corrector") -> Optional[object]:
|
|
7
|
+
"""Initialize Langfuse client if keys are present; return client or None.
|
|
8
|
+
|
|
9
|
+
This avoids hard dependency at import time; caller can check for None and
|
|
10
|
+
no-op if observability is not configured.
|
|
11
|
+
"""
|
|
12
|
+
secret = os.getenv("LANGFUSE_SECRET_KEY")
|
|
13
|
+
public = os.getenv("LANGFUSE_PUBLIC_KEY")
|
|
14
|
+
host = os.getenv("LANGFUSE_HOST", "https://cloud.langfuse.com")
|
|
15
|
+
if not (secret and public):
|
|
16
|
+
return None
|
|
17
|
+
try:
|
|
18
|
+
from langfuse import Langfuse # type: ignore
|
|
19
|
+
|
|
20
|
+
client = Langfuse(secret_key=secret, public_key=public, host=host, sdk_integration=client_name)
|
|
21
|
+
return client
|
|
22
|
+
except Exception:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def record_metrics(client: Optional[object], name: str, metrics: Dict[str, Any]) -> None:
|
|
27
|
+
"""Record custom metrics to Langfuse if initialized."""
|
|
28
|
+
if client is None:
|
|
29
|
+
return
|
|
30
|
+
try:
|
|
31
|
+
# Minimal shape to avoid strict coupling; callers can extend
|
|
32
|
+
client.trace(name=name, metadata=metrics)
|
|
33
|
+
except Exception:
|
|
34
|
+
# Swallow observability errors to never impact core flow
|
|
35
|
+
pass
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class MetricsAggregator:
|
|
9
|
+
"""In-memory metrics aggregator for agentic correction API."""
|
|
10
|
+
|
|
11
|
+
total_sessions: int = 0
|
|
12
|
+
total_processing_time_ms: int = 0
|
|
13
|
+
total_feedback: int = 0
|
|
14
|
+
model_counts: Dict[str, int] = field(default_factory=dict)
|
|
15
|
+
model_total_time_ms: Dict[str, int] = field(default_factory=dict)
|
|
16
|
+
fallback_count: int = 0
|
|
17
|
+
|
|
18
|
+
def record_session(self, model_id: str, processing_time_ms: int, fallback_used: bool) -> None:
|
|
19
|
+
self.total_sessions += 1
|
|
20
|
+
self.total_processing_time_ms += max(0, int(processing_time_ms))
|
|
21
|
+
if model_id:
|
|
22
|
+
self.model_counts[model_id] = self.model_counts.get(model_id, 0) + 1
|
|
23
|
+
self.model_total_time_ms[model_id] = self.model_total_time_ms.get(model_id, 0) + max(0, int(processing_time_ms))
|
|
24
|
+
if fallback_used:
|
|
25
|
+
self.fallback_count += 1
|
|
26
|
+
|
|
27
|
+
def record_feedback(self) -> None:
|
|
28
|
+
self.total_feedback += 1
|
|
29
|
+
|
|
30
|
+
def snapshot(self, time_range: str = "day", session_id: str | None = None) -> Dict[str, Any]:
|
|
31
|
+
avg_time = int(self.total_processing_time_ms / self.total_sessions) if self.total_sessions else 0
|
|
32
|
+
# Compute simple per-model avg latencies
|
|
33
|
+
per_model_avg = {m: int(self.model_total_time_ms.get(m, 0) / c) if c else 0 for m, c in self.model_counts.items()}
|
|
34
|
+
# Placeholders for accuracy/cost until we collect these
|
|
35
|
+
return {
|
|
36
|
+
"timeRange": time_range,
|
|
37
|
+
"totalSessions": self.total_sessions,
|
|
38
|
+
"averageAccuracy": 0.0,
|
|
39
|
+
"errorReduction": 0.0,
|
|
40
|
+
"averageProcessingTime": avg_time,
|
|
41
|
+
"modelPerformance": {"counts": self.model_counts, "avgLatencyMs": per_model_avg, "fallbacks": self.fallback_count},
|
|
42
|
+
"costSummary": {},
|
|
43
|
+
"userSatisfaction": 0.0,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from typing import Iterator
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@contextmanager
|
|
9
|
+
def timer() -> Iterator[float]:
|
|
10
|
+
start = time.time()
|
|
11
|
+
try:
|
|
12
|
+
yield start
|
|
13
|
+
finally:
|
|
14
|
+
pass
|
|
15
|
+
|
|
16
|
+
def elapsed_ms(start: float) -> int:
|
|
17
|
+
return int((time.time() - start) * 1000)
|
|
18
|
+
|
|
19
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Gap classification prompt builder for agentic correction."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Optional
|
|
4
|
+
import yaml
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_few_shot_examples() -> Dict[str, List[Dict]]:
|
|
10
|
+
"""Load few-shot examples from examples.yaml if it exists."""
|
|
11
|
+
examples_path = Path(__file__).parent / "examples.yaml"
|
|
12
|
+
|
|
13
|
+
if not examples_path.exists():
|
|
14
|
+
return get_hardcoded_examples()
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
with open(examples_path, 'r') as f:
|
|
18
|
+
data = yaml.safe_load(f)
|
|
19
|
+
return data.get('examples_by_category', {})
|
|
20
|
+
except Exception:
|
|
21
|
+
return get_hardcoded_examples()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_hardcoded_examples() -> Dict[str, List[Dict]]:
|
|
25
|
+
"""Hardcoded examples from gaps_review.yaml for initial training."""
|
|
26
|
+
return {
|
|
27
|
+
"sound_alike": [
|
|
28
|
+
{
|
|
29
|
+
"gap_text": "out, I'm starting over",
|
|
30
|
+
"preceding": "Oh no, was it worth it? Starting",
|
|
31
|
+
"following": "gonna sleep With the next person",
|
|
32
|
+
"reference": "Starting now I'm starting over",
|
|
33
|
+
"reasoning": "Transcription heard 'out' but reference lyrics show 'now' - common sound-alike error",
|
|
34
|
+
"action": "REPLACE 'out' with 'now'"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"gap_text": "And you said to watch it",
|
|
38
|
+
"preceding": "You're a time, uh, uh, uh",
|
|
39
|
+
"following": "just in time But to wreck",
|
|
40
|
+
"reference": "You set the watch You're just in time",
|
|
41
|
+
"reasoning": "Transcription heard 'And you said to watch it' but reference shows 'You set the watch You're' - sound-alike with extra word 'And'",
|
|
42
|
+
"action": "REPLACE with reference text"
|
|
43
|
+
}
|
|
44
|
+
],
|
|
45
|
+
"background_vocals": [
|
|
46
|
+
{
|
|
47
|
+
"gap_text": "it? (Big business)",
|
|
48
|
+
"preceding": "Oh no, was it worth it? Was it worth",
|
|
49
|
+
"following": "Was it worth it? (Was it worth",
|
|
50
|
+
"reference": "was it worth what you did to big business?",
|
|
51
|
+
"reasoning": "Words in parentheses are background vocals not in reference lyrics",
|
|
52
|
+
"action": "DELETE words in parentheses"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"gap_text": "(Was it worth it?) Was",
|
|
56
|
+
"preceding": "it? (Big business) Was it worth it?",
|
|
57
|
+
"following": "it worth it? (Your friends)",
|
|
58
|
+
"reference": "Was it worth what you did to big business?",
|
|
59
|
+
"reasoning": "Parenthesized phrase is backing vocal repetition",
|
|
60
|
+
"action": "DELETE parenthesized words"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"extra_words": [
|
|
64
|
+
{
|
|
65
|
+
"gap_text": "But to wreck my life",
|
|
66
|
+
"preceding": "said to watch it just in time",
|
|
67
|
+
"following": "To bring back what I left",
|
|
68
|
+
"reference": "You're just in time To wreck my life",
|
|
69
|
+
"reasoning": "Transcription adds filler word 'But' not in reference lyrics",
|
|
70
|
+
"action": "DELETE 'But'"
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"punctuation_only": [
|
|
74
|
+
{
|
|
75
|
+
"gap_text": "Tick- tock, you're",
|
|
76
|
+
"preceding": "They got no, they got no concept of time",
|
|
77
|
+
"following": "not a clock You're a time bomb",
|
|
78
|
+
"reference": "Tick tock, you're not a clock",
|
|
79
|
+
"reasoning": "Only difference is hyphen in 'Tick-tock' vs 'Tick tock' - stylistic",
|
|
80
|
+
"action": "NO_ACTION"
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
"no_error": [
|
|
84
|
+
{
|
|
85
|
+
"gap_text": "you're telling lies Well,",
|
|
86
|
+
"preceding": "You swore together forever Now",
|
|
87
|
+
"following": "tell me your words They got",
|
|
88
|
+
"reference_genius": "Now you're telling lies",
|
|
89
|
+
"reference_lrclib": "Now you're telling me lies",
|
|
90
|
+
"reasoning": "Genius reference matches transcription exactly (without 'me'), so transcription is correct",
|
|
91
|
+
"action": "NO_ACTION"
|
|
92
|
+
}
|
|
93
|
+
],
|
|
94
|
+
"repeated_section": [
|
|
95
|
+
{
|
|
96
|
+
"gap_text": "You're a time bomb, baby You're",
|
|
97
|
+
"preceding": "Tick-tock, you're not a clock",
|
|
98
|
+
"following": "a time bomb, baby, oh",
|
|
99
|
+
"reference": "You're a time bomb baby",
|
|
100
|
+
"reasoning": "Reference lyrics don't show repetition, but cannot confirm without audio",
|
|
101
|
+
"action": "FLAG for human review"
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"complex_multi_error": [
|
|
105
|
+
{
|
|
106
|
+
"gap_text": "Right here, did you dance for later? That's what you said? Well, here's an answer You're out in life You have to try",
|
|
107
|
+
"reference": "Five years and you fell for a waiter I'm sure he says he's an actor So you're acting like",
|
|
108
|
+
"reasoning": "50-word gap with multiple sound-alike errors throughout, too complex for automatic correction",
|
|
109
|
+
"action": "FLAG for human review"
|
|
110
|
+
}
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def build_classification_prompt(
|
|
116
|
+
gap_text: str,
|
|
117
|
+
preceding_words: str,
|
|
118
|
+
following_words: str,
|
|
119
|
+
reference_contexts: Dict[str, str],
|
|
120
|
+
artist: Optional[str] = None,
|
|
121
|
+
title: Optional[str] = None,
|
|
122
|
+
gap_id: Optional[str] = None
|
|
123
|
+
) -> str:
|
|
124
|
+
"""Build a prompt for classifying a gap in the transcription.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
gap_text: The text of the gap that needs classification
|
|
128
|
+
preceding_words: Text immediately before the gap
|
|
129
|
+
following_words: Text immediately after the gap
|
|
130
|
+
reference_contexts: Dictionary of reference lyrics from each source
|
|
131
|
+
artist: Song artist name for context
|
|
132
|
+
title: Song title for context
|
|
133
|
+
gap_id: Identifier for the gap
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Formatted prompt string for the LLM
|
|
137
|
+
"""
|
|
138
|
+
examples = load_few_shot_examples()
|
|
139
|
+
|
|
140
|
+
# Build few-shot examples section
|
|
141
|
+
examples_text = "## Example Classifications\n\n"
|
|
142
|
+
for category, category_examples in examples.items():
|
|
143
|
+
if category_examples:
|
|
144
|
+
examples_text += f"### {category.upper().replace('_', ' ')}\n\n"
|
|
145
|
+
for ex in category_examples[:2]: # Limit to 2 examples per category
|
|
146
|
+
examples_text += f"**Gap:** {ex['gap_text']}\n"
|
|
147
|
+
examples_text += f"**Context:** ...{ex.get('preceding', '')}... [GAP] ...{ex.get('following', '')}...\n"
|
|
148
|
+
if 'reference' in ex:
|
|
149
|
+
examples_text += f"**Reference:** {ex['reference']}\n"
|
|
150
|
+
examples_text += f"**Reasoning:** {ex['reasoning']}\n"
|
|
151
|
+
examples_text += f"**Action:** {ex['action']}\n\n"
|
|
152
|
+
|
|
153
|
+
# Build reference lyrics section
|
|
154
|
+
references_text = ""
|
|
155
|
+
if reference_contexts:
|
|
156
|
+
references_text = "## Available Reference Lyrics\n\n"
|
|
157
|
+
for source, context in reference_contexts.items():
|
|
158
|
+
references_text += f"**{source.upper()}:** {context}\n\n"
|
|
159
|
+
|
|
160
|
+
# Build song context
|
|
161
|
+
song_context = ""
|
|
162
|
+
if artist and title:
|
|
163
|
+
song_context = f"\n## Song Context\n\n**Artist:** {artist}\n**Title:** {title}\n\nNote: The song title and artist name may help identify proper nouns or unusual words that could be mis-heard.\n"
|
|
164
|
+
|
|
165
|
+
prompt = f"""You are an expert at analyzing transcription errors in song lyrics. Your task is to classify gaps (mismatches between transcription and reference lyrics) into categories to determine the best correction approach.
|
|
166
|
+
|
|
167
|
+
{song_context}
|
|
168
|
+
|
|
169
|
+
## Categories
|
|
170
|
+
|
|
171
|
+
Use these EXACT category names in your response:
|
|
172
|
+
|
|
173
|
+
1. **PUNCTUATION_ONLY**: Only difference is punctuation, capitalization, or symbols (hyphens, quotes). No text changes needed.
|
|
174
|
+
|
|
175
|
+
2. **SOUND_ALIKE**: Transcription mis-heard words that sound similar (e.g., "out" vs "now", "said to watch" vs "set the watch"). Common for homophones or similar-sounding phrases.
|
|
176
|
+
|
|
177
|
+
3. **BACKGROUND_VOCALS**: Transcription includes backing vocals (usually in parentheses) that aren't in the main reference lyrics. Should typically be removed for karaoke.
|
|
178
|
+
|
|
179
|
+
4. **EXTRA_WORDS**: Transcription adds common filler words like "And", "But", "Well" at sentence starts that aren't in reference lyrics.
|
|
180
|
+
|
|
181
|
+
5. **REPEATED_SECTION**: Transcription shows repeated chorus/lyrics that may or may not appear in condensed reference lyrics. Often needs human verification via audio.
|
|
182
|
+
|
|
183
|
+
6. **COMPLEX_MULTI_ERROR**: Large gaps (many words) with multiple different error types. Too complex for automatic correction.
|
|
184
|
+
|
|
185
|
+
7. **NO_ERROR**: At least one reference source matches the transcription exactly, indicating the transcription is correct and other references are incomplete/wrong.
|
|
186
|
+
|
|
187
|
+
8. **AMBIGUOUS**: Cannot determine correct action without listening to audio. Similar to repeated sections but less clear.
|
|
188
|
+
|
|
189
|
+
{examples_text}
|
|
190
|
+
|
|
191
|
+
## Gap to Classify
|
|
192
|
+
|
|
193
|
+
**Gap ID:** {gap_id or 'unknown'}
|
|
194
|
+
|
|
195
|
+
**Preceding Context:** {preceding_words}
|
|
196
|
+
|
|
197
|
+
**Gap Text:** {gap_text}
|
|
198
|
+
|
|
199
|
+
**Following Context:** {following_words}
|
|
200
|
+
|
|
201
|
+
{references_text}
|
|
202
|
+
|
|
203
|
+
## Important Guidelines
|
|
204
|
+
|
|
205
|
+
- If ANY reference source matches the gap text exactly (ignoring punctuation), classify as **NO_ERROR**
|
|
206
|
+
- Consider whether the song title/artist contains words that might appear in the gap
|
|
207
|
+
- Parentheses in transcription usually indicate background vocals
|
|
208
|
+
- Sound-alike errors are very common in song transcription
|
|
209
|
+
- Flag for human review when uncertain
|
|
210
|
+
|
|
211
|
+
## Your Task
|
|
212
|
+
|
|
213
|
+
Analyze this gap and respond with a JSON object matching this schema:
|
|
214
|
+
|
|
215
|
+
{{
|
|
216
|
+
"gap_id": "{gap_id or 'unknown'}",
|
|
217
|
+
"category": "<one of the 8 categories above>",
|
|
218
|
+
"confidence": <float between 0 and 1>,
|
|
219
|
+
"reasoning": "<detailed explanation for your classification>",
|
|
220
|
+
"suggested_handler": "<name of handler or null>"
|
|
221
|
+
}}
|
|
222
|
+
|
|
223
|
+
Provide ONLY the JSON response, no other text.
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
return prompt
|
|
227
|
+
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseAIProvider(ABC):
|
|
8
|
+
"""Abstract provider interface for generating correction proposals.
|
|
9
|
+
|
|
10
|
+
Implementations should honor timeouts and retry policies according to
|
|
11
|
+
ProviderConfig and return structured proposals validated upstream.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def name(self) -> str:
|
|
16
|
+
raise NotImplementedError
|
|
17
|
+
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def generate_correction_proposals(
|
|
20
|
+
self,
|
|
21
|
+
prompt: str,
|
|
22
|
+
schema: Dict[str, Any],
|
|
23
|
+
session_id: str | None = None
|
|
24
|
+
) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Return a list of correction proposals as dictionaries matching `schema`.
|
|
26
|
+
|
|
27
|
+
The schema is provided so implementations can guide structured outputs.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
prompt: The correction prompt
|
|
31
|
+
schema: JSON schema for the expected output structure
|
|
32
|
+
session_id: Optional Langfuse session ID for grouping traces
|
|
33
|
+
"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Circuit breaker pattern implementation for AI provider reliability."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
from .config import ProviderConfig
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CircuitBreaker:
|
|
14
|
+
"""Circuit breaker for protecting against cascading failures.
|
|
15
|
+
|
|
16
|
+
Tracks failures per model and temporarily stops requests when
|
|
17
|
+
failure threshold is exceeded. Automatically resets after a timeout.
|
|
18
|
+
|
|
19
|
+
Single Responsibility: Failure tracking and circuit state management only.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self, config: ProviderConfig):
|
|
23
|
+
"""Initialize circuit breaker with configuration.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Provider configuration with thresholds and timeouts
|
|
27
|
+
"""
|
|
28
|
+
self._config = config
|
|
29
|
+
self._failures: Dict[str, int] = {}
|
|
30
|
+
self._open_until: Dict[str, float] = {}
|
|
31
|
+
|
|
32
|
+
def is_open(self, model: str) -> bool:
|
|
33
|
+
"""Check if circuit breaker is open for this model.
|
|
34
|
+
|
|
35
|
+
An open circuit means requests should be rejected immediately
|
|
36
|
+
to prevent cascading failures.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
model: Model identifier to check
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
True if circuit is open (reject requests), False if closed (allow)
|
|
43
|
+
"""
|
|
44
|
+
now = time.time()
|
|
45
|
+
open_until = self._open_until.get(model, 0)
|
|
46
|
+
|
|
47
|
+
if now < open_until:
|
|
48
|
+
remaining = int(open_until - now)
|
|
49
|
+
logger.debug(
|
|
50
|
+
f"🤖 Circuit breaker open for {model}, "
|
|
51
|
+
f"retry in {remaining}s"
|
|
52
|
+
)
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
# Circuit was open but timeout expired - close it
|
|
56
|
+
if model in self._open_until:
|
|
57
|
+
logger.info(f"🤖 Circuit breaker closed for {model} (timeout expired)")
|
|
58
|
+
del self._open_until[model]
|
|
59
|
+
self._failures[model] = 0
|
|
60
|
+
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def get_open_until(self, model: str) -> float:
|
|
64
|
+
"""Get timestamp when circuit will close for this model.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
model: Model identifier
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Unix timestamp when circuit will close, or 0 if not open
|
|
71
|
+
"""
|
|
72
|
+
return self._open_until.get(model, 0)
|
|
73
|
+
|
|
74
|
+
def record_failure(self, model: str) -> None:
|
|
75
|
+
"""Record a failure for this model and maybe open the circuit.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
model: Model identifier that failed
|
|
79
|
+
"""
|
|
80
|
+
self._failures[model] = self._failures.get(model, 0) + 1
|
|
81
|
+
failure_count = self._failures[model]
|
|
82
|
+
|
|
83
|
+
logger.debug(
|
|
84
|
+
f"🤖 Recorded failure for {model}, "
|
|
85
|
+
f"total: {failure_count}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Check if we should open the circuit
|
|
89
|
+
threshold = self._config.circuit_breaker_failure_threshold
|
|
90
|
+
if failure_count >= threshold:
|
|
91
|
+
self._open_circuit(model)
|
|
92
|
+
|
|
93
|
+
def record_success(self, model: str) -> None:
|
|
94
|
+
"""Record a successful call and reset failure count.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
model: Model identifier that succeeded
|
|
98
|
+
"""
|
|
99
|
+
if model in self._failures and self._failures[model] > 0:
|
|
100
|
+
logger.debug(
|
|
101
|
+
f"🤖 Reset failure count for {model} "
|
|
102
|
+
f"(was {self._failures[model]})"
|
|
103
|
+
)
|
|
104
|
+
self._failures[model] = 0
|
|
105
|
+
|
|
106
|
+
def _open_circuit(self, model: str) -> None:
|
|
107
|
+
"""Open the circuit breaker for this model.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
model: Model identifier to open circuit for
|
|
111
|
+
"""
|
|
112
|
+
open_seconds = self._config.circuit_breaker_open_seconds
|
|
113
|
+
self._open_until[model] = time.time() + open_seconds
|
|
114
|
+
|
|
115
|
+
logger.warning(
|
|
116
|
+
f"🤖 Circuit breaker opened for {model} "
|
|
117
|
+
f"({self._failures[model]} failures >= "
|
|
118
|
+
f"{self._config.circuit_breaker_failure_threshold} threshold), "
|
|
119
|
+
f"will retry in {open_seconds}s"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
def reset(self, model: str) -> None:
|
|
123
|
+
"""Manually reset circuit breaker for a model.
|
|
124
|
+
|
|
125
|
+
Useful for testing or administrative reset.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
model: Model identifier to reset
|
|
129
|
+
"""
|
|
130
|
+
self._failures[model] = 0
|
|
131
|
+
if model in self._open_until:
|
|
132
|
+
del self._open_until[model]
|
|
133
|
+
logger.info(f"🤖 Circuit breaker manually reset for {model}")
|
|
134
|
+
|
|
135
|
+
def get_failure_count(self, model: str) -> int:
|
|
136
|
+
"""Get current failure count for a model.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
model: Model identifier
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Number of consecutive failures
|
|
143
|
+
"""
|
|
144
|
+
return self._failures.get(model, 0)
|
|
145
|
+
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class ProviderConfig:
|
|
8
|
+
"""Centralized configuration for AI providers.
|
|
9
|
+
|
|
10
|
+
Values are loaded from environment variables to keep credentials out of code.
|
|
11
|
+
This module is safe to import during setup; it does not perform any network I/O.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
openai_api_key: Optional[str]
|
|
15
|
+
anthropic_api_key: Optional[str]
|
|
16
|
+
google_api_key: Optional[str]
|
|
17
|
+
openrouter_api_key: Optional[str]
|
|
18
|
+
privacy_mode: bool
|
|
19
|
+
cache_dir: str
|
|
20
|
+
|
|
21
|
+
request_timeout_seconds: float = 30.0
|
|
22
|
+
max_retries: int = 2
|
|
23
|
+
retry_backoff_base_seconds: float = 0.2
|
|
24
|
+
retry_backoff_factor: float = 2.0
|
|
25
|
+
circuit_breaker_failure_threshold: int = 3
|
|
26
|
+
circuit_breaker_open_seconds: int = 60
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def from_env(cache_dir: Optional[str] = None) -> "ProviderConfig":
|
|
30
|
+
"""Create config from environment variables.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
cache_dir: Cache directory path. If None, uses LYRICS_TRANSCRIBER_CACHE_DIR
|
|
34
|
+
env var or defaults to ~/lyrics-transcriber-cache
|
|
35
|
+
"""
|
|
36
|
+
if cache_dir is None:
|
|
37
|
+
cache_dir = os.getenv(
|
|
38
|
+
"LYRICS_TRANSCRIBER_CACHE_DIR",
|
|
39
|
+
os.path.join(os.path.expanduser("~"), "lyrics-transcriber-cache")
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
return ProviderConfig(
|
|
43
|
+
openai_api_key=os.getenv("OPENAI_API_KEY"),
|
|
44
|
+
anthropic_api_key=os.getenv("ANTHROPIC_API_KEY"),
|
|
45
|
+
google_api_key=os.getenv("GOOGLE_API_KEY"),
|
|
46
|
+
openrouter_api_key=os.getenv("OPENROUTER_API_KEY"),
|
|
47
|
+
privacy_mode=os.getenv("PRIVACY_MODE", "false").lower() in {"1", "true", "yes"},
|
|
48
|
+
cache_dir=cache_dir,
|
|
49
|
+
request_timeout_seconds=float(os.getenv("AGENTIC_TIMEOUT_SECONDS", "30.0")),
|
|
50
|
+
max_retries=int(os.getenv("AGENTIC_MAX_RETRIES", "2")),
|
|
51
|
+
retry_backoff_base_seconds=float(os.getenv("AGENTIC_BACKOFF_BASE_SECONDS", "0.2")),
|
|
52
|
+
retry_backoff_factor=float(os.getenv("AGENTIC_BACKOFF_FACTOR", "2.0")),
|
|
53
|
+
circuit_breaker_failure_threshold=int(os.getenv("AGENTIC_CIRCUIT_THRESHOLD", "3")),
|
|
54
|
+
circuit_breaker_open_seconds=int(os.getenv("AGENTIC_CIRCUIT_OPEN_SECONDS", "60")),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
def validate_environment(self, logger: Optional[object] = None) -> None:
|
|
58
|
+
"""Log warnings if required keys are missing for non-privacy mode."""
|
|
59
|
+
def _log(msg: str) -> None:
|
|
60
|
+
try:
|
|
61
|
+
if logger is not None:
|
|
62
|
+
logger.warning(msg)
|
|
63
|
+
else:
|
|
64
|
+
print(msg)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
if self.privacy_mode:
|
|
69
|
+
return
|
|
70
|
+
if not any([self.openai_api_key, self.anthropic_api_key, self.google_api_key, self.openrouter_api_key]):
|
|
71
|
+
_log("No AI provider API keys configured; set PRIVACY_MODE=1 to avoid cloud usage or add provider keys.")
|
|
72
|
+
|
|
73
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Constants for the agentic correction providers module."""
|
|
2
|
+
|
|
3
|
+
# Logging constants
|
|
4
|
+
PROMPT_LOG_LENGTH = 200 # Characters to log from prompts
|
|
5
|
+
RESPONSE_LOG_LENGTH = 500 # Characters to log from responses
|
|
6
|
+
|
|
7
|
+
# Model specification format
|
|
8
|
+
MODEL_SPEC_FORMAT = "provider/model" # Expected format for model identifiers
|
|
9
|
+
|
|
10
|
+
# Default Langfuse host
|
|
11
|
+
DEFAULT_LANGFUSE_HOST = "https://cloud.langfuse.com"
|
|
12
|
+
|
|
13
|
+
# Raw response indicator
|
|
14
|
+
RAW_RESPONSE_KEY = "raw" # Key used to wrap unparsed responses
|
|
15
|
+
|
|
16
|
+
# Error response keys
|
|
17
|
+
ERROR_KEY = "error"
|
|
18
|
+
ERROR_MESSAGE_KEY = "message"
|
|
19
|
+
|
|
20
|
+
# Circuit breaker error types
|
|
21
|
+
CIRCUIT_OPEN_ERROR = "circuit_open"
|
|
22
|
+
MODEL_INIT_ERROR = "model_init_failed"
|
|
23
|
+
PROVIDER_ERROR = "provider_error"
|
|
24
|
+
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from typing import List, Dict, Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_ollama_available() -> bool:
|
|
5
|
+
"""Return True if a local Ollama server responds to a simple list() call.
|
|
6
|
+
|
|
7
|
+
This function is intentionally lightweight and safe to call during setup.
|
|
8
|
+
"""
|
|
9
|
+
try:
|
|
10
|
+
import ollama # type: ignore
|
|
11
|
+
|
|
12
|
+
_ = ollama.list()
|
|
13
|
+
return True
|
|
14
|
+
except Exception:
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_ollama_models() -> List[Dict[str, Any]]:
|
|
19
|
+
"""Return available local models from Ollama if available; otherwise empty list."""
|
|
20
|
+
try:
|
|
21
|
+
import ollama # type: ignore
|
|
22
|
+
|
|
23
|
+
data = ollama.list() or {}
|
|
24
|
+
return data.get("models", []) if isinstance(data, dict) else []
|
|
25
|
+
except Exception:
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
|