import hashlib import re from pathlib import Path from fastapi import FastAPI from fastapi.responses import FileResponse, JSONResponse, PlainTextResponse from gtts import gTTS ROOT = Path(__file__).resolve().parent STATIC_DIR = ROOT / "static" SLIDES_DIR = ROOT / "slides" AUDIO_DIR = ROOT / "audio" SCRIPT_FILE = ROOT / "scripts.txt" AUDIO_DIR.mkdir(exist_ok=True) app = FastAPI() def read_slide_data(): """ scripts.txt에서 ## 제목과 「원고」를 추출합니다. slides/0.png ~ slides/19.png와 index 번호로 연결됩니다. """ if not SCRIPT_FILE.exists(): return [] text = SCRIPT_FILE.read_text(encoding="utf-8") titles = re.findall(r"##\s*(.+)", text) speeches = re.findall(r"「([\s\S]*?)」", text) slides = [] for i, speech in enumerate(speeches): title = titles[i].strip() if i < len(titles) else f"Slide {i}" image_name = f"{i}.png" image_path = SLIDES_DIR / image_name slides.append({ "index": i, "title": title, "speech": speech.strip(), "image": f"/slides/{image_name}", "image_exists": image_path.exists(), "audio": f"/api/audio/{i}" }) return slides @app.get("/") def index(): return FileResponse(STATIC_DIR / "index.html") @app.get("/style.css") def css(): return FileResponse(STATIC_DIR / "style.css", media_type="text/css") @app.get("/app.js") def js(): return FileResponse(STATIC_DIR / "app.js", media_type="application/javascript") @app.get("/api/slides") def api_slides(): return JSONResponse(read_slide_data()) @app.get("/slides/{image_name}") def slide_image(image_name: str): """ slides/0.png ~ slides/19.png 제공. 허용된 파일명 패턴만 제공합니다. """ if not re.fullmatch(r"\d+\.png", image_name): return PlainTextResponse("Invalid image name", status_code=400) image_path = SLIDES_DIR / image_name if not image_path.exists(): return PlainTextResponse(f"Image not found: {image_name}", status_code=404) return FileResponse(image_path, media_type="image/png") @app.get("/api/audio/{slide_index}") def api_audio(slide_index: int): """ 현재 페이지 원고를 gTTS로 mp3 생성 후 반환합니다. 한 번 생성된 mp3는 audio/에 캐시됩니다. """ slides = read_slide_data() if slide_index < 0 or slide_index >= len(slides): return PlainTextResponse("Invalid slide index", status_code=404) text = slides[slide_index]["speech"] if not text: return PlainTextResponse("Empty script", status_code=400) key = hashlib.md5(f"{slide_index}-{text}".encode("utf-8")).hexdigest() mp3_path = AUDIO_DIR / f"slide_{slide_index:02d}_{key}.mp3" if not mp3_path.exists(): try: tts = gTTS(text=text, lang="en", slow=False) tts.save(str(mp3_path)) except Exception as e: return PlainTextResponse(f"TTS generation failed: {e}", status_code=500) return FileResponse(mp3_path, media_type="audio/mpeg") @app.get("/health") def health(): return JSONResponse({ "status": "ok", "scripts_txt": SCRIPT_FILE.exists(), "slides_dir": SLIDES_DIR.exists(), "static_dir": STATIC_DIR.exists(), "slide_count": len(read_slide_data()) })