Skip to content

PlusMagi's Blog By Pitt Phunsanit

Plus emotional magic to the knowledge of logic.

  • About’s Pitt
Close Button

เปลี่ยนวิดีโอประชุมเป็นโน้ต Obsidian อัตโนมัติด้วย Whisper.cpp (Metal Stack) และ Pythonเปลี่ยนวิดีโอประชุมเป็นโน้ต Obsidian อัตโนมัติด้วย Whisper.cpp (Metal Stack) และ Python

2023-01-222023-01-22| phunsanitphunsanit| 0 Comment | 07:00

เคยไหม? ประชุมเสร็จแล้วต้องมานั่งเปิดวิดีโอย้อนหลังเพื่อสรุปรายงานการประชุม (Minutes of Meeting) วันนี้เราจะมาสร้าง Automated Transcription Pipeline บนเครื่อง Mac ที่จะคอย “เฝ้า” โฟลเดอร์เก็บไฟล์ประชุม เมื่อมีคลิปใหม่เข้ามา ระบบจะดึงเสียงไปแปลงเป็นข้อความภาษาไทยให้อัตโนมัติ แยกย่อยให้ทุก ๆ 5 นาที แล้วส่งตรงเข้า Obsidian ทันที!


🛠️ โครงสร้างระบบ (Directory Setup)

เพื่อให้ระบบทำงานได้ถูกต้อง Script นี้จะอ้างอิง Path บน Mac ของคุณดังนี้ครับ

  • 📂 โฟลเดอร์เก็บคลิปประชุม: /Users/common/Meetings (ที่เก็บไฟล์ .mp4, .mov, .mkv)
  • 📂 โฟลเดอร์เก็บ AI Model: /Users/common/models/ggml-large-v3.bin (Model ภาษาไทยความแม่นยำสูง)
  • 📂 โฟลเดอร์ปลายทาง Obsidian: /Users/common/Obsidian (โฟลเดอร์สำหรับเก็บไฟล์ .md เพื่อเปิดอ่านใน Obsidian)

💡 เจาะลึกการทำงานของ Script

Script นี้ถูกออกแบบมาให้รันแบบ Persistent Loop (ทำงานตลอดเวลา) โดยมี Logic สำคัญดังนี้

  1. Batch Scan (กวาดรวบจบในรอบเดียว): เมื่อถูกเรียกใช้งาน Script จะเข้าไป Scan หาไฟล์วิดีโอทั้งหมดในโฟลเดอร์แหล่งข้อมูลแบบ Recursive (ค้นหาลงไปในโฟลเดอร์ย่อยด้วย) เพื่อนำมาจัดคิวประมวลผลเป็นชุด ๆ จนครบทั้งหมดในคราวเดียว ก่อนจะทำการปิดโปรแกรมลงอย่างปลอดภัยเพื่อให้ระบบสามารถ Sleep หรือ Shutdown เครื่องต่อได้ทันที
  2. Smart Skip (ทำเฉพาะไฟล์ใหม่): ระบบจะเช็กก่อนว่าวิดีโอนั้น ๆ เคยถูกถอดความหรือยัง ถ้าตรวจพบไฟล์คู่หู _mom.md และ _summaries.md ใน Obsidian แล้ว มันจะข้ามทันที ไม่ทำซ้ำให้เปลืองแรงเครื่อง
  3. High-Performance Audio Extraction & Transcription: * ใช้ ffmpeg แปลงเสียงจากวิดีโอให้เป็นคลื่นเสียงแบบ 16kHz Mono PCM 16-bit ซึ่งเป็น Spec ที่ดีที่สุดสำหรับ Whisper
    • ส่งต่อให้ whisper-cli ทำงานร่วมกับ Apple Silicon (Metal Stack) ดึงพลังของ GPU บน Mac มาประมวลผลภาษาไทยอย่างรวดเร็ว
  4. 5-Minute Time Block Grouping: แทนที่จะพ่นข้อความเป็นก้อนยาว ๆ อ่านยาก Script มี RegEx ช่วยดักจับ Timecode แล้วหั่นเนื้อหาแยกเป็นหัวข้อ ### minute 00:00, ### minute 05:00 ทำให้คุณกด Jump ย้อนไปฟังในวิดีโอจริงได้ง่ายมาก
  5. Obsidian Double-File Output: ระบบจะสร้างไฟล์ให้ 2 ไฟล์เสมอ:
    • [ชื่อไฟล์]_mom.md: เก็บ Metadata และ Raw Transcription แยกตามเวลา (เป็นก้อนความรู้หลักที่ห้ามแก้ไข)
    • [ชื่อไฟล์]_summaries.md: ไฟล์ว่างที่ลิงก์กลับไปหาไฟล์หลัก เพื่อให้คุณเขียนสรุปประเด็นสำคัญ หรือจดโน้ตเพิ่มเติมได้โดยไม่กระทบกับตัวถอดความเดิม

