Skill: henrique-simoes/audio-ptbr-autoreply (version 2.1.3)

--- name: audio-ptbr-autoreply version: 2.1.3 description: | Brazilian Portuguese voice reply skill helpers for OpenClaw. Transcribes audio locally with wav2vec2, generates a reply with the local OpenClaw agent by default, synthesizes audio locally with Piper, and optionally uses Anthropic only when ANTHROPIC_API_KEY is set. Installer uses a local virtualenv only and does not auto-install OS packages. category: voice tags: - voice - audio - speech-to-text - text-to-speech - portuguese - accessibility triggers: - command: "/voz" description: "Selecionar voz ou listar vozes" action: "bash process.sh --voice-command "{{args}}"" config: tools: media: audio: scope: default: "allow" --- # Audio PT Auto-Reply 🎙️🇧🇷 Talk to your OpenClaw agent in Brazilian Portuguese, and get a voice reply back. That is the whole idea. You send an audio message. The skill transcribes it locally. Your OpenClaw agent answers. The answer comes back as audio. 🔊 No big platform. No weird magic. Just a small, useful voice loop for people who would rather speak than type. ## Why this exists ✨ Typing is not always the best way to talk to an agent. Sometimes you are on your phone. Sometimes you are walking. Sometimes you are doing something else. Sometimes voice just feels more natural. Audio PT Auto-Reply gives OpenClaw a simple PT-BR voice workflow that feels closer to messaging a person than operating a tool. It is especially useful for Telegram-style interactions, accessibility workflows, quick mobile replies, and hands-busy situations. ## What it does 🧩 Audio PT Auto-Reply adds a focused voice pipeline to OpenClaw: * 🎧 transcribes Brazilian Portuguese audio locally with [REDACTED_SECRET] * 🧠 asks your local OpenClaw agent to generate a short reply by default * ☁️ can optionally use Anthropic only when `ANTHROPIC_API_KEY` is set * 🗣️ turns the answer into speech with local Piper voices * 🎚️ lets you choose voices with `/voz` * 🩺 includes a health check so setup problems are easier to find ## What it does not do 🚧 This skill is intentionally small and careful. It does not request `sudo`. It does not install system packages behind your back. It does not modify other skills. It does not read unrelated files. It does not upload audio files to third-party services. It does not ship a public automatic audio hook that expands untrusted template values inside a shell command. That last part matters. Earlier hook-based builds were too easy to make risky because values like `{{MediaPath}}` could be expanded by the platform into a shell command before the skill code had a chance to validate them. So this build keeps the useful part, the voice pipeline, and removes the risky public hook surface. Safer, cleaner, easier to review. 🛡️ ## Privacy model 🔒 By default, the skill is local-first: * audio transcription runs locally * speech synthesis runs locally * response generation uses the local OpenClaw CLI * audio files are not uploaded by this skill Optional external mode: * if `ANTHROPIC_API_KEY` is present, transcript text may be sent to Anthropic for response generation * audio is still not uploaded by this skill * unset `ANTHROPIC_API_KEY` to keep response generation local ## Install ⚙️ Run: ```bash bash install.sh ``` The installer creates a virtualenv inside the skill directory, installs Python dependencies there, downloads Piper, downloads PT-BR voices, writes the default voice config, and runs a health check. It expects these system dependencies to already exist: ```text python3 ffmpeg tar curl or wget ``` If something is missing, the installer stops and tells you what to install manually. ## Use 🗣️ List available voices: ```bash /voz listar ``` Choose a voice: ```bash /voz jeff /voz cadu /voz faber /voz miro /voz feminina /voz masculina ``` Process an audio file manually: ```bash bash process.sh --audio-file /absolute/path/to/audio.ogg ``` When synthesis succeeds, the script prints a `MEDIA:` directive pointing to the generated voice reply. ## Optional environment variables 🧰 ```text ANTHROPIC_API_KEY Enables Anthropic response generation AUDIO_VOICE Sets the default voice RESPONSE_TIMEOUT Response timeout in seconds, default 30 SYNTHESIS_TIMEOUT Synthesis timeout in seconds, default 45 WORKSPACE Overrides the OpenClaw workspace path PYTHON_BIN Overrides the Python executable used by install.sh ``` ## Safety note for hooks 🛡️ This public package does not register an automatic `message.audio.receive` hook. That is deliberate. Shell-templated hooks can become unsafe when the platform expands values like media paths, targets, or message IDs into a shell command string before your script receives them. For public distribution, the safer choice is to ship the voice pipeline without that hook. `LOCAL_HOOK_EXAMPLE.md` exists only for local operators who understand the risk and want to wire a hook manually in a controlled environment. ## Files included 📦 ```text install.sh Installer with local virtualenv setup process.sh Main voice-processing entry point health_check.py Setup validation LOCAL_HOOK_EXAMPLE.md Local-only hook notes requirements.txt Required Python dependencies requirements-optional.txt Optional Anthropic dependency scripts/transcribe_universal.py Local PT-BR transcription scripts/claude_adapter.py OpenClaw or optional Anthropic response generation scripts/synthesize_universal.py Piper TTS synthesis scripts/voice_config.py Voice selection storage ``` ## Good fit ✅ Use this skill if you want a small Portuguese voice loop for OpenClaw, especially when you care about local transcription, local speech synthesis, and a public package that avoids unnecessary permission creep. It is not trying to be a full voice assistant platform. It is just a focused voice-reply helper: audio in, agent response, audio out. 🎙️→🧠→🔊

