<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BytePlus Seedance Batch Video Generator</title>
<style>
:root {
--bg-color: #121212;
--container-bg: #1e1e1e;
--text-color: #e5e5e5;
--border-color: #333333;
--accent-color: #3b82f6;
--accent-hover: #2563eb;
--danger-color: #ef4444;
--danger-hover: #dc2626;
--success-color: #10b981;
--success-hover: #059669;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.branding-header {
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--container-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
margin-bottom: 25px;
box-shadow: 0 8px 16px rgba(0,0,0,0.4);
flex-wrap: wrap;
gap: 20px;
}
.creator-brand, .sponsor-brand {
display: flex;
align-items: center;
gap: 15px;
}
.brand-text {
display: flex;
flex-direction: column;
gap: 6px;
}
.align-right {
text-align: right;
}
.brand-title {
font-size: 11px;
color: #9ca3af;
text-transform: uppercase;
letter-spacing: 1px;
font-weight: 700;
}
.brand-logo-yt {
height: 90px;
width: 90px;
object-fit: cover;
border-radius: 50%;
}
.brand-logo-byteplus {
height: 40px;
object-fit: contain;
background-color: #ffffff;
padding: 10px 15px;
border-radius: 8px;
}
.promo-link {
text-decoration: none;
font-size: 13px;
font-weight: bold;
padding: 8px 16px;
border-radius: 6px;
transition: all 0.2s ease-in-out;
display: inline-flex;
align-items: center;
gap: 5px;
}
.yt-link {
background-color: rgba(239, 68, 68, 0.1);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.yt-link:hover {
background-color: #ef4444;
color: white;
box-shadow: 0 0 12px rgba(239, 68, 68, 0.4);
}
.affiliate-link {
background-color: rgba(59, 130, 246, 0.1);
color: #3b82f6;
border: 1px solid rgba(59, 130, 246, 0.3);
}
.affiliate-link:hover {
background-color: #3b82f6;
color: white;
box-shadow: 0 0 12px rgba(59, 130, 246, 0.4);
}
@media (max-width: 650px) {
.branding-header {
flex-direction: column;
justify-content: center;
}
.creator-brand {
flex-direction: column;
text-align: center;
}
.sponsor-brand {
flex-direction: column-reverse;
text-align: center;
border-top: 1px solid var(--border-color);
padding-top: 20px;
width: 100%;
}
.align-right {
text-align: center;
}
}
h2 {
text-align: center;
margin-bottom: 25px;
margin-top: 10px;
color: white;
font-size: 24px;
}
.card {
background-color: var(--container-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 25px;
margin-bottom: 20px;
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.card h3 {
margin-top: 0;
border-bottom: 1px solid var(--border-color);
padding-bottom: 15px;
color: white;
font-size: 18px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 14px;
}
.info-text {
font-size: 13px;
color: #9ca3af;
margin-bottom: 10px;
display: block;
}
input[type="text"], input[type="password"], textarea, input[type="file"], select {
width: 100%;
padding: 12px;
margin-bottom: 20px;
background-color: #252525;
border: 1px solid var(--border-color);
color: white;
border-radius: 6px;
box-sizing: border-box;
font-family: inherit;
}
input[type="file"] {
padding: 8px;
}
.settings-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0 15px;
}
.settings-full {
grid-column: 1 / -1;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
margin: 0;
cursor: pointer;
}
button {
padding: 12px 24px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
color: white;
transition: all 0.2s;
}
.btn-primary { background-color: var(--accent-color); }
.btn-primary:hover { background-color: var(--accent-hover); }
.btn-danger { background-color: var(--danger-color); }
.btn-danger:hover { background-color: var(--danger-hover); }
.btn-success { background-color: var(--success-color); }
.btn-success:hover { background-color: var(--success-hover); box-shadow: 0 0 12px rgba(16, 185, 129, 0.4); }
.prompt-item {
border: 1px solid var(--border-color);
padding: 20px;
border-radius: 8px;
margin-bottom: 15px;
position: relative;
background-color: #202020;
}
.remove-btn {
position: absolute;
top: 15px;
right: 15px;
padding: 6px 12px;
font-size: 12px;
}
.file-preview {
max-width: 200px;
max-height: 200px;
margin-top: 10px;
display: none;
border-radius: 6px;
border: 1px solid var(--border-color);
}
.results-table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
.results-table th, .results-table td {
border: 1px solid var(--border-color);
padding: 14px 12px;
text-align: left;
}
.results-table th {
background-color: #252525;
color: white;
}
.status-badge {
padding: 6px 12px;
border-radius: 12px;
font-size: 11px;
font-weight: bold;
display: inline-block;
letter-spacing: 0.5px;
}
.status-queued { background-color: #f59e0b; color: #fff;}
.status-running { background-color: #3b82f6; color: #fff;}
.status-succeeded { background-color: var(--success-color); color: #fff;}
.status-failed { background-color: var(--danger-color); color: #fff;}
.flex-row {
display: flex;
gap: 10px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="branding-header">
<div class="creator-brand">
<img src="https://aitoolsarena.com/wp-content/uploads/2026/06/how-to-ai-logo.png" alt="How To In 5 Minutes" class="brand-logo-yt">
<div class="brand-text">
<span class="brand-title">Created By: How To In 5 Minutes</span>
<a href="https://www.youtube.com/@howtoin5minutes" target="_blank" class="promo-link yt-link">
▶ Subscribe on YouTube
</a>
</div>
</div>
<div class="sponsor-brand">
<div class="brand-text align-right">
<span class="brand-title">Powered By</span>
<a href="https://bit.ly/4vRwcYS" target="_blank" class="promo-link affiliate-link">
🚀 Get Seedance API Key
</a>
</div>
<img src="https://aitoolsarena.com/wp-content/uploads/2026/06/BytePlus_Company_Logo.png" alt="BytePlus" class="brand-logo-byteplus">
</div>
</div>
<h2>BytePlus Seedance Batch Video Generator</h2>
<div class="card">
<h3>ModelArk Configuration & Video Parameters</h3>
<div class="settings-full">
<label for="apiKey">API Key (Bearer Token)</label>
<input type="password" id="apiKey" placeholder="Enter your BytePlus API Key...">
</div>
<div class="settings-grid">
<div>
<label for="modelPreset">Seedance Model Preset</label>
<select id="modelPreset" onchange="document.getElementById('endpointId').value = this.value">
<option value="dreamina-seedance-2-0-260128">Seedance 2.0 (Pro)</option>
<option value="dreamina-seedance-2-0-fast-260128">Seedance 2.0 Fast</option>
<option value="dreamina-seedance-2-0-mini-260615">Seedance 2.0 Mini</option>
<option value="">-- Custom Endpoint --</option>
</select>
</div>
<div>
<label for="endpointId">Specific Model ID / Endpoint ID</label>
<input type="text" id="endpointId" value="dreamina-seedance-2-0-260128" placeholder="ep-xxxxxxxxxxxx">
</div>
<div>
<label for="resolution">Output Resolution</label>
<select id="resolution">
<option value="720p">720p (Standard)</option>
<option value="480p">480p (Low / Fast)</option>
<option value="1080p">1080p (FHD)</option>
<option value="4k">4K (UHD 10-bit)</option>
</select>
</div>
<div>
<label for="aspectRatio">Aspect Ratio</label>
<select id="aspectRatio">
<option value="16:9">16:9 (Landscape - Default)</option>
<option value="9:16">9:16 (Portrait / Shorts)</option>
<option value="21:9">21:9 (Cinematic Widescreen)</option>
<option value="4:3">4:3 (Classic TV)</option>
<option value="3:4">3:4 (Portrait Feed)</option>
<option value="1:1">1:1 (Square / Feed)</option>
</select>
</div>
<div>
<label for="duration">Video Duration</label>
<select id="duration">
<option value="auto">Auto (Recommended)</option>
<option value="4">4 Seconds</option>
<option value="5">5 Seconds</option>
<option value="6">6 Seconds</option>
<option value="7">7 Seconds</option>
<option value="8">8 Seconds</option>
<option value="9">9 Seconds</option>
<option value="10">10 Seconds</option>
<option value="11">11 Seconds</option>
<option value="12">12 Seconds</option>
<option value="13">13 Seconds</option>
<option value="14">14 Seconds</option>
<option value="15">15 Seconds</option>
</select>
</div>
<div style="display: flex; align-items: center; margin-top: 10px;">
<label class="checkbox-label">
<input type="checkbox" id="generateAudio" checked>
Include Built-in Audio Generation
</label>
</div>
</div>
</div>
<div class="card">
<h3>Task List (Batch Items)</h3>
<div id="promptsContainer"></div>
<div class="flex-row">
<button class="btn-primary" onclick="addPromptItem()">+ Add New Prompt</button>
<button class="btn-success" onclick="startBatch()" style="margin-left: auto;">🚀 Start Batch Generation</button>
</div>
</div>
<div class="card" id="resultsCard" style="display: none;">
<h3>Generation Results</h3>
<table class="results-table">
<thead>
<tr>
<th>Task ID</th>
<th>Prompt Snippet</th>
<th>Status</th>
<th>Action / Result</th>
</tr>
</thead>
<tbody id="resultsBody">
</tbody>
</table>
</div>
</div>
<script>
const API_BASE = "https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks";
let promptCounter = 0;
let activeTasks = [];
let pollingInterval = null;
document.addEventListener("DOMContentLoaded", () => {
addPromptItem();
});
function addPromptItem() {
promptCounter++;
const id = promptCounter;
const div = document.createElement('div');
div.className = 'prompt-item';
div.id = `prompt-item-${id}`;
div.innerHTML = `
<button class="btn-danger remove-btn" onclick="removePromptItem(${id})">Remove</button>
<label>Text Prompt</label>
<span class="info-text">Describe the video instructions, visual references, and style (less than 1000 characters).</span>
<textarea id="text-${id}" rows="3" placeholder="Example: Cinematic shot of fine fabric details, high fashion lighting..."></textarea>
<label>Image Reference (Image-to-Video / First Frame)</label>
<span class="info-text">Optional. Supports .jpeg, .png, .webp (Max 30MB).</span>
<input type="file" id="file-${id}" accept=".jpeg, .jpg, .png, .webp" onchange="handleFileChange(event, ${id})">
<input type="hidden" id="base64-${id}">
<div><img id="preview-${id}" class="file-preview"></div>
`;
document.getElementById('promptsContainer').appendChild(div);
}
function removePromptItem(id) {
const el = document.getElementById(`prompt-item-${id}`);
if (el) el.remove();
}
function handleFileChange(event, id) {
const file = event.target.files[0];
if (!file) return;
if (file.size > 30 * 1024 * 1024) {
alert("Warning: File sizes over 30MB are not supported by the API.");
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function(e) {
const base64String = e.target.result;
document.getElementById(`base64-${id}`).value = base64String;
const preview = document.getElementById(`preview-${id}`);
preview.src = base64String;
preview.style.display = 'block';
};
reader.readAsDataURL(file);
}
async function startBatch() {
const apiKey = document.getElementById('apiKey').value.trim();
const endpointId = document.getElementById('endpointId').value.trim();
const aspectRatio = document.getElementById('aspectRatio').value;
const duration = document.getElementById('duration').value;
const generateAudio = document.getElementById('generateAudio').checked;
if (!apiKey || !endpointId) {
alert("Please enter the API Key and Model/Endpoint ID first!");
return;
}
const items = document.querySelectorAll('.prompt-item');
if (items.length === 0) {
alert("Add at least one prompt item to generate.");
return;
}
document.getElementById('resultsCard').style.display = 'block';
const tbody = document.getElementById('resultsBody');
const newTasks = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const idExtracted = item.id.split('-')[2];
const text = document.getElementById(`text-${idExtracted}`).value.trim();
const base64 = document.getElementById(`base64-${idExtracted}`).value;
if (!text && !base64) continue;
const content = [];
if (text) {
content.push({ type: "text", text: text });
}
if (base64) {
content.push({
type: "image_url",
image_url: { url: base64 }
});
}
const payload = {
model: endpointId,
content: content,
generate_audio: generateAudio,
ratio: aspectRatio,
watermark: false
};
if (duration !== "auto") {
payload.duration = parseInt(duration);
}
try {
const response = await fetch(API_BASE, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${apiKey}`
},
body: JSON.stringify(payload)
});
const data = await response.json();
if (data.id) {
const snippet = text ? (text.substring(0, 30) + '...') : 'I2V (Image Only)';
newTasks.push({ id: data.id, status: 'queued' });
const tr = document.createElement('tr');
tr.id = `task-row-${data.id}`;
tr.innerHTML = `
<td>${data.id}</td>
<td>${snippet}</td>
<td id="status-${data.id}"><span class="status-badge status-queued">QUEUED</span></td>
<td id="action-${data.id}">Processing request...</td>
`;
tbody.appendChild(tr);
} else {
alert(`Failed to submit prompt row ${i+1}: ${data.error?.message || JSON.stringify(data)}`);
}
} catch (err) {
console.error("Fetch Error: ", err);
alert("Connection failed when calling BytePlus API. Check browser console.");
}
}
activeTasks.push(...newTasks);
if (activeTasks.length > 0 && !pollingInterval) {
pollingInterval = setInterval(() => pollTasks(apiKey), 5000);
}
}
function findVideoUrl(obj) {
if (typeof obj === 'string' && obj.startsWith('http') && (obj.includes('.mp4') || obj.includes('.mov') || obj.includes('sign='))) {
return obj;
}
if (obj !== null && typeof obj === 'object') {
if (obj.video_url && typeof obj.video_url === 'string') return obj.video_url;
if (obj.video_url && obj.video_url.url) return obj.video_url.url;
if (obj.url && typeof obj.url === 'string') return obj.url;
for (let key in obj) {
let res = findVideoUrl(obj[key]);
if (res) return res;
}
}
return null;
}
async function pollTasks(apiKey) {
const pendingTasks = activeTasks.filter(t => t.status === 'queued' || t.status === 'running');
if (pendingTasks.length === 0) {
clearInterval(pollingInterval);
pollingInterval = null;
return;
}
for (let task of pendingTasks) {
try {
const response = await fetch(`${API_BASE}/${task.id}`, {
method: "GET",
headers: {
"Authorization": `Bearer ${apiKey}`
}
});
const data = await response.json();
if (data.status) {
task.status = data.status;
const statusCell = document.getElementById(`status-${task.id}`);
const actionCell = document.getElementById(`action-${task.id}`);
let badgeClass = 'status-queued';
if (task.status === 'running') badgeClass = 'status-running';
else if (task.status === 'succeeded') badgeClass = 'status-succeeded';
else if (task.status === 'failed' || task.status === 'expired') badgeClass = 'status-failed';
statusCell.innerHTML = `<span class="status-badge ${badgeClass}">${task.status.toUpperCase()}</span>`;
if (task.status === 'succeeded') {
const videoUrl = findVideoUrl(data.content);
if (videoUrl) {
actionCell.innerHTML = `<a href="${videoUrl}" target="_blank" class="btn-success" style="padding: 6px 12px; text-decoration: none; border-radius: 4px; display: inline-block;">View / Download Video</a>`;
} else {
actionCell.innerHTML = `<button class="btn-primary" onclick="alert(JSON.stringify(${JSON.stringify(data.content).replace(/"/g, '"')}))">View Raw JSON Data</button>`;
}
} else if (task.status === 'failed' || task.status === 'expired') {
actionCell.innerHTML = `Error: ${data.error?.message || 'Timeout / System Error'}`;
}
}
} catch (err) {
console.error(`Error while checking task ${task.id}:`, err);
}
}
}
</script>
</body>
</html>