@synsci/cli-darwin-x64 1.1.89 → 1.1.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -81,9 +81,10 @@ def _load_env_file():
81
81
  class ScientificSchematicGenerator:
82
82
  """Generate scientific schematics using AI with smart iterative refinement.
83
83
 
84
- Uses Gemini 3 Pro for quality review to determine if regeneration is needed.
85
- Multiple passes only occur if the generated schematic doesn't meet the
86
- quality threshold for the target document type.
84
+ Uses Nano Banana Pro (with Nano Banana Standard fallback) for generation
85
+ and Gemini 3.1 Pro for quality review. Multiple passes only occur if the
86
+ generated schematic doesn't meet the quality threshold for the target
87
+ document type.
87
88
  """
88
89
 
89
90
  # Quality thresholds by document type (score out of 10)
@@ -167,11 +168,15 @@ LAYOUT:
167
168
  self.verbose = verbose
168
169
  self._last_error = None # Track last error for better reporting
169
170
  self.base_url = "https://openrouter.ai/api/v1"
171
+ # Image generation models in priority order (fallback if primary fails)
170
172
  # Nano Banana Pro - Google's advanced image generation model
171
- # https://openrouter.ai/google/gemini-3-pro-image-preview
172
- self.image_model = "google/gemini-3-pro-image-preview"
173
- # Gemini 3 Pro for quality review - excellent vision and reasoning
174
- self.review_model = "google/gemini-3-pro"
173
+ self.image_models = [
174
+ "google/gemini-3-pro-image-preview", # Nano Banana Pro (best quality)
175
+ "google/gemini-2.5-flash-image", # Nano Banana Standard (fallback)
176
+ ]
177
+ self.image_model = self.image_models[0]
178
+ # Gemini 3.1 Pro for quality review - excellent vision and reasoning
179
+ self.review_model = "google/gemini-3.1-pro-preview"
175
180
 
176
181
  def _log(self, message: str):
177
182
  """Log message if verbose mode is enabled."""
@@ -336,76 +341,99 @@ LAYOUT:
336
341
  base64_data = base64.b64encode(image_data).decode("utf-8")
337
342
  return f"data:{mime_type};base64,{base64_data}"
338
343
 
339
- def generate_image(self, prompt: str) -> Optional[bytes]:
340
- """
341
- Generate an image using Nano Banana Pro.
342
-
343
- Args:
344
- prompt: Description of the diagram to generate
345
-
346
- Returns:
347
- Image bytes or None if generation failed
348
- """
349
- self._last_error = None # Reset error
350
-
351
- messages = [{"role": "user", "content": prompt}]
344
+ def _diagnose_response(self, response: Dict[str, Any], model: str):
345
+ """Print diagnostic info when image extraction fails."""
346
+ print(f" [diagnostic] Model: {model}")
347
+ if "error" in response:
348
+ print(f" [diagnostic] API error: {response['error']}")
349
+ if "choices" in response and response["choices"]:
350
+ msg = response["choices"][0].get("message", {})
351
+ print(f" [diagnostic] Message keys: {list(msg.keys())}")
352
+ finish = response["choices"][0].get("finish_reason", "unknown")
353
+ print(f" [diagnostic] Finish reason: {finish}")
354
+ content = msg.get("content", "")
355
+ if isinstance(content, str):
356
+ preview = content[:300] + "..." if len(content) > 300 else content
357
+ if preview:
358
+ print(f" [diagnostic] Content: {preview}")
359
+ elif isinstance(content, list):
360
+ print(f" [diagnostic] Content is list with {len(content)} items:")
361
+ for i, item in enumerate(content[:5]):
362
+ if isinstance(item, dict):
363
+ print(f" Item {i}: type={item.get('type')}, keys={list(item.keys())}")
364
+ images = msg.get("images", [])
365
+ if images:
366
+ print(f" [diagnostic] Images field: {len(images)} items")
367
+ reasoning = msg.get("reasoning", "")
368
+ if reasoning:
369
+ preview = reasoning[:200] + "..." if len(reasoning) > 200 else reasoning
370
+ print(f" [diagnostic] Reasoning: {preview}")
371
+ else:
372
+ print(f" [diagnostic] Response keys: {list(response.keys())}")
373
+ # Show raw response preview (truncated)
374
+ raw = json.dumps(response)[:500]
375
+ print(f" [diagnostic] Raw response: {raw}")
352
376
 
377
+ def _try_generate_with_model(self, model: str, messages: List[Dict[str, Any]]) -> Optional[bytes]:
378
+ """Try to generate an image with a specific model. Returns image bytes or None."""
353
379
  try:
354
- response = self._make_request(model=self.image_model, messages=messages, modalities=["image", "text"])
380
+ response = self._make_request(model=model, messages=messages, modalities=["image", "text"])
355
381
 
