fix: separate analyses dir so caching works with read-only recordings mount

The Docker web container mounts ./recordings as :ro, causing every cache
write to fail silently (PermissionError swallowed by bare except).

Fix: add --analyses-dir flag (default: <recordings>/analyses for local runs).
docker-compose.yml adds ./analyses:/analyses (writable) and passes
--analyses-dir /analyses to web.py. Cache write failures now print a
warning instead of being swallowed silently.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 23:29:09 +02:00
parent b6b328dfb8
commit 4aea07ae40
3 changed files with 42 additions and 27 deletions
+32 -21
View File
@@ -207,19 +207,18 @@ def analyze_flac(path: Path, window_samples: int = WINDOW_SAMPLES,
# Analysis cache helpers
# ---------------------------------------------------------------------------
def _analysis_cache_path(base: Path, audio_path: Path) -> Path:
rel = audio_path.relative_to(base)
return base / 'analyses' / rel.parent / (rel.name + '.analysis.json')
def _analysis_cache_path(analyses_base: Path, recordings_base: Path, audio_path: Path) -> Path:
rel = audio_path.relative_to(recordings_base)
return analyses_base / rel.parent / (rel.name + '.analysis.json')
def prune_orphan_analyses(base: Path):
analyses_dir = base / 'analyses'
if not analyses_dir.exists():
def prune_orphan_analyses(analyses_base: Path, recordings_base: Path):
if not analyses_base.exists():
return
removed = 0
for cache in analyses_dir.rglob('*.analysis.json'):
rel = cache.relative_to(analyses_dir)
audio_path = base / rel.parent / rel.name[:-len('.analysis.json')]
for cache in analyses_base.rglob('*.analysis.json'):
rel = cache.relative_to(analyses_base)
audio_path = recordings_base / rel.parent / rel.name[:-len('.analysis.json')]
if not audio_path.exists():
try:
cache.unlink()
@@ -283,6 +282,7 @@ def list_files(recordings_dir: str):
class _Handler(BaseHTTPRequestHandler):
recordings_dir: str = 'recordings'
analyses_dir: str = 'recordings/analyses'
threshold: float = LOUD_THRESHOLD
min_gap: float = MIN_GAP_SECONDS
@@ -358,8 +358,9 @@ class _Handler(BaseHTTPRequestHandler):
except Exception:
pass
base = Path(self.recordings_dir).resolve()
cache_path = _analysis_cache_path(base, path)
recordings_base = Path(self.recordings_dir).resolve()
analyses_base = Path(self.analyses_dir).resolve()
cache_path = _analysis_cache_path(analyses_base, recordings_base, path)
try:
cached = json.loads(cache_path.read_text('utf-8'))
if cached.get('threshold') == threshold and cached.get('min_gap') == min_gap:
@@ -386,8 +387,8 @@ class _Handler(BaseHTTPRequestHandler):
tmp = cache_path.with_suffix('.tmp')
tmp.write_text(json.dumps({'threshold': threshold, 'min_gap': min_gap, 'result': result}), 'utf-8')
os.replace(tmp, cache_path)
except Exception:
pass
except Exception as e:
print(f'Warning: could not write analysis cache {cache_path}: {e}', flush=True)
self._send(200, json.dumps(result).encode('utf-8'), 'application/json')
@@ -512,7 +513,11 @@ class _Handler(BaseHTTPRequestHandler):
return
try:
_analysis_cache_path(Path(self.recordings_dir).resolve(), path).unlink()
_analysis_cache_path(
Path(self.analyses_dir).resolve(),
Path(self.recordings_dir).resolve(),
path,
).unlink()
except Exception:
pass
@@ -1637,25 +1642,31 @@ def main():
help='Bind address (default: 0.0.0.0)')
parser.add_argument('--threshold', type=float, default=LOUD_THRESHOLD,
help=f'RMS loudness threshold 01 (default: {LOUD_THRESHOLD})')
parser.add_argument('--min-gap', type=float, default=MIN_GAP_SECONDS,
parser.add_argument('--min-gap', type=float, default=MIN_GAP_SECONDS,
help=f'Seconds gap for merging loud sections (default: {MIN_GAP_SECONDS})')
parser.add_argument('--analyses-dir', default=None,
help='Directory for analysis cache files (default: <recordings-dir>/analyses)')
args = parser.parse_args()
rec_dir = Path(args.dir)
if not rec_dir.exists():
print(f"Warning: recordings directory '{args.dir}' does not exist yet.")
rec_dir = Path(args.dir).resolve()
analyses_dir = Path(args.analyses_dir).resolve() if args.analyses_dir else rec_dir / 'analyses'
prune_orphan_analyses(rec_dir.resolve())
if not rec_dir.exists():
print(f"Warning: recordings directory '{rec_dir}' does not exist yet.")
prune_orphan_analyses(analyses_dir, rec_dir)
class Handler(_Handler):
recordings_dir = str(rec_dir.resolve())
recordings_dir = str(rec_dir)
analyses_dir = str(analyses_dir)
threshold = args.threshold
min_gap = args.min_gap
server = ThreadingHTTPServer((args.host, args.port), Handler)
print(f"ISR Web running → http://{args.host}:{args.port}/")
print(f"Recordings dir → {rec_dir.resolve()}")
print(f"Recordings dir → {rec_dir}")
print(f"Analyses dir → {analyses_dir}")
print(f"Loud threshold → {args.threshold}")
if not NUMPY_AVAILABLE:
print("Note: numpy not installed — WAV RMS uses pure Python (slower); FLAC analysis unavailable")