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,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sophnet-image-edit
|
|
3
|
+
description: Use when a user requests Sophnet image editing (image-to-image), including multi-image editing tasks (composition, style transfer, region swap) where all uploaded image paths must be resolved from Media Understanding logs (usually media/inbound/images/*) and passed in order to model Qwen-Image-Edit-2509.
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Edit Sophnet images with strict multi-image path/order handling
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Sophnet Image Edit
|
|
9
|
+
|
|
10
|
+
## Overview
|
|
11
|
+
|
|
12
|
+
Edit existing images with Python scripts that handle task polling and structured output.
|
|
13
|
+
|
|
14
|
+
Script responsibilities:
|
|
15
|
+
|
|
16
|
+
- `edit_image.py`: core API caller and polling loop, outputs `INPUT_IMAGE_COUNT`, `TASK_ID`, `STATUS`, and `IMAGE_URL`.
|
|
17
|
+
- `edit_and_preview.sh`: wrapper for local use, calls `edit_image.py`, downloads first result, adds `PREVIEW_PATH`.
|
|
18
|
+
|
|
19
|
+
## When to Use
|
|
20
|
+
|
|
21
|
+
- User asks to edit or transform existing images with Sophnet.
|
|
22
|
+
- Task uses one or more source images (URL or uploaded local files).
|
|
23
|
+
- Task depends on image order/role, such as:
|
|
24
|
+
- put part of image-2 into image-1
|
|
25
|
+
- render image-2 with image-1 style
|
|
26
|
+
- blend/composite many images into one result
|
|
27
|
+
- Do not use when the task is pure text-to-image generation; use `sophnet-image-generate`.
|
|
28
|
+
|
|
29
|
+
## Image Path Resolution
|
|
30
|
+
|
|
31
|
+
When users upload images in chat channels, files are usually saved under `media/inbound/images/` in the workspace. Use Media Understanding logs to get exact resolved paths.
|
|
32
|
+
|
|
33
|
+
Typical log pattern:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
[Media Understanding] Resolved relative path: "media/inbound/images/xxx.jpg" -> "/absolute/path/to/workspace/media/inbound/images/xxx.jpg"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Path rules:
|
|
40
|
+
|
|
41
|
+
- `--image` accepts URL, data URI, or local file path.
|
|
42
|
+
- Local file paths are auto-converted to data URI by the script.
|
|
43
|
+
- For multi-image tasks, pass every image explicitly and in required order.
|
|
44
|
+
|
|
45
|
+
## Multi-Image Rules
|
|
46
|
+
|
|
47
|
+
1. Keep source image order stable and explicit.
|
|
48
|
+
2. Bind prompt language to order (image-1, image-2, image-3...).
|
|
49
|
+
3. Do not drop "reference" images; pass all images used by the prompt.
|
|
50
|
+
4. Verify script output `INPUT_IMAGE_COUNT` equals expected image count before trusting results.
|
|
51
|
+
|
|
52
|
+
Recommended prompt pattern for multi-image tasks:
|
|
53
|
+
|
|
54
|
+
- `图1作为底图,图2提供主体元素,图3提供风格参考;将图2主体抠出放到图1右上角,并按图3风格整体渲染。`
|
|
55
|
+
|
|
56
|
+
## Quick Reference
|
|
57
|
+
|
|
58
|
+
| Goal | Command |
|
|
59
|
+
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
60
|
+
| Edit with one source | `bash {baseDir}/scripts/edit_and_preview.sh --prompt "..." --image "https://example.com/source.jpg"` |
|
|
61
|
+
| Edit with two sources (ordered) | `bash {baseDir}/scripts/edit_and_preview.sh --prompt "图1作为底图,将图2主体放入图1左下角" --image "media/inbound/images/img1.jpg" --image "media/inbound/images/img2.jpg"` |
|
|
62
|
+
| Style transfer across two images | `uv run --project {baseDir} python {baseDir}/scripts/edit_image.py --prompt "使用图1的画风渲染图2内容" --image "/abs/path/style.jpg" --image "/abs/path/content.jpg"` |
|
|
63
|
+
| Many images via CSV | `uv run --project {baseDir} python {baseDir}/scripts/edit_image.py --prompt "..." --images "media/inbound/images/a.jpg,media/inbound/images/b.jpg,https://example.com/c.jpg"` |
|
|
64
|
+
| Many images via list file | `uv run --project {baseDir} python {baseDir}/scripts/edit_image.py --prompt "..." --images-file "/tmp/image-list.txt"` |
|
|
65
|
+
| Validate image count only | `uv run --project {baseDir} python {baseDir}/scripts/edit_image.py --prompt "..." --images-file "/tmp/image-list.txt" --dry-run` |
|
|
66
|
+
| Show options | `uv run --project {baseDir} python {baseDir}/scripts/edit_image.py --help` |
|
|
67
|
+
|
|
68
|
+
Notes:
|
|
69
|
+
|
|
70
|
+
- `--dry-run` does not require API key and is for input-count verification only.
|
|
71
|
+
- Prefer `--images-file` when handling many images or values that may contain commas.
|
|
72
|
+
|
|
73
|
+
`/tmp/image-list.txt` format example:
|
|
74
|
+
|
|
75
|
+
```text
|
|
76
|
+
# One image reference per line
|
|
77
|
+
media/inbound/images/base.jpg
|
|
78
|
+
media/inbound/images/object.png
|
|
79
|
+
https://example.com/style.jpg
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Script Output Fields
|
|
83
|
+
|
|
84
|
+
The script outputs structured key=value lines. Parse all of them:
|
|
85
|
+
|
|
86
|
+
| Field | Description |
|
|
87
|
+
| ------------------- | ------------------------------------------------------------ |
|
|
88
|
+
| `INPUT_IMAGE_COUNT` | Number of source images passed to the API |
|
|
89
|
+
| `TASK_ID` | API task identifier |
|
|
90
|
+
| `STATUS` | `succeeded` or `failed` |
|
|
91
|
+
| `OUTPUT_COUNT` | Number of result images |
|
|
92
|
+
| `IMAGE_URL` | Publicly accessible signed URL for each result image |
|
|
93
|
+
| `PREVIEW_PATH` | Local file path (only when OSS re-upload failed as fallback) |
|
|
94
|
+
|
|
95
|
+
## Presenting Results to Users
|
|
96
|
+
|
|
97
|
+
After a successful edit, present the results with context -- not just a bare URL. Include:
|
|
98
|
+
|
|
99
|
+
1. What was done: mention the edit instruction / style applied.
|
|
100
|
+
2. Input/output image counts.
|
|
101
|
+
3. The result image URL(s): provide the `IMAGE_URL` so the user can view/download.
|
|
102
|
+
4. If `PREVIEW_PATH` is present, mention the local file path.
|
|
103
|
+
|
|
104
|
+
Example response pattern:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
✅ 图片编辑完成!
|
|
108
|
+
|
|
109
|
+
🎨 **编辑指令**:把图片变成水彩画风格
|
|
110
|
+
🧾 **输入图片数**:1
|
|
111
|
+
🖼️ **结果图片**:
|
|
112
|
+
⚠️**提示**: 本内容有AI生成,图片保存24小时,请及时下载!
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Implementation
|
|
116
|
+
|
|
117
|
+
1. Resolve all uploaded image paths from Media Understanding logs.
|
|
118
|
+
2. If this edit step follows `sophnet-image-generate`, prefer upstream `IMAGE_URL` for handoff; use `PREVIEW_PATH` only when local-file input is explicitly intended.
|
|
119
|
+
3. Build prompt with explicit image order semantics.
|
|
120
|
+
4. Run script with one `--image` per image, or `--images`, or `--images-file`.
|
|
121
|
+
5. For multi-image tasks, run once with `--dry-run` and confirm `INPUT_IMAGE_COUNT` matches intended image count.
|
|
122
|
+
6. Parse ALL output fields (see table above) and present them to the user.
|
|
123
|
+
7. Use `PREVIEW_PATH` when present.
|
|
124
|
+
|
|
125
|
+
API payload shape:
|
|
126
|
+
|
|
127
|
+
- `model`: `Qwen-Image-Edit-2509`
|
|
128
|
+
- `input.prompt`: edit instruction with image-role semantics
|
|
129
|
+
- `input.images[]`: all source image refs in strict order
|
|
130
|
+
- `parameters`: `size`, `n`, `watermark`
|
|
131
|
+
|
|
132
|
+
## Common Mistakes
|
|
133
|
+
|
|
134
|
+
- Passing only one image for a multi-image prompt.
|
|
135
|
+
- Mentioning image-2/image-3 in prompt but not passing those images.
|
|
136
|
+
- Reordering `--image` arguments unintentionally.
|
|
137
|
+
- Ignoring `INPUT_IMAGE_COUNT` mismatch.
|
|
138
|
+
- Using non-existent local paths.
|
|
139
|
+
- In generate→edit workflows, defaulting to `PREVIEW_PATH` instead of `IMAGE_URL` without reason.
|
|
140
|
+
- Only showing a bare URL without context -- always present a summary with edit details.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# Edit image and download the first result for preview.
|
|
4
|
+
# Wrapper: calls edit_image.py via uv, downloads first result.
|
|
5
|
+
#
|
|
6
|
+
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
+
SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
+
EDIT_SCRIPT="${SCRIPT_DIR}/edit_image.py"
|
|
12
|
+
|
|
13
|
+
if [[ ! -f "$EDIT_SCRIPT" ]]; then
|
|
14
|
+
echo "Error: edit_image.py not found at ${EDIT_SCRIPT}" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
if ! command -v curl >/dev/null 2>&1; then
|
|
19
|
+
echo "Error: curl not found." >&2
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
for arg in "$@"; do
|
|
24
|
+
if [[ "$arg" == "-h" || "$arg" == "--help" ]]; then
|
|
25
|
+
uv run --project "$SKILL_DIR" python "$EDIT_SCRIPT" --help
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
done
|
|
29
|
+
|
|
30
|
+
if ! output="$(uv run --project "$SKILL_DIR" python "$EDIT_SCRIPT" "$@" 2>&1)"; then
|
|
31
|
+
echo "$output" >&2
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
echo "$output"
|
|
36
|
+
|
|
37
|
+
# Python already downloads and re-uploads the image; check if PREVIEW_PATH was produced
|
|
38
|
+
preview_path="$(printf '%s\n' "$output" | awk '/^PREVIEW_PATH=/{print substr($0,14); exit}')"
|
|
39
|
+
|
|
40
|
+
if [[ -n "$preview_path" && -f "$preview_path" ]]; then
|
|
41
|
+
exit 0
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# Fallback: download via curl if Python didn't produce a local file
|
|
45
|
+
image_url="$(printf '%s\n' "$output" | awk '/^IMAGE_URL=/{print substr($0,11); exit}')"
|
|
46
|
+
|
|
47
|
+
if [[ -z "$image_url" ]]; then
|
|
48
|
+
echo "Error: No image URL found in output" >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
task_id="$(printf '%s\n' "$output" | awk '/^TASK_ID=/{print substr($0,9); exit}')"
|
|
53
|
+
: "${task_id:=$(date +%s)}"
|
|
54
|
+
|
|
55
|
+
path_no_query="${image_url%%\?*}"
|
|
56
|
+
ext="${path_no_query##*.}"
|
|
57
|
+
case "$ext" in
|
|
58
|
+
jpg|jpeg|png|gif|webp|bmp) ;;
|
|
59
|
+
*) ext="png" ;;
|
|
60
|
+
esac
|
|
61
|
+
temp_file="$(mktemp "/tmp/edited_${task_id}_XXXXXX.${ext}")"
|
|
62
|
+
|
|
63
|
+
if curl -fsSL "$image_url" -o "$temp_file"; then
|
|
64
|
+
echo "PREVIEW_PATH=${temp_file}"
|
|
65
|
+
else
|
|
66
|
+
rm -f "$temp_file"
|
|
67
|
+
echo "Warning: Failed to download image for preview" >&2
|
|
68
|
+
fi
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Sophnet image editing (image-to-image) with async task polling.
|
|
4
|
+
Supports multiple source images via --image, --images (CSV), or --images-file.
|
|
5
|
+
Outputs machine-friendly INPUT_IMAGE_COUNT, TASK_ID, STATUS, and IMAGE_URL lines.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import mimetypes
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
import tempfile
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import requests
|
|
18
|
+
import sophnet_tools
|
|
19
|
+
|
|
20
|
+
API_URL = "https://www.sophnet.com/api/open-apis/projects/easyllms/imagegenerator/task"
|
|
21
|
+
VALID_MODELS = ["Qwen-Image-Edit-2509"]
|
|
22
|
+
|
|
23
|
+
MIME_MAP = {
|
|
24
|
+
".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
25
|
+
".png": "image/png", ".webp": "image/webp",
|
|
26
|
+
".gif": "image/gif", ".bmp": "image/bmp",
|
|
27
|
+
".avif": "image/avif",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_bool(value):
|
|
32
|
+
if value.lower() in ("true", "1", "yes", "y"):
|
|
33
|
+
return True
|
|
34
|
+
if value.lower() in ("false", "0", "no", "n"):
|
|
35
|
+
return False
|
|
36
|
+
raise argparse.ArgumentTypeError(f"invalid boolean value: {value!r}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def is_remote_ref(ref):
|
|
40
|
+
return ref.startswith(("http://", "https://", "data:image/"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def path_to_data_uri(path):
|
|
44
|
+
ext = os.path.splitext(path)[1].lower()
|
|
45
|
+
mime = MIME_MAP.get(ext)
|
|
46
|
+
if not mime:
|
|
47
|
+
mime, _ = mimetypes.guess_type(path)
|
|
48
|
+
if not mime:
|
|
49
|
+
mime = "application/octet-stream"
|
|
50
|
+
with open(path, "rb") as f:
|
|
51
|
+
b64 = base64.b64encode(f.read()).decode("ascii")
|
|
52
|
+
return f"data:{mime};base64,{b64}"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def resolve_image(raw):
|
|
56
|
+
ref = raw.lstrip("@")
|
|
57
|
+
if is_remote_ref(ref):
|
|
58
|
+
return ref
|
|
59
|
+
if os.path.isfile(ref):
|
|
60
|
+
return path_to_data_uri(ref)
|
|
61
|
+
print(f"Error: --image input not found: {raw}", file=sys.stderr)
|
|
62
|
+
print("Hint: for uploaded files, use the resolved path from Media Understanding logs, "
|
|
63
|
+
"usually under media/inbound/images/.", file=sys.stderr)
|
|
64
|
+
sys.exit(1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def load_images_csv(csv_str):
|
|
68
|
+
items = []
|
|
69
|
+
for item in csv_str.split(","):
|
|
70
|
+
item = item.strip()
|
|
71
|
+
if item:
|
|
72
|
+
items.append(item)
|
|
73
|
+
return items
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_images_file(path):
|
|
77
|
+
if not os.path.isfile(path):
|
|
78
|
+
print(f"Error: --images-file not found: {path}", file=sys.stderr)
|
|
79
|
+
sys.exit(1)
|
|
80
|
+
items = []
|
|
81
|
+
with open(path, "r") as f:
|
|
82
|
+
for line in f:
|
|
83
|
+
line = line.strip().rstrip("\r")
|
|
84
|
+
if not line or line.startswith("#"):
|
|
85
|
+
continue
|
|
86
|
+
items.append(line)
|
|
87
|
+
return items
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_task(api_key, prompt, model, image_refs,
|
|
91
|
+
size="1024*1024", n=1, watermark=False):
|
|
92
|
+
resolved = [resolve_image(ref) for ref in image_refs]
|
|
93
|
+
payload = {
|
|
94
|
+
"model": model,
|
|
95
|
+
"input": {"prompt": prompt, "images": resolved},
|
|
96
|
+
"parameters": {
|
|
97
|
+
"size": size,
|
|
98
|
+
"n": n,
|
|
99
|
+
"watermark": watermark,
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
headers = {
|
|
104
|
+
"Authorization": f"Bearer {api_key}",
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
}
|
|
107
|
+
resp = requests.post(API_URL, json=payload, headers=headers, timeout=120)
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
data = resp.json()
|
|
110
|
+
|
|
111
|
+
for key in ("task_id", "taskId", "taskID", "id"):
|
|
112
|
+
if key in data:
|
|
113
|
+
return data[key]
|
|
114
|
+
if isinstance(data.get("output"), dict) and key in data["output"]:
|
|
115
|
+
return data["output"][key]
|
|
116
|
+
|
|
117
|
+
print("Error: task_id not found in response.", file=sys.stderr)
|
|
118
|
+
print(json.dumps(data, ensure_ascii=False), file=sys.stderr)
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def poll_task(api_key, task_id, poll_interval=2, max_wait=300):
|
|
123
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
124
|
+
start = time.time()
|
|
125
|
+
|
|
126
|
+
while True:
|
|
127
|
+
elapsed = time.time() - start
|
|
128
|
+
if elapsed > max_wait:
|
|
129
|
+
print(f"Error: timed out after {max_wait}s.", file=sys.stderr)
|
|
130
|
+
sys.exit(1)
|
|
131
|
+
|
|
132
|
+
resp = requests.get(f"{API_URL}/{task_id}", headers=headers, timeout=60)
|
|
133
|
+
resp.raise_for_status()
|
|
134
|
+
data = resp.json()
|
|
135
|
+
|
|
136
|
+
status = None
|
|
137
|
+
for key in ("status", "taskStatus", "task_status"):
|
|
138
|
+
if key in data:
|
|
139
|
+
status = data[key]
|
|
140
|
+
break
|
|
141
|
+
if isinstance(data.get("output"), dict) and key in data["output"]:
|
|
142
|
+
status = data["output"][key]
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
if status and status.lower() in ("succeeded", "success"):
|
|
146
|
+
return data
|
|
147
|
+
if status and status.lower() in ("failed", "error"):
|
|
148
|
+
print("STATUS=failed", file=sys.stderr)
|
|
149
|
+
print(json.dumps(data, ensure_ascii=False), file=sys.stderr)
|
|
150
|
+
sys.exit(1)
|
|
151
|
+
|
|
152
|
+
time.sleep(poll_interval)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def extract_urls(data):
|
|
156
|
+
urls = []
|
|
157
|
+
|
|
158
|
+
def _walk(obj):
|
|
159
|
+
if isinstance(obj, dict):
|
|
160
|
+
if "url" in obj and isinstance(obj["url"], str):
|
|
161
|
+
urls.append(obj["url"])
|
|
162
|
+
for v in obj.values():
|
|
163
|
+
_walk(v)
|
|
164
|
+
elif isinstance(obj, list):
|
|
165
|
+
for item in obj:
|
|
166
|
+
_walk(item)
|
|
167
|
+
|
|
168
|
+
_walk(data)
|
|
169
|
+
return urls
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def reupload_for_signed_url(api_key, raw_url):
|
|
173
|
+
"""Download from raw API URL (using API auth) and re-upload
|
|
174
|
+
to Sophnet OSS to obtain a publicly accessible signed URL.
|
|
175
|
+
Returns (signed_url, local_path) or (None, None) on failure."""
|
|
176
|
+
headers = {"Authorization": f"Bearer {api_key}"}
|
|
177
|
+
try:
|
|
178
|
+
resp = requests.get(raw_url, headers=headers, timeout=120, stream=True)
|
|
179
|
+
resp.raise_for_status()
|
|
180
|
+
except requests.RequestException as e:
|
|
181
|
+
print(f"Warning: failed to download image: {e}", file=sys.stderr)
|
|
182
|
+
return None, None
|
|
183
|
+
|
|
184
|
+
ext = os.path.splitext(raw_url.split("?")[0])[-1] or ".png"
|
|
185
|
+
fd, tmp_path = tempfile.mkstemp(suffix=ext, prefix="img_reup_")
|
|
186
|
+
try:
|
|
187
|
+
with os.fdopen(fd, "wb") as f:
|
|
188
|
+
for chunk in resp.iter_content(8192):
|
|
189
|
+
f.write(chunk)
|
|
190
|
+
except IOError as e:
|
|
191
|
+
print(f"Warning: failed to write temp file: {e}", file=sys.stderr)
|
|
192
|
+
os.unlink(tmp_path)
|
|
193
|
+
return None, None
|
|
194
|
+
|
|
195
|
+
signed_url = sophnet_tools.upload_oss(tmp_path)
|
|
196
|
+
if not signed_url:
|
|
197
|
+
print("Warning: upload_oss returned no signed URL, keeping local file", file=sys.stderr)
|
|
198
|
+
return None, tmp_path
|
|
199
|
+
|
|
200
|
+
return signed_url, tmp_path
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class AppendImageAction(argparse.Action):
|
|
204
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
205
|
+
items = getattr(namespace, self.dest, None) or []
|
|
206
|
+
items.append(values)
|
|
207
|
+
setattr(namespace, self.dest, items)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def main():
|
|
211
|
+
parser = argparse.ArgumentParser(description="Sophnet image editing")
|
|
212
|
+
parser.add_argument("--prompt", required=True, help="Edit instruction prompt")
|
|
213
|
+
parser.add_argument("--image", dest="images", action=AppendImageAction, default=[],
|
|
214
|
+
help="Source image (URL, data URI, or local path). Repeatable.")
|
|
215
|
+
parser.add_argument("--images", dest="images_csv", default=None,
|
|
216
|
+
help="Comma-separated image refs")
|
|
217
|
+
parser.add_argument("--images-file", default=None,
|
|
218
|
+
help="Read image refs from file, one per line")
|
|
219
|
+
parser.add_argument("--model", default="Qwen-Image-Edit-2509",
|
|
220
|
+
choices=VALID_MODELS, help="Model name")
|
|
221
|
+
parser.add_argument("--size", default="1024*1024", help="Image size")
|
|
222
|
+
parser.add_argument("--n", type=int, default=1, help="Number of outputs")
|
|
223
|
+
parser.add_argument("--watermark", type=parse_bool, default=False)
|
|
224
|
+
parser.add_argument("--poll-interval", type=int, default=2)
|
|
225
|
+
parser.add_argument("--max-wait", type=int, default=300)
|
|
226
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
227
|
+
help="Validate and print input count only")
|
|
228
|
+
args = parser.parse_args()
|
|
229
|
+
|
|
230
|
+
all_images = list(args.images)
|
|
231
|
+
if args.images_csv:
|
|
232
|
+
all_images.extend(load_images_csv(args.images_csv))
|
|
233
|
+
if args.images_file:
|
|
234
|
+
all_images.extend(load_images_file(args.images_file))
|
|
235
|
+
|
|
236
|
+
if not all_images:
|
|
237
|
+
print("Error: at least one --image is required.", file=sys.stderr)
|
|
238
|
+
sys.exit(1)
|
|
239
|
+
|
|
240
|
+
if args.dry_run:
|
|
241
|
+
print(f"INPUT_IMAGE_COUNT={len(all_images)}")
|
|
242
|
+
print("STATUS=dry_run")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
api_key = sophnet_tools.get_api_key()
|
|
246
|
+
if not api_key:
|
|
247
|
+
print("Error: No API key found.", file=sys.stderr)
|
|
248
|
+
sys.exit(1)
|
|
249
|
+
|
|
250
|
+
task_id = create_task(
|
|
251
|
+
api_key, args.prompt, args.model, all_images,
|
|
252
|
+
size=args.size, n=args.n, watermark=args.watermark,
|
|
253
|
+
)
|
|
254
|
+
print(f"INPUT_IMAGE_COUNT={len(all_images)}")
|
|
255
|
+
print(f"TASK_ID={task_id}")
|
|
256
|
+
|
|
257
|
+
result = poll_task(api_key, task_id, args.poll_interval, args.max_wait)
|
|
258
|
+
|
|
259
|
+
print("STATUS=succeeded")
|
|
260
|
+
|
|
261
|
+
raw_urls = extract_urls(result)
|
|
262
|
+
if not raw_urls:
|
|
263
|
+
print("Error: url not found in response.", file=sys.stderr)
|
|
264
|
+
print(json.dumps(result, ensure_ascii=False), file=sys.stderr)
|
|
265
|
+
sys.exit(1)
|
|
266
|
+
|
|
267
|
+
print(f"OUTPUT_COUNT={len(raw_urls)}")
|
|
268
|
+
for i, raw_url in enumerate(raw_urls, 1):
|
|
269
|
+
signed_url, local_path = reupload_for_signed_url(api_key, raw_url)
|
|
270
|
+
print(f"IMAGE_URL={signed_url or raw_url}")
|
|
271
|
+
if local_path:
|
|
272
|
+
if signed_url:
|
|
273
|
+
os.unlink(local_path)
|
|
274
|
+
else:
|
|
275
|
+
print(f"PREVIEW_PATH={local_path}")
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
if __name__ == "__main__":
|
|
279
|
+
main()
|