karaoke-gen 0.75.54__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.

Potentially problematic release.


This version of karaoke-gen might be problematic. Click here for more details.

Files changed (287) hide show
  1. karaoke_gen/__init__.py +38 -0
  2. karaoke_gen/audio_fetcher.py +1614 -0
  3. karaoke_gen/audio_processor.py +790 -0
  4. karaoke_gen/config.py +83 -0
  5. karaoke_gen/file_handler.py +387 -0
  6. karaoke_gen/instrumental_review/__init__.py +45 -0
  7. karaoke_gen/instrumental_review/analyzer.py +408 -0
  8. karaoke_gen/instrumental_review/editor.py +322 -0
  9. karaoke_gen/instrumental_review/models.py +171 -0
  10. karaoke_gen/instrumental_review/server.py +475 -0
  11. karaoke_gen/instrumental_review/static/index.html +1529 -0
  12. karaoke_gen/instrumental_review/waveform.py +409 -0
  13. karaoke_gen/karaoke_finalise/__init__.py +1 -0
  14. karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
  15. karaoke_gen/karaoke_gen.py +1026 -0
  16. karaoke_gen/lyrics_processor.py +474 -0
  17. karaoke_gen/metadata.py +160 -0
  18. karaoke_gen/pipeline/__init__.py +87 -0
  19. karaoke_gen/pipeline/base.py +215 -0
  20. karaoke_gen/pipeline/context.py +230 -0
  21. karaoke_gen/pipeline/executors/__init__.py +21 -0
  22. karaoke_gen/pipeline/executors/local.py +159 -0
  23. karaoke_gen/pipeline/executors/remote.py +257 -0
  24. karaoke_gen/pipeline/stages/__init__.py +27 -0
  25. karaoke_gen/pipeline/stages/finalize.py +202 -0
  26. karaoke_gen/pipeline/stages/render.py +165 -0
  27. karaoke_gen/pipeline/stages/screens.py +139 -0
  28. karaoke_gen/pipeline/stages/separation.py +191 -0
  29. karaoke_gen/pipeline/stages/transcription.py +191 -0
  30. karaoke_gen/resources/AvenirNext-Bold.ttf +0 -0
  31. karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
  32. karaoke_gen/resources/Oswald-Bold.ttf +0 -0
  33. karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
  34. karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
  35. karaoke_gen/style_loader.py +531 -0
  36. karaoke_gen/utils/__init__.py +18 -0
  37. karaoke_gen/utils/bulk_cli.py +492 -0
  38. karaoke_gen/utils/cli_args.py +432 -0
  39. karaoke_gen/utils/gen_cli.py +978 -0
  40. karaoke_gen/utils/remote_cli.py +3268 -0
  41. karaoke_gen/video_background_processor.py +351 -0
  42. karaoke_gen/video_generator.py +424 -0
  43. karaoke_gen-0.75.54.dist-info/METADATA +718 -0
  44. karaoke_gen-0.75.54.dist-info/RECORD +287 -0
  45. karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
  46. karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
  47. karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -0
  48. lyrics_transcriber/__init__.py +10 -0
  49. lyrics_transcriber/cli/__init__.py +0 -0
  50. lyrics_transcriber/cli/cli_main.py +285 -0
  51. lyrics_transcriber/core/__init__.py +0 -0
  52. lyrics_transcriber/core/config.py +50 -0
  53. lyrics_transcriber/core/controller.py +594 -0
  54. lyrics_transcriber/correction/__init__.py +0 -0
  55. lyrics_transcriber/correction/agentic/__init__.py +9 -0
  56. lyrics_transcriber/correction/agentic/adapter.py +71 -0
  57. lyrics_transcriber/correction/agentic/agent.py +313 -0
  58. lyrics_transcriber/correction/agentic/feedback/aggregator.py +12 -0
  59. lyrics_transcriber/correction/agentic/feedback/collector.py +17 -0
  60. lyrics_transcriber/correction/agentic/feedback/retention.py +24 -0
  61. lyrics_transcriber/correction/agentic/feedback/store.py +76 -0
  62. lyrics_transcriber/correction/agentic/handlers/__init__.py +24 -0
  63. lyrics_transcriber/correction/agentic/handlers/ambiguous.py +44 -0
  64. lyrics_transcriber/correction/agentic/handlers/background_vocals.py +68 -0
  65. lyrics_transcriber/correction/agentic/handlers/base.py +51 -0
  66. lyrics_transcriber/correction/agentic/handlers/complex_multi_error.py +46 -0
  67. lyrics_transcriber/correction/agentic/handlers/extra_words.py +74 -0
  68. lyrics_transcriber/correction/agentic/handlers/no_error.py +42 -0
  69. lyrics_transcriber/correction/agentic/handlers/punctuation.py +44 -0
  70. lyrics_transcriber/correction/agentic/handlers/registry.py +60 -0
  71. lyrics_transcriber/correction/agentic/handlers/repeated_section.py +44 -0
  72. lyrics_transcriber/correction/agentic/handlers/sound_alike.py +126 -0
  73. lyrics_transcriber/correction/agentic/models/__init__.py +5 -0
  74. lyrics_transcriber/correction/agentic/models/ai_correction.py +31 -0
  75. lyrics_transcriber/correction/agentic/models/correction_session.py +30 -0
  76. lyrics_transcriber/correction/agentic/models/enums.py +38 -0
  77. lyrics_transcriber/correction/agentic/models/human_feedback.py +30 -0
  78. lyrics_transcriber/correction/agentic/models/learning_data.py +26 -0
  79. lyrics_transcriber/correction/agentic/models/observability_metrics.py +28 -0
  80. lyrics_transcriber/correction/agentic/models/schemas.py +46 -0
  81. lyrics_transcriber/correction/agentic/models/utils.py +19 -0
  82. lyrics_transcriber/correction/agentic/observability/__init__.py +5 -0
  83. lyrics_transcriber/correction/agentic/observability/langfuse_integration.py +35 -0
  84. lyrics_transcriber/correction/agentic/observability/metrics.py +46 -0
  85. lyrics_transcriber/correction/agentic/observability/performance.py +19 -0
  86. lyrics_transcriber/correction/agentic/prompts/__init__.py +2 -0
  87. lyrics_transcriber/correction/agentic/prompts/classifier.py +227 -0
  88. lyrics_transcriber/correction/agentic/providers/__init__.py +6 -0
  89. lyrics_transcriber/correction/agentic/providers/base.py +36 -0
  90. lyrics_transcriber/correction/agentic/providers/circuit_breaker.py +145 -0
  91. lyrics_transcriber/correction/agentic/providers/config.py +73 -0
  92. lyrics_transcriber/correction/agentic/providers/constants.py +24 -0
  93. lyrics_transcriber/correction/agentic/providers/health.py +28 -0
  94. lyrics_transcriber/correction/agentic/providers/langchain_bridge.py +212 -0
  95. lyrics_transcriber/correction/agentic/providers/model_factory.py +209 -0
  96. lyrics_transcriber/correction/agentic/providers/response_cache.py +218 -0
  97. lyrics_transcriber/correction/agentic/providers/response_parser.py +111 -0
  98. lyrics_transcriber/correction/agentic/providers/retry_executor.py +127 -0
  99. lyrics_transcriber/correction/agentic/router.py +35 -0
  100. lyrics_transcriber/correction/agentic/workflows/__init__.py +5 -0
  101. lyrics_transcriber/correction/agentic/workflows/consensus_workflow.py +24 -0
  102. lyrics_transcriber/correction/agentic/workflows/correction_graph.py +59 -0
  103. lyrics_transcriber/correction/agentic/workflows/feedback_workflow.py +24 -0
  104. lyrics_transcriber/correction/anchor_sequence.py +919 -0
  105. lyrics_transcriber/correction/corrector.py +760 -0
  106. lyrics_transcriber/correction/feedback/__init__.py +2 -0
  107. lyrics_transcriber/correction/feedback/schemas.py +107 -0
  108. lyrics_transcriber/correction/feedback/store.py +236 -0
  109. lyrics_transcriber/correction/handlers/__init__.py +0 -0
  110. lyrics_transcriber/correction/handlers/base.py +52 -0
  111. lyrics_transcriber/correction/handlers/extend_anchor.py +149 -0
  112. lyrics_transcriber/correction/handlers/levenshtein.py +189 -0
  113. lyrics_transcriber/correction/handlers/llm.py +293 -0
  114. lyrics_transcriber/correction/handlers/llm_providers.py +60 -0
  115. lyrics_transcriber/correction/handlers/no_space_punct_match.py +154 -0
  116. lyrics_transcriber/correction/handlers/relaxed_word_count_match.py +85 -0
  117. lyrics_transcriber/correction/handlers/repeat.py +88 -0
  118. lyrics_transcriber/correction/handlers/sound_alike.py +259 -0
  119. lyrics_transcriber/correction/handlers/syllables_match.py +252 -0
  120. lyrics_transcriber/correction/handlers/word_count_match.py +80 -0
  121. lyrics_transcriber/correction/handlers/word_operations.py +187 -0
  122. lyrics_transcriber/correction/operations.py +352 -0
  123. lyrics_transcriber/correction/phrase_analyzer.py +435 -0
  124. lyrics_transcriber/correction/text_utils.py +30 -0
  125. lyrics_transcriber/frontend/.gitignore +23 -0
  126. lyrics_transcriber/frontend/.yarn/releases/yarn-4.7.0.cjs +935 -0
  127. lyrics_transcriber/frontend/.yarnrc.yml +3 -0
  128. lyrics_transcriber/frontend/README.md +50 -0
  129. lyrics_transcriber/frontend/REPLACE_ALL_FUNCTIONALITY.md +210 -0
  130. lyrics_transcriber/frontend/__init__.py +25 -0
  131. lyrics_transcriber/frontend/eslint.config.js +28 -0
  132. lyrics_transcriber/frontend/index.html +18 -0
  133. lyrics_transcriber/frontend/package.json +42 -0
  134. lyrics_transcriber/frontend/public/android-chrome-192x192.png +0 -0
  135. lyrics_transcriber/frontend/public/android-chrome-512x512.png +0 -0
  136. lyrics_transcriber/frontend/public/apple-touch-icon.png +0 -0
  137. lyrics_transcriber/frontend/public/favicon-16x16.png +0 -0
  138. lyrics_transcriber/frontend/public/favicon-32x32.png +0 -0
  139. lyrics_transcriber/frontend/public/favicon.ico +0 -0
  140. lyrics_transcriber/frontend/public/nomad-karaoke-logo.png +0 -0
  141. lyrics_transcriber/frontend/src/App.tsx +214 -0
  142. lyrics_transcriber/frontend/src/api.ts +254 -0
  143. lyrics_transcriber/frontend/src/components/AIFeedbackModal.tsx +77 -0
  144. lyrics_transcriber/frontend/src/components/AddLyricsModal.tsx +114 -0
  145. lyrics_transcriber/frontend/src/components/AgenticCorrectionMetrics.tsx +204 -0
  146. lyrics_transcriber/frontend/src/components/AudioPlayer.tsx +180 -0
  147. lyrics_transcriber/frontend/src/components/CorrectedWordWithActions.tsx +167 -0
  148. lyrics_transcriber/frontend/src/components/CorrectionAnnotationModal.tsx +359 -0
  149. lyrics_transcriber/frontend/src/components/CorrectionDetailCard.tsx +281 -0
  150. lyrics_transcriber/frontend/src/components/CorrectionMetrics.tsx +162 -0
  151. lyrics_transcriber/frontend/src/components/DurationTimelineView.tsx +257 -0
  152. lyrics_transcriber/frontend/src/components/EditActionBar.tsx +68 -0
  153. lyrics_transcriber/frontend/src/components/EditModal.tsx +702 -0
  154. lyrics_transcriber/frontend/src/components/EditTimelineSection.tsx +496 -0
  155. lyrics_transcriber/frontend/src/components/EditWordList.tsx +379 -0
  156. lyrics_transcriber/frontend/src/components/FileUpload.tsx +77 -0
  157. lyrics_transcriber/frontend/src/components/FindReplaceModal.tsx +467 -0
  158. lyrics_transcriber/frontend/src/components/Header.tsx +413 -0
  159. lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
  160. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
  161. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
  162. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
  163. lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
  164. lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
  165. lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -0
  166. lyrics_transcriber/frontend/src/components/ModeSelector.tsx +67 -0
  167. lyrics_transcriber/frontend/src/components/ModelSelector.tsx +23 -0
  168. lyrics_transcriber/frontend/src/components/PreviewVideoSection.tsx +144 -0
  169. lyrics_transcriber/frontend/src/components/ReferenceView.tsx +268 -0
  170. lyrics_transcriber/frontend/src/components/ReplaceAllLyricsModal.tsx +336 -0
  171. lyrics_transcriber/frontend/src/components/ReviewChangesModal.tsx +354 -0
  172. lyrics_transcriber/frontend/src/components/SegmentDetailsModal.tsx +64 -0
  173. lyrics_transcriber/frontend/src/components/TimelineEditor.tsx +376 -0
  174. lyrics_transcriber/frontend/src/components/TimingOffsetModal.tsx +131 -0
  175. lyrics_transcriber/frontend/src/components/TranscriptionView.tsx +256 -0
  176. lyrics_transcriber/frontend/src/components/WordDivider.tsx +187 -0
  177. lyrics_transcriber/frontend/src/components/shared/components/HighlightedText.tsx +379 -0
  178. lyrics_transcriber/frontend/src/components/shared/components/SourceSelector.tsx +56 -0
  179. lyrics_transcriber/frontend/src/components/shared/components/Word.tsx +87 -0
  180. lyrics_transcriber/frontend/src/components/shared/constants.ts +20 -0
  181. lyrics_transcriber/frontend/src/components/shared/hooks/useWordClick.ts +180 -0
  182. lyrics_transcriber/frontend/src/components/shared/styles.ts +13 -0
  183. lyrics_transcriber/frontend/src/components/shared/types.js +2 -0
  184. lyrics_transcriber/frontend/src/components/shared/types.ts +129 -0
  185. lyrics_transcriber/frontend/src/components/shared/utils/keyboardHandlers.ts +177 -0
  186. lyrics_transcriber/frontend/src/components/shared/utils/localStorage.ts +78 -0
  187. lyrics_transcriber/frontend/src/components/shared/utils/referenceLineCalculator.ts +75 -0
  188. lyrics_transcriber/frontend/src/components/shared/utils/segmentOperations.ts +360 -0
  189. lyrics_transcriber/frontend/src/components/shared/utils/timingUtils.ts +110 -0
  190. lyrics_transcriber/frontend/src/components/shared/utils/wordUtils.ts +22 -0
  191. lyrics_transcriber/frontend/src/hooks/useManualSync.ts +435 -0
  192. lyrics_transcriber/frontend/src/main.tsx +17 -0
  193. lyrics_transcriber/frontend/src/theme.ts +177 -0
  194. lyrics_transcriber/frontend/src/types/global.d.ts +9 -0
  195. lyrics_transcriber/frontend/src/types.js +2 -0
  196. lyrics_transcriber/frontend/src/types.ts +199 -0
  197. lyrics_transcriber/frontend/src/validation.ts +132 -0
  198. lyrics_transcriber/frontend/src/vite-env.d.ts +1 -0
  199. lyrics_transcriber/frontend/tsconfig.app.json +26 -0
  200. lyrics_transcriber/frontend/tsconfig.json +25 -0
  201. lyrics_transcriber/frontend/tsconfig.node.json +23 -0
  202. lyrics_transcriber/frontend/tsconfig.tsbuildinfo +1 -0
  203. lyrics_transcriber/frontend/update_version.js +11 -0
  204. lyrics_transcriber/frontend/vite.config.d.ts +2 -0
  205. lyrics_transcriber/frontend/vite.config.js +10 -0
  206. lyrics_transcriber/frontend/vite.config.ts +11 -0
  207. lyrics_transcriber/frontend/web_assets/android-chrome-192x192.png +0 -0
  208. lyrics_transcriber/frontend/web_assets/android-chrome-512x512.png +0 -0
  209. lyrics_transcriber/frontend/web_assets/apple-touch-icon.png +0 -0
  210. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js +43288 -0
  211. lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.js.map +1 -0
  212. lyrics_transcriber/frontend/web_assets/favicon-16x16.png +0 -0
  213. lyrics_transcriber/frontend/web_assets/favicon-32x32.png +0 -0
  214. lyrics_transcriber/frontend/web_assets/favicon.ico +0 -0
  215. lyrics_transcriber/frontend/web_assets/index.html +18 -0
  216. lyrics_transcriber/frontend/web_assets/nomad-karaoke-logo.png +0 -0
  217. lyrics_transcriber/frontend/yarn.lock +3752 -0
  218. lyrics_transcriber/lyrics/__init__.py +0 -0
  219. lyrics_transcriber/lyrics/base_lyrics_provider.py +211 -0
  220. lyrics_transcriber/lyrics/file_provider.py +95 -0
  221. lyrics_transcriber/lyrics/genius.py +384 -0
  222. lyrics_transcriber/lyrics/lrclib.py +231 -0
  223. lyrics_transcriber/lyrics/musixmatch.py +156 -0
  224. lyrics_transcriber/lyrics/spotify.py +290 -0
  225. lyrics_transcriber/lyrics/user_input_provider.py +44 -0
  226. lyrics_transcriber/output/__init__.py +0 -0
  227. lyrics_transcriber/output/ass/__init__.py +21 -0
  228. lyrics_transcriber/output/ass/ass.py +2088 -0
  229. lyrics_transcriber/output/ass/ass_specs.txt +732 -0
  230. lyrics_transcriber/output/ass/config.py +180 -0
  231. lyrics_transcriber/output/ass/constants.py +23 -0
  232. lyrics_transcriber/output/ass/event.py +94 -0
  233. lyrics_transcriber/output/ass/formatters.py +132 -0
  234. lyrics_transcriber/output/ass/lyrics_line.py +265 -0
  235. lyrics_transcriber/output/ass/lyrics_screen.py +252 -0
  236. lyrics_transcriber/output/ass/section_detector.py +89 -0
  237. lyrics_transcriber/output/ass/section_screen.py +106 -0
  238. lyrics_transcriber/output/ass/style.py +187 -0
  239. lyrics_transcriber/output/cdg.py +619 -0
  240. lyrics_transcriber/output/cdgmaker/__init__.py +0 -0
  241. lyrics_transcriber/output/cdgmaker/cdg.py +262 -0
  242. lyrics_transcriber/output/cdgmaker/composer.py +2260 -0
  243. lyrics_transcriber/output/cdgmaker/config.py +151 -0
  244. lyrics_transcriber/output/cdgmaker/images/instrumental.png +0 -0
  245. lyrics_transcriber/output/cdgmaker/images/intro.png +0 -0
  246. lyrics_transcriber/output/cdgmaker/pack.py +507 -0
  247. lyrics_transcriber/output/cdgmaker/render.py +346 -0
  248. lyrics_transcriber/output/cdgmaker/transitions/centertexttoplogobottomtext.png +0 -0
  249. lyrics_transcriber/output/cdgmaker/transitions/circlein.png +0 -0
  250. lyrics_transcriber/output/cdgmaker/transitions/circleout.png +0 -0
  251. lyrics_transcriber/output/cdgmaker/transitions/fizzle.png +0 -0
  252. lyrics_transcriber/output/cdgmaker/transitions/largecentertexttoplogo.png +0 -0
  253. lyrics_transcriber/output/cdgmaker/transitions/rectangle.png +0 -0
  254. lyrics_transcriber/output/cdgmaker/transitions/spiral.png +0 -0
  255. lyrics_transcriber/output/cdgmaker/transitions/topleftmusicalnotes.png +0 -0
  256. lyrics_transcriber/output/cdgmaker/transitions/wipein.png +0 -0
  257. lyrics_transcriber/output/cdgmaker/transitions/wipeleft.png +0 -0
  258. lyrics_transcriber/output/cdgmaker/transitions/wipeout.png +0 -0
  259. lyrics_transcriber/output/cdgmaker/transitions/wiperight.png +0 -0
  260. lyrics_transcriber/output/cdgmaker/utils.py +132 -0
  261. lyrics_transcriber/output/countdown_processor.py +306 -0
  262. lyrics_transcriber/output/fonts/AvenirNext-Bold.ttf +0 -0
  263. lyrics_transcriber/output/fonts/DMSans-VariableFont_opsz,wght.ttf +0 -0
  264. lyrics_transcriber/output/fonts/DMSerifDisplay-Regular.ttf +0 -0
  265. lyrics_transcriber/output/fonts/Oswald-SemiBold.ttf +0 -0
  266. lyrics_transcriber/output/fonts/Zurich_Cn_BT_Bold.ttf +0 -0
  267. lyrics_transcriber/output/fonts/arial.ttf +0 -0
  268. lyrics_transcriber/output/fonts/georgia.ttf +0 -0
  269. lyrics_transcriber/output/fonts/verdana.ttf +0 -0
  270. lyrics_transcriber/output/generator.py +257 -0
  271. lyrics_transcriber/output/lrc_to_cdg.py +61 -0
  272. lyrics_transcriber/output/lyrics_file.py +102 -0
  273. lyrics_transcriber/output/plain_text.py +96 -0
  274. lyrics_transcriber/output/segment_resizer.py +431 -0
  275. lyrics_transcriber/output/subtitles.py +397 -0
  276. lyrics_transcriber/output/video.py +544 -0
  277. lyrics_transcriber/review/__init__.py +0 -0
  278. lyrics_transcriber/review/server.py +676 -0
  279. lyrics_transcriber/storage/__init__.py +0 -0
  280. lyrics_transcriber/storage/dropbox.py +225 -0
  281. lyrics_transcriber/transcribers/__init__.py +0 -0
  282. lyrics_transcriber/transcribers/audioshake.py +379 -0
  283. lyrics_transcriber/transcribers/base_transcriber.py +157 -0
  284. lyrics_transcriber/transcribers/whisper.py +330 -0
  285. lyrics_transcriber/types.py +650 -0
  286. lyrics_transcriber/utils/__init__.py +0 -0
  287. lyrics_transcriber/utils/word_utils.py +27 -0
