feat: add AI generation services and usage tracking
This commit is contained in:
parent
91e0930182
commit
762fa77583
12 changed files with 611 additions and 2 deletions
|
|
@ -11,6 +11,7 @@
|
|||
use App\Features\SocialAccountLimit;
|
||||
use App\Features\WorkspaceLimit;
|
||||
use App\Http\Controllers\App\Controller;
|
||||
use App\Models\AiUsageLog;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
|
|
@ -46,9 +47,9 @@ public function index(Request $request): Response|RedirectResponse
|
|||
'socialAccountLimit' => Feature::for($account)->value(SocialAccountLimit::class),
|
||||
'memberCount' => $totalMembers,
|
||||
'memberLimit' => Feature::for($account)->value(MemberLimit::class),
|
||||
'aiImagesUsed' => 0,
|
||||
'aiImagesUsed' => AiUsageLog::monthlyCount($account->id, 'image'),
|
||||
'aiImagesLimit' => Feature::for($account)->value(AiImagesLimit::class),
|
||||
'aiVideosUsed' => 0,
|
||||
'aiVideosUsed' => AiUsageLog::monthlyCount($account->id, 'video'),
|
||||
'aiVideosLimit' => Feature::for($account)->value(AiVideosLimit::class),
|
||||
'dataRetentionDays' => Feature::for($account)->value(DataRetentionDays::class),
|
||||
],
|
||||
|
|
|
|||
46
app/Models/AiUsageLog.php
Normal file
46
app/Models/AiUsageLog.php
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Database\Factories\AiUsageLogFactory;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AiUsageLog extends Model
|
||||
{
|
||||
/** @use HasFactory<AiUsageLogFactory> */
|
||||
use HasFactory, HasUuids;
|
||||
|
||||
protected $fillable = [
|
||||
'account_id',
|
||||
'workspace_id',
|
||||
'user_id',
|
||||
'post_id',
|
||||
'type',
|
||||
'provider',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['metadata' => 'array'];
|
||||
}
|
||||
|
||||
public function account(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Account::class);
|
||||
}
|
||||
|
||||
public static function monthlyCount(string $accountId, string $type): int
|
||||
{
|
||||
return static::where('account_id', $accountId)
|
||||
->where('type', $type)
|
||||
->whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->count();
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
use App\Listeners\StripeEventListener;
|
||||
use App\Models\Account;
|
||||
use App\Models\AiMessage;
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Invite;
|
||||
use App\Models\Media;
|
||||
use App\Models\Notification;
|
||||
|
|
@ -92,6 +93,7 @@ protected function configureMorphMap(): void
|
|||
Relation::enforceMorphMap([
|
||||
'account' => Account::class,
|
||||
'aiMessage' => AiMessage::class,
|
||||
'aiUsageLog' => AiUsageLog::class,
|
||||
'invite' => Invite::class,
|
||||
'media' => Media::class,
|
||||
'notification' => Notification::class,
|
||||
|
|
|
|||
85
app/Services/Ai/AudioGenerationService.php
Normal file
85
app/Services/Ai/AudioGenerationService.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AudioGenerationService
|
||||
{
|
||||
private string $apiKey;
|
||||
|
||||
private string $baseUrl = 'https://api.elevenlabs.io/v1';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('services.elevenlabs.api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, path: string, url: string, mime_type: string, type: string}
|
||||
*/
|
||||
public function generate(string $text, Workspace $workspace, ?string $userId = null, ?string $postId = null, string $voiceId = 'EXAVITQu4vr4xnSDxMaL'): array
|
||||
{
|
||||
$response = Http::timeout(120)
|
||||
->withHeaders([
|
||||
'xi-api-key' => $this->apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
'Accept' => 'audio/mpeg',
|
||||
])
|
||||
->post("{$this->baseUrl}/text-to-speech/{$voiceId}", [
|
||||
'text' => $text,
|
||||
'model_id' => 'eleven_multilingual_v2',
|
||||
'voice_settings' => [
|
||||
'stability' => 0.5,
|
||||
'similarity_boost' => 0.75,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('AudioGenerationService failed', ['body' => $response->body()]);
|
||||
|
||||
throw new \RuntimeException('Failed to generate audio. Please try again.');
|
||||
}
|
||||
|
||||
$filename = Str::uuid().'.mp3';
|
||||
$path = 'medias/'.$filename;
|
||||
|
||||
Storage::put($path, $response->body());
|
||||
|
||||
$media = $workspace->media()->create([
|
||||
'group_id' => Str::uuid()->toString(),
|
||||
'collection' => 'assets',
|
||||
'type' => 'document',
|
||||
'path' => $path,
|
||||
'original_filename' => 'ai-generated.mp3',
|
||||
'mime_type' => 'audio/mpeg',
|
||||
'size' => strlen($response->body()),
|
||||
'order' => 0,
|
||||
'meta' => ['ai_generated' => true, 'text' => Str::limit($text, 200)],
|
||||
]);
|
||||
|
||||
AiUsageLog::create([
|
||||
'account_id' => $workspace->account_id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $userId,
|
||||
'post_id' => $postId,
|
||||
'type' => 'audio',
|
||||
'provider' => 'elevenlabs',
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'path' => $media->path,
|
||||
'url' => $media->url,
|
||||
'mime_type' => 'audio/mpeg',
|
||||
'type' => 'audio',
|
||||
];
|
||||
}
|
||||
}
|
||||
91
app/Services/Ai/ImageGenerationService.php
Normal file
91
app/Services/Ai/ImageGenerationService.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ImageGenerationService
|
||||
{
|
||||
private string $apiKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('ai.providers.gemini.api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, path: string, url: string, mime_type: string, type: string}
|
||||
*/
|
||||
public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null): array
|
||||
{
|
||||
$response = Http::timeout(120)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key={$this->apiKey}", [
|
||||
'contents' => [['parts' => [['text' => $prompt]]]],
|
||||
'generationConfig' => ['responseModalities' => ['TEXT', 'IMAGE']],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('ImageGenerationService failed', ['body' => $response->body()]);
|
||||
|
||||
throw new \RuntimeException('Failed to generate image. Please try again.');
|
||||
}
|
||||
|
||||
$parts = data_get($response->json(), 'candidates.0.content.parts', []);
|
||||
|
||||
foreach ($parts as $part) {
|
||||
if (data_get($part, 'inlineData')) {
|
||||
$imageData = base64_decode(data_get($part, 'inlineData.data'));
|
||||
$mimeType = data_get($part, 'inlineData.mimeType', 'image/png');
|
||||
$extension = match ($mimeType) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/webp' => 'webp',
|
||||
default => 'png',
|
||||
};
|
||||
|
||||
$filename = Str::uuid().'.'.$extension;
|
||||
$path = 'medias/'.$filename;
|
||||
|
||||
Storage::put($path, $imageData);
|
||||
|
||||
$media = $workspace->media()->create([
|
||||
'group_id' => Str::uuid()->toString(),
|
||||
'collection' => 'assets',
|
||||
'type' => 'image',
|
||||
'path' => $path,
|
||||
'original_filename' => 'ai-generated.'.$extension,
|
||||
'mime_type' => $mimeType,
|
||||
'size' => strlen($imageData),
|
||||
'order' => 0,
|
||||
'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)],
|
||||
]);
|
||||
|
||||
AiUsageLog::create([
|
||||
'account_id' => $workspace->account_id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $userId,
|
||||
'post_id' => $postId,
|
||||
'type' => 'image',
|
||||
'provider' => 'gemini',
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'path' => $media->path,
|
||||
'url' => $media->url,
|
||||
'mime_type' => $mimeType,
|
||||
'type' => 'image',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
throw new \RuntimeException('No image was generated. Try a different prompt.');
|
||||
}
|
||||
}
|
||||
37
app/Services/Ai/IntentDetector.php
Normal file
37
app/Services/Ai/IntentDetector.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
class IntentDetector
|
||||
{
|
||||
public function detect(string $prompt): string
|
||||
{
|
||||
$lower = mb_strtolower($prompt);
|
||||
|
||||
$videoKeywords = ['video', 'clip', 'reel', 'animation', 'animate', 'footage'];
|
||||
$imageKeywords = ['image', 'photo', 'picture', 'illustration', 'draw', 'design', 'visual', 'graphic'];
|
||||
$audioKeywords = ['audio', 'voice', 'narrate', 'speak', 'tts', 'voiceover', 'text to speech'];
|
||||
|
||||
foreach ($videoKeywords as $keyword) {
|
||||
if (str_contains($lower, $keyword)) {
|
||||
return 'video';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($imageKeywords as $keyword) {
|
||||
if (str_contains($lower, $keyword)) {
|
||||
return 'image';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($audioKeywords as $keyword) {
|
||||
if (str_contains($lower, $keyword)) {
|
||||
return 'audio';
|
||||
}
|
||||
}
|
||||
|
||||
return 'text';
|
||||
}
|
||||
}
|
||||
55
app/Services/Ai/TextGenerationService.php
Normal file
55
app/Services/Ai/TextGenerationService.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TextGenerationService
|
||||
{
|
||||
private string $apiKey;
|
||||
|
||||
private string $baseUrl = 'https://api.openai.com/v1';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('ai.providers.openai.api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, array{role: string, content: string}> $history
|
||||
*/
|
||||
public function generate(string $prompt, array $history = []): string
|
||||
{
|
||||
$messages = [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'You are a social media content expert. Help users write engaging captions, hashtags, and post content. Be creative, concise, and on-brand. Respond in the same language the user writes in.',
|
||||
],
|
||||
...$history,
|
||||
['role' => 'user', 'content' => $prompt],
|
||||
];
|
||||
|
||||
$response = Http::timeout(60)
|
||||
->withHeaders([
|
||||
'Authorization' => "Bearer {$this->apiKey}",
|
||||
'Content-Type' => 'application/json',
|
||||
])
|
||||
->post("{$this->baseUrl}/chat/completions", [
|
||||
'model' => 'gpt-4o',
|
||||
'messages' => $messages,
|
||||
'max_tokens' => 2048,
|
||||
'temperature' => 0.7,
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('TextGenerationService failed', ['body' => $response->body()]);
|
||||
|
||||
throw new \RuntimeException('Failed to generate text. Please try again.');
|
||||
}
|
||||
|
||||
return data_get($response->json(), 'choices.0.message.content', '');
|
||||
}
|
||||
}
|
||||
112
app/Services/Ai/VideoGenerationService.php
Normal file
112
app/Services/Ai/VideoGenerationService.php
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class VideoGenerationService
|
||||
{
|
||||
private string $apiKey;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->apiKey = config('ai.providers.gemini.api_key', '');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{id: string, path: string, url: string, mime_type: string, type: string}
|
||||
*/
|
||||
public function generate(string $prompt, Workspace $workspace, ?string $userId = null, ?string $postId = null): array
|
||||
{
|
||||
$response = Http::timeout(30)
|
||||
->withHeaders(['Content-Type' => 'application/json'])
|
||||
->post("https://generativelanguage.googleapis.com/v1beta/models/veo-3.0-generate-preview:predictLongRunning?key={$this->apiKey}", [
|
||||
'instances' => [['prompt' => $prompt]],
|
||||
'parameters' => [
|
||||
'aspectRatio' => '9:16',
|
||||
'sampleCount' => 1,
|
||||
'durationSeconds' => 8,
|
||||
'generateAudio' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
Log::error('VideoGenerationService start failed', ['body' => $response->body()]);
|
||||
|
||||
throw new \RuntimeException('Failed to start video generation. Please try again.');
|
||||
}
|
||||
|
||||
$operationName = data_get($response->json(), 'name');
|
||||
|
||||
if (! $operationName) {
|
||||
throw new \RuntimeException('No operation returned from video generation API.');
|
||||
}
|
||||
|
||||
$maxAttempts = 30;
|
||||
$videoData = null;
|
||||
|
||||
for ($i = 0; $i < $maxAttempts; $i++) {
|
||||
sleep(10);
|
||||
|
||||
$statusResponse = Http::timeout(30)
|
||||
->get("https://generativelanguage.googleapis.com/v1beta/{$operationName}?key={$this->apiKey}");
|
||||
|
||||
if ($statusResponse->failed()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = $statusResponse->json();
|
||||
|
||||
if (data_get($status, 'done')) {
|
||||
$videoData = data_get($status, 'response.predictions.0.bytesBase64Encoded');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $videoData) {
|
||||
throw new \RuntimeException('Video generation timed out. Please try again.');
|
||||
}
|
||||
|
||||
$decoded = base64_decode($videoData);
|
||||
$filename = Str::uuid().'.mp4';
|
||||
$path = 'medias/'.$filename;
|
||||
|
||||
Storage::put($path, $decoded);
|
||||
|
||||
$media = $workspace->media()->create([
|
||||
'group_id' => Str::uuid()->toString(),
|
||||
'collection' => 'assets',
|
||||
'type' => 'video',
|
||||
'path' => $path,
|
||||
'original_filename' => 'ai-generated.mp4',
|
||||
'mime_type' => 'video/mp4',
|
||||
'size' => strlen($decoded),
|
||||
'order' => 0,
|
||||
'meta' => ['ai_generated' => true, 'prompt' => Str::limit($prompt, 200)],
|
||||
]);
|
||||
|
||||
AiUsageLog::create([
|
||||
'account_id' => $workspace->account_id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'user_id' => $userId,
|
||||
'post_id' => $postId,
|
||||
'type' => 'video',
|
||||
'provider' => 'veo',
|
||||
]);
|
||||
|
||||
return [
|
||||
'id' => $media->id,
|
||||
'path' => $media->path,
|
||||
'url' => $media->url,
|
||||
'mime_type' => 'video/mp4',
|
||||
'type' => 'video',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -122,4 +122,8 @@
|
|||
'api_key' => env('GIPHY_API_KEY'),
|
||||
],
|
||||
|
||||
'elevenlabs' => [
|
||||
'api_key' => env('ELEVENLABS_API_KEY'),
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
48
database/factories/AiUsageLogFactory.php
Normal file
48
database/factories/AiUsageLogFactory.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Account;
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Workspace;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/** @extends Factory<AiUsageLog> */
|
||||
class AiUsageLogFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'account_id' => Account::factory(),
|
||||
'workspace_id' => Workspace::factory(),
|
||||
'type' => fake()->randomElement(['image', 'video', 'audio']),
|
||||
'provider' => fake()->randomElement(['gemini', 'veo', 'elevenlabs']),
|
||||
];
|
||||
}
|
||||
|
||||
public function image(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'type' => 'image',
|
||||
'provider' => 'gemini',
|
||||
]);
|
||||
}
|
||||
|
||||
public function video(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'type' => 'video',
|
||||
'provider' => 'veo',
|
||||
]);
|
||||
}
|
||||
|
||||
public function audio(): static
|
||||
{
|
||||
return $this->state(fn () => [
|
||||
'type' => 'audio',
|
||||
'provider' => 'elevenlabs',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('ai_usage_logs', function (Blueprint $table) {
|
||||
$table->uuid('id')->primary();
|
||||
$table->uuid('account_id');
|
||||
$table->uuid('workspace_id');
|
||||
$table->uuid('user_id')->nullable();
|
||||
$table->uuid('post_id')->nullable();
|
||||
$table->string('type'); // image, video, audio
|
||||
$table->string('provider')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('account_id')->references('id')->on('accounts')->cascadeOnDelete();
|
||||
$table->index(['account_id', 'type', 'created_at']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('ai_usage_logs');
|
||||
}
|
||||
};
|
||||
95
tests/Feature/AiUsageLogTest.php
Normal file
95
tests/Feature/AiUsageLogTest.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Account;
|
||||
use App\Models\AiUsageLog;
|
||||
use App\Models\Workspace;
|
||||
use App\Services\Ai\IntentDetector;
|
||||
|
||||
test('monthly count returns correct count for account and type', function () {
|
||||
$account = Account::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['account_id' => $account->id]);
|
||||
|
||||
AiUsageLog::factory()->image()->count(3)->create([
|
||||
'account_id' => $account->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
]);
|
||||
|
||||
AiUsageLog::factory()->video()->count(2)->create([
|
||||
'account_id' => $account->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
]);
|
||||
|
||||
expect(AiUsageLog::monthlyCount($account->id, 'image'))->toBe(3);
|
||||
expect(AiUsageLog::monthlyCount($account->id, 'video'))->toBe(2);
|
||||
expect(AiUsageLog::monthlyCount($account->id, 'audio'))->toBe(0);
|
||||
});
|
||||
|
||||
test('monthly count excludes logs from other months', function () {
|
||||
$account = Account::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['account_id' => $account->id]);
|
||||
|
||||
AiUsageLog::factory()->image()->create([
|
||||
'account_id' => $account->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
]);
|
||||
|
||||
AiUsageLog::factory()->image()->create([
|
||||
'account_id' => $account->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
'created_at' => now()->subMonth(),
|
||||
]);
|
||||
|
||||
expect(AiUsageLog::monthlyCount($account->id, 'image'))->toBe(1);
|
||||
});
|
||||
|
||||
test('monthly count excludes logs from other accounts', function () {
|
||||
$account = Account::factory()->create();
|
||||
$otherAccount = Account::factory()->create();
|
||||
$workspace = Workspace::factory()->create(['account_id' => $account->id]);
|
||||
$otherWorkspace = Workspace::factory()->create(['account_id' => $otherAccount->id]);
|
||||
|
||||
AiUsageLog::factory()->image()->create([
|
||||
'account_id' => $account->id,
|
||||
'workspace_id' => $workspace->id,
|
||||
]);
|
||||
|
||||
AiUsageLog::factory()->image()->create([
|
||||
'account_id' => $otherAccount->id,
|
||||
'workspace_id' => $otherWorkspace->id,
|
||||
]);
|
||||
|
||||
expect(AiUsageLog::monthlyCount($account->id, 'image'))->toBe(1);
|
||||
});
|
||||
|
||||
test('intent detector detects video intent', function () {
|
||||
$detector = new IntentDetector;
|
||||
|
||||
expect($detector->detect('Create a video for my product'))->toBe('video');
|
||||
expect($detector->detect('Make a reel about coffee'))->toBe('video');
|
||||
expect($detector->detect('Animate this logo'))->toBe('video');
|
||||
});
|
||||
|
||||
test('intent detector detects image intent', function () {
|
||||
$detector = new IntentDetector;
|
||||
|
||||
expect($detector->detect('Generate an image of a sunset'))->toBe('image');
|
||||
expect($detector->detect('Draw me a logo'))->toBe('image');
|
||||
expect($detector->detect('Create a visual for my post'))->toBe('image');
|
||||
});
|
||||
|
||||
test('intent detector detects audio intent', function () {
|
||||
$detector = new IntentDetector;
|
||||
|
||||
expect($detector->detect('Create a voiceover for this text'))->toBe('audio');
|
||||
expect($detector->detect('Convert this to audio narration'))->toBe('audio');
|
||||
expect($detector->detect('Generate TTS for my caption'))->toBe('audio');
|
||||
});
|
||||
|
||||
test('intent detector defaults to text', function () {
|
||||
$detector = new IntentDetector;
|
||||
|
||||
expect($detector->detect('Write a caption for my post'))->toBe('text');
|
||||
expect($detector->detect('Help me with hashtags'))->toBe('text');
|
||||
});
|
||||
Loading…
Reference in a new issue