| |
| """ |
| Witz-Bot β Reachy Mini Joke Bot |
| |
| SchlΓ€ft per Default. Wacht bei GerΓ€uschen auf, gΓ€hnt, streckt sich, |
| schaut sich um, erzΓ€hlt einen deutschen Witz und lacht β dann schlΓ€ft er wieder. |
| |
| Voraussetzungen (einmalig installieren): |
| pip install edge-tts miniaudio |
| """ |
|
|
| import asyncio |
| import time |
| import random |
|
|
| import miniaudio |
| import numpy as np |
| import edge_tts |
|
|
| from reachy_mini import ReachyMini |
| from reachy_mini.utils import create_head_pose |
|
|
| try: |
| from reachy_mini.motion.recorded_move import RecordedMoves |
| _emotions = RecordedMoves("pollen-robotics/reachy-mini-emotions-library") |
| except Exception: |
| _emotions = None |
|
|
| |
|
|
| TTS_VOICE = "de-DE-KatjaNeural" |
| AUDIO_RATE = 16_000 |
| WAKE_THRESHOLD = 4 |
| POLL_INTERVAL = 0.15 |
|
|
| |
| ANT_NORMAL = [-0.1745, 0.1745] |
| ANT_DROOP = [-2.2, 2.2 ] |
| ANT_ALERT = [-0.4, 0.4 ] |
|
|
| JOKES: list[tuple[str, str]] = [ |
| ("Warum kΓΆnnen Geister so schlecht lΓΌgen?", |
| "Weil man durch sie hindurchsehen kann!"), |
| ("Wie nennt man einen Bumerang, der nicht zurΓΌckkommt?", |
| "Einen Stock!"), |
| ("Was macht ein Pirat am Computer?", |
| "Er drΓΌckt die Enter-Taste!"), |
| ("Warum gehen Skelette nicht in den Krieg?", |
| "Weil sie keinen Mumm haben!"), |
| ("Warum ist die Sechs so traurig?", |
| "Weil Sieben Acht Neun!"), |
| ("Wie heiΓt der Bruder von Robocop?", |
| "Robocup!"), |
| ("Was sagt ein Vampir, wenn er telefoniert?", |
| "Auf Wieder-beiΓen! Γh β¦ ich meine: Auf WiederhΓΆren!"), |
| ("Was ist der Unterschied zwischen einem Snowboarder und einem Schachspieler?", |
| "Einer macht das Brett β der andere brettert!"), |
| ("Warum macht ein Besen so gute Arbeit?", |
| "Er ist eben gut im Kehren!"), |
| ("Was sagt ein Hai, wenn er gegen etwas stΓΆΓt?", |
| "Haihai!"), |
| ("Was ist groΓ, grΓΌn und geht rauf und runter?", |
| "Ein Kaktus im Fahrstuhl!"), |
| ("Treffen sich zwei JΓ€ger. Beide tot.", |
| "Nein, SpaΓ β das ist kein Witz, das ist TragΓΆdie!"), |
| ] |
|
|
|
|
| |
|
|
| async def _fetch_mp3(text: str) -> bytes: |
| chunks: list[bytes] = [] |
| async for chunk in edge_tts.Communicate(text, TTS_VOICE).stream(): |
| if chunk["type"] == "audio": |
| chunks.append(chunk["data"]) |
| return b"".join(chunks) |
|
|
|
|
| def synth(text: str) -> np.ndarray: |
| """Text β float32-Array (N, 1) bei AUDIO_RATE Hz fΓΌr den Roboter-Lautsprecher.""" |
| mp3_bytes = asyncio.run(_fetch_mp3(text)) |
| decoded = miniaudio.decode( |
| mp3_bytes, |
| nchannels=1, |
| sample_rate=AUDIO_RATE, |
| output_format=miniaudio.SampleFormat.FLOAT32, |
| ) |
| samples = np.frombuffer(decoded.samples, dtype=np.float32).copy() |
| return samples.reshape(-1, 1) |
|
|
|
|
| def play(mini: ReachyMini, samples: np.ndarray) -> None: |
| """Audio zum Roboter-Lautsprecher schicken und auf Ende warten.""" |
| mini.media.push_audio_sample(samples) |
| time.sleep(len(samples) / AUDIO_RATE + 0.3) |
|
|
|
|
| |
|
|
| def do_yawn(mini: ReachyMini) -> None: |
| """GΓ€hnen: Kopf lehnt zurΓΌck, Antennen hΓ€ngen schlaff, kehrt zurΓΌck.""" |
| |
| mini.goto_target( |
| create_head_pose(pitch=-22, degrees=True), |
| antennas=ANT_DROOP, |
| duration=1.5, |
| ) |
| time.sleep(2.0) |
| |
| mini.goto_target(create_head_pose(), antennas=ANT_NORMAL, duration=1.0) |
| time.sleep(1.1) |
|
|
|
|
| def do_stretch(mini: ReachyMini) -> None: |
| """Strecken: KΓΆrper dreht sich links und rechts (Hals/WirbelsΓ€ule lockern).""" |
| mini.goto_target(create_head_pose(), body_yaw=np.deg2rad(40), duration=1.5) |
| time.sleep(1.6) |
| mini.goto_target(create_head_pose(), body_yaw=np.deg2rad(-40), duration=2.0) |
| time.sleep(2.1) |
| mini.goto_target(create_head_pose(), body_yaw=0.0, duration=1.0) |
| time.sleep(1.1) |
|
|
|
|
| def look_around(mini: ReachyMini) -> None: |
| """Schaut sich neugierig um: links β rechts β Mitte.""" |
| mini.goto_target( |
| create_head_pose(yaw=50, degrees=True), |
| antennas=ANT_ALERT, |
| duration=0.8, |
| ) |
| time.sleep(1.0) |
| mini.goto_target(create_head_pose(yaw=-50, degrees=True), duration=1.2) |
| time.sleep(1.3) |
| mini.goto_target(create_head_pose(), antennas=ANT_NORMAL, duration=0.6) |
| time.sleep(0.7) |
|
|
|
|
| def do_laugh(mini: ReachyMini) -> None: |
| """Lachen: Emotions-Library wenn verfΓΌgbar, sonst schnelles Kopfnicken.""" |
| if _emotions is not None: |
| try: |
| mini.play_move(_emotions.get("happy"), initial_goto_duration=0.4) |
| return |
| except Exception: |
| pass |
|
|
| |
| for i in range(7): |
| pitch = 20 if i % 2 == 0 else -5 |
| ant = ANT_ALERT if i % 2 == 0 else ANT_NORMAL |
| mini.goto_target( |
| create_head_pose(pitch=pitch, degrees=True), |
| antennas=ant, |
| duration=0.18, |
| ) |
| time.sleep(0.2) |
| mini.goto_target(create_head_pose(), antennas=ANT_NORMAL, duration=0.5) |
| time.sleep(0.6) |
|
|
|
|
| |
|
|
| def wait_for_sound(mini: ReachyMini) -> None: |
| """Blockiert, bis Sprache/GerΓ€usch zuverlΓ€ssig erkannt wird.""" |
| print(" Warte auf GerΓ€usch β¦", end="", flush=True) |
| consecutive = 0 |
| while consecutive < WAKE_THRESHOLD: |
| _, is_speech = mini.media.get_DoA() |
| if is_speech: |
| consecutive += 1 |
| else: |
| consecutive = max(0, consecutive - 1) |
| time.sleep(POLL_INTERVAL) |
| print(" GerΓ€usch erkannt!") |
|
|
|
|
| |
|
|
| def main() -> None: |
| print("Witz-Bot startet β¦") |
| print(" Synthetisiere Witze (benΓΆtigt Internet) β¦") |
|
|
| jokes_audio: list[tuple[np.ndarray, np.ndarray]] = [] |
| for i, (setup, punchline) in enumerate(JOKES, 1): |
| print(f" [{i}/{len(JOKES)}] {setup[:50]}β¦") |
| jokes_audio.append((synth(setup), synth(punchline))) |
|
|
| print(f" {len(jokes_audio)} Witze bereit. Verbinde mit Roboter β¦") |
|
|
| with ReachyMini(media_backend="default") as mini: |
| mini.media.start_recording() |
| mini.media.start_playing() |
| print(" Verbunden! DrΓΌcke Strg+C zum Beenden.\n") |
|
|
| try: |
| while True: |
| print("Schlafen β¦") |
| mini.goto_sleep() |
|
|
| wait_for_sound(mini) |
|
|
| mini.wake_up() |
|
|
| do_yawn(mini) |
| do_stretch(mini) |
| look_around(mini) |
|
|
| idx = random.randrange(len(JOKES)) |
| setup_text, punchline_text = JOKES[idx] |
| setup_audio, punchline_audio = jokes_audio[idx] |
|
|
| print(f"\nWitz #{idx + 1}: {setup_text}") |
| play(mini, setup_audio) |
|
|
| time.sleep(1.5) |
|
|
| print(f" β {punchline_text}\n") |
| play(mini, punchline_audio) |
|
|
| time.sleep(0.5) |
| do_laugh(mini) |
| time.sleep(1.0) |
|
|
| except KeyboardInterrupt: |
| print("\nBeende Witz-Bot β¦") |
| mini.goto_sleep() |
| finally: |
| mini.media.stop_recording() |
| mini.media.stop_playing() |
| print("TschΓΌss!") |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|