2026/3/23

Structure Concurrency

是一種讓多執行緒邏輯變得更容易理解與管理的程式設計模型,目標是讓「並行任務的生命週期」像區塊結構(block structure)一樣有明確的範圍。

  • 子任務(threads, tasks)都必須在離開某個區塊前結束

  • 執行緒之間的層級關係(parent/child)是語法上明確可見的。

  • 不會有「孤兒執行緒」(dangling thread)偷偷跑在背景。

傳統的 thread

  • 任務彼此獨立、缺乏邏輯關聯。

  • 錯誤傳遞困難(子執行緒例外不會自動向上傳遞)。

  • 難以在結束前確保所有子任務完成。

void process() {
    Thread t = new Thread(() -> downloadFile());
    t.start();
    // ... do other work ...
    // 忘記 join() 或沒有捕捉例外,就可能造成 thread 泄漏
}

Structure Concurrency (JEP 453 / Java 21)

  • ShutdownOnFailure: 若任一子任務失敗,其他會自動中止。

  • ShutdownOnSuccess: 若有一個成功,其餘終止(常用於競賽式查詢)。

  • 範圍清晰、錯誤可控、資源可預期。

import java.util.concurrent.*;

void process() throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Future<String> f1 = scope.fork(() -> downloadFile());
        Future<String> f2 = scope.fork(() -> fetchMetadata());
        scope.join();              // 等待所有子任務
        scope.throwIfFailed();     // 傳遞例外
        System.out.println(f1.resultNow() + f2.resultNow());
    } // scope 區塊結束時,自動關閉所有未完成子任務
}
  • Virtual Threads 解決「效能 & 可擴充性」。

  • Structured Concurrency 解決「邏輯一致性 & 錯誤處理」。

Example

import java.util.concurrent.*;
import java.lang.management.*;

public class StructuredConcurrencyVirtualThreadsExample {

    public static void main(String[] args) throws Exception {
        System.out.println("Java: " + System.getProperty("java.version"));
        System.out.println();

        System.out.println("=== Example 1: Structured Concurrency (ShutdownOnFailure) ===");
        example();

        System.out.println();
        System.out.println("=== Example 2: Virtual vs Native Thread Performance Test ===");
        compareVirtualAndNativeThreads();
    }

    static void example() {
        // 使用 StructuredTaskScope.open() 創建作用域
        try (var scope = StructuredTaskScope.open()) {
            // 創建三個子任務
            var f1 = scope.fork(() -> simulatedIo("Service-A", 1200, false));
            var f2 = scope.fork(() -> simulatedIo("Service-B", 2500, true));
            var f3 = scope.fork(() -> simulatedIo("Service-C", 3000, false));

            // 等待所有子任務完成
            scope.join();

            // 處理結果
            System.out.println("Results: " +
                (f1.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f1.get() : "Failed") + ", " +
                (f2.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f2.get() : "Failed") + ", " +
                (f3.state() == StructuredTaskScope.Subtask.State.SUCCESS ? f3.get() : "Failed"));

        } catch (Exception e) {
            System.out.println("Scope finished with failure: " + e);
        }
    }

    /**
     * Compare Virtual Threads vs Native Threads with ASCII table output.
     */
    static void compareVirtualAndNativeThreads() throws Exception {
        int taskCount = 10_000;
        System.out.printf("Launching %,d simulated I/O tasks...\n", taskCount);

        long nativeTime = measureThreadTypePerformance(taskCount, false);
        long virtualTime = measureThreadTypePerformance(taskCount, true);

        long usedMem = getUsedMemoryMB();

        // ASCII Table output
        System.out.println();
        System.out.println("+--------------------+----------------+----------------+");
        System.out.println("| Thread Type        | Time (ms)      | Observations   |");
        System.out.println("+--------------------+----------------+----------------+");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Native Threads", nativeTime, "limited pool");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Virtual Threads", virtualTime, "scales easily");
        System.out.println("+--------------------+----------------+----------------+");
        System.out.printf ("| %-18s | %-14d | %-14s |%n", "Heap Used (MB)", usedMem, "after test");
        System.out.println("+--------------------+----------------+----------------+");
    }

    static long measureThreadTypePerformance(int count, boolean virtual) throws Exception {
        long start = System.currentTimeMillis();

        ExecutorService executor = virtual ? Executors.newVirtualThreadPerTaskExecutor()
                                           : Executors.newFixedThreadPool(200);

        try (executor) {
            for (int i = 0; i < count; i++) {
                int id = i;
                executor.submit(() -> {
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException ignored) {}
                    return null;
                });
            }
        }

        long end = System.currentTimeMillis();
        return end - start;
    }

    static String simulatedIo(String name, long millis, boolean fail) throws InterruptedException {
        System.out.printf("[%s] running on %s, sleep %dms%n", name, Thread.currentThread(), millis);
        Thread.sleep(millis);
        if (fail) throw new RuntimeException(name + " failed!");
        return name + "-done";
    }

    static long getUsedMemoryMB() {
        MemoryMXBean mbean = ManagementFactory.getMemoryMXBean();
        long used = mbean.getHeapMemoryUsage().getUsed();
        return used / (1024 * 1024);
    }
}

編譯與執行

javac --enable-preview --release 25 StructuredConcurrencyVirtualThreadsExample.java
java --enable-preview StructuredConcurrencyVirtualThreadsExample

執行結果

Java: 25

