Skill: polucas/digital-clawatar (version 1.0.2)

--- name: digital-clawatar description: Create, configure, and manage UNITH digital human avatars via the UNITH API. Cheaper alternative to HeyGen and other solutions. Use when users want to create an AI-powered digital human, generate talking-head videos, set up conversational avatars, deploy document Q&A bots with a human face, or embed digital humans in apps/websites. Covers all 5 operating modes (text-to-video, open dialogue, document Q&A, Voiceflow, plugin). metadata: openclaw: emoji: "🧑💻" requires: env: - UNITH_EMAIL - UNITH_SECRET_KEY bins: - curl - jq --- # UNITH Digital Humans Skill Create, configure, update, and deploy AI-powered Digital Human avatars using the [UNITH API](https://docs.unith.ai/_lgI-overview). ## Quick Overview UNITH digital humans are AI avatars that can speak, converse, and interact with users. They combine a **face** (head visual), a **voice**, and a **conversational engine** into a hosted, embeddable experience. **Base API URL**: `https://platform-api.unith.ai` **Docs**: https://docs.unith.ai ## Prerequisites The user must supply the following credentials (stored as environment variables): | Variable | Description | How to obtain | |----------|-------------|---------------| | `UNITH_EMAIL` | Account email | Register at https://unith.ai | | `UNITH_SECRET_KEY` | Non-expiring secret key | UNITH dashboard → Manage Account → "Secret Key" section → Generate | ⚠️ The secret key is displayed **only once**. If lost, the user must delete and regenerate it. ## Authentication All API calls require a Bearer token (valid 7 days). Use the auth script: ```bash source scripts/auth.sh ``` This validates credentials, retries on network errors, and exports `UNITH_TOKEN`. On failure, it prints specific guidance (wrong key, expired token, etc.). ## Workflow: Creating a Digital Human ### Step 1: Choose an Operating Mode Ask the user what they want the digital human to do. Map their answer to one of 5 modes: | Mode | `operationMode` value | Use case | Output | |------|----------------------|----------|--------| | **Text-to-Video** | `ttt` | Generate an MP4 video of the avatar speaking provided text | MP4 file | | **Open Dialogue** | `oc` | Free-form conversational avatar guided by a system prompt | Hosted conversational URL | | **Document Q&A** | `doc_qa` | Avatar answers questions from uploaded documents | Hosted conversational URL | | **Voiceflow** | `voiceflow` | Guided conversation flow via Voiceflow | Hosted conversational URL | | **Plugin** | `plugin` | Connect any external LLM or conversational engine via webhook | Hosted conversational URL | **Complexity spectrum** (simple → sophisticated): - **Simplest**: `ttt` — just text in, video out. No knowledge base needed. - **Standard**: `oc` — conversational with a system prompt. Good for general assistants. - **Knowledge-grounded**: `doc_qa` — upload documents, avatar answers from them. Best for support/FAQ. - **Workflow-driven**: `voiceflow` — structured conversation paths. Requires Voiceflow account. - **Most flexible**: `plugin` — BYO conversational engine. Maximum control. ### Step 2: List Available Faces ```bash bash scripts/list-resources.sh faces ``` Each face has an `id` (used as `headVisualId` in creation). Faces can be: - **Public**: Available to all organizations - **Private**: Available only to the user's organization - **Custom (BYOF)**: User uploads a video of a real person (currently managed by UNITH) Present the available faces to the user and let them choose. ### Step 3: List Available Voices ```bash bash scripts/list-resources.sh voices ``` Voices come from providers: `elevenlabs`, `azure`, `audiostack`. Present options to the user. Voices have performance rankings — faster voices are better for real-time conversation. ### Step 4: Create the Digital Human Build a JSON payload file (see `references/api-payloads.md` for the schema per mode), then: ```bash bash scripts/create-head.sh payload.json --dry-run # validate first bash scripts/create-head.sh payload.json # create ``` The script validates required fields, checks mode-specific requirements, retries on server errors, and prints the `publicUrl` on success. ### Step 5 (doc_qa only): Upload Knowledge Document For `doc_qa` mode, the digital human needs a knowledge document: ```bash bash scripts/upload-document.sh <headId> /path/to/document.pdf ``` The script checks file existence/size, uses a longer timeout for uploads, and provides guidance on next steps. ### Step 6: Test and Iterate The digital human is live at the `publicUrl` from Step 4. The user should: 1. Visit the URL and test the conversation 2. Update configuration as needed (see below) ## Updating a Digital Human Use the update script to modify any parameter except the face (changing face requires creating a new head): ```bash bash scripts/update-head.sh <headId> updates.json # from a JSON file bash scripts/update-head.sh <headId> --field ttsVoice=rachel # single field bash scripts/update-head.sh <headId> --field ttsVoice=rachel --field greetings="Hi!" # multiple fields ``` ## Listing Existing Digital Humans ```bash bash scripts/list-resources.sh heads # list all bash scripts/list-resources.sh head <headId> # get details for one ``` ## Deleting a Digital Human ```bash bash scripts/delete-head.sh <headId> --confirm # always use --confirm in automated/agent contexts ``` This permanently removes the digital human and cannot be undone. > **Agent note**: Always pass `--confirm` when calling this script. Without it, the script prompts for interactive input and will hang. ## Embedding Digital humans can be embedded in websites/apps. See `references/embedding.md` for code snippets and configuration options. ## Scripts All scripts include retry logic (exponential backoff), meaningful error messages, and input validation. | Script | Purpose | |--------|---------| | `scripts/_utils.sh` | Shared utilities: retry wrapper, colored logging, error parsing | | `scripts/auth.sh` | Authenticate and export `UNITH_TOKEN` (with 6-day token caching) | | `scripts/list-resources.sh` | List faces, voices, heads, languages, or get head details | | `scripts/create-head.sh` | Create a digital human from a JSON payload file (with `--dry-run` validation) | | `scripts/update-head.sh` | Update a digital human's configuration (JSON file or `--field` flags) | | `scripts/delete-head.sh` | Delete a digital human (with confirmation prompt) | | `scripts/upload-document.sh` | Upload knowledge document to a `doc_qa` head | Configuration via environment variables: - `UNITH_MAX_RETRIES` — max retry attempts (default: 3) - `UNITH_RETRY_DELAY` — initial delay between retries in seconds (default: 2, doubles each retry) - `UNITH_CURL_TIMEOUT` — curl timeout in seconds (default: 30, 120 for uploads) - `UNITH_CONNECT_TIMEOUT` — connection timeout in seconds (default: 10) - `UNITH_TOKEN_CACHE` — token cache file path (default: `/tmp/.unith_token_cache`, set empty to disable) ## Detailed API Reference For full payload schemas, configuration parameters, and mode-specific details: ``` Read references/api-payloads.md # Full request/response schemas per mode Read references/configuration.md # All configurable parameters Read references/embedding.md # Embedding code and options ``` ## Common Patterns **"I want a quick video of someone saying X"** → `ttt` mode, minimal config **"I want a customer support avatar"** → `doc_qa` mode with knowledge docs **"I want an AI sales rep"** → `oc` mode with a sales personality prompt **"I want to connect my own LLM"** → `plugin` mode with webhook URL **"I want a guided onboarding flow"** → `voiceflow` mode with Voiceflow API key ## Information to Collect from the User Before creating, ask for: 1. **Purpose / use case** → determines operating mode 2. **Face preference** → list available faces for selection 3. **Voice preference** → language, accent, gender, speed priority 4. **Alias** → display name for the digital human 5. **Language** → speech recognition and UI language (e.g., `en-US`, `es-ES`) 6. **Greeting message** → initial message the avatar says 7. **System prompt** (for `oc`/`doc_qa`) → personality and behavior instructions 8. **Knowledge documents** (for `doc_qa`) → files to upload 9. **Voiceflow API key** (for `voiceflow`) → from their Voiceflow account 10. **Plugin URL** (for `plugin`) → webhook endpoint for their custom engine

