Showing posts with label Automation. Show all posts
Showing posts with label Automation. Show all posts

Illustrated Storytelling with AI on Mac Mini

In an era where AI tools are increasingly cloud-dependent, there's something empowering about running everything on your own hardware. No subscriptions, no data privacy concerns, and full control over the process. Today, I'm sharing how I built an automated system to create illustrated short stories—complete with narrative text, dialogues, and custom images—using only local software and hardware. The backbone is n8n, an open-source workflow automation tool, combined with local AI models like Ollama for text generation and a diffusion model Flux for images.

This setup runs on my home server, triggered automatically at night to generate a fresh sci-fi storybook. It outputs a polished HTML file with 7 illustrated panels. Best of all? It's 100% offline after initial setup. Let's dive into how it works, based on analyzing the n8n workflow I use.

Why Go Local for AI Storytelling?

Before we get technical, here's the motivation:

  • Privacy and Cost: No sending data to remote servers; everything stays on your machine.
  • Customization: Tailor models and prompts without API limits.
  • Sustainability: Use efficient local models to avoid the energy footprint of cloud AI.
  • Fun Factor: Automate creative output—wake up to a new story every day!

Tools involved:

  • n8n: Orchestrates the workflow.
  • Ollama: Runs local LLMs like Gemma and Qwen for text generation.
  • Flux Model: A local image generation model "x/flux2-klein:9b" via a local API endpoint.
  • Local Storage: Files are written directly to disk.

If you're new to this, install n8n (self-hosted) and Ollama (and pull all models listed in this post).

Hardware-wise, the Mac Mini M4 Pro is ideal for these AI tasks, leveraging its powerful integrated GPU for efficient image generation, while CPU handles text seamlessly.

Workflow Summary: How the Magic Happens

The workflow, titled "Generate Storybook," is a linear yet branched automation that starts with a trigger, generates content step-by-step, and ends with output and tracking. It's designed for 7-panel stories.



1. Triggering the Workflow

  • Schedule Trigger: Fires automatically at 11:00 PM daily. This ensures a "story of the day" without manual intervention.
  • Manual Trigger: For testing—click "Execute Workflow" in n8n to run it on demand.
  • Optional Custom Story Input: If manual, you can set a predefined story prompt (e.g., "First Human colony on Mars discovers hidden caves...").

2. Generating the Story Title

  • SciFi Title Randomizer (Ollama Node): Uses the Qwen 0.8B model to generate one random sci-fi short story title. Prompt: "give me ONE random title for a science fiction short story."
  • Example output: Something like "Echoes of the Void" or whatever the model dreams up.
  • Settings: Temperature 1 for creativity, no persistent context (keep_alive: 0m).

3. Creating Panel Descriptions and Dialogues

  • Generate Descriptions (Agent Node with Ollama): Takes the title as input and uses Gemma 12B to create a 7-panel script.
  • System Prompt: Instructs the AI to output valid JSON only: A title and an array of 7 panels, each with a number, visual description, and dialogue.
  • Rules: Always 7 panels; no extra text.
  • JSON Parser (Code Node): Cleans the AI output.

4. Enhancing the Narrative

  • Generate Flowing Story (Agent Node with Ollama): Uses Gemma 12B again to rewrite the script into flowing prose.
  • Input: The 7-panel descriptions and dialogues.
  • System Prompt: Rewrite as exactly 7 paragraphs (one per panel), each 6 sentences long, in a "storybook style" with emotion and transitions. Output: JSON array of 7 strings.
  • Temperature: 0.7 for balanced creativity.
  • Merge Outputs (Merge Node): Combines the parsed script with the flowing story paragraphs.
  • Attach Story to Panels (Code Node): Attaches each paragraph to its corresponding panel. Fallbacks to original description if parsing fails. Logs for debugging.

5. Preparing for Image Generation

  • Split Out Panels (SplitOut Node): Breaks the 7 panels into individual items for parallel processing.
  • Generate Seed for Panels (Code Node): Creates a single random seed (0 to 2^32-1) for the entire story and attaches it to every panel. This ensures consistent style across images (e.g., same artistic theme).
  • Generate Prompt for Images (Set Node): Builds image prompts: "Consistent theme throughout: Photo realistic of [panel description]. No text, no speech bubbles... purely visual image."

