Skip to content

Commit d87b998

Browse files
authored
Merge pull request #1534 from Scottcjn/bottube-studio-multimodal
Studio: add Image (Gemini) + Voice (XTTS) modes alongside Video
2 parents 64d5ecb + efd3f49 commit d87b998

2 files changed

Lines changed: 221 additions & 90 deletions

File tree

bottube_templates/studio.html

Lines changed: 92 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
{% extends "base.html" %}
2-
{% block title %}Studio — Pay RTC, Generate Video{% endblock %}
2+
{% block title %}Studio — Pay RTC, Generate Video · Image · Voice{% endblock %}
33

44
{% block extra_css %}
55
<style>
66
.st-wrap { max-width: 860px; margin: 0 auto; padding: 8px 0 48px; }
77
.st-hero { text-align:center; padding: 26px 16px 6px; }
88
.st-hero h1 { font-size: 30px; margin-bottom: 8px; }
99
.st-hero p { color: var(--text-secondary); font-size:16px; max-width:580px; margin:0 auto; }
10-
.st-bal { text-align:center; margin: 14px 0 6px; color: var(--text-secondary); font-size: 14px; }
10+
.st-modes { display:flex; justify-content:center; gap:8px; margin:18px auto 6px; flex-wrap:wrap; }
11+
.st-mode { padding:9px 20px; border:1px solid var(--border); background:var(--bg-secondary); color:var(--text-secondary);
12+
border-radius:999px; cursor:pointer; font-weight:700; font-size:14px; }
13+
.st-mode.active { background:var(--accent); color:#0a0a0a; border-color:var(--accent); }
14+
.st-bal { text-align:center; margin: 12px 0 6px; color: var(--text-secondary); font-size: 14px; }
1115
.st-bal strong { color:#ffcf33; font-size: 18px; }
1216
.st-prompt { width:100%; box-sizing:border-box; background:var(--bg-secondary); border:1px solid var(--border);
1317
border-radius:var(--radius); color:var(--text-primary); padding:14px; font-size:15px; min-height:84px; resize:vertical; margin:10px 0 6px; }
@@ -22,8 +26,15 @@
2226
.tier.t0 .tbadge { background:#2ba640; } .tier.t1 .tbadge { background:#3ea6ff; } .tier.t2 .tbadge { background:#ff4444; }
2327
.tier .gen { width:100%; padding:10px; border:none; border-radius:999px; background:var(--accent); color:#0a0a0a; font-weight:700; cursor:pointer; }
2428
.tier .gen:hover { background:var(--accent-hover); } .tier .gen:disabled { opacity:.5; cursor:default; }
29+
.st-single { text-align:center; margin-top:8px; }
30+
.st-single .gen { padding:13px 34px; border:none; border-radius:999px; background:var(--accent); color:#0a0a0a; font-weight:800; font-size:16px; cursor:pointer; }
31+
.st-single .gen:disabled { opacity:.5; cursor:default; }
32+
.st-single .price { color:#ffcf33; font-weight:700; }
2533
.st-status { text-align:center; margin:18px auto; max-width:600px; color:var(--text-secondary); font-size:14px; min-height:20px; }
2634
.st-status a { color:var(--accent); font-weight:700; }
35+
.st-result { max-width:600px; margin:16px auto 0; text-align:center; }
36+
.st-result img { max-width:100%; border-radius:var(--radius); border:1px solid var(--border); }
37+
.st-result audio { width:100%; margin-top:6px; }
2738
.st-foot { text-align:center; color:var(--text-muted); font-size:12px; margin-top:24px; }
2839
</style>
2940
{% endblock %}
@@ -32,60 +43,111 @@
3243
<div class="st-wrap">
3344
<div class="st-hero">
3445
<span style="display:inline-block;background:#3ea6ff;color:#001;font-weight:700;font-size:12px;letter-spacing:.5px;padding:4px 12px;border-radius:999px;margin-bottom:12px;">🎬 BOTTUBE STUDIO</span>
35-
<h1>Pay RTC · Generate an AI Video</h1>
36-
<p>Type a prompt, pick a tier, pay in RTC — we render it on the LTX-2 pipeline and post it to your channel.</p>
46+
<h1>Pay RTC · Generate AI Media</h1>
47+
<p>Type a prompt, pick a mode, pay in RTC — video, image, or voice, rendered on our own pipeline. No gatekeepers.</p>
48+
</div>
49+
50+
<div class="st-modes">
51+
<button class="st-mode active" data-mode="video" onclick="stMode('video')">🎬 Video</button>
52+
<button class="st-mode" data-mode="image" onclick="stMode('image')">🖼️ Image</button>
53+
<button class="st-mode" data-mode="voice" onclick="stMode('voice')">🔊 Voice</button>
3754
</div>
3855

3956
<div class="st-bal" id="st-bal">Checking your RTC balance…</div>
4057

41-
<textarea class="st-prompt" id="st-prompt" maxlength="500" placeholder="Describe the video you want… e.g. 'a neon cajun bayou at night, fireflies over the water, cinematic'"></textarea>
58+
<textarea class="st-prompt" id="st-prompt" maxlength="1000" placeholder="Describe what you want…"></textarea>
4259

43-
<div class="tiers">
44-
{% for t in studio_tiers %}
60+
<!-- VIDEO: 3 tiers -->
61+
<div class="tiers" id="mode-video">
62+
{% for t in video_tiers %}
4563
<div class="tier t{{ loop.index0 }}">
4664
<span class="tbadge">{{ t.badge }}</span>
4765
<h3>{{ t.name }}</h3>
4866
<p>{{ t.desc }}</p>
4967
<div class="price">{{ t.rtc }} RTC</div>
50-
<button class="gen" data-tier="{{ t.key }}" onclick="stGenerate(this.getAttribute('data-tier'))">Generate · {{ t.rtc }} RTC</button>
68+
<button class="gen" data-tier="{{ t.key }}" onclick="stGenerate('video', this.getAttribute('data-tier'))">Generate · {{ t.rtc }} RTC</button>
5169
</div>
5270
{% endfor %}
5371
</div>
5472

73+
<!-- IMAGE -->
74+
<div class="st-single" id="mode-image" style="display:none">
75+
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:12px;">A single high-quality AI image (Gemini 2.5 Flash Image). Posted nowhere — yours to download.</p>
76+
<button class="gen" onclick="stGenerate('image')">Generate Image · <span class="price">{{ image_rtc }} RTC</span></button>
77+
</div>
78+
79+
<!-- VOICE -->
80+
<div class="st-single" id="mode-voice" style="display:none">
81+
<p style="color:var(--text-secondary);font-size:14px;margin-bottom:12px;">Type up to ~600 characters; we voice it in the Sophia Elya XTTS voice.</p>
82+
<button class="gen" id="voice-btn" onclick="stGenerate('voice')">Generate Voice · <span class="price">{{ voice_rtc }} RTC</span></button>
83+
</div>
84+
5585
<div class="st-status" id="st-status"></div>
86+
<div class="st-result" id="st-result"></div>
5687

5788
<div class="st-foot">
58-
Generation engine is pluggable — LTX-2 today, more backends soon. RTC powers it; no gatekeepers.
89+
Generation engine is pluggable — more backends soon. RTC powers it; no gatekeepers.
5990
Need RTC? <a href="/bridge/wrtc">Bridge credits</a>.
6091
</div>
6192
</div>
6293

6394
<script>
6495
(function () {
6596
var statusEl = document.getElementById("st-status");
97+
var resultEl = document.getElementById("st-result");
6698
var balEl = document.getElementById("st-bal");
67-
var buttons = function(){ return document.querySelectorAll(".tier .gen"); };
68-
function setBusy(b){ buttons().forEach(function(x){ x.disabled = b; }); }
99+
var promptEl = document.getElementById("st-prompt");
100+
var curMode = "video";
101+
var signedIn = false, voiceEnabled = true;
102+
103+
function allButtons(){ return document.querySelectorAll(".gen"); }
104+
function setBusy(b){ allButtons().forEach(function(x){ x.disabled = b; }); }
69105
function say(m){ statusEl.innerHTML = m; }
106+
function clearResult(){ resultEl.innerHTML = ""; }
107+
108+
window.stMode = function (mode) {
109+
curMode = mode;
110+
document.querySelectorAll(".st-mode").forEach(function(b){
111+
b.classList.toggle("active", b.getAttribute("data-mode") === mode);
112+
});
113+
document.getElementById("mode-video").style.display = (mode === "video") ? "" : "none";
114+
document.getElementById("mode-image").style.display = (mode === "image") ? "" : "none";
115+
document.getElementById("mode-voice").style.display = (mode === "voice") ? "" : "none";
116+
say(""); clearResult();
117+
promptEl.placeholder = (mode === "video")
118+
? "Describe the video you want… e.g. 'a neon cajun bayou at night, fireflies over the water, cinematic'"
119+
: (mode === "image")
120+
? "Describe the image… e.g. 'a vintage IBM POWER8 server glowing in a cathedral of voltage, dramatic light'"
121+
: "Type what Sophia should say… e.g. 'Welcome to Elyan Labs. Let me show you what we built.'";
122+
};
70123

71124
function loadInfo() {
72125
fetch("/api/studio/info", { credentials: "same-origin" })
73126
.then(function(r){ return r.json(); })
74127
.then(function(d){
75-
if (!d.signed_in) { balEl.innerHTML = 'Sign in to generate — <a href="/login">log in</a>.'; setBusy(true); }
128+
signedIn = !!d.signed_in;
129+
voiceEnabled = d.voice_enabled !== false;
130+
if (!signedIn) { balEl.innerHTML = 'Sign in to generate — <a href="/login">log in</a>.'; setBusy(true); }
76131
else { balEl.innerHTML = "Your balance: <strong>" + (d.rtc_balance != null ? d.rtc_balance : "?") + " RTC</strong>"; }
132+
if (!voiceEnabled) {
133+
var vb = document.getElementById("voice-btn");
134+
if (vb) { vb.disabled = true; vb.textContent = "Voice temporarily unavailable"; }
135+
}
77136
})
78137
.catch(function(){ balEl.textContent = ""; });
79138
}
80139
loadInfo();
81140

82-
window.stGenerate = function (tier) {
83-
var prompt = (document.getElementById("st-prompt").value || "").trim();
141+
window.stGenerate = function (mode, tier) {
142+
var prompt = (promptEl.value || "").trim();
84143
if (!prompt) { say("Type a prompt first."); return; }
85-
setBusy(true); say("Charging RTC and starting generation…");
144+
setBusy(true); clearResult();
145+
say(mode === "video" ? "Charging RTC and starting generation…" : "Charging RTC and generating…");
146+
var payload = { type: mode, prompt: prompt };
147+
if (mode === "video") payload.tier = tier;
86148
fetch("/api/studio/generate", {
87149
method: "POST", headers: { "Content-Type": "application/json" },
88-
credentials: "same-origin", body: JSON.stringify({ prompt: prompt, tier: tier })
150+
credentials: "same-origin", body: JSON.stringify(payload)
89151
}).then(function(r){ return r.json().then(function(d){ return { ok: r.ok, status: r.status, d: d }; }); })
90152
.then(function(res){
91153
if (!res.ok) {
@@ -95,8 +157,20 @@ <h3>{{ t.name }}</h3>
95157
return;
96158
}
97159
balEl.innerHTML = "Your balance: <strong>" + res.d.new_balance + " RTC</strong>";
98-
say("✅ Charged " + res.d.charged_rtc + " RTC. Generating your video… this can take a couple minutes.");
99-
poll(res.d.status_url);
160+
if (res.d.type === "image") {
161+
setBusy(false);
162+
say("✅ Charged " + res.d.charged_rtc + " RTC.");
163+
resultEl.innerHTML = '<img src="' + res.d.media_url + '" alt="generated image">' +
164+
'<div style="margin-top:8px"><a href="' + res.d.media_url + '" download>Download image →</a></div>';
165+
} else if (res.d.type === "voice") {
166+
setBusy(false);
167+
say("✅ Charged " + res.d.charged_rtc + " RTC.");
168+
resultEl.innerHTML = '<audio controls autoplay src="' + res.d.media_url + '"></audio>' +
169+
'<div style="margin-top:8px"><a href="' + res.d.media_url + '" download>Download audio →</a></div>';
170+
} else {
171+
say("✅ Charged " + res.d.charged_rtc + " RTC. Generating your video… this can take a couple minutes.");
172+
poll(res.d.status_url);
173+
}
100174
})
101175
.catch(function(){ setBusy(false); say("Couldn't reach the studio. Try again."); });
102176
};

0 commit comments

Comments
 (0)