Files
dreamstudio/README.md
T

54 lines
3.8 KiB
Markdown
Raw Normal View History

# dreamstudio
A Laravel app that generates images, video, and text through [Runware.ai](https://runware.ai)'s API, with every generation's real cost tracked and a hard monthly spending cap enforced before any request goes out.
It's the shared foundation for three ad-monetized products (an SEO image gallery site, a free viral tool, a faceless Shorts channel) that will sit on top of the same Runware integration — but on its own it's a working generator: submit a prompt, pick a type, get an image/video/text result, and always know exactly what it cost.
## How it works
**Generation flow** (`app/Services/Runware/GenerationService.php`)
1. Resolve which model to use — an explicit override, or the cheapest active model for that type (see "Models" below).
2. Estimate the cost of the request (`CostEstimator`) and check it against the remaining monthly budget (`BudgetGuard`) *before* calling Runware.
3. Call the API (`RunwareClient`) and persist a `Generation` row with the result.
4. Reconcile the estimate with the real cost Runware reports and convert it to EUR.
Image and text generation run synchronously in the request. Video is different — Runware generates it asynchronously, so submitting a video prompt queues `App\Jobs\GenerateVideoJob`, which submits the task, polls Runware's `getResponse` endpoint until it's done, downloads the result, and records the final cost. All generated files are downloaded and stored on local disk (`storage/app/public`, served via the `public` symlink) rather than left on Runware's CDN.
**Budget enforcement** (`app/Services/Runware/BudgetGuard.php`)
- A configurable hard cap in EUR (`RUNWARE_MONTHLY_CAP_EUR`, default 20) resets every calendar month.
- Every request is checked *before* it's sent: current month's spend + this request's estimated cost + a safety margin must stay under the cap, or it's rejected with no API call made.
- The check-and-reserve step is protected by an atomic cache lock, so two requests arriving at the same time can't both slip through right at the boundary.
- Runware bills in USD; conversion to EUR uses a live rate fetched daily (`CurrencyRateService`, via the free Frankfurter API) and cached, falling back to a configured static rate if the fetch ever fails — the cap never stops enforcing just because a rate refresh failed.
**Models & pricing** (`app/Models/RunwareModel.php`, `/runware-models`)
Runware's model-search API doesn't return pricing, so there's no way to discover "the cheapest model" from their API directly. Instead, models are entered locally (AIR id, label, unit, USD price) through the `/runware-models` admin page, and the cheapest *active* model per type is automatically flagged as the default whenever a model is added, updated, or removed — no manual "set default" step.
## Stack
Laravel 13, MySQL in production (a local sqlite fallback is used when the shared dev DB is unavailable — see `.env`), Blade + Tailwind (no frontend framework), local disk storage for generated media, database-backed queue for the video job.
## Routes
- `/generations` — submit a new generation (image/video/text) and see history + the live budget widget.
- `/runware-models` — add/activate/deactivate/delete models and their prices.
## Setup
```bash
composer install
npm install && npm run build # or npm run dev
cp .env.example .env && php artisan key:generate
php artisan migrate
php artisan storage:link
```
Set `RUNWARE_API_KEY` in `.env`, then add at least one model per type at `/runware-models` (prices from [runware.ai/pricing](https://runware.ai/pricing)). Run `php artisan queue:work` for video generations to actually process.
## Tests
```bash
php artisan test
```
Runware calls are mocked with `Http::fake()` in `tests/Feature/GenerationServiceTest.php` and `RunwareModelDefaultTest.php` — no API key needed to run the suite.