add user gen image
parent
7fc884acdc
commit
2c7c76aec3
@ -0,0 +1,283 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from controllers.service_api_with_auth import api
|
||||
from controllers.service_api_with_auth.app.error import NotChatAppError
|
||||
from controllers.service_api_with_auth.wraps import validate_user_token_and_extract_info
|
||||
from core.app.entities.app_invoke_entities import InvokeFrom
|
||||
from core.file import FileTransferMethod, FileType
|
||||
from extensions.ext_database import db
|
||||
from fields.end_user_fields import image_fields, image_list_fields
|
||||
from flask_restful import Resource, fields, marshal_with, reqparse # type: ignore
|
||||
from libs.helper import TimestampField, uuid_value
|
||||
from models.enums import CreatedByRole
|
||||
from models.model import App, AppMode, Conversation, EndUser, Message, UserGeneratedImage
|
||||
from models.types import StringUUID
|
||||
from services.image_generation_service import ImageGenerationService
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import BadRequest, InternalServerError, NotFound
|
||||
|
||||
# Constants
|
||||
DEFAULT_DAILY_LIMIT = 5
|
||||
MIN_CONVERSATION_ROUNDS = 10
|
||||
|
||||
|
||||
class ImageGenerateApi(Resource):
|
||||
@validate_user_token_and_extract_info
|
||||
def post(self, app_model: App, end_user: EndUser):
|
||||
"""Generate a personalized image based on conversation content.
|
||||
---
|
||||
tags:
|
||||
- service/image
|
||||
summary: Generate a personalized image
|
||||
description: Generate an image with encouraging text based on conversation
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- name: body
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- conversation_id
|
||||
- content_type
|
||||
properties:
|
||||
conversation_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the conversation to use for image generation
|
||||
content_type:
|
||||
type: string
|
||||
enum: [self_message, summary_advice]
|
||||
description: Type of text content to generate
|
||||
responses:
|
||||
200:
|
||||
description: Image generation started
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
example: success
|
||||
message:
|
||||
type: string
|
||||
example: Image generation started
|
||||
400:
|
||||
description: Invalid request or conversation not suitable
|
||||
401:
|
||||
description: Invalid or missing token
|
||||
403:
|
||||
description: Daily limit reached
|
||||
404:
|
||||
description: Conversation not found or not a chat app
|
||||
"""
|
||||
app_mode = AppMode.value_of(app_model.mode)
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("conversation_id", required=True, type=uuid_value, location="json")
|
||||
parser.add_argument(
|
||||
"content_type", required=True, type=str, choices=["self_message", "summary_advice"], location="json"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
conversation_id = str(args["conversation_id"])
|
||||
content_type = args["content_type"]
|
||||
|
||||
# Check if conversation exists
|
||||
conversation = (
|
||||
db.session.query(Conversation)
|
||||
.filter(
|
||||
Conversation.id == conversation_id,
|
||||
Conversation.app_id == app_model.id,
|
||||
Conversation.from_end_user_id == end_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not conversation:
|
||||
raise NotFound("Conversation not found")
|
||||
|
||||
# Check if conversation has enough rounds
|
||||
messages_count = db.session.query(Message).filter(Message.conversation_id == conversation_id).count()
|
||||
|
||||
if messages_count < MIN_CONVERSATION_ROUNDS:
|
||||
return {
|
||||
"result": "error",
|
||||
"message": "I need to know more about you before creating an image. Please continue our conversation.",
|
||||
}, 400
|
||||
|
||||
# Check if user has reached daily limit
|
||||
today = datetime.utcnow().date()
|
||||
tomorrow = today + timedelta(days=1)
|
||||
today_start = datetime.combine(today, datetime.min.time())
|
||||
today_end = datetime.combine(tomorrow, datetime.min.time())
|
||||
|
||||
daily_count = (
|
||||
db.session.query(UserGeneratedImage)
|
||||
.filter(
|
||||
UserGeneratedImage.end_user_id == end_user.id,
|
||||
UserGeneratedImage.created_at >= today_start,
|
||||
UserGeneratedImage.created_at < today_end,
|
||||
)
|
||||
.count()
|
||||
)
|
||||
|
||||
if daily_count >= DEFAULT_DAILY_LIMIT:
|
||||
return {
|
||||
"result": "error",
|
||||
"message": (
|
||||
f"You've reached your daily limit of {DEFAULT_DAILY_LIMIT} generated images. Please try again tomorrow."
|
||||
),
|
||||
}, 403
|
||||
|
||||
try:
|
||||
# Use the service to generate the image
|
||||
# This would typically be done asynchronously in a background task
|
||||
# For simplicity, we're doing it synchronously here
|
||||
image_id = ImageGenerationService.process_image_generation_request(
|
||||
app_id=str(app_model.id),
|
||||
conversation_id=conversation_id,
|
||||
end_user_id=str(end_user.id),
|
||||
content_type=content_type,
|
||||
)
|
||||
|
||||
if image_id:
|
||||
return {"result": "success", "message": "Image generated successfully.", "image_id": image_id}
|
||||
else:
|
||||
return {"result": "error", "message": "Failed to generate image. Please try again later."}, 500
|
||||
|
||||
except Exception as e:
|
||||
raise InternalServerError("Failed to generate image")
|
||||
|
||||
|
||||
class ImageListApi(Resource):
|
||||
@validate_user_token_and_extract_info
|
||||
@marshal_with(image_list_fields)
|
||||
def get(self, app_model: App, end_user: EndUser):
|
||||
"""Get user-generated images list.
|
||||
---
|
||||
tags:
|
||||
- service/image
|
||||
summary: List user generated images
|
||||
description: Get a list of images generated for the current user
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
description: Number of images to return
|
||||
- name: offset
|
||||
in: query
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
description: Offset for pagination
|
||||
responses:
|
||||
200:
|
||||
description: Images retrieved successfully
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
data:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
has_more:
|
||||
type: boolean
|
||||
401:
|
||||
description: Invalid or missing token
|
||||
404:
|
||||
description: Not a chat app
|
||||
"""
|
||||
app_mode = AppMode.value_of(app_model.mode)
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
parser = reqparse.RequestParser()
|
||||
parser.add_argument("limit", type=int, required=False, default=20, location="args")
|
||||
parser.add_argument("offset", type=int, required=False, default=0, location="args")
|
||||
args = parser.parse_args()
|
||||
|
||||
limit = min(args["limit"], 100)
|
||||
offset = max(args["offset"], 0)
|
||||
|
||||
# Get images for the user
|
||||
query = (
|
||||
db.session.query(UserGeneratedImage)
|
||||
.filter(UserGeneratedImage.app_id == app_model.id, UserGeneratedImage.end_user_id == end_user.id)
|
||||
.order_by(UserGeneratedImage.created_at.desc())
|
||||
)
|
||||
|
||||
total_count = query.count()
|
||||
images = query.limit(limit).offset(offset).all()
|
||||
|
||||
return {"data": images, "has_more": (offset + limit) < total_count}
|
||||
|
||||
|
||||
class ImageDetailApi(Resource):
|
||||
@validate_user_token_and_extract_info
|
||||
@marshal_with(image_fields)
|
||||
def get(self, app_model: App, end_user: EndUser, image_id):
|
||||
"""Get a specific generated image.
|
||||
---
|
||||
tags:
|
||||
- service/image
|
||||
summary: Get image details
|
||||
description: Get details of a specific generated image
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
parameters:
|
||||
- name: image_id
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
format: uuid
|
||||
description: ID of the image to retrieve
|
||||
responses:
|
||||
200:
|
||||
description: Image retrieved successfully
|
||||
schema:
|
||||
type: object
|
||||
401:
|
||||
description: Invalid or missing token
|
||||
404:
|
||||
description: Image not found or not a chat app
|
||||
"""
|
||||
app_mode = AppMode.value_of(app_model.mode)
|
||||
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT}:
|
||||
raise NotChatAppError()
|
||||
|
||||
image_id = str(image_id)
|
||||
|
||||
# Get the image
|
||||
image = (
|
||||
db.session.query(UserGeneratedImage)
|
||||
.filter(
|
||||
UserGeneratedImage.id == image_id,
|
||||
UserGeneratedImage.app_id == app_model.id,
|
||||
UserGeneratedImage.end_user_id == end_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not image:
|
||||
raise NotFound("Image not found")
|
||||
|
||||
return image
|
||||
|
||||
|
||||
# Register API resources
|
||||
api.add_resource(ImageGenerateApi, "/images/generate")
|
||||
api.add_resource(ImageListApi, "/images")
|
||||
api.add_resource(ImageDetailApi, "/images/<uuid:image_id>", endpoint="image_detail")
|
||||
@ -0,0 +1,47 @@
|
||||
"""add user gen image
|
||||
|
||||
Revision ID: e4e52e0dfb56
|
||||
Revises: 4b37d4034604
|
||||
Create Date: 2025-03-29 21:40:54.197571
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import models as models
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'e4e52e0dfb56'
|
||||
down_revision = '4b37d4034604'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('user_generated_images',
|
||||
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
|
||||
sa.Column('app_id', models.types.StringUUID(), nullable=False),
|
||||
sa.Column('end_user_id', models.types.StringUUID(), nullable=False),
|
||||
sa.Column('conversation_id', models.types.StringUUID(), nullable=False),
|
||||
sa.Column('image_url', sa.Text(), nullable=False),
|
||||
sa.Column('content_type', sa.String(length=255), nullable=False),
|
||||
sa.Column('text_content', sa.Text(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name='user_generated_image_pkey')
|
||||
)
|
||||
with op.batch_alter_table('user_generated_images', schema=None) as batch_op:
|
||||
batch_op.create_index('user_generated_image_app_idx', ['app_id'], unique=False)
|
||||
batch_op.create_index('user_generated_image_user_idx', ['end_user_id'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('user_generated_images', schema=None) as batch_op:
|
||||
batch_op.drop_index('user_generated_image_user_idx')
|
||||
batch_op.drop_index('user_generated_image_app_idx')
|
||||
|
||||
op.drop_table('user_generated_images')
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,169 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
# Import the UserGeneratedImage model from our controller module
|
||||
# This is a bit of a circular import, but it's the simplest solution for now
|
||||
from controllers.service_api_with_auth.app.image_generate import UserGeneratedImage
|
||||
from extensions.ext_database import db
|
||||
from models.enums import CreatedByRole
|
||||
from models.model import App, Conversation, EndUser, Message, UploadFile
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
# Configure logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageGenerationService:
|
||||
@staticmethod
|
||||
def generate_motivational_text(conversation_id: str, content_type: str) -> str:
|
||||
"""
|
||||
Generate motivational text based on conversation history.
|
||||
|
||||
Args:
|
||||
conversation_id: The ID of the conversation
|
||||
content_type: Type of content to generate ('self_message' or 'summary_advice')
|
||||
|
||||
Returns:
|
||||
str: Generated text content
|
||||
"""
|
||||
# In a real implementation, this would call a large language model
|
||||
# Here we'll just use placeholders based on the content type
|
||||
|
||||
with Session(db.engine) as session:
|
||||
# Get the last few messages from the conversation to understand context
|
||||
messages = (
|
||||
session.query(Message)
|
||||
.filter(Message.conversation_id == conversation_id)
|
||||
.order_by(Message.created_at.desc())
|
||||
.limit(20)
|
||||
.all()
|
||||
)
|
||||
|
||||
# Reverse to get chronological order
|
||||
messages.reverse()
|
||||
|
||||
# Extract conversation context
|
||||
context = "\n".join([f"User: {msg.query}\nAI: {msg.answer}" for msg in messages])
|
||||
|
||||
# In production, you would pass this context to a language model
|
||||
# For demonstration, we'll return placeholder text
|
||||
|
||||
if content_type == "self_message":
|
||||
sample_messages = [
|
||||
"You've got this! Take one small step today.",
|
||||
"Remember your strength - you've overcome challenges before.",
|
||||
"Be kind to yourself today, you deserve it.",
|
||||
"Your feelings are valid, and you have the power to work through them.",
|
||||
"Small progress is still progress. Celebrate your wins today.",
|
||||
]
|
||||
return random.choice(sample_messages)
|
||||
else: # summary_advice
|
||||
sample_advice = [
|
||||
"Based on our conversation, I notice you tend to be hard on yourself. Try practicing self-compassion by speaking to yourself as you would to a friend.",
|
||||
"I've observed that you often describe feeling overwhelmed. Breaking tasks into smaller steps might help manage these feelings better.",
|
||||
"In our discussions, I noticed patterns of negative self-talk. Consider challenging these thoughts by asking 'Is this really true?' when they arise.",
|
||||
"From our conversations, it seems you might benefit from more self-care routines. Even 5 minutes of mindfulness daily could make a difference.",
|
||||
"You've mentioned feeling anxious in social situations. Progressive exposure to small social interactions might help build confidence over time.",
|
||||
]
|
||||
return random.choice(sample_advice)
|
||||
|
||||
@staticmethod
|
||||
def generate_background_image() -> str:
|
||||
"""
|
||||
Generate or select a background image.
|
||||
|
||||
In a real implementation, this might call an image generation API
|
||||
or select from pre-generated images.
|
||||
|
||||
Returns:
|
||||
str: URL of the generated/selected image
|
||||
"""
|
||||
# In a real implementation, this would integrate with an image generation API
|
||||
# or select from a pool of pre-generated images
|
||||
|
||||
# For this example, we'll return a placeholder
|
||||
placeholder_images = [
|
||||
"https://example.com/background1.jpg",
|
||||
"https://example.com/background2.jpg",
|
||||
"https://example.com/background3.jpg",
|
||||
"https://example.com/background4.jpg",
|
||||
"https://example.com/background5.jpg",
|
||||
]
|
||||
|
||||
return random.choice(placeholder_images)
|
||||
|
||||
@staticmethod
|
||||
def overlay_text_on_image(image_url: str, text: str) -> str:
|
||||
"""
|
||||
Overlay text on the image.
|
||||
|
||||
In a real implementation, this would use image processing libraries.
|
||||
|
||||
Args:
|
||||
image_url: URL of the background image
|
||||
text: Text to overlay on the image
|
||||
|
||||
Returns:
|
||||
str: URL of the final image with text
|
||||
"""
|
||||
# In a real implementation, this would use image processing libraries
|
||||
# like Pillow to overlay text on the image
|
||||
|
||||
# For this example, we'll just return the same URL
|
||||
# In production, you would process the image and save it to storage
|
||||
return image_url
|
||||
|
||||
@staticmethod
|
||||
def process_image_generation_request(
|
||||
app_id: str, conversation_id: str, end_user_id: str, content_type: str
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Process an image generation request.
|
||||
|
||||
Args:
|
||||
app_id: The ID of the app
|
||||
conversation_id: The ID of the conversation
|
||||
end_user_id: The ID of the end user
|
||||
content_type: Type of content to generate ('self_message' or 'summary_advice')
|
||||
|
||||
Returns:
|
||||
Optional[str]: ID of the generated image if successful, None otherwise
|
||||
"""
|
||||
try:
|
||||
# 1. Generate motivational text based on conversation history
|
||||
text_content = ImageGenerationService.generate_motivational_text(
|
||||
conversation_id=conversation_id, content_type=content_type
|
||||
)
|
||||
|
||||
# 2. Generate or select a background image
|
||||
image_url = ImageGenerationService.generate_background_image()
|
||||
|
||||
# 3. Overlay text on the image
|
||||
final_image_url = ImageGenerationService.overlay_text_on_image(image_url=image_url, text=text_content)
|
||||
|
||||
# 4. Create and save the user generated image record
|
||||
with Session(db.engine) as session:
|
||||
new_image = UserGeneratedImage(
|
||||
app_id=app_id,
|
||||
end_user_id=end_user_id,
|
||||
conversation_id=conversation_id,
|
||||
image_url=final_image_url,
|
||||
content_type=content_type,
|
||||
text_content=text_content,
|
||||
)
|
||||
|
||||
session.add(new_image)
|
||||
session.commit()
|
||||
|
||||
image_id = str(new_image.id)
|
||||
logger.info(f"Generated image {image_id} for user {end_user_id}")
|
||||
|
||||
return image_id
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating image: {str(e)}")
|
||||
return None
|
||||
Loading…
Reference in New Issue