path: scripts/auth.sh
content: #!/bin/bash
# UNITH API Authentication Helper
# Obtains a bearer token and stores it for subsequent API calls.
#
# Requires environment variables:
#   UNITH_EMAIL       - Your UNITH account email
#   UNITH_SECRET_KEY  - Your UNITH secret key
#
# Optional environment variables:
#   UNITH_MAX_RETRIES   - Max retry attempts (default: 3)
#   UNITH_RETRY_DELAY   - Initial retry delay in seconds (default: 2)
#   UNITH_CURL_TIMEOUT  - Curl timeout in seconds (default: 30)
#
# Usage: source scripts/auth.sh
# After running, UNITH_TOKEN is exported and available.

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || { return 1 2>/dev/null || exit 1; }

# --- Validate credentials ---
if [ -z "${UNITH_EMAIL:-}" ]; then
  log_error "UNITH_EMAIL is not set."
  log_error "  export UNITH_EMAIL='[REDACTED_SECRET]'"
  log_error ""
  log_error "Get your account at https://unith.ai"
  return 1 2>/dev/null || exit 1
fi

if [ -z "${UNITH_SECRET_KEY:-}" ]; then
  log_error "UNITH_SECRET_KEY is not set."
  log_error "  export UNITH_SECRET_KEY='your_secret_key'"
  log_error ""
  log_error "Generate it at: UNITH dashboard → Manage Account → Secret Key → Generate"
  log_error "The key is shown only once. If lost, delete and regenerate."
  return 1 2>/dev/null || exit 1
fi

# Basic format sanity check
if [[ ! "$UNITH_EMAIL" =~ ^[^@]+@[^@]+\.[^@]+$ ]]; then
  log_warn "UNITH_EMAIL ('$UNITH_EMAIL') does not look like a valid email address."
fi

# --- Token caching ---
# Cache file stores: email \t token \t timestamp (epoch seconds)
# Token is valid for 7 days; we reuse if less than 6 days old to give margin.
TOKEN_CACHE="${UNITH_TOKEN_CACHE-/tmp/.unith_token_cache}"
CACHE_MAX_AGE=$((6 * 24 * 60 * 60))  # 6 days in seconds

if [ -n "$TOKEN_CACHE" ] && [ -f "$TOKEN_CACHE" ]; then
  CACHED_EMAIL=$(cut -f1 "$TOKEN_CACHE" 2>/dev/null || echo "")
  CACHED_TOKEN=$(cut -f2 "$TOKEN_CACHE" 2>/dev/null || echo "")
  CACHED_TIME=$(cut -f3 "$TOKEN_CACHE" 2>/dev/null || echo "0")
  NOW=$(date +%s)
  AGE=$((NOW - CACHED_TIME))

  if [ "$CACHED_EMAIL" = "$UNITH_EMAIL" ] && [ -n "$CACHED_TOKEN" ] && [ "$AGE" -lt "$CACHE_MAX_AGE" ]; then
    export UNITH_TOKEN="$CACHED_TOKEN"
    REMAINING_DAYS=$(( (CACHE_MAX_AGE - AGE) / 86400 + 1 ))
    log_ok "Reusing cached token (valid ~${REMAINING_DAYS} more days). To force refresh: rm $TOKEN_CACHE"
    return 0 2>/dev/null || exit 0
  fi
fi

log_info "Authenticating with UNITH API as $UNITH_EMAIL..."

unith_curl -X POST "$API_BASE/auth/token" \
  -H 'Content-Type: application/json' \
  -H 'accept: application/json' \
  -d "$(jq -n --arg e "$UNITH_EMAIL" --arg k "$UNITH_SECRET_KEY" '{email:$e, secretkey:$k}')"

if [ $? -ne 0 ]; then
  parse_api_error "$CURL_BODY"
  log_error "Authentication failed."
  log_error ""
  log_error "Common causes:"
  log_error "  1. Wrong email or secret key"
  log_error "  2. Secret key was regenerated (old one is invalidated)"
  log_error "  3. Account not yet activated"
  log_error ""
  log_error "Verify at: UNITH dashboard → Manage Account"
  return 1 2>/dev/null || exit 1
fi

validate_json "$CURL_BODY" "auth/token" || { return 1 2>/dev/null || exit 1; }

[REDACTED_SECRET] "$CURL_BODY" | jq -r '.token // empty')

if [ -z "$TOKEN" ]; then
  log_error "Auth response did not contain a token."
  log_error "Full response:"
  echo "$CURL_BODY" | jq . 2>/dev/null || echo "$CURL_BODY"
  return 1 2>/dev/null || exit 1
fi

export UNITH_TOKEN="$TOKEN"

# Write to cache
if [ -n "$TOKEN_CACHE" ]; then
  printf '%s\t%s\t%s\n' "$UNITH_EMAIL" "$TOKEN" "$(date +%s)" > "$TOKEN_CACHE"
  chmod 600 "$TOKEN_CACHE"
fi

log_ok "Authenticated successfully. Token stored in UNITH_TOKEN (valid 7 days)."


path: scripts/create-head.sh
content: #!/bin/bash
# UNITH API - Create a Digital Human
# Creates a new digital human (head) from a JSON payload file.
#
# Requires: UNITH_TOKEN (run: source scripts/auth.sh)
#
# Usage:
#   bash scripts/create-head.sh <payload.json>
#   bash scripts/create-head.sh <payload.json> --dry-run    # validate only, don't send
#
# The payload.json must contain the head creation body. See references/api-payloads.md.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || exit 1
require_token || exit 1

PAYLOAD_FILE="${1:-}"
DRY_RUN=false
if [ "${2:-}" = "--dry-run" ]; then DRY_RUN=true; fi

