#!/bin/bash #============================================================================== # Momentry Core — Fresh Install Script # Usage: bash install_momentry.sh # # Phases: # 1. 環境 (Environment) — system prereqs, service dependencies, config # 2. Core — build/install the core binary (API server) # 3. Worker — build/install the worker binary (pipeline processor) # 4. Agents/Processors — processor scripts, Python deps, verification #============================================================================== set -euo pipefail PROJECT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" COLOR=true # ─── Color helpers ─── if [ "$COLOR" = true ]; then R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'; B='\033[0;34m'; C='\033[0;36m'; N='\033[0m' else R=''; G=''; Y=''; B=''; C=''; N='' fi ok() { echo -e " ${G}✓${N} $1"; } fail() { echo -e " ${R}✗${N} $1"; FAILURES+=("$1"); } info() { echo -e " ${B}→${N} $1"; } warn() { echo -e " ${Y}⚠${N} $1"; } header(){ echo -e "\n${C}─── $1 ───${N}"; } sub() { echo -e "\n ${B}$1${N}"; } FAILURES=() cd "$PROJECT_DIR" echo -e "${C}========================================${N}" echo -e "${C} Momentry Core — Fresh Install${N}" echo -e "${C} Project: $PROJECT_DIR${N}" echo -e "${C} Date: $(date '+%Y-%m-%d %H:%M:%S')${N}" echo -e "${C}========================================${N}" # ═══════════════════════════════════════════════════════════════ # Phase 1: 環境 (Environment) # ═══════════════════════════════════════════════════════════════ header "Phase 1/4 — 環境 (Environment)" # ── 1a. System prerequisites ── sub "System prerequisites" if xcode-select -p &>/dev/null; then ok "Xcode CLI tools" else info "Installing Xcode Command Line Tools..." xcode-select --install || true echo " Press any key after installation completes, then re-run." read -rn1 xcode-select -p &>/dev/null && ok "Xcode CLI tools" || fail "Xcode CLI tools" fi if command -v brew &>/dev/null; then ok "Homebrew $(brew --version | head -1)" else info "Installing Homebrew..." /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" eval "$(/opt/homebrew/bin/brew shellenv)" command -v brew &>/dev/null && ok "Homebrew" || fail "Homebrew" fi for tool in git curl jq wget tree cmake pkg-config; do command -v "$tool" &>/dev/null || brew install "$tool" &>/dev/null || true done ok "Basic tools (git curl jq wget tree cmake pkg-config)" if command -v rustc &>/dev/null; then ok "Rust $(rustc --version)" else info "Installing Rust via rustup..." curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y source "$HOME/.cargo/env" rustc --version &>/dev/null && ok "Rust" || fail "Rust" fi PYTHON_BIN="${MOMENTRY_PYTHON_PATH:-/opt/homebrew/bin/python3.11}" if [ ! -f "$PYTHON_BIN" ]; then info "Installing Python 3.11 via Homebrew..." brew install python@3.11 PYTHON_BIN="/opt/homebrew/bin/python3.11" fi ok "Python 3.11 ($PYTHON_BIN)" # PG build deps for dep in readline zlib icu4c openssl e2fsprogs; do brew list "$dep" &>/dev/null 2>&1 || brew install "$dep" &>/dev/null || true done ok "Build deps (readline zlib icu4c openssl e2fsprogs)" # ── 1b. Service dependencies ── sub "Service dependencies" # PostgreSQL PG_BIN="${PG_BIN:-$HOME/pgsql/18.3/bin}" PG_DATA="${PG_DATA:-$HOME/pgsql/data}" if [ ! -f "$PG_BIN/postgres" ]; then info "Building PostgreSQL 18.3 from source..." bash "$PROJECT_DIR/scripts/setup/01_postgresql.sh" fi "$PG_BIN/pg_isready" -q 2>/dev/null || "$PG_BIN/pg_ctl" -D "$PG_DATA" -l "$HOME/pgsql/pg.log" start 2>/dev/null || true sleep 2 "$PG_BIN/pg_isready" -q 2>/dev/null && ok "PostgreSQL" || fail "PostgreSQL" DB_NAME="${DB_NAME:-momentry}" "$PG_BIN/psql" -U accusys -d postgres -tc "SELECT 1 FROM pg_database WHERE datname='$DB_NAME'" | grep -q 1 || \ "$PG_BIN/createdb" -U accusys "$DB_NAME" 2>/dev/null || true "$PG_BIN/psql" -U accusys -d "$DB_NAME" -c "CREATE EXTENSION IF NOT EXISTS vector" &>/dev/null || true "$PG_BIN/psql" -U accusys -d "$DB_NAME" -c "SELECT 1" &>/dev/null && ok "Database '$DB_NAME'" || fail "Database '$DB_NAME'" # Redis redis-cli ping 2>/dev/null | grep -q PONG || { brew install redis &>/dev/null && brew services start redis &>/dev/null || true; } redis-cli ping 2>/dev/null | grep -q PONG && ok "Redis" || fail "Redis" # MongoDB if ! command -v mongosh &>/dev/null || ! mongosh --quiet --eval "db.adminCommand('ping')" &>/dev/null; then brew tap mongodb/brew &>/dev/null brew install mongodb-community &>/dev/null && brew services start mongodb-community &>/dev/null || true sleep 3 fi mongosh --quiet --eval "db.adminCommand('ping')" &>/dev/null && ok "MongoDB" || fail "MongoDB" # Qdrant QDRANT_BIN="$PROJECT_DIR/services/qdrant/target/release/qdrant" if [ ! -f "$QDRANT_BIN" ] && [ -d "$PROJECT_DIR/services/qdrant" ]; then info "Building Qdrant..." cd "$PROJECT_DIR/services/qdrant" && cargo build --release --bin qdrant 2>&1 | tail -3 && cd "$PROJECT_DIR" fi if curl -sf http://localhost:6333/healthz &>/dev/null; then ok "Qdrant" elif [ -f "$QDRANT_BIN" ]; then nohup "$QDRANT_BIN" > "$HOME/qdrant.log" 2>&1 & for i in $(seq 1 15); do sleep 2; curl -sf http://localhost:6333/healthz &>/dev/null && break; done curl -sf http://localhost:6333/healthz &>/dev/null && ok "Qdrant" || fail "Qdrant" else warn "Qdrant source not found — skip (will need manual setup)" fi # ── 1c. External tools ── sub "External tools" for tool in ffmpeg ffprobe rsync yt-dlp; do command -v "$tool" &>/dev/null || brew install "$tool" &>/dev/null || true done ok "ffmpeg ffprobe rsync yt-dlp" command -v soffice &>/dev/null || brew install --cask libreoffice &>/dev/null || true command -v soffice &>/dev/null && ok "LibreOffice" || warn "LibreOffice" # ── 1d. Configuration ── sub "Configuration" [ -f "$PROJECT_DIR/.env" ] && ok ".env exists" || { cp "$PROJECT_DIR/.env.example" "$PROJECT_DIR/.env" ok ".env created from template" info " Edit $PROJECT_DIR/.env to customize settings" } WATCH_DIR="${MOMENTRY_SFTP_ROOT:-$PROJECT_DIR/storage/watch}" for d in output output_dev thumbnails storage data; do mkdir -p "$PROJECT_DIR/$d"; done mkdir -p "$WATCH_DIR" ok "Directories (output output_dev thumbnails storage data watch)" "$PG_BIN/psql" -U accusys -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS dev" &>/dev/null "$PG_BIN/psql" -U accusys -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS public" &>/dev/null ok "Database schemas (dev, public)" # Git repo (build.rs needs git hash) if [ ! -d "$PROJECT_DIR/.git" ]; then git init && git add -A && git commit -m "init" 2>/dev/null || true fi ok "Git repository (for build hash)" # ── 1e. Startup check ── sub "Startup check" cd "$PROJECT_DIR" "$PG_BIN/pg_isready" -q 2>/dev/null && ok "PostgreSQL" || fail "PostgreSQL" redis-cli ping 2>/dev/null | grep -q PONG && ok "Redis" || fail "Redis" mongosh --quiet --eval "db.adminCommand('ping')" &>/dev/null && ok "MongoDB" || fail "MongoDB" curl -sf http://localhost:6333/healthz &>/dev/null && ok "Qdrant" || warn "Qdrant" ok "All core services verified" # ═══════════════════════════════════════════════════════════════ # Phase 2: Core (API server binary) # ═══════════════════════════════════════════════════════════════ header "Phase 2/4 — Core (API Server)" cd "$PROJECT_DIR" # Migrations — apply all release/migrate_*.sql in order for mig in "$PROJECT_DIR"/release/migrate_*.sql; do [ ! -f "$mig" ] && continue MIG_NAME=$(basename "$mig") MIG_HASH=$(shasum -a 256 "$mig" | awk '{print $1}') # Check if already applied ALREADY=$("$PG_BIN/psql" -U accusys -d "$DB_NAME" -t -A -c \ "SELECT COUNT(*) FROM schema_migrations WHERE filename='$MIG_NAME'" 2>/dev/null || echo "0") if [ "$ALREADY" -gt 0 ]; then ok "Migration $MIG_NAME (already applied)" continue fi # Apply migration T0=$(date +%s%N) if "$PG_BIN/psql" -U accusys -d "$DB_NAME" -f "$mig" &>/dev/null; then T1=$(date +%s%N) DURATION_MS=$(( (T1 - T0) / 1000000 )) # Record in schema_migrations "$PG_BIN/psql" -U accusys -d "$DB_NAME" -c \ "INSERT INTO schema_migrations (filename, checksum, duration_ms) VALUES ('$MIG_NAME', '$MIG_HASH', $DURATION_MS) ON CONFLICT (filename) DO UPDATE SET checksum=EXCLUDED.checksum" &>/dev/null || true ok "Migration $MIG_NAME (${DURATION_MS}ms)" else fail "Migration $MIG_NAME FAILED" fi done ok "Database migrations applied" # Build core binary info "Building momentry_playground (API + worker binary)..." cargo build --bin momentry_playground 2>&1 | tail -3 if [ -f "$PROJECT_DIR/target/debug/momentry_playground" ]; then ok "momentry_playground binary ($(ls -lh target/debug/momentry_playground | awk '{print $5}'))" else fail "momentry_playground build" fi # Start API server if curl -sf http://127.0.0.1:3003/health &>/dev/null; then ok "API server already running" else DATABASE_SCHEMA=dev nohup target/debug/momentry_playground server --port 3003 \ > "$PROJECT_DIR/playground_boot.log" 2>&1 & for i in $(seq 1 10); do sleep 2; curl -sf http://127.0.0.1:3003/health &>/dev/null && break; done curl -sf http://127.0.0.1:3003/health &>/dev/null && \ ok "API server started (port 3003)" || fail "API server start" fi # Health check HEALTH=$(curl -sf http://127.0.0.1:3003/health 2>/dev/null || echo '{"status":"error"}') echo "$HEALTH" | python3 -c " import json,sys; d=json.load(sys.stdin) print(f' Version: {d.get(\"version\",\"?\")}') print(f' Build: {d.get(\"build_git_hash\",\"?\")}') print(f' Timestamp: {d.get(\"build_timestamp\",\"?\")}') print(f' Status: {d.get(\"status\",\"?\")}')" 2>/dev/null echo "$HEALTH" | python3 -c "import json,sys;d=json.load(sys.stdin);exit(0 if d.get('status')=='ok' else 1)" 2>/dev/null && \ ok "Health: ok" || warn "Health: degraded" # ═══════════════════════════════════════════════════════════════ # Phase 3: Worker (pipeline processing binary) # ═══════════════════════════════════════════════════════════════ header "Phase 3/4 — Worker (Pipeline Processor)" # Worker is the same binary (`momentry_playground worker`), already built above. # This phase verifies it can start and pick up jobs. # Test worker configuration info "Worker binary: target/debug/momentry_playground" info "Worker command: DATABASE_SCHEMA=dev ./target/debug/momentry_playground worker --max-concurrent 2 --poll-interval 5" # Create Qdrant collection for dev QDRANT_COLLECTION="${QDRANT_COLLECTION:-momentry_dev_rule1_v2}" EXISTS=$(curl -sf "http://localhost:6333/collections/$QDRANT_COLLECTION" 2>/dev/null | \ python3 -c "import sys,json;d=json.load(sys.stdin);print(d.get('result',{}).get('status','not_found'))" 2>/dev/null || echo "error") if [ "$EXISTS" = "not_found" ] || [ "$EXISTS" = "error" ]; then curl -sf -X PUT "http://localhost:6333/collections/$QDRANT_COLLECTION" \ -H "Content-Type: application/json" \ -d '{"vectors":{"size":768,"distance":"Cosine"}}' &>/dev/null || true fi curl -sf "http://localhost:6333/collections/$QDRANT_COLLECTION" &>/dev/null && \ ok "Qdrant collection '$QDRANT_COLLECTION'" || warn "Qdrant collection" # Test worker dry-run (verify it can at least parse config) info "Verifying worker can start (dry-run)..." DATABASE_SCHEMA=dev timeout 5 ./target/debug/momentry_playground worker \ --max-concurrent 1 --poll-interval 10 2>&1 | head -5 || true ok "Worker binary verified" # ═══════════════════════════════════════════════════════════════ # Phase 4: Watcher (File Detection) # ═══════════════════════════════════════════════════════════════ header "Phase 4/5 — Watcher (File Detection)" # Watcher is embedded in the server binary — auto-starts with `momentry_playground server`. # It polls the watch directory every 60s, detecting new files (detection only, # never auto-modifies). Configuration via MOMENTRY_SFTP_ROOT env var. WATCH_DIR="${MOMENTRY_SFTP_ROOT:-$PROJECT_DIR/storage/watch}" mkdir -p "$WATCH_DIR" # Verify watcher auto-started with the server (check logs for [WATCHER] message) if [ -f "$PROJECT_DIR/playground_boot.log" ] && grep -q "\[WATCHER\]" "$PROJECT_DIR/playground_boot.log" 2>/dev/null; then ok "Watcher started (check server logs for [WATCHER] messages)" elif curl -sf http://127.0.0.1:3003/health &>/dev/null; then # Server is running — watcher should be running inside it ok "Watcher should be running (auto-started with server)" info " Watch dir: $WATCH_DIR" info " Poll interval: 60s" else warn "Watcher status unknown (server not running)" fi # Place a test marker file to confirm watcher detects it TEST_MARKER="$WATCH_DIR/.watcher_test_$(date +%s)" touch "$TEST_MARKER" 2>/dev/null && ok "Watcher directory writable" || warn "Watcher directory not writable" rm -f "$TEST_MARKER" 2>/dev/null || true # ═══════════════════════════════════════════════════════════════ # Phase 5: Agents/Processors (Python scripts + ML models) # ═══════════════════════════════════════════════════════════════ header "Phase 5/5 — Agents/Processors" # ── 4a. Python dependencies ── sub "Python packages" for pkg in PyPDF2 python-docx openpyxl python-pptx; do if "$PYTHON_BIN" -c "import ${pkg%%=*}" &>/dev/null 2>&1; then ok "$pkg" else "$PYTHON_BIN" -m pip install "$pkg" --quiet && ok "$pkg" || warn "$pkg" fi done # ── 4b. Processor script inventory ── sub "Processor script inventory" cd "$PROJECT_DIR" SCRIPT_COUNT=$(find scripts -name '*.py' -type f 2>/dev/null | wc -l | tr -d ' ') ok "Total .py files: $SCRIPT_COUNT" # Check core processor scripts (required) PROCESSORS=(asr_processor yolo_processor face_processor pose_processor \ ocr_processor cut_processor caption_processor scene_classifier \ story_processor asrx_processor probe_file visual_chunk_processor) MISSING=0 for p in "${PROCESSORS[@]}"; do FILE=$(find scripts -name "${p}.py" -type f 2>/dev/null | head -1) if [ -n "$FILE" ]; then ok "Processor: $p" else warn "Processor: $p — not found" MISSING=$((MISSING + 1)) fi done # ── 4c. Script integrity (SHA256 check) ── sub "Script integrity (SHA256)" CHECKSUMS_FILE="$PROJECT_DIR/scripts/checksums.sha256" CS_TOTAL=0; CS_PASS=0; CS_FAIL=0 if [ -f "$CHECKSUMS_FILE" ]; then while IFS= read -r line; do [ -z "$line" ] && continue EXPECTED_HASH=$(echo "$line" | awk '{print $1}') FILE_PATH=$(echo "$line" | awk '{print $2}') FULL_PATH="$PROJECT_DIR/scripts/$FILE_PATH" CS_TOTAL=$((CS_TOTAL + 1)) if [ -f "$FULL_PATH" ]; then ACTUAL_HASH=$(shasum -a 256 "$FULL_PATH" 2>/dev/null | awk '{print $1}') if [ "$ACTUAL_HASH" = "$EXPECTED_HASH" ]; then CS_PASS=$((CS_PASS + 1)) else CS_FAIL=$((CS_FAIL + 1)) [ $CS_FAIL -le 5 ] && warn "$FILE_PATH — hash mismatch" fi else CS_FAIL=$((CS_FAIL + 1)) [ $CS_FAIL -le 5 ] && warn "$FILE_PATH — not found" fi done < "$CHECKSUMS_FILE" ok "$CS_PASS/$CS_TOTAL scripts match checksums" [ $CS_FAIL -gt 0 ] && fail "Script integrity: $CS_FAIL mismatches" || true else warn "checksums.sha256 not found — skipping integrity check" fi # ── 4d. ML models ── sub "ML models" MODEL_COUNT=$(find models -type f 2>/dev/null | wc -l | tr -d ' ') ok "Model files: $MODEL_COUNT" # ── 4e. Processor verification via API ── sub "Processor verification" DETAILED=$(curl -sf http://127.0.0.1:3003/health/detailed 2>/dev/null || echo '{}') echo "$DETAILED" | python3 -c " import json,sys d=json.load(sys.stdin) p=d.get('pipeline',{}) proc=p.get('processors',{}) print(f' Scripts dir: {p.get(\"scripts_ready\",\"?\")}') print(f' Script count: {p.get(\"scripts_count\",\"?\")}') print(f' Models dir: {p.get(\"models_ready\",\"?\")}') print(f' Model count: {p.get(\"models_count\",\"?\")}') print(f' ffmpeg: {p.get(\"ffmpeg\",\"?\")}') print(f' Embedding: {p.get(\"embedding_server\",{}).get(\"status\",\"?\")}') print(f' LLM: {p.get(\"llm\",{}).get(\"status\",\"?\")}') print(f' rsync: {p.get(\"rsync\",{}).get(\"status\",\"?\")}') print(f' Embedding srv:{p.get(\"embedding_server\",{}).get(\"status\",\"?\")}') for k in ['asr','yolo','face','pose','ocr','cut','caption','scene','story','asrx','probe','visual_chunk']: print(f' {k}: {\"✓\" if proc.get(k) else \"✗\"}') " 2>/dev/null # ── 4f. API smoke test ── sub "API smoke test" API_KEY="${MOMENTRY_API_KEY:-muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69}" STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \ -H "X-API-Key: $API_KEY" \ "http://127.0.0.1:3003/api/v1/videos?page=1&page_size=1" 2>/dev/null || echo "000") ok "GET /api/v1/videos → HTTP $STATUS" # ═══════════════════════════════════════════════════════════════ # Summary # ═══════════════════════════════════════════════════════════════ echo "" echo -e "${C}========================================${N}" if [ ${#FAILURES[@]} -eq 0 ]; then echo -e "${G} Install Complete — All Checks Passed${N}" else echo -e "${Y} Install Complete — ${#FAILURES[@]} Warnings${N}" for f in "${FAILURES[@]}"; do echo -e " ${R}✗${N} $f"; done fi echo -e "${C}========================================${N}" echo "" echo " API: http://127.0.0.1:3003" echo " Project: $PROJECT_DIR" echo " Config: $PROJECT_DIR/.env" echo " Scripts: $SCRIPT_COUNT .py files" echo " Watch dir: $WATCH_DIR" echo "" echo " Commands:" echo " Start server: DATABASE_SCHEMA=dev ./target/debug/momentry_playground server --port 3003" echo " Start worker: DATABASE_SCHEMA=dev ./target/debug/momentry_playground worker --max-concurrent 2" echo " Quick check: bash scripts/setup/check_momentry.sh" echo " Upgrade: bash scripts/setup/upgrade_momentry.sh " echo "" echo " Components installed:" echo " ✓ 環境 (Environment) — services, tools, config" echo " ✓ Core — API server (port 3003)" echo " ✓ Worker — pipeline processor" echo " ✓ Watcher — file detection (auto-started with server)" echo " ✓ Agents/Processors — $SCRIPT_COUNT .py scripts, 12 processors" echo "" exit $([ ${#FAILURES[@]} -eq 0 ] && echo 0 || echo 1)