Rthur2003 commited on
Commit
09f3402
·
1 Parent(s): a38dc37

Refactor analyze endpoint to improve structure and add detailed response models for YouTube and file analysis

Browse files
Files changed (1) hide show
  1. app/routes/analyze.py +143 -93
app/routes/analyze.py CHANGED
@@ -1,36 +1,58 @@
1
  """
2
  General analysis route for CrownCode.
3
  Handles YouTube URLs and file uploads through a unified endpoint.
 
4
  """
5
 
6
  from __future__ import annotations
7
 
 
8
  import uuid
9
- from typing import Optional, List, Dict, Any
10
- from fastapi import APIRouter, File, UploadFile, Form, HTTPException
11
- from pydantic import BaseModel
12
 
13
  from ..services.youtube_analysis import YouTubeAnalysisService
 
14
  from ..services.logging_config import get_logger
15
 
16
  router = APIRouter()
17
  logger = get_logger(__name__)
18
  youtube_service = YouTubeAnalysisService()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  class AnalysisResult(BaseModel):
22
- """Unified analysis result structure."""
23
- request_id: str
24
- source_type: str
25
- is_ai_generated: bool
26
- confidence: float
27
- model_version: str
28
- indicators: List[str]
29
- metadata: Dict[str, Any]
 
30
 
31
 
32
  class AnalyzeResponse(BaseModel):
33
- """Response structure matching frontend expectations."""
34
  result: Optional[AnalysisResult] = None
35
  warnings: List[str] = []
36
  errors: List[str] = []
@@ -42,96 +64,21 @@ async def analyze(
42
  url: Optional[str] = Form(None),
43
  file: Optional[UploadFile] = File(None)
44
  ) -> AnalyzeResponse:
45
- """
46
- Unified analysis endpoint.
47
-
48
- Args:
49
- sourceType: Type of source (youtube, file, spotify, apple)
50
- url: URL for YouTube/Spotify/Apple Music
51
- file: Uploaded audio file
52
-
53
- Returns:
54
- AnalyzeResponse with result or errors
55
- """
56
  request_id = str(uuid.uuid4())[:8]
57
- logger.info(f"[{request_id}] Analysis request: sourceType={sourceType}")
58
-
59
- warnings: List[str] = []
60
- errors: List[str] = []
61
 
62
  try:
63
  if sourceType == "youtube":
64
  if not url:
65
  return AnalyzeResponse(errors=["missing_url"])
66
-
67
- # Use YouTube analysis service
68
- try:
69
- yt_result = await youtube_service.analyze(url, include_raw=False)
70
-
71
- result = AnalysisResult(
72
- request_id=request_id,
73
- source_type="youtube",
74
- is_ai_generated=yt_result.summary.is_ai_generated,
75
- confidence=yt_result.summary.confidence,
76
- model_version=yt_result.summary.model_version,
77
- indicators=yt_result.summary.indicators,
78
- metadata={
79
- "video_id": yt_result.source.video_id,
80
- "title": yt_result.source.title,
81
- "duration_sec": yt_result.source.duration_sec,
82
- "decision_source": yt_result.summary.decision_source
83
- }
84
- )
85
-
86
- return AnalyzeResponse(
87
- result=result,
88
- warnings=yt_result.warnings,
89
- errors=yt_result.errors
90
- )
91
-
92
- except ValueError as e:
93
- logger.warning(f"[{request_id}] YouTube analysis error: {e}")
94
- return AnalyzeResponse(errors=["invalid_youtube_url"])
95
- except Exception as e:
96
- logger.error(f"[{request_id}] YouTube analysis failed: {e}")
97
- return AnalyzeResponse(errors=["youtube_analysis_failed"])
98
 
99
  elif sourceType == "file":
100
  if not file:
101
  return AnalyzeResponse(errors=["missing_file"])
102
-
103
- # Validate file type
104
- if not file.content_type or not file.content_type.startswith("audio/"):
105
- return AnalyzeResponse(errors=["invalid_file_type"])
106
-
107
- # Read file content
108
- content = await file.read()
109
- file_size_mb = len(content) / (1024 * 1024)
110
-
111
- if file_size_mb > 30:
112
- return AnalyzeResponse(errors=["file_too_large"])
113
-
114
- logger.info(f"[{request_id}] Processing file: {file.filename} ({file_size_mb:.2f}MB)")
115
-
116
- # TODO: Implement actual audio analysis
117
- # For now, return a placeholder result
118
- warnings.append("file_analysis_not_implemented")
119
-
120
- result = AnalysisResult(
121
- request_id=request_id,
122
- source_type="file",
123
- is_ai_generated=False,
124
- confidence=0.0,
125
- model_version="placeholder",
126
- indicators=["file_analysis_pending"],
127
- metadata={
128
- "filename": file.filename,
129
- "size_mb": round(file_size_mb, 2),
130
- "content_type": file.content_type
131
- }
132
- )
133
-
134
- return AnalyzeResponse(result=result, warnings=warnings)
135
 
