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,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
+ 🖼️ **结果图片**:![image](IMAGE_URL)
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,9 @@
1
+ [project]
2
+ name = "sophnet-image-edit"
3
+ version = "1.0.0"
4
+ description = "Sophnet image editing (image-to-image) with task polling"
5
+ requires-python = ">=3.8"
6
+ dependencies = [
7
+ "requests>=2.28.0",
8
+ "sophnet-tools>=0.0.1",
9
+ ]
@@ -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()