@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
|
|
85
|
-
Multiple passes only occur if the
|
|
86
|
-
quality threshold for the target
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
340
|
-
"""
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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=
|
|
380
|
+
response = self._make_request(model=model, messages=messages, modalities=["image", "text"])
|
|
355
381
|
|
|
356
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|