歌词转录_模板
高级
这是一个自动化工作流,包含 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)可能需要您自行付费。
相关工作流推荐
WordPress博客自动化专业版(深度研究)v2.1市场
使用GPT-4o、Perplexity AI和多语言支持自动化SEO优化的博客创建
If
Set
Xml
+27
125 节点Daniel Ng
内容创作
使用OpenAI和Firecrawl从产品URL创建AI生成的Meta广告活动
使用OpenAI和Firecrawl从产品URL创建AI生成的Meta广告活动
If
Set
Code
+15
40 节点Adam Crafts
内容创作
在可视化参考库中探索n8n节点
在可视化参考库中探索n8n节点
If
Ftp
Set
+93
113 节点I versus AI
其他
(Duc)深度研究市场模板
集成PerplexityAI研究和OpenAI内容的多层级WordPress博客生成器
If
Set
Xml
+28
132 节点Daniel Ng
人工智能
使用OpenAI gpt-image-1和AI字幕创建并发布Instagram轮播图
从Google Sheets使用GPT-Image-1和AI字幕自动生成Instagram轮播图
If
Set
Code
+12
32 节点Jorge Martínez
多模态 AI
Apollo 数据抓取与触达流程 1 ✅
使用 Apollo、AI 解析和定时邮件跟进自动生成潜在客户
If
Code
Wait
+13
39 节点Deniz
内容创作