> ## Documentation Index
> Fetch the complete documentation index at: https://stem-docs.intellectualpoint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Text-to-speech

> Learn how espeak works, build a TTS explorer, then combine speech with movement to create a talking robot driver.

## Text-to-speech: give the robot a voice

<Info>
  **Time**: 10:15 AM to 10:55 AM
</Info>

Your robot has a speaker and now you are going to make it talk. You will learn how the `espeak` text-to-speech engine works, build a program to explore different voices, and then combine speech with driving controls.

***

## Test espeak from the terminal

Before writing any code, verify the speaker works:

```bash theme={null}
espeak "Hello, I am PiCar X" 2>/dev/null
```

You should hear the robot speak. The `2>/dev/null` suppresses audio system warnings — the speech still plays normally.

<Warning>
  If you hear nothing, make sure the batteries are charged (above 7V) and run the audio setup:

  ```bash theme={null}
  cd ~/robot-hat && sudo bash i2samp.sh
  ```

  Reboot and try again.
</Warning>

### espeak parameters

espeak takes several flags that change how the speech sounds:

| Flag | What it does                            | Example                     |
| ---- | --------------------------------------- | --------------------------- |
| `-s` | Speed in words per minute (default 175) | `-s 120` (slower)           |
| `-p` | Pitch from 0–99 (default 50)            | `-p 80` (higher)            |
| `-v` | Voice / language                        | `-v en+f3` (female English) |

Try these in the terminal:

```bash theme={null}
espeak -s 80 "I am speaking very slowly" 2>/dev/null
espeak -s 250 "Now I am speaking fast" 2>/dev/null
espeak -v en+f3 "This is a female voice" 2>/dev/null
espeak -v de "Hallo, ich bin ein Roboter" 2>/dev/null
```

***

## Program 1: TTS explorer

Build a menu-driven program that lets you try all the espeak options interactively.

```bash theme={null}
nano ~/camp/day2/tts_demo.py
```

### Step 1 — The speak function

This function wraps espeak so you can call it from Python. It pipes audio through PulseAudio with a direct-espeak fallback:

```python theme={null}
#!/usr/bin/env python3
import os
import sys
import time

def speak(text, speed=150, pitch=50, voice="en"):
    """Speak text using espeak."""
    safe = text.replace("'", "'\\''")
    ret = os.system(
        f"espeak --stdout -s {speed} -p {pitch} -v {voice} '{safe}' "
        f"2>/dev/null | paplay 2>/dev/null"
    )
    if ret != 0:
        os.system(
            f"espeak -s {speed} -p {pitch} -v {voice} '{safe}' 2>/dev/null"
        )
```

`os.system()` runs a shell command. The `--stdout` flag makes espeak output audio data instead of playing it directly, and `paplay` plays it through PulseAudio to the correct speaker. The `safe` variable escapes single quotes so phrases like "I'm a robot" do not break the command.

### Step 2 — Demo functions

Each demo explores one parameter. Here is the speed demo:

```python theme={null}
def demo_speeds():
    print("\n  --- Different Speeds ---\n")
    text = "Speed test one two three"
    for speed in [80, 120, 175, 250]:
        label = {80: "slow", 120: "normal", 175: "fast", 250: "very fast"}
        print(f"  Speed {speed} ({label.get(speed, '')})...")
        speak(text, speed=speed)
        time.sleep(0.3)
```

The voice demo cycles through different languages and variants:

```python theme={null}
def demo_voices():
    print("\n  --- Different Voices ---\n")
    voices = [
        ("en",    "English (default)"),
        ("en+f3", "English (female)"),
        ("en+m7", "English (whisper)"),
        ("en-sc", "English (Scottish)"),
        ("de",    "German"),
        ("fr",    "French"),
        ("es",    "Spanish"),
    ]
    for voice_id, label in voices:
        print(f"  Voice: {label} ({voice_id})")
        speak("Hello, I am your robot.", voice=voice_id)
        time.sleep(0.3)
```