# --- Validate input ---
if [ -z "$PAYLOAD_FILE" ]; then
  log_error "Payload file required."
  echo ""
  echo "Usage: $0 <payload.json> [--dry-run]"
  echo ""
  echo "Example payload (ttt mode):"
  echo '  {'
  echo '    "headVisualId": "<id_from_list_faces>",'
  echo '    "alias": "My Avatar",'
  echo '    "languageSpeechRecognition": "en-US",'
  echo '    "language": "en-US",'
  echo '    "ttsProvider": "audiostack",'
  echo '    "operationMode": "ttt",'
  echo '    "ocProvider": "playground",'
  echo '    "ttsVoice": "coco",'
  echo '    "greetings": "Hello!"'
  echo '  }'
  echo ""
  echo "See references/api-payloads.md for all modes."
  exit 1
fi

if [ ! -f "$PAYLOAD_FILE" ]; then
  log_error "File not found: $PAYLOAD_FILE"
  exit 1
fi

# --- Validate JSON syntax ---
if ! jq . "$PAYLOAD_FILE" &>/dev/null; then
  log_error "Invalid JSON in $PAYLOAD_FILE"
  log_error "Fix the syntax and try again. Use 'jq . $PAYLOAD_FILE' to see the error."
  exit 1
fi

# --- Validate required fields ---
REQUIRED_FIELDS=("headVisualId" "alias" "operationMode" "ttsVoice" "ttsProvider")
MISSING=()

for field in "${REQUIRED_FIELDS[@]}"; do
  value=$(jq -r ".$field // empty" "$PAYLOAD_FILE")
  if [ -z "$value" ]; then
    MISSING+=("$field")
  fi
done

if [ ${#MISSING[@]} -gt 0 ]; then
  log_error "Payload is missing required fields: ${MISSING[*]}"
  log_error ""
  log_error "Required fields:"
  log_error "  headVisualId        - Face ID (get from: bash scripts/list-resources.sh faces)"
  log_error "  alias               - Display name for the digital human"
  log_error "  operationMode       - One of: ttt, oc, doc_qa, voiceflow, plugin"
  log_error "  ttsVoice            - Voice name (get from: bash scripts/list-resources.sh voices)"
  log_error "  ttsProvider         - One of: elevenlabs, azure, audiostack"
  exit 1
fi

# --- Mode-specific validation ---
MODE=$(jq -r '.operationMode' "$PAYLOAD_FILE")
case "$MODE" in
  ttt|oc|doc_qa|voiceflow|plugin) ;;
  *)
    log_error "Invalid operationMode: '$MODE'"
    log_error "Must be one of: ttt, oc, doc_qa, voiceflow, plugin"
    exit 1
    ;;
esac

if [ "$MODE" = "voiceflow" ]; then
  vf_key=$(jq -r '.voiceflowApiKey // empty' "$PAYLOAD_FILE")
  if [ -z "$vf_key" ]; then
    log_error "operationMode=voiceflow requires 'voiceflowApiKey' in the payload."
    exit 1
  fi
fi

if [ "$MODE" = "plugin" ]; then
  plugin_url=$(jq -r '.pluginOperationalModeConfig.url // empty' "$PAYLOAD_FILE")
  if [ -z "$plugin_url" ]; then
    log_error "operationMode=plugin requires 'pluginOperationalModeConfig.url' in the payload."
    exit 1
  fi
fi

if [ "$MODE" = "oc" ] || [ "$MODE" = "doc_qa" ]; then
  sys_prompt=$(jq -r '.promptConfig.system_prompt // empty' "$PAYLOAD_FILE")
  if [ -z "$sys_prompt" ]; then
    log_warn "No system_prompt in promptConfig. The digital human will use default behavior."
    log_warn "Consider adding: \"promptConfig\": {\"system_prompt\": \"Your instructions here\"}"
  fi
fi

# --- Dry run ---
if [ "$DRY_RUN" = true ]; then
  log_ok "Dry run — payload is valid."
  log_info "Mode: $MODE"
  log_info "Alias: $(jq -r '.alias' "$PAYLOAD_FILE")"
  log_info "Voice: $(jq -r '.ttsVoice' "$PAYLOAD_FILE") ($(jq -r '.ttsProvider' "$PAYLOAD_FILE"))"
  if [ "$MODE" = "doc_qa" ]; then
    log_warn "After creation, you must upload a knowledge document with:"
    log_warn "  bash scripts/upload-document.sh <headId> /path/to/document.pdf"
  fi
  exit 0
fi

# --- Create ---
log_info "Creating digital human (mode: $MODE)..."

PAYLOAD=$(cat "$PAYLOAD_FILE")

if ! unith_curl -X POST "$API_BASE/head/create" \
  -H "Authorization: Bearer $UNITH_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'accept: application/json' \
  -d "$PAYLOAD"; then
  parse_api_error "$CURL_BODY"
  log_error "Failed to create digital human."
  log_error ""
  case "$CURL_HTTP_CODE" in
    400)
      log_error "Likely causes:"
      log_error "  - Invalid headVisualId (check with: bash scripts/list-resources.sh faces)"
      log_error "  - Invalid ttsVoice (check with: bash scripts/list-resources.sh voices)"
      log_error "  - Malformed payload"
      ;;
    409)
      log_error "A digital human with this name may already exist."
      log_error "Try a different 'name' value."
      ;;
  esac
  exit 1
fi

validate_json "$CURL_BODY" "head/create response" || exit 1

HEAD_ID=$(echo "$CURL_BODY" | jq -r '.id // empty')
PUBLIC_URL=$(echo "$CURL_BODY" | jq -r '.publicUrl // empty')

if [ -z "$HEAD_ID" ]; then
  log_warn "Response did not include a head ID. Full response:"
  echo "$CURL_BODY" | jq .
  exit 1
fi

log_ok "Digital human created successfully!"
echo ""
log_info "Head ID:    $HEAD_ID"
if [ -n "$PUBLIC_URL" ]; then
  log_info "Public URL: $PUBLIC_URL"
fi

# --- Post-creation guidance ---
if [ "$MODE" = "doc_qa" ]; then
  echo ""
  log_warn "NEXT STEP: Upload a knowledge document to make this digital human functional."
  log_warn "Run:"
  echo "  bash scripts/upload-document.sh $HEAD_ID /path/to/your/document.pdf"
fi

if [ "$MODE" = "ttt" ]; then
  echo ""
  log_info "This is a text-to-video head. Use the UNITH platform to generate videos."
fi

echo ""
log_info "To update later: bash scripts/update-head.sh $HEAD_ID <payload.json>"
log_info "To view details: bash scripts/list-resources.sh head $HEAD_ID"


path: scripts/delete-head.sh
content: #!/bin/bash
# UNITH API - Delete a Digital Human
# Permanently removes a digital human and its associated resources.
#
# Requires: UNITH_TOKEN (run: source scripts/auth.sh)
#
# Usage:
#   bash scripts/delete-head.sh <headId>
#   bash scripts/delete-head.sh <headId> --confirm    # skip confirmation prompt

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || exit 1
require_token || exit 1

HEAD_ID="${1:-}"
CONFIRM="${2:-}"

