支援多模態參考輸入(圖片 / 影片 / 音訊)、asset:// 數位人素材引用、[Image N] / [Video N] / [Audio N] 提示詞編號、任務匯出 / 匯入 JSON、本地 mp4 快取、TOS pre-signed URL 安全上傳,以及可重整恢復的併發任務追蹤(sessionStorage 持久化 + 背景輪詢 + 可設定的任務 TTL)。
- Node.js 20.19+ 或 22.12+(Vite 8 / vitest 4 的官方需求;測試在 Node 20–25 通過)
- npm 9 以上
| 項目 | 用途 | 取得方式 |
|---|---|---|
| ModelArk API Key | 影片生成驗證 | BytePlus ModelArk console |
| Seedance 2.0 Endpoint ID | 推論部署 | 自行依 BytePlus 官方文件 在 ModelArk 部署 Seedance 2.0 endpoint,部署後拿到 ID |
| TOS Bucket | 影片 / 音訊參考素材的安全託管 | BytePlus TOS console 自行建立 |
| 兩組 AccessKey / SecretKey(見下) | 一組給 TOS、一組給 ARK Asset 庫 | BytePlus IAM |
所有憑證都從 UI 輸入,.env 不需要填 AKSK。 Header 右上角會看到三顆膠囊(推論 / 素材庫 / TOS);新使用者打開時三顆都是灰色。
兩個入口都會打開右側憑證 Drawer:
- 點任一膠囊:drawer 開啟並自動展開、聚焦對應憑證的第一個欄位
- 點齒輪 / 按
⌘,(Mac)或Ctrl+,(Windows/Linux):drawer 開啟,三個區塊都收合,自己選要編輯哪個
Drawer 內三個區塊:
- 推論憑證 — ModelArk API Key + Endpoint ID。輸入即時驗證(regex 比對 BytePlus 真實格式
ark-{UUID}-{5字元}/ 純 UUID 對 API key;ep-YYYYMMDDHHMMSS-xxxxx對 endpoint),不打網路驗證;沒有「測試連線」按鈕。 - 私有素材庫憑證 — ARK AccessKey / SecretKey + Project Name(預設
default)。按「測試連線」打/local-api/asset/verify。 - 物件儲存憑證 — TOS AccessKey / SecretKey + Region(下拉選單:
ap-southeast-1/ap-southeast-3/cn-beijing/cn-shanghai/cn-guangzhou/cn-hongkong)+ Bucket(接受純 bucket 名稱或bucket/prefix格式 — 省略 prefix 會用預設seedance-2-0/)。Endpoint 自動由 Region 推導(tos-{region}.bytepluses.com),UI 不再顯示。測試連線會打/local-api/tos/verify,順便 bootstrap bucket CORS。
每個區塊改任何欄位 → 對應 Header 膠囊立刻變灰(pend),需重新測試才會變綠。
所有 secret 欄位(5 個 AKSK)右側有眼睛 icon — 預設 type=password,點一下切到明文檢查。drawer 關閉時自動回到隱藏。
填入後存在瀏覽器 sessionStorage(per-tab),不寫進 .env.local。同一個 tab 內 refresh 會保留欄位與驗證狀態;關掉 tab 或開新 tab 就清空 — 公用機器上的下一個使用者不會看到上一個人的金鑰。
.env 中的 AKSK 欄位(TOS_ACCESS_KEY、TOS_SECRET_KEY、BYTEPLUS_AK、BYTEPLUS_SK)僅供 npm run tos:bootstrap 這個 operator-only 的 CLI script 使用,執行中的 server 不再讀取這些變數。
本專案需要兩組分離的 AccessKey / SecretKey,分別承擔不同職責,請不要共用同一組:
| AKSK | 在哪裡輸入 | 需要的權限 | 用途 |
|---|---|---|---|
| TOS 那組 | Drawer →「物件儲存憑證」(或點 Header TOS 膠囊) |
TOS Full Access | 簽 PUT / GET pre-signed URL;驗證時自動 bootstrap bucket CORS |
| ARK 那組 | Drawer →「私有素材庫憑證」(或點 Header 素材庫 膠囊) |
ARK Full Access | 呼叫 ARK Asset Open API(建立 / 列出 / 刪除私有素材) |
關鍵是 TOS 與 ARK 各用一把獨立的 AKSK、權限只給對應 service(不要直接給 Admin / 全權限 key)。它們可以是同一個 BytePlus 帳號下的兩把 key,只要權限分開即可。
要在影片生成時透過 asset://asset-xxx 引用私有素材庫的 asset,三項憑證必須處在同一個 BytePlus 帳號 + 同一個 project:
- 推論端:Seedance Endpoint ID(
ep-...)+ ModelArk API Key — 隸屬於某個 BytePlus 帳號 / project - 素材端:ARK AKSK + Project Name — 也隸屬於某個 BytePlus 帳號 / project
如果推論用 A 帳號 / project-X 的 ep + API key,但 asset 是在 B 帳號 / project-Y 建立的,Seedance 推論時會找不到 asset,會失敗。**三邊都對齊到同一個 BytePlus 帳號 + 同一個 project(多數人就是 default)**才能 cross-reference。
TOS 不受此限制 — bucket 可以在不同 BytePlus 帳號或不同 project(pre-signed URL 是純 HTTPS GET,不依賴帳號 / project 對齊)。
cd byteplus-ai-gen-platform
npm install到 BytePlus TOS console 建立一個 bucket:
- Region 自選(例如
ap-southeast-1),之後在 Drawer →「物件儲存憑證」下拉選單選 region;Endpoint 由 Region 自動推導(tos-<region>.bytepluses.com),不用手動填 - 不需要先設 CORS — 在 Drawer 填入 TOS 憑證並按「測試連線」時會自動套上
預設所有素材會丟在 seedance-2-0/ 前綴下,避免污染既有資料。要分流可在 Bucket 欄位輸入 mybucket/myproject/,前綴就會用 myproject/。
.env.local 對於一般使用是非必要的——所有 AKSK 憑證都從 Header 膠囊 / Drawer 填入,TOS bucket 的 CORS 也會在 UI 驗證憑證時自動套上。
只有在需要覆寫 server runtime 選項(PORT、EXPORTS_DIR 等)時才需要建立 .env.local:
cp .env.example .env.local
# 僅填 server runtime 選項(PORT、EXPORTS_DIR 等).env.local 已被 .gitignore 排除,不會進 git。
npm run devnpm run dev 會用 concurrently 同時跑兩個 process:
- Express API server on http://127.0.0.1:3000(簽章、本地檔案 API、ARK proxy、SPA 靜態服務)
- Vite frontend on http://localhost:5173(透過 proxy 把
/local-api、/exports、/api轉到 3000)
開啟 **http://localhost:5173**,會自動導向影片生成頁。
如果只想跑前端(後端已經單獨啟動),用 npm run dev:vite-only。
npm test # 全套單元測試-
點 Header 右上角任一灰色膠囊(或按
⌘,/Ctrl+,開齒輪 drawer)設定憑證:- 推論憑證:填入 ModelArk API Key(純 UUID 或
ark-{UUID}-{5字元}格式)和 Endpoint ID(格式如ep-20260327154051-xxxxx)。輸入即時驗證,格式正確 pill 立刻變綠。 - 私有素材庫憑證:填入 ARK AccessKey / SecretKey;Project Name 預設已是
default(多數 tenant 自動建好的 project),按「測試連線」確認後端能用該 AKSK 通過 ARK - 物件儲存憑證:下拉選 Region、填 TOS AccessKey / SecretKey、Bucket(純名稱或
mybucket/prefix),按「測試連線」確認連線(驗證時會自動設定 bucket CORS)
三顆膠囊全綠(已驗證)後就可以開始用。憑證存在 sessionStorage,這個 tab 內有效。
- 推論憑證:填入 ModelArk API Key(純 UUID 或
-
在「提示詞」欄位描述你想要的影片內容;可在文字中以
[Image 1]/[Video 1]/[Audio 1]引用素材(編號規則見下方「提示詞素材編號」說明,非必填,不寫也能正常生成)。 -
(可選)上傳參考圖片 / 影片 / 音訊(影片與音訊會自動上傳到 TOS 並轉成短效 pre-signed URL)。
-
(可選)在「Asset 參考」加入
asset-xxxxxID、asset://URI 或https://...URL(會被解析成正確的引用形式)。 -
(可選)調整 Seed(預設
-1= 每次隨機;指定整數可在相同提示詞下取得相似輸出;🎲 按鈕產生隨機固定值方便重現)。 -
調整畫面比例(預設
Adaptive,模型自選)、影片長度(4–15 秒,或Auto)。 -
點擊「🎬 生成」按鈕;任務會送出後自動輪詢,影片完成後出現在中間預覽區。
-
完成後可在右側「📋 任務紀錄」匯出 JSON 或下載本地 mp4;下次啟動 dev server 點「📥 匯入」即可載入歷史任務並回放。
從頂部 tab「私有素材庫管理」進入,可以:
- 列出 / 搜尋既有 Asset Group 與 Asset(Image / Video / Audio)
- 建立 Asset Group(首次使用可以先建一個
default試試) - 上傳檔案(先進 TOS pre-signed PUT,再以 12 小時 GET URL 給 ARK 的
CreateAsset,背景每 5 秒輪詢GetAsset直到 Active / Failed) - 狀態視覺化:卡片右上角依 Asset 狀態顯示 — Active 綠色小點(最低調)、Processing 黃色「處理中」膠囊 + 旋轉中 spinner、Failed 紅色「失敗」膠囊 +
⚠️ 預覽 placeholder - 狀態過濾 chips:在類型過濾旁邊有第二列 chips(全部 / Active / Processing / Failed),server-side 過濾
- 背景輪詢:列表上的 Processing 資產每 5 秒自動 poll → 終態時自動翻成 Active / Failed,不用手動 refresh
- 重新命名 Asset / Asset Group;級聯刪除 Asset Group(需輸入完整名稱二次確認)
- 點縮圖開預覽抽屜(影片用
preload=metadata,按 play 才下載);抽屜的「基本資訊」區塊也有狀態欄位 - 一鍵複製
asset://asset-xxxURI,回到「影片生成」頁貼到「Asset 參考」即可使用
注意事項:
- ARK Asset API 採 AK/SK 簽章(
server/routes/asset.ts在 server 端做 HMAC-SHA256 v4);瀏覽器只打/local-api/asset/*,密鑰不進前端。 - 上傳的 12 小時 GET URL 是給 ARK 用的內部 URL;UI 預覽用的 URL 來自
ListAssets / GetAsset,同樣 12 小時,每次重整頁面會重抓。 - DeleteAssetGroup 會級聯刪除組內所有資產,不可復原。
CreateAsset一律送Moderation = { Strategy: "Skip" }(先在 console 關閉 Secure Mode)。
- 參考圖片 0–9 張(1 張=圖生影片;多張=多模態 / 角色參考)
- 參考影片 0–3 段,總長 ≤ 15 秒(mp4 / mov)
- 參考音訊 0–3 段,總長 ≤ 15 秒(wav / mp3)
- Asset 參考 任意數量,引用 ModelArk 私有素材庫的素材。欄位接受三種輸入:純
asset-xxxID、asset://asset-xxxURI、或https://...URL(可串接前一段任務的last_frame_url/video_url)
- 可設定的任務最長等待時間:左側「進階設定」下拉,1 / 2 / 4 / 8 / 24 / 48 / 72 小時(預設 1 小時)。對應 ARK 的
execution_expires_after,超過 server 端會自動標expired,不會卡在排隊狀態無限期算配額。 - 狀態 + 即時計時:每張卡顯示
排隊中 / 處理中 / 完成 / 失敗 / 已取消 / 已過期,queued/running 卡片即時跳秒(mm:ss),終態凍結在最終時長。orphanedchip(⚠ 無法查詢)出現代表此任務由其他金鑰建立、現在這把金鑰沒權限查詢。 - 取消 / 刪除按鈕:
queued狀態的「取消任務」呼叫 ARK DELETE → 立即停扣費;終態的「刪除記錄」清前端記錄(ARK 後端記錄 7 天後自動清)。 - 背景輪詢:App 層
useBackgroundPoller掃activeTaskIds,每個任務一個獨立 loop。間隔採 3 段 backoff(0–30s 用 3s、30s–5min 用 10s、5min+ 用 20s),status 變化時 reset。沒 API key 時暫停(不移除);401/403 時標 orphan 並停止。 - 重整恢復:
videoStore用 sessionStorage 持久化(per-tab)— 同 tab refresh 後 history、進行中任務、設定都還在;polling 自動恢復。關 tab / 新 tab 起就乾淨,不會洩漏給共用瀏覽器的下一個使用者。 - 參考媒體失效提示:reload 後參考媒體因
File物件無法序列化變成 stale chip(📎+filename+已失效);submit 時會 toast 擋下,要使用者移除後重新上傳。
模型會解析 prompt 中的 [Image 1] / [Video 1] / [Audio 1] 等記號,數字依該類型在 content 陣列中的出現順序遞增。前端 UI 會即時在每個縮圖右下角顯示對應編號(例如已上傳 2 張圖、再加 1 個 image-type asset → asset 行右側顯示 [Image 3]),不需要手動數。
一鍵插入:縮圖右下角的徽章和 Asset 參考 row 右側的 [Type N] 都是按鈕 — 點下去會把對應字串插入到提示詞 textarea 的游標位置(前一個字非空白會自動補空白),免手動打或複製貼上。Asset 參考的按鈕在欄位內容是有效形式時(asset-xxx / asset://... / http(s)://...)才會啟用。
- 每個成功的任務都可以匯出成 JSON(含完整 request body 與結果)
- 匯出檔存在
exports/下;本地 mp4 存在exports/mp4/ - 「📥 匯入」可選多份 JSON 載入,並自動把最後一個有 video 的任務帶到中間預覽區(不會自動播放)
- 匯入項目顯示「📥 已匯入」標籤以區別現場生成的任務
不使用 base64 inline(Seedance 影片不支援;音訊 base64 上限 64 MB),改走 TOS pre-signed URL:
Browser ──① POST /local-api/tos/sign-put──▶ Express server(瀏覽器隨請求帶 TOS creds)
◀── PUT pre-signed URL(15 分鐘有效)─
──② PUT file ────────────────────────▶ TOS bucket
──③ POST /local-api/tos/sign-get ───▶ Express server
◀── GET pre-signed URL(3 小時有效)─
──④ POST /api/v3/contents/...──────▶ Seedance(用 GET URL 拉影片)
AKSK 由瀏覽器隨請求帶到 Express server 做簽章,server 簽完即丟、不持久化。GET URL 過期後物件不再具備 public access。
# 開發 / 建置
npm run dev # concurrently 跑 Vite (5173) + Express (3000)
npm run dev:vite-only # 僅 Vite,假設 Express 已另外啟動
npm run server:dev # 只跑 Express dev server (tsx watch)
npm run server:start # production: NODE_ENV=production tsx server/index.ts
npm run build # tsc -b && vite build
npm run preview # 預覽生產版本
npm run lint # ESLint 檢查
# 測試(vitest + jsdom + Testing Library)
npm test # 跑全部測試
npm run test:watch # watch 模式
# TOS bucket 設定(通常不需要手動跑 — UI 驗證 TOS 憑證時會自動 bootstrap CORS)
npm run tos:bootstrap # 手動套用 CORS rule(idempotent;需要 .env.local 中的 TOS AKSK)
npm run tos:cors:show # 印出目前 bucket 的 CORS 設定(純讀;需要 .env.local 中的 TOS AKSK)byteplus-ai-gen-platform/
├── scripts/
│ ├── tos-bootstrap.ts # 透過 TOS AKSK 設定 bucket CORS(npm run tos:bootstrap)
│ └── loadDotenv.ts # 共用 .env / .env.local 載入器
├── server/ # Express 後端(取代舊的 vite-plugin-*)
│ ├── index.ts # entry: load env + listen
│ ├── app.ts # createApp() 工廠 — 掛載所有 router
│ ├── config/env.ts # PORT / NODE_ENV 載入與驗證
│ ├── signers/
│ │ ├── tos.ts # TOS pre-signed URL 純邏輯(dev + prod 共用)
│ │ ├── tosCors.ts # TOS bucket CORS merge / 驗證邏輯
│ │ └── asset.ts # ARK Asset HMAC v4 簽章純邏輯
│ └── routes/
│ ├── tos.ts # POST /local-api/tos/sign-put | /sign-get
│ ├── tosVerify.ts # POST /local-api/tos/verify(BYO 憑證驗證 + CORS bootstrap)
│ ├── asset.ts # POST /local-api/asset/*(catch-all dispatch)
│ ├── assetVerify.ts # POST /local-api/asset/verify(ARK 憑證驗證)
│ ├── localApi.ts # 匯出 / 匯入 / 下載 mp4 / asset / frame
│ ├── exportsStatic.ts # GET /exports/* 靜態檔
│ ├── arkProxy.ts # /api/* → ark.ap-southeast.bytepluses.com
│ └── spaFallback.ts # production: 服務 dist/ + index.html fallback
├── vite.config.ts # Vite 設定 + /local-api、/exports、/api proxy 到 Express (3000)
├── docs/
│ └── superpowers/ # 開發計畫 / 規格文件(plans/, specs/)
├── src/
│ ├── api/
│ │ ├── client.ts # axios 實例 + 自動帶 API Key
│ │ ├── video.ts # Seedance 任務 CRUD + 輪詢
│ │ ├── asset.ts # ARK Asset 庫前端 client(走 server/routes/asset.ts)
│ │ ├── tos.ts # 前端 TOS client(走 server/routes/tos.ts)
│ │ ├── verify.ts # 前端憑證驗證 client(走 tosVerify + assetVerify)
│ │ ├── local.ts # 前端對 server/routes/localApi.ts 的 client
│ │ └── fileUtils.ts # base64 轉換(圖片用)
│ ├── components/
│ │ ├── layout/
│ │ │ ├── AppLayout.tsx
│ │ │ └── Header.tsx
│ │ ├── common/
│ │ │ └── ResizeHandle.tsx # 三欄佈局的拖拉縮放 handle
│ │ ├── credentials/ # Header pills + 右側 Drawer 憑證 UI
│ │ │ ├── schema.ts # CREDENTIALS 常數(推論 / 素材庫 / TOS 三組定義)
│ │ │ ├── uiStore.ts # drawer open + target + expandedSection(無持久化)
│ │ │ ├── HeaderStatusPills.tsx # Header 三顆狀態膠囊
│ │ │ ├── CredentialsDrawer.tsx # 右側 drawer 容器(accordion)
│ │ │ ├── CredentialSection.tsx # 單一憑證區塊(展開 / 收合 + 驗證狀態 pill)
│ │ │ └── CredentialForm.tsx # schema-driven 欄位渲染
│ │ ├── video/
│ │ │ ├── VideoGenPage.tsx # 三欄佈局
│ │ │ ├── VideoParams.tsx # 左側參數面板(含 seed 輸入、可點 [Type N] 插入 prompt)
│ │ │ ├── VideoPreview.tsx # 中間預覽(無 autoplay)
│ │ │ ├── VideoHistory.tsx # 右側任務紀錄 + 匯入 / 匯出
│ │ │ └── MediaUploader.tsx # 拖拽上傳 + 縮圖 + 可點 [Type N] 徽章(onLabelClick prop)
│ │ └── assets/ # 私有素材庫管理頁(/assets)
│ │ ├── AssetLibraryPage.tsx # 整頁佈局 + 過濾 chips + 背景輪詢 mount
│ │ ├── AssetGroupSidebar.tsx
│ │ ├── AssetGrid.tsx
│ │ ├── AssetCard.tsx # 含狀態膠囊 / Active 綠點 / Failed ⚠️ placeholder
│ │ ├── AssetTypeFilterChips.tsx
│ │ ├── AssetStatusFilterChips.tsx # 全部 / Active / Processing / Failed(server-side 過濾)
│ │ ├── AssetPreviewDrawer.tsx # 含狀態欄位 / Failed / Processing 預覽分支
│ │ ├── AssetUploadDialog.tsx
│ │ ├── AudioWaveformDecoration.tsx
│ │ └── UploadProgressPanel.tsx
│ ├── hooks/
│ │ ├── useVideoGeneration.ts # 建任務 + 寫 history(不 await polling,由 useBackgroundPoller 接手)
│ │ ├── useBackgroundPoller.ts # App 層常駐:掃 activeTaskIds、3-tier backoff、auth-pause、orphan 偵測
│ │ ├── useNow.ts # 1 秒 ticker(live mm:ss elapsed 計時用;intervalMs=0 可停用)
│ │ ├── useReferenceUpload.ts # 影片 / 音訊收檔即上傳 TOS(id-based 容錯)
│ │ ├── useAssetUpload.ts # /assets 頁的並行上傳(最多 8 條)+ ARK CreateAsset 5s 輪詢(含 429 退避)
│ │ ├── useAssetStatusPoller.ts # 背景輪詢 Processing 資產(5s);跳過上傳中的 id 避免雙 poller race
│ │ └── useResizableWidth.ts # 欄寬拖拉持久化 hook
│ ├── stores/
│ │ ├── authStore.ts # API Key / Endpoint / AssetCreds / TosCreds + verifyState(sessionStorage 持久化,per-tab)
│ │ ├── videoStore.ts # 影片參數 / 參考素材 / 任務狀態 / 歷史(sessionStorage 持久化;參考媒體 reload 後保留 filename 並標 stale)
│ │ └── assetStore.ts # /assets 頁的 group / asset 列表狀態
│ ├── types/
│ │ ├── index.ts # 影片生成相關型別(API + UI store)
│ │ └── asset.ts # ARK Asset 庫型別
│ ├── utils/
│ │ ├── contentLabels.ts # [Image N] / [Video N] / [Audio N] 編號計算
│ │ ├── mediaValidation.ts # 影片 / 音訊長度與格式驗證
│ │ ├── panelScale.ts # 三欄佈局縮放邏輯
│ │ ├── tosRegions.ts # TOS region 列表 + region → endpoint 轉換
│ │ ├── tosBucket.ts # parseBucketAndPrefix(接受純 bucket 或 bucket/prefix)
│ │ └── clipboard.ts # asset:// URI 複製
│ ├── __tests__/ # vitest + jsdom 測試(執行 `npm test` 看當前數量)
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css # 深色主題樣式
├── .env.example # 環境變數範本(commit)
├── .env.local # 真實值(git 排除)
└── exports/ # 匯出的 JSON 與 mp4(git 排除)
Express 上掛 /local-api/tos/* 三個 endpoint:
POST /sign-put接收{ filename, contentType, creds },回 PUT pre-signed URL(15 分鐘有效)+ 物件 keyPOST /sign-get接收{ key, expiresSec?, creds },回 GET pre-signed URL(預設 3 小時,上限 7 天)POST /verify接收{ creds },驗證 TOS 憑證是否有效(headBucket),並自動將當前 origin merge 進 bucket 的 CORS 規則(idempotent)
物件 key 格式:${keyPrefix}${YYYY}/${MM}/${uuid}-${sanitized-filename},由 server 端組(client 不能任意指定 key)。keyPrefix 由前端 parseBucketAndPrefix 從 Bucket 欄位拆出(純 bucket → seedance-2-0/ 預設;mybucket/foo → foo/)。所有 endpoint 的 AKSK 都由瀏覽器隨請求帶入(BYO 模式),server 不持久化;endpoint 也在 server 端透過 withDerivedEndpoint/deriveEndpoint 從 region 推導,不必由 client 帶。
Express 上掛 /local-api/asset/*,把瀏覽器請求轉發到 ark.ap-southeast-1.byteplusapi.com,並用 ARK 那組 AKSK 在 server 端做 HMAC-SHA256 v4 簽章。瀏覽器只看到 /local-api/asset/*,AKSK 與 ProjectName 不會進前端。涵蓋的 ARK Asset Open API:ListAssetGroups / CreateAssetGroup / DeleteAssetGroup / ListAssets / CreateAsset / GetAsset / DeleteAsset,以及一個 download-asset 代理(避免 CORS 直下載)。
Express 上掛 /local-api/* 提供本機 file system 操作:
POST /export寫入任務 JSON 到exports/<task_id>.jsonPOST /download-mp4下載遠端影片 →exports/mp4/<task_id>.mp4GET /list-exports、GET /import/:taskId、GET /check-mp4/:taskId
影片 / 音訊上傳採 id-based 索引,每個項目進 store 時帶一個 crypto.randomUUID()。當 promise 完成時用 id 找回對應項目,使用者中途移除 / 新增不會污染其他項目。
- Create-only 流程:建任務 + push
taskId到activeTaskIds+ 寫 queued history entry,不 await polling(由useBackgroundPoller接手) - 帶
execution_expires_after給 ARK(從videoStore.executionExpiresAfter讀取) - 影片 / 音訊一律使用
media.uploadedUrl(不再 fallback 到 base64,因 Seedance 影片不支援) - 參考素材尚未完成上傳、或重整後變 stale 時拒送並 toast 提示
content陣列順序固定為[text, …images, …videos, …audios, …assets],與[Type N]編號規則對齊
App 層 <App> 內常駐 mount 一次(無 cleanup,移到 route 元件會 leak loops)。掃 videoStore.activeTaskIds,每個 taskId 一個獨立 polling loop,用 useRef<Set<string>> dedupe 避免 re-render 重複起 loop。
- 3-tier backoff:0–30s 間隔 3s、30s–5min 間隔 10s、5min+ 間隔 20s;status 變化 reset 計時。
- Auth-pause:偵測到
authStore.apiKey為空 → sleep 5s 重試(不移除任務,等使用者填回金鑰)。 - Orphan 偵測:
apiClientinterceptor 把 HTTP status 黏到 Error 物件上;poller 檢查err.status === 401 || 403→ 標orphaned: true+removeActiveTask。 - 取消 race 防護:
getVideoTaskresolve 後再檢查activeTaskIds.includes(taskId),使用者中途取消 → 直接 return,不寫陳舊 history。 - 終態處理:
succeeded / failed / cancelled / expired都會寫updatedAt+ 對應欄位(succeeded 寫 videoUrl/lastFrameUrl/seed/resolution/fps;failed 寫 error),然後removeActiveTask。
執行 npm test 看當前測試結果(檔案數與 case 數會隨開發增加,以實際輸出為準)。主要涵蓋範圍(非完整列表):
影片生成模組
| 檔案 | 範圍 |
|---|---|
videoApi.test.ts |
Seedance 任務 CRUD、3 段 polling backoff (nextPollInterval)、timeout 後最後一次 sync 不 throw |
videoGeneration.test.ts |
Create-only 流程:寫入 history + 進 activeTaskIds、發 execution_expires_after、validation、stale ref 阻擋 |
videoStore.test.ts |
store actions、resetForNewTask、預設值、removeHistory、executionExpiresAfter、EXECUTION_EXPIRES_OPTIONS 範圍 |
videoStorePersist.test.ts |
sessionStorage 持久化 round-trip:prompt、設定、history、refs flatten 為 filename stub、rehydrate 為 stale |
videoParams.test.tsx |
左側面板:layout 順序、新任務、duration/ratio 選項、label 顯示、任務最長等待時間 下拉 |
videoHistory.test.tsx |
右側面板:所有項目都顯示、imported tag、匯入流程、auto preview、已過期 / 無法查詢 狀態、live duration、取消任務、刪除記錄 |
videoPreview.test.tsx |
中間預覽:無 autoplay、有 controls、source 優先順序 |
useBackgroundPoller.test.tsx |
App 層 polling:終態移除、dedupe、401 → orphaned、expired 寫入、無金鑰暫停、failed 帶 updatedAt |
useNow.test.ts |
1 秒 ticker:初值、推進、unmount cleanup、intervalMs=0 不啟動 setInterval |
referenceUpload.test.ts |
上傳 hook:成功 / 失敗 / 移除中 / 多筆併發 |
mediaUploader.test.tsx |
拖拽上傳 UI:accept rule、[Type N] badge、stale rehydrated chips(📎+filename+已失效)+ 移除按鈕 |
mediaValidation.test.ts |
影片 / 音訊長度與格式驗證 |
contentLabels.test.ts |
[Image N] / [Video N] / [Audio N] 編號計算 |
exportImport.test.ts |
匯出 payload 結構、匯入解析邏輯 |
私有素材庫模組
| 檔案 | 範圍 |
|---|---|
assetApi.test.ts |
ARK Asset Open API client |
assetStore.test.ts |
group / asset 列表 store actions |
assetTypes.test.ts |
image/video/audio 型別與 mime 判斷 |
assetLibraryPage.test.tsx |
整頁佈局與互動 |
assetGroupSidebar.test.tsx |
左側 group 列表:建立 / 重新命名 / 級聯刪除 |
assetCard.test.tsx |
card v2:type badge、media branches |
assetTypeFilterChips.test.tsx |
型別過濾 chips + 計數 |
assetStatusFilterChips.test.tsx |
狀態過濾 chips(全部/Active/Processing/Failed)+ aria-pressed |
useAssetStatusPoller.test.ts |
背景輪詢 hook:每 Processing id 一個 poller、不重複註冊、跳過 terminal、跳過上傳中 |
assetUploadDialog.test.tsx |
上傳對話框:file picker、name cap、moderation skip |
useAssetUpload.test.ts |
並行上傳 hook(最多 8 條)+ ARK CreateAsset 輪詢(含 429 退避) |
uploadProgressPanel.test.tsx |
上傳進度面板:成功 / 失敗 / 進行中 |
audioWaveform.test.tsx |
音訊縮圖視覺化 |
vitePluginAsset.test.ts |
plugin 純函式:HMAC v4 簽章、canonical request、URL 組裝 |
憑證 / 認證模組
| 檔案 | 範圍 |
|---|---|
headerStatusPills.test.tsx |
Header 三顆狀態膠囊:data-state、aria-label、click → openDrawer(key) |
credentialsDrawer.test.tsx |
Drawer 容器:open class、ESC、backdrop、accordion 一次只開一個、target highlight、inert |
credentialSection.test.tsx |
單一 accordion section:title/hint、toggle、target autoFocus、測試連線、pending disable |
credentialForm.test.tsx |
Schema-driven 表單:欄位渲染、secret type、setField 寫入、warn banner |
authStoreVerify.test.ts |
verifyState slice:setField reset、verify 三條 path(local 格式 / asset / tos) |
authStoreMigration.test.ts |
zustand auth store 版本遷移(v1→v2→v3→v4→v5)+ localStorage 清理 |
TOS / 本機 API 共用
| 檔案 | 範圍 |
|---|---|
tosClient.test.ts |
前端 TOS client(mock fetch;含 bucket/prefix 解析驗證) |
tosBucket.test.ts |
parseBucketAndPrefix:純 bucket / bucket+prefix 各種輸入 |
tosRegions.test.ts |
TOS_REGIONS list + regionToEndpoint helper |
vitePluginTos.test.ts |
plugin 純函式:env 載入、key 組裝、簽名邏輯、TTL 邊界 |
tosBootstrap.test.ts |
bootstrap script:env 校驗、CORS payload、showOnly |
tosCorsMerge.test.ts |
CORS merge 邏輯:idempotent rule 合併、wildcard 處理 |
localApi.test.ts |
本機 export / import / mp4 client |
Server 端路由(Express + supertest)
| 檔案 | 範圍 |
|---|---|
serverTosRoutes.test.ts |
TOS sign-put / sign-get 路由 |
serverTosVerify.test.ts |
TOS verify 路由:憑證驗證 + CORS bootstrap |
serverAssetRoutes.test.ts |
ARK Asset catch-all dispatch 路由 |
serverAssetVerify.test.ts |
ARK Asset verify 路由 |
serverArkProxy.test.ts |
/api/* → ARK Open API proxy |
serverExportsStatic.test.ts |
/exports/* 靜態檔服務 |
serverLocalApiRoutes.test.ts |
本機 file system 操作路由 |
serverSpaFallback.test.ts |
production SPA fallback |
src/__tests__/setup.ts 內含 in-memory localStorage 與 sessionStorage polyfill,確保在 Node 25 + jsdom 環境下 zustand persist middleware(現在用 sessionStorage)不會掛掉。
後端已抽出為獨立 Express server(server/ 目錄)。生產部署完整教程:
docs/deployment-guide.md— 從 BytePlus VM 0→1(Phase 0 後端抽取已完成;Phase 1 起:VM provisioning、Nginx 反向代理、Let's Encrypt、systemd service、備份)
產品環境啟動:
npm run build
NODE_ENV=production PORT=3000 npm run server:startExpress 同時負責 API 路由與 dist/ 的 SPA 靜態服務,無需另外跑 Vite。
- 圖片生成(Seedream 模型)
- Chat / LLM
- 虛擬人模型
- 語音模型
- TOS bucket lifecycle 自動清理
- 上傳前 client-side 影片時長檢查(防 > 15s)