feat: Archipelago demo stack (lightweight)

This commit is contained in:
Dorian
2026-03-17 02:14:04 +00:00
commit 6b15143b8a
534 changed files with 75115 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
#!/bin/bash
# Create simple placeholder icons using ImageMagick or fallback to SVG
cd "$(dirname "$0")/.."
ICON_DIR="public/assets/img/app-icons"
# Create endurain placeholder
if command -v convert &> /dev/null; then
convert -size 512x512 xc:none -fill "rgba(100,150,255,200)" -draw "circle 256,256 256,100" -pointsize 200 -fill white -gravity center -annotate +0+0 "E" "$ICON_DIR/endurain.png" 2>/dev/null && echo "✅ Created endurain.png"
elif command -v magick &> /dev/null; then
magick -size 512x512 xc:none -fill "rgba(100,150,255,200)" -draw "circle 256,256 256,100" -pointsize 200 -fill white -gravity center -annotate +0+0 "E" "$ICON_DIR/endurain.png" 2>/dev/null && echo "✅ Created endurain.png"
else
# Fallback: Create simple SVG
cat > "$ICON_DIR/endurain.svg" << 'SVGEOF'
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<circle cx="256" cy="256" r="156" fill="rgba(100,150,255,200)"/>
<text x="256" y="256" font-size="200" fill="white" text-anchor="middle" dominant-baseline="central" font-weight="bold">E</text>
</svg>
SVGEOF
echo "✅ Created endurain.svg"
fi
# Create morphos-server placeholder
if command -v convert &> /dev/null; then
convert -size 512x512 xc:none -fill "rgba(150,100,255,200)" -draw "rectangle 100,100 412,412" -pointsize 200 -fill white -gravity center -annotate +0+0 "M" "$ICON_DIR/morphos-server.png" 2>/dev/null && echo "✅ Created morphos-server.png"
elif command -v magick &> /dev/null; then
magick -size 512x512 xc:none -fill "rgba(150,100,255,200)" -draw "rectangle 100,100 412,412" -pointsize 200 -fill white -gravity center -annotate +0+0 "M" "$ICON_DIR/morphos-server.png" 2>/dev/null && echo "✅ Created morphos-server.png"
else
# Fallback: Create simple SVG
cat > "$ICON_DIR/morphos-server.svg" << 'SVGEOF'
<svg width="512" height="512" xmlns="http://www.w3.org/2000/svg">
<rect x="100" y="100" width="312" height="312" rx="40" fill="rgba(150,100,255,200)"/>
<text x="256" y="256" font-size="200" fill="white" text-anchor="middle" dominant-baseline="central" font-weight="bold">M</text>
</svg>
SVGEOF
echo "✅ Created morphos-server.svg"
fi

View File

