jebin2 commited on
Commit
943db10
·
1 Parent(s): ec6ad2f

server added

Browse files
comic_panel_extractor/config.py CHANGED
@@ -7,5 +7,8 @@ class Config:
7
  output_folder: str = "temp_dir"
8
  distance_threshold: int = 70
9
  vertical_threshold: int = 30
10
- text_cood_path: str = f"{output_folder}/detect_and_group_text.json"
11
- min_text_length: int = 2
 
 
 
 
7
  output_folder: str = "temp_dir"
8
  distance_threshold: int = 70
9
  vertical_threshold: int = 30
10
+ text_cood_file_name: str = "detect_and_group_text.json"
11
+ min_text_length: int = 2
12
+
13
+ def get_text_cood_file_path(config: Config):
14
+ return f'{config.output_folder}/{config.text_cood_file_name}'
comic_panel_extractor/main.py CHANGED
@@ -13,11 +13,12 @@ import shutil
13
  class ComicPanelExtractor:
14
  """Main class that orchestrates the comic panel extraction process."""
15
 
16
- def __init__(self, config: Config):
17
  self.config = config
18
- if Path(self.config.output_folder).exists():
19
- shutil.rmtree(self.config.output_folder)
20
- Path(self.config.output_folder).mkdir(exist_ok=True)
 
21
 
22
  self.image_processor = ImageProcessor(self.config)
23
  self.panel_extractor = PanelExtractor(self.config)
@@ -37,11 +38,11 @@ class ComicPanelExtractor:
37
  cleaned_path = self.image_processor.clean_dilated_image(dilated_path)
38
 
39
  # Step 4: Extract panels
40
- panel_images, panel_data = self.panel_extractor.extract_panels(
41
  cleaned_path, min_width_ratio=0.1
42
  )
43
 
44
- return panel_images, panel_data
45
 
46
  def _detect_text_bubbles(self) -> List[dict]:
47
  """Detect text bubbles in the comic image."""
 
13
  class ComicPanelExtractor:
14
  """Main class that orchestrates the comic panel extraction process."""
15
 
16
+ def __init__(self, config: Config, reset: bool = True):
17
  self.config = config
18
+ if reset:
19
+ if Path(self.config.output_folder).exists():
20
+ shutil.rmtree(self.config.output_folder)
21
+ Path(self.config.output_folder).mkdir(exist_ok=True)
22
 
23
  self.image_processor = ImageProcessor(self.config)
24
  self.panel_extractor = PanelExtractor(self.config)
 
38
  cleaned_path = self.image_processor.clean_dilated_image(dilated_path)
39
 
40
  # Step 4: Extract panels
41
+ panel_images, panel_data, all_panel_path = self.panel_extractor.extract_panels(
42
  cleaned_path, min_width_ratio=0.1
43
  )
44
 
45
+ return panel_images, panel_data, all_panel_path
46
 
47
  def _detect_text_bubbles(self) -> List[dict]:
48
  """Detect text bubbles in the comic image."""
comic_panel_extractor/panel_extractor.py CHANGED
@@ -60,11 +60,11 @@ class PanelExtractor:
60
  )
61
 
62
  # Extract panel images and save
63
- panel_images, panel_data = self._save_panels(
64
  filtered_panels, original, width, height
65
  )
66
 
67
- return panel_images, panel_data
68
 
69
  def _find_panel_rows(self, dilated: np.ndarray, row_thresh: int) -> List[Tuple[int, int]]:
70
  """Find panel rows by analyzing horizontal black percentages."""
@@ -157,6 +157,7 @@ class PanelExtractor:
157
  visual_output = original.copy()
158
  panel_images = []
159
  panel_data = []
 
160
 
161
  for idx, (x1, y1, x2, y2) in enumerate(panels, 1):
162
  # Extract panel image
@@ -170,6 +171,7 @@ class PanelExtractor:
170
  # Save panel image
171
  panel_path = f'{self.config.output_folder}/panel_{idx}.jpg'
172
  cv2.imwrite(str(panel_path), panel_img)
 
173
 
174
  # Draw visualization
175
  cv2.rectangle(visual_output, (x1, y1), (x2, y2), (0, 255, 0), 2)
@@ -181,4 +183,4 @@ class PanelExtractor:
181
  cv2.imwrite(str(visual_path), visual_output)
