Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
d4c71ad
fix: Pin PyInstaller to 6.16.0 to avoid conda hook regression
grider-transwithai Dec 26, 2025
3e2fc63
feat: Add Modal cloud GPU inference support
Dec 30, 2025
84bfe9f
fix: Improve modal_infer.py usability and fix bugs
Dec 31, 2025
5f1b639
refactor: 优化文件夹处理逻辑,逐文件上传避免连接断开
Randomless Jan 3, 2026
04fc0dd
fix: 修复文件名包含空格导致远程执行失败的问题
Randomless Jan 3, 2026
d863ce1
fix: 使用固定文件名 todo 避免全角字符导致文件找不到
Randomless Jan 3, 2026
4a56d1e
fix: 添加 Volume 同步等待,解决文件上传后容器看不到的问题
Randomless Jan 3, 2026
00da127
fix: 移除本地 volume.commit() 调用(只能在容器内调用)
Randomless Jan 3, 2026
bdaedca
refactor: 通过函数返回值传输结果文件,避免 volume 同步问题
Randomless Jan 3, 2026
56d1764
fix: add modal_infer to build and CI
Randomless Jan 11, 2026
72b5e81
拆分infer与modal打包并隔离依赖
Jan 13, 2026
1236802
CI适配engine与client输出结构
Jan 13, 2026
1a00c3a
amend 把.spec 的 COLLECT 拆成两个输出目录(dist/engine 和 dist/client),以彻底隔离 DLL 冲突
Jan 13, 2026
c45c08c
修改Verify build succeeded检查目录隔离下的两个子目录
Randomless Jan 13, 2026
aec0e56
change back to v1.5 .spec and workflow
Randomless Jan 13, 2026
7fd3f7e
standalone modal build process
Randomless Jan 13, 2026
3c6d112
change modal.exe output path
Randomless Jan 13, 2026
d4c30e9
install modal dependencies on the fly
Randomless Jan 13, 2026
bc5efca
新增了 ensure_utf8_stdio(),强制把 stdout/stderr 改成 UTF‑8(并用 errors="replace…
Randomless Jan 13, 2026
d39e0f5
新增 REPO_REF = "v1.4",并在 clone/update 时强制 checkout/reset 到该版本
Randomless Jan 13, 2026
25eebe2
fix: audio_suffixes variable added to modal_infer.py
Randomless Jan 13, 2026
f517369
clean diff with v1.6
Randomless Jan 13, 2026
e90a330
remove HEAD<< artifacts from rebase process
Jan 14, 2026
be2b53b
revert pyinstall version in cuda122.yaml as request
Jan 15, 2026
a71ccc2
移除 REPO_REF 版本锁定,改为使用 main 分支最新代码
Randomless Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build-release-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -648,4 +648,4 @@ jobs:
repository: ${{ github.repository }}
tag_name: ${{ github.ref }}
files: faster_whisper_transwithai_windows_cu128-chickenrice.zip
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
40 changes: 39 additions & 1 deletion build_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,43 @@ def build():
# Verify build succeeded and check for CUDA libraries
if result.returncode == 0:
dist_dir = Path("dist/faster_whisper_transwithai_chickenrice")
# Build modal_infer if modal.spec is present (separate target).
modal_spec = Path("modal.spec")
if modal_spec.exists():
# Ensure modal dependencies are available in the current env.
try:
import modal # noqa: F401
import questionary # noqa: F401
except ImportError:
print("\nmodal/questionary not found; installing for modal.spec build...")
install_cmd = [
sys.executable, "-m", "pip", "install",
"modal", "questionary",
]
install_result = subprocess.run(install_cmd, capture_output=False)
if install_result.returncode != 0:
print("\nFailed to install modal/questionary.")
return 1

modal_cmd = [
sys.executable, "-m", "PyInstaller",
"--clean",
"--noconfirm",
"--distpath", str(Path("dist") / "faster_whisper_transwithai_chickenrice"),
"--workpath", str(Path("build") / "modal"),
str(modal_spec),
]
print(f"\nRunning: {' '.join(modal_cmd)}")
modal_result = subprocess.run(modal_cmd, capture_output=False)
if modal_result.returncode != 0:
print("\nModal build failed!")
return 1

dist_root = Path("dist")
dist_dir = dist_root / "faster_whisper_transwithai_chickenrice"
engine_dir = dist_root / "engine"
client_dir = dist_root / "client"

if dist_dir.exists():
# Quick verification of critical libraries
print("\nVerifying CUDA libraries in distribution...")
Expand Down Expand Up @@ -258,4 +295,5 @@ def build():
return 0

if __name__ == "__main__":
sys.exit(build())
sys.exit(build())

21 changes: 21 additions & 0 deletions environment-modal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Conda environment for Modal inference (local client only)
# This environment is for running modal_infer.py locally to submit jobs to Modal
name: faster-whisper-modal
channels:
- conda-forge
- defaults

dependencies:
# Python version
- python=3.10

# Core dependencies
- pip

# Pip dependencies
- pip:
# Modal client for submitting jobs
- modal

# Interactive CLI prompts
- questionary
54 changes: 54 additions & 0 deletions modal.spec
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# -*- mode: python ; coding: utf-8 -*-
import os
from PyInstaller.utils.hooks import collect_all

block_cipher = None

datas = [("environment-cuda128.yml", ".")]
binaries = []
hiddenimports = []

for package in ["modal", "questionary", "prompt_toolkit", "rich", "typer", "click"]:
try:
pkg_datas, pkg_binaries, pkg_hiddenimports = collect_all(package)
datas += pkg_datas
binaries += pkg_binaries
hiddenimports += pkg_hiddenimports
except Exception:
pass

a = Analysis(
["modal_infer.py"],
pathex=[],
binaries=binaries,
datas=datas,
hiddenimports=hiddenimports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

exe = EXE(
pyz,
a.scripts,
a.binaries,
a.zipfiles,
a.datas,
exclude_binaries=False,
name="modal_infer",
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon="transwithai.ico" if os.path.exists("transwithai.ico") else None,
)
68 changes: 45 additions & 23 deletions modal_infer.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,38 @@
"""feature-modal: 交互式 CLI,完成 Modal App 构建、音频上传、推理执行与结果回传。"""

from __future__ import annotations

import argparse
import io
import logging
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from pathlib import Path, PurePosixPath
import subprocess
from typing import Dict, List, Optional, Sequence, Tuple
from uuid import uuid4

def ensure_utf8_stdio() -> None:
for name in ("stdout", "stderr"):
stream = getattr(sys, name, None)
if stream is None:
continue
try:
encoding = getattr(stream, "encoding", None)
if encoding and encoding.lower().startswith("utf-8"):
continue
if hasattr(stream, "reconfigure"):
stream.reconfigure(encoding="utf-8", errors="replace")
elif hasattr(stream, "buffer"):
setattr(
sys,
name,
io.TextIOWrapper(stream.buffer, encoding="utf-8", errors="replace"),
)
except Exception:
pass

ensure_utf8_stdio()

try:
import questionary # type: ignore
from questionary import Choice # type: ignore
Expand All @@ -28,11 +49,11 @@
APP_NAME = "Faster-Whisper-TransWithAI-ChickenRice"
REPO_URL = "https://github.com/TransWithAI/Faster-Whisper-TransWithAI-ChickenRice"
VOLUME_NAME = "Faster_Whisper"
VOLUME_ROOT = Path("/Faster_Whisper")
VOLUME_ROOT = "/Faster_Whisper"
REMOTE_MOUNT = VOLUME_ROOT
APP_ROOT_REL = Path(APP_NAME)
SESSION_SUBDIR = Path("sessions")
REPO_VOLUME_DIR = VOLUME_ROOT / "repo"
APP_ROOT_REL = APP_NAME
SESSION_SUBDIR = "sessions"
REPO_VOLUME_DIR = f"{VOLUME_ROOT}/repo"
SUB_FORMATS = "srt,vtt,lrc"
SUB_SUFFIXES = {".srt", ".vtt", ".lrc"}
AUDIO_SUFFIXES = {
Expand Down Expand Up @@ -63,6 +84,10 @@
"B200",
]

def resolve_resource_path(filename: str) -> Path:
base_dir = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent))
return base_dir / filename


