영상 중복 탐지 완성 + 간단 편집(회전·자르기) + 동시 재생 + 별점·태그 세트
CI / windows-x64 (push) Canceled after 0s
CI / windows-arm64-cross (push) Canceled after 0s
CI / windows-arm64-native (push) Canceled after 0s
CI / macos (push) Canceled after 0s

중복 탐지
- 이미지 근접쌍 밴드 색인 + 스트리밍 클러스터, 후보 검증(타일 다수결·크롭 해시)
- 색 지문으로 색만 다른 오탐 제거, 회전·반전본 탐지(8변형 질의, 선택 옵션)
- 오디오 지문(chromaprint)을 같은 디코드 패스에서 뽑아 레터박스 사본을 잡는다.
  배경음만 같은 남남은 시각 확인 패스(트림 축 프레임 거리)로 걸러낸다 — 실측 9 vs 29.
- 비교 재생(CompareView): 여러 사본을 offset 정렬해 나란히 재생
- 지문 생성 속도 조절(느림·보통·빠름) — 재시작 없이 반영

간단 편집 (원본을 직접 고침, Ctrl+Z로 되돌림)
- 회전·반전: JPEG은 EXIF 방향 태그 2바이트만, MP4는 회전 행렬만 — 화질 손실 0.
  방향값 전이표는 군의 성질(90도x4=제자리)로 검산하고, 실제 화소를 transpose=1과
  비교해 "시계 방향"이 정말 시계인지 확인한다.
- 자르기: 화면 좌표 기준(회전 메타가 붙어 있어도 어긋나지 않는다)
- 구간 자르기: 스트림 복사라 무손실. 필름스트립 타임라인 + 손잡이 끌기 + 구간 미리보기.
- 되돌릴 수 없는 편집(자르기·반전)은 원본을 .archive_trash/edits로 옮겨 보관
- 편집이 수정시각을 바꾸지 않는다 — 기본 정렬이 촬영일이라 항목이 튀면 다시 찾아야 한다

동시 재생
- 고른 영상을 최대 9개 격자에 놓고 함께 재생. 소리는 한 칸만.
- 실시간 변환이 필요한 코덱은 2개까지만 — 그 이상은 ffmpeg가 기계를 멈춘다

