Skip to content

Instantly share code, notes, and snippets.

@psiborg
Last active June 5, 2025 03:06
Show Gist options
  • Save psiborg/c4da7a48d67b8ac6846ba0956aa78409 to your computer and use it in GitHub Desktop.
Save psiborg/c4da7a48d67b8ac6846ba0956aa78409 to your computer and use it in GitHub Desktop.

Revisions

  1. psiborg revised this gist Jun 5, 2025. 1 changed file with 97 additions and 0 deletions.
    97 changes: 97 additions & 0 deletions describe_diagrams.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,97 @@
    #!/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."
  2. psiborg revised this gist Jun 5, 2025. 1 changed file with 349 additions and 0 deletions.
    349 changes: 349 additions & 0 deletions dir_to_html.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,349 @@
    #!/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()
  3. psiborg revised this gist May 28, 2025. 2 changed files with 3 additions and 1 deletion.
    2 changes: 1 addition & 1 deletion ffprobe-tags.sh
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    #!/bin/sh
    #!/usr/bin/env bash

    for file in *.mp3 *.flac; do
    if [ -f "$file" ]; then
    2 changes: 2 additions & 0 deletions mkv-mp4-1080.sh
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    #!/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;
  4. psiborg created this gist May 28, 2025.
    10 changes: 10 additions & 0 deletions ffprobe-tags.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,10 @@
    #!/bin/sh

    for file in *.mp3 *.flac; do
    if [ -f "$file" ]; then
    echo "Metadata for: $file"
    #ffprobe -i "$file" -show_entries format_tags -v quiet -print_format json
    ffprobe -i "$file" -show_entries format_tags -v quiet -print_format flat
    echo "-------------------------------------------------------------------------------"
    fi
    done
    5 changes: 5 additions & 0 deletions flac-mp3.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    #!/usr/bin/env bash

    for f in *.flac; do
    ffmpeg -i "$f" -ab 320k -map_metadata 0 -id3v2_version 3 "${f%".flac"}.mp3"
    done
    5 changes: 5 additions & 0 deletions h265-h264.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    #!/usr/bin/env bash

    for f in *.mkv; do
    ffmpeg -i "$f" -x265-params crf=23 "${f%".mkv"}.mp4"
    done;
    3 changes: 3 additions & 0 deletions mkv-mp4-1080.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    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;
    5 changes: 5 additions & 0 deletions mkv-mp4-720.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    #!/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;
    6 changes: 6 additions & 0 deletions mkv-mp4.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    #!/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;
    5 changes: 5 additions & 0 deletions mkv-srt.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,5 @@
    #!/usr/bin/env bash

    for f in *.mkv; do
    ffmpeg -i "$f" "${f%".mkv"}.srt"
    done;
    6 changes: 6 additions & 0 deletions mp4-h264.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    #!/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
    26 changes: 26 additions & 0 deletions upd-ollama-models.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,26 @@
    #!/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
    4 changes: 4 additions & 0 deletions yt-batch.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    #!/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"
    3 changes: 3 additions & 0 deletions yt-mp3.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,3 @@
    #!/usr/bin/env bash

    yt-dlp --extract-audio --audio-format mp3 --audio-quality 0 -o "%(title)s.%(ext)s" $1
    4 changes: 4 additions & 0 deletions yt.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,4 @@
    #!/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"