trying to fix crashes
This commit is contained in:
@ -2,12 +2,15 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Query, Request, HTTPException
|
from fastapi import FastAPI, Query, Request, HTTPException
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
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
|
from routers import transcribe, export, ai, captions, audio
|
||||||
|
|
||||||
@ -61,20 +64,69 @@ MIME_MAP = {
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/file")
|
@app.get("/file")
|
||||||
async def serve_local_file(request: Request, path: str = Query(...)):
|
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)."""
|
"""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)
|
file_path = Path(path)
|
||||||
if not file_path.is_file():
|
if not file_path.is_file():
|
||||||
logger.warning(f"[serve_file] File not found: {path}")
|
logger.warning(f"[serve_file] File not found: {path}")
|
||||||
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
raise HTTPException(status_code=404, detail=f"File not found: {path}")
|
||||||
|
|
||||||
file_size = file_path.stat().st_size
|
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")
|
range_header = request.headers.get("range")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[serve_file] {file_path.name} | size={file_size} | "
|
f"[serve_file] Serving {file_path.name} | size={file_size} | "
|
||||||
f"type={content_type} | range={range_header or 'none'}"
|
f"type={content_type} | range={range_header or 'none'} | transcoded={should_transcode}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if content_type == "application/octet-stream":
|
if content_type == "application/octet-stream":
|
||||||
|
|||||||
@ -3,10 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' ipc: http://ipc.localhost http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; media-src 'self' file: blob: http://localhost:* http://127.0.0.1:*; img-src 'self' data: blob:;" />
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'self' ipc: http://ipc.localhost http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*; media-src 'self' file: blob: http://localhost:* http://127.0.0.1:*; img-src 'self' data: blob:;" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
||||||
<title>TalkEdit</title>
|
<title>TalkEdit</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-editor-bg text-editor-text antialiased">
|
<body class="bg-editor-bg text-editor-text antialiased">
|
||||||
|
|||||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -8,6 +8,8 @@
|
|||||||
"name": "talkedit-frontend",
|
"name": "talkedit-frontend",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-fs": "^2",
|
"@tauri-apps/plugin-fs": "^2",
|
||||||
@ -768,6 +770,24 @@
|
|||||||
"node": ">=18"
|
"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": {
|
"node_modules/@jridgewell/gen-mapping": {
|
||||||
"version": "0.3.13",
|
"version": "0.3.13",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||||
|
|||||||
@ -10,6 +10,8 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@fontsource/inter": "^5.2.8",
|
||||||
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
"@tauri-apps/plugin-dialog": "^2",
|
"@tauri-apps/plugin-dialog": "^2",
|
||||||
"@tauri-apps/plugin-fs": "^2",
|
"@tauri-apps/plugin-fs": "^2",
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export default function VideoPlayer() {
|
|||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
className="max-w-full max-h-full"
|
className="max-w-full max-h-full"
|
||||||
controls={false}
|
controls={false}
|
||||||
|
preload="none"
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.error('Audio load error:', e);
|
console.error('Audio load error:', e);
|
||||||
@ -80,6 +81,9 @@ export default function VideoPlayer() {
|
|||||||
onLoadStart={() => console.log('Audio load start:', videoUrl)}
|
onLoadStart={() => console.log('Audio load start:', videoUrl)}
|
||||||
onLoadedData={() => console.log('Audio loaded data')}
|
onLoadedData={() => console.log('Audio loaded data')}
|
||||||
onCanPlay={() => console.log('Audio can play')}
|
onCanPlay={() => console.log('Audio can play')}
|
||||||
|
onProgress={() => console.log('Audio progress event')}
|
||||||
|
onStalled={() => console.log('Audio stalled')}
|
||||||
|
onSuspend={() => console.log('Audio suspended')}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<video
|
<video
|
||||||
@ -87,6 +91,7 @@ export default function VideoPlayer() {
|
|||||||
src={videoUrl}
|
src={videoUrl}
|
||||||
className="max-w-full max-h-full object-contain"
|
className="max-w-full max-h-full object-contain"
|
||||||
playsInline
|
playsInline
|
||||||
|
preload="none"
|
||||||
onClick={togglePlay}
|
onClick={togglePlay}
|
||||||
onError={(e) => {
|
onError={(e) => {
|
||||||
console.error('Video load error:', e);
|
console.error('Video load error:', e);
|
||||||
@ -95,6 +100,9 @@ export default function VideoPlayer() {
|
|||||||
onLoadStart={() => console.log('Video load start:', videoUrl)}
|
onLoadStart={() => console.log('Video load start:', videoUrl)}
|
||||||
onLoadedData={() => console.log('Video loaded data')}
|
onLoadedData={() => console.log('Video loaded data')}
|
||||||
onCanPlay={() => console.log('Video can play')}
|
onCanPlay={() => console.log('Video can play')}
|
||||||
|
onProgress={() => console.log('Video progress event')}
|
||||||
|
onStalled={() => console.log('Video stalled')}
|
||||||
|
onSuspend={() => console.log('Video suspended')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,3 +1,11 @@
|
|||||||
|
@import '@fontsource/inter/300.css';
|
||||||
|
@import '@fontsource/inter/400.css';
|
||||||
|
@import '@fontsource/inter/500.css';
|
||||||
|
@import '@fontsource/inter/600.css';
|
||||||
|
@import '@fontsource/inter/700.css';
|
||||||
|
@import '@fontsource/jetbrains-mono/400.css';
|
||||||
|
@import '@fontsource/jetbrains-mono/500.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
@ -116,7 +116,13 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
|
|
||||||
loadVideo: (path) => {
|
loadVideo: (path) => {
|
||||||
const backend = get().backendUrl;
|
const backend = get().backendUrl;
|
||||||
const url = `${backend}/file?path=${encodeURIComponent(path)}`;
|
const buildMediaUrl = (filePath: string) => {
|
||||||
|
const isWav = filePath.toLowerCase().endsWith('.wav');
|
||||||
|
return isWav
|
||||||
|
? `${backend}/file?path=${encodeURIComponent(filePath)}&format=mp3`
|
||||||
|
: `${backend}/file?path=${encodeURIComponent(filePath)}`;
|
||||||
|
};
|
||||||
|
const url = buildMediaUrl(path);
|
||||||
set({
|
set({
|
||||||
...initialState,
|
...initialState,
|
||||||
backendUrl: backend,
|
backendUrl: backend,
|
||||||
@ -304,7 +310,10 @@ export const useEditorStore = create<EditorState & EditorActions>()(
|
|||||||
|
|
||||||
loadProject: (data) => {
|
loadProject: (data) => {
|
||||||
const backend = get().backendUrl;
|
const backend = get().backendUrl;
|
||||||
const url = `${backend}/file?path=${encodeURIComponent(data.videoPath)}`;
|
const isWav = data.videoPath.toLowerCase().endsWith('.wav');
|
||||||
|
const url = isWav
|
||||||
|
? `${backend}/file?path=${encodeURIComponent(data.videoPath)}&format=mp3`
|
||||||
|
: `${backend}/file?path=${encodeURIComponent(data.videoPath)}`;
|
||||||
|
|
||||||
let globalIdx = 0;
|
let globalIdx = 0;
|
||||||
const annotatedSegments = (data.segments || []).map((seg: Segment) => {
|
const annotatedSegments = (data.segments || []).map((seg: Segment) => {
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*; media-src 'self' http://127.0.0.1:* http://localhost:* file: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: blob:"
|
"csp": "default-src 'self'; connect-src 'self' http://127.0.0.1:* http://localhost:* ws://127.0.0.1:* ws://localhost:*; media-src 'self' http://127.0.0.1:* http://localhost:* file: blob:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self' data:; img-src 'self' data: blob:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
Reference in New Issue
Block a user