diff --git a/backend/main.py b/backend/main.py
index 8662c42..5bef8b6 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -2,12 +2,15 @@ import logging
import os
import stat
import sys
+import tempfile
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Query, Request, HTTPException
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.responses import StreamingResponse
+from fastapi.responses import StreamingResponse, FileResponse
+
+import ffmpeg
from routers import transcribe, export, ai, captions, audio
@@ -61,20 +64,69 @@ MIME_MAP = {
@app.get("/file")
-async def serve_local_file(request: Request, path: str = Query(...)):
- """Stream a local file with HTTP Range support (required for video seeking)."""
+async def serve_local_file(request: Request, path: str = Query(...), format: str = Query(None)):
+ """Stream a local file with HTTP Range support (required for video seeking).
+ Optionally transcode audio files to MP3 for better browser compatibility."""
file_path = Path(path)
if not file_path.is_file():
logger.warning(f"[serve_file] File not found: {path}")
raise HTTPException(status_code=404, detail=f"File not found: {path}")
file_size = file_path.stat().st_size
- content_type = MIME_MAP.get(file_path.suffix.lower(), "application/octet-stream")
+ original_ext = file_path.suffix.lower()
+
+ # Check if we should transcode this file
+ should_transcode = (
+ original_ext == '.wav' and
+ (format == 'mp3' or file_size > 10 * 1024 * 1024) # Transcode WAV if > 10MB or explicitly requested
+ )
+
+ if should_transcode:
+ logger.info(f"[serve_file] Transcoding {file_path.name} to MP3 (size: {file_size})")
+
+ # Create cache directory
+ cache_dir = Path(__file__).parent / "cache"
+ cache_dir.mkdir(exist_ok=True)
+
+ # Create cache filename
+ import hashlib
+ file_hash = hashlib.md5(str(file_path).encode()).hexdigest()
+ cache_path = cache_dir / f"{file_hash}.mp3"
+
+ # Check if cached version exists
+ if not cache_path.exists():
+ logger.info(f"[serve_file] Creating cached MP3: {cache_path}")
+
+ try:
+ # Transcode to MP3 using ffmpeg
+ (
+ ffmpeg
+ .input(str(file_path))
+ .output(str(cache_path), acodec='libmp3lame', ab='128k')
+ .run(overwrite_output=True, quiet=True)
+ )
+ except ffmpeg.Error as e:
+ logger.error(f"[serve_file] Transcoding failed: {e}")
+ # Fall back to original file
+ cache_path = file_path
+ else:
+ logger.info(f"[serve_file] Transcoding completed: {cache_path}")
+ else:
+ logger.info(f"[serve_file] Using cached MP3: {cache_path}")
+
+ # Use the transcoded file
+ file_path = cache_path
+ file_size = file_path.stat().st_size
+ content_type = "audio/mpeg"
+
+ else:
+ content_type = MIME_MAP.get(original_ext, "application/octet-stream")
+
range_header = request.headers.get("range")
logger.info(
- f"[serve_file] {file_path.name} | size={file_size} | "
- f"type={content_type} | range={range_header or 'none'}"
+ f"[serve_file] Serving {file_path.name} | size={file_size} | "
+ f"type={content_type} | range={range_header or 'none'} | transcoded={should_transcode}"
)
if content_type == "application/octet-stream":
diff --git a/frontend/index.html b/frontend/index.html
index 572b310..099cfe3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -3,10 +3,7 @@
-
-
-
-
+
TalkEdit
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 94702eb..b063e69 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -8,6 +8,8 @@
"name": "talkedit-frontend",
"version": "0.1.0",
"dependencies": {
+ "@fontsource/inter": "^5.2.8",
+ "@fontsource/jetbrains-mono": "^5.2.8",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2",
@@ -768,6 +770,24 @@
"node": ">=18"
}
},
+ "node_modules/@fontsource/inter": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
+ "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
+ "node_modules/@fontsource/jetbrains-mono": {
+ "version": "5.2.8",
+ "resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
+ "integrity": "sha512-6w8/SG4kqvIMu7xd7wt6x3idn1Qux3p9N62s6G3rfldOUYHpWcc2FKrqf+Vo44jRvqWj2oAtTHrZXEP23oSKwQ==",
+ "license": "OFL-1.1",
+ "funding": {
+ "url": "https://github.com/sponsors/ayuhito"
+ }
+ },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
diff --git a/frontend/package.json b/frontend/package.json
index 3c56bf7..46ac12c 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
+ "@fontsource/inter": "^5.2.8",
+ "@fontsource/jetbrains-mono": "^5.2.8",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-dialog": "^2",
"@tauri-apps/plugin-fs": "^2",
diff --git a/frontend/src/components/VideoPlayer.tsx b/frontend/src/components/VideoPlayer.tsx
index daf677f..2914c97 100644
--- a/frontend/src/components/VideoPlayer.tsx
+++ b/frontend/src/components/VideoPlayer.tsx
@@ -72,6 +72,7 @@ export default function VideoPlayer() {
src={videoUrl}
className="max-w-full max-h-full"
controls={false}
+ preload="none"
onClick={togglePlay}
onError={(e) => {
console.error('Audio load error:', e);
@@ -80,6 +81,9 @@ export default function VideoPlayer() {
onLoadStart={() => console.log('Audio load start:', videoUrl)}
onLoadedData={() => console.log('Audio loaded data')}
onCanPlay={() => console.log('Audio can play')}
+ onProgress={() => console.log('Audio progress event')}
+ onStalled={() => console.log('Audio stalled')}
+ onSuspend={() => console.log('Audio suspended')}
/>
) : (