You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
624 lines
47 KiB
624 lines
47 KiB
{% extends "layout.html" %}
|
|
{% block main %}
|
|
|
|
<div class="bg-[#F2EFEB] min-h-[calc(100vh-6.5rem)] py-8">
|
|
<div class="container mx-auto px-4 max-w-5xl">
|
|
<div class="flex flex-col md:flex-row justify-between items-end gap-4 mb-8">
|
|
<div>
|
|
<h1 class="text-3xl font-display font-black uppercase tracking-tighter text-[#111111] mb-2">Settings</h1>
|
|
<p class="text-[#111111]/50 font-mono text-sm">Configure platform credentials and video generation preferences.</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button id="defaultSettingsBtn" type="button" class="btn-ghost-neo text-sm" data-demo-disabled>
|
|
Reset Defaults
|
|
</button>
|
|
<button form="settingsForm" type="submit" class="btn-primary-neo text-sm" data-demo-disabled>
|
|
Save Changes
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<form id="settingsForm" action="{{ app_url('/settings') }}" method="post" novalidate>
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-8">
|
|
|
|
<!-- Navigation Tabs -->
|
|
<div class="lg:col-span-1">
|
|
<ul class="border border-[#111111] bg-white sticky top-24" id="settingsTabs">
|
|
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer active" data-tab="platform"><i data-lucide="layout-template" class="w-4 h-4"></i> Platform</a></li>
|
|
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="content"><i data-lucide="file-text" class="w-4 h-4"></i> Content</a></li>
|
|
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="visuals"><i data-lucide="palette" class="w-4 h-4"></i> Visuals</a></li>
|
|
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] border-b border-[#111111]/20 hover:bg-[#111111]/5 cursor-pointer" data-tab="audio"><i data-lucide="volume-2" class="w-4 h-4"></i> Audio</a></li>
|
|
<li><a class="flex gap-3 py-3 px-4 font-mono text-sm uppercase tracking-wider text-[#111111] hover:bg-[#111111]/5 cursor-pointer" data-tab="integration"><i data-lucide="share-2" class="w-4 h-4"></i> Integration</a></li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="lg:col-span-3 space-y-6">
|
|
|
|
<!-- Platform Tab -->
|
|
<div id="tab-platform" class="tab-pane">
|
|
<div class="card-neo">
|
|
<div class="p-6">
|
|
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111] mb-6">Source Configuration</h2>
|
|
|
|
<div class="form-control w-full mb-8">
|
|
<label class="label"><span class="label-text text-[#111111] font-mono text-xs uppercase tracking-wider font-medium">Content Platform</span></label>
|
|
<select name="settings.platform" id="platformSelect" class="input-neo w-full bg-white">
|
|
<option value="reddit">Reddit</option>
|
|
<option value="threads">Threads (Meta)</option>
|
|
</select>
|
|
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Which social media platform to pull content from</span></label>
|
|
</div>
|
|
|
|
<!-- Reddit Section -->
|
|
<div class="platform-section space-y-6" data-platform="reddit">
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Reddit Credentials</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client ID</span></label>
|
|
<input name="reddit.creds.client_id" value="{{ data['reddit.creds.client_id'] }}" type="text" class="input-neo w-full" placeholder="Your Client ID">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client Secret</span></label>
|
|
<input name="reddit.creds.client_secret" value="{{ data['reddit.creds.client_secret'] }}" type="text" class="input-neo w-full" placeholder="Your Client Secret">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Username</span></label>
|
|
<input name="reddit.creds.username" value="{{ data['reddit.creds.username'] }}" type="text" class="input-neo w-full" placeholder="Reddit Username">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Password</span></label>
|
|
<input name="reddit.creds.password" value="{{ data['reddit.creds.password'] }}" type="password" class="input-neo w-full" placeholder="Reddit Password">
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
|
|
<input name="reddit.creds.2fa" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono">Enable 2FA Support</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Threads Section -->
|
|
<div class="platform-section space-y-6 hidden" data-platform="threads">
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Threads Configuration</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Discovery Method</span></label>
|
|
<select name="threads.discovery_method" id="threadsDiscoveryMethodSelect" class="input-neo w-full bg-white">
|
|
<option value="api">API (Your own posts)</option>
|
|
<option value="scrape">Scrape (For You feed)</option>
|
|
</select>
|
|
</div>
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 threads-discovery-section hidden" data-discovery-methods="scrape" data-preserve-hidden="true">/// Threads Login</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 threads-discovery-section hidden" data-discovery-methods="scrape" data-preserve-hidden="true">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads Username</span></label>
|
|
<input name="threads.creds.username" value="{{ data.get('threads.creds.username', '') }}" type="text" class="input-neo w-full" placeholder="Username">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads Password</span></label>
|
|
<input name="threads.creds.password" value="{{ data.get('threads.creds.password', '') }}" type="password" class="input-neo w-full" placeholder="Password">
|
|
</div>
|
|
</div>
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 threads-discovery-section hidden" data-discovery-methods="api">/// Threads API Credentials</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control w-full threads-discovery-section hidden" data-discovery-methods="api">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Access Token</span></label>
|
|
<input name="threads.creds.access_token" value="{{ data.get('threads.creds.access_token', '') }}" type="text" class="input-neo w-full" placeholder="Long-lived Graph API Token">
|
|
</div>
|
|
<div class="form-control w-full threads-discovery-section hidden" data-discovery-methods="api">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Threads User ID</span></label>
|
|
<input name="threads.creds.user_id" value="{{ data.get('threads.creds.user_id', '') }}" type="text" class="input-neo w-full" placeholder="12345678901234567">
|
|
</div>
|
|
</div>
|
|
<p class="text-[#111111]/50 font-mono text-xs leading-relaxed threads-discovery-section hidden" data-discovery-methods="api">
|
|
Screenshots still reuse your saved Threads username/password. Switch to Scrape if you need to edit those login credentials.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content Tab -->
|
|
<div id="tab-content" class="tab-pane hidden">
|
|
<div class="card-neo">
|
|
<div class="p-6 space-y-6">
|
|
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111] mb-2">Content Filtering</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<!-- Reddit Options -->
|
|
<div class="platform-section space-y-4" data-platform="reddit">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Target Subreddit</span></label>
|
|
<input name="reddit.thread.subreddit" value="{{ data['reddit.thread.subreddit'] }}" type="text" class="input-neo w-full">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Max Comment Length: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="reddit.thread.max_comment_length" type="range" min="100" max="1000" step="50" class="range range-xs accent-[#DE6C56]" value="{{ data['reddit.thread.max_comment_length'] }}">
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Min Comments: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="reddit.thread.min_comments" type="range" min="1" max="100" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['reddit.thread.min_comments'] }}">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Blocked Words</span></label>
|
|
<input name="reddit.thread.blocked_words" value="{{ data['reddit.thread.blocked_words'] }}" type="text" class="input-neo w-full" placeholder="nsfw, spoiler, politics">
|
|
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated. Matching posts and comments are skipped.</span></label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Threads Options -->
|
|
<div class="platform-section hidden space-y-4" data-platform="threads">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Search Queries</span></label>
|
|
<input name="threads.thread.search_queries" value="{{ data['threads.thread.search_queries'] }}" type="text" class="input-neo w-full" placeholder="news,viral,stories">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Max Reply Length: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="threads.thread.max_reply_length" type="range" min="100" max="1000" step="50" class="range range-xs accent-[#DE6C56]" value="{{ data['threads.thread.max_reply_length'] }}">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Min Replies: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="threads.thread.min_replies" type="range" min="1" max="50" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['threads.thread.min_replies'] }}">
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Blocked Words</span></label>
|
|
<input name="threads.thread.blocked_words" value="{{ data['threads.thread.blocked_words'] }}" type="text" class="input-neo w-full" placeholder="nsfw, spoiler, politics">
|
|
<label class="label"><span class="label-text-alt text-[#111111]/40 font-mono text-xs">Comma-separated. Matching posts and replies are skipped.</span></label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// General</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
|
|
<input name="settings.allow_nsfw" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono">Allow NSFW Content</span>
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Videos to generate: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="settings.times_to_run" type="range" min="1" max="50" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.times_to_run'] }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Visuals Tab -->
|
|
<div id="tab-visuals" class="tab-pane hidden">
|
|
<div class="card-neo">
|
|
<div class="p-6 space-y-6">
|
|
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">Visual Styling</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Screenshot Theme</span></label>
|
|
<select name="settings.theme" class="input-neo w-full bg-white">
|
|
<option value="dark">Dark</option>
|
|
<option value="light">Light</option>
|
|
<option value="transparent">Transparent</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Comment Opacity: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="settings.opacity" type="range" min="0" max="1" step="0.05" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.opacity'] }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2">/// Backgrounds</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Video Background</span></label>
|
|
<select name="settings.background.background_video" class="input-neo w-full bg-white">
|
|
{% for background in checks["settings.background.background_video"]["options"] %}
|
|
<option value="{{background}}">{{ background or 'Random' }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<label class="label"><a href="{{ app_url('/backgrounds') }}" target="_blank" class="label-text-alt text-[#DE6C56] hover:text-[#111111] font-mono text-xs">Manage video files →</a></label>
|
|
</div>
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">Audio Track</span></label>
|
|
<select name="settings.background.background_audio" class="input-neo w-full bg-white">
|
|
{% for audio in checks["settings.background.background_audio"]["options"] %}
|
|
<option value="{{audio}}">{{ audio or 'None' }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
|
|
<input name="settings.background.background_thumbnail" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono">Generate Thumbnail overlay</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audio Tab -->
|
|
<div id="tab-audio" class="tab-pane hidden">
|
|
<div class="card-neo">
|
|
<div class="p-6 space-y-6">
|
|
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">Voice & Speech</h2>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111] font-mono text-xs font-medium">TTS Provider</span></label>
|
|
<select name="settings.tts.voice_choice" id="voiceChoiceSelect" class="input-neo w-full bg-white">
|
|
{% for provider in checks["settings.tts.voice_choice"]["options"] %}
|
|
<option value="{{ provider }}">{% if provider == "streamlabspolly" %}Streamlabs Polly (Free){% elif provider == "googletranslate" %}Google Translate{% elif provider == "awspolly" %}AWS Polly{% elif provider == "pyttsx" %}System Voice (pyttsx){% else %}{{ provider }}{% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Silence between comments: <span class="val-display font-mono text-[#DE6C56]"></span>s</span></label>
|
|
<input name="settings.tts.silence_duration" type="range" min="0" max="5" step="0.1" class="range range-xs accent-[#DE6C56]" value="{{ data['settings.tts.silence_duration'] }}">
|
|
</div>
|
|
<div class="flex flex-col gap-3 justify-center">
|
|
<div class="flex items-center gap-4">
|
|
<input name="settings.tts.random_voice" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono">Randomize voices</span>
|
|
</div>
|
|
<div class="flex items-center gap-4">
|
|
<input name="settings.tts.no_emojis" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#DE6C56] [&:checked]:bg-[#DE6C56] [&:checked]:border-[#DE6C56]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono">Strip emojis</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 tts-provider-section hidden" data-tts-providers="elevenlabs,OpenAI,tiktok">/// API Credentials</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="elevenlabs">
|
|
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">ElevenLabs Key</span></label>
|
|
<input name="settings.tts.elevenlabs_api_key" value="{{ data.get('settings.tts.elevenlabs_api_key', '') }}" type="password" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
|
|
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">OpenAI Key</span></label>
|
|
<input name="settings.tts.openai_api_key" value="{{ data.get('settings.tts.openai_api_key', '') }}" type="password" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
|
|
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">OpenAI API URL</span></label>
|
|
<input name="settings.tts.openai_api_url" value="{{ data.get('settings.tts.openai_api_url', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden md:col-span-2" data-tts-providers="tiktok">
|
|
<label class="label"><span class="label-text text-[#111111]/50 font-mono text-xs">TikTok Session ID</span></label>
|
|
<input name="settings.tts.tiktok_sessionid" value="{{ data.get('settings.tts.tiktok_sessionid', '') }}" type="password" class="input-neo w-full text-sm">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="font-mono text-xs uppercase tracking-widest text-[#111111]/30 py-2 tts-provider-section hidden" data-tts-providers="Supertonic,elevenlabs,OpenAI,streamlabspolly,awspolly,tiktok,pyttsx">/// Provider Settings</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Supertonic Voice</span></label>
|
|
<select name="settings.tts.supertonic_voice" class="input-neo w-full bg-white">
|
|
{% for voice in checks["settings.tts.supertonic_voice"]["options"] %}
|
|
<option value="{{ voice }}">{{ voice }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Language Code</span></label>
|
|
<input name="settings.tts.supertonic_lang" value="{{ data.get('settings.tts.supertonic_lang', '') }}" type="text" class="input-neo w-full text-sm" placeholder="na">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Quality Steps: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="settings.tts.supertonic_steps" type="range" min="5" max="12" step="1" class="range range-xs accent-[#DE6C56]" value="{{ data.get('settings.tts.supertonic_steps', 8) }}">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="Supertonic">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Playback Speed: <span class="val-display font-mono text-[#DE6C56]"></span></span></label>
|
|
<input name="settings.tts.supertonic_speed" type="range" min="0.7" max="2" step="0.05" class="range range-xs accent-[#DE6C56]" value="{{ data.get('settings.tts.supertonic_speed', 1.05) }}">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden md:col-span-2" data-tts-providers="Supertonic">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Custom Voice JSON Path</span></label>
|
|
<input name="settings.tts.supertonic_custom_voice_path" value="{{ data.get('settings.tts.supertonic_custom_voice_path', '') }}" type="text" class="input-neo w-full text-sm" placeholder="/app/voices/my-voice.json">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="elevenlabs">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">ElevenLabs Voice</span></label>
|
|
<select name="settings.tts.elevenlabs_voice_name" class="input-neo w-full bg-white">
|
|
{% for voice in checks["settings.tts.elevenlabs_voice_name"]["options"] %}
|
|
<option value="{{ voice }}">{{ voice }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">OpenAI Voice</span></label>
|
|
<select name="settings.tts.openai_voice_name" class="input-neo w-full bg-white">
|
|
{% for voice in checks["settings.tts.openai_voice_name"]["options"] %}
|
|
<option value="{{ voice }}">{{ voice }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="OpenAI">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">OpenAI Model</span></label>
|
|
<select name="settings.tts.openai_model" class="input-neo w-full bg-white">
|
|
{% for model in checks["settings.tts.openai_model"]["options"] %}
|
|
<option value="{{ model }}">{{ model }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="streamlabspolly">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Streamlabs Voice</span></label>
|
|
<input name="settings.tts.streamlabs_polly_voice" value="{{ data.get('settings.tts.streamlabs_polly_voice', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="awspolly">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">AWS Polly Voice</span></label>
|
|
<input name="settings.tts.aws_polly_voice" value="{{ data.get('settings.tts.aws_polly_voice', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="tiktok">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">TikTok Voice</span></label>
|
|
<input name="settings.tts.tiktok_voice" value="{{ data.get('settings.tts.tiktok_voice', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="pyttsx">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">System Voice Index</span></label>
|
|
<input name="settings.tts.python_voice" value="{{ data.get('settings.tts.python_voice', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
<div class="form-control tts-provider-section hidden" data-tts-providers="pyttsx">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Installed Voice Count</span></label>
|
|
<input name="settings.tts.py_voice_num" value="{{ data.get('settings.tts.py_voice_num', '') }}" type="text" class="input-neo w-full text-sm">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Integration Tab -->
|
|
<div id="tab-integration" class="tab-pane hidden">
|
|
<div class="card-neo">
|
|
<div class="p-6 space-y-6">
|
|
<h2 class="font-display font-black uppercase tracking-tighter text-xl text-[#111111]">YouTube Integration</h2>
|
|
|
|
<div class="flex items-center gap-4 bg-white border border-[#111111] p-4">
|
|
<input name="youtube.enabled" type="checkbox" class="toggle toggle-lg [--tglbg:#F2EFEB] [--toggle-border:#111111] bg-[#111111]/20 hover:bg-[#4ADE80] [&:checked]:bg-[#4ADE80] [&:checked]:border-[#4ADE80]" value="True">
|
|
<span class="text-sm text-[#111111] font-mono font-medium">Auto-upload after rendering</span>
|
|
</div>
|
|
|
|
<div class="form-control w-full">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Client Secret Path</span></label>
|
|
<input name="youtube.client_secret_path" value="{{ data['youtube.client_secret_path'] }}" type="text" class="input-neo w-full" placeholder="path/to/secret.json">
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div class="form-control">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Privacy</span></label>
|
|
<select name="youtube.privacy" class="input-neo w-full bg-white">
|
|
<option value="public">Public</option>
|
|
<option value="private">Private</option>
|
|
<option value="unlisted">Unlisted</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-control col-span-2">
|
|
<label class="label"><span class="label-text text-[#111111]/60 font-mono text-xs">Tags (comma-separated)</span></label>
|
|
<input name="youtube.tags" value="{{ data['youtube.tags'] }}" type="text" class="input-neo w-full" placeholder="shorts, reddit, viral">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const data = {{ data | tojson | safe }};
|
|
const validateChecks = {{ checks | tojson | safe }};
|
|
const form = document.getElementById('settingsForm');
|
|
function lockDemoFields() {
|
|
if (!window.PUBLIC_DEMO_MODE) return;
|
|
form.querySelectorAll('input, select, textarea').forEach(function(el) {
|
|
el.disabled = true;
|
|
});
|
|
}
|
|
lockDemoFields();
|
|
|
|
// ---- Tab Switching -------------------------------------------------
|
|
const tabs = document.querySelectorAll('#settingsTabs a');
|
|
const panes = document.querySelectorAll('.tab-pane');
|
|
|
|
tabs.forEach(tab => {
|
|
tab.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const target = tab.dataset.tab;
|
|
|
|
tabs.forEach(t => t.classList.remove('active'));
|
|
tab.classList.add('active');
|
|
|
|
panes.forEach(p => p.classList.toggle('hidden', p.id !== `tab-${target}`));
|
|
});
|
|
});
|
|
|
|
// ---- Form Initialization -------------------------------------------
|
|
form.querySelectorAll('input, select, textarea').forEach(input => {
|
|
const name = input.name;
|
|
if (data[name] !== undefined) {
|
|
if (input.type === 'checkbox') {
|
|
input.checked = (data[name] === "True" || data[name] === true);
|
|
} else {
|
|
input.value = data[name];
|
|
}
|
|
} else if (validateChecks[name] && validateChecks[name].default !== undefined) {
|
|
if (input.type === 'checkbox') {
|
|
input.checked = (validateChecks[name].default === "True" || validateChecks[name].default === true);
|
|
} else {
|
|
input.value = validateChecks[name].default;
|
|
}
|
|
}
|
|
|
|
if (input.type === 'range') {
|
|
updateRangeDisplay(input);
|
|
input.addEventListener('input', () => updateRangeDisplay(input));
|
|
}
|
|
});
|
|
|
|
function updateRangeDisplay(input) {
|
|
const display = input.closest('.form-control')?.querySelector('.val-display');
|
|
if (display) display.textContent = input.value;
|
|
}
|
|
|
|
// ---- Platform Visibility -------------------------------------------
|
|
const platformSelect = document.getElementById('platformSelect');
|
|
const threadsDiscoveryMethodSelect = document.getElementById('threadsDiscoveryMethodSelect');
|
|
|
|
function applyPlatformVisibility() {
|
|
const current = platformSelect.value || "reddit";
|
|
document.querySelectorAll('.platform-section').forEach(section => {
|
|
const matches = section.dataset.platform === current;
|
|
section.classList.toggle('hidden', !matches);
|
|
section.querySelectorAll('input, select, textarea').forEach(el => el.disabled = !matches);
|
|
});
|
|
lockDemoFields();
|
|
}
|
|
|
|
function matchesDiscoveryMethod(section, method) {
|
|
return (section.dataset.discoveryMethods || '')
|
|
.split(',')
|
|
.map(value => value.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
.includes((method || '').trim().toLowerCase());
|
|
}
|
|
|
|
function applyThreadsDiscoveryVisibility() {
|
|
const isThreads = (platformSelect.value || 'reddit') === 'threads';
|
|
const current = threadsDiscoveryMethodSelect?.value || 'api';
|
|
document.querySelectorAll('.threads-discovery-section').forEach(section => {
|
|
const matches = isThreads && matchesDiscoveryMethod(section, current);
|
|
const preserveHidden = section.dataset.preserveHidden === 'true';
|
|
section.classList.toggle('hidden', !matches);
|
|
section.querySelectorAll('input, select, textarea').forEach(el => {
|
|
el.disabled = !isThreads || (!matches && !preserveHidden);
|
|
});
|
|
});
|
|
lockDemoFields();
|
|
}
|
|
|
|
platformSelect.addEventListener('change', applyPlatformVisibility);
|
|
platformSelect.addEventListener('change', applyThreadsDiscoveryVisibility);
|
|
threadsDiscoveryMethodSelect?.addEventListener('change', applyThreadsDiscoveryVisibility);
|
|
applyPlatformVisibility();
|
|
applyThreadsDiscoveryVisibility();
|
|
|
|
// ---- TTS Provider Visibility ---------------------------------------
|
|
const voiceChoiceSelect = document.getElementById('voiceChoiceSelect');
|
|
|
|
function matchesTtsProvider(section, provider) {
|
|
return (section.dataset.ttsProviders || '')
|
|
.split(',')
|
|
.map(value => value.trim().toLowerCase())
|
|
.filter(Boolean)
|
|
.includes((provider || '').trim().toLowerCase());
|
|
}
|
|
|
|
function applyTtsVisibility() {
|
|
const current = voiceChoiceSelect.value || 'Supertonic';
|
|
document.querySelectorAll('.tts-provider-section').forEach(section => {
|
|
const matches = matchesTtsProvider(section, current);
|
|
section.classList.toggle('hidden', !matches);
|
|
section.querySelectorAll('input, select, textarea').forEach(el => {
|
|
el.disabled = !matches;
|
|
});
|
|
});
|
|
lockDemoFields();
|
|
}
|
|
|
|
voiceChoiceSelect.addEventListener('change', applyTtsVisibility);
|
|
applyTtsVisibility();
|
|
|
|
// ---- Validation ----------------------------------------------------
|
|
function validateInput(input) {
|
|
const check = validateChecks[input.name];
|
|
if (!check) return true;
|
|
|
|
let value = input.value;
|
|
let isValid = true;
|
|
|
|
if (value.length === 0) {
|
|
isValid = !!check.optional;
|
|
} else {
|
|
if (check.type === 'int' || check.type === 'float') {
|
|
const num = parseFloat(value);
|
|
if (check.nmin !== undefined && num < check.nmin) isValid = false;
|
|
if (check.nmax !== undefined && num > check.nmax) isValid = false;
|
|
} else {
|
|
if (check.nmin !== undefined && value.length < check.nmin) isValid = false;
|
|
if (check.nmax !== undefined && value.length > check.nmax) isValid = false;
|
|
}
|
|
|
|
if (isValid && check.regex) {
|
|
const re = new RegExp(check.regex);
|
|
if (!re.test(value)) isValid = false;
|
|
}
|
|
}
|
|
|
|
input.classList.toggle('input-error', !isValid);
|
|
input.classList.toggle('select-error', !isValid && input.tagName === 'SELECT');
|
|
return isValid;
|
|
}
|
|
|
|
form.querySelectorAll('input, select').forEach(input => {
|
|
input.addEventListener('change', () => validateInput(input));
|
|
if (input.tagName === 'INPUT') input.addEventListener('keyup', () => validateInput(input));
|
|
});
|
|
|
|
// ---- Submit Logic --------------------------------------------------
|
|
form.addEventListener('submit', (e) => {
|
|
let formIsValid = true;
|
|
const enabledInputs = form.querySelectorAll('input:not(:disabled), select:not(:disabled)');
|
|
|
|
enabledInputs.forEach(input => {
|
|
if (!validateInput(input)) {
|
|
formIsValid = false;
|
|
const pane = input.closest('.tab-pane');
|
|
if (pane) {
|
|
const tabBtn = document.querySelector(`[data-tab="${pane.id.replace('tab-', '')}"]`);
|
|
if (tabBtn) tabBtn.click();
|
|
}
|
|
}
|
|
});
|
|
|
|
if (!formIsValid) {
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
form.querySelectorAll('input[type="checkbox"]:not(:disabled)').forEach(cb => {
|
|
if (!cb.checked) {
|
|
const hidden = document.createElement('input');
|
|
hidden.type = 'hidden';
|
|
hidden.name = cb.name;
|
|
hidden.value = 'False';
|
|
form.appendChild(hidden);
|
|
}
|
|
});
|
|
});
|
|
|
|
// ---- Defaults ------------------------------------------------------
|
|
document.getElementById('defaultSettingsBtn').addEventListener('click', () => {
|
|
if (!confirm('Are you sure you want to reset visible settings to defaults?')) return;
|
|
|
|
form.querySelectorAll('input:not(:disabled), select:not(:disabled)').forEach(input => {
|
|
const check = validateChecks[input.name];
|
|
if (check && check.default !== undefined) {
|
|
if (input.type === 'checkbox') {
|
|
input.checked = (check.default === "True" || check.default === true);
|
|
} else {
|
|
input.value = check.default;
|
|
}
|
|
if (input.type === 'range') updateRangeDisplay(input);
|
|
validateInput(input);
|
|
}
|
|
});
|
|
|
|
applyPlatformVisibility();
|
|
applyThreadsDiscoveryVisibility();
|
|
applyTtsVisibility();
|
|
});
|
|
|
|
lucide.createIcons();
|
|
});
|
|
</script>
|
|
|
|
{% endblock %}
|