Téléverser les fichiers vers "/"

This commit is contained in:
Johnny Fontaine 2026-04-09 08:26:31 +00:00
parent f95bce07d7
commit 5ee99efaff
5 changed files with 3755 additions and 0 deletions

851
build.py Normal file
View File

@ -0,0 +1,851 @@
#!/usr/bin/env python3
"""
build.py Compilation + installation de TranscribeStation
Produit un binaire onefile PyInstaller et installe les raccourcis système.
Usage :
python build.py # build + install (plateforme courante)
python build.py --linux # build Linux
python build.py --windows # build Windows (Wine ou .bat natif)
python build.py --both # Linux + Windows
python build.py --install-only # installe sans recompiler
python build.py --clean # nettoyer build/ dist/ *.spec icon.*
Raccourcis créés :
Linux ~/.local/share/applications/TranscribeStation.desktop
~/.local/share/icons/hicolor/256x256/apps/TranscribeStation.png
~/Bureau/TranscribeStation.desktop (si dossier existe)
Windows %APPDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\TranscribeStation.lnk
%USERPROFILE%\\Desktop\\TranscribeStation.lnk
"""
import sys
import os
import shutil
import platform
import subprocess
import argparse
import textwrap
import urllib.request
import zipfile
import tempfile
from pathlib import Path
# ─── Configuration ────────────────────────────────────────────────────────────
APP_NAME = "TranscribeStation"
APP_VERSION = "1.2.0"
APP_AUTHOR = "H3Campus"
APP_URL = "https://github.com/h3campus/transcribe-station"
APP_COMMENT = "Gestionnaire de transcription audio JT-Tools by Johnny"
APP_COMMENT_WIN = "Gestionnaire de transcription audio - JT-Tools by Johnny"
APP_CATS = "Audio;AudioVideo;Office;"
MAIN_SCRIPT = "transcribe_station.py"
ICON_ICO = Path("icon.ico")
ICON_PNG = Path("icon.png")
BUILD_DIR = Path("build")
DIST_DIR = Path("dist")
HIDDEN_IMPORTS = [
"PySide6.QtMultimedia",
"PySide6.QtCore",
"PySide6.QtGui",
"PySide6.QtWidgets",
"soundfile",
"numpy",
"hid",
"cffi",
"_cffi_backend",
]
# --collect-all hid : inclut la DLL native hidapi.dll dans le bundle Windows
COLLECT_ALL = ["hid"]
ADD_DATA: list[str] = []
# hidapi.dll natif Windows (absent du package pip hid 1.x qui est un pur wrapper ctypes)
HIDAPI_VERSION = "0.14.0"
HIDAPI_DLL_URL = (
f"https://github.com/libusb/hidapi/releases/download/"
f"hidapi-{HIDAPI_VERSION}/hidapi-win.zip"
)
# ─── Couleurs terminal ─────────────────────────────────────────────────────────
R='\033[0;31m'; G='\033[0;32m'; Y='\033[1;33m'
C='\033[0;36m'; B='\033[1m'; N='\033[0m'
def ok(m): print(f"{G}[OK]{N} {m}")
def info(m):print(f"{C}[..]{N} {m}")
def warn(m):print(f"{Y}[!!]{N} {m}")
def err(m): print(f"{R}[XX]{N} {m}", file=sys.stderr)
def step(m):print(f"\n{B}{m}{N}")
# ─── hidapi.dll ───────────────────────────────────────────────────────────────
def _fetch_hidapi_dll() -> Path | None:
"""Télécharge hidapi.dll (x64) depuis les releases GitHub, mise en cache dans build/."""
dll_cache = BUILD_DIR / "hidapi.dll"
if dll_cache.exists():
ok(f"hidapi.dll en cache : {dll_cache}")
return dll_cache
info(f"Téléchargement hidapi.dll v{HIDAPI_VERSION} depuis GitHub...")
BUILD_DIR.mkdir(parents=True, exist_ok=True)
try:
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
tmp_path = tmp.name
urllib.request.urlretrieve(HIDAPI_DLL_URL, tmp_path)
with zipfile.ZipFile(tmp_path) as zf:
candidates = [n for n in zf.namelist()
if n.lower().endswith("hidapi.dll") and "x64" in n.lower()]
if not candidates:
candidates = [n for n in zf.namelist()
if n.lower().endswith("hidapi.dll")]
if candidates:
with zf.open(candidates[0]) as src, open(dll_cache, "wb") as dst:
dst.write(src.read())
ok(f"hidapi.dll extrait ({candidates[0]}) → {dll_cache}")
return dll_cache
warn("hidapi.dll introuvable dans l'archive")
except Exception as exc:
warn(f"Échec téléchargement hidapi.dll : {exc}")
finally:
try:
os.unlink(tmp_path)
except Exception:
pass
return None
def _install_hidapi_dll_onedir(internal_dir: Path) -> None:
"""Copie hidapi.dll dans _internal/ d'un bundle onedir."""
if not internal_dir.exists():
warn(f"Dossier _internal introuvable : {internal_dir}")
return
dll = _fetch_hidapi_dll()
if dll:
dest = internal_dir / "hidapi.dll"
shutil.copy2(dll, dest)
ok(f"hidapi.dll installé → {dest}")
else:
warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas sans cette DLL.")
# ─── Helpers ──────────────────────────────────────────────────────────────────
def run(cmd: list, check=True) -> int:
info(" ".join(str(c) for c in cmd))
r = subprocess.run(cmd, check=False)
if check and r.returncode != 0:
err(f"Échec (code {r.returncode})")
sys.exit(r.returncode)
return r.returncode
def pip_install(pkgs: list, python=sys.executable):
run([python, "-m", "pip", "install", "--quiet"] + pkgs)
def validate_wine_pyside(wine: str, wine_python: str) -> bool:
probe = subprocess.run(
[
wine, wine_python, "-c",
(
"from pathlib import Path; "
"import importlib.metadata as md; "
"import PySide6; "
"qt_dir = Path(PySide6.__file__).resolve().parent; "
"missing = [name for name in ('Qt6Core.dll', 'Qt6Multimedia.dll') if not (qt_dir / name).exists()]; "
"print('PYSIDE6_VERSION=' + md.version('PySide6')); "
"print('QT_DIR=' + str(qt_dir).replace(chr(92), '/')); "
"print('MISSING_DLLS=' + ','.join(missing)); "
"from PySide6 import QtCore; "
"print('QTCORE_OK=1')"
),
],
check=False,
capture_output=True,
text=True,
)
output = (probe.stdout or "") + (probe.stderr or "")
for line in output.splitlines():
if line.strip():
info(f"Wine Qt probe : {line}")
if probe.returncode != 0:
missing = ""
for line in output.splitlines():
if line.startswith("MISSING_DLLS="):
missing = line.split("=", 1)[1].strip()
break
err("PySide6 est installé dans Wine, mais QtCore ne se charge pas.")
if missing:
err(f"DLL Qt manquantes dans le wheel Windows : {missing}")
err("Le binaire Windows généré via Wine serait invalide.")
err("Utilisez le script build_windows.bat sur Windows natif.")
return False
return True
# ─── Génération d'icônes ──────────────────────────────────────────────────────
def _draw_icon(draw, sz):
"""Dessine le logo waveform TranscribeStation."""
draw.rounded_rectangle(
[0, 0, sz - 1, sz - 1],
radius=int(sz * 0.18),
fill=(59, 110, 220, 255),
)
bars = [0.25, 0.45, 0.70, 0.90, 0.65, 1.0, 0.55, 0.80, 0.40, 0.20]
n, mg = len(bars), sz * 0.13
bw = (sz - 2 * mg) / (n * 1.7)
gap = (sz - 2 * mg) / n
mid = sz / 2
for i, h in enumerate(bars):
x = mg + i * gap + (gap - bw) / 2
bh = h * sz * 0.36
draw.rounded_rectangle(
[x, mid - bh, x + bw, mid + bh],
radius=bw / 2,
fill=(255, 255, 255, 215),
)
def generate_icons() -> bool:
"""Génère icon.png (256px) et icon.ico (multi-tailles). Retourne True si OK."""
try:
from PIL import Image, ImageDraw
except ImportError:
warn("Pillow absent → icônes ignorées (pip install Pillow)")
return False
sizes_ico = [16, 32, 48, 64, 128, 256]
images = []
for sz in sizes_ico:
img = Image.new("RGBA", (sz, sz), (0, 0, 0, 0))
_draw_icon(ImageDraw.Draw(img), sz)
images.append(img)
# PNG 256 px pour Linux XDG
images[-1].save(str(ICON_PNG))
ok(f"Icône PNG : {ICON_PNG}")
# ICO multi-tailles pour Windows
images[0].save(
str(ICON_ICO), format="ICO",
sizes=[(s, s) for s in sizes_ico],
append_images=images[1:],
)
ok(f"Icône ICO : {ICON_ICO}")
return True
# ─── Raccourci GNOME / Linux ──────────────────────────────────────────────────
def install_linux_shortcut(binary: Path):
step("📌 Installation du raccourci GNOME...")
binary = binary.resolve()
# 1. Icône → XDG hicolor
xdg_icon_dir = Path.home() / ".local/share/icons/hicolor/256x256/apps"
xdg_icon_dir.mkdir(parents=True, exist_ok=True)
xdg_icon = xdg_icon_dir / f"{APP_NAME}.png"
if ICON_PNG.exists():
shutil.copy(ICON_PNG, xdg_icon)
ok(f"Icône installée : {xdg_icon}")
else:
warn("icon.png absent — raccourci sans icône")
# 2. Fichier .desktop → applications
apps_dir = Path.home() / ".local/share/applications"
apps_dir.mkdir(parents=True, exist_ok=True)
desktop_path = apps_dir / f"{APP_NAME}.desktop"
desktop_content = textwrap.dedent(f"""\
[Desktop Entry]
Version=1.0
Type=Application
Name={APP_NAME}
GenericName=Transcription Audio
Comment={APP_COMMENT}
Exec={binary}
Icon={APP_NAME}
Terminal=false
Categories={APP_CATS}
StartupNotify=true
StartupWMClass={APP_NAME}
Keywords=transcription;audio;dictée;pédalier;
""")
desktop_path.write_text(desktop_content, encoding="utf-8")
desktop_path.chmod(0o755)
ok(f".desktop installé : {desktop_path}")
# 3. Raccourci Bureau (Desktop)
for bureau in ["Bureau", "Desktop", "Escritorio"]:
bureau_dir = Path.home() / bureau
if bureau_dir.is_dir():
bureau_link = bureau_dir / f"{APP_NAME}.desktop"
shutil.copy(desktop_path, bureau_link)
bureau_link.chmod(0o755)
ok(f"Raccourci bureau : {bureau_link}")
break
# 4. Mettre à jour la base des .desktop
for cmd in [
["update-desktop-database", str(apps_dir)],
["gtk-update-icon-cache", "-f", "-t",
str(Path.home() / ".local/share/icons/hicolor")],
["xdg-desktop-menu", "forceupdate"],
]:
if shutil.which(cmd[0]):
run(cmd, check=False)
ok("Raccourci GNOME installé. Visible au prochain rechargement de session.")
def uninstall_linux_shortcut():
step("🗑 Désinstallation raccourci GNOME...")
targets = [
Path.home() / f".local/share/applications/{APP_NAME}.desktop",
Path.home() / f".local/share/icons/hicolor/256x256/apps/{APP_NAME}.png",
]
for bureau in ["Bureau", "Desktop", "Escritorio"]:
targets.append(Path.home() / bureau / f"{APP_NAME}.desktop")
for t in targets:
if t.exists():
t.unlink()
ok(f"Supprimé : {t}")
# ─── Raccourci Windows (depuis Windows natif) ─────────────────────────────────
def install_windows_shortcut(binary: Path):
"""Crée Menu Démarrer + Bureau via PowerShell (à appeler depuis Windows)."""
step("📌 Installation du raccourci Windows...")
binary = binary.resolve()
# param() en premiere ligne : ExePath est passe par le bat via -ExePath "..."
# Ne PAS hardcoder $ExePath dans le corps du script (serait ecrase par la ligne param)
ps_script = textwrap.dedent(f"""
param([string]$ExePath)
$AppName = "{APP_NAME}"
$Comment = "{APP_COMMENT_WIN}"
function New-Shortcut([string]$dest) {{
$wsh = New-Object -ComObject WScript.Shell
$lnk = $wsh.CreateShortcut($dest)
$lnk.TargetPath = $ExePath
$lnk.IconLocation = "$ExePath,0"
$lnk.Description = $Comment
$lnk.WorkingDirectory = [System.IO.Path]::GetDirectoryName($ExePath)
$lnk.Save()
Write-Host "[OK] Raccourci : $dest"
}}
$startMenu = "$env:APPDATA\\Microsoft\\Windows\\Start Menu\\Programs"
New-Shortcut "$startMenu\\$AppName.lnk"
$desktop = [Environment]::GetFolderPath("Desktop")
New-Shortcut "$desktop\\$AppName.lnk"
Write-Host ""
Write-Host "[OK] Shortcuts installed successfully."
""").strip()
ps_file = DIST_DIR / "windows" / "install_shortcut.ps1"
ps_file.parent.mkdir(parents=True, exist_ok=True)
ps_file.write_text(ps_script, encoding="utf-8-sig")
ok(f"Script PowerShell généré : {ps_file}")
if platform.system() == "Windows":
info("Exécution de PowerShell...")
run(["powershell", "-ExecutionPolicy", "Bypass",
"-File", str(ps_file)], check=False)
else:
warn("Cross-compilation : exécutez ce script sur la machine Windows cible :")
warn(f" powershell -ExecutionPolicy Bypass -File {ps_file}")
# ─── Build Linux ──────────────────────────────────────────────────────────────
def build_linux(install: bool = True):
step("🐧 Build Linux")
pip_install(["pyinstaller", "Pillow"])
generate_icons()
run([sys.executable, "-m", "PyInstaller",
"--onefile", "--clean", "--noconfirm",
f"--name={APP_NAME}",
"--distpath=dist/linux",
"--workpath=build/linux",
"--specpath=build/linux",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
*(f"--add-data={d}" for d in ADD_DATA),
f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
MAIN_SCRIPT,
])
binary = Path(f"dist/linux/{APP_NAME}")
if binary.exists():
binary.chmod(0o755)
size = binary.stat().st_size // (1024 * 1024)
ok(f"Binaire Linux : {binary} ({size} Mo)")
if install:
install_linux_shortcut(binary)
else:
err("Binaire introuvable dans dist/linux/")
# ─── Build Windows ────────────────────────────────────────────────────────────
def build_windows(install: bool = True):
step("🪟 Build Windows")
# ── Sur machine Windows native ────────────────────────────────────────────
if platform.system() == "Windows":
pip_install(["pyinstaller", "Pillow"])
generate_icons()
icon_arg = f"--icon={ICON_ICO.resolve()}" if ICON_ICO.exists() else ""
main_script = str(Path(MAIN_SCRIPT).resolve())
run([sys.executable, "-m", "PyInstaller",
"--onedir", "--clean", "--noconfirm", "--windowed",
f"--name={APP_NAME}",
"--distpath=dist/windows",
"--workpath=build/windows",
"--specpath=build/windows",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
icon_arg,
main_script,
])
binary = Path(f"dist/windows/{APP_NAME}/{APP_NAME}.exe")
if binary.exists():
ok(f"Binaire Windows : {binary}")
_install_hidapi_dll_onedir(Path(f"dist/windows/{APP_NAME}/_internal"))
if install:
install_windows_shortcut(binary)
else:
err("Binaire introuvable.")
return
# ── Cross-compilation depuis Linux via Wine ────────────────────────────────
wine = shutil.which("wine") or shutil.which("wine64")
wine_python = None
if wine:
for candidate in [
Path.home() / ".wine/drive_c/Python312/python.exe",
Path.home() / ".wine/drive_c/Python311/python.exe",
Path.home() / ".wine/drive_c/Python310/python.exe",
]:
if candidate.exists():
wine_python = candidate
break
if wine and wine_python:
info(f"Wine Python : {wine_python}")
pip_install(["pyinstaller", "PySide6", "numpy", "soundfile", "hid", "Pillow"],
python=str(wine_python))
generate_icons()
if not validate_wine_pyside(wine, str(wine_python)):
sys.exit(1)
# Récupérer hidapi.dll pour l'embarquer dans le onefile via --add-binary
hidapi_dll = _fetch_hidapi_dll()
add_binary_args = [f"--add-binary={hidapi_dll}:."] if hidapi_dll else []
if not hidapi_dll:
warn("hidapi.dll non disponible — le pédalier ne fonctionnera pas dans le .exe Wine")
run([wine, str(wine_python), "-m", "PyInstaller",
"--onefile", "--clean", "--noconfirm", "--windowed",
f"--name={APP_NAME}",
"--distpath=dist/windows",
"--workpath=build/windows",
"--specpath=build/windows",
"--log-level=WARN",
*(f"--hidden-import={h}" for h in HIDDEN_IMPORTS),
*(f"--collect-all={m}" for m in COLLECT_ALL),
*add_binary_args,
f"--icon={ICON_ICO}" if ICON_ICO.exists() else "",
MAIN_SCRIPT,
])
binary = Path(f"dist/windows/{APP_NAME}.exe")
if binary.exists():
size = binary.stat().st_size // (1024 * 1024)
ok(f"Binaire Windows : {binary} ({size} Mo)")
install_windows_shortcut(Path(f"dist/windows/{APP_NAME}.exe"))
else:
warn("Wine ou Python Windows introuvable → génération du script natif Windows")
_write_windows_bat(install=install)
def _write_windows_bat(install: bool = True):
"""Genere build_windows.bat + _build_icons.py dans dist/ pour execution native sur Windows."""
out_dir = Path("dist") / "windows"
out_dir.mkdir(parents=True, exist_ok=True)
bat_path = out_dir / "build_windows.bat"
icons_path = out_dir / "_build_icons.py"
# ── Script Python icones dans un fichier separe (evite l'interpretation batch) ──
icon_py = (
"from PIL import Image, ImageDraw\n"
"sizes = [16, 32, 48, 64, 128, 256]\n"
"imgs = []\n"
"for sz in sizes:\n"
" img = Image.new('RGBA', (sz, sz), (0, 0, 0, 0))\n"
" d = ImageDraw.Draw(img)\n"
" d.rounded_rectangle([0, 0, sz-1, sz-1], radius=int(sz*.18), fill=(59, 110, 220, 255))\n"
" bars = [.25, .45, .7, .9, .65, 1., .55, .8, .4, .2]\n"
" n, mg = len(bars), sz*.13\n"
" bw = (sz - 2*mg) / (n * 1.7)\n"
" gap = (sz - 2*mg) / n\n"
" mid = sz / 2\n"
" for i, h in enumerate(bars):\n"
" x = mg + i*gap + (gap - bw) / 2\n"
" bh = h * sz * .36\n"
" d.rounded_rectangle([x, mid-bh, x+bw, mid+bh], radius=bw/2, fill=(255, 255, 255, 215))\n"
" imgs.append(img)\n"
"imgs[-1].save('icon.png')\n"
"imgs[0].save('icon.ico', format='ICO', sizes=[(s,s) for s in sizes], append_images=imgs[1:])\n"
"print('[OK] Icones generees')\n"
)
icons_path.write_text(icon_py, encoding="utf-8")
ok(f"Script icones genere : {icons_path}")
# ── Arguments hidden-import + collect-all PyInstaller ──
hi_lines = " ^\n ".join(f"--hidden-import {h}" for h in HIDDEN_IMPORTS)
ca_lines = " ^\n ".join(f"--collect-all {m}" for m in COLLECT_ALL)
shortcut_block = ""
if install:
shortcut_block = (
"echo [5/5] Installation des raccourcis...\n"
"REM Raccourci vers binaire LOCAL (WScript.Shell refuse les chemins reseau/partages)\n"
f'set "LOCAL_EXE=%LOCAL_DIST%\\{APP_NAME}\\{APP_NAME}.exe"\n'
f'if exist "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" (\n'
f' powershell -NoProfile -ExecutionPolicy Bypass ^\n'
f' -File "%PROJECT_ROOT%dist\\windows\\install_shortcut.ps1" ^\n'
f' -ExePath "%LOCAL_EXE%"\n'
f') else (\n'
f' echo [!!] Script PowerShell introuvable\n'
f')\n'
)
# Venv court pour contourner MAX_PATH avec PySide6 (Microsoft Store Python)
# C:\ts\.venv\Lib\site-packages\ = 30 chars vs ~140 chars pour AppData\Packages\...
VENV = "C:\\ts\\.venv"
# ASCII uniquement dans le bat
bat = (
"@echo off\n"
"setlocal EnableDelayedExpansion\n"
f"REM build_windows.bat -- {APP_NAME} v{APP_VERSION}\n"
"REM A executer sur une machine Windows avec Python 3.12+\n"
"echo.\n"
f"echo *** Build {APP_NAME} v{APP_VERSION} pour Windows ***\n"
"echo.\n"
"\n"
"REM -- Localisation du projet\n"
"set \"SCRIPT_DIR=%~dp0\"\n"
"for %%I in (\"%SCRIPT_DIR%\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
"for %%I in (\"%SCRIPT_DIR%\\..\\..\") do set \"PROJECT_ROOT=%%~fI\\\"\n"
f"if exist \"%PROJECT_ROOT%{MAIN_SCRIPT}\" goto ROOT_FOUND\n"
f"echo [XX] {MAIN_SCRIPT} introuvable.\n"
"pause & exit /b 1\n"
"\n"
":ROOT_FOUND\n"
"echo [OK] Dossier projet : %PROJECT_ROOT%\n"
"echo.\n"
"\n"
"REM -- Localisation d'un vrai Python (evite le stub Windows Store)\n"
"set \"REAL_PYTHON=\"\n"
"\n"
"REM Essai 1 : py launcher (installe avec Python officiel)\n"
"where py >nul 2>&1\n"
"if not errorlevel 1 (\n"
" py -3 -c \"import sys; print(sys.executable)\" >nul 2>&1\n"
" if not errorlevel 1 (\n"
" for /f \"delims=\" %%P in ('py -3 -c \"import sys; print(sys.executable)\"') do set \"REAL_PYTHON=%%P\"\n"
" )\n"
")\n"
"\n"
"REM Essai 2 : emplacements standard Python 3.x\n"
"if not defined REAL_PYTHON (\n"
" for %%V in (313 312 311 310 39) do (\n"
" if not defined REAL_PYTHON (\n"
" if exist \"C:\\Python%%V\\python.exe\" set \"REAL_PYTHON=C:\\Python%%V\\python.exe\"\n"
" )\n"
" )\n"
")\n"
"REM Essai 3 : AppData\\Local\\Programs\\Python\n"
"if not defined REAL_PYTHON (\n"
" for %%V in (313 312 311 310 39) do (\n"
" if not defined REAL_PYTHON (\n"
" set \"_CANDIDATE=%LOCALAPPDATA%\\Programs\\Python\\Python%%V\\python.exe\"\n"
" if exist \"!_CANDIDATE!\" set \"REAL_PYTHON=!_CANDIDATE!\"\n"
" )\n"
" )\n"
")\n"
"\n"
"if not defined REAL_PYTHON (\n"
" echo [XX] Python 3 introuvable. Installez Python depuis https://www.python.org/downloads/\n"
" echo [XX] Assurez-vous de cocher \"Add python.exe to PATH\" lors de l'installation.\n"
" pause & exit /b 1\n"
")\n"
"echo [OK] Python trouve : %REAL_PYTHON%\n"
"\n"
"REM -- Venv a chemin court pour eviter MAX_PATH avec PySide6\n"
f"set \"VENV={VENV}\"\n"
"set \"VPYTHON=%VENV%\\Scripts\\python.exe\"\n"
"set \"VPIP=%VENV%\\Scripts\\pip.exe\"\n"
"echo [1/5] Preparation venv dans %VENV%...\n"
"if exist \"%VENV%\" (\n"
" \"%VPYTHON%\" -c \"import sys\" >nul 2>&1\n"
" if errorlevel 1 (\n"
" echo [!!] Venv existant invalide - recreation...\n"
" rmdir /S /Q \"%VENV%\"\n"
" )\n"
")\n"
"if not exist \"%VPYTHON%\" (\n"
" mkdir C:\\ts 2>nul\n"
" \"%REAL_PYTHON%\" -m venv \"%VENV%\"\n"
" if errorlevel 1 ( echo ERREUR venv & pause & exit /b 1 )\n"
")\n"
"echo [OK] Venv pret.\n"
"\n"
"echo [2/5] Installation des dependances Python...\n"
"\"%VPIP%\" install --quiet --upgrade pip\n"
"\"%VPIP%\" install --quiet pyinstaller PySide6 numpy soundfile hid Pillow\n"
"if errorlevel 1 ( echo ERREUR pip & pause & exit /b 1 )\n"
"\"%VPYTHON%\" -c \"from PySide6.QtMultimedia import QMediaPlayer; print('[OK] PySide6 + QtMultimedia OK')\"\n"
"if errorlevel 1 ( echo ERREUR PySide6.QtMultimedia & pause & exit /b 1 )\n"
"\n"
"echo [3/5] Generation des icones...\n"
"copy /Y \"%SCRIPT_DIR%_build_icons.py\" \"%PROJECT_ROOT%_build_icons.py\" > nul\n"
"\"%VPYTHON%\" \"%PROJECT_ROOT%_build_icons.py\"\n"
"if errorlevel 1 ( echo ERREUR icones & pause & exit /b 1 )\n"
"del \"%PROJECT_ROOT%_build_icons.py\" > nul 2>&1\n"
"\n"
"REM -- Build dans C:\\ts (chemin local court) pour eviter les erreurs reseau/MAX_PATH\n"
"set \"LOCAL_DIST=C:\\ts\\dist\"\n"
"set \"LOCAL_BUILD=C:\\ts\\build\"\n"
"\n"
"echo [4/5] Compilation PyInstaller (sortie locale C:\\ts\\dist)...\n"
"\"%VENV%\\Scripts\\pyinstaller.exe\" --onedir --windowed --clean --noconfirm ^\n"
f" --name {APP_NAME} ^\n"
" --distpath \"%LOCAL_DIST%\" ^\n"
" --workpath \"%LOCAL_BUILD%\" ^\n"
" --specpath \"%LOCAL_BUILD%\" ^\n"
" --icon \"%PROJECT_ROOT%icon.ico\" ^\n"
f" {hi_lines} ^\n"
f" {ca_lines} ^\n"
f" \"%PROJECT_ROOT%{MAIN_SCRIPT}\"\n"
"if errorlevel 1 ( echo ERREUR PyInstaller & pause & exit /b 1 )\n"
"\n"
"REM -- Ajout de hidapi.dll (absent du package pip hid)\n"
f"set \"HIDAPI_URL=https://github.com/libusb/hidapi/releases/download/hidapi-{HIDAPI_VERSION}/hidapi-win.zip\"\n"
f"set \"HIDAPI_ZIP=C:\\ts\\hidapi-win.zip\"\n"
f"set \"HIDAPI_DLL=%LOCAL_DIST%\\{APP_NAME}\\_internal\\hidapi.dll\"\n"
"if not exist \"%HIDAPI_DLL%\" (\n"
" echo Telechargement hidapi.dll...\n"
" powershell -NoProfile -Command \"Invoke-WebRequest -Uri '%HIDAPI_URL%' -OutFile '%HIDAPI_ZIP%'\"\n"
" if not errorlevel 1 (\n"
" powershell -NoProfile -Command \""
f"Expand-Archive -Force '%HIDAPI_ZIP%' 'C:\\\\ts\\\\hidapi_tmp'; "
f"$dll = Get-ChildItem 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Filter hidapi.dll | "
f"Where-Object {{ $_.FullName -match 'x64' }} | Select-Object -First 1; "
f"if ($dll) {{ Copy-Item $dll.FullName '%HIDAPI_DLL%'; Write-Host '[OK] hidapi.dll installe' }} "
f"else {{ Write-Host '[!!] hidapi.dll x64 introuvable dans archive' }}; "
f"Remove-Item 'C:\\\\ts\\\\hidapi_tmp' -Recurse -Force\"\n"
" ) else (\n"
" echo [!!] Echec telechargement hidapi.dll - le pedalier ne fonctionnera pas.\n"
" )\n"
") else (\n"
" echo [OK] hidapi.dll deja present.\n"
")\n"
"\n"
"echo Copie du binaire vers le dossier projet...\n"
f"if exist \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\" rmdir /S /Q \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\"\n"
f"xcopy /E /I /Y \"%LOCAL_DIST%\\{APP_NAME}\" \"%PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\"\n"
"if errorlevel 1 ( echo ERREUR copie xcopy & pause & exit /b 1 )\n"
f"echo [OK] Binaire copie dans %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\\n"
"\n"
+ shortcut_block +
"\n"
"copy /Y \"%PROJECT_ROOT%icon.ico\" \"C:\\ts\\icon.ico\" > nul\n"
"\n"
"echo [6/6] Creation de l'installeur (Inno Setup)...\n"
"set \"ISCC_EXE=\"\n"
"if exist \"C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\" "
"set \"ISCC_EXE=C:\\Program Files (x86)\\Inno Setup 6\\ISCC.exe\"\n"
"if exist \"C:\\Program Files\\Inno Setup 6\\ISCC.exe\" "
"set \"ISCC_EXE=C:\\Program Files\\Inno Setup 6\\ISCC.exe\"\n"
"if not defined ISCC_EXE (\n"
" echo [!!] Inno Setup 6 non installe - installeur ignore.\n"
" echo [!!] Telecharger gratuitement : https://jrsoftware.org/isdl.php\n"
" echo [!!] Puis relancez ce script pour generer l'installeur.\n"
" goto BUILD_DONE\n"
")\n"
f"mkdir C:\\ts\\installer 2>nul\n"
f"echo Compilation Inno Setup...\n"
f"\"%ISCC_EXE%\" \"%SCRIPT_DIR%{APP_NAME}.iss\"\n"
"if errorlevel 1 ( echo [!!] Erreur Inno Setup & goto BUILD_DONE )\n"
f"echo [OK] Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe\n"
"\n"
":BUILD_DONE\n"
"echo.\n"
f"echo *** Build termine ! ***\n"
f"echo Binaire : %PROJECT_ROOT%dist\\windows\\{APP_NAME}\\{APP_NAME}.exe\n"
f"echo Installeur : C:\\ts\\installer\\{APP_NAME}_Setup_v{APP_VERSION}.exe (si Inno Setup installe)\n"
"echo.\n"
"pause\n"
)
bat_crlf = bat.replace("\n", "\r\n")
bat_path.write_bytes(bat_crlf.encode("ascii", errors="replace"))
ok(f"Script Windows genere : {bat_path}")
ok(f"Script icones genere : {icons_path}")
generate_inno_script()
warn(f"Sur Windows : lancez dist\\windows\\build_windows.bat depuis la racine du projet")
warn(f"Inno Setup requis pour l'installeur : https://jrsoftware.org/isdl.php")
# ─── Installeur Inno Setup ────────────────────────────────────────────────────
def generate_inno_script():
"""
Génère TranscribeStation.iss pour Inno Setup 6.
Sortie : dist/windows/TranscribeStation.iss (CRLF + BOM UTF-8)
Compilation : ISCC.exe TranscribeStation.iss
Résultat : C:\\ts\\installer\\TranscribeStation_Setup_v{APP_VERSION}.exe
"""
# On utilise une string ordinaire pour éviter les conflits entre
# les accolades Inno Setup {app} et le f-string Python.
# N = APP_NAME, V = APP_VERSION, A = APP_AUTHOR, U = APP_URL
N, V, A, U = APP_NAME, APP_VERSION, APP_AUTHOR, APP_URL
iss = (
"[Setup]\n"
f"AppName={N}\n"
f"AppVersion={V}\n"
f"AppVerName={N} {V}\n"
f"AppPublisher={A}\n"
f"AppPublisherURL={U}\n"
f"AppSupportURL={U}\n"
f"DefaultDirName={{localappdata}}\\Programs\\{N}\n"
f"DefaultGroupName={N}\n"
"AllowNoIcons=yes\n"
"OutputDir=C:\\ts\\installer\n"
f"OutputBaseFilename={N}_Setup_v{V}\n"
"SetupIconFile=C:\\ts\\icon.ico\n"
"Compression=lzma2/ultra64\n"
"SolidCompression=yes\n"
"WizardStyle=modern\n"
"WizardSizePercent=120\n"
"PrivilegesRequired=lowest\n"
"ArchitecturesAllowed=x64compatible\n"
"ArchitecturesInstallIn64BitMode=x64compatible\n"
f"UninstallDisplayIcon={{app}}\\{N}.exe\n"
f"UninstallDisplayName={N} {V}\n"
"CloseApplications=yes\n"
"\n"
"[Languages]\n"
'Name: "french"; MessagesFile: "compiler:Languages\\French.isl"\n'
"\n"
"[Tasks]\n"
'Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; '
'GroupDescription: "{cm:AdditionalIcons}"\n'
"\n"
"[Files]\n"
f'Source: "C:\\ts\\dist\\{N}\\*"; '
'DestDir: "{app}"; '
"Flags: ignoreversion recursesubdirs createallsubdirs\n"
"\n"
"[Icons]\n"
f'Name: "{{group}}\\{N}"; Filename: "{{app}}\\{N}.exe"\n'
f'Name: "{{userdesktop}}\\{N}"; Filename: "{{app}}\\{N}.exe"; Tasks: desktopicon\n'
"\n"
"[Run]\n"
f'Filename: "{{app}}\\{N}.exe"; '
f'Description: "Lancer {N}"; '
"Flags: nowait postinstall skipifsilent\n"
"\n"
"[UninstallDelete]\n"
'Type: filesandordirs; Name: "{app}"\n'
"\n"
"[Code]\n"
"// Verifie que l'architecture est x64\n"
"function InitializeSetup(): Boolean;\n"
"begin\n"
" Result := True;\n"
" if not Is64BitInstallMode then begin\n"
f" MsgBox('{N} necessite Windows 64 bits.', mbError, MB_OK);\n"
" Result := False;\n"
" end;\n"
"end;\n"
)
out = DIST_DIR / "windows" / f"{APP_NAME}.iss"
out.parent.mkdir(parents=True, exist_ok=True)
# BOM UTF-8 + CRLF obligatoires pour Inno Setup
iss_crlf = iss.replace("\n", "\r\n")
out.write_bytes(b"\xef\xbb\xbf" + iss_crlf.encode("utf-8"))
ok(f"Script Inno Setup généré : {out}")
return out
# ─── Nettoyage ────────────────────────────────────────────────────────────────
def clean():
step("🧹 Nettoyage...")
for d in [BUILD_DIR, DIST_DIR]:
if d.exists():
shutil.rmtree(d); ok(f"Supprimé : {d}/")
for f in Path(".").glob("*.spec"):
f.unlink(); ok(f"Supprimé : {f}")
for icon in [ICON_ICO, ICON_PNG]:
if icon.exists():
icon.unlink(); ok(f"Supprimé : {icon}")
ok("Nettoyage terminé.")
# ─── Point d'entrée ───────────────────────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(
description=f"Build + install {APP_NAME}",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent("""
Exemples :
python build.py build + raccourcis (auto-detect OS)
python build.py --linux build Linux + raccourci GNOME
python build.py --windows build Windows + raccourci Menu Démarrer
python build.py --both les deux
python build.py --no-install build sans créer de raccourcis
python build.py --uninstall supprimer le raccourci GNOME
python build.py --clean supprimer artefacts build/dist/
"""),
)
parser.add_argument("--linux", action="store_true")
parser.add_argument("--windows", action="store_true")
parser.add_argument("--both", action="store_true")
parser.add_argument("--no-install", action="store_true",
help="Ne pas installer de raccourci")
parser.add_argument("--install-only", action="store_true",
help="Installer raccourci sans recompiler")
parser.add_argument("--uninstall", action="store_true",
help="Désinstaller raccourci GNOME")
parser.add_argument("--clean", action="store_true")
args = parser.parse_args()
os.chdir(Path(__file__).parent)
do_install = not args.no_install
if args.clean:
clean(); return
if args.uninstall:
uninstall_linux_shortcut(); return
if args.install_only:
binary = Path(f"dist/linux/{APP_NAME}")
if binary.exists():
generate_icons()
install_linux_shortcut(binary)
else:
err(f"Binaire introuvable : {binary}")
err("Lancez d'abord : python build.py --linux")
return
if args.both:
build_linux(install=do_install)
build_windows(install=do_install)
return
if args.windows:
build_windows(install=do_install); return
if args.linux or platform.system() == "Linux":
build_linux(install=do_install)
elif platform.system() == "Windows":
build_windows(install=do_install)
else:
err(f"Plateforme inconnue : {platform.system()}")
err("Utilisez --linux ou --windows")
if __name__ == "__main__":
main()

BIN
icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

4
launch.sh Normal file
View File

@ -0,0 +1,4 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/.venv/bin/activate"
exec python "$SCRIPT_DIR/transcribe_station.py" "$@"

2900
transcribe_station.py Normal file

File diff suppressed because it is too large Load Diff