path: _meta.json
content: ownerId: kn7d13re5xd7rac3vrjg74bgk584mgyg
slug: audio-ptbr-autoreply
version: 2.1.3
publishedAt: 1776869661901

path: process.sh
content: #!/usr/bin/env bash
set -euo pipefail
IFS=$'
	'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="${WORKSPACE:-${HOME}/.openclaw/workspace}"
VENV_PY="${SCRIPT_DIR}/.venv/bin/python"

log() { printf '%s
' "$*" >&2; }
fail() { log "ERROR: $*"; exit 1; }

python_cmd() {
  if [[ -x "$VENV_PY" ]]; then
    printf '%s
' "$VENV_PY"
  elif command -v python3 >/dev/null 2>&1; then
    printf 'python3
'
  else
    fail "python3 not found"
  fi
}

usage() {
  cat <<'TXT'
Usage:
  bash process.sh --voice-command "listar"
  bash process.sh --audio-file /absolute/path/to/audio.ogg [--target TARGET] [--reply-to MESSAGE_ID]
TXT
}

handle_voice_command() {
  local arg="${1:-}"
  local py
  py="$(python_cmd)"

  case "${arg,,}" in
    ""|"listar"|"lista")
      "$py" "${SCRIPT_DIR}/scripts/voice_config.py" list
      ;;
    "jeff"|"cadu"|"faber"|"miro"|"feminina"|"masculina")
      "$py" "${SCRIPT_DIR}/scripts/voice_config.py" set "$arg"
      ;;
    *)
      usage
      return 1
      ;;
  esac
}

send_media_directive() {
  local audio_path="$1"
  local target="${2:-}"
  local reply_to="${3:-}"

  printf 'MEDIA:%s [[audio_as_voice]]
' "$audio_path"

  if [[ -n "$target" ]] && command -v openclaw >/dev/null 2>&1; then
    openclaw sessions send       --target "$target"       --message "MEDIA:${audio_path} [[audio_as_voice]]"       --reply-to "$reply_to" >/dev/null 2>&1 || true
  fi
}

