分類
程序 软件

將 flightradar24 航班位置記錄轉換為 gpx 文檔

前幾天坐飛機,一覺醒來看到壯麗的雪山。拍了不少照片,但是並不清楚雪山的大概位置。想要給飛機上拍攝的照片加上地理位置,需要 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&timestamp=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'")

分類
软件

使用 Vegasaur 導出 Vegas Pro 視頻項目中的字幕

我想要將 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 格式字幕樣式的嘗試。

分類
說說

250504

早上 Emanon 像往常一樣做了煎餅。吃晚飯去後山轉了一圈,這座小山很少有人爬。斑腿泛樹蛙趴在人工修建的蓄水池沿上,它們和它們的後代只有很小的機率離開這個人工水池——它的四壁又高又直,只有一個稍矮的小豁口。水池四周還有金屬欄杆,在欄杆下面吊着的蛹過了三天了還是沒有任何動靜。小路邊的同一顆荔枝樹上,總能看到龍眼雞。今年的應該是一個荔枝的大年,荔枝目前長得都挺好,已經有大拇指大小。今天還有很多豹尺蛾成蝶,走在路上很容易驚起一片。比較有趣的是兩次遇到老鼠,之前從來沒遇到過。可惜沒能留下照片。閒聊到 rat 和 mouse 的區別,但是我們兩個都不知道。回來問 AI,AI 說 rat 個體較大,尾巴較長,行為上更具攻擊性,而且多與人類共生。而 mouse 一般生活在草地和森林。所以今天見到的應該是兩隻 mice。走到下山口的時候,看到有人在登記上山。一位女性正要填表格,她問道這個山叫什麼名字,然後她看到了表格上「行山」字樣,就自言自語說原來是行山啊。行山這個粵語在我的輸入法里也不是一個詞彙。

今天是五四青年節。祝願青年朋友們能有好工作、有好食物、有好空氣、有好心情。

分類
說說

250412

前幾天吃帶魚,想起賣大梯子的東北演員小二顧本彬。願他在天堂過的快樂。

分類
Linux

AlmaLinux 9 with Xfce for daily use

隨着年紀的增長,不可避免的越來越保守。所以打算試試保固期更長的 AlmaLinux 來替換隔兩年就要升級的 Fedora。

安裝 AlmaLinux 9 與 Windows 10 雙系統

製作安裝盤

curl -O https://repo.almalinux.org/almalinux/RPM-GPG-KEY-AlmaLinux-9
gpg --import RPM-GPG-KEY-AlmaLinux-9
curl -O https://repo.almalinux.org/almalinux/9/isos/x86_64/CHECKSUM
gpg --verify CHECKSUM
sha256sum AlmaLinux-9.4-x86_64-Live-XFCE.iso

sudo fdisk -l
sudo dd if=./AlmaLinux-9.4-x86_64-Live-XFCE.iso of=/dev/sdb status=progress conv=fsync bs=4M

安装 AlmaLinux 9

此段從略。大概流程是先到 Windows 中使用磁盤管理工具釋放出安裝 AlmaLinux 的磁盤空間。建議 100 GB 以上吧。然後重啟電腦進入 BIOS (一般是在屏幕出現品牌 Logo 時按 F2 或 F10) 將「從優盤啟動」的優先級提到「從硬盤啟動」前。然後插入優盤啟動就進入 AlmaLinux 9 了,選擇其中的安裝到硬盤即可。

AlmaLinux 9 with Xfce 小撇步

Flatpak

使用 Flatpak 來安裝常用的軟件可以避免依賴出現問題,而且軟件更新速度也更快。下面是我推薦的一些軟件:

Delta Chat	chat.delta.desktop 基於 Email 的加密聊天
Bilal Elmoussaoui	com.belmoussaoui.Decoder 二維碼生產與掃描
Flameshot	org.flameshot.Flameshot 截圖(在鍵盤快捷方式里設置 flatpak run org.flameshot.Flameshot gui 可以一鍵截圖)
Kovid Goyal	com.calibre_ebook.calibre 電子書管理與轉換
Vlad Krupinskii	io.github.mrvladus.List  Todo 列表
The GoldenDict-ng Community	io.github.xiaoyifang.goldendict_ng 字典
Michal Kosciesza	net.mkiol.Jupii 投屏
The GIMP team	org.gimp.GIMP  圖片修改
digiKam Team	org.kde.digikam 照片管理
KDE	org.kde.kcalc 計算器
KDE	org.kde.kdenlive 視頻剪輯
The qBittorrent Project	org.qbittorrent.qBittorrent 種子下載
VideoLAN et al.	org.videolan.VLC 音視頻播放器

AppImage

AppImage 類軟件很像 Windows 里的綠色軟件,無需安裝(有時需要在文件屬性里勾上「可執行」),雙擊既可運行。

  • Beekeeper-Studio 數據庫客戶端
  • IPTVnator 在線電視播放器
  • LibreCAD CAD
  • Simple mind map 開源腦圖(節目僅支持簡體中文)

其他軟件

Firefox ESR 作為我的主力瀏覽器一直是通過到官網直接下載,然後解壓即可運行的。值得一提的是,截至 2024 年 11 月,Firefox 官網以及賬號書籤同步系統都未被牆。Firefox 的多賬戶玩法請參考 Profile Manager - Create, remove or switch Firefox profiles. (firefox -P)

有時需要 Google Chrome 來救急。安裝方法如下:

#新建文件
sudo nano /etc/yum.repos.d/google-chrome.repo
#內容如下
[google-chrome]
name=google-chrome
baseurl=https://dl.google.com/linux/chrome/rpm/stable/x86_64
enabled=1
gpgcheck=1
gpgkey=https://dl.google.com/linux/linux_signing_key.pub

#更新並安裝 Chrome
sudo dnf update
sudo dnf install google-chrome-stable

#查看 Chrome 版本
google-chrome --version

Bugs

偶爾地,從 Windows 10 關機後再啟動 AlmaLinux,Wi-Fi會停止工作。我也不知道如何永久解決此問題,但是可以通過再次進入 Windows 10,從右下角快捷方式里禁用 Wi-Fi,再打開 AlmaLinux,Wi-Fi 就正常工作了。這個 Bug 並不常出現。

分類
其它

佚名詩三首

常夢回唐景
夢完眼晴晴
夢返夢去到新寧
夢妻夢兒夢村井
夢未清
再夢仍美境
每夢尚誣心火盛
幾時夢得到家庭


稀里糊塗快一年
異地謀生萬事難
醉來明月應笑我
只添白髮未添錢


無權無勢無老婆
無兒無女無負擔
無憂無慮無波瀾
無牽無掛無羈絆
分類
說說

240930

物質的壟斷使人貧窮,權力的壟斷使人懦弱,信息的壟斷使人愚蠢,三個一起壟斷使人感恩。

這段話在微信朋友圈發送後,只有自己能看到。