This commit is contained in:
parent
f5305c10d8
commit
30999a2cb2
11 changed files with 2486 additions and 0 deletions
271
clio-api/importer.py
Normal file
271
clio-api/importer.py
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
"""
|
||||
Import functionality for recordings and transcriptions.
|
||||
|
||||
Supports idempotent import of:
|
||||
- JSON transcription files (from transcribe.py output)
|
||||
- Audio files from various sources
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import database
|
||||
|
||||
|
||||
def parse_timestamp_from_filename(filename: str) -> Optional[datetime]:
|
||||
"""
|
||||
Extract timestamp from filename like 'recording_2026-04-26_19-52-47.json'.
|
||||
Returns None if pattern doesn't match.
|
||||
"""
|
||||
pattern = r'recording_(\d{4})-(\d{2})-(\d{2})_(\d{2})-(\d{2})-(\d{2})'
|
||||
match = re.search(pattern, filename)
|
||||
if match:
|
||||
year, month, day, hour, minute, second = map(int, match.groups())
|
||||
return datetime(year, month, day, hour, minute, second)
|
||||
return None
|
||||
|
||||
|
||||
def import_transcription_json(
|
||||
json_path: Path,
|
||||
source_name: str = "phone",
|
||||
audio_base_path: Optional[Path] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Import a single transcription JSON file.
|
||||
|
||||
Returns dict with:
|
||||
- status: 'created', 'exists', or 'error'
|
||||
- recording_id: ID if created or exists
|
||||
- message: description
|
||||
"""
|
||||
try:
|
||||
with open(json_path) as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, IOError) as e:
|
||||
return {"status": "error", "message": f"Failed to read JSON: {e}"}
|
||||
|
||||
source_file = data.get("source_file", str(json_path))
|
||||
|
||||
# Check if already imported (idempotent)
|
||||
existing = database.get_recording_by_source_file(source_file)
|
||||
if existing:
|
||||
return {
|
||||
"status": "exists",
|
||||
"recording_id": existing["id"],
|
||||
"message": f"Recording already imported: {existing['filename']}",
|
||||
}
|
||||
|
||||
# Parse filename and timestamp
|
||||
filename = Path(source_file).name
|
||||
recorded_at = parse_timestamp_from_filename(filename)
|
||||
|
||||
# Determine audio path
|
||||
audio_path = None
|
||||
if audio_base_path:
|
||||
# Try to find audio file relative to base path
|
||||
audio_file = Path(source_file)
|
||||
if audio_file.exists():
|
||||
audio_path = str(audio_file)
|
||||
else:
|
||||
# Try in audio_base_path
|
||||
potential = audio_base_path / audio_file.name
|
||||
if potential.exists():
|
||||
audio_path = str(potential)
|
||||
|
||||
# Create recording entry
|
||||
recording_id = database.create_recording(
|
||||
source_file=source_file,
|
||||
filename=filename,
|
||||
source_name=source_name,
|
||||
audio_path=audio_path,
|
||||
duration=data.get("duration"),
|
||||
language=data.get("language"),
|
||||
language_probability=data.get("language_probability"),
|
||||
recorded_at=recorded_at,
|
||||
transcribed_at=datetime.now(),
|
||||
)
|
||||
|
||||
# Import segments
|
||||
segments = data.get("segments", [])
|
||||
segment_count = database.create_segments(recording_id, segments)
|
||||
|
||||
return {
|
||||
"status": "created",
|
||||
"recording_id": recording_id,
|
||||
"message": f"Imported {filename} with {segment_count} segments",
|
||||
"segments": segment_count,
|
||||
}
|
||||
|
||||
|
||||
def import_transcription_directory(
|
||||
directory: Path,
|
||||
source_name: str = "phone",
|
||||
audio_base_path: Optional[Path] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Import all JSON transcription files from a directory.
|
||||
|
||||
Returns summary with counts of created, existing, and errors.
|
||||
"""
|
||||
results = {
|
||||
"created": 0,
|
||||
"exists": 0,
|
||||
"errors": 0,
|
||||
"details": [],
|
||||
}
|
||||
|
||||
json_files = sorted(directory.glob("*.json"))
|
||||
|
||||
for json_path in json_files:
|
||||
result = import_transcription_json(json_path, source_name, audio_base_path)
|
||||
results["details"].append({
|
||||
"file": json_path.name,
|
||||
**result,
|
||||
})
|
||||
|
||||
if result["status"] == "created":
|
||||
results["created"] += 1
|
||||
elif result["status"] == "exists":
|
||||
results["exists"] += 1
|
||||
else:
|
||||
results["errors"] += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def import_audio_file(
|
||||
audio_path: Path,
|
||||
source_name: str,
|
||||
move_to: Optional[Path] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Import a single audio file (without transcription).
|
||||
|
||||
If move_to is provided, the file will be moved there.
|
||||
Otherwise, just creates a database entry pointing to current location.
|
||||
"""
|
||||
if not audio_path.exists():
|
||||
return {"status": "error", "message": f"File not found: {audio_path}"}
|
||||
|
||||
source_file = str(audio_path.resolve())
|
||||
|
||||
# Check if already imported
|
||||
existing = database.get_recording_by_source_file(source_file)
|
||||
if existing:
|
||||
return {
|
||||
"status": "exists",
|
||||
"recording_id": existing["id"],
|
||||
"message": f"Audio already imported: {existing['filename']}",
|
||||
}
|
||||
|
||||
filename = audio_path.name
|
||||
recorded_at = parse_timestamp_from_filename(filename)
|
||||
|
||||
# Get file size
|
||||
file_size = audio_path.stat().st_size
|
||||
|
||||
# Move file if requested
|
||||
final_path = str(audio_path.resolve())
|
||||
if move_to:
|
||||
move_to.mkdir(parents=True, exist_ok=True)
|
||||
dest = move_to / filename
|
||||
audio_path.rename(dest)
|
||||
final_path = str(dest.resolve())
|
||||
|
||||
recording_id = database.create_recording(
|
||||
source_file=source_file,
|
||||
filename=filename,
|
||||
source_name=source_name,
|
||||
audio_path=final_path,
|
||||
recorded_at=recorded_at,
|
||||
)
|
||||
|
||||
# Update file size
|
||||
database.update_recording(recording_id, file_size_bytes=file_size)
|
||||
|
||||
return {
|
||||
"status": "created",
|
||||
"recording_id": recording_id,
|
||||
"message": f"Imported audio: {filename}",
|
||||
}
|
||||
|
||||
|
||||
def import_audio_directory(
|
||||
directory: Path,
|
||||
source_name: str,
|
||||
extensions: tuple[str, ...] = (".ogg", ".mp3", ".wav", ".m4a", ".flac", ".webm"),
|
||||
move_to: Optional[Path] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Import all audio files from a directory.
|
||||
"""
|
||||
results = {
|
||||
"created": 0,
|
||||
"exists": 0,
|
||||
"errors": 0,
|
||||
"details": [],
|
||||
}
|
||||
|
||||
# Recursively find all audio files
|
||||
audio_files = sorted(
|
||||
p for ext in extensions
|
||||
for p in directory.rglob(f"*{ext}")
|
||||
)
|
||||
|
||||
for audio_path in audio_files:
|
||||
result = import_audio_file(audio_path, source_name, move_to)
|
||||
results["details"].append({
|
||||
"file": audio_path.name,
|
||||
**result,
|
||||
})
|
||||
|
||||
if result["status"] == "created":
|
||||
results["created"] += 1
|
||||
elif result["status"] == "exists":
|
||||
results["exists"] += 1
|
||||
else:
|
||||
results["errors"] += 1
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def link_audio_to_transcriptions(audio_dir: Path) -> dict:
|
||||
"""
|
||||
Link audio files to existing transcription records that are missing audio_path.
|
||||
|
||||
Matches by filename pattern (e.g., recording_2026-04-26_19-52-47.ogg).
|
||||
"""
|
||||
updated = 0
|
||||
not_found = 0
|
||||
|
||||
with database.get_db() as conn:
|
||||
# Get all recordings without audio_path
|
||||
cur = conn.execute(
|
||||
"SELECT id, filename, source_file FROM recordings WHERE audio_path IS NULL"
|
||||
)
|
||||
recordings = cur.fetchall()
|
||||
|
||||
for rec in recordings:
|
||||
# Try to find matching audio file
|
||||
base_name = Path(rec["filename"]).stem
|
||||
for ext in (".ogg", ".mp3", ".wav", ".m4a", ".flac", ".webm"):
|
||||
potential = audio_dir / f"{base_name}{ext}"
|
||||
if potential.exists():
|
||||
conn.execute(
|
||||
"UPDATE recordings SET audio_path = ? WHERE id = ?",
|
||||
(str(potential.resolve()), rec["id"])
|
||||
)
|
||||
updated += 1
|
||||
break
|
||||
else:
|
||||
not_found += 1
|
||||
|
||||
return {
|
||||
"updated": updated,
|
||||
"not_found": not_found,
|
||||
"message": f"Linked {updated} recordings to audio files, {not_found} still missing",
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue