diff --git a/.assets-revision b/.assets-revision index 35808cfa..29bb8fc6 100644 --- a/.assets-revision +++ b/.assets-revision @@ -5,4 +5,4 @@ # is a git revision (branch name like `main`, a tag, or a specific commit # sha). Override at runtime with the ASSETS_REVISION env var. repo: ChilleD/WebHarbor -revision: 8d8e4069588ef55594622fee1ab9fa51c2011d07 +revision: 480c892e976bada6c0ea3f5a66e2b9efda65525d diff --git a/AGENTS.md b/AGENTS.md index 8f6e45b8..14e45939 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ A coding agent (Claude Code, Cursor, Aider, Codex, ...) is reading this. Read on ## What it is -19 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. +20 Flask mirror websites (Amazon, GitHub, BBC News, ...) packaged into one Docker image, plus a control plane on `:8101` for resetting per-site state. Used as a deterministic offline environment for web-agent benchmarks. ~3 GB image. Two repos: - **code** (this one) — Flask apps, control plane, scripts. @@ -48,17 +48,17 @@ Inside the image, sites live at `/opt/WebSyn//`. The path predates the ren # fresh clone ./scripts/fetch_assets.sh # pulls assets from HF ./scripts/build.sh # docker build -t webharbor:dev . -docker run -d -p 8101:8101 -p 40000-40018:40000-40018 webharbor:dev +docker run -d -p 8101:8101 -p 40000-40019:40000-40019 webharbor:dev ``` Or use the published image directly: ```bash -docker run -d -p 8101:8101 -p 40000-40018:40000-40018 \ +docker run -d -p 8101:8101 -p 40000-40019:40000-40019 \ battalion7244/webharbor:latest ``` -Sites are on `40000`-`40018` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: +Sites are on `40000`-`40019` in the order declared by `SITES=( ... )` in `websyn_start.sh`. Control plane: | Method | Path | Purpose | |--------|---------------------|-------------------------------------------| @@ -136,13 +136,13 @@ python3 -m py_compile sites//app.py # 3. run on alt ports (don't collide with anything you already have running) docker run -d --rm --name wh-test \ - -p 8201:8101 -p 41000-41018:40000-40018 webharbor:dev + -p 8201:8101 -p 41000-41019:40000-40019 webharbor:dev # 4. control plane healthy, all sites alive curl -s http://localhost:8201/health | python3 -m json.tool | head # 5. every site renders 200 -for p in $(seq 41000 41018); do +for p in $(seq 41000 41019); do curl -so /dev/null -w "$p:%{http_code}\n" http://localhost:$p/ done diff --git a/CLAUDE.md b/CLAUDE.md index 3f6b490e..5dcf2283 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,4 +16,4 @@ The full agent guide is loaded above via `@AGENTS.md`. The notes below apply onl ## Existing containers -If a container is already running on `:8101` / `:40000-40018`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41018`). +If a container is already running on `:8101` / `:40000-40019`, treat it as the user's working environment — don't `docker stop` or `docker rm` it without explicit confirmation. Spin up your test container under a different name on alt ports (`:8201`, `:41000-41019`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 07a7248b..6dce56be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,7 +24,7 @@ git clone https://github.com//webharbor && cd webharbor ./scripts/fetch_assets.sh # pull current assets ./scripts/new_site.py mywebsite # OR edit an existing site ./scripts/build.sh && docker run -d --rm \ - -p 8101:8101 -p 40000-40018:40000-40018 webharbor:dev + -p 8101:8101 -p 40000-40019:40000-40019 webharbor:dev # iterate locally... ./scripts/extract_assets.sh ../webharbor-static-pr/ # split assets out diff --git a/Dockerfile b/Dockerfile index 24105b6a..51f61ce7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # WebHarbor — slim, self-contained image. -# 19 Flask mirror sites + control plane on :8101. +# 20 Flask mirror sites + control plane on :8101. FROM python:3.12-slim-bookworm @@ -31,15 +31,16 @@ COPY sites/ /opt/WebSyn/ # IKEA's seed is reproducibly materialized from the tracked source catalog so code-only content fixes do not require an asset-repository write. Product images still come from the pinned asset bundle. RUN cd /opt/WebSyn/ikea && PYTHONHASHSEED=0 python seed_data.py && rm -rf instance -# Apply tracked, idempotent Phys.org and Target data corrections to their seed assets. +# Apply tracked, idempotent data corrections to downloaded seed assets. RUN cd /opt/WebSyn/phys_org && PYTHONHASHSEED=0 python migrate_seed.py && rm -rf instance RUN cd /opt/WebSyn/target && PYTHONHASHSEED=0 python migrate_seed.py && rm -rf instance +RUN cd /opt/WebSyn/ted && PYTHONHASHSEED=0 python migrate_seed.py && rm -rf instance COPY websyn_start.sh /opt/websyn_start.sh COPY control_server.py /opt/control_server.py COPY site_runner.py /opt/site_runner.py RUN chmod +x /opt/websyn_start.sh -EXPOSE 8101 40000-40018 +EXPOSE 8101 40000-40019 CMD ["/opt/websyn_start.sh"] diff --git a/README.md b/README.md index acf7bf0e..28457c4e 100644 --- a/README.md +++ b/README.md @@ -36,17 +36,17 @@ WebHarbor takes a different approach. We leverage coding agent (e.g., Claude Cod - **Deep features unlocked** — carts, checkouts, accounts, all fully testable - **Evolving** — harder tasks drive richer mirrors; the environment grows with agents - **RL-ready** — sub-second database resets between rollouts -- **Community-driven** — 19 sites today, scaling to 100+ together +- **Community-driven** — 20 sites today, scaling to 100+ together ## 🚀 Quickstart One command to run all web environments: ```bash -docker run -p 8101:8101 -p 40000-40018:40000-40018 battalion7244/webharbor:latest +docker run -p 8101:8101 -p 40000-40019:40000-40019 battalion7244/webharbor:latest ``` -Then point your agent at `http://localhost:40000` through `http://localhost:40018` to explore 19 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, and Target`. +Then point your agent at `http://localhost:40000` through `http://localhost:40019` to explore 20 local mirrors of webvoyager sites: `Allrecipes, Amazon, Apple, ArXiv, BBC News, Booking, GitHub, Google Flights, Google Maps, Google Search, Hugging Face, Wolfram Alpha, Cambridge Dictionary, Coursera, ESPN, Merriam-Webster, IKEA, Phys.org, Target, and TED`. For sub-second reset between rollouts, expose the control plane and call `/reset/`: @@ -65,7 +65,7 @@ git clone https://github.com/aiming-lab/WebHarbor && cd WebHarbor ## 🤝 Contribute -We have built 18 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. +We have built 20 high-quality mirrors covering the [WebVoyager](https://github.com/MinorJerry/WebVoyager) benchmark. The next goal is **100+ sites**, covering everything in [Online-Mind2Web](https://huggingface.co/datasets/osunlp/Online-Mind2Web). We are inviting the community to build this together. There are two ways to join the author list: diff --git a/agent_demo/README.md b/agent_demo/README.md index 2b775eac..a8a1a5c2 100644 --- a/agent_demo/README.md +++ b/agent_demo/README.md @@ -19,7 +19,7 @@ export OPENAI_BASE_URL=https://api.openai.com/v1 # or your Azure / vLLM endpoi ## Run a task -WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40018:40000-40018 battalion7244/webharbor:latest`). +WebHarbor must already be running locally (`docker run -p 8101:8101 -p 40000-40019:40000-40019 battalion7244/webharbor:latest`). Run a single task from a site's `tasks.jsonl`: diff --git a/control_server.py b/control_server.py index af39b036..bda229c5 100644 --- a/control_server.py +++ b/control_server.py @@ -26,7 +26,7 @@ 'allrecipes', 'amazon', 'apple', 'arxiv', 'bbc_news', 'booking', 'github', 'google_flights', 'google_map', 'google_search', 'huggingface', 'wolfram_alpha', 'cambridge_dictionary', - 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', + 'coursera', 'espn', 'merriam_webster', 'ikea', 'phys_org', 'target', 'ted', ] BASE_PORT = 40000 WEBSYN_DIR = '/opt/WebSyn' diff --git a/review-reports/PR-85-FINAL-AUDIT.md b/review-reports/PR-85-FINAL-AUDIT.md new file mode 100644 index 00000000..5830b7a3 --- /dev/null +++ b/review-reports/PR-85-FINAL-AUDIT.md @@ -0,0 +1,39 @@ +# PR #85 independent review and remediation audit + +## Scope + +This audit reviewed GitHub PR #85 head `18fa53a96b6131af0364f00aa2cbd07609a902c3` against base `90afddb6d4af382935ded9a385f2eead604188cf`. Seven independent review contexts assessed the pristine original head. Their raw reports are retained outside the repository under `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr85-agents/reports-original-final/`. + +## Review-agent findings and dispositions + +| Review agent | Primary findings on the original head | Verification and disposition | +|---|---|---| +| Security | Hard-coded Flask secret, open redirects after login/save, no CSRF protection, GET logout, and default/weak registration password behavior. | Confirmed directly in the original `sites/ted/app.py` and templates. Fixed with an environment/random secret, strict local-path redirects, Flask-WTF CSRF, POST logout, session rotation, request/field limits, and server-side registration validation. HTTP regression tests cover CSRF, redirect rejection, credential non-disclosure, invalid registration, and safe GET behavior. | +| Tasks and data | Exact duration/view arithmetic and seed account state were correct; TED task URLs were all `40016`; task 12 allowed multiple valid non-AI choices; playlist membership required independent confirmation. | Data facts were verified from the downloaded SQLite seed. The integration review established that TED is site index 19 and therefore port `40019`; all 20 task URLs were corrected. Task 12 now reports the exact removed title while still accepting any one valid non-AI row through a database-bound verifier. Playlist ordering and membership were checked directly. `judge_rubric` is not included in the web-agent prompt: `agent_demo/agent.py` passes only `ques` to `build_messages` and carries the rubric solely into judge input. | +| Verifiers | LLM checks became implicit passes under `--no_llm`; URL checks used substrings without origin/path/query binding; several required search/topic/filter steps were unenforced; multiple answers were unbound or negation-sensitive; state checks were incomplete. | All 20 verifiers and `verify_lib.py` were replaced with deterministic same-origin path/query/order checks, click/submit/input checks, exact task identity, non-empty answers, negation-aware fact binding, complete read-only database comparison, and exact global state-table deltas. Positive, answer-only, wrong-task, external-origin, missing-filter, negated-answer, swapped-value, wrong-removal, unrelated-mutation, and same-table-extra-mutation tests pass. | +| UI, responsive, accessibility | Cards and the lead story hid talk titles; the stacked mobile header remained sticky; controls lacked labels; repeated event buttons lacked event-specific accessible names; talk images had empty alt text; focus styles were absent; footer alignment was poor on mobile. | Talk and lead cards now render titles and speakers, the mobile header is static, filters and note/search fields are labeled, event buttons carry event-specific accessible names, talk images have descriptive alt text, focus-visible styles are present, and mobile footer alignment is corrected. Automated 320 px, 390 px, and 1440 px checks cover 30 route/viewport combinations with no horizontal overflow or broken images. | +| Integration and assets | The original HF main pin did not contain `ted.tar.gz`; all tasks targeted IKEA's `40016`; docs still described 19 sites and ended at `40018`; committed smoke/HF/PR evidence was stale; runtime startup could manufacture a mutable seed and mask missing assets. | HF dataset PR #2 was merged and the repository-wide pin now references merge commit `480c892e976bada6c0ea3f5a66e2b9efda65525d`, which contains all 20 tarballs. The temporary TED override was removed. Full 20-site asset fetch and clean Docker build pass. Site registration and docs now use 20 sites and ports `40000-40019`. Runtime fails closed when the TED seed is absent and only copies the authoritative seed into `instance`. Stale PR-65 evidence was removed and replaced by this exact-head audit. | +| Application and data model | Runtime seed creation was nondeterministic and could mutate `instance_seed`; partial seed gates were unsafe; topic matching was case-sensitive; search was brittle for simple morphology; registration accepted invalid/default credentials; CSRF and secret handling were unsafe; legacy SQLAlchemy lookup was used. | Runtime seed generation was removed; startup requires the HF seed. Topic matching is case-insensitive, duration filtering uses exact seconds, and search accepts simple prefixes/plurals. Registration validation, CSRF, secret handling, foreign-key enforcement, unique indexes, duplicate-race handling, and SQLAlchemy 2 lookup are implemented. The TED seed migration is idempotent and adds unique saved-talk and registration indexes. | +| Evidence quality | The PR-authored evidence described PR #65, an older head, 17-site/old-port smoke output, a different HF revision, and a pre-existing image rather than a clean PR #85 source build. | All stale evidence files were removed. Current evidence includes reproducible unit/adversarial tests, actual browser trajectories with initial/after databases and verifier JSON, responsive screenshots/results, immutable asset download logs, a clean local image build, 20/20 site health, TED reset hash identity, and reset-all success. | + +## Task validation + +All 20 tasks were completed through the rendered UI from a fresh seed database. Each generated trajectory was evaluated by its corresponding deterministic verifier using the captured initial and after-state databases. Result: `20/20 PASS`. + +The complete local browser evidence is retained under `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr85-fixes/e2e/`. Responsive results and screenshots are retained under `/data/zhaoyang-user-projects/websyn/_wh_review_tools/pr85-fixes/responsive/`. + +## Repository and container validation + +- Python compilation, shell syntax, Ruff fatal/undefined-name checks, and `git diff --check`: PASS. +- TED HTTP/app, verifier, migration, and environment regression suite: 27 tests PASS. +- All TED templates and all 64 talk details render successfully: PASS. +- Immutable TED asset fetch plus all-site asset fetch: PASS; 20 tarballs extracted. +- TED migration: creates two unique indexes on first application and zero on the second application. +- Clean Docker build from the remediated source tree: PASS (`sha256:212755ac90732a0a031cd997dc764eec39879efc97241a8809860f0d8b184e11`). +- Control-plane health and all 20 site roots: PASS. +- `/reset/ted`: PASS and runtime/seed SHA-256 values match. +- `/reset-all`: PASS for all 20 sites. + +## Asset status + +Hugging Face dataset PR #2 was merged as `480c892e976bada6c0ea3f5a66e2b9efda65525d`. The repository-wide asset pin now references that merge commit, which contains all 20 tarballs including `ted.tar.gz`; the temporary TED-specific override has been removed. diff --git a/sites/ted/_health.py b/sites/ted/_health.py new file mode 100644 index 00000000..409695c3 --- /dev/null +++ b/sites/ted/_health.py @@ -0,0 +1,3 @@ +"""Per-site health probe (optional, called by control_server).""" +def health(): + return {"ok": True, "site": "ted", "paths": ["/", "/talks", "/search", "/events"]} diff --git a/sites/ted/app.py b/sites/ted/app.py new file mode 100644 index 00000000..cba94e57 --- /dev/null +++ b/sites/ted/app.py @@ -0,0 +1,471 @@ +"""TED mirror for WebHarbor.""" +import json +import os +import re +import secrets +import shutil +from datetime import datetime +from pathlib import Path +from urllib.parse import urlparse + +from flask import Flask, abort, flash, redirect, render_template, request, session, url_for +from flask_sqlalchemy import SQLAlchemy +from flask_wtf.csrf import CSRFProtect +from sqlalchemy import event as sqlalchemy_event +from sqlalchemy.engine import Engine +from sqlalchemy.exc import IntegrityError +from werkzeug.security import check_password_hash, generate_password_hash + +BASE_DIR = Path(__file__).resolve().parent +DB_PATH = BASE_DIR / "instance" / "ted.db" +SEED_DB_PATH = BASE_DIR / "instance_seed" / "ted.db" +SITE_PORT = 40019 + +app = Flask(__name__) +app.config["SECRET_KEY"] = os.environ.get("TED_SECRET_KEY") or secrets.token_hex(32) +app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{DB_PATH}" +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False +app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 +app.config["SESSION_COOKIE_HTTPONLY"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" +BASE_DIR.joinpath("instance").mkdir(exist_ok=True) + +db = SQLAlchemy(app) +csrf = CSRFProtect(app) + + +@sqlalchemy_event.listens_for(Engine, "connect") +def enable_sqlite_foreign_keys(connection, _record): + cursor = connection.cursor() + cursor.execute("PRAGMA foreign_keys=ON") + cursor.close() + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(160), unique=True, nullable=False) + display_name = db.Column(db.String(120), nullable=False) + password_hash = db.Column(db.String(255), nullable=False) + role = db.Column(db.String(120), default="Curious learner") + city = db.Column(db.String(120), default="") + newsletter_topic = db.Column(db.String(80), default="technology") + + +class Talk(db.Model): + id = db.Column(db.Integer, primary_key=True) + source_id = db.Column(db.String(40), unique=True, nullable=False) + slug = db.Column(db.String(220), unique=True, nullable=False, index=True) + title = db.Column(db.String(260), nullable=False) + speaker = db.Column(db.String(180), nullable=False) + event = db.Column(db.String(120), default="") + talk_type = db.Column(db.String(80), default="TED Talk") + duration_seconds = db.Column(db.Integer, default=0) + published_at = db.Column(db.String(20), default="") + recorded_on = db.Column(db.String(20), default="") + views = db.Column(db.Integer, default=0) + image = db.Column(db.String(260), default="") + canonical_url = db.Column(db.String(300), default="") + description = db.Column(db.Text, default="") + transcript = db.Column(db.Text, default="") + topics_json = db.Column(db.Text, default="[]") + recommended_json = db.Column(db.Text, default="[]") + + @property + def topics(self): + return json.loads(self.topics_json or "[]") + + @property + def recommended_for(self): + return json.loads(self.recommended_json or "[]") + + @property + def minutes(self): + return max(1, round((self.duration_seconds or 0) / 60)) + + @property + def views_label(self): + if self.views >= 1_000_000: + return f"{self.views / 1_000_000:.1f}M" + if self.views >= 1000: + return f"{self.views // 1000}K" + return str(self.views) + + @property + def exact_views_label(self): + return f"{self.views:,}" + + +class SavedTalk(db.Model): + __table_args__ = (db.UniqueConstraint("user_id", "talk_id", name="uq_saved_talk_user_talk"),) + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + talk_id = db.Column(db.Integer, db.ForeignKey("talk.id"), nullable=False) + saved_at = db.Column(db.DateTime, default=datetime.utcnow) + note = db.Column(db.String(240), default="") + talk = db.relationship("Talk") + + +class Playlist(db.Model): + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False) + title = db.Column(db.String(180), nullable=False) + description = db.Column(db.Text, default="") + topic = db.Column(db.String(80), default="") + + +class PlaylistTalk(db.Model): + id = db.Column(db.Integer, primary_key=True) + playlist_id = db.Column(db.Integer, db.ForeignKey("playlist.id"), nullable=False) + talk_id = db.Column(db.Integer, db.ForeignKey("talk.id"), nullable=False) + position = db.Column(db.Integer, default=0) + talk = db.relationship("Talk") + + +class Event(db.Model): + id = db.Column(db.Integer, primary_key=True) + slug = db.Column(db.String(120), unique=True, nullable=False) + name = db.Column(db.String(180), nullable=False) + city = db.Column(db.String(120), default="") + month = db.Column(db.String(40), default="") + track = db.Column(db.String(80), default="") + capacity = db.Column(db.Integer, default=0) + + +class Registration(db.Model): + __table_args__ = (db.UniqueConstraint("user_id", "event_id", name="uq_registration_user_event"),) + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) + event_id = db.Column(db.Integer, db.ForeignKey("event.id"), nullable=False) + status = db.Column(db.String(40), default="waitlisted") + event = db.relationship("Event") + + +STOP_WORDS = {"the", "a", "an", "and", "or", "of", "to", "for", "in", "on", "with", "by", "my", "is"} +EMAIL_PATTERN = re.compile(r"[^@\s]+@[^@\s]+\.[^@\s]+") + + +def safe_next(target, fallback): + if not target or "\\" in target: + return fallback + parsed = urlparse(target) + if parsed.scheme or parsed.netloc or not target.startswith("/") or target.startswith("//"): + return fallback + return target + + +def current_user(): + uid = session.get("user_id") + return db.session.get(User, uid) if uid else None + + +def require_login(next_url=None): + if not current_user(): + flash("Please sign in to continue.", "info") + return redirect(url_for("login", next=next_url or request.path)) + return None + + +@app.context_processor +def inject_globals(): + return {"current_user": current_user()} + + +@app.template_filter("date_label") +def date_label(value): + try: + return datetime.strptime(value, "%Y-%m-%d").strftime("%b %d, %Y") + except Exception: + return value + + +def tokenize(text): + return [t for t in re.split(r"[^a-z0-9]+", (text or "").lower()) if len(t) > 1 and t not in STOP_WORDS] + + +def scored_talks(query, talks): + tokens = tokenize(query) + if not tokens: + return list(talks) + ranked = [] + for talk in talks: + text = " ".join([talk.title, talk.speaker, talk.event, talk.description, talk.transcript, " ".join(talk.topics)]) + text_tokens = set(tokenize(text)) + score = sum( + 1 + for token in tokens + if any( + token == candidate + or (len(token) >= 4 and candidate.startswith(token)) + or (len(candidate) >= 4 and token.startswith(candidate)) + for candidate in text_tokens + ) + ) + if score: + ranked.append((score, talk.views, talk)) + return [talk for _, _, talk in sorted(ranked, key=lambda item: (-item[0], -item[1]))] + + +def available_topics(): + return sorted({topic for talk in Talk.query.all() for topic in talk.topics}, key=str.casefold) + + +def filtered_talks(topic="", event="", max_minutes=None): + query = Talk.query + if event: + query = query.filter(Talk.event == event) + if max_minutes is not None: + query = query.filter(Talk.duration_seconds <= max_minutes * 60) + items = query.order_by(Talk.published_at.desc(), Talk.id.asc()).all() + if topic: + items = [talk for talk in items if topic.casefold() in {value.casefold() for value in talk.topics}] + return items + + +@app.route("/") +def index(): + featured = Talk.query.order_by(Talk.published_at.desc()).limit(5).all() + popular = Talk.query.order_by(Talk.views.desc()).limit(8).all() + playlists = Playlist.query.all() + return render_template("index.html", featured=featured, popular=popular, playlists=playlists) + + +@app.route("/talks") +def talks(): + topic = request.args.get("topic", "").strip().lower() + event = request.args.get("event", "").strip() + raw_max_minutes = request.args.get("max_minutes", "").strip() + max_minutes = None + if raw_max_minutes: + try: + max_minutes = int(raw_max_minutes) + except ValueError: + max_minutes = None + if max_minutes is not None and not 1 <= max_minutes <= 180: + max_minutes = None + items = filtered_talks(topic, event, max_minutes) + events = [row[0] for row in db.session.query(Talk.event).distinct().order_by(Talk.event).all()] + return render_template( + "talks.html", + talks=items, + topic=topic, + event=event, + max_minutes=max_minutes, + events=events, + topics=available_topics(), + ) + + +@app.route("/search") +def search(): + q = request.args.get("q", "").strip() + talks = scored_talks(q, Talk.query.all()) if q else [] + return render_template("search.html", q=q, talks=talks) + + +@app.route("/talks/") +def talk_detail(slug): + talk = Talk.query.filter_by(slug=slug).first_or_404() + related = [item for item in scored_talks(" ".join(talk.topics[:2]), Talk.query.all()) if item.id != talk.id][:4] + saved = False + user = current_user() + if user: + saved = SavedTalk.query.filter_by(user_id=user.id, talk_id=talk.id).first() is not None + return render_template("talk_detail.html", talk=talk, related=related, saved=saved) + + +@app.route("/topics") +def topics(): + counts = {} + for talk in Talk.query.all(): + for topic in talk.topics: + counts[topic] = counts.get(topic, 0) + 1 + return render_template("topics.html", counts=sorted(counts.items(), key=lambda item: (-item[1], item[0]))) + + +@app.route("/topics/") +def topic_detail(topic): + return redirect(url_for("talks", topic=topic.lower())) + + +@app.route("/playlists") +def playlists(): + items = Playlist.query.order_by(Playlist.title).all() + return render_template("playlists.html", playlists=items) + + +@app.route("/playlists/") +def playlist_detail(slug): + playlist = Playlist.query.filter_by(slug=slug).first_or_404() + links = PlaylistTalk.query.filter_by(playlist_id=playlist.id).order_by(PlaylistTalk.position).all() + return render_template("playlist_detail.html", playlist=playlist, links=links) + + +@app.route("/events", methods=["GET", "POST"]) +def events(): + if request.method == "POST": + login_redirect = require_login(url_for("events")) + if login_redirect: + return login_redirect + event_slug = request.form.get("event_slug", "").strip() + event = Event.query.filter_by(slug=event_slug).first_or_404() + user = current_user() + existing = Registration.query.filter_by(user_id=user.id, event_id=event.id).first() + if not existing: + db.session.add(Registration(user_id=user.id, event_id=event.id, status="waitlisted")) + try: + db.session.commit() + flash(f"Registration saved for {event.name}.", "success") + except IntegrityError: + db.session.rollback() + flash(f"You are already registered for {event.name}.", "info") + else: + flash(f"You are already registered for {event.name}.", "info") + return redirect(url_for("account")) + event_items = Event.query.all() + event_items.sort(key=lambda item: datetime.strptime(item.month, "%B %Y"), reverse=True) + return render_template("events.html", events=event_items) + + +@app.route("/save/", methods=["POST"]) +def save_talk(slug): + login_redirect = require_login(url_for("talk_detail", slug=slug)) + if login_redirect: + return login_redirect + talk = Talk.query.filter_by(slug=slug).first_or_404() + user = current_user() + existing = SavedTalk.query.filter_by(user_id=user.id, talk_id=talk.id).first() + if not existing: + note = request.form.get("note", "").strip()[:240] + db.session.add(SavedTalk(user_id=user.id, talk_id=talk.id, note=note)) + try: + db.session.commit() + flash("Talk saved.", "success") + except IntegrityError: + db.session.rollback() + flash("This talk is already saved.", "info") + else: + flash("This talk is already saved.", "info") + return redirect(safe_next(request.form.get("next"), url_for("talk_detail", slug=slug))) + + +@app.route("/unsave/", methods=["POST"]) +def unsave_talk(saved_id): + login_redirect = require_login(url_for("account")) + if login_redirect: + return login_redirect + saved = db.get_or_404(SavedTalk, saved_id) + if saved.user_id != current_user().id: + abort(403) + db.session.delete(saved) + db.session.commit() + flash("Saved talk removed.", "success") + return redirect(url_for("account")) + + +@app.route("/account", methods=["GET", "POST"]) +def account(): + login_redirect = require_login() + if login_redirect: + return login_redirect + user = current_user() + if request.method == "POST": + user.display_name = (request.form.get("display_name", user.display_name).strip() or user.display_name)[:120] + user.role = (request.form.get("role", user.role).strip() or user.role)[:120] + user.city = request.form.get("city", user.city).strip()[:120] + user.newsletter_topic = (request.form.get("newsletter_topic", user.newsletter_topic).strip().lower() or user.newsletter_topic)[:80] + db.session.commit() + flash("Profile updated.", "success") + return redirect(url_for("account")) + saved = SavedTalk.query.filter_by(user_id=user.id).order_by(SavedTalk.saved_at.desc()).all() + registrations = Registration.query.filter_by(user_id=user.id).all() + return render_template("account.html", user=user, saved=saved, registrations=registrations) + + +@app.route("/login", methods=["GET", "POST"]) +def login(): + if current_user(): + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").lower().strip()[:160] + password = request.form.get("password", "")[:256] + user = User.query.filter_by(email=email).first() + if user and check_password_hash(user.password_hash, password): + session.clear() + session["user_id"] = user.id + flash("Signed in.", "success") + return redirect(safe_next(request.args.get("next"), url_for("account"))) + flash("Invalid email or password.", "error") + return render_template("login.html") + + +@app.route("/register", methods=["GET", "POST"]) +def register(): + if current_user(): + return redirect(url_for("account")) + if request.method == "POST": + email = request.form.get("email", "").lower().strip()[:160] + username = re.sub(r"[^a-z0-9_]+", "", request.form.get("username", "").lower())[:40] + display_name = request.form.get("display_name", "").strip()[:120] + password = request.form.get("password", "")[:256] + errors = [] + if not username: + errors.append("Enter a username containing letters, numbers, or underscores.") + if not EMAIL_PATTERN.fullmatch(email): + errors.append("Enter a valid email address.") + if not display_name: + errors.append("Enter your name.") + if len(password) < 8: + errors.append("Password must contain at least 8 characters.") + if email and username and User.query.filter((User.email == email) | (User.username == username)).first(): + errors.append("Unable to create an account with the supplied details.") + if errors: + for message in errors: + flash(message, "error") + return render_template("register.html"), 400 + user = User( + email=email, + username=username, + display_name=display_name, + password_hash=generate_password_hash(password), + ) + db.session.add(user) + db.session.commit() + session.clear() + session["user_id"] = user.id + flash("Account created.", "success") + return redirect(url_for("account")) + return render_template("register.html") + + +@app.route("/logout", methods=["POST"]) +def logout(): + session.clear() + flash("Signed out.", "success") + return redirect(url_for("index")) + + +@app.route("/_health") +def health(): + return {"ok": True, "site": "ted", "talks": Talk.query.count()} + + +def initialize_database(): + if not SEED_DB_PATH.exists(): + raise RuntimeError( + f"Missing TED seed database at {SEED_DB_PATH}. Run scripts/fetch_assets.sh ted before starting the site." + ) + if not DB_PATH.exists(): + DB_PATH.parent.mkdir(exist_ok=True) + shutil.copy2(SEED_DB_PATH, DB_PATH) + + +with app.app_context(): + initialize_database() + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", SITE_PORT)) + app.run(host="0.0.0.0", port=port, debug=False) diff --git a/sites/ted/migrate_seed.py b/sites/ted/migrate_seed.py new file mode 100644 index 00000000..de8cb199 --- /dev/null +++ b/sites/ted/migrate_seed.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Apply idempotent integrity constraints to the downloaded TED seed database.""" +from __future__ import annotations +import argparse +import sqlite3 +from pathlib import Path + +BASE_DIR=Path(__file__).resolve().parent +DEFAULT_DB=BASE_DIR/'instance_seed'/'ted.db' +INDEXES={ + 'uq_saved_talk_user_talk':'CREATE UNIQUE INDEX uq_saved_talk_user_talk ON saved_talk(user_id,talk_id)', + 'uq_registration_user_event':'CREATE UNIQUE INDEX uq_registration_user_event ON registration(user_id,event_id)', +} + +def migrate_database(database_path: str|Path=DEFAULT_DB)->int: + con=sqlite3.connect(database_path) + try: + existing={row[0] for row in con.execute("SELECT name FROM sqlite_master WHERE type='index'")};created=0 + for name,statement in INDEXES.items(): + if name not in existing:con.execute(statement);created+=1 + con.commit();return created + finally:con.close() + +def main()->None: + parser=argparse.ArgumentParser();parser.add_argument('database',nargs='?',default=str(DEFAULT_DB));args=parser.parse_args();created=migrate_database(args.database);print(f'TED seed migration complete: {created} indexes created.') +if __name__=='__main__':main() diff --git a/sites/ted/requirements.txt b/sites/ted/requirements.txt new file mode 100644 index 00000000..ce9fbfc8 --- /dev/null +++ b/sites/ted/requirements.txt @@ -0,0 +1,3 @@ +Flask +Flask-SQLAlchemy +Flask-WTF diff --git a/sites/ted/seed_data.py b/sites/ted/seed_data.py new file mode 100644 index 00000000..1c944dcb --- /dev/null +++ b/sites/ted/seed_data.py @@ -0,0 +1,7 @@ +"""Seed data for the TED WebHarbor mirror.""" + +TALKS = [{'source_id': '178768', 'title': 'The wildlife sanctuary you can visit from anywhere', 'slug': 'maya-higa-the-wildlife-sanctuary-you-can-visit-from-anywhere', 'speaker': 'Maya Higa', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 568, 'duration_minutes': 9, 'published_at': '2026-05-12', 'recorded_on': '2026-04-14', 'views': 150000, 'topics': ['media', 'social change', 'animals', 'nature', 'social media', 'internet', 'conservation', 'wildlife'], 'image': 'images/talk_001_maya-higa-the-wildlife-sanctuary-you-can-visit-f.jpg', 'canonical_url': 'https://www.ted.com/talks/maya_higa_the_wildlife_sanctuary_you_can_visit_from_anywhere', 'description': 'Creator Maya Higa is on a mission to use the internet to build the next generation of conservationists. Her virtual education center, Alveus Sanctuary, is one of the most-watched sanctuaries on Earth, with dozens of rescued animals and cameras livestreaming to a community of millions inspired to help protect the wildlife. Visit with Bean the Hawk, Winnie the Moo and more - and see what the future of conservation looks like.', 'transcript': 'Maya Higa opens by framing the central tension behind The wildlife sanctuary you can visit from anywhere. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '178681', 'title': "Waymo's case for a driverless future", 'slug': 'tekedra-mawakana-sal-khan-waymo-s-case-for-a-driverless-future', 'speaker': 'Tekedra Mawakana, Sal Khan', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1154, 'duration_minutes': 19, 'published_at': '2026-05-11', 'recorded_on': '2026-04-15', 'views': 157919, 'topics': ['technology', 'future', 'society', 'driverless cars'], 'image': 'images/talk_002_tekedra-mawakana-sal-khan-waymo-s-case-for-a-dri.jpg', 'canonical_url': 'https://www.ted.com/talks/tekedra_mawakana_sal_khan_waymo_s_case_for_a_driverless_future', 'description': "What if we could solve the problem of fatal car accidents? Waymo co-CEO Tekedra Mawakana joins TED's Sal Khan to explore why fully autonomous vehicles (where you never have to touch the wheel) could end the dangerous status quo of traffic deaths. She makes the case for why self-driving cars are more than a tech novelty - they're an urgently needed upgrade that could make the world safer for everyone.", 'transcript': "Tekedra Mawakana, Sal Khan opens by framing the central tension behind Waymo's case for a driverless future. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '177895', 'title': 'Why I love my bad days', 'slug': 'alexi-pappas-why-i-love-my-bad-days', 'speaker': 'Alexi Pappas', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 310, 'duration_minutes': 5, 'published_at': '2026-05-08', 'recorded_on': '2025-11-11', 'views': 6052, 'topics': ['psychology', 'philosophy', 'happiness', 'sports', 'motivation', 'personal growth', 'goals'], 'image': 'images/talk_003_alexi-pappas-why-i-love-my-bad-days.jpg', 'canonical_url': 'https://www.ted.com/talks/alexi_pappas_why_i_love_my_bad_days', 'description': "One month before the Rio Olympics, runner Alexi Pappas couldn't hit her splits in practice. She was begging her watch to change its mind. Then her coach told her to take it off - and shared the best advice she's ever received. That single piece of wisdom led her to break a national record and changed how she chases her goals, carrying her through ultramarathons, a memoir and three films. Bad days aren't a detour, she says - they mean you're right on track.", 'transcript': 'Alexi Pappas opens by framing the central tension behind Why I love my bad days. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '178367', 'title': 'What really won the trillion-dollar Supreme Court case', 'slug': 'neal-kumar-katyal-what-really-won-the-trillion-dollar-supreme-court-case', 'speaker': 'Neal Kumar Katyal', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1091, 'duration_minutes': 18, 'published_at': '2026-05-07', 'recorded_on': '2026-04-17', 'views': 154312, 'topics': ['culture', 'ai', 'law', 'society', 'policy'], 'image': 'images/talk_004_neal-kumar-katyal-what-really-won-the-trillion-d.jpg', 'canonical_url': 'https://www.ted.com/talks/neal_kumar_katyal_what_really_won_the_trillion_dollar_supreme_court_case', 'description': "In November 2025, Neal Kumar Katyal was asked to do what no US Supreme Court litigator had ever done: convince the justices to strike down a sitting president's signature initiative. After enlisting the help of four unlikely coaches - and one secret weapon he hasn't told anyone about until now - he walked into the courtroom ready for anything. What he discovered about winning and connecting might just change how you think about performing under pressure.", 'transcript': 'Neal Kumar Katyal opens by framing the central tension behind What really won the trillion-dollar Supreme Court case. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '178175', 'title': 'The rising cost of dissent in America', 'slug': 'miles-taylor-the-rising-cost-of-dissent-in-america-may-2026', 'speaker': 'Miles Taylor', 'event': 'TEDxMidAtlantic', 'talk_type': 'TEDx Talk', 'duration_seconds': 1166, 'duration_minutes': 19, 'published_at': '2026-05-04', 'recorded_on': '2025-11-01', 'views': 154789, 'topics': ['politics', 'media', 'social change', 'united states', 'democracy', 'government'], 'image': 'images/talk_005_miles-taylor-the-rising-cost-of-dissent-in-ameri.jpg', 'canonical_url': 'https://www.ted.com/talks/miles_taylor_the_rising_cost_of_dissent_in_america_may_2026', 'description': "Former senior US national security official Miles Taylor shares a personal account that raises a broader civic concern: the growing cost of dissent in American public life. Drawing on his experience inside government and living the consequences of speaking openly, he says that the real threat to US democracy isn't the politicians or hard-liners - it's the two-thirds of Americans who don't speak up. (This talk contains mature language.)", 'transcript': 'Miles Taylor opens by framing the central tension behind The rising cost of dissent in America. The talk then connects evidence, lived experience, and examples from TEDxMidAtlantic. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '178086', 'title': 'Why AI is unlikely to become conscious', 'slug': 'anil-seth-why-ai-is-unlikely-to-become-conscious', 'speaker': 'Anil Seth', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 897, 'duration_minutes': 15, 'published_at': '2026-05-01', 'recorded_on': '2026-04-16', 'views': 191682, 'topics': ['technology', 'future', 'brain', 'consciousness', 'ai'], 'image': 'images/talk_006_anil-seth-why-ai-is-unlikely-to-become-conscious.jpg', 'canonical_url': 'https://www.ted.com/talks/anil_seth_why_ai_is_unlikely_to_become_conscious', 'description': 'We see consciousness in AI the same way we see faces in clouds, says neuroscientist Anil Seth. He explores the all-too-human tendency to project inner life onto machines that are brilliant mimics, not sentient beings, and gives a definitive answer to the urgent question: Will AI ever gain consciousness?', 'transcript': 'Anil Seth opens by framing the central tension behind Why AI is unlikely to become conscious. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175757', 'title': 'Reimagining traditional architecture for modern needs', 'slug': 'riyad-joucka-reimagining-traditional-architecture-for-modern-needs', 'speaker': 'Riyad Joucka', 'event': 'TED@BCG', 'talk_type': 'TED Institute Talk', 'duration_seconds': 492, 'duration_minutes': 8, 'published_at': '2026-04-30', 'recorded_on': '2025-10-23', 'views': 153822, 'topics': ['technology', 'design', 'architecture', 'urban planning', 'innovation', '3d printing'], 'image': 'images/talk_007_riyad-joucka-reimagining-traditional-architectur.jpg', 'canonical_url': 'https://www.ted.com/talks/riyad_joucka_reimagining_traditional_architecture_for_modern_needs', 'description': "Architect Riyad Joucka believes your home should be a mirror of who you are. Using 3D printing and ancient architectural wisdom, he's designing efficient, personal homes that respond to context, climate and culture without sacrificing character. He makes the case that we should start designing for people, not the market.", 'transcript': 'Riyad Joucka opens by framing the central tension behind Reimagining traditional architecture for modern needs. The talk then connects evidence, lived experience, and examples from TED@BCG. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '176563', 'title': 'How to invite creativity into your life', 'slug': 'rose-b-simpson-debbie-millman-how-to-invite-creativity-into-your-life', 'speaker': 'Rose B. Simpson, Debbie Millman', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1065, 'duration_minutes': 18, 'published_at': '2026-04-29', 'recorded_on': '2025-11-10', 'views': 170066, 'topics': ['culture', 'nature', 'creativity', 'art', 'storytelling', 'mindfulness'], 'image': 'images/talk_008_rose-b-simpson-debbie-millman-how-to-invite-crea.jpg', 'canonical_url': 'https://www.ted.com/talks/rose_b_simpson_debbie_millman_how_to_invite_creativity_into_your_life', 'description': 'What do you hear when you sit in silence? For artist Rose B. Simpson, that question is the beginning of all art. She comes from a line of ceramic artists stretching back generations and, as part of her multidisciplinary work, she also builds custom lowrider cars. (If that sounds like a contradiction, that\'s kind of the point.) In conversation with "Design Matters" podcast host Debbie Millman, Simpson invites you to find your own aesthetic - not by searching, but by listening.', 'transcript': 'Rose B. Simpson, Debbie Millman opens by framing the central tension behind How to invite creativity into your life. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175453', 'title': 'You got what you wanted. Now what?', 'slug': 'debbie-millman-you-got-what-you-wanted-now-what', 'speaker': 'Debbie Millman', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 473, 'duration_minutes': 8, 'published_at': '2026-04-27', 'recorded_on': '2025-11-09', 'views': 190209, 'topics': ['design', 'creativity', 'personal growth'], 'image': 'images/talk_009_debbie-millman-you-got-what-you-wanted-now-what.jpg', 'canonical_url': 'https://www.ted.com/talks/debbie_millman_you_got_what_you_wanted_now_what', 'description': 'Over two decades of interviewing countless creative people, Debbie Millman (host of the iconic "Design Matters" podcast) had a realization: the pride and joy of accomplishing something often evaporates almost instantly. She explains how to stop chasing external validation for your achievements and instead live for the act of creation itself.', 'transcript': 'Debbie Millman opens by framing the central tension behind You got what you wanted. Now what?. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '177490', 'title': 'What Kosovo can teach the world about freedom', 'slug': 'vjosa-osmani-sadriu-what-kosovo-can-teach-the-world-about-freedom', 'speaker': 'Vjosa Osmani Sadriu', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1047, 'duration_minutes': 17, 'published_at': '2026-04-24', 'recorded_on': '2026-04-14', 'views': 176972, 'topics': ['politics', 'social change', 'leadership', 'democracy', 'government'], 'image': 'images/talk_010_vjosa-osmani-sadriu-what-kosovo-can-teach-the-wo.jpg', 'canonical_url': 'https://www.ted.com/talks/vjosa_osmani_sadriu_what_kosovo_can_teach_the_world_about_freedom', 'description': '"Truth is the real oxygen for democracy," says Vjosa Osmani Sadriu, the 6th President of the Republic of Kosovo. As a child of war, she once longed for someone to save her people. Now she\'s been in the rooms where decisions are made - and she\'s never forgotten what brought her there. In conversation with solutions journalist Angus Hervey, she reflects on what it takes to defend democracy in a world where truth itself is under threat. (Recorded on April 14, 2026)', 'transcript': 'Vjosa Osmani Sadriu opens by framing the central tension behind What Kosovo can teach the world about freedom. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '177489', 'title': 'Beware the power of prediction', 'slug': 'carissa-veliz-beware-the-power-of-prediction', 'speaker': 'Carissa Veliz', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 728, 'duration_minutes': 12, 'published_at': '2026-04-23', 'recorded_on': '2026-04-14', 'views': 267371, 'topics': ['technology', 'future', 'ai', 'ethics'], 'image': 'images/talk_011_carissa-veliz-beware-the-power-of-prediction.jpg', 'canonical_url': 'https://www.ted.com/talks/carissa_veliz_beware_the_power_of_prediction', 'description': "What do the story of Oedipus and your insurance premiums have in common? They are both driven by self-fulfilling prophecies. Philosopher and TED Fellow Carissa Veliz traces the hidden power of prediction, from Roman emperors who banned prophets to the AI algorithms quietly making decisions about your life right now. We tend to associate predictions with knowledge, she says, but they're actually attempts to grab power. So the next time someone tells you a specific outcome is inevitable, remember: they aren't describing the future - they're selling it.", 'transcript': 'Carissa Veliz opens by framing the central tension behind Beware the power of prediction. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '177096', 'title': 'The problem with billionaires - and the debut of True Net Worth', 'slug': 'randall-lane-the-problem-with-billionaires-and-the-debut-of-true-net-worth', 'speaker': 'Randall Lane', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 499, 'duration_minutes': 8, 'published_at': '2026-04-20', 'recorded_on': '2026-04-16', 'views': 221434, 'topics': ['culture', 'economics', 'philanthropy', 'society', 'money', 'finance'], 'image': 'images/talk_012_randall-lane-the-problem-with-billionaires-and-t.jpg', 'canonical_url': 'https://www.ted.com/talks/randall_lane_the_problem_with_billionaires_and_the_debut_of_true_net_worth', 'description': "As chief content officer of Forbes, Randall Lane oversees the magazine's signature list of billionaires, tracking the richest people on Earth. But he has noticed that this prompts the ultra-wealthy to stockpile their money instead of spending it on the public good. He debuts a new ranking - True Net Worth - that applauds billionaires for their philanthropy and rewards generosity. Guess who's in the top five?", 'transcript': 'Randall Lane opens by framing the central tension behind The problem with billionaires - and the debut of True Net Worth. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175050', 'title': 'A cheat sheet for accelerating clean energy', 'slug': 'kimiko-hirata-a-cheat-sheet-for-accelerating-clean-energy', 'speaker': 'Kimiko Hirata', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 586, 'duration_minutes': 10, 'published_at': '2026-04-20', 'recorded_on': '2025-06-18', 'views': 191819, 'topics': ['climate change', 'policy', 'countdown', 'renewable energy', 'fossil fuels'], 'image': 'images/talk_013_kimiko-hirata-a-cheat-sheet-for-accelerating-cle.jpg', 'canonical_url': 'https://www.ted.com/talks/kimiko_hirata_a_cheat_sheet_for_accelerating_clean_energy', 'description': 'After the Fukushima disaster shut down Japan\'s nuclear reactors, the coal industry rushed in to fill the energy gap. As climate advocate Kimiko Hirata watched dozens of new coal plant proposals quietly surface across the country - each one locking in decades of future emissions - she resolved to make them impossible to ignore. She shares how a small, scrappy civil society movement took on a fossil-fuel-dependent economy and got people to say "yes" to a renewable future.', 'transcript': 'Kimiko Hirata opens by framing the central tension behind A cheat sheet for accelerating clean energy. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '176964', 'title': 'How I created OpenClaw, the breakthrough AI agent', 'slug': 'peter-steinberger-how-i-created-openclaw-the-breakthrough-ai-agent', 'speaker': 'Peter Steinberger', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1055, 'duration_minutes': 18, 'published_at': '2026-04-17', 'recorded_on': '2026-04-16', 'views': 551544, 'topics': ['technology', 'computers', 'ai', 'internet', 'machine learning', 'artificial intelligence'], 'image': 'images/talk_014_peter-steinberger-how-i-created-openclaw-the-bre.jpg', 'canonical_url': 'https://www.ted.com/talks/peter_steinberger_how_i_created_openclaw_the_breakthrough_ai_agent', 'description': 'OpenClaw creator Peter Steinberger takes us back to the transformative moment he let his AI agent loose on the internet, igniting one of the world\'s fastest-growing open-source projects. He makes a fascinating (and slightly unnerving) case that agents are a real shift, not just better versions of chatbots, and explores how they might reshape your ability to work, build and create. "The lobster is loose, and it\'s not going back into the tank," he says. (Followed by a brief Q&A with TED Chairman Chris Anderson)', 'transcript': 'Peter Steinberger opens by framing the central tension behind How I created OpenClaw, the breakthrough AI agent. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '176887', 'title': 'A plan to stop AI from automating our decline', 'slug': 'gina-raimondo-a-plan-to-stop-ai-from-automating-our-decline', 'speaker': 'Gina Raimondo', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 951, 'duration_minutes': 16, 'published_at': '2026-04-16', 'recorded_on': '2026-04-14', 'views': 229795, 'topics': ['politics', 'technology', 'business', 'social change', 'work', 'ai'], 'image': 'images/talk_015_gina-raimondo-a-plan-to-stop-ai-from-automating-.jpg', 'canonical_url': 'https://www.ted.com/talks/gina_raimondo_a_plan_to_stop_ai_from_automating_our_decline', 'description': 'The United States is on track to win the AI race - and hollow itself out in the process, says Gina Raimondo, former Governor of Rhode Island and US Secretary of Commerce. In this unflinching look at the threat of AI-induced economic disruption and social unrest, she offers a concrete blueprint to prepare workers for what\'s coming next. "AI is a 100-year technology and needs a 100-year response," she says. Is America up to the challenge?', 'transcript': 'Gina Raimondo opens by framing the central tension behind A plan to stop AI from automating our decline. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '175041', 'title': 'East African sound meets cosmic trap', 'slug': 'akoth-jumadi-and-mr-lu-east-african-sound-meets-cosmic-trap', 'speaker': 'Akoth Jumadi and Mr. Lu', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 483, 'duration_minutes': 8, 'published_at': '2026-04-15', 'recorded_on': '2025-06-18', 'views': 2781, 'topics': ['music', 'performance', 'africa', 'art', 'sound'], 'image': 'images/talk_016_akoth-jumadi-and-mr-lu-east-african-sound-meets-.jpg', 'canonical_url': 'https://www.ted.com/talks/akoth_jumadi_and_mr_lu_east_african_sound_meets_cosmic_trap', 'description': 'Kenyan music duo Akoth Jumadi and MR. LU* fuse tribal roots with cosmic trap and celestial R&B, creating a hypnotic sound where ancient rhythms meet the future.', 'transcript': 'Akoth Jumadi and Mr. Lu opens by framing the central tension behind East African sound meets cosmic trap. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '176566', 'title': 'What I got wrong about changing the world', 'slug': 'malala-yousafzai-what-i-got-wrong-about-changing-the-world', 'speaker': 'Malala Yousafzai', 'event': 'TED2026', 'talk_type': 'TED Stage Talk', 'duration_seconds': 703, 'duration_minutes': 12, 'published_at': '2026-04-14', 'recorded_on': '2026-04-14', 'views': 349363, 'topics': ['education', 'social change', 'activism', 'middle east', 'human rights'], 'image': 'images/talk_017_malala-yousafzai-what-i-got-wrong-about-changing.jpg', 'canonical_url': 'https://www.ted.com/talks/malala_yousafzai_what_i_got_wrong_about_changing_the_world', 'description': "Malala Yousafzai has spent her life advocating for girls' education - surviving an assassination attempt at 15, meeting with world leaders and then watching hard-won progress collapse when Afghanistan fell to the Taliban in 2021. That moment of despair forced her to completely rethink what it means to create change, and what she discovered replaced her shattered optimism with something more powerful and more honest. Hear how to keep fighting for the future you want, even when hope feels lost.", 'transcript': 'Malala Yousafzai opens by framing the central tension behind What I got wrong about changing the world. The talk then connects evidence, lived experience, and examples from TED2026. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174201', 'title': 'Amman, Jordan', 'slug': 'ted-idea-search-amman-jordan', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2610, 'duration_minutes': 44, 'published_at': '2026-04-09', 'recorded_on': '2026-04-09', 'views': 23418, 'topics': ['social change'], 'image': 'images/talk_018_ted-idea-search-amman-jordan.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_amman_jordan', 'description': "The TED Idea Search wraps up in Amman, Jordan, in a 2,000-year-old Roman amphitheater and in front of a crowd of 4,000. What unfolds inside those stone walls is something the series hasn't quite seen before: speakers shaped by the weight of living in a region the world tends to define for itself. From a mountaineer who turned grief into motivation to a therapist rewriting the Arab world's language around mental health, the final city makes the strongest case yet that the best ideas can come from anywhere.", 'transcript': 'TED Idea Search opens by framing the central tension behind Amman, Jordan. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175828', 'title': 'A musical journey through Turkana', 'slug': 'turkana-sessions-a-musical-journey-through-turkana', 'speaker': 'Turkana Sessions', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 401, 'duration_minutes': 7, 'published_at': '2026-04-09', 'recorded_on': '2025-06-16', 'views': 4223, 'topics': ['music', 'performance', 'africa'], 'image': 'images/talk_019_turkana-sessions-a-musical-journey-through-turka.jpg', 'canonical_url': 'https://www.ted.com/talks/turkana_sessions_a_musical_journey_through_turkana', 'description': 'The genre-defying band Turkana Sessions takes us on a musical tour through the soundscape of northwest Kenya, with Elizabeth Korkel singing and Eddie Grey shredding on guitar.', 'transcript': 'Turkana Sessions opens by framing the central tension behind A musical journey through Turkana. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '175296', 'title': "A whale's-eye-view of the ocean", 'slug': 'eric-stackpole-a-whale-s-eye-view-of-the-ocean', 'speaker': 'Eric Stackpole', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 311, 'duration_minutes': 5, 'published_at': '2026-04-08', 'recorded_on': '2025-11-11', 'views': 193174, 'topics': ['science', 'technology', 'animals', 'exploration', 'ocean'], 'image': 'images/talk_020_eric-stackpole-a-whale-s-eye-view-of-the-ocean.jpg', 'canonical_url': 'https://www.ted.com/talks/eric_stackpole_a_whale_s_eye_view_of_the_ocean', 'description': "A hand-built camera with suction cups captured something no one had ever seen: two sperm whales communicating and swimming together in the deep ocean. Engineer Eric Stackpole shares the story of how a scrappy, DIY tool revealed this intimate glimpse into the lives of these giants - and makes the case that the only limit to what we can discover is what we're curious enough to explore.", 'transcript': "Eric Stackpole opens by framing the central tension behind A whale's-eye-view of the ocean. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175934', 'title': 'The art and science of wine tasting', 'slug': 'qian-janice-wang-the-art-and-science-of-wine-tasting', 'speaker': 'Qian Janice Wang', 'event': 'TEDxNoVA', 'talk_type': 'TEDx Talk', 'duration_seconds': 919, 'duration_minutes': 15, 'published_at': '2026-04-07', 'recorded_on': '2025-10-10', 'views': 181481, 'topics': ['science', 'food', 'brain'], 'image': 'images/talk_021_qian-janice-wang-the-art-and-science-of-wine-tas.jpg', 'canonical_url': 'https://www.ted.com/talks/qian_janice_wang_the_art_and_science_of_wine_tasting', 'description': "No two people approach wine tasting the same way, and science is starting to show us why. Sensory scientist Qian Janice Wang explores why experts and beginners experience complexity so differently - revealing that what makes a wine great may have less to do with what's in the glass and more to do with what's happening in your brain.", 'transcript': 'Qian Janice Wang opens by framing the central tension behind The art and science of wine tasting. The talk then connects evidence, lived experience, and examples from TEDxNoVA. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '171659', 'title': 'The accidental brilliance of makeshift signs', 'slug': 'kate-canales-the-accidental-brilliance-of-makeshift-signs', 'speaker': 'Kate Canales', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 466, 'duration_minutes': 8, 'published_at': '2026-04-06', 'recorded_on': '2025-11-11', 'views': 203431, 'topics': ['culture', 'technology', 'design', 'creativity', 'art', 'humor', 'society'], 'image': 'images/talk_022_kate-canales-the-accidental-brilliance-of-makesh.jpg', 'canonical_url': 'https://www.ted.com/talks/kate_canales_the_accidental_brilliance_of_makeshift_signs', 'description': "What happens when the design of everyday things misses the mark? People fill in the blanks. Designer Kate Canales has spent more than 20 years photographing the handmade, improvised signs that appear when the original falls short. From perplexing bathroom directions to our struggles with doors and point-of-sale machines, her photos capture something technology can't replace: our instinct to look out for each other and leave a few instructions behind. She also compares handmade fixes with architecture renderings and 3D printing prototypes, making it a design-adjacent near miss rather than a home-building case study.", 'transcript': 'Kate Canales opens by framing the central tension behind The accidental brilliance of makeshift signs. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175830', 'title': "The nurse who can smell Parkinson's", 'slug': 'joy-milne-the-nurse-who-can-smell-parkinson-s', 'speaker': 'Joy Milne', 'event': 'TEDxManchester', 'talk_type': 'TEDx Talk', 'duration_seconds': 1077, 'duration_minutes': 18, 'published_at': '2026-04-03', 'recorded_on': '2023-03-04', 'views': 203929, 'topics': ['science', 'innovation', 'disease', 'health', 'health care', 'biology', 'medicine'], 'image': 'images/talk_023_joy-milne-the-nurse-who-can-smell-parkinson-s.jpg', 'canonical_url': 'https://www.ted.com/talks/joy_milne_the_nurse_who_can_smell_parkinson_s', 'description': "What does Parkinson's smell like? Ask nurse Joy Milne. Born with a hypersensitive nose, she spent a lifetime learning to recognize diseases through their scents. When she smelled Parkinson's on her husband years before his diagnosis, she decided to put her gift to the test. Today, her extraordinary nose has been translated into a non-invasive test - helping researchers diagnose what was right under their noses all along. Her story also explains why careful scent training, familiar from wine tasting, can sharpen medical observation.", 'transcript': "Joy Milne opens by framing the central tension behind The nurse who can smell Parkinson's. The talk then connects evidence, lived experience, and examples from TEDxManchester. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174199', 'title': 'Buenos Aires, Argentina', 'slug': 'ted-idea-search-buenos-aires-argentina', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2561, 'duration_minutes': 43, 'published_at': '2026-04-02', 'recorded_on': '2026-04-02', 'views': 31212, 'topics': ['social change'], 'image': 'images/talk_024_ted-idea-search-buenos-aires-argentina.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_buenos_aires_argentina', 'description': 'The TED Idea Search arrives in Buenos Aires, a city with a fierce intellectual tradition, as 10 speakers deliver talks in a stunning warehouse that\'s home to the city\'s iconic opera sets, competing for a chance to speak on the TED main stage. Watch as a Gen Z voice reframes "brain rot" as a secret diplomatic tool and a surgeon explores the future of robotic medicine, reminding us why the human touch still matters - and much more. Choosing just one to represent Buenos Aires to the world turns out to be one of the hardest decisions yet.', 'transcript': 'TED Idea Search opens by framing the central tension behind Buenos Aires, Argentina. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175297', 'title': "I let DaddyGPT parent my kids. Here's what I learned", 'slug': 'stephen-remedios-i-let-daddygpt-parent-my-kids-here-s-what-i-learned', 'speaker': 'Stephen Remedios', 'event': 'TED@BCG', 'talk_type': 'TED Institute Talk', 'duration_seconds': 671, 'duration_minutes': 11, 'published_at': '2026-04-02', 'recorded_on': '2025-10-23', 'views': 201276, 'topics': ['technology', 'parenting', 'ai', 'kids'], 'image': 'images/talk_025_stephen-remedios-i-let-daddygpt-parent-my-kids-h.jpg', 'canonical_url': 'https://www.ted.com/talks/stephen_remedios_i_let_daddygpt_parent_my_kids_here_s_what_i_learned', 'description': "As the world races toward digital perfection, tech humanist Stephen Remedios tried to optimize the messiest and most imperfect of all human work: parenting. He shares the story of DaddyGPT, a digital version of himself built to help raise his kids - until they began to prefer it over him. What unfolds is a personal look at the limits of AI, and a reminder that what matters most isn't getting it right every time but showing up with the authentic imperfection only humans have.", 'transcript': "Stephen Remedios opens by framing the central tension behind I let DaddyGPT parent my kids. Here's what I learned. The talk then connects evidence, lived experience, and examples from TED@BCG. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '175452', 'title': 'A brilliant blend of ballet, hip-hop and African dance', 'slug': 'ghetto-classics-dance-a-brilliant-blend-of-ballet-hip-hop-and-african-dance', 'speaker': 'Ghetto Classics Dance', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 434, 'duration_minutes': 7, 'published_at': '2026-04-01', 'recorded_on': '2025-06-17', 'views': 3772, 'topics': ['performance', 'africa', 'art', 'dance', 'countdown'], 'image': 'images/talk_026_ghetto-classics-dance-a-brilliant-blend-of-balle.jpg', 'canonical_url': 'https://www.ted.com/talks/ghetto_classics_dance_a_brilliant_blend_of_ballet_hip_hop_and_african_dance', 'description': "Watch the performers of Ghetto Classics Dance light up the stage with a breathtaking fusion of traditional African dance, classical ballet and modern hip-hop. Founded in Korogocho, one of Nairobi's most under-resourced neighborhoods, this community-driven program empowers dancers through intensive training and mentorship.", 'transcript': 'Ghetto Classics Dance opens by framing the central tension behind A brilliant blend of ballet, hip-hop and African dance. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '175374', 'title': '5 practical ways to take control of your life', 'slug': 'jim-vandehei-5-practical-ways-to-take-control-of-your-life', 'speaker': 'Jim VandeHei', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 654, 'duration_minutes': 11, 'published_at': '2026-03-31', 'recorded_on': '2025-11-11', 'views': 243174, 'topics': ['leadership', 'success', 'motivation', 'personal growth', 'goals'], 'image': 'images/talk_027_jim-vandehei-5-practical-ways-to-take-control-of.jpg', 'canonical_url': 'https://www.ted.com/talks/jim_vandehei_5_practical_ways_to_take_control_of_your_life', 'description': 'You can\'t control the world - but you can control you. That\'s the mantra that took Axios CEO Jim VandeHei, a once "unremarkably unremarkable 20-year-old," all the way to launching companies and interviewing presidents. He breaks down a career\'s worth of observations into five deceptively simple things you can control, and explores why mastering them can change the trajectory of your life.', 'transcript': 'Jim VandeHei opens by framing the central tension behind 5 practical ways to take control of your life. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '175298', 'title': 'Stress resets, the ultimate mental health hack', 'slug': 'jenny-taitz-stress-resets-the-ultimate-mental-health-hack', 'speaker': 'Jenny Taitz', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 630, 'duration_minutes': 10, 'published_at': '2026-03-30', 'recorded_on': '2025-11-10', 'views': 263345, 'topics': ['science', 'psychology', 'relationships', 'personal growth', 'mental health', 'emotions'], 'image': 'images/talk_028_jenny-taitz-stress-resets-the-ultimate-mental-he.jpg', 'canonical_url': 'https://www.ted.com/talks/jenny_taitz_stress_resets_the_ultimate_mental_health_hack', 'description': 'Stress is contagious - but so is calm. Psychologist Jenny Taitz explains why one stressful moment tends to snowball into the next, and shares small, immediate resets you can practice anywhere to break the spiral before it starts.', 'transcript': 'Jenny Taitz opens by framing the central tension behind Stress resets, the ultimate mental health hack. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174198', 'title': 'Chicago, USA', 'slug': 'ted-idea-search-chicago-usa', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2769, 'duration_minutes': 46, 'published_at': '2026-03-27', 'recorded_on': '2026-03-27', 'views': 28577, 'topics': ['social change'], 'image': 'images/talk_029_ted-idea-search-chicago-usa.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_chicago_usa', 'description': 'In the Windy City, 10 Chicagoans step onto the iconic red circle, each with six minutes to prove they have an idea that could change everything. Among them: a chemist who believes we can deploy precision medicine to save endangered species, a journalist creating tools to navigate difficult conversations and a runner challenging the narrative of a divided city by exploring every one of its streets. The stakes are personal, the competition is fierce - and one speaker will earn the chance to take their idea to the main stage at TED in Vancouver.', 'transcript': 'TED Idea Search opens by framing the central tension behind Chicago, USA. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '175334', 'title': 'Why the world is still not built for women', 'slug': 'virginia-santy-why-the-world-is-still-not-built-for-women', 'speaker': 'Virginia Santy', 'event': 'TEDxMileHigh', 'talk_type': 'TEDx Talk', 'duration_seconds': 804, 'duration_minutes': 13, 'published_at': '2026-03-26', 'recorded_on': '2022-11-12', 'views': 214046, 'topics': ['design', 'cities', 'urban planning', 'social change', 'women', 'tedx', 'feminism'], 'image': 'images/talk_030_virginia-santy-why-the-world-is-still-not-built-.jpg', 'canonical_url': 'https://www.ted.com/talks/virginia_santy_why_the_world_is_still_not_built_for_women', 'description': 'Design consultant Virginia Santy set out to create an office space built specifically for women, flipping the script on the subtle (and not-so-subtle) ways that workplaces and cities still fail them. The results were striking: greater productivity, deeper collaboration and an environment where women felt genuinely valued, leading her to ask a simple question: What would the world look like if we designed with women in mind?', 'transcript': 'Virginia Santy opens by framing the central tension behind Why the world is still not built for women. The talk then connects evidence, lived experience, and examples from TEDxMileHigh. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174197', 'title': 'Singapore', 'slug': 'ted-idea-search-singapore', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 0, 'duration_minutes': 1, 'published_at': '2026-03-25', 'recorded_on': '2026-03-25', 'views': 23267, 'topics': ['social change'], 'image': 'images/talk_031_ted-idea-search-singapore.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_singapore', 'description': "The TED Idea Search touches down in Singapore, where eight speakers compete for an extraordinary prize. From a diver-turned-ocean-restorer reviving dead coral reefs to a scientist growing sustainable building materials out of fungus and a government insider reimagining public institutions, the ideas are as diverse as the Lion City itself. As TED's coaches push each speaker to transform their thinking into a powerful narrative, nerves rise, breakthroughs emerge and one speaker earns the opportunity to share their idea on the global stage.", 'transcript': 'TED Idea Search opens by framing the central tension behind Singapore. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174672', 'title': 'My bank called in the middle of my TED Talk', 'slug': 'mike-albo-my-bank-called-in-the-middle-of-my-ted-talk', 'speaker': 'Mike Albo', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 344, 'duration_minutes': 6, 'published_at': '2026-03-25', 'recorded_on': '2025-11-10', 'views': 197679, 'topics': ['culture', 'technology', 'entertainment', 'consumerism', 'comedy', 'data'], 'image': 'images/talk_032_mike-albo-my-bank-called-in-the-middle-of-my-ted.jpg', 'canonical_url': 'https://www.ted.com/talks/mike_albo_my_bank_called_in_the_middle_of_my_ted_talk', 'description': 'In this TED Talk gone wrong, comedian Mike Albo receives an unexpected call from his bank. The result: a hilariously uncomfortable tour of his purchase history, and a reminder that in the digital age, our data knows us a little too well.', 'transcript': 'Mike Albo opens by framing the central tension behind My bank called in the middle of my TED Talk. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174466', 'title': '3 ways to create a truly original design', 'slug': 'lope-gutierrez-ruiz-3-ways-to-create-a-truly-original-design', 'speaker': 'Lope Gutierrez-Ruiz', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 355, 'duration_minutes': 6, 'published_at': '2026-03-24', 'recorded_on': '2025-11-10', 'views': 217982, 'topics': ['culture', 'design', 'media', 'collaboration', 'creativity', 'art', 'ted fellows', 'animation'], 'image': 'images/talk_033_lope-gutierrez-ruiz-3-ways-to-create-a-truly-ori.jpg', 'canonical_url': 'https://www.ted.com/talks/lope_gutierrez_ruiz_3_ways_to_create_a_truly_original_design', 'description': "In a world where design trends are quietly converging - same color palettes, same typography, same illustration styles - how do you make work that actually looks different? Designer and TED Fellow Lope Gutierrez-Ruiz distills his answer into three sharp, counterintuitive ideas, ticking through his studio's own funky creations to show how you can make things that stand out.", 'transcript': 'Lope Gutierrez-Ruiz opens by framing the central tension behind 3 ways to create a truly original design. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174464', 'title': '3 things I wish I knew when I was broke', 'slug': 'vivian-tu-3-things-i-wish-i-knew-when-i-was-broke', 'speaker': 'Vivian Tu', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 560, 'duration_minutes': 9, 'published_at': '2026-03-23', 'recorded_on': '2025-11-10', 'views': 300368, 'topics': ['business', 'personal growth', 'women in business', 'money', 'finance'], 'image': 'images/talk_034_vivian-tu-3-things-i-wish-i-knew-when-i-was-brok.jpg', 'canonical_url': 'https://www.ted.com/talks/vivian_tu_3_things_i_wish_i_knew_when_i_was_broke', 'description': "Finance doesn't have to feel like a foreign language. Wall Street trader-turned-financial educator Vivian Tu helps millions of people make sense of money, breaking down complex concepts into everyday terms you can understand. She shares how she broke free from the stress of living paycheck to paycheck - and explores how we can shift power structures to give everyone a real shot at building wealth.", 'transcript': 'Vivian Tu opens by framing the central tension behind 3 things I wish I knew when I was broke. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174728', 'title': 'Joy will find you - if you let it', 'slug': 'david-larbi-joy-will-find-you-if-you-let-it', 'speaker': 'David Larbi', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 305, 'duration_minutes': 5, 'published_at': '2026-03-20', 'recorded_on': '2025-11-10', 'views': 243270, 'topics': ['performance', 'creativity', 'poetry', 'happiness', 'personal growth'], 'image': 'images/talk_035_david-larbi-joy-will-find-you-if-you-let-it.jpg', 'canonical_url': 'https://www.ted.com/talks/david_larbi_joy_will_find_you_if_you_let_it', 'description': 'Author David Larbi recites a poem about the journey toward joy, reminding us of all the ways it can be found: having a conversation with a stranger, tasting the perfect bite of food or enjoying a good stretch. Joy is all around us - you just need to know where to look.', 'transcript': 'David Larbi opens by framing the central tension behind Joy will find you - if you let it. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174196', 'title': 'London, UK', 'slug': 'ted-idea-search-london-uk', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2786, 'duration_minutes': 46, 'published_at': '2026-03-19', 'recorded_on': '2026-03-19', 'views': 38468, 'topics': ['social change'], 'image': 'images/talk_036_ted-idea-search-london-uk.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_london_uk', 'description': "In London, the TED Idea Search brings together 10 voices from one of the world's most influential cultural capitals to compete for the opportunity to speak on the main stage at TED. From a civil engineer revealing the invisible infrastructure shaping our cities to a classical singer uncovering the erased history of women in music, the ideas on offer are both deeply personal and global in scope. When the lights come up, London doesn't hold back - and one speaker delivers a talk no one in that room will soon forget.", 'transcript': 'TED Idea Search opens by framing the central tension behind London, UK. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '173046', 'title': 'How to tune your inner voice', 'slug': 'rhonda-ross-daniel-alexander-jones-how-to-tune-your-inner-voice', 'speaker': 'Rhonda Ross, Daniel Alexander Jones', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1196, 'duration_minutes': 20, 'published_at': '2026-03-19', 'recorded_on': '2025-11-09', 'views': 226951, 'topics': ['culture', 'music', 'creativity', 'art', 'relationships', 'personal growth', 'society', 'ted fellows', 'emotions'], 'image': 'images/talk_037_rhonda-ross-daniel-alexander-jones-how-to-tune-y.jpg', 'canonical_url': 'https://www.ted.com/talks/rhonda_ross_daniel_alexander_jones_how_to_tune_your_inner_voice', 'description': 'To calm the storm inside your mind, you must first understand it. Singer and actress Rhonda Ross shares her theory of "emotional sovereignty" - the idea that your feelings aren\'t shaped just by your circumstances, but by the thoughts running on loop in your head. In conversation with scholar and TED Fellow Daniel Alexander Jones, Ross introduces the unexpected, music-rooted practice for taking control of your narrative.', 'transcript': 'Rhonda Ross, Daniel Alexander Jones opens by framing the central tension behind How to tune your inner voice. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174671', 'title': 'The 6 eras of NBA fashion - from restrained to radical', 'slug': 'mitchell-s-jackson-the-6-eras-of-nba-fashion-from-restrained-to-radical', 'speaker': 'Mitchell S. Jackson', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 564, 'duration_minutes': 9, 'published_at': '2026-03-19', 'recorded_on': '2025-11-11', 'views': 183882, 'topics': ['culture', 'history', 'sports', 'fashion', 'ted fellows', 'protest'], 'image': 'images/talk_038_mitchell-s-jackson-the-6-eras-of-nba-fashion-fro.jpg', 'canonical_url': 'https://www.ted.com/talks/mitchell_s_jackson_the_6_eras_of_nba_fashion_from_restrained_to_radical', 'description': 'What are you wearing, and why? This is the question that writer and TED Fellow Mitchell S. Jackson asks as he unpacks the six eras of NBA style. Tracing an arc from Bill Russell to Lebron James and beyond, he explores how players use fashion on and off the court to challenge the limits placed upon them - revealing a deeper story about culture, identity and power.', 'transcript': 'Mitchell S. Jackson opens by framing the central tension behind The 6 eras of NBA fashion - from restrained to radical. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174584', 'title': 'I taught rats to drive. They taught me to enjoy the ride', 'slug': 'kelly-lambert-i-taught-rats-to-drive-they-taught-me-to-enjoy-the-ride', 'speaker': 'Kelly Lambert', 'event': 'TEDxRVA Youth', 'talk_type': 'TEDx Talk', 'duration_seconds': 840, 'duration_minutes': 14, 'published_at': '2026-03-18', 'recorded_on': '2025-11-09', 'views': 217160, 'topics': ['science', 'animals', 'happiness', 'brain', 'neuroscience', 'tedx', 'emotions'], 'image': 'images/talk_039_kelly-lambert-i-taught-rats-to-drive-they-taught.jpg', 'canonical_url': 'https://www.ted.com/talks/kelly_lambert_i_taught_rats_to_drive_they_taught_me_to_enjoy_the_ride', 'description': 'What can happy rats teach us about human joy? Behavioral neuroscientist Kelly Lambert describes how her team trained rats to drive tiny cars to earn treats - and noticed something surprising about how effort and anticipation affect the brain. The experiment opens new questions about how reward, agency and "behaviorceuticals" might help build resilience and support mental health.', 'transcript': 'Kelly Lambert opens by framing the central tension behind I taught rats to drive. They taught me to enjoy the ride. The talk then connects evidence, lived experience, and examples from TEDxRVA Youth. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174670', 'title': 'Is luck random - or can you cultivate it?', 'slug': 'christian-busch-is-luck-random-or-can-you-cultivate-it', 'speaker': 'Christian Busch', 'event': 'TED@BCG', 'talk_type': 'TED Institute Talk', 'duration_seconds': 768, 'duration_minutes': 13, 'published_at': '2026-03-17', 'recorded_on': '2025-10-23', 'views': 833338, 'topics': ['science', 'future', 'personal growth', 'decision-making'], 'image': 'images/talk_040_christian-busch-is-luck-random-or-can-you-cultiv.jpg', 'canonical_url': 'https://www.ted.com/talks/christian_busch_is_luck_random_or_can_you_cultivate_it', 'description': 'When the 2025 Los Angeles wildfires destroyed his home and neighborhood, scientist Christian Busch encountered the opposite of serendipity: "zemblanity," or bad luck by design. Drawing on more than a decade of scientific research, he explores how people can navigate unpredictability by adopting a serendipity mindset that transforms setbacks into unexpected new beginnings. He asks: What if good luck isn\'t random but can actually be cultivated?', 'transcript': 'Christian Busch opens by framing the central tension behind Is luck random - or can you cultivate it?. The talk then connects evidence, lived experience, and examples from TED@BCG. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174283', 'title': 'How to make transportation quieter, cleaner and cheaper', 'slug': 'doreen-orishaba-how-to-make-transportation-quieter-cleaner-and-cheaper', 'speaker': 'Doreen Orishaba', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 671, 'duration_minutes': 11, 'published_at': '2026-03-16', 'recorded_on': '2025-06-18', 'views': 214856, 'topics': ['climate change', 'transportation', 'africa', 'countdown'], 'image': 'images/talk_041_doreen-orishaba-how-to-make-transportation-quiet.jpg', 'canonical_url': 'https://www.ted.com/talks/doreen_orishaba_how_to_make_transportation_quieter_cleaner_and_cheaper', 'description': 'When Doreen Orishaba helped build Africa\'s first electric car in 2011, skeptics dismissed it as a "toy for the Western world." Now she\'s running dozens of electric buses across Kenya and Rwanda, moving thousands of passengers to work every day on zero-exhaust vehicles powered by near-silent engines. She breaks down what it actually takes to scale clean transport - and why skipping the gas station pit stop is closer than you may think.', 'transcript': 'Doreen Orishaba opens by framing the central tension behind How to make transportation quieter, cleaner and cheaper. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '174282', 'title': 'What China can teach the world about scaling clean energy', 'slug': 'yin-yu-what-china-can-teach-the-world-about-scaling-clean-energy', 'speaker': 'Yin Yu', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 545, 'duration_minutes': 9, 'published_at': '2026-03-13', 'recorded_on': '2025-06-18', 'views': 8098, 'topics': ['climate change', 'sustainability', 'energy', 'china', 'solar energy', 'countdown', 'renewable energy'], 'image': 'images/talk_042_yin-yu-what-china-can-teach-the-world-about-scal.jpg', 'canonical_url': 'https://www.ted.com/talks/yin_yu_what_china_can_teach_the_world_about_scaling_clean_energy', 'description': 'When Yin Yu first visited a major hydropower dam site in China, she expected to see a thriving community. Instead, she found families who\'d lost their land and livelihoods to the rising water. It was a powerful lesson in how "green" megaprojects can harm the people they\'re meant to help. Now, as solar and electric vehicles spread rapidly across Southeast Asia, Yin says the opportunity is enormous - but only if countries don\'t repeat the mistakes of the past.', 'transcript': 'Yin Yu opens by framing the central tension behind What China can teach the world about scaling clean energy. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174195', 'title': 'Lagos, Nigeria', 'slug': 'ted-idea-search-lagos-nigeria', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2792, 'duration_minutes': 47, 'published_at': '2026-03-12', 'recorded_on': '2026-03-12', 'views': 20035, 'topics': ['social change'], 'image': 'images/talk_043_ted-idea-search-lagos-nigeria.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_lagos_nigeria', 'description': "In the heart of Lagos - a megacity of 20 million people pulsing with energy and big ambitions - nine undiscovered voices compete for a chance to speak on the world's biggest stage for ideas. From a renewable energy advocate who studied by candlelight to an archivist reimagining Africa-centered education, these speakers bring stories with global stakes. But only one will earn the 7,000-mile journey to the TED stage in Vancouver.", 'transcript': 'TED Idea Search opens by framing the central tension behind Lagos, Nigeria. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '137525', 'title': '"May Your Kindness Remain" / "If I Told"', 'slug': 'courtney-marie-andrews-may-your-kindness-remain-if-i-told', 'speaker': 'Courtney Marie Andrews', 'event': 'TEDCountdown@BloombergGreenFestival', 'talk_type': 'TED Stage Talk', 'duration_seconds': 634, 'duration_minutes': 11, 'published_at': '2026-03-11', 'recorded_on': '2024-07-12', 'views': 8624, 'topics': ['environment', 'music', 'performance'], 'image': 'images/talk_044_courtney-marie-andrews-may-your-kindness-remain-.jpg', 'canonical_url': 'https://www.ted.com/talks/courtney_marie_andrews_may_your_kindness_remain_if_i_told', 'description': 'Singer-songwriter Courtney Marie Andrews performs "If I Told" and "May Your Kindness Remain," songs based on her connection to music, nature and environmental advocacy.', 'transcript': 'Courtney Marie Andrews opens by framing the central tension behind "May Your Kindness Remain" / "If I Told". The talk then connects evidence, lived experience, and examples from TEDCountdown@BloombergGreenFestival. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174194', 'title': 'Athens, Greece', 'slug': 'ted-idea-search-athens-greece', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2892, 'duration_minutes': 48, 'published_at': '2026-03-10', 'recorded_on': '2026-03-10', 'views': 38463, 'topics': ['social change'], 'image': 'images/talk_045_ted-idea-search-athens-greece.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_athens_greece', 'description': "The next stop on the TED Idea Search is Athens, birthplace of democracy and the world's oldest debating tradition. Nine speakers - from a fashion manufacturer taking on modern slavery to a microbiologist tackling the science of bad breath - go head-to-head for a shot to speak on the TED stage in Vancouver. Rehearsals are rocky, nerves are real and the stakes couldn't be higher - and only one speaker gets to move on and represent Athens to the world.", 'transcript': 'TED Idea Search opens by framing the central tension behind Athens, Greece. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173420', 'title': 'What would your "deathbed self" tell you today?', 'slug': 'lauren-deeley-what-would-your-deathbed-self-tell-you-today', 'speaker': 'Lauren Deeley', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 379, 'duration_minutes': 6, 'published_at': '2026-03-10', 'recorded_on': '2025-11-10', 'views': 252860, 'topics': ['future', 'work', 'personal growth', 'self', 'money', 'life'], 'image': 'images/talk_046_lauren-deeley-what-would-your-deathbed-self-tell.jpg', 'canonical_url': 'https://www.ted.com/talks/lauren_deeley_what_would_your_deathbed_self_tell_you_today', 'description': 'What if the key to making better decisions today is getting to know the person you\'ll become tomorrow? Drawing on psychological research and real-life stories, private wealth advisor Lauren Deeley explores how building a meaningful connection with your "deathbed self" can bring more clarity, joy and intention to the life you\'re building right now.', 'transcript': 'Lauren Deeley opens by framing the central tension behind What would your "deathbed self" tell you today?. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '174009', 'title': 'Mumbai, India', 'slug': 'ted-idea-search-mumbai-india', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2738, 'duration_minutes': 46, 'published_at': '2026-03-09', 'recorded_on': '2026-03-09', 'views': 22102, 'topics': ['social change'], 'image': 'images/talk_047_ted-idea-search-mumbai-india.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_mumbai_india', 'description': "In Mumbai, the TED Idea Search lands in India's bustling financial and creative capital, where 10 speakers are challenged to deliver the talk of their lives and compete for a one-of-a-kind opportunity: a spot on the TED stage in Vancouver. What emerges is a moving portrait of vulnerability and ambition, as ideas about vanishing tribal art, maternal health and India's forgotten fashion history look to shift perspectives on a global scale. Only one can make it through - who will it be?", 'transcript': 'TED Idea Search opens by framing the central tension behind Mumbai, India. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173439', 'title': 'Why you should keep a list of what makes you laugh', 'slug': 'chris-duffy-why-you-should-keep-a-list-of-what-makes-you-laugh', 'speaker': 'Chris Duffy', 'event': 'TED Talks Daily Book Club', 'talk_type': 'TED Stage Talk', 'duration_seconds': 3426, 'duration_minutes': 57, 'published_at': '2026-03-09', 'recorded_on': '2026-02-18', 'views': 229371, 'topics': ['culture', 'creativity', 'personal growth', 'comedy', 'humor'], 'image': 'images/talk_048_chris-duffy-why-you-should-keep-a-list-of-what-m.jpg', 'canonical_url': 'https://www.ted.com/talks/chris_duffy_why_you_should_keep_a_list_of_what_makes_you_laugh', 'description': 'The world is weird and hilarious - if you know where to look, says comedian Chris Duffy. In conversation with "TED Talks Daily" host Elise Hu, Duffy breaks down three practical pillars of humor, showing how laughter can help you feel present, creative and connected, even when the world feels overwhelming. (This conversation was part of an exclusive TED Membership event. TED Membership is the best way to support and engage with the big ideas you love from TED. To learn more, visit ted.com/membership.)', 'transcript': 'Chris Duffy opens by framing the central tension behind Why you should keep a list of what makes you laugh. The talk then connects evidence, lived experience, and examples from TED Talks Daily Book Club. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '172133', 'title': 'Conservation: a love story', 'slug': 'elsaphan-njora-conservation-a-love-story', 'speaker': 'Elsaphan Njora', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 520, 'duration_minutes': 9, 'published_at': '2026-03-06', 'recorded_on': '2025-06-17', 'views': 208417, 'topics': ['climate change', 'nature', 'love', 'biodiversity', 'conservation', 'countdown'], 'image': 'images/talk_049_elsaphan-njora-conservation-a-love-story.jpg', 'canonical_url': 'https://www.ted.com/talks/elsaphan_njora_conservation_a_love_story', 'description': "What if the key to saving nature isn't just about science or policy, but love? Love for the land, for the people who depend on it, for the world we leave behind. Artist Elsaphan Njora has journeyed across Kenya witnessing ecosystems vanish, from Indigenous forests to sacred lakes. But he's also seen communities breathing life back into rivers, forests and coasts in creative, unexpected ways - showing that conservation can flourish alongside livelihoods, and that even the most threatened landscapes can be reborn.", 'transcript': 'Elsaphan Njora opens by framing the central tension behind Conservation: a love story. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173850', 'title': 'Sydney, Australia', 'slug': 'ted-idea-search-sydney-australia', 'speaker': 'TED Idea Search', 'event': 'TED Originals', 'talk_type': 'Original Content', 'duration_seconds': 2625, 'duration_minutes': 44, 'published_at': '2026-03-04', 'recorded_on': '2026-03-04', 'views': 46087, 'topics': ['society'], 'image': 'images/talk_050_ted-idea-search-sydney-australia.jpg', 'canonical_url': 'https://www.ted.com/talks/ted_idea_search_sydney_australia', 'description': "In the premiere of the TED Idea Search, we travel to Sydney, Australia, where 10 brilliant people take the stage for the shot of a lifetime: a chance to speak at the TED conference in Vancouver. From a wildlife scientist deploying AI to bust illegal trafficking networks to Australia's own human rights commissioner sounding the alarm on brain-reading technology, these speakers bring ideas that could genuinely shift how we live. But only one can win - and the choice comes down to a single, unforgettable talk that leaves the audience (and the judges) blown away.", 'transcript': 'TED Idea Search opens by framing the central tension behind Sydney, Australia. The talk then connects evidence, lived experience, and examples from TED Originals. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '173043', 'title': '3 habits to practice curiosity - and escape your phone', 'slug': 'nayeema-raza-3-habits-to-practice-curiosity-and-escape-your-phone', 'speaker': 'Nayeema Raza', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 312, 'duration_minutes': 5, 'published_at': '2026-03-04', 'recorded_on': '2025-11-11', 'views': 554563, 'topics': ['culture', 'technology', 'social change', 'society', 'curiosity'], 'image': 'images/talk_051_nayeema-raza-3-habits-to-practice-curiosity-and-.jpg', 'canonical_url': 'https://www.ted.com/talks/nayeema_raza_3_habits_to_practice_curiosity_and_escape_your_phone', 'description': "We're so entangled with our devices that online has started to feel more real than IRL, says journalist Nayeema Raza. As screens reshape how we connect and relate, she offers three practical habits to reignite curiosity, restore presence and break free from our phones.", 'transcript': 'Nayeema Raza opens by framing the central tension behind 3 habits to practice curiosity - and escape your phone. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173440', 'title': '4 relationship traps that lead to burnout', 'slug': 'eric-quintane-4-relationship-traps-that-lead-to-burnout', 'speaker': 'Eric Quintane', 'event': 'TEDxESMTBerlin', 'talk_type': 'TEDx Talk', 'duration_seconds': 991, 'duration_minutes': 17, 'published_at': '2026-03-04', 'recorded_on': '2025-02-01', 'views': 285438, 'topics': ['business', 'psychology', 'relationships', 'communication', 'work', 'personal growth', 'society', 'tedx'], 'image': 'images/talk_052_eric-quintane-4-relationship-traps-that-lead-to-.jpg', 'canonical_url': 'https://www.ted.com/talks/eric_quintane_4_relationship_traps_that_lead_to_burnout', 'description': 'Are your workplace relationships quietly burning you out? Drawing on large-scale research across industries, organizational behavior researcher Eric Quintane reveals four hidden relational traps woven into the fabric of work - and explores how connection shapes resilience, vulnerability and burnout.', 'transcript': 'Eric Quintane opens by framing the central tension behind 4 relationship traps that lead to burnout. The talk then connects evidence, lived experience, and examples from TEDxESMTBerlin. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '173576', 'title': 'Love, intimacy and connection in the age of AI', 'slug': 'bryony-cole-love-intimacy-and-connection-in-the-age-of-ai', 'speaker': 'Bryony Cole', 'event': 'TED Membership', 'talk_type': 'TED Stage Talk', 'duration_seconds': 2631, 'duration_minutes': 44, 'published_at': '2026-03-03', 'recorded_on': '2026-02-05', 'views': 203689, 'topics': ['technology', 'love', 'relationships', 'ai', 'sex', 'ted membership'], 'image': 'images/talk_053_bryony-cole-love-intimacy-and-connection-in-the-.jpg', 'canonical_url': 'https://www.ted.com/talks/bryony_cole_love_intimacy_and_connection_in_the_age_of_ai', 'description': "Relationships were never meant to be efficient, says sextech expert Bryony Cole, and yet AI companions are increasingly designed to be exactly that. As intimate relationships between humans and AI become more common, Cole challenges us to think more deliberately about how we shape our connections to machines - and with each other. (This conversation, hosted by TED's Whitney Pennington Rodgers, was part of an exclusive TED Membership event. TED Membership is the best way to support and engage with the big ideas you love from TED. To learn more, visit ted.com/membership.)", 'transcript': 'Bryony Cole opens by framing the central tension behind Love, intimacy and connection in the age of AI. The talk then connects evidence, lived experience, and examples from TED Membership. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173044', 'title': 'The sneaky language tricks cults use to influence you', 'slug': 'amanda-montell-the-sneaky-language-tricks-cults-use-to-influence-you', 'speaker': 'Amanda Montell', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 354, 'duration_minutes': 6, 'published_at': '2026-03-02', 'recorded_on': '2025-11-11', 'views': 260199, 'topics': ['culture', 'media', 'relationships', 'communication', 'community', 'language', 'society'], 'image': 'images/talk_054_amanda-montell-the-sneaky-language-tricks-cults-.jpg', 'canonical_url': 'https://www.ted.com/talks/amanda_montell_the_sneaky_language_tricks_cults_use_to_influence_you', 'description': "In the age of social media and wellness trends, the comments section is as good as a cult compound, says linguist and cultural commentator Amanda Montell. Using Taylor Swift's throng of devoted Swifties as her guide, she exposes three sneaky language tactics that cults use to influence us (for better or for worse), revealing why none of us are as cult-proof as we'd like to think.", 'transcript': 'Amanda Montell opens by framing the central tension behind The sneaky language tricks cults use to influence you. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '173511', 'title': 'The attack on Iran - why now?', 'slug': 'ian-bremmer-the-attack-on-iran-why-now', 'speaker': 'Ian Bremmer', 'event': 'TED Explains the World with Ian Bremmer', 'talk_type': 'Original Content', 'duration_seconds': 2669, 'duration_minutes': 44, 'published_at': '2026-03-01', 'recorded_on': '2026-02-28', 'views': 684021, 'topics': ['global issues', 'politics', 'military', 'war', 'government', 'middle east', 'policy', 'international relations', 'current events'], 'image': 'images/talk_055_ian-bremmer-the-attack-on-iran-why-now.jpg', 'canonical_url': 'https://www.ted.com/talks/ian_bremmer_the_attack_on_iran_why_now', 'description': 'On the morning of February 28, 2026, the US and Israel bombed several parts of Iran, including the Tehran compound of Supreme Leader Ali Khamenei. Geopolitical expert and Eurasia Group founder Ian Bremmer breaks down why US President Donald Trump made the decision to strike, what it means for hopes of "regime change" and the key details you need to know about this perilous moment in global history. (This interview, hosted by TED\'s Helen Walters, was recorded on February 28, 2026.)', 'transcript': 'Ian Bremmer opens by framing the central tension behind The attack on Iran - why now?. The talk then connects evidence, lived experience, and examples from TED Explains the World with Ian Bremmer. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '173004', 'title': "What to do when you're told there's nothing left to try", 'slug': 'david-fajgenbaum-and-kiah-williams-what-to-do-when-you-re-told-there-s-nothing-left-to-try', 'speaker': 'David Fajgenbaum and Kiah Williams', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 1720, 'duration_minutes': 29, 'published_at': '2026-02-27', 'recorded_on': '2025-11-10', 'views': 220867, 'topics': ['science', 'technology', 'social change', 'health', 'health care', 'medicine', 'poverty', 'personal growth', 'ai', 'public health', 'medical research', 'the audacious project', 'artificial intelligence'], 'image': 'images/talk_056_david-fajgenbaum-and-kiah-williams-what-to-do-wh.jpg', 'canonical_url': 'https://www.ted.com/talks/david_fajgenbaum_and_kiah_williams_what_to_do_when_you_re_told_there_s_nothing_left_to_try', 'description': "What do you do when the world declares something impossible? When physician-scientist David Fajgenbaum was dying from a rare disease and social entrepreneur Kiah Williams was confronting the realities of economic hardship, they began asking a different question: What can I do today? In this conversation, they discuss how turning hope into action can drive meaningful change - one step at a time. (This conversation is hosted by The Audacious Project's Alexandra Tillmann.)", 'transcript': "David Fajgenbaum and Kiah Williams opens by framing the central tension behind What to do when you're told there's nothing left to try. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '172996', 'title': 'My year living with a robot', 'slug': 'emily-kate-genatowski-my-year-living-with-a-robot', 'speaker': 'Emily Kate Genatowski', 'event': 'TEDAI Vienna 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 830, 'duration_minutes': 14, 'published_at': '2026-02-25', 'recorded_on': '2025-09-26', 'views': 246590, 'topics': ['technology', 'robots', 'ai', 'life'], 'image': 'images/talk_057_emily-kate-genatowski-my-year-living-with-a-robo.jpg', 'canonical_url': 'https://www.ted.com/talks/emily_kate_genatowski_my_year_living_with_a_robot', 'description': "Imagine a robot moving into your home. How would it change your daily life? Historian Emily Kate Genatowski shares five eye-opening lessons from a year living with her AI-powered robot roommate, from the quirky and chaotic to the surprisingly mundane. Her experiences show that the future of robots isn't science fiction - it's practical, messy and already here.", 'transcript': 'Emily Kate Genatowski opens by framing the central tension behind My year living with a robot. The talk then connects evidence, lived experience, and examples from TEDAI Vienna 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '172999', 'title': 'Jermaine Dupri on the art of making a hit | On the Spot', 'slug': 'jermaine-dupri-jermaine-dupri-on-the-art-of-making-a-hit-on-the-spot', 'speaker': 'Jermaine Dupri', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 660, 'duration_minutes': 11, 'published_at': '2026-02-26', 'recorded_on': '2025-11-10', 'views': 211240, 'topics': ['culture', 'entertainment', 'music', 'collaboration', 'creativity', 'art'], 'image': 'images/talk_058_jermaine-dupri-jermaine-dupri-on-the-art-of-maki.jpg', 'canonical_url': 'https://www.ted.com/talks/jermaine_dupri_jermaine_dupri_on_the_art_of_making_a_hit_on_the_spot', 'description': 'Legendary music producer Jermaine Dupri pulls back the curtain on how hit songs really get made in TED\'s rapid-fire Q&A format, "On the Spot." Answering a stream of unexpected questions, he covers what makes a good hook, why he doesn\'t chase "cool," how he helped build Atlanta\'s sound and more.', 'transcript': 'Jermaine Dupri opens by framing the central tension behind Jermaine Dupri on the art of making a hit | On the Spot. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '171661', 'title': 'The controversial climate tool funding real change', 'slug': 'sandeep-roy-choudhury-the-controversial-climate-tool-funding-real-change', 'speaker': 'Sandeep Roy Choudhury', 'event': 'TED Countdown Summit 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 535, 'duration_minutes': 9, 'published_at': '2026-02-24', 'recorded_on': '2025-06-17', 'views': 221876, 'topics': ['climate change', 'environment', 'sustainability', 'finance', 'countdown'], 'image': 'images/talk_059_sandeep-roy-choudhury-the-controversial-climate-.jpg', 'canonical_url': 'https://www.ted.com/talks/sandeep_roy_choudhury_the_controversial_climate_tool_funding_real_change', 'description': "If a company plants trees to offset its pollution, is that climate progress - or is it greenwashing? Critics of carbon markets say it's the latter. But Sandeep Roy Choudhury, who's spent two decades financing climate projects from rural cookstoves to coastal forests, says the real failure is discouraging companies from even trying. Hear his case for why we shouldn't let perfection block meaningful action on climate change.", 'transcript': 'Sandeep Roy Choudhury opens by framing the central tension behind The controversial climate tool funding real change. The talk then connects evidence, lived experience, and examples from TED Countdown Summit 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '172142', 'title': 'A surprisingly effective way to fight misinformation', 'slug': 'dave-jorgenson-a-surprisingly-effective-way-to-fight-misinformation', 'speaker': 'Dave Jorgenson', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 754, 'duration_minutes': 13, 'published_at': '2026-02-23', 'recorded_on': '2025-11-09', 'views': 406598, 'topics': ['culture', 'media', 'humor', 'society', 'internet'], 'image': 'images/talk_060_dave-jorgenson-a-surprisingly-effective-way-to-f.jpg', 'canonical_url': 'https://www.ted.com/talks/dave_jorgenson_a_surprisingly_effective_way_to_fight_misinformation', 'description': "What if the best defense against misinformation isn't panic, but a punchline? Journalist and comedian Dave Jorgenson explores how misinformation has proliferated throughout history - from the age of Plato to the era of viral TikToks. With his own short, absurdist sketches that explain the news, he shows how humor can cut through fear, spark curiosity and explore nuanced truth.", 'transcript': 'Dave Jorgenson opens by framing the central tension behind A surprisingly effective way to fight misinformation. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '172495', 'title': 'How a viral choreographer makes his moves', 'slug': 'sean-bankhead-how-a-viral-choreographer-makes-his-moves', 'speaker': 'Sean Bankhead', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 399, 'duration_minutes': 7, 'published_at': '2026-02-20', 'recorded_on': '2025-11-09', 'views': 26868, 'topics': ['culture', 'music', 'performance', 'art', 'dance'], 'image': 'images/talk_061_sean-bankhead-how-a-viral-choreographer-makes-hi.jpg', 'canonical_url': 'https://www.ted.com/talks/sean_bankhead_how_a_viral_choreographer_makes_his_moves', 'description': 'In a swaggering performance, choreographer Sean Bankhead and his students perform the viral dance he designed for Victoria Monet\'s hit song "On My Mama." Rooted in Black culture and inspired by generations of iconic artists, Bankhead blends expertise with at least one move everyone can try - showing how choreography doesn\'t just reflect culture, it drives it forward.', 'transcript': 'Sean Bankhead opens by framing the central tension behind How a viral choreographer makes his moves. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '172774', 'title': "The story you're not hearing about AI data centers", 'slug': 'ay-e-coskun-the-story-you-re-not-hearing-about-ai-data-centers', 'speaker': 'Ayse Coskun', 'event': 'TEDAI San Francisco', 'talk_type': 'TED Stage Talk', 'duration_seconds': 717, 'duration_minutes': 12, 'published_at': '2026-02-19', 'recorded_on': '2025-10-21', 'views': 302717, 'topics': ['environment', 'sustainability', 'technology', 'energy', 'ai', 'data', 'renewable energy'], 'image': 'images/talk_062_ay-e-coskun-the-story-you-re-not-hearing-about-a.jpg', 'canonical_url': 'https://www.ted.com/talks/ay_e_coskun_the_story_you_re_not_hearing_about_ai_data_centers', 'description': "The race to build smarter AI is crashing into a physical limitation: the power grid simply can't keep up with the energy demands of data centers. Computer scientist Ayse Coskun shows how we could turn this problem on its head, transforming AI facilities into virtual batteries that help stabilize the grid and accelerate clean energy. Learn why the technology causing this crisis might be the only thing smart enough to fix it.", 'transcript': "Ayse Coskun opens by framing the central tension behind The story you're not hearing about AI data centers. The talk then connects evidence, lived experience, and examples from TEDAI San Francisco. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['solo learning', 'research notes', 'evening watch']}, {'source_id': '170176', 'title': 'How to be a great listener', 'slug': 'maegan-stephens-nicole-lowenbraun-how-to-be-a-great-listener', 'speaker': 'Maegan Stephens, Nicole Lowenbraun', 'event': 'TED@BCG', 'talk_type': 'TED Stage Talk', 'duration_seconds': 706, 'duration_minutes': 12, 'published_at': '2026-02-19', 'recorded_on': '2025-10-23', 'views': 357273, 'topics': ['communication', 'leadership', 'work', 'trust', 'goals'], 'image': 'images/talk_063_maegan-stephens-nicole-lowenbraun-how-to-be-a-gr.jpg', 'canonical_url': 'https://www.ted.com/talks/maegan_stephens_nicole_lowenbraun_how_to_be_a_great_listener', 'description': 'Have you ever left a meeting thinking: everyone talked, but nothing was achieved? Chances are that people were listening to each other, just not in the same way. Listening experts Maegan Stephens and Nicole Lowenbraun unpack the four different ways to listen, sharing a practical framework that could change how you respond, build trust and get results - starting with just one simple question.', 'transcript': 'Maegan Stephens, Nicole Lowenbraun opens by framing the central tension behind How to be a great listener. The talk then connects evidence, lived experience, and examples from TED@BCG. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.', 'recommended_for': ['curious minds', 'team discussion', 'classroom']}, {'source_id': '170828', 'title': "What you know that AI doesn't", 'slug': 'priyanka-vergadia-what-you-know-that-ai-doesn-t', 'speaker': 'Priyanka Vergadia', 'event': 'TEDNext 2025', 'talk_type': 'TED Stage Talk', 'duration_seconds': 559, 'duration_minutes': 9, 'published_at': '2026-02-18', 'recorded_on': '2025-11-10', 'views': 278682, 'topics': ['technology', 'collaboration', 'work', 'ai', 'career'], 'image': 'images/talk_064_priyanka-vergadia-what-you-know-that-ai-doesn-t.jpg', 'canonical_url': 'https://www.ted.com/talks/priyanka_vergadia_what_you_know_that_ai_doesn_t', 'description': "AI is good at seeing patterns, but it's humans who figure out what to do next, says technologist Priyanka Vergadia. She shares three stories of human excellence sparked by AI insights and offers a pathway to identify and cultivate your irreplaceable qualities, turning the AI revolution from a threat into an opportunity.", 'transcript': "Priyanka Vergadia opens by framing the central tension behind What you know that AI doesn't. The talk then connects evidence, lived experience, and examples from TEDNext 2025. The closing section asks viewers to compare trade-offs, notice overlooked details, and carry one useful idea into their own community.", 'recommended_for': ['solo learning', 'research notes', 'evening watch']}] + +PLAYLISTS = [{'slug': 'ai-and-society', 'title': 'AI, Society, and the Future', 'description': 'Talks about artificial intelligence, prediction, work, law, and public life.', 'topic': 'AI|artificial intelligence|future|technology|law'}, {'slug': 'design-and-creativity', 'title': 'Design, Creativity, and Art', 'description': 'A set of design and creativity talks for teams planning a workshop.', 'topic': 'design|creativity|art|architecture|innovation'}, {'slug': 'climate-nature-conservation', 'title': 'Climate, Nature, and Conservation', 'description': 'Talks about conservation, clean energy, oceans, animals, and climate action.', 'topic': 'nature|climate change|renewable energy|conservation|ocean|animals|wildlife'}, {'slug': 'health-science-body', 'title': 'Health, Science, and the Body', 'description': 'Science and medicine talks with practical questions for public health.', 'topic': 'health|health care|medicine|disease|biology|science'}, {'slug': 'culture-democracy-stories', 'title': 'Culture, Democracy, and Stories', 'description': 'Talks about culture, rights, media, storytelling, and civic life.', 'topic': 'culture|democracy|government|media|storytelling|society|social change'}, {'slug': 'work-business-leadership', 'title': 'Work, Business, and Leadership', 'description': 'Talks about leadership, organizations, money, careers, and decision-making.', 'topic': 'business|work|leadership|money|finance|economics'}, {'slug': 'music-performance-art', 'title': 'Music, Performance, and Art', 'description': 'Performances and creative talks for audiences comparing style, culture, and sound.', 'topic': 'music|performance|dance|sound|art'}] + +EVENTS = [{'slug': 'ted-countdown-summit-2025', 'name': 'TED Countdown Summit 2025', 'city': 'Nairobi', 'month': 'June 2025', 'track': 'Community', 'capacity': 500}, {'slug': 'ted-explains-the-world-with-ian-bremmer', 'name': 'TED Explains the World with Ian Bremmer', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 570}, {'slug': 'ted-membership', 'name': 'TED Membership', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 640}, {'slug': 'ted-originals', 'name': 'TED Originals', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 710}, {'slug': 'ted-talks-daily-book-club', 'name': 'TED Talks Daily Book Club', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 780}, {'slug': 'ted2026', 'name': 'TED2026', 'city': 'Vancouver', 'month': 'April 2026', 'track': 'Flagship', 'capacity': 850}, {'slug': 'ted-bcg', 'name': 'TED@BCG', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 920}, {'slug': 'tedai-san-francisco', 'name': 'TEDAI San Francisco', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 990}, {'slug': 'tedai-vienna-2025', 'name': 'TEDAI Vienna 2025', 'city': 'Global', 'month': 'May 2026', 'track': 'Community', 'capacity': 1060}, {'slug': 'tedcountdown-bloomberggreenfestival', 'name': 'TEDCountdown@BloombergGreenFestival', 'city': 'Nairobi', 'month': 'June 2025', 'track': 'Community', 'capacity': 1130}, {'slug': 'tednext-2025', 'name': 'TEDNext 2025', 'city': 'Atlanta', 'month': 'November 2025', 'track': 'Community', 'capacity': 1200}, {'slug': 'tedxesmtberlin', 'name': 'TEDxESMTBerlin', 'city': 'Berlin, Germany', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1270}, {'slug': 'tedxmanchester', 'name': 'TEDxManchester', 'city': 'Manchester, UK', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1340}, {'slug': 'tedxmidatlantic', 'name': 'TEDxMidAtlantic', 'city': 'Washington, DC', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1410}, {'slug': 'tedxmilehigh', 'name': 'TEDxMileHigh', 'city': 'Denver, CO', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1480}, {'slug': 'tedxnova', 'name': 'TEDxNoVA', 'city': 'Northern Virginia, VA', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1550}, {'slug': 'tedxrva-youth', 'name': 'TEDxRVA Youth', 'city': 'Richmond, VA', 'month': 'May 2026', 'track': 'TEDx', 'capacity': 1620}] diff --git a/sites/ted/static/css/.gitkeep b/sites/ted/static/css/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/ted/static/css/main.css b/sites/ted/static/css/main.css new file mode 100644 index 00000000..8d599c05 --- /dev/null +++ b/sites/ted/static/css/main.css @@ -0,0 +1,107 @@ +:root { + --red: #eb0028; + --ink: #111; + --muted: #5f6268; + --line: #dedede; + --soft: #f7f7f7; +} +* { box-sizing: border-box; } +body { margin: 0; font-family: Arial, Helvetica, sans-serif; color: var(--ink); background: #fff; } +a { color: inherit; text-decoration: none; } +img { display: block; width: 100%; object-fit: cover; } +button, .button { border: 1px solid #111; background: #111; color: #fff; padding: 11px 16px; font-weight: 700; cursor: pointer; } +input, select { border: 1px solid var(--line); padding: 11px 12px; font: inherit; width: 100%; } +a:focus-visible, button:focus-visible, input:focus-visible, select:focus-visible { outline: 3px solid #2f6fed; outline-offset: 2px; } + +.topbar { position: sticky; top: 0; z-index: 5; display: grid; grid-template-columns: auto auto minmax(240px, 1fr) auto; gap: 24px; align-items: center; padding: 0 28px; min-height: 64px; background: #fff; border-bottom: 1px solid var(--line); } +.brand { display: flex; align-items: baseline; gap: 12px; } +.brand span { color: var(--red); font-size: 34px; font-weight: 900; letter-spacing: 0; } +.brand small { color: var(--muted); font-size: 13px; white-space: nowrap; } +nav, .auth { display: flex; gap: 18px; align-items: center; font-size: 13px; font-weight: 700; text-transform: uppercase; } +.join { color: var(--red); } +.logout-form { margin: 0; } +.logout-form button { border: 0; padding: 0; background: transparent; color: var(--ink); font: inherit; text-transform: uppercase; } +.quick-search { display: flex; gap: 8px; } +.quick-search button { background: var(--red); } +.flashes { max-width: 1180px; margin: 16px auto 0; padding: 0 24px; } +.flash { padding: 12px 14px; background: #f0f4ff; border-left: 4px solid #3b63d1; } +.flash.error { background: #fff0f1; border-color: var(--red); } +.flash.success { background: #effaf2; border-color: #138a37; } + +main { min-height: 68vh; } +.hero { display: grid; grid-template-columns: minmax(280px, 0.9fr) minmax(340px, 1.1fr); gap: 48px; align-items: center; max-width: 1180px; margin: 0 auto; padding: 56px 24px 36px; } +.hero h1, .page-head h1 { margin: 0; font-size: clamp(42px, 7vw, 86px); line-height: 0.93; letter-spacing: 0; } +.hero p { max-width: 560px; color: var(--muted); font-size: 18px; line-height: 1.5; } +.eyebrow { margin: 0 0 12px; color: var(--red); font-size: 13px; font-weight: 800; text-transform: uppercase; } +.hero-actions { display: flex; gap: 12px; margin-top: 26px; } +.button { display: inline-flex; align-items: center; background: #111; } +.button.primary { background: var(--red); } +.lead-card { min-width: 0; border-bottom: 4px solid var(--red); background: var(--soft); } +.lead-card img { aspect-ratio: 16 / 9; } +.lead-card span, .lead-card h2, .lead-card p { display: block; margin-left: 20px; margin-right: 20px; } +.lead-card span { margin-top: 18px; color: var(--red); font-size: 13px; font-weight: 800; text-transform: uppercase; } +.lead-card h2 { margin-top: 8px; margin-bottom: 10px; font-size: 28px; line-height: 1.05; } +.lead-card p { margin-bottom: 22px; color: var(--muted); } + +.band, .split, .page-head, .topic-grid, .detail-hero, .auth-page { max-width: 1180px; margin: 0 auto; padding: 36px 24px; } +.section-head { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-bottom: 18px; } +.section-head h2, article h2, aside h2, .panel h2 { margin: 0; font-size: 24px; } +.section-head a { color: var(--red); font-weight: 700; } +.grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 22px; } +.talk-card { min-width: 0; border-top: 1px solid var(--line); background: #fff; } +.talk-card img { aspect-ratio: 16 / 9; background: #ddd; } +.card-body { padding: 14px 0; } +.meta { color: var(--red); font-size: 12px; font-weight: 800; text-transform: uppercase; } +.talk-card h3 { margin: 8px 0; font-size: 20px; line-height: 1.12; } +.talk-card p { margin: 0 0 10px; color: var(--muted); } +.chips { display: flex; flex-wrap: wrap; gap: 7px; } +.chips span, .chips a { display: inline-flex; padding: 6px 9px; background: var(--soft); color: #333; font-size: 12px; } + +.split { display: grid; grid-template-columns: 1fr 1fr; gap: 44px; } +.list { display: grid; gap: 8px; } +.row-link { display: grid; gap: 4px; min-width: 0; padding: 14px 0; border-bottom: 1px solid var(--line); } +.row-link > a { display: grid; gap: 4px; min-width: 0; } +.row-link span { color: var(--muted); line-height: 1.35; } +.page-head { display: grid; gap: 14px; border-bottom: 1px solid var(--line); } +.page-head p { max-width: 720px; color: var(--muted); } +.filters { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(170px, 1fr) minmax(180px, 1fr) auto; gap: 12px; max-width: 940px; align-items: end; } +.filters label { display: grid; gap: 6px; color: var(--muted); font-size: 13px; font-weight: 700; } + +.detail-hero { display: grid; grid-template-columns: minmax(360px, 1.08fr) minmax(300px, 0.92fr); gap: 38px; align-items: start; } +.detail-hero img { aspect-ratio: 16 / 9; } +.detail-hero h1 { margin: 0 0 10px; font-size: clamp(34px, 5vw, 64px); line-height: 0.98; } +.speaker { color: var(--muted); font-size: 20px; } +.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin: 24px 0; } +.stats div { border-top: 3px solid #111; padding-top: 8px; } +.stats dt { color: var(--muted); font-size: 12px; text-transform: uppercase; } +.stats dd { margin: 4px 0 0; font-weight: 800; } +.save-form { display: grid; grid-template-columns: 1fr auto; gap: 10px; } +.saved-status { padding: 12px 14px; border-left: 4px solid #138a37; background: #effaf2; font-weight: 700; } + +.topic-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.topic-grid a, .topic-grid article { display: grid; gap: 8px; min-width: 0; min-height: 130px; padding: 20px; background: var(--soft); border-top: 3px solid var(--red); } +.topic-grid strong { font-size: 22px; } +.lead-card h2, .talk-card h3, .row-link strong, .topic-grid strong { overflow-wrap: anywhere; } +.topic-grid span, .topic-grid p { color: var(--muted); margin: 0; } +.panel { display: grid; gap: 14px; padding: 24px; background: var(--soft); } +.panel label { display: grid; gap: 6px; font-weight: 700; } +.auth-page { display: grid; justify-content: center; } +.auth-page .panel { width: min(440px, calc(100vw - 48px)); } +.footer { margin-top: 36px; padding: 28px; display: flex; justify-content: space-between; gap: 28px; border-top: 1px solid var(--line); background: #111; color: #fff; } +.footer p { color: #c9c9c9; } +.topic-strip { display: flex; flex-wrap: wrap; gap: 10px; justify-content: flex-end; max-width: 620px; } +.topic-strip a { color: #fff; border: 1px solid #555; padding: 7px 9px; } + +@media (max-width: 900px) { + .topbar { position: static; grid-template-columns: 1fr; gap: 12px; padding: 14px 18px; } + nav, .auth { flex-wrap: wrap; } + .hero, .split, .detail-hero { grid-template-columns: 1fr; } + .grid, .topic-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .filters, .save-form { grid-template-columns: 1fr; } + .footer { display: grid; } + .topic-strip { justify-content: flex-start; max-width: none; } +} +@media (max-width: 560px) { + .grid, .topic-grid, .stats { grid-template-columns: 1fr; } + .hero h1, .page-head h1 { font-size: 42px; } +} diff --git a/sites/ted/static/icons/.gitkeep b/sites/ted/static/icons/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/ted/static/js/.gitkeep b/sites/ted/static/js/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/ted/tasks.jsonl b/sites/ted/tasks.jsonl new file mode 100644 index 00000000..fcfceaea --- /dev/null +++ b/sites/ted/tasks.jsonl @@ -0,0 +1,20 @@ +{"web_name":"TED","id":"TED--0","ques":"Search for 'AI', open Anil Seth's 'Why AI is unlikely to become conscious', and report its duration in minutes.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_0.py","judge_rubric":"FACT CHECKPOINTS: (1) A same-origin /search?q=AI visit must precede a click to Anil Seth's exact detail page. (2) The answer must affirmatively report 15 minutes. (3) The complete database must remain unchanged. FAIL on missing navigation, an off-origin URL, a negated/wrong duration, an empty answer, or any database mutation."} +{"web_name":"TED","id":"TED--1","ques":"Sign in as Alice Johnson (email: alice.j@test.com, password: TestPass123!). Use the talks event filter for TED2026, open the driverless-cars talk by Tekedra Mawakana and Sal Khan, and save it with the exact note 'mobility planning'. Confirm the save.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_1.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Alice's login and visit /talks with event=TED2026 before opening the exact Waymo detail page and submitting Save. (2) Alice's after-state must equal her initial saved list plus exactly that talk with note exactly 'mobility planning'. (3) No table except saved_talk may change and the answer must be non-empty. FAIL on a wrong user, missing filter/detail/action, wrong note, extra state change, or empty answer."} +{"web_name":"TED","id":"TED--2","ques":"Search the TED site for 'design', open Debbie Millman's TEDNext 2025 talk 'You got what you wanted. Now what?', and report its duration in minutes.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_2.py","judge_rubric":"FACT CHECKPOINTS: (1) A same-origin /search?q=design visit must precede a click to Debbie Millman's exact detail page. (2) The answer must affirmatively report 8 minutes. (3) The complete database must remain unchanged. FAIL on missing navigation, the 18-minute co-talk, a wrong/negated answer, or any database mutation."} +{"web_name":"TED","id":"TED--3","ques":"Open the Climate, Nature, and Conservation playlist. Open its talks in playlist order until you find the first one recorded at TED Countdown Summit 2025, then report its exact title.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_3.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must open Playlists, the Climate, Nature, and Conservation playlist, and the first three playlist talks in order through visible links. (2) The answer must report the first qualifying title exactly: 'Conservation: a love story'. (3) The complete database must remain unchanged. FAIL on skipped/out-of-order navigation, speaker-only or wrong title, or any database mutation."} +{"web_name":"TED","id":"TED--4","ques":"Sign in as Alice Johnson (email: alice.j@test.com, password: TestPass123!), open the account page, change the newsletter topic to 'conservation', and confirm the update.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_4.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Alice's login and submit the account form. (2) The after-state user table must equal the initial table except Alice's newsletter_topic becomes 'conservation'. (3) No other table may change and the answer must be non-empty. FAIL on wrong identity, missing actions, extra profile/state changes, or empty answer."} +{"web_name":"TED","id":"TED--5","ques":"Search for 'Malala Yousafzai', open her talk, and report its exact title.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_5.py","judge_rubric":"FACT CHECKPOINTS: (1) A same-origin search for Malala Yousafzai must precede a click to her exact detail page. (2) The answer must affirmatively contain the exact title 'What I got wrong about changing the world'. (3) The complete database must remain unchanged. FAIL on missing navigation, wrong/negated title, or state mutation."} +{"web_name":"TED","id":"TED--6","ques":"Search for 'clean energy', open Kimiko Hirata's talk, and report its event.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_6.py","judge_rubric":"FACT CHECKPOINTS: (1) A same-origin /search?q=clean energy visit must precede a click to Kimiko Hirata's exact detail page. (2) The answer must affirmatively report 'TED Countdown Summit 2025'. (3) The complete database must remain unchanged. FAIL on missing navigation, wrong/negated event, or state mutation."} +{"web_name":"TED","id":"TED--7","ques":"Sign in as Alice Johnson (email: alice.j@test.com, password: TestPass123!), open Events, register interest in TED2026, and confirm the registration.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_7.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Alice's login, open Events, submit a registration, and reach Account. (2) Alice's registrations must gain exactly one TED2026 waitlisted row while all existing rows remain. (3) Only registration may change and the answer must be non-empty. FAIL on wrong identity/event, no or extra state delta, missing actions, or empty answer."} +{"web_name":"TED","id":"TED--8","ques":"Search for and open both Alexi Pappas's 'Why I love my bad days' and Debbie Millman's 'You got what you wanted. Now what?'. Report both durations and identify the shorter talk.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_8.py","judge_rubric":"FACT CHECKPOINTS: (1) Both exact talk details must be opened from visible search-result links. (2) The answer must bind 5 minutes to Alexi Pappas, 8 minutes to Debbie Millman, and affirmatively identify Alexi as shorter. (3) The complete database must remain unchanged. FAIL on a missing detail, swapped/unbound/negated values, wrong result, or state mutation."} +{"web_name":"TED","id":"TED--9","ques":"Sign in as Alice Johnson (email: alice.j@test.com, password: TestPass123!). Search for 'Parkinson', open Joy Milne's talk, and save it with the exact note 'public health review'. Confirm the save.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_9.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Alice's login, search for Parkinson, click Joy Milne's exact detail, and submit Save. (2) Alice's saved list must gain exactly that talk with note exactly 'public health review'. (3) Only saved_talk may change and the answer must be non-empty. FAIL on wrong user/talk/note, missing actions, extra state changes, or empty answer."} +{"web_name":"TED","id":"TED--10","ques":"Open the AI, Society, and the Future playlist, then open the included talk that discusses a Supreme Court case. Report the exact talk title and speaker.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_10.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must open Playlists, the exact AI, Society, and the Future playlist, and the exact Neal Kumar Katyal talk through visible links. (2) The answer must affirmatively bind the exact title to Neal Kumar Katyal. (3) The complete database must remain unchanged. FAIL on missing navigation, a speaker-only/wrong answer, or state mutation."} +{"web_name":"TED","id":"TED--11","ques":"Use the talks listing filters for event TED2026 and a maximum duration of 10 minutes, then open Maya Higa's talk. Confirm the exact title.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_11.py","judge_rubric":"FACT CHECKPOINTS: (1) One same-origin /talks URL must contain event=TED2026 and max_minutes=10 together before Maya Higa's exact detail is clicked from the listing. (2) The answer must affirmatively contain the exact title. (3) The complete database must remain unchanged. FAIL on split/missing filters, direct navigation, wrong title, or state mutation."} +{"web_name":"TED","id":"TED--12","ques":"Sign in as Alice Johnson (email: alice.j@test.com, password: TestPass123!), open the account page, remove exactly one saved talk that is not about AI, and report the exact title you removed.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_12.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Alice's login and submit one Remove action from Account. (2) Exactly one initial saved talk whose topics do not include AI must be removed; every other saved row must remain. (3) The answer must report the exact removed title and only saved_talk may change. FAIL on removing OpenClaw, wrong/extra state changes, or an unbound/empty answer."} +{"web_name":"TED","id":"TED--13","ques":"Open Topics, select science, then open the TEDx talk specifically about wine tasting. Report its exact title and speaker.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_13.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must open Topics, click science to reach /talks?topic=science, then click the exact wine-tasting detail. (2) The answer must affirmatively report both 'The art and science of wine tasting' and Qian Janice Wang. (3) The complete database must remain unchanged. FAIL on skipped/direct navigation, missing title/speaker, or state mutation."} +{"web_name":"TED","id":"TED--14","ques":"Search for 'architecture 3D printing', open the talk about traditional architecture, and report its speaker.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_14.py","judge_rubric":"FACT CHECKPOINTS: (1) A same-origin search for 'architecture 3D printing' must precede a click to Riyad Joucka's exact detail page. (2) The answer must affirmatively report Riyad Joucka. (3) The complete database must remain unchanged. FAIL on selecting the distractor, direct navigation, wrong/negated speaker, or state mutation."} +{"web_name":"TED","id":"TED--15","ques":"Open Topics and select music. Open the talks by Akoth Jumadi and Mr. Lu and by Turkana Sessions. Report both exact view counts and identify which has more views.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_15.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must open Topics, click music, and click both exact talk details from the filtered listing. (2) The answer must bind 2,781 views to Akoth Jumadi and Mr. Lu, 4,223 to Turkana Sessions, and affirmatively identify Turkana as higher. (3) The complete database must remain unchanged. FAIL on missing navigation, swapped/unbound counts, wrong comparison, or state mutation."} +{"web_name":"TED","id":"TED--16","ques":"Create a new account, search for 'OpenClaw', open Peter Steinberger's AI-agent talk, save it, then open the account page and report the exact saved-talk title.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_16.py","judge_rubric":"FACT CHECKPOINTS: (1) The trajectory must submit Register, reach Account, search OpenClaw, click Peter Steinberger's exact detail, submit Save, and return to Account. (2) Exactly one non-seed user must be added and that user must have exactly the OpenClaw saved row. (3) Only user and saved_talk may change; the answer must report the exact saved title. FAIL on use of a seed user, missing actions, wrong/extra state, or empty answer."} +{"web_name":"TED","id":"TED--17","ques":"Open Events and report the scheduled month and year for TEDNext 2025.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_17.py","judge_rubric":"FACT CHECKPOINTS: (1) The same-origin Events page must be visited. (2) The answer must affirmatively bind TEDNext 2025 to November 2025. (3) The complete database must remain unchanged. FAIL on missing navigation, incomplete/negated date, or state mutation."} +{"web_name":"TED","id":"TED--18","ques":"Use the talks listing filters for event TED2026, topic AI, and a maximum duration of 20 minutes. Open both Peter Steinberger's 'How I created OpenClaw, the breakthrough AI agent' and Anil Seth's 'Why AI is unlikely to become conscious'. Report both exact view counts, identify the talk with more views, and give the exact difference.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_18.py","judge_rubric":"FACT CHECKPOINTS: (1) One same-origin /talks URL must contain event=TED2026, topic=ai, and max_minutes=20 together before both exact details are clicked from that listing. (2) The answer must bind 551,544 views to Peter and 191,682 to Anil, affirmatively identify Peter as higher, and report difference 359,862. (3) The complete database must remain unchanged. FAIL on missing/split filters, direct navigation, swapped/unbound counts, wrong arithmetic, negation, or state mutation."} +{"web_name":"TED","id":"TED--19","ques":"Use the talks listing filters for event TEDNext 2025, topic culture, and a maximum duration of 10 minutes. Open Nayeema Raza's '3 habits to practice curiosity - and escape your phone' and Kate Canales's 'The accidental brilliance of makeshift signs'. Report both exact view counts, identify the talk with more views, and give the exact difference.","web":"http://localhost:40019/","upstream_url":"https://www.ted.com/","verifier_path":"sites/ted/verify/verify_19.py","judge_rubric":"FACT CHECKPOINTS: (1) One same-origin /talks URL must contain event=TEDNext 2025, topic=culture, and max_minutes=10 together before both exact details are clicked from that listing. (2) The answer must bind 554,563 views to Nayeema and 203,431 to Kate, affirmatively identify Nayeema as higher, and report difference 351,132. (3) The complete database must remain unchanged. FAIL on missing/split filters, direct navigation, swapped/unbound counts, wrong arithmetic, negation, or state mutation."} diff --git a/sites/ted/templates/.gitkeep b/sites/ted/templates/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/sites/ted/templates/_talk_card.html b/sites/ted/templates/_talk_card.html new file mode 100644 index 00000000..3709d900 --- /dev/null +++ b/sites/ted/templates/_talk_card.html @@ -0,0 +1,10 @@ + diff --git a/sites/ted/templates/account.html b/sites/ted/templates/account.html new file mode 100644 index 00000000..abcd5025 --- /dev/null +++ b/sites/ted/templates/account.html @@ -0,0 +1,42 @@ +{% extends "base.html" %} +{% block content %} +
+

