tester343 commited on
Commit
68aca8d
·
verified ·
1 Parent(s): 53c6075

Update app_enhanced.py

Browse files
Files changed (1) hide show
  1. app_enhanced.py +62 -33
app_enhanced.py CHANGED
@@ -91,7 +91,6 @@ INDEX_HTML = '''
91
  .file-label:hover { background: #34495e; }
92
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
93
  .submit-btn:hover { background: #d35400; }
94
-
95
  .restore-btn { margin-top: 15px; background: #27ae60; color: white; padding: 10px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; display: none; }
96
 
97
  .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
@@ -102,11 +101,11 @@ INDEX_HTML = '''
102
  .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
103
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
104
  .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
105
- .panel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s; }
106
  .panel img.pannable { cursor: grab; }
107
  .panel img.panning { cursor: grabbing; }
108
 
109
- /* --- SPEECH BUBBLE (EXACT SHARK FIN CSS) --- */
110
  .speech-bubble {
111
  position: absolute; display: flex; justify-content: center; align-items: center;
112
  width: 150px; height: 80px; min-width: 60px; min-height: 40px; box-sizing: border-box;
@@ -117,24 +116,65 @@ INDEX_HTML = '''
117
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
118
  .speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.9); text-align:center; padding:8px; z-index:100; resize:none; }
119
 
 
120
  .speech-bubble.speech {
121
- --b: 3em; --h: 1.8em; --t: 0.6; --p: var(--tail-pos, 50%); --r: 1.2em;
 
 
 
 
122
  --c: var(--bubble-fill-color, #4ECDC4);
123
- background: var(--c); color: var(--bubble-text-color, #fff); padding: 1em; position: absolute;
 
 
 
 
 
 
124
  border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r);
125
  }
 
 
 
 
 
 
126
  .speech-bubble.speech:before {
127
  content: ""; position: absolute; width: var(--b); height: var(--h);
 
128
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
129
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
130
  }
131
- .speech-bubble.speech.tail-bottom:before { top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); }
132
- .speech-bubble.speech.tail-top { border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r); }
133
- .speech-bubble.speech.tail-top:before { bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: scaleY(-1); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
135
- .speech-bubble.speech.tail-left:before { right: 99%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(90deg); transform-origin: top right; }
 
 
 
 
 
136
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
137
- .speech-bubble.speech.tail-right:before { left: 99%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b))); transform: rotate(-90deg); transform-origin: top left; }
 
 
 
138
 
139
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
140
  .speech-bubble.thought::after { display:none; }
@@ -165,7 +205,6 @@ INDEX_HTML = '''
165
  <span id="fn">No file selected</span>
166
  <button class="submit-btn" onclick="upload()">Generate Comic</button>
167
  <button id="restore-btn" class="restore-btn" onclick="restoreSession()">📂 Restore Unsaved Session</button>
168
-
169
  <div class="loading-view" id="loading-view" style="display:none;">
170
  <div class="loader"></div>
171
  <p id="status-text">Starting...</p>
@@ -214,10 +253,11 @@ INDEX_HTML = '''
214
  </div>
215
  <input type="text" id="timestamp-input" placeholder="mm:ss">
216
  <button onclick="gotoTimestamp()">Go to Time</button>
 
217
  <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
218
  </div>
219
  <hr>
220
- <button onclick="saveLocal()">💾 Force Save Draft</button>
221
  <button onclick="exportComic()" style="background:#ccffcc; font-weight:bold;">📥 Download PNG</button>
222
  <button onclick="location.reload()" style="color:red; margin-top:10px;">↺ Start Over</button>
223
  </div>
@@ -245,13 +285,10 @@ INDEX_HTML = '''
245
  if(!savedData) return alert("No saved session found.");
246
  try {
247
  const state = JSON.parse(savedData);
248
- // Restore SID from state if needed, or keep current
249
  renderFromState(state);
250
  document.getElementById('upload-container').style.display = 'none';
251
  document.getElementById('editor-container').style.display = 'block';
252
- } catch(e) {
253
- alert("Failed to restore: " + e);
254
- }
255
  }
256
 
