Files
ajarbot/n8n_workflows/content_pipeline_v2.json

755 lines
39 KiB
JSON
Raw Permalink Normal View History

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
2026-04-23 07:54:01 -06:00
{
"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"
}
}