diff --git a/RedditVideoMakerBotGenerateTTS.json b/RedditVideoMakerBotGenerateTTS.json new file mode 100644 index 0000000..d1d0995 --- /dev/null +++ b/RedditVideoMakerBotGenerateTTS.json @@ -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" + } +} diff --git a/RedditVideoMakerBotMain.json b/RedditVideoMakerBotMain.json new file mode 100644 index 0000000..3addf85 --- /dev/null +++ b/RedditVideoMakerBotMain.json @@ -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" + } +} diff --git a/RedditVideoMakerBotProcessPost.json b/RedditVideoMakerBotProcessPost.json new file mode 100644 index 0000000..b5a156f --- /dev/null +++ b/RedditVideoMakerBotProcessPost.json @@ -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" + } +}