分類
软件

在 Alma Linux 9 和安卓 14 上使用 Fcitx5 RIME 和霧凇拼音

系統自帶的輸入法無法直接輸入類似紅耳鵯、鵲鴝、純白變色龍之類的物種名字,但是霧凇拼音有更強大的詞庫,還能糾正一些輸入時的錯誤,所以推薦使用。

Alma Linux 9 使用 Flatpak 安裝 Fcitx5

Alma Linux 9 的官方軟件源中沒有 Fcitx5,所以使用 Flatpak 安裝。flatpak install org.fcitx.Fcitx5然後安裝 RIME flatpak install fcitx.Fcitx5.Addon.Rime,最後運行輸入法flatpak run org.fcitx.Fcitx5 &,就可以在輸入法面板看到 Fcitx5 了。

雾凇拼音發佈頁面下載 full.zip 將內容解壓到 ~/.var/app/org.fcitx.Fcitx5/data/fcitx5/rime/ .如果不需要雙拼,可以在 default.yaml 配置文件中的 schema_list: 下僅保存 - schema: rime_ice 刪除其他方案。

在 Fcitx5 輸入法圖標上右鍵,選擇輸入法設置,取消選擇「Only Show Current Language」,找到「Rime」將其添加到當前輸入法列表中。在 Globle Options 選項卡中,刪除 Temporarily Toggle Input Method 的默認快捷鍵 Shift,不然一按就會離開 Rime 輸入法。再次在 Fcitx5 輸入法圖標上右鍵,選擇重新啓動,應該就能看到 Rime 輸入法來,然後就能選擇霧凇拼音 > Deploy。

自動啓動的話,由於沒有找到 ~/.local/share/flatpak/exports/share/applications/org.fcitx.Fcitx5.desktop 所以我是在 Settings > Session and Startup,Application Autustart 中添加 flatpak run org.fcitx.Fcitx5 & 條目來啓動的。

順帶一題,默認的繁簡切換快捷鍵是 Ctrl+Shift+4。候選詞翻頁鍵是 - 和 =。

Android 使用 F-Droid 安裝 Fcitx5 和霧凇拼音

首先在 F-Droid 安裝 Fcitx5(即 小企鵝輸入法)Fcitx5 (RIME Plugin) ,然後將 full.zip 中的文件解壓到 data/rime。在安卓 14 系統中,第三方文件管理器似乎無法打開 Android/data/org.fcitx.fcitx5.android 文件夾。所以你需要先將文件解壓好,比如放在 Downlaods 文件夾中,然後打開 Fcitx5 > 輸入法 > 添加輸入法 > Rime。然後點 Rime 右邊的配置按鈕 > 使用者文件夾,選擇安卓系統自帶的文件應用,先清空 data/rime 文件夹。然后點左上角的菜單,到 Downloads 文件夾找到解壓的文件,全選,複製,然後再按菜單按鈕,點 Fcitx5 > data > rime 然後貼上。在回到 Fcitx5 應用,右上角菜单 > 开发者 > 重新启动。最後隨便點擊一個輸入框,喚出鍵盤,找到鍵盤右上角的三點菜單(如果沒有三點菜單,先按下鍵盤坐上角的 >) > 霧凇拼音 > 重新部署。就完成了。如果不需要雙拼方案就修改 default.yaml 文件,僅在 - schema: 下保留 rime_ice 即可。然後可能要在霧凇拼音的菜單中再次部署 Deploy 下。

按照上述方法安裝配置完畢,字庫是沒有問題了,不過沒有聯想詞功能。如果日常使用中(如聊天時)很依賴聯想詞,可以使用 Fcitx 5 自帶的 Pinyin 輸入法。輸入法切換只需要按一下,並不複雜。參考:透過Flatpak跑Fcitx5,安裝Linux的中文輸入法

本文更新於 2026/07/16。

分類
软件

使用 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 格式字幕樣式的嘗試。

分類
Linux 软件

使用 nginx 代理 NextDNS 的 DoH 服務

免費版的 NextDNS 每個月有 300,000 次請求,個人一般是夠用的。通過 nginx 代理其 DoH 服務可以用自己的域名來使用 NextDNS 的服務。Nginx 配置文件 /etc/nginx/conf.d/MY.DOMAIN.conf 如下:

# Cache the dns query
proxy_cache_path /var/cache/nginx/dns levels=1:2 keys_zone=dns_cache:10m max_size=1g inactive=1d use_temp_path=off;

server {
    server_name my.domain;

    root /usr/share/nginx/html;

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/my.domain/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/my.domain/privkey.pem; # managed <p>by</p> Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

    http2 on;
    resolver 45.90.28.136;

    location /YOUR_DNS_QUERY_PATH {
        proxy_pass https://dns.nextdns.io/YOUR_ID/YOUR_TAG;
        proxy_set_header Host dns.nextdns.io;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache dns_cache;
        proxy_cache_valid 1d;
        proxy_cache_use_stale error timeout updating;
    }
    
    location / {
    }
}
server {
    if ($host = my.domain) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    listen 80;
    listen [::]:80;
    server_name my.domain;
    return 404; # managed by Certbot


}