그 외
- 별점(스냅샷 flags 남는 비트에 얹어 항목당 바이트 증가 0)
- 태그: 검색 없이 클릭으로 붙이기, 세트+단축키, 이름 수정 시 붙은 항목에 전파
- 파일 로깅(tracing-appender) — 릴리스는 콘솔이 없어 stdout만으로는 원인을 못 찾았다
- ARM64 크로스 컴파일 스크립트(관리자 권한 없이 VS 카탈로그에서 조립)
- DB 마이그레이션 v7~v10

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
ncakanghan
2026-08-03 16:18:24 +09:00
co-authored by Claude Opus 5
parent 67d2ddc59e
commit 8aa74c1a8d
67 changed files with 19747 additions and 1283 deletions
+165
View File
@@ -0,0 +1,165 @@
# Lay out the MSVC ARM64 cross toolchain locally, without the VS Installer.
# ASCII only - PS 5.1 reads BOM-less files as ANSI.
#
# WHY: the VS Installer on some machines refuses every unattended `modify` - it insists on
# updating itself first and exits with "Status changed to UpdateAvailable". Six different
# invocations (setup.exe modify/update, vs_installer.exe, --noUpdateInstaller, --quiet,
# --passive) all stopped there. The same payloads are plain .vsix (zip) files in the VS
# catalog with direct download URLs, so we fetch and extract them into the repo instead.
#
# This changes nothing outside the repo: no admin, no registry, no Program Files.
# If the ARM64 component is later installed properly, build-portable.ps1 prefers that.
#
# Usage: .\scripts\fetch-arm64-toolchain.ps1 [-Force]
# Output: .toolchain\arm64\ (VC\Tools\MSVC\<ver>\{bin,lib}\...)
param([switch]$Force)
$ErrorActionPreference = "Stop"
$root = Split-Path $PSScriptRoot -Parent
$dest = Join-Path $root ".toolchain\arm64"
# Two independent halves: MSVC ARM64 (compiler + CRT) and clang (assembles .S).
# Each is skipped when already present, so re-running only fetches what is missing.
$haveMsvc = $false
if ((Test-Path $dest) -and -not $Force) {
$haveMsvc = [bool](Get-ChildItem $dest -Recurse -Filter "cl.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "Hostx64\\arm64" } | Select-Object -First 1)
}
$cache = Join-Path $root ".toolchain\.cache"
New-Item -ItemType Directory -Path $cache -Force | Out-Null
# --- resolve packages from the VS catalog on disk ---------------------------
if ($haveMsvc) { Write-Host "[arm64] msvc already present - skipping" }
else {
$chans = Join-Path $env:LOCALAPPDATA "Microsoft\VisualStudio\Packages\_Channels"
if (-not (Test-Path $chans)) { throw "VS catalog not found - is Visual Studio installed?" }
$catalog = Get-ChildItem $chans -Recurse -Filter "catalog.json" |
Sort-Object Length -Descending | Select-Object -First 1
if (-not $catalog) { throw "catalog.json not found under $chans" }
$vcRoot = "C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC"
if (-not (Test-Path $vcRoot)) {
$vsw = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$vcRoot = Join-Path (& $vsw -latest -products * -property installationPath) "VC\Tools\MSVC"
}
$toolset = (Get-ChildItem $vcRoot -Directory | Sort-Object Name | Select-Object -Last 1).Name
$minor = ($toolset -split '\.')[0..1] -join '.'
Write-Host "[arm64] host toolset $toolset (family $minor) - matching ARM64 payloads"
# Parsing a 18MB json with ConvertFrom-Json is slow in PS 5.1; node is already a dependency.
$listScript = @'
const fs=require('fs');
const cat=JSON.parse(fs.readFileSync(process.argv[2],'utf8'));
const minor=process.argv[3].replace('.','\\.');
// 순서가 곧 우선순위다 - 뒤 패키지는 '없는 파일만' 채운다(아래 fill-only 참고).
// Tools : cl/link/lib (Hostx64 -> ARM64)
// CRT.Desktop : libcmt, libvcruntime, libcpmt ...
// CRT.Store : oldnames.lib 이 여기에만 있다(데스크톱 패키지에 빠져 있어 링크가 LNK1104로 죽는다)
const want=[
new RegExp(`^Microsoft\\.VC\\.${minor}\\.Tools\\.HostX64\\.TargetARM64\\.base$`,'i'),
new RegExp(`^Microsoft\\.VC\\.${minor}\\.CRT\\.ARM64\\.Desktop\\.base$`,'i'),
new RegExp(`^Microsoft\\.VC\\.${minor}\\.CRT\\.ARM64\\.Store\\.base$`,'i'),
];
const seen=new Set(); const out=[];
for(const re of want){
for(const p of cat.packages){
const id=p.id||'';
if(!re.test(id)||seen.has(id)) continue;
for(const pl of (p.payloads||[])){ seen.add(id); out.push({id,url:pl.url,size:pl.size,sha256:pl.sha256}); break; }
}
}
process.stdout.write(JSON.stringify(out));
'@
$tmpJs = Join-Path $env:TEMP "vs-arm-list.js"
Set-Content -Path $tmpJs -Value $listScript -Encoding ascii
$pkgs = & node $tmpJs $catalog.FullName $minor | ConvertFrom-Json
if (-not $pkgs -or $pkgs.Count -lt 2) { throw "ARM64 payloads for $minor not found in the catalog" }
# --- download + extract -----------------------------------------------------
New-Item -ItemType Directory -Path $dest -Force | Out-Null
foreach ($p in $pkgs) {
$file = Join-Path $cache "$($p.id).vsix"
if (-not (Test-Path $file) -or (Get-Item $file).Length -ne $p.size) {
Write-Host ("[arm64] download {0} ({1:N1} MB)" -f $p.id, ($p.size / 1MB))
Invoke-WebRequest -Uri $p.url -OutFile $file -UseBasicParsing
}
# Payloads are signed; verify before extracting anything into the repo.
$got = (Get-FileHash $file -Algorithm SHA256).Hash
if ($p.sha256 -and $got -ne $p.sha256.ToUpper()) {
Remove-Item $file -Force
throw "sha256 mismatch for $($p.id) - download discarded"
}
Write-Host "[arm64] extract $($p.id)"
# .vsix is a zip; the useful tree is under Contents\.
# Expand-Archive refuses anything not named .zip, so go through the .NET API.
$stage = Join-Path $cache "x_$($p.id)"
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
Add-Type -AssemblyName System.IO.Compression.FileSystem
[System.IO.Compression.ZipFile]::ExtractToDirectory($file, $stage)
$contents = Join-Path $stage "Contents"
if (-not (Test-Path $contents)) { throw "unexpected vsix layout: $stage" }
# **Fill only, never overwrite.** The Store package carries its own store/uwp flavour of
# libcmt & friends next to the one file we actually need from it (oldnames.lib); copying
# over the Desktop CRT would silently swap the runtime out from under the build.
$prefix = (Resolve-Path $contents).Path.TrimEnd('\') + '\'
foreach ($src in Get-ChildItem $contents -Recurse -File) {
$rel = $src.FullName.Substring($prefix.Length)
$dst = Join-Path $dest $rel
if (Test-Path $dst) { continue }
New-Item -ItemType Directory -Path (Split-Path $dst -Parent) -Force | Out-Null
Copy-Item $src.FullName $dst
}
Remove-Item $stage -Recurse -Force
}
$cl = Get-ChildItem $dest -Recurse -Filter "cl.exe" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "Hostx64\\arm64" } | Select-Object -First 1
if (-not $cl) { throw "cl.exe for Hostx64/arm64 not found after extraction" }
Write-Host "[arm64] msvc ready: $($cl.FullName)"
}
# --- clang -----------------------------------------------------------------
# cl.exe cannot assemble the .S files that aws-lc-sys and ring ship for aarch64-windows
# (it prints "unrecognized source file type" and SKIPS them, so the link fails on a missing
# .o). Both crates expect a clang-compatible driver there. We use the plain tarball rather
# than LLVM's installer: the installer asks for elevation, and on this machine every
# elevation path is what pushed us to this script in the first place.
$llvmDir = Join-Path $root ".toolchain\llvm"
$clangCl = Join-Path $llvmDir "bin\clang-cl.exe"
if ((Test-Path $clangCl) -and -not $Force) {
Write-Host "[arm64] clang already present: $clangCl"
return
}
Write-Host "[arm64] resolving latest LLVM release"
$rel = Invoke-RestMethod "https://api.github.com/repos/llvm/llvm-project/releases/latest" `
-Headers @{ "User-Agent" = "archive-build" }
$asset = $rel.assets | Where-Object { $_.name -match "^clang\+llvm-.*-x86_64-pc-windows-msvc\.tar\.xz$" } |
Select-Object -First 1
if (-not $asset) { throw "no x86_64-pc-windows-msvc tarball in LLVM release $($rel.tag_name)" }
$tar = Join-Path $cache $asset.name
if (-not (Test-Path $tar) -or (Get-Item $tar).Length -ne $asset.size) {
Write-Host ("[arm64] download {0} ({1:N0} MB) - this one is large" -f $asset.name, ($asset.size / 1MB))
# Invoke-WebRequest buffers the whole body in memory on PS 5.1; use the streaming client.
$wc = New-Object System.Net.WebClient
$wc.Headers.Add("User-Agent", "archive-build")
$wc.DownloadFile($asset.browser_download_url, $tar)
}
Write-Host "[arm64] extract clang (bin + builtin headers only)"
$stage = Join-Path $cache "llvm_x"
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
New-Item -ItemType Directory -Path $stage -Force | Out-Null
# Windows ships bsdtar, which reads .tar.xz. Pull only what the build needs - the full
# tree is ~2.5GB and we want clang-cl, clang, and the compiler's builtin headers.
& tar -xf $tar -C $stage --strip-components=1 "*/bin/clang*.exe" "*/bin/llvm-lib.exe" "*/lib/clang/*"
if ($LASTEXITCODE -ne 0) { throw "tar extraction failed" }
if (Test-Path $llvmDir) { Remove-Item $llvmDir -Recurse -Force }
New-Item -ItemType Directory -Path (Split-Path $llvmDir -Parent) -Force | Out-Null
Move-Item $stage $llvmDir
if (-not (Test-Path $clangCl)) { throw "clang-cl.exe missing after extraction" }
Write-Host "[arm64] clang ready: $clangCl"