scmlewis commited on
Commit
4cf5765
·
verified ·
1 Parent(s): 160b28a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +177 -68
app.py CHANGED
@@ -12,10 +12,15 @@ def save_binary_file(file_name, data):
12
  f.write(data)
13
 
14
  def generate_edit(prompt, pil_image, api_key, model="gemini-2.0-flash-exp"):
 
15
  client = genai.Client(api_key=(api_key.strip() if api_key and api_key.strip() != "" else os.environ.get("GEMINI_API_KEY")))
 
 
16
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_img:
17
  image_path = tmp_img.name
18
  pil_image.save(image_path)
 
 
19
  files = [client.files.upload(file=image_path)]
20
  contents = [
21
  types.Content(
@@ -26,6 +31,8 @@ def generate_edit(prompt, pil_image, api_key, model="gemini-2.0-flash-exp"):
26
  ],
27
  ),
28
  ]
 
 
29
  generate_content_config = types.GenerateContentConfig(
30
  temperature=1,
31
  top_p=0.95,
@@ -34,8 +41,11 @@ def generate_edit(prompt, pil_image, api_key, model="gemini-2.0-flash-exp"):
34
  response_modalities=["image", "text"],
35
  response_mime_type="text/plain",
36
  )
 
37
  text_response = ""
38
- image_path_result = None
 
 
39
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_out:
40
  out_path = tmp_out.name
