mirror of
				https://github.com/kevinveenbirkenbach/computer-playbook.git
				synced 2025-11-03 19:58:14 +00:00 
			
		
		
		
	Compare commits
	
		
			4 Commits
		
	
	
		
			08ad58e7c8
			...
			c1b94778b4
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c1b94778b4 | |||
| 338f66352a | |||
| d4f9aa21e5 | |||
| 624566d34e | 
							
								
								
									
										124
									
								
								cli/sounds.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								cli/sounds.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					import numpy as np
 | 
				
			||||||
 | 
					import simpleaudio as sa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Sound:
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Sound effects for the application with enhanced complexity.
 | 
				
			||||||
 | 
					    Each sound uses at least 6 distinct tones and lasts no more than max_length seconds,
 | 
				
			||||||
 | 
					    except the intro sound which is a detailed 26-second Berlin techno-style build-up, 12-second celebration with a descending-fifth chord sequence of 7 chords, and breakdown with melodic background.
 | 
				
			||||||
 | 
					    Transitions between phases now crossfade over 3 seconds for smoother flow.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fs = 44100  # Sampling rate (samples per second)
 | 
				
			||||||
 | 
					    complexity_factor = 10  # Number of harmonics to sum for richer timbres
 | 
				
			||||||
 | 
					    max_length = 2.0  # Maximum total duration of any sound in seconds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _generate_complex_wave(frequency: float, duration: float, harmonics: int = None) -> np.ndarray:
 | 
				
			||||||
 | 
					        if harmonics is None:
 | 
				
			||||||
 | 
					            harmonics = Sound.complexity_factor
 | 
				
			||||||
 | 
					        t = np.linspace(0, duration, int(Sound.fs * duration), False)
 | 
				
			||||||
 | 
					        wave = np.zeros_like(t)
 | 
				
			||||||
 | 
					        for n in range(1, harmonics + 1):
 | 
				
			||||||
 | 
					            wave += (1 / n) * np.sin(2 * np.pi * frequency * n * t)
 | 
				
			||||||
 | 
					        # ADSR envelope
 | 
				
			||||||
 | 
					        attack = int(0.02 * Sound.fs)
 | 
				
			||||||
 | 
					        release = int(0.05 * Sound.fs)
 | 
				
			||||||
 | 
					        env = np.ones_like(wave)
 | 
				
			||||||
 | 
					        env[:attack] = np.linspace(0, 1, attack)
 | 
				
			||||||
 | 
					        env[-release:] = np.linspace(1, 0, release)
 | 
				
			||||||
 | 
					        wave *= env
 | 
				
			||||||
 | 
					        wave /= np.max(np.abs(wave))
 | 
				
			||||||
 | 
					        return (wave * (2**15 - 1)).astype(np.int16)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _crossfade(w1: np.ndarray, w2: np.ndarray, fade_len: int) -> np.ndarray:
 | 
				
			||||||
 | 
					        # Ensure fade_len less than each
 | 
				
			||||||
 | 
					        fade_len = min(fade_len, len(w1), len(w2))
 | 
				
			||||||
 | 
					        fade_out = np.linspace(1, 0, fade_len)
 | 
				
			||||||
 | 
					        fade_in = np.linspace(0, 1, fade_len)
 | 
				
			||||||
 | 
					        w1_end = w1[-fade_len:] * fade_out
 | 
				
			||||||
 | 
					        w2_start = w2[:fade_len] * fade_in
 | 
				
			||||||
 | 
					        middle = (w1_end + w2_start).astype(np.int16)
 | 
				
			||||||
 | 
					        return np.concatenate([w1[:-fade_len], middle, w2[fade_len:]])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def _play(wave: np.ndarray):
 | 
				
			||||||
 | 
					        play_obj = sa.play_buffer(wave, 1, 2, Sound.fs)
 | 
				
			||||||
 | 
					        play_obj.wait_done()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def play_cymais_intro_sound(cls):
 | 
				
			||||||
 | 
					        # Phase durations
 | 
				
			||||||
 | 
					        build_time = 10.0
 | 
				
			||||||
 | 
					        celebr_time = 12.0
 | 
				
			||||||
 | 
					        breakdown_time = 10.0
 | 
				
			||||||
 | 
					        overlap = 3.0  # seconds of crossfade
 | 
				
			||||||
 | 
					        bass_seg = 0.125  # 1/8s kick
 | 
				
			||||||
 | 
					        melody_seg = 0.25  # 2/8s melody
 | 
				
			||||||
 | 
					        bass_freq = 65.41  # C2 kick
 | 
				
			||||||
 | 
					        melody_freqs = [261.63, 293.66, 329.63, 392.00, 440.00, 523.25]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Build-up phase
 | 
				
			||||||
 | 
					        steps = int(build_time / (bass_seg + melody_seg))
 | 
				
			||||||
 | 
					        build_seq = []
 | 
				
			||||||
 | 
					        for i in range(steps):
 | 
				
			||||||
 | 
					            amp = (i + 1) / steps
 | 
				
			||||||
 | 
					            b = cls._generate_complex_wave(bass_freq, bass_seg).astype(np.float32) * amp
 | 
				
			||||||
 | 
					            m = cls._generate_complex_wave(melody_freqs[i % len(melody_freqs)], melody_seg).astype(np.float32) * amp
 | 
				
			||||||
 | 
					            build_seq.append(b.astype(np.int16))
 | 
				
			||||||
 | 
					            build_seq.append(m.astype(np.int16))
 | 
				
			||||||
 | 
					        build_wave = np.concatenate(build_seq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Celebration phase: 7 descending-fifth chords
 | 
				
			||||||
 | 
					        roots = [523.25, 349.23, 233.08, 155.56, 103.83, 69.30, 46.25]
 | 
				
			||||||
 | 
					        chord_time = celebr_time / len(roots)
 | 
				
			||||||
 | 
					        celebr_seq = []
 | 
				
			||||||
 | 
					        for root in roots:
 | 
				
			||||||
 | 
					            t = np.linspace(0, chord_time, int(cls.fs * chord_time), False)
 | 
				
			||||||
 | 
					            chord = sum(np.sin(2 * np.pi * f * t) for f in [root, root * 5/4, root * 3/2])
 | 
				
			||||||
 | 
					            chord /= np.max(np.abs(chord))
 | 
				
			||||||
 | 
					            celebr_seq.append((chord * (2**15 - 1)).astype(np.int16))
 | 
				
			||||||
 | 
					        celebr_wave = np.concatenate(celebr_seq)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Breakdown phase (mirror of build-up)
 | 
				
			||||||
 | 
					        breakdown_wave = np.concatenate(list(reversed(build_seq)))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Crossfade transitions
 | 
				
			||||||
 | 
					        fade_samples = int(overlap * cls.fs)
 | 
				
			||||||
 | 
					        bc = cls._crossfade(build_wave, celebr_wave, fade_samples)
 | 
				
			||||||
 | 
					        full = cls._crossfade(bc, breakdown_wave, fade_samples)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        cls._play(full)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def play_start_sound(cls):
 | 
				
			||||||
 | 
					        freqs = [523.25, 659.26, 783.99, 880.00, 1046.50, 1174.66]
 | 
				
			||||||
 | 
					        cls._prepare_and_play(freqs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def play_finished_successfully_sound(cls):
 | 
				
			||||||
 | 
					        freqs = [523.25, 587.33, 659.26, 783.99, 880.00, 987.77]
 | 
				
			||||||
 | 
					        cls._prepare_and_play(freqs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def play_finished_failed_sound(cls):
 | 
				
			||||||
 | 
					        freqs = [880.00, 830.61, 783.99, 659.26, 622.25, 523.25]
 | 
				
			||||||
 | 
					        durations = [0.4, 0.3, 0.25, 0.25, 0.25, 0.25]
 | 
				
			||||||
 | 
					        cls._prepare_and_play(freqs, durations)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def play_warning_sound(cls):
 | 
				
			||||||
 | 
					        freqs = [700.00, 550.00, 750.00, 500.00, 800.00, 450.00]
 | 
				
			||||||
 | 
					        cls._prepare_and_play(freqs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @classmethod
 | 
				
			||||||
 | 
					    def _prepare_and_play(cls, freqs, durations=None):
 | 
				
			||||||
 | 
					        count = len(freqs)
 | 
				
			||||||
 | 
					        if durations is None:
 | 
				
			||||||
 | 
					            durations = [cls.max_length / count] * count
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            total = sum(durations)
 | 
				
			||||||
 | 
					            durations = [d * cls.max_length / total for d in durations]
 | 
				
			||||||
 | 
					        waves = [cls._generate_complex_wave(f, d) for f, d in zip(freqs, durations)]
 | 
				
			||||||
 | 
					        cls._play(np.concatenate(waves))
 | 
				
			||||||
							
								
								
									
										101
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										101
									
								
								main.py
									
									
									
									
									
								
							@@ -5,6 +5,10 @@ import os
 | 
				
			|||||||
import subprocess
 | 
					import subprocess
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
import textwrap
 | 
					import textwrap
 | 
				
			||||||
 | 
					import threading
 | 
				
			||||||
 | 
					import signal
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from cli.sounds import Sound
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def format_command_help(name, description, indent=2, col_width=36, width=80):
 | 
					def format_command_help(name, description, indent=2, col_width=36, width=80):
 | 
				
			||||||
    prefix = " " * indent + f"{name:<{col_width - indent}}"
 | 
					    prefix = " " * indent + f"{name:<{col_width - indent}}"
 | 
				
			||||||
@@ -22,7 +26,6 @@ def list_cli_commands(cli_dir):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def extract_description_via_help(cli_script_path):
 | 
					def extract_description_via_help(cli_script_path):
 | 
				
			||||||
    """Run `script --help` and extract the first non-usage line after usage block."""
 | 
					 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
        result = subprocess.run(
 | 
					        result = subprocess.run(
 | 
				
			||||||
            [sys.executable, cli_script_path, "--help"],
 | 
					            [sys.executable, cli_script_path, "--help"],
 | 
				
			||||||
@@ -31,13 +34,10 @@ def extract_description_via_help(cli_script_path):
 | 
				
			|||||||
            check=True
 | 
					            check=True
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        lines = result.stdout.splitlines()
 | 
					        lines = result.stdout.splitlines()
 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Skip until first empty line after usage block
 | 
					 | 
				
			||||||
        for i, line in enumerate(lines):
 | 
					        for i, line in enumerate(lines):
 | 
				
			||||||
            if line.strip().startswith("usage:"):
 | 
					            if line.strip().startswith("usage:"):
 | 
				
			||||||
                continue
 | 
					                continue
 | 
				
			||||||
            if line.strip() == "":
 | 
					            if line.strip() == "":
 | 
				
			||||||
                # description usually comes after usage and empty line
 | 
					 | 
				
			||||||
                for j in range(i+1, len(lines)):
 | 
					                for j in range(i+1, len(lines)):
 | 
				
			||||||
                    desc = lines[j].strip()
 | 
					                    desc = lines[j].strip()
 | 
				
			||||||
                    if desc:
 | 
					                    if desc:
 | 
				
			||||||
@@ -46,41 +46,92 @@ def extract_description_via_help(cli_script_path):
 | 
				
			|||||||
    except Exception:
 | 
					    except Exception:
 | 
				
			||||||
        return "-"
 | 
					        return "-"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def main():
 | 
					def play_start_intro():
 | 
				
			||||||
 | 
					    Sound.play_start_sound()
 | 
				
			||||||
 | 
					    Sound.play_cymais_intro_sound()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def failure_with_warning_loop():
 | 
				
			||||||
 | 
					    Sound.play_finished_failed_sound()
 | 
				
			||||||
 | 
					    print("Warning: command failed. Press Ctrl+C to stop sound warnings.")
 | 
				
			||||||
 | 
					    try:
 | 
				
			||||||
 | 
					        while True:
 | 
				
			||||||
 | 
					            Sound.play_warning_sound()
 | 
				
			||||||
 | 
					    except KeyboardInterrupt:
 | 
				
			||||||
 | 
					        print("Warnings stopped by user.")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if __name__ == "__main__":
 | 
				
			||||||
 | 
					    # Parse --no-sound early and remove from args
 | 
				
			||||||
 | 
					    no_sound = False
 | 
				
			||||||
 | 
					    if '--no-sound' in sys.argv:
 | 
				
			||||||
 | 
					        no_sound = True
 | 
				
			||||||
 | 
					        sys.argv.remove('--no-sound')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Setup segfault handler to catch crashes
 | 
				
			||||||
 | 
					    def segv_handler(signum, frame):
 | 
				
			||||||
 | 
					        if not no_sound:
 | 
				
			||||||
 | 
					            Sound.play_finished_failed_sound()
 | 
				
			||||||
 | 
					            # Loop warning until interrupted
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                while True:
 | 
				
			||||||
 | 
					                    Sound.play_warning_sound()
 | 
				
			||||||
 | 
					            except KeyboardInterrupt:
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					        print("Segmentation fault detected. Exiting.")
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 | 
					    signal.signal(signal.SIGSEGV, segv_handler)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Play intro sounds
 | 
				
			||||||
 | 
					    if not no_sound:
 | 
				
			||||||
 | 
					        threading.Thread(target=play_start_intro, daemon=True).start()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Change to script directory
 | 
				
			||||||
    script_dir = os.path.dirname(os.path.realpath(__file__))
 | 
					    script_dir = os.path.dirname(os.path.realpath(__file__))
 | 
				
			||||||
    cli_dir = os.path.join(script_dir, "cli")
 | 
					    cli_dir = os.path.join(script_dir, "cli")
 | 
				
			||||||
    os.chdir(script_dir)
 | 
					    os.chdir(script_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    available_cli_commands = list_cli_commands(cli_dir)
 | 
					    available_cli_commands = list_cli_commands(cli_dir)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Special case: user ran `cymais playbook --help`
 | 
					            # Handle help invocation
 | 
				
			||||||
    if len(sys.argv) >= 3 and sys.argv[1] in available_cli_commands and sys.argv[2] == "--help":
 | 
					    if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'):
 | 
				
			||||||
        cli_script_path = os.path.join(cli_dir, f"{sys.argv[1]}.py")
 | 
					        print("CyMaIS CLI – proxy to tools in ./cli/")
 | 
				
			||||||
        subprocess.run([sys.executable, cli_script_path, "--help"])
 | 
					        print("Usage: cymais [--no-sound] <command> [options]")
 | 
				
			||||||
        sys.exit(0)
 | 
					        print("Options:")
 | 
				
			||||||
 | 
					        print("  --no-sound        Suppress all sounds during execution")
 | 
				
			||||||
    # Global --help
 | 
					        print("  -h, --help        Show this help message and exit")
 | 
				
			||||||
    if "--help" in sys.argv or "-h" in sys.argv or len(sys.argv) == 1:
 | 
					 | 
				
			||||||
        print("CyMaIS CLI – proxy to tools in ./cli/\n")
 | 
					 | 
				
			||||||
        print("Usage:")
 | 
					 | 
				
			||||||
        print("  cymais <command> [options]\n")
 | 
					 | 
				
			||||||
        print("Available commands:")
 | 
					        print("Available commands:")
 | 
				
			||||||
        for cmd in available_cli_commands:
 | 
					        for cmd in available_cli_commands:
 | 
				
			||||||
            path = os.path.join(cli_dir, f"{cmd}.py")
 | 
					            path = os.path.join(cli_dir, f"{cmd}.py")
 | 
				
			||||||
            desc = extract_description_via_help(path)
 | 
					            desc = extract_description_via_help(path)
 | 
				
			||||||
            print(format_command_help(cmd, desc))
 | 
					            print(format_command_help(cmd, desc))
 | 
				
			||||||
        print("\nUse 'cymais <command> --help' for details on each command.")
 | 
					 | 
				
			||||||
        sys.exit(0)
 | 
					        sys.exit(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Default flow
 | 
					    # Special-case per-command help
 | 
				
			||||||
 | 
					    if len(sys.argv) >= 3 and sys.argv[1] in available_cli_commands and sys.argv[2] in ('-h', '--help'):
 | 
				
			||||||
 | 
					        subprocess.run([sys.executable, os.path.join(cli_dir, f"{sys.argv[1]}.py"), "--help"])
 | 
				
			||||||
 | 
					        sys.exit(0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Execute chosen command
 | 
				
			||||||
    parser = argparse.ArgumentParser(add_help=False)
 | 
					    parser = argparse.ArgumentParser(add_help=False)
 | 
				
			||||||
    parser.add_argument("cli_command", choices=available_cli_commands)
 | 
					    parser.add_argument('cli_command', choices=available_cli_commands)
 | 
				
			||||||
    parser.add_argument("cli_args", nargs=argparse.REMAINDER)
 | 
					    parser.add_argument('cli_args', nargs=argparse.REMAINDER)
 | 
				
			||||||
    args = parser.parse_args()
 | 
					    args = parser.parse_args()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cli_script_path = os.path.join(cli_dir, f"{args.cli_command}.py")
 | 
					    cmd_path = os.path.join(cli_dir, f"{args.cli_command}.py")
 | 
				
			||||||
    full_cmd = [sys.executable, cli_script_path] + args.cli_args
 | 
					    full_cmd = [sys.executable, cmd_path] + args.cli_args
 | 
				
			||||||
    subprocess.run(full_cmd, check=True)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if __name__ == "__main__":
 | 
					    try:
 | 
				
			||||||
    main()
 | 
					        proc = subprocess.Popen(full_cmd)
 | 
				
			||||||
 | 
					        proc.wait()
 | 
				
			||||||
 | 
					        rc = proc.returncode
 | 
				
			||||||
 | 
					        if rc != 0:
 | 
				
			||||||
 | 
					            print(f"Command '{args.cli_command}' failed with exit code {rc}.")
 | 
				
			||||||
 | 
					            failure_with_warning_loop()
 | 
				
			||||||
 | 
					            sys.exit(rc)
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            if not no_sound:
 | 
				
			||||||
 | 
					                Sound.play_finished_successfully_sound()
 | 
				
			||||||
 | 
					            sys.exit(0)
 | 
				
			||||||
 | 
					    except Exception as e:
 | 
				
			||||||
 | 
					        print(f"Exception running command: {e}")
 | 
				
			||||||
 | 
					        failure_with_warning_loop()
 | 
				
			||||||
 | 
					        sys.exit(1)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +1,3 @@
 | 
				
			|||||||
colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
 | 
					colorscheme-generator @ https://github.com/kevinveenbirkenbach/colorscheme-generator/archive/refs/tags/v0.3.0.zip
 | 
				
			||||||
 | 
					simpleaudio
 | 
				
			||||||
 | 
					numpy
 | 
				
			||||||
@@ -2,7 +2,7 @@
 | 
				
			|||||||
  command: >
 | 
					  command: >
 | 
				
			||||||
    docker-compose exec -T -u www-data application
 | 
					    docker-compose exec -T -u www-data application
 | 
				
			||||||
    wp core install
 | 
					    wp core install
 | 
				
			||||||
      --url="{{ web_protocol }}://{{ domains | get_domain(application_id)[0] }}"
 | 
					      --url="{{ web_protocol }}://{{ domains | get_domain(application_id) }}"
 | 
				
			||||||
      --title="{{ applications[application_id].title }}"
 | 
					      --title="{{ applications[application_id].title }}"
 | 
				
			||||||
      --admin_user="{{ applications[application_id].users.administrator.username }}"
 | 
					      --admin_user="{{ applications[application_id].users.administrator.username }}"
 | 
				
			||||||
      --admin_password="{{ applications[application_id].credentials.administrator_password }}"
 | 
					      --admin_password="{{ applications[application_id].credentials.administrator_password }}"
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user