Rthur2003 commited on
Commit
d3cb4fb
·
1 Parent(s): 047eaf1

Add Crown Commend API routes for YouTube comment generation and posting

Browse files
Files changed (1) hide show
  1. app/routes/commend/router.py +281 -0
app/routes/commend/router.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Crown Commend API Routes
3
+ AI-powered YouTube comment generation and posting.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import os
9
+ import time
10
+ from typing import Literal, Optional, Dict, Any, List
11
+
12
+ from fastapi import APIRouter, HTTPException, status
13
+ from pydantic import BaseModel, Field, field_validator
14
+
15
+ from .youtube_service import (
16
+ extract_video_id,
17
+ get_video_details,
18
+ get_channel_details,
19
+ get_video_comments,
20
+ get_video_transcript,
21
+ post_youtube_comment
22
+ )
23
+ from .gemini_service import generate_comment, summarize_transcript
24
+ from ...services.logging_config import get_logger
25
+
26
+ logger = get_logger(__name__)
27
+ router = APIRouter(prefix="/api/commend", tags=["commend"])
28
+
29
+
30
+ # --- Pydantic Models ---
31
+
32
+ class GenerateCommentRequest(BaseModel):
33
+ """Request model for comment generation."""
34
+ videoUrl: str = Field(..., min_length=10, description="YouTube video URL")
35
+ language: Literal['Turkish', 'English', 'Russian', 'Chinese', 'Japanese'] = Field(
36
+ default='Turkish',
37
+ description="Target language for the comment"
38
+ )
39
+ commentStyle: str = Field(
40
+ default='supportive',
41
+ description="Comment style (supportive, analytical, humorous, etc.)"
42
+ )
43
+
44
+ @field_validator('videoUrl')
45
+ @classmethod
46
+ def validate_youtube_url(cls, v: str) -> str:
47
+ if not extract_video_id(v):
48
+ raise ValueError('Invalid YouTube URL format')
49
+ return v
50
+
51
+
52
+ class PostCommentRequest(BaseModel):
53
+ """Request model for posting a comment."""
54
+ videoUrl: str = Field(..., min_length=10)
55
+ commentText: str = Field(..., min_length=1, max_length=10000)
56
+
57
+ @field_validator('videoUrl')
58
+ @classmethod
59
+ def validate_youtube_url(cls, v: str) -> str:
60
+ if not extract_video_id(v):
61
+ raise ValueError('Invalid YouTube URL format')
62
+ return v
63
+
64
+
65
+ class VideoDetailsResponse(BaseModel):
66
+ """Response model for video details."""
67
+ videoId: str
68
+ title: str
69
+ channelName: str
70
+ channelId: str
71
+ description: str
72
+ thumbnailUrl: Optional[str]
73
+ viewCount: int
74
+ likeCount: int
75
+ commentCount: int
76
+ duration: int
77
+ publishedAt: Optional[str]
78
+ subscriberCount: Optional[int] = None
79
+
80
+
81
+ class GenerateCommentResponse(BaseModel):
82
+ """Response model for comment generation."""
83
+ status: str
84
+ generatedText: str
85
+ videoDetails: VideoDetailsResponse
86
+ processingTime: float
87
+ hasTranscript: bool
88
+
89
+
90
+ class PostCommentResponse(BaseModel):
91
+ """Response model for posting a comment."""
92
+ status: str
93
+ message: str
94
+ commentId: Optional[str] = None
95
+ postedAt: Optional[str] = None
96
+
97
+
98
+ class CommendHealthResponse(BaseModel):
99
+ """Health check response for Commend service."""
100
+ status: str
101
+ geminiConfigured: bool
102
+ youtubeConfigured: bool
103
+ message: str
104
+
105
+
106
+ # --- API Endpoints ---
107
+
108
+ @router.get("/health", response_model=CommendHealthResponse)
109
+ async def commend_health_check():
110
+ """Check Commend service health and configuration."""
111
+ gemini_key = os.getenv('COMMEND_GEMINI_API_KEY')
112
+ youtube_token = os.getenv('COMMEND_TOKEN_JSON')
113
+
114
+ return CommendHealthResponse(
115
+ status="ok" if gemini_key and youtube_token else "degraded",
116
+ geminiConfigured=bool(gemini_key),
117
+ youtubeConfigured=bool(youtube_token),
118
+ message="Crown Commend service is running"
119
+ )
120
+
121
+
122
+ @router.post("/video-details", response_model=VideoDetailsResponse)
123
+ async def get_video_info(request: GenerateCommentRequest):
124
+ """
125
+ Fetch video details without generating a comment.
126
+ Useful for preview before generation.
127
+ """
128
+ video_id = extract_video_id(request.videoUrl)
129
+
130
+ details, error = get_video_details(request.videoUrl)
131
+ if error:
132
+ raise HTTPException(
133
+ status_code=status.HTTP_400_BAD_REQUEST,
134
+ detail=error
135
+ )
136
+
137
+ # Optionally fetch channel subscriber count
138
+ if details.get('channelId'):
139
+ channel_info, _ = get_channel_details(details['channelId'])
140
+ if channel_info:
141
+ details['subscriberCount'] = channel_info.get('subscriberCount')
142
+
143
+ return VideoDetailsResponse(**details)
144
+
145
+
146
+ @router.post("/generate", response_model=GenerateCommentResponse)
147
+ async def generate_youtube_comment(request: GenerateCommentRequest):
148
+ """
149
+ Generate an AI-powered comment for a YouTube video.
150
+
151
+ The comment is generated based on:
152
+ - Video title, description, and metadata
153
+ - Existing comments on the video
154
+ - Video transcript (if available)
155
+ - Selected language and style
156
+ """
157
+ start_time = time.time()
158
+ video_id = extract_video_id(request.videoUrl)
159
+
160
+ # 1. Fetch video details
161
+ details, error = get_video_details(request.videoUrl)
162
+ if error:
163
+ logger.warning(f"Video details error: {error}")
164
+ raise HTTPException(
165
+ status_code=status.HTTP_400_BAD_REQUEST,
166
+ detail=f"Could not fetch video details: {error}"
167
+ )
168
+
169
+ # 2. Fetch channel stats
170
+ if details.get('channelId'):
171
+ channel_info, _ = get_channel_details(details['channelId'])
172
+ if channel_info:
173
+ details['subscriberCount'] = channel_info.get('subscriberCount')
174
+
175
+ # 3. Fetch existing comments
176
+ existing_comments, _ = get_video_comments(video_id, max_results=10)
177
+
178
+ # 4. Fetch and summarize transcript
179
+ transcript_summary = None
180
+ has_transcript = False
181
+ transcript_text, _ = get_video_transcript(video_id)
182
+ if transcript_text:
183
+ has_transcript = True
184
+ summary, _ = summarize_transcript(transcript_text, request.language)
185
+ if summary:
186
+ transcript_summary = summary
187
+
188
+ # 5. Generate comment
189
+ generated_text, gen_error = generate_comment(
190
+ video_details=details,
191
+ comment_style=request.commentStyle,
192
+ language=request.language,
193
+ existing_comments=existing_comments,
194
+ transcript_summary=transcript_summary
195
+ )
196
+
197
+ if gen_error:
198
+ logger.error(f"Comment generation failed: {gen_error}")
199
+ raise HTTPException(
200
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
201
+ detail=f"Comment generation failed: {gen_error}"
202
+ )
203
+
204
+ processing_time = time.time() - start_time
205
+
206
+ return GenerateCommentResponse(
207
+ status="success",
208
+ generatedText=generated_text,
209
+ videoDetails=VideoDetailsResponse(**details),
210
+ processingTime=round(processing_time, 2),
211
+ hasTranscript=has_transcript
212
+ )
213
+
214
+
215
+ @router.post("/post", response_model=PostCommentResponse)
216
+ async def post_comment_to_youtube(request: PostCommentRequest):
217
+ """
218
+ Post a comment to YouTube.
219
+
220
+ Note: This requires valid OAuth credentials with YouTube posting permissions.
221
+ Comments include an AI disclaimer as required by platform policies.
222
+ """
223
+ video_id = extract_video_id(request.videoUrl)
224
+
225
+ # Post to YouTube
226
+ result, error = post_youtube_comment(video_id, request.commentText)
227
+
228
+ if error:
229
+ logger.error(f"Comment posting failed: {error}")
230
+ raise HTTPException(
231
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
232
+ detail=error
233
+ )
234
+
235
+ return PostCommentResponse(
236
+ status="success",
237
+ message="Comment posted successfully",
238
+ commentId=result.get('commentId'),
239
+ postedAt=result.get('postedAt')
240
+ )
241
+
242
+
243
+ @router.get("/styles")
244
+ async def get_comment_styles():
245
+ """Get available comment styles with descriptions."""
246
+ return {
247
+ "styles": [
248
+ {
249
+ "id": "supportive",
250
+ "name": {"tr": "Destekleyici", "en": "Supportive"},
251
+ "description": {"tr": "Pozitif ve teşvik edici", "en": "Positive and encouraging"}
252
+ },
253
+ {
254
+ "id": "analytical",
255
+ "name": {"tr": "Analitik", "en": "Analytical"},
256
+ "description": {"tr": "Detaylı ve düşündürücü", "en": "Detailed and thought-provoking"}
257
+ },
258
+ {
259
+ "id": "humorous",
260
+ "name": {"tr": "Esprili", "en": "Humorous"},
261
+ "description": {"tr": "Eğlenceli ve samimi", "en": "Fun and friendly"}
262
+ },
263
+ {
264
+ "id": "curious",
265
+ "name": {"tr": "Meraklı", "en": "Curious"},
266
+ "description": {"tr": "Soru soran ve ilgili", "en": "Questioning and engaged"}
267
+ },
268
+ {
269
+ "id": "professional",
270
+ "name": {"tr": "Profesyonel", "en": "Professional"},
271
+ "description": {"tr": "Resmi ve saygılı", "en": "Formal and respectful"}
272
+ }
273
+ ],
274
+ "languages": [
275
+ {"code": "Turkish", "name": "Türkçe", "flag": "🇹🇷"},
276
+ {"code": "English", "name": "English", "flag": "🇬🇧"},
277
+ {"code": "Russian", "name": "Русский", "flag": "🇷🇺"},
278
+ {"code": "Chinese", "name": "中文", "flag": "🇨🇳"},
279
+ {"code": "Japanese", "name": "日本語", "flag": "🇯🇵"}
280
+ ]
281
+ }