conectese 0.1.14
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.
- package/README.md +265 -0
- package/_conectese/.conectese-version +1 -0
- package/_conectese/config/playwright.config.json +11 -0
- package/_conectese/core/architect.agent.yaml +110 -0
- package/_conectese/core/best-practices/_catalog.yaml +116 -0
- package/_conectese/core/best-practices/blog-post.md +132 -0
- package/_conectese/core/best-practices/blog-seo.md +127 -0
- package/_conectese/core/best-practices/copywriting.md +426 -0
- package/_conectese/core/best-practices/data-analysis.md +401 -0
- package/_conectese/core/best-practices/email-newsletter.md +118 -0
- package/_conectese/core/best-practices/email-sales.md +110 -0
- package/_conectese/core/best-practices/image-design.md +348 -0
- package/_conectese/core/best-practices/instagram-feed.md +235 -0
- package/_conectese/core/best-practices/instagram-reels.md +112 -0
- package/_conectese/core/best-practices/instagram-stories.md +107 -0
- package/_conectese/core/best-practices/linkedin-article.md +116 -0
- package/_conectese/core/best-practices/linkedin-post.md +121 -0
- package/_conectese/core/best-practices/researching.md +349 -0
- package/_conectese/core/best-practices/review.md +269 -0
- package/_conectese/core/best-practices/social-networks-publishing.md +294 -0
- package/_conectese/core/best-practices/strategist.md +344 -0
- package/_conectese/core/best-practices/technical-writing.md +365 -0
- package/_conectese/core/best-practices/twitter-post.md +105 -0
- package/_conectese/core/best-practices/twitter-thread.md +122 -0
- package/_conectese/core/best-practices/whatsapp-broadcast.md +107 -0
- package/_conectese/core/best-practices/youtube-script.md +122 -0
- package/_conectese/core/best-practices/youtube-shorts.md +112 -0
- package/_conectese/core/prompts/build.prompt.md +547 -0
- package/_conectese/core/prompts/design.prompt.md +469 -0
- package/_conectese/core/prompts/discovery.prompt.md +269 -0
- package/_conectese/core/prompts/sherlock-instagram.md +123 -0
- package/_conectese/core/prompts/sherlock-linkedin.md +73 -0
- package/_conectese/core/prompts/sherlock-shared.md +684 -0
- package/_conectese/core/prompts/sherlock-twitter.md +78 -0
- package/_conectese/core/prompts/sherlock-youtube.md +85 -0
- package/_conectese/core/runner.pipeline.md +535 -0
- package/_conectese/core/skills.engine.md +381 -0
- package/agents/data-extractor/AGENT.md +13 -0
- package/agents/direito-adaneiro/AGENT.md +18 -0
- package/agents/direito-administrativo/AGENT.md +18 -0
- package/agents/direito-aeroporta-rio/AGENT.md +18 -0
- package/agents/direito-agra-rio/AGENT.md +18 -0
- package/agents/direito-ambiental/AGENT.md +18 -0
- package/agents/direito-banca-rio/AGENT.md +18 -0
- package/agents/direito-civil/AGENT.md +18 -0
- package/agents/direito-constitcional/AGENT.md +18 -0
- package/agents/direito-da-crianc-a-e-do-adolescente-eca/AGENT.md +18 -0
- package/agents/direito-da-propriedade-intelectal/AGENT.md +18 -0
- package/agents/direito-de-ami-lia/AGENT.md +18 -0
- package/agents/direito-de-tra-nsito/AGENT.md +18 -0
- package/agents/direito-desportivo/AGENT.md +18 -0
- package/agents/direito-digital/AGENT.md +18 -0
- package/agents/direito-do-consmidor/AGENT.md +18 -0
- package/agents/direito-do-trabalho/AGENT.md +18 -0
- package/agents/direito-econo-mico/AGENT.md +18 -0
- package/agents/direito-eleitoral/AGENT.md +18 -0
- package/agents/direito-empresarial/AGENT.md +18 -0
- package/agents/direito-imobilia-rio/AGENT.md +18 -0
- package/agents/direito-inanceiro/AGENT.md +18 -0
- package/agents/direito-internacional/AGENT.md +18 -0
- package/agents/direito-mari-timo/AGENT.md +18 -0
- package/agents/direito-me-dico-e-da-sa-de/AGENT.md +18 -0
- package/agents/direito-militar/AGENT.md +18 -0
- package/agents/direito-ndia-rio/AGENT.md +18 -0
- package/agents/direito-notarial-e-registral/AGENT.md +18 -0
- package/agents/direito-penal/AGENT.md +18 -0
- package/agents/direito-previdencia-rio/AGENT.md +18 -0
- package/agents/direito-processal-civil/AGENT.md +18 -0
- package/agents/direito-processal-do-trabalho/AGENT.md +18 -0
- package/agents/direito-processal-militar/AGENT.md +18 -0
- package/agents/direito-processal-penal/AGENT.md +18 -0
- package/agents/direito-rbani-stico/AGENT.md +18 -0
- package/agents/direito-secrita-rio/AGENT.md +18 -0
- package/agents/direito-sindical/AGENT.md +18 -0
- package/agents/direito-societa-rio/AGENT.md +18 -0
- package/agents/direito-tribta-rio/AGENT.md +18 -0
- package/agents/direitos-hmanos/AGENT.md +18 -0
- package/agents/legal-analyst/AGENT.md +16 -0
- package/agents/legal-synthesizer/AGENT.md +13 -0
- package/agents/lgpd-anonymizer/AGENT.md +14 -0
- package/agents/lgpd-restorer/AGENT.md +14 -0
- package/agents/task-router/AGENT.md +13 -0
- package/bin/conectese.js +73 -0
- package/dashboard/index.html +12 -0
- package/dashboard/package-lock.json +1971 -0
- package/dashboard/package.json +28 -0
- package/dashboard/public/assets/avatars/Female1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Female2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female4_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female5_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female5_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female5_wave.png +0 -0
- package/dashboard/public/assets/avatars/Female6_blink.png +0 -0
- package/dashboard/public/assets/avatars/Female6_talk.png +0 -0
- package/dashboard/public/assets/avatars/Female6_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male1_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male1_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male2_1wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_2wave.png +0 -0
- package/dashboard/public/assets/avatars/Male2_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male2_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male3_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male3_wave.png +0 -0
- package/dashboard/public/assets/avatars/Male4_blink.png +0 -0
- package/dashboard/public/assets/avatars/Male4_talk.png +0 -0
- package/dashboard/public/assets/avatars/Male4_wave.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_black_up.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding-1.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_down_coding.png +0 -0
- package/dashboard/public/assets/desks/desktop_set_white_up.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan.png +0 -0
- package/dashboard/public/assets/furniture/armchair_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/backpack_blue.png +0 -0
- package/dashboard/public/assets/furniture/backpack_red.png +0 -0
- package/dashboard/public/assets/furniture/blinds.png +0 -0
- package/dashboard/public/assets/furniture/blinds_large_closed_white.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf.png +0 -0
- package/dashboard/public/assets/furniture/bookshelf_purple_tall.png +0 -0
- package/dashboard/public/assets/furniture/bulletin_board.png +0 -0
- package/dashboard/public/assets/furniture/clock.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug.png +0 -0
- package/dashboard/public/assets/furniture/coffee_mug_blue.png +0 -0
- package/dashboard/public/assets/furniture/coffee_table.png +0 -0
- package/dashboard/public/assets/furniture/coffeepot_right.png +0 -0
- package/dashboard/public/assets/furniture/coffeetable_black_horizontal.png +0 -0
- package/dashboard/public/assets/furniture/couch.png +0 -0
- package/dashboard/public/assets/furniture/couch_tan_down.png +0 -0
- package/dashboard/public/assets/furniture/cushion_blue.png +0 -0
- package/dashboard/public/assets/furniture/cushion_tan.png +0 -0
- package/dashboard/public/assets/furniture/desk_wood.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug.png +0 -0
- package/dashboard/public/assets/furniture/fancy_rug_wide.png +0 -0
- package/dashboard/public/assets/furniture/flowers1.png +0 -0
- package/dashboard/public/assets/furniture/flowers2.png +0 -0
- package/dashboard/public/assets/furniture/lamp_tan.png +0 -0
- package/dashboard/public/assets/furniture/lantern.png +0 -0
- package/dashboard/public/assets/furniture/monstera.png +0 -0
- package/dashboard/public/assets/furniture/monstera_small.png +0 -0
- package/dashboard/public/assets/furniture/picture_frame.png +0 -0
- package/dashboard/public/assets/furniture/plant1.png +0 -0
- package/dashboard/public/assets/furniture/plant2.png +0 -0
- package/dashboard/public/assets/furniture/plant3.png +0 -0
- package/dashboard/public/assets/furniture/plant_poof.png +0 -0
- package/dashboard/public/assets/furniture/plant_spindly.png +0 -0
- package/dashboard/public/assets/furniture/poster_blue.png +0 -0
- package/dashboard/public/assets/furniture/rug.png +0 -0
- package/dashboard/public/assets/furniture/succulent_blue.png +0 -0
- package/dashboard/public/assets/furniture/succulent_green.png +0 -0
- package/dashboard/public/assets/furniture/treasurechest_closed_gold.png +0 -0
- package/dashboard/public/assets/furniture/water_cooler_better.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard.png +0 -0
- package/dashboard/public/assets/furniture/whiteboard_stand_graph.png +0 -0
- package/dashboard/public/assets/furniture/window_blinds_open.png +0 -0
- package/dashboard/src/App.tsx +46 -0
- package/dashboard/src/components/SquadCard.tsx +47 -0
- package/dashboard/src/components/SquadSelector.tsx +61 -0
- package/dashboard/src/components/StatusBadge.tsx +32 -0
- package/dashboard/src/components/StatusBar.tsx +97 -0
- package/dashboard/src/hooks/useSquadSocket.ts +135 -0
- package/dashboard/src/lib/formatTime.ts +16 -0
- package/dashboard/src/lib/normalizeState.ts +25 -0
- package/dashboard/src/main.tsx +10 -0
- package/dashboard/src/office/AgentSprite.ts +241 -0
- package/dashboard/src/office/OfficeScene.ts +153 -0
- package/dashboard/src/office/PhaserGame.tsx +80 -0
- package/dashboard/src/office/RoomBuilder.ts +190 -0
- package/dashboard/src/office/assetKeys.ts +150 -0
- package/dashboard/src/office/palette.ts +32 -0
- package/dashboard/src/plugin/squadWatcher.ts +233 -0
- package/dashboard/src/store/useSquadStore.ts +56 -0
- package/dashboard/src/styles/globals.css +36 -0
- package/dashboard/src/types/state.ts +63 -0
- package/dashboard/src/vite-env.d.ts +1 -0
- package/dashboard/test-results/.last-run.json +4 -0
- package/dashboard/tsconfig.json +24 -0
- package/dashboard/tsconfig.tsbuildinfo +1 -0
- package/dashboard/vite.config.ts +13 -0
- package/package.json +53 -0
- package/skills/README.md +63 -0
- package/skills/apify/SKILL.md +55 -0
- package/skills/blotato/SKILL.md +63 -0
- package/skills/canva/SKILL.md +60 -0
- package/skills/conectese-agent-creator/SKILL.md +192 -0
- package/skills/conectese-skill-creator/SKILL.md +407 -0
- package/skills/conectese-skill-creator/agents/analyzer.md +274 -0
- package/skills/conectese-skill-creator/agents/comparator.md +202 -0
- package/skills/conectese-skill-creator/agents/grader.md +223 -0
- package/skills/conectese-skill-creator/assets/eval_review.html +146 -0
- package/skills/conectese-skill-creator/eval-viewer/generate_review.py +471 -0
- package/skills/conectese-skill-creator/eval-viewer/viewer.html +1325 -0
- package/skills/conectese-skill-creator/references/schemas.md +430 -0
- package/skills/conectese-skill-creator/references/skill-format.md +235 -0
- package/skills/conectese-skill-creator/scripts/__init__.py +0 -0
- package/skills/conectese-skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/skills/conectese-skill-creator/scripts/quick_validate.py +103 -0
- package/skills/conectese-skill-creator/scripts/run_eval.py +310 -0
- package/skills/conectese-skill-creator/scripts/utils.py +47 -0
- package/skills/image-ai-generator/SKILL.md +124 -0
- package/skills/image-ai-generator/scripts/generate.py +175 -0
- package/skills/image-creator/SKILL.md +155 -0
- package/skills/image-fetcher/SKILL.md +91 -0
- package/skills/instagram-publisher/SKILL.md +119 -0
- package/skills/instagram-publisher/scripts/publish.js +165 -0
- package/skills/resend/SKILL.md +80 -0
- package/skills/template-designer/SKILL.md +201 -0
- package/skills/template-designer/base-templates/model-a.html +27 -0
- package/skills/template-designer/base-templates/model-b.html +31 -0
- package/skills/template-designer/base-templates/model-c.html +42 -0
- package/src/agents-cli.js +158 -0
- package/src/agents.js +134 -0
- package/src/i18n.js +48 -0
- package/src/init.js +341 -0
- package/src/locales/en.json +73 -0
- package/src/locales/es.json +72 -0
- package/src/locales/pt-BR.json +72 -0
- package/src/logger.js +38 -0
- package/src/prompt.js +46 -0
- package/src/readme/README.md +119 -0
- package/src/runs.js +90 -0
- package/src/skills-cli.js +157 -0
- package/src/skills.js +146 -0
- package/src/update.js +169 -0
- package/templates/_conectese/.conectese-version +1 -0
- package/templates/_conectese/_investigations/.gitkeep +0 -0
- package/templates/ide-templates/antigravity/.agent/rules/conectese.md +55 -0
- package/templates/ide-templates/antigravity/.agent/workflows/conectese.md +102 -0
- package/templates/ide-templates/claude-code/.claude/skills/conectese/SKILL.md +182 -0
- package/templates/ide-templates/claude-code/.mcp.json +8 -0
- package/templates/ide-templates/claude-code/CLAUDE.md +43 -0
- package/templates/ide-templates/codex/.agents/skills/conectese/SKILL.md +6 -0
- package/templates/ide-templates/codex/AGENTS.md +105 -0
- package/templates/ide-templates/cursor/.cursor/commands/conectese.md +9 -0
- package/templates/ide-templates/cursor/.cursor/mcp.json +8 -0
- package/templates/ide-templates/cursor/.cursor/rules/conectese.mdc +48 -0
- package/templates/ide-templates/cursor/.cursorignore +3 -0
- package/templates/ide-templates/opencode/.opencode/commands/conectese.md +9 -0
- package/templates/ide-templates/opencode/AGENTS.md +105 -0
- package/templates/ide-templates/vscode-copilot/.github/prompts/conectese.prompt.md +201 -0
- package/templates/ide-templates/vscode-copilot/.vscode/mcp.json +8 -0
- package/templates/ide-templates/vscode-copilot/.vscode/settings.json +3 -0
- package/templates/package.json +8 -0
- package/templates/squads/.gitkeep +0 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run trigger evaluation for a skill description.
|
|
3
|
+
|
|
4
|
+
Tests whether a skill's description causes Claude to trigger (read the skill)
|
|
5
|
+
for a set of queries. Outputs results as JSON.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import select
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
import uuid
|
|
16
|
+
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from scripts.utils import parse_skill_md
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_project_root() -> Path:
|
|
23
|
+
"""Find the project root by walking up from cwd looking for .claude/.
|
|
24
|
+
|
|
25
|
+
Mimics how Claude Code discovers its project root, so the command file
|
|
26
|
+
we create ends up where claude -p will look for it.
|
|
27
|
+
"""
|
|
28
|
+
current = Path.cwd()
|
|
29
|
+
for parent in [current, *current.parents]:
|
|
30
|
+
if (parent / ".claude").is_dir():
|
|
31
|
+
return parent
|
|
32
|
+
return current
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_single_query(
|
|
36
|
+
query: str,
|
|
37
|
+
skill_name: str,
|
|
38
|
+
skill_description: str,
|
|
39
|
+
timeout: int,
|
|
40
|
+
project_root: str,
|
|
41
|
+
model: str | None = None,
|
|
42
|
+
) -> bool:
|
|
43
|
+
"""Run a single query and return whether the skill was triggered.
|
|
44
|
+
|
|
45
|
+
Creates a command file in .claude/commands/ so it appears in Claude's
|
|
46
|
+
available_skills list, then runs `claude -p` with the raw query.
|
|
47
|
+
Uses --include-partial-messages to detect triggering early from
|
|
48
|
+
stream events (content_block_start) rather than waiting for the
|
|
49
|
+
full assistant message, which only arrives after tool execution.
|
|
50
|
+
"""
|
|
51
|
+
unique_id = uuid.uuid4().hex[:8]
|
|
52
|
+
clean_name = f"{skill_name}-skill-{unique_id}"
|
|
53
|
+
project_commands_dir = Path(project_root) / ".claude" / "commands"
|
|
54
|
+
command_file = project_commands_dir / f"{clean_name}.md"
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
project_commands_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
# Use YAML block scalar to avoid breaking on quotes in description
|
|
59
|
+
indented_desc = "\n ".join(skill_description.split("\n"))
|
|
60
|
+
command_content = (
|
|
61
|
+
f"---\n"
|
|
62
|
+
f"description: |\n"
|
|
63
|
+
f" {indented_desc}\n"
|
|
64
|
+
f"---\n\n"
|
|
65
|
+
f"# {skill_name}\n\n"
|
|
66
|
+
f"This skill handles: {skill_description}\n"
|
|
67
|
+
)
|
|
68
|
+
command_file.write_text(command_content)
|
|
69
|
+
|
|
70
|
+
cmd = [
|
|
71
|
+
"claude",
|
|
72
|
+
"-p", query,
|
|
73
|
+
"--output-format", "stream-json",
|
|
74
|
+
"--verbose",
|
|
75
|
+
"--include-partial-messages",
|
|
76
|
+
]
|
|
77
|
+
if model:
|
|
78
|
+
cmd.extend(["--model", model])
|
|
79
|
+
|
|
80
|
+
# Remove CLAUDECODE env var to allow nesting claude -p inside a
|
|
81
|
+
# Claude Code session. The guard is for interactive terminal conflicts;
|
|
82
|
+
# programmatic subprocess usage is safe.
|
|
83
|
+
env = {k: v for k, v in os.environ.items() if k != "CLAUDECODE"}
|
|
84
|
+
|
|
85
|
+
process = subprocess.Popen(
|
|
86
|
+
cmd,
|
|
87
|
+
stdout=subprocess.PIPE,
|
|
88
|
+
stderr=subprocess.DEVNULL,
|
|
89
|
+
cwd=project_root,
|
|
90
|
+
env=env,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
triggered = False
|
|
94
|
+
start_time = time.time()
|
|
95
|
+
buffer = ""
|
|
96
|
+
# Track state for stream event detection
|
|
97
|
+
pending_tool_name = None
|
|
98
|
+
accumulated_json = ""
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
while time.time() - start_time < timeout:
|
|
102
|
+
if process.poll() is not None:
|
|
103
|
+
remaining = process.stdout.read()
|
|
104
|
+
if remaining:
|
|
105
|
+
buffer += remaining.decode("utf-8", errors="replace")
|
|
106
|
+
break
|
|
107
|
+
|
|
108
|
+
ready, _, _ = select.select([process.stdout], [], [], 1.0)
|
|
109
|
+
if not ready:
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
chunk = os.read(process.stdout.fileno(), 8192)
|
|
113
|
+
if not chunk:
|
|
114
|
+
break
|
|
115
|
+
buffer += chunk.decode("utf-8", errors="replace")
|
|
116
|
+
|
|
117
|
+
while "\n" in buffer:
|
|
118
|
+
line, buffer = buffer.split("\n", 1)
|
|
119
|
+
line = line.strip()
|
|
120
|
+
if not line:
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
event = json.loads(line)
|
|
125
|
+
except json.JSONDecodeError:
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Early detection via stream events
|
|
129
|
+
if event.get("type") == "stream_event":
|
|
130
|
+
se = event.get("event", {})
|
|
131
|
+
se_type = se.get("type", "")
|
|
132
|
+
|
|
133
|
+
if se_type == "content_block_start":
|
|
134
|
+
cb = se.get("content_block", {})
|
|
135
|
+
if cb.get("type") == "tool_use":
|
|
136
|
+
tool_name = cb.get("name", "")
|
|
137
|
+
if tool_name in ("Skill", "Read"):
|
|
138
|
+
pending_tool_name = tool_name
|
|
139
|
+
accumulated_json = ""
|
|
140
|
+
else:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
elif se_type == "content_block_delta" and pending_tool_name:
|
|
144
|
+
delta = se.get("delta", {})
|
|
145
|
+
if delta.get("type") == "input_json_delta":
|
|
146
|
+
accumulated_json += delta.get("partial_json", "")
|
|
147
|
+
if clean_name in accumulated_json:
|
|
148
|
+
return True
|
|
149
|
+
|
|
150
|
+
elif se_type in ("content_block_stop", "message_stop"):
|
|
151
|
+
if pending_tool_name:
|
|
152
|
+
return clean_name in accumulated_json
|
|
153
|
+
if se_type == "message_stop":
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
# Fallback: full assistant message
|
|
157
|
+
elif event.get("type") == "assistant":
|
|
158
|
+
message = event.get("message", {})
|
|
159
|
+
for content_item in message.get("content", []):
|
|
160
|
+
if content_item.get("type") != "tool_use":
|
|
161
|
+
continue
|
|
162
|
+
tool_name = content_item.get("name", "")
|
|
163
|
+
tool_input = content_item.get("input", {})
|
|
164
|
+
if tool_name == "Skill" and clean_name in tool_input.get("skill", ""):
|
|
165
|
+
triggered = True
|
|
166
|
+
elif tool_name == "Read" and clean_name in tool_input.get("file_path", ""):
|
|
167
|
+
triggered = True
|
|
168
|
+
return triggered
|
|
169
|
+
|
|
170
|
+
elif event.get("type") == "result":
|
|
171
|
+
return triggered
|
|
172
|
+
finally:
|
|
173
|
+
# Clean up process on any exit path (return, exception, timeout)
|
|
174
|
+
if process.poll() is None:
|
|
175
|
+
process.kill()
|
|
176
|
+
process.wait()
|
|
177
|
+
|
|
178
|
+
return triggered
|
|
179
|
+
finally:
|
|
180
|
+
if command_file.exists():
|
|
181
|
+
command_file.unlink()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def run_eval(
|
|
185
|
+
eval_set: list[dict],
|
|
186
|
+
skill_name: str,
|
|
187
|
+
description: str,
|
|
188
|
+
num_workers: int,
|
|
189
|
+
timeout: int,
|
|
190
|
+
project_root: Path,
|
|
191
|
+
runs_per_query: int = 1,
|
|
192
|
+
trigger_threshold: float = 0.5,
|
|
193
|
+
model: str | None = None,
|
|
194
|
+
) -> dict:
|
|
195
|
+
"""Run the full eval set and return results."""
|
|
196
|
+
results = []
|
|
197
|
+
|
|
198
|
+
with ProcessPoolExecutor(max_workers=num_workers) as executor:
|
|
199
|
+
future_to_info = {}
|
|
200
|
+
for item in eval_set:
|
|
201
|
+
for run_idx in range(runs_per_query):
|
|
202
|
+
future = executor.submit(
|
|
203
|
+
run_single_query,
|
|
204
|
+
item["query"],
|
|
205
|
+
skill_name,
|
|
206
|
+
description,
|
|
207
|
+
timeout,
|
|
208
|
+
str(project_root),
|
|
209
|
+
model,
|
|
210
|
+
)
|
|
211
|
+
future_to_info[future] = (item, run_idx)
|
|
212
|
+
|
|
213
|
+
query_triggers: dict[str, list[bool]] = {}
|
|
214
|
+
query_items: dict[str, dict] = {}
|
|
215
|
+
for future in as_completed(future_to_info):
|
|
216
|
+
item, _ = future_to_info[future]
|
|
217
|
+
query = item["query"]
|
|
218
|
+
query_items[query] = item
|
|
219
|
+
if query not in query_triggers:
|
|
220
|
+
query_triggers[query] = []
|
|
221
|
+
try:
|
|
222
|
+
query_triggers[query].append(future.result())
|
|
223
|
+
except Exception as e:
|
|
224
|
+
print(f"Warning: query failed: {e}", file=sys.stderr)
|
|
225
|
+
query_triggers[query].append(False)
|
|
226
|
+
|
|
227
|
+
for query, triggers in query_triggers.items():
|
|
228
|
+
item = query_items[query]
|
|
229
|
+
trigger_rate = sum(triggers) / len(triggers)
|
|
230
|
+
should_trigger = item["should_trigger"]
|
|
231
|
+
if should_trigger:
|
|
232
|
+
did_pass = trigger_rate >= trigger_threshold
|
|
233
|
+
else:
|
|
234
|
+
did_pass = trigger_rate < trigger_threshold
|
|
235
|
+
results.append({
|
|
236
|
+
"query": query,
|
|
237
|
+
"should_trigger": should_trigger,
|
|
238
|
+
"trigger_rate": trigger_rate,
|
|
239
|
+
"triggers": sum(triggers),
|
|
240
|
+
"runs": len(triggers),
|
|
241
|
+
"pass": did_pass,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
passed = sum(1 for r in results if r["pass"])
|
|
245
|
+
total = len(results)
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
"skill_name": skill_name,
|
|
249
|
+
"description": description,
|
|
250
|
+
"results": results,
|
|
251
|
+
"summary": {
|
|
252
|
+
"total": total,
|
|
253
|
+
"passed": passed,
|
|
254
|
+
"failed": total - passed,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main():
|
|
260
|
+
parser = argparse.ArgumentParser(description="Run trigger evaluation for a skill description")
|
|
261
|
+
parser.add_argument("--eval-set", required=True, help="Path to eval set JSON file")
|
|
262
|
+
parser.add_argument("--skill-path", required=True, help="Path to skill directory")
|
|
263
|
+
parser.add_argument("--description", default=None, help="Override description to test")
|
|
264
|
+
parser.add_argument("--num-workers", type=int, default=10, help="Number of parallel workers")
|
|
265
|
+
parser.add_argument("--timeout", type=int, default=30, help="Timeout per query in seconds")
|
|
266
|
+
parser.add_argument("--runs-per-query", type=int, default=3, help="Number of runs per query")
|
|
267
|
+
parser.add_argument("--trigger-threshold", type=float, default=0.5, help="Trigger rate threshold")
|
|
268
|
+
parser.add_argument("--model", default=None, help="Model to use for claude -p (default: user's configured model)")
|
|
269
|
+
parser.add_argument("--verbose", action="store_true", help="Print progress to stderr")
|
|
270
|
+
args = parser.parse_args()
|
|
271
|
+
|
|
272
|
+
eval_set = json.loads(Path(args.eval_set).read_text())
|
|
273
|
+
skill_path = Path(args.skill_path)
|
|
274
|
+
|
|
275
|
+
if not (skill_path / "SKILL.md").exists():
|
|
276
|
+
print(f"Error: No SKILL.md found at {skill_path}", file=sys.stderr)
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
|
|
279
|
+
name, original_description, content = parse_skill_md(skill_path)
|
|
280
|
+
description = args.description or original_description
|
|
281
|
+
project_root = find_project_root()
|
|
282
|
+
|
|
283
|
+
if args.verbose:
|
|
284
|
+
print(f"Evaluating: {description}", file=sys.stderr)
|
|
285
|
+
|
|
286
|
+
output = run_eval(
|
|
287
|
+
eval_set=eval_set,
|
|
288
|
+
skill_name=name,
|
|
289
|
+
description=description,
|
|
290
|
+
num_workers=args.num_workers,
|
|
291
|
+
timeout=args.timeout,
|
|
292
|
+
project_root=project_root,
|
|
293
|
+
runs_per_query=args.runs_per_query,
|
|
294
|
+
trigger_threshold=args.trigger_threshold,
|
|
295
|
+
model=args.model,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if args.verbose:
|
|
299
|
+
summary = output["summary"]
|
|
300
|
+
print(f"Results: {summary['passed']}/{summary['total']} passed", file=sys.stderr)
|
|
301
|
+
for r in output["results"]:
|
|
302
|
+
status = "PASS" if r["pass"] else "FAIL"
|
|
303
|
+
rate_str = f"{r['triggers']}/{r['runs']}"
|
|
304
|
+
print(f" [{status}] rate={rate_str} expected={r['should_trigger']}: {r['query'][:70]}", file=sys.stderr)
|
|
305
|
+
|
|
306
|
+
print(json.dumps(output, indent=2))
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
if __name__ == "__main__":
|
|
310
|
+
main()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Shared utilities for skill-creator scripts."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def parse_skill_md(skill_path: Path) -> tuple[str, str, str]:
|
|
8
|
+
"""Parse a SKILL.md file, returning (name, description, full_content)."""
|
|
9
|
+
content = (skill_path / "SKILL.md").read_text()
|
|
10
|
+
lines = content.split("\n")
|
|
11
|
+
|
|
12
|
+
if lines[0].strip() != "---":
|
|
13
|
+
raise ValueError("SKILL.md missing frontmatter (no opening ---)")
|
|
14
|
+
|
|
15
|
+
end_idx = None
|
|
16
|
+
for i, line in enumerate(lines[1:], start=1):
|
|
17
|
+
if line.strip() == "---":
|
|
18
|
+
end_idx = i
|
|
19
|
+
break
|
|
20
|
+
|
|
21
|
+
if end_idx is None:
|
|
22
|
+
raise ValueError("SKILL.md missing frontmatter (no closing ---)")
|
|
23
|
+
|
|
24
|
+
name = ""
|
|
25
|
+
description = ""
|
|
26
|
+
frontmatter_lines = lines[1:end_idx]
|
|
27
|
+
i = 0
|
|
28
|
+
while i < len(frontmatter_lines):
|
|
29
|
+
line = frontmatter_lines[i]
|
|
30
|
+
if line.startswith("name:"):
|
|
31
|
+
name = line[len("name:"):].strip().strip('"').strip("'")
|
|
32
|
+
elif line.startswith("description:"):
|
|
33
|
+
value = line[len("description:"):].strip()
|
|
34
|
+
# Handle YAML multiline indicators (>, |, >-, |-)
|
|
35
|
+
if value in (">", "|", ">-", "|-"):
|
|
36
|
+
continuation_lines: list[str] = []
|
|
37
|
+
i += 1
|
|
38
|
+
while i < len(frontmatter_lines) and (frontmatter_lines[i].startswith(" ") or frontmatter_lines[i].startswith("\t")):
|
|
39
|
+
continuation_lines.append(frontmatter_lines[i].strip())
|
|
40
|
+
i += 1
|
|
41
|
+
description = " ".join(continuation_lines)
|
|
42
|
+
continue
|
|
43
|
+
else:
|
|
44
|
+
description = value.strip('"').strip("'")
|
|
45
|
+
i += 1
|
|
46
|
+
|
|
47
|
+
return name, description, content
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image-ai-generator
|
|
3
|
+
description: >
|
|
4
|
+
Generates images via Openrouter API using AI image models.
|
|
5
|
+
Supports two modes: test (cheap model for iteration) and production (high-quality model for final output).
|
|
6
|
+
Handles prompt construction, API calls, base64 decoding, and file saving.
|
|
7
|
+
Supports reference images (logos, mascots) for brand-consistent generation.
|
|
8
|
+
description_pt-BR: >
|
|
9
|
+
Gera imagens via API do Openrouter usando modelos de IA.
|
|
10
|
+
Suporta dois modos: test (modelo barato para iteração) e production (modelo de alta qualidade para output final).
|
|
11
|
+
Cuida da construção de prompts, chamadas de API, decodificação base64 e salvamento de arquivos.
|
|
12
|
+
Suporta imagens de referência (logos, mascotes) para geração consistente com a marca.
|
|
13
|
+
type: script
|
|
14
|
+
version: "1.0.0"
|
|
15
|
+
script:
|
|
16
|
+
path: scripts/generate.py
|
|
17
|
+
runtime: python3
|
|
18
|
+
invoke: "python3 {skill_path}/scripts/generate.py --prompt \"{prompt}\" --output \"{output}\" --mode \"{mode}\""
|
|
19
|
+
env:
|
|
20
|
+
- OPENROUTER_API_KEY
|
|
21
|
+
categories: [assets, images, ai, generation]
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# Image Generator
|
|
25
|
+
|
|
26
|
+
## When to use
|
|
27
|
+
|
|
28
|
+
Use the Image Generator when you need to create visual assets from text prompts. This skill calls the Openrouter API with AI image generation models and saves the resulting images locally.
|
|
29
|
+
|
|
30
|
+
**IMPORTANT: Think twice before generating images.** Image generation costs money and takes time. Before generating:
|
|
31
|
+
1. Check if a suitable image already exists in the squad's assets folder
|
|
32
|
+
2. Check if a web search could find a free/open image that works
|
|
33
|
+
3. Consider if the image is truly necessary for the content quality
|
|
34
|
+
4. Only generate when no existing alternative is good enough
|
|
35
|
+
5. **Generate only what you need** — never batch-generate "test variations". One image is enough to validate a concept.
|
|
36
|
+
|
|
37
|
+
## Modes
|
|
38
|
+
|
|
39
|
+
### Test mode (`--mode test`)
|
|
40
|
+
- **Model:** `sourceful/riverflow-v2-fast`
|
|
41
|
+
- **When to use:** During iteration, testing layouts, checking composition, reviewing concepts
|
|
42
|
+
- **Cost:** ~R$0.01-0.02 per image (very low)
|
|
43
|
+
- **Quality:** Good enough for layout validation, not for final output
|
|
44
|
+
|
|
45
|
+
### Production mode (`--mode production`)
|
|
46
|
+
- **Model:** `google/gemini-3.1-flash-image-preview`
|
|
47
|
+
- **When to use:** Only when generating the final images that will be published or delivered
|
|
48
|
+
- **Cost:** ~R$0.07-0.10 per image
|
|
49
|
+
- **Quality:** High quality, suitable for social media and publishing
|
|
50
|
+
|
|
51
|
+
**Default mode is `test`.** Only switch to `production` when the user has approved the layout/composition and you are generating the final deliverable images.
|
|
52
|
+
|
|
53
|
+
## Instructions
|
|
54
|
+
|
|
55
|
+
### Single image generation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
python3 skills/image-generator/scripts/generate.py \
|
|
59
|
+
--prompt "A detailed description of the image to generate" \
|
|
60
|
+
--output "squads/{squad}/output/{run_id}/assets/image-name.jpg" \
|
|
61
|
+
--mode test
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### With a reference image (logo, mascot, brand asset)
|
|
65
|
+
|
|
66
|
+
Use `--reference` to send a local image to the model as visual context. The model will incorporate the referenced image (e.g., a logo or mascot) into the generated output.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
python3 skills/image-generator/scripts/generate.py \
|
|
70
|
+
--prompt "A social media banner featuring the company logo prominently in the center" \
|
|
71
|
+
--output "squads/{squad}/output/{run_id}/assets/banner.jpg" \
|
|
72
|
+
--reference "squads/{squad}/assets/logo.png" \
|
|
73
|
+
--mode production
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Supported reference formats: PNG, JPEG, WEBP, GIF.
|
|
77
|
+
|
|
78
|
+
### Batch generation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python3 skills/image-generator/scripts/generate.py \
|
|
82
|
+
--batch "squads/{squad}/output/{run_id}/assets/batch.json" \
|
|
83
|
+
--mode production
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The batch JSON file should contain:
|
|
87
|
+
```json
|
|
88
|
+
[
|
|
89
|
+
{"prompt": "Description of image 1", "output": "path/to/image1.jpg"},
|
|
90
|
+
{"prompt": "Description of image 2", "output": "path/to/image2.jpg"}
|
|
91
|
+
]
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Each item can optionally include a `"reference": "path/to/ref.png"` field.
|
|
95
|
+
|
|
96
|
+
### Prompt guidelines
|
|
97
|
+
|
|
98
|
+
- Be specific about composition, lighting, style, and mood
|
|
99
|
+
- Specify aspect ratio or orientation when relevant (e.g., "portrait 3:4", "landscape 16:9")
|
|
100
|
+
- Include "hyper realistic, 4K quality" for photographic styles
|
|
101
|
+
- Include "clean composition" to avoid cluttered outputs
|
|
102
|
+
- Avoid requesting text in images — AI models struggle with text rendering
|
|
103
|
+
|
|
104
|
+
### Cost awareness
|
|
105
|
+
|
|
106
|
+
- Each production image costs approximately R$0.07-0.10
|
|
107
|
+
- Each test image costs approximately R$0.01-0.02
|
|
108
|
+
- A typical carousel with 8 images costs ~R$0.60-0.80 in production mode
|
|
109
|
+
- **Always use test mode first**, then regenerate only the approved concepts in production mode
|
|
110
|
+
- When testing, generate **1 image only** — not 3, not 5, just 1
|
|
111
|
+
|
|
112
|
+
## Available operations
|
|
113
|
+
|
|
114
|
+
- **Single generation** — Generate one image from a text prompt
|
|
115
|
+
- **Batch generation** — Generate multiple images from a JSON batch file
|
|
116
|
+
- **Mode selection** — Choose between test (cheap) and production (high-quality) models
|
|
117
|
+
- **Reference image** — Send a logo/mascot/brand asset as visual context for the generation
|
|
118
|
+
|
|
119
|
+
## Error handling
|
|
120
|
+
|
|
121
|
+
- If `OPENROUTER_API_KEY` is not set, the script exits with an error message. Set it in your `.env` file or environment.
|
|
122
|
+
- If the API returns an error, the script prints the error code and body, then exits with code 1.
|
|
123
|
+
- If no image is found in the API response, the script reports which model was used and exits with code 1.
|
|
124
|
+
- For batch mode, partial failures are reported with a success count summary.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Image Generator — Conectese Skill
|
|
4
|
+
Generates images via Openrouter API using AI image models.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
# Single image
|
|
8
|
+
python3 generate.py --prompt "description" --output "path/to/image.jpg" --mode test
|
|
9
|
+
|
|
10
|
+
# Single image with reference (logo/mascot)
|
|
11
|
+
python3 generate.py --prompt "description" --output "path/to/image.jpg" --reference "path/to/logo.png" --mode production
|
|
12
|
+
|
|
13
|
+
# Batch (JSON file with list of {prompt, output} objects)
|
|
14
|
+
python3 generate.py --batch "path/to/batch.json" --mode production
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import base64
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
import urllib.request
|
|
24
|
+
import urllib.error
|
|
25
|
+
|
|
26
|
+
# Model configuration per mode
|
|
27
|
+
MODELS = {
|
|
28
|
+
"test": "sourceful/riverflow-v2-fast",
|
|
29
|
+
"production": "google/gemini-3.1-flash-image-preview",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
API_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def load_api_key():
|
|
36
|
+
"""Load OPENROUTER_API_KEY from environment."""
|
|
37
|
+
key = os.environ.get("OPENROUTER_API_KEY")
|
|
38
|
+
if not key:
|
|
39
|
+
# Try loading from .env in project root
|
|
40
|
+
env_candidates = [
|
|
41
|
+
os.path.join(os.getcwd(), ".env"),
|
|
42
|
+
os.path.join(os.path.dirname(__file__), "..", "..", "..", ".env"),
|
|
43
|
+
]
|
|
44
|
+
for env_path in env_candidates:
|
|
45
|
+
env_path = os.path.abspath(env_path)
|
|
46
|
+
if os.path.exists(env_path):
|
|
47
|
+
with open(env_path, "r") as f:
|
|
48
|
+
for line in f:
|
|
49
|
+
line = line.strip()
|
|
50
|
+
if line.startswith("OPENROUTER_API_KEY=") and not line.startswith("#"):
|
|
51
|
+
key = line.split("=", 1)[1].strip().strip('"').strip("'")
|
|
52
|
+
break
|
|
53
|
+
if key:
|
|
54
|
+
break
|
|
55
|
+
if not key:
|
|
56
|
+
print("ERROR: OPENROUTER_API_KEY not found in environment or .env file", file=sys.stderr)
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
return key
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def generate_image(prompt, output_path, mode, api_key, reference_image=None):
|
|
62
|
+
"""Generate a single image and save to output_path."""
|
|
63
|
+
model = MODELS.get(mode, MODELS["test"])
|
|
64
|
+
|
|
65
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
66
|
+
|
|
67
|
+
if reference_image and os.path.exists(reference_image):
|
|
68
|
+
# Multimodal: send reference image + text prompt
|
|
69
|
+
ext = os.path.splitext(reference_image)[1].lower()
|
|
70
|
+
mime_map = {".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif"}
|
|
71
|
+
mime = mime_map.get(ext, "image/png")
|
|
72
|
+
with open(reference_image, "rb") as img_f:
|
|
73
|
+
img_b64 = base64.b64encode(img_f.read()).decode("utf-8")
|
|
74
|
+
content = [
|
|
75
|
+
{"type": "image_url", "image_url": {"url": f"data:{mime};base64,{img_b64}"}},
|
|
76
|
+
{"type": "text", "text": f"Generate an image using the logo/mascot shown in the reference image above. {prompt}. Only output the image, no text."}
|
|
77
|
+
]
|
|
78
|
+
else:
|
|
79
|
+
content = f"Generate an image: {prompt}. Only output the image, no text."
|
|
80
|
+
|
|
81
|
+
payload = json.dumps({
|
|
82
|
+
"model": model,
|
|
83
|
+
"messages": [{
|
|
84
|
+
"role": "user",
|
|
85
|
+
"content": content
|
|
86
|
+
}]
|
|
87
|
+
}).encode("utf-8")
|
|
88
|
+
|
|
89
|
+
req = urllib.request.Request(
|
|
90
|
+
API_URL,
|
|
91
|
+
data=payload,
|
|
92
|
+
headers={
|
|
93
|
+
"Authorization": f"Bearer {api_key}",
|
|
94
|
+
"Content-Type": "application/json",
|
|
95
|
+
},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
100
|
+
data = json.loads(resp.read().decode("utf-8"))
|
|
101
|
+
except urllib.error.HTTPError as e:
|
|
102
|
+
error_body = e.read().decode("utf-8", errors="replace")
|
|
103
|
+
print(f" API error [{e.code}]: {error_body[:200]}", file=sys.stderr)
|
|
104
|
+
return False
|
|
105
|
+
except Exception as e:
|
|
106
|
+
print(f" Request error: {e}", file=sys.stderr)
|
|
107
|
+
return False
|
|
108
|
+
|
|
109
|
+
images = data.get("choices", [{}])[0].get("message", {}).get("images", [])
|
|
110
|
+
if not images:
|
|
111
|
+
# Some models return image in content as base64
|
|
112
|
+
content_resp = data.get("choices", [{}])[0].get("message", {}).get("content", "")
|
|
113
|
+
if content_resp and isinstance(content_resp, str) and content_resp.startswith("data:image"):
|
|
114
|
+
img_data = content_resp.split(",", 1)[1] if "," in content_resp else content_resp
|
|
115
|
+
else:
|
|
116
|
+
print(f" No image returned by model {model}", file=sys.stderr)
|
|
117
|
+
return False
|
|
118
|
+
else:
|
|
119
|
+
img_data = images[0].get("image_url", {}).get("url", "")
|
|
120
|
+
if img_data.startswith("data:"):
|
|
121
|
+
img_data = img_data.split(",", 1)[1]
|
|
122
|
+
|
|
123
|
+
with open(output_path, "wb") as f:
|
|
124
|
+
f.write(base64.b64decode(img_data))
|
|
125
|
+
|
|
126
|
+
size_kb = os.path.getsize(output_path) / 1024
|
|
127
|
+
print(f" OK: {output_path} ({size_kb:.0f} KB)")
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def main():
|
|
132
|
+
parser = argparse.ArgumentParser(description="Generate images via Openrouter API")
|
|
133
|
+
parser.add_argument("--prompt", help="Text prompt for single image generation")
|
|
134
|
+
parser.add_argument("--output", help="Output file path for single image")
|
|
135
|
+
parser.add_argument("--batch", help="Path to JSON batch file")
|
|
136
|
+
parser.add_argument("--mode", choices=["test", "production"], default="test",
|
|
137
|
+
help="Generation mode: test (cheap) or production (high-quality)")
|
|
138
|
+
parser.add_argument("--reference", help="Path to reference image to include in the prompt")
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
|
|
141
|
+
if not args.prompt and not args.batch:
|
|
142
|
+
parser.error("Either --prompt or --batch is required")
|
|
143
|
+
|
|
144
|
+
api_key = load_api_key()
|
|
145
|
+
model = MODELS[args.mode]
|
|
146
|
+
print(f"Image Generator — Mode: {args.mode} | Model: {model}")
|
|
147
|
+
|
|
148
|
+
if args.batch:
|
|
149
|
+
# Batch mode
|
|
150
|
+
with open(args.batch, "r") as f:
|
|
151
|
+
items = json.load(f)
|
|
152
|
+
print(f"Generating {len(items)} images...\n")
|
|
153
|
+
success = 0
|
|
154
|
+
for i, item in enumerate(items, 1):
|
|
155
|
+
prompt = item["prompt"]
|
|
156
|
+
output = item["output"]
|
|
157
|
+
ref = item.get("reference")
|
|
158
|
+
print(f"[{i}/{len(items)}] {os.path.basename(output)}...")
|
|
159
|
+
if generate_image(prompt, output, args.mode, api_key, reference_image=ref):
|
|
160
|
+
success += 1
|
|
161
|
+
if i < len(items):
|
|
162
|
+
time.sleep(1) # Rate limiting
|
|
163
|
+
print(f"\nDone: {success}/{len(items)} images generated.")
|
|
164
|
+
sys.exit(0 if success == len(items) else 1)
|
|
165
|
+
else:
|
|
166
|
+
# Single mode
|
|
167
|
+
if not args.output:
|
|
168
|
+
parser.error("--output is required for single image generation")
|
|
169
|
+
print(f"Generating: {os.path.basename(args.output)}...")
|
|
170
|
+
ok = generate_image(args.prompt, args.output, args.mode, api_key, reference_image=args.reference)
|
|
171
|
+
sys.exit(0 if ok else 1)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|