257
  function renderFromState(pagesData) {
@@ -269,9 +306,7 @@ INDEX_HTML = '''
269
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
270
 
271
  const img = document.createElement('img');
272
- // Ensure image path includes SID if it's relative, or use full saved src
273
  img.src = pan.src;
274
- // Restore pan/zoom
275
  img.dataset.zoom = pan.zoom || 100;
276
  img.dataset.translateX = pan.tx || 0;
277
  img.dataset.translateY = pan.ty || 0;
@@ -279,10 +314,8 @@ INDEX_HTML = '''
279
  img.onmousedown = startPan;
280
  pDiv.appendChild(img);
281
 
282
- // Restore bubbles
283
  pan.bubbles.forEach(bData => {
284
  const b = createBubble(bData.text, bData.left, bData.top, bData.type, bData.tailPos, bData.width, bData.height, bData.colors);
285
- // Re-apply rotation classes
286
  if(bData.classes) b.className = bData.classes;
287
  pDiv.appendChild(b);
288
  });
@@ -326,7 +359,6 @@ INDEX_HTML = '''
326
  pages.push({ panels: panels });
327
  });
328
  localStorage.setItem('comic_autosave', JSON.stringify(pages));
329
- console.log("Autosaved.");
330
  }
331
 
332
  // --- UPLOAD ---
@@ -368,7 +400,7 @@ INDEX_HTML = '''
368
  }))
369
  }));
370
  renderFromState(cleanData);
371
- saveLocal(); // Initial save
372
  });
373
  }
374
 
@@ -380,22 +412,18 @@ INDEX_HTML = '''
380
  if(w) b.style.width = w; if(h) b.style.height = h;
381
  b.dataset.type = type;
382
  b.style.setProperty('--tail-pos', tailPos || '50%');
383
-
384
  if(colors) {
385
  if(colors.text) b.style.setProperty('--bubble-text-color', colors.text);
386
  if(colors.fill) b.style.setProperty('--bubble-fill-color', colors.fill);
387
  }
388
-
389
  b.innerHTML = `<span class="bubble-text">${text}</span><div class="resize-handle se"></div>`;
390
 
391
- // Handlers
392
  b.onmousedown = (e) => {
393
  if(e.target.classList.contains('resize-handle')) return;
394
  e.stopPropagation(); selectedBubble = b;
395
  document.querySelectorAll('.speech-bubble').forEach(el=>el.classList.remove('selected'));
396
  b.classList.add('selected');
397
  selectBubbleUI(b);
398
-
399
  const ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
400
  document.onmousemove = (ev) => {
401
  b.style.left = (ev.clientX-ox)+'px'; b.style.top = (ev.clientY-oy)+'px';
@@ -421,7 +449,6 @@ INDEX_HTML = '''
421
  b.appendChild(txt); span.style.display='none'; txt.focus();
422
  txt.onblur = () => { span.innerText = txt.value; txt.remove(); span.style.display='block'; saveLocal(); };
423
  };
424
-
425
  return b;
426
  }
427
 
@@ -430,6 +457,7 @@ INDEX_HTML = '''
430
  el.classList.add('selected');
431
  selectedPanel = el;
432
  selectBubbleUI(null);
 
433
  }
434
 
435
  function selectBubbleUI(el) {
@@ -507,13 +535,13 @@ INDEX_HTML = '''
507
  img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
508
  img.classList.toggle('pannable', z>1);
509
  }
510
- function resetPanelTransform() {
511
  if(!selectedPanel) return;
512
  const img = selectedPanel.querySelector('img');
513
- img.dataset.zoom=100; img.dataset.translateX=0; img.dataset.translateY=0;
514
- document.getElementById('zoom-slider').value = 100;
515
  updateImageTransform(img);
516
- saveLocal();
517
  }
518
 
519
  // API