The custom message mode lets you type anything:

```python theme={null}
def demo_custom():
    print("\n  --- Custom Message ---")
    print("  Type a message and the robot will say it.")
    print("  Type 'done' to finish.\n")
    while True:
        msg = input("  Say: ").strip()
        if msg.lower() in ('done', 'quit', 'exit', 'q'):
            break
        if msg:
            speak(msg)
```

### Step 3 — Menu

Wire the demos into a menu:

```python theme={null}
if __name__ == "__main__":
    print("=" * 50)
    print("  PiCar-X — Text-to-Speech Demo")
    print("=" * 50)

    speak("Ready", speed=175)

    print("""
  [1] Different speeds
  [2] Different voices
  [3] Type your own message
  [q] Quit
""")

    while True:
        choice = input("  Pick 1-3 or q: ").strip().lower()
        if choice == '1': demo_speeds()
        elif choice == '2': demo_voices()
        elif choice == '3': demo_custom()
        elif choice in ('q', 'quit'): break
        else: print("  Enter 1-3 or q.")

    print("\n  Bye!\n")
```

### Run it

```bash theme={null}
python3 ~/camp/day2/tts_demo.py
```

<Note>
  This script does not need `sudo` because it does not use motors or servos — it only plays audio.
</Note>

### Final code: tts\_demo.py

<Accordion title="Click to see the complete program">
  ```python theme={null}
  #!/usr/bin/env python3
  import os
  import sys
  import time


  def speak(text, speed=150, pitch=50, voice="en"):
      safe = text.replace("'", "'\\''")
      ret = os.system(f"espeak --stdout -s {speed} -p {pitch} -v {voice} '{safe}' 2>/dev/null | paplay 2>/dev/null")
      if ret != 0:
          ret = os.system(f"espeak -s {speed} -p {pitch} -v {voice} '{safe}' 2>/dev/null")
      return ret == 0


  def demo_basic():
      print("\n  --- 1. Basic Speech ---\n")
      phrases = [
          "Hello! I am PiCar X.",
          "I can talk using text to speech.",
          "Pretty cool, right?",
      ]
      for phrase in phrases:
          print(f"  Saying: \"{phrase}\"")
          speak(phrase)
          time.sleep(0.5)


  def demo_speeds():
      print("\n  --- 2. Different Speeds ---\n")
      text = "Speed test one two three"
      for speed in [80, 120, 175, 250]:
          label = {80: "slow", 120: "normal", 175: "fast", 250: "very fast"}
          print(f"  Speed {speed} ({label.get(speed, '')})...")
          speak(text, speed=speed)
          time.sleep(0.3)


  def demo_voices():
      print("\n  --- 3. Different Voices ---\n")
      voices = [
          ("en",    "English (default)"),
          ("en+f3", "English (female)"),
          ("en+m7", "English (whisper)"),
          ("en-sc", "English (Scottish)"),
          ("de",    "German"),
          ("fr",    "French"),
          ("es",    "Spanish"),
      ]
      for voice_id, label in voices:
          print(f"  Voice: {label} ({voice_id})")
          speak("Hello, I am your robot.", voice=voice_id)
          time.sleep(0.3)


  def demo_pitch():
      print("\n  --- 4. Different Pitches ---\n")
      for pitch in [10, 30, 50, 70, 99]:
          label = "low" if pitch < 30 else "mid" if pitch < 60 else "high"
          print(f"  Pitch {pitch} ({label})...")
          speak("Testing pitch", pitch=pitch)
          time.sleep(0.3)


  def demo_custom():
      print("\n  --- 5. Custom Message ---")
      print("  Type a message and the robot will say it.")
      print("  Type 'done' to finish.\n")
      while True:
          msg = input("  Say: ").strip()
          if msg.lower() in ('done', 'quit', 'exit', 'q'):
              break
          if msg:
              speak(msg)


  if __name__ == "__main__":
      print("=" * 50)
      print("  PiCar-X — Text-to-Speech Demo")
      print("=" * 50)

      if not speak("Ready", speed=175):
          sys.exit(1)

      print("""
    [1] Basic speech    [2] Different speeds
    [3] Different voices [4] Different pitches
    [5] Custom message   [a] Run all  [q] Quit
  """)

      while True:
          choice = input("  Pick 1-5, a, or q: ").strip().lower()
          if choice == '1': demo_basic()
          elif choice == '2': demo_speeds()
          elif choice == '3': demo_voices()
          elif choice == '4': demo_pitch()
          elif choice == '5': demo_custom()
          elif choice == 'a':
              demo_basic(); demo_speeds(); demo_voices()
              demo_pitch(); demo_custom()
          elif choice in ('q', 'quit'): break
          else: print("  Enter 1-5, a, or q.")

      print("\n  Bye!\n")
  ```
