Last active
October 11, 2025 07:50
-
-
Save xwero/f2de7cef0a0576687fd8824fc6d74837 to your computer and use it in GitHub Desktop.
LLM image gallery generator first prompt output
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env php | |
| <?php | |
| declare(strict_types=1); | |
| require __DIR__ . '/vendor/autoload.php'; // Composer autoloader (if used) | |
| use Symfony\Component\Process\Process; | |
| /* ------------------------------------------------------------- | |
| Helper functions (no global state) | |
| ------------------------------------------------------------- */ | |
| function fail(string $msg, array $files, array $dirs): void | |
| { | |
| // rollback | |
| foreach (array_reverse($files) as $f) { @unlink($f); } | |
| foreach (array_reverse($dirs) as $d) { @rmdir($d); } | |
| fwrite(STDERR, "❌ $msg\n"); | |
| echo "👎\n"; | |
| exit(1); | |
| } | |
| function success(): void | |
| { | |
| echo "👍\n"; | |
| } | |
| /* ------------------------------------------------------------- | |
| Argument validation | |
| ------------------------------------------------------------- */ | |
| if ($argc !== 2) { | |
| fwrite(STDERR, "Usage: php image-gallery.php <directory>\n"); | |
| exit(1); | |
| } | |
| $sourceDir = realpath($argv[1]); | |
| if ($sourceDir === false || !is_dir($sourceDir)) { | |
| fwrite(STDERR, "Invalid directory.\n"); | |
| exit(1); | |
| } | |
| /* ------------------------------------------------------------- | |
| Prepare paths and rollback containers | |
| ------------------------------------------------------------- */ | |
| $webDir = $sourceDir . DIRECTORY_SEPARATOR . 'web'; | |
| $zipName = basename($sourceDir) . '.zip'; | |
| $zipPath = $webDir . DIRECTORY_SEPARATOR . $zipName; | |
| $createdFiles = []; | |
| $createdDirs = []; | |
| /* ------------------------------------------------------------- | |
| 1️⃣ Create web directory | |
| ------------------------------------------------------------- */ | |
| if (!mkdir($webDir) && !is_dir($webDir)) { | |
| fail("Unable to create web directory.", $createdFiles, $createdDirs); | |
| } | |
| $createdDirs[] = $webDir; | |
| /* ------------------------------------------------------------- | |
| 2️⃣ Scan JPEGs and read EXIF timestamps | |
| ------------------------------------------------------------- */ | |
| $images = []; | |
| $it = new RecursiveIteratorIterator( | |
| new RecursiveDirectoryIterator($sourceDir, FilesystemIterator::SKIP_DOTS) | |
| ); | |
| foreach ($it as $fi) { | |
| if (!$fi->isFile()) continue; | |
| if (!preg_match('/\.(jpe?g)$/i', $fi->getFilename())) continue; | |
| $exif = @exif_read_data($fi->getPathname(), 'IFD0'); | |
| if ($exif === false) { | |
| fail("Failed to read EXIF from {$fi->getFilename()}.", $createdFiles, $createdDirs); | |
| } | |
| $ts = $exif['DateTimeOriginal'] ?? $exif['DateTime'] ?? null; | |
| if ($ts === null) { | |
| fail("No timestamp in EXIF for {$fi->getFilename()}.", $createdFiles, $createdDirs); | |
| } | |
| $images[] = [ | |
| 'path' => $fi->getPathname(), | |
| 'filename' => $fi->getFilename(), | |
| 'timestamp' => strtotime($ts), | |
| 'base' => pathinfo($fi->getFilename(), PATHINFO_FILENAME), | |
| ]; | |
| } | |
| if (empty($images)) { | |
| fail('No JPEG images found.', $createdFiles, $createdDirs); | |
| } | |
| /* ------------------------------------------------------------- | |
| 3️⃣ Sort by timestamp (oldest → newest) | |
| ------------------------------------------------------------- */ | |
| usort($images, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']); | |
| /* ------------------------------------------------------------- | |
| 4️⃣ Generate thumbnails & previews in parallel (pcntl_fork) | |
| ------------------------------------------------------------- */ | |
| $children = []; | |
| foreach ($images as $idx => $img) { | |
| $pid = pcntl_fork(); | |
| if ($pid === -1) { | |
| fail('Failed to fork for image processing.', $createdFiles, $createdDirs); | |
| } | |
| if ($pid === 0) { // child | |
| // thumbnail (max height 200) | |
| $thumb = $webDir . DIRECTORY_SEPARATOR . $img['base'] . '_thumb.jpg'; | |
| $proc = new Process(['convert', $img['path'], '-auto-orient', '-resize', 'x200', $thumb]); | |
| $proc->run(); | |
| if (!$proc->isSuccessful()) { | |
| exit(1); | |
| } | |
| // preview (max height 1000) | |
| $preview = $webDir . DIRECTORY_SEPARATOR . $img['base'] . '_preview.jpg'; | |
| $proc = new Process(['convert', $img['path'], '-auto-orient', '-resize', 'x1000', $preview]); | |
| $proc->run(); | |
| if (!$proc->isSuccessful()) { | |
| exit(1); | |
| } | |
| // communicate filenames back via temporary file | |
| $tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "img_{$idx}_" . getmypid() . '.txt'; | |
| file_put_contents($tmp, json_encode([ | |
| 'thumb' => basename($thumb), | |
| 'preview' => basename($preview), | |
| ])); | |
| exit(0); | |
| } | |
| $children[] = $pid; | |
| } | |
| /* ------------------------------------------------------------- | |
| 5️⃣ Wait for all image workers and collect filenames | |
| ------------------------------------------------------------- */ | |
| foreach ($children as $pid) { | |
| pcntl_waitpid($pid, $status); | |
| if ($status !== 0) { | |
| fail('One of the image workers failed.', $createdFiles, $createdDirs); | |
| } | |
| } | |
| foreach (glob(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'img_*_*.txt') as $tmpFile) { | |
| $data = json_decode(file_get_contents($tmpFile), true); | |
| $base = pathinfo($tmpFile, PATHINFO_FILENAME); | |
| $idx = (int)explode('_', $base)[1]; | |
| $images[$idx]['thumb'] = $data['thumb']; | |
| $images[$idx]['preview'] = $data['preview']; | |
| @unlink($tmpFile); | |
| $createdFiles[] = $webDir . DIRECTORY_SEPARATOR . $data['thumb']; | |
| $createdFiles[] = $webDir . DIRECTORY_SEPARATOR . $data['preview']; | |
| } | |
| /* ------------------------------------------------------------- | |
| 6️⃣ Create ZIP archive in a separate process | |
| ------------------------------------------------------------- */ | |
| $zipPid = pcntl_fork(); | |
| if ($zipPid === -1) { | |
| fail('Failed to fork for ZIP creation.', $createdFiles, $createdDirs); | |
| } | |
| if ($zipPid === 0) { // child | |
| $paths = array_column($images, 'path'); | |
| $proc = new Process(array_merge(['zip', '-j', $zipPath], $paths)); | |
| $proc->run(); | |
| if (!$proc->isSuccessful()) { | |
| exit(1); | |
| } | |
| exit(0); | |
| } | |
| pcntl_waitpid($zipPid, $zipStatus); | |
| if ($zipStatus !== 0) { | |
| fail('ZIP creation failed.', $createdFiles, $createdDirs); | |
| } | |
| $createdFiles[] = $zipPath; | |
| /* ------------------------------------------------------------- | |
| 7️⃣ Render index.html from template | |
| ------------------------------------------------------------- */ | |
| $templatePath = __DIR__ . '/template.html'; | |
| $template = @file_get_contents($templatePath); | |
| if ($template === false) { | |
| fail('Unable to read HTML template.', $createdFiles, $createdDirs); | |
| } | |
| $galleryHtml = ''; | |
| foreach ($images as $img) { | |
| $galleryHtml .= <<<HTML | |
| <div class="thumb"> | |
| <img src="{$img['thumb']}" data-preview="{$img['preview']}" alt="{$img['filename']}"> | |
| </div> | |
| HTML; | |
| } | |
| $rendered = str_replace(['{{GALLERY}}', '{{ZIP_LINK}}'], [$galleryHtml, $zipName], $template); | |
| $indexPath = $webDir . DIRECTORY_SEPARATOR . 'index.html'; | |
| if (file_put_contents($indexPath, $rendered) === false) { | |
| fail('Failed to write index.html.', $createdFiles, $createdDirs); | |
| } | |
| $createdFiles[] = $indexPath; | |
| /* ------------------------------------------------------------- | |
| 8️⃣ All done | |
| ------------------------------------------------------------- */ | |
| success(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Create a CLI command image-gallery, that runs PHP code, that accepts a directory as an argument. | |
| The command searches for all jpegs in the directory and extracts the EXIF information. | |
| Once that is done it creates a child directory named web, and an index.html page from a template. | |
| The template contains the images that are sorted by the timestamp in the EXIF information. The images are tumbnails and by clicking on them a dialog, use the dialog tag, should open to see a preview image. The template should also contain a link to a zip file with the name of the directory that is stored in the web directory. | |
| After creating the index.html file create the tumbnail and preview images base on the aspect ratio that is found in de EXIF information. The thumbnails should be maximum 200 pixels in height and the preview images should be maximum 1000 pixels in height. | |
| Also create the zip file with the name of the directory in the web directory. | |
| The creating of the images and zip file should be done in parallel. | |
| When something goes wrong with the creation of the directory or files rollback the changes and show a thumbs down emoji. When everything is successful show a thumbs up emoji. | |
| Use best practices like not using globals. | |
| Only generate the code and template. | |
| I'm using duck.ai with the GPT-OSS 120B model. I added an extra prompt to not use classes. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Image Gallery</title> | |
| <style> | |
| body {font-family: Arial, sans-serif; margin: 20px;} | |
| .gallery {display: flex; flex-wrap: wrap; gap: 12px;} | |
| .thumb img {height: 200px; cursor: pointer; border: 1px solid #ccc;} | |
| dialog {border: none; border-radius: 8px; max-width: 90vw; max-height: 90vh;} | |
| dialog img {max-height: 80vh; max-width: 100%;} | |
| dialog::backdrop {background: rgba(0,0,0,0.6);} | |
| .close-btn {position: absolute; top: 8px; right: 12px; background: transparent; | |
| border: none; font-size: 1.5rem; cursor: pointer; color: #fff;} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Image Gallery</h1> | |
| <a href="{{ZIP_LINK}}" download>Download ZIP</a> | |
| <div class="gallery"> | |
| {{GALLERY}} | |
| </div> | |
| <!-- Dialog element reused for every preview --> | |
| <dialog id="previewDialog"> | |
| <button class="close-btn" id="closeDialog">×</button> | |
| <img id="previewImg" src="" alt="Preview"> | |
| </dialog> | |
| <script> | |
| const dialog = document.getElementById('previewDialog'); | |
| const previewImg = document.getElementById('previewImg'); | |
| const closeBtn = document.getElementById('closeDialog'); | |
| document.querySelectorAll('.thumb img').forEach(img => { | |
| img.addEventListener('click', () => { | |
| previewImg.src = img.dataset.preview; | |
| dialog.showModal(); | |
| }); | |
| }); | |
| closeBtn.addEventListener('click', () => dialog.close()); | |
| dialog.addEventListener('click', e => { | |
| if (e.target === dialog) dialog.close(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment