imagepipeline 0.3.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ """ImagePipeline Python SDK"""
2
+
3
+ from .client import ImagePipeline
4
+ from .models import Job, JobStatus, SegmentItem, SegmentResult, UploadResult
5
+
6
+ __all__ = ["ImagePipeline", "Job", "JobStatus", "UploadResult", "SegmentItem", "SegmentResult"]
7
+ __version__ = "0.3.0"
@@ -0,0 +1,114 @@
1
+ """Low-level HTTP transport shared by all resource classes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any, Dict, IO, Optional
7
+
8
+ import requests as _requests
9
+
10
+ from .exceptions import (
11
+ APIError,
12
+ AuthenticationError,
13
+ JobFailedError,
14
+ JobTimeoutError,
15
+ RateLimitError,
16
+ )
17
+ from .models import Job, JobStatus
18
+
19
+ _DEFAULT_BASE_URL = "https://api.imagepipeline.io"
20
+ _DEFAULT_POLL_INTERVAL = 3 # seconds
21
+ _DEFAULT_TIMEOUT = 300 # seconds
22
+ _SDK_VERSION = "0.3.0"
23
+
24
+
25
+ class _Transport:
26
+ """Wraps requests.Session with auth headers and error handling."""
27
+
28
+ def __init__(self, api_key: str, base_url: str, timeout: int):
29
+ self._base_url = base_url.rstrip("/")
30
+ self._timeout = timeout
31
+ self._session = _requests.Session()
32
+ self._session.headers.update({
33
+ "X-API-Key": api_key,
34
+ "Content-Type": "application/json",
35
+ "User-Agent": f"imagepipeline-python/{_SDK_VERSION}",
36
+ })
37
+
38
+ def post(self, path: str, body: dict) -> dict:
39
+ url = f"{self._base_url}{path}"
40
+ resp = self._session.post(url, json=body, timeout=self._timeout)
41
+ return self._handle(resp)
42
+
43
+ def get(self, path: str) -> dict:
44
+ url = f"{self._base_url}{path}"
45
+ resp = self._session.get(url, timeout=self._timeout)
46
+ return self._handle(resp)
47
+
48
+ def post_file(self, path: str, file_obj: IO[bytes], filename: str, content_type: str) -> dict:
49
+ """POST multipart/form-data — used by the upload endpoint."""
50
+ url = f"{self._base_url}{path}"
51
+ # Remove Content-Type so requests sets multipart boundary automatically
52
+ headers = {k: v for k, v in self._session.headers.items() if k.lower() != "content-type"}
53
+ resp = self._session.post(
54
+ url,
55
+ files={"file": (filename, file_obj, content_type)},
56
+ headers=headers,
57
+ timeout=self._timeout,
58
+ )
59
+ return self._handle(resp)
60
+
61
+ def _handle(self, resp: _requests.Response) -> dict:
62
+ if resp.status_code == 401:
63
+ raise AuthenticationError("Invalid or missing API key.")
64
+ if resp.status_code == 429:
65
+ retry_after = int(resp.headers.get("Retry-After", 60))
66
+ raise RateLimitError("Rate limit exceeded.", retry_after=retry_after)
67
+ if resp.status_code == 204:
68
+ return {}
69
+ if not resp.ok:
70
+ try:
71
+ detail = resp.json().get("detail") or resp.text
72
+ except Exception:
73
+ detail = resp.text
74
+ raise APIError(resp.status_code, detail)
75
+ return resp.json()
76
+
77
+ def submit_and_poll(
78
+ self,
79
+ endpoint: str,
80
+ body: dict,
81
+ poll_interval: int = _DEFAULT_POLL_INTERVAL,
82
+ timeout: int = _DEFAULT_TIMEOUT,
83
+ ) -> Job:
84
+ """POST to endpoint, then poll /status/{job_id} until done."""
85
+ data = self.post(f"/{endpoint}", body)
86
+ job_id = data["job_id"]
87
+ return self.poll(endpoint, job_id, poll_interval=poll_interval, timeout=timeout)
88
+
89
+ def submit(self, endpoint: str, body: dict) -> Job:
90
+ """POST to endpoint and return immediately without polling."""
91
+ data = self.post(f"/{endpoint}", body)
92
+ return Job(
93
+ job_id=data["job_id"],
94
+ status=JobStatus(data.get("status", "queued")),
95
+ endpoint=endpoint,
96
+ )
97
+
98
+ def poll(
99
+ self,
100
+ endpoint: str,
101
+ job_id: str,
102
+ poll_interval: int = _DEFAULT_POLL_INTERVAL,
103
+ timeout: int = _DEFAULT_TIMEOUT,
104
+ ) -> Job:
105
+ deadline = time.monotonic() + timeout
106
+ while time.monotonic() < deadline:
107
+ data = self.get(f"/{endpoint}/status/{job_id}")
108
+ status = JobStatus(data.get("status", "queued"))
109
+ if status == JobStatus.COMPLETED:
110
+ return Job._from_status_response(data, endpoint)
111
+ if status == JobStatus.FAILED:
112
+ raise JobFailedError(job_id, data.get("error"))
113
+ time.sleep(poll_interval)
114
+ raise JobTimeoutError(job_id, timeout)
@@ -0,0 +1,88 @@
1
+ """Top-level ImagePipeline client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._transport import _Transport
6
+ from .resources import (
7
+ BackgroundResource,
8
+ BrandingResource,
9
+ EditResource,
10
+ GenerateResource,
11
+ IdentityResource,
12
+ SegmentResource,
13
+ UploadResource,
14
+ UpscaleResource,
15
+ )
16
+
17
+ _DEFAULT_BASE_URL = "https://api.imagepipeline.io"
18
+
19
+
20
+ class ImagePipeline:
21
+ """Client for the ImagePipeline API.
22
+
23
+ Usage::
24
+
25
+ from imagepipeline import ImagePipeline
26
+
27
+ ip = ImagePipeline("ip_live_xxxxxxxxxxxx")
28
+
29
+ # Generate an image
30
+ result = ip.generate.image(prompt="sunset over tokyo", width=1024, height=1024)
31
+ print(result.url)
32
+
33
+ # Upload a local file, then edit it
34
+ upload = ip.upload.image("product.png")
35
+ job = ip.edit.image(
36
+ input_image=[person_url, upload.url],
37
+ prompt="dress the person in the product from image 2",
38
+ mask_segment="upper-clothes",
39
+ )
40
+
41
+ # Virtual try-on
42
+ result = ip.identity.tryon(
43
+ person_image="https://.../person.jpg",
44
+ clothing_image="https://.../shirt.jpg",
45
+ gender="woman",
46
+ )
47
+
48
+ # Segment an image to find label names
49
+ seg = ip.segment.image("https://.../person.jpg")
50
+ print(seg.segments) # [SegmentItem(label='upper-clothes', display='Top / Shirt'), ...]
51
+
52
+ # Background change (subject is automatically preserved)
53
+ result = ip.background.change(
54
+ input_image="https://.../photo.jpg",
55
+ prompt="tropical beach at sunset",
56
+ )
57
+
58
+ # Manage identity profiles
59
+ profile = ip.identity.create_profile(
60
+ name="Brand Model",
61
+ prompt_template="{{ user_prompt }}, Caucasian woman, 20s, blue eyes",
62
+ seed_strategy="fixed",
63
+ fixed_seed=42,
64
+ )
65
+ print(profile["profile_id"])
66
+
67
+ Args:
68
+ api_key: Your API key (starts with ``ip_live_``).
69
+ base_url: Override the API base URL (useful for staging environments).
70
+ timeout: HTTP request timeout in seconds.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ api_key: str,
76
+ base_url: str = _DEFAULT_BASE_URL,
77
+ timeout: int = 30,
78
+ ):
79
+ self._transport = _Transport(api_key=api_key, base_url=base_url, timeout=timeout)
80
+
81
+ self.generate = GenerateResource(self._transport)
82
+ self.identity = IdentityResource(self._transport)
83
+ self.edit = EditResource(self._transport)
84
+ self.background = BackgroundResource(self._transport)
85
+ self.branding = BrandingResource(self._transport)
86
+ self.upscale = UpscaleResource(self._transport)
87
+ self.upload = UploadResource(self._transport)
88
+ self.segment = SegmentResource(self._transport)
@@ -0,0 +1,45 @@
1
+ """Exceptions raised by the ImagePipeline SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class ImagePipelineError(Exception):
7
+ """Base exception for all SDK errors."""
8
+
9
+
10
+ class AuthenticationError(ImagePipelineError):
11
+ """API key is missing or invalid (401)."""
12
+
13
+
14
+ class RateLimitError(ImagePipelineError):
15
+ """Rate limit exceeded (429). Retry after the indicated delay."""
16
+
17
+ def __init__(self, message: str, retry_after: int = 60):
18
+ super().__init__(message)
19
+ self.retry_after = retry_after
20
+
21
+
22
+ class JobFailedError(ImagePipelineError):
23
+ """The submitted job completed with status 'failed'."""
24
+
25
+ def __init__(self, job_id: str, reason: str | None = None):
26
+ super().__init__(f"Job {job_id} failed: {reason or 'unknown error'}")
27
+ self.job_id = job_id
28
+ self.reason = reason
29
+
30
+
31
+ class JobTimeoutError(ImagePipelineError):
32
+ """Polling timed out before the job completed."""
33
+
34
+ def __init__(self, job_id: str, timeout: int):
35
+ super().__init__(f"Job {job_id} did not complete within {timeout}s")
36
+ self.job_id = job_id
37
+ self.timeout = timeout
38
+
39
+
40
+ class APIError(ImagePipelineError):
41
+ """Unexpected HTTP error from the API."""
42
+
43
+ def __init__(self, status_code: int, message: str):
44
+ super().__init__(f"HTTP {status_code}: {message}")
45
+ self.status_code = status_code
@@ -0,0 +1,111 @@
1
+ """Core data models for the ImagePipeline SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from enum import Enum
7
+ from typing import Any, Dict, List, Optional
8
+
9
+
10
+ class JobStatus(str, Enum):
11
+ QUEUED = "queued"
12
+ PENDING = "pending"
13
+ PROCESSING = "processing"
14
+ COMPLETED = "completed"
15
+ FAILED = "failed"
16
+ CANCELLED = "cancelled"
17
+
18
+
19
+ @dataclass
20
+ class Job:
21
+ """Represents a submitted or completed job."""
22
+
23
+ job_id: str
24
+ status: JobStatus
25
+ endpoint: str
26
+ result_url: Optional[str] = None
27
+ error: Optional[str] = None
28
+
29
+ # Progress
30
+ progress: Optional[int] = None
31
+ result_mime_type: Optional[str] = None
32
+
33
+ # Timing
34
+ queue_wait_seconds: Optional[float] = None
35
+ inference_time_seconds: Optional[float] = None
36
+ total_elapsed_seconds: Optional[float] = None
37
+ estimated_time_seconds: Optional[int] = None
38
+ processing_time_seconds: Optional[float] = None
39
+
40
+ # Queue position (enterprise plans)
41
+ queue_position: Optional[int] = None
42
+ queue_metrics: Optional[Dict[str, Any]] = None
43
+ queue_metrics_hint: Optional[str] = None
44
+
45
+ # Failure info
46
+ failure_reason_code: Optional[str] = None
47
+ retryable: Optional[bool] = None
48
+ warnings: Optional[List[str]] = None
49
+ prompt_hash: Optional[str] = None
50
+
51
+ # Billing
52
+ cost_usd: Optional[float] = None
53
+ balance_remaining_usd: Optional[float] = None
54
+
55
+ # Background removal specific
56
+ cutout_url: Optional[str] = None
57
+
58
+ @property
59
+ def url(self) -> Optional[str]:
60
+ """Alias for result_url."""
61
+ return self.result_url
62
+
63
+ @classmethod
64
+ def _from_status_response(cls, data: dict, endpoint: str) -> "Job":
65
+ return cls(
66
+ job_id=data.get("job_id", ""),
67
+ status=JobStatus(data.get("status", "queued")),
68
+ endpoint=endpoint,
69
+ result_url=data.get("result_url"),
70
+ error=data.get("error"),
71
+ progress=data.get("progress"),
72
+ result_mime_type=data.get("result_mime_type"),
73
+ queue_wait_seconds=data.get("queue_wait_seconds"),
74
+ inference_time_seconds=data.get("inference_time_seconds"),
75
+ total_elapsed_seconds=data.get("total_elapsed_seconds"),
76
+ estimated_time_seconds=data.get("estimated_time_seconds"),
77
+ processing_time_seconds=data.get("processing_time_seconds"),
78
+ queue_position=data.get("queue_position"),
79
+ queue_metrics=data.get("queue_metrics"),
80
+ queue_metrics_hint=data.get("queue_metrics_hint"),
81
+ failure_reason_code=data.get("failure_reason_code"),
82
+ retryable=data.get("retryable"),
83
+ warnings=data.get("warnings"),
84
+ prompt_hash=data.get("prompt_hash"),
85
+ cost_usd=data.get("cost_usd"),
86
+ balance_remaining_usd=data.get("balance_remaining_usd"),
87
+ cutout_url=data.get("cutout_url"),
88
+ )
89
+
90
+
91
+ @dataclass
92
+ class UploadResult:
93
+ """Result of a file upload."""
94
+ url: str
95
+ filename: str
96
+ content_type: str
97
+ size_bytes: int
98
+
99
+
100
+ @dataclass
101
+ class SegmentItem:
102
+ """A single detected segment from segmentation."""
103
+ label: str
104
+ display: str
105
+
106
+
107
+ @dataclass
108
+ class SegmentResult:
109
+ """Result of image segmentation."""
110
+ preview_url: str
111
+ segments: List[SegmentItem]
imagepipeline/py.typed ADDED
File without changes
@@ -0,0 +1,23 @@
1
+ """Resource sub-package init."""
2
+
3
+ from .generate import GenerateResource
4
+ from .identity import IdentityResource
5
+ from .misc import (
6
+ BackgroundResource,
7
+ BrandingResource,
8
+ EditResource,
9
+ SegmentResource,
10
+ UploadResource,
11
+ UpscaleResource,
12
+ )
13
+
14
+ __all__ = [
15
+ "GenerateResource",
16
+ "IdentityResource",
17
+ "EditResource",
18
+ "BackgroundResource",
19
+ "BrandingResource",
20
+ "UpscaleResource",
21
+ "UploadResource",
22
+ "SegmentResource",
23
+ ]
@@ -0,0 +1,179 @@
1
+ """Generate namespace: image, video, speech, 3d."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import List, Optional
6
+
7
+ from .._transport import _Transport
8
+ from ..models import Job
9
+
10
+
11
+ class GenerateResource:
12
+ def __init__(self, transport: _Transport):
13
+ self._t = transport
14
+
15
+ def image(
16
+ self,
17
+ *,
18
+ prompt: str,
19
+ width: int = 1024,
20
+ height: int = 1024,
21
+ seed: int = -1,
22
+ output_format: str = "webp",
23
+ num_inference_steps: Optional[int] = None,
24
+ guidance_scale: Optional[float] = None,
25
+ enhance_prompt: bool = False,
26
+ logo_url: Optional[str] = None,
27
+ profile_id: Optional[str] = None,
28
+ callback_url: Optional[str] = None,
29
+ wait: bool = True,
30
+ ) -> Job:
31
+ """Generate an image from a text prompt using Z-Image Turbo.
32
+
33
+ Args:
34
+ prompt: Text description of the image to generate.
35
+ width: Output width in pixels (max 1024).
36
+ height: Output height in pixels (max 1024).
37
+ seed: Reproducibility seed (-1 for random).
38
+ output_format: ``'webp'`` (default), ``'jpeg'``, or ``'png'``.
39
+ num_inference_steps: Diffusion steps (default 8). Higher = slower but sharper.
40
+ guidance_scale: CFG scale. Leave unset to use the model default (0.0 for Z-Image Turbo).
41
+ enhance_prompt: Run the prompt through a lightweight AI enhancer before generation.
42
+ Expands terse prompts into detailed visual descriptions. Adds ~1–2 s.
43
+ logo_url: Public URL of your company logo (PNG/WebP). Stamped at bottom-right at 50% opacity.
44
+ profile_id: Identity profile ID — applies the profile's prompt template and quality settings.
45
+ callback_url: Webhook URL. We POST a ``WebhookEvent`` on completion.
46
+ wait: If True (default), poll until complete and return a finished Job.
47
+ If False, return immediately with a QUEUED Job.
48
+ """
49
+ body: dict = {
50
+ "prompt": prompt,
51
+ "width": width,
52
+ "height": height,
53
+ "seed": seed,
54
+ "output_format": output_format,
55
+ "enhance_prompt": enhance_prompt,
56
+ }
57
+ if num_inference_steps is not None:
58
+ body["num_inference_steps"] = num_inference_steps
59
+ if guidance_scale is not None:
60
+ body["guidance_scale"] = guidance_scale
61
+ if logo_url:
62
+ body["logo_url"] = logo_url
63
+ if profile_id:
64
+ body["profile_id"] = profile_id
65
+ if callback_url:
66
+ body["callback_url"] = callback_url
67
+ endpoint = "generate/image/v1"
68
+ return self._t.submit_and_poll(endpoint, body) if wait else self._t.submit(endpoint, body)
69
+
70
+ def video(
71
+ self,
72
+ *,
73
+ input_image: str,
74
+ prompt: str = "make this image come alive, cinematic motion, smooth animation",
75
+ width: int = 896,
76
+ height: int = 512,
77
+ duration_seconds: float = 2.0,
78
+ seed: int = 42,
79
+ callback_url: Optional[str] = None,
80
+ wait: bool = True,
81
+ ) -> Job:
82
+ """Generate a short video from an input image (image-to-video).
83
+
84
+ Args:
85
+ input_image: Public URL of the image to animate.
86
+ prompt: Animation style description.
87
+ width: Output width in pixels (max 1536, divisible by 32).
88
+ height: Output height in pixels (max 1536, divisible by 32).
89
+ duration_seconds: Video length in seconds (0.1–10.0).
90
+ seed: Reproducibility seed.
91
+ callback_url: Webhook URL. We POST a ``WebhookEvent`` on completion.
92
+ wait: Poll until complete if True.
93
+ """
94
+ body: dict = {
95
+ "input_image": input_image,
96
+ "prompt": prompt,
97
+ "width": width,
98
+ "height": height,
99
+ "duration_seconds": duration_seconds,
100
+ "seed": seed,
101
+ }
102
+ if callback_url:
103
+ body["callback_url"] = callback_url
104
+ endpoint = "generate/video/v1"
105
+ return self._t.submit_and_poll(endpoint, body) if wait else self._t.submit(endpoint, body)
106
+
107
+ def speech(
108
+ self,
109
+ *,
110
+ text: str,
111
+ language_id: str = "en",
112
+ target_voice_path: Optional[str] = None,
113
+ max_new_tokens: int = 256,
114
+ exaggeration: float = 0.5,
115
+ apply_watermark: bool = True,
116
+ callback_url: Optional[str] = None,
117
+ wait: bool = True,
118
+ ) -> Job:
119
+ """Convert text to speech.
120
+
121
+ Args:
122
+ text: Text to synthesise (max 5000 chars).
123
+ language_id: Language code e.g. ``'en'``, ``'es'``, ``'fr'``.
124
+ target_voice_path: URL of a reference voice sample for voice cloning.
125
+ max_new_tokens: Maximum tokens to generate (max 1024).
126
+ exaggeration: Voice expressiveness (0.0–1.0).
127
+ apply_watermark: Apply audio watermark.
128
+ callback_url: Webhook URL. We POST a ``WebhookEvent`` on completion.
129
+ wait: Poll until complete if True.
130
+ """
131
+ body: dict = {
132
+ "text": text,
133
+ "language_id": language_id,
134
+ "max_new_tokens": max_new_tokens,
135
+ "exaggeration": exaggeration,
136
+ "apply_watermark": apply_watermark,
137
+ }
138
+ if target_voice_path:
139
+ body["target_voice_path"] = target_voice_path
140
+ if callback_url:
141
+ body["callback_url"] = callback_url
142
+ endpoint = "generate/speech/v1"
143
+ return self._t.submit_and_poll(endpoint, body) if wait else self._t.submit(endpoint, body)
144
+
145
+ def generate_3d(
146
+ self,
147
+ *,
148
+ image_path: str,
149
+ mode: str = "generate_and_paint",
150
+ mesh_save_name: Optional[str] = None,
151
+ painted_save_name: Optional[str] = None,
152
+ auto_unload: bool = True,
153
+ callback_url: Optional[str] = None,
154
+ wait: bool = True,
155
+ ) -> Job:
156
+ """Convert an image to a 3D mesh (Pro plan required).
157
+
158
+ Args:
159
+ image_path: Public URL of the image to convert.
160
+ mode: ``'generate'`` | ``'paint'`` | ``'generate_and_paint'``.
161
+ mesh_save_name: Optional filename for the output mesh.
162
+ painted_save_name: Optional filename for the textured mesh.
163
+ auto_unload: Unload model from GPU after generation.
164
+ callback_url: Webhook URL. We POST a ``WebhookEvent`` on completion.
165
+ wait: Poll until complete if True.
166
+ """
167
+ body: dict = {
168
+ "image_path": image_path,
169
+ "mode": mode,
170
+ "auto_unload": auto_unload,
171
+ }
172
+ if mesh_save_name:
173
+ body["mesh_save_name"] = mesh_save_name
174
+ if painted_save_name:
175
+ body["painted_save_name"] = painted_save_name
176
+ if callback_url:
177
+ body["callback_url"] = callback_url
178
+ endpoint = "generate/3d/v1"
179
+ return self._t.submit_and_poll(endpoint, body) if wait else self._t.submit(endpoint, body)