Last active
June 5, 2025 03:06
-
-
Save psiborg/c4da7a48d67b8ac6846ba0956aa78409 to your computer and use it in GitHub Desktop.
Shell Scripts
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 bash | |
# Usage: ./describe_diagrams.sh /path/to/folder | |
# Exit on error, treat unset variables as an error | |
set -euo pipefail | |
# Ensure folder argument is provided | |
if [ -z "$1" ]; then | |
echo "Usage: $0 /path/to/folder" | |
exit 1 | |
fi | |
FOLDER="$1" | |
# Check if ImageMagick's 'convert' is installed | |
if ! command -v convert &> /dev/null; then | |
echo "Error: 'convert' (ImageMagick) is required but not installed." | |
exit 1 | |
fi | |
# Check if ollama is available | |
if ! command -v ollama &> /dev/null; then | |
echo "Error: 'ollama' is required but not installed or not in PATH." | |
exit 1 | |
fi | |
# --- Define and create output directories --- | |
# These will be subdirectories within the main FOLDER | |
TEXT_OUTPUT_SUBDIR="text" | |
TEMP_IMAGE_SUBDIR="temp" | |
TEXT_OUTPUT_DIR="${FOLDER}/${TEXT_OUTPUT_SUBDIR}" | |
TEMP_IMAGE_DIR="${FOLDER}/${TEMP_IMAGE_SUBDIR}" | |
echo "Ensuring text output directory exists: $TEXT_OUTPUT_DIR" | |
mkdir -p "$TEXT_OUTPUT_DIR" | |
echo "Ensuring temporary image directory exists: $TEMP_IMAGE_DIR" | |
mkdir -p "$TEMP_IMAGE_DIR" | |
# --- End of output directory creation --- | |
echo # Add a newline for better readability | |
# Loop through image files (case-insensitive) using process substitution and NUL delimiters | |
while IFS= read -r -d $'\0' IMAGE; do | |
BASENAME=$(basename "$IMAGE") | |
NAME="${BASENAME%.*}" | |
# DIRNAME=$(dirname "$IMAGE") | |
TXTFILE="${TEXT_OUTPUT_DIR}/${NAME}.txt" | |
EXT_NO_DOT="${IMAGE##*.}" | |
EXT_LOWER="${EXT_NO_DOT,,}" # Bash 4+ for lowercase | |
echo "Processing: $BASENAME" | |
ANALYZE_IMAGE="$IMAGE" # Default to original image | |
PATH_TO_FLATTENED_GIF="" # Path for the temporary flattened GIF, if created | |
# Prepare image for analysis | |
if [[ "$EXT_LOWER" == "gif" ]]; then | |
# Flattened GIF will go into the TEMP_IMAGE_DIR | |
PATH_TO_FLATTENED_GIF="${TEMP_IMAGE_DIR}/${NAME}_flattened.png" | |
echo "Flattening animated GIF..." | |
if convert "$IMAGE" -coalesce -layers merge +repage "$PATH_TO_FLATTENED_GIF"; then | |
ANALYZE_IMAGE="$PATH_TO_FLATTENED_GIF" # ollama should analyze this flattened version | |
else | |
echo "Error: Failed to flatten GIF: $IMAGE. Skipping." | |
rm -f "$PATH_TO_FLATTENED_GIF" # Attempt to clean up partial file on error | |
continue | |
fi | |
fi | |
# Prompt tailored for system design and technical diagrams | |
PROMPT="You are a technical writer preparing a 1-paragraph summary for a software architecture diagram to be used in a product or solution catalog. Summarize the purpose of the system, highlight the major components (such as services, databases, APIs, etc.), and explain how they interact at a high level. Use clear and professional language suitable for software engineers, architects, and decision-makers. Do not include implementation details or code — focus on structure and flow." | |
# Run ollama and write output to txt file | |
echo "Running ollama analysis..." | |
# Redirect ollama's stdin from /dev/null | |
if ollama run llava "$PROMPT" "$ANALYZE_IMAGE" > "$TXTFILE" < /dev/null; then | |
echo "Saved description." | |
else | |
echo "Error: ollama command failed for $ANALYZE_IMAGE. Check $TXTFILE for partial output or errors." | |
# Optionally, you could 'continue' here if you don't want to attempt cleanup for failed ollama | |
fi | |
# Clean up temporary image if used | |
#if [[ -n "$PATH_TO_FLATTENED_GIF" ]] && [[ -f "$PATH_TO_FLATTENED_GIF" ]]; then | |
#echo "Removing temporary image: $PATH_TO_FLATTENED_GIF" | |
#rm -f "$PATH_TO_FLATTENED_GIF" | |
#fi | |
echo | |
done < <(find "$FOLDER" -type f \( -iname "*.jpg" -o -iname "*.png" -o -iname "*.gif" \) -print0) | |
echo "Processing complete." |
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 python3 | |
import os | |
from pathlib import Path | |
from collections import defaultdict | |
SUPPORTED_EXTENSIONS = { | |
".gif": "image", | |
".jpeg": "image", | |
".jpg": "image", | |
".md": "text", | |
".mp3": "audio", | |
".mp4": "video", | |
".ogg": "audio", | |
".pdf": "iframe", | |
".png": "image", | |
".svg": "image", | |
".txt": "text", | |
".wav": "audio", | |
".webm": "video", | |
".webp": "image" | |
} | |
HTML_TEMPLATE = """ | |
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>File Viewer</title> | |
<style> | |
body {{ margin: 0; font-family: sans-serif; font-size: 8pt; display: flex; height: 100vh; }} | |
nav {{ | |
width: 25%; | |
background: #f4f4f4; | |
overflow-y: auto; | |
border-right: 1px solid #ccc; | |
padding: 10px; | |
box-sizing: border-box; | |
}} | |
.content-area {{ | |
width: 75%; | |
display: flex; | |
flex-direction: column; | |
height: 100vh; | |
overflow: hidden; /* Important for containing flex children */ | |
}} | |
main {{ | |
flex-grow: 1; /* Takes up available space not used by resizer/preview */ | |
min-height: 50px; /* Minimum height for main content */ | |
padding: 10px; | |
overflow: auto; | |
box-sizing: border-box; | |
}} | |
#resizer {{ | |
height: 8px; /* Height of the draggable resizer bar */ | |
background: #ccc; | |
cursor: ns-resize; | |
flex-shrink: 0; /* Prevent resizer from shrinking */ | |
}} | |
#preview-pane {{ | |
/* flex-basis will be set by JS. Initial value can be from here or JS. */ | |
flex-basis: 30%; /* Default height as a percentage */ | |
flex-shrink: 0; /* Prevent shrinking beyond its basis unless forced by JS */ | |
min-height: 20px; /* Absolute minimum height, also used for "collapsed" state */ | |
background: #e9e9e9; | |
overflow-y: auto; | |
padding: 10px; | |
box-sizing: border-box; | |
}} | |
main iframe, main video, main audio {{ | |
width: 100%; | |
height: 100%; | |
display: block; | |
}} | |
main img {{ | |
max-width: 100%; | |
max-height: 100%; | |
display: block; | |
object-fit: contain; | |
}} | |
.text-container {{ white-space: pre-wrap; font-family: monospace; }} | |
#preview-pane .text-container {{ font-size: 0.9em; }} | |
ul {{ list-style-type: disc; padding-left: 12px; }} | |
li a {{ display: block; padding: 2px 0; text-decoration: none; color: #333; }} | |
li a:hover {{ background: #ff0; }} | |
</style> | |
<script> | |
// Configuration for resizable/collapsible pane | |
const MIN_PREVIEW_PANE_HEIGHT_PX = 20; // Min height for preview pane (pixels), also collapsed height | |
const MIN_MAIN_PANE_HEIGHT_PX = 50; // Min height for main content area (pixels) | |
const RESIZER_HEIGHT_PX = 8; // Must match #resizer height in CSS (pixels) | |
let lastUserSetPreviewFlexBasis = '30%'; // Stores the user's preferred size or default | |
function showContent(type, src, textContent = "", descriptionContent = "") {{ | |
const mainEl = document.getElementById("main"); | |
const previewPaneEl = document.getElementById("preview-pane"); | |
const resizerEl = document.getElementById("resizer"); | |
// Display main content | |
if (type === "iframe") {{ | |
mainEl.innerHTML = `<iframe src="${{src}}" frameborder="0"></iframe>`; | |
}} else if (type === "image") {{ | |
mainEl.innerHTML = `<img src="${{src}}">`; | |
}} else if (type === "audio") {{ | |
mainEl.innerHTML = `<audio controls src="${{src}}"></audio>`; | |
}} else if (type === "video") {{ | |
mainEl.innerHTML = `<video controls src="${{src}}"></video>`; | |
}} else if (type === "text") {{ | |
mainEl.innerHTML = `<div class="text-container">${{textContent}}</div>`; | |
}} else {{ | |
mainEl.innerHTML = `<h2>Preview for ${{type}} not supported yet.</h2>`; | |
}} | |
// Display description in preview pane and manage its state | |
if (descriptionContent && descriptionContent.trim() !== "") {{ | |
previewPaneEl.innerHTML = `<div class="text-container">${{descriptionContent}}</div>`; | |
previewPaneEl.style.flexBasis = lastUserSetPreviewFlexBasis; // Restore to user's size or default | |
previewPaneEl.style.display = ''; // Ensure visible | |
resizerEl.style.display = ''; // Show resizer | |
previewPaneEl.style.overflowY = 'auto'; | |
}} else {{ | |
previewPaneEl.innerHTML = "<p>No description available.</p>"; | |
previewPaneEl.style.flexBasis = `${{MIN_PREVIEW_PANE_HEIGHT_PX}}px`; // Collapse | |
// previewPaneEl.style.display = 'none'; // Alternative: hide completely | |
resizerEl.style.display = 'none'; // Hide resizer when collapsed | |
previewPaneEl.style.overflowY = 'hidden'; // No scroll needed for "no description" | |
}} | |
}} | |
document.addEventListener('DOMContentLoaded', () => {{ | |
const resizer = document.getElementById('resizer'); | |
const mainPane = document.getElementById('main'); | |
const previewPane = document.getElementById('preview-pane'); | |
const contentArea = document.querySelector('.content-area'); | |
// Initialize lastUserSetPreviewFlexBasis from CSS or set a default | |
const initialComputedFlexBasis = window.getComputedStyle(previewPane).flexBasis; | |
if (initialComputedFlexBasis && initialComputedFlexBasis !== 'auto' && parseFloat(initialComputedFlexBasis) >= MIN_PREVIEW_PANE_HEIGHT_PX) {{ | |
lastUserSetPreviewFlexBasis = initialComputedFlexBasis; | |
}} else {{ | |
lastUserSetPreviewFlexBasis = '30%'; // Fallback default if CSS is not suitable | |
}} | |
// Apply initial flexBasis based on whether there's a default description (usually not on first load) | |
// This means the initial state will be collapsed if the welcome message has no "description" | |
// Or, we can explicitly set it for the first load state: | |
if (previewPane.innerHTML.includes("Description will appear here.")) {{ | |
previewPane.style.flexBasis = `${{MIN_PREVIEW_PANE_HEIGHT_PX}}px`; | |
resizer.style.display = 'none'; | |
previewPane.style.overflowY = 'hidden'; | |
}} else {{ | |
previewPane.style.flexBasis = lastUserSetPreviewFlexBasis; | |
}} | |
let isResizing = false; | |
let startY_mouse; | |
resizer.addEventListener('mousedown', (e) => {{ | |
e.preventDefault(); // Prevent text selection during drag | |
isResizing = true; | |
startY_mouse = e.clientY; | |
// Store the starting height of the preview pane in pixels | |
const currentPreviewPaneHeight = previewPane.offsetHeight; | |
document.body.style.cursor = 'ns-resize'; | |
mainPane.style.userSelect = 'none'; | |
previewPane.style.userSelect = 'none'; | |
document.addEventListener('mousemove', handleMouseMove); | |
document.addEventListener('mouseup', handleMouseUp); | |
function handleMouseMove(ev) {{ | |
if (!isResizing) return; | |
const deltaY = ev.clientY - startY_mouse; | |
let newPreviewHeight = currentPreviewPaneHeight - deltaY; | |
const contentAreaHeight = contentArea.offsetHeight; | |
// Max preview height: content area height - min main pane height - resizer height | |
const maxPreviewHeight = contentAreaHeight - MIN_MAIN_PANE_HEIGHT_PX - RESIZER_HEIGHT_PX; | |
if (newPreviewHeight < MIN_PREVIEW_PANE_HEIGHT_PX) {{ | |
newPreviewHeight = MIN_PREVIEW_PANE_HEIGHT_PX; | |
}} | |
if (newPreviewHeight > maxPreviewHeight) {{ | |
newPreviewHeight = maxPreviewHeight; | |
}} | |
previewPane.style.flexBasis = `${{newPreviewHeight}}px`; | |
}} | |
function handleMouseUp() {{ | |
if (!isResizing) return; | |
isResizing = false; | |
document.body.style.cursor = ''; | |
mainPane.style.userSelect = ''; | |
previewPane.style.userSelect = ''; | |
// Update lastUserSetPreviewFlexBasis with the new size in pixels | |
lastUserSetPreviewFlexBasis = previewPane.style.flexBasis; | |
document.removeEventListener('mousemove', handleMouseMove); | |
document.removeEventListener('mouseup', handleMouseUp); | |
}} | |
}}); | |
}}); | |
</script> | |
</head> | |
<body> | |
<nav> | |
{tree} | |
</nav> | |
<div class="content-area"> | |
<main id="main"> | |
<h2>Select a file to preview</h2> | |
</main> | |
<div id="resizer"></div> | |
<div id="preview-pane"> | |
<p>Description will appear here.</p> | |
</div> | |
</div> | |
</body> | |
</html> | |
""" | |
def build_tree(paths_with_desc_info): | |
""" | |
Builds a tree structure from a list of paths, each with its type, content, and description. | |
paths_with_desc_info: list of tuples (path_str, file_type_str, main_content_str, description_content_str) | |
""" | |
tree = lambda: defaultdict(tree) | |
root = tree() | |
for path, file_type, main_content, description_content in paths_with_desc_info: | |
parts = path.split('/') | |
current = root | |
for part in parts[:-1]: | |
current = current[part] | |
current[parts[-1]] = { | |
'_type': file_type, | |
'_content': main_content, # Content of the file itself (if text) | |
'_description': description_content # Content of the associated .txt/.md | |
} | |
return root | |
def render_tree(d, prefix=""): | |
html = "<ul>" | |
for key, value in sorted(d.items()): | |
if isinstance(value, dict) and "_type" in value: # It's a file node | |
file_type = value["_type"] | |
main_content_for_js = value["_content"] | |
description_content_for_js = value["_description"] | |
escaped_main_content = main_content_for_js.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$").replace("\r", "\\r").replace("\n", "\\n") | |
escaped_description_content = description_content_for_js.replace("\\", "\\\\").replace("`", "\\`").replace("$", "\\$").replace("\r", "\\r").replace("\n", "\\n") | |
file_path_for_js = prefix + key | |
js_escaped_path = file_path_for_js.replace("\\", "\\\\").replace("'", "\\'") | |
html += (f'<li><a href="javascript:void(0)" ' | |
f'onclick="showContent(\'{file_type}\', \'{js_escaped_path}\', ' | |
f'`{escaped_main_content}`, `{escaped_description_content}`)">{key}</a></li>') | |
elif isinstance(value, dict): # It's a directory node | |
html += f"<li>{key}{render_tree(value, prefix + key + '/')}</li>" | |
html += "</ul>" | |
return html | |
def gather_files_nested(base_dir): | |
file_info_list = [] | |
for root, dirs, files in os.walk(base_dir): | |
dirs.sort() | |
files.sort() | |
for file in files: | |
ext = Path(file).suffix.lower() | |
if ext in SUPPORTED_EXTENSIONS: | |
abs_path = Path(root) / file | |
rel_path = abs_path.relative_to(base_dir).as_posix() | |
file_type = SUPPORTED_EXTENSIONS[ext] | |
content = "" | |
if file_type == "text": | |
try: | |
with open(abs_path, "r", encoding="utf-8", errors="ignore") as f: | |
content = f.read() | |
except Exception: | |
content = f"Could not read file: {rel_path}" | |
file_info_list.append((rel_path, file_type, content)) | |
return file_info_list | |
def create_static_index(directory="."): | |
output_file = Path(directory) / "index.html" | |
if output_file.exists(): | |
response = input(f"{output_file} already exists. Overwrite? (y/N): ").strip().lower() | |
if response != "y": | |
print("Aborted.") | |
return | |
all_files_data = gather_files_nested(directory) | |
text_file_contents_map = {} | |
for rel_path, file_type, content in all_files_data: | |
if file_type == "text": | |
text_file_contents_map[rel_path] = content | |
files_data_with_descriptions_attached = [] | |
text_files_used_for_non_text_description = set() | |
for rel_path_main_file, file_type_main_file, main_content_if_text in all_files_data: | |
description_text_for_main_file = "" | |
current_file_p_obj = Path(rel_path_main_file) | |
potential_desc_txt_p_obj = current_file_p_obj.with_suffix('.txt') | |
if potential_desc_txt_p_obj != current_file_p_obj: | |
desc_txt_rel_path_str = potential_desc_txt_p_obj.as_posix() | |
if desc_txt_rel_path_str in text_file_contents_map: | |
description_text_for_main_file = text_file_contents_map[desc_txt_rel_path_str] | |
if file_type_main_file != "text": | |
text_files_used_for_non_text_description.add(desc_txt_rel_path_str) | |
if not description_text_for_main_file: | |
potential_desc_md_p_obj = current_file_p_obj.with_suffix('.md') | |
if potential_desc_md_p_obj != current_file_p_obj: | |
desc_md_rel_path_str = potential_desc_md_p_obj.as_posix() | |
if desc_md_rel_path_str in text_file_contents_map: | |
description_text_for_main_file = text_file_contents_map[desc_md_rel_path_str] | |
if file_type_main_file != "text": | |
text_files_used_for_non_text_description.add(desc_md_rel_path_str) | |
files_data_with_descriptions_attached.append( | |
(rel_path_main_file, file_type_main_file, main_content_if_text, description_text_for_main_file) | |
) | |
processed_file_data_for_tree = [] | |
for rel_path, file_type, main_content, description_content in files_data_with_descriptions_attached: | |
if file_type == "text" and rel_path in text_files_used_for_non_text_description: | |
pass | |
else: | |
processed_file_data_for_tree.append( | |
(rel_path, file_type, main_content, description_content) | |
) | |
tree_dict = build_tree(processed_file_data_for_tree) | |
html_tree = render_tree(tree_dict) | |
html_output = HTML_TEMPLATE.format(tree=html_tree) | |
with open(output_file, "w", encoding="utf-8") as f: | |
f.write(html_output) | |
print(f"Created index.html in: {output_file.resolve()}") | |
if __name__ == "__main__": | |
create_static_index() |
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 bash | |
for f in *.flac; do | |
ffmpeg -i "$f" -ab 320k -map_metadata 0 -id3v2_version 3 "${f%".flac"}.mp3" | |
done |
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 bash | |
for f in *.mkv; do | |
ffmpeg -i "$f" -x265-params crf=23 "${f%".mkv"}.mp4" | |
done; |
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 bash | |
for f in *.mkv; do | |
ffmpeg -i "$f" -map 0:v:0 -map 0:a:0 -metadata title="${f%}" -vf "scale=1920:1080" -pix_fmt yuv420p -preset slow -crf 23 "${f%".mkv"}.mp4" | |
done; |
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 bash | |
for f in *.mkv; do | |
ffmpeg -i "$f" -map 0:v:0 -map 0:a:0 -metadata title="${f%}" -vf "scale=1280:720" -pix_fmt yuv420p -preset slow -crf 23 "${f%".mkv"}.mp4" | |
done; |
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 bash | |
for f in *.mkv; do | |
ffmpeg -i "$f" "${f%".mkv"}.srt" | |
ffmpeg -i "$f" -map 0:v:0 -map 0:a:0 -metadata title="${f%}" -codec copy "${f%".mkv"}.mp4" | |
done; |
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 bash | |
for f in *.mkv; do | |
ffmpeg -i "$f" "${f%".mkv"}.srt" | |
done; |
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 bash | |
for f in *.mp4; do | |
base="${f%.mp4}" | |
ffmpeg -i "$f" -vf scale=1280:720 -c:v libx264 -crf 23 -preset medium -c:a copy "${base}_720p_h264.mp4" | |
done |
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 bash | |
ollama --version | |
echo | |
ollama list | |
echo | |
echo "Getting list of installed models..." | |
echo | |
models=$(ollama list | awk 'NR>1 {print $1}') | |
if [ -z "$models" ]; then | |
echo "No models found to update." | |
echo | |
exit 0 | |
fi | |
for model in $models; do | |
echo "Pulling latest for $model..." | |
echo | |
ollama pull "$model" | |
echo | |
done | |
ollama list |
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 bash | |
yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' --batch-file $1 | |
#yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' --batch-file $1 --cookies "/path/to/cookies.txt" |
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 bash | |
yt-dlp --extract-audio --audio-format mp3 --audio-quality 0 -o "%(title)s.%(ext)s" $1 |
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 bash | |
yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' $1 | |
#yt-dlp -f 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best' $1 --cookies "/path/to/cookies.txt" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment