Initial commit - workspace setup
- AGENTS.md, SOUL.md, USER.md, IDENTITY.md - ANAF monitor (declarații fiscale) - Kanban board + Notes UI - Email tools - Memory system
This commit is contained in:
306
kanban/api.py
Normal file
306
kanban/api.py
Normal file
@@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple API server for Echo Task Board.
|
||||
Handles YouTube summarization requests.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from http.server import HTTPServer, SimpleHTTPRequestHandler
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).parent.parent
|
||||
TOOLS_DIR = BASE_DIR / 'tools'
|
||||
NOTES_DIR = BASE_DIR / 'notes' / 'youtube'
|
||||
KANBAN_DIR = BASE_DIR / 'kanban'
|
||||
|
||||
class TaskBoardHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def do_POST(self):
|
||||
if self.path == '/api/youtube':
|
||||
self.handle_youtube()
|
||||
elif self.path == '/api/files':
|
||||
self.handle_files_post()
|
||||
else:
|
||||
self.send_error(404)
|
||||
|
||||
def handle_files_post(self):
|
||||
"""Save file content."""
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
path = data.get('path', '')
|
||||
content = data.get('content', '')
|
||||
|
||||
workspace = Path('/home/moltbot/clawd')
|
||||
target = (workspace / path).resolve()
|
||||
|
||||
if not str(target).startswith(str(workspace)):
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
|
||||
# Create parent dirs if needed
|
||||
target.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write file
|
||||
target.write_text(content, encoding='utf-8')
|
||||
|
||||
self.send_json({
|
||||
'status': 'saved',
|
||||
'path': path,
|
||||
'size': len(content)
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == '/api/status':
|
||||
self.send_json({'status': 'ok', 'time': datetime.now().isoformat()})
|
||||
elif self.path.startswith('/api/files'):
|
||||
self.handle_files_get()
|
||||
elif self.path.startswith('/api/'):
|
||||
self.send_error(404)
|
||||
else:
|
||||
# Serve static files
|
||||
super().do_GET()
|
||||
|
||||
def handle_files_get(self):
|
||||
"""List files or get file content."""
|
||||
from urllib.parse import urlparse, parse_qs
|
||||
parsed = urlparse(self.path)
|
||||
params = parse_qs(parsed.query)
|
||||
|
||||
path = params.get('path', [''])[0]
|
||||
action = params.get('action', ['list'])[0]
|
||||
|
||||
# Security: only allow access within workspace
|
||||
workspace = Path('/home/moltbot/clawd')
|
||||
try:
|
||||
target = (workspace / path).resolve()
|
||||
if not str(target).startswith(str(workspace)):
|
||||
self.send_json({'error': 'Access denied'}, 403)
|
||||
return
|
||||
except:
|
||||
self.send_json({'error': 'Invalid path'}, 400)
|
||||
return
|
||||
|
||||
if action == 'list':
|
||||
if not target.exists():
|
||||
self.send_json({'error': 'Path not found'}, 404)
|
||||
return
|
||||
|
||||
if target.is_file():
|
||||
# Return file content
|
||||
try:
|
||||
content = target.read_text(encoding='utf-8', errors='replace')
|
||||
self.send_json({
|
||||
'type': 'file',
|
||||
'path': path,
|
||||
'name': target.name,
|
||||
'content': content[:100000], # Limit to 100KB
|
||||
'size': target.stat().st_size,
|
||||
'truncated': target.stat().st_size > 100000
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
else:
|
||||
# List directory
|
||||
items = []
|
||||
try:
|
||||
for item in sorted(target.iterdir()):
|
||||
if item.name.startswith('.'):
|
||||
continue
|
||||
items.append({
|
||||
'name': item.name,
|
||||
'type': 'dir' if item.is_dir() else 'file',
|
||||
'size': item.stat().st_size if item.is_file() else None,
|
||||
'path': str(item.relative_to(workspace))
|
||||
})
|
||||
self.send_json({
|
||||
'type': 'dir',
|
||||
'path': path,
|
||||
'items': items
|
||||
})
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
else:
|
||||
self.send_json({'error': 'Unknown action'}, 400)
|
||||
|
||||
def handle_youtube(self):
|
||||
try:
|
||||
content_length = int(self.headers['Content-Length'])
|
||||
post_data = self.rfile.read(content_length).decode('utf-8')
|
||||
data = json.loads(post_data)
|
||||
|
||||
url = data.get('url', '').strip()
|
||||
|
||||
if not url or 'youtube.com' not in url and 'youtu.be' not in url:
|
||||
self.send_json({'error': 'URL YouTube invalid'}, 400)
|
||||
return
|
||||
|
||||
# Process synchronously (simpler, avoids fork issues)
|
||||
try:
|
||||
print(f"Processing YouTube URL: {url}")
|
||||
result = process_youtube(url)
|
||||
print(f"Processing result: {result}")
|
||||
self.send_json({
|
||||
'status': 'done',
|
||||
'message': 'Notița a fost creată! Refresh pagina Notes.'
|
||||
})
|
||||
except Exception as e:
|
||||
import traceback
|
||||
print(f"YouTube processing error: {e}")
|
||||
traceback.print_exc()
|
||||
self.send_json({
|
||||
'status': 'error',
|
||||
'message': f'Eroare: {str(e)}'
|
||||
}, 500)
|
||||
|
||||
except Exception as e:
|
||||
self.send_json({'error': str(e)}, 500)
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.send_response(code)
|
||||
self.send_header('Content-Type', 'application/json')
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps(data).encode())
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
|
||||
self.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
self.end_headers()
|
||||
|
||||
|
||||
def process_youtube(url):
|
||||
"""Download subtitles, summarize, save note."""
|
||||
import time
|
||||
|
||||
# Get video info and subtitles
|
||||
yt_dlp = os.path.expanduser('~/.local/bin/yt-dlp')
|
||||
|
||||
# Get title
|
||||
result = subprocess.run(
|
||||
[yt_dlp, '--dump-json', '--no-download', url],
|
||||
capture_output=True, text=True, timeout=30
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
print(f"Failed to get video info: {result.stderr}")
|
||||
return
|
||||
|
||||
info = json.loads(result.stdout)
|
||||
title = info.get('title', 'Unknown')
|
||||
duration = info.get('duration', 0)
|
||||
video_id = info.get('id', 'unknown')
|
||||
|
||||
# Download subtitles
|
||||
temp_dir = Path('/tmp/yt_subs')
|
||||
temp_dir.mkdir(exist_ok=True)
|
||||
|
||||
for f in temp_dir.glob('*'):
|
||||
f.unlink()
|
||||
|
||||
subprocess.run([
|
||||
yt_dlp, '--write-auto-subs', '--sub-langs', 'en',
|
||||
'--skip-download', '--sub-format', 'vtt',
|
||||
'-o', str(temp_dir / '%(id)s'),
|
||||
url
|
||||
], capture_output=True, timeout=120)
|
||||
|
||||
# Find and read subtitle file
|
||||
transcript = None
|
||||
for sub_file in temp_dir.glob('*.vtt'):
|
||||
content = sub_file.read_text(encoding='utf-8', errors='replace')
|
||||
transcript = clean_vtt(content)
|
||||
break
|
||||
|
||||
if not transcript:
|
||||
print("No subtitles found")
|
||||
return
|
||||
|
||||
# Create note filename
|
||||
date_str = datetime.now().strftime('%Y-%m-%d')
|
||||
slug = re.sub(r'[^\w\s-]', '', title.lower())[:50].strip().replace(' ', '-')
|
||||
filename = f"{date_str}_{slug}.md"
|
||||
|
||||
# Create simple note (without AI summary for now - just transcript)
|
||||
note_content = f"""# {title}
|
||||
|
||||
**Video:** {url}
|
||||
**Duration:** {duration // 60}:{duration % 60:02d}
|
||||
**Saved:** {date_str}
|
||||
**Tags:** #youtube #to-summarize
|
||||
|
||||
---
|
||||
|
||||
## Transcript
|
||||
|
||||
{transcript[:15000]}
|
||||
|
||||
---
|
||||
|
||||
*Notă: Sumarizarea va fi adăugată de Echo.*
|
||||
"""
|
||||
|
||||
# Save note
|
||||
NOTES_DIR.mkdir(parents=True, exist_ok=True)
|
||||
note_path = NOTES_DIR / filename
|
||||
note_path.write_text(note_content, encoding='utf-8')
|
||||
|
||||
# Update index
|
||||
subprocess.run([
|
||||
sys.executable, str(TOOLS_DIR / 'update_notes_index.py')
|
||||
], capture_output=True)
|
||||
|
||||
# Add task to kanban
|
||||
subprocess.run([
|
||||
sys.executable, str(KANBAN_DIR / 'update_task.py'),
|
||||
'add', 'in-progress', f'Sumarizare: {title[:30]}...', url, 'medium'
|
||||
], capture_output=True)
|
||||
|
||||
print(f"Created note: {filename}")
|
||||
return filename
|
||||
|
||||
|
||||
def clean_vtt(content):
|
||||
"""Convert VTT to plain text."""
|
||||
lines = []
|
||||
seen = set()
|
||||
|
||||
for line in content.split('\n'):
|
||||
if any([
|
||||
line.startswith('WEBVTT'),
|
||||
line.startswith('Kind:'),
|
||||
line.startswith('Language:'),
|
||||
'-->' in line,
|
||||
line.strip().startswith('<'),
|
||||
not line.strip(),
|
||||
re.match(r'^\d+$', line.strip())
|
||||
]):
|
||||
continue
|
||||
|
||||
clean = re.sub(r'<[^>]+>', '', line).strip()
|
||||
if clean and clean not in seen:
|
||||
seen.add(clean)
|
||||
lines.append(clean)
|
||||
|
||||
return ' '.join(lines)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = 8080
|
||||
os.chdir(KANBAN_DIR)
|
||||
|
||||
print(f"Starting Echo Task Board API on port {port}")
|
||||
httpd = HTTPServer(('0.0.0.0', port), TaskBoardHandler)
|
||||
httpd.serve_forever()
|
||||
Reference in New Issue
Block a user