Files
ajarbot/bfk_workflow_update.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

1 line
28 KiB
JSON

{"name": "Content Pipeline - BlendedFamilyKitchen", "nodes": [{"parameters": {"rule": {"interval": [{"field": "minutes", "minutesInterval": 30}]}}, "id": "eb4a0cfa-bebc-45b5-b783-e33709b17926", "name": "Schedule Trigger", "type": "n8n-nodes-base.scheduleTrigger", "position": [-16, 208], "typeVersion": 1.1}, {"parameters": {"url": "http://192.168.2.200:5000/webapi/auth.cgi", "sendQuery": true, "queryParameters": {"parameters": [{"name": "api", "value": "SYNO.API.Auth"}, {"name": "version", "value": "6"}, {"name": "method", "value": "login"}, {"name": "account", "value": "garvis-api"}, {"name": "passwd", "value": "@ms88Str"}, {"name": "session", "value": "FileStation"}, {"name": "format", "value": "sid"}]}, "options": {}}, "id": "0f2f5a90-a8fd-404a-b243-a824c240f565", "name": "NAS Login", "type": "n8n-nodes-base.httpRequest", "position": [192, 208], "typeVersion": 4.1}, {"parameters": {"url": "http://192.168.2.200:5000/webapi/entry.cgi", "sendQuery": true, "queryParameters": {"parameters": [{"name": "api", "value": "SYNO.FileStation.List"}, {"name": "version", "value": "2"}, {"name": "method", "value": "list"}, {"name": "folder_path", "value": "/BlendedFamilyKitchen/DropZone"}, {"name": "additional", "value": "size,time"}, {"name": "_sid", "value": "={{ $('NAS Login').item.json.data.sid }}"}]}, "options": {}}, "id": "02569323-a788-46aa-8f2a-b2e3da2904a3", "name": "Poll NAS DropZone", "type": "n8n-nodes-base.httpRequest", "position": [416, 208], "typeVersion": 4.1}, {"parameters": {"conditions": {"options": {"leftValue": "", "caseSensitive": true, "typeValidation": "loose"}, "combinator": "and", "conditions": [{"id": "cond1", "operator": {"type": "number", "operation": "gt"}, "leftValue": "={{ $json.data?.files?.length }}", "rightValue": "0"}]}, "options": {}}, "id": "762a445d-c774-4e68-9abd-b05b6247c68a", "name": "IF New Files?", "type": "n8n-nodes-base.if", "position": [640, 208], "typeVersion": 2}, {"parameters": {"jsCode": "// Split data.files array into individual items\nconst files = $input.first().json.data?.files || $input.first().json.files || [];\nreturn files.map(file => ({\n json: typeof file === 'string' ? { name: file } : file\n}));"}, "id": "cdf361f7-7b3f-46e3-97cc-a27cdb416e4e", "name": "Split Into Batches", "type": "n8n-nodes-base.code", "position": [944, 192], "typeVersion": 2}, {"parameters": {"assignments": {"assignments": [{"id": "a1", "name": "filename", "type": "string", "value": "={{ $json.name }}"}, {"id": "a2", "name": "filepath", "type": "string", "value": "={{ $json.path }}"}, {"id": "a3", "name": "size", "type": "number", "value": "={{ $json.additional?.size }}"}, {"id": "a4", "name": "created", "type": "string", "value": "={{ $json.additional?.time?.crtime }}"}, {"id": "a5", "name": "extension", "type": "string", "value": "={{ $json.name.split('.').pop().toLowerCase() }}"}]}, "options": {}}, "id": "493da64e-3b20-4ab0-bfd2-dec138fc2148", "name": "Extract Metadata", "type": "n8n-nodes-base.set", "position": [1168, 208], "typeVersion": 3.4}, {"parameters": {"url": "=http://192.168.2.200:5000/webapi/entry.cgi", "sendQuery": true, "queryParameters": {"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 }}"}, {"name": "mode", "value": "download"}]}, "options": {"response": {"response": {"responseFormat": "file"}}}}, "id": "8e79824b-2f3a-4a16-924c-d04350db3f3b", "name": "Download Raw Video", "type": "n8n-nodes-base.httpRequest", "position": [1376, 192], "typeVersion": 4.1}, {"parameters": {"command": "=mkdir -p /opt/n8n/bfk_raw /opt/n8n/bfk_audio && ffmpeg -y -i /opt/n8n/bfk_raw/{{ $json.filename }} -vn -acodec pcm_s16le -ar 16000 -ac 1 /opt/n8n/bfk_audio/{{ $json.filename.replace(/\\.[^.]+$/, '.wav') }} && echo '{\"audio_path\": \"/opt/n8n/bfk_audio/{{ $json.filename.replace(/\\.[^.]+$/, '.wav') }}\", \"video_path\": \"/opt/n8n/bfk_raw/{{ $json.filename }}\"}'"}, "id": "57a337cf-f7bc-4184-92fc-ba30e766399b", "name": "FFmpeg Extract Audio", "type": "n8n-nodes-base.executeCommand", "position": [1824, 192], "typeVersion": 1}, {"parameters": {"method": "POST", "url": "http://192.168.2.102:8787/v1/audio/transcriptions", "sendBody": true, "contentType": "multipart-form-data", "bodyParameters": {"parameters": [{"parameterType": "formBinaryData", "name": "file", "inputDataFieldName": "data"}, {"name": "model", "value": "Systran/faster-whisper-base"}, {"name": "language", "value": "en"}, {"name": "response_format", "value": "srt"}]}, "options": {}}, "id": "aa607dad-22f6-4e2a-b430-c2e0a146755d", "name": "Whisper Transcribe", "type": "n8n-nodes-base.httpRequest", "position": [2240, 416], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key=AIzaSyDQPi9kXbVpxW790RBuUCeC8t_UgyxX7m4", "sendHeaders": true, "headerParameters": {"parameters": [{"name": "Content-Type", "value": "application/json"}]}, "sendBody": true, "specifyBody": "json", "jsonBody": "={{ JSON.stringify({ contents: [{ parts: [{ text: \"You are a TikTok content expert for a blended family cooking channel called BlendedFamilyKitchen. Given this transcript from a cooking video, generate:\\n1) Three hook text options (max 8 words each, attention-grabbing, food-focused)\\n2) A caption/description with relevant hashtags (mix of popular and niche)\\n3) Three title options\\n\\nFormat your response as JSON with keys: hooks (array of 3 strings), caption (string), titles (array of 3 strings).\\n\\nTranscript: \" + String($json.srt_content || $json.text || $('Whisper Transcribe').item.json.data || $('Whisper Transcribe').item.json.text || 'No transcript available') }] }], generationConfig: { temperature: 0.8, maxOutputTokens: 500, responseMimeType: \"application/json\" } }) }}", "options": {}}, "id": "d7f55aaa-9361-4aff-86cc-641c5d7c57dc", "name": "AI Generate Hooks", "type": "n8n-nodes-base.httpRequest", "position": [2656, -64], "typeVersion": 4.1}, {"parameters": {"command": "=ffmpeg -y -i /opt/n8n/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 /opt/n8n/bfk_processed/normalized_{{ $('Extract Metadata').item.json.filename }} && echo 'normalized'"}, "id": "3a685677-482b-4f12-a912-13cb5cdcc45e", "name": "FFmpeg Normalize Video", "type": "n8n-nodes-base.executeCommand", "position": [2768, -336], "typeVersion": 1}, {"parameters": {"command": "=ffmpeg -y -i /opt/n8n/bfk_processed/normalized_{{ $('Extract Metadata').item.json.filename }} -vf \"subtitles=/opt/n8n/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 /opt/n8n/bfk_processed/captioned_{{ $('Extract Metadata').item.json.filename }} && echo 'captioned'"}, "id": "7d5d82f3-dbd5-4303-a005-b87c557eb077", "name": "FFmpeg Burn Captions", "type": "n8n-nodes-base.executeCommand", "position": [2992, -336], "typeVersion": 1}, {"parameters": {"command": "=ffmpeg -y -i /opt/n8n/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 /opt/n8n/bfk_processed/final_{{ $('Extract Metadata').item.json.filename }} && echo 'music_mixed'"}, "id": "c76b07ed-aa36-4bcb-a0b3-527e24c7eb25", "name": "FFmpeg Mix Background Music", "type": "n8n-nodes-base.executeCommand", "position": [3216, -336], "typeVersion": 1}, {"parameters": {"command": "=ffmpeg -y -i /opt/n8n/bfk_processed/final_{{ $('Extract Metadata').item.json.filename }} -vf \"select=gt(scene\\,0.3)\" -frames:v 1 -q:v 2 /opt/n8n/bfk_thumbs/thumb_{{ $('Extract Metadata').item.json.filename.replace(/\\.[^.]+$/, '.jpg') }} && echo 'thumbnail_extracted'"}, "id": "0cd53a7d-c1e8-4a56-b899-44315c8ca04d", "name": "FFmpeg Extract Thumbnail", "type": "n8n-nodes-base.executeCommand", "position": [3424, -336], "typeVersion": 1}, {"parameters": {"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"}]}, "options": {}}, "id": "9b8e701d-83b1-41b5-bb6c-b03feec16feb", "name": "Scrape Trending Sounds", "type": "n8n-nodes-base.httpRequest", "position": [2912, 528], "typeVersion": 4.1}, {"parameters": {"assignments": {"assignments": [{"id": "s1", "name": "trending_sounds", "type": "string", "value": "={{ $json.data?.sound_list?.slice(0,3).map(s => s.title + ' by ' + s.author).join('\\n') || 'No trending data available' }}"}, {"id": "s2", "name": "curated_kitchen_sounds", "type": "string", "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"}, {"id": "s3", "name": "sound_recommendation", "type": "string", "value": "Curated kitchen tracks auto-applied at low volume. Trending sounds listed below for optional manual swap in TikTok app before posting."}]}, "options": {}}, "id": "9045886e-6aa1-44fc-851d-8d0b7a7e8b7d", "name": "Format Sound Suggestions", "type": "n8n-nodes-base.set", "position": [3120, 528], "typeVersion": 3.4}, {"parameters": {"mode": "combine", "options": {}}, "id": "03c9a02e-917a-4da1-84e3-545cd81ac79c", "name": "Merge Results", "type": "n8n-nodes-base.merge", "position": [3632, 304], "typeVersion": 3}, {"parameters": {"method": "POST", "url": "http://192.168.2.200: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 }}"}]}, "options": {}}, "id": "cc5ed7db-528e-4bac-8e9a-5b86981983f5", "name": "Upload to NAS Processed", "type": "n8n-nodes-base.httpRequest", "position": [3856, 304], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\ud83c\udfac *New Video Ready!*\\n\\n\ud83d\udcc1 {{ $('Extract Metadata').item.json.filename }}\\n\\n*Hook Options:*\\n{{ $('AI Generate Hooks').item.json.content?.[0]?.text || 'Processing...' }}\\n\\n*Sound Options:*\\n\ud83c\udfb5 _Auto-applied:_ Kitchen background track\\n\\n\ud83d\udcc8 _Trending sounds this week:_\\n{{ $json.trending_sounds || 'No trending data' }}\\n\\n\ud83c\udfb6 _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\": \"\u2705 Approve & Post\", \"callback_data\": \"approve_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"\ud83d\udcdd Edit First\", \"callback_data\": \"edit_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"\ud83c\udfb5 Change Sound\", \"callback_data\": \"sound_{{ $('Extract Metadata').item.json.filename }}\"}, {\"text\": \"\u23f0 Schedule\", \"callback_data\": \"schedule_{{ $('Extract Metadata').item.json.filename }}\"}],\n [{\"text\": \"\u274c Reject\", \"callback_data\": \"reject_{{ $('Extract Metadata').item.json.filename }}\"}]\n ]\n }\n}", "options": {}}, "id": "13b546d8-6383-491a-8bdb-87e47d3b7cc8", "name": "Notify Cloe Telegram", "type": "n8n-nodes-base.httpRequest", "position": [4064, 304], "typeVersion": 4.1}, {"parameters": {"url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/getUpdates", "sendQuery": true, "queryParameters": {"parameters": [{"name": "offset", "value": "-1"}, {"name": "timeout", "value": "30"}]}, "options": {}}, "id": "48abfe94-9a2f-45c9-9677-cc69b5b1c64c", "name": "Wait for Cloe Response", "type": "n8n-nodes-base.httpRequest", "position": [4288, 304], "typeVersion": 4.1}, {"parameters": {"rules": {"values": [{"conditions": {"conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "approve"}]}, "renameOutput": true, "outputKey": "Approve"}, {"conditions": {"conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "edit"}]}, "renameOutput": true, "outputKey": "Edit"}, {"conditions": {"conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "sound"}]}, "renameOutput": true, "outputKey": "Sound"}, {"conditions": {"conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "reject"}]}, "renameOutput": true, "outputKey": "Reject"}, {"conditions": {"conditions": [{"operator": {"type": "string", "operation": "equals"}, "leftValue": "={{ $json.result?.[0]?.callback_query?.data?.split('_')[0] }}", "rightValue": "schedule"}]}, "renameOutput": true, "outputKey": "Schedule"}]}, "options": {}}, "id": "3065737b-5bc1-42d1-b1d1-ee2c9a1f5681", "name": "Switch Cloe Decision", "type": "n8n-nodes-base.switch", "position": [4512, 304], "typeVersion": 3}, {"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}", "options": {}}, "id": "24dd1511-a6ad-421d-9e6f-15c14904ca35", "name": "Handle Approve", "type": "n8n-nodes-base.httpRequest", "position": [4768, 64], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\ud83d\udcdd Opening video for edit. File is in NAS /Processed folder. Reply here when done editing and I'll re-process.\"\n}", "options": {}}, "id": "f59a0455-cbf2-4316-a637-99b5c9da4362", "name": "Handle Edit", "type": "n8n-nodes-base.httpRequest", "position": [4768, 224], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\ud83c\udfb5 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}", "options": {}}, "id": "b69cbfb8-160b-4c6f-a812-19c21a3f2f2d", "name": "Handle Sound Change", "type": "n8n-nodes-base.httpRequest", "position": [4768, 384], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\u274c Video rejected. Moving to /Rejected folder on NAS. File: {{ $('Extract Metadata').item.json.filename }}\"\n}", "options": {}}, "id": "afe8eb7a-dd0f-4da7-8475-0ad655e0fa81", "name": "Handle Reject", "type": "n8n-nodes-base.httpRequest", "position": [4768, 544], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\u23f0 Schedule mode. Reply with your preferred post time (e.g. 'tomorrow 6pm', 'friday 12pm') and I'll queue it up.\"\n}", "options": {}}, "id": "0eb42673-7d47-4b9d-91b2-207aeaba1930", "name": "Handle Schedule", "type": "n8n-nodes-base.httpRequest", "position": [4768, 704], "typeVersion": 4.1}, {"parameters": {"method": "POST", "url": "http://192.168.2.200: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 }}"}]}, "options": {}}, "id": "566438cb-6b68-4618-8591-f561c90a402e", "name": "Archive Original on NAS", "type": "n8n-nodes-base.httpRequest", "position": [4992, 64], "typeVersion": 4.1}, {"parameters": {}, "id": "438d4e2b-a5d0-4f6b-8eb2-3a37dd1e487e", "name": "Error Trigger", "type": "n8n-nodes-base.errorTrigger", "position": [272, 704], "typeVersion": 1}, {"parameters": {"method": "POST", "url": "=https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage", "sendBody": true, "specifyBody": "json", "jsonBody": "={\n \"chat_id\": \"8088983654\",\n \"text\": \"\ud83d\udea8 *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}", "options": {}}, "id": "6c1aeca4-18bb-400e-a54a-8126454a4102", "name": "Notify Jordan Error", "type": "n8n-nodes-base.httpRequest", "position": [480, 704], "typeVersion": 4.1}, {"parameters": {"content": "## \ud83d\udcc2 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 \u2014 Cloe drops raw videos here\n- /BlendedFamilyKitchen/Processed \u2014 enhanced videos land here\n- /BlendedFamilyKitchen/Archive \u2014 originals moved here after processing\n- /BlendedFamilyKitchen/Rejected \u2014 rejected videos moved here\n- /BlendedFamilyKitchen/Music \u2014 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 garvis-api and @ms88Str\n5. Test auth endpoint manually first\n6. Verify file extensions filter (.mp4, .mov, .avi)", "height": 580, "width": 560}, "id": "9debd5f5-f348-4e17-a3b9-6bbd5196b0fa", "name": "Sticky - NAS Polling", "type": "n8n-nodes-base.stickyNote", "position": [-64, -640], "typeVersion": 1}, {"parameters": {"content": "## \ud83e\udd16 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 /opt/n8n/bfk_raw/\n2. FFmpeg: Extract audio (WAV 16kHz mono for Whisper)\n3. Whisper: Transcribe \u2192 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: /opt/n8n/bfk_raw, /opt/n8n/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": 692, "width": 704}, "id": "eec19a28-a850-4e14-b186-bf94d163bbd4", "name": "Sticky - AI Processing", "type": "n8n-nodes-base.stickyNote", "position": [1104, -784], "typeVersion": 1}, {"parameters": {"content": "## \ud83c\udfb5 Stage 3: Sound Suggestions\n\nParallel branch \u2014 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}, "id": "c5582fca-41ca-435e-af34-dc7385543527", "name": "Sticky - Sound Suggestions", "type": "n8n-nodes-base.stickyNote", "position": [2848, 704], "typeVersion": 1}, {"parameters": {"content": "## \ud83d\udcf1 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- \u2705 Approve: Posts directly to TikTok via API\n- \ud83d\udcdd Edit: Notifies Cloe file is in /Processed, waits for re-trigger\n- \ud83c\udfb5 Sound: Sends trending sound list, Cloe applies in TikTok app\n- \u23f0 Schedule: Asks for preferred time, queues for later posting\n- \u274c 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": 744, "width": 560}, "id": "d1a9f460-f1e7-47de-9c21-9764d6665cac", "name": "Sticky - Cloe Approval", "type": "n8n-nodes-base.stickyNote", "position": [3744, -560], "typeVersion": 1}, {"parameters": {"content": "## \ud83d\udea8 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": 380, "width": 400}, "id": "0d4ed488-aee6-4896-ac23-7b17eff44945", "name": "Sticky - Error Handling", "type": "n8n-nodes-base.stickyNote", "position": [240, 864], "typeVersion": 1}, {"parameters": {"fileName": "=/opt/n8n/bfk_raw/{{ $('Extract Metadata').item.json.filename }}", "options": {}}, "id": "7df7f187-a469-4317-a8c8-e63023ff7ed0", "name": "Save Video to Disk", "type": "n8n-nodes-base.writeBinaryFile", "position": [1600, 208], "typeVersion": 1}, {"parameters": {"httpMethod": "POST", "path": "bfk-trigger", "options": {}}, "id": "webhook-trigger-001", "name": "Webhook Trigger", "type": "n8n-nodes-base.webhook", "position": [-208, 592], "webhookId": "bfk-trigger-001", "typeVersion": 2}, {"parameters": {"filePath": "=/opt/n8n/bfk_audio/{{ $('Extract Metadata').item.json.filename.replace(/\\.[^.]+$/, '.wav') }}"}, "id": "0a3985d5-7371-4096-a071-55bc4563ec1d", "name": "Load Audio File", "type": "n8n-nodes-base.readBinaryFile", "position": [2032, 208], "typeVersion": 1}, {"parameters": {"command": "=echo '{{ Buffer.from($json.data || $json.text || \"\").toString(\"base64\") }}' | base64 -d > /opt/n8n/bfk_subs/{{ $(\"Extract Metadata\").item.json.filename.replace(/\\.[^.]+$/, \".srt\") }} && echo \"saved\""}, "id": "save-srt-to-disk-001", "name": "Save SRT to Disk", "type": "n8n-nodes-base.executeCommand", "position": [2480, 192], "typeVersion": 1}], "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": "Save Video to Disk", "type": "main", "index": 0}]]}, "FFmpeg Extract Audio": {"main": [[{"node": "Load Audio File", "type": "main", "index": 0}]]}, "Whisper Transcribe": {"main": [[{"node": "Save SRT to Disk", "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}]]}, "Error Trigger": {"main": [[{"node": "Notify Jordan Error", "type": "main", "index": 0}]]}, "Save Video to Disk": {"main": [[{"node": "FFmpeg Extract Audio", "type": "main", "index": 0}]]}, "Webhook Trigger": {"main": [[{"node": "NAS Login", "type": "main", "index": 0}]]}, "Load Audio File": {"main": [[{"node": "Whisper Transcribe", "type": "main", "index": 0}]]}, "Save SRT to Disk": {"main": [[{"node": "AI Generate Hooks", "type": "main", "index": 0}]]}}, "settings": {"executionOrder": "v1"}}