feat: Implement image data processing utility and enhance post content generation logic
Browse files- backend/api/posts.py +10 -5
- backend/models/post.py +0 -42
- backend/models/social_account.py +0 -48
- backend/models/source.py +0 -36
- backend/scheduler/apscheduler_service.py +48 -7
- backend/utils/image_utils.py +39 -0
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
|
| 128 |
# image_data could be bytes (from base64) or a string (URL)
|
| 129 |
-
if isinstance(generated_result, tuple):
|
| 130 |
-
generated_content
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|