code.pablomurad.com

fast-scripts

28 double-click Windows batch tools for video, audio and files

more stuff
author
Pablo Murad
date
commit
91ad877b609c1970114b901276fa63385f9c37cc
parent
8ea94b47fd8a9d47a6729b6de2aac84198404e27
changes
52 files +5738 −312
@@ -0,0 +1,356 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Comprimir PDF - reduz o tamanho de PDFs (Ghostscript)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Comprimir PDF' } catch {}
+
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'ComprimirPdf'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Comprimir PDF'
+# O Ghostscript costuma NAO estar no PATH: procura tambem onde ele se instala.
+$gsExe = $null
+foreach ($n in @('gswin64c','gswin32c')) {
+    $c = Get-Command $n -ErrorAction SilentlyContinue
+    if ($c) { $gsExe = $c.Source; break }
+}
+if (-not $gsExe) {
+    foreach ($base in @("$env:ProgramFiles\gs", "${env:ProgramFiles(x86)}\gs")) {
+        if (-not (Test-Path -LiteralPath $base)) { continue }
+        $achado = Get-ChildItem -LiteralPath $base -Recurse -Filter 'gswin*c.exe' -ErrorAction SilentlyContinue |
+                  Sort-Object FullName -Descending | Select-Object -First 1
+        if ($achado) { $gsExe = $achado.FullName; break }
+    }
+}
+if (-not $gsExe) {
+    Write-Host ''
+    Write-Host '[ERRO] Ghostscript não foi encontrado (é ele que comprime o PDF).' -ForegroundColor Red
+    Write-Host 'Instale com:  winget install ArtifexSoftware.GhostScript' -ForegroundColor White
+    Pausar; exit 1
+}
+Write-Host ("[OK] Ghostscript: {0}" -f $gsExe) -ForegroundColor Green
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$ent = Pedir-Entrada $padrao @('.pdf') 'Pasta ou PDF'
+$itens = Listar $ent @('.pdf')
+if ($itens.Count -eq 0) { Write-Host 'Nenhum PDF encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+$niveis = @(
+    [pscustomobject]@{ Id=1; Nome='Tela    - menor arquivo (72 dpi)';     V='/screen' },
+    [pscustomobject]@{ Id=2; Nome='E-book  - equilibrado (150 dpi)';      V='/ebook' },
+    [pscustomobject]@{ Id=3; Nome='Impressão - boa qualidade (300 dpi)';  V='/printer' },
+    [pscustomobject]@{ Id=4; Nome='Gráfica - máxima qualidade';           V='/prepress' }
+)
+$idPadrao = if ($config -and $config.UltimoNivel) { [int]$config.UltimoNivel } else { 2 }
+Titulo 'Nível de compressão'
+foreach ($n in $niveis) {
+    $m = if ($n.Id -eq $idPadrao) { '>' } else { ' ' }
+    $cor = if ($n.Id -eq $idPadrao) { 'White' } else { 'Gray' }
+    Write-Host ("{0} [{1}] {2}" -f $m, $n.Id, $n.Nome) -ForegroundColor $cor
+}
+Write-Host "Enter = usar a opção padrão [$idPadrao]" -ForegroundColor DarkGray
+$nivel = $null
+while (-not $nivel) {
+    $e = Read-Host 'Nível (1-4)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$idPadrao" }
+    $nivel = $niveis | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1
+    if (-not $nivel) { Write-Host '[!] Escolha de 1 a 4.' -ForegroundColor Yellow }
+}
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Comprimido' })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoNivel=$nivel.Id; UltimaSaida=$destino })
+
+$ok=0;$ign=0;$err=0
+$tarefas = @(); $antes = @{}
+foreach ($f in $itens) {
+    $saida = Join-Path $destino $f.Name
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
+    $antes[$saida] = $f.Length
+    $a = @('-sDEVICE=pdfwrite','-dCompatibilityLevel=1.4',"-dPDFSETTINGS=$($nivel.V)",
+           '-dNOPAUSE','-dQUIET','-dBATCH','-dSAFER',"-sOutputFile=$saida",$f.FullName)
+    $tarefas += @{ Exe=$gsExe; Rotulo=$f.Name; Alvo=$saida; Args=$a }
+}
+if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow }
+
+Titulo 'Comprimindo'
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'imagem'
+    Write-Host ("{0} PDF(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    Relatar $rs $tarefas ([ref]$ok) ([ref]$err)
+    Write-Host ''
+    $totA = 0; $totD = 0
+    foreach ($t in $tarefas) {
+        if (Test-Path -LiteralPath $t.Alvo) {
+            $a = $antes[$t.Alvo]; $d = (Get-Item -LiteralPath $t.Alvo).Length
+            $totA += $a; $totD += $d
+            $pc = if ($a -gt 0) { [int]((1 - ($d / $a)) * 100) } else { 0 }
+            Write-Host ("  {0,-42} {1,10} -> {2,10}   ({3}% menor)" -f $t.Rotulo, (Tam $a), (Tam $d), $pc) -ForegroundColor Gray
+        }
+    }
+    if ($totA -gt 0) {
+        Write-Host ''
+        Write-Host ("TOTAL: {0} -> {1}   ({2}% menor)" -f (Tam $totA), (Tam $totD), [int]((1 - ($totD/$totA)) * 100)) -ForegroundColor Cyan
+    }
+}
+Resumo $ok $ign $err $destino
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Converter Imagens' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar {
     Write-Host ''
     Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray
@@ -278,31 +427,21 @@ $convertidos = 0
 $ignorados   = 0
 $erros       = 0
-$indice      = 0
 
+# --- 1) monta as tarefas (barato, sem processar nada ainda) ---
+$tarefas = @()
 foreach ($arquivo in $arquivos) {
-    $indice++
-
     $relativo = $arquivo.FullName.Substring($origem.Length).TrimStart('\')
     $subpasta = Split-Path -Path $relativo -Parent
     $pastaSaida = if ([string]::IsNullOrWhiteSpace($subpasta)) { $destino } else { Join-Path $destino $subpasta }
     New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
 
-    # Extensão de saída: mantém a original ou usa a do formato escolhido
     $extSaida = if ([string]::IsNullOrEmpty($formato.Ext)) { $arquivo.Extension.ToLowerInvariant() } else { $formato.Ext }
     $saida = Join-Path $pastaSaida ($arquivo.BaseName + $extSaida)
 
-    Write-Host ''
-    Write-Host ("[{0}/{1}]" -f $indice, $arquivos.Count) -ForegroundColor Magenta -NoNewline
-    Write-Host " $($arquivo.Name)"
+    if (Test-Path -LiteralPath $saida) { $ignorados++; continue }
 
-    if (Test-Path -LiteralPath $saida) {
-        Write-Host "   [IGNORADO] Já existe na pasta de destino." -ForegroundColor Yellow
-        $ignorados++
-        continue
-    }
-
-    # Entrada: para GIF/multiframe indo para formato de imagem única, pega o 1o quadro
+    # GIF/multiframe indo para formato de imagem unica: pega o 1o quadro
     $entradaMagick = $arquivo.FullName
     if ($arquivo.Extension.ToLowerInvariant() -eq '.gif' -and $extSaida -ne '.gif') {
         $entradaMagick = "$($arquivo.FullName)[0]"
@@ -310,27 +449,36 @@ } 
     $imgArgs = @($entradaMagick)
     if ($resizeArg) { $imgArgs += @('-resize', $resizeArg) }
-    # JPG não tem transparência: achata sobre fundo branco
     if ($extSaida -eq '.jpg' -or $extSaida -eq '.jpeg') {
         $imgArgs += @('-background', 'white', '-alpha', 'remove', '-alpha', 'off')
     }
-    if ($extSaida -in '.jpg', '.jpeg', '.webp') {
-        $imgArgs += @('-quality', '85')
-    }
+    if ($extSaida -in '.jpg', '.jpeg', '.webp') { $imgArgs += @('-quality', '85') }
     $imgArgs += $saida
 
-    & magick @imgArgs
+    $tarefas += @{ Exe = 'magick'; Rotulo = $arquivo.Name; Alvo = $saida; Args = $imgArgs }
+}
+
+if ($ignorados -gt 0) { Write-Host "$ignorados ja existiam no destino e foram puladas." -ForegroundColor Yellow }
 
-    if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) {
-        Write-Host "   [OK] Convertida." -ForegroundColor Green
-        $convertidos++
-    } else {
-        Write-Host "   [ERRO] Falha ao converter." -ForegroundColor Red
-        if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
-        $erros++
+# --- 2) processa em paralelo ---
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'imagem'
+    Write-Host ("Convertendo {0} imagem(ns), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+
+    # --- 3) resumo, mostrando o motivo real de cada falha ---
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $convertidos++ }
+        else {
+            $erros++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
     }
 }
-
 # ------------------------------------------------------------
 # 6) Resumo final
 # ------------------------------------------------------------
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Imagens e PDF' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -116,19 +265,33 @@ Salvar-Config ([pscustomobject]@{ UltimaSaidaPdf=($config.UltimaSaidaPdf); UltimaSaidaImg=$destino }) 
 Titulo 'Processando'
 $ok=0;$err=0
+$tarefas = @(); $pastas = @()
 foreach ($pf in $pdfs) {
     $outDir = Join-Path $destino ($pf.BaseName + '-imagens')
     New-Item -ItemType Directory -Path $outDir -Force | Out-Null
     $padraoSaida = Join-Path $outDir ('pagina-%03d' + $extSaida)
-    Write-Host ''; Write-Host $pf.Name -ForegroundColor Magenta
-    $imgArgs = @('-density',"$dpi",$pf.FullName)
-    if ($extSaida -eq '.jpg') { $imgArgs += @('-quality','90','-background','white','-alpha','remove') }
-    $imgArgs += $padraoSaida
-    & magick @imgArgs
-    if ($LASTEXITCODE -eq 0) {
-        $qtd = @(Get-ChildItem -LiteralPath $outDir -File).Count
-        Write-Host "   [OK] $qtd página(s) -> $outDir" -ForegroundColor Green; $ok++
-    } else { Write-Host '   [ERRO] Falha ao converter.' -ForegroundColor Red; $err++ }
+    $a = @('-density',"$dpi",$pf.FullName)
+    if ($extSaida -eq '.jpg') { $a += @('-quality','90','-background','white','-alpha','remove') }
+    $a += $padraoSaida
+    $tarefas += @{ Exe='magick'; Rotulo=$pf.Name; Alvo=$outDir; Args=$a }
+    $pastas += $outDir
+}
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'imagem'
+    Write-Host ("{0} PDF(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    for ($i = 0; $i -lt $tarefas.Count; $i++) {
+        $qtd = @(Get-ChildItem -LiteralPath $pastas[$i] -File -ErrorAction SilentlyContinue).Count
+        if ($rs[$i].Codigo -eq 0 -and $qtd -gt 0) {
+            Write-Host ("  [OK] {0}: {1} pagina(s)" -f $tarefas[$i].Rotulo, $qtd) -ForegroundColor Green
+            $ok++
+        } else {
+            $err++
+            Write-Host ("  [ERRO] {0}" -f $tarefas[$i].Rotulo) -ForegroundColor Red
+            $motivo = ($rs[$i].Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+        }
+    }
 }
 Titulo 'Concluído'
 Write-Host "PDFs convertidos: $ok" -ForegroundColor Green
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = "Marca d'água" } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -114,30 +263,61 @@ Write-Host "Imagens encontradas: $($arquivos.Count)" -ForegroundColor Cyan if ($arquivos.Count -eq 0) { Write-Host 'Nenhuma imagem encontrada.' -ForegroundColor Yellow; Pausar; exit 0 }
 New-Item -ItemType Directory -Path $destino -Force | Out-Null
 
-$ok=0;$ign=0;$err=0;$i=0
+$ok=0;$ign=0;$err=0
+
+# --- 1) mede as larguras: 1 processo por imagem, porem todos em paralelo ---
+$sonda = @()
+foreach ($a in $arquivos) { $sonda += @{ Exe='magick'; Rotulo=$a.Name; Args=@('identify','-format','%w',"$($a.FullName)[0]") } }
+$larguras = @{}
+if ($sonda.Count -gt 0) {
+    Write-Host 'Medindo as imagens...' -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'imagem') -SemProgresso
+    for ($i = 0; $i -lt $rs.Count; $i++) {
+        $w = 0
+        if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $w = ("$($rs[$i].Saida)".Trim() -replace '\D','') -as [int] }
+        if (-not $w -or $w -le 0) { $w = 1000 }
+        $larguras[$arquivos[$i].FullName] = [int]$w
+    }
+}
+
+# --- 2) monta as tarefas de composicao ---
+$tarefas = @()
 foreach ($a in $arquivos) {
-    $i++
     $rel = $a.FullName.Substring($origem.Length).TrimStart('\'); $sub = Split-Path $rel -Parent
     $pastaSaida = if ([string]::IsNullOrWhiteSpace($sub)) { $destino } else { Join-Path $destino $sub }
     New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
     $saida = Join-Path $pastaSaida $a.Name
-    Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$arquivos.Count,$a.Name) -ForegroundColor Magenta
-    if (Test-Path -LiteralPath $saida) { Write-Host '   [IGNORADO] Já existe.' -ForegroundColor Yellow; $ign++; continue }
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
 
+    $w = [int]$larguras[$a.FullName]; if ($w -le 0) { $w = 1000 }
     if ($tipo -eq 'texto') {
-        $w = [int](& magick identify -format '%w' "$($a.FullName)[0]" 2>$null)
-        if (-not $w -or $w -le 0) { $w = 1000 }
         $ps = [int]($w / 22); if ($ps -lt 12) { $ps = 12 }
         $imgArgs = @($a.FullName,'-gravity',$grav,'-pointsize',"$ps",'-fill','rgba(255,255,255,0.85)','-undercolor','rgba(0,0,0,0.35)','-annotate',$offset,$texto,$saida)
     } else {
-        $w = [int](& magick identify -format '%w' "$($a.FullName)[0]" 2>$null)
-        if (-not $w -or $w -le 0) { $w = 1000 }
         $lw = [int]($w * $pct / 100); if ($lw -lt 10) { $lw = 10 }
         $imgArgs = @($a.FullName,'(',$logo,'-resize',"${lw}x",')','-gravity',$grav,'-geometry',$offset,'-composite',$saida)
     }
-    & magick @imgArgs
-    if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host '   [OK] Marca aplicada.' -ForegroundColor Green; $ok++ }
-    else { Write-Host '   [ERRO] Falha.' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ }
+    $tarefas += @{ Exe='magick'; Rotulo=$a.Name; Alvo=$saida; Args=$imgArgs }
+}
+
+if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram puladas." -ForegroundColor Yellow }
+
+# --- 3) aplica em paralelo ---
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'imagem'
+    Write-Host ("Aplicando em {0} imagem(ns), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok++ }
+        else {
+            $err++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
 }
 Titulo 'Concluído'
 Write-Host "Aplicadas: $ok" -ForegroundColor Green
@@ -36,15 +36,47 @@ Write-Host 'Analisando... (agrupa por tamanho e depois compara o conteúdo)' -ForegroundColor Gray $todos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Length -gt 0 })
 
 # 1) agrupa por tamanho (rápido); 2) só calcula hash de quem tem tamanho repetido
-$porTamanho = $todos | Group-Object Length | Where-Object { $_.Count -gt 1 }
+function Hash-SHA256([string]$caminho) {
+    $sha = [System.Security.Cryptography.SHA256]::Create()
+    $fs  = [System.IO.File]::OpenRead($caminho)
+    try { return ([BitConverter]::ToString($sha.ComputeHash($fs)) -replace '-', '') }
+    finally { $fs.Close(); $sha.Dispose() }
+}
+# Pre-filtro barato: hash so das PONTAS do arquivo (primeiros/ultimos 64 KB).
+# So quem colide nas pontas paga o SHA256 do arquivo inteiro.
+function Hash-Parcial([string]$caminho, [long]$tam) {
+    $n = 65536
+    try { $fs = [System.IO.File]::OpenRead($caminho) } catch { return "ERRO$caminho" }
+    try {
+        $md5 = [System.Security.Cryptography.MD5]::Create()
+        $buf = New-Object byte[] $n
+        $lido = $fs.Read($buf, 0, $n)
+        [void]$md5.TransformBlock($buf, 0, $lido, $buf, 0)
+        if ($tam -gt ($n * 2)) {
+            [void]$fs.Seek(-$n, [System.IO.SeekOrigin]::End)
+            $lido2 = $fs.Read($buf, 0, $n)
+            [void]$md5.TransformBlock($buf, 0, $lido2, $buf, 0)
+        }
+        [void]$md5.TransformFinalBlock((New-Object byte[] 0), 0, 0)
+        return ([BitConverter]::ToString($md5.Hash) + "_$tam")
+    } catch { return "ERRO$caminho" } finally { $fs.Close() }
+}
+
+$porTamanho = @($todos | Group-Object Length | Where-Object { $_.Count -gt 1 })
 $grupos = @()
