Zelyanoth commited on
Commit
3261235
·
1 Parent(s): 995f94b

feat: Implement image data processing utility and enhance post content generation logic

Browse files
backend/api/posts.py CHANGED
@@ -6,6 +6,7 @@ from flask import Blueprint, request, jsonify, current_app, send_file
6
  from flask_jwt_extended import jwt_required, get_jwt_identity
7
  from backend.services.content_service import ContentService
8
  from backend.services.linkedin_service import LinkedInService
 
9
 
10
  posts_bp = Blueprint('posts', __name__)
11
 
@@ -124,10 +125,14 @@ def _generate_post_task(user_id, job_id, job_store, hugging_key):
124
  content_service = ContentService(hugging_key=hugging_key)
125
  generated_result = content_service.generate_post_content(user_id)
126
 
127
- # Handle the case where generated_result might be a tuple (content, image_data)
128
  # image_data could be bytes (from base64) or a string (URL)
129
- if isinstance(generated_result, tuple):
130
- generated_content, image_data = generated_result
 
 
 
 
131
  else:
132
  generated_content = generated_result
133
  image_data = None
@@ -447,7 +452,7 @@ def publish_post_direct():
447
 
448
  # Add optional fields if provided
449
  if image_data:
450
- post_data['image_content_url'] = image_data
451
 
452
  if 'scheduled_at' in data:
453
  post_data['scheduled_at'] = data['scheduled_at']
@@ -562,7 +567,7 @@ def create_post():
562
 
563
  # Add optional fields if provided
564
  if image_data is not None:
565
- post_data['image_content_url'] = image_data
566
 
567
  if 'scheduled_at' in data:
568
  post_data['scheduled_at'] = data['scheduled_at']
 
6
  from flask_jwt_extended import jwt_required, get_jwt_identity
7
  from backend.services.content_service import ContentService
8
  from backend.services.linkedin_service import LinkedInService
9
+ from backend.utils.image_utils import ensure_bytes_format
10
 
11
  posts_bp = Blueprint('posts', __name__)
12
 
 
125
  content_service = ContentService(hugging_key=hugging_key)
126
  generated_result = content_service.generate_post_content(user_id)
127
 
128
+ # Handle the case where generated_result might be a tuple, list, or string
129
  # image_data could be bytes (from base64) or a string (URL)
130
+ if isinstance(generated_result, (tuple, list)) and len(generated_result) >= 2:
131
+ generated_content = generated_result[0] if generated_result[0] is not None else "Generated content will appear here..."
132
+ image_data = generated_result[1] if generated_result[1] is not None else None
133
+ elif isinstance(generated_result, (tuple, list)) and len(generated_result) == 1:
134
+ generated_content = generated_result[0] if generated_result[0] is not None else "Generated content will appear here..."
135
+ image_data = None
136
  else:
137
  generated_content = generated_result
138
  image_data = None
 
452
 
453
  # Add optional fields if provided
454
  if image_data:
455
+ post_data['image_content_url'] = ensure_bytes_format(image_data)
456
 
457
  if 'scheduled_at' in data:
458
  post_data['scheduled_at'] = data['scheduled_at']
 
567
 
568
  # Add optional fields if provided
569
  if image_data is not None:
570
+ post_data['image_content_url'] = ensure_bytes_format(image_data)
571
 
572
  if 'scheduled_at' in data:
573
  post_data['scheduled_at'] = data['scheduled_at']
