feat: Archipelago demo stack (lightweight)
This commit is contained in:
38
neode-ui/scripts/create-placeholder-icons.sh
Executable file
38
neode-ui/scripts/create-placeholder-icons.sh
Executable 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
|
||||
|
||||
173
neode-ui/scripts/download-app-icons.js
Executable file
173
neode-ui/scripts/download-app-icons.js
Executable 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)
|
||||
|
||||
77
neode-ui/scripts/generate-welcome-speech.js
Normal file
77
neode-ui/scripts/generate-welcome-speech.js
Normal 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.')
|
||||
Reference in New Issue
Block a user