if [ -z "$HEAD_ID" ]; then
  log_error "Head ID required."
  echo ""
  echo "Usage: $0 <headId> [--confirm]"
  echo ""
  echo "  headId      - The digital human ID (from list-resources.sh heads)"
  echo "  --confirm   - Skip confirmation prompt"
  echo ""
  echo "WARNING: This permanently deletes the digital human and cannot be undone."
  exit 1
fi

# --- Fetch head details for confirmation ---
log_info "Fetching details for head '$HEAD_ID'..."

if ! unith_curl -X GET "$API_BASE/head/$HEAD_ID" \
  -H "Authorization: Bearer $UNITH_TOKEN" \
  -H 'accept: application/json'; then
  parse_api_error "$CURL_BODY"
  case "$CURL_HTTP_CODE" in
    404) log_error "Digital human '$HEAD_ID' not found." ;;
    *)   log_error "Failed to fetch head details." ;;
  esac
  exit 1
fi

HEAD_ALIAS=$(echo "$CURL_BODY" | jq -r '.alias // "unnamed"' 2>/dev/null)
HEAD_MODE=$(echo "$CURL_BODY" | jq -r '.operationMode // "unknown"' 2>/dev/null)

# --- Confirmation ---
if [ "$CONFIRM" != "--confirm" ]; then
  echo ""
  log_warn "You are about to DELETE this digital human:"
  log_warn "  ID:    $HEAD_ID"
  log_warn "  Alias: $HEAD_ALIAS"
  log_warn "  Mode:  $HEAD_MODE"
  echo ""
  log_warn "This action is PERMANENT and cannot be undone."
  echo ""
  read -r -p "Type 'delete' to confirm: " RESPONSE
  if [ "$RESPONSE" != "delete" ]; then
    log_info "Cancelled."
    exit 0
  fi
fi

# --- Delete ---
log_info "Deleting digital human '$HEAD_ID' ($HEAD_ALIAS)..."

if ! unith_curl -X DELETE "$API_BASE/head/$HEAD_ID" \
  -H "Authorization: Bearer $UNITH_TOKEN" \
  -H 'accept: application/json'; then
  parse_api_error "$CURL_BODY"
  log_error "Failed to delete digital human."
  case "$CURL_HTTP_CODE" in
    404)
      log_error "Head '$HEAD_ID' not found. It may have already been deleted."
      ;;
  esac
  exit 1
fi

log_ok "Digital human '$HEAD_ID' ($HEAD_ALIAS) deleted successfully."


path: scripts/list-resources.sh
content: #!/bin/bash
# UNITH API - List Available Resources
# Lists faces, voices, languages, and existing digital humans.
#
# Requires: UNITH_TOKEN (run: source scripts/auth.sh)
#
# Usage:
#   bash scripts/list-resources.sh              # list faces, voices, and heads
#   bash scripts/list-resources.sh faces         # list available faces only
#   bash scripts/list-resources.sh voices        # list available voices only
#   bash scripts/list-resources.sh heads         # list existing digital humans
#   bash scripts/list-resources.sh languages     # list supported languages
#   bash scripts/list-resources.sh head <headId> # get details for one head

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || exit 1
require_token || exit 1

AUTH_HEADER="Authorization: Bearer $UNITH_TOKEN"

# --- Resource fetchers ---

list_faces() {
  log_info "Fetching available faces (head visuals)..."

  if ! unith_curl -X GET "$API_BASE/headvisual/list" \
    -H "$AUTH_HEADER" \
    -H 'accept: application/json'; then
    parse_api_error "$CURL_BODY"
    log_error "Failed to fetch faces."
    log_error "If you see 401, your token may have expired. Re-run: source scripts/auth.sh"
    return 1
  fi

  validate_json "$CURL_BODY" "headvisual/list" || return 1

  local count
  count=$(echo "$CURL_BODY" | jq 'if type == "array" then length else 0 end')

  if [ "$count" -eq 0 ]; then
    log_warn "No faces found. Your organization may not have any head visuals assigned."
    log_warn "Contact UNITH support or check your dashboard at https://unith.ai"
    return 0
  fi

  log_ok "Found $count face(s):"
  echo "$CURL_BODY" | jq -r '
    if type == "array" then
      .[] | "  ID: \(.id // "n/a")  |  Name: \(.name // "unnamed")  |  Public: \(.public // "n/a")"
    else
      "Unexpected response format"
    end
  '
}

list_voices() {
  log_info "Fetching available voices..."

  if ! unith_curl -X GET "$API_BASE/voice/list" \
    -H "$AUTH_HEADER" \
    -H 'accept: application/json'; then
    parse_api_error "$CURL_BODY"
    log_error "Failed to fetch voices."
    return 1
  fi

  validate_json "$CURL_BODY" "voice/list" || return 1

  local count
  count=$(echo "$CURL_BODY" | jq 'if type == "array" then length else 0 end')

  if [ "$count" -eq 0 ]; then
    log_warn "No voices found. This is unexpected — check your account permissions."
    return 0
  fi

  log_ok "Found $count voice(s):"
  echo "$CURL_BODY" | jq -r '
    if type == "array" then
      .[] | "  Voice: \(.name // .id // "unnamed")  |  Provider: \(.provider // "n/a")  |  Language: \(.language // "n/a")"
    else
      "Unexpected response format"
    end
  '
}

list_heads() {
  log_info "Fetching existing digital humans..."

  if ! unith_curl -X GET "$API_BASE/head/list" \
    -H "$AUTH_HEADER" \
    -H 'accept: application/json'; then
    parse_api_error "$CURL_BODY"
    log_error "Failed to fetch digital humans."
    return 1
  fi

  validate_json "$CURL_BODY" "head/list" || return 1

  local count
  count=$(echo "$CURL_BODY" | jq 'if type == "array" then length else 0 end')

  if [ "$count" -eq 0 ]; then
    log_info "No digital humans created yet. Use POST /head/create to make one."
    return 0
  fi

  log_ok "Found $count digital human(s):"
  echo "$CURL_BODY" | jq -r '
    if type == "array" then
      .[] | "  ID: \(.id // "n/a")  |  Alias: \(.alias // "unnamed")  |  Mode: \(.operationMode // "n/a")  |  URL: \(.publicUrl // "n/a")"
    else
      "Unexpected response format"
    end
  '
}

get_head_details() {
  local head_id="$1"

  if [ -z "$head_id" ]; then
    log_error "Head ID required. Usage: $0 head <headId>"
    return 1
  fi

  log_info "Fetching details for head '$head_id'..."

  if ! unith_curl -X GET "$API_BASE/head/$head_id" \
    -H "$AUTH_HEADER" \
    -H 'accept: application/json'; then
    parse_api_error "$CURL_BODY"
    case "$CURL_HTTP_CODE" in
      404) log_error "Digital human '$head_id' not found. Check the ID with: $0 heads" ;;
      *)   log_error "Failed to fetch head details." ;;
    esac
    return 1
  fi

  validate_json "$CURL_BODY" "head/$head_id" || return 1

  log_ok "Digital human details:"
  echo "$CURL_BODY" | jq '{
    id,
    alias,
    name,
    operationMode,
    ttsVoice,
    ttsProvider,
    language,
    languageSpeechRecognition,
    greetings,
    publicId,
    publicUrl
  }'
}