batch_transcribe_whisper_cpp.py

import os
import sys
import time
import subprocess
import warnings
import re
import argparse
import threading
from queue import Queue, Empty
from datetime import datetime
from pathlib import Path

# Suppress warnings noise
warnings.filterwarnings("ignore", category=RuntimeWarning)
warnings.filterwarnings("ignore", message=".*NotOpenSSLWarning.*")

# ==========================================
# CONFIGURATION
# ==========================================
SOURCE_DIR = Path("/Users/common/Meetings").resolve()
OBSIDIAN_DIR = Path("/Users/common/Obsidian").resolve()
USER_GROUP = "staff"

# Whisper.cpp Settings
WHISPER_LANGUAGE = "th"
WHISPER_MODEL_PATH = "/Users/common/models/ggml-large-v3.bin"
WHISPER_CLI_PATH = "whisper-cli"

# ค่ากำหนดความเสถียร
REPETITION_THRESHOLD = 5
SILENCE_THRESHOLD_SEC = 30
WHISPER_TIMEOUT_SEC = 45

# คำหลอนยอดฮิตที่จะสั่งให้ข้ามเดดแอร์ (กรณีไม่ได้ Guide เวลามา)
HALLUCINATION_KEYWORDS = ["โปรดติดตามตอนต่อไป", "โปรดติดตามต่อไป", "ขอบคุณครับ", "ขอบคุณสำหรับการรับชม", "thank you"]

def get_readable_file_size(file_path):
	try:
		size_bytes = os.path.getsize(file_path)
		for unit in ['Bytes', 'KB', 'MB', 'GB']:
			if size_bytes < 1024.0:
				return f"{size_bytes:.2f} {unit}"
			size_bytes /= 1024.0
	except Exception:
		return "Unknown"

def get_video_duration_ffprobe(file_path):
	try:
		cmd = [
			"ffprobe", "-v", "error", "-show_entries", "format=duration",
			"-of", "default=noprint_wrappers=1:nocikey=1", str(file_path)
		]
		result = subprocess.run(cmd, capture_output=True, text=True, check=True)
		return float(result.stdout.strip())
	except Exception:
		return 0.0