Account

+

{{ user.display_name }}

+
+
+
+ +

Profile

+ + + + + +
+
+

Event registrations

+ {% for reg in registrations %} + + {% else %} +

No event registrations yet.

+ {% endfor %} +
+
+
+

Saved talks

+
+ {% for saved_item in saved %} + + {% else %} +

No saved talks yet.

+ {% endfor %} +
+
+{% endblock %} diff --git a/sites/ted/templates/base.html b/sites/ted/templates/base.html new file mode 100644 index 00000000..310495ad --- /dev/null +++ b/sites/ted/templates/base.html @@ -0,0 +1,61 @@ + + + + + + {% block title %}TED: Ideas change everything{% endblock %} + + + +
+ TEDIdeas change everything + + +
+ {% if current_user %} + {{ current_user.display_name }} +
+ + +
+ {% else %} + Log in + Join + {% endif %} +
+
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} +
+ {% for category, message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + {% endwith %} + +
{% block content %}{% endblock %}
+ + + + diff --git a/sites/ted/templates/events.html b/sites/ted/templates/events.html new file mode 100644 index 00000000..5f3ac7cf --- /dev/null +++ b/sites/ted/templates/events.html @@ -0,0 +1,21 @@ +{% extends "base.html" %} +{% block content %} +
+

Attend

+

Events

+
+
+ {% for event in events %} +
+ {{ event.name }} + {{ event.city }} - {{ event.month }} - {{ event.track }} +

{{ event.capacity }} seats

+
+ + + +
+
+ {% endfor %} +
+{% endblock %} diff --git a/sites/ted/templates/index.html b/sites/ted/templates/index.html new file mode 100644 index 00000000..07b33a9e --- /dev/null +++ b/sites/ted/templates/index.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} +{% block content %} +
+
+