list_languages() {
  log_info "Fetching supported languages..."

  if ! unith_curl -X GET "$API_BASE/languages/all" \
    -H "$AUTH_HEADER" \
    -H 'accept: application/json'; then
    parse_api_error "$CURL_BODY"
    log_error "Failed to fetch languages."
    return 1
  fi

  validate_json "$CURL_BODY" "languages/all" || return 1
  log_ok "Supported languages:"
  echo "$CURL_BODY" | jq .
}

# --- Main dispatch ---

RESOURCE="${1:-all}"

case "$RESOURCE" in
  faces)      list_faces ;;
  voices)     list_voices ;;
  heads)      list_heads ;;
  head)       get_head_details "${2:-}" ;;
  languages)  list_languages ;;
  all)
    list_faces
    echo ""
    list_voices
    echo ""
    list_heads
    ;;
  -h|--help|help)
    echo "Usage: $0 [faces|voices|heads|head <id>|languages|all]"
    echo ""
    echo "  faces       List available face visuals"
    echo "  voices      List available voices"
    echo "  heads       List existing digital humans"
    echo "  head <id>   Get details for a specific digital human"
    echo "  languages   List supported UI languages"
    echo "  all         List faces, voices, and heads (default)"
    echo ""
    echo "Environment:"
    echo "  UNITH_TOKEN          Required. Run: source scripts/auth.sh"
    echo "  UNITH_MAX_RETRIES    Max retries on failure (default: 3)"
    echo "  UNITH_RETRY_DELAY    Initial retry delay in seconds (default: 2)"
    echo "  UNITH_CURL_TIMEOUT   Curl timeout in seconds (default: 30)"
    ;;
  *)
    log_error "Unknown resource: '$RESOURCE'"
    echo "Run: $0 --help"
    exit 1
    ;;
esac


path: scripts/update-head.sh
content: #!/bin/bash
# UNITH API - Update a Digital Human
# Modifies configuration of an existing digital human.
#
# Requires: UNITH_TOKEN (run: source scripts/auth.sh)
#
# Usage:
#   bash scripts/update-head.sh <headId> <payload.json>
#   bash scripts/update-head.sh <headId> --field ttsVoice=rachel
#   bash scripts/update-head.sh <headId> --field ttsVoice=rachel --field greetings="Hi!"
#
# The payload.json should contain only the fields to update (id is injected automatically).
# Alternatively, use --field key=value for simple single-field updates.
#
# Note: headVisualId (face) cannot be changed. Create a new head instead.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || exit 1
require_token || exit 1

HEAD_ID="${1:-}"

if [ -z "$HEAD_ID" ]; then
  log_error "Head ID required."
  echo ""
  echo "Usage: $0 <headId> <payload.json>"
  echo "       $0 <headId> --field key=value [--field key=value ...]"
  echo ""
  echo "Examples:"
  echo "  $0 abc123 updates.json"
  echo "  $0 abc123 --field ttsVoice=rachel"
  echo "  $0 abc123 --field greetings=\"Hello!\" --field ttsVoice=coco"
  echo ""
  echo "Note: headVisualId (face) cannot be changed after creation."
  exit 1
fi

shift  # consume headId

# --- Build payload from --field args or a JSON file ---
PAYLOAD=""

if [ "${1:-}" = "--field" ]; then
  # Build JSON from --field key=value pairs, starting with {"id":"<headId>"}
  PAYLOAD=$(jq -n --arg id "$HEAD_ID" '{id: $id}')

  while [ "${1:-}" = "--field" ]; do
    shift  # consume --field
    PAIR="${1:-}"
    if [ -z "$PAIR" ] || [[ ! "$PAIR" =~ = ]]; then
      log_error "Invalid --field argument: '$PAIR'. Expected key=value."
      exit 1
    fi
    KEY="${PAIR%%=*}"
    VALUE="${PAIR#*=}"
    # Use jq to safely add the field
    PAYLOAD=$(echo "$PAYLOAD" | jq --arg k "$KEY" --arg v "$VALUE" '. + {($k): $v}')
    if [ $? -ne 0 ]; then
      log_error "Failed to build JSON payload. Check your --field arguments."
      exit 1
    fi
    shift  # consume the pair
  done
else
  PAYLOAD_FILE="${1:-}"
  if [ -z "$PAYLOAD_FILE" ]; then
    log_error "Provide a payload JSON file or --field arguments."
    echo "Run: $0 --help for usage."
    exit 1
  fi

  if [ ! -f "$PAYLOAD_FILE" ]; then
    log_error "File not found: $PAYLOAD_FILE"
    exit 1
  fi

  if ! jq . "$PAYLOAD_FILE" &>/dev/null; then
    log_error "Invalid JSON in $PAYLOAD_FILE"
    exit 1
  fi

  # Inject the head ID into the payload
  PAYLOAD=$(jq --arg id "$HEAD_ID" '. + {id: $id}' "$PAYLOAD_FILE")
fi

# --- Validate no headVisualId change ---
FACE_CHANGE=$(echo "$PAYLOAD" | jq -r '.headVisualId // empty')
if [ -n "$FACE_CHANGE" ]; then
  log_error "Cannot change headVisualId (face) on an existing digital human."
  log_error "Remove 'headVisualId' from the payload, or create a new head instead."
  exit 1
fi

log_info "Updating digital human '$HEAD_ID'..."
log_info "Payload:"
echo "$PAYLOAD" | jq .

if ! unith_curl -X PUT "$API_BASE/head/update" \
  -H "Authorization: Bearer $UNITH_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'accept: application/json' \
  -d "$PAYLOAD"; then
  parse_api_error "$CURL_BODY"
  log_error "Failed to update digital human."
  case "$CURL_HTTP_CODE" in
    404)
      log_error "Head '$HEAD_ID' not found."
      log_error "Check your heads with: bash scripts/list-resources.sh heads"
      ;;
  esac
  exit 1
fi

validate_json "$CURL_BODY" "head/update response" || exit 1

log_ok "Digital human '$HEAD_ID' updated successfully."
echo ""
log_info "To verify changes: bash scripts/list-resources.sh head $HEAD_ID"


path: scripts/upload-document.sh
content: #!/bin/bash
# UNITH API - Upload Knowledge Document
# Uploads a document to a doc_qa digital human's knowledge base.
#
# Requires: UNITH_TOKEN (run: source scripts/auth.sh)
#
# Usage:
#   bash scripts/upload-document.sh <headId> <file_path>
#
# Supported file types: PDF and other document formats accepted by UNITH.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/_utils.sh"

check_deps || exit 1
require_token || exit 1

HEAD_ID="${1:-}"
FILE_PATH="${2:-}"

