|
|
""" |
|
|
Simple, clean comic generator that: |
|
|
1. Selects ONLY 12 meaningful story moments |
|
|
2. Preserves original image quality and colors |
|
|
3. Uses proper grid layouts (3x4 for 12 panels) |
|
|
""" |
|
|
|
|
|
import os |
|
|
import cv2 |
|
|
import json |
|
|
import srt |
|
|
import numpy as np |
|
|
from typing import List, Dict |
|
|
|
|
|
class SimpleComicGenerator: |
|
|
def __init__(self): |
|
|
self.target_panels = 12 |
|
|
self.frames_dir = 'frames/final' |
|
|
self.output_dir = 'output' |
|
|
|
|
|
def generate_meaningful_comic(self, video_path: str) -> bool: |
|
|
"""Generate comic with only meaningful story moments""" |
|
|
try: |
|
|
print("π¬ Starting Simple Comic Generation...") |
|
|
print(f"π Target: {self.target_panels} meaningful panels") |
|
|
|
|
|
|
|
|
subtitles = self._load_subtitles() |
|
|
if not subtitles: |
|
|
print("β No subtitles found") |
|
|
return False |
|
|
|
|
|
print(f"π Found {len(subtitles)} total subtitles") |
|
|
|
|
|
|
|
|
meaningful_moments = self._select_meaningful_moments(subtitles) |
|
|
print(f"β
Selected {len(meaningful_moments)} key story moments") |
|
|
|
|
|
|
|
|
self._extract_meaningful_frames(video_path, meaningful_moments) |
|
|
|
|
|
|
|
|
self._create_comic_pages() |
|
|
|
|
|
print("β
Comic generation complete!") |
|
|
return True |
|
|
|
|
|
except Exception as e: |
|
|
print(f"β Error: {e}") |
|
|
return False |
|
|
|
|
|
def _load_subtitles(self) -> List[Dict]: |
|
|
"""Load subtitles from SRT file""" |
|
|
try: |
|
|
with open('test1.srt', 'r', encoding='utf-8') as f: |
|
|
subs = list(srt.parse(f.read())) |
|
|
|
|
|
|
|
|
subtitle_list = [] |
|
|
for sub in subs: |
|
|
subtitle_list.append({ |
|
|
'index': sub.index, |
|
|
'text': sub.content, |
|
|
'start': sub.start.total_seconds(), |
|
|
'end': sub.end.total_seconds() |
|
|
}) |
|
|
return subtitle_list |
|
|
except: |
|
|
return [] |
|
|
|
|
|
def _select_meaningful_moments(self, subtitles: List[Dict]) -> List[Dict]: |
|
|
"""Select ONLY the most meaningful story moments""" |
|
|
|
|
|
|
|
|
scored_subs = [] |
|
|
total = len(subtitles) |
|
|
|
|
|
for i, sub in enumerate(subtitles): |
|
|
score = 0 |
|
|
text = sub['text'].lower() |
|
|
position = i / total |
|
|
|
|
|
|
|
|
if position < 0.1: |
|
|
score += 5 |
|
|
elif position > 0.9: |
|
|
score += 5 |
|
|
elif 0.45 < position < 0.55: |
|
|
score += 4 |
|
|
|
|
|
|
|
|
important_words = [ |
|
|
'but', 'however', 'suddenly', 'finally', 'then', |
|
|
'help', 'save', 'fight', 'love', 'hate', 'die', |
|
|
'win', 'lose', 'find', 'discover', 'realize', |
|
|
'important', 'must', 'need', 'want' |
|
|
] |
|
|
|
|
|
for word in important_words: |
|
|
if word in text: |
|
|
score += 3 |
|
|
|
|
|
|
|
|
if '!' in text: |
|
|
score += 2 |
|
|
if '?' in text: |
|
|
score += 1 |
|
|
|
|
|
|
|
|
if len(text.split()) > 10: |
|
|
score += 2 |
|
|
elif len(text.split()) > 5: |
|
|
score += 1 |
|
|
|
|
|
scored_subs.append((score, i, sub)) |
|
|
|
|
|
|
|
|
scored_subs.sort(key=lambda x: x[0], reverse=True) |
|
|
|
|
|
|
|
|
selected = [] |
|
|
selected_indices = set() |
|
|
|
|
|
|
|
|
if subtitles: |
|
|
selected.append(subtitles[0]) |
|
|
selected_indices.add(0) |
|
|
if len(subtitles) > 1: |
|
|
selected.append(subtitles[-1]) |
|
|
selected_indices.add(len(subtitles) - 1) |
|
|
|
|
|
|
|
|
min_spacing = max(1, total // (self.target_panels * 2)) |
|
|
|
|
|
for score, idx, sub in scored_subs: |
|
|
if len(selected) >= self.target_panels: |
|
|
break |
|
|
|
|
|
|
|
|
too_close = False |
|
|
for sel_idx in selected_indices: |
|
|
if abs(idx - sel_idx) < min_spacing: |
|
|
too_close = True |
|
|
break |
|
|
|
|
|
if not too_close and idx not in selected_indices: |
|
|
selected.append(sub) |
|
|
selected_indices.add(idx) |
|
|
|
|
|
|
|
|
selected.sort(key=lambda x: x['start']) |
|
|
|
|
|
|
|
|
return selected[:self.target_panels] |
|
|
|
|
|
def _extract_meaningful_frames(self, video_path: str, moments: List[Dict]): |
|
|
"""Extract frames ONLY for meaningful moments""" |
|
|
|
|
|
|
|
|
os.makedirs(self.frames_dir, exist_ok=True) |
|
|
for f in os.listdir(self.frames_dir): |
|
|
if f.endswith('.png'): |
|
|
os.remove(os.path.join(self.frames_dir, f)) |
|
|
|
|
|
cap = cv2.VideoCapture(video_path) |
|
|
fps = cap.get(cv2.CAP_PROP_FPS) |
|
|
|
|
|
print(f"π₯ Extracting {len(moments)} frames...") |
|
|
|
|
|
for i, moment in enumerate(moments): |
|
|
|
|
|
timestamp = (moment['start'] + moment['end']) / 2 |
|
|
frame_num = int(timestamp * fps) |
|
|
|
|
|
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num) |
|
|
ret, frame = cap.read() |
|
|
|
|
|
if ret: |
|
|
|
|
|
output_path = os.path.join(self.frames_dir, f'frame{i:03d}.png') |
|
|
cv2.imwrite(output_path, frame, [cv2.IMWRITE_PNG_COMPRESSION, 0]) |
|
|
print(f" β Frame {i+1}/{len(moments)}: {moment['text'][:50]}...") |
|
|
else: |
|
|
print(f" β Failed to extract frame {i+1}") |
|
|
|
|
|
cap.release() |
|
|
print(f"β
Extracted {len(moments)} frames") |
|
|
|
|
|
def _create_comic_pages(self): |
|
|
"""Create comic pages with proper grid layout""" |
|
|
|
|
|
frames = sorted([f for f in os.listdir(self.frames_dir) if f.endswith('.png')]) |
|
|
num_frames = len(frames) |
|
|
|
|
|
if num_frames == 0: |
|
|
print("β No frames to create comic") |
|
|
return |
|
|
|
|
|
print(f"π Creating comic with {num_frames} panels...") |
|
|
|
|
|
|
|
|
if num_frames <= 6: |
|
|
layout = "2x3" |
|
|
rows, cols = 2, 3 |
|
|
elif num_frames <= 9: |
|
|
layout = "3x3" |
|
|
rows, cols = 3, 3 |
|
|
elif num_frames <= 12: |
|
|
layout = "3x4" |
|
|
rows, cols = 3, 4 |
|
|
else: |
|
|
layout = "4x4" |
|
|
rows, cols = 4, 4 |
|
|
|
|
|
print(f"π Using {layout} grid layout") |
|
|
|
|
|
|
|
|
comic_data = { |
|
|
'frames': frames, |
|
|
'layout': layout, |
|
|
'rows': rows, |
|
|
'cols': cols, |
|
|
'total_panels': num_frames |
|
|
} |
|
|
|
|
|
os.makedirs(self.output_dir, exist_ok=True) |
|
|
with open(os.path.join(self.output_dir, 'comic_data.json'), 'w') as f: |
|
|
json.dump(comic_data, f, indent=2) |
|
|
|
|
|
|
|
|
self._create_html_viewer(frames, rows, cols) |
|
|
|
|
|
def _create_html_viewer(self, frames: List[str], rows: int, cols: int): |
|
|
"""Create simple HTML viewer for the comic""" |
|
|
|
|
|
html = '''<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>Story Comic - 12 Key Moments</title> |
|
|
<style> |
|
|
body { |
|
|
margin: 0; |
|
|
padding: 20px; |
|
|
background: #f0f0f0; |
|
|
font-family: Arial, sans-serif; |
|
|
} |
|
|
.comic-container { |
|
|
max-width: 1200px; |
|
|
margin: 0 auto; |
|
|
background: white; |
|
|
padding: 20px; |
|
|
box-shadow: 0 0 20px rgba(0,0,0,0.1); |
|
|
} |
|
|
.comic-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(''' + str(cols) + ''', 1fr); |
|
|
grid-template-rows: repeat(''' + str(rows) + ''', 1fr); |
|
|
gap: 10px; |
|
|
width: 100%; |
|
|
} |
|
|
.panel { |
|
|
position: relative; |
|
|
border: 2px solid #333; |
|
|
overflow: hidden; |
|
|
background: #fff; |
|
|
} |
|
|
.panel img { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
object-fit: contain; |
|
|
display: block; |
|
|
background: #000; |
|
|
} |
|
|
.panel-number { |
|
|
position: absolute; |
|
|
top: 5px; |
|
|
left: 5px; |
|
|
background: rgba(0,0,0,0.7); |
|
|
color: white; |
|
|
padding: 2px 6px; |
|
|
border-radius: 3px; |
|
|
font-size: 12px; |
|
|
} |
|
|
h1 { |
|
|
text-align: center; |
|
|
margin-bottom: 30px; |
|
|
} |
|
|
.info { |
|
|
text-align: center; |
|
|
color: #666; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="comic-container"> |
|
|
<h1>π Story Comic - Key Moments</h1> |
|
|
<div class="info">''' + str(len(frames)) + ''' panels showing the most important story moments</div> |
|
|
<div class="comic-grid"> |
|
|
''' |
|
|
|
|
|
for i, frame in enumerate(frames): |
|
|
html += f''' |
|
|
<div class="panel"> |
|
|
<div class="panel-number">{i+1}</div> |
|
|
<img src="../frames/final/{frame}" alt="Panel {i+1}"> |
|
|
</div> |
|
|
''' |
|
|
|
|
|
html += ''' |
|
|
</div> |
|
|
</div> |
|
|
</body> |
|
|
</html>''' |
|
|
|
|
|
output_path = os.path.join(self.output_dir, 'comic_simple.html') |
|
|
with open(output_path, 'w', encoding='utf-8') as f: |
|
|
f.write(html) |
|
|
|
|
|
print(f"β
Comic viewer saved to: {output_path}") |