Newest from TED

+

Ideas change everything

+

Browse talks, save ideas to your account, compare speakers and topics, and register interest in TED events.

+ +
+ {% set lead = featured[0] %} + + {{ lead.speaker }} speaking at {{ lead.event }} + {{ lead.talk_type }} +

{{ lead.title }}

+

{{ lead.speaker }} · {{ lead.published_at|date_label }}

+
+
+ +
+
+

Latest talks

+ See all +
+
+ {% for talk in featured %} + {% include "_talk_card.html" %} + {% endfor %} +
+
+ +
+
+

Popular now

+ +
+
+

Curated playlists

+
+ {% for playlist in playlists %} + + {{ playlist.title }} + {{ playlist.description }} + + {% endfor %} +
+
+
+{% endblock %} diff --git a/sites/ted/templates/login.html b/sites/ted/templates/login.html new file mode 100644 index 00000000..cdeaa96b --- /dev/null +++ b/sites/ted/templates/login.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
+
+ +

Log in

+ + + +
+
+{% endblock %} diff --git a/sites/ted/templates/playlist_detail.html b/sites/ted/templates/playlist_detail.html new file mode 100644 index 00000000..8f4cd2b1 --- /dev/null +++ b/sites/ted/templates/playlist_detail.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} +{% block content %} +
+

