#!/bin/bash # ══ CÀI CALITACUT — MỘT LỆNH (macOS) ══════════════════════════════════════════════════════════ # curl -fsSL "https://cai.calita.vn/cai.sh?k=" | bash # Worker cai.calita.vn nhúng mã học viên vào chỗ khach trước khi trả file này về. # Học viên KHÔNG phải: tải file, bấm đúp, vào Cài đặt bảo mật, gõ tên thương hiệu, dán lệnh thứ hai. set -uo pipefail export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 MA_HV="khach" NEN_CHINH="https://cai.calita.vn" NEN_DP="${CC_NEN_DP:-https://cai2.calita.vn}" # nguồn dự phòng: cùng nội dung, khác đường vào DICH="${CC_DICH:-$HOME/CalitaCut}" TAI="$DICH/.goi" LOG="${CC_LOG:-$HOME/CalitaCut-cai-dat.log}" CHI_KIEM="${CC_KIEM_TRA:-}" # --kiem-tra: chỉ kiểm máy rồi báo, không cài mkdir -p "$DICH" "$TAI" exec > >(tee -a "$LOG") 2>&1 T0=$(date +%s) ok() { echo " ✅ $*"; } xau() { echo " ❌ $*"; } muc() { echo; echo "── $1 ─────────────────────────"; } # ── báo về Calita (không kèm token, không kèm email, đường dẫn cắt tên người) ── bao() { # bao [lỗi] local buoc="$1" loi="${2:-}" cuoi cuoi=$(tail -50 "$LOG" 2>/dev/null | sed -e "s|$HOME|~|g" -e 's/[A-Za-z0-9._%+-]*@[A-Za-z0-9.-]*//g' \ -e 's/\(sk-[A-Za-z0-9_-]*\|[0-9]\{8,\}:[A-Za-z0-9_-]\{30,\}\)//g') /usr/bin/curl -fsS -m 20 -X POST "$NEN_CHINH/bao" -H 'content-type: application/json' \ --data-binary @<(/usr/bin/python3 -c ' import json,sys print(json.dumps({"ma":sys.argv[1],"os":sys.argv[2],"buoc":sys.argv[3],"loi":sys.argv[4],"log":sys.stdin.read()})) ' "$MA_HV" "macOS $(sw_vers -productVersion) $(uname -m)" "$buoc" "$loi" <<< "$cuoi") >/dev/null 2>&1 || true } dung() { echo; xau "$1"; [ $# -gt 1 ] && echo " → $2" bao "DỪNG: $1" "${2:-}" echo; echo "Chưa cài xong. Dán lại đúng dòng lệnh đó là máy làm tiếp từ chỗ dở." exit 1 } echo "════ CÀI CALITACUT · mã $MA_HV ════" echo "Máy tự làm hết. Cắm điện, giữ wifi. Khi trình duyệt mở ra, bấm Cho phép." # ══ 1. MÁY ══ muc "1/6 Kiểm máy" bao "bắt đầu" [ "$(uname -m)" = "arm64" ] || dung "Máy này dùng chip Intel, bản CalitaCut hiện tại chỉ chạy trên Mac chip Apple (M1 trở lên)." \ "Anh chị dùng giúp một máy Mac M1/M2/M3/M4, hoặc nhắn lại cho lớp để được hướng dẫn." ok "Mac chip Apple — $(sysctl -n machdep.cpu.brand_string)" MACOS=$(sw_vers -productVersion); [ "${MACOS%%.*}" -ge 13 ] || dung "macOS $MACOS quá cũ, cần macOS 13 trở lên." "Cài đặt hệ thống › Cập nhật phần mềm." ok "macOS $MACOS" RAM=$(( $(sysctl -n hw.memsize) / 1073741824 )); ok "RAM ${RAM} GB" TRONG=$(df -g "$HOME" | tail -1 | awk '{print $4}') if [ "$TRONG" -lt 40 ]; then echo " ổ còn ${TRONG} GB — dọn rác hệ thống…" rm -rf "$HOME/Library/Caches"/* 2>/dev/null; rm -rf /private/var/folders/*/*/T/* 2>/dev/null TRONG=$(df -g "$HOME" | tail -1 | awk '{print $4}') fi [ "$TRONG" -ge 40 ] || dung "Ổ đĩa chỉ còn ${TRONG} GB trống, cần 40 GB." "Xoá bớt phim/ảnh cũ rồi dán lại dòng lệnh." ok "ổ trống ${TRONG} GB" /usr/bin/curl -fsS -m 12 -o /dev/null "$NEN_CHINH/cai.sh" || dung "Máy không vào được mạng." "Nối wifi rồi dán lại dòng lệnh." ok "có mạng" if [ -n "$CHI_KIEM" ]; then bao "kiểm máy xong (chỉ kiểm)"; echo; echo "MÁY ĐẠT. Chờ tới buổi học thì dán lệnh cài."; exit 0; fi # ══ 2. CLAUDE (làm trước khi tải gói lớn) ══ muc "2/6 Tài khoản Claude" CLI="$HOME/.local/bin/claude" [ -x "$CLI" ] || { echo " đang cài Claude Code…"; /usr/bin/curl -fsSL https://claude.ai/install.sh | bash >/dev/null 2>&1; } [ -x "$CLI" ] || dung "Không cài được Claude Code." "Kiểm wifi rồi dán lại dòng lệnh." ok "Claude Code $("$CLI" --version 2>/dev/null | head -1)" da_dn() { "$CLI" auth status 2>/dev/null | grep -q '"loggedIn": true'; } if ! da_dn; then echo " Mở trình duyệt để anh chị đăng nhập tài khoản Claude…" "$CLI" auth login --claudeai >/dev/null 2>&1 & CHO=0 while ! da_dn && [ $CHO -lt 900 ]; do sleep 15; CHO=$((CHO+15)) [ $((CHO % 60)) -eq 0 ] && echo " … đang chờ anh chị bấm Cho phép trên trình duyệt ($((CHO/60)) phút)" if [ $CHO = 60 ] && ! pgrep -f "claude auth login" >/dev/null; then printf '%s' "https://claude.ai/login" | pbcopy 2>/dev/null echo " Trình duyệt không tự mở. Mở Chrome, dán (Cmd+V) rồi đăng nhập Claude." fi done da_dn || { bao "chờ đăng nhập Claude quá 15 phút"; dung "Chưa đăng nhập được Claude sau 15 phút." "Dán lại dòng lệnh khi anh chị rảnh tay."; } fi ok "đã đăng nhập Claude"; bao "xong Claude" # ══ 3. GOOGLE DRIVE ══ muc "3/6 Google Drive" CS="$HOME/Library/CloudStorage" tim_goc() { ls -d "$CS"/GoogleDrive-*/"Drive của tôi" "$CS"/GoogleDrive-*/"My Drive" 2>/dev/null; } if [ -n "${CC_KHO:-}" ]; then # CALITA KIỂM THỬ: chỉ định sẵn kho, bỏ qua dò Drive KHO="$CC_KHO"; mkdir -p "$KHO" elif [ -z "$(tim_goc)" ]; then if [ ! -d "/Applications/Google Drive.app" ]; then echo " đang tải Google Drive…" /usr/bin/curl -fsSL -o "$TAI/drive.dmg" "https://dl.google.com/drive-file-stream/GoogleDrive.dmg" || dung "Không tải được Google Drive." "Kiểm wifi rồi dán lại dòng lệnh." MNT=$(hdiutil attach -nobrowse "$TAI/drive.dmg" | tail -1 | awk '{print $NF}') echo " Máy sẽ hỏi MẬT KHẨU MÁY của anh chị để cài Google Drive. Gõ mật khẩu rồi bấm Enter." sudo installer -pkg "$MNT"/*.pkg -target / || { hdiutil detach "$MNT" >/dev/null 2>&1; dung "Cài Google Drive không xong." "Dán lại dòng lệnh và nhập mật khẩu máy."; } hdiutil detach "$MNT" >/dev/null 2>&1 fi open -a "Google Drive" >/dev/null 2>&1 echo " Đăng nhập Gmail của anh chị trong cửa sổ Google Drive vừa mở…" CHO=0 while [ -z "$(tim_goc)" ] && [ $CHO -lt 900 ]; do sleep 15; CHO=$((CHO+15)); [ $((CHO % 60)) -eq 0 ] && echo " … đang chờ Google Drive ($((CHO/60)) phút)"; done [ -n "$(tim_goc)" ] || { bao "chờ Google Drive quá 15 phút"; dung "Chưa thấy Google Drive." "Đăng nhập Google Drive rồi dán lại dòng lệnh."; } fi if [ -z "${CC_KHO:-}" ]; then GOC="" while read -r g; do [ -d "$g/CalitaCut Kho" ] && GOC="$g" && break; done <<< "$(tim_goc)" [ -n "$GOC" ] || GOC=$(tim_goc | head -1) KHO="$GOC/CalitaCut Kho" fi for s in "Video cần edit" "Thoại video" "video" "hinh" "screenshot" "nhac" "Anh + Nhac" "Chờ duyệt"; do mkdir -p "$KHO/$s"; done ok "kho: ${KHO/#$HOME/~}"; bao "xong Drive" # ══ 4. TẢI GÓI (nhiều nguồn, đổi nguồn khi đứng 30 giây) ══ muc "4/6 Tải bộ dựng" tai() { # tai — thử nguồn chính rồi nguồn dự phòng local khoa="$1" f="$2" nen for nen in "$NEN_CHINH" "$NEN_DP"; do /usr/bin/curl -fL --speed-limit 1 --speed-time 30 --retry 2 --retry-delay 3 -# -o "$f" "$nen/$khoa" && return 0 echo " nguồn $nen không ăn, đổi nguồn…" done return 1 } tai "goi/ban-ke.json" "$TAI/ban-ke.json" || dung "Không tải được bản kê gói cài." "Kiểm wifi rồi dán lại dòng lệnh." CAU_HINH_HV="$TAI/hv.json" tai "hv/$MA_HV.json" "$CAU_HINH_HV" || echo " (không có cấu hình riêng cho mã $MA_HV — dùng mặc định)" TEN_TH=$(/usr/bin/python3 -c "import json,sys;print(json.load(open(sys.argv[1])).get('ten_thuong_hieu','CalitaCut'))" "$CAU_HINH_HV" 2>/dev/null || echo CalitaCut) ok "tên thương hiệu của anh chị: $TEN_TH" PHIEN=$(/usr/bin/python3 -c "import json;print(json.load(open('$TAI/ban-ke.json'))['phien_ban'])") while IFS=$'\t' read -r ten khoa sha cỡ; do [ -n "$ten" ] || continue f="$TAI/$ten" if [ -f "$f" ]; then [ "$(/usr/bin/shasum -a 256 "$f" | cut -d' ' -f1)" = "$sha" ] && { ok "$ten đã có sẵn, đúng bản"; continue; } [ "$(stat -f%z "$f")" -ge "$cỡ" ] && rm -f "$f" # bản khác, không phải tải dở fi for lan in 1 2 3; do echo " tải $ten ($((cỡ/1048576)) MB)…" tai "$khoa" "$f" || true [ "$(/usr/bin/shasum -a 256 "$f" 2>/dev/null | cut -d' ' -f1)" = "$sha" ] && break rm -f "$f" done [ -f "$f" ] || dung "Tải $ten không được sau 3 lượt." "Kiểm wifi rồi dán lại dòng lệnh — máy tải tiếp từ chỗ dở." ok "$ten tải xong, đúng bản" done <<< "$(/usr/bin/python3 -c " import json d=json.load(open('$TAI/ban-ke.json')) for g in d['goi']: print('\t'.join([g['ten'],g['khoa'],g['sha256'],str(g['bytes'])]))")" for ten in $(/usr/bin/python3 -c " import json;print(' '.join(g['ten'] for g in json.load(open('$TAI/ban-ke.json'))['goi']))"); do echo " giải nén $ten…"; /usr/bin/ditto -x -k "$TAI/$ten" "$DICH" || dung "Giải nén $ten lỗi." "Dán lại dòng lệnh." done ok "đã đặt bộ dựng vào ${DICH/#$HOME/~}"; bao "xong tải gói $PHIEN" # ══ 4b. XOÁ ẢNH KHÁCH CỦA BẢN CŨ (lệnh 20/09 — máy đã cài bản trước) ══ CU="$HOME/video-editor" if [ -d "$CU" ] && [ "$CU" != "$DICH" ]; then echo " thấy bản cài cũ ở ~/video-editor — chuyển cấu hình và video sang bản mới, xoá ảnh khách trong bản cũ" [ -f "$CU/.env" ] && cp "$CU/.env" "$DICH/.env" 2>/dev/null [ -d "$CU/work" ] && rsync -a --exclude '_*' "$CU/work/" "$DICH/work/" 2>/dev/null fi N_XOA=0 for goc in "$CU" "$DICH"; do [ -d "$goc" ] || continue while IFS= read -r f; do [ -n "$f" ] && [ -f "$goc/$f" ] && rm -f "$goc/$f" && N_XOA=$((N_XOA+1)) done <<< "$(/usr/bin/python3 -c " import json,sys d=json.load(open('$TAI/ban-ke.json')) print('\n'.join(d.get('anh_khach_can_xoa',[])))" 2>/dev/null)" done [ "$N_XOA" -gt 0 ] && ok "đã xoá $N_XOA ảnh/video khách còn sót của bản cài cũ" # ══ 5. CÀI CÔNG CỤ ══ muc "5/6 Cài bộ công cụ (lâu nhất, 15–40 phút tuỳ mạng)" export UV_INSTALL_DIR="$DICH/.tools/uv" UV_PYTHON_INSTALL_DIR="$DICH/.tools/python" UV_NO_MODIFY_PATH=1 UV_HTTP_TIMEOUT=600 UV="$DICH/.tools/uv/uv"; VP="$DICH/.venv/bin/python" [ -x "$UV" ] || /usr/bin/curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 [ -x "$UV" ] || dung "Không cài được bộ quản lý Python (uv)." "Kiểm wifi rồi dán lại dòng lệnh." "$UV" python install 3.12 >/dev/null 2>&1 [ -x "$VP" ] || "$UV" venv --python 3.12 "$DICH/.venv" >/dev/null 2>&1 [ -x "$VP" ] || dung "Không dựng được môi trường Python." "Dán lại dòng lệnh." ok "Python $("$VP" -V 2>&1 | cut -d' ' -f2)" for lan in 1 2 3; do "$UV" pip install --python "$VP" -q -r "$DICH/requirements-lock.txt" && break echo " cài thư viện lỗi, thử lại lượt $((lan+1))…"; sleep $((lan*10)) done for m in mlx_whisper cv2 playwright yaml PIL; do "$VP" -c "import $m" 2>/dev/null || dung "Thư viện $m không nạp được." "Dán lại dòng lệnh."; done ok "thư viện đã sẵn sàng" FF="$DICH/.tools/ffmpeg" if [ ! -x "$FF/ffmpeg" ]; then mkdir -p "$FF" for b in ffmpeg ffprobe; do /usr/bin/curl -Ls --max-time 600 -o "$TAI/$b.zip" "https://ffmpeg.martin-riedl.de/redirect/latest/macos/arm64/release/$b.zip" && /usr/bin/unzip -oq "$TAI/$b.zip" -d "$FF" done chmod +x "$FF"/* 2>/dev/null; xattr -dr com.apple.quarantine "$FF" 2>/dev/null fi "$FF/ffmpeg" -version >/dev/null 2>&1 || dung "Không chạy được ffmpeg." "Dán lại dòng lệnh." ok "ffmpeg $("$FF/ffmpeg" -version | head -1 | cut -d' ' -f3)" "$VP" -m playwright install chromium >/dev/null 2>&1 MD="$DICH/.tools/models/whisper-large-v3-mlx-8bit" if [ ! -f "$MD/weights.npz" ]; then echo " tải model nghe–chép (1,6 GB, lâu nhất)…" "$VP" -c " from huggingface_hub import snapshot_download snapshot_download('mlx-community/whisper-large-v3-mlx-8bit', local_dir='$MD')" >/dev/null 2>&1 fi [ -f "$MD/weights.npz" ] || dung "Không tải được model nghe–chép." "Kiểm wifi rồi dán lại dòng lệnh." ok "model nghe–chép đã có"; bao "xong công cụ" # điền tên thương hiệu + đường kho, KHÔNG hỏi học viên "$VP" - "$DICH" "$KHO" "$TEN_TH" <<'PYX' import sys from pathlib import Path G, kho, ten = Path(sys.argv[1]), sys.argv[2], sys.argv[3] n = 0 for p in (q for q in G.rglob("*") if q.is_file() and q.suffix in {".yaml", ".md", ".html", ".txt", ".py", ".json"}): if ".venv" in p.parts or ".tools" in p.parts: continue try: s = p.read_text(encoding="utf-8") except Exception: continue if "{{TEN_THUONG_HIEU}}" in s or "{{KHO_DRIVE}}" in s: p.write_text(s.replace("{{TEN_THUONG_HIEU}}", ten).replace("{{KHO_DRIVE}}", kho), encoding="utf-8"); n += 1 print(f" điền tên thương hiệu và đường kho vào {n} file") PYX # ══ 6. DỰNG THỬ ══ muc "6/6 Dựng thử một video mẫu" # Bật bộ canh SAU khi dựng thử xong: bật trước thì lịch 3 phút nhảy vào dựng một video khác # ngay giữa lúc đang dựng thử ⇒ hai tiến trình chồng nhau, ffmpeg sập 0xC0000005 (đã mắc 20/09). PATH="$FF:$HOME/.local/bin:$PATH" "$VP" "$DICH/ops/kiem_tra_sau_cai.py"; MA=$? # bộ canh: chạy khi đăng nhập máy, chống ngủ khi cắm sạc PL="$HOME/Library/LaunchAgents/com.calitacut.watch.plist" mkdir -p "$HOME/Library/LaunchAgents" "$DICH/work" "$DICH/logs" cat > "$PL" < Labelcom.calitacut.watch ProgramArguments /usr/bin/caffeinate-dims $DICH/.venv/bin/python$DICH/watch.py WorkingDirectory$DICH RunAtLoadStartInterval180 ProcessTypeBackground StandardOutPath$DICH/work/_watch.log StandardErrorPath$DICH/work/_watch.log EnvironmentVariablesPATH$DICH/.tools/ffmpeg:/usr/local/bin:/usr/bin:/bin LANGen_US.UTF-8 PLIST launchctl bootout "gui/$(id -u)" "$PL" >/dev/null 2>&1 launchctl bootstrap "gui/$(id -u)" "$PL" >/dev/null 2>&1 || launchctl load "$PL" >/dev/null 2>&1 launchctl list 2>/dev/null | grep -q com.calitacut.watch && ok "bộ canh đã bật (3 phút xem thư mục một lần)" || xau "bộ canh chưa bật" rm -f "$DICH/2-CHAY-CALITACUT.command" 2>/dev/null ln -sfn "$KHO/Video cần edit" "$HOME/Desktop/CalitaCut - Tha video vao day" 2>/dev/null ok "đã tạo lối tắt trên Desktop" PHUT=$(( ($(date +%s) - T0) / 60 )) if [ "$MA" = "0" ]; then bao "CÀI XONG – ĐẠT ($PHUT phút)" echo; echo "ĐÃ XONG." echo "Thả video vào thư mục 'CalitaCut - Tha video vao day' trên Desktop." echo "Khoảng 30 phút sau, mở Google Drive › CalitaCut Kho › Chờ duyệt để xem bản dựng." exit 0 else bao "bộ kiểm sau cài báo lệch" "mã $MA" echo; echo "Máy đã cài xong nhưng bản dựng thử có mục lệch. Calita đã nhận được báo cáo và sẽ liên hệ anh chị." exit 1 fi