diff --git a/.gitignore b/.gitignore index 4655933..b384963 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ vendor pages/*.html -web/previews/*.jpg \ No newline at end of file +web/previews/*.jpg +.env.local \ No newline at end of file diff --git a/Pexels.md b/Pexels.md new file mode 100644 index 0000000..0f6c4df --- /dev/null +++ b/Pexels.md @@ -0,0 +1,129 @@ +--- +name: pexels-api +description: Domain knowledge for the Pexels stock photo and video API. +--- + +# Pexels API + +Pexels provides a large library of high-quality stock photos and videos licensed under the Pexels License (personal and commercial use, no attribution required but appreciated). + +## Data Model + +### Photo Resource +| Field | Type | Description | +|---|---|---| +| `id` | integer | Unique numeric photo identifier | +| `width` | integer | Original width in pixels | +| `height` | integer | Original height in pixels | +| `url` | string | Pexels web page URL for this photo | +| `photographer` | string | Photographer display name | +| `photographer_url` | string | Photographer's Pexels profile URL | +| `photographer_id` | integer | Photographer's numeric ID | +| `avg_color` | string | Hex color (e.g. `#978E82`), useful as image placeholder | +| `alt` | string | Alt text describing the photo | +| `liked` | boolean | Whether the current user liked this photo | +| `src` | object | Pre-sized image URLs (see below) | + +**`src` object sizes:** +| Key | Dimensions | Notes | +|---|---|---| +| `original` | Full resolution | Matches `width`×`height` | +| `large2x` | W 940 × H 650 @2x | Retina-ready | +| `large` | W 940 × H 650 | Standard large | +| `medium` | H 350 (proportional) | Proportionally scaled | +| `small` | H 130 (proportional) | Thumbnail | +| `portrait` | W 800 × H 1200 | Cropped portrait | +| `landscape` | W 1200 × H 627 | Cropped landscape | +| `tiny` | W 280 × H 200 | Smallest preview | + +### Video Resource +| Field | Type | Description | +|---|---|---| +| `id` | integer | Unique numeric video identifier | +| `width` | integer | Original width in pixels | +| `height` | integer | Original height in pixels | +| `url` | string | Pexels web page URL for this video | +| `image` | string | Video screenshot/poster URL | +| `duration` | integer | Duration in seconds | +| `user` | object | Videographer: `id` (int), `name` (str), `url` (str) | +| `video_files` | array | Available quality versions (see below) | +| `video_pictures` | array | Preview thumbnails: `id`, `picture` (URL), `nr` (index) | + +**`video_files` element:** +| Field | Type | Description | +|---|---|---| +| `id` | integer | File variant ID | +| `quality` | string/null | `"hd"`, `"sd"`, `"hls"`, or null | +| `file_type` | string | MIME type (e.g. `video/mp4`) | +| `width` | integer/null | Width (null for HLS) | +| `height` | integer/null | Height (null for HLS) | +| `fps` | number | Frames per second | +| `link` | string | Direct download URL | +| `size` | integer | File size in bytes | + +### Collection Resource +| Field | Type | Description | +|---|---|---| +| `id` | string | Alphanumeric collection identifier (e.g. `"8xntbhr"`) | +| `title` | string | Collection name | +| `description` | string/null | Optional description | +| `private` | boolean | Whether the collection is private | +| `media_count` | integer | Total photos + videos | +| `photos_count` | integer | Number of photos | +| `videos_count` | integer | Number of videos | + +## Entity Relationships + +- A **Photo** belongs to a **Photographer** (via `photographer_id`). +- A **Video** belongs to a **User/Videographer** (via `user.id`). +- A **Collection** contains mixed **Photo** and **Video** objects. Collection media items include an extra `type` field (`"Photo"` or `"Video"`) to discriminate. +- Collections are read-only via API — create/edit on pexels.com, iOS, or Android app. + +## API Endpoints + +### Photos (base: `/v1/`) +| Endpoint | Method | Description | +|---|---|---| +| `/v1/search?query=...` | GET | Full-text search with `orientation`, `size`, `color`, `locale` filters | +| `/v1/curated` | GET | Trending editor-curated photos, updated hourly | +| `/v1/photos/:id` | GET | Single Photo by numeric ID | + +### Videos (base: `/v1/videos/`) +| Endpoint | Method | Description | +|---|---|---| +| `/v1/videos/search?query=...` | GET | Full-text search with `orientation`, `size`, `locale` filters | +| `/v1/videos/popular` | GET | Trending videos with `min_width`, `min_height`, `min_duration`, `max_duration` filters | +| `/v1/videos/videos/:id` | GET | Single Video by numeric ID | + +### Collections (base: `/v1/collections/`) +| Endpoint | Method | Description | +|---|---|---| +| `/v1/collections/featured` | GET | Editor-curated themed collections | +| `/v1/collections` | GET | Your saved collections | +| `/v1/collections/:id` | GET | All media in a collection, filterable by `type` (photos/videos) and `sort` (asc/desc) | + +## Pagination + +All list endpoints accept `page` (default 1) and `per_page` (default 15, max 80). Responses include `page`, `per_page`, `total_results`, and optional `next_page`/`prev_page` URL strings. The `total_results` for search endpoints is capped at a high number (e.g. 8000) even when more results exist. + +## Rate Limits + +- **Default**: 200 requests/hour, 20,000 requests/month. +- **Response headers**: `X-Ratelimit-Limit` (monthly quota), `X-Ratelimit-Remaining`, `X-Ratelimit-Reset` (UNIX timestamp for monthly rollover). +- Rate limit headers are only returned on successful (2xx) responses, not on 429 errors. +- Higher limits available with proper attribution — contact api@pexels.com. + +## Search Tips & Gotchas + +- Queries can be broad ("nature", "people") or specific ("group of people working"). +- **Color filter** (photos only): Named colors (red, orange, yellow, green, turquoise, blue, violet, pink, brown, black, gray, white) or hex codes (e.g. `#ffffff`). +- **Locale** affects search relevance for non-English queries. Supported: en-US, pt-BR, es-ES, ca-ES, de-DE, it-IT, fr-FR, sv-SE, id-ID, pl-PL, ja-JP, zh-TW, zh-CN, ko-KR, th-TH, nl-NL, hu-HU, vi-VN, cs-CZ, da-DK, fi-FI, uk-UA, el-GR, ro-RO, nb-NO, sk-SK, tr-TR, ru-RU. +- **Size filter** means different things: for photos, `large` = 24MP+, `medium` = 12MP+, `small` = 4MP+; for videos, `large` = 4K, `medium` = Full HD, `small` = HD. +- Video `quality` field may be null for newer video files — use `width`/`height` to determine actual resolution. +- Collection IDs are alphanumeric strings (not integers like photo/video IDs). +- The deprecated video base URL `https://api.pexels.com/videos/` still works but should not be used — use `/v1/videos/` instead. + +## Attribution + +Best practice: link to the photo/video page on Pexels and credit the photographer/videographer (e.g. "Photo by John Doe on Pexels"). Use the `url` field from the response for attribution links. + diff --git a/README.md b/README.md index 7bb4cb5..b5e9beb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ -# AI on request +# Website on Request + + ## Benchmarks: `lama-server -hf unsloth/gemma-4-E4B-it-GGUF:UD-Q3_K_XL --reasoning off -fa on -ngl 99 -b 2048 -ub 2048 -c 4096 --temp 1.0 --top-p 0.95 --top-k 64` -> 130 t/s - `llama-server -hf unsloth/gemma-4-E2B-it-GGUF:Q4_K_S --reasoning off -fa on -ngl 99 -b 2048 -ub 2048 -c 4096 --temp 1.0 --top-p 0.95 --top-k 64` -> 187 t/s `llama-server -hf unsloth/gemma-4-E2B-it-GGUF:Q3_K_S --reasoning off -fa on -ngl 99 -b 2048 -ub 2048 -c 4096 --temp 1.0 --top-p 0.95 --top-k 64` -> 186 t/s diff --git a/web/generator.php b/web/generator.php index 131421b..79f1c68 100644 --- a/web/generator.php +++ b/web/generator.php @@ -4,6 +4,137 @@ declare(strict_types=1); require_once __DIR__ . '/../vendor/autoload.php'; +function readLocalEnvValue(string $envFilePath, string $key): string +{ + if (! is_file($envFilePath) || ! is_readable($envFilePath)) { + return ''; + } + + $lines = file($envFilePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines === false) { + return ''; + } + + foreach ($lines as $line) { + $trimmed = trim($line); + if ($trimmed === '' || str_starts_with($trimmed, '#')) { + continue; + } + + $parts = explode('=', $trimmed, 2); + if (count($parts) !== 2) { + continue; + } + + if (trim($parts[0]) !== $key) { + continue; + } + + return trim(trim($parts[1]), "\"'"); + } + + return ''; +} + +function extractHtmlTitle(string $html): string +{ + if (preg_match('/