Emanon 旅行時拍得視頻,今天發現被我不小心刪除了(其實是複製到的沒有複製全,遺漏了一部分)。Emanon 非常生氣,因為本月已經發生過一次視頻數據遺失,那是去年中旬的視頻,我也不知道為什麼會消失。雖然這些文件我都保存兩份,以防止硬件故障,但是兩份數據又確實都缺失了。以後要儘快按順序處理完旅行後的媒體文件,免得中斷後夜長夢多。
住宿不丟東西的訣竅--恢復原狀。離店前簡單整理下床鋪,把遙控器放回原位,關掉燈和空調,帶走產生的垃圾。然後只需回望一眼房間即可確定自己所有物品已經帶齊。
出門不落東西的訣竅--預留充足的時間。如果出門前需要在家裡做飯吃飯,就留兩個小時;如果不吃飯,就留一個小時。在這一個小時裡完成洗漱以及未完成的打包,如最後才裝包的手機充電器和電腦。只要能避免慌張出門,就能大大降低出門落東西的概率。
出門不丟手機的訣竅……這個真的很難!以我丟手機的經驗來說,發生次數最多的是遺落在公交車上,遺落在轎車上以及遺落在沙灘上(在國外旅行痛失兩部手機的慘痛記憶)。我常年穿迪卡儂的一款速幹長褲,其口袋帶拉鍊,只需輕輕一拉,手機便萬無一失,遺憾的是我經常不拉。我目前的做法就是「記得拉拉鍊」,養成放在固定可靠地方的習慣。由於遺失手機,我們在很多地方都遇到好心人幫忙,也不失為一種旅行體驗。當然我們也撿到過兩三次手機,不過我們並沒有見過失主,我們最多將其放在更顯眼的位置,希望失主能順利尋回。
既然說到遺失手機,自然就帶來遺失數據的問題,這個其實比較簡單--及時備份。從大公司到開源軟件,選擇有不少。有些軟件也有自己的備份方法,要儘早多加利用,莫等珍貴資料遺失才追悔莫及。注意:本機備份等於沒有備份,一定要保存至另一個設備或雲端才算數。手機遺失通常還會伴隨 SIM 卡的遺失,像微信這種以「安全」為由,在新設備登錄必須短信驗證就廢了。此時一個備用的聯繫方式就很必要,這裡我強烈推薦下 DeltaChat(端對端加密、提供默認服務器方便註冊且不驗證個人資料、中國可用)。最近出國試了下 eSim 發現方便不少,價格和中國移動境外包差不多,但是數據不過牆。而且可以按量付費,非常適合我們這種不刷視頻,只是看看地圖的低流量用戶。手機遺失的時候也只需在網上註銷那張卡,然後重新申領一張就行了。
安裝
sudo dnf update -y sudo dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo sudo dnf install docker-ce docker-ce-cli containerd.io #Start and Enable Docker sudo systemctl start docker #sudo systemctl enable docker #Verify Installation sudo docker run hello-world sudo usermod -aG docker YOUR_USER sudo systemctl enable docker
更換 Docker 源
時間來到 2024 年,默認的軟件源不是{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}就是Unable to find image 'hello-world:latest' locally
docker: Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.1.42:55114->54.236.113.205:443: read: connection reset by peer.,可以通過修改軟件源比如 https://docke.eu.org 來解決。
sudo nano /etc/docker/daemon.json
#內容為:
{
"registry-mirrors": ["https://docke.eu.org"]
}
#然後重啟服務
sudo systemctl restart docker
使用代理
如果更換 Docker 源也不行,還有一個簡單的辦法,就是使用代理。
sudo mkdir -p /etc/systemd/system/docker.service.d sudo nano /etc/systemd/system/docker.service.d/http-proxy.conf [Service] Environment="HTTP_PROXY=socks5://127.0.0.1:44082" Environment="HTTPS_PROXY=socks5://127.0.0.1:44082" Environment="NO_PROXY=localhost,127.0.0.1,::1" #restart docker sudo systemctl daemon-reload sudo systemctl restart docker #verify the setting systemctl show docker | grep -i proxy
postgres Docker
#file:docker-compose.yml
services:
db:
image: postgres
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes:
- ./docker-data-pg:/var/lib/postgresql/data
deploy:
resources:
limits:
cpus: ${POSTGRES_CPU_LIMIT}
memory: ${POSTGRES_MEM_LIMIT}
restart: ${POSTGRES_RESTART_POLICY}
#file:.env POSTGRES_USER=userp POSTGRES_PASSWORD=passwordp POSTGRES_DB=dbname #restart | no POSTGRES_RESTART_POLICY=no POSTGRES_CPU_LIMIT=2.0 POSTGRES_MEM_LIMIT=1024M
#啟動 docker sudo docker compose up -d
在網上看到有人提醒說,compose 文件中的資源限制(CPU、內存)會導致容器內服務重啟。推薦的做法是在容器內服務中配置硬件限制。
遷移 Docker 與數據
在舊電腦上:
#查看 docker 名字 docker ps #導出所有數據庫數據 docker exec -t <container_name> pg_dumpall -U <username> > all_databases.sql
複製 docker-compose.yml 和 all_databases.sql 到新電腦上,然後:
#啟動 docker sudo docker compose up -d #導入所有數據庫數據(方法一) sudo docker exec -i <container_name> psql -U <username> -d postgres < all_databases.sql #導入所有數據庫數據(方法二) sudo docker cp all_databases.sql <container_name>:/tmp/all_databases.sql sudo docker exec -i <container_name> psql -U <username> -d postgres -f /tmp/all_databases.sql
Docker 常用命令
#build new docker docker compose up -d --build #查看 docker 名字 docker ps #進入 docker 容器 docker exec -it DOCKER_NAME bash #If You Change environment, volumes, ports, or depends_on (data safe) docker compose up -d #If You Change the Dockerfile or build Settings docker compose up -d --build #stop and remove docker container cleanly docker compose down #remove everything even data docker compose down -v #If you've removed services or images and want to reclaim disk space docker system prune
站點升級:服務器升級到 2GB 內存。數據庫和 WordPress 放到了 Docker 里。v2fly 在 Cloudflare 的中轉下速度仍然不錯。
前幾天坐飛機,一覺醒來看到壯麗的雪山。拍了不少照片,但是並不清楚雪山的大概位置。想要給飛機上拍攝的照片加上地理位置,需要 gpx 文檔,然後用 digiKam 就可以標記照片的位置。怎麼取得航班的位置呢?有兩個方法。
第一個是 FlightAware。搜索到航班的歷史記錄如這個 ZH8960。頁面上有個「+ Google Earth」的圖標,點擊後會得到一個 kml 檔案。然後到 kml2gpx.com 即可將文檔轉換為所需要的 gpx 文檔。但是這個方法的問題在於 FlightAware 的數據會有一部分是直線,看起來就是沒有 GPS 數據,所以我最終使用的方法二。
第二個略複雜,是用 flightradar24 的數據。搜索結果頁也有 kml 檔案下載按鈕,但是需要成為會員才能使用。所以點擊旁邊的回放按鈕,來到回放頁面。然後找到包含 GPS 數據的網絡請求:按 F12 按鈕打開瀏覽器開發者工具 > 切換到「網絡」標籤 > 刷新頁面 > 在過濾請求的輸入框里填寫 flight-playback.json 即可看到所需要的請求 > 在請求上右鍵 > 選擇「保存回應為(Save Response As)即可獲得包含 GPS 數據的 json 文檔。然後使用我的這個 Python 腳本即可將 json 文檔轉換為 gpx 文檔。
######################
#file json2gpx.py
# convert json file from www.flightradar24.com to gps file.
# get the json file by open the web page: https://www.flightradar24.com/data/flights/zh8960#3a6f819a
# then save the request https://api.flightradar24.com/common/v1/flight-playback.json?flightId=3a6f819a×tamp=1747838700 as flight-playback.json
# run python json2gpx.py, and get output.gpx
######################
import json
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
#read json from file
with open('flight-playback.json', 'r') as f:
data_all = json.load(f)
data = data_all['result']['response']['data']['flight']['track']
# Create GPX root
gpx = ET.Element("gpx", version="1.1", creator="JSON-to-GPX Converter", xmlns="http://www.topografix.com/GPX/1/1")
trk = ET.SubElement(gpx, "trk")
trkseg = ET.SubElement(trk, "trkseg")
# Add track points
for point in data:
trkpt = ET.SubElement(trkseg, "trkpt", lat=str(point["latitude"]), lon=str(point["longitude"]))
ET.SubElement(trkpt, "ele").text = str(point["altitude"]["meters"])
timestamp_iso = datetime.fromtimestamp(point["timestamp"], tz=timezone.utc).isoformat().replace("+00:00", "Z")
ET.SubElement(trkpt, "time").text = timestamp_iso
# Write to GPX file
tree = ET.ElementTree(gpx)
with open("output.gpx", "wb") as f:
tree.write(f, encoding="utf-8", xml_declaration=True)
print("GPX file created as 'output.gpx'")
值得留意的是,flightradar24 僅為非會員提供一周內的免費數據。如果你要查詢的航班資料大於一周,就要付費了。
本文更新於 2025/10/26。
我想要將 Vegas Pro 中的字幕和文字導出並生成 Youtube 的 Chapter 章節。一開始我嘗試了 Tools 里的 Scripting,但是並沒有成功。後來找到 Vegasaur Toolkit 這個工具,它有 30 天免費試用,可以將軌道上的文字導出成 srt 字幕文件。然後我使用 ffmpeg 將 srt 轉換成 vtt 字幕文件,最後再通過 Python 腳本將其轉換成 Youtube 支持的章節文本文檔。
下載 Vegasaur Toolkit,並安裝。我的 Vegas Pro 版本是 18,我安裝的 Vegasaur Toolkit 版本是 3.9.5。安裝完,打開 .veg 文件,鼠標選中要導出的字幕軌道,然後點擊 View > Extensions > Vegasaur > Timeline > Text Generation Wizard.在彈出的窗口中選擇 Export text events,然後點 Next。然後選擇 Selected Tracks 和 Save to File 並設置要導出的文件名和文件格式。最後點擊 Finish 就導出了。
剩下的步驟我使用了腳本來輔助完成。我有兩條字幕軌道,章節文本基本上都位於第一條軌道,但是偶爾第而條軌道也會有,所有我還需要合併一下兩個軌道導出的字幕文件:
#vtt utils
import subprocess
import sys
from datetime import timedelta
#usage:
#srt to youtube chapters
#python main.py srt2yt 1.srt 2.srt
def add_style_to_vtt(input_file, output_file, style):
with open(input_file, 'r') as f:
lines = f.readlines()
with open(output_file, 'w') as f:
for line in lines:
if '-->' in line:
start, end = line.strip().split(' --> ')
# Check if the timecodes are less than 60 minutes
if len(start) <= 9: # Check if start time is less than or equal to 60 minutes
start = '00:' + start
if len(end) <= 9: # Check if end time is less than or equal to 60 minutes
end = '00:' + end
# Add style settings after the timecode
#line = line.strip() + ' ' + style + '\n'
line = f'{start} --> {end} {style}\n'
f.write(line)
def combine_two_vtt(input_file_1, input_file_2, output_file):
# with open(input_file_1, 'r') as f:
# lines_1 = f.readlines()
# with open(input_file_2, 'r') as f:
# lines_2 = f.readlines()
# with open(output_file, 'w') as f:
# f.writelines(lines_1 + lines_2)
vtt1_lines = parse_vtt_file(input_file_1)
vtt2_lines = parse_vtt_file(input_file_2)
lines = vtt1_lines + vtt2_lines
write_sorted_vtt(lines, output_file)
def time_to_seconds(time_str):
try:
hours, minutes, seconds = map(float, time_str.split(':'))
except:
hours = 0
minutes, seconds = map(float, time_str.split(':'))
return hours * 3600 + minutes * 60 + seconds
def seconds_to_time(seconds):
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
seconds = seconds % 60
return f'{hours:02.0f}:{minutes:02.0f}:{seconds:06.3f}'
def youtube_chapters(input_file, output_file):
with open(input_file, 'r') as f:
lines = f.readlines()
with open(output_file, 'w') as f:
last_time = '00:00'
i = 0
while i < len(lines):
if '-->' in lines[i]:
caption_list=[]
first_dot_index = lines[i].find('.')
current_time = lines[i][:first_dot_index]
#if current_time - last time < 10: then current time = last time + 10 seconds
if time_to_seconds(current_time) - time_to_seconds(last_time) < 10:
current_time = seconds_to_time(time_to_seconds(last_time) + 10)
# set the current time to the last time
last_time = current_time
caption_list.append(current_time)
while i < len(lines)-1 and lines[i+1] != '\n':
caption_list.append(lines[i+1].strip())
i += 1
if len(caption_list) == 4:
#remove the third element of list
caption_list.pop(2)
caption = ' '.join(caption_list)
f.write(caption + '\n')
i += 1
def parse_vtt_timestamp(ts):
"""Parses a VTT timestamp into a timedelta object."""
parts = ts.split(":")
if len(parts) == 3:
h, m, s = parts
elif len(parts) == 2:
h = 0
m, s = parts
else:
raise ValueError(f"Unexpected timestamp format: {ts}")
s, ms = s.split(".")
return timedelta(hours=int(h), minutes=int(m), seconds=int(s), milliseconds=int(ms))
def parse_vtt_file(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
entries = []
i = 0
while i < len(lines):
line = lines[i].strip()
if "-->" in line:
timestamp = line
start_time = parse_vtt_timestamp(timestamp.split(" --> ")[0])
content = []
i += 1
while i < len(lines) and lines[i].strip() != "":
content.append(lines[i].rstrip('\n'))
i += 1
entries.append((start_time, timestamp, content))
else:
i += 1
return entries
def write_sorted_vtt(entries, output_path):
entries.sort(key=lambda x: x[0])
with open(output_path, 'w', encoding='utf-8') as f:
f.write("WEBVTT\n\n")
for _, timestamp, content in entries:
f.write(f"{timestamp}\n")
for line in content:
f.write(f"{line}\n")
f.write("\n")
def format_youtube_time(td):
total_seconds = int(td.total_seconds())
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours:02}:{minutes:02}:{seconds:02}"
else:
return f"{minutes:02}:{seconds:02}"
def parse_vtt_for_chapters(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
entries = []
i = 0
while i < len(lines):
line = lines[i].strip()
if "-->" in line:
timestamp_line = line
start_str, end_str = timestamp_line.split(" --> ")
start = parse_vtt_timestamp(start_str.strip())
end = parse_vtt_timestamp(end_str.strip())
duration = end - start
i += 1
content = []
while i < len(lines) and lines[i].strip():
content.append(lines[i].strip())
i += 1
# Expected format: Chinese name on 1st line, English name on 3rd line
if len(content) >= 2:
chinese_name = content[0]
english_name = content[-1]
entries.append((chinese_name, english_name, start, duration))
else:
i += 1
# Deduplicate: keep only the longest-duration entry per Chinese name
chapter_dict = {}
for cname, ename, start, duration in entries:
if cname not in chapter_dict or duration > chapter_dict[cname][1]:
chapter_dict[cname] = (ename, duration, start)
# Sort by start time
sorted_chapters = sorted(
[(data[2], cname, data[0]) for cname, data in chapter_dict.items()],
key=lambda x: x[0]
)
return sorted_chapters
def write_youtube_chapters(chapters, output_path):
with open(output_path, 'w', encoding='utf-8') as f:
for start, cname, ename in chapters:
time_str = format_youtube_time(start)
f.write(f"{time_str} {cname} {ename}\n")
if __name__ == "__main__":
# choose function from command line arguments
if len(sys.argv) > 2:
# combine two vtt files
func = sys.argv[1]
if func == 'combine':
input_file_1 = sys.argv[2]
input_file_2 = sys.argv[3]
output_file = input_file_1[:-4] + '.combined.vtt'
combine_two_vtt(input_file_1, input_file_2, output_file)
elif func == 'style':
input_file = sys.argv[2]
output_file = input_file[:-4] + '.style.vtt'
style = sys.argv[3]
# style = 'position:100% align:right size:50%'
# style = 'position:0% align:left size:50%'
add_style_to_vtt(input_file, output_file, style)
elif func == 'youtube':
input_file = sys.argv[2]
output_file = input_file[:-4] + '.youtube.txt'
# youtube_chapters(input_file, output_file)
chapters = parse_vtt_for_chapters(input_file)
write_youtube_chapters(chapters, output_file)
elif func == 'srt2yt':
input_file_1 = sys.argv[2]
input_file_2 = sys.argv[3]
vtt1 = input_file_1[:-4] + '.vtt'
vtt2 = input_file_2[:-4] + '.vtt'
subprocess.run(['ffmpeg', '-i', input_file_1, vtt1])
subprocess.run(['ffmpeg', '-i', input_file_2, vtt2])
#wait for user to input anything to continue
input("Press enter to continue...")
combined_file = input_file_1[:-4] + '.combined.vtt'
combine_two_vtt(vtt1, vtt2, combined_file)
yt_file = input_file_1[:-4] + '.youtube.txt'
chapters = parse_vtt_for_chapters(combined_file)
write_youtube_chapters(chapters, yt_file)
腳本中還有一些 Youtube 支持的 vtt 格式字幕樣式的嘗試。
早上 Emanon 像往常一樣做了煎餅。吃晚飯去後山轉了一圈,這座小山很少有人爬。斑腿泛樹蛙趴在人工修建的蓄水池沿上,它們和它們的後代只有很小的機率離開這個人工水池——它的四壁又高又直,只有一個稍矮的小豁口。水池四周還有金屬欄杆,在欄杆下面吊着的蛹過了三天了還是沒有任何動靜。小路邊的同一顆荔枝樹上,總能看到龍眼雞。今年的應該是一個荔枝的大年,荔枝目前長得都挺好,已經有大拇指大小。今天還有很多豹尺蛾成蝶,走在路上很容易驚起一片。比較有趣的是兩次遇到老鼠,之前從來沒遇到過。可惜沒能留下照片。閒聊到 rat 和 mouse 的區別,但是我們兩個都不知道。回來問 AI,AI 說 rat 個體較大,尾巴較長,行為上更具攻擊性,而且多與人類共生。而 mouse 一般生活在草地和森林。所以今天見到的應該是兩隻 mice。走到下山口的時候,看到有人在登記上山。一位女性正要填表格,她問道這個山叫什麼名字,然後她看到了表格上「行山」字樣,就自言自語說原來是行山啊。行山這個粵語在我的輸入法里也不是一個詞彙。
今天是五四青年節。祝願青年朋友們能有好工作、有好食物、有好空氣、有好心情。