@@ -0,0 +1,173 @@
#!/usr/bin/env node
/**
* Script to download app icons from GitHub repositories
* Downloads icons for all dummy apps from Start9Labs/{app-id}-startos repos
*/
import fs from 'fs'
import path from 'path'
import https from 'https'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const appIds = [
'bitcoin',
'btcpay-server',
'homeassistant',
'grafana',
'endurain',
'fedimint',
'morphos-server',
'lightning-stack',
'mempool',
'ollama',
'searxng',
'onlyoffice',
'penpot'
]
// Map app IDs to their Start9 repo names (some might differ)
const repoMap = {
'bitcoin': 'bitcoind-startos',
'btcpay-server': 'btcpayserver-startos',
'homeassistant': 'home-assistant-startos',
'grafana': 'grafana-startos',
'lightning-stack': 'lnd-startos',
'mempool': 'mempool-startos',
'searxng': 'searxng-startos',
'onlyoffice': 'onlyoffice-startos',
'penpot': 'penpot-startos',
}
// Custom icon URLs for apps without Start9 repos
const customIconUrls = {
'fedimint': [
'https://raw.githubusercontent.com/fedibtc/fedimint-ui/master/apps/router/public/favicon.svg',
],
}
const iconDir = path.join(__dirname, '../public/assets/img/app-icons')
// Ensure directory exists
if (!fs.existsSync(iconDir)) {
fs.mkdirSync(iconDir, { recursive: true })
}
function downloadFile(url, filepath) {
return new Promise((resolve, reject) => {
const file = fs.createWriteStream(filepath)
https.get(url, (response) => {
if (response.statusCode === 200) {
response.pipe(file)
file.on('finish', () => {
file.close()
console.log(`✅ Downloaded: ${path.basename(filepath)}`)
resolve()
})
} else if (response.statusCode === 404) {
file.close()
fs.unlinkSync(filepath) // Delete empty file
console.log(`⚠️ Not found: ${url}`)
reject(new Error(`404: ${url}`))
} else {
file.close()
fs.unlinkSync(filepath)
reject(new Error(`HTTP ${response.statusCode}: ${url}`))
}
}).on('error', (err) => {
file.close()
if (fs.existsSync(filepath)) {
fs.unlinkSync(filepath)
}
reject(err)
})
})
}
async function downloadIcon(appId) {
const targetExt = 'webp' // Prefer webp for consistency with mempool, etc.
const fallbackExts = ['webp', 'png', 'svg']
const filepath = path.join(iconDir, `${appId}.webp`)
// Skip if file already exists
if (appId === 'fedimint' && fs.existsSync(path.join(iconDir, 'fedimint.png'))) {
console.log(`⏭️ Skipping ${appId} (fedimint.png exists)`)
return true
}
for (const ext of fallbackExts) {
const fp = path.join(iconDir, `${appId}.${ext}`)
if (fs.existsSync(fp)) {
console.log(`⏭️ Skipping ${appId} (already exists)`)
return true
}
}
// Try custom URLs first (e.g. fedimint from fedimint-ui)
if (customIconUrls[appId]) {
for (const url of customIconUrls[appId]) {
try {
const ext = url.endsWith('.svg') ? 'svg' : (url.endsWith('.png') ? 'png' : 'webp')
const fp = path.join(iconDir, `${appId}.${ext}`)
await downloadFile(url, fp)
return true
} catch (err) {
continue
}
}
}
const repoName = repoMap[appId] || `${appId}-startos`
const iconPaths = ['icon.png', 'icon.svg', 'assets/icon.png', 'assets/icon.svg']
for (const iconPath of iconPaths) {
const url = `https://raw.githubusercontent.com/Start9Labs/${repoName}/main/${iconPath}`
const extension = iconPath.endsWith('.svg') ? 'svg' : 'png'
const fp = path.join(iconDir, `${appId}.${extension}`)
try {
await downloadFile(url, fp)
return true
} catch (err) {
continue
}
}
console.log(`❌ Failed to download icon for ${appId}`)
return false
}
async function main() {
console.log('Downloading app icons from GitHub...\n')
const results = {
success: [],
failed: []
}
for (const appId of appIds) {
try {
const success = await downloadIcon(appId)
if (success) {
results.success.push(appId)
} else {
results.failed.push(appId)
}
// Small delay to avoid rate limiting
await new Promise(resolve => setTimeout(resolve, 500))
} catch (err) {
console.error(`Error downloading ${appId}:`, err.message)
results.failed.push(appId)
}
}
console.log(`\n✅ Successfully downloaded ${results.success.length} icons`)
if (results.failed.length > 0) {
console.log(`❌ Failed to download ${results.failed.length} icons:`, results.failed.join(', '))
}
}
main().catch(console.error)

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env node
/**
* Generate "Welcome Noderunner" speech using ElevenLabs AI voice.
* Slower, softer, sci-fi style with reverb/echo effects.
*
* Usage:
* ELEVENLABS_API_KEY=your_key node scripts/generate-welcome-speech.js
*
* Optional voice ID (browse https://elevenlabs.io/voice-library/sensual):
* ELEVENLABS_VOICE_ID=voice_id node scripts/generate-welcome-speech.js
*/
import { writeFileSync, mkdirSync, readFileSync, unlinkSync } from 'fs'
import { dirname, join } from 'path'
import { fileURLToPath } from 'url'
import { execSync } from 'child_process'
const __dirname = dirname(fileURLToPath(import.meta.url))
const API_KEY = process.env.ELEVENLABS_API_KEY
// Sarah - mature, reassuring, confident female (softer than Rachel)
const VOICE_ID = process.env.ELEVENLABS_VOICE_ID || 'EXAVITQu4vr4xnSDxMaL'
const OUTPUT_PATH = join(__dirname, '../public/assets/audio/welcome-noderunner.mp3')
const RAW_PATH = join(__dirname, '../public/assets/audio/welcome-noderunner-raw.mp3')
if (!API_KEY) {
console.error('Set ELEVENLABS_API_KEY (get a free key at elevenlabs.io)')
process.exit(1)
}
// Slower (0.78), softer (higher stability 0.65), more expressive (style 0.6)
const res = await fetch(
`https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}?output_format=mp3_44100_128`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'xi-api-key': API_KEY,
},
body: JSON.stringify({
text: 'Welcome Noderunner',
model_id: 'eleven_multilingual_v2',
voice_settings: {
stability: 0.65,
similarity_boost: 0.8,
style: 0.6,
use_speaker_boost: true,
speed: 0.7,
},
}),
}
)
if (!res.ok) {
const err = await res.text()
console.error('ElevenLabs API error:', res.status, err)
process.exit(1)
}
const buf = Buffer.from(await res.arrayBuffer())
mkdirSync(dirname(OUTPUT_PATH), { recursive: true })
writeFileSync(RAW_PATH, buf)
// Add sci-fi reverb: dense short delays that blend (no distinct echo)
try {
execSync(
`ffmpeg -y -i "${RAW_PATH}" -af "aecho=0.6:0.15:25|45|70:0.55|0.45|0.35,highpass=f=80,equalizer=f=4000:t=q:w=1:g=-1" -q:a 2 "${OUTPUT_PATH}" 2>/dev/null`,
{ stdio: 'pipe' }
)
unlinkSync(RAW_PATH)
} catch {
writeFileSync(OUTPUT_PATH, buf)
try { unlinkSync(RAW_PATH) } catch {}
}
console.log('Generated:', OUTPUT_PATH)
console.log('Add this file to git and deploy.')