{
  "name": "Turn text into downloadable speech with Speech Expert",
  "nodes": [
    {
      "id": "2a440653-47fd-5e7e-9cdc-4b70d9ce8d80",
      "name": "Workflow guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -900
      ],
      "parameters": {
        "content": "## Turn text into downloadable speech with Speech Expert\n### How it works\nEnter text and a catalogue voice name. Speech Expert creates an asynchronous speech operation. The workflow waits for a complete result and downloads a WAV file. Cancelled, failed, and partial operations are never presented as successful full synthesis.\n\n### Setup\nImport this inactive workflow into n8n. Create a Header Auth credential named Speech Expert: set the header name to `Authorization` and its value to `Api-Key YOUR_KEY`. Select it in every HTTP Request node. Create a separate Basic Auth credential and select it in Input form so only intended users can spend your API balance. Keep credentials out of workflow JSON and screenshots. Run the test form before publishing a production form.\n\nThe default is Russian speech, the pavel-higgs catalogue voice, speed 1.0, and Higgs synthesis. You can select eleven_v4 for voices supporting that engine. Obtain exact voice names and supported engines from GET /api/tts/v1/voices; provider voice IDs are not catalogue names.\n\n### Customization\nReplace the form with your existing workflow input while preserving binary field names. The API host is fixed deliberately; only send credentials to your trusted Speech Expert endpoint. Review your n8n file-size, binary-storage and execution-retention settings. Errors return a downloadable report. Paid creation requests are never automatically retried. See [setup and API details](https://speech-expert.ru/documentation/integrations/n8n/).\n",
        "width": 980,
        "height": 650,
        "color": 1
      }
    },
    {
      "id": "79af96bf-ce8c-5ea3-bc26-9319b77c3a55",
      "name": "Input form",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.4,
      "position": [
        0,
        0
      ],
      "parameters": {
        "authentication": "basicAuth",
        "formTitle": "Turn text into downloadable speech with Speech Expert",
        "formDescription": "Synthesize text into WAV using your Speech Expert balance. Use an exact catalogue voice name.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Text to speak",
              "fieldName": "text",
              "fieldType": "textarea",
              "requiredField": true
            },
            {
              "fieldLabel": "Catalogue voice name (blank: pavel-higgs)",
              "fieldName": "voice",
              "fieldType": "text",
              "requiredField": false
            },
            {
              "fieldLabel": "Synthesis engine",
              "fieldName": "engine",
              "fieldType": "dropdown",
              "requiredField": false,
              "fieldOptions": {
                "values": [
                  {
                    "option": "higgs"
                  },
                  {
                    "option": "eleven_v4"
                  }
                ]
              }
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {}
      },
      "webhookId": "0590e986-3ce1-54f3-8485-6e78acbec690"
    },
    {
      "id": "c793a25e-a22f-58e1-8589-33b06609572f",
      "name": "Validate input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "\nconst f = $input.first().json;\nconst text = String(f.text || '').trim();\nif (!text || text.length > 40000) throw new Error('Text must contain 1–40000 characters.');\nconst engine = f.engine || 'higgs';\nif (!['higgs','eleven_v4'].includes(engine)) throw new Error('Unsupported synthesis engine.');\nreturn [{json:{text,voice:String(f.voice || 'pavel-higgs').trim(),engine}}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "9a7d3706-855a-5c50-811b-6010a0f5931f",
      "name": "Create synthesis",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://speech-expert.ru/api/tts/v1/operations",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "={{ $json.text }}"
            },
            {
              "name": "voice",
              "value": "={{ $json.voice }}"
            },
            {
              "name": "synthesisEngine",
              "value": "={{ $json.engine }}"
            },
            {
              "name": "lang",
              "value": "ru-RU"
            },
            {
              "name": "speed",
              "value": "1.0"
            },
            {
              "name": "sampleRateHertz",
              "value": "48000"
            }
          ]
        }
      },
      "retryOnFail": false,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "868c35ce-4a6a-5d2a-a11a-dd4a260eef91",
      "name": "Track operation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "jsCode": "\nconst o = $input.first().json;\nif (o.error || typeof o.id !== 'string' || !o.id.trim()) throw new Error('Invalid operation response: ' + JSON.stringify(o.error || o));\nreturn [{json:{operationId:o.id,deadline:Date.now()+10*60*1000}}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "c5628755-e0ee-520e-a8ee-d415a0faba5c",
      "name": "Wait before polling",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        880,
        0
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 5,
        "unit": "seconds"
      },
      "webhookId": "85a93541-ebd2-5c7c-ae98-b98eae4a2f7e"
    },
    {
      "id": "20c4bb2b-1fcf-5890-b8ea-8bed1593bc88",
      "name": "Read operation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://speech-expert.ru/api/tts/v1/operations/' + encodeURIComponent($('Track operation').first().json.operationId) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "a81fa772-a846-5779-b24d-223f1fd2367a",
      "name": "Inspect operation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "jsCode": "\nconst o = $input.first().json;\nconst t = $('Track operation').first().json;\nif (o.error) throw new Error('Operation ' + t.operationId + ': ' + JSON.stringify(o.error));\nif (o.id !== t.operationId || typeof o.done !== 'boolean') throw new Error('Invalid status response for ' + t.operationId);\nif (!o.done && (Date.now() >= t.deadline || $runIndex >= 119))\n  throw new Error('Polling limit reached. Operation ' + t.operationId + ' may still be running; query its status before resubmitting.');\n\nif (o.done && (o.metadata?.status !== 'completed' || o.metadata?.result_available !== true))\n  throw new Error('TTS is not a complete result: ' + o.metadata?.status + ' (operation ' + t.operationId + ')');\n\nreturn [{json:o}];"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "2eaaaaa8-1526-5375-ae61-7ea319288935",
      "name": "Operation finished?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "done",
              "leftValue": "={{ $json.done }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "8ec0eff1-c5f0-5cbc-b2c6-f194083a7f58",
      "name": "Bounded polling",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        850,
        -220
      ],
      "parameters": {
        "content": "### Wait for completion\nPoll every five seconds for up to ten minutes or 120 responses. HTTP GET can retry once. A timeout does not cancel the paid operation; the error report keeps its ID for recovery.",
        "width": 920,
        "height": 180,
        "color": 7
      }
    },
    {
      "id": "00f66ff4-1291-5a0e-a56c-4fe5aba74ff5",
      "name": "Download WAV",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2200,
        0
      ],
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://speech-expert.ru/api/tts/v1/operations/' + encodeURIComponent($('Track operation').first().json.operationId) + '/audio' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 300000,
          "response": {
            "response": {
              "responseFormat": "file",
              "outputPropertyName": "data"
            }
          }
        }
      },
      "retryOnFail": true,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "0f8e044a-e1a2-5eb4-8e0c-0a48865bd1d3",
      "name": "Text and voice",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -220
      ],
      "parameters": {
        "content": "### Create speech\nUse an exact voice name from the authenticated voices catalogue. Higgs is the default; eleven_v4 requires a compatible voice. The resulting WAV is downloaded immediately because operation audio has limited retention.",
        "width": 760,
        "height": 180,
        "color": 7
      }
    },
    {
      "id": "c5c528bd-eea4-595d-a573-d76729fb258f",
      "name": "Download result",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.4,
      "position": [
        2640,
        0
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "returnBinary",
        "completionTitle": "Your result is ready",
        "completionMessage": "Download the result below.",
        "inputDataFieldName": "data",
        "options": {}
      }
    },
    {
      "id": "85b9bcfc-bb34-5019-bc97-73bfcd150285",
      "name": "Prepare error report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1980,
        520
      ],
      "parameters": {
        "jsCode": "let operationId = null; try { operationId = $('Track operation').first(0).json.operationId ?? null; } catch {}\nif (!operationId) for (const n of ['Create recognition', 'Create synthesis']) { try { const id = $(n).first(0).json.id; if (typeof id === 'string' && id) operationId = id; } catch {} }\nlet recordingIds = []; for (const n of ['Upload source', 'Upload A', 'Upload B']) { try { const id = $(n).first(0).json.id; if (id) recordingIds.push(id); } catch {} }\n\nconst e = $input.first().json;\nconst message = typeof e.error === 'string' ? e.error : (e.error?.message || e.message || 'Request failed. See the n8n execution for details.');\nreturn [{json:{report:JSON.stringify({status:'failed',message,operationId,recordingIds,\n  note:'No paid POST was retried automatically. An interrupted request may still be running. Check its status before submitting again.'},null,2)}}];\n"
      }
    },
    {
      "id": "5738fb8d-4467-5171-b8b1-771d2061f121",
      "name": "Save error report",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        2200,
        520
      ],
      "parameters": {
        "operation": "toText",
        "sourceProperty": "report",
        "binaryPropertyName": "data",
        "options": {
          "fileName": "speech-expert-error.json",
          "encoding": "utf8"
        }
      }
    },
    {
      "id": "a9580522-d09e-5e13-a286-86a4d331c6ea",
      "name": "Download error report",
      "type": "n8n-nodes-base.form",
      "typeVersion": 2.4,
      "position": [
        2420,
        520
      ],
      "parameters": {
        "operation": "completion",
        "respondWith": "returnBinary",
        "completionTitle": "The request did not finish",
        "completionMessage": "Download the error details. Check an existing operation before retrying.",
        "inputDataFieldName": "data",
        "options": {}
      }
    }
  ],
  "connections": {
    "Input form": {
      "main": [
        [
          {
            "node": "Validate input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate input": {
      "main": [
        [
          {
            "node": "Create synthesis",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create synthesis": {
      "main": [
        [
          {
            "node": "Track operation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Track operation": {
      "main": [
        [
          {
            "node": "Wait before polling",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait before polling": {
      "main": [
        [
          {
            "node": "Read operation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read operation": {
      "main": [
        [
          {
            "node": "Inspect operation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inspect operation": {
      "main": [
        [
          {
            "node": "Operation finished?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Operation finished?": {
      "main": [
        [
          {
            "node": "Download WAV",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait before polling",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download WAV": {
      "main": [
        [
          {
            "node": "Download result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare error report": {
      "main": [
        [
          {
            "node": "Save error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save error report": {
      "main": [
        [
          {
            "node": "Download error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "tags": []
}