41
  for chunk in client.models.generate_content_stream(
@@ -48,128 +58,227 @@ def generate_edit(prompt, pil_image, api_key, model="gemini-2.0-flash-exp"):
48
  candidate = chunk.candidates[0].content.parts[0]
49
  if candidate.inline_data:
50
  save_binary_file(out_path, candidate.inline_data.data)
51
- image_path_result = out_path
52
  break
53
  else:
54
  text_response += chunk.text + "\n"
55
- del files
56
- return image_path_result, text_response
 
 
 
 
 
57
 
58
- def process_image_and_prompt(pil_image, prompt, api_key, progress=None):
59
  try:
60
- if progress:
61
- progress("Generating…")
 
62
  image_path, text_out = generate_edit(prompt, pil_image, api_key)
63
  if image_path:
64
  img = Image.open(image_path)
65
  if img.mode == "RGBA":
66
  img = img.convert("RGB")
67
- if progress:
68
- progress("Completed ✓")
 
69
  return img, "Image generated successfully!", None
70
  else:
71
- if progress:
72
- progress(f"Warning: {text_out.strip() or 'No image generated.'}")
73
- return None, f"⚠️ {text_out.strip()?text_out.strip():'No image generated.'}", None
 
74
  except Exception as e:
75
- if progress:
76
- progress("Error")
77
  return None, f"❌ Generation failed: {str(e)}", None
78
 
79
- def reset_inputs(api_key_value=""):
80
- return None, "", api_key_value, ""
81
 
82
  # Styles
83
  css_style = """
84
- :root{--bg:#0e111a;--panel:#151a24;--text:#e9eefc;--muted:#9fb3c8;--accent:#6a8efd}
85
- html,body, #app{height:100%}
86
- body{background:#0e111a;color:var(--text);font-family:Inter, system-ui, Arial}
87
- .header-block{width:100%; display:flex; justify-content:center; padding:14px 0 6px}
88
- .header-gradient{width:100%; padding:26px 0; border-radius:12px; background:linear-gradient(90deg,#6a8efd,#44abc7); box-shadow:0 2px 12px rgb(0 0 0 / 15%)}
89
- .header-title{margin:0; font-size:2.8rem; font-weight:900; color:#fff; text-shadow:0 2px 8px rgba(0,0,0,.25)}
90
- .header-subtitle{color:#e7f0ff; font-size:1.05rem; margin-top:6px}
91
- .gradient-button{background:linear-gradient(90deg,#44abc7,#6a8efd); border:none; color:white; font-weight:700; padding:12px 28px; border-radius:10px; cursor:pointer; margin-right:8px}
92
- .gradient-button:hover{background:linear-gradient(90deg,#6a8efd,#44abc7)}
93
- .main{display:flex; gap:22px; align-items:flex-start; padding:0 14px}
94
- .sidebar{width:320px; background:#141923; padding:18px; border-radius:12px; box-shadow:0 2px 10px rgb(0 0 0 / 0.25)}
95
- .sidebar h2{color:#8ab4ff; font-size:1rem; margin:8px 0}
96
- .sidebar ul{margin:0 0 12px 18px; padding:0; color:#dbeafe; line-height:2}
97
- .main-panel{flex:1; min-width:0}
98
- .section-header{font-size:1.15rem; font-weight:800; color:#cbd5e1; margin:6px 0 8px}
99
- .row{display:flex; gap:16px; align-items:flex-start}
100
- .input-area, .output-area{ background:#1b2030; border-radius:12px; padding:12px; margin-bottom:8px}
101
- #status-text{ height:1.6em; line-height:1.6em; resize:none; overflow:hidden; text-align:left; padding:6px 8px; border-radius:6px; border:1px solid #2d2f40; background:#0f1320; color:#cbd5e1; font-weight:600}
102
- #output-image{ display:flex; justify-content:center; align-items:center; min-height:240px; padding:6px 0}
103
- #output-image img{ max-width:100%; max-height:420px; object-fit:contain; border-radius:12px; background:#23252b}
104
- @media (max-width: 1100px){
105
- .sidebar{ display:none } /* optional: collapse for narrow screens to preserve space */
106
- .main{ gap:12px }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  }
108
  """
109
 
110
- # UI
111
  with gr.Blocks(css=css_style) as app:
112
  gr.HTML(
113
  """
114
  <div class='header-block'>
115
  <div class='header-gradient'>
116
- <h1 class='header-title'>🖼️ Image Editor <span style="font-size:1.2em"> (Powered by Gemini)</span> 🔮</h1>
117
  <div class='header-subtitle'>Step-by-step prompts with a persistent status banner and progress feedback</div>
118
  </div>
119
  </div>
120
  """
121
  )
122
 
123
- with gr.Row(class_name="main"):
124
- # Sidebar with instructions and API key link
125
  with gr.Column(scale=3, elem_classes="sidebar"):
126
  gr.Markdown(
127
  """
128
  <h2>📖 How to Use</h2>
129
  <ul>
130
- <li>Step 1: Upload Image</li>
131
- <li>Step 2: Enter Editing Prompt</li>
132
- <li>Step 3: View Output</li>
133
  </ul>
134
- <hr style='border-color:#2c3244' />
135
  <h2>🔑 API Key</h2>
136
  <div>Get your key here: <a href="https://aistudio.google.com/apikey" target="_blank">Get your Google API key</a></div>
137
  """
138
  )
139
- # Main workflow
140
  with gr.Column(scale=9, elem_classes="main-panel"):
141
- with gr.Row():
142
  # Step 1: Upload Image
143
- with gr.Column():
144
- gr.Markdown("<div class='section-header'>Step 1: Upload Image</div>")
145
- image_input = gr.Image(type="pil", label=None, image_mode="RGBA")
146
 
147
- # Step 2: Prompt + API key
148
- gr.Markdown("<div class='section-header'>Step 2: Enter Editing Prompt</div>")
149
- prompt_input = gr.Textbox(label="Edit Prompt", placeholder="Describe how to edit the image", lines=2)
150
- api_key_input = gr.Textbox(label="Gemini API Key (required)", placeholder="Enter your Gemini API key here", type="password")
151
 
152
- with gr.Row():
153
- submit_btn = gr.Button("Generate Edit", elem_classes="gradient-button")
154
- reset_btn = gr.Button("Reset Inputs")
155
 
156
  # Step 3: Output
157
- with gr.Column():
158
- gr.Markdown("<div class='section-header'>Step 3: Image Output</div>")
159
- output_image = gr.Image(label=None, show_label=False, type="pil")
160
- status_text = gr.Textbox(label="Status", interactive=False, lines=1, id="status-text")
161
 
162
- # Callbacks
163
  def on_submit(pil_img, prompt, key, progress=None):
164
  if not key or key.strip() == "":
165
  raise gr.Error("Gemini API Key is required!")
166
  img, stat, _ = process_image_and_prompt(pil_img, prompt, key, progress)
167
  return img, stat
168
 
169
- def progress_fn(msg):
170
- # local progress hook: simply returns a placeholder by updating status_text listener
171
- return
172
-
173
  submit_btn.click(
174
  fn=on_submit,
175
  inputs=[image_input, prompt_input, api_key_input],
 
12
  f.write(data)
13
 
14
  def generate_edit(prompt, pil_image, api_key, model="gemini-2.0-flash-exp"):
15
+ # Initialize client
16
  client = genai.Client(api_key=(api_key.strip() if api_key and api_key.strip() != "" else os.environ.get("GEMINI_API_KEY")))
17
+
18
+ # Save image to a temp path for upload
19
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_img:
20
  image_path = tmp_img.name
21
  pil_image.save(image_path)
22
+
23
+ # Upload and prepare content
24
  files = [client.files.upload(file=image_path)]
25
  contents = [
26
  types.Content(
 
31
  ],
32
  ),
33
  ]
34
+
35
+ # Config with image + text modalities
36
  generate_content_config = types.GenerateContentConfig(
37
  temperature=1,
38
  top_p=0.95,
 
41
  response_modalities=["image", "text"],
42
  response_mime_type="text/plain",
43
  )
44
+
45
  text_response = ""
46
+ image_out_path = None
47
+
48
+ # Streamed generation to capture inline image data
49
  with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp_out:
50
  out_path = tmp_out.name
51
  for chunk in client.models.generate_content_stream(
 
58
  candidate = chunk.candidates[0].content.parts[0]
59
  if candidate.inline_data:
60
  save_binary_file(out_path, candidate.inline_data.data)
61
+ image_out_path = out_path
62
  break
63
  else:
64
  text_response += chunk.text + "\n"
65
+
66
+ # Cleanup
67
+ try:
68
+ del files
69
+ except Exception:
70
+ pass
71
+ return image_out_path, text_response
72
 
73
+ def process_image_and_prompt(pil_image, prompt, api_key, progress_callback=None):
74
  try:
75
+ # Indicate starting
76
+ if progress_callback:
77
+ progress_callback("Generating…")
78
  image_path, text_out = generate_edit(prompt, pil_image, api_key)
79
  if image_path:
80
  img = Image.open(image_path)
81
  if img.mode == "RGBA":
82
  img = img.convert("RGB")
83
+ # success
84
+ if progress_callback:
85
+ progress_callback("Done ✓")
86
  return img, "Image generated successfully!", None
87
  else:
88
+ # fail to generate image
89
+ if progress_callback:
90
+ progress_callback("Failed to generate image")
91
+ return None, f"⚠️ {text_out.strip()}", None
92
  except Exception as e:
93
+ if progress_callback:
94
+ progress_callback("Error")
95
  return None, f"❌ Generation failed: {str(e)}", None
96
 
97
+ def reset_inputs(api_key_value=None):
98
+ return None, "", api_key_value or "", ""
99
 
100
  # Styles
101
  css_style = """
102
+ :root {
103
+ --bg: #14161c;
104
+ --panel: #1e1f25;
105
+ --text: #e8eaf6;
106
+ --muted: #a0aec0;
107
+ --accent: #6a8efd;
108
+ }
109
+ body, .app-container {
110
+ background: var(--bg);
111
+ color: var(--text);
112
+ }
113
+ .header-block {
114
+ width: 100%;
115
+ display: flex;
116
+ align-items: center;
117
+ justify-content: center;
118
+ padding: 18px;
119
+ }
120
+ .header-gradient {
121
+ width: 100%;
122
+ padding: 28px 0;
123
+ border-radius: 14px;
124
+ background: linear-gradient(90deg, #6a8efd, #44abc7);
125
+ box-shadow: 0 2px 12px rgb(50 50 70 / 12%);
126
+ text-align: center;
127
+ }
128
+ .header-title {
129
+ margin: 0;
130
+ font-size: 2.8rem;
131
+ font-weight: 900;
132
+ color: #fff;
133
+ text-shadow: 1px 3px 12px rgba(0,0,0,.25);
134
+ }
135
+ .header-subtitle {
136
+ margin-top: 6px;
137
+ font-size: 1.05rem;
138
+ color: #e8f2ff;
139
+ }
140
+ .gradient-button {
141
+ background: linear-gradient(90deg, #44abc7, #6a8efd);
142
+ color: white;
143
+ font-weight: 700;
144
+ border: none;
145
+ padding: 12px 28px;
146
+ border-radius: 10px;
147
+ cursor: pointer;
148
+ transition: background 0.25s ease;
149
+ }
150
+ .gradient-button:hover {
151
+ background: linear-gradient(90deg, #6a8efd, #44abc7);
152
+ }
153
+ .main {
154
+ display: flex;
155
+ gap: 22px;
156
+ }
157
+ .sidebar {
158
+ background: #1f2230;
159
+ padding: 20px;
160
+ border-radius: 12px;
161
+ min-height: 360px;
162
+ width: 320px;
163
+ box-shadow: 0 2px 10px rgb(0 0 0 / 0.25);
164
+ }
165
+ .sidebar h2 {
166
+ color: #8ab4ff;
167
+ font-size: 1rem;
168
+ margin: 6px 0 8px;
169
+ }
170
+ .sidebar ul {
171
+ margin: 0;
172
+ padding-left: 18px;
173
+ color: #dbeafe;
174
+ line-height: 1.8;
175
+ }
176
+ .sidebar a { color: #97b7ff; text-decoration: none; }
177
+ .sidebar a:hover { text-decoration: underline; }
178
+
179
+ .main-panel {
180
+ flex: 1;
181
+ min-width: 0;
182
+ }
183
+ .section-header {
184
+ font-size: 1.15rem;
185
+ font-weight: 700;
186
+ color: #cbd5e1;
187
+ margin: 8px 0;
188
+ }
189
+ .input-area, .output-area {
190
+ background: #1b1e28;
191
+ border-radius: 12px;
192
+ padding: 14px;
193
+ box-shadow: inset 0 0 0 rgba(0,0,0,0.0);
194
+ }
195
+ .input-area { margin-bottom: 12px; }
196
+ .output-area { margin-top: 6px; text-align: center; }
197
+ #status-text {
198
+ height: 1.2em;
199
+ line-height: 1.2em;
200
+ font-weight: 600;
201
+ text-align: left;
202
+ overflow: hidden;
203
+ white-space: nowrap;
204
+ padding: 0;
205
+ border: none;
206
+ background: transparent;
207
+ color: #cbd5e1;
208
+ }
209
+ #output-image {
210
+ display: flex;
211
+ justify-content: center;
212
+ align-items: center;
213
+ }
214
+ #output-image img {
215
+ max-width: 100%;
216
+ max-height: 420px;
217
+ width: auto;
218
+ height: auto;
219
+ object-fit: contain;
220
+ border-radius: 12px;
221
+ background: #23252b;
222
  }
223
  """
224
 
225
+ # Layout
226
  with gr.Blocks(css=css_style) as app:
227
  gr.HTML(
228
  """
229
  <div class='header-block'>
230
  <div class='header-gradient'>
231
+ <h1 class='header-title'>🖼️ Image Editor <span style="font-size:1.1em;">(Powered by Gemini)</span> 🔮</h1>
232
  <div class='header-subtitle'>Step-by-step prompts with a persistent status banner and progress feedback</div>
233
  </div>
234
  </div>
235
  """
236
  )
237
 
238
+ with gr.Row():
239
+ # Sidebar (instructions)
240
  with gr.Column(scale=3, elem_classes="sidebar"):
241
  gr.Markdown(
242
  """
243
  <h2>📖 How to Use</h2>
244
  <ul>
245
+ <li>Step-by-step prompts guide the editing process.</li>
246
+ <li>Upload a PNG image, enter a prompt, then generate.</li>
247
+ <li>Keep your Gemini API key secure.</li>
248
  </ul>
249
+ <hr>
250
  <h2>🔑 API Key</h2>
251
  <div>Get your key here: <a href="https://aistudio.google.com/apikey" target="_blank">Get your Google API key</a></div>
252
  """
253
  )
254
+ # Main panel (steps and outputs)
255
  with gr.Column(scale=9, elem_classes="main-panel"):
256
+ with gr.Column():
257
  # Step 1: Upload Image
258
+ gr.Markdown("<div class='section-header'>Step 1: Upload Image</div>")
259
+ image_input = gr.Image(type="pil", label=None, image_mode="RGBA")
 
260
 
261
+ # Step 2: Prompt + API Key
262
+ gr.Markdown("<div class='section-header'>Step 2: Enter Editing Prompt</div>")
263
+ prompt_input = gr.Textbox(label="Edit Prompt", placeholder="Describe how to edit the image", lines=2)
264
+ api_key_input = gr.Textbox(label="Gemini API Key (required)", placeholder="Enter your Gemini API key here", type="password")
265
 
266
+ with gr.Row():
267
+ submit_btn = gr.Button("Generate Edit", elem_classes="gradient-button")
268
+ reset_btn = gr.Button("Reset Inputs")
269
 
270
  # Step 3: Output
271
+ gr.Markdown("<div class='section-header'>Step 3: Image Output</div>")
272
+ output_image = gr.Image(label=None, show_label=False, type="pil")
273
+ status_text = gr.Textbox(label="Status", interactive=False, lines=1, elem_id="status-text")
 
274
 
275
+ # Callback wiring
276
  def on_submit(pil_img, prompt, key, progress=None):
277
  if not key or key.strip() == "":
278
  raise gr.Error("Gemini API Key is required!")
279
  img, stat, _ = process_image_and_prompt(pil_img, prompt, key, progress)
280
  return img, stat
281
 
 
 
 
 
282
  submit_btn.click(
283
  fn=on_submit,
284
  inputs=[image_input, prompt_input, api_key_input],