Files
havvk-ComfyUI_AIIA/aiia_web_export_nodes.py
T

235 lines
8.5 KiB
Python
Executable File

import os
import json
import torchaudio
import folder_paths
class AIIA_Web_Export:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"audio": ("AUDIO",),
"segments_info": ("STRING", {"forceInput": True}),
"filename_prefix": ("STRING", {"default": "aiia_lesson_01"}),
"template": (["Split Screen (Left/Right)", "Presentation (Top/Bottom)"], {"default": "Split Screen (Left/Right)"}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("html_file_path",)
OUTPUT_NODE = True
FUNCTION = "export_web"
CATEGORY = "AIIA/Web"
def export_web(self, audio, segments_info, filename_prefix, template):
# 1. Prepare Output Directory
output_dir = folder_paths.get_output_directory()
# Create a subfolder to keep things clean? or just flat
# Let's use a subfolder named after the prefix to keep assets together
save_dir = os.path.join(output_dir, filename_prefix)
os.makedirs(save_dir, exist_ok=True)
# 2. Save Audio
audio_name = f"audio.wav"
audio_path = os.path.join(save_dir, audio_name)
waveform = audio["waveform"]
sample_rate = audio["sample_rate"]
if waveform.ndim == 3: waveform = waveform.squeeze(0)
torchaudio.save(audio_path, waveform, sample_rate)
# 3. Parse Segments
try:
segments = json.loads(segments_info)
except:
segments = []
# 4. Generate HTML
html_content = self._generate_html(segments, audio_name, template, filename_prefix)
html_name = f"index.html"
html_path = os.path.join(save_dir, html_name)
with open(html_path, "w", encoding="utf-8") as f:
f.write(html_content)
print(f"[AIIA Web Export] Saved to {html_path}")
return (html_path,)
def _generate_html(self, segments, audio_filename, template, title):
# Serialize segments to JSON for JS
segments_json = json.dumps(segments)
# CSS Styles
css = """
body { font-family: 'Segoe UI', sans-serif; margin: 0; background: #121212; color: #fff; height: 100vh; overflow: hidden; }
#app { display: flex; width: 100%; height: 100%; }
/* Split Screen */
.layout-split { flex-direction: row; }
.layout-split #visual-container { flex: 2; border-right: 1px solid #333; position: relative; }
.layout-split #sidebar { flex: 1; display: flex; flex-direction: column; background: #1e1e1e; }
/* Presentation */
.layout-presentation { flex-direction: column; }
.layout-presentation #visual-container { flex: 3; border-bottom: 1px solid #333; position: relative; }
.layout-presentation #sidebar { flex: 1; display: flex; flex-direction: row; background: #1e1e1e; }
.layout-presentation #subtitle-container { flex: 1; }
.layout-presentation #controls { width: 300px; }
/* Visual Area */
#visual-container iframe, #visual-container img { width: 100%; height: 100%; border: none; object-fit: contain; }
#visual-placeholder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #666; }
/* Subtitle Area */
#subtitle-container { flex: 1; padding: 20px; overflow-y: auto; scroll-behavior: smooth; position: relative; }
.seg { padding: 10px; margin-bottom: 10px; border-radius: 8px; cursor: pointer; transition: background 0.2s; }
.seg:hover { background: #333; }
.seg.active { background: #3a3a3a; border-left: 4px solid #007bff; }
.seg-time { font-size: 0.8em; color: #888; margin-bottom: 4px; }
.seg-speaker { font-weight: bold; color: #4fc3f7; margin-right: 8px; }
.seg-text { font-size: 1.1em; line-height: 1.5; }
/* Controls */
#controls { padding: 20px; background: #252525; border-top: 1px solid #333; }
audio { width: 100%; }
"""
layout_class = "layout-split" if "Split" in template else "layout-presentation"
return f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{title}</title>
<style>{css}</style>
</head>
<body>
<div id="app" class="{layout_class}">
<div id="visual-container">
<div id="visual-placeholder">Waiting for Visual Content...</div>
</div>
<div id="sidebar">
<div id="subtitle-container">
<!-- Segments go here -->
</div>
<div id="controls">
<audio id="player" controls>
<source src="{audio_filename}" type="audio/wav">
</audio>
</div>
</div>
</div>
<script>
const segments = {segments_json};
const visualContainer = document.getElementById('visual-container');
const subtitleContainer = document.getElementById('subtitle-container');
const player = document.getElementById('player');
let currentVisual = null;
// Render Subtitle List
segments.forEach((seg, index) => {{
const div = document.createElement('div');
div.className = 'seg';
div.id = 'seg-' + index;
div.onclick = () => {{ player.currentTime = seg.start; player.play(); }};
div.innerHTML = `
<div class="seg-time">${{formatTime(seg.start)}}</div>
<div><span class="seg-speaker">${{seg.speaker || 'Unknown'}}:</span> <span class="seg-text">${{seg.text}}</span></div>
`;
subtitleContainer.appendChild(div);
seg.el = div;
}});
// Time Update Loop
player.ontimeupdate = () => {{
const t = player.currentTime;
// Find active segment
let activeIdx = -1;
let activeVisual = null;
for (let i = 0; i < segments.length; i++) {{
const seg = segments[i];
if (t >= seg.start && t < seg.end) {{
activeIdx = i;
}}
// Track visual based on passed time (last valid visual)
if (t >= seg.start && seg.visual) {{
activeVisual = seg.visual;
}}
}}
// Highlight Subtitle
document.querySelectorAll('.seg.active').forEach(el => el.classList.remove('active'));
if (activeIdx !== -1) {{
const el = segments[activeIdx].el;
el.classList.add('active');
scrollIntoViewIfNeeded(el, subtitleContainer);
}}
// Update Visual
updateVisual(activeVisual);
}};
function updateVisual(url) {{
if (url === currentVisual) return;
currentVisual = url;
visualContainer.innerHTML = '';
if (!url) {{
visualContainer.innerHTML = '<div id="visual-placeholder">No Visual Content</div>';
return;
}}
// Detect Type (Image vs Iframe)
const isImage = url.match(/\\.(jpeg|jpg|gif|png|webp)$/i);
if (isImage) {{
const img = document.createElement('img');
img.src = url;
visualContainer.appendChild(img);
}} else {{
const iframe = document.createElement('iframe');
iframe.src = url;
visualContainer.appendChild(iframe);
}}
}}
function formatTime(s) {{
const m = Math.floor(s / 60);
const ss = Math.floor(s % 60);
return `${{m}}:${{ss.toString().padStart(2, '0')}}`;
}}
function scrollIntoViewIfNeeded(target, parent) {{
const rect = target.getBoundingClientRect();
const parentRect = parent.getBoundingClientRect();
if (rect.bottom > parentRect.bottom || rect.top < parentRect.top) {{
target.scrollIntoView({{ behavior: 'smooth', block: 'center' }});
}}
}}
</script>
</body>
</html>
"""
NODE_CLASS_MAPPINGS = {
"AIIA_Web_Export": AIIA_Web_Export
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AIIA_Web_Export": "🌐 AIIA Web Export (Interactive)"
}