playlist-admin 2.15.17 → 2.15.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,100 +1,100 @@
1
- # playlist-admin
2
-
3
- A Flutter desktop app for Spotify-native music library management: browse the
4
- Spotify home feed, search & stream, playlist sync, batch downloads, and stats.
5
-
6
- ## Features
7
-
8
- - **Spotify 原生整合** — 個人化主頁 / 搜尋串流 / 你的歌單(內部 GQL API,WebView 登入)
9
- - **串流播放** — Spotify 搜尋 → yt-dlp 解析 → ffmpeg VBR 0 轉碼 → 本地 HTTP 串流(可選快取 cache\stream\)
10
- - **原生下載** — yt-dlp 256k Opus → mp3 VBR 0(`preferredquality: 0`),批量一鍵補全
11
- - **播放清單同步** — Spotify embed API 抓取 → m3u8 + 自動下載缺漏曲目
12
- - **工作列媒體控制器** — SMTC 進度條 + 拖動 seek + 播放/暫停/上下首
13
- - **播放佇列** — 拖曳排序 / 移除 / 跳轉 / play next,重啟還原(cache\queue.json)
14
- - **播放統計** — 50%/4min scrobble 規則 → Top 曲目 / 聆聽分鐘 / 最近播放
15
- - **下載統計** — 每輪下載成功率紀錄(cache\downloads_log.json)
16
- - **Sleep timer / Crossfade / 全域熱鍵**(Space、←/→ seek、N/P 切歌)
17
- - **Podcast + RAG** — RSS 下載、Groq 轉錄、Ollama chroma 問答
18
- - **Discord RPC** — 現播歌曲 + 封面
19
- - **一起聽(Jam)** — 免費版 Spotify Jam:開房間(免註冊/免外部伺服器),
20
- 同一個 Wi-Fi 下大家加入,共享播放佇列、即時同步播放進度、投票跳歌、
21
- 聊天室。手機(Android/iOS)也能加入房主一起聽。
22
-
23
- ## 資料夾結構(`Music\playlist-admin\`)
24
-
25
- ```
26
- music\ ← 唯一音樂庫(mp3)
27
- playlists\ ← m3u8
28
- lyrics\ ← LRC
29
- podcasts\ ← 播客音檔
30
- cache\spotify ← Spotify 快取 / snapshot / session
31
- cache\podcast ← 播客處理快取 / chroma_db
32
- cache\lufs ← LUFS 快取
33
- cache\stream ← 串流快取
34
- cache\thumbnails
35
- exports\ ← USB 匯出
36
- logs\ ← 日誌
37
- tools\ ← bridge 腳本
38
- config.json ← 設定
39
- ```
40
-
41
- ## Build
42
-
43
- ```bash
44
- flutter pub get
45
- flutter build windows --release # 桌面版
46
- flutter build apk --release # Android(手機版)
47
- flutter build ios --release # iOS(需 macOS + Xcode)
48
- ```
49
-
50
- ## 一起聽(Jam)
51
-
52
- 在側邊欄選「一起聽」:
53
-
54
- 1. **建立房間** — 輸入暱稱 →「建立新房間」。房主端會自動開啟
55
- WebSocket + 音訊串流伺服器,並顯示房主位址(`IP:埠口`)與 6 位房間代碼。
56
- 2. **加入房間** — 對方輸入房主位址 + 房間代碼 + 暱稱即可加入
57
- (手機/桌機皆可;手機需與房主連同一個 Wi-Fi)。
58
- 3. 任何人都能**搜尋 Spotify 加歌**(房主需先在本機「搜尋」頁登入 Spotify)、
59
- **投票**(上下箭頭排序)、**投票跳歌**(達半數跳過)、**聊天**。
60
- 播放進度由房主掌控,成員每幾秒做 drift 校正,確保同步。
61
-
62
- 技術重點:
63
-
64
- - 房主是「伺服器」:`lib/services/jam_service.dart` 用 `dart:io` 自架
65
- HttpServer + WebSocket,成員連 `ws://<ip>:<port>/jam/ws`。
66
- - 歌曲來源:本機音樂庫(`local`,`/jam/audio` 支援 Range 串流)或
67
- Spotify 搜尋後由 yt-dlp 串流(`stream`,走既有的 `StreamServer`)。
68
- - 成員端透過 `PlayerController.playJamUrl()` 播放房主提供的 URL,
69
- `jamFollowMode` 讓播放/暫停/上下首/seek 指令自動轉送給房主。
70
- - 手機相容:Windows 專用程式碼(win32 單一實例、SMTC、WebView 登入)
71
- 都已加平台守衛;Android/iOS 已加入 `flutter create` 平台資料夾與
72
- 網路權限(INTERNET / cleartext / 本地網路描述)。
73
-
74
- ## CLI
75
-
76
- ```bash
77
- playlist-admin pipeline [--step N] # 全管線(抓取→下載→修剪→整理→LUFS)
78
- playlist-admin podcast # Podcast 管線
79
- playlist-admin status # 狀態
80
- playlist-admin favorite list|toggle # 我的最愛
81
- playlist-admin rag build|query # Podcast RAG
82
- ```
83
-
84
- ## Release
85
-
86
- Push a `v*` tag to trigger GitHub Actions:
87
-
88
- ```bash
89
- git tag v2.9.0
90
- git push origin v2.9.0
91
- ```
92
-
93
- ## Dependencies
94
-
95
- - `http` + `html` — Spotify embed API
96
- - `crypto` — TOTP(Spotify token)
97
- - `webview_windows`(vendor)— 登入 cookie 擷取
98
- - `audioplayers` — 播放
99
- - `fl_chart` — 圖表
1
+ # playlist-admin
2
+
3
+ A Flutter desktop app for Spotify-native music library management: browse the
4
+ Spotify home feed, search & stream, playlist sync, batch downloads, and stats.
5
+
6
+ ## Features
7
+
8
+ - **Spotify 原生整合** — 個人化主頁 / 搜尋串流 / 你的歌單(內部 GQL API,WebView 登入)
9
+ - **串流播放** — Spotify 搜尋 → yt-dlp 解析 → ffmpeg VBR 0 轉碼 → 本地 HTTP 串流(可選快取 cache\stream\)
10
+ - **原生下載** — yt-dlp 256k Opus → mp3 VBR 0(`preferredquality: 0`),批量一鍵補全
11
+ - **播放清單同步** — Spotify embed API 抓取 → m3u8 + 自動下載缺漏曲目
12
+ - **工作列媒體控制器** — SMTC 進度條 + 拖動 seek + 播放/暫停/上下首
13
+ - **播放佇列** — 拖曳排序 / 移除 / 跳轉 / play next,重啟還原(cache\queue.json)
14
+ - **播放統計** — 50%/4min scrobble 規則 → Top 曲目 / 聆聽分鐘 / 最近播放
15
+ - **下載統計** — 每輪下載成功率紀錄(cache\downloads_log.json)
16
+ - **Sleep timer / Crossfade / 全域熱鍵**(Space、←/→ seek、N/P 切歌)
17
+ - **Podcast + RAG** — RSS 下載、Groq 轉錄、Ollama chroma 問答
18
+ - **Discord RPC** — 現播歌曲 + 封面
19
+ - **一起聽(Jam)** — 免費版 Spotify Jam:開房間(免註冊/免外部伺服器),
20
+ 同一個 Wi-Fi 下大家加入,共享播放佇列、即時同步播放進度、投票跳歌、
21
+ 聊天室。手機(Android/iOS)也能加入房主一起聽。
22
+
23
+ ## 資料夾結構(`Music\playlist-admin\`)
24
+
25
+ ```
26
+ music\ ← 唯一音樂庫(mp3)
27
+ playlists\ ← m3u8
28
+ lyrics\ ← LRC
29
+ podcasts\ ← 播客音檔
30
+ cache\spotify ← Spotify 快取 / snapshot / session
31
+ cache\podcast ← 播客處理快取 / chroma_db
32
+ cache\lufs ← LUFS 快取
33
+ cache\stream ← 串流快取
34
+ cache\thumbnails
35
+ exports\ ← USB 匯出
36
+ logs\ ← 日誌
37
+ tools\ ← bridge 腳本
38
+ config.json ← 設定
39
+ ```
40
+
41
+ ## Build
42
+
43
+ ```bash
44
+ flutter pub get
45
+ flutter build windows --release # 桌面版
46
+ flutter build apk --release # Android(手機版)
47
+ flutter build ios --release # iOS(需 macOS + Xcode)
48
+ ```
49
+
50
+ ## 一起聽(Jam)
51
+
52
+ 在側邊欄選「一起聽」:
53
+
54
+ 1. **建立房間** — 輸入暱稱 →「建立新房間」。房主端會自動開啟
55
+ WebSocket + 音訊串流伺服器,並顯示房主位址(`IP:埠口`)與 6 位房間代碼。
56
+ 2. **加入房間** — 對方輸入房主位址 + 房間代碼 + 暱稱即可加入
57
+ (手機/桌機皆可;手機需與房主連同一個 Wi-Fi)。
58
+ 3. 任何人都能**搜尋 Spotify 加歌**(房主需先在本機「搜尋」頁登入 Spotify)、
59
+ **投票**(上下箭頭排序)、**投票跳歌**(達半數跳過)、**聊天**。
60
+ 播放進度由房主掌控,成員每幾秒做 drift 校正,確保同步。
61
+
62
+ 技術重點:
63
+
64
+ - 房主是「伺服器」:`lib/services/jam_service.dart` 用 `dart:io` 自架
65
+ HttpServer + WebSocket,成員連 `ws://<ip>:<port>/jam/ws`。
66
+ - 歌曲來源:本機音樂庫(`local`,`/jam/audio` 支援 Range 串流)或
67
+ Spotify 搜尋後由 yt-dlp 串流(`stream`,走既有的 `StreamServer`)。
68
+ - 成員端透過 `PlayerController.playJamUrl()` 播放房主提供的 URL,
69
+ `jamFollowMode` 讓播放/暫停/上下首/seek 指令自動轉送給房主。
70
+ - 手機相容:Windows 專用程式碼(win32 單一實例、SMTC、WebView 登入)
71
+ 都已加平台守衛;Android/iOS 已加入 `flutter create` 平台資料夾與
72
+ 網路權限(INTERNET / cleartext / 本地網路描述)。
73
+
74
+ ## CLI
75
+
76
+ ```bash
77
+ playlist-admin pipeline [--step N] # 全管線(抓取→下載→修剪→整理→LUFS)
78
+ playlist-admin podcast # Podcast 管線
79
+ playlist-admin status # 狀態
80
+ playlist-admin favorite list|toggle # 我的最愛
81
+ playlist-admin rag build|query # Podcast RAG
82
+ ```
83
+
84
+ ## Release
85
+
86
+ Push a `v*` tag to trigger GitHub Actions:
87
+
88
+ ```bash
89
+ git tag v2.9.0
90
+ git push origin v2.9.0
91
+ ```
92
+
93
+ ## Dependencies
94
+
95
+ - `http` + `html` — Spotify embed API
96
+ - `crypto` — TOTP(Spotify token)
97
+ - `webview_windows`(vendor)— 登入 cookie 擷取
98
+ - `audioplayers` — 播放
99
+ - `fl_chart` — 圖表
100
100
  - `flutter_discord_rpc` — Discord presence