backend/models/post.py DELETED
@@ -1,42 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional, Union
3
- from datetime import datetime
4
-
5
- @dataclass
6
- class Post:
7
- """Post model representing a social media post."""
8
- id: str
9
- social_account_id: str
10
- Text_content: str
11
- is_published: bool = True
12
- sched: Optional[str] = None
13
- image_content_url: Optional[Union[str, bytes]] = None # Can be URL string or bytes
14
- created_at: Optional[datetime] = None
15
- scheduled_at: Optional[datetime] = None
16
-
17
- @classmethod
18
- def from_dict(cls, data: dict):
19
- """Create a Post instance from a dictionary."""
20
- return cls(
21
- id=data['id'],
22
- social_account_id=data['social_account_id'],
23
- Text_content=data['Text_content'],
24
- is_published=data.get('is_published', False),
25
- sched=data.get('sched'),
26
- image_content_url=data.get('image_content_url'),
27
- created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None,
28
- scheduled_at=datetime.fromisoformat(data['scheduled_at'].replace('Z', '+00:00')) if data.get('scheduled_at') else None
29
- )
30
-
31
- def to_dict(self):
32
- """Convert Post instance to dictionary."""
33
- return {
34
- 'id': self.id,
35
- 'social_account_id': self.social_account_id,
36
- 'Text_content': self.Text_content,
37
- 'is_published': self.is_published,
38
- 'sched': self.sched,
39
- 'image_content_url': self.image_content_url,
40
- 'created_at': self.created_at.isoformat() if self.created_at else None,
41
- 'scheduled_at': self.scheduled_at.isoformat() if self.scheduled_at else None
42
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/models/social_account.py DELETED
@@ -1,48 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional
3
- from datetime import datetime
4
-
5
- @dataclass
6
- class SocialAccount:
7
- """Social account model representing a social media account."""
8
- id: str
9
- user_id: str
10
- social_network: str
11
- account_name: str
12
- token: Optional[str] = None
13
- sub: Optional[str] = None
14
- given_name: Optional[str] = None
15
- family_name: Optional[str] = None
16
- picture: Optional[str] = None
17
- created_at: Optional[datetime] = None
18
-
19
- @classmethod
20
- def from_dict(cls, data: dict):
21
- """Create a SocialAccount instance from a dictionary."""
22
- return cls(
23
- id=data['id'],
24
- user_id=data['user_id'],
25
- social_network=data['social_network'],
26
- account_name=data['account_name'],
27
- token=data.get('token'),
28
- sub=data.get('sub'),
29
- given_name=data.get('given_name'),
30
- family_name=data.get('family_name'),
31
- picture=data.get('picture'),
32
- created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None
33
- )
34
-
35
- def to_dict(self):
36
- """Convert SocialAccount instance to dictionary."""
37
- return {
38
- 'id': self.id,
39
- 'user_id': self.user_id,
40
- 'social_network': self.social_network,
41
- 'account_name': self.account_name,
42
- 'token': self.token,
43
- 'sub': self.sub,
44
- 'given_name': self.given_name,
45
- 'family_name': self.family_name,
46
- 'picture': self.picture,
47
- 'created_at': self.created_at.isoformat() if self.created_at else None
48
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/models/source.py DELETED
@@ -1,36 +0,0 @@
1
- from dataclasses import dataclass
2
- from typing import Optional
3
- from datetime import datetime
4
-
5
- @dataclass
6
- class Source:
7
- """Source model representing an RSS source."""
8
- id: str
9
- user_id: str
10
- source: str
11
- category: Optional[str] = None
12
- last_update: Optional[datetime] = None
13
- created_at: Optional[datetime] = None
14
-
15
- @classmethod
16
- def from_dict(cls, data: dict):
17
- """Create a Source instance from a dictionary."""
18
- return cls(
19
- id=data['id'],
20
- user_id=data['user_id'],
21
- source=data['source'],
22
- category=data.get('category'),
23
- last_update=datetime.fromisoformat(data['last_update'].replace('Z', '+00:00')) if data.get('last_update') else None,
24
- created_at=datetime.fromisoformat(data['created_at'].replace('Z', '+00:00')) if data.get('created_at') else None
25
- )
26
-
27
- def to_dict(self):
28
- """Convert Source instance to dictionary."""
29
- return {
30
- 'id': self.id,
31
- 'user_id': self.user_id,
32
- 'source': self.source,
33
- 'category': self.category,
34
- 'last_update': self.last_update.isoformat() if self.last_update else None,
35
- 'created_at': self.created_at.isoformat() if self.created_at else None
36
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
backend/scheduler/apscheduler_service.py CHANGED
@@ -9,6 +9,7 @@ from apscheduler.executors.pool import ThreadPoolExecutor
9
  from backend.services.content_service import ContentService
10
  from backend.services.linkedin_service import LinkedInService
11
  from backend.utils.database import init_supabase
 
12
  from backend.config import Config
13
  from backend.utils.timezone_utils import (
14
  parse_timezone_schedule,
@@ -247,7 +248,40 @@ class APSchedulerService:
247
  content_service = ContentService()
248
 
249
  # Generate content using content service
250
- generated_content = content_service.generate_post_content(user_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
  # Store generated content in database
253
  # We need to get the social account ID from the schedule
@@ -264,16 +298,23 @@ class APSchedulerService:
264
 
265
  social_account_id = schedule_response.data[0]['id_social']
266
 
 
 
 
 
 
 
 
 
 
 
 
 
267
  # Store the generated content
268
  response = (
269
  self.supabase_client
270
  .table("Post_content")
271
- .insert({
272
- "id_social": social_account_id,
273
- "Text_content": generated_content,
274
- "is_published": False,
275
- "sched": schedule_id
276
- })
277
  .execute()
278
  )
279
 
 
9
  from backend.services.content_service import ContentService
10
  from backend.services.linkedin_service import LinkedInService
11
  from backend.utils.database import init_supabase
12
+ from backend.utils.image_utils import ensure_bytes_format
13
  from backend.config import Config
14
  from backend.utils.timezone_utils import (
15
  parse_timezone_schedule,
 
248
  content_service = ContentService()
249
 
250
  # Generate content using content service
251
+ generated_result = content_service.generate_post_content(user_id)
252
+
253
+ # Ensure proper extraction of text content and image data from tuple
254
+ # ContentService.generate_post_content() always returns a tuple: (text_content, image_data)
255
+ if isinstance(generated_result, (tuple, list)) and len(generated_result) >= 1:
256
+ # Extract text content (first element) and ensure it's a string
257
+ text_content = generated_result[0] if generated_result[0] is not None else "Generated content will appear here..."
258
+ # Extract image data (second element) if it exists
259
+ image_data = generated_result[1] if len(generated_result) >= 2 and generated_result[1] is not None else None
260
+
261
+ # Additional safeguard: ensure text_content is always a string, never a list/tuple
262
+ if not isinstance(text_content, str):
263
+ text_content = str(text_content) if text_content is not None else "Generated content will appear here..."
264
+ else:
265
+ # Fallback for unexpected return types
266
+ text_content = str(generated_result) if generated_result is not None else "Generated content will appear here..."
267
+ image_data = None
268
+
269
+ # Final validation to ensure text_content is never stored as a list/tuple
270
+ if isinstance(text_content, (list, tuple)):
271
+ # Convert list/tuple to string representation
272
+ text_content = str(text_content)
273
+
274
+ # Process image data for proper storage
275
+ processed_image_data = None
276
+ if image_data is not None:
277
+ try:
278
+ # Use the shared utility function to ensure proper bytes format
279
+ processed_image_data = ensure_bytes_format(image_data)
280
+ logger.info(f"✅ Image data processed for schedule {schedule_id}")
281
+ except Exception as e:
282
+ logger.error(f"❌ Error processing image data for schedule {schedule_id}: {str(e)}")
283
+ # Continue with text content even if image processing fails
284
+ processed_image_data = None
285
 
286
  # Store generated content in database
287
  # We need to get the social account ID from the schedule
 
298
 
299
  social_account_id = schedule_response.data[0]['id_social']
300
 
301
+ # Prepare post data
302
+ post_data = {
303
+ "id_social": social_account_id,
304
+ "Text_content": text_content,
305
+ "is_published": False,
306
+ "sched": schedule_id
307
+ }
308
+
309
+ # Add processed image data if present
310
+ if processed_image_data is not None:
311
+ post_data["image_content_url"] = processed_image_data
312
+
313
  # Store the generated content
314
  response = (
315
  self.supabase_client
316
  .table("Post_content")
317
+ .insert(post_data)
 
 
 
 
 
318
  .execute()
319
  )
320
 
backend/utils/image_utils.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Utility functions for image handling and processing."""
2
+
3
+ import base64
4
+
5
+
6
+ def ensure_bytes_format(image_data):
7
+ """
8
+ Ensure image data is in the proper format for storage.
9
+
10
+ Args:
11
+ image_data: Image data that could be bytes, base64 string, or URL string
12
+
13
+ Returns:
14
+ Properly formatted data for database storage
15
+ """
16
+ if image_data is None:
17
+ return None
18
+
19
+ # If it's already bytes, return as is
20
+ if isinstance(image_data, bytes):
21
+ return image_data
22
+
23
+ # If it's a string, check if it's base64 encoded
24
+ if isinstance(image_data, str):
25
+ # Check if it's a data URL
26
+ if image_data.startswith('data:image/'):
27
+ try:
28
+ # Extract base64 part and decode to bytes
29
+ base64_part = image_data.split(',')[1]
30
+ return base64.b64decode(base64_part)
31
+ except Exception:
32
+ # If decoding fails, store as string (URL)
33
+ return image_data
34
+ else:
35
+ # Assume it's a URL, store as string
36
+ return image_data
37
+
38
+ # For any other type, return as is
39
+ return image_data