@@ -563,7 +591,7 @@ INDEX_HTML = '''
563
  }
564
  }
565
 
566
- // Color Listeners Logic
567
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
568
  if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveLocal(); }
569
  });
@@ -630,7 +658,8 @@ class EnhancedComicGenerator:
630
  frame_files = []
631
  bubbles = []
632
 
633
- for i, sub in enumerate(subs[:12]):
 
634
  mid = (sub.start.total_seconds() + sub.end.total_seconds())/2
635
  cap.set(cv2.CAP_PROP_POS_MSEC, mid*1000)
636
  ret, frame = cap.read()
 
91
  .file-label:hover { background: #34495e; }
92
  .submit-btn { width: 100%; padding: 15px; background: #e67e22; color: white; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: 0.2s; }
93
  .submit-btn:hover { background: #d35400; }
 
94
  .restore-btn { margin-top: 15px; background: #27ae60; color: white; padding: 10px; width: 100%; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; display: none; }
95
 
96
  .loader { width: 120px; height: 20px; background: radial-gradient(circle 10px, #e67e22 100%, transparent 0); background-size: 20px 20px; animation: ball 1s infinite linear; margin: 20px auto; }
 
101
  .comic-grid { display: grid; grid-template-columns: 285px 285px; grid-template-rows: 185px 185px; gap: 10px; width: 100%; height: 100%; padding: 10px; box-sizing: border-box; }
102
  .panel { position: relative; overflow: hidden; border: 2px solid #000; background: #eee; cursor: pointer; }
103
  .panel.selected { outline: 3px solid #2196F3; outline-offset: -3px; }
104
+ .panel img { width: 100%; height: 100%; object-fit: cover; transition: transform 0.1s ease-out; transform-origin: center center; }
105
  .panel img.pannable { cursor: grab; }
106
  .panel img.panning { cursor: grabbing; }
107
 
108
+ /* --- SPEECH BUBBLE (SHARK FIN) --- */
109
  .speech-bubble {
110
  position: absolute; display: flex; justify-content: center; align-items: center;
111
  width: 150px; height: 80px; min-width: 60px; min-height: 40px; box-sizing: border-box;
 
116
  .speech-bubble.selected { outline: 2px dashed #4CAF50; }
117
  .speech-bubble textarea { position: absolute; top:0; left:0; width:100%; height:100%; box-sizing:border-box; border:1px solid #4CAF50; background:rgba(255,255,255,0.9); text-align:center; padding:8px; z-index:100; resize:none; }
118
 
119
+ /* The specific CSS request from user */
120
  .speech-bubble.speech {
121
+ --b: 3em; /* tail base */
122
+ --h: 1.8em; /* tail height */
123
+ --t: 0.6; /* thickness */
124
+ --p: var(--tail-pos, 50%); /* slider pos */
125
+ --r: 1.2em; /* radius */
126
  --c: var(--bubble-fill-color, #4ECDC4);
127
+
128
+ background: var(--c);
129
+ color: var(--bubble-text-color, #fff);
130
+ padding: 1em;
131
+ position: absolute;
132
+
133
+ /* Exact border radius formula provided */
134
  border-radius: var(--r) var(--r) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) / var(--r);
135
  }
136
+
137
+ /*
138
+ TAIL IMPLEMENTATION:
139
+ Using 'radial-gradient' background instead of mask.
140
+ This visually creates the EXACT concave shape requested but is 100% export-safe.
141
+ */
142
  .speech-bubble.speech:before {
143
  content: ""; position: absolute; width: var(--b); height: var(--h);
144
+ /* Gradient mimics the mask curve */
145
  background: radial-gradient(100% 100% at 100% 0, transparent calc(var(--t) * 100% - 1px), var(--c) calc(var(--t) * 100%));
146
  border-bottom-left-radius: 100%; pointer-events: none; z-index: 1;
147
  }
148
+
149
+ /* ROTATION LOGIC (Adapting the shape for 4 sides) */
150
+
151
+ /* Bottom (Default) */
152
+ .speech-bubble.speech.tail-bottom:before {
153
+ top: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
154
+ }
155
+
156
+ /* Top */
157
+ .speech-bubble.speech.tail-top {
158
+ border-radius: min(var(--r), calc(var(--p) - (1 - var(--t)) * var(--b) / 2)) min(var(--r), calc(100% - var(--p) - (1 - var(--t)) * var(--b) / 2)) var(--r) var(--r) / var(--r);
159
+ }
160
+ .speech-bubble.speech.tail-top:before {
161
+ bottom: 99%; left: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
162
+ transform: scaleY(-1);
163
+ }
164
+
165
+ /* Left */
166
  .speech-bubble.speech.tail-left { border-radius: var(--r); }
167
+ .speech-bubble.speech.tail-left:before {
168
+ right: 99%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
169
+ transform: rotate(90deg); transform-origin: top right;
170
+ }
171
+
172
+ /* Right */
173
  .speech-bubble.speech.tail-right { border-radius: var(--r); }
174
+ .speech-bubble.speech.tail-right:before {
175
+ left: 99%; top: clamp(0%, calc(var(--p) - (1 - var(--t)) * var(--b) / 2), calc(100% - (1 - var(--t)) * var(--b)));
176
+ transform: rotate(-90deg); transform-origin: top left;
177
+ }
178
 
179
  .speech-bubble.thought { background: white; border: 2px dashed #555; color: #333; border-radius: 50%; }
180
  .speech-bubble.thought::after { display:none; }
 
205
  <span id="fn">No file selected</span>
206
  <button class="submit-btn" onclick="upload()">Generate Comic</button>
207
  <button id="restore-btn" class="restore-btn" onclick="restoreSession()">📂 Restore Unsaved Session</button>
 
208
  <div class="loading-view" id="loading-view" style="display:none;">
209
  <div class="loader"></div>
210
  <p id="status-text">Starting...</p>
 
253
  </div>
254
  <input type="text" id="timestamp-input" placeholder="mm:ss">
255
  <button onclick="gotoTimestamp()">Go to Time</button>
256
+ <label>Zoom:</label>
257
  <input type="range" id="zoom-slider" min="100" max="300" value="100" step="5" disabled oninput="handleZoom(this)">
258
  </div>
259
  <hr>
260
+ <button onclick="saveLocal()">💾 Force Save</button>
261
  <button onclick="exportComic()" style="background:#ccffcc; font-weight:bold;">📥 Download PNG</button>
262
  <button onclick="location.reload()" style="color:red; margin-top:10px;">↺ Start Over</button>
263
  </div>
 
285
  if(!savedData) return alert("No saved session found.");
286
  try {
287
  const state = JSON.parse(savedData);
 
288
  renderFromState(state);
289
  document.getElementById('upload-container').style.display = 'none';
290
  document.getElementById('editor-container').style.display = 'block';
291
+ } catch(e) { alert("Failed to restore"); }
 
 
292
  }
293
 
294
  function renderFromState(pagesData) {
 
306
  pDiv.onclick = (e) => { e.stopPropagation(); selectPanel(pDiv); };
307
 
308
  const img = document.createElement('img');
 
309
  img.src = pan.src;
 
310
  img.dataset.zoom = pan.zoom || 100;
311
  img.dataset.translateX = pan.tx || 0;
312
  img.dataset.translateY = pan.ty || 0;
 
314
  img.onmousedown = startPan;
315
  pDiv.appendChild(img);
316
 
 
317
  pan.bubbles.forEach(bData => {
318
  const b = createBubble(bData.text, bData.left, bData.top, bData.type, bData.tailPos, bData.width, bData.height, bData.colors);
 
319
  if(bData.classes) b.className = bData.classes;
320
  pDiv.appendChild(b);
321
  });
 
359
  pages.push({ panels: panels });
360
  });
361
  localStorage.setItem('comic_autosave', JSON.stringify(pages));
 
362
  }
363
 
364
  // --- UPLOAD ---
 
400
  }))
401
  }));
402
  renderFromState(cleanData);
403
+ saveLocal();
404
  });
405
  }
406
 
 
412
  if(w) b.style.width = w; if(h) b.style.height = h;
413
  b.dataset.type = type;
414
  b.style.setProperty('--tail-pos', tailPos || '50%');
 
415
  if(colors) {
416
  if(colors.text) b.style.setProperty('--bubble-text-color', colors.text);
417
  if(colors.fill) b.style.setProperty('--bubble-fill-color', colors.fill);
418
  }
 
419
  b.innerHTML = `<span class="bubble-text">${text}</span><div class="resize-handle se"></div>`;
420
 
 
421
  b.onmousedown = (e) => {
422
  if(e.target.classList.contains('resize-handle')) return;
423
  e.stopPropagation(); selectedBubble = b;
424
  document.querySelectorAll('.speech-bubble').forEach(el=>el.classList.remove('selected'));
425
  b.classList.add('selected');
426
  selectBubbleUI(b);
 
427
  const ox = e.clientX - b.offsetLeft, oy = e.clientY - b.offsetTop;
428
  document.onmousemove = (ev) => {
429
  b.style.left = (ev.clientX-ox)+'px'; b.style.top = (ev.clientY-oy)+'px';
 
449
  b.appendChild(txt); span.style.display='none'; txt.focus();
450
  txt.onblur = () => { span.innerText = txt.value; txt.remove(); span.style.display='block'; saveLocal(); };
451
  };
 
452
  return b;
453
  }
454
 
 
457
  el.classList.add('selected');
458
  selectedPanel = el;
459
  selectBubbleUI(null);
460
+ resetPanelTransform(true);
461
  }
462
 
463
  function selectBubbleUI(el) {
 
535
  img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${z})`;
536
  img.classList.toggle('pannable', z>1);
537
  }
538
+ function resetPanelTransform(loadOnly=false) {
539
  if(!selectedPanel) return;
540
  const img = selectedPanel.querySelector('img');
541
+ if(!loadOnly) { img.dataset.zoom=100; img.dataset.translateX=0; img.dataset.translateY=0; }
542
+ document.getElementById('zoom-slider').value = img.dataset.zoom || 100;
543
  updateImageTransform(img);
544
+ if(!loadOnly) saveLocal();
545
  }
546
 
547
  // API
 
591
  }
592
  }
593
 
594
+ // Color Listeners
595
  document.getElementById('bubble-text-color').addEventListener('input', (e) => {
596
  if(selectedBubble) { selectedBubble.style.setProperty('--bubble-text-color', e.target.value); saveLocal(); }
597
  });
 
658
  frame_files = []
659
  bubbles = []
660
 
661
+ # Generate UP TO 48 PANELS (12 PAGES)
662
+ for i, sub in enumerate(subs[:48]):
663
  mid = (sub.start.total_seconds() + sub.end.total_seconds())/2
664
  cap.set(cv2.CAP_PROP_POS_MSEC, mid*1000)
665
  ret, frame = cap.read()