diff --git a/.github/workflows/build-all.yml b/.github/workflows/build-all.yml
index 42b33ea..191303c 100644
--- a/.github/workflows/build-all.yml
+++ b/.github/workflows/build-all.yml
@@ -22,20 +22,30 @@ jobs:
compression-level: 6
custom-hostname: picompose
+ build-lva:
+ uses: ./.github/workflows/build-image-template.yml
+ with:
+ image-name: PiCompose_Linux-Voice-Assistant
+ stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-other ./03-stage-linux-voice-assistant ./04-stage-finish
+ compression: zip
+ compression-level: 6
+ custom-hostname: picompose
# 2MICHAT-v1
build-2michat:
+ if: false # disabled
uses: ./.github/workflows/build-image-template.yml
with:
- image-name: PiCompose-2MicHat
+ image-name: PiCompose-2MicHat-v1
stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-2michat-v1 ./04-stage-finish
compression: xz
compression-level: 6
custom-hostname: picompose
build-2michat-lva:
+ if: false # disabled
uses: ./.github/workflows/build-image-template.yml
with:
- image-name: PiCompose_2MicHat_Linux-Voice-Assistant
+ image-name: PiCompose_2MicHat-v1_Linux-Voice-Assistant
stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-2michat-v1 ./03-stage-linux-voice-assistant ./04-stage-finish
compression: xz
compression-level: 6
@@ -54,7 +64,7 @@ jobs:
build-respeaker_lite-lva:
uses: ./.github/workflows/build-image-template.yml
with:
- image-name: PiCompose_Respeaker-lite_Linux-Voice-Assistant
+ image-name: PiCompose_Respeaker_lite_Linux-Voice-Assistant
stage-list: stage0 stage1 stage2 ./01-stage-picompose ./02-stage-audiodriver-respeaker_lite ./03-stage-linux-voice-assistant ./04-stage-finish
compression: xz
compression-level: 6
@@ -63,4 +73,5 @@ jobs:
# RPI IMAGER JSON
generate-rpi-imager-json:
needs: [build, build-2michat, build-respeaker_lite, build-2michat-lva, build-respeaker_lite-lva]
+ if: always()
uses: ./.github/workflows/create-rpi-image-json.yml
diff --git a/.github/workflows/build-image-template.yml b/.github/workflows/build-image-template.yml
index cf69015..4d37ea6 100644
--- a/.github/workflows/build-image-template.yml
+++ b/.github/workflows/build-image-template.yml
@@ -31,6 +31,11 @@ on:
type: boolean
default: false
description: "Enable rpi-imager.json snippet generation"
+ releaseversion:
+ required: false
+ type: string
+ default: "trixie"
+ description: "OS Release"
jobs:
build_images:
@@ -84,7 +89,7 @@ jobs:
stage-list: ${{ inputs.stage-list }}
# Build configuration
- release: trixie
+ release: ${{ inputs.releaseversion }}
compression: ${{ inputs.compression }}
compression-level: ${{ inputs.compression-level }}
diff --git a/README.md b/README.md
index 887f296..2865b40 100644
--- a/README.md
+++ b/README.md
@@ -42,18 +42,12 @@ There is a seperated page for the supported hardware. You can find the link to i
Here is a Overview for the specific images of each hardware:
-
-| Name | Hardware | What's in the Image? |
-| ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| **Base Image** | Your own hardware... | • Docker & Docker Compose (piCompose)
• Automatic Docker Compose deployment
• Pipewire Audio Server
• SSH enabled (pi User) |
-| **[Sattelite1](docs/hardware_sattelite1.md)** |
| • Base Image
• Satellite1 Hat Driver
Image is currently work in progress! |
-| **[Sattelite1](docs/hardware_sattelite1.md)**
**+Linux-Voice-Assistant**
**+Snapcast** |
| • Satellite1 Hat Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
Image is currently work in progress! |
-| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)** |
| • Base Image
• Seeed Voicecard Driver |
-| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)**
**+Linux-Voice-Assistant**
**+Snapcast** |
| • 2-Mic HAT Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• 2-Mic HAT GPIO LED Control
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant |
-| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)** |
| • Base Image
• Audio keep-alive service
• Workaround for connectivity issues in combination with the Pi Zero 2W.
There is a USB connectivity issue with the Pi Zero 2W. Use this board with at least Raspberry PI 3. |
-| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)**
**+Linux-Voice-Assistant**
**+Snapcast** |
| • ReSpeaker Lite Image
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
• Workaround for connectivity issues in combination with the Pi Zero 2W.
There is a USB connectivity issue with the Pi Zero 2W. Use this board with at least Raspberry PI 3. |
-
-
+| Name | Hardware | What's in the Image? |
+| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **Base Image** | If you use other hardware... | • Docker & Docker Compose (piCompose)
• Automatic Docker Compose deployment
• Pipewire Audio Server
• SSH enabled (pi User) |
+| **[Satellite1](docs/hardware_sattelite1.md)** |
| • * Everything from Base Image
• Satellite1 Hat Driver
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
Image is currently work in progress! |
+| **[ReSpeaker 2-Mic HAT v1](docs/hardware_2mic_v1.md)** |
| • * Everything from Base Image
• Seeed Voicecard Driver
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
Current latest available build version is 1.1.1 See [#41](https://github.com/HinTak/seeed-voicecard/issues/41) |
+| **[ReSpeaker Lite](docs/hardware_respeaker_lite.md)** |
| • * Everything from Base Image
• Audio keep-alive service
• Linux-Voice-Assistant (OpenHomeFoundation)
• Snapcast MultiRoom Audio Client
• Pre-configured for Home Assistant
• Workaround for connectivity issues in combination with the Pi Zero 2W.
### Installation
diff --git a/scripts/generate-imager-json.py b/scripts/generate-imager-json.py
index 5590ff6..58fac9e 100755
--- a/scripts/generate-imager-json.py
+++ b/scripts/generate-imager-json.py
@@ -6,130 +6,229 @@
import sys
import re
import urllib.request
+from collections import defaultdict
OWNER = "florian-asche"
REPO = "PiCompose"
OUTPUT_FILE = "rpi-imager.json"
API_URL = f"https://api.github.com/repos/{OWNER}/{REPO}/releases"
-def main():
+HARDWARE_TYPES = ["Respeaker-lite", "2MicHat", "2MicHat-v1", "2MicHat-v2", "None"]
+
+
+def fetch_releases():
+ """Fetch releases from GitHub API."""
print("Fetching releases from GitHub...")
-
try:
with urllib.request.urlopen(API_URL) as response:
releases_data = json.loads(response.read().decode())
except Exception as e:
print(f"Error: Failed to fetch releases - {e}")
sys.exit(1)
-
print("Successfully fetched releases")
+ return releases_data
+
- # Get the latest version
- LATEST_VERSION = releases_data[0]['tag_name']
- if LATEST_VERSION.startswith('v'):
- LATEST_VERSION = LATEST_VERSION[1:]
- print(f"Latest version: {LATEST_VERSION}")
-
- # Find the first release that has .xz or .zip files (this will be our "latest" with files)
- latest_with_files_tag = None
- for release in releases_data:
- for asset in release['assets']:
- if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'):
- latest_with_files_tag = release['tag_name']
- break
- if latest_with_files_tag:
- break
-
- # Collect all releases with .xz or .zip files
+def get_latest_version(releases):
+ """Get the latest version tag from releases (only tags starting with 'v')."""
+ for release in releases:
+ tag = release['tag_name']
+ if tag.startswith('v') and len(tag) > 1:
+ version = tag[1:]
+ return tag, version
+ return None, None
+
+
+def find_release_by_tag(releases, tag):
+ """Find a specific release by tag name."""
+ for release in releases:
+ if release['tag_name'] == tag:
+ return release
+ return None
+
+
+def extract_image_files(release):
+ """Extract .xz and .zip image files from a release."""
+ xz_files = []
+ for asset in release['assets']:
+ if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'):
+ xz_files.append({
+ 'name': asset['name'],
+ 'url': asset['browser_download_url']
+ })
+ return xz_files
+
+
+def get_releases_with_files(releases):
+ """Get all releases that have image files, excluding 'main' branch."""
releases_with_files = []
- for release in releases_data:
+ for release in releases:
tag_name = release['tag_name']
-
- xz_files = []
- for asset in release['assets']:
- if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'):
- xz_files.append({
- 'name': asset['name'],
- 'url': asset['browser_download_url']
- })
-
- if xz_files and tag_name != "main":
- # Format version name
+ files = extract_image_files(release)
+ if files and tag_name != "main":
version = tag_name[1:] if tag_name.startswith('v') else tag_name
-
releases_with_files.append({
'tag_name': tag_name,
'version': version,
- 'files': xz_files
+ 'files': files
})
+ return releases_with_files
- def format_image_name(filename):
- """Extract clean image name from filename"""
- # Remove .img.xz suffix
- name = filename.replace('.img.xz', '')
- # Remove date prefix (image_YYYY-MM-DD-)
- name = re.sub(r'^image_\d{4}-\d{2}-\d{2}-?', '', name)
- # Replace underscores and hyphens with spaces
- name = name.replace('_', ' ').replace('-', ' ')
- clean_name = name.strip()
- # Add original filename in brackets for clarity
- return f"{clean_name} ({filename})"
-
- def build_release_subitems(files):
- """Build subitems list from files"""
- # Umgekehrte Sortierung: neueste / letzte Datei zuerst anzeigen
- reversed_files = reversed(files)
- return [
- {
- "name": format_image_name(f['name']),
- "description": "PiCompose image",
- "url": f['url'],
- "init_format": "systemd",
- "devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit", "pi3-32bit"],
- "capabilities": ["ssh", "wifi", "hostname", "locale"]
- }
- for f in reversed_files
- ]
- os_list = []
+def extract_metadata(filename):
+ """Extract date and hardware type from filename."""
+ original = filename
+ name = filename.replace('.img.xz', '').replace('.zip', '')
+
+ date_match = re.match(r'^image_(\d{4}-\d{2}-\d{2})', name)
+ date_prefix = date_match.group(1) if date_match else ''
+ name = re.sub(r'^image_\d{4}-\d{2}-\d{2}-?', '', name)
+
+ hardware = "None"
+
+ if 'Respeaker' in name and 'lite' in name.lower():
+ hardware = "Respeaker-lite"
+ elif '2MicHat' in name:
+ if '2MicHat-v2' in name:
+ hardware = "2MicHat-v2"
+ else:
+ hardware = "2MicHat-v1"
+
+ parts = name.split('_')
+
+ return date_prefix, hardware, parts, original
- # Find main branch release (nightly)
- main_release = None
- for release in releases_data:
- if release['tag_name'] == "main":
- main_xz_files = []
- for asset in release['assets']:
- if asset['name'].endswith('.xz') or asset['name'].endswith('.zip'):
- main_xz_files.append({
- 'name': asset['name'],
- 'url': asset['browser_download_url']
- })
- if main_xz_files:
- main_release = main_xz_files
- break
-
- # PiCompose (Latest) - all files from the first stable release with .xz files
- if releases_with_files:
- latest = releases_with_files[0]
- os_list.append({
- "name": "PiCompose (Latest)",
- "description": "Latest stable PiCompose images",
+
+def format_image_name(parts, original):
+ """Format clean image name from parts."""
+ clean_parts = [p.strip() for p in parts if p.strip()]
+ clean_name = ' - '.join(clean_parts)
+ return clean_name, original
+
+
+def create_image_entry(filename, url):
+ """Create a single image entry for subitems."""
+ date_prefix, hardware, parts, original = extract_metadata(filename)
+ clean_name, original = format_image_name(parts, original)
+
+ return {
+ "name": clean_name,
+ "description": "PiCompose image",
+ "url": url,
+ "init_format": "systemd",
+ "devices": ["pi5-64bit", "pi4-64bit", "pi3-64bit", "pi3-32bit"],
+ "capabilities": ["ssh", "wifi", "hostname", "locale"]
+ }
+
+
+def group_files_by_date_and_hardware(files):
+ """Group files by date and hardware type."""
+ grouped = defaultdict(lambda: defaultdict(list))
+
+ for f in files:
+ date_prefix, hardware, parts, original = extract_metadata(f['name'])
+
+ if date_prefix:
+ grouped[date_prefix][hardware].append(f)
+
+ return grouped
+
+
+def build_date_hardware_subitems(files):
+ """Build nested subitems: Date -> Hardware -> Images."""
+ grouped = group_files_by_date_and_hardware(files)
+
+ if not grouped:
+ return build_simple_subitems(files)
+
+ date_items = []
+ for date in sorted(grouped.keys(), reverse=True):
+ hardware_items = []
+
+ for hardware in grouped[date]:
+ image_entries = [
+ create_image_entry(f['name'], f['url'])
+ for f in grouped[date][hardware]
+ ]
+
+ hardware_items.append({
+ "name": hardware,
+ "description": f"Hardware: {hardware}",
+ "icon": "icons/cat_raspberry_pi_os.png",
+ "subitems": image_entries
+ })
+
+ date_items.append({
+ "name": date,
+ "description": f"Images from {date}",
"icon": "icons/cat_raspberry_pi_os.png",
- "subitems": build_release_subitems(latest['files'])
+ "subitems": hardware_items
})
+
+ return date_items
+
+
+def build_simple_subitems(files):
+ """Build simple subitems without date/hardware grouping."""
+ return [
+ create_image_entry(f['name'], f['url'])
+ for f in reversed(files)
+ ]
+
+
+def get_main_release(releases):
+ """Get the main branch (nightly) release if available."""
+ for release in releases:
+ if release['tag_name'] == "main":
+ files = extract_image_files(release)
+ if files:
+ return files
+ return None
+
+
+def build_devices_list():
+ """Build the devices list for rpi-imager."""
+ return [
+ {"name": "Raspberry Pi 5", "tags": ["pi5-64bit", "pi5-32bit"], "default": True, "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_5.png", "description": "Raspberry Pi 5, 500 / 500+, and Compute Module 5", "matching_type": "exclusive", "capabilities": []},
+ {"name": "Raspberry Pi 4", "tags": ["pi4-64bit", "pi4-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_4.png", "description": "Raspberry Pi 4 Model B, 400, and Compute Module 4 / 4S", "matching_type": "inclusive", "capabilities": []},
+ {"name": "Raspberry Pi 3", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_3.png", "description": "Raspberry Pi 3 Model A+ / B / B+ and Compute Module 3 / 3+", "matching_type": "inclusive", "capabilities": []},
+ {"name": "Raspberry Pi Zero 2 W", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_Zero_2_W.png", "description": "Raspberry Pi Zero 2 W", "matching_type": "inclusive", "capabilities": []},
+ {"name": "No filtering", "tags": [], "description": "Show every possible image", "matching_type": "inclusive", "capabilities": []}
+ ]
+
+
+def main():
+ releases = fetch_releases()
+
+ latest_tag, latest_version = get_latest_version(releases)
+ print(f"Latest version: {latest_version}")
+
+ releases_with_files = get_releases_with_files(releases)
+ main_release = get_main_release(releases)
+
+ os_list = []
+
+ latest_release = find_release_by_tag(releases, latest_tag)
+ if latest_release:
+ latest_files = extract_image_files(latest_release)
+ if latest_files:
+ os_list.append({
+ "name": "PiCompose - stable",
+ "description": "Latest stable tagged version images",
+ "icon": "icons/cat_raspberry_pi_os.png",
+ "subitems": build_date_hardware_subitems(latest_files)
+ })
- # PiCompose (Nightly) - files from main branch release if available
if main_release:
os_list.append({
- "name": "PiCompose (Nightly / Main)",
- "description": "Latest development build from main branch",
+ "name": "PiCompose - development",
+ "description": "Latest development builds from main branch",
"icon": "icons/cat_raspberry_pi_os.png",
- "subitems": build_release_subitems(main_release)
+ "subitems": build_date_hardware_subitems(main_release)
})
- # PiCompose (All Versions) - grouped by release
os_list.append({
- "name": "PiCompose (All Versions)",
+ "name": "PiCompose - all versions / branches",
"description": "All available PiCompose versions",
"icon": "icons/cat_raspberry_pi_os.png",
"subitems": [
@@ -137,31 +236,21 @@ def build_release_subitems(files):
"name": r['version'],
"description": f"PiCompose {r['tag_name']} release",
"icon": "icons/cat_raspberry_pi_os.png",
- "subitems": build_release_subitems(r['files'])
+ "subitems": build_date_hardware_subitems(r['files'])
}
for r in releases_with_files
]
})
- # Build final JSON
- devices = [
- {"name": "Raspberry Pi 5", "tags": ["pi5-64bit", "pi5-32bit"], "default": True, "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_5.png", "description": "Raspberry Pi 5, 500 / 500+, and Compute Module 5", "matching_type": "exclusive", "capabilities": []},
- {"name": "Raspberry Pi 4", "tags": ["pi4-64bit", "pi4-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_4.png", "description": "Raspberry Pi 4 Model B, 400, and Compute Module 4 / 4S", "matching_type": "inclusive", "capabilities": []},
- {"name": "Raspberry Pi 3", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_3.png", "description": "Raspberry Pi 3 Model A+ / B / B+ and Compute Module 3 / 3+", "matching_type": "inclusive", "capabilities": []},
- {"name": "Raspberry Pi Zero 2 W", "tags": ["pi3-64bit", "pi3-32bit"], "icon": "https://downloads.raspberrypi.com/imager/icons/RPi_Zero_2_W.png", "description": "Raspberry Pi Zero 2 W", "matching_type": "inclusive", "capabilities": []},
- {"name": "No filtering", "tags": [], "description": "Show every possible image", "matching_type": "inclusive", "capabilities": []}
- ]
-
final_data = {
"imager": {
- "latest_version": LATEST_VERSION,
+ "latest_version": latest_version,
"url": "https://www.raspberrypi.com/software/",
- "devices": devices
+ "devices": build_devices_list()
},
"os_list": os_list
}
- # Write output
with open(OUTPUT_FILE, 'w') as f:
json.dump(final_data, f, indent=2)
@@ -169,11 +258,11 @@ def build_release_subitems(files):
print("")
print("Content preview:")
print(json.dumps(final_data, indent=2))
-
- # Count .xz files
+
total_xz = sum(len(r['files']) for r in releases_with_files)
print(f"")
print(f"Total .xz files: {total_xz}")
+
if __name__ == "__main__":
- main()
+ main()
\ No newline at end of file