@dataclass
class ModelProfile:
Expand Down Expand Up @@ -117,7 +142,8 @@ def rel_to_volume_path(path: Path) -> str:


def rel_to_container_path(path: Path) -> str:
return str((REMOTE_MOUNT / path).as_posix())
base = PurePosixPath(REMOTE_MOUNT)
return str((base / path.as_posix()).as_posix())


def volume_path_to_relative(path: str) -> Path:
Expand Down Expand Up @@ -321,7 +347,7 @@ def upload_single_file(
base_dir: 基础目录(用于文件夹模式,输出到此目录)
"""
session_id = f"{datetime.now().strftime('%Y%m%d-%H%M%S')}-{uuid4().hex[:6]}"
remote_session_rel = SESSION_SUBDIR / session_id
remote_session_rel = Path(SESSION_SUBDIR) / session_id
remote_logs_rel = remote_session_rel / "logs"

# 使用固定文件名避免全角字符等问题
Expand Down Expand Up @@ -389,7 +415,7 @@ def build_modal_image() -> modal.Image:
modal.Image.micromamba(python_version="3.10")
.apt_install("git")
.micromamba_install(
spec_file="environment-cuda128.yml",
spec_file=str(resolve_resource_path("environment-cuda128.yml")),
channels=["conda-forge", "defaults"],
)
.pip_install("modal", "questionary")
Expand Down Expand Up @@ -625,7 +651,7 @@ def run(cmd: Sequence[str], cwd: Optional[str] = None, env: Optional[dict] = Non
subprocess.run(cmd, check=True, cwd=cwd, env=env)

mount_root = Path(job["mount_root"])
repo_dir = REPO_VOLUME_DIR
repo_dir = Path(REPO_VOLUME_DIR)

# log 文件放在 session 目录下,而不是 logs 子目录
session_dir = Path(job["remote_output_dir"])
Expand All @@ -640,10 +666,10 @@ def log(msg: str) -> None:

if not (repo_dir / ".git").exists():
log("开始克隆仓库...")
run(["git", "clone", REPO_URL, str(repo_dir)])
run(["git", "clone", "--depth", "1", REPO_URL, str(repo_dir)])
else:
log("更新仓库...")
run(["git", "-C", str(repo_dir), "fetch", "origin", "main"])
run(["git", "-C", str(repo_dir), "fetch", "origin"])
run(["git", "-C", str(repo_dir), "reset", "--hard", "origin/main"])

model_profile = job["model_profile"]
Expand Down Expand Up @@ -689,16 +715,12 @@ def snapshot(path: str) -> set:
cmd = [
"python",
str(repo_dir / "infer.py"),
"--device",
"cuda",
"--model_name_or_path",
str(model_path),
"--sub_formats",
job["sub_formats"],
"--log_level",
"INFO",
"--output_dir",
str(output_dir),
"--audio_suffixes", "mp3,wav,flac,m4a,aac,ogg,wma,mp4,mkv,avi,mov,webm,flv,wmv",
"--device","cuda",
"--model_name_or_path",str(model_path),
"--sub_formats",job["sub_formats"],
"--log_level","INFO",
"--output_dir",str(output_dir),
]
if job["enable_batching"]:
cmd.append("--enable_batching")
Expand Down
30 changes: 5 additions & 25 deletions project.spec
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ datas += [
]

a = Analysis(
['infer.py', 'modal_infer.py'],
['infer.py'],
pathex=[],
binaries=binaries,
datas=datas,
Expand Down Expand Up @@ -374,9 +374,9 @@ a = Analysis(

pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)

infer_exe = EXE(
exe = EXE(
pyz,
[a.scripts[0]],
a.scripts,
[],
exclude_binaries=True,
name='infer',
Expand All @@ -393,33 +393,13 @@ infer_exe = EXE(
icon='transwithai.ico' if os.path.exists('transwithai.ico') else None,
)

modal_exe = EXE(
pyz,
[a.scripts[1]],
[],
exclude_binaries=True,
name='modal_infer',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
console=True,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='transwithai.ico' if os.path.exists('transwithai.ico') else None,
)

coll = COLLECT(
infer_exe,
modal_exe,
exe,
a.binaries,
a.zipfiles,
a.datas,
strip=False,
upx=False,
upx_exclude=[],
name='faster_whisper_transwithai_chickenrice',
)
)
7 changes: 4 additions & 3 deletions 使用说明.txt
Original file line number Diff line number Diff line change
Expand Up @@ -100,13 +100,14 @@ GPU模式(仅限NVIDIA显卡):

1. 环境配置:

使用现有的 Conda 环境(已包含 modal 支持):
使用 Conda 创建轻量级环境(仅需 modal 和 questionary 库):

```bash
conda activate faster-whisper-cu118 # 或 cu122, cu128
conda env create -f environment-modal.yml
conda activate faster-whisper-modal
```

或在现有环境中手动安装
或手动安装
```bash
pip install modal questionary
```
Expand Down
Loading