182
 
183
  print(f"✅ Extracted {len(panels)} panels after filtering.")
184
- return panel_images, panel_data
 
60
  )
61
 
62
  # Extract panel images and save
63
+ panel_images, panel_data, all_panel_path = self._save_panels(
64
  filtered_panels, original, width, height
65
  )
66
 
67
+ return panel_images, panel_data, all_panel_path
68
 
69
  def _find_panel_rows(self, dilated: np.ndarray, row_thresh: int) -> List[Tuple[int, int]]:
70
  """Find panel rows by analyzing horizontal black percentages."""
 
157
  visual_output = original.copy()
158
  panel_images = []
159
  panel_data = []
160
+ all_panel_path = []
161
 
162
  for idx, (x1, y1, x2, y2) in enumerate(panels, 1):
163
  # Extract panel image
 
171
  # Save panel image
172
  panel_path = f'{self.config.output_folder}/panel_{idx}.jpg'
173
  cv2.imwrite(str(panel_path), panel_img)
174
+ all_panel_path.append(panel_path)
175
 
176
  # Draw visualization
177
  cv2.rectangle(visual_output, (x1, y1), (x2, y2), (0, 255, 0), 2)
 
183
  cv2.imwrite(str(visual_path), visual_output)
184
 
185
  print(f"✅ Extracted {len(panels)} panels after filtering.")
