#!/bin/bash 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 ─────────────────────────"; } json_chu() { printf '%s' "$1" | /usr/bin/awk '{gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); gsub(/\t/,"\\t"); printf "%s",$0}'; } json_chu_nhieu_dong() { /usr/bin/awk '{gsub(/\\/,"\\\\"); gsub(/"/,"\\\""); gsub(/\t/,"\\t"); printf "%s%s",(NR>1?"\\n":""),$0}'; } json_lay() { # json_lay → in giá trị; thiếu khoá / hỏng file thì KHÔNG in gì, mã 1 [ -f "$1" ] || return 1 /usr/bin/osascript -l JavaScript - "$1" "$2" 2>/dev/null <<'JXA' function run(a) { ObjC.import('Foundation'); var s = $.NSString.stringWithContentsOfFileEncodingError(a[0], $.NSUTF8StringEncoding, null); if (!s || s.isNil()) throw new Error('doc'); var v = JSON.parse(ObjC.unwrap(s)); var ks = String(a[1]).split('.'); for (var i = 0; i < ks.length; i++) { if (v === null || v === undefined || typeof v !== 'object') throw new Error('khoa'); v = v[ks[i]]; } if (v === null || v === undefined) throw new Error('khoa'); if (Array.isArray(v)) return String(v.length); if (typeof v === 'object') throw new Error('object'); return String(v); } JXA } 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') local ten_may nguoi doi phien_co ten_may=$(/usr/sbin/scutil --get ComputerName 2>/dev/null || hostname -s) nguoi=$(id -un 2>/dev/null); doi=$(sysctl -n hw.model 2>/dev/null) phien_co=$(json_lay "$TAI/ban-ke.json" phien_ban 2>/dev/null || echo "chưa cài") /usr/bin/curl -fsS -m 20 -X POST "$NEN_CHINH/bao" -H 'content-type: application/json' \ --data-binary @<(printf '{"ma":"%s","os":"%s","buoc":"%s","loi":"%s","log":"%s","may":{"ten_may":"%s","nguoi_dung":"%s","doi_may":"%s","ban_dang_co":"%s"}}' \ "$(json_chu "$MA_HV")" "$(json_chu "macOS $(sw_vers -productVersion) $(uname -m)")" \ "$(json_chu "$buoc")" "$(json_chu "$loi")" "$(printf '%s' "$cuoi" | json_chu_nhieu_dong)" \ "$(json_chu "$ten_may")" "$(json_chu "$nguoi")" "$(json_chu "$doi")" "$(json_chu "$phien_co")") >/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." muc "1/6 Kiểm máy" bao "bắt đầu" CHIP="${CC_GIA_LAP_CHIP:-$(uname -m)}" # CC_GIA_LAP_CHIP: Calita giả lập nhánh Intel để kiểm thử MACOS="${CC_GIA_LAP_MACOS:-$(sw_vers -productVersion)}"; MJ=${MACOS%%.*} # CC_GIA_LAP_MACOS: Calita kiểm thử if [ "$CHIP" = "arm64" ]; then NHANH="apple" ok "Mac chip Apple — $(sysctl -n machdep.cpu.brand_string)" [ "$MJ" -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." else NHANH="intel" ok "Mac chip Intel — $(sysctl -n machdep.cpu.brand_string 2>/dev/null || echo Intel)" [ "$MJ" -ge 14 ] || dung "Máy Mac chip Intel cần macOS 14 (Sonoma) trở lên, máy này đang là macOS ${MACOS}." \ "Mở Cài đặt hệ thống › Cài đặt chung › Cập nhật phần mềm. Nếu máy không lên được macOS 14 thì nhắn cho lớp." fi ok "macOS ${MACOS}" RAM="${CC_GIA_LAP_RAM:-$(( $(sysctl -n hw.memsize) / 1073741824 ))}" # CC_GIA_LAP_RAM: Calita kiểm thử NHAN="${CC_GIA_LAP_NHAN:-$(sysctl -n hw.physicalcpu 2>/dev/null || echo 4)}" # CC_GIA_LAP_NHAN: Calita kiểm thử if [ "$NHANH" = "intel" ]; then [ "$RAM" -ge 8 ] || dung "Máy chỉ có ${RAM} GB RAM." "Bản CalitaCut cần ít nhất 8 GB RAM." UOC=$(( 6 + 48 / NHAN )) ok "RAM ${RAM} GB · ${NHAN} nhân — video 60 giây dựng khoảng ${UOC} phút" [ "$RAM" -lt 16 ] && echo " Máy 8 GB chạy được nhưng chậm hơn; lúc máy đang dựng anh chị đừng mở thêm ứng dụng nặng." else ok "RAM ${RAM} GB" fi NGUONG_O="${CC_NGUONG_O:-40}" # CALITA KIỂM THỬ: hạ ngưỡng ổ trống TRONG=$(df -g "$HOME" | tail -1 | awk '{print $4}') [ "$TRONG" -ge "$NGUONG_O" ] || dung "Ổ đĩa chỉ còn ${TRONG} GB trống, CalitaCut cần ${NGUONG_O} GB — anh chị cần dọn thêm $((NGUONG_O - TRONG)) GB." \ "Xoá bớt phim, ảnh hoặc ứng dụng không dùng (hoặc dời sang ổ ngoài), rồi dán lại đúng dòng lệnh này." 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 GP_KET=""; GP_HAN=""; GP_MIEN=""; DAU_MAY="" if [ "$MA_HV" != "lopcalit-048797" ]; then U=$(/usr/sbin/ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | awk -F'"' '/IOPlatformUUID/{print $4}' | tr '[:lower:]' '[:upper:]') [ -n "${CC_GIA_LAP_MAY:-}" ] && U="$CC_GIA_LAP_MAY" # CALITA KIỂM THỬ: giả làm máy khác [ -n "$U" ] && DAU_MAY=$(printf '%s' "$U" | /usr/bin/shasum -a 256 | cut -c1-16) GP_JSON=$(/usr/bin/curl -fsS -m 20 -X POST "$NEN_CHINH/gp" -H 'content-type: application/json' \ --data-binary "{\"ma\":\"$(json_chu "$MA_HV")\",\"dau_may\":\"$DAU_MAY\",\"buoc\":\"cai\"}" 2>/dev/null) GP_TAM=$(mktemp -t cc_gp); printf '%s' "$GP_JSON" > "$GP_TAM" GP_KET=$(json_lay "$GP_TAM" ket); GP_HAN=$(json_lay "$GP_TAM" het_han); GP_MIEN=$(json_lay "$GP_TAM" mien_gia_han) rm -f "$GP_TAM" case "$GP_KET" in ok) ok "giấy phép: $([ "$GP_MIEN" = "true" ] && echo "không thời hạn" || echo "dùng tới ${GP_HAN}")" ;; khoa|het_han) bao "giấy phép từ chối lúc cài" "$GP_KET" dung "Giấy phép CalitaCut đã hết hạn hoặc bị tạm dừng." "Liên hệ đội hỗ trợ lớp thầy Hải." ;; may_khac) bao "giấy phép từ chối lúc cài" "mã đã gắn máy khác" dung "Mã cài này đã được dùng trên một máy khác. Mỗi mã chỉ cài được một máy." "Liên hệ đội hỗ trợ lớp thầy Hải (đổi máy thì đội hỗ trợ gỡ máy cũ cho anh chị)." ;; tu_do) : ;; *) echo " (chưa hỏi được máy chủ giấy phép — vẫn cài tiếp, bộ canh tự hỏi lại sau)" ;; esac fi DON="" UID_=$(id -u) for pl in "$HOME/Library/LaunchAgents/"com.calitacut*.plist "$HOME/Library/LaunchAgents/"com.calita.watch*.plist; do [ -f "$pl" ] || continue nhan=$(basename "$pl" .plist) [ "$nhan" = "${CC_NHAN_CANH:-com.calitacut.watch}" ] && continue launchctl bootout "gui/$UID_/$nhan" >/dev/null 2>&1 launchctl unload -w "$pl" >/dev/null 2>&1 mv "$pl" "$pl.tat-$(date +%m%d)" 2>/dev/null && DON="$DON; lịch chạy cũ $nhan (đã tắt, file đổi tên giữ lại)" done for pid in $([ -z "${CC_KHONG_DON:-}" ] && pgrep -f "watch\.py" 2>/dev/null); do # CC_KHONG_DON: Calita cài thử, không tắt bộ canh thật [ "$pid" = "$$" ] && continue kill "$pid" 2>/dev/null && DON="$DON; tiến trình bộ canh cũ (PID $pid)" done CU_TRANG="" for d in "$HOME/CalitaCut" "$HOME/Desktop/CalitaCut" "$HOME/Documents/CalitaCut" "$HOME/Downloads/CalitaCut" "/Applications/CalitaCut"; do [ "$d" = "$DICH" ] && continue [ -f "$d/work/_watch_state.json" ] || continue CU_TRANG="$d/work/_watch_state.json" DON="$DON; thư mục cài cũ $d (GIỮ NGUYÊN, không xoá)" done if [ -n "$CU_TRANG" ] && [ ! -f "$DICH/work/_watch_state.json" ]; then mkdir -p "$DICH/work" && cp "$CU_TRANG" "$DICH/work/_watch_state.json" 2>/dev/null && DON="$DON; mang sổ video đã dựng của bản cũ sang" fi if [ -n "$DON" ]; then muc "1b/6 Dọn bản cũ (không xoá video, không đụng Drive)" echo "$DON" | tr ';' '\n' | while read -r x; do [ -n "$x" ] && ok "$(echo "$x" | sed 's/^ *//')"; done bao "don ban cu" "$DON" fi 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 [ -n "${CC_GIA_LAP_KHONG_MO:-}" ]; then # CALITA KIỂM THỬ nhánh trình duyệt không mở 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." echo " (kiểm thử: đã chép link vào bộ nhớ tạm, bỏ qua chờ)" elif ! da_dn; then echo " Trình duyệt sẽ mở trang Claude: đăng nhập rồi bấm Cho phép (Authorize)." echo " Nếu trang hiện một dãy mã: chép dãy mã đó, quay lại cửa sổ này, dán (Cmd+V) rồi bấm Enter." echo " Không giới hạn thời gian. Muốn thoát: bấm Control + C (dán lại dòng lệnh khi rảnh)." LAN=0 while ! da_dn; do LAN=$((LAN+1)) if ( : < /dev/tty ) 2>/dev/null; then "$CLI" auth login --claudeai < /dev/tty || true else "$CLI" auth login --claudeai >/dev/null 2>&1 & # không có bàn phím (chạy từ xa): chờ trình duyệt quay về while ! da_dn && kill -0 $! 2>/dev/null; do sleep 5; done fi da_dn && break echo " ❌ Chưa đăng nhập được (lần $LAN): mã dán sai, dán thiếu hoặc mã đã hết hạn." echo " → Trình duyệt sẽ mở lại. Đăng nhập, bấm Cho phép, chép ĐÚNG dãy mã mới rồi dán vào đây." [ $LAN -eq 3 ] && bao "đăng nhập Claude chưa được sau 3 lần" "mã dán sai hoặc hết hạn" [ -n "${CC_DN_TOI_DA:-}" ] && [ $LAN -ge "$CC_DN_TOI_DA" ] && { echo " (kiểm thử: dừng sau $LAN lần)"; break; } # CALITA KIỂM THỬ sleep 2 done da_dn || dung "Vẫn chưa đăng nhập được Claude." "Dán lại dòng lệnh khi anh chị rảnh tay." fi ok "đã đăng nhập Claude"; bao "xong Claude" 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 ten_chuan_hoa() { /usr/bin/perl -MUnicode::Normalize -MEncode -e ' use utf8; my ($kho, $ten) = map { decode_utf8($_) } @ARGV; $ten = NFC($ten); sub k { my $x = NFD(shift); $x =~ s/\s*\(\d+\)\s*$//; $x =~ s/\p{Mn}//g; $x =~ s/\x{111}/d/g; $x =~ s/\x{110}/D/g; $x = lc $x; $x =~ s/\s+/ /g; $x =~ s/^ | $//g; $x } opendir(my $dh, encode_utf8($kho)) or exit 0; my @m = grep { !/^\./ && -d encode_utf8("$kho/$_") } map { decode_utf8($_) } readdir $dh; closedir $dh; exit 0 if grep { $_ eq $ten } @m; my @c = grep { k($_) eq k($ten) } @m; exit 0 unless @c; my $tam = "$kho/.calitacut-doi-ten-$$"; if (rename(encode_utf8("$kho/$c[0]"), encode_utf8($tam)) && rename(encode_utf8($tam), encode_utf8("$kho/$ten"))) { print encode_utf8(" đổi tên thư mục \"$c[0]\" → \"$ten\" (tên chuẩn, không tạo thêm bộ mới)\n") } ' "$1" "$2" } for s in "Video cần edit" "Thoại video" "video" "hinh" "screenshot" "nhac" "Anh + Nhac" "Chờ duyệt"; do ten_chuan_hoa "$KHO" "$s"; mkdir -p "$KHO/$s" done ok "kho: ${KHO/#$HOME/~}" echo " Làm MỘT LẦN cho nhanh: mở Finder › Google Drive › CalitaCut Kho, chuột phải thư mục" echo " 'Video cần edit' › Offline access › Available offline. Sau đó video tự tải về máy."; bao "xong Drive" muc "4/6 Tải bộ dựng" TAI_RC=0 tai() { # tai — thử nguồn chính rồi nguồn dự phòng; mã lỗi curl cuối cùng để ở TAI_RC 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" && { TAI_RC=0; return 0; } TAI_RC=$? echo " nguồn $nen không ăn (mã $TAI_RC), đổi nguồn…" done return 1 } la_loi_mang() { case "$TAI_RC" in 5|6|7|28|35|52|55|56) return 0 ;; *) return 1 ;; esac; } loi_tai() { if la_loi_mang; then echo "mạng chập chờn, mã $TAI_RC"; elif [ "$TAI_RC" = 22 ]; then echo "máy chủ chưa có gói này"; elif [ "$TAI_RC" = 0 ]; then echo "tải về nhưng không khớp bản kê"; else echo "lỗi bộ cài, mã $TAI_RC"; fi; } viec_tai() { if la_loi_mang; then echo "Kiểm wifi rồi dán lại dòng lệnh — máy tải tiếp từ chỗ dở."; elif [ "$TAI_RC" = 0 ]; then echo "Dán lại dòng lệnh; vẫn báo thế thì chụp màn hình gửi lớp."; else echo "Không phải lỗi wifi. Chụp màn hình gửi lớp để Calita sửa."; fi; } ke_goi() { local n i n=$(json_lay "$TAI/ban-ke.json" goi 2>/dev/null) || return 1 i=0 while [ "$i" -lt "$n" ]; do printf '%s\t%s\t%s\t%s\n' \ "$(json_lay "$TAI/ban-ke.json" goi.$i.ten 2>/dev/null)" \ "$(json_lay "$TAI/ban-ke.json" goi.$i.khoa 2>/dev/null)" \ "$(json_lay "$TAI/ban-ke.json" goi.$i.sha256 2>/dev/null)" \ "$(json_lay "$TAI/ban-ke.json" goi.$i.bytes 2>/dev/null)" i=$((i+1)) done } 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)" BAN_KE=$(json_lay "$CAU_HINH_HV" ban_ke) printf '%s' "$BAN_KE" | /usr/bin/grep -Eq '^goi/[A-Za-z0-9._/-]+\.json$' || BAN_KE="goi/ban-ke.json" [ "$BAN_KE" = "goi/ban-ke.json" ] || ok "kênh riêng: $BAN_KE" tai "$BAN_KE" "$TAI/ban-ke.json" || dung "Không tải được bản kê gói cài ($(loi_tai))." "$(viec_tai)" TEN_TH=$(json_lay "$CAU_HINH_HV" ten_thuong_hieu 2>/dev/null || echo CalitaCut) [ -n "$TEN_TH" ] || TEN_TH=CalitaCut ok "tên thương hiệu của anh chị: $TEN_TH" PHIEN=$(json_lay "$TAI/ban-ke.json" phien_ban 2>/dev/null) [ -n "$PHIEN" ] || dung "Bản kê gói cài đọc không được." "Dán lại dòng lệnh." while IFS=$'\t' read -r ten khoa sha sz; 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 "${sz}" ] && 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} ($(( sz / 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 ($(loi_tai))." "$(viec_tai)" ok "${ten} tải xong, đúng bản" done <<< "$(ke_goi)" KE_CU="$DICH/work/_ma_sha256_da_cai.json" if [ -x "$DICH/.venv/bin/python" ] && [ -f "$DICH/ops/kiem_ma_tay.py" ] && [ -f "$KE_CU" ]; then "$DICH/.venv/bin/python" "$DICH/ops/kiem_ma_tay.py" "$DICH" "$KE_CU" "$TEN_TH" "$KHO" || true [ -f "$DICH/work/_ma_da_sua.json" ] && bao "mã đã bị sửa tay" "$(cat "$DICH/work/_ma_da_sua.json" | head -c 400)" fi [ -f "$DICH/config.yaml" ] && cp "$DICH/config.yaml" "$TAI/config.cu.yaml" 2>/dev/null for ten in $(ke_goi | /usr/bin/awk -F'\t' '{print $1}'); 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 mkdir -p "$DICH/work"; printf '%s' "$MA_HV" > "$DICH/work/_ma_hv.txt" # (22/09) để lệnh tự kiểm tra biết của ai if [ "$MA_HV" != "lopcalit-048797" ] && [ "$GP_KET" != "tu_do" ]; then printf '{"quan_ly": true, "lan_cai": %s, "ket": "%s", "lan_ok": %s, "het_han": %s, "mien_gia_han": %s}\n' "$(date +%s)" "$GP_KET" \ "$([ "$GP_KET" = "ok" ] && date +%s || echo null)" "$([ -n "$GP_HAN" ] && echo "\"$GP_HAN\"" || echo null)" \ "$([ "$GP_MIEN" = "true" ] && echo true || echo false)" > "$DICH/work/_giay_phep.json" else rm -f "$DICH/work/_giay_phep.json" fi rm -f "$DICH/work/_tam_dung.txt" 2>/dev/null # (23/09, ca D2) cài lại = gỡ TẠM DỪNG của bộ canh ok "đã đặt bộ dựng vào ${DICH/#$HOME/~}"; bao "xong tải gói $PHIEN" CU="$HOME/video-editor" la_kho_goc() { [ -d "$1/.git" ] || [ -f "$1/ops/phat_r2.py" ] || [ -f "$1/ops/phat_hoc_vien.py" ]; } if la_kho_goc "$CU"; then echo " (bỏ qua ${CU}: đây là kho gốc của Calita, không phải bản cài học viên)" CU="" fi if [ -n "$CU" ] && [ -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 ke_anh_khach() { local n i n=$(json_lay "$TAI/ban-ke.json" anh_khach_can_xoa 2>/dev/null) || return 0 i=0 while [ "$i" -lt "$n" ]; do json_lay "$TAI/ban-ke.json" anh_khach_can_xoa.$i 2>/dev/null i=$((i+1)) done } N_XOA=0 for goc in "$CU" "$DICH"; do [ -n "$goc" ] && [ -d "$goc" ] || continue la_kho_goc "$goc" && continue while IFS= read -r f; do [ -n "$f" ] && [ -f "$goc/$f" ] && rm -f "$goc/$f" && N_XOA=$((N_XOA+1)) done <<< "$(ke_anh_khach)" done [ "$N_XOA" -gt 0 ] && ok "đã xoá $N_XOA ảnh/video khách còn sót của bản cài 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" uv_chay() { [ -x "$UV" ] && "$UV" --version >/dev/null 2>&1; } py_chay() { [ -x "$VP" ] && "$VP" -V >/dev/null 2>&1; } if ! uv_chay; then [ -e "$UV" ] && { echo " uv sẵn có không chạy được — cài lại"; rm -rf "$DICH/.tools/uv"; } /usr/bin/curl -LsSf https://astral.sh/uv/install.sh | sh >/dev/null 2>&1 fi uv_chay || 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 if ! py_chay; then [ -e "$DICH/.venv" ] && { echo " môi trường Python cũ không chạy được — dựng lại"; rm -rf "$DICH/.venv"; } "$UV" venv --python 3.12 "$DICH/.venv" >/dev/null 2>&1 fi py_chay || 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)" REQ="$DICH/requirements-lock.txt" [ "$NHANH" = "intel" ] && REQ="$DICH/requirements-mac-intel.txt" if [ ! -f "$REQ" ]; then dung "Gói cài trên máy chủ chưa có phần dành cho máy Mac chip Intel (thiếu $(basename "$REQ"))." \ "Nhắn cho lớp để Calita phát lại gói, rồi dán lại đúng dòng lệnh đó." fi [ "$NHANH" != "intel" ] || [ -f "$DICH/config.mac-intel.yaml" ] || \ dung "Gói cài thiếu config.mac-intel.yaml (phần chỉnh cho máy chip Intel)." \ "Nhắn cho lớp để Calita phát lại gói, rồi dán lại đúng dòng lệnh đó." for lan in 1 2 3; do "$UV" pip install --python "$VP" -q -r "$REQ" && break echo " cài thư viện lỗi, thử lại lượt $((lan+1))…"; sleep $((lan*10)) done MODULE="mlx_whisper cv2 playwright yaml PIL" [ "$NHANH" = "intel" ] && MODULE="faster_whisper ctranslate2 cv2 playwright yaml PIL" for m in ${MODULE}; 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" FF_ARCH=arm64; [ "$NHANH" = "intel" ] && FF_ARCH=amd64 chay_duoc() { [ -x "$1" ] && "$1" -version >/dev/null 2>&1; } ARCH_CAN=arm64; [ "$FF_ARCH" = "amd64" ] && ARCH_CAN=x86_64 dung_arch() { /usr/bin/lipo -archs "$1" 2>/dev/null | tr ' ' '\n' | grep -qx "$ARCH_CAN"; } if ! chay_duoc "$FF/ffmpeg" || ! chay_duoc "$FF/ffprobe" || ! dung_arch "$FF/ffmpeg" || ! dung_arch "$FF/ffprobe"; then if [ -e "$FF/ffmpeg" ] || [ -e "$FF/ffprobe" ]; then LY_DO="không chạy được" dung_arch "$FF/ffmpeg" || LY_DO="sai kiến trúc ($(/usr/bin/lipo -archs "$FF/ffmpeg" 2>/dev/null || echo '?'), máy này cần $ARCH_CAN)" echo " ffmpeg sẵn có trong thư mục cài $LY_DO — xoá và tải lại bản $FF_ARCH" bao "ffmpeg cũ phải thay: $LY_DO" rm -rf "$FF" fi mkdir -p "$FF" for b in ffmpeg ffprobe; do tai "may/$b-mac-$FF_ARCH.zip" "$TAI/$b.zip" \ || /usr/bin/curl -Ls --max-time 600 -o "$TAI/$b.zip" "https://ffmpeg.martin-riedl.de/redirect/latest/macos/$FF_ARCH/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 if ! FF_LOI=$("$FF/ffmpeg" -version 2>&1); then echo " ffmpeg báo: $(printf '%s' "$FF_LOI" | head -2)" dung "Không chạy được ffmpeg (kiến trúc $FF_ARCH)." "Gửi dòng 'ffmpeg báo:' ngay trên cho lớp." fi ok "ffmpeg $("$FF/ffmpeg" -version | head -1 | cut -d' ' -f3)" "$VP" -m playwright install chromium >/dev/null 2>&1 if [ "$NHANH" = "intel" ]; then MD="$DICH/.tools/models/faster-whisper-large-v3-turbo-ct2"; KHO_MD="deepdml/faster-whisper-large-v3-turbo-ct2"; F_MD="model.bin" else MD="$DICH/.tools/models/whisper-large-v3-mlx-8bit"; KHO_MD="mlx-community/whisper-large-v3-mlx-8bit"; F_MD="weights.npz" fi if [ ! -f "$MD/$F_MD" ]; 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('${KHO_MD}', local_dir='${MD}')" >/dev/null 2>&1 fi KQ_MD=$("$VP" "$DICH/ops/kiem_model.py" --sua 2>&1 | tail -1) case "$KQ_MD" in *"đủ"*) ok "model nghe–chép đủ file" ;; *) bao "model nghe–chép hỏng" "$KQ_MD"; dung "Không tải đủ model nghe–chép (${KQ_MD#model nghe–chép: })." "Kiểm wifi rồi dán lại dòng lệnh — máy tải tiếp phần còn thiếu." ;; esac bao "xong công cụ" "$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 if [ -f "$TAI/config.cu.yaml" ] && [ -f "$DICH/ops/gop_config.py" ]; then "$VP" "$DICH/ops/gop_config.py" "$TAI/config.cu.yaml" "$DICH/config.yaml" \ && cp "$TAI/config.cu.yaml" "$DICH/config.yaml" fi [ -f "$DICH/ops/kiem_ma_tay.py" ] && "$VP" "$DICH/ops/kiem_ma_tay.py" --ghi "$DICH" "$DICH/work/_ma_sha256_da_cai.json" || true muc "6/6 Bật bộ canh và dựng thử một video mẫu" if [ -n "${CC_KHONG_TASK:-}" ]; then ok "bỏ qua bật bộ canh (CC_KHONG_TASK — Calita kiểm thử)"; else NHAN_CANH="${CC_NHAN_CANH:-com.calitacut.watch}" # CC_NHAN_CANH: Calita thử, không đè bộ canh thật PL="$HOME/Library/LaunchAgents/$NHAN_CANH.plist" mkdir -p "$HOME/Library/LaunchAgents" "$DICH/work" "$DICH/logs" cat > "$PL" < Label$NHAN_CANH 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 UID_="$(id -u)" da_nap() { local i for i in 1 2 3 4 5 6; do launchctl print "gui/$UID_/$NHAN_CANH" >/dev/null 2>&1 && return 0 ( set +o pipefail; launchctl list 2>/dev/null | grep -q "$NHAN_CANH" ) && return 0 sleep 1 done return 1 } CANH_OK=0; CANH_CACH=""; CANH_LOI="" launchctl bootout "gui/$UID_/$NHAN_CANH" >/dev/null 2>&1 sleep 2 # launchd cần một nhịp sau bootout, nạp lại ngay hay trượt L=$(launchctl bootstrap "gui/$UID_" "$PL" 2>&1); R=$? if [ $R -eq 0 ] && da_nap; then CANH_OK=1; CANH_CACH="launchctl bootstrap"; else CANH_LOI="bootstrap: mã $R ${L}" echo " (cách 1 không được: mã $R ${L:0:90})" L=$(launchctl load -w "$PL" 2>&1); R=$? if [ $R -eq 0 ] && da_nap; then CANH_OK=1; CANH_CACH="launchctl load -w"; else CANH_LOI="$CANH_LOI · load: mã $R ${L}" echo " (cách 2 không được: mã $R ${L:0:90})" launchctl enable "gui/$UID_/$NHAN_CANH" >/dev/null 2>&1 L=$(launchctl bootstrap "gui/$UID_" "$PL" 2>&1); R=$? if [ $R -eq 0 ] && da_nap; then CANH_OK=1; CANH_CACH="launchctl enable + bootstrap"; else CANH_LOI="$CANH_LOI · enable+bootstrap: mã $R ${L}" echo " (cách 3 không được: mã $R ${L:0:90})" fi fi fi if [ "$CANH_OK" = "1" ]; then NHIP="$DICH/work/_watch_nhip.txt"; CU_NHIP=$(cat "$NHIP" 2>/dev/null || echo 0) launchctl kickstart -p "gui/$UID_/$NHAN_CANH" >/dev/null 2>&1 for _i in 1 2 3 4 5 6 7 8 9 10; do sleep 4 [ "$(cat "$NHIP" 2>/dev/null || echo 0)" != "$CU_NHIP" ] && break done if [ "$(cat "$NHIP" 2>/dev/null || echo 0)" = "$CU_NHIP" ]; then CANH_OK=0; CANH_LOI="$CANH_LOI · đã nạp nhưng chạy thử không lên nhịp (macOS có thể đang CHẶN mục chạy nền)" echo " (đã nạp nhưng chạy thử không thấy nhịp — macOS có thể đang chặn mục chạy nền)" fi fi if [ "$CANH_OK" = "0" ]; then nohup bash -c "while true; do \"$VP\" \"$DICH/watch.py\" >> \"$DICH/work/_watch.log\" 2>&1; sleep 180; done" \ >/dev/null 2>&1 & sleep 6 if pgrep -f "$DICH/watch.py" >/dev/null 2>&1 || [ -f "$DICH/work/_watch_nhip.txt" ]; then CANH_OK=2; CANH_CACH="chạy nền tạm (chưa tự bật lại sau khi tắt máy)" fi fi if [ "$CANH_OK" = "1" ]; then ok "bộ canh đã bật — $CANH_CACH (3 phút xem thư mục một lần)" elif [ "$CANH_OK" = "2" ]; then xau "bộ canh ĐANG chạy tạm, nhưng chưa tự bật lại khi mở máy lần sau" echo " → Mở: Cài đặt hệ thống › Cài đặt chung › Mục đăng nhập & Tiện ích mở rộng" echo " (macOS 12 trở xuống: Tuỳ chọn hệ thống › Người dùng & Nhóm › Mục đăng nhập)" echo " → Bật công tắc cho mục tên \"$NHAN_CANH\" trong phần 'Cho phép ở chế độ nền'." bao "bộ canh chỉ chạy tạm" "$CANH_LOI" else xau "CHƯA bật được bộ canh trên máy này" echo " → Máy sẽ KHÔNG tự nhặt video. Nhắn cho lớp: 'máy em không bật được bộ canh'." echo " → Muốn dựng ngay một video: mở Terminal, dán: \"$VP\" \"$DICH/watch.py\"" bao "KHÔNG bật được bộ canh" "$CANH_LOI" fi rm -f "$DICH/2-CHAY-CALITACUT.command" 2>/dev/null fi "$VP" "$DICH/ops/gom_thu_muc.py" --cai 2>&1 | sed 's/^/ /' || true PATH="$FF:$HOME/.local/bin:$PATH" "$VP" "$DICH/ops/kiem_tra_sau_cai.py" < /dev/null; MA=$? [ -f "$DICH/work/_kiem_tra.json" ] && grep -q '"dat": true' "$DICH/work/_kiem_tra.json" && MA=0 if [ -n "${CC_KHONG_TASK:-}" ]; then ok "bỏ qua tạo lối tắt Desktop (CC_KHONG_TASK — Calita kiểm thử)" else 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" fi PHUT=$(( ($(date +%s) - T0) / 60 )) if [ "$MA" = "0" ] && [ "${CANH_OK:-1}" != "1" ]; then bao "CÀI XONG nhưng CHƯA có bộ canh ($PHUT phút)" echo; echo "CHƯA XONG HẲN." if [ "${CANH_OK:-0}" = "2" ]; then echo "Máy dựng được video và ĐANG canh thư mục, nhưng tắt máy rồi mở lại là dừng." echo "Bật vĩnh viễn: Cài đặt hệ thống › Cài đặt chung › Mục đăng nhập & Tiện ích mở rộng" echo "→ bật công tắc cho mục \"$NHAN_CANH\" ở phần 'Cho phép ở chế độ nền'." else echo "Máy dựng được video, nhưng CHƯA tự nhặt video trong thư mục." echo "Nhắn cho lớp: 'máy em cài xong nhưng không bật được bộ canh' — Calita bật giúp." fi elif [ "$MA" = "0" ]; then NEN="" grep -q '"dung_thu_chay_nen": true' "$DICH/work/_kiem_tra.json" 2>/dev/null && NEN=1 bao "CÀI XONG – ĐẠT ($PHUT phút)" "${NEN:+dung thu chay nen}" 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." if [ -n "$NEN" ]; then echo echo "Máy anh chị ít nhân nên bản dựng MẪU đang chạy nền để đối chiếu." echo "Không cần chờ nó, và nó không ảnh hưởng gì tới video của anh chị." echo "Video thật sẽ dựng lâu hơn máy khác — cứ thả vào rồi để đó." fi 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 }