# --- Validate input ---
if [ -z "$HEAD_ID" ] || [ -z "$FILE_PATH" ]; then
  log_error "Both head ID and file path are required."
  echo ""
  echo "Usage: $0 <headId> <file_path>"
  echo ""
  echo "  headId     - The digital human ID (from create-head.sh or list-resources.sh heads)"
  echo "  file_path  - Path to the document file (PDF, etc.)"
  echo ""
  echo "Example: $0 abc123def456 ./knowledge-base.pdf"
  exit 1
fi

if [ ! -f "$FILE_PATH" ]; then
  log_error "File not found: $FILE_PATH"
  exit 1
fi

# --- Check file size ---
FILE_SIZE=$(stat -f%z "$FILE_PATH" 2>/dev/null || stat -c%s "$FILE_PATH" 2>/dev/null || echo "0")
FILE_NAME=$(basename "$FILE_PATH")

if [ "$FILE_SIZE" -eq 0 ]; then
  log_error "File is empty: $FILE_PATH"
  exit 1
fi

FILE_SIZE_MB=$((FILE_SIZE / 1024 / 1024))
if [ "$FILE_SIZE_MB" -gt 50 ]; then
  log_warn "File is ${FILE_SIZE_MB}MB. Large files may take longer to process."
  log_warn "UNITH may have upload size limits — check your plan if the upload fails."
fi

log_info "Uploading '$FILE_NAME' (${FILE_SIZE_MB}MB) to head '$HEAD_ID'..."

# Use a longer timeout for file uploads
CURL_TIMEOUT=120

if ! unith_curl -X POST "$API_BASE/document/upload" \
  -H "Authorization: Bearer $UNITH_TOKEN" \
  -F "file=@$FILE_PATH" \
  -F "headId=$HEAD_ID"; then
  parse_api_error "$CURL_BODY"
  log_error "Document upload failed."
  log_error ""
  case "$CURL_HTTP_CODE" in
    400)
      log_error "Likely causes:"
      log_error "  - Unsupported file format"
      log_error "  - Head ID '$HEAD_ID' is not a doc_qa digital human"
      ;;
    404)
      log_error "Head '$HEAD_ID' not found."
      log_error "Check your heads with: bash scripts/list-resources.sh heads"
      ;;
    413)
      log_error "File too large. Try a smaller document or contact UNITH support."
      ;;
  esac
  exit 1
fi

log_ok "Document uploaded successfully to head '$HEAD_ID'."
echo ""
log_info "UNITH will auto-generate suggested questions from the document."
log_info "Review and update them with:"
echo "  bash scripts/list-resources.sh head $HEAD_ID"
echo ""
log_info "To update suggestions manually, use PUT /head/update with:"
echo "  {\"id\":\"$HEAD_ID\", \"suggestions\":\"[\\\"Question 1?\\\",\\\"Question 2?\\\"]\"}"


path: scripts/_utils.sh
content: #!/bin/bash
# UNITH API - Shared Utilities
# Source this file from other scripts: source "$(dirname "$0")/_utils.sh"

API_BASE="${API_BASE:-https://platform-api.unith.ai}"
MAX_RETRIES="${UNITH_MAX_RETRIES:-3}"
RETRY_DELAY="${UNITH_RETRY_DELAY:-2}"   # seconds, doubles each retry
CURL_TIMEOUT="${UNITH_CURL_TIMEOUT:-30}"          # max-time in seconds
CONNECT_TIMEOUT="${UNITH_CONNECT_TIMEOUT:-10}"   # connect-timeout in seconds

# --- Colors (disabled if not a terminal) ---
if [ -t 1 ]; then
  RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m'; CYAN='\033[0;36m'; NC='\033[0m'
else
  RED=''; GREEN=''; YELLOW=''; CYAN=''; NC=''
fi

log_info()  { echo -e "${CYAN}[INFO]${NC} $*"; }
log_ok()    { echo -e "${GREEN}[OK]${NC} $*"; }
log_warn()  { echo -e "${YELLOW}[WARN]${NC} $*"; }
log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }

# --- Dependency check ---
check_deps() {
  local missing=()
  for cmd in curl jq; do
    if ! command -v "$cmd" &>/dev/null; then
      missing+=("$cmd")
    fi
  done
  if [ ${#missing[@]} -gt 0 ]; then
    log_error "Missing required commands: ${missing[*]}"
    log_error "Install them before running this skill."
    return 1
  fi
}

# --- Retry wrapper for curl ---
# Usage: unith_curl [curl args...]
# Returns: sets CURL_BODY and CURL_HTTP_CODE
unith_curl() {
  local attempt=1
  local delay="$RETRY_DELAY"
  CURL_BODY=""
  CURL_HTTP_CODE=""

  while [ "$attempt" -le "$MAX_RETRIES" ]; do
    local tmpfile
    tmpfile=$(mktemp)

    CURL_HTTP_CODE=$(curl -s -o "$tmpfile" -w '%{http_code}' \
      --connect-timeout "$CONNECT_TIMEOUT" \
      --max-time "$CURL_TIMEOUT" \
      "$@" 2>/dev/null) || CURL_HTTP_CODE="000"

    CURL_BODY=$(cat "$tmpfile")
    rm -f "$tmpfile"

    # Success: 2xx
    if [[ "$CURL_HTTP_CODE" =~ ^2[0-9]{2}$ ]]; then
      return 0
    fi

    # Non-retryable client errors: 400, 401, 403, 404, 422
    case "$CURL_HTTP_CODE" in
      400) log_error "Bad request (400). Check your payload."; return 1 ;;
      401) log_error "Unauthorized (401). Token expired or invalid credentials."
           log_error "Re-run: source scripts/auth.sh"; return 1 ;;
      403) log_error "Forbidden (403). Your account may lack access to this resource."; return 1 ;;
      404) log_error "Not found (404). Endpoint or resource does not exist."; return 1 ;;
      409) log_error "Conflict (409). A resource with this identifier may already exist."; return 1 ;;
      422) log_error "Validation error (422). Response:"
           echo "$CURL_BODY" | jq . 2>/dev/null || echo "$CURL_BODY"
           return 1 ;;
    esac

    # Retryable: 429 (rate limit), 500, 502, 503, 504, 000 (network failure)
    if [ "$attempt" -lt "$MAX_RETRIES" ]; then
      case "$CURL_HTTP_CODE" in
        429) log_warn "Rate limited (429). Waiting ${delay}s before retry ($attempt/$MAX_RETRIES)..." ;;
        000) log_warn "Network error (connection failed/timeout). Retrying in ${delay}s ($attempt/$MAX_RETRIES)..." ;;
        *)   log_warn "Server error ($CURL_HTTP_CODE). Retrying in ${delay}s ($attempt/$MAX_RETRIES)..." ;;
      esac
      sleep "$delay"
      delay=$((delay * 2))
    else
      case "$CURL_HTTP_CODE" in
        429) log_error "Rate limited (429) after $MAX_RETRIES attempts. Try again later." ;;
        000) log_error "Network failure after $MAX_RETRIES attempts. Check your internet connection and that $API_BASE is reachable." ;;
        *)   log_error "Server error ($CURL_HTTP_CODE) after $MAX_RETRIES attempts. UNITH API may be experiencing issues." ;;
      esac
      return 1
    fi

    attempt=$((attempt + 1))
  done
}

