Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- src/helpers.py +8 -0
- src/meta_data.py +165 -0
- src/mongo_logger.py +36 -0
- src/prompt_service.py +55 -0
- src/r2_uploader.py +13 -0
src/helpers.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64, os
|
| 2 |
+
|
| 3 |
+
def encode_image_to_base64(image_path: str) -> str:
|
| 4 |
+
with open(image_path, "rb") as f:
|
| 5 |
+
return base64.b64encode(f.read()).decode("utf-8")
|
| 6 |
+
|
| 7 |
+
def is_valid_image(file_name: str) -> bool:
|
| 8 |
+
return file_name.lower().endswith((".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"))
|
src/meta_data.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
import piexif
|
| 3 |
+
from PIL import Image
|
| 4 |
+
from io import BytesIO
|
| 5 |
+
|
| 6 |
+
def generate_metadata(model):
|
| 7 |
+
# Define different values based on iPhone model
|
| 8 |
+
exposure_times = {
|
| 9 |
+
"iPhone 11": "1/60",
|
| 10 |
+
"iPhone 11 Pro": "1/70",
|
| 11 |
+
"iPhone 12": "1/100",
|
| 12 |
+
"iPhone 12 Pro": "1/110",
|
| 13 |
+
"iPhone 13": "1/120",
|
| 14 |
+
"iPhone 13 Pro": "1/130",
|
| 15 |
+
"iPhone 13 Pro Max": "1/140",
|
| 16 |
+
"iPhone 14": "1/200",
|
| 17 |
+
"iPhone 14 Pro": "1/220",
|
| 18 |
+
"iPhone 14 Pro Max": "1/240",
|
| 19 |
+
"iPhone 15": "1/300",
|
| 20 |
+
"iPhone 15 Plus": "1/320",
|
| 21 |
+
"iPhone 15 Pro": "1/400",
|
| 22 |
+
"iPhone 15 Pro Max": "1/500",
|
| 23 |
+
"iPhone 16": "1/600",
|
| 24 |
+
"iPhone 16 Pro": "1/700",
|
| 25 |
+
"iPhone 16 Pro Max": "1/1000"
|
| 26 |
+
}
|
| 27 |
+
f_numbers = {
|
| 28 |
+
"iPhone 11": "f/1.8",
|
| 29 |
+
"iPhone 11 Pro": "f/1.8",
|
| 30 |
+
"iPhone 12": "f/1.6",
|
| 31 |
+
"iPhone 12 Pro": "f/1.6",
|
| 32 |
+
"iPhone 13": "f/1.5",
|
| 33 |
+
"iPhone 13 Pro": "f/1.5",
|
| 34 |
+
"iPhone 13 Pro Max": "f/1.5",
|
| 35 |
+
"iPhone 14": "f/1.4",
|
| 36 |
+
"iPhone 14 Pro": "f/1.4",
|
| 37 |
+
"iPhone 14 Pro Max": "f/1.4",
|
| 38 |
+
"iPhone 15": "f/1.4",
|
| 39 |
+
"iPhone 15 Plus": "f/1.4",
|
| 40 |
+
"iPhone 15 Pro": "f/1.4",
|
| 41 |
+
"iPhone 15 Pro Max": "f/1.3",
|
| 42 |
+
"iPhone 16": "f/1.3",
|
| 43 |
+
"iPhone 16 Pro": "f/1.3",
|
| 44 |
+
"iPhone 16 Pro Max": "f/1.3"
|
| 45 |
+
}
|
| 46 |
+
focal_lengths = {
|
| 47 |
+
"iPhone 11": "3.99 mm",
|
| 48 |
+
"iPhone 11 Pro": "4.0 mm",
|
| 49 |
+
"iPhone 12": "4.2 mm",
|
| 50 |
+
"iPhone 12 Pro": "4.3 mm",
|
| 51 |
+
"iPhone 13": "5.0 mm",
|
| 52 |
+
"iPhone 13 Pro": "5.1 mm",
|
| 53 |
+
"iPhone 13 Pro Max": "5.2 mm",
|
| 54 |
+
"iPhone 14": "6.0 mm",
|
| 55 |
+
"iPhone 14 Pro": "6.1 mm",
|
| 56 |
+
"iPhone 14 Pro Max": "6.2 mm",
|
| 57 |
+
"iPhone 15": "6.0 mm",
|
| 58 |
+
"iPhone 15 Plus": "6.1 mm",
|
| 59 |
+
"iPhone 15 Pro": "6.1 mm",
|
| 60 |
+
"iPhone 15 Pro Max": "6.2 mm",
|
| 61 |
+
"iPhone 16": "6.5 mm",
|
| 62 |
+
"iPhone 16 Pro": "6.6 mm",
|
| 63 |
+
"iPhone 16 Pro Max": "6.7 mm"
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
metadata = {
|
| 67 |
+
"Make": "Apple",
|
| 68 |
+
"Model": model,
|
| 69 |
+
"Software": "iOS {}".format(random.choice([14, 15, 16, 17, 18, 19])),
|
| 70 |
+
"Orientation": random.choice(["Horizontal (normal)", "Rotate 90 CW", "Rotate 180", "Rotate 90 CCW"]),
|
| 71 |
+
"DateTime": "{}:{}:{} {:02}:{:02}:{:02}".format(random.randint(2022, 2024), random.randint(1, 12), random.randint(1, 28), random.randint(0, 23), random.randint(0, 59), random.randint(0, 59)),
|
| 72 |
+
"ExposureTime": exposure_times[model],
|
| 73 |
+
"FNumber": f_numbers[model],
|
| 74 |
+
"ISOSpeedRatings": random.choice([100, 200, 400, 800, 1600]),
|
| 75 |
+
"FocalLength": focal_lengths[model],
|
| 76 |
+
"Flash": random.choice(["Flash fired", "Flash did not fire, compulsory mode", "Flash fired, auto mode"]),
|
| 77 |
+
"WhiteBalance": random.choice(["Auto", "Manual"]),
|
| 78 |
+
"MeteringMode": random.choice(["Pattern", "CenterWeightedAverage", "Spot"]),
|
| 79 |
+
"SceneCaptureType": random.choice(["Standard", "Landscape", "Portrait", "NightScene"]),
|
| 80 |
+
"GPSLatitude": "{:.4f} N".format(random.uniform(0.0, 90.0)),
|
| 81 |
+
"GPSLongitude": "{:.4f} W".format(random.uniform(0.0, 180.0)),
|
| 82 |
+
"Altitude": "{:.1f} m".format(random.uniform(0, 100)),
|
| 83 |
+
"LensMake": "Apple",
|
| 84 |
+
"LensModel": "{} back triple camera {} f/1.5".format(model, focal_lengths[model]),
|
| 85 |
+
"ColorSpace": random.choice(["sRGB", "Adobe RGB"]),
|
| 86 |
+
"PixelXDimension": random.choice([3024, 4032, 2160]),
|
| 87 |
+
"PixelYDimension": random.choice([3024, 4032, 2160]),
|
| 88 |
+
"ExposureBiasValue": random.choice(["0 EV", "+1 EV", "-1 EV"]),
|
| 89 |
+
"BrightnessValue": "{:.1f}".format(random.uniform(-5, 10)),
|
| 90 |
+
"ExposureMode": random.choice(["Auto Exposure", "Manual Exposure"])
|
| 91 |
+
}
|
| 92 |
+
return metadata
|
| 93 |
+
|
| 94 |
+
def dms_coordinates(value):
|
| 95 |
+
"""Convert decimal degrees to EXIF GPS (DMS format)."""
|
| 96 |
+
degrees = int(value)
|
| 97 |
+
minutes_float = (value - degrees) * 60
|
| 98 |
+
minutes = int(minutes_float)
|
| 99 |
+
seconds = round((minutes_float - minutes) * 60 * 10000)
|
| 100 |
+
return [(degrees, 1), (minutes, 1), (seconds, 10000)]
|
| 101 |
+
|
| 102 |
+
def meta_data_helper_function(image_bytes,model=None):
|
| 103 |
+
"""
|
| 104 |
+
Takes raw image bytes, adds realistic EXIF metadata, and returns new bytes.
|
| 105 |
+
"""
|
| 106 |
+
if model is None:
|
| 107 |
+
model = random.choice([
|
| 108 |
+
"iPhone 11", "iPhone 11 Pro", "iPhone 12", "iPhone 12 Pro",
|
| 109 |
+
"iPhone 13", "iPhone 13 Pro", "iPhone 13 Pro Max", "iPhone 14",
|
| 110 |
+
"iPhone 14 Pro", "iPhone 14 Pro Max", "iPhone 15", "iPhone 15 Plus",
|
| 111 |
+
"iPhone 15 Pro", "iPhone 15 Pro Max", "iPhone 16", "iPhone 16 Pro",
|
| 112 |
+
"iPhone 16 Pro Max"
|
| 113 |
+
])
|
| 114 |
+
|
| 115 |
+
# Load image from bytes
|
| 116 |
+
img = Image.open(BytesIO(image_bytes))
|
| 117 |
+
img = img.convert("RGB") # Ensure compatibility
|
| 118 |
+
|
| 119 |
+
# Generate metadata
|
| 120 |
+
metadata = generate_metadata(model)
|
| 121 |
+
|
| 122 |
+
# Build EXIF data
|
| 123 |
+
exif_dict = {"0th": {}, "Exif": {}, "GPS": {}}
|
| 124 |
+
|
| 125 |
+
exif_dict["0th"][piexif.ImageIFD.Make] = metadata["Make"]
|
| 126 |
+
exif_dict["0th"][piexif.ImageIFD.Model] = metadata["Model"]
|
| 127 |
+
exif_dict["0th"][piexif.ImageIFD.Software] = metadata["Software"]
|
| 128 |
+
exif_dict["0th"][piexif.ImageIFD.DateTime] = metadata["DateTime"]
|
| 129 |
+
|
| 130 |
+
# Exposure time (rational)
|
| 131 |
+
num, den = map(int, metadata["ExposureTime"].split("/"))
|
| 132 |
+
exif_dict["Exif"][piexif.ExifIFD.ExposureTime] = (num, den)
|
| 133 |
+
|
| 134 |
+
# FNumber
|
| 135 |
+
f_number = float(metadata["FNumber"].replace("f/", ""))
|
| 136 |
+
exif_dict["Exif"][piexif.ExifIFD.FNumber] = (int(f_number * 10), 10)
|
| 137 |
+
|
| 138 |
+
# ISO
|
| 139 |
+
exif_dict["Exif"][piexif.ExifIFD.ISOSpeedRatings] = metadata["ISOSpeedRatings"]
|
| 140 |
+
|
| 141 |
+
# Focal Length
|
| 142 |
+
focal_length = float(metadata["FocalLength"].split()[0])
|
| 143 |
+
exif_dict["Exif"][piexif.ExifIFD.FocalLength] = (int(focal_length * 10), 10)
|
| 144 |
+
|
| 145 |
+
# GPS
|
| 146 |
+
lat = float(metadata["GPSLatitude"].split()[0])
|
| 147 |
+
lon = float(metadata["GPSLongitude"].split()[0])
|
| 148 |
+
|
| 149 |
+
exif_dict["GPS"][piexif.GPSIFD.GPSLatitudeRef] = b'N' if lat >= 0 else b'S'
|
| 150 |
+
exif_dict["GPS"][piexif.GPSIFD.GPSLatitude] = dms_coordinates(abs(lat))
|
| 151 |
+
|
| 152 |
+
exif_dict["GPS"][piexif.GPSIFD.GPSLongitudeRef] = b'E' if lon >= 0 else b'W'
|
| 153 |
+
exif_dict["GPS"][piexif.GPSIFD.GPSLongitude] = dms_coordinates(abs(lon))
|
| 154 |
+
|
| 155 |
+
altitude_value = float(metadata["Altitude"].replace(" m", ""))
|
| 156 |
+
exif_dict["GPS"][piexif.GPSIFD.GPSAltitude] = (int(altitude_value), 1)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
# Dump to bytes
|
| 160 |
+
exif_bytes = piexif.dump(exif_dict)
|
| 161 |
+
|
| 162 |
+
# Save to in-memory buffer
|
| 163 |
+
output_io = BytesIO()
|
| 164 |
+
img.save(output_io, format="JPEG", exif=exif_bytes)
|
| 165 |
+
return output_io.getvalue()
|
src/mongo_logger.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, uuid
|
| 2 |
+
from pymongo import MongoClient
|
| 3 |
+
from datetime import datetime, timezone
|
| 4 |
+
|
| 5 |
+
client = MongoClient(os.getenv("MONGO_URI"))
|
| 6 |
+
collection = client.adgenesis.records
|
| 7 |
+
|
| 8 |
+
def create_log(category, file_name, prompt):
|
| 9 |
+
log_id = str(uuid.uuid4())
|
| 10 |
+
collection.insert_one({
|
| 11 |
+
"_id": log_id,
|
| 12 |
+
"category": category,
|
| 13 |
+
"prompt": prompt,
|
| 14 |
+
"file_name": file_name,
|
| 15 |
+
"status": "in_progress",
|
| 16 |
+
"urls": [],
|
| 17 |
+
"message": "Processing started",
|
| 18 |
+
"created_at": datetime.now(timezone.utc),
|
| 19 |
+
"updated_at": datetime.now(timezone.utc),
|
| 20 |
+
"lob": "test"
|
| 21 |
+
})
|
| 22 |
+
return log_id
|
| 23 |
+
|
| 24 |
+
def update_log_status(log_id, status, urls=None, message=None):
|
| 25 |
+
update = {
|
| 26 |
+
"$set": {
|
| 27 |
+
"status": status,
|
| 28 |
+
"updated_at": datetime.now(timezone.utc)
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
if urls:
|
| 32 |
+
update["$set"]["urls"] = urls
|
| 33 |
+
if message:
|
| 34 |
+
update["$set"]["message"] = message
|
| 35 |
+
|
| 36 |
+
collection.update_one({"_id": log_id}, update)
|
src/prompt_service.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import openai, os, json, re
|
| 2 |
+
|
| 3 |
+
openai.api_key = os.getenv("OPENAI_API_KEY")
|
| 4 |
+
sys_prompt = os.getenv("SYS_PROMPT")
|
| 5 |
+
|
| 6 |
+
def get_prompts(image_base64, category, user_prompt, sentiment, negative_prompt):
|
| 7 |
+
try:
|
| 8 |
+
if negative_prompt:
|
| 9 |
+
message = [
|
| 10 |
+
{
|
| 11 |
+
"role": "system",
|
| 12 |
+
"content": f"""{sys_prompt}
|
| 13 |
+
Return only a JSON with 2 variations of ad prompts for image generation, based on the input image.
|
| 14 |
+
Respond in this JSON format:\n{{\"variations\": [\"Prompt 1\", \"Prompt 2\", ..., \"Prompt 10\"]}}"""
|
| 15 |
+
},
|
| 16 |
+
{
|
| 17 |
+
"role": "user",
|
| 18 |
+
"content": [
|
| 19 |
+
{"type": "text", "text": f"""Generate prompt variations for this ad image for {category} category having {sentiment} sentiment in JSON format only based on the following instruction:
|
| 20 |
+
{user_prompt}
|
| 21 |
+
Don't consider following things and treat them as negative prompt:
|
| 22 |
+
{negative_prompt}"""},
|
| 23 |
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
|
| 24 |
+
]
|
| 25 |
+
}
|
| 26 |
+
]
|
| 27 |
+
else:
|
| 28 |
+
message = [
|
| 29 |
+
{
|
| 30 |
+
"role": "system",
|
| 31 |
+
"content": f"""{sys_prompt}
|
| 32 |
+
Return only a JSON with 2 variations of ad prompts for image generation, based on the input image.
|
| 33 |
+
Respond in this JSON format:\n{{\"variations\": [\"Prompt 1\", \"Prompt 2\", ..., \"Prompt 10\"]}}"""
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"role": "user",
|
| 37 |
+
"content": [
|
| 38 |
+
{"type": "text",
|
| 39 |
+
"text": f"""Generate prompt variations for this ad image for {category} category having {sentiment} sentiment in JSON format only based on the following instruction:
|
| 40 |
+
{user_prompt}"""},
|
| 41 |
+
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}}
|
| 42 |
+
]
|
| 43 |
+
}
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
response = openai.chat.completions.create(
|
| 47 |
+
model="gpt-4o",
|
| 48 |
+
messages=message,
|
| 49 |
+
temperature=0.7
|
| 50 |
+
)
|
| 51 |
+
content = response.choices[0].message.content.strip()
|
| 52 |
+
content = re.sub(r"^```json\s*|\s*```$", "", content)
|
| 53 |
+
return json.loads(content)["variations"]
|
| 54 |
+
except Exception:
|
| 55 |
+
return []
|
src/r2_uploader.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import boto3, os
|
| 2 |
+
from uuid import uuid4
|
| 3 |
+
|
| 4 |
+
def upload_image_to_r2(image_bytes):
|
| 5 |
+
s3 = boto3.client(
|
| 6 |
+
"s3",
|
| 7 |
+
endpoint_url=os.getenv("R2_ENDPOINT"),
|
| 8 |
+
aws_access_key_id=os.getenv("R2_ACCESS_KEY"),
|
| 9 |
+
aws_secret_access_key=os.getenv("R2_SECRET_KEY")
|
| 10 |
+
)
|
| 11 |
+
file_key = f"{uuid4().hex}.png"
|
| 12 |
+
s3.put_object(Bucket=os.getenv("R2_BUCKET_NAME"), Key=file_key, Body=image_bytes, ContentType="image/png")
|
| 13 |
+
return f"{os.getenv('R2_ENDPOINT')}/{os.getenv('R2_BUCKET_NAME')}/{file_key}"
|