teknolis / app /Services /FreepikMusicService.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
6.87 kB
<?php
namespace App\Services;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use RuntimeException;
class FreepikMusicService
{
public function generate(string $prompt, int $duration = 15): array
{
$apiKey = trim((string) config('services.freepik.api_key', ''));
$timeout = max(20, (int) config('services.freepik.timeout', 90));
$cleanPrompt = trim($prompt);
$duration = max(5, min(60, $duration));
if ($apiKey === '') {
throw new RuntimeException($this->missingApiKeyMessage('Music generator'));
}
if ($cleanPrompt === '') {
throw new RuntimeException('Prompt musik Freepik tidak boleh kosong.');
}
$response = $this->client($apiKey, $timeout)
->post($this->generateEndpoint(), [
'prompt' => $cleanPrompt,
'duration' => $duration,
]);
$data = $this->extractData($response, 'Freepik music request gagal');
$audioUrl = $this->extractGeneratedUrl($data);
if ($audioUrl === null) {
$taskId = trim((string) data_get($data, 'task_id', data_get($data, 'id', '')));
if ($taskId === '') {
throw new RuntimeException('Freepik music tidak mengembalikan task_id.');
}
$audioUrl = $this->awaitGeneratedUrl($apiKey, $timeout, $taskId);
}
return [
'url' => $audioUrl,
'provider' => 'freepik-music',
'duration_seconds' => $duration,
'prompt' => $cleanPrompt,
];
}
private function awaitGeneratedUrl(string $apiKey, int $timeout, string $taskId): string
{
$deadline = microtime(true) + max(35, min(240, $timeout * 2));
$lastError = null;
do {
try {
$response = $this->client($apiKey, min(25, $timeout))
->get($this->statusEndpoint($taskId));
$data = $this->extractData($response, 'Gagal membaca status task Freepik music');
$generatedUrl = $this->extractGeneratedUrl($data);
if ($generatedUrl !== null) {
return $generatedUrl;
}
$status = strtoupper(trim((string) data_get($data, 'status', '')));
if (in_array($status, ['FAILED', 'ERROR', 'REJECTED', 'CANCELLED'], true)) {
throw new RuntimeException('Freepik music task berakhir dengan status '.$status.'.');
}
} catch (\Throwable $pollError) {
$lastError = $pollError;
}
usleep(1500000);
} while (microtime(true) < $deadline);
if ($lastError !== null) {
throw new RuntimeException('Timeout saat menunggu hasil musik dari Freepik. Detail terakhir: '.$lastError->getMessage());
}
throw new RuntimeException('Timeout saat menunggu hasil musik dari Freepik.');
}
private function client(string $apiKey, int $timeout)
{
return Http::connectTimeout(20)
->timeout($timeout)
->acceptJson()
->withHeaders([
'x-freepik-api-key' => $apiKey,
]);
}
private function generateEndpoint(): string
{
$configured = trim((string) config('services.freepik.music_generate_endpoint', ''));
if ($configured !== '') {
return $configured;
}
return rtrim($this->apiOrigin(), '/').'/v1/ai/music-generation/generate';
}
private function statusEndpoint(string $taskId): string
{
$configured = trim((string) config('services.freepik.music_status_endpoint', ''));
if ($configured !== '') {
return rtrim($configured, '/').'/'.$taskId;
}
return rtrim($this->apiOrigin(), '/').'/v1/ai/music-generation/'.$taskId;
}
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
{
if (! $response->successful()) {
throw new RuntimeException($prefix.': '.$response->status().' - '.$this->responseError($response));
}
$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 missingApiKeyMessage(string $feature): 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 $feature.' belum tersedia di mode Hugging Face-only.';
}
return 'FREEPIK_API_KEY belum diisi di file .env';
}
private function extractGeneratedUrl(array $data): ?string
{
$candidates = data_get($data, 'generated', []);
if (! is_array($candidates)) {
$candidates = [];
}
if ($candidates === []) {
$single = data_get($data, 'url', data_get($data, 'audio_url'));
if (is_string($single) && Str::startsWith($single, ['http://', 'https://'])) {
return trim($single);
}
return null;
}
foreach ($candidates as $candidate) {
if (is_string($candidate) && Str::startsWith($candidate, ['http://', 'https://'])) {
return trim($candidate);
}
if (is_array($candidate)) {
$url = data_get($candidate, 'url', data_get($candidate, 'audio_url'));
if (is_string($url) && Str::startsWith(trim($url), ['http://', 'https://'])) {
return trim($url);
}
}
}
return null;
}
}