rairo commited on
Commit
8dd936e
·
verified ·
1 Parent(s): 931ff2c

Update sozo_gen.py

Browse files
Files changed (1) hide show
  1. sozo_gen.py +118 -0
sozo_gen.py CHANGED
@@ -24,6 +24,8 @@ from typing import Dict, List, Tuple, Any
24
  from langchain_google_genai import ChatGoogleGenerativeAI
25
  from google import genai
26
  import requests
 
 
27
 
28
  # --- Configuration ---
29
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
@@ -83,6 +85,122 @@ def clean_narration(txt: str) -> str:
83
 
84
  def placeholder_img() -> Image.Image: return Image.new("RGB", (WIDTH, HEIGHT), (230, 230, 230))
85
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
  # NEW: Keyword extraction for better Pexels searches
87
  def extract_keywords_for_query(text: str, llm) -> str:
88
  prompt = f"""
 
24
  from langchain_google_genai import ChatGoogleGenerativeAI
25
  from google import genai
26
  import requests
27
+ # In sozo_gen.py, near the other google imports
28
+ from google.genai import types as genai_types
29
 
30
  # --- Configuration ---
31
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - [%(funcName)s] - %(message)s')
 
85
 
86
  def placeholder_img() -> Image.Image: return Image.new("RGB", (WIDTH, HEIGHT), (230, 230, 230))
87
 
88
+ # In sozo_gen.py, add these new functions at the end of the file
89
+
90
+ def generate_image_with_gemini(prompt: str) -> Image.Image:
91
+ """Generates an image using the specified Gemini model and client configuration."""
92
+ logging.info(f"Generating Gemini image with prompt: '{prompt}'")
93
+ try:
94
+ # Use the genai.Client as per the correct implementation
95
+ client = genai.Client(api_key=API_KEY)
96
+ full_prompt = f"A professional, 3d digital art style illustration for a business presentation: {prompt}"
97
+
98
+ response = client.models.generate_content(
99
+ model="gemini-2.0-flash-exp",
100
+ contents=full_prompt,
101
+ config=genai_types.GenerateContentConfig(
102
+ response_modalities=["Text", "Image"]
103
+ ),
104
+ )
105
+
106
+ # Find the image part in the response
107
+ img_part = next((part for part in response.candidates[0].content.parts if part.content_type == "Image"), None)
108
+
109
+ if img_part:
110
+ # The content is already bytes, so we can open it directly
111
+ return Image.open(io.BytesIO(img_part.content)).convert("RGB")
112
+ else:
113
+ logging.error("Gemini response did not contain an image.")
114
+ return None
115
+ except Exception as e:
116
+ logging.error(f"Gemini image generation failed: {e}")
117
+ return None
118
+
119
+ def generate_slides_from_report(raw_md: str, chart_urls: dict, uid: str, project_id: str, bucket, llm):
120
+ """
121
+ Uses an AI planner to convert a report into a 10-slide presentation deck.
122
+ """
123
+ logging.info(f"Generating slides for project {project_id}")
124
+
125
+ planner_prompt = f"""
126
+ You are an expert presentation designer. Your task is to convert the following data analysis report into a concise and visually engaging 10-slide deck.
127
+
128
+ **Full Report Content:**
129
+ ---
130
+ {raw_md}
131
+ ---
132
+
133
+ **Instructions:**
134
+ 1. Read the entire report to understand the core narrative and key findings.
135
+ 2. Create a plan for exactly 10 slides.
136
+ 3. For each slide, define a `title` and short `content` (2-3 bullet points or a brief paragraph).
137
+ 4. For the visual on each slide, you must decide between two types:
138
+ - If a report section is supported by an existing chart (indicated by a `<generate_chart:...>` tag), you **must** use it. Set `visual_type: "existing_chart"` and `visual_ref: "the exact chart description from the tag"`.
139
+ - For key points without a chart (like introductions, conclusions, or text-only insights), you **must** request a new image. Set `visual_type: "new_image"` and `visual_ref: "a concise, descriptive prompt for an AI to generate a 3D digital art style illustration"`.
140
+ 5. You must request exactly 3-4 new images to balance the presentation.
141
+
142
+ **Output Format:**
143
+ Return ONLY a valid JSON array of 10 slide objects. Do not include any other text or markdown formatting.
144
+
145
+ Example:
146
+ [
147
+ {{ "slide_number": 1, "title": "Introduction", "content": "...", "visual_type": "new_image", "visual_ref": "A 3D illustration of a rising stock chart" }},
148
+ {{ "slide_number": 2, "title": "Sales by Region", "content": "...", "visual_type": "existing_chart", "visual_ref": "bar | Sales by Region" }},
149
+ ...
150
+ ]
151
+ """
152
+
153
+ try:
154
+ plan_response = llm.invoke(planner_prompt).content.strip()
155
+ if plan_response.startswith("```json"):
156
+ plan_response = plan_response[7:-3]
157
+ slide_plan = json.loads(plan_response)
158
+ except Exception as e:
159
+ logging.error(f"Failed to generate or parse slide plan: {e}")
160
+ return None
161
+
162
+ final_slides = []
163
+ for slide in slide_plan:
164
+ try:
165
+ image_url = None
166
+ visual_type = slide.get("visual_type")
167
+ visual_ref = slide.get("visual_ref")
168
+
169
+ if visual_type == "existing_chart":
170
+ sanitized_ref = sanitize_for_firebase_key(visual_ref)
171
+ image_url = chart_urls.get(sanitized_ref)
172
+ if not image_url:
173
+ logging.warning(f"Could not find existing chart for ref: '{visual_ref}' (sanitized: '{sanitized_ref}')")
174
+
175
+ elif visual_type == "new_image":
176
+ img = generate_image_with_gemini(visual_ref)
177
+ if img:
178
+ with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as temp_file:
179
+ img_path = Path(temp_file.name)
180
+ img.save(img_path, format="PNG")
181
+
182
+ blob_name = f"sozo_projects/{uid}/{project_id}/slides/slide_{uuid.uuid4().hex}.png"
183
+ blob = bucket.blob(blob_name)
184
+ blob.upload_from_filename(str(img_path))
185
+ image_url = blob.public_url
186
+ logging.info(f"Uploaded new slide image to {image_url}")
187
+ os.unlink(img_path)
188
+
189
+ if not image_url:
190
+ logging.warning(f"Visual generation failed for slide {slide.get('slide_number')}. Skipping visual for this slide.")
191
+
192
+ final_slides.append({
193
+ "slide_number": slide.get("slide_number"),
194
+ "title": slide.get("title"),
195
+ "content": slide.get("content"),
196
+ "image_url": image_url or ""
197
+ })
198
+ except Exception as slide_e:
199
+ logging.error(f"Failed to process slide {slide.get('slide_number')}: {slide_e}")
200
+ continue
201
+
202
+ return final_slides
203
+
204
  # NEW: Keyword extraction for better Pexels searches
205
  def extract_keywords_for_query(text: str, llm) -> str:
206
  prompt = f"""