diff --git a/fern/llms.txt b/fern/llms.txt
index 9fa77eaa62..cff0793f3e 100644
--- a/fern/llms.txt
+++ b/fern/llms.txt
@@ -160,7 +160,7 @@ Feature pages are grouped by channel. SDK pages come first when available, follo
- [Transfer calls with SIP REFER](/docs/platform/voice/sip-refer): Transfer SIP calls with SWML, Relay, or REST, then handle the result or fall back to a bridged transfer.
- [Call legs](/docs/platform/voice/call-legs): What a call leg is, how connects, forwards, and fan-outs create legs, how to tell legs apart by their identifiers, and how each leg appears in logs, callbacks, and charges.
- [Transcribe calls](/docs/platform/voice/transcribe-calls): Transcribe a live call as it happens with SWML, Relay, or REST and receive each utterance on your webhook, then summarize the conversation with AI, stop transcribing, or transcribe the whole call after it ends.
-- [Live translation](/docs/server-sdks/reference/python/relay/call/live-translate): Start or stop live translation on a call.
+- [Translate calls](/docs/platform/voice/translate-calls): Translate a live call between two languages with SWML, Relay, or REST so each party hears the other in their own language, then read the translated conversation on your webhook, choose voices and tone filters, get AI summaries, and stop translating.
- [Stream call audio](/docs/platform/voice/call-streaming): Stream a live call to your own WebSocket server with SWML, Relay, or REST, then save or process the audio as it arrives, and play audio back into the call on a bidirectional stream.
- [Conferencing](/docs/server-sdks/reference/python/relay/call/join-conference): Join a call to an ad-hoc audio conference.
- [Secure payments](/docs/server-sdks/reference/python/relay/call/pay): Collect payment information on a call.
diff --git a/fern/products/platform/pages/calling/voice/overview.mdx b/fern/products/platform/pages/calling/voice/overview.mdx
index 718890f0ee..bd9793c8a6 100644
--- a/fern/products/platform/pages/calling/voice/overview.mdx
+++ b/fern/products/platform/pages/calling/voice/overview.mdx
@@ -105,6 +105,9 @@ SignalWire's advanced APIs and elastic cloud infrastructure make it a breeze to
Why one call shows up as several records, and how to tell the legs apart.
+
+ Let a caller and your team talk in their own languages, with each side translated live.
+
{/* Core links */}
diff --git a/fern/products/platform/pages/calling/voice/translate-calls.mdx b/fern/products/platform/pages/calling/voice/translate-calls.mdx
new file mode 100644
index 0000000000..182efb7465
--- /dev/null
+++ b/fern/products/platform/pages/calling/voice/translate-calls.mdx
@@ -0,0 +1,1055 @@
+---
+title: Translate calls
+slug: voice/translate-calls
+description: Translate a live call between two languages with SWML, Relay, or REST so each party hears the other in their own language, then read the translated conversation on your webhook, choose voices and tone filters, get AI summaries, and stop translating.
+subtitle: Let two people talk across languages on one call
+max-toc-depth: 3
+---
+
+[make-and-receive-calls]: /docs/platform/voice/make-and-receive-calls
+[prepare-calls]: /docs/platform/voice/make-and-receive-calls#prepare-for-your-first-call
+[answer-swml]: /docs/platform/voice/make-and-receive-calls#answer-a-call-via-swml
+[answer-relay]: /docs/platform/voice/make-and-receive-calls#answer-a-call-via-websocket-relay
+[forward-calls]: /docs/platform/voice/forward-calls
+[caller-id]: /docs/platform/voice/how-to-set-caller-id-or-cnam
+[trial-mode]: /docs/platform/trial-mode
+[resources]: /docs/platform/resources
+[server-sdks]: /docs/server-sdks
+[relay-guide]: /docs/server-sdks/guides/relay-client
+[swml-intro]: /docs/swml
+[browser-sdk]: /docs/browser-sdk
+[cfb-intro]: /docs/call-flow-builder
+[call-commands]: /docs/apis/rest/calls/call-commands
+[swml-live-translate]: /docs/swml/reference/calling/live-translate
+[swml-connect]: /docs/swml/reference/calling/connect
+[verify-signature]: /docs/platform/webhooks#verify-webhook-signature
+[tts]: /docs/platform/voice/tts
+[tts-azure]: /docs/platform/voice/tts/azure
+[tts-elevenlabs]: /docs/platform/voice/tts/elevenlabs
+[transcribe-calls]: /docs/platform/voice/transcribe-calls
+
+Live translation lets two people who speak different languages talk on one call. SignalWire
+recognizes what each person says, translates it, and speaks the translation to the other person in a
+synthesized voice. Start by translating a call to your number, with a small server that prints each
+translated utterance, then start translation from code, read the translated conversation on your
+webhook, choose voices and tone filters, get AI summaries, and stop translating.
+
+## Pick the right product for call translation
+
+Translation is available across these products, but not every function is available on each.
+
+| Function | SWML | WebSocket (Relay) | REST | Browser SDK | Call Flow Builder |
+|---|---|---|---|---|---|
+| Translate both sides of a call as it happens | | | | | |
+| Get each translated utterance on your webhook as it's spoken | | | | | |
+| Get an AI summary of the conversation, during or after the call | | | | | |
+| Stop translating before the call ends | | | | | |
+
+- [SWML][swml-intro] is the document that describes how the call should flow. The Server SDKs'
+ SWML builder writes it, or you can write it by hand.
+- [WebSocket (Relay)][relay-guide], a Server SDK namespace, holds a live WebSocket connection to a
+ call in progress and controls it from code.
+- The [REST Calling API][call-commands] sends the same commands as Relay to a call in progress over
+ HTTP, by call ID, from any process.
+- The [Browser SDK][browser-sdk] places and answers calls in a web page. Translation is started by
+ the server-side products above.
+- [Call Flow Builder][cfb-intro] draws call flows on a canvas, with a node for each step of the
+ call. Translation is started by the products above.
+
+## Prepare for call translation
+
+This guide answers and forwards calls the way [Make and receive calls][make-and-receive-calls] and
+[Forward calls][forward-calls] do. Have these values ready:
+
+- Your Space URL, such as `.signalwire.com`.
+- Your Project ID and an API token with the **Voice** permission.
+- A phone number in your Space.
+- Two phones you can answer: one to call your number from, and one that receives the translated
+ call. Ask a second person to answer it, or put both phones in front of you.
+- A machine that can run a small server, and a tunnel such as [ngrok](https://ngrok.com/) that
+ gives it a public HTTPS URL.
+
+
+A [trial project][trial-mode] only places calls to and receives calls from numbers you've
+purchased or verified, and can't call internationally. Verify both phones you'll test with, as
+[Prepare for your first call][prepare-calls] shows.
+
+
+
+Translation captures what people say on the call and sends the text to your webhook. Many
+jurisdictions require you to tell callers before you record or transcribe them. Check the rules that
+apply to your callers before you translate a real conversation.
+
+
+If you only need the text of a call in one language, see [Transcribe calls][transcribe-calls]
+instead.
+
+Replace these values in the samples you run:
+
+| Value | Replace with |
+|---|---|
+| `` | Your Space's subdomain in `.signalwire.com` |
+| `` | Your Project ID |
+| `` | Your API token with the Voice permission, used only in server code |
+| `` | The public hostname of your server's tunnel |
+| `` | A password you choose for the SWML your server serves |
+| `` | Your server's translation webhook, `https:///translations` |
+| `` | The phone that receives the translated call, in E.164 format, such as `+12025550123` |
+| `` | Your SignalWire phone number, or a [verified caller ID][caller-id], in E.164 format, when Relay connects the call |
+| `` | The ID of a live call, when you send a command over REST |
+
+## How call translation works
+
+The [`live_translate`][swml-live-translate] SWML method and the `calling.live_translate` command,
+sent over Relay or REST, all take the same `action`: `start` a session, `summarize`
+it, or `stop` it. `start` names two languages, `from_lang` and `to_lang`, and lists the sides of the
+call to translate in `direction`:
+
+- `remote-caller` is the audio SignalWire receives from the call: the person who called your
+ number, or the person your app dialed.
+- `local-caller` is the audio SignalWire sends into the call, such as the other party of a
+ [`connect`][swml-connect].
+
+For each side you list, SignalWire mutes that person's own voice and plays the translation in its
+place, so the listener hears only the translated speech. With both sides listed, each person hears
+only the other's translation. A side you leave out passes through untouched.
+
+Bayview Taxi uses this for riders who call in Spanish. Their calls are connected to Ada, a
+dispatcher who speaks English. Every sample in this guide starts translation with the same values:
+
+```json
+{
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"]
+}
+```
+
+With those values, each person speaks and hears one language:
+
+| Person | Side of the call | Speaks | Hears the other person in |
+|---|---|---|---|
+| The rider, who calls your number | `remote-caller` | Spanish, the `from_lang` | Spanish, in the `from_voice` voice |
+| Ada, whom the call connects to | `local-caller` | English, the `to_lang` | English, in the `to_voice` voice |
+
+The order of `direction` decides which side speaks which language. With `local-caller` listed
+first, as here, the caller speaks `from_lang` and the connected party speaks `to_lang`. Listing
+`remote-caller` first swaps the two, so keep this order and change the languages to fit your
+callers. `from_voice` always speaks `from_lang`, and `to_voice` always speaks `to_lang`. When you
+list one side only, that side speaks `to_lang`, and the other person hears it in `from_lang`.
+
+The `local-caller` side carries a voice only once another party is on the call, so the samples
+start translation, then `connect` the caller to Ada. Translation runs in the background: after
+`start`, the call moves on to its next step at once.
+
+When the session has a `webhook`, SignalWire sends it HTTP `POST` requests: one for each translated
+utterance when `live_events` is `true`, one when you ask for a summary, and one with the whole
+conversation when the session ends.
+
+## Translate a call
+
+Every sample in this guide sends translation events to one small server that prints them. Run it
+first, then start translation from the product your app uses.
+
+### Translate a call via SWML
+
+When someone calls your number, SignalWire fetches the call's SWML from your server and runs it.
+The server below serves that document and receives the translation events, so one tunnel covers
+both.
+
+
+
+
+
+This server answers calls with a SWML document that starts translation and connects the caller to
+your second phone. It prints each translated utterance that arrives at `/translations`. The
+document is served only to requests that carry the username `signalwire` and the password you set.
+
+
+
+```python
+# Install: python -m pip install signalwire-sdk fastapi "uvicorn[standard]"
+# Save as translate_app.py and run: python translate_app.py
+import uvicorn
+from fastapi import FastAPI, Request
+from signalwire import SWMLBuilder, SWMLService
+
+service = SWMLService(
+ name="translate-call",
+ basic_auth=("signalwire", ""),
+)
+swml = SWMLBuilder(service)
+swml.answer().live_translate(action={
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": True,
+ }
+}).connect(to="")
+
+app = FastAPI()
+# SignalWire fetches the call's SWML from the server's root URL.
+app.include_router(service.as_router())
+
+
+# Translation events arrive at /translations.
+@app.post("/translations")
+async def translations(request: Request):
+ event = await request.json()
+ kind = event["channel_data"]["content_disposition"]
+ if kind == "translate.translate_utterance":
+ for key in ("primary_utterance", "secondary_utterance"):
+ utterance = event.get(key)
+ if utterance:
+ print(f"{utterance['role']} [{utterance['lang']}]: {utterance['content']}")
+ elif kind == "translate.live_summary":
+ for key in ("primary_conversation_summary", "secondary_conversation_summary"):
+ if event.get(key):
+ print(f"Summary so far: {event[key]}")
+ elif kind == "translate.conversation_log":
+ print(f"Translation ended after {len(event['primary_conversation_log'])} utterances")
+ for key in ("primary_conversation_summary", "secondary_conversation_summary"):
+ if event.get(key):
+ print(f"Summary: {event[key]}")
+ return {}
+
+
+uvicorn.run(app, host="0.0.0.0", port=8080)
+```
+
+
+```typescript
+// Install: npm install @signalwire/sdk @hono/node-server hono
+// This sample also runs as JavaScript: save as translate-app.mjs,
+// then run: node translate-app.mjs
+import { serve } from "@hono/node-server";
+import { Hono } from "hono";
+import { SWMLService } from "@signalwire/sdk";
+
+const service = new SWMLService({
+ name: "translate-call",
+ basicAuth: ["signalwire", ""],
+});
+const swml = service.getBuilder();
+swml.answer().live_translate({
+ action: {
+ start: {
+ from_lang: "es-ES",
+ to_lang: "en-US",
+ direction: ["local-caller", "remote-caller"],
+ webhook: "",
+ live_events: true,
+ },
+ },
+}).connect({ to: "" });
+
+const app = new Hono();
+
+// Translation events arrive at /translations.
+app.post("/translations", async (c) => {
+ const event = await c.req.json();
+ const kind = event.channel_data.content_disposition;
+ if (kind === "translate.translate_utterance") {
+ for (const utterance of [event.primary_utterance, event.secondary_utterance]) {
+ if (utterance) console.log(`${utterance.role} [${utterance.lang}]: ${utterance.content}`);
+ }
+ } else if (kind === "translate.live_summary") {
+ for (const summary of [event.primary_conversation_summary, event.secondary_conversation_summary]) {
+ if (summary) console.log(`Summary so far: ${summary}`);
+ }
+ } else if (kind === "translate.conversation_log") {
+ console.log(`Translation ended after ${event.primary_conversation_log.length} utterances`);
+ for (const summary of [event.primary_conversation_summary, event.secondary_conversation_summary]) {
+ if (summary) console.log(`Summary: ${summary}`);
+ }
+ }
+ return c.json({});
+});
+
+// SignalWire fetches the call's SWML from the server's root URL.
+app.route("/", service.getApp());
+
+serve({ fetch: app.fetch, port: 8080 });
+console.log("Listening on port 8080");
+```
+
+
+
+Start a tunnel to port `8080`, such as `ngrok http 8080`, and use its hostname as
+`` in the webhook URL before you start the server. Check that the document is
+reachable through the tunnel:
+
+```bash
+curl -u "signalwire:" "https:///"
+```
+
+The response is the SWML document, starting with `{"version": "1.0.0"`. A `401` means the password
+doesn't match the one in your server.
+
+
+
+
+
+Create a SWML Script that fetches the document from your server:
+
+1. Open **My Resources**, select **+ Add**, then **Script**, then **SWML Script**.
+2. Name it **Translate call** and leave **Used For** set to **Calling**.
+3. Under **Handle Calls Using**, choose **External URL** and enter
+ `https://signalwire:@/` in **Primary Script URL**.
+4. Select **Create**.
+
+Then assign the script, a [Resource][resources], to your phone number:
+
+
+
+To do both in code, see [Answer a call via SWML][answer-swml].
+
+
+
+
+
+Call your SignalWire number from your first phone, and answer your second phone when it rings. Say
+something in Spanish on the first phone, such as "Necesito un taxi al aeropuerto." The second phone
+hears it in English, in a synthesized voice, and never hears the Spanish. Answer in English on the
+second phone, and the first phone hears Spanish.
+
+The server prints each utterance in both languages, with the side of the call that spoke:
+
+```text
+remote-caller [es-ES]: Necesito un taxi al aeropuerto.
+remote-caller [en-US]: I need a taxi to the airport.
+local-caller [es-ES]: Claro. ¿Cuál es la dirección de recogida?
+local-caller [en-US]: Sure. What's the pickup address?
+```
+
+Your wording and the translations will differ. When either phone hangs up, the server prints
+`Translation ended after` with the number of utterances. If it doesn't work, see
+[Troubleshoot call translation](#troubleshoot-call-translation).
+
+
+
+
+
+The same document in YAML and JSON, to paste into a hosted SWML Script instead:
+
+
+
+```yaml
+version: 1.0.0
+sections:
+ main:
+ - answer: {}
+ - live_translate:
+ action:
+ start:
+ from_lang: es-ES
+ to_lang: en-US
+ direction:
+ - local-caller
+ - remote-caller
+ webhook:
+ live_events: true
+ - connect:
+ to:
+```
+
+
+```json
+{
+ "version": "1.0.0",
+ "sections": {
+ "main": [
+ { "answer": {} },
+ {
+ "live_translate": {
+ "action": {
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": true
+ }
+ }
+ }
+ },
+ { "connect": { "to": "" } }
+ ]
+ }
+}
+```
+
+
+
+### Start translation on a live call via WebSocket (Relay)
+
+Send the live translate command to an answered call, passing the same `action` the
+SWML method takes. Translation events still go to your webhook, so keep the
+[translation server](#run-the-translation-server) and its tunnel running.
+
+This handler subscribes to the `translate-calls` topic. When a call arrives, it answers, starts
+translation, and connects the caller to your second phone. It hangs up when the connected call
+ends.
+
+
+
+```python
+# Install: python -m pip install signalwire-sdk
+# Save as translate_call_relay.py and run: python translate_call_relay.py
+from signalwire.relay import ConnectEvent, RelayClient
+
+client = RelayClient(
+ project="",
+ token="",
+ contexts=["translate-calls"],
+)
+
+@client.on_call
+async def handle_call(call):
+ async def on_connect(event: ConnectEvent):
+ print(f"Connect: {event.connect_state}")
+ if event.connect_state in ("disconnected", "failed") and call.state != "ended":
+ await call.hangup()
+
+ call.on("calling.call.connect", on_connect)
+ await call.answer()
+ await call.live_translate({
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": True,
+ }
+ })
+ await call.connect(
+ devices=[[{
+ "type": "phone",
+ "params": {
+ "from_number": "",
+ "to_number": "",
+ "timeout": 30,
+ },
+ }]],
+ )
+ await call.wait_for_ended()
+
+client.run()
+```
+
+
+```typescript
+// Install: npm install @signalwire/sdk
+// This sample also runs as JavaScript: save as translate-call-relay.mjs,
+// then run: node translate-call-relay.mjs
+import { RelayClient } from "@signalwire/sdk";
+
+const client = new RelayClient({
+ project: "",
+ token: "",
+ contexts: ["translate-calls"],
+});
+
+client.onCall(async (call) => {
+ call.on("calling.call.connect", async (event) => {
+ const state = event.params.connect_state;
+ console.log(`Connect: ${state}`);
+ if ((state === "disconnected" || state === "failed") && call.state !== "ended") {
+ await call.hangup();
+ }
+ });
+ await call.answer();
+ await call.liveTranslate({
+ start: {
+ from_lang: "es-ES",
+ to_lang: "en-US",
+ direction: ["local-caller", "remote-caller"],
+ webhook: "",
+ live_events: true,
+ },
+ });
+ await call.connect([[{
+ type: "phone",
+ params: {
+ from_number: "",
+ to_number: "",
+ timeout: 30,
+ },
+ }]]);
+ await call.waitForEnded();
+});
+
+await client.run();
+```
+
+
+
+The Server SDKs send the `action` keys as written, so they stay in snake_case in every language.
+Run the handler, then create a Relay Application with `translate-calls` as its **Topic** and assign
+it to your phone number in place of the SWML Script, as
+[Answer a call via WebSocket (Relay)][answer-relay] shows. Call your number and speak Spanish: the
+handler prints `Connect: connected` when your second phone answers, and the translation server prints
+each utterance as in the SWML walkthrough.
+
+To start translation on a call your process doesn't hold, send the `calling.live_translate` command
+with the answered call's ID and a `start` action. The call ID is the `id` from a `dial` response, or
+the `call_id` delivered to your SWML endpoint or webhook. The [Server SDKs][server-sdks] wrap the
+command:
+
+
+
+```python
+# Install: python -m pip install signalwire-sdk
+# Save as translate_rest.py and run: python translate_rest.py
+from signalwire.rest import RestClient
+
+client = RestClient(
+ project="",
+ token="",
+ host=".signalwire.com",
+)
+
+client.calling.live_translate(
+ "",
+ action={
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": True,
+ }
+ },
+)
+print("Translation started")
+```
+
+
+```typescript
+// Install: npm install @signalwire/sdk
+// This sample also runs as JavaScript: save as translate-rest.mjs,
+// then run: node translate-rest.mjs
+import { RestClient } from "@signalwire/sdk";
+
+const client = new RestClient({
+ project: "",
+ token: "",
+ host: ".signalwire.com",
+});
+
+await client.calling.liveTranslate("", {
+ start: {
+ from_lang: "es-ES",
+ to_lang: "en-US",
+ direction: ["local-caller", "remote-caller"],
+ webhook: "",
+ live_events: true,
+ },
+});
+console.log("Translation started");
+```
+
+
+```bash
+curl -X POST "https://.signalwire.com/api/calling/calls" \
+ -u ":" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "command": "calling.live_translate",
+ "id": "",
+ "params": {
+ "action": {
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": true
+ }
+ }
+ }
+ }'
+```
+
+
+
+To try it, run the program while a forwarded call from [Forward calls][forward-calls] is up and both
+phones are talking. The translation server starts printing utterances as soon as the command
+succeeds. The cURL request goes to [Call commands][call-commands].
+
+Sending `start` again with the same languages while a session runs changes nothing. With different
+languages, it restarts translation in the new ones.
+
+## Read translation events on your webhook
+
+Each request is a JSON `POST`. Read `channel_data.content_disposition` first, then the fields for
+that event:
+
+| `content_disposition` | Sent | Fields to read |
+|---|---|---|
+| `translate.translate_utterance` | Once per translated utterance, when `live_events` is `true` | `primary_utterance`, `secondary_utterance` |
+| `translate.live_summary` | When you send `summarize` | `primary_conversation_summary`, `secondary_conversation_summary` |
+| `translate.conversation_log` | Once, when the session ends | `primary_conversation_log`, `secondary_conversation_log`, and their summaries when `ai_summary` is `true` |
+
+Every event carries the conversation in both languages. The `primary_` fields hold it in
+`from_lang`, and the `secondary_` fields hold it in `to_lang`. `role` says which side of the call
+spoke, using the values you passed in `direction`. Every event also carries `channel_data.call_id`,
+so one server can follow several calls at once.
+
+An utterance event looks like this, shortened to the fields the translation server reads:
+
+```json
+{
+ "channel_data": {
+ "content_type": "text/json",
+ "content_disposition": "translate.translate_utterance",
+ "call_id": "76ac3c36-56da-4a3e-a0d6-b5f8df6da9ad"
+ },
+ "primary_utterance": {
+ "role": "remote-caller",
+ "lang": "es-ES",
+ "content": "Necesito un taxi al aeropuerto.",
+ "utterance_id": "0b6f3c1e-8a52-4a8f-9a8e-2f1d7c9e4b10"
+ },
+ "secondary_utterance": {
+ "role": "remote-caller",
+ "lang": "en-US",
+ "content": "I need a taxi to the airport.",
+ "utterance_id": "0b6f3c1e-8a52-4a8f-9a8e-2f1d7c9e4b10"
+ }
+}
+```
+
+Each utterance can also carry `confidence`, the recognizer's confidence in the text, and
+`filter_type` and `filter_value` when a [tone filter](#translation-options-voices-tone-filters-and-speech-detection)
+rewrote it.
+
+The event sent when the session ends carries the whole conversation in order, in each language. It
+also carries `translate_data`, which lists each translated side with the language it was spoken in
+and the language it was translated to. With `ai_summary` set to `true`, `translate_data` is instead a
+flat object for the first-listed side only, with `from_language`, `to_language`, `to_voice`, `role`,
+and `position`. Log entries name their language in `language`:
+
+```json
+{
+ "channel_data": {
+ "content_type": "text/json",
+ "content_disposition": "translate.conversation_log",
+ "call_id": "76ac3c36-56da-4a3e-a0d6-b5f8df6da9ad"
+ },
+ "translate_data": {
+ "remote-caller": { "role": "remote-caller", "from_language": "es-ES", "to_language": "en-US" },
+ "local-caller": { "role": "local-caller", "from_language": "en-US", "to_language": "es-ES" }
+ },
+ "primary_conversation_log": [
+ { "role": "remote-caller", "language": "es-ES", "content": "Necesito un taxi al aeropuerto." },
+ { "role": "local-caller", "language": "es-ES", "content": "Claro. ¿Cuál es la dirección de recogida?" }
+ ],
+ "secondary_conversation_log": [
+ { "role": "remote-caller", "language": "en-US", "content": "I need a taxi to the airport." },
+ { "role": "local-caller", "language": "en-US", "content": "Sure. What's the pickup address?" }
+ ]
+}
+```
+
+SignalWire sends this event whenever the session has a `webhook`, whether the session ended because
+you stopped it or because the call ended. A summary you request mid-call carries
+`primary_conversation_summary` and `secondary_conversation_summary`, one summary for each
+conversation log.
+
+Answer each request with a `2xx` status, and do slow work after you respond. SignalWire retries an
+utterance event that fails to connect or gets a `5xx` status, so a slow handler can receive the same
+utterance more than once. Use `utterance_id` to ignore repeats.
+
+
+Requests carry an `X-SignalWire-Signature` header, so your server can confirm they came from
+SignalWire, as [Verify webhook signature][verify-signature] shows. You can also put a username and
+password in the webhook URL, as in `https://:@/translations`,
+and SignalWire sends them as HTTP Basic authentication. If you reject unsigned requests, keep
+`live_events` set to `true`: with it off, the end-of-session event can arrive without a signature.
+
+
+## Translation options: voices, tone filters, and speech detection
+
+`start` takes more options alongside the languages. The same options work in SWML, Relay, and REST.
+The [`live_translate` reference][swml-live-translate] lists every option.
+
+### Choose the voices for each language
+
+`from_voice` speaks the `from_lang` translations, and `to_voice` speaks the `to_lang`
+translations. Without them, both use `azure.en-US-RyanMultilingualNeural`. A voice is the engine
+name in lowercase, a dot, and the voice ID, such as `azure.es-ES-ElviraNeural` or `elevenlabs.josh`.
+The engine prefix is case-sensitive: a voice whose engine prefix SignalWire doesn't recognize, such
+as a capitalized one, falls back to the default voice. The [text-to-speech voices][tts] pages
+list each engine's voices and languages, such as [Azure][tts-azure] and [ElevenLabs][tts-elevenlabs].
+
+### Change the tone of the translation with filters
+
+A filter rewrites the translation in a tone you choose before it's spoken.
+`filter_from` applies to speech in `from_lang`, here what the rider says, and `filter_to` applies to
+speech in `to_lang`, here what Ada says. Use a preset or your own instruction:
+
+| Value | The translation is |
+|---|---|
+| `polite` | Stripped of insults, keeping the speaker's sentiment |
+| `rude` | Given insults, keeping the speaker's sentiment |
+| `professional` | Stripped of slang |
+| `shakespeare` | Rewritten in iambic pentameter |
+| `gen-z` | Rewritten in Gen Z slang |
+| `prompt:` | Rewritten as your instruction says, such as `prompt:Use formal business language` |
+
+Set `from_lang` and `to_lang` to the same language and add a filter to rewrite the tone of a call
+without translating it. Without a filter, a same-language session repeats each person's words in the
+synthesized voice.
+
+This change to the [first run's](#translate-a-call-via-swml) `start` action has the rider hear Ada
+in an Azure Spanish voice and Ada hear the rider in an ElevenLabs voice, and keeps the rider's words
+polite for Ada:
+
+
+
+```python {7-9}
+swml.answer().live_translate(action={
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "from_voice": "azure.es-ES-ElviraNeural",
+ "to_voice": "elevenlabs.josh",
+ "filter_from": "polite",
+ "live_events": True,
+ }
+}).connect(to="")
+```
+
+
+```typescript {8-10}
+swml.answer().live_translate({
+ action: {
+ start: {
+ from_lang: "es-ES",
+ to_lang: "en-US",
+ direction: ["local-caller", "remote-caller"],
+ webhook: "",
+ from_voice: "azure.es-ES-ElviraNeural",
+ to_voice: "elevenlabs.josh",
+ filter_from: "polite",
+ live_events: true,
+ },
+ },
+}).connect({ to: "" });
+```
+
+
+```yaml {9-11}
+- live_translate:
+ action:
+ start:
+ from_lang: es-ES
+ to_lang: en-US
+ direction:
+ - local-caller
+ - remote-caller
+ from_voice: azure.es-ES-ElviraNeural
+ to_voice: elevenlabs.josh
+ filter_from: polite
+ webhook:
+ live_events: true
+```
+
+
+```json {7-9}
+{
+ "live_translate": {
+ "action": {
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "from_voice": "azure.es-ES-ElviraNeural",
+ "to_voice": "elevenlabs.josh",
+ "filter_from": "polite",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": true
+ }
+ }
+ }
+}
+```
+
+
+
+### Tune speech detection for your callers
+
+`speech_engine` picks the speech recognizer: `deepgram`, the default, or
+`google`. When the recognizer splits utterances too early or too late for your callers, tune
+`vad_silence_ms`, the silence in milliseconds that ends an utterance, `700` by default with Deepgram
+and `500` with Google. `vad_thresh`, `400` by default, sets how loud audio must be to count as
+speech.
+
+## Get AI summaries of the translated conversation
+
+Live translation can hand the conversation to an AI model and send you a summary of each
+conversation log, either once when the session ends or whenever you ask while the call is up.
+
+### Summarize the call when translation ends
+
+Add `ai_summary: true` to `start`, and optionally `ai_summary_prompt` to say what the summary should
+contain. The event sent when the session ends then includes `primary_conversation_summary` and
+`secondary_conversation_summary`. This change to the [first run's](#translate-a-call-via-swml)
+`start` action works the same way in Relay and REST:
+
+
+
+```python {8-9}
+swml.answer().live_translate(action={
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": True,
+ "ai_summary": True,
+ "ai_summary_prompt": "List the pickup address, the destination, and the pickup time.",
+ }
+}).connect(to="")
+```
+
+
+```typescript {9-10}
+swml.answer().live_translate({
+ action: {
+ start: {
+ from_lang: "es-ES",
+ to_lang: "en-US",
+ direction: ["local-caller", "remote-caller"],
+ webhook: "",
+ live_events: true,
+ ai_summary: true,
+ ai_summary_prompt: "List the pickup address, the destination, and the pickup time.",
+ },
+ },
+}).connect({ to: "" });
+```
+
+
+```yaml {11-12}
+- live_translate:
+ action:
+ start:
+ from_lang: es-ES
+ to_lang: en-US
+ direction:
+ - local-caller
+ - remote-caller
+ webhook:
+ live_events: true
+ ai_summary: true
+ ai_summary_prompt: List the pickup address, the destination, and the pickup time.
+```
+
+
+```json {10-11}
+{
+ "live_translate": {
+ "action": {
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "",
+ "live_events": true,
+ "ai_summary": true,
+ "ai_summary_prompt": "List the pickup address, the destination, and the pickup time."
+ }
+ }
+ }
+}
+```
+
+
+
+After the call ends, the translation server prints each summary on a `Summary:` line.
+
+### Summarize a call in progress via WebSocket (Relay)
+
+Send the `summarize` action while translation is running to get a summary of the conversation so
+far, and keep translating. Its `prompt` and `webhook` are optional. An empty `summarize` object
+sends the summary to the webhook from `start` and uses the default summary prompt. In the
+[Relay handler](#start-translation-on-a-live-call-via-websocket-relay), add it wherever your app
+needs a summary. The REST client sends the same action to a call by ID:
+
+
+
+```python
+await call.live_translate({
+ "summarize": {"prompt": "Summarize the ride request in one sentence."}
+})
+```
+
+
+```typescript
+await call.liveTranslate({
+ summarize: { prompt: "Summarize the ride request in one sentence." },
+});
+```
+
+
+```python
+client.calling.live_translate(
+ "",
+ action={"summarize": {"prompt": "Summarize the ride request in one sentence."}},
+)
+```
+
+
+```typescript
+await client.calling.liveTranslate("", {
+ summarize: { prompt: "Summarize the ride request in one sentence." },
+});
+```
+
+
+```bash
+curl -X POST "https://.signalwire.com/api/calling/calls" \
+ -u ":" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "command": "calling.live_translate",
+ "id": "",
+ "params": {
+ "action": {
+ "summarize": { "prompt": "Summarize the ride request in one sentence." }
+ }
+ }
+ }'
+```
+
+
+
+When the summaries arrive, the translation server prints each one on a `Summary so far:` line.
+The cURL request goes to [Call commands][call-commands]. A SWML document can send the same action as
+a `live_translate` step.
+
+## Stop call translation
+
+A session stops by itself when the call ends. Stop it earlier with the `stop` action, which stops
+both sides at once. SignalWire then sends the
+[event for the end of the session](#read-translation-events-on-your-webhook) with everything
+translated until that point.
+
+### Stop call translation via SWML
+
+Set `action` to the string `stop`:
+
+
+
+```python
+swml.live_translate(action="stop")
+```
+
+
+```typescript
+swml.live_translate({ action: "stop" });
+```
+
+
+```yaml
+- live_translate:
+ action: stop
+```
+
+
+```json
+{ "live_translate": { "action": "stop" } }
+```
+
+
+
+### Stop call translation via WebSocket (Relay)
+
+The Relay samples send `stop` as a key with an empty object, and the REST samples send the string
+`stop`. The platform accepts either form:
+
+
+
+```python
+await call.live_translate({"stop": {}})
+```
+
+
+```typescript
+await call.liveTranslate({ stop: {} });
+```
+
+
+```python
+client.calling.live_translate("", action="stop")
+```
+
+
+```typescript
+await client.calling.liveTranslate("", "stop");
+```
+
+
+```bash
+curl -X POST "https://.signalwire.com/api/calling/calls" \
+ -u ":" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "command": "calling.live_translate",
+ "id": "",
+ "params": { "action": "stop" }
+ }'
+```
+
+
+
+To translate again, send a new `start` action.
+
+## Troubleshoot call translation
+
+Find the symptom you see:
+
+- **Each phone hears the other's own voice, untranslated.** Translation didn't start. Check that
+ `start` has `from_lang`, `to_lang`, and `direction`. A SWML document carries on with the call
+ when `start` is missing one of them, so the call connects without translation.
+- **The translations make no sense, or come out in the wrong language.** Each person is speaking the
+ other's language. Check the order of `direction` against
+ [How call translation works](#how-call-translation-works): with `local-caller` first, the caller
+ speaks `from_lang`.
+- **You hear a different voice from the one you set.** SignalWire didn't recognize the voice and used
+ the default. Check that the engine name is lowercase, as in `azure.es-ES-ElviraNeural`.
+- **The call connects and translates, but your server prints nothing.** SignalWire couldn't reach
+ your webhook. Check that the URL begins with `https://` and ends with `/translations`, and that
+ the tunnel is running and points at port `8080`.
+- **The server prints the end of the session, but no utterances.** `live_events` isn't `true` on
+ `start`. Without it, the conversation arrives only when the session ends.
+- **You hear an error or silence when you call your number.** SignalWire couldn't fetch your SWML.
+ Rerun the `curl` check with the exact URL in the SWML Script, and confirm that the server and the
+ tunnel are still running. A `401` means the password in the URL doesn't match the server's.
+- **You hear your number's old behavior.** The number still points at another Resource. Open it
+ under **Phone Numbers** and check **Inbound Call Settings**.
+- **The second phone never rings.** Check that `` is in E.164 format, and in a
+ trial project, that it's a verified number.
+
+## Next steps
+
+
+
+ Every option for starting, summarizing, and stopping live translation
+
+
+ Turn a call into text in one language and summarize it with AI
+
+
+ Have an AI agent talk with callers in their own language
+
+
+ Find a voice for each language you translate into
+
+
diff --git a/fern/products/server-sdks/pages/reference/python/relay/call/live-translate.mdx b/fern/products/server-sdks/pages/reference/python/relay/call/live-translate.mdx
index 6fe9673927..0811ab1f0f 100644
--- a/fern/products/server-sdks/pages/reference/python/relay/call/live-translate.mdx
+++ b/fern/products/server-sdks/pages/reference/python/relay/call/live-translate.mdx
@@ -1,38 +1,144 @@
---
title: "live_translate"
slug: reference/python/relay/call/live-translate
-description: Start or stop live translation on a call.
+description: Start, summarize, or stop live translation on a call.
max-toc-depth: 3
---
-Start or stop live translation on the call. Translates spoken audio into
-another language in real-time.
+[swml-live-translate]: /docs/swml/reference/calling/live-translate
+[translate-calls]: /docs/platform/voice/translate-calls
+
+Start, summarize, or stop live translation on the call. Live translation recognizes what each
+party says, translates it, and speaks the translation to the other party in place of the original
+audio. The `action` dictionary takes the same shape as the [`live_translate`][swml-live-translate]
+SWML method: a single `start`, `summarize`, `inject`, or `stop` key.
+
+
+See the [translate calls guide][translate-calls] for which language each party speaks and for the
+webhook events.
+
## **Parameters**
- Action configuration for the live translation.
+ The action to perform. Set exactly one of `start`, `summarize`, `inject`, or `stop`.
-
- The action to perform.
- - `"start"` -- begin live translation
- - `"stop"` -- end live translation
+
+ Start a live translation session.
+
+
+
+
+
+ One of the two languages of the conversation, such as `"es-ES"`. `from_voice` speaks it.
+
+
+
+ The other language of the conversation, such as `"en-US"`. `to_voice` speaks it.
+
+
+
+ The sides of the call to translate: `"remote-caller"`, `"local-caller"`, or both. The order
+ decides which side speaks which language.
+
+
+
+ The URL that receives translation events as HTTP `POST` requests.
+
+
+
+ The voice that speaks translations into `from_lang`, as `.` with a lowercase
+ engine name, such as `"azure.es-ES-ElviraNeural"`.
+
+
+
+ The voice that speaks translations into `to_lang`, such as `"elevenlabs.josh"`.
+
+
+
+ A tone filter for speech in `from_lang`: `"polite"`, `"rude"`, `"professional"`,
+ `"shakespeare"`, `"gen-z"`, or a custom instruction prefixed with `"prompt:"`.
+
+
+
+ A tone filter for speech in `to_lang`, with the same values as `filter_from`.
+
+
+
+ Whether to send each translated utterance to the webhook as it is spoken.
+
+
+
+ Whether to add AI summaries to the event sent to the webhook when the session ends.
+
+
+
+ The prompt that tells the AI how to summarize the conversation when `ai_summary` is enabled.
+
+
+
+ The speech recognition engine: `"deepgram"` or `"google"`.
-
- Source language code (e.g., `"en-US"`).
+
+ The timeout for speech recognition, in milliseconds.
-
- Target language code (e.g., `"es-ES"`).
+
+ The silence, in milliseconds, that ends an utterance. Defaults to `700` with Deepgram and `500`
+ with Google.
+
+
+ Voice activity detection threshold, from `0` to `1800`.
+
+
+
+
+
+ Request an AI summary of the conversation so far, while translation continues. Pass an empty
+ dictionary to send the summary to the webhook from `start` with the default summary prompt.
+
+
+
+
+
+ The URL that receives the summary.
+
+
+
+ The prompt that tells the AI how to summarize the conversation.
+
+
+
+
+
+ Insert a message into the session, to be translated and spoken on the call.
+
+
+
+
+
+ The message to translate and speak.
+
+
+
+ The side of the call: `"remote-caller"` or `"local-caller"`.
+
+
+
+
+
+ Stop the live translation session on both sides. Pass an empty dictionary.
+
+
- URL to receive translation results via webhook.
+ A status URL sent with the command. Translation events go to `action.start.webhook`.
## **Returns**
@@ -41,7 +147,7 @@ another language in real-time.
## **Example**
-```python {14}
+```python {14-21}
from signalwire.relay import RelayClient
client = RelayClient(
@@ -54,14 +160,17 @@ client = RelayClient(
async def handle_call(call):
await call.answer()
- # Start live translation from English to Spanish
+ # Start live translation between Spanish and English
await call.live_translate({
- "action": "start",
- "source_language": "en-US",
- "target_language": "es-ES",
+ "start": {
+ "from_lang": "es-ES",
+ "to_lang": "en-US",
+ "direction": ["local-caller", "remote-caller"],
+ "webhook": "https://example.com/translation-events",
+ }
})
- # Let the call proceed...
+ # Connect the caller to the other party, then let the call proceed...
await call.wait_for_ended()
client.run()
diff --git a/fern/products/server-sdks/pages/reference/python/rest/calling/live-translate.mdx b/fern/products/server-sdks/pages/reference/python/rest/calling/live-translate.mdx
index 648125089f..991658d834 100644
--- a/fern/products/server-sdks/pages/reference/python/rest/calling/live-translate.mdx
+++ b/fern/products/server-sdks/pages/reference/python/rest/calling/live-translate.mdx
@@ -1,12 +1,13 @@
---
title: "live_translate"
slug: reference/python/rest/calling/live-translate
-description: Start live translation on an active call via REST.
+description: Start, summarize, or stop live translation on an active call via REST.
max-toc-depth: 3
---
-Start live translation on an active call. Spoken audio is transcribed and
-translated in real time to a target language.
+Start, summarize, or stop live translation on an active call. Spoken audio is transcribed and
+translated in real time, and each party hears the other in their own language. Pass the action
+as `action`: a `start`, `summarize`, or `inject` object, or the string `"stop"`.
## **Request**
diff --git a/fern/products/server-sdks/pages/reference/typescript/relay/call/live-translate.mdx b/fern/products/server-sdks/pages/reference/typescript/relay/call/live-translate.mdx
index fac4fdccb1..ae70636089 100644
--- a/fern/products/server-sdks/pages/reference/typescript/relay/call/live-translate.mdx
+++ b/fern/products/server-sdks/pages/reference/typescript/relay/call/live-translate.mdx
@@ -1,47 +1,161 @@
---
title: "liveTranslate"
slug: reference/typescript/relay/call/live-translate
-description: Start or stop live translation on a call.
+description: Start, summarize, or stop live translation on a call.
max-toc-depth: 3
---
-Start or stop live translation on the call. Translates spoken audio into
-another language in real-time.
+[swml-live-translate]: /docs/swml/reference/calling/live-translate
+[translate-calls]: /docs/platform/voice/translate-calls
+
+Start, summarize, or stop live translation on the call. Live translation recognizes what each
+party says, translates it, and speaks the translation to the other party in place of the original
+audio. The `action` object takes the same shape as the [`live_translate`][swml-live-translate]
+SWML method: a single `start`, `summarize`, `inject`, or `stop` key.
+
+
+See the [translate calls guide][translate-calls] for which language each party speaks and for the
+webhook events.
+
## **Parameters**
- Action configuration for the live translation.
+ The action to perform. Set exactly one of `start`, `summarize`, `inject`, or `stop`.
+
+
+
+
+
+ Start a live translation session.
-
- The action to perform.
- - `"start"` -- begin live translation
- - `"stop"` -- end live translation
+
+ One of the two languages of the conversation, such as `"es-ES"`. `from_voice` speaks it.
+
+
+
+ The other language of the conversation, such as `"en-US"`. `to_voice` speaks it.
+
+
+
+ The sides of the call to translate: `"remote-caller"`, `"local-caller"`, or both. The order
+ decides which side speaks which language.
+
+
+
+ The URL that receives translation events as HTTP `POST` requests.
+
+
+
+ The voice that speaks translations into `from_lang`, as `.` with a lowercase
+ engine name, such as `"azure.es-ES-ElviraNeural"`.
+
+
+
+ The voice that speaks translations into `to_lang`, such as `"elevenlabs.josh"`.
-
- Source language code (e.g., `"en-US"`).
+
+ A tone filter for speech in `from_lang`: `"polite"`, `"rude"`, `"professional"`,
+ `"shakespeare"`, `"gen-z"`, or a custom instruction prefixed with `"prompt:"`.
-
- Target language code (e.g., `"es-ES"`).
+
+ A tone filter for speech in `to_lang`, with the same values as `filter_from`.
+
+
+ Whether to send each translated utterance to the webhook as it is spoken.
+
+
+
+ Whether to add AI summaries to the event sent to the webhook when the session ends.
+
+
+
+ The prompt that tells the AI how to summarize the conversation when `ai_summary` is enabled.
+
+
+
+ The speech recognition engine: `"deepgram"` or `"google"`.
+
+
+
+ The timeout for speech recognition, in milliseconds.
+
+
+
+ The silence, in milliseconds, that ends an utterance. Defaults to `700` with Deepgram and `500`
+ with Google.
+
+
+
+ Voice activity detection threshold, from `0` to `1800`.
+
+
-
- URL to receive translation results via webhook.
+
+ Request an AI summary of the conversation so far, while translation continues. Pass an empty
+ object to send the summary to the webhook from `start` with the default summary prompt.
+
+
+
+ The URL that receives the summary.
+
+
+
+ The prompt that tells the AI how to summarize the conversation.
+
+
+
+
+
+ Insert a message into the session, to be translated and spoken on the call.
+
+
+
+
+
+ The message to translate and speak.
+
+
+
+ The side of the call: `"remote-caller"` or `"local-caller"`.
+
+
+
+
+
+ Stop the live translation session on both sides. Pass an empty object.
+
+
+
+
+
+ Optional settings for the command.
+
+
+
+
+
+ A status URL sent with the command. Translation events go to `action.start.webhook`.
+
+
+
+
## **Returns**
`Promise>` -- Server response confirming the operation.
## **Example**
-```typescript {13}
+```typescript {13-20}
import { RelayClient } from '@signalwire/sdk';
const client = new RelayClient({
@@ -53,14 +167,17 @@ const client = new RelayClient({
client.onCall(async (call) => {
await call.answer();
- // Start live translation from English to Spanish
+ // Start live translation between Spanish and English
await call.liveTranslate({
- action: 'start',
- source_language: 'en-US',
- target_language: 'es-ES',
+ start: {
+ from_lang: 'es-ES',
+ to_lang: 'en-US',
+ direction: ['local-caller', 'remote-caller'],
+ webhook: 'https://example.com/translation-events',
+ },
});
- // Let the call proceed...
+ // Connect the caller to the other party, then let the call proceed...
await call.waitForEnded();
});
diff --git a/fern/products/server-sdks/pages/reference/typescript/rest/calling/live-translate.mdx b/fern/products/server-sdks/pages/reference/typescript/rest/calling/live-translate.mdx
index 3f06aeea0e..fca6b280d9 100644
--- a/fern/products/server-sdks/pages/reference/typescript/rest/calling/live-translate.mdx
+++ b/fern/products/server-sdks/pages/reference/typescript/rest/calling/live-translate.mdx
@@ -1,12 +1,13 @@
---
title: "liveTranslate"
slug: reference/typescript/rest/calling/live-translate
-description: Start live translation on an active call via REST.
+description: Start, summarize, or stop live translation on an active call via REST.
max-toc-depth: 3
---
-Start live translation on an active call. Spoken audio is transcribed and
-translated in real time to a target language.
+Start, summarize, or stop live translation on an active call. Spoken audio is transcribed and
+translated in real time, and each party hears the other in their own language. Pass the action
+as the second argument: a `start`, `summarize`, or `inject` object, or the string `"stop"`.
## **Request**
@@ -18,7 +19,7 @@ translated in real time to a target language.
## **Example**
-```typescript {9}
+```typescript {9-17}
import { RestClient } from "@signalwire/sdk";
const client = new RestClient({
diff --git a/fern/products/swml/pages/reference/methods/calling/live_translate/index.mdx b/fern/products/swml/pages/reference/methods/calling/live_translate/index.mdx
index 8423a3f2f5..722ef6f570 100644
--- a/fern/products/swml/pages/reference/methods/calling/live_translate/index.mdx
+++ b/fern/products/swml/pages/reference/methods/calling/live_translate/index.mdx
@@ -7,6 +7,7 @@ max-toc-depth: 3
---
Start live translation of the call. The translation will be sent to the specified webhook URL.
+The [translate calls guide](/docs/platform/voice/translate-calls) walks through a first call, which language each party speaks, the webhook events, and AI summaries.
## **Properties**