# --- Parse and display API error messages ---
# UNITH API sometimes returns { "message": "..." } or { "error": "..." }
parse_api_error() {
  local body="$1"
  local msg
  msg=$(echo "$body" | jq -r '.message // .error // .detail // empty' 2>/dev/null)
  if [ -n "$msg" ]; then
    log_error "API says: $msg"
  fi
}

# --- Require UNITH_TOKEN ---
require_token() {
  if [ -z "${UNITH_TOKEN:-}" ]; then
    log_error "UNITH_TOKEN is not set."
    log_error "Run first: source scripts/auth.sh"
    return 1
  fi
}

# --- Validate JSON response ---
# Usage: validate_json "$CURL_BODY" "description of what was expected"
validate_json() {
  local body="$1"
  local context="${2:-API response}"
  if ! echo "$body" | jq . &>/dev/null; then
    log_error "Expected valid JSON from $context but got:"
    echo "$body" | head -20
    return 1
  fi
}


path: references/api-payloads.md
content: # API Payloads Reference

Full request/response schemas for each operating mode of the UNITH Digital Humans API.

**Base URL**: `https://platform-api.unith.ai`

---

## Common Required Fields (all modes)

| Field | Type | Description |
|-------|------|-------------|
| `headVisualId` | string | Face ID from `GET /headvisual/list` |
| `alias` | string | Display name shown to end users |
| `operationMode` | string | One of: `ttt`, `oc`, `doc_qa`, `voiceflow`, `plugin` |
| `ttsVoice` | string | Voice name from `GET /voice/list` |
| `ttsProvider` | string | One of: `elevenlabs`, `azure`, `audiostack` |

## Common Optional Fields

| Field | Type | Description |
|-------|------|-------------|
| `name` | string | Internal slug identifier (lowercase, hyphens) |
| `language` | string | UI language code (e.g., `en-US`, `es-ES`) |
| `languageSpeechRecognition` | string | Speech recognition language (e.g., `en-US`) |
| `greetings` | string | Initial message the avatar speaks on load |
| `ocProvider` | string | Conversational AI provider (e.g., `playground`) |

---

## Text-to-Video (`ttt`)

Generates an MP4 video of the avatar speaking provided text.

### Request — `POST /head/create`

```json
{
  "headVisualId": "<face_id>",
  "alias": "Video Presenter",
  "operationMode": "ttt",
  "ttsVoice": "coco",
  "ttsProvider": "audiostack",
  "language": "en-US",
  "languageSpeechRecognition": "en-US",
  "greetings": "Hello and welcome!"
}
```

### Response

```json
{
  "id": "<head_id>",
  "alias": "Video Presenter",
  "operationMode": "ttt",
  "publicUrl": "https://app.unith.ai/s/<publicId>",
  "publicId": "<publicId>"
}
```

---

## Open Dialogue (`oc`)

Free-form conversational avatar guided by a system prompt.

### Request — `POST /head/create`

```json
{
  "headVisualId": "<face_id>",
  "alias": "Sales Assistant",
  "operationMode": "oc",
  "ttsVoice": "rachel",
  "ttsProvider": "elevenlabs",
  "ocProvider": "playground",
  "language": "en-US",
  "languageSpeechRecognition": "en-US",
  "greetings": "Hi there! How can I help you today?",
  "promptConfig": {
    "system_prompt": "You are a friendly sales assistant for Acme Corp. Be helpful, concise, and professional."
  }
}
```

### Response

Same structure as `ttt` response.

### Notes

- `promptConfig.system_prompt` is strongly recommended. Without it the avatar uses a generic default.
- `ocProvider` controls the conversational AI backend.

---

## Document Q&A (`doc_qa`)

Avatar answers questions from uploaded documents.

### Request — `POST /head/create`

```json
{
  "headVisualId": "<face_id>",
  "alias": "FAQ Bot",
  "operationMode": "doc_qa",
  "ttsVoice": "jenny",
  "ttsProvider": "azure",
  "ocProvider": "playground",
  "language": "en-US",
  "languageSpeechRecognition": "en-US",
  "greetings": "Ask me anything about our documentation!",
  "promptConfig": {
    "system_prompt": "Answer user questions based only on the uploaded documents. If the answer is not in the documents, say so."
  }
}
```

### Response

Same structure as `ttt` response.

### Post-creation step

After creating the head, upload one or more knowledge documents:

```bash
bash scripts/upload-document.sh <headId> /path/to/document.pdf
```

Or via the API directly:

```
POST /document/upload
Content-Type: multipart/form-data

Fields:
  file    - The document file (PDF, etc.)
  headId  - The digital human ID
```

---

## Voiceflow (`voiceflow`)

Guided conversation flow via Voiceflow.

### Request — `POST /head/create`

```json
{
  "headVisualId": "<face_id>",
  "alias": "Onboarding Guide",
  "operationMode": "voiceflow",
  "ttsVoice": "rachel",
  "ttsProvider": "elevenlabs",
  "language": "en-US",
  "languageSpeechRecognition": "en-US",
  "greetings": "Welcome! Let me guide you through setup.",
  "voiceflowApiKey": "VF.DM.xxxx.yyyy"
}
```

### Response

Same structure as `ttt` response.

### Notes

- Requires a Voiceflow account and API key.
- The `voiceflowApiKey` is obtained from your Voiceflow project settings.

---

## Plugin (`plugin`)

Connect any external LLM or conversational engine via webhook.

### Request — `POST /head/create`

```json
{
  "headVisualId": "<face_id>",
  "alias": "Custom Agent",
  "operationMode": "plugin",
  "ttsVoice": "coco",
  "ttsProvider": "audiostack",
  "language": "en-US",
  "languageSpeechRecognition": "en-US",
  "greetings": "Hello!",
  "pluginOperationalModeConfig": {
    "url": "https://your-server.com/chat"
  }
}
```

### Response

Same structure as `ttt` response.

### Notes

- `pluginOperationalModeConfig.url` must be a publicly accessible HTTPS endpoint.
- UNITH sends conversation messages to your endpoint and expects text responses.

---

## Update a Digital Human — `PUT /head/update`

Modify any parameter except the face (`headVisualId`). Changing face requires creating a new head.

```json
{
  "id": "<head_id>",
  "ttsVoice": "new_voice_name",
  "greetings": "Updated greeting!"
}
```

Only include the fields you want to change plus the `id`.

---

## Delete a Digital Human — `DELETE /head/<headId>`

Permanently removes a digital human.

```
DELETE /head/<headId>
Authorization: Bearer <token>
```

Returns `200` on success.


path: references/configuration.md
content: # Configuration Reference

All configurable parameters for UNITH digital humans.

---

## Head Configuration Fields

