Spaces:
tester343
/
Configuration error

Testcomic / app_enhanced.py
tester343's picture
Update app_enhanced.py
5ccb565 verified
import spaces # <--- CRITICAL: MUST BE THE FIRST IMPORT
import os
import time
import threading
import json
import traceback
import shutil
import cv2
import numpy as np
import srt
from flask import Flask, jsonify, request, send_from_directory, send_file
# ======================================================
# πŸš€ ZEROGPU CONFIGURATION
# ======================================================
@spaces.GPU
def gpu_warmup():
import torch
return torch.cuda.is_available()
# ======================================================
# πŸ’Ύ STORAGE SETUP
# ======================================================
BASE_STORAGE_PATH = '/data' if os.path.exists('/data') else '.'
BASE_USER_DIR = os.path.join(BASE_STORAGE_PATH, "userdata")
os.makedirs(BASE_USER_DIR, exist_ok=True)
# ======================================================
# πŸ”§ JSON SANITIZER (FIX FOR int64 ERROR)
# ======================================================
def sanitize_json(obj):
if isinstance(obj, dict):
return {k: sanitize_json(v) for k, v in obj.items()}
elif isinstance(obj, list):
return [sanitize_json(v) for v in obj]
elif isinstance(obj, (np.int64, np.int32, np.int16)):
return int(obj)
elif isinstance(obj, (np.float64, np.float32)):
return float(obj)
elif isinstance(obj, np.ndarray):
return sanitize_json(obj.tolist())
return obj
# ======================================================
# 🧠 CORE GPU GENERATOR
# ======================================================
@spaces.GPU(duration=300)
def generate_comic_gpu(video_path, user_dir, frames_dir, metadata_path, target_pages):
# Heavy AI imports inside function to avoid Startup Timeout
from backend.keyframes.keyframes import black_bar_crop
from backend.simple_color_enhancer import SimpleColorEnhancer
from backend.subtitles.subs_real import get_real_subtitles
from backend.ai_bubble_placement import ai_bubble_placer
from backend.ai_enhanced_core import face_detector
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS) or 25
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
duration = total_frames / fps
cap.release()
# 1. Subtitles
user_srt = os.path.join(user_dir, 'subs.srt')
try:
get_real_subtitles(video_path)
if os.path.exists('test1.srt'): shutil.move('test1.srt', user_srt)
except:
with open(user_srt, 'w') as f: f.write("1\n00:00:01,000 --> 00:00:05,000\nDialogue...\n")
with open(user_srt, 'r', encoding='utf-8') as f:
try: all_subs = list(srt.parse(f.read()))
except: all_subs = []
# 2. Logic for 5 Panels Per Page
panels_per_page = 5
target_pages = int(target_pages)
total_needed = target_pages * panels_per_page
if not all_subs:
times = np.linspace(1, max(1.1, duration-1), total_needed)
moments = [{'text': '', 'start': t} for t in times]
elif len(all_subs) <= total_needed:
moments = [{'text': s.content, 'start': s.start.total_seconds()} for s in all_subs]
while len(moments) < total_needed: moments.append({'text': '', 'start': duration/2})
else:
indices = np.linspace(0, len(all_subs) - 1, total_needed, dtype=int)
moments = [{'text': all_subs[i].content, 'start': all_subs[i].start.total_seconds()} for i in indices]
# 3. Frame Extraction
frame_metadata = {}
cap = cv2.VideoCapture(video_path)
frame_files = []
for i, m in enumerate(moments):
cap.set(cv2.CAP_PROP_POS_MSEC, m['start'] * 1000)
ret, frame = cap.read()
if ret:
fname = f"frame_{i:04d}.png"
p = os.path.join(frames_dir, fname)
cv2.imwrite(p, frame)
frame_metadata[fname] = {'dialogue': m['text'], 'time': m['start']}
frame_files.append(fname)
cap.release()
with open(metadata_path, 'w') as f:
json.dump(sanitize_json(frame_metadata), f)
try: black_bar_crop()
except: pass
# 4. Enhance & Assemble
se = SimpleColorEnhancer()
pages_data = []
for p_idx in range(target_pages):
p_p, p_b = [], []
start = p_idx * 5
for i in range(start, start + 5):
if i >= len(frame_files): break
f_name = frame_files[i]
img_p = os.path.join(frames_dir, f_name)
try: se.enhance_single(img_p, img_p)
except: pass
txt = frame_metadata[f_name]['dialogue']
try:
faces = face_detector.detect_faces(img_p)
lip = face_detector.get_lip_position(img_p, faces[0]) if faces else (-1, -1)
bx, by = ai_bubble_placer.place_bubble_ai(img_p, lip)
item = {'dialog': txt, 'x': bx, 'y': by}
except:
item = {'dialog': txt, 'x': 50, 'y': 25}
p_p.append({'image': f_name})
p_b.append(item)
pages_data.append({'panels': p_p, 'bubbles': p_b})
return sanitize_json(pages_data)
# ======================================================
# πŸ”§ APP ENGINE
# ======================================================
app = Flask(__name__)
@app.route('/')
def index(): return INDEX_HTML
@app.route('/uploader', methods=['POST'])
def uploader():
sid = request.args.get('sid')
u_dir = os.path.join(BASE_USER_DIR, sid)
f_dir = os.path.join(u_dir, 'frames'); o_dir = os.path.join(u_dir, 'output')
os.makedirs(f_dir, exist_ok=True); os.makedirs(o_dir, exist_ok=True)
vid_p = os.path.join(u_dir, 'video.mp4')
request.files['file'].save(vid_p)
pages = request.form.get('pages', 2)
def task():
try:
with open(os.path.join(o_dir, 'status.json'), 'w') as f:
json.dump({'message': 'Generating Panels...', 'progress': 30}, f)
data = generate_comic_gpu(vid_p, u_dir, f_dir, os.path.join(f_dir, 'meta.json'), pages)
with open(os.path.join(o_dir, 'pages.json'), 'w') as f: json.dump(data, f)
with open(os.path.join(o_dir, 'status.json'), 'w') as f:
json.dump({'message': 'Complete', 'progress': 100}, f)
except Exception as e:
with open(os.path.join(o_dir, 'status.json'), 'w') as f:
json.dump({'message': f'Error: {str(e)}', 'progress': -1}, f)
threading.Thread(target=task).start()
return jsonify({'success': True})
@app.route('/status')
def status():
sid = request.args.get('sid')
p = os.path.join(BASE_USER_DIR, sid, 'output', 'status.json')
if os.path.exists(p): return send_file(p)
return jsonify({'progress': 0})
@app.route('/frames/<sid>/<path:filename>')
def get_frame(sid, filename):
return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'frames'), filename)
@app.route('/output/<sid>/<path:filename>')
def get_output(sid, filename):
return send_from_directory(os.path.join(BASE_USER_DIR, sid, 'output'), filename)
# ======================================================
# 🌐 UI HTML (USING YOUR FINAL COORDINATES)
# ======================================================
INDEX_HTML = '''
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8"><title>Elite Geometric Comic Maker</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Comic+Neue:wght@700&family=Lato:wght@400;900&display=swap" rel="stylesheet">
<style>
/* COORDINATES INJECTED FROM YOUR TOOL */
:root {
--w: 1000px;
--h: 700px;
--tierY: 350px;
--gut: 5.25px; /* Half of 10.5px Gutter */
--r1-topX: 641.2px;
--r1-botX: 588.2px;
--r2L-topX: 284.2px;
--r2L-botX: 314.2px;
--r2R-topX: 618.2px;
--r2R-botX: 678.2px;
}
body { background: #111; font-family: 'Lato', sans-serif; margin: 0; padding: 20px; color: white; }
.setup-box { max-width: 450px; margin: 80px auto; background: white; padding: 40px; border-radius: 12px; color: black; text-align: center; }
.comic-page {
background: white; width: var(--w); height: var(--h); margin: 40px auto;
border: 12px solid black; position: relative; overflow: hidden;
}
.panel { position: absolute; background: #000; overflow: hidden; cursor: pointer; }
.panel img { width: 120%; height: 120%; object-fit: cover; position: absolute; top: -10%; left: -10%; pointer-events: none; }
/* ⚑ YOUR PRECISE GEOMETRY ⚑ */
/* Panel 1 (Top Left) */
.p1 { top: 0; left: 0; width: 100%; height: var(--tierY); clip-path: polygon(0 0, calc(var(--r1-topX) - var(--gut)) 0, calc(var(--r1-botX) - var(--gut)) var(--tierY), 0 var(--tierY)); }
/* Panel 2 (Top Right) */
.p2 { top: 0; left: 0; width: 100%; height: var(--tierY); clip-path: polygon(calc(var(--r1-topX) + var(--gut)) 0, var(--w) 0, var(--w) var(--tierY), calc(var(--r1-botX) + var(--gut)) var(--tierY)); }
/* Panel 3 (Bottom Left) */
.p3 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(0 0, calc(var(--r2L-topX) - var(--gut)) 0, calc(var(--r2L-botX) - var(--gut)) 100%, 0 100%); }
/* Panel 4 (Bottom Center) */
.p4 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(calc(var(--r2L-topX) + var(--gut)) 0, calc(var(--r2R-topX) - var(--gut)) 0, calc(var(--r2R-botX) - var(--gut)) 100%, calc(var(--r2L-botX) + var(--gut)) 100%); }
/* Panel 5 (Bottom Right) */
.p5 { top: calc(var(--tierY) + var(--gut)); left: 0; width: 100%; height: calc(var(--h) - var(--tierY)); clip-path: polygon(calc(var(--r2R-topX) + var(--gut)) 0, var(--w) 0, var(--w) 100%, calc(var(--r2R-botX) + var(--gut)) 100%); }
.bubble {
position: absolute; background: white; border: 2.5px solid black; border-radius: 25px;
padding: 10px 18px; font-family: 'Comic Neue'; font-weight: bold; font-size: 15px;
color: black; min-width: 100px; text-align: center; cursor: move; z-index: 10;
}
.controls { position: fixed; bottom: 20px; right: 20px; background: #000; padding: 20px; border-radius: 12px; width: 220px; border: 2px solid #333; }
button { width: 100%; padding: 10px; margin-top: 10px; cursor: pointer; font-weight: bold; border-radius: 6px; border: none; }
.hidden { display: none; }
.loader { border: 5px solid #333; border-top: 5px solid #00d2ff; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 20px auto; }
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
</style>
</head>
<body>
<div id="u-zone" class="setup-box">
<h1>🎬 Elite Comic Maker</h1>
<p>Using Final Dragged Coordinates</p>
<input type="file" id="vid" accept="video/mp4"><br><br>
<label>Total Pages: </label><input type="number" id="pg" value="2" style="width:50px">
<br><br>
<button onclick="start()" style="background:#00d2ff; color:black;">πŸš€ GENERATE COMIC</button>
<div id="loading" class="hidden"><div class="loader"></div><p id="st">Acquiring GPU...</p></div>
</div>
<div id="editor-zone" class="hidden">
<div id="output"></div>
<div class="controls">
<h4 style="margin:0; color:#00d2ff;">EDITOR</h4>
<button onclick="addB()" style="background:#2ecc71; color:white;">πŸ’¬ Add Bubble</button>
<button onclick="exportPNG()" style="background:#3498db; color:white;">πŸ“₯ Download PNGs</button>
<button onclick="location.reload()" style="background:#e74c3c; color:white;">🏠 Reset</button>
</div>
</div>
<script>
let sid = 's' + Math.random().toString(36).substr(2,9);
let selP = null;
async function start() {
const f = document.getElementById('vid').files[0];
if(!f) return alert("Select a video!");
document.getElementById('loading').classList.remove('hidden');
const fd = new FormData(); fd.append('file', f); fd.append('pages', document.getElementById('pg').value);
await fetch(`/uploader?sid=${sid}`, {method: 'POST', body: fd});
const itv = setInterval(async () => {
const r = await fetch(`/status?sid=${sid}`); const d = await r.json();
document.getElementById('st').innerText = d.message || "Working...";
if(d.progress >= 100) { clearInterval(itv); load(); }
}, 2000);
}
async function load() {
const r = await fetch(`/output/${sid}/pages.json`); const pages = await r.json();
document.getElementById('u-zone').classList.add('hidden');
document.getElementById('editor-zone').classList.remove('hidden');
const out = document.getElementById('output');
pages.forEach(p => {
const pgDiv = document.createElement('div'); pgDiv.className = 'comic-page';
p.panels.forEach((pan, i) => {
const pDiv = document.createElement('div'); pDiv.className = 'panel p' + (i+1);
pDiv.onclick = (e) => { e.stopPropagation(); selP=pDiv; };
const img = document.createElement('img'); img.src = `/frames/${sid}/${pan.image}`;
pDiv.appendChild(img);
if(p.bubbles[i]) pDiv.appendChild(createB(p.bubbles[i].dialog, p.bubbles[i].x, p.bubbles[i].y));
pgDiv.appendChild(pDiv);
});
out.appendChild(pgDiv);
});
}
function createB(txt, x, y) {
const b = document.createElement('div'); b.className = 'bubble';
b.innerText = txt || '...'; b.style.left = (x || 50) + 'px'; b.style.top = (y || 20) + 'px';
b.onmousedown = (e) => {
e.stopPropagation();
let ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
document.onmousemove = (ev) => { b.style.left=(ev.clientX-ox)+'px'; b.style.top=(ev.clientY-oy)+'px'; };
document.onmouseup = () => { document.onmousemove = null; };
};
b.ondblclick = () => { let n = prompt("Edit text:", b.innerText); if(n) b.innerText = n; };
return b;
}
function addB() { if(selP) selP.appendChild(createB("Dialogue", 60, 60)); }
async function exportPNG() {
const pgs = document.querySelectorAll('.comic-page');
for(let pg of pgs) {
const url = await htmlToImage.toPng(pg, {pixelRatio: 2});
const l = document.createElement('a'); l.download='ComicPage.png'; l.href=url; l.click();
}
}
</script>
</body></html>
'''
if __name__ == '__main__':
try: gpu_warmup()
except: pass
app.run(host='0.0.0.0', port=7860)