</Accordion>

***

## Program 2: talking driver

Now combine speech with movement. The robot announces each action before performing it. Speech runs in a **background thread** so the robot keeps driving while it talks.

```bash theme={null}
nano ~/camp/day2/talking_driver.py
```

### Step 1 — Background speech

The key difference from `tts_demo.py`: speech must not block driving. Use `threading` to run espeak in the background:

```python theme={null}
#!/usr/bin/env python3
import os
import sys
import time
import threading

from picarx import Picarx

def getch():
    import tty, termios
    fd = sys.stdin.fileno()
    old = termios.tcgetattr(fd)
    try:
        tty.setraw(fd)
        ch = sys.stdin.read(1)
    finally:
        termios.tcsetattr(fd, termios.TCSADRAIN, old)
    return ch

def speak(text, speed=175):
    """Speak in a background thread so driving does not pause."""
    def _say():
        safe = text.replace("'", "'\\''")
        os.system(f"espeak -s {speed} '{safe}' 2>/dev/null")
    threading.Thread(target=_say, daemon=True).start()
```

`threading.Thread(target=_say, daemon=True).start()` launches `_say` in a separate thread. The main loop immediately continues to the next keypress without waiting for espeak to finish. `daemon=True` means the thread automatically stops when the program exits.

### Step 2 — The control loop

Same WASD pattern as the keyboard driver, but each action calls `speak()` first:

```python theme={null}
px = Picarx()
speed = 35
steer_angle = 0
steer_step = 10
CLEAR = "\033[2K\r"

speak("Ready to drive")
time.sleep(1)

print("  Controls: W=fwd S=back A=left D=right SPACE=stop Q=quit\n")

try:
    while True:
        ch = getch()

        if ch in ('q', 'Q', '\x03'):
            speak("Goodbye")
            time.sleep(1)
            break
        elif ch in ('w', 'W'):
            speak("Forward")
            px.forward(speed)
            sys.stdout.write(f"{CLEAR}  >> Forward at {speed}%")
            sys.stdout.flush()
        elif ch in ('s', 'S'):
            speak("Backward")
            px.backward(speed)
            sys.stdout.write(f"{CLEAR}  >> Backward at {speed}%")
            sys.stdout.flush()
        elif ch in ('a', 'A'):
            steer_angle = max(steer_angle - steer_step, -35)
            speak("Turning left")
            px.set_dir_servo_angle(steer_angle)
            sys.stdout.write(f"{CLEAR}  >> Steer {steer_angle:+d}°")
            sys.stdout.flush()
        elif ch in ('d', 'D'):
            steer_angle = min(steer_angle + steer_step, 35)
            speak("Turning right")
            px.set_dir_servo_angle(steer_angle)
            sys.stdout.write(f"{CLEAR}  >> Steer {steer_angle:+d}°")
            sys.stdout.flush()
        elif ch == ' ':
            speak("Stopping")
            px.stop()
            steer_angle = 0
            px.set_dir_servo_angle(0)
            sys.stdout.write(f"{CLEAR}  >> Stopped")
            sys.stdout.flush()

except KeyboardInterrupt:
    pass
finally:
    px.stop()
    px.set_dir_servo_angle(0)
    print(f"\n\n  Stopped. Bye!\n")
```