package/cli/index.js CHANGED
@@ -1,128 +1,128 @@
1
- #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
-
6
- // Thin launcher: the CLI engine lives in the Flutter app binary
7
- // (lib/cli_main.dart), so GUI and CLI share one implementation.
8
- // This script just locates the built exe / rag scripts and forwards args.
9
-
10
- const HELP = `playlist-admin CLI
11
-
12
- Runs the same engine as the GUI app (shared Dart code). Requires a built
13
- release: flutter build windows --release
14
-
15
- Usage:
16
- playlist-admin pipeline [--step N] Run full pipeline (or from step N)
17
- playlist-admin podcast Run podcast pipeline
18
- playlist-admin status Show status
19
- playlist-admin favorite list List favorite songs
20
- playlist-admin favorite toggle <song> Toggle favorite (我的最愛)
21
- playlist-admin rag build [--reset] Build podcast RAG vector DB
22
- playlist-admin rag query "問題" [--topk N] [--show 節目] [--json]
23
- `;
24
-
25
- function projectRoot() {
26
- if (process.env.PA_ROOT && fs.existsSync(process.env.PA_ROOT)) {
27
- return process.env.PA_ROOT;
28
- }
29
- // Start from cwd and walk up to find the Flutter project (works whether
30
- // invoked locally or from a globally installed npm package).
31
- let dir = process.cwd();
32
- while (true) {
33
- if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) return dir;
34
- const parent = path.dirname(dir);
35
- if (parent === dir) return null;
36
- dir = parent;
37
- }
38
- }
39
-
40
- function exePath(root) {
41
- const candidates = [
42
- path.join(root, 'build', 'windows', 'x64', 'runner', 'Release', 'playlist-admin.exe'),
43
- path.join(root, 'build', 'windows', 'x64', 'runner', 'Debug', 'playlist-admin.exe'),
44
- path.join(root, 'build', 'windows', 'x64', 'runner', 'Release', 'playlist_administrator.exe'),
45
- ];
46
- return candidates.find((c) => fs.existsSync(c)) || null;
47
- }
48
-
49
- function python() {
50
- return process.env.PYTHON || 'python';
51
- }
52
-
53
- function forward(exe, args) {
54
- // Deliver CLI args via PA_CLI_ARGS (JSON) — Flutter Dart exposes no
55
- // command-line args in release on this SDK (see lib/main.dart).
56
- // PA_ROOT lets the Python bridge find rag/ scripts when invoked from the CLI.
57
- const root = projectRoot();
58
- const child = spawn(exe, [], {
59
- stdio: 'inherit',
60
- windowsHide: false,
61
- env: {
62
- ...process.env,
63
- PA_CLI_ARGS: JSON.stringify(args),
64
- ...(root ? { PA_ROOT: root } : {}),
65
- },
66
- });
67
- child.on('exit', (code) => process.exit(code ?? 1));
68
- child.on('error', (e) => {
69
- console.error(`無法啟動 ${exe}: ${e.message}`);
70
- process.exit(1);
71
- });
72
- }
73
-
74
- function forwardPy(args) {
75
- // Python scripts take real argv.
76
- const child = spawn(python(), args, { stdio: 'inherit', windowsHide: false });
77
- child.on('exit', (code) => process.exit(code ?? 1));
78
- child.on('error', (e) => {
79
- console.error(`無法啟動 python: ${e.message}`);
80
- process.exit(1);
81
- });
82
- }
83
-
84
- function runRag(args) {
85
- const root = projectRoot();
86
- if (!root) {
87
- console.error('找不到專案根目錄(pubspec.yaml)。請在專案內執行,或設定 PA_ROOT');
88
- process.exit(1);
89
- }
90
- const sub = args[0];
91
- const script = path.join(root, 'rag', sub === 'build' ? 'build_db.py' : 'query.py');
92
- if (!fs.existsSync(script)) {
93
- console.error(`找不到 ${script}`);
94
- process.exit(1);
95
- }
96
- forwardPy([script, ...args.slice(1)]);
97
- }
98
-
99
- async function main() {
100
- const args = process.argv.slice(2);
101
- if (args.length === 0) {
102
- console.log(HELP);
103
- return;
104
- }
105
- if (args[0] === 'rag') {
106
- runRag(args.slice(1));
107
- return;
108
- }
109
-
110
- const root = projectRoot();
111
- if (!root) {
112
- console.error('找不到專案根目錄(pubspec.yaml)。請在專案內執行,或設定 PA_ROOT');
113
- process.exit(1);
114
- }
115
- const exe = exePath(root);
116
- if (!exe) {
117
- console.error(`找不到 build 好的 exe。
118
- 請先在專案內執行: flutter build windows --release
119
- 或設定 PA_ROOT=<專案根目錄>`);
120
- process.exit(1);
121
- }
122
- forward(exe, args);
123
- }
124
-
125
- main().catch((e) => {
126
- console.error(e);
127
- process.exit(1);
128
- });
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+
6
+ // Thin launcher: the CLI engine lives in the Flutter app binary
7
+ // (lib/cli_main.dart), so GUI and CLI share one implementation.
8
+ // This script just locates the built exe / rag scripts and forwards args.
9
+
10
+ const HELP = `playlist-admin CLI
11
+
12
+ Runs the same engine as the GUI app (shared Dart code). Requires a built
13
+ release: flutter build windows --release
14
+
15
+ Usage:
16
+ playlist-admin pipeline [--step N] Run full pipeline (or from step N)
17
+ playlist-admin podcast Run podcast pipeline
18
+ playlist-admin status Show status
19
+ playlist-admin favorite list List favorite songs
20
+ playlist-admin favorite toggle <song> Toggle favorite (我的最愛)
21
+ playlist-admin rag build [--reset] Build podcast RAG vector DB
22
+ playlist-admin rag query "問題" [--topk N] [--show 節目] [--json]
23
+ `;
24
+
25
+ function projectRoot() {
26
+ if (process.env.PA_ROOT && fs.existsSync(process.env.PA_ROOT)) {
27
+ return process.env.PA_ROOT;
28
+ }
29
+ // Start from cwd and walk up to find the Flutter project (works whether
30
+ // invoked locally or from a globally installed npm package).
31
+ let dir = process.cwd();
32
+ while (true) {
33
+ if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) return dir;
34
+ const parent = path.dirname(dir);
35
+ if (parent === dir) return null;
36
+ dir = parent;
37
+ }
38
+ }
39
+
40
+ function exePath(root) {
41
+ const candidates = [
42
+ path.join(root, 'build', 'windows', 'x64', 'runner', 'Release', 'playlist-admin.exe'),
43
+ path.join(root, 'build', 'windows', 'x64', 'runner', 'Debug', 'playlist-admin.exe'),
44
+ path.join(root, 'build', 'windows', 'x64', 'runner', 'Release', 'playlist_administrator.exe'),
45
+ ];
46
+ return candidates.find((c) => fs.existsSync(c)) || null;
47
+ }
48
+
49
+ function python() {
50
+ return process.env.PYTHON || 'python';
51
+ }
52
+
53
+ function forward(exe, args) {
54
+ // Deliver CLI args via PA_CLI_ARGS (JSON) — Flutter Dart exposes no
55
+ // command-line args in release on this SDK (see lib/main.dart).
56
+ // PA_ROOT lets the Python bridge find rag/ scripts when invoked from the CLI.
57
+ const root = projectRoot();
58
+ const child = spawn(exe, [], {
59
+ stdio: 'inherit',
60
+ windowsHide: false,
61
+ env: {
62
+ ...process.env,
63
+ PA_CLI_ARGS: JSON.stringify(args),
64
+ ...(root ? { PA_ROOT: root } : {}),
65
+ },
66
+ });
67
+ child.on('exit', (code) => process.exit(code ?? 1));
68
+ child.on('error', (e) => {
69
+ console.error(`無法啟動 ${exe}: ${e.message}`);
70
+ process.exit(1);
71
+ });
72
+ }
73
+
74
+ function forwardPy(args) {
75
+ // Python scripts take real argv.
76
+ const child = spawn(python(), args, { stdio: 'inherit', windowsHide: false });
77
+ child.on('exit', (code) => process.exit(code ?? 1));
78
+ child.on('error', (e) => {
79
+ console.error(`無法啟動 python: ${e.message}`);
80
+ process.exit(1);
81
+ });
82
+ }
83
+
84
+ function runRag(args) {
85
+ const root = projectRoot();
86
+ if (!root) {
87
+ console.error('找不到專案根目錄(pubspec.yaml)。請在專案內執行,或設定 PA_ROOT');
88
+ process.exit(1);
89
+ }
90
+ const sub = args[0];
91
+ const script = path.join(root, 'rag', sub === 'build' ? 'build_db.py' : 'query.py');
92
+ if (!fs.existsSync(script)) {
93
+ console.error(`找不到 ${script}`);
94
+ process.exit(1);
95
+ }
96
+ forwardPy([script, ...args.slice(1)]);
97
+ }
98
+
99
+ async function main() {
100
+ const args = process.argv.slice(2);
101
+ if (args.length === 0) {
102
+ console.log(HELP);
103
+ return;
104
+ }
105
+ if (args[0] === 'rag') {
106
+ runRag(args.slice(1));
107
+ return;
108
+ }
109
+
110
+ const root = projectRoot();
111
+ if (!root) {
112
+ console.error('找不到專案根目錄(pubspec.yaml)。請在專案內執行,或設定 PA_ROOT');
113
+ process.exit(1);
114
+ }
115
+ const exe = exePath(root);
116
+ if (!exe) {
117
+ console.error(`找不到 build 好的 exe。
118
+ 請先在專案內執行: flutter build windows --release
119
+ 或設定 PA_ROOT=<專案根目錄>`);
120
+ process.exit(1);
121
+ }
122
+ forward(exe, args);
123
+ }
124
+
125
+ main().catch((e) => {
126
+ console.error(e);
127
+ process.exit(1);
128
+ });
package/package.json CHANGED
@@ -1,34 +1,34 @@
1
- {
2
- "name": "playlist-admin",
3
- "version": "2.15.17",
4
- "description": "playlist-admin CLI - thin launcher for the Flutter app engine (pipeline, podcast) + podcast RAG query",
5
- "type": "module",
6
- "bin": {
7
- "playlist-admin": "./cli/index.js"
8
- },
9
- "scripts": {
10
- "pipeline": "node cli/index.js pipeline",
11
- "podcast": "node cli/index.js podcast",
12
- "status": "node cli/index.js status",
13
- "test": "node cli/index.js status && node cli/index.js rag query \"test\" --topk 1 --json"
14
- },
15
- "files": [
16
- "cli/index.js",
17
- "rag/*.py",
18
- "rag/README.md"
19
- ],
20
- "keywords": [
21
- "playlist",
22
- "spotify",
23
- "podcast",
24
- "m3u8",
25
- "music",
26
- "ffmpeg",
27
- "spotube"
28
- ],
29
- "license": "MIT",
30
- "engines": {
31
- "node": ">=20"
32
- },
33
- "private": false
34
- }
1
+ {
2
+ "name": "playlist-admin",
3
+ "version": "2.15.18",
4
+ "description": "playlist-admin CLI - thin launcher for the Flutter app engine (pipeline, podcast) + podcast RAG query",
5
+ "type": "module",
6
+ "bin": {
7
+ "playlist-admin": "./cli/index.js"
8
+ },
9
+ "scripts": {
10
+ "pipeline": "node cli/index.js pipeline",
11
+ "podcast": "node cli/index.js podcast",
12
+ "status": "node cli/index.js status",
13
+ "test": "node cli/index.js status && node cli/index.js rag query \"test\" --topk 1 --json"
14
+ },
15
+ "files": [
16
+ "cli/index.js",
17
+ "rag/*.py",
18
+ "rag/README.md"
19
+ ],
20
+ "keywords": [
21
+ "playlist",
22
+ "spotify",
23
+ "podcast",
24
+ "m3u8",
25
+ "music",
26
+ "ffmpeg",
27
+ "spotube"
28
+ ],
29
+ "license": "MIT",
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "private": false
34
+ }
package/rag/README.md CHANGED
@@ -1,48 +1,48 @@
1
- # Podcast RAG
2
-
3
- 對 podcast 逐字稿做本地語意檢索(embeddings + ChromaDB)。
4
-
5
- ## 前置需求
6
-
7
- - 安裝 Ollama:`https://ollama.com/download`
8
- ```bash
9
- ollama pull bge-m3 # embedding 模型
10
- ollama pull qwen2.5:7b # (選用)本機生成
11
- ```
12
- - Python 套件:
13
- ```bash
14
- pip install chromadb requests
15
- ```
16
- - 逐字稿:`podcast_downloads/<節目>/*.txt`(podcast pipeline 會自動產生 .txt)
17
-
18
- ## 指令
19
-
20
- CLI(需已 build exe 或直接 python 執行):
21
-
22
- ```bash
23
- playlist-admin rag build # 增量索引(沒被 index 過的 .txt)
24
- playlist-admin rag build --reset # 全部重建
25
- playlist-admin rag query "2026年台股會一直漲嗎?" --topk 8
26
- playlist-admin rag query "問題" --json # JSON 含全文
27
- playlist-admin rag query "問題" --show 游庭皓 # 只搜指定節目
28
- ```
29
-
30
- 直接用 Python(路徑從 config.json 自動解析):
31
-
32
- ```bash
33
- python rag/build_db.py
34
- python rag/query.py "問題"
35
- python rag/srt_to_text.py # 把缺 .txt 的 .srt 補轉成 .txt
36
- ```
37
-
38
- ## Pipeline 整合
39
-
40
- podcast pipeline 產生/更新逐字稿後,跑 `playlist-admin rag build`
41
- 即可把新集數加進向量庫(程式會自動跳過已索引的檔名)。
42
-
43
- ## 檔案
44
-
45
- - `build_db.py` — 逐字稿切 chunk → Ollama embeddings → ChromaDB(cosine)
46
- - `query.py` — 問題 embedding → top-k 片段 → 輸出命中的節目/日期/全文偏移
47
- - `srt_to_text.py` — srt → 純文字
1
+ # Podcast RAG
2
+
3
+ 對 podcast 逐字稿做本地語意檢索(embeddings + ChromaDB)。
4
+
5
+ ## 前置需求
6
+
7
+ - 安裝 Ollama:`https://ollama.com/download`
8
+ ```bash
9
+ ollama pull bge-m3 # embedding 模型
10
+ ollama pull qwen2.5:7b # (選用)本機生成
11
+ ```
12
+ - Python 套件:
13
+ ```bash
14
+ pip install chromadb requests
15
+ ```
16
+ - 逐字稿:`podcast_downloads/<節目>/*.txt`(podcast pipeline 會自動產生 .txt)
17
+
18
+ ## 指令
19
+
20
+ CLI(需已 build exe 或直接 python 執行):
21
+
22
+ ```bash
23
+ playlist-admin rag build # 增量索引(沒被 index 過的 .txt)
24
+ playlist-admin rag build --reset # 全部重建
25
+ playlist-admin rag query "2026年台股會一直漲嗎?" --topk 8
26
+ playlist-admin rag query "問題" --json # JSON 含全文
27
+ playlist-admin rag query "問題" --show 游庭皓 # 只搜指定節目
28
+ ```
29
+
30
+ 直接用 Python(路徑從 config.json 自動解析):
31
+
32
+ ```bash
33
+ python rag/build_db.py
34
+ python rag/query.py "問題"
35
+ python rag/srt_to_text.py # 把缺 .txt 的 .srt 補轉成 .txt
36
+ ```
37
+
38
+ ## Pipeline 整合
39
+
40
+ podcast pipeline 產生/更新逐字稿後,跑 `playlist-admin rag build`
41
+ 即可把新集數加進向量庫(程式會自動跳過已索引的檔名)。
42
+
43
+ ## 檔案
44
+
45
+ - `build_db.py` — 逐字稿切 chunk → Ollama embeddings → ChromaDB(cosine)
46
+ - `query.py` — 問題 embedding → top-k 片段 → 輸出命中的節目/日期/全文偏移
47
+ - `srt_to_text.py` — srt → 純文字
48
48
  - `_resolve.py` — 從 app config 解析 podcast_downloads 與 chroma_db 路徑