Update memory: Camie Tagger v2 upgrade results
This commit is contained in:
7
skills/cheapest-image-generation/.clawhub/origin.json
Normal file
7
skills/cheapest-image-generation/.clawhub/origin.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"registry": "https://clawhub.ai",
|
||||
"slug": "cheapest-image-generation",
|
||||
"installedVersion": "1.0.2",
|
||||
"installedAt": 1774541492833
|
||||
}
|
||||
77
skills/cheapest-image-generation/SKILL.md
Normal file
77
skills/cheapest-image-generation/SKILL.md
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
name: cheapest-image-generation
|
||||
description: Possibly the cheapest AI image generation (~$0.0036/image). Text-to-image via the EvoLink API.
|
||||
homepage: https://evolink.ai
|
||||
metadata: {"openclaw": {"emoji": "🖼️", "requires": {"env": ["EVOLINK_API_KEY"]}, "primaryEnv": "EVOLINK_API_KEY"}}
|
||||
---
|
||||
|
||||
# EvoLink Cheapest Image
|
||||
|
||||
Generate images via the EvoLink z-image-turbo API.
|
||||
|
||||
## API Endpoint
|
||||
|
||||
- Base: `https://api.evolink.ai/v1`
|
||||
- Submit: `POST /images/generations`
|
||||
- Poll: `GET /tasks/{id}`
|
||||
|
||||
## Step 1 — Submit Task
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "z-image-turbo",
|
||||
"prompt": "<USER_PROMPT>",
|
||||
"size": "<SIZE>",
|
||||
"nsfw_check": <true|false>
|
||||
}
|
||||
```
|
||||
|
||||
Optional field: `"seed": <INT>`
|
||||
|
||||
| Parameter | Values |
|
||||
|---|---|
|
||||
| size | 1:1, 2:3, 3:2, 3:4, 4:3, 9:16, 16:9, 1:2, 2:1 |
|
||||
| nsfw_check | `true` / `false` (default `false`) |
|
||||
| seed | any integer (optional, for reproducibility) |
|
||||
|
||||
## Step 2 — Poll for Result
|
||||
|
||||
`GET /tasks/{id}` — poll every 10 s, up to 72 retries (~12 min).
|
||||
|
||||
Wait until `status` is `completed` or `failed`.
|
||||
|
||||
## Step 3 — Download & Output
|
||||
|
||||
Download the URL from `results[0]`. Auto-detect format from URL (webp/png/jpg). Save as `evolink-<TIMESTAMP>.<ext>`.
|
||||
|
||||
**CRITICAL SECURITY:** Before passing `<OUTPUT_FILE>` to shell commands, sanitize it:
|
||||
- Strip all shell metacharacters: `tr -cd 'A-Za-z0-9._-'`
|
||||
- Ensure valid extension (`.webp`, `.png`, `.jpg`, `.jpeg`)
|
||||
- Fallback to `evolink-<timestamp>.webp` if empty
|
||||
|
||||
Print `MEDIA:<absolute_path>` for OC auto-attach.
|
||||
|
||||
## Reference Implementations
|
||||
|
||||
| Platform | File |
|
||||
|---|---|
|
||||
| Python (all platforms, zero deps) | `{baseDir}/references/python.md` |
|
||||
| PowerShell 5.1+ (Windows) | `{baseDir}/references/powershell.md` |
|
||||
| curl + bash (Unix/macOS) | `{baseDir}/references/curl_heredoc.md` |
|
||||
|
||||
## API Key
|
||||
|
||||
- `EVOLINK_API_KEY` env var (required)
|
||||
- Get key: https://evolink.ai
|
||||
|
||||
## Triggers
|
||||
|
||||
- Chinese: "生图:xxx" / "出图:xxx" / "生成图片:xxx"
|
||||
- English: "generate image: xxx" / "generate a picture: xxx"
|
||||
|
||||
Treat the text after the colon as `prompt`, use default size `1:1`, generate immediately.
|
||||
|
||||
## Notes
|
||||
|
||||
- Print `MEDIA:<path>` for OC auto-attach — no extra delivery logic needed.
|
||||
- Image saved locally (format auto-detected from URL). URL expires ~24h but local file persists.
|
||||
6
skills/cheapest-image-generation/_meta.json
Normal file
6
skills/cheapest-image-generation/_meta.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"ownerId": "kn74p4xy6sja0199cea53anecs81kqjs",
|
||||
"slug": "cheapest-image-generation",
|
||||
"version": "1.0.2",
|
||||
"publishedAt": 1771684089493
|
||||
}
|
||||
103
skills/cheapest-image-generation/references/curl_heredoc.md
Normal file
103
skills/cheapest-image-generation/references/curl_heredoc.md
Normal file
@@ -0,0 +1,103 @@
|
||||
# curl + bash (Unix/macOS, no dependencies)
|
||||
|
||||
Run in a single shell call (avoid relying on exported variables persisting across tool calls).
|
||||
|
||||
Replace:
|
||||
- `<API_KEY>` with the user's Evolink key
|
||||
- `<OUTPUT_FILE>` with `evolink-<TIMESTAMP>.webp` (MUST be sanitized, see below)
|
||||
- `<USER_PROMPT>`, `<SIZE>`, and `<true|false>`
|
||||
|
||||
**SECURITY: Sanitize `<OUTPUT_FILE>` before substitution:**
|
||||
```bash
|
||||
# Strip all shell metacharacters, keep only alphanumeric, dash, underscore, dot
|
||||
SAFE_NAME=$(echo "<OUTPUT_FILE>" | tr -cd 'A-Za-z0-9._-')
|
||||
```
|
||||
|
||||
```bash
|
||||
API_KEY="<API_KEY>"
|
||||
RAW_OUT="<OUTPUT_FILE>"
|
||||
# CRITICAL: Sanitize filename to prevent shell injection
|
||||
OUT_FILE=$(echo "$RAW_OUT" | tr -cd 'A-Za-z0-9._-')
|
||||
# Ensure it has a valid extension
|
||||
if [[ ! "$OUT_FILE" =~ \.(webp|png|jpg|jpeg)$ ]]; then
|
||||
OUT_FILE="${OUT_FILE}.webp"
|
||||
fi
|
||||
# Ensure it's not empty
|
||||
if [ -z "$OUT_FILE" ]; then
|
||||
OUT_FILE="evolink-$(date +%s).webp"
|
||||
fi
|
||||
|
||||
PROMPT="<USER_PROMPT>"
|
||||
SIZE="<SIZE>"
|
||||
NSFW_CHECK=<true|false>
|
||||
|
||||
json_escape() {
|
||||
local s="$1"
|
||||
s="${s//\\/\\\\}"
|
||||
s="${s//\"/\\\"}"
|
||||
s="${s//$'\n'/\\n}"
|
||||
s="${s//$'\t'/\\t}"
|
||||
s="${s//$'\r'/\\r}"
|
||||
printf '%s' "$s"
|
||||
}
|
||||
|
||||
PROMPT_ESC=$(json_escape "$PROMPT")
|
||||
|
||||
RESP=$(curl -s -X POST "https://api.evolink.ai/v1/images/generations" \
|
||||
-H "Authorization: Bearer $API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<EVOLINK_END
|
||||
{"model":"z-image-turbo","prompt":"$PROMPT_ESC","size":"$SIZE","nsfw_check":$NSFW_CHECK}
|
||||
EVOLINK_END
|
||||
)
|
||||
|
||||
TASK_ID=$(echo "$RESP" | grep -o '"id":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$TASK_ID" ]; then
|
||||
echo "Error: Failed to submit task. Response: $RESP"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MAX_RETRIES=72
|
||||
for i in $(seq 1 $MAX_RETRIES); do
|
||||
sleep 10
|
||||
TASK=$(curl -s "https://api.evolink.ai/v1/tasks/$TASK_ID" \
|
||||
-H "Authorization: Bearer $API_KEY")
|
||||
STATUS=$(echo "$TASK" | grep -o '"status":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ "$STATUS" = "completed" ]; then
|
||||
URL=$(echo "$TASK" | grep -o '"results":\["[^"]*"\]' | grep -o 'https://[^"]*')
|
||||
curl -s -o "$OUT_FILE" "$URL"
|
||||
# Safe path resolution (no command substitution on user input)
|
||||
FULL_PATH="$(cd "$(dirname "$OUT_FILE")" && pwd)/$(basename "$OUT_FILE")"
|
||||
echo "MEDIA:$FULL_PATH"
|
||||
break
|
||||
fi
|
||||
if [ "$STATUS" = "failed" ]; then
|
||||
echo "Generation failed: $TASK"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$i" -eq "$MAX_RETRIES" ]; then
|
||||
echo "Timed out after max retries."
|
||||
fi
|
||||
```
|
||||
|
||||
If you only have a URL and no file yet, download it immediately (URL expires in ~24 hours):
|
||||
|
||||
```bash
|
||||
RAW_OUT="<OUTPUT_FILE>"
|
||||
# CRITICAL: Sanitize filename
|
||||
OUT_FILE=$(echo "$RAW_OUT" | tr -cd 'A-Za-z0-9._-')
|
||||
if [[ ! "$OUT_FILE" =~ \.(webp|png|jpg|jpeg)$ ]]; then
|
||||
OUT_FILE="${OUT_FILE}.webp"
|
||||
fi
|
||||
if [ -z "$OUT_FILE" ]; then
|
||||
OUT_FILE="evolink-result.webp"
|
||||
fi
|
||||
|
||||
curl -L -o "$OUT_FILE" "<URL>"
|
||||
FULL_PATH="$(cd "$(dirname "$OUT_FILE")" && pwd)/$(basename "$OUT_FILE")"
|
||||
echo "MEDIA:$FULL_PATH"
|
||||
```
|
||||
131
skills/cheapest-image-generation/references/powershell.md
Normal file
131
skills/cheapest-image-generation/references/powershell.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# PowerShell fallback (Windows, no Python needed)
|
||||
|
||||
> Requires PowerShell 5.1+ (pre-installed on all Windows). Save the script below as a `.ps1` file and run with `powershell -File generate.ps1 -Prompt "a cute cat"`.
|
||||
|
||||
```powershell
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Generate an image via Evolink Z-Image-Turbo (Windows PowerShell fallback).
|
||||
.DESCRIPTION
|
||||
Zero external dependencies. Uses Invoke-RestMethod (built into PowerShell 5.1+).
|
||||
Outputs MEDIA:<path> for OpenClaw auto-attach.
|
||||
#>
|
||||
param(
|
||||
[string]$ApiKey = $env:EVOLINK_API_KEY,
|
||||
[Parameter(Mandatory)][string]$Prompt,
|
||||
[string]$Size = "1:1",
|
||||
[string]$NsfwCheck = "false",
|
||||
[Nullable[int]]$Seed,
|
||||
[string]$Out,
|
||||
[int]$PollSeconds = 10,
|
||||
[int]$MaxRetries = 72,
|
||||
[switch]$Verbose_
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not $ApiKey) {
|
||||
Write-Error "EVOLINK_API_KEY not set and -ApiKey not provided."
|
||||
exit 2
|
||||
}
|
||||
|
||||
if ($Prompt.Length -gt 2000) {
|
||||
Write-Error "Prompt exceeds 2000 characters."
|
||||
exit 2
|
||||
}
|
||||
|
||||
if ($Out) {
|
||||
$Out = [System.IO.Path]::GetFullPath($Out)
|
||||
}
|
||||
|
||||
$apiBase = "https://api.evolink.ai/v1"
|
||||
$headers = @{
|
||||
"Authorization" = "Bearer $ApiKey"
|
||||
"User-Agent" = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
"Accept" = "application/json"
|
||||
}
|
||||
|
||||
$body = @{
|
||||
model = "z-image-turbo"
|
||||
prompt = $Prompt
|
||||
size = $Size
|
||||
nsfw_check = ($NsfwCheck -eq "true")
|
||||
}
|
||||
if ($null -ne $Seed) {
|
||||
$body["seed"] = $Seed
|
||||
}
|
||||
|
||||
$jsonBody = $body | ConvertTo-Json -Compress -Depth 5
|
||||
|
||||
try {
|
||||
$resp = Invoke-RestMethod -Uri "$apiBase/images/generations" -Method POST -Headers $headers -Body $jsonBody -ContentType "application/json" -TimeoutSec 60
|
||||
} catch {
|
||||
Write-Error "Failed to submit task: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$taskId = if ($resp.id) { $resp.id } elseif ($resp.task_id) { $resp.task_id } else { $null }
|
||||
if (-not $taskId) {
|
||||
Write-Error "Failed to submit task; response: $($resp | ConvertTo-Json -Compress)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Verbose_) {
|
||||
Write-Host "Task submitted: $taskId"
|
||||
}
|
||||
|
||||
for ($i = 1; $i -le $MaxRetries; $i++) {
|
||||
Start-Sleep -Seconds $PollSeconds
|
||||
try {
|
||||
$task = Invoke-RestMethod -Uri "$apiBase/tasks/$taskId" -Method GET -Headers $headers -TimeoutSec 60
|
||||
} catch {
|
||||
Write-Error "Failed to poll task: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$status = $task.status
|
||||
if ($Verbose_) {
|
||||
Write-Host "[$i] Status: $status"
|
||||
}
|
||||
|
||||
if ($status -eq "completed") {
|
||||
$results = $task.results
|
||||
if (-not $results -or $results.Count -eq 0) {
|
||||
Write-Error "Task completed but no results found: $($task | ConvertTo-Json -Compress)"
|
||||
exit 1
|
||||
}
|
||||
$url = $results[0]
|
||||
|
||||
# Infer extension from URL
|
||||
if (-not $Out) {
|
||||
$urlPath = ([System.Uri]$url).AbsolutePath
|
||||
$ext = [System.IO.Path]::GetExtension($urlPath).ToLower()
|
||||
if ($ext -notin ".png", ".jpg", ".jpeg", ".webp") { $ext = ".webp" }
|
||||
$ts = Get-Date -Format "yyyyMMdd-HHmmss-fff"
|
||||
$Out = [System.IO.Path]::GetFullPath("evolink-$ts$ext")
|
||||
}
|
||||
|
||||
try {
|
||||
Invoke-WebRequest -Uri $url -OutFile $Out -TimeoutSec 120 -UserAgent "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||
} catch {
|
||||
Write-Error "Failed to download image: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Verbose_) {
|
||||
Write-Host "Image URL: $url"
|
||||
Write-Host "Downloaded to: $Out"
|
||||
}
|
||||
Write-Output "MEDIA:$Out"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($status -eq "failed") {
|
||||
Write-Error "Generation failed: $($task | ConvertTo-Json -Compress)"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
Write-Error "Timed out after $MaxRetries polls. Task ID: $taskId"
|
||||
exit 1
|
||||
```
|
||||
158
skills/cheapest-image-generation/references/python.md
Normal file
158
skills/cheapest-image-generation/references/python.md
Normal file
@@ -0,0 +1,158 @@
|
||||
# Python reference (all platforms, zero dependencies)
|
||||
|
||||
> Requires Python 3.6+. Uses only stdlib (`urllib`, `json`, `time`).
|
||||
|
||||
Save the script below as `generate.py` and run with `python3 generate.py --prompt "a cute cat" --size "1:1"`.
|
||||
|
||||
Options: `--size` (1:1, 2:3, 3:2, 3:4, 4:3, 9:16, 16:9, 1:2, 2:1), `--seed INT`, `--nsfw-check true`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as _dt
|
||||
import json
|
||||
import os
|
||||
import posixpath
|
||||
import sys
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
API_BASE = "https://api.evolink.ai/v1"
|
||||
_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
||||
|
||||
|
||||
def _json_request(url: str, api_key: str, method: str = "GET", payload=None, timeout_s: int = 60):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": _UA,
|
||||
"Accept": "application/json",
|
||||
}
|
||||
data = None
|
||||
if payload is not None:
|
||||
data = json.dumps(payload).encode("utf-8")
|
||||
|
||||
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
|
||||
body = resp.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
err_body = e.read()
|
||||
raise RuntimeError(f"HTTP {e.code} {e.reason}: {err_body.decode('utf-8', 'replace')}") from None
|
||||
except urllib.error.URLError as e:
|
||||
raise RuntimeError(f"Network error: {e.reason}") from None
|
||||
|
||||
try:
|
||||
return json.loads(body.decode("utf-8"))
|
||||
except Exception:
|
||||
raise RuntimeError(f"Non-JSON response: {body[:200]!r}") from None
|
||||
|
||||
|
||||
def _download(url: str, out_file: str, timeout_s: int = 120):
|
||||
req = urllib.request.Request(url, method="GET", headers={"User-Agent": _UA})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=timeout_s) as resp:
|
||||
content = resp.read()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Failed to download result: {e}") from None
|
||||
|
||||
with open(out_file, "wb") as f:
|
||||
f.write(content)
|
||||
|
||||
|
||||
def _default_out_file(ext: str = ".webp"):
|
||||
ts = _dt.datetime.now().strftime("%Y%m%d-%H%M%S-%f")
|
||||
return f"evolink-{ts}{ext}"
|
||||
|
||||
|
||||
def _ext_from_url(url: str) -> str:
|
||||
path = urlparse(url).path
|
||||
_, ext = posixpath.splitext(path)
|
||||
return ext.lower() if ext in (".png", ".jpg", ".jpeg", ".webp") else ".webp"
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
parser = argparse.ArgumentParser(description="Generate an image via Evolink Z-Image-Turbo.")
|
||||
parser.add_argument(
|
||||
"--api-key",
|
||||
default=os.getenv("EVOLINK_API_KEY", ""),
|
||||
help="Evolink API key (or set EVOLINK_API_KEY).",
|
||||
)
|
||||
parser.add_argument("--prompt", required=True, help="Image prompt (max 2000 chars recommended).")
|
||||
parser.add_argument("--size", default="1:1", help="Aspect ratio (e.g. 1:1, 9:16) or custom WxH.")
|
||||
parser.add_argument("--nsfw-check", default="false", choices=["true", "false"], help="Stricter NSFW filtering.")
|
||||
parser.add_argument("--seed", type=int, default=None, help="Optional seed for reproducibility.")
|
||||
parser.add_argument("--out", default=None, help="Output filename (default: evolink-<timestamp>.<ext>).")
|
||||
parser.add_argument("--poll-seconds", type=int, default=10, help="Seconds between polls.")
|
||||
parser.add_argument("--max-retries", type=int, default=72, help="Max polling attempts.")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print task id and per-poll status (debug).")
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
api_key = (args.api_key or "").strip()
|
||||
if not api_key:
|
||||
print("Error: EVOLINK_API_KEY not set and --api-key not provided.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if len(args.prompt) > 2000:
|
||||
print("Error: prompt exceeds 2000 characters.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
out_file = os.path.abspath(args.out) if args.out else None
|
||||
|
||||
payload = {
|
||||
"model": "z-image-turbo",
|
||||
"prompt": args.prompt,
|
||||
"size": args.size,
|
||||
"nsfw_check": (args.nsfw_check == "true"),
|
||||
}
|
||||
if args.seed is not None:
|
||||
payload["seed"] = args.seed
|
||||
|
||||
resp = _json_request(f"{API_BASE}/images/generations", api_key=api_key, method="POST", payload=payload)
|
||||
task_id = resp.get("id") or resp.get("task_id")
|
||||
if not task_id:
|
||||
raise RuntimeError(f"Failed to submit task; response: {resp}")
|
||||
|
||||
if args.verbose:
|
||||
print(f"Task submitted: {task_id}")
|
||||
|
||||
for i in range(1, args.max_retries + 1):
|
||||
time.sleep(args.poll_seconds)
|
||||
task = _json_request(f"{API_BASE}/tasks/{task_id}", api_key=api_key, method="GET")
|
||||
status = task.get("status")
|
||||
if args.verbose:
|
||||
print(f"[{i}] Status: {status}")
|
||||
|
||||
if status == "completed":
|
||||
results = task.get("results") or []
|
||||
if not results:
|
||||
raise RuntimeError(f"Task completed but no results field found: {task}")
|
||||
url = results[0]
|
||||
ext = _ext_from_url(url)
|
||||
if not args.out:
|
||||
out_file = os.path.abspath(_default_out_file(ext))
|
||||
_download(url, out_file=out_file)
|
||||
if args.verbose:
|
||||
print(f"Image URL: {url}")
|
||||
print(f"Downloaded to: {out_file}")
|
||||
print(f"MEDIA:{out_file}")
|
||||
return 0
|
||||
|
||||
if status == "failed":
|
||||
raise RuntimeError(f"Generation failed: {task}")
|
||||
|
||||
raise RuntimeError(f"Timed out after {args.max_retries} polls. Task ID: {task_id}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main(sys.argv[1:]))
|
||||
except Exception as e:
|
||||
print(f"Error: {e}", file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
```
|
||||
Reference in New Issue
Block a user