### Run it

```bash theme={null}
sudo python3 ~/camp/day2/talking_driver.py
```

Drive the robot around and listen — it says "Forward", "Turning left", "Stopping", etc. as you press keys.

### Final code: talking\_driver.py

<Accordion title="Click to see the complete program">
  ```python theme={null}
  #!/usr/bin/env python3
  import os
  import sys
  import time
  import threading

  from picarx import Picarx


  def getch():
      import tty, termios
      fd = sys.stdin.fileno()
      old = termios.tcgetattr(fd)
      try:
          tty.setraw(fd)
          ch = sys.stdin.read(1)
      finally:
          termios.tcsetattr(fd, termios.TCSADRAIN, old)
      return ch


  def speak(text, speed=175):
      def _say():
          safe = text.replace("'", "'\\''")
          os.system(f"espeak -s {speed} '{safe}' 2>/dev/null")
      threading.Thread(target=_say, daemon=True).start()


  px = Picarx()
  speed = 35
  steer_angle = 0
  steer_step = 10
  CLEAR = "\033[2K\r"

  print("=" * 50)
  print("  PiCar-X — Talking Driver")
  print("=" * 50)
  print("  Controls: W=fwd S=back A=left D=right SPACE=stop Q=quit\n")

  speak("Ready to drive")
  time.sleep(1)

  try:
      while True:
          ch = getch()

          if ch in ('q', 'Q', '\x03'):
              speak("Goodbye")
              time.sleep(1)
              break
          elif ch in ('w', 'W'):
              speak("Forward")
              px.forward(speed)
              sys.stdout.write(f"{CLEAR}  >> Forward at {speed}%")
              sys.stdout.flush()
          elif ch in ('s', 'S'):
              speak("Backward")
              px.backward(speed)
              sys.stdout.write(f"{CLEAR}  >> Backward at {speed}%")
              sys.stdout.flush()
          elif ch in ('a', 'A'):
              steer_angle = max(steer_angle - steer_step, -35)
              speak("Turning left")
              px.set_dir_servo_angle(steer_angle)
              sys.stdout.write(f"{CLEAR}  >> Steer {steer_angle:+d}°")
              sys.stdout.flush()
          elif ch in ('d', 'D'):
              steer_angle = min(steer_angle + steer_step, 35)
              speak("Turning right")
              px.set_dir_servo_angle(steer_angle)
              sys.stdout.write(f"{CLEAR}  >> Steer {steer_angle:+d}°")
              sys.stdout.flush()
          elif ch == ' ':
              speak("Stopping")
              px.stop()
              steer_angle = 0
              px.set_dir_servo_angle(0)
              sys.stdout.write(f"{CLEAR}  >> Stopped")
              sys.stdout.flush()

  except KeyboardInterrupt:
      pass
  finally:
      px.stop()
      px.set_dir_servo_angle(0)
      print(f"\n\n  Stopped. Bye!\n")
  ```
</Accordion>

***

## Challenge: customize the talking driver

<Steps>
  <Step title="Custom startup message">
    Change the `speak("Ready to drive")` line to your own greeting. The robot will say it every time the script starts.
  </Step>

  <Step title="Add speed announcements">
    Add `elif` blocks for **+** and **-** keys that announce the new speed. For example: `speak(f"Speed {speed} percent")`.
  </Step>

  <Step title="Custom voice">
    Change the voice in the `speak()` function. Try `espeak -v en+f3` for a female voice, or `-v en+m7` for a whisper.
  </Step>
</Steps>

<Note>
  After this section, take a 10-minute break (10:55 AM to 11:05 AM).
</Note>
