{
  "name": "Compare two TTS samples with Speech Expert",
  "nodes": [
    {
      "id": "d3eea7b7-a8b0-5f38-9abc-fa87ad70acd5",
      "name": "Workflow guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -900
      ],
      "parameters": {
        "content": "## Compare two TTS samples with Speech Expert\n### How it works\nUpload two complete synthesized clips and their exact intended text. Speech Expert listens to variants A and B, describes pronunciation, prosody, text fidelity and audible artifacts, and returns a structured comparison with a preference and limitations. This is a subjective model assessment, not measured MOS.\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\nBoth source import and analysis use your API key, without portal cookies. Uploaded sources stay in your account archive. Preview retrieves the current price; analysis runs only if that price fits your maximum credits. The exact quote is then used as the server-side spending cap. Analysis is synchronous and can take several minutes.\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": "b6ec2b59-8090-56ba-bcd8-2a42b71ebbef",
      "name": "Input form",
      "type": "n8n-nodes-base.formTrigger",
      "typeVersion": 2.4,
      "position": [
        0,
        0
      ],
      "parameters": {
        "authentication": "basicAuth",
        "formTitle": "Compare two TTS samples with Speech Expert",
        "formDescription": "Sources are saved in your Speech Expert account. The current quote must fit your credit cap before analysis starts.",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Variant A",
              "fieldName": "audioA",
              "fieldType": "file",
              "requiredField": true,
              "multipleFiles": false,
              "acceptFileTypes": ".wav,.mp3,.m4a,.ogg,.flac,.mp4,.webm"
            },
            {
              "fieldLabel": "Variant B",
              "fieldName": "audioB",
              "fieldType": "file",
              "requiredField": true,
              "multipleFiles": false,
              "acceptFileTypes": ".wav,.mp3,.m4a,.ogg,.flac,.mp4,.webm"
            },
            {
              "fieldLabel": "Exact intended text (same for A and B)",
              "fieldName": "targetText",
              "fieldType": "textarea",
              "requiredField": true
            },
            {
              "fieldLabel": "Optional language, register or dialect context",
              "fieldName": "speechContext",
              "fieldType": "textarea",
              "requiredField": false
            },
            {
              "fieldLabel": "Maximum credits for this analysis",
              "fieldName": "maxCredits",
              "fieldType": "number",
              "requiredField": true
            }
          ]
        },
        "responseMode": "onReceived",
        "options": {}
      },
      "webhookId": "08a0cda4-a8fa-5872-ad6d-cd4dfe779f70"
    },
    {
      "id": "6c1562c7-1a03-562c-888e-54b1b380c2f6",
      "name": "Validate input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "\nconst i = $input.first(), f = i.json;\nif (f.maxCredits === '' || f.maxCredits == null || !Number.isFinite(Number(f.maxCredits)) || Number(f.maxCredits) < 0)\n  throw new Error('Enter a non-negative maximum credit amount.');\n// Random UUID per source, retained in this node's output for retries of that import.\n// This is an idempotency label, not a secret. No sandbox module imports are needed.\nconst sourceId = () => 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {\n  const r = Math.floor(Math.random()*16);\n  return (c === 'x' ? r : (r & 3) | 8).toString(16);\n});\n\nif (!i.binary?.audioA || !i.binary?.audioB) throw new Error('Upload both variant A and variant B.');\nconst targetText = String(f.targetText || '').trim(), speechContext = String(f.speechContext || '').trim();\nif (!targetText || targetText.length > 4000 || speechContext.length > 1000) throw new Error('Target text: 1–4000 characters; context: up to 1000.');\nreturn [{json:{maxCredits:Number(f.maxCredits),targetText,speechContext,presetId:'tts_ab_quality',\n  requestIdA:sourceId(),requestIdB:sourceId(),pairId:'n8n-' + $execution.id},binary:i.binary}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "b33ebec2-2a3c-563b-856f-3d04a36de531",
      "name": "Upload A",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        440,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://speech-expert.ru/api/creator-research/sources",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 300000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "audio",
              "inputDataFieldName": "audioA"
            },
            {
              "name": "requestId",
              "value": "={{ $json.requestIdA }}"
            },
            {
              "name": "presetId",
              "value": "tts_ab_quality"
            },
            {
              "name": "name",
              "value": "n8n TTS comparison A"
            }
          ]
        }
      },
      "retryOnFail": false,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "6b3973ca-c8ed-5b03-b3d8-d46ba03f3e35",
      "name": "Prepare variant B",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        660,
        0
      ],
      "parameters": {
        "jsCode": "\nconst a = $input.first().json;\nif (!a.id || !Number.isInteger(a.duration_ms) || a.duration_ms < 1 || a.duration_ms > 120000) throw new Error('Variant A must be a complete file of up to 120 seconds.');\nconst input = $('Validate input').first();\nreturn [{json:input.json,binary:input.binary}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "2ffbfe86-c5b3-5d8c-aa38-394225844f63",
      "name": "Upload B",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        880,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://speech-expert.ru/api/creator-research/sources",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 300000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "audio",
              "inputDataFieldName": "audioB"
            },
            {
              "name": "requestId",
              "value": "={{ $json.requestIdB }}"
            },
            {
              "name": "presetId",
              "value": "tts_ab_quality"
            },
            {
              "name": "name",
              "value": "n8n TTS comparison B"
            }
          ]
        }
      },
      "retryOnFail": false,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "6ac910ea-c9ce-5408-95e5-d8f062466342",
      "name": "Build analysis request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1100,
        0
      ],
      "parameters": {
        "jsCode": "\nconst a = $('Upload A').first().json, b = $input.first().json, f = $('Validate input').first().json;\nif (!b.id || a.id === b.id || !Number.isInteger(b.duration_ms) || b.duration_ms < 1 || b.duration_ms > 120000) throw new Error('Variant B must be a distinct complete file of up to 120 seconds.');\nreturn [{json:{recordingId:a.id,comparisonRecordingId:b.id,presetId:'tts_ab_quality',startMs:0,endMs:a.duration_ms,\n  comparisonEndMs:b.duration_ms,pairId:f.pairId,targetText:f.targetText,speechContext:f.speechContext,evidenceMode:'none'}}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "72456de8-9ac1-5907-8736-e9e950040fa6",
      "name": "Preview price",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1320,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://speech-expert.ru/api/creator-research/preview",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 60000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}"
      },
      "retryOnFail": false,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "29191443-196d-5ada-a8c0-ed26a8602c1d",
      "name": "Check credit cap",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1540,
        0
      ],
      "parameters": {
        "jsCode": "\nconst quote = $input.first().json.billing?.credits;\nconst cap = $('Validate input').first().json.maxCredits;\nif (quote == null || quote === '' || !Number.isFinite(Number(quote)) || Number(quote) < 0) throw new Error('Preview did not return a valid price. Analysis was not started.');\nif (Number(quote) > cap) throw new Error('Quote ' + quote + ' credits exceeds your cap of ' + cap + '. Analysis was not started.');\nreturn [{json:{...$('Build analysis request').first().json,maxCredits:String(quote)}}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "7e65b6dc-615f-5982-8113-f3f19fc7c3d9",
      "name": "Analyze audio",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1760,
        0
      ],
      "parameters": {
        "method": "POST",
        "url": "https://speech-expert.ru/api/creator-research/analyze",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 600000,
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ $json }}"
      },
      "retryOnFail": false,
      "maxTries": 2,
      "waitBetweenTries": 1000,
      "onError": "continueErrorOutput"
    },
    {
      "id": "e3137c6c-0098-5844-a38e-c10647a6508b",
      "name": "Build research report",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2200,
        0
      ],
      "parameters": {
        "jsCode": "\nconst r = $input.first().json;\nif (r.status !== 'ready' || !r.payload || typeof r.payload !== 'object') throw new Error('Analysis did not return a ready report.');\nreturn [{json:{report:JSON.stringify(r,null,2)}}];\n"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "bb04675f-1601-56d4-831f-b8cb19a4b0a3",
      "name": "Import sources",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -220
      ],
      "parameters": {
        "content": "### Upload through the API\nEach source gets its own requestId. Uploaded audio remains in the account archive. A/B comparison uses two entire files of up to 120 seconds each. Ordinary research uses one selected 20–120 second interval.",
        "width": 1050,
        "height": 180,
        "color": 7
      }
    },
    {
      "id": "a8716de4-b042-5e98-a988-1fcde432ccb2",
      "name": "Quote and research",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1100,
        -220
      ],
      "parameters": {
        "content": "### Enforce the budget\nPreview is not analysis. Check its current price against the form cap, then submit that exact quote as maxCredits. Do not automatically retry analysis on a timeout: inspect the saved recording results first.",
        "width": 1050,
        "height": 180,
        "color": 7
      }
    },
    {
      "id": "57c78af0-6caf-5b4a-8f84-54f85f02f98d",
      "name": "Save result file",
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        2420,
        0
      ],
      "parameters": {
        "operation": "toText",
        "sourceProperty": "report",
        "binaryPropertyName": "data",
        "options": {
          "fileName": "speech-expert-tts-comparison.json",
          "encoding": "utf8"
        }
      }
    },
    {
      "id": "b811158e-760f-51e1-afb0-ab1ff3641935",
      "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": "bfb91429-a98c-5f13-a416-979eade6f8de",
      "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": "cdb44ae1-7371-579b-9493-a36cec84f8fa",
      "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": "a80582e5-31cd-5647-af86-4c4aec01b1fc",
      "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": "Upload A",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload A": {
      "main": [
        [
          {
            "node": "Prepare variant B",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare variant B": {
      "main": [
        [
          {
            "node": "Upload B",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload B": {
      "main": [
        [
          {
            "node": "Build analysis request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build analysis request": {
      "main": [
        [
          {
            "node": "Preview price",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Preview price": {
      "main": [
        [
          {
            "node": "Check credit cap",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check credit cap": {
      "main": [
        [
          {
            "node": "Analyze audio",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze audio": {
      "main": [
        [
          {
            "node": "Build research report",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build research report": {
      "main": [
        [
          {
            "node": "Save result file",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Prepare error report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save result file": {
      "main": [
        [
          {
            "node": "Download result",
            "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": []
}
