Pipeline Reihenfolge umgestellt (erst Entrauschen, dann Schnitt), Klangkette nachgeschaerft
This commit is contained in:
parent
85b9e785bc
commit
ee23e4c146
3 changed files with 91 additions and 63 deletions
|
|
@ -149,7 +149,7 @@
|
||||||
syncLoudnormOptions();
|
syncLoudnormOptions();
|
||||||
|
|
||||||
// ---------- Pipeline Ablauf ----------
|
// ---------- Pipeline Ablauf ----------
|
||||||
const STEP_IDS = ['cut', 'denoise', 'loudnorm'];
|
const STEP_IDS = ['denoise', 'cut', 'loudnorm'];
|
||||||
|
|
||||||
function resetSteps() {
|
function resetSteps() {
|
||||||
for (const id of STEP_IDS) {
|
for (const id of STEP_IDS) {
|
||||||
|
|
|
||||||
|
|
@ -87,9 +87,10 @@
|
||||||
<h2>Über die Werkzeuge</h2>
|
<h2>Über die Werkzeuge</h2>
|
||||||
<div class="hint-small">
|
<div class="hint-small">
|
||||||
Schnitt: <strong>auto-editor</strong> (Public Domain). Entrauschen:
|
Schnitt: <strong>auto-editor</strong> (Public Domain). Entrauschen:
|
||||||
<strong>DeepFilterNet</strong> (MIT/Apache 2.0). Lautstärke: <strong>FFmpeg</strong>
|
<strong>DeepFilterNet</strong> (MIT/Apache 2.0). Klangveredelung &
|
||||||
loudnorm Filter (LGPL/GPL). Alle drei laufen als eigenständige Programme neben
|
Lautstärke: <strong>FFmpeg</strong> (EQ, Kompressor, loudnorm Filter, LGPL/GPL).
|
||||||
NovaPhonic auf diesem Rechner, keines davon braucht eine Grafikkarte.
|
Alle drei laufen als eigenständige Programme neben NovaPhonic auf diesem
|
||||||
|
Rechner, keines davon braucht eine Grafikkarte.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -124,19 +125,19 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<h2>Ablauf</h2>
|
<h2>Ablauf</h2>
|
||||||
<div class="pipeline-steps">
|
<div class="pipeline-steps">
|
||||||
<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">
|
<div class="pipeline-step" id="step-denoise">
|
||||||
<span class="ps-dot"></span>
|
<span class="ps-dot"></span>
|
||||||
<span class="ps-label">Rauschen entfernen</span>
|
<span class="ps-label">Rauschen entfernen</span>
|
||||||
<span class="ps-detail" id="step-denoise-detail"></span>
|
<span class="ps-detail" id="step-denoise-detail"></span>
|
||||||
</div>
|
</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-loudnorm">
|
<div class="pipeline-step" id="step-loudnorm">
|
||||||
<span class="ps-dot"></span>
|
<span class="ps-dot"></span>
|
||||||
<span class="ps-label">Lautstärke angleichen</span>
|
<span class="ps-label">Klang veredeln & Lautstärke angleichen</span>
|
||||||
<span class="ps-detail" id="step-loudnorm-detail"></span>
|
<span class="ps-detail" id="step-loudnorm-detail"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -242,50 +242,22 @@ async fn run_pipeline(
|
||||||
stem: &str,
|
stem: &str,
|
||||||
ext: &str,
|
ext: &str,
|
||||||
) -> Result<PathBuf, String> {
|
) -> Result<PathBuf, String> {
|
||||||
// ---------- 1) Schnitt (auto-editor) ----------
|
// ---------- 1) Entrauschen (DeepFilterNet), optional ----------
|
||||||
emit_progress(app, "cut", "running", Some("startet…".into()), None);
|
// Bewusst VOR dem Schnitt statt danach (bis 2026-08-19 war das andersherum):
|
||||||
let cut_path = work_dir.join(format!("{stem}_geschnitten.{ext}"));
|
// auto-editor erkennt Stille anhand einer festen Lautstärke-Schwelle
|
||||||
let margin = format!("{}sec", options.margin_seconds);
|
// (Standard ca. -28 dB). Enthält die Aufnahme ein leises, aber durchgehendes
|
||||||
run_sidecar(
|
// Hintergrundgeräusch (Raumbrummen, Lüfter), liegt das oft über dieser
|
||||||
&app,
|
// Schwelle, auto-editor findet dann an keiner Stelle echte Stille und
|
||||||
"auto-editor",
|
// schneidet nichts weg. Nutzerfeedback vom 2026-08-19: "Der Clip ist
|
||||||
vec![
|
// durchgehend ungeschnitten, die natürlichen Sprechpausen sind noch
|
||||||
input.to_string_lossy().to_string(),
|
// vollständig erhalten." Wenn DeepFilterNet das Hintergrundgeräusch schon
|
||||||
"--margin".into(),
|
// vor dem Schnitt entfernt, bleibt an den Pausen echte digitale Stille
|
||||||
margin,
|
// übrig, und die Standard-Schwelle von auto-editor funktioniert wieder
|
||||||
// Bitte beim ersten Testlauf prüfen, ob dieses Flag in der installierten
|
// zuverlässig, auch bei künftigen Aufnahmen mit anderem Grundrauschen,
|
||||||
// auto-editor Version noch so heißt (verhindert, dass sich nach dem
|
// ohne dass man die Schwelle manuell pro Video nachjustieren muss.
|
||||||
// Schneiden automatisch ein Videoplayer öffnet).
|
let mut current = input.to_path_buf();
|
||||||
"--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
|
|
||||||
})?;
|
|
||||||
emit_progress(&app, "cut", "done", Some("fertig".into()), None);
|
|
||||||
|
|
||||||
let mut current = cut_path;
|
|
||||||
|
|
||||||
// ---------- 2) Entrauschen (DeepFilterNet), optional ----------
|
|
||||||
if options.denoise {
|
if options.denoise {
|
||||||
emit_progress(&app, "denoise", "running", Some("startet…".into()), None);
|
emit_progress(app, "denoise", "running", Some("startet…".into()), None);
|
||||||
let raw_audio = work_dir.join(format!("{stem}_audio.wav"));
|
let raw_audio = work_dir.join(format!("{stem}_audio.wav"));
|
||||||
let denoised_audio = work_dir.join(format!("{stem}_entrauscht.wav"));
|
let denoised_audio = work_dir.join(format!("{stem}_entrauscht.wav"));
|
||||||
|
|
||||||
|
|
@ -390,13 +362,61 @@ async fn run_pipeline(
|
||||||
|
|
||||||
let _ = fs::remove_file(&raw_audio);
|
let _ = fs::remove_file(&raw_audio);
|
||||||
let _ = fs::remove_file(&denoised_audio);
|
let _ = fs::remove_file(&denoised_audio);
|
||||||
let _ = fs::remove_file(¤t);
|
|
||||||
current = remuxed;
|
current = remuxed;
|
||||||
emit_progress(&app, "denoise", "done", Some("fertig".into()), None);
|
emit_progress(&app, "denoise", "done", Some("fertig".into()), None);
|
||||||
} else {
|
} else {
|
||||||
emit_progress(&app, "denoise", "done", Some("übersprungen".into()), None);
|
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 ----------
|
// ---------- 3) Klangveredelung & Lautstärke (FFmpeg), optional ----------
|
||||||
let final_path = work_dir.join(format!("{stem}_nova.{ext}"));
|
let final_path = work_dir.join(format!("{stem}_nova.{ext}"));
|
||||||
if options.loudnorm {
|
if options.loudnorm {
|
||||||
|
|
@ -406,16 +426,23 @@ async fn run_pipeline(
|
||||||
// weniger präzise als Zweipass, für Sprachaufnahmen in der Praxis meist
|
// weniger präzise als Zweipass, für Sprachaufnahmen in der Praxis meist
|
||||||
// ausreichend. Lässt sich als Ausbaustufe später nachrüsten.
|
// ausreichend. Lässt sich als Ausbaustufe später nachrüsten.
|
||||||
//
|
//
|
||||||
// Vor loudnorm zusätzlich drei Klangschritte, weil reines loudnorm nur die
|
// Vor loudnorm zusätzlich vier Klangschritte, weil reines loudnorm nur die
|
||||||
// Lautheit regelt, aber keine Klangfarbe: erst ein sanfter Low Shelf um
|
// Lautheit regelt, aber keine Klangfarbe. Stand 2026-08-19, zweite
|
||||||
// 180 Hz, der das dumpfe, wummernde Volumen aus der Stimme nimmt, dann ein
|
// Nachschärfung: nach dem ersten Versuch (Low/High Shelf) war der Klang
|
||||||
// sanfter High Shelf ab 4000 Hz für mehr Klarheit und Präsenz (das war der
|
// laut Nutzerfeedback immer noch "bassbetont und im mittleren Bereich
|
||||||
// Haupt-Unterschied zu Auphonic im Nutzerfeedback vom 2026-08-19), und ein
|
// etwas topfig". Deshalb jetzt: 1) echter Hochpassfilter bei 90 Hz statt
|
||||||
// leichter Kompressor, der die Stimme etwas durchsetzungsfähiger macht und
|
// eines sanften Shelfs, entfernt Trittschall/Dröhnen konsequenter (Wert
|
||||||
// kleine Lautstärkeschwankungen ausgleicht. Alle Werte bewusst moderat
|
// stammt aus einer externen Analyse-Empfehlung, die JJ eingeholt hat).
|
||||||
// gewählt, um keine unnatürlichen oder metallischen Artefakte zu erzeugen.
|
// 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!(
|
let filter = format!(
|
||||||
"bass=frequency=180:gain=-4:width=0.7,treble=frequency=4000:gain=3.5:width=0.6,acompressor=threshold=0.1:ratio=2.5:attack=15:release=200:makeup=1.5,loudnorm=I={}:TP=-1.5:LRA=11",
|
"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
|
options.loudnorm_target
|
||||||
);
|
);
|
||||||
run_sidecar(
|
run_sidecar(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue