diff --git a/.github/workflows/build-release-conda.yml b/.github/workflows/build-release-conda.yml index 195be49..3d22b0c 100644 --- a/.github/workflows/build-release-conda.yml +++ b/.github/workflows/build-release-conda.yml @@ -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 }} \ No newline at end of file diff --git a/build_windows.py b/build_windows.py index 83c3eb9..7932ade 100644 --- a/build_windows.py +++ b/build_windows.py @@ -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...") @@ -258,4 +295,5 @@ def build(): return 0 if __name__ == "__main__": - sys.exit(build()) \ No newline at end of file + sys.exit(build()) + diff --git a/environment-modal.yml b/environment-modal.yml new file mode 100644 index 0000000..2bef4a6 --- /dev/null +++ b/environment-modal.yml @@ -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 diff --git a/modal.spec b/modal.spec new file mode 100644 index 0000000..a9ad761 --- /dev/null +++ b/modal.spec @@ -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, +) diff --git a/modal_infer.py b/modal_infer.py index d6abd78..4cb753b 100644 --- a/modal_infer.py +++ b/modal_infer.py @@ -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 @@ -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 = { @@ -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: @@ -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: @@ -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" # 使用固定文件名避免全角字符等问题 @@ -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") @@ -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"]) @@ -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"] @@ -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") diff --git a/project.spec b/project.spec index efcb59a..b8078db 100644 --- a/project.spec +++ b/project.spec @@ -346,7 +346,7 @@ datas += [ ] a = Analysis( - ['infer.py', 'modal_infer.py'], + ['infer.py'], pathex=[], binaries=binaries, datas=datas, @@ -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', @@ -393,28 +393,8 @@ 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, @@ -422,4 +402,4 @@ coll = COLLECT( upx=False, upx_exclude=[], name='faster_whisper_transwithai_chickenrice', -) +) \ No newline at end of file diff --git "a/\344\275\277\347\224\250\350\257\264\346\230\216.txt" "b/\344\275\277\347\224\250\350\257\264\346\230\216.txt" index ae7c8cd..a55207f 100644 --- "a/\344\275\277\347\224\250\350\257\264\346\230\216.txt" +++ "b/\344\275\277\347\224\250\350\257\264\346\230\216.txt" @@ -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 ```