| 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()) |
| }) |
|
|
|
|