diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2d7c9fb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,98 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project + +TranscribeStation est une application de bureau PyQt6 pour la gestion de fichiers audio à transcrire, avec support natif des pédales Olympus (RS27H/N, RS28H/N, RS31H/N). Interface entièrement en français. + +## Commandes essentielles + +### Développement (mode venv) +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install PySide6>=6.5.0 numpy>=1.24.0 soundfile>=0.12.0 hid>=1.0.5 +python transcribe_station.py +``` + +### Installation automatique (Debian 12/13) +```bash +chmod +x install.sh && ./install.sh +./launch.sh +``` + +### Compilation en binaire standalone +```bash +pip install pyinstaller Pillow + +python build.py # auto-détection OS +python build.py --linux # binaire Linux + raccourci GNOME +python build.py --windows # binaire Windows + raccourci Menu Démarrer (via Wine) +python build.py --both # les deux plateformes +python build.py --clean # nettoyer dist/ et build/ +python build.py --install-only # installer raccourcis sans recompiler +``` + +Les binaires sont produits dans `dist/linux/TranscribeStation` et `dist/windows/TranscribeStation.exe`. + +### Dépendances système (Debian) +```bash +sudo apt-get install -y libhidapi-dev libhidapi-hidraw0 libusb-1.0-0 \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly gstreamer1.0-libav +``` + +### Permissions pédales USB (udev) +```bash +# Ajouter à /etc/udev/rules.d/99-olympus-pedal.rules +# SUBSYSTEM=="hidraw", ATTRS{idVendor}=="07b4", MODE="0666" +sudo udevadm control --reload-rules && sudo udevadm trigger +``` + +## Architecture + +L'application est **monolithique** : toute la logique métier et UI réside dans `transcribe_station.py` (~2 150 lignes). Le fichier `build.py` (~567 lignes) gère la compilation PyInstaller et les raccourcis OS. + +### Classes principales (transcribe_station.py) + +| Classe | Rôle | +|--------|------| +| `AppSettings` | Persistance des préférences (Linux: `~/.config/TranscribeStation/settings.json`, Windows: `%APPDATA%\TranscribeStation\settings.json`) | +| `DictationStatus` | Enum : TODO / IN_PROGRESS / SUSPENDED / DONE | +| `DictationFile` | Dataclass représentant un fichier audio + métadonnées + statut | +| `FootPedalWorker` | QThread : lecture HID non-bloquante, reconnexion auto (retry 2s) | +| `WaveformWidget` | QWidget : rendu de la forme d'onde via QPainter, seek à la souris | +| `PlayerPanel` | QWidget : QMediaPlayer + WaveformWidget + contrôles transport | +| `DictationTable` | QTableWidget : liste des fichiers, source de vérité `_files` | +| `FolderTree` | QTreeWidget : sidebar gauche, filtres par statut + navigation dossiers | +| `SettingsDialog` | QDialog : onglets Pédalier + Général | +| `MainWindow` | QMainWindow : orchestration principale | + +### Flux de données clé + +1. **Ouverture dossier** → scan audio (`.wav .mp3 .flac .ogg .mp4 .m4a .dss .ds2`) → lecture `.transcribe_station.json` → `DictationTable` +2. **Double-clic fichier** → statut passe à `IN_PROGRESS` → `PlayerPanel.set_file()` → chargement waveform (numpy/soundfile) +3. **Pédales** → `FootPedalWorker` (thread HID) → signaux Qt → actions player (`rewind 0x04`, `play 0x02`, `forward 0x08`) +4. **Changement statut** → clic droit `DictationTable` → mise à jour `DictationFile` → sauvegarde `.transcribe_station.json` + +### Persistance + +- **Par dossier :** `.transcribe_station.json` (JSON, DictationFile sérialisés via `to_dict()`/`from_dict()`) +- **Préférences globales :** `~/.config/TranscribeStation/settings.json` + +### Thèmes + +Deux thèmes (light/dark) définis par des palettes de couleurs hardcodées dans `MainWindow`. Pas de fichiers externes. Sélection via `AppSettings.theme`. + +### Backend audio + +`QMediaPlayer` + `QAudioOutput` (GStreamer sur Linux). Les formats `.dss`/`.ds2` nécessitent des plugins GStreamer spéciaux (non garantis). + +## Constantes importantes + +- `VID` pédales Olympus : `0x07B4` +- `PID` RS27H/N : `0x0110`, RS28H/N : `0x0111`, RS31H/N : `0x0112` +- Skip par défaut : `3000 ms` +- Formats audio supportés : `.wav .mp3 .flac .ogg .mp4 .m4a .dss .ds2` +- Raccourcis clavier : 15 définis dans `MainWindow._build_toolbar()` et `keyPressEvent()` diff --git a/build.sh b/build.sh new file mode 100644 index 0000000..d519957 --- /dev/null +++ b/build.sh @@ -0,0 +1,783 @@ +#!/usr/bin/env bash +# ╔══════════════════════════════════════════════════════════════════════════╗ +# ║ build.sh – Compilation + installation de TranscribeStation ║ +# ║ JT-Tools by Johnny · H3Campus · Mars 2026 ║ +# ╚══════════════════════════════════════════════════════════════════════════╝ +# +# Usage : +# ./build.sh Build Linux + raccourci GNOME (défaut) +# ./build.sh --linux Build Linux uniquement +# ./build.sh --windows Génère build_windows.bat (cross depuis Linux) +# ./build.sh --both Linux + Windows +# ./build.sh --no-install Build sans créer de raccourcis +# ./build.sh --install-only Raccourci seulement (binaire déjà compilé) +# ./build.sh --uninstall Supprime le raccourci GNOME +# ./build.sh --clean Supprime build/ dist/ icon.* +# ./build.sh --help Affiche cette aide +# +set -euo pipefail +IFS=$'\n\t' + +# ── Constantes ───────────────────────────────────────────────────────────────── +APP_NAME="TranscribeStation" +APP_VERSION="1.0.0" +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;" +APP_KEYWORDS="transcription;audio;dictée;pédalier;olympus;" +MAIN_SCRIPT="transcribe_station.py" +ICON_PNG="icon.png" +ICON_ICO="icon.ico" +DIST_LINUX="dist/linux" +DIST_WINDOWS="dist/windows" +VENV_DIR=".venv" + +HIDDEN_IMPORTS=( + "PyQt6.QtMultimedia" + "PyQt6.QtCore" + "PyQt6.QtGui" + "PyQt6.QtWidgets" + "soundfile" + "numpy" + "hid" + "cffi" + "_cffi_backend" +) + +# ── Couleurs ─────────────────────────────────────────────────────────────────── +R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' +C='\033[0;36m' B='\033[1m' N='\033[0m' + +ok() { echo -e "${G}[OK]${N} $*"; } +info() { echo -e "${C}[..]${N} $*"; } +warn() { echo -e "${Y}[!!]${N} $*"; } +err() { echo -e "${R}[XX]${N} $*" >&2; } +step() { echo -e "\n${B}══ $* ══${N}"; } +die() { err "$*"; exit 1; } + +# ── Détection Python / venv ──────────────────────────────────────────────────── +find_python() { + if [[ -f "$VENV_DIR/bin/python" ]]; then + echo "$VENV_DIR/bin/python" + elif command -v python3 &>/dev/null; then + echo "$(command -v python3)" + else + die "python3 introuvable. Installez Python 3.10+." + fi +} + +find_pip() { + local py; py=$(find_python) + echo "$py -m pip" +} + +# ── Validation PyQt6 dans Wine ──────────────────────────────────────────────── +check_wine_pyqt_runtime() { + local wine_cmd="$1" + local wine_python="$2" + local wine_run=("env" "WINEDEBUG=-all,err+all" "$wine_cmd") + local probe_output + local rc + + probe_output=$("${wine_run[@]}" "$wine_python" -c $'from pathlib import Path\nimport importlib.metadata as md\nimport PyQt6\nqt_dir = Path(PyQt6.__file__).resolve().parent / "Qt6" / "bin"\nmissing = [name for name in ("icuuc.dll", "icuin.dll", "icudt.dll") if not (qt_dir / name).exists()]\nprint("PYQT6_VERSION=" + md.version("PyQt6"))\ntry:\n print("PYQT6_QT6_VERSION=" + md.version("PyQt6-Qt6"))\nexcept Exception:\n print("PYQT6_QT6_VERSION=?")\nprint("QT_BIN=" + str(qt_dir).replace(chr(92), "/"))\nprint("MISSING_DLLS=" + ",".join(missing))\nfrom PyQt6 import QtCore\nprint("QTCORE_OK=1")' 2>&1) + rc=$? + + while IFS= read -r line; do + [[ -n "$line" ]] && info "Wine Qt probe : $line" + done <<< "$probe_output" + + if [[ $rc -ne 0 ]]; then + local missing_dlls="" + missing_dlls=$(printf '%s\n' "$probe_output" | sed -n 's/^MISSING_DLLS=//p' | tail -n1) + err "PyQt6 est installé dans Wine, mais QtCore ne se charge pas." + [[ -n "$missing_dlls" ]] && err "DLL Qt manquantes dans le wheel Windows : $missing_dlls" + err "Le binaire Windows généré via Wine serait invalide." + err "Utilisez dist/windows/build_windows.bat sur Windows natif." + return 1 + fi +} + +# ── Génération des icônes via Python/Pillow ──────────────────────────────────── +generate_icons() { + step "Génération des icônes" + local py; py=$(find_python) + + if ! $py -c "import PIL" 2>/dev/null; then + info "Installation de Pillow..." + $py -m pip install --quiet Pillow + fi + + info "Création de $ICON_PNG et $ICON_ICO..." + $py - << 'PYICON' +from PIL import Image, ImageDraw +import sys + +def draw_logo(draw, sz): + 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), + ) + +sizes = [16, 32, 48, 64, 128, 256] +images = [] +for sz in sizes: + img = Image.new("RGBA", (sz, sz), (0, 0, 0, 0)) + draw_logo(ImageDraw.Draw(img), sz) + images.append(img) + +images[-1].save("icon.png") +images[0].save("icon.ico", format="ICO", + sizes=[(s, s) for s in sizes], + append_images=images[1:]) +print("OK") +PYICON + + [[ -f "$ICON_PNG" ]] && ok "$ICON_PNG généré" || warn "$ICON_PNG absent (Pillow échoué ?)" + [[ -f "$ICON_ICO" ]] && ok "$ICON_ICO généré" || warn "$ICON_ICO absent" +} + +# ── Build PyInstaller Linux ──────────────────────────────────────────────────── +build_linux() { + step "Build Linux (PyInstaller --onefile)" + + local py; py=$(find_python) + + # Vérifier / installer PyInstaller + if ! $py -m PyInstaller --version &>/dev/null; then + info "Installation de PyInstaller..." + $py -m pip install --quiet pyinstaller + fi + + generate_icons + + # Construire les arguments hidden-import + local hi_args=() + for h in "${HIDDEN_IMPORTS[@]}"; do + hi_args+=("--hidden-import=$h") + done + + # Argument icône (optionnel) + local icon_arg=() + [[ -f "$ICON_ICO" ]] && icon_arg=("--icon=$ICON_ICO") + + info "Lancement de PyInstaller..." + $py -m PyInstaller \ + --onefile \ + --clean \ + --noconfirm \ + --name="$APP_NAME" \ + --distpath="$DIST_LINUX" \ + --workpath="build/linux" \ + --specpath="build/linux" \ + --log-level=WARN \ + "${hi_args[@]}" \ + "${icon_arg[@]}" \ + "$MAIN_SCRIPT" + + local binary="$DIST_LINUX/$APP_NAME" + if [[ -f "$binary" ]]; then + chmod +x "$binary" + local size + size=$(du -m "$binary" | cut -f1) + ok "Binaire : $binary (${size} Mo)" + + # Copier l'icône PNG à côté du binaire (chemin absolu dans le .desktop) + if [[ -f "$ICON_PNG" ]]; then + cp "$ICON_PNG" "$DIST_LINUX/$APP_NAME.png" + ok "Icône copiée : $DIST_LINUX/$APP_NAME.png" + fi + else + die "Binaire introuvable dans $DIST_LINUX/" + fi +} + +# ── Installation raccourci GNOME ─────────────────────────────────────────────── +install_gnome_shortcut() { + step "Installation raccourci GNOME" + + local binary + binary="$(pwd)/$DIST_LINUX/$APP_NAME" + [[ -f "$binary" ]] || die "Binaire introuvable : $binary (lancez d'abord --linux)" + + # 1. Icône XDG hicolor (prioritaire) + fallback chemin absolu + local icon_dir="$HOME/.local/share/icons/hicolor/256x256/apps" + local icon_ref="$APP_NAME" # référence symbolique XDG (défaut) + local icon_sizes=("16x16" "32x32" "48x48" "64x64" "128x128" "256x256") + + mkdir -p "$icon_dir" + + # Chercher l'icône : priorité au dist/linux/ puis au répertoire courant + local icon_src="" + for candidate in "$DIST_LINUX/$APP_NAME.png" "$ICON_PNG"; do + [[ -f "$candidate" ]] && icon_src="$candidate" && break + done + + if [[ -n "$icon_src" ]]; then + # Installer dans tous les formats XDG disponibles + for sz in "${icon_sizes[@]}"; do + local sz_dir="$HOME/.local/share/icons/hicolor/$sz/apps" + mkdir -p "$sz_dir" + done + cp "$icon_src" "$icon_dir/$APP_NAME.png" + ok "Icône XDG installée : $icon_dir/$APP_NAME.png" + + # Fallback : chemin absolu si XDG n'est pas disponible + icon_ref="$icon_dir/$APP_NAME.png" + else + warn "Aucune icône trouvée — raccourci sans icône" + warn "Relancez après --linux pour générer $ICON_PNG" + fi + + # 2. Fichier .desktop + local apps_dir="$HOME/.local/share/applications" + mkdir -p "$apps_dir" + local desktop="$apps_dir/$APP_NAME.desktop" + + cat > "$desktop" << DESKTOP +[Desktop Entry] +Version=1.0 +Type=Application +Name=$APP_NAME +GenericName=Transcription Audio +Comment=$APP_COMMENT +Exec=$binary +Icon=$icon_ref +Terminal=false +Categories=$APP_CATS +StartupNotify=true +StartupWMClass=$APP_NAME +Keywords=$APP_KEYWORDS +DESKTOP + + chmod 755 "$desktop" + ok ".desktop installé : $desktop" + + # 3. Raccourci bureau (Bureau / Desktop / Escritorio) + local placed=false + for bureau_name in "Bureau" "Desktop" "Escritorio"; do + local bureau_dir="$HOME/$bureau_name" + if [[ -d "$bureau_dir" ]]; then + cp "$desktop" "$bureau_dir/$APP_NAME.desktop" + chmod 755 "$bureau_dir/$APP_NAME.desktop" + ok "Raccourci bureau : $bureau_dir/$APP_NAME.desktop" + placed=true + break + fi + done + $placed || warn "Dossier Bureau/Desktop introuvable — raccourci bureau ignoré" + + # 4. Mise à jour des bases de données + for cmd_args in \ + "update-desktop-database $apps_dir" \ + "gtk-update-icon-cache -f -t $HOME/.local/share/icons/hicolor" \ + "xdg-desktop-menu forceupdate" + do + local cmd; cmd=$(echo "$cmd_args" | cut -d' ' -f1) + if command -v "$cmd" &>/dev/null; then + # shellcheck disable=SC2086 + $cmd_args 2>/dev/null && info "Mise à jour : $cmd" || true + fi + done + + ok "Raccourci GNOME installé — visible après reconnexion si nécessaire" +} + +# ── Désinstallation raccourci GNOME ─────────────────────────────────────────── +uninstall_gnome_shortcut() { + step "Désinstallation raccourci GNOME" + local removed=false + + local targets=( + "$HOME/.local/share/applications/$APP_NAME.desktop" + "$HOME/.local/share/icons/hicolor/256x256/apps/$APP_NAME.png" + "$HOME/Bureau/$APP_NAME.desktop" + "$HOME/Desktop/$APP_NAME.desktop" + "$HOME/Escritorio/$APP_NAME.desktop" + ) + + for t in "${targets[@]}"; do + if [[ -e "$t" ]]; then + rm -f "$t" + ok "Supprimé : $t" + removed=true + fi + done + + $removed || info "Aucun fichier à supprimer" + + if command -v update-desktop-database &>/dev/null; then + update-desktop-database "$HOME/.local/share/applications" 2>/dev/null || true + fi +} + +# ── Génération build_windows.bat ────────────────────────────────────────────── +build_windows() { + step "Génération du script Windows" + + mkdir -p "$DIST_WINDOWS" + + # Construire la liste des --hidden-import pour le .bat + local hi_bat="" + for h in "${HIDDEN_IMPORTS[@]}"; do + hi_bat+=" --hidden-import $h ^\n" + done + + # Construire les arguments hidden-import pour le .bat + local hi_bat_args="" + for h in "${HIDDEN_IMPORTS[@]}"; do + hi_bat_args+=" --hidden-import $h ^\\"$'\n' + done + + cat > "$DIST_WINDOWS/build_windows.bat" << WINBAT +@echo off +chcp 65001 > nul +setlocal EnableDelayedExpansion +REM ═══════════════════════════════════════════════════════════════ +REM build_windows.bat – $APP_NAME v$APP_VERSION +REM JT-Tools by Johnny – H3Campus +REM Compatible : Windows natif ET Wine (Linux) +REM ═══════════════════════════════════════════════════════════════ +echo. +echo *** Build $APP_NAME v$APP_VERSION *** +echo. + +REM ── Localiser le dossier projet ─────────────────────────────────────── +set "SCRIPT_DIR=%~dp0" +for %%I in ("%CD%") do set "PROJECT_ROOT=%%~fI" +if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND +for %%I in ("%SCRIPT_DIR%\..\..") do set "PROJECT_ROOT=%%~fI" +if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND +for %%I in ("%SCRIPT_DIR%") do set "PROJECT_ROOT=%%~fI" +if exist "%PROJECT_ROOT%\$MAIN_SCRIPT" goto :ROOT_FOUND +echo [XX] $MAIN_SCRIPT introuvable. +echo [XX] Placez build_windows.bat dans dist\windows\ du projet, +echo [XX] ou lancez-le depuis la racine contenant $MAIN_SCRIPT. +pause & exit /b 1 + +:ROOT_FOUND +cd /d "%PROJECT_ROOT%" +echo [OK] Dossier projet : %PROJECT_ROOT% +echo. + +REM ── Localiser Python (cherche dans les emplacements standard) ───────── +set PYTHON_EXE= + +REM Essayer d'abord "python" dans le PATH +python --version >nul 2>&1 +if not errorlevel 1 ( set PYTHON_EXE=python & goto :PYTHON_FOUND ) + +REM Chercher dans les emplacements fixes courants +for %%P in ( + "C:\Python312\python.exe" + "C:\Python311\python.exe" + "C:\Python310\python.exe" + "C:\Python39\python.exe" + "%LOCALAPPDATA%\Programs\Python\Python312\python.exe" + "%LOCALAPPDATA%\Programs\Python\Python311\python.exe" + "%LOCALAPPDATA%\Programs\Python\Python310\python.exe" + "%APPDATA%\..\Local\Programs\Python\Python312\python.exe" +) do ( + if exist %%P ( set PYTHON_EXE=%%P & goto :PYTHON_FOUND ) +) + +REM Utiliser le launcher py si disponible +py -3.12 --version >nul 2>&1 && set PYTHON_EXE=py -3.12 & goto :PYTHON_FOUND +py -3.11 --version >nul 2>&1 && set PYTHON_EXE=py -3.11 & goto :PYTHON_FOUND +py --version >nul 2>&1 && set PYTHON_EXE=py & goto :PYTHON_FOUND + +echo [XX] Python introuvable. Installez Python 3.12 : +echo https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe +pause & exit /b 1 + +:PYTHON_FOUND +for /f "tokens=2" %%v in ('%PYTHON_EXE% --version 2^>^&1') do set PY_FOUND=%%v +echo [OK] Python trouve : %PYTHON_EXE% (%PY_FOUND%) +echo. + +REM ── Ajouter Scripts/ au PATH pour que pip/pyinstaller soit accessible ─ +for /f "delims=" %%d in ('%PYTHON_EXE% -c "import sysconfig; print(sysconfig.get_path(chr(115)+chr(99)+chr(114)+chr(105)+chr(112)+chr(116)+chr(115)))"') do set PY_SCRIPTS=%%d +if defined PY_SCRIPTS ( set PATH=%PY_SCRIPTS%;%PATH% ) + +REM ── [1/4] Installation des dependances ─────────────────────────────── +echo [1/4] Installation des dependances Python... +%PYTHON_EXE% -m pip install --upgrade pip setuptools wheel --quiet +%PYTHON_EXE% -m pip install pyinstaller PyQt6 PyQt6-Qt6 numpy soundfile hid Pillow ^ + --prefer-binary --quiet ^ + --trusted-host pypi.org ^ + --trusted-host pypi.python.org ^ + --trusted-host files.pythonhosted.org +if errorlevel 1 ( + echo [XX] Echec pip install. Nouvelle tentative sans --quiet... + %PYTHON_EXE% -m pip install pyinstaller PyQt6 PyQt6-Qt6 numpy soundfile hid Pillow ^ + --prefer-binary ^ + --trusted-host pypi.org ^ + --trusted-host pypi.python.org ^ + --trusted-host files.pythonhosted.org + if errorlevel 1 ( echo [XX] ERREUR installation paquets & pause & exit /b 1 ) +) +echo [OK] Dependances installees. +%PYTHON_EXE% -c "from PyQt6 import QtCore" +if errorlevel 1 ( + echo [XX] PyQt6 est installe mais QtCore ne se charge pas. + echo [XX] Verifiez PyQt6 / PyQt6-Qt6 ou essayez une autre version. + pause & exit /b 1 +) +echo. + +REM ── [2/4] Generation des icones (via fichier temp) ─────────────────── +echo [2/4] Generation des icones... + +REM Ecrire le script Python dans un fichier temp (evite les problemes de guillemets cmd) +set ICON_PY=%TEMP%\build_icon_%RANDOM%.py +( +echo from PIL import Image, ImageDraw +echo def logo(d, sz^): +echo d.rounded_rectangle([0,0,sz-1,sz-1], radius=int(sz*.18^), fill=(59,110,220,255^)^) +echo bars=[.25,.45,.7,.9,.65,1.,.55,.8,.4,.2] +echo n,mg=len(bars^),sz*.13; bw=(sz-2*mg^)/(n*1.7^); gap=(sz-2*mg^)/n; mid=sz/2 +echo for i,h in enumerate(bars^): +echo x=mg+i*gap+(gap-bw^)/2; bh=h*sz*.36 +echo d.rounded_rectangle([x,mid-bh,x+bw,mid+bh],radius=bw/2,fill=(255,255,255,215^)^) +echo sizes=[16,32,48,64,128,256]; imgs=[] +echo for sz in sizes: +echo img=Image.new('RGBA',(sz,sz^),(0,0,0,0^)^); logo(ImageDraw.Draw(img^),sz^); imgs.append(img^) +echo imgs[-1].save('icon.png'^) +echo imgs[0].save('icon.ico',format='ICO',sizes=[(s,s^) for s in sizes],append_images=imgs[1:]^) +echo print('[OK] Icones generees'^) +) > "%ICON_PY%" + +%PYTHON_EXE% "%ICON_PY%" +del "%ICON_PY%" 2>nul +if not exist icon.ico ( echo [!!] Icone ignoree (Pillow absent ?^) ) else ( echo [OK] icon.ico et icon.png. ) +echo. + +REM ── [3/4] Compilation PyInstaller ──────────────────────────────────── +echo [3/4] Compilation PyInstaller... +%PYTHON_EXE% -m PyInstaller ^ + --onedir --windowed --clean --noconfirm ^ + --name $APP_NAME ^ + --distpath "%PROJECT_ROOT%\dist\windows" ^ + --workpath "%PROJECT_ROOT%\build\windows" ^ + --specpath "%PROJECT_ROOT%\build\windows" ^ + --hidden-import PyQt6.QtMultimedia ^ + --hidden-import PyQt6.QtCore ^ + --hidden-import PyQt6.QtGui ^ + --hidden-import PyQt6.QtWidgets ^ + --hidden-import soundfile ^ + --hidden-import numpy ^ + --hidden-import hid ^ + --hidden-import cffi ^ + --hidden-import _cffi_backend ^ + --icon "%PROJECT_ROOT%\icon.ico" ^ + "%PROJECT_ROOT%\$MAIN_SCRIPT" +if errorlevel 1 ( echo [XX] ERREUR PyInstaller & pause & exit /b 1 ) +echo [OK] Binaire : %PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe +echo. + +REM ── [4/4] Raccourcis Bureau + Menu Demarrer ─────────────────────────── +echo [4/4] Installation des raccourcis... +if exist "%PROJECT_ROOT%\dist\windows\install_shortcut.ps1" ( + powershell -NoProfile -ExecutionPolicy Bypass ^ + -File "%PROJECT_ROOT%\dist\windows\install_shortcut.ps1" ^ + -ExePath "%PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe" +) else ( + echo [!!] Script PowerShell introuvable : %PROJECT_ROOT%\dist\windows\install_shortcut.ps1 +) + +echo. +echo *** Build termine ! *** +echo Binaire : %PROJECT_ROOT%\dist\windows\$APP_NAME\$APP_NAME.exe +echo Raccourcis : Bureau + Menu Demarrer +echo. +pause +WINBAT + + # Script PowerShell standalone pour install raccourci seulement + cat > "$DIST_WINDOWS/install_shortcut.ps1" << PSSCRIPT +# install_shortcut.ps1 - Install shortcuts for $APP_NAME +# Usage : powershell -ExecutionPolicy Bypass -File install_shortcut.ps1 +param([string]\$ExePath = "") + +if (-not \$ExePath) { + \$here = Split-Path \$MyInvocation.MyCommand.Path + \$ExePath = Join-Path \$here "$APP_NAME\$APP_NAME.exe" + if (-not (Test-Path \$ExePath)) { + \$ExePath = Join-Path \$here "$APP_NAME.exe" + } +} + +if (-not (Test-Path \$ExePath)) { + Write-Error "Executable introuvable : \$ExePath" + exit 1 +} + +\$exe = Resolve-Path \$ExePath +\$desc = "$APP_COMMENT_WIN" +\$dirs = @( + [Environment]::GetFolderPath("Desktop"), + "\$env:APPDATA\Microsoft\Windows\Start Menu\Programs" +) + +foreach (\$dir in \$dirs) { + \$lnk = Join-Path \$dir "$APP_NAME.lnk" + \$wsh = New-Object -ComObject WScript.Shell + \$sc = \$wsh.CreateShortcut(\$lnk) + \$sc.TargetPath = \$exe + \$sc.IconLocation = "\$exe,0" + \$sc.Description = \$desc + \$sc.WorkingDirectory = Split-Path \$exe + \$sc.Save() + Write-Host "[OK] Raccourci : \$lnk" +} + +Write-Host "" +Write-Host "[OK] Installation terminee." +PSSCRIPT + + ok "Script Windows : $DIST_WINDOWS/build_windows.bat" + ok "Script PowerShell : $DIST_WINDOWS/install_shortcut.ps1" + echo "" + warn "Pour Windows natif : copiez dist/windows/ et double-cliquez build_windows.bat" + echo "" + + # ── Compilation via Wine (appels directs, pas via cmd.exe) ─────────────── + local wine_cmd + wine_cmd=$(command -v wine64 || command -v wine || true) + [[ -z "$wine_cmd" ]] && return + + step "Compilation Windows via Wine" + info "Wine : $wine_cmd" + + # Chercher python.exe dans le préfixe Wine + local wine_prefix="${WINEPREFIX:-$HOME/.wine}" + local wine_python="" + for pyver in Python312 Python311 Python310 Python39; do + local candidate="$wine_prefix/drive_c/$pyver/python.exe" + if [[ -f "$candidate" ]]; then + wine_python="$candidate" + break + fi + done + # Chercher aussi dans Program Files + if [[ -z "$wine_python" ]]; then + for base in "$wine_prefix/drive_c/users/$USER/AppData/Local/Programs" "$wine_prefix/drive_c/Program Files" "$wine_prefix/drive_c/Program Files (x86)"; do + for pyver in Python312 Python311 Python310; do + local candidate="$base/Python/$pyver/python.exe" + [[ -f "$candidate" ]] && wine_python="$candidate" && break 2 + done + done + fi + + if [[ -z "$wine_python" ]]; then + warn "Python Windows introuvable dans Wine ($wine_prefix)." + warn "Installez-le avec :" + info " wget https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe" + info " wine python-3.12.10-amd64.exe /quiet InstallAllUsers=0 TargetDir=C:\\Python312" + info "Puis relancez : ./build.sh --windows" + return + fi + + ok "Python Wine : $wine_python" + # Chemin Windows pour Wine (Z: = racine Linux sur Wine standard) + local wine_py_win + wine_py_win=$(winepath -w "$wine_python" 2>/dev/null || echo "$wine_python") + + # Générer les icônes côté Linux avant la compilation + generate_icons + + # ── Avertissement Wine + numpy ─────────────────────────────────────────────── + # ucrtbase.dll.crealf n'est pas implémenté dans Wine → numpy crashe PyInstaller. + # PyInstaller découvre les hooks via les entry_points de TOUS les paquets installés. + # Si numpy est présent dans le Python Wine, son hook crashe Wine même avec --exclude-module. + # Solution : désinstaller numpy/soundfile AVANT PyInstaller, réinstaller si nécessaire. + warn "Build Wine : numpy/soundfile seront temporairement désinstallés du Python Wine" + warn " (ucrtbase.dll.crealf non implémenté → PyInstaller crasherait)." + warn " La waveform sera absente du .exe Wine." + warn " Pour un .exe complet → build_windows.bat sur Windows natif." + echo "" + + # WINEDEBUG via variable d'environnement pour Wine lui-même (pas export bash) + # Utiliser WINEDEBUG dans la commande wine directement + local wine_run=("env" "WINEDEBUG=-all,err+all" "$wine_cmd") + + local pip_trust=( + --trusted-host pypi.org + --trusted-host pypi.python.org + --trusted-host files.pythonhosted.org + ) + + info "Mise à jour de pip dans Wine..." + "${wine_run[@]}" "$wine_python" -m pip install --upgrade pip setuptools wheel --quiet "${pip_trust[@]}" 2>/dev/null || true + + # ── Désinstaller numpy/soundfile/scipy du Python Wine ──────────────────────── + # (ils peuvent rester d'une installation précédente) + info "Désinstallation de numpy/soundfile/scipy du Python Wine (anti-crash ucrtbase)..." + "${wine_run[@]}" "$wine_python" -m pip uninstall -y numpy soundfile scipy PyInstaller-hooks-contrib 2>/dev/null || true + + # ── Installer les dépendances sans numpy/soundfile ──────────────────────────── + info "Installation PyInstaller + PyQt6 (sans numpy/soundfile)..." + "${wine_run[@]}" "$wine_python" -m pip install pyinstaller PyQt6 PyQt6-Qt6 hid Pillow --prefer-binary --quiet "${pip_trust[@]}" + if [[ $? -ne 0 ]]; then + err "Échec pip install Wine." + return + fi + ok "Dépendances installées (PyQt6 + PyInstaller, sans numpy)." + + check_wine_pyqt_runtime "$wine_cmd" "$wine_python" || return 1 + + # ── Vérifier que numpy est bien absent ─────────────────────────────────────── + if "${wine_run[@]}" "$wine_python" -c "import numpy" 2>/dev/null; then + err "numpy est encore présent dans le Python Wine malgré la désinstallation." + err "Désinstallez-le manuellement : wine $wine_python -m pip uninstall -y numpy" + return + fi + ok "numpy absent du Python Wine — PyInstaller peut s'exécuter." + + info "Lancement de PyInstaller (Windows via Wine)..." + + # Hidden imports : exclure ce qui crashe Wine + local hi_args_wine=() + for h in "${HIDDEN_IMPORTS[@]}"; do + case "$h" in + numpy|soundfile|scipy|cffi|_cffi_backend) ;; + *) hi_args_wine+=(--hidden-import "$h") ;; + esac + done + + local icon_arg_wine=() + [[ -f "$ICON_ICO" ]] && icon_arg_wine=(--icon "$(winepath -w "$(pwd)/$ICON_ICO" 2>/dev/null || echo "$ICON_ICO")") + + local src_win; src_win=$(winepath -w "$(pwd)/$MAIN_SCRIPT" 2>/dev/null || echo "$MAIN_SCRIPT") + local dist_win; dist_win=$(winepath -w "$(pwd)/$DIST_WINDOWS" 2>/dev/null || echo "$DIST_WINDOWS") + local build_win; build_win=$(winepath -w "$(pwd)/build/windows" 2>/dev/null || echo "build/windows") + + "${wine_run[@]}" "$wine_python" -m PyInstaller --onefile --windowed --clean --noconfirm --name="$APP_NAME" --distpath="$dist_win" --workpath="$build_win" --specpath="$build_win" --log-level=WARN --exclude-module numpy --exclude-module soundfile --exclude-module scipy --exclude-module _multiarray_umath "${hi_args_wine[@]}" "${icon_arg_wine[@]}" "$src_win" 2>&1 | grep -v "^0[0-9a-f]*:fixme:\|^0[0-9a-f]*:err:\|WineDbg\|Register dump\|Stack dump\|Backtrace\|Modules:\|Threads:\|System info" + local rc=${PIPESTATUS[0]} + + local binary="$DIST_WINDOWS/$APP_NAME.exe" + if [[ -f "$binary" && $rc -eq 0 ]]; then + local size; size=$(du -m "$binary" | cut -f1) + ok "Binaire Windows : $binary (${size} Mo)" + warn "Ce binaire N'INCLUT PAS la waveform (numpy exclu pour compatibilité Wine)." + warn "Pour un .exe complet avec waveform :" + warn " Copiez build_windows.bat sur une machine Windows et exécutez-le." + else + err "PyInstaller échoué (code $rc)." + err "Solution recommandée : utilisez build_windows.bat sur Windows natif." + fi +} + +# ── Nettoyage ────────────────────────────────────────────────────────────────── +clean() { + step "Nettoyage" + local removed=false + + for target in build dist "$ICON_PNG" "$ICON_ICO" *.spec; do + if [[ -e "$target" ]]; then + rm -rf "$target" + ok "Supprimé : $target" + removed=true + fi + done + + $removed || info "Rien à nettoyer" +} + +# ── Aide ─────────────────────────────────────────────────────────────────────── +usage() { + sed -n '2,15p' "$0" | sed 's/^# \{0,1\}//' + exit 0 +} + +# ── Résumé final ─────────────────────────────────────────────────────────────── +summary() { + echo "" + echo -e "${B}${G}╔══════════════════════════════════════════╗${N}" + echo -e "${B}${G}║ $APP_NAME v$APP_VERSION – Build OK ║${N}" + echo -e "${B}${G}╚══════════════════════════════════════════╝${N}" + echo "" + [[ -f "$DIST_LINUX/$APP_NAME" ]] && \ + echo -e " Linux : ${C}$DIST_LINUX/$APP_NAME${N}" + [[ -f "$DIST_WINDOWS/build_windows.bat" ]] && \ + echo -e " Windows: ${C}$DIST_WINDOWS/build_windows.bat${N}" + echo "" +} + +# ── Point d'entrée ───────────────────────────────────────────────────────────── +main() { + # Aller dans le répertoire du script + cd "$(dirname "$(realpath "$0")")" + + # Vérifications de base + [[ $EUID -eq 0 ]] && die "Ne pas exécuter en root." + [[ -f "$MAIN_SCRIPT" ]] || die "$MAIN_SCRIPT introuvable dans $(pwd)" + + local do_linux=false + local do_windows=false + local do_install=true + local do_install_only=false + local do_uninstall=false + local do_clean=false + + # Défaut : si aucun argument, build linux + [[ $# -eq 0 ]] && do_linux=true && do_install=true + + while [[ $# -gt 0 ]]; do + case "$1" in + --linux) do_linux=true ;; + --windows) do_windows=true ;; + --both) do_linux=true; do_windows=true ;; + --no-install) do_install=false ;; + --install-only) do_install_only=true ;; + --uninstall) do_uninstall=true ;; + --clean) do_clean=true ;; + --help|-h) usage ;; + *) die "Option inconnue : $1 (--help pour l'aide)" ;; + esac + shift + done + + # ── Actions exclusives ────────────────────────────────────────────────── + if $do_clean; then clean; exit 0; fi + if $do_uninstall; then uninstall_gnome_shortcut; exit 0; fi + if $do_install_only; then + generate_icons + install_gnome_shortcut + exit 0 + fi + + # ── Affichage de l'en-tête ───────────────────────────────────────────── + echo -e "\n${B}╔══════════════════════════════════════════════╗${N}" + echo -e "${B}║ $APP_NAME – Build v$APP_VERSION ║${N}" + echo -e "${B}║ JT-Tools by Johnny · H3Campus ║${N}" + echo -e "${B}╚══════════════════════════════════════════════╝${N}" + + # ── Build Linux ───────────────────────────────────────────────────────── + if $do_linux; then + build_linux + $do_install && install_gnome_shortcut + fi + + # ── Build Windows ─────────────────────────────────────────────────────── + if $do_windows; then + build_windows + fi + + summary +} + +main "$@" diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..a7b1b96 --- /dev/null +++ b/install.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash +# install.sh – Installation de TranscribeStation +# Testé sur Debian 12 (Bookworm) et Debian 13 (Trixie) +set -euo pipefail + +# ─── Couleurs terminal ──────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $*"; } +success() { echo -e "${GREEN}[OK]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERR]${NC} $*" >&2; exit 1; } +step() { echo -e "\n${BOLD}$*${NC}"; } + +echo -e "${BOLD}" +echo "╔══════════════════════════════════════════════╗" +echo "║ TranscribeStation – Installation ║" +echo "║ Debian 12/13 · Python · Qt6 ║" +echo "╚══════════════════════════════════════════════╝" +echo -e "${NC}" + +# ─── Vérifications préliminaires ───────────────────────────────────────────── +if [[ $EUID -eq 0 ]]; then + error "Ne pas lancer ce script en root. Utilisez votre compte normal." +fi + +command -v python3 >/dev/null 2>&1 || error "python3 introuvable." +PYTHON_VERSION=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') +info "Python détecté : $PYTHON_VERSION" + +# ─── Étape 1 : Dépendances système ─────────────────────────────────────────── +step "[1/6] Installation des dépendances système..." +sudo apt-get update -qq +sudo apt-get install -y \ + python3 python3-pip python3-venv \ + libhidapi-dev libhidapi-hidraw0 libusb-1.0-0 \ + gstreamer1.0-plugins-good \ + gstreamer1.0-plugins-bad \ + gstreamer1.0-plugins-ugly \ + gstreamer1.0-libav \ + fonts-noto-color-emoji \ + 2>&1 | grep -E "^(Inst|Conf|E:)" || true +success "Dépendances système installées." + +# ─── Étape 2 : Environnement virtuel Python ─────────────────────────────────── +step "[2/6] Création de l'environnement virtuel..." +if [ -d ".venv" ]; then + warn "Environnement .venv existant détecté — réutilisation." +else + python3 -m venv .venv + success "Environnement virtuel créé dans .venv/" +fi +source .venv/bin/activate + +# ─── Étape 3 : Mise à jour de pip ──────────────────────────────────────────── +step "[3/6] Mise à jour de pip..." +pip install --upgrade pip --quiet +success "pip mis à jour : $(pip --version | awk '{print $2}')" + +# ─── Étape 4 : Dépendances Python ──────────────────────────────────────────── +step "[4/6] Installation des dépendances Python..." + +info "Installation de PySide6 (interface + lecteur audio)..." +pip install "PySide6>=6.5.0" --quiet +success "PySide6 $(pip show PySide6 | awk '/^Version/{print $2}')" + +info "Installation de numpy + soundfile (affichage waveform)..." +pip install "numpy>=1.24.0" "soundfile>=0.12.0" --quiet +success "numpy $(pip show numpy | awk '/^Version/{print $2}') soundfile $(pip show soundfile | awk '/^Version/{print $2}')" + +info "Installation de hid (support pédalier USB HID)..." +if pip install "hid>=1.0.5" --quiet 2>/dev/null; then + success "hid $(pip show hid | awk '/^Version/{print $2}')" +else + warn "Le paquet 'hid' n'a pas pu être installé automatiquement." + warn "Tentative via hidapi..." + if pip install "hidapi>=0.14.0" --quiet 2>/dev/null; then + success "hidapi installé (alias hid)." + else + warn "Support pédalier désactivé. Pour l'activer plus tard :" + warn " pip install hid ou pip install hidapi" + fi +fi + +info "Vérification des imports Python..." +python3 - << 'PYCHECK' +import importlib +for pkg, mod in [("PySide6","PySide6.QtWidgets"),("numpy","numpy"), + ("soundfile","soundfile"),("hid","hid")]: + try: + importlib.import_module(mod) + print(f" \u2705 {pkg}") + except ImportError: + print(f" \u26a0\ufe0f {pkg} (non disponible)") +PYCHECK + +# ─── Étape 5 : Règle udev pédalier Olympus ─────────────────────────────────── +step "[5/6] Configuration udev pour le pédalier Olympus..." +UDEV_FILE="/etc/udev/rules.d/99-olympus-pedal.rules" +RULE='SUBSYSTEM=="hidraw", ATTRS{idVendor}=="07b4", MODE="0666", GROUP="plugdev"' + +if [ -f "$UDEV_FILE" ]; then + warn "Règle udev déjà présente : $UDEV_FILE" +else + echo "$RULE" | sudo tee "$UDEV_FILE" > /dev/null + sudo udevadm control --reload-rules + sudo udevadm trigger + success "Règle udev créée : $UDEV_FILE" +fi + +if groups "$USER" | grep -q plugdev; then + success "Utilisateur '$USER' déjà dans le groupe 'plugdev'." +else + sudo usermod -aG plugdev "$USER" + success "Utilisateur '$USER' ajouté au groupe 'plugdev'." + warn "Déconnectez-vous et reconnectez-vous pour activer le groupe." +fi + +# ─── Étape 6 : Création du lanceur ─────────────────────────────────────────── +step "[6/6] Création du script de lancement..." +cat > launch.sh << 'LAUNCHEOF' +#!/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" "$@" +LAUNCHEOF +chmod +x launch.sh +success "Script de lancement créé : ./launch.sh" + +# ─── Résumé ────────────────────────────────────────────────────────────────── +echo "" +echo -e "${GREEN}${BOLD}Installation terminée !${NC}" +echo "" +echo -e " Lancer l'application :" +echo -e " ${CYAN}./launch.sh${NC}" +echo -e " ou" +echo -e " ${CYAN}source .venv/bin/activate && python transcribe_station.py${NC}" +echo "" +echo -e " ${YELLOW}Rappel :${NC} si vous avez été ajouté au groupe 'plugdev'," +echo -e " déconnectez-vous/reconnectez-vous avant de brancher le pédalier." +echo "" diff --git a/python-3.12.10-amd64.exe b/python-3.12.10-amd64.exe new file mode 100644 index 0000000..0fb87d4 Binary files /dev/null and b/python-3.12.10-amd64.exe differ diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0cf8233 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +# TranscribeStation – dépendances +PySide6>=6.5.0 +numpy>=1.24.0 +soundfile>=0.12.0 +hid>=1.0.5 # support pédalier USB HID