186
+ return panel_images, panel_data, all_panel_path
comic_panel_extractor/server.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, File, UploadFile, HTTPException, Form, Query
2
+ from fastapi.responses import HTMLResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import cv2
5
+ import numpy as np
6
+ from PIL import Image
7
+ import io
8
+ import base64
9
+ import os
10
+ from typing import List
11
+ import uuid
12
+ from .config import Config
13
+ from .main import ComicPanelExtractor
14
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
15
+ import traceback
16
+ from pathlib import Path
17
+ import shutil
18
+ import time
19
+
20
+ base_output_folder = "api_outputs"
21
+ static_folder = "./static"
22
+ output_folder = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), f'./{base_output_folder}')))
23
+ static_folder = os.path.abspath(os.path.realpath(os.path.join(os.path.dirname(__file__), static_folder)))
24
+
25
+ # Create directories for uploads and outputs
26
+ os.makedirs(output_folder, exist_ok=True)
27
+ os.makedirs(static_folder, exist_ok=True)
28
+
29
+ # Templates
30
+ template_dirs = [static_folder]
31
+ env = Environment(
32
+ loader=FileSystemLoader(template_dirs),
33
+ autoescape=select_autoescape(['html', 'xml'])
34
+ )
35
+
36
+ app = FastAPI(title="Comic Panel Extractor", version="1.0.0")
37
+
38
+ # Mount static files
39
+ app.mount(static_folder, StaticFiles(directory=static_folder), name="static")
40
+ # app.mount(output_folder, StaticFiles(directory=output_folder), name="api_outputs")
41
+
42
+ def delete_folder_if_old_or_empty(parent_folder, age_days=1):
43
+ """
44
+ Delete subfolders inside `parent_folder` if they are empty
45
+ or older than `age_days`.
46
+
47
+ Args:
48
+ parent_folder (str): Path to the parent directory.
49
+ age_days (int): Number of days before a folder is considered old.
50
+ """
51
+ try:
52
+ current_time = time.time()
53
+ age_seconds = age_days * 24 * 60 * 60
54
+
55
+ # Loop through all items in the parent folder
56
+ for entry in os.scandir(parent_folder):
57
+ if entry.is_dir():
58
+ folder_path = entry.path
59
+ # Check if folder is empty
60
+ if not os.listdir(folder_path):
61
+ shutil.rmtree(folder_path)
62
+ print(f"Deleted empty folder: {folder_path}")
63
+ continue
64
+
65
+ # Check if folder is older than age_days
66
+ folder_mtime = os.path.getmtime(folder_path)
67
+ if current_time - folder_mtime > age_seconds:
68
+ shutil.rmtree(folder_path)
69
+ print(f"Deleted old folder (>{age_days} day): {folder_path}")
70
+
71
+ except Exception as e:
72
+ print(f"Error cleaning subfolders in {parent_folder}: {e}")
73
+
74
+ # Routes
75
+ @app.get("/", response_class=HTMLResponse)
76
+ async def index(request: Request):
77
+ delete_folder_if_old_or_empty(output_folder)
78
+ template = env.get_template("index.html") # From tool/
79
+ html_content = template.render(request=request)
80
+ return HTMLResponse(content=html_content)
81
+
82
+ @app.post("/convert")
83
+ async def convert_comic(file: UploadFile = File(...)):
84
+ """
85
+ Upload a comic page and extract panels
86
+ """
87
+ # Generate unique filename
88
+ file_id = os.path.splitext(file.filename)[0]
89
+ specific_output_folder = f'{output_folder}/{file_id}'
90
+
91
+ shutil.rmtree(specific_output_folder, ignore_errors=True)
92
+ Path(specific_output_folder).mkdir(exist_ok=True)
93
+ file_path = f'{specific_output_folder}/{file.filename}'
94
+
95
+ # Save uploaded file
96
+ try:
97
+ content = await file.read()
98
+ with open(file_path, "wb") as f:
99
+ f.write(content)
100
+
101
+ # Extract panels
102
+ config = Config()
103
+ config.input_path = file_path
104
+ config.output_folder = specific_output_folder
105
+ _, _, all_panel_path = ComicPanelExtractor(config, reset=False).extract_panels_from_comic()
106
+ all_panel_path = [f'/{base_output_folder}{path.split(output_folder)[-1]}' for path in all_panel_path]
107
+
108
+ return {
109
+ "success": True,
110
+ "message": f"Extracted {len(all_panel_path)} panels",
111
+ "panels": all_panel_path
112
+ }
113
+
114
+ except Exception as e:
115
+ print(f"Error processing image: {str(e)} {traceback.format_exc()}")
116
+ raise HTTPException(status_code=500, detail=f"Error processing image: {str(e)} {traceback.format_exc()}")
117
+
118
+ @app.get("/api_outputs/{folder}/{filename}")
119
+ async def get_output_file(folder: str, filename: str):
120
+ file_path = os.path.join(output_folder, folder, filename)
121
+ if not os.path.exists(file_path):
122
+ raise HTTPException(status_code=404, detail="File not found")
123
+ return FileResponse(file_path)
124
+
125
+ def main():
126
+ import uvicorn
127
+ uvicorn.run(app, host="0.0.0.0", port=8000)
128
+
129
+ if __name__ == "__main__":
130
+ main()
comic_panel_extractor/static/index.html ADDED
@@ -0,0 +1,539 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Comic Panel Extractor</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ padding: 20px;
20
+ color: #333;
21
+ }
22
+
23
+ .container {
24
+ max-width: 1200px;
25
+ margin: 0 auto;
26
+ background: rgba(255, 255, 255, 0.95);
27
+ border-radius: 20px;
28
+ padding: 40px;
29
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
30
+ backdrop-filter: blur(10px);
31
+ }
32
+
33
+ h1 {
34
+ text-align: center;
35
+ font-size: 2.5em;
36
+ font-weight: 700;
37
+ background: linear-gradient(45deg, #667eea, #764ba2);
38
+ -webkit-background-clip: text;
39
+ -webkit-text-fill-color: transparent;
40
+ background-clip: text;
41
+ margin-bottom: 40px;
42
+ }
43
+
44
+ .upload-section {
45
+ margin-bottom: 40px;
46
+ }
47
+
48
+ .upload-area {
49
+ border: 3px dashed #667eea;
50
+ border-radius: 15px;
51
+ padding: 60px 40px;
52
+ text-align: center;
53
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
54
+ transition: all 0.3s ease;
55
+ cursor: pointer;
56
+ position: relative;
57
+ overflow: hidden;
58
+ }
59
+
60
+ .upload-area:hover {
61
+ border-color: #764ba2;
62
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.2) 0%, rgba(118, 75, 162, 0.2) 100%);
63
+ transform: translateY(-2px);
64
+ }
65
+
66
+ .upload-area.dragover {
67
+ border-color: #764ba2;
68
+ background: linear-gradient(135deg, rgba(102, 126, 234, 0.3) 0%, rgba(118, 75, 162, 0.3) 100%);
69
+ }
70
+
71
+ .upload-icon {
72
+ font-size: 3em;
73
+ margin-bottom: 20px;
74
+ color: #667eea;
75
+ }
76
+
77
+ .upload-text {
78
+ font-size: 1.2em;
79
+ color: #555;
80
+ margin-bottom: 10px;
81
+ }
82
+
83
+ .upload-hint {
84
+ font-size: 0.9em;
85
+ color: #888;
86
+ }
87
+
88
+ #file-input {
89
+ display: none;
90
+ }
91
+
92
+ .btn {
93
+ background: linear-gradient(45deg, #667eea, #764ba2);
94
+ color: white;
95
+ border: none;
96
+ padding: 12px 30px;
97
+ border-radius: 25px;
98
+ font-size: 1em;
99
+ font-weight: 600;
100
+ cursor: pointer;
101
+ transition: all 0.3s ease;
102
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
103
+ margin: 10px;
104
+ }
105
+
106
+ .btn:hover {
107
+ transform: translateY(-2px);
108
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
109
+ }
110
+
111
+ .btn:active {
112
+ transform: translateY(0);
113
+ }
114
+
115
+ .btn-clear {
116
+ background: linear-gradient(45deg, #ff6b6b, #ee5a24);
117
+ box-shadow: 0 4px 15px rgba(255, 107, 107, 0.3);
118
+ }
119
+
120
+ .btn-clear:hover {
121
+ box-shadow: 0 6px 20px rgba(255, 107, 107, 0.4);
122
+ }
123
+
124
+ .loading {
125
+ display: none;
126
+ text-align: center;
127
+ padding: 20px;
128
+ }
129
+
130
+ .spinner {
131
+ border: 4px solid #f3f3f3;
132
+ border-top: 4px solid #667eea;
133
+ border-radius: 50%;
134
+ width: 40px;
135
+ height: 40px;
136
+ animation: spin 1s linear infinite;
137
+ margin: 0 auto 20px;
138
+ }
139
+
140
+ @keyframes spin {
141
+ 0% {
142
+ transform: rotate(0deg);
143
+ }
144
+
145
+ 100% {
146
+ transform: rotate(360deg);
147
+ }
148
+ }
149
+
150
+ .results {
151
+ margin-top: 40px;
152
+ }
153
+
154
+ .results h2 {
155
+ font-size: 1.8em;
156
+ margin-bottom: 20px;
157
+ color: #333;
158
+ }
159
+
160
+ .panels-grid {
161
+ display: grid;
162
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
163
+ gap: 20px;
164
+ margin-top: 20px;
165
+ }
166
+
167
+ .panel-card {
168
+ background: white;
169
+ border-radius: 15px;
170
+ padding: 20px;
171
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
172
+ transition: transform 0.3s ease;
173
+ overflow: hidden;
174
+ }
175
+
176
+ .panel-card:hover {
177
+ transform: translateY(-5px);
178
+ }
179
+
180
+ .panel-card img {
181
+ width: 100%;
182
+ height: 200px;
183
+ object-fit: cover;
184
+ border-radius: 10px;
185
+ margin-bottom: 15px;
186
+ }
187
+
188
+ .panel-title {
189
+ font-weight: 600;
190
+ color: #333;
191
+ margin-bottom: 10px;
192
+ }
193
+
194
+ .panel-download {
195
+ background: linear-gradient(45deg, #667eea, #764ba2);
196
+ color: white;
197
+ text-decoration: none;
198
+ padding: 8px 20px;
199
+ border-radius: 20px;
200
+ font-size: 0.9em;
201
+ font-weight: 600;
202
+ transition: all 0.3s ease;
203
+ display: inline-block;
204
+ }
205
+
206
+ .panel-download:hover {
207
+ transform: translateY(-2px);
208
+ box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
209
+ }
210
+
211
+ .message {
212
+ padding: 15px;
213
+ border-radius: 10px;
214
+ margin: 20px 0;
215
+ font-weight: 500;
216
+ }
217
+
218
+ .message.success {
219
+ background: linear-gradient(135deg, rgba(76, 175, 80, 0.1) 0%, rgba(139, 195, 74, 0.1) 100%);
220
+ color: #2e7d32;
221
+ border: 1px solid rgba(76, 175, 80, 0.3);
222
+ }
223
+
224
+ .message.error {
225
+ background: linear-gradient(135deg, rgba(244, 67, 54, 0.1) 0%, rgba(255, 87, 34, 0.1) 100%);
226
+ color: #c62828;
227
+ border: 1px solid rgba(244, 67, 54, 0.3);
228
+ }
229
+
230
+ .controls {
231
+ text-align: center;
232
+ margin-bottom: 30px;
233
+ }
234
+
235
+ .upload-preview {
236
+ margin-top: 20px;
237
+ text-align: center;
238
+ }
239
+
240
+ .upload-preview img {
241
+ max-width: 300px;
242
+ border-radius: 10px;
243
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
244
+ cursor: pointer;
245
+ transition: transform 0.3s ease;
246
+ }
247
+
248
+ .upload-preview img:hover {
249
+ transform: scale(1.05);
250
+ }
251
+
252
+ /* Modal */
253
+ .modal {
254
+ display: none;
255
+ position: fixed;
256
+ z-index: 9999;
257
+ left: 0;
258
+ top: 0;
259
+ width: 100%;
260
+ height: 100%;
261
+ overflow: auto;
262
+ background-color: rgba(0, 0, 0, 0.8);
263
+ }
264
+
265
+ .modal-content {
266
+ display: flex;
267
+ justify-content: center;
268
+ align-items: center;
269
+ margin: 0;
270
+ width: 100%;
271
+ height: 100%;
272
+ max-width: none;
273
+ max-height: none;
274
+ border-radius: 0;
275
+ }
276
+
277
+ .modal-content img {
278
+ max-width: 90%;
279
+ max-height: 90%;
280
+ width: auto;
281
+ height: auto;
282
+ object-fit: contain;
283
+ border-radius: 10px;
284
+ }
285
+
286
+ .close-modal {
287
+ position: absolute;
288
+ top: 20px;
289
+ right: 40px;
290
+ font-size: 40px;
291
+ font-weight: bold;
292
+ color: white;
293
+ cursor: pointer;
294
+ }
295
+
296
+ .upload-area img {
297
+ max-width: 100%;
298
+ max-height: 250px;
299
+ border-radius: 10px;
300
+ display: block;
301
+ margin: 0 auto;
302
+ cursor: pointer;
303
+ transition: transform 0.3s ease;
304
+ }
305
+
306
+ .upload-area img:hover {
307
+ transform: scale(1.05);
308
+ }
309
+
310
+ @media (max-width: 768px) {
311
+ .container {
312
+ padding: 20px;
313
+ }
314
+
315
+ h1 {
316
+ font-size: 2em;
317
+ }
318
+
319
+ .upload-area {
320
+ padding: 40px 20px;
321
+ }
322
+
323
+ .panels-grid {
324
+ grid-template-columns: 1fr;
325
+ }
326
+ }
327
+ </style>
328
+ </head>
329
+
330
+ <body>
331
+ <div class="container">
332
+ <h1>Comic Panel Extractor</h1>
333
+
334
+ <div class="upload-section">
335
+ <div class="upload-area" id="upload-area">
336
+ <div class="upload-content" id="upload-content">
337
+ <div class="upload-icon">📚</div>
338
+ <div class="upload-text">Click or drag your comic page here</div>
339
+ <div class="upload-hint">Supports JPG, PNG, and other image formats</div>
340
+ </div>
341
+ <input type="file" id="file-input" accept="image/*">
342
+ </div>
343
+ </div>
344
+
345
+ <div class="controls">
346
+ <button class="btn" onclick="document.getElementById('file-input').click()">
347
+ Choose File
348
+ </button>
349
+ <button class="btn btn-clear" onclick="clearPanels()">
350
+ Clear All Panels
351
+ </button>
352
+ </div>
353
+
354
+ <div class="loading" id="loading">
355
+ <div class="spinner"></div>
356
+ <p>Extracting comic panels...</p>
357
+ </div>
358
+
359
+ <div id="message-container"></div>
360
+
361
+ <div class="results" id="results" style="display: none;">
362
+ <h2>Extracted Panels</h2>
363
+ <div class="panels-grid" id="panels-grid"></div>
364
+ </div>
365
+ </div>
366
+
367
+ <!-- Image Modal -->
368
+ <div id="image-modal" class="modal" onclick="closeModal()">
369
+ <span class="close-modal" onclick="closeModal()">&times;</span>
370
+ <div class="modal-content" id="modal-content">
371
+ <img id="modal-image" src="" alt="Preview">
372
+ </div>
373
+ </div>
374
+
375
+ <script>
376
+ const uploadArea = document.getElementById('upload-area');
377
+ const fileInput = document.getElementById('file-input');
378
+ const uploadContent = document.getElementById('upload-content');
379
+ let uploadedImageUrl = null;
380
+ const loading = document.getElementById('loading');
381
+ const results = document.getElementById('results');
382
+ const panelsGrid = document.getElementById('panels-grid');
383
+ const messageContainer = document.getElementById('message-container');
384
+ const uploadPreview = document.getElementById('upload-preview');
385
+ const modal = document.getElementById('image-modal');
386
+ const modalImage = document.getElementById('modal-image');
387
+
388
+ function generateUniqueId(file, length = 12) {
389
+ ext = "." + file.name.split(".")[file.name.split(".").length - 1]
390
+ const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
391
+ return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('') + ext;
392
+ }
393
+
394
+ function showMessage(message, type = 'success') {
395
+ messageContainer.innerHTML = `<div class="message ${type}">${message}</div>`;
396
+ setTimeout(() => {
397
+ messageContainer.innerHTML = '';
398
+ }, 5000);
399
+ }
400
+
401
+ function handleFile(file) {
402
+ const reader = new FileReader();
403
+ reader.onload = (e) => {
404
+ uploadedImageUrl = e.target.result;
405
+ const previewImage = `
406
+ <img src="${e.target.result}" alt="Uploaded Image" id="uploaded-preview">
407
+ `;
408
+ uploadContent.innerHTML = previewImage;
409
+ };
410
+ reader.readAsDataURL(file);
411
+
412
+ const newFileName = generateUniqueId(file);
413
+ const renamedFile = new File([file], newFileName, { type: file.type });
414
+
415
+ const formData = new FormData();
416
+ formData.append('file', renamedFile);
417
+
418
+ loading.style.display = 'block';
419
+ results.style.display = 'none';
420
+
421
+ fetch('/convert', {
422
+ method: 'POST',
423
+ body: formData
424
+ })
425
+ .then(response => response.json())
426
+ .then(data => {
427
+ loading.style.display = 'none';
428
+
429
+ if (data.success) {
430
+ showMessage(data.message, 'success');
431
+ displayPanels(data.panels);
432
+ } else {
433
+ showMessage(data.message || 'An error occurred', 'error');
434
+ }
435
+ })
436
+ .catch(error => {
437
+ loading.style.display = 'none';
438
+ showMessage('Error uploading file: ' + error.message, 'error');
439
+ });
440
+ }
441
+
442
+ // Single click handler for upload area
443
+ uploadArea.addEventListener('click', (e) => {
444
+ // If clicking on preview image, open modal
445
+ if (e.target.id === 'uploaded-preview') {
446
+ openModal(uploadedImageUrl);
447
+ e.stopPropagation();
448
+ return;
449
+ }
450
+
451
+ // Only open file picker if no image is uploaded yet
452
+ if (!uploadedImageUrl) {
453
+ fileInput.click();
454
+ }
455
+ });
456
+
457
+ // Drag & drop support
458
+ uploadArea.addEventListener('dragover', (e) => {
459
+ e.preventDefault();
460
+ uploadArea.classList.add('dragover');
461
+ });
462
+
463
+ uploadArea.addEventListener('dragleave', () => {
464
+ uploadArea.classList.remove('dragover');
465
+ });
466
+
467
+ uploadArea.addEventListener('drop', (e) => {
468
+ e.preventDefault();
469
+ uploadArea.classList.remove('dragover');
470
+ const files = e.dataTransfer.files;
471
+ if (files.length > 0) {
472
+ handleFile(files[0]);
473
+ }
474
+ });
475
+
476
+ fileInput.addEventListener('change', (e) => {
477
+ if (e.target.files.length > 0) {
478
+ handleFile(e.target.files[0]);
479
+ }
480
+ });
481
+
482
+ function displayPanels(panels) {
483
+ panelsGrid.innerHTML = '';
484
+
485
+ if (panels.length === 0) {
486
+ results.style.display = 'none';
487
+ showMessage('No panels were detected in the image.', 'error');
488
+ return;
489
+ }
490
+
491
+ panels.forEach((panel, index) => {
492
+ const panelCard = document.createElement('div');
493
+ panelCard.className = 'panel-card';
494
+ panelCard.innerHTML = `
495
+ <img src="${panel}" alt="Panel ${index + 1}" onclick="openModal('${panel}')">
496
+ <div class="panel-title">Panel ${index + 1}</div>
497
+ <a href="${panel}" download="${panel}" class="panel-download">
498
+ Download
499
+ </a>
500
+ `;
501
+ panelsGrid.appendChild(panelCard);
502
+ });
503
+
504
+ results.style.display = 'block';
505
+ }
506
+
507
+ function clearPanels() {
508
+ fetch('/clear', {
509
+ method: 'DELETE'
510
+ })
511
+ .then(response => response.json())
512
+ .then(data => {
513
+ if (data.success) {
514
+ showMessage(data.message, 'success');
515
+ results.style.display = 'none';
516
+ panelsGrid.innerHTML = '';
517
+ uploadPreview.innerHTML = '';
518
+ } else {
519
+ showMessage(data.message || 'Error clearing panels', 'error');
520
+ }
521
+ })
522
+ .catch(error => {
523
+ showMessage('Error clearing panels: ' + error.message, 'error');
524
+ });
525
+ }
526
+
527
+ function openModal(src) {
528
+ modal.style.display = 'block';
529
+ modalImage.src = src;
530
+ }
531
+
532
+ function closeModal() {
533
+ modal.style.display = 'none';
534
+ modalImage.src = '';
535
+ }
536
+ </script>
537
+ </body>
538
+
539
+ </html>
comic_panel_extractor/text_detector.py CHANGED
@@ -5,7 +5,7 @@ from typing import List, Optional
5
  from dataclasses import dataclass
6
  import numpy as np
7
 
8
- from .config import Config
9
 
10
  @dataclass
11
  class TextDetection:
@@ -104,13 +104,14 @@ class TextDetector:
104
 
105
  def detect_and_group_text(self) -> str:
106
  """Main method to detect and group text, saving results to JSON."""
107
- if not os.path.exists(self.config.text_cood_path):
 
108
  detections = self.detect_text()
109
  groups = self.group_text_regions(detections)
110
- self._save_groups_to_json(groups, self.config.text_cood_path)
111
- print(f"Grouped bubbles saved: {self.config.text_cood_path}")
112
 
113
- return self.config.text_cood_path
114
 
115
  def _save_groups_to_json(self, groups: List[TextDetection], output_path: str):
116
  """Save grouped text detections to JSON file."""
 
5
  from dataclasses import dataclass
6
  import numpy as np
7
 
8
+ from .config import Config, get_text_cood_file_path
9
 
10
  @dataclass
11
  class TextDetection:
 
104
 
105
  def detect_and_group_text(self) -> str:
106
  """Main method to detect and group text, saving results to JSON."""
107
+ text_coord_path = get_text_cood_file_path(self.config)
108
+ if not os.path.exists(text_coord_path):
109
  detections = self.detect_text()
110
  groups = self.group_text_regions(detections)
111
+ self._save_groups_to_json(groups, text_coord_path)
112
+ print(f"Grouped bubbles saved: {text_coord_path}")
113
 
114
+ return text_coord_path
115
 
116
  def _save_groups_to_json(self, groups: List[TextDetection], output_path: str):
117
  """Save grouped text detections to JSON file."""
requirements.txt CHANGED
@@ -1,4 +1,8 @@
1
  moviepy==1.0.3
2
  numpy
3
  opencv-python
4
- easyocr
 
 
 
 
 
1
  moviepy==1.0.3
2
  numpy
3
  opencv-python
4
+ easyocr
5
+ fastapi
6
+ uvicorn
7
+ python-multipart
8
+ jinja2
setup.py CHANGED
@@ -32,6 +32,7 @@ setup(
32
  entry_points={
33
  "console_scripts": [
34
  "comic-panel-extractor=comic_panel_extractor.cli:main",
 
35
  ],
36
  },
37
  include_package_data=True,
 
32
  entry_points={
33
  "console_scripts": [
34
  "comic-panel-extractor=comic_panel_extractor.cli:main",
35
+ "serve-comic-panel-extractor=comic_panel_extractor.server:main",
36
  ],
37
  },
38
  include_package_data=True,