sophhub 0.1.0
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/bin/sophhub.js +21 -0
- package/package.json +32 -0
- package/skills/VERSIONS.md +27 -0
- package/skills/builtin/clawhub/SKILL.md +77 -0
- package/skills/builtin/flight-booking/SKILL.md +288 -0
- package/skills/builtin/flight-booking/scripts/flight_booking.py +1232 -0
- package/skills/builtin/inventory-management/SKILL.md +241 -0
- package/skills/builtin/inventory-management/scripts/inventory.py +1844 -0
- package/skills/builtin/schedule-reminder/SKILL.md +619 -0
- package/skills/builtin/schedule-reminder/schedule_template.md +68 -0
- package/skills/builtin/schedule-reminder/scripts/append_event.py +204 -0
- package/skills/builtin/schedule-reminder/scripts/create_reminders.sh +163 -0
- package/skills/builtin/schedule-reminder/scripts/daily_activate.sh +175 -0
- package/skills/builtin/schedule-reminder/scripts/parse_schedule.py +704 -0
- package/skills/builtin/schedule-reminder/scripts/setup.sh +242 -0
- package/skills/builtin/schedule-reminder//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -0
- package/skills/builtin/skill-creator/SKILL.md +370 -0
- package/skills/builtin/skill-creator/license.txt +202 -0
- package/skills/builtin/skill-creator/scripts/init_skill.py +378 -0
- package/skills/builtin/skill-creator/scripts/package_skill.py +111 -0
- package/skills/builtin/skill-creator/scripts/quick_validate.py +101 -0
- package/skills/builtin/sophnet-customer-management/SKILL.md +271 -0
- package/skills/builtin/sophnet-customer-management/pyproject.toml +15 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__main__.py +5 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/cli.py +67 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/customer.py +60 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/export_file.py +18 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/import_file.py +15 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/reminder.py +26 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/schema.py +28 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/config.py +54 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/__init__.py +2 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/exporter.py +85 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/models.py +84 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/normalizer.py +144 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/parser.py +241 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/query.py +109 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/reminder.py +121 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/repository.py +397 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/schema.py +106 -0
- package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/service.py +565 -0
- package/skills/builtin/sophnet-customer-management/uv.lock +48 -0
- package/skills/builtin/sophnet-customized-marketing/SKILL.md +144 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/campaign-planning.md +187 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/content-generation.md +124 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/marketing-calendar.md +59 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/multi-channel-bundle.md +94 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/poster-generation.md +182 -0
- package/skills/builtin/sophnet-customized-marketing/playbooks/style-profile-workflow.md +103 -0
- package/skills/builtin/sophnet-customized-marketing/pyproject.toml +9 -0
- package/skills/builtin/sophnet-customized-marketing/references/campaign-mechanics.md +168 -0
- package/skills/builtin/sophnet-customized-marketing/references/content-safety.md +26 -0
- package/skills/builtin/sophnet-customized-marketing/references/marketing-date-checklist.md +99 -0
- package/skills/builtin/sophnet-customized-marketing/references/platform-writing-guidelines.md +88 -0
- package/skills/builtin/sophnet-customized-marketing/references/quality-checklist.md +44 -0
- package/skills/builtin/sophnet-customized-marketing/scripts/generate_poster.py +585 -0
- package/skills/builtin/sophnet-customized-marketing/scripts/style_profile.py +215 -0
- package/skills/builtin/sophnet-face-search/SKILL.md +115 -0
- package/skills/builtin/sophnet-face-search/pyproject.toml +11 -0
- package/skills/builtin/sophnet-face-search/scripts/face_search.py +336 -0
- package/skills/builtin/sophnet-face-search/uv.lock +508 -0
- package/skills/builtin/sophnet-image-edit/SKILL.md +140 -0
- package/skills/builtin/sophnet-image-edit/pyproject.toml +9 -0
- package/skills/builtin/sophnet-image-edit/scripts/edit_and_preview.sh +68 -0
- package/skills/builtin/sophnet-image-edit/scripts/edit_image.py +279 -0
- package/skills/builtin/sophnet-image-edit/uv.lock +234 -0
- package/skills/builtin/sophnet-image-generate/SKILL.md +62 -0
- package/skills/builtin/sophnet-image-generate/pyproject.toml +9 -0
- package/skills/builtin/sophnet-image-generate/scripts/generate_image.py +156 -0
- package/skills/builtin/sophnet-image-generate/uv.lock +234 -0
- package/skills/builtin/sophnet-image-ocr/SKILL.md +167 -0
- package/skills/builtin/sophnet-image-ocr/pyproject.toml +13 -0
- package/skills/builtin/sophnet-image-ocr/scripts/ocr.py +226 -0
- package/skills/builtin/sophnet-image-ocr/uv.lock +234 -0
- package/skills/builtin/sophnet-infinite-talk/SKILL.md +140 -0
- package/skills/builtin/sophnet-infinite-talk/pyproject.toml +9 -0
- package/skills/builtin/sophnet-infinite-talk/scripts/gen.py +172 -0
- package/skills/builtin/sophnet-oss/SKILL.md +109 -0
- package/skills/builtin/sophnet-oss/pyproject.toml +8 -0
- package/skills/builtin/sophnet-oss/scripts/upload_file.py +43 -0
- package/skills/builtin/sophnet-qa-install/SKILL.md +210 -0
- package/skills/builtin/sophnet-qa-install/pyproject.toml +6 -0
- package/skills/builtin/sophnet-qa-install/scripts/backup_md.py +35 -0
- package/skills/builtin/sophnet-qa-install/scripts/check_installed.py +143 -0
- package/skills/builtin/sophnet-qa-install/scripts/update_config.py +142 -0
- package/skills/builtin/sophnet-qa-install/scripts/update_md.py +73 -0
- package/skills/builtin/sophnet-training-install/SKILL.md +211 -0
- package/skills/builtin/sophnet-training-install/pyproject.toml +6 -0
- package/skills/builtin/sophnet-training-install/scripts/backup_md.py +35 -0
- package/skills/builtin/sophnet-training-install/scripts/check_installed.py +144 -0
- package/skills/builtin/sophnet-training-install/scripts/update_config.py +142 -0
- package/skills/builtin/sophnet-training-install/scripts/update_md.py +73 -0
- package/skills/builtin/sophnet-tts/SKILL.md +79 -0
- package/skills/builtin/sophnet-tts/pyproject.toml +9 -0
- package/skills/builtin/sophnet-tts/scripts/gen_tts.py +130 -0
- package/skills/builtin/sophnet-video-generate/SKILL.md +116 -0
- package/skills/builtin/sophnet-video-generate/scripts/gen_video.py +304 -0
- package/skills/builtin/video-understand/SKILL.md +79 -0
- package/skills/builtin/video-understand/scripts/video_understand.py +204 -0
- package/skills/builtin/weather/SKILL.md +112 -0
- package/skills/builtin/web-scraper/SKILL.md +101 -0
- package/skills/builtin/web-scraper/scripts/scrape.py +270 -0
- package/skills/builtin/website-builder/SKILL.md +266 -0
- package/skills/builtin/website-builder/scripts/deploy_site.sh +46 -0
- package/skills/store/didi-ride/SKILL.md +309 -0
- package/skills/store/didi-ride/_meta.json +6 -0
- package/skills/store/didi-ride/assets/PREFERENCE.md +58 -0
- package/skills/store/didi-ride/package.json +15 -0
- package/skills/store/didi-ride/references/api_references.md +171 -0
- package/skills/store/didi-ride/references/error_handling.md +68 -0
- package/skills/store/didi-ride/references/setup.md +73 -0
- package/skills/store/didi-ride/references/workflow.md +150 -0
- package/skills/store/flyai/SKILL.md +119 -0
- package/skills/store/flyai/references/fliggy-fast-search.md +53 -0
- package/skills/store/flyai/references/search-flight.md +89 -0
- package/skills/store/flyai/references/search-hotels.md +57 -0
- package/skills/store/flyai/references/search-poi.md +49 -0
- package/src/commands/download.js +103 -0
- package/src/commands/list.js +67 -0
- package/src/utils/config.js +24 -0
- package/src/utils/gitlab.js +67 -0
- package/src/utils/paths.js +19 -0
- package/src/utils/versions.js +38 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sophnet Video Generation Script
|
|
4
|
+
Supports text-to-video and image-to-video generation using Wan2.6 models.
|
|
5
|
+
"""
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import subprocess
|
|
9
|
+
import argparse
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
import requests
|
|
13
|
+
from typing import Optional
|
|
14
|
+
import sophnet_tools
|
|
15
|
+
|
|
16
|
+
# API Configuration
|
|
17
|
+
SOPH_API_KEY = sophnet_tools.get_api_key()
|
|
18
|
+
if SOPH_API_KEY is None:
|
|
19
|
+
print("错误: 未找到 Sophnet API Key,请确保已正确配置环境变量 SOPH_API_KEY")
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
GENERATE_URL = "https://www.sophnet.com/api/open-apis/projects/easyllms/videogenerator/generate"
|
|
22
|
+
|
|
23
|
+
def create_request(
|
|
24
|
+
prompt: Optional[str],
|
|
25
|
+
model: str = "Wan2.6-T2V",
|
|
26
|
+
negative_prompt: Optional[str] = None,
|
|
27
|
+
first_frame_url: Optional[str] = None,
|
|
28
|
+
last_frame_url: Optional[str] = None,
|
|
29
|
+
size: str = "1280x720",
|
|
30
|
+
duration: int = 5,
|
|
31
|
+
generate_audio: bool = True,
|
|
32
|
+
draft: bool = False
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""Build request body for video generation."""
|
|
35
|
+
content = []
|
|
36
|
+
|
|
37
|
+
# Add text content
|
|
38
|
+
if prompt:
|
|
39
|
+
text_content = {"type": "text", "text": prompt}
|
|
40
|
+
if negative_prompt:
|
|
41
|
+
text_content["negative_prompt"] = negative_prompt
|
|
42
|
+
content.append(text_content)
|
|
43
|
+
|
|
44
|
+
# Add first frame
|
|
45
|
+
if first_frame_url:
|
|
46
|
+
content.append({
|
|
47
|
+
"type": "image_url",
|
|
48
|
+
"image_url": {"url": first_frame_url},
|
|
49
|
+
"role": "first_frame"
|
|
50
|
+
})
|
|
51
|
+
if model == "Wan2.6-T2V":
|
|
52
|
+
# If user specified T2V but provided an image, switch to I2V
|
|
53
|
+
model = "Wan2.6-I2V"
|
|
54
|
+
|
|
55
|
+
# Add last frame
|
|
56
|
+
if last_frame_url:
|
|
57
|
+
content.append({
|
|
58
|
+
"type": "image_url",
|
|
59
|
+
"image_url": {"url": last_frame_url},
|
|
60
|
+
"role": "last_frame"
|
|
61
|
+
})
|
|
62
|
+
else:
|
|
63
|
+
model = "Wan2.6-T2V"
|
|
64
|
+
|
|
65
|
+
# Build parameters
|
|
66
|
+
parameters = {
|
|
67
|
+
"size": size.replace("x", "*"),
|
|
68
|
+
"duration": duration
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {"model": model, "content": content, "parameters": parameters}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def make_request(req_data: dict) -> dict:
|
|
75
|
+
"""Make HTTP request with authentication."""
|
|
76
|
+
headers = {
|
|
77
|
+
"Authorization": f"Bearer {SOPH_API_KEY}",
|
|
78
|
+
"Content-Type": "application/json"
|
|
79
|
+
}
|
|
80
|
+
try:
|
|
81
|
+
response = requests.request(
|
|
82
|
+
"POST",
|
|
83
|
+
GENERATE_URL,
|
|
84
|
+
data=json.dumps(req_data).encode("utf-8"),
|
|
85
|
+
headers=headers,
|
|
86
|
+
timeout=60
|
|
87
|
+
)
|
|
88
|
+
if response.status_code == 200:
|
|
89
|
+
return response.json()
|
|
90
|
+
else:
|
|
91
|
+
print(f"HTTP请求失败,状态码: {response.status_code}, 响应内容: {response.text}")
|
|
92
|
+
return None
|
|
93
|
+
except requests.exceptions.RequestException as e:
|
|
94
|
+
print(f"HTTP请求超时: {e}")
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def make_get_request(task_id: str) -> dict:
|
|
99
|
+
"""Make GET request with authentication."""
|
|
100
|
+
headers = {
|
|
101
|
+
"Authorization": f"Bearer {SOPH_API_KEY}",
|
|
102
|
+
"Content-Type": "application/json"
|
|
103
|
+
}
|
|
104
|
+
url = f"{GENERATE_URL}/{task_id}"
|
|
105
|
+
response = requests.request("GET", url, headers=headers, timeout=60)
|
|
106
|
+
if response.status_code == 200:
|
|
107
|
+
return response.json().get("result", {})
|
|
108
|
+
else:
|
|
109
|
+
print(f"HTTP请求失败,状态码: {response.status_code}, 响应内容: {response.text}")
|
|
110
|
+
return None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def is_url(path: str) -> bool:
|
|
114
|
+
"""Check if the input string is a URL."""
|
|
115
|
+
return path.startswith(("http://", "https://"))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# 使用浏览器式请求头,避免 CDN/OSS 因 User-Agent 拒绝下载
|
|
119
|
+
DOWNLOAD_HEADERS = {
|
|
120
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
|
121
|
+
"Accept": "*/*",
|
|
122
|
+
"Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_file_with_requests(url: str, result_name: str, result_path: str) -> bool:
|
|
127
|
+
"""使用 requests 下载文件(带浏览器请求头,避免被服务器拒绝)。"""
|
|
128
|
+
try:
|
|
129
|
+
out_dir = os.path.expanduser(result_path)
|
|
130
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
131
|
+
out_path = os.path.join(out_dir, result_name)
|
|
132
|
+
resp = requests.get(url, headers=DOWNLOAD_HEADERS, timeout=30, stream=True)
|
|
133
|
+
resp.raise_for_status()
|
|
134
|
+
with open(out_path, "wb") as f:
|
|
135
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
136
|
+
if chunk:
|
|
137
|
+
f.write(chunk)
|
|
138
|
+
return os.path.isfile(out_path) and os.path.getsize(out_path) > 0
|
|
139
|
+
except requests.exceptions.RequestException as e:
|
|
140
|
+
print(f"requests 下载失败: {e}")
|
|
141
|
+
return False
|
|
142
|
+
except Exception as e:
|
|
143
|
+
print(f"下载过程中发生异常: {e}")
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def get_file_with_wget(url: str, result_name: str, result_path: str) -> bool:
|
|
148
|
+
"""Download file using wget (with browser User-Agent)."""
|
|
149
|
+
try:
|
|
150
|
+
os.makedirs(os.path.expanduser(result_path), exist_ok=True)
|
|
151
|
+
out_file = os.path.join(os.path.expanduser(result_path), result_name)
|
|
152
|
+
user_agent = DOWNLOAD_HEADERS["User-Agent"]
|
|
153
|
+
result = subprocess.run(
|
|
154
|
+
["wget", "-O", out_file,
|
|
155
|
+
"--tries=1", "--timeout=30",
|
|
156
|
+
f"--user-agent={user_agent}",
|
|
157
|
+
url],
|
|
158
|
+
capture_output=True,
|
|
159
|
+
text=True
|
|
160
|
+
)
|
|
161
|
+
if result.returncode == 0 and os.path.isfile(out_file) and os.path.getsize(out_file) > 0:
|
|
162
|
+
return True
|
|
163
|
+
else:
|
|
164
|
+
print(f"wget 下载失败,返回码: {result.returncode}, 标准输出: {result.stdout}, 标准错误: {result.stderr}")
|
|
165
|
+
return False
|
|
166
|
+
except Exception as e:
|
|
167
|
+
print(f"执行 wget 过程中发生异常: {e}")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def gen_video(
|
|
172
|
+
prompt: str,
|
|
173
|
+
model: str = "Wan2.6-T2V",
|
|
174
|
+
size: str = "1280*720",
|
|
175
|
+
duration: int = 5,
|
|
176
|
+
first_frame: Optional[str] = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Generate video using Sophnet Video Generation API."""
|
|
179
|
+
result_video_url = None
|
|
180
|
+
input_image = None
|
|
181
|
+
local_video_url = None
|
|
182
|
+
result_path = "/home/node/.openclaw/workspace/media/inbound/videos/"
|
|
183
|
+
# Handle first frame
|
|
184
|
+
if first_frame:
|
|
185
|
+
if is_url(first_frame):
|
|
186
|
+
input_image = first_frame
|
|
187
|
+
elif os.path.isfile(first_frame):
|
|
188
|
+
input_image = sophnet_tools.upload_oss(first_frame)
|
|
189
|
+
if input_image is None:
|
|
190
|
+
print("上传第一帧图片失败")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
# Build request
|
|
193
|
+
request_body = create_request(
|
|
194
|
+
model=model,
|
|
195
|
+
prompt=prompt,
|
|
196
|
+
first_frame_url=input_image,
|
|
197
|
+
size=size,
|
|
198
|
+
duration=duration,
|
|
199
|
+
)
|
|
200
|
+
print(json.dumps(request_body, indent=2))
|
|
201
|
+
|
|
202
|
+
request = make_request(request_body)
|
|
203
|
+
if request is None:
|
|
204
|
+
return result_video_url, local_video_url
|
|
205
|
+
if request.get("status") != 0:
|
|
206
|
+
print(f"视频生成失败,错误信息: {request.get('message', '未知错误')}")
|
|
207
|
+
return result_video_url, local_video_url
|
|
208
|
+
task_id = request.get("result", {}).get("task_id")
|
|
209
|
+
if not task_id:
|
|
210
|
+
print("视频生成失败,未返回任务ID")
|
|
211
|
+
return result_video_url
|
|
212
|
+
print(f"视频生成请求已提交,任务ID: {task_id}")
|
|
213
|
+
# Poll for completion
|
|
214
|
+
time_start = time.time()
|
|
215
|
+
while True:
|
|
216
|
+
request = make_get_request(task_id)
|
|
217
|
+
if request is None:
|
|
218
|
+
print("获取任务状态失败")
|
|
219
|
+
return result_video_url, local_video_url
|
|
220
|
+
|
|
221
|
+
if request.get("status") == "succeeded":
|
|
222
|
+
video_url = request.get("content", {}).get("video_url")
|
|
223
|
+
if video_url:
|
|
224
|
+
if get_file_with_requests(video_url, result_name=f"{task_id}.mp4", result_path=result_path):
|
|
225
|
+
local_video_url = os.path.join(os.path.expanduser(result_path), f"{task_id}.mp4")
|
|
226
|
+
# Re-upload to get a persistent URL
|
|
227
|
+
result_video_url = sophnet_tools.upload_oss(local_video_url)
|
|
228
|
+
if result_video_url:
|
|
229
|
+
return result_video_url, local_video_url
|
|
230
|
+
else:
|
|
231
|
+
return video_url, local_video_url
|
|
232
|
+
else:
|
|
233
|
+
return video_url, local_video_url
|
|
234
|
+
else:
|
|
235
|
+
print(f"视频生成失败")
|
|
236
|
+
return None, local_video_url # Return the direct URL if download fails
|
|
237
|
+
else:
|
|
238
|
+
print(f"当前状态: {request}")
|
|
239
|
+
if request.get("status", "").find("failed") != -1:
|
|
240
|
+
print(f"视频生成失败,错误信息: {request.get('status', '未知错误')}")
|
|
241
|
+
return result_video_url, local_video_url
|
|
242
|
+
print(f"视频生成中,当前状态: {request.get('status', '未知状态')}")
|
|
243
|
+
if time.time() - time_start > 300: # 5分钟超时
|
|
244
|
+
print("视频生成超时")
|
|
245
|
+
return result_video_url, local_video_url
|
|
246
|
+
time.sleep(5)
|
|
247
|
+
|
|
248
|
+
if __name__ == "__main__":
|
|
249
|
+
supported_models = ["Wan2.6-T2V", "Wan2.6-I2V", "ViduQ2-pro", "Seedance-1.5-Pro"]
|
|
250
|
+
parser = argparse.ArgumentParser(
|
|
251
|
+
description="Generate videos using Sophnet Video Generation API"
|
|
252
|
+
)
|
|
253
|
+
parser.add_argument("--prompt", help="Text prompt (required for text-to-video)")
|
|
254
|
+
parser.add_argument("--model", type=str, default="Wan2.6-T2V",
|
|
255
|
+
help="Model name. Defaults to Wan2.6-I2V if image provided, otherwise Wan2.6-T2V")
|
|
256
|
+
parser.add_argument("--size", type=str, default="1280*720", help="Resolution (default: 1280*720)")
|
|
257
|
+
parser.add_argument("--duration", type=int, default=5, help="Video duration in seconds (default: 5)")
|
|
258
|
+
parser.add_argument("--first-frame", type=str, help="First frame image URL or path")
|
|
259
|
+
parser.add_argument("--generate-audio", action="store_true", help="Generate audio")
|
|
260
|
+
args = parser.parse_args()
|
|
261
|
+
|
|
262
|
+
# Auto-select model based on whether first frame is provided
|
|
263
|
+
model = args.model
|
|
264
|
+
if args.first_frame and model.find("Wan2.6") != -1:
|
|
265
|
+
# If user specified T2V but provided an image, switch to I2V
|
|
266
|
+
model = "Wan2.6-I2V"
|
|
267
|
+
elif args.first_frame is None and model.find("Wan2.6") != -1:
|
|
268
|
+
# If no image provided, ensure using T2V
|
|
269
|
+
model = "Wan2.6-T2V"
|
|
270
|
+
elif model.find("ViduQ2") != -1:
|
|
271
|
+
model = "ViduQ2-pro"
|
|
272
|
+
elif model.find("Seedance") != -1:
|
|
273
|
+
model = "Seedance-1.5-Pro"
|
|
274
|
+
elif model not in supported_models:
|
|
275
|
+
if args.first_frame:
|
|
276
|
+
print(f"警告: 模型 '{args.model}' 不受支持,已自动切换到 Wan2.6-I2V")
|
|
277
|
+
model = "Wan2.6-I2V"
|
|
278
|
+
else:
|
|
279
|
+
print(f"警告: 模型 '{args.model}' 不受支持,已自动切换到 Wan2.6-T2V")
|
|
280
|
+
model = "Wan2.6-T2V"
|
|
281
|
+
|
|
282
|
+
result_url, local_video_url = gen_video(
|
|
283
|
+
args.prompt,
|
|
284
|
+
model,
|
|
285
|
+
args.size,
|
|
286
|
+
args.duration,
|
|
287
|
+
args.first_frame,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
if result_url:
|
|
291
|
+
print(f"\n您的视频提示词为: {args.prompt}")
|
|
292
|
+
print(f"视频生成成功: {result_url}")
|
|
293
|
+
print(f"视频本地保存路径: {local_video_url}")
|
|
294
|
+
print(f"提示: 视频保存24小时,请及时下载")
|
|
295
|
+
|
|
296
|
+
# 生成支持的模型列表(排除当前使用的模型)
|
|
297
|
+
other_models = [m for m in supported_models if m != model]
|
|
298
|
+
other_models_str = "、".join(other_models)
|
|
299
|
+
print(f"当前使用的模型为 {model},还支持 {other_models_str}")
|
|
300
|
+
print(f"如果想要指定模型,可以直接对我说:使用 [模型名称] 模型,生成 [视频描述]")
|
|
301
|
+
print(f"提示: 模型名称可以是: Wan2.6-T2V、Wan2.6-I2V、ViduQ2-pro 或 Seedance-1.5-Pro")
|
|
302
|
+
else:
|
|
303
|
+
print("\n视频生成失败")
|
|
304
|
+
sys.exit(1)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-understand
|
|
3
|
+
description: Analyze and understand video content using Qwen3-VL vision model. Use when the user provides a video URL or a local video file path and wants to understand, describe, summarize, or answer questions about the video content. Triggers on requests like "What happens in this video?", "Describe this video", "Summarize the video at this URL", or any task requiring visual comprehension of video material.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Video Understand
|
|
7
|
+
|
|
8
|
+
Analyze video content by sending a video URL to the Qwen3-VL-235B-A22B-Instruct model via OpenAI-compatible API. Supports scene description, content summarization, action recognition, Q&A, and any visual comprehension task. Accepts both video URLs and local file paths (local files are automatically uploaded to OSS).
|
|
9
|
+
|
|
10
|
+
## Quick Start
|
|
11
|
+
|
|
12
|
+
Run the bundled script with a video URL or local file and a prompt:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
# Using a video URL
|
|
16
|
+
python3 {baseDir}/scripts/video_understand.py \
|
|
17
|
+
--video-url "https://example.com/video.mp4" \
|
|
18
|
+
--prompt "Describe what happens in this video"
|
|
19
|
+
|
|
20
|
+
# Using a local video file
|
|
21
|
+
python3 {baseDir}/scripts/video_understand.py \
|
|
22
|
+
--video-file "/path/to/local/video.mp4" \
|
|
23
|
+
--prompt "Describe what happens in this video"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The script reads API credentials from the environment or the openclaw config automatically.
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Basic Video Description
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv run {baseDir}/scripts/video_understand.py \
|
|
34
|
+
--video-url "https://example.com/video.mp4" \
|
|
35
|
+
--prompt "详细描述这个视频的内容"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Analyze a Local Video File
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv run {baseDir}/scripts/video_understand.py \
|
|
42
|
+
--video-file "/home/node/.openclaw/workspace/my_video.mp4" \
|
|
43
|
+
--prompt "详细描述这个视频的内容"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Video Q&A
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv run {baseDir}/scripts/video_understand.py \
|
|
50
|
+
--video-url "https://example.com/demo.mp4" \
|
|
51
|
+
--prompt "How many people appear in the video and what are they doing?"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Parameters
|
|
55
|
+
|
|
56
|
+
| Parameter | Required | Default | Description |
|
|
57
|
+
| -------------- | ---------------- | ------- | ------------------------------------------------------- |
|
|
58
|
+
| `--video-url` | Yes (either one) | — | Public URL of the video to analyze |
|
|
59
|
+
| `--video-file` | Yes (either one) | — | Local video file path (auto-uploaded to OSS to get URL) |
|
|
60
|
+
| `--prompt` | Yes | — | Question or instruction |
|
|
61
|
+
| `--base-url` | No | auto | Override API base URL |
|
|
62
|
+
| `--api-key` | No | auto | Override API key |
|
|
63
|
+
|
|
64
|
+
`--video-url` and `--video-file` are mutually exclusive — provide exactly one of them.
|
|
65
|
+
|
|
66
|
+
## Model Details
|
|
67
|
+
|
|
68
|
+
- **Model**: `Qwen3-VL-235B-A22B-Instruct` via provider `sophnet`
|
|
69
|
+
- **API**: OpenAI-compatible completions endpoint
|
|
70
|
+
- **Input**: Video URL + text prompt in multimodal message format
|
|
71
|
+
- **Context window**: 128,000 tokens
|
|
72
|
+
- **Max output**: 8,192 tokens
|
|
73
|
+
|
|
74
|
+
## Notes
|
|
75
|
+
|
|
76
|
+
- When using `--video-url`, the video must be accessible via a public URL.
|
|
77
|
+
- When using `--video-file`, the file will be uploaded to OSS and a signed URL will be generated automatically.
|
|
78
|
+
- Supported video formats depend on the model backend; common formats (mp4, webm, mov) are generally supported.
|
|
79
|
+
- The script auto-detects credentials from `~/.openclaw/openclaw.json`. Override with `--base-url` and `--api-key` if needed.
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Analyze video content via Qwen3-VL model using OpenAI-compatible API."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
|
|
11
|
+
DEFAULT_MODEL = "Qwen3-VL-235B-A22B-Instruct"
|
|
12
|
+
OPENCLAW_CONFIG = os.path.expanduser("/home/node/.openclaw/openclaw.json")
|
|
13
|
+
UPLOAD_URL = "https://www.sophnet.com/api/open-apis/projects/upload"
|
|
14
|
+
|
|
15
|
+
def _build_multipart(file_path: str) -> tuple:
|
|
16
|
+
"""Build multipart/form-data body for file upload. Returns (body_bytes, content_type)."""
|
|
17
|
+
boundary = f"----PythonFormBoundary{os.urandom(16).hex()}"
|
|
18
|
+
file_name = os.path.basename(file_path)
|
|
19
|
+
|
|
20
|
+
lines = []
|
|
21
|
+
lines.append(f"--{boundary}")
|
|
22
|
+
lines.append(f'Content-Disposition: form-data; name="file"; filename="{file_name}"')
|
|
23
|
+
lines.append("Content-Type: application/octet-stream")
|
|
24
|
+
lines.append("")
|
|
25
|
+
|
|
26
|
+
header = "\r\n".join(lines).encode("utf-8") + b"\r\n"
|
|
27
|
+
footer = f"\r\n--{boundary}--\r\n".encode("utf-8")
|
|
28
|
+
|
|
29
|
+
with open(file_path, "rb") as f:
|
|
30
|
+
file_data = f.read()
|
|
31
|
+
|
|
32
|
+
body = header + file_data + footer
|
|
33
|
+
content_type = f"multipart/form-data; boundary={boundary}"
|
|
34
|
+
return body, content_type
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def upload_oss(file_path: str, api_key: str = None) -> str:
|
|
38
|
+
"""Upload a file to OSS and return the signed URL."""
|
|
39
|
+
if not os.path.isfile(file_path):
|
|
40
|
+
print(f"错误: 文件 '{file_path}' 不存在或不是一个文件。", file=sys.stderr)
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
if not api_key:
|
|
43
|
+
_, api_key = load_openclaw_config()
|
|
44
|
+
|
|
45
|
+
if not api_key:
|
|
46
|
+
print("错误: 上传需要 API key,请提供 --api-key 或配置 ~/.openclaw/openclaw.json",
|
|
47
|
+
file=sys.stderr)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
body, content_type = _build_multipart(file_path)
|
|
52
|
+
except IOError as e:
|
|
53
|
+
print(f"错误: 无法读取文件 - {e}", file=sys.stderr)
|
|
54
|
+
sys.exit(1)
|
|
55
|
+
|
|
56
|
+
req = urllib.request.Request(
|
|
57
|
+
UPLOAD_URL,
|
|
58
|
+
data=body,
|
|
59
|
+
headers={
|
|
60
|
+
"Authorization": f"Bearer {api_key}",
|
|
61
|
+
"Content-Type": content_type,
|
|
62
|
+
},
|
|
63
|
+
method="POST",
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
with urllib.request.urlopen(req, timeout=120) as resp:
|
|
68
|
+
resp_data = resp.read().decode("utf-8")
|
|
69
|
+
except urllib.error.HTTPError as e:
|
|
70
|
+
resp_body = e.read().decode("utf-8", errors="replace")
|
|
71
|
+
print(f"上传失败: HTTP {e.code}", file=sys.stderr)
|
|
72
|
+
print(f"服务器响应: {resp_body}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
except urllib.error.URLError as e:
|
|
75
|
+
print(f"错误: 网络请求异常 - {e.reason}", file=sys.stderr)
|
|
76
|
+
sys.exit(1)
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
json_data = json.loads(resp_data)
|
|
80
|
+
except ValueError:
|
|
81
|
+
print("错误: 服务器返回的不是有效的 JSON 格式", file=sys.stderr)
|
|
82
|
+
print(f"响应内容: {resp_data[:200]}...", file=sys.stderr)
|
|
83
|
+
sys.exit(1)
|
|
84
|
+
|
|
85
|
+
result = json_data.get("result")
|
|
86
|
+
if not result or not isinstance(result, dict):
|
|
87
|
+
print("错误: 响应中缺少 'result' 字段或格式错误", file=sys.stderr)
|
|
88
|
+
sys.exit(1)
|
|
89
|
+
|
|
90
|
+
signed_url = result.get("signedUrl")
|
|
91
|
+
if not signed_url:
|
|
92
|
+
print("错误: 未返回有效的 signedUrl", file=sys.stderr)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
|
|
95
|
+
return signed_url
|
|
96
|
+
|
|
97
|
+
def load_openclaw_config():
|
|
98
|
+
"""Load API credentials from openclaw.json."""
|
|
99
|
+
if not os.path.exists(OPENCLAW_CONFIG):
|
|
100
|
+
return None, None
|
|
101
|
+
with open(OPENCLAW_CONFIG, "r") as f:
|
|
102
|
+
config = json.load(f)
|
|
103
|
+
providers = config.get("models", {}).get("providers", {})
|
|
104
|
+
sophnet = providers.get("sophnet", {})
|
|
105
|
+
return sophnet.get("baseUrl"), sophnet.get("apiKey")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def call_video_understand(video_url, prompt, base_url=None, api_key=None):
|
|
109
|
+
if not base_url or not api_key:
|
|
110
|
+
cfg_url, cfg_key = load_openclaw_config()
|
|
111
|
+
base_url = base_url or cfg_url
|
|
112
|
+
api_key = api_key or cfg_key
|
|
113
|
+
|
|
114
|
+
if not base_url or not api_key:
|
|
115
|
+
print("Error: API base URL and key are required. "
|
|
116
|
+
"Provide --base-url/--api-key or configure ~/.openclaw/openclaw.json",
|
|
117
|
+
file=sys.stderr)
|
|
118
|
+
sys.exit(1)
|
|
119
|
+
|
|
120
|
+
endpoint = f"{base_url.rstrip('/')}/chat/completions"
|
|
121
|
+
|
|
122
|
+
payload = {
|
|
123
|
+
"model": DEFAULT_MODEL,
|
|
124
|
+
"messages": [
|
|
125
|
+
{
|
|
126
|
+
"role": "user",
|
|
127
|
+
"content": [
|
|
128
|
+
{
|
|
129
|
+
"type": "video_url",
|
|
130
|
+
"video_url": {"url": video_url}
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"type": "text",
|
|
134
|
+
"text": prompt
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
data = json.dumps(payload).encode("utf-8")
|
|
142
|
+
req = urllib.request.Request(
|
|
143
|
+
endpoint,
|
|
144
|
+
data=data,
|
|
145
|
+
headers={
|
|
146
|
+
"Content-Type": "application/json",
|
|
147
|
+
"Authorization": f"Bearer {api_key}"
|
|
148
|
+
},
|
|
149
|
+
method="POST"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
try:
|
|
153
|
+
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
154
|
+
result = json.loads(resp.read().decode("utf-8"))
|
|
155
|
+
except urllib.error.HTTPError as e:
|
|
156
|
+
body = e.read().decode("utf-8", errors="replace")
|
|
157
|
+
print(f"API error {e.code}: {body}", file=sys.stderr)
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
except urllib.error.URLError as e:
|
|
160
|
+
print(f"Request failed: {e.reason}", file=sys.stderr)
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
choices = result.get("choices", [])
|
|
164
|
+
if not choices:
|
|
165
|
+
print("No response from model.", file=sys.stderr)
|
|
166
|
+
sys.exit(1)
|
|
167
|
+
|
|
168
|
+
content = choices[0].get("message", {}).get("content", "")
|
|
169
|
+
print(content)
|
|
170
|
+
|
|
171
|
+
# usage = result.get("usage")
|
|
172
|
+
# if usage:
|
|
173
|
+
# print(f"\n--- Token usage: prompt={usage.get('prompt_tokens', '?')}, "
|
|
174
|
+
# f"completion={usage.get('completion_tokens', '?')}, "
|
|
175
|
+
# f"total={usage.get('total_tokens', '?')} ---",
|
|
176
|
+
# file=sys.stderr)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def main():
|
|
180
|
+
parser = argparse.ArgumentParser(description="Analyze video content with Qwen3-VL")
|
|
181
|
+
video_source = parser.add_mutually_exclusive_group(required=True)
|
|
182
|
+
video_source.add_argument("--video-url", help="Public URL of the video")
|
|
183
|
+
video_source.add_argument("--video-file", help="Local path to a video file (will be uploaded to OSS first)")
|
|
184
|
+
parser.add_argument("--prompt", required=True, help="Question or instruction about the video")
|
|
185
|
+
parser.add_argument("--base-url", default=None, help="Override API base URL")
|
|
186
|
+
parser.add_argument("--api-key", default=None, help="Override API key")
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
|
|
189
|
+
video_url = args.video_url
|
|
190
|
+
if args.video_file:
|
|
191
|
+
print(f"正在上传视频文件: {args.video_file} ...", file=sys.stderr)
|
|
192
|
+
video_url = upload_oss(args.video_file, api_key=args.api_key)
|
|
193
|
+
print(f"上传完成,获取到视频 URL{video_url}", file=sys.stderr)
|
|
194
|
+
|
|
195
|
+
call_video_understand(
|
|
196
|
+
video_url=video_url,
|
|
197
|
+
prompt=args.prompt,
|
|
198
|
+
base_url=args.base_url,
|
|
199
|
+
api_key=args.api_key,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: weather
|
|
3
|
+
description: "Get current weather and forecasts via wttr.in or Open-Meteo. Use when: user asks about weather, temperature, or forecasts for any location. NOT for: historical weather data, severe weather alerts, or detailed meteorological analysis. No API key needed."
|
|
4
|
+
homepage: https://wttr.in/:help
|
|
5
|
+
metadata: { "openclaw": { "emoji": "🌤️", "requires": { "bins": ["curl"] } } }
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Weather Skill
|
|
9
|
+
|
|
10
|
+
Get current weather conditions and forecasts.
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
✅ **USE this skill when:**
|
|
15
|
+
|
|
16
|
+
- "What's the weather?"
|
|
17
|
+
- "Will it rain today/tomorrow?"
|
|
18
|
+
- "Temperature in [city]"
|
|
19
|
+
- "Weather forecast for the week"
|
|
20
|
+
- Travel planning weather checks
|
|
21
|
+
|
|
22
|
+
## When NOT to Use
|
|
23
|
+
|
|
24
|
+
❌ **DON'T use this skill when:**
|
|
25
|
+
|
|
26
|
+
- Historical weather data → use weather archives/APIs
|
|
27
|
+
- Climate analysis or trends → use specialized data sources
|
|
28
|
+
- Hyper-local microclimate data → use local sensors
|
|
29
|
+
- Severe weather alerts → check official NWS sources
|
|
30
|
+
- Aviation/marine weather → use specialized services (METAR, etc.)
|
|
31
|
+
|
|
32
|
+
## Location
|
|
33
|
+
|
|
34
|
+
Always include a city, region, or airport code in weather queries.
|
|
35
|
+
|
|
36
|
+
## Commands
|
|
37
|
+
|
|
38
|
+
### Current Weather
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# One-line summary
|
|
42
|
+
curl "wttr.in/London?format=3"
|
|
43
|
+
|
|
44
|
+
# Detailed current conditions
|
|
45
|
+
curl "wttr.in/London?0"
|
|
46
|
+
|
|
47
|
+
# Specific city
|
|
48
|
+
curl "wttr.in/New+York?format=3"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Forecasts
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 3-day forecast
|
|
55
|
+
curl "wttr.in/London"
|
|
56
|
+
|
|
57
|
+
# Week forecast
|
|
58
|
+
curl "wttr.in/London?format=v2"
|
|
59
|
+
|
|
60
|
+
# Specific day (0=today, 1=tomorrow, 2=day after)
|
|
61
|
+
curl "wttr.in/London?1"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Format Options
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# One-liner
|
|
68
|
+
curl "wttr.in/London?format=%l:+%c+%t+%w"
|
|
69
|
+
|
|
70
|
+
# JSON output
|
|
71
|
+
curl "wttr.in/London?format=j1"
|
|
72
|
+
|
|
73
|
+
# PNG image
|
|
74
|
+
curl "wttr.in/London.png"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Format Codes
|
|
78
|
+
|
|
79
|
+
- `%c` — Weather condition emoji
|
|
80
|
+
- `%t` — Temperature
|
|
81
|
+
- `%f` — "Feels like"
|
|
82
|
+
- `%w` — Wind
|
|
83
|
+
- `%h` — Humidity
|
|
84
|
+
- `%p` — Precipitation
|
|
85
|
+
- `%l` — Location
|
|
86
|
+
|
|
87
|
+
## Quick Responses
|
|
88
|
+
|
|
89
|
+
**"What's the weather?"**
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
curl -s "wttr.in/London?format=%l:+%c+%t+(feels+like+%f),+%w+wind,+%h+humidity"
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**"Will it rain?"**
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
curl -s "wttr.in/London?format=%l:+%c+%p"
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**"Weekend forecast"**
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
curl "wttr.in/London?format=v2"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Notes
|
|
108
|
+
|
|
109
|
+
- No API key needed (uses wttr.in)
|
|
110
|
+
- Rate limited; don't spam requests
|
|
111
|
+
- Works for most global cities
|
|
112
|
+
- Supports airport codes: `curl wttr.in/ORD`
|