Files
ajarbot/n8n_workflows/content_pipeline_v2.json
Jordan Ramos 916f86725d 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

755 lines
39 KiB
JSON
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"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"
}
}