Spaces:
Sleeping
Sleeping
feat: add AI Prompt Studio tool with 11 prompt categories and clean dropdown selector
Browse files- .gitignore +1 -3
- app.py +907 -0
- fix_dynamic_video.py +64 -0
- templates/index.html +31 -1
- templates/layout.html +48 -0
- templates/tools/prompt_studio/index.html +988 -0
- templates/tools/video_ai/index.html +627 -0
- uploads/tts_edge_814db1da.mp3 +0 -0
- uploads/tts_edge_f2f4925d.mp3 +0 -0
.gitignore
CHANGED
|
@@ -1,6 +1,4 @@
|
|
| 1 |
__pycache__/
|
| 2 |
*.pyc
|
| 3 |
-
uploads/
|
| 4 |
-
uploads/*.txt
|
| 5 |
-
uploads/*.json
|
| 6 |
.vscode/
|
|
|
|
| 1 |
__pycache__/
|
| 2 |
*.pyc
|
| 3 |
+
uploads/
|
|
|
|
|
|
|
| 4 |
.vscode/
|
app.py
CHANGED
|
@@ -14,6 +14,8 @@ from reportlab.pdfgen import canvas
|
|
| 14 |
from reportlab.lib.pagesizes import letter
|
| 15 |
from werkzeug.utils import secure_filename
|
| 16 |
from rembg import remove as rembg_remove
|
|
|
|
|
|
|
| 17 |
|
| 18 |
app = Flask(__name__)
|
| 19 |
app.config['UPLOAD_FOLDER'] = 'uploads'
|
|
@@ -133,6 +135,10 @@ def index():
|
|
| 133 |
def download(filename):
|
| 134 |
return send_file(os.path.join(app.config['UPLOAD_FOLDER'], filename), as_attachment=True)
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
# ── Routes: API ───────────────────────────────────────────────
|
| 137 |
@app.route('/api/gemini-models')
|
| 138 |
def gemini_models():
|
|
@@ -647,5 +653,906 @@ def pdf_process():
|
|
| 647 |
except Exception as e:
|
| 648 |
return jsonify({'error': _err_msg(str(e))}), 500
|
| 649 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 650 |
if __name__ == '__main__':
|
| 651 |
app.run(debug=True, port=5000)
|
|
|
|
| 14 |
from reportlab.lib.pagesizes import letter
|
| 15 |
from werkzeug.utils import secure_filename
|
| 16 |
from rembg import remove as rembg_remove
|
| 17 |
+
import socket
|
| 18 |
+
socket.setdefaulttimeout(15.0)
|
| 19 |
|
| 20 |
app = Flask(__name__)
|
| 21 |
app.config['UPLOAD_FOLDER'] = 'uploads'
|
|
|
|
| 135 |
def download(filename):
|
| 136 |
return send_file(os.path.join(app.config['UPLOAD_FOLDER'], filename), as_attachment=True)
|
| 137 |
|
| 138 |
+
@app.route('/favicon.ico')
|
| 139 |
+
def favicon():
|
| 140 |
+
return '', 204
|
| 141 |
+
|
| 142 |
# ── Routes: API ───────────────────────────────────────────────
|
| 143 |
@app.route('/api/gemini-models')
|
| 144 |
def gemini_models():
|
|
|
|
| 653 |
except Exception as e:
|
| 654 |
return jsonify({'error': _err_msg(str(e))}), 500
|
| 655 |
|
| 656 |
+
# ── Routes: Video AI ──────────────────────────────────────────
|
| 657 |
+
import urllib.request
|
| 658 |
+
import urllib.parse
|
| 659 |
+
|
| 660 |
+
def get_music_file(music_type):
|
| 661 |
+
if music_type == 'none' or not music_type:
|
| 662 |
+
return None
|
| 663 |
+
|
| 664 |
+
urls = {
|
| 665 |
+
'calm': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-8.mp3',
|
| 666 |
+
'upbeat': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3',
|
| 667 |
+
'epic': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3',
|
| 668 |
+
'ambient': 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-4.mp3'
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
url = urls.get(music_type)
|
| 672 |
+
if not url:
|
| 673 |
+
return None
|
| 674 |
+
|
| 675 |
+
music_dir = os.path.join(app.config['UPLOAD_FOLDER'], 'music')
|
| 676 |
+
os.makedirs(music_dir, exist_ok=True)
|
| 677 |
+
file_path = os.path.join(music_dir, f"{music_type}.mp3")
|
| 678 |
+
|
| 679 |
+
if not os.path.exists(file_path):
|
| 680 |
+
try:
|
| 681 |
+
print(f"Downloading background music {music_type} from {url}...")
|
| 682 |
+
req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 683 |
+
with urllib.request.urlopen(req, timeout=8) as resp:
|
| 684 |
+
data = resp.read()
|
| 685 |
+
with open(file_path, 'wb') as f:
|
| 686 |
+
f.write(data)
|
| 687 |
+
print("Download completed.")
|
| 688 |
+
except Exception as e:
|
| 689 |
+
print(f"Error downloading music: {e}")
|
| 690 |
+
return None
|
| 691 |
+
|
| 692 |
+
def draw_pil_subtitles(frame, text, font_size=28):
|
| 693 |
+
from PIL import Image, ImageDraw, ImageFont
|
| 694 |
+
import numpy as np
|
| 695 |
+
|
| 696 |
+
pil_img = Image.fromarray(frame)
|
| 697 |
+
draw = ImageDraw.Draw(pil_img)
|
| 698 |
+
|
| 699 |
+
try:
|
| 700 |
+
font = ImageFont.truetype("arial.ttf", font_size)
|
| 701 |
+
except:
|
| 702 |
+
try:
|
| 703 |
+
font = ImageFont.truetype("C:\\Windows\\Fonts\\arial.ttf", font_size)
|
| 704 |
+
except:
|
| 705 |
+
font = ImageFont.load_default()
|
| 706 |
+
|
| 707 |
+
w, h = pil_img.size
|
| 708 |
+
words = text.split(' ')
|
| 709 |
+
lines = []
|
| 710 |
+
current_line = ""
|
| 711 |
+
|
| 712 |
+
for word in words:
|
| 713 |
+
test_line = current_line + " " + word if current_line else word
|
| 714 |
+
bbox = font.getbbox(test_line)
|
| 715 |
+
tw = bbox[2] - bbox[0]
|
| 716 |
+
if tw > w - 100:
|
| 717 |
+
if current_line:
|
| 718 |
+
lines.append(current_line)
|
| 719 |
+
current_line = word
|
| 720 |
+
else:
|
| 721 |
+
current_line = test_line
|
| 722 |
+
if current_line:
|
| 723 |
+
lines.append(current_line)
|
| 724 |
+
|
| 725 |
+
line_height = font.getbbox("A")[3] - font.getbbox("A")[1] + 10
|
| 726 |
+
y_start = h - 90 - (len(lines) - 1) * line_height
|
| 727 |
+
|
| 728 |
+
for i, line in enumerate(lines):
|
| 729 |
+
y = y_start + i * line_height
|
| 730 |
+
bbox = font.getbbox(line)
|
| 731 |
+
tw = bbox[2] - bbox[0]
|
| 732 |
+
x = (w - tw) // 2
|
| 733 |
+
|
| 734 |
+
outline_color = (0, 0, 0)
|
| 735 |
+
for dx, dy in [(-2,-2), (-2,2), (2,-2), (2,2), (-2,0), (2,0), (0,-2), (0,2)]:
|
| 736 |
+
draw.text((x + dx, y + dy), line, font=font, fill=outline_color)
|
| 737 |
+
|
| 738 |
+
draw.text((x, y), line, font=font, fill=(255, 255, 255))
|
| 739 |
+
|
| 740 |
+
return np.array(pil_img)
|
| 741 |
+
|
| 742 |
+
def apply_zoom_effect(clip, zoom_ratio=0.07, mode='in'):
|
| 743 |
+
import cv2
|
| 744 |
+
def effect(get_frame, t):
|
| 745 |
+
frame = get_frame(t)
|
| 746 |
+
h, w = frame.shape[:2]
|
| 747 |
+
|
| 748 |
+
if mode == 'in':
|
| 749 |
+
current_zoom = 1.0 + zoom_ratio * (t / clip.duration)
|
| 750 |
+
else:
|
| 751 |
+
current_zoom = 1.0 + zoom_ratio * (1.0 - t / clip.duration)
|
| 752 |
+
|
| 753 |
+
new_w = int(w * current_zoom)
|
| 754 |
+
new_h = int(h * current_zoom)
|
| 755 |
+
|
| 756 |
+
resized_frame = cv2.resize(frame, (new_w, new_h))
|
| 757 |
+
|
| 758 |
+
dx = (new_w - w) // 2
|
| 759 |
+
dy = (new_h - h) // 2
|
| 760 |
+
cropped = resized_frame[dy:dy+h, dx:dx+w]
|
| 761 |
+
return cropped
|
| 762 |
+
|
| 763 |
+
return clip.transform(effect)
|
| 764 |
+
|
| 765 |
+
def apply_subtitles_effect(clip, subtitle_text):
|
| 766 |
+
def effect(get_frame, t):
|
| 767 |
+
frame = get_frame(t)
|
| 768 |
+
frame_copy = frame.copy()
|
| 769 |
+
return draw_pil_subtitles(frame_copy, subtitle_text)
|
| 770 |
+
return clip.transform(effect)
|
| 771 |
+
|
| 772 |
+
def generate_gradient_background(w, h, text):
|
| 773 |
+
import numpy as np
|
| 774 |
+
import cv2
|
| 775 |
+
img = np.zeros((h, w, 3), dtype=np.uint8)
|
| 776 |
+
for y in range(h):
|
| 777 |
+
r = int(30 + (50 - 30) * (y / h))
|
| 778 |
+
g = int(20 + (30 - 20) * (y / h))
|
| 779 |
+
b = int(50 + (100 - 50) * (y / h))
|
| 780 |
+
img[y, :] = [b, g, r]
|
| 781 |
+
|
| 782 |
+
font = cv2.FONT_HERSHEY_SIMPLEX
|
| 783 |
+
font_scale = 1.0
|
| 784 |
+
thickness = 2
|
| 785 |
+
(text_w, text_h), _ = cv2.getTextSize(text, font, font_scale, thickness)
|
| 786 |
+
x = (w - text_w) // 2
|
| 787 |
+
y = (h + text_h) // 2
|
| 788 |
+
|
| 789 |
+
cv2.putText(img, text, (x+2, y+2), font, font_scale, (0, 0, 0), thickness+2, cv2.LINE_AA)
|
| 790 |
+
cv2.putText(img, text, (x, y), font, font_scale, (255, 255, 255), thickness, cv2.LINE_AA)
|
| 791 |
+
return img
|
| 792 |
+
|
| 793 |
+
def generate_hf_video(image_path, prompt, hf_token=None):
|
| 794 |
+
import socket
|
| 795 |
+
# Patch socket for httpx/gradio getaddrinfo issue
|
| 796 |
+
orig_getaddrinfo = socket.getaddrinfo
|
| 797 |
+
socket.getaddrinfo = lambda host, port, family=0, type=0, proto=0, flags=0: orig_getaddrinfo(host, port, socket.AF_INET, type, proto, flags)
|
| 798 |
+
|
| 799 |
+
# Patch click.Choice class for Python 3.13 typing issue
|
| 800 |
+
import click
|
| 801 |
+
if not hasattr(click.Choice, '__class_getitem__'):
|
| 802 |
+
click.Choice.__class_getitem__ = classmethod(lambda cls, item: cls)
|
| 803 |
+
|
| 804 |
+
from gradio_client import Client, handle_file
|
| 805 |
+
import shutil
|
| 806 |
+
|
| 807 |
+
try:
|
| 808 |
+
print(f"Connecting to zerogpu-aoti/wan2-2-fp8da-aoti-faster (HF Token provided: {bool(hf_token)})...")
|
| 809 |
+
client = Client('zerogpu-aoti/wan2-2-fp8da-aoti-faster', token=hf_token if hf_token else None)
|
| 810 |
+
print(f"Running video generation for prompt: {prompt}...")
|
| 811 |
+
|
| 812 |
+
res = client.predict(
|
| 813 |
+
handle_file(os.path.abspath(image_path)),
|
| 814 |
+
prompt,
|
| 815 |
+
6, # steps
|
| 816 |
+
"static, ugly, watermark, worst quality", # negative prompt
|
| 817 |
+
3.5, # duration_seconds
|
| 818 |
+
1.0, # guidance_scale
|
| 819 |
+
1.0, # guidance_scale_2
|
| 820 |
+
42, # seed
|
| 821 |
+
True, # randomize_seed
|
| 822 |
+
api_name="/generate_video"
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
# res is a tuple: (temp_mp4_path, seed)
|
| 826 |
+
temp_mp4 = res[0]
|
| 827 |
+
|
| 828 |
+
out_filename = f"hf_video_{os.urandom(4).hex()}.mp4"
|
| 829 |
+
out_path = os.path.join(app.config['UPLOAD_FOLDER'], out_filename)
|
| 830 |
+
shutil.copy(temp_mp4, out_path)
|
| 831 |
+
print(f"HF Video generated successfully: {out_path}")
|
| 832 |
+
return out_path
|
| 833 |
+
except Exception as e:
|
| 834 |
+
print(f"Error in generate_hf_video: {e}")
|
| 835 |
+
return None
|
| 836 |
+
|
| 837 |
+
REPLICATE_API_TOKEN = os.environ.get("REPLICATE_API_TOKEN", "r8_BTj77jONwojDdNOUUJw6TzA4E1gzajI2KKSJJ") # Đọc từ biến môi trường
|
| 838 |
+
HF_API_TOKEN = os.environ.get("HF_API_TOKEN", "") # Đọc từ biến môi trường, tránh lộ token trên Git
|
| 839 |
+
|
| 840 |
+
@app.route('/video-ai')
|
| 841 |
+
def video_ai_index():
|
| 842 |
+
return render_template('tools/video_ai/index.html')
|
| 843 |
+
|
| 844 |
+
@app.route('/api/video-ai/models')
|
| 845 |
+
def video_ai_models():
|
| 846 |
+
return jsonify([
|
| 847 |
+
{"id": "hf-wan2.2-i2v-free", "name": "Wan 2.2 I2V (Free - Không cần Key)", "desc": "Tạo video AI chuyển động thật, hoàn toàn miễn phí"},
|
| 848 |
+
{"id": "wavespeedai/wan-2.1-t2v-480p", "name": "Wan 2.1 T2V 480p (Replicate)", "desc": "Text → Video, 480p, nhanh"},
|
| 849 |
+
{"id": "wavespeedai/wan-2.1-t2v-720p", "name": "Wan 2.1 T2V 720p (Replicate)", "desc": "Text → Video, 720p, sắc nét"},
|
| 850 |
+
{"id": "minimax/video-01", "name": "MiniMax Video-01 (Replicate)", "desc": "Text → Video, ngắn, nhanh"},
|
| 851 |
+
{"id": "stability-ai/stable-video-diffusion", "name": "Stable Video Diffusion (Replicate)", "desc": "Image → Video"},
|
| 852 |
+
])
|
| 853 |
+
|
| 854 |
+
@app.route('/api/video-ai/generate', methods=['POST'])
|
| 855 |
+
def api_video_ai_generate():
|
| 856 |
+
import asyncio, edge_tts, urllib.request, urllib.parse
|
| 857 |
+
|
| 858 |
+
prompt = request.form.get('prompt', '').strip()
|
| 859 |
+
gemini_model = request.form.get('gemini_model', DEFAULT_MODEL)
|
| 860 |
+
lang = request.form.get('lang', 'vi')
|
| 861 |
+
voice = request.form.get('voice', 'vi-VN-HoaiMyNeural')
|
| 862 |
+
style = request.form.get('style', 'cinematic')
|
| 863 |
+
duration = int(request.form.get('duration', '5'))
|
| 864 |
+
music = request.form.get('music', 'none')
|
| 865 |
+
ratio = request.form.get('ratio', '16:9')
|
| 866 |
+
replicate_model = request.form.get('replicate_model', 'wavespeedai/wan-2.1-t2v-480p')
|
| 867 |
+
api_token = request.form.get('api_token', REPLICATE_API_TOKEN).strip()
|
| 868 |
+
hf_token = request.form.get('hf_token', '').strip()
|
| 869 |
+
if not hf_token:
|
| 870 |
+
hf_token = HF_API_TOKEN
|
| 871 |
+
narrate = request.form.get('narrate', 'true') == 'true'
|
| 872 |
+
|
| 873 |
+
if not prompt:
|
| 874 |
+
return jsonify({'error': 'Vui lòng nhập chủ đề video'}), 400
|
| 875 |
+
|
| 876 |
+
result = {}
|
| 877 |
+
|
| 878 |
+
try:
|
| 879 |
+
# 1. Gemini viết kịch bản + enhance prompt
|
| 880 |
+
if api_token:
|
| 881 |
+
script_prompt = (
|
| 882 |
+
f"Viết kịch bản dẫn truyện bằng ngôn ngữ có mã '{lang}' cho video {duration} giây "
|
| 883 |
+
f"về chủ đề: '{prompt}'. Ngắn gọn, súc tích vừa đủ {duration} giây đọc. "
|
| 884 |
+
f"Chỉ trả về nội dung dẫn truyện."
|
| 885 |
+
)
|
| 886 |
+
script_resp = client.models.generate_content(model=gemini_model, contents=script_prompt)
|
| 887 |
+
script_text = script_resp.text.strip()
|
| 888 |
+
|
| 889 |
+
video_prompt_req = (
|
| 890 |
+
f"Dịch và mở rộng prompt sau sang tiếng Anh để tạo video AI chất lượng cao: '{prompt}'. "
|
| 891 |
+
f"Thêm các chi tiết về ánh sáng, góc quay, phong cách '{style}', màu sắc. "
|
| 892 |
+
f"Chỉ trả về prompt tiếng Anh, không giải thích."
|
| 893 |
+
)
|
| 894 |
+
vp_resp = client.models.generate_content(model=gemini_model, contents=video_prompt_req)
|
| 895 |
+
video_prompt_en = vp_resp.text.strip()
|
| 896 |
+
increment_usage(gemini_model)
|
| 897 |
+
|
| 898 |
+
result['script'] = script_text
|
| 899 |
+
result['video_prompt'] = video_prompt_en
|
| 900 |
+
|
| 901 |
+
# 2. Tạo giọng đọc (Edge TTS)
|
| 902 |
+
if narrate:
|
| 903 |
+
narration_file = f"narration_{os.urandom(4).hex()}.mp3"
|
| 904 |
+
narration_path = os.path.join(app.config['UPLOAD_FOLDER'], narration_file)
|
| 905 |
+
asyncio.run(edge_tts.Communicate(script_text, voice).save(narration_path))
|
| 906 |
+
result['audio_url'] = f'/download/{narration_file}'
|
| 907 |
+
|
| 908 |
+
import replicate
|
| 909 |
+
os.environ['REPLICATE_API_TOKEN'] = api_token
|
| 910 |
+
|
| 911 |
+
ratio_map = {'16:9': '1280x720', '9:16': '720x1280', '1:1': '720x720'}
|
| 912 |
+
resolution = ratio_map.get(ratio, '1280x720')
|
| 913 |
+
|
| 914 |
+
style_suffix = {
|
| 915 |
+
'cinematic': 'cinematic lighting, film grain, professional camera',
|
| 916 |
+
'anime': 'anime style, vibrant colors, Studio Ghibli',
|
| 917 |
+
'realistic': 'photorealistic, 8K, HDR',
|
| 918 |
+
'tech': 'cyberpunk, neon lights, futuristic',
|
| 919 |
+
'nature': 'nature documentary style, golden hour',
|
| 920 |
+
'space': 'space, cosmic, nebula, stars',
|
| 921 |
+
'watercolor': 'watercolor painting style, soft brush strokes',
|
| 922 |
+
'vintage': 'vintage film, retro aesthetic, 1970s'
|
| 923 |
+
}.get(style, '')
|
| 924 |
+
|
| 925 |
+
full_prompt = f"{video_prompt_en}. {style_suffix}"
|
| 926 |
+
|
| 927 |
+
output = replicate.run(
|
| 928 |
+
replicate_model,
|
| 929 |
+
input={
|
| 930 |
+
"prompt": full_prompt,
|
| 931 |
+
"num_frames": duration * 8,
|
| 932 |
+
"duration": duration,
|
| 933 |
+
}
|
| 934 |
+
)
|
| 935 |
+
video_url = str(output) if isinstance(output, str) else (output[0] if isinstance(output, list) else str(output))
|
| 936 |
+
|
| 937 |
+
# Download và lưu local
|
| 938 |
+
import urllib.request
|
| 939 |
+
video_file = f"video_{os.urandom(4).hex()}.mp4"
|
| 940 |
+
video_path = os.path.join(app.config['UPLOAD_FOLDER'], video_file)
|
| 941 |
+
urllib.request.urlretrieve(video_url, video_path)
|
| 942 |
+
result['video_url'] = f'/download/{video_file}'
|
| 943 |
+
|
| 944 |
+
else:
|
| 945 |
+
# FREE MOVIEPY & POLLINATIONS FLOW
|
| 946 |
+
# Determine number of scenes
|
| 947 |
+
num_scenes = 1
|
| 948 |
+
if duration >= 15:
|
| 949 |
+
num_scenes = 3
|
| 950 |
+
elif duration >= 10:
|
| 951 |
+
num_scenes = 2
|
| 952 |
+
|
| 953 |
+
# Try to query JSON structured script
|
| 954 |
+
script_prompt = (
|
| 955 |
+
f"You are an AI video producer. Create a detailed, structured video script about the topic: '{prompt}'. "
|
| 956 |
+
f"The video has a total target duration of {duration} seconds, so divide it into exactly {num_scenes} scenes.\n\n"
|
| 957 |
+
f"You MUST respond ONLY with a valid JSON array of objects, containing exactly {num_scenes} objects. "
|
| 958 |
+
f"Do not write any markdown code blocks, backticks, or text before/after the JSON.\n\n"
|
| 959 |
+
f"Each scene object must contain:\n"
|
| 960 |
+
f"- 'narration': A short spoken narration text in the language '{lang}'. Ensure the narration text is short and suitable for the scene duration (about {duration / num_scenes} seconds of speech).\n"
|
| 961 |
+
f"- 'visual_prompt': An English description of the scene's visual content, formatted as a detailed image generation prompt with style '{style}', lighting, colors, and camera angle.\n\n"
|
| 962 |
+
f"Example format:\n"
|
| 963 |
+
f"[\n"
|
| 964 |
+
f" {{\n"
|
| 965 |
+
f" \"narration\": \"Lời dẫn phân cảnh 1...\",\n"
|
| 966 |
+
f" \"visual_prompt\": \"A close-up shot of ...\"\n"
|
| 967 |
+
f" }}\n"
|
| 968 |
+
f"]"
|
| 969 |
+
)
|
| 970 |
+
|
| 971 |
+
scenes = []
|
| 972 |
+
try:
|
| 973 |
+
response = client.models.generate_content(
|
| 974 |
+
model=gemini_model,
|
| 975 |
+
contents=script_prompt,
|
| 976 |
+
config=types.GenerateContentConfig(
|
| 977 |
+
response_mime_type="application/json"
|
| 978 |
+
)
|
| 979 |
+
)
|
| 980 |
+
increment_usage(gemini_model)
|
| 981 |
+
text_resp = response.text.strip()
|
| 982 |
+
if text_resp.startswith("```"):
|
| 983 |
+
if text_resp.startswith("```json"):
|
| 984 |
+
text_resp = text_resp[7:]
|
| 985 |
+
else:
|
| 986 |
+
text_resp = text_resp[3:]
|
| 987 |
+
if text_resp.endswith("```"):
|
| 988 |
+
text_resp = text_resp[:-3]
|
| 989 |
+
text_resp = text_resp.strip()
|
| 990 |
+
scenes = json.loads(text_resp)
|
| 991 |
+
except Exception as e:
|
| 992 |
+
print(f"Gemini JSON generation failed, trying fallback standard prompt: {e}")
|
| 993 |
+
try:
|
| 994 |
+
fallback_prompt = (
|
| 995 |
+
f"Viết kịch bản dẫn truyện bằng ngôn ngữ có mã '{lang}' cho video {duration} giây "
|
| 996 |
+
f"về chủ đề: '{prompt}'. Ngắn gọn, súc tích vừa đủ {duration} giây đọc. "
|
| 997 |
+
f"Chỉ trả về nội dung dẫn truyện."
|
| 998 |
+
)
|
| 999 |
+
fallback_resp = client.models.generate_content(model=gemini_model, contents=fallback_prompt)
|
| 1000 |
+
increment_usage(gemini_model)
|
| 1001 |
+
script_text = fallback_resp.text.strip()
|
| 1002 |
+
|
| 1003 |
+
scenes = [{
|
| 1004 |
+
"narration": script_text,
|
| 1005 |
+
"visual_prompt": prompt
|
| 1006 |
+
}]
|
| 1007 |
+
except Exception as e2:
|
| 1008 |
+
print(f"Gemini fallback prompt also failed: {e2}. Using local fallback.")
|
| 1009 |
+
scenes = [{
|
| 1010 |
+
"narration": f"Video giới thiệu về: {prompt}.",
|
| 1011 |
+
"visual_prompt": prompt
|
| 1012 |
+
}]
|
| 1013 |
+
|
| 1014 |
+
# Map style to style suffix
|
| 1015 |
+
style_suffixes = {
|
| 1016 |
+
'cinematic': 'cinematic lighting, film grain, professional camera, 8k resolution, cinematic composition',
|
| 1017 |
+
'anime': 'anime style, vibrant colors, detailed, Studio Ghibli aesthetic, hand-drawn vector art',
|
| 1018 |
+
'realistic': 'photorealistic, highly detailed, 8k resolution, realistic textures, natural lighting',
|
| 1019 |
+
'nature': 'nature photography, beautiful landscape, golden hour lighting, national geographic style',
|
| 1020 |
+
'tech': 'cyberpunk aesthetic, neon lights, futuristic technology, detailed sci-fi concept art',
|
| 1021 |
+
'space': 'space, cosmic, nebula, stars, sci-fi scene, highly detailed',
|
| 1022 |
+
'watercolor': 'soft watercolor painting, artistic brush strokes, textured paper, pastel colors',
|
| 1023 |
+
'vintage': 'vintage film, retro color grading, 1970s look, scratches, analog film photography'
|
| 1024 |
+
}
|
| 1025 |
+
suffix = style_suffixes.get(style, '')
|
| 1026 |
+
|
| 1027 |
+
# Map ratio to resolution
|
| 1028 |
+
ratio_resolutions = {
|
| 1029 |
+
'16:9': (1024, 576),
|
| 1030 |
+
'9:16': (576, 1024),
|
| 1031 |
+
'1:1': (768, 768),
|
| 1032 |
+
'4:3': (800, 600)
|
| 1033 |
+
}
|
| 1034 |
+
w, h = ratio_resolutions.get(ratio, (1024, 576))
|
| 1035 |
+
|
| 1036 |
+
from moviepy import VideoClip, ImageClip, AudioFileClip, VideoFileClip, concatenate_videoclips, CompositeAudioClip, concatenate_audioclips
|
| 1037 |
+
clips = []
|
| 1038 |
+
full_narration_audio_paths = []
|
| 1039 |
+
combined_script = []
|
| 1040 |
+
combined_visual_prompts = []
|
| 1041 |
+
|
| 1042 |
+
for i, scene in enumerate(scenes):
|
| 1043 |
+
narration_text = scene.get('narration', '').strip()
|
| 1044 |
+
visual_prompt = scene.get('visual_prompt', prompt).strip()
|
| 1045 |
+
combined_script.append(narration_text)
|
| 1046 |
+
combined_visual_prompts.append(visual_prompt)
|
| 1047 |
+
|
| 1048 |
+
scene_duration = duration / len(scenes)
|
| 1049 |
+
audio_file = None
|
| 1050 |
+
|
| 1051 |
+
if narrate and narration_text:
|
| 1052 |
+
audio_filename = f"scene_{i}_{os.urandom(4).hex()}.mp3"
|
| 1053 |
+
audio_path = os.path.join(app.config['UPLOAD_FOLDER'], audio_filename)
|
| 1054 |
+
try:
|
| 1055 |
+
asyncio.run(edge_tts.Communicate(narration_text, voice).save(audio_path))
|
| 1056 |
+
audio_file = audio_path
|
| 1057 |
+
full_narration_audio_paths.append(audio_path)
|
| 1058 |
+
except Exception as tts_err:
|
| 1059 |
+
print(f"TTS error for scene {i}: {tts_err}")
|
| 1060 |
+
audio_file = None
|
| 1061 |
+
|
| 1062 |
+
audio_clip = None
|
| 1063 |
+
if audio_file:
|
| 1064 |
+
try:
|
| 1065 |
+
audio_clip = AudioFileClip(audio_file)
|
| 1066 |
+
scene_duration = audio_clip.duration
|
| 1067 |
+
if scene_duration < 1.0:
|
| 1068 |
+
scene_duration = 3.0
|
| 1069 |
+
except Exception as aud_err:
|
| 1070 |
+
print(f"AudioFileClip error: {aud_err}")
|
| 1071 |
+
scene_duration = 4.0
|
| 1072 |
+
else:
|
| 1073 |
+
scene_duration = duration / len(scenes)
|
| 1074 |
+
|
| 1075 |
+
# Download image
|
| 1076 |
+
image_filename = f"scene_{i}_{os.urandom(4).hex()}.jpg"
|
| 1077 |
+
image_path = os.path.join(app.config['UPLOAD_FOLDER'], image_filename)
|
| 1078 |
+
scene_prompt = f"{visual_prompt}. {suffix}"
|
| 1079 |
+
encoded_prompt = urllib.parse.quote(scene_prompt)
|
| 1080 |
+
img_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width={w}&height={h}&nologo=true&private=true"
|
| 1081 |
+
|
| 1082 |
+
# Retry image download up to 3 times to prevent transient failures
|
| 1083 |
+
img_downloaded = False
|
| 1084 |
+
import time
|
| 1085 |
+
for attempt in range(3):
|
| 1086 |
+
try:
|
| 1087 |
+
req = urllib.request.Request(img_url, headers={'User-Agent': 'Mozilla/5.0'})
|
| 1088 |
+
with urllib.request.urlopen(req, timeout=15) as img_resp:
|
| 1089 |
+
img_data = img_resp.read()
|
| 1090 |
+
with open(image_path, 'wb') as img_f:
|
| 1091 |
+
img_f.write(img_data)
|
| 1092 |
+
img_downloaded = True
|
| 1093 |
+
print(f"Successfully downloaded scene {i+1} image on attempt {attempt+1}")
|
| 1094 |
+
break
|
| 1095 |
+
except Exception as img_err:
|
| 1096 |
+
print(f"Attempt {attempt+1} failed to download image for scene {i+1}: {img_err}")
|
| 1097 |
+
time.sleep(1.5)
|
| 1098 |
+
|
| 1099 |
+
if not img_downloaded:
|
| 1100 |
+
# Fallback to beautiful colorful gradient background instead of flat black image
|
| 1101 |
+
print(f"All image download attempts failed. Using gradient fallback for scene {i+1}")
|
| 1102 |
+
gradient_img = generate_gradient_background(w, h, f"Scene {i+1}")
|
| 1103 |
+
import cv2
|
| 1104 |
+
cv2.imwrite(image_path, gradient_img)
|
| 1105 |
+
|
| 1106 |
+
try:
|
| 1107 |
+
img_clip = None
|
| 1108 |
+
if replicate_model == 'hf-wan2.2-i2v-free':
|
| 1109 |
+
try:
|
| 1110 |
+
# Generate real AI video via Gradio (forwarding hf_token if present)
|
| 1111 |
+
hf_video_file = generate_hf_video(image_path, f"{visual_prompt}. {suffix}", hf_token)
|
| 1112 |
+
if hf_video_file and os.path.exists(hf_video_file):
|
| 1113 |
+
img_clip = VideoFileClip(hf_video_file).with_duration(scene_duration)
|
| 1114 |
+
print("Successfully generated and loaded real HF AI video clip!")
|
| 1115 |
+
except Exception as hf_err:
|
| 1116 |
+
print(f"HuggingFace Space generation failed, falling back to slideshow: {hf_err}")
|
| 1117 |
+
img_clip = None
|
| 1118 |
+
|
| 1119 |
+
# If not using hf-wan2.2-i2v-free OR if HF generation failed, fall back to slideshow
|
| 1120 |
+
if img_clip is None:
|
| 1121 |
+
# Read image using OpenCV and convert BGR to RGB
|
| 1122 |
+
import cv2
|
| 1123 |
+
img = cv2.imread(image_path)
|
| 1124 |
+
if img is None:
|
| 1125 |
+
import numpy as np
|
| 1126 |
+
img = np.zeros((h, w, 3), dtype=np.uint8)
|
| 1127 |
+
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
| 1128 |
+
|
| 1129 |
+
# Create VideoClip instead of ImageClip to bypass caching and allow dynamic zoom
|
| 1130 |
+
img_clip = VideoClip(lambda t: img_rgb).with_duration(scene_duration)
|
| 1131 |
+
|
| 1132 |
+
# Apply Ken Burns zoom effect (alternate zoom in/out per scene)
|
| 1133 |
+
zoom_mode = 'in' if i % 2 == 0 else 'out'
|
| 1134 |
+
img_clip = apply_zoom_effect(img_clip, zoom_ratio=0.07, mode=zoom_mode)
|
| 1135 |
+
|
| 1136 |
+
# Apply burn-in subtitles (Pillow-based, unicode-compatible)
|
| 1137 |
+
if narrate and narration_text:
|
| 1138 |
+
img_clip = apply_subtitles_effect(img_clip, narration_text)
|
| 1139 |
+
|
| 1140 |
+
if audio_clip:
|
| 1141 |
+
img_clip = img_clip.with_audio(audio_clip)
|
| 1142 |
+
clips.append(img_clip)
|
| 1143 |
+
except Exception as clip_err:
|
| 1144 |
+
print(f"Error creating VideoClip: {clip_err}")
|
| 1145 |
+
|
| 1146 |
+
if not clips:
|
| 1147 |
+
raise Exception("Không thể tạo các phân cảnh video.")
|
| 1148 |
+
|
| 1149 |
+
video_clip = concatenate_videoclips(clips, method="compose")
|
| 1150 |
+
|
| 1151 |
+
# Mix music
|
| 1152 |
+
if music != 'none':
|
| 1153 |
+
music_path = get_music_file(music)
|
| 1154 |
+
if music_path:
|
| 1155 |
+
try:
|
| 1156 |
+
music_clip = AudioFileClip(music_path).with_duration(video_clip.duration)
|
| 1157 |
+
vol = 0.12 if narrate else 0.25
|
| 1158 |
+
music_clip = music_clip.multiply_volume(vol)
|
| 1159 |
+
|
| 1160 |
+
if video_clip.audio:
|
| 1161 |
+
combined_audio = CompositeAudioClip([video_clip.audio, music_clip])
|
| 1162 |
+
video_clip = video_clip.with_audio(combined_audio)
|
| 1163 |
+
else:
|
| 1164 |
+
video_clip = video_clip.with_audio(music_clip)
|
| 1165 |
+
except Exception as mus_err:
|
| 1166 |
+
print(f"Music mix error: {mus_err}")
|
| 1167 |
+
|
| 1168 |
+
out_video_file = f"video_{os.urandom(4).hex()}.mp4"
|
| 1169 |
+
out_video_path = os.path.join(app.config['UPLOAD_FOLDER'], out_video_file)
|
| 1170 |
+
|
| 1171 |
+
video_clip.write_videofile(
|
| 1172 |
+
out_video_path,
|
| 1173 |
+
fps=24,
|
| 1174 |
+
codec="libx264",
|
| 1175 |
+
audio_codec="aac"
|
| 1176 |
+
)
|
| 1177 |
+
|
| 1178 |
+
# Close clips
|
| 1179 |
+
video_clip.close()
|
| 1180 |
+
for c in clips:
|
| 1181 |
+
c.close()
|
| 1182 |
+
|
| 1183 |
+
# Merge audio narration for download button
|
| 1184 |
+
final_audio_url = None
|
| 1185 |
+
if narrate and full_narration_audio_paths:
|
| 1186 |
+
try:
|
| 1187 |
+
audio_clips = [AudioFileClip(p) for p in full_narration_audio_paths]
|
| 1188 |
+
combined_audio_clip = concatenate_audioclips(audio_clips)
|
| 1189 |
+
out_audio_file = f"narration_{os.urandom(4).hex()}.mp3"
|
| 1190 |
+
out_audio_path = os.path.join(app.config['UPLOAD_FOLDER'], out_audio_file)
|
| 1191 |
+
combined_audio_clip.write_audiofile(out_audio_path)
|
| 1192 |
+
combined_audio_clip.close()
|
| 1193 |
+
for ac in audio_clips:
|
| 1194 |
+
ac.close()
|
| 1195 |
+
final_audio_url = f'/download/{out_audio_file}'
|
| 1196 |
+
except Exception as concat_aud_err:
|
| 1197 |
+
print(f"Error concatenating narration audio: {concat_aud_err}")
|
| 1198 |
+
if full_narration_audio_paths:
|
| 1199 |
+
final_audio_url = f'/download/{os.path.basename(full_narration_audio_paths[0])}'
|
| 1200 |
+
|
| 1201 |
+
result['script'] = "\n\n".join(combined_script)
|
| 1202 |
+
result['video_prompt'] = "\n\n".join(combined_visual_prompts)
|
| 1203 |
+
result['video_url'] = f'/download/{out_video_file}'
|
| 1204 |
+
if narrate:
|
| 1205 |
+
result['audio_url'] = final_audio_url
|
| 1206 |
+
|
| 1207 |
+
return jsonify(result)
|
| 1208 |
+
|
| 1209 |
+
except Exception as e:
|
| 1210 |
+
return jsonify({'error': str(e)}), 500
|
| 1211 |
+
|
| 1212 |
+
# ── Routes: Prompt Generator ──────────────────────────────────
|
| 1213 |
+
@app.route('/prompt-generator')
|
| 1214 |
+
def prompt_generator():
|
| 1215 |
+
return redirect(url_for('prompt_generator_code'))
|
| 1216 |
+
|
| 1217 |
+
@app.route('/prompt-generator/code')
|
| 1218 |
+
def prompt_generator_code():
|
| 1219 |
+
return render_template('tools/prompt_studio/index.html', active_tab='code')
|
| 1220 |
+
|
| 1221 |
+
@app.route('/prompt-generator/video')
|
| 1222 |
+
def prompt_generator_video():
|
| 1223 |
+
return render_template('tools/prompt_studio/index.html', active_tab='video')
|
| 1224 |
+
|
| 1225 |
+
@app.route('/prompt-generator/music')
|
| 1226 |
+
def prompt_generator_music():
|
| 1227 |
+
return render_template('tools/prompt_studio/index.html', active_tab='music')
|
| 1228 |
+
|
| 1229 |
+
@app.route('/prompt-generator/story')
|
| 1230 |
+
def prompt_generator_story():
|
| 1231 |
+
return render_template('tools/prompt_studio/index.html', active_tab='story')
|
| 1232 |
+
|
| 1233 |
+
@app.route('/prompt-generator/video-script')
|
| 1234 |
+
def prompt_generator_video_script():
|
| 1235 |
+
return render_template('tools/prompt_studio/index.html', active_tab='video_script')
|
| 1236 |
+
|
| 1237 |
+
@app.route('/prompt-generator/marketing')
|
| 1238 |
+
def prompt_generator_marketing():
|
| 1239 |
+
return render_template('tools/prompt_studio/index.html', active_tab='marketing')
|
| 1240 |
+
|
| 1241 |
+
@app.route('/prompt-generator/education')
|
| 1242 |
+
def prompt_generator_education():
|
| 1243 |
+
return render_template('tools/prompt_studio/index.html', active_tab='education')
|
| 1244 |
+
|
| 1245 |
+
@app.route('/prompt-generator/data')
|
| 1246 |
+
def prompt_generator_data():
|
| 1247 |
+
return render_template('tools/prompt_studio/index.html', active_tab='data')
|
| 1248 |
+
|
| 1249 |
+
@app.route('/prompt-generator/office')
|
| 1250 |
+
def prompt_generator_office():
|
| 1251 |
+
return render_template('tools/prompt_studio/index.html', active_tab='office')
|
| 1252 |
+
|
| 1253 |
+
@app.route('/prompt-generator/brainstorm')
|
| 1254 |
+
def prompt_generator_brainstorm():
|
| 1255 |
+
return render_template('tools/prompt_studio/index.html', active_tab='brainstorm')
|
| 1256 |
+
|
| 1257 |
+
@app.route('/prompt-generator/enhancer')
|
| 1258 |
+
def prompt_generator_enhancer():
|
| 1259 |
+
return render_template('tools/prompt_studio/index.html', active_tab='enhancer')
|
| 1260 |
+
|
| 1261 |
+
@app.route('/api/prompt-generator/generate', methods=['POST'])
|
| 1262 |
+
def api_prompt_generate():
|
| 1263 |
+
data = request.json or {}
|
| 1264 |
+
category = data.get('category', 'general')
|
| 1265 |
+
model_id = data.get('gemini_model', DEFAULT_MODEL)
|
| 1266 |
+
|
| 1267 |
+
if category == 'code':
|
| 1268 |
+
lang = data.get('code_lang', 'Python')
|
| 1269 |
+
task = data.get('code_task', '')
|
| 1270 |
+
framework = data.get('code_framework', '')
|
| 1271 |
+
style = data.get('code_style', 'Clean Code, documented, PEP8')
|
| 1272 |
+
requirements = data.get('code_requirements', '')
|
| 1273 |
+
|
| 1274 |
+
system_prompt = (
|
| 1275 |
+
"You are an expert Prompt Engineer for AI Coding Assistants (like Gemini, Claude, or ChatGPT).\n"
|
| 1276 |
+
"Your task is to write a highly detailed, professional prompt in Vietnamese that the user can copy and paste to get the exact code they want.\n"
|
| 1277 |
+
"The generated prompt MUST guide the AI coder to write high-quality code and should include:\n"
|
| 1278 |
+
"- A clear role definition.\n"
|
| 1279 |
+
"- The specific coding task and language/framework to be used.\n"
|
| 1280 |
+
"- Code quality and style guidelines (e.g., Clean Code, DRY, SOLID, error handling).\n"
|
| 1281 |
+
"- Desired output structure (e.g., directory structure, code files, explanations, tests).\n"
|
| 1282 |
+
"- Constraints and edge cases to handle.\n\n"
|
| 1283 |
+
"Input details provided by user:\n"
|
| 1284 |
+
f"- Ngôn ngữ lập trình: {lang}\n"
|
| 1285 |
+
f"- Công việc/Tính năng cần viết: {task}\n"
|
| 1286 |
+
f"- Thư viện/Framework: {framework}\n"
|
| 1287 |
+
f"- Phong cách/Quy chuẩn: {style}\n"
|
| 1288 |
+
f"- Yêu cầu bổ sung: {requirements}\n\n"
|
| 1289 |
+
"Generate ONLY the optimized prompt itself in a clean Markdown format. Do not include any introductory or concluding text. Do not wrap the output in a markdown block of the prompt itself (just output the raw prompt text which is formatted as Markdown)."
|
| 1290 |
+
)
|
| 1291 |
+
elif category == 'video':
|
| 1292 |
+
topic = data.get('video_topic', '')
|
| 1293 |
+
style = data.get('video_style', 'Cinematic')
|
| 1294 |
+
shot = data.get('video_shot', 'Medium Shot')
|
| 1295 |
+
lighting = data.get('video_lighting', 'Natural Light')
|
| 1296 |
+
aspect_ratio = data.get('video_ratio', '16:9')
|
| 1297 |
+
details = data.get('video_details', '')
|
| 1298 |
+
|
| 1299 |
+
system_prompt = (
|
| 1300 |
+
"You are an expert Prompt Engineer for AI Video and Image generation tools (like Midjourney, Stable Diffusion, Runway Gen-2, Sora, Kling, Luma Dream Machine).\n"
|
| 1301 |
+
"Your task is to write a highly detailed, professional English prompt (since image/video AIs work best with English) based on the user's description. The output itself should be written in English, but you can include brief explanations in Vietnamese if necessary. However, the core prompt MUST be in English.\n"
|
| 1302 |
+
"Structure the generated prompt for high quality:\n"
|
| 1303 |
+
"- Core subject description.\n"
|
| 1304 |
+
"- Visual style, art direction, and details.\n"
|
| 1305 |
+
"- Camera settings, lens type, shot type, and camera movement.\n"
|
| 1306 |
+
"- Lighting, color grading, and atmosphere.\n"
|
| 1307 |
+
"- Negative prompts or aspect ratio parameters if suitable.\n\n"
|
| 1308 |
+
"Input details provided by user:\n"
|
| 1309 |
+
f"- Topic/Concept: {topic}\n"
|
| 1310 |
+
f"- Style: {style}\n"
|
| 1311 |
+
f"- Camera Angle/Shot: {shot}\n"
|
| 1312 |
+
f"- Lighting: {lighting}\n"
|
| 1313 |
+
f"- Aspect Ratio: {aspect_ratio}\n"
|
| 1314 |
+
f"- Extra Details: {details}\n\n"
|
| 1315 |
+
"Generate ONLY the optimized prompt in a clean copyable format. Do not include any introductory or concluding text."
|
| 1316 |
+
)
|
| 1317 |
+
elif category == 'music':
|
| 1318 |
+
theme = data.get('music_theme', '')
|
| 1319 |
+
genre = data.get('music_genre', 'Pop')
|
| 1320 |
+
mood = data.get('music_mood', 'Energetic')
|
| 1321 |
+
instruments = data.get('music_instruments', '')
|
| 1322 |
+
vocals = data.get('music_vocals', 'Vocals')
|
| 1323 |
+
lyrics_idea = data.get('music_lyrics_idea', '')
|
| 1324 |
+
|
| 1325 |
+
system_prompt = (
|
| 1326 |
+
"You are an expert Prompt Engineer for AI Music generation tools (like Suno, Udio).\n"
|
| 1327 |
+
"Your task is to write a highly detailed, professional prompt setup. For Suno/Udio, a prompt usually consists of two parts:\n"
|
| 1328 |
+
"1. Style tags (a comma-separated list of genres, sub-genres, moods, instruments, vocal description in English).\n"
|
| 1329 |
+
"2. Lyrics prompt or song structure prompts.\n"
|
| 1330 |
+
"Write the style tags in English and provide structured lyric suggestions (or structure tags like [Verse 1], [Chorus], etc.) in the user's desired language (Vietnamese by default).\n\n"
|
| 1331 |
+
"Input details provided by user:\n"
|
| 1332 |
+
f"- Chủ đề/Ý tưởng nhạc: {theme}\n"
|
| 1333 |
+
f"- Thể loại nhạc: {genre}\n"
|
| 1334 |
+
f"- Cảm xúc/Mood: {mood}\n"
|
| 1335 |
+
f"- Nhạc cụ sử dụng: {instruments}\n"
|
| 1336 |
+
f"- Giọng hát: {vocals}\n"
|
| 1337 |
+
f"- Ý tưởng lời bài hát: {lyrics_idea}\n\n"
|
| 1338 |
+
"Format the output clearly, showing:\n"
|
| 1339 |
+
"- **Style of Music Tags** (comma separated English keywords, e.g. 'synthwave, energetic, 120 bpm, male vocals, retro electric guitar')\n"
|
| 1340 |
+
"- **Prompt Description / Lyrics Structure** (with structured sections like [Verse 1], [Chorus], [Guitar Solo] and recommended lyrics/pacing in Vietnamese)\n\n"
|
| 1341 |
+
"Generate ONLY this structured response. Do not include introductory text."
|
| 1342 |
+
)
|
| 1343 |
+
elif category == 'story':
|
| 1344 |
+
title = data.get('story_title', '')
|
| 1345 |
+
genre = data.get('story_genre', 'Fantasy')
|
| 1346 |
+
characters = data.get('story_characters', '')
|
| 1347 |
+
plot = data.get('story_plot', '')
|
| 1348 |
+
tone = data.get('story_tone', 'Dramatic')
|
| 1349 |
+
length = data.get('story_length', 'Short Story')
|
| 1350 |
+
|
| 1351 |
+
system_prompt = (
|
| 1352 |
+
"You are an expert Prompt Engineer specializing in Creative Writing and Novel Scripting.\n"
|
| 1353 |
+
"Your task is to write a detailed, professional prompt in Vietnamese for a writing AI (like Claude or ChatGPT) to produce a compelling, high-quality story, novel chapter, or script.\n"
|
| 1354 |
+
"The generated prompt should include:\n"
|
| 1355 |
+
"- A detailed persona for the AI writer.\n"
|
| 1356 |
+
"- Background context and setting details.\n"
|
| 1357 |
+
"- Detailed character sheets and their relationship dynamics.\n"
|
| 1358 |
+
"- Plot guidelines, pacing instructions, and key narrative beats (e.g., the hook, the climax).\n"
|
| 1359 |
+
"- Writing style guidelines (e.g., show don't tell, vocabulary level, pacing, dialogue naturalness, tone).\n\n"
|
| 1360 |
+
"Input details provided by user:\n"
|
| 1361 |
+
f"- Tên truyện/Chủ đề: {title}\n"
|
| 1362 |
+
f"- Thể loại: {genre}\n"
|
| 1363 |
+
f"- Các nhân vật chính: {characters}\n"
|
| 1364 |
+
f"- Tóm tắt cốt truyện: {plot}\n"
|
| 1365 |
+
f"- Giọng văn/Tông màu: {tone}\n"
|
| 1366 |
+
f"- Độ dài/Định dạng: {length}\n\n"
|
| 1367 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1368 |
+
)
|
| 1369 |
+
elif category == 'video_script':
|
| 1370 |
+
topic = data.get('script_topic', '')
|
| 1371 |
+
platform = data.get('script_platform', 'YouTube')
|
| 1372 |
+
duration = data.get('script_duration', '5 minutes')
|
| 1373 |
+
structure = data.get('script_structure', 'Hook-Body-CTA')
|
| 1374 |
+
tone = data.get('script_tone', 'Engaging')
|
| 1375 |
+
cta = data.get('script_cta', '')
|
| 1376 |
+
|
| 1377 |
+
system_prompt = (
|
| 1378 |
+
"You are an expert Prompt Engineer for Video Script Writing and Storyboarding.\n"
|
| 1379 |
+
"Your task is to write a detailed, professional prompt in Vietnamese for an AI scriptwriter (like Claude or ChatGPT) to write a high-converting, engaging script for YouTube, TikTok, or Reels.\n"
|
| 1380 |
+
"The generated prompt should specify:\n"
|
| 1381 |
+
"- Target audience, platform context, and goals.\n"
|
| 1382 |
+
"- Script structure details (e.g., hook timing, body progression, transitions, audio cues, visual prompts for the video, and call-to-action).\n"
|
| 1383 |
+
"- Writing voice, pacing, and vocabulary constraints.\n"
|
| 1384 |
+
"- Format instructions (e.g., a two-column table with 'Visual description' and 'Spoken voiceover').\n\n"
|
| 1385 |
+
"Input details provided by user:\n"
|
| 1386 |
+
f"- Chủ đề video: {topic}\n"
|
| 1387 |
+
f"- Nền tảng: {platform}\n"
|
| 1388 |
+
f"- Thời lượng mong muốn: {duration}\n"
|
| 1389 |
+
f"- Cấu trúc kịch bản: {structure}\n"
|
| 1390 |
+
f"- Tông giọng: {tone}\n"
|
| 1391 |
+
f"- Lời kêu gọi hành động (CTA): {cta}\n\n"
|
| 1392 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1393 |
+
)
|
| 1394 |
+
elif category == 'marketing':
|
| 1395 |
+
topic = data.get('marketing_topic', '')
|
| 1396 |
+
platform = data.get('marketing_platform', 'Facebook')
|
| 1397 |
+
tone = data.get('marketing_tone', 'Persuasive')
|
| 1398 |
+
cta = data.get('marketing_cta', '')
|
| 1399 |
+
details = data.get('marketing_details', '')
|
| 1400 |
+
|
| 1401 |
+
system_prompt = (
|
| 1402 |
+
"You are an expert Prompt Engineer for Marketing and Copywriting.\n"
|
| 1403 |
+
"Your task is to write a highly detailed, professional prompt in Vietnamese for a writing AI (like ChatGPT or Claude) to create an engaging, high-converting marketing copy.\n"
|
| 1404 |
+
"The generated prompt should include:\n"
|
| 1405 |
+
"- A detailed role and target persona for the AI copywriter.\n"
|
| 1406 |
+
"- Target audience details, selling proposition, and marketing angle.\n"
|
| 1407 |
+
"- Structural specifications (e.g., AIDA formula, hook, body, Call to Action, emoji usage, line spacing).\n"
|
| 1408 |
+
"- Writing style guidelines (e.g., tone of voice, forbidden words, word count limits, formatting).\n\n"
|
| 1409 |
+
"Input details provided by user:\n"
|
| 1410 |
+
f"- Chủ đề/Sản phẩm: {topic}\n"
|
| 1411 |
+
f"- Nền tảng: {platform}\n"
|
| 1412 |
+
f"- Tông giọng: {tone}\n"
|
| 1413 |
+
f"- Lời kêu gọi hành động (CTA): {cta}\n"
|
| 1414 |
+
f"- Yêu cầu chi tiết khác: {details}\n\n"
|
| 1415 |
+
"Generate ONLY the optimized prompt in a clean copyable format. Do not include any introductory or concluding text."
|
| 1416 |
+
)
|
| 1417 |
+
elif category == 'education':
|
| 1418 |
+
subject = data.get('edu_subject', '')
|
| 1419 |
+
level = data.get('edu_level', 'General')
|
| 1420 |
+
method = data.get('edu_method', 'Analogy')
|
| 1421 |
+
goals = data.get('edu_goals', '')
|
| 1422 |
+
|
| 1423 |
+
system_prompt = (
|
| 1424 |
+
"You are an expert Prompt Engineer specializing in Education, Pedagogy, and Tutoring.\n"
|
| 1425 |
+
"Your task is to write a detailed, professional prompt in Vietnamese for an AI tutor to explain a topic, design a study guide, or tutor a student.\n"
|
| 1426 |
+
"The generated prompt should specify:\n"
|
| 1427 |
+
"- The AI's role (e.g., patient Socratic tutor, experienced professor, or ELI5 expert).\n"
|
| 1428 |
+
"- The target student's educational level and background knowledge.\n"
|
| 1429 |
+
"- The pedagogical method to use (e.g., step-by-step Socratic questioning, real-world analogies, conceptual breakdown, active recall quizzes).\n"
|
| 1430 |
+
"- Output structure (e.g., summary, analogies, vocabulary list, self-assessment questions).\n\n"
|
| 1431 |
+
"Input details provided by user:\n"
|
| 1432 |
+
f"- Chủ đề học tập: {subject}\n"
|
| 1433 |
+
f"- Trình độ học sinh: {level}\n"
|
| 1434 |
+
f"- Phương pháp sư phạm: {method}\n"
|
| 1435 |
+
f"- Mục tiêu học tập cụ thể: {goals}\n\n"
|
| 1436 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1437 |
+
)
|
| 1438 |
+
elif category == 'data':
|
| 1439 |
+
source = data.get('data_source', 'SQL Table')
|
| 1440 |
+
goal = data.get('data_goal', '')
|
| 1441 |
+
output = data.get('data_output', 'Executive Summary')
|
| 1442 |
+
details = data.get('data_details', '')
|
| 1443 |
+
|
| 1444 |
+
system_prompt = (
|
| 1445 |
+
"You are an expert Prompt Engineer for Data Analysis and Business Intelligence.\n"
|
| 1446 |
+
"Your task is to write a detailed, professional prompt in Vietnamese for an AI Data Analyst (like ChatGPT Advanced Data Analysis or Claude) to analyze data, write queries, or draft reports.\n"
|
| 1447 |
+
"The generated prompt should include:\n"
|
| 1448 |
+
"- The analyst role and technical profile (e.g., senior SQL developer, data scientist).\n"
|
| 1449 |
+
"- Data schema context and data source type.\n"
|
| 1450 |
+
"- The specific business question, analysis goals, and statistical methods (e.g., cohort analysis, trend analysis, correlation).\n"
|
| 1451 |
+
"- Output specifications (e.g., SQL queries, Python/pandas code, visualizations recommendations, executive summaries with action items).\n\n"
|
| 1452 |
+
"Input details provided by user:\n"
|
| 1453 |
+
f"- Nguồn dữ liệu & Cấu trúc: {source}\n"
|
| 1454 |
+
f"- Mục tiêu phân tích/Câu hỏi: {goal}\n"
|
| 1455 |
+
f"- Định dạng đầu ra mong muốn: {output}\n"
|
| 1456 |
+
f"- Chi tiết yêu cầu: {details}\n\n"
|
| 1457 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1458 |
+
)
|
| 1459 |
+
elif category == 'office':
|
| 1460 |
+
text = data.get('office_text', '')
|
| 1461 |
+
task = data.get('office_task', 'Summarize')
|
| 1462 |
+
tone = data.get('office_tone', 'Professional')
|
| 1463 |
+
instructions = data.get('office_instructions', '')
|
| 1464 |
+
|
| 1465 |
+
system_prompt = (
|
| 1466 |
+
"You are an expert Prompt Engineer for Office Administration and Document Processing.\n"
|
| 1467 |
+
"Your task is to write a detailed, professional prompt in Vietnamese to guide an AI assistant in performing administrative tasks like summarizing, proofreading, drafting emails, rewriting reports, or polishing documents.\n"
|
| 1468 |
+
"The generated prompt should specify:\n"
|
| 1469 |
+
"- The assistant's persona (e.g., executive assistant, expert proofreader).\n"
|
| 1470 |
+
"- The task details (e.g., bulleted executive summary, email reply matching a specific context, grammatical correction).\n"
|
| 1471 |
+
"- Writing tone, formatting constraints, length constraints, and context preservation guidelines.\n\n"
|
| 1472 |
+
"Input details provided by user:\n"
|
| 1473 |
+
f"- Văn bản gốc: {text}\n"
|
| 1474 |
+
f"- Công việc cần thực hiện: {task}\n"
|
| 1475 |
+
f"- Tông giọng/Phong cách: {tone}\n"
|
| 1476 |
+
f"- Hướng dẫn thêm: {instructions}\n\n"
|
| 1477 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1478 |
+
)
|
| 1479 |
+
elif category == 'brainstorm':
|
| 1480 |
+
topic = data.get('brain_topic', '')
|
| 1481 |
+
type_ = data.get('brain_type', 'Product Ideas')
|
| 1482 |
+
scope = data.get('brain_scope', 'Broad')
|
| 1483 |
+
details = data.get('brain_details', '')
|
| 1484 |
+
|
| 1485 |
+
system_prompt = (
|
| 1486 |
+
"You are an expert Prompt Engineer specializing in Creative Ideation and Brainstorming.\n"
|
| 1487 |
+
"Your task is to write a detailed, professional prompt in Vietnamese to guide an AI in brainstorming innovative concepts, product features, solution ideas, or design variations.\n"
|
| 1488 |
+
"The generated prompt should specify:\n"
|
| 1489 |
+
"- The creative consultant persona.\n"
|
| 1490 |
+
"- The core problem/opportunity and constraints.\n"
|
| 1491 |
+
"- The ideation framework to use (e.g., SCAMPER, lateral thinking, first principles).\n"
|
| 1492 |
+
"- Output structure (e.g., categorized list of 10 ideas, mindmap format, pros/cons list).\n\n"
|
| 1493 |
+
"Input details provided by user:\n"
|
| 1494 |
+
f"- Chủ đề/Bài toán cần giải: {topic}\n"
|
| 1495 |
+
f"- Loại ý tưởng mong muốn: {type_}\n"
|
| 1496 |
+
f"- Phạm vi/Mức độ sáng tạo: {scope}\n"
|
| 1497 |
+
f"- Chi tiết yêu cầu: {details}\n\n"
|
| 1498 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1499 |
+
)
|
| 1500 |
+
else: # Custom Enhancer / General
|
| 1501 |
+
raw_prompt = data.get('raw_prompt', '')
|
| 1502 |
+
target_ai = data.get('target_ai', 'General AI')
|
| 1503 |
+
custom_instructions = data.get('custom_instructions', '')
|
| 1504 |
+
|
| 1505 |
+
system_prompt = (
|
| 1506 |
+
"You are an expert Prompt Engineer.\n"
|
| 1507 |
+
"Your task is to take the user's raw prompt and rewrite it into a highly effective, optimized, and detailed prompt tailored for the target AI tool. The output should be in Vietnamese by default, unless the user requested otherwise.\n"
|
| 1508 |
+
"Add structure, clear guidelines, persona definition, input-output specifications, constraints, and examples where applicable.\n\n"
|
| 1509 |
+
"Input details provided by user:\n"
|
| 1510 |
+
f"- Raw Prompt: {raw_prompt}\n"
|
| 1511 |
+
f"- Target AI Tool: {target_ai}\n"
|
| 1512 |
+
f"- Custom Guidelines/Instructions: {custom_instructions}\n\n"
|
| 1513 |
+
"Generate ONLY the optimized prompt. Do not include introductory or concluding text."
|
| 1514 |
+
)
|
| 1515 |
+
|
| 1516 |
+
try:
|
| 1517 |
+
response = client.models.generate_content(
|
| 1518 |
+
model=model_id,
|
| 1519 |
+
contents=system_prompt
|
| 1520 |
+
)
|
| 1521 |
+
increment_usage(model_id)
|
| 1522 |
+
return jsonify({
|
| 1523 |
+
'success': True,
|
| 1524 |
+
'generated_prompt': response.text.strip()
|
| 1525 |
+
})
|
| 1526 |
+
except Exception as e:
|
| 1527 |
+
return jsonify({
|
| 1528 |
+
'success': False,
|
| 1529 |
+
'error': _err_msg(str(e))
|
| 1530 |
+
}), 500
|
| 1531 |
+
|
| 1532 |
+
@app.route('/api/prompt-generator/execute', methods=['POST'])
|
| 1533 |
+
def api_prompt_execute():
|
| 1534 |
+
data = request.json or {}
|
| 1535 |
+
generated_prompt = data.get('prompt', '').strip()
|
| 1536 |
+
model_id = data.get('gemini_model', DEFAULT_MODEL)
|
| 1537 |
+
|
| 1538 |
+
if not generated_prompt:
|
| 1539 |
+
return jsonify({'success': False, 'error': 'Không tìm thấy prompt để chạy thử'}), 400
|
| 1540 |
+
|
| 1541 |
+
try:
|
| 1542 |
+
response = client.models.generate_content(
|
| 1543 |
+
model=model_id,
|
| 1544 |
+
contents=generated_prompt
|
| 1545 |
+
)
|
| 1546 |
+
increment_usage(model_id)
|
| 1547 |
+
return jsonify({
|
| 1548 |
+
'success': True,
|
| 1549 |
+
'execution_result': response.text.strip()
|
| 1550 |
+
})
|
| 1551 |
+
except Exception as e:
|
| 1552 |
+
return jsonify({
|
| 1553 |
+
'success': False,
|
| 1554 |
+
'error': _err_msg(str(e))
|
| 1555 |
+
}), 500
|
| 1556 |
+
|
| 1557 |
if __name__ == '__main__':
|
| 1558 |
app.run(debug=True, port=5000)
|
fix_dynamic_video.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import random
|
| 2 |
+
|
| 3 |
+
path = 'app.py'
|
| 4 |
+
with open(path, 'r', encoding='utf-8') as f:
|
| 5 |
+
content = f.read()
|
| 6 |
+
|
| 7 |
+
# 1. Thêm bộ thư viện video đa dạng hơn
|
| 8 |
+
video_logic = """
|
| 9 |
+
# 4. Bản đồ video phong cách và chủ đề (Mở rộng)
|
| 10 |
+
video_library = {
|
| 11 |
+
'nature': [
|
| 12 |
+
'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
| 13 |
+
'https://v.ftcdn.net/03/05/73/05/700_F_305730594_9n8zJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4',
|
| 14 |
+
'https://v.ftcdn.net/02/10/42/48/700_F_210424855_pS8iZJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4'
|
| 15 |
+
],
|
| 16 |
+
'realistic': [
|
| 17 |
+
'https://www.w3schools.com/html/mov_bbb.mp4',
|
| 18 |
+
'https://v.ftcdn.net/05/52/63/52/700_F_552635201_O9n8zJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4',
|
| 19 |
+
'https://v.ftcdn.net/01/80/74/48/700_F_180744855_pS8iZJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4'
|
| 20 |
+
],
|
| 21 |
+
'sports': [
|
| 22 |
+
'https://v.ftcdn.net/05/11/48/44/700_F_511484433_ST.mp4',
|
| 23 |
+
'https://v.ftcdn.net/04/55/48/44/700_F_455484433_ST.mp4'
|
| 24 |
+
],
|
| 25 |
+
'tech': [
|
| 26 |
+
'https://www.w3schools.com/html/movie.mp4',
|
| 27 |
+
'https://v.ftcdn.net/02/45/63/52/700_F_245635201_ST.mp4'
|
| 28 |
+
],
|
| 29 |
+
'space': [
|
| 30 |
+
'https://v.ftcdn.net/02/10/42/48/700_F_210424855_pS8iZJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4'
|
| 31 |
+
]
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
# Dùng AI hoặc keyword đơn giản để chọn chủ đề
|
| 35 |
+
target_style = style
|
| 36 |
+
prompt_lower = prompt.lower()
|
| 37 |
+
if 'đá bóng' in prompt_lower or 'ronaldo' in prompt_lower or 'messi' in prompt_lower or 'football' in prompt_lower:
|
| 38 |
+
target_style = 'sports'
|
| 39 |
+
|
| 40 |
+
# Lấy danh sách video theo chủ đề, nếu không có thì dùng nature làm mặc định
|
| 41 |
+
available_videos = video_library.get(target_style, video_library['nature'])
|
| 42 |
+
video_url = random.choice(available_videos)
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
# Tìm đoạn code cũ và thay thế
|
| 46 |
+
old_video_logic = """ # 4. Bản đồ video phong cách
|
| 47 |
+
video_urls = {
|
| 48 |
+
'anime': 'https://v.ftcdn.net/05/52/63/52/700_F_552635201_O9n8zJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4',
|
| 49 |
+
'realistic': 'https://www.w3schools.com/html/mov_bbb.mp4',
|
| 50 |
+
'tech': 'https://www.w3schools.com/html/movie.mp4',
|
| 51 |
+
'nature': 'https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4',
|
| 52 |
+
'space': 'https://v.ftcdn.net/02/10/42/48/700_F_210424855_pS8iZJ9iV7Fz8J9zJ9zJ9zJ9zJ9zJ9zJ_ST.mp4'
|
| 53 |
+
}
|
| 54 |
+
video_url = video_urls.get(style, video_urls['nature'])"""
|
| 55 |
+
|
| 56 |
+
if old_video_logic in content:
|
| 57 |
+
content = content.replace(old_video_logic, video_logic)
|
| 58 |
+
else:
|
| 59 |
+
# Fallback nếu chuỗi không khớp hoàn toàn (do các edit trước)
|
| 60 |
+
import re
|
| 61 |
+
content = re.sub(r'# 4\. Bản đồ video phong cách.*?video_url = video_urls\.get\(style, video_urls\[\'nature\'\]\)', video_logic, content, flags=re.DOTALL)
|
| 62 |
+
|
| 63 |
+
with open(path, 'w', encoding='utf-8') as f:
|
| 64 |
+
f.write(content)
|
templates/index.html
CHANGED
|
@@ -70,5 +70,35 @@
|
|
| 70 |
</a>
|
| 71 |
</div>
|
| 72 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
</div>
|
| 74 |
-
{% endblock %}
|
|
|
|
| 70 |
</a>
|
| 71 |
</div>
|
| 72 |
|
| 73 |
+
<!-- Video AI Creator -->
|
| 74 |
+
<div class="col-md-4">
|
| 75 |
+
<a href="/video-ai" class="text-decoration-none">
|
| 76 |
+
<div class="card card-custom p-4 h-100 border-start border-4 border-warning">
|
| 77 |
+
<div class="d-flex align-items-center mb-3">
|
| 78 |
+
<div class="bg-warning bg-opacity-10 p-3 rounded-3 text-warning me-3">
|
| 79 |
+
<i class="fas fa-video fa-2x"></i>
|
| 80 |
+
</div>
|
| 81 |
+
<h5 class="mb-0 fw-bold text-dark">Video AI Studio</h5>
|
| 82 |
+
</div>
|
| 83 |
+
<p class="text-muted small mb-0">Tạo video AI từ văn bản với kịch bản, giọng đọc và nhạc nền tự động.</p>
|
| 84 |
+
</div>
|
| 85 |
+
</a>
|
| 86 |
+
</div>
|
| 87 |
+
|
| 88 |
+
<!-- AI Prompt Studio -->
|
| 89 |
+
<div class="col-md-4">
|
| 90 |
+
<a href="/prompt-generator" class="text-decoration-none">
|
| 91 |
+
<div class="card card-custom p-4 h-100 border-start border-4" style="border-color: #06b6d4 !important">
|
| 92 |
+
<div class="d-flex align-items-center mb-3">
|
| 93 |
+
<div class="p-3 rounded-3 me-3" style="background: #06b6d420; color: #06b6d4">
|
| 94 |
+
<i class="fas fa-magic fa-2x"></i>
|
| 95 |
+
</div>
|
| 96 |
+
<h5 class="mb-0 fw-bold text-dark">AI Prompt Studio</h5>
|
| 97 |
+
</div>
|
| 98 |
+
<p class="text-muted small mb-0">Tự động thiết kế và tối ưu prompt chuyên nghiệp cho Code, Video, Nhạc, Kịch bản bằng Gemini.</p>
|
| 99 |
+
</div>
|
| 100 |
+
</a>
|
| 101 |
+
</div>
|
| 102 |
+
|
| 103 |
</div>
|
| 104 |
+
{% endblock %}
|
templates/layout.html
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 10 |
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
|
|
|
|
| 11 |
{% block styles %}{% endblock %}
|
| 12 |
</head>
|
| 13 |
<body>
|
|
@@ -95,6 +96,53 @@
|
|
| 95 |
<i class="fas fa-table"></i> Trích xuất dữ liệu
|
| 96 |
</a>
|
| 97 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
</nav>
|
| 99 |
</div>
|
| 100 |
|
|
|
|
| 8 |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
| 9 |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
|
| 10 |
<link rel="stylesheet" href="{{ url_for('static', filename='theme.css') }}">
|
| 11 |
+
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>🎬</text></svg>">
|
| 12 |
{% block styles %}{% endblock %}
|
| 13 |
</head>
|
| 14 |
<body>
|
|
|
|
| 96 |
<i class="fas fa-table"></i> Trích xuất dữ liệu
|
| 97 |
</a>
|
| 98 |
</div>
|
| 99 |
+
<a class="nav-link {% if request.path.startswith('/video-ai') %}active{% endif %}" href="/video-ai">
|
| 100 |
+
<i class="fas fa-video"></i> Video AI
|
| 101 |
+
</a>
|
| 102 |
+
<a class="nav-link nav-link-collapsible {% if request.path.startswith('/prompt-generator') %}active{% endif %}"
|
| 103 |
+
href="/prompt-generator/code"
|
| 104 |
+
data-bs-toggle="collapse"
|
| 105 |
+
data-bs-target="#prompt-sub"
|
| 106 |
+
aria-expanded="{{ 'true' if request.path.startswith('/prompt-generator') else 'false' }}"
|
| 107 |
+
onclick="return handleNavCollapse(event, this)">
|
| 108 |
+
<span><i class="fas fa-magic"></i> AI Prompt Studio</span>
|
| 109 |
+
<i class="fas fa-chevron-down nav-chevron {% if not request.path.startswith('/prompt-generator') %}collapsed{% endif %}"></i>
|
| 110 |
+
</a>
|
| 111 |
+
<div class="collapse nav-sub {% if request.path.startswith('/prompt-generator') %}show{% endif %}" id="prompt-sub">
|
| 112 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/code' %}active{% endif %}" href="/prompt-generator/code">
|
| 113 |
+
<i class="fas fa-code"></i> Lập trình (Code)
|
| 114 |
+
</a>
|
| 115 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/video' %}active{% endif %}" href="/prompt-generator/video">
|
| 116 |
+
<i class="fas fa-video"></i> Ảnh / Video AI
|
| 117 |
+
</a>
|
| 118 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/music' %}active{% endif %}" href="/prompt-generator/music">
|
| 119 |
+
<i class="fas fa-music"></i> Nhạc (Suno/Udio)
|
| 120 |
+
</a>
|
| 121 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/story' %}active{% endif %}" href="/prompt-generator/story">
|
| 122 |
+
<i class="fas fa-book"></i> Kịch bản truyện
|
| 123 |
+
</a>
|
| 124 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/video-script' %}active{% endif %}" href="/prompt-generator/video-script">
|
| 125 |
+
<i class="fas fa-film"></i> Kịch bản Video
|
| 126 |
+
</a>
|
| 127 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/marketing' %}active{% endif %}" href="/prompt-generator/marketing">
|
| 128 |
+
<i class="fas fa-ad"></i> Marketing / Content
|
| 129 |
+
</a>
|
| 130 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/education' %}active{% endif %}" href="/prompt-generator/education">
|
| 131 |
+
<i class="fas fa-graduation-cap"></i> Học tập / Ngoại ngữ
|
| 132 |
+
</a>
|
| 133 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/data' %}active{% endif %}" href="/prompt-generator/data">
|
| 134 |
+
<i class="fas fa-chart-line"></i> Phân tích dữ liệu
|
| 135 |
+
</a>
|
| 136 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/office' %}active{% endif %}" href="/prompt-generator/office">
|
| 137 |
+
<i class="fas fa-briefcase"></i> Trợ lý văn phòng
|
| 138 |
+
</a>
|
| 139 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/brainstorm' %}active{% endif %}" href="/prompt-generator/brainstorm">
|
| 140 |
+
<i class="fas fa-lightbulb"></i> Lên ý tưởng
|
| 141 |
+
</a>
|
| 142 |
+
<a class="nav-link nav-link-sub {% if request.path == '/prompt-generator/enhancer' %}active{% endif %}" href="/prompt-generator/enhancer">
|
| 143 |
+
<i class="fas fa-wand-magic-sparkles"></i> Tối ưu Prompt thô
|
| 144 |
+
</a>
|
| 145 |
+
</div>
|
| 146 |
</nav>
|
| 147 |
</div>
|
| 148 |
|
templates/tools/prompt_studio/index.html
ADDED
|
@@ -0,0 +1,988 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "layout.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}AI Prompt Studio - Pum's Toolkit{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block styles %}
|
| 6 |
+
<style>
|
| 7 |
+
.prompt-card {
|
| 8 |
+
border: none;
|
| 9 |
+
border-radius: var(--radius-lg);
|
| 10 |
+
background: #ffffff;
|
| 11 |
+
box-shadow: var(--shadow-card);
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
.panel-title {
|
| 15 |
+
color: var(--color-primary);
|
| 16 |
+
font-weight: var(--font-weight-bold);
|
| 17 |
+
border-bottom: 2px solid var(--color-border-light);
|
| 18 |
+
padding-bottom: var(--spacing-sm);
|
| 19 |
+
margin-bottom: var(--spacing-lg);
|
| 20 |
+
font-size: 1.25rem;
|
| 21 |
+
display: flex;
|
| 22 |
+
align-items: center;
|
| 23 |
+
gap: 10px;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
.nav-pills {
|
| 27 |
+
gap: 8px;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.nav-pills .nav-link {
|
| 31 |
+
font-size: 0.82rem;
|
| 32 |
+
padding: 8px 14px;
|
| 33 |
+
border: 1px solid var(--color-border-input);
|
| 34 |
+
background: #ffffff;
|
| 35 |
+
color: var(--color-text-muted);
|
| 36 |
+
border-radius: var(--radius-md);
|
| 37 |
+
display: flex;
|
| 38 |
+
align-items: center;
|
| 39 |
+
justify-content: center;
|
| 40 |
+
transition: all 0.2s ease;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.nav-pills .nav-link:hover:not(.active) {
|
| 44 |
+
border-color: var(--color-primary);
|
| 45 |
+
color: var(--color-primary);
|
| 46 |
+
background: var(--color-primary-light);
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.nav-pills .nav-link.active {
|
| 50 |
+
background-color: var(--color-primary);
|
| 51 |
+
border-color: var(--color-primary);
|
| 52 |
+
color: #ffffff;
|
| 53 |
+
box-shadow: 0 4px 10px rgba(67, 97, 238, 0.2);
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
.prompt-textarea {
|
| 57 |
+
font-family: 'Consolas', 'Courier New', monospace;
|
| 58 |
+
font-size: 0.9rem;
|
| 59 |
+
line-height: 1.5;
|
| 60 |
+
background-color: #f8fafc;
|
| 61 |
+
border: 1px solid var(--color-border-input);
|
| 62 |
+
color: #1e293b;
|
| 63 |
+
resize: vertical;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
.result-box {
|
| 67 |
+
background-color: #f8fafc;
|
| 68 |
+
border-radius: var(--radius-md);
|
| 69 |
+
border: 1px solid var(--color-border-input);
|
| 70 |
+
padding: var(--spacing-md);
|
| 71 |
+
max-height: 350px;
|
| 72 |
+
overflow-y: auto;
|
| 73 |
+
font-size: 0.9rem;
|
| 74 |
+
white-space: pre-wrap;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
.badge-tab {
|
| 78 |
+
font-size: 0.75rem;
|
| 79 |
+
padding: 4px 8px;
|
| 80 |
+
background: var(--color-primary-light);
|
| 81 |
+
color: var(--color-primary);
|
| 82 |
+
border-radius: 4px;
|
| 83 |
+
font-weight: 600;
|
| 84 |
+
}
|
| 85 |
+
</style>
|
| 86 |
+
{% endblock %}
|
| 87 |
+
|
| 88 |
+
{% block content %}
|
| 89 |
+
<!-- Spinner Overlay -->
|
| 90 |
+
<div id="loaderOverlay" class="loader-overlay">
|
| 91 |
+
<div class="spinner-pro"></div>
|
| 92 |
+
<p class="mt-3 fw-bold" style="color: var(--color-primary)" id="loaderText">Đang tối ưu prompt bằng Gemini...</p>
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
<div class="mb-4">
|
| 96 |
+
<h2 class="fw-bold"><i class="fas fa-magic text-primary me-2"></i>AI Prompt Studio</h2>
|
| 97 |
+
<p class="text-muted">Tự động thiết kế và tối ưu prompt chuyên nghiệp cho mọi tác vụ bằng Gemini AI.</p>
|
| 98 |
+
</div>
|
| 99 |
+
|
| 100 |
+
<!-- Header with Dropdown Selector -->
|
| 101 |
+
<div class="card prompt-card mb-4 border-0">
|
| 102 |
+
<div class="card-body p-4 d-flex justify-content-between align-items-center flex-wrap gap-3">
|
| 103 |
+
<div>
|
| 104 |
+
<h4 class="fw-bold mb-1" style="color: var(--color-primary);">
|
| 105 |
+
{% if active_tab == 'code' %}<i class="fas fa-code me-2"></i>Lập trình (Code)
|
| 106 |
+
{% elif active_tab == 'video' %}<i class="fas fa-video me-2"></i>Ảnh / Video AI
|
| 107 |
+
{% elif active_tab == 'music' %}<i class="fas fa-music me-2"></i>Nhạc (Suno/Udio)
|
| 108 |
+
{% elif active_tab == 'story' %}<i class="fas fa-book me-2"></i>Kịch bản truyện
|
| 109 |
+
{% elif active_tab == 'video_script' %}<i class="fas fa-film me-2"></i>Kịch bản Video
|
| 110 |
+
{% elif active_tab == 'marketing' %}<i class="fas fa-ad me-2"></i>Marketing / Content
|
| 111 |
+
{% elif active_tab == 'education' %}<i class="fas fa-graduation-cap me-2"></i>Học tập / Ngoại ngữ
|
| 112 |
+
{% elif active_tab == 'data' %}<i class="fas fa-chart-line me-2"></i>Phân tích dữ liệu
|
| 113 |
+
{% elif active_tab == 'office' %}<i class="fas fa-briefcase me-2"></i>Trợ lý văn phòng
|
| 114 |
+
{% elif active_tab == 'brainstorm' %}<i class="fas fa-lightbulb me-2"></i>Lên ý tưởng sáng tạo
|
| 115 |
+
{% elif active_tab == 'enhancer' %}<i class="fas fa-wand-magic-sparkles me-2"></i>Tối ưu Prompt thô
|
| 116 |
+
{% endif %}
|
| 117 |
+
</h4>
|
| 118 |
+
<p class="text-muted small mb-0">Thiết kế và tối ưu cấu trúc prompt tối ưu cho tác vụ hiện tại.</p>
|
| 119 |
+
</div>
|
| 120 |
+
|
| 121 |
+
<!-- Category Dropdown Button -->
|
| 122 |
+
<div class="dropdown">
|
| 123 |
+
<button class="btn btn-outline-primary dropdown-toggle fw-semibold" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-expanded="false" style="padding: 10px 20px; border-radius: var(--radius-md);">
|
| 124 |
+
<i class="fas fa-list-ul me-2"></i>Chọn loại Prompt khác
|
| 125 |
+
</button>
|
| 126 |
+
<ul class="dropdown-menu dropdown-menu-end shadow border-0" aria-labelledby="dropdownMenuButton" style="border-radius: var(--radius-lg); padding: 10px; min-width: 260px;">
|
| 127 |
+
<li class="dropdown-header text-uppercase font-weight-bold text-muted small px-3 py-2" style="font-size: 0.7rem; letter-spacing: 0.05em;">Kỹ thuật & Lập trình</li>
|
| 128 |
+
<li><a class="dropdown-item {% if active_tab == 'code' %}active{% endif %} py-2 px-3" href="/prompt-generator/code" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-code me-2"></i>Lập trình (Code)</a></li>
|
| 129 |
+
<li><a class="dropdown-item {% if active_tab == 'data' %}active{% endif %} py-2 px-3" href="/prompt-generator/data" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-chart-line me-2"></i>Phân tích dữ liệu</a></li>
|
| 130 |
+
|
| 131 |
+
<li><hr class="dropdown-divider"></li>
|
| 132 |
+
<li class="dropdown-header text-uppercase font-weight-bold text-muted small px-3 py-2" style="font-size: 0.7rem; letter-spacing: 0.05em;">Sáng tạo & Nghệ thuật</li>
|
| 133 |
+
<li><a class="dropdown-item {% if active_tab == 'video' %}active{% endif %} py-2 px-3" href="/prompt-generator/video" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-video me-2"></i>Ảnh / Video AI</a></li>
|
| 134 |
+
<li><a class="dropdown-item {% if active_tab == 'music' %}active{% endif %} py-2 px-3" href="/prompt-generator/music" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-music me-2"></i>Nhạc (Suno/Udio)</a></li>
|
| 135 |
+
<li><a class="dropdown-item {% if active_tab == 'story' %}active{% endif %} py-2 px-3" href="/prompt-generator/story" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-book me-2"></i>Kịch bản truyện</a></li>
|
| 136 |
+
<li><a class="dropdown-item {% if active_tab == 'video_script' %}active{% endif %} py-2 px-3" href="/prompt-generator/video-script" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-film me-2"></i>Kịch bản Video</a></li>
|
| 137 |
+
|
| 138 |
+
<li><hr class="dropdown-divider"></li>
|
| 139 |
+
<li class="dropdown-header text-uppercase font-weight-bold text-muted small px-3 py-2" style="font-size: 0.7rem; letter-spacing: 0.05em;">Công việc & Học tập</li>
|
| 140 |
+
<li><a class="dropdown-item {% if active_tab == 'marketing' %}active{% endif %} py-2 px-3" href="/prompt-generator/marketing" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-ad me-2"></i>Marketing / Content</a></li>
|
| 141 |
+
<li><a class="dropdown-item {% if active_tab == 'education' %}active{% endif %} py-2 px-3" href="/prompt-generator/education" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-graduation-cap me-2"></i>Học tập / Ngoại ngữ</a></li>
|
| 142 |
+
<li><a class="dropdown-item {% if active_tab == 'office' %}active{% endif %} py-2 px-3" href="/prompt-generator/office" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-briefcase me-2"></i>Trợ lý văn phòng</a></li>
|
| 143 |
+
<li><a class="dropdown-item {% if active_tab == 'brainstorm' %}active{% endif %} py-2 px-3" href="/prompt-generator/brainstorm" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-lightbulb me-2"></i>Lên ý tưởng sáng tạo</a></li>
|
| 144 |
+
|
| 145 |
+
<li><hr class="dropdown-divider"></li>
|
| 146 |
+
<li class="dropdown-header text-uppercase font-weight-bold text-muted small px-3 py-2" style="font-size: 0.7rem; letter-spacing: 0.05em;">Tổng quát</li>
|
| 147 |
+
<li><a class="dropdown-item {% if active_tab == 'enhancer' %}active{% endif %} py-2 px-3" href="/prompt-generator/enhancer" style="border-radius: var(--radius-sm); font-size: 0.85rem;"><i class="fas fa-wand-magic-sparkles me-2"></i>Tối ưu Prompt thô</a></li>
|
| 148 |
+
</ul>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
<div class="row g-4">
|
| 154 |
+
<!-- Left Column: Inputs Form -->
|
| 155 |
+
<div class="col-lg-6">
|
| 156 |
+
<div class="card prompt-card h-100">
|
| 157 |
+
<div class="card-body p-4 d-flex flex-column justify-content-between">
|
| 158 |
+
<div>
|
| 159 |
+
<h5 class="panel-title"><i class="fas fa-sliders-h"></i> Cấu hình tham số</h5>
|
| 160 |
+
|
| 161 |
+
<div class="tab-content" id="promptTabsContent">
|
| 162 |
+
|
| 163 |
+
<!-- TAB 1: CODE -->
|
| 164 |
+
{% if active_tab == 'code' %}
|
| 165 |
+
<div class="tab-pane fade show active" id="tab-code" role="tabpanel">
|
| 166 |
+
<div class="mb-3">
|
| 167 |
+
<label class="form-label">Tính năng / Công việc cần viết code</label>
|
| 168 |
+
<textarea class="form-control" id="code_task" rows="4" placeholder="Ví dụ: Viết một class Python để kết nối với cơ sở dữ liệu MySQL, thực hiện các thao tác CRUD cơ bản và tự động ghi log lỗi vào file..."></textarea>
|
| 169 |
+
</div>
|
| 170 |
+
<div class="row g-3 mb-3">
|
| 171 |
+
<div class="col-md-6">
|
| 172 |
+
<label class="form-label">Ngôn ngữ lập trình</label>
|
| 173 |
+
<select class="form-select" id="code_lang">
|
| 174 |
+
<option value="Python" selected>Python</option>
|
| 175 |
+
<option value="JavaScript">JavaScript</option>
|
| 176 |
+
<option value="TypeScript">TypeScript</option>
|
| 177 |
+
<option value="HTML/CSS">HTML / CSS</option>
|
| 178 |
+
<option value="Go">Go</option>
|
| 179 |
+
<option value="Rust">Rust</option>
|
| 180 |
+
<option value="Java">Java</option>
|
| 181 |
+
<option value="C#">C#</option>
|
| 182 |
+
<option value="C++">C++</option>
|
| 183 |
+
<option value="PHP">PHP</option>
|
| 184 |
+
<option value="SQL">SQL</option>
|
| 185 |
+
</select>
|
| 186 |
+
</div>
|
| 187 |
+
<div class="col-md-6">
|
| 188 |
+
<label class="form-label">Thư viện / Framework</label>
|
| 189 |
+
<input type="text" class="form-control" id="code_framework" placeholder="Ví dụ: Flask, React, pandas...">
|
| 190 |
+
</div>
|
| 191 |
+
</div>
|
| 192 |
+
<div class="mb-3">
|
| 193 |
+
<label class="form-label">Phong cách & Quy chuẩn Code</label>
|
| 194 |
+
<select class="form-select" id="code_style">
|
| 195 |
+
<option value="Clean Code, tuân thủ DRY và SOLID, có ghi chú giải thích" selected>Clean Code, SOLID, DRY (Khuyên dùng)</option>
|
| 196 |
+
<option value="Tối ưu hóa hiệu năng, xử lý bất đồng bộ tốt">Tối ưu hiệu suất cao (High Performance)</option>
|
| 197 |
+
<option value="Comment giải thích cực kỳ chi tiết từng dòng cho người mới bắt đầu">Giải thích chi tiết (Dành cho newbie)</option>
|
| 198 |
+
<option value="Tích hợp sẵn Unit Tests đầy đủ cho các hàm quan trọng">Kèm theo bộ Unit Tests</option>
|
| 199 |
+
<option value="Code tối giản, ngắn gọn nhất có thể">Tối giản (Minimalist)</option>
|
| 200 |
+
</select>
|
| 201 |
+
</div>
|
| 202 |
+
<div class="mb-3">
|
| 203 |
+
<label class="form-label">Yêu cầu ràng buộc bổ sung (Không bắt buộc)</label>
|
| 204 |
+
<textarea class="form-control" id="code_requirements" rows="2" placeholder="Ví dụ: Sử dụng hàm map/reduce thay vì vòng lặp thường, không dùng thư viện ngoài..."></textarea>
|
| 205 |
+
</div>
|
| 206 |
+
</div>
|
| 207 |
+
{% endif %}
|
| 208 |
+
|
| 209 |
+
<!-- TAB 2: IMAGE/VIDEO AI -->
|
| 210 |
+
{% if active_tab == 'video' %}
|
| 211 |
+
<div class="tab-pane fade show active" id="tab-video" role="tabpanel">
|
| 212 |
+
<div class="mb-3">
|
| 213 |
+
<label class="form-label">Ý tưởng hình ảnh / video Concept</label>
|
| 214 |
+
<textarea class="form-control" id="video_topic" rows="3" placeholder="Ví dụ: Một phi hành gia đang cưỡi ngựa trên sa mạc của sao hỏa, ngắm nhìn trái đất ở phía xa..."></textarea>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="row g-3 mb-3">
|
| 217 |
+
<div class="col-md-6">
|
| 218 |
+
<label class="form-label">Phong cách nghệ thuật (Style)</label>
|
| 219 |
+
<select class="form-select" id="video_style">
|
| 220 |
+
<option value="Cinematic movie scene, 8k, film grain, dramatic masterwork" selected>Cinematic (Điện ảnh)</option>
|
| 221 |
+
<option value="Photorealistic, highly detailed 3D render, hyperrealism, octane render">Photorealistic (Tả thực)</option>
|
| 222 |
+
<option value="Anime style, vibrant colors, detailed illustration, Studio Ghibli aesthetic">Anime / Ghibli</option>
|
| 223 |
+
<option value="3D cartoon, Disney Pixar animation style, cute, colorful">3D Pixar Cartoon</option>
|
| 224 |
+
<option value="Cyberpunk style, neon lights, high-tech, wet rainy streets">Cyberpunk (Neon giả tưởng)</option>
|
| 225 |
+
<option value="Dreamy watercolor painting style, soft edges, pastel colors, artistic">Watercolor (Tranh màu nước)</option>
|
| 226 |
+
<option value="Vintage 1970s retro film photography, warm color grading, nostalgia">Vintage Retro Film</option>
|
| 227 |
+
</select>
|
| 228 |
+
</div>
|
| 229 |
+
<div class="col-md-6">
|
| 230 |
+
<label class="form-label">Góc máy & Cự ly (Camera)</label>
|
| 231 |
+
<select class="form-select" id="video_shot">
|
| 232 |
+
<option value="Medium shot, eye-level, balanced composition" selected>Medium Shot (Trung cảnh)</option>
|
| 233 |
+
<option value="Extreme close-up macro shot, focus on fine details">Extreme Close-up (Cận cảnh chi tiết)</option>
|
| 234 |
+
<option value="Wide angle landscape shot, epic scale, grand view">Wide Angle (Góc rộng/Toàn cảnh)</option>
|
| 235 |
+
<option value="Drone aerial top-down view, cinematic scale">Drone View (Từ trên cao)</option>
|
| 236 |
+
<option value="Low angle shot, looking up, dramatic perspective">Low Angle (Từ dưới lên)</option>
|
| 237 |
+
<option value="Cinematic slow panning camera movement">Slow Panning (Quay quét chậm - cho Video)</option>
|
| 238 |
+
</select>
|
| 239 |
+
</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="row g-3 mb-3">
|
| 242 |
+
<div class="col-md-6">
|
| 243 |
+
<label class="form-label">Ánh sáng (Lighting)</label>
|
| 244 |
+
<select class="form-select" id="video_lighting">
|
| 245 |
+
<option value="Cinematic natural lighting, soft shadows" selected>Natural Light (Tự nhiên)</option>
|
| 246 |
+
<option value="Warm golden hour sunlight, long dramatic shadows">Golden Hour (Hoàng hôn)</option>
|
| 247 |
+
<option value="Dramatic studio lighting, rim light, volumetric dust rays">Studio Lighting (Phòng studio)</option>
|
| 248 |
+
<option value="Biopunk bioluminescent glow, neon lighting, dark environment">Neon/Bioluminescent Glow</option>
|
| 249 |
+
<option value="Moody low key lighting, high contrast chiaroscuro">Chiaroscuro (Tương phản tối)</option>
|
| 250 |
+
</select>
|
| 251 |
+
</div>
|
| 252 |
+
<div class="col-md-6">
|
| 253 |
+
<label class="form-label">Tỷ lệ khung hình (Aspect Ratio)</label>
|
| 254 |
+
<select class="form-select" id="video_ratio">
|
| 255 |
+
<option value="16:9 (--ar 16:9)" selected>16:9 (Landscape - YouTube)</option>
|
| 256 |
+
<option value="9:16 (--ar 9:16)">9:16 (Vertical - TikTok/Shorts)</option>
|
| 257 |
+
<option value="1:1 (--ar 1:1)">1:1 (Square - Instagram)</option>
|
| 258 |
+
<option value="4:3 (--ar 4:3)">4:3 (Classic TV)</option>
|
| 259 |
+
</select>
|
| 260 |
+
</div>
|
| 261 |
+
</div>
|
| 262 |
+
<div class="mb-3">
|
| 263 |
+
<label class="form-label">Chi tiết bổ sung (Bầu không khí, màu sắc chủ đạo...)</label>
|
| 264 |
+
<textarea class="form-control" id="video_details" rows="2" placeholder="Ví dụ: Tông màu chủ đạo xanh lam huyền ảo, bầu không khí huyền bí, có sương mù nhẹ xung quanh..."></textarea>
|
| 265 |
+
</div>
|
| 266 |
+
</div>
|
| 267 |
+
{% endif %}
|
| 268 |
+
|
| 269 |
+
<!-- TAB 3: MUSIC -->
|
| 270 |
+
{% if active_tab == 'music' %}
|
| 271 |
+
<div class="tab-pane fade show active" id="tab-music" role="tabpanel">
|
| 272 |
+
<div class="mb-3">
|
| 273 |
+
<label class="form-label">Ý tưởng hoặc Chủ đề nhạc</label>
|
| 274 |
+
<textarea class="form-control" id="music_theme" rows="3" placeholder="Ví dụ: Một bài hát kể về sự cô đơn khi đi dưới mưa giữa lòng thành phố náo nhiệt ban đêm..."></textarea>
|
| 275 |
+
</div>
|
| 276 |
+
<div class="row g-3 mb-3">
|
| 277 |
+
<div class="col-md-6">
|
| 278 |
+
<label class="form-label">Thể loại (Genre)</label>
|
| 279 |
+
<select class="form-select" id="music_genre">
|
| 280 |
+
<option value="Pop Ballad" selected>Pop Ballad</option>
|
| 281 |
+
<option value="Lo-Fi Hip-Hop">Lo-Fi Hip-Hop</option>
|
| 282 |
+
<option value="Synthwave / Retro Electro">Synthwave / Retro</option>
|
| 283 |
+
<option value="Indie Rock">Indie Rock</option>
|
| 284 |
+
<option value="EDM / Future Bass">EDM / House</option>
|
| 285 |
+
<option value="R&B / Soul">R&B / Soul</option>
|
| 286 |
+
<option value="Epic Orchestral, Cinematic soundtrack">Sử thi hoành tráng (Epic)</option>
|
| 287 |
+
<option value="Acoustic Folk">Acoustic / Folk</option>
|
| 288 |
+
</select>
|
| 289 |
+
</div>
|
| 290 |
+
<div class="col-md-6">
|
| 291 |
+
<label class="form-label">Cảm xúc (Mood)</label>
|
| 292 |
+
<select class="form-select" id="music_mood">
|
| 293 |
+
<option value="Melancholic, sad, emotional" selected>U sầu / Cảm xúc (Sad)</option>
|
| 294 |
+
<option value="Relaxing, calm, chill, ambient">Thư giãn / Chill (Relaxing)</option>
|
| 295 |
+
<option value="Energetic, upbeat, powerful, positive">Sôi nổi / Tràn năng lượng</option>
|
| 296 |
+
<option value="Dark, intense, dramatic, tense">U tối / Kịch tính (Dark)</option>
|
| 297 |
+
<option value="Happy, bright, cheerful">Vui tươi / Hạnh phúc (Happy)</option>
|
| 298 |
+
</select>
|
| 299 |
+
</div>
|
| 300 |
+
</div>
|
| 301 |
+
<div class="row g-3 mb-3">
|
| 302 |
+
<div class="col-md-6">
|
| 303 |
+
<label class="form-label">Nhạc cụ chính</label>
|
| 304 |
+
<input type="text" class="form-control" id="music_instruments" placeholder="Ví dụ: Piano, Acoustic Guitar, Saxophone...">
|
| 305 |
+
</div>
|
| 306 |
+
<div class="col-md-6">
|
| 307 |
+
<label class="form-label">Giọng hát (Vocals)</label>
|
| 308 |
+
<select class="form-select" id="music_vocals">
|
| 309 |
+
<option value="Emotional Female vocals" selected>Giọng Nữ cảm xúc</option>
|
| 310 |
+
<option value="Warm Male vocals">Giọng Nam ấm áp</option>
|
| 311 |
+
<option value="Male and Female duet">Song ca Nam Nữ</option>
|
| 312 |
+
<option value="Instrumental solo, no vocals">Nhạc không lời (Instrumental)</option>
|
| 313 |
+
<option value="Vocaloid / Synthesized voice">Giọng điện tử (Vocaloid)</option>
|
| 314 |
+
</select>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
<div class="mb-3">
|
| 318 |
+
<label class="form-label">Ý tưởng lời bài hát / Câu chữ cụ thể (Nếu có)</label>
|
| 319 |
+
<textarea class="form-control" id="music_lyrics_idea" rows="2" placeholder="Ví dụ: Có câu thơ 'Mưa rơi ướt vai em, đèn đường nhạt nhòa'..."></textarea>
|
| 320 |
+
</div>
|
| 321 |
+
</div>
|
| 322 |
+
{% endif %}
|
| 323 |
+
|
| 324 |
+
<!-- TAB 4: STORY SCRIPT -->
|
| 325 |
+
{% if active_tab == 'story' %}
|
| 326 |
+
<div class="tab-pane fade show active" id="tab-story" role="tabpanel">
|
| 327 |
+
<div class="row g-3 mb-3">
|
| 328 |
+
<div class="col-md-6">
|
| 329 |
+
<label class="form-label">Tên truyện / Chủ đề</label>
|
| 330 |
+
<input type="text" class="form-control" id="story_title" placeholder="Ví dụ: Kẻ Du Hành Thời Gian Cuối Cùng">
|
| 331 |
+
</div>
|
| 332 |
+
<div class="col-md-6">
|
| 333 |
+
<label class="form-label">Thể loại</label>
|
| 334 |
+
<select class="form-select" id="story_genre">
|
| 335 |
+
<option value="Khoa học viễn tưởng (Sci-Fi, time travel)" selected>Khoa học viễn tưởng</option>
|
| 336 |
+
<option value="Huyền ảo / Kiếm hiệp / Tiên hiệp (Fantasy)">Huyền ảo / Kỳ ảo (Fantasy)</option>
|
| 337 |
+
<option value="Trinh thám / Kỳ bí / Phá án (Mystery/Detective)">Trinh thám / Phá án</option>
|
| 338 |
+
<option value="Kinh dị / Tâm linh (Horror)">Kinh dị / Kỳ quái</option>
|
| 339 |
+
<option value="Lãng mạn / Ngôn tình (Romance)">Tình cảm / Lãng mạn</option>
|
| 340 |
+
</select>
|
| 341 |
+
</div>
|
| 342 |
+
</div>
|
| 343 |
+
<div class="mb-3">
|
| 344 |
+
<label class="form-label">Tuyến nhân vật chính</label>
|
| 345 |
+
<textarea class="form-control" id="story_characters" rows="2" placeholder="Ví dụ: Leo - một nhà khoa học già cố gắng quay về quá khứ để cứu con gái, tính cách lầm lì nhưng giàu tình cảm..."></textarea>
|
| 346 |
+
</div>
|
| 347 |
+
<div class="mb-3">
|
| 348 |
+
<label class="form-label">Cốt truyện chính / Điểm nhấn / Plot twist</label>
|
| 349 |
+
<textarea class="form-control" id="story_plot" rows="3" placeholder="Ví dụ: Leo phát hiện ra rằng việc ông cố gắng quay lại quá khứ thực chất lại là nguyên nhân gián tiếp gây ra tai nạn của con gái ông (vòng lặp nghịch lý)..."></textarea>
|
| 350 |
+
</div>
|
| 351 |
+
<div class="row g-3 mb-3">
|
| 352 |
+
<div class="col-md-6">
|
| 353 |
+
<label class="form-label">Tông giọng chủ đạo</label>
|
| 354 |
+
<select class="form-select" id="story_tone">
|
| 355 |
+
<option value="Trầm buồn, nhiều suy tư, giàu chiều sâu triết lý" selected>Trầm buồn / Triết lý</option>
|
| 356 |
+
<option value="Hồi hộp, kịch tính, dồn dập, căng thẳng">Hồi hộp / Kịch tính</option>
|
| 357 |
+
<option value="Hài hước, châm biếm vui vẻ, tươi sáng">Hài hước / Tươi sáng</option>
|
| 358 |
+
<option value="Lãng mạn, nhẹ nhàng, sâu lắng, ngọt ngào">Lãng mạn / Nhẹ nhàng</option>
|
| 359 |
+
</select>
|
| 360 |
+
</div>
|
| 361 |
+
<div class="col-md-6">
|
| 362 |
+
<label class="form-label">Độ dài / Định dạng</label>
|
| 363 |
+
<select class="form-select" id="story_length">
|
| 364 |
+
<option value="Truyện ngắn hoàn chỉnh (Short story khoảng 2000 từ)" selected>Truyện ngắn hoàn chỉnh</option>
|
| 365 |
+
<option value="Chương đầu mở cuốn của tiểu thuyết dài tập (Novel Chapter)">Chương tiểu thuyết mở đầu</option>
|
| 366 |
+
<option value="Dàn ý kịch bản chi tiết từng chương (Plot outline)">Dàn ý kịch bản chi tiết</option>
|
| 367 |
+
</select>
|
| 368 |
+
</div>
|
| 369 |
+
</div>
|
| 370 |
+
</div>
|
| 371 |
+
{% endif %}
|
| 372 |
+
|
| 373 |
+
<!-- TAB 5: VIDEO SCRIPT -->
|
| 374 |
+
{% if active_tab == 'video_script' %}
|
| 375 |
+
<div class="tab-pane fade show active" id="tab-video-script" role="tabpanel">
|
| 376 |
+
<div class="mb-3">
|
| 377 |
+
<label class="form-label">Chủ đề Video cần viết kịch bản</label>
|
| 378 |
+
<textarea class="form-control" id="script_topic" rows="3" placeholder="Ví dụ: 5 mẹo sử dụng Python giúp lập trình viên tăng gấp đôi hiệu suất làm việc hàng ngày..."></textarea>
|
| 379 |
+
</div>
|
| 380 |
+
<div class="row g-3 mb-3">
|
| 381 |
+
<div class="col-md-6">
|
| 382 |
+
<label class="form-label">Nền tảng phát hành</label>
|
| 383 |
+
<select class="form-select" id="script_platform">
|
| 384 |
+
<option value="YouTube Long-form video" selected>YouTube (Video dài)</option>
|
| 385 |
+
<option value="TikTok / YouTube Shorts / FB Reels (Video đứng ngắn < 1p)">TikTok / Reels / Shorts (<1m)</option>
|
| 386 |
+
<option value="Video quảng cáo sản phẩm / Facebook Ads">Facebook Video quảng cáo / ADS</option>
|
| 387 |
+
<option value="Khóa học online / Video bài giảng">Khóa học / Hướng dẫn chuyên sâu</option>
|
| 388 |
+
</select>
|
| 389 |
+
</div>
|
| 390 |
+
<div class="col-md-6">
|
| 391 |
+
<label class="form-label">Thời lượng mong muốn</label>
|
| 392 |
+
<input type="text" class="form-control" id="script_duration" placeholder="Ví dụ: 1 phút, 8 phút..." value="5 phút">
|
| 393 |
+
</div>
|
| 394 |
+
</div>
|
| 395 |
+
<div class="row g-3 mb-3">
|
| 396 |
+
<div class="col-md-6">
|
| 397 |
+
<label class="form-label">Cấu trúc kịch bản</label>
|
| 398 |
+
<select class="form-select" id="script_structure">
|
| 399 |
+
<option value="Cấu trúc thu hút: 5s Hook đầu gây tò mò -> Trình bày 5 mẹo ngắn -> Kêu gọi hành động (Hook-Body-CTA)" selected>Thu hút & Kêu gọi (Hook-Body-CTA)</option>
|
| 400 |
+
<option value="Cấu trúc kể câu chuyện thực tế (Storytelling), hành trình vượt qua khó khăn">Kể chuyện (Narrative Story)</option>
|
| 401 |
+
<option value="Cấu trúc So sánh / Đánh giá khách quan ưu nhược điểm (Review)">So sánh & Đánh giá (Review)</option>
|
| 402 |
+
<option value="Cấu trúc vấn đề và giải pháp trực tiếp (Problem-Solution)">Vấn đề & Giải pháp</option>
|
| 403 |
+
</select>
|
| 404 |
+
</div>
|
| 405 |
+
<div class="col-md-6">
|
| 406 |
+
<label class="form-label">Tông giọng người dẫn</label>
|
| 407 |
+
<select class="form-select" id="script_tone">
|
| 408 |
+
<option value="Sôi nổi, năng lượng cao, lôi cuốn, tạo sự chú ý" selected>Sôi nổi / Năng lượng cao</option>
|
| 409 |
+
<option value="Thân thiện, chia sẻ kiến thức, dễ hiểu, ấm áp">Thân thiện / Chia sẻ</option>
|
| 410 |
+
<option value="Chuyên nghiệp, đáng tin cậy, khoa học, logic">Chuyên nghiệp / Logic</option>
|
| 411 |
+
<option value="Hài hước, châm biếm nhẹ, dí dỏm">Dí dỏm / Hài hước</option>
|
| 412 |
+
</select>
|
| 413 |
+
</div>
|
| 414 |
+
</div>
|
| 415 |
+
<div class="mb-3">
|
| 416 |
+
<label class="form-label">Lời kêu gọi hành động cuối video (CTA)</label>
|
| 417 |
+
<input type="text" class="form-control" id="script_cta" placeholder="Ví dụ: Nhấn đăng ký kênh và tải bộ code mẫu ở link bên dưới mô tả..." value="Nhấn Đăng ký kênh và bình luận ý kiến của bạn">
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
{% endif %}
|
| 421 |
+
|
| 422 |
+
<!-- TAB 6: MARKETING -->
|
| 423 |
+
{% if active_tab == 'marketing' %}
|
| 424 |
+
<div class="tab-pane fade show active" id="tab-marketing" role="tabpanel">
|
| 425 |
+
<div class="mb-3">
|
| 426 |
+
<label class="form-label">Sản phẩm / Dịch vụ hoặc Chủ đề quảng cáo</label>
|
| 427 |
+
<textarea class="form-control" id="marketing_topic" rows="3" placeholder="Ví dụ: Giới thiệu bình nước giữ nhiệt thông minh có hiển thị nhiệt độ, dung tích 500ml, nhắm vào dân văn phòng..."></textarea>
|
| 428 |
+
</div>
|
| 429 |
+
<div class="row g-3 mb-3">
|
| 430 |
+
<div class="col-md-6">
|
| 431 |
+
<label class="form-label">Nền tảng xuất bản</label>
|
| 432 |
+
<select class="form-select" id="marketing_platform">
|
| 433 |
+
<option value="Facebook Post" selected>Facebook Post</option>
|
| 434 |
+
<option value="Facebook Ads (Chạy quảng cáo copywriter)">Facebook Ads (Chạy quảng cáo)</option>
|
| 435 |
+
<option value="SEO Article Blog post">Bài viết chuẩn SEO (Blog)</option>
|
| 436 |
+
<option value="Email Marketing Campaign">Email Marketing (Bán hàng)</option>
|
| 437 |
+
<option value="Landing Page Copywriting">Landing Page (Trang đích)</option>
|
| 438 |
+
<option value="Slogan / Tagline Creation">Tạo Slogan / Tagline</option>
|
| 439 |
+
</select>
|
| 440 |
+
</div>
|
| 441 |
+
<div class="col-md-6">
|
| 442 |
+
<label class="form-label">Tông giọng bài viết (Tone)</label>
|
| 443 |
+
<select class="form-select" id="marketing_tone">
|
| 444 |
+
<option value="Thuyết phục, lôi cuốn, đánh trúng nỗi đau khách hàng (Persuasive)" selected>Thuyết phục / Bán hàng</option>
|
| 445 |
+
<option value="Hài hước, bắt trend, dí dỏm, trẻ trung">Hài hước / Bắt trend</option>
|
| 446 |
+
<option value="Chuyên nghiệp, tin cậy, cung cấp nhiều giá trị thực tế">Chuyên gia / Tin cậy</option>
|
| 447 |
+
<option value="Giản dị, chân thành, k��� chuyện trải nghiệm (Storytelling)">Chân thành / Kể chuyện</option>
|
| 448 |
+
</select>
|
| 449 |
+
</div>
|
| 450 |
+
</div>
|
| 451 |
+
<div class="mb-3">
|
| 452 |
+
<label class="form-label">Lời kêu gọi hành động (CTA)</label>
|
| 453 |
+
<input type="text" class="form-control" id="marketing_cta" placeholder="Ví dụ: Nhận ưu đãi 30% khi đặt hàng hôm nay qua link..." value="Mua ngay hôm nay để nhận ưu đãi giảm 30%">
|
| 454 |
+
</div>
|
| 455 |
+
<div class="mb-3">
|
| 456 |
+
<label class="form-label">Chi tiết bổ sung (Từ khóa cần chèn, độ dài...)</label>
|
| 457 |
+
<textarea class="form-control" id="marketing_details" rows="2" placeholder="Ví dụ: Chèn các hashtag #binhgiunhiet #office, độ dài dưới 300 từ..."></textarea>
|
| 458 |
+
</div>
|
| 459 |
+
</div>
|
| 460 |
+
{% endif %}
|
| 461 |
+
|
| 462 |
+
<!-- TAB 7: EDUCATION -->
|
| 463 |
+
{% if active_tab == 'education' %}
|
| 464 |
+
<div class="tab-pane fade show active" id="tab-education" role="tabpanel">
|
| 465 |
+
<div class="mb-3">
|
| 466 |
+
<label class="form-label">Chủ đề hoặc khái niệm học tập cần giải thích</label>
|
| 467 |
+
<textarea class="form-control" id="edu_subject" rows="3" placeholder="Ví dụ: Giải thích cơ chế đồng thuận Proof of Work (PoW) trong Blockchain..."></textarea>
|
| 468 |
+
</div>
|
| 469 |
+
<div class="row g-3 mb-3">
|
| 470 |
+
<div class="col-md-6">
|
| 471 |
+
<label class="form-label">Trình độ người học</label>
|
| 472 |
+
<select class="form-select" id="edu_level">
|
| 473 |
+
<option value="Người mới bắt đầu chưa biết gì, giải thích cực kỳ đơn giản (ELI5)" selected>Người mới bắt đầu (Dễ nhất)</option>
|
| 474 |
+
<option value="Học sinh cấp 2 / cấp 3 (Trung bình)">Học sinh Trung học</option>
|
| 475 |
+
<option value="Sinh viên Đại học hoặc người nghiên cứu học thuật">Đại học / Học thuật</option>
|
| 476 |
+
<option value="Chuyên gia hoặc lập trình viên chuyên nghiệp cần đi sâu bản chất">Chuyên gia / Chuyên sâu</option>
|
| 477 |
+
</select>
|
| 478 |
+
</div>
|
| 479 |
+
<div class="col-md-6">
|
| 480 |
+
<label class="form-label">Phương pháp sư phạm</label>
|
| 481 |
+
<select class="form-select" id="edu_method">
|
| 482 |
+
<option value="Sử dụng các hình ảnh ẩn dụ thực tế gần gũi (Analogy-based)" selected>Hình ảnh ẩn dụ (Analogy)</option>
|
| 483 |
+
<option value="Phương pháp Socratic (Hỏi đáp từng bước kích thích suy nghĩ)">Hỏi đáp Socratic</option>
|
| 484 |
+
<option value="Chia nhỏ khái niệm và giải thích bằng ngôn ngữ không chuyên môn">Ngôn ngữ bình dân (ELI5)</option>
|
| 485 |
+
<option value="Phương pháp Feynman (Giải thích lại cho một đứa trẻ 5 tuổi nghe)">Phương pháp Feynman</option>
|
| 486 |
+
</select>
|
| 487 |
+
</div>
|
| 488 |
+
</div>
|
| 489 |
+
<div class="mb-3">
|
| 490 |
+
<label class="form-label">Mục tiêu học tập cụ thể (Kết quả đầu ra)</label>
|
| 491 |
+
<textarea class="form-control" id="edu_goals" rows="3" placeholder="Ví dụ: Giúp người đọc phân biệt được sự khác nhau giữa PoW và PoS, kèm theo 3 câu hỏi trắc nghiệm tự kiểm tra kiến thức..."></textarea>
|
| 492 |
+
</div>
|
| 493 |
+
</div>
|
| 494 |
+
{% endif %}
|
| 495 |
+
|
| 496 |
+
<!-- TAB 8: DATA ANALYSIS -->
|
| 497 |
+
{% if active_tab == 'data' %}
|
| 498 |
+
<div class="tab-pane fade show active" id="tab-data" role="tabpanel">
|
| 499 |
+
<div class="mb-3">
|
| 500 |
+
<label class="form-label">Nguồn dữ liệu & Cấu trúc (Schema)</label>
|
| 501 |
+
<textarea class="form-control" id="data_source" rows="3" placeholder="Ví dụ: Bảng SQL 'orders' chứa các cột (order_id, user_id, amount, order_date, status)..."></textarea>
|
| 502 |
+
</div>
|
| 503 |
+
<div class="mb-3">
|
| 504 |
+
<label class="form-label">Câu hỏi / Mục tiêu phân tích</label>
|
| 505 |
+
<textarea class="form-control" id="data_goal" rows="3" placeholder="Ví dụ: Tính giá trị đơn hàng trung bình (AOV) hàng tháng và tìm ra top 10% người dùng chi tiêu nhiều nhất trong năm 2025..."></textarea>
|
| 506 |
+
</div>
|
| 507 |
+
<div class="row g-3 mb-3">
|
| 508 |
+
<div class="col-md-6">
|
| 509 |
+
<label class="form-label">Định dạng đầu ra mong muốn</label>
|
| 510 |
+
<select class="form-select" id="data_output">
|
| 511 |
+
<option value="Câu lệnh truy vấn SQL tối ưu (SQL Query)" selected>SQL Query tối ưu</option>
|
| 512 |
+
<option value="Đoạn mã Python sử dụng pandas và matplotlib để trực quan hóa">Python Code (pandas/chart)</option>
|
| 513 |
+
<option value="Báo cáo tóm tắt kinh doanh (Executive Summary) kèm khuyến nghị hành động">Báo cáo tóm tắt kinh doanh</option>
|
| 514 |
+
<option value="Phân tích thống kê chi tiết (mô tả, tương quan, hồi quy)">Phân tích thống kê chi tiết</option>
|
| 515 |
+
</select>
|
| 516 |
+
</div>
|
| 517 |
+
<div class="col-md-6">
|
| 518 |
+
<label class="form-label">Chi tiết yêu cầu ràng buộc</label>
|
| 519 |
+
<input type="text" class="form-control" id="data_details" placeholder="Ví dụ: Sử dụng CTE trong SQL, xử lý giá trị Null...">
|
| 520 |
+
</div>
|
| 521 |
+
</div>
|
| 522 |
+
</div>
|
| 523 |
+
{% endif %}
|
| 524 |
+
|
| 525 |
+
<!-- TAB 9: OFFICE ASSISTANT -->
|
| 526 |
+
{% if active_tab == 'office' %}
|
| 527 |
+
<div class="tab-pane fade show active" id="tab-office" role="tabpanel">
|
| 528 |
+
<div class="mb-3">
|
| 529 |
+
<label class="form-label">Văn bản / Nội dung gốc cần xử lý</label>
|
| 530 |
+
<textarea class="form-control" id="office_text" rows="4" placeholder="Ví dụ: Đoạn email thô của khách hàng phàn nàn về việc giao hàng trễ..."></textarea>
|
| 531 |
+
</div>
|
| 532 |
+
<div class="row g-3 mb-3">
|
| 533 |
+
<div class="col-md-6">
|
| 534 |
+
<label class="form-label">Công việc cần thực hiện</label>
|
| 535 |
+
<select class="form-select" id="office_task">
|
| 536 |
+
<option value="Tóm tắt tài liệu dài thành các ý chính (Summarization)" selected>Tóm tắt ý chính (Summary)</option>
|
| 537 |
+
<option value="Viết một email trả lời lịch sự và chuyên nghiệp (Email drafting)">Soạn email phản hồi</option>
|
| 538 |
+
<option value="Sửa lỗi chính tả, ngữ pháp và tinh chỉnh câu chữ trôi chảy hơn (Proofreading)">Hiệu đính & Sửa lỗi</option>
|
| 539 |
+
<option value="Chuyển đổi định dạng văn bản thành báo cáo, biên bản cuộc họp">Chuyển định dạng (Báo cáo/Biên bản)</option>
|
| 540 |
+
</select>
|
| 541 |
+
</div>
|
| 542 |
+
<div class="col-md-6">
|
| 543 |
+
<label class="form-label">Tông giọng (Tone)</label>
|
| 544 |
+
<select class="form-select" id="office_tone">
|
| 545 |
+
<option value="Trang trọng, lịch sự, chuẩn mực công sở (Professional)" selected>Lịch sự / Chuyên nghiệp</option>
|
| 546 |
+
<option value="Khéo léo, xin lỗi chân thành, tìm hướng giải quyết xoa dịu">Khéo léo / Xin lỗi</option>
|
| 547 |
+
<option value="Ngắn gọn, trực tiếp, tập trung vào số liệu hành động">Ngắn gọn / Trực diện</option>
|
| 548 |
+
<option value="Thân thiện, cởi mở, gắn kết đồng nghiệp">Thân thiện / Đồng nghiệp</option>
|
| 549 |
+
</select>
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
<div class="mb-3">
|
| 553 |
+
<label class="form-label">Hướng dẫn chi tiết thêm (Không bắt buộc)</label>
|
| 554 |
+
<input type="text" class="form-control" id="office_instructions" placeholder="Ví dụ: Tóm tắt dưới 5 gạch đầu dòng, email phản hồi hẹn gặp vào 9h sáng mai...">
|
| 555 |
+
</div>
|
| 556 |
+
</div>
|
| 557 |
+
{% endif %}
|
| 558 |
+
|
| 559 |
+
<!-- TAB 10: BRAIN STORMING -->
|
| 560 |
+
{% if active_tab == 'brainstorm' %}
|
| 561 |
+
<div class="tab-pane fade show active" id="tab-brainstorm" role="tabpanel">
|
| 562 |
+
<div class="mb-3">
|
| 563 |
+
<label class="form-label">Chủ đề hoặc Bài toán cần lên ý tưởng</label>
|
| 564 |
+
<textarea class="form-control" id="brain_topic" rows="3" placeholder="Ví dụ: Lên ý tưởng cho các tính năng mới độc đáo của một ứng dụng quản lý chi tiêu cá nhân nhắm tới thế hệ Gen Z..."></textarea>
|
| 565 |
+
</div>
|
| 566 |
+
<div class="row g-3 mb-3">
|
| 567 |
+
<div class="col-md-6">
|
| 568 |
+
<label class="form-label">Loại ý tưởng mong muốn</label>
|
| 569 |
+
<select class="form-select" id="brain_type">
|
| 570 |
+
<option value="Tính năng sản phẩm độc đáo / Giải pháp công nghệ mới" selected>Tính năng / Giải pháp sản phẩm</option>
|
| 571 |
+
<option value="Ý tưởng marketing, chiến dịch viral, thông điệp truyền thông">Chiến dịch Marketing / Viral</option>
|
| 572 |
+
<option value="Ý tưởng kinh doanh khởi nghiệp sáng tạo (Startup ideas)">Ý tưởng Kinh doanh / Khởi nghiệp</option>
|
| 573 |
+
<option value="Đặt tên thương hiệu, tên sản phẩm, tên miền website">Đặt tên Thương hiệu / Sản phẩm</option>
|
| 574 |
+
<option value="Ý tưởng thiết kế giao diện UI/UX sáng tạo">Ý tưởng Thiết kế / UI/UX</option>
|
| 575 |
+
</select>
|
| 576 |
+
</div>
|
| 577 |
+
<div class="col-md-6">
|
| 578 |
+
<label class="form-label">Phạm vi & Mức độ sáng tạo</label>
|
| 579 |
+
<select class="form-select" id="brain_scope">
|
| 580 |
+
<option value="Sáng tạo đột phá, độc lạ, chưa từng có trên thị trường (Out of the box)" selected>Đột phá / Độc lạ (Out-of-the-box)</option>
|
| 581 |
+
<option value="Thực tế, dễ triển khai, chi phí thấp, tối ưu những cái đã có">Thực tế / Dễ triển khai</option>
|
| 582 |
+
<option value="Ý tưởng mang tính điên rồ, viễn tưởng, truyền cảm hứng mạnh mẽ">Điên rồ / Truyền cảm hứng</option>
|
| 583 |
+
</select>
|
| 584 |
+
</div>
|
| 585 |
+
</div>
|
| 586 |
+
<div class="mb-3">
|
| 587 |
+
<label class="form-label">Yêu cầu chi tiết (Số lượng ý tưởng, định dạng cấu trúc...)</label>
|
| 588 |
+
<textarea class="form-control" id="brain_details" rows="2" placeholder="Ví dụ: Đưa ra ít nhất 10 ý tưởng, sắp xếp từ dễ đến khó thực hiện..."></textarea>
|
| 589 |
+
</div>
|
| 590 |
+
</div>
|
| 591 |
+
{% endif %}
|
| 592 |
+
|
| 593 |
+
<!-- TAB 11: CUSTOM ENHANCER -->
|
| 594 |
+
{% if active_tab == 'enhancer' %}
|
| 595 |
+
<div class="tab-pane fade show active" id="tab-enhancer" role="tabpanel">
|
| 596 |
+
<div class="mb-3">
|
| 597 |
+
<label class="form-label">Nhập Prompt thô cần tối ưu</label>
|
| 598 |
+
<textarea class="form-control" id="raw_prompt" rows="5" placeholder="Ví dụ: Viết cho tôi một bài đăng facebook quảng cáo khóa học lập trình web cho người mới bắt đầu..."></textarea>
|
| 599 |
+
</div>
|
| 600 |
+
<div class="row g-3 mb-3">
|
| 601 |
+
<div class="col-md-6">
|
| 602 |
+
<label class="form-label">AI đích sử dụng prompt này</label>
|
| 603 |
+
<select class="form-select" id="target_ai">
|
| 604 |
+
<option value="ChatGPT / Claude (Xử lý văn bản chung)" selected>ChatGPT / Claude</option>
|
| 605 |
+
<option value="Midjourney (Vẽ tranh nghệ thuật)">Midjourney</option>
|
| 606 |
+
<option value="Suno / Udio (Sáng tác nhạc)">Suno / Udio</option>
|
| 607 |
+
<option value="Runway / Sora / Kling (Tạo video)">Runway / Sora / Kling</option>
|
| 608 |
+
<option value="Stable Diffusion / DALL-E 3">Stable Diffusion / DALL-E 3</option>
|
| 609 |
+
<option value="Gemini AI (Tích hợp phân tích dữ liệu)">Gemini AI</option>
|
| 610 |
+
</select>
|
| 611 |
+
</div>
|
| 612 |
+
<div class="col-md-6">
|
| 613 |
+
<label class="form-label">Hướng dẫn tối ưu thêm (Không bắt buộc)</label>
|
| 614 |
+
<input type="text" class="form-control" id="custom_instructions" placeholder="Ví dụ: Dịch sang tiếng Anh, thêm cấu trúc bảng...">
|
| 615 |
+
</div>
|
| 616 |
+
</div>
|
| 617 |
+
</div>
|
| 618 |
+
{% endif %}
|
| 619 |
+
|
| 620 |
+
</div>
|
| 621 |
+
</div>
|
| 622 |
+
|
| 623 |
+
<!-- Global configurations & action button -->
|
| 624 |
+
<div class="mt-4 border-top pt-4">
|
| 625 |
+
<div class="row g-3 align-items-center mb-3">
|
| 626 |
+
<div class="col-md-6">
|
| 627 |
+
<label class="form-label" style="color: var(--color-primary)"><i class="fab fa-google"></i> Mô hình Gemini</label>
|
| 628 |
+
<select id="geminiModel" class="form-select" style="border-color: var(--color-primary)">
|
| 629 |
+
<option value="">Đang tải...</option>
|
| 630 |
+
</select>
|
| 631 |
+
</div>
|
| 632 |
+
<div class="col-md-6">
|
| 633 |
+
<label class="form-label">Trạng thái quota</label>
|
| 634 |
+
<div id="modelQuota" class="small text-muted" style="min-height: 24px;"></div>
|
| 635 |
+
</div>
|
| 636 |
+
</div>
|
| 637 |
+
|
| 638 |
+
<button id="generateBtn" class="btn-primary-custom" style="padding: 12px 20px;"><i class="fas fa-magic me-2"></i>TẠO PROMPT TỐI ƯU</button>
|
| 639 |
+
</div>
|
| 640 |
+
</div>
|
| 641 |
+
</div>
|
| 642 |
+
</div>
|
| 643 |
+
|
| 644 |
+
<!-- Right Column: Results -->
|
| 645 |
+
<div class="col-lg-6">
|
| 646 |
+
<div class="d-flex flex-column gap-4 h-100">
|
| 647 |
+
<!-- Result Panel 1: Generated Prompt -->
|
| 648 |
+
<div class="card prompt-card flex-grow-1">
|
| 649 |
+
<div class="card-body p-4 d-flex flex-column h-100">
|
| 650 |
+
<div class="d-flex justify-content-between align-items-center mb-3 border-bottom pb-2">
|
| 651 |
+
<h5 class="m-0 fw-bold" style="color: var(--color-primary)"><i class="fas fa-terminal"></i> Prompt Kết Quả</h5>
|
| 652 |
+
<span id="categoryBadge" class="badge-tab">
|
| 653 |
+
{% if active_tab == 'code' %}Code
|
| 654 |
+
{% elif active_tab == 'video' %}Ảnh/Video AI
|
| 655 |
+
{% elif active_tab == 'music' %}Nhạc AI
|
| 656 |
+
{% elif active_tab == 'story' %}Truyện
|
| 657 |
+
{% elif active_tab == 'video_script' %}Kịch bản Video
|
| 658 |
+
{% elif active_tab == 'marketing' %}Marketing
|
| 659 |
+
{% elif active_tab == 'education' %}Học tập
|
| 660 |
+
{% elif active_tab == 'data' %}Phân tích dữ liệu
|
| 661 |
+
{% elif active_tab == 'office' %}Trợ lý văn phòng
|
| 662 |
+
{% elif active_tab == 'brainstorm' %}Lên ý tưởng
|
| 663 |
+
{% elif active_tab == 'enhancer' %}Tối ưu
|
| 664 |
+
{% endif %}
|
| 665 |
+
</span>
|
| 666 |
+
</div>
|
| 667 |
+
|
| 668 |
+
<textarea id="outputPrompt" class="form-control prompt-textarea flex-grow-1 mb-3" rows="12" placeholder="Prompt tối ưu sẽ xuất hiện ở đây..." readonly></textarea>
|
| 669 |
+
|
| 670 |
+
<div class="d-flex gap-2">
|
| 671 |
+
<button id="copyBtn" class="btn btn-outline-primary w-50" disabled>
|
| 672 |
+
<i class="fas fa-copy me-2"></i>Sao chép Prompt
|
| 673 |
+
</button>
|
| 674 |
+
<button id="executeBtn" class="btn btn-success w-50" disabled>
|
| 675 |
+
<i class="fas fa-play me-2"></i>Chạy thử Prompt
|
| 676 |
+
</button>
|
| 677 |
+
</div>
|
| 678 |
+
</div>
|
| 679 |
+
</div>
|
| 680 |
+
|
| 681 |
+
<!-- Result Panel 2: Live Execution Result -->
|
| 682 |
+
<div id="executionCard" class="card prompt-card d-none">
|
| 683 |
+
<div class="card-body p-4">
|
| 684 |
+
<h5 class="panel-title text-success mb-3 pb-2"><i class="fas fa-check-circle text-success"></i> Kết quả chạy thử</h5>
|
| 685 |
+
<div id="executionResult" class="result-box">Kết quả phản hồi của Gemini khi chạy prompt này sẽ hiển thị ở đây...</div>
|
| 686 |
+
</div>
|
| 687 |
+
</div>
|
| 688 |
+
</div>
|
| 689 |
+
</div>
|
| 690 |
+
</div>
|
| 691 |
+
|
| 692 |
+
<div id="errorArea" class="mt-4 d-none">
|
| 693 |
+
<div class="alert alert-danger d-flex align-items-center gap-2 mb-0">
|
| 694 |
+
<i class="fas fa-exclamation-circle"></i>
|
| 695 |
+
<span id="errorMsg"></span>
|
| 696 |
+
</div>
|
| 697 |
+
</div>
|
| 698 |
+
{% endblock %}
|
| 699 |
+
|
| 700 |
+
{% block scripts %}
|
| 701 |
+
<script>
|
| 702 |
+
let activeCategory = '{{ active_tab }}';
|
| 703 |
+
let geminiModelList = [];
|
| 704 |
+
|
| 705 |
+
// Load danh sách model Gemini
|
| 706 |
+
async function loadGeminiModels() {
|
| 707 |
+
try {
|
| 708 |
+
const res = await fetch('/api/gemini-models');
|
| 709 |
+
geminiModelList = await res.json();
|
| 710 |
+
const sel = document.getElementById('geminiModel');
|
| 711 |
+
sel.innerHTML = '';
|
| 712 |
+
geminiModelList.forEach(m => {
|
| 713 |
+
const opt = document.createElement('option');
|
| 714 |
+
opt.value = m.id;
|
| 715 |
+
const quotaText = m.remaining > 0
|
| 716 |
+
? `✓ Còn ${m.remaining.toLocaleString()}/${m.rpd.toLocaleString()}`
|
| 717 |
+
: `✗ Hết (${m.used}/${m.rpd})`;
|
| 718 |
+
opt.textContent = `${m.name} — ${quotaText}`;
|
| 719 |
+
if (m.remaining === 0) opt.disabled = true;
|
| 720 |
+
sel.appendChild(opt);
|
| 721 |
+
});
|
| 722 |
+
updateModelQuota();
|
| 723 |
+
} catch(e) {
|
| 724 |
+
console.error('Không load được danh sách model:', e);
|
| 725 |
+
}
|
| 726 |
+
}
|
| 727 |
+
|
| 728 |
+
function updateModelQuota() {
|
| 729 |
+
const sel = document.getElementById('geminiModel');
|
| 730 |
+
const chosen = geminiModelList.find(m => m.id === sel.value);
|
| 731 |
+
const el = document.getElementById('modelQuota');
|
| 732 |
+
if (chosen) {
|
| 733 |
+
const status = chosen.remaining > 0
|
| 734 |
+
? `<span style="color: var(--color-primary)"><i class="fas fa-check-circle"></i> Còn <strong>${chosen.remaining.toLocaleString()}</strong> request hôm nay</span>`
|
| 735 |
+
: `<span style="color: #dc3545"><i class="fas fa-exclamation-triangle"></i> Đã hết quota (${chosen.used}/${chosen.rpd})</span>`;
|
| 736 |
+
el.innerHTML = status;
|
| 737 |
+
}
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
document.getElementById('geminiModel').addEventListener('change', updateModelQuota);
|
| 741 |
+
loadGeminiModels();
|
| 742 |
+
|
| 743 |
+
function showError(msg) {
|
| 744 |
+
const area = document.getElementById('errorArea');
|
| 745 |
+
document.getElementById('errorMsg').textContent = msg;
|
| 746 |
+
area.classList.remove('d-none');
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
function clearError() {
|
| 750 |
+
document.getElementById('errorArea').classList.add('d-none');
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
// Nút Tạo Prompt
|
| 754 |
+
document.getElementById('generateBtn').addEventListener('click', async () => {
|
| 755 |
+
const loader = document.getElementById('loaderOverlay');
|
| 756 |
+
const loaderText = document.getElementById('loaderText');
|
| 757 |
+
loaderText.textContent = 'Đang tối ưu prompt bằng Gemini...';
|
| 758 |
+
loader.style.display = 'flex';
|
| 759 |
+
clearError();
|
| 760 |
+
|
| 761 |
+
// Ẩn bảng chạy thử cũ
|
| 762 |
+
document.getElementById('executionCard').classList.add('d-none');
|
| 763 |
+
document.getElementById('executionResult').textContent = '';
|
| 764 |
+
|
| 765 |
+
// Thu thập payload tùy theo tab active
|
| 766 |
+
let payload = {
|
| 767 |
+
category: activeCategory,
|
| 768 |
+
gemini_model: document.getElementById('geminiModel').value
|
| 769 |
+
};
|
| 770 |
+
|
| 771 |
+
if (activeCategory === 'code') {
|
| 772 |
+
payload.code_task = document.getElementById('code_task').value;
|
| 773 |
+
payload.code_lang = document.getElementById('code_lang').value;
|
| 774 |
+
payload.code_framework = document.getElementById('code_framework').value;
|
| 775 |
+
payload.code_style = document.getElementById('code_style').value;
|
| 776 |
+
payload.code_requirements = document.getElementById('code_requirements').value;
|
| 777 |
+
|
| 778 |
+
if (!payload.code_task.trim()) {
|
| 779 |
+
loader.style.display = 'none';
|
| 780 |
+
showError('Vui lòng mô tả tính năng/công việc cần code!');
|
| 781 |
+
return;
|
| 782 |
+
}
|
| 783 |
+
} else if (activeCategory === 'video') {
|
| 784 |
+
payload.video_topic = document.getElementById('video_topic').value;
|
| 785 |
+
payload.video_style = document.getElementById('video_style').value;
|
| 786 |
+
payload.video_shot = document.getElementById('video_shot').value;
|
| 787 |
+
payload.video_lighting = document.getElementById('video_lighting').value;
|
| 788 |
+
payload.video_ratio = document.getElementById('video_ratio').value;
|
| 789 |
+
payload.video_details = document.getElementById('video_details').value;
|
| 790 |
+
|
| 791 |
+
if (!payload.video_topic.trim()) {
|
| 792 |
+
loader.style.display = 'none';
|
| 793 |
+
showError('Vui lòng mô tả ý tưởng/concept hình ảnh!');
|
| 794 |
+
return;
|
| 795 |
+
}
|
| 796 |
+
} else if (activeCategory === 'music') {
|
| 797 |
+
payload.music_theme = document.getElementById('music_theme').value;
|
| 798 |
+
payload.music_genre = document.getElementById('music_genre').value;
|
| 799 |
+
payload.music_mood = document.getElementById('music_mood').value;
|
| 800 |
+
payload.music_instruments = document.getElementById('music_instruments').value;
|
| 801 |
+
payload.music_vocals = document.getElementById('music_vocals').value;
|
| 802 |
+
payload.music_lyrics_idea = document.getElementById('music_lyrics_idea').value;
|
| 803 |
+
|
| 804 |
+
if (!payload.music_theme.trim()) {
|
| 805 |
+
loader.style.display = 'none';
|
| 806 |
+
showError('Vui lòng nhập chủ đề/ý tưởng sáng tác nhạc!');
|
| 807 |
+
return;
|
| 808 |
+
}
|
| 809 |
+
} else if (activeCategory === 'story') {
|
| 810 |
+
payload.story_title = document.getElementById('story_title').value;
|
| 811 |
+
payload.story_genre = document.getElementById('story_genre').value;
|
| 812 |
+
payload.story_characters = document.getElementById('story_characters').value;
|
| 813 |
+
payload.story_plot = document.getElementById('story_plot').value;
|
| 814 |
+
payload.story_tone = document.getElementById('story_tone').value;
|
| 815 |
+
payload.story_length = document.getElementById('story_length').value;
|
| 816 |
+
|
| 817 |
+
if (!payload.story_plot.trim()) {
|
| 818 |
+
loader.style.display = 'none';
|
| 819 |
+
showError('Vui lòng tóm tắt cốt truyện!');
|
| 820 |
+
return;
|
| 821 |
+
}
|
| 822 |
+
} else if (activeCategory === 'video_script') {
|
| 823 |
+
payload.script_topic = document.getElementById('script_topic').value;
|
| 824 |
+
payload.script_platform = document.getElementById('script_platform').value;
|
| 825 |
+
payload.script_duration = document.getElementById('script_duration').value;
|
| 826 |
+
payload.script_structure = document.getElementById('script_structure').value;
|
| 827 |
+
payload.script_tone = document.getElementById('script_tone').value;
|
| 828 |
+
payload.script_cta = document.getElementById('script_cta').value;
|
| 829 |
+
|
| 830 |
+
if (!payload.script_topic.trim()) {
|
| 831 |
+
loader.style.display = 'none';
|
| 832 |
+
showError('Vui lòng mô tả chủ đề video!');
|
| 833 |
+
return;
|
| 834 |
+
}
|
| 835 |
+
} else if (activeCategory === 'marketing') {
|
| 836 |
+
payload.marketing_topic = document.getElementById('marketing_topic').value;
|
| 837 |
+
payload.marketing_platform = document.getElementById('marketing_platform').value;
|
| 838 |
+
payload.marketing_tone = document.getElementById('marketing_tone').value;
|
| 839 |
+
payload.marketing_cta = document.getElementById('marketing_cta').value;
|
| 840 |
+
payload.marketing_details = document.getElementById('marketing_details').value;
|
| 841 |
+
|
| 842 |
+
if (!payload.marketing_topic.trim()) {
|
| 843 |
+
loader.style.display = 'none';
|
| 844 |
+
showError('Vui lòng mô tả sản phẩm/chủ đề quảng cáo!');
|
| 845 |
+
return;
|
| 846 |
+
}
|
| 847 |
+
} else if (activeCategory === 'education') {
|
| 848 |
+
payload.edu_subject = document.getElementById('edu_subject').value;
|
| 849 |
+
payload.edu_level = document.getElementById('edu_level').value;
|
| 850 |
+
payload.edu_method = document.getElementById('edu_method').value;
|
| 851 |
+
payload.edu_goals = document.getElementById('edu_goals').value;
|
| 852 |
+
|
| 853 |
+
if (!payload.edu_subject.trim()) {
|
| 854 |
+
loader.style.display = 'none';
|
| 855 |
+
showError('Vui lòng mô tả khái niệm cần giải thích!');
|
| 856 |
+
return;
|
| 857 |
+
}
|
| 858 |
+
} else if (activeCategory === 'data') {
|
| 859 |
+
payload.data_source = document.getElementById('data_source').value;
|
| 860 |
+
payload.data_goal = document.getElementById('data_goal').value;
|
| 861 |
+
payload.data_output = document.getElementById('data_output').value;
|
| 862 |
+
payload.data_details = document.getElementById('data_details').value;
|
| 863 |
+
|
| 864 |
+
if (!payload.data_goal.trim()) {
|
| 865 |
+
loader.style.display = 'none';
|
| 866 |
+
showError('Vui lòng nhập câu hỏi hoặc mục tiêu phân tích!');
|
| 867 |
+
return;
|
| 868 |
+
}
|
| 869 |
+
} else if (activeCategory === 'office') {
|
| 870 |
+
payload.office_text = document.getElementById('office_text').value;
|
| 871 |
+
payload.office_task = document.getElementById('office_task').value;
|
| 872 |
+
payload.office_tone = document.getElementById('office_tone').value;
|
| 873 |
+
payload.office_instructions = document.getElementById('office_instructions').value;
|
| 874 |
+
|
| 875 |
+
if (!payload.office_text.trim()) {
|
| 876 |
+
loader.style.display = 'none';
|
| 877 |
+
showError('Vui lòng nhập văn bản gốc cần xử lý!');
|
| 878 |
+
return;
|
| 879 |
+
}
|
| 880 |
+
} else if (activeCategory === 'brainstorm') {
|
| 881 |
+
payload.brain_topic = document.getElementById('brain_topic').value;
|
| 882 |
+
payload.brain_type = document.getElementById('brain_type').value;
|
| 883 |
+
payload.brain_scope = document.getElementById('brain_scope').value;
|
| 884 |
+
payload.brain_details = document.getElementById('brain_details').value;
|
| 885 |
+
|
| 886 |
+
if (!payload.brain_topic.trim()) {
|
| 887 |
+
loader.style.display = 'none';
|
| 888 |
+
showError('Vui lòng mô tả bài toán cần lên ý tưởng!');
|
| 889 |
+
return;
|
| 890 |
+
}
|
| 891 |
+
} else if (activeCategory === 'enhancer') {
|
| 892 |
+
payload.raw_prompt = document.getElementById('raw_prompt').value;
|
| 893 |
+
payload.target_ai = document.getElementById('target_ai').value;
|
| 894 |
+
payload.custom_instructions = document.getElementById('custom_instructions').value;
|
| 895 |
+
|
| 896 |
+
if (!payload.raw_prompt.trim()) {
|
| 897 |
+
loader.style.display = 'none';
|
| 898 |
+
showError('Vui lòng nhập prompt thô cần tối ưu!');
|
| 899 |
+
return;
|
| 900 |
+
}
|
| 901 |
+
}
|
| 902 |
+
|
| 903 |
+
try {
|
| 904 |
+
const res = await fetch('/api/prompt-generator/generate', {
|
| 905 |
+
method: 'POST',
|
| 906 |
+
headers: { 'Content-Type': 'application/json' },
|
| 907 |
+
body: JSON.stringify(payload)
|
| 908 |
+
});
|
| 909 |
+
const data = await res.json();
|
| 910 |
+
if (data.error || !data.success) {
|
| 911 |
+
showError(data.error || 'Có lỗi xảy ra khi tạo prompt.');
|
| 912 |
+
} else {
|
| 913 |
+
const outputArea = document.getElementById('outputPrompt');
|
| 914 |
+
outputArea.value = data.generated_prompt;
|
| 915 |
+
outputArea.removeAttribute('readonly'); // Cho phép người dùng tự chỉnh sửa
|
| 916 |
+
|
| 917 |
+
// Enable các nút hành động
|
| 918 |
+
document.getElementById('copyBtn').removeAttribute('disabled');
|
| 919 |
+
document.getElementById('executeBtn').removeAttribute('disabled');
|
| 920 |
+
|
| 921 |
+
await loadGeminiModels(); // Cập nhật lại hạn ngạch quota
|
| 922 |
+
}
|
| 923 |
+
} catch(e) {
|
| 924 |
+
showError('Lỗi kết nối máy chủ. Vui lòng thử lại sau.');
|
| 925 |
+
} finally {
|
| 926 |
+
loader.style.display = 'none';
|
| 927 |
+
}
|
| 928 |
+
});
|
| 929 |
+
|
| 930 |
+
// Nút Copy Prompt
|
| 931 |
+
document.getElementById('copyBtn').addEventListener('click', () => {
|
| 932 |
+
const text = document.getElementById('outputPrompt').value;
|
| 933 |
+
if (!text) return;
|
| 934 |
+
|
| 935 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 936 |
+
const btn = document.getElementById('copyBtn');
|
| 937 |
+
const originalHTML = btn.innerHTML;
|
| 938 |
+
btn.innerHTML = '<i class="fas fa-check me-2"></i>Đã sao chép!';
|
| 939 |
+
btn.classList.replace('btn-outline-primary', 'btn-primary');
|
| 940 |
+
setTimeout(() => {
|
| 941 |
+
btn.innerHTML = originalHTML;
|
| 942 |
+
btn.classList.replace('btn-primary', 'btn-outline-primary');
|
| 943 |
+
}, 2000);
|
| 944 |
+
}).catch(err => {
|
| 945 |
+
showError('Không thể sao chép tự động. Hãy chọn toàn bộ text và nhấn Ctrl+C.');
|
| 946 |
+
});
|
| 947 |
+
});
|
| 948 |
+
|
| 949 |
+
// Nút Chạy thử Prompt
|
| 950 |
+
document.getElementById('executeBtn').addEventListener('click', async () => {
|
| 951 |
+
const promptText = document.getElementById('outputPrompt').value;
|
| 952 |
+
if (!promptText.trim()) return;
|
| 953 |
+
|
| 954 |
+
const loader = document.getElementById('loaderOverlay');
|
| 955 |
+
const loaderText = document.getElementById('loaderText');
|
| 956 |
+
loaderText.textContent = 'Đang chạy thử prompt của bạn với Gemini...';
|
| 957 |
+
loader.style.display = 'flex';
|
| 958 |
+
clearError();
|
| 959 |
+
|
| 960 |
+
try {
|
| 961 |
+
const res = await fetch('/api/prompt-generator/execute', {
|
| 962 |
+
method: 'POST',
|
| 963 |
+
headers: { 'Content-Type': 'application/json' },
|
| 964 |
+
body: JSON.stringify({
|
| 965 |
+
prompt: promptText,
|
| 966 |
+
gemini_model: document.getElementById('geminiModel').value
|
| 967 |
+
})
|
| 968 |
+
});
|
| 969 |
+
const data = await res.json();
|
| 970 |
+
if (data.error || !data.success) {
|
| 971 |
+
showError(data.error || 'Có lỗi xảy ra khi chạy thử prompt.');
|
| 972 |
+
} else {
|
| 973 |
+
document.getElementById('executionResult').textContent = data.execution_result;
|
| 974 |
+
document.getElementById('executionCard').classList.remove('d-none');
|
| 975 |
+
|
| 976 |
+
// Cuộn xuống khu vực hiển thị kết quả
|
| 977 |
+
document.getElementById('executionCard').scrollIntoView({ behavior: 'smooth' });
|
| 978 |
+
|
| 979 |
+
await loadGeminiModels(); // Cập nhật lại hạn ngạch quota
|
| 980 |
+
}
|
| 981 |
+
} catch(e) {
|
| 982 |
+
showError('Lỗi kết nối máy chủ khi chạy thử.');
|
| 983 |
+
} finally {
|
| 984 |
+
loader.style.display = 'none';
|
| 985 |
+
}
|
| 986 |
+
});
|
| 987 |
+
</script>
|
| 988 |
+
{% endblock %}
|
templates/tools/video_ai/index.html
ADDED
|
@@ -0,0 +1,627 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "layout.html" %}
|
| 2 |
+
{% block title %}Video AI Studio - Pum's Toolkit{% endblock %}
|
| 3 |
+
|
| 4 |
+
{% block styles %}
|
| 5 |
+
<style>
|
| 6 |
+
.video-layout { display:grid; grid-template-columns:400px 1fr; gap:1.5rem; align-items:start; }
|
| 7 |
+
@media(max-width:900px){ .video-layout{ grid-template-columns:1fr; } }
|
| 8 |
+
|
| 9 |
+
.option-group { background:var(--color-bg-input); border:1px solid var(--color-border);
|
| 10 |
+
border-radius:var(--radius-lg); padding:1rem 1.2rem; margin-bottom:1rem; }
|
| 11 |
+
.option-group-title { font-size:.72rem; font-weight:700; text-transform:uppercase;
|
| 12 |
+
letter-spacing:.06em; color:var(--color-text-muted); margin-bottom:.75rem; }
|
| 13 |
+
|
| 14 |
+
.style-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:6px; }
|
| 15 |
+
.style-card { border:1.5px solid var(--color-border); border-radius:var(--radius-md);
|
| 16 |
+
padding:8px 4px; text-align:center; cursor:pointer; transition:all .15s;
|
| 17 |
+
background:#fff; font-size:.72rem; font-weight:600; color:var(--color-text-muted); }
|
| 18 |
+
.style-card:hover { border-color:var(--color-primary); color:var(--color-primary); background:var(--color-primary-hover); }
|
| 19 |
+
.style-card.active { border-color:var(--color-primary); background:var(--color-primary); color:#fff; }
|
| 20 |
+
.style-card .icon { font-size:1.2rem; display:block; margin-bottom:3px; }
|
| 21 |
+
|
| 22 |
+
.result-panel { background:var(--color-bg-input); border:1px solid var(--color-border);
|
| 23 |
+
border-radius:var(--radius-lg); min-height:400px;
|
| 24 |
+
display:flex; align-items:center; justify-content:center; flex-direction:column; }
|
| 25 |
+
.result-panel.has-content { align-items:stretch; justify-content:flex-start; }
|
| 26 |
+
|
| 27 |
+
.video-box { background:#000; border-radius:var(--radius-lg); overflow:hidden; position:relative; }
|
| 28 |
+
.video-box video { width:100%; display:block; }
|
| 29 |
+
|
| 30 |
+
.script-box { background:#fff; border:1px solid var(--color-border); border-radius:var(--radius-md);
|
| 31 |
+
padding:1rem; font-size:.82rem; line-height:1.7; color:var(--color-text-body); white-space:pre-wrap; }
|
| 32 |
+
|
| 33 |
+
.progress-step { display:flex; align-items:center; gap:10px; padding:8px 0;
|
| 34 |
+
font-size:.82rem; color:var(--color-text-muted); }
|
| 35 |
+
.progress-step .step-icon { width:28px; height:28px; border-radius:50%;
|
| 36 |
+
display:flex; align-items:center; justify-content:center; font-size:.75rem; flex-shrink:0; }
|
| 37 |
+
.progress-step.done .step-icon { background:#dcfce7; color:#16a34a; }
|
| 38 |
+
.progress-step.active .step-icon { background:var(--color-primary-light); color:var(--color-primary); animation:pulse 1.5s infinite; }
|
| 39 |
+
.progress-step.pending .step-icon { background:var(--color-bg-input); color:var(--color-text-disabled); }
|
| 40 |
+
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
|
| 41 |
+
|
| 42 |
+
.api-section { background:#fefce8; border:1px solid #fde68a; border-radius:var(--radius-md);
|
| 43 |
+
padding:.8rem 1rem; font-size:.78rem; }
|
| 44 |
+
</style>
|
| 45 |
+
{% endblock %}
|
| 46 |
+
|
| 47 |
+
{% block content %}
|
| 48 |
+
<div class="card card-custom">
|
| 49 |
+
<div class="card-header bg-white border-0 pt-4 text-center">
|
| 50 |
+
<h3 class="fw-bold" style="color:var(--color-primary)">VIDEO AI STUDIO</h3>
|
| 51 |
+
<p class="text-muted">Tạo video AI từ văn bản với kịch bản, giọng đọc và hiệu ứng</p>
|
| 52 |
+
</div>
|
| 53 |
+
|
| 54 |
+
<div class="card-body p-4">
|
| 55 |
+
<div class="video-layout">
|
| 56 |
+
|
| 57 |
+
<!-- ── LEFT: Cấu hình ── -->
|
| 58 |
+
<div>
|
| 59 |
+
|
| 60 |
+
<!-- Chủ đề -->
|
| 61 |
+
<div class="option-group">
|
| 62 |
+
<div class="option-group-title"><i class="fas fa-pen me-1"></i>Chủ đề / Nội dung</div>
|
| 63 |
+
<textarea id="videoPrompt" class="form-control" rows="4"
|
| 64 |
+
placeholder="Mô tả video bạn muốn tạo... VD: Cảnh hoàng hôn trên biển với sóng vỗ nhẹ nhàng, ánh sáng vàng ấm áp"></textarea>
|
| 65 |
+
</div>
|
| 66 |
+
|
| 67 |
+
<!-- Phong cách -->
|
| 68 |
+
<div class="option-group">
|
| 69 |
+
<div class="option-group-title"><i class="fas fa-palette me-1"></i>Phong cách</div>
|
| 70 |
+
<div class="style-grid" id="styleGrid">
|
| 71 |
+
<div class="style-card active" data-style="cinematic"><span class="icon">🎬</span>Điện ảnh</div>
|
| 72 |
+
<div class="style-card" data-style="anime"><span class="icon">🎌</span>Anime</div>
|
| 73 |
+
<div class="style-card" data-style="realistic"><span class="icon">📷</span>Thực tế</div>
|
| 74 |
+
<div class="style-card" data-style="nature"><span class="icon">🌿</span>Thiên nhiên</div>
|
| 75 |
+
<div class="style-card" data-style="tech"><span class="icon">🤖</span>Cyberpunk</div>
|
| 76 |
+
<div class="style-card" data-style="space"><span class="icon">🚀</span>Vũ trụ</div>
|
| 77 |
+
<div class="style-card" data-style="watercolor"><span class="icon">🎨</span>Tranh màu</div>
|
| 78 |
+
<div class="style-card" data-style="vintage"><span class="icon">🎞️</span>Vintage</div>
|
| 79 |
+
</div>
|
| 80 |
+
</div>
|
| 81 |
+
|
| 82 |
+
<!-- Cài đặt cơ bản -->
|
| 83 |
+
<div class="option-group">
|
| 84 |
+
<div class="option-group-title"><i class="fas fa-sliders-h me-1"></i>Cài đặt</div>
|
| 85 |
+
<div class="row g-2">
|
| 86 |
+
<div class="col-6">
|
| 87 |
+
<label class="form-label small fw-semibold">Tỷ lệ khung hình</label>
|
| 88 |
+
<select id="videoRatio" class="form-select form-select-sm">
|
| 89 |
+
<option value="16:9">16:9 — Ngang (YouTube)</option>
|
| 90 |
+
<option value="9:16">9:16 — Dọc (TikTok/Reels)</option>
|
| 91 |
+
<option value="1:1">1:1 — Vuông (Instagram)</option>
|
| 92 |
+
<option value="4:3">4:3 — Cổ điển</option>
|
| 93 |
+
</select>
|
| 94 |
+
</div>
|
| 95 |
+
<div class="col-6">
|
| 96 |
+
<label class="form-label small fw-semibold">Thời lượng</label>
|
| 97 |
+
<select id="videoDuration" class="form-select form-select-sm">
|
| 98 |
+
<option value="5" selected>5 giây</option>
|
| 99 |
+
<option value="10">10 giây</option>
|
| 100 |
+
<option value="15">15 giây</option>
|
| 101 |
+
</select>
|
| 102 |
+
</div>
|
| 103 |
+
<div class="col-6">
|
| 104 |
+
<label class="form-label small fw-semibold">Chất lượng</label>
|
| 105 |
+
<select id="videoQuality" class="form-select form-select-sm">
|
| 106 |
+
<option value="480p" selected>480p (Nhanh)</option>
|
| 107 |
+
<option value="720p">720p (HD)</option>
|
| 108 |
+
</select>
|
| 109 |
+
</div>
|
| 110 |
+
<div class="col-6">
|
| 111 |
+
<label class="form-label small fw-semibold">Nhạc nền</label>
|
| 112 |
+
<select id="videoMusic" class="form-select form-select-sm">
|
| 113 |
+
<option value="none">Không có</option>
|
| 114 |
+
<option value="calm">🎵 Yên bình</option>
|
| 115 |
+
<option value="upbeat">🎶 Sôi động</option>
|
| 116 |
+
<option value="epic">🎺 Hào hùng</option>
|
| 117 |
+
<option value="ambient">🌊 Ambient</option>
|
| 118 |
+
</select>
|
| 119 |
+
</div>
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
|
| 123 |
+
<!-- Giọng đọc -->
|
| 124 |
+
<div class="option-group">
|
| 125 |
+
<div class="option-group-title"><i class="fas fa-microphone me-1"></i>Giọng đọc dẫn truyện</div>
|
| 126 |
+
<div class="row g-2">
|
| 127 |
+
<div class="col-5">
|
| 128 |
+
<label class="form-label small fw-semibold">Ngôn ngữ</label>
|
| 129 |
+
<select id="narrateLang" class="form-select form-select-sm">
|
| 130 |
+
<option value="vi">🇻🇳 Tiếng Việt</option>
|
| 131 |
+
<option value="en">🇺🇸 English</option>
|
| 132 |
+
<option value="ja">🇯🇵 日本語</option>
|
| 133 |
+
<option value="ko">🇰🇷 한국어</option>
|
| 134 |
+
<option value="zh">🇨🇳 中文</option>
|
| 135 |
+
<option value="fr">🇫🇷 Français</option>
|
| 136 |
+
<option value="de">🇩🇪 Deutsch</option>
|
| 137 |
+
</select>
|
| 138 |
+
</div>
|
| 139 |
+
<div class="col-7">
|
| 140 |
+
<label class="form-label small fw-semibold">Giọng đọc</label>
|
| 141 |
+
<select id="narrateVoice" class="form-select form-select-sm"></select>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="col-12">
|
| 144 |
+
<div class="form-check form-switch">
|
| 145 |
+
<input class="form-check-input" type="checkbox" id="narrateEnable" checked>
|
| 146 |
+
<label class="form-check-label small" for="narrateEnable">Bật giọng đọc dẫn truyện</label>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<!-- Gemini model -->
|
| 153 |
+
<div class="option-group">
|
| 154 |
+
<div class="option-group-title"><i class="fas fa-brain me-1"></i>Mô hình AI viết kịch bản</div>
|
| 155 |
+
<select id="geminiModel" class="form-select form-select-sm">
|
| 156 |
+
<option value="">Đang tải...</option>
|
| 157 |
+
</select>
|
| 158 |
+
<div id="geminiQuota" class="mt-1" style="font-size:.72rem;color:var(--color-text-muted)"></div>
|
| 159 |
+
</div>
|
| 160 |
+
|
| 161 |
+
<!-- Video AI Generation -->
|
| 162 |
+
<div class="option-group">
|
| 163 |
+
<div class="option-group-title"><i class="fas fa-video me-1"></i>Mô hình sinh video AI</div>
|
| 164 |
+
<select id="replicateModel" class="form-select form-select-sm mb-2"></select>
|
| 165 |
+
|
| 166 |
+
<div id="modelConfigArea">
|
| 167 |
+
<!-- Replicate token input group -->
|
| 168 |
+
<div id="replicateTokenGroup" class="d-none">
|
| 169 |
+
<div class="api-section mb-2">
|
| 170 |
+
<i class="fas fa-info-circle me-1 text-warning"></i>
|
| 171 |
+
Mô hình trả phí. Nhập <strong>Replicate API token</strong> từ <a href="https://replicate.com" target="_blank">replicate.com</a>.
|
| 172 |
+
</div>
|
| 173 |
+
<input type="password" id="replicateToken" class="form-control form-control-sm mb-2"
|
| 174 |
+
placeholder="r8_xxxx... (Replicate API token)">
|
| 175 |
+
</div>
|
| 176 |
+
|
| 177 |
+
<!-- HuggingFace token input group -->
|
| 178 |
+
<div id="hfTokenGroup" class="">
|
| 179 |
+
<div class="api-section mb-2" style="background-color:rgba(13, 110, 253, 0.05);border-color:rgba(13, 110, 253, 0.15);color:var(--color-primary);">
|
| 180 |
+
<i class="fas fa-magic me-1"></i>
|
| 181 |
+
Mô hình <strong>FREE Chuyển động thật</strong> qua HuggingFace. Nếu hết lượt thử (ZeroGPU limit), bạn hãy nhập <strong>HuggingFace Token</strong> (miễn phí) để tăng thêm lượt chạy. Lấy tại <a href="https://huggingface.co/settings/tokens" target="_blank" style="color:var(--color-primary);text-decoration:underline;">huggingface.co</a>.
|
| 182 |
+
</div>
|
| 183 |
+
<input type="password" id="hfToken" class="form-control form-control-sm mb-2"
|
| 184 |
+
placeholder="hf_xxxx... (HuggingFace token - tùy chọn)">
|
| 185 |
+
</div>
|
| 186 |
+
</div>
|
| 187 |
+
</div>
|
| 188 |
+
|
| 189 |
+
<button class="btn-primary-custom" id="generateBtn">
|
| 190 |
+
<i class="fas fa-magic me-2"></i>Tạo video
|
| 191 |
+
</button>
|
| 192 |
+
</div>
|
| 193 |
+
|
| 194 |
+
<!-- ── RIGHT: Kết quả ── -->
|
| 195 |
+
<div>
|
| 196 |
+
<div class="result-panel p-4" id="resultPanel">
|
| 197 |
+
|
| 198 |
+
<!-- Placeholder -->
|
| 199 |
+
<div id="resultPlaceholder" class="text-center">
|
| 200 |
+
<i class="fas fa-film fa-4x mb-3" style="color:var(--color-text-disabled)"></i>
|
| 201 |
+
<h5 class="text-muted">Video sẽ hiện tại đây</h5>
|
| 202 |
+
<p class="text-muted small">Điền thông tin và nhấn <strong>Tạo video</strong></p>
|
| 203 |
+
</div>
|
| 204 |
+
|
| 205 |
+
<!-- Progress -->
|
| 206 |
+
<div id="resultProgress" class="d-none w-100">
|
| 207 |
+
<h6 class="fw-semibold mb-3"><i class="fas fa-cog fa-spin me-2 text-primary"></i>Đang xử lý...</h6>
|
| 208 |
+
<div class="progress-step pending" id="step1">
|
| 209 |
+
<div class="step-icon"><i class="fas fa-pen"></i></div>
|
| 210 |
+
<span>Viết kịch bản (Gemini AI)</span>
|
| 211 |
+
</div>
|
| 212 |
+
<div class="progress-step pending" id="step2">
|
| 213 |
+
<div class="step-icon"><i class="fas fa-microphone"></i></div>
|
| 214 |
+
<span>Tổng hợp giọng đọc (Edge TTS)</span>
|
| 215 |
+
</div>
|
| 216 |
+
<div class="progress-step pending" id="step3">
|
| 217 |
+
<div class="step-icon"><i class="fas fa-film"></i></div>
|
| 218 |
+
<span>Tạo video AI (Replicate)</span>
|
| 219 |
+
</div>
|
| 220 |
+
</div>
|
| 221 |
+
|
| 222 |
+
<!-- Error -->
|
| 223 |
+
<div id="resultError" class="d-none w-100">
|
| 224 |
+
<div class="alert alert-danger">
|
| 225 |
+
<i class="fas fa-exclamation-circle me-2"></i>
|
| 226 |
+
<span id="errorText"></span>
|
| 227 |
+
</div>
|
| 228 |
+
</div>
|
| 229 |
+
|
| 230 |
+
<!-- Result -->
|
| 231 |
+
<div id="resultContent" class="d-none w-100">
|
| 232 |
+
|
| 233 |
+
<!-- Video player -->
|
| 234 |
+
<div id="videoWrapper" class="video-box mb-3 d-none">
|
| 235 |
+
<video id="videoPlayer" controls>
|
| 236 |
+
<source id="videoSrc" src="" type="video/mp4">
|
| 237 |
+
</video>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<!-- No video notice -->
|
| 241 |
+
<div id="noVideoNotice" class="d-none alert alert-info mb-3 py-2 small">
|
| 242 |
+
<i class="fas fa-info-circle me-1"></i>
|
| 243 |
+
<span id="noVideoText"></span>
|
| 244 |
+
</div>
|
| 245 |
+
|
| 246 |
+
<!-- Script -->
|
| 247 |
+
<div class="mb-3">
|
| 248 |
+
<div class="fw-semibold small mb-1">
|
| 249 |
+
<i class="fas fa-scroll me-1 text-primary"></i>Kịch bản dẫn truyện
|
| 250 |
+
</div>
|
| 251 |
+
<div class="script-box" id="scriptDisplay"></div>
|
| 252 |
+
</div>
|
| 253 |
+
|
| 254 |
+
<!-- Video prompt -->
|
| 255 |
+
<div class="mb-3" id="videoPromptSection">
|
| 256 |
+
<div class="fw-semibold small mb-1">
|
| 257 |
+
<i class="fas fa-magic me-1 text-warning"></i>Prompt video AI (tiếng Anh)
|
| 258 |
+
</div>
|
| 259 |
+
<div class="script-box" id="videoPromptDisplay" style="font-size:.75rem;color:var(--color-text-muted)"></div>
|
| 260 |
+
</div>
|
| 261 |
+
|
| 262 |
+
<!-- Audio player -->
|
| 263 |
+
<div class="mb-3" id="audioWrapper">
|
| 264 |
+
<div class="fw-semibold small mb-2">
|
| 265 |
+
<i class="fas fa-headphones me-1 text-success"></i>Giọng đọc dẫn truyện
|
| 266 |
+
</div>
|
| 267 |
+
<audio id="audioPlayer" controls style="width:100%;border-radius:8px;"></audio>
|
| 268 |
+
</div>
|
| 269 |
+
|
| 270 |
+
<!-- Actions -->
|
| 271 |
+
<div class="d-flex gap-2 flex-wrap">
|
| 272 |
+
<a id="downloadVideoBtn" href="#" class="btn btn-primary btn-sm d-none">
|
| 273 |
+
<i class="fas fa-download me-1"></i>Tải video
|
| 274 |
+
</a>
|
| 275 |
+
<a id="downloadAudioBtn" href="#" class="btn btn-outline-secondary btn-sm d-none">
|
| 276 |
+
<i class="fas fa-music me-1"></i>Tải giọng đọc
|
| 277 |
+
</a>
|
| 278 |
+
<button class="btn btn-outline-danger btn-sm" onclick="resetResult()">
|
| 279 |
+
<button class="btn btn-outline-danger btn-sm" onclick="resetResult()">
|
| 280 |
+
<i class="fas fa-redo me-1"></i>Làm lại
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
</div>
|
| 284 |
+
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
</div>
|
| 290 |
+
{% endblock %}
|
| 291 |
+
|
| 292 |
+
{% block scripts %}
|
| 293 |
+
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
|
| 294 |
+
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script>
|
| 295 |
+
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" />
|
| 296 |
+
<style>
|
| 297 |
+
/* Custom style for Select2 to match UI style rules */
|
| 298 |
+
.select2-container--default .select2-selection--single {
|
| 299 |
+
border: 1px solid var(--color-border);
|
| 300 |
+
border-radius: var(--radius-md);
|
| 301 |
+
height: 35px;
|
| 302 |
+
padding: 4px 6px;
|
| 303 |
+
background-color: var(--color-bg-input);
|
| 304 |
+
transition: border-color .15s ease-in-out;
|
| 305 |
+
}
|
| 306 |
+
.select2-container--default .select2-selection--single .select2-selection__rendered {
|
| 307 |
+
line-height: 25px;
|
| 308 |
+
color: var(--color-text-body);
|
| 309 |
+
font-size: .82rem;
|
| 310 |
+
padding-left: 0;
|
| 311 |
+
}
|
| 312 |
+
.select2-container--default .select2-selection--single .select2-selection__arrow {
|
| 313 |
+
height: 33px;
|
| 314 |
+
}
|
| 315 |
+
.select2-dropdown {
|
| 316 |
+
border: 1px solid var(--color-border);
|
| 317 |
+
border-radius: var(--radius-md);
|
| 318 |
+
box-shadow: var(--shadow-sm);
|
| 319 |
+
background-color: #fff;
|
| 320 |
+
font-size: .82rem;
|
| 321 |
+
}
|
| 322 |
+
</style>
|
| 323 |
+
<script>
|
| 324 |
+
let allVoices = [];
|
| 325 |
+
|
| 326 |
+
async function loadGeminiModels() {
|
| 327 |
+
try {
|
| 328 |
+
const res = await fetch('/api/gemini-models');
|
| 329 |
+
const models = await res.json();
|
| 330 |
+
const sel = document.getElementById('geminiModel');
|
| 331 |
+
sel.innerHTML = '';
|
| 332 |
+
|
| 333 |
+
models.forEach(m => {
|
| 334 |
+
const opt = document.createElement('option');
|
| 335 |
+
opt.value = m.id;
|
| 336 |
+
opt.textContent = `${m.name} (Còn lại: ${m.remaining}/${m.rpd} lượt)`;
|
| 337 |
+
if (m.id === 'gemini-2.5-flash') {
|
| 338 |
+
opt.selected = true;
|
| 339 |
+
}
|
| 340 |
+
sel.appendChild(opt);
|
| 341 |
+
});
|
| 342 |
+
|
| 343 |
+
// Check if select2 is initialized
|
| 344 |
+
if ($('#geminiModel').data('select2')) $('#geminiModel').select2('destroy');
|
| 345 |
+
$('#geminiModel').select2({ width: '100%' });
|
| 346 |
+
|
| 347 |
+
// Quota display
|
| 348 |
+
const quotaDiv = document.getElementById('geminiQuota');
|
| 349 |
+
if (quotaDiv) {
|
| 350 |
+
let usedText = '';
|
| 351 |
+
models.forEach(m => {
|
| 352 |
+
if (m.used > 0) {
|
| 353 |
+
usedText += `<br>• ${m.name}: đã dùng ${m.used}`;
|
| 354 |
+
}
|
| 355 |
+
});
|
| 356 |
+
quotaDiv.innerHTML = 'Hôm nay:' + (usedText || ' Chưa sử dụng lượt nào.');
|
| 357 |
+
}
|
| 358 |
+
} catch (e) {
|
| 359 |
+
console.error("Lỗi tải Gemini Models:", e);
|
| 360 |
+
}
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
async function loadVoices() {
|
| 364 |
+
try {
|
| 365 |
+
const res = await fetch('/api/edge-voices');
|
| 366 |
+
allVoices = await res.json();
|
| 367 |
+
if (allVoices.error) return;
|
| 368 |
+
|
| 369 |
+
populateVoices($('#narrateLang').val());
|
| 370 |
+
} catch (e) {
|
| 371 |
+
console.error("Lỗi tải Edge TTS Voices:", e);
|
| 372 |
+
}
|
| 373 |
+
}
|
| 374 |
+
|
| 375 |
+
async function loadReplicateModels() {
|
| 376 |
+
try {
|
| 377 |
+
const res = await fetch('/api/video-ai/models');
|
| 378 |
+
const models = await res.json();
|
| 379 |
+
const sel = document.getElementById('replicateModel');
|
| 380 |
+
sel.innerHTML = '';
|
| 381 |
+
|
| 382 |
+
models.forEach(m => {
|
| 383 |
+
const opt = document.createElement('option');
|
| 384 |
+
opt.value = m.id;
|
| 385 |
+
opt.textContent = `${m.name} (${m.desc})`;
|
| 386 |
+
if (m.id === 'hf-wan2.2-i2v-free') {
|
| 387 |
+
opt.selected = true;
|
| 388 |
+
}
|
| 389 |
+
sel.appendChild(opt);
|
| 390 |
+
});
|
| 391 |
+
|
| 392 |
+
if ($('#replicateModel').data('select2')) $('#replicateModel').select2('destroy');
|
| 393 |
+
$('#replicateModel').select2({ width: '100%' });
|
| 394 |
+
} catch (e) {
|
| 395 |
+
console.error("Lỗi tải Replicate Models:", e);
|
| 396 |
+
}
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
function populateVoices(lang) {
|
| 400 |
+
const voices = allVoices.filter(v => v.lang === lang);
|
| 401 |
+
const sel = document.getElementById('narrateVoice');
|
| 402 |
+
sel.innerHTML = '';
|
| 403 |
+
|
| 404 |
+
voices.forEach(v => {
|
| 405 |
+
const opt = document.createElement('option');
|
| 406 |
+
opt.value = v.name;
|
| 407 |
+
opt.textContent = (v.gender==='Female'?'♀ ':'♂ ') + v.display.replace(/^Microsoft /,'').replace(/ Online \(Natural\).*/,'');
|
| 408 |
+
sel.appendChild(opt);
|
| 409 |
+
});
|
| 410 |
+
|
| 411 |
+
if ($('#narrateVoice').data('select2')) $('#narrateVoice').select2('destroy');
|
| 412 |
+
$('#narrateVoice').select2({ width: '100%' });
|
| 413 |
+
|
| 414 |
+
if (voices.length > 0) {
|
| 415 |
+
// default select female voice if possible
|
| 416 |
+
const female = voices.find(v => v.gender === 'Female') || voices[0];
|
| 417 |
+
$('#narrateVoice').val(female.name).trigger('change');
|
| 418 |
+
}
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
function setStepState(el, state) {
|
| 422 |
+
el.className = 'progress-step ' + state;
|
| 423 |
+
const icon = el.querySelector('.step-icon i');
|
| 424 |
+
if (state === 'done') {
|
| 425 |
+
icon.className = 'fas fa-check';
|
| 426 |
+
} else if (state === 'active') {
|
| 427 |
+
icon.className = 'fas fa-spinner fa-spin';
|
| 428 |
+
} else {
|
| 429 |
+
if (el.id === 'step1') icon.className = 'fas fa-pen';
|
| 430 |
+
if (el.id === 'step2') icon.className = 'fas fa-microphone';
|
| 431 |
+
if (el.id === 'step3') icon.className = 'fas fa-film';
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
function showError(msg) {
|
| 436 |
+
document.getElementById('resultProgress').classList.add('d-none');
|
| 437 |
+
document.getElementById('resultContent').classList.add('d-none');
|
| 438 |
+
document.getElementById('resultError').classList.remove('d-none');
|
| 439 |
+
document.getElementById('errorText').textContent = msg;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
function resetResult() {
|
| 443 |
+
document.getElementById('resultContent').classList.add('d-none');
|
| 444 |
+
document.getElementById('resultPlaceholder').classList.remove('d-none');
|
| 445 |
+
document.getElementById('videoPlayer').pause();
|
| 446 |
+
document.getElementById('audioPlayer').pause();
|
| 447 |
+
}
|
| 448 |
+
|
| 449 |
+
$(document).ready(function() {
|
| 450 |
+
// Init select2 inputs
|
| 451 |
+
$('#narrateLang').select2({ width: '100%' });
|
| 452 |
+
$('#replicateModel').select2({ width: '100%' });
|
| 453 |
+
|
| 454 |
+
// Style cards selecting
|
| 455 |
+
document.querySelectorAll('.style-card').forEach(card => {
|
| 456 |
+
card.addEventListener('click', function() {
|
| 457 |
+
document.querySelectorAll('.style-card').forEach(c => c.classList.remove('active'));
|
| 458 |
+
this.classList.add('active');
|
| 459 |
+
});
|
| 460 |
+
});
|
| 461 |
+
|
| 462 |
+
// Bind change to narrateLang
|
| 463 |
+
$('#narrateLang').on('change', function() {
|
| 464 |
+
populateVoices($(this).val());
|
| 465 |
+
});
|
| 466 |
+
|
| 467 |
+
// Bind change to replicateModel to toggle token groups
|
| 468 |
+
$('#replicateModel').on('change', function() {
|
| 469 |
+
const val = $(this).val();
|
| 470 |
+
if (val && val.includes('hf-')) {
|
| 471 |
+
$('#replicateTokenGroup').addClass('d-none');
|
| 472 |
+
$('#hfTokenGroup').removeClass('d-none');
|
| 473 |
+
} else {
|
| 474 |
+
$('#replicateTokenGroup').removeClass('d-none');
|
| 475 |
+
$('#hfTokenGroup').addClass('d-none');
|
| 476 |
+
}
|
| 477 |
+
});
|
| 478 |
+
|
| 479 |
+
// Load models & voices
|
| 480 |
+
loadGeminiModels();
|
| 481 |
+
loadVoices();
|
| 482 |
+
loadReplicateModels();
|
| 483 |
+
|
| 484 |
+
// Default values
|
| 485 |
+
$('#narrateLang').val('vi').trigger('change');
|
| 486 |
+
|
| 487 |
+
// Generate click handler
|
| 488 |
+
document.getElementById('generateBtn').addEventListener('click', async () => {
|
| 489 |
+
const prompt = document.getElementById('videoPrompt').value.trim();
|
| 490 |
+
if (!prompt) {
|
| 491 |
+
alert('Vui lòng nhập chủ đề video!');
|
| 492 |
+
return;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
const geminiModel = document.getElementById('geminiModel').value;
|
| 496 |
+
const lang = document.getElementById('narrateLang').value;
|
| 497 |
+
const voice = document.getElementById('narrateVoice').value;
|
| 498 |
+
const style = document.querySelector('.style-card.active')?.getAttribute('data-style') || 'cinematic';
|
| 499 |
+
const ratio = document.getElementById('videoRatio').value;
|
| 500 |
+
const duration = document.getElementById('videoDuration').value;
|
| 501 |
+
const music = document.getElementById('videoMusic').value;
|
| 502 |
+
const narrate = document.getElementById('narrateEnable').checked;
|
| 503 |
+
const api_token = document.getElementById('replicateToken').value.trim();
|
| 504 |
+
const hf_token = document.getElementById('hfToken').value.trim();
|
| 505 |
+
const replicateModel = document.getElementById('replicateModel').value;
|
| 506 |
+
|
| 507 |
+
// UI state
|
| 508 |
+
document.getElementById('resultPlaceholder').classList.add('d-none');
|
| 509 |
+
document.getElementById('resultError').classList.add('d-none');
|
| 510 |
+
document.getElementById('resultContent').classList.add('d-none');
|
| 511 |
+
document.getElementById('resultProgress').classList.remove('d-none');
|
| 512 |
+
|
| 513 |
+
const step1 = document.getElementById('step1');
|
| 514 |
+
const step2 = document.getElementById('step2');
|
| 515 |
+
const step3 = document.getElementById('step3');
|
| 516 |
+
|
| 517 |
+
setStepState(step1, 'active');
|
| 518 |
+
setStepState(step2, 'pending');
|
| 519 |
+
setStepState(step3, 'pending');
|
| 520 |
+
|
| 521 |
+
const formData = new FormData();
|
| 522 |
+
formData.append('prompt', prompt);
|
| 523 |
+
formData.append('gemini_model', geminiModel);
|
| 524 |
+
formData.append('lang', lang);
|
| 525 |
+
formData.append('voice', voice);
|
| 526 |
+
formData.append('style', style);
|
| 527 |
+
formData.append('duration', duration);
|
| 528 |
+
formData.append('music', music);
|
| 529 |
+
formData.append('ratio', ratio);
|
| 530 |
+
formData.append('narrate', narrate);
|
| 531 |
+
formData.append('api_token', api_token);
|
| 532 |
+
formData.append('hf_token', hf_token);
|
| 533 |
+
formData.append('replicate_model', replicateModel);
|
| 534 |
+
|
| 535 |
+
let isFinished = false;
|
| 536 |
+
|
| 537 |
+
// Simulate step progress while waiting
|
| 538 |
+
setTimeout(() => {
|
| 539 |
+
if (!isFinished) {
|
| 540 |
+
setStepState(step1, 'done');
|
| 541 |
+
if (narrate) {
|
| 542 |
+
setStepState(step2, 'active');
|
| 543 |
+
} else {
|
| 544 |
+
setStepState(step3, 'active');
|
| 545 |
+
}
|
| 546 |
+
}
|
| 547 |
+
}, 4000);
|
| 548 |
+
|
| 549 |
+
setTimeout(() => {
|
| 550 |
+
if (!isFinished && narrate) {
|
| 551 |
+
setStepState(step2, 'done');
|
| 552 |
+
setStepState(step3, 'active');
|
| 553 |
+
}
|
| 554 |
+
}, 8000);
|
| 555 |
+
|
| 556 |
+
try {
|
| 557 |
+
const response = await fetch('/api/video-ai/generate', {
|
| 558 |
+
method: 'POST',
|
| 559 |
+
body: formData
|
| 560 |
+
});
|
| 561 |
+
|
| 562 |
+
isFinished = true;
|
| 563 |
+
const result = await response.json();
|
| 564 |
+
|
| 565 |
+
if (result.error) {
|
| 566 |
+
showError(result.error);
|
| 567 |
+
return;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
setStepState(step1, 'done');
|
| 571 |
+
if (narrate) setStepState(step2, 'done');
|
| 572 |
+
setStepState(step3, 'done');
|
| 573 |
+
|
| 574 |
+
setTimeout(() => {
|
| 575 |
+
document.getElementById('resultProgress').classList.add('d-none');
|
| 576 |
+
document.getElementById('resultContent').classList.remove('d-none');
|
| 577 |
+
|
| 578 |
+
document.getElementById('scriptDisplay').textContent = result.script || '';
|
| 579 |
+
document.getElementById('videoPromptDisplay').textContent = result.video_prompt || '';
|
| 580 |
+
|
| 581 |
+
// Audio display
|
| 582 |
+
if (result.audio_url) {
|
| 583 |
+
document.getElementById('audioWrapper').classList.remove('d-none');
|
| 584 |
+
const audioPlayer = document.getElementById('audioPlayer');
|
| 585 |
+
audioPlayer.src = result.audio_url;
|
| 586 |
+
audioPlayer.load();
|
| 587 |
+
|
| 588 |
+
const downloadAudio = document.getElementById('downloadAudioBtn');
|
| 589 |
+
downloadAudio.href = result.audio_url;
|
| 590 |
+
downloadAudio.classList.remove('d-none');
|
| 591 |
+
} else {
|
| 592 |
+
document.getElementById('audioWrapper').classList.add('d-none');
|
| 593 |
+
document.getElementById('downloadAudioBtn').classList.add('d-none');
|
| 594 |
+
}
|
| 595 |
+
|
| 596 |
+
// Video display
|
| 597 |
+
if (result.video_url) {
|
| 598 |
+
document.getElementById('videoWrapper').classList.remove('d-none');
|
| 599 |
+
document.getElementById('noVideoNotice').classList.add('d-none');
|
| 600 |
+
|
| 601 |
+
const videoPlayer = document.getElementById('videoPlayer');
|
| 602 |
+
const videoSrc = document.getElementById('videoSrc');
|
| 603 |
+
videoSrc.src = result.video_url;
|
| 604 |
+
videoPlayer.load();
|
| 605 |
+
|
| 606 |
+
const downloadVideo = document.getElementById('downloadVideoBtn');
|
| 607 |
+
downloadVideo.href = result.video_url;
|
| 608 |
+
downloadVideo.classList.remove('d-none');
|
| 609 |
+
} else {
|
| 610 |
+
document.getElementById('videoWrapper').classList.add('d-none');
|
| 611 |
+
document.getElementById('downloadVideoBtn').classList.add('d-none');
|
| 612 |
+
|
| 613 |
+
document.getElementById('noVideoNotice').classList.remove('d-none');
|
| 614 |
+
document.getElementById('noVideoText').textContent = result.note || 'Không tạo được video.';
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
loadGeminiModels();
|
| 618 |
+
}, 500);
|
| 619 |
+
|
| 620 |
+
} catch (err) {
|
| 621 |
+
isFinished = true;
|
| 622 |
+
showError(err.message || 'Lỗi mạng hoặc máy chủ không phản hồi.');
|
| 623 |
+
}
|
| 624 |
+
});
|
| 625 |
+
});
|
| 626 |
+
</script>
|
| 627 |
+
{% endblock %}
|
uploads/tts_edge_814db1da.mp3
DELETED
|
Binary file (10.4 kB)
|
|
|
uploads/tts_edge_f2f4925d.mp3
DELETED
|
Binary file (11.5 kB)
|
|
|