Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
8 changed files with 103 additions and 552 deletions
170
.github/workflows/build.yml
vendored
170
.github/workflows/build.yml
vendored
|
|
@ -1,23 +1,11 @@
|
|||
name: Windows Installer bauen
|
||||
|
||||
# Von GitHub Actions auf selbstgehostete Forgejo Actions umgestellt (Windows
|
||||
# Runner rcp-24, derselbe Runner wie NovaStudioCast, siehe NOVA-SERVER/forgejo-runner).
|
||||
# Zwei Aenderungen gegenueber der alten GitHub Version:
|
||||
# 1. dtolnay/rust-toolchain ist auf dieser Forgejo Instanz nicht gespiegelt
|
||||
# (nur die offiziellen actions/* Actions sind das), deshalb Rust hier
|
||||
# direkt per rustup eingerichtet.
|
||||
# 2. tauri-apps/tauri-action ist ebenfalls nicht gespiegelt, deshalb wird bei
|
||||
# einem Versions-Tag von Hand gebaut, signiert und als Forgejo Release
|
||||
# veroeffentlicht (Installer + latest.json), statt diese GitHub Action zu
|
||||
# nutzen. "npm run tauri build" erzeugt bei gesetzten
|
||||
# TAURI_SIGNING_PRIVATE_KEY(_PASSWORD) Umgebungsvariablen automatisch eine
|
||||
# .sig Signaturdatei neben dem Installer, unabhaengig von tauri-action.
|
||||
# actions/upload-artifact bewusst bei v3 belassen, v4+ wird von Forgejo nicht
|
||||
# unterstuetzt (GHESNotSupportedError, siehe NovaStudioCast build.yml).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'tauri-app/**'
|
||||
- '.github/workflows/build.yml'
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch: {}
|
||||
|
|
@ -34,8 +22,6 @@ jobs:
|
|||
steps:
|
||||
- name: Code auschecken
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Node.js einrichten
|
||||
uses: actions/setup-node@v4
|
||||
|
|
@ -43,11 +29,7 @@ jobs:
|
|||
node-version: 20
|
||||
|
||||
- name: Rust einrichten
|
||||
shell: pwsh
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal
|
||||
rustup default stable
|
||||
rustup target add x86_64-pc-windows-msvc
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Node Abhängigkeiten installieren
|
||||
working-directory: tauri-app
|
||||
|
|
@ -61,44 +43,24 @@ jobs:
|
|||
New-Item -ItemType Directory -Force -Path $binDir | Out-Null
|
||||
$triple = "x86_64-pc-windows-msvc"
|
||||
|
||||
function Assert-ValidExe($path) {
|
||||
if (-not (Test-Path $path)) {
|
||||
Write-Error "$path wurde nicht angelegt, Download ist fehlgeschlagen."
|
||||
}
|
||||
$size = (Get-Item $path).Length
|
||||
if ($size -lt 1MB) {
|
||||
Write-Error "$path ist mit $size Bytes verdaechtig klein fuer ein echtes Programm, vermutlich ist beim Download eine Fehlerseite statt der Datei angekommen. Abgebrochen, bevor eine kaputte Datei eingepackt wird."
|
||||
}
|
||||
$stream = [System.IO.File]::OpenRead($path)
|
||||
$header = New-Object byte[] 2
|
||||
$stream.Read($header, 0, 2) | Out-Null
|
||||
$stream.Close()
|
||||
if ($header[0] -ne 0x4D -or $header[1] -ne 0x5A) {
|
||||
Write-Error "$path beginnt nicht mit der erwarteten MZ Kennung einer Windows exe Datei, vermutlich keine echte ausfuehrbare Datei."
|
||||
}
|
||||
Write-Host " $path geprueft, $size Bytes, gueltige exe Kennung."
|
||||
}
|
||||
|
||||
Write-Host "Lade auto-editor..."
|
||||
Invoke-WebRequest -Uri "https://github.com/WyattBlue/auto-editor/releases/latest/download/auto-editor-windows-x86_64.exe" `
|
||||
-OutFile "$binDir/auto-editor-$triple.exe"
|
||||
Assert-ValidExe "$binDir/auto-editor-$triple.exe"
|
||||
|
||||
Write-Host "Lade FFmpeg (gyan.dev essentials build)..."
|
||||
Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip" -OutFile "ffmpeg.zip"
|
||||
Expand-Archive -Path "ffmpeg.zip" -DestinationPath "ffmpeg-extracted"
|
||||
$ffmpegExe = Get-ChildItem -Path "ffmpeg-extracted" -Recurse -Filter "ffmpeg.exe" | Select-Object -First 1
|
||||
Copy-Item $ffmpegExe.FullName "$binDir/ffmpeg-$triple.exe"
|
||||
Assert-ValidExe "$binDir/ffmpeg-$triple.exe"
|
||||
|
||||
Write-Host "Ermittle aktuelle DeepFilterNet Windows Datei..."
|
||||
$release = Invoke-RestMethod -Uri "https://api.github.com/repos/Rikorose/DeepFilterNet/releases/latest" -Headers @{ "User-Agent" = "novaphonic-build" }
|
||||
$winAsset = $release.assets | Where-Object { $_.name -match "(?i)deep-filter-.*x86_64-pc-windows-msvc\.exe$" } | Select-Object -First 1
|
||||
$winAsset = $release.assets | Where-Object { $_.name -match "(?i)win" -and $_.name -match "(?i)deep-filter" } | Select-Object -First 1
|
||||
if (-not $winAsset) {
|
||||
Write-Error "Kein passendes DeepFilterNet Windows Asset in der aktuellen Release gefunden. Bitte https://github.com/Rikorose/DeepFilterNet/releases manuell pruefen und diesen Schritt anpassen."
|
||||
}
|
||||
Write-Host "Gefunden: $($winAsset.name)"
|
||||
Invoke-WebRequest -Uri $winAsset.browser_download_url -OutFile "deepfilter-download" -Headers @{ "User-Agent" = "novaphonic-build" }
|
||||
Invoke-WebRequest -Uri $winAsset.browser_download_url -OutFile "deepfilter-download"
|
||||
if ($winAsset.name -match "\.zip$") {
|
||||
Expand-Archive -Path "deepfilter-download" -DestinationPath "deepfilter-extracted"
|
||||
$dfExe = Get-ChildItem -Path "deepfilter-extracted" -Recurse -Filter "*.exe" | Select-Object -First 1
|
||||
|
|
@ -106,7 +68,6 @@ jobs:
|
|||
} else {
|
||||
Copy-Item "deepfilter-download" "$binDir/deep-filter-$triple.exe"
|
||||
}
|
||||
Assert-ValidExe "$binDir/deep-filter-$triple.exe"
|
||||
|
||||
- name: Nur bauen, kein Release (normaler Push ohne Versions-Tag)
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
|
|
@ -115,117 +76,22 @@ jobs:
|
|||
|
||||
- name: Installer als Artefakt hochladen (nur bei normalem Push)
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: novaphonic-windows-installer
|
||||
path: |
|
||||
tauri-app/src-tauri/target/release/bundle/nsis/*.exe
|
||||
|
||||
- name: Änderungsliste seit dem letzten Tag ermitteln (nur bei Versions-Tag)
|
||||
- name: Bauen, signieren und GitHub Release veröffentlichen (nur bei Versions-Tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
id: changelog
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$currentTag = "${{ github.ref_name }}"
|
||||
$prevTag = git tag --sort=-v:refname | Where-Object { $_ -ne $currentTag } | Select-Object -First 1
|
||||
Write-Host "Aktueller Tag: $currentTag"
|
||||
Write-Host "Vorheriger Tag: $(if ($prevTag) { $prevTag } else { 'keiner, es ist das erste Release' })"
|
||||
if ($prevTag) {
|
||||
$log = git log --pretty=format:"- %s" "$prevTag..$currentTag"
|
||||
} else {
|
||||
$log = git log --pretty=format:"- %s" "$currentTag"
|
||||
}
|
||||
$changelog = "changelog<<CHANGELOG_EOF`n$($log -join "`n")`nCHANGELOG_EOF"
|
||||
Add-Content -Path $env:GITHUB_OUTPUT -Value $changelog
|
||||
|
||||
- name: Versionsnummer aus Tag automatisch setzen (nur bei Versions-Tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
working-directory: tauri-app
|
||||
shell: pwsh
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$version = "${{ github.ref_name }}" -replace '^v', ''
|
||||
Write-Host "Setze Version auf $version in tauri.conf.json, package.json, Cargo.toml ..."
|
||||
|
||||
$confPath = "src-tauri/tauri.conf.json"
|
||||
$content = Get-Content $confPath -Raw
|
||||
$neu = $content -replace '("version"\s*:\s*)"[^"]*"', "`$1`"$version`""
|
||||
if ($neu -eq $content) { Write-Error "Version in $confPath nicht gefunden/ersetzt." }
|
||||
Set-Content -Path $confPath -Value $neu -NoNewline
|
||||
|
||||
$pkgPath = "package.json"
|
||||
$content = Get-Content $pkgPath -Raw
|
||||
$neu = $content -replace '("version"\s*:\s*)"[^"]*"', "`$1`"$version`""
|
||||
if ($neu -eq $content) { Write-Error "Version in $pkgPath nicht gefunden/ersetzt." }
|
||||
Set-Content -Path $pkgPath -Value $neu -NoNewline
|
||||
|
||||
$cargoPath = "src-tauri/Cargo.toml"
|
||||
$content = Get-Content $cargoPath -Raw
|
||||
$neu = $content -replace '(?m)^version = "[^"]*"', "version = `"$version`""
|
||||
if ($neu -eq $content) { Write-Error "Version in $cargoPath nicht gefunden/ersetzt." }
|
||||
Set-Content -Path $cargoPath -Value $neu -NoNewline
|
||||
|
||||
Write-Host " Erledigt, alle drei Dateien jetzt auf $version."
|
||||
|
||||
- name: Bauen und signieren (nur bei Versions-Tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
working-directory: tauri-app
|
||||
run: npm run tauri build
|
||||
|
||||
- name: Forgejo Release veröffentlichen (nur bei Versions-Tag)
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: pwsh
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_RELEASE_TOKEN }}
|
||||
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
|
||||
run: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$repoApi = "https://forgejo.kiw-schmiede.eu/api/v1/repos/kiw-schmiede/novaphonic"
|
||||
$headers = @{ Authorization = "token $env:FORGEJO_TOKEN" }
|
||||
$tag = "${{ github.ref_name }}"
|
||||
|
||||
$bundleDir = "tauri-app/src-tauri/target/release/bundle/nsis"
|
||||
$installer = Get-ChildItem -Path $bundleDir -Filter "*.exe" | Select-Object -First 1
|
||||
$sigFile = "$($installer.FullName).sig"
|
||||
if (-not (Test-Path $sigFile)) {
|
||||
Write-Error "Keine .sig Datei neben $($installer.Name) gefunden, Signierung ist vermutlich fehlgeschlagen (TAURI_SIGNING_PRIVATE_KEY gesetzt?)."
|
||||
}
|
||||
$signature = Get-Content $sigFile -Raw
|
||||
|
||||
Write-Host "Lege Forgejo Release fuer $tag an ..."
|
||||
$releaseBody = @{
|
||||
tag_name = $tag
|
||||
name = "NovaPhonic $tag"
|
||||
body = "NovaPhonic $tag. Enthaelt den Windows Installer sowie das Update Manifest fuer die In App Aktualisierung.`n`nAenderungen in diesem Release:`n$env:CHANGELOG"
|
||||
draft = $false
|
||||
prerelease = $false
|
||||
} | ConvertTo-Json
|
||||
$release = Invoke-RestMethod -Uri "$repoApi/releases" -Method Post -Headers $headers -ContentType "application/json" -Body $releaseBody
|
||||
Write-Host " Release ID $($release.id) angelegt."
|
||||
|
||||
Write-Host "Lade Installer hoch ..."
|
||||
$installerUploadUrl = "$repoApi/releases/$($release.id)/assets?name=$($installer.Name)"
|
||||
Invoke-RestMethod -Uri $installerUploadUrl -Method Post -Headers $headers -Form @{ attachment = Get-Item $installer.FullName } | Out-Null
|
||||
|
||||
$installerAsset = (Invoke-RestMethod -Uri "$repoApi/releases/$($release.id)" -Headers $headers).assets | Where-Object { $_.name -eq $installer.Name }
|
||||
|
||||
Write-Host "Baue latest.json (Update Manifest) ..."
|
||||
$latestJson = @{
|
||||
version = $tag
|
||||
notes = "NovaPhonic $tag"
|
||||
pub_date = (Get-Date -AsUTC -Format "yyyy-MM-ddTHH:mm:ssZ")
|
||||
platforms = @{
|
||||
"windows-x86_64" = @{
|
||||
signature = $signature
|
||||
url = $installerAsset.browser_download_url
|
||||
}
|
||||
}
|
||||
} | ConvertTo-Json -Depth 5
|
||||
Set-Content -Path "latest.json" -Value $latestJson -NoNewline
|
||||
|
||||
Write-Host "Lade latest.json hoch ..."
|
||||
$manifestUploadUrl = "$repoApi/releases/$($release.id)/assets?name=latest.json"
|
||||
Invoke-RestMethod -Uri $manifestUploadUrl -Method Post -Headers $headers -Form @{ attachment = Get-Item "latest.json" } | Out-Null
|
||||
|
||||
Write-Host "Fertig: https://forgejo.kiw-schmiede.eu/kiw-schmiede/novaphonic/releases/tag/$tag"
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
projectPath: tauri-app
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: 'NovaPhonic ${{ github.ref_name }}'
|
||||
releaseBody: 'Automatisch erstelltes Release. Enthält den Windows Installer sowie das Update Manifest für die In App Aktualisierung.'
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
includeUpdaterJson: true
|
||||
|
|
|
|||
87
app/app.js
87
app/app.js
|
|
@ -148,67 +148,8 @@
|
|||
loudnormToggle.addEventListener('change', syncLoudnormOptions);
|
||||
syncLoudnormOptions();
|
||||
|
||||
// ---------- Automatisches Speichern ----------
|
||||
// Standard bleibt der "Speichern unter" Dialog nach jeder Verarbeitung (wie
|
||||
// bisher). Wer die Checkbox aktiviert, bekommt stattdessen jedes Ergebnis
|
||||
// automatisch in einem einmal gewählten Ordner abgelegt, ohne Dialog. Die
|
||||
// Einstellung wird über zwei kleine Rust Commands (load_settings/save_settings)
|
||||
// in einer settings Datei im App Konfigurationsordner gespeichert, damit sie
|
||||
// App-Neustarts übersteht.
|
||||
const autoSaveToggle = $('autoSaveToggle');
|
||||
const autoSaveFolderRow = $('autoSaveFolderRow');
|
||||
const autoSaveFolderPath = $('autoSaveFolderPath');
|
||||
const chooseAutoSaveFolderBtn = $('chooseAutoSaveFolderBtn');
|
||||
let appSettings = { autoSaveEnabled: false, autoSaveFolder: null };
|
||||
|
||||
function syncAutoSaveUi() {
|
||||
autoSaveFolderRow.style.display = autoSaveToggle.checked ? 'block' : 'none';
|
||||
autoSaveFolderPath.textContent = appSettings.autoSaveFolder || 'Kein Ordner ausgewählt';
|
||||
}
|
||||
|
||||
async function persistSettings() {
|
||||
appSettings.autoSaveEnabled = autoSaveToggle.checked;
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
await window.__TAURI__.core.invoke('save_settings', { settings: appSettings });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
autoSaveToggle.addEventListener('change', () => {
|
||||
syncAutoSaveUi();
|
||||
persistSettings();
|
||||
});
|
||||
|
||||
chooseAutoSaveFolderBtn.addEventListener('click', async () => {
|
||||
if (!isTauri()) return;
|
||||
try {
|
||||
const folder = await window.__TAURI__.core.invoke('pick_save_folder');
|
||||
if (folder) {
|
||||
appSettings.autoSaveFolder = folder;
|
||||
syncAutoSaveUi();
|
||||
persistSettings();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadSettings() {
|
||||
if (!isTauri()) { syncAutoSaveUi(); return; }
|
||||
try {
|
||||
const loaded = await window.__TAURI__.core.invoke('load_settings');
|
||||
if (loaded) appSettings = loaded;
|
||||
autoSaveToggle.checked = !!appSettings.autoSaveEnabled;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
syncAutoSaveUi();
|
||||
}
|
||||
|
||||
// ---------- Pipeline Ablauf ----------
|
||||
const STEP_IDS = ['denoise', 'cut', 'loudnorm'];
|
||||
const STEP_IDS = ['cut', 'denoise', 'loudnorm'];
|
||||
|
||||
function resetSteps() {
|
||||
for (const id of STEP_IDS) {
|
||||
|
|
@ -274,31 +215,8 @@
|
|||
}
|
||||
});
|
||||
|
||||
async function showResult(result) {
|
||||
function showResult(result) {
|
||||
const name = result.outputPath.split(/[\\/]/).pop();
|
||||
|
||||
// Automatisches Speichern: wenn aktiviert und ein Ordner gewählt wurde,
|
||||
// landet das Ergebnis sofort dort, ohne "Speichern unter" Dialog.
|
||||
if (appSettings.autoSaveEnabled && appSettings.autoSaveFolder) {
|
||||
try {
|
||||
const saved = await window.__TAURI__.core.invoke('auto_save_output', {
|
||||
sourcePath: result.outputPath,
|
||||
suggestedName: name,
|
||||
targetFolder: appSettings.autoSaveFolder
|
||||
});
|
||||
$('resultArea').innerHTML =
|
||||
'<div class="result-box"><span class="rb-name">' + name.replace(/</g, '<') + '</span></div>' +
|
||||
'<div class="status-ok" style="margin-top:8px;">Automatisch gespeichert unter: ' +
|
||||
String(saved).replace(/</g, '<') + '</div>';
|
||||
return;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// Bei einem Fehler beim automatischen Speichern (z.B. Ordner nicht mehr
|
||||
// vorhanden) auf den manuellen Weg zurückfallen, damit das fertige
|
||||
// Ergebnis nicht verloren geht.
|
||||
}
|
||||
}
|
||||
|
||||
$('resultArea').innerHTML =
|
||||
'<div class="result-box">' +
|
||||
'<span class="rb-name">' + name.replace(/</g, '<') + '</span>' +
|
||||
|
|
@ -386,6 +304,5 @@
|
|||
// ---------- Start ----------
|
||||
initProgressListener();
|
||||
checkTools();
|
||||
loadSettings();
|
||||
checkForUpdate(true);
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
nichts hochgeladen und keine Verbindung zu einem Server aufgebaut.
|
||||
</div>
|
||||
|
||||
<div id="toolWarning" class="tool-warning" style="display:none;">
|
||||
<strong>Werkzeuge fehlen.</strong>
|
||||
<span id="toolWarningText">Es konnten nicht alle benötigten Programme gefunden werden.</span>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div>
|
||||
<div class="panel">
|
||||
|
|
@ -81,28 +86,15 @@
|
|||
<option value="-14">-14 LUFS (Streaming, laut)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-row" style="margin-top:16px;">
|
||||
<input type="checkbox" id="autoSaveToggle">
|
||||
<label for="autoSaveToggle">Ergebnis automatisch speichern, ohne Dialog</label>
|
||||
</div>
|
||||
<div class="row" id="autoSaveFolderRow" style="margin-top:10px; display:none;">
|
||||
<div class="selected-file">
|
||||
<span class="sf-name" id="autoSaveFolderPath">Kein Ordner ausgewählt</span>
|
||||
<button class="secondary" type="button" id="chooseAutoSaveFolderBtn">Ordner wählen</button>
|
||||
</div>
|
||||
<div class="hint-small">Jedes fertige Video landet automatisch in diesem Ordner, ohne dass du jedes Mal einen Speicherort auswählen musst.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h2>Über die Werkzeuge</h2>
|
||||
<div class="hint-small">
|
||||
Schnitt: <strong>auto-editor</strong> (Public Domain). Entrauschen:
|
||||
<strong>DeepFilterNet</strong> (MIT/Apache 2.0). Klangveredelung &
|
||||
Lautstärke: <strong>FFmpeg</strong> (EQ, Kompressor, loudnorm Filter, LGPL/GPL).
|
||||
Alle drei laufen als eigenständige Programme neben NovaPhonic auf diesem
|
||||
Rechner, keines davon braucht eine Grafikkarte.
|
||||
<strong>DeepFilterNet</strong> (MIT/Apache 2.0). Lautstärke: <strong>FFmpeg</strong>
|
||||
loudnorm Filter (LGPL/GPL). Alle drei laufen als eigenständige Programme neben
|
||||
NovaPhonic auf diesem Rechner, keines davon braucht eine Grafikkarte.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -137,19 +129,19 @@
|
|||
<div class="panel">
|
||||
<h2>Ablauf</h2>
|
||||
<div class="pipeline-steps">
|
||||
<div class="pipeline-step" id="step-denoise">
|
||||
<span class="ps-dot"></span>
|
||||
<span class="ps-label">Rauschen entfernen</span>
|
||||
<span class="ps-detail" id="step-denoise-detail"></span>
|
||||
</div>
|
||||
<div class="pipeline-step" id="step-cut">
|
||||
<span class="ps-dot"></span>
|
||||
<span class="ps-label">Füllwörter & Stille schneiden</span>
|
||||
<span class="ps-detail" id="step-cut-detail"></span>
|
||||
</div>
|
||||
<div class="pipeline-step" id="step-denoise">
|
||||
<span class="ps-dot"></span>
|
||||
<span class="ps-label">Rauschen entfernen</span>
|
||||
<span class="ps-detail" id="step-denoise-detail"></span>
|
||||
</div>
|
||||
<div class="pipeline-step" id="step-loudnorm">
|
||||
<span class="ps-dot"></span>
|
||||
<span class="ps-label">Klang veredeln & Lautstärke angleichen</span>
|
||||
<span class="ps-label">Lautstärke angleichen</span>
|
||||
<span class="ps-detail" id="step-loudnorm-detail"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -174,11 +166,6 @@
|
|||
Veröffentlichung kurz ansehen.
|
||||
</div>
|
||||
|
||||
<div id="toolWarning" class="tool-warning" style="display:none;">
|
||||
<strong>Hinweis:</strong>
|
||||
<span id="toolWarningText">Es konnten nicht alle benötigten Programme gefunden werden.</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
|
|
|
|||
|
|
@ -331,12 +331,15 @@ button:disabled { opacity: 0.4; cursor: not-allowed; }
|
|||
.privacy-note strong { color: var(--accent-text); }
|
||||
|
||||
.tool-warning {
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
background: var(--danger-bg);
|
||||
border: 1px solid var(--danger);
|
||||
color: var(--text);
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.tool-warning strong { color: var(--danger); font-weight: 600; }
|
||||
.tool-warning strong { color: var(--danger); }
|
||||
|
||||
.update-banner {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "novaphonic",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"tauri": "tauri"
|
||||
|
|
@ -9,4 +9,3 @@
|
|||
"@tauri-apps/cli": "^2"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "novaphonic"
|
||||
version = "0.1.3"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "NovaPhonic, ein Tool von KI-WERKE zum automatischen Videoschnitt und zur Audio Veredelung"
|
||||
|
||||
|
|
@ -19,4 +19,3 @@ tauri-plugin-shell = "2"
|
|||
[[bin]]
|
||||
name = "novaphonic"
|
||||
path = "src/main.rs"
|
||||
|
||||
|
|
|
|||
|
|
@ -6,21 +6,18 @@
|
|||
// Dateipfade verarbeitet, nie Videoinhalte selbst in den Arbeitsspeicher der
|
||||
// Oberfläche geladen.
|
||||
//
|
||||
// Alle Zwischenschritte (geschnittenes Video, rohe/entrauschte Tonspur, usw.)
|
||||
// laufen in einem echten temporären Systemordner (std::env::temp_dir()), nicht im
|
||||
// Ordner der Eingabedatei. Grund: Bei einem echten Testlauf am 2026-08-19 lag das
|
||||
// Eingabevideo in einem Nextcloud-synchronisierten Ordner, der Sync-Client hat
|
||||
// neu entstehende Zwischen-Dateien sofort selbst kurz geöffnet, was zu "Zugriff
|
||||
// verweigert" Fehlern führte, sobald NovaPhonic dieselbe Datei gleich danach
|
||||
// weiterverarbeiten wollte. Der temporäre Arbeitsordner wird bei einem Fehler
|
||||
// sofort wieder vollständig gelöscht, bei Erfolg bleibt nur das fertige Ergebnis
|
||||
// bis zum Speichern über den "Speichern unter" Dialog liegen, siehe
|
||||
// `save_output_as`, das räumt danach ebenfalls auf.
|
||||
// Wichtiger Hinweis für den ersten echten Build: In dieser Entwicklungsumgebung
|
||||
// steht kein Rust Compiler zur Verfügung, dieser Code wurde also nicht kompiliert,
|
||||
// nur sorgfältig von Hand geprüft. Zwei Stellen bitte beim ersten Testlauf gezielt
|
||||
// gegenprüfen: 1) die genauen Kommandozeilen-Optionen von `deep-filter` (siehe
|
||||
// `deep-filter --help`, hier als `-o <Zieldatei>` angenommen), 2) ob `auto-editor`
|
||||
// den Flag `--no-open` in der installierten Version so kennt. Beides ist unten mit
|
||||
// Kommentaren markiert.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use tauri::{AppHandle, Emitter, Manager};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tauri_plugin_shell::process::CommandEvent;
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
|
||||
|
|
@ -50,118 +47,12 @@ fn save_output_as(source_path: String, suggested_name: String) -> Result<String,
|
|||
Some(dest) => {
|
||||
fs::copy(&source_path, &dest)
|
||||
.map_err(|e| format!("Konnte Datei nicht speichern: {e}"))?;
|
||||
cleanup_temp_work_dir(&source_path);
|
||||
Ok(dest.to_string_lossy().to_string())
|
||||
}
|
||||
None => Err("abgebrochen".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Löscht den temporären Arbeitsordner einer fertigen Ergebnisdatei, nachdem sie
|
||||
// erfolgreich an ihr endgültiges Ziel kopiert wurde. Sicherheitshalber nur, wenn
|
||||
// der Pfad wirklich im System-Temp-Ordner liegt und unserem eigenen Namensmuster
|
||||
// entspricht, damit hier niemals versehentlich ein echter Nutzerordner betroffen
|
||||
// sein kann.
|
||||
fn cleanup_temp_work_dir(source_path: &str) {
|
||||
let path = PathBuf::from(source_path);
|
||||
let Some(work_dir) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
let Some(folder_name) = work_dir.file_name().and_then(|n| n.to_str()) else {
|
||||
return;
|
||||
};
|
||||
if work_dir.starts_with(std::env::temp_dir()) && folder_name.starts_with("novaphonic-") {
|
||||
let _ = fs::remove_dir_all(work_dir);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Einstellungen (automatisches Standardspeicherziel) ----------
|
||||
// Wird als kleine JSON Datei im App Konfigurationsordner des Nutzers abgelegt
|
||||
// (z.B. unter Windows in AppData\Roaming\...), nicht im Temp-Ordner, damit sie
|
||||
// App-Neustarts übersteht. Betrifft ausschließlich diese Einstellung, niemals
|
||||
// Videoinhalte oder Zwischenergebnisse.
|
||||
#[derive(Serialize, Deserialize, Clone, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AppSettings {
|
||||
auto_save_enabled: bool,
|
||||
auto_save_folder: Option<String>,
|
||||
}
|
||||
|
||||
fn settings_path(app: &AppHandle) -> Result<PathBuf, String> {
|
||||
let dir = app
|
||||
.path()
|
||||
.app_config_dir()
|
||||
.map_err(|e| format!("Konnte Konfigurationsordner nicht ermitteln: {e}"))?;
|
||||
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||
Ok(dir.join("settings.json"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn load_settings(app: AppHandle) -> AppSettings {
|
||||
let Ok(path) = settings_path(&app) else {
|
||||
return AppSettings::default();
|
||||
};
|
||||
match fs::read_to_string(&path) {
|
||||
Ok(text) => serde_json::from_str(&text).unwrap_or_default(),
|
||||
Err(_) => AppSettings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> {
|
||||
let path = settings_path(&app)?;
|
||||
let text = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
|
||||
fs::write(&path, text).map_err(|e| format!("Konnte Einstellungen nicht speichern: {e}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn pick_save_folder() -> Option<String> {
|
||||
let folder = rfd::FileDialog::new().pick_folder()?;
|
||||
Some(folder.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
// Kopiert das fertige Ergebnis direkt in den vom Nutzer festgelegten Standard-
|
||||
// Ordner, ohne "Speichern unter" Dialog, und räumt danach wie gewohnt den
|
||||
// temporären Arbeitsordner auf. Bei einem Namenskonflikt wird die bestehende
|
||||
// Datei bewusst nicht überschrieben, sondern ein Zähler an den Dateinamen
|
||||
// angehängt, damit nie versehentlich ein älteres Ergebnis verloren geht.
|
||||
#[tauri::command]
|
||||
fn auto_save_output(
|
||||
source_path: String,
|
||||
suggested_name: String,
|
||||
target_folder: String,
|
||||
) -> Result<String, String> {
|
||||
let folder = PathBuf::from(&target_folder);
|
||||
if !folder.is_dir() {
|
||||
return Err(format!(
|
||||
"Zielordner nicht gefunden: {}",
|
||||
folder.display()
|
||||
));
|
||||
}
|
||||
let name_path = PathBuf::from(&suggested_name);
|
||||
let stem = name_path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("nova_ergebnis")
|
||||
.to_string();
|
||||
let ext = name_path
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("mp4")
|
||||
.to_string();
|
||||
|
||||
let mut dest = folder.join(&suggested_name);
|
||||
let mut counter = 1;
|
||||
while dest.exists() {
|
||||
dest = folder.join(format!("{stem} ({counter}).{ext}"));
|
||||
counter += 1;
|
||||
}
|
||||
|
||||
fs::copy(&source_path, &dest).map_err(|e| format!("Konnte Datei nicht speichern: {e}"))?;
|
||||
cleanup_temp_work_dir(&source_path);
|
||||
Ok(dest.to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
// Wichtig: Tauri spricht Sidecar Programme zur Laufzeit nur über ihren bloßen
|
||||
// Namen an (z.B. "auto-editor"), der Ordnerpfad "binaries/" wird ausschließlich
|
||||
// beim Einrichten in tauri.conf.json (externalBin) und in den Capabilities
|
||||
|
|
@ -295,58 +186,44 @@ async fn process_video(app: AppHandle, options: ProcessOptions) -> Result<Proces
|
|||
let stem = input
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("video")
|
||||
.to_string();
|
||||
let ext = input
|
||||
.extension()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("mp4")
|
||||
.to_string();
|
||||
.unwrap_or("video");
|
||||
let ext = input.extension().and_then(|s| s.to_str()).unwrap_or("mp4");
|
||||
let parent = input.parent().unwrap_or_else(|| Path::new("."));
|
||||
|
||||
// Eigener, garantiert eindeutiger Arbeitsordner im System-Temp-Verzeichnis,
|
||||
// pro Verarbeitungslauf neu, damit sich mehrere Läufe nie in die Quere kommen.
|
||||
let work_dir =
|
||||
std::env::temp_dir().join(format!("novaphonic-{stem}-{}", std::process::id()));
|
||||
fs::create_dir_all(&work_dir).map_err(|e| e.to_string())?;
|
||||
// ---------- 1) Schnitt (auto-editor) ----------
|
||||
emit_progress(&app, "cut", "running", Some("startet…".into()), None);
|
||||
let cut_path = parent.join(format!("{stem}_geschnitten.{ext}"));
|
||||
let margin = format!("{}sec", options.margin_seconds);
|
||||
run_sidecar(
|
||||
&app,
|
||||
"auto-editor",
|
||||
vec![
|
||||
input.to_string_lossy().to_string(),
|
||||
"--margin".into(),
|
||||
margin,
|
||||
// Bitte beim ersten Testlauf prüfen, ob dieses Flag in der installierten
|
||||
// auto-editor Version noch so heißt (verhindert, dass sich nach dem
|
||||
// Schneiden automatisch ein Videoplayer öffnet).
|
||||
"--no-open".into(),
|
||||
"-o".into(),
|
||||
cut_path.to_string_lossy().to_string(),
|
||||
],
|
||||
"cut",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
emit_progress(&app, "cut", "error", Some(e.clone()), None);
|
||||
e
|
||||
})?;
|
||||
emit_progress(&app, "cut", "done", Some("fertig".into()), None);
|
||||
|
||||
match run_pipeline(&app, &options, &input, &work_dir, &stem, &ext).await {
|
||||
Ok(final_path) => Ok(ProcessResult {
|
||||
output_path: final_path.to_string_lossy().to_string(),
|
||||
}),
|
||||
Err(e) => {
|
||||
// Bei einem Fehler bleibt nichts Halbfertiges im Temp-Ordner liegen.
|
||||
let _ = fs::remove_dir_all(&work_dir);
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut current = cut_path;
|
||||
|
||||
async fn run_pipeline(
|
||||
app: &AppHandle,
|
||||
options: &ProcessOptions,
|
||||
input: &Path,
|
||||
work_dir: &Path,
|
||||
stem: &str,
|
||||
ext: &str,
|
||||
) -> Result<PathBuf, String> {
|
||||
// ---------- 1) Entrauschen (DeepFilterNet), optional ----------
|
||||
// Bewusst VOR dem Schnitt statt danach (bis 2026-08-19 war das andersherum):
|
||||
// auto-editor erkennt Stille anhand einer festen Lautstärke-Schwelle
|
||||
// (Standard ca. -28 dB). Enthält die Aufnahme ein leises, aber durchgehendes
|
||||
// Hintergrundgeräusch (Raumbrummen, Lüfter), liegt das oft über dieser
|
||||
// Schwelle, auto-editor findet dann an keiner Stelle echte Stille und
|
||||
// schneidet nichts weg. Nutzerfeedback vom 2026-08-19: "Der Clip ist
|
||||
// durchgehend ungeschnitten, die natürlichen Sprechpausen sind noch
|
||||
// vollständig erhalten." Wenn DeepFilterNet das Hintergrundgeräusch schon
|
||||
// vor dem Schnitt entfernt, bleibt an den Pausen echte digitale Stille
|
||||
// übrig, und die Standard-Schwelle von auto-editor funktioniert wieder
|
||||
// zuverlässig, auch bei künftigen Aufnahmen mit anderem Grundrauschen,
|
||||
// ohne dass man die Schwelle manuell pro Video nachjustieren muss.
|
||||
let mut current = input.to_path_buf();
|
||||
// ---------- 2) Entrauschen (DeepFilterNet), optional ----------
|
||||
if options.denoise {
|
||||
emit_progress(app, "denoise", "running", Some("startet…".into()), None);
|
||||
let raw_audio = work_dir.join(format!("{stem}_audio.wav"));
|
||||
let denoised_audio = work_dir.join(format!("{stem}_entrauscht.wav"));
|
||||
emit_progress(&app, "denoise", "running", Some("startet…".into()), None);
|
||||
let raw_audio = parent.join(format!("{stem}_audio.wav"));
|
||||
let denoised_audio = parent.join(format!("{stem}_entrauscht.wav"));
|
||||
|
||||
run_sidecar(
|
||||
&app,
|
||||
|
|
@ -372,29 +249,18 @@ async fn run_pipeline(
|
|||
e
|
||||
})?;
|
||||
|
||||
// deep-filter erwartet bei -o einen AusgabeORDNER, keine Zieldatei, und
|
||||
// schreibt dort eine Datei mit demselben Namen wie die Eingabedatei hinein
|
||||
// (per echtem Testlauf bestätigt, 2026-08-19). Deshalb erst in einen eigenen
|
||||
// Zwischenordner schreiben lassen und die entstandene Datei danach auf den
|
||||
// gewünschten Namen umbenennen.
|
||||
let denoise_out_dir = work_dir.join(format!("{stem}_denoise_tmp"));
|
||||
fs::create_dir_all(&denoise_out_dir).map_err(|e| e.to_string())?;
|
||||
// Achtung, bitte gegen `deep-filter --help` prüfen: hier wird eine exakte
|
||||
// Ausgabedatei per -o angenommen. Manche deep-filter Versionen erwarten
|
||||
// stattdessen einen Ausgabeordner (z.B. -o <Ordner>), in dem eine Datei
|
||||
// mit demselben Namen wie die Eingabe erzeugt wird. Falls das der Fall
|
||||
// ist, muss diese Stelle entsprechend angepasst werden.
|
||||
run_sidecar(
|
||||
&app,
|
||||
"deep-filter",
|
||||
vec![
|
||||
raw_audio.to_string_lossy().to_string(),
|
||||
// Begrenzt, wie stark DeepFilterNet maximal absenken darf. Ohne
|
||||
// dieses Limit greift das Modell an manchen Stellen so stark ein,
|
||||
// dass mit dem Rauschen auch ein Teil der hohen Frequenzen der
|
||||
// Stimme mit entfernt wird, das klingt dann dumpf und mattiert
|
||||
// statt klar. Nutzerfeedback vom 2026-08-19: Klang zu dumpf im
|
||||
// Vergleich zu Auphonic. 30 dB ist ein vorsichtiger Mittelweg,
|
||||
// lässt sich bei Bedarf noch weiter anpassen.
|
||||
"--atten-lim-db".into(),
|
||||
"30".into(),
|
||||
"-o".into(),
|
||||
denoise_out_dir.to_string_lossy().to_string(),
|
||||
denoised_audio.to_string_lossy().to_string(),
|
||||
],
|
||||
"denoise",
|
||||
)
|
||||
|
|
@ -403,24 +269,8 @@ async fn run_pipeline(
|
|||
emit_progress(&app, "denoise", "error", Some(e.clone()), None);
|
||||
e
|
||||
})?;
|
||||
let denoise_result = denoise_out_dir.join(
|
||||
raw_audio
|
||||
.file_name()
|
||||
.ok_or_else(|| "Ungültiger Audio Dateiname.".to_string())?,
|
||||
);
|
||||
fs::rename(&denoise_result, &denoised_audio)
|
||||
.or_else(|_| fs::copy(&denoise_result, &denoised_audio).map(|_| ()))
|
||||
.map_err(|e| {
|
||||
let msg = format!(
|
||||
"Entrauschte Datei nicht am erwarteten Ort {} gefunden: {e}",
|
||||
denoise_result.display()
|
||||
);
|
||||
emit_progress(&app, "denoise", "error", Some(msg.clone()), None);
|
||||
msg
|
||||
})?;
|
||||
let _ = fs::remove_dir_all(&denoise_out_dir);
|
||||
|
||||
let remuxed = work_dir.join(format!("{stem}_entrauscht_video.{ext}"));
|
||||
let remuxed = parent.join(format!("{stem}_entrauscht_video.{ext}"));
|
||||
run_sidecar(
|
||||
&app,
|
||||
"ffmpeg",
|
||||
|
|
@ -449,89 +299,22 @@ async fn run_pipeline(
|
|||
|
||||
let _ = fs::remove_file(&raw_audio);
|
||||
let _ = fs::remove_file(&denoised_audio);
|
||||
let _ = fs::remove_file(¤t);
|
||||
current = remuxed;
|
||||
emit_progress(&app, "denoise", "done", Some("fertig".into()), None);
|
||||
} else {
|
||||
emit_progress(&app, "denoise", "done", Some("übersprungen".into()), None);
|
||||
}
|
||||
|
||||
// ---------- 2) Schnitt (auto-editor) ----------
|
||||
emit_progress(app, "cut", "running", Some("startet…".into()), None);
|
||||
let cut_path = work_dir.join(format!("{stem}_geschnitten.{ext}"));
|
||||
let margin = format!("{}sec", options.margin_seconds);
|
||||
// Läuft jetzt auf `current`, also auf der schon entrauschten Datei (falls
|
||||
// Entrauschen aktiv war) statt auf der Originaldatei, siehe Kommentar oben.
|
||||
let cut_input = current.clone();
|
||||
run_sidecar(
|
||||
&app,
|
||||
"auto-editor",
|
||||
vec![
|
||||
cut_input.to_string_lossy().to_string(),
|
||||
"--margin".into(),
|
||||
margin,
|
||||
// Bitte beim ersten Testlauf prüfen, ob dieses Flag in der installierten
|
||||
// auto-editor Version noch so heißt (verhindert, dass sich nach dem
|
||||
// Schneiden automatisch ein Videoplayer öffnet).
|
||||
"--no-open".into(),
|
||||
// Weicher Übergang statt hartem Jump Cut: auto-editor blendet Bild und
|
||||
// Ton an jedem Schnittpunkt kurz ineinander (0,15s), statt hart zu
|
||||
// schneiden. Die zweite Zahl (0,05s) sorgt dafür, dass auch sehr kurze
|
||||
// Schnitte (z.B. ein einzelnes rausgeschnittenes Füllwort) noch eine
|
||||
// Überblendung bekommen, auto-editor würde sonst nur Schnitte ab 1
|
||||
// Sekunde Länge überblenden. Nutzerfeedback vom 2026-08-19: harte,
|
||||
// unruhige Schnittpunkte im Bild. Wichtig: die Zeitangaben brauchen
|
||||
// zwingend eine Einheit ("sec"), eine nackte Kommazahl wie "0.15"
|
||||
// lehnt auto-editor mit "Time format expects an integer" ab (per
|
||||
// echtem Testlauf am 2026-08-19 bestätigt).
|
||||
"--transition".into(),
|
||||
"dissolve:0.15sec:0.05sec".into(),
|
||||
"-o".into(),
|
||||
cut_path.to_string_lossy().to_string(),
|
||||
],
|
||||
"cut",
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
emit_progress(&app, "cut", "error", Some(e.clone()), None);
|
||||
e
|
||||
})?;
|
||||
// Zwischenergebnis aus Schritt 1 aufräumen, aber niemals die Originaldatei
|
||||
// des Nutzers löschen (die liegt außerhalb des Temp-Ordners, falls
|
||||
// Entrauschen deaktiviert war und `cut_input` noch auf sie zeigt).
|
||||
if cut_input.as_path() != input {
|
||||
let _ = fs::remove_file(&cut_input);
|
||||
}
|
||||
current = cut_path;
|
||||
emit_progress(&app, "cut", "done", Some("fertig".into()), None);
|
||||
|
||||
// ---------- 3) Klangveredelung & Lautstärke (FFmpeg), optional ----------
|
||||
let final_path = work_dir.join(format!("{stem}_nova.{ext}"));
|
||||
// ---------- 3) Lautstärke & Klangbalance (FFmpeg loudnorm), optional ----------
|
||||
let final_path = parent.join(format!("{stem}_nova.{ext}"));
|
||||
if options.loudnorm {
|
||||
emit_progress(&app, "loudnorm", "running", Some("startet…".into()), None);
|
||||
// Bewusst Einzelpass-Loudnorm statt klassischem Zweipass-Verfahren: kein
|
||||
// Parsen von FFmpeg Messwerten aus stderr nötig, dadurch robuster. Etwas
|
||||
// weniger präzise als Zweipass, für Sprachaufnahmen in der Praxis meist
|
||||
// ausreichend. Lässt sich als Ausbaustufe später nachrüsten.
|
||||
//
|
||||
// Vor loudnorm zusätzlich vier Klangschritte, weil reines loudnorm nur die
|
||||
// Lautheit regelt, aber keine Klangfarbe. Stand 2026-08-19, zweite
|
||||
// Nachschärfung: nach dem ersten Versuch (Low/High Shelf) war der Klang
|
||||
// laut Nutzerfeedback immer noch "bassbetont und im mittleren Bereich
|
||||
// etwas topfig". Deshalb jetzt: 1) echter Hochpassfilter bei 90 Hz statt
|
||||
// eines sanften Shelfs, entfernt Trittschall/Dröhnen konsequenter (Wert
|
||||
// stammt aus einer externen Analyse-Empfehlung, die JJ eingeholt hat).
|
||||
// 2) gezielte, moderate Absenkung um 350 Hz, genau der Bereich, der für
|
||||
// den "topfigen" Klang bei nah sitzenden Lavaliermikrofonen typisch ist,
|
||||
// das hatte die externe Empfehlung noch nicht abgedeckt. 3) ein
|
||||
// schmalerer Presence Boost um 3500 Hz statt eines breiten High Shelf ab
|
||||
// 4000 Hz, wirkt natürlicher (ebenfalls aus der externen Empfehlung
|
||||
// übernommen). 4) unveränderter, leichter Kompressor für gleichmäßigere
|
||||
// Aussteuerung. Alle Werte bewusst moderat, um keine unnatürlichen oder
|
||||
// metallischen Artefakte zu erzeugen.
|
||||
let filter = format!(
|
||||
"highpass=f=90,equalizer=f=350:t=q:w=1.5:g=-3,equalizer=f=3500:t=q:w=1.2:g=3.5,acompressor=threshold=0.1:ratio=2.5:attack=15:release=200:makeup=1.5,loudnorm=I={}:TP=-1.5:LRA=11",
|
||||
options.loudnorm_target
|
||||
);
|
||||
let filter = format!("loudnorm=I={}:TP=-1.5:LRA=11", options.loudnorm_target);
|
||||
run_sidecar(
|
||||
&app,
|
||||
"ffmpeg",
|
||||
|
|
@ -560,7 +343,9 @@ async fn run_pipeline(
|
|||
emit_progress(&app, "loudnorm", "done", Some("übersprungen".into()), None);
|
||||
}
|
||||
|
||||
Ok(final_path)
|
||||
Ok(ProcessResult {
|
||||
output_path: final_path.to_string_lossy().to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
|
@ -572,11 +357,7 @@ fn main() {
|
|||
pick_video_file,
|
||||
save_output_as,
|
||||
check_tools,
|
||||
process_video,
|
||||
load_settings,
|
||||
save_settings,
|
||||
pick_save_folder,
|
||||
auto_save_output
|
||||
process_video
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("Fehler beim Starten der Tauri Anwendung");
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "NovaPhonic",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.kiwerkepro.novaphonic",
|
||||
"build": {
|
||||
"frontendDist": "../../app"
|
||||
|
|
@ -44,11 +44,10 @@
|
|||
},
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEE1NzVEMkZBOEM1NDhGRApSV1Q5U01Xb0wxMVhDaENFOHRPV29oeUxkdmEzYkQ1NTV6NHE0NHUwbERYeUliRkErVXBoU3AvTwo=",
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDMxOUJBODQxRTFFMzNFRjcKUldUM1B1UGhRYWliTVIySHBRUno3R2NvSC9SV2hzV1BXVFp3Rit5eThhSVBvdmxJM1k3ZzBLV1oK",
|
||||
"endpoints": [
|
||||
"https://github.com/kiwerkepro-org/novaphonic/releases/latest/download/latest.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue