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 ; }
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 %}
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