6. Generating Illustrations Locally

  • HTTP Request Node: Sends a POST to a local server (http://host.docker.internal:11434/api/generate—Ollama's API for images).
  • Payload: JSON with model ("x/flux2-klein:9b"), prompt, width/height (512x768), steps (10 for quick gen), and the shared seed.
  • Timeout: 1 hour (images can take time on local hardware).
  • Non-streaming for full output at once.
  • base64 Converter (Code Node): Extracts base64-encoded images from the response (handles various formats), cleans them, and prepares binary data for each panel (e.g., panel-01.png).
  • Write Images to Disk (ReadWriteFile Node): Saves PNG files locally (e.g., /files/panel-01.png).

7. Assembling the Storybook

  • Restore Story Data (Set Node): Reattaches story paragraphs, dialogues, and title to the image data.
  • Generate HTML (Code Node): Builds a beautiful HTML page:
  • Styles: Vintage storybook aesthetic (Georgia font, parchment background, bordered images).
  • Layout: Each panel as a "page" div with image (base64-embedded) and text column (story paragraph + optional dialogue in a styled box).
  • Randomly alternates image left/right for visual interest.
  • Outputs as binary data with a safe filename (e.g., Purr_fect_Pursuit.html).
  • Write HTML to Disk (ReadWriteFile Node): Saves the HTML locally (e.g., /files/index.html).

8. Storybook Examples

The following short stories were written and illustrated 100% by LOCAL AI on my Mac Mini M4 Pro using the workflow described above:


This page contains affiliate links — purchases made through them may earn a commission at no extra cost to you.


Using an External USB drive with the Ubiquiti Cloudkey Gen2 Plus

Backing Up Unifi Cloudkey Gen2 Plus Video Footage to an External HDD

Backing Up Unifi Cloudkey Gen2 Plus Video Footage to an External HDD

There’s plenty of information and posts available in the Unifi forums that explains how to replace the default 1TB hard drive with one of larger capacity, but there seems to be another issue that’s being overlooked.

Data retention and backups are critical in a corporate environment, and an issue that several people observed was that the scheduled backups on a Unifi Cloudkey Gen2 Plus only include the system configuration, and do NOT include the proprietary video archive files.

The Unifi Cloudkey Gen2 Plus is essentially a mini Linux computer. I noticed there was a spare USB Type C port on the back of the device that the vendor states is “reserved for future use”, as shown below on the right.

Unifi Cloudkey Gen2 Plus USB Port

I’ve tested several external drives on this port using a small converter I bought on Amazon, and while I ran into some issues with the popular small form factor drives that are powered by the USB port itself, I ended up finding a GREAT solution that involves using an externally powered full size 6TB USB external HDD by Western Digital.

Using the steps below, you can mirror your video footage from its default location to an external drive.

Note: There are 3 ways to power a Cloudkey Gen2 Plus. This method works best when using PoE Switch port or a PoE injector.

Required Hardware

  1. Ubiquiti Cloudkey Gen2 Plus
  2. WD 6TB Elements External Hard Drive (or a similar one, as long as it has its own power supply)
  3. USB-C to USB Adapter (this model is tested and confirmed to work)

Initial USB Drive Install and Detection

  1. Plug the USB-C to USB adapter into the spare USB-C port on the back of the Cloudkey (see diagram above).
  2. Plug the power cord to the new external HDD and turn on its power.
  3. Plug the USB cord of the HDD into the adapter you’ve installed on the Cloudkey.
  4. SSH into your Cloudkey and run lsusb. The output should show the new drive.
root@UCK-G2-Plus:~# lsusb
Bus 002 Device 005: ID 1058:25a3 Western Digital Technologies, Inc. Elements Desktop (WDBWLG)
Bus 002 Device 003: ID 174c:1153 ASMedia Technology Inc. ASM1153 SATA 3Gb/s bridge
Bus 002 Device 004: ID 0b95:1790 ASIX Electronics Corp. AX88179 Gigabit Ethernet
Bus 002 Device 002: ID 0451:8440 Texas Instruments, Inc.
Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub
Bus 001 Device 005: ID 0451:82ff Texas Instruments, Inc.
Bus 001 Device 002: ID 0451:8442 Texas Instruments, Inc.
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

Configure the New USB Drive

  1. The command fdisk -l should show you the new drive location. In my case, it was /dev/sdb. Pay close attention to the output and be sure you are identifying the new disk correctly!
root@UCK-G2-Plus:~# fdisk -l | grep Disk  <— This command only displays the disk information.
Disk /dev/mtdblock0: 64 KiB, 65536 bytes, 128 sectors
Disk /dev/mmcblk0: 29.1 GiB, 31268536320 bytes, 61071360 sectors
Disklabel type: gpt
Disk identifier: 98101B32-BBE2-4BF2-A06E-2BB33D000C20
Disk /dev/mmcblk1: 1.9 GiB, 2002780160 bytes, 3911680 sectors
Disklabel type: gpt
Disk identifier: 853D3873-C240-465B-BAAB-71BADB716C09
Disk /dev/sda: 931.5 GiB, 1000204886016 bytes, 1953525168 sectors
Disklabel type: gpt
Disk identifier: 95FA9511-D663-4144-BA52-885FBA96352F
Disk /dev/sdb: 5.5 TiB, 6001140957184 bytes, 11720978432 sectors
Disklabel type: gpt
Disk identifier: B0A5E08B-AA2D-42CD-AEDB-92220EE744A7
  1. Next, you’ll need to clear the partition table on the disk (since most new drives come with a DOS partition by default) and create a new Linux EXT4 partition.
root@UCK-G2-Plus:~# fdisk /dev/sdb  <— Make sure you type the correct disk here!
Welcome to fdisk (util-linux 2.29.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): d  <— Type “d” to delete the factory defined partition.
Selected partition 1
Partition 1 has been deleted.

Command (m for help): w  <— Type “w” to write the table to disk.
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
  1. Now you can create a new Linux EXT4 filesystem.
root@UCK-G2-Plus:~# fdisk /dev/sdb  <— Make sure you type the correct disk here!
Welcome to fdisk (util-linux 2.29.2).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.

Command (m for help): n  <— Type “n” for new partition.
Partition type
   p   primary (0 primary, 0 extended, 4 free)
   e   extended (container for logical partitions)
Select (default p): p  <— Type “p” for primary.
Partition number (1-4, default 1):
First sector (2048-1953525167, default 2048):
Last sector, +sectors or +size{K,M,G,T,P} (2048-1953525167, default 1953525167):

Created a new partition 1 of type 'Linux' and of size 931.5 GiB.

Command (m for help): w  <— Type “w” to write the table to disk. This will create /dev/sdb1.
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.
  1. Lastly, format the new /dev/sdb1 partition type as Linux EXT4:
root@UCK-G2-Plus:~# mkfs.ext4 /dev/sdb1  <— Make sure you type the correct partition here!
mke2fs 1.42.13 (17-May-2015)
/dev/sdb1 contains a file system
Proceed anyway? (y,n) y  <— Type “y” to proceed
Creating filesystem with 244190390 4k blocks and 61054976 inodes
Filesystem UUID: 4b892979-ccef-4353-ba05-2a70b0d25cf4
Superblock backups stored on blocks:
	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
	4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
	102400000, 214990848
Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done

Mount the New USB Drive

  1. Create a mount point and mount the new drive.
root@UCK-G2-Plus:~# mkdir /mnt/backup  <— Create a mount point.
root@UCK-G2-Plus:~# mount /dev/sdb1 /mnt/backup  <— Mount the new partition.
root@UCK-G2-Plus:~# df -h  <— View all mounted drives.
Filesystem      Size  Used Avail Use% Mounted on
udev            1.5G     0  1.5G   0% /dev
tmpfs           299M  5.0M  294M   2% /run
/dev/mmcblk0p46 5.9G  516M  5.3G   9% /mnt/.rwfs
/dev/disk/by-partlabel/rootfs 363M  363M     0 100% /mnt/.rofs
overlayfs-root  5.9G  516M  5.3G   9% /
tmpfs           1.5G  4.0K  1.5G   1% /dev/shm
tmpfs           5.0M     0  5.0M   0% /run/lock
tmpfs           1.5G     0  1.5G   0% /sys/fs/cgroup
tmpfs           746M  132K  746M   1% /tmp
/dev/mmcblk0p45 976M  142M  809M  15% /persist
/dev/mmcblk0p47  19G   45M   19G   1% /srv-internal
/dev/sda4       878G  5.7G  872G   1% /srv
/dev/sda2        16G  156M   16G   1% /srv/var
/dev/sda3        32G  123M   32G   1% /srv/db
/dev/mmcblk1p1  1.9G  7.8M  1.8G   1% /data
tmpfs           256M  2.9M  254M   2% /srv/unifi-protect/temp
/dev/sdb1       5.5T  1.7T  3.5T  34% /mnt/backup  <— There it is!
  1. Mount the new USB drive upon reboot (2 methods are shown below).
root@UCK-G2-Plus:~# vi /etc/rc3.d/S01unifi  <— add “mount /dev/sdb1 /mnt/backup”
save the file by typing “:wq!”

OR, if you are not comfortable with the vi editor, this command will do the same thing:

root@UCK-G2-Plus:/etc/rc3.d# echo "mount /dev/sdb1 /mnt/backup" | cat - /etc/rc3.d/S01unifi > temp && mv temp /etc/rc3.d/S01unifi

Synchronize the Default Video Location with a Folder on the New USB Drive

root@UCK-G2-Plus:~# apt install rsync
root@UCK-G2-Plus:~# mkdir /mnt/backup/video
root@UCK-G2-Plus:~# rsync -aP /srv/unifi-protect/video/ /mnt/backup/video/
sending incremental file list
./
2020/
2020/07/
2020/07/12/
2020/07/12/FCECDAD88B53_0_rotating_1594587207715.ubv
1,073,741,824 100%   73.21MB/s    0:00:13 (xfr#1, to-chk=4/11)
2020/07/12/FCECDAD88B53_0_timelapse_1594587847873.ubv
1,073,741,824 100%   73.07MB/s    0:00:14 (xfr#2, to-chk=3/11)
2020/07/12/FCECDAD88B53_2_rotating_1594587207671.ubv
1,073,741,824 100%   74.73MB/s    0:00:13 (xfr#3, to-chk=2/11)
2020/07/12/FCECDAD88B53_2_timelapse_1594589641979.ubv
1,073,741,824 100%   69.41MB/s    0:00:14 (xfr#4, to-chk=1/11)
2020/07/26/
2020/07/26/FCECDAD88B53_0_rotating_1595775383126.ubv
1,073,741,824 100%   67.99MB/s    0:00:15 (xfr#5, to-chk=0/11)
pool/

Configure a Scheduled Task to Mirror Your Videos Each Day

Use the cron scheduler service to create your daily backups using the rsync command above.

root@UCK-G2-Plus:~# crontab -e  <— This basically opens a vi editor where you can add the rsync command and timing variables using the vi command set.

Using Amazon Echo to run a Tasker Task


Using Amazon Echo to run a Tasker Task


The following logic should help get the gears turning in your head as to what you can accomplish using Tasker on your favorite Android system.

In this example, I use my Amazon Echo to toggle a Virtual Switch that I've created in the Samsung Smartthings API website.  I am using an Android application called SharpTools (available in the Play Store) to "Subscribe" to the Virtual Switch state changes.  SharpTools offers a Tasker Plugin (not free) which allows you to create a Tasker Profile to interface with the Virtual Button state.  Using this logic, I have created an example that can mute and unmute my tablet using voice commands on my Amazon Echo.  Of course, the actions or tasks that you have Tasker perform is totally up to you!

System Requirements
Download and install the following Android applications:
  • Amazon Alexa (free, but you'll need an Echo device for this to work)
  • SharpTools (free, but requires plugin puchase to interface with Tasker)
  • Tasker (not free, but WELL WORTH IT!)
Once installed, purchase open SharpTools and purchase the Tasker Plugin

Here are the required steps to use your Amazon Echo and Samsung Smartthings to run a Tasker Task:
  1. Log into your Samsung Smartthings API using a web browser and setup the virtual switch device.  
  2. Click on My Locations and then click your hub.
  3. Click the header for My Devices.
  4. Create the virtual switch.
  5. Log into your Amazon Alexa application and find the new virtual switch smarthome device.
  6. Create a friendly group name for the device in the Alexa app.  TIP: in this example, my group is named Tablet Sound, and only contains the new virtual switch device.
  7. Open Sharptools and authorized the virtual switch.
  8. In Sharptools, long-click on the new virtual switch and "Subscribe to Switch".
  9. In Tasker, create a task to perform the actions you'd like based on the ON or OFF status of the virtual switch.  TIP: Set variables to represent each status.
  10. In Tasker, create a profile to monitor the switch state using the Sharptools plugin, and link it to the Task created above.
  • Select "Event" as the Profile type.
  • Click "Plugin"
  • Click "SharpTools"
  • Click "Thing State"
  • Click on the Configuration pencil
  • Type the exact name of the virtual switch (you can leave the attribute blank)
  • Click Apply, and then back
  • Select the task you created above
Start talking to your Amazon Echo device to initiate the Tasker tasks!
"Alexa, turn on Tablet Sound"

Please share with our fellow SmartHome enthusiasts if you like this method!  Feel free to comment, too!