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')} /> ) : (