feat: RSO observation system, child safety, Discord adapter, Telegram watchdog, email attachments
Core agent improvements: - RSO (Relevance Scoring & Observation) system: interaction_logger, memory_scorer, signal_detector - Memory access logging (memory_access_log table) for relevance scoring; high-signal turn detection - Rich conversation storage for notable turns; compact_conversation truncates long user messages - Task-type classifier (query/action/analysis/creative) for observation tagging - Nested sub-agent visibility: deep delegations now register against the main agent's manager Child safety (Gabriel profile): - child_safety.py: filtering, audit logging, prompt constants for restricted sessions - .kiro/specs/child-safety-profile: requirements, design, tasks specs - GABRIEL_BOT_PROPOSAL.md: initial proposal doc - Reduced context window (10 msgs) and tutor-mode identity for restricted users Telegram adapter: - Polling watchdog: auto-restarts updater if polling drops unexpectedly - get_me() with exponential-backoff retry on NetworkError at startup - Correct stop() ordering: signal watchdog before cancelling tasks Email / Gmail: - send_email: supports file attachments (attachments list param) - get_email: surfaces attachment metadata in response Scheduled tasks / weather: - Remove OpenWeatherMap API calls from morning-weather task; use wttr.in exclusively - New scheduled tasks and scheduler state persistence Discord: - adapters/discord/__init__.py scaffold - discord-plugin: MCP plugin for Claude Code Discord integration (server.ts, skills, config) Infrastructure: - n8n workflow exports (garvis_webhook, content_pipeline variants) - memory_workspace: context, homelab-repo-updates, weekly observation summaries, error logs - UCS C240 migration plan doc - requirements.txt: new deps - .claude/settings.json, fix_hooks.py: hook/permission tuning
This commit is contained in:
607
n8n_workflows/content_pipeline.json
Normal file
607
n8n_workflows/content_pipeline.json
Normal file
@@ -0,0 +1,607 @@
|
||||
{
|
||||
"name": "Content Pipeline - BlendedFamilyKitchen",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{"field": "minutes", "minutesInterval": 30}]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000001",
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.2,
|
||||
"position": [0, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/auth.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.API.Auth"},
|
||||
{"name": "version", "value": "6"},
|
||||
{"name": "method", "value": "login"},
|
||||
{"name": "account", "value": "={{ $env.SYNOLOGY_USER }}"},
|
||||
{"name": "passwd", "value": "={{ $env.SYNOLOGY_PASS }}"},
|
||||
{"name": "format", "value": "sid"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000002",
|
||||
"name": "NAS Login",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [220, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "/BlendedFamilyKitchen/DropZone"},
|
||||
{"name": "_sid", "value": "={{ $json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000003",
|
||||
"name": "Poll NAS DropZone",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [440, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond1",
|
||||
"leftValue": "={{ $json.data.files.length }}",
|
||||
"rightValue": "0",
|
||||
"operator": {"type": "number", "operation": "gt"}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000004",
|
||||
"name": "IF New Files?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [660, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "data.files"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000005",
|
||||
"name": "Split Into Batches",
|
||||
"type": "n8n-nodes-base.splitOut",
|
||||
"typeVersion": 1,
|
||||
"position": [880, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "a1", "name": "filename", "value": "={{ $json.name }}", "type": "string"},
|
||||
{"id": "a2", "name": "filepath", "value": "={{ $json.path }}", "type": "string"},
|
||||
{"id": "a3", "name": "filesize", "value": "={{ $json.additional?.size }}", "type": "number"},
|
||||
{"id": "a4", "name": "timestamp", "value": "={{ $now.toISO() }}", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000006",
|
||||
"name": "Extract Metadata",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [1100, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Download"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "download"},
|
||||
{"name": "path", "value": "={{ $json.filepath }}"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
},
|
||||
"options": {"response": {"response": {"responseFormat": "file"}}}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000007",
|
||||
"name": "Download Raw Video",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1320, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/{{ $json.filename }} -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/{{ $json.filename }}_audio.wav && echo '{\"status\":\"ok\",\"audio_file\":\"/tmp/{{ $json.filename }}_audio.wav\"}'"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000008",
|
||||
"name": "FFmpeg Extract Audio",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [1540, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:9000/asr",
|
||||
"sendBody": true,
|
||||
"contentType": "multipart-form-data",
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "audio_file", "value": "={{ $json.audio_file }}"},
|
||||
{"name": "task", "value": "transcribe"},
|
||||
{"name": "language", "value": "en"},
|
||||
{"name": "output", "value": "srt"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000009",
|
||||
"name": "Whisper Transcribe",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1760, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "x-api-key", "value": "={{ $env.ANTHROPIC_API_KEY }}"},
|
||||
{"name": "anthropic-version", "value": "2023-06-01"},
|
||||
{"name": "content-type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"model\": \"claude-haiku-4-5-20251001\",\n \"max_tokens\": 500,\n \"messages\": [{\n \"role\": \"user\",\n \"content\": \"You are a TikTok content strategist for a blended family cooking channel. Given this video transcript, generate:\\n1. Three hook text options (bold, attention-grabbing, 5-8 words max)\\n2. A brief video description with relevant hashtags\\n3. Best posting time suggestion\\n\\nTranscript: {{ $json.data }}\\n\\nRespond in JSON format: {hooks: [str,str,str], description: str, hashtags: [str], best_time: str}\"\n }]\n}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000010",
|
||||
"name": "AI Generate Hooks",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1980, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/{{ $('Extract Metadata').item.json.filename }} \\\n -vf \"scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,eq=brightness=0.04:contrast=1.1:saturation=1.15,unsharp=5:5:0.3\" \\\n -af \"loudnorm=I=-16:TP=-1.5:LRA=11\" \\\n -c:v libx264 -preset medium -b:v 6M \\\n -c:a aac -b:a 128k \\\n /tmp/{{ $('Extract Metadata').item.json.filename }}_normalized.mp4 && echo '{\"status\":\"ok\"}'"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000011",
|
||||
"name": "FFmpeg Normalize Video",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2200, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/{{ $('Extract Metadata').item.json.filename }}_normalized.mp4 \\\n -vf \"subtitles=/tmp/{{ $('Extract Metadata').item.json.filename }}_captions.srt:force_style='FontName=Arial,FontSize=22,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2,Shadow=1,Alignment=2,MarginV=40'\" \\\n -c:v libx264 -preset medium -b:v 6M \\\n -c:a copy \\\n /tmp/{{ $('Extract Metadata').item.json.filename }}_captioned.mp4 && echo '{\"status\":\"ok\"}'"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000012",
|
||||
"name": "FFmpeg Burn Captions",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2420, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/{{ $('Extract Metadata').item.json.filename }}_captioned.mp4 \\\n -i /opt/music_library/kitchen_vibes/track_01.mp3 \\\n -filter_complex \"[1:a]volume=0.12[bg];[0:a][bg]amix=inputs=2:duration=first\" \\\n -c:v copy -c:a aac -b:a 128k \\\n /tmp/{{ $('Extract Metadata').item.json.filename }}_final.mp4 && echo '{\"status\":\"ok\"}'"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000013",
|
||||
"name": "FFmpeg Mix Background Music",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2640, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/{{ $('Extract Metadata').item.json.filename }}_final.mp4 \\\n -vf \"select='gt(scene,0.3)',scale=1080:1920\" \\\n -frames:v 1 \\\n /tmp/{{ $('Extract Metadata').item.json.filename }}_thumbnail.jpg && echo '{\"status\":\"ok\"}'"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000014",
|
||||
"name": "FFmpeg Extract Thumbnail",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2860, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://ads.tiktok.com/creative_radar_api/v1/popular_trend/sound/list",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{"name": "period", "value": "7"},
|
||||
{"name": "page", "value": "1"},
|
||||
{"name": "limit", "value": "10"},
|
||||
{"name": "country_code", "value": "US"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000015",
|
||||
"name": "Scrape Trending Sounds",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [2860, 500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "s1", "name": "trending_sounds", "value": "={{ $json.data?.sound_list?.slice(0,3).map(s => s.title + ' by ' + s.author).join('\\n') || 'No trending data available' }}", "type": "string"},
|
||||
{"id": "s2", "name": "curated_picks", "value": "🍳 Kitchen Vibes:\n• Cozy Cooking Lo-Fi (royalty-free)\n• Sunday Morning Jazz (royalty-free)\n• Feel Good Acoustic (royalty-free)", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000016",
|
||||
"name": "Format Sound Suggestions",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [3080, 500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"mergeByFields": {},
|
||||
"combinationMode": "mergeByPosition",
|
||||
"options": {}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000017",
|
||||
"name": "Merge Results",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"position": [3300, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Upload"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "upload"},
|
||||
{"name": "path", "value": "/BlendedFamilyKitchen/Processed"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000018",
|
||||
"name": "Upload to NAS Processed",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [3520, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\",\n \"parse_mode\": \"HTML\",\n \"text\": \"🎬 <b>New Video Ready for Review!</b>\\n\\n📁 <b>File:</b> {{ $('Extract Metadata').item.json.filename }}\\n\\n✍️ <b>Hook Options:</b>\\n1️⃣ {{ $('AI Generate Hooks').item.json.content[0].text.hooks[0] }}\\n2️⃣ {{ $('AI Generate Hooks').item.json.content[0].text.hooks[1] }}\\n3️⃣ {{ $('AI Generate Hooks').item.json.content[0].text.hooks[2] }}\\n\\n📝 <b>Description:</b>\\n{{ $('AI Generate Hooks').item.json.content[0].text.description }}\\n\\n🎵 <b>Trending Sounds This Week:</b>\\n{{ $('Format Sound Suggestions').item.json.trending_sounds }}\\n\\n🍳 <b>Kitchen-Appropriate Picks:</b>\\n{{ $('Format Sound Suggestions').item.json.curated_picks }}\\n\\n⏰ <b>Best Time to Post:</b> {{ $('AI Generate Hooks').item.json.content[0].text.best_time }}\\n\\n👇 <b>What would you like to do?</b>\",\n \"reply_markup\": {\n \"inline_keyboard\": [\n [{\"text\": \"✅ Approve & Post\", \"callback_data\": \"approve_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"📝 Edit First\", \"callback_data\": \"edit_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"🎵 Change Sound\", \"callback_data\": \"sound_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"❌ Reject\", \"callback_data\": \"reject_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"⏰ Schedule for Later\", \"callback_data\": \"schedule_{{ $('Extract Metadata').item.json.filename }}\"}]\n ]\n }\n}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000019",
|
||||
"name": "Notify Cloe via Telegram",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [3740, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/getUpdates",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{"name": "offset", "value": "-1"},
|
||||
{"name": "timeout", "value": "30"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000020",
|
||||
"name": "Wait for Cloe Response",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [3960, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"rules": [
|
||||
{"value": "approve", "output": 0},
|
||||
{"value": "edit", "output": 1},
|
||||
{"value": "sound", "output": 2},
|
||||
{"value": "reject", "output": 3},
|
||||
{"value": "schedule", "output": 4}
|
||||
]
|
||||
},
|
||||
"value": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000021",
|
||||
"name": "Switch Cloe Decision",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3,
|
||||
"position": [4180, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\", \"text\": \"✅ Video approved! Moving to publish queue...\"}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000022",
|
||||
"name": "Handle Approve",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4400, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\", \"text\": \"📝 Opening video in edit mode. Make your changes and re-drop when ready!\"}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000023",
|
||||
"name": "Handle Edit",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4400, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\", \"text\": \"🎵 Pick a sound to use:\\n\\n🔥 Trending:\\n{{ $('Format Sound Suggestions').item.json.trending_sounds }}\\n\\n🍳 Kitchen Picks:\\n{{ $('Format Sound Suggestions').item.json.curated_picks }}\\n\\nReply with the sound name or paste a TikTok sound link!\"}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000024",
|
||||
"name": "Handle Sound Change",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4400, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\", \"text\": \"❌ Video rejected. Moving to archive.\"}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000025",
|
||||
"name": "Handle Reject",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4400, 480]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\"chat_id\": \"{{ $env.CLOE_CHAT_ID }}\", \"text\": \"⏰ When should this go live? Reply with a date/time (e.g., 'Tomorrow 6pm' or 'Friday 12pm MST')\"}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000026",
|
||||
"name": "Handle Schedule",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4400, 620]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Rename"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "rename"},
|
||||
{"name": "path", "value": "={{ $('Extract Metadata').item.json.filepath }}"},
|
||||
{"name": "name", "value": "=archived_{{ $('Extract Metadata').item.json.filename }}"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000027",
|
||||
"name": "Archive Original on NAS",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [4620, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000028",
|
||||
"name": "Error Trigger",
|
||||
"type": "n8n-nodes-base.errorTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"{{ $env.JORDAN_CHAT_ID }}\",\n \"parse_mode\": \"HTML\",\n \"text\": \"🚨 <b>Content Pipeline Error</b>\\n\\n<b>Node:</b> {{ $json.execution?.error?.node?.name || 'Unknown' }}\\n<b>Error:</b> {{ $json.execution?.error?.message || 'Unknown error' }}\\n<b>Execution:</b> {{ $json.execution?.id }}\\n\\n<a href='http://192.168.2.113:5678/workflow/{{ $json.workflow?.id }}/executions/{{ $json.execution?.id }}'>View in n8n</a>\"\n}"
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000029",
|
||||
"name": "Notify Jordan Error",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [220, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 📁 Stage 1: NAS Drop Zone Polling\n\n**How it works:**\nPolls Synology NAS every 30 min for new files in /BlendedFamilyKitchen/DropZone\n\n**Cloe's workflow:**\n1. Records video on phone\n2. Saves/uploads to NAS DropZone folder\n3. Pipeline auto-detects and processes\n\n**TODO - Infrastructure:**\n- [ ] Create NAS folders: /BlendedFamilyKitchen/DropZone, /Processed, /Archive\n- [ ] Set up Synology FileStation API access\n- [ ] Add SYNOLOGY_USER and SYNOLOGY_PASS to n8n env vars\n- [ ] Test NAS API connectivity from CT 113\n- [ ] Set up Synology DS file app on Cloe's phone for easy upload",
|
||||
"width": 520,
|
||||
"height": 380
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000030",
|
||||
"name": "Sticky - NAS Polling",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-40, -140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🤖 Stage 2: AI Processing Pipeline\n\n**Processing chain:**\n1. FFmpeg extracts audio → Whisper transcribes → .srt captions\n2. Claude AI generates 3 hook options + description + hashtags\n3. FFmpeg normalizes video (9:16, color, audio levels)\n4. FFmpeg burns captions (white text, black outline, bottom-third)\n5. FFmpeg mixes low-volume background music from curated library\n6. FFmpeg extracts best thumbnail frame (scene detection)\n\n**TODO - Infrastructure:**\n- [ ] Deploy Whisper Docker container (onerahmet/openai-whisper-asr-webservice)\n- [ ] Install FFmpeg on CT 113 (apt install ffmpeg)\n- [ ] Add ANTHROPIC_API_KEY to n8n env vars\n- [ ] Create /opt/music_library/kitchen_vibes/ with 10-20 royalty-free tracks\n- [ ] Test full processing chain with a sample video",
|
||||
"width": 560,
|
||||
"height": 420
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000031",
|
||||
"name": "Sticky - AI Processing",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1280, -180]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🎵 Stage 3: Sound Suggestions\n\n**Trending Sounds:**\nScrapes TikTok Creative Center API weekly for top 10 trending sounds in US region. Presents top 3 to Cloe as suggestions.\n\n**Curated Kitchen Picks:**\nPre-selected royalty-free tracks appropriate for cooking content. Cloe can choose from these without worrying about copyright.\n\n**Cloe's autonomy preserved:**\n- She picks the final sound (or uses her own)\n- Suggestions are recommendations, not auto-applied\n- She can paste any TikTok sound link to use instead\n\n**TODO:**\n- [ ] Test TikTok Creative Radar API access\n- [ ] Build curated music library (royalty-free)\n- [ ] If API blocked, fallback to manual weekly curation",
|
||||
"width": 520,
|
||||
"height": 420
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000032",
|
||||
"name": "Sticky - Sound Suggestions",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2820, 680]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 📱 Stage 4: Cloe Approval Flow\n\n**Telegram notification includes:**\n- Video filename and preview info\n- 3 AI-generated hook options (she picks one or writes her own)\n- Auto-generated description with hashtags\n- Trending sound suggestions + curated kitchen picks\n- Best posting time recommendation\n\n**Cloe's options (inline keyboard buttons):**\n✅ Approve & Post — publishes as-is with selected hook\n📝 Edit First — sends video back for manual edits\n🎵 Change Sound — shows full sound list to pick from\n❌ Reject — archives the video\n⏰ Schedule — asks for preferred date/time\n\n**TODO:**\n- [ ] Set up Telegram bot for Cloe (or use Garvis bot)\n- [ ] Add TELEGRAM_BOT_TOKEN and CLOE_CHAT_ID to n8n env\n- [ ] Implement callback query handler for button responses\n- [ ] Test full approval flow end-to-end",
|
||||
"width": 560,
|
||||
"height": 480
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000033",
|
||||
"name": "Sticky - Cloe Approval",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [3680, -160]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🚨 Error Handling\n\n**On any node failure:**\n- Error trigger catches the failure\n- Sends detailed error notification to Jordan via Telegram\n- Includes: failed node name, error message, execution ID\n- Direct link to the failed execution in n8n UI\n\n**TODO:**\n- [ ] Add JORDAN_CHAT_ID to n8n env vars\n- [ ] Test error handling with intentional failure\n- [ ] Consider retry logic for transient failures (NAS timeout, API rate limit)",
|
||||
"width": 480,
|
||||
"height": 320
|
||||
},
|
||||
"id": "a1b2c3d4-1111-4000-8000-000000000034",
|
||||
"name": "Sticky - Error Handling",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-40, 560]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Schedule Trigger": {
|
||||
"main": [[{"node": "NAS Login", "type": "main", "index": 0}]]
|
||||
},
|
||||
"NAS Login": {
|
||||
"main": [[{"node": "Poll NAS DropZone", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Poll NAS DropZone": {
|
||||
"main": [[{"node": "IF New Files?", "type": "main", "index": 0}]]
|
||||
},
|
||||
"IF New Files?": {
|
||||
"main": [
|
||||
[{"node": "Split Into Batches", "type": "main", "index": 0}],
|
||||
[]
|
||||
]
|
||||
},
|
||||
"Split Into Batches": {
|
||||
"main": [[{"node": "Extract Metadata", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Extract Metadata": {
|
||||
"main": [[{"node": "Download Raw Video", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Download Raw Video": {
|
||||
"main": [[{"node": "FFmpeg Extract Audio", "type": "main", "index": 0}]]
|
||||
},
|
||||
"FFmpeg Extract Audio": {
|
||||
"main": [[{"node": "Whisper Transcribe", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Whisper Transcribe": {
|
||||
"main": [[{"node": "AI Generate Hooks", "type": "main", "index": 0}]]
|
||||
},
|
||||
"AI Generate Hooks": {
|
||||
"main": [[{"node": "FFmpeg Normalize Video", "type": "main", "index": 0}]]
|
||||
},
|
||||
"FFmpeg Normalize Video": {
|
||||
"main": [[{"node": "FFmpeg Burn Captions", "type": "main", "index": 0}]]
|
||||
},
|
||||
"FFmpeg Burn Captions": {
|
||||
"main": [[{"node": "FFmpeg Mix Background Music", "type": "main", "index": 0}]]
|
||||
},
|
||||
"FFmpeg Mix Background Music": {
|
||||
"main": [[{"node": "FFmpeg Extract Thumbnail", "type": "main", "index": 0}]]
|
||||
},
|
||||
"FFmpeg Extract Thumbnail": {
|
||||
"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Scrape Trending Sounds": {
|
||||
"main": [[{"node": "Format Sound Suggestions", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Format Sound Suggestions": {
|
||||
"main": [[{"node": "Merge Results", "type": "main", "index": 1}]]
|
||||
},
|
||||
"Merge Results": {
|
||||
"main": [[{"node": "Upload to NAS Processed", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Upload to NAS Processed": {
|
||||
"main": [[{"node": "Notify Cloe via Telegram", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Notify Cloe via Telegram": {
|
||||
"main": [[{"node": "Wait for Cloe Response", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Wait for Cloe Response": {
|
||||
"main": [[{"node": "Switch Cloe Decision", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Switch Cloe Decision": {
|
||||
"main": [
|
||||
[{"node": "Handle Approve", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Edit", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Sound Change", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Reject", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Schedule", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Handle Approve": {
|
||||
"main": [[{"node": "Archive Original on NAS", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Error Trigger": {
|
||||
"main": [[{"node": "Notify Jordan Error", "type": "main", "index": 0}]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [],
|
||||
"triggerCount": 1,
|
||||
"active": false
|
||||
}
|
||||
1
n8n_workflows/content_pipeline_clean.json
Normal file
1
n8n_workflows/content_pipeline_clean.json
Normal file
File diff suppressed because one or more lines are too long
755
n8n_workflows/content_pipeline_v2.json
Normal file
755
n8n_workflows/content_pipeline_v2.json
Normal file
@@ -0,0 +1,755 @@
|
||||
{
|
||||
"name": "Content Pipeline - BlendedFamilyKitchen",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{"field": "minutes", "minutesInterval": 30}]
|
||||
}
|
||||
},
|
||||
"name": "Schedule Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [260, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/auth.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.API.Auth"},
|
||||
{"name": "version", "value": "3"},
|
||||
{"name": "method", "value": "login"},
|
||||
{"name": "account", "value": "TODO_NAS_USER"},
|
||||
{"name": "passwd", "value": "TODO_NAS_PASS"},
|
||||
{"name": "session", "value": "FileStation"},
|
||||
{"name": "format", "value": "sid"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "NAS Login",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [480, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "/BlendedFamilyKitchen/DropZone"},
|
||||
{"name": "_sid", "value": "={{ $json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Poll NAS DropZone",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [700, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "cond1",
|
||||
"leftValue": "={{ $json.data?.files?.length }}",
|
||||
"rightValue": "0",
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "gt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"name": "IF New Files?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [920, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"fieldToSplitOut": "data.files"
|
||||
},
|
||||
"name": "Split Into Batches",
|
||||
"type": "n8n-nodes-base.splitOut",
|
||||
"typeVersion": 1,
|
||||
"position": [1140, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "a1", "name": "filename", "value": "={{ $json.name }}", "type": "string"},
|
||||
{"id": "a2", "name": "filepath", "value": "={{ $json.path }}", "type": "string"},
|
||||
{"id": "a3", "name": "size", "value": "={{ $json.additional?.size }}", "type": "number"},
|
||||
{"id": "a4", "name": "created", "value": "={{ $json.additional?.time?.crtime }}", "type": "string"},
|
||||
{"id": "a5", "name": "extension", "value": "={{ $json.name.split('.').pop().toLowerCase() }}", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Extract Metadata",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [1360, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "=http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Download"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "download"},
|
||||
{"name": "path", "value": "={{ $json.filepath }}"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"response": {"response": {"responseFormat": "file"}}
|
||||
}
|
||||
},
|
||||
"name": "Download Raw Video",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [1580, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/bfk_raw/{{ $json.filename }} -vn -acodec pcm_s16le -ar 16000 -ac 1 /tmp/bfk_audio/{{ $json.filename.replace(/\\.[^.]+$/, '.wav') }} && echo '{\"audio_path\": \"/tmp/bfk_audio/{{ $json.filename.replace(/\\.[^.]+$/, '.wav') }}\", \"video_path\": \"/tmp/bfk_raw/{{ $json.filename }}\"}'"
|
||||
},
|
||||
"name": "FFmpeg Extract Audio",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [1800, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://localhost:9000/asr",
|
||||
"sendBody": true,
|
||||
"contentType": "multipart-form-data",
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "audio_file", "value": "TODO: binary from audio extraction"},
|
||||
{"name": "output", "value": "srt"},
|
||||
{"name": "language", "value": "en"},
|
||||
{"name": "word_timestamps", "value": "true"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Whisper Transcribe",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [2020, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.anthropic.com/v1/messages",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "x-api-key", "value": "TODO_CLAUDE_API_KEY"},
|
||||
{"name": "anthropic-version", "value": "2023-06-01"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"model\": \"claude-sonnet-4-20250514\",\n \"max_tokens\": 500,\n \"messages\": [{\n \"role\": \"user\",\n \"content\": \"You are a TikTok content expert for a blended family cooking channel. Given this transcript, generate: 1) Three hook text options (max 8 words, attention-grabbing, food-focused), 2) A caption/description with hashtags, 3) Three title options. Transcript: {{ $json.text }}\"\n }]\n}"
|
||||
},
|
||||
"name": "AI Generate Hooks",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [2240, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/bfk_raw/{{ $('Extract Metadata').item.json.filename }} -vf \"scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,eq=brightness=0.04:contrast=1.05:saturation=1.15,unsharp=5:5:0.3\" -c:v libx264 -preset medium -crf 23 -c:a aac -b:a 128k -ar 44100 -movflags +faststart /tmp/bfk_processed/normalized_{{ $('Extract Metadata').item.json.filename }} && echo 'normalized'"
|
||||
},
|
||||
"name": "FFmpeg Normalize Video",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2460, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/bfk_processed/normalized_{{ $('Extract Metadata').item.json.filename }} -vf \"subtitles=/tmp/bfk_subs/{{ $('Extract Metadata').item.json.filename.replace(/\\.[^.]+$/, '.srt') }}:force_style='FontName=Arial,FontSize=14,PrimaryColour=&H00FFFFFF,OutlineColour=&H00000000,Outline=2,Shadow=1,Alignment=2,MarginV=40'\" -c:v libx264 -preset medium -crf 23 -c:a copy /tmp/bfk_processed/captioned_{{ $('Extract Metadata').item.json.filename }} && echo 'captioned'"
|
||||
},
|
||||
"name": "FFmpeg Burn Captions",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2680, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/bfk_processed/captioned_{{ $('Extract Metadata').item.json.filename }} -i /data/music/kitchen_bg_01.mp3 -filter_complex \"[1:a]volume=0.12[bg];[0:a][bg]amix=inputs=2:duration=first:dropout_transition=2[aout]\" -map 0:v -map \"[aout]\" -c:v copy -c:a aac -b:a 128k /tmp/bfk_processed/final_{{ $('Extract Metadata').item.json.filename }} && echo 'music_mixed'"
|
||||
},
|
||||
"name": "FFmpeg Mix Background Music",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [2900, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=ffmpeg -i /tmp/bfk_processed/final_{{ $('Extract Metadata').item.json.filename }} -vf \"select=gt(scene\\,0.3)\" -frames:v 1 -q:v 2 /tmp/bfk_thumbs/thumb_{{ $('Extract Metadata').item.json.filename.replace(/\\.[^.]+$/, '.jpg') }} && echo 'thumbnail_extracted'"
|
||||
},
|
||||
"name": "FFmpeg Extract Thumbnail",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [3120, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "GET",
|
||||
"url": "https://ads.tiktok.com/creative_radar_api/v1/popular_trend/sound/list",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{"name": "period", "value": "7"},
|
||||
{"name": "page", "value": "1"},
|
||||
{"name": "limit", "value": "10"},
|
||||
{"name": "country_code", "value": "US"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Scrape Trending Sounds",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [2900, 520]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "s1",
|
||||
"name": "trending_sounds",
|
||||
"value": "={{ $json.data?.sound_list?.slice(0,3).map(s => s.title + ' by ' + s.author).join('\\n') || 'No trending data available' }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "s2",
|
||||
"name": "curated_kitchen_sounds",
|
||||
"value": "1. Upbeat Cooking Vibes - kitchen_bg_01.mp3\n2. Morning Kitchen Jazz - kitchen_bg_02.mp3\n3. Family Dinner Warmth - kitchen_bg_03.mp3\n4. Quick Recipe Energy - kitchen_bg_04.mp3\n5. Sunday Cook Chill - kitchen_bg_05.mp3",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "s3",
|
||||
"name": "sound_recommendation",
|
||||
"value": "Curated kitchen tracks auto-applied at low volume. Trending sounds listed below for optional manual swap in TikTok app before posting.",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Format Sound Suggestions",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [3120, 520]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "combine",
|
||||
"combinationMode": "mergeByPosition",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Merge Results",
|
||||
"type": "n8n-nodes-base.merge",
|
||||
"typeVersion": 3,
|
||||
"position": [3400, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Upload"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "upload"},
|
||||
{"name": "path", "value": "/BlendedFamilyKitchen/Processed"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Upload to NAS Processed",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [3620, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"🎬 *New Video Ready!*\\n\\n📁 {{ $('Extract Metadata').item.json.filename }}\\n\\n*Hook Options:*\\n{{ $('AI Generate Hooks').item.json.content?.[0]?.text || 'Processing...' }}\\n\\n*Sound Options:*\\n🎵 _Auto-applied:_ Kitchen background track\\n\\n📈 _Trending sounds this week:_\\n{{ $json.trending_sounds || 'No trending data' }}\\n\\n🎶 _Curated kitchen library:_\\n{{ $json.curated_kitchen_sounds }}\\n\\nTap below to decide:\",\n \"parse_mode\": \"Markdown\",\n \"reply_markup\": {\n \"inline_keyboard\": [\n [{\"text\": \"✅ Approve & Post\", \"callback_data\": \"approve_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"📝 Edit First\", \"callback_data\": \"edit_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"🎵 Change Sound\", \"callback_data\": \"sound_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"⏰ Schedule\", \"callback_data\": \"schedule_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"❌ Reject\", \"callback_data\": \"reject_{{ $('Extract Metadata').item.json.filename }}\"}]\n ]\n }\n}"
|
||||
},
|
||||
"name": "Notify Cloe Telegram",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [3840, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/getUpdates",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{"name": "offset", "value": "-1"},
|
||||
{"name": "timeout", "value": "30"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Wait for Cloe Response",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4060, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "approve", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Approve"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "edit", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Edit"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "sound", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Sound"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "reject", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Reject"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "schedule", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Schedule"}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "Switch Cloe Decision",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3,
|
||||
"position": [4280, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://open.tiktokapis.com/v2/post/publish/video/init/",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\n \"post_info\": {\n \"title\": \"TODO: from AI hooks\",\n \"privacy_level\": \"PUBLIC_TO_EVERYONE\"\n },\n \"source_info\": {\n \"source\": \"FILE_UPLOAD\"\n }\n}"
|
||||
},
|
||||
"name": "Handle Approve",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4540, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"📝 Opening video for edit. File is in NAS /Processed folder. Reply here when done editing and I'll re-process.\"\n}"
|
||||
},
|
||||
"name": "Handle Edit",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4540, 220]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"🎵 Sound change requested. Apply your chosen trending sound in the TikTok app, then tap Approve to post.\\n\\nThis week's trending:\\n{{ $('Format Sound Suggestions').item.json.trending_sounds }}\"\n}"
|
||||
},
|
||||
"name": "Handle Sound Change",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4540, 380]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"❌ Video rejected. Moving to /Rejected folder on NAS. File: {{ $('Extract Metadata').item.json.filename }}\"\n}"
|
||||
},
|
||||
"name": "Handle Reject",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4540, 540]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"⏰ Schedule mode. Reply with your preferred post time (e.g. 'tomorrow 6pm', 'friday 12pm') and I'll queue it up.\"\n}"
|
||||
},
|
||||
"name": "Handle Schedule",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4540, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Rename"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "rename"},
|
||||
{"name": "path", "value": "={{ $('Extract Metadata').item.json.filepath }}"},
|
||||
{"name": "name", "value": "=/BlendedFamilyKitchen/Archive/{{ $('Extract Metadata').item.json.filename }}"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Archive Original on NAS",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [4760, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "Error Trigger",
|
||||
"type": "n8n-nodes-base.errorTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [260, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_JORDAN_CHAT_ID\",\n \"text\": \"🚨 *Content Pipeline Error*\\n\\nWorkflow: {{ $workflow.name }}\\nNode: {{ $json.execution?.error?.node?.name || 'Unknown' }}\\nError: {{ $json.execution?.error?.message || 'Unknown error' }}\\nTimestamp: {{ new Date().toLocaleString('en-US', {timeZone: 'America/Denver'}) }}\",\n \"parse_mode\": \"Markdown\"\n}"
|
||||
},
|
||||
"name": "Notify Jordan Error",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [480, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 📂 Stage 1: NAS Drop Zone Polling\n\nPolls Synology NAS every 30 min for new files in /BlendedFamilyKitchen/DropZone.\n\n**Folder Structure Needed on NAS:**\n- /BlendedFamilyKitchen/DropZone — Cloe drops raw videos here\n- /BlendedFamilyKitchen/Processed — enhanced videos land here\n- /BlendedFamilyKitchen/Archive — originals moved here after processing\n- /BlendedFamilyKitchen/Rejected — rejected videos moved here\n- /BlendedFamilyKitchen/Music — curated royalty-free background tracks\n\n**Synology FileStation API:**\n- Auth: POST /webapi/auth.cgi (SYNO.API.Auth v3)\n- List: POST /webapi/entry.cgi (SYNO.FileStation.List v2)\n- Download: POST /webapi/entry.cgi (SYNO.FileStation.Download v2)\n- Upload: POST /webapi/entry.cgi (SYNO.FileStation.Upload v2)\n\n**TODO:**\n1. Create NAS shared folders listed above\n2. Create a dedicated n8n API user on Synology\n3. Enable FileStation API in DSM\n4. Replace TODO_NAS_USER and TODO_NAS_PASS\n5. Test auth endpoint manually first\n6. Verify file extensions filter (.mp4, .mov, .avi)",
|
||||
"height": 580,
|
||||
"width": 560
|
||||
},
|
||||
"name": "Sticky - NAS Polling",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [200, -280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🤖 Stage 2: AI Processing Pipeline\n\nDownloads raw video, processes through FFmpeg + Whisper + Claude.\n\n**Processing Steps (in order):**\n1. Download raw file from NAS to local /tmp/bfk_raw/\n2. FFmpeg: Extract audio (WAV 16kHz mono for Whisper)\n3. Whisper: Transcribe → generate .srt subtitle file\n4. Claude API: Generate 3 hook options + caption + hashtags\n5. FFmpeg: Normalize video (9:16, color correct, sharpen)\n6. FFmpeg: Burn captions from .srt (white text, black outline)\n7. FFmpeg: Mix low-volume background music from curated library\n8. FFmpeg: Extract best thumbnail frame (scene detection)\n\n**Infrastructure Needed:**\n- Whisper Docker container (e.g. openai/whisper or faster-whisper)\n - Recommend: onerahmet/openai-whisper-asr-webservice\n - Deploy on VM with GPU or on CT 113 (CPU-only, slower)\n - API: POST /asr with audio_file + output=srt\n- FFmpeg installed on CT 113 (apt install ffmpeg)\n- Claude API key in n8n credentials\n- Temp dirs: /tmp/bfk_raw, /tmp/bfk_audio, /tmp/bfk_subs, /tmp/bfk_processed, /tmp/bfk_thumbs\n\n**TODO:**\n1. Deploy Whisper container\n2. Install FFmpeg on CT 113\n3. Create temp directories\n4. Add Claude API credential in n8n\n5. Test Whisper with a sample audio file",
|
||||
"height": 660,
|
||||
"width": 560
|
||||
},
|
||||
"name": "Sticky - AI Processing",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1520, -320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🎵 Stage 3: Sound Suggestions\n\nParallel branch — scrapes trending TikTok sounds and merges with curated kitchen library.\n\n**Cloe's Creative Control:**\n- Background music auto-applied at LOW volume (12%) from curated library\n- Trending sounds presented as SUGGESTIONS only\n- Cloe can swap sounds in TikTok app before posting\n- Sound Change button in Telegram sends her the trending list\n\n**Curated Kitchen Music Library:**\n- Store royalty-free tracks in /BlendedFamilyKitchen/Music/ on NAS\n- Categorize: upbeat, chill, morning, family dinner, quick recipe\n- Sources: Epidemic Sound, Artlist, TikTok Commercial Music Library\n- Name format: kitchen_bg_01.mp3, kitchen_bg_02.mp3, etc.\n\n**Trending Sounds Scraping:**\n- TikTok Creative Center API (free, no auth needed)\n- Endpoint: ads.tiktok.com/creative_radar_api/v1/popular_trend/sound/list\n- Alternative: Apify TikTok Music Trend API\n- Refresh weekly, cache results\n\n**TODO:**\n1. Curate 10-20 royalty-free kitchen tracks\n2. Upload to NAS /BlendedFamilyKitchen/Music/\n3. Test TikTok Creative Center API endpoint\n4. Build fallback if API requires auth",
|
||||
"height": 620,
|
||||
"width": 560
|
||||
},
|
||||
"name": "Sticky - Sound Suggestions",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [2840, 700]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 📱 Stage 4: Cloe Approval via Telegram\n\nSends processed video preview to Cloe with inline action buttons.\n\n**Telegram Message Includes:**\n- Video filename and preview thumbnail\n- 3 AI-generated hook text options\n- AI-generated caption with hashtags\n- Auto-applied background music info\n- Trending sound suggestions (this week)\n- Curated kitchen sound options\n- Action buttons: Approve | Edit | Change Sound | Schedule | Reject\n\n**Button Actions:**\n- ✅ Approve: Posts directly to TikTok via API\n- 📝 Edit: Notifies Cloe file is in /Processed, waits for re-trigger\n- 🎵 Sound: Sends trending sound list, Cloe applies in TikTok app\n- ⏰ Schedule: Asks for preferred time, queues for later posting\n- ❌ Reject: Moves to /Rejected folder, notifies\n\n**Cloe Maintains Full Autonomy:**\n- Nothing posts without her explicit approval\n- She can edit videos before approving\n- She chooses final sound/music\n- She sets posting schedule\n- Pipeline does the grunt work, she makes creative decisions\n\n**TODO:**\n1. Create Telegram bot for BFK notifications\n2. Get Cloe's Telegram chat_id\n3. Set TELEGRAM_BOT_TOKEN in n8n environment\n4. Set up TikTok API app + OAuth for posting\n5. Test inline keyboard callbacks\n6. Add webhook endpoint for Telegram callback responses",
|
||||
"height": 680,
|
||||
"width": 560
|
||||
},
|
||||
"name": "Sticky - Cloe Approval",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [3780, -380]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🚨 Stage 5: Error Handling\n\nCatches any workflow errors and notifies Jordan via Telegram.\n\n**Error Notification Includes:**\n- Workflow name\n- Failed node name\n- Error message\n- MST timestamp\n\n**TODO:**\n1. Set Jordan's Telegram chat_id\n2. Consider adding retry logic for transient failures\n3. Add dead letter queue for persistent failures",
|
||||
"height": 300,
|
||||
"width": 400
|
||||
},
|
||||
"name": "Sticky - Error Handling",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [200, 560]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"amount": 48,
|
||||
"unit": "hours"
|
||||
},
|
||||
"name": "Wait 48hrs",
|
||||
"type": "n8n-nodes-base.wait",
|
||||
"typeVersion": 1.1,
|
||||
"position": [4980, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://open.tiktokapis.com/v2/video/query/",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "Authorization", "value": "=Bearer {{ $env.TIKTOK_ACCESS_TOKEN }}"},
|
||||
{"name": "Content-Type", "value": "application/json"}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"filters\": {\n \"video_ids\": [\"{{ $('Handle Approve').item.json.data?.publish_id || 'unknown' }}\"]\n },\n \"fields\": [\"id\", \"title\", \"view_count\", \"like_count\", \"comment_count\", \"share_count\", \"create_time\"]\n}"
|
||||
},
|
||||
"name": "TikTok Query Video Stats",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [5200, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "an1",
|
||||
"name": "video_id",
|
||||
"value": "={{ $json.data?.videos?.[0]?.id || 'unknown' }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "an2",
|
||||
"name": "title",
|
||||
"value": "={{ $json.data?.videos?.[0]?.title || $('Extract Metadata').item.json.filename }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "an3",
|
||||
"name": "views",
|
||||
"value": "={{ $json.data?.videos?.[0]?.view_count || 0 }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"id": "an4",
|
||||
"name": "likes",
|
||||
"value": "={{ $json.data?.videos?.[0]?.like_count || 0 }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"id": "an5",
|
||||
"name": "comments",
|
||||
"value": "={{ $json.data?.videos?.[0]?.comment_count || 0 }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"id": "an6",
|
||||
"name": "shares",
|
||||
"value": "={{ $json.data?.videos?.[0]?.share_count || 0 }}",
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"id": "an7",
|
||||
"name": "engagement_rate",
|
||||
"value": "={{ $json.data?.videos?.[0]?.view_count > 0 ? (($json.data.videos[0].like_count + $json.data.videos[0].comment_count + $json.data.videos[0].share_count) / $json.data.videos[0].view_count * 100).toFixed(2) : '0' }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "an8",
|
||||
"name": "polled_at",
|
||||
"value": "={{ new Date().toLocaleString('en-US', {timeZone: 'America/Denver'}) }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "an9",
|
||||
"name": "filename",
|
||||
"value": "={{ $('Extract Metadata').item.json.filename }}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Parse Analytics",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [5420, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.Upload"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "upload"},
|
||||
{"name": "path", "value": "/BlendedFamilyKitchen/Analytics"},
|
||||
{"name": "filename", "value": "=analytics_{{ $json.video_id }}_{{ Date.now() }}.json"},
|
||||
{"name": "content", "value": "={{ JSON.stringify({video_id: $json.video_id, title: $json.title, views: $json.views, likes: $json.likes, comments: $json.comments, shares: $json.shares, engagement_rate: $json.engagement_rate, polled_at: $json.polled_at, filename: $json.filename}) }}"},
|
||||
{"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Store Analytics to NAS",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [5640, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"📊 *48hr Performance Check*\\n\\n📁 {{ $json.filename }}\\n\\n👁️ Views: {{ $json.views.toLocaleString() }}\\n❤️ Likes: {{ $json.likes.toLocaleString() }}\\n💬 Comments: {{ $json.comments }}\\n🔄 Shares: {{ $json.shares }}\\n📈 Engagement: {{ $json.engagement_rate }}%\\n\\n_Polled {{ $json.polled_at }} MST_\",\n \"parse_mode\": \"Markdown\"\n}"
|
||||
},
|
||||
"name": "Notify Cloe Analytics",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [5860, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rule": {
|
||||
"interval": [{"field": "cronExpression", "expression": "0 9 * * 0"}]
|
||||
}
|
||||
},
|
||||
"name": "Weekly Summary Trigger",
|
||||
"type": "n8n-nodes-base.scheduleTrigger",
|
||||
"typeVersion": 1.1,
|
||||
"position": [5200, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "/BlendedFamilyKitchen/Analytics"},
|
||||
{"name": "sort_by", "value": "crtime"},
|
||||
{"name": "sort_direction", "value": "desc"},
|
||||
{"name": "limit", "value": "20"},
|
||||
{"name": "_sid", "value": "TODO_REAUTH_NEEDED"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Fetch Weekly Analytics Files",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [5420, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Aggregate analytics from all JSON files fetched this week\nconst files = $input.all();\nlet totalViews = 0, totalLikes = 0, totalComments = 0, totalShares = 0;\nlet videoSummaries = [];\nlet videoCount = 0;\n\nfor (const item of files) {\n try {\n const data = typeof item.json === 'string' ? JSON.parse(item.json) : item.json;\n if (data.views !== undefined) {\n totalViews += Number(data.views) || 0;\n totalLikes += Number(data.likes) || 0;\n totalComments += Number(data.comments) || 0;\n totalShares += Number(data.shares) || 0;\n videoCount++;\n videoSummaries.push(`• ${data.title || data.filename}: ${Number(data.views).toLocaleString()} views, ${data.engagement_rate}% eng`);\n }\n } catch(e) { /* skip unparseable */ }\n}\n\nconst avgEngagement = totalViews > 0 \n ? ((totalLikes + totalComments + totalShares) / totalViews * 100).toFixed(2) \n : '0';\n\nreturn [{\n json: {\n total_views: totalViews,\n total_likes: totalLikes,\n total_comments: totalComments,\n total_shares: totalShares,\n avg_engagement: avgEngagement,\n video_count: videoCount,\n video_breakdown: videoSummaries.join('\\n') || 'No videos tracked this week',\n week_ending: new Date().toLocaleDateString('en-US', {timeZone: 'America/Denver'})\n }\n}];"
|
||||
},
|
||||
"name": "Aggregate Weekly Stats",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [5640, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"TODO_CLOE_CHAT_ID\",\n \"text\": \"📊 *Weekly Performance Summary*\\n_Week ending {{ $json.week_ending }}_\\n\\n🎬 Videos tracked: {{ $json.video_count }}\\n👁️ Total views: {{ $json.total_views.toLocaleString() }}\\n❤️ Total likes: {{ $json.total_likes.toLocaleString() }}\\n💬 Total comments: {{ $json.total_comments }}\\n🔄 Total shares: {{ $json.total_shares }}\\n📈 Avg engagement: {{ $json.avg_engagement }}%\\n\\n*Per-Video Breakdown:*\\n{{ $json.video_breakdown }}\\n\\n_Keep it up! 🔥_\",\n \"parse_mode\": \"Markdown\"\n}"
|
||||
},
|
||||
"name": "Send Weekly Summary to Cloe",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [5860, 400]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 📊 Stage 6: Analytics Polling & Weekly Summary\n\nTracks video performance after posting and sends weekly digests.\n\n**Per-Video Flow (after Approve):**\n1. Wait 48 hours after TikTok post\n2. Query TikTok Display API for video stats\n3. Parse: views, likes, comments, shares, engagement rate\n4. Store analytics JSON to NAS /BlendedFamilyKitchen/Analytics/\n5. Send 48hr performance snapshot to Cloe via Telegram\n\n**Weekly Summary (Sundays 9am MST):**\n1. Fetch all analytics files from NAS\n2. Aggregate totals across all tracked videos\n3. Calculate average engagement rate\n4. Send formatted weekly digest to Cloe\n\n**Metrics Available via TikTok Display API:**\n- ✅ Views, likes, comments, shares (available)\n- ❌ Watch time, completion rate (TikTok Studio only)\n- ❌ Demographics, traffic sources (TikTok Studio only)\n\n**Engagement Rate Formula:**\n(likes + comments + shares) / views × 100\n\n**Folder Structure:**\n- /BlendedFamilyKitchen/Analytics/ — JSON files per video\n- Filename: analytics_{video_id}_{timestamp}.json\n\n**TikTok API Requirements:**\n- App: TikTok Developer Portal → Create App\n- Scopes: video.list, video.info\n- Auth: OAuth 2.0 → access_token stored in n8n env\n- Endpoint: POST https://open.tiktokapis.com/v2/video/query/\n- Rate limit: 100 requests/day\n\n**TODO:**\n1. Register TikTok Developer App\n2. Complete OAuth flow for Cloe's account\n3. Store TIKTOK_ACCESS_TOKEN in n8n env vars\n4. Create /BlendedFamilyKitchen/Analytics/ folder on NAS\n5. Set Cloe's Telegram chat_id\n6. Test Display API with a sample video ID\n7. Add token refresh logic (tokens expire every 24hrs)\n8. Consider adding 7-day follow-up poll for longer-term metrics",
|
||||
"height": 780,
|
||||
"width": 560
|
||||
},
|
||||
"name": "Sticky - Analytics",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [4920, -380]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Schedule Trigger": {"main": [[{"node": "NAS Login", "type": "main", "index": 0}]]},
|
||||
"NAS Login": {"main": [[{"node": "Poll NAS DropZone", "type": "main", "index": 0}]]},
|
||||
"Poll NAS DropZone": {"main": [[{"node": "IF New Files?", "type": "main", "index": 0}]]},
|
||||
"IF New Files?": {"main": [[{"node": "Split Into Batches", "type": "main", "index": 0}]]},
|
||||
"Split Into Batches": {"main": [[{"node": "Extract Metadata", "type": "main", "index": 0}]]},
|
||||
"Extract Metadata": {"main": [[{"node": "Download Raw Video", "type": "main", "index": 0}]]},
|
||||
"Download Raw Video": {"main": [[{"node": "FFmpeg Extract Audio", "type": "main", "index": 0}]]},
|
||||
"FFmpeg Extract Audio": {"main": [[{"node": "Whisper Transcribe", "type": "main", "index": 0}]]},
|
||||
"Whisper Transcribe": {"main": [[{"node": "AI Generate Hooks", "type": "main", "index": 0}]]},
|
||||
"AI Generate Hooks": {"main": [[{"node": "FFmpeg Normalize Video", "type": "main", "index": 0}]]},
|
||||
"FFmpeg Normalize Video": {"main": [[{"node": "FFmpeg Burn Captions", "type": "main", "index": 0}]]},
|
||||
"FFmpeg Burn Captions": {"main": [[{"node": "FFmpeg Mix Background Music", "type": "main", "index": 0}]]},
|
||||
"FFmpeg Mix Background Music": {"main": [[{"node": "FFmpeg Extract Thumbnail", "type": "main", "index": 0}]]},
|
||||
"FFmpeg Extract Thumbnail": {"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]},
|
||||
"Scrape Trending Sounds": {"main": [[{"node": "Format Sound Suggestions", "type": "main", "index": 0}]]},
|
||||
"Format Sound Suggestions": {"main": [[{"node": "Merge Results", "type": "main", "index": 0}]]},
|
||||
"Merge Results": {"main": [[{"node": "Upload to NAS Processed", "type": "main", "index": 0}]]},
|
||||
"Upload to NAS Processed": {"main": [[{"node": "Notify Cloe Telegram", "type": "main", "index": 0}]]},
|
||||
"Notify Cloe Telegram": {"main": [[{"node": "Wait for Cloe Response", "type": "main", "index": 0}]]},
|
||||
"Wait for Cloe Response": {"main": [[{"node": "Switch Cloe Decision", "type": "main", "index": 0}]]},
|
||||
"Switch Cloe Decision": {
|
||||
"main": [
|
||||
[{"node": "Handle Approve", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Edit", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Sound Change", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Reject", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Schedule", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Handle Approve": {"main": [[{"node": "Archive Original on NAS", "type": "main", "index": 0}]]},
|
||||
"Archive Original on NAS": {"main": [[{"node": "Wait 48hrs", "type": "main", "index": 0}]]},
|
||||
"Wait 48hrs": {"main": [[{"node": "TikTok Query Video Stats", "type": "main", "index": 0}]]},
|
||||
"TikTok Query Video Stats": {"main": [[{"node": "Parse Analytics", "type": "main", "index": 0}]]},
|
||||
"Parse Analytics": {"main": [[{"node": "Store Analytics to NAS", "type": "main", "index": 0}]]},
|
||||
"Store Analytics to NAS": {"main": [[{"node": "Notify Cloe Analytics", "type": "main", "index": 0}]]},
|
||||
"Weekly Summary Trigger": {"main": [[{"node": "Fetch Weekly Analytics Files", "type": "main", "index": 0}]]},
|
||||
"Fetch Weekly Analytics Files": {"main": [[{"node": "Aggregate Weekly Stats", "type": "main", "index": 0}]]},
|
||||
"Aggregate Weekly Stats": {"main": [[{"node": "Send Weekly Summary to Cloe", "type": "main", "index": 0}]]},
|
||||
"Error Trigger": {"main": [[{"node": "Notify Jordan Error", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
241
n8n_workflows/garvis_webhook.json
Normal file
241
n8n_workflows/garvis_webhook.json
Normal file
@@ -0,0 +1,241 @@
|
||||
{
|
||||
"name": "Garvis Webhook - Bot Actions",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "garvis",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000001",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [0, 300],
|
||||
"webhookId": "garvis-webhook"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {"caseSensitive": true, "leftValue": "", "typeValidation": "strict"},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "auth1",
|
||||
"leftValue": "={{ $json.headers['x-garvis-secret'] }}",
|
||||
"rightValue": "={{ $env.GARVIS_WEBHOOK_SECRET }}",
|
||||
"operator": {"type": "string", "operation": "equals"}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000002",
|
||||
"name": "IF Auth Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [220, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={\"error\": \"Unauthorized\", \"message\": \"Invalid or missing x-garvis-secret header\"}",
|
||||
"options": {"responseCode": 401}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000003",
|
||||
"name": "Respond 401",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [440, 480]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"rules": [
|
||||
{"value": "run_pipeline", "output": 0},
|
||||
{"value": "check_nas", "output": 1},
|
||||
{"value": "check_services", "output": 2},
|
||||
{"value": "send_message", "output": 3},
|
||||
{"value": "get_status", "output": 4}
|
||||
]
|
||||
},
|
||||
"value": "={{ $json.body.action }}"
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000004",
|
||||
"name": "Switch Action",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3,
|
||||
"position": [440, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.113:5678/api/v1/workflows/{{ $env.CONTENT_PIPELINE_ID }}/activate",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{"name": "X-N8N-API-KEY", "value": "={{ $env.N8N_API_KEY }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000005",
|
||||
"name": "Trigger Content Pipeline",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [700, 0]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.210:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "={{ $json.body.path || '/BlendedFamilyKitchen/DropZone' }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000006",
|
||||
"name": "Check NAS Files",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [700, 140]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "=echo '{\"proxmox\": \"checking...\", \"loki\": \"checking...\", \"nas\": \"checking...\"}' && curl -s -o /dev/null -w '%{http_code}' http://192.168.2.100:8006 && curl -s -o /dev/null -w '%{http_code}' http://192.168.2.114:3100/ready && curl -s -o /dev/null -w '%{http_code}' http://192.168.2.210:5000"
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000007",
|
||||
"name": "Check Services Health",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"{{ $json.body.chat_id || $env.JORDAN_CHAT_ID }}\",\n \"text\": \"{{ $json.body.message }}\",\n \"parse_mode\": \"HTML\"\n}"
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000008",
|
||||
"name": "Send Telegram Message",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [700, 420]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "st1", "name": "status", "value": "operational", "type": "string"},
|
||||
{"id": "st2", "name": "workflows_active", "value": "={{ $env.ACTIVE_WORKFLOW_COUNT || '0' }}", "type": "string"},
|
||||
{"id": "st3", "name": "uptime", "value": "={{ $now.toISO() }}", "type": "string"},
|
||||
{"id": "st4", "name": "version", "value": "1.0.0", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000009",
|
||||
"name": "Get System Status",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [700, 560]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify({success: true, action: $('Switch Action').item.json.body.action, result: $json}) }}",
|
||||
"options": {"responseCode": 200}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000010",
|
||||
"name": "Respond 200 Success",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [960, 280]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={\"error\": \"Unknown action\", \"message\": \"Valid actions: run_pipeline, check_nas, check_services, send_message, get_status\", \"received\": \"{{ $json.body.action }}\"}",
|
||||
"options": {"responseCode": 400}
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000011",
|
||||
"name": "Respond 400 Unknown",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [700, 720]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🔐 Webhook Auth & Entry\n\n**Endpoint:** POST http://192.168.2.113:5678/webhook/garvis\n\n**Request format:**\n```json\n{\n \"action\": \"run_pipeline|check_nas|check_services|send_message|get_status\",\n \"message\": \"(for send_message)\",\n \"chat_id\": \"(optional, defaults to Jordan)\",\n \"path\": \"(optional, for check_nas)\"\n}\n```\n\n**Auth:** x-garvis-secret header must match GARVIS_WEBHOOK_SECRET env var\n\n**Test command:**\n```bash\ncurl -X POST http://192.168.2.113:5678/webhook/garvis \\\n -H 'Content-Type: application/json' \\\n -H 'x-garvis-secret: YOUR_SECRET' \\\n -d '{\"action\": \"get_status\"}'\n```\n\n**TODO:**\n- [ ] Generate GARVIS_WEBHOOK_SECRET and add to n8n env\n- [ ] Add webhook URL to Garvis bot config\n- [ ] Test from Garvis agent with real request",
|
||||
"width": 560,
|
||||
"height": 520
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000012",
|
||||
"name": "Sticky - Webhook Setup",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [-40, -260]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🔀 Action Handlers\n\n**run_pipeline** — Triggers the Content Pipeline workflow via n8n API. Use when Garvis needs to manually kick off video processing.\n\n**check_nas** — Lists files in a NAS directory. Defaults to /BlendedFamilyKitchen/DropZone. Pass custom path in request body.\n\n**check_services** — Pings Proxmox (8006), Loki (3100), and NAS (5000) to verify they're responding. Returns HTTP status codes.\n\n**send_message** — Sends a Telegram message to specified chat_id (or Jordan by default). Garvis uses this for notifications.\n\n**get_status** — Returns n8n system status: operational state, active workflow count, uptime, version.\n\n**TODO:**\n- [ ] Add CONTENT_PIPELINE_ID env var after deploying content pipeline\n- [ ] Add N8N_API_KEY env var for internal API calls\n- [ ] Add TELEGRAM_BOT_TOKEN and JORDAN_CHAT_ID env vars\n- [ ] Consider adding: restart_vm, run_backup, deploy_update actions",
|
||||
"width": 560,
|
||||
"height": 480
|
||||
},
|
||||
"id": "b2c3d4e5-2222-4000-8000-000000000013",
|
||||
"name": "Sticky - Action Handlers",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [600, -260]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [[{"node": "IF Auth Valid?", "type": "main", "index": 0}]]
|
||||
},
|
||||
"IF Auth Valid?": {
|
||||
"main": [
|
||||
[{"node": "Switch Action", "type": "main", "index": 0}],
|
||||
[{"node": "Respond 401", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Switch Action": {
|
||||
"main": [
|
||||
[{"node": "Trigger Content Pipeline", "type": "main", "index": 0}],
|
||||
[{"node": "Check NAS Files", "type": "main", "index": 0}],
|
||||
[{"node": "Check Services Health", "type": "main", "index": 0}],
|
||||
[{"node": "Send Telegram Message", "type": "main", "index": 0}],
|
||||
[{"node": "Get System Status", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Trigger Content Pipeline": {
|
||||
"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Check NAS Files": {
|
||||
"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Check Services Health": {
|
||||
"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Send Telegram Message": {
|
||||
"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Get System Status": {
|
||||
"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [],
|
||||
"triggerCount": 1,
|
||||
"active": false
|
||||
}
|
||||
1
n8n_workflows/garvis_webhook_clean.json
Normal file
1
n8n_workflows/garvis_webhook_clean.json
Normal file
File diff suppressed because one or more lines are too long
247
n8n_workflows/garvis_webhook_v2.json
Normal file
247
n8n_workflows/garvis_webhook_v2.json
Normal file
@@ -0,0 +1,247 @@
|
||||
{
|
||||
"name": "Garvis Webhook - Bot Actions",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "garvis",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [260, 300],
|
||||
"webhookId": "garvis-webhook-001"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "auth1",
|
||||
"leftValue": "={{ $json.headers['x-garvis-secret'] }}",
|
||||
"rightValue": "={{ $env.GARVIS_WEBHOOK_SECRET }}",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
"name": "IF Auth Valid?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [480, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {},
|
||||
"respondWith": "json",
|
||||
"responseBody": "={\"error\": \"Unauthorized\", \"status\": 401}",
|
||||
"responseCode": 401
|
||||
},
|
||||
"name": "Respond 401",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [700, 500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "run_pipeline", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Run Pipeline"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "check_nas", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Check NAS"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "check_services", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Check Services"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "send_message", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Send Message"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "get_status", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Get Status"},
|
||||
{"conditions": {"conditions": [{"leftValue": "={{ $json.body.action }}", "rightValue": "get_analytics", "operator": {"type": "string", "operation": "equals"}}]}, "renameOutput": true, "outputKey": "Get Analytics"}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "Switch Action",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3,
|
||||
"position": [700, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "rp1", "name": "result", "value": "Pipeline triggered manually. Processing DropZone...", "type": "string"},
|
||||
{"id": "rp2", "name": "action", "value": "run_pipeline", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Handle Run Pipeline",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [960, 60]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "/BlendedFamilyKitchen/DropZone"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Handle Check NAS",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [960, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"command": "echo '{\"docker\": \"'$(systemctl is-active docker)'\", \"n8n\": \"running\", \"whisper\": \"'$(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:9000/health 2>/dev/null || echo 'unreachable')'\", \"ffmpeg\": \"'$(ffmpeg -version 2>/dev/null | head -1 || echo 'not installed')'\"}'"
|
||||
},
|
||||
"name": "Handle Check Services",
|
||||
"type": "n8n-nodes-base.executeCommand",
|
||||
"typeVersion": 1,
|
||||
"position": [960, 340]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={\n \"chat_id\": \"{{ $json.body.chat_id || 'TODO_DEFAULT_CHAT_ID' }}\",\n \"text\": \"{{ $json.body.message }}\",\n \"parse_mode\": \"Markdown\"\n}"
|
||||
},
|
||||
"name": "Handle Send Message",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [960, 480]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"mode": "manual",
|
||||
"duplicateItem": false,
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "gs1", "name": "status", "value": "operational", "type": "string"},
|
||||
{"id": "gs2", "name": "workflows", "value": "content_pipeline: inactive, garvis_webhook: active", "type": "string"},
|
||||
{"id": "gs3", "name": "uptime", "value": "={{ new Date().toISOString() }}", "type": "string"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Handle Get Status",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [960, 620]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {},
|
||||
"respondWith": "json",
|
||||
"responseBody": "={\"success\": true, \"action\": \"{{ $json.body?.action || 'unknown' }}\", \"result\": {{ JSON.stringify($json) }}}",
|
||||
"responseCode": 200
|
||||
},
|
||||
"name": "Respond 200 Success",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [1200, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {},
|
||||
"respondWith": "json",
|
||||
"responseBody": "={\"error\": \"Unknown action\", \"received\": \"{{ $json.body?.action }}\", \"available\": [\"run_pipeline\", \"check_nas\", \"check_services\", \"send_message\", \"get_status\", \"get_analytics\"]}",
|
||||
"responseCode": 400
|
||||
},
|
||||
"name": "Respond 400 Unknown",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [960, 800]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🔐 Garvis Webhook — Auth & Setup\n\n**Endpoint:** POST http://192.168.2.113:5678/webhook/garvis\n\n**Authentication:**\n- Header: x-garvis-secret\n- Validate against env var GARVIS_WEBHOOK_SECRET\n- Returns 401 if invalid\n\n**Request Format:**\n```json\n{\n \"action\": \"run_pipeline|check_nas|check_services|send_message|get_status\",\n \"message\": \"optional message text\",\n \"chat_id\": \"optional telegram chat id\"\n}\n```\n\n**Test with curl:**\n```\ncurl -X POST http://192.168.2.113:5678/webhook/garvis \\\n -H 'Content-Type: application/json' \\\n -H 'x-garvis-secret: YOUR_SECRET' \\\n -d '{\"action\": \"get_status\"}'\n```\n\n**TODO:**\n1. Set GARVIS_WEBHOOK_SECRET in n8n environment variables\n2. Set TELEGRAM_BOT_TOKEN in n8n environment variables\n3. Activate workflow when ready\n4. Test each action endpoint\n5. Add webhook URL to Garvis bot config",
|
||||
"height": 620,
|
||||
"width": 520
|
||||
},
|
||||
"name": "Sticky - Webhook Setup",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [200, -320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "http://192.168.2.40:5000/webapi/entry.cgi",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{"name": "api", "value": "SYNO.FileStation.List"},
|
||||
{"name": "version", "value": "2"},
|
||||
{"name": "method", "value": "list"},
|
||||
{"name": "folder_path", "value": "/BlendedFamilyKitchen/Analytics"},
|
||||
{"name": "sort_by", "value": "crtime"},
|
||||
{"name": "sort_direction", "value": "desc"},
|
||||
{"name": "limit", "value": "={{ $json.body.limit || 10 }}"}
|
||||
]
|
||||
}
|
||||
},
|
||||
"name": "Handle Get Analytics",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [960, 760]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "## 🔀 Action Router — Available Actions\n\n**run_pipeline:** Manually triggers the Content Pipeline workflow. Use when Cloe drops a video and doesn't want to wait for the 30-min poll cycle.\n\n**check_nas:** Queries Synology FileStation API to list files in DropZone. Returns file count and names. Useful for: \"Garvis, anything in the drop zone?\"\n\n**check_services:** Runs local service checks — Docker, Whisper, FFmpeg availability. Returns health status JSON. Useful for: \"Garvis, is the pipeline infrastructure healthy?\"\n\n**send_message:** Forwards a message to a Telegram chat via the bot. Requires chat_id and message in request body. Useful for: cross-service notifications.\n\n**get_status:** Returns current n8n workflow states, uptime, and basic system info. Useful for: \"Garvis, n8n status?\"\n\n**TODO:**\n1. Wire run_pipeline to actually trigger Content Pipeline (use n8n Execute Workflow node)\n2. Add NAS auth to check_nas handler\n3. Expand check_services with more endpoints\n4. Add rate limiting / cooldown logic\n5. Add logging to each action handler",
|
||||
"height": 580,
|
||||
"width": 520
|
||||
},
|
||||
"name": "Sticky - Action Router",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [880, -320]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {"main": [[{"node": "IF Auth Valid?", "type": "main", "index": 0}]]},
|
||||
"IF Auth Valid?": {
|
||||
"main": [
|
||||
[{"node": "Switch Action", "type": "main", "index": 0}],
|
||||
[{"node": "Respond 401", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Switch Action": {
|
||||
"main": [
|
||||
[{"node": "Handle Run Pipeline", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Check NAS", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Check Services", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Send Message", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Get Status", "type": "main", "index": 0}],
|
||||
[{"node": "Handle Get Analytics", "type": "main", "index": 0}],
|
||||
[{"node": "Respond 400 Unknown", "type": "main", "index": 0}]
|
||||
]
|
||||
},
|
||||
"Handle Run Pipeline": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]},
|
||||
"Handle Check NAS": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]},
|
||||
"Handle Check Services": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]},
|
||||
"Handle Send Message": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]},
|
||||
"Handle Get Status": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]},
|
||||
"Handle Get Analytics": {"main": [[{"node": "Respond 200 Success", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user