This commit is contained in:
@@ -6,3 +6,7 @@ worker/__pycache__/
|
||||
*.pyc
|
||||
node_modules/
|
||||
dist/
|
||||
.venv/
|
||||
.tools/
|
||||
.local/
|
||||
*.log
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
- how it was verified
|
||||
- what is still risky or incomplete
|
||||
- If a push fails or a change remains local-only, that must be written here explicitly.
|
||||
- Active planning and handoff notes should be written in Korean.
|
||||
- After each meaningful completed task:
|
||||
- update this file in context
|
||||
- push the latest git state when operationally possible
|
||||
- Local git credentials used for push automation must stay in an ignored local-only file and must never be committed.
|
||||
|
||||
## Current State At A Glance
|
||||
- Project: `ai-media-hub`
|
||||
@@ -35,6 +40,7 @@
|
||||
- Search preview delivery is now moving away from persistent on-disk preview caching toward live proxy / live transcode behavior, with Google Video preview reuse added to result cards and modal playback.
|
||||
- Search breadth and Gemini review budget were widened again because the latest user feedback still reported a thin visible result count even after smarter filtering.
|
||||
- The codebase is now back on the broader-search / modal-fitting direction associated with `5ca7aef`, with an added Gemini deadline guard to reduce reverse-proxy `504` risk.
|
||||
- Windows 11 local development now has a repo-local PowerShell bootstrap / run / self-test / push workflow built around `.venv`, `.tools`, and `.local` so the machine does not need global Go / ffmpeg / yt-dlp changes for this repo.
|
||||
|
||||
## Current Architecture
|
||||
- `backend/main.go`
|
||||
@@ -217,6 +223,7 @@
|
||||
- [x] Local self-test workflow
|
||||
- [x] Source-specific search collectors
|
||||
- [x] Shared ranker service layer
|
||||
- [x] Windows 11 PowerShell local bootstrap / self-test / push workflow
|
||||
|
||||
## Important Current Constraints / Known Problems
|
||||
- Search backend quality is still the most fragile subsystem.
|
||||
@@ -426,6 +433,24 @@
|
||||
- `node` is still not installed on this machine.
|
||||
- This is acceptable for the current repo because there is still no Node-based frontend build or lint workflow in-tree.
|
||||
- If future frontend work adds a Node toolchain, document the exact version and setup steps here before pushing.
|
||||
- Windows 11 repo-local workflow:
|
||||
- `scripts/setup-dev.ps1`
|
||||
- creates `.venv`
|
||||
- installs `worker/requirements.txt` into the repo-local venv
|
||||
- downloads repo-local Go into `.tools/go`
|
||||
- downloads repo-local ffmpeg into `.tools/ffmpeg`
|
||||
- creates local command shims under `.tools/bin` such as `python3.cmd`
|
||||
- `scripts/enter-dev-shell.ps1`
|
||||
- prepends `.venv`, `.tools/bin`, local Go, and local ffmpeg to `PATH`
|
||||
- pins `GOPATH`, `GOMODCACHE`, and `GOCACHE` into `.local` so Go does not need user-profile write access
|
||||
- `scripts/run-dev.ps1`
|
||||
- launches the backend with Windows-friendly local environment wiring
|
||||
- `scripts/selftest.ps1`
|
||||
- PowerShell-native replacement for the Linux shell smoke test
|
||||
- verifies `gofmt`, Python syntax, `go test ./...`, frontend syntax via `node --check`, backend build, mock SearXNG boot, `/healthz`, `/api/search`, and `/api/upload`
|
||||
- `scripts/push.ps1`
|
||||
- reads ignored local credentials from `.local/git-credentials.psd1`
|
||||
- pushes without storing credentials in tracked files
|
||||
|
||||
## Local Self-Test Workflow
|
||||
- Primary command:
|
||||
@@ -630,6 +655,33 @@
|
||||
- If behavior in the browser does not match the latest backend/frontend code, the first assumption should be stale frontend assets until proven otherwise
|
||||
|
||||
## Recent Change Log
|
||||
- Date: `2026-03-17`
|
||||
- What changed:
|
||||
- Added repo-local Windows 11 PowerShell workflows:
|
||||
- `scripts/dev-tools.ps1`
|
||||
- `scripts/setup-dev.ps1`
|
||||
- `scripts/enter-dev-shell.ps1`
|
||||
- `scripts/run-dev.ps1`
|
||||
- `scripts/selftest.ps1`
|
||||
- `scripts/push.ps1`
|
||||
- Added ignored local directories for `.venv`, `.tools`, and `.local`.
|
||||
- Created a local-only git credential file for automated push flow and kept it excluded from git.
|
||||
- Pinned PowerShell tooling to repo-local Go caches under `.local` so `go test` no longer depends on writable user-profile Go paths.
|
||||
- Why it changed:
|
||||
- The active development machine is now Windows 11, and the user requested that setup, verification, and push flows work through PowerShell while minimizing machine-wide side effects.
|
||||
- The previous local workflow was Linux-shell-oriented and did not directly cover Windows PowerShell usage.
|
||||
- Initial Windows self-test attempts failed because Go wanted to write into the default user Go cache path, which was not reliably writable in this environment.
|
||||
- How it was verified:
|
||||
- `pwsh -NoProfile -File scripts/setup-dev.ps1 -SkipPythonDeps -SkipGoDownload -SkipFFmpegDownload`
|
||||
- elevated `pwsh -NoProfile -File scripts/setup-dev.ps1`
|
||||
- `pwsh -NoProfile -Command ". .\scripts\enter-dev-shell.ps1; python3 --version; yt-dlp --version; go version; ffmpeg -version | Select-Object -First 1"`
|
||||
- `pwsh -NoProfile -File scripts/selftest.ps1`
|
||||
- `git check-ignore -v .local\git-credentials.psd1 .venv .tools`
|
||||
- What is still risky or incomplete:
|
||||
- `scripts/setup-dev.ps1` still requires network access for Python package install and local Go / ffmpeg downloads on a truly fresh machine.
|
||||
- The backend runtime still invokes `python3` by name, so Windows usage depends on entering the repo-local dev shell or otherwise exposing the generated shim on `PATH`.
|
||||
- Push automation now uses an ignored local credential file, but credential rotation remains a manual step.
|
||||
|
||||
- Date: `2026-03-17`
|
||||
- What changed:
|
||||
- Restored hard/ignorable Gemini batch error filtering so low-value thumbnail skips and no-visual candidate skips do not count as user-facing partial batch failures when useful recommendations were still recovered.
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$script:RepoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$script:ToolsRoot = Join-Path $script:RepoRoot ".tools"
|
||||
$script:BinRoot = Join-Path $script:ToolsRoot "bin"
|
||||
$script:GoRoot = Join-Path $script:ToolsRoot "go"
|
||||
$script:GoBin = Join-Path $script:GoRoot "bin"
|
||||
$script:FFmpegRoot = Join-Path $script:ToolsRoot "ffmpeg"
|
||||
$script:FFmpegBin = Join-Path $script:FFmpegRoot "bin"
|
||||
$script:VenvRoot = Join-Path $script:RepoRoot ".venv"
|
||||
$script:VenvScripts = Join-Path $script:VenvRoot "Scripts"
|
||||
$script:LocalRoot = Join-Path $script:RepoRoot ".local"
|
||||
$script:GoPathRoot = Join-Path $script:LocalRoot "go"
|
||||
$script:GoModCacheRoot = Join-Path $script:GoPathRoot "pkg\mod"
|
||||
$script:GoBuildCacheRoot = Join-Path $script:LocalRoot "go-build-cache"
|
||||
$script:CredentialFile = Join-Path $script:LocalRoot "git-credentials.psd1"
|
||||
|
||||
function Write-Step {
|
||||
param([string]$Message)
|
||||
Write-Host "[ai-media-hub] $Message"
|
||||
}
|
||||
|
||||
function Invoke-CheckedCommand {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$FilePath,
|
||||
[string[]]$Arguments = @()
|
||||
)
|
||||
|
||||
& $FilePath @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
$joined = if ($Arguments.Count -gt 0) { " " + ($Arguments -join " ") } else { "" }
|
||||
throw "명령 실행 실패: $FilePath$joined"
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-Directory {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
New-Item -ItemType Directory -Path $Path | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RepoRoot { return $script:RepoRoot }
|
||||
function Get-ToolsRoot { return $script:ToolsRoot }
|
||||
function Get-BinRoot { return $script:BinRoot }
|
||||
function Get-GoRoot { return $script:GoRoot }
|
||||
function Get-GoBin { return $script:GoBin }
|
||||
function Get-FFmpegBin { return $script:FFmpegBin }
|
||||
function Get-VenvRoot { return $script:VenvRoot }
|
||||
function Get-VenvScripts { return $script:VenvScripts }
|
||||
function Get-LocalRoot { return $script:LocalRoot }
|
||||
function Get-GoPathRoot { return $script:GoPathRoot }
|
||||
function Get-GoModCacheRoot { return $script:GoModCacheRoot }
|
||||
function Get-GoBuildCacheRoot { return $script:GoBuildCacheRoot }
|
||||
function Get-CredentialFile { return $script:CredentialFile }
|
||||
|
||||
function Resolve-SystemCommand {
|
||||
param([Parameter(Mandatory = $true)][string]$Name)
|
||||
try {
|
||||
return (Get-Command $Name -ErrorAction Stop).Source
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Resolve-PythonExe {
|
||||
$candidates = @(
|
||||
(Join-Path $script:VenvScripts "python.exe"),
|
||||
(Resolve-SystemCommand -Name "python")
|
||||
)
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
throw "python.exe를 찾을 수 없습니다. Python 3.12+가 필요합니다."
|
||||
}
|
||||
|
||||
function Resolve-BasePythonExe {
|
||||
$all = @(Get-Command python -All -ErrorAction SilentlyContinue)
|
||||
foreach ($item in $all) {
|
||||
if ($item.Source -and $item.Source -notlike "*\.venv\*") {
|
||||
return $item.Source
|
||||
}
|
||||
}
|
||||
$fallback = Resolve-SystemCommand -Name "python"
|
||||
if ($fallback -and $fallback -notlike "*\.venv\*") {
|
||||
return $fallback
|
||||
}
|
||||
throw "시스템 python.exe를 찾지 못했습니다."
|
||||
}
|
||||
|
||||
function Resolve-VenvPythonExe {
|
||||
$pythonExe = Join-Path $script:VenvScripts "python.exe"
|
||||
if (-not (Test-Path -LiteralPath $pythonExe)) {
|
||||
throw ".venv가 아직 준비되지 않았습니다. scripts/setup-dev.ps1를 먼저 실행하세요."
|
||||
}
|
||||
return $pythonExe
|
||||
}
|
||||
|
||||
function Use-LocalTooling {
|
||||
Ensure-Directory -Path $script:ToolsRoot
|
||||
Ensure-Directory -Path $script:BinRoot
|
||||
Ensure-Directory -Path $script:LocalRoot
|
||||
Ensure-Directory -Path $script:GoPathRoot
|
||||
Ensure-Directory -Path $script:GoModCacheRoot
|
||||
Ensure-Directory -Path $script:GoBuildCacheRoot
|
||||
|
||||
$segments = @(
|
||||
$script:BinRoot,
|
||||
$script:GoBin,
|
||||
$script:FFmpegBin,
|
||||
$script:VenvScripts,
|
||||
$env:PATH
|
||||
) | Where-Object { $_ -and $_.Trim() -ne "" }
|
||||
|
||||
$env:PATH = ($segments | Select-Object -Unique) -join ";"
|
||||
$env:GOPATH = $script:GoPathRoot
|
||||
$env:GOMODCACHE = $script:GoModCacheRoot
|
||||
$env:GOCACHE = $script:GoBuildCacheRoot
|
||||
}
|
||||
|
||||
function Ensure-Venv {
|
||||
$venvPython = Join-Path $script:VenvScripts "python.exe"
|
||||
if (Test-Path -LiteralPath $venvPython) {
|
||||
return $venvPython
|
||||
}
|
||||
|
||||
Ensure-Directory -Path $script:LocalRoot
|
||||
$pythonExe = Resolve-SystemCommand -Name "python"
|
||||
if (-not $pythonExe) {
|
||||
throw "시스템 python이 없어 .venv를 만들 수 없습니다."
|
||||
}
|
||||
|
||||
Write-Step ".venv 생성"
|
||||
& $pythonExe -m venv $script:VenvRoot
|
||||
return $venvPython
|
||||
}
|
||||
|
||||
function Install-PythonRequirements {
|
||||
param([switch]$UpgradePip)
|
||||
|
||||
$venvPython = Ensure-Venv
|
||||
$systemPython = Resolve-BasePythonExe
|
||||
|
||||
if ($UpgradePip) {
|
||||
Write-Step "pip 업그레이드"
|
||||
Invoke-CheckedCommand -FilePath $systemPython -Arguments @("-m", "pip", "--python", $venvPython, "install", "--upgrade", "pip")
|
||||
}
|
||||
|
||||
Write-Step "worker requirements 설치"
|
||||
Invoke-CheckedCommand -FilePath $systemPython -Arguments @("-m", "pip", "--python", $venvPython, "install", "-r", (Join-Path $script:RepoRoot "worker\requirements.txt"))
|
||||
}
|
||||
|
||||
function New-CmdShim {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string]$Target
|
||||
)
|
||||
|
||||
$content = @(
|
||||
"@echo off",
|
||||
"set SCRIPT_DIR=%~dp0",
|
||||
"`"%SCRIPT_DIR%$Target`" %*"
|
||||
) -join "`r`n"
|
||||
Set-Content -LiteralPath $Path -Value $content -Encoding ASCII
|
||||
}
|
||||
|
||||
function Ensure-CommandShims {
|
||||
Ensure-Directory -Path $script:BinRoot
|
||||
|
||||
$pythonShimTarget = "..\..\ .venv\Scripts\python.exe".Replace(" ", "")
|
||||
New-CmdShim -Path (Join-Path $script:BinRoot "python3.cmd") -Target $pythonShimTarget
|
||||
|
||||
$ytDlpExe = Join-Path $script:VenvScripts "yt-dlp.exe"
|
||||
if (Test-Path -LiteralPath $ytDlpExe) {
|
||||
New-CmdShim -Path (Join-Path $script:BinRoot "yt-dlp.cmd") -Target "..\..\ .venv\Scripts\yt-dlp.exe".Replace(" ", "")
|
||||
}
|
||||
}
|
||||
|
||||
function Test-GoReady {
|
||||
return [bool](Resolve-SystemCommand -Name "go")
|
||||
}
|
||||
|
||||
function Test-FFmpegReady {
|
||||
return [bool](Resolve-SystemCommand -Name "ffmpeg")
|
||||
}
|
||||
|
||||
function Test-YtDlpReady {
|
||||
return [bool](Resolve-SystemCommand -Name "yt-dlp")
|
||||
}
|
||||
|
||||
function Install-LocalGo {
|
||||
param(
|
||||
[string]$Version = "1.24.0",
|
||||
[string]$ArchiveUrl = ""
|
||||
)
|
||||
|
||||
if (Test-Path -LiteralPath (Join-Path $script:GoBin "go.exe")) {
|
||||
Write-Step "로컬 Go 이미 준비됨"
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $ArchiveUrl) {
|
||||
$ArchiveUrl = "https://go.dev/dl/go$Version.windows-amd64.zip"
|
||||
}
|
||||
|
||||
Ensure-Directory -Path $script:ToolsRoot
|
||||
$archivePath = Join-Path $script:LocalRoot "go-$Version.windows-amd64.zip"
|
||||
Write-Step "로컬 Go 다운로드: $ArchiveUrl"
|
||||
Invoke-WebRequest -Uri $ArchiveUrl -OutFile $archivePath
|
||||
|
||||
if (Test-Path -LiteralPath $script:GoRoot) {
|
||||
Remove-Item -Recurse -Force $script:GoRoot
|
||||
}
|
||||
Expand-Archive -LiteralPath $archivePath -DestinationPath $script:ToolsRoot -Force
|
||||
}
|
||||
|
||||
function Install-LocalFFmpeg {
|
||||
param([string]$ArchiveUrl = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip")
|
||||
|
||||
if (Test-Path -LiteralPath (Join-Path $script:FFmpegBin "ffmpeg.exe")) {
|
||||
Write-Step "로컬 ffmpeg 이미 준비됨"
|
||||
return
|
||||
}
|
||||
|
||||
Ensure-Directory -Path $script:ToolsRoot
|
||||
$archivePath = Join-Path $script:LocalRoot "ffmpeg-release-essentials.zip"
|
||||
Write-Step "로컬 ffmpeg 다운로드: $ArchiveUrl"
|
||||
Invoke-WebRequest -Uri $ArchiveUrl -OutFile $archivePath
|
||||
|
||||
$extractRoot = Join-Path $script:LocalRoot "ffmpeg-extract"
|
||||
if (Test-Path -LiteralPath $extractRoot) {
|
||||
Remove-Item -Recurse -Force $extractRoot
|
||||
}
|
||||
Expand-Archive -LiteralPath $archivePath -DestinationPath $extractRoot -Force
|
||||
|
||||
$binCandidate = Get-ChildItem -Path $extractRoot -Recurse -Filter "ffmpeg.exe" | Select-Object -First 1
|
||||
if (-not $binCandidate) {
|
||||
throw "ffmpeg.exe를 압축 파일에서 찾지 못했습니다."
|
||||
}
|
||||
|
||||
$sourceBin = Split-Path -Parent $binCandidate.FullName
|
||||
if (Test-Path -LiteralPath $script:FFmpegRoot) {
|
||||
Remove-Item -Recurse -Force $script:FFmpegRoot
|
||||
}
|
||||
Ensure-Directory -Path $script:FFmpegBin
|
||||
Copy-Item -Path (Join-Path $sourceBin "*") -Destination $script:FFmpegBin -Recurse -Force
|
||||
}
|
||||
|
||||
function Get-AppEnvironment {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$WorkspaceRoot,
|
||||
[Parameter(Mandatory = $true)][string]$DownloadsDir,
|
||||
[Parameter(Mandatory = $true)][string]$SqlitePath,
|
||||
[Parameter(Mandatory = $true)][string]$AppAddr,
|
||||
[string]$SearxUrl = "http://127.0.0.1:18080"
|
||||
)
|
||||
|
||||
return @{
|
||||
APP_ROOT = $WorkspaceRoot
|
||||
APP_ADDR = $AppAddr
|
||||
SQLITE_PATH = $SqlitePath
|
||||
DOWNLOADS_DIR = $DownloadsDir
|
||||
FRONTEND_DIR = (Join-Path $WorkspaceRoot "frontend")
|
||||
WORKER_SCRIPT = (Join-Path $WorkspaceRoot "worker\downloader.py")
|
||||
SEARXNG_BASE_URL = $SearxUrl
|
||||
SEARXNG_GOOGLE_VIDEO_ENGINE = "google videos"
|
||||
SEARXNG_WEB_ENGINE = "google"
|
||||
}
|
||||
}
|
||||
|
||||
function Import-GitCredential {
|
||||
$credentialFile = Get-CredentialFile
|
||||
if (-not (Test-Path -LiteralPath $credentialFile)) {
|
||||
throw "git 자격정보 파일이 없습니다: $credentialFile"
|
||||
}
|
||||
return Import-PowerShellDataFile -Path $credentialFile
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot "dev-tools.ps1")
|
||||
|
||||
Use-LocalTooling
|
||||
Write-Step "로컬 개발 PATH 적용 완료"
|
||||
Write-Host (" Repo : " + (Get-RepoRoot))
|
||||
Write-Host (" .venv : " + (Get-VenvRoot))
|
||||
Write-Host (" .tools : " + (Get-ToolsRoot))
|
||||
@@ -0,0 +1,31 @@
|
||||
param(
|
||||
[string]$Remote = "origin",
|
||||
[string]$Branch = "",
|
||||
[switch]$SetUpstream
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot "dev-tools.ps1")
|
||||
|
||||
Use-LocalTooling
|
||||
$credential = Import-GitCredential
|
||||
if (-not $Branch) {
|
||||
$Branch = (& git branch --show-current).Trim()
|
||||
}
|
||||
if (-not $Branch) {
|
||||
throw "현재 브랜치를 찾지 못했습니다."
|
||||
}
|
||||
|
||||
$pair = "{0}:{1}" -f $credential.Username, $credential.Password
|
||||
$token = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($pair))
|
||||
$gitArgs = @("-c", "http.extraheader=AUTHORIZATION: Basic $token", "push")
|
||||
if ($SetUpstream) {
|
||||
$gitArgs += @("--set-upstream", $Remote, $Branch)
|
||||
} else {
|
||||
$gitArgs += @($Remote, $Branch)
|
||||
}
|
||||
|
||||
Write-Step "git push $Remote $Branch"
|
||||
& git @gitArgs
|
||||
@@ -0,0 +1,28 @@
|
||||
param(
|
||||
[string]$AppAddr = "127.0.0.1:8080",
|
||||
[string]$SqlitePath = "",
|
||||
[string]$DownloadsDir = "",
|
||||
[string]$SearxUrl = "http://127.0.0.1:18080"
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot "dev-tools.ps1")
|
||||
|
||||
Use-LocalTooling
|
||||
$repoRoot = Get-RepoRoot
|
||||
if (-not $SqlitePath) {
|
||||
$SqlitePath = Join-Path $repoRoot "db\media.dev.db"
|
||||
}
|
||||
if (-not $DownloadsDir) {
|
||||
$DownloadsDir = Join-Path $repoRoot "downloads"
|
||||
}
|
||||
|
||||
$envMap = Get-AppEnvironment -WorkspaceRoot $repoRoot -DownloadsDir $DownloadsDir -SqlitePath $SqlitePath -AppAddr $AppAddr -SearxUrl $SearxUrl
|
||||
foreach ($entry in $envMap.GetEnumerator()) {
|
||||
Set-Item -Path ("Env:" + $entry.Key) -Value $entry.Value
|
||||
}
|
||||
|
||||
Write-Step "백엔드 실행"
|
||||
Invoke-CheckedCommand -FilePath "go" -Arguments @("run", "./backend")
|
||||
@@ -0,0 +1,158 @@
|
||||
param(
|
||||
[int]$MockPort = 18080,
|
||||
[int]$AppPort = 18081,
|
||||
[switch]$SkipGoFmt
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot "dev-tools.ps1")
|
||||
|
||||
Use-LocalTooling
|
||||
$repoRoot = Get-RepoRoot
|
||||
$tmpRoot = Join-Path (Get-LocalRoot) ("selftest-" + [guid]::NewGuid().ToString("N"))
|
||||
Ensure-Directory -Path $tmpRoot
|
||||
$mockProcess = $null
|
||||
$appProcess = $null
|
||||
$appStdoutTask = $null
|
||||
$appStderrTask = $null
|
||||
|
||||
function Stop-TrackedProcess {
|
||||
param($Process)
|
||||
if ($null -ne $Process -and -not $Process.HasExited) {
|
||||
Stop-Process -Id $Process.Id -Force
|
||||
$Process.WaitForExit()
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not $SkipGoFmt) {
|
||||
Write-Step "gofmt"
|
||||
Invoke-CheckedCommand -FilePath "gofmt" -Arguments @(
|
||||
"-w",
|
||||
(Join-Path $repoRoot "backend\main.go"),
|
||||
(Join-Path $repoRoot "backend\handlers\api.go"),
|
||||
(Join-Path $repoRoot "backend\models\db.go"),
|
||||
(Join-Path $repoRoot "backend\services\cse.go"),
|
||||
(Join-Path $repoRoot "backend\services\cse_test.go"),
|
||||
(Join-Path $repoRoot "backend\services\search_collectors.go"),
|
||||
(Join-Path $repoRoot "backend\services\ranker.go"),
|
||||
(Join-Path $repoRoot "backend\services\gemini.go"),
|
||||
(Join-Path $repoRoot "backend\services\gemini_test.go")
|
||||
)
|
||||
}
|
||||
|
||||
Write-Step "python syntax"
|
||||
Invoke-CheckedCommand -FilePath (Resolve-VenvPythonExe) -Arguments @(
|
||||
"-m",
|
||||
"py_compile",
|
||||
(Join-Path $repoRoot "worker\downloader.py"),
|
||||
(Join-Path $repoRoot "scripts\mock_searxng.py")
|
||||
)
|
||||
|
||||
Write-Step "go test"
|
||||
Invoke-CheckedCommand -FilePath "go" -Arguments @("test", "./...")
|
||||
|
||||
Write-Step "frontend syntax"
|
||||
Invoke-CheckedCommand -FilePath "node" -Arguments @("--check", (Join-Path $repoRoot "frontend\app.js"))
|
||||
|
||||
Write-Step "go build"
|
||||
$binaryPath = Join-Path $tmpRoot "ai-media-hub.exe"
|
||||
Invoke-CheckedCommand -FilePath "go" -Arguments @("build", "-o", $binaryPath, "./backend")
|
||||
|
||||
Write-Step "start mock searxng"
|
||||
$mockLog = Join-Path $tmpRoot "mock-searxng.stdout.log"
|
||||
$mockErrLog = Join-Path $tmpRoot "mock-searxng.stderr.log"
|
||||
$mockProcess = Start-Process -FilePath (Resolve-VenvPythonExe) `
|
||||
-ArgumentList @((Join-Path $repoRoot "scripts\mock_searxng.py"), "--port", $MockPort) `
|
||||
-RedirectStandardOutput $mockLog `
|
||||
-RedirectStandardError $mockErrLog `
|
||||
-PassThru `
|
||||
-WindowStyle Hidden
|
||||
|
||||
Write-Step "start app"
|
||||
$downloadsDir = Join-Path $tmpRoot "downloads"
|
||||
Ensure-Directory -Path $downloadsDir
|
||||
$appLog = Join-Path $tmpRoot "app.log"
|
||||
$envMap = Get-AppEnvironment `
|
||||
-WorkspaceRoot $repoRoot `
|
||||
-DownloadsDir $downloadsDir `
|
||||
-SqlitePath (Join-Path $tmpRoot "media.db") `
|
||||
-AppAddr ("127.0.0.1:{0}" -f $AppPort) `
|
||||
-SearxUrl ("http://127.0.0.1:{0}" -f $MockPort)
|
||||
|
||||
$appStartInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$appStartInfo.FileName = $binaryPath
|
||||
$appStartInfo.WorkingDirectory = $repoRoot
|
||||
$appStartInfo.UseShellExecute = $false
|
||||
$appStartInfo.RedirectStandardOutput = $true
|
||||
$appStartInfo.RedirectStandardError = $true
|
||||
foreach ($entry in $envMap.GetEnumerator()) {
|
||||
$appStartInfo.Environment[$entry.Key] = [string]$entry.Value
|
||||
}
|
||||
$appProcess = New-Object System.Diagnostics.Process
|
||||
$appProcess.StartInfo = $appStartInfo
|
||||
$null = $appProcess.Start()
|
||||
$appStdoutTask = $appProcess.StandardOutput.ReadToEndAsync()
|
||||
$appStderrTask = $appProcess.StandardError.ReadToEndAsync()
|
||||
|
||||
$healthUrl = "http://127.0.0.1:$AppPort/healthz"
|
||||
$healthy = $false
|
||||
for ($i = 0; $i -lt 30; $i++) {
|
||||
try {
|
||||
$health = Invoke-RestMethod -Uri $healthUrl -TimeoutSec 2
|
||||
if ($health.status -eq "ok") {
|
||||
$healthy = $true
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
}
|
||||
if (-not $healthy) {
|
||||
throw "/healthz 확인 실패"
|
||||
}
|
||||
|
||||
Write-Step "verify search"
|
||||
$searchPayload = @{
|
||||
query = "city rain"
|
||||
platforms = @("envato", "artgrid", "google video")
|
||||
} | ConvertTo-Json
|
||||
$search = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$AppPort/api/search" -ContentType "application/json" -Body $searchPayload
|
||||
$searchResults = @($search.results)
|
||||
if ($null -eq $search.results -or $searchResults.Count -lt 2) {
|
||||
throw "검색 결과가 너무 적습니다."
|
||||
}
|
||||
if (@($searchResults | Where-Object { -not $_.link }).Count -gt 0) {
|
||||
throw "검색 결과에 link가 비어 있습니다."
|
||||
}
|
||||
|
||||
Write-Step "verify upload"
|
||||
$sampleFile = Join-Path $tmpRoot "sample.txt"
|
||||
Set-Content -LiteralPath $sampleFile -Value "selftest upload" -Encoding UTF8
|
||||
$form = @{
|
||||
file = Get-Item -LiteralPath $sampleFile
|
||||
}
|
||||
$upload = Invoke-RestMethod -Method Post -Uri "http://127.0.0.1:$AppPort/api/upload" -Form $form
|
||||
if (-not $upload.filename) {
|
||||
throw "업로드 응답에 filename이 없습니다."
|
||||
}
|
||||
$uploadedPath = Join-Path $downloadsDir $upload.filename
|
||||
if (-not (Test-Path -LiteralPath $uploadedPath)) {
|
||||
throw "업로드된 파일이 downloads에 존재하지 않습니다."
|
||||
}
|
||||
|
||||
Write-Step "selftest ok"
|
||||
} finally {
|
||||
Stop-TrackedProcess -Process $appProcess
|
||||
Stop-TrackedProcess -Process $mockProcess
|
||||
if ($appStdoutTask) {
|
||||
$appStdoutTask.Wait()
|
||||
$appStdoutTask.Result | Set-Content -LiteralPath (Join-Path $tmpRoot "app.log") -Encoding UTF8
|
||||
}
|
||||
if ($appStderrTask) {
|
||||
$appStderrTask.Wait()
|
||||
$appStderrTask.Result | Add-Content -LiteralPath (Join-Path $tmpRoot "app.log") -Encoding UTF8
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
param(
|
||||
[switch]$SkipPythonDeps,
|
||||
[switch]$SkipGoDownload,
|
||||
[switch]$SkipFFmpegDownload,
|
||||
[switch]$UpgradePip
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
. (Join-Path $PSScriptRoot "dev-tools.ps1")
|
||||
|
||||
Use-LocalTooling
|
||||
Ensure-Directory -Path (Get-ToolsRoot)
|
||||
Ensure-Directory -Path (Get-LocalRoot)
|
||||
|
||||
$pythonExe = Ensure-Venv
|
||||
Write-Step "Python 사용 경로: $pythonExe"
|
||||
|
||||
if (-not $SkipPythonDeps) {
|
||||
Install-PythonRequirements -UpgradePip:$UpgradePip
|
||||
}
|
||||
|
||||
Ensure-CommandShims
|
||||
|
||||
if (-not $SkipGoDownload -and -not (Test-GoReady)) {
|
||||
Install-LocalGo
|
||||
}
|
||||
|
||||
if (-not $SkipFFmpegDownload -and -not (Test-FFmpegReady)) {
|
||||
Install-LocalFFmpeg
|
||||
}
|
||||
|
||||
Use-LocalTooling
|
||||
|
||||
Write-Step "준비 상태"
|
||||
Write-Host (" python3: " + ((Resolve-SystemCommand -Name "python3") ?? "미설치"))
|
||||
Write-Host (" python : " + ((Resolve-SystemCommand -Name "python") ?? "미설치"))
|
||||
Write-Host (" yt-dlp : " + ((Resolve-SystemCommand -Name "yt-dlp") ?? "미설치"))
|
||||
Write-Host (" go : " + ((Resolve-SystemCommand -Name "go") ?? "미설치"))
|
||||
Write-Host (" ffmpeg : " + ((Resolve-SystemCommand -Name "ffmpeg") ?? "미설치"))
|
||||
Write-Step "현재 셸에 PATH를 적용하려면 다음처럼 dot-source 하세요."
|
||||
Write-Host " . .\scripts\enter-dev-shell.ps1"
|
||||
Reference in New Issue
Block a user