main() {
  local py audio_file="" target="" reply_to="" voice_cmd=""
  py="$(python_cmd)"

  while [[ $# -gt 0 ]]; do
    case "$1" in
      --voice-command)
        shift
        [[ $# -gt 0 ]] || fail "Missing value for --voice-command"
        voice_cmd="$1"
        ;;
      --audio-file)
        shift
        [[ $# -gt 0 ]] || fail "Missing value for --audio-file"
        audio_file="$1"
        ;;
      --target)
        shift
        [[ $# -gt 0 ]] || fail "Missing value for --target"
        target="$1"
        ;;
      --reply-to)
        shift
        [[ $# -gt 0 ]] || fail "Missing value for --reply-to"
        reply_to="$1"
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        fail "Unknown argument: $1"
        ;;
    esac
    shift
  done

  if [[ -n "$voice_cmd" ]]; then
    handle_voice_command "$voice_cmd"
    exit 0
  fi

  [[ -n "$audio_file" ]] || fail "Missing --audio-file"
  [[ -f "$audio_file" ]] || fail "Audio file not found: $audio_file"

  local transcript response audio_path voice transcript_file response_file
  voice="$($py "${SCRIPT_DIR}/scripts/voice_config.py" get)"

  transcript_file="$(mktemp)"
  response_file="$(mktemp)"
  trap 'rm -f "$transcript_file" "$response_file"' EXIT

  "$py" "${SCRIPT_DIR}/scripts/transcribe_universal.py" --text "$audio_file" > "$transcript_file" || fail "Transcription failed"
  [[ -s "$transcript_file" ]] || fail "Empty transcript"

  "$py" "${SCRIPT_DIR}/scripts/claude_adapter.py" --stdin --text < "$transcript_file" > "$response_file" || fail "Response generation failed"
  [[ -s "$response_file" ]] || fail "Empty response"

  audio_path="$($py "${SCRIPT_DIR}/scripts/synthesize_universal.py" --stdin --voice "$voice" --print-path < "$response_file")" || fail "Synthesis failed"
  [[ -f "$audio_path" ]] || fail "Audio output not found"

  send_media_directive "$audio_path" "$target" "$reply_to"
}

main "$@"


path: requirements.txt
content: torch==2.1.2
torchaudio==2.1.2
transformers==4.36.2
numpy==1.24.3


path: install.sh
content: #!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="${WORKSPACE:-${HOME}/.openclaw/workspace}"
PIPER_DIR="${WORKSPACE}/piper"
VENV_DIR="${SCRIPT_DIR}/.venv"
PYTHON_BIN="${PYTHON_BIN:-python3}"

PIPER_VERSION="v1.2.0"

declare -A VOICE_URLS=(
  ["pt_BR-jeff-medium.onnx"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/jeff/medium/pt_BR-jeff-medium.onnx"
  ["pt_BR-jeff-medium.onnx.json"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/jeff/medium/pt_BR-jeff-medium.onnx.json"
  ["pt_BR-cadu-medium.onnx"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/cadu/medium/pt_BR-cadu-medium.onnx"
  ["pt_BR-cadu-medium.onnx.json"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/cadu/medium/pt_BR-cadu-medium.onnx.json"
  ["pt_BR-faber-medium.onnx"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx"
  ["pt_BR-faber-medium.onnx.json"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/faber/medium/pt_BR-faber-medium.onnx.json"
  ["pt_BR-miro-high.onnx"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/miro/high/pt_BR-miro-high.onnx"
  ["pt_BR-miro-high.onnx.json"]="https://huggingface.co/rhasspy/piper-voices/resolve/main/pt/pt_BR/miro/high/pt_BR-miro-high.onnx.json"
)

info() { printf '%s\n' "$*"; }
fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

have_cmd() {
  command -v "$1" >/dev/null 2>&1
}

download_to() {
  local url="$1"
  local dest="$2"
  if have_cmd curl; then
    curl -fsSL "$url" -o "$dest"
  elif have_cmd wget; then
    wget -q "$url" -O "$dest"
  else
    fail "Neither curl nor wget is installed. Install one of them and rerun."
  fi
}

detect_piper_archive() {
  local arch os file
  arch="$(uname -m)"
  os="$(uname -s)"

  case "$arch" in
    arm64|aarch64) file="piper_arm64.tar.gz" ;;
    x86_64|amd64) file="piper_x86_64.tar.gz" ;;
    *) fail "Unsupported architecture: ${arch}" ;;
  esac

  case "$os" in
    Linux|Darwin) ;;
    *) fail "Unsupported operating system: ${os}" ;;
  esac

  printf 'https://github.com/rhasspy/piper/releases/download/%s/%s\n' "$PIPER_VERSION" "$file"
}

check_os_dependencies() {
  have_cmd "$PYTHON_BIN" || fail "python3 is required but not found."
  have_cmd ffmpeg || fail "ffmpeg is required but not found."
  have_cmd tar || fail "tar is required but not found."
  if ! have_cmd curl && ! have_cmd wget; then
    fail "curl or wget is required but neither is installed."
  fi
}

create_venv() {
  info "[1/5] Creating local virtualenv"
  "$PYTHON_BIN" -m venv "$VENV_DIR"
  "${VENV_DIR}/bin/python" -m pip install --upgrade pip setuptools wheel
  "${VENV_DIR}/bin/pip" install -r "${SCRIPT_DIR}/requirements.txt"
  if [[ -n "${ANTHROPIC_API_KEY:-}" ]]; then
    info "ANTHROPIC_API_KEY detected; installing optional Anthropic dependency"
    "${VENV_DIR}/bin/pip" install -r "${SCRIPT_DIR}/requirements-optional.txt"
  fi
}

install_piper() {
  info "[2/5] Installing Piper into ${PIPER_DIR}"
  mkdir -p "$PIPER_DIR"
  if [[ -x "${PIPER_DIR}/piper/piper" ]]; then
    info "Piper already present"
    return 0
  fi

  local archive_url archive_path
  archive_url="$(detect_piper_archive)"
  archive_path="${PIPER_DIR}/piper.tar.gz"
  download_to "$archive_url" "$archive_path"
  tar -xzf "$archive_path" -C "$PIPER_DIR"
  rm -f "$archive_path"
  chmod +x "${PIPER_DIR}/piper/piper"
}

install_voices() {
  info "[3/5] Downloading PT-BR voice models"
  mkdir -p "$PIPER_DIR"
  local name url
  for name in "${!VOICE_URLS[@]}"; do
    url="${VOICE_URLS[$name]}"
    if [[ -f "${PIPER_DIR}/${name}" ]]; then
      info "  - ${name} already present"
    else
      info "  - downloading ${name}"
      download_to "$url" "${PIPER_DIR}/${name}"
    fi
  done
}

write_default_config() {
  info "[4/5] Writing default voice config"
  mkdir -p "$WORKSPACE"
  if [[ ! -f "${WORKSPACE}/.audio_pt_voice_config.json" ]]; then
    cat > "${WORKSPACE}/.audio_pt_voice_config.json" <<'JSON'
{"voice":"jeff"}
JSON
  fi
}

run_health_check() {
  info "[5/5] Running health check"
  "${VENV_DIR}/bin/python" "${SCRIPT_DIR}/health_check.py"
}

main() {
  check_os_dependencies
  create_venv
  install_piper
  install_voices
  write_default_config
  run_health_check
  info "Install complete."
  info "Restart OpenClaw after installation if needed."
}

main "$@"


path: LOCAL_HOOK_EXAMPLE.md
content: # Optional local hook example

Do not include this hook in the public ClawHub build unless the platform provides a structured non-shell hook API.

Example of the **old risky shape**:

```yaml
hooks:
  - event: "message.audio.receive"
    action: "bash process.sh "{{MediaPath}}" "{{Target}}" "{{MessageID}}""
```

Why risky:
- the templated values are expanded into a shell command string before `process.sh` runs
- if a value contains shell-breaking characters, quoting can be bypassed at the shell parsing layer

Safer direction:
- wait for an OpenClaw hook mechanism that passes structured arguments without shell interpolation
- or register the hook only in a locally controlled environment after review and acceptance of that platform-level risk


path: requirements-optional.txt
content: anthropic>=0.51.0,<1.0


path: README.md
content: # Audio PT Auto-Reply 🎙️🇧🇷

Talk to your OpenClaw agent in Brazilian Portuguese, and get a voice reply back.

That is the whole idea.

You send an audio message.
The skill transcribes it locally.
Your OpenClaw agent answers.
The answer comes back as audio. 🔊

No big platform. No weird magic. Just a small, useful voice loop for people who would rather speak than type.

## Why this exists ✨

Typing is not always the best way to talk to an agent.

Sometimes you are on your phone. Sometimes you are walking. Sometimes you are doing something else. Sometimes voice just feels more natural.

Audio PT Auto-Reply gives OpenClaw a simple PT-BR voice workflow that feels closer to messaging a person than operating a tool.

It is especially useful for Telegram-style interactions, accessibility workflows, quick mobile replies, and hands-busy situations.

## What it does 🧩

Audio PT Auto-Reply adds a focused voice pipeline to OpenClaw:

* 🎧 transcribes Brazilian Portuguese audio locally with `jonatasgrosman/wav2vec2-large-xlsr-53-portuguese`
* 🧠 asks your local OpenClaw agent to generate a short reply by default
* ☁️ can optionally use Anthropic only when `ANTHROPIC_API_KEY` is set
* 🗣️ turns the answer into speech with local Piper voices
* 🎚️ lets you choose voices with `/voz`
* 🩺 includes a health check so setup problems are easier to find

## What it does not do 🚧

This skill is intentionally small and careful.

It does not request `sudo`.
It does not install system packages behind your back.
It does not modify other skills.
It does not read unrelated files.
It does not upload audio files to third-party services.
It does not ship a public automatic audio hook that expands untrusted template values inside a shell command.

That last part matters.

Earlier hook-based builds were too easy to make risky because values like `{{MediaPath}}` could be expanded by the platform into a shell command before the skill code had a chance to validate them.

So this build keeps the useful part, the voice pipeline, and removes the risky public hook surface. Safer, cleaner, easier to review. 🛡️

## Privacy model 🔒

By default, the skill is local-first:

* audio transcription runs locally
* speech synthesis runs locally
* response generation uses the local OpenClaw CLI
* audio files are not uploaded by this skill

Optional external mode:

* if `ANTHROPIC_API_KEY` is present, transcript text may be sent to Anthropic for response generation
* audio is still not uploaded by this skill
* unset `ANTHROPIC_API_KEY` to keep response generation local

## Install ⚙️

Run:

```bash
bash install.sh
```

The installer creates a virtualenv inside the skill directory, installs Python dependencies there, downloads Piper, downloads PT-BR voices, writes the default voice config, and runs a health check.

It expects these system dependencies to already exist:

```text
python3
ffmpeg
tar
curl or wget
```

If something is missing, the installer stops and tells you what to install manually.

## Use 🗣️

List available voices:

```bash
/voz listar
```

Choose a voice:

```bash
/voz jeff
/voz cadu
/voz faber
/voz miro
/voz feminina
/voz masculina
```

Process an audio file manually:

```bash
bash process.sh --audio-file /absolute/path/to/audio.ogg
```

When synthesis succeeds, the script prints a `MEDIA:` directive pointing to the generated voice reply.

## Optional environment variables 🧰

```text
ANTHROPIC_API_KEY     Enables Anthropic response generation
AUDIO_VOICE           Sets the default voice
RESPONSE_TIMEOUT      Response timeout in seconds, default 30
SYNTHESIS_TIMEOUT     Synthesis timeout in seconds, default 45
WORKSPACE             Overrides the OpenClaw workspace path
PYTHON_BIN            Overrides the Python executable used by install.sh
```

## Safety note for hooks 🛡️

This public package does not register an automatic `message.audio.receive` hook.

That is deliberate.

Shell-templated hooks can become unsafe when the platform expands values like media paths, targets, or message IDs into a shell command string before your script receives them.

For public distribution, the safer choice is to ship the voice pipeline without that hook. `LOCAL_HOOK_EXAMPLE.md` exists only for local operators who understand the risk and want to wire a hook manually in a controlled environment.

## Files included 📦

```text
install.sh                         Installer with local virtualenv setup
process.sh                         Main voice-processing entry point
health_check.py                    Setup validation
LOCAL_HOOK_EXAMPLE.md              Local-only hook notes
requirements.txt                   Required Python dependencies
requirements-optional.txt          Optional Anthropic dependency
scripts/transcribe_universal.py    Local PT-BR transcription
scripts/claude_adapter.py          OpenClaw or optional Anthropic response generation
scripts/synthesize_universal.py    Piper TTS synthesis
scripts/voice_config.py            Voice selection storage
```

## Good fit ✅

Use this skill if you want a small Portuguese voice loop for OpenClaw, especially when you care about local transcription, local speech synthesis, and a public package that avoids unnecessary permission creep.

It is not trying to be a full voice assistant platform.

It is just a focused voice-reply helper: audio in, agent response, audio out. 🎙️→🧠→🔊


path: health_check.py
content: #!/usr/bin/env python3
from __future__ import annotations

import json
import os
import shutil
import subprocess
import sys
from pathlib import Path


SCRIPT_DIR = Path(__file__).resolve().parent
WORKSPACE = Path(os.environ.get("WORKSPACE", Path.home() / ".openclaw" / "workspace"))
PIPER_DIR = WORKSPACE / "piper"
VENV_PY = SCRIPT_DIR / ".venv" / "bin" / "python"

REQUIRED_VOICES = [
    "pt_BR-jeff-medium.onnx",
    "pt_BR-cadu-medium.onnx",
    "pt_BR-faber-medium.onnx",
    "pt_BR-miro-high.onnx",
]


def check(name: str, ok: bool, details: str = "") -> bool:
    prefix = "✅" if ok else "❌"
    print(f"{prefix} {name}")
    if details:
        print(f"   {details}")
    return ok


def main() -> int:
    print("Audio PT Auto-Reply - Health Check")
    print("=" * 50)

    ok_all = True

    ok_all &= check("python3", shutil.which("python3") is not None, "Required OS dependency")
    ok_all &= check("ffmpeg", shutil.which("ffmpeg") is not None, "Required OS dependency")
    ok_all &= check("tar", shutil.which("tar") is not None, "Required OS dependency")
    ok_all &= check("curl/wget", shutil.which("curl") is not None or shutil.which("wget") is not None, "Required OS dependency")

    ok_all &= check("virtualenv python", VENV_PY.exists(), str(VENV_PY))
    piper_bin = PIPER_DIR / "piper" / "piper"
    ok_all &= check("piper binary", piper_bin.exists() and os.access(piper_bin, os.X_OK), str(piper_bin))

    for voice in REQUIRED_VOICES:
      ok_all &= check(f"voice model: {voice}", (PIPER_DIR / voice).exists(), str(PIPER_DIR / voice))

    cfg_path = WORKSPACE / ".audio_pt_voice_config.json"
    ok_all &= check("voice config", cfg_path.exists(), str(cfg_path))

    if VENV_PY.exists():
        try:
            result = subprocess.run(
                [str(VENV_PY), "-c", "import torch, torchaudio, transformers; print('ok')"],
                check=True,
                capture_output=True,
                text=True,
                timeout=20,
            )
            ok_all &= check("python dependencies", result.stdout.strip() == "ok", "torch, torchaudio, transformers")
        except Exception as exc:
            ok_all &= check("python dependencies", False, str(exc))

        try:
            result = subprocess.run(
                [str(VENV_PY), str(SCRIPT_DIR / "scripts" / "voice_config.py"), "get"],
                check=True,
                capture_output=True,
                text=True,
                timeout=10,
            )
            ok_all &= check("voice_config.py", bool(result.stdout.strip()), result.stdout.strip())
        except Exception as exc:
            ok_all &= check("voice_config.py", False, str(exc))

    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            result = subprocess.run(
                [str(VENV_PY), "-c", "import anthropic; print('ok')"],
                check=True,
                capture_output=True,
                text=True,
                timeout=10,
            )
            ok_all &= check("anthropic optional dependency", result.stdout.strip() == "ok", "ANTHROPIC_API_KEY is set")
        except Exception as exc:
            ok_all &= check("anthropic optional dependency", False, f"Set but package missing: {exc}")
    else:
        check("anthropic optional dependency", True, "Not configured; local mode will be used")

    print("=" * 50)
    print("Ready" if ok_all else "One or more checks failed")
    return 0 if ok_all else 1


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/transcribe_universal.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path
from typing import Any

try:
    from transformers import pipeline
except ImportError as exc:
    raise SystemExit("transformers not installed in this environment") from exc


MODEL_NAME = "jonatasgrosman/wav2vec2-large-xlsr-53-portuguese"
_PIPELINE = None


def get_pipeline():
    global _PIPELINE
    if _PIPELINE is None:
        print("Loading ASR model...", file=sys.stderr)
        _PIPELINE = pipeline("automatic-speech-recognition", model=MODEL_NAME)
    return _PIPELINE


def transcribe(audio_path: str) -> dict[str, Any]:
    path = Path(audio_path)
    if not path.exists():
        raise FileNotFoundError(f"Audio file not found: {path}")

    result = get_pipeline()(str(path))
    text = str(result.get("text", "")).strip()

    return {
        "success": True,
        "text": text,
        "model": MODEL_NAME,
        "language": "pt-BR",
    }


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("audio_path")
    parser.add_argument("--text", action="store_true", help="Print only transcribed text")
    args = parser.parse_args()

    try:
        data = transcribe(args.audio_path)
    except Exception as exc:
        print(json.dumps({"success": False, "error": str(exc)}), file=sys.stderr)
        return 1

    if args.text:
        print(data["text"])
    else:
        print(json.dumps(data, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/voice_config.py
content: #!/usr/bin/env python3
from __future__ import annotations

import json
import os
import sys
from pathlib import Path

WORKSPACE = Path(os.environ.get("WORKSPACE", Path.home() / ".openclaw" / "workspace"))
CONFIG_PATH = WORKSPACE / ".audio_pt_voice_config.json"

VOICE_MAP = {
    "jeff": "jeff",
    "cadu": "cadu",
    "faber": "faber",
    "miro": "miro",
    "feminina": "miro",
    "masculina": "jeff",
}

VOICE_LABELS = {
    "jeff": "Jeff",
    "cadu": "Cadu",
    "faber": "Faber",
    "miro": "Miro",
}


def ensure_config() -> None:
    WORKSPACE.mkdir(parents=True, exist_ok=True)
    if not CONFIG_PATH.exists():
        CONFIG_PATH.write_text(json.dumps({"voice": "jeff"}), encoding="utf-8")


def get_voice() -> str:
    ensure_config()
    try:
        data = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
        voice = data.get("voice", "jeff")
        return voice if voice in VOICE_LABELS else "jeff"
    except Exception:
        return "jeff"


def set_voice(name: str) -> str:
    ensure_config()
    normalized = VOICE_MAP.get(name.lower())
    if not normalized:
        raise ValueError(f"Unknown voice: {name}")
    CONFIG_PATH.write_text(json.dumps({"voice": normalized}), encoding="utf-8")
    return normalized


def print_list() -> None:
    print("Vozes disponíveis:")
    print("- jeff")
    print("- cadu")
    print("- faber")
    print("- miro")
    print("- feminina -> miro")
    print("- masculina -> jeff")
    print(f"Atual: {get_voice()}")


def main(argv: list[str]) -> int:
    if len(argv) < 2:
        print(get_voice())
        return 0

    cmd = argv[1].lower()

    if cmd == "get":
        print(get_voice())
        return 0
    if cmd == "list":
        print_list()
        return 0
    if cmd == "set":
        if len(argv) < 3:
            print("Usage: voice_config.py set <voice>", file=sys.stderr)
            return 1
        try:
            voice = set_voice(argv[2])
        except ValueError as exc:
            print(str(exc), file=sys.stderr)
            return 1
        print(f"Voz definida para: {voice}")
        return 0

    # convenience: direct voice name as subcommand
    if cmd in VOICE_MAP:
        voice = set_voice(cmd)
        print(f"Voz definida para: {voice}")
        return 0

    print("Usage: voice_config.py [get|list|set <voice>]", file=sys.stderr)
    return 1


if __name__ == "__main__":
    raise SystemExit(main(sys.argv))


path: scripts/claude_adapter.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import os
import subprocess
import sys
from pathlib import Path
from typing import Any

try:
    from anthropic import Anthropic
except Exception:
    Anthropic = None  # type: ignore

DEFAULT_TIMEOUT = int(os.environ.get("RESPONSE_TIMEOUT", "30"))


class ResponseError(Exception):
    pass


def read_transcript(args: argparse.Namespace) -> str:
    if args.stdin:
        return sys.stdin.read().strip()
    if args.input_file:
        return Path(args.input_file).read_text(encoding="utf-8").strip()
    if args.transcript is not None:
        return args.transcript.strip()
    raise ResponseError("No transcript provided")


def get_anthropic_response(transcript: str, timeout: int) -> str:
    [REDACTED_SECRET]"ANTHROPIC_API_KEY")
    if not api_key:
        raise ResponseError("ANTHROPIC_API_KEY not configured")
    if Anthropic is None:
        raise ResponseError("anthropic package not installed")

    client = Anthropic([REDACTED_SECRET])
    msg = client.messages.create(
        model="claude-3-5-sonnet-20241022",
        max_tokens=180,
        system=(
            "Você é um assistente de voz em português brasileiro. "
            "Responda de forma curta, natural e clara, em no máximo 2 frases. "
            "Sem listas, sem markdown, sem emojis."
        ),
        messages=[{"role": "user", "content": transcript}],
        timeout=timeout,
    )
    text = msg.content[0].text.strip()
    if not text:
        raise ResponseError("Empty Anthropic response")
    return text


def get_openclaw_response(transcript: str, timeout: int) -> str:
    try:
        result = subprocess.run(
            ["openclaw", "infer", "model", "run", "--prompt", transcript, "--json"],
            capture_output=True,
            text=True,
            timeout=timeout,
            check=False,
        )
    except FileNotFoundError as exc:
        raise ResponseError("openclaw command not found") from exc
    except subprocess.TimeoutExpired as exc:
        raise ResponseError(f"openclaw timed out after {timeout}s") from exc

    if result.returncode != 0:
        raise ResponseError(result.stderr.strip() or f"openclaw exited with {result.returncode}")

    try:
        payload = json.loads(result.stdout)
        text = str(payload.get("response", "")).strip()
        if text:
            return text
    except json.JSONDecodeError:
        pass

    fallback = result.stdout.strip()
    if fallback:
        return fallback
    raise ResponseError("Empty local OpenClaw response")


def generate_response(transcript: str, timeout: int) -> dict[str, Any]:
    transcript = transcript.strip()
    if not transcript:
        raise ValueError("Transcript cannot be empty")

    errors: list[str] = []

    if os.environ.get("ANTHROPIC_API_KEY"):
        try:
            text = get_anthropic_response(transcript, timeout)
            return {"success": True, "response": text, "provider": "anthropic"}
        except Exception as exc:
            errors.append(f"anthropic: {exc}")

    try:
        text = get_openclaw_response(transcript, timeout)
        return {"success": True, "response": text, "provider": "openclaw"}
    except Exception as exc:
        errors.append(f"openclaw: {exc}")

    fallback = f"Entendi. Você disse: {transcript[:80]}"
    return {"success": True, "response": fallback, "provider": "fallback", "errors": errors}


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("transcript", nargs="?")
    parser.add_argument("--stdin", action="store_true", help="Read transcript from stdin")
    parser.add_argument("--input-file", help="Read transcript from a UTF-8 text file")
    parser.add_argument("--text", action="store_true", help="Print only response text")
    parser.add_argument("--json", action="store_true", help="Print JSON payload")
    parser.add_argument("--timeout", type=int, default=DEFAULT_TIMEOUT)
    args = parser.parse_args()

    try:
        transcript = read_transcript(args)
        data = generate_response(transcript, args.timeout)
    except Exception as exc:
        payload = {"success": False, "error": str(exc)}
        print(json.dumps(payload, ensure_ascii=False), file=sys.stderr)
        return 1

    if args.text:
        print(data["response"])
    else:
        print(json.dumps(data, ensure_ascii=False))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


path: scripts/synthesize_universal.py
content: #!/usr/bin/env python3
from __future__ import annotations

import argparse
import os
import subprocess
import sys
import tempfile
from pathlib import Path

WORKSPACE = Path(os.environ.get("WORKSPACE", Path.home() / ".openclaw" / "workspace"))
PIPER_DIR = WORKSPACE / "piper"
DEFAULT_TIMEOUT = int(os.environ.get("SYNTHESIS_TIMEOUT", "45"))

VOICE_MAP = {
    "jeff": "pt_BR-jeff-medium.onnx",
    "cadu": "pt_BR-cadu-medium.onnx",
    "faber": "pt_BR-faber-medium.onnx",
    "miro": "pt_BR-miro-high.onnx",
    "feminina": "pt_BR-miro-high.onnx",
    "masculina": "pt_BR-jeff-medium.onnx",
}


class SynthesisError(Exception):
    pass


def read_text(args: argparse.Namespace) -> str:
    if args.stdin:
        return sys.stdin.read().strip()
    if args.input_file:
        return Path(args.input_file).read_text(encoding="utf-8").strip()
    if args.text is not None:
        return args.text.strip()
    raise SynthesisError("No text provided")


def get_paths(voice: str) -> tuple[Path, Path]:
    model = VOICE_MAP.get(voice.lower(), VOICE_MAP["jeff"])
    piper_bin = PIPER_DIR / "piper" / "piper"
    model_path = PIPER_DIR / model

    if not piper_bin.exists():
        raise SynthesisError(f"Piper binary not found: {piper_bin}")
    if not os.access(piper_bin, os.X_OK):
        raise SynthesisError(f"Piper binary is not executable: {piper_bin}")
    if not model_path.exists():
        raise SynthesisError(f"Voice model not found: {model_path}")

    return piper_bin, model_path


def synthesize_to_file(text: str, voice: str = "jeff", timeout: int = DEFAULT_TIMEOUT) -> Path:
    if not text.strip():
        raise SynthesisError("Text cannot be empty")

    piper_bin, model_path = get_paths(voice)
    out_dir = Path(tempfile.gettempdir())
    wav_path = out_dir / "audio_pt_reply.wav"
    ogg_path = out_dir / "audio_pt_reply.ogg"

    for path in (wav_path, ogg_path):
        if path.exists():
            path.unlink()

    try:
        subprocess.run(
            [str(piper_bin), "--model", str(model_path), "--output_file", str(wav_path)],
            input=text.encode("utf-8"),
            capture_output=True,
            timeout=timeout,
            check=True,
        )
    except subprocess.TimeoutExpired as exc:
        raise SynthesisError(f"Piper timed out after {timeout}s") from exc
    except subprocess.CalledProcessError as exc:
        raise SynthesisError(exc.stderr.decode("utf-8", errors="ignore")) from exc

    try:
        subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-i",
                str(wav_path),
                "-c:a",
                "libopus",
                "-b:a",
                "32k",
                str(ogg_path),
            ],
            capture_output=True,
            timeout=timeout,
            check=True,
        )
    except subprocess.TimeoutExpired as exc:
        raise SynthesisError(f"ffmpeg timed out after {timeout}s") from exc
    except subprocess.CalledProcessError as exc:
        raise SynthesisError(exc.stderr.decode("utf-8", errors="ignore")) from exc
    finally:
        if wav_path.exists():
            wav_path.unlink()

    if not ogg_path.exists():
        raise SynthesisError("OGG output was not created")
    return ogg_path


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("text", nargs="?")
    parser.add_argument("--stdin", action="store_true", help="Read text from stdin")
    parser.add_argument("--input-file", help="Read text from a UTF-8 file")
    parser.add_argument("--voice", default=os.environ.get("AUDIO_VOICE", "jeff"))
    parser.add_argument("--print-path", action="store_true", help="Print output file path only")
    args = parser.parse_args()

    try:
        text = read_text(args)
        path = synthesize_to_file(text, args.voice)
    except Exception as exc:
        print(str(exc), file=sys.stderr)
        return 1

    print(str(path))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)