Update app_enhanced.py
Browse files- app_enhanced.py +207 -26
app_enhanced.py
CHANGED
|
@@ -474,7 +474,13 @@ class EnhancedComicGenerator:
|
|
| 474 |
def _save_results(self, pages):
|
| 475 |
try:
|
| 476 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 477 |
-
pages_data = [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 478 |
with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
|
| 479 |
json.dump(pages_data, f, indent=2)
|
| 480 |
self._copy_template_files()
|
|
@@ -664,9 +670,38 @@ class EnhancedComicGenerator:
|
|
| 664 |
return bubbleDiv;
|
| 665 |
}
|
| 666 |
|
| 667 |
-
function applyBubbleType(bubble, type) {
|
| 668 |
-
|
| 669 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 670 |
|
| 671 |
function selectPanel(panel) {
|
| 672 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
@@ -735,17 +770,172 @@ class EnhancedComicGenerator:
|
|
| 735 |
}
|
| 736 |
}
|
| 737 |
|
| 738 |
-
async function exportPagesToPNG() {
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 749 |
</script>
|
| 750 |
</body>
|
| 751 |
</html>'''
|
|
@@ -785,17 +975,8 @@ def status():
|
|
| 785 |
|
| 786 |
@app.route('/handle_link', methods=['POST'])
|
| 787 |
def handle_link():
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
if not link: return "❌ No link provided"
|
| 791 |
-
import yt_dlp
|
| 792 |
-
ydl_opts = {'outtmpl': comic_generator.video_path, 'format': 'best[height<=720]', 'overwrites': True}
|
| 793 |
-
with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([link])
|
| 794 |
-
threading.Thread(target=comic_generator.generate_comic).start()
|
| 795 |
-
return "Generation started. Please poll /status for updates."
|
| 796 |
-
except Exception as e:
|
| 797 |
-
traceback.print_exc()
|
| 798 |
-
return f"❌ An unexpected error occurred: {str(e)}"
|
| 799 |
|
| 800 |
@app.route('/replace_panel', methods=['POST'])
|
| 801 |
def replace_panel():
|
|
|
|
| 474 |
def _save_results(self, pages):
|
| 475 |
try:
|
| 476 |
os.makedirs(self.output_dir, exist_ok=True)
|
| 477 |
+
pages_data = []
|
| 478 |
+
for page in pages:
|
| 479 |
+
page_dict = {
|
| 480 |
+
'panels': [p.__dict__ for p in page.panels],
|
| 481 |
+
'bubbles': [b.__dict__ for b in page.bubbles]
|
| 482 |
+
}
|
| 483 |
+
pages_data.append(page_dict)
|
| 484 |
with open(os.path.join(self.output_dir, 'pages.json'), 'w', encoding='utf-8') as f:
|
| 485 |
json.dump(pages_data, f, indent=2)
|
| 486 |
self._copy_template_files()
|
|
|
|
| 670 |
return bubbleDiv;
|
| 671 |
}
|
| 672 |
|
| 673 |
+
function applyBubbleType(bubble, type) {
|
| 674 |
+
bubble.querySelectorAll('.thought-dot').forEach(el => el.remove());
|
| 675 |
+
let classesToKeep = 'speech-bubble';
|
| 676 |
+
if (bubble.classList.contains('selected')) classesToKeep += ' selected';
|
| 677 |
+
if (bubble.classList.contains('flipped')) classesToKeep += ' flipped';
|
| 678 |
+
if (bubble.classList.contains('flipped-vertical')) classesToKeep += ' flipped-vertical';
|
| 679 |
+
bubble.className = classesToKeep;
|
| 680 |
+
bubble.classList.add(type);
|
| 681 |
+
bubble.dataset.type = type;
|
| 682 |
+
if (type === 'thought') {
|
| 683 |
+
for (let i = 1; i <= 2; i++) {
|
| 684 |
+
const dot = document.createElement('div');
|
| 685 |
+
dot.className = `thought-dot thought-dot-${i}`;
|
| 686 |
+
bubble.appendChild(dot);
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
function changeBubbleType(type) {
|
| 692 |
+
if (!currentlySelectedBubble) return;
|
| 693 |
+
applyBubbleType(currentlySelectedBubble, type);
|
| 694 |
+
}
|
| 695 |
+
|
| 696 |
+
function rotateBubbleTail() {
|
| 697 |
+
if (!currentlySelectedBubble) { alert("Please select a bubble first."); return; }
|
| 698 |
+
const isFlippedH = currentlySelectedBubble.classList.contains('flipped');
|
| 699 |
+
const isFlippedV = currentlySelectedBubble.classList.contains('flipped-vertical');
|
| 700 |
+
if (!isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped'); }
|
| 701 |
+
else if (isFlippedH && !isFlippedV) { currentlySelectedBubble.classList.add('flipped-vertical'); }
|
| 702 |
+
else if (isFlippedH && isFlippedV) { currentlySelectedBubble.classList.remove('flipped'); }
|
| 703 |
+
else { currentlySelectedBubble.classList.remove('flipped-vertical'); }
|
| 704 |
+
}
|
| 705 |
|
| 706 |
function selectPanel(panel) {
|
| 707 |
document.querySelectorAll('.panel.selected').forEach(p => p.classList.remove('selected'));
|
|
|
|
| 770 |
}
|
| 771 |
}
|
| 772 |
|
| 773 |
+
async function exportPagesToPNG() {
|
| 774 |
+
const pages = document.querySelectorAll('.comic-page');
|
| 775 |
+
if (pages.length === 0) return alert("No pages found.");
|
| 776 |
+
alert(`Starting export of ${pages.length} page(s).`);
|
| 777 |
+
for (let i = 0; i < pages.length; i++) {
|
| 778 |
+
try {
|
| 779 |
+
const canvas = await html2canvas(pages[i], { scale: 2 });
|
| 780 |
+
const link = document.createElement('a');
|
| 781 |
+
link.download = `comic-page-${i + 1}.png`;
|
| 782 |
+
link.href = canvas.toDataURL('image/png');
|
| 783 |
+
link.click();
|
| 784 |
+
} catch (err) { alert(`Failed to export page ${i + 1}.`); }
|
| 785 |
+
}
|
| 786 |
+
}
|
| 787 |
+
|
| 788 |
+
function replacePanelImage() {
|
| 789 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 790 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 791 |
+
const uploader = document.getElementById('image-uploader');
|
| 792 |
+
uploader.onchange = (event) => {
|
| 793 |
+
const file = event.target.files[0];
|
| 794 |
+
if (!file) return;
|
| 795 |
+
const formData = new FormData();
|
| 796 |
+
formData.append('image', file);
|
| 797 |
+
img.style.opacity = '0.5';
|
| 798 |
+
fetch('/replace_panel', { method: 'POST', body: formData })
|
| 799 |
+
.then(response => response.json())
|
| 800 |
+
.then(data => {
|
| 801 |
+
if (data.success) {
|
| 802 |
+
img.src = `/frames/final/${data.new_filename}?t=${new Date().getTime()}`;
|
| 803 |
+
} else { alert('Error replacing image: ' + data.error); }
|
| 804 |
+
img.style.opacity = '1';
|
| 805 |
+
})
|
| 806 |
+
.catch(() => {
|
| 807 |
+
alert('An error occurred during the upload.');
|
| 808 |
+
img.style.opacity = '1';
|
| 809 |
+
});
|
| 810 |
+
uploader.value = '';
|
| 811 |
+
};
|
| 812 |
+
uploader.click();
|
| 813 |
+
}
|
| 814 |
+
|
| 815 |
+
function adjustFrame(direction) {
|
| 816 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 817 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 818 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 819 |
+
img.style.opacity = '0.5';
|
| 820 |
+
fetch('/regenerate_frame', {
|
| 821 |
+
method: 'POST',
|
| 822 |
+
headers: { 'Content-Type': 'application/json' },
|
| 823 |
+
body: JSON.stringify({ filename, direction })
|
| 824 |
+
})
|
| 825 |
+
.then(res => res.json())
|
| 826 |
+
.then(data => {
|
| 827 |
+
if (data.success) {
|
| 828 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 829 |
+
} else { alert('Error: ' + data.message); }
|
| 830 |
+
img.style.opacity = '1';
|
| 831 |
+
})
|
| 832 |
+
.catch(() => {
|
| 833 |
+
alert('An error occurred.');
|
| 834 |
+
img.style.opacity = '1';
|
| 835 |
+
});
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
function updateImageTransform(img) {
|
| 839 |
+
const zoom = (img.dataset.zoom || 100) / 100;
|
| 840 |
+
const x = img.dataset.translateX || 0;
|
| 841 |
+
const y = img.dataset.translateY || 0;
|
| 842 |
+
img.style.transform = `translateX(${x}px) translateY(${y}px) scale(${zoom})`;
|
| 843 |
+
img.classList.toggle('pannable', zoom > 1);
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
function handleZoom(event) {
|
| 847 |
+
if (!currentlySelectedPanel) return;
|
| 848 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 849 |
+
img.dataset.zoom = event.target.value;
|
| 850 |
+
updateImageTransform(img);
|
| 851 |
+
}
|
| 852 |
+
|
| 853 |
+
function resetPanelTransform() {
|
| 854 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 855 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 856 |
+
img.dataset.zoom = 100;
|
| 857 |
+
img.dataset.translateX = 0;
|
| 858 |
+
img.dataset.translateY = 0;
|
| 859 |
+
document.getElementById('zoom-slider').value = 100;
|
| 860 |
+
updateImageTransform(img);
|
| 861 |
+
}
|
| 862 |
+
|
| 863 |
+
function startPan(event) {
|
| 864 |
+
if (event.button !== 0) return;
|
| 865 |
+
const img = event.target;
|
| 866 |
+
if (parseFloat(img.dataset.zoom || 100) <= 100) return;
|
| 867 |
+
event.preventDefault();
|
| 868 |
+
isPanning = true;
|
| 869 |
+
img.classList.add('panning');
|
| 870 |
+
panStartX = event.clientX;
|
| 871 |
+
panStartY = event.clientY;
|
| 872 |
+
panStartTranslateX = parseFloat(img.dataset.translateX || 0);
|
| 873 |
+
panStartTranslateY = parseFloat(img.dataset.translateY || 0);
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
function panImage(event) {
|
| 877 |
+
if (!isPanning || !currentlySelectedPanel) return;
|
| 878 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 879 |
+
img.dataset.translateX = panStartTranslateX + (event.clientX - panStartX);
|
| 880 |
+
img.dataset.translateY = panStartTranslateY + (event.clientY - panStartY);
|
| 881 |
+
updateImageTransform(img);
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
function stopPan() {
|
| 885 |
+
if (!isPanning) return;
|
| 886 |
+
isPanning = false;
|
| 887 |
+
currentlySelectedPanel?.querySelector('img')?.classList.remove('panning');
|
| 888 |
+
}
|
| 889 |
+
|
| 890 |
+
function addBubbleToPanel() {
|
| 891 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 892 |
+
const newBubble = createBubbleElement({
|
| 893 |
+
id: `new-bubble-${Date.now()}`,
|
| 894 |
+
text: 'New Text...',
|
| 895 |
+
left: '10%',
|
| 896 |
+
top: '10%'
|
| 897 |
+
});
|
| 898 |
+
currentlySelectedPanel.appendChild(newBubble);
|
| 899 |
+
initializeBubbleEvents(newBubble);
|
| 900 |
+
selectBubble(newBubble);
|
| 901 |
+
editBubbleText(newBubble);
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
function gotoTimestamp() {
|
| 905 |
+
if (!currentlySelectedPanel) { alert("Please select a panel first."); return; }
|
| 906 |
+
const input = document.getElementById('timestamp-input');
|
| 907 |
+
const timeStr = input.value.trim();
|
| 908 |
+
if (!timeStr) return;
|
| 909 |
+
let parsedSeconds = 0;
|
| 910 |
+
if (timeStr.includes(':')) {
|
| 911 |
+
const parts = timeStr.split(':');
|
| 912 |
+
parsedSeconds = parseInt(parts[0], 10) * 60 + parseFloat(parts[1]);
|
| 913 |
+
} else {
|
| 914 |
+
parsedSeconds = parseFloat(timeStr);
|
| 915 |
+
}
|
| 916 |
+
if (isNaN(parsedSeconds)) { alert("Invalid time format."); return; }
|
| 917 |
+
const img = currentlySelectedPanel.querySelector('img');
|
| 918 |
+
let filename = img.src.substring(img.src.lastIndexOf('/') + 1).split('?')[0];
|
| 919 |
+
img.style.opacity = '0.5';
|
| 920 |
+
fetch('/goto_timestamp', {
|
| 921 |
+
method: 'POST',
|
| 922 |
+
headers: { 'Content-Type': 'application/json' },
|
| 923 |
+
body: JSON.stringify({ filename, timestamp: parsedSeconds })
|
| 924 |
+
})
|
| 925 |
+
.then(res => res.json())
|
| 926 |
+
.then(data => {
|
| 927 |
+
if (data.success) {
|
| 928 |
+
img.src = `/frames/final/${filename}?t=${new Date().getTime()}`;
|
| 929 |
+
input.value = '';
|
| 930 |
+
resetPanelTransform();
|
| 931 |
+
} else { alert('Error: ' + data.message); }
|
| 932 |
+
img.style.opacity = '1';
|
| 933 |
+
})
|
| 934 |
+
.catch(() => {
|
| 935 |
+
alert('An error occurred.');
|
| 936 |
+
img.style.opacity = '1';
|
| 937 |
+
});
|
| 938 |
+
}
|
| 939 |
</script>
|
| 940 |
</body>
|
| 941 |
</html>'''
|
|
|
|
| 975 |
|
| 976 |
@app.route('/handle_link', methods=['POST'])
|
| 977 |
def handle_link():
|
| 978 |
+
# This route is disabled in the UI but remains functional
|
| 979 |
+
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 980 |
|
| 981 |
@app.route('/replace_panel', methods=['POST'])
|
| 982 |
def replace_panel():
|