ibombonato commited on
Commit
02d4d4d
·
verified ·
1 Parent(s): 9362af6

feat: support many template in image generation (#13)

Browse files

- feat: support many template in image generation (547fbf9dcedd839f06720760776af450e5633062)

.gitattributes CHANGED
@@ -38,3 +38,5 @@ assets/Montserrat-Regular.ttf filter=lfs diff=lfs merge=lfs -text
38
  assets/template_natura_empty.jpg filter=lfs diff=lfs merge=lfs -text
39
  assets/template_1.png filter=lfs diff=lfs merge=lfs -text
40
  assets/template_2.png filter=lfs diff=lfs merge=lfs -text
 
 
 
38
  assets/template_natura_empty.jpg filter=lfs diff=lfs merge=lfs -text
39
  assets/template_1.png filter=lfs diff=lfs merge=lfs -text
40
  assets/template_2.png filter=lfs diff=lfs merge=lfs -text
41
+ assets/template_b_avon.png filter=lfs diff=lfs merge=lfs -text
42
+ assets/template_b_natura.png filter=lfs diff=lfs merge=lfs -text
app.py CHANGED
@@ -132,6 +132,11 @@ with gr.Blocks() as demo:
132
  image_original_price_input = gr.Number(label="Original Price", placeholder="Enter original price...")
133
  image_final_price_input = gr.Number(label="Final Price", placeholder="Enter final price...")
134
  image_coupon_code_input = gr.Textbox(label="Coupon Code", placeholder="Enter coupon code...")
 
 
 
 
 
135
  gen_image_btn = gr.Button("Generate Image")
136
  with gr.Column():
137
  image_output = gr.Image(label="Generated Image", height=500, type="filepath", interactive=False, show_share_button=True)
@@ -168,7 +173,7 @@ with gr.Blocks() as demo:
168
 
169
 
170
 
171
- def generate_image(product_image_url, product_name, original_price, final_price, coupon_code):
172
  tool = GenerateImageTool()
173
  original_price_str = f"{original_price:.2f}".replace('.', ',')
174
  final_price_str = f"{final_price:.2f}".replace('.', ',')
@@ -180,7 +185,8 @@ with gr.Blocks() as demo:
180
  product_name=product_name,
181
  original_price=original_price_str,
182
  final_price=final_price_str,
183
- coupon_code=coupon_code
 
184
  )
185
 
186
  yield gr.update(interactive=True, value="Generate Image"), image_path, gr.update(interactive=True)
@@ -207,7 +213,7 @@ with gr.Blocks() as demo:
207
  inputs=[fragrantica_url_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input],
208
  outputs=fragrantica_output)
209
  gen_image_btn.click(generate_image,
210
- inputs=[image_product_url_input, image_product_name_input, image_original_price_input, image_final_price_input, image_coupon_code_input],
211
  outputs=[gen_image_btn, image_output, share_button])
212
  share_button.click(fn=process_image_for_sharing,
213
  inputs=[image_output],
 
132
  image_original_price_input = gr.Number(label="Original Price", placeholder="Enter original price...")
133
  image_final_price_input = gr.Number(label="Final Price", placeholder="Enter final price...")
134
  image_coupon_code_input = gr.Textbox(label="Coupon Code", placeholder="Enter coupon code...")
135
+ template_selection = gr.Radio(
136
+ choices=["lidi_promo", "natura", "avon"],
137
+ value="lidi_promo",
138
+ label="Template Selection"
139
+ )
140
  gen_image_btn = gr.Button("Generate Image")
141
  with gr.Column():
142
  image_output = gr.Image(label="Generated Image", height=500, type="filepath", interactive=False, show_share_button=True)
 
173
 
174
 
175
 
176
+ def generate_image(product_image_url, product_name, original_price, final_price, coupon_code, template_name):
177
  tool = GenerateImageTool()
178
  original_price_str = f"{original_price:.2f}".replace('.', ',')
179
  final_price_str = f"{final_price:.2f}".replace('.', ',')
 
185
  product_name=product_name,
186
  original_price=original_price_str,
187
  final_price=final_price_str,
188
+ coupon_code=coupon_code,
189
+ template_name=template_name
190
  )
