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.
- karaoke_gen/__init__.py +38 -0
- karaoke_gen/audio_fetcher.py +1614 -0
- karaoke_gen/audio_processor.py +790 -0
- karaoke_gen/config.py +83 -0
- karaoke_gen/file_handler.py +387 -0
- karaoke_gen/instrumental_review/__init__.py +45 -0
- karaoke_gen/instrumental_review/analyzer.py +408 -0
- karaoke_gen/instrumental_review/editor.py +322 -0
- karaoke_gen/instrumental_review/models.py +171 -0
- karaoke_gen/instrumental_review/server.py +475 -0
- karaoke_gen/instrumental_review/static/index.html +1529 -0
- karaoke_gen/instrumental_review/waveform.py +409 -0
- karaoke_gen/karaoke_finalise/__init__.py +1 -0
- karaoke_gen/karaoke_finalise/karaoke_finalise.py +1833 -0
- karaoke_gen/karaoke_gen.py +1026 -0
- karaoke_gen/lyrics_processor.py +474 -0
- karaoke_gen/metadata.py +160 -0
- 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/resources/AvenirNext-Bold.ttf +0 -0
- karaoke_gen/resources/Montserrat-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-Bold.ttf +0 -0
- karaoke_gen/resources/Oswald-SemiBold.ttf +0 -0
- karaoke_gen/resources/Zurich_Cn_BT_Bold.ttf +0 -0
- karaoke_gen/style_loader.py +531 -0
- karaoke_gen/utils/__init__.py +18 -0
- karaoke_gen/utils/bulk_cli.py +492 -0
- karaoke_gen/utils/cli_args.py +432 -0
- karaoke_gen/utils/gen_cli.py +978 -0
- karaoke_gen/utils/remote_cli.py +3268 -0
- karaoke_gen/video_background_processor.py +351 -0
- karaoke_gen/video_generator.py +424 -0
- karaoke_gen-0.75.54.dist-info/METADATA +718 -0
- karaoke_gen-0.75.54.dist-info/RECORD +287 -0
- karaoke_gen-0.75.54.dist-info/WHEEL +4 -0
- karaoke_gen-0.75.54.dist-info/entry_points.txt +5 -0
- karaoke_gen-0.75.54.dist-info/licenses/LICENSE +21 -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 +594 -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 +919 -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 +214 -0
- lyrics_transcriber/frontend/src/api.ts +254 -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 +413 -0
- lyrics_transcriber/frontend/src/components/LyricsAnalyzer.tsx +1387 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/SyncControls.tsx +185 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/TimelineCanvas.tsx +704 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/UpcomingWordsBar.tsx +80 -0
- lyrics_transcriber/frontend/src/components/LyricsSynchronizer/index.tsx +905 -0
- lyrics_transcriber/frontend/src/components/MetricsDashboard.tsx +51 -0
- lyrics_transcriber/frontend/src/components/ModeSelectionModal.tsx +127 -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 +336 -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-BECn1o8Q.js +43288 -0
- lyrics_transcriber/frontend/web_assets/assets/index-BECn1o8Q.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 +306 -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 +379 -0
- lyrics_transcriber/transcribers/base_transcriber.py +157 -0
- lyrics_transcriber/transcribers/whisper.py +330 -0
- lyrics_transcriber/types.py +650 -0
- lyrics_transcriber/utils/__init__.py +0 -0
- lyrics_transcriber/utils/word_utils.py +27 -0
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import socket
|
|
3
|
+
from fastapi import FastAPI, Body, HTTPException
|
|
4
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from lyrics_transcriber.types import CorrectionResult, WordCorrection, LyricsSegment, LyricsData, LyricsMetadata, Word
|
|
7
|
+
import time
|
|
8
|
+
import os
|
|
9
|
+
import urllib.parse
|
|
10
|
+
from fastapi.staticfiles import StaticFiles
|
|
11
|
+
from fastapi.responses import FileResponse, JSONResponse
|
|
12
|
+
import hashlib
|
|
13
|
+
from lyrics_transcriber.core.config import OutputConfig
|
|
14
|
+
import uvicorn
|
|
15
|
+
import webbrowser
|
|
16
|
+
from threading import Thread
|
|
17
|
+
from lyrics_transcriber.output.generator import OutputGenerator
|
|
18
|
+
import json
|
|
19
|
+
from lyrics_transcriber.correction.corrector import LyricsCorrector
|
|
20
|
+
from lyrics_transcriber.types import TranscriptionResult, TranscriptionData
|
|
21
|
+
from lyrics_transcriber.lyrics.user_input_provider import UserInputProvider
|
|
22
|
+
from lyrics_transcriber.correction.operations import CorrectionOperations
|
|
23
|
+
import uuid
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
# Optional: used to introspect local models for /api/v1/models
|
|
27
|
+
from lyrics_transcriber.correction.agentic.providers.health import (
|
|
28
|
+
is_ollama_available,
|
|
29
|
+
get_ollama_models,
|
|
30
|
+
)
|
|
31
|
+
except Exception:
|
|
32
|
+
def is_ollama_available() -> bool: # type: ignore
|
|
33
|
+
return False
|
|
34
|
+
|
|
35
|
+
def get_ollama_models(): # type: ignore
|
|
36
|
+
return []
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
from lyrics_transcriber.correction.agentic.observability.metrics import MetricsAggregator
|
|
40
|
+
except Exception:
|
|
41
|
+
MetricsAggregator = None # type: ignore
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
from lyrics_transcriber.correction.agentic.observability.langfuse_integration import (
|
|
45
|
+
setup_langfuse,
|
|
46
|
+
record_metrics as lf_record,
|
|
47
|
+
)
|
|
48
|
+
except Exception:
|
|
49
|
+
setup_langfuse = lambda *args, **kwargs: None # type: ignore
|
|
50
|
+
lf_record = lambda *args, **kwargs: None # type: ignore
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from lyrics_transcriber.correction.agentic.feedback.store import FeedbackStore
|
|
54
|
+
except Exception:
|
|
55
|
+
FeedbackStore = None # type: ignore
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
from lyrics_transcriber.correction.feedback.store import FeedbackStore as NewFeedbackStore
|
|
59
|
+
from lyrics_transcriber.correction.feedback.schemas import CorrectionAnnotation
|
|
60
|
+
except Exception:
|
|
61
|
+
NewFeedbackStore = None # type: ignore
|
|
62
|
+
CorrectionAnnotation = None # type: ignore
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class ReviewServer:
|
|
66
|
+
"""Handles the review process through a web interface."""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
correction_result: CorrectionResult,
|
|
71
|
+
output_config: OutputConfig,
|
|
72
|
+
audio_filepath: str,
|
|
73
|
+
logger: logging.Logger,
|
|
74
|
+
):
|
|
75
|
+
"""Initialize the review server."""
|
|
76
|
+
self.correction_result = correction_result
|
|
77
|
+
self.output_config = output_config
|
|
78
|
+
self.audio_filepath = audio_filepath
|
|
79
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
80
|
+
self.review_completed = False
|
|
81
|
+
|
|
82
|
+
# Create FastAPI instance and configure
|
|
83
|
+
self.app = FastAPI()
|
|
84
|
+
self._configure_cors()
|
|
85
|
+
self._register_routes()
|
|
86
|
+
self._mount_frontend()
|
|
87
|
+
# Initialize optional SQLite store for sessions/feedback (legacy)
|
|
88
|
+
try:
|
|
89
|
+
default_db = os.path.join(self.output_config.cache_dir, "agentic_feedback.sqlite3")
|
|
90
|
+
self._store = FeedbackStore(default_db) if FeedbackStore else None
|
|
91
|
+
except Exception:
|
|
92
|
+
self._store = None
|
|
93
|
+
|
|
94
|
+
# Initialize new annotation store
|
|
95
|
+
try:
|
|
96
|
+
self._annotation_store = NewFeedbackStore(storage_dir=self.output_config.cache_dir) if NewFeedbackStore else None
|
|
97
|
+
except Exception:
|
|
98
|
+
self._annotation_store = None
|
|
99
|
+
# Metrics aggregator
|
|
100
|
+
self._metrics = MetricsAggregator() if MetricsAggregator else None
|
|
101
|
+
# LangFuse (optional)
|
|
102
|
+
try:
|
|
103
|
+
self._langfuse = setup_langfuse("agentic-corrector")
|
|
104
|
+
except Exception:
|
|
105
|
+
self._langfuse = None
|
|
106
|
+
|
|
107
|
+
def _configure_cors(self) -> None:
|
|
108
|
+
"""Configure CORS middleware."""
|
|
109
|
+
# Allow localhost development ports and the hosted review UI
|
|
110
|
+
allowed_origins = (
|
|
111
|
+
[f"http://localhost:{port}" for port in range(3000, 5174)]
|
|
112
|
+
+ [f"http://127.0.0.1:{port}" for port in range(3000, 5174)]
|
|
113
|
+
+ ["https://lyrics.nomadkaraoke.com"]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Also allow custom review UI URL if set
|
|
117
|
+
custom_ui = os.environ.get("LYRICS_REVIEW_UI_URL", "")
|
|
118
|
+
if custom_ui and custom_ui.lower() != "local" and custom_ui not in allowed_origins:
|
|
119
|
+
allowed_origins.append(custom_ui)
|
|
120
|
+
|
|
121
|
+
self.app.add_middleware(
|
|
122
|
+
CORSMiddleware,
|
|
123
|
+
allow_origins=allowed_origins,
|
|
124
|
+
allow_credentials=True,
|
|
125
|
+
allow_methods=["*"],
|
|
126
|
+
allow_headers=["*"],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@self.app.exception_handler(HTTPException)
|
|
130
|
+
async def _http_exception_handler(request, exc: HTTPException):
|
|
131
|
+
return JSONResponse(status_code=exc.status_code, content={"error": "HTTPException", "message": exc.detail, "details": {}})
|
|
132
|
+
|
|
133
|
+
@self.app.exception_handler(Exception)
|
|
134
|
+
async def _unhandled_exception_handler(request, exc: Exception):
|
|
135
|
+
return JSONResponse(status_code=500, content={"error": "InternalServerError", "message": str(exc), "details": {}})
|
|
136
|
+
|
|
137
|
+
def _mount_frontend(self) -> None:
|
|
138
|
+
"""Mount the frontend static files."""
|
|
139
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
140
|
+
from lyrics_transcriber.frontend import get_frontend_assets_dir
|
|
141
|
+
frontend_dir = get_frontend_assets_dir()
|
|
142
|
+
|
|
143
|
+
if not os.path.exists(frontend_dir):
|
|
144
|
+
raise FileNotFoundError(f"Frontend assets not found at {frontend_dir}")
|
|
145
|
+
|
|
146
|
+
self.app.mount("/", StaticFiles(directory=frontend_dir, html=True), name="frontend")
|
|
147
|
+
|
|
148
|
+
def _register_routes(self) -> None:
|
|
149
|
+
"""Register API routes."""
|
|
150
|
+
self.app.add_api_route("/api/correction-data", self.get_correction_data, methods=["GET"])
|
|
151
|
+
self.app.add_api_route("/api/complete", self.complete_review, methods=["POST"])
|
|
152
|
+
self.app.add_api_route("/api/preview-video", self.generate_preview_video, methods=["POST"])
|
|
153
|
+
self.app.add_api_route("/api/preview-video/{preview_hash}", self.get_preview_video, methods=["GET"])
|
|
154
|
+
self.app.add_api_route("/api/audio/{audio_hash}", self.get_audio, methods=["GET"])
|
|
155
|
+
self.app.add_api_route("/api/ping", self.ping, methods=["GET"])
|
|
156
|
+
self.app.add_api_route("/api/handlers", self.update_handlers, methods=["POST"])
|
|
157
|
+
self.app.add_api_route("/api/add-lyrics", self.add_lyrics, methods=["POST"])
|
|
158
|
+
|
|
159
|
+
# Agentic AI v1 endpoints (contract-compliant scaffolds)
|
|
160
|
+
self.app.add_api_route("/api/v1/correction/agentic", self.post_correction_agentic, methods=["POST"])
|
|
161
|
+
self.app.add_api_route("/api/v1/correction/session/{session_id}", self.get_correction_session_v1, methods=["GET"])
|
|
162
|
+
self.app.add_api_route("/api/v1/feedback", self.post_feedback_v1, methods=["POST"])
|
|
163
|
+
self.app.add_api_route("/api/v1/models", self.get_models_v1, methods=["GET"])
|
|
164
|
+
self.app.add_api_route("/api/v1/models", self.put_models_v1, methods=["PUT"])
|
|
165
|
+
self.app.add_api_route("/api/v1/metrics", self.get_metrics_v1, methods=["GET"])
|
|
166
|
+
|
|
167
|
+
# Annotation endpoints
|
|
168
|
+
self.app.add_api_route("/api/v1/annotations", self.post_annotation, methods=["POST"])
|
|
169
|
+
self.app.add_api_route("/api/v1/annotations/{audio_hash}", self.get_annotations_by_song, methods=["GET"])
|
|
170
|
+
self.app.add_api_route("/api/v1/annotations/stats", self.get_annotation_stats, methods=["GET"])
|
|
171
|
+
|
|
172
|
+
async def get_correction_data(self):
|
|
173
|
+
"""Get the correction data."""
|
|
174
|
+
return self.correction_result.to_dict()
|
|
175
|
+
|
|
176
|
+
# ------------------------------
|
|
177
|
+
# API v1: Agentic AI scaffolds
|
|
178
|
+
# ------------------------------
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def _session_store(self) -> Dict[str, Dict[str, Any]]:
|
|
182
|
+
if not hasattr(self, "__session_store"):
|
|
183
|
+
self.__session_store = {}
|
|
184
|
+
return self.__session_store # type: ignore[attr-defined]
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def _feedback_store(self) -> Dict[str, Dict[str, Any]]:
|
|
188
|
+
if not hasattr(self, "__feedback_store"):
|
|
189
|
+
self.__feedback_store = {}
|
|
190
|
+
return self.__feedback_store # type: ignore[attr-defined]
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def _model_registry(self) -> Dict[str, Dict[str, Any]]:
|
|
194
|
+
if not hasattr(self, "__model_registry"):
|
|
195
|
+
# Seed with a few placeholders
|
|
196
|
+
models: Dict[str, Dict[str, Any]] = {}
|
|
197
|
+
# Local models via Ollama
|
|
198
|
+
if is_ollama_available():
|
|
199
|
+
for m in get_ollama_models():
|
|
200
|
+
mid = m.get("model") or m.get("name") or "ollama-unknown"
|
|
201
|
+
models[mid] = {
|
|
202
|
+
"id": mid,
|
|
203
|
+
"name": mid,
|
|
204
|
+
"type": "local",
|
|
205
|
+
"available": True,
|
|
206
|
+
"responseTimeMs": 0,
|
|
207
|
+
"costPerToken": 0.0,
|
|
208
|
+
"accuracy": 0.0,
|
|
209
|
+
}
|
|
210
|
+
# Cloud placeholders
|
|
211
|
+
for mid in ["anthropic/claude-4-sonnet", "gpt-5", "gemini-2.5-pro"]:
|
|
212
|
+
if mid not in models:
|
|
213
|
+
models[mid] = {
|
|
214
|
+
"id": mid,
|
|
215
|
+
"name": mid,
|
|
216
|
+
"type": "cloud",
|
|
217
|
+
"available": False,
|
|
218
|
+
"responseTimeMs": 0,
|
|
219
|
+
"costPerToken": 0.0,
|
|
220
|
+
"accuracy": 0.0,
|
|
221
|
+
}
|
|
222
|
+
self.__model_registry = models
|
|
223
|
+
return self.__model_registry # type: ignore[attr-defined]
|
|
224
|
+
|
|
225
|
+
async def post_correction_agentic(self, request: Dict[str, Any] = Body(...)):
|
|
226
|
+
"""POST /api/v1/correction/agentic
|
|
227
|
+
Minimal scaffold: validates required fields and returns a stub response.
|
|
228
|
+
"""
|
|
229
|
+
start_time = time.time()
|
|
230
|
+
if not isinstance(request, dict):
|
|
231
|
+
raise HTTPException(status_code=400, detail="Invalid request body")
|
|
232
|
+
|
|
233
|
+
if "transcriptionData" not in request or "audioFileHash" not in request:
|
|
234
|
+
raise HTTPException(status_code=400, detail="Missing required fields: transcriptionData, audioFileHash")
|
|
235
|
+
|
|
236
|
+
session_id = str(uuid.uuid4())
|
|
237
|
+
session_record = {
|
|
238
|
+
"id": session_id,
|
|
239
|
+
"audioFileHash": request.get("audioFileHash"),
|
|
240
|
+
"sessionType": "FULL_CORRECTION",
|
|
241
|
+
"aiModelConfig": {"model": (request.get("modelPreferences") or [None])[0]},
|
|
242
|
+
"totalCorrections": 0,
|
|
243
|
+
"acceptedCorrections": 0,
|
|
244
|
+
"humanModifications": 0,
|
|
245
|
+
"sessionDurationMs": 0,
|
|
246
|
+
"accuracyImprovement": 0.0,
|
|
247
|
+
"startedAt": None,
|
|
248
|
+
"completedAt": None,
|
|
249
|
+
"status": "IN_PROGRESS",
|
|
250
|
+
}
|
|
251
|
+
self._session_store[session_id] = session_record
|
|
252
|
+
if self._store:
|
|
253
|
+
try:
|
|
254
|
+
self._store.put_session(session_id, json.dumps(session_record))
|
|
255
|
+
except Exception:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# Simulate provider availability based on model preferences
|
|
259
|
+
preferred = (request.get("modelPreferences") or ["unknown"])[0]
|
|
260
|
+
model_entry = self._model_registry.get(preferred)
|
|
261
|
+
if model_entry and not model_entry.get("available", False):
|
|
262
|
+
# Service unavailable → return 503 with fallback details
|
|
263
|
+
from fastapi.responses import JSONResponse
|
|
264
|
+
if self._metrics:
|
|
265
|
+
self._metrics.record_session(preferred, int((time.time() - start_time) * 1000), fallback_used=True)
|
|
266
|
+
content = {
|
|
267
|
+
"corrections": [],
|
|
268
|
+
"fallbackReason": f"Model {preferred} unavailable",
|
|
269
|
+
"originalSystemUsed": "rule-based",
|
|
270
|
+
"processingTimeMs": int((time.time() - start_time) * 1000),
|
|
271
|
+
}
|
|
272
|
+
lf_record(self._langfuse, "post_correction_agentic_fallback", {"model": preferred, **content})
|
|
273
|
+
return JSONResponse(status_code=503, content=content)
|
|
274
|
+
|
|
275
|
+
response = {
|
|
276
|
+
"sessionId": session_id,
|
|
277
|
+
"corrections": [],
|
|
278
|
+
"processingTimeMs": int((time.time() - start_time) * 1000),
|
|
279
|
+
"modelUsed": preferred,
|
|
280
|
+
"fallbackUsed": False,
|
|
281
|
+
"accuracyEstimate": 0.0,
|
|
282
|
+
}
|
|
283
|
+
if self._metrics:
|
|
284
|
+
self._metrics.record_session(preferred, response["processingTimeMs"], fallback_used=False)
|
|
285
|
+
lf_record(self._langfuse, "post_correction_agentic", {"model": preferred, **response})
|
|
286
|
+
return response
|
|
287
|
+
|
|
288
|
+
async def get_correction_session_v1(self, session_id: str):
|
|
289
|
+
data = self._session_store.get(session_id)
|
|
290
|
+
if not data:
|
|
291
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
|
292
|
+
return data
|
|
293
|
+
|
|
294
|
+
async def post_feedback_v1(self, request: Dict[str, Any] = Body(...)):
|
|
295
|
+
if not isinstance(request, dict):
|
|
296
|
+
raise HTTPException(status_code=400, detail="Invalid request body")
|
|
297
|
+
required = ["aiCorrectionId", "reviewerAction", "reasonCategory"]
|
|
298
|
+
if any(k not in request for k in required):
|
|
299
|
+
raise HTTPException(status_code=400, detail="Missing required feedback fields")
|
|
300
|
+
|
|
301
|
+
feedback_id = str(uuid.uuid4())
|
|
302
|
+
record = {**request, "id": feedback_id}
|
|
303
|
+
self._feedback_store[feedback_id] = record
|
|
304
|
+
if self._store:
|
|
305
|
+
try:
|
|
306
|
+
self._store.put_feedback(feedback_id, request.get("sessionId"), json.dumps(record))
|
|
307
|
+
except Exception:
|
|
308
|
+
pass
|
|
309
|
+
if self._metrics:
|
|
310
|
+
self._metrics.record_feedback()
|
|
311
|
+
return {"feedbackId": feedback_id, "recorded": True, "learningDataUpdated": False}
|
|
312
|
+
|
|
313
|
+
async def get_models_v1(self):
|
|
314
|
+
return {"models": list(self._model_registry.values())}
|
|
315
|
+
|
|
316
|
+
async def put_models_v1(self, config: Dict[str, Any] = Body(...)):
|
|
317
|
+
if not isinstance(config, dict) or "modelId" not in config:
|
|
318
|
+
raise HTTPException(status_code=400, detail="Invalid model configuration")
|
|
319
|
+
mid = config["modelId"]
|
|
320
|
+
entry = self._model_registry.get(mid, {
|
|
321
|
+
"id": mid,
|
|
322
|
+
"name": mid,
|
|
323
|
+
"type": "cloud",
|
|
324
|
+
"available": False,
|
|
325
|
+
"responseTimeMs": 0,
|
|
326
|
+
"costPerToken": 0.0,
|
|
327
|
+
"accuracy": 0.0,
|
|
328
|
+
})
|
|
329
|
+
if "enabled" in config:
|
|
330
|
+
entry["available"] = bool(config["enabled"]) or entry.get("available", False)
|
|
331
|
+
if "priority" in config:
|
|
332
|
+
entry["priority"] = config["priority"]
|
|
333
|
+
if "configuration" in config and isinstance(config["configuration"], dict):
|
|
334
|
+
entry["configuration"] = config["configuration"]
|
|
335
|
+
self._model_registry[mid] = entry
|
|
336
|
+
return {"status": "ok"}
|
|
337
|
+
|
|
338
|
+
async def get_metrics_v1(self, timeRange: str = "day", sessionId: Optional[str] = None):
|
|
339
|
+
if self._metrics:
|
|
340
|
+
return self._metrics.snapshot(time_range=timeRange, session_id=sessionId)
|
|
341
|
+
# Fallback if metrics unavailable
|
|
342
|
+
return {"timeRange": timeRange, "totalSessions": len(self._session_store), "averageAccuracy": 0.0, "errorReduction": 0.0, "averageProcessingTime": 0, "modelPerformance": {}, "costSummary": {}, "userSatisfaction": 0.0}
|
|
343
|
+
|
|
344
|
+
# ------------------------------
|
|
345
|
+
# Annotation endpoints
|
|
346
|
+
# ------------------------------
|
|
347
|
+
|
|
348
|
+
async def post_annotation(self, annotation_data: Dict[str, Any] = Body(...)):
|
|
349
|
+
"""Save a correction annotation."""
|
|
350
|
+
if not self._annotation_store or not CorrectionAnnotation:
|
|
351
|
+
raise HTTPException(status_code=501, detail="Annotation system not available")
|
|
352
|
+
|
|
353
|
+
try:
|
|
354
|
+
# Validate and create annotation
|
|
355
|
+
annotation = CorrectionAnnotation.model_validate(annotation_data)
|
|
356
|
+
|
|
357
|
+
# Save to store
|
|
358
|
+
success = self._annotation_store.save_annotation(annotation)
|
|
359
|
+
|
|
360
|
+
if success:
|
|
361
|
+
return {"status": "success", "annotation_id": annotation.annotation_id}
|
|
362
|
+
else:
|
|
363
|
+
raise HTTPException(status_code=500, detail="Failed to save annotation")
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
self.logger.error(f"Failed to save annotation: {e}")
|
|
367
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
368
|
+
|
|
369
|
+
async def get_annotations_by_song(self, audio_hash: str):
|
|
370
|
+
"""Get all annotations for a specific song."""
|
|
371
|
+
if not self._annotation_store:
|
|
372
|
+
raise HTTPException(status_code=501, detail="Annotation system not available")
|
|
373
|
+
|
|
374
|
+
try:
|
|
375
|
+
annotations = self._annotation_store.get_annotations_by_song(audio_hash)
|
|
376
|
+
return {
|
|
377
|
+
"audio_hash": audio_hash,
|
|
378
|
+
"count": len(annotations),
|
|
379
|
+
"annotations": [a.model_dump() for a in annotations]
|
|
380
|
+
}
|
|
381
|
+
except Exception as e:
|
|
382
|
+
self.logger.error(f"Failed to get annotations: {e}")
|
|
383
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
384
|
+
|
|
385
|
+
async def get_annotation_stats(self):
|
|
386
|
+
"""Get aggregated statistics from all annotations."""
|
|
387
|
+
if not self._annotation_store:
|
|
388
|
+
raise HTTPException(status_code=501, detail="Annotation system not available")
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
stats = self._annotation_store.get_statistics()
|
|
392
|
+
return stats.model_dump()
|
|
393
|
+
except Exception as e:
|
|
394
|
+
self.logger.error(f"Failed to get annotation statistics: {e}")
|
|
395
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
396
|
+
|
|
397
|
+
def _update_correction_result(self, base_result: CorrectionResult, updated_data: Dict[str, Any]) -> CorrectionResult:
|
|
398
|
+
"""Update a CorrectionResult with new correction data."""
|
|
399
|
+
return CorrectionOperations.update_correction_result_with_data(base_result, updated_data)
|
|
400
|
+
|
|
401
|
+
async def complete_review(self, updated_data: Dict[str, Any] = Body(...)):
|
|
402
|
+
"""Complete the review process."""
|
|
403
|
+
try:
|
|
404
|
+
self.correction_result = self._update_correction_result(self.correction_result, updated_data)
|
|
405
|
+
self.review_completed = True
|
|
406
|
+
return {"status": "success"}
|
|
407
|
+
except Exception as e:
|
|
408
|
+
self.logger.error(f"Failed to update correction data: {str(e)}")
|
|
409
|
+
return {"status": "error", "message": str(e)}
|
|
410
|
+
|
|
411
|
+
async def ping(self):
|
|
412
|
+
"""Simple ping endpoint for testing."""
|
|
413
|
+
return {"status": "ok"}
|
|
414
|
+
|
|
415
|
+
async def get_audio(self, audio_hash: str):
|
|
416
|
+
"""Stream the audio file."""
|
|
417
|
+
try:
|
|
418
|
+
if (
|
|
419
|
+
not self.audio_filepath
|
|
420
|
+
or not os.path.exists(self.audio_filepath)
|
|
421
|
+
or not self.correction_result.metadata
|
|
422
|
+
or self.correction_result.metadata.get("audio_hash") != audio_hash
|
|
423
|
+
):
|
|
424
|
+
raise FileNotFoundError("Audio file not found")
|
|
425
|
+
|
|
426
|
+
return FileResponse(self.audio_filepath, media_type="audio/mpeg", filename=os.path.basename(self.audio_filepath))
|
|
427
|
+
except Exception as e:
|
|
428
|
+
raise HTTPException(status_code=404, detail="Audio file not found")
|
|
429
|
+
|
|
430
|
+
async def generate_preview_video(self, updated_data: Dict[str, Any] = Body(...)):
|
|
431
|
+
"""Generate a preview video with the current corrections."""
|
|
432
|
+
try:
|
|
433
|
+
# Use shared operation for preview generation
|
|
434
|
+
result = CorrectionOperations.generate_preview_video(
|
|
435
|
+
correction_result=self.correction_result,
|
|
436
|
+
updated_data=updated_data,
|
|
437
|
+
output_config=self.output_config,
|
|
438
|
+
audio_filepath=self.audio_filepath,
|
|
439
|
+
logger=self.logger
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Store the path for later retrieval
|
|
443
|
+
if not hasattr(self, "preview_videos"):
|
|
444
|
+
self.preview_videos = {}
|
|
445
|
+
self.preview_videos[result["preview_hash"]] = result["video_path"]
|
|
446
|
+
|
|
447
|
+
return {"status": "success", "preview_hash": result["preview_hash"]}
|
|
448
|
+
|
|
449
|
+
except Exception as e:
|
|
450
|
+
self.logger.error(f"Failed to generate preview video: {str(e)}")
|
|
451
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
452
|
+
|
|
453
|
+
async def get_preview_video(self, preview_hash: str):
|
|
454
|
+
"""Stream the preview video."""
|
|
455
|
+
try:
|
|
456
|
+
if not hasattr(self, "preview_videos") or preview_hash not in self.preview_videos:
|
|
457
|
+
raise FileNotFoundError("Preview video not found")
|
|
458
|
+
|
|
459
|
+
video_path = self.preview_videos[preview_hash]
|
|
460
|
+
if not os.path.exists(video_path):
|
|
461
|
+
raise FileNotFoundError("Preview video file not found")
|
|
462
|
+
|
|
463
|
+
return FileResponse(
|
|
464
|
+
video_path,
|
|
465
|
+
media_type="video/mp4",
|
|
466
|
+
filename=os.path.basename(video_path),
|
|
467
|
+
headers={
|
|
468
|
+
"Accept-Ranges": "bytes",
|
|
469
|
+
"Content-Disposition": "inline",
|
|
470
|
+
"Cache-Control": "no-cache",
|
|
471
|
+
"X-Content-Type-Options": "nosniff",
|
|
472
|
+
},
|
|
473
|
+
)
|
|
474
|
+
except Exception as e:
|
|
475
|
+
self.logger.error(f"Failed to stream preview video: {str(e)}")
|
|
476
|
+
raise HTTPException(status_code=404, detail="Preview video not found")
|
|
477
|
+
|
|
478
|
+
async def update_handlers(self, enabled_handlers: List[str] = Body(...)):
|
|
479
|
+
"""Update enabled correction handlers and rerun correction."""
|
|
480
|
+
try:
|
|
481
|
+
# Use shared operation for handler updates
|
|
482
|
+
self.correction_result = CorrectionOperations.update_correction_handlers(
|
|
483
|
+
correction_result=self.correction_result,
|
|
484
|
+
enabled_handlers=enabled_handlers,
|
|
485
|
+
cache_dir=self.output_config.cache_dir,
|
|
486
|
+
logger=self.logger
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return {"status": "success", "data": self.correction_result.to_dict()}
|
|
490
|
+
except Exception as e:
|
|
491
|
+
self.logger.error(f"Failed to update handlers: {str(e)}")
|
|
492
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
493
|
+
|
|
494
|
+
def _create_lyrics_data_from_text(self, text: str, source: str) -> LyricsData:
|
|
495
|
+
"""Create LyricsData object from plain text lyrics."""
|
|
496
|
+
self.logger.info(f"Creating LyricsData for source '{source}'")
|
|
497
|
+
|
|
498
|
+
# Split text into lines and create segments
|
|
499
|
+
lines = [line.strip() for line in text.split("\n") if line.strip()]
|
|
500
|
+
self.logger.info(f"Found {len(lines)} non-empty lines in input text")
|
|
501
|
+
|
|
502
|
+
segments = []
|
|
503
|
+
for i, line in enumerate(lines):
|
|
504
|
+
# Split line into words
|
|
505
|
+
word_texts = line.strip().split()
|
|
506
|
+
words = []
|
|
507
|
+
|
|
508
|
+
for j, word_text in enumerate(word_texts):
|
|
509
|
+
word = Word(
|
|
510
|
+
id=f"manual_{source}_word_{i}_{j}", # Create unique ID for each word
|
|
511
|
+
text=word_text,
|
|
512
|
+
start_time=0.0, # Placeholder timing
|
|
513
|
+
end_time=0.0,
|
|
514
|
+
confidence=1.0, # Reference lyrics are considered ground truth
|
|
515
|
+
created_during_correction=False,
|
|
516
|
+
)
|
|
517
|
+
words.append(word)
|
|
518
|
+
|
|
519
|
+
segments.append(
|
|
520
|
+
LyricsSegment(
|
|
521
|
+
id=f"manual_{source}_{i}",
|
|
522
|
+
text=line,
|
|
523
|
+
words=words, # Now including the word objects
|
|
524
|
+
start_time=0.0, # Placeholder timing
|
|
525
|
+
end_time=0.0,
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Create metadata
|
|
530
|
+
self.logger.info("Creating metadata for LyricsData")
|
|
531
|
+
metadata = LyricsMetadata(
|
|
532
|
+
source=source,
|
|
533
|
+
track_name=self.correction_result.metadata.get("title", "") or "",
|
|
534
|
+
artist_names=self.correction_result.metadata.get("artist", "") or "",
|
|
535
|
+
is_synced=False,
|
|
536
|
+
lyrics_provider="manual",
|
|
537
|
+
lyrics_provider_id="",
|
|
538
|
+
album_name=None,
|
|
539
|
+
duration_ms=None,
|
|
540
|
+
explicit=None,
|
|
541
|
+
language=None,
|
|
542
|
+
provider_metadata={},
|
|
543
|
+
)
|
|
544
|
+
self.logger.info(f"Created metadata: {metadata}")
|
|
545
|
+
|
|
546
|
+
lyrics_data = LyricsData(segments=segments, metadata=metadata, source=source)
|
|
547
|
+
self.logger.info(f"Created LyricsData with {len(segments)} segments and {sum(len(s.words) for s in segments)} total words")
|
|
548
|
+
|
|
549
|
+
return lyrics_data
|
|
550
|
+
|
|
551
|
+
async def add_lyrics(self, data: Dict[str, str] = Body(...)):
|
|
552
|
+
"""Add new lyrics source and rerun correction."""
|
|
553
|
+
try:
|
|
554
|
+
source = data.get("source", "").strip()
|
|
555
|
+
lyrics_text = data.get("lyrics", "").strip()
|
|
556
|
+
|
|
557
|
+
self.logger.info(f"Received request to add lyrics source '{source}' with {len(lyrics_text)} characters")
|
|
558
|
+
|
|
559
|
+
# Use shared operation for adding lyrics source
|
|
560
|
+
self.correction_result = CorrectionOperations.add_lyrics_source(
|
|
561
|
+
correction_result=self.correction_result,
|
|
562
|
+
source=source,
|
|
563
|
+
lyrics_text=lyrics_text,
|
|
564
|
+
cache_dir=self.output_config.cache_dir,
|
|
565
|
+
logger=self.logger
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
return {"status": "success", "data": self.correction_result.to_dict()}
|
|
569
|
+
|
|
570
|
+
except ValueError as e:
|
|
571
|
+
# Convert ValueError to HTTPException for API consistency
|
|
572
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
573
|
+
except Exception as e:
|
|
574
|
+
self.logger.error(f"Failed to add lyrics: {str(e)}", exc_info=True)
|
|
575
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
576
|
+
|
|
577
|
+
def start(self) -> CorrectionResult:
|
|
578
|
+
"""Start the review server and wait for completion."""
|
|
579
|
+
# Generate audio hash if audio file exists
|
|
580
|
+
if self.audio_filepath and os.path.exists(self.audio_filepath):
|
|
581
|
+
with open(self.audio_filepath, "rb") as f:
|
|
582
|
+
audio_hash = hashlib.md5(f.read()).hexdigest()
|
|
583
|
+
if not self.correction_result.metadata:
|
|
584
|
+
self.correction_result.metadata = {}
|
|
585
|
+
self.correction_result.metadata["audio_hash"] = audio_hash
|
|
586
|
+
|
|
587
|
+
server = None
|
|
588
|
+
server_thread = None
|
|
589
|
+
sock = None
|
|
590
|
+
|
|
591
|
+
try:
|
|
592
|
+
# Check port availability
|
|
593
|
+
while True:
|
|
594
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
595
|
+
sock.settimeout(1)
|
|
596
|
+
if sock.connect_ex(("127.0.0.1", 8000)) == 0:
|
|
597
|
+
# Port is in use, get process info
|
|
598
|
+
process_info = ""
|
|
599
|
+
if os.name != "nt": # Unix-like systems
|
|
600
|
+
try:
|
|
601
|
+
process_info = os.popen("lsof -i:8000").read().strip()
|
|
602
|
+
except:
|
|
603
|
+
pass
|
|
604
|
+
|
|
605
|
+
self.logger.warning(
|
|
606
|
+
f"Port 8000 is in use. Waiting for it to become available...\n"
|
|
607
|
+
f"Process using port 8000:\n{process_info}\n"
|
|
608
|
+
f"To manually free the port, you can run: lsof -ti:8000 | xargs kill -9"
|
|
609
|
+
)
|
|
610
|
+
sock.close()
|
|
611
|
+
time.sleep(30)
|
|
612
|
+
else:
|
|
613
|
+
sock.close()
|
|
614
|
+
break
|
|
615
|
+
|
|
616
|
+
# Start server
|
|
617
|
+
config = uvicorn.Config(self.app, host="127.0.0.1", port=8000, log_level="error")
|
|
618
|
+
server = uvicorn.Server(config)
|
|
619
|
+
server_thread = Thread(target=server.run, daemon=True)
|
|
620
|
+
server_thread.start()
|
|
621
|
+
time.sleep(0.5) # Reduced wait time
|
|
622
|
+
|
|
623
|
+
# Open browser and wait for completion
|
|
624
|
+
base_api_url = "http://localhost:8000/api"
|
|
625
|
+
encoded_api_url = urllib.parse.quote(base_api_url, safe="")
|
|
626
|
+
audio_hash_param = (
|
|
627
|
+
f"&audioHash={self.correction_result.metadata.get('audio_hash', '')}"
|
|
628
|
+
if self.correction_result.metadata and "audio_hash" in self.correction_result.metadata
|
|
629
|
+
else ""
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
# Use bundled local frontend by default for local karaoke-gen runs
|
|
633
|
+
# Can override with LYRICS_REVIEW_UI_URL env var (e.g., http://localhost:5173 for Vite dev)
|
|
634
|
+
review_ui_url = os.environ.get("LYRICS_REVIEW_UI_URL", "local")
|
|
635
|
+
if review_ui_url.lower() == "local":
|
|
636
|
+
# Use the bundled local frontend served by this FastAPI server
|
|
637
|
+
browser_url = f"http://localhost:8000?baseApiUrl={encoded_api_url}{audio_hash_param}"
|
|
638
|
+
else:
|
|
639
|
+
# Use an external review UI (dev server or hosted)
|
|
640
|
+
browser_url = f"{review_ui_url}?baseApiUrl={encoded_api_url}{audio_hash_param}"
|
|
641
|
+
|
|
642
|
+
self.logger.info(f"Opening review UI: {browser_url}")
|
|
643
|
+
webbrowser.open(browser_url)
|
|
644
|
+
|
|
645
|
+
while not self.review_completed:
|
|
646
|
+
time.sleep(0.1)
|
|
647
|
+
|
|
648
|
+
return self.correction_result
|
|
649
|
+
|
|
650
|
+
except KeyboardInterrupt:
|
|
651
|
+
self.logger.info("Received interrupt, shutting down server...")
|
|
652
|
+
raise
|
|
653
|
+
except Exception as e:
|
|
654
|
+
self.logger.error(f"Error during review server operation: {e}")
|
|
655
|
+
raise
|
|
656
|
+
finally:
|
|
657
|
+
# Comprehensive cleanup
|
|
658
|
+
if sock:
|
|
659
|
+
try:
|
|
660
|
+
sock.close()
|
|
661
|
+
except:
|
|
662
|
+
pass
|
|
663
|
+
|
|
664
|
+
if server:
|
|
665
|
+
server.should_exit = True
|
|
666
|
+
|
|
667
|
+
if server_thread and server_thread.is_alive():
|
|
668
|
+
server_thread.join(timeout=1)
|
|
669
|
+
|
|
670
|
+
# Force cleanup any remaining server resources
|
|
671
|
+
try:
|
|
672
|
+
import multiprocessing.resource_tracker
|
|
673
|
+
|
|
674
|
+
multiprocessing.resource_tracker._resource_tracker = None
|
|
675
|
+
except:
|
|
676
|
+
pass
|
|
File without changes
|