rairo commited on
Commit
15dbffc
Β·
verified Β·
1 Parent(s): c7229af

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +453 -32
app.py CHANGED
@@ -1,42 +1,463 @@
1
- import os
2
  import streamlit as st
3
- from google import genai
4
- from google.genai import types
5
  from PIL import Image
6
  from io import BytesIO
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
 
8
 
9
- # Function to generate image from prompt
10
- def generate_image(prompt):
11
- api_key = os.getenv("GEMINI_API_KEY")
12
- if not api_key:
13
- st.error("GEMINI_API_KEY environment variable not set.")
14
- return None
15
- client = genai.Client(api_key=api_key)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
- response = client.models.generate_content(
18
- model="models/gemini-2.0-flash-exp",
19
- contents=[prompt],
20
- config=types.GenerateContentConfig(response_modalities=['Text', 'Image'])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  )
22
- for part in response.candidates[0].content.parts:
23
- if part.inline_data is not None:
24
- image = Image.open(BytesIO(part.inline_data.data))
25
- return image
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  except Exception as e:
27
- st.error(f"Error generating image: {e}")
28
- return None
29
-
30
- # Streamlit app
31
- st.title('Gemini Image Generator')
32
- prompt = st.text_input('Enter a prompt for image generation:')
33
- if st.button('Generate Image'):
34
- if prompt:
35
- with st.spinner('Generating image...'):
36
- image = generate_image(prompt)
37
- if image:
38
- st.image(image, caption='Generated Image')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  else:
40
- st.error("Failed to generate image.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  else:
42
- st.warning('Please enter a prompt.')
 
 
 
 
 
 
 
1
  import streamlit as st
 
 
2
  from PIL import Image
3
  from io import BytesIO
4
+ from google import genai
5
+ from google.genai import types
6
+ import re
7
+ import time
8
+ import os
9
+
10
+ # Add this line to disable Streamlit analytics which often causes PermissionError in containerized environments
11
+ os.environ["STREAMLIT_ANALYTICS_ENABLED"] = "false"
12
+
13
+ # ─────────────────────────────────────────────────────────────────────────────
14
+
15
+ # ─── 1. CONFIGURATION ─────────────────────────────────────────────────────────
16
+
17
+ # 1.1 Load your Google API key from environment or Streamlit secrets
18
+
19
+ try:
20
+ API_KEY = st.secrets["GOOGLE_API_KEY"]
21
+ except:
22
+ API_KEY = os.environ.get("GOOGLE_API_KEY")
23
+
24
+ if not API_KEY:
25
+ st.error("Please set GOOGLE_API_KEY in your environment variables or Streamlit secrets")
26
+ st.stop()
27
+
28
+ # 1.2 Initialize the GenAI client
29
+
30
+ client = genai.Client(api_key=API_KEY)
31
+
32
+ # 1.3 Constants
33
+
34
+ CATEGORY_MODEL = "gemini-2.0-flash-exp"
35
+ GENERATION_MODEL = "gemini-2.0-flash-exp"
36
+
37
+ # 1.4 Helper to parse numbered steps out of Gemini text
38
+
39
+ def parse_numbered_steps(text):
40
+ """
41
+ Expect Gemini to return something like:
42
+ 1. First step description...
43
+ 2. Second step description...
44
+ ...
45
+ Returns a list of (step_number, step_text).
46
+ """
47
+ # Split on patterns like "1.", "2." at line starts
48
+ raw_steps = re.split(r"\n\s*\d+\.\s+", text.strip())
49
+ # The first split entry might be empty if text starts with "1. "
50
+ if raw_steps and raw_steps[0].strip() == "":
51
+ raw_steps.pop(0)
52
+ steps = []
53
+ for idx, chunk in enumerate(raw_steps, start=1):
54
+ # Remove any trailing whitespace/newlines
55
+ steps.append((idx, chunk.strip()))
56
+ return steps
57
+
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+
60
+ # ─── 2. SESSION STATE SETUP ───────────────────────────────────────────────────
61
+
62
+ if "steps" not in st.session_state:
63
+ st.session_state.steps = [] # List[(int, str)] of parsed steps
64
+ if "images" not in st.session_state:
65
+ st.session_state.images = {} # Dict[int, PIL.Image] for inline graphics
66
+ if "tools_list" not in st.session_state:
67
+ st.session_state.tools_list = [] # List[str] of required tools/materials
68
+ if "current_step" not in st.session_state:
69
+ st.session_state.current_step = 1
70
+ if "done_flags" not in st.session_state:
71
+ st.session_state.done_flags = {} # Dict[int, bool] for checkboxes
72
+ if "notes" not in st.session_state:
73
+ st.session_state.notes = {} # Dict[int, str]
74
+ if "timers" not in st.session_state:
75
+ st.session_state.timers = {} # Dict[int, int] storing seconds left for any timer
76
+ if "category" not in st.session_state:
77
+ st.session_state.category = None
78
+ if "prompt_sent" not in st.session_state:
79
+ st.session_state.prompt_sent = False
80
+ if "timer_running" not in st.session_state:
81
+ st.session_state.timer_running = {}
82
+ if "last_tick" not in st.session_state:
83
+ st.session_state.last_tick = {}
84
 
85
+ # ─────────────────────────────────────────────────────────────────────────────
86
 
87
+ # ─── 3. LAYOUT & FUNCTIONS ───────────────────────────────────────────────────
88
+
89
+ def reset_state():
90
+ """ Clear out all session state so user can start fresh. """
91
+ st.session_state.steps = []
92
+ st.session_state.images = {}
93
+ st.session_state.tools_list = []
94
+ st.session_state.current_step = 1
95
+ st.session_state.done_flags = {}
96
+ st.session_state.notes = {}
97
+ st.session_state.timers = {}
98
+ st.session_state.category = None
99
+ st.session_state.prompt_sent = False
100
+ st.session_state.timer_running = {}
101
+ st.session_state.last_tick = {}
102
+
103
+ def convert_pil_to_genai_format(pil_image):
104
+ """Convert PIL Image to format expected by Google GenAI"""
105
+ # Convert to RGB if necessary
106
+ if pil_image.mode != 'RGB':
107
+ pil_image = pil_image.convert('RGB')
108
+
109
+ # Save to bytes
110
+ img_byte_arr = BytesIO()
111
+ pil_image.save(img_byte_arr, format='JPEG')
112
+ img_byte_arr = img_byte_arr.getvalue()
113
+
114
+ return {
115
+ 'mime_type': 'image/jpeg',
116
+ 'data': img_byte_arr
117
+ }
118
+
119
+ def detect_category_and_generate(uploaded_file, context_text):
120
+ """
121
+ 1) Call Gemini to identify the category (appliance, automotive, gardening, upcycling).
122
+ 2) Then call Gemini to get:
123
+ - a "tools & materials" list
124
+ - numbered step-by-step instructions
125
+ 3) Parse everything into session_state.
126
+ """
127
  try:
128
+ # Load and process the uploaded image
129
+ image = Image.open(uploaded_file)
130
+ image_data = convert_pil_to_genai_format(image)
131
+
132
+ # 3.1 IDENTIFY CATEGORY
133
+ category_prompt = (
134
+ "You are an expert DIY assistant. "
135
+ "Here is a single user-uploaded image. "
136
+ "Determine if it belongs to one of these categories (exactly): "
137
+ "home appliance repair, automotive maintenance, gardening & urban farming, or upcycling & sustainable crafts. "
138
+ "Reply back with exactly one category name."
139
+ )
140
+
141
+ cat_resp = client.models.generate_content(
142
+ model=CATEGORY_MODEL,
143
+ contents=[
144
+ category_prompt,
145
+ image_data
146
+ ]
147
+ )
148
+ cat_text = cat_resp.text.strip()
149
+ st.session_state.category = cat_text
150
+
151
+ # 3.2 REQUEST TOOLS + NUMBERED STEPS
152
+ detailed_prompt = (
153
+ f"Category: {cat_text}\n"
154
+ f"Context: {context_text}\n\n"
155
+ "Provide a detailed repair/maintenance guide with the following format:\n\n"
156
+ "TOOLS AND MATERIALS:\n"
157
+ "- Tool A\n"
158
+ "- Material B\n"
159
+ "- Part C\n\n"
160
+ "STEPS:\n"
161
+ "1. First step instructions (be specific and detailed)...\n"
162
+ "2. Second step instructions...\n"
163
+ "3. Continue with all necessary steps...\n\n"
164
+ "Keep each numbered step clear and actionable (2-3 sentences max). "
165
+ "Include safety warnings where appropriate."
166
  )
167
+
168
+ full_resp = client.models.generate_content(
169
+ model=GENERATION_MODEL,
170
+ contents=[
171
+ detailed_prompt,
172
+ image_data
173
+ ]
174
+ )
175
+
176
+ # 3.3 PARSE out tools + numbered steps
177
+ response_text = full_resp.text
178
+
179
+ # Split into tools and steps sections
180
+ tools_section = ""
181
+ steps_section = ""
182
+
183
+ if "TOOLS AND MATERIALS:" in response_text:
184
+ parts = response_text.split("TOOLS AND MATERIALS:")
185
+ if len(parts) > 1:
186
+ remaining = parts[1]
187
+ if "STEPS:" in remaining:
188
+ tools_part, steps_part = remaining.split("STEPS:", 1)
189
+ tools_section = tools_part.strip()
190
+ steps_section = steps_part.strip()
191
+ else:
192
+ tools_section = remaining.strip()
193
+ elif "STEPS:" in response_text:
194
+ parts = response_text.split("STEPS:")
195
+ if len(parts) > 1:
196
+ steps_section = parts[1].strip()
197
+ else:
198
+ # Fallback: try to parse the whole response
199
+ lines = response_text.split('\n')
200
+ tools_lines = []
201
+ steps_lines = []
202
+ current_section = "unknown"
203
+
204
+ for line in lines:
205
+ line = line.strip()
206
+ if line.startswith('-') and current_section != "steps":
207
+ current_section = "tools"
208
+ tools_lines.append(line)
209
+ elif re.match(r'^\d+\.', line):
210
+ current_section = "steps"
211
+ steps_lines.append(line)
212
+ elif current_section == "steps" and line:
213
+ steps_lines.append(line)
214
+
215
+ tools_section = '\n'.join(tools_lines)
216
+ steps_section = '\n'.join(steps_lines)
217
+
218
+ # Parse tools
219
+ tools = []
220
+ for line in tools_section.split('\n'):
221
+ line = line.strip()
222
+ if line.startswith('-'):
223
+ tools.append(line.lstrip('- ').strip())
224
+
225
+ # Parse steps
226
+ parsed_steps = parse_numbered_steps(steps_section)
227
+
228
+ # Store in session_state
229
+ st.session_state.tools_list = tools
230
+ st.session_state.steps = parsed_steps
231
+
232
+ # Initialize done flags / timers / notes
233
+ for idx, step_text in parsed_steps:
234
+ st.session_state.done_flags[idx] = False
235
+ st.session_state.notes[idx] = ""
236
+ # If the step text mentions "wait X minutes" or "wait X seconds", extract and set up a timer
237
+ timer_match = re.search(r'wait\s+(\d+)\s*(seconds?|minutes?)', step_text.lower())
238
+ if timer_match:
239
+ val = int(timer_match.group(1))
240
+ unit = timer_match.group(2)
241
+ seconds_left = val * (60 if "minute" in unit else 1)
242
+ st.session_state.timers[idx] = seconds_left
243
+ else:
244
+ st.session_state.timers[idx] = 0
245
+
246
+ st.session_state.prompt_sent = True
247
+
248
  except Exception as e:
249
+ st.error(f"Error processing request: {str(e)}")
250
+ st.error("Please check your API key and try again.")
251
+
252
+ def render_sidebar_navigation():
253
+ """
254
+ A sidebar listing each step by number + a checkbox (miniaturized)
255
+ so users can click to jump directly to any step.
256
+ Also show overall progress.
257
+ """
258
+ st.sidebar.markdown("## Steps Navigation")
259
+ total_steps = len(st.session_state.steps)
260
+ if total_steps > 0:
261
+ completed = sum(1 for done in st.session_state.done_flags.values() if done)
262
+ st.sidebar.progress(completed / total_steps)
263
+ st.sidebar.write(f"Progress: {completed}/{total_steps} steps")
264
+
265
+ for (idx, text) in st.session_state.steps:
266
+ is_done = st.session_state.done_flags.get(idx, False)
267
+ # We show "βœ“" if done, else "Β·"
268
+ label = f"{'βœ“' if is_done else 'Β·'} Step {idx}"
269
+ if st.sidebar.button(label, key=f"nav_{idx}"):
270
+ st.session_state.current_step = idx
271
+ st.rerun()
272
+
273
+ def render_tools_list():
274
+ """ Show the Tools/Materials list in an expander. """
275
+ if st.session_state.tools_list:
276
+ with st.expander("πŸ”§ Required Tools & Materials", expanded=False):
277
+ for item in st.session_state.tools_list:
278
+ st.write(f"- {item}")
279
+
280
+ def render_step(idx, text):
281
+ """
282
+ Render a single step "slide":
283
+ - Show step number in header
284
+ - Show the instruction text
285
+ - If there is a timer, show a countdown widget
286
+ - Provide a checkbox to mark as "Done"
287
+ - Provide a text area for personal notes & optional photo upload
288
+ - "Prev" / "Next" buttons
289
+ """
290
+ total = len(st.session_state.steps)
291
+ st.markdown(f"### Step {idx} of {total}")
292
+
293
+ # Show the step instruction
294
+ st.write(text)
295
+
296
+ # Timer functionality
297
+ seconds_left = st.session_state.timers.get(idx, 0)
298
+ if seconds_left > 0:
299
+ if idx not in st.session_state.timer_running:
300
+ st.session_state.timer_running[idx] = False
301
+
302
+ if not st.session_state.timer_running[idx]:
303
+ mins = seconds_left // 60
304
+ secs = seconds_left % 60
305
+ if st.button(f"⏱️ Start timer ({mins}m {secs}s)", key=f"start_{idx}"):
306
+ st.session_state.timer_running[idx] = True
307
+ st.session_state.last_tick[idx] = time.time()
308
+ st.rerun()
309
+ else:
310
+ # Show countdown
311
+ if idx in st.session_state.last_tick:
312
+ now = time.time()
313
+ elapsed = int(now - st.session_state.last_tick[idx])
314
+ if elapsed >= 1:
315
+ st.session_state.timers[idx] = max(0, st.session_state.timers[idx] - elapsed)
316
+ st.session_state.last_tick[idx] = now
317
+
318
+ remaining = st.session_state.timers[idx]
319
+ if remaining > 0:
320
+ mins = remaining // 60
321
+ secs = remaining % 60
322
+ st.metric("⏲️ Timer", f"{mins:02d}:{secs:02d}")
323
+ time.sleep(1)
324
+ st.rerun()
325
  else:
326
+ st.success("⏰ Timer completed!")
327
+ st.session_state.timer_running[idx] = False
328
+
329
+ # Done checkbox
330
+ done = st.checkbox("βœ… Mark this step as completed",
331
+ value=st.session_state.done_flags.get(idx, False),
332
+ key=f"done_{idx}")
333
+ st.session_state.done_flags[idx] = done
334
+
335
+ # Notes section
336
+ notes = st.text_area("πŸ“ Your notes for this step:",
337
+ value=st.session_state.notes.get(idx, ""),
338
+ height=100,
339
+ key=f"notes_{idx}")
340
+ st.session_state.notes[idx] = notes
341
+
342
+ # Photo upload
343
+ photo = st.file_uploader("πŸ“· Upload a progress photo (optional)",
344
+ type=["jpg", "jpeg", "png"],
345
+ key=f"photo_{idx}")
346
+ if photo:
347
+ st.image(Image.open(photo), caption=f"Progress photo for step {idx}", use_container_width=True)
348
+
349
+ # Navigation buttons
350
+ st.markdown("---")
351
+ col1, col2, col3 = st.columns([1, 2, 1])
352
+
353
+ with col1:
354
+ if idx > 1 and st.button("⬅️ Previous", key=f"prev_{idx}"):
355
+ st.session_state.current_step = idx - 1
356
+ st.rerun()
357
+
358
+ with col3:
359
+ if idx < total and st.button("Next ➑️", key=f"next_{idx}"):
360
+ st.session_state.current_step = idx + 1
361
+ st.rerun()
362
+
363
+ # ─────────────────────────────────────────────────────────────────────────────
364
+
365
+ # ─── 4. APP LAYOUT ─────────────────────────────────────────────────────────────
366
+
367
+ st.set_page_config(page_title="NeoFix DIY Assistant", page_icon="πŸ› οΈ", layout="wide")
368
+
369
+ st.title("πŸ› οΈ NeoFix AI-Powered DIY Assistant")
370
+
371
+ with st.expander("ℹ️ How it works", expanded=False):
372
+ st.write(
373
+ """
374
+ 1. **Upload a photo** of the item you want to fix or build (appliance, car part, plant, craft project).
375
+ 2. **Add context** (optional) - describe what’s wrong or what you want to achieve.
376
+ 3. **Get AI guidance** - The AI will detect the category and provide step-by-step instructions.
377
+ 4. **Follow the steps** - Each step includes:
378
+ - Clear instructions
379
+ - Progress tracking with checkboxes
380
+ - Timer functionality for waiting periods
381
+ - Note-taking area
382
+ - Photo upload for progress tracking
383
+ 5. **Navigate easily** - Use the sidebar to jump between steps and track overall progress.
384
+ """
385
+ )
386
+
387
+ # Main upload section
388
+
389
+ st.markdown("---")
390
+ col1, col2 = st.columns([3, 1])
391
+
392
+ with col1:
393
+ uploaded_image = st.file_uploader("πŸ“· Upload a photo of your project",
394
+ type=["jpg", "jpeg", "png"],
395
+ help="Supported formats: JPG, JPEG, PNG")
396
+ context_text = st.text_area("✏️ Describe the issue or your goal (optional)",
397
+ height=80,
398
+ placeholder="e.g., 'My toaster won’t turn on' or 'I want to turn this into a planter'")
399
+
400
+ with col2:
401
+ st.markdown("### Actions")
402
+ if st.button("πŸš€ Get AI Guidance", type="primary", use_container_width=True):
403
+ if not uploaded_image:
404
+ st.warning("⚠️ Please upload an image first!")
405
+ else:
406
+ with st.spinner("πŸ€– Analyzing your image and generating instructions..."):
407
+ detect_category_and_generate(uploaded_image, context_text)
408
+
409
+ if st.button("πŸ”„ Start Over", use_container_width=True):
410
+ reset_state()
411
+ st.success("βœ… Reset complete!")
412
+ st.rerun()
413
+
414
+ # Show the tutorial interface if we have generated steps
415
+
416
+ if st.session_state.prompt_sent:
417
+ # Sidebar navigation
418
+ render_sidebar_navigation()
419
+
420
+ # Main content area
421
+ st.markdown("---")
422
+
423
+ # Show detected category
424
+ if st.session_state.category:
425
+ st.markdown(f"### πŸ” Detected Category: **{st.session_state.category}**")
426
+
427
+ # Show tools list
428
+ render_tools_list()
429
+
430
+ st.markdown("---")
431
+
432
+ # Show current step
433
+ if st.session_state.steps:
434
+ # Ensure current_step is valid
435
+ max_step = len(st.session_state.steps)
436
+ if st.session_state.current_step > max_step:
437
+ st.session_state.current_step = max_step
438
+ elif st.session_state.current_step < 1:
439
+ st.session_state.current_step = 1
440
+
441
+ step_tuple = st.session_state.steps[st.session_state.current_step - 1]
442
+ step_num, step_text = step_tuple
443
+ render_step(step_num, step_text)
444
+
445
+ # Overall progress at bottom
446
+ st.markdown("---")
447
+ total_steps = len(st.session_state.steps)
448
+ done_count = sum(1 for d in st.session_state.done_flags.values() if d)
449
+ progress = done_count / total_steps if total_steps > 0 else 0
450
+
451
+ st.progress(progress)
452
+ st.markdown(f"**Overall Progress:** {done_count} of {total_steps} steps completed ({progress:.0%})")
453
+
454
+ if done_count == total_steps:
455
+ st.balloons()
456
+ st.success("πŸŽ‰ Congratulations! You've completed all steps!")
457
  else:
458
+ st.error("No steps generated. Please try uploading a different image.")
459
+
460
+ # Footer
461
+
462
+ st.markdown("---")
463
+ st.markdown("*Powered by Google Gemini AI - Your intelligent DIY companion*")