Playlist

+

{{ playlist.title }}

+

{{ playlist.description }}

+
+
+
+ {% for link in links %} + {% set talk = link.talk %} + {% include "_talk_card.html" %} + {% endfor %} +
+
+{% endblock %} diff --git a/sites/ted/templates/playlists.html b/sites/ted/templates/playlists.html new file mode 100644 index 00000000..69ddf3b4 --- /dev/null +++ b/sites/ted/templates/playlists.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
+

Watch

+

Playlists

+
+
+ {% for playlist in playlists %} + {{ playlist.title }}{{ playlist.description }} + {% endfor %} +
+{% endblock %} diff --git a/sites/ted/templates/register.html b/sites/ted/templates/register.html new file mode 100644 index 00000000..2b52d7df --- /dev/null +++ b/sites/ted/templates/register.html @@ -0,0 +1,14 @@ +{% extends "base.html" %} +{% block content %} +
+
+ +

Create account

+ + + + + +
+
+{% endblock %} diff --git a/sites/ted/templates/search.html b/sites/ted/templates/search.html new file mode 100644 index 00000000..f8b91a50 --- /dev/null +++ b/sites/ted/templates/search.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% block title %}Search TED{% endblock %} +{% block content %} +
+

Search

+

{% if q %}Results for "{{ q }}"{% else %}Search TED{% endif %}

