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); | |
| use Symfony\Component\Process\Process; | |
| require __DIR__ . '/vendor/autoload.php'; | |
| function abort(string $msg): void | |
| { | |
| fwrite(STDERR, "Error: $msg\n"); | |
| // attempt rollback if possible | |
| exit(1); | |
| } | |
| // ---------- Argument handling ---------- | |
| $directory = $argv[1] ?? null; | |
| if (!$directory || !is_dir($directory)) { | |
| abort('A valid directory path must be provided.'); | |
| } | |
| $directory = rtrim(realpath($directory), DIRECTORY_SEPARATOR); | |
| $webDir = $directory . DIRECTORY_SEPARATOR . 'web'; | |
| // ---------- Prepare rollback ---------- | |
| $createdFiles = []; | |
| $createdDirs = []; | |
| function registerFile(string $path): void | |
| { | |
| global $createdFiles; | |
| $createdFiles[] = $path; | |
| } | |
| function registerDir(string $path): void | |
| { | |
| global $createdDirs; | |
| $createdDirs[] = $path; | |
| } | |
| function rollback(): void | |
| { | |
| global $createdFiles, $createdDirs; | |
| foreach (array_reverse($createdFiles) as $file) { | |
| @unlink($file); | |
| } | |
| foreach (array_reverse($createdDirs) as $dir) { | |
| @rmdir($dir); | |
| } | |
| } | |
| // ---------- Create web directory ---------- | |
| if (!mkdir($webDir) && !is_dir($webDir)) { | |
| abort("Failed to create web directory at $webDir"); | |
| } | |
| registerDir($webDir); | |
| // ---------- Scan JPEG files ---------- | |
| $images = []; | |
| $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory)); | |
| foreach ($iterator as $fileInfo) { | |
| if ($fileInfo->isFile() && preg_match('/\.(jpe?g)$/i', $fileInfo->getFilename())) { | |
| $path = $fileInfo->getPathname(); | |
| $exif = @exif_read_data($path, 'IFD0'); | |
| if ($exif === false) { | |
| abort("Failed to read EXIF data from {$fileInfo->getFilename()}"); | |
| } | |
| $timestamp = $exif['DateTimeOriginal'] ?? $exif['DateTime'] ?? null; | |
| if (!$timestamp) { | |
| abort("No timestamp in EXIF for {$fileInfo->getFilename()}"); | |
| } | |
| $images[] = [ | |
| 'path' => $path, | |
| 'filename' => $fileInfo->getFilename(), | |
| 'timestamp' => strtotime($timestamp), | |
| 'orientation' => $exif['Orientation'] ?? 1, | |
| ]; | |
| } | |
| } | |
| if (empty($images)) { | |
| abort('No JPEG images found.'); | |
| } | |
| // ---------- Sort by timestamp ---------- | |
| usort($images, fn($a, $b) => $a['timestamp'] <=> $b['timestamp']); | |
| // ---------- Generate thumbnails & previews ---------- | |
| foreach ($images as $idx => $img) { | |
| $srcPath = $img['path']; | |
| $base = pathinfo($img['filename'], PATHINFO_FILENAME); | |
| // thumbnail (max height 200) | |
| $thumbPath = $webDir . DIRECTORY_SEPARATOR . $base . '_thumb.jpg'; | |
| $cmd = [ | |
| 'convert', | |
| $srcPath, | |
| '-auto-orient', | |
| '-resize', 'x200', | |
| $thumbPath | |
| ]; | |
| $process = new Process($cmd); | |
| $process->run(); | |
| if (!$process->isSuccessful()) { | |
| abort("Thumbnail creation failed for {$img['filename']}"); | |
| } | |
| registerFile($thumbPath); | |
| // preview (max height 1000) | |
| $previewPath = $webDir . DIRECTORY_SEPARATOR . $base . '_preview.jpg'; | |
| $cmd = [ | |
| 'convert', | |
| $srcPath, | |
| '-auto-orient', | |
| '-resize', 'x1000', | |
| $previewPath | |
| ]; | |
| $process = new Process($cmd); | |
| $process->run(); | |
| if (!$process->isSuccessful()) { | |
| abort("Preview creation failed for {$img['filename']}"); | |
| } | |
| registerFile($previewPath); | |
| // store paths for template | |
| $images[$idx]['thumb'] = basename($thumbPath); | |
| $images[$idx]['preview'] = basename($previewPath); | |
| } | |
| // ---------- Create ZIP in parallel ---------- | |
| $zipName = basename($directory) . '.zip'; | |
| $zipPath = $webDir . DIRECTORY_SEPARATOR . $zipName; | |
| $zipProcess = new Process(['zip', '-j', $zipPath, ...array_column($images, 'path')]); | |
| $zipProcess->start(); // non‑blocking | |
| // ---------- Render index.html ---------- | |
| $template = file_get_contents(__DIR__ . '/template.html'); | |
| if ($template === false) { | |
| abort('Failed to load HTML template.'); | |
| } | |
| $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) { | |
| abort('Failed to write index.html'); | |
| } | |
| registerFile($indexPath); | |
| // ---------- Wait for ZIP to finish ---------- | |
| $zipProcess->wait(); | |
| if (!$zipProcess->isSuccessful()) { | |
| abort('ZIP creation failed.'); | |
| } | |
| registerFile($zipPath); | |
| // ---------- Success ---------- | |
| echo "👍\n"; |
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 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. | |
| In a parallel proces create the zip file with the name of the directory in the web directory. | |
| When something goes wrong with the creation of the directory or files rollback the changes. When everything is successful show a thumbs up emoji. | |
| Only generate the code and template. | |
| I'm using duck.ai with the GPT-OSS 120B model |
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: 10px;} | |
| .thumb img {height: 200px; cursor: pointer; border: 1px solid #ccc;} | |
| .dialog {display:none; position:fixed; top:0; left:0; width:100%; height:100%; | |
| background:rgba(0,0,0,0.8); align-items:center; justify-content:center;} | |
| .dialog img {max-height:90vh;} | |
| .dialog button {position:absolute; top:10px; right:10px; font-size:1.5rem;} | |
| </style> | |
| </head> | |
| <body> | |
| <h1>Image Gallery</h1> | |
| <a href="{{ZIP_LINK}}" download>Download ZIP</a> | |
| <div class="gallery"> | |
| {{GALLERY}} | |
| </div> | |
| <div class="dialog" id="previewDialog"> | |
| <button id="closeBtn">×</button> | |
| <img id="previewImg" src="" alt="Preview"> | |
| </div> | |
| <script> | |
| document.querySelectorAll('.thumb img').forEach(img => { | |
| img.addEventListener('click', () => { | |
| document.getElementById('previewImg').src = img.dataset.preview; | |
| document.getElementById('previewDialog').style.display = 'flex'; | |
| }); | |
| }); | |
| document.getElementById('closeBtn').addEventListener('click', () => { | |
| document.getElementById('previewDialog').style.display = 'none'; | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment