#requires -Version 5.1 $ErrorActionPreference = "Stop" # Корень проекта определяется независимо от текущей папки PowerShell. $ProjectRoot = Split-Path -Parent $PSScriptRoot $ToolsDir = Join-Path $ProjectRoot "tools" $OutputDir = Join-Path $ProjectRoot "output" if (-not (Test-Path $OutputDir)) { New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null } function Find-Tool { param( [Parameter(Mandatory = $true)] [string]$Name, [string[]]$LocalNames = @() ) foreach ($localName in $LocalNames) { $localPath = Join-Path $ToolsDir $localName if (Test-Path $localPath -PathType Leaf) { return (Resolve-Path $localPath).Path } } $command = Get-Command $Name -ErrorAction SilentlyContinue if ($command) { return $command.Source } return $null } function Require-Tool { param( [Parameter(Mandatory = $true)] [string]$Name, [string[]]$LocalNames = @(), [string]$InstallHint = "" ) $tool = Find-Tool -Name $Name -LocalNames $LocalNames if (-not $tool) { Write-Host "" Write-Host "ОШИБКА: не найдена зависимость '$Name'." -ForegroundColor Red Write-Host "Проверены:" Write-Host " 1. Папка: $ToolsDir" Write-Host " 2. Системная переменная PATH" if ($InstallHint) { Write-Host "" Write-Host $InstallHint } Write-Host "" Read-Host "Нажмите Enter для выхода" exit 1 } return $tool } $YtDlp = Require-Tool ` -Name "yt-dlp" ` -LocalNames @("yt-dlp.exe", "yt-dlp.cmd", "yt-dlp.ps1") ` -InstallHint "Скачать yt-dlp: https://github.com/yt-dlp/yt-dlp/releases/latest" $Ffmpeg = Require-Tool ` -Name "ffmpeg" ` -LocalNames @("ffmpeg.exe") ` -InstallHint "Скачать FFmpeg: https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z" $VotCli = Require-Tool ` -Name "vot-cli" ` -LocalNames @("vot-cli.cmd", "vot-cli.exe", "vot-cli.ps1") ` -InstallHint "Установка vot-cli: npm install -g vot-cli" $Node = Require-Tool ` -Name "node" ` -LocalNames @("node.exe") ` -InstallHint "Установите Node.js: https://nodejs.org/" $Npm = Require-Tool ` -Name "npm" ` -LocalNames @("npm.cmd", "npm.exe", "npm.ps1") ` -InstallHint "npm устанавливается вместе с Node.js." $FfmpegDir = Split-Path -Parent $Ffmpeg function Get-SafeFileName { param([string]$Name) $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() foreach ($char in $invalidChars) { $Name = $Name.Replace([string]$char, "_") } return $Name.Trim() } function Process-Video { param( [Parameter(Mandatory = $true)] [string]$VideoLink, [double]$OriginalSoundRatio = 0.2 ) $TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("youtube-tools-" + [Guid]::NewGuid().ToString("N")) $TempVideoDir = Join-Path $TempRoot "video" $TempAudioDir = Join-Path $TempRoot "audio" New-Item -ItemType Directory -Path $TempVideoDir -Force | Out-Null New-Item -ItemType Directory -Path $TempAudioDir -Force | Out-Null try { Write-Host "" Write-Host "Получение названия видео..." $Title = (& $YtDlp "--get-title" "--no-playlist" $VideoLink | Select-Object -First 1) if ([string]::IsNullOrWhiteSpace($Title)) { $Title = "YouTubeVideo" } $SafeTitle = Get-SafeFileName $Title $TempVideoTemplate = Join-Path $TempVideoDir "%(title)s.%(ext)s" Write-Host "Скачивание исходного видео..." & $YtDlp ` "--no-playlist" ` "-f" "bv*+ba/b" ` "--ffmpeg-location" $FfmpegDir ` "-o" $TempVideoTemplate ` $VideoLink if ($LASTEXITCODE -ne 0) { throw "yt-dlp завершил работу с кодом $LASTEXITCODE." } $VideoFile = Get-ChildItem -Path $TempVideoDir -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $VideoFile) { throw "Не найден скачанный видеофайл." } Write-Host "Получение переведённой аудиодорожки..." & $VotCli $VideoLink "--output" $TempAudioDir if ($LASTEXITCODE -ne 0) { throw "vot-cli завершил работу с кодом $LASTEXITCODE." } $AudioFile = Get-ChildItem -Path $TempAudioDir -File | Sort-Object LastWriteTime -Descending | Select-Object -First 1 if (-not $AudioFile) { throw "vot-cli не создал аудиофайл." } $OutputFile = Join-Path $OutputDir ($SafeTitle + "_перевод.mp4") Write-Host "Смешивание оригинального звука и перевода..." $Filter = "[0:a]volume=$OriginalSoundRatio[original];[original][1:a]amix=inputs=2:duration=longest[audio_out]" & $Ffmpeg ` "-hide_banner" ` "-i" $VideoFile.FullName ` "-i" $AudioFile.FullName ` "-c:v" "copy" ` "-c:a" "aac" ` "-b:a" "128k" ` "-filter_complex" $Filter ` "-map" "0:v:0" ` "-map" "[audio_out]" ` "-movflags" "+faststart" ` "-y" ` $OutputFile if ($LASTEXITCODE -ne 0) { throw "FFmpeg завершил работу с кодом $LASTEXITCODE." } Write-Host "" Write-Host "Готово: $OutputFile" -ForegroundColor Green } finally { if (Test-Path $TempRoot) { Remove-Item -Path $TempRoot -Recurse -Force -ErrorAction SilentlyContinue } } } Write-Host "" Write-Host "============================================================" Write-Host " СКАЧИВАНИЕ YOUTUBE С ЗАКАДРОВЫМ ПЕРЕВОДОМ" Write-Host "============================================================" Write-Host "Результат: $OutputDir" Write-Host "" $VideoLinksInput = Read-Host "Введите ссылку или несколько ссылок через запятую" if ([string]::IsNullOrWhiteSpace($VideoLinksInput)) { Write-Host "" Write-Host "ОШИБКА: ссылки не введены." -ForegroundColor Red Read-Host "Нажмите Enter для выхода" exit 1 } $VideoLinks = $VideoLinksInput -split "," | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } foreach ($VideoLink in $VideoLinks) { Write-Host "" Write-Host "------------------------------------------------------------" Write-Host "Обработка: $VideoLink" Write-Host "------------------------------------------------------------" try { Process-Video -VideoLink $VideoLink -OriginalSoundRatio 0.2 } catch { Write-Host "" Write-Host "ОШИБКА: $($_.Exception.Message)" -ForegroundColor Red } } Write-Host "" Write-Host "Обработка завершена." Write-Host "Результаты находятся в: $OutputDir" Write-Host "" Read-Host "Нажмите Enter для выхода"