Spaces:
Running
Running
Add simulated livestream replay theater, custom generator configuration, and multi-stage LLM pipeline
Browse files- .gitignore +5 -0
- app.py +249 -4
- pipeline.py +285 -0
- requirements.txt +5 -0
- sample/demo_chat.json +568 -0
- theater_template.html +541 -0
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.venv/
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.DS_Store
|
| 5 |
+
extract.py
|
app.py
CHANGED
|
@@ -1,7 +1,252 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
def
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import html
|
| 4 |
import gradio as gr
|
| 5 |
+
import pymupdf4llm
|
| 6 |
+
from pipeline import run_livestream_pipeline, extract_youtube_video_id, DEFAULT_HF_TOKEN
|
| 7 |
|
| 8 |
+
def load_template_with_data(video_id: str, chat_data: list) -> str:
|
| 9 |
+
template_path = os.path.join(os.path.dirname(__file__), "theater_template.html")
|
| 10 |
+
with open(template_path, "r", encoding="utf-8") as f:
|
| 11 |
+
template = f.read()
|
| 12 |
+
|
| 13 |
+
# Inject values
|
| 14 |
+
inner_html = template.replace("{{VIDEO_ID}}", video_id)
|
| 15 |
+
inner_html = inner_html.replace("{{CHAT_DATA_JSON}}", json.dumps(chat_data, ensure_ascii=False))
|
| 16 |
+
|
| 17 |
+
# Escape HTML to be safely embedded inside srcdoc attribute of an iframe
|
| 18 |
+
escaped_inner_html = html.escape(inner_html)
|
| 19 |
+
|
| 20 |
+
# Wrap in iframe to ensure scripts execute correctly in Gradio 6+ without innerHTML restrictions
|
| 21 |
+
iframe_code = (
|
| 22 |
+
f'<iframe srcdoc="{escaped_inner_html}" style="width: 100%; height: 620px; '
|
| 23 |
+
f'border: none; border-radius: 12px; background-color: #0b0c10; box-shadow: 0 4px 12px rgba(0,0,0,0.45);"></iframe>'
|
| 24 |
+
)
|
| 25 |
+
return iframe_code
|
| 26 |
|
| 27 |
+
def get_demo_html() -> str:
|
| 28 |
+
demo_json_path = os.path.join(os.path.dirname(__file__), "sample", "demo_chat.json")
|
| 29 |
+
if os.path.exists(demo_json_path):
|
| 30 |
+
with open(demo_json_path, "r", encoding="utf-8") as f:
|
| 31 |
+
chat_data = json.load(f)
|
| 32 |
+
else:
|
| 33 |
+
chat_data = []
|
| 34 |
+
return load_template_with_data("xLljoibgUvk", chat_data)
|
| 35 |
+
|
| 36 |
+
# Global store for custom simulations
|
| 37 |
+
custom_simulation_store = {
|
| 38 |
+
"video_id": "",
|
| 39 |
+
"chat_data": None
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
def play_simulation_selection(selection: str) -> str:
|
| 43 |
+
if selection == "Steve Jobs 1983 Speech (Demo)":
|
| 44 |
+
return get_demo_html()
|
| 45 |
+
elif selection == "My Custom Simulation":
|
| 46 |
+
if custom_simulation_store["video_id"] and custom_simulation_store["chat_data"]:
|
| 47 |
+
return load_template_with_data(custom_simulation_store["video_id"], custom_simulation_store["chat_data"])
|
| 48 |
+
else:
|
| 49 |
+
return "<div style='color:#ff0055; text-align:center; padding:50px; font-family:sans-serif;'>No custom simulation has been generated yet. Please configure and run one in the 'Generator' tab.</div>"
|
| 50 |
+
return ""
|
| 51 |
+
|
| 52 |
+
def handle_generation(yt_url: str, pdf_file, doc_text: str, srt_text: str, hf_token: str):
|
| 53 |
+
# 1. Validate YouTube Link
|
| 54 |
+
video_id = extract_youtube_video_id(yt_url)
|
| 55 |
+
if not video_id:
|
| 56 |
+
return (
|
| 57 |
+
gr.update(),
|
| 58 |
+
"### ❌ Error\nInvalid YouTube URL. Please provide a valid YouTube link or 11-character Video ID.",
|
| 59 |
+
gr.update()
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
# 2. Extract Document Text
|
| 63 |
+
document_content = ""
|
| 64 |
+
if pdf_file is not None:
|
| 65 |
+
try:
|
| 66 |
+
document_content = pymupdf4llm.to_markdown(pdf_file.name)
|
| 67 |
+
except Exception as e:
|
| 68 |
+
return (
|
| 69 |
+
gr.update(),
|
| 70 |
+
f"### ❌ Error\nFailed to extract text from PDF: {e}",
|
| 71 |
+
gr.update()
|
| 72 |
+
)
|
| 73 |
+
elif doc_text.strip():
|
| 74 |
+
document_content = doc_text.strip()
|
| 75 |
+
else:
|
| 76 |
+
return (
|
| 77 |
+
gr.update(),
|
| 78 |
+
"### ❌ Error\nPlease upload a PDF/text file or paste some reference document text.",
|
| 79 |
+
gr.update()
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
# 3. Clean manual transcript input if any
|
| 83 |
+
manual_transcript = srt_text.strip() if srt_text.strip() else None
|
| 84 |
+
|
| 85 |
+
# 4. Use provided token or default token
|
| 86 |
+
token = hf_token.strip() if hf_token.strip() else DEFAULT_HF_TOKEN
|
| 87 |
+
|
| 88 |
+
status_msg = "### ⚙️ Running Pipeline...\n"
|
| 89 |
+
if manual_transcript:
|
| 90 |
+
status_msg += "- Processing manually pasted transcript...\n"
|
| 91 |
+
else:
|
| 92 |
+
status_msg += "- Downloading YouTube captions (will fallback to manual paste prompt if IP is blocked)...\n"
|
| 93 |
+
|
| 94 |
+
status_msg += "- Segmenting transcript with Flash model...\n"
|
| 95 |
+
status_msg += "- Extracting document themes with Flash model...\n"
|
| 96 |
+
status_msg += "- Mapping content and generating draft comments with Pro model...\n"
|
| 97 |
+
status_msg += "- Refining comments with Flash model..."
|
| 98 |
+
|
| 99 |
+
# 5. Run the pipeline
|
| 100 |
+
try:
|
| 101 |
+
chat_data = run_livestream_pipeline(
|
| 102 |
+
video_id=video_id,
|
| 103 |
+
doc_text=document_content,
|
| 104 |
+
transcript_text=manual_transcript,
|
| 105 |
+
token=token
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
# Save to global store
|
| 109 |
+
custom_simulation_store["video_id"] = video_id
|
| 110 |
+
custom_simulation_store["chat_data"] = chat_data
|
| 111 |
+
|
| 112 |
+
success_msg = (
|
| 113 |
+
f"### 🎉 Success!\n"
|
| 114 |
+
f"Livestream simulation generated successfully for video ID `{video_id}`!\n"
|
| 115 |
+
f"Navigate back to the **Theater Mode** tab and select **My Custom Simulation** to play it."
|
| 116 |
+
)
|
| 117 |
+
|
| 118 |
+
# Create updated HTML player
|
| 119 |
+
new_html = load_template_with_data(video_id, chat_data)
|
| 120 |
+
|
| 121 |
+
return (
|
| 122 |
+
new_html,
|
| 123 |
+
success_msg,
|
| 124 |
+
gr.update(choices=["Steve Jobs 1983 Speech (Demo)", "My Custom Simulation"], value="My Custom Simulation")
|
| 125 |
+
)
|
| 126 |
+
except Exception as e:
|
| 127 |
+
error_msg = f"### ❌ Error running pipeline\n{e}"
|
| 128 |
+
if "YouTube transcript could not be fetched automatically" in str(e):
|
| 129 |
+
error_msg += (
|
| 130 |
+
"\n\n**Tip**: YouTube blocked the server IP from accessing the subtitles automatically. "
|
| 131 |
+
"Please copy and paste the video transcript or subtitles into the 'Manual Captions / Transcript Paste' "
|
| 132 |
+
"input box below to bypass the block."
|
| 133 |
+
)
|
| 134 |
+
return (
|
| 135 |
+
gr.update(),
|
| 136 |
+
error_msg,
|
| 137 |
+
gr.update()
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
# Gradio Theme
|
| 141 |
+
custom_theme = gr.themes.Default(
|
| 142 |
+
primary_hue="purple",
|
| 143 |
+
secondary_hue="indigo",
|
| 144 |
+
neutral_hue="slate"
|
| 145 |
+
).set(
|
| 146 |
+
body_background_fill="#0b0c10",
|
| 147 |
+
block_background_fill="#161a23",
|
| 148 |
+
block_border_color="rgba(255, 255, 255, 0.08)",
|
| 149 |
+
button_primary_background_fill="#7f5af0",
|
| 150 |
+
button_primary_text_color="#ffffff",
|
| 151 |
+
button_primary_background_fill_hover="#9370db"
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
with gr.Blocks(title="ReLiveStream - Interactive Replay") as demo:
|
| 155 |
+
gr.HTML(
|
| 156 |
+
"""
|
| 157 |
+
<div style="text-align: center; margin-bottom: 20px; padding-top: 10px;">
|
| 158 |
+
<h1 style="color: #fffffe; font-size: 2.2rem; font-weight: 700; margin-bottom: 5px; letter-spacing: -0.5px;">ReLiveStream</h1>
|
| 159 |
+
<p style="color: #94a1b2; font-size: 1rem;">Simulate realistic audience livestream chat replays based on reference documents</p>
|
| 160 |
+
</div>
|
| 161 |
+
"""
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
with gr.Tabs():
|
| 165 |
+
# Tab 1: Theater Player
|
| 166 |
+
with gr.TabItem("🎭 Theater Mode"):
|
| 167 |
+
with gr.Row():
|
| 168 |
+
sim_selector = gr.Radio(
|
| 169 |
+
choices=["Steve Jobs 1983 Speech (Demo)", "My Custom Simulation"],
|
| 170 |
+
value="Steve Jobs 1983 Speech (Demo)",
|
| 171 |
+
label="Choose Simulation to Play",
|
| 172 |
+
interactive=True
|
| 173 |
+
)
|
| 174 |
+
|
| 175 |
+
# The player frame
|
| 176 |
+
player_frame = gr.HTML(value=get_demo_html())
|
| 177 |
+
|
| 178 |
+
# Trigger updates when selection changes
|
| 179 |
+
sim_selector.change(
|
| 180 |
+
fn=play_simulation_selection,
|
| 181 |
+
inputs=[sim_selector],
|
| 182 |
+
outputs=[player_frame]
|
| 183 |
+
)
|
| 184 |
+
|
| 185 |
+
# Tab 2: Generator Config
|
| 186 |
+
with gr.TabItem("⚙️ Configure Custom Simulation"):
|
| 187 |
+
gr.Markdown(
|
| 188 |
+
"""
|
| 189 |
+
### Configure Your Custom Simulation
|
| 190 |
+
Input a YouTube link and upload a reference document (e.g. policy paper, textbook, research article).
|
| 191 |
+
The AI will download the video transcript, map it to key themes from the reference document,
|
| 192 |
+
and generate a synchronized chat replay representing varied internet users (skeptics, hype, experts, etc.).
|
| 193 |
+
"""
|
| 194 |
+
)
|
| 195 |
+
|
| 196 |
+
with gr.Row():
|
| 197 |
+
with gr.Column(scale=1):
|
| 198 |
+
yt_url_input = gr.Textbox(
|
| 199 |
+
label="YouTube URL or Video ID",
|
| 200 |
+
placeholder="https://www.youtube.com/watch?v=...",
|
| 201 |
+
info="Maximum length: 10 minutes recommended."
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
token_input = gr.Textbox(
|
| 205 |
+
label="Hugging Face Token (Optional)",
|
| 206 |
+
placeholder="Leave blank to use default token...",
|
| 207 |
+
type="password",
|
| 208 |
+
info="Token used to authenticate the Serverless Router."
|
| 209 |
+
)
|
| 210 |
+
|
| 211 |
+
pdf_input = gr.File(
|
| 212 |
+
label="Upload Reference PDF/Text",
|
| 213 |
+
file_types=[".pdf", ".txt"],
|
| 214 |
+
file_count="single"
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
fallback_text_input = gr.Textbox(
|
| 218 |
+
label="Or Paste Reference Text",
|
| 219 |
+
placeholder="Alternative if not uploading a file...",
|
| 220 |
+
lines=4
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
with gr.Column(scale=1):
|
| 224 |
+
fallback_srt_input = gr.Textbox(
|
| 225 |
+
label="Manual Captions / Transcript Paste (Optional Fallback)",
|
| 226 |
+
placeholder="If auto-fetching fails (IP block), paste the SRT format subtitles or plain text here...",
|
| 227 |
+
lines=12
|
| 228 |
+
)
|
| 229 |
+
|
| 230 |
+
generate_btn = gr.Button("🚀 Generate Simulation", variant="primary")
|
| 231 |
+
|
| 232 |
+
status_output = gr.Markdown(value="*Awaiting configuration...*")
|
| 233 |
+
|
| 234 |
+
# Link callback
|
| 235 |
+
generate_btn.click(
|
| 236 |
+
fn=handle_generation,
|
| 237 |
+
inputs=[
|
| 238 |
+
yt_url_input,
|
| 239 |
+
pdf_input,
|
| 240 |
+
fallback_text_input,
|
| 241 |
+
fallback_srt_input,
|
| 242 |
+
token_input
|
| 243 |
+
],
|
| 244 |
+
outputs=[
|
| 245 |
+
player_frame,
|
| 246 |
+
status_output,
|
| 247 |
+
sim_selector
|
| 248 |
+
]
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
if __name__ == "__main__":
|
| 252 |
+
demo.launch(theme=custom_theme)
|
pipeline.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import re
|
| 3 |
+
import json
|
| 4 |
+
import requests
|
| 5 |
+
import pymupdf4llm
|
| 6 |
+
from concurrent.futures import ThreadPoolExecutor
|
| 7 |
+
from youtube_transcript_api import YouTubeTranscriptApi
|
| 8 |
+
|
| 9 |
+
DEFAULT_HF_TOKEN = os.environ.get("HF_TOKEN", "")
|
| 10 |
+
FLASH_MODEL = "meta-llama/Llama-3.3-70B-Instruct"
|
| 11 |
+
PRO_MODEL = "deepseek-ai/DeepSeek-V4-Pro:novita"
|
| 12 |
+
|
| 13 |
+
def extract_youtube_video_id(url: str) -> str:
|
| 14 |
+
url = url.strip()
|
| 15 |
+
if len(url) == 11 and re.match(r'^[a-zA-Z0-9_-]{11}$', url):
|
| 16 |
+
return url
|
| 17 |
+
patterns = [
|
| 18 |
+
r'(?:v=|\/v\/|embed\/|shorts\/|youtu\.be\/|\/embed\/|\/watch\?v=|\&v=)([a-zA-Z0-9_-]{11})'
|
| 19 |
+
]
|
| 20 |
+
for pattern in patterns:
|
| 21 |
+
match = re.search(pattern, url)
|
| 22 |
+
if match:
|
| 23 |
+
return match.group(1)
|
| 24 |
+
return ""
|
| 25 |
+
|
| 26 |
+
def format_timestamp(seconds: float) -> str:
|
| 27 |
+
mins = int(seconds // 60)
|
| 28 |
+
secs = int(seconds % 60)
|
| 29 |
+
return f"[{mins:02d}:{secs:02d}]"
|
| 30 |
+
|
| 31 |
+
def format_transcript_lines(transcript: list) -> str:
|
| 32 |
+
lines = []
|
| 33 |
+
for entry in transcript:
|
| 34 |
+
time_str = format_timestamp(entry["start"])
|
| 35 |
+
lines.append(f"{time_str} {entry['text']}")
|
| 36 |
+
return "\n".join(lines)
|
| 37 |
+
|
| 38 |
+
def clean_json_text(text: str) -> str:
|
| 39 |
+
text = text.strip()
|
| 40 |
+
# Remove markdown code block wraps
|
| 41 |
+
if text.startswith("```json"):
|
| 42 |
+
text = text[7:]
|
| 43 |
+
elif text.startswith("```"):
|
| 44 |
+
text = text[3:]
|
| 45 |
+
if text.endswith("```"):
|
| 46 |
+
text = text[:-3]
|
| 47 |
+
return text.strip()
|
| 48 |
+
|
| 49 |
+
def call_hf_router(model: str, messages: list, token: str) -> str:
|
| 50 |
+
import time
|
| 51 |
+
headers = {
|
| 52 |
+
"Authorization": f"Bearer {token}",
|
| 53 |
+
"Content-Type": "application/json"
|
| 54 |
+
}
|
| 55 |
+
data = {
|
| 56 |
+
"model": model,
|
| 57 |
+
"messages": messages,
|
| 58 |
+
"temperature": 0.7
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
max_retries = 3
|
| 62 |
+
for attempt in range(max_retries):
|
| 63 |
+
try:
|
| 64 |
+
response = requests.post(
|
| 65 |
+
"https://router.huggingface.co/v1/chat/completions",
|
| 66 |
+
headers=headers,
|
| 67 |
+
json=data,
|
| 68 |
+
timeout=45
|
| 69 |
+
)
|
| 70 |
+
response.raise_for_status()
|
| 71 |
+
res_json = response.json()
|
| 72 |
+
return res_json["choices"][0]["message"]["content"]
|
| 73 |
+
except Exception as e:
|
| 74 |
+
if attempt == max_retries - 1:
|
| 75 |
+
raise e
|
| 76 |
+
print(f"HF API call failed (attempt {attempt+1}/{max_retries}): {e}. Retrying...")
|
| 77 |
+
time.sleep(2 ** attempt + 1)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def parse_srt(srt_text: str) -> list:
|
| 81 |
+
entries = []
|
| 82 |
+
# Normalize newlines
|
| 83 |
+
srt_text = srt_text.replace('\r\n', '\n').replace('\r', '\n')
|
| 84 |
+
blocks = re.split(r'\n\s*\n', srt_text.strip())
|
| 85 |
+
for block in blocks:
|
| 86 |
+
lines = [line.strip() for line in block.split('\n') if line.strip()]
|
| 87 |
+
if len(lines) >= 3:
|
| 88 |
+
time_line = lines[1]
|
| 89 |
+
text = " ".join(lines[2:])
|
| 90 |
+
# Match formats: 00:00:03,320 --> 00:00:05,960 or 00:00:03.320 --> 00:00:05.960
|
| 91 |
+
match = re.match(r'(\d+):(\d+):(\d+)[,\.](\d+)\s*-->\s*(\d+):(\d+):(\d+)[,\.](\d+)', time_line)
|
| 92 |
+
if match:
|
| 93 |
+
h1, m1, s1, ms1, h2, m2, s2, ms2 = map(int, match.groups())
|
| 94 |
+
start_secs = h1 * 3600 + m1 * 60 + s1 + ms1 / 1000.0
|
| 95 |
+
end_secs = h2 * 3600 + m2 * 60 + s2 + ms2 / 1000.0
|
| 96 |
+
duration = end_secs - start_secs
|
| 97 |
+
entries.append({
|
| 98 |
+
"text": text,
|
| 99 |
+
"start": start_secs,
|
| 100 |
+
"duration": duration
|
| 101 |
+
})
|
| 102 |
+
return entries
|
| 103 |
+
|
| 104 |
+
def parse_transcript_text(text: str) -> list:
|
| 105 |
+
text = text.strip()
|
| 106 |
+
if "-->" in text:
|
| 107 |
+
try:
|
| 108 |
+
entries = parse_srt(text)
|
| 109 |
+
if entries:
|
| 110 |
+
return entries
|
| 111 |
+
except Exception:
|
| 112 |
+
pass
|
| 113 |
+
|
| 114 |
+
# Fallback to plain text paragraph segmentation
|
| 115 |
+
paragraphs = [p.strip() for p in text.split('\n') if p.strip()]
|
| 116 |
+
entries = []
|
| 117 |
+
current_time = 0.0
|
| 118 |
+
for p in paragraphs:
|
| 119 |
+
words = len(p.split())
|
| 120 |
+
duration = max(3.0, min(15.0, words / 3.0))
|
| 121 |
+
entries.append({
|
| 122 |
+
"text": p,
|
| 123 |
+
"start": current_time,
|
| 124 |
+
"duration": duration
|
| 125 |
+
})
|
| 126 |
+
current_time += duration + 1.0
|
| 127 |
+
return entries
|
| 128 |
+
|
| 129 |
+
# --- STAGE 1a: Segment transcript ---
|
| 130 |
+
def stage_1a_segment_transcript(transcript_text: str, token: str) -> list:
|
| 131 |
+
system_prompt = (
|
| 132 |
+
"You are an AI assistant that segments a video transcript into logical topic segments. "
|
| 133 |
+
"You are given a transcript formatted with timestamps.\n"
|
| 134 |
+
"Your task is to group consecutive lines into segments of about 30 to 60 seconds (but align with natural topic boundaries).\n"
|
| 135 |
+
"For each segment, output:\n"
|
| 136 |
+
"- start: the start time in seconds (float or int)\n"
|
| 137 |
+
"- end: the end time in seconds (float or int)\n"
|
| 138 |
+
"- text: the concatenated text in this segment\n\n"
|
| 139 |
+
"Return ONLY a JSON list of segments. Do not include markdown wraps or conversational text outside the JSON."
|
| 140 |
+
)
|
| 141 |
+
user_prompt = f"Transcript:\n{transcript_text}"
|
| 142 |
+
|
| 143 |
+
messages = [
|
| 144 |
+
{"role": "system", "content": system_prompt},
|
| 145 |
+
{"role": "user", "content": user_prompt}
|
| 146 |
+
]
|
| 147 |
+
content = call_hf_router(FLASH_MODEL, messages, token)
|
| 148 |
+
cleaned = clean_json_text(content)
|
| 149 |
+
return json.loads(cleaned)
|
| 150 |
+
|
| 151 |
+
# --- STAGE 1b: Extract themes ---
|
| 152 |
+
def stage_1b_extract_themes(doc_text: str, token: str) -> str:
|
| 153 |
+
if len(doc_text) > 40000:
|
| 154 |
+
doc_text = doc_text[:40000] + "\n...[Document Truncated]..."
|
| 155 |
+
|
| 156 |
+
system_prompt = (
|
| 157 |
+
"You are an expert researcher. Extract the main themes, key concepts, core arguments, facts, terminology, "
|
| 158 |
+
"and themes from the following document.\n"
|
| 159 |
+
"Summarize them in a concise but detailed bulleted list that can be used by another AI model to generate chat reactions."
|
| 160 |
+
)
|
| 161 |
+
user_prompt = f"Document:\n{doc_text}"
|
| 162 |
+
|
| 163 |
+
messages = [
|
| 164 |
+
{"role": "system", "content": system_prompt},
|
| 165 |
+
{"role": "user", "content": user_prompt}
|
| 166 |
+
]
|
| 167 |
+
return call_hf_router(FLASH_MODEL, messages, token)
|
| 168 |
+
|
| 169 |
+
# --- STAGE 2: Generate draft comments (Pro model) ---
|
| 170 |
+
def stage_2_generate_draft_segment(segment: dict, themes: str, token: str) -> dict:
|
| 171 |
+
system_prompt = (
|
| 172 |
+
"You are simulating audience chat reactions for a livestream of an educational or historical video.\n"
|
| 173 |
+
"You are given:\n"
|
| 174 |
+
"1. The caption text of a specific video segment.\n"
|
| 175 |
+
"2. Extracted themes and concepts from a reference document.\n\n"
|
| 176 |
+
"Your task is to generate 8 to 15 draft chat messages from different users reacting to this video segment.\n"
|
| 177 |
+
"Crucially:\n"
|
| 178 |
+
"- The comments must react directly to the video segment content.\n"
|
| 179 |
+
"- The comments should reference ideas, concepts, facts, or perspectives from the reference document when relevant "
|
| 180 |
+
"(e.g., creating conceptual bridges, pointing out contradictions, or validating the video), but they MUST NOT directly quote or paraphrase the source document.\n"
|
| 181 |
+
"- Translate complex conceptual relationships between the video and the document into raw thoughts or reactions.\n\n"
|
| 182 |
+
"Format your response as a JSON object with:\n"
|
| 183 |
+
"- timestamp: {timestamp}\n"
|
| 184 |
+
"- _internal_logic: Briefly state how this segment relates to the source document themes (analogy, contradiction, validation).\n"
|
| 185 |
+
"- messages: a list of objects containing 'username' and 'text'.\n\n"
|
| 186 |
+
"Return ONLY the JSON. Do not include markdown wraps."
|
| 187 |
+
)
|
| 188 |
+
user_prompt = (
|
| 189 |
+
f"Video Segment ({segment['start']}s - {segment['end']}s):\n{segment['text']}\n\n"
|
| 190 |
+
f"Extracted Reference Document Themes:\n{themes}\n\n"
|
| 191 |
+
f"Generate draft comments for timestamp: {segment['start']}"
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
messages = [
|
| 195 |
+
{"role": "system", "content": system_prompt},
|
| 196 |
+
{"role": "user", "content": user_prompt}
|
| 197 |
+
]
|
| 198 |
+
content = call_hf_router(PRO_MODEL, messages, token)
|
| 199 |
+
cleaned = clean_json_text(content)
|
| 200 |
+
data = json.loads(cleaned)
|
| 201 |
+
data["timestamp"] = segment["start"]
|
| 202 |
+
return data
|
| 203 |
+
|
| 204 |
+
# --- STAGE 3: Style and pacing (Flash model) ---
|
| 205 |
+
def stage_3_stylize_segment(draft_data: dict, token: str) -> dict:
|
| 206 |
+
system_prompt = (
|
| 207 |
+
"You are a style polisher for livestream chat replays (YouTube/Twitch).\n"
|
| 208 |
+
"Your job is to take raw draft chat messages and rewrite them to sound like authentic, lively internet comments.\n\n"
|
| 209 |
+
"Apply these stylistic rules:\n"
|
| 210 |
+
"- Make them short and concise.\n"
|
| 211 |
+
"- Inject internet slang (e.g., lol, wtf, lmao, fr, no cap, ngl, bruh) and standard emotes (e.g., LUL, PogChamp, Kappa, MonkaS, BibleThump, 5Head, Pog, Pepega).\n"
|
| 212 |
+
"- Make username/text combinations feel natural. Some users can be experts reading into philosophical tension; some take things at face value; some only react emotionally or to video aesthetics; some use sarcasm/memes.\n"
|
| 213 |
+
"- Vary message lengths and pacing.\n"
|
| 214 |
+
"- Avoid sounding like AI-generated summaries or formal text.\n\n"
|
| 215 |
+
"Return ONLY the updated JSON with the exact same structure. Do not include markdown wraps."
|
| 216 |
+
)
|
| 217 |
+
user_prompt = f"Draft JSON:\n{json.dumps(draft_data)}"
|
| 218 |
+
|
| 219 |
+
messages = [
|
| 220 |
+
{"role": "system", "content": system_prompt},
|
| 221 |
+
{"role": "user", "content": user_prompt}
|
| 222 |
+
]
|
| 223 |
+
content = call_hf_router(FLASH_MODEL, messages, token)
|
| 224 |
+
cleaned = clean_json_text(content)
|
| 225 |
+
return json.loads(cleaned)
|
| 226 |
+
|
| 227 |
+
# --- Full Pipeline Orchestration ---
|
| 228 |
+
def run_livestream_pipeline(video_id: str, doc_text: str, transcript_text: str = None, token: str = None) -> list:
|
| 229 |
+
if not token:
|
| 230 |
+
token = os.environ.get("HF_TOKEN", DEFAULT_HF_TOKEN)
|
| 231 |
+
if not token:
|
| 232 |
+
raise ValueError(
|
| 233 |
+
"Hugging Face API Token not found. Please set the 'HF_TOKEN' secret in your Space settings "
|
| 234 |
+
"or provide it in the input box."
|
| 235 |
+
)
|
| 236 |
+
|
| 237 |
+
if transcript_text:
|
| 238 |
+
print("Parsing provided transcript text...")
|
| 239 |
+
raw_transcript = parse_transcript_text(transcript_text)
|
| 240 |
+
else:
|
| 241 |
+
print("Fetching YouTube transcript from API...")
|
| 242 |
+
try:
|
| 243 |
+
api = YouTubeTranscriptApi()
|
| 244 |
+
raw_transcript = api.fetch(video_id)
|
| 245 |
+
except Exception as e:
|
| 246 |
+
raise ValueError(
|
| 247 |
+
f"YouTube transcript could not be fetched automatically: {e}. "
|
| 248 |
+
"Please provide the transcript text manually."
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
transcript_text_formatted = format_transcript_lines(raw_transcript)
|
| 252 |
+
|
| 253 |
+
print("Stage 1a: Segmenting transcript...")
|
| 254 |
+
segments = stage_1a_segment_transcript(transcript_text_formatted, token)
|
| 255 |
+
print(f"Segmented into {len(segments)} blocks.")
|
| 256 |
+
|
| 257 |
+
print("Stage 1b: Extracting themes from document...")
|
| 258 |
+
themes = stage_1b_extract_themes(doc_text, token)
|
| 259 |
+
print("Themes extracted successfully.")
|
| 260 |
+
|
| 261 |
+
# Stage 2: Parallel comment generation
|
| 262 |
+
print("Stage 2: Generating draft comments (Pro model)...")
|
| 263 |
+
draft_segments = []
|
| 264 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
| 265 |
+
futures = [
|
| 266 |
+
executor.submit(stage_2_generate_draft_segment, seg, themes, token)
|
| 267 |
+
for seg in segments
|
| 268 |
+
]
|
| 269 |
+
for fut in futures:
|
| 270 |
+
draft_segments.append(fut.result())
|
| 271 |
+
|
| 272 |
+
# Stage 3: Parallel stylization
|
| 273 |
+
print("Stage 3: Stylizing comments...")
|
| 274 |
+
final_segments = []
|
| 275 |
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
| 276 |
+
futures = [
|
| 277 |
+
executor.submit(stage_3_stylize_segment, draft, token)
|
| 278 |
+
for draft in draft_segments
|
| 279 |
+
]
|
| 280 |
+
for fut in futures:
|
| 281 |
+
final_segments.append(fut.result())
|
| 282 |
+
|
| 283 |
+
# Sort segments by timestamp to be safe
|
| 284 |
+
final_segments.sort(key=lambda x: x.get("timestamp", 0))
|
| 285 |
+
return final_segments
|
requirements.txt
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.0.0
|
| 2 |
+
pymupdf4llm
|
| 3 |
+
youtube-transcript-api
|
| 4 |
+
requests
|
| 5 |
+
openai
|
sample/demo_chat.json
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"timestamp": 0,
|
| 4 |
+
"_internal_logic": "The video segment shows a person using manual, effortful methods (cutting, pasting, dial-up) to create and share a drawing. This contrasts with modern frictionless AI tools. The chat messages react to this by referencing the value of effort, desirable difficulties, and personal connection, aligning with the document's themes about friction and AI's potential to remove meaningful struggle.",
|
| 5 |
+
"messages": [
|
| 6 |
+
{
|
| 7 |
+
"username": "RetroTechFan",
|
| 8 |
+
"text": "lmao cutting and pasting? so extra compared to drag and drop lol"
|
| 9 |
+
},
|
| 10 |
+
{
|
| 11 |
+
"username": "AspenLocal",
|
| 12 |
+
"text": "omg dial-up!! the wait was part of the hype, ngl"
|
| 13 |
+
},
|
| 14 |
+
{
|
| 15 |
+
"username": "ArtLover22",
|
| 16 |
+
"text": "no cap, the struggle of making it is what makes it fire, PogChamp"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"username": "AI_Ethics_Watcher",
|
| 20 |
+
"text": "wtf if he had ai, would it even feel like an accomplishment? MonkaS"
|
| 21 |
+
},
|
| 22 |
+
{
|
| 23 |
+
"username": "LearningNerd",
|
| 24 |
+
"text": "desirable difficulties 101 - the struggle is what makes it stick, fr"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"username": "TechHistorian",
|
| 28 |
+
"text": "early internet was all about the grind, now it's all frictionless, BibleThump"
|
| 29 |
+
},
|
| 30 |
+
{
|
| 31 |
+
"username": "PhilosophyChick",
|
| 32 |
+
"text": "struggling with scissors and glue = personal touch, AI removes that, Kappa"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"username": "DialUpDreamer",
|
| 36 |
+
"text": "electronic mailbox in aspen = OG social media, so much more intentional, 5Head"
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"username": "CognitiveScientist",
|
| 40 |
+
"text": "effort paradox, we seek challenges even when easier ways exist, Pog"
|
| 41 |
+
},
|
| 42 |
+
{
|
| 43 |
+
"username": "FutureFear",
|
| 44 |
+
"text": "if ai existed back then, would he have even bothered? Pepega, idk man"
|
| 45 |
+
}
|
| 46 |
+
]
|
| 47 |
+
},
|
| 48 |
+
{
|
| 49 |
+
"timestamp": 20,
|
| 50 |
+
"_internal_logic": "The video segment discusses the early days of personal computers as a new medium and a critical 15-year window to integrate them into society. This mirrors the current inflection point with AI, where we also face a choice to harness it thoughtfully while preserving necessary human friction—or to allow it to short-circuit effort and learning. The speaker's 'do it great or do it so' creates a conceptual bridge to the document's tension between beneficial friction and the risks of over-reliance.",
|
| 51 |
+
"messages": [
|
| 52 |
+
{
|
| 53 |
+
"username": "TechHistorian",
|
| 54 |
+
"text": "Lol this AI moment feels like deja vu, no cap. Will we blow it again in 15 yrs?"
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"username": "SyntaxSculptor",
|
| 58 |
+
"text": "Computers as a 'new medium'... now AI. But seamless convenience might make us skip the hard part, PogChamp"
|
| 59 |
+
},
|
| 60 |
+
{
|
| 61 |
+
"username": "DeepThoughtLurker",
|
| 62 |
+
"text": "Learning BASIC was a struggle, but it wired my brain differently, ngl. Is AI removing that useful struggle? MonkaS"
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"username": "RetroFuturist",
|
| 66 |
+
"text": "'Do it great or do it so'... that's the choice, fr. Keeping the grit means 'great', imo"
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"username": "CodexNomad",
|
| 70 |
+
"text": "Omg, first phase of AI will be over before we notice the skills we've lost, wtf"
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"username": "MeaningMiner",
|
| 74 |
+
"text": "Maybe early computer pioneers understood something we're about to forget, BibleThump. Removing friction = short-circuiting understanding?"
|
| 75 |
+
},
|
| 76 |
+
{
|
| 77 |
+
"username": "SociotechPonder",
|
| 78 |
+
"text": "Meeting AI for the first time... and we wanna automate away the human bits, Kappa. What's the point tho?"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"username": "AnalogSoul",
|
| 82 |
+
"text": "We're in a 15-yr window, just like back then. Are we designing AI to help or replace us, 5Head"
|
| 83 |
+
},
|
| 84 |
+
{
|
| 85 |
+
"username": "WisdomBuffer",
|
| 86 |
+
"text": "Effort paradox, bruh: computer revolution was hard, and that gave it value. Will AI offer the same depth? Pog"
|
| 87 |
+
},
|
| 88 |
+
{
|
| 89 |
+
"username": "FutureProofMind",
|
| 90 |
+
"text": "Document's warning is real, imo. It's not just about tools, it's about how they shape cognition, Pepega"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"username": "QuietRevolutionary",
|
| 94 |
+
"text": "Loneliness as a signal, lol. Will AI companions just silence it or push us toward real connection? LUL"
|
| 95 |
+
},
|
| 96 |
+
{
|
| 97 |
+
"username": "PixelPhilosopher",
|
| 98 |
+
"text": "We're in the awkward dating phase with AI, ngl. Next 15 yrs will define whether it's a relationship or a crutch, no cap"
|
| 99 |
+
}
|
| 100 |
+
]
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"timestamp": 43,
|
| 104 |
+
"_internal_logic": "analogy: The segment exemplifies the value of conceptual friction (desirable difficulties) in learning, as the speaker prompts the audience to struggle with defining a program rather than receiving an effortless explanation. This aligns with the document's argument that friction is essential for deep understanding.",
|
| 105 |
+
"messages": [
|
| 106 |
+
{
|
| 107 |
+
"username": "TechHistorian",
|
| 108 |
+
"text": "LUL the silence is golden, no cap. Real learning happens when we're stuck"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"username": "LearningFan",
|
| 112 |
+
"text": "Desirable difficulty 101, wtf would we learn if it was easy?"
|
| 113 |
+
},
|
| 114 |
+
{
|
| 115 |
+
"username": "AI_Skeptic",
|
| 116 |
+
"text": "Bruh, we'd just ask ChatGPT and miss the whole point. Struggle is key"
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"username": "DeepThinker42",
|
| 120 |
+
"text": "He's making us earn it. Effort paradox, ngl, it's real"
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"username": "CuriousCat",
|
| 124 |
+
"text": "LOL nobody knows wth a program is. It's like defining 'thinking'... PogChamp"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"username": "NeuroNerd",
|
| 128 |
+
"text": "That 'sort of sort of' is brains encoding info deeply, MonkaS. Friction for the win"
|
| 129 |
+
},
|
| 130 |
+
{
|
| 131 |
+
"username": "AnalogHeart",
|
| 132 |
+
"text": "AI tutor just blurting out a def? No thanks, human awkwardness is where it's at, Pepega"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"username": "CodePoet",
|
| 136 |
+
"text": "Concepts get richer when we grapple, not when pre-digested. Kappa"
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"username": "FuturePast",
|
| 140 |
+
"text": "Anti-frictionless AI argument in one clip. Let us be confused, let us think, 5Head"
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"username": "SocraticDog",
|
| 144 |
+
"text": "Jobs is like an analog AI, generating friction. The irony tho... BibleThump"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"username": "MindGym",
|
| 148 |
+
"text": "That hesitation is cognitive effort, burning calories. Good thing, imo, Pog"
|
| 149 |
+
}
|
| 150 |
+
]
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"timestamp": 59,
|
| 154 |
+
"_internal_logic": "The video's description of computer programs as pure, archetypal ideas connects to the document's emphasis on intellectual work requiring cognitive friction. The analogy to television programming sets up a contrast between active creation (programming) and passive consumption (TV), mirroring the document's concern about AI removing the 'desirable difficulties' that make learning and creation meaningful.",
|
| 155 |
+
"messages": [
|
| 156 |
+
{
|
| 157 |
+
"username": "ByteWanderer",
|
| 158 |
+
"text": "lol programs are just ideas? no cap, thats deep. coding is like pure mind magic LUL"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"username": "DeepThoughts42",
|
| 162 |
+
"text": "wtf i know right? we love the struggle of coding, even tho its hard. thats the fun part, ngl PogChamp"
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"username": "NeonNerdette",
|
| 166 |
+
"text": "omg every bug is like a flaw in ur mental model MonkaS. debugging is a brain workout, no excuses"
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"username": "QuantumLeap99",
|
| 170 |
+
"text": "coding is like conjuring reality from thin air, fr. no wonder its so addictive, lol"
|
| 171 |
+
},
|
| 172 |
+
{
|
| 173 |
+
"username": "AI_Hawk",
|
| 174 |
+
"text": "ai just gives u the code, but then u miss the friction, bruh. thats why we forget everything it writes, no cap"
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"username": "PhiloCoder",
|
| 178 |
+
"text": "never seen an electron, never seen a program... both are just inferred from effects, biblethump. what even is 'real'?"
|
| 179 |
+
},
|
| 180 |
+
{
|
| 181 |
+
"username": "GrindsetMaximalist",
|
| 182 |
+
"text": "tv comparison is savage, lol. tv rots ur brain, coding sharpens it. choose ur archetype, 5Head"
|
| 183 |
+
},
|
| 184 |
+
{
|
| 185 |
+
"username": "Retro_Futurist",
|
| 186 |
+
"text": "ada lovelace was literally an archetypal programmer, pog. ideas on paper, turned into logic"
|
| 187 |
+
},
|
| 188 |
+
{
|
| 189 |
+
"username": "ZenCipher",
|
| 190 |
+
"text": "programming is turning thought into logic, no cap. remove the struggle, u lose the art. ai cant replicate that, pepega"
|
| 191 |
+
},
|
| 192 |
+
{
|
| 193 |
+
"username": "PonderThis22",
|
| 194 |
+
"text": "ai companions seem real, but are just patterns, ngl. no friction, no depth... just like tv friendships, kappa"
|
| 195 |
+
},
|
| 196 |
+
{
|
| 197 |
+
"username": "Sudo_Mystic",
|
| 198 |
+
"text": "code has no physical form, but creates everything digital... thats the ultimate paradox, lol. we love the invisible labor, monkas"
|
| 199 |
+
}
|
| 200 |
+
]
|
| 201 |
+
},
|
| 202 |
+
{
|
| 203 |
+
"timestamp": 98,
|
| 204 |
+
"_internal_logic": "Analogy: The video segment describes how television can flawlessly recreate emotional experiences, acting as a 'frictionless' medium for feelings. This mirrors the document's concern about AI removing friction from intellectual and social processes, potentially diminishing the value of the authentic, effortful experience.",
|
| 205 |
+
"messages": [
|
| 206 |
+
{
|
| 207 |
+
"username": "history_buff99",
|
| 208 |
+
"text": "lol @ JFK footage still hitting hard, but does TV sanitize the pain? fr"
|
| 209 |
+
},
|
| 210 |
+
{
|
| 211 |
+
"username": "moonwatcher",
|
| 212 |
+
"text": "PogChamp moon landing replays get me every time, but where's the grit? LUL"
|
| 213 |
+
},
|
| 214 |
+
{
|
| 215 |
+
"username": "skeptical_sam",
|
| 216 |
+
"text": "wtf, pre-packaged emotions? no cap, that's wild"
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"username": "deep_thinker",
|
| 220 |
+
"text": "Interesting point... TV = no friction, just feelings on a platter, ngl"
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"username": "media_critic",
|
| 224 |
+
"text": "BibleThump do we lose our authentic reactions if we only get them from TV? MonkaS"
|
| 225 |
+
},
|
| 226 |
+
{
|
| 227 |
+
"username": "nostalgia_fan",
|
| 228 |
+
"text": "I'm a sucker for nostalgia, lol. TV trains our emotional responses, Pepega"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"username": "effort_matters",
|
| 232 |
+
"text": "5Head the moon landing was earned, not just a highlight reel. no cap"
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"username": "ai_observer",
|
| 236 |
+
"text": "Kappa TV = early AI? replicating experiences without the grind, bruh"
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"username": "reality_check",
|
| 240 |
+
"text": "Pog but isn't perfect TV kinda... limited? you can't feel the tension, lol"
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"username": "balance_seeker",
|
| 244 |
+
"text": "fascinating, it captures the feeling, but not the learning. we need struggle, ngl, to really get it"
|
| 245 |
+
}
|
| 246 |
+
]
|
| 247 |
+
},
|
| 248 |
+
{
|
| 249 |
+
"timestamp": 139,
|
| 250 |
+
"_internal_logic": "The video's emphasis on capturing underlying principles mirrors the document's concern about AI abstracting away the messy, effortful reality. It validates the idea that while principles can generate infinite variations, the original friction of experience—argued in the document—might be essential for deeper learning and meaning.",
|
| 251 |
+
"messages": [
|
| 252 |
+
{
|
| 253 |
+
"username": "CodeDreamer",
|
| 254 |
+
"text": "lol so programming is like extracting laws from experience. but does AI really 'get' it if it didnt live it?"
|
| 255 |
+
},
|
| 256 |
+
{
|
| 257 |
+
"username": "FrictionFan",
|
| 258 |
+
"text": "no cap, struggle is key. if computers just capture principles, we lose the grind LUL"
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"username": "RetroGameNerd",
|
| 262 |
+
"text": "Pong is the perfect example, bruh! unique matches, consistent physics PogChamp"
|
| 263 |
+
},
|
| 264 |
+
{
|
| 265 |
+
"username": "DeepThoughts42",
|
| 266 |
+
"text": "does learning need messy experience or just principles? doc says desirable difficulties matter, ngl"
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"username": "AI_Skeptic",
|
| 270 |
+
"text": "AI gives us 'rules' of social interaction, but without human effort, it's hollow, fr"
|
| 271 |
+
},
|
| 272 |
+
{
|
| 273 |
+
"username": "PhilosoGamer",
|
| 274 |
+
"text": "effort paradox, dude! we enjoy games cuz we struggle within rules. no friction, no thrill BibleThump"
|
| 275 |
+
},
|
| 276 |
+
{
|
| 277 |
+
"username": "LazyCoder",
|
| 278 |
+
"text": "wish learning programming was easier, but then i'd forget lol. struggle is the teacher, i guess"
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"username": "MeaningSeeker",
|
| 282 |
+
"text": "principles vs experience: map vs territory. AI gives us map, but we need to walk the walk, MonkaS"
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"username": "OldSchoolDev",
|
| 286 |
+
"text": "programming doesn't copy experience, it finds invariant. gotta wrestle with reality first, Kappa"
|
| 287 |
+
},
|
| 288 |
+
{
|
| 289 |
+
"username": "NeoHumanist",
|
| 290 |
+
"text": "this video from 60s predicted AI replacing effort. now we got AI companions, wild Pepega"
|
| 291 |
+
}
|
| 292 |
+
]
|
| 293 |
+
},
|
| 294 |
+
{
|
| 295 |
+
"timestamp": 185,
|
| 296 |
+
"_internal_logic": "The segment describes a classic educational game that embodies 'desirable difficulties' and 'friction' in learning, contrasting with modern AI tools that remove such effort. The game's design forces players to struggle with resource management, leading to deeper understanding, which aligns with the document's argument about the importance of effort in learning.",
|
| 297 |
+
"messages": [
|
| 298 |
+
{
|
| 299 |
+
"username": "Lurker4Lyfe",
|
| 300 |
+
"text": "anti-friction simulator, no cap. grain storage on you, lol"
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"username": "HistoryBuff99",
|
| 304 |
+
"text": "my uncle learned more from Hammurabi than a semester of lectures, ngl. struggle is real"
|
| 305 |
+
},
|
| 306 |
+
{
|
| 307 |
+
"username": "TechieTina",
|
| 308 |
+
"text": "modern version with AI advisor? kid learns zero, wtf. friction is the lesson, fr"
|
| 309 |
+
},
|
| 310 |
+
{
|
| 311 |
+
"username": "OldSchoolGamer",
|
| 312 |
+
"text": "peak 'effort paradox' - hate the decisions, can't stop, MonkaS"
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"username": "DeepThought42",
|
| 316 |
+
"text": "that moment you starve your people... feedback loop, no tutorial can replicate, BibleThump"
|
| 317 |
+
},
|
| 318 |
+
{
|
| 319 |
+
"username": "SkepticalSara",
|
| 320 |
+
"text": "70s game understood desirable difficulties better than AI apps today, sad! PogChamp"
|
| 321 |
+
},
|
| 322 |
+
{
|
| 323 |
+
"username": "CasualObsvr",
|
| 324 |
+
"text": "i'd ask ChatGPT for optimal planting, then realize i cheated, lol"
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"username": "FrictionFanatic",
|
| 328 |
+
"text": "cognitive muscle, every bushel count is a workout, 5Head"
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"username": "GameDesignNerd",
|
| 332 |
+
"text": "no hand-holding, frustration = lesson, notice that? Kappa"
|
| 333 |
+
},
|
| 334 |
+
{
|
| 335 |
+
"username": "NostalgiaTrip",
|
| 336 |
+
"text": "playing as a kid taught me decisions have weight, can't replicate with AI, Pepega"
|
| 337 |
+
},
|
| 338 |
+
{
|
| 339 |
+
"username": "ThoughtfulTroll",
|
| 340 |
+
"text": "ancient Sumerian kings had it rough, no ChatGPT, lmao"
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"username": "EduInnovator",
|
| 344 |
+
"text": "removing friction from learning tools is a mistake, struggle builds reasoning, no cap"
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"username": "RandomRambler",
|
| 348 |
+
"text": "lack of polish makes it more engaging, imagine the consequences, Pog"
|
| 349 |
+
}
|
| 350 |
+
]
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"timestamp": 243,
|
| 354 |
+
"_internal_logic": "The video segment illustrates an interactive, effortful learning method where kids engage deeply with a crude macroeconomic model, validating the reference document's emphasis on friction and struggle as essential for learning. The simulation provides desirable difficulties through trial-and-error and emotional consequences (starvation), contrasting with frictionless AI tools that might short-circuit such deep engagement.",
|
| 355 |
+
"messages": [
|
| 356 |
+
{
|
| 357 |
+
"username": "EcoSimFanatic",
|
| 358 |
+
"text": "Lmao this is the effort paradox, bruh. Kids love the struggle"
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"username": "FrictionMatters",
|
| 362 |
+
"text": "Desirable difficulties, no cap. Clunkiness is key"
|
| 363 |
+
},
|
| 364 |
+
{
|
| 365 |
+
"username": "LearnByDoing99",
|
| 366 |
+
"text": "When your village starves, that's the real learning. No AI can replicate that emotional sting, ngl"
|
| 367 |
+
},
|
| 368 |
+
{
|
| 369 |
+
"username": "OldSchoolEd",
|
| 370 |
+
"text": "PogChamp to this. Give kids consequences and they'll grind for hours, lol"
|
| 371 |
+
},
|
| 372 |
+
{
|
| 373 |
+
"username": "SkepticalBot",
|
| 374 |
+
"text": "But where's the line before it's too easy? Don't wanna make it a crutch, fr"
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"username": "NeuroNerd42",
|
| 378 |
+
"text": "Research backs this up, btw. Struggle = deeper encoding. They'll remember that planting rule forever, MonkaS"
|
| 379 |
+
},
|
| 380 |
+
{
|
| 381 |
+
"username": "ChillPanda23",
|
| 382 |
+
"text": "lol imagine an AI just saying 'plant 100 units'. zero effort, zero retention, Pog"
|
| 383 |
+
},
|
| 384 |
+
{
|
| 385 |
+
"username": "HistoryBuff_01",
|
| 386 |
+
"text": "This is how we used to learn, through play and sim. Modern tools are robbing us of grit, no cap"
|
| 387 |
+
},
|
| 388 |
+
{
|
| 389 |
+
"username": "AI_Observer",
|
| 390 |
+
"text": "Fascinating contrast with AI tutoring. Genuine feedback loops > spoon-fed answers, 5Head"
|
| 391 |
+
},
|
| 392 |
+
{
|
| 393 |
+
"username": "EduRebel",
|
| 394 |
+
"text": "The 'crude' model makes them fill in gaps mentally. That's the friction we need, BibleThump"
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"username": "DeepThinker2023",
|
| 398 |
+
"text": "Unintended consequences are the best teacher, imo. Hot village effect, anyone? Kappa"
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"username": "SimulatedMind",
|
| 402 |
+
"text": "This predates AI hype, but proves the point: effortful problem-solving > shortcuts, Pepega"
|
| 403 |
+
},
|
| 404 |
+
{
|
| 405 |
+
"username": "ParentGamer",
|
| 406 |
+
"text": "I'd rather my kid fail and learn than get perfect grades with AI help, ngl. This segment is gold, Pog"
|
| 407 |
+
},
|
| 408 |
+
{
|
| 409 |
+
"username": "EconProf88",
|
| 410 |
+
"text": "Engagement born from friction, not AI. Notice he said 'they'll sit there for hours'? That's the power of struggle, LUL"
|
| 411 |
+
}
|
| 412 |
+
]
|
| 413 |
+
},
|
| 414 |
+
{
|
| 415 |
+
"timestamp": 305,
|
| 416 |
+
"_internal_logic": "The video segment highlights the value of direct access to sources (books) but laments the inability to ask questions. This directly parallels the document's discussion on AI as an interactive intermediary that removes the friction of unanswered questions, but potentially undermines the deep learning that comes from struggling with static texts. The segment creates a tension: the desire for dialogue versus the benefits of solitary intellectual effort. Comments bridge this by reacting to AI as a solution to Aristotle's silence, while questioning whether that convenience erodes the 'desirable difficulties' of reading.",
|
| 417 |
+
"messages": [
|
| 418 |
+
{
|
| 419 |
+
"username": "CuriousCat99",
|
| 420 |
+
"text": "Lol, Aristotle on speed dial now! No cap, AI is a game changer"
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"username": "DeepThinkr",
|
| 424 |
+
"text": "But what if the struggle is what makes it worth it? Ngl, instant answers are overrated"
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"username": "FrictionFan",
|
| 428 |
+
"text": "PogChamp to that gap between you and the author! It's where the magic happens, bruh"
|
| 429 |
+
},
|
| 430 |
+
{
|
| 431 |
+
"username": "LazySloth",
|
| 432 |
+
"text": "I'm lowkey here for AI answering all my Qs. Less stress, more gain, lol"
|
| 433 |
+
},
|
| 434 |
+
{
|
| 435 |
+
"username": "OldSoul_Reader",
|
| 436 |
+
"text": "Books were pure, no filter. Now we're adding a middleman? Kappa"
|
| 437 |
+
},
|
| 438 |
+
{
|
| 439 |
+
"username": "NeuroNerd23",
|
| 440 |
+
"text": "Science says easy answers = shallow learning. That 'can't ask' might be a blessing in disguise, MonkaS"
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"username": "PlatoStan",
|
| 444 |
+
"text": "Imagine being in Plato's dialogues, tho! AI could make it happen, but would it feel as rewarding? 5Head"
|
| 445 |
+
},
|
| 446 |
+
{
|
| 447 |
+
"username": "CypherPunk42",
|
| 448 |
+
"text": "Effort paradox, folks! We want it easy, but the struggle is what makes it real, no cap"
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"username": "BookHoarder",
|
| 452 |
+
"text": "Books don't coddle you, they make you level up. AI might make it too easy, Pepega"
|
| 453 |
+
},
|
| 454 |
+
{
|
| 455 |
+
"username": "ZenMonk",
|
| 456 |
+
"text": "The silence after reading... that's where the growth happens. Immediate answers can be a distraction, BibleThump"
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"username": "TechOptimist",
|
| 460 |
+
"text": "But think of all the people who don't have access to great teachers! AI could be a total game changer, Pog"
|
| 461 |
+
},
|
| 462 |
+
{
|
| 463 |
+
"username": "SocraticMethod",
|
| 464 |
+
"text": "The beauty is in the questions you ask yourself. AI answering them for you might kill that inner dialogue, LUL"
|
| 465 |
+
}
|
| 466 |
+
]
|
| 467 |
+
},
|
| 468 |
+
{
|
| 469 |
+
"timestamp": 357,
|
| 470 |
+
"_internal_logic": "The segment's vision of using AI to simulate a deceased thinker's responses removes the inherent friction of loss and the effort of interpretation, directly contradicting the document's emphasis on the value of 'desirable difficulties' and the 'effort paradox.' The speaker's excitement overlooks how struggling with incomplete knowledge and absence might be essential to intellectual growth.",
|
| 471 |
+
"messages": [
|
| 472 |
+
{
|
| 473 |
+
"username": "DeepThink42",
|
| 474 |
+
"text": "lol no cap, using AI to simulate dead thinkers is cheating, fr"
|
| 475 |
+
},
|
| 476 |
+
{
|
| 477 |
+
"username": "CuriousCat",
|
| 478 |
+
"text": "this 'friction removal' is lowkey terrifying, ngl LUL"
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"username": "BookwormBill",
|
| 482 |
+
"text": "PogChamp to the struggle of interpreting fragments, that's where the magic's at"
|
| 483 |
+
},
|
| 484 |
+
{
|
| 485 |
+
"username": "EffortParadoxFan",
|
| 486 |
+
"text": "doesn't the effort paradox say we value things more when we grind for them? instant answers = no thanks, bruh"
|
| 487 |
+
},
|
| 488 |
+
{
|
| 489 |
+
"username": "GhostOfSocrates",
|
| 490 |
+
"text": "give me the sweat and tears of reading primary texts any day, MonkaS"
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"username": "LoneWolf99",
|
| 494 |
+
"text": "loneliness for a dead philosopher is weirdly poetic, tho... maybe that's what drives us to dig deeper"
|
| 495 |
+
},
|
| 496 |
+
{
|
| 497 |
+
"username": "SeanceSkeptic",
|
| 498 |
+
"text": "this digital seance is cool and all, but what if people stop reading the originals? Kappa"
|
| 499 |
+
},
|
| 500 |
+
{
|
| 501 |
+
"username": "NoPainNoGain",
|
| 502 |
+
"text": "no struggle, no deep understanding, period. 5Head"
|
| 503 |
+
},
|
| 504 |
+
{
|
| 505 |
+
"username": "JunkFoodMind",
|
| 506 |
+
"text": "intellectual junk food, anyone? easy to consume, but where's the substance? Pepega"
|
| 507 |
+
},
|
| 508 |
+
{
|
| 509 |
+
"username": "MessyWisdom",
|
| 510 |
+
"text": "what if the AI smooths away the good stuff? the messy bits are where it's at, BibleThump"
|
| 511 |
+
}
|
| 512 |
+
]
|
| 513 |
+
},
|
| 514 |
+
{
|
| 515 |
+
"timestamp": 422,
|
| 516 |
+
"_internal_logic": "The speaker's motivation implicitly validates the document's emphasis on preserving beneficial friction and effort, as they are choosing a path that likely involves more struggle for meaningful outcomes.",
|
| 517 |
+
"messages": [
|
| 518 |
+
{
|
| 519 |
+
"username": "CuriousCat",
|
| 520 |
+
"text": "lol he's all about that struggle life 🔥"
|
| 521 |
+
},
|
| 522 |
+
{
|
| 523 |
+
"username": "DeepThinker42",
|
| 524 |
+
"text": "PogChamp keeping that friction alive is key"
|
| 525 |
+
},
|
| 526 |
+
{
|
| 527 |
+
"username": "EffortEnthusiast",
|
| 528 |
+
"text": "no cap, removing effort makes learning pointless fr"
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"username": "FrictionFan",
|
| 532 |
+
"text": "yaaas desirable difficulties all the way 👍"
|
| 533 |
+
},
|
| 534 |
+
{
|
| 535 |
+
"username": "NoEasyPaths",
|
| 536 |
+
"text": "he's lowkey rejecting the easy path on purpose lol"
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"username": "LonelySignal",
|
| 540 |
+
"text": "BibleThump loneliness is just a signal, not a bug"
|
| 541 |
+
},
|
| 542 |
+
{
|
| 543 |
+
"username": "ParadoxHunter",
|
| 544 |
+
"text": "it's all about the effort paradox, we need challenges ngl"
|
| 545 |
+
},
|
| 546 |
+
{
|
| 547 |
+
"username": "MeaningMaker",
|
| 548 |
+
"text": "so inspiring, pushing back against AI making everything too easy Pog"
|
| 549 |
+
},
|
| 550 |
+
{
|
| 551 |
+
"username": "SupplementorSub",
|
| 552 |
+
"text": "is he saying AI should be a supplement, not a substitute? 👀"
|
| 553 |
+
},
|
| 554 |
+
{
|
| 555 |
+
"username": "RecallRider",
|
| 556 |
+
"text": "LUL reminds me of how struggling leads to deeper retention"
|
| 557 |
+
},
|
| 558 |
+
{
|
| 559 |
+
"username": "GradSchoolGrind",
|
| 560 |
+
"text": "same, doing my PhD on this and his reasoning is spot on, no cap"
|
| 561 |
+
},
|
| 562 |
+
{
|
| 563 |
+
"username": "FrictionIsHuman",
|
| 564 |
+
"text": "friction is what makes life meaningful, bruh MonkaS"
|
| 565 |
+
}
|
| 566 |
+
]
|
| 567 |
+
}
|
| 568 |
+
]
|
theater_template.html
ADDED
|
@@ -0,0 +1,541 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>ReLiveStream Theater</title>
|
| 6 |
+
<!-- Google Fonts: Inter -->
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
| 9 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
| 10 |
+
|
| 11 |
+
<style>
|
| 12 |
+
:root {
|
| 13 |
+
--bg-color: #0b0c10;
|
| 14 |
+
--panel-bg: rgba(22, 26, 35, 0.65);
|
| 15 |
+
--border-color: rgba(255, 255, 255, 0.08);
|
| 16 |
+
--accent-color: #7f5af0;
|
| 17 |
+
--accent-glow: rgba(127, 90, 240, 0.35);
|
| 18 |
+
--text-color: #fffffe;
|
| 19 |
+
--text-muted: #94a1b2;
|
| 20 |
+
--live-color: #ff0055;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
* {
|
| 24 |
+
box-sizing: border-box;
|
| 25 |
+
margin: 0;
|
| 26 |
+
padding: 0;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
body {
|
| 30 |
+
font-family: 'Inter', sans-serif;
|
| 31 |
+
background-color: var(--bg-color);
|
| 32 |
+
color: var(--text-color);
|
| 33 |
+
display: flex;
|
| 34 |
+
flex-direction: column;
|
| 35 |
+
align-items: center;
|
| 36 |
+
justify-content: center;
|
| 37 |
+
width: 100%;
|
| 38 |
+
height: 100vh;
|
| 39 |
+
overflow: hidden;
|
| 40 |
+
padding: 10px;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
.theater-container {
|
| 44 |
+
display: grid;
|
| 45 |
+
grid-template-columns: 1.8fr 1fr;
|
| 46 |
+
width: 100%;
|
| 47 |
+
max-width: 1350px;
|
| 48 |
+
height: 90vh;
|
| 49 |
+
gap: 16px;
|
| 50 |
+
background: rgba(15, 15, 20, 0.4);
|
| 51 |
+
border-radius: 16px;
|
| 52 |
+
border: 1px solid var(--border-color);
|
| 53 |
+
padding: 16px;
|
| 54 |
+
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37);
|
| 55 |
+
backdrop-filter: blur(8px);
|
| 56 |
+
-webkit-backdrop-filter: blur(8px);
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
@media (max-width: 1024px) {
|
| 60 |
+
.theater-container {
|
| 61 |
+
grid-template-columns: 1fr;
|
| 62 |
+
grid-template-rows: auto 1fr;
|
| 63 |
+
height: auto;
|
| 64 |
+
max-height: none;
|
| 65 |
+
overflow-y: auto;
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
/* Video section */
|
| 70 |
+
.video-wrapper {
|
| 71 |
+
position: relative;
|
| 72 |
+
width: 100%;
|
| 73 |
+
height: 100%;
|
| 74 |
+
display: flex;
|
| 75 |
+
flex-direction: column;
|
| 76 |
+
background: #000;
|
| 77 |
+
border-radius: 12px;
|
| 78 |
+
border: 1px solid var(--border-color);
|
| 79 |
+
overflow: hidden;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
.video-container {
|
| 83 |
+
position: relative;
|
| 84 |
+
width: 100%;
|
| 85 |
+
flex-grow: 1;
|
| 86 |
+
background: #000;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.video-container iframe {
|
| 90 |
+
position: absolute;
|
| 91 |
+
top: 0;
|
| 92 |
+
left: 0;
|
| 93 |
+
width: 100%;
|
| 94 |
+
height: 100%;
|
| 95 |
+
border: 0;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
/* Chat section */
|
| 99 |
+
.chat-wrapper {
|
| 100 |
+
display: flex;
|
| 101 |
+
flex-direction: column;
|
| 102 |
+
background: var(--panel-bg);
|
| 103 |
+
border: 1px solid var(--border-color);
|
| 104 |
+
border-radius: 12px;
|
| 105 |
+
height: 100%;
|
| 106 |
+
overflow: hidden;
|
| 107 |
+
box-shadow: inset 0 0 20px rgba(0,0,0,0.2);
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.chat-header {
|
| 111 |
+
display: flex;
|
| 112 |
+
justify-content: space-between;
|
| 113 |
+
align-items: center;
|
| 114 |
+
padding: 14px 18px;
|
| 115 |
+
border-bottom: 1px solid var(--border-color);
|
| 116 |
+
background: rgba(0,0,0,0.15);
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.chat-title {
|
| 120 |
+
font-size: 14px;
|
| 121 |
+
font-weight: 600;
|
| 122 |
+
text-transform: uppercase;
|
| 123 |
+
letter-spacing: 0.8px;
|
| 124 |
+
display: flex;
|
| 125 |
+
align-items: center;
|
| 126 |
+
gap: 8px;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.live-dot {
|
| 130 |
+
width: 8px;
|
| 131 |
+
height: 8px;
|
| 132 |
+
background-color: var(--live-color);
|
| 133 |
+
border-radius: 50%;
|
| 134 |
+
animation: pulse 1.5s infinite;
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
@keyframes pulse {
|
| 138 |
+
0% { box-shadow: 0 0 0 0 rgba(255, 0, 85, 0.7); }
|
| 139 |
+
70% { box-shadow: 0 0 0 6px rgba(255, 0, 85, 0); }
|
| 140 |
+
100% { box-shadow: 0 0 0 0 rgba(255, 0, 85, 0); }
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.chat-messages {
|
| 144 |
+
flex-grow: 1;
|
| 145 |
+
overflow-y: auto;
|
| 146 |
+
padding: 16px;
|
| 147 |
+
display: flex;
|
| 148 |
+
flex-direction: column;
|
| 149 |
+
gap: 12px;
|
| 150 |
+
scroll-behavior: smooth;
|
| 151 |
+
}
|
| 152 |
+
|
| 153 |
+
/* Custom Scrollbar */
|
| 154 |
+
.chat-messages::-webkit-scrollbar {
|
| 155 |
+
width: 6px;
|
| 156 |
+
}
|
| 157 |
+
.chat-messages::-webkit-scrollbar-track {
|
| 158 |
+
background: transparent;
|
| 159 |
+
}
|
| 160 |
+
.chat-messages::-webkit-scrollbar-thumb {
|
| 161 |
+
background: rgba(255, 255, 255, 0.1);
|
| 162 |
+
border-radius: 4px;
|
| 163 |
+
}
|
| 164 |
+
.chat-messages::-webkit-scrollbar-thumb:hover {
|
| 165 |
+
background: rgba(255, 255, 255, 0.2);
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
/* Chat Messages styling */
|
| 169 |
+
.message-row {
|
| 170 |
+
display: flex;
|
| 171 |
+
flex-direction: column;
|
| 172 |
+
padding: 6px 8px;
|
| 173 |
+
background: rgba(255,255,255,0.02);
|
| 174 |
+
border-radius: 6px;
|
| 175 |
+
border-left: 3px solid transparent;
|
| 176 |
+
transition: background 0.2s ease;
|
| 177 |
+
animation: fadeIn 0.3s ease-out;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.message-row:hover {
|
| 181 |
+
background: rgba(255, 255, 255, 0.05);
|
| 182 |
+
border-left-color: var(--accent-color);
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
@keyframes fadeIn {
|
| 186 |
+
from { opacity: 0; transform: translateY(4px); }
|
| 187 |
+
to { opacity: 1; transform: translateY(0); }
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.message-meta {
|
| 191 |
+
display: flex;
|
| 192 |
+
align-items: center;
|
| 193 |
+
gap: 8px;
|
| 194 |
+
margin-bottom: 4px;
|
| 195 |
+
}
|
| 196 |
+
|
| 197 |
+
.message-time {
|
| 198 |
+
font-size: 11px;
|
| 199 |
+
color: var(--text-muted);
|
| 200 |
+
font-variant-numeric: tabular-nums;
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
.message-username {
|
| 204 |
+
font-size: 13px;
|
| 205 |
+
font-weight: 600;
|
| 206 |
+
cursor: pointer;
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
.badge {
|
| 210 |
+
font-size: 9px;
|
| 211 |
+
font-weight: 700;
|
| 212 |
+
text-transform: uppercase;
|
| 213 |
+
padding: 1px 4px;
|
| 214 |
+
border-radius: 3px;
|
| 215 |
+
letter-spacing: 0.5px;
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
.badge-mod {
|
| 219 |
+
background: #2cb67d;
|
| 220 |
+
color: #fff;
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
.badge-sub {
|
| 224 |
+
background: var(--accent-color);
|
| 225 |
+
color: #fff;
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
.message-text {
|
| 229 |
+
font-size: 13px;
|
| 230 |
+
line-height: 1.5;
|
| 231 |
+
color: #e2e8f0;
|
| 232 |
+
word-break: break-word;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* Styled Emotes */
|
| 236 |
+
.emote {
|
| 237 |
+
display: inline-block;
|
| 238 |
+
padding: 2px 6px;
|
| 239 |
+
border-radius: 4px;
|
| 240 |
+
font-size: 11px;
|
| 241 |
+
font-weight: 700;
|
| 242 |
+
background: rgba(255,255,255,0.1);
|
| 243 |
+
margin: 0 2px;
|
| 244 |
+
border: 1px solid rgba(255,255,255,0.1);
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.emote.pogchamp { color: #ffbe0b; background: rgba(255, 190, 11, 0.1); }
|
| 248 |
+
.emote.kappa { color: #a2d2ff; background: rgba(162, 210, 255, 0.1); }
|
| 249 |
+
.emote.monkas { color: #ff006e; background: rgba(255, 0, 110, 0.1); }
|
| 250 |
+
.emote.lul { color: #3a86c8; background: rgba(58, 134, 200, 0.1); }
|
| 251 |
+
.emote.biblethump { color: #8338ec; background: rgba(131, 56, 236, 0.1); }
|
| 252 |
+
.emote.head5 { color: #06d6a0; background: rgba(6, 214, 160, 0.1); }
|
| 253 |
+
.emote.pog { color: #ffd166; background: rgba(255, 209, 102, 0.1); }
|
| 254 |
+
.emote.pepega { color: #ef476f; background: rgba(239, 71, 111, 0.1); }
|
| 255 |
+
|
| 256 |
+
.chat-bottom {
|
| 257 |
+
padding: 14px 18px;
|
| 258 |
+
border-top: 1px solid var(--border-color);
|
| 259 |
+
background: rgba(0,0,0,0.15);
|
| 260 |
+
display: flex;
|
| 261 |
+
align-items: center;
|
| 262 |
+
gap: 10px;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.chat-input-placeholder {
|
| 266 |
+
flex-grow: 1;
|
| 267 |
+
background: rgba(255,255,255,0.03);
|
| 268 |
+
border: 1px solid var(--border-color);
|
| 269 |
+
border-radius: 6px;
|
| 270 |
+
padding: 8px 12px;
|
| 271 |
+
font-size: 13px;
|
| 272 |
+
color: var(--text-muted);
|
| 273 |
+
user-select: none;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.chat-btn {
|
| 277 |
+
background: var(--accent-color);
|
| 278 |
+
color: #fff;
|
| 279 |
+
border: 0;
|
| 280 |
+
border-radius: 6px;
|
| 281 |
+
padding: 8px 14px;
|
| 282 |
+
font-size: 12px;
|
| 283 |
+
font-weight: 600;
|
| 284 |
+
cursor: not-allowed;
|
| 285 |
+
opacity: 0.6;
|
| 286 |
+
}
|
| 287 |
+
</style>
|
| 288 |
+
</head>
|
| 289 |
+
<body>
|
| 290 |
+
|
| 291 |
+
<div class="theater-container">
|
| 292 |
+
<!-- Left: Video Player -->
|
| 293 |
+
<div class="video-wrapper">
|
| 294 |
+
<div class="video-container">
|
| 295 |
+
<div id="player"></div>
|
| 296 |
+
</div>
|
| 297 |
+
</div>
|
| 298 |
+
|
| 299 |
+
<!-- Right: Chat Window -->
|
| 300 |
+
<div class="chat-wrapper">
|
| 301 |
+
<div class="chat-header">
|
| 302 |
+
<div class="chat-title">
|
| 303 |
+
<div class="live-dot"></div>
|
| 304 |
+
<span>Live Chat Replay</span>
|
| 305 |
+
</div>
|
| 306 |
+
<div class="message-time" id="video-time-display">00:00</div>
|
| 307 |
+
</div>
|
| 308 |
+
|
| 309 |
+
<div class="chat-messages" id="chat-messages">
|
| 310 |
+
<!-- Messages stream in here -->
|
| 311 |
+
</div>
|
| 312 |
+
|
| 313 |
+
<div class="chat-bottom">
|
| 314 |
+
<div class="chat-input-placeholder">Simulated replay - chat is read-only</div>
|
| 315 |
+
<button class="chat-btn" disabled>Send</button>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
</div>
|
| 319 |
+
|
| 320 |
+
<!-- YouTube IFrame API -->
|
| 321 |
+
<script src="https://www.youtube.com/iframe_api"></script>
|
| 322 |
+
|
| 323 |
+
<script>
|
| 324 |
+
// Embedded Chat Data Placeholder (Replaced by Python script)
|
| 325 |
+
const chatData = {{CHAT_DATA_JSON}};
|
| 326 |
+
const videoId = "{{VIDEO_ID}}";
|
| 327 |
+
|
| 328 |
+
let player;
|
| 329 |
+
let updateInterval;
|
| 330 |
+
let lastTime = -1;
|
| 331 |
+
let displayedMessageIds = new Set();
|
| 332 |
+
let flattenedMessages = [];
|
| 333 |
+
|
| 334 |
+
// Emote tags matching
|
| 335 |
+
const emoteMap = {
|
| 336 |
+
"PogChamp": "😲 PogChamp",
|
| 337 |
+
"Kappa": "😏 Kappa",
|
| 338 |
+
"MonkaS": "😰 MonkaS",
|
| 339 |
+
"LUL": "😂 LUL",
|
| 340 |
+
"BibleThump": "😭 BibleThump",
|
| 341 |
+
"5Head": "🧠 5Head",
|
| 342 |
+
"Pog": "🤩 Pog",
|
| 343 |
+
"Pepega": "🤪 Pepega"
|
| 344 |
+
};
|
| 345 |
+
|
| 346 |
+
function parseEmotes(text) {
|
| 347 |
+
let html = text;
|
| 348 |
+
// Escape HTML
|
| 349 |
+
html = html.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
| 350 |
+
|
| 351 |
+
Object.keys(emoteMap).forEach(key => {
|
| 352 |
+
const regex = new RegExp(`\\b${key}\\b`, 'g');
|
| 353 |
+
const className = key.toLowerCase();
|
| 354 |
+
html = html.replace(regex, `<span class="emote ${className}">${emoteMap[key]}</span>`);
|
| 355 |
+
});
|
| 356 |
+
return html;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
// Assign randomized usernames color based on hash
|
| 360 |
+
function getUsernameColor(username) {
|
| 361 |
+
const colors = ['#ff7096', '#ff85a1', '#f15bb5', '#9b5de5', '#00bbf9', '#00f5d4', '#fee440', '#f15bb5', '#38b000', '#0077b6', '#e07a5f', '#f4a261', '#e76f51', '#2ec4b6', '#e63946'];
|
| 362 |
+
let hash = 0;
|
| 363 |
+
for (let i = 0; i < username.length; i++) {
|
| 364 |
+
hash = username.charCodeAt(i) + ((hash << 5) - hash);
|
| 365 |
+
}
|
| 366 |
+
const index = Math.abs(hash) % colors.length;
|
| 367 |
+
return colors[index];
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
// Prep and flatten messages with random offsets within segment durations
|
| 371 |
+
function prepareMessages() {
|
| 372 |
+
flattenedMessages = [];
|
| 373 |
+
|
| 374 |
+
for (let i = 0; i < chatData.length; i++) {
|
| 375 |
+
const segment = chatData[i];
|
| 376 |
+
const start = segment.timestamp;
|
| 377 |
+
|
| 378 |
+
// Find next segment timestamp to determine segment duration
|
| 379 |
+
const nextStart = (i < chatData.length - 1) ? chatData[i+1].timestamp : start + 30;
|
| 380 |
+
const duration = Math.max(5, nextStart - start);
|
| 381 |
+
|
| 382 |
+
const messages = segment.messages || [];
|
| 383 |
+
messages.forEach((msg, index) => {
|
| 384 |
+
// Spread messages evenly or randomly across the duration
|
| 385 |
+
const randomOffset = Math.random() * duration;
|
| 386 |
+
const displayTime = start + randomOffset;
|
| 387 |
+
|
| 388 |
+
// Generate unique ID
|
| 389 |
+
const id = `${start}_${index}_${msg.username}`;
|
| 390 |
+
|
| 391 |
+
// Assign visual badges
|
| 392 |
+
const isMod = (index % 15 === 0);
|
| 393 |
+
const isSub = (index % 4 === 0) && !isMod;
|
| 394 |
+
|
| 395 |
+
flattenedMessages.push({
|
| 396 |
+
id: id,
|
| 397 |
+
displayTime: displayTime,
|
| 398 |
+
username: msg.username,
|
| 399 |
+
text: msg.text,
|
| 400 |
+
isMod: isMod,
|
| 401 |
+
isSub: isSub
|
| 402 |
+
});
|
| 403 |
+
});
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
// Sort messages by displayTime
|
| 407 |
+
flattenedMessages.sort((a, b) => a.displayTime - b.displayTime);
|
| 408 |
+
}
|
| 409 |
+
|
| 410 |
+
// Create HTML elements for messages
|
| 411 |
+
function createMessageElement(msg) {
|
| 412 |
+
const row = document.createElement('div');
|
| 413 |
+
row.className = 'message-row';
|
| 414 |
+
|
| 415 |
+
const meta = document.createElement('div');
|
| 416 |
+
meta.className = 'message-meta';
|
| 417 |
+
|
| 418 |
+
const timeSpan = document.createElement('span');
|
| 419 |
+
timeSpan.className = 'message-time';
|
| 420 |
+
const mins = Math.floor(msg.displayTime / 60);
|
| 421 |
+
const secs = Math.floor(msg.displayTime % 60);
|
| 422 |
+
timeSpan.textContent = `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
| 423 |
+
meta.appendChild(timeSpan);
|
| 424 |
+
|
| 425 |
+
if (msg.isMod) {
|
| 426 |
+
const badge = document.createElement('span');
|
| 427 |
+
badge.className = 'badge badge-mod';
|
| 428 |
+
badge.textContent = 'Mod';
|
| 429 |
+
meta.appendChild(badge);
|
| 430 |
+
} else if (msg.isSub) {
|
| 431 |
+
const badge = document.createElement('span');
|
| 432 |
+
badge.className = 'badge badge-sub';
|
| 433 |
+
badge.textContent = 'Sub';
|
| 434 |
+
meta.appendChild(badge);
|
| 435 |
+
}
|
| 436 |
+
|
| 437 |
+
const userSpan = document.createElement('span');
|
| 438 |
+
userSpan.className = 'message-username';
|
| 439 |
+
userSpan.style.color = getUsernameColor(msg.username);
|
| 440 |
+
userSpan.textContent = msg.username;
|
| 441 |
+
meta.appendChild(userSpan);
|
| 442 |
+
|
| 443 |
+
row.appendChild(meta);
|
| 444 |
+
|
| 445 |
+
const textDiv = document.createElement('div');
|
| 446 |
+
textDiv.className = 'message-text';
|
| 447 |
+
textDiv.innerHTML = parseEmotes(msg.text);
|
| 448 |
+
row.appendChild(textDiv);
|
| 449 |
+
|
| 450 |
+
return row;
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
// Synchronize chat with video current time
|
| 454 |
+
function updateChat() {
|
| 455 |
+
if (!player || typeof player.getCurrentTime !== 'function') return;
|
| 456 |
+
|
| 457 |
+
const currentTime = player.getCurrentTime();
|
| 458 |
+
|
| 459 |
+
// Update timer display
|
| 460 |
+
const displayMins = Math.floor(currentTime / 60);
|
| 461 |
+
const displaySecs = Math.floor(currentTime % 60);
|
| 462 |
+
document.getElementById('video-time-display').textContent =
|
| 463 |
+
`${displayMins.toString().padStart(2, '0')}:${displaySecs.toString().padStart(2, '0')}`;
|
| 464 |
+
|
| 465 |
+
// Handle seek backwards or forwards significantly
|
| 466 |
+
if (currentTime < lastTime || currentTime - lastTime > 6) {
|
| 467 |
+
const chatMessagesDiv = document.getElementById('chat-messages');
|
| 468 |
+
chatMessagesDiv.innerHTML = '';
|
| 469 |
+
displayedMessageIds.clear();
|
| 470 |
+
|
| 471 |
+
// Catch up with messages immediately prior to seek point (max 12 messages to prevent overload)
|
| 472 |
+
const pastMessages = flattenedMessages.filter(m => m.displayTime <= currentTime);
|
| 473 |
+
const catchupCount = 12;
|
| 474 |
+
const startIdx = Math.max(0, pastMessages.length - catchupCount);
|
| 475 |
+
const toRender = pastMessages.slice(startIdx);
|
| 476 |
+
|
| 477 |
+
toRender.forEach(m => {
|
| 478 |
+
displayedMessageIds.add(m.id);
|
| 479 |
+
chatMessagesDiv.appendChild(createMessageElement(m));
|
| 480 |
+
});
|
| 481 |
+
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
lastTime = currentTime;
|
| 485 |
+
|
| 486 |
+
// Filter new messages to display
|
| 487 |
+
const newMessages = flattenedMessages.filter(m => m.displayTime <= currentTime && !displayedMessageIds.has(m.id));
|
| 488 |
+
|
| 489 |
+
if (newMessages.length > 0) {
|
| 490 |
+
const chatMessagesDiv = document.getElementById('chat-messages');
|
| 491 |
+
newMessages.forEach(m => {
|
| 492 |
+
displayedMessageIds.add(m.id);
|
| 493 |
+
chatMessagesDiv.appendChild(createMessageElement(m));
|
| 494 |
+
});
|
| 495 |
+
|
| 496 |
+
// Keep list to a reasonable limit (e.g. 100 elements max to maintain DOM performance)
|
| 497 |
+
while (chatMessagesDiv.children.length > 100) {
|
| 498 |
+
chatMessagesDiv.removeChild(chatMessagesDiv.firstChild);
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
chatMessagesDiv.scrollTop = chatMessagesDiv.scrollHeight;
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
// YouTube IFrame Player API initialization
|
| 506 |
+
function onYouTubeIframeAPIReady() {
|
| 507 |
+
player = new YT.Player('player', {
|
| 508 |
+
height: '100%',
|
| 509 |
+
width: '100%',
|
| 510 |
+
videoId: videoId,
|
| 511 |
+
playerVars: {
|
| 512 |
+
'playsinline': 1,
|
| 513 |
+
'modestbranding': 1,
|
| 514 |
+
'rel': 0
|
| 515 |
+
},
|
| 516 |
+
events: {
|
| 517 |
+
'onStateChange': onPlayerStateChange
|
| 518 |
+
}
|
| 519 |
+
});
|
| 520 |
+
}
|
| 521 |
+
|
| 522 |
+
function onPlayerStateChange(event) {
|
| 523 |
+
if (event.data === YT.PlayerState.PLAYING) {
|
| 524 |
+
if (!updateInterval) {
|
| 525 |
+
updateInterval = setInterval(updateChat, 250);
|
| 526 |
+
}
|
| 527 |
+
} else {
|
| 528 |
+
if (updateInterval) {
|
| 529 |
+
clearInterval(updateInterval);
|
| 530 |
+
updateInterval = null;
|
| 531 |
+
}
|
| 532 |
+
// Run one final update to align timestamps
|
| 533 |
+
updateChat();
|
| 534 |
+
}
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
// Initialize
|
| 538 |
+
prepareMessages();
|
| 539 |
+
</script>
|
| 540 |
+
</body>
|
| 541 |
+
</html>
|