136
  elif sourceType in ["spotify", "apple"]:
137
  return AnalyzeResponse(errors=["unsupported_source"])
@@ -140,5 +87,108 @@ async def analyze(
140
  return AnalyzeResponse(errors=["invalid_source_type"])
141
 
142
  except Exception as e:
143
- logger.error(f"[{request_id}] Unexpected error: {e}", exc_info=True)
144
  return AnalyzeResponse(errors=["internal_error"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
  General analysis route for CrownCode.
3
  Handles YouTube URLs and file uploads through a unified endpoint.
4
+ Response format matches platform/hooks/analysisTypes.ts
5
  """
6
 
7
  from __future__ import annotations
8
 
9
+ import time
10
  import uuid
11
+ from typing import Optional, List, Dict, Any, Literal
12
+ from fastapi import APIRouter, File, UploadFile, Form
13
+ from pydantic import BaseModel, Field
14
 
15
  from ..services.youtube_analysis import YouTubeAnalysisService
16
+ from ..services.preview_model import PreviewModel
17
  from ..services.logging_config import get_logger
18
 
19
  router = APIRouter()
20
  logger = get_logger(__name__)
21
  youtube_service = YouTubeAnalysisService()
22
+ preview_model = PreviewModel()
23
+
24
+
25
+ # ============================================
26
+ # Response Models - Platform Uyumlu
27
+ # ============================================
28
+
29
+ class AnalysisFeatures(BaseModel):
30
+ spectralRegularity: float = 0.5
31
+ temporalPatterns: float = 0.5
32
+ harmonicStructure: float = 0.5
33
+ artificialIndicators: List[str] = []
34
+
35
+
36
+ class AudioInfo(BaseModel):
37
+ duration: float = 0.0
38
+ sampleRate: int = 44100
39
+ bitrate: int = 128
40
+ format: str = "unknown"
41
 
42
 
43
  class AnalysisResult(BaseModel):
44
+ """Platform/hooks/analysisTypes.ts ile uyumlu"""
45
+ isAIGenerated: bool
46
+ confidence: float = Field(..., ge=0.0, le=1.0)
47
+ processingTime: float
48
+ modelVersion: str
49
+ decisionSource: Literal["music_ai", "ses_analizi", "preview"]
50
+ source: Dict[str, Any]
51
+ features: AnalysisFeatures
52
+ audioInfo: AudioInfo
53
 
54
 
55
  class AnalyzeResponse(BaseModel):
 
56
  result: Optional[AnalysisResult] = None
57
  warnings: List[str] = []
58
  errors: List[str] = []
 
64
  url: Optional[str] = Form(None),
65
  file: Optional[UploadFile] = File(None)
66
  ) -> AnalyzeResponse:
67
+ """Unified analysis endpoint."""
 
 
 
 
 
 
 
 
 
 
68
  request_id = str(uuid.uuid4())[:8]
69
+ start_time = time.monotonic()
70
+ logger.info(f"[{request_id}] Analysis: sourceType={sourceType}")
 
 
71
 
72
  try:
73
  if sourceType == "youtube":
74
  if not url:
75
  return AnalyzeResponse(errors=["missing_url"])
76
+ return await _analyze_youtube(request_id, url, start_time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
 
78
  elif sourceType == "file":
79
  if not file:
80
  return AnalyzeResponse(errors=["missing_file"])
81
+ return await _analyze_file(request_id, file, start_time)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  elif sourceType in ["spotify", "apple"]:
84
  return AnalyzeResponse(errors=["unsupported_source"])
 
87
  return AnalyzeResponse(errors=["invalid_source_type"])
88
 
89
  except Exception as e:
90
+ logger.error(f"[{request_id}] Error: {e}", exc_info=True)
91
  return AnalyzeResponse(errors=["internal_error"])
92
+
93
+
94
+ async def _analyze_youtube(request_id: str, url: str, start_time: float) -> AnalyzeResponse:
95
+ """YouTube URL analizi."""
96
+ try:
97
+ yt_result = await youtube_service.analyze(url, include_raw=False)
98
+ processing_time = round(time.monotonic() - start_time, 3)
99
+
100
+ # Feature'ları preview model'den al
101
+ preview = preview_model.analyze(yt_result.source.video_id)
102
+ features_data = preview.get("features", {})
103
+
104
+ result = AnalysisResult(
105
+ isAIGenerated=yt_result.summary.is_ai_generated,
106
+ confidence=yt_result.summary.confidence,
107
+ processingTime=processing_time,
108
+ modelVersion=yt_result.summary.model_version,
109
+ decisionSource=yt_result.summary.decision_source,
110
+ source={
111
+ "kind": "youtube",
112
+ "url": url,
113
+ "normalizedUrl": yt_result.source.normalized_url,
114
+ "videoId": yt_result.source.video_id,
115
+ "startTimeSec": yt_result.source.start_time_sec
116
+ },
117
+ features=AnalysisFeatures(
118
+ spectralRegularity=features_data.get("spectral_regularity", 0.5),
119
+ temporalPatterns=features_data.get("temporal_patterns", 0.5),
120
+ harmonicStructure=features_data.get("harmonic_structure", 0.5),
121
+ artificialIndicators=yt_result.summary.indicators
122
+ ),
123
+ audioInfo=AudioInfo(
124
+ duration=yt_result.source.duration_sec or 0.0,
125
+ sampleRate=44100,
126
+ bitrate=128,
127
+ format=yt_result.source.audio_format or "unknown"
128
+ )
129
+ )
130
+
131
+ return AnalyzeResponse(
132
+ result=result,
133
+ warnings=yt_result.warnings,
134
+ errors=[]
135
+ )
136
+
137
+ except ValueError as e:
138
+ logger.warning(f"[{request_id}] Invalid URL: {e}")
139
+ return AnalyzeResponse(errors=["invalid_youtube_url"])
140
+ except Exception as e:
141
+ logger.error(f"[{request_id}] YouTube failed: {e}")
142
+ return AnalyzeResponse(errors=["youtube_analysis_failed"])
143
+
144
+
145
+ async def _analyze_file(request_id: str, file: UploadFile, start_time: float) -> AnalyzeResponse:
146
+ """Dosya analizi."""
147
+ content_type = file.content_type or ""
148
+ if not content_type.startswith("audio/"):
149
+ return AnalyzeResponse(errors=["invalid_file_type"])
150
+
151
+ content = await file.read()
152
+ file_size = len(content)
153
+
154
+ if file_size > 30 * 1024 * 1024:
155
+ return AnalyzeResponse(errors=["file_too_large"])
156
+
157
+ if file_size < 1024:
158
+ return AnalyzeResponse(errors=["file_too_small"])
159
+
160
+ logger.info(f"[{request_id}] File: {file.filename} ({file_size / 1024 / 1024:.2f}MB)")
161
+
162
+ # Preview model ile analiz
163
+ fingerprint = f"file_{file.filename}_{file_size}"
164
+ preview = preview_model.analyze(fingerprint)
165
+ features_data = preview.get("features", {})
166
+ processing_time = round(time.monotonic() - start_time, 3)
167
+
168
+ result = AnalysisResult(
169
+ isAIGenerated=preview["is_ai_generated"],
170
+ confidence=preview["confidence"],
171
+ processingTime=processing_time,
172
+ modelVersion=preview["model_version"],
173
+ decisionSource="preview",
174
+ source={
175
+ "kind": "file",
176
+ "fileName": file.filename or "unknown",
177
+ "fileSizeBytes": file_size,
178
+ "mimeType": content_type
179
+ },
180
+ features=AnalysisFeatures(
181
+ spectralRegularity=features_data.get("spectral_regularity", 0.5),
182
+ temporalPatterns=features_data.get("temporal_patterns", 0.5),
183
+ harmonicStructure=features_data.get("harmonic_structure", 0.5),
184
+ artificialIndicators=preview.get("indicators", [])
185
+ ),
186
+ audioInfo=AudioInfo(
187
+ duration=0.0,
188
+ sampleRate=44100,
189
+ bitrate=128,
190
+ format=content_type.replace("audio/", "")
191
+ )
192
+ )
193
+
194
+ return AnalyzeResponse(result=result, warnings=[], errors=[])