recorder.py aktualisiert
This commit is contained in:
+42
-49
@@ -39,6 +39,10 @@ RECORDINGS_DIR = Path(
|
|||||||
os.environ.get("RECORDINGS_DIR", "/recordings")
|
os.environ.get("RECORDINGS_DIR", "/recordings")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
FINISHED_DIR = Path(
|
||||||
|
os.environ.get("FINISHED_DIR", "/finished")
|
||||||
|
)
|
||||||
|
|
||||||
WEB_PORT = int(
|
WEB_PORT = int(
|
||||||
os.environ.get("WEB_PORT", "8080")
|
os.environ.get("WEB_PORT", "8080")
|
||||||
)
|
)
|
||||||
@@ -133,8 +137,8 @@ def format_duration(seconds):
|
|||||||
return f"{minutes}:{seconds:02d}"
|
return f"{minutes}:{seconds:02d}"
|
||||||
|
|
||||||
|
|
||||||
def get_duration(filename):
|
def get_duration(filename, directory=RECORDINGS_DIR):
|
||||||
path = RECORDINGS_DIR / filename
|
path = directory / filename
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@@ -285,55 +289,54 @@ def start_recording():
|
|||||||
|
|
||||||
|
|
||||||
def normalize_recording(filename):
|
def normalize_recording(filename):
|
||||||
"""Normalize a completed MP3 and replace it only after success."""
|
"""Normalize a completed MP3 in place in RECORDINGS_DIR."""
|
||||||
if not NORMALIZE:
|
|
||||||
return
|
|
||||||
|
|
||||||
source = RECORDINGS_DIR / filename
|
source = RECORDINGS_DIR / filename
|
||||||
if not source.exists():
|
if not source.exists():
|
||||||
return
|
return False
|
||||||
|
|
||||||
|
if not NORMALIZE:
|
||||||
|
print(f"[normalize] disabled: {filename}", flush=True)
|
||||||
|
return True
|
||||||
|
|
||||||
temp = RECORDINGS_DIR / (filename + ".normalizing.mp3")
|
temp = RECORDINGS_DIR / (filename + ".normalizing.mp3")
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg", "-hide_banner", "-loglevel", "warning", "-y",
|
||||||
"-hide_banner",
|
|
||||||
"-loglevel", "warning",
|
|
||||||
"-y",
|
|
||||||
"-i", str(source),
|
"-i", str(source),
|
||||||
"-af",
|
"-af", (
|
||||||
f"loudnorm=I={NORMALIZE_TARGET}:TP={NORMALIZE_TRUE_PEAK}:LRA={NORMALIZE_LRA}",
|
f"loudnorm=I={NORMALIZE_TARGET}:"
|
||||||
|
f"TP={NORMALIZE_TRUE_PEAK}:"
|
||||||
|
f"LRA={NORMALIZE_LRA}"
|
||||||
|
),
|
||||||
"-c:a", "libmp3lame",
|
"-c:a", "libmp3lame",
|
||||||
"-b:a", "128k",
|
"-b:a", "128k",
|
||||||
str(temp),
|
str(temp),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
print(f"[normalize] starting: {filename}", flush=True)
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
stdout=subprocess.DEVNULL,
|
capture_output=True,
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
text=True,
|
text=True,
|
||||||
check=False,
|
timeout=1800,
|
||||||
)
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
if result.returncode != 0 or not temp.exists() or temp.stat().st_size == 0:
|
|
||||||
print(
|
print(
|
||||||
f"[normalize] failed for {filename}: "
|
f"[normalize] ffmpeg failed ({result.returncode}): "
|
||||||
f"{result.stderr.strip()[-1000:]}",
|
f"{result.stderr[-2000:]}",
|
||||||
flush=True,
|
flush=True,
|
||||||
)
|
)
|
||||||
temp.unlink(missing_ok=True)
|
temp.unlink(missing_ok=True)
|
||||||
return
|
return False
|
||||||
|
|
||||||
# Only replace the original after a complete successful conversion.
|
|
||||||
temp.replace(source)
|
temp.replace(source)
|
||||||
print(f"[normalize] completed: {filename}", flush=True)
|
print(f"[normalize] completed: {filename}", flush=True)
|
||||||
|
return True
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
print(f"[normalize] exception for {filename}: {exc}", flush=True)
|
print(f"[normalize] error: {exc}", flush=True)
|
||||||
temp.unlink(missing_ok=True)
|
temp.unlink(missing_ok=True)
|
||||||
|
return False
|
||||||
|
|
||||||
def stop_recording():
|
def stop_recording():
|
||||||
global ffmpeg_process
|
global ffmpeg_process
|
||||||
@@ -378,10 +381,9 @@ def stop_recording():
|
|||||||
recording_filename = None
|
recording_filename = None
|
||||||
recording_started = None
|
recording_started = None
|
||||||
|
|
||||||
# The live recording is now complete. Normalize it before it appears
|
# The live recording is now complete. Normalize it in RECORDINGS_DIR.
|
||||||
# as the finished recording in the web UI.
|
if filename:
|
||||||
if filename and NORMALIZE:
|
normalizing_filename = filename if NORMALIZE else None
|
||||||
normalizing_filename = filename
|
|
||||||
try:
|
try:
|
||||||
normalize_recording(filename)
|
normalize_recording(filename)
|
||||||
finally:
|
finally:
|
||||||
@@ -763,14 +765,14 @@ a.button:hover {
|
|||||||
|
|
||||||
<td>
|
<td>
|
||||||
<audio controls preload="none">
|
<audio controls preload="none">
|
||||||
<source src="/recordings/{{ item.filename }}"
|
<source src="/finished/{{ item.filename }}"
|
||||||
type="audio/mpeg">
|
type="audio/mpeg">
|
||||||
</audio>
|
</audio>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<a class="button"
|
<a class="button"
|
||||||
href="/recordings/{{ item.filename }}"
|
href="/finished/{{ item.filename }}"
|
||||||
download>
|
download>
|
||||||
Download
|
Download
|
||||||
</a>
|
</a>
|
||||||
@@ -848,36 +850,22 @@ def render_index():
|
|||||||
|
|
||||||
files = []
|
files = []
|
||||||
|
|
||||||
RECORDINGS_DIR.mkdir(
|
FINISHED_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
parents=True,
|
|
||||||
exist_ok=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
for path in RECORDINGS_DIR.glob("*.mp3"):
|
for path in FINISHED_DIR.glob("*.mp3"):
|
||||||
try:
|
try:
|
||||||
stat = path.stat()
|
stat = path.stat()
|
||||||
|
|
||||||
# Skip the currently active file from the finished list.
|
|
||||||
if path.name == recording_filename:
|
|
||||||
continue
|
|
||||||
|
|
||||||
files.append({
|
files.append({
|
||||||
"filename": path.name,
|
"filename": path.name,
|
||||||
"datetime": format_datetime(stat.st_mtime),
|
"datetime": format_datetime(stat.st_mtime),
|
||||||
"size": format_size(stat.st_size),
|
"size": format_size(stat.st_size),
|
||||||
"duration": format_duration(
|
"duration": format_duration(get_duration(path.name, FINISHED_DIR)),
|
||||||
get_duration(path.name)
|
|
||||||
),
|
|
||||||
"mtime": stat.st_mtime,
|
"mtime": stat.st_mtime,
|
||||||
})
|
})
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
files.sort(
|
files.sort(key=lambda item: item["mtime"], reverse=True)
|
||||||
key=lambda item: item["mtime"],
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
with state_lock:
|
with state_lock:
|
||||||
process = ffmpeg_process
|
process = ffmpeg_process
|
||||||
@@ -926,6 +914,11 @@ def api_status():
|
|||||||
return render_index()
|
return render_index()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/finished/<path:filename>")
|
||||||
|
def finished(filename):
|
||||||
|
return send_from_directory(FINISHED_DIR, filename, as_attachment=False)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/recordings/<path:filename>")
|
@app.route("/recordings/<path:filename>")
|
||||||
def recording(filename):
|
def recording(filename):
|
||||||
path = RECORDINGS_DIR / filename
|
path = RECORDINGS_DIR / filename
|
||||||
|
|||||||
Reference in New Issue
Block a user