aigorithm commited on
Commit
fc11a91
·
verified ·
1 Parent(s): 3b2add9

Upload 2 files

Browse files
Files changed (2) hide show
  1. index.html +57 -8
  2. main.js +75 -28
index.html CHANGED
@@ -4,15 +4,64 @@
4
  <head>
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
- <title>Chat Animator + Video Export</title>
8
  <style>
9
- body { font-family: sans-serif; margin: 0; background: #f0f2f5; }
10
- .app { padding: 20px; max-width: 420px; margin: auto; }
11
- .character, .message { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
12
- .character img, .message img { width: 30px; height: 30px; border-radius: 50%; }
13
- .bubble { padding: 10px 14px; border-radius: 20px; max-width: 70%; background: #fff; }
14
- input, select, button { padding: 6px; margin: 4px 0; width: 100%; }
15
- .controls, .chat { margin-top: 16px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  </style>
17
  </head>
18
  <body>
 
4
  <head>
5
  <meta charset="UTF-8" />
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
7
+ <title>Chat Animator Final</title>
8
  <style>
9
+ html, body {
10
+ margin: 0;
11
+ background: #000;
12
+ height: 100%;
13
+ }
14
+ .app {
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: center;
18
+ background: #f0f2f5;
19
+ width: 100%;
20
+ max-width: 360px;
21
+ height: 640px;
22
+ margin: auto;
23
+ padding: 10px;
24
+ font-family: sans-serif;
25
+ box-sizing: border-box;
26
+ }
27
+ .character, .message {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: 8px;
31
+ margin-bottom: 10px;
32
+ }
33
+ .character img, .message img {
34
+ width: 30px;
35
+ height: 30px;
36
+ border-radius: 50%;
37
+ }
38
+ .bubble {
39
+ padding: 10px 14px;
40
+ border-radius: 20px;
41
+ max-width: 70%;
42
+ background: #fff;
43
+ opacity: 0;
44
+ animation: fadeIn 1s forwards;
45
+ }
46
+ .controls, .chat {
47
+ width: 100%;
48
+ margin-top: 10px;
49
+ }
50
+ input, select, button {
51
+ padding: 6px;
52
+ margin: 4px 0;
53
+ width: 100%;
54
+ }
55
+ @keyframes fadeIn {
56
+ to {
57
+ opacity: 1;
58
+ }
59
+ }
60
+ .typing {
61
+ font-style: italic;
62
+ opacity: 0.6;
63
+ animation: fadeIn 0.8s forwards;
64
+ }
65
  </style>
66
  </head>
67
  <body>
main.js CHANGED
@@ -1,23 +1,29 @@
1
 
2
  import { createElement as h, useState, useRef } from "https://esm.sh/react";
3
  import { createRoot } from "https://esm.sh/react-dom/client";
4
- import html2canvas from "https://esm.sh/html2canvas";
 
 
 
 
 
 
 
5
 
6
  const App = () => {
7
  const [characters, setCharacters] = useState([
8
- { id: 0, name: "User", avatar: "https://i.pravatar.cc/30?img=3" },
9
- { id: 1, name: "Bot", avatar: "https://i.pravatar.cc/30?img=7" }
10
  ]);
11
  const [selectedChar, setSelectedChar] = useState(0);
12
  const [messages, setMessages] = useState([]);
13
  const [input, setInput] = useState("");
14
- const [playing, setPlaying] = useState(false);
15
  const chatRef = useRef(null);
16
- const streamRef = useRef(null);
17
 
18
  const addCharacter = () => {
19
  const id = characters.length;
20
- setCharacters([...characters, { id, name: "New", avatar: "" }]);
21
  };
22
 
23
  const handleAvatar = (id, file) => {
@@ -29,39 +35,67 @@ const App = () => {
29
  setCharacters(characters.map(c => c.id === id ? { ...c, name } : c));
30
  };
31
 
 
 
 
 
32
  const addMessage = () => {
33
  if (!input.trim()) return;
34
  setMessages([...messages, { charId: selectedChar, text: input }]);
35
  setInput("");
36
  };
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  const playbackAndRecord = async () => {
39
- setPlaying(true);
40
- const container = chatRef.current;
41
- const allMsgs = [...messages];
42
- const tempContainer = container.cloneNode(false);
43
- container.parentNode.appendChild(tempContainer);
44
- const stream = tempContainer.captureStream(30);
45
- streamRef.current = stream;
46
  const recorder = new MediaRecorder(stream);
47
- let chunks = [];
48
 
49
- recorder.ondataavailable = (e) => chunks.push(e.data);
50
  recorder.onstop = () => {
51
  const blob = new Blob(chunks, { type: "video/webm" });
52
- const link = document.createElement("a");
53
- link.href = URL.createObjectURL(blob);
54
- link.download = "chat.mp4";
55
- link.click();
56
- tempContainer.remove();
57
- setPlaying(false);
 
58
  };
59
 
60
  recorder.start();
61
 
62
- for (let i = 0; i < allMsgs.length; i++) {
63
- const msg = allMsgs[i];
64
  const char = characters.find(c => c.id === msg.charId);
 
 
 
 
 
 
 
 
 
 
65
  const msgDiv = document.createElement("div");
66
  msgDiv.className = "message";
67
  const avatar = document.createElement("img");
@@ -71,8 +105,15 @@ const App = () => {
71
  text.textContent = msg.text;
72
  msgDiv.appendChild(avatar);
73
  msgDiv.appendChild(text);
74
- tempContainer.appendChild(msgDiv);
75
- await new Promise(r => setTimeout(r, 1500));
 
 
 
 
 
 
 
76
  }
77
 
78
  recorder.stop();
@@ -87,6 +128,12 @@ const App = () => {
87
  value: char.name,
88
  onChange: (e) => handleNameChange(char.id, e.target.value)
89
  }),
 
 
 
 
 
 
90
  h("input", {
91
  type: "file",
92
  accept: "image/*",
@@ -109,7 +156,7 @@ const App = () => {
109
  h("button", { onClick: addMessage }, "Send Message")
110
  ]),
111
 
112
- h("div", { className: "chat", ref: chatRef }, messages.map((msg, i) => {
113
  const char = characters.find(c => c.id === msg.charId);
114
  return h("div", { className: "message", key: i }, [
115
  h("img", { src: char?.avatar }),
@@ -119,9 +166,9 @@ const App = () => {
119
 
120
  h("button", {
121
  onClick: playbackAndRecord,
122
- disabled: playing,
123
  style: { marginTop: 20 }
124
- }, playing ? "Recording..." : "🎬 Export Video")
125
  ]);
126
  };
127
 
 
1
 
2
  import { createElement as h, useState, useRef } from "https://esm.sh/react";
3
  import { createRoot } from "https://esm.sh/react-dom/client";
4
+
5
+ const voices = {
6
+ Rachel: "21m00Tcm4TlvDq8ikWAM",
7
+ Adam: "pNInz6obpgDQGcFmaJgB",
8
+ Bella: "EXAVITQu4vr4xnSDxMaL"
9
+ };
10
+ const defaultVoice = "Rachel";
11
+ const apiKey = "sk_4e67c39c0e9cbc87462cd2643e1f4d1d9959d7d81203adc2";
12
 
13
  const App = () => {
14
  const [characters, setCharacters] = useState([
15
+ { id: 0, name: "User", avatar: "https://i.pravatar.cc/30?img=3", voice: "Rachel" },
16
+ { id: 1, name: "Bot", avatar: "https://i.pravatar.cc/30?img=7", voice: "Adam" }
17
  ]);
18
  const [selectedChar, setSelectedChar] = useState(0);
19
  const [messages, setMessages] = useState([]);
20
  const [input, setInput] = useState("");
21
+ const [recording, setRecording] = useState(false);
22
  const chatRef = useRef(null);
 
23
 
24
  const addCharacter = () => {
25
  const id = characters.length;
26
+ setCharacters([...characters, { id, name: "New", avatar: "", voice: defaultVoice }]);
27
  };
28
 
29
  const handleAvatar = (id, file) => {
 
35
  setCharacters(characters.map(c => c.id === id ? { ...c, name } : c));
36
  };
37
 
38
+ const handleVoiceChange = (id, voice) => {
39
+ setCharacters(characters.map(c => c.id === id ? { ...c, voice } : c));
40
+ };
41
+
42
  const addMessage = () => {
43
  if (!input.trim()) return;
44
  setMessages([...messages, { charId: selectedChar, text: input }]);
45
  setInput("");
46
  };
47
 
48
+ const getVoiceBlob = async (text, voiceId) => {
49
+ const res = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
50
+ method: "POST",
51
+ headers: {
52
+ "Content-Type": "application/json",
53
+ "xi-api-key": apiKey
54
+ },
55
+ body: JSON.stringify({
56
+ text,
57
+ model_id: "eleven_monolingual_v1",
58
+ voice_settings: { stability: 0.4, similarity_boost: 0.75 }
59
+ })
60
+ });
61
+ return await res.blob();
62
+ };
63
+
64
  const playbackAndRecord = async () => {
65
+ setRecording(true);
66
+ const chat = document.getElementById("chatArea");
67
+ const clone = chat.cloneNode(false);
68
+ chat.parentNode.appendChild(clone);
69
+ const stream = clone.captureStream(30);
 
 
70
  const recorder = new MediaRecorder(stream);
71
+ const chunks = [];
72
 
73
+ recorder.ondataavailable = e => chunks.push(e.data);
74
  recorder.onstop = () => {
75
  const blob = new Blob(chunks, { type: "video/webm" });
76
+ const url = URL.createObjectURL(blob);
77
+ const a = document.createElement("a");
78
+ a.href = url;
79
+ a.download = "chat_voice_typing.mp4";
80
+ a.click();
81
+ clone.remove();
82
+ setRecording(false);
83
  };
84
 
85
  recorder.start();
86
 
87
+ for (const msg of messages) {
 
88
  const char = characters.find(c => c.id === msg.charId);
89
+
90
+ // Typing animation
91
+ const typingDiv = document.createElement("div");
92
+ typingDiv.className = "message typing";
93
+ typingDiv.textContent = `${char.name} is typing...`;
94
+ clone.appendChild(typingDiv);
95
+ await new Promise(r => setTimeout(r, 1200));
96
+ typingDiv.remove();
97
+
98
+ // Real message
99
  const msgDiv = document.createElement("div");
100
  msgDiv.className = "message";
101
  const avatar = document.createElement("img");
 
105
  text.textContent = msg.text;
106
  msgDiv.appendChild(avatar);
107
  msgDiv.appendChild(text);
108
+ clone.appendChild(msgDiv);
109
+
110
+ // Voice playback
111
+ const voiceBlob = await getVoiceBlob(msg.text, voices[char.voice]);
112
+ const audio = new Audio(URL.createObjectURL(voiceBlob));
113
+ await new Promise(res => {
114
+ audio.onended = res;
115
+ audio.play();
116
+ });
117
  }
118
 
119
  recorder.stop();
 
128
  value: char.name,
129
  onChange: (e) => handleNameChange(char.id, e.target.value)
130
  }),
131
+ h("select", {
132
+ value: char.voice,
133
+ onChange: (e) => handleVoiceChange(char.id, e.target.value)
134
+ }, Object.keys(voices).map(v =>
135
+ h("option", { value: v, key: v }, v)
136
+ )),
137
  h("input", {
138
  type: "file",
139
  accept: "image/*",
 
156
  h("button", { onClick: addMessage }, "Send Message")
157
  ]),
158
 
159
+ h("div", { id: "chatArea", className: "chat", ref: chatRef }, messages.map((msg, i) => {
160
  const char = characters.find(c => c.id === msg.charId);
161
  return h("div", { className: "message", key: i }, [
162
  h("img", { src: char?.avatar }),
 
166
 
167
  h("button", {
168
  onClick: playbackAndRecord,
169
+ disabled: recording,
170
  style: { marginTop: 20 }
171
+ }, recording ? "Recording..." : "📱 Export TikTok-Style MP4")
172
  ]);
173
  };
174