Nginx 搭配 Cerbot 獲取免費證書請參考:網站用上了letsencrypt的免費ssl證書

搭建完成後可以通過 curl 來檢查服務是否正常:curl -H 'accept: application/dns-json' 'https://my.domain/YOUR_DNS_QUERY_PATH?name=ft.shaman.eu.org&type=A'

使用 personalDNSfilter 以在安卓手機上使用 DoH

使用 personalDNSfilter (去 F-droid 下載)實現的本地 VPN 來修改系統的DNS查詢,它同時還能攔截廣告。配置非常簡單,只需要在 DNS 配置欄選中禁用 DNS 服務器查找,選中文本編輯模式,並在輸入框中加入下面一行並啟用即可。

[MY.SERVER.IP]::443::DOH::https://my.domain/YOUR_DNS_QUERY_PATH

也可是使用重一點的 Rethink 來管理 DNS。加入自己的 DoH 也很簡單。選擇 Other DNS,DoH,新增。名稱隨便,Resolver URL 填:https://my.domain/YOUR_DNS_QUERY_PATH 即可。

在 Xfce 口味的 Fedora 上使用 DoH

似乎是沒辦法直接在圖形界面上進行設置。我最終是用熟悉的 dnscrypt-proxy 2 來代理。不熟悉的朋友請參考:Fedora 使用 DNSCrypt。只需要修改三處文件即可:

#file:dnscrypt-proxy.toml

#設置 server_names,默認是注釋掉的
server_names = ['mydns']

#如果在中國,可以把 bootstrap_resolvers 設置成 114
bootstrap_resolvers  = ['114.114.114.114:53', '8.8.8.8:53']

#拉到文件底部,添加這兩行
  [static.mydns]
  stamp = 'sdns://AgAAAAAAAAAABzEuMS4xLjEACW15LmRvbWFpbhQvWU9VUl9ETlNfUVVFUllfUEFUSA'

stamp 的生成請打開這個網頁:Online DNS Stamp calculator Protocol 選擇 DoH,IP 寫 IP,Host 寫域名,Path 寫/YOUR_DNS_QUERY_PATH ,然後複製生成的 Stamp 就可以了。

今天是 2024 年 4 月 1 日,看看自己域名的 DoH 可以存活多久。安卓網絡設置里雖然可以直接指定 DoT,NextDNS 的 DoT 也可以用 Nginx 代理,但是感覺陣亡幾率更大(專用端口),所以我選擇了更穩妥一點的 DoH。

2024-04-05: 移動網絡偶爾出現 SSL handshake timed out IO Error occured! Check network or DNS config! 和 Read timed out IO Error occured! Check network or DNS config! 但是還是能用。

2024-04-06: 晚上移動網絡開始出現 SSL handshake aborted: ssl=0x7b2db05fc8: I/O error during system call, Connection reset by peer IO Error occured! Check network or DNS config! 基本用不了了。之間訪問網站也顯示 The connection was reset 。所以清明節沒過完域名就掛了,存活時間約 5 天。如果想要繼續使用被屏蔽的域名,或者直接使用 NextDNS,無論是 DNSCrypt 還是 personalDNSfilter 都支持代理。一息尚存的 DNSCrypt 內置不少 DNS,還可以在不用代理的情況下使用。

2024-04-15:偶爾外出就餐時連接餐廳的 WiFi,發現電信的寬帶仍然可以使用。但是移動網絡和移動寬帶已經完全無法連上。


另請參閱:使用 Python 或/和 Cloudflare Worker 代理 NextDNS 的 DoH 服務

本文更新於 2024/04/24。

分類
软件

Proton VPN stops network after suspend in Fedora

Proton VPN的免費用戶有日本、荷蘭和美國三個國家的節點可供使用,安卓用戶可以從 F-Driod 下載官方客戶端,Fedora 需要先在添加軟件源然後就可以方便安裝。使用的方便程度以及免費節點的速度都非常好。但是牆內直接是用不了的。

有一個問題是,在Fedora 中,連接着 Proton VPN 的時候 Suspend,然後喚醒的時候有機率會網絡不通,其實是 Proton VPN 異常關閉導致的。Proton VPN 生成了一個包含 ipv6leak 的連接,用來防止泄露 IP,但是異常關閉的時候,卻沒來得及刪除它,所以網絡都斷了。可以嘗試這樣解決:

#list all the links, find the link contains 'ipv6leak'
ip link
#remove the link
sudo ip link delete ipv6leakintrf0

