pymlsapi 0.1.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,694 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Dict, List, Optional
4
+
5
+ from pymlsapi.async_http import AsyncHttpClient
6
+ from pymlsapi.http import HttpClient
7
+ from pymlsapi.models.studio import (
8
+ DeclutterResult,
9
+ DeStageEmptyResult,
10
+ ReplaceFurnitureResult,
11
+ ReplaceMaterialResult,
12
+ RestyleResult,
13
+ StagingResult,
14
+ StudioJob,
15
+ TwilightResult,
16
+ WallColorsResult,
17
+ )
18
+ from pymlsapi.resources.studio.jobs import AsyncStudioJobsResource, StudioJobsResource
19
+
20
+
21
+ class StagingResource:
22
+ """Synchronous Virtual Staging and Room Transformation operations."""
23
+
24
+ def __init__(self, http: HttpClient, jobs: StudioJobsResource) -> None:
25
+ self._http = http
26
+ self._jobs = jobs
27
+
28
+ def stage(
29
+ self,
30
+ photo_url: str,
31
+ room_type: Optional[str] = "living_room",
32
+ style: Optional[str] = "modern",
33
+ preserve_flooring: bool = True,
34
+ custom_staging_instructions: Optional[str] = None,
35
+ webhook_url: Optional[str] = None,
36
+ ) -> StudioJob:
37
+ payload: Dict[str, Any] = {
38
+ "photo_url": photo_url,
39
+ "room_type": room_type,
40
+ "style": style,
41
+ "preserve_flooring": preserve_flooring,
42
+ }
43
+ if custom_staging_instructions:
44
+ payload["custom_staging_instructions"] = custom_staging_instructions
45
+ if webhook_url:
46
+ payload["webhook_url"] = webhook_url
47
+ data = self._http.post("/v1/studio/staging/furnish", json=payload)
48
+ return StudioJob.model_validate(data)
49
+
50
+ def stage_and_wait(
51
+ self,
52
+ photo_url: str,
53
+ room_type: Optional[str] = "living_room",
54
+ style: Optional[str] = "modern",
55
+ preserve_flooring: bool = True,
56
+ custom_staging_instructions: Optional[str] = None,
57
+ webhook_url: Optional[str] = None,
58
+ timeout_seconds: float = 90.0,
59
+ poll_interval: float = 2.0,
60
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
61
+ ) -> StagingResult:
62
+ job = self.stage(
63
+ photo_url=photo_url,
64
+ room_type=room_type,
65
+ style=style,
66
+ preserve_flooring=preserve_flooring,
67
+ custom_staging_instructions=custom_staging_instructions,
68
+ webhook_url=webhook_url,
69
+ )
70
+ completed = self._jobs.wait_for(
71
+ job.job_id,
72
+ timeout_seconds=timeout_seconds,
73
+ poll_interval=poll_interval,
74
+ on_progress=on_progress,
75
+ )
76
+ return StagingResult.model_validate(completed.result or {})
77
+
78
+ def declutter(
79
+ self,
80
+ photo_url: str,
81
+ room_type: Optional[str] = None,
82
+ removal_targets: Optional[List[str]] = None,
83
+ webhook_url: Optional[str] = None,
84
+ ) -> StudioJob:
85
+ payload: Dict[str, Any] = {"photo_url": photo_url}
86
+ if room_type:
87
+ payload["room_type"] = room_type
88
+ if removal_targets:
89
+ payload["removal_targets"] = removal_targets
90
+ if webhook_url:
91
+ payload["webhook_url"] = webhook_url
92
+ data = self._http.post("/v1/studio/staging/declutter", json=payload)
93
+ return StudioJob.model_validate(data)
94
+
95
+ def declutter_and_wait(
96
+ self,
97
+ photo_url: str,
98
+ room_type: Optional[str] = None,
99
+ removal_targets: Optional[List[str]] = None,
100
+ webhook_url: Optional[str] = None,
101
+ timeout_seconds: float = 90.0,
102
+ poll_interval: float = 2.0,
103
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
104
+ ) -> DeclutterResult:
105
+ job = self.declutter(
106
+ photo_url, room_type=room_type, removal_targets=removal_targets, webhook_url=webhook_url
107
+ )
108
+ completed = self._jobs.wait_for(
109
+ job.job_id,
110
+ timeout_seconds=timeout_seconds,
111
+ poll_interval=poll_interval,
112
+ on_progress=on_progress,
113
+ )
114
+ return DeclutterResult.model_validate(completed.result or {})
115
+
116
+ def empty(
117
+ self,
118
+ photo_url: str,
119
+ room_type: Optional[str] = None,
120
+ restore_flooring: Optional[str] = None,
121
+ webhook_url: Optional[str] = None,
122
+ ) -> StudioJob:
123
+ payload: Dict[str, Any] = {"photo_url": photo_url}
124
+ if room_type:
125
+ payload["room_type"] = room_type
126
+ if restore_flooring:
127
+ payload["restore_flooring"] = restore_flooring
128
+ if webhook_url:
129
+ payload["webhook_url"] = webhook_url
130
+ data = self._http.post("/v1/studio/staging/empty", json=payload)
131
+ return StudioJob.model_validate(data)
132
+
133
+ def empty_and_wait(
134
+ self,
135
+ photo_url: str,
136
+ room_type: Optional[str] = None,
137
+ restore_flooring: Optional[str] = None,
138
+ webhook_url: Optional[str] = None,
139
+ timeout_seconds: float = 90.0,
140
+ poll_interval: float = 2.0,
141
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
142
+ ) -> DeStageEmptyResult:
143
+ job = self.empty(
144
+ photo_url,
145
+ room_type=room_type,
146
+ restore_flooring=restore_flooring,
147
+ webhook_url=webhook_url,
148
+ )
149
+ completed = self._jobs.wait_for(
150
+ job.job_id,
151
+ timeout_seconds=timeout_seconds,
152
+ poll_interval=poll_interval,
153
+ on_progress=on_progress,
154
+ )
155
+ return DeStageEmptyResult.model_validate(completed.result or {})
156
+
157
+ def restyle(
158
+ self,
159
+ photo_url: str,
160
+ target_style: str = "scandinavian",
161
+ room_type: Optional[str] = None,
162
+ webhook_url: Optional[str] = None,
163
+ ) -> StudioJob:
164
+ payload: Dict[str, Any] = {"photo_url": photo_url, "target_style": target_style}
165
+ if room_type:
166
+ payload["room_type"] = room_type
167
+ if webhook_url:
168
+ payload["webhook_url"] = webhook_url
169
+ data = self._http.post("/v1/studio/staging/restyle", json=payload)
170
+ return StudioJob.model_validate(data)
171
+
172
+ def restyle_and_wait(
173
+ self,
174
+ photo_url: str,
175
+ target_style: str = "scandinavian",
176
+ room_type: Optional[str] = None,
177
+ webhook_url: Optional[str] = None,
178
+ timeout_seconds: float = 90.0,
179
+ poll_interval: float = 2.0,
180
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
181
+ ) -> RestyleResult:
182
+ job = self.restyle(
183
+ photo_url, target_style=target_style, room_type=room_type, webhook_url=webhook_url
184
+ )
185
+ completed = self._jobs.wait_for(
186
+ job.job_id,
187
+ timeout_seconds=timeout_seconds,
188
+ poll_interval=poll_interval,
189
+ on_progress=on_progress,
190
+ )
191
+ return RestyleResult.model_validate(completed.result or {})
192
+
193
+ def replace_furniture(
194
+ self,
195
+ room_photo_url: str,
196
+ target_furniture: str,
197
+ product_description: Optional[str] = None,
198
+ reference_product_image_url: Optional[str] = None,
199
+ webhook_url: Optional[str] = None,
200
+ ) -> StudioJob:
201
+ payload: Dict[str, Any] = {
202
+ "room_photo_url": room_photo_url,
203
+ "target_furniture": target_furniture,
204
+ }
205
+ if product_description:
206
+ payload["product_description"] = product_description
207
+ if reference_product_image_url:
208
+ payload["reference_product_image_url"] = reference_product_image_url
209
+ if webhook_url:
210
+ payload["webhook_url"] = webhook_url
211
+ data = self._http.post("/v1/studio/staging/replace-furniture", json=payload)
212
+ return StudioJob.model_validate(data)
213
+
214
+ def replace_furniture_and_wait(
215
+ self,
216
+ room_photo_url: str,
217
+ target_furniture: str,
218
+ product_description: Optional[str] = None,
219
+ reference_product_image_url: Optional[str] = None,
220
+ webhook_url: Optional[str] = None,
221
+ timeout_seconds: float = 90.0,
222
+ poll_interval: float = 2.0,
223
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
224
+ ) -> ReplaceFurnitureResult:
225
+ job = self.replace_furniture(
226
+ room_photo_url,
227
+ target_furniture,
228
+ product_description=product_description,
229
+ reference_product_image_url=reference_product_image_url,
230
+ webhook_url=webhook_url,
231
+ )
232
+ completed = self._jobs.wait_for(
233
+ job.job_id,
234
+ timeout_seconds=timeout_seconds,
235
+ poll_interval=poll_interval,
236
+ on_progress=on_progress,
237
+ )
238
+ return ReplaceFurnitureResult.model_validate(completed.result or {})
239
+
240
+ def replace_material(
241
+ self,
242
+ room_photo_url: str,
243
+ surface_type: str,
244
+ material_preset: Optional[str] = None,
245
+ material_sample_image_url: Optional[str] = None,
246
+ webhook_url: Optional[str] = None,
247
+ ) -> StudioJob:
248
+ payload: Dict[str, Any] = {
249
+ "room_photo_url": room_photo_url,
250
+ "surface_type": surface_type,
251
+ }
252
+ if material_preset:
253
+ payload["material_preset"] = material_preset
254
+ if material_sample_image_url:
255
+ payload["material_sample_image_url"] = material_sample_image_url
256
+ if webhook_url:
257
+ payload["webhook_url"] = webhook_url
258
+ data = self._http.post("/v1/studio/staging/replace-material", json=payload)
259
+ return StudioJob.model_validate(data)
260
+
261
+ def replace_material_and_wait(
262
+ self,
263
+ room_photo_url: str,
264
+ surface_type: str,
265
+ material_preset: Optional[str] = None,
266
+ material_sample_image_url: Optional[str] = None,
267
+ webhook_url: Optional[str] = None,
268
+ timeout_seconds: float = 90.0,
269
+ poll_interval: float = 2.0,
270
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
271
+ ) -> ReplaceMaterialResult:
272
+ job = self.replace_material(
273
+ room_photo_url,
274
+ surface_type,
275
+ material_preset=material_preset,
276
+ material_sample_image_url=material_sample_image_url,
277
+ webhook_url=webhook_url,
278
+ )
279
+ completed = self._jobs.wait_for(
280
+ job.job_id,
281
+ timeout_seconds=timeout_seconds,
282
+ poll_interval=poll_interval,
283
+ on_progress=on_progress,
284
+ )
285
+ return ReplaceMaterialResult.model_validate(completed.result or {})
286
+
287
+ def wall_colors(
288
+ self,
289
+ photo_url: str,
290
+ palette_preset: Optional[str] = "popular_neutrals",
291
+ webhook_url: Optional[str] = None,
292
+ ) -> StudioJob:
293
+ payload: Dict[str, Any] = {"photo_url": photo_url}
294
+ if palette_preset:
295
+ payload["palette_preset"] = palette_preset
296
+ if webhook_url:
297
+ payload["webhook_url"] = webhook_url
298
+ data = self._http.post("/v1/studio/staging/wall-colors", json=payload)
299
+ return StudioJob.model_validate(data)
300
+
301
+ def wall_colors_and_wait(
302
+ self,
303
+ photo_url: str,
304
+ palette_preset: Optional[str] = "popular_neutrals",
305
+ webhook_url: Optional[str] = None,
306
+ timeout_seconds: float = 90.0,
307
+ poll_interval: float = 2.0,
308
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
309
+ ) -> WallColorsResult:
310
+ job = self.wall_colors(photo_url, palette_preset=palette_preset, webhook_url=webhook_url)
311
+ completed = self._jobs.wait_for(
312
+ job.job_id,
313
+ timeout_seconds=timeout_seconds,
314
+ poll_interval=poll_interval,
315
+ on_progress=on_progress,
316
+ )
317
+ return WallColorsResult.model_validate(completed.result or {})
318
+
319
+ def twilight(
320
+ self,
321
+ photo_url: str,
322
+ mode: str = "day_to_dusk",
323
+ interior_lighting: bool = True,
324
+ webhook_url: Optional[str] = None,
325
+ ) -> StudioJob:
326
+ payload: Dict[str, Any] = {
327
+ "photo_url": photo_url,
328
+ "mode": mode,
329
+ "interior_lighting": interior_lighting,
330
+ }
331
+ if webhook_url:
332
+ payload["webhook_url"] = webhook_url
333
+ data = self._http.post("/v1/studio/staging/twilight", json=payload)
334
+ return StudioJob.model_validate(data)
335
+
336
+ def twilight_and_wait(
337
+ self,
338
+ photo_url: str,
339
+ mode: str = "day_to_dusk",
340
+ interior_lighting: bool = True,
341
+ webhook_url: Optional[str] = None,
342
+ timeout_seconds: float = 90.0,
343
+ poll_interval: float = 2.0,
344
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
345
+ ) -> TwilightResult:
346
+ job = self.twilight(
347
+ photo_url, mode=mode, interior_lighting=interior_lighting, webhook_url=webhook_url
348
+ )
349
+ completed = self._jobs.wait_for(
350
+ job.job_id,
351
+ timeout_seconds=timeout_seconds,
352
+ poll_interval=poll_interval,
353
+ on_progress=on_progress,
354
+ )
355
+ return TwilightResult.model_validate(completed.result or {})
356
+
357
+
358
+ class AsyncStagingResource:
359
+ """Asynchronous Virtual Staging and Room Transformation operations."""
360
+
361
+ def __init__(self, http: AsyncHttpClient, jobs: AsyncStudioJobsResource) -> None:
362
+ self._http = http
363
+ self._jobs = jobs
364
+
365
+ async def stage(
366
+ self,
367
+ photo_url: str,
368
+ room_type: Optional[str] = "living_room",
369
+ style: Optional[str] = "modern",
370
+ preserve_flooring: bool = True,
371
+ custom_staging_instructions: Optional[str] = None,
372
+ webhook_url: Optional[str] = None,
373
+ ) -> StudioJob:
374
+ payload: Dict[str, Any] = {
375
+ "photo_url": photo_url,
376
+ "room_type": room_type,
377
+ "style": style,
378
+ "preserve_flooring": preserve_flooring,
379
+ }
380
+ if custom_staging_instructions:
381
+ payload["custom_staging_instructions"] = custom_staging_instructions
382
+ if webhook_url:
383
+ payload["webhook_url"] = webhook_url
384
+ data = await self._http.post("/v1/studio/staging/furnish", json=payload)
385
+ return StudioJob.model_validate(data)
386
+
387
+ async def stage_and_wait(
388
+ self,
389
+ photo_url: str,
390
+ room_type: Optional[str] = "living_room",
391
+ style: Optional[str] = "modern",
392
+ preserve_flooring: bool = True,
393
+ custom_staging_instructions: Optional[str] = None,
394
+ webhook_url: Optional[str] = None,
395
+ timeout_seconds: float = 90.0,
396
+ poll_interval: float = 2.0,
397
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
398
+ ) -> StagingResult:
399
+ job = await self.stage(
400
+ photo_url=photo_url,
401
+ room_type=room_type,
402
+ style=style,
403
+ preserve_flooring=preserve_flooring,
404
+ custom_staging_instructions=custom_staging_instructions,
405
+ webhook_url=webhook_url,
406
+ )
407
+ completed = await self._jobs.wait_for(
408
+ job.job_id,
409
+ timeout_seconds=timeout_seconds,
410
+ poll_interval=poll_interval,
411
+ on_progress=on_progress,
412
+ )
413
+ return StagingResult.model_validate(completed.result or {})
414
+
415
+ async def declutter(
416
+ self,
417
+ photo_url: str,
418
+ room_type: Optional[str] = None,
419
+ removal_targets: Optional[List[str]] = None,
420
+ webhook_url: Optional[str] = None,
421
+ ) -> StudioJob:
422
+ payload: Dict[str, Any] = {"photo_url": photo_url}
423
+ if room_type:
424
+ payload["room_type"] = room_type
425
+ if removal_targets:
426
+ payload["removal_targets"] = removal_targets
427
+ if webhook_url:
428
+ payload["webhook_url"] = webhook_url
429
+ data = await self._http.post("/v1/studio/staging/declutter", json=payload)
430
+ return StudioJob.model_validate(data)
431
+
432
+ async def declutter_and_wait(
433
+ self,
434
+ photo_url: str,
435
+ room_type: Optional[str] = None,
436
+ removal_targets: Optional[List[str]] = None,
437
+ webhook_url: Optional[str] = None,
438
+ timeout_seconds: float = 90.0,
439
+ poll_interval: float = 2.0,
440
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
441
+ ) -> DeclutterResult:
442
+ job = await self.declutter(
443
+ photo_url, room_type=room_type, removal_targets=removal_targets, webhook_url=webhook_url
444
+ )
445
+ completed = await self._jobs.wait_for(
446
+ job.job_id,
447
+ timeout_seconds=timeout_seconds,
448
+ poll_interval=poll_interval,
449
+ on_progress=on_progress,
450
+ )
451
+ return DeclutterResult.model_validate(completed.result or {})
452
+
453
+ async def empty(
454
+ self,
455
+ photo_url: str,
456
+ room_type: Optional[str] = None,
457
+ restore_flooring: Optional[str] = None,
458
+ webhook_url: Optional[str] = None,
459
+ ) -> StudioJob:
460
+ payload: Dict[str, Any] = {"photo_url": photo_url}
461
+ if room_type:
462
+ payload["room_type"] = room_type
463
+ if restore_flooring:
464
+ payload["restore_flooring"] = restore_flooring
465
+ if webhook_url:
466
+ payload["webhook_url"] = webhook_url
467
+ data = await self._http.post("/v1/studio/staging/empty", json=payload)
468
+ return StudioJob.model_validate(data)
469
+
470
+ async def empty_and_wait(
471
+ self,
472
+ photo_url: str,
473
+ room_type: Optional[str] = None,
474
+ restore_flooring: Optional[str] = None,
475
+ webhook_url: Optional[str] = None,
476
+ timeout_seconds: float = 90.0,
477
+ poll_interval: float = 2.0,
478
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
479
+ ) -> DeStageEmptyResult:
480
+ job = await self.empty(
481
+ photo_url,
482
+ room_type=room_type,
483
+ restore_flooring=restore_flooring,
484
+ webhook_url=webhook_url,
485
+ )
486
+ completed = await self._jobs.wait_for(
487
+ job.job_id,
488
+ timeout_seconds=timeout_seconds,
489
+ poll_interval=poll_interval,
490
+ on_progress=on_progress,
491
+ )
492
+ return DeStageEmptyResult.model_validate(completed.result or {})
493
+
494
+ async def restyle(
495
+ self,
496
+ photo_url: str,
497
+ target_style: str = "scandinavian",
498
+ room_type: Optional[str] = None,
499
+ webhook_url: Optional[str] = None,
500
+ ) -> StudioJob:
501
+ payload: Dict[str, Any] = {"photo_url": photo_url, "target_style": target_style}
502
+ if room_type:
503
+ payload["room_type"] = room_type
504
+ if webhook_url:
505
+ payload["webhook_url"] = webhook_url
506
+ data = await self._http.post("/v1/studio/staging/restyle", json=payload)
507
+ return StudioJob.model_validate(data)
508
+
509
+ async def restyle_and_wait(
510
+ self,
511
+ photo_url: str,
512
+ target_style: str = "scandinavian",
513
+ room_type: Optional[str] = None,
514
+ webhook_url: Optional[str] = None,
515
+ timeout_seconds: float = 90.0,
516
+ poll_interval: float = 2.0,
517
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
518
+ ) -> RestyleResult:
519
+ job = await self.restyle(
520
+ photo_url, target_style=target_style, room_type=room_type, webhook_url=webhook_url
521
+ )
522
+ completed = await self._jobs.wait_for(
523
+ job.job_id,
524
+ timeout_seconds=timeout_seconds,
525
+ poll_interval=poll_interval,
526
+ on_progress=on_progress,
527
+ )
528
+ return RestyleResult.model_validate(completed.result or {})
529
+
530
+ async def replace_furniture(
531
+ self,
532
+ room_photo_url: str,
533
+ target_furniture: str,
534
+ product_description: Optional[str] = None,
535
+ reference_product_image_url: Optional[str] = None,
536
+ webhook_url: Optional[str] = None,
537
+ ) -> StudioJob:
538
+ payload: Dict[str, Any] = {
539
+ "room_photo_url": room_photo_url,
540
+ "target_furniture": target_furniture,
541
+ }
542
+ if product_description:
543
+ payload["product_description"] = product_description
544
+ if reference_product_image_url:
545
+ payload["reference_product_image_url"] = reference_product_image_url
546
+ if webhook_url:
547
+ payload["webhook_url"] = webhook_url
548
+ data = await self._http.post("/v1/studio/staging/replace-furniture", json=payload)
549
+ return StudioJob.model_validate(data)
550
+
551
+ async def replace_furniture_and_wait(
552
+ self,
553
+ room_photo_url: str,
554
+ target_furniture: str,
555
+ product_description: Optional[str] = None,
556
+ reference_product_image_url: Optional[str] = None,
557
+ webhook_url: Optional[str] = None,
558
+ timeout_seconds: float = 90.0,
559
+ poll_interval: float = 2.0,
560
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
561
+ ) -> ReplaceFurnitureResult:
562
+ job = await self.replace_furniture(
563
+ room_photo_url,
564
+ target_furniture,
565
+ product_description=product_description,
566
+ reference_product_image_url=reference_product_image_url,
567
+ webhook_url=webhook_url,
568
+ )
569
+ completed = await self._jobs.wait_for(
570
+ job.job_id,
571
+ timeout_seconds=timeout_seconds,
572
+ poll_interval=poll_interval,
573
+ on_progress=on_progress,
574
+ )
575
+ return ReplaceFurnitureResult.model_validate(completed.result or {})
576
+
577
+ async def replace_material(
578
+ self,
579
+ room_photo_url: str,
580
+ surface_type: str,
581
+ material_preset: Optional[str] = None,
582
+ material_sample_image_url: Optional[str] = None,
583
+ webhook_url: Optional[str] = None,
584
+ ) -> StudioJob:
585
+ payload: Dict[str, Any] = {
586
+ "room_photo_url": room_photo_url,
587
+ "surface_type": surface_type,
588
+ }
589
+ if material_preset:
590
+ payload["material_preset"] = material_preset
591
+ if material_sample_image_url:
592
+ payload["material_sample_image_url"] = material_sample_image_url
593
+ if webhook_url:
594
+ payload["webhook_url"] = webhook_url
595
+ data = await self._http.post("/v1/studio/staging/replace-material", json=payload)
596
+ return StudioJob.model_validate(data)
597
+
598
+ async def replace_material_and_wait(
599
+ self,
600
+ room_photo_url: str,
601
+ surface_type: str,
602
+ material_preset: Optional[str] = None,
603
+ material_sample_image_url: Optional[str] = None,
604
+ webhook_url: Optional[str] = None,
605
+ timeout_seconds: float = 90.0,
606
+ poll_interval: float = 2.0,
607
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
608
+ ) -> ReplaceMaterialResult:
609
+ job = await self.replace_material(
610
+ room_photo_url,
611
+ surface_type,
612
+ material_preset=material_preset,
613
+ material_sample_image_url=material_sample_image_url,
614
+ webhook_url=webhook_url,
615
+ )
616
+ completed = await self._jobs.wait_for(
617
+ job.job_id,
618
+ timeout_seconds=timeout_seconds,
619
+ poll_interval=poll_interval,
620
+ on_progress=on_progress,
621
+ )
622
+ return ReplaceMaterialResult.model_validate(completed.result or {})
623
+
624
+ async def wall_colors(
625
+ self,
626
+ photo_url: str,
627
+ palette_preset: Optional[str] = "popular_neutrals",
628
+ webhook_url: Optional[str] = None,
629
+ ) -> StudioJob:
630
+ payload: Dict[str, Any] = {"photo_url": photo_url}
631
+ if palette_preset:
632
+ payload["palette_preset"] = palette_preset
633
+ if webhook_url:
634
+ payload["webhook_url"] = webhook_url
635
+ data = await self._http.post("/v1/studio/staging/wall-colors", json=payload)
636
+ return StudioJob.model_validate(data)
637
+
638
+ async def wall_colors_and_wait(
639
+ self,
640
+ photo_url: str,
641
+ palette_preset: Optional[str] = "popular_neutrals",
642
+ webhook_url: Optional[str] = None,
643
+ timeout_seconds: float = 90.0,
644
+ poll_interval: float = 2.0,
645
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
646
+ ) -> WallColorsResult:
647
+ job = await self.wall_colors(
648
+ photo_url, palette_preset=palette_preset, webhook_url=webhook_url
649
+ )
650
+ completed = await self._jobs.wait_for(
651
+ job.job_id,
652
+ timeout_seconds=timeout_seconds,
653
+ poll_interval=poll_interval,
654
+ on_progress=on_progress,
655
+ )
656
+ return WallColorsResult.model_validate(completed.result or {})
657
+
658
+ async def twilight(
659
+ self,
660
+ photo_url: str,
661
+ mode: str = "day_to_dusk",
662
+ interior_lighting: bool = True,
663
+ webhook_url: Optional[str] = None,
664
+ ) -> StudioJob:
665
+ payload: Dict[str, Any] = {
666
+ "photo_url": photo_url,
667
+ "mode": mode,
668
+ "interior_lighting": interior_lighting,
669
+ }
670
+ if webhook_url:
671
+ payload["webhook_url"] = webhook_url
672
+ data = await self._http.post("/v1/studio/staging/twilight", json=payload)
673
+ return StudioJob.model_validate(data)
674
+
675
+ async def twilight_and_wait(
676
+ self,
677
+ photo_url: str,
678
+ mode: str = "day_to_dusk",
679
+ interior_lighting: bool = True,
680
+ webhook_url: Optional[str] = None,
681
+ timeout_seconds: float = 90.0,
682
+ poll_interval: float = 2.0,
683
+ on_progress: Optional[Callable[[StudioJob], None]] = None,
684
+ ) -> TwilightResult:
685
+ job = await self.twilight(
686
+ photo_url, mode=mode, interior_lighting=interior_lighting, webhook_url=webhook_url
687
+ )
688
+ completed = await self._jobs.wait_for(
689
+ job.job_id,
690
+ timeout_seconds=timeout_seconds,
691
+ poll_interval=poll_interval,
692
+ on_progress=on_progress,
693
+ )
694
+ return TwilightResult.model_validate(completed.result or {})