191
 
192
  yield gr.update(interactive=True, value="Generate Image"), image_path, gr.update(interactive=True)
 
213
  inputs=[fragrantica_url_input, openai_key_input, natura_token_input, openai_base_url_input, openai_model_name_input],
214
  outputs=fragrantica_output)
215
  gen_image_btn.click(generate_image,
216
+ inputs=[image_product_url_input, image_product_name_input, image_original_price_input, image_final_price_input, image_coupon_code_input, template_selection],
217
  outputs=[gen_image_btn, image_output, share_button])
218
  share_button.click(fn=process_image_for_sharing,
219
  inputs=[image_output],
assets/template_b_avon.png ADDED

Git LFS Details

  • SHA256: 150f991692dde2638e6213a46bcdbd1df3b09d9d92cece43f2582cadbcbbf3d0
  • Pointer size: 131 Bytes
  • Size of remote file: 458 kB
assets/template_b_natura.png ADDED

Git LFS Details

  • SHA256: 49d7b6152f917edc5b0a2ed964c1c10173c64895ee6f5b1d8c02008d01a981b0
  • Pointer size: 131 Bytes
  • Size of remote file: 176 kB
image_generator_tool.py CHANGED
@@ -1,9 +1,6 @@
1
  from crewai.tools import BaseTool
2
  from pydantic import BaseModel, Field
3
- from PIL import Image, ImageDraw, ImageFont
4
- import requests
5
- from io import BytesIO
6
- import base64
7
 
8
  class GenerateImageToolInput(BaseModel):
9
  """Input for the Generate Image Tool."""
@@ -12,63 +9,28 @@ class GenerateImageToolInput(BaseModel):
12
  original_price: str = Field(..., description="Original price of the product.")
13
  final_price: str = Field(..., description="Final price of the product.")
14
  coupon_code: str = Field(..., description="Coupon code to be displayed on the image.")
15
-
16
- import tempfile
17
 
18
  class GenerateImageTool(BaseTool):
19
  name: str = "Generate Image Tool"
20
  description: str = "Generates a promotional image for a product using a template and returns the file path."
21
  args_schema = GenerateImageToolInput
22
 
23
- def _run(self, product_image_url: str, product_name: str, original_price: str, final_price: str, coupon_code: str) -> str:
24
- template_path = 'assets/template_1.png'
25
-
26
  try:
27
- template_image = Image.open(template_path).convert("RGBA")
28
- response = requests.get(product_image_url)
29
- product_image_data = BytesIO(response.content)
30
- product_image = Image.open(product_image_data).convert("RGBA")
31
-
32
- box_size = (442, 353)
33
- box_position = (140, 280)
34
-
35
- product_image_resized = product_image.copy()
36
- product_image_resized.thumbnail(box_size)
37
-
38
- paste_x = box_position[0] + (box_size[0] - product_image_resized.width) // 2
39
- paste_y = box_position[1] + (box_size[1] - product_image_resized.height) // 2
40
- paste_position = (paste_x, paste_y)
41
-
42
- template_image.paste(product_image_resized, paste_position, product_image_resized)
43
-
44
- draw = ImageDraw.Draw(template_image)
45
-
46
- try:
47
- font_name = ImageFont.truetype("assets/Montserrat-Bold.ttf", 47)
48
- font_price_from = ImageFont.truetype("assets/Montserrat-Regular.ttf", 28)
49
- font_price = ImageFont.truetype("assets/Montserrat-Bold.ttf", 47)
50
- font_cupom = ImageFont.truetype("assets/Montserrat-Bold.ttf", 33)
51
- except IOError:
52
- print("Arial font not found. Using default font.")
53
- font_name = ImageFont.load_default()
54
- font_price_from = ImageFont.load_default()
55
- font_price = ImageFont.load_default()
56
- font_cupom = ImageFont.load_default()
57
-
58
- white_color = "#FFFFFF"
59
- yellow_color = "#FEE161"
60
- black_color = "#000000"
61
-
62
- draw.text((360, 710), product_name, font=font_name, fill=white_color, anchor="ms")
63
- draw.text((360, 800), f"De: R$ {original_price}", font=font_price_from, fill=white_color, anchor="ms")
64
- draw.text((360, 860), f"Por: R$ {final_price}", font=font_price, fill=yellow_color, anchor="ms")
65
- draw.text((360, 993), coupon_code, font=font_cupom, fill=black_color, anchor="ms")
66
-
67
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
68
- template_image.save(temp_file.name)
69
- return temp_file.name
70
-
71
- except FileNotFoundError:
72
- return f"Error: The template file '{template_path}' was not found."
73
  except Exception as e:
74
  return f"An error occurred: {e}"
 
1
  from crewai.tools import BaseTool
2
  from pydantic import BaseModel, Field
3
+ from template_system import TemplateRegistry
 
 
 
4
 
5
  class GenerateImageToolInput(BaseModel):
6
  """Input for the Generate Image Tool."""
 
9
  original_price: str = Field(..., description="Original price of the product.")
10
  final_price: str = Field(..., description="Final price of the product.")
11
  coupon_code: str = Field(..., description="Coupon code to be displayed on the image.")
12
+ template_name: str = Field(default="lidi_promo", description="Name of the template to use for generating the image.")
 
13
 
14
  class GenerateImageTool(BaseTool):
15
  name: str = "Generate Image Tool"
16
  description: str = "Generates a promotional image for a product using a template and returns the file path."
17
  args_schema = GenerateImageToolInput
18
 
19
+ def _run(self, product_image_url: str, product_name: str, original_price: str, final_price: str, coupon_code: str, template_name: str = "lidi_promo") -> str:
 
 
20
  try:
21
+ # Get the template instance
22
+ template = TemplateRegistry.get_template(template_name)
23
+
24
+ # Generate the image using the template
25
+ return template.generate_image(
26
+ product_image_url=product_image_url,
27
+ product_name=product_name,
28
+ original_price=original_price,
29
+ final_price=final_price,
30
+ coupon_code=coupon_code
31
+ )
32
+
33
+ except ValueError as e:
34
+ return f"Error: {e}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  except Exception as e:
36
  return f"An error occurred: {e}"
template_system.py ADDED
@@ -0,0 +1,359 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any
3
+ from PIL import Image, ImageDraw, ImageFont
4
+ import requests
5
+ from io import BytesIO
6
+ import tempfile
7
+
8
+
9
+ class Template(ABC):
10
+ """
11
+ Abstract base class for image templates.
12
+ Each template defines its own configuration for box sizes, positions, colors, and fonts.
13
+ """
14
+
15
+ def __init__(self, template_path: str = None):
16
+ self.template_path = template_path
17
+
18
+ @abstractmethod
19
+ def get_box_config(self) -> Dict[str, Any]:
20
+ """Return box configuration including size and position for product image."""
21
+ pass
22
+
23
+ @abstractmethod
24
+ def get_text_config(self) -> Dict[str, Dict[str, Any]]:
25
+ """Return text configuration including positions, colors, and fonts for all text elements."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def get_font_config(self) -> Dict[str, Dict[str, Any]]:
30
+ """Return font configuration for different text elements."""
31
+ pass
32
+
33
+ def load_template_image(self) -> Image.Image:
34
+ """Load and return the template image."""
35
+ return Image.open(self.template_path).convert("RGBA")
36
+
37
+ def load_fonts(self) -> Dict[str, ImageFont.FreeTypeFont]:
38
+ """Load and return all required fonts."""
39
+ fonts = {}
40
+ font_config = self.get_font_config()
41
+
42
+ for font_name, config in font_config.items():
43
+ try:
44
+ fonts[font_name] = ImageFont.truetype(config['path'], config['size'])
45
+ except IOError:
46
+ print(f"Font {config['path']} not found. Using default font.")
47
+ fonts[font_name] = ImageFont.load_default()
48
+
49
+ return fonts
50
+
51
+ def generate_image(self, product_image_url: str, product_name: str,
52
+ original_price: str, final_price: str, coupon_code: str) -> str:
53
+ """
54
+ Generate the promotional image using this template's configuration.
55
+
56
+ Args:
57
+ product_image_url: URL of the product image
58
+ product_name: Name of the product
59
+ original_price: Original price of the product
60
+ final_price: Final price of the product
61
+ coupon_code: Coupon code to display
62
+
63
+ Returns:
64
+ Path to the generated image file
65
+ """
66
+ try:
67
+ # Load template and fonts
68
+ template_image = self.load_template_image()
69
+ fonts = self.load_fonts()
70
+
71
+ # Fetch and process product image
72
+ response = requests.get(product_image_url)
73
+ product_image_data = BytesIO(response.content)
74
+ product_image = Image.open(product_image_data).convert("RGBA")
75
+
76
+ # Get box configuration
77
+ box_config = self.get_box_config()
78
+ box_size = box_config['size']
79
+ box_position = box_config['position']
80
+
81
+ # Resize product image to fit within box while preserving aspect ratio
82
+ product_image_resized = product_image.copy()
83
+ product_image_resized.thumbnail(box_size)
84
+
85
+ # Calculate position to center the image in the box
86
+ paste_x = box_position[0] + (box_size[0] - product_image_resized.width) // 2
87
+ paste_y = box_position[1] + (box_size[1] - product_image_resized.height) // 2
88
+ paste_position = (paste_x, paste_y)
89
+
90
+ # Paste product image onto template
91
+ template_image.paste(product_image_resized, paste_position, product_image_resized)
92
+
93
+ # Draw text elements
94
+ draw = ImageDraw.Draw(template_image)
95
+ text_config = self.get_text_config()
96
+
97
+ # Draw each text element
98
+ for element_name, config in text_config.items():
99
+ text_content = self._get_text_content(element_name, product_name,
100
+ original_price, final_price, coupon_code)
101
+ position = config['position']
102
+ color = config['color']
103
+ font_name = config['font']
104
+ anchor = config.get('anchor', 'ms')
105
+
106
+ draw.text(position, text_content, font=fonts[font_name],
107
+ fill=color, anchor=anchor)
108
+
109
+ # Save the result
110
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
111
+ template_image.save(temp_file.name)
112
+ return temp_file.name
113
+
114
+ except FileNotFoundError:
115
+ return f"Error: The template file '{self.template_path}' was not found."
116
+ except Exception as e:
117
+ return f"An error occurred: {e}"
118
+
119
+ def _get_text_content(self, element_name: str, product_name: str,
120
+ original_price: str, final_price: str, coupon_code: str) -> str:
121
+ """Get the actual text content for each text element."""
122
+ content_map = {
123
+ 'product_name': product_name,
124
+ 'original_price': f"De: R$ {original_price}",
125
+ 'final_price': f"Por: R$ {final_price}",
126
+ 'coupon_code': coupon_code
127
+ }
128
+ return content_map.get(element_name, '')
129
+
130
+
131
+ class TemplateRegistry:
132
+ """Registry for managing different template types."""
133
+
134
+ _templates = {}
135
+
136
+ @classmethod
137
+ def register(cls, name: str, template_class):
138
+ """Register a template class."""
139
+ cls._templates[name] = template_class
140
+
141
+ @classmethod
142
+ def get_template(cls, name: str) -> Template:
143
+ """Get a template instance by name."""
144
+ if name not in cls._templates:
145
+ raise ValueError(f"Template '{name}' not found")
146
+ template_instance = cls._templates[name]()
147
+ return template_instance
148
+
149
+ @classmethod
150
+ def list_templates(cls) -> list:
151
+ """List all registered template names."""
152
+ return list(cls._templates.keys())
153
+
154
+
155
+ class LidiPromoTemplate(Template):
156
+ """
157
+ Template implementation for Lidi promotional images.
158
+ Uses the original hardcoded values from the existing implementation.
159
+ """
160
+
161
+ def __init__(self, template_path: str = None):
162
+ super().__init__(template_path or "assets/template_1.png")
163
+
164
+ def get_box_config(self) -> Dict[str, Any]:
165
+ """Return box configuration for product image."""
166
+ return {
167
+ 'size': (442, 353),
168
+ 'position': (140, 280) # (x, y) from top-left corner
169
+ }
170
+
171
+ def get_text_config(self) -> Dict[str, Dict[str, Any]]:
172
+ """Return text configuration for all text elements."""
173
+ return {
174
+ 'product_name': {
175
+ 'position': (360, 710),
176
+ 'color': '#FFFFFF',
177
+ 'font': 'font_name',
178
+ 'anchor': 'ms'
179
+ },
180
+ 'original_price': {
181
+ 'position': (360, 800),
182
+ 'color': '#FFFFFF',
183
+ 'font': 'font_price_from',
184
+ 'anchor': 'ms'
185
+ },
186
+ 'final_price': {
187
+ 'position': (360, 860),
188
+ 'color': '#FEE161', # Yellow color from original design
189
+ 'font': 'font_price',
190
+ 'anchor': 'ms'
191
+ },
192
+ 'coupon_code': {
193
+ 'position': (360, 993),
194
+ 'color': '#000000',
195
+ 'font': 'font_cupom',
196
+ 'anchor': 'ms'
197
+ }
198
+ }
199
+
200
+ def get_font_config(self) -> Dict[str, Dict[str, Any]]:
201
+ """Return font configuration for different text elements."""
202
+ return {
203
+ 'font_name': {
204
+ 'path': 'assets/Montserrat-Bold.ttf',
205
+ 'size': 47
206
+ },
207
+ 'font_price_from': {
208
+ 'path': 'assets/Montserrat-Regular.ttf',
209
+ 'size': 28
210
+ },
211
+ 'font_price': {
212
+ 'path': 'assets/Montserrat-Bold.ttf',
213
+ 'size': 47
214
+ },
215
+ 'font_cupom': {
216
+ 'path': 'assets/Montserrat-Bold.ttf',
217
+ 'size': 33
218
+ }
219
+ }
220
+
221
+
222
+ class NaturaTemplate(Template):
223
+ """
224
+ Template implementation for Natura promotional images.
225
+ Uses template_b_natura.png with different configuration.
226
+ """
227
+
228
+ def __init__(self, template_path: str = None):
229
+ super().__init__(template_path or "assets/template_b_natura.png")
230
+
231
+ def get_box_config(self) -> Dict[str, Any]:
232
+ """Return box configuration for product image."""
233
+ return {
234
+ 'size': (602, 424),
235
+ 'position': (54, 254) # (x, y) from top-left corner
236
+ }
237
+
238
+ def get_text_config(self) -> Dict[str, Dict[str, Any]]:
239
+ """Return text configuration for all text elements."""
240
+ return {
241
+ 'product_name': {
242
+ 'position': (72, 727),
243
+ 'color': '#000000',
244
+ 'font': 'font_name',
245
+ 'anchor': 'ls'
246
+ },
247
+ 'original_price': {
248
+ 'position': (72, 765),
249
+ 'color': '#666666',
250
+ 'font': 'font_price_from',
251
+ 'anchor': 'ls'
252
+ },
253
+ 'final_price': {
254
+ 'position': (90, 837),
255
+ 'color': '#FFFFFF',
256
+ 'font': 'font_price',
257
+ 'anchor': 'lm'
258
+ },
259
+ 'coupon_code': {
260
+ 'position': (461, 957),
261
+ 'color': '#FFFFFF',
262
+ 'font': 'font_cupom',
263
+ 'anchor': 'ms'
264
+ }
265
+ }
266
+
267
+ def get_font_config(self) -> Dict[str, Dict[str, Any]]:
268
+ """Return font configuration for different text elements."""
269
+ return {
270
+ 'font_name': {
271
+ 'path': 'assets/Montserrat-Bold.ttf',
272
+ 'size': 32
273
+ },
274
+ 'font_price_from': {
275
+ 'path': 'assets/Montserrat-Regular.ttf',
276
+ 'size': 22
277
+ },
278
+ 'font_price': {
279
+ 'path': 'assets/Montserrat-Bold.ttf',
280
+ 'size': 40
281
+ },
282
+ 'font_cupom': {
283
+ 'path': 'assets/Montserrat-Bold.ttf',
284
+ 'size': 42
285
+ }
286
+ }
287
+
288
+
289
+ class AvonTemplate(Template):
290
+ """
291
+ Template implementation for Avon promotional images.
292
+ Uses template_b_avon.png with Avon-specific configuration.
293
+ """
294
+
295
+ def __init__(self, template_path: str = None):
296
+ super().__init__(template_path or "assets/template_b_avon.png")
297
+
298
+ def get_box_config(self) -> Dict[str, Any]:
299
+ """Return box configuration for product image."""
300
+ return {
301
+ 'size': (602, 424),
302
+ 'position': (54, 254) # (x, y) from top-left corner
303
+ }
304
+
305
+ def get_text_config(self) -> Dict[str, Dict[str, Any]]:
306
+ """Return text configuration for all text elements."""
307
+ return {
308
+ 'product_name': {
309
+ 'position': (72, 727),
310
+ 'color': '#000000',
311
+ 'font': 'font_name',
312
+ 'anchor': 'ls'
313
+ },
314
+ 'original_price': {
315
+ 'position': (72, 765),
316
+ 'color': '#666666',
317
+ 'font': 'font_price_from',
318
+ 'anchor': 'ls'
319
+ },
320
+ 'final_price': {
321
+ 'position': (90, 837),
322
+ 'color': '#FFFFFF',
323
+ 'font': 'font_price',
324
+ 'anchor': 'lm'
325
+ },
326
+ 'coupon_code': {
327
+ 'position': (461, 957),
328
+ 'color': '#FFFFFF',
329
+ 'font': 'font_cupom',
330
+ 'anchor': 'ms'
331
+ }
332
+ }
333
+
334
+ def get_font_config(self) -> Dict[str, Dict[str, Any]]:
335
+ """Return font configuration for different text elements."""
336
+ return {
337
+ 'font_name': {
338
+ 'path': 'assets/Montserrat-Bold.ttf',
339
+ 'size': 32
340
+ },
341
+ 'font_price_from': {
342
+ 'path': 'assets/Montserrat-Regular.ttf',
343
+ 'size': 22
344
+ },
345
+ 'font_price': {
346
+ 'path': 'assets/Montserrat-Bold.ttf',
347
+ 'size': 40
348
+ },
349
+ 'font_cupom': {
350
+ 'path': 'assets/Montserrat-Bold.ttf',
351
+ 'size': 42
352
+ }
353
+ }
354
+
355
+
356
+ # Register additional templates
357
+ TemplateRegistry.register('lidi_promo', LidiPromoTemplate)
358
+ TemplateRegistry.register('natura', NaturaTemplate)
359
+ TemplateRegistry.register('avon', AvonTemplate)