8
n8n 中文网amn8n.com

歌词转录_模板

高级

这是一个自动化工作流,包含 18 个节点。主要使用 If, Code, Wait, FormTrigger, HttpRequest 等节点。 使用Whisper AI和GPT-5-nano从音频创建.SRT字幕和.LRC歌词文件

前置要求
  • 可能需要目标 API 的认证凭证
  • OpenAI API Key

分类

-
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "ym5RZpXRcp7ZnW8X",
  "meta": {
    "instanceId": "b1699e1d8ef82aaaaf2eed0ed67f215d7574a625e2d012a1bcd013054b0defdf",
    "templateCredsSetupCompleted": true
  },
  "name": "歌词转录_模板",
  "tags": [
    {
      "id": "5WzUYUnG7iVDJG7q",
      "name": "TEMPLATE",
      "createdAt": "2025-10-13T19:43:42.665Z",
      "updatedAt": "2025-10-13T19:43:42.665Z"
    }
  ],
  "nodes": [
    {
      "id": "d3f1c98a-1ff5-47ae-a68c-432355827779",
      "name": "OpenAI 聊天模型",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -544,
        176
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-nano",
          "cachedResultName": "gpt-5-nano"
        },
        "options": {}
      },
      "credentials": {
        "openAiApi": {
          "id": "SejrVHsogrtvT4yC",
          "name": "TEMPLATE"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "805bc1ef-55d7-4f4c-b82f-8921d7645f4e",
      "name": "Whisper转录",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -704,
        0
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/audio/transcriptions",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "contentType": "multipart-form-data",
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "file",
              "parameterType": "formBinaryData",
              "inputDataFieldName": "Audio_File"
            },
            {
              "name": "model",
              "value": "whisper-1"
            },
            {
              "name": "response_format",
              "value": "verbose_json"
            },
            {
              "name": "timestamp_granularities[]",
              "value": "word"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "YOUR API KEY"
            },
            {
              "name": "Content-Type",
              "value": "multipart/form-data"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "a4760f60-c22e-4325-abf1-4e34665b4154",
      "name": "音频输入",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -864,
        0
      ],
      "webhookId": "9ad0442a-b661-487c-8d8a-09a54400de62",
      "parameters": {
        "options": {},
        "formTitle": "Upload audio file (max 25mb)",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Audio File",
              "multipleFiles": false,
              "requiredField": true,
              "acceptFileTypes": ".mp3"
            },
            {
              "fieldType": "radio",
              "fieldLabel": "QualityCheck",
              "fieldOptions": {
                "values": [
                  {
                    "option": "YES"
                  },
                  {
                    "option": "NO"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "responseMode": "lastNode",
        "formDescription": "Here you can upload your audio file and you get subtitles file back."
      },
      "typeVersion": 2.3
    },
    {
      "id": "3b4b5409-5cbf-41ec-9be0-bdf4eee690d0",
      "name": "时间戳匹配",
      "type": "n8n-nodes-base.code",
      "position": [
        -64,
        176
      ],
      "parameters": {
        "jsCode": "const WhisperTranscribe = $('WhisperTranscribe').first().json;\nconst words = WhisperTranscribe[\"words\"];\nconst lyrics = $json[\"text\"];\n\nconst minWordMatchRatio = 0.7; // tolerance (0-1) for fuzzy matching per line\nconst segments = lyrics.split(/\\r?\\n/).filter(l => l.trim().length > 0);\nconst normalize = s => s.toLowerCase().replace(/[^a-z0-9']/g, \" \").trim();\nconst whisperWords = words.map(w => ({\n  ...w,\n  norm: normalize(w.word)\n}));\n\nconst result = [];\n\nlet currentIndex = 0;\nfor (const line of segments) {\n  const lineWords = normalize(line).split(/\\s+/).filter(Boolean);\n\n  // Try to match this line to consecutive words from Whisper\n  let startIndex = -1;\n  let endIndex = -1;\n  let matchCount = 0;\n\n  for (let i = currentIndex; i < whisperWords.length; i++) {\n    if (lineWords.includes(whisperWords[i].norm)) {\n      if (startIndex === -1) startIndex = i;\n      endIndex = i;\n      matchCount++;\n      if (matchCount / lineWords.length >= minWordMatchRatio) break;\n    }\n  }\n\n  if (startIndex !== -1 && endIndex !== -1) {\n    const start = whisperWords[startIndex].start;\n    const end = whisperWords[endIndex].end;\n    result.push({ start, end, text: line });\n    currentIndex = endIndex + 1;\n  } else {\n    // fallback: if not found, approximate based on previous\n    const prevEnd = result.length ? result[result.length - 1].end : words[0].start;\n    const approxEnd = prevEnd + 2.5; // arbitrary 2.5s window\n    result.push({ start: prevEnd, end: approxEnd, text: line });\n  }\n}\n\nreturn [{ json: { timedSegments: result } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "3bdb855d-0dbb-496c-8d44-9050981a344a",
      "name": "字幕准备",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        176
      ],
      "parameters": {
        "jsCode": "const PostProcessedLyrics = $('RoutingQualityCheck').first().json;\nconst plainText = PostProcessedLyrics[\"text\"];\nconst segments = $json[\"timedSegments\"];\n\nfunction toSrtTime(sec) {\n  const h = Math.floor(sec / 3600);\n  const m = Math.floor((sec % 3600) / 60);\n  const s = Math.floor(sec % 60);\n  const ms = Math.floor((sec * 1000) % 1000);\n  return `${h.toString().padStart(2, \"0\")}:${m\n    .toString()\n    .padStart(2, \"0\")}:${s.toString().padStart(2, \"0\")},${ms\n    .toString()\n    .padStart(3, \"0\")}`;\n}\n\nfunction toLrcTime(sec) {\n  const m = Math.floor(sec / 60);\n  const s = Math.floor(sec % 60);\n  const cs = Math.floor((sec % 1) * 100); // centiseconds\n  return `[${m.toString().padStart(2, \"0\")}:${s\n    .toString()\n    .padStart(2, \"0\")}.${cs.toString().padStart(2, \"0\")}]`;\n}\n\n// --- generate SRT ---\nlet srt = \"\";\nsegments.forEach((seg, i) => {\n  const start = toSrtTime(seg.start);\n  const end = toSrtTime(seg.end);\n  srt += `${i + 1}\\n${start} --> ${end}\\n${seg.text.trim()}\\n\\n`;\n});\n\n// --- generate LRC ---\nlet lrc = \"\";\nsegments.forEach(seg => {\n  const time = toLrcTime(seg.start);\n  lrc += `${time}${seg.text.trim()}\\n`;\n});\n\n// --- return both ---\nreturn [\n  {\n    json: {\n      srtContent: srt.trim(),\n      lrcContent: lrc.trim(),\n      segmentCount: segments.length,\n      plainText\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "5c72a572-a67b-423c-92f4-fedbb0c0dbd6",
      "name": "质量检查",
      "type": "n8n-nodes-base.wait",
      "position": [
        80,
        -16
      ],
      "webhookId": "779090da-886d-406d-8a0e-daa0d91bc74b",
      "parameters": {
        "resume": "form",
        "options": {},
        "formTitle": "Lyrics Review",
        "formFields": {
          "values": [
            {
              "fieldType": "file",
              "fieldLabel": "Corrected lyrics",
              "multipleFiles": false,
              "acceptFileTypes": ".txt"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "b1ac8f7e-bcb1-4732-b549-b910ec605784",
      "name": "路由质量检查",
      "type": "n8n-nodes-base.if",
      "position": [
        -240,
        0
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "38864488-adc7-429d-b65f-ed254d2eeacf",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $('AudioInput').item.json.QualityCheck }}",
              "rightValue": "YES"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "404c19e5-9351-4abb-aef3-900dbb7a4587",
      "name": "差异匹配 + 源准备",
      "type": "n8n-nodes-base.code",
      "position": [
        272,
        -16
      ],
      "parameters": {
        "jsCode": "// Načtení dat\nconst WhisperTranscribe = $('WhisperTranscribe').first().json;\nconst words = WhisperTranscribe[\"words\"];\n\n// Načtení upravených lyrics\nlet lyricsText = items[0].json.lyricsText;\nif (!lyricsText && items[0].binary && items[0].binary.Corrected_lyrics) {\n  lyricsText = Buffer.from(items[0].binary.Corrected_lyrics.data, \"base64\").toString(\"utf8\");\n}\n\n// CLEANUP: Odstranění garbage na konci textu\nlyricsText = lyricsText.replace(/\\t\\d+\\s*$/, '').trim();\n\n// Funkce pro normalizaci textu\nfunction normalizeWord(word) {\n  return word\n    .toLowerCase()\n    .replace(/[.,!?;:\"\"\"''—-]/g, '')\n    .trim();\n}\n\n// Funkce pro tokenizaci textu na slova\nfunction tokenize(text) {\n  return text\n    .replace(/\\\\n/g, ' ')\n    .replace(/\\n/g, ' ')\n    .split(/\\s+/)\n    .filter(w => w.length > 0);\n}\n\n// Získání původního textu z words\nconst originalText = words.map(w => w.word).join(' ');\nconst originalWords = tokenize(originalText);\nconst correctedWords = tokenize(lyricsText);\n\n// Levenshtein distance\nfunction levenshtein(a, b) {\n  const matrix = [];\n  for (let i = 0; i <= b.length; i++) {\n    matrix[i] = [i];\n  }\n  for (let j = 0; j <= a.length; j++) {\n    matrix[0][j] = j;\n  }\n  for (let i = 1; i <= b.length; i++) {\n    for (let j = 1; j <= a.length; j++) {\n      if (b.charAt(i - 1) === a.charAt(j - 1)) {\n        matrix[i][j] = matrix[i - 1][j - 1];\n      } else {\n        matrix[i][j] = Math.min(\n          matrix[i - 1][j - 1] + 1,\n          matrix[i][j - 1] + 1,\n          matrix[i - 1][j] + 1\n        );\n      }\n    }\n  }\n  return matrix[b.length][a.length];\n}\n\n// Alignment\nfunction alignWords(original, corrected, timestamps) {\n  const m = original.length;\n  const n = corrected.length;\n  \n  const dp = Array(m + 1).fill(null).map(() => Array(n + 1).fill(0));\n  const backtrack = Array(m + 1).fill(null).map(() => Array(n + 1).fill(null));\n  \n  for (let i = 0; i <= m; i++) dp[i][0] = i * -1;\n  for (let j = 0; j <= n; j++) dp[0][j] = j * -1;\n  \n  for (let i = 1; i <= m; i++) {\n    for (let j = 1; j <= n; j++) {\n      const origNorm = normalizeWord(original[i - 1]);\n      const corrNorm = normalizeWord(corrected[j - 1]);\n      \n      let matchScore = 0;\n      if (origNorm === corrNorm) {\n        matchScore = 2;\n      } else {\n        const dist = levenshtein(origNorm, corrNorm);\n        const maxLen = Math.max(origNorm.length, corrNorm.length);\n        if (maxLen > 0 && dist / maxLen < 0.3) {\n          matchScore = 1;\n        } else {\n          matchScore = -1;\n        }\n      }\n      \n      const match = dp[i - 1][j - 1] + matchScore;\n      const del = dp[i - 1][j] - 1;\n      const ins = dp[i][j - 1] - 1;\n      \n      dp[i][j] = Math.max(match, del, ins);\n      \n      if (dp[i][j] === match) backtrack[i][j] = 'match';\n      else if (dp[i][j] === del) backtrack[i][j] = 'delete';\n      else backtrack[i][j] = 'insert';\n    }\n  }\n  \n  const alignment = [];\n  let i = m, j = n;\n  \n  while (i > 0 || j > 0) {\n    if (i === 0) {\n      alignment.unshift({\n        correctedWord: corrected[j - 1],\n        originalIndex: null,\n        timestamp: null,\n        type: 'inserted'\n      });\n      j--;\n    } else if (j === 0) {\n      i--;\n    } else {\n      const action = backtrack[i][j];\n      \n      if (action === 'match') {\n        alignment.unshift({\n          correctedWord: corrected[j - 1],\n          originalIndex: i - 1,\n          timestamp: timestamps[i - 1],\n          type: normalizeWord(original[i - 1]) === normalizeWord(corrected[j - 1]) ? 'match' : 'modified'\n        });\n        i--; j--;\n      } else if (action === 'delete') {\n        i--;\n      } else {\n        let interpolatedTimestamp = null;\n        if (i > 0 && i < m) {\n          const prevTimestamp = timestamps[i - 1];\n          const nextTimestamp = timestamps[i];\n          interpolatedTimestamp = {\n            start: prevTimestamp.end,\n            end: nextTimestamp.start\n          };\n        }\n        \n        alignment.unshift({\n          correctedWord: corrected[j - 1],\n          originalIndex: null,\n          timestamp: interpolatedTimestamp,\n          type: 'inserted'\n        });\n        j--;\n      }\n    }\n  }\n  \n  return alignment;\n}\n\nconst alignment = alignWords(originalWords, correctedWords, words);\n\nconst alignedWords = alignment.map((item, index) => {\n  return {\n    word: item.correctedWord,\n    start: item.timestamp?.start || null,\n    end: item.timestamp?.end || null,\n    type: item.type,\n    originalIndex: item.originalIndex\n  };\n});\n\n// ============================================\n// GENEROVÁNÍ .LRC SOUBORU\n// ============================================\n\nfunction formatLRCTime(seconds) {\n  if (seconds === null || isNaN(seconds)) return '[00:00.00]';\n  const minutes = Math.floor(seconds / 60);\n  const secs = (seconds % 60);\n  const secsStr = secs.toFixed(2).padStart(5, '0');\n  return `[${String(minutes).padStart(2, '0')}:${secsStr}]`;\n}\n\nconst lyricsLines = lyricsText\n  .replace(/\\\\n/g, '\\n')\n  .split('\\n')\n  .map(line => line.trim())\n  .filter(line => line.length > 0);\n\nconst lrcLines = [];\nlet wordIndex = 0;\n\nfor (const line of lyricsLines) {\n  const lineWords = tokenize(line);\n  \n  if (lineWords.length === 0) continue;\n  \n  let lineStart = null;\n  let matchedWords = 0;\n  const startWordIndex = wordIndex;\n  \n  for (let i = wordIndex; i < alignedWords.length && matchedWords < lineWords.length; i++) {\n    const alignedWord = alignedWords[i];\n    const normalizedAligned = normalizeWord(alignedWord.word);\n    const normalizedLine = normalizeWord(lineWords[matchedWords]);\n    \n    if (normalizedAligned === normalizedLine) {\n      if (lineStart === null && alignedWord.start !== null) {\n        lineStart = alignedWord.start;\n      }\n      matchedWords++;\n      wordIndex = i + 1;\n    }\n  }\n  \n  // Fallback pro missing timestamps\n  if (lineStart === null) {\n    if (lrcLines.length > 0) {\n      const lastTime = lrcLines[lrcLines.length - 1].time;\n      lineStart = lastTime + 2;\n    } else {\n      lineStart = 0;\n    }\n  }\n  \n  lrcLines.push({\n    time: lineStart,\n    text: line\n  });\n}\n\n// Deduplikace timestampů v LRC\nconst lrcLinesDeduped = [];\nconst usedTimestamps = new Set();\n\nfor (const line of lrcLines) {\n  let adjustedTime = line.time;\n  let offset = 0;\n  \n  while (usedTimestamps.has(adjustedTime.toFixed(2))) {\n    offset += 0.01;\n    adjustedTime = line.time + offset;\n  }\n  \n  usedTimestamps.add(adjustedTime.toFixed(2));\n  lrcLinesDeduped.push({\n    time: adjustedTime,\n    text: line.text\n  });\n}\n\nlrcLinesDeduped.sort((a, b) => a.time - b.time);\nconst lrcContent = lrcLinesDeduped\n  .map(line => `${formatLRCTime(line.time)}${line.text}`)\n  .join('\\n');\n\n// ============================================\n// GENEROVÁNÍ .SRT SOUBORU (VYLEPŠENO)\n// ============================================\n\nfunction formatSRTTime(seconds) {\n  if (seconds === null || isNaN(seconds)) return '00:00:00,000';\n  const hours = Math.floor(seconds / 3600);\n  const minutes = Math.floor((seconds % 3600) / 60);\n  const secs = Math.floor(seconds % 60);\n  const ms = Math.floor((seconds % 1) * 1000);\n  \n  return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')},${String(ms).padStart(3, '0')}`;\n}\n\nconst srtEntries = [];\nconst MIN_DURATION = 0.8;  // Minimální délka titulku\nconst MAX_DURATION = 5.0;  // Maximální délka titulku\nconst CHARS_PER_SECOND = 20; // Rychlost čtení\n\nwordIndex = 0;\n\nfor (let lineIdx = 0; lineIdx < lyricsLines.length; lineIdx++) {\n  const line = lyricsLines[lineIdx];\n  const lineWords = tokenize(line);\n  \n  if (lineWords.length === 0) continue;\n  \n  let lineStart = null;\n  let lineEnd = null;\n  let matchedWords = 0;\n  let firstMatchIdx = null;\n  let lastMatchIdx = null;\n  \n  // Najdeme všechna slova pro tento řádek\n  for (let i = wordIndex; i < alignedWords.length && matchedWords < lineWords.length; i++) {\n    const alignedWord = alignedWords[i];\n    const normalizedAligned = normalizeWord(alignedWord.word);\n    const normalizedLine = normalizeWord(lineWords[matchedWords]);\n    \n    if (normalizedAligned === normalizedLine) {\n      if (firstMatchIdx === null) {\n        firstMatchIdx = i;\n      }\n      lastMatchIdx = i;\n      \n      if (lineStart === null && alignedWord.start !== null) {\n        lineStart = alignedWord.start;\n      }\n      if (alignedWord.end !== null) {\n        lineEnd = alignedWord.end;\n      }\n      matchedWords++;\n    }\n  }\n  \n  // Posuneme wordIndex na konec matchů\n  if (lastMatchIdx !== null) {\n    wordIndex = lastMatchIdx + 1;\n  }\n  \n  // Validace a úprava časů\n  if (lineStart !== null && lineEnd !== null) {\n    // Zajistíme že end > start\n    if (lineEnd <= lineStart) {\n      lineEnd = lineStart + MIN_DURATION;\n    }\n    \n    let duration = lineEnd - lineStart;\n    const textLength = line.length;\n    \n    // Výpočet ideální délky podle textu\n    const idealDuration = Math.max(MIN_DURATION, textLength / CHARS_PER_SECOND);\n    \n    // Pokud je duration příliš krátká, prodloužíme\n    if (duration < idealDuration) {\n      lineEnd = lineStart + idealDuration;\n      duration = idealDuration;\n    }\n    \n    // Pokud je příliš dlouhá, zkrátíme\n    if (duration > MAX_DURATION) {\n      lineEnd = lineStart + MAX_DURATION;\n      duration = MAX_DURATION;\n    }\n    \n    // Kontrola překryvu s následujícím titulkem\n    if (lineIdx < lyricsLines.length - 1) {\n      // Najdeme start dalšího řádku\n      let nextStart = null;\n      const nextLine = lyricsLines[lineIdx + 1];\n      const nextLineWords = tokenize(nextLine);\n      let tempMatched = 0;\n      \n      for (let i = wordIndex; i < alignedWords.length && tempMatched < nextLineWords.length; i++) {\n        const alignedWord = alignedWords[i];\n        const normalizedAligned = normalizeWord(alignedWord.word);\n        const normalizedNext = normalizeWord(nextLineWords[tempMatched]);\n        \n        if (normalizedAligned === normalizedNext) {\n          if (nextStart === null && alignedWord.start !== null) {\n            nextStart = alignedWord.start;\n            break;\n          }\n        }\n      }\n      \n      // Pokud by se překrýval s dalším, zkrátíme s malou mezerou\n      if (nextStart !== null && lineEnd > nextStart - 0.1) {\n        lineEnd = Math.max(lineStart + MIN_DURATION, nextStart - 0.1);\n      }\n    }\n    \n    // Finální validace\n    if (lineEnd > lineStart && (lineEnd - lineStart) >= 0.1) {\n      srtEntries.push({\n        start: lineStart,\n        end: lineEnd,\n        text: line\n      });\n    }\n  }\n}\n\n// Vytvoření SRT formátu\nconst srtContent = srtEntries\n  .map((entry, index) => {\n    return `${index + 1}\\n${formatSRTTime(entry.start)} --> ${formatSRTTime(entry.end)}\\n${entry.text}\\n`;\n  })\n  .join('\\n');\n\n// ============================================\n// STATISTIKY\n// ============================================\n\nconst stats = {\n  totalOriginal: originalWords.length,\n  totalCorrected: correctedWords.length,\n  matched: alignment.filter(a => a.type === 'match').length,\n  modified: alignment.filter(a => a.type === 'modified').length,\n  inserted: alignment.filter(a => a.type === 'inserted').length,\n  lrcLinesGenerated: lrcLinesDeduped.length,\n  srtEntriesGenerated: srtEntries.length\n};\n\nreturn [{\n  json: {\n    alignedWords: alignedWords,\n    stats: stats,\n    correctedLyrics: lyricsText,\n    lrcContent: lrcContent,\n    srtContent: srtContent\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "fcb24b3f-17c1-43a9-9966-bf0e542d1c4c",
      "name": "SRT",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        464,
        -16
      ],
      "parameters": {
        "options": {},
        "operation": "toText",
        "sourceProperty": "srtContent",
        "binaryPropertyName": "=SrtFile_ {{ $('AudioInput').item.json['Audio File'].filename }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "aa743fae-91a0-4275-a940-874399341e98",
      "name": "LRC",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        464,
        176
      ],
      "parameters": {
        "options": {},
        "operation": "toText",
        "sourceProperty": "lrcContent",
        "binaryPropertyName": "=LRC_FILE_ {{ $('AudioInput').item.json['Audio File'].filename }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "6dad3945-e25a-40b9-bac0-fd2d17a1dba7",
      "name": "已转录歌词",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        -64,
        -16
      ],
      "parameters": {
        "options": {},
        "operation": "toText",
        "sourceProperty": "text",
        "binaryPropertyName": "=TRANSCRIBED_{{ $('AudioInput').item.json['Audio File'].filename }}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "201a2931-ae46-43bb-85d2-0abc47ca2c98",
      "name": "后处理",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        -544,
        0
      ],
      "parameters": {
        "text": "={{ $json.text }}",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "You are helping with preparing song lyrics for musicians. Take the following transcription and split it into lyric-like lines. Keep lines short (2–8 words), natural for singing/rap phrasing, and do not change the wording."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.7
    },
    {
      "id": "6a1a5512-f2c3-4b98-b89f-4f328e42422c",
      "name": "便签",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -240,
        -256
      ],
      "parameters": {
        "color": 5,
        "width": 416,
        "height": 192,
        "content": "## 质量控制检查点"
      },
      "typeVersion": 1
    },
    {
      "id": "a7725722-9e75-439e-b4c8-bb0bb16520bb",
      "name": "便签1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -544,
        -256
      ],
      "parameters": {
        "color": 5,
        "width": 224,
        "height": 224,
        "content": "## AI歌词分段"
      },
      "typeVersion": 1
    },
    {
      "id": "81fb4fa3-41fe-4750-bdaf-b8518e16acdf",
      "name": "便签2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -848,
        -256
      ],
      "parameters": {
        "color": 5,
        "height": 224,
        "content": "## 音频输入与转录"
      },
      "typeVersion": 1
    },
    {
      "id": "16538472-625d-425a-b374-2ad750a0ac3e",
      "name": "便签3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        512,
        -272
      ],
      "parameters": {
        "color": 5,
        "width": 288,
        "height": 224,
        "content": "## 导出就绪文件"
      },
      "typeVersion": 1
    },
    {
      "id": "0169bbce-6ab2-4ff6-9799-9204a3ba0400",
      "name": "便签4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        -256
      ],
      "parameters": {
        "color": 5,
        "width": 272,
        "height": 192,
        "content": "## 智能时间戳对齐"
      },
      "typeVersion": 1
    },
    {
      "id": "1bb72b32-f5f0-4a98-801c-ba751f3db0c3",
      "name": "便签5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1328,
        -368
      ],
      "parameters": {
        "color": 5,
        "width": 400,
        "height": 592,
        "content": "## 🎵 使用Whisper AI的字幕和歌词生成器"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "25d9495b-ed66-4b9d-b118-4192b37e8f79",
  "connections": {
    "AudioInput": {
      "main": [
        [
          {
            "node": "WhisperTranscribe",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "QualityCheck": {
      "main": [
        [
          {
            "node": "DiffMatch + SrcPrep",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "PostProcessing": {
      "main": [
        [
          {
            "node": "RoutingQualityCheck",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "PostProcessing",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "TimestampMatching": {
      "main": [
        [
          {
            "node": "SubtitlesPreparation",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "TranscribedLyrics": {
      "main": [
        [
          {
            "node": "QualityCheck",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "WhisperTranscribe": {
      "main": [
        [
          {
            "node": "PostProcessing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "DiffMatch + SrcPrep": {
      "main": [
        [
          {
            "node": "SRT",
            "type": "main",
            "index": 0
          },
          {
            "node": "LRC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RoutingQualityCheck": {
      "main": [
        [
          {
            "node": "TranscribedLyrics",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "TimestampMatching",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "SubtitlesPreparation": {
      "main": [
        [
          {
            "node": "SRT",
            "type": "main",
            "index": 0
          },
          {
            "node": "LRC",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量18
分类-
节点类型9
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

外部链接
在 n8n.io 查看

分享此工作流