=== Example 1: Structured Concurrency (ShutdownOnFailure) ===
[Service-A] running on VirtualThread[#26]/runnable@ForkJoinPool-1-worker-1, sleep 1200ms
[Service-C] running on VirtualThread[#30]/runnable@ForkJoinPool-1-worker-2, sleep 3000ms
[Service-B] running on VirtualThread[#28]/runnable@ForkJoinPool-1-worker-4, sleep 2500ms
Scope finished with failure: java.util.concurrent.StructuredTaskScope$FailedException: java.lang.RuntimeException: Service-B failed!

=== Example 2: Virtual vs Native Thread Performance Test ===
Launching 10,000 simulated I/O tasks...

+--------------------+----------------+----------------+
| Thread Type        | Time (ms)      | Observations   |
+--------------------+----------------+----------------+
| Native Threads     | 10232          | limited pool   |
| Virtual Threads    | 240            | scales easily  |
+--------------------+----------------+----------------+
| Heap Used (MB)     | 61             | after test     |
+--------------------+----------------+----------------+

同時支援舊的 Native Thread

  • 自動 join 或取消子任務;

  • 錯誤可統一處理;

  • 可以使用任意 Executor 來決定底層執行緒類型。

    static void example2() {
        try (var scope = StructuredTaskScope.open()) {

            // Virtual Thread Executor
            ExecutorService virtualExecutor = Executors.newVirtualThreadPerTaskExecutor();

            // fork 只能傳 Callable,不傳 Executor
            Callable<String> task1 = () -> simulatedIo("VT-A", 500, false);
            Callable<String> task2 = () -> simulatedIo("VT-B", 800, false);

            var f1 = scope.fork(task1);
            var f2 = scope.fork(task2);

            // Native Thread Executor
            ExecutorService nativeExecutor = Executors.newFixedThreadPool(2);
            Callable<String> cpuTask = () -> heavyComputation("NT-A");
            var f3 = scope.fork(cpuTask);

            scope.join(); // 等待所有子任務完成

            System.out.println("Results: " + f1.get() + ", " + f2.get() + ", " + f3.get());

            virtualExecutor.shutdown();
        } catch (Exception e) {
            System.out.println("Scope finished with failure: " + e);
        }

    }

    // 模擬 CPU 任務
    static String heavyComputation(String name) {
        System.out.printf("[%s] CPU task on %s%n", name, Thread.currentThread());
        long sum = 0;
        for (long i = 0; i < 10_000_000L; i++) sum += i;
        return name + "-done";
    }

2026/3/16

Java Virtual Thread

Java 從 Green Thread 時代開始,演進到使用 OS Thread,然後到了 Java 21 版,正式採用 Virtual Thread,以適應微服務時代伺服器的變革。

Green Threads

  • JDK 為了跨平台的特性,要讓程式可以在單核心,沒有多個 thread 的 OS 上運作,所以設計了內建的 thread 跟 scheduler

  • 在 JDK 1.1 (1997) 時期,Java 使用 green threads(由 JVM 在使用者空間模擬出來的執行緒,不直接用 OS thread)。

  • 優點:跨平台,不依賴作業系統執行緒,能讓多執行緒程式在沒有多執行緒支援的 OS 上跑。

  • 缺點:

    • JVM 本身要實作 thread scheduler,效能比不上 OS 提供的 thread。

    • 遇到 blocking I/O(例如 read socket),整個 JVM scheduler 都會被卡住,所有 green threads 都會停住。

  • 從 JDK 1.3 (2000) 開始,Java 全面切換到 1:1 OS threads 模型

Native Thread

作業系統核心會用 time-sharing 或 優先權排程 來管理 threads。

  • 重量級資源單位

    • 每個 OS thread 需要 stack (通常 1MB 預設)、TCB (thread control block)、kernel data structures。

    • 建立/銷毀成本高 (微秒到毫秒級)。

  • 數量有限

    • 即使硬體支援很多核心,實務上 JVM 或應用程式能開的 OS thread 數量大約只有幾千到幾萬。
  • blocking I/O 會卡住 thread

    • 假設某個 OS thread 正在 read() 一個 socket,整個 thread 會被 kernel block。

    • 即使這時 CPU 沒事幹,這個 thread 對 JVM 來說就是「卡住不能用」。

  • 搶佔式排程 (preemptive)

    • OS scheduler 會把 CPU 切給不同 threads。

    • context switch 成本高,要切換 CPU 狀態、register、stack。

  • 共享記憶體

    • 所有 thread 共用同一個 address space,必須透過鎖 (lock, monitor) 控制同步,容易出現 race condition、deadlock。
  • OS thread 模型對「大量 I/O 密集型應用」來說效率很差

    • 適合少量、CPU 密集的任務

    • 不適合製作非常大量的網路連線伺服器

Virtual Thread

  • Project Loom 在 Java 19 引入預覽功能、Java 21 變成正式 GA 的一個重要新功能
  • 輕量級:建立/銷毀成本非常低,幾 KB stack 就能跑。

  • blocking I/O 處理方式不同

    • Virtual Thread 呼叫阻塞 API (Socket.read()),JVM 會攔截並讓出 OS thread。

    • 這樣 OS thread 可以拿去執行其他 Virtual Thread,不會浪費資源。

  • 數量級提升

    • 可以開數百萬個 virtual threads,對應數百萬個並發請求。

    • 讓程式碼還是同步/直觀,但效能接近非同步 I/O 模型。

  • 簡化程式碼

    • 不需要複雜的 callback、CompletableFuture、reactive pipeline,直接用同步程式碼就能寫出 scalable 程式。
  • 把「blocking I/O」變得非阻塞化,同時又保留傳統同步 API 的簡單性,讓 Java 更適合現代微服務/高併發應用。

  • 虛擬線程更適合 I/O 型或高併發場景。如果是非常 CPU密集或如果內部有同步鎖 (synchronized) 或重度共享資源競爭,則使用傳統線程。

Erlang Process

  • 極度輕量:一個 process 只佔用幾 KB 記憶體,可以同時開數百萬個。由 BEAM VM 調度

  • 獨立記憶體空間:每個 process 有自己的 heap/stack,不共享狀態。

  • 排程由 BEAM 虛擬機控制:BEAM 用 OS threads(通常一個核心對應一個 scheduler thread)去執行上千萬個 Erlang processes。

  • preemptive scheduling:Erlang process 執行一定數量的 reductions(指令數)後會自動讓出 CPU。

  • Erlang process 的定位 比較接近 Java 的 Virtual Thread

    • 因為 Erlang 從設計之初就為了 massive concurrency,整個 I/O 模型與錯誤隔離都是以「百萬 process」為目標;

    • Java Virtual Thread 是在既有 thread-based API 上加的輕量執行緒,強調「低成本封裝既有同步程式碼」。

Erlang Process vs Java Threads 比較

特性 Erlang Process Java OS Thread Java Virtual Thread
管理單位 BEAM VM (使用 scheduler threads) OS Kernel JVM (基於 OS threads)
重量級 / 輕量級 超輕量 (幾 KB) 重量級 (MB) 輕量 (KB 級)
建立數量 百萬級 幾千~幾萬 百萬級
排程 BEAM VM preemptive scheduling OS scheduler JVM scheduler
blocking I/O 不會卡住整個 VM,process 掛起,scheduler 跑其他 process 卡住 OS thread 掛起虛擬執行緒,釋放 OS thread
共享記憶體 不共享,透過 message passing 共享,需要鎖 不共享(但可用同步物件)
錯誤隔離 完整隔離,崩潰不會影響其他 process 線程崩潰可能拖垮 JVM 崩潰只影響該 virtual thread
設計哲學 為 massive concurrency 與容錯而生 傳統 multi-threading 保留同步 API,實現高併發
適用場景 聊天室、遊戲伺服器(每個玩家一個 process)、即時推播(pub/sub 模式) I/O 密集、RPC、WebSocket CPU 密集、JNI、非阻塞演算法

2026/3/9

Shell Script 語法比較表

整理常用 Shell (sh, bash, zsh) 在語法、功能上的差異與相容性,方便參考。

類型 sh (POSIX) bash zsh 備註
Shebang #!/bin/sh #!/bin/bash #!/bin/zsh 建議腳本跨平台用 #!/bin/sh
變數宣告 name=value 相同 相同 不能有空格
字串插值 "Hello $name" 相同 相同 都支援
命令替換 `date`$(date) 相同 相同 建議用 $( )
條件判斷 [ "$a" = "$b" ] [ "$a" = "$b" ][[ $a == $b ]] [ "$a" = "$b" ][[ $a = $b ]] [[ ... ]] 不是 POSIX 的寫法
邏輯運算 [ "$a" = 1 ] && [ "$b" = 2 ] [[ $a = 1 && $b = 2 ]] 相同 [[ ... && ... ]] 非 POSIX
數學運算 $((1+2)) ((i++)) / $((1+2)) ((i++)) / $((1+2)) POSIX sh 只能 $(( ))
陣列 ❌ 不支援 arr=(a b c)${arr[0]} arr=(a b c)${arr[1]} Bash 陣列從 0 起算,Zsh 從 1 起算
關聯陣列 key=value declare -A map; map[key]=val typeset -A map; map[key]=val POSIX sh 沒有
brace expansion {1..5} {1..5} POSIX sh 不支援
迴圈 for for i in 1 2 3; do ...; done for i in {1..3}; do ...; done brace expansion 會展開整個序列 {1..3} 不是 POSIX
函數定義 foo() { ... } foo() { ... }function foo { ... } 相同 function foo {} 不是 POSIX
字串長度 ${#var} 相同 相同
字串比較 = == / = == / = (glob) POSIX sh 只能用 =
大小寫轉換 ${var^^} / ${var,,} ${(U)var} / ${(L)var} Bash/Zsh 特殊功能
字串切割 ${var%pattern} / ${var#pattern} 支援更多:${var^^} (大寫) 支援更多::${(U)var} POSIX 只有 % #
printf / echo printf 標準,echo 不一定支援 -e echo -e 可用 echo -e 可能無效,用 print 建議用 printf
測試檔案 [ -f file ] 相同 相同
正則比對 [[ string =~ regex ]] [[ string =~ regex ]] 但 regex 語法不同 POSIX sh 無 regex 功能
展開 (globbing) 基本 * ? [ ] shopt -s globstar Zsh 預設更強大 (e.g. **/*.txt) Zsh glob 功能最強
補全 (tab) bash-completion 內建強大補全 互動環境差異,不影響 script
錯誤處理 set -e 相同,加強版 set -o pipefail 相同 pipefail 不是 POSIX
信號處理 trap 'cmd' INT TERM 相同 相同
source 檔案 . file source file / . file source file / . file POSIX sh 用 .
目錄堆疊 pushd / popd pushd / popd POSIX sh 無目錄堆疊功能
互動功能 readline、history、completion history、completion、prompt customization Zsh 提供最強互動功能
local / typeset local / typeset local / typeset POSIX sh 不支援函數內局部變數

  • sh
    • POSIX,相容性最好,但功能有限。
    • 如果要製作跨平台的 script,就使用標準的 POSIX 語法
  • bash
    • 增強 POSIX,支援陣列、關聯陣列、[[ ]]、brace expansion、Bashisms。
    • 一般在 mac/linux,可使用 bash script
  • zsh
    • 幾乎包含 Bash 功能,互動功能更強(prompt、補全、glob、history)
    • 陣列索引從 1 開始,部分語法行為不同。
    • 使用者互動操作時,可使用 zsh

2026/3/2

zsh

在 RockyLinux 測試 zsh。目前比較常見的,還是使用 bash,如果要測試 zsh,需要另外安裝。

安裝

dnf -y install zsh

修改預設的 shell

chsh -s /bin/zsh

基本的設定檔

可直接跳到下面的 Oh My Zsh

# 1. 基本環境
export LANG=en_US.UTF-8
export EDITOR=vim
export PATH="/usr/local/bin:$PATH"

# 2. Prompt 設定
PROMPT='[%*]%n@%m %~$ '

# 3. Alias 常用指令
alias ll='ls -lh'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'

# 4. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 5. 自動補全 & 修正
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全時忽略大小寫

# 6. 補全行為微調
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

功能有

  • 乾淨的 prompt[時間]使用者@主機 當前路徑$

  • 常用 aliasll, la, gs, ..

  • 歷史 → 儲存 5000 筆並共享不同視窗

  • 自動補全 → tab 補全,支援大小寫不敏感

  • 錯字修正 & auto cd → 輸入資料夾名稱會自動進去

Oh My Zsh

Oh My Zsh 是 zsh 常見的 plugin 管理工具,可安裝多個 plugin 擴充 zsh 的功能,但也要注意,載入越多 plugin 會讓 zsh 啟動變慢。

安裝

sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

常見的 theme 工具是 Powerlevel10k,但這邊不使用,直接用基本的 Prompt

修改 ~/.zshrc 增加以下這個部分,然後就能將一些不同用途的設定檔分開

for file in .zshrc_*;
do source $file;
done

增加常用 plugins

安裝

cd $ZSH/custom/plugins
# 安裝 zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-autosuggestions.git

# 安裝 zsh-syntax-highlighting
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git

# zsh-completions
git clone https://github.com/zsh-users/zsh-completions

設定檔

~/.zshrc_basic

# 1. 指定 Oh My Zsh 安裝路徑
export ZSH="$HOME/.oh-my-zsh"

# 3. Plugins
# git           → git alias & 補全
# zsh-autosuggestions → 歷史自動建議
# zsh-syntax-highlighting → 命令語法高亮
# history-substring-search → 部分字串歷史搜索
# extract       → 快速解壓縮
plugins=(
  git
  zsh-autosuggestions
  zsh-syntax-highlighting
  zsh-completions
  history-substring-search
  extract
  colored-man-pages
  z
)

# 4. 啟動 Oh My Zsh
source $ZSH/oh-my-zsh.sh

# 5. PATH & 編輯器
export PATH="/usr/local/bin:$PATH"
export EDITOR=vim
export LANG=en_US.UTF-8

# 6. Alias 常用指令
alias ll='ls -lh'
#alias la='ls -A'
alias la='ls -la'
alias ..='cd ..'
alias gs='git status'
alias gp='git push'
alias gd='git diff'

# 7. 歷史設定
HISTSIZE=5000
SAVEHIST=5000
HISTFILE=~/.zsh_history
setopt share_history      # 多個 zsh session 共享歷史

# 8. 自動補全 & 行為優化
autoload -Uz compinit && compinit
setopt correct            # 拼字錯誤自動建議
setopt autocd             # 輸入資料夾名稱自動 cd
setopt nocaseglob         # 補全大小寫不敏感
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'

另外做一個 ~/.zshrc_prompt

#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f$ '
#PROMPT='%F{green}[%D{%H:%M}]%f%F{yellow}%n@%m%f %F{blue}%~%f${git_prompt_info} $ '
autoload -Uz vcs_info
precmd() { vcs_info }
zstyle ':vcs_info:git:*' formats '(%b)'

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n@%m%f %F{magenta}%~%f${vcs_info_msg_0_} $ '

增加功能,區分本地跟遠端的 ssh

Zsh / Bash 都可以靠環境變數來判斷:

  • 本地登入 -> SSH_CONNECTION / SSH_TTY 不存在

  • 遠端登入 (SSH) -> SSH_CONNECTIONSSH_TTY 存在

if [[ -n "$SSH_CONNECTION" ]]; then
  HOST_STYLE="%F{red}@%m%f(ssh)"
else
  HOST_STYLE="%F{yellow}@%m%f"
fi

PROMPT='%F{blue}[%D{%H:%M}]%f%F{green}%n%f'"$HOST_STYLE"' %F{magenta}%~%f${vcs_info_msg_0_} $ '

重新登入後,就可以使用 zsh

extract plugin

extract 定義了一個名為 extract 的函數,用於解壓縮你傳遞給它的檔案,並支援多種檔案類型。

使用方式為 extract 檔案名稱

不需要知道具體的解壓縮命令,只需執行 extract 就可以解壓大部分常見檔案,直接輸入 x 檔案名稱 也可以

z plugin

Usage: z [OPTION]... [ARGUMENT]
Jump to a directory that you have visited frequently or recently, or a bit of both, based on the partial
string ARGUMENT.

With no ARGUMENT, list the directory history in ascending rank.

  --add Add a directory to the database
  -c    Only match subdirectories of the current directory
  -e    Echo the best match without going to it
  -h    Display this help and exit
  -l    List all matches without going to them
  -r    Match by rank
  -t    Match by recent access
  -x    Remove a directory from the database (by default, the current directory)
  -xR   Remove a directory and its subdirectories from the database (by default, the current directory)

autojump

z 是用 shell script 實作,autojump 是 python,z 的速度比較快,且內建於 zsh。一般建議就直接使用 z,如果要使用 autojump,需要做另外的設定。

安裝

cd $ZSH/custom/plugins

# 安裝 autojump
git clone https://github.com/wting/autojump.git

注意 autojump 需要另外安裝套件

dnf -y install autojump

因為 autojump 只有提供 bash 版本的初始化 script

/usr/share/autojump/autojump.bash

現在要使用 zsh,故要另外產生一個 zsh 版本的初始化

到 autojump 原始的 github 網頁取得 autojump.zsh

把 autojump.zsh 放到 /usr/share/autojump/autojump.zsh

修改設定檔

plugins=(
  autojump
)

# Autojump 初始化
# 確認 autojump 安裝後再 source
if [ -f /usr/share/autojump/autojump.zsh ]; then
  source /usr/share/autojump/autojump.zsh
fi
# 任意切換目錄
cd ~/download/dir1
cd ~/download/dir2

# 檢查 autojump
j -s

# autojump
j dir1

2026/2/9

Cytoscape.js

Cytoscape.js 是一個處理資料視覺化的 javascript library,當我們要對資料關係進行可視化顯示時,例如社交網路關係或網路拓樸圖時,Cytoscape.js 是個不錯的選擇。

Cytoscape 和 Cytoscape.js 是兩個完全獨立不同的軟體

  • Cytoscape

    • 使用 Java 語言編寫的用於網絡可視化的桌面應用程序

    • 需要安裝 Java SDK 才能使用

    • 用於大型網絡分析和可視化的高性能應用程序

  • Cytoscape.js

    • 用於網絡可視化的 javascript library,本身不是一個完整的 Web Application

    • 可以在大多數瀏覽器上使用

    • 不需要 plugin 即可運行

    • 需要編寫程式來建構 Web Application

    • 支援 Extensions

    • 基於 CSS 將資料映射到元件屬性

sample1

建立一個 圓形排列 的 4 個節點 (A, B, C, D),節點之間有箭頭連線,點擊節點會有事件

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>test1</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
            display: block;
        }
    </style>
</head>
<body>
    <h2>test1</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'a', label: '節點 A' } },
                { data: { id: 'b', label: '節點 B' } },
                { data: { id: 'c', label: '節點 C' } },
                { data: { id: 'd', label: '節點 D' } },
                { data: { id: 'ab', source: 'a', target: 'b' } },
                { data: { id: 'bc', source: 'b', target: 'c' } },
                { data: { id: 'cd', source: 'c', target: 'd' } },
                { data: { id: 'da', source: 'd', target: 'a' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 3,
                        'line-color': '#AAAAAA',
                        'target-arrow-color': '#AAAAAA',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'circle'
            }
        });

        // 點擊事件
        cy.on('tap', 'node', function(evt) {
            let node = evt.target;
            console.log('你點了節點: ' + node.id());
        });
    </script>
</body>
</html>

sample2

流程圖,layout 調整為 dagre extension。

使用時要引用 dagre library 及 Cytoscape.js 的 extension

dagre 正是 Cytoscape.js 常用來畫flowchart或 directed graph 的 layout。適合做 flowchart, network topology, workflow

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Flowchart</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <script src="https://unpkg.com/dagre/dist/dagre.min.js"></script>
    <script src="https://unpkg.com/cytoscape-dagre/cytoscape-dagre.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>flowchart</h2>
    <div id="cy"></div>

    <script>
        cytoscape.use(cytoscapeDagre);

        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'start', label: '開始' } },
                { data: { id: 'step1', label: '步驟 1' } },
                { data: { id: 'step2', label: '步驟 2' } },
                { data: { id: 'decision', label: '判斷 ?' } },
                { data: { id: 'end', label: '結束' } },
                { data: { id: 's1', source: 'start', target: 'step1' } },
                { data: { id: 's2', source: 'step1', target: 'step2' } },
                { data: { id: 's3', source: 'step2', target: 'decision' } },
                { data: { id: 's4', source: 'decision', target: 'end' } },
                { data: { id: 's5', source: 'decision', target: 'step1' } }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#28a745',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#28a745'
                    }
                },
                {
                    selector: 'node[id="decision"]',
                    style: {
                        'shape': 'diamond',
                        'background-color': '#ffc107',
                        'text-outline-color': '#ffc107'
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle',
                        'curve-style': 'bezier',
                    }
                }
            ],

            layout: {
                name: 'dagre',
                // rankDir: 'TB'  // top-to-bottom
                rankDir: 'LR' // 由左到右 排列
            }
        });
    </script>
</body>
</html>

sample3

鐵路模擬,增加火車在鐵軌上移動的動畫

<!DOCTYPE html>
<html lang="zh-Hant">
<head>
    <meta charset="UTF-8">
    <title>Cytoscape.js Railway</title>
    <script src="https://unpkg.com/cytoscape/dist/cytoscape.min.js"></script>
    <style>
        #cy {
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>
    <h2>Railway</h2>
    <div id="cy"></div>

    <script>
        const cy = cytoscape({
            container: document.getElementById('cy'),

            elements: [
                { data: { id: 'station1', label: '車站 1' } },
                { data: { id: 'station2', label: '車站 2' } },
                { data: { id: 'station3', label: '車站 3' } },
                { data: { id: 'checkpoint4', label: '檢查點 4' } },
                { data: { id: 'checkpoint5', label: '檢查點 5' } },
                { data: { id: 'checkpoint6', label: '檢查點 6' } },
                { data: { id: 's1', source: 'station1', target: 'station2' } },
                { data: { id: 's2', source: 'station2', target: 'station3' } },
                { data: { id: 's3', source: 'station2', target: 'checkpoint4' } },
                { data: { id: 's4', source: 'checkpoint4', target: 'checkpoint5' } },
                { data: { id: 's5', source: 'checkpoint5', target: 'station3' } },
                { data: { id: 's6', source: 'checkpoint5', target: 'checkpoint6' } },

                // 列車節點
                { data: { id: 'train1', label: '🚆' }, classes: 'train' }
            ],

            style: [
                {
                    selector: 'node',
                    style: {
                        'shape': 'ellipse',
                        'background-color': '#0074D9',
                        'label': 'data(label)',
                        'color': '#fff',
                        'text-valign': 'center',
                        'text-outline-width': 2,
                        'text-outline-color': '#0074D9'
                    }
                },
                {
                    selector: 'node[id^="station"]',
                    style: {
                        'shape': 'round-rectangle',
                        'background-color': '#17a2b8',
                        'text-outline-color': '#17a2b8'
                    }
                },
                {
                    selector: 'node.train',
                    style: {
                        'background-color': 'red',
                        'shape': 'ellipse',
                        'label': 'data(label)',
                        'font-size': 24,
                        'width': 30,
                        'height': 30
                    }
                },
                {
                    selector: 'edge',
                    style: {
                        'width': 2,
                        'line-color': '#555',
                        'target-arrow-color': '#555',
                        'target-arrow-shape': 'triangle'
                    }
                }
            ],

            layout: {
                name: 'breadthfirst',
                directed: true,
                padding: 20
            }
        });

        function moveAlongEdge(train, fromNode, toNode, duration, callback) {
            const start = fromNode.position();
            const end = toNode.position();
            const startTime = performance.now();

            function animate(now) {
                const elapsed = now - startTime;
                const t = Math.min(elapsed / duration, 1); // 0~1
                const x = start.x + (end.x - start.x) * t;
                const y = start.y + (end.y - start.y) * t;
                train.position({ x, y });

                if (t < 1) {
                    requestAnimationFrame(animate);
                } else if (callback) {
                    callback();
                }
            }

            requestAnimationFrame(animate);
        }

        function moveTrain(path) {
            let i = 0;
            const train = cy.getElementById('train1');

            function step() {
                if (i >= path.length - 1) return;
                const fromNode = cy.getElementById(path[i]);
                const toNode = cy.getElementById(path[i + 1]);

                moveAlongEdge(train, fromNode, toNode, 2000, () => {
                    i++;
                    step();
                });
            }

            step();
        }

        // 定義路徑
        const route = ['station1', 'station2', 'checkpoint4', 'checkpoint5', 'station3'];

        // 初始化列車位置
        cy.getElementById('train1').position(cy.getElementById(route[0]).position());

        // 2 秒後啟動列車
        setTimeout(() => moveTrain(route), 2000);
    </script>
</body>
</html>

2026/2/2

systemd template unit service

systemd template unit 是一種樣板服務 (service template),可以用同一份 unit 檔去啟動多個獨立的 service instance。當我們需要用同一個 service daemon 啟動多個 service instance 時,就可以透過這個方式,讓 service 對應到不同的設定檔,同時並存於一台機器中。

httpd

在 /usr/lib/systemd/system 目錄,除了 httpd.service,還有 httpd@.service

  • @ 代表這個 unit 是一個「模板」。

  • %i 代表實例名稱 (instance name),會在啟動的時候被替換。

systemd template 支援一些 specifier,常見的有:

  • %i → instance name (例如 site1 / site2)

  • %I → instance name,保持大小寫

  • %n → 完整的 unit name (httpd@site1.service)

  • %p → prefix name (httpd)

httpd@service 的內容是這樣

httpd@.service
# This is a template for httpd instances.
# See httpd@.service(8) for more information.

[Unit]
Description=The Apache HTTP Server
After=network.target remote-fs.target nss-lookup.target
Documentation=man:httpd@.service(8)

[Service]
Type=notify
Environment=LANG=C
Environment=HTTPD_INSTANCE=%i
ExecStartPre=/bin/mkdir -m 710 -p /run/httpd/instance-%i
ExecStartPre=/bin/chown root.apache /run/httpd/instance-%i
ExecStart=/usr/sbin/httpd $OPTIONS -DFOREGROUND -f conf/%i.conf
ExecReload=/usr/sbin/httpd $OPTIONS -k graceful -f conf/%i.conf
# Send SIGWINCH for graceful stop
KillSignal=SIGWINCH
KillMode=mixed
PrivateTmp=true

service 會讀取 /etc/httpd/conf/%i.conf 設定檔,並將 pid 放在 /run/httpd/instance-%i

所以要產生兩個 httpd unit service 設定檔

cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site1.conf
cp /etc/httpd/conf/httpd.conf /etc/httpd/conf/site2.conf

修改 site1.conf 以下這些設定。site2.conf 就改另一個 Listen 8001,site1 改為 site2,去掉其他 Directory 的部分

Listen 8000
PidFile /run/httpd-site1.pid

DocumentRoot "/var/www/site1"

<Directory "/var/www/site1">
    Options Indexes FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

ErrorLog "/var/log/httpd/site1_error.log"

CustomLog "/var/log/httpd/site1_access.log" combined

啟動

systemctl start httpd@site1
systemctl start httpd@site2

systemctl enable httpd@site1
systemctl enable httpd@site2

haproxy

如果是 haproxy,因為套件裡面沒有 unit service,我們需要自己製作一個

首先產生 /usr/lib/systemd/system/haproxy@.service 檔案

[Unit]
Description=HAProxy Load Balancer %i instance
After=network-online.target
Wants=network-online.target

[Service]
Environment="CONFIG=/etc/haproxy/%i.cfg" "PIDFILE=/run/haproxy-%i.pid" "CFGDIR=/etc/haproxy/conf.d.%i"
EnvironmentFile=/etc/sysconfig/haproxy.%i
ExecStartPre=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecStart=/usr/sbin/haproxy -Ws -f $CONFIG -f $CFGDIR -p $PIDFILE $OPTIONS
ExecReload=/usr/sbin/haproxy -f $CONFIG -f $CFGDIR -c -q $OPTIONS
ExecReload=/bin/kill -USR2 $MAINPID
SuccessExitStatus=143
KillMode=mixed
Type=notify

[Install]
WantedBy=multi-user.target

製作設定檔

cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site1
cp /etc/sysconfig/haproxy /etc/sysconfig/haproxy.site2

製作 /etc/haproxy/sit1.cfg

global
    log 127.0.0.1 local2
    chroot /var/lib/haproxy
    pidfile /var/run/haproxy-site1.pid
    stats socket /var/run/haproxy.admin.sock mode 660 level admin

    maxconn     50000
    maxconnrate 100000
    maxsessrate 100000
    user        haproxy
    group       haproxy
    daemon
    nbproc  1
    ca-base     /etc/pki/site1
    crt-base    /etc/pki/site1
    tune.ssl.default-dh-param   2048
    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats-site1

    ssl-default-bind-options no-sslv3
    ssl-default-bind-options no-sslv3 no-tlsv11 no-tlsv10

defaults
    log global
    mode    http
    option  httplog clf
    option  forwardfor
    option  dontlognull
    option  httpchk
    option  http-keep-alive
    retries 3
    maxconn 50000
    rate-limit sessions 20000
    option  http-server-close
    timeout connect 1h
    timeout client  1h
    timeout server  1h
    #timeout connect 5000
    #timeout client  50000
    #timeout server  50000
    timeout tunnel  1h

frontend http_redirect
    bind    *:80
    mode    http
    acl kill_it method TRACE
    http-request deny if kill_it
    redirect   scheme https code 301 if !{ ssl_fc }
    default_backend web_server

frontend https_switch
    bind    *:443 ssl crt server.pem ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384
    mode    http
    option  forwardfor
    reqadd  X-Forwarded-Proto:\ https

    default_backend web_server

backend web_server
    mode    http
    fullconn    50000
    balance leastconn
    option      forwardfor
    #cookie      SERVERID insert indirect nocache
    #cookie SESSIONID prefix indirect nocache
    cookie  SESSIONID prefix nocache
    http-request        set-header X-Forwarded-Port %[dst_port]
    http-request        add-header X-Forwarded-Proto https if { ssl_fc }
    #option      httpchk GET /
    option  httpchk *
    server  W01 localhost:8000 weight 10 check cookie W01 inter 5s rise 2 fall 3

製作另一個設定檔 /etc/haproxy/site2.cfg,注意要修改 bind port

然後注意,申請兩個 ssl 憑證,放到 /etc/pki/site1 跟 /etc/pki/site2

啟動

systemctl start haproxy@site1
systemctl start haproxy@site2

systemctl enable haproxy@site1
systemctl enable haproxy@site2

2026/1/26

ChatTTS

ChatTTS GitHub - 2noise/ChatTTS: A generative speech model for daily dialogue. 是支援中文與英文兩種語言的 TTS engine,特別的是,不僅只是單純的轉換為語音,還能調整音色,設定語速,增加笑聲口頭語等等功能。ChatTTS使用約100,000小時的中文和英文數據進行訓練,開源版本是一個在 40,000 小時語音資料上進行無監督微調的預訓練模型。為了限制 ChatTTS 的使用,在 40,000 小時模型的訓練過程中包含了少量高頻噪音。

文字的部分要注意有些標點符號不支援,另外還有阿拉伯數字也不支援,使用前要先把數字轉換為中文。

安裝

pip3 install torchaudio soundfile
pip3 install git+https://github.com/2noise/ChatTTS

測試

最基本的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

texts = ["In some versions of torchaudio, the first line works but in other versions, so does the second line.", "I needed to install both torchaudio and soundfile in conda isolated environment"]

wavs = chat.infer(texts)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"basic_output{i}.wav", torch.from_numpy(wavs[i]), 24000)

以下是一個可調整參數的測試程式

import ChatTTS
import torch
import torchaudio

chat = ChatTTS.Chat()
chat.load(compile=False) # Set to True for better performance

# 需要转化为音频的文本内容
text = '重返白宮才要滿三週的川普已經為了削減聯邦支出而發布一系列行政命令。'


###################################
# Sample a speaker from Gaussian.

rand_spk = chat.sample_random_speaker()
print(rand_spk) # save it for later timbre recovery

params_infer_code = ChatTTS.Chat.InferCodeParams(
    spk_emb = rand_spk, # add sampled speaker
    temperature = .3,   # using custom temperature
    top_P = 0.7,        # top P decode
    top_K = 20,         # top K decode
)

###################################
# For sentence level manual control.

# use oral_(0-9), laugh_(0-2), break_(0-7)
# to generate special token in text to synthesize.
params_refine_text = ChatTTS.Chat.RefineTextParams(
    prompt='[oral_2][laugh_0][break_6]',
)

wavs = chat.infer(
    text,
    use_decoder=True,
    params_refine_text=params_refine_text,
    params_infer_code=params_infer_code,
)

for i in range(len(wavs)):
    """
    In some versions of torchaudio, the first line works but in other versions, so does the second line.
    """
    try:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]).unsqueeze(0), 24000)
    except:
        torchaudio.save(f"output{i}.wav", torch.from_numpy(wavs[i]), 24000)

固定音色

程式是以亂數產生一個 speaker。參考# ChatTTS的进阶用法,手把手带你实现个性化配音,音色、语速、停顿,口语 的說明,speaker 是一個 768 維度的向量,所以可以儲存後,然後複製到程式內,就可以固定音色。

例如可用以下的 speaker 向量,產生一個女性音色的語音。

speaker_vector = '-4.741,0.419,-3.355,3.652,-1.682,-1.254,9.719,1.436,0.871,12.334,-0.175,-2.653,-3.132,0.525,1.573,-0.351,0.030,-3.154,0.935,-0.111,-6.306,-1.840,-0.818,9.773,-1.842,-3.433,-6.200,-4.311,1.162,1.023,11.552,2.769,-2.408,-1.494,-1.143,12.412,0.832,-1.203,5.425,-1.481,0.737,-1.487,6.381,5.821,0.599,6.186,5.379,-2.141,0.697,5.005,-4.944,0.840,-4.974,0.531,-0.679,2.237,4.360,0.438,2.029,1.647,-2.247,-1.716,6.338,1.922,0.731,-2.077,0.707,4.959,-1.969,5.641,2.392,-0.953,0.574,1.061,-9.335,0.658,-0.466,4.813,1.383,-0.907,5.417,-7.383,-3.272,-1.727,2.056,1.996,2.313,-0.492,3.373,0.844,-8.175,-0.558,0.735,-0.921,8.387,-7.800,0.775,1.629,-6.029,0.709,-2.767,-0.534,2.035,2.396,2.278,2.584,3.040,-6.845,7.649,-2.812,-1.958,8.794,2.551,3.977,0.076,-2.073,-4.160,0.806,3.798,-1.968,-4.690,5.702,-4.376,-2.396,1.368,-0.707,4.930,6.926,1.655,4.423,-1.482,-3.670,2.988,-3.296,0.767,3.306,1.623,-3.604,-2.182,-1.480,-2.661,-1.515,-2.546,3.455,-3.500,-3.163,-1.376,-12.772,1.931,4.422,6.434,-0.386,-0.704,-2.720,2.177,-0.666,12.417,4.228,0.823,-1.740,1.285,-2.173,-4.285,-6.220,2.479,3.135,-2.790,1.395,0.946,-0.052,9.148,-2.802,-5.604,-1.884,1.796,-0.391,-1.499,0.661,-2.691,0.680,0.848,3.765,0.092,7.978,3.023,2.450,-15.073,5.077,3.269,2.715,-0.862,2.187,13.048,-7.028,-1.602,-6.784,-3.143,-1.703,1.001,-2.883,0.818,-4.012,4.455,-1.545,-14.483,-1.008,-3.995,2.366,3.961,1.254,-0.458,-1.175,2.027,1.830,2.682,0.131,-1.839,-28.123,-1.482,16.475,2.328,-13.377,-0.980,9.557,0.870,-3.266,-3.214,3.577,2.059,1.676,-0.621,-6.370,-2.842,0.054,-0.059,-3.179,3.182,3.411,4.419,-1.688,-0.663,-5.189,-5.542,-1.146,2.676,2.224,-5.519,6.069,24.349,2.509,4.799,0.024,-2.849,-1.192,-16.989,1.845,6.337,-1.936,-0.585,1.691,-3.564,0.931,0.223,4.314,-2.609,0.544,-1.931,3.604,1.248,-0.852,2.991,-1.499,-3.836,1.774,-0.744,0.824,7.597,-1.538,-0.009,0.494,-2.253,-1.293,-0.475,-3.816,8.165,0.285,-3.348,3.599,-4.959,-1.498,-1.492,-0.867,0.421,-2.191,-1.627,6.027,3.667,-21.459,2.594,-2.997,5.076,0.197,-3.305,3.998,1.642,-6.221,3.177,-3.344,5.457,0.671,-2.765,-0.447,1.080,2.504,1.809,1.144,2.752,0.081,-3.700,0.215,-2.199,3.647,1.977,1.326,3.086,34.789,-1.017,-14.257,-3.121,-0.568,-0.316,11.455,0.625,-6.517,-0.244,-8.490,9.220,0.068,-2.253,-1.485,3.372,2.002,-3.357,3.394,1.879,16.467,-2.271,1.377,-0.611,-5.875,1.004,12.487,2.204,0.115,-4.908,-6.992,-1.821,0.211,0.540,1.239,-2.488,-0.411,2.132,2.130,0.984,-10.669,-7.456,0.624,-0.357,7.948,2.150,-2.052,3.772,-4.367,-11.910,-2.094,3.987,-1.565,0.618,1.152,1.308,-0.807,1.212,-4.476,0.024,-6.449,-0.236,5.085,1.265,-0.586,-2.313,3.642,-0.766,3.626,6.524,-1.686,-2.524,-0.985,-6.501,-2.558,0.487,-0.662,-1.734,0.275,-9.230,-3.785,3.031,1.264,15.340,2.094,1.997,0.408,9.130,0.578,-2.239,-1.493,11.034,2.201,6.757,3.432,-4.133,-3.668,2.099,-6.798,-0.102,2.348,6.910,17.910,-0.779,4.389,1.432,-0.649,5.115,-1.064,3.580,4.129,-4.289,-2.387,-0.327,-1.975,-0.892,5.327,-3.908,3.639,-8.247,-1.876,-10.866,2.139,-3.932,-0.031,-1.444,0.567,-5.543,-2.906,1.399,-0.107,-3.044,-4.660,-1.235,-1.011,9.577,2.294,6.615,-1.279,-2.159,-3.050,-6.493,-7.282,-8.546,5.393,2.050,10.068,3.494,8.810,2.820,3.063,0.603,1.965,2.896,-3.049,7.106,-0.224,-1.016,2.531,-0.902,1.436,-1.843,1.129,6.746,-2.184,0.801,-0.965,-7.555,-18.409,6.176,-3.706,2.261,4.158,-0.928,2.164,-3.248,-4.892,-0.008,-0.521,7.931,-10.693,4.320,-0.841,4.446,-1.591,-0.702,4.075,3.323,-3.406,-1.198,-5.518,-0.036,-2.247,-2.638,2.160,-9.644,-3.858,2.402,-2.640,1.683,-0.961,-3.076,0.226,5.106,0.712,0.669,2.539,-4.340,-0.892,0.732,0.775,-2.757,4.365,-2.368,5.368,0.342,-0.655,0.240,0.775,3.686,-4.008,16.296,4.973,1.851,4.747,0.652,-2.117,6.470,2.189,-8.467,3.236,3.745,-1.332,3.583,-2.504,5.596,-2.440,0.995,-2.267,-3.322,3.490,1.156,1.716,0.669,-3.640,-1.709,5.055,6.265,-3.963,2.863,14.129,5.180,-3.590,0.393,0.234,-3.978,6.946,-0.521,1.925,-1.497,-0.283,0.895,-3.969,5.338,-1.808,-3.578,2.699,2.728,-0.895,-2.175,-2.717,2.574,4.571,1.131,2.187,3.620,-0.388,-3.685,0.979,2.731,-2.164,1.628,-1.006,-7.766,-11.033,-10.985,-2.413,-1.967,0.790,0.826,-1.623,-1.783,3.021,1.598,-0.931,-0.605,-1.684,1.408,-2.771,-2.354,5.564,-2.296,-4.774,-2.830,-5.149,2.731,-3.314,-1.002,3.522,3.235,-1.598,1.923,-2.755,-3.900,-3.519,-1.673,-2.049,-10.404,6.773,1.071,0.247,1.120,-0.794,2.187,-0.189,-5.591,4.361,1.772,1.067,1.895,-5.649,0.946,-2.834,-0.082,3.295,-7.659,-0.128,2.077,-1.638,0.301,-0.974,4.331,11.711,4.199,1.545,-3.236,-4.404,-1.333,0.623,1.414,-0.240,-0.816,-0.808,-1.382,0.632,-5.238,0.120,10.634,-2.026,1.702,-0.469,1.252,1.173,3.015,-8.798,1.633,-5.323,2.149,-6.481,11.635,3.072,5.642,5.252,4.702,-3.523,-0.594,4.150,1.392,0.554,-4.377,3.646,-0.884,1.468,0.779,2.372,-0.101,-5.702,0.539,-0.440,5.149,-0.011,-1.899,-1.349,-0.355,0.076,-0.100,-0.004,5.346,6.276,0.966,-3.138,-2.633,-3.124,3.606,-3.793,-3.332,2.359,-0.739,-3.301,-2.775,-0.491,3.283,-1.394,-1.883,1.203,1.097,2.233,2.170,-2.980,-15.800,-6.791,-0.175,-4.600,-3.840,-4.179,6.568,5.935,-0.431,4.623,4.601,-1.726,0.410,2.591,4.016,8.169,1.763,-3.058,-1.340,6.276,4.682,-0.089,1.301,-4.817' # 768维向量
rand_spk = torch.tensor([float(x) for x in speaker_vector.split(',')])

ChatTTS ### 稳定音色查找与音色可查詢並下載已經測試過提供的音色檔案,例如,下載一個音色檔案:seed_2147_restored_emb.pt 後,用以下程式載入該音色檔案,因為是使用 CPU,沒有 GPU,故需要加上 map_location=torch.device('cpu')

rand_spk = torch.load('seed_2147_restored_emb.pt', map_location=torch.device('cpu'))

特效

增加笑聲或停頓,要在文字中間加上

  • [laugh] 代表笑聲

  • [uv_break] 代表停頓

通過 params_refine_text 中的 prompt 參數可以控制笑聲和停頓的強度:

笑聲:laugh_(0-2):laugh_0、laugh_1、laugh_2(笑聲愈加強烈) 停頓:break_(0-7):break_0 至 break_7(停頓逐漸明顯)

實際合成語音時,會自動加上一些停頓,可加上 skip_refine_text=True,強制以原始的方式合成

chat.infer([text], skip_refine_text=True, params_refine_text={"prompt": '[oral_2][laugh_0][break_6]'})

References

ChatTTS: Text-to-Speech For Chat

ChatTTS使用demo示例(包含长文本生成语音、固定音色pt文件)

# ChatTTS使用技巧:如何精细化控制语气、音色、语速 附一键整合包!

## ChatTTS归因分析-音色性别转换方法

# 揭秘ChatTTS:高可控语音合成神器上手实录 带你玩转ChatTTS!

2026/1/19

CRDT

CDRT conflict-free replicated data type 無衝突複製資料類型是一種可以在網路中的多台電腦上複製的資料結構,每一個副本可獨立各自更新,不需要在副本之間進行協調,已透過數學確認可解決可能出現的不一致問題。

對於不同節點上的共用資料,如果在某一個節點上更新了資料,會產生不一致性,如果節點之間沒有協調更新的權限而產生資料衝突,就可能需要放棄一部分更新,以達到最終的一致性,分散式計算都集中在如何防止複製資料的並行更新問題上。

另一種方式 Optimistic Replication,是讓各節點都能做更新,允許所有更新都能同時執行,然後再來考慮如何合併解決衝突問題,CRDT 是一種資料結構,可在不同的副本上進行更新,最後透過合併解決衝突問題。

CRDT 種類

基於狀態的 CRDT 比較容易實作,但需要每個 CRDT 都要將狀態傳給其他副本,傳輸資料耗費較大,基於操作的 CRDT 只需要傳送更新的動作記錄。

基於操作的 CRDT

也稱為 交換性複製資料類型(commutative replicated data types,或CmRDT)

只傳輸更新操作來傳播狀態,必須確保操作過程都有傳遞給每一個資料副本,可不依照順序,但不能重複。

基於狀態的 CRDT

被稱為收斂複製資料類型(convergent replicated data types,或CvRDT)

必須將完整的本地端狀態,重送給其他副本。

State-Based LWW-Element-Graph CRDT

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation 這是一個簡易的 java library,實作了 CvRDT,用 graph 方式,連結不同的節點。

當兩個節點沒有連線時,就是 split-brain 狀態,CRDT 可在節點連接後,讓不同的副本資料合併達到一致性。

這邊測試四種演算法

  • Grow-Only Set:會一直不斷增加的 Set,最終不同節點的 Set 會合併再一起

  • Increment-Only Counter: 結果會是所有節點的加總總和

  • PN Counter:結果是所有節點的 加總總和 - 減法總和

  • Last-Writer-Wins Register:在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄

<!-- Experimental CRDT-implementations for the JVM -->
        <dependency>
            <groupId>com.netopyr.wurmloch</groupId>
            <artifactId>wurmloch-crdt</artifactId>
            <version>0.1.0</version>
        </dependency>
import com.netopyr.wurmloch.crdt.GCounter;
import com.netopyr.wurmloch.crdt.GSet;
import com.netopyr.wurmloch.crdt.LWWRegister;
import com.netopyr.wurmloch.crdt.PNCounter;
import com.netopyr.wurmloch.store.LocalCrdtStore;
import org.junit.Test;

import static org.junit.Assert.*;

public class CRDTTest {
    @Test
    public void gset() {
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GSet<String> replica1 = crdtStore1.createGSet("fruit");
        GSet<String> replica2 = crdtStore2.<String>findGSet("fruit").get();

        replica1.add("apple");
        replica2.add("banana");

        // 確認replica1, replica2 都有 "apple" 及  “”banana
        assertTrue( replica1.contains("apple") );
        assertTrue( replica1.contains("banana") );
        assertTrue( replica2.contains("apple") );
        assertTrue( replica2.contains("banana") );

        // 刻意斷線
        crdtStore1.disconnect(crdtStore2);
        // 異動 replica1, replica2
        replica1.add("strawberry");
        replica2.add("pear");

        assertTrue( replica1.contains("strawberry") );
        assertFalse( replica2.contains("strawberry") );
        assertFalse( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );

        // 連線
        crdtStore1.connect(crdtStore2);

        assertTrue( replica1.contains("strawberry") );
        assertTrue( replica2.contains("strawberry") );
        assertTrue( replica1.contains("pear") );
        assertTrue( replica2.contains("pear") );
    }

    @Test
    public void gcounter() {
        // 結果會是所有節點的 sum
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        GCounter replica1 = crdtStore1.createGCounter("counter");
        GCounter replica2 = crdtStore2.findGCounter("counter").get();

        replica1.increment();
        replica2.increment(2L);

        assertEquals(3L, replica1.get());
        assertEquals(3L, replica2.get());

        // 斷線
        crdtStore1.disconnect(crdtStore2);

        replica1.increment(3L);
        replica2.increment(5L);

        assertEquals(6L, replica1.get());
        assertEquals(8L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);
        assertEquals(11L, replica1.get());
        assertEquals(11L, replica2.get());
    }

    @Test
    public void pncounter() {
        // 結果是所有節點的 加總總和 - 減法總和
        LocalCrdtStore crdtStore1 = new LocalCrdtStore();
        LocalCrdtStore crdtStore2 = new LocalCrdtStore();
        crdtStore1.connect(crdtStore2);

        PNCounter replica1 = crdtStore1.createPNCounter("pncounter");
        PNCounter replica2 = crdtStore2.findPNCounter("pncounter").get();

        replica1.increment();
        replica2.decrement(2L);

        assertEquals(-1L, replica1.get());
        assertEquals(-1L, replica2.get());

        // disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.decrement(3L);
        replica2.increment(5L);

        assertEquals(-4L, replica1.get());
        assertEquals(4L, replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        assertEquals(1L, replica1.get());
        assertEquals(1L, replica2.get());
    }

    @Test
    public void lwwregister() {
        // 在合併時,只會保留最新的異動內容。同時發生的操作會被丟棄
        LocalCrdtStore crdtStore1 = new LocalCrdtStore("crdt");
        LocalCrdtStore crdtStore2 = new LocalCrdtStore("crdt");
        crdtStore1.connect(crdtStore2);

        LWWRegister<String> replica1 = crdtStore1.createLWWRegister("lwwregister");
        LWWRegister<String> replica2 = crdtStore2.<String>findLWWRegister("lwwregister").get();

        replica1.set("apple");
        replica2.set("banana");

        assertEquals("banana", replica1.get());
        assertEquals("banana", replica2.get());

        //disconnect
        crdtStore1.disconnect(crdtStore2);

        replica1.set("strawberry");
        replica2.set("pear");

        assertEquals("strawberry", replica1.get());
        assertEquals("pear", replica2.get());

        // connect
        crdtStore1.connect(crdtStore2);

        // buggy:應該要是 pear
        assertEquals("strawberry", replica1.get());
        assertEquals("strawberry", replica2.get());
    }
}

References

無衝突複製資料類型 - 維基百科,自由的百科全書

# CRDT — 將非同步資料整合

CRDT - HackMD

Introduction to Conflict-Free Replicated Data Types | Baeldung

GitHub - juliuskrah/crdt-java: A minimal CRDT implementation

2026/1/12

JavaFX 08 webviewJavaFX 08 webview

WebView 是一個可以嵌入網頁瀏覽功能的元件,使用內建的 WebKit 引擎來顯示 HTML 內容(包括 JavaScript、CSS 等)。這對於桌面應用程式需要嵌入網頁內容(如:顯示文件、使用 Web UI 元件)時非常有用。

但該 WebKit engine 不是最新版本,雖然支援 html5/css/javascript,但不支援新的標準,不支援多個分頁,沒有 js console 開發者工具,無法使用 react/vue 這些框架,不能跟 javafx node 直接互動(無法將 node 插入 html DOM)。

webview 裡面的頁面跟 js,可以跟 java code 互動

以下是一個雙向互動的 sample

js 可可

package javafx.webview;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebEngine;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import netscape.javascript.JSObject;

public class WebViewDemo extends Application {

    @Override
    public void start(Stage primaryStage) {
        WebView webView = new WebView();
        WebEngine webEngine = webView.getEngine();

        // 載入 HTML 字串
        String html = """
            <html>
            <head>
                <script>
                    function callJava() {
                        javaApp.showMessage("Hello from JavaScript!");
                    }

                    function fromJava(data) {
                        document.getElementById("result").innerText = "Java says: " + data;
                    }
                </script>
            </head>
            <body>
                <h2>JavaFX WebView 雙向互動</h2>
                <button onclick="callJava()">呼叫 Java 方法</button>
                <p id="result"></p>
            </body>
            </html>
            """;

        webEngine.loadContent(html);

        // 頁面載入完成後建立 bridge
        webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> {
            if (newState == javafx.concurrent.Worker.State.SUCCEEDED) {
                JSObject window = (JSObject) webEngine.executeScript("window");
                window.setMember("javaApp", new JavaBridge(webEngine));
            }
        });

        BorderPane root = new BorderPane(webView);
        Scene scene = new Scene(root, 800, 600);
        primaryStage.setScene(scene);
        primaryStage.setTitle("JavaFX WebView 雙向互動範例");
        primaryStage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    // Java Bridge 類別
    public static class JavaBridge {
        private final WebEngine webEngine;

        public JavaBridge(WebEngine webEngine) {
            this.webEngine = webEngine;
        }

        // JS 呼叫這個方法
        public void showMessage(String msg) {
            System.out.println("JavaScript 傳來訊息: " + msg);
            // Java 反呼叫 JS 的 function
            webEngine.executeScript("fromJava('收到: " + msg + "')");
        }
    }
}

2026/1/5

JavaFX 07 chart

用 javafx 繪製圖表的工具

  • 圓餅圖 PieChart 無需軸
  • 折線圖 LineChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 面積圖 AreaChart<X,Y> 同折線圖
  • 長條圖 BarChart<X,Y> 通常 CategoryAxis/NumberAxis
  • 氣泡圖 BubbleChart<Number,Number> 用第三值表示半徑
  • 散點圖 ScatterChart<Number,Number> 點陣圖
  • 累積面積圖 StackedAreaChart<X,Y> 多組堆疊數據區域
  • 累積長條圖 StackedBarChart<X,Y> 長條堆疊比較
package javafx.chart;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.*;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;

//圖表類型    JavaFX 類別名稱    X/Y 軸類型
//圓餅圖    PieChart    無需軸
//折線圖    LineChart<X,Y>    通常 CategoryAxis/NumberAxis
//面積圖    AreaChart<X,Y>    同折線圖
//長條圖    BarChart<X,Y>    通常 CategoryAxis/NumberAxis
//氣泡圖    BubbleChart<Number,Number>    用第三值表示半徑
//散點圖    ScatterChart<Number,Number>    點陣圖
//累積面積圖    StackedAreaChart<X,Y>    多組堆疊數據區域
//累積長條圖    StackedBarChart<X,Y>    長條堆疊比較

public class AllChartsDemo extends Application {

    private BorderPane root;
    private ComboBox<String> chartSelector;

    @Override
    public void start(Stage primaryStage) {
        root = new BorderPane();
        root.setPadding(new Insets(10));

        chartSelector = new ComboBox<>();
        chartSelector.getItems().addAll(
                "PieChart", "LineChart", "AreaChart", "BarChart",
                "BubbleChart", "ScatterChart", "StackedAreaChart", "StackedBarChart"
        );
        chartSelector.setValue("PieChart");
        chartSelector.setOnAction(e -> updateChart(chartSelector.getValue()));

        root.setTop(chartSelector);
        updateChart(chartSelector.getValue());

        Scene scene = new Scene(root, 800, 600);
        primaryStage.setTitle("JavaFX Chart Selector Demo");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    private void updateChart(String type) {
        switch (type) {
            case "PieChart":
                PieChart pieChart = new PieChart();
                pieChart.setTitle("Pie Chart");
                pieChart.getData().addAll(
                        new PieChart.Data("Java", 30),
                        new PieChart.Data("Python", 25),
                        new PieChart.Data("C++", 20),
                        new PieChart.Data("Kotlin", 15),
                        new PieChart.Data("Other", 10)
                );
                root.setCenter(pieChart);
                break;

            case "LineChart":
                CategoryAxis x1 = new CategoryAxis();
                NumberAxis y1 = new NumberAxis();
                LineChart<String, Number> lineChart = new LineChart<>(x1, y1);
                lineChart.setTitle("Line Chart");
                XYChart.Series<String, Number> lineSeries = new XYChart.Series<>();
                lineSeries.setName("Sales");
                lineSeries.getData().add(new XYChart.Data<>("Mon", 10));
                lineSeries.getData().add(new XYChart.Data<>("Tue", 20));
                lineSeries.getData().add(new XYChart.Data<>("Wed", 15));
                lineSeries.getData().add(new XYChart.Data<>("Thu", 25));
                lineSeries.getData().add(new XYChart.Data<>("Fri", 18));
                lineChart.getData().add(lineSeries);
                root.setCenter(lineChart);
                break;

            case "AreaChart":
                AreaChart<String, Number> areaChart = new AreaChart<>(new CategoryAxis(), new NumberAxis());
                areaChart.setTitle("Area Chart");
                XYChart.Series<String, Number> areaSeries = new XYChart.Series<>();
                areaSeries.setName("Traffic");
                areaSeries.getData().add(new XYChart.Data<>("Jan", 200));
                areaSeries.getData().add(new XYChart.Data<>("Feb", 300));
                areaSeries.getData().add(new XYChart.Data<>("Mar", 150));
                areaSeries.getData().add(new XYChart.Data<>("Apr", 250));
                areaChart.getData().add(areaSeries);
                root.setCenter(areaChart);
                break;

            case "BarChart":
                BarChart<String, Number> barChart = new BarChart<>(new CategoryAxis(), new NumberAxis());
                barChart.setTitle("Bar Chart");
                XYChart.Series<String, Number> barSeries = new XYChart.Series<>();
                barSeries.setName("Downloads");
                barSeries.getData().add(new XYChart.Data<>("Chrome", 60));
                barSeries.getData().add(new XYChart.Data<>("Firefox", 40));
                barSeries.getData().add(new XYChart.Data<>("Edge", 30));
                barChart.getData().add(barSeries);
                root.setCenter(barChart);
                break;

            case "BubbleChart":
                BubbleChart<Number, Number> bubbleChart = new BubbleChart<>(new NumberAxis(), new NumberAxis());
                bubbleChart.setTitle("Bubble Chart");
                XYChart.Series<Number, Number> bubbleSeries = new XYChart.Series<>();
                bubbleSeries.setName("Bubbles");
                bubbleSeries.getData().add(new XYChart.Data<>(10, 20, 5));
                bubbleSeries.getData().add(new XYChart.Data<>(15, 30, 10));
                bubbleSeries.getData().add(new XYChart.Data<>(25, 10, 7));
                bubbleChart.getData().add(bubbleSeries);
                root.setCenter(bubbleChart);
                break;

            case "ScatterChart":
                ScatterChart<Number, Number> scatterChart = new ScatterChart<>(new NumberAxis(), new NumberAxis());
                scatterChart.setTitle("Scatter Chart");
                XYChart.Series<Number, Number> scatterSeries = new XYChart.Series<>();
                scatterSeries.setName("Points");
                scatterSeries.getData().add(new XYChart.Data<>(5, 20));
                scatterSeries.getData().add(new XYChart.Data<>(10, 40));
                scatterSeries.getData().add(new XYChart.Data<>(15, 25));
                scatterChart.getData().add(scatterSeries);
                root.setCenter(scatterChart);
                break;

            case "StackedAreaChart":
                StackedAreaChart<String, Number> stackedAreaChart = new StackedAreaChart<>(new CategoryAxis(), new NumberAxis());
                stackedAreaChart.setTitle("Stacked Area Chart");
                XYChart.Series<String, Number> sa1 = new XYChart.Series<>();
                sa1.setName("Team A");
                sa1.getData().add(new XYChart.Data<>("Q1", 100));
                sa1.getData().add(new XYChart.Data<>("Q2", 120));
                sa1.getData().add(new XYChart.Data<>("Q3", 90));
                sa1.getData().add(new XYChart.Data<>("Q4", 110));
                XYChart.Series<String, Number> sa2 = new XYChart.Series<>();
                sa2.setName("Team B");
                sa2.getData().add(new XYChart.Data<>("Q1", 80));
                sa2.getData().add(new XYChart.Data<>("Q2", 95));
                sa2.getData().add(new XYChart.Data<>("Q3", 70));
                sa2.getData().add(new XYChart.Data<>("Q4", 85));
                stackedAreaChart.getData().addAll(sa1, sa2);
                root.setCenter(stackedAreaChart);
                break;

            case "StackedBarChart":
                StackedBarChart<String, Number> stackedBarChart = new StackedBarChart<>(new CategoryAxis(), new NumberAxis());
                stackedBarChart.setTitle("Stacked Bar Chart");
                XYChart.Series<String, Number> sb1 = new XYChart.Series<>();
                sb1.setName("Male");
                sb1.getData().add(new XYChart.Data<>("2021", 60));
                sb1.getData().add(new XYChart.Data<>("2022", 70));
                XYChart.Series<String, Number> sb2 = new XYChart.Series<>();
                sb2.setName("Female");
                sb2.getData().add(new XYChart.Data<>("2021", 40));
                sb2.getData().add(new XYChart.Data<>("2022", 55));
                stackedBarChart.getData().addAll(sb1, sb2);
                root.setCenter(stackedBarChart);
                break;
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}