Skip to content

Commit 3ce45d4

Browse files
committed
fix: authenticate HF downloads and add per-step timeouts/heartbeats
A test run on 2026-08-16 spent 28 minutes with zero log output before hitting the outer colab-exec timeout during model download — anonymous HF Hub requests are rate-limited unpredictably. This adds: - Optional HF_TOKEN secret, uploaded to the Colab VM and consumed via the standard HF_TOKEN env var before indextts2 download runs. - Per-step timeouts inside synthesize.py (clone/sync/download/check/ batch) enforced independently of the outer `colab exec --timeout`, each with periodic heartbeat output and stall detection, so a stuck step is diagnosable instead of going silent for the whole run. - Raised the outer colab-exec timeout to 2700s and job timeout-minutes to 55 to give the per-step timeouts room to matter. Verified run(), setup_hf_token(), and count_batch_tasks() locally against success/timeout/failure/stall scenarios before pushing.
1 parent 6f722f7 commit 3ce45d4

3 files changed

Lines changed: 116 additions & 14 deletions

File tree

.github/workflows/synthesize.yml

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
github.event_name == 'workflow_dispatch' ||
4545
contains(github.event.issue.labels.*.name, 'synthesize')
4646
runs-on: ubuntu-latest
47-
timeout-minutes: 45
47+
timeout-minutes: 55
4848
steps:
4949
- name: Checkout
5050
uses: actions/checkout@v4
@@ -117,19 +117,34 @@ jobs:
117117
echo "session=$SESSION" >> "$GITHUB_OUTPUT"
118118
colab --auth=adc new -s "$SESSION" --gpu T4
119119
120+
- name: Write HuggingFace token (optional, improves download stability)
121+
env:
122+
HF_TOKEN: ${{ secrets.HF_TOKEN }}
123+
run: |
124+
if [ -z "$HF_TOKEN" ]; then
125+
echo "::warning::HF_TOKEN secret not set — model downloads will be unauthenticated and may be slower or rate-limited. See README."
126+
else
127+
printf '%s' "$HF_TOKEN" > hf_token.txt
128+
fi
129+
120130
- name: Upload reference audio and batch file
121131
run: |
122132
SESSION="${{ steps.colab_new.outputs.session }}"
123133
colab --auth=adc upload -s "$SESSION" assets/ref_voice.wav /content/ref.wav
124134
colab --auth=adc upload -s "$SESSION" batch.jsonl /content/batch.jsonl
135+
if [ -f hf_token.txt ]; then
136+
colab --auth=adc upload -s "$SESSION" hf_token.txt /content/hf_token
137+
fi
125138
126139
- name: Run synthesis on Colab GPU
127140
run: |
128141
SESSION="${{ steps.colab_new.outputs.session }}"
129142
# colab exec's default --timeout is 30s, meant for short interactive
130-
# cells. Our script does a full env build + model download + batch
131-
# synth in one call, which legitimately takes 15-20 min.
132-
colab --auth=adc exec -s "$SESSION" -f colab_job/synthesize.py --timeout 1800
143+
# cells. synthesize.py enforces its own per-step timeouts internally
144+
# (git clone/uv sync/model download/batch synth) and prints periodic
145+
# heartbeats; this outer value is just the overall safety net, kept
146+
# comfortably above the sum of the internal step timeouts.
147+
colab --auth=adc exec -s "$SESSION" -f colab_job/synthesize.py --timeout 2700
133148
134149
- name: Download result
135150
run: |

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,15 @@ gh release create + gh issue comment/close
3434

