가사 전취_템플릿
고급
이것은자동화 워크플로우로, 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": "LyricsTranscribe_TEMPLATE",
"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": "WhisperTranscribe",
"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": "AudioInput",
"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": "TimestampMatching",
"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": "SubtitlesPreparation",
"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": "QualityCheck",
"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": "RoutingQualityCheck",
"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": "DiffMatch + SrcPrep",
"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": "TranscribedLyrics",
"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": "PostProcessing",
"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": "## QUALITY CONTROL CHECKPOINT\nChoose your path:\n- Auto: Skip to file generation\n- Manual: Download TXT, make corrections, re-upload for timestamp matching"
},
"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 LYRICS SEGMENTATION\nGPT-5-nano formats raw transcription into natural lyric lines (2-8 words per line) while preserving original wording."
},
"typeVersion": 1
},
{
"id": "81fb4fa3-41fe-4750-bdaf-b8518e16acdf",
"name": "메모2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-848,
-256
],
"parameters": {
"color": 5,
"height": 224,
"content": "## AUDIO INPUT & TRANSCRIPTION\nUpload your vocal track (MP3) and let Whisper AI transcribe it with precise timestamps. Works best with clean vocal recordings."
},
"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": "## EXPORT READY FILES\nGenerate professional subtitle files:\n- .SRT for YouTube & video platforms\n- .LRC for Musixmatch & streaming services"
},
"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": "## SMART TIMESTAMP ALIGNMENT\nAdvanced diff & matching algorithm aligns your corrections with original Whisper timestamps using Levenshtein distance."
},
"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": "## 🎵 SUBTITLE & LYRICS GENERATOR WITH WHISPER AI\n\nTransform vocal tracks into professional subtitle and lyrics files with AI-powered transcription and intelligent segmentation.\n\nKEY FEATURES:\n- Whisper AI transcription with word-level timestamps\n- GPT-5-nano intelligent lyrics segmentation (2-8 words/line)\n- Optional quality check with manual correction workflow\n- Smart timestamp alignment using Levenshtein distance\n- Dual output: .SRT (YouTube/video) + .LRC (streaming/Musixmatch)\n- No disk storage - download files directly\n- Supports multiple languages via ISO codes\n\nPERFECT FOR:\nMusicians • Record Labels • Content Creators • Video Editors\n\n⚡ Upload MP3 → AI processes → Download professional subtitle files"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "25d9495b-ed66-4b9d-b118-4192b37e8f79",
"connections": {
"a4760f60-c22e-4325-abf1-4e34665b4154": {
"main": [
[
{
"node": "805bc1ef-55d7-4f4c-b82f-8921d7645f4e",
"type": "main",
"index": 0
}
]
]
},
"5c72a572-a67b-423c-92f4-fedbb0c0dbd6": {
"main": [
[
{
"node": "404c19e5-9351-4abb-aef3-900dbb7a4587",
"type": "main",
"index": 0
}
]
]
},
"201a2931-ae46-43bb-85d2-0abc47ca2c98": {
"main": [
[
{
"node": "b1ac8f7e-bcb1-4732-b549-b910ec605784",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "201a2931-ae46-43bb-85d2-0abc47ca2c98",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"3b4b5409-5cbf-41ec-9be0-bdf4eee690d0": {
"main": [
[
{
"node": "3bdb855d-0dbb-496c-8d44-9050981a344a",
"type": "main",
"index": 0
}
]
]
},
"6dad3945-e25a-40b9-bac0-fd2d17a1dba7": {
"main": [
[
{
"node": "5c72a572-a67b-423c-92f4-fedbb0c0dbd6",
"type": "main",
"index": 0
}
]
]
},
"805bc1ef-55d7-4f4c-b82f-8921d7645f4e": {
"main": [
[
{
"node": "201a2931-ae46-43bb-85d2-0abc47ca2c98",
"type": "main",
"index": 0
}
]
]
},
"404c19e5-9351-4abb-aef3-900dbb7a4587": {
"main": [
[
{
"node": "fcb24b3f-17c1-43a9-9966-bf0e542d1c4c",
"type": "main",
"index": 0
},
{
"node": "aa743fae-91a0-4275-a940-874399341e98",
"type": "main",
"index": 0
}
]
]
},
"b1ac8f7e-bcb1-4732-b549-b910ec605784": {
"main": [
[
{
"node": "6dad3945-e25a-40b9-bac0-fd2d17a1dba7",
"type": "main",
"index": 0
}
],
[
{
"node": "3b4b5409-5cbf-41ec-9be0-bdf4eee690d0",
"type": "main",
"index": 0
}
]
]
},
"3bdb855d-0dbb-496c-8d44-9050981a344a": {
"main": [
[
{
"node": "fcb24b3f-17c1-43a9-9966-bf0e542d1c4c",
"type": "main",
"index": 0
},
{
"node": "aa743fae-91a0-4275-a940-874399341e98",
"type": "main",
"index": 0
}
]
]
}
}
}자주 묻는 질문
이 워크플로우를 어떻게 사용하나요?
위의 JSON 구성 코드를 복사하여 n8n 인스턴스에서 새 워크플로우를 생성하고 "JSON에서 가져오기"를 선택한 후, 구성을 붙여넣고 필요에 따라 인증 설정을 수정하세요.
이 워크플로우는 어떤 시나리오에 적합한가요?
고급
유료인가요?
이 워크플로우는 완전히 무료이며 직접 가져와 사용할 수 있습니다. 다만, 워크플로우에서 사용하는 타사 서비스(예: OpenAI API)는 사용자 직접 비용을 지불해야 할 수 있습니다.
관련 워크플로우 추천
WordPress 블로그 자동화 프로페셔널 에디션(심층 연구) v2.1 마켓
GPT-4o, Perplexity AI 및 다국어 지원을 사용한 SEO 최적화 블로그 생성 자동화
If
Set
Xml
+
If
Set
Xml
125 노드Daniel Ng
콘텐츠 제작
OpenAI와 Firecrawl로 제품 URL에서 AI 생성 Meta 광고 캠페인 생성
OpenAI와 Firecrawl을 통해 제품 URL로 AI 생성 Meta 광고 캠페인 생성
If
Set
Code
+
If
Set
Code
40 노드Adam Crafts
콘텐츠 제작
시각화 참조 라이브러리에서 n8n 노드를 탐색
可视化 참조 라이브러리에서 n8n 노드를 탐색
If
Ftp
Set
+
If
Ftp
Set
113 노드I versus AI
기타
OpenAI gpt-image-1 및 AI 캡션을 사용한 Instagram 캐러셀 생성 및 게시
Google Sheets에서 GPT-Image-1와 AI 캡션으로 Instagram 캐러셀 자동 생성
If
Set
Code
+
If
Set
Code
32 노드Jorge Martínez
멀티모달 AI
Apollo 데이터 스크래핑 및 리치 프로세스 1 ✅
Apollo, AI 파싱 및 예정 이메일 후속 조치를 사용한 잠재 고객 자동 생성
If
Code
Wait
+
If
Code
Wait
39 노드Deniz
콘텐츠 제작
반려동물 가게 4
🐶 펫 샵 예약 AI 대리자
If
Set
Code
+
If
Set
Code
187 노드Bruno Dias
인공지능