teknolis / app /Services /FreepikStockAssetService.php
adwandw's picture
Enable HF-only mode: add hf_only config, always show Remix Lab, hide Remix Stock in HF-only, improve error message for stock remix
da7bbba
Raw
History Blame Contribute Delete
18.6 kB
<?php
namespace App\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use RuntimeException;
class FreepikStockAssetService
{
public function importForEdit(string $kind, int $assetId): array
{
$normalizedKind = $this->normalizeKind($kind);
return match ($normalizedKind) {
'images' => $this->importImagePreview($assetId),
'videos' => $this->importVideoPreview($assetId),
'icons' => $this->importIconPng($assetId),
default => throw new RuntimeException('Asset jenis ini belum didukung untuk AI edit di dalam project.'),
};
}
public function downloadToProject(string $kind, int $assetId): array
{
$normalizedKind = $this->normalizeKind($kind);
return match ($normalizedKind) {
'images' => $this->downloadImageAsset($assetId),
'videos' => $this->downloadVideoAsset($assetId),
'icons' => $this->downloadIconAsset($assetId),
'audio' => $this->downloadMusicAsset($assetId),
default => throw new RuntimeException('Jenis asset Freepik tidak valid untuk didownload.'),
};
}
private function importImagePreview(int $assetId): array
{
$detail = $this->detail('images', $assetId);
$sourceUrl = trim((string) data_get($detail, 'image.source.url', ''));
if ($sourceUrl === '') {
$download = $this->downloadImageAsset($assetId);
return [
'type' => 'image',
'provider' => 'freepik-stock',
'title' => (string) ($download['title'] ?? 'Freepik image'),
'url' => $download['public_url'],
'thumbnail_url' => $download['public_url'],
'width' => null,
'height' => null,
'kind' => 'images',
'remote_id' => $assetId,
];
}
$stored = $this->storeRemoteFile($sourceUrl, [
'folder' => 'freepik_stock/imports/images',
'filename' => $this->slugFilename((string) data_get($detail, 'title', 'freepik-image'), $sourceUrl),
]);
return [
'type' => 'image',
'provider' => 'freepik-stock',
'title' => (string) data_get($detail, 'title', 'Freepik image'),
'url' => $stored['public_url'],
'thumbnail_url' => $stored['public_url'],
'width' => $this->numericOrNull(data_get($detail, 'image.source.width')),
'height' => $this->numericOrNull(data_get($detail, 'image.source.height')),
'kind' => 'images',
'remote_id' => $assetId,
];
}
private function importVideoPreview(int $assetId): array
{
$detail = $this->detail('videos', $assetId);
$previewUrl = trim((string) data_get($detail, 'previews.0.url', ''));
$thumbnailUrl = trim((string) data_get($detail, 'thumbnails.0.url', ''));
if ($previewUrl === '') {
throw new RuntimeException('Freepik video preview tidak tersedia untuk AI edit.');
}
$stored = $this->storeRemoteFile($previewUrl, [
'folder' => 'freepik_stock/imports/videos',
'filename' => $this->slugFilename((string) data_get($detail, 'name', 'freepik-video'), $previewUrl),
]);
if ($thumbnailUrl !== '') {
try {
$thumbnailStored = $this->storeRemoteFile($thumbnailUrl, [
'folder' => 'freepik_stock/imports/video_thumbs',
'filename' => $this->slugFilename((string) data_get($detail, 'name', 'freepik-video-thumb'), $thumbnailUrl),
]);
$thumbnailUrl = $thumbnailStored['public_url'];
} catch (\Throwable) {
// Keep the original remote thumbnail if the local copy fails.
}
}
return [
'type' => 'video',
'provider' => 'freepik-stock',
'title' => (string) data_get($detail, 'name', 'Freepik video'),
'url' => $stored['public_url'],
'thumbnail_url' => $thumbnailUrl,
'width' => $this->numericOrNull(data_get($detail, 'thumbnails.0.width')),
'height' => $this->numericOrNull(data_get($detail, 'thumbnails.0.height')),
'duration_seconds' => $this->durationToSeconds((string) data_get($detail, 'duration', '')),
'kind' => 'videos',
'remote_id' => $assetId,
];
}
private function importIconPng(int $assetId): array
{
$download = $this->downloadIconAsset($assetId);
return [
'type' => 'image',
'provider' => 'freepik-stock',
'title' => (string) ($download['title'] ?? 'Freepik icon'),
'url' => $download['public_url'],
'thumbnail_url' => $download['public_url'],
'width' => 512,
'height' => 512,
'kind' => 'icons',
'remote_id' => $assetId,
];
}
private function downloadImageAsset(int $assetId): array
{
$detail = $this->detail('images', $assetId);
$download = $this->authorizedGet($this->endpoint("/v1/resources/{$assetId}/download"), [
'image_size' => 'original',
], 'Freepik resource download gagal');
$data = $this->extractData($download, 'Freepik resource download gagal');
$remoteUrl = trim((string) data_get($data, 'signed_url', data_get($data, 'url', '')));
if ($remoteUrl === '') {
throw new RuntimeException('Freepik resource download tidak mengembalikan URL file.');
}
$stored = $this->storeRemoteFile($remoteUrl, [
'folder' => 'freepik_stock/downloads/images',
'filename' => (string) data_get($data, 'filename', $this->slugFilename((string) data_get($detail, 'title', 'freepik-image'), $remoteUrl)),
]);
return [
...$stored,
'title' => (string) data_get($detail, 'title', 'Freepik image'),
'kind' => 'images',
'remote_id' => $assetId,
];
}
private function downloadVideoAsset(int $assetId): array
{
$detail = $this->detail('videos', $assetId);
$download = $this->authorizedGet($this->endpoint("/v1/videos/{$assetId}/download"), [], 'Freepik video download gagal');
$data = $this->extractData($download, 'Freepik video download gagal');
$remoteUrl = trim((string) data_get($data, 'url', ''));
if ($remoteUrl === '') {
throw new RuntimeException('Freepik video download tidak mengembalikan URL file.');
}
$stored = $this->storeRemoteFile($remoteUrl, [
'folder' => 'freepik_stock/downloads/videos',
'filename' => (string) data_get($data, 'filename', $this->slugFilename((string) data_get($detail, 'name', 'freepik-video'), $remoteUrl)),
]);
return [
...$stored,
'title' => (string) data_get($detail, 'name', 'Freepik video'),
'kind' => 'videos',
'remote_id' => $assetId,
];
}
private function downloadIconAsset(int $assetId): array
{
$detail = $this->detail('icons', $assetId);
$download = $this->authorizedGet($this->endpoint("/v1/icons/{$assetId}/download"), [
'format' => 'png',
'png_size' => 512,
], 'Freepik icon download gagal');
$data = $this->extractData($download, 'Freepik icon download gagal');
$remoteUrl = trim((string) data_get($data, 'url', ''));
if ($remoteUrl === '') {
throw new RuntimeException('Freepik icon download tidak mengembalikan URL file.');
}
$stored = $this->storeRemoteFile($remoteUrl, [
'folder' => 'freepik_stock/downloads/icons',
'filename' => (string) data_get($data, 'filename', $this->slugFilename((string) data_get($detail, 'name', 'freepik-icon'), $remoteUrl)),
]);
return [
...$stored,
'title' => (string) data_get($detail, 'name', 'Freepik icon'),
'kind' => 'icons',
'remote_id' => $assetId,
];
}
private function downloadMusicAsset(int $assetId): array
{
$detail = $this->detail('audio', $assetId);
$remoteUrl = trim((string) data_get($detail, 'download_url', data_get($detail, 'file_url', '')));
if ($remoteUrl === '') {
throw new RuntimeException('Freepik music download URL tidak tersedia.');
}
$stored = $this->storeRemoteFile($remoteUrl, [
'folder' => 'freepik_stock/downloads/audio',
'filename' => $this->slugFilename((string) data_get($detail, 'title', 'freepik-audio'), $remoteUrl),
]);
return [
...$stored,
'title' => (string) data_get($detail, 'title', 'Freepik audio'),
'kind' => 'audio',
'remote_id' => $assetId,
];
}
private function detail(string $kind, int $assetId): array
{
return match ($this->normalizeKind($kind)) {
'images' => $this->extractData(
$this->authorizedGet($this->endpoint("/v1/resources/{$assetId}"), [], 'Freepik resource detail gagal'),
'Freepik resource detail gagal'
),
'videos' => $this->extractData(
$this->authorizedGet($this->endpoint("/v1/videos/{$assetId}"), [], 'Freepik video detail gagal'),
'Freepik video detail gagal'
),
'icons' => $this->extractData(
$this->authorizedGet($this->endpoint("/v1/icons/{$assetId}"), [], 'Freepik icon detail gagal'),
'Freepik icon detail gagal'
),
'audio' => $this->extractRawOrData(
$this->authorizedGet($this->endpoint("/v1/music/{$assetId}"), [], 'Freepik music detail gagal'),
'Freepik music detail gagal'
),
default => throw new RuntimeException('Jenis asset tidak didukung.'),
};
}
private function authorizedGet(string $url, array $query, string $prefix): Response
{
try {
$response = $this->client()->get($url, $query);
} catch (ConnectionException $e) {
throw new RuntimeException($prefix.': koneksi ke Freepik sempat terputus (network reset/TLS). Coba ulangi beberapa detik lagi.');
}
if (! $response->successful()) {
throw new RuntimeException($prefix.': '.$response->status().' - '.$this->responseError($response));
}
return $response;
}
private function client()
{
$apiKey = trim((string) config('services.freepik.api_key', ''));
if ($apiKey === '') {
throw new RuntimeException($this->missingApiKeyMessage());
}
return Http::connectTimeout(20)
->timeout(90)
->retry(3, 600, fn ($exception) => $exception instanceof ConnectionException, throw: false)
->acceptJson()
->withOptions([
'force_ip_resolve' => 'v4',
])
->withHeaders([
'x-freepik-api-key' => $apiKey,
'User-Agent' => 'TeknoMedia/1.0 (+Laravel Freepik Client)',
]);
}
private function endpoint(string $path): string
{
return rtrim($this->apiOrigin(), '/').$path;
}
private function apiOrigin(): string
{
$configured = trim((string) config('services.freepik.base_url', 'https://api.freepik.com/v1/ai/text-to-image'));
$scheme = parse_url($configured, PHP_URL_SCHEME) ?: 'https';
$host = parse_url($configured, PHP_URL_HOST);
$port = parse_url($configured, PHP_URL_PORT);
if (! is_string($host) || $host === '') {
return 'https://api.freepik.com';
}
return $scheme.'://'.$host.($port ? ':'.$port : '');
}
private function extractData(Response $response, string $prefix): array
{
$json = $response->json();
$data = data_get($json, 'data', $json);
if (! is_array($data)) {
throw new RuntimeException($prefix.': format response Freepik tidak dikenali.');
}
return $data;
}
private function extractRawOrData(Response $response, string $prefix): array
{
$json = $response->json();
$data = data_get($json, 'data', $json);
if (! is_array($data)) {
throw new RuntimeException($prefix.': format response Freepik tidak dikenali.');
}
return $data;
}
private function responseError(Response $response): string
{
$message = data_get($response->json(), 'message')
?? data_get($response->json(), 'error.message')
?? data_get($response->json(), 'detail')
?? $response->body();
return Str::limit(trim((string) $message), 260);
}
private function storeRemoteFile(string $remoteUrl, array $options = []): array
{
$folder = trim((string) ($options['folder'] ?? 'freepik_stock/downloads'));
$preferredName = trim((string) ($options['filename'] ?? ''));
$tempPath = tempnam(sys_get_temp_dir(), 'fp_');
if ($tempPath === false) {
throw new RuntimeException('Gagal membuat file sementara untuk download Freepik.');
}
try {
try {
$response = Http::connectTimeout(20)
->timeout(180)
->retry(3, 600, fn ($exception) => $exception instanceof ConnectionException, throw: false)
->withOptions([
'sink' => $tempPath,
'allow_redirects' => true,
'force_ip_resolve' => 'v4',
])
->withHeaders([
'User-Agent' => 'TeknoMedia/1.0 (+Laravel Freepik Client)',
])
->get($remoteUrl);
} catch (ConnectionException $e) {
throw new RuntimeException('Koneksi download asset Freepik sempat reset. Coba ulangi sekali lagi.');
}
if (! $response->successful()) {
throw new RuntimeException('Gagal mengambil file dari Freepik download URL: '.$response->status());
}
$pathSource = $preferredName !== ''
? $preferredName
: (parse_url($remoteUrl, PHP_URL_PATH) ?: '');
$extension = strtolower(pathinfo($pathSource, PATHINFO_EXTENSION));
if ($extension === '') {
$extension = $this->extensionFromMime((string) $response->header('Content-Type'));
}
$baseName = $preferredName !== ''
? pathinfo($preferredName, PATHINFO_FILENAME)
: 'freepik_'.Str::lower(Str::random(10));
$safeFilename = Str::slug($baseName);
if ($safeFilename === '') {
$safeFilename = 'freepik_'.Str::lower(Str::random(10));
}
$finalFilename = $safeFilename.($extension !== '' ? '.'.$extension : '');
$relativePath = trim($folder, '/').'/'.now()->format('Ymd_His').'_'.$finalFilename;
$stream = fopen($tempPath, 'rb');
if ($stream === false) {
throw new RuntimeException('Gagal membaca file sementara Freepik.');
}
Storage::disk('public')->writeStream($relativePath, $stream);
if (is_resource($stream)) {
fclose($stream);
}
return [
'relative_path' => $relativePath,
'public_url' => Storage::url($relativePath),
'url' => Storage::url($relativePath),
'path' => Storage::disk('public')->path($relativePath),
'filename' => $finalFilename,
'mime' => (string) $response->header('Content-Type', 'application/octet-stream'),
];
} finally {
if (is_file($tempPath)) {
@unlink($tempPath);
}
}
}
private function slugFilename(string $title, string $remoteUrl): string
{
$baseName = Str::slug($title);
if ($baseName === '') {
$baseName = 'freepik_'.Str::lower(Str::random(10));
}
$extension = strtolower(pathinfo(parse_url($remoteUrl, PHP_URL_PATH) ?: $remoteUrl, PATHINFO_EXTENSION));
return $baseName.($extension !== '' ? '.'.$extension : '');
}
private function extensionFromMime(string $mime): string
{
return match (Str::before($mime, ';')) {
'image/jpeg' => 'jpg',
'image/png' => 'png',
'image/webp' => 'webp',
'image/svg+xml' => 'svg',
'video/mp4' => 'mp4',
'audio/mpeg' => 'mp3',
'audio/mp3' => 'mp3',
'audio/wav' => 'wav',
'application/zip' => 'zip',
default => '',
};
}
private function normalizeKind(string $kind): string
{
return match ($kind) {
'image', 'images', 'resource', 'resources' => 'images',
'video', 'videos' => 'videos',
'icon', 'icons' => 'icons',
'audio', 'music' => 'audio',
default => 'unknown',
};
}
private function numericOrNull(mixed $value): ?int
{
return is_numeric($value) ? (int) $value : null;
}
private function durationToSeconds(string $duration): ?int
{
$parts = array_map('intval', explode(':', $duration));
if (count($parts) === 3) {
return ($parts[0] * 3600) + ($parts[1] * 60) + $parts[2];
}
if (count($parts) === 2) {
return ($parts[0] * 60) + $parts[1];
}
return null;
}
private function missingApiKeyMessage(): string
{
$value = strtolower(trim((string) (
config('services.huggingface.hf_only')
?? $_ENV['TOOLS_HF_ONLY']
?? $_SERVER['TOOLS_HF_ONLY']
?? getenv('TOOLS_HF_ONLY')
?? 'false'
)));
if (in_array($value, ['1', 'true', 'yes', 'on'], true)) {
return 'Fitur stock import/download dimatikan di mode Hugging Face-only.';
}
return 'FREEPIK_API_KEY belum diisi di file .env';
}
}