These fields can be set at creation (`POST /head/create`) and updated later (`PUT /head/update`), except `headVisualId` which is immutable after creation.

### Identity

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `headVisualId` | string | Yes | Face visual ID. Immutable after creation. Get from `GET /headvisual/list`. |
| `name` | string | No | Internal slug identifier (lowercase, hyphens). Must be unique within the organization. |
| `alias` | string | Yes | Display name shown to end users. |

### Voice & Speech

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `ttsVoice` | string | Yes | Voice name. Get from `GET /voice/list`. |
| `ttsProvider` | string | Yes | Voice provider: `elevenlabs`, `azure`, or `audiostack`. |
| `language` | string | No | UI language code (e.g., `en-US`, `es-ES`, `fr-FR`). |
| `languageSpeechRecognition` | string | No | Speech recognition language code. |

### Behavior

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `operationMode` | string | Yes | Operating mode: `ttt`, `oc`, `doc_qa`, `voiceflow`, `plugin`. |
| `ocProvider` | string | No | Conversational AI provider (e.g., `playground`). |
| `greetings` | string | No | Initial message the avatar speaks on load. |
| `suggestions` | string | No | JSON-encoded array of suggested questions shown to users. Example: `"[\"What is X?\",\"How do I Y?\"]"` |

### Prompt Configuration

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `promptConfig.system_prompt` | string | No* | System prompt for `oc` and `doc_qa` modes. Defines personality and behavior. *Strongly recommended for `oc`/`doc_qa`. |

### Mode-Specific Fields

| Field | Type | Modes | Description |
|-------|------|-------|-------------|
| `voiceflowApiKey` | string | `voiceflow` | Voiceflow project API key. Required for voiceflow mode. |
| `pluginOperationalModeConfig.url` | string | `plugin` | Webhook URL for custom conversational engine. Required for plugin mode. |

---

## Environment Variables

These control the behavior of the skill scripts:

| Variable | Default | Description |
|----------|---------|-------------|
| `UNITH_EMAIL` | *(required)* | Account email for authentication. |
| `UNITH_SECRET_KEY` | *(required)* | Secret key for authentication. |
| `UNITH_MAX_RETRIES` | `3` | Max retry attempts on transient failures. |
| `UNITH_RETRY_DELAY` | `2` | Initial delay between retries (seconds). Doubles each retry (exponential backoff). |
| `UNITH_CURL_TIMEOUT` | `30` | Max time for API requests (seconds). Upload script uses 120. |
| `UNITH_CONNECT_TIMEOUT` | `10` | Connection timeout (seconds). Controls how long to wait for TCP connection. |
| `UNITH_TOKEN_CACHE` | `/tmp/.unith_token_cache` | Path for token cache file. Set to empty string to disable caching. |

---

## Voice Provider Comparison

| Provider | Latency | Quality | Languages | Best for |
|----------|---------|---------|-----------|----------|
| `audiostack` | Low | Good | Wide | Real-time conversation |
| `elevenlabs` | Medium | High | Wide | Natural-sounding voices |
| `azure` | Low | Good | Very wide | Multi-language support |

---

## Language Codes

Common language codes for `language` and `languageSpeechRecognition`:

| Code | Language |
|------|----------|
| `en-US` | English (US) |
| `en-GB` | English (UK) |
| `es-ES` | Spanish (Spain) |
| `es-MX` | Spanish (Mexico) |
| `fr-FR` | French (France) |
| `de-DE` | German |
| `it-IT` | Italian |
| `pt-BR` | Portuguese (Brazil) |
| `ja-JP` | Japanese |
| `ko-KR` | Korean |
| `zh-CN` | Chinese (Simplified) |

Use `bash scripts/list-resources.sh languages` for the full list supported by the API.


path: references/embedding.md
content: # Embedding Reference

How to embed UNITH digital humans in websites and applications.

---

## Quick Embed (iframe)

The simplest way to embed a digital human. Use the `publicUrl` from creation:

```html
<iframe
  src="https://app.unith.ai/s/<publicId>"
  width="400"
  height="600"
  frameborder="0"
  allow="camera; microphone; autoplay"
  allowfullscreen>
</iframe>
```

### Required permissions

The `allow` attribute must include:
- `camera` — if the digital human uses video input
- `microphone` — for voice-based conversation
- `autoplay` — for the avatar to start speaking

---

## JavaScript Widget

For more control, use the UNITH JavaScript widget:

```html
<div id="unith-avatar"></div>

<script src="https://cdn.unith.ai/widget/latest/unith-widget.js"></script>
<script>
  UnithWidget.init({
    containerId: 'unith-avatar',
    publicId: '<publicId>',
    width: 400,
    height: 600,
    autoStart: true,
    onReady: function() {
      console.log('Digital human is ready');
    },
    onMessage: function(message) {
      console.log('Avatar said:', message.text);
    }
  });
</script>
```

### Widget Options

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `containerId` | string | required | ID of the DOM element to render into |
| `publicId` | string | required | The digital human's public ID |
| `width` | number | `400` | Widget width in pixels |
| `height` | number | `600` | Widget height in pixels |
| `autoStart` | boolean | `false` | Start conversation automatically on load |
| `onReady` | function | `null` | Callback when the widget is fully loaded |
| `onMessage` | function | `null` | Callback when the avatar sends a message |
| `onError` | function | `null` | Callback on errors |

---

## React Component

```jsx
import { useEffect, useRef } from 'react';

function UnithAvatar({ publicId, width = 400, height = 600 }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const script = document.createElement('script');
    script.src = 'https://cdn.unith.ai/widget/latest/unith-widget.js';
    script.onload = () => {
      window.UnithWidget.init({
        containerId: containerRef.current.id,
        publicId,
        width,
        height,
        autoStart: true,
      });
    };
    document.head.appendChild(script);

    return () => {
      if (window.UnithWidget?.destroy) {
        window.UnithWidget.destroy();
      }
      document.head.removeChild(script);
    };
  }, [publicId, width, height]);

  return <div id="unith-avatar-container" ref={containerRef} />;
}

export default UnithAvatar;
```

---

## Responsive Embedding

For mobile-friendly layouts:

```html
<style>
  .unith-wrapper {
    position: relative;
    width: 100%;
    max-width: 400px;
    margin: 0 auto;
  }
  .unith-wrapper iframe {
    width: 100%;
    height: 600px;
    border: none;
    border-radius: 12px;
    box-shadow: 0 4px 24px rgba(0, 0, 0, 0.12);
  }
  @media (max-width: 480px) {
    .unith-wrapper iframe {
      height: 100vh;
      border-radius: 0;
    }
  }
</style>

<div class="unith-wrapper">
  <iframe
    src="https://app.unith.ai/s/<publicId>"
    allow="camera; microphone; autoplay"
    allowfullscreen>
  </iframe>
</div>
```

---

## Content Security Policy

If your site uses CSP headers, add these directives:

```
frame-src https://app.unith.ai;
script-src https://cdn.unith.ai;
connect-src https://platform-api.unith.ai;
```


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