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.
Files changed (125) hide show
  1. package/bin/sophhub.js +21 -0
  2. package/package.json +32 -0
  3. package/skills/VERSIONS.md +27 -0
  4. package/skills/builtin/clawhub/SKILL.md +77 -0
  5. package/skills/builtin/flight-booking/SKILL.md +288 -0
  6. package/skills/builtin/flight-booking/scripts/flight_booking.py +1232 -0
  7. package/skills/builtin/inventory-management/SKILL.md +241 -0
  8. package/skills/builtin/inventory-management/scripts/inventory.py +1844 -0
  9. package/skills/builtin/schedule-reminder/SKILL.md +619 -0
  10. package/skills/builtin/schedule-reminder/schedule_template.md +68 -0
  11. package/skills/builtin/schedule-reminder/scripts/append_event.py +204 -0
  12. package/skills/builtin/schedule-reminder/scripts/create_reminders.sh +163 -0
  13. package/skills/builtin/schedule-reminder/scripts/daily_activate.sh +175 -0
  14. package/skills/builtin/schedule-reminder/scripts/parse_schedule.py +704 -0
  15. package/skills/builtin/schedule-reminder/scripts/setup.sh +242 -0
  16. package/skills/builtin/schedule-reminder//347/224/250/346/210/267/346/214/207/345/215/227.md +311 -0
  17. package/skills/builtin/skill-creator/SKILL.md +370 -0
  18. package/skills/builtin/skill-creator/license.txt +202 -0
  19. package/skills/builtin/skill-creator/scripts/init_skill.py +378 -0
  20. package/skills/builtin/skill-creator/scripts/package_skill.py +111 -0
  21. package/skills/builtin/skill-creator/scripts/quick_validate.py +101 -0
  22. package/skills/builtin/sophnet-customer-management/SKILL.md +271 -0
  23. package/skills/builtin/sophnet-customer-management/pyproject.toml +15 -0
  24. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__init__.py +2 -0
  25. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/__main__.py +5 -0
  26. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/cli.py +67 -0
  27. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/__init__.py +2 -0
  28. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/customer.py +60 -0
  29. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/export_file.py +18 -0
  30. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/import_file.py +15 -0
  31. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/reminder.py +26 -0
  32. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/commands/schema.py +28 -0
  33. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_cli/config.py +54 -0
  34. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/__init__.py +2 -0
  35. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/exporter.py +85 -0
  36. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/models.py +84 -0
  37. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/normalizer.py +144 -0
  38. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/parser.py +241 -0
  39. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/query.py +109 -0
  40. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/reminder.py +121 -0
  41. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/repository.py +397 -0
  42. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/schema.py +106 -0
  43. package/skills/builtin/sophnet-customer-management/src/customer_mgmt_core/service.py +565 -0
  44. package/skills/builtin/sophnet-customer-management/uv.lock +48 -0
  45. package/skills/builtin/sophnet-customized-marketing/SKILL.md +144 -0
  46. package/skills/builtin/sophnet-customized-marketing/playbooks/campaign-planning.md +187 -0
  47. package/skills/builtin/sophnet-customized-marketing/playbooks/content-generation.md +124 -0
  48. package/skills/builtin/sophnet-customized-marketing/playbooks/marketing-calendar.md +59 -0
  49. package/skills/builtin/sophnet-customized-marketing/playbooks/multi-channel-bundle.md +94 -0
  50. package/skills/builtin/sophnet-customized-marketing/playbooks/poster-generation.md +182 -0
  51. package/skills/builtin/sophnet-customized-marketing/playbooks/style-profile-workflow.md +103 -0
  52. package/skills/builtin/sophnet-customized-marketing/pyproject.toml +9 -0
  53. package/skills/builtin/sophnet-customized-marketing/references/campaign-mechanics.md +168 -0
  54. package/skills/builtin/sophnet-customized-marketing/references/content-safety.md +26 -0
  55. package/skills/builtin/sophnet-customized-marketing/references/marketing-date-checklist.md +99 -0
  56. package/skills/builtin/sophnet-customized-marketing/references/platform-writing-guidelines.md +88 -0
  57. package/skills/builtin/sophnet-customized-marketing/references/quality-checklist.md +44 -0
  58. package/skills/builtin/sophnet-customized-marketing/scripts/generate_poster.py +585 -0
  59. package/skills/builtin/sophnet-customized-marketing/scripts/style_profile.py +215 -0
  60. package/skills/builtin/sophnet-face-search/SKILL.md +115 -0
  61. package/skills/builtin/sophnet-face-search/pyproject.toml +11 -0
  62. package/skills/builtin/sophnet-face-search/scripts/face_search.py +336 -0
  63. package/skills/builtin/sophnet-face-search/uv.lock +508 -0
  64. package/skills/builtin/sophnet-image-edit/SKILL.md +140 -0
  65. package/skills/builtin/sophnet-image-edit/pyproject.toml +9 -0
  66. package/skills/builtin/sophnet-image-edit/scripts/edit_and_preview.sh +68 -0
  67. package/skills/builtin/sophnet-image-edit/scripts/edit_image.py +279 -0
  68. package/skills/builtin/sophnet-image-edit/uv.lock +234 -0
  69. package/skills/builtin/sophnet-image-generate/SKILL.md +62 -0
  70. package/skills/builtin/sophnet-image-generate/pyproject.toml +9 -0
  71. package/skills/builtin/sophnet-image-generate/scripts/generate_image.py +156 -0
  72. package/skills/builtin/sophnet-image-generate/uv.lock +234 -0
  73. package/skills/builtin/sophnet-image-ocr/SKILL.md +167 -0
  74. package/skills/builtin/sophnet-image-ocr/pyproject.toml +13 -0
  75. package/skills/builtin/sophnet-image-ocr/scripts/ocr.py +226 -0
  76. package/skills/builtin/sophnet-image-ocr/uv.lock +234 -0
  77. package/skills/builtin/sophnet-infinite-talk/SKILL.md +140 -0
  78. package/skills/builtin/sophnet-infinite-talk/pyproject.toml +9 -0
  79. package/skills/builtin/sophnet-infinite-talk/scripts/gen.py +172 -0
  80. package/skills/builtin/sophnet-oss/SKILL.md +109 -0
  81. package/skills/builtin/sophnet-oss/pyproject.toml +8 -0
  82. package/skills/builtin/sophnet-oss/scripts/upload_file.py +43 -0
  83. package/skills/builtin/sophnet-qa-install/SKILL.md +210 -0
  84. package/skills/builtin/sophnet-qa-install/pyproject.toml +6 -0
  85. package/skills/builtin/sophnet-qa-install/scripts/backup_md.py +35 -0
  86. package/skills/builtin/sophnet-qa-install/scripts/check_installed.py +143 -0
  87. package/skills/builtin/sophnet-qa-install/scripts/update_config.py +142 -0
  88. package/skills/builtin/sophnet-qa-install/scripts/update_md.py +73 -0
  89. package/skills/builtin/sophnet-training-install/SKILL.md +211 -0
  90. package/skills/builtin/sophnet-training-install/pyproject.toml +6 -0
  91. package/skills/builtin/sophnet-training-install/scripts/backup_md.py +35 -0
  92. package/skills/builtin/sophnet-training-install/scripts/check_installed.py +144 -0
  93. package/skills/builtin/sophnet-training-install/scripts/update_config.py +142 -0
  94. package/skills/builtin/sophnet-training-install/scripts/update_md.py +73 -0
  95. package/skills/builtin/sophnet-tts/SKILL.md +79 -0
  96. package/skills/builtin/sophnet-tts/pyproject.toml +9 -0
  97. package/skills/builtin/sophnet-tts/scripts/gen_tts.py +130 -0
  98. package/skills/builtin/sophnet-video-generate/SKILL.md +116 -0
  99. package/skills/builtin/sophnet-video-generate/scripts/gen_video.py +304 -0
  100. package/skills/builtin/video-understand/SKILL.md +79 -0
  101. package/skills/builtin/video-understand/scripts/video_understand.py +204 -0
  102. package/skills/builtin/weather/SKILL.md +112 -0
  103. package/skills/builtin/web-scraper/SKILL.md +101 -0
  104. package/skills/builtin/web-scraper/scripts/scrape.py +270 -0
  105. package/skills/builtin/website-builder/SKILL.md +266 -0
  106. package/skills/builtin/website-builder/scripts/deploy_site.sh +46 -0
  107. package/skills/store/didi-ride/SKILL.md +309 -0
  108. package/skills/store/didi-ride/_meta.json +6 -0
  109. package/skills/store/didi-ride/assets/PREFERENCE.md +58 -0
  110. package/skills/store/didi-ride/package.json +15 -0
  111. package/skills/store/didi-ride/references/api_references.md +171 -0
  112. package/skills/store/didi-ride/references/error_handling.md +68 -0
  113. package/skills/store/didi-ride/references/setup.md +73 -0
  114. package/skills/store/didi-ride/references/workflow.md +150 -0
  115. package/skills/store/flyai/SKILL.md +119 -0
  116. package/skills/store/flyai/references/fliggy-fast-search.md +53 -0
  117. package/skills/store/flyai/references/search-flight.md +89 -0
  118. package/skills/store/flyai/references/search-hotels.md +57 -0
  119. package/skills/store/flyai/references/search-poi.md +49 -0
  120. package/src/commands/download.js +103 -0
  121. package/src/commands/list.js +67 -0
  122. package/src/utils/config.js +24 -0
  123. package/src/utils/gitlab.js +67 -0
  124. package/src/utils/paths.js +19 -0
  125. 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`