+$gi = 0
 foreach ($g in $porTamanho) {
-    $porHash = $g.Group | Group-Object { (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash }
-    foreach ($h in ($porHash | Where-Object { $_.Count -gt 1 })) {
-        $grupos += ,@($h.Group | Sort-Object FullName)
+    $gi++
+    Write-Host ("`r  Comparando... {0}/{1} grupos   " -f $gi, $porTamanho.Count) -NoNewline -ForegroundColor Cyan
+    $porParcial = $g.Group | Group-Object { Hash-Parcial $_.FullName $_.Length }
+    foreach ($pp in ($porParcial | Where-Object { $_.Count -gt 1 })) {
+        $porHash = $pp.Group | Group-Object { Hash-SHA256 $_.FullName }
+        foreach ($h in ($porHash | Where-Object { $_.Count -gt 1 })) {
+            $grupos += ,@($h.Group | Sort-Object FullName)
+        }
     }
 }
-
+if ($porTamanho.Count -gt 0) { Write-Host '' }
 if ($grupos.Count -eq 0) { Titulo 'Resultado'; Write-Host 'Nenhum arquivo duplicado encontrado.' -ForegroundColor Green; Pausar; exit 0 }
 
 $totalDup = 0; $espaco = 0
@@ -0,0 +1,193 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Compactar / Extrair - compacta e extrai arquivos em lote
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Compactar / Extrair' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'CompactarZip'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Compactar / Extrair'
+# Get-Command devolve .Source e Get-Item devolve .FullName: normaliza tudo
+# num caminho unico para nao misturar os dois tipos.
+$seteExe = $null
+$c7 = Get-Command 7z -ErrorAction SilentlyContinue
+if ($c7) { $seteExe = $c7.Source }
+else {
+    foreach ($cand in @("$env:ProgramFiles\7-Zip\7z.exe", "${env:ProgramFiles(x86)}\7-Zip\7z.exe")) {
+        if (Test-Path -LiteralPath $cand) { $seteExe = $cand; break }
+    }
+}
+if ($seteExe) { Write-Host ("[OK] 7-Zip: {0}" -f $seteExe) -ForegroundColor Green }
+else { Write-Host '[OK] Usando o compactador do Windows (instale o 7-Zip para mais formatos: winget install 7zip.7zip)' -ForegroundColor Gray }
+$config = Carregar-Config
+
+Titulo 'O que você quer fazer?'
+Write-Host '  [1] Compactar uma pasta' -ForegroundColor White
+Write-Host '  [2] Extrair arquivos compactados' -ForegroundColor White
+$acao = $null
+while (-not $acao) { $a = Read-Host 'Opção (1-2)'; switch ($a.Trim()) { '1' { $acao='zip' } '2' { $acao='unzip' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } }
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+
+if ($acao -eq 'zip') {
+    $pasta = $null
+    while (-not $pasta) {
+        Titulo 'Pasta para compactar'
+        Write-Host "  $padrao" -ForegroundColor White
+        $r = Read-Host 'Pasta (Enter = confirmar)'
+        $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x }
+    }
+    $nome = (Read-Host "Nome do arquivo (Enter = $([System.IO.Path]::GetFileName($pasta)).zip)").Trim()
+    if ([string]::IsNullOrWhiteSpace($nome)) { $nome = [System.IO.Path]::GetFileName($pasta) + '.zip' }
+    if ($nome -notmatch '\.(zip|7z)$') { $nome += '.zip' }
+    $destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Split-Path $pasta -Parent })
+    Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta; UltimaSaida=$destino })
+    $saida = Join-Path $destino $nome
+    if (Test-Path -LiteralPath $saida) {
+        $s = Read-Host 'Já existe um arquivo com esse nome. Sobrescrever? (s/N)'
+        if ($s -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
+        Remove-Item -LiteralPath $saida -Force
+    }
+    Titulo 'Compactando'
+    Write-Host "Saída: $saida"
+    $t = Measure-Command {
+        if ($seteExe) { & $seteExe a -mmt=on -- $saida (Join-Path $pasta '*') | Out-Null; $global:rc = $LASTEXITCODE }
+        else { Compress-Archive -Path (Join-Path $pasta '*') -DestinationPath $saida -CompressionLevel Optimal -Force; $global:rc = 0 }
+    }
+    if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) {
+        Write-Host ("[OK] Pronto em {0:N1}s  ({1})" -f $t.TotalSeconds, (Tam (Get-Item $saida).Length)) -ForegroundColor Green
+        Resumo 1 0 0 $destino
+    } else { Write-Host '[ERRO] Falha ao compactar.' -ForegroundColor Red; Resumo 0 0 1 $destino }
+}
+else {
+    $ent = Pedir-Entrada $padrao @('.zip','.7z','.rar','.tar','.gz') 'Pasta ou arquivo compactado'
+    $itens = Listar $ent @('.zip','.7z','.rar','.tar','.gz')
+    if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo compactado encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+    $destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Extraido' })
+    Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino })
+    Titulo 'Extraindo'
+    $ok=0;$err=0
+    foreach ($f in $itens) {
+        $sub = Join-Path $destino $f.BaseName
+        New-Item -ItemType Directory -Path $sub -Force | Out-Null
+        Write-Host ("  {0}..." -f $f.Name) -ForegroundColor Gray
+        $falhou = $false
+        if ($seteExe) { & $seteExe x -y "-o$sub" -- $f.FullName | Out-Null; if ($LASTEXITCODE -ne 0) { $falhou = $true } }
+        elseif ($f.Extension -eq '.zip') { try { Expand-Archive -LiteralPath $f.FullName -DestinationPath $sub -Force } catch { $falhou = $true } }
+        else { Write-Host '     (formato exige o 7-Zip)' -ForegroundColor Yellow; $falhou = $true }
+        if ($falhou) { $err++ } else { $ok++ }
+    }
+    Resumo $ok 0 $err $destino
+}
@@ -0,0 +1,321 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Limpar Metadados - remove EXIF/GPS de fotos e vídeos (privacidade)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Limpar Metadados' } catch {}
+
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'LimparMetadados'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Limpar Metadados'
+Checar 'magick' 'ImageMagick' 'winget install ImageMagick.ImageMagick' | Out-Null
+$temFfmpeg = [bool](Get-Command ffmpeg -ErrorAction SilentlyContinue)
+if ($temFfmpeg) { Write-Host '[OK] FFmpeg encontrado (vídeos também serão limpos).' -ForegroundColor Green }
+else { Write-Host '[!] Sem FFmpeg: só as imagens serão limpas.' -ForegroundColor Yellow }
+$config = Carregar-Config
+
+Write-Host ''
+Write-Host 'Remove EXIF, GPS, câmera, data e outros metadados - útil antes de publicar.' -ForegroundColor DarkGray
+Write-Host 'Os arquivos ORIGINAIS não são alterados: as cópias limpas vão para a pasta de saída.' -ForegroundColor DarkGray
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$aceitos = $extImg + $extVideo
+$ent = Pedir-Entrada $padrao $aceitos 'Pasta ou arquivo'
+$itens = Listar $ent $aceitos
+if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+# previa: mostra o que existe hoje numa amostra
+Titulo 'Prévia'
+$amostra = $itens | Where-Object { $extImg -contains $_.Extension.ToLowerInvariant() } | Select-Object -First 1
+if ($amostra) {
+    $info = & magick identify -format '%[EXIF:Make] %[EXIF:Model] %[EXIF:DateTimeOriginal] %[EXIF:GPSLatitude]' "$($amostra.FullName)[0]" 2>$null
+    if ("$info".Trim()) { Write-Host ("  Exemplo ({0}) contém: {1}" -f $amostra.Name, "$info".Trim()) -ForegroundColor Yellow }
+    else { Write-Host ("  Exemplo ({0}): sem EXIF relevante." -f $amostra.Name) -ForegroundColor Gray }
+}
+Write-Host ("  {0} arquivo(s) serão limpos." -f $itens.Count) -ForegroundColor White
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'SemMetadados' })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino })
+
+$ok=0;$ign=0;$err=0
+$tarefas = @()
+foreach ($f in $itens) {
+    $saida = Join-Path $destino $f.Name
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
+    if ($extImg -contains $f.Extension.ToLowerInvariant()) {
+        $tarefas += @{ Exe='magick'; Rotulo=$f.Name; Alvo=$saida; Args=@($f.FullName,'-strip',$saida) }
+    } elseif ($temFfmpeg) {
+        $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida
+                       Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-map_metadata','-1','-c','copy','-y',$saida) }
+    }
+}
+if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow }
+
+Titulo 'Limpando'
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'imagem'
+    Write-Host ("{0} arquivo(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    Relatar $rs $tarefas ([ref]$ok) ([ref]$err)
+}
+Resumo $ok $ign $err $destino
@@ -0,0 +1,185 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Limpar Nomes - normaliza nomes de arquivo para web/upload
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Limpar Nomes' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'LimparNomes'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Limpar Nomes'
+Write-Host '[OK] Não precisa de nenhum programa extra.' -ForegroundColor Green
+$config = Carregar-Config
+
+Write-Host ''
+Write-Host 'Deixa os nomes seguros para web/upload: tira acentos, troca espaços por hífen e usa minúsculas.' -ForegroundColor DarkGray
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$pasta = $null
+while (-not $pasta) {
+    Titulo 'Pasta'
+    Write-Host "  $padrao" -ForegroundColor White
+    $r = Read-Host 'Pasta (Enter = confirmar)'
+    $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+    if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x }
+}
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta })
+
+function Limpar-Nome([string]$nome) {
+    $base = [System.IO.Path]::GetFileNameWithoutExtension($nome)
+    $ext  = [System.IO.Path]::GetExtension($nome).ToLowerInvariant()
+    $n = $base.Normalize([System.Text.NormalizationForm]::FormD)
+    $sb = New-Object System.Text.StringBuilder
+    foreach ($ch in $n.ToCharArray()) {
+        if ([System.Globalization.CharUnicodeInfo]::GetUnicodeCategory($ch) -ne [System.Globalization.UnicodeCategory]::NonSpacingMark) { [void]$sb.Append($ch) }
+    }
+    $r = $sb.ToString().ToLowerInvariant()
+    $r = $r -replace '[^a-z0-9]+', '-'
+    $r = $r -replace '-+', '-'
+    $r = $r.Trim('-')
+    if ([string]::IsNullOrWhiteSpace($r)) { $r = 'arquivo' }
+    return $r + $ext
+}
+
+$arquivos = @(Get-ChildItem -LiteralPath $pasta -File | Sort-Object Name)
+if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+$plano = @(); $reservados = @()
+foreach ($a in $arquivos) {
+    $novo = Limpar-Nome $a.Name
+    if ($novo -ceq $a.Name) { continue }
+    $alvo = Join-Path $pasta $novo
+    $b = [System.IO.Path]::GetFileNameWithoutExtension($novo); $e = [System.IO.Path]::GetExtension($novo); $i = 1
+    while ((Test-Path -LiteralPath $alvo) -or ($reservados -contains $alvo)) { $alvo = Join-Path $pasta ("$b-$i$e"); $i++ }
+    $reservados += $alvo
+    $plano += [pscustomobject]@{ De=$a; Para=$alvo }
+}
+if ($plano.Count -eq 0) { Write-Host 'Todos os nomes já estão limpos. Nada a fazer.' -ForegroundColor Green; Pausar; exit 0 }
+
+Titulo 'Prévia'
+$mostra = [Math]::Min($plano.Count, 15)
+for ($i=0; $i -lt $mostra; $i++) {
+    Write-Host ("  {0}" -f $plano[$i].De.Name) -ForegroundColor Gray -NoNewline
+    Write-Host ("   ->   {0}" -f (Split-Path $plano[$i].Para -Leaf)) -ForegroundColor White
+}
+if ($plano.Count -gt $mostra) { Write-Host ("  ... e mais {0}." -f ($plano.Count - $mostra)) -ForegroundColor DarkGray }
+Write-Host ''
+$ok = Read-Host "Renomear $($plano.Count) arquivo(s)? (s/N)"
+if ($ok -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+$feitos=0;$err=0
+foreach ($p in $plano) {
+    try { Rename-Item -LiteralPath $p.De.FullName -NewName (Split-Path $p.Para -Leaf) -ErrorAction Stop; $feitos++ }
+    catch { Write-Host ("  [ERRO] {0}: {1}" -f $p.De.Name, $_.Exception.Message) -ForegroundColor Red; $err++ }
+}
+Resumo $feitos 0 $err $pasta
@@ -13,6 +13,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Renomear em lote' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -32,7 +181,8 @@ $extImgExif = @('.jpg','.jpeg','.tif','.tiff','.heic','.heif','.png','.webp') function Data-Do-Arquivo($file) {
     if ($magick -and ($extImgExif -contains $file.Extension.ToLowerInvariant())) {
         try {
-            $d = & magick identify -format '%[EXIF:DateTimeOriginal]' "$($file.FullName)[0]" 2>$null
+            $d = if ($cacheExif -and $cacheExif.ContainsKey($file.FullName)) { $cacheExif[$file.FullName] }
+                 else { & magick identify -format '%[EXIF:DateTimeOriginal]' "$($file.FullName)[0]" 2>$null }
             if ($d -and ($d.Trim() -match '^\d{4}:\d{2}:\d{2}\s\d{2}:\d{2}:\d{2}$')) {
                 return [datetime]::ParseExact($d.Trim(),'yyyy:MM:dd HH:mm:ss',$null)
             }
@@ -51,6 +201,22 @@ $p = (Read-Host 'Cole o caminho da pasta').Trim().Trim('"')     if (Test-Path -LiteralPath $p -PathType Container) { $pasta = (Resolve-Path -LiteralPath $p).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow }
 }
 $arquivos = @(Get-ChildItem -LiteralPath $pasta -File | Sort-Object Name)
+
+# Le as datas EXIF de TODAS as fotos de uma vez (antes era 1 processo por
+# arquivo, em serie). O resultado alimenta Data-Do-Arquivo pelo cache.
+$cacheExif = @{}
+if ($magick) {
+    $fotos = @($arquivos | Where-Object { $extImgExif -contains $_.Extension.ToLowerInvariant() })
+    if ($fotos.Count -gt 1) {
+        Write-Host 'Lendo as datas das fotos...' -ForegroundColor Gray
+        $tar = @()
+        foreach ($f in $fotos) { $tar += @{ Exe='magick'; Rotulo=$f.Name; Args=@('identify','-format','%[EXIF:DateTimeOriginal]',"$($f.FullName)[0]") } }
+        $rs = Executar-EmParalelo -Tarefas $tar -Limite (Limite-Padrao 'imagem') -SemProgresso
+        for ($i = 0; $i -lt $fotos.Count; $i++) {
+            if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $cacheExif[$fotos[$i].FullName] = "$($rs[$i].Saida)".Trim() }
+        }
+    }
+}
 if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 }
 
 Titulo 'Modo de renomeação'
@@ -1,11 +1,95 @@ # fast-scripts
 
 Programas rápidos para Windows, em lote e com interface no terminal.
+**26 programas** que você abre com um duplo-clique — sem instalar nada, sem linha de comando.
 
 ![PowerShell](https://img.shields.io/badge/PowerShell-5391FE?style=flat&logo=powershell&logoColor=white)
 ![Batch](https://img.shields.io/badge/Batch-.cmd-4D4D4D?style=flat)
 ![Windows](https://img.shields.io/badge/Windows-0078D6?style=flat&logo=windows&logoColor=white)
 
+## Começando
+
+Abra o **`canivete.cmd`** na raiz: ele lista todos os programas e abre o que você escolher.
+Ou vá direto na pasta e dê um duplo-clique no programa que quiser.
+
+Todos seguem a mesma lógica: perguntam a **pasta ou o arquivo**, as **opções**, e a **pasta de saída** — lembrando suas escolhas da próxima vez e criando a pasta se ela não existir. Nada destrutivo acontece sem prévia e confirmação.
+
+## Os programas
+
+### 🎬 Vídeos
+
+| Programa | O que faz |
+|---|---|
+| [transcodificar](docs/transcodificar.md) | Converte vídeos em lote, com GPU e 5 níveis de qualidade |
+| [baixar-videos](docs/baixar-videos.md) | Baixa vídeos e áudio de mais de mil sites (yt-dlp) |
+| [extrair-audio](docs/extrair-audio.md) | Tira o áudio de vídeos (MP3/M4A/WAV) |
+| [cortar-video](docs/cortar-video.md) | Corta um trecho do vídeo — instantâneo |
+| [juntar-videos](docs/juntar-videos.md) | Junta vários vídeos num só |
+| [criar-gif](docs/criar-gif.md) | Vídeo vira GIF, ou fotos viram slideshow |
+| [marca-dagua-video](docs/marca-dagua-video.md) | Marca d'água de texto ou logo em vídeos |
+| [normalizar-audio](docs/normalizar-audio.md) | Padroniza o volume (EBU R128) |
+| [info-midia](docs/info-midia.md) | Relatório de codec, resolução e bitrate + planilha |
+
+### 🖼️ Imagens
+
+| Programa | O que faz |
+|---|---|
+| [converter-imagens](docs/converter-imagens.md) | Redimensiona e converte imagens em lote |
+| [marca-dagua](docs/marca-dagua.md) | Marca d'água de texto ou logo em fotos |
+| [imagens-e-pdf](docs/imagens-e-pdf.md) | Junta imagens num PDF ou explode PDF em imagens |
+| [comprimir-pdf](docs/comprimir-pdf.md) | Reduz o tamanho de PDFs |
+
+### 🗂️ Organizar
+
+| Programa | O que faz |
+|---|---|
+| [renomear-em-lote](docs/renomear-em-lote.md) | Renomeia por sequência, data ou localizar/substituir |
+| [achar-duplicados](docs/achar-duplicados.md) | Acha arquivos iguais por conteúdo e libera espaço |
+| [organizar-por-tipo](docs/organizar-por-tipo.md) | Separa a bagunça em subpastas por categoria |
+| [compactar-zip](docs/compactar-zip.md) | Compacta e extrai arquivos |
+| [limpar-metadados](docs/limpar-metadados.md) | Remove EXIF e GPS de fotos e vídeos |
+| [limpar-nomes](docs/limpar-nomes.md) | Deixa os nomes de arquivo seguros para web |
+
+### 🧹 Úteis
+
+| Programa | O que faz |
+|---|---|
+| [backup-espelhado](docs/backup-espelhado.md) | Backup incremental e rápido com robocopy |
+| [verificar-discos](docs/verificar-discos.md) | Saúde dos HDs e SSDs (SMART) |
+| [verificar-integridade](docs/verificar-integridade.md) | Detecta arquivos corrompidos silenciosamente |
+| [analisar-espaco](docs/analisar-espaco.md) | Descobre o que está enchendo o disco |
+| [limpeza-segura](docs/limpeza-segura.md) | Limpa temporários e lixeira, com prévia |
+| [relatorio-sistema](docs/relatorio-sistema.md) | Specs do PC, memória, discos e espaço |
+
+### 🔪 Raiz
+
+| Programa | O que faz |
+|---|---|
+| [canivete](docs/canivete.md) | Menu central que abre todos os outros |
+
+## Instalação
+
+Não tem instalação: é só copiar a pasta. O que alguns programas precisam:
+
+```
+winget install Gyan.FFmpeg                    # vídeo e áudio
+winget install ImageMagick.ImageMagick        # imagens
+winget install ArtifexSoftware.GhostScript    # PDF
+winget install 7zip.7zip                      # opcional: .rar e .7z
+winget install smartmontools                  # opcional: SMART avançado
+```
+
+O **yt-dlp se instala sozinho** quando necessário. Cada programa avisa na tela se faltar algo — veja a lista completa em **[docs/DEPENDENCIAS.md](docs/DEPENDENCIAS.md)**.
+
+## Velocidade
+
+Os programas que trabalham em lote processam **vários arquivos ao mesmo tempo**, ajustando o número de processos ao tipo de trabalho. Em imagens o ganho medido chega a **4,2×**. O `transcodificar` e o `marca-dagua-video` usam a **placa NVIDIA** quando ela existe, e o `extrair-audio` **copia o áudio sem recodificar** quando o formato já é o certo — nesse caso é quase instantâneo.
+
+## Para quem for mexer no código
+
+As regras técnicas da coleção (cabeçalho polyglot, codificação, paralelismo e as armadilhas já mapeadas) estão em **[docs/PADROES.md](docs/PADROES.md)**.
+
 ---
 
 **Pablo Murad** — pablomurad[at]pm[dot]me
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,192 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Baixar Vídeos - download de vídeos/áudio (yt-dlp)
+#  Baixa de YouTube, Instagram, TikTok, X, Vimeo e +1000 sites
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Baixar Vídeos' } catch {}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+
+$configDir  = Join-Path $env:APPDATA 'BaixarVideos'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Perfis de qualidade / formato
+$perfis = @(
+    [pscustomobject]@{ Id=1; Nome='Melhor qualidade (resolução máxima)'; Tipo='video'; Altura=0 },
+    [pscustomobject]@{ Id=2; Nome='1080p (Full HD)';                      Tipo='video'; Altura=1080 },
+    [pscustomobject]@{ Id=3; Nome='720p (HD)';                            Tipo='video'; Altura=720 },
+    [pscustomobject]@{ Id=4; Nome='480p';                                 Tipo='video'; Altura=480 },
+    [pscustomobject]@{ Id=5; Nome='360p (compacto)';                      Tipo='video'; Altura=360 },
+    [pscustomobject]@{ Id=6; Nome='Só áudio - MP3';                       Tipo='mp3';   Altura=0 },
+    [pscustomobject]@{ Id=7; Nome='Só áudio - M4A';                       Tipo='m4a';   Altura=0 }
+)
+
+Titulo 'Baixar Vídeos'
+Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+Write-Host ''
+Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+
+# ------------------------------------------------------------
+# 1) Localizar / baixar o yt-dlp (o baixador mais moderno)
+# ------------------------------------------------------------
+$localDir = Join-Path $env:LOCALAPPDATA 'fast-scripts'
+$localExe = Join-Path $localDir 'yt-dlp.exe'
+$ytdlp = $null
+$cmd = Get-Command yt-dlp -ErrorAction SilentlyContinue
+if ($cmd) { $ytdlp = $cmd.Source }
+elseif (Test-Path -LiteralPath $localExe) { $ytdlp = $localExe }
+else {
+    Write-Host ''
+    Write-Host 'yt-dlp não encontrado. Baixando a versão mais recente (site oficial)...' -ForegroundColor Yellow
+    try {
+        New-Item -ItemType Directory -Path $localDir -Force | Out-Null
+        try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
+        $url = 'https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe'
+        Invoke-WebRequest -Uri $url -OutFile $localExe -UseBasicParsing
+        $ytdlp = $localExe
+        Write-Host '[OK] yt-dlp baixado.' -ForegroundColor Green
+    } catch {
+        Write-Host "[ERRO] Não foi possível baixar o yt-dlp: $($_.Exception.Message)" -ForegroundColor Red
+        Write-Host 'Verifique sua internet e tente de novo.' -ForegroundColor Yellow
+        Pausar; exit 1
+    }
+}
+Write-Host "[OK] yt-dlp: $ytdlp" -ForegroundColor Green
+
+# Auto-atualiza SÓ a cópia que este programa baixou (não mexe numa instalação sua)
+if ($ytdlp -eq $localExe) {
+    $idadeDias = (New-TimeSpan -Start (Get-Item $localExe).LastWriteTime -End (Get-Date)).TotalDays
+    if ($idadeDias -gt 15) {
+        Write-Host 'Procurando atualização do yt-dlp...' -ForegroundColor Gray
+        & $ytdlp -U 2>$null | Out-Null
+    }
+}
+
+# ffmpeg (necessário p/ juntar vídeo+áudio de alta qualidade e converter áudio)
+$ffmpeg = Get-Command ffmpeg -ErrorAction SilentlyContinue
+$ffmpegDir = if ($ffmpeg) { Split-Path $ffmpeg.Source -Parent } else { $null }
+if ($ffmpeg) { Write-Host "[OK] FFmpeg: $($ffmpeg.Source)" -ForegroundColor Green }
+else { Write-Host '[!] FFmpeg não encontrado - alta qualidade e conversão de áudio podem falhar. Instale: winget install Gyan.FFmpeg' -ForegroundColor Yellow }
+
+$config = Carregar-Config
+
+# ------------------------------------------------------------
+# 2) Link(s)
+# ------------------------------------------------------------
+$urls = @()
+while ($urls.Count -eq 0) {
+    Titulo 'Link do vídeo'
+    Write-Host 'Cole o link (ou vários separados por espaço).' -ForegroundColor DarkGray
+    Write-Host 'Funciona com YouTube, Instagram, TikTok, X, Vimeo, Facebook e +1000 sites.' -ForegroundColor DarkGray
+    $resp = Read-Host 'Link'
+    $urls = @($resp -split '\s+' | Where-Object { $_ -match '^https?://' })
+    if ($urls.Count -eq 0) { Write-Host '[!] Cole um link válido (começando com http).' -ForegroundColor Yellow }
+}
+
+# ------------------------------------------------------------
+# 3) Qualidade / formato
+# ------------------------------------------------------------
+$qPadrao = if ($config -and $config.UltimaQualidade) { [int]$config.UltimaQualidade } else { 1 }
+Titulo 'Qualidade / formato'
+foreach ($p in $perfis) {
+    $m = if ($p.Id -eq $qPadrao) { '>' } else { ' ' }
+    $c = if ($p.Id -eq $qPadrao) { 'White' } else { 'Gray' }
+    Write-Host ("{0} [{1}] {2}" -f $m, $p.Id, $p.Nome) -ForegroundColor $c
+}
+Write-Host "Enter = usar a opção padrão [$qPadrao]" -ForegroundColor DarkGray
+$perfil = $null
+while (-not $perfil) {
+    $e = Read-Host 'Qualidade (1-7)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$qPadrao" }
+    $perfil = $perfis | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1
+    if (-not $perfil) { Write-Host '[!] Escolha de 1 a 7.' -ForegroundColor Yellow }
+}
+
+# ------------------------------------------------------------
+# 4) Playlist?
+# ------------------------------------------------------------
+$plPadrao = if ($config -and $config.UltimaPlaylist) { "$($config.UltimaPlaylist)" } else { 'nao' }
+Titulo 'Playlist'
+Write-Host 'Se o link fizer parte de uma playlist, baixar a playlist INTEIRA?' -ForegroundColor Gray
+$dicaPl = if ($plPadrao -eq 'sim') { '(Enter = SIM)' } else { '(Enter = NÃO)' }
+$rpl = Read-Host "Baixar playlist inteira? (s/n) $dicaPl"
+$playlist = if ([string]::IsNullOrWhiteSpace($rpl)) { $plPadrao -eq 'sim' } else { $rpl -match '^(s|sim|y|yes)$' }
+
+# ------------------------------------------------------------
+# 5) Pasta de saída (lembra a última; cria se não existir)
+# ------------------------------------------------------------
+$saidaPadrao = if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $env:USERPROFILE 'Downloads\Baixados' }
+while ($true) {
+    Titulo 'Pasta de saída'
+    Write-Host "  $saidaPadrao" -ForegroundColor White
+    Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+    $rsaida = Read-Host 'Pasta de saída'
+    $destino = if ([string]::IsNullOrWhiteSpace($rsaida)) { $saidaPadrao } else { $rsaida.Trim().Trim('"') }
+    try { New-Item -ItemType Directory -Path $destino -Force | Out-Null; break }
+    catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+}
+$destino = (Resolve-Path -LiteralPath $destino).Path
+
+Salvar-Config ([pscustomobject]@{ UltimaQualidade=$perfil.Id; UltimaPlaylist=$(if ($playlist) {'sim'} else {'nao'}); UltimaSaida=$destino })
+
+# ------------------------------------------------------------
+# 6) Monta os argumentos do yt-dlp
+# ------------------------------------------------------------
+switch ($perfil.Tipo) {
+    'mp3' { $fmtArgs = @('-f','ba/b','-x','--audio-format','mp3','--audio-quality','0') }
+    'm4a' { $fmtArgs = @('-f','ba/b','-x','--audio-format','m4a') }
+    default {
+        if ($perfil.Altura -eq 0) { $fmtArgs = @('-f','bv*+ba/b','--merge-output-format','mp4') }
+        else { $fmtArgs = @('-f',"bv*[height<=$($perfil.Altura)]+ba/b[height<=$($perfil.Altura)]/b[height<=$($perfil.Altura)]",'--merge-output-format','mp4') }
+    }
+}
+
+$ytArgs = @()
+$ytArgs += $fmtArgs
+$ytArgs += @('-o', (Join-Path $destino '%(title)s.%(ext)s'))
+$ytArgs += @('--embed-metadata','--embed-thumbnail')     # título, capa, etc.
+$ytArgs += @('--no-overwrites')                          # pula o que já foi baixado
+$ytArgs += @('--concurrent-fragments','8')               # baixa varios pedacos de uma vez
+# O aria2c baixa em muitas conexoes ao mesmo tempo: bem mais rapido quando existe.
+$aria = Get-Command aria2c -ErrorAction SilentlyContinue
+if ($aria) {
+    Write-Host '[OK] aria2c encontrado: download acelerado ligado.' -ForegroundColor Green
+    $ytArgs += @('--downloader','aria2c','--downloader-args','aria2c:-x16 -s16 -k1M')
+}               # download mais rápido
+$ytArgs += @('--ignore-errors')                          # em lote, continua se um falhar
+$ytArgs += if ($playlist) { '--yes-playlist' } else { '--no-playlist' }
+if ($ffmpegDir) { $ytArgs += @('--ffmpeg-location', $ffmpegDir) }
+
+# ------------------------------------------------------------
+# 7) Baixar
+# ------------------------------------------------------------
+Titulo 'Baixando'
+Write-Host "Qualidade: $($perfil.Nome)"
+Write-Host "Playlist:  $(if ($playlist) {'sim'} else {'não'})"
+Write-Host "Destino:   $destino"
+Write-Host "Links:     $($urls.Count)"
+Write-Host ''
+
+& $ytdlp @ytArgs @urls
+$rc = $LASTEXITCODE
+
+Titulo 'Concluído'
+if ($rc -eq 0) { Write-Host '[OK] Download finalizado.' -ForegroundColor Green }
+else { Write-Host "[!] Terminou com avisos/erros (código $rc). Alguns itens podem não ter baixado." -ForegroundColor Yellow }
+Write-Host "Pasta: $destino"
+Pausar
@@ -0,0 +1,188 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Cortar Vídeo - apara um trecho do vídeo (FFmpeg)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Cortar Vídeo' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'CortarVideo'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Cortar Vídeo'
+Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$ent = Pedir-Entrada $padrao $extVideo 'Vídeo para cortar'
+if ($ent.Modo -ne 'arquivo') {
+    $lista = Listar $ent $extVideo
+    if ($lista.Count -eq 0) { Write-Host 'Nenhum vídeo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 }
+    Titulo 'Qual vídeo?'
+    for ($i=0; $i -lt $lista.Count; $i++) { Write-Host ("  [{0}] {1}" -f ($i+1), $lista[$i].Name) -ForegroundColor Gray }
+    $sel = $null
+    while (-not $sel) { $e = Read-Host "Número (1-$($lista.Count))"; $n = $e -as [int]; if ($n -ge 1 -and $n -le $lista.Count) { $sel = $lista[$n-1] } else { Write-Host '[!] Inválido.' -ForegroundColor Yellow } }
+    $video = $sel
+} else { $video = Get-Item -LiteralPath $ent.Caminho }
+
+# duração, para ajudar o usuário
+$dur = 0.0
+try {
+    $b = (& ffprobe -v error -show_entries format=duration -of csv=p=0 $video.FullName 2>$null)
+    [double]::TryParse(("$b" -replace ',', '.'), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$dur) | Out-Null
+} catch {}
+Titulo 'Trecho'
+Write-Host ("  Vídeo: {0}" -f $video.Name) -ForegroundColor White
+if ($dur -gt 0) { Write-Host ("  Duração total: {0:hh\:mm\:ss}" -f [TimeSpan]::FromSeconds($dur)) -ForegroundColor Gray }
+Write-Host '  Formato aceito: 90   ou   01:30   ou   00:01:30' -ForegroundColor DarkGray
+$inicio = Read-Host 'Início (Enter = 0)'
+if ([string]::IsNullOrWhiteSpace($inicio)) { $inicio = '0' }
+$fim = Read-Host 'Fim (Enter = até o final)'
+
+Titulo 'Modo do corte'
+Write-Host '> [1] Rápido - sem recodificar (instantâneo; corta no quadro-chave mais próximo)' -ForegroundColor White
+Write-Host '  [2] Exato  - recodifica (corta no ponto exato, demora mais)' -ForegroundColor Gray
+$m = Read-Host 'Modo (Enter = 1)'
+$exato = ($m.Trim() -eq '2')
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $video.DirectoryName 'Cortado' })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$video.DirectoryName; UltimaSaida=$destino })
+
+$saida = Join-Path $destino ($video.BaseName + '-corte' + $video.Extension)
+$n = 1
+while (Test-Path -LiteralPath $saida) { $saida = Join-Path $destino ($video.BaseName + "-corte ($n)" + $video.Extension); $n++ }
+
+$ffArgs = @('-hide_banner','-nostdin','-loglevel','error')
+if ($exato) {
+    $ffArgs += @('-i', $video.FullName, '-ss', $inicio)
+    if (-not [string]::IsNullOrWhiteSpace($fim)) { $ffArgs += @('-to', $fim) }
+    $ffArgs += @('-c:v','libx264','-preset','veryfast','-crf','20','-c:a','aac','-b:a','160k')
+} else {
+    $ffArgs += @('-ss', $inicio)
+    if (-not [string]::IsNullOrWhiteSpace($fim)) { $ffArgs += @('-to', $fim) }
+    $ffArgs += @('-i', $video.FullName, '-c', 'copy', '-avoid_negative_ts', 'make_zero')
+}
+$ffArgs += @('-y', $saida)
+
+Titulo 'Cortando'
+Write-Host ("Modo: {0}" -f $(if ($exato) { 'exato (recodifica)' } else { 'rápido (sem recodificar)' }))
+Write-Host "Saída: $saida"
+Write-Host ''
+$t = Measure-Command { & ffmpeg @ffArgs }
+$rc = $LASTEXITCODE
+if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) {
+    Write-Host ("[OK] Corte pronto em {0:N1}s  ({1})" -f $t.TotalSeconds, (Tam (Get-Item $saida).Length)) -ForegroundColor Green
+    Resumo 1 0 0 $destino
+} else {
+    Write-Host '[ERRO] Não foi possível cortar (confira os tempos informados).' -ForegroundColor Red
+    if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
+    Resumo 0 0 1 $destino
+}
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Criar GIF / Slideshow' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -25,6 +174,19 @@ $configFile = Join-Path $configDir 'config.json' function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
 function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
 
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
 $extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.ts','.3gp','.vob')
 $extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
 
@@ -82,19 +244,54 @@ Write-Host "Vídeos: $($videos.Count)" -ForegroundColor Cyan     if ($videos.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
     New-Item -ItemType Directory -Path $destino -Force | Out-Null
 
-    $ok=0;$err=0;$i=0
+    $ok=0;$ign=0;$err=0
+
+    # Cada GIF exige 2 passagens (gerar paleta + aplicar). Fazemos as duas
+    # fases em paralelo, em vez de 2 processos em serie por video.
+    $alvos = @()
     foreach ($v in $videos) {
-        $i++
         $saida = Join-Path $destino ($v.BaseName + '.gif')
-        Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$videos.Count,$v.Name) -ForegroundColor Magenta
-        if (Test-Path -LiteralPath $saida) { Write-Host '   [IGNORADO] Já existe.' -ForegroundColor Yellow; continue }
-        $paleta = Join-Path $env:TEMP ("paleta_{0}.png" -f ([guid]::NewGuid().ToString('N')))
-        $filtro = "fps=$fps,scale=${larg}:-1:flags=lanczos"
-        & ffmpeg -hide_banner -nostdin -i $v.FullName -vf "$filtro,palettegen" -y $paleta 2>$null
-        & ffmpeg -hide_banner -nostdin -i $v.FullName -i $paleta -lavfi "$filtro [x]; [x][1:v] paletteuse" -y $saida 2>$null
-        Remove-Item -LiteralPath $paleta -Force -ErrorAction SilentlyContinue
-        if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host '   [OK] GIF criado.' -ForegroundColor Green; $ok++ }
-        else { Write-Host '   [ERRO] Falha.' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ }
+        if (Test-Path -LiteralPath $saida) { $ign++; continue }
+        $alvos += [pscustomobject]@{
+            Video  = $v
+            Saida  = $saida
+            Paleta = Join-Path $env:TEMP ("paleta_{0}.png" -f ([guid]::NewGuid().ToString('N')))
+            Filtro = "fps=$fps,scale=${larg}:-1:flags=lanczos"
+        }
+    }
+    if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram pulados." -ForegroundColor Yellow }
+
+    if ($alvos.Count -gt 0) {
+        $lim = Limite-Padrao 'cpu'
+        Write-Host ("{0} video(s), ate {1} ao mesmo tempo..." -f $alvos.Count, $lim) -ForegroundColor Gray
+
+        Write-Host '  Fase 1/2: gerando as paletas de cor...' -ForegroundColor DarkGray
+        $t1 = @()
+        foreach ($a in $alvos) {
+            $t1 += @{ Exe='ffmpeg'; Rotulo=$a.Video.Name; Alvo=$a.Paleta
+                      Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$a.Video.FullName,'-vf',"$($a.Filtro),palettegen",'-y',$a.Paleta) }
+        }
+        $r1 = Executar-EmParalelo -Tarefas $t1 -Limite $lim
+
+        Write-Host '  Fase 2/2: montando os GIFs...' -ForegroundColor DarkGray
+        $t2 = @(); $mapa = @()
+        for ($i = 0; $i -lt $alvos.Count; $i++) {
+            if ($r1[$i].Codigo -ne 0 -or -not (Test-Path -LiteralPath $alvos[$i].Paleta)) {
+                $err++
+                Write-Host ("  [ERRO] {0} (paleta)" -f $alvos[$i].Video.Name) -ForegroundColor Red
+                continue
+            }
+            $a = $alvos[$i]
+            $t2 += @{ Exe='ffmpeg'; Rotulo=$a.Video.Name; Alvo=$a.Saida
+                      Args=@('-hide_banner','-nostdin','-loglevel','error','-i',$a.Video.FullName,'-i',$a.Paleta,
+                             '-lavfi',"$($a.Filtro) [x]; [x][1:v] paletteuse",'-y',$a.Saida) }
+            $mapa += $a
+        }
+        if ($t2.Count -gt 0) {
+            $r2 = Executar-EmParalelo -Tarefas $t2 -Limite $lim
+            Relatar $r2 $t2 ([ref]$ok) ([ref]$err)
+        }
+        foreach ($a in $alvos) { if (Test-Path -LiteralPath $a.Paleta) { Remove-Item -LiteralPath $a.Paleta -Force -ErrorAction SilentlyContinue } }
     }
     Titulo 'Concluído'
     Write-Host "GIFs criados: $ok" -ForegroundColor Green
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Extrair Áudio' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -106,19 +255,62 @@ Write-Host "Vídeos encontrados: $($arquivos.Count)" -ForegroundColor Cyan if ($arquivos.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
 New-Item -ItemType Directory -Path $destino -Force | Out-Null
 
-$ok=0;$ign=0;$err=0;$i=0
+# --- 1) sonda os codecs de audio em paralelo (para poder copiar sem recodificar) ---
+$sonda = @()
 foreach ($a in $arquivos) {
-    $i++
+    $sonda += @{ Exe = 'ffprobe'; Rotulo = $a.Name
+                 Args = @('-v','error','-select_streams','a:0','-show_entries','stream=codec_name','-of','csv=p=0',$a.FullName) }
+}
+$codecs = @{}
+if ($sonda.Count -gt 0) {
+    Write-Host 'Analisando os audios...' -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'sonda') -SemProgresso
+    for ($i = 0; $i -lt $rs.Count; $i++) {
+        $c = ''
+        if ($rs[$i] -and $rs[$i].Codigo -eq 0) { $c = ($rs[$i].Saida -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 1) }
+        $codecs[$arquivos[$i].FullName] = "$c".Trim()
+    }
+}
+
+$ok=0;$ign=0;$err=0;$copiados=0
+
+# --- 2) monta as tarefas ---
+$tarefas = @()
+foreach ($a in $arquivos) {
     $rel = $a.FullName.Substring($origem.Length).TrimStart('\'); $sub = Split-Path $rel -Parent
     $pastaSaida = if ([string]::IsNullOrWhiteSpace($sub)) { $destino } else { Join-Path $destino $sub }
     New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
     $saida = Join-Path $pastaSaida ($a.BaseName + $formato.Ext)
-    Write-Host ''; Write-Host ("[{0}/{1}] {2}" -f $i,$arquivos.Count,$a.Name) -ForegroundColor Magenta
-    if (Test-Path -LiteralPath $saida) { Write-Host '   [IGNORADO] Já existe.' -ForegroundColor Yellow; $ign++; continue }
-    $ffArgs = @('-hide_banner','-nostdin','-i',$a.FullName,'-vn') + $formato.Args + @('-y',$saida)
-    & ffmpeg @ffArgs
-    if ($LASTEXITCODE -eq 0 -and (Test-Path -LiteralPath $saida)) { Write-Host '   [OK] Áudio extraído.' -ForegroundColor Green; $ok++ }
-    else { Write-Host '   [ERRO] Falha (o vídeo tem áudio?).' -ForegroundColor Red; if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }; $err++ }
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
+
+    # Se o codec da fonte ja e' o do destino, copia o stream: quase instantaneo
+    $cod = "$($codecs[$a.FullName])".ToLowerInvariant()
+    $podeCopiar = ($formato.Ext -eq '.m4a' -and $cod -eq 'aac') -or ($formato.Ext -eq '.mp3' -and $cod -eq 'mp3')
+    if ($podeCopiar) { $audioArgs = @('-c:a','copy'); $copiados++ } else { $audioArgs = $formato.Args }
+
+    $ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-i',$a.FullName,'-vn') + $audioArgs + @('-y',$saida)
+    $tarefas += @{ Exe = 'ffmpeg'; Rotulo = $a.Name; Alvo = $saida; Args = $ffArgs }
+}
+
+if ($ign -gt 0) { Write-Host "$ign ja existiam no destino e foram pulados." -ForegroundColor Yellow }
+if ($copiados -gt 0) { Write-Host "$copiados serao copiados sem recodificar (bem mais rapido)." -ForegroundColor Green }
+
+# --- 3) executa em paralelo ---
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'cpu'
+    Write-Host ("Extraindo de {0} video(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok++ }
+        else {
+            $err++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
 }
 Titulo 'Concluído'
 Write-Host "Extraídos: $ok" -ForegroundColor Green
@@ -0,0 +1,317 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Info de Mídia - relatório de codec, resolução e bitrate (FFprobe)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Info de Mídia' } catch {}
+
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'InfoMidia'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Info de Mídia'
+Checar 'ffprobe' 'FFprobe (FFmpeg)' 'winget install Gyan.FFmpeg' | Out-Null
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$ent = Pedir-Entrada $padrao ($extVideo + $extAudio) 'Pasta ou arquivo de mídia'
+$itens = Listar $ent ($extVideo + $extAudio)
+if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo de mídia encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+Titulo 'Analisando'
+Write-Host ("{0} arquivo(s). Lendo em paralelo..." -f $itens.Count) -ForegroundColor Gray
+$tarefas = @()
+foreach ($f in $itens) {
+    $tarefas += @{ Exe='ffprobe'; Rotulo=$f.Name
+                   Args=@('-v','error','-show_entries','format=duration,bit_rate:stream=codec_type,codec_name,width,height,channels','-of','default=noprint_wrappers=1',$f.FullName) }
+}
+$rs = Executar-EmParalelo -Tarefas $tarefas -Limite (Limite-Padrao 'sonda')
+
+$linhas = @()
+for ($i=0; $i -lt $itens.Count; $i++) {
+    $f = $itens[$i]; $s = if ($rs[$i]) { "$($rs[$i].Saida)" } else { '' }
+    function Pega($chave) { $m = [regex]::Match($s, "(?m)^$chave=(.+)$"); if ($m.Success) { $m.Groups[1].Value.Trim() } else { '' } }
+    $dur = 0.0; [double]::TryParse(((Pega 'duration') -replace ',', '.'), [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$dur) | Out-Null
+    $w = Pega 'width'; $h = Pega 'height'
+    $codecs = [regex]::Matches($s, "(?m)^codec_name=(.+)$") | ForEach-Object { $_.Groups[1].Value.Trim() }
+    $br = Pega 'bit_rate'; $brTxt = if ($br -as [double]) { "{0:N0} kbps" -f ([double]$br / 1000) } else { '' }
+    $linhas += [pscustomobject]@{
+        Arquivo   = $f.Name
+        Duracao   = if ($dur -gt 0) { "{0:hh\:mm\:ss}" -f [TimeSpan]::FromSeconds($dur) } else { '' }
+        Resolucao = if ($w -and $h) { "${w}x${h}" } else { '' }
+        Codecs    = ($codecs -join '/')
+        Bitrate   = $brTxt
+        Tamanho   = (Tam $f.Length)
+        Caminho   = $f.FullName
+    }
+}
+
+Titulo 'Resultado'
+$linhas | Format-Table Arquivo, Duracao, Resolucao, Codecs, Bitrate, Tamanho -AutoSize | Out-String -Width 200 | Write-Host
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { $ent.Origem })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimaSaida=$destino })
+$csv = Join-Path $destino ("info-midia_{0}.csv" -f (Get-Date -Format 'yyyy-MM-dd_HH-mm'))
+try {
+    $linhas | Export-Csv -LiteralPath $csv -NoTypeInformation -Encoding UTF8 -Delimiter ';'
+    Write-Host "[OK] Planilha salva em: $csv" -ForegroundColor Green
+} catch { Write-Host "[!] Não foi possível salvar o CSV: $($_.Exception.Message)" -ForegroundColor Yellow }
+Resumo $linhas.Count 0 0 $destino
@@ -0,0 +1,181 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Juntar Vídeos - concatena vários vídeos num só (FFmpeg)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Juntar Vídeos' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'JuntarVideos'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Juntar Vídeos'
+Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$pasta = $null
+while (-not $pasta) {
+    Titulo 'Pasta com os vídeos'
+    Write-Host "  $padrao" -ForegroundColor White
+    Write-Host 'Enter = confirmar  |  os vídeos são juntados em ordem alfabética' -ForegroundColor DarkGray
+    $r = Read-Host 'Pasta'
+    $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+    if (Test-Path -LiteralPath $c -PathType Container) { $pasta = (Resolve-Path -LiteralPath $c).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $c }
+}
+$videos = @(Get-ChildItem -LiteralPath $pasta -File | Where-Object { $extVideo -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+if ($videos.Count -lt 2) { Write-Host 'São necessários pelo menos 2 vídeos nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+Titulo 'Ordem'
+for ($i=0; $i -lt $videos.Count; $i++) { Write-Host ("  {0,2}. {1}" -f ($i+1), $videos[$i].Name) -ForegroundColor Gray }
+Write-Host ''
+$ok = Read-Host "Juntar esses $($videos.Count) vídeos nessa ordem? (s/N)"
+if ($ok -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+Titulo 'Modo'
+Write-Host '> [1] Rápido - sem recodificar (exige que sejam do mesmo tipo/codec)' -ForegroundColor White
+Write-Host '  [2] Seguro - recodifica tudo para um padrão (sempre funciona, demora)' -ForegroundColor Gray
+$m = Read-Host 'Modo (Enter = 1)'
+$recodificar = ($m.Trim() -eq '2')
+
+$nome = (Read-Host 'Nome do arquivo final (Enter = juntado.mp4)').Trim()
+if ([string]::IsNullOrWhiteSpace($nome)) { $nome = 'juntado.mp4' }
+if ($nome -notmatch '\.\w+$') { $nome += '.mp4' }
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { $pasta })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta; UltimaSaida=$destino })
+$saida = Join-Path $destino $nome
+
+$lista = Join-Path $env:TEMP ("juntar_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
+$sb = New-Object System.Text.StringBuilder
+foreach ($v in $videos) { [void]$sb.AppendLine("file '" + ($v.FullName -replace '\\', '/') + "'") }
+[System.IO.File]::WriteAllText($lista, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false)))
+
+$ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-f','concat','-safe','0','-i',$lista)
+if ($recodificar) { $ffArgs += @('-c:v','libx264','-preset','veryfast','-crf','20','-c:a','aac','-b:a','160k') }
+else { $ffArgs += @('-c','copy') }
+$ffArgs += @('-movflags','+faststart','-y',$saida)
+
+Titulo 'Juntando'
+Write-Host "Saída: $saida"
+Write-Host ''
+& ffmpeg @ffArgs
+$rc = $LASTEXITCODE
+Remove-Item -LiteralPath $lista -Force -ErrorAction SilentlyContinue
+
+if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) {
+    Write-Host ("[OK] {0} vídeos juntados ({1})." -f $videos.Count, (Tam (Get-Item $saida).Length)) -ForegroundColor Green
+    Resumo 1 0 0 $destino
+} else {
+    Write-Host '[ERRO] Falhou. Se usou o modo rápido, tente o modo seguro (opção 2).' -ForegroundColor Red
+    if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
+    Resumo 0 0 1 $destino
+}
@@ -0,0 +1,350 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Marca d'água em Vídeo - aplica texto ou logo sobre vídeos (FFmpeg)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Marca d''água em Vídeo' } catch {}
+
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'MarcaDaguaVideo'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir "Marca d'água em Vídeo"
+Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$ent = Pedir-Entrada $padrao $extVideo 'Pasta ou vídeo'
+$itens = Listar $ent $extVideo
+if ($itens.Count -eq 0) { Write-Host 'Nenhum vídeo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+Titulo "Tipo de marca d'água"
+Write-Host '  [1] Texto' -ForegroundColor White
+Write-Host '  [2] Logo (arquivo PNG, de preferência com fundo transparente)' -ForegroundColor White
+$tipo = $null
+while (-not $tipo) { $t = Read-Host 'Tipo (1-2)'; switch ($t.Trim()) { '1' { $tipo='texto' } '2' { $tipo='logo' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } }
+
+$texto = $null; $logo = $null; $pct = 20
+if ($tipo -eq 'texto') {
+    while ([string]::IsNullOrWhiteSpace($texto)) { $texto = Read-Host 'Texto da marca' }
+} else {
+    while (-not $logo) {
+        $l = (Read-Host 'Caminho do logo (PNG)').Trim().Trim('"')
+        if (Test-Path -LiteralPath $l -PathType Leaf) { $logo = (Resolve-Path -LiteralPath $l).Path } else { Write-Host '[!] Arquivo não encontrado.' -ForegroundColor Yellow }
+    }
+    $p = Read-Host 'Largura do logo em % do vídeo (Enter = 20)'
+    if (-not [string]::IsNullOrWhiteSpace($p) -and ($p -as [int])) { $pct = [int]$p }
+}
+
+$posicoes = @(
+    [pscustomobject]@{ Id=1; Nome='Inferior direita';  XY='W-w-20:H-h-20'; TXY='w-tw-20:y=h-th-20' },
+    [pscustomobject]@{ Id=2; Nome='Inferior esquerda'; XY='20:H-h-20';     TXY='20:y=h-th-20' },
+    [pscustomobject]@{ Id=3; Nome='Superior direita';  XY='W-w-20:20';     TXY='w-tw-20:y=20' },
+    [pscustomobject]@{ Id=4; Nome='Superior esquerda'; XY='20:20';         TXY='20:y=20' },
+    [pscustomobject]@{ Id=5; Nome='Centro';            XY='(W-w)/2:(H-h)/2'; TXY='(w-tw)/2:y=(h-th)/2' }
+)
+Titulo 'Posição'
+foreach ($p in $posicoes) { Write-Host ("  [{0}] {1}" -f $p.Id, $p.Nome) -ForegroundColor Gray }
+$pos = $null
+while (-not $pos) { $e = Read-Host 'Posição (1-5, Enter = 1)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = '1' }; $pos = $posicoes | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1; if (-not $pos) { Write-Host '[!] Inválido.' -ForegroundColor Yellow } }
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'ComMarca' })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoTipo=$tipo; UltimoTexto=$texto; UltimoLogo=$logo; UltimaPosicao=$pos.Id; UltimaSaida=$destino })
+
+# O drawtext do FFmpeg nao tem fonte padrao no Windows: e' preciso apontar uma.
+$fonteArq = $null
+foreach ($cand in @('C:\Windows\Fonts\arial.ttf','C:\Windows\Fonts\segoeui.ttf','C:\Windows\Fonts\calibri.ttf','C:\Windows\Fonts\tahoma.ttf')) {
+    if (Test-Path -LiteralPath $cand) { $fonteArq = $cand; break }
+}
+if (-not $fonteArq -and $tipo -eq 'texto') {
+    Write-Host '[ERRO] Nenhuma fonte encontrada em C:\Windows\Fonts para desenhar o texto.' -ForegroundColor Red
+    Pausar; exit 1
+}
+# no filtro do ffmpeg o caminho vai com / e o ':' da unidade precisa ser escapado
+$fonteFf = if ($fonteArq) { ($fonteArq -replace '\\', '/') -replace '^([A-Za-z]):', '$1\:' } else { '' }
+$temNvenc = [bool](& ffmpeg -hide_banner -encoders 2>$null | Select-String 'h264_nvenc')
+$vcodec = if ($temNvenc) { @('-c:v','h264_nvenc','-preset','p5','-cq','23') } else { @('-c:v','libx264','-preset','veryfast','-crf','21') }
+
+$ok=0;$ign=0;$err=0
+$tarefas = @()
+foreach ($f in $itens) {
+    $saida = Join-Path $destino ($f.BaseName + '.mp4')
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
+    if ($tipo -eq 'texto') {
+        $txt = $texto -replace '\\', '\\\\' -replace ':', '\:' -replace "'", ""
+        $filtro = "drawtext=fontfile='$fonteFf':text='$txt':fontcolor=white@0.85:fontsize=h/22:box=1:boxcolor=black@0.35:boxborderw=8:x=$($pos.TXY)"
+        $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-vf',$filtro) + $vcodec + @('-c:a','copy','-y',$saida)
+    } else {
+        $filtro = "[1][0]scale2ref=w=iw*$pct/100:h=ow/mdar[logo][base];[base][logo]overlay=$($pos.XY)"
+        $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,'-i',$logo,'-filter_complex',$filtro) + $vcodec + @('-c:a','copy','-y',$saida)
+    }
+    $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida; Args=$a }
+}
+if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow }
+
+Titulo 'Aplicando'
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao $(if ($temNvenc) { 'gpu' } else { 'cpu' })
+    Write-Host ("{0} vídeo(s), até {1} ao mesmo tempo ({2})..." -f $tarefas.Count, $lim, $(if ($temNvenc) { 'GPU NVENC' } else { 'CPU' })) -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    Relatar $rs $tarefas ([ref]$ok) ([ref]$err)
+}
+Resumo $ok $ign $err $destino
@@ -0,0 +1,323 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Normalizar Áudio - padroniza o volume em lote, EBU R128 (FFmpeg)
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Normalizar Áudio' } catch {}
+
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'NormalizarAudio'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Normalizar Áudio'
+Checar 'ffmpeg' 'FFmpeg' 'winget install Gyan.FFmpeg' | Out-Null
+$config = Carregar-Config
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$ent = Pedir-Entrada $padrao ($extVideo + $extAudio) 'Pasta ou arquivo'
+$itens = Listar $ent ($extVideo + $extAudio)
+if ($itens.Count -eq 0) { Write-Host 'Nenhum arquivo encontrado.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+$alvos = @(
+    [pscustomobject]@{ Id=1; Nome='Streaming / YouTube  (-14 LUFS)'; I=-14 },
+    [pscustomobject]@{ Id=2; Nome='Podcast / voz        (-16 LUFS)'; I=-16 },
+    [pscustomobject]@{ Id=3; Nome='TV / broadcast       (-23 LUFS)'; I=-23 }
+)
+$padraoId = if ($config -and $config.UltimoAlvo) { [int]$config.UltimoAlvo } else { 1 }
+Titulo 'Volume alvo'
+foreach ($a in $alvos) {
+    $m = if ($a.Id -eq $padraoId) { '>' } else { ' ' }
+    $c = if ($a.Id -eq $padraoId) { 'White' } else { 'Gray' }
+    Write-Host ("{0} [{1}] {2}" -f $m, $a.Id, $a.Nome) -ForegroundColor $c
+}
+Write-Host "Enter = usar a opção padrão [$padraoId]" -ForegroundColor DarkGray
+$alvo = $null
+while (-not $alvo) {
+    $e = Read-Host 'Alvo (1-3)'; if ([string]::IsNullOrWhiteSpace($e)) { $e = "$padraoId" }
+    $alvo = $alvos | Where-Object { $_.Id -eq ($e -as [int]) } | Select-Object -First 1
+    if (-not $alvo) { Write-Host '[!] Escolha de 1 a 3.' -ForegroundColor Yellow }
+}
+
+$destino = Pedir-Saida $(if ($config -and $config.UltimaSaida) { $config.UltimaSaida } else { Join-Path $ent.Origem 'Normalizado' })
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$ent.Origem; UltimoAlvo=$alvo.Id; UltimaSaida=$destino })
+
+$ok=0;$ign=0;$err=0
+$tarefas = @()
+foreach ($f in $itens) {
+    $saida = Join-Path $destino $f.Name
+    if (Test-Path -LiteralPath $saida) { $ign++; continue }
+    $ehVideo = $extVideo -contains $f.Extension.ToLowerInvariant()
+    $a = @('-hide_banner','-nostdin','-loglevel','error','-i',$f.FullName,
+           '-af',"loudnorm=I=$($alvo.I):TP=-1.5:LRA=11")
+    if ($ehVideo) { $a += @('-c:v','copy') }
+    $a += @('-y',$saida)
+    $tarefas += @{ Exe='ffmpeg'; Rotulo=$f.Name; Alvo=$saida; Args=$a }
+}
+if ($ign -gt 0) { Write-Host "$ign já existiam no destino e foram pulados." -ForegroundColor Yellow }
+
+Titulo 'Normalizando'
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao 'cpu'
+    Write-Host ("{0} arquivo(s), até {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    Relatar $rs $tarefas ([ref]$ok) ([ref]$err)
+}
+Resumo $ok $ign $err $destino
@@ -12,6 +12,155 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Transcodificar Vídeos' } catch {}
 
