{
  "name": "Speech Expert — голосовые заявки в Telegram (GigaAM v3)",
  "nodes": [
    {
      "id": "f55ae872-7944-5dc9-934a-478bf4f50de7",
      "name": "Telegram voice",
      "type": "n8n-nodes-base.telegramTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ],
      "parameters": {
        "updates": [
          "message"
        ],
        "additionalFields": {
          "download": false
        }
      },
      "webhookId": "787e4ada-7c91-56b2-9d30-5e998a3fa334"
    },
    {
      "id": "de84a4af-eadb-5ddc-956c-3c9114795936",
      "name": "Allow demo voice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        240,
        0
      ],
      "parameters": {
        "jsCode": "// Setup: add ONLY your own private Telegram chat ID as a quoted string.\n// Empty list intentionally rejects every update before downloading or spending credits.\nconst ALLOWED_CHAT_IDS = [];\nconst items = $input.all();\nif (items.length !== 1) return [];\nconst update = items[0].json;\nconst message = update.message;\nif (!message || message.chat?.type !== 'private' || message.from?.is_bot !== false) return [];\nconst chatId = String(message.chat.id ?? '');\nif (!/^[1-9][0-9]{0,15}$/.test(chatId) || !ALLOWED_CHAT_IDS.includes(chatId)) return [];\nif (String(message.from.id ?? '') !== chatId) return [];\nif (!Number.isSafeInteger(update.update_id) || !Number.isSafeInteger(message.message_id)) return [];\nconst voice = message.voice;\nif (!voice || typeof voice.file_id !== 'string' || !voice.file_id || voice.file_id.length > 1024) return [];\nif (!Number.isFinite(voice.duration) || voice.duration <= 0 || voice.duration > 120) return [];\nif (!Number.isSafeInteger(voice.file_size) || voice.file_size <= 0 || voice.file_size > 20 * 1024 * 1024) return [];\nreturn [{json:{chatId, messageId:message.message_id, updateId:update.update_id,\n  fileId:voice.file_id, declaredBytes:voice.file_size, durationSeconds:voice.duration,\n  startedAt:Date.now()}}];"
      }
    },
    {
      "id": "235f04c2-a8aa-559b-8ef1-83d6966313d0",
      "name": "Download voice",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        480,
        0
      ],
      "parameters": {
        "resource": "file",
        "operation": "get",
        "fileId": "={{ $json.fileId }}",
        "download": true,
        "additionalFields": {}
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "591c5359-1c27-5470-9658-493513d0cc85",
      "name": "Validate downloaded voice",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        720,
        0
      ],
      "parameters": {
        "jsCode": "const source = $('Allow demo voice').first().json;\nconst items = $input.all();\nif (items.length !== 1 || !items[0].binary?.data) throw new Error('Voice download failed.');\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nif (!buffer.length || buffer.length > 20 * 1024 * 1024 || buffer.length !== source.declaredBytes)\n  throw new Error('Downloaded voice did not match the allowed size.');\n// Drop Telegram file paths and other download metadata before the paid operation.\nreturn [{json:{chatId:source.chatId, messageId:source.messageId, updateId:source.updateId},\n  binary:{data:items[0].binary.data}}];"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "884cd4cb-5663-586e-bbdb-346f7a2752ef",
      "name": "Create GigaAM transcription",
      "type": "n8n-nodes-speech-expert.speechExpert",
      "typeVersion": 1,
      "position": [
        960,
        0
      ],
      "parameters": {
        "resource": "transcription",
        "operation": "create",
        "binaryPropertyName": "data",
        "language": "ru-RU",
        "modelOverride": "GigaAM-v3-RNNT",
        "summaryPreset": "short",
        "privacyMode": true,
        "externalAiConsent": true,
        "simplify": true
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "e9df4b47-13e0-5f44-acd3-93f425f57528",
      "name": "Track operation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1200,
        0
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nif (items.length !== 1) throw new Error('Unexpected create response. Check this execution before creating another task.');\nconst operation = items[0].json;\nif (operation.error || typeof operation.id !== 'string' || !/^[A-Za-z0-9_-]{1,128}$/.test(operation.id))\n  throw new Error('No valid operation ID. Check this execution before creating another task.');\nreturn [{json:{operationId:operation.id, deadline:Date.now()+600000}}];"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "70d721d3-7a19-5b7d-9e50-86d4668c7c47",
      "name": "Wait 1 second",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        1440,
        0
      ],
      "parameters": {
        "resume": "timeInterval",
        "amount": 1,
        "unit": "seconds"
      }
    },
    {
      "id": "89d4c657-0a43-5daf-893d-226a346b7703",
      "name": "Get operation",
      "type": "n8n-nodes-speech-expert.speechExpert",
      "typeVersion": 1,
      "position": [
        1680,
        0
      ],
      "parameters": {
        "resource": "transcription",
        "operation": "get",
        "operationId": "={{ $('Track operation').first().json.operationId }}",
        "simplify": true
      },
      "retryOnFail": false,
      "onError": "continueErrorOutput"
    },
    {
      "id": "9647ff1d-a91a-50b3-872b-f220316f1108",
      "name": "Inspect operation",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1920,
        0
      ],
      "parameters": {
        "jsCode": "const tracked = $('Track operation').first().json;\nconst operation = $input.first().json;\nif (operation.error) throw new Error('The transcription could not be completed.');\nif (operation.id !== tracked.operationId || typeof operation.done !== 'boolean')\n  throw new Error('Unexpected operation status.');\nif (!operation.done && (Date.now() >= tracked.deadline || $runIndex >= 599))\n  throw new Error('Polling stopped. Check the retained operation before creating another task.');\nif (operation.done && typeof operation.response?.result !== 'string')\n  throw new Error('The completed operation has no transcript.');\nreturn [{json:operation}];"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "dbb3bc26-ff34-536c-bb73-91dadc0e67c1",
      "name": "Operation finished?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2160,
        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": "b6bb5753-098c-5d80-906f-63633bfd12ec",
      "name": "Format result",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2400,
        -80
      ],
      "parameters": {
        "jsCode": "const operation = $input.first().json;\nconst source = $('Allow demo voice').first().json;\nconst tracked = $('Track operation').first().json;\nconst response = operation.response;\nif (!response || typeof response.result !== 'string') throw new Error('No transcript to send.');\n// Only these public result fields are allowed into the Telegram reply.\nconst transcript = response.result.trim() || '(Речь не распознана.)';\nconst summary = response.summarization_status === 'ready' && typeof response.summarization?.summary === 'string'\n  ? response.summarization.summary.trim() : '';\nconst seconds = Math.max(0, Math.round((Date.now() - source.startedAt) / 1000));\nconst summaryText = summary || 'Резюме недоступно. Расшифровка сохранена выше.';\nconst text = 'Расшифровка\\n' + transcript + '\\n\\nКраткое резюме\\n' + summaryText\n  + '\\n\\nОт получения сообщения до результата: ' + seconds + ' с.';\nreturn [{json:{chatId:source.chatId, messageId:source.messageId,\n  operationId:tracked.operationId, text}}];"
      },
      "onError": "continueErrorOutput"
    },
    {
      "id": "f16af79b-aa0e-5e81-ab0c-24da0de4a913",
      "name": "Safe error message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2160,
        400
      ],
      "parameters": {
        "jsCode": "const source = $('Allow demo voice').first().json;\nlet operationId = null;\ntry { operationId = $('Track operation').first().json.operationId; } catch {}\n// Never forward the raw Telegram/API error, headers, URLs, metadata, or provider details.\nreturn [{json:{chatId:source.chatId, messageId:source.messageId, operationId,\n  text:'Обработка не завершилась. Проверьте выполнение в n8n перед повторным запуском: задача могла уже быть создана. Повторная отправка может списать кредиты ещё раз.'}}];"
      }
    },
    {
      "id": "5ac8a437-1b71-596a-83fe-7f16181f9a3b",
      "name": "Split plain text",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2640,
        -80
      ],
      "parameters": {
        "jsCode": "const source = $input.first().json;\nif (typeof source.text !== 'string' || !source.text) throw new Error('Empty reply.');\n// Telegram's native node defaults to Markdown even when Parse Mode is omitted.\n// Explicit HTML plus escaping renders plain text without interpreting model output.\nconst escape = text => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\nconst chunks = [];\nlet chunk = '';\nfor (const char of source.text) {\n  if (chunk.length + char.length > 3500) { chunks.push(chunk); chunk = ''; }\n  chunk += char;\n}\nif (chunk) chunks.push(chunk);\nreturn chunks.map((part, index) => ({json:{chatId:source.chatId, messageId:source.messageId,\n  operationId:source.operationId, part:index+1, parts:chunks.length,\n  text:escape(chunks.length > 1 ? '('+(index+1)+'/'+chunks.length+')\\n'+part : part)}}));"
      }
    },
    {
      "id": "8f963a24-2c3e-5124-af94-25931d484dff",
      "name": "Reply in demo chat",
      "type": "n8n-nodes-base.telegram",
      "typeVersion": 1.2,
      "position": [
        2880,
        -80
      ],
      "parameters": {
        "resource": "message",
        "operation": "sendMessage",
        "chatId": "={{ $json.chatId }}",
        "text": "={{ $json.text }}",
        "replyMarkup": "none",
        "additionalFields": {
          "parse_mode": "HTML",
          "appendAttribution": false,
          "disable_web_page_preview": true,
          "reply_to_message_id": "={{ $json.messageId }}"
        }
      },
      "retryOnFail": false
    },
    {
      "id": "abe1ec12-8a01-523e-bf63-01f10daf4085",
      "name": "Demo setup",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -500
      ],
      "parameters": {
        "content": "## Голосовая заявка → текст и резюме в Telegram\nРусский язык, GigaAM v3 RNNT и короткое резюме. Шаблон импортируется выключенным. Требуется n8n-nodes-speech-expert 0.1.0. Выберите одни Telegram credentials в Telegram voice / Download voice / Reply in demo chat и Speech Expert API credentials в Create GigaAM transcription / Get operation.\n\nВ Allow demo voice добавьте свой личный chat ID строкой в ALLOWED_CHAT_IDS. Пустой список ничего не пропускает. Только личные чаты и voice ≤120 с / 20 МиБ. Проверка выполняется до скачивания. Используйте отдельного бота и собственную запись.\n\nРезюме включено: externalAiConsent=true разрешает внешнюю AI-обработку текста. API Speech Expert оплачивается по актуальному тарифу сервиса. Проверьте настройки и баланс, затем выполните тест. Для Telegram Trigger нужен доступный извне HTTPS webhook n8n. Один бот имеет один webhook; тестовый запуск может заменить его существующее подключение.",
        "width": 1180,
        "height": 400,
        "color": 5
      }
    },
    {
      "id": "20d64a1f-8472-5d60-af98-df573c707db7",
      "name": "Operation and result",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1440,
        -500
      ],
      "parameters": {
        "content": "## Обработка и результат\nCreate вызывается один раз без retry. Track operation сохраняет ID; Get опрашивает ту же задачу раз в 1 секунду, не более 600 ответов / 10 минут. У запросов native node timeout 300 с; всё выполнение ограничено 20 минутами. При обрыве проверьте сохранённый ID перед новой платной задачей.\n\nРезюме — фактический короткий текст API, без заранее заполненных полей заявки. При недоступном резюме расшифровка всё равно отправляется. В ответе только текст, резюме и время до результата; стоимость не выдумывается. Ответы разбиваются на безопасные части и экранируются.\n\nПрогресс и результаты остаются в execution history n8n (включая аудио/текст); настройте хранение на своей инсталляции. Это шаблон для личного чата: автоматической дедупликации между повторными Telegram updates или ручными перезапусками нет. Не запускайте повторно Create для той же записи.",
        "width": 1160,
        "height": 400,
        "color": 7
      }
    }
  ],
  "connections": {
    "Telegram voice": {
      "main": [
        [
          {
            "node": "Allow demo voice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Allow demo voice": {
      "main": [
        [
          {
            "node": "Download voice",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download voice": {
      "main": [
        [
          {
            "node": "Validate downloaded voice",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate downloaded voice": {
      "main": [
        [
          {
            "node": "Create GigaAM transcription",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create GigaAM transcription": {
      "main": [
        [
          {
            "node": "Track operation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Track operation": {
      "main": [
        [
          {
            "node": "Wait 1 second",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 1 second": {
      "main": [
        [
          {
            "node": "Get operation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get operation": {
      "main": [
        [
          {
            "node": "Inspect operation",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inspect operation": {
      "main": [
        [
          {
            "node": "Operation finished?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Operation finished?": {
      "main": [
        [
          {
            "node": "Format result",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 1 second",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format result": {
      "main": [
        [
          {
            "node": "Split plain text",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Safe error message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Safe error message": {
      "main": [
        [
          {
            "node": "Split plain text",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split plain text": {
      "main": [
        [
          {
            "node": "Reply in demo chat",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "executionTimeout": 1200,
    "saveExecutionProgress": true,
    "saveManualExecutions": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "pinData": {},
  "tags": []
}