3535
- **同時只能有一個 GPU runtime**(Colab 免費帳號限制)。Workflow 用 `concurrency` group 序列化多個 issue,但如果有人開著瀏覽器裡的互動式 Colab notebook 占用 T4,CI 會直接失敗(`TooManyAssignmentsError`)。發生時去 [colab.research.google.com](https://colab.research.google.com) → 執行階段 → 管理工作階段 把它斷開。
3636
- **每次 run 都是全新 VM**,環境建置+模型下載(~6GB)沒有快取,每次都要重來一遍,佔掉大部分時間。要加速可以考慮把 venv/checkpoints 打包存 Drive,run 開始時解壓——目前先不做,避免過早優化。
37+
- **模型下載速度不穩定**:2026-08-16 實測過一次匿名下載卡了 28 分鐘沒完成(HuggingFace 對未登入請求的限速本來就不保證)。設定 `HF_TOKEN` secret(見下)能大幅改善這個問題。`colab_job/synthesize.py` 內建每一步自己的 timeout+定期心跳輸出,卡住時能立刻看出是哪一步、卡了多久,而不是像修這個問題之前那樣整段 30 分鐘無聲無息。
3738
- **參考聲音固定**:用的是 repo 裡 `assets/ref_voice.wav`(7 秒乾淨人聲,已做響度正規化)。目前 v1 不支援每個 issue 換一個參考音檔,如果要換,直接替換這個檔案再 commit。
3839
- **情緒是整段套用同一個預設**,不支援每行不同情緒。這 6 個預設向量寫死在 `scripts/parse_issue.py``EMOTION_PRESETS`
3940
- 一次最多 40 行 / 4000 字,超過會直接失敗(不做靜默截斷)。
4041

4142
## 一次性設定(repo owner 才需要做)
4243

44+
### `COLAB_ADC_CREDENTIALS`(必要)
45+
4346
Colab CLI 用你 Google 帳號的 [Application Default Credentials](https://cloud.google.com/docs/authentication/application-default-credentials) 認證。這把憑證存進 GitHub Secrets 後,CI 就能用你的身分開 Colab GPU session——這代表:
4447

4548
- CI 會消耗你 Google 帳號的 Colab 免費運算額度
@@ -52,3 +55,14 @@ gcloud auth application-default login \
5255

5356
gh secret set COLAB_ADC_CREDENTIALS < ~/.config/gcloud/application_default_credentials.json
5457
```
58+
59+
### `HF_TOKEN`(選填,但強烈建議)
60+
61+
匿名對 HuggingFace Hub 發請求會被限速,速度不保證(實測卡過 28 分鐘沒下完 ~6GB 的模型)。去 [huggingface.co/settings/tokens](https://huggingface.co/settings/tokens) 拿一個 **read** 權限的 token,存進 secret:
62+
63+
```bash
64+
gh secret set HF_TOKEN --repo htlin222/index-tts-in-colab
65+
# 貼上 token,按 Ctrl-D
66+
```
67+
68+
沒設這個 secret 不會讓 pipeline 失敗,只是下載會退回匿名、變慢變不穩。

colab_job/synthesize.py

Lines changed: 83 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,24 @@
33
Expects, already placed on the VM before this script runs:
44
/content/ref.wav -- reference voice clip (via `colab upload`)
55
/content/batch.jsonl -- indextts2 batch tasks (via `colab upload`)
6+
/content/hf_token -- optional HuggingFace token (via `colab upload`),
7+
improves model-download stability/speed over
8+
anonymous requests. Read once, then deleted.
69
710
Produces:
811
/content/output.wav -- concatenated synthesis result
12+
13+
Each step gets its own timeout enforced by *this* script (not just the
14+
outer `colab exec --timeout`), and prints a heartbeat line periodically
15+
so a stuck step is diagnosable instead of going silent for the whole
16+
outer deadline. A run on 2026-08-16 spent 28 minutes with zero log
17+
output before hitting the outer timeout during model download --
18+
these heartbeats plus per-step timeouts are the direct fix for that.
919
"""
20+
import os
1021
import subprocess
1122
import sys
23+
import time
1224
from pathlib import Path
1325

1426
WORK = Path("/content")
@@ -17,34 +29,95 @@
1729
BATCH_FILE = WORK / "batch.jsonl"
1830
VOICE_FILE = WORK / "ref.wav"
1931
OUTPUT_FILE = WORK / "output.wav"
32+
HF_TOKEN_FILE = WORK / "hf_token"
33+
STEP_LOG = WORK / "_step.log"
34+
35+
HEARTBEAT_SECONDS = 60
36+
STALL_HEARTBEATS_BEFORE_WARNING = 5 # 5 min of zero new output -> call it out
37+
38+
39+
def run(cmd, cwd=None, timeout=600):
40+
print(f"\n>>> {' '.join(cmd)} (timeout={timeout}s)", flush=True)
41+
start = time.monotonic()
42+
stall_count = 0
43+
last_size = -1
44+
with open(STEP_LOG, "wb") as logf:
45+
proc = subprocess.Popen(cmd, cwd=cwd, stdout=logf, stderr=subprocess.STDOUT)
46+
while proc.poll() is None:
47+
elapsed = time.monotonic() - start
48+
if elapsed > timeout:
49+
proc.kill()
50+
proc.wait()
51+
_print_tail()
52+
raise RuntimeError(
53+
f"command exceeded {timeout}s timeout after {elapsed:.0f}s: {' '.join(cmd)}"
54+
)
55+
time.sleep(HEARTBEAT_SECONDS)
56+
size = STEP_LOG.stat().st_size
57+
grew = size != last_size
58+
stall_count = 0 if grew else stall_count + 1
59+
note = "" if grew else f" [no new output for {stall_count * HEARTBEAT_SECONDS}s]"
60+
print(f" ... still running ({elapsed:.0f}s elapsed, {size} bytes so far){note}", flush=True)
61+
if stall_count >= STALL_HEARTBEATS_BEFORE_WARNING:
62+
print(f" ⚠ possible stall: no output growth for "
63+
f"{stall_count * HEARTBEAT_SECONDS}s (will still respect the {timeout}s timeout)", flush=True)
64+
last_size = size
65+
66+
_print_tail()
67+
if proc.returncode != 0:
68+
raise RuntimeError(f"command failed ({proc.returncode}): {' '.join(cmd)}")
69+
2070

71+
def _print_tail(max_bytes=20000):
72+
data = STEP_LOG.read_bytes()
73+
if len(data) > max_bytes:
74+
print(f"[... truncated, showing last {max_bytes} bytes of {len(data)} ...]")
75+
data = data[-max_bytes:]
76+
sys.stdout.write(data.decode(errors="replace"))
77+
sys.stdout.flush()
2178

22-
def run(cmd, cwd=None):
23-
print(f"\n>>> {' '.join(cmd)}", flush=True)
24-
result = subprocess.run(cmd, cwd=cwd)
25-
if result.returncode != 0:
26-
raise RuntimeError(f"command failed ({result.returncode}): {' '.join(cmd)}")
79+
80+
def setup_hf_token():
81+
if not HF_TOKEN_FILE.is_file():
82+
print(">> no /content/hf_token uploaded; HF downloads will be unauthenticated "
83+
"(slower/rate-limited on the free tier).")
84+
return
85+
token = HF_TOKEN_FILE.read_text().strip()
86+
HF_TOKEN_FILE.unlink() # don't leave it sitting on the VM disk
87+
if token:
88+
os.environ["HF_TOKEN"] = token
89+
print(">> HF_TOKEN set from uploaded credential.")
90+
91+
92+
def count_batch_tasks():
93+
with open(BATCH_FILE, encoding="utf-8") as f:
94+
return sum(1 for line in f if line.strip())
2795

2896

2997
def main():
3098
assert BATCH_FILE.is_file(), f"missing {BATCH_FILE}, did the upload step run?"
3199
assert VOICE_FILE.is_file(), f"missing {VOICE_FILE}, did the upload step run?"
32100

101+
setup_hf_token()
102+
33103
if not REPO.is_dir():
34104
run(["git", "clone", "--depth", "1",
35-
"https://github.com/index-tts/index-tts.git", str(REPO)])
105+
"https://github.com/index-tts/index-tts.git", str(REPO)], timeout=180)
36106

37-
run(["uv", "sync"], cwd=str(REPO))
107+
run(["uv", "sync"], cwd=str(REPO), timeout=600)
38108
run(["uv", "run", "indextts2", "download", "--model-dir", str(MODEL_DIR)],
39-
cwd=str(REPO))
109+
cwd=str(REPO), timeout=1200)
40110
run(["uv", "run", "indextts2", "check", "--model-dir", str(MODEL_DIR),
41-
"--device", "cuda"], cwd=str(REPO))
111+
"--device", "cuda"], cwd=str(REPO), timeout=90)
112+
113+
num_tasks = count_batch_tasks()
114+
batch_timeout = 120 + 90 * num_tasks
42115
run(["uv", "run", "indextts2", "batch",
43116
"--batch-file", str(BATCH_FILE),
44117
"--model-dir", str(MODEL_DIR),
45118
"--concat", "--output", str(OUTPUT_FILE),
46119
"--no-cuda-kernel", "--force", "--verbose"],
47-
cwd=str(REPO))
120+
cwd=str(REPO), timeout=batch_timeout)
48121

49122
assert OUTPUT_FILE.is_file(), "synthesis finished but output.wav was not created"
50123
size = OUTPUT_FILE.stat().st_size

0 commit comments

Comments
 (0)