Spaces:
Running
Running
Upload 2 files
Browse files- index.html +84 -84
- main.js +53 -78
index.html
CHANGED
@@ -3,121 +3,121 @@
|
|
3 |
<html lang="en">
|
4 |
<head>
|
5 |
<meta charset="UTF-8" />
|
6 |
-
<title>
|
7 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
8 |
<style>
|
9 |
-
body {
|
10 |
margin: 0;
|
|
|
|
|
|
|
11 |
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
12 |
-
background: #f9f9f9;
|
13 |
-
color: #111;
|
14 |
-
display: flex;
|
15 |
-
flex-direction: column;
|
16 |
-
align-items: center;
|
17 |
}
|
18 |
-
|
19 |
-
margin-top: 16px;
|
20 |
-
}
|
21 |
-
.chat-preview {
|
22 |
width: 360px;
|
23 |
-
height:
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
30 |
}
|
31 |
.message {
|
32 |
display: flex;
|
33 |
-
margin-bottom: 10px;
|
34 |
align-items: flex-end;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
35 |
}
|
36 |
-
.message.left { flex-direction: row; }
|
37 |
-
.message.right { flex-direction: row-reverse; }
|
38 |
.bubble {
|
39 |
-
padding: 10px 14px;
|
40 |
-
border-radius: 18px;
|
41 |
max-width: 70%;
|
|
|
42 |
font-size: 14px;
|
|
|
|
|
43 |
}
|
44 |
-
.
|
|
|
|
|
|
|
45 |
background: #e5e5ea;
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
51 |
}
|
52 |
-
.
|
53 |
-
width:
|
54 |
-
height:
|
|
|
55 |
border-radius: 50%;
|
56 |
-
|
57 |
}
|
58 |
-
.
|
59 |
-
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
gap: 10px;
|
64 |
-
padding: 10px;
|
65 |
}
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
border-radius: 12px;
|
70 |
-
padding: 12px;
|
71 |
-
display: flex;
|
72 |
-
flex-direction: column;
|
73 |
-
gap: 6px;
|
74 |
-
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
75 |
}
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
|
80 |
-
border: 1px solid #ccc;
|
81 |
}
|
82 |
button {
|
83 |
-
|
|
|
84 |
border: none;
|
85 |
-
border-radius:
|
86 |
color: white;
|
87 |
-
|
88 |
-
font-size: 14px;
|
89 |
cursor: pointer;
|
90 |
}
|
91 |
-
.controls {
|
92 |
-
display: flex;
|
93 |
-
flex-wrap: wrap;
|
94 |
-
gap: 10px;
|
95 |
-
width: 100%;
|
96 |
-
max-width: 420px;
|
97 |
-
justify-content: space-between;
|
98 |
-
padding: 0 10px;
|
99 |
-
}
|
100 |
-
.controls input[type="text"] {
|
101 |
-
flex: 1;
|
102 |
-
}
|
103 |
</style>
|
104 |
</head>
|
105 |
<body>
|
106 |
-
<
|
107 |
-
|
108 |
-
<
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
</select>
|
114 |
-
<input type="text" id="prompt" placeholder="Generate chat: funny convo about aliens">
|
115 |
-
<button onclick="generateAI()">🤖 Generate</button>
|
116 |
-
<button onclick="addMessage()">+ Add</button>
|
117 |
-
<button onclick="renderAndExport()">🎬 Export Video</button>
|
118 |
</div>
|
119 |
-
<div class="chat-preview" id="chatPreview"></div>
|
120 |
-
<div class="editor" id="editor"></div>
|
121 |
<script type="module" src="main.js"></script>
|
122 |
</body>
|
123 |
</html>
|
|
|
3 |
<html lang="en">
|
4 |
<head>
|
5 |
<meta charset="UTF-8" />
|
6 |
+
<title>Chat Animator TikTok Export</title>
|
7 |
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
8 |
<style>
|
9 |
+
html, body {
|
10 |
margin: 0;
|
11 |
+
padding: 0;
|
12 |
+
background: #000;
|
13 |
+
height: 100%;
|
14 |
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
|
|
|
|
|
|
|
|
|
|
|
15 |
}
|
16 |
+
.phone-frame {
|
|
|
|
|
|
|
17 |
width: 360px;
|
18 |
+
height: 640px;
|
19 |
+
margin: 0 auto;
|
20 |
+
position: relative;
|
21 |
+
overflow: hidden;
|
22 |
+
border-radius: 32px;
|
23 |
+
box-shadow: 0 0 12px rgba(0,0,0,0.6);
|
24 |
+
}
|
25 |
+
video#bg {
|
26 |
+
position: absolute;
|
27 |
+
width: 100%;
|
28 |
+
height: 100%;
|
29 |
+
object-fit: cover;
|
30 |
+
z-index: 0;
|
31 |
+
}
|
32 |
+
#chat {
|
33 |
+
position: absolute;
|
34 |
+
z-index: 1;
|
35 |
+
width: 100%;
|
36 |
+
height: 100%;
|
37 |
+
padding: 12px;
|
38 |
+
display: flex;
|
39 |
+
flex-direction: column;
|
40 |
+
justify-content: flex-end;
|
41 |
+
overflow-y: hidden;
|
42 |
}
|
43 |
.message {
|
44 |
display: flex;
|
|
|
45 |
align-items: flex-end;
|
46 |
+
margin-bottom: 10px;
|
47 |
+
opacity: 0;
|
48 |
+
transform: translateY(20px);
|
49 |
+
animation: fadeIn 0.6s ease forwards;
|
50 |
+
}
|
51 |
+
.left { flex-direction: row; }
|
52 |
+
.right { flex-direction: row-reverse; }
|
53 |
+
.avatar {
|
54 |
+
width: 32px;
|
55 |
+
height: 32px;
|
56 |
+
border-radius: 50%;
|
57 |
+
margin: 0 6px;
|
58 |
}
|
|
|
|
|
59 |
.bubble {
|
|
|
|
|
60 |
max-width: 70%;
|
61 |
+
padding: 10px 14px;
|
62 |
font-size: 14px;
|
63 |
+
border-radius: 18px;
|
64 |
+
background: white;
|
65 |
}
|
66 |
+
.right .bubble { background: #007aff; color: white; }
|
67 |
+
.typing {
|
68 |
+
width: 48px;
|
69 |
+
height: 20px;
|
70 |
background: #e5e5ea;
|
71 |
+
border-radius: 12px;
|
72 |
+
display: flex;
|
73 |
+
align-items: center;
|
74 |
+
justify-content: center;
|
75 |
+
gap: 3px;
|
76 |
+
margin-bottom: 10px;
|
77 |
+
animation: fadeIn 0.3s ease;
|
78 |
}
|
79 |
+
.typing span {
|
80 |
+
width: 6px;
|
81 |
+
height: 6px;
|
82 |
+
background: #999;
|
83 |
border-radius: 50%;
|
84 |
+
animation: blink 1.4s infinite;
|
85 |
}
|
86 |
+
.typing span:nth-child(2) { animation-delay: 0.2s; }
|
87 |
+
.typing span:nth-child(3) { animation-delay: 0.4s; }
|
88 |
+
|
89 |
+
@keyframes fadeIn {
|
90 |
+
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
91 |
}
|
92 |
+
@keyframes blink {
|
93 |
+
0%, 80%, 100% { opacity: 0.2; }
|
94 |
+
40% { opacity: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
}
|
96 |
+
#controls {
|
97 |
+
text-align: center;
|
98 |
+
margin: 10px auto;
|
99 |
+
color: white;
|
|
|
100 |
}
|
101 |
button {
|
102 |
+
padding: 10px 20px;
|
103 |
+
background: #25d366;
|
104 |
border: none;
|
105 |
+
border-radius: 8px;
|
106 |
color: white;
|
107 |
+
font-size: 16px;
|
|
|
108 |
cursor: pointer;
|
109 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
</style>
|
111 |
</head>
|
112 |
<body>
|
113 |
+
<div class="phone-frame">
|
114 |
+
<video id="bg" muted loop></video>
|
115 |
+
<div id="chat"></div>
|
116 |
+
</div>
|
117 |
+
<div id="controls">
|
118 |
+
<input type="file" accept="video/mp4" onchange="loadBackground(event)">
|
119 |
+
<button onclick="start()">🎬 Export MP4 with Voice</button>
|
|
|
|
|
|
|
|
|
|
|
120 |
</div>
|
|
|
|
|
121 |
<script type="module" src="main.js"></script>
|
122 |
</body>
|
123 |
</html>
|
main.js
CHANGED
@@ -1,112 +1,87 @@
|
|
1 |
|
2 |
-
|
3 |
-
|
4 |
-
|
|
|
|
|
|
|
5 |
];
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
imessage: { left: "#e5e5ea", right: "#007aff" },
|
11 |
-
whatsapp: { left: "#dcf8c6", right: "#34b7f1" },
|
12 |
-
instagram: { left: "#fff0f5", right: "#ffb6c1" },
|
13 |
-
reddit: { left: "#f6f7f8", right: "#ccc" }
|
14 |
};
|
15 |
const apiKey = "sk_4e67c39c0e9cbc87462cd2643e1f4d1d9959d7d81203adc2";
|
16 |
-
const voiceIdMap = { Rachel: "21m00Tcm4TlvDq8ikWAM", Adam: "pNInz6obpgDQGcFmaJgB", Bella: "EXAVITQu4vr4xnSDxMaL" };
|
17 |
-
|
18 |
-
function renderPreview() {
|
19 |
-
const style = document.getElementById("styleSelect").value;
|
20 |
-
preview.innerHTML = "";
|
21 |
-
messages.forEach(msg => {
|
22 |
-
const wrap = document.createElement("div");
|
23 |
-
wrap.className = "message " + msg.side;
|
24 |
-
const avatar = document.createElement("img");
|
25 |
-
avatar.src = msg.avatar;
|
26 |
-
avatar.className = "avatar";
|
27 |
-
const bubble = document.createElement("div");
|
28 |
-
bubble.className = "bubble";
|
29 |
-
bubble.style.background = styles[style][msg.side];
|
30 |
-
bubble.textContent = msg.text;
|
31 |
-
wrap.appendChild(avatar);
|
32 |
-
wrap.appendChild(bubble);
|
33 |
-
preview.appendChild(wrap);
|
34 |
-
});
|
35 |
-
}
|
36 |
|
37 |
-
function
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
row.innerHTML = `
|
43 |
-
<input type="text" value="${msg.name}" placeholder="Name" onchange="messages[${i}].name=this.value">
|
44 |
-
<input type="text" value="${msg.avatar}" placeholder="Avatar URL" onchange="messages[${i}].avatar=this.value">
|
45 |
-
<select onchange="messages[${i}].voice=this.value">
|
46 |
-
<option ${msg.voice==="Rachel"?"selected":""}>Rachel</option>
|
47 |
-
<option ${msg.voice==="Adam"?"selected":""}>Adam</option>
|
48 |
-
<option ${msg.voice==="Bella"?"selected":""}>Bella</option>
|
49 |
-
</select>
|
50 |
-
<select onchange="messages[${i}].side=this.value">
|
51 |
-
<option value="left" ${msg.side==="left"?"selected":""}>Left</option>
|
52 |
-
<option value="right" ${msg.side==="right"?"selected":""}>Right</option>
|
53 |
-
</select>
|
54 |
-
<input type="text" value="${msg.text}" placeholder="Message" onchange="messages[${i}].text=this.value">
|
55 |
-
`;
|
56 |
-
editor.appendChild(row);
|
57 |
-
});
|
58 |
-
renderPreview();
|
59 |
-
}
|
60 |
-
|
61 |
-
function addMessage() {
|
62 |
-
messages.push({ name: "New", side: "left", avatar: "https://i.pravatar.cc/30", voice: "Rachel", text: "..." });
|
63 |
-
renderEditor();
|
64 |
}
|
65 |
|
66 |
async function getVoiceBlob(text, voice) {
|
67 |
-
const voiceId =
|
68 |
const res = await fetch("https://api.elevenlabs.io/v1/text-to-speech/" + voiceId, {
|
69 |
method: "POST",
|
70 |
headers: {
|
71 |
"Content-Type": "application/json",
|
72 |
"xi-api-key": apiKey
|
73 |
},
|
74 |
-
body: JSON.stringify({ text, model_id: "eleven_monolingual_v1", voice_settings: { stability: 0.
|
75 |
});
|
76 |
return await res.blob();
|
77 |
}
|
78 |
|
79 |
-
async function
|
80 |
-
|
81 |
-
|
|
|
82 |
const recorder = new MediaRecorder(stream);
|
83 |
const chunks = [];
|
|
|
84 |
recorder.ondataavailable = e => chunks.push(e.data);
|
85 |
recorder.onstop = () => {
|
86 |
const blob = new Blob(chunks, { type: "video/webm" });
|
87 |
const a = document.createElement("a");
|
88 |
a.href = URL.createObjectURL(blob);
|
89 |
-
a.download = "
|
90 |
a.click();
|
91 |
};
|
|
|
92 |
recorder.start();
|
|
|
|
|
93 |
for (const msg of messages) {
|
94 |
-
const
|
95 |
-
|
96 |
-
|
97 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
98 |
audio.play();
|
99 |
});
|
100 |
}
|
101 |
-
recorder.stop();
|
102 |
-
}
|
103 |
|
104 |
-
|
105 |
-
const prompt = document.getElementById("prompt").value || "funny conversation between Suren and Elon Musk";
|
106 |
-
const mock = [
|
107 |
-
{ name: "Suren", side: "left", avatar: "https://i.pravatar.cc/30?img=4", voice: "Rachel", text: "Did you launch that rocket again?" },
|
108 |
-
{ name: "Elon", side: "right", avatar: "https://i.pravatar.cc/30?img=8", voice: "Adam", text: "Yes... but it came back with fries." }
|
109 |
-
];
|
110 |
-
messages = mock;
|
111 |
-
renderEditor();
|
112 |
}
|
|
|
1 |
|
2 |
+
const chat = document.getElementById("chat");
|
3 |
+
const bg = document.getElementById("bg");
|
4 |
+
const messages = [
|
5 |
+
{ name: "Suren", side: "left", avatar: "https://i.pravatar.cc/30?img=5", voice: "Rachel", text: "Are you ready to launch?" },
|
6 |
+
{ name: "Elon", side: "right", avatar: "https://i.pravatar.cc/30?img=9", voice: "Adam", text: "Always ready. 3...2...1..." },
|
7 |
+
{ name: "Suren", side: "left", avatar: "https://i.pravatar.cc/30?img=5", voice: "Rachel", text: "We’re live on TikTok now!" }
|
8 |
];
|
9 |
+
const voiceMap = {
|
10 |
+
Rachel: "21m00Tcm4TlvDq8ikWAM",
|
11 |
+
Adam: "pNInz6obpgDQGcFmaJgB",
|
12 |
+
Bella: "EXAVITQu4vr4xnSDxMaL"
|
|
|
|
|
|
|
|
|
13 |
};
|
14 |
const apiKey = "sk_4e67c39c0e9cbc87462cd2643e1f4d1d9959d7d81203adc2";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
|
16 |
+
function loadBackground(e) {
|
17 |
+
const file = e.target.files[0];
|
18 |
+
const url = URL.createObjectURL(file);
|
19 |
+
bg.src = url;
|
20 |
+
bg.play();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
}
|
22 |
|
23 |
async function getVoiceBlob(text, voice) {
|
24 |
+
const voiceId = voiceMap[voice] || voiceMap.Rachel;
|
25 |
const res = await fetch("https://api.elevenlabs.io/v1/text-to-speech/" + voiceId, {
|
26 |
method: "POST",
|
27 |
headers: {
|
28 |
"Content-Type": "application/json",
|
29 |
"xi-api-key": apiKey
|
30 |
},
|
31 |
+
body: JSON.stringify({ text, model_id: "eleven_monolingual_v1", voice_settings: { stability: 0.4, similarity_boost: 0.75 } })
|
32 |
});
|
33 |
return await res.blob();
|
34 |
}
|
35 |
|
36 |
+
async function start() {
|
37 |
+
chat.innerHTML = "";
|
38 |
+
bg.currentTime = 0;
|
39 |
+
const stream = document.querySelector(".phone-frame").captureStream(30);
|
40 |
const recorder = new MediaRecorder(stream);
|
41 |
const chunks = [];
|
42 |
+
|
43 |
recorder.ondataavailable = e => chunks.push(e.data);
|
44 |
recorder.onstop = () => {
|
45 |
const blob = new Blob(chunks, { type: "video/webm" });
|
46 |
const a = document.createElement("a");
|
47 |
a.href = URL.createObjectURL(blob);
|
48 |
+
a.download = "chat_tiktok_ready.mp4";
|
49 |
a.click();
|
50 |
};
|
51 |
+
|
52 |
recorder.start();
|
53 |
+
bg.play();
|
54 |
+
|
55 |
for (const msg of messages) {
|
56 |
+
const typing = document.createElement("div");
|
57 |
+
typing.className = "typing";
|
58 |
+
typing.innerHTML = "<span></span><span></span><span></span>";
|
59 |
+
chat.appendChild(typing);
|
60 |
+
chat.scrollTop = chat.scrollHeight;
|
61 |
+
await new Promise(r => setTimeout(r, 1200));
|
62 |
+
chat.removeChild(typing);
|
63 |
+
|
64 |
+
const wrap = document.createElement("div");
|
65 |
+
wrap.className = "message " + msg.side;
|
66 |
+
const avatar = document.createElement("img");
|
67 |
+
avatar.src = msg.avatar;
|
68 |
+
avatar.className = "avatar";
|
69 |
+
const bubble = document.createElement("div");
|
70 |
+
bubble.className = "bubble";
|
71 |
+
bubble.textContent = msg.text;
|
72 |
+
|
73 |
+
wrap.appendChild(avatar);
|
74 |
+
wrap.appendChild(bubble);
|
75 |
+
chat.appendChild(wrap);
|
76 |
+
chat.scrollTop = chat.scrollHeight;
|
77 |
+
|
78 |
+
const blob = await getVoiceBlob(msg.text, msg.voice);
|
79 |
+
const audio = new Audio(URL.createObjectURL(blob));
|
80 |
+
await new Promise(r => {
|
81 |
+
audio.onended = r;
|
82 |
audio.play();
|
83 |
});
|
84 |
}
|
|
|
|
|
85 |
|
86 |
+
recorder.stop();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
}
|