356
- # Debug: print response structure if verbose
357
- if self.verbose:
358
- self._log(f"Response keys: {response.keys()}")
359
- if "error" in response:
360
- self._log(f"API Error: {response['error']}")
361
- if "choices" in response and response["choices"]:
362
- msg = response["choices"][0].get("message", {})
363
- self._log(f"Message keys: {msg.keys()}")
364
- # Show content preview without printing huge base64 data
365
- content = msg.get("content", "")
366
- if isinstance(content, str):
367
- preview = content[:200] + "..." if len(content) > 200 else content
368
- self._log(f"Content preview: {preview}")
369
- elif isinstance(content, list):
370
- self._log(f"Content is list with {len(content)} items")
371
- for i, item in enumerate(content[:3]):
372
- if isinstance(item, dict):
373
- self._log(f" Item {i}: type={item.get('type')}")
374
-
375
- # Check for API errors in response
382
+ # Check for API errors in response body
376
383
  if "error" in response:
377
384
  error_msg = response["error"]
378
385
  if isinstance(error_msg, dict):
379
386
  error_msg = error_msg.get("message", str(error_msg))
380
- self._last_error = f"API Error: {error_msg}"
381
- print(f"✗ {self._last_error}")
387
+ print(f" {model}: API error — {error_msg}")
382
388
  return None
383
389
 
384
390
  image_data = self._extract_image_from_response(response)
385
391
  if image_data:
386
- self._log(f"✓ Generated image ({len(image_data)} bytes)")
387
- else:
388
- self._last_error = "No image data in API response - model may not support image generation"
389
- self._log(f"✗ {self._last_error}")
390
- # Additional debug info when image extraction fails
391
- if self.verbose and "choices" in response:
392
- msg = response["choices"][0].get("message", {})
393
- self._log(f"Full message structure: {json.dumps({k: type(v).__name__ for k, v in msg.items()})}")
394
-
395
- return image_data
392
+ self._log(f"✓ Generated image with {model} ({len(image_data)} bytes)")
393
+ return image_data
394
+
395
+ # Image extraction failed — print diagnostics
396
+ print(f" ✗ {model}: No image data in response")
397
+ self._diagnose_response(response, model)
398
+ return None
399
+
396
400
  except RuntimeError as e:
397
- self._last_error = str(e)
398
- self._log(f"✗ Generation failed: {self._last_error}")
401
+ print(f" ✗ {model}: {str(e)}")
399
402
  return None
400
403
  except Exception as e:
401
- self._last_error = f"Unexpected error: {str(e)}"
402
- self._log(f"✗ Generation failed: {self._last_error}")
403
- import traceback
404
-
404
+ print(f" ✗ {model}: Unexpected error {str(e)}")
405
405
  if self.verbose:
406
+ import traceback
406
407
  traceback.print_exc()
407
408
  return None
408
409
 
410
+ def generate_image(self, prompt: str) -> Optional[bytes]:
411
+ """
412
+ Generate an image using available image models with automatic fallback.
413
+
414
+ Tries each model in priority order. If the primary model fails or returns
415
+ no image data, falls back to the next model.
416
+
417
+ Args:
418
+ prompt: Description of the diagram to generate
419
+
420
+ Returns:
421
+ Image bytes or None if all models failed
422
+ """
423
+ self._last_error = None
424
+
425
+ messages = [{"role": "user", "content": prompt}]
426
+
427
+ for model in self.image_models:
428
+ self._log(f"Trying {model}...")
429
+ image_data = self._try_generate_with_model(model, messages)
430
+ if image_data:
431
+ self.image_model = model # Track which model succeeded
432
+ return image_data
433
+
434
+ self._last_error = f"All image models failed: {', '.join(self.image_models)}"
435
+ return None
436
+
409
437
  def review_image(
410
438
  self, image_path: str, original_prompt: str, iteration: int, doc_type: str = "default", max_iterations: int = 2
411
439
  ) -> Tuple[str, float, bool]:
@@ -625,6 +653,8 @@ Generate a publication-quality scientific diagram that meets all the guidelines
625
653
  print(f"Document Type: {doc_type}")
626
654
  print(f"Quality Threshold: {threshold}/10")
627
655
  print(f"Max Iterations: {iterations}")
656
+ print(f"Image Models: {' → '.join(self.image_models)}")
657
+ print(f"Review Model: {self.review_model}")
628
658
  print(f"Output: {output_path}")
629
659
  print(f"{'=' * 60}\n")
630
660
 
@@ -649,7 +679,7 @@ Generate a publication-quality scientific diagram that meets all the guidelines
649
679
  print(f"✓ Saved: {iter_path}")
650
680
 
651
681
  # Review image using Gemini 3 Pro
652
- print("Reviewing image with Gemini 3 Pro...")
682
+ print(f"Reviewing image with {self.review_model}...")
653
683
  critique, score, needs_improvement = self.review_image(str(iter_path), user_prompt, i, doc_type, iterations)
654
684
  print(f"✓ Score: {score}/10 (threshold: {threshold}/10)")
655
685
 
package/bin/synsc CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@synsci/cli-darwin-x64",
3
- "version": "1.1.89",
3
+ "version": "1.1.90",
4
4
  "os": [
5
5
  "darwin"
6
6
  ],