Upload beautify
This commit is contained in:
366
README.md
366
README.md
@@ -54,6 +54,372 @@ In order to ensure that the Laravel community is welcoming to all, please review
|
||||
|
||||
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
|
||||
|
||||
## Vision & AI Auto-Tagging Integration
|
||||
|
||||
## Upload UI Feature Flag (`uploads.v2`)
|
||||
|
||||
The new React upload wizard is behind a feature flag and is **disabled by default**.
|
||||
|
||||
- Flag env var: `SKINBASE_UPLOADS_V2`
|
||||
- Config key: `features.uploads_v2`
|
||||
- Client flags source: `window.SKINBASE_FLAGS`
|
||||
|
||||
### Default behavior
|
||||
|
||||
- `SKINBASE_UPLOADS_V2=false` → legacy upload UI is rendered.
|
||||
- `SKINBASE_UPLOADS_V2=true` → `UploadWizard` is rendered.
|
||||
|
||||
### Setup
|
||||
|
||||
In `.env` (or `.env.example` for project defaults):
|
||||
|
||||
```dotenv
|
||||
SKINBASE_UPLOADS_V2=false
|
||||
```
|
||||
|
||||
Enable explicitly when ready:
|
||||
|
||||
```dotenv
|
||||
SKINBASE_UPLOADS_V2=true
|
||||
```
|
||||
|
||||
After changing env values, clear/reload config as usual:
|
||||
|
||||
```bash
|
||||
php artisan config:clear
|
||||
```
|
||||
|
||||
The system intentionally keeps legacy upload as the default until the flag is explicitly turned on.
|
||||
|
||||
## Upload Moderation UI Flow
|
||||
|
||||
Admin moderation for draft uploads is available through a dedicated queue page.
|
||||
|
||||
- Page route: `/admin/uploads/moderation`
|
||||
- Access: authenticated users with `role=admin` or `role=moderator`
|
||||
- Data source: `GET /api/admin/uploads/pending`
|
||||
|
||||
### Queue behavior
|
||||
|
||||
1. The page loads pending draft uploads (`moderation_status=pending`).
|
||||
2. Moderators can enter an optional note per upload.
|
||||
3. Approve action calls:
|
||||
|
||||
- `POST /api/admin/uploads/{id}/approve`
|
||||
- Sets moderation to approved and records moderator + timestamp.
|
||||
|
||||
4. Reject action calls:
|
||||
|
||||
- `POST /api/admin/uploads/{id}/reject`
|
||||
- Sets upload status/processing state to rejected and stores note.
|
||||
|
||||
### Publish gate
|
||||
|
||||
- Normal users can publish only when `moderation_status=approved`.
|
||||
- Admin users can publish with override behavior.
|
||||
|
||||
## Similar Artworks Analytics (A/B Evaluation)
|
||||
|
||||
The artwork page similar-items block emits two event types:
|
||||
|
||||
- `impression` (block rendered)
|
||||
- `click` (item clicked)
|
||||
|
||||
Events are stored in `similar_artwork_events` and aggregated daily into `similar_artwork_daily_metrics` by `algo_version`.
|
||||
|
||||
- Ingest endpoint: `POST /api/analytics/similar-artworks`
|
||||
- Aggregation command: `php artisan analytics:aggregate-similar-artworks --date=YYYY-MM-DD`
|
||||
- Scheduler: runs daily at `03:10`
|
||||
|
||||
## Personalized Discovery Foundation (Phase 8)
|
||||
|
||||
This foundation adds versioned, async-only ingestion and profile normalization for personalized discovery.
|
||||
|
||||
- Tables:
|
||||
- `user_interest_profiles`
|
||||
- `user_discovery_events`
|
||||
- `user_recommendation_cache`
|
||||
- Ingest endpoint: `POST /api/discovery/events` (auth required)
|
||||
- Supported event types: `view`, `click`, `favorite`, `download`
|
||||
- Processing model: non-blocking queue job (`IngestUserDiscoveryEventJob`)
|
||||
- Normalization: recency-decay + score normalization in `UserInterestProfileService`
|
||||
|
||||
No feed ranking/UI behavior is introduced in this foundation step.
|
||||
|
||||
### Feed Endpoint Skeleton
|
||||
|
||||
The backend now exposes a personalized feed API skeleton:
|
||||
|
||||
- Endpoint: `GET /api/v1/feed` (auth required)
|
||||
- Query params:
|
||||
- `limit` (1-50, default 24)
|
||||
- `cursor` (opaque cursor token for pagination)
|
||||
- `algo_version` (optional override)
|
||||
- Response includes `data` items and `meta.next_cursor` for cursor pagination.
|
||||
|
||||
Behavior:
|
||||
|
||||
- Reads `user_recommendation_cache` by `user_id + algo_version`.
|
||||
- On cache miss/stale, returns immediate fallback results and dispatches async regeneration job.
|
||||
- Regeneration runs in queue (`RegenerateUserRecommendationCacheJob`) and writes refreshed cache.
|
||||
- Includes cold-start fallback (`popular + similar`) and a diversity guard to avoid near-duplicates.
|
||||
|
||||
## Feed Analytics Instrumentation
|
||||
|
||||
Feed analytics now track:
|
||||
|
||||
- `feed_impression`
|
||||
- `feed_click`
|
||||
|
||||
Payload dimensions:
|
||||
|
||||
- `user_id` (derived from auth session)
|
||||
- `artwork_id`
|
||||
- `position`
|
||||
- `algo_version`
|
||||
- `source` (`personalized`, `cold_start`, `fallback`)
|
||||
|
||||
Optional:
|
||||
|
||||
- `dwell_seconds` (for click dwell bucket metrics)
|
||||
|
||||
Endpoints:
|
||||
|
||||
- Ingest: `POST /api/analytics/feed` (auth required)
|
||||
- Daily aggregation: `php artisan analytics:aggregate-feed --date=YYYY-MM-DD`
|
||||
- Admin report: `GET /api/admin/reports/feed-performance`
|
||||
|
||||
Daily metrics include CTR, save-rate, and dwell buckets.
|
||||
|
||||
For non-blocking client transport, use `navigator.sendBeacon` with `fetch(..., { keepalive: true })` fallback.
|
||||
Reference helper: `resources/js/lib/feedAnalytics.js`.
|
||||
|
||||
## Phase 8B: Ranking Weight Tuning (Manual + Data-Driven)
|
||||
|
||||
Discovery ranking now supports versioned blend weights per `algo_version` in `config/discovery.php`.
|
||||
|
||||
- Blend terms: `w1` interest, `w2` recency, `w3` popularity, `w4` novelty
|
||||
- Per-algo sets: `discovery.ranking.algo_weight_sets`
|
||||
- Safe rollout: deterministic traffic split by `algo_version` with config gates (`g10`, `g50`, `g100`)
|
||||
- Emergency rollback: `DISCOVERY_FORCE_ALGO_VERSION=clip-cosine-v1`
|
||||
|
||||
Offline evaluator and A/B helper:
|
||||
|
||||
- Evaluate objective across one/all algos:
|
||||
- `php artisan analytics:evaluate-feed-weights --from=YYYY-MM-DD --to=YYYY-MM-DD`
|
||||
- Optional: `--algo=clip-cosine-v1`
|
||||
- Baseline vs candidate comparison:
|
||||
- `php artisan analytics:compare-feed-ab clip-cosine-v1 clip-cosine-v2 --from=YYYY-MM-DD --to=YYYY-MM-DD`
|
||||
|
||||
Objective score uses `feed_daily_metrics` and configurable objective weights in `discovery.evaluation.objective_weights`.
|
||||
|
||||
Temporary production policy: set `DISCOVERY_EVAL_SAVE_RATE_INFORMATIONAL=true` to keep `save_rate` visible but excluded from objective score until save-event ingestion is verified.
|
||||
|
||||
Operational runbook: `docs/feed-rollout-runbook.md`.
|
||||
|
||||
## Operations / Runbooks
|
||||
|
||||
- Upload UI v2 rollout, post-deploy monitoring, and rollback: `docs/ui/upload-v2-rollout-runbook.md`
|
||||
- Feed rollout and rollback: `docs/feed-rollout-runbook.md`
|
||||
|
||||
No automatic tuning is enabled in this phase.
|
||||
|
||||
Skinbase uses asynchronous AI tagging via `AutoTagArtworkJob`.
|
||||
The job calls external vision services (CLIP and optional YOLO), normalizes tags, and attaches them through `TagService` as AI tags with confidence values.
|
||||
|
||||
### Critical Safety Rule
|
||||
|
||||
⚠️ **Publish must never depend on vision services.**
|
||||
|
||||
- Upload/publish flow dispatches AI tagging to queue after publish work.
|
||||
- Vision failures, timeouts, or service outages must not block artwork publish.
|
||||
- If AI tagging fails, artwork remains published and can be tagged later (retry/manual/batch).
|
||||
|
||||
### Environment Variables (Vision)
|
||||
|
||||
Set these in `.env` (all are optional; defaults are in `config/vision.php`):
|
||||
|
||||
#### Global
|
||||
|
||||
- `VISION_ENABLED` (default: `true`)
|
||||
- Master switch for all AI auto-tagging.
|
||||
- `VISION_QUEUE` (default: `default`)
|
||||
- Queue name used by `AutoTagArtworkJob`.
|
||||
- `VISION_IMAGE_VARIANT` (default: `md`)
|
||||
- Derivative variant sent to vision services (e.g. `md`, `lg`).
|
||||
|
||||
#### CLIP
|
||||
|
||||
- `CLIP_BASE_URL` (default: empty)
|
||||
- Base URL for CLIP service (example: `https://clip.internal`).
|
||||
- If empty, CLIP call is skipped.
|
||||
- `CLIP_ANALYZE_ENDPOINT` (default: `/analyze`)
|
||||
- Path appended to `CLIP_BASE_URL`.
|
||||
- `CLIP_TIMEOUT_SECONDS` (default: `8`)
|
||||
- Request timeout for CLIP calls.
|
||||
- `CLIP_CONNECT_TIMEOUT_SECONDS` (default: `2`)
|
||||
- Connection timeout for CLIP calls.
|
||||
- `CLIP_HTTP_RETRIES` (default: `1`)
|
||||
- HTTP retry attempts for CLIP requests.
|
||||
- `CLIP_HTTP_RETRY_DELAY_MS` (default: `200`)
|
||||
- Delay between CLIP retries.
|
||||
|
||||
#### YOLO (optional)
|
||||
|
||||
- `YOLO_ENABLED` (default: `true`)
|
||||
- Enables YOLO integration.
|
||||
- `YOLO_BASE_URL` (default: empty)
|
||||
- Base URL for YOLO service. If empty, YOLO call is skipped.
|
||||
- `YOLO_ANALYZE_ENDPOINT` (default: `/analyze`)
|
||||
- Path appended to `YOLO_BASE_URL`.
|
||||
- `YOLO_TIMEOUT_SECONDS` (default: `8`)
|
||||
- Request timeout for YOLO calls.
|
||||
- `YOLO_CONNECT_TIMEOUT_SECONDS` (default: `2`)
|
||||
- Connection timeout for YOLO calls.
|
||||
- `YOLO_HTTP_RETRIES` (default: `1`)
|
||||
- HTTP retry attempts for YOLO requests.
|
||||
- `YOLO_HTTP_RETRY_DELAY_MS` (default: `200`)
|
||||
- Delay between YOLO retries.
|
||||
- `YOLO_PHOTOGRAPHY_ONLY` (default: `true`)
|
||||
- When `true`, YOLO is called only for artworks in photography content type.
|
||||
|
||||
### Expected CLIP Response Format
|
||||
|
||||
CLIP `/analyze` should return tags as either a direct list or under `tags` / `data`:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "tag": "cyberpunk", "confidence": 0.42 },
|
||||
{ "tag": "city", "confidence": 0.31 }
|
||||
]
|
||||
```
|
||||
|
||||
Also accepted:
|
||||
|
||||
```json
|
||||
{
|
||||
"tags": [
|
||||
{ "tag": "cyberpunk", "confidence": 0.42 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{ "tag": "cyberpunk", "confidence": 0.42 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Expected YOLO Response Format
|
||||
|
||||
YOLO may return the same tag list format as CLIP, or object detections:
|
||||
|
||||
```json
|
||||
{
|
||||
"objects": [
|
||||
{ "label": "person", "confidence": 0.91 },
|
||||
{ "label": "camera", "confidence": 0.67 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`label` values are converted to tags, confidence is preserved when present.
|
||||
|
||||
### AutoTagArtworkJob Behavior
|
||||
|
||||
- Calls CLIP `/analyze` when `VISION_ENABLED=true` and `CLIP_BASE_URL` is set.
|
||||
- Optionally calls YOLO based on `YOLO_ENABLED` and `YOLO_PHOTOGRAPHY_ONLY`.
|
||||
- Merges CLIP + YOLO tags and keeps highest confidence for duplicates.
|
||||
- Normalizes tags before attach (lowercase, cleanup, slug-safe format).
|
||||
- Uses `TagService::attachAiTags()` to store pivot data:
|
||||
- `source = ai`
|
||||
- `confidence = <float|null>`
|
||||
- Runs with queue retry + timeout safety (`tries`, `backoff`, `timeout`).
|
||||
- Logs failures with reference/context for troubleshooting.
|
||||
- On non-retriable response scenarios (e.g. 4xx), job exits safely without blocking publish.
|
||||
|
||||
### Queue / Worker Requirements (`VISION_QUEUE`)
|
||||
|
||||
- Ensure a worker is running for the configured queue.
|
||||
- Example worker command:
|
||||
|
||||
```bash
|
||||
php artisan queue:work --queue=default
|
||||
```
|
||||
|
||||
- If `VISION_QUEUE=vision`, run worker for that queue:
|
||||
|
||||
```bash
|
||||
php artisan queue:work --queue=vision
|
||||
```
|
||||
|
||||
- In production, use Supervisor/systemd/Horizon to keep workers alive.
|
||||
- Without an active worker, auto-tagging jobs remain queued and will not execute.
|
||||
|
||||
### Local vs Production Notes
|
||||
|
||||
#### Local development
|
||||
|
||||
- For fully offline local work, set `VISION_ENABLED=false`.
|
||||
- Or set only `CLIP_BASE_URL`/`YOLO_BASE_URL` you can reach locally.
|
||||
- Prefer short timeouts to avoid slow dev feedback loops.
|
||||
|
||||
#### Production
|
||||
|
||||
- Use internal/private service endpoints for CLIP/YOLO when possible.
|
||||
- Keep conservative timeouts and low retry counts to prevent queue congestion.
|
||||
- Monitor failed jobs and logs for vision service reliability.
|
||||
- Scale queue workers based on upload volume and service latency.
|
||||
|
||||
### Verify Setup (Health + Test Call)
|
||||
|
||||
After configuring env vars and restarting workers, verify in this order:
|
||||
|
||||
Quick helper (PowerShell):
|
||||
|
||||
```powershell
|
||||
pwsh -File ./scripts/vision-smoke.ps1
|
||||
```
|
||||
|
||||
Optional flags:
|
||||
|
||||
```powershell
|
||||
pwsh -File ./scripts/vision-smoke.ps1 -EnvFile ".env" -SampleImageUrl "https://files.skinbase.org/img/aa/bb/cc/md.webp"
|
||||
pwsh -File ./scripts/vision-smoke.ps1 -SkipAnalyze
|
||||
```
|
||||
|
||||
1. Confirm queue worker is consuming `VISION_QUEUE`.
|
||||
|
||||
```bash
|
||||
php artisan queue:work --queue=default
|
||||
```
|
||||
|
||||
1. Check CLIP/YOLO health endpoints (replace host/port as needed):
|
||||
|
||||
```bash
|
||||
curl -fsS "$CLIP_BASE_URL/health"
|
||||
curl -fsS "$YOLO_BASE_URL/health"
|
||||
```
|
||||
|
||||
1. Make a direct analyze test call (CLIP example):
|
||||
|
||||
```bash
|
||||
curl -X POST "$CLIP_BASE_URL$CLIP_ANALYZE_ENDPOINT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"image_url":"https://files.skinbase.org/img/aa/bb/cc/md.webp"}'
|
||||
```
|
||||
|
||||
1. Trigger an upload/publish and confirm:
|
||||
|
||||
- Publish response succeeds even if CLIP/YOLO is down.
|
||||
- `AutoTagArtworkJob` is queued/executed asynchronously.
|
||||
- AI tags appear on the artwork when services are healthy.
|
||||
- Failures are logged, but publish is unaffected.
|
||||
|
||||
## License
|
||||
|
||||
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
|
||||
|
||||
Reference in New Issue
Block a user