Commit Β·
e224b41
1
Parent(s): 50a5f17
Complete Bar Race module with HTML UI and JS handlers
Browse files- modules/bar_race/__init__.py +6 -1
- static/index.html +130 -0
modules/bar_race/__init__.py
CHANGED
|
@@ -8,9 +8,14 @@ from fastapi import FastAPI
|
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
_app = None
|
| 12 |
|
| 13 |
-
def register(app: FastAPI):
|
| 14 |
"""Register Bar Race module routes"""
|
| 15 |
global _app
|
| 16 |
_app = app
|
|
|
|
| 8 |
|
| 9 |
logger = logging.getLogger(__name__)
|
| 10 |
|
| 11 |
+
# Module metadata for auto-discovery
|
| 12 |
+
MODULE_NAME = "bar_race"
|
| 13 |
+
MODULE_PREFIX = "/api/bar-race"
|
| 14 |
+
MODULE_DESCRIPTION = "Bar Chart Race Video Generator"
|
| 15 |
+
|
| 16 |
_app = None
|
| 17 |
|
| 18 |
+
def register(app: FastAPI, config=None):
|
| 19 |
"""Register Bar Race module routes"""
|
| 20 |
global _app
|
| 21 |
_app = app
|
static/index.html
CHANGED
|
@@ -279,6 +279,9 @@
|
|
| 279 |
<button class="tab-btn" data-tab="quiz">
|
| 280 |
π― Quiz Reel
|
| 281 |
</button>
|
|
|
|
|
|
|
|
|
|
| 282 |
</div>
|
| 283 |
|
| 284 |
<!-- Story Reels Tab -->
|
|
@@ -643,6 +646,69 @@
|
|
| 643 |
</div>
|
| 644 |
</div>
|
| 645 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 646 |
<script>
|
| 647 |
// Tab switching
|
| 648 |
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
@@ -1166,6 +1232,70 @@
|
|
| 1166 |
return id;
|
| 1167 |
}
|
| 1168 |
}, 100); // End of setTimeout
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1169 |
</script>
|
| 1170 |
|
| 1171 |
<!-- Chat Widget Button -->
|
|
|
|
| 279 |
<button class="tab-btn" data-tab="quiz">
|
| 280 |
π― Quiz Reel
|
| 281 |
</button>
|
| 282 |
+
<button class="tab-btn" data-tab="barrace">
|
| 283 |
+
π Bar Race
|
| 284 |
+
</button>
|
| 285 |
</div>
|
| 286 |
|
| 287 |
<!-- Story Reels Tab -->
|
|
|
|
| 646 |
</div>
|
| 647 |
</div>
|
| 648 |
|
| 649 |
+
<!-- Bar Race Tab -->
|
| 650 |
+
<div id="barrace-tab" class="tab-content">
|
| 651 |
+
<div class="card">
|
| 652 |
+
<h2>π Bar Chart Race Generator</h2>
|
| 653 |
+
<p style="color: var(--text-secondary); margin-bottom: 1.5rem;">
|
| 654 |
+
Create animated bar chart race videos showing data over time
|
| 655 |
+
</p>
|
| 656 |
+
|
| 657 |
+
<form id="barRaceForm">
|
| 658 |
+
<div class="form-group">
|
| 659 |
+
<label>Topic *</label>
|
| 660 |
+
<select id="barRaceTopic" required>
|
| 661 |
+
<option value="gdp_nominal">GDP (Nominal) - Richest Countries</option>
|
| 662 |
+
<option value="population">Population - Most Populated Countries</option>
|
| 663 |
+
<option value="gdp_per_capita">GDP Per Capita</option>
|
| 664 |
+
<option value="social_media_users">Social Media Users</option>
|
| 665 |
+
<option value="youtube_subscribers">YouTube Subscribers</option>
|
| 666 |
+
<option value="military_spending">Military Expenditure</option>
|
| 667 |
+
<option value="olympic_medals">Olympic Gold Medals</option>
|
| 668 |
+
<option value="life_expectancy">Life Expectancy</option>
|
| 669 |
+
<option value="browser_market_share">Browser Market Share</option>
|
| 670 |
+
</select>
|
| 671 |
+
</div>
|
| 672 |
+
|
| 673 |
+
<div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
| 674 |
+
<div class="form-group">
|
| 675 |
+
<label>Start Year</label>
|
| 676 |
+
<input type="number" id="barRaceYearStart" value="2000" min="1960" max="2024">
|
| 677 |
+
</div>
|
| 678 |
+
<div class="form-group">
|
| 679 |
+
<label>End Year</label>
|
| 680 |
+
<input type="number" id="barRaceYearEnd" value="2024" min="1960" max="2024">
|
| 681 |
+
</div>
|
| 682 |
+
</div>
|
| 683 |
+
|
| 684 |
+
<div class="form-row" style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;">
|
| 685 |
+
<div class="form-group">
|
| 686 |
+
<label>Top N (Bars)</label>
|
| 687 |
+
<select id="barRaceTopN">
|
| 688 |
+
<option value="5">5</option>
|
| 689 |
+
<option value="8">8</option>
|
| 690 |
+
<option value="10" selected>10</option>
|
| 691 |
+
<option value="15">15</option>
|
| 692 |
+
</select>
|
| 693 |
+
</div>
|
| 694 |
+
<div class="form-group">
|
| 695 |
+
<label>Duration (seconds)</label>
|
| 696 |
+
<select id="barRaceDuration">
|
| 697 |
+
<option value="30">30s</option>
|
| 698 |
+
<option value="60" selected>60s</option>
|
| 699 |
+
<option value="90">90s</option>
|
| 700 |
+
<option value="120">120s</option>
|
| 701 |
+
</select>
|
| 702 |
+
</div>
|
| 703 |
+
</div>
|
| 704 |
+
|
| 705 |
+
<button type="submit" class="btn btn-primary" style="width: 100%;">π Generate Bar Race Video</button>
|
| 706 |
+
</form>
|
| 707 |
+
|
| 708 |
+
<div id="barRaceStatus" class="status hidden"></div>
|
| 709 |
+
</div>
|
| 710 |
+
</div>
|
| 711 |
+
|
| 712 |
<script>
|
| 713 |
// Tab switching
|
| 714 |
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
|
|
| 1232 |
return id;
|
| 1233 |
}
|
| 1234 |
}, 100); // End of setTimeout
|
| 1235 |
+
|
| 1236 |
+
// Bar Race Form
|
| 1237 |
+
document.getElementById('barRaceForm').addEventListener('submit', async (e) => {
|
| 1238 |
+
e.preventDefault();
|
| 1239 |
+
const status = document.getElementById('barRaceStatus');
|
| 1240 |
+
status.className = 'status processing';
|
| 1241 |
+
status.innerHTML = 'β³ Starting bar race generation...';
|
| 1242 |
+
|
| 1243 |
+
const topic = document.getElementById('barRaceTopic').value;
|
| 1244 |
+
const yearStart = parseInt(document.getElementById('barRaceYearStart').value);
|
| 1245 |
+
const yearEnd = parseInt(document.getElementById('barRaceYearEnd').value);
|
| 1246 |
+
const topN = parseInt(document.getElementById('barRaceTopN').value);
|
| 1247 |
+
const duration = parseInt(document.getElementById('barRaceDuration').value);
|
| 1248 |
+
|
| 1249 |
+
try {
|
| 1250 |
+
const response = await fetch('/api/bar-race/generate', {
|
| 1251 |
+
method: 'POST',
|
| 1252 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1253 |
+
body: JSON.stringify({
|
| 1254 |
+
topic: topic,
|
| 1255 |
+
year_start: yearStart,
|
| 1256 |
+
year_end: yearEnd,
|
| 1257 |
+
top_n: topN,
|
| 1258 |
+
duration_seconds: duration,
|
| 1259 |
+
fps: 30
|
| 1260 |
+
})
|
| 1261 |
+
});
|
| 1262 |
+
|
| 1263 |
+
const data = await response.json();
|
| 1264 |
+
if (!response.ok) throw new Error(data.detail || 'Failed to start');
|
| 1265 |
+
|
| 1266 |
+
status.innerHTML = `β³ Job started: ${data.job_id}. Generating frames...`;
|
| 1267 |
+
pollBarRaceStatus(data.job_id);
|
| 1268 |
+
|
| 1269 |
+
} catch (err) {
|
| 1270 |
+
status.className = 'status error';
|
| 1271 |
+
status.innerHTML = 'β ' + err.message;
|
| 1272 |
+
}
|
| 1273 |
+
});
|
| 1274 |
+
|
| 1275 |
+
async function pollBarRaceStatus(jobId) {
|
| 1276 |
+
const status = document.getElementById('barRaceStatus');
|
| 1277 |
+
const poll = async () => {
|
| 1278 |
+
try {
|
| 1279 |
+
const res = await fetch(`/api/bar-race/${jobId}/status`);
|
| 1280 |
+
const data = await res.json();
|
| 1281 |
+
|
| 1282 |
+
if (data.status === 'ready') {
|
| 1283 |
+
status.className = 'status success';
|
| 1284 |
+
status.innerHTML = `β
Video ready! <a href="${data.video_url}" target="_blank" style="color:#4ade80;">Download Video</a>`;
|
| 1285 |
+
} else if (data.status === 'failed') {
|
| 1286 |
+
status.className = 'status error';
|
| 1287 |
+
status.innerHTML = 'β Failed: ' + (data.error || 'Unknown error');
|
| 1288 |
+
} else {
|
| 1289 |
+
status.innerHTML = `β³ ${data.status}... ${data.progress}%`;
|
| 1290 |
+
setTimeout(poll, 2000);
|
| 1291 |
+
}
|
| 1292 |
+
} catch (err) {
|
| 1293 |
+
status.className = 'status error';
|
| 1294 |
+
status.innerHTML = 'β Status check failed: ' + err.message;
|
| 1295 |
+
}
|
| 1296 |
+
};
|
| 1297 |
+
poll();
|
| 1298 |
+
}
|
| 1299 |
</script>
|
| 1300 |
|
| 1301 |
<!-- Chat Widget Button -->
|