+# ============================================================
+#  Executar-EmParalelo - pool de processos externos (PS 5.1)
+#  Cada tarefa: @{ Exe='ffmpeg'; Args=@(...); Rotulo='x.mp4'; Dur=123.4 }
+#  Dur e' opcional (segundos) e so' pondera a barra de progresso.
+#  Devolve 1 objeto por tarefa: Indice/Rotulo/Codigo/Saida/Erro/Segundos
+# ============================================================
+function Limite-Padrao([string]$tipo) {
+    $n = [Environment]::ProcessorCount
+    switch ($tipo) {
+        'gpu'    { 3 }
+        'cpu'    { [Math]::Max(2, [Math]::Min(4,  [int]($n / 6))) }
+        'imagem' { [Math]::Max(2, [Math]::Min(8,  [int]($n / 3))) }
+        'sonda'  { [Math]::Max(4, [Math]::Min(12, [int]($n / 2))) }
+        default  { 4 }
+    }
+}
+
+function Executar-EmParalelo {
+    param(
+        [object[]]$Tarefas,
+        [int]$Limite = 4,
+        [switch]$SemProgresso
+    )
+
+    # Quoting do Windows (CommandLineToArgvW). NAO simplificar: dobrar as
+    # barras finais e' o que impede um caminho terminado em '\' de comer
+    # a aspa de fechamento.
+    function _Citar([string]$a) {
+        if ($a -eq '')            { return '""' }
+        if ($a -notmatch '[\s"]') { return $a }
+        $s = ''; $bs = 0
+        foreach ($c in $a.ToCharArray()) {
+            if ($c -eq '\') { $bs++; continue }
+            if ($c -eq '"') { $s += ('\' * ($bs * 2 + 1)) + '"'; $bs = 0; continue }
+            if ($bs) { $s += ('\' * $bs); $bs = 0 }
+            $s += $c
+        }
+        if ($bs) { $s += ('\' * ($bs * 2)) }
+        return '"' + $s + '"'
+    }
+
+    $tot = @($Tarefas).Count
+    if ($tot -eq 0) { return @() }
+    if ($Limite -lt 1) { $Limite = 1 }
+
+    $res    = New-Object object[] $tot
+    $ativos = New-Object System.Collections.ArrayList
+    $prox = 0; $feitos = 0; $falhas = 0
+    $durTotal = 0.0; foreach ($t in $Tarefas) { if ($t.Dur) { $durTotal += [double]$t.Dur } }
+    $durFeita = 0.0
+    $t0 = Get-Date
+
+    try {
+        while ($feitos -lt $tot) {
+
+            while ($ativos.Count -lt $Limite -and $prox -lt $tot) {
+                $t = $Tarefas[$prox]
+                $psi = New-Object System.Diagnostics.ProcessStartInfo
+                $psi.FileName               = $t.Exe
+                $psi.Arguments              = ((@($t.Args) | ForEach-Object { _Citar $_ }) -join ' ')
+                $psi.UseShellExecute        = $false
+                $psi.CreateNoWindow         = $true
+                $psi.RedirectStandardOutput = $true
+                $psi.RedirectStandardError  = $true
+                $psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
+                $psi.StandardErrorEncoding  = [System.Text.Encoding]::UTF8
+                $p = [System.Diagnostics.Process]::Start($psi)
+                [void]$ativos.Add([pscustomobject]@{
+                    Idx   = $prox
+                    Rot   = $t.Rotulo
+                    Dur   = $(if ($t.Dur) { [double]$t.Dur } else { 0.0 })
+                    Proc  = $p
+                    Linha = $p.StandardOutput.ReadLineAsync()
+                    Err   = $p.StandardError.ReadToEndAsync()
+                    Buf   = (New-Object System.Text.StringBuilder)
+                    Seg   = 0.0
+                    Ini   = (Get-Date)
+                })
+                $prox++
+            }
+
+            # drena stdout sem bloquear (senao o pipe cheio trava o processo)
+            foreach ($a in $ativos) {
+                while ($a.Linha -and $a.Linha.IsCompleted) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    if ($l -match '^out_time_us=(\d+)' -or $l -match '^out_time_ms=(\d+)') {
+                        $a.Seg = [double]$matches[1] / 1000000.0
+                    }
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+            }
+
+            for ($i = $ativos.Count - 1; $i -ge 0; $i--) {
+                $a = $ativos[$i]
+                if (-not $a.Proc.HasExited) { continue }
+                while ($a.Linha) {
+                    $l = $a.Linha.Result
+                    if ($null -eq $l) { $a.Linha = $null; break }
+                    [void]$a.Buf.AppendLine($l)
+                    $a.Linha = $a.Proc.StandardOutput.ReadLineAsync()
+                }
+                $rc = $a.Proc.ExitCode
+                $res[$a.Idx] = [pscustomobject]@{
+                    Indice   = $a.Idx
+                    Rotulo   = $a.Rot
+                    Codigo   = $rc
+                    Saida    = $a.Buf.ToString()
+                    Erro     = $a.Err.Result
+                    Segundos = [math]::Round(((Get-Date) - $a.Ini).TotalSeconds, 1)
+                }
+                $durFeita += $a.Dur
+                try { $a.Proc.Dispose() } catch {}
+                $ativos.RemoveAt($i)
+                $feitos++
+                if ($rc -ne 0) { $falhas++ }
+            }
+
+            if (-not $SemProgresso) {
+                if ($durTotal -gt 0) {
+                    $parcial = $durFeita; foreach ($a in $ativos) { $parcial += $a.Seg }
+                    $frac = $parcial / $durTotal
+                } else {
+                    $frac = $feitos / $tot
+                }
+                if ($frac -gt 1) { $frac = 1 }
+                $pct = [int]($frac * 100)
+                $dec = (Get-Date) - $t0
+                $eta = if ($frac -gt 0.01) { [TimeSpan]::FromSeconds($dec.TotalSeconds / $frac - $dec.TotalSeconds) } else { [TimeSpan]::Zero }
+                $ch  = [int]($pct / 5)
+                Write-Host ("`r  [{0}] {1,3}%  {2}/{3}  ativos:{4}  falhas:{5}  restante ~{6:mm\:ss}   " -f `
+                    (('#' * $ch) + ('-' * (20 - $ch))), $pct, $feitos, $tot, $ativos.Count, $falhas, $eta) `
+                    -NoNewline -ForegroundColor Cyan
+            }
+
+            if ($feitos -lt $tot) { Start-Sleep -Milliseconds 150 }
+        }
+    }
+    finally {
+        foreach ($a in $ativos) {
+            try { if (-not $a.Proc.HasExited) { $a.Proc.Kill() } } catch {}
+            try { $a.Proc.Dispose() } catch {}
+        }
+    }
+    if (-not $SemProgresso) { Write-Host '' }
+    return $res
+}
+
 function Pausar {
     Write-Host ''
     Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray
@@ -259,64 +408,66 @@ $convertidos = 0
 $ignorados   = 0
 $erros       = 0
-$indice      = 0
 
+# --- 1) sonda as duracoes em paralelo (alimenta a barra global) ---
+$sonda = @()
 foreach ($arquivo in $arquivos) {
-    $indice++
+    $sonda += @{ Exe='ffprobe'; Rotulo=$arquivo.Name
+                 Args=@('-v','error','-show_entries','format=duration','-of','csv=p=0',$arquivo.FullName) }
+}
+$duracoes = @{}
+if ($sonda.Count -gt 0) {
+    Write-Host 'Analisando os videos...' -ForegroundColor Gray
+    $rs = Executar-EmParalelo -Tarefas $sonda -Limite (Limite-Padrao 'sonda') -SemProgresso
+    for ($i = 0; $i -lt $rs.Count; $i++) {
+        $d = 0.0
+        if ($rs[$i] -and $rs[$i].Codigo -eq 0) {
+            $bruto = (("$($rs[$i].Saida)" -split "`n" | Where-Object { $_.Trim() } | Select-Object -First 1) -replace ',', '.')
+            [double]::TryParse($bruto, [System.Globalization.NumberStyles]::Float, [System.Globalization.CultureInfo]::InvariantCulture, [ref]$d) | Out-Null
+        }
+        $duracoes[$arquivos[$i].FullName] = $d
+    }
+}
 
+# --- 2) monta as tarefas ---
+switch ($motor) {
+    'gpu'          { $vcodec = @('-c:v','h264_nvenc','-preset','p5','-cq',"$($perfil.Crf)",'-pix_fmt','yuv420p') }
+    'cpu-rapido'   { $vcodec = @('-c:v','libx264','-preset','veryfast','-crf',"$($perfil.Crf)",'-pix_fmt','yuv420p') }
+    default        { $vcodec = @('-c:v','libx264','-preset','medium','-crf',"$($perfil.Crf)",'-pix_fmt','yuv420p') }
+}
+$tarefas = @()
+foreach ($arquivo in $arquivos) {
     $relativo = $arquivo.FullName.Substring($origem.Length).TrimStart('\')
     $subpasta = Split-Path -Path $relativo -Parent
     $pastaSaida = if ([string]::IsNullOrWhiteSpace($subpasta)) { $destino } else { Join-Path $destino $subpasta }
     New-Item -ItemType Directory -Path $pastaSaida -Force | Out-Null
-
     $saida = Join-Path $pastaSaida ($arquivo.BaseName + '.mp4')
-
-    Write-Host ''
-    Write-Host ("[{0}/{1}]" -f $indice, $arquivos.Count) -ForegroundColor Magenta -NoNewline
-    Write-Host " $($arquivo.Name)"
-
-    if (Test-Path -LiteralPath $saida) {
-        Write-Host "   [IGNORADO] Já existe na pasta de destino." -ForegroundColor Yellow
-        $ignorados++
-        continue
-    }
-
-    # Codec de vídeo conforme o motor escolhido
-    switch ($motor) {
-        'gpu'          { $vcodec = @('-c:v', 'h264_nvenc', '-preset', 'p5', '-cq', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') }
-        'cpu-rapido'   { $vcodec = @('-c:v', 'libx264', '-preset', 'veryfast', '-crf', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') }
-        default        { $vcodec = @('-c:v', 'libx264', '-preset', 'medium', '-crf', "$($perfil.Crf)", '-pix_fmt', 'yuv420p') }
-    }
-    $ffArgs = @('-hide_banner', '-nostdin', '-i', $arquivo.FullName, '-map', '0:v:0', '-map', '0:a?', '-vf', $vf) +
+    if (Test-Path -LiteralPath $saida) { $ignorados++; continue }
+    $ffArgs = @('-hide_banner','-nostdin','-loglevel','error','-i',$arquivo.FullName,'-map','0:v:0','-map','0:a?','-vf',$vf) +
               $vcodec +
-              @('-c:a', 'aac', '-b:a', $perfil.Audio, '-ac', '2', '-movflags', '+faststart', '-progress', 'pipe:1', '-nostats', '-y', $saida)
+              @('-c:a','aac','-b:a',$perfil.Audio,'-ac','2','-movflags','+faststart','-progress','pipe:1','-nostats','-y',$saida)
+    $tarefas += @{ Exe='ffmpeg'; Rotulo=$arquivo.Name; Alvo=$saida; Dur=$duracoes[$arquivo.FullName]; Args=$ffArgs }
+}
 
-    # Duração do vídeo para a barra de progresso
-    $dur = 0.0
-    try { $dur = [double](& ffprobe -v error -show_entries format=duration -of csv=p=0 $arquivo.FullName 2>$null) } catch {}
+if ($ignorados -gt 0) { Write-Host "$ignorados ja existiam no destino e foram pulados." -ForegroundColor Yellow }
 
-    & ffmpeg @ffArgs 2>$null | ForEach-Object {
-        if ($dur -gt 0 -and $_ -match '^out_time=(\d+):(\d+):([\d.]+)') {
-            $seg = [int]$matches[1]*3600 + [int]$matches[2]*60 + [double]$matches[3]
-            $pct = [int](($seg / $dur) * 100); if ($pct -gt 100) { $pct = 100 }
-            $cheio = [int]($pct / 5)
-            $barra = ('#' * $cheio) + ('-' * (20 - $cheio))
-            Write-Host ("`r   [{0}] {1,3}%  " -f $barra, $pct) -NoNewline -ForegroundColor Cyan
+# --- 3) converte em paralelo ---
+if ($tarefas.Count -gt 0) {
+    $lim = Limite-Padrao $(if ($motor -eq 'gpu') { 'gpu' } else { 'cpu' })
+    Write-Host ("Convertendo {0} video(s), ate {1} ao mesmo tempo..." -f $tarefas.Count, $lim) -ForegroundColor Gray
+    $resultados = Executar-EmParalelo -Tarefas $tarefas -Limite $lim
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $convertidos++ }
+        else {
+            $erros++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
         }
     }
-    $rc = $LASTEXITCODE
-    if ($dur -gt 0) { Write-Host ("`r   [{0}] 100%  " -f ('#' * 20)) -ForegroundColor Cyan }
-
-    if ($rc -eq 0 -and (Test-Path -LiteralPath $saida)) {
-        Write-Host "   [OK] Convertido." -ForegroundColor Green
-        $convertidos++
-    } else {
-        Write-Host "   [ERRO] Falha ao converter." -ForegroundColor Red
-        if (Test-Path -LiteralPath $saida) { Remove-Item -LiteralPath $saida -Force -ErrorAction SilentlyContinue }
-        $erros++
-    }
 }
-
 # ------------------------------------------------------------
 # 5) Resumo final
 # ------------------------------------------------------------
@@ -0,0 +1,170 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Canivete - menu central de todos os programas
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Canivete' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'Canivete'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+$raizCanivete = Split-Path -Parent $MyInvocation.MyCommand.Path
+if (-not $raizCanivete) { $raizCanivete = (Get-Location).Path }
+
+$ordem = @('Vídeos','Imagens','Organizar','Úteis')
+$icone = @{ 'Vídeos'='[V]'; 'Imagens'='[I]'; 'Organizar'='[O]'; 'Úteis'='[U]' }
+
+while ($true) {
+    Clear-Host
+    Titulo 'CANIVETE - todos os programas'
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+
+    $itens = @()
+    $n = 0
+    foreach ($cat in $ordem) {
+        $dir = Join-Path $raizCanivete $cat
+        if (-not (Test-Path -LiteralPath $dir)) { continue }
+        $cmds = @(Get-ChildItem -LiteralPath $dir -File -Filter *.cmd | Sort-Object Name)
+        if ($cmds.Count -eq 0) { continue }
+        Write-Host ''
+        Write-Host ("  {0} {1}" -f $icone[$cat], $cat.ToUpperInvariant()) -ForegroundColor Cyan
+        foreach ($c in $cmds) {
+            $n++
+            $itens += $c
+            $nome = $c.BaseName -replace '-', ' '
+            Write-Host ("    {0,2}. {1}" -f $n, $nome) -ForegroundColor Gray
+        }
+    }
+    if ($itens.Count -eq 0) {
+        Write-Host ''
+        Write-Host '[!] Nenhum programa encontrado. Coloque o canivete.cmd na pasta raiz da coleção.' -ForegroundColor Yellow
+        Pausar; exit 1
+    }
+
+    Write-Host ''
+    Write-Host '     0. Sair' -ForegroundColor DarkGray
+    Write-Host ''
+    $e = Read-Host "Abrir qual? (1-$($itens.Count), 0 = sair)"
+    if ([string]::IsNullOrWhiteSpace($e) -or $e.Trim() -eq '0') { break }
+    $i = $e -as [int]
+    if ($i -ge 1 -and $i -le $itens.Count) {
+        $alvo = $itens[$i-1]
+        Write-Host ''
+        Write-Host ("Abrindo {0}..." -f $alvo.BaseName) -ForegroundColor Green
+        try { Start-Process -FilePath $alvo.FullName | Out-Null }
+        catch { Write-Host "[ERRO] Não foi possível abrir: $($_.Exception.Message)" -ForegroundColor Red; Start-Sleep -Seconds 2 }
+        Start-Sleep -Milliseconds 600
+    } else {
+        Write-Host '[!] Opção inválida.' -ForegroundColor Yellow
+        Start-Sleep -Seconds 1
+    }
+}
@@ -0,0 +1,24 @@+# Dependências
+
+A maioria dos programas **não precisa de nada**. Os que precisam avisam na tela e mostram o comando de instalação.
+
+## Tabela
+
+| Ferramenta | Para que serve | Como instalar | Usada por |
+|---|---|---|---|
+| **FFmpeg** | Tudo de vídeo e áudio | `winget install Gyan.FFmpeg` | transcodificar, extrair-audio, criar-gif, cortar-video, juntar-videos, marca-dagua-video, info-midia, normalizar-audio, baixar-videos, limpar-metadados |
+| **ImageMagick** | Tudo de imagem | `winget install ImageMagick.ImageMagick` | converter-imagens, marca-dagua, imagens-e-pdf, limpar-metadados, renomear-em-lote (data da foto) |
+| **Ghostscript** | Ler e comprimir PDF | `winget install ArtifexSoftware.GhostScript` | comprimir-pdf, imagens-e-pdf (PDF → imagens) |
+| **yt-dlp** | Baixar de sites | *baixado automaticamente* | baixar-videos |
+| **7-Zip** *(opcional)* | `.rar`, `.7z` e mais velocidade | `winget install 7zip.7zip` | compactar-zip |
+| **smartmontools** *(opcional)* | Detalhes SMART avançados | `winget install smartmontools` | verificar-discos |
+
+## Observações
+
+- **O yt-dlp se instala sozinho.** Se não existir na máquina, o `baixar-videos` baixa a versão oficial mais recente em `%LOCALAPPDATA%\fast-scripts` e ainda a mantém atualizada.
+- **O Ghostscript costuma não ficar no PATH.** Os programas procuram também em `C:\Program Files\gs`, então funciona mesmo assim.
+- **Placa NVIDIA** não é uma dependência, mas é aproveitada automaticamente quando existe (transcodificar e marca-dagua-video).
+- Programas que **não precisam de nada**: organizar-por-tipo, achar-duplicados, limpar-nomes, backup-espelhado, analisar-espaco, verificar-integridade, relatorio-sistema, limpeza-segura, verificar-discos, canivete.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,104 @@+# Padrões do projeto
+
+Contrato técnico da coleção — leia antes de criar ou alterar qualquer programa.
+
+## 1. Um arquivo, autocontido
+
+Cada programa é **um único `.cmd`**, sem depender de nenhum outro arquivo. Os programas ficam num compartilhamento de rede e podem ser copiados isoladamente, então **não existe biblioteca compartilhada**: o código comum é duplicado de propósito, mas deve ser **idêntico** em todos.
+
+## 2. Cabeçalho polyglot
+
+O arquivo é batch e PowerShell ao mesmo tempo. O batch relança o próprio arquivo no PowerShell:
+
+```
+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+```
+
+**Variante para quem precisa de administrador** (`verificar-discos`, `limpeza-segura`): expõe o próprio caminho numa variável, porque o script precisa se relançar elevado.
+
+```
+<# :
+@echo off
+set "SELF=%~f0"
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText($env:SELF,[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+```
+
+## 3. Codificação — a regra que mais quebra
+
+Os `.cmd` **precisam** ser gravados em **UTF-8 sem BOM** com quebras **CRLF**.
+
+- Com BOM, o batch não reconhece a primeira linha e o duplo-clique falha.
+- Com LF, o `cmd.exe` se perde no meio do arquivo.
+
+Depois de qualquer edição, normalize:
+
+```powershell
+$t = [System.IO.File]::ReadAllText($p, [System.Text.Encoding]::UTF8)
+$t = $t -replace "`r`n","`n" -replace "`n","`r`n"
+[System.IO.File]::WriteAllText($p, $t, (New-Object System.Text.UTF8Encoding($false)))
+```
+
+Os primeiros bytes devem ser `3C 23 20 3A 0D`.
+
+> **Atenção:** para scripts `.ps1` auxiliares vale o **contrário** — o Windows PowerShell 5.1 lê `.ps1` sem BOM como ANSI e destrói os acentos. Esses devem ter BOM.
+
+## 4. Verificação obrigatória
+
+Antes de considerar qualquer arquivo pronto:
+
+```powershell
+$e = $null; $tok = $null
+[void][System.Management.Automation.Language.Parser]::ParseInput($t, [ref]$tok, [ref]$e)
+```
+
+E rodar de verdade, alimentando os prompts por `stdin`, numa pasta de teste.
+
+## 5. Experiência padrão
+
+1. Verificar as dependências primeiro e avisar com o comando `winget` se faltar.
+2. Perguntar **pasta ou arquivo** (aceitar os dois).
+3. Menus com `>` marcando a opção padrão; Enter aceita o padrão.
+4. Perguntar a **pasta de saída**, sugerindo a última usada e **criando-a se não existir**.
+5. Lembrar as escolhas em `%APPDATA%\<Nome>\config.json`.
+6. Preservar subpastas e **pular o que já foi feito**.
+7. Nada destrutivo sem **prévia + confirmação**.
+8. Resumo no fim e `Pausar` antes de fechar.
+9. Assinatura `Desenvolvido por Pablo Murad - 2026` no comentário do cabeçalho **e** na tela.
+
+## 6. Paralelismo
+
+Programas que processam lotes usam a função `Executar-EmParalelo`, colada dentro de cada `.cmd`.
+
+**Pontos que não podem ser simplificados:**
+
+- Usa `System.Diagnostics.Process`. **Nunca** `Start-Process -ArgumentList` (quebra o quoting) nem `Start-Job` (~125 ms de overhead por item).
+- O helper `_Citar` implementa as regras do `CommandLineToArgvW`. A regra crítica é **dobrar as barras finais** — sem isso, uma pasta colada do Explorer terminada em `\` engole a aspa de fechamento.
+- Redirecionar **stdout e stderr**. Não redirecionar o stderr trava o ffmpeg quando o buffer do pipe enche.
+- Ler `$p.ExitCode` por processo — `$LASTEXITCODE` é global e não sobrevive ao paralelismo. Testar `-ne 0`, nunca `-gt 0` (o ffmpeg devolve `-2`).
+
+**Limites medidos nesta máquina (24 núcleos, RTX 5070 Ti):**
+
+| Tipo | Limite | Ganho medido |
+|------|--------|--------------|
+| `imagem` | 8 | até **4,2×** |
+| `cpu` (x264) | 4 | **1,57×** — com 12 fica *pior* que sequencial |
+| `gpu` (NVENC) | 3 | ~10% — o encoder satura sozinho |
+| `sonda` (ffprobe) | 12 | elimina N inicializações em série |
+
+## 7. Armadilhas já encontradas
+
+- **`Get-FileHash` não está disponível** quando o `.cmd` roda via `Invoke-Expression`. Use `[System.Security.Cryptography.SHA256]` direto.
+- **Variáveis do script não resolvem dentro de funções** nesse contexto: passe o que precisar como **parâmetro**.
+- **`catch {}` vazio esconde bugs.** Sempre mostre o motivo do erro.
+- **O robocopy é traduzido:** em português imprime `*Arquivo EXTRA`, em inglês `*EXTRA File`. Use uma regex que pegue os dois.
+- **`Get-Command` devolve `.Source`; `Get-Item` devolve `.FullName`.** Normalize num caminho só.
+- **O `drawtext` do FFmpeg não tem fonte padrão no Windows:** é preciso apontar um `fontfile`, com o `:` da unidade escapado.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,23 +1,39 @@-# Achar duplicados
+# Achar Duplicados
 
-Programa que encontra arquivos iguais pelo conteúdo (não só pelo nome) dentro de uma pasta e subpastas.
+Encontra arquivos com o **conteúdo** idêntico (mesmo com nomes diferentes) e mostra quanto espaço dá para recuperar.
 
 ## Como abrir
 
-Abra o arquivo `achar-duplicados.cmd` na pasta `Organizar` (duplo clique ou Execute).
+Dê um duplo-clique em `Organizar\achar-duplicados.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
 
 ## Passo a passo
 
-1. Cole o caminho da pasta a analisar.
-2. Aguarde a análise (pastas grandes podem demorar).
-3. Se houver duplicados, veja o resumo: grupos de iguais, cópias extras e espaço que dá para recuperar. Até 15 grupos são listados na tela; o primeiro de cada grupo é marcado como manter.
-4. Escolha o que fazer:
-   - **Só listar** (Enter = padrão) — nada é alterado.
-   - **Mover as cópias extras** para a subpasta `_Duplicados`.
-   - **Apagar as cópias extras** — pede confirmação digitando `APAGAR`; sempre mantém 1 original.
+1. Informe a **pasta** (ele olha também as subpastas).
+2. Aguarde a comparação.
+3. Veja os grupos de iguais.
+4. Escolha o que fazer com as cópias extras.
+
+## Opções
+
+| # | Ação | O que faz |
+|---|------|-----------|
+| 1 | Só listar | Não altera nada — é o padrão |
+| 2 | Mover | Move as cópias extras para a subpasta `_Duplicados` |
+| 3 | Apagar | Apaga as cópias extras (exige digitar `APAGAR`) |
+
+## Onde salva
+
+A opção 2 cria a subpasta `_Duplicados` dentro da pasta analisada.
 
 ## Dicas
 
-- Pastas com muitos arquivos iguais de tamanho passam por uma comparação mais profunda — paciência.
-- A opção de apagar é permanente; se tiver dúvida, use só listar ou mover.
-- Pressione Enter no final para fechar a janela.
+- **Sempre mantém uma cópia** de cada arquivo — o primeiro em ordem alfabética, marcado como `[manter]`.
+- É rápido porque compara em três etapas: primeiro o tamanho, depois só as pontas do arquivo, e só então o conteúdo inteiro.
+- Na dúvida, use a opção 2 (mover): dá para conferir antes de apagar de vez.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@+# Analisar Espaço
+
+Responde "o que está enchendo meu HD?" — mostra o espaço de cada unidade, as maiores subpastas e os maiores arquivos.
+
+## Como abrir
+
+Dê um duplo-clique em `Úteis\analisar-espaco.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
+
+## Passo a passo
+
+1. Veja o **espaço de cada unidade** (aparece logo de cara).
+2. Informe a **pasta** para analisar em detalhe.
+3. Veja as maiores subpastas e os maiores arquivos.
+
+
+## Onde salva
+
+Nada é salvo — é só leitura.
+
+## Dicas
+
+- **Nada é alterado**: o programa só lê.
+- Comece pela raiz da unidade (`C:\`) para achar o vilão, depois vá afunilando.
+- Combina bem com o [achar-duplicados](achar-duplicados.md) e a [limpeza-segura](limpeza-segura.md).
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,40 @@+# Backup Espelhado
+
+Copia uma pasta para outro lugar (HD externo, rede) de forma incremental — só o que mudou — usando o robocopy com vários núcleos.
+
+## Como abrir
+
+Dê um duplo-clique em `Úteis\backup-espelhado.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra (o robocopy já vem no Windows)
+
+## Passo a passo
+
+1. Informe a **pasta de origem**.
+2. Informe a **pasta de destino** (é criada se não existir).
+3. Escolha o **modo**.
+4. Veja a **prévia** (simulação — nada é copiado ainda).
+5. Confirme.
+
+## Opções
+
+| # | Modo | Cuidado |
+|---|------|---------|
+| 1 | Cópia | Só adiciona e atualiza. **Nunca apaga nada** no destino |
+| 2 | Espelho | Deixa o destino idêntico à origem — **apaga no destino** o que não existe mais na origem |
+
+## Onde salva
+
+Na pasta de destino. O log fica em `%LOCALAPPDATA%\fast-scripts\logs-backup`.
+
+## Dicas
+
+- A **prévia é uma simulação real**: mostra quantos arquivos seriam copiados e, no modo espelho, exatamente quais seriam apagados.
+- O modo espelho exige digitar **`ESPELHAR`** para confirmar — não dá para apagar sem querer.
+- O log fica **fora do destino** de propósito: senão o próprio espelho apagaria os logs antigos.
+- É incremental: a segunda execução copia só o que mudou, e é muito mais rápida.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,44 @@+# Baixar Vídeos
+
+Baixa vídeos ou só o áudio de YouTube, Instagram, TikTok, X, Vimeo, Facebook e mais de mil outros sites.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\baixar-videos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **yt-dlp** — *o programa baixa sozinho se não existir* (versão oficial mais recente)
+- **FFmpeg** — para juntar vídeo+áudio em alta qualidade e converter áudio
+
+## Passo a passo
+
+1. Cole o **link** (pode colar vários separados por espaço).
+2. Escolha a **qualidade/formato** (1 a 7).
+3. Diga se quer a **playlist inteira** ou só o vídeo.
+4. Confirme a **pasta de saída**.
+
+## Opções
+
+| # | Formato |
+|---|---------|
+| 1 | Melhor qualidade disponível |
+| 2 | 1080p |
+| 3 | 720p |
+| 4 | 480p |
+| 5 | 360p (compacto) |
+| 6 | Só áudio — MP3 |
+| 7 | Só áudio — M4A |
+
+## Onde salva
+
+Na pasta que você escolher (padrão: `Downloads\Baixados`). O nome do arquivo vem do título do vídeo.
+
+## Dicas
+
+- Já baixados são **pulados** automaticamente.
+- Embute capa e informações (título, autor) no arquivo.
+- A cópia do yt-dlp que o programa baixa **se atualiza sozinha** a cada 15 dias.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,27 @@+# Canivete (menu central)
+
+O ponto de entrada da coleção: lista todos os programas por categoria e abre o que você escolher.
+
+## Como abrir
+
+Dê um duplo-clique em `canivete.cmd`, na pasta raiz.
+
+## O que precisa
+
+- Nenhum programa extra
+
+## Passo a passo
+
+1. Abra o `canivete.cmd`.
+2. Veja a lista numerada, agrupada por categoria.
+3. Digite o **número** do programa e tecle Enter — ele abre numa janela própria.
+4. O menu continua aberto: dá para abrir outro em seguida. Digite `0` para sair.
+
+## Dicas
+
+- A lista é montada **na hora**, varrendo as pastas. Se você adicionar um `.cmd` novo em qualquer categoria, ele aparece sozinho no menu — não precisa editar nada.
+- O `canivete.cmd` precisa ficar na **raiz** da coleção (ao lado das pastas `Vídeos`, `Imagens`, `Organizar` e `Úteis`).
+- Crie um atalho dele na Área de Trabalho para ter tudo a um clique.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@+# Compactar / Extrair
+
+Compacta uma pasta num ZIP, ou extrai vários arquivos compactados de uma vez.
+
+## Como abrir
+
+Dê um duplo-clique em `Organizar\compactar-zip.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra (usa o compactador do Windows)
+- *(opcional)* **7-Zip** — `winget install 7zip.7zip` — mais rápido e abre `.rar` e `.7z`
+
+## Passo a passo
+
+1. Escolha **[1] Compactar** ou **[2] Extrair**.
+2. Informe a pasta (ou os arquivos compactados).
+3. Dê o nome do arquivo e confirme a **pasta de saída**.
+
+
+## Onde salva
+
+Na pasta de saída escolhida. Ao extrair, cada arquivo vai para uma subpasta com o nome dele.
+
+## Dicas
+
+- Sem o 7-Zip, só funciona com `.zip`. Com ele, abre também `.rar`, `.7z` e `.tar`.
+- O 7-Zip usa todos os núcleos do processador, então é bem mais rápido em pastas grandes.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,39 @@+# Comprimir PDF
+
+Diminui o tamanho de arquivos PDF, mostrando quanto cada um encolheu.
+
+## Como abrir
+
+Dê um duplo-clique em `Imagens\comprimir-pdf.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **Ghostscript** — `winget install ArtifexSoftware.GhostScript`
+  *(o programa acha o Ghostscript mesmo que ele não esteja no PATH)*
+
+## Passo a passo
+
+1. Informe a **pasta ou um PDF**.
+2. Escolha o **nível de compressão**.
+3. Confirme a **pasta de saída**.
+
+## Opções
+
+| # | Nível | Resolução das imagens |
+|---|-------|-----------------------|
+| 1 | Tela | 72 dpi — menor arquivo |
+| 2 | E-book | 150 dpi — equilibrado |
+| 3 | Impressão | 300 dpi |
+| 4 | Gráfica | máxima qualidade |
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: subpasta `Comprimido`).
+
+## Dicas
+
+- No fim aparece uma tabela **antes → depois** com a porcentagem de redução de cada arquivo.
+- PDFs cheios de imagens encolhem muito; PDFs só de texto quase não mudam.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,45 +1,43 @@ # Converter Imagens
 
-Programa para redimensionar e/ou converter imagens, uma de cada vez ou várias de uma pasta.
+Redimensiona e converte imagens em lote, sempre mantendo a proporção.
 
 ## Como abrir
 
-Abra o arquivo `converter-imagens.cmd` na pasta `Imagens` (duplo clique ou Execute).
+Dê um duplo-clique em `Imagens\converter-imagens.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+- **ImageMagick** — `winget install ImageMagick.ImageMagick`
 
 ## Passo a passo
 
-1. **Pasta ou imagem**  
-   Confirme o caminho sugerido (Enter) ou cole/digite o caminho de uma pasta ou de uma imagem.  
-   Se a pasta não existir, o programa pergunta se quer criá-la.
+1. Informe a **pasta ou uma imagem**.
+2. Escolha o **formato de saída**.
+3. Escolha como informar o **tamanho**.
+4. Confirme a **pasta de saída**.
 
-2. **Formato de saída**  
-   Escolha o número da opção:
-   - manter o formato original (só redimensiona)
-   - JPG
-   - PNG
-   - WebP  
+## Opções
 
-   Enter usa a opção marcada como padrão (a última que você usou, quando houver).
+**Formato:** manter o original · JPG · PNG · WebP
 
-3. **Tamanho**  
-   Escolha:
-   - pixels do maior lado (ex.: 1920)
-   - porcentagem (ex.: 50)
-   - não redimensionar (só muda o formato)  
+**Tamanho**
 
-   Depois informe o valor, se for o caso. Enter pode repetir o valor usado da última vez.
+| # | Modo | Exemplo |
+|---|------|---------|
+| 1 | Pixels do maior lado | `1920` — a imagem cabe em 1920×1920 |
+| 2 | Porcentagem | `50` — metade do tamanho |
+| 3 | Não redimensionar | Só converte o formato |
 
-4. **Aguarde o processamento**  
-   O programa lista cada imagem e mostra se foi convertida, ignorada (já existia no destino) ou se deu erro.
+## Onde salva
 
-5. **Resultado**  
-   Os arquivos ficam na pasta `Convertido`, dentro da pasta de origem (ou ao lado da imagem única). A estrutura de subpastas é preservada.
+Na pasta de saída escolhida (padrão: subpasta `Convertido`), preservando as subpastas.
 
 ## Dicas
 
-- Você pode processar uma pasta inteira (incluindo subpastas) ou só um arquivo.
-- Formatos comuns de entrada são aceitos (fotos e imagens do dia a dia).
-- O programa lembra pasta, formato e tamanho usados nas últimas execuções.
-- Pressione Enter no final para fechar a janela.
+- **Nunca amplia** a imagem — só reduz.
+- Converter PNG com transparência para JPG achata sobre fundo branco automaticamente.
+- É o programa que mais ganha com o processamento paralelo: lotes grandes ficam **várias vezes mais rápidos**.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,38 @@+# Cortar Vídeo
+
+Recorta um trecho do vídeo informando início e fim. No modo rápido é praticamente instantâneo, porque não recodifica nada.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\cortar-video.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
+
+## Passo a passo
+
+1. Informe o **vídeo** (ou uma pasta, e escolha pelo número).
+2. Informe o **início** e o **fim** — aceita `90`, `01:30` ou `00:01:30`.
+3. Escolha o **modo** do corte.
+4. Confirme a **pasta de saída**.
+
+## Opções
+
+| # | Modo | Como funciona |
+|---|------|---------------|
+| 1 | Rápido | Não recodifica — **instantâneo**. Corta no quadro-chave mais próximo (pode variar alguns décimos) |
+| 2 | Exato | Recodifica para cortar no ponto exato. Demora mais |
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: subpasta `Cortado`). O arquivo sai como `<nome>-corte`.
+
+## Dicas
+
+- Use o **rápido** em 99% dos casos: um vídeo de 1 hora é cortado em menos de um segundo.
+- Só use o **exato** se precisar do frame exato (ex.: sincronizar com áudio).
+- Deixe o fim em branco para cortar até o final do vídeo.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,35 +1,32 @@ # Criar GIF / Slideshow
 
-Programa para transformar vídeo em GIF, ou uma pasta de fotos em um vídeo (slideshow).
+Dois usos: transformar um vídeo em GIF, ou transformar uma pasta de fotos num vídeo de slideshow.
 
 ## Como abrir
 
-Abra o arquivo `criar-gif.cmd` na pasta `Vídeos` (duplo clique ou Execute).
+Dê um duplo-clique em `Vídeos\criar-gif.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+## O que precisa
 
-## O que escolher
+- **FFmpeg** — `winget install Gyan.FFmpeg`
 
-1. **Vídeo → GIF**
-2. **Fotos (pasta) → vídeo (slideshow)**
+## Passo a passo
 
----
+1. Escolha **[1] Vídeo → GIF** ou **[2] Fotos → vídeo**.
+2. **GIF:** informe pasta/vídeo, a largura (padrão 480 px) e os quadros por segundo (padrão 12).
+3. **Slideshow:** informe a pasta das fotos, quantos segundos cada foto aparece e o nome do vídeo.
+4. Confirme a **pasta de saída**.
 
-### Vídeo → GIF
 
-1. Cole o caminho de um vídeo ou de uma pasta com vídeos.
-2. Informe a largura do GIF em pixels (Enter = 480).
-3. Informe os quadros por segundo (Enter = 12).
-4. Os GIFs ficam na pasta `GIF`, ao lado dos vídeos. Arquivos que já existem são ignorados.
+## Onde salva
 
-### Fotos → slideshow
-
-1. Cole o caminho da pasta com as fotos.
-2. Informe quantos segundos cada foto aparece (Enter = 2).
-3. Digite o nome do vídeo (Enter = `slideshow.mp4`).
-4. O arquivo é criado **dentro da mesma pasta**. A ordem das fotos é alfabética.
+Na pasta de saída escolhida (padrão: subpasta `GIF` para os GIFs; a própria pasta das fotos para o slideshow).
 
 ## Dicas
 
-- GIFs muito longos ou com muita resolução ficam grandes; use largura e FPS menores se precisar.
-- Pressione Enter no final para fechar a janela.
+- O GIF usa paleta otimizada (duas passagens), o que deixa a imagem bem melhor que um GIF comum.
+- GIF é pesado por natureza: largura 480 e 12 fps já dão um bom equilíbrio.
+- No slideshow, fotos em pé e deitadas são ajustadas para 1280×720 com bordas.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,34 +1,37 @@ # Extrair Áudio
 
-Programa para tirar o áudio de vídeos, um de cada vez ou vários de uma pasta.
+Tira a trilha de áudio de vídeos e salva como MP3, M4A ou WAV.
 
 ## Como abrir
 
-Abra o arquivo `extrair-audio.cmd` na pasta `Vídeos` (duplo clique ou Execute).
+Dê um duplo-clique em `Vídeos\extrair-audio.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
 
 ## Passo a passo
 
-1. **Pasta ou vídeo**  
-   Confirme o caminho sugerido (Enter) ou cole/digite uma pasta ou um arquivo de vídeo.
+1. Informe a **pasta ou um vídeo**.
+2. Escolha o **formato** do áudio.
+3. Confirme a **pasta de saída**.
 
-2. **Formato do áudio**  
-   Escolha:
-   - MP3 — compatível com tudo  
-   - M4A — bom equilíbrio qualidade × tamanho  
-   - WAV — sem compressão (arquivo maior)  
+## Opções
 
-   Enter usa o formato padrão (o último usado, quando houver).
+| # | Formato | Observação |
+|---|---------|------------|
+| 1 | MP3 (192 kbps) | Compatível com tudo |
+| 2 | M4A / AAC (192 kbps) | Melhor qualidade no mesmo tamanho |
+| 3 | WAV | Sem compressão, arquivo grande |
 
-3. **Aguarde o processamento**  
-   Cada vídeo é listado; arquivos que já existem no destino são ignorados.
+## Onde salva
 
-4. **Resultado**  
-   Os áudios ficam na pasta `Áudio`, dentro da pasta de origem. Subpastas são preservadas.
+Na pasta de saída escolhida (padrão: subpasta `Áudio`), preservando as subpastas.
 
 ## Dicas
 
-- O programa lembra a última pasta e o formato usados.
-- Se um vídeo não tiver faixa de áudio, a extração pode falhar.
-- Pressione Enter no final para fechar a janela.
+- **Muito rápido quando dá para copiar:** se o áudio do vídeo já for do formato pedido (por exemplo M4A de um MP4), ele **copia sem recodificar** — é quase instantâneo. O programa avisa na tela quantos entraram nesse caso.
+- Processa vários vídeos ao mesmo tempo.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,35 +1,32 @@ # Imagens e PDF
 
-Programa para juntar imagens em um PDF, ou transformar páginas de PDF em imagens.
+Vai nos dois sentidos: junta várias imagens num único PDF, ou transforma cada página de um PDF numa imagem.
 
 ## Como abrir
 
-Abra o arquivo `imagens-e-pdf.cmd` na pasta `Imagens` (duplo clique ou Execute).
+Dê um duplo-clique em `Imagens\imagens-e-pdf.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+## O que precisa
 
-## O que escolher
+- **ImageMagick** — `winget install ImageMagick.ImageMagick`
+- **Ghostscript** (só para PDF → imagens) — `winget install ArtifexSoftware.GhostScript`
 
-1. **Imagens → PDF** — junta as imagens de uma pasta em um único PDF.
-2. **PDF → Imagens** — salva cada página do PDF como imagem.
+## Passo a passo
 
----
+1. Escolha **[1] Imagens → PDF** ou **[2] PDF → Imagens**.
+2. Informe a pasta (ou o PDF).
+3. Para PDF → imagens, escolha PNG ou JPG e a qualidade em DPI.
+4. Confirme a **pasta de saída**.
 
-### Imagens → PDF
 
-1. Cole o caminho da pasta com as imagens.
-2. Confira quantas imagens foram encontradas (ordem alfabética).
-3. Digite o nome do PDF (Enter = `saida.pdf`).
-4. O arquivo é criado **dentro da mesma pasta**.
+## Onde salva
 
-### PDF → Imagens
-
-1. Cole o caminho de um PDF ou de uma pasta com PDFs.
-2. Escolha o formato: PNG (Enter = padrão) ou JPG.
-3. Informe a qualidade em DPI (Enter = 150; 300 = alta).
-4. As imagens vão para uma pasta com o nome do PDF + `-imagens` (ex.: `documento-imagens`).
+O PDF vai para a pasta escolhida. As imagens vão para uma subpasta `<nome do pdf>-imagens`.
 
 ## Dicas
 
-- Nas imagens → PDF, a ordem das páginas segue o nome dos arquivos (A–Z).
-- Pressione Enter no final para fechar a janela.
+- As imagens entram no PDF em **ordem alfabética** — numere os nomes se a ordem importa.
+- 150 DPI serve para ler na tela; use 300 DPI se for imprimir.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,30 @@+# Info de Mídia
+
+Mostra uma tabela com codec, resolução, duração, bitrate e tamanho de todos os vídeos e áudios de uma pasta — e salva tudo numa planilha.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\info-midia.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **FFmpeg / FFprobe** — `winget install Gyan.FFmpeg`
+
+## Passo a passo
+
+1. Informe a **pasta ou um arquivo**.
+2. Ele lê tudo em paralelo e mostra a tabela.
+3. Confirme a **pasta de saída** para salvar a planilha.
+
+
+## Onde salva
+
+Um arquivo `info-midia_<data>.csv` na pasta de saída escolhida (separado por `;`, abre direto no Excel).
+
+## Dicas
+
+- Ótimo para descobrir **por que um vídeo está pesado** antes de transcodificar.
+- Também serve para conferir se um lote saiu na resolução certa.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@+# Juntar Vídeos
+
+Emenda vários vídeos de uma pasta num arquivo só, na ordem alfabética dos nomes.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\juntar-videos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
+
+## Passo a passo
+
+1. Informe a **pasta** com os vídeos.
+2. Confira a **ordem** mostrada na tela e confirme.
+3. Escolha o **modo** (rápido ou seguro).
+4. Dê o **nome** do arquivo final e confirme a pasta de saída.
+
+## Opções
+
+| # | Modo | Quando usar |
+|---|------|-------------|
+| 1 | Rápido | Vídeos do mesmo tipo/codec — não recodifica, é quase instantâneo |
+| 2 | Seguro | Vídeos diferentes entre si — recodifica tudo para um padrão. Sempre funciona |
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: a própria pasta dos vídeos).
+
+## Dicas
+
+- Para controlar a ordem, renomeie com números na frente: `01-intro.mp4`, `02-meio.mp4`... (o [limpar-nomes](limpar-nomes.md) e o [renomear-em-lote](renomear-em-lote.md) ajudam).
+- Se o modo rápido falhar, é sinal de que os vídeos são diferentes entre si: use o modo seguro.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,31 @@+# Limpar Metadados
+
+Remove os dados escondidos dos arquivos — localização GPS, modelo da câmera, data, programa usado. Importante antes de publicar fotos na internet.
+
+## Como abrir
+
+Dê um duplo-clique em `Organizar\limpar-metadados.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **ImageMagick** — `winget install ImageMagick.ImageMagick`
+- *(opcional)* **FFmpeg** — para limpar também os vídeos
+
+## Passo a passo
+
+1. Informe a **pasta ou um arquivo**.
+2. Veja a **prévia** do que existe hoje numa amostra.
+3. Confirme a **pasta de saída**.
+
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: subpasta `SemMetadados`).
+
+## Dicas
+
+- **Os originais não são alterados** — as cópias limpas vão para a pasta de saída.
+- Fotos de celular costumam carregar a **localização exata** de onde foram tiradas. Vale limpar antes de publicar.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@+# Limpar Nomes
+
+Ajeita os nomes dos arquivos para uso na web e em uploads: tira acentos, troca espaços por hífen e deixa tudo em minúsculas.
+
+## Como abrir
+
+Dê um duplo-clique em `Organizar\limpar-nomes.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
+
+## Passo a passo
+
+1. Informe a **pasta**.
+2. **Confira a prévia** (nome antigo → nome novo).
+3. Confirme.
+
+## Exemplo
+
+| Antes | Depois |
+|-------|--------|
+| `Relatório Final (versão 2).pdf` | `relatorio-final-versao-2.pdf` |
+| `Foto  Açaí — Praia.JPG` | `foto-acai-praia.jpg` |
+
+## Onde salva
+
+Renomeia na própria pasta.
+
+## Dicas
+
+- Nomes que ficariam repetidos ganham `-1`, `-2`... — nada é sobrescrito.
+- Arquivos que já estão com o nome limpo são ignorados.
+- Útil antes de subir arquivos para um site, servidor Linux ou repositório.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,21 +1,38 @@-# Limpeza segura
+# Limpeza Segura
 
-Programa que limpa arquivos temporários do sistema e, se quiser, esvazia a Lixeira — sempre com prévia antes de apagar.
+Libera espaço apagando arquivos temporários e esvaziando a lixeira — sempre mostrando antes quanto dá para recuperar.
 
 ## Como abrir
 
-Abra o arquivo `limpeza-segura.cmd` na pasta `Úteis` (duplo clique ou Execute).
+Dê um duplo-clique em `Úteis\limpeza-segura.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
 
 ## Passo a passo
 
-1. Aguarde a análise. Nada é apagado ainda.
-2. Veja a prévia: temporários do usuário, do Windows e cache de atualizações, com tamanho aproximado e total recuperável.
-3. Confirme com `s` (ou `sim`) para limpar esses temporários. Qualquer outra resposta pula essa etapa. Arquivos em uso são pulados automaticamente.
-4. Em seguida, o programa pergunta se quer **esvaziar a Lixeira** também (`s`/`sim` ou não).
-5. Ao terminar, veja quanto espaço foi liberado (quando a limpeza rodou).
+1. O programa **pede permissão de administrador** (sem ela, os temporários do Windows não podem ser lidos nem limpos).
+2. Veja a **prévia** com o tamanho de cada local.
+3. Confirme se quer limpar.
+4. Responda se quer esvaziar a **lixeira** também.
+
+## O que é limpo
+
+- Temporários do usuário (`%TEMP%`)
+- Temporários do Windows (`C:\Windows\Temp`)
+- Cache de downloads do Windows Update
+- Lixeira (opcional, pergunta separada)
+
+## Onde salva
+
+Nada é salvo — só apaga.
 
 ## Dicas
 
-- A limpeza é só de pastas temporárias/cache listadas — não mexe em Documentos, Downloads, etc.
-- Esvaziar a Lixeira é permanente; confirme só se tiver certeza.
-- Pressione Enter no final para fechar a janela.
+- **Nada é apagado sem você confirmar.**
+- Arquivos em uso são pulados automaticamente.
+- De propósito **não** mexe no Prefetch: apagá-lo não traz ganho e pode deixar os programas abrindo mais devagar.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,42 @@+# Marca d'água em Vídeo
+
+Coloca um texto ou um logo por cima dos vídeos, em lote.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\marca-dagua-video.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
+- *(opcional)* placa **NVIDIA** — é usada automaticamente se existir
+
+## Passo a passo
+
+1. Informe a **pasta ou um vídeo**.
+2. Escolha **texto** ou **logo** (PNG).
+3. Escolha a **posição** (5 opções).
+4. Confirme a **pasta de saída**.
+
+## Opções
+
+| # | Posição |
+|---|---------|
+| 1 | Inferior direita |
+| 2 | Inferior esquerda |
+| 3 | Superior direita |
+| 4 | Superior esquerda |
+| 5 | Centro |
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: subpasta `ComMarca`). Sai sempre em MP4.
+
+## Dicas
+
+- O **áudio é copiado sem recodificar**, então não perde qualidade nem tempo.
+- Use um PNG com fundo transparente para o logo ficar bonito.
+- O tamanho do logo é em % da largura do vídeo (padrão 20%), então fica proporcional em qualquer resolução.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,32 +1,31 @@ # Marca d'água
 
-Programa para aplicar texto ou logo em imagens, uma de cada vez ou várias de uma pasta.
+Aplica um texto ou um logo sobre as imagens, em lote.
 
 ## Como abrir
 
-Abra o arquivo `marca-dagua.cmd` na pasta `Imagens` (duplo clique ou Execute).
+Dê um duplo-clique em `Imagens\marca-dagua.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+- **ImageMagick** — `winget install ImageMagick.ImageMagick`
 
 ## Passo a passo
 
-1. **Pasta ou imagem**  
-   Confirme o caminho sugerido (Enter) ou cole/digite uma pasta ou uma imagem.
+1. Informe a **pasta ou uma imagem**.
+2. Escolha **texto** ou **logo** (PNG).
+3. Escolha a **posição** (5 opções).
+4. Confirme a **pasta de saída**.
 
-2. **Tipo de marca**  
-   - **Texto** — digite o texto que aparecerá na foto.  
-   - **Logo** — informe o caminho de um PNG (de preferência com fundo transparente) e, se quiser, o tamanho em % da largura (Enter = 20).
 
-3. **Posição**  
-   Escolha: inferior direita (padrão), inferior esquerda, superior direita, superior esquerda ou centro.
-
-4. **Aguarde o processamento**  
-   Cada imagem é listada; as que já existem no destino são ignoradas.
+## Onde salva
 
-5. **Resultado**  
-   As fotos ficam na pasta `ComMarca`, dentro da pasta de origem. Subpastas são preservadas.
+Na pasta de saída escolhida (padrão: subpasta `ComMarca`), preservando as subpastas.
 
 ## Dicas
 
-- O programa lembra a última pasta e opções usadas.
-- Pressione Enter no final para fechar a janela.
+- O tamanho do texto e do logo é **proporcional à imagem**, então fica igual em fotos grandes e pequenas.
+- Use PNG com fundo transparente para o logo.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,37 @@+# Normalizar Áudio
+
+Deixa todos os arquivos com o mesmo volume percebido, usando o padrão EBU R128 — acaba com aquele vídeo baixo demais e o seguinte estourando.
+
+## Como abrir
+
+Dê um duplo-clique em `Vídeos\normalizar-audio.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
+
+## Passo a passo
+
+1. Informe a **pasta ou um arquivo** (vídeo ou áudio).
+2. Escolha o **volume alvo**.
+3. Confirme a **pasta de saída**.
+
+## Opções
+
+| # | Alvo | Para que serve |
+|---|------|----------------|
+| 1 | -14 LUFS | Streaming / YouTube |
+| 2 | -16 LUFS | Podcast / voz |
+| 3 | -23 LUFS | TV / broadcast |
+
+## Onde salva
+
+Na pasta de saída escolhida (padrão: subpasta `Normalizado`).
+
+## Dicas
+
+- Em vídeos, a **imagem é copiada sem recodificar** — só o áudio é tratado.
+- Processa vários ao mesmo tempo.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,20 +1,33 @@-# Organizar por tipo
+# Organizar por Tipo
 
-Programa que separa os arquivos soltos de uma pasta em subpastas por categoria (imagens, vídeos, áudio, documentos, etc.).
+Pega uma pasta bagunçada e separa os arquivos em subpastas por categoria.
 
 ## Como abrir
 
-Abra o arquivo `organizar-por-tipo.cmd` na pasta `Organizar` (duplo clique ou Execute).
+Dê um duplo-clique em `Organizar\organizar-por-tipo.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
 
 ## Passo a passo
 
-1. Cole o caminho da pasta a organizar.
-2. Veja a prévia: quantos arquivos vão para cada categoria (Imagens, Vídeos, Áudio, Documentos, Compactados, Programas, Outros).
-3. Confirme com `s` (ou `sim`) para mover. Qualquer outra resposta cancela.
-4. Os arquivos são movidos para subpastas com esses nomes, **dentro da mesma pasta**. Se já existir um arquivo com o mesmo nome, um número é acrescentado.
+1. Informe a **pasta**.
+2. Veja a **prévia** com a contagem por categoria.
+3. Confirme.
+
+## Categorias
+
+Imagens · Vídeos · Áudio · Documentos · Compactados · Programas · Outros
+
+## Onde salva
+
+Cria as subpastas dentro da própria pasta e move os arquivos para elas.
 
 ## Dicas
 
-- Só organiza arquivos **soltos** na pasta (não entra em subpastas já existentes).
-- Confira a prévia antes de confirmar.
-- Pressione Enter no final para fechar a janela.
+- Só mexe nos arquivos **soltos no primeiro nível** — não entra nas subpastas que já existem.
+- O que não se encaixa em nenhuma categoria vai para `Outros`.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,19 +1,29 @@-# Relatório do sistema
+# Relatório do Sistema
 
-Programa que mostra um resumo do computador: nome, sistema, processador, memória, vídeo, discos e espaço nas unidades.
+Um retrato do computador: processador, memória, placa de vídeo, discos e espaço livre em cada unidade.
 
 ## Como abrir
 
-Abra o arquivo `relatorio-sistema.cmd` na pasta `Úteis` (duplo clique ou Execute).
+Dê um duplo-clique em `Úteis\relatorio-sistema.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
 
 ## Passo a passo
 
-1. O relatório aparece na tela em seções (computador, processador e memória, vídeo, discos, espaço nas unidades).
-2. No final, escolha se quer **salvar num arquivo .txt** (`s`/`sim` ou não).  
-   Se salvar, o arquivo vai para a **Área de trabalho**, com data e hora no nome.
-3. Pressione Enter para fechar.
+1. Abra — o relatório aparece na hora.
+2. Escolha se quer **salvar num arquivo .txt**.
+
+
+## Onde salva
+
+Se você pedir, salva um `relatorio-sistema_<data>.txt` na Área de Trabalho.
 
 ## Dicas
 
-- Útil para guardar um “retrato” do PC ou enviar para alguém que vá te ajudar.
-- Unidades quase cheias aparecem em destaque na tela.
+- Útil para mandar para o suporte ou guardar a configuração da máquina.
+- As unidades com mais de 90% de uso aparecem em vermelho.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,23 +1,40 @@-# Renomear em lote
+# Renomear em Lote
 
-Programa para renomear vários arquivos de uma pasta de uma vez, ou organizá-los em pastas por data.
+Renomeia vários arquivos de uma vez, de quatro formas diferentes — sempre com prévia antes de mexer em qualquer coisa.
 
 ## Como abrir
 
-Abra o arquivo `renomear-em-lote.cmd` na pasta `Organizar` (duplo clique ou Execute).
+Dê um duplo-clique em `Organizar\renomear-em-lote.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra (o **ImageMagick** é usado, se existir, para ler a data da foto)
 
 ## Passo a passo
 
-1. Cole o caminho da pasta (só arquivos **nessa pasta**, sem subpastas).
-2. Escolha o modo:
-   - **Sequência** — prefixo + número (ex.: Foto_001). Informe prefixo, número inicial e quantos dígitos.
-   - **Data/hora** — nome com data e hora do arquivo (em fotos, tenta usar a data da foto quando disponível).
-   - **Localizar e substituir** — troca um trecho do nome por outro (pode deixar o substituto vazio para remover).
-   - **Mover para pastas por data** — cria pastas `AAAA-MM` e move os arquivos conforme a data.
-3. Veja a prévia na tela e confirme com `s` (ou `sim`). Qualquer outra resposta cancela.
+1. Informe a **pasta**.
+2. Escolha o **modo**.
+3. Responda o que o modo pedir.
+4. **Confira a prévia** e confirme.
+
+## Opções
+
+| # | Modo | Resultado |
+|---|------|-----------|
+| 1 | Sequência | `Foto_001`, `Foto_002`, ... |
+| 2 | Data/hora | `2026-09-19_14-30-05` |
+| 3 | Localizar e substituir | Troca um texto no nome |
+| 4 | Mover para pastas por data | Cria pastas `2026-09`, `2026-08`... |
+
+## Onde salva
+
+Renomeia na própria pasta. No modo 4, move os arquivos para subpastas por mês.
 
 ## Dicas
 
-- Sempre há prévia antes de aplicar — confira antes de confirmar.
-- Se o novo nome já existir, o programa evita conflito acrescentando um número.
-- Pressione Enter no final para fechar a janela.
+- Nos modos 2 e 4 ele tenta ler a **data original da foto** (EXIF); se não achar, usa a data do arquivo.
+- Nomes repetidos ganham `(1)`, `(2)`... automaticamente — nada é sobrescrito.
+- **Sempre mostra a prévia** e pede confirmação antes de renomear.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,39 +1,53 @@-# Transcodificar
+# Transcodificar Vídeos
 
-Programa para converter vídeos, um de cada vez ou vários de uma pasta, com escolha de qualidade.
+Converte vídeos em lote para MP4, escolhendo a qualidade e quem faz o trabalho pesado (placa de vídeo ou processador).
 
 ## Como abrir
 
-Abra o arquivo `transcodificar.cmd` na pasta `Vídeos` (duplo clique ou Execute).
+Dê um duplo-clique em `Vídeos\transcodificar.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
 
-Na primeira vez, se faltar algo necessário no computador, o programa avisa e mostra o que instalar. Depois de instalar, feche e abra de novo.
+## O que precisa
+
+- **FFmpeg** — `winget install Gyan.FFmpeg`
+- *(opcional)* placa **NVIDIA** para o modo GPU
 
 ## Passo a passo
 
-1. **Pasta ou vídeo**  
-   Confirme o caminho sugerido (Enter) ou cole/digite o caminho de uma pasta ou de um arquivo de vídeo.  
-   Se a pasta não existir, o programa pergunta se quer criá-la.
+1. Informe a **pasta ou um vídeo** (ele lembra o último caminho).
+2. Escolha a **qualidade** (1 a 5).
+3. Escolha o **motor** de codificação (velocidade × tamanho).
+4. Confirme a **pasta de saída** (é criada se não existir).
+5. Ele converte vários ao mesmo tempo, com barra de progresso.
 
-2. **Qualidade de saída**  
-   Escolha o número da opção:
-   - Máxima (resolução original)
-   - Alta (1080p)
-   - Média (720p)
-   - Baixa (480p)
-   - Mínima (compacta, para prévia ou envio rápido)  
+## Opções
 
-   Enter usa a opção marcada como padrão (a última que você usou; se for a primeira vez, costuma ser a mínima).
+**Qualidade**
 
-3. **Aguarde o processamento**  
-   O programa lista cada vídeo e mostra se foi convertido, ignorado (já existia no destino) ou se deu erro. Vídeos longos podem demorar.
+| # | Perfil | Resolução | CRF |
+|---|--------|-----------|-----|
+| 1 | Máxima | original | 18 |
+| 2 | Alta | 1080p | 20 |
+| 3 | Média | 720p | 23 |
+| 4 | Baixa | 480p | 26 |
+| 5 | Mínima (compacta) | ~320p | 30 |
 
-4. **Resultado**  
-   Os arquivos ficam na pasta `Transcodificado`, dentro da pasta de origem (ou ao lado do vídeo único), em formato adequado para uso geral. A estrutura de subpastas é preservada.
+**Motor**
+
+| # | Motor | Quando usar |
+|---|-------|-------------|
+| 1 | GPU NVENC | Qualidades altas e vídeos longos — bem mais rápido |
+| 2 | CPU rápido (veryfast) | Melhor para a qualidade mínima e lotes |
+| 3 | CPU qualidade (medium) | Arquivo menor no mesmo nível |
+
+## Onde salva
+
+Na pasta de saída que você escolher (padrão: subpasta `Transcodificado` dentro da origem). A estrutura de subpastas é preservada.
 
 ## Dicas
 
-- Você pode processar uma pasta inteira (incluindo subpastas) ou só um arquivo.
-- Formatos comuns de vídeo são aceitos.
-- Qualidade maior = arquivo maior e conversão mais lenta; qualidade menor = arquivo mais leve.
-- O programa lembra a pasta e a qualidade usadas nas últimas execuções.
-- Pressione Enter no final para fechar a janela.
+- Nunca amplia o vídeo: se a origem for menor que o perfil, mantém o tamanho.
+- Arquivos já convertidos são **pulados** — dá para rodar de novo sem medo.
+- Para a qualidade mínima, o **CPU rápido** costuma bater a GPU (o vídeo é pequeno demais para compensar ligar a placa).
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -1,26 +1,39 @@ # Verificar Discos
 
-Programa para consultar a saúde dos HDs e SSDs do computador. Só lê informações; não apaga nem altera dados.
+Mostra a saúde dos seus HDs e SSDs — temperatura, desgaste, erros — e pode fazer um teste completo de leitura procurando setores defeituosos.
 
 ## Como abrir
 
-Abra o arquivo `verificar-discos.cmd` na pasta `Úteis` (duplo clique ou Execute).
+Dê um duplo-clique em `Úteis\verificar-discos.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
 
-O Windows pode pedir permissão de administrador — aceite para ver o máximo de informações. Sem isso, parte dos dados pode faltar.
+## O que precisa
 
-## Menu
+- Nenhum programa extra (usa o próprio Windows)
+- *(opcional)* **smartmontools** — `winget install smartmontools` — para os detalhes SMART avançados
 
-Depois de abrir, escolha uma opção pelo número:
+## Passo a passo
+
+1. O programa **pede permissão de administrador** (necessária para ler os sensores).
+2. Escolha uma opção do menu.
+3. Ele volta ao menu ao terminar.
+
+## Opções
+
+| # | Opção | Tempo |
+|---|-------|-------|
+| 1 | Diagnóstico de saúde | Segundos |
+| 2 | Teste de superfície | **Horas** — lê o disco inteiro |
+| 3 | Resumo simples | Instantâneo |
 
-1. **Diagnóstico de saúde** — rápido e recomendado no dia a dia. Mostra, para cada disco, se está bem, temperatura, desgaste, horas ligado e erros, quando disponíveis.
-2. **Teste de superfície** — lê o disco inteiro em busca de problemas. Pode levar horas. É seguro (só leitura). Você escolhe qual disco testar; Enter cancela. Use Ctrl+C se quiser interromper no meio.
-3. **Resumo simples** — lista rápida de cada disco com OK ou aviso/falha.
-0. **Sair** — fecha o programa (Enter vazio também sai).
+## Onde salva
 
-Após as opções 1, 2 ou 3, pressione Enter para voltar ao menu.
+Nada é salvo — é só leitura.
 
 ## Dicas
 
-- Comece pelo diagnóstico de saúde (opção 1); use o teste de superfície só se quiser uma verificação mais profunda.
-- O teste de superfície demora muito em discos grandes — deixe o PC ligado e evite desligar no meio.
-- Se algum disco aparecer com aviso ou falha, considere fazer backup e procurar assistência.
+- **Nada é alterado no disco**: mesmo o teste de superfície apenas *lê*.
+- HDs externos por USB às vezes não expõem os dados SMART — é limitação da gaveta, não do programa.
+- O teste de superfície pode ser cancelado a qualquer momento com Ctrl+C.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,39 @@+# Verificar Integridade
+
+Gera uma "impressão digital" de cada arquivo de uma pasta e, depois, confere se algo mudou — detectando corrupção silenciosa e cópias que deram errado.
+
+## Como abrir
+
+Dê um duplo-clique em `Úteis\verificar-integridade.cmd` — ou abra o `canivete.cmd` na raiz e escolha pelo número.
+
+## O que precisa
+
+- Nenhum programa extra
+
+## Passo a passo
+
+1. Escolha **[1] Gerar** o registro ou **[2] Conferir**.
+2. Informe a **pasta**.
+3. Aguarde o cálculo (mostra o progresso).
+
+## O resultado da conferência
+
+| Estado | Significa |
+|--------|-----------|
+| Intactos | O conteúdo continua igual |
+| **ALTERADOS** | O conteúdo mudou — possível corrupção |
+| Sumidos | Estavam no registro e não existem mais |
+| Novos | Apareceram depois do registro |
+
+## Onde salva
+
+Um arquivo `integridade.sha256` dentro da própria pasta analisada.
+
+## Dicas
+
+- Rode o **[1] Gerar** logo depois de fazer um backup, e o **[2] Conferir** meses depois para saber se algum arquivo apodreceu no disco.
+- Combina com o [backup-espelhado](backup-espelhado.md): copie e depois confira se chegou íntegro.
+- Nada é alterado nos seus arquivos — só leitura.
+
+---
+*Desenvolvido por Pablo Murad - 2026*
@@ -0,0 +1,175 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Analisar Espaço - mostra o que está ocupando o disco
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Analisar Espaço' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'AnalisarEspaco'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Analisar Espaço'
+Write-Host '[OK] Não precisa de nenhum programa extra. Só leitura: nada é alterado.' -ForegroundColor Green
+$config = Carregar-Config
+
+Titulo 'Espaço nas unidades'
+Get-Volume | Where-Object { $_.DriveLetter -and $_.Size -gt 0 } | Sort-Object DriveLetter | ForEach-Object {
+    $usado = $_.Size - $_.SizeRemaining
+    $pc = if ($_.Size -gt 0) { [int](($usado / $_.Size) * 100) } else { 0 }
+    $ch = [int]($pc / 5)
+    $cor = if ($pc -ge 90) { 'Red' } elseif ($pc -ge 75) { 'Yellow' } else { 'Gray' }
+    Write-Host ("  {0}:  [{1}] {2,3}%   {3} livres de {4}" -f $_.DriveLetter, (('#'*$ch)+('-'*(20-$ch))), $pc, (Tam $_.SizeRemaining), (Tam $_.Size)) -ForegroundColor $cor
+}
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Downloads' }
+$pasta = $null
+while (-not $pasta) {
+    Titulo 'Pasta para analisar'
+    Write-Host "  $padrao" -ForegroundColor White
+    $r = Read-Host 'Pasta (Enter = confirmar)'
+    $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+    if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x }
+}
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta })
+
+Titulo 'Analisando'
+Write-Host 'Lendo a árvore de pastas (pode demorar em pastas enormes)...' -ForegroundColor Gray
+$todos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -Force -ErrorAction SilentlyContinue)
+if ($todos.Count -eq 0) { Write-Host 'Pasta vazia ou sem acesso.' -ForegroundColor Yellow; Pausar; exit 0 }
+$total = ($todos | Measure-Object -Property Length -Sum).Sum
+
+Titulo 'Maiores subpastas'
+$subs = @(Get-ChildItem -LiteralPath $pasta -Directory -Force -ErrorAction SilentlyContinue)
+$linhas = @()
+foreach ($s in $subs) {
+    $b = (Get-ChildItem -LiteralPath $s.FullName -Recurse -File -Force -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
+    $linhas += [pscustomobject]@{ Nome=$s.Name; Bytes=[int64]$b }
+}
+$naRaiz = ($todos | Where-Object { $_.DirectoryName -eq $pasta } | Measure-Object -Property Length -Sum).Sum
+if ($naRaiz -gt 0) { $linhas += [pscustomobject]@{ Nome='(arquivos soltos aqui)'; Bytes=[int64]$naRaiz } }
+$linhas = $linhas | Sort-Object Bytes -Descending | Select-Object -First 15
+foreach ($l in $linhas) {
+    $pc = if ($total -gt 0) { [int](($l.Bytes / $total) * 100) } else { 0 }
+    $ch = [int]($pc / 5)
+    Write-Host ("  {0,10}  [{1}] {2,3}%  {3}" -f (Tam $l.Bytes), (('#'*$ch)+('-'*(20-$ch))), $pc, $l.Nome) -ForegroundColor Gray
+}
+
+Titulo 'Maiores arquivos'
+$todos | Sort-Object Length -Descending | Select-Object -First 15 | ForEach-Object {
+    Write-Host ("  {0,10}  {1}" -f (Tam $_.Length), $_.FullName.Substring($pasta.Length).TrimStart('\')) -ForegroundColor Gray
+}
+
+Titulo 'Resumo'
+Write-Host ("Pasta:    {0}" -f $pasta)
+Write-Host ("Arquivos: {0:N0}" -f $todos.Count)
+Write-Host ("Tamanho:  {0}" -f (Tam $total)) -ForegroundColor Cyan
+Pausar
@@ -0,0 +1,194 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Backup Espelhado - copia/espelha pastas com robocopy multithread
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Backup Espelhado' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'BackupEspelhado'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Backup Espelhado'
+Write-Host '[OK] Usa o robocopy do próprio Windows (multithread).' -ForegroundColor Green
+$config = Carregar-Config
+
+$origem = $null
+$padrao = if ($config -and $config.UltimaOrigem) { $config.UltimaOrigem } else { Join-Path $env:USERPROFILE 'Documents' }
+while (-not $origem) {
+    Titulo 'Pasta de ORIGEM (o que será copiado)'
+    Write-Host "  $padrao" -ForegroundColor White
+    $r = Read-Host 'Origem (Enter = confirmar)'
+    $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+    if (Test-Path -LiteralPath $x -PathType Container) { $origem = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x }
+}
+$destino = Pedir-Saida $(if ($config -and $config.UltimoDestino) { $config.UltimoDestino } else { 'D:\Backup' })
+if ($destino.TrimEnd('\') -ieq $origem.TrimEnd('\')) { Write-Host '[ERRO] Origem e destino são a mesma pasta.' -ForegroundColor Red; Pausar; exit 1 }
+
+Titulo 'Modo'
+Write-Host '> [1] Cópia  - só adiciona/atualiza; NUNCA apaga nada no destino' -ForegroundColor White
+Write-Host '  [2] Espelho - deixa o destino IDÊNTICO à origem (APAGA no destino o que não existe mais na origem)' -ForegroundColor Yellow
+$m = Read-Host 'Modo (Enter = 1)'
+$espelho = ($m.Trim() -eq '2')
+$modoArg = if ($espelho) { '/MIR' } else { '/E' }
+
+Salvar-Config ([pscustomobject]@{ UltimaOrigem=$origem; UltimoDestino=$destino })
+
+$comuns = @($origem, $destino, '/MT:16', '/R:1', '/W:1', '/NP', '/NDL', '/XJ')
+
+Titulo 'Prévia (simulação - nada é copiado ou apagado)'
+Write-Host 'Analisando as diferenças...' -ForegroundColor Gray
+$previa = & robocopy @comuns $modoArg /L /NJH /NJS 2>&1
+$aCopiar = @($previa | Where-Object { $_ -match '\S' -and $_ -notmatch '^\s*\*.*EXTRA' }).Count
+$aApagar = @($previa | Where-Object { $_ -match '^\s*\*.*EXTRA' }).Count
+Write-Host ''
+Write-Host ("  Serão copiados/atualizados: {0} item(ns)" -f $aCopiar) -ForegroundColor Cyan
+if ($espelho) {
+    Write-Host ("  Serão APAGADOS no destino:  {0} item(ns)" -f $aApagar) -ForegroundColor $(if ($aApagar -gt 0) { 'Red' } else { 'Gray' })
+    if ($aApagar -gt 0) {
+        Write-Host ''
+        Write-Host '  Exemplos do que será apagado no destino:' -ForegroundColor Red
+        $previa | Where-Object { $_ -match '^\s*\*.*EXTRA' } | Select-Object -First 10 | ForEach-Object { Write-Host ("    " + $_.Trim()) -ForegroundColor DarkGray }
+    }
+}
+Write-Host ''
+Write-Host "Origem:  $origem"
+Write-Host "Destino: $destino"
+Write-Host ("Modo:    {0}" -f $(if ($espelho) { 'ESPELHO (apaga no destino)' } else { 'Cópia (não apaga)' })) -ForegroundColor $(if ($espelho) { 'Yellow' } else { 'Gray' })
+Write-Host ''
+if ($espelho -and $aApagar -gt 0) {
+    $conf = Read-Host 'Isto APAGA arquivos no destino. Digite ESPELHAR para confirmar'
+    if ($conf -cne 'ESPELHAR') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
+} else {
+    $conf = Read-Host 'Fazer o backup agora? (s/N)'
+    if ($conf -notmatch '^(s|sim|y|yes)$') { Write-Host 'Cancelado.' -ForegroundColor Yellow; Pausar; exit 0 }
+}
+
+$pastaLog = Join-Path $env:LOCALAPPDATA 'fast-scripts\logs-backup'
+New-Item -ItemType Directory -Path $pastaLog -Force | Out-Null
+$log = Join-Path $pastaLog ("backup_{0}.log" -f (Get-Date -Format 'yyyy-MM-dd_HH-mm'))
+Titulo 'Copiando'
+Write-Host "Log: $log" -ForegroundColor DarkGray
+Write-Host ''
+$t = Measure-Command { & robocopy @comuns $modoArg "/LOG+:$log" /TEE | Out-Null }
+$rc = $LASTEXITCODE
+
+# robocopy: 0-7 = sucesso (8+ = erro real)
+Titulo 'Concluído'
+if ($rc -lt 8) {
+    Write-Host ("[OK] Backup finalizado em {0:hh\:mm\:ss}." -f $t) -ForegroundColor Green
+    Write-Host ("Código do robocopy: {0} (0-7 = sucesso)" -f $rc) -ForegroundColor DarkGray
+} else {
+    Write-Host ("[ERRO] O robocopy retornou {0} - houve falhas. Veja o log." -f $rc) -ForegroundColor Red
+}
+Write-Host "Destino: $destino"
+Write-Host "Log:     $log"
+Pausar
@@ -1,6 +1,7 @@ <# :
 @echo off
-powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+set "SELF=%~f0"
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText($env:SELF,[System.Text.Encoding]::UTF8))"
 exit /b %errorlevel%
 #>
 
@@ -13,6 +14,17 @@ try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
 try { $Host.UI.RawUI.WindowTitle = 'Limpeza segura' } catch {}
 
+# ------------------------------------------------------------
+# Auto-elevacao: sem admin, C:\Windows\Temp e o cache do Windows
+# Update nao podem ser lidos nem limpos (falhava em silencio).
+# ------------------------------------------------------------
+$souAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+if (-not $souAdmin) {
+    Write-Host 'Solicitando privilegios de administrador...' -ForegroundColor Yellow
+    try { Start-Process -FilePath 'cmd.exe' -ArgumentList '/c', "`"$env:SELF`"" -Verb RunAs | Out-Null }
+    catch { Write-Host '[!] Sem elevacao: os temporarios do Windows serao ignorados.' -ForegroundColor Yellow; Start-Sleep -Seconds 2 }
+    exit
+}
 function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
 function Titulo($t) {
     Write-Host ''
@@ -0,0 +1,231 @@+<# :
+@echo off
+powershell -NoLogo -NoProfile -ExecutionPolicy Bypass -Command "Invoke-Expression ([System.IO.File]::ReadAllText('%~f0',[System.Text.Encoding]::UTF8))"
+exit /b %errorlevel%
+#>
+
+# ============================================================
+#  Verificar Integridade - detecta arquivos corrompidos por checksum SHA256
+#  Desenvolvido por Pablo Murad - 2026
+# ============================================================
+
+try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
+try { $Host.UI.RawUI.WindowTitle = 'Verificar Integridade' } catch {}
+function Pausar { Write-Host ''; Write-Host 'Pressione Enter para fechar...' -ForegroundColor DarkGray; [void][System.Console]::ReadLine() }
+function Titulo($t) {
+    Write-Host ''
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+    Write-Host "  $t" -ForegroundColor Cyan
+    Write-Host '============================================================' -ForegroundColor DarkCyan
+}
+function Tam($bytes) {
+    if ($bytes -ge 1GB) { '{0:N2} GB' -f ($bytes/1GB) }
+    elseif ($bytes -ge 1MB) { '{0:N1} MB' -f ($bytes/1MB) }
+    else { '{0:N0} KB' -f ($bytes/1KB) }
+}
+
+$configDir  = Join-Path $env:APPDATA 'VerificarIntegridade'
+$configFile = Join-Path $configDir 'config.json'
+function Carregar-Config { if (Test-Path -LiteralPath $configFile) { try { return Get-Content -LiteralPath $configFile -Raw -Encoding UTF8 | ConvertFrom-Json } catch {} }; return $null }
+function Salvar-Config($o) { try { New-Item -ItemType Directory -Path $configDir -Force | Out-Null; $o | ConvertTo-Json | Set-Content -LiteralPath $configFile -Encoding UTF8 } catch {} }
+
+# Pergunta a pasta de saida: sugere a ultima usada e cria se nao existir
+function Pedir-Saida($padrao) {
+    while ($true) {
+        Titulo 'Pasta de saída'
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = usar esta pasta   |   ou cole/digite outra' -ForegroundColor DarkGray
+        $r = Read-Host 'Pasta de saída'
+        $d = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        try { New-Item -ItemType Directory -Path $d -Force | Out-Null; return (Resolve-Path -LiteralPath $d).Path }
+        catch { Write-Host '[!] Não foi possível criar/usar essa pasta.' -ForegroundColor Yellow }
+    }
+}
+
+function Checar($exe, $nomeAmigavel, $comandoInstalar) {
+    $c = Get-Command $exe -ErrorAction SilentlyContinue
+    if (-not $c) {
+        Write-Host ''
+        Write-Host "[ERRO] $nomeAmigavel não foi encontrado." -ForegroundColor Red
+        Write-Host "Instale com:  $comandoInstalar" -ForegroundColor White
+        Pausar; exit 1
+    }
+    Write-Host ("[OK] {0}: {1}" -f $nomeAmigavel, $c.Source) -ForegroundColor Green
+    return $c
+}
+
+function Abrir($titulo) {
+    Titulo $titulo
+    Write-Host '  Desenvolvido por Pablo Murad - 2026' -ForegroundColor DarkGray
+    Write-Host ''
+    Write-Host 'Verificando o que é necessário...' -ForegroundColor Gray
+}
+
+$extVideo = @('.mp4','.mkv','.avi','.mov','.wmv','.flv','.webm','.m4v','.mpg','.mpeg','.mts','.m2ts','.ts','.3gp','.vob')
+$extAudio = @('.mp3','.m4a','.wav','.flac','.aac','.ogg','.wma','.opus')
+$extImg   = @('.jpg','.jpeg','.png','.webp','.bmp','.tif','.tiff')
+
+# Pede uma PASTA ou um ARQUIVO; devolve @{ Modo; Caminho; Origem }
+function Pedir-Entrada($padrao, $exts, $rotulo) {
+    while ($true) {
+        Titulo $rotulo
+        Write-Host "  $padrao" -ForegroundColor White
+        Write-Host 'Enter = confirmar   |   ou cole/digite uma PASTA ou um ARQUIVO' -ForegroundColor DarkGray
+        $r = Read-Host 'Caminho'
+        $c = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+        if (Test-Path -LiteralPath $c -PathType Leaf) {
+            if ($exts -notcontains [System.IO.Path]::GetExtension($c).ToLowerInvariant()) {
+                Write-Host '[!] Esse arquivo não é de um tipo suportado.' -ForegroundColor Yellow; $padrao = $c; continue
+            }
+            $f = Get-Item -LiteralPath $c
+            return @{ Modo='arquivo'; Caminho=$f.FullName; Origem=$f.DirectoryName }
+        }
+        elseif (Test-Path -LiteralPath $c -PathType Container) {
+            $p = (Resolve-Path -LiteralPath $c).Path
+            return @{ Modo='pasta'; Caminho=$p; Origem=$p }
+        }
+        Write-Host "[!] Caminho não encontrado." -ForegroundColor Yellow
+        $padrao = $c
+    }
+}
+
+function Listar($ent, $exts) {
+    if ($ent.Modo -eq 'arquivo') { return @(Get-Item -LiteralPath $ent.Caminho) }
+    return @(Get-ChildItem -LiteralPath $ent.Caminho -File | Where-Object { $exts -contains $_.Extension.ToLowerInvariant() } | Sort-Object Name)
+}
+
+function Resumo($ok, $ign, $err, $destino) {
+    Titulo 'Concluído'
+    Write-Host "Prontos:   $ok" -ForegroundColor Green
+    if ($ign -gt 0) { Write-Host "Ignorados: $ign" -ForegroundColor Yellow }
+    Write-Host "Erros:     $err" -ForegroundColor $(if ($err -gt 0) { 'Red' } else { 'Gray' })
+    if ($destino) { Write-Host "Destino:   $destino" }
+    Pausar
+}
+
+function Relatar($resultados, $tarefas, [ref]$ok, [ref]$err) {
+    foreach ($x in $resultados) {
+        $alvo = $tarefas[$x.Indice].Alvo
+        if ($x.Codigo -eq 0 -and (Test-Path -LiteralPath $alvo)) { $ok.Value++ }
+        else {
+            $err.Value++
+            Write-Host ("  [ERRO] {0}" -f $x.Rotulo) -ForegroundColor Red
+            $motivo = ($x.Erro -split "`n" | Where-Object { $_.Trim() } | Select-Object -Last 1)
+            if ($motivo) { Write-Host ("         {0}" -f $motivo.Trim()) -ForegroundColor DarkGray }
+            if (Test-Path -LiteralPath $alvo) { Remove-Item -LiteralPath $alvo -Force -ErrorAction SilentlyContinue }
+        }
+    }
+}
+
+Abrir 'Verificar Integridade'
+Write-Host '[OK] Não precisa de nenhum programa extra.' -ForegroundColor Green
+$config = Carregar-Config
+
+Write-Host ''
+Write-Host 'Gera uma "impressão digital" de cada arquivo e confere depois,' -ForegroundColor DarkGray
+Write-Host 'detectando arquivos corrompidos silenciosamente ou cópias com erro.' -ForegroundColor DarkGray
+
+Titulo 'O que você quer fazer?'
+Write-Host '  [1] Gerar o registro de integridade da pasta' -ForegroundColor White
+Write-Host '  [2] Conferir a pasta contra um registro existente' -ForegroundColor White
+$acao = $null
+while (-not $acao) { $a = Read-Host 'Opção (1-2)'; switch ($a.Trim()) { '1' { $acao='gerar' } '2' { $acao='conferir' } default { Write-Host '[!] Digite 1 ou 2.' -ForegroundColor Yellow } } }
+
+$padrao = if ($config -and $config.UltimaEntrada) { $config.UltimaEntrada } else { Join-Path $env:USERPROFILE 'Documents' }
+$pasta = $null
+while (-not $pasta) {
+    Titulo 'Pasta'
+    Write-Host "  $padrao" -ForegroundColor White
+    $r = Read-Host 'Pasta (Enter = confirmar)'
+    $x = if ([string]::IsNullOrWhiteSpace($r)) { $padrao } else { $r.Trim().Trim('"') }
+    if (Test-Path -LiteralPath $x -PathType Container) { $pasta = (Resolve-Path -LiteralPath $x).Path } else { Write-Host '[!] Pasta não encontrada.' -ForegroundColor Yellow; $padrao = $x }
+}
+Salvar-Config ([pscustomobject]@{ UltimaEntrada=$pasta })
+$registro = Join-Path $pasta 'integridade.sha256'
+
+$arquivos = @(Get-ChildItem -LiteralPath $pasta -Recurse -File -Force -ErrorAction SilentlyContinue | Where-Object { $_.Name -ne 'integridade.sha256' })
+if ($arquivos.Count -eq 0) { Write-Host 'Nenhum arquivo nessa pasta.' -ForegroundColor Yellow; Pausar; exit 0 }
+
+function Hash-SHA256([string]$caminho) {
+    $sha = [System.Security.Cryptography.SHA256]::Create()
+    $fs  = [System.IO.File]::OpenRead($caminho)
+    try { return ([BitConverter]::ToString($sha.ComputeHash($fs)) -replace '-', '') }
+    finally { $fs.Close(); $sha.Dispose() }
+}
+# $base e' passado explicitamente: dentro da funcao o escopo do script
+# nao e' confiavel quando o arquivo roda via Invoke-Expression.
+function Calcular($lista, $rotulo, $base) {
+    $mapa = @{}
+    $i = 0
+    $falhas = 0
+    foreach ($f in $lista) {
+        $i++
+        $pc = [int](($i / $lista.Count) * 100); $ch = [int]($pc / 5)
+        Write-Host ("`r  {0} [{1}] {2,3}%  {3}/{4}   " -f $rotulo, (('#'*$ch)+('-'*(20-$ch))), $pc, $i, $lista.Count) -NoNewline -ForegroundColor Cyan
+        $rel = $f.FullName
+        if ($base -and $f.FullName.StartsWith($base, [System.StringComparison]::OrdinalIgnoreCase)) {
+            $rel = $f.FullName.Substring($base.Length).TrimStart('\')
+        }
+        try { $mapa[$rel] = Hash-SHA256 $f.FullName }
+        catch {
+            $falhas++
+            Write-Host ("`r  [ERRO] {0}: {1}" -f $f.Name, $_.Exception.Message) -ForegroundColor Red
+        }
+    }
+    Write-Host ''
+    if ($falhas -gt 0) { Write-Host ("  [!] {0} arquivo(s) nao puderam ser lidos." -f $falhas) -ForegroundColor Yellow }
+    return $mapa
+}
+
+if ($acao -eq 'gerar') {
+    Titulo 'Gerando'
+    $mapa = Calcular $arquivos 'Calculando' $pasta
+    $sb = New-Object System.Text.StringBuilder
+    foreach ($k in ($mapa.Keys | Sort-Object)) { [void]$sb.AppendLine("$($mapa[$k]) *$k") }
+    [System.IO.File]::WriteAllText($registro, $sb.ToString(), (New-Object System.Text.UTF8Encoding($false)))
+    Titulo 'Concluído'
+    Write-Host ("[OK] Registro criado com {0} arquivo(s)." -f $mapa.Count) -ForegroundColor Green
+    Write-Host "Arquivo: $registro"
+    Write-Host 'Guarde-o junto com os dados: rode a opção [2] depois para conferir.' -ForegroundColor DarkGray
+    Pausar
+}
+else {
+    if (-not (Test-Path -LiteralPath $registro)) {
+        Write-Host ''
+        Write-Host "[ERRO] Não achei o registro: $registro" -ForegroundColor Red
+        Write-Host 'Rode primeiro a opção [1] nessa pasta.' -ForegroundColor Yellow
+        Pausar; exit 1
+    }
+    $antigo = @{}
+    foreach ($l in [System.IO.File]::ReadAllLines($registro, [System.Text.Encoding]::UTF8)) {
+        $m = [regex]::Match($l, '^([0-9A-Fa-f]{64}) \*(.+)$')
+        if ($m.Success) { $antigo[$m.Groups[2].Value] = $m.Groups[1].Value.ToUpperInvariant() }
+    }
+    Titulo 'Conferindo'
+    $novo = Calcular $arquivos 'Conferindo' $pasta
+
+    $iguais=0; $alterados=@(); $sumidos=@(); $novos=@()
+    foreach ($k in $antigo.Keys) {
+        if (-not $novo.ContainsKey($k)) { $sumidos += $k }
+        elseif ($novo[$k] -ne $antigo[$k]) { $alterados += $k }
+        else { $iguais++ }
+    }
+    foreach ($k in $novo.Keys) { if (-not $antigo.ContainsKey($k)) { $novos += $k } }
+
+    Titulo 'Resultado'
+    Write-Host ("  Intactos:  {0}" -f $iguais) -ForegroundColor Green
+    Write-Host ("  ALTERADOS: {0}" -f $alterados.Count) -ForegroundColor $(if ($alterados.Count) { 'Red' } else { 'Gray' })
+    Write-Host ("  Sumidos:   {0}" -f $sumidos.Count) -ForegroundColor $(if ($sumidos.Count) { 'Yellow' } else { 'Gray' })
+    Write-Host ("  Novos:     {0}" -f $novos.Count) -ForegroundColor Gray
+    if ($alterados.Count) {
+        Write-Host ''
+        Write-Host '  Arquivos com conteúdo diferente (possível corrupção):' -ForegroundColor Red
+        $alterados | Select-Object -First 20 | ForEach-Object { Write-Host "    $_" -ForegroundColor DarkGray }
+    }
+    if ($sumidos.Count) {
+        Write-Host ''
+        Write-Host '  Arquivos que sumiram:' -ForegroundColor Yellow
+        $sumidos | Select-Object -First 20 | ForEach-Object { Write-Host "    $_" -ForegroundColor DarkGray }
+    }
+    Pausar
+}