This commit introduces three n8n workflow JSON files: - RedditVideoMakerBotMain.json - RedditVideoMakerBotProcessPost.json - RedditVideoMakerBotGenerateTTS.json These workflows are designed to automate the creation of videos from Reddit posts using open-source tools like Piper TTS for text-to-speech and ffmpeg for audio/video manipulation and captioning. The workflows require you to configure paths to these tools, voice models, local video background folders, and font files within the n8n environment.pull/2347/head
parent
64bf647de9
commit
fd1a666f41
@ -0,0 +1,187 @@
|
||||
{
|
||||
"name": "RedditVideoMakerBot Generate TTS",
|
||||
"tags": ["TTS", "Audio", "Subworkflow", "FOSS", "Piper"],
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "# Reddit Video Maker Bot - Generate TTS Subworkflow (Piper TTS)\n\nThis subworkflow generates text-to-speech audio for a given text segment using Piper TTS (via command line) and converts it to MP3 using ffmpeg.\n\n**Inputs:**\n- `text_segment` (string): The text to convert to speech.\n- `piper_voice_model_path` (string): Path to the Piper voice model file (e.g., en_US-lessac-medium.onnx).\n\n**Outputs:**\n- `tts_audio_binary` (binary): The generated MP3 audio data.\n- `audio_file_path` (string): The path to the saved MP3 audio file (e.g., `audio_cache/tts_output.mp3`).\n\n**Prerequisites on n8n execution environment:**\n- Piper TTS installed and accessible via `piper` command.\n- ffmpeg installed and accessible via `ffmpeg` command.\n- `audio_cache/` directory must be writable by n8n."
|
||||
},
|
||||
"name": "Sticky Note: Explanation",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
-50,
|
||||
-450
|
||||
],
|
||||
"id": "StickyNote_TTS_Explanation"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{
|
||||
"name": "text_segment",
|
||||
"type": "string",
|
||||
"example": "Hello, this is a test sentence."
|
||||
},
|
||||
{
|
||||
"name": "piper_voice_model_path",
|
||||
"type": "string",
|
||||
"example": "/models/piper/en_US-lessac-medium.onnx"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "When Executed by Another Workflow",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [
|
||||
0,
|
||||
-200
|
||||
],
|
||||
"id": "ExecuteWorkflowTrigger_TTS"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "mkdir -p audio_cache && piper --model {{ $json.piper_voice_model_path }} --output_file audio_cache/tts_output.wav",
|
||||
"stdin": "{{ $json.text_segment }}",
|
||||
"options": {
|
||||
"shell": true,
|
||||
"timeout": 120000
|
||||
}
|
||||
},
|
||||
"name": "Generate WAV with Piper TTS",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
250,
|
||||
-200
|
||||
],
|
||||
"id": "ExecuteCommand_PiperTTS",
|
||||
"notesInFlow": true,
|
||||
"notes": "Ensure 'audio_cache' exists. Timeout set to 2 mins."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "ffmpeg -y -i audio_cache/tts_output.wav -codec:a libmp3lame -qscale:a 2 audio_cache/tts_output.mp3",
|
||||
"options": {
|
||||
"shell": true,
|
||||
"timeout": 60000
|
||||
}
|
||||
},
|
||||
"name": "Convert WAV to MP3",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 2.1,
|
||||
"position": [
|
||||
500,
|
||||
-200
|
||||
],
|
||||
"id": "ExecuteCommand_FFmpeg_WAV_to_MP3",
|
||||
"notesInFlow": true,
|
||||
"notes": "Overwrite if exists. Timeout 1 min."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"filePath": "audio_cache/tts_output.mp3",
|
||||
"outputPropertyName": "tts_audio_binary",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Read MP3 Audio File",
|
||||
"type": "n8n-nodes-base.readBinaryFile",
|
||||
"typeVersion": 1.2,
|
||||
"position": [
|
||||
750,
|
||||
-200
|
||||
],
|
||||
"id": "ReadBinaryFile_MP3"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "audio_file_path",
|
||||
"value": "={{ $json.tts_audio_binary.fileName || 'audio_cache/tts_output.mp3' }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"keepOnlySet": false
|
||||
}
|
||||
},
|
||||
"name": "Set Output Data",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
1000,
|
||||
-200
|
||||
],
|
||||
"id": "Set_Output_Data"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"ExecuteWorkflowTrigger_TTS": [
|
||||
{
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Generate WAV with Piper TTS",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"Generate WAV with Piper TTS": [
|
||||
{
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Convert WAV to MP3",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"Convert WAV to MP3": [
|
||||
{
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Read MP3 Audio File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
],
|
||||
"Read MP3 Audio File": [
|
||||
{
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set Output Data",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "Europe/London",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"versionId": "1b9a12a4-8f6c-4c2e-9d1a-5b7c0f8e2d3a",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "YOUR_INSTANCE_ID"
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
{
|
||||
"name": "RedditVideoMakerBot Main",
|
||||
"tags": ["Reddit", "Video Automation", "FOSS", "Main Workflow"],
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "# Reddit Video Maker Bot - Main Workflow (FOSS)\n\nThis workflow automates short-form video creation from Reddit posts using FOSS tools.\n\n**Steps:**\n1. **Schedule Trigger**: Runs periodically.\n2. **Set Configuration**: Defines global parameters (subreddit, Piper paths, ffmpeg paths, local folders, etc.). **USER MUST CONFIGURE THESE PATHS.**\n3. **Reddit - Get Top Posts**: Fetches posts.\n4. **Code - Filter Posts**: Filters posts.\n5. **Split In Batches**: Processes one post at a time.\n6. **Execute Subworkflow: Process Post**: Triggers video generation for each post.\n7. **Log Processed Post**: Logs completion.\n\n**Prerequisites for n8n environment:**\n- `ffmpeg` installed.\n- Piper TTS installed, with voice models at specified paths.\n- Specified local folders (`video_background_local_folder_path`, `audio_cache/`, `results/`) must exist and be writable/readable."
|
||||
},
|
||||
"name": "Sticky Note: Main Explanation",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"id": "StickyNote_Main_Explanation",
|
||||
"position": [
|
||||
0,
|
||||
-400
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"triggerTimes": {
|
||||
"item": [
|
||||
{
|
||||
"hour": 3,
|
||||
"minute": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1,
|
||||
"id": "ScheduleTrigger_Main",
|
||||
"position": [
|
||||
0,
|
||||
-150
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{
|
||||
"name": "subreddit",
|
||||
"value": "askreddit"
|
||||
},
|
||||
{
|
||||
"name": "timeframe",
|
||||
"value": "day"
|
||||
},
|
||||
{
|
||||
"name": "piper_voice_model_path_author",
|
||||
"value": "/opt/n8n/piper_voices/en_US-lessac-medium.onnx"
|
||||
},
|
||||
{
|
||||
"name": "piper_voice_model_path_commenter",
|
||||
"value": "/opt/n8n/piper_voices/en_US-ryan-medium.onnx"
|
||||
},
|
||||
{
|
||||
"name": "video_background_local_folder_path",
|
||||
"value": "/opt/n8n/video_backgrounds/"
|
||||
},
|
||||
{
|
||||
"name": "output_resolution",
|
||||
"value": "1080x1920"
|
||||
},
|
||||
{
|
||||
"name": "caption_font_path",
|
||||
"value": "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
|
||||
},
|
||||
{
|
||||
"name": "caption_font_size",
|
||||
"value": "48"
|
||||
},
|
||||
{
|
||||
"name": "caption_color",
|
||||
"value": "white"
|
||||
},
|
||||
{
|
||||
"name": "ffmpeg_path",
|
||||
"value": "ffmpeg"
|
||||
},
|
||||
{
|
||||
"name": "piper_path",
|
||||
"value": "piper"
|
||||
}
|
||||
],
|
||||
"number": [
|
||||
{
|
||||
"name": "post_limit",
|
||||
"value": 3
|
||||
},
|
||||
{
|
||||
"name": "min_score",
|
||||
"value": 50
|
||||
},
|
||||
{
|
||||
"name": "min_title_length",
|
||||
"value": 10
|
||||
},
|
||||
{
|
||||
"name": "min_content_length",
|
||||
"value": 30
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "Set Configuration",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"id": "Set_Configuration_Main",
|
||||
"position": [
|
||||
250,
|
||||
-150
|
||||
],
|
||||
"notes": "USER ACTION: Update all paths and settings here to match your environment!"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://www.reddit.com/r/{{$node['Set Configuration'].json['subreddit']}}/top.json?t={{$node['Set Configuration'].json['timeframe']}}&limit={{$node['Set Configuration'].json['post_limit']}}",
|
||||
"options": {
|
||||
"response": {
|
||||
"responseFormat": "json"
|
||||
}
|
||||
},
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "n8n-RedditVideoMakerBot/1.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Reddit - Get Top Posts",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"id": "HttpRequest_GetRedditPosts",
|
||||
"position": [
|
||||
500,
|
||||
-150
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const minScore = $node['Set Configuration'].json.min_score;\nconst minTitleLength = $node['Set Configuration'].json.min_title_length;\nconst minContentLength = $node['Set Configuration'].json.min_content_length;\n\nlet filteredItems = [];\nif (items[0].json.data && items[0].json.data.children) {\n filteredItems = items[0].json.data.children.filter(item => {\n const post = item.data;\n const isSelfPost = post.is_self;\n const score = post.score;\n const titleLength = post.title.length;\n const contentLength = isSelfPost ? post.selftext.length : 0;\n return score >= minScore && titleLength >= minTitleLength && (isSelfPost ? contentLength >= minContentLength : true);\n });\n}\nreturn filteredItems.map(item => ({ json: item.data }));"
|
||||
},
|
||||
"name": "Code - Filter Posts",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "Code_FilterPosts",
|
||||
"position": [
|
||||
750,
|
||||
-150
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"batchSize": 1,
|
||||
"options": {}
|
||||
},
|
||||
"name": "Split In Batches",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"id": "SplitInBatches_Main",
|
||||
"position": [
|
||||
1000,
|
||||
-150
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowId": {
|
||||
"__rl": true,
|
||||
"mode": "name",
|
||||
"value": "RedditVideoMakerBot Process Post"
|
||||
},
|
||||
"workflowInputs": {
|
||||
"value": {
|
||||
"reddit_post_data": "={{ $json }}",
|
||||
"config": "={{ $node['Set Configuration'].json }}"
|
||||
},
|
||||
"schema": [
|
||||
{ "id": "reddit_post_data", "type": "object", "displayName": "Reddit Post Data" },
|
||||
{ "id": "config", "type": "object", "displayName": "Global Configuration" }
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"waitForSubWorkflow": true
|
||||
}
|
||||
},
|
||||
"name": "Execute Subworkflow: Process Post",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.2,
|
||||
"id": "ExecuteWorkflow_ProcessPost",
|
||||
"position": [
|
||||
1250,
|
||||
-150
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"message": "Successfully processed Reddit post: {{ $json.reddit_post_data.title }}",
|
||||
"logLevel": "info",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Log Processed Post",
|
||||
"type": "n8n-nodes-base.logMessage",
|
||||
"typeVersion": 1,
|
||||
"id": "LogMessage_ProcessedPost",
|
||||
"position": [
|
||||
1500,
|
||||
-150
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"ScheduleTrigger_Main": [{"main":[[{"node":"Set Configuration","type":"main","index":0}]]}],
|
||||
"Set Configuration": [{"main":[[{"node":"Reddit - Get Top Posts","type":"main","index":0}]]}],
|
||||
"Reddit - Get Top Posts": [{"main":[[{"node":"Code - Filter Posts","type":"main","index":0}]]}],
|
||||
"Code - Filter Posts": [{"main":[[{"node":"Split In Batches","type":"main","index":0}]]}],
|
||||
"Split In Batches": [{"main":[[{"node":"Execute Subworkflow: Process Post","type":"main","index":0}]]}],
|
||||
"Execute Subworkflow: Process Post": [{"main":[[{"node":"Log Processed Post","type":"main","index":0}]]}]
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "Europe/London",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"versionId": "cc8f1b9a-3e4d-4f2a-8c9a-1b2c3d4e5f6a",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "YOUR_INSTANCE_ID"
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
{
|
||||
"name": "RedditVideoMakerBot Process Post",
|
||||
"tags": ["Reddit", "Video Automation", "Subworkflow", "FOSS", "ffmpeg"],
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"content": "# Reddit Video Maker Bot - Process Post Subworkflow (FOSS)\n\nProcesses a single Reddit post to create a video using FOSS tools.\n\n**Inputs:**\n- `reddit_post_data` (object): Data for the Reddit post.\n- `config` (object): Global configuration from main workflow.\n\n**Steps:**\n1. Fetch Reddit comments.\n2. Prepare script (title, selftext, comments).\n3. Generate TTS for each script part via 'Generate TTS' subworkflow.\n4. Concatenate all TTS audio files using ffmpeg.\n5. Select a random background video from local folder.\n6. Generate SRT captions from script.\n7. Assemble final video (background + audio + captions) using ffmpeg.\n8. Generate basic YouTube metadata.\n9. (Future Step: Upload to YouTube - HTTPRequest node for this is complex and omitted for brevity but would be similar to original user example if needed)\n\n**Prerequisites on n8n execution environment:**\n- As per Main and TTS workflows (ffmpeg, Piper, local folders)."
|
||||
},
|
||||
"name": "Sticky Note: Process Post Explanation",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"id": "StickyNote_ProcessPost_Explanation",
|
||||
"position": [0, -600]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowInputs": {
|
||||
"values": [
|
||||
{ "name": "reddit_post_data", "type": "object" },
|
||||
{ "name": "config", "type": "object" }
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "When Executed by Another Workflow",
|
||||
"type": "n8n-nodes-base.executeWorkflowTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"id": "ExecuteWorkflowTrigger_ProcessPost",
|
||||
"position": [0, -400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://www.reddit.com{{ $json.reddit_post_data.permalink }}.json",
|
||||
"options": { "response": { "responseFormat": "json" } },
|
||||
"headerParameters": { "parameters": [{ "name": "User-Agent", "value": "n8n-RedditVideoMakerBot/1.0" }] }
|
||||
},
|
||||
"name": "Reddit - Get Comments",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"id": "HttpRequest_GetComments",
|
||||
"position": [250, -400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const post = $node['When Executed by Another Workflow'].json.reddit_post_data;\nconst commentsResponse = $input.item.json; // Response from Get Comments\nlet script = [];\n\nscript.push({ text: post.title, speaker: 'author', duration_estimate: Math.max(3, post.title.length / 15) });\nif (post.is_self && post.selftext) {\n script.push({ text: post.selftext, speaker: 'author', duration_estimate: Math.max(5, post.selftext.length / 15) });\n}\n\nif (commentsResponse && commentsResponse.length > 1 && commentsResponse[1].data && commentsResponse[1].data.children) {\n const comments = commentsResponse[1].data.children;\n comments.slice(0, 5).forEach(commentItem => { // Top 5 comments\n if (commentItem.data && commentItem.data.body && commentItem.data.body.length > 10) { // Basic filter for comment quality\n script.push({ text: commentItem.data.body, speaker: 'commenter', duration_estimate: Math.max(3, commentItem.data.body.length / 15) });\n }\n });\n}\nreturn [{ json: { script: script, post_id: post.id } }];"
|
||||
},
|
||||
"name": "Code - Prepare Script & Estimate Durations",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "Code_PrepareScript",
|
||||
"position": [500, -400]
|
||||
},
|
||||
{
|
||||
"parameters": { "batchSize": 1, "fieldToSplitOut": "script", "options": {} },
|
||||
"name": "Split Script for TTS",
|
||||
"type": "n8n-nodes-base.splitInBatches",
|
||||
"typeVersion": 3,
|
||||
"id": "SplitInBatches_TTS",
|
||||
"position": [750, -400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"workflowId": { "__rl": true, "mode": "name", "value": "RedditVideoMakerBot Generate TTS" },
|
||||
"workflowInputs": {
|
||||
"value": {
|
||||
"text_segment": "={{ $json.text }}",
|
||||
"piper_voice_model_path": "={{ $json.speaker === 'author' ? $node['When Executed by Another Workflow'].json.config.piper_voice_model_path_author : $node['When Executed by Another Workflow'].json.config.piper_voice_model_path_commenter }}"
|
||||
}
|
||||
},
|
||||
"options": { "waitForSubWorkflow": true }
|
||||
},
|
||||
"name": "Execute Subworkflow: Generate TTS",
|
||||
"type": "n8n-nodes-base.executeWorkflow",
|
||||
"typeVersion": 1.2,
|
||||
"id": "ExecuteWorkflow_GenerateTTS",
|
||||
"position": [1000, -400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const items = $input.all();\nconst fs = require('fs');\nconst postId = items[0].json.post_id; // Ensure post_id is available from one of the items\nlet concatListContent = '';\nlet tempFilePaths = [];\n\nif (!fs.existsSync('audio_cache')) fs.mkdirSync('audio_cache', { recursive: true });\n\nfor (let i = 0; i < items.length; i++) {\n if (items[i].binary && items[i].binary.tts_audio_binary && items[i].binary.tts_audio_binary.data) {\n const audioData = Buffer.from(items[i].binary.tts_audio_binary.data, 'base64');\n const tempFileName = `audio_cache/segment_${postId}_${i}.mp3`;\n try {\n fs.writeFileSync(tempFileName, audioData);\n concatListContent += `file '${tempFileName.replace(/'/g, \"'\\\\\\\\'"')}'\\n`; // Escape single quotes in filenames for ffmpeg list\n tempFilePaths.push(tempFileName);\n } catch (e) {\n console.error(`Error writing temp audio file ${tempFileName}:`, e);\n throw e; // Propagate error\n }\n } else {\n console.warn(`Missing binary audio data for item ${i}`);\n }\n}\nconst listFilePath = `audio_cache/ffmpeg_concat_list_${postId}.txt`;\nfs.writeFileSync(listFilePath, concatListContent);\n\nreturn { json: { concat_list_path: listFilePath, concatenated_audio_path: `audio_cache/concatenated_audio_${postId}.mp3`, postId: postId, temp_audio_files: tempFilePaths } };"
|
||||
},
|
||||
"name": "Code - Write Audios & Prepare Concat List",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "Code_WriteAudiosAndPrepareConcatList",
|
||||
"position": [1250, -400],
|
||||
"notes": "Writes TTS binaries to temp files and creates ffmpeg concat list. Requires fs access."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "mkdir -p audio_cache && {{ $node['When Executed by Another Workflow'].json.config.ffmpeg_path }} -y -f concat -safe 0 -i {{ $json.concat_list_path }} -c copy {{ $json.concatenated_audio_path }}",
|
||||
"options": {"shell": true, "timeout": 120000 }
|
||||
},
|
||||
"name": "Execute Command: Concatenate Audio",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 2.1,
|
||||
"id": "ExecuteCommand_ConcatAudio",
|
||||
"position": [1500, -400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const fs = require('fs');\nconst folderPath = $node['When Executed by Another Workflow'].json.config.video_background_local_folder_path;\nlet background_video_path = '';\ntry {\n if (!fs.existsSync(folderPath)) { throw new Error(`Background video folder not found: ${folderPath}`); }\n const files = fs.readdirSync(folderPath);\n const mp4Files = files.filter(file => file.toLowerCase().endsWith('.mp4'));\n if (mp4Files.length > 0) {\n background_video_path = folderPath + (folderPath.endsWith('/') ? '' : '/') + mp4Files[Math.floor(Math.random() * mp4Files.length)];\n } else { throw new Error(`No .mp4 files found in ${folderPath}`); }\n} catch (e) {\n console.error('Error accessing background video folder:', e);\n throw e; // Propagate error\n}\nreturn { json: { ...$input.item.json, background_video_path: background_video_path } };"
|
||||
},
|
||||
"name": "Code - Get Random Background Video",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "Code_GetBackgroundVideo",
|
||||
"position": [0, -150],
|
||||
"notes": "Needs fs access. Ensure folder and MP4 files exist."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const script = $node['Code - Prepare Script & Estimate Durations'].json.script;\nconst postId = $input.item.json.postId; \nlet srtContent = '';\nlet currentTime = 0; // seconds\n\nfunction formatTime(seconds) {\n const h = Math.floor(seconds / 3600).toString().padStart(2, '0');\n const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');\n const s = Math.floor(seconds % 60).toString().padStart(2, '0');\n const ms = Math.floor((seconds - Math.floor(seconds)) * 1000).toString().padStart(3, '0');\n return `${h}:${m}:${s},${ms}`;\n}\n\nfor (let i = 0; i < script.length; i++) {\n const segment = script[i];\n const startTime = currentTime;\n const duration = segment.duration_estimate > 0 ? segment.duration_estimate : 3; // Ensure duration is positive\n const endTime = startTime + duration;\n \n srtContent += `${i + 1}\\n`;\n srtContent += `${formatTime(startTime)} --> ${formatTime(endTime)}\\n`;\n srtContent += `${segment.text.replace(/\\n/g, ' ')}\\n\\n`; // Replace newlines in text to avoid SRT issues\n \n currentTime = endTime;\n}\n\nconst fs = require('fs');\nif (!fs.existsSync('audio_cache')) fs.mkdirSync('audio_cache', { recursive: true });\nconst srtFilePath = `audio_cache/captions_${postId}.srt`;\nfs.writeFileSync(srtFilePath, srtContent);\n\nreturn { json: { ...$input.item.json, srt_file_path: srtFilePath, total_script_duration: currentTime } };"
|
||||
},
|
||||
"name": "Code - Generate SRT Captions",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"id": "Code_GenerateSRTCaptions",
|
||||
"position": [250, -150],
|
||||
"notes": "Needs fs access. Caption timing is based on estimated duration."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "mkdir -p results && {{ $node['When Executed by Another Workflow'].json.config.ffmpeg_path }} -y -i {{ $json.background_video_path }} -i {{ $json.concatenated_audio_path }} -vf \"subtitles='{{ $json.srt_file_path }}':force_style='FontFile={{ $node['When Executed by Another Workflow'].json.config.caption_font_path }},FontSize={{ $node['When Executed by Another Workflow'].json.config.caption_font_size }},PrimaryColour=&HFF{{ $node['When Executed by Another Workflow'].json.config.caption_color.replace('#', '') }}',scale={{ $node['When Executed by Another Workflow'].json.config.output_resolution }}\" -c:v libx264 -c:a aac -strict experimental -shortest -t {{ $json.total_script_duration + 1 }} results/final_video_{{ $json.postId }}.mp4",
|
||||
"options": { "shell": true, "timeout": 600000 }
|
||||
},
|
||||
"name": "Execute Command: Assemble Video",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 2.1,
|
||||
"id": "ExecuteCommand_AssembleVideo",
|
||||
"position": [500, -150],
|
||||
"notes": "Timeout 10 mins. Ensure font path (escaped) and color format are correct for ffmpeg. PrimaryColour is AABBGGRR."
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"values": {
|
||||
"string": [
|
||||
{ "name": "youtube_title", "value": "Reddit Story: {{ $node['When Executed by Another Workflow'].json.reddit_post_data.title }}" },
|
||||
{ "name": "youtube_description", "value": "A video generated from the Reddit post titled: {{ $node['When Executed by Another Workflow'].json.reddit_post_data.title }}\\n\\n{{ $node['When Executed by Another Workflow'].json.reddit_post_data.selftext ? $node['When Executed by Another Workflow'].json.reddit_post_data.selftext.substring(0,500) : '' }}\\n\\n#Reddit #AskReddit #Shorts" },
|
||||
{ "name": "final_video_path", "value": "results/final_video_{{ $json.postId }}.mp4" }
|
||||
]
|
||||
},
|
||||
"options": {"keepOnlySet": false}
|
||||
},
|
||||
"name": "Set - YouTube Metadata & Final Path",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"id": "Set_YouTubeMetadata",
|
||||
"position": [750, -150]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"ExecuteWorkflowTrigger_ProcessPost": [{"main":[[{"node":"Reddit - Get Comments","type":"main","index":0}]]}],
|
||||
"Reddit - Get Comments": [{"main":[[{"node":"Code - Prepare Script & Estimate Durations","type":"main","index":0}]]}],
|
||||
"Code - Prepare Script & Estimate Durations": [{"main":[[{"node":"Split Script for TTS","type":"main","index":0}]]}],
|
||||
"Split Script for TTS": [{"main":[[{"node":"Execute Subworkflow: Generate TTS","type":"main","index":0}]]}],
|
||||
"Execute Subworkflow: Generate TTS": [{"main":[[{"node":"Code - Write Audios & Prepare Concat List","type":"main","index":0}]]}],
|
||||
"Code - Write Audios & Prepare Concat List": [{"main":[[{"node":"Execute Command: Concatenate Audio","type":"main","index":0}]]}],
|
||||
"Execute Command: Concatenate Audio": [{"main":[[{"node":"Code - Get Random Background Video","type":"main","index":0}]]}],
|
||||
"Code - Get Random Background Video": [{"main":[[{"node":"Code - Generate SRT Captions","type":"main","index":0}]]}],
|
||||
"Code - Generate SRT Captions": [{"main":[[{"node":"Execute Command: Assemble Video","type":"main","index":0}]]}],
|
||||
"Execute Command: Assemble Video": [{"main":[[{"node":"Set - YouTube Metadata & Final Path","type":"main","index":0}]]}]
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1",
|
||||
"timezone": "Europe/London",
|
||||
"callerPolicy": "workflowsFromSameOwner"
|
||||
},
|
||||
"versionId": "9d5e2c8f-1a2b-3c4d-5e6f-7a8b9c0d1e2f",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "YOUR_INSTANCE_ID"
|
||||
}
|
||||
}
|
Loading…
Reference in new issue