teknolis / app /Services /FalTryOnService.php
Codex
Deploy latest assistant, DeepSeek defaults, and automation updates
74acf19
Raw
History Blame Contribute Delete
5.43 kB
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class FalTryOnService
{
public function isAvailable(): bool
{
return trim((string) config('services.fal.api_key', '')) !== '';
}
public function generate(string $personImageUrl, string $clothingImageUrl, array $options = []): array
{
$apiKey = trim((string) config('services.fal.api_key', ''));
$baseUrl = rtrim((string) config('services.fal.base_url', 'https://queue.fal.run'), '/');
$model = trim((string) config('services.fal.tryon_model', 'fal-ai/image-apps-v2/virtual-try-on'));
$pollIntervalMs = max(500, (int) config('services.fal.poll_interval_ms', 1500));
$timeoutSec = max(20, (int) config('services.fal.timeout', 180));
if ($apiKey === '') {
throw new RuntimeException('FAL_KEY belum diisi di server, jadi Try On Outfit belum bisa diproses.');
}
$payload = [
'person_image_url' => trim($personImageUrl),
'clothing_image_url' => trim($clothingImageUrl),
'preserve_pose' => (bool) ($options['preserve_pose'] ?? true),
'aspect_ratio' => [
'ratio' => $this->normalizeAspectRatio((string) ($options['aspect_ratio'] ?? '3:4')),
],
];
$start = microtime(true);
$submit = Http::timeout($timeoutSec)
->withHeaders([
'Authorization' => 'Key '.$apiKey,
'Content-Type' => 'application/json',
])
->post($baseUrl.'/'.$model, $payload);
if (! $submit->successful()) {
$error = trim((string) data_get($submit->json(), 'detail', data_get($submit->json(), 'error', $submit->body())));
throw new RuntimeException('fal.ai Try On gagal: '.$submit->status().' - '.$error);
}
$submitJson = $submit->json();
$directImage = $this->extractImage($submitJson);
if ($directImage !== null) {
return $this->normalizeResponse($directImage, $options, $submitJson);
}
$statusUrl = trim((string) data_get($submitJson, 'status_url', ''));
if ($statusUrl === '') {
throw new RuntimeException('fal.ai tidak mengembalikan status_url untuk Try On.');
}
while ((microtime(true) - $start) < $timeoutSec) {
usleep($pollIntervalMs * 1000);
$status = Http::timeout(30)
->withHeaders([
'Authorization' => 'Key '.$apiKey,
'Content-Type' => 'application/json',
])
->get($statusUrl);
if (! $status->successful()) {
continue;
}
$statusJson = $status->json();
$image = $this->extractImage($statusJson);
if ($image !== null) {
return $this->normalizeResponse($image, $options, $statusJson);
}
$state = strtoupper((string) data_get($statusJson, 'status', ''));
if (in_array($state, ['FAILED', 'ERROR', 'CANCELLED'], true)) {
$error = (string) data_get($statusJson, 'error', 'unknown error');
throw new RuntimeException('fal.ai Try On gagal: '.$error);
}
}
throw new RuntimeException('fal.ai Try On timeout. Coba ulangi dengan foto dan outfit yang lebih jelas.');
}
private function normalizeAspectRatio(string $value): string
{
$ratio = trim($value);
return in_array($ratio, ['1:1', '16:9', '9:16', '4:3', '3:4'], true)
? $ratio
: '3:4';
}
private function extractImage(array $payload): ?array
{
$candidates = [
data_get($payload, 'images.0'),
data_get($payload, 'data.images.0'),
data_get($payload, 'response.images.0'),
data_get($payload, 'output.images.0'),
];
foreach ($candidates as $candidate) {
if (is_array($candidate) && trim((string) ($candidate['url'] ?? '')) !== '') {
return $candidate;
}
}
return null;
}
private function normalizeResponse(array $image, array $options, array $raw): array
{
$url = trim((string) ($image['url'] ?? ''));
if ($url === '') {
throw new RuntimeException('fal.ai Try On selesai, tapi URL hasilnya kosong.');
}
return [
'url' => $url,
'thumbnail_url' => $url,
'width' => (int) ($image['width'] ?? 0),
'height' => (int) ($image['height'] ?? 0),
'provider' => 'fal-tryon',
'prompt' => (string) ($options['prompt'] ?? 'Virtual try on result'),
'aspect' => (string) ($options['aspect_ratio'] ?? '3:4'),
'meta' => array_merge([
'source' => 'tools_tryon',
'request_id' => data_get($raw, 'request_id'),
'model' => config('services.fal.tryon_model', 'fal-ai/image-apps-v2/virtual-try-on'),
'person_image_url' => (string) ($options['person_image_url'] ?? ''),
'clothing_image_url' => (string) ($options['clothing_image_url'] ?? ''),
'preserve_pose' => (bool) ($options['preserve_pose'] ?? true),
], is_array($options['meta'] ?? null) ? $options['meta'] : []),
];
}
}