@aiyiran/myclaw 1.0.201 → 1.0.202
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/index.js +113 -25
- package/{inject-clear-models.js → injects/inject-clear-models.js} +1 -1
- package/{inject-image.js → injects/inject-image.js} +1 -1
- package/{inject-minimax.js → injects/inject-minimax.js} +1 -1
- package/{inject-search.js → injects/inject-search.js} +1 -1
- package/{inject-token.js → injects/inject-token.js} +1 -1
- package/injects/inject-tooldeny.js +50 -0
- package/{inject-zai.js → injects/inject-zai.js} +1 -1
- package/package.json +1 -1
- package/{patch-manifest.json → patches/patch-manifest.json} +15 -5
- package/{patch-reset.js → patches/patch-reset.js} +1 -1
- package/{patch-skill.js → patches/patch-skill.js} +6 -1
- package/pull.js +1 -1
- package/skills/vapi-image-gen-1.0.1/README.md +92 -0
- package/skills/vapi-image-gen-1.0.1/SKILL.md +75 -0
- package/skills/vapi-image-gen-1.0.1/_meta.json +6 -0
- package/skills/vapi-image-gen-1.0.1/scripts/gen.py +259 -0
- package/skills/yiran-skill-media/SKILL.md +74 -0
- package/skills/yiran-skill-media/config.json +26 -0
- package/skills/yiran-skill-media/references/image-api.md +88 -0
- package/skills/yiran-skill-media/references/music-api.md +120 -0
- package/skills/yiran-skill-media/scripts/generate.py +165 -0
- package/skills/yiran-skill-media/scripts/generation_log.json +20 -0
- package/skills/yiran-skill-media/scripts/image.sh +43 -0
- package/skills/yiran-skill-media/scripts/music.sh +46 -0
- package/skills/yiran-skill-media/scripts/providers/__init__.py +15 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/__init__.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_image.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/minimax_music.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/__pycache__/vapi_image.cpython-311.pyc +0 -0
- package/skills/yiran-skill-media/scripts/providers/minimax_image.py +75 -0
- package/skills/yiran-skill-media/scripts/providers/minimax_music.py +61 -0
- package/skills/yiran-skill-media/scripts/providers/vapi_image.py +63 -0
- package/skills/yiran-skill-media/scripts/registry.py +133 -0
- /package/{inject-workspaceAndSoul.js → injects/inject-workspaceAndSoul.js} +0 -0
- /package/{patch-agent.js → patches/patch-agent.js} +0 -0
- /package/{patch.js → patches/patch.js} +0 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
统一资源生成调度器
|
|
4
|
+
用法:
|
|
5
|
+
python3 generate.py image "prompt" [--aspect-ratio 16:9] [--output path]
|
|
6
|
+
python3 generate.py music "prompt" [--lyrics "歌词"] [--instrumental] [--output path]
|
|
7
|
+
"""
|
|
8
|
+
import argparse
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import tempfile
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
17
|
+
CONFIG_PATH = os.path.join(SCRIPT_DIR, "..", "config.json")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_config():
|
|
21
|
+
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
|
|
22
|
+
return json.load(f)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_output_dir():
|
|
26
|
+
"""Resolve output directory. Fallback to /tmp/media/ if primary is unwritable."""
|
|
27
|
+
workspace = os.environ.get("WORKSPACE_NAME", "main")
|
|
28
|
+
openclaw_home = os.environ.get("OPENCLAW_HOME", os.path.join(os.path.expanduser("~"), ".openclaw"))
|
|
29
|
+
|
|
30
|
+
if workspace == "main":
|
|
31
|
+
base = os.path.join(openclaw_home, "workspace")
|
|
32
|
+
else:
|
|
33
|
+
base = os.path.join(openclaw_home, f"workspace-{workspace}")
|
|
34
|
+
|
|
35
|
+
out_dir = os.path.join(base, "media")
|
|
36
|
+
try:
|
|
37
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
38
|
+
return out_dir
|
|
39
|
+
except OSError:
|
|
40
|
+
# Primary dir unwritable (e.g. /root on macOS) → fallback to tmp
|
|
41
|
+
fallback = os.path.join(tempfile.gettempdir(), "media")
|
|
42
|
+
os.makedirs(fallback, exist_ok=True)
|
|
43
|
+
print(f"[warn] {out_dir} not writable, using {fallback}", file=sys.stderr)
|
|
44
|
+
return fallback
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def make_output_path(out_dir, resource_type, ext):
|
|
48
|
+
"""Generate timestamped filename."""
|
|
49
|
+
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
50
|
+
return os.path.join(out_dir, f"{resource_type}_{ts}.{ext}")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def append_log(log_path, entry):
|
|
54
|
+
"""Append a generation record. Never raises — logging failure is non-fatal."""
|
|
55
|
+
try:
|
|
56
|
+
if os.path.exists(log_path):
|
|
57
|
+
with open(log_path, "r", encoding="utf-8") as f:
|
|
58
|
+
log = json.load(f)
|
|
59
|
+
else:
|
|
60
|
+
log = []
|
|
61
|
+
|
|
62
|
+
log.append(entry)
|
|
63
|
+
|
|
64
|
+
with open(log_path, "w", encoding="utf-8") as f:
|
|
65
|
+
json.dump(log, f, ensure_ascii=False, indent=2)
|
|
66
|
+
|
|
67
|
+
print(f"[log] 已记录到 {log_path}", file=sys.stderr)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"[warn] log write failed: {e}", file=sys.stderr)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def dispatch(resource_type, prompt, **kwargs):
|
|
73
|
+
"""Route to primary provider, fallback on failure.
|
|
74
|
+
|
|
75
|
+
Always tries primary first, then fallback if configured.
|
|
76
|
+
Returns (files, used_provider_cfg).
|
|
77
|
+
"""
|
|
78
|
+
cfg = load_config()
|
|
79
|
+
resource_cfg = cfg.get(resource_type)
|
|
80
|
+
if not resource_cfg:
|
|
81
|
+
raise ValueError(f"unknown resource type: {resource_type}")
|
|
82
|
+
|
|
83
|
+
providers = [resource_cfg["primary"]]
|
|
84
|
+
if resource_cfg.get("fallback"):
|
|
85
|
+
providers.append(resource_cfg["fallback"])
|
|
86
|
+
|
|
87
|
+
sys.path.insert(0, SCRIPT_DIR)
|
|
88
|
+
from providers import get_adapter
|
|
89
|
+
|
|
90
|
+
errors = []
|
|
91
|
+
for provider_cfg in providers:
|
|
92
|
+
name = provider_cfg["provider"]
|
|
93
|
+
try:
|
|
94
|
+
adapter = get_adapter(name)
|
|
95
|
+
files = adapter.generate(prompt, config=provider_cfg, **kwargs)
|
|
96
|
+
return files, provider_cfg
|
|
97
|
+
except Exception as e:
|
|
98
|
+
msg = f"[{name}] failed: {e}"
|
|
99
|
+
print(msg, file=sys.stderr)
|
|
100
|
+
errors.append(msg)
|
|
101
|
+
|
|
102
|
+
raise RuntimeError("all providers failed:\n" + "\n".join(errors))
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
parser = argparse.ArgumentParser(description="Unified media generation dispatcher")
|
|
107
|
+
parser.add_argument("type", choices=["image", "music"], help="Resource type")
|
|
108
|
+
parser.add_argument("prompt", help="Generation prompt")
|
|
109
|
+
parser.add_argument("--aspect-ratio", default="1:1", help="Image aspect ratio (image only)")
|
|
110
|
+
parser.add_argument("--lyrics", default=None, help="Lyrics text (music only)")
|
|
111
|
+
parser.add_argument("--instrumental", action="store_true", help="Instrumental mode (music only)")
|
|
112
|
+
parser.add_argument("--output", default=None, help="Output file path")
|
|
113
|
+
args = parser.parse_args()
|
|
114
|
+
|
|
115
|
+
out_dir = get_output_dir()
|
|
116
|
+
|
|
117
|
+
if args.type == "image":
|
|
118
|
+
output_path = args.output or make_output_path(out_dir, "image", "png")
|
|
119
|
+
kwargs = {
|
|
120
|
+
"out_dir": out_dir,
|
|
121
|
+
"output_path": output_path,
|
|
122
|
+
"aspect_ratio": args.aspect_ratio,
|
|
123
|
+
}
|
|
124
|
+
else:
|
|
125
|
+
output_path = args.output or make_output_path(out_dir, "music", "mp3")
|
|
126
|
+
kwargs = {
|
|
127
|
+
"out_dir": out_dir,
|
|
128
|
+
"output_path": output_path,
|
|
129
|
+
"lyrics": args.lyrics,
|
|
130
|
+
"instrumental": args.instrumental,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
start_time = time.time()
|
|
134
|
+
start_dt = datetime.now()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
files, used_provider = dispatch(args.type, args.prompt, **kwargs)
|
|
138
|
+
duration = round(time.time() - start_time, 2)
|
|
139
|
+
end_dt = datetime.now()
|
|
140
|
+
|
|
141
|
+
# Log — non-fatal
|
|
142
|
+
log_entry = {
|
|
143
|
+
"id": start_dt.strftime("%Y%m%d_%H%M%S"),
|
|
144
|
+
"type": args.type,
|
|
145
|
+
"name": os.path.basename(files[0]) if files else None,
|
|
146
|
+
"files": files,
|
|
147
|
+
"prompt": args.prompt,
|
|
148
|
+
"params": {k: v for k, v in kwargs.items() if k != "out_dir"},
|
|
149
|
+
"provider": used_provider["provider"],
|
|
150
|
+
"model": used_provider["model"],
|
|
151
|
+
"started_at": start_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
|
152
|
+
"finished_at": end_dt.strftime("%Y-%m-%d %H:%M:%S"),
|
|
153
|
+
"duration_seconds": duration,
|
|
154
|
+
}
|
|
155
|
+
log_path = os.path.join(SCRIPT_DIR, "generation_log.json")
|
|
156
|
+
append_log(log_path, log_entry)
|
|
157
|
+
|
|
158
|
+
print(json.dumps({"success": True, "files": files}))
|
|
159
|
+
except Exception as e:
|
|
160
|
+
print(json.dumps({"success": False, "error": str(e)}))
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "20260413_141112",
|
|
4
|
+
"type": "image",
|
|
5
|
+
"name": "image_20260413_141112.png",
|
|
6
|
+
"files": [
|
|
7
|
+
"/Users/yiran/.openclaw/workspace/media/image_20260413_141112.png"
|
|
8
|
+
],
|
|
9
|
+
"prompt": "a cute cat sitting on a windowsill",
|
|
10
|
+
"params": {
|
|
11
|
+
"output_path": "/Users/yiran/.openclaw/workspace/media/image_20260413_141112.png",
|
|
12
|
+
"aspect_ratio": "1:1"
|
|
13
|
+
},
|
|
14
|
+
"provider": "vapi_image",
|
|
15
|
+
"model": "nano-banana-pro",
|
|
16
|
+
"started_at": "2026-04-13 14:11:12",
|
|
17
|
+
"finished_at": "2026-04-13 14:11:36",
|
|
18
|
+
"duration_seconds": 23.5
|
|
19
|
+
}
|
|
20
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 图片生成入口
|
|
3
|
+
# 用法: WORKSPACE_NAME=main ./image.sh "描述" [--aspect-ratio 16:9] [--registry]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PROMPT="${1:-}"
|
|
7
|
+
shift 2>/dev/null || true
|
|
8
|
+
|
|
9
|
+
if [ -z "$PROMPT" ]; then
|
|
10
|
+
echo "用法: WORKSPACE_NAME=xxx ./image.sh \"描述\" [--aspect-ratio 16:9] [--registry]"
|
|
11
|
+
echo "示例: WORKSPACE_NAME=main ./image.sh \"a cute cat\" --aspect-ratio 16:9 --registry"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
ASPECT_RATIO=""
|
|
16
|
+
REGISTRY=false
|
|
17
|
+
|
|
18
|
+
while [ $# -gt 0 ]; do
|
|
19
|
+
case "$1" in
|
|
20
|
+
--aspect-ratio) ASPECT_RATIO="$2"; shift 2 ;;
|
|
21
|
+
--registry) REGISTRY=true; shift ;;
|
|
22
|
+
*) shift ;;
|
|
23
|
+
esac
|
|
24
|
+
done
|
|
25
|
+
|
|
26
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
27
|
+
|
|
28
|
+
# Build args for generate.py
|
|
29
|
+
ARGS=()
|
|
30
|
+
ARGS+=("image")
|
|
31
|
+
ARGS+=("$PROMPT")
|
|
32
|
+
[ -n "$ASPECT_RATIO" ] && ARGS+=(--aspect-ratio "$ASPECT_RATIO")
|
|
33
|
+
|
|
34
|
+
RESULT=$(python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}")
|
|
35
|
+
echo "$RESULT"
|
|
36
|
+
|
|
37
|
+
# Parse output path and optionally register
|
|
38
|
+
if [ "$REGISTRY" = true ]; then
|
|
39
|
+
OUTPUT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['files'][0])" 2>/dev/null || true)
|
|
40
|
+
if [ -n "$OUTPUT" ]; then
|
|
41
|
+
WORKSPACE_NAME="${WORKSPACE_NAME:-main}" AUTO_REGISTRY=true python3 "$SCRIPT_DIR/registry.py" "$OUTPUT"
|
|
42
|
+
fi
|
|
43
|
+
fi
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# 音乐生成入口
|
|
3
|
+
# 用法: WORKSPACE_NAME=main ./music.sh "描述" [--lyrics "歌词"] [--instrumental] [--registry]
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
PROMPT="${1:-}"
|
|
7
|
+
shift 2>/dev/null || true
|
|
8
|
+
|
|
9
|
+
if [ -z "$PROMPT" ]; then
|
|
10
|
+
echo "用法: WORKSPACE_NAME=xxx ./music.sh \"描述\" [--lyrics \"歌词\"] [--instrumental] [--registry]"
|
|
11
|
+
echo "示例: WORKSPACE_NAME=main ./music.sh \"relaxing guitar\" --instrumental --registry"
|
|
12
|
+
exit 1
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
LYRICS=""
|
|
16
|
+
INSTRUMENTAL=false
|
|
17
|
+
REGISTRY=false
|
|
18
|
+
|
|
19
|
+
while [ $# -gt 0 ]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--lyrics) LYRICS="$2"; shift 2 ;;
|
|
22
|
+
--instrumental) INSTRUMENTAL=true; shift ;;
|
|
23
|
+
--registry) REGISTRY=true; shift ;;
|
|
24
|
+
*) shift ;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
|
|
28
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
29
|
+
|
|
30
|
+
# Build args for generate.py
|
|
31
|
+
ARGS=()
|
|
32
|
+
ARGS+=("music")
|
|
33
|
+
ARGS+=("$PROMPT")
|
|
34
|
+
[ -n "$LYRICS" ] && ARGS+=(--lyrics "$LYRICS")
|
|
35
|
+
[ "$INSTRUMENTAL" = true ] && ARGS+=(--instrumental)
|
|
36
|
+
|
|
37
|
+
RESULT=$(python3 "$SCRIPT_DIR/generate.py" "${ARGS[@]}")
|
|
38
|
+
echo "$RESULT"
|
|
39
|
+
|
|
40
|
+
# Parse output path and optionally register
|
|
41
|
+
if [ "$REGISTRY" = true ]; then
|
|
42
|
+
OUTPUT=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d['files'][0])" 2>/dev/null || true)
|
|
43
|
+
if [ -n "$OUTPUT" ]; then
|
|
44
|
+
WORKSPACE_NAME="${WORKSPACE_NAME:-main}" AUTO_REGISTRY=true python3 "$SCRIPT_DIR/registry.py" "$OUTPUT"
|
|
45
|
+
fi
|
|
46
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .vapi_image import VAPIImageAdapter
|
|
2
|
+
from .minimax_image import MiniMaxImageAdapter
|
|
3
|
+
from .minimax_music import MiniMaxMusicAdapter
|
|
4
|
+
|
|
5
|
+
ADAPTERS = {
|
|
6
|
+
"vapi_image": VAPIImageAdapter(),
|
|
7
|
+
"minimax_image": MiniMaxImageAdapter(),
|
|
8
|
+
"minimax_music": MiniMaxMusicAdapter(),
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
def get_adapter(name: str):
|
|
12
|
+
adapter = ADAPTERS.get(name)
|
|
13
|
+
if not adapter:
|
|
14
|
+
raise ValueError(f"unknown provider: {name}")
|
|
15
|
+
return adapter
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""MiniMax image adapter — POST /image_generation, base64 decode to save."""
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MiniMaxImageAdapter:
|
|
11
|
+
@staticmethod
|
|
12
|
+
def _ratio_to_size(ratio_str, max_dim=2048):
|
|
13
|
+
"""Parse '16:9' style ratio and compute (width, height) capped at max_dim."""
|
|
14
|
+
parts = ratio_str.split(":")
|
|
15
|
+
if len(parts) != 2:
|
|
16
|
+
return 1024, 1024
|
|
17
|
+
try:
|
|
18
|
+
rw, rh = float(parts[0]), float(parts[1])
|
|
19
|
+
except ValueError:
|
|
20
|
+
return 1024, 1024
|
|
21
|
+
# Scale so the longer side = max_dim
|
|
22
|
+
scale = max_dim / max(rw, rh)
|
|
23
|
+
w = round(rw * scale)
|
|
24
|
+
h = round(rh * scale)
|
|
25
|
+
return w, h
|
|
26
|
+
|
|
27
|
+
def generate(self, prompt, config, **kwargs):
|
|
28
|
+
base_url = config["base_url"].rstrip("/")
|
|
29
|
+
api_key = config["api_key"]
|
|
30
|
+
model = config["model"]
|
|
31
|
+
out_dir = kwargs["out_dir"]
|
|
32
|
+
|
|
33
|
+
# 默认 1024x1024;传了 aspect_ratio 则按 2048 上限换算 width/height
|
|
34
|
+
aspect_ratio = kwargs.get("aspect_ratio")
|
|
35
|
+
if aspect_ratio and aspect_ratio != "1:1":
|
|
36
|
+
w, h = self._ratio_to_size(aspect_ratio, max_dim=2048)
|
|
37
|
+
size_params = {"width": w, "height": h}
|
|
38
|
+
else:
|
|
39
|
+
size_params = {"width": 1024, "height": 1024}
|
|
40
|
+
|
|
41
|
+
endpoint = f"{base_url}/image_generation"
|
|
42
|
+
headers = {
|
|
43
|
+
"Authorization": f"Bearer {api_key}",
|
|
44
|
+
"Content-Type": "application/json",
|
|
45
|
+
}
|
|
46
|
+
payload = {
|
|
47
|
+
"model": model,
|
|
48
|
+
"prompt": prompt,
|
|
49
|
+
"response_format": "base64",
|
|
50
|
+
"n": 1,
|
|
51
|
+
**size_params,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
print(f"[minimax_image] generating with model={model} size={size_params}...", file=sys.stderr)
|
|
55
|
+
resp = requests.post(endpoint, headers=headers, json=payload, timeout=180)
|
|
56
|
+
data = resp.json()
|
|
57
|
+
|
|
58
|
+
if data.get("base_resp", {}).get("status_code") != 0:
|
|
59
|
+
raise RuntimeError(f"MiniMax error: {data.get('base_resp', {}).get('status_msg')}")
|
|
60
|
+
|
|
61
|
+
image_list = data.get("data", {}).get("image_base64", [])
|
|
62
|
+
if not image_list:
|
|
63
|
+
raise RuntimeError("no image data in response")
|
|
64
|
+
|
|
65
|
+
saved = []
|
|
66
|
+
for i, img_b64 in enumerate(image_list):
|
|
67
|
+
decoded = base64.b64decode(img_b64)
|
|
68
|
+
ext = "png" if decoded[:8].startswith(b"\x89PNG") else "jpg"
|
|
69
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i+1}.{ext}")
|
|
70
|
+
with open(fname, "wb") as f:
|
|
71
|
+
f.write(decoded)
|
|
72
|
+
saved.append(fname)
|
|
73
|
+
print(f"[minimax_image] saved: {fname}", file=sys.stderr)
|
|
74
|
+
|
|
75
|
+
return saved
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""MiniMax music adapter — POST /music_generation, download audio to save."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class MiniMaxMusicAdapter:
|
|
10
|
+
def generate(self, prompt, config, **kwargs):
|
|
11
|
+
base_url = config["base_url"].rstrip("/")
|
|
12
|
+
api_key = config["api_key"]
|
|
13
|
+
model = config["model"]
|
|
14
|
+
out_dir = kwargs["out_dir"]
|
|
15
|
+
lyrics = kwargs.get("lyrics")
|
|
16
|
+
is_instrumental = kwargs.get("instrumental", False)
|
|
17
|
+
|
|
18
|
+
endpoint = f"{base_url}/music_generation"
|
|
19
|
+
headers = {
|
|
20
|
+
"Authorization": f"Bearer {api_key}",
|
|
21
|
+
"Content-Type": "application/json",
|
|
22
|
+
}
|
|
23
|
+
payload = {
|
|
24
|
+
"model": model,
|
|
25
|
+
"prompt": prompt,
|
|
26
|
+
"is_instrumental": is_instrumental,
|
|
27
|
+
"output_format": "url",
|
|
28
|
+
"audio_setting": {
|
|
29
|
+
"sample_rate": 44100,
|
|
30
|
+
"bitrate": 256000,
|
|
31
|
+
"format": "mp3",
|
|
32
|
+
},
|
|
33
|
+
}
|
|
34
|
+
if lyrics:
|
|
35
|
+
payload["lyrics"] = lyrics
|
|
36
|
+
|
|
37
|
+
print(f"[minimax_music] generating with model={model}...", file=sys.stderr)
|
|
38
|
+
resp = requests.post(endpoint, headers=headers, json=payload, timeout=120)
|
|
39
|
+
data = resp.json()
|
|
40
|
+
|
|
41
|
+
if data.get("base_resp", {}).get("status_code") != 0:
|
|
42
|
+
raise RuntimeError(f"MiniMax error: {data.get('base_resp', {}).get('status_msg')}")
|
|
43
|
+
|
|
44
|
+
audio_data = data.get("data", {})
|
|
45
|
+
status = audio_data.get("status")
|
|
46
|
+
if status == 1:
|
|
47
|
+
raise RuntimeError("music still synthesizing (status=1), retry later")
|
|
48
|
+
|
|
49
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, "music.mp3")
|
|
50
|
+
audio_url = audio_data.get("audio")
|
|
51
|
+
if not audio_url:
|
|
52
|
+
raise RuntimeError("no audio URL in response")
|
|
53
|
+
|
|
54
|
+
r = requests.get(audio_url, timeout=60)
|
|
55
|
+
with open(fname, "wb") as f:
|
|
56
|
+
f.write(r.content)
|
|
57
|
+
|
|
58
|
+
duration_ms = data.get("extra_info", {}).get("music_duration", 0)
|
|
59
|
+
print(f"[minimax_music] saved: {fname} ({duration_ms/1000:.1f}s)", file=sys.stderr)
|
|
60
|
+
|
|
61
|
+
return [fname]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""VAPI image adapter — POST /images/generations, download URL to save."""
|
|
2
|
+
import base64
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VAPIImageAdapter:
|
|
11
|
+
def generate(self, prompt, config, **kwargs):
|
|
12
|
+
base_url = config["base_url"].rstrip("/")
|
|
13
|
+
api_key = config["api_key"]
|
|
14
|
+
model = config["model"]
|
|
15
|
+
aspect_ratio = kwargs.get("aspect_ratio", "")
|
|
16
|
+
out_dir = kwargs["out_dir"]
|
|
17
|
+
|
|
18
|
+
url = f"{base_url}/images/generations"
|
|
19
|
+
payload = {
|
|
20
|
+
"model": model,
|
|
21
|
+
"prompt": prompt,
|
|
22
|
+
"n": 1,
|
|
23
|
+
"response_format": "url",
|
|
24
|
+
}
|
|
25
|
+
if aspect_ratio:
|
|
26
|
+
payload["aspect_ratio"] = aspect_ratio
|
|
27
|
+
|
|
28
|
+
headers = {
|
|
29
|
+
"Authorization": f"Bearer {api_key}",
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
print(f"[vapi_image] generating with model={model}...", file=sys.stderr)
|
|
34
|
+
resp = requests.post(url, headers=headers, json=payload, timeout=180)
|
|
35
|
+
if resp.status_code != 200:
|
|
36
|
+
raise RuntimeError(f"VAPI API error ({resp.status_code}): {resp.text}")
|
|
37
|
+
|
|
38
|
+
data = resp.json()
|
|
39
|
+
images = data.get("data", [])
|
|
40
|
+
if not images:
|
|
41
|
+
raise RuntimeError("no image data in response")
|
|
42
|
+
|
|
43
|
+
saved = []
|
|
44
|
+
for i, item in enumerate(images):
|
|
45
|
+
image_url = item.get("url")
|
|
46
|
+
b64 = item.get("b64_json")
|
|
47
|
+
fname = kwargs.get("output_path") or os.path.join(out_dir, f"image_{i+1}.png")
|
|
48
|
+
|
|
49
|
+
if b64:
|
|
50
|
+
with open(fname, "wb") as f:
|
|
51
|
+
f.write(base64.b64decode(b64))
|
|
52
|
+
elif image_url:
|
|
53
|
+
r = requests.get(image_url, timeout=120)
|
|
54
|
+
r.raise_for_status()
|
|
55
|
+
with open(fname, "wb") as f:
|
|
56
|
+
f.write(r.content)
|
|
57
|
+
else:
|
|
58
|
+
raise RuntimeError(f"no image data for item {i}")
|
|
59
|
+
|
|
60
|
+
saved.append(fname)
|
|
61
|
+
print(f"[vapi_image] saved: {fname}", file=sys.stderr)
|
|
62
|
+
|
|
63
|
+
return saved
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MiniMax 资源注册脚本
|
|
4
|
+
生成文件后自动注册到 __MY_ARTIFACTS__.json
|
|
5
|
+
|
|
6
|
+
用法:
|
|
7
|
+
AUTO_REGISTRY=true ./image.sh "描述"
|
|
8
|
+
AUTO_REGISTRY=false ./image.sh "描述" # 不注册
|
|
9
|
+
|
|
10
|
+
环境变量:
|
|
11
|
+
WORKSPACE_NAME - workspace名称,main 或其他
|
|
12
|
+
AUTO_REGISTRY - true=注册到JSON, false=不注册
|
|
13
|
+
MINIMAX_API_KEY - API密钥(不需要,生成用)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone, timedelta
|
|
20
|
+
|
|
21
|
+
WORKSPACE = os.environ.get("WORKSPACE_NAME", "main")
|
|
22
|
+
AUTO_REGISTRY = os.environ.get("AUTO_REGISTRY", "false").lower() == "true"
|
|
23
|
+
|
|
24
|
+
OPENCLAW_HOME = os.environ.get("OPENCLAW_HOME", os.path.join(os.path.expanduser("~"), ".openclaw"))
|
|
25
|
+
|
|
26
|
+
# 路径构建
|
|
27
|
+
if WORKSPACE == "main":
|
|
28
|
+
WORKSPACE_ROOT = os.path.join(OPENCLAW_HOME, "workspace")
|
|
29
|
+
else:
|
|
30
|
+
WORKSPACE_ROOT = os.path.join(OPENCLAW_HOME, f"workspace-{WORKSPACE}")
|
|
31
|
+
|
|
32
|
+
MYCLAW_DIR = os.path.join(WORKSPACE_ROOT, ".myclaw")
|
|
33
|
+
REGISTRY_FILE = os.path.join(MYCLAW_DIR, "__MY_ARTIFACTS__.json")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def load_registry():
|
|
37
|
+
"""加载或初始化 registry"""
|
|
38
|
+
if os.path.exists(REGISTRY_FILE):
|
|
39
|
+
with open(REGISTRY_FILE, "r", encoding="utf-8") as f:
|
|
40
|
+
return json.load(f)
|
|
41
|
+
return {
|
|
42
|
+
"workspace_id": WORKSPACE,
|
|
43
|
+
"title": f"我的资源库",
|
|
44
|
+
"release_version": 1,
|
|
45
|
+
"created_at": datetime.now(timezone(timedelta(hours=8))).isoformat(),
|
|
46
|
+
"updated_at": datetime.now(timezone(timedelta(hours=8))).isoformat(),
|
|
47
|
+
"version": 1,
|
|
48
|
+
"base_url": "https://claw.kekouen.cn/cmd/api/preview?path=",
|
|
49
|
+
"preview_path": "index.html",
|
|
50
|
+
"assets": []
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def save_registry(data):
|
|
55
|
+
"""保存 registry"""
|
|
56
|
+
os.makedirs(MYCLAW_DIR, exist_ok=True)
|
|
57
|
+
data["updated_at"] = datetime.now(timezone(timedelta(hours=8))).isoformat()
|
|
58
|
+
with open(REGISTRY_FILE, "w", encoding="utf-8") as f:
|
|
59
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def guess_type(filename):
|
|
63
|
+
"""根据扩展名判断资源类型"""
|
|
64
|
+
ext = os.path.splitext(filename)[1].lower()
|
|
65
|
+
type_map = {
|
|
66
|
+
".png": "image", ".jpg": "image", ".jpeg": "image",
|
|
67
|
+
".gif": "image", ".webp": "image", ".bmp": "image",
|
|
68
|
+
".mp4": "video", ".mov": "video", ".avi": "video",
|
|
69
|
+
".mp3": "audio", ".wav": "audio", ".flac": "audio",
|
|
70
|
+
".html": "html", ".htm": "html",
|
|
71
|
+
".pdf": "document", ".doc": "document", ".docx": "document",
|
|
72
|
+
}
|
|
73
|
+
return type_map.get(ext, "file")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def register_asset(filepath, name=None):
|
|
77
|
+
"""
|
|
78
|
+
注册一个文件到 JSON
|
|
79
|
+
filepath: 完整路径,如 /root/.openclaw/workspace/media/image_xxx.png
|
|
80
|
+
"""
|
|
81
|
+
if not AUTO_REGISTRY:
|
|
82
|
+
print(f"[Registry] AUTO_REGISTRY=false,跳过注册")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
filename = os.path.basename(filepath)
|
|
86
|
+
rel_path = os.path.relpath(filepath, WORKSPACE_ROOT)
|
|
87
|
+
asset_type = guess_type(filename)
|
|
88
|
+
|
|
89
|
+
# 生成 id
|
|
90
|
+
import hashlib
|
|
91
|
+
asset_id = hashlib.md5(filename.encode()).hexdigest()[:8]
|
|
92
|
+
asset_id = f"asset-{asset_id}"
|
|
93
|
+
|
|
94
|
+
# 生成 name
|
|
95
|
+
if not name:
|
|
96
|
+
name = os.path.splitext(filename)[0]
|
|
97
|
+
|
|
98
|
+
registry = load_registry()
|
|
99
|
+
|
|
100
|
+
# 检查是否已存在(同名文件)
|
|
101
|
+
for i, asset in enumerate(registry["assets"]):
|
|
102
|
+
if asset.get("path") == rel_path:
|
|
103
|
+
registry["assets"][i] = {
|
|
104
|
+
"id": asset_id,
|
|
105
|
+
"type": asset_type,
|
|
106
|
+
"name": name,
|
|
107
|
+
"path": rel_path
|
|
108
|
+
}
|
|
109
|
+
save_registry(registry)
|
|
110
|
+
print(f"[Registry] 已更新: {rel_path}")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
# 新增
|
|
114
|
+
registry["assets"].append({
|
|
115
|
+
"id": asset_id,
|
|
116
|
+
"type": asset_type,
|
|
117
|
+
"name": name,
|
|
118
|
+
"path": rel_path
|
|
119
|
+
})
|
|
120
|
+
save_registry(registry)
|
|
121
|
+
print(f"[Registry] 已注册: {rel_path} ({asset_type})")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
# CLI 模式
|
|
126
|
+
if len(sys.argv) < 2:
|
|
127
|
+
print(f"用法: AUTO_REGISTRY=true python3 registry.py <文件路径> [名称]")
|
|
128
|
+
print(f"环境: WORKSPACE_NAME={WORKSPACE}, AUTO_REGISTRY={AUTO_REGISTRY}")
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
filepath = sys.argv[1]
|
|
132
|
+
name = sys.argv[2] if len(sys.argv) > 2 else None
|
|
133
|
+
register_asset(filepath, name)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|