From d118be619ff6eed4b5e002e1b00dc5d075481f1f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:24:32 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Arregla=20la=20prueba=20del=20cat=C3=A1logo?= =?UTF-8?q?=20en=20la=20CI:=20huella=20sin=20Get-FileHash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La prueba CatalogoEntradaTests fallaba en la CI de main con «The term 'Get-FileHash' is not recognized»: en Windows PowerShell 5.1 ese cmdlet es una función de script de Microsoft.PowerShell.Utility que solo existe si PSModulePath apunta a $PSHOME\Modules, y el powershell.exe que lanza el host de pruebas (a su vez lanzado desde pwsh) heredaba otro PSModulePath. No era la huella del zip: la prueba ya usa un zip sintético. - build\catalogo-entrada.ps1 calcula el SHA-256 con las clases de .NET, válidas en 5.1 y en pwsh, sin depender de módulos. - La prueba quita PSModulePath del entorno del hijo para que Windows PowerShell arme el suyo por omisión. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CfAg611SDfRn9w3dXEzcMb --- build/catalogo-entrada.ps1 | 13 ++++++++++++- tests/Subtitular.Tests/CatalogoEntradaTests.cs | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/build/catalogo-entrada.ps1 b/build/catalogo-entrada.ps1 index a90256c..17886ef 100644 --- a/build/catalogo-entrada.ps1 +++ b/build/catalogo-entrada.ps1 @@ -40,7 +40,18 @@ if (-not (Test-Path -LiteralPath $Zip)) { throw "No se encontró el archivo zip de distribución: $Zip (corre antes build\distribuir.ps1)." } -$sha256 = (Get-FileHash -LiteralPath $Zip -Algorithm SHA256).Hash.ToLowerInvariant() +# Huella con las clases de .NET y no con Get-FileHash: en Windows PowerShell 5.1 es una función de script de +# Microsoft.PowerShell.Utility que solo existe si PSModulePath apunta a $PSHOME\Modules, y un proceso lanzado +# desde pwsh o desde el host de pruebas puede heredar otro PSModulePath (la CI de main fallaba por eso). +$hasher = [System.Security.Cryptography.SHA256]::Create() +$flujo = [System.IO.File]::OpenRead($Zip) +try { + $sha256 = [System.BitConverter]::ToString($hasher.ComputeHash($flujo)).Replace('-', '').ToLowerInvariant() +} +finally { + $flujo.Dispose() + $hasher.Dispose() +} $size = (Get-Item -LiteralPath $Zip).Length $url = "https://github.com/Dathinel/subtitular/releases/download/v$Version/Subtitular-$Version-win-x64.zip" diff --git a/tests/Subtitular.Tests/CatalogoEntradaTests.cs b/tests/Subtitular.Tests/CatalogoEntradaTests.cs index d64a58f..0480e3d 100644 --- a/tests/Subtitular.Tests/CatalogoEntradaTests.cs +++ b/tests/Subtitular.Tests/CatalogoEntradaTests.cs @@ -38,6 +38,9 @@ public void Catalogo_entrada_tiene_la_huella_real_del_zip_y_pasa_por_SignedIndex string jsonPath = Path.Combine(temp, "catalogo.json"); var psi = new ProcessStartInfo("powershell") { RedirectStandardOutput = true, RedirectStandardError = true }; + // Windows PowerShell 5.1 arma su PSModulePath por omisión solo si no lo hereda: el del host de pruebas + // (lanzado desde pwsh en la CI) no le servía y sus módulos propios no cargaban. + psi.Environment.Remove("PSModulePath"); psi.ArgumentList.Add("-NoProfile"); psi.ArgumentList.Add("-ExecutionPolicy"); psi.ArgumentList.Add("Bypass"); From 633615de2e1841f4d3db7cce547bdb12734eb02b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 31 Oct 2026 23:30:25 +0000 Subject: [PATCH 2/3] Calidad de vida: cola de archivos, recordar lo elegido, abrir carpeta y atajos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Cola: soltar o elegir varios videos o audios; se procesan uno tras otro (JobBatch) con «Archivo 2 de 5 · nombre · etapa» y cada uno se guarda al terminar. Un archivo que falla no para a los demás; Cancelar corta el archivo en curso y los tokens matan ffmpeg y el módulo (ya lo hacían). - Preferencias (preferencias.json): última carpeta del archivo elegido (carpeta inicial del diálogo), carpeta de salida, idioma, formato, traducción, voz y la casilla nueva «Abrir la carpeta al terminar». - Abrir el Explorador con el resultado seleccionado al guardar. - Ctrl+O abre «Elegir…» y Ctrl+Entrar pulsa «Generar subtítulos». - FileDialogs: varios archivos (FOS_ALLOWMULTISELECT) y carpeta inicial. - Pruebas: BatchTests (orden, progreso por archivo, error aislado, cancelar, carpeta del resultado) y PreferencesTests. CHANGELOG, README y pruebas a mano. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01CfAg611SDfRn9w3dXEzcMb --- CHANGELOG.md | 13 ++ README.md | 2 +- docs/pruebas-a-mano.md | 10 ++ src/Subtitular.App/FileDialogs.cs | 72 +++++++-- src/Subtitular.App/MainWindow.xaml | 21 ++- src/Subtitular.App/MainWindow.xaml.cs | 172 +++++++++++++++++---- src/Subtitular.Core/JobBatch.cs | 116 ++++++++++++++ src/Subtitular.Core/Model.cs | 1 + src/Subtitular.Core/Preferences.cs | 64 ++++++++ src/Subtitular.Core/SubtitleJob.cs | 7 +- tests/Subtitular.Tests/BatchTests.cs | 93 +++++++++++ tests/Subtitular.Tests/PreferencesTests.cs | 47 ++++++ 12 files changed, 572 insertions(+), 46 deletions(-) create mode 100644 src/Subtitular.Core/JobBatch.cs create mode 100644 src/Subtitular.Core/Preferences.cs create mode 100644 tests/Subtitular.Tests/BatchTests.cs create mode 100644 tests/Subtitular.Tests/PreferencesTests.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ede2a4..4be3f69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Historial de cambios de Subtitular +## [Sin publicar] + +### Añadido +- **Cola de archivos**: se pueden soltar o elegir varios videos o audios a la vez; se procesan uno tras otro con «Archivo 2 de 5 · nombre · etapa» en la barra, cada uno se guarda al terminar y un archivo que falla no para a los demás. Cancelar corta el archivo en curso y no deja procesos de FFmpeg ni del módulo vivos. +- **Abrir la carpeta al terminar**: casilla (activada de fábrica) que abre el Explorador con el resultado seleccionado al guardar. +- **Atajos**: `Ctrl+O` abre «Elegir…» y `Ctrl+Entrar` pulsa «Generar subtítulos». + +### Cambiado +- La ventana recuerda la última carpeta (la del archivo elegido, para el diálogo, y la de salida), el idioma del audio, el formato, «Traducir a…», «Leer en voz alta» y la casilla de abrir la carpeta (`%LOCALAPPDATA%\Dathinel\Subtitular\preferencias.json`). + +### Corregido +- La CI de `main` fallaba en `CatalogoEntradaTests`: `build\catalogo-entrada.ps1` dependía de `Get-FileHash`, que en Windows PowerShell 5.1 no existe si el proceso hereda un `PSModulePath` ajeno (el del host de pruebas lanzado desde pwsh). La huella se calcula ahora con las clases de .NET. + ## [1.0.0] - 2026-10-24 ### Añadido diff --git a/README.md b/README.md index db8bcf7..539c288 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Subtítulos automáticos para Windows, en tu PC y sin conexión. Le das un video ## Qué hay -- **`Subtitular.exe`** (ventana): suelta un video o audio, elige idioma, formato (SRT/VTT), carpeta, «Traducir a…» y «Leer en voz alta»; barra de progreso por etapa, Cancelar, vista previa editable bloque a bloque y Guardar. Los errores salen en español dentro de la ventana. +- **`Subtitular.exe`** (ventana): suelta un video o audio (o varios: se procesan en cola y cada uno se guarda al terminar), elige idioma, formato (SRT/VTT), carpeta, «Traducir a…» y «Leer en voz alta»; barra de progreso por etapa y por archivo, Cancelar, vista previa editable bloque a bloque y Guardar. Recuerda lo último elegido, abre la carpeta del resultado al terminar y responde a `Ctrl+O` y `Ctrl+Entrar`. Los errores salen en español dentro de la ventana. - **`Subtitular.Cli.exe`**: `Subtitular.Cli video.mp4 [--idioma es] [--formato vtt] [--traducir en] [--voz] [--salida carpeta]`. Deja `video.srt` (o `video.en.srt`) y, con `--voz`, `video.wav` con cada frase en su tiempo. - Toda la lógica vive en `Subtitular.Core`; la ventana y la CLI solo la llaman. Cifras en `docs/informe-fase-1.md` y `docs/informe-fase-2.md`. diff --git a/docs/pruebas-a-mano.md b/docs/pruebas-a-mano.md index 94ea0aa..b6d775e 100644 --- a/docs/pruebas-a-mano.md +++ b/docs/pruebas-a-mano.md @@ -24,3 +24,13 @@ Lo que solo se juzga a ojo. Sin captura: la app no tiene `--captura`. 5. **Ventana.** Mueve y redimensiona la ventana, ciérrala y ábrela: vuelve al mismo sitio y tamaño (`%LOCALAPPDATA%\Dathinel\Subtitular\ventana.json`). No se deja bajar de 800×700. Si la cierras maximizada, se queda el último tamaño normal. 6. **Acerca de.** Arriba a la derecha: versión, licencia y el crédito de Rodaje; los dos enlaces abren GitHub en el navegador. 7. **Ritmo.** ¿Las tres tarjetas (archivo, ajustes, subtítulos) con sombra suave se ven ordenadas o recargadas? Con Tab, ¿se ve el foco en cada control? + +## Calidad de vida (1.0.1) + +Lo que la CI no puede comprobar (la lógica tiene pruebas en `BatchTests` y `PreferencesTests`). + +1. **Recordar.** Elige un archivo de una carpeta nueva, pon «Inglés», VTT, «Traducir a… Francés», marca la voz y una carpeta de salida; cierra y abre: todo debe seguir igual, y «Elegir…» debe abrirse en la carpeta del último archivo (`%LOCALAPPDATA%\Dathinel\Subtitular\preferencias.json`). Borra la carpeta de salida y vuelve a abrir: el campo queda vacío (junto al video). +2. **Varios archivos.** Arrastra tres videos a la vez (o elígelos con Ctrl en el diálogo): la cabecera dice «3 archivos en cola» y debajo sus nombres. Generar: la barra dice «Archivo 1 de 3 · nombre · Transcribiendo · 45 %…», late indeterminada al empezar cada archivo, y al final «3 archivos listos» con la vista previa del último; en la carpeta deben estar los tres `.srt`. Mezcla un `.txt` en lo que sueltas: barra roja con su nombre y los videos siguen cargados. Renombra el segundo video antes de generar: «2 archivos listos, 1 con error: …» y los otros dos guardados. +3. **Cancelar en cola.** Con tres archivos, cancela durante el segundo: «Operación cancelada.» en menos de un segundo, el primero ya guardado, y en el Administrador de tareas no queda ningún `ffmpeg.exe` ni `Rodaje.Module.*.exe`. +4. **Abrir la carpeta.** Con la casilla marcada, al guardar un archivo se abre el Explorador con el `.srt` seleccionado; con varios, se abre la carpeta. Desmarcada, no se abre nada. +5. **Atajos.** `Ctrl+O` abre el diálogo de elegir; `Ctrl+Entrar` genera (y no hace nada mientras ya está generando o sin archivo). Con el foco dentro de un bloque de la vista previa, `Ctrl+Entrar` también debe generar y no meter un salto de línea. diff --git a/src/Subtitular.App/FileDialogs.cs b/src/Subtitular.App/FileDialogs.cs index 525f4b1..32c50c3 100644 --- a/src/Subtitular.App/FileDialogs.cs +++ b/src/Subtitular.App/FileDialogs.cs @@ -1,4 +1,5 @@ // Copiado de Rastrear (src/Rastrear.App/FileDialogs.cs), sin SaveSingle: candidato a biblioteca compartida (Herramientas-IA\PLAN.md, «Cómo se comparte el código con Rodaje»). +// Añadido aquí: varios archivos a la vez (OpenMany) y carpeta inicial (la última elegida). using System.Runtime.InteropServices; using TerraFX.Interop.Windows; using static TerraFX.Interop.Windows.Windows; @@ -14,6 +15,7 @@ internal static unsafe class FileDialogs private const uint FosNoChangeDir = 0x8; private const uint FosPickFolders = 0x20; private const uint FosForceFileSystem = 0x40; + private const uint FosAllowMultiSelect = 0x200; private const uint FosPathMustExist = 0x800; private const uint FosFileMustExist = 0x1000; @@ -23,45 +25,72 @@ public static readonly (string Name, string Spec) MediaFilter = public static readonly (string Name, string Spec) AllFilter = ("Todos los archivos", "*.*"); - public static string? OpenSingle(nint owner, string title, params (string Name, string Spec)[] filters) + public static string? OpenSingle(nint owner, string title, params (string Name, string Spec)[] filters) => + OpenMany(owner, title, null, false, filters).FirstOrDefault(); + + /// Uno o varios archivos (Ctrl o Mayús en el diálogo); vacío si se cancela. Empieza en si existe. + public static IReadOnlyList OpenMany(nint owner, string title, string? initialDir, bool multi, params (string Name, string Spec)[] filters) { IFileOpenDialog* dialog = null; - IShellItem* item = null; + IShellItemArray* items = null; try { if (CoCreateInstance(__uuidof(), null, (uint)CLSCTX.CLSCTX_INPROC_SERVER, __uuidof(), (void**)&dialog).FAILED) { - return null; + return []; } uint options; dialog->GetOptions(&options); options |= FosForceFileSystem | FosFileMustExist | FosPathMustExist | FosNoChangeDir; + if (multi) + { + options |= FosAllowMultiSelect; + } dialog->SetOptions(options); Configure((IFileDialog*)dialog, title, filters); + SetFolder((IFileDialog*)dialog, initialDir); if (dialog->Show((HWND)owner).FAILED) { - return null; + return []; } - if (dialog->GetResult(&item).FAILED) + if (dialog->GetResults(&items).FAILED || items == null) { - return null; + return []; } - return PathOf(item); + uint count; + items->GetCount(&count); + List paths = new((int)count); + for (uint i = 0; i < count; i++) + { + IShellItem* item = null; + try + { + if (items->GetItemAt(i, &item).SUCCEEDED && PathOf(item) is { } path) + { + paths.Add(path); + } + } + finally + { + if (item != null) item->Release(); + } + } + return paths; } finally { - if (item != null) item->Release(); + if (items != null) items->Release(); if (dialog != null) dialog->Release(); } } - public static string? ChooseFolder(nint owner, string title) + public static string? ChooseFolder(nint owner, string title, string? initialDir = null) { IFileOpenDialog* dialog = null; IShellItem* item = null; @@ -82,6 +111,7 @@ public static readonly (string Name, string Spec) AllFilter = { dialog->SetTitle(pTitle); } + SetFolder((IFileDialog*)dialog, initialDir); if (dialog->Show((HWND)owner).FAILED) { @@ -102,6 +132,30 @@ public static readonly (string Name, string Spec) AllFilter = } } + /// Carpeta por la que empieza el diálogo; si no existe o falla, Windows pone la de siempre. + private static void SetFolder(IFileDialog* dialog, string? dir) + { + if (string.IsNullOrWhiteSpace(dir) || !Directory.Exists(dir)) + { + return; + } + IShellItem* folder = null; + try + { + fixed (char* pDir = dir) + { + if (SHCreateItemFromParsingName(pDir, null, __uuidof(), (void**)&folder).SUCCEEDED && folder != null) + { + dialog->SetFolder(folder); + } + } + } + finally + { + if (folder != null) folder->Release(); + } + } + private static void Configure(IFileDialog* dialog, string title, (string Name, string Spec)[] filters) { fixed (char* pTitle = title) diff --git a/src/Subtitular.App/MainWindow.xaml b/src/Subtitular.App/MainWindow.xaml index 39af858..bded90b 100644 --- a/src/Subtitular.App/MainWindow.xaml +++ b/src/Subtitular.App/MainWindow.xaml @@ -73,10 +73,14 @@ - - - + + @@ -98,10 +102,12 @@ +