@@ -0,0 +1,218 @@
1
+ """Response caching for LLM calls to avoid redundant API requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import hashlib
7
+ import logging
8
+ from pathlib import Path
9
+ from typing import Optional, Dict, Any
10
+ from datetime import datetime
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class ResponseCache:
16
+ """Caches LLM responses based on prompt hash.
17
+
18
+ This allows reusing responses when iterating on frontend/UI changes
19
+ without re-running expensive LLM inference calls.
20
+
21
+ Cache Structure:
22
+ {
23
+ "prompt_hash": {
24
+ "prompt": "full prompt text",
25
+ "response": "llm response",
26
+ "timestamp": "iso datetime",
27
+ "model": "model identifier",
28
+ "metadata": {...}
29
+ }
30
+ }
31
+ """
32
+
33
+ def __init__(self, cache_dir: str = "cache", enabled: bool = True):
34
+ """Initialize response cache.
35
+
36
+ Args:
37
+ cache_dir: Directory to store cache file
38
+ enabled: Whether caching is enabled (can be disabled via env var)
39
+ """
40
+ self.cache_dir = Path(cache_dir)
41
+ self.cache_dir.mkdir(parents=True, exist_ok=True)
42
+ self.cache_file = self.cache_dir / "llm_response_cache.json"
43
+ self.enabled = enabled
44
+ self._cache: Dict[str, Dict[str, Any]] = {}
45
+ self._load_cache()
46
+
47
+ def _load_cache(self) -> None:
48
+ """Load cache from disk."""
49
+ if not self.cache_file.exists():
50
+ self._cache = {}
51
+ return
52
+
53
+ try:
54
+ with open(self.cache_file, 'r', encoding='utf-8') as f:
55
+ self._cache = json.load(f)
56
+ logger.debug(f"📦 Loaded {len(self._cache)} cached responses")
57
+ except Exception as e:
58
+ logger.warning(f"Failed to load cache: {e}")
59
+ self._cache = {}
60
+
61
+ def _save_cache(self) -> None:
62
+ """Save cache to disk."""
63
+ try:
64
+ with open(self.cache_file, 'w', encoding='utf-8') as f:
65
+ json.dump(self._cache, f, indent=2, ensure_ascii=False)
66
+ logger.debug(f"💾 Saved {len(self._cache)} cached responses")
67
+ except Exception as e:
68
+ logger.warning(f"Failed to save cache: {e}")
69
+
70
+ def _compute_hash(self, prompt: str, model: str) -> str:
71
+ """Compute hash for prompt + model combination.
72
+
73
+ Args:
74
+ prompt: The full prompt text
75
+ model: Model identifier
76
+
77
+ Returns:
78
+ SHA256 hash as hex string
79
+ """
80
+ # Include both prompt and model in hash
81
+ combined = f"{model}::{prompt}"
82
+ return hashlib.sha256(combined.encode('utf-8')).hexdigest()
83
+
84
+ def get(self, prompt: str, model: str) -> Optional[str]:
85
+ """Get cached response for prompt if available.
86
+
87
+ Args:
88
+ prompt: The prompt text
89
+ model: Model identifier
90
+
91
+ Returns:
92
+ Cached response string or None if not found
93
+ """
94
+ if not self.enabled:
95
+ return None
96
+
97
+ prompt_hash = self._compute_hash(prompt, model)
98
+
99
+ if prompt_hash in self._cache:
100
+ cached = self._cache[prompt_hash]
101
+ logger.info(f"🎯 Cache HIT for {model} (hash: {prompt_hash[:8]}...)")
102
+ logger.debug(f" Cached at: {cached.get('timestamp')}")
103
+ return cached.get('response')
104
+
105
+ logger.debug(f"📭 Cache MISS for {model} (hash: {prompt_hash[:8]}...)")
106
+ return None
107
+
108
+ def set(
109
+ self,
110
+ prompt: str,
111
+ model: str,
112
+ response: str,
113
+ metadata: Optional[Dict[str, Any]] = None
114
+ ) -> None:
115
+ """Store response in cache.
116
+
117
+ Args:
118
+ prompt: The prompt text
119
+ model: Model identifier
120
+ response: The LLM response
121
+ metadata: Optional metadata to store with cache entry
122
+ """
123
+ if not self.enabled:
124
+ return
125
+
126
+ prompt_hash = self._compute_hash(prompt, model)
127
+
128
+ self._cache[prompt_hash] = {
129
+ "prompt": prompt[:500] + "..." if len(prompt) > 500 else prompt, # Truncate for readability
130
+ "response": response,
131
+ "timestamp": datetime.utcnow().isoformat(),
132
+ "model": model,
133
+ "metadata": metadata or {}
134
+ }
135
+
136
+ # Save to disk immediately (for persistence across runs)
137
+ self._save_cache()
138
+ logger.debug(f"💾 Cached response for {model} (hash: {prompt_hash[:8]}...)")
139
+
140
+ def clear(self) -> int:
141
+ """Clear all cached responses.
142
+
143
+ Returns:
144
+ Number of entries cleared
145
+ """
146
+ count = len(self._cache)
147
+ self._cache = {}
148
+ self._save_cache()
149
+ logger.info(f"🗑️ Cleared {count} cached responses")
150
+ return count
151
+
152
+ def get_stats(self) -> Dict[str, Any]:
153
+ """Get cache statistics.
154
+
155
+ Returns:
156
+ Dictionary with cache statistics
157
+ """
158
+ if not self._cache:
159
+ return {
160
+ "total_entries": 0,
161
+ "cache_file": str(self.cache_file),
162
+ "enabled": self.enabled
163
+ }
164
+
165
+ # Count by model
166
+ by_model = {}
167
+ for entry in self._cache.values():
168
+ model = entry.get('model', 'unknown')
169
+ by_model[model] = by_model.get(model, 0) + 1
170
+
171
+ # Find oldest and newest
172
+ timestamps = [
173
+ datetime.fromisoformat(entry['timestamp'])
174
+ for entry in self._cache.values()
175
+ if 'timestamp' in entry
176
+ ]
177
+
178
+ return {
179
+ "total_entries": len(self._cache),
180
+ "by_model": by_model,
181
+ "oldest": min(timestamps).isoformat() if timestamps else None,
182
+ "newest": max(timestamps).isoformat() if timestamps else None,
183
+ "cache_file": str(self.cache_file),
184
+ "enabled": self.enabled
185
+ }
186
+
187
+ def prune_old_entries(self, days: int = 30) -> int:
188
+ """Remove cache entries older than specified days.
189
+
190
+ Args:
191
+ days: Remove entries older than this many days
192
+
193
+ Returns:
194
+ Number of entries removed
195
+ """
196
+ from datetime import timedelta
197
+
198
+ cutoff = datetime.utcnow() - timedelta(days=days)
199
+
200
+ to_remove = []
201
+ for prompt_hash, entry in self._cache.items():
202
+ if 'timestamp' in entry:
203
+ try:
204
+ entry_time = datetime.fromisoformat(entry['timestamp'])
205
+ if entry_time < cutoff:
206
+ to_remove.append(prompt_hash)
207
+ except Exception:
208
+ pass
209
+
210
+ for prompt_hash in to_remove:
211
+ del self._cache[prompt_hash]
212
+
213
+ if to_remove:
214
+ self._save_cache()
215
+ logger.info(f"🗑️ Pruned {len(to_remove)} old cache entries (older than {days} days)")
216
+
217
+ return len(to_remove)
218
+
@@ -0,0 +1,111 @@
1
+ """Parser for LLM responses into structured correction proposals."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import logging
6
+ from typing import List, Dict, Any
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ResponseParser:
12
+ """Parses LLM responses into structured proposal dictionaries.
13
+
14
+ Handles both JSON and raw text responses, providing consistent
15
+ output format for downstream processing.
16
+
17
+ Single Responsibility: Response parsing only, no model invocation.
18
+ """
19
+
20
+ def parse(self, content: str) -> List[Dict[str, Any]]:
21
+ """Parse response content into proposal dictionaries.
22
+
23
+ Attempts to parse as JSON first. If that fails, tries to fix
24
+ common JSON issues and retries. Falls back to raw content.
25
+
26
+ Args:
27
+ content: Raw response content from LLM
28
+
29
+ Returns:
30
+ List of proposal dictionaries. On parse failure, returns
31
+ [{"raw": content}] to preserve the response.
32
+ """
33
+ # Try JSON parsing first
34
+ try:
35
+ data = json.loads(content)
36
+ return self._normalize_json_response(data)
37
+ except json.JSONDecodeError as e:
38
+ logger.debug(f"🤖 Response is not valid JSON: {e}")
39
+
40
+ # Try to fix common issues
41
+ fixed_content = self._attempt_json_fix(content)
42
+ if fixed_content != content:
43
+ try:
44
+ data = json.loads(fixed_content)
45
+ logger.debug("🤖 Successfully parsed after JSON fix")
46
+ return self._normalize_json_response(data)
47
+ except json.JSONDecodeError:
48
+ pass # Fall through to raw handling
49
+
50
+ return self._handle_raw_response(content)
51
+
52
+ def _attempt_json_fix(self, content: str) -> str:
53
+ """Attempt to fix common JSON formatting issues.
54
+
55
+ Args:
56
+ content: Raw JSON string
57
+
58
+ Returns:
59
+ Fixed JSON string (or original if no fixes applied)
60
+ """
61
+ # Fix 1: Replace invalid escape sequences like \' with '
62
+ # (JSON only allows \", \\, \/, \b, \f, \n, \r, \t)
63
+ fixed = content.replace("\\'", "'")
64
+
65
+ # Fix 2: Remove any trailing commas before } or ]
66
+ import re
67
+ fixed = re.sub(r',\s*}', '}', fixed)
68
+ fixed = re.sub(r',\s*]', ']', fixed)
69
+
70
+ return fixed
71
+
72
+ def _normalize_json_response(self, data: Any) -> List[Dict[str, Any]]:
73
+ """Normalize JSON data into a list of dictionaries.
74
+
75
+ Handles both single dict and list of dicts responses.
76
+
77
+ Args:
78
+ data: Parsed JSON data
79
+
80
+ Returns:
81
+ List of dictionaries
82
+ """
83
+ if isinstance(data, dict):
84
+ # Single proposal - wrap in list
85
+ return [data]
86
+ elif isinstance(data, list):
87
+ # Already a list - return as-is
88
+ return data
89
+ else:
90
+ # Unexpected type - wrap in error dict
91
+ logger.warning(f"🤖 Unexpected JSON type: {type(data)}")
92
+ return [{"error": "unexpected_type", "data": str(data)}]
93
+
94
+ def _handle_raw_response(self, content: str) -> List[Dict[str, Any]]:
95
+ """Handle non-JSON responses.
96
+
97
+ Wraps raw content in a dict for downstream handling.
98
+ The "raw" key indicates this needs manual processing.
99
+
100
+ Args:
101
+ content: Raw response text
102
+
103
+ Returns:
104
+ List with single dict containing raw content
105
+ """
106
+ logger.info(
107
+ f"🤖 Returning raw response (non-JSON): "
108
+ f"{content[:100]}{'...' if len(content) > 100 else ''}"
109
+ )
110
+ return [{"raw": content}]
111
+
@@ -0,0 +1,127 @@
1
+ """Retry execution logic with exponential backoff."""
2
+ from __future__ import annotations
3
+
4
+ import time
5
+ import random
6
+ import logging
7
+ from typing import Callable, TypeVar, Generic
8
+ from dataclasses import dataclass
9
+
10
+ from .config import ProviderConfig
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ T = TypeVar('T')
15
+
16
+
17
+ @dataclass
18
+ class ExecutionResult(Generic[T]):
19
+ """Result of a retry execution attempt.
20
+
21
+ Attributes:
22
+ success: Whether execution succeeded
23
+ value: The return value if successful
24
+ error: Error message if failed
25
+ attempts: Number of attempts made
26
+ """
27
+ success: bool
28
+ value: T | None = None
29
+ error: str | None = None
30
+ attempts: int = 0
31
+
32
+
33
+ class RetryExecutor:
34
+ """Executes operations with retry logic and exponential backoff.
35
+
36
+ Implements exponential backoff with jitter to prevent thundering herd.
37
+
38
+ Single Responsibility: Retry logic only, no model-specific behavior.
39
+ """
40
+
41
+ def __init__(self, config: ProviderConfig):
42
+ """Initialize retry executor with configuration.
43
+
44
+ Args:
45
+ config: Provider configuration with retry parameters
46
+ """
47
+ self._config = config
48
+
49
+ def execute_with_retry(
50
+ self,
51
+ operation: Callable[[], T],
52
+ operation_name: str = "operation"
53
+ ) -> ExecutionResult[T]:
54
+ """Execute operation with retry logic.
55
+
56
+ Args:
57
+ operation: Callable that performs the operation
58
+ operation_name: Name for logging purposes
59
+
60
+ Returns:
61
+ ExecutionResult with success/failure status and value/error
62
+ """
63
+ max_attempts = max(1, self._config.max_retries + 1)
64
+ last_error: Exception | None = None
65
+
66
+ for attempt in range(max_attempts):
67
+ try:
68
+ logger.debug(
69
+ f"🤖 Executing {operation_name} "
70
+ f"(attempt {attempt + 1}/{max_attempts})"
71
+ )
72
+
73
+ result = operation()
74
+
75
+ logger.debug(f"🤖 {operation_name} succeeded on attempt {attempt + 1}")
76
+ return ExecutionResult(
77
+ success=True,
78
+ value=result,
79
+ attempts=attempt + 1
80
+ )
81
+
82
+ except Exception as e:
83
+ last_error = e
84
+ logger.warning(
85
+ f"🤖 {operation_name} failed on attempt {attempt + 1}: {e}"
86
+ )
87
+
88
+ # Don't sleep after the last attempt
89
+ if attempt < max_attempts - 1:
90
+ sleep_duration = self._calculate_backoff(attempt)
91
+ logger.debug(f"🤖 Backing off for {sleep_duration:.2f}s")
92
+ time.sleep(sleep_duration)
93
+
94
+ # All attempts failed
95
+ error_msg = str(last_error) if last_error else "unknown error"
96
+ logger.error(
97
+ f"🤖 {operation_name} failed after {max_attempts} attempts: {error_msg}"
98
+ )
99
+
100
+ return ExecutionResult(
101
+ success=False,
102
+ error=error_msg,
103
+ attempts=max_attempts
104
+ )
105
+
106
+ def _calculate_backoff(self, attempt: int) -> float:
107
+ """Calculate backoff duration with exponential backoff and jitter.
108
+
109
+ Formula: base * (factor ^ attempt) + random_jitter
110
+
111
+ Args:
112
+ attempt: Current attempt number (0-indexed)
113
+
114
+ Returns:
115
+ Sleep duration in seconds
116
+ """
117
+ base = self._config.retry_backoff_base_seconds
118
+ factor = self._config.retry_backoff_factor
119
+
120
+ # Exponential backoff
121
+ backoff = base * (factor ** attempt)
122
+
123
+ # Add jitter (0-50ms) to prevent thundering herd
124
+ jitter = random.uniform(0, 0.05)
125
+
126
+ return backoff + jitter
127
+
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from typing import Dict, Any
5
+
6
+ from .providers.config import ProviderConfig
7
+
8
+
9
+ class ModelRouter:
10
+ """Rules-based routing by gap type/length/uncertainty (scaffold)."""
11
+
12
+ def __init__(self, config: ProviderConfig | None = None):
13
+ self._config = config or ProviderConfig.from_env()
14
+
15
+ def choose_model(self, gap_type: str, uncertainty: float) -> str:
16
+ """Choose appropriate model based on gap characteristics.
17
+
18
+ Returns model identifier in format "provider/model" for LangChain:
19
+ - "ollama/gpt-oss:latest" for local Ollama models
20
+ - "openai/gpt-4" for OpenAI models
21
+ - "anthropic/claude-3-sonnet-20240229" for Anthropic models
22
+ """
23
+ # Simple baseline per technical guidance
24
+ if self._config.privacy_mode:
25
+ # Use the actual model from env, or default to a common Ollama model
26
+ return os.getenv("AGENTIC_AI_MODEL", "ollama/gpt-oss:latest")
27
+
28
+ # For high-uncertainty gaps, use Claude (best reasoning)
29
+ if uncertainty > 0.5:
30
+ return "anthropic/claude-3-sonnet-20240229"
31
+
32
+ # Default to GPT-4 for general cases
33
+ return "openai/gpt-4"
34
+
35
+
@@ -0,0 +1,5 @@
1
+ """LangGraph workflows for agentic correction (scaffold)."""
2
+
3
+ __all__ = []
4
+
5
+
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ def build_consensus_workflow() -> Any:
7
+ """Return a minimal consensus workflow (scaffold).
8
+
9
+ Returns None if langgraph not installed to avoid hard dependency.
10
+ """
11
+ try:
12
+ from langgraph.graph import StateGraph # type: ignore
13
+ except Exception:
14
+ return None
15
+
16
+ def merge_results(state: Dict[str, Any]) -> Dict[str, Any]:
17
+ return state
18
+
19
+ g = StateGraph(dict)
20
+ g.add_node("MergeResults", merge_results)
21
+ g.set_entry_point("MergeResults")
22
+ return g.compile()
23
+
24
+
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict, Any, List, Annotated
4
+ from typing_extensions import TypedDict
5
+
6
+
7
+ class CorrectionState(TypedDict):
8
+ """State for the correction workflow.
9
+
10
+ This is a minimal state for now, but can be expanded as we add
11
+ more sophisticated correction logic (e.g., multi-step reasoning,
12
+ validation loops, etc.)
13
+ """
14
+ prompt: str
15
+ proposals: List[Dict[str, Any]]
16
+
17
+
18
+ def build_correction_graph(callbacks=None) -> Any:
19
+ """Build a LangGraph workflow for lyrics correction.
20
+
21
+ Currently a simple pass-through, but structured to allow future
22
+ expansion with multi-step reasoning, validation loops, etc.
23
+
24
+ Args:
25
+ callbacks: Optional callbacks (e.g., Langfuse handlers) to attach
26
+
27
+ Returns:
28
+ Compiled LangGraph or None if LangGraph not installed
29
+ """
30
+ try:
31
+ from langgraph.graph import StateGraph, END
32
+ except ImportError:
33
+ return None
34
+
35
+ def correction_node(state: CorrectionState) -> CorrectionState:
36
+ """Main correction node - currently a pass-through.
37
+
38
+ Future expansion: This could invoke sub-agents, do multi-step
39
+ reasoning, or implement validation loops.
40
+ """
41
+ # For now, just pass through - actual correction happens in provider
42
+ return state
43
+
44
+ # Build the graph
45
+ graph_builder = StateGraph(CorrectionState)
46
+ graph_builder.add_node("correct", correction_node)
47
+ graph_builder.set_entry_point("correct")
48
+ graph_builder.set_finish_point("correct")
49
+
50
+ # Compile with optional callbacks
51
+ # Note: Per Langfuse docs, we can use .with_config() to add callbacks
52
+ compiled = graph_builder.compile()
53
+
54
+ if callbacks:
55
+ return compiled.with_config({"callbacks": callbacks})
56
+
57
+ return compiled
58
+
59
+
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+
6
+ def build_feedback_workflow() -> Any:
7
+ """Return a minimal feedback processing workflow (scaffold).
8
+
9
+ Returns None if langgraph not installed to avoid hard dependency.
10
+ """
11
+ try:
12
+ from langgraph.graph import StateGraph # type: ignore
13
+ except Exception:
14
+ return None
15
+
16
+ def process_feedback(state: Dict[str, Any]) -> Dict[str, Any]:
17
+ return state
18
+
19
+ g = StateGraph(dict)
20
+ g.add_node("ProcessFeedback", process_feedback)
21
+ g.set_entry_point("ProcessFeedback")
22
+ return g.compile()
23
+
24
+