Skip to content

Commit d8ad1d6

Browse files
authored
Merge pull request #9 from SimpNick6703/code-rewrite
v1.1.3 - Refactor build process and improve code structure
2 parents ffc33ca + 1a23f94 commit d8ad1d6

6 files changed

Lines changed: 1483 additions & 1319 deletions

File tree

.github/workflows/deploy.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
branches:
66
- main
77
paths:
8-
- index.html
8+
- docs/**
99
- .github/workflows/deploy.yml
1010
workflow_dispatch:
1111

@@ -36,7 +36,7 @@ jobs:
3636
shell: bash
3737
run: |
3838
mkdir -p _site
39-
cp index.html _site/index.html
39+
cp -R docs/* _site/
4040
4141
- name: Upload artifact
4242
uses: actions/upload-pages-artifact@v3

build.py

Lines changed: 52 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import logging
2222
import urllib.request
2323
from pathlib import Path
24+
import concurrent.futures
2425

2526
# --- Configuration ---
2627
PRESET_SIZES = [10, 50, 100, 500]
@@ -74,7 +75,8 @@ def get_platform_suffix() -> str:
7475

7576
def download_file(url: str, dest: str):
7677
"""Download a file from URL to destination."""
77-
log.info(" Downloading from %s", url[:60] + "..." if len(url) > 60 else url)
78+
short_url = (url[:60] + "...") if len(url) > 60 else url
79+
log.info(" Downloading from %s", short_url)
7880
urllib.request.urlretrieve(url, dest)
7981

8082

@@ -117,10 +119,13 @@ def download_ffmpeg() -> bool:
117119
basename = os.path.basename(member.name)
118120
if basename in ["ffmpeg", "ffprobe"]:
119121
# Extract file content to current directory
120-
with tf.extractfile(member) as src, open(basename, 'wb') as dst:
121-
dst.write(src.read())
122-
os.chmod(basename, 0o755)
123-
log.info(" Extracted %s", basename)
122+
src = tf.extractfile(member)
123+
if src is not None:
124+
with open(basename, 'wb') as dst:
125+
dst.write(src.read())
126+
src.close()
127+
os.chmod(basename, 0o755)
128+
log.info(" Extracted %s", basename)
124129

125130
os.remove(archive_path)
126131

@@ -173,40 +178,23 @@ def check_ffmpeg_available() -> bool:
173178

174179

175180
def create_preset_script(target_mb: int, codec: str, temp_dir: str) -> str:
176-
"""Create a temporary script with hardcoded target size."""
177-
with open(SOURCE_SCRIPT, "r", encoding="utf-8") as f:
178-
content = f.read()
179-
180-
# Replace the default target_mb value in the main block
181-
content = re.sub(
182-
r'(target_mb\s*=\s*)\d+',
183-
f'\\g<1>{target_mb}',
184-
content
185-
)
186-
187-
# Replace the default codec value
188-
content = re.sub(
189-
r'CODEC_TYPE\s*=\s*"hevc"',
190-
f'CODEC_TYPE = "{codec}"',
191-
content
192-
)
193-
194-
# Update the usage message to reflect the preset
195-
content = re.sub(
196-
r'print\("Usage: python script\.py <input> \[output\] \[size_mb\]"\)',
197-
f'print("Usage: {target_mb}mb-{codec} <input> [output] [size_mb]")',
198-
content
199-
)
200-
181+
"""Create a temporary script wrapper with hardcoded preset parameters."""
182+
content = f'''import sys
183+
import videocompress
184+
185+
# Preset Wrapper: {target_mb}mb-{codec}
186+
sys.argv[1:1] = ["{codec}", "{target_mb}"]
187+
videocompress.main()
188+
'''
201189
script_path = os.path.join(temp_dir, f"{target_mb}mb_{codec}.py")
202190
with open(script_path, "w", encoding="utf-8") as f:
203191
f.write(content)
204192

205193
return script_path
206194

207195

208-
def build_executable(script_path: str, target_mb: int, codec: str) -> bool:
209-
"""Build a single executable using PyInstaller."""
196+
def build_executable(script_path: str, target_mb: int, codec: str, work_dir: str, config_dir: str) -> bool:
197+
"""Build a single executable using PyInstaller with isolation."""
210198
platform_suffix = get_platform_suffix()
211199

212200
version = os.environ.get("BUILD_VERSION")
@@ -237,16 +225,21 @@ def build_executable(script_path: str, target_mb: int, codec: str) -> bool:
237225
"--onefile",
238226
"--console",
239227
f"--name={output_name}",
228+
f"--workpath={work_dir}",
240229
f"--distpath={OUTPUT_DIR}",
241230
"--clean",
242231
"--noconfirm",
243232
] + add_binary_args + [script_path]
244233

234+
# Isolate PyInstaller's global metadata/bincache to avoid race conditions
235+
env = os.environ.copy()
236+
env["PYINSTALLER_CONFIG_DIR"] = config_dir
237+
245238
try:
246-
subprocess.run(cmd, check=True)
239+
subprocess.run(cmd, check=True, env=env)
247240
log.info(" Successfully built: %s", output_name)
248241
return True
249-
except subprocess.CalledProcessError as e:
242+
except Exception as e:
250243
log.error(" Failed to build %s: %s", output_name, e)
251244
return False
252245

@@ -299,14 +292,32 @@ def main() -> int:
299292

300293
# Build each preset
301294
log.info("Building presets: %s", PRESET_SIZES)
302-
results = {}
303-
304-
with tempfile.TemporaryDirectory(prefix="vidcomp_build_") as temp_dir:
305-
for size in PRESET_SIZES:
306-
for codec in PRESET_CODECS:
307-
script_path = create_preset_script(size, codec, temp_dir)
308-
results[f"{size}mb-{codec}"] = build_executable(script_path, size, codec)
295+
results: dict[str, bool] = {}
309296

297+
try:
298+
with tempfile.TemporaryDirectory(prefix="vidcomp_build_") as temp_dir:
299+
with concurrent.futures.ThreadPoolExecutor() as executor:
300+
futures = {}
301+
for size in PRESET_SIZES:
302+
for codec in PRESET_CODECS:
303+
task_name = f"{size}mb_{codec}"
304+
task_dir = os.path.join(temp_dir, task_name)
305+
task_work = os.path.join(task_dir, "work")
306+
task_config = os.path.join(task_dir, "config")
307+
os.makedirs(task_work, exist_ok=True)
308+
os.makedirs(task_config, exist_ok=True)
309+
310+
script_path = create_preset_script(size, codec, task_dir)
311+
future = executor.submit(build_executable, script_path, size, codec, task_work, task_config) # type: ignore
312+
futures[future] = f"{size}mb-{codec}"
313+
314+
for future in concurrent.futures.as_completed(futures):
315+
name = futures[future]
316+
results[name] = future.result()
317+
except Exception as e:
318+
log.error("Failed allocating preset staging area: %s", e)
319+
return 1
320+
310321
# Clean build artifacts by default (unless --verbose)
311322
if not verbose:
312323
log.info("Cleaning build artifacts...")

docs/app.js

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
document.addEventListener('DOMContentLoaded', () => {
2+
// --- Theme Toggle Logic ---
3+
const themeBtn = document.getElementById('theme-toggle');
4+
const sunIcon = document.getElementById('icon-sun');
5+
const moonIcon = document.getElementById('icon-moon');
6+
let encodeChart = null;
7+
8+
function toggleTheme() {
9+
document.body.classList.toggle('light-mode');
10+
const isLight = document.body.classList.contains('light-mode');
11+
12+
// Icon visibility swap
13+
if(isLight) {
14+
sunIcon.classList.remove('hidden');
15+
moonIcon.classList.add('hidden');
16+
} else {
17+
sunIcon.classList.add('hidden');
18+
moonIcon.classList.remove('hidden');
19+
}
20+
21+
// Update Chart Colors
22+
if(encodeChart) {
23+
Chart.defaults.color = isLight ? '#475569' : '#94a3b8';
24+
Chart.defaults.borderColor = isLight ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)';
25+
encodeChart.update();
26+
}
27+
}
28+
29+
if(themeBtn) {
30+
themeBtn.addEventListener('click', toggleTheme);
31+
}
32+
33+
// --- Navigation Logic ---
34+
const navBtns = document.querySelectorAll('.nav-btn');
35+
const sections = document.querySelectorAll('.content-section');
36+
37+
navBtns.forEach(btn => {
38+
btn.addEventListener('click', () => {
39+
navBtns.forEach(b => {
40+
b.classList.remove('tab-active');
41+
b.classList.add('text-slate-400');
42+
});
43+
btn.classList.add('tab-active');
44+
btn.classList.remove('text-slate-400');
45+
46+
sections.forEach(sec => sec.classList.add('hidden-section'));
47+
const target = document.getElementById(btn.dataset.target);
48+
if(target) target.classList.remove('hidden-section');
49+
});
50+
});
51+
52+
// --- Chart.js Initialization ---
53+
Chart.defaults.color = '#94a3b8';
54+
Chart.defaults.font.family = "'JetBrains Mono', monospace";
55+
56+
const ctx = document.getElementById('encodeChart').getContext('2d');
57+
encodeChart = new Chart(ctx, {
58+
type: 'bar',
59+
data: {
60+
labels: ['Serial', 'Parallel'],
61+
datasets: [
62+
{ label: 'Chunk 1', data: [240, 60], backgroundColor: '#06b6d4', barThickness: 40 },
63+
{ label: 'Chunk 2', data: [0, 60], backgroundColor: '#8b5cf6', barThickness: 40 },
64+
{ label: 'Stitch Overhead', data: [0, 5], backgroundColor: '#d946ef', barThickness: 40 }
65+
]
66+
},
67+
options: {
68+
indexAxis: 'y',
69+
responsive: true,
70+
maintainAspectRatio: false,
71+
plugins: {
72+
legend: { position: 'bottom' },
73+
tooltip: { callbacks: { label: (c) => c.dataset.label } }
74+
},
75+
scales: {
76+
x: { stacked: true, grid: { color: 'rgba(255,255,255,0.05)' }, ticks: { display: false } },
77+
y: { stacked: true, grid: { display: false } }
78+
}
79+
}
80+
});
81+
82+
// --- BPP Calculator Logic ---
83+
const MIN_BPP = 0.04;
84+
const MB_TO_BITS = 8 * 1024 * 1024;
85+
86+
function calculateBPP() {
87+
const targetMB = parseFloat(document.getElementById('calc-mb').value) || 100;
88+
const duration = parseFloat(document.getElementById('calc-dur').value) || 120;
89+
const srcW = parseInt(document.getElementById('calc-w').value) || 1920;
90+
const srcH = parseInt(document.getElementById('calc-h').value) || 1080;
91+
const srcFps = parseFloat(document.getElementById('calc-fps').value) || 60;
92+
93+
const targetBits = targetMB * MB_TO_BITS;
94+
const aspectRatio = srcW / srcH;
95+
96+
// Filter Options
97+
let heightOptions = [2160, 1440, 1080, 720];
98+
let fpsOptions = [120.0, 90.0, 60.0];
99+
100+
let validHeights = heightOptions.filter(h => h <= srcH);
101+
if (!validHeights.includes(srcH)) validHeights.unshift(srcH);
102+
103+
let validFps = fpsOptions.filter(f => f <= srcFps);
104+
if (!validFps.includes(srcFps)) validFps.unshift(srcFps);
105+
106+
let candidates = [];
107+
validHeights.forEach(h => {
108+
const w = Math.round(h * aspectRatio);
109+
validFps.forEach(f => {
110+
const pixelsPerSec = w * h * f;
111+
const bpp = targetBits / (duration * pixelsPerSec);
112+
candidates.push({ h, w, f, bpp, pps: pixelsPerSec, fpsPriority: f >= 60 });
113+
});
114+
});
115+
116+
// Sort logic: Top candidates > MIN_BPP, then FPS >= 60, then throughput
117+
let safeOnes = candidates.filter(c => c.bpp >= MIN_BPP);
118+
safeOnes.sort((a, b) => {
119+
if (a.fpsPriority !== b.fpsPriority) return a.fpsPriority ? -1 : 1;
120+
return b.pps - a.pps;
121+
});
122+
123+
const best = safeOnes.length > 0 ? safeOnes[0] : candidates[candidates.length - 1]; // Fallback to smallest
124+
125+
// Render Results
126+
const tbody = document.getElementById('calc-results');
127+
if(tbody) {
128+
tbody.innerHTML = '';
129+
candidates.forEach(c => {
130+
const isBest = c === best;
131+
const isSafe = c.bpp >= MIN_BPP;
132+
133+
const tr = document.createElement('tr');
134+
if (isBest) tr.className = 'bg-cyan-900/30 border-l-2 border-cyan-400';
135+
tr.innerHTML = `
136+
<td class="px-4 py-3 ${isBest ? 'text-cyan-400 font-bold' : ''}">${c.w}x${c.h}</td>
137+
<td class="px-4 py-3">${c.f}</td>
138+
<td class="px-4 py-3 ${isSafe ? 'text-emerald-400' : 'text-rose-400'}">${c.bpp.toFixed(4)}</td>
139+
<td class="px-4 py-3 uppercase text-[10px]">${isSafe ? '<span class="text-emerald-400">Pass</span>' : '<span class="text-rose-400">Fail</span>'}</td>
140+
`;
141+
tbody.appendChild(tr);
142+
});
143+
}
144+
145+
const conc = document.getElementById('calc-conclusion');
146+
if(conc) {
147+
conc.classList.remove('hidden', 'border-emerald-500', 'bg-emerald-900/20', 'border-amber-500', 'bg-amber-900/20');
148+
if (best.bpp >= MIN_BPP) {
149+
conc.classList.add('border-emerald-500', 'bg-emerald-900/20');
150+
conc.innerHTML = `<strong>Quality Pass:</strong> Script will use <strong>${best.w}x${best.h} @ ${best.f}fps</strong>. Clarity threshold maintained.`;
151+
} else {
152+
conc.classList.add('border-amber-500', 'bg-amber-900/20');
153+
conc.innerHTML = `<strong>Downscale Required:</strong> Target size is too restrictive. Scaling to <strong>${best.w}x${best.h} @ ${best.f}fps</strong> to avoid pixelation.`;
154+
}
155+
}
156+
}
157+
158+
const debounce = (f, w) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => f(...a), w); }; };
159+
const debouncedCalc = debounce(calculateBPP, 500);
160+
['calc-mb', 'calc-dur', 'calc-w', 'calc-h', 'calc-fps'].forEach(id => {
161+
const el = document.getElementById(id);
162+
if(el) el.addEventListener('input', debouncedCalc);
163+
});
164+
calculateBPP();
165+
166+
// --- Hardware Matrix Logic ---
167+
const osData = {
168+
win: {
169+
title: "Windows Execution Priority", icon: "fa-windows", color: "text-blue-400",
170+
chain: [
171+
{ name: "Nvidia NVENC", desc: "H.265/H.264 via dedicated hardware. Supports parallel 2-pass.", icon: "fa-microchip" },
172+
{ name: "AMD AMF", desc: "H.265/H.264 via AMD's Media Framework.", icon: "fa-microchip" },
173+
{ name: "Intel Quick Sync (QSV)", desc: "H.265/H.264 via Intel's integrated GPU.", icon: "fa-microchip" },
174+
{ name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" }
175+
],
176+
note: "The script probes for encoders for the selected codec (H.265/HEVC or H.264). If dedicated GPU encoding fails or is unavailable, it defaults to CPU."
177+
},
178+
lin: {
179+
title: "Linux Execution Priority", icon: "fa-linux", color: "text-yellow-400",
180+
chain: [
181+
{ name: "Nvidia NVENC", desc: "H.265/H.264 via dedicated hardware. Supports parallel 2-pass.", icon: "fa-microchip" },
182+
{ name: "VA-API", desc: "Unified API for AMD and Intel hardware acceleration.", icon: "fa-layer-group" },
183+
{ name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" }
184+
],
185+
note: "VA-API is a versatile API that covers both AMD and Intel hardware on Linux. NVENC is preferred if an Nvidia GPU is present."
186+
},
187+
mac: {
188+
title: "macOS Execution Priority", icon: "fa-apple", color: "text-slate-200",
189+
chain: [
190+
{ name: "Apple VideoToolbox", desc: "Native API for Apple Silicon, AMD, and Intel hardware.", icon: "fa-apple" },
191+
{ name: "CPU Fallback (libx265/libx264)", desc: "Software encoding. Most compatible, but slowest.", icon: "fa-server" }
192+
],
193+
note: "VideoToolbox is the native macOS framework that abstracts hardware (Apple Silicon, Intel, AMD). Nvidia encoding is not supported on modern macOS."
194+
}
195+
};
196+
197+
function renderChain(k) {
198+
const d = osData[k];
199+
const osTitle = document.getElementById('os-title');
200+
const osNote = document.getElementById('os-note');
201+
const priorityChain = document.getElementById('priority-chain');
202+
203+
if(osTitle && osNote && priorityChain) {
204+
osTitle.innerHTML = `<i class="fa-brands ${d.icon} ${d.color} mr-3"></i>${d.title}`;
205+
osNote.textContent = d.note;
206+
priorityChain.innerHTML = '';
207+
d.chain.forEach((e, i) => {
208+
const el = document.createElement('div');
209+
el.className = `flex items-center p-4 bg-slate-800/50 rounded-lg border border-slate-700 hover:border-slate-500 transition-all ${i===0?'ring-1 ring-cyan-500/30':''}`;
210+
el.innerHTML = `<div class="w-8 h-8 rounded-full ${i===0?'bg-cyan-500 text-slate-900':'bg-slate-700 text-white'} flex items-center justify-center font-bold text-sm mr-4">${i+1}</div><div class="flex-grow"><div class="font-bold text-white font-mono text-sm">${e.name}</div><div class="text-xs text-slate-400">${e.desc}</div></div><i class="fa-solid ${e.icon} text-slate-600 text-xl ml-4"></i>`;
211+
priorityChain.appendChild(el);
212+
});
213+
}
214+
}
215+
216+
document.querySelectorAll('.os-btn').forEach(b => b.addEventListener('click', e => {
217+
document.querySelectorAll('.os-btn').forEach(x => { x.classList.remove('os-btn-active'); x.classList.add('bg-slate-800', 'border-slate-600'); });
218+
const btn = e.target.closest('button');
219+
if(btn) {
220+
btn.classList.remove('bg-slate-800', 'border-slate-600');
221+
btn.classList.add('os-btn-active');
222+
renderChain(btn.id.replace('btn-', ''));
223+
}
224+
}));
225+
226+
renderChain('win');
227+
});

0 commit comments

Comments
 (0)