Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.

Expand Down
13 changes: 12 additions & 1 deletion build/catalogo-entrada.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
10 changes: 10 additions & 0 deletions docs/pruebas-a-mano.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
72 changes: 63 additions & 9 deletions src/Subtitular.App/FileDialogs.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand All @@ -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();

/// <summary>Uno o varios archivos (Ctrl o Mayús en el diálogo); vacío si se cancela. Empieza en <paramref name="initialDir"/> si existe.</summary>
public static IReadOnlyList<string> 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<FileOpenDialog>(), null, (uint)CLSCTX.CLSCTX_INPROC_SERVER,
__uuidof<IFileOpenDialog>(), (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<string> 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;
Expand All @@ -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)
{
Expand All @@ -102,6 +132,30 @@ public static readonly (string Name, string Spec) AllFilter =
}
}

/// <summary>Carpeta por la que empieza el diálogo; si no existe o falla, Windows pone la de siempre.</summary>
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<IShellItem>(), (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)
Expand Down
21 changes: 16 additions & 5 deletions src/Subtitular.App/MainWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,14 @@
<!-- Archivo: zona para soltar. -->
<Border Grid.Row="1" Style="{StaticResource Tarjeta}" Translation="0,0,8" BorderBrush="{ThemeResource AcentoBrush}" Background="{ThemeResource AcentoSuaveBrush}">
<StackPanel Orientation="Horizontal" Spacing="16" VerticalAlignment="Center">
<Button Content="Elegir…" Click="OnChoose" />
<Button Content="Elegir…" Click="OnChoose" ToolTipService.ToolTip="Elegir uno o varios archivos (Ctrl+O)">
<Button.KeyboardAccelerators>
<KeyboardAccelerator Modifiers="Control" Key="O" />
</Button.KeyboardAccelerators>
</Button>
<StackPanel VerticalAlignment="Center" Spacing="4">
<TextBlock x:Name="FileName" Text="Suelta aquí un video o un audio" Style="{ThemeResource BodyStrongTextBlockStyle}" />
<TextBlock x:Name="FileInfo" Text="mp4, mkv, mov, mp3, wav y más; o pulsa Elegir…" Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
<TextBlock x:Name="FileName" Text="Suelta aquí uno o varios videos o audios" Style="{ThemeResource BodyStrongTextBlockStyle}" />
<TextBlock x:Name="FileInfo" Text="mp4, mkv, mov, mp3, wav y más; o pulsa Elegir… (Ctrl+O)" Foreground="{ThemeResource TextFillColorSecondaryBrush}" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</Border>
Expand All @@ -98,10 +102,12 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="OutputDirBox" Header="Carpeta de salida" PlaceholderText="Junto al video" />
<Button Grid.Column="1" Content="Carpeta…" Click="OnChooseFolder" VerticalAlignment="Bottom" />
<CheckBox x:Name="SpeakBox" Grid.Column="2" Content="Leer en voz alta (WAV junto al video)" Margin="16,0,0,0" VerticalAlignment="Bottom" />
<CheckBox x:Name="OpenFolderBox" Grid.Column="3" Content="Abrir la carpeta al terminar" Margin="16,0,0,0" VerticalAlignment="Bottom" IsChecked="True" />
</Grid>
</StackPanel>
</Border>
Expand All @@ -114,7 +120,12 @@
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="RunButton" Content="Generar subtítulos" Style="{ThemeResource AccentButtonStyle}" Click="OnRun" IsEnabled="False" />
<Button x:Name="RunButton" Content="Generar subtítulos" Style="{ThemeResource AccentButtonStyle}" Click="OnRun" IsEnabled="False"
ToolTipService.ToolTip="Ctrl+Entrar">
<Button.KeyboardAccelerators>
<KeyboardAccelerator Modifiers="Control" Key="Enter" />
</Button.KeyboardAccelerators>
</Button>
<Button x:Name="CancelButton" Grid.Column="1" Content="Cancelar" Click="OnCancel" IsEnabled="False" />
<StackPanel Grid.Column="2" VerticalAlignment="Center" Spacing="4" Margin="8,0,0,0">
<TextBlock x:Name="StageText" Foreground="{ThemeResource TextFillColorSecondaryBrush}" />
Expand Down Expand Up @@ -168,7 +179,7 @@
</Viewbox>
<TextBlock Text="Aquí saldrán los subtítulos" Style="{StaticResource Seccion}" HorizontalAlignment="Center" Margin="0" />
<TextBlock TextWrapping="Wrap" TextAlignment="Center" Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Text="Suelta un video o un audio, elige el idioma y pulsa «Generar subtítulos». Podrás corregir cada bloque antes de guardarlo." />
Text="Suelta un video o un audio, elige el idioma y pulsa «Generar subtítulos» (Ctrl+Entrar). Podrás corregir cada bloque antes de guardarlo. Con varios archivos a la vez, cada uno se guarda al terminar." />
</StackPanel>
</Grid>
</Border>
Expand Down
Loading
Loading