def format_duration_hh_mm_ssss(duration_sec):
	hours = int(duration_sec // 3600)
	minutes = int((duration_sec % 3600) // 60)
	seconds = duration_sec % 60
	sec_str = f"{seconds:07.4f}".replace('.', ':')
	return f"{hours:02d}:{minutes:02d}:{sec_str}"

def format_seconds_to_hh_mm_ss(seconds_int):
	h = int(seconds_int // 3600)
	m = int((seconds_int % 3600) // 60)
	s = int(seconds_int % 60)
	return f"{h:02d}:{m:02d}:{s:02d}"

def format_seconds_to_hh_mm_ss_ms(seconds_float):
	total_sec = int(seconds_float)
	ms = int((seconds_float - total_sec) * 1000)
	h = total_sec // 3600
	m = (total_sec % 3600) // 60
	s = total_sec % 60
	return f"{h:02d}:{m:02d}:{s:02d}.{ms:03d}"

def clean_hallucination_inline(text):
	words = text.split()
	if not words:
		return text
	match_pairs = re.findall(r'(.+?)(?:\s+\1){4,}', text)
	if match_pairs:
		for repeated_word in match_pairs:
			repeated_word_clean = repeated_word.strip()
			text = re.sub(rf'({re.escape(repeated_word_clean)})(?:\s+\1)+', r'\1', text)
	return text.strip()

def parse_start_time_to_seconds(time_arg):
	if not time_arg:
		return 0
	time_arg = str(time_arg).strip()

	if ":" in time_arg:
		parts = time_arg.split(":")
		if len(parts) == 2:
			hours = int(parts[0])
			minutes = int(parts[1])
			return (hours * 3600) + (minutes * 60)
		elif len(parts) == 3:
			return (int(parts[0]) * 3600) + (int(parts[1]) * 60) + int(parts[2])
	else:
		try:
			return int(time_arg) * 60
		except ValueError:
			return 0

def enqueue_output(out, queue):
	for line in iter(out.readline, ''):
		queue.put(line)
	out.close()

def flush_block_to_file(file_path, interval_idx, text_list):
	"""ฟังก์ชันช่วยเขียนบล็อก 5 นาทีที่เสร็จสมบูรณ์ลงไฟล์ Obsidian ทันที"""
	if not text_list:
		return
	block_start_sec = interval_idx * 300
	block_end_sec = block_start_sec + 300
	time_tag = f"[{format_seconds_to_hh_mm_ss(block_start_sec)}] - [{format_seconds_to_hh_mm_ss(block_end_sec)}]"

	combined_chunk = " ".join(text_list)
	with open(file_path, "a", encoding="utf-8") as f_append:
		f_append.write(f"### 🕒 {time_tag}\n{combined_chunk}\n\n")
		f_append.flush()

# ==========================================
# MAIN EXECUTION
# ==========================================
if __name__ == "__main__":
	parser = argparse.ArgumentParser(description="PTT Meeting Minutes Transcriber Suite")
	parser.add_argument("-f", "--file", default=None, help="ชื่อไฟล์วิดีโอบางส่วนเพื่อเจาะจงรันเดี่ยว")
	parser.add_argument("-t", "--time", default=None, help="เวลาเริ่มต้นที่จะฟังต่อ")
	args = parser.parse_args()

	print("🚀 Starting Persistent Video Processing Pipeline...")

	video_files = []
	is_partial_mode = False
	global_offset_seconds = parse_start_time_to_seconds(args.time)

	if args.file:
		print(f"[Target Mode] Searching for file containing: '{args.file}'")
		found_file = next(SOURCE_DIR.rglob(f"*{args.file}*"), None)
		if found_file and found_file.suffix.lower() in (".avi", ".mkv", ".mov", ".mp4"):
			video_files.append(found_file.resolve())
			if global_offset_seconds > 0:
				is_partial_mode = True
				print(f"[Voice Seeking Mode] Aligned target timeline offset: {format_seconds_to_hh_mm_ss(global_offset_seconds)} ({global_offset_seconds} sec)")
		else:
			print(f"❌ ไม่พบไฟล์วิดีโอที่ระบุด้วยคำค้น '{args.file}' ใน {SOURCE_DIR}")
			sys.exit(1)
	else:
		print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Scanning source directory (Batch Mode)...")
		video_files = sorted([f.resolve() for f in SOURCE_DIR.rglob("*") if f.suffix.lower() in (".avi", ".mkv", ".mov", ".mp4")])

	print(f"Total video files to check: {len(video_files)}")

	for video_path in video_files:
		relative_dir = video_path.parent.relative_to(SOURCE_DIR)
		target_folder = OBSIDIAN_DIR / relative_dir
		target_folder.mkdir(parents=True, exist_ok=True)

		video_stem = video_path.stem
		output_path_md = target_folder / f"{video_stem}_mom.md"
		output_path_summaries = target_folder / f"{video_stem}_summaries.md"

		if not is_partial_mode and output_path_md.exists() and output_path_summaries.exists():
			print(f"[SKIP] Already processed in {relative_dir if str(relative_dir) != '.' else 'root'}: {video_path.name}")
			continue

		start_process_time_raw = time.time()
		raw_duration_sec = get_video_duration_ffprobe(video_path)
		video_duration_str = format_duration_hh_mm_ssss(raw_duration_sec)

		now = datetime.now()
		start_process_time = now.strftime("%Y-%m-%d %H:%M:%S")
		sec_with_micro = f"{now.second}.{now.microsecond:04d}".replace('.', ':')
		formatted_meta_date = f"{now.strftime('%Y-%m')}-{now.strftime('%H:%M')}:{sec_with_micro}"
		file_size_str = get_readable_file_size(video_path)

		metadata_block = f'''> [!info] **📋 Session Metadata & Attendance**
> - 📅 **Original File Date:** `{formatted_meta_date}`
> - 🎬 **Reference Video Path:** `{video_path}`
> - 📦 **Video File Size:** `{file_size_str}`
> - ⏳ **Total Video Duration:** `{video_duration_str}`
> - ⚙️ **AI Processing Engine:** `whisper.cpp + Safe Block Flush Strategy`
> - ⏱️ **Transcription Start Time:** `{start_process_time}`'''

		print(f"\n==================================================")
		print(f"[Processing] Starting execution pipeline for: {video_path.name}")
		print(f"==================================================")

		# วางโครงหัวไฟล์ก่อนเริ่มสตรีมข้อมูล
		if not is_partial_mode or not output_path_md.exists():
			with open(output_path_md, "w", encoding="utf-8") as f_init:
				f_init.write(f"# 📂 Transcribed Knowledge Base: {video_path.name}\n\n{metadata_block}\n\n## 🕒 Timeline Log (สเปกช่วงละ 5 นาทีสากล)\n\n")
			with open(output_path_summaries, "w", encoding="utf-8") as f_sum:
				f_sum.write(f"# 📝 Additional Summaries & Personal Notes: {video_path.name}\n\n> [!info] **🔗 Linked Assets**\n> - 📑 **Primary MoM Note:** [[{output_path_md.name}]]\n> - 🎬 **Source Video:** `{video_path.name}`\n\n---\n\n## ✒️ Custom Notes & Takeaways\n")

		current_seek_offset = global_offset_seconds
		timestamp_regex = re.compile(r"^\[(\d{2}):(\d{2}):(\d{2})\.(\d{3})\s+-->\s+(\d{2}):(\d{2}):(\d{2})\.(\d{3})\](.*)")

		is_first_loop_segment = True
		last_sentence_end_total_sec = global_offset_seconds

		# โครงสร้างหน่วยความจำสำหรับบัฟเฟอร์ข้อมูลช่วงละ 5 นาที
		active_interval_block = -1
		buffered_text_list = []

		while True:
			if raw_duration_sec > 0 and current_seek_offset >= int(raw_duration_sec):
				break

			print(f"\n--- [Loop Segment] Scanning from point: {format_seconds_to_hh_mm_ss(current_seek_offset)} ---")
			temp_wav_path = target_folder / f"current_processing_audio_{int(time.time())}.wav"

			ffmpeg_cmd = ["ffmpeg", "-y", "-ss", format_seconds_to_hh_mm_ss(current_seek_offset), "-i", str(video_path), "-ar", "16000", "-ac", "1", "-c:a", "pcm_s16le", str(temp_wav_path)]

			try:
				subprocess.run(ffmpeg_cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
			except Exception:
				print("❌ Audio extraction failed")
				break

			whisper_cmd = [WHISPER_CLI_PATH, "-m", str(WHISPER_MODEL_PATH), "-l", str(WHISPER_LANGUAGE), "-f", str(temp_wav_path)]
			process = subprocess.Popen(whisper_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1)

			q = Queue()
			t = threading.Thread(target=enqueue_output, args=(process.stdout, q))
			t.daemon = True
			t.start()

			last_activity_time = time.time()
			segment_has_real_speech = False
			highest_segment_end_sec = 0
			previous_line_text = ""
			consecutive_repetition_count = 0
			should_skip_due_to_hallucination = False

			while True:
				try:
					line = q.get(timeout=1.0)
				except Empty:
					if process.poll() is not None:
						break
					if time.time() - last_activity_time > WHISPER_TIMEOUT_SEC:
						print(f"⚠️ [Timeout] Whisper frozen in silence for {WHISPER_TIMEOUT_SEC}s. Auto-resuming...")
						process.terminate()
						process.wait()
						break
					continue

				clean_line = line.strip()
				match = timestamp_regex.match(clean_line)
				if match:
					last_activity_time = time.time()

					start_sec_raw = (int(match.group(1)) * 3600) + (int(match.group(2)) * 60) + int(match.group(3)) + (int(match.group(4)) / 1000.0)
					end_sec_raw = (int(match.group(5)) * 3600) + (int(match.group(6)) * 60) + int(match.group(7)) + (int(match.group(8)) / 1000.0)

					current_text = match.group(9).strip()
					current_text_cleaned = clean_hallucination_inline(current_text)

					is_hallucination = any(kw in current_text_cleaned for kw in HALLUCINATION_KEYWORDS)
					if is_hallucination and not (is_partial_mode and is_first_loop_segment):
						print(f"🛑 [DeadAir Detector] Hallucination token caught ('{current_text_cleaned}'). Skipping ahead safely...")
						should_skip_due_to_hallucination = True
						process.terminate()
						process.wait()
						break

					if current_text_cleaned == previous_line_text and current_text_cleaned != "":
						consecutive_repetition_count += 1
					else:
						previous_line_text = current_text_cleaned
						consecutive_repetition_count = 0

					if consecutive_repetition_count >= REPETITION_THRESHOLD:
						continue

					segment_has_real_speech = True
					highest_segment_end_sec = max(highest_segment_end_sec, end_sec_raw)

					sync_start_sec = start_sec_raw + current_seek_offset
					sync_end_sec = end_sec_raw + current_seek_offset

					# คำนวณบล็อกเวลาหลัก (กล่องละ 5 นาที)
					interval_block = int(sync_start_sec // 300)

					# [กลไก Auto-Flush] หากข้อความวิ่งทะลุข้ามกล่อง 5 นาทีเดิม ให้ทำการ Flush ของเก่าลงแผ่นดิสก์ทันที
					if active_interval_block != -1 and interval_block != active_interval_block:
						flush_block_to_file(output_path_md, active_interval_block, buffered_text_list)
						buffered_text_list = []

					active_interval_block = interval_block

					# ตรวจหา Gap ช่วงเงียบภายในบล็อก
					if sync_start_sec - last_sentence_end_total_sec >= SILENCE_THRESHOLD_SEC:
						silence_notice = f"*[ไม่มีเสียง ช่วง {format_seconds_to_hh_mm_ss(last_sentence_end_total_sec)} - {format_seconds_to_hh_mm_ss(sync_start_sec)}]*"
						buffered_text_list.append(silence_notice)

					if current_text_cleaned:
						buffered_text_list.append(current_text_cleaned)

					last_sentence_end_total_sec = sync_end_sec
					print(f"Buffered -> [{format_seconds_to_hh_mm_ss(sync_start_sec)}] {current_text_cleaned}")

			if temp_wav_path.exists():
				os.remove(temp_wav_path)

			is_first_loop_segment = False

			# ป้องกันข้อมูลค้างกรณีลูปสะดุดขาดตอน: สั่งเขียนบล็อกปัจจุบันเก็บลงดิสก์ก่อนปิดเซกเมนต์ย่อย
			if buffered_text_list and should_skip_due_to_hallucination:
				flush_block_to_file(output_path_md, active_interval_block, buffered_text_list)
				buffered_text_list = []

			if should_skip_due_to_hallucination:
				current_seek_offset += 30
			elif segment_has_real_speech and highest_segment_end_sec > 0:
				current_seek_offset += int(highest_segment_end_sec) + 1
			else:
				current_seek_offset += 60

			if process.returncode == 0 and not segment_has_real_speech and not should_skip_due_to_hallucination:
				break

		# เก็บตก Flush บล็อกเวลาสุดท้ายที่ค้างอยู่ในความจำหลัก
		if buffered_text_list:
			flush_block_to_file(output_path_md, active_interval_block, buffered_text_list)

		# ตรวจสอบความเงียบยาวปิดท้ายคลิป
		if raw_duration_sec > 0 and raw_duration_sec - last_sentence_end_total_sec >= SILENCE_THRESHOLD_SEC:
			with open(output_path_md, "a", encoding="utf-8") as f_append:
				f_append.write(f"### 🕒 Final Boundary\n*[ไม่มีเสียง ช่วง {format_seconds_to_hh_mm_ss(last_sentence_end_total_sec)} - {format_seconds_to_hh_mm_ss(int(raw_duration_sec))}]*\n\n")

		# เขียนบันทึกปิดท้าย Pipeline
		with open(output_path_md, "a", encoding="utf-8") as f_append:
			f_append.write(f"---\n🏁 **Processing End Time:** `{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}`\nStatus: Successfully transcribed\n")

		elapsed_total_min = (time.time() - start_process_time_raw) / 60
		print(f"\n Pipeline finalized cleanly. (Execution time: {elapsed_total_min:.2f} mins)")
		print("=" * 60)

	sys.exit(0)

⏱️ เคล็ดลับสำคัญ: ป้องกัน Mac หลับด้วย caffeinate

เนื่องจากคลิปการประชุมมักจะมีความยาว และการประมวลผล AI ขนาดใหญ่ (Large-v3 Model) ต้องใช้เวลา หากปล่อย Mac ทิ้งไว้ หน้าจออาจจะดับหรือเครื่องเข้าสู่ Sleep Mode ทำให้กระบวนการทั้งหมดหยุดชะงัก

เราจึงต้องใช้คำสั่ง caffeinate ซึ่งเป็น Command Line พื้นฐานของ macOS ในการสั่งให้เครื่องตื่นตัวตลอดเวลาตราบใดที่ Script ยังทำงานอยู่

วิธีการรัน Script ร่วมกับ caffeinate

เปิด Terminal แล้วใช้คำสั่งนี้ในการเริ่มระบบ
caffeinate -i python3 batch_transcribe_whisper_cpp.py && sudo shutdown -h now
คำสั่งนี้สามารถแยกโครงสร้างออกเป็น 3 ส่วนหลัก ที่ทำงานร่วมกันดังนี้ครับ

คำสั่งนี้สามารถแยกโครงสร้างออกเป็น 3 ส่วนหลัก ที่ทำงานร่วมกันดังนี้ครับ:

1. caffeinate -i python3 batch_transcribe_whisper_cpp.py (ตื่นไว้จนกว่าจะเสร็จ)

  • caffeinate: เป็นคำสั่ง built-in ของ macOS เปรียบเสมือนการป้อนกาแฟให้ Mac เพื่อสั่งห้ามไม่ให้เครื่องแอบหลับ (Sleep) ระหว่างที่ทำงานหนัก
  • -i: ย่อมาจาก Idle sleep prevention เป็นการเจาะจงว่า “ตราบใดที่โปรแกรมที่พ่วงท้ายยังรันอยู่ ห้ามเครื่องหลับเด็ดขาด” แม้ว่าหน้าจอจะดับไปแล้วก็ตาม
  • python3 batch_transcribe_whisper_cpp.py: คือโปรแกรม Python ที่ทำหน้าที่แปลงไฟล์เสียงของคุณ

สรุปส่วนที่ 1: Mac จะตื่นตัวและจ่ายไฟเต็มกำลังให้โปรแกรม Whisper.cpp แปลงไฟล์วิดีโอจนกว่าจะเสร็จสิ้นทุกไฟล์

2. && (ตัวเชื่อมเงื่อนไข “ถ้าสำเร็จ…ให้ทำต่อ”)

  • ในทาง Command Line เครื่องหมาย && คือการเชื่อมคำสั่งแบบมีเงื่อนไข หมายความว่า “ให้คำสั่งแรก (ส่วนที่ 1) ทำงานให้เสร็จสมบูรณ์และไม่มีฟ้อง Error ก่อน ถึงจะยอมให้ทำคำสั่งถัดไป (ส่วนที่ 3)”
  • หากโปรแกรม Python เกิด Crash หรือคุณกด Ctrl + C เพื่อสั่งหยุดกลางคัน ระบบจะไม่ทำคำสั่งปิดเครื่องหลังจากนั้น เพื่อความปลอดภัยของข้อมูลครับ

3. sudo shutdown -h now (ปิดเครื่องทันที)

  • sudo: ย่อมาจาก SuperUser DO เป็นการขอสิทธิ์ผู้ดูแลระบบ (Admin) เนื่องจากคำสั่งปิดเครื่องเป็นคำสั่งระดับโครงสร้างระบบ
  • shutdown: คำสั่งสั่งปิดการทำงานของระบบปฏิบัติการ
  • -h: ย่อมาจาก Halt หมายถึงให้ตัดการทำงานของฮาร์ดแวร์และดับไฟเครื่องทั้งหมด (ปิดเครื่องสนิท ไม่ใช่แค่รีสตาร์ท)
  • now: สั่งให้ทำทันที ณ วินาทีนั้น โดยไม่ต้องตั้งเวลานับถอยหลัง

🔄 ลำดับเหตุการณ์จริงเมื่อคุณกดรันคำสั่งนี้:

  1. Terminal จะถามรหัสผ่านเครื่อง Mac ทันที (จาก sudo): คุณต้องพิมพ์รหัสผ่านของเครื่องแล้วกด Enter (ตอนพิมพ์รหัสจะไม่ขึ้นตัวอักษรใด ๆ เป็นเรื่องปกติของความปลอดภัยบน Mac)
  2. Python เริ่มทำงาน สแกนและถอดความเสียงการประชุมไปเรื่อย ๆ โดยมี caffeinate คอยค้ำไม่ให้เครื่องหลับ
  3. เมื่อ Python แปลงไฟล์จนหมดโฟลเดอร์ และจบโปรแกรมลงอย่างสมบูรณ์
  4. เงื่อนไข && ทำงาน ส่งไม้ต่อให้ sudo shutdown -h now
  5. Mac ของคุณจะทำกระบวนการปิดเครื่อง (Shutdown) ตัวเองโดยอัตโนมัติทันทีครับ เหมาะมากสำหรับการเปิดรันทิ้งไว้ก่อนนอน หรือก่อนเลิกงานครับ

อ่านเพิ่มเติม

  • การติดตั้ง Whisper ฟังเสียง
  • Whisper: เสียงกระซิบจาก video
  • Obsidian: จด note ง่าย ๆ
Read MoreRead More

Posts pagination

ก่อนหน้า 1 … 244 245 246 … 1,209 ถัดไป

Projects

  • Statement Columns Mapping Helper
  • PlusMagi Site Search
  • jQuery Plus Repeater

Recent Posts

  • Linux: ตรวจสอบความถูกต้องของระบบด้วยสปีดขั้นสุด 🚀
  • Tarball: เป็น version control ?
  • SecDevOps: เปลี่ยนความปลอดภัยให้เป็นเนื้อเดียวกับ Code และ Operation
  • WordPress: ดูว่า post ทุก ๆ วันเสาร์ใช่มั๋ย
  • เทคนิคตั้งค่า umask 002 ให้กับทุก User บน Mac อัตโนมัติผ่าน Terminal (Batch Script)

Archives

Categories

  • AI (26)
  • Businesses (28)
  • Design (56)
    • UX/UI (22)
  • Histories (16)
  • Life (120)
    • Books (51)
    • Cartoon (6)
    • Movies (11)
    • Sci-Fi (20)
    • Tips and Tricks (26)
    • พุทธ (7)
  • Network (147)
    • Apache HTTP Server (17)
    • IOT (1)
    • Nginx (31)
    • Stalwart (6)
  • Operating Systems (282)
    • Unix-like (207)
      • Android (16)
        • F-Droid (5)
      • Homebrew (21)
      • iPhone (8)
      • Linux (120)
      • macOS (100)
        • OrbStack (11)
      • Oh My ZSH (4)
      • Shell Script (42)
      • SSH (11)
    • Windows (121)
      • PowerShell (26)
      • WSL (25)
  • Programming (822)
    • .NET (20)
      • .NET Core EF (6)
      • C# (18)
    • API (39)
      • REST (8)
      • Swagger (7)
    • Database (189)
      • DBeaver (3)
      • MariaDB (32)
      • MySql (69)
      • Oracle Database (6)
        • 10g (3)
      • PostgreSQL (9)
      • RDBMS (2)
      • SQL Server (85)
        • SSMS (8)
        • T-SQL (26)
      • SQLite (1)
    • PowerBuilder (11)
    • Python (6)
    • Rust (5)
    • System Analyst (SA) (37)
    • Testing (27)
      • Automated Testing (13)
        • Playwright (6)
    • UML (7)
    • Web (447)
      • Backend (303)
        • Golang (2)
        • Java (49)
          • Spring Boot (14)
        • Node.js (3)
        • PHP (254)
          • Laravel (50)
          • Yii (5)
      • Frontend (136)
        • CSS (18)
          • Tailwind CSS (5)
        • JavaScript (123)
          • Angular (3)
          • jQuery (62)
          • Tabulator (19)
          • Vue.js (5)
      • WordPress (26)
  • Programs (104)
    • Excel (12)
  • SecDevOps (97)
    • CI/CD (10)
    • Docker (41)
    • GIT (34)
  • Security (114)
  • Uncategorized (6)
  • กฎหมาย (9)

Sirat WordPress Theme By VWThemes

Scroll Up
Exit mobile version