值得一提的是,如果你同時有使用 NextDNS,DNS 也可能成爲網絡中斷假象的原因之一。測試辦法有兩個,一是使用 Tor 打開任意網頁,因爲 Tor 並不依賴系統的 DNS 設置,如果能上網證明本地 DNS 可能有問題。二是使用 dig 命令,如果運行 dig ft.shaman.eu.org 返回 no servers could be reached 但是運行 dig ft.shaman.eu.org @45.90.28.136( 牆內可用 114.114.114.114 )返回正確 IP 地址,則也證明 DNS 可能有問題。可嘗試重啓 NextDNS 來解決:

nextdns restart
分類
软件

使用 Shelter 把老大哥的應用隔離起來

我在索尼手機上已經使用 Shelter 很久了,不用 Root,隔離應用,簡單有效。

把不常用的應用裝到『工作模式』,使用時打開,不用時凍結。即使需要給予那些應用一些敏感權限,但是工作模式有自己的通訊錄等資料,所以也無需太擔心。也可以設置自動凍結,但是我沒有試過。

掃描工作模式中新增的媒體

有一個問題是,當我從主要模式複製了照片到工作模式(如微信),但是從工作模式的應用里卻看不到那張照片(打開朋友圈里的相冊卻看不到這張照片)。這時需要觸發一下系統的媒體掃描,在沒有安裝額外應用的情況下,似乎只能通過重啓手機來觸發。但是好在有應用可裝,那就是 SD Scanner。把它安裝在工作模式,按一下 START RESCAN 就可以了。雖然這個應用的最後更新日期是 2017 年,但它在安卓 10 的系統上仍然能工作的很好。我沒有更新系統的手機,如果 SD Scanner 無法在更新系統的手機上使用,你可以嘗試下載一個叫 AZ MediaScaner 的閉源軟件,它是最近更新的,但是它有用戶跟蹤。

分類
软件

轉用 Visual Studio Code

之前無論是 PHP、Java 還是 Python,我都使用 Eclipse 這款開源的老編輯器。直到最近因爲種種原因切換到了 Visual Studio Code。

其中最重要的一點是,我的網絡成本增加,使用 Visual Studio Code 的遠程模式( Remote Development using SSH ),可以在服務器上編程與調試,節省了很多流量。遠程還有一個優點是網絡質量比本地更好,比如安裝編譯安卓應用所需的谷歌依賴,那些動輒上 GB 大小的依賴,再也不必考慮代理的問題。

我還是習慣於用 python -m venv project_venv 的方式建立項目專用的虛擬環境,VS Code 會自動識別項目中的虛擬環境,很方便。

代碼檢查與美化( linting )也是開箱即用。Eclipse 雖然可以自由配置多種 linter,但是我費了很多心力都不得其法。

在離開 Eclipse 前給他們捐了一點錢,我還是很樂意再用回去的,如果 Eclipse 能在日常編程體驗上追上其他編輯器。前不久還收到一個 Eclipse 用戶調查,我甚至看不懂他們新產品是做什麼的,可能我就快要被編程行業所淘汰了……

分類
软件

使用 Screen Stream 來分享安卓屏幕

Screen Stream 是一款由 Dmitriy Krivoruchko 開發的老牌(始於 2016 年)開源安卓屏幕共享軟件。使用方便,還支持中文,推薦有需要的朋友使用。

它主要是局域網共享屏幕,甲手機打開軟件,點擊開始即可。然後乙手機,在瀏覽器裏輸入甲手機的網址(或掃碼)就可以觀看了。就是這麼簡單!如果想要通過因特網分享屏幕,那麼你得確保你有公網 IP,使用方法是一樣的。

通過調教幀率和畫質,可以減輕手機網絡和性能消耗(少發一點熱)。通過設置安全選項,可以控制只通過 WiFi 網絡分享(即不通過手機網絡分享)或者只在本機分享(相當於是程序接口)。

通過服務器實現屏幕共享

由於沒有公網 IP,又想要分享屏幕給遠方親友,則可以通過服務器來實現。思路有很多,我有實現一個非常簡單辦法。對分享一方來說,可以通過 Termux 來把圖片 Post 到服務器,服務器收到圖片後展示出來即可。所用的 Termux 命令爲:

watch -n 3 "curl http://127.0.0.0.1:8080/stream.jpeg | curl -k -s -X POST --data-binary @- https://my.image.server/image_in"
#即每隔 3 秒,把圖片傳送到服務器一次

我的圖片服務是一個簡單的 Django 網站,免費部署在 pythonanywhere,等我有空了可以把代碼整理下發出來。我也測試過讓甲通過瀏覽器,用 Javascript 來把圖片轉發到服務器,Chrome 下是可以的,但是火狐裏網頁一到後臺,JS 就停止工作,搜了一下也沒找到火狐網頁保活的辦法。

理論上,把視頻流直接轉發到 Nginx,應該更加高效流暢,但是我沒有測試過。