;;; emacsconf-publish.el --- Publishing -*- lexical-binding: t; -*-
;; Copyright (C) 2021 Sacha Chua
;; Author: Sacha Chua
;; Keywords: multimedia
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see .
;;; Commentary:
;;
;;; Code:
(require 'emacsconf-schedule)
(defcustom emacsconf-media-base-url "https://media.emacsconf.org/" "Base URL for published media files."
:type 'string
:group 'emacsconf)
(defcustom emacsconf-main-extensions '("--main.webm" "--main.opus" "--main.org" ".org" ".odp" ".pdf" ".pptx" ".el" "--compressed56.webm" "--main.vtt" "--main_fr.vtt" "--main_ja.vtt" "--main_es.vtt" "--main--chapters.vtt" "--script.fountain" "--main.pdf" "--slides.pdf")
"Extensions to list on public pages."
:type '(repeat string)
:group 'emacsconf)
(defcustom emacsconf-publish-backstage-extensions '(".en.srv2" ".srt" "--original.mp4")
"Extensions to list in the staging area."
:group 'emacsconf)
(defcustom emacsconf-public-media-directory (concat "/ssh:orga@media.emacsconf.org:/var/www/media.emacsconf.org/" emacsconf-year "/")
"Can be over TRAMP" :type 'string :group 'emacsconf)
(defun emacsconf-publish-info-pages-for-talk (talk)
"Publish the before and after pages for this talk."
(interactive (list (emacsconf-complete-talk-info)))
(let ((info (emacsconf-get-talk-info)))
(emacsconf-publish-before-page talk info)
(emacsconf-publish-after-page talk info)))
(defun emacsconf-publish-update-talk (talk)
"Publish the schedule page and the page for this talk."
(interactive (list (emacsconf-complete-talk-info)))
(when (stringp talk) (setq talk (emacsconf-resolve-talk talk)))
(when (functionp 'emacsconf-upcoming-insert-or-update)
(emacsconf-upcoming-insert-or-update))
(emacsconf-publish-with-wiki-change
(let ((info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
(emacsconf-publish-before-page talk info)
(emacsconf-publish-after-page talk info)
(emacsconf-publish-schedule info))))
(defun emacsconf-publish-add-talk ()
"Add the current talk to the wiki."
(interactive)
(emacsconf-publish-talk-page (emacsconf-get-talk-info-for-subtree))
(emacsconf-publish-info-pages)
(magit-status-setup-buffer emacsconf-directory))
(defun emacsconf-publish-update-conf-html ()
"Update the schedules and export the page so I can easily review it."
(interactive)
(cl-letf* ((new-org (>= (string-to-number (org-version)) 9.5))
;; Fix bug probably introduced by org 9.5, but not investigated
;; thoroughly.
((symbol-function 'org-src-mode--maybe-disable-indent-tabs-mode)
(eval `(lambda ()
(when (or ,(when new-org
'(not org-src--tab-width))
(= org-src--tab-width 0))
(setq indent-tabs-mode nil))))))
(let ((org-confirm-babel-evaluate (or (null emacsconf-allow-dangerous-stuff)
org-confirm-babel-evaluate)))
(org-update-all-dblocks)
(org-babel-execute-buffer)
(org-html-export-to-html))))
(defun emacsconf-publish-regenerate-wiki (&optional force)
(interactive)
(when
(let ((info (emacsconf-get-talk-info))
(force (or force (yes-or-no-p "Overwrite existing talk pages? "))))
(emacsconf-publish-info-pages info)
(emacsconf-publish-schedule info)
(magit-status emacsconf-directory))))
(declare-function 'emacsconf-ical-generate-all "emacsconf-ical")
(defun emacsconf-update-schedule ()
"Change times for talks."
(interactive)
(emacsconf-publish-with-wiki-change
(emacsconf-publish-info-pages)
(emacsconf-publish-schedule)
(emacsconf-ical-generate-all)
(emacsconf-publish-schedule-org-files)
(emacsconf-publish-watch-pages)
(when (functionp 'emacsconf-pentabarf-generate)
(emacsconf-pentabarf-generate))))
(defun emacsconf-update-and-publish ()
(interactive)
(with-current-buffer (find-file-noselect emacsconf-org-file)
(emacsconf-update-schedules)
(emacsconf-upcoming-update-file)
(emacsconf-update-schedules-in-wiki)
(emacsconf-publish-update-conf-html)
(setq emacsconf-info (emacsconf-get-talk-info))))
(defun emacsconf-publish-update-media ()
"Update public media.emacsconf.org directory."
(interactive)
(emacsconf-publish-public-index-on-wiki)
(when emacsconf-public-media-directory
(emacsconf-publish-public-index (expand-file-name "index.html" emacsconf-public-media-directory))
(emacsconf-publish-playlist (expand-file-name "index.m3u" emacsconf-public-media-directory)
(concat emacsconf-name emacsconf-year)
(emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))
(format "https://media.emacsconf.org/%s/" emacsconf-year)))
(when emacsconf-backstage-dir
(emacsconf-publish-backstage-index (expand-file-name "index.html" emacsconf-backstage-dir)))
(emacsconf-publish-playlist (expand-file-name "index.m3u" emacsconf-backstage-dir)
(concat emacsconf-name emacsconf-year)
(emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))
(format "https://media.emacsconf.org/%s/backstage/" emacsconf-year)))
(defun emacsconf-publish-index-card (talk)
"Format an HTML card for TALK."
(let* ((file-prefix (plist-get talk :file-prefix))
(video-file (plist-get talk :video-file))
(video (and file-prefix
(emacsconf-publish-index-card-video
(or (plist-get talk :video-id)
(concat (plist-get talk :slug) "-mainVideo"))
video-file talk))))
;; Add extra information to the talk
(setq talk
(append
talk
(list
:time-info (emacsconf-surround "Duration: " (plist-get talk :video-duration) " minutes" "")
:video-html (or (plist-get video :video) "")
:audio-html (or (plist-get video :audio) "")
:chapter-list (or (plist-get video :chapter-list) "")
:resources (or (plist-get video :resources) "")
:extra (or (plist-get talk :extra) "")
:speaker-info (or (plist-get talk :speakers-with-pronouns) ""))))
(emacsconf-replace-plist-in-string
talk
"
\n\n"))
(defun emacsconf-publish-before-page (talk &optional info)
"Generate the page that has the info included before the abstract.
This includes the intro note, the schedule, and talk resources."
(interactive (list (emacsconf-complete-talk-info)))
(setq info (or info (emacsconf-publish-prepare-for-display (emacsconf-get-talk-info))))
(with-temp-file (expand-file-name (format "%s-before.md" (plist-get talk :slug))
(expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory)))
(insert "\n")
(insert (emacsconf-surround "" (plist-get talk :intro-note) "\n\n" ""))
(let ((is-live (emacsconf-talk-live-p talk)))
(when is-live (emacsconf-publish-captions-in-wiki talk))
(when (member emacsconf-publishing-phase '(schedule conference))
(insert (emacsconf-publish-format-talk-schedule-image talk info)))
(insert (emacsconf-publish-format-talk-schedule-info talk) "\n\n")
(insert
(if (plist-get talk :public) (emacsconf-wiki-talk-resources talk) "")
"\n# Description\n"))
(insert "")))
(defun emacsconf-format-transcript-from-list (subtitles paragraphs video-id &optional lang)
"Return subtitle directives for SUBTITLES split by PARAGRAPHS."
(when (stringp subtitles) (setq subtitles (subed-parse-file subtitles)))
(when (stringp paragraphs) (setq paragraphs (subed-parse-file paragraphs)))
(mapconcat
(lambda (sub)
(let ((msecs (elt sub 1)))
(format "[[!template %stext=\"\"\"%s\"\"\" start=\"%s\" video=\"%s\" id=\"subtitle\"%s]]"
(if (and paragraphs (>= msecs (elt (car paragraphs) 1)))
(progn
(while (and paragraphs (>= (elt sub 1) (elt (car paragraphs) 1)))
(setq paragraphs (cdr paragraphs)))
"new=\"1\" ")
"")
(replace-regexp-in-string "^#" "\\\\#"
(replace-regexp-in-string "\"" """ (elt sub 3)))
(concat (format-seconds "%02h:%02m:%02s" (/ (floor msecs) 1000))
"." (format "%03d" (mod (floor msecs) 1000)))
video-id
(emacsconf-surround " lang=\"" lang "\"" ""))))
subtitles "\n"))
(defun emacsconf-publish-format-transcript (talk &optional video-id lang)
"Format the transcript for TALK, adding paragraph markers when possible."
(require 'subed)
(let* ((chapters (plist-get talk :chapter-file))
(subtitles
(subed-parse-file (if lang
(format "%s_%s.vtt"
(file-name-sans-extension
(plist-get talk :caption-file))
lang)
(plist-get talk :caption-file))))
(pars (subed-parse-file chapters)))
(if subtitles
(format "
# %s
%s
"
(plist-get talk :slug)
(or video-id "mainVideo")
(emacsconf-surround "-" lang "" "")
(if lang (assoc-default lang emacsconf-publish-subtitle-languages) "Transcript")
(emacsconf-format-transcript-from-list
subtitles pars (concat video-id "-" (plist-get talk :slug))))
"")))
(defun emacsconf-publish-format-captions (talk)
(let ((transcripts
(mapconcat
(lambda (lang)
(let ((filename
(emacsconf-talk-file
talk
(if lang
(format "--main_%s.vtt" lang)
"--main.vtt"))))
(if (emacsconf-captions-edited-p filename) ; todo: cache this somewhere
(emacsconf-publish-format-transcript
(append
(list :chapter-file (emacsconf-talk-file talk "--main--chapters.vtt")
:caption-file (emacsconf-talk-file talk "--main.vtt"))
talk)
"mainVideo" lang)
"")))
(cons nil (mapcar 'car emacsconf-publish-subtitle-languages))
"")))
(if (> (length transcripts) 0)
(concat transcripts
(emacsconf-surround
(if (string-match "[,;]" (or (plist-get talk :captioner) ""))
"\n\nCaptioners: "
"\n\nCaptioner: ")
(plist-get talk :captioner)
"\n\n"))
"")))
(defun emacsconf-publish-after-page (talk &optional info)
"Generate the page with info included after the abstract.
This includes captions, contact, and an invitation to participate."
(interactive (list (emacsconf-complete-talk-info)))
;; Contact information
(with-temp-file (expand-file-name (format "%s-after.md" (plist-get talk :slug))
(expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory)))
(insert
"\n"
"\n\n"
;; main transcript
(if (plist-get talk :public) (emacsconf-publish-format-captions talk) "")
(emacsconf-publish-format-email-questions-and-comments talk) "\n"
(if (eq emacsconf-publishing-phase 'cfp)
(format "\n----\nGot an idea for an EmacsConf talk or session? We'd love to hear from you! Check out the [[Call for Participation|/%s/cfp]] for details.\n" emacsconf-year)
"")
"\n\n\n")))
(defun emacsconf-sort-by-track-then-schedule (a b)
;; Gen,Dev; then by time
(cond
((and (plist-get a :speakers)
(not (plist-get b :speakers))) t)
((and (plist-get b :speakers)
(not (plist-get a :speakers))) nil)
((string< (plist-get a :track)
(plist-get b :track)) nil)
((string< (plist-get b :track)
(plist-get a :track)) t)
((time-less-p (plist-get a :start-time)
(plist-get b :start-time)) t)
(t nil)))
(defun emacsconf-publish-nav-pages (&optional talks)
"Generate links to the next and previous talks.
During the schedule and conference phase, the talks are sorted by time.
Otherwise, they're sorted by track and then schedule."
(interactive (list (emacsconf-publish-prepare-for-display (or emacsconf-schedule-draft (emacsconf-get-talk-info)))))
(let* ((next-talks (cdr talks))
(prev-talks (cons nil talks))
(label (if (member emacsconf-publishing-phase '(schedule conference))
"time"
"track")))
(unless (file-directory-p (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory)))
(mkdir (expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory))))
(while talks
(let* ((o (pop talks))
(next-talk (emacsconf-format-talk-link (pop next-talks)))
(prev-talk (emacsconf-format-talk-link (pop prev-talks))))
(with-temp-file (expand-file-name (format "%s-nav.md" (plist-get o :slug))
(expand-file-name "info" (expand-file-name emacsconf-year emacsconf-directory)))
(insert (concat "\n
Back to the [[talks]] \n"
(if prev-talk (format "Previous by %s: %s \n" label prev-talk) "")
(if next-talk (format "Next by %s: %s \n" label next-talk) "")
(if (plist-get o :track) ; tagging doesn't work here because ikiwiki will list the nav page
(format "Track: %s \n" (plist-get o :track) (plist-get o :track))
"")
"
")))))))
(defun emacsconf-publish-prepare-for-display (&optional talks)
"Return the sequence of TALKS to publish."
(seq-filter 'emacsconf-publish-talk-p
(sort (copy-sequence (or talks emacsconf-schedule-draft (emacsconf-filter-talks (emacsconf-get-talk-info))))
(if (member emacsconf-publishing-phase '(schedule conference))
#'emacsconf-sort-by-scheduled
#'emacsconf-sort-by-track-then-schedule))))
(defun emacsconf-publish-info-pages (&optional info)
"Populate year/info/*-nav, -before, and -after files."
(interactive (list nil))
(setq info (or info (emacsconf-publish-prepare-for-display info)))
(emacsconf-publish-with-wiki-change
(emacsconf-publish-nav-pages info)
(emacsconf-publish-schedule info)
(mapc (lambda (o)
(emacsconf-publish-before-page o info)
(emacsconf-publish-after-page o info))
info)))
(defun emacsconf-publish-before-pages (&optional info)
"Populate -before files."
(interactive)
(setq info (or info (emacsconf-get-talk-info)))
(setq info (seq-remove (lambda (o) (string= (plist-get o :status) "CANCELLED"))
(sort (emacsconf-filter-talks info) #'emacsconf-sort-by-scheduled)))
(emacsconf-publish-with-wiki-change
(mapc (lambda (o)
(emacsconf-publish-before-page o info))
info)))
(defun emacsconf-publish-talks-page (&optional emacsconf-info)
"Generate a list of talks."
(interactive)
(let ((info (or emacsconf-info
(sort
(seq-filter #'emacsconf-publish-talk-p
(or emacsconf-schedule-draft (emacsconf-get-talk-info)))
#'emacsconf-sort-by-track-then-schedule)))
(range (format "<%s %s-%s>"
emacsconf-date
(plist-get (car emacsconf-tracks) :start)
(plist-get (car emacsconf-tracks) :end))))
(with-temp-file (expand-file-name "talk-details.md" (expand-file-name emacsconf-year emacsconf-directory))
))
(defun emacsconf-generate-main-schedule-with-tracks (&optional info)
(interactive (list nil))
(setq info (or info (emacsconf-publish-prepare-for-display info)))
(with-temp-file (expand-file-name "schedule-details.md"
(expand-file-name emacsconf-year emacsconf-directory))
(when (member emacsconf-publishing-phase '(schedule conference))
(insert
(emacsconf-replace-plist-in-string
(list
:timezone emacsconf-timezone
:gmt-offset emacsconf-timezone-offset
:alternative-timezones
(string-join (emacsconf-timezone-strings range nil "~%-l:%M %p")
" / ")
:icals
(concat
(format "%s.ics - "
emacsconf-media-base-url
emacsconf-year
emacsconf-id)
(mapconcat (lambda (track)
(format "%s-%s.ics"
emacsconf-media-base-url
emacsconf-year
emacsconf-id
(plist-get track :id)))
emacsconf-tracks " - "))
:schedule-directory
(concat emacsconf-media-base-url emacsconf-year "/schedules/"))
"The conference is from ${alternative-timezones}.
Times below are in %{timezone} (GMT${gmt-offset}). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting.
You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local timezone. Alternatively, you can use these timezone-translated Org files: <${schedule-directory}>")))
;; By track
(let* ((by-day (mapcar (lambda (o))
(seq-group-by (lambda (o)
(format-time-string "%Y-%m-%d" nil emacsconf-timezone))
info)))
(links
(mapconcat (lambda (track)
(concat (cadr track) ": "
(mapconcat (lambda (sec)
(format "%s"
(car track) (car sec) (cadr sec)))
(mapcar (lambda (day)
(list (downcase (format-time-string "%a" (date-to-time (car day))))
(format-time-string "%A" (date-to-time (car day)))))
by-day)
" - ")))
(mapcar (lambda (o) (list (plist-get o :id)
(plist-get o :name)))
emacsconf-tracks)
" | ")))
(insert
(mapconcat
(lambda (track)
(let* ((id (elt track 0))
(label (elt track 1))
(start (elt track 2))
(end (elt track 3)))
(format "\n\n\n
## %s\n\n%s\n
\n"
id
label
label
(mapconcat
(lambda (section)
(let ((section-id (elt section 0))
(section-label (elt section 1))
(section-seq (caddr section)))
(format "%s\n\n\n### %s track - %s\n%s\n"
links
id section-id
label
section-label
(emacsconf-publish-format-main-schedule
section-seq))))
`(("sat" "Saturday, Dec 3" ,sat)
("sun" "Sunday, Dec 4" ,sun))
"\n\n")
))
()
)
'(("gen" "General" "^GEN Sat" "^DEV Sat")
("dev" "Development" "^DEV Sat"))
"\n")))
(let ((by-day (sort (seq-remove (lambda (s) (string-match "^\\(GEN\\|DEV\\)" (plist-get s :title)))
info)
#'emacsconf-sort-by-scheduled)))
(insert
"\n\n\n# By day\n\n## Saturday\n"
;; Everything all together
(emacsconf-publish-format-main-schedule
(emacsconf-schedule-get-subsequence
by-day
"Saturday opening remarks"
"Saturday closing remarks"))
"\n\n## Sunday\n"
;; Everything all together
(emacsconf-publish-format-main-schedule
(emacsconf-schedule-get-subsequence
by-day
"Sunday opening remarks"
"Sunday closing remarks"))
))))
(defun emacsconf-publish-format-interleaved-schedule (&optional info)
"Return a list with the schedule for INFO.
Entries are sorted chronologically, with different tracks interleaved."
(setq info (or info (emacsconf-get-talk-info)))
(let* ((by-day (emacsconf-by-day (emacsconf-publish-prepare-for-display info)))
(cancelled (seq-filter (lambda (o) (string= (plist-get o :status) "CANCELLED")) info))
(dates (seq-map (lambda (o) (plist-get (cadr o) :start-time)) by-day))
(links (mapcar (lambda (o)
(format "%s"
(format-time-string "%Y-%m-%d" o emacsconf-timezone)
(format-time-string "%a %b %-e" o emacsconf-timezone)))
dates))
(height 150)
(width 600))
(concat
(mapconcat (lambda (day)
(let ((day-start (date-to-time
(concat (format-time-string "%Y-%m-%dT" (plist-get (cadr day) :start-time) emacsconf-timezone)
emacsconf-schedule-start-time
emacsconf-timezone-offset)))
(day-end (date-to-time (concat (format-time-string "%Y-%m-%dT" (plist-get (cadr day) :start-time) emacsconf-timezone)
emacsconf-schedule-end-time
emacsconf-timezone-offset))))
(concat
(if (> (length links) 1) (concat "Jump to: " (string-join links " - ")) "")
(format "\n"
(format-time-string "%Y-%m-%d"
(plist-get (cadr day) :start-time)
emacsconf-timezone))
(format-time-string "# %A %b %-e, %Y\n" (plist-get (cadr day) :start-time) emacsconf-timezone)
(format "[[!inline pages=\"internal(%s/schedule-%s)\" raw=\"yes\"]]" emacsconf-year (car day))
"\n\n"
(format "
"
(plist-get talk :title)
(plist-get talk :speakers)))
cancelled "\n"))
"")
)))
(defun emacsconf-publish-schedule (&optional info)
"Generate the schedule or program."
(interactive)
(emacsconf-publish-schedule-svg-snippets)
(setq info (or info (emacsconf-publish-prepare-for-display info)))
(with-temp-file (expand-file-name "schedule-details.md"
(expand-file-name emacsconf-year emacsconf-directory))
(when (member emacsconf-publishing-phase '(schedule conference))
(insert
(emacsconf-replace-plist-in-string
(list
:timezone emacsconf-timezone
:gmt-offset emacsconf-timezone-offset
:alternative-timezones
(string-join (emacsconf-timezone-strings range nil "~%-l:%M %p")
" / ")
:icals
(concat
(format "%s.ics - "
emacsconf-media-base-url
emacsconf-year
emacsconf-id
emacsconf-id)
(mapconcat (lambda (track)
(format "%s-%s.ics"
emacsconf-media-base-url
emacsconf-year
emacsconf-id
(plist-get track :id)
emacsconf-id
(plist-get track :id)))
emacsconf-tracks " - "))
:schedule-directory
(concat emacsconf-media-base-url emacsconf-year "/schedules/"))
"The conference is from ${alternative-timezones}.
Times below are in ${timezone} (GMT${gmt-offset}). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting.
You can also get this schedule as iCalendar files: ${icals}. Importing that into your calendar should translate things into your local timezone. Alternatively, you can use these timezone-translated Org files: <${schedule-directory}>
")))
(insert
(if (member emacsconf-publishing-phase '(cfp program))
(let ((sorted (emacsconf-publish-prepare-for-display (or info (emacsconf-get-talk-info)))))
(mapconcat
(lambda (track)
(concat
"Jump to: "
;; links to other tracks
(string-join (seq-keep (lambda (track-link)
(unless (string= (plist-get track-link :id)
(plist-get track :id))
(format "%s"
(plist-get track-link :id)
(plist-get track-link :name))))
emacsconf-tracks)
" | ")
"\n\n"
(let ((track-talks (seq-filter (lambda (o) (string= (plist-get o :track)
(plist-get track :name)))
sorted)))
(format
"
"
(length talks)
(length captioned)
(apply '+ (mapcar (lambda (info) (string-to-number (plist-get info :time))) captioned))
(length received)
(apply '+ (mapcar (lambda (info) (string-to-number (plist-get info :time)))
received)))))
(defun emacsconf-publish-sched-directive (o)
"Format the schedule directive with info for O."
(format "[[!template id=sched%s]]"
(let ((result "")
(attrs (append
(pcase emacsconf-publishing-phase
('program
(list
:time (plist-get o :time)))
((or 'schedule 'conference)
(list
:status (pcase (plist-get o :status)
("CAPTIONED" "captioned")
("PREREC_RECEIVED" "received")
("DONE" "done")
("STARTED" "now playing")
(_ nil))
:time (plist-get o :time)
:q-and-a (plist-get o :qa-link)
:note (plist-get o :sched-note)
:pad (and emacsconf-publish-include-pads (plist-get o :pad-url))
:startutc (format-time-string "%FT%T%z" (plist-get o :start-time) t)
:endutc (format-time-string "%FT%T%z" (plist-get o :end-time) t)
:start (format-time-string "%-l:%M" (plist-get o :start-time) emacsconf-timezone)
:end (format-time-string "%-l:%M" (plist-get o :end-time) emacsconf-timezone)))
('resources
(list
:pad nil
:channel nil
:resources (mapconcat (lambda (s) (concat "
" s "
"))
(emacsconf-publish-link-file-formats-as-list
(append o
(list :base-url (format "%s%s/" emacsconf-media-base-url emacsconf-year))))
""))))
(list
:title (plist-get o :title)
:url (concat "/" (plist-get o :url))
:speakers (plist-get o :speakers)
:track (if (member emacsconf-publishing-phase '(schedule conference)) (plist-get o :track))
:watch (plist-get o :watch-url)
:slug (plist-get o :slug)
:note
(string-join
(delq nil
(list
(when (plist-get o :captions-edited)
"captioned")
(when (and (plist-get o :public)
(or (plist-get o :toobnix-url)
(plist-get o :video-file)))
"video posted")))
", ")
)
)))
(while attrs
(let ((field (pop attrs))
(val (pop attrs)))
(when val
(setq result (concat result " " (substring (symbol-name field) 1) "=\"\"\"" val "\"\"\"")))))
result)))
(defun emacsconf-publish-format-main-schedule (info)
"Include the schedule information for INFO."
(mapconcat #'emacsconf-publish-sched-directive info "\n"))
(defun emacsconf-publish-talk-files (talk &optional files)
"Return a list of files for TALK.
Use FILES as the file list, or look in the cache directory."
(seq-filter (lambda (o)
(string-match (concat "^" (regexp-quote (plist-get talk :file-prefix))) o))
(or files
(directory-files emacsconf-cache-dir))))
(defun emacsconf-sum (field talks)
(apply '+ (seq-map (lambda (talk)
(string-to-number
(or
(if (listp field)
(car
(seq-keep
(lambda (f)
(plist-get talk f))
field))
(plist-get talk field))
"0")))
talks)))
(defun emacsconf-publish-backstage-org-on-state-change (talk)
(save-window-excursion
(emacsconf-with-talk-heading talk
(when (and (member org-state '("PROCESSING" "TO_ASSIGN"))
(not (plist-get talk :video-time)))
(emacsconf-publish-cache-video-data talk))
(when (member org-state '("TO_CAPTION"))
(unless (or noninteractive (org-entry-get (point) "CAPTIONER"))
(org-entry-put (point) "CAPTIONER"
(assoc-default "CUSTOM_ID" (emacsconf-complete-volunteer)))))
(when (member org-state '("WAITING_FOR_PREREC" "TO_ASSIGN" "TO_CAPTION" "TO_STREAM"))
(emacsconf-publish-backstage-index)))))
(defun emacsconf-publish-backstage-to-assign (by-status files)
(emacsconf-publish-backstage-list
(assoc-default "TO_ASSIGN" by-status)
files
"to be captioned, waiting for volunteers"
"
You can e-mail sacha@sachachua.com to call dibs on editing the captions for one of these talks. We use OpenAI Whisper to provide auto-generated VTT that you can use as a starting point, but you can also write the captions from scratch if you like. The starting point for the main video ends in --main.vtt. If you're writing the captions from scratch, you can choose to include timing information, or we can probably figure them out afterwards with a forced alignment tool. More info: captioning tips
"
(lambda (f)
(append
f
(list :extra
(if (plist-get f :caption-note) (concat "
" (plist-get f :caption-note) "
") "")
:files
(emacsconf-publish-talk-files f files))))))
(defun emacsconf-publish-backstage-to-caption (by-status files)
(emacsconf-publish-backstage-list
(assoc-default "TO_CAPTION" by-status)
files
"being captioned"
"People are working on these ones, yay! Captioning process/tips"
(lambda (f)
(append
f
(list :extra
(concat "
"
(emacsconf-surround "Being captioned by " (plist-get f :captioner) " " "")
(emacsconf-surround "Note: " (plist-get f :caption-note) "" "")
"
")
:files
(emacsconf-publish-talk-files f files))))))
(defun emacsconf-publish-backstage-list (talks files header-description &optional description modify-func)
"Display a list of TALKS.
Use the files listed in FILES.
Start the header with HEADER-DESCRIPTION.
If MODIFY-FUNC is specified, use it to modify the talk."
(format
"
"
(if (string= status "TO_ASSIGN")
"TO_ASSIGN (waiting for captioning volunteers)"
status)
(length (assoc-default status by-status))
(emacsconf-sum '(:video-time :time) (assoc-default status by-status))
(mapconcat (lambda (o) (format "%s%s"
(plist-get o :slug)
(plist-get o :slug)
(emacsconf-surround " (" (plist-get o :video-duration) ")" "")))
(assoc-default status by-status)
", ")))
(pcase emacsconf-backstage-phase
('prerec '("WAITING_FOR_PREREC" "PROCESSING" "TO_ASSIGN" "TO_CAPTION" "TO_CHECK" "TO_STREAM"))
('harvest '("TO_ARCHIVE" "TO_REVIEW_QA" "TO_INDEX_QA" "TO_CAPTION_QA")))
"")
"
"
;; alphabetical index
"
Alphabetical index: "
(mapconcat (lambda (o)
(format "%s"
(plist-get o :slug)
(plist-get o :slug)))
(sort talks (lambda (a b) (string< (plist-get a :slug) (plist-get b :slug))))
", ")
"
"
(pcase emacsconf-backstage-phase
('prerec
(concat
(emacsconf-publish-backstage-list
(append
(assoc-default "TO_PROCESS" by-status)
(assoc-default "PROCESSING" by-status)
(assoc-default "TO_AUTOCAP" by-status))
files
"being processed"
"Not ready for captioning yet, but they will be eventually")
(emacsconf-publish-backstage-to-assign by-status files)
(emacsconf-publish-backstage-to-caption by-status files)
(emacsconf-publish-backstage-list
(assoc-default "TO_CHECK" by-status) files
"to be checked"
"These can be checked to see if the subtitle timings are correct, audio/video is fine, etc.")
(emacsconf-publish-backstage-list
(assoc-default "TO_STREAM" by-status)
files
"ready to be streamed")
(emacsconf-publish-backstage-list
(assoc-default "WAITING_FOR_PREREC" by-status) files
"we're waiting for"
"Speakers might submit these, do them live, or cancel the talks.")))
('harvest
(let ((stages
'(("TO_REVIEW_QA" .
"Please review the --bbb-webcams.webm file and/or the --bbb-webcams.vtt and tell us (emacsconf-submit@gnu.org) if a Q&A session can be published or if it needs to be trimmed (lots of silence at the end of the recording, accidentally included sensitive information, etc.).")
("TO_INDEX_QA" .
"Please review the --answers.webm and --answers.vtt files to make chapter markers so that people can jump to specific parts of the Q&A session. The harvesting page on the wiki has some notes on the process. That way, it's easier for people to see the questions and
answers without needing to listen to everything again. You can see asmblox for an example of the Q&A chapter markers.")
("TO_CAPTION_QA" .
"Please edit the --answers.vtt for the Q&A talk you're interested in, correcting misrecognized words and cleaning it up so that it's nice to use as closed captions. All talks should now have large-model VTTs to make it easier to edit."))))
(mapconcat
(lambda (stage)
(let ((status (car stage)))
(format
"
%s: %d talk(s) (%d minutes)
%s%s"
status
(length (assoc-default status by-status))
(emacsconf-sum :video-time (assoc-default status by-status))
(cdr stage)
(mapconcat
(lambda (f)
(format "
Conference info, how to watch/participate: https://emacsconf.org/2021/
Guidelines for conduct: https://emacsconf.org/conduct/
Except where otherwise noted, the material on the EmacsConf pad are dual-licensed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International Public License ; and the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) an later version.
Copies of these two licenses are included in the EmacsConf wiki repository, in the COPYING.GPL and COPYING.CC-BY-SA files (https://emacsconf.org/COPYING/).
By contributing to this pad, you agree to make your contributions available under the above licenses. You are also promising that you are the author of your changes, or that you copied them from a work in the public domain or a work released under a free license that is compatible with the above two licenses. DO NOT SUBMIT COPYRIGHTED WORK WITHOUT PERMISSION.
This pad is here to be curated by everybody and its rough structure is like this:
General info and license
A section for each talk -> please do add questions and notes
A general feedback section
"
(mapconcat
(lambda (o)
(let ((url (format "https://emacsconf.org/%s/talks/%s" emacsconf-year (plist-get o :slug))))
(format "------------------------------------------------------------------------------------------------- Talk%s: %s
Speaker(s): %s
Talk page: %s
Actual start of talk EST:   ; Start of Q&A: End of Q&A: Questions:
Speakers may answer in any order or skip questions. As much as possible, put your questions at the top level instead of under another question. If adding an answer, please indicate [speaker] or your nick accordingly. Volunteers, please add new slots as ones get filled.
Q1:  ;
A:
Q2:  ;
A:
Q3:  ;
A:
Q4:  ;
A:
Links and other notes:
sample text
sample text
sample text
sample text
" (plist-get o :slug) (plist-get o :title) (plist-get o :speakers) url url))) talks "
\n")
"
------------------------------------------------------------------------------------------------- General Feedback: What went well?
sample text
sample text
sample text
sample text
------------------------------------------------------------------------------------------------- General Feedback: What to improve?
For better performance, we recommend watching ${stream-hires} using a streaming media player. Examples:
mpv ${stream-hires}
vlc ${stream-hires}
ffplay ${stream-hires}
If you have limited bandwidth, you can watch the low-res stream ${480p}.
If you don't have a streaming media player, you can watch using the player below.
" (emacsconf-publish-page-nav nav "links") " | ${stream-nav}
"
"
${brief}
" (emacsconf-publish-page-nav nav "chat") " | ${stream-nav}
" (emacsconf-publish-page-nav nav "sched") " | ${stream-nav}
"
"
Legend:
Solid lines: Q&A will be through a BigBlueButton room (you can ask questions there or through IRC/Etherpad)
Dashed lines: Q&A will be over IRC or the Etherpad, or the speaker will follow up afterwards
Times are in Eastern Standard Time (America/Toronto, GMT-5). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting.
Times are in Eastern Standard Time (America/Toronto, GMT-5). If you have Javascript enabled, clicking on talk pages should include times in your computer's local time setting.
Depending on which media player you use, you may enter the stream address
in a graphical user interface or provide it as an argument to the program
when launching it from the terminal.
If you experience any disruptions, try reloading the page you're using
to watch the video. If that still doesn't work, please check our
status page at https://status.emacsconf.org for updates on the
status of various parts of our infrastructure, and instructions on how
to get in touch with us about disruptions.
To participate in the Q&A, please check the talk page for the Q&A
details, including the Etherpad link, IRC channel, and optionally
a BigBlueButton room (BBB) for Q&A. If you plan to participate in
Q&A in the BigBlueButton room, please use headphones or earphones
in order to minimize audio feedback. The link on the talk page
will take you to a waiting room that will automatically refresh
when the host has opened the Q&A.