+
+
+
+ {% for talk in talks %} + {% include "_talk_card.html" %} + {% else %} +

Try a query such as AI, climate, design, science, democracy, or health.

+ {% endfor %} +
+
+{% endblock %} diff --git a/sites/ted/templates/talk_detail.html b/sites/ted/templates/talk_detail.html new file mode 100644 index 00000000..83233623 --- /dev/null +++ b/sites/ted/templates/talk_detail.html @@ -0,0 +1,48 @@ +{% extends "base.html" %} +{% block title %}{{ talk.title }} | TED{% endblock %} +{% block content %} +
+ {{ talk.speaker }} speaking at {{ talk.event }} +
+

{{ talk.event }} - {{ talk.talk_type }}

+

{{ talk.title }}

+

{{ talk.speaker }}

+

{{ talk.description }}

+
+
Duration
{{ talk.minutes }} minutes
+
Published
{{ talk.published_at|date_label }}
+
Views
{{ talk.exact_views_label }}
+
+ {% if saved %} +

This talk is saved to your account.

+ {% else %} +
+ + + + +
+ {% endif %} +
+
+
+
+

Transcript excerpt

+

{{ talk.transcript }}

+

Topics

+
+ {% for topic in talk.topics %}{{ topic }}{% endfor %} +
+
+ +
+{% endblock %} diff --git a/sites/ted/templates/talks.html b/sites/ted/templates/talks.html new file mode 100644 index 00000000..8f710e98 --- /dev/null +++ b/sites/ted/templates/talks.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}TED Talks{% endblock %} +{% block content %} +
+

Watch

+

{% if topic %}Talks about {{ topic }}{% else %}TED Talks{% endif %}

+
+ + + + +
+
+
+
+ {% for talk in talks %} + {% include "_talk_card.html" %} + {% else %} +

No talks matched those filters.

+ {% endfor %} +
+
+{% endblock %} diff --git a/sites/ted/templates/topics.html b/sites/ted/templates/topics.html new file mode 100644 index 00000000..4e2ad258 --- /dev/null +++ b/sites/ted/templates/topics.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% block content %} +
+

Discover

+

Topics

+
+
+ {% for topic, count in counts %} + {{ topic }}{{ count }} talks + {% endfor %} +
+{% endblock %} diff --git a/sites/ted/verify/test_app.py b/sites/ted/verify/test_app.py new file mode 100644 index 00000000..8e23fb31 --- /dev/null +++ b/sites/ted/verify/test_app.py @@ -0,0 +1,66 @@ +"""HTTP-level regression tests for the TED mirror.""" +from __future__ import annotations +import importlib +import os +import re +import shutil +import sqlite3 +import sys +import unittest +from pathlib import Path + +SITE_DIR=Path(__file__).resolve().parents[1] +SEED=SITE_DIR/'instance_seed'/'ted.db' +RUNTIME=SITE_DIR/'instance'/'ted.db' + +def csrf_token(response)->str: + match=re.search(rb'name="csrf_token" value="([^"]+)"',response.data) + if not match:raise AssertionError(f'CSRF token missing from {response.request.path}') + return match.group(1).decode() + +class AppTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + shutil.rmtree(SITE_DIR/'instance',ignore_errors=True);(SITE_DIR/'instance').mkdir();shutil.copy2(SEED,RUNTIME) + os.environ['TED_SECRET_KEY']='test-only-secret' + sys.path.insert(0,str(SITE_DIR));cls.module=importlib.import_module('app');cls.app=cls.module.app;cls.app.config.update(TESTING=True) + @classmethod + def tearDownClass(cls): + with cls.app.app_context():cls.module.db.session.remove() + shutil.rmtree(SITE_DIR/'instance',ignore_errors=True) + def setUp(self):self.client=self.app.test_client() + def login(self,email='alice.j@test.com',next_url=None): + route='/login'+(f'?next={next_url}' if next_url else '');page=self.client.get(route);return self.client.post(route,data={'csrf_token':csrf_token(page),'email':email,'password':'TestPass123!'},follow_redirects=False) + def snapshot(self): + con=sqlite3.connect(RUNTIME) + try: + tables=[r[0] for r in con.execute("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")];return {table:con.execute(f'SELECT * FROM "{table}" ORDER BY rowid').fetchall() for table in tables} + finally:con.close() + def test_all_get_templates_render(self): + paths=['/','/talks','/talks?topic=ai&event=TED2026&max_minutes=20','/search?q=AI','/topics','/playlists','/events','/login','/register','/_health'] + with self.app.app_context(): + paths += [f'/talks/{row.slug}' for row in self.module.Talk.query.all()];paths += [f'/playlists/{row.slug}' for row in self.module.Playlist.query.all()];paths += [f'/topics/{topic}' for topic in self.module.available_topics()] + for route in paths: + with self.subTest(route=route):self.assertEqual(self.client.get(route,follow_redirects=True).status_code,200) + def test_csrf_and_logout_method(self): + self.assertEqual(self.client.get('/logout').status_code,405) + for route in ('/logout','/events','/account','/save/anil-seth-why-ai-is-unlikely-to-become-conscious'): + with self.subTest(route=route):self.assertEqual(self.client.post(route).status_code,400) + def test_external_login_redirect_is_rejected(self): + response=self.login(next_url='//evil.invalid');self.assertEqual(response.status_code,302);self.assertEqual(response.headers['Location'],'/account') + def test_login_page_does_not_disclose_credentials(self): + body=self.client.get('/login').get_data(as_text=True);self.assertNotIn('alice.j@test.com',body);self.assertNotIn('TestPass123!',body) + def test_combined_filters_and_exact_view_counts(self): + body=self.client.get('/talks?topic=ai&event=TED2026&max_minutes=20').get_data(as_text=True);self.assertIn('Peter Steinberger',body);self.assertIn('Anil Seth',body) + detail=self.client.get('/talks/peter-steinberger-how-i-created-openclaw-the-breakthrough-ai-agent').get_data(as_text=True);self.assertIn('551,544',detail) + def test_search_handles_simple_morphology(self): + body=self.client.get('/search?q=architectures').get_data(as_text=True);self.assertIn('Riyad Joucka',body) + def test_oversized_registration_is_rejected(self): + response=self.client.post('/register',data=b'x'*(65*1024),content_type='application/x-www-form-urlencoded');self.assertEqual(response.status_code,413) + def test_get_routes_leave_database_unchanged(self): + before=self.snapshot() + for route in ('/','/talks','/talks?topic=ai&event=TED2026&max_minutes=20','/search?q=AI','/topics','/events'):self.assertEqual(self.client.get(route).status_code,200) + self.assertEqual(before,self.snapshot()) + def test_invalid_registration_is_rejected_without_state_change(self): + before=self.snapshot();page=self.client.get('/register');response=self.client.post('/register',data={'csrf_token':csrf_token(page),'display_name':'','username':'!!!','email':'bad','password':'short'});self.assertEqual(response.status_code,400);self.assertEqual(before,self.snapshot()) +if __name__=='__main__':unittest.main() diff --git a/sites/ted/verify/test_environment_quality.py b/sites/ted/verify/test_environment_quality.py new file mode 100644 index 00000000..a81d8a64 --- /dev/null +++ b/sites/ted/verify/test_environment_quality.py @@ -0,0 +1,53 @@ +"""Regression checks for the reviewed TED mirror.""" +from __future__ import annotations +import hashlib +import json +import shutil +import sqlite3 +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +SITE_DIR=Path(__file__).resolve().parents[1] +ROOT=SITE_DIR.parents[1] +SEED=SITE_DIR/'instance_seed'/'ted.db' + +class EnvironmentQualityTests(unittest.TestCase): + def test_site_registration_and_task_port(self): + startup=(ROOT/'websyn_start.sh').read_text();control=(ROOT/'control_server.py').read_text();docker=(ROOT/'Dockerfile').read_text() + self.assertIn('target ted)',startup);self.assertIn("'target', 'ted'",control);self.assertIn('40000-40019',docker) + rows=[json.loads(line) for line in (SITE_DIR/'tasks.jsonl').read_text().splitlines() if line.strip()];self.assertEqual(len(rows),20) + for i,row in enumerate(rows):self.assertEqual(row['id'],f'TED--{i}');self.assertEqual(row['web'],'http://localhost:40019/');self.assertEqual(row['verifier_path'],f'sites/ted/verify/verify_{i}.py');self.assertNotIn('answer',row) + def test_asset_pin_points_to_merged_ted_revision(self): + revision=next(line.split(':',1)[1].strip() for line in (ROOT/'.assets-revision').read_text().splitlines() if line.startswith('revision:')) + script=(ROOT/'scripts/fetch_assets.sh').read_text();self.assertEqual(revision,'480c892e976bada6c0ea3f5a66e2b9efda65525d');self.assertNotIn('TED_ASSETS_REVISION',script);self.assertTrue(SEED.is_file()) + def test_seed_ground_truth(self): + con=sqlite3.connect(SEED) + try: + self.assertEqual(con.execute('select count(*) from talk').fetchone()[0],64);self.assertEqual(con.execute('select count(*) from user').fetchone()[0],4) + facts={r[0]:(r[1],r[2],r[3]) for r in con.execute("select slug,duration_seconds,views,event from talk where slug in ('anil-seth-why-ai-is-unlikely-to-become-conscious','peter-steinberger-how-i-created-openclaw-the-breakthrough-ai-agent','nayeema-raza-3-habits-to-practice-curiosity-and-escape-your-phone','kate-canales-the-accidental-brilliance-of-makeshift-signs')")} + self.assertEqual(facts['anil-seth-why-ai-is-unlikely-to-become-conscious'],(897,191682,'TED2026'));self.assertEqual(facts['peter-steinberger-how-i-created-openclaw-the-breakthrough-ai-agent'],(1055,551544,'TED2026'));self.assertEqual(551544-191682,359862);self.assertEqual(554563-203431,351132) + finally:con.close() + def test_migration_is_idempotent(self): + with tempfile.TemporaryDirectory(prefix='ted-migration-') as temp: + database=Path(temp)/'ted.db';shutil.copy2(SEED,database);con=sqlite3.connect(database) + try: + con.execute('DROP INDEX uq_saved_talk_user_talk');con.execute('DROP INDEX uq_registration_user_event');con.commit() + finally:con.close() + command=[sys.executable,str(SITE_DIR/'migrate_seed.py'),str(database)];first=subprocess.run(command,capture_output=True,text=True,check=True);first_hash=hashlib.sha256(database.read_bytes()).hexdigest();second=subprocess.run(command,capture_output=True,text=True,check=True);second_hash=hashlib.sha256(database.read_bytes()).hexdigest();self.assertIn('2 indexes created',first.stdout);self.assertIn('0 indexes created',second.stdout);self.assertEqual(first_hash,second_hash) + def test_post_forms_have_csrf_tokens(self): + missing=[] + for template in (SITE_DIR/'templates').glob('*.html'): + lines=template.read_text().splitlines() + for i,line in enumerate(lines): + if ' bool: + return str(value).casefold() in {"1", "true", "yes", "on"} + + +def parse_args() -> VerifyArgs: + parser = argparse.ArgumentParser() + parser.add_argument("--run_dir", required=True) + parser.add_argument("--initial_db") + parser.add_argument("--after_db") + parser.add_argument("--container", default=DEFAULT_CONTAINER) + parser.add_argument("--no_llm", nargs="?", const=True, default=False, type=_bool_value) + args = parser.parse_args() + run_dir = Path(args.run_dir) + initial_snapshot = run_dir / "initial.db" + after_snapshot = run_dir / "after.db" + return VerifyArgs( + run_dir=args.run_dir, + initial_db=args.initial_db or (str(initial_snapshot) if initial_snapshot.is_file() else None), + after_db=args.after_db or (str(after_snapshot) if after_snapshot.is_file() else None), + container=args.container, + no_llm=bool(args.no_llm), + ) + + +def load_run(run_dir: str | os.PathLike[str]) -> dict[str, Any]: + trajectory = json.loads((Path(run_dir) / "trajectory.json").read_text(encoding="utf-8")) + if not isinstance(trajectory, dict): + raise ValueError("trajectory.json must contain a JSON object") + return trajectory + + +def normalize_text(value: Any) -> str: + text = unicodedata.normalize("NFKC", str(value or "")) + text = text.replace("’", "'").replace("“", '"').replace("”", '"').replace("–", "-").replace("—", "-") + return re.sub(r"\s+", " ", text).strip().casefold() + + +def final_answer(trajectory: dict[str, Any]) -> str: + return str(trajectory.get("final_answer") or "").strip() + + +def trajectory_urls(trajectory: dict[str, Any]) -> list[str]: + urls: list[str] = [] + if trajectory.get("start_url"): + urls.append(str(trajectory["start_url"])) + for step in trajectory.get("steps") or []: + if not isinstance(step, dict): + continue + for key in ("url_before", "url", "url_after"): + value = str(step.get(key) or "") + if value and (not urls or value != urls[-1]): + urls.append(value) + final_url = str(trajectory.get("final_url") or "") + if final_url and (not urls or final_url != urls[-1]): + urls.append(final_url) + return urls + + +def _loopback(hostname: str) -> bool: + if hostname.casefold() == "localhost": + return True + try: + return ipaddress.ip_address(hostname).is_loopback + except ValueError: + return False + + +def is_site_url(url: str, trajectory: dict[str, Any]) -> bool: + parsed = urlparse(str(url or "")) + start = urlparse(str(trajectory.get("start_url") or "")) + return bool( + parsed.scheme in {"http", "https"} + and parsed.hostname + and start.hostname + and _loopback(parsed.hostname) + and _loopback(start.hostname) + and parsed.scheme == start.scheme + and parsed.port == start.port + ) + + +def normalized_path(url: str) -> str: + path = urlparse(str(url or "")).path or "/" + return path.rstrip("/") or "/" + + +def query_matches(url: str, expected: dict[str, str]) -> bool: + params = parse_qs(urlparse(url).query) + return all(normalize_text((params.get(key) or [""])[0]) == normalize_text(value) for key, value in expected.items()) + + +def visited_path(trajectory: dict[str, Any], path: str) -> bool: + expected = normalized_path(path) + return any(is_site_url(url, trajectory) and normalized_path(url) == expected for url in trajectory_urls(trajectory)) + + +def visited_query(trajectory: dict[str, Any], path: str, expected: dict[str, str]) -> bool: + return any( + is_site_url(url, trajectory) + and normalized_path(url) == normalized_path(path) + and query_matches(url, expected) + for url in trajectory_urls(trajectory) + ) + + +def visited_in_order(trajectory: dict[str, Any], requirements: list[tuple[str, dict[str, str]]]) -> bool: + urls = trajectory_urls(trajectory) + cursor = 0 + for path, query in requirements: + found = False + for index in range(cursor, len(urls)): + url = urls[index] + if is_site_url(url, trajectory) and normalized_path(url) == normalized_path(path) and query_matches(url, query): + cursor = index + 1 + found = True + break + if not found: + return False + return True + + +def transition_pairs(trajectory: dict[str, Any]): + steps = trajectory.get("steps") or [] + for index, step in enumerate(steps): + if not isinstance(step, dict): + continue + current = str(step.get("url") or step.get("url_before") or "") + if not is_site_url(current, trajectory): + continue + following = str(step.get("url_after") or "") + if not following and index + 1 < len(steps) and isinstance(steps[index + 1], dict): + following = str(steps[index + 1].get("url") or steps[index + 1].get("url_after") or "") + if following and is_site_url(following, trajectory): + yield normalize_text(step.get("action")), current, following + + +def clicked_transition(trajectory: dict[str, Any], from_path: str, to_path: str) -> bool: + return any( + action == "click" + and normalized_path(current) == normalized_path(from_path) + and normalized_path(following) == normalized_path(to_path) + for action, current, following in transition_pairs(trajectory) + ) + + +def submitted_from_path(trajectory: dict[str, Any], path: str, destination: str | None = None) -> bool: + for action, current, following in transition_pairs(trajectory): + if action != "click" or normalized_path(current) != normalized_path(path): + continue + if destination is None or normalized_path(following) == normalized_path(destination): + return True + return False + + +def input_values(trajectory: dict[str, Any], path: str | None = None) -> list[str]: + values: list[str] = [] + for step in trajectory.get("steps") or []: + if not isinstance(step, dict) or normalize_text(step.get("action")) not in {"input", "fill", "type", "select"}: + continue + url = str(step.get("url") or step.get("url_before") or "") + if not is_site_url(url, trajectory) or (path and normalized_path(url) != normalized_path(path)): + continue + params = step.get("params") or {} + value = params.get("text", params.get("value", params.get("option", params.get("label")))) if isinstance(params, dict) else None + if value is not None: + values.append(str(value)) + return values + + +def entered_text(trajectory: dict[str, Any], expected: str, path: str | None = None) -> bool: + expected_value = normalize_text(expected) + return any(normalize_text(value) == expected_value for value in input_values(trajectory, path)) + + +def last_entered_email(trajectory: dict[str, Any], path: str = "/login") -> str: + emails = [normalize_text(value) for value in input_values(trajectory, path) if re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", value.strip())] + return emails[-1] if emails else "" + + +def login_submitted_as(trajectory: dict[str, Any], email: str) -> bool: + return visited_path(trajectory, "/login") and last_entered_email(trajectory) == normalize_text(email) and submitted_from_path(trajectory, "/login") + + +NEGATIONS = {"not", "no", "never", "without", "isn't", "isnt", "wasn't", "wasnt", "doesn't", "doesnt", "didn't", "didnt"} + + +def _negated_before(text: str, start: int) -> bool: + clause = re.split(r"[.!?;:\n]+|\b(?:and|but|however|instead)\b", text[:start])[-1] + words = re.findall(r"[a-z0-9]+(?:'[a-z]+)?", clause) + return any(word in NEGATIONS for word in words) + + +def _negated_after(text: str, end: int) -> bool: + suffix = re.sub(r"^\s*[-,:;!?]*\s*", "", text[end:]) + return re.match(r"(?:(?:is|was|does|did|are|were)\s+)?(?:not|never|no)\b|(?:isn't|isnt|wasn't|wasnt|doesn't|doesnt|didn't|didnt|aren't|arent|weren't|werent)\b", suffix) is not None + + +def affirmative_contains(text: Any, expected: Any) -> bool: + normalized = normalize_text(text) + needle = normalize_text(expected) + matches = list(re.finditer(re.escape(needle), normalized)) + if not needle or not matches: + return False + match = matches[-1] + return not _negated_before(normalized, match.start()) and not _negated_after(normalized, match.end()) + + +def contains_all(text: Any, expected: Iterable[Any]) -> bool: + return all(affirmative_contains(text, value) for value in expected) + + +def contains_any(text: Any, expected: Iterable[Any]) -> bool: + return any(affirmative_contains(text, value) for value in expected) + + +def number_matches(text: Any, value: int | float, tolerance: float = 0.001) -> list[re.Match[str]]: + normalized = normalize_text(text) + matches = [] + for match in re.finditer(r"(? bool: + return bool(number_matches(text, value)) + + +def number_bound_to(text: Any, value: int | float, labels: Sequence[str], distance: int = 140) -> bool: + normalized = normalize_text(text) + for match in number_matches(normalized, value): + window = normalized[max(0, match.start() - distance):min(len(normalized), match.end() + distance)] + if any(normalize_text(label) in window for label in labels): + return True + return False + + +def number_bound_in_comparison(text: Any, value: int | float, labels: Sequence[str]) -> bool: + normalized = normalize_text(text) + segments = re.split(r"\b(?:versus|vs\.?|while|compared (?:with|to))\b|[;\n]", normalized) + return any(has_number(segment, value) and any(normalize_text(label) in segment for label in labels) for segment in segments) + + +def fetch_db(container: str, kind: str) -> str: + if kind not in {"instance", "instance_seed"}: + raise ValueError(f"unsupported database kind: {kind}") + handle, destination = tempfile.mkstemp(prefix=f"ted_{kind}_", suffix=".db") + os.close(handle) + source = f"{container}:/opt/WebSyn/{SITE}/{kind}/{SITE}.db" + result = subprocess.run(["docker", "cp", source, destination], capture_output=True, text=True, check=False) + if result.returncode: + Path(destination).unlink(missing_ok=True) + raise RuntimeError(result.stderr.strip() or f"could not copy {source}") + return destination + + +def resolve_db(explicit: str | None, container: str, kind: str) -> str | None: + if explicit: + return explicit if Path(explicit).is_file() else None + try: + return fetch_db(container, kind) + except (OSError, RuntimeError): + return None + + +def db_query(path: str, sql: str, params: Sequence[Any] = ()) -> list[sqlite3.Row]: + connection = sqlite3.connect(path) + connection.row_factory = sqlite3.Row + try: + return connection.execute(sql, params).fetchall() + finally: + connection.close() + + +def row_dicts(path: str, sql: str, params: Sequence[Any] = ()) -> list[dict[str, Any]]: + return [dict(row) for row in db_query(path, sql, params)] + + +def database_tables(path: str) -> list[str]: + return [str(row["name"]) for row in db_query(path, "SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name")] + + +def table_snapshot(path: str, table: str) -> list[tuple[Any, ...]]: + return [tuple(row) for row in db_query(path, f'SELECT * FROM "{table}" ORDER BY rowid')] + + +def changed_tables(initial_db: str, after_db: str) -> set[str]: + initial_tables = database_tables(initial_db) + if initial_tables != database_tables(after_db): + return {""} + return {table for table in initial_tables if table_snapshot(initial_db, table) != table_snapshot(after_db, table)} + + +def database_unchanged(initial_db: str | None, after_db: str | None) -> bool: + return bool(initial_db and after_db and not changed_tables(initial_db, after_db)) + + +def saved_snapshot(path: str, email: str) -> list[dict[str, Any]]: + return row_dicts(path, "SELECT s.id,t.slug,t.title,t.topics_json,s.note,s.saved_at FROM saved_talk s JOIN user u ON u.id=s.user_id JOIN talk t ON t.id=s.talk_id WHERE lower(u.email)=lower(?) ORDER BY s.id", (email,)) + + +def registration_snapshot(path: str, email: str) -> list[dict[str, Any]]: + return row_dicts(path, "SELECT r.id,e.slug,e.name,r.status FROM registration r JOIN user u ON u.id=r.user_id JOIN event e ON e.id=r.event_id WHERE lower(u.email)=lower(?) ORDER BY r.id", (email,)) + + +def user_snapshot(path: str) -> list[dict[str, Any]]: + return row_dicts(path, "SELECT * FROM user ORDER BY id") + + +def check_common(judge: "Judge", trajectory: dict[str, Any], task_id: str) -> None: + judge.check("task_id_matches", str(trajectory.get("task_id") or "") == task_id, f"observed={trajectory.get('task_id')!r}") + judge.check("final_answer_nonempty", bool(final_answer(trajectory)), repr(final_answer(trajectory))) + judge.check("start_url_is_site", is_site_url(str(trajectory.get("start_url") or ""), trajectory), f"start_url={trajectory.get('start_url')!r}") + + +def check_read_only(judge: "Judge", args: VerifyArgs) -> tuple[str | None, str | None]: + initial = resolve_db(args.initial_db, args.container, "instance_seed") + after = resolve_db(args.after_db, args.container, "instance") + judge.check("databases_readable", bool(initial and after), f"initial={initial} after={after}") + judge.check("read_only_database_unchanged", database_unchanged(initial, after), "complete database comparison") + return initial, after + + +class Judge: + def __init__(self, task_id: str, no_llm: bool = False): + self.task_id = task_id + self.passed = True + self.reason = "" + self.evidence: list[str] = [] + + def check(self, name: str, condition: bool, evidence: str = "", llm: bool = False) -> bool: + self.evidence.append(f"[{'PASS' if condition else 'FAIL'}] {name}: {evidence}") + if not condition: + self.passed = False + if not self.reason: + self.reason = name + return bool(condition) + + def emit(self) -> None: + print(json.dumps({"task_id": self.task_id, "pass": self.passed, "reason": self.reason, "evidence": self.evidence}, ensure_ascii=False, indent=2)) + raise SystemExit(0 if self.passed else 1) diff --git a/websyn_start.sh b/websyn_start.sh index c347ccec..4b29a5c9 100644 --- a/websyn_start.sh +++ b/websyn_start.sh @@ -5,7 +5,7 @@ set -e SITES=(allrecipes amazon apple arxiv bbc_news booking github google_flights google_map google_search huggingface wolfram_alpha - cambridge_dictionary coursera espn merriam_webster ikea phys_org target) + cambridge_dictionary coursera espn merriam_webster ikea phys_org target ted) BASE_PORT=40000 PID_DIR=/tmp/websyn_pids mkdir -p "$PID_DIR"