import os, json, uuid, shutil, asyncio, time from pathlib import Path from dotenv import load_dotenv from fastapi import FastAPI, HTTPException, UploadFile from fastapi.responses import FileResponse, PlainTextResponse from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from pydantic import BaseModel from vidmaker import make_synced_video from tiktok_utils import download_tiktok, extract_audio, transcribe_with_replicate from suno_utils import audio_to_suno_song , get_aligned_lyrics# For deprecated /api/full endpoint from suno_step_by_step_utils import upload_audio_step_by_step, upload_and_remix_step_by_step, StepLogger from suno_vocal_cover_utils import upload_and_create_vocal_cover_v45 load_dotenv() BASE = Path(__file__).parent FRONT = BASE.parent / "frontend" STORE = BASE / "storage"; STORE.mkdir(exist_ok=True) app = FastAPI(title="mvmaker one‑click") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) # ─── helpers ───────────────────────────────────────────────────── JOBS: dict[str, dict] = {} def jlog(jid:str,msg:str): p=STORE/jid/"log.txt"; p.parent.mkdir(parents=True,exist_ok=True) with p.open("a") as f: f.write(f"[{jid}] {msg}\n") print(f"[{jid}] {msg}") JOBS.setdefault(jid,{}).update(note=msg) def jerr(jid:str,e:Exception): import traceback, textwrap; traceback.print_exc() JOBS[jid].update(status="error",error=f"{type(e).__name__}: {e}") jlog(jid,f"ERROR {e}") # ─── models ────────────────────────────────────────────────────── class FullReq(BaseModel): # one‑click body url: str # TikTok URL prompt: str = "lo‑fi beat" # Suno style/genre class JobStatus(BaseModel): id:str; status:str output:str|None=None; sync:str|None=None error:str|None=None; note:str|None=None clips:list|None=None; transcripts:dict|None=None class SunoUploadReq(BaseModel): file_path: str # Path to local WAV file for testing class SunoRemixReq(BaseModel): file_path: str # Path to local WAV file for testing prompt: str = "dubstep, heavy bass" # Style/genre tags audio_weight: float = 0.8 # Audio influence (0-1) # ─── routes ────────────────────────────────────────────────────── @app.api_route("/api/ping",methods=["GET","POST"]) async def ping(): return {"ok":True} def hoot_to_transcript(hoot_json): transcript = "" for t in hoot_json: transcript += t["text"] return transcript @app.post("/api/full/v2", response_model=JobStatus) async def full_v2(req: FullReq): """Enhanced TikTok → Suno → synced video pipeline with cover generation""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) JOBS[jid] = {"status": "processing", "note": "queued"} async def run(): env_vars = dict(REPLICATE_API_TOKEN="r8_HGsdsEyz339nplinKh9Ex5Se2mxM8Sc1lbdu4", WHISPER_MODEL="villesau/whisper-timestamped:c5b122b7e513b1b5a6ef849891c538869b77cc932cbd0f8203e11d3b357553b8", SUNO_TOKEN="ae6cb0480e2e493cbab61a7324030ec5", SUNO_DEBUG="1") for k, v in env_vars.items(): os.environ[k] = v try: # # 1) TikTok download jlog(jid, "Downloading TikTok...") mp4 = await asyncio.to_thread(download_tiktok, req.url, jdir) (jdir / "video_path.txt").write_text(mp4) # # 2) extract WAV wav = jdir / "audio.wav" jlog(jid, "Extracting audio...") await asyncio.to_thread(extract_audio, mp4, str(wav)) (jdir / "audio_path.txt").write_text(str(wav)) # #upload clip clip_id, _ = upload_audio_step_by_step(wav, jdir / "generation_log.txt") # get transcript # clip_id = "e89a99eb-0533-4a68-a50f-f0242645a4c2" hoot_json = get_aligned_lyrics(clip_id) transcript = hoot_to_transcript(hoot_json["aligned_lyrics"]) (jdir / "video.json").write_text(json.dumps(hoot_json, indent=2)) # 3) Generate ONE song request (Suno returns 2 clips) song = jdir / "suno.wav" prompt = req.prompt if req.prompt else "dubstep, heavy bass" # if transcript_text.strip(): # Use v4.5 vocal cover approach jlog(jid, f"Generating vocal cover with v4.5: style='{prompt}', audio_weight=1.0") result, _ =upload_and_create_vocal_cover_v45(clip_id, transcript, prompt, # Basic genre tags jdir / "generation_log.txt", 0.7 # 100% audio strength ) # else: # # Fall back to instrumental remix # jlog(jid, f"Generating instrumental remix: prompt='{prompt}', audio_weight=1.0") # result, _ = await asyncio.to_thread( # upload_and_remix_step_by_step, # wav, # prompt, # jdir / "generation_log.txt", # 1.0 # 100% weight # ) # Suno returns 2 clips, we'll use the first one for sync upload_id = result["upload_clip_id"] clip_id = result["generated_clips"][0]["id"] jlog(jid, f"Generation complete! Got {len(result['generated_clips'])} clips") jlog(jid, f"Using first clip for sync: {clip_id}") # Download the first clip from suno_download import download_suno_clip if not download_suno_clip(clip_id, song): jlog(jid, f"WARNING: Failed to download clip, saving ID") song.write_text(f"CLIP_ID: {clip_id}\n") assert False # 4) Transcribe the generated song (first clip) jlog(jid, "Transcribing generated song...") sjson = get_aligned_lyrics(clip_id, post=False) (jdir / "song.json").write_text(json.dumps(sjson, indent=2)) # 5) Make synced video out = jdir / "output.mp4" sync = jdir / "combined.json" jlog(jid, "Building synced video...") make_synced_video( clip_path=mp4, # Original TikTok video song_path=str(song), # Generated Suno audio (first clip) video_json_path=str(jdir / "video.json"), # Original transcript song_json_path=str(jdir / "song.json"), # Generated song transcript out_path=str(out), # Output video export_sync=str(sync), # Sync data export smooth=3 # Smoothing window ) # Update job status JOBS[jid].update({ "status": "done", "output": f"/api/download/{jid}", "sync": f"/api/sync/{jid}", "note": "✅ Video sync complete!" }) jlog(jid, "✅ Finished!") except Exception as e: jerr(jid, e) asyncio.create_task(run()) return JobStatus(id=jid, status="processing") @app.get("/api/status/{jid}",response_model=JobStatus) def status(jid:str): if jid not in JOBS: raise HTTPException(404,"job") # Add clip info if available job_data = JOBS[jid].copy() jdir = STORE / jid # Try to get clip IDs from generation logs clips = [] for log_name in ["generation1_log.txt", "generation2_log.txt"]: log_file = jdir / log_name if log_file.exists(): content = log_file.read_text() import re # Find clip IDs in the log matches = re.findall(r'Generated clip[^:]*: ([a-f0-9-]{36})', content) if not matches: # Try another pattern matches = re.findall(r'"id":\s*"([a-f0-9-]{36})"', content)[:2] for clip_id in matches: clips.append({ "id": clip_id, "url": f"https://cdn1.suno.ai/{clip_id}.mp3" }) if clips: job_data["clips"] = clips # Add paths to transcription files transcripts = {} for name in ["video.json", "song.json", "cover_song.json"]: path = jdir / name if path.exists(): transcripts[name] = f"/api/transcript/{jid}/{name}" if transcripts: job_data["transcripts"] = transcripts return JobStatus(id=jid,**job_data) @app.get("/api/log/{jid}") def log(jid:str): p=STORE/jid/"log.txt" if not p.exists(): return PlainTextResponse("",media_type="text/plain") return FileResponse(p,media_type="text/plain") @app.get("/api/download/{jid}") def dl(jid:str, type: str = None): if type == "cover": p = STORE/jid/"output_cover.mp4" filename = "synced_cover.mp4" else: p = STORE/jid/"output.mp4" filename = "synced.mp4" if not p.exists(): raise HTTPException(404, "mp4") return FileResponse(p, filename=filename, media_type="video/mp4") @app.get("/api/sync/{jid}") def sync(jid:str, type: str = None): if type == "cover": p = STORE/jid/"combined_cover.json" filename = "combined_cover.json" else: p = STORE/jid/"combined.json" filename = "combined.json" if not p.exists(): raise HTTPException(404, "json") return FileResponse(p, media_type="application/json", filename=filename) @app.get("/api/debug/{jid}") def get_debug_info(jid: str): """Get debug information including clip IDs and audio paths""" if jid not in JOBS: raise HTTPException(404, "Job not found") jdir = STORE / jid debug_info = { "job_id": jid, "status": JOBS[jid].get("status"), "clips": [], "audio_files": [] } # Find clip IDs from logs gen1_log = jdir / "generation1_log.txt" gen2_log = jdir / "generation2_log.txt" for log_file in [gen1_log, gen2_log]: if log_file.exists(): content = log_file.read_text() # Extract clip IDs from logs import re clip_ids = re.findall(r'"id":\s*"([a-f0-9-]{36})"', content) for clip_id in clip_ids[:2]: # First 2 are the generated clips debug_info["clips"].append({ "id": clip_id, "audio_url": f"https://cdn1.suno.ai/{clip_id}.mp3" }) # List audio files for audio_file in jdir.glob("*.wav"): debug_info["audio_files"].append({ "name": audio_file.name, "path": str(audio_file) }) return debug_info @app.post("/api/transcribe_audio") async def transcribe_audio_file(req: dict): """Transcribe an audio file using Whisper""" audio_path = req.get("audio_path") if not audio_path: raise HTTPException(400, "audio_path required") audio_file = Path(audio_path) if not audio_file.exists(): raise HTTPException(404, "Audio file not found") try: # Use our existing Whisper transcription transcript = await asyncio.to_thread(transcribe_with_replicate, str(audio_file)) # Format transcript for display formatted_segments = [] full_text = "" for seg in transcript.get("segments", []): formatted_segments.append({ "start": seg.get("start", 0), "end": seg.get("end", 0), "text": seg.get("text", "") }) full_text += seg.get("text", "") + " " return { "success": True, "full_text": full_text.strip(), "segments": formatted_segments, "audio_path": audio_path } except Exception as e: return { "success": False, "error": str(e), "audio_path": audio_path } @app.post("/api/suno_upload_steps") async def suno_upload_steps(req: SunoUploadReq): """Step-by-step Suno upload with detailed logging""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) log_file = jdir / "suno_upload_log.txt" JOBS[jid] = {"status": "processing", "note": "Starting Suno upload..."} async def run(): try: # Convert string path to Path object audio_path = Path(req.file_path) if not audio_path.exists(): raise FileNotFoundError(f"Audio file not found: {audio_path}") # Run the step-by-step upload clip_id, logger = await asyncio.to_thread( upload_audio_step_by_step, audio_path, log_file ) JOBS[jid].update({ "status": "done", "clip_id": clip_id, "note": f"Upload complete! Clip ID: {clip_id}", "logs": logger.get_logs() }) except Exception as e: import traceback traceback.print_exc() JOBS[jid].update({ "status": "error", "error": f"{type(e).__name__}: {e}", "note": "Upload failed" }) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.get("/api/suno_upload_logs/{jid}") def get_suno_logs(jid: str): """Get logs for a Suno upload job""" if jid not in JOBS: raise HTTPException(404, "Job not found") job = JOBS[jid] log_file = STORE / jid / "suno_upload_log.txt" # Read log file if exists logs_text = "" if log_file.exists(): logs_text = log_file.read_text() return { "job_id": jid, "status": job.get("status"), "clip_id": job.get("clip_id"), "error": job.get("error"), "note": job.get("note"), "logs": logs_text, "logs_structured": job.get("logs", []) } @app.post("/api/suno_upload_remix") async def suno_upload_remix(req: SunoRemixReq): """Step-by-step Suno upload + remix with detailed logging""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) log_file = jdir / "suno_remix_log.txt" JOBS[jid] = {"status": "processing", "note": "Starting Suno upload + remix..."} async def run(): try: # Convert string path to Path object audio_path = Path(req.file_path) if not audio_path.exists(): raise FileNotFoundError(f"Audio file not found: {audio_path}") # Run the step-by-step upload + remix result, logger = await asyncio.to_thread( upload_and_remix_step_by_step, audio_path, req.prompt, log_file, req.audio_weight ) JOBS[jid].update({ "status": "done", "upload_clip_id": result["upload_clip_id"], "generation_id": result["generation_id"], "generated_clips": result["generated_clips"], "total_time": result["total_time"], "note": f"Remix complete! Generated {len(result['generated_clips'])} clips", "logs": logger.get_logs() }) except Exception as e: import traceback traceback.print_exc() JOBS[jid].update({ "status": "error", "error": f"{type(e).__name__}: {e}", "note": "Remix failed" }) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.post("/api/test/step1_download") async def test_step1_download(req: dict): """Step 1: Download TikTok and extract audio""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) test_url = req.get("url", "https://www.tiktok.com/@ethan.stee1e/video/7529957148734786838") async def run(): try: # Download TikTok jlog(jid, "Step 1: Downloading TikTok...") mp4 = await asyncio.to_thread(download_tiktok, test_url, jdir) jlog(jid, f"Downloaded to: {mp4}") # Extract audio wav = jdir / "audio.wav" jlog(jid, "Step 1: Extracting audio...") await asyncio.to_thread(extract_audio, mp4, str(wav)) jlog(jid, f"Audio extracted to: {wav}") # Save path for step 2 (jdir / "audio_path.txt").write_text(str(wav)) JOBS[jid] = { "status": "done", "audio_path": str(wav), "note": "Step 1 complete: Audio ready for upload" } except Exception as e: import traceback traceback.print_exc() JOBS[jid] = { "status": "error", "error": f"{type(e).__name__}: {e}", "note": "Step 1 failed" } asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.post("/api/test/step2_suno_exact") async def test_step2_suno_exact(req: dict): """Step 2: Upload to Suno using EXACT same method as working remix""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) audio_path = req.get("audio_path", "/Users/matthewgordon/Desktop/mvmaker/backend/wobblywiggly_vox.wav") prompt = req.get("prompt", "house music") log_file = jdir / "suno_test_log.txt" JOBS[jid] = {"status": "processing", "note": "Starting Suno test..."} async def run(): try: # Use EXACT same function as working remix result, logger = await asyncio.to_thread( upload_and_remix_step_by_step, Path(audio_path), prompt, log_file, 1.0 # 100% audio weight ) JOBS[jid].update({ "status": "done", "upload_clip_id": result["upload_clip_id"], "generation_id": result["generation_id"], "generated_clips": result["generated_clips"], "total_time": result["total_time"], "note": f"Success! Generated {len(result['generated_clips'])} clips", "logs": logger.get_logs() }) except Exception as e: import traceback traceback.print_exc() JOBS[jid].update({ "status": "error", "error": f"{type(e).__name__}: {e}", "note": "Suno test failed" }) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.post("/api/test/exact_working_test") async def test_exact_working(): """Test with exact same parameters as working example""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) # Use EXACT same file as working example test_audio = Path("/Users/matthewgordon/Desktop/mvmaker/backend/wobblywiggly_vox.wav") if not test_audio.exists(): return {"error": "Test audio file not found"} log_file = jdir / "test_log.txt" JOBS[jid] = {"status": "processing", "note": "Testing with exact working parameters..."} async def run(): try: # Use EXACT parameters from working example result, logger = await asyncio.to_thread( upload_and_remix_step_by_step, test_audio, "dubstep", # Exact prompt from working example log_file, 0.8 # Exact audio weight from working example ) JOBS[jid].update({ "status": "done", "result": result, "note": f"Success! Check if audio contains original" }) except Exception as e: import traceback traceback.print_exc() JOBS[jid].update({ "status": "error", "error": f"{type(e).__name__}: {e}" }) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.get("/api/test/status/{jid}") def test_status(jid: str): """Get test job status""" if jid not in JOBS: raise HTTPException(404, "Job not found") return JOBS[jid] @app.post("/api/test/overpainting") async def test_overpainting(req: dict): """Test overpainting approach for vocal preservation""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) audio_path = req.get("audio_path", "/Users/matthewgordon/Desktop/mvmaker/backend/wobblywiggly_vox.wav") transcript = req.get("transcript", "Test transcript for vocal preservation") style_tags = req.get("style_tags", "house music") JOBS[jid] = {"status": "processing", "note": "Starting overpainting test..."} async def run(): try: jlog(jid, f"Testing overpainting with audio: {audio_path}") jlog(jid, f"Transcript: {transcript[:100]}..." if len(transcript) > 100 else f"Transcript: {transcript}") jlog(jid, f"Style tags: {style_tags}") result, logger = await asyncio.to_thread( upload_and_overpaint_with_transcript, Path(audio_path), transcript, style_tags, jdir / "overpainting_log.txt", 1.0 # 100% audio weight ) JOBS[jid].update({ "status": "done", "upload_clip_id": result["upload_clip_id"], "generation_id": result["generation_id"], "generated_clips": result["generated_clips"], "total_time": result["total_time"], "note": f"Success! Generated {len(result['generated_clips'])} clips" }) # Log clip details for clip in result["generated_clips"]: jlog(jid, f"Generated clip {clip['id']}: task={clip.get('metadata', {}).get('task')}, has_vocal={clip.get('metadata', {}).get('has_vocal')}") except Exception as e: import traceback traceback.print_exc() JOBS[jid].update({ "status": "error", "error": f"{type(e).__name__}: {e}", "note": "Overpainting test failed" }) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.get("/api/suno_remix_logs/{jid}") def get_suno_remix_logs(jid: str): """Get logs for a Suno remix job""" if jid not in JOBS: raise HTTPException(404, "Job not found") job = JOBS[jid] log_file = STORE / jid / "suno_remix_log.txt" # Read log file if exists logs_text = "" if log_file.exists(): logs_text = log_file.read_text() return { "job_id": jid, "status": job.get("status"), "upload_clip_id": job.get("upload_clip_id"), "generation_id": job.get("generation_id"), "generated_clips": job.get("generated_clips"), "total_time": job.get("total_time"), "error": job.get("error"), "note": job.get("note"), "logs": logs_text, "logs_structured": job.get("logs", []) } @app.get("/api/audio/{jid}/{filename}") def serve_audio(jid: str, filename: str): """Serve audio files from job storage""" audio_path = STORE / jid / filename if not audio_path.exists() or not audio_path.suffix in [".wav", ".mp3"]: raise HTTPException(404, "Audio file not found") media_type = "audio/wav" if audio_path.suffix == ".wav" else "audio/mpeg" return FileResponse(audio_path, media_type=media_type) @app.get("/api/transcript/{jid}/{filename}") def serve_transcript(jid: str, filename: str): """Serve transcript JSON files""" if not filename.endswith(".json"): raise HTTPException(400, "Only JSON files allowed") transcript_path = STORE / jid / filename if not transcript_path.exists(): raise HTTPException(404, "Transcript file not found") return FileResponse(transcript_path, media_type="application/json") # Debug endpoints @app.post("/api/debug/test_sync_data") async def debug_test_sync_data(req: dict): """Test sync compatibility with JSON data directly""" video_transcript = req.get("video_transcript") song_transcript = req.get("song_transcript") if not video_transcript or not song_transcript: raise HTTPException(400, "Both transcript data required") try: # Extract language info video_language = video_transcript.get("language", "unknown") song_language = song_transcript.get("output", {}).get("language", song_transcript.get("language", "unknown")) # Get word counts and debug info from vidmaker import _flatten_words, _norm # Handle both standard Whisper format and Replicate format if "output" in song_transcript: # Replicate format v_words = _flatten_words({"output": video_transcript}) s_words = _flatten_words(song_transcript) else: # Standard Whisper format v_words = _flatten_words({"output": video_transcript}) s_words = _flatten_words({"output": song_transcript}) # Normalize words for lst in (v_words, s_words): for d in lst: d["norm"] = _norm(d["raw"]) video_words = len(v_words) song_words = len(s_words) # Find matching words v_norm_words = set(w["norm"] for w in v_words if w["norm"]) s_norm_words = set(w["norm"] for w in s_words if w["norm"]) matching_words = len(v_norm_words & s_norm_words) # Try to build sync anchors anchor_count = 0 debug_info = "" try: from vidmaker import build_combined_sync anchors = build_combined_sync(video_transcript, song_transcript) anchor_count = len(anchors) debug_info = f"✅ Sync successful! Found {anchor_count} anchors.\n" debug_info += f"First few anchors:\n" for i, anchor in enumerate(anchors[:3]): debug_info += f" {i+1}. Song {anchor['songStart']:.2f}s-{anchor['songEnd']:.2f}s → Video {anchor['videoStart']:.2f}s-{anchor['videoEnd']:.2f}s\n" except Exception as e: debug_info = f"❌ Sync failed: {str(e)}\n" debug_info += f"\nDetailed analysis:\n" debug_info += f"Video language: {video_language}\n" debug_info += f"Song language: {song_language}\n" debug_info += f"Video words (first 10): {[w['raw'] for w in v_words[:10]]}\n" debug_info += f"Song words (first 10): {[w['raw'] for w in s_words[:10]]}\n" debug_info += f"Video normalized (first 10): {[w['norm'] for w in v_words[:10]]}\n" debug_info += f"Song normalized (first 10): {[w['norm'] for w in s_words[:10]]}\n" debug_info += f"Common words: {list(v_norm_words & s_norm_words)[:20]}\n" return { "video_language": video_language, "song_language": song_language, "video_words": video_words, "song_words": song_words, "matching_words": matching_words, "anchor_count": anchor_count, "debug_info": debug_info } except Exception as e: import traceback traceback.print_exc() raise HTTPException(500, f"Debug test failed: {str(e)}") @app.post("/api/debug/test_sync") async def debug_test_sync(req: dict): """Test sync compatibility between two transcript files""" video_transcript_path = req.get("video_transcript_path") song_transcript_path = req.get("song_transcript_path") if not video_transcript_path or not song_transcript_path: raise HTTPException(400, "Both transcript paths required") try: # Load transcript files import json from pathlib import Path video_path = Path(video_transcript_path) song_path = Path(song_transcript_path) if not video_path.exists(): raise HTTPException(404, f"Video transcript not found: {video_path}") if not song_path.exists(): raise HTTPException(404, f"Song transcript not found: {song_path}") with open(video_path) as f: video_json = json.load(f) with open(song_path) as f: song_json = json.load(f) # Extract language info video_language = video_json.get("language", "unknown") song_language = song_json.get("output", {}).get("language", song_json.get("language", "unknown")) # Get word counts and debug info from vidmaker import _flatten_words, _norm # Handle both standard Whisper format and Replicate format if "output" in song_json: # Replicate format v_words = _flatten_words({"output": video_json}) s_words = _flatten_words(song_json) else: # Standard Whisper format v_words = _flatten_words({"output": video_json}) s_words = _flatten_words({"output": song_json}) # Normalize words for lst in (v_words, s_words): for d in lst: d["norm"] = _norm(d["raw"]) video_words = len(v_words) song_words = len(s_words) # Find matching words v_norm_words = set(w["norm"] for w in v_words if w["norm"]) s_norm_words = set(w["norm"] for w in s_words if w["norm"]) matching_words = len(v_norm_words & s_norm_words) # Try to build sync anchors anchor_count = 0 debug_info = "" try: from vidmaker import build_combined_sync anchors = build_combined_sync(video_json, song_json) anchor_count = len(anchors) debug_info = f"✅ Sync successful! Found {anchor_count} anchors.\n" debug_info += f"First few anchors:\n" for i, anchor in enumerate(anchors[:3]): debug_info += f" {i+1}. Song {anchor['songStart']:.2f}s-{anchor['songEnd']:.2f}s → Video {anchor['videoStart']:.2f}s-{anchor['videoEnd']:.2f}s\n" except Exception as e: debug_info = f"❌ Sync failed: {str(e)}\n" debug_info += f"\nDetailed analysis:\n" debug_info += f"Video language: {video_language}\n" debug_info += f"Song language: {song_language}\n" debug_info += f"Video words (first 10): {[w['raw'] for w in v_words[:10]]}\n" debug_info += f"Song words (first 10): {[w['raw'] for w in s_words[:10]]}\n" debug_info += f"Video normalized (first 10): {[w['norm'] for w in v_words[:10]]}\n" debug_info += f"Song normalized (first 10): {[w['norm'] for w in s_words[:10]]}\n" debug_info += f"Common words: {list(v_norm_words & s_norm_words)[:20]}\n" return { "video_language": video_language, "song_language": song_language, "video_words": video_words, "song_words": song_words, "matching_words": matching_words, "anchor_count": anchor_count, "debug_info": debug_info } except Exception as e: import traceback traceback.print_exc() raise HTTPException(500, f"Debug test failed: {str(e)}") @app.post("/api/debug/render_video_upload") async def debug_render_video_upload( video: UploadFile, song: UploadFile, video_transcript: UploadFile, song_transcript: UploadFile ): """Render video with uploaded files""" jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) JOBS[jid] = {"status": "processing", "note": "Uploading files..."} async def run(): try: import json jlog(jid, "📤 Receiving uploaded files...") # Save uploaded files video_path = jdir / f"video{Path(video.filename).suffix}" song_path = jdir / f"song{Path(song.filename).suffix}" video_transcript_path = jdir / "video.json" song_transcript_path = jdir / "song.json" # Write video file with open(video_path, "wb") as f: content = await video.read() f.write(content) jlog(jid, f"✅ Video saved: {video.filename}") # Write song file with open(song_path, "wb") as f: content = await song.read() f.write(content) jlog(jid, f"✅ Song saved: {song.filename}") # Write transcript files video_transcript_content = await video_transcript.read() with open(video_transcript_path, "wb") as f: f.write(video_transcript_content) jlog(jid, f"✅ Video transcript saved: {video_transcript.filename}") song_transcript_content = await song_transcript.read() with open(song_transcript_path, "wb") as f: f.write(song_transcript_content) jlog(jid, f"✅ Song transcript saved: {song_transcript.filename}") # Render the video out = jdir / "output.mp4" sync = jdir / "combined.json" jlog(jid, "🎬 Starting video synchronization...") await asyncio.to_thread( make_synced_video, clip_path=str(video_path), song_path=str(song_path), video_json_path=str(video_transcript_path), song_json_path=str(song_transcript_path), out_path=str(out), export_sync=str(sync), smooth=3 ) JOBS[jid].update( status="done", output=f"/api/download/{jid}", sync=f"/api/sync/{jid}", note="✅ Debug render complete!" ) jlog(jid, "🎉 Debug render successful!") except Exception as e: jerr(jid, e) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.post("/api/debug/render_video") async def debug_render_video(req: dict): """Render video with debug files""" video_path = req.get("video_path") song_path = req.get("song_path") video_transcript_path = req.get("video_transcript_path") song_transcript_path = req.get("song_transcript_path") if not all([video_path, song_path, video_transcript_path, song_transcript_path]): raise HTTPException(400, "All file paths required") jid = str(uuid.uuid4()) jdir = STORE / jid jdir.mkdir(parents=True, exist_ok=True) JOBS[jid] = {"status": "processing", "note": "Starting debug render..."} async def run(): try: from pathlib import Path import shutil # Verify all files exist for path, name in [(video_path, "video"), (song_path, "song"), (video_transcript_path, "video transcript"), (song_transcript_path, "song transcript")]: if not Path(path).exists(): raise FileNotFoundError(f"{name} file not found: {path}") jlog(jid, "✅ All files found, starting render...") # Copy files to job directory for consistency shutil.copy2(video_path, jdir / "video.mp4") shutil.copy2(song_path, jdir / "song.wav") shutil.copy2(video_transcript_path, jdir / "video.json") shutil.copy2(song_transcript_path, jdir / "song.json") jlog(jid, "📁 Files copied to job directory") # Render the video out = jdir / "output.mp4" sync = jdir / "combined.json" jlog(jid, "🎬 Starting video synchronization...") await asyncio.to_thread( make_synced_video, clip_path=str(jdir / "video.mp4"), song_path=str(jdir / "song.wav"), video_json_path=str(jdir / "video.json"), song_json_path=str(jdir / "song.json"), out_path=str(out), export_sync=str(sync), smooth=3 ) JOBS[jid].update( status="done", output=f"/api/download/{jid}", sync=f"/api/sync/{jid}", note="✅ Debug render complete!" ) jlog(jid, "🎉 Debug render successful!") except Exception as e: jerr(jid, e) asyncio.create_task(run()) return {"job_id": jid, "status": "processing"} @app.get("/api/debug/status/{jid}") def debug_status(jid: str): """Get debug job status with logs""" if jid not in JOBS: raise HTTPException(404, "Job not found") job_data = JOBS[jid].copy() # Add log content log_file = STORE / jid / "log.txt" if log_file.exists(): job_data["log"] = log_file.read_text() return job_data if FRONT.exists(): app.mount("/",StaticFiles(directory=str(FRONT),html=True),name="static")