{
  "manifest_version": "1.0.0",
  "template": {
    "id": "a08f8c49-1ecc-4143-b860-2332348032d3",
    "slug": "speaches-audio-api-model-cache",
    "name": "Speaches Audio API + Model Cache",
    "description": "OpenAI-compatible speech to text and text to speech with a model cache",
    "url": "https://railway.com/deploy/speaches-audio-api-model-cache",
    "upstream": {
      "image": "ghcr.io/speaches-ai/speaches:0.9.0-rc.3-cpu"
    }
  },
  "services": [
    {
      "name": "Speaches",
      "source": {
        "image": "ghcr.io/speaches-ai/speaches:0.9.0-rc.3-cpu"
      },
      "needs_volume": true,
      "volume_mount_path": "/home/ubuntu/.cache/huggingface/hub",
      "http": true
    }
  ],
  "required_inputs": [
    {
      "key": "PORT",
      "service": "Speaches",
      "description": "Port Railway's healthcheck and edge proxy probe. Speaches itself reads UVICORN_PORT, so keep the two equal (8000, the upstream default and the domain target port).",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "API_KEY",
      "service": "Speaches",
      "description": "Bearer token required on every /v1 route (Authorization: Bearer <key>). Missing or wrong keys get HTTP 403. /health, /docs and /openapi.json stay public so the Railway healthcheck works. Leave it set; deleting the variable makes the whole speech API open to anyone with the URL.",
      "secret": true,
      "strategy": "generate",
      "generate": "random_base64_32"
    },
    {
      "key": "HF_HOME",
      "service": "Speaches",
      "description": "Hugging Face home directory. Model files land in $HF_HOME/hub, which is exactly the volume mount path, so downloaded Whisper, Kokoro and Piper models survive redeploys. Change it only if you also move the volume mount.",
      "secret": false,
      "strategy": "default",
      "default": "/home/ubuntu/.cache/huggingface"
    },
    {
      "key": "ENABLE_UI",
      "service": "Speaches",
      "description": "Optional. Serve the Gradio playground at / (speech to text, text to speech, voice chat tabs). The playground page itself is public; it only reaches the API after you paste the API_KEY into its API Key box. Set false for an API-only endpoint and a slightly faster boot.",
      "secret": false,
      "strategy": "default",
      "default": "true"
    },
    {
      "key": "LOG_LEVEL",
      "service": "Speaches",
      "description": "Application log level: debug, info, warning, error or critical. Upstream defaults to debug, which logs every audio chunk and makes Railway logs unreadable.",
      "secret": false,
      "strategy": "default",
      "default": "info"
    },
    {
      "key": "UVICORN_HOST",
      "service": "Speaches",
      "description": "Bind address. Keep 0.0.0.0: uvicorn binds '::' as IPv6-only (asyncio sets IPV6_V6ONLY) and Railway's healthcheck then never connects. Side effect: other Railway services cannot reach this one over private networking, which is IPv6 only. Call the public domain instead.",
      "secret": false,
      "strategy": "default",
      "default": "0.0.0.0"
    },
    {
      "key": "UVICORN_PORT",
      "service": "Speaches",
      "description": "Port uvicorn binds. The image CMD is 'uvicorn --factory speaches.main:create_app' with no flags, and uvicorn's CLI reads UVICORN_* environment variables. Keep equal to PORT.",
      "secret": false,
      "strategy": "default",
      "default": "8000"
    },
    {
      "key": "STT_MODEL_TTL",
      "service": "Speaches",
      "description": "Optional. Seconds a speech-to-text model stays in memory after its last use. -1 keeps it loaded forever (fastest, most RAM), 0 unloads immediately (least RAM, slow first request).",
      "secret": false,
      "strategy": "default",
      "default": "300"
    },
    {
      "key": "TTS_MODEL_TTL",
      "service": "Speaches",
      "description": "Optional. Seconds a text-to-speech model stays in memory after its last use. Same semantics as STT_MODEL_TTL.",
      "secret": false,
      "strategy": "default",
      "default": "300"
    },
    {
      "key": "VAD_MODEL_TTL",
      "service": "Speaches",
      "description": "Optional. Seconds the Silero voice-activity-detection model stays loaded. It is small, ships inside the image and runs on every transcription, so -1 (never unload) is the sensible default.",
      "secret": false,
      "strategy": "default",
      "default": "-1"
    },
    {
      "key": "PRELOAD_MODELS",
      "service": "Speaches",
      "description": "JSON list of model IDs downloaded during startup, so the API works on first request. Speaches never downloads models implicitly: any /v1 request naming a model that is not in the cache returns 404. About 800 MB on first boot, then served from the volume. The process exits if a download fails, so keep the list short. Set to [] to skip and download on demand with POST /v1/models/{model_id}.",
      "secret": false,
      "strategy": "default",
      "default": "[\"Systran/faster-whisper-small\",\"speaches-ai/Kokoro-82M-v1.0-ONNX\"]"
    },
    {
      "key": "RAILWAY_RUN_UID",
      "service": "Speaches",
      "description": "Run the container as root. Required: the image runs as user 'ubuntu' (UID 1000) and the Railway volume mounts root-owned, so without this the Hugging Face cache write fails with 'PermissionError: [Errno 13] Permission denied'.",
      "secret": false,
      "strategy": "default",
      "default": "0"
    },
    {
      "key": "LOOPBACK_HOST_URL",
      "service": "Speaches",
      "description": "URL the bundled playground and the voice-chat feature use to call this same server. Required behind a reverse proxy such as Railway's edge, otherwise Gradio guesses the scheme from forwarded headers and can end up on a redirect. Must match UVICORN_PORT.",
      "secret": false,
      "strategy": "default",
      "default": "http://localhost:8000"
    },
    {
      "key": "WHISPER__CPU_THREADS",
      "service": "Speaches",
      "description": "Optional. CTranslate2 worker threads. Upstream default 0 means 'all cores', and containers see the host's core count rather than the plan's vCPU limit, which oversubscribes and slows transcription. Raise it if you raise the service's vCPU limit.",
      "secret": false,
      "strategy": "default",
      "default": "4"
    },
    {
      "key": "WHISPER__COMPUTE_TYPE",
      "service": "Speaches",
      "description": "CTranslate2 quantization for Whisper. int8 is roughly 4x smaller and 2 to 3x faster than the float32 fallback CPUs use for 'default', at a small accuracy cost. Use float32 for best quality on a larger plan.",
      "secret": false,
      "strategy": "default",
      "default": "int8"
    },
    {
      "key": "HF_HUB_ENABLE_HF_TRANSFER",
      "service": "Speaches",
      "description": "Use the Rust-based hf_transfer downloader for model downloads (shipped in the image via huggingface-hub[hf-transfer]). Cuts first-boot download time. Set to 0 if a download ever fails and you want the plain Python downloader's error messages.",
      "secret": false,
      "strategy": "default",
      "default": "1"
    },
    {
      "key": "WHISPER__INFERENCE_DEVICE",
      "service": "Speaches",
      "description": "Device for faster-whisper transcription. Railway has no GPUs, so pin cpu. Note the double underscore: it maps to the nested whisper.inference_device setting.",
      "secret": false,
      "strategy": "default",
      "default": "cpu"
    },
    {
      "key": "RAILWAY_DEPLOYMENT_DRAINING_SECONDS",
      "service": "Speaches",
      "description": "Optional. Seconds Railway waits after SIGTERM so in-flight transcriptions and streamed audio responses can finish.",
      "secret": false,
      "strategy": "default",
      "default": "60"
    }
  ],
  "deploy": {
    "mcp": {
      "server": "railway",
      "tool": "deploy_template",
      "args": {
        "template_code": "speaches-audio-api-model-cache"
      }
    },
    "cli": "railway deploy --template speaches-audio-api-model-cache",
    "api": {
      "method": "POST",
      "path": "/graphql/v2",
      "body": {
        "query": "mutation templateDeploy($input: TemplateDeployV2Input!) { templateDeployV2(input: $input) { projectId workflowId } }",
        "variables": {
          "input": {
            "templateId": "a08f8c49-1ecc-4143-b860-2332348032d3",
            "serializedConfig": {
              "services": {
                "e9012e95-c784-40ea-99a9-de5098a74863": {
                  "icon": "https://avatars.githubusercontent.com/u/192631691?v=4",
                  "name": "Speaches",
                  "build": {},
                  "deploy": {
                    "healthcheckPath": "/health",
                    "requiredMountPath": "/home/ubuntu/.cache/huggingface/hub",
                    "restartPolicyType": "ON_FAILURE",
                    "healthcheckTimeout": 900,
                    "restartPolicyMaxRetries": 5
                  },
                  "source": {
                    "image": "ghcr.io/speaches-ai/speaches:0.9.0-rc.3-cpu"
                  },
                  "variables": {
                    "PORT": {
                      "description": "Port Railway's healthcheck and edge proxy probe. Speaches itself reads UVICORN_PORT, so keep the two equal (8000, the upstream default and the domain target port).",
                      "defaultValue": "8000"
                    },
                    "API_KEY": {
                      "description": "Bearer token required on every /v1 route (Authorization: Bearer <key>). Missing or wrong keys get HTTP 403. /health, /docs and /openapi.json stay public so the Railway healthcheck works. Leave it set; deleting the variable makes the whole speech API open to anyone with the URL.",
                      "defaultValue": "{{API_KEY}}"
                    },
                    "HF_HOME": {
                      "description": "Hugging Face home directory. Model files land in $HF_HOME/hub, which is exactly the volume mount path, so downloaded Whisper, Kokoro and Piper models survive redeploys. Change it only if you also move the volume mount.",
                      "defaultValue": "/home/ubuntu/.cache/huggingface"
                    },
                    "HF_TOKEN": {
                      "isOptional": true,
                      "description": "Optional. Hugging Face access token, only needed for gated or private model repositories. The default Whisper, Kokoro and Piper models are public and need no token.",
                      "defaultValue": ""
                    },
                    "ENABLE_UI": {
                      "isOptional": true,
                      "description": "Optional. Serve the Gradio playground at / (speech to text, text to speech, voice chat tabs). The playground page itself is public; it only reaches the API after you paste the API_KEY into its API Key box. Set false for an API-only endpoint and a slightly faster boot.",
                      "defaultValue": "true"
                    },
                    "LOG_LEVEL": {
                      "description": "Application log level: debug, info, warning, error or critical. Upstream defaults to debug, which logs every audio chunk and makes Railway logs unreadable.",
                      "defaultValue": "info"
                    },
                    "UVICORN_HOST": {
                      "description": "Bind address. Keep 0.0.0.0: uvicorn binds '::' as IPv6-only (asyncio sets IPV6_V6ONLY) and Railway's healthcheck then never connects. Side effect: other Railway services cannot reach this one over private networking, which is IPv6 only. Call the public domain instead.",
                      "defaultValue": "0.0.0.0"
                    },
                    "UVICORN_PORT": {
                      "description": "Port uvicorn binds. The image CMD is 'uvicorn --factory speaches.main:create_app' with no flags, and uvicorn's CLI reads UVICORN_* environment variables. Keep equal to PORT.",
                      "defaultValue": "8000"
                    },
                    "ALLOW_ORIGINS": {
                      "isOptional": true,
                      "description": "Optional. JSON list of CORS origins, for example [\"https://app.example.com\"]. Left unset no CORS middleware is added at all, so browsers cannot call the API cross-origin. Never ship an empty string: the value must be valid JSON.",
                      "defaultValue": ""
                    },
                    "STT_MODEL_TTL": {
                      "isOptional": true,
                      "description": "Optional. Seconds a speech-to-text model stays in memory after its last use. -1 keeps it loaded forever (fastest, most RAM), 0 unloads immediately (least RAM, slow first request).",
                      "defaultValue": "300"
                    },
                    "TTS_MODEL_TTL": {
                      "isOptional": true,
                      "description": "Optional. Seconds a text-to-speech model stays in memory after its last use. Same semantics as STT_MODEL_TTL.",
                      "defaultValue": "300"
                    },
                    "VAD_MODEL_TTL": {
                      "isOptional": true,
                      "description": "Optional. Seconds the Silero voice-activity-detection model stays loaded. It is small, ships inside the image and runs on every transcription, so -1 (never unload) is the sensible default.",
                      "defaultValue": "-1"
                    },
                    "PRELOAD_MODELS": {
                      "description": "JSON list of model IDs downloaded during startup, so the API works on first request. Speaches never downloads models implicitly: any /v1 request naming a model that is not in the cache returns 404. About 800 MB on first boot, then served from the volume. The process exits if a download fails, so keep the list short. Set to [] to skip and download on demand with POST /v1/models/{model_id}.",
                      "defaultValue": "[\"Systran/faster-whisper-small\",\"speaches-ai/Kokoro-82M-v1.0-ONNX\"]"
                    },
                    "RAILWAY_RUN_UID": {
                      "description": "Run the container as root. Required: the image runs as user 'ubuntu' (UID 1000) and the Railway volume mounts root-owned, so without this the Hugging Face cache write fails with 'PermissionError: [Errno 13] Permission denied'.",
                      "defaultValue": "0"
                    },
                    "LOOPBACK_HOST_URL": {
                      "description": "URL the bundled playground and the voice-chat feature use to call this same server. Required behind a reverse proxy such as Railway's edge, otherwise Gradio guesses the scheme from forwarded headers and can end up on a redirect. Must match UVICORN_PORT.",
                      "defaultValue": "http://localhost:8000"
                    },
                    "WHISPER__CPU_THREADS": {
                      "isOptional": true,
                      "description": "Optional. CTranslate2 worker threads. Upstream default 0 means 'all cores', and containers see the host's core count rather than the plan's vCPU limit, which oversubscribes and slows transcription. Raise it if you raise the service's vCPU limit.",
                      "defaultValue": "4"
                    },
                    "WHISPER__COMPUTE_TYPE": {
                      "description": "CTranslate2 quantization for Whisper. int8 is roughly 4x smaller and 2 to 3x faster than the float32 fallback CPUs use for 'default', at a small accuracy cost. Use float32 for best quality on a larger plan.",
                      "defaultValue": "int8"
                    },
                    "CHAT_COMPLETION_API_KEY": {
                      "isOptional": true,
                      "description": "Optional. API key for CHAT_COMPLETION_BASE_URL. Only used by the playground's voice-chat tab.",
                      "defaultValue": ""
                    },
                    "CHAT_COMPLETION_BASE_URL": {
                      "isOptional": true,
                      "description": "Optional. OpenAI-compatible chat completions endpoint used by the playground's voice-chat tab, for example https://your-litellm.up.railway.app/v1. Upstream defaults to a local Ollama at http://localhost:11434/v1, which does not exist here, so the voice-chat tab stays broken until you set this.",
                      "defaultValue": ""
                    },
                    "HF_HUB_ENABLE_HF_TRANSFER": {
                      "description": "Use the Rust-based hf_transfer downloader for model downloads (shipped in the image via huggingface-hub[hf-transfer]). Cuts first-boot download time. Set to 0 if a download ever fails and you want the plain Python downloader's error messages.",
                      "defaultValue": "1"
                    },
                    "WHISPER__INFERENCE_DEVICE": {
                      "description": "Device for faster-whisper transcription. Railway has no GPUs, so pin cpu. Note the double underscore: it maps to the nested whisper.inference_device setting.",
                      "defaultValue": "cpu"
                    },
                    "RAILWAY_DEPLOYMENT_DRAINING_SECONDS": {
                      "isOptional": true,
                      "description": "Optional. Seconds Railway waits after SIGTERM so in-flight transcriptions and streamed audio responses can finish.",
                      "defaultValue": "60"
                    }
                  },
                  "networking": {
                    "tcpProxies": {},
                    "serviceDomains": {
                      "speaches-dom": {
                        "port": 8000
                      }
                    }
                  },
                  "volumeMounts": {
                    "0e6d05b5-2a39-47a1-a1c2-c41540ec14a0": {
                      "mountPath": "/home/ubuntu/.cache/huggingface/hub"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "post_deploy": {
    "healthcheck": {
      "service": "Speaches",
      "method": "GET",
      "path": "/health",
      "expect_status": 200
    }
  },
  "resources": {
    "expected_services": 1,
    "needs_volume": true
  },
  "generated_at": "2026-09-21T04:14:46.774Z",
  "generator_version": "0.1.0",
  "status": "validated",
  "validated_at": "2026-09-21T02:19:45.967Z",
  "success_rate_30d": 1,
  "validation": {
    "last_run_id": "run_0d881f3f419847b4ae52",
    "checks": [
      {
        "name": "workflow_completed",
        "passed": true
      },
      {
        "name": "all_services_deployed",
        "passed": true
      },
      {
        "name": "healthcheck",
        "passed": true
      },
      {
        "name": "stays_up",
        "passed": true
      }
    ],
    "typical_ready_seconds": 58,
    "typical_build_seconds": 0,
    "typical_start